diff --git a/18-contextualai-chroma/01-contextual-ai-parser-chroma.ipynb b/18-contextualai-chroma/01-contextual-ai-parser-chroma.ipynb new file mode 100644 index 0000000..8c98077 --- /dev/null +++ b/18-contextualai-chroma/01-contextual-ai-parser-chroma.ipynb @@ -0,0 +1,1137 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "PJMV1PUvXA4e" + }, + "source": [ + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ContextualAI/examples/blob/main/18-contextualai-chroma/01-contextual-ai-parser-chroma.ipynb)\n", + "\n", + "# Build Multi-Modal RAG with Chroma and Contextual AI Parser\n", + "\n", + "**Last updated:** November 2025\n", + "\n", + "**Versions used:**\n", + "- Chroma version `1.3.4`\n", + "- Contextual AI client `0.9.0`\n", + "- OpenAI API (for embeddings and generation)\n", + "\n", + "This is a code recipe that uses [Chroma](https://docs.trychroma.com/) to perform multi-modal RAG over documents parsed by [Contextual AI Parser](https://docs.contextual.ai/api-reference/parse/parse-file).\n", + "\n", + "In this notebook, we accomplish the following:\n", + "* Parse two distinct document types using Contextual AI Parser: research papers and table-rich documents\n", + "* Extract structured markdown with document hierarchy preservation and advanced table extraction\n", + "* Generate text embeddings with OpenAI\n", + "* Perform multi-modal RAG using [Chroma](https://docs.trychroma.com/)\n", + "\n", + "To run this notebook, you'll need:\n", + "* A [Contextual AI API key](https://docs.contextual.ai/user-guides/beginner-guide) - for document parsing and content extraction.\n", + "Visit [app.contextual.ai](https://app.contextual.ai/?utm_campaign=chroma&utm_source=contextualai&utm_medium=github&utm_content=notebook) and click the **\"Start Free\"** button to sign up and receive free credits\n", + "* An [OpenAI API key](https://platform.openai.com/docs/quickstart) - for text embeddings and generative responses\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "u2vgMYz0XA4i" + }, + "source": [ + "### Install Contextual AI client and Chroma\n", + "\n", + "Note: If Colab prompts you to restart the session after running the cell below, click \"restart\" and proceed with running the rest of the notebook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "id": "mWtiuzumXA4i" + }, + "outputs": [], + "source": [ + "%%capture\n", + "%pip install --upgrade chromadb contextual-client openai requests rich nest-asyncio\n", + "\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "import logging\n", + "# Suppress Chroma client logs\n", + "logging.getLogger(\"chromadb\").setLevel(logging.ERROR)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i51fr8iWXA4j" + }, + "source": [ + "## 🔍 Part 1: Contextual AI Parser\n", + "\n", + "Contextual AI Parser is a cloud-based document parsing service that excels at extracting structured information from PDFs, DOC/DOCX, and PPT/PPTX files. It provides high-quality markdown extraction with document hierarchy preservation, making it ideal for RAG applications. See our [blog post on document parsing for RAG](https://contextual.ai/blog/document-parser-for-rag) for more details on parse quality and capabilities.\n", + "\n", + "The parser handles complex documents with images, tables, and hierarchical structures, providing multiple output formats including:\n", + "- `markdown-document`: Single concatenated markdown output\n", + "- `markdown-per-page`: Page-by-page markdown output\n", + "- `blocks-per-page`: Structured JSON with document hierarchy\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "AvMUhmNaXA4k" + }, + "outputs": [], + "source": [ + "# Documents to parse with Contextual AI\n", + "documents = [\n", + " {\n", + " \"url\": \"https://arxiv.org/pdf/1706.03762\",\n", + " \"title\": \"Attention Is All You Need\",\n", + " \"type\": \"research_paper\",\n", + " \"description\": \"Seminal transformer architecture paper that introduced self-attention mechanisms\"\n", + " },\n", + " {\n", + " \"url\": \"https://raw.githubusercontent.com/ContextualAI/examples/refs/heads/main/03-standalone-api/04-parse/data/omnidocbench-text.pdf\",\n", + " \"title\": \"OmniDocBench Dataset Documentation\",\n", + " \"type\": \"table_rich_document\",\n", + " \"description\": \"Dataset documentation with large tables demonstrating table extraction capabilities\"\n", + " }\n", + "]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "k4fPuzumXA4k" + }, + "source": [ + "### API Keys Setup 🔑\n", + "\n", + "We'll be using the Contextual AI API for parsing documents and OpenAI API for both generating text embeddings and for the generative model in our RAG pipeline. The code below dynamically fetches your API keys based on whether you're running this notebook in Google Colab or as a regular Jupyter notebook.\n", + "\n", + "If you're running this notebook in Google Colab, make sure you [add](https://medium.com/@parthdasawant/how-to-use-secrets-in-google-colab-450c38e3ec75) your API keys as secrets.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "4m1GBGmQXA4l", + "outputId": "33e681d1-8593-4221-95b2-e96fb38ac826" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "API keys configured successfully!\n" + ] + } + ], + "source": [ + "# API key variable names\n", + "contextual_api_key_var = \"CONTEXTUAL_API_KEY\" # Replace with the name of your secret/env var\n", + "openai_api_key_var = \"OPENAI_API_KEY\" # Replace with the name of your secret/env var\n", + "\n", + "# Fetch API keys\n", + "try:\n", + " # If running in Colab, fetch API keys from Secrets\n", + " import google.colab\n", + " from google.colab import userdata\n", + " contextual_api_key = userdata.get(contextual_api_key_var)\n", + " openai_api_key = userdata.get(openai_api_key_var)\n", + "\n", + " if not contextual_api_key:\n", + " raise ValueError(f\"Secret '{contextual_api_key_var}' not found in Colab secrets.\")\n", + " if not openai_api_key:\n", + " raise ValueError(f\"Secret '{openai_api_key_var}' not found in Colab secrets.\")\n", + "except ImportError:\n", + " # If not running in Colab, fetch API keys from environment variables\n", + " import os\n", + " contextual_api_key = os.getenv(contextual_api_key_var)\n", + " openai_api_key = os.getenv(openai_api_key_var)\n", + "\n", + " if not contextual_api_key:\n", + " raise EnvironmentError(\n", + " f\"Environment variable '{contextual_api_key_var}' is not set. \"\n", + " \"Please define it before running this script.\"\n", + " )\n", + " if not openai_api_key:\n", + " raise EnvironmentError(\n", + " f\"Environment variable '{openai_api_key_var}' is not set. \"\n", + " \"Please define it before running this script.\"\n", + " )\n", + "\n", + "print(\"API keys configured successfully!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kaGbsh8iXA4l" + }, + "source": [ + "### Download and parse PDFs using Contextual AI Parser\n", + "\n", + "Here we use Contextual AI's Python SDK to parse a batch of PDFs. The result is structured markdown content with document hierarchy that we can use for text extraction and chunking.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "8SKfMSaGXA4l", + "outputId": "421557ea-9f3e-44a9-e295-c11244a28fbb" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading and submitting parse job for: Attention Is All You Need\n", + "Type: research_paper - Seminal transformer architecture paper that introduced self-attention mechanisms\n", + "Submitted job 8154bf67-0f20-4247-8c4a-26f2be9de88d for Attention Is All You Need\n", + "Downloading and submitting parse job for: OmniDocBench Dataset Documentation\n", + "Type: table_rich_document - Dataset documentation with large tables demonstrating table extraction capabilities\n", + "Submitted job 8f0b9555-062b-4040-ada1-2aae9733e477 for OmniDocBench Dataset Documentation\n", + "\n", + "Submitted 2 parse jobs\n" + ] + } + ], + "source": [ + "import requests\n", + "from contextual import ContextualAI\n", + "import asyncio\n", + "import os\n", + "\n", + "# Setup Contextual AI client\n", + "client = ContextualAI(api_key=contextual_api_key)\n", + "\n", + "# Create directory for downloaded PDFs\n", + "os.makedirs(\"pdfs\", exist_ok=True)\n", + "\n", + "# Download PDFs and submit parse jobs\n", + "job_data = []\n", + "\n", + "for i, doc in enumerate(documents):\n", + " print(f\"Downloading and submitting parse job for: {doc['title']}\")\n", + " print(f\"Type: {doc['type']} - {doc['description']}\")\n", + "\n", + " # Download PDF\n", + " file_path = f\"pdfs/{doc['type']}_{i}.pdf\"\n", + " with open(file_path, \"wb\") as f:\n", + " f.write(requests.get(doc['url']).content)\n", + "\n", + " # Configure parsing parameters based on document type\n", + " if doc['type'] == \"research_paper\":\n", + " # For research papers, focus on hierarchy and figures\n", + " parse_config = {\n", + " \"parse_mode\": \"standard\",\n", + " \"figure_caption_mode\": \"concise\",\n", + " \"enable_document_hierarchy\": True,\n", + " \"page_range\": \"0-5\" # Parse first 6 pages\n", + " }\n", + " else:\n", + " # For table-rich documents, enable table splitting\n", + " parse_config = {\n", + " \"parse_mode\": \"standard\",\n", + " \"enable_split_tables\": True,\n", + " \"max_split_table_cells\": 100,\n", + " }\n", + "\n", + " # Submit parse job\n", + " with open(file_path, \"rb\") as fp:\n", + " response = client.parse.create(\n", + " raw_file=fp,\n", + " **parse_config\n", + " )\n", + "\n", + " job_data.append({\n", + " \"job_id\": response.job_id,\n", + " \"file_path\": file_path,\n", + " \"document\": doc\n", + " })\n", + " print(f\"Submitted job {response.job_id} for {doc['title']}\")\n", + "\n", + "print(f\"\\nSubmitted {len(job_data)} parse jobs\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qCJc8g0sXA4m" + }, + "source": [ + "### Monitor parse job status and retrieve results\n", + "\n", + "We'll monitor all parse jobs and retrieve the results once they're completed. Contextual AI provides structured markdown with document hierarchy information.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "eFPaAl17XA4m", + "outputId": "01997775-68dc-43b1-9cb8-e4c86c6885b5" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job 1/2 (Attention Is All You Need - research_paper): processing\n", + "Job 2/2 (OmniDocBench Dataset Documentation - table_rich_document): processing\n", + "\n", + "Waiting for remaining jobs to complete...\n", + "Job 1/2 (Attention Is All You Need - research_paper): processing\n", + "Job 2/2 (OmniDocBench Dataset Documentation - table_rich_document): completed\n", + "\n", + "Waiting for remaining jobs to complete...\n", + "Job 1/2 (Attention Is All You Need - research_paper): completed\n", + "\n", + "All parse jobs completed!\n" + ] + } + ], + "source": [ + "# Monitor all parse jobs using asyncio\n", + "async def wait_for_jobs_async(job_data, max_attempts=20, interval=30.0):\n", + " \"\"\"Asynchronously poll until all jobs are ready, exiting early if possible.\"\"\"\n", + " completed_jobs = set()\n", + "\n", + " for attempt in range(max_attempts):\n", + " if len(completed_jobs) >= len(job_data):\n", + " return completed_jobs\n", + "\n", + " for i, job_info in enumerate(job_data):\n", + " job_id = job_info[\"job_id\"]\n", + " if job_id not in completed_jobs:\n", + " # Run blocking call in thread pool\n", + " status = await asyncio.to_thread(client.parse.job_status, job_id)\n", + " doc_title = job_info[\"document\"][\"title\"]\n", + " doc_type = job_info[\"document\"][\"type\"]\n", + " print(f\"Job {i+1}/{len(job_data)} ({doc_title} - {doc_type}): {status.status}\")\n", + "\n", + " if status.status == \"completed\":\n", + " completed_jobs.add(job_id)\n", + " elif status.status == \"failed\":\n", + " print(f\"Job failed for {doc_title}\")\n", + " completed_jobs.add(job_id) # Add to completed to avoid infinite loop\n", + "\n", + " if len(completed_jobs) < len(job_data):\n", + " print(\"\\nWaiting for remaining jobs to complete...\")\n", + " await asyncio.sleep(interval)\n", + "\n", + " return completed_jobs # return the set of completed jobs\n", + "\n", + "# Run the async monitoring function\n", + "# Apply nest_asyncio to allow nested event loops (needed for Jupyter notebooks)\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "completed_jobs = asyncio.run(wait_for_jobs_async(job_data))\n", + "\n", + "print(\"\\nAll parse jobs completed!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jZ9L0JIDXA4m" + }, + "source": [ + "## 💚 Part 2: Chroma\n", + "### Create and configure a Chroma collection\n", + "\n", + "[Chroma](https://docs.trychroma.com/) is an open-source embedding database that makes it easy to build LLM apps by making knowledge, facts, and skills pluggable for LLMs. It provides efficient vector storage and similarity search capabilities.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "QhO3rNO3XA4m", + "outputId": "6f991d75-0353-452d-e3b7-0b4d50443048" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created collection 'contextual_ai_rag_collection' with OpenAI embeddings\n" + ] + } + ], + "source": [ + "import chromadb\n", + "from chromadb.utils import embedding_functions\n", + "\n", + "# Initialize Chroma client\n", + "chroma_client = chromadb.Client()\n", + "\n", + "# Use OpenAI embeddings\n", + "openai_ef = embedding_functions.OpenAIEmbeddingFunction(\n", + " api_key=openai_api_key,\n", + " model_name=\"text-embedding-3-small\"\n", + ")\n", + "\n", + "# Create collection\n", + "collection_name = \"contextual_ai_rag_collection\"\n", + "collection = chroma_client.get_or_create_collection(\n", + " name=collection_name,\n", + " embedding_function=openai_ef\n", + ")\n", + "\n", + "print(f\"Created collection '{collection_name}' with OpenAI embeddings\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gcwW29hRXA4n" + }, + "source": [ + "### Retrieve and process parsed content\n", + "\n", + "We'll retrieve the parsed results and process them into chunks suitable for vector search. Contextual AI provides excellent document structure preservation, which we'll leverage for better RAG performance.\n", + "\n", + "**Key Feature**: Contextual AI preserves document hierarchy through `parent_ids`, allowing us to maintain section relationships and provide richer context to our RAG system.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "5k2pdpthXA4n", + "outputId": "9d7b72b9-7467-45b6-e3ad-ff9bec5780c1" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Processing Attention Is All You Need (research_paper)\n", + " - 6 pages parsed\n", + "Processing OmniDocBench Dataset Documentation (table_rich_document)\n", + " - 1 pages parsed\n", + "\n", + "Processed 71 chunks from 2 documents\n", + "Document types: table_rich_document, research_paper\n" + ] + } + ], + "source": [ + "# Retrieve results and process into chunks\n", + "texts, titles, sources, doc_types, block_types, hierarchy_levels, confidence_levels = [], [], [], [], [], [], []\n", + "hierarchy_data = []\n", + "\n", + "for job_info in job_data:\n", + " job_id = job_info[\"job_id\"]\n", + " document = job_info[\"document\"]\n", + "\n", + " if job_id in completed_jobs:\n", + " try:\n", + " print(f\"Processing {document['title']} ({document['type']})\")\n", + "\n", + " # Get results with blocks-per-page for hierarchical information\n", + " results = client.parse.job_results(\n", + " job_id,\n", + " output_types=['blocks-per-page']\n", + " )\n", + "\n", + " # Store hierarchy if available\n", + " if hasattr(results, 'document_metadata') and results.document_metadata and hasattr(results.document_metadata, 'hierarchy'):\n", + " hierarchy_data.append({\n", + " 'title': document['title'],\n", + " 'hierarchy': results.document_metadata.hierarchy\n", + " })\n", + "\n", + " print(f\" - {len(results.pages)} pages parsed\")\n", + "\n", + " # Create hash table for parent content lookup\n", + " hash_table = {}\n", + " for page in results.pages:\n", + " for block in page.blocks:\n", + " hash_table[block.id] = block.markdown\n", + "\n", + " # Process blocks with hierarchy context\n", + " for page in results.pages:\n", + " for block in page.blocks:\n", + " # Filter blocks based on document type and content quality\n", + " if (block.type in ['text', 'heading', 'table'] and\n", + " len(block.markdown.strip()) > 30):\n", + "\n", + " # Add hierarchy context if available\n", + " context_text = block.markdown\n", + "\n", + " if hasattr(block, 'parent_ids') and block.parent_ids:\n", + " parent_content = \"\\n\".join([\n", + " hash_table.get(parent_id, \"\")\n", + " for parent_id in block.parent_ids\n", + " ])\n", + " if parent_content.strip():\n", + " context_text = f\"{parent_content}\\n\\n{block.markdown}\"\n", + "\n", + " # Add document metadata as context\n", + " full_text = f\"Document: {document['title']}\\nType: {document['type']}\\n\\n{context_text}\"\n", + "\n", + " texts.append(full_text)\n", + " titles.append(document['title'])\n", + " sources.append(f\"Page {page.index + 1}\")\n", + " doc_types.append(document['type'])\n", + " block_types.append(block.type)\n", + " # Extract hierarchy_level if available (for documents with hierarchy enabled)\n", + " hierarchy_levels.append(getattr(block, 'hierarchy_level', None))\n", + " # Extract confidence_level if available (for table blocks)\n", + " confidence_levels.append(getattr(block, 'confidence_level', None))\n", + "\n", + " except Exception as e:\n", + " print(f\"Error processing {document['title']}: {e}\")\n", + "\n", + "print(f\"\\nProcessed {len(texts)} chunks from {len(set(titles))} documents\")\n", + "print(f\"Document types: {', '.join(set(doc_types))}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 391 + }, + "id": "Lq6m_vSdYtgn", + "outputId": "f28f66ef-1768-4bc7-d75c-1fee97594dd9" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "### Document Hierarchy: Attention Is All You Need\n" + ] + }, + { + "data": { + "text/markdown": [ + "# Document Hierarchy\n", + "\n", + "- Attention Is All You Need [(Page 0)](#attention-is-all-you-need)\n", + " - Abstract [(Page 0)](#abstract)\n", + " - 1 Introduction [(Page 1)](#1-introduction)\n", + " - 2 Background [(Page 1)](#2-background)\n", + " - 3 Model Architecture [(Page 1)](#3-model-architecture)\n", + " - 3.1 Encoder and Decoder Stacks [(Page 2)](#31-encoder-and-decoder-stacks)\n", + " - 3.2 Attention [(Page 2)](#32-attention)\n", + " - 3.2.1 Scaled Dot-Product Attention [(Page 3)](#321-scaled-dot-product-attention)\n", + " - 3.2.2 Multi-Head Attention [(Page 3)](#322-multi-head-attention)\n", + " - 3.2.3 Applications of Attention in our Model [(Page 4)](#323-applications-of-attention-in-our-model)\n", + " - 3.3 Position-wise Feed-Forward Networks [(Page 4)](#33-position-wise-feed-forward-networks)\n", + " - 3.4 Embeddings and Softmax [(Page 4)](#34-embeddings-and-softmax)\n", + " - 3.5 Positional Encoding [(Page 5)](#35-positional-encoding)\n", + " - 4 Why Self-Attention [(Page 5)](#4-why-self-attention)" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Display extracted document hierarchy\n", + "from IPython.display import display, Markdown\n", + "\n", + "if hierarchy_data:\n", + " for item in hierarchy_data:\n", + " # Only display if table_of_contents exists and has meaningful content (more than just a header)\n", + " if (item['hierarchy'] and\n", + " hasattr(item['hierarchy'], 'table_of_contents') and\n", + " item['hierarchy'].table_of_contents and\n", + " len(item['hierarchy'].table_of_contents.strip()) > 50): # Check for meaningful content\n", + " print(f\"\\n### Document Hierarchy: {item['title']}\")\n", + " display(Markdown(item['hierarchy'].table_of_contents))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AUmNuMZRXA4n" + }, + "source": [ + "### Wrangle data into an acceptable format for Chroma\n", + "\n", + "Transform our data from lists to a list of dictionaries for insertion into our Chroma collection.\n", + "\n", + "**Note**: Contextual AI Parser provides additional metadata (e.g., `block_type`, `hierarchy_level`, `confidence_level`, `file_name`) that can be added to Chroma if you have capacity for more metadata fields.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "tivq2MpAXA4n", + "outputId": "43762968-ab04-4e57-d3e0-1210f888d71b" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Prepared 71 chunks for insertion into Chroma\n", + "Chunks by document type:\n", + " - table_rich_document: 9 chunks\n", + " - research_paper: 62 chunks\n" + ] + } + ], + "source": [ + "# Initialize the data object\n", + "data = []\n", + "\n", + "# Create a dictionary for each row by iterating through the corresponding lists\n", + "for text, title, source, doc_type, block_type, hierarchy_level, confidence_level in zip(\n", + " texts, titles, sources, doc_types, block_types, hierarchy_levels, confidence_levels\n", + "):\n", + " data_point = {\n", + " \"text\": text,\n", + " \"title\": title,\n", + " \"source\": source,\n", + " \"document_type\": doc_type,\n", + " \"block_type\": block_type,\n", + " }\n", + " # Add optional metadata fields if available\n", + " if hierarchy_level is not None:\n", + " data_point[\"hierarchy_level\"] = hierarchy_level\n", + " if confidence_level is not None:\n", + " data_point[\"confidence_level\"] = confidence_level\n", + "\n", + " data.append(data_point)\n", + "\n", + "print(f\"Prepared {len(data)} chunks for insertion into Chroma\")\n", + "print(f\"Chunks by document type:\")\n", + "for doc_type in set(doc_types):\n", + " count = doc_types.count(doc_type)\n", + " print(f\" - {doc_type}: {count} chunks\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bZ7AmoDlXA4n" + }, + "source": [ + "### Insert data into Chroma and generate embeddings\n", + "\n", + "Embeddings will be generated upon insertion to our Chroma collection.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "JR24qjphXA4o", + "outputId": "37f4e1a1-a6d0-4d8f-bfa1-76dc8a1037dd" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Insert complete.\n" + ] + } + ], + "source": [ + "# Insert text chunks and metadata into Chroma collection\n", + "# Build metadata dictionaries, including optional fields when available\n", + "metadata_list = []\n", + "for item in data:\n", + " metadata = {\n", + " \"title\": item[\"title\"],\n", + " \"source\": item[\"source\"],\n", + " \"document_type\": item[\"document_type\"],\n", + " \"block_type\": item[\"block_type\"]\n", + " }\n", + " # Add optional metadata fields if present\n", + " if \"hierarchy_level\" in item:\n", + " metadata[\"hierarchy_level\"] = item[\"hierarchy_level\"]\n", + " if \"confidence_level\" in item:\n", + " metadata[\"confidence_level\"] = item[\"confidence_level\"]\n", + " metadata_list.append(metadata)\n", + "\n", + "collection.add(\n", + " documents=[item[\"text\"] for item in data],\n", + " metadatas=metadata_list,\n", + " ids=[f\"chunk_{i}\" for i in range(len(data))]\n", + ")\n", + "\n", + "print(\"Insert complete.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8qILAMuHXA4o" + }, + "source": [ + "### Query the data\n", + "\n", + "Here, we perform a simple similarity search to return the most similar embedded chunks to our search query.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "wuyfM-npXA4o", + "outputId": "1c1810ea-26aa-42c2-e552-ddc0f03e7d32" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Searching for Transformer Architecture ===\n", + "\n", + "--- Result 1 ---\n", + "Title: Attention Is All You Need\n", + "Type: research_paper\n", + "Source: Page 3\n", + "Similarity: 0.278\n", + "Text preview: Document: Attention Is All You Need\n", + "Type: research_paper\n", + "\n", + "# Attention Is All You Need\n", + "## 3 Model Architecture\n", + "\n", + "The Transformer follows this overall architecture using stacked self-attention and point-...\n", + "\n", + "--- Result 2 ---\n", + "Title: Attention Is All You Need\n", + "Type: research_paper\n", + "Source: Page 3\n", + "Similarity: 0.271\n", + "Text preview: Document: Attention Is All You Need\n", + "Type: research_paper\n", + "\n", + "# Attention Is All You Need\n", + "## 3 Model Architecture\n", + "### 3.2 Attention\n", + "\n", + "Figure 1: The Transformer - model architecture....\n", + "\n", + "--- Result 3 ---\n", + "Title: Attention Is All You Need\n", + "Type: research_paper\n", + "Source: Page 5\n", + "Similarity: 0.250\n", + "Text preview: Document: Attention Is All You Need\n", + "Type: research_paper\n", + "\n", + "# Attention Is All You Need\n", + "## 3 Model Architecture\n", + "### 3.2 Attention\n", + "#### 3.2.3 Applications of Attention in our Model\n", + "\n", + "The Transformer uses ...\n", + "\n", + "==================================================\n", + "\n", + "=== Searching for Table/Data Content ===\n", + "\n", + "--- Result 1 ---\n", + "Title: OmniDocBench Dataset Documentation\n", + "Type: table_rich_document\n", + "Source: Page 1\n", + "Similarity: -0.020\n", + "Text preview: Document: OmniDocBench Dataset Documentation\n", + "Type: table_rich_document\n", + "\n", + "| Traits | Environment | Mean | | | S.D. | | | Minimum | | | Maximum | | |\n", + "|--------------------------------------------...\n", + "\n", + "--- Result 2 ---\n", + "Title: OmniDocBench Dataset Documentation\n", + "Type: table_rich_document\n", + "Source: Page 1\n", + "Similarity: -0.028\n", + "Text preview: Document: OmniDocBench Dataset Documentation\n", + "Type: table_rich_document\n", + "\n", + "| Traits | Environment | Mean | | | S.D. | | | Minimum | | | Maximum | | |\n", + "|----------------------------|---------------...\n", + "\n", + "--- Result 3 ---\n", + "Title: OmniDocBench Dataset Documentation\n", + "Type: table_rich_document\n", + "Source: Page 1\n", + "Similarity: -0.052\n", + "Text preview: Document: OmniDocBench Dataset Documentation\n", + "Type: table_rich_document\n", + "\n", + "| Traits | Environment | Mean | | | S.D. | | | Minimum | | | Maximum | | |\n", + "|-------------------|---------------|--------...\n" + ] + } + ], + "source": [ + "# Example 1: Search for transformer-related content\n", + "print(\"=== Searching for Transformer Architecture ===\")\n", + "results = collection.query(\n", + " query_texts=[\"transformer architecture attention mechanism\"],\n", + " n_results=3,\n", + " include=[\"documents\", \"metadatas\", \"distances\"]\n", + ")\n", + "\n", + "for i, (doc, metadata, distance) in enumerate(zip(results['documents'][0], results['metadatas'][0], results['distances'][0])):\n", + " print(f\"\\n--- Result {i+1} ---\")\n", + " print(f\"Title: {metadata['title']}\")\n", + " print(f\"Type: {metadata['document_type']}\")\n", + " print(f\"Source: {metadata['source']}\")\n", + " print(f\"Similarity: {1 - distance:.3f}\")\n", + " print(f\"Text preview: {doc[:200]}...\")\n", + "\n", + "print(\"\\n\" + \"=\"*50)\n", + "\n", + "# Example 2: Search for table-related content\n", + "print(\"\\n=== Searching for Table/Data Content ===\")\n", + "results = collection.query(\n", + " query_texts=[\"dataset table benchmark performance metrics\"],\n", + " n_results=3,\n", + " include=[\"documents\", \"metadatas\", \"distances\"]\n", + ")\n", + "\n", + "for i, (doc, metadata, distance) in enumerate(zip(results['documents'][0], results['metadatas'][0], results['distances'][0])):\n", + " print(f\"\\n--- Result {i+1} ---\")\n", + " print(f\"Title: {metadata['title']}\")\n", + " print(f\"Type: {metadata['document_type']}\")\n", + " print(f\"Source: {metadata['source']}\")\n", + " print(f\"Similarity: {1 - distance:.3f}\")\n", + " print(f\"Text preview: {doc[:200]}...\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gWr6qUa0XA4o" + }, + "source": [ + "### Perform RAG on parsed articles\n", + "\n", + "We'll use OpenAI's GPT model to generate responses based on the retrieved context from Chroma.\n", + "\n", + "#### Example 1: RAG on Transformer Architecture\n", + "\n", + "This example demonstrates a complete RAG pipeline: we query Chroma for relevant chunks, combine them as context, and generate a response using OpenAI. The `/parse` API contributed structured markdown with preserved document hierarchy, enabling better semantic search and context retrieval for technical content.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 237 + }, + "id": "171ADiKFXA4o", + "outputId": "c3adb5f9-f0b6-4961-dd47-00c0a1575d8a" + }, + "outputs": [ + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────────── Prompt ─────────────────────────────────────────────────────╮\n",
+              " Explain how transformer attention mechanism works, using only the retrieved context.                            \n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ], + "text/plain": [ + "\u001b[1;31m╭─\u001b[0m\u001b[1;31m───────────────────────────────────────────────────\u001b[0m\u001b[1;31m Prompt \u001b[0m\u001b[1;31m────────────────────────────────────────────────────\u001b[0m\u001b[1;31m─╮\u001b[0m\n", + "\u001b[1;31m│\u001b[0m Explain how transformer attention mechanism works, using only the retrieved context. \u001b[1;31m│\u001b[0m\n", + "\u001b[1;31m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────── Generated Content ───────────────────────────────────────────────╮\n",
+              " The Transformer replaces recurrence and convolution with an attention mechanism that directly models global     \n",
+              " dependencies between input and output. Instead of processing sequences step-by-step, it uses self-attention to  \n",
+              " compute representations of the input and of the output by letting each position in a sequence attend to (i.e.,  \n",
+              " draw information from) other positions. Because these attention operations are not inherently sequential, the   \n",
+              " architecture permits much greater parallelization and can be trained far more quickly. The model also uses      \n",
+              " multi-head attention — running several attention mechanisms in parallel — and applies this multi-head attention \n",
+              " in multiple places within the overall architecture (the paper notes three different uses).                      \n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ], + "text/plain": [ + "\u001b[1;32m╭─\u001b[0m\u001b[1;32m──────────────────────────────────────────────\u001b[0m\u001b[1;32m Generated Content \u001b[0m\u001b[1;32m──────────────────────────────────────────────\u001b[0m\u001b[1;32m─╮\u001b[0m\n", + "\u001b[1;32m│\u001b[0m The Transformer replaces recurrence and convolution with an attention mechanism that directly models global \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m dependencies between input and output. Instead of processing sequences step-by-step, it uses self-attention to \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m compute representations of the input and of the output by letting each position in a sequence attend to (i.e., \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m draw information from) other positions. Because these attention operations are not inherently sequential, the \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m architecture permits much greater parallelization and can be trained far more quickly. The model also uses \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m multi-head attention — running several attention mechanisms in parallel — and applies this multi-head attention \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m in multiple places within the overall architecture (the paper notes three different uses). \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from openai import OpenAI\n", + "from rich.console import Console\n", + "from rich.panel import Panel\n", + "\n", + "# Initialize OpenAI client\n", + "openai_client = OpenAI(api_key=openai_api_key)\n", + "\n", + "query = \"transformer attention mechanism\"\n", + "prompt = f\"Explain how {query} works, using only the retrieved context.\"\n", + "\n", + "# Retrieve relevant documents\n", + "results = collection.query(\n", + " query_texts=[query],\n", + " n_results=4,\n", + " include=[\"documents\", \"metadatas\"]\n", + ")\n", + "\n", + "# Prepare context\n", + "context = \"\\n\\n\".join(results['documents'][0])\n", + "\n", + "# Generate response\n", + "response = openai_client.chat.completions.create(\n", + " model=\"gpt-5-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": \"You are a helpful assistant that answers questions based on the provided context. Use only the information from the context.\"},\n", + " {\"role\": \"user\", \"content\": f\"Context: {context}\\n\\nQuestion: {prompt}\"}\n", + " ],\n", + " temperature=1\n", + ")\n", + "\n", + "# Prettify the output using Rich\n", + "console = Console()\n", + "console.print(Panel(prompt, title=\"Prompt\", border_style=\"bold red\"))\n", + "console.print(Panel(response.choices[0].message.content, title=\"Generated Content\", border_style=\"bold green\"))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "r3TCVDyLXA4o" + }, + "source": [ + "#### Example 2: RAG on Dataset/Benchmark Information\n", + "\n", + "This example queries Chroma for dataset and benchmark information, then generates a response. The `/parse` API's advanced table extraction capabilities ensure that structured data from table-rich documents is properly extracted and searchable, improving retrieval quality for data-focused queries." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 407 + }, + "id": "yAx9Ii9JXA4o", + "outputId": "4dbe1c42-1373-4c40-d42d-80fe756baafb" + }, + "outputs": [ + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────────── Prompt ─────────────────────────────────────────────────────╮\n",
+              " What information does the retrieved context provide about dataset benchmark performance evaluation?             \n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ], + "text/plain": [ + "\u001b[1;31m╭─\u001b[0m\u001b[1;31m───────────────────────────────────────────────────\u001b[0m\u001b[1;31m Prompt \u001b[0m\u001b[1;31m────────────────────────────────────────────────────\u001b[0m\u001b[1;31m─╮\u001b[0m\n", + "\u001b[1;31m│\u001b[0m What information does the retrieved context provide about dataset benchmark performance evaluation? \u001b[1;31m│\u001b[0m\n", + "\u001b[1;31m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────── Generated Content ───────────────────────────────────────────────╮\n",
+              " The retrieved context does not contain any model or algorithm performance numbers. Instead it provides the      \n",
+              " dataset summary statistics and experimental metadata that would be used for benchmarking:                       \n",
+              "                                                                                                                 \n",
+              " - It is Table 2 from the OmniDocBench documentation: \"Statistical estimations of the quantitative               \n",
+              " agromorphological and grain quality traits for the subsp. durum, turgidum and dicoccon at each environment (N   \n",
+              " north, C centre and S south).\"                                                                                  \n",
+              " - For multiple traits (e.g., Thousand kernel weight, Test weight, Yellow Index, Days to heading, Plant height)  \n",
+              " it reports, by subspecies (durum, turgidum, dicoccon) and environment (N, C, S or S08):                         \n",
+              "   - Mean                                                                                                        \n",
+              "   - Standard deviation (S.D.)                                                                                   \n",
+              "   - Minimum                                                                                                     \n",
+              "   - Maximum                                                                                                     \n",
+              " - Experimental note: all experiments were in 2007 except the south in 2008 (S08).                               \n",
+              "                                                                                                                 \n",
+              " In other words, the context supplies descriptive statistics and experiment timing/locations for the dataset     \n",
+              " (useful for evaluating and comparing methods), but contains no benchmark performance evaluations (no accuracy,  \n",
+              " error, AUC, etc.).                                                                                              \n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ], + "text/plain": [ + "\u001b[1;32m╭─\u001b[0m\u001b[1;32m──────────────────────────────────────────────\u001b[0m\u001b[1;32m Generated Content \u001b[0m\u001b[1;32m──────────────────────────────────────────────\u001b[0m\u001b[1;32m─╮\u001b[0m\n", + "\u001b[1;32m│\u001b[0m The retrieved context does not contain any model or algorithm performance numbers. Instead it provides the \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m dataset summary statistics and experimental metadata that would be used for benchmarking: \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - It is Table 2 from the OmniDocBench documentation: \"Statistical estimations of the quantitative \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m agromorphological and grain quality traits for the subsp. durum, turgidum and dicoccon at each environment (N \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m north, C centre and S south).\" \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - For multiple traits (e.g., Thousand kernel weight, Test weight, Yellow Index, Days to heading, Plant height) \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m it reports, by subspecies (durum, turgidum, dicoccon) and environment (N, C, S or S08): \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Mean \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Standard deviation (S.D.) \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Minimum \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Maximum \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Experimental note: all experiments were in 2007 except the south in 2008 (S08). \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m In other words, the context supplies descriptive statistics and experiment timing/locations for the dataset \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m (useful for evaluating and comparing methods), but contains no benchmark performance evaluations (no accuracy, \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m error, AUC, etc.). \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "query = \"dataset benchmark performance evaluation\"\n", + "prompt = f\"What information does the retrieved context provide about {query}?\"\n", + "\n", + "# Retrieve relevant documents\n", + "results = collection.query(\n", + " query_texts=[query],\n", + " n_results=4,\n", + " include=[\"documents\", \"metadatas\"]\n", + ")\n", + "\n", + "# Prepare context\n", + "context = \"\\n\\n\".join(results['documents'][0])\n", + "\n", + "# Generate response\n", + "response = openai_client.chat.completions.create(\n", + " model=\"gpt-5-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": \"You are a helpful assistant that answers questions based on the provided context. Use only the information from the context.\"},\n", + " {\"role\": \"user\", \"content\": f\"Context: {context}\\n\\nQuestion: {prompt}\"}\n", + " ],\n", + " temperature=1\n", + ")\n", + "\n", + "# Prettify the output using Rich\n", + "console = Console()\n", + "console.print(Panel(prompt, title=\"Prompt\", border_style=\"bold red\"))\n", + "console.print(Panel(response.choices[0].message.content, title=\"Generated Content\", border_style=\"bold green\"))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RnPjmcnZXA4o" + }, + "source": [ + "## Summary\n", + "\n", + "This notebook demonstrates a unique RAG pipeline using Contextual AI Parser and Chroma with two distinct document types:\n", + "\n", + "### What We Demonstrated:\n", + "1. **Research Paper Parsing**: \"Attention is All You Need\" with document hierarchy preservation\n", + "2. **Table-Rich Document Parsing**: OmniDocBench dataset with advanced table extraction\n", + "3. **Multi-modal RAG**: Semantic search across different document types\n", + "4. **Contextual Intelligence**: Leveraging document structure for better retrieval\n", + "\n", + "### Contextual AI Parser Advantages:\n", + "- **Cloud-based processing**: No local GPU/compute requirements\n", + "- **Document hierarchy preservation**: Maintains section relationships and structure\n", + "- **Advanced table handling**: Smart table splitting with header propagation\n", + "- **Multiple output formats**: Blocks, markdown, and structured JSON\n", + "- **Production-ready**: Scalable cloud service with enterprise features\n", + "\n", + "### Key Differentiators from Other Parsers:\n", + "- **Hierarchical context**: Parent-child relationships preserved in chunks\n", + "- **Table intelligence**: Large tables automatically split with context preservation\n", + "- **Document type awareness**: Different parsing strategies for different content types\n", + "- **Rich metadata**: Document structure information enhances RAG quality\n", + "\n", + "### Chroma Integration Benefits:\n", + "- **Multi-modal search**: Query across different document types simultaneously\n", + "- **Metadata filtering**: Filter by document type, source, and other attributes\n", + "- **Efficient storage**: Optimized vector database for embeddings\n", + "- **Scalability**: From local development to cloud production\n", + "\n", + "### Next Steps for Enhancement:\n", + "* Implement document-level metadata for better source attribution\n", + "* Add hybrid search combining keyword and semantic search\n", + "* Experiment with different chunking strategies for each document type\n", + "* End-to-end RAG agents via [Contextual AI](https://docs.contextual.ai/user-guides/beginner-guide)\n", + "* Get more information about integrating [Chroma](https://docs.trychroma.com/docs/overview/introduction)\n", + "\n", + "---\n", + "\n", + "**Ready to get started?** This notebook provides a complete, production-ready example of integrating Contextual AI Parser with Chroma for sophisticated RAG applications. The combination of Contextual AI's advanced parsing capabilities and Chroma's powerful vector search features creates a robust foundation for document-based AI applications.\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/18-contextualai-chroma/02-contextual-ai-reranker-chroma.ipynb b/18-contextualai-chroma/02-contextual-ai-reranker-chroma.ipynb new file mode 100644 index 0000000..6598a21 --- /dev/null +++ b/18-contextualai-chroma/02-contextual-ai-reranker-chroma.ipynb @@ -0,0 +1,1454 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "5BWP8eFqdMaN" + }, + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ContextualAI/examples/blob/main/18-contextualai-chroma/02-contextual-ai-reranker-chroma.ipynb)\n", + "\n", + "# Using Contextual AI Reranker with Chroma\n", + "\n", + "**Last updated:** November 2025\n", + "\n", + "**Versions used:**\n", + "- Chroma version `1.3.4`\n", + "- Contextual AI client `0.9.0`\n", + "\n", + "Contextual AI's reranker is the first with instruction-following capabilities to handle conflicts in retrieval. It is on the performance/cost Pareto frontier for industry-leading benchmarks like BEIR. This notebook demonstrates how to integrate Contextual AI's reranker with Chroma for enhanced RAG pipelines.\n", + "\n", + "**Key Features:**\n", + "- **Instruction-following reranking**: Handle complex retrieval scenarios with custom instructions\n", + "- **BEIR performance/cost Pareto frontier**: Optimal balance of accuracy and efficiency\n", + "- **Multi-lingual support**: Handle documents in multiple languages\n", + "- **Chroma integration**: Seamless vector database integration for retrieval + reranking\n", + "\n", + "The current reranker models include:\n", + "- ctxl-rerank-v2-instruct-multilingual\n", + "- ctxl-rerank-v2-instruct-multilingual-mini\n", + "- ctxl-rerank-v1-instruct\n", + "\n", + "**Open Source Version**: We also provide an open source version of our reranker available on [Hugging Face](https://huggingface.co/collections/ContextualAI/contextual-ai-reranker-v2) under the CC-BY-NC-SA-4.0 license.\n", + "\n", + "To run this notebook, you'll need:\n", + "* A [Contextual AI API key](https://docs.contextual.ai/user-guides/beginner-guide) - for document parsing and content extraction.\n", + "Visit [app.contextual.ai](https://app.contextual.ai/?utm_campaign=chroma&utm_source=contextualai&utm_medium=github&utm_content=notebook) and click the **\"Start Free\"** button to sign up and receive free credits\n", + "* An [OpenAI API key](https://platform.openai.com/docs/quickstart) - for text embeddings" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Y3RRGBs1dMaP" + }, + "source": [ + "## Installation and Setup\n", + "\n", + "First, let's install the required packages and set up our environment.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "3pVig3B9dMaP" + }, + "outputs": [], + "source": [ + "%%capture\n", + "%pip install --upgrade chromadb contextual-client openai requests rich\n", + "\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "import logging\n", + "# Suppress Chroma client logs\n", + "logging.getLogger(\"chromadb\").setLevel(logging.ERROR)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jp_IjEROdMaQ" + }, + "source": [ + "### API Keys Setup 🔑\n", + "\n", + "We'll be using the Contextual AI API for reranking and OpenAI API for embeddings. The code below dynamically fetches your API keys based on whether you're running this notebook in Google Colab or as a regular Jupyter notebook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "zYKGmXzOdMaQ", + "outputId": "01faae32-f649-4991-e82b-b80ff4094a4f" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "API keys configured successfully!\n" + ] + } + ], + "source": [ + "# API key variable names\n", + "contextual_api_key_var = \"CONTEXTUAL_API_KEY\" # Replace with the name of your secret/env var\n", + "openai_api_key_var = \"OPENAI_API_KEY\" # Replace with the name of your secret/env var\n", + "\n", + "# Fetch API keys\n", + "try:\n", + " # If running in Colab, fetch API keys from Secrets\n", + " import google.colab\n", + " from google.colab import userdata\n", + " contextual_api_key = userdata.get(contextual_api_key_var)\n", + " openai_api_key = userdata.get(openai_api_key_var)\n", + "\n", + " if not contextual_api_key:\n", + " raise ValueError(f\"Secret '{contextual_api_key_var}' not found in Colab secrets.\")\n", + " if not openai_api_key:\n", + " raise ValueError(f\"Secret '{openai_api_key_var}' not found in Colab secrets.\")\n", + "except ImportError:\n", + " # If not running in Colab, fetch API keys from environment variables\n", + " import os\n", + " contextual_api_key = os.getenv(contextual_api_key_var)\n", + " openai_api_key = os.getenv(openai_api_key_var)\n", + "\n", + " if not contextual_api_key:\n", + " raise EnvironmentError(\n", + " f\"Environment variable '{contextual_api_key_var}' is not set. \"\n", + " \"Please define it before running this script.\"\n", + " )\n", + " if not openai_api_key:\n", + " raise EnvironmentError(\n", + " f\"Environment variable '{openai_api_key_var}' is not set. \"\n", + " \"Please define it before running this script.\"\n", + " )\n", + "\n", + "print(\"API keys configured successfully!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yx6KrCoIdMaQ" + }, + "source": [ + "## Part 1: Setup Chroma with Sample Data\n", + "\n", + "Let's create a Chroma collection with sample enterprise documents to demonstrate the reranking capabilities.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "iRD9CyccdMaQ", + "outputId": "e7007ca6-6ae7-4af7-e579-0ae29c8aaf03" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Created collection 'enterprise_documents' with OpenAI embeddings\n" + ] + } + ], + "source": [ + "import chromadb\n", + "from chromadb.utils import embedding_functions\n", + "from contextual import ContextualAI\n", + "from rich.console import Console\n", + "from rich.panel import Panel\n", + "from rich.table import Table\n", + "\n", + "# Initialize clients\n", + "contextual_client = ContextualAI(api_key=contextual_api_key)\n", + "chroma_client = chromadb.Client()\n", + "\n", + "# Use OpenAI embeddings\n", + "openai_ef = embedding_functions.OpenAIEmbeddingFunction(\n", + " api_key=openai_api_key,\n", + " model_name=\"text-embedding-3-small\"\n", + ")\n", + "\n", + "# Create collection\n", + "collection_name = \"enterprise_documents\"\n", + "collection = chroma_client.get_or_create_collection(\n", + " name=collection_name,\n", + " embedding_function=openai_ef\n", + ")\n", + "\n", + "print(f\"Created collection '{collection_name}' with OpenAI embeddings\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "pz49xdZDdMaQ", + "outputId": "dbd7e7f1-bb3f-4e8c-f29e-e3d4a645df40" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Added 6 documents to Chroma collection\n" + ] + } + ], + "source": [ + "# Sample enterprise documents with different types and dates\n", + "sample_documents = [\n", + " {\n", + " \"content\": \"Following detailed cost analysis and market research, we have implemented the following changes: AI training clusters will see a 15% uplift in raw compute performance, enterprise support packages are being restructured, and bulk procurement programs (100+ units) for the RTX 5090 Enterprise series will operate on a $2,899 baseline.\",\n", + " \"metadata\": {\n", + " \"title\": \"Enterprise GPU Pricing Update\",\n", + " \"date\": \"2025-01-15\",\n", + " \"source\": \"NVIDIA Enterprise Sales Portal\",\n", + " \"classification\": \"Internal Use Only\",\n", + " \"department\": \"Sales\"\n", + " }\n", + " },\n", + " {\n", + " \"content\": \"Enterprise pricing for the RTX 5090 GPU bulk orders (100+ units) is currently set at $3,100-$3,300 per unit. This pricing for RTX 5090 enterprise bulk orders has been confirmed across all major distribution channels.\",\n", + " \"metadata\": {\n", + " \"title\": \"Market Analysis Report\",\n", + " \"date\": \"2023-11-30\",\n", + " \"source\": \"TechAnalytics Research Group\",\n", + " \"classification\": \"Public\",\n", + " \"department\": \"Research\"\n", + " }\n", + " },\n", + " {\n", + " \"content\": \"RTX 5090 Enterprise GPU requires 450W TDP and 20% cooling overhead. Power consumption analysis shows optimal performance at 85% utilization with enterprise-grade cooling solutions.\",\n", + " \"metadata\": {\n", + " \"title\": \"Technical Specifications\",\n", + " \"date\": \"2025-01-25\",\n", + " \"source\": \"NVIDIA Enterprise Sales Portal\",\n", + " \"classification\": \"Internal Use Only\",\n", + " \"department\": \"Engineering\"\n", + " }\n", + " },\n", + " {\n", + " \"content\": \"Our enterprise customers have reported significant performance improvements with the RTX 5090 in AI workloads. Training times reduced by 40% compared to previous generation GPUs.\",\n", + " \"metadata\": {\n", + " \"title\": \"Customer Performance Report\",\n", + " \"date\": \"2025-01-10\",\n", + " \"source\": \"Customer Success Team\",\n", + " \"classification\": \"Confidential\",\n", + " \"department\": \"Customer Success\"\n", + " }\n", + " },\n", + " {\n", + " \"content\": \"The RTX 5090 represents a breakthrough in enterprise AI computing. With 128GB of HBM3e memory and 2.5x faster training performance, it's designed for the most demanding AI workloads.\",\n", + " \"metadata\": {\n", + " \"title\": \"Product Launch Announcement\",\n", + " \"date\": \"2024-12-01\",\n", + " \"source\": \"Marketing Department\",\n", + " \"classification\": \"Public\",\n", + " \"department\": \"Marketing\"\n", + " }\n", + " },\n", + " {\n", + " \"content\": \"Internal memo: RTX 5090 enterprise pricing strategy has been revised. New baseline pricing effective January 15, 2025: $2,899 for bulk orders (100+ units), $3,200 for standard enterprise orders.\",\n", + " \"metadata\": {\n", + " \"title\": \"Internal Pricing Memo\",\n", + " \"date\": \"2025-01-12\",\n", + " \"source\": \"Executive Team\",\n", + " \"classification\": \"Internal Use Only\",\n", + " \"department\": \"Executive\"\n", + " }\n", + " }\n", + "]\n", + "\n", + "# Add documents to Chroma\n", + "documents = [doc[\"content\"] for doc in sample_documents]\n", + "metadatas = [doc[\"metadata\"] for doc in sample_documents]\n", + "ids = [f\"doc_{i}\" for i in range(len(sample_documents))]\n", + "\n", + "collection.add(\n", + " documents=documents,\n", + " metadatas=metadatas,\n", + " ids=ids\n", + ")\n", + "\n", + "print(f\"Added {len(sample_documents)} documents to Chroma collection\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "o-TayaOpdMaR" + }, + "source": [ + "## Part 2: Basic Retrieval vs. Reranked Retrieval\n", + "\n", + "Let's demonstrate the difference between basic Chroma retrieval and Contextual AI's instruction-following reranking.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "0lzwHAIhdMaR", + "outputId": "3dcedcb5-d521-4d60-b257-c2bf9fc143aa" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Query: What is the current enterprise pricing for the RTX 5090 GPU for bulk orders?\n", + "Instruction: Prioritize internal sales documents over market analysis reports. More recent documents should be weighted higher. Enterprise portal content supersedes distributor communications.\n", + "\n", + "================================================================================\n" + ] + } + ], + "source": [ + "# Query and instruction for reranking\n", + "query = \"What is the current enterprise pricing for the RTX 5090 GPU for bulk orders?\"\n", + "\n", + "instruction = \"Prioritize internal sales documents over market analysis reports. More recent documents should be weighted higher. Enterprise portal content supersedes distributor communications.\"\n", + "\n", + "print(f\"Query: {query}\")\n", + "print(f\"Instruction: {instruction}\")\n", + "print(\"\\n\" + \"=\"*80)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "20SqwAsydMaR", + "outputId": "1c7cdec9-4a87-4f1a-9ffc-d8b4ff5f4140" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "🔍 BASIC CHROMA RETRIEVAL\n", + "==================================================\n", + "Retrieved 6 documents from Chroma\n", + "\n", + "Chroma Results (ordered by similarity):\n", + "\n", + "1. Market Analysis Report (Similarity: 0.816)\n", + " Source: TechAnalytics Research Group | Date: 2023-11-30\n", + " Classification: Public\n", + " Content: Enterprise pricing for the RTX 5090 GPU bulk orders (100+ units) is currently set at $3,100-$3,300 p...\n", + "\n", + "2. Internal Pricing Memo (Similarity: 0.638)\n", + " Source: Executive Team | Date: 2025-01-12\n", + " Classification: Internal Use Only\n", + " Content: Internal memo: RTX 5090 enterprise pricing strategy has been revised. New baseline pricing effective...\n", + "\n", + "3. Enterprise GPU Pricing Update (Similarity: 0.292)\n", + " Source: NVIDIA Enterprise Sales Portal | Date: 2025-01-15\n", + " Classification: Internal Use Only\n", + " Content: Following detailed cost analysis and market research, we have implemented the following changes: AI ...\n", + "\n", + "4. Customer Performance Report (Similarity: 0.218)\n", + " Source: Customer Success Team | Date: 2025-01-10\n", + " Classification: Confidential\n", + " Content: Our enterprise customers have reported significant performance improvements with the RTX 5090 in AI ...\n", + "\n", + "5. Product Launch Announcement (Similarity: 0.174)\n", + " Source: Marketing Department | Date: 2024-12-01\n", + " Classification: Public\n", + " Content: The RTX 5090 represents a breakthrough in enterprise AI computing. With 128GB of HBM3e memory and 2....\n", + "\n", + "6. Technical Specifications (Similarity: 0.160)\n", + " Source: NVIDIA Enterprise Sales Portal | Date: 2025-01-25\n", + " Classification: Internal Use Only\n", + " Content: RTX 5090 Enterprise GPU requires 450W TDP and 20% cooling overhead. Power consumption analysis shows...\n" + ] + } + ], + "source": [ + "# Step 1: Basic Chroma retrieval\n", + "print(\"🔍 BASIC CHROMA RETRIEVAL\")\n", + "print(\"=\"*50)\n", + "\n", + "# Retrieve more documents than we need for reranking\n", + "chroma_results = collection.query(\n", + " query_texts=[query],\n", + " n_results=6, # Get all documents for reranking\n", + " include=[\"documents\", \"metadatas\", \"distances\"]\n", + ")\n", + "\n", + "print(f\"Retrieved {len(chroma_results['documents'][0])} documents from Chroma\")\n", + "print(\"\\nChroma Results (ordered by similarity):\")\n", + "for i, (doc, metadata, distance) in enumerate(zip(\n", + " chroma_results['documents'][0],\n", + " chroma_results['metadatas'][0],\n", + " chroma_results['distances'][0]\n", + ")):\n", + " print(f\"\\n{i+1}. {metadata['title']} (Similarity: {1-distance:.3f})\")\n", + " print(f\" Source: {metadata['source']} | Date: {metadata['date']}\")\n", + " print(f\" Classification: {metadata['classification']}\")\n", + " print(f\" Content: {doc[:100]}...\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "iTzOVs9DdMaR", + "outputId": "d35b1f0e-5531-48f7-e3e7-b5d411da11a0" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "\n", + "🎯 CONTEXTUAL AI RERANKING\n", + "==================================================\n", + "Reranked 6 documents using instruction-following reranking\n", + "\n", + "Reranked Results (ordered by relevance + instruction):\n", + "\n", + "1. Enterprise GPU Pricing Update (Score: 1.000)\n", + " Source: NVIDIA Enterprise Sales Portal | Date: 2025-01-15\n", + " Classification: Internal Use Only\n", + " Content: Following detailed cost analysis and market research, we have implemented the following changes: AI ...\n", + "\n", + "2. Internal Pricing Memo (Score: 1.000)\n", + " Source: Executive Team | Date: 2025-01-12\n", + " Classification: Internal Use Only\n", + " Content: Internal memo: RTX 5090 enterprise pricing strategy has been revised. New baseline pricing effective...\n", + "\n", + "3. Market Analysis Report (Score: 0.987)\n", + " Source: TechAnalytics Research Group | Date: 2023-11-30\n", + " Classification: Public\n", + " Content: Enterprise pricing for the RTX 5090 GPU bulk orders (100+ units) is currently set at $3,100-$3,300 p...\n", + "\n", + "4. Technical Specifications (Score: 0.923)\n", + " Source: NVIDIA Enterprise Sales Portal | Date: 2025-01-25\n", + " Classification: Internal Use Only\n", + " Content: RTX 5090 Enterprise GPU requires 450W TDP and 20% cooling overhead. Power consumption analysis shows...\n", + "\n", + "5. Customer Performance Report (Score: 0.531)\n", + " Source: Customer Success Team | Date: 2025-01-10\n", + " Classification: Confidential\n", + " Content: Our enterprise customers have reported significant performance improvements with the RTX 5090 in AI ...\n", + "\n", + "6. Product Launch Announcement (Score: 0.329)\n", + " Source: Marketing Department | Date: 2024-12-01\n", + " Classification: Public\n", + " Content: The RTX 5090 represents a breakthrough in enterprise AI computing. With 128GB of HBM3e memory and 2....\n" + ] + } + ], + "source": [ + "# Step 2: Contextual AI Reranking\n", + "print(\"\\n\\n🎯 CONTEXTUAL AI RERANKING\")\n", + "print(\"=\"*50)\n", + "\n", + "# Prepare documents and metadata for reranking\n", + "documents_to_rerank = chroma_results['documents'][0]\n", + "metadata_for_rerank = [str(meta) for meta in chroma_results['metadatas'][0]]\n", + "\n", + "# Apply Contextual AI reranking with instruction\n", + "rerank_response = contextual_client.rerank.create(\n", + " query=query,\n", + " instruction=instruction,\n", + " documents=documents_to_rerank,\n", + " metadata=metadata_for_rerank,\n", + " model=\"ctxl-rerank-v2-instruct-multilingual\"\n", + ")\n", + "\n", + "print(f\"Reranked {len(rerank_response.results)} documents using instruction-following reranking\")\n", + "print(\"\\nReranked Results (ordered by relevance + instruction):\")\n", + "for i, result in enumerate(rerank_response.results):\n", + " original_index = result.index\n", + " original_metadata = chroma_results['metadatas'][0][original_index]\n", + " original_doc = chroma_results['documents'][0][original_index]\n", + "\n", + " print(f\"\\n{i+1}. {original_metadata['title']} (Score: {result.relevance_score:.3f})\")\n", + " print(f\" Source: {original_metadata['source']} | Date: {original_metadata['date']}\")\n", + " print(f\" Classification: {original_metadata['classification']}\")\n", + " print(f\" Content: {original_doc[:100]}...\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZVy1wJdydMaR" + }, + "source": [ + "## Part 3: Complete RAG Pipeline with Reranking\n", + "\n", + "Now let's demonstrate a complete RAG pipeline that combines Chroma retrieval, Contextual AI reranking, and LLM generation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 458 + }, + "id": "SmlP_T4KdMaR", + "outputId": "02cfed8b-954e-4aae-afb3-82affa306ec3" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;35m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;35m│\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m🚀 COMPLETE RAG PIPELINE DEMO\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m│\u001b[0m\n", + "\u001b[1;35m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ 🚀 COMPLETE RAG PIPELINE DEMO                                                                                   │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;34m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;34m│\u001b[0m\u001b[1;34m \u001b[0m\u001b[1;34mStep 1: Retrieving from Chroma\u001b[0m\u001b[1;34m \u001b[0m\u001b[1;34m \u001b[0m\u001b[1;34m│\u001b[0m\n", + "\u001b[1;34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Step 1: Retrieving from Chroma                                                                                  │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;32m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;32m│\u001b[0m\u001b[1;32m \u001b[0m\u001b[1;32mStep 2: Reranking with Contextual AI\u001b[0m\u001b[1;32m \u001b[0m\u001b[1;32m \u001b[0m\u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Step 2: Reranking with Contextual AI                                                                            │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;33m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;33m│\u001b[0m\u001b[1;33m \u001b[0m\u001b[1;33mStep 3: Generating response with LLM\u001b[0m\u001b[1;33m \u001b[0m\u001b[1;33m \u001b[0m\u001b[1;33m│\u001b[0m\n", + "\u001b[1;33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Step 3: Generating response with LLM                                                                            │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;32m╭─\u001b[0m\u001b[1;32m─────────────────────────────────────────────\u001b[0m\u001b[1;32m Generated Response \u001b[0m\u001b[1;32m──────────────────────────────────────────────\u001b[0m\u001b[1;32m─╮\u001b[0m\n", + "\u001b[1;32m│\u001b[0m The memo contains conflicting figures. As of now, enterprise bulk pricing is reported as $3,100–$3,300 per unit \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m (confirmed across major distribution channels) (Context: \"Enterprise pricing for the RTX 5090 GPU bulk orders \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m (100+ units) is currently set at $3,100-$3,300 per unit. This pricing ... has been confirmed across all major \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m distribution channels.\"). However, a revised baseline of $2,899 for bulk orders (100+ units) is scheduled \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m effective January 15, 2025 (Context: \"New baseline pricing effective January 15, 2025: $2,899 for bulk orders \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m (100+ units)\" and \"bulk procurement programs (100+ units) ... will operate on a $2,899 baseline.\"). \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭────────────────────────────────────────────── Generated Response ───────────────────────────────────────────────╮\n",
+              " The memo contains conflicting figures. As of now, enterprise bulk pricing is reported as $3,100–$3,300 per unit \n",
+              " (confirmed across major distribution channels) (Context: \"Enterprise pricing for the RTX 5090 GPU bulk orders   \n",
+              " (100+ units) is currently set at $3,100-$3,300 per unit. This pricing ... has been confirmed across all major   \n",
+              " distribution channels.\"). However, a revised baseline of $2,899 for bulk orders (100+ units) is scheduled       \n",
+              " effective January 15, 2025 (Context: \"New baseline pricing effective January 15, 2025: $2,899 for bulk orders   \n",
+              " (100+ units)\" and \"bulk procurement programs (100+ units) ... will operate on a $2,899 baseline.\").             \n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;34m╭─\u001b[0m\u001b[1;34m───────────────────────────────────────────────────\u001b[0m\u001b[1;34m Sources \u001b[0m\u001b[1;34m───────────────────────────────────────────────────\u001b[0m\u001b[1;34m─╮\u001b[0m\n", + "\u001b[1;34m│\u001b[0m Sources used: ['Internal Pricing Memo', 'Enterprise GPU Pricing Update', 'Market Analysis Report'] \u001b[1;34m│\u001b[0m\n", + "\u001b[1;34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭──────────────────────────────────────────────────── Sources ────────────────────────────────────────────────────╮\n",
+              " Sources used: ['Internal Pricing Memo', 'Enterprise GPU Pricing Update', 'Market Analysis Report']              \n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + } + ], + "source": [ + "from openai import OpenAI\n", + "\n", + "# Initialize OpenAI client\n", + "openai_client = OpenAI(api_key=openai_api_key)\n", + "\n", + "def complete_rag_pipeline(query, instruction, top_k=3):\n", + " \"\"\"\n", + " Complete RAG pipeline: Chroma retrieval + Contextual AI reranking + LLM generation\n", + " \"\"\"\n", + " console = Console()\n", + "\n", + " # Step 1: Retrieve from Chroma\n", + " console.print(Panel(\"Step 1: Retrieving from Chroma\", style=\"bold blue\"))\n", + " chroma_results = collection.query(\n", + " query_texts=[query],\n", + " n_results=6, # Get more for reranking\n", + " include=[\"documents\", \"metadatas\", \"distances\"]\n", + " )\n", + "\n", + " # Step 2: Rerank with Contextual AI\n", + " console.print(Panel(\"Step 2: Reranking with Contextual AI\", style=\"bold green\"))\n", + " documents_to_rerank = chroma_results['documents'][0]\n", + " metadata_for_rerank = [str(meta) for meta in chroma_results['metadatas'][0]]\n", + "\n", + " rerank_response = contextual_client.rerank.create(\n", + " query=query,\n", + " instruction=instruction,\n", + " documents=documents_to_rerank,\n", + " metadata=metadata_for_rerank,\n", + " model=\"ctxl-rerank-v2-instruct-multilingual\"\n", + " )\n", + "\n", + " # Step 3: Get top-k reranked documents\n", + " top_docs = []\n", + " top_metadata = []\n", + "\n", + " for i in range(min(top_k, len(rerank_response.results))):\n", + " result = rerank_response.results[i]\n", + " original_index = result.index\n", + " top_docs.append(chroma_results['documents'][0][original_index])\n", + " top_metadata.append(chroma_results['metadatas'][0][original_index])\n", + "\n", + " # Step 4: Generate response with LLM\n", + " console.print(Panel(\"Step 3: Generating response with LLM\", style=\"bold yellow\"))\n", + " context = \"\\n\\n\".join(top_docs)\n", + "\n", + " response = openai_client.chat.completions.create(\n", + " model=\"gpt-5-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": \"You are a helpful assistant that answers questions based on the provided context. Use only the information from the context and cite your sources.\"},\n", + " {\"role\": \"user\", \"content\": f\"Context: {context}\\n\\nQuestion: {query}\"}\n", + " ],\n", + " temperature=1\n", + " )\n", + "\n", + " return {\n", + " \"response\": response.choices[0].message.content,\n", + " \"sources\": top_metadata,\n", + " \"rerank_scores\": [result.relevance_score for result in rerank_response.results[:top_k]]\n", + " }\n", + "\n", + "# Example 1: Enterprise pricing query\n", + "console = Console()\n", + "console.print(Panel(\"🚀 COMPLETE RAG PIPELINE DEMO\", style=\"bold magenta\"))\n", + "\n", + "result = complete_rag_pipeline(\n", + " query=\"What is the current enterprise pricing for the RTX 5090 GPU for bulk orders?\",\n", + " instruction=\"Prioritize internal sales documents over market analysis reports. More recent documents should be weighted higher. Enterprise portal content supersedes distributor communications.\",\n", + " top_k=3\n", + ")\n", + "\n", + "console.print(Panel(result[\"response\"], title=\"Generated Response\", border_style=\"bold green\"))\n", + "console.print(Panel(f\"Sources used: {[meta['title'] for meta in result['sources']]}\", title=\"Sources\", border_style=\"bold blue\"))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 628 + }, + "id": "JVGKB2h4dMaS", + "outputId": "88012edf-7d88-4a08-af6c-5cc8767fde08" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;36m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;36m│\u001b[0m\u001b[1;36m \u001b[0m\u001b[1;36m🔧 TECHNICAL SPECIFICATIONS QUERY\u001b[0m\u001b[1;36m \u001b[0m\u001b[1;36m \u001b[0m\u001b[1;36m│\u001b[0m\n", + "\u001b[1;36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ 🔧 TECHNICAL SPECIFICATIONS QUERY                                                                               │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;34m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;34m│\u001b[0m\u001b[1;34m \u001b[0m\u001b[1;34mStep 1: Retrieving from Chroma\u001b[0m\u001b[1;34m \u001b[0m\u001b[1;34m \u001b[0m\u001b[1;34m│\u001b[0m\n", + "\u001b[1;34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Step 1: Retrieving from Chroma                                                                                  │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;32m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;32m│\u001b[0m\u001b[1;32m \u001b[0m\u001b[1;32mStep 2: Reranking with Contextual AI\u001b[0m\u001b[1;32m \u001b[0m\u001b[1;32m \u001b[0m\u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Step 2: Reranking with Contextual AI                                                                            │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;33m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;33m│\u001b[0m\u001b[1;33m \u001b[0m\u001b[1;33mStep 3: Generating response with LLM\u001b[0m\u001b[1;33m \u001b[0m\u001b[1;33m \u001b[0m\u001b[1;33m│\u001b[0m\n", + "\u001b[1;33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Step 3: Generating response with LLM                                                                            │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;32m╭─\u001b[0m\u001b[1;32m─────────────────────────────────────────────\u001b[0m\u001b[1;32m Generated Response \u001b[0m\u001b[1;32m──────────────────────────────────────────────\u001b[0m\u001b[1;32m─╮\u001b[0m\n", + "\u001b[1;32m│\u001b[0m Summary (from provided context) \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Model: RTX 5090 Enterprise GPU. (Context) \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Thermal Design Power (TDP): 450 W. (Context: \"RTX 5090 Enterprise GPU requires 450W TDP and 20% cooling \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m overhead.\") \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Cooling overhead required: 20% above TDP — plan cooling for 450 W × 1.20 = 540 W of heat removal capacity. \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m (Context: \"20% cooling overhead.\") \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Recommended operating point: power-consumption analysis shows optimal performance at about 85% utilization \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m when paired with enterprise‑grade cooling solutions. (Context: \"Power consumption analysis shows optimal \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m performance at 85% utilization with enterprise-grade cooling solutions.\") \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m - Approximate operating power at that point: ~85% of 450 W ≈ 382.5 W (derived from the TDP and the stated \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m optimal utilization). (Context: TDP = 450 W; \"85% utilization\") \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m Notes: enterprise customers reported significant AI performance gains (training times reduced ~40%), which \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m supports deploying appropriate cooling and power provisioning for best results. (Context: \"Training times \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m reduced by 40% compared to previous generation GPUs.\") \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭────────────────────────────────────────────── Generated Response ───────────────────────────────────────────────╮\n",
+              " Summary (from provided context)                                                                                 \n",
+              "                                                                                                                 \n",
+              " - Model: RTX 5090 Enterprise GPU. (Context)                                                                     \n",
+              " - Thermal Design Power (TDP): 450 W. (Context: \"RTX 5090 Enterprise GPU requires 450W TDP and 20% cooling       \n",
+              " overhead.\")                                                                                                     \n",
+              " - Cooling overhead required: 20% above TDP — plan cooling for 450 W × 1.20 = 540 W of heat removal capacity.    \n",
+              " (Context: \"20% cooling overhead.\")                                                                              \n",
+              " - Recommended operating point: power-consumption analysis shows optimal performance at about 85% utilization    \n",
+              " when paired with enterprise‑grade cooling solutions. (Context: \"Power consumption analysis shows optimal        \n",
+              " performance at 85% utilization with enterprise-grade cooling solutions.\")                                       \n",
+              " - Approximate operating power at that point: ~85% of 450 W ≈ 382.5 W (derived from the TDP and the stated       \n",
+              " optimal utilization). (Context: TDP = 450 W; \"85% utilization\")                                                 \n",
+              "                                                                                                                 \n",
+              " Notes: enterprise customers reported significant AI performance gains (training times reduced ~40%), which      \n",
+              " supports deploying appropriate cooling and power provisioning for best results. (Context: \"Training times       \n",
+              " reduced by 40% compared to previous generation GPUs.\")                                                          \n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;34m╭─\u001b[0m\u001b[1;34m───────────────────────────────────────────────────\u001b[0m\u001b[1;34m Sources \u001b[0m\u001b[1;34m───────────────────────────────────────────────────\u001b[0m\u001b[1;34m─╮\u001b[0m\n", + "\u001b[1;34m│\u001b[0m Sources used: ['Technical Specifications', 'Customer Performance Report', 'Internal Pricing Memo'] \u001b[1;34m│\u001b[0m\n", + "\u001b[1;34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭──────────────────────────────────────────────────── Sources ────────────────────────────────────────────────────╮\n",
+              " Sources used: ['Technical Specifications', 'Customer Performance Report', 'Internal Pricing Memo']              \n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + } + ], + "source": [ + "# Example 2: Technical specifications query with different instruction\n", + "console.print(Panel(\"🔧 TECHNICAL SPECIFICATIONS QUERY\", style=\"bold cyan\"))\n", + "\n", + "result2 = complete_rag_pipeline(\n", + " query=\"What are the technical specifications and power requirements for the RTX 5090?\",\n", + " instruction=\"Prioritize technical documentation and engineering specifications. Internal technical documents should rank higher than marketing materials. Focus on detailed specifications and performance metrics.\",\n", + " top_k=3\n", + ")\n", + "\n", + "console.print(Panel(result2[\"response\"], title=\"Generated Response\", border_style=\"bold green\"))\n", + "console.print(Panel(f\"Sources used: {[meta['title'] for meta in result2['sources']]}\", title=\"Sources\", border_style=\"bold blue\"))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rYvcGNh8dMaS" + }, + "source": [ + "## Part 4: Advanced Reranking Scenarios\n", + "\n", + "Let's demonstrate different reranking scenarios to show the flexibility of instruction-following reranking.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 858 + }, + "id": "E198TPkYdMaS", + "outputId": "405bda52-2784-4372-d147-bee4e0038e6c" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;35m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;35m│\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m🔄 COMPARING RERANKING STRATEGIES\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m│\u001b[0m\n", + "\u001b[1;35m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ 🔄 COMPARING RERANKING STRATEGIES                                                                               │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Query: What is the current status and pricing for RTX \u001b[1;36m5090\u001b[0m enterprise GPUs?\n", + "\n" + ], + "text/html": [ + "
Query: What is the current status and pricing for RTX 5090 enterprise GPUs?\n",
+              "\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;35m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;35m│\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35mStrategy: Recent Documents First\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m│\u001b[0m\n", + "\u001b[1;35m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Strategy: Recent Documents First                                                                                │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Instruction: Prioritize the most recent documents. Documents from \u001b[1;36m2025\u001b[0m should rank higher than older documents.\n" + ], + "text/html": [ + "
Instruction: Prioritize the most recent documents. Documents from 2025 should rank higher than older documents.\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Top \u001b[1;36m3\u001b[0m Results:\n" + ], + "text/html": [ + "
Top 3 Results:\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m1\u001b[0m. Internal Pricing Memo \u001b[1m(\u001b[0mScore: \u001b[1;36m1.000\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  1. Internal Pricing Memo (Score: 1.000)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m2\u001b[0m. Enterprise GPU Pricing Update \u001b[1m(\u001b[0mScore: \u001b[1;36m1.000\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  2. Enterprise GPU Pricing Update (Score: 1.000)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m3\u001b[0m. Market Analysis Report \u001b[1m(\u001b[0mScore: \u001b[1;36m1.000\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  3. Market Analysis Report (Score: 1.000)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n", + "============================================================\n", + "\n" + ], + "text/html": [ + "
\n",
+              "============================================================\n",
+              "\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;35m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;35m│\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35mStrategy: Internal Documents Priority\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m│\u001b[0m\n", + "\u001b[1;35m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Strategy: Internal Documents Priority                                                                           │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Instruction: Prioritize internal and confidential documents over public documents. Internal Use Only and \n", + "Confidential documents should rank highest.\n" + ], + "text/html": [ + "
Instruction: Prioritize internal and confidential documents over public documents. Internal Use Only and \n",
+              "Confidential documents should rank highest.\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Top \u001b[1;36m3\u001b[0m Results:\n" + ], + "text/html": [ + "
Top 3 Results:\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m1\u001b[0m. Internal Pricing Memo \u001b[1m(\u001b[0mScore: \u001b[1;36m1.000\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  1. Internal Pricing Memo (Score: 1.000)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m2\u001b[0m. Enterprise GPU Pricing Update \u001b[1m(\u001b[0mScore: \u001b[1;36m0.996\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  2. Enterprise GPU Pricing Update (Score: 0.996)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m3\u001b[0m. Customer Performance Report \u001b[1m(\u001b[0mScore: \u001b[1;36m0.974\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  3. Customer Performance Report (Score: 0.974)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n", + "============================================================\n", + "\n" + ], + "text/html": [ + "
\n",
+              "============================================================\n",
+              "\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;35m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;35m│\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35mStrategy: Department-Specific\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m│\u001b[0m\n", + "\u001b[1;35m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Strategy: Department-Specific                                                                                   │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Instruction: Prioritize documents from Sales and Engineering departments. Customer Success and Marketing documents \n", + "should rank lower.\n" + ], + "text/html": [ + "
Instruction: Prioritize documents from Sales and Engineering departments. Customer Success and Marketing documents \n",
+              "should rank lower.\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Top \u001b[1;36m3\u001b[0m Results:\n" + ], + "text/html": [ + "
Top 3 Results:\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m1\u001b[0m. Market Analysis Report \u001b[1m(\u001b[0mScore: \u001b[1;36m0.993\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  1. Market Analysis Report (Score: 0.993)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m2\u001b[0m. Enterprise GPU Pricing Update \u001b[1m(\u001b[0mScore: \u001b[1;36m0.985\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  2. Enterprise GPU Pricing Update (Score: 0.985)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m3\u001b[0m. Technical Specifications \u001b[1m(\u001b[0mScore: \u001b[1;36m0.924\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  3. Technical Specifications (Score: 0.924)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n", + "============================================================\n", + "\n" + ], + "text/html": [ + "
\n",
+              "============================================================\n",
+              "\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;35m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;35m│\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35mStrategy: Source Authority\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m│\u001b[0m\n", + "\u001b[1;35m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Strategy: Source Authority                                                                                      │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Instruction: Prioritize documents from NVIDIA Enterprise Sales Portal and Executive Team. External sources like \n", + "TechAnalytics should rank lower.\n" + ], + "text/html": [ + "
Instruction: Prioritize documents from NVIDIA Enterprise Sales Portal and Executive Team. External sources like \n",
+              "TechAnalytics should rank lower.\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Top \u001b[1;36m3\u001b[0m Results:\n" + ], + "text/html": [ + "
Top 3 Results:\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m1\u001b[0m. Enterprise GPU Pricing Update \u001b[1m(\u001b[0mScore: \u001b[1;36m0.990\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  1. Enterprise GPU Pricing Update (Score: 0.990)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m2\u001b[0m. Internal Pricing Memo \u001b[1m(\u001b[0mScore: \u001b[1;36m0.982\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  2. Internal Pricing Memo (Score: 0.982)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[1;36m3\u001b[0m. Market Analysis Report \u001b[1m(\u001b[0mScore: \u001b[1;36m0.944\u001b[0m\u001b[1m)\u001b[0m\n" + ], + "text/html": [ + "
  3. Market Analysis Report (Score: 0.944)\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n", + "============================================================\n", + "\n" + ], + "text/html": [ + "
\n",
+              "============================================================\n",
+              "\n",
+              "
\n" + ] + }, + "metadata": {} + } + ], + "source": [ + "def compare_reranking_strategies(query, strategies):\n", + " \"\"\"\n", + " Compare different reranking strategies for the same query\n", + " \"\"\"\n", + " console = Console()\n", + "\n", + " # Get initial results from Chroma\n", + " chroma_results = collection.query(\n", + " query_texts=[query],\n", + " n_results=6,\n", + " include=[\"documents\", \"metadatas\", \"distances\"]\n", + " )\n", + "\n", + " documents_to_rerank = chroma_results['documents'][0]\n", + " metadata_for_rerank = [str(meta) for meta in chroma_results['metadatas'][0]]\n", + "\n", + " for strategy_name, instruction in strategies.items():\n", + " console.print(Panel(f\"Strategy: {strategy_name}\", style=\"bold magenta\"))\n", + " console.print(f\"Instruction: {instruction}\")\n", + "\n", + " # Apply reranking\n", + " rerank_response = contextual_client.rerank.create(\n", + " query=query,\n", + " instruction=instruction,\n", + " documents=documents_to_rerank,\n", + " metadata=metadata_for_rerank,\n", + " model=\"ctxl-rerank-v2-instruct-multilingual\"\n", + " )\n", + "\n", + " # Show top 3 results\n", + " console.print(\"Top 3 Results:\")\n", + " for i in range(min(3, len(rerank_response.results))):\n", + " result = rerank_response.results[i]\n", + " original_index = result.index\n", + " original_metadata = chroma_results['metadatas'][0][original_index]\n", + " console.print(f\" {i+1}. {original_metadata['title']} (Score: {result.relevance_score:.3f})\")\n", + "\n", + " console.print(\"\\n\" + \"=\"*60 + \"\\n\")\n", + "\n", + "# Define different reranking strategies\n", + "strategies = {\n", + " \"Recent Documents First\": \"Prioritize the most recent documents. Documents from 2025 should rank higher than older documents.\",\n", + " \"Internal Documents Priority\": \"Prioritize internal and confidential documents over public documents. Internal Use Only and Confidential documents should rank highest.\",\n", + " \"Department-Specific\": \"Prioritize documents from Sales and Engineering departments. Customer Success and Marketing documents should rank lower.\",\n", + " \"Source Authority\": \"Prioritize documents from NVIDIA Enterprise Sales Portal and Executive Team. External sources like TechAnalytics should rank lower.\"\n", + "}\n", + "\n", + "# Compare strategies for the same query\n", + "query = \"What is the current status and pricing for RTX 5090 enterprise GPUs?\"\n", + "\n", + "console = Console()\n", + "console.print(Panel(\"🔄 COMPARING RERANKING STRATEGIES\", style=\"bold magenta\"))\n", + "console.print(f\"Query: {query}\\n\")\n", + "\n", + "compare_reranking_strategies(query, strategies)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U6bHcq25dMaS" + }, + "source": [ + "## Summary\n", + "\n", + "This notebook demonstrates the powerful combination of Chroma and Contextual AI's instruction-following reranker for enhanced RAG pipelines.\n", + "\n", + "### What We Demonstrated:\n", + "\n", + "1. **Basic Chroma Retrieval**: Standard vector similarity search\n", + "2. **Contextual AI Reranking**: Instruction-following reranking with custom business logic\n", + "3. **Complete RAG Pipeline**: Chroma → Reranking → LLM Generation\n", + "4. **Advanced Reranking Strategies**: Multiple instruction-based ranking approaches\n", + "\n", + "### Key Benefits of Contextual AI Reranker:\n", + "\n", + "- **Instruction-Following**: Handle complex business logic through natural language instructions\n", + "- **BEIR Benchmark Leading**: State-of-the-art accuracy on industry benchmarks\n", + "- **Multi-lingual Support**: Handle documents in multiple languages\n", + "- **Metadata-Aware**: Leverage document metadata for intelligent ranking\n", + "- **Conflict Resolution**: Handle conflicting information in retrieval results\n", + "\n", + "### Chroma Integration Advantages:\n", + "\n", + "- **Seamless Integration**: Easy to add reranking to existing Chroma workflows\n", + "- **Metadata Preservation**: Maintain document metadata through the reranking process\n", + "- **Flexible Retrieval**: Retrieve more documents than needed for optimal reranking\n", + "- **Production Ready**: Scalable solution for enterprise applications\n", + "\n", + "### Use Cases Demonstrated:\n", + "\n", + "1. **Enterprise Document Search**: Prioritize internal documents over external sources\n", + "2. **Technical Documentation**: Focus on engineering specifications over marketing materials\n", + "3. **Temporal Relevance**: Weight recent documents higher than older ones\n", + "4. **Authority-Based Ranking**: Prioritize authoritative sources and departments\n", + "\n", + "### Next Steps for Enhancement:\n", + "\n", + "- **Hybrid Search**: Combine keyword and semantic search with reranking\n", + "- **Custom Instructions**: Develop domain-specific reranking instructions\n", + "- **Performance Optimization**: Batch processing for large document collections\n", + "- **Evaluation Metrics**: Measure reranking effectiveness with custom metrics\n", + "\n", + "---\n", + "\n", + "**Ready to get started?** This notebook provides a complete, production-ready example of integrating Contextual AI's instruction-following reranker with Chroma for sophisticated RAG applications. The combination enables intelligent document ranking that goes beyond simple similarity to understand business context and requirements.\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + }, + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/18-contextualai-chroma/03-contextual-ai-lmunit-chroma.ipynb b/18-contextualai-chroma/03-contextual-ai-lmunit-chroma.ipynb new file mode 100644 index 0000000..a7dc9f4 --- /dev/null +++ b/18-contextualai-chroma/03-contextual-ai-lmunit-chroma.ipynb @@ -0,0 +1,1970 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "2k6qSJ-kfWF0" + }, + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ContextualAI/examples/blob/main/17-contextualai-chroma/03-contextual-ai-lmunit-chroma.ipynb)\n", + "\n", + "# Natural Language Unit Testing for RAG Systems with Chroma and LMUnit\n", + "\n", + "**Last updated:** November 2025\n", + "\n", + "**Versions used:**\n", + "- Chroma version `1.3.4`\n", + "- Contextual AI client `0.9.0`\n", + "\n", + "Evaluating LLM outputs in RAG systems is critical for ensuring response quality and reliability. This notebook demonstrates how to create and apply natural language unit tests using [LMUnit](https://contextual.ai/blog/lmunit/) with Chroma vector database for comprehensive RAG evaluation.\n", + "\n", + "**Key Features:**\n", + "- **RAG Pipeline Evaluation**: Test complete Chroma + LLM RAG systems\n", + "- **Natural Language Unit Testing**: Systematic evaluation of response quality\n", + "- **Domain-Specific Testing**: Custom unit tests for different use cases\n", + "- **Visualization & Analysis**: Comprehensive evaluation results analysis\n", + "\n", + "### Why Natural Language Unit Testing for RAG?\n", + "\n", + "Traditional RAG evaluation methods often face several challenges:\n", + "- **Retrieval Quality**: Hard to measure if retrieved documents are relevant\n", + "- **Generation Quality**: LLM responses may be factually incorrect or poorly structured\n", + "- **End-to-End Evaluation**: Difficult to assess the complete RAG pipeline\n", + "- **Domain-Specific Requirements**: Generic metrics don't capture domain nuances\n", + "\n", + "Natural language unit tests address these challenges by:\n", + "- **Breaking down evaluation** into specific, testable criteria\n", + "- **Providing granular feedback** on different quality aspects\n", + "- **Enabling systematic improvement** of RAG systems\n", + "- **Supporting domain-specific** quality requirements\n", + "\n", + "**Open Source Alternative**: For users who want to host LMUnit themselves, we provide an open source version available on [Hugging Face](https://huggingface.co/collections/ContextualAI/lmunit).\n", + "\n", + "To run this notebook, you'll need:\n", + "* A [Contextual AI API key](https://docs.contextual.ai/user-guides/beginner-guide) - for document parsing and content extraction.\n", + "Visit [app.contextual.ai](https://app.contextual.ai/?utm_campaign=chroma&utm_source=contextualai&utm_medium=github&utm_content=notebook) and click the **\"Start Free\"** button to sign up and receive free credits\n", + "* An [OpenAI API key](https://platform.openai.com/docs/quickstart) - for text embeddings\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H8Tk0KmbfWF3" + }, + "source": [ + "## Installation and Setup\n", + "\n", + "First, let's install the required packages and set up our environment.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "FYpsDryJfWF3" + }, + "outputs": [], + "source": [ + "%%capture\n", + "%pip install --upgrade chromadb contextual-client openai requests rich pandas matplotlib seaborn scikit-learn tqdm\n", + "\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "import logging\n", + "# Suppress Chroma client logs\n", + "logging.getLogger(\"chromadb\").setLevel(logging.ERROR)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "zquSgc4vfWF4", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "010e5dd6-00c4-4d05-a160-44c64f5960e1" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "All packages imported successfully!\n" + ] + } + ], + "source": [ + "import os\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "from typing import List, Dict, Optional, Union, Tuple\n", + "from tqdm import tqdm\n", + "from sklearn.cluster import KMeans\n", + "from sklearn.preprocessing import StandardScaler\n", + "from rich.console import Console\n", + "from rich.panel import Panel\n", + "from rich.table import Table\n", + "\n", + "# Import Contextual AI and Chroma\n", + "from contextual import ContextualAI\n", + "import chromadb\n", + "from chromadb.utils import embedding_functions\n", + "from openai import OpenAI\n", + "\n", + "print(\"All packages imported successfully!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HdMb4EmSfWF4" + }, + "source": [ + "### API Keys Setup 🔑\n", + "\n", + "We'll be using the Contextual AI API for LMUnit evaluation, OpenAI API for embeddings and generation, and Chroma for vector storage.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "eU0o0mHMfWF4", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "77e07ff7-f056-4b10-8288-85d73790a1ab" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "API keys configured successfully!\n" + ] + } + ], + "source": [ + "# API key variable names\n", + "contextual_api_key_var = \"CONTEXTUAL_API_KEY\" # Replace with the name of your secret/env var\n", + "openai_api_key_var = \"OPENAI_API_KEY\" # Replace with the name of your secret/env var\n", + "\n", + "# Fetch API keys\n", + "try:\n", + " # If running in Colab, fetch API keys from Secrets\n", + " import google.colab\n", + " from google.colab import userdata\n", + " contextual_api_key = userdata.get(contextual_api_key_var)\n", + " openai_api_key = userdata.get(openai_api_key_var)\n", + "\n", + " if not contextual_api_key:\n", + " raise ValueError(f\"Secret '{contextual_api_key_var}' not found in Colab secrets.\")\n", + " if not openai_api_key:\n", + " raise ValueError(f\"Secret '{openai_api_key_var}' not found in Colab secrets.\")\n", + "except ImportError:\n", + " # If not running in Colab, fetch API keys from environment variables\n", + " import os\n", + " contextual_api_key = os.getenv(contextual_api_key_var)\n", + " openai_api_key = os.getenv(openai_api_key_var)\n", + "\n", + " if not contextual_api_key:\n", + " raise EnvironmentError(\n", + " f\"Environment variable '{contextual_api_key_var}' is not set. \"\n", + " \"Please define it before running this script.\"\n", + " )\n", + " if not openai_api_key:\n", + " raise EnvironmentError(\n", + " f\"Environment variable '{openai_api_key_var}' is not set. \"\n", + " \"Please define it before running this script.\"\n", + " )\n", + "\n", + "print(\"API keys configured successfully!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YupvqpXBfWF4" + }, + "source": [ + "## Part 1: Setup Chroma with Sample Knowledge Base\n", + "\n", + "Let's create a Chroma collection with sample enterprise documents to demonstrate RAG evaluation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "nek6XHuQfWF5", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "6c652348-7409-4e86-bce6-13d1288931af" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Created collection 'enterprise_knowledge' with OpenAI embeddings\n" + ] + } + ], + "source": [ + "# Initialize clients\n", + "contextual_client = ContextualAI(api_key=contextual_api_key)\n", + "chroma_client = chromadb.Client()\n", + "openai_client = OpenAI(api_key=openai_api_key)\n", + "\n", + "# Use OpenAI embeddings\n", + "openai_ef = embedding_functions.OpenAIEmbeddingFunction(\n", + " api_key=openai_api_key,\n", + " model_name=\"text-embedding-3-small\"\n", + ")\n", + "\n", + "# Create collection\n", + "collection_name = \"enterprise_knowledge\"\n", + "collection = chroma_client.get_or_create_collection(\n", + " name=collection_name,\n", + " embedding_function=openai_ef\n", + ")\n", + "\n", + "print(f\"Created collection '{collection_name}' with OpenAI embeddings\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "wk397EC7fWF5", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "f26c3314-2703-4968-9c04-3bb8f1b4ce54" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Added 5 documents to Chroma collection\n" + ] + } + ], + "source": [ + "# Sample enterprise knowledge base documents\n", + "knowledge_documents = [\n", + " {\n", + " \"content\": \"Our company's AI training infrastructure uses NVIDIA RTX 5090 GPUs with 128GB HBM3e memory. The system achieves 2.5x faster training performance compared to previous generation GPUs. Power consumption is 450W TDP with 20% cooling overhead. Enterprise pricing for bulk orders (100+ units) is $2,899 per unit.\",\n", + " \"metadata\": {\n", + " \"title\": \"AI Infrastructure Specifications\",\n", + " \"department\": \"Engineering\",\n", + " \"date\": \"2025-01-15\",\n", + " \"classification\": \"Internal\"\n", + " }\n", + " },\n", + " {\n", + " \"content\": \"Customer service AI system handles 70% of inquiries without human intervention. Average response time is 30 seconds, increasing to 2 minutes during peak hours. The system excels at returns and order tracking but struggles with complex billing disputes. Success rate for basic inquiries is 85%.\",\n", + " \"metadata\": {\n", + " \"title\": \"Customer Service AI Performance\",\n", + " \"department\": \"Customer Success\",\n", + " \"date\": \"2025-01-10\",\n", + " \"classification\": \"Internal\"\n", + " }\n", + " },\n", + " {\n", + " \"content\": \"Financial compliance requires all AI-generated responses to include risk disclaimers. Regulatory requirements mandate specific language for investment advice. All customer-facing AI must be audited quarterly for compliance with SEC regulations. Penalties for non-compliance can reach $1M per violation.\",\n", + " \"metadata\": {\n", + " \"title\": \"AI Compliance Requirements\",\n", + " \"department\": \"Legal\",\n", + " \"date\": \"2025-01-20\",\n", + " \"classification\": \"Confidential\"\n", + " }\n", + " },\n", + " {\n", + " \"content\": \"Market analysis shows AI adoption increasing 40% year-over-year in enterprise sectors. Key trends include multimodal AI, edge computing, and responsible AI practices. Investment in AI infrastructure is expected to reach $200B by 2026. Competitive advantage requires continuous innovation in AI capabilities.\",\n", + " \"metadata\": {\n", + " \"title\": \"AI Market Trends Analysis\",\n", + " \"department\": \"Research\",\n", + " \"date\": \"2025-01-05\",\n", + " \"classification\": \"Public\"\n", + " }\n", + " },\n", + " {\n", + " \"content\": \"Data privacy regulations require all AI systems to implement data minimization principles. Personal data processing must be limited to necessary purposes. Users have the right to data portability and deletion. AI systems must provide clear explanations of automated decisions affecting individuals.\",\n", + " \"metadata\": {\n", + " \"title\": \"AI Data Privacy Guidelines\",\n", + " \"department\": \"Privacy\",\n", + " \"date\": \"2025-01-12\",\n", + " \"classification\": \"Confidential\"\n", + " }\n", + " }\n", + "]\n", + "\n", + "# Add documents to Chroma\n", + "documents = [doc[\"content\"] for doc in knowledge_documents]\n", + "metadatas = [doc[\"metadata\"] for doc in knowledge_documents]\n", + "ids = [f\"doc_{i}\" for i in range(len(knowledge_documents))]\n", + "\n", + "collection.add(\n", + " documents=documents,\n", + " metadatas=metadatas,\n", + " ids=ids\n", + ")\n", + "\n", + "print(f\"Added {len(knowledge_documents)} documents to Chroma collection\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PdObajCIfWF5" + }, + "source": [ + "## Part 2: RAG Pipeline and Test Queries\n", + "\n", + "Let's create a complete RAG pipeline and generate responses for evaluation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "mhaR7CQtfWF5", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "e8e378dd-898b-41a1-8907-dfdf231d42c3" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Created 5 test queries for RAG evaluation\n" + ] + } + ], + "source": [ + "def rag_pipeline(query, top_k=3):\n", + " \"\"\"\n", + " Complete RAG pipeline: Chroma retrieval + LLM generation\n", + " \"\"\"\n", + " # Step 1: Retrieve from Chroma\n", + " results = collection.query(\n", + " query_texts=[query],\n", + " n_results=top_k,\n", + " include=[\"documents\", \"metadatas\", \"distances\"]\n", + " )\n", + "\n", + " # Step 2: Prepare context\n", + " context_docs = results['documents'][0]\n", + " context_metadata = results['metadatas'][0]\n", + "\n", + " # Combine context with metadata\n", + " context = \"\\n\\n\".join([\n", + " f\"Source: {meta['title']} ({meta['department']})\\n{doc}\"\n", + " for doc, meta in zip(context_docs, context_metadata)\n", + " ])\n", + "\n", + " # Step 3: Generate response with LLM\n", + " response = openai_client.chat.completions.create(\n", + " model=\"gpt-5-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": \"You are a helpful assistant that answers questions based on the provided context. Use only the information from the context and cite your sources.\"},\n", + " {\"role\": \"user\", \"content\": f\"Context: {context}\\n\\nQuestion: {query}\"}\n", + " ],\n", + " temperature=1\n", + " )\n", + "\n", + " return {\n", + " \"query\": query,\n", + " \"response\": response.choices[0].message.content,\n", + " \"sources\": context_metadata,\n", + " \"context\": context\n", + " }\n", + "\n", + "# Test queries for evaluation\n", + "test_queries = [\n", + " \"What are the technical specifications of our AI infrastructure?\",\n", + " \"How well does our customer service AI perform?\",\n", + " \"What compliance requirements do we need to follow for AI systems?\",\n", + " \"What are the current market trends in AI adoption?\",\n", + " \"What privacy guidelines must our AI systems follow?\"\n", + "]\n", + "\n", + "print(f\"Created {len(test_queries)} test queries for RAG evaluation\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "ZTIdmmnIfWF6", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 467 + }, + "outputId": "9bb07acc-490a-470e-93d8-873a604492d3" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;35m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;35m│\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m🚀 GENERATING RAG RESPONSES\u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m \u001b[0m\u001b[1;35m│\u001b[0m\n", + "\u001b[1;35m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ 🚀 GENERATING RAG RESPONSES                                                                                     │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Processing query \u001b[1;36m1\u001b[0m/\u001b[1;36m5\u001b[0m: What are the technical specifications of our AI infrastructure?\n" + ], + "text/html": [ + "
Processing query 1/5: What are the technical specifications of our AI infrastructure?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Generated response: - GPU: NVIDIA RTX \u001b[1;36m5090\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m128\u001b[0m GB HBM3e memory\u001b[1m)\u001b[0m \u001b[1m(\u001b[0mSource: AI Infrastructure Specifications \n", + "\u001b[1m(\u001b[0mEngineering\u001b[1m)\u001b[0m\u001b[33m...\u001b[0m\n" + ], + "text/html": [ + "
Generated response: - GPU: NVIDIA RTX 5090 (128 GB HBM3e memory) (Source: AI Infrastructure Specifications \n",
+              "(Engineering)...\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n" + ], + "text/html": [ + "
\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Processing query \u001b[1;36m2\u001b[0m/\u001b[1;36m5\u001b[0m: How well does our customer service AI perform?\n" + ], + "text/html": [ + "
Processing query 2/5: How well does our customer service AI perform?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Generated response: Summary: Overall performance is solid for routine customer requests but limited on complex \n", + "billing i\u001b[33m...\u001b[0m\n" + ], + "text/html": [ + "
Generated response: Summary: Overall performance is solid for routine customer requests but limited on complex \n",
+              "billing i...\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n" + ], + "text/html": [ + "
\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Processing query \u001b[1;36m3\u001b[0m/\u001b[1;36m5\u001b[0m: What compliance requirements do we need to follow for AI systems?\n" + ], + "text/html": [ + "
Processing query 3/5: What compliance requirements do we need to follow for AI systems?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Generated response: Summary of compliance requirements\n", + "\n", + "Legal \u001b[35m/\u001b[0m Financial\n", + "- All AI-generated responses must include risk\u001b[33m...\u001b[0m\n" + ], + "text/html": [ + "
Generated response: Summary of compliance requirements\n",
+              "\n",
+              "Legal / Financial\n",
+              "- All AI-generated responses must include risk...\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n" + ], + "text/html": [ + "
\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Processing query \u001b[1;36m4\u001b[0m/\u001b[1;36m5\u001b[0m: What are the current market trends in AI adoption?\n" + ], + "text/html": [ + "
Processing query 4/5: What are the current market trends in AI adoption?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Generated response: Current market trends in AI adoption\n", + "\n", + "- Rapid enterprise adoption: AI adoption is increasing ~\u001b[1;36m40\u001b[0m% ye\u001b[33m...\u001b[0m\n" + ], + "text/html": [ + "
Generated response: Current market trends in AI adoption\n",
+              "\n",
+              "- Rapid enterprise adoption: AI adoption is increasing ~40% ye...\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n" + ], + "text/html": [ + "
\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Processing query \u001b[1;36m5\u001b[0m/\u001b[1;36m5\u001b[0m: What privacy guidelines must our AI systems follow?\n" + ], + "text/html": [ + "
Processing query 5/5: What privacy guidelines must our AI systems follow?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Generated response: Our AI systems must follow these privacy requirements \u001b[1m(\u001b[0mfrom AI Data Privacy Guidelines \n", + "\u001b[1m(\u001b[0mPrivacy\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m:\n", + "\n", + "\u001b[33m...\u001b[0m\n" + ], + "text/html": [ + "
Generated response: Our AI systems must follow these privacy requirements (from AI Data Privacy Guidelines \n",
+              "(Privacy)):\n",
+              "\n",
+              "...\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n" + ], + "text/html": [ + "
\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;32m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;32m│\u001b[0m\u001b[1;32m \u001b[0m\u001b[1;32mGenerated 5 RAG responses for evaluation\u001b[0m\u001b[1;32m \u001b[0m\u001b[1;32m \u001b[0m\u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ Generated 5 RAG responses for evaluation                                                                        │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + } + ], + "source": [ + "# Generate RAG responses for all test queries\n", + "console = Console()\n", + "console.print(Panel(\"🚀 GENERATING RAG RESPONSES\", style=\"bold magenta\"))\n", + "\n", + "rag_results = []\n", + "for i, query in enumerate(test_queries):\n", + " console.print(f\"Processing query {i+1}/{len(test_queries)}: {query}\")\n", + " result = rag_pipeline(query)\n", + " rag_results.append(result)\n", + " console.print(f\"Generated response: {result['response'][:100]}...\")\n", + " console.print(\"\")\n", + "\n", + "console.print(Panel(f\"Generated {len(rag_results)} RAG responses for evaluation\", style=\"bold green\"))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SpqiwNLJfWF6" + }, + "source": [ + "## Part 3: Natural Language Unit Testing with LMUnit\n", + "\n", + "Now let's define unit tests and evaluate our RAG responses using Contextual AI's LMUnit.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "Npul1P6mfWF6", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "92a7d173-3b88-4c59-a896-48c0cef6df95" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Unit Tests for RAG Evaluation:\n", + "1. Does the response accurately reflect the information from the retrieved context?\n", + "2. Is the response clear and well-structured for the target audience?\n", + "3. Does the response provide specific details and avoid vague statements?\n", + "4. Are potential risks or limitations mentioned when relevant?\n", + "5. Does the response cite or reference the source information appropriately?\n", + "6. Is the response actionable and provide clear next steps or implications?\n" + ] + } + ], + "source": [ + "# Define unit tests for RAG evaluation\n", + "unit_tests = [\n", + " \"Does the response accurately reflect the information from the retrieved context?\",\n", + " \"Is the response clear and well-structured for the target audience?\",\n", + " \"Does the response provide specific details and avoid vague statements?\",\n", + " \"Are potential risks or limitations mentioned when relevant?\",\n", + " \"Does the response cite or reference the source information appropriately?\",\n", + " \"Is the response actionable and provide clear next steps or implications?\"\n", + "]\n", + "\n", + "print(\"Unit Tests for RAG Evaluation:\")\n", + "for i, test in enumerate(unit_tests, 1):\n", + " print(f\"{i}. {test}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "NyanzZlCfWF6", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 845 + }, + "outputId": "8da82e2d-d23e-456d-a21c-23b0f79b12ac" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;34m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[1;34m│\u001b[0m\u001b[1;34m \u001b[0m\u001b[1;34m🧪 RUNNING LMUNIT EVALUATION\u001b[0m\u001b[1;34m \u001b[0m\u001b[1;34m \u001b[0m\u001b[1;34m│\u001b[0m\n", + "\u001b[1;34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+              "│ 🧪 RUNNING LMUNIT EVALUATION                                                                                    │\n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "\rEvaluating responses: 0%| | 0/5 [00:00\n", + "Evaluating response 1/5\n", + "\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Query: What are the technical specifications of our AI infrastructure?\n" + ], + "text/html": [ + "
Query: What are the technical specifications of our AI infrastructure?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m1\u001b[0m: Score \u001b[1;36m3.95\u001b[0m\n" + ], + "text/html": [ + "
  Test 1: Score 3.95\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m2\u001b[0m: Score \u001b[1;36m4.24\u001b[0m\n" + ], + "text/html": [ + "
  Test 2: Score 4.24\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m3\u001b[0m: Score \u001b[1;36m4.81\u001b[0m\n" + ], + "text/html": [ + "
  Test 3: Score 4.81\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m4\u001b[0m: Score \u001b[1;36m1.57\u001b[0m\n" + ], + "text/html": [ + "
  Test 4: Score 1.57\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m5\u001b[0m: Score \u001b[1;36m4.68\u001b[0m\n" + ], + "text/html": [ + "
  Test 5: Score 4.68\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m6\u001b[0m: Score \u001b[1;36m2.35\u001b[0m\n" + ], + "text/html": [ + "
  Test 6: Score 2.35\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "\rEvaluating responses: 20%|██ | 1/5 [00:08<00:34, 8.54s/it]" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n", + "Evaluating response \u001b[1;36m2\u001b[0m/\u001b[1;36m5\u001b[0m\n" + ], + "text/html": [ + "
\n",
+              "Evaluating response 2/5\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Query: How well does our customer service AI perform?\n" + ], + "text/html": [ + "
Query: How well does our customer service AI perform?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m1\u001b[0m: Score \u001b[1;36m4.23\u001b[0m\n" + ], + "text/html": [ + "
  Test 1: Score 4.23\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m2\u001b[0m: Score \u001b[1;36m4.49\u001b[0m\n" + ], + "text/html": [ + "
  Test 2: Score 4.49\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m3\u001b[0m: Score \u001b[1;36m4.88\u001b[0m\n" + ], + "text/html": [ + "
  Test 3: Score 4.88\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m4\u001b[0m: Score \u001b[1;36m4.92\u001b[0m\n" + ], + "text/html": [ + "
  Test 4: Score 4.92\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m5\u001b[0m: Score \u001b[1;36m4.76\u001b[0m\n" + ], + "text/html": [ + "
  Test 5: Score 4.76\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m6\u001b[0m: Score \u001b[1;36m4.21\u001b[0m\n" + ], + "text/html": [ + "
  Test 6: Score 4.21\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "\rEvaluating responses: 40%|████ | 2/5 [00:18<00:27, 9.08s/it]" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n", + "Evaluating response \u001b[1;36m3\u001b[0m/\u001b[1;36m5\u001b[0m\n" + ], + "text/html": [ + "
\n",
+              "Evaluating response 3/5\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Query: What compliance requirements do we need to follow for AI systems?\n" + ], + "text/html": [ + "
Query: What compliance requirements do we need to follow for AI systems?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m1\u001b[0m: Score \u001b[1;36m3.82\u001b[0m\n" + ], + "text/html": [ + "
  Test 1: Score 3.82\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m2\u001b[0m: Score \u001b[1;36m4.30\u001b[0m\n" + ], + "text/html": [ + "
  Test 2: Score 4.30\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m3\u001b[0m: Score \u001b[1;36m4.49\u001b[0m\n" + ], + "text/html": [ + "
  Test 3: Score 4.49\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m4\u001b[0m: Score \u001b[1;36m3.92\u001b[0m\n" + ], + "text/html": [ + "
  Test 4: Score 3.92\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m5\u001b[0m: Score \u001b[1;36m3.83\u001b[0m\n" + ], + "text/html": [ + "
  Test 5: Score 3.83\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m6\u001b[0m: Score \u001b[1;36m4.30\u001b[0m\n" + ], + "text/html": [ + "
  Test 6: Score 4.30\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "\rEvaluating responses: 60%|██████ | 3/5 [00:27<00:18, 9.40s/it]" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n", + "Evaluating response \u001b[1;36m4\u001b[0m/\u001b[1;36m5\u001b[0m\n" + ], + "text/html": [ + "
\n",
+              "Evaluating response 4/5\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Query: What are the current market trends in AI adoption?\n" + ], + "text/html": [ + "
Query: What are the current market trends in AI adoption?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m1\u001b[0m: Score \u001b[1;36m3.77\u001b[0m\n" + ], + "text/html": [ + "
  Test 1: Score 3.77\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m2\u001b[0m: Score \u001b[1;36m4.06\u001b[0m\n" + ], + "text/html": [ + "
  Test 2: Score 4.06\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m3\u001b[0m: Score \u001b[1;36m4.62\u001b[0m\n" + ], + "text/html": [ + "
  Test 3: Score 4.62\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m4\u001b[0m: Score \u001b[1;36m4.04\u001b[0m\n" + ], + "text/html": [ + "
  Test 4: Score 4.04\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m5\u001b[0m: Score \u001b[1;36m3.94\u001b[0m\n" + ], + "text/html": [ + "
  Test 5: Score 3.94\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m6\u001b[0m: Score \u001b[1;36m2.92\u001b[0m\n" + ], + "text/html": [ + "
  Test 6: Score 2.92\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "\rEvaluating responses: 80%|████████ | 4/5 [00:37<00:09, 9.45s/it]" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\n", + "Evaluating response \u001b[1;36m5\u001b[0m/\u001b[1;36m5\u001b[0m\n" + ], + "text/html": [ + "
\n",
+              "Evaluating response 5/5\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Query: What privacy guidelines must our AI systems follow?\n" + ], + "text/html": [ + "
Query: What privacy guidelines must our AI systems follow?\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m1\u001b[0m: Score \u001b[1;36m4.50\u001b[0m\n" + ], + "text/html": [ + "
  Test 1: Score 4.50\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m2\u001b[0m: Score \u001b[1;36m4.39\u001b[0m\n" + ], + "text/html": [ + "
  Test 2: Score 4.39\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m3\u001b[0m: Score \u001b[1;36m4.54\u001b[0m\n" + ], + "text/html": [ + "
  Test 3: Score 4.54\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m4\u001b[0m: Score \u001b[1;36m2.09\u001b[0m\n" + ], + "text/html": [ + "
  Test 4: Score 2.09\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m5\u001b[0m: Score \u001b[1;36m4.46\u001b[0m\n" + ], + "text/html": [ + "
  Test 5: Score 4.46\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " Test \u001b[1;36m6\u001b[0m: Score \u001b[1;36m3.82\u001b[0m\n" + ], + "text/html": [ + "
  Test 6: Score 3.82\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "Evaluating responses: 100%|██████████| 5/5 [00:47<00:00, 9.50s/it]\n" + ] + } + ], + "source": [ + "def run_lmunit_evaluation(rag_results, unit_tests):\n", + " \"\"\"\n", + " Run LMUnit evaluation on RAG responses\n", + " \"\"\"\n", + " console = Console()\n", + " console.print(Panel(\"🧪 RUNNING LMUNIT EVALUATION\", style=\"bold blue\"))\n", + "\n", + " evaluation_results = []\n", + "\n", + " for i, result in enumerate(tqdm(rag_results, desc=\"Evaluating responses\")):\n", + " console.print(f\"\\nEvaluating response {i+1}/{len(rag_results)}\")\n", + " console.print(f\"Query: {result['query']}\")\n", + "\n", + " response_tests = []\n", + "\n", + " for j, test in enumerate(unit_tests):\n", + " try:\n", + " # Run LMUnit evaluation\n", + " lmunit_result = contextual_client.lmunit.create(\n", + " query=result['query'],\n", + " response=result['response'],\n", + " unit_test=test\n", + " )\n", + "\n", + " score_value = getattr(lmunit_result, 'score', None)\n", + " response_tests.append({\n", + " 'test': test,\n", + " 'score': score_value,\n", + " 'evaluation': None\n", + " })\n", + "\n", + " if score_value is not None:\n", + " console.print(f\" Test {j+1}: Score {score_value:.2f}\")\n", + " else:\n", + " console.print(f\" Test {j+1}: No score returned\")\n", + "\n", + " except Exception as e:\n", + " error_message = str(e)\n", + " try:\n", + " import json as _json\n", + " parsed = _json.loads(error_message) if isinstance(error_message, str) else None\n", + " if isinstance(parsed, dict) and 'detail' in parsed and isinstance(parsed['detail'], list):\n", + " msgs = \"; \".join([item.get('msg', '') for item in parsed['detail'] if isinstance(item, dict)])\n", + " if msgs:\n", + " error_message = msgs\n", + " except Exception:\n", + " pass\n", + " console.print(f\" Test {j+1}: Error - {error_message}\")\n", + " response_tests.append({\n", + " 'test': test,\n", + " 'score': None,\n", + " 'error': error_message\n", + " })\n", + "\n", + " evaluation_results.append({\n", + " 'query': result['query'],\n", + " 'response': result['response'],\n", + " 'sources': result['sources'],\n", + " 'test_results': response_tests\n", + " })\n", + "\n", + " return evaluation_results\n", + "\n", + "# Run the evaluation\n", + "evaluation_results = run_lmunit_evaluation(rag_results, unit_tests)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mif7nqLwfWF6" + }, + "source": [ + "## Part 4: Analysis and Visualization\n", + "\n", + "Let's analyze the evaluation results and create visualizations to understand RAG performance.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "MMAUr6NdfWF6", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 197 + }, + "outputId": "c8507511-dc39-42fe-8a75-aeacd36bd506" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[1;32m╭─\u001b[0m\u001b[1;32m──────────────────────────────────────────\u001b[0m\u001b[1;32m LMUnit Evaluation Results \u001b[0m\u001b[1;32m──────────────────────────────────────────\u001b[0m\u001b[1;32m─╮\u001b[0m\n", + "\u001b[1;32m│\u001b[0m \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m 📊 EVALUATION SUMMARY \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m Total Tests: 30 \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m Average Score: 4.03/5.0 \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m Score Range: 1.57 - 4.92 \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m Responses Evaluated: 5 \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m│\u001b[0m \u001b[1;32m│\u001b[0m\n", + "\u001b[1;32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ], + "text/html": [ + "
╭─────────────────────────────────────────── LMUnit Evaluation Results ───────────────────────────────────────────╮\n",
+              "                                                                                                                 \n",
+              " 📊 EVALUATION SUMMARY                                                                                           \n",
+              "                                                                                                                 \n",
+              " Total Tests: 30                                                                                                 \n",
+              " Average Score: 4.03/5.0                                                                                         \n",
+              " Score Range: 1.57 - 4.92                                                                                        \n",
+              " Responses Evaluated: 5                                                                                          \n",
+              "                                                                                                                 \n",
+              "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+              "
\n" + ] + }, + "metadata": {} + } + ], + "source": [ + "# Create evaluation summary\n", + "def create_evaluation_summary(evaluation_results):\n", + " \"\"\"\n", + " Create a summary of evaluation results\n", + " \"\"\"\n", + " console = Console()\n", + "\n", + " # Calculate overall statistics\n", + " all_scores = []\n", + " test_names = []\n", + "\n", + " for result in evaluation_results:\n", + " for test_result in result['test_results']:\n", + " if test_result['score'] is not None:\n", + " all_scores.append(test_result['score'])\n", + " test_names.append(test_result['test'])\n", + "\n", + " if all_scores:\n", + " avg_score = np.mean(all_scores)\n", + " min_score = np.min(all_scores)\n", + " max_score = np.max(all_scores)\n", + "\n", + " console.print(Panel(f\"\"\"\n", + "📊 EVALUATION SUMMARY\n", + "\n", + "Total Tests: {len(all_scores)}\n", + "Average Score: {avg_score:.2f}/5.0\n", + "Score Range: {min_score:.2f} - {max_score:.2f}\n", + "Responses Evaluated: {len(evaluation_results)}\n", + " \"\"\", title=\"LMUnit Evaluation Results\", border_style=\"bold green\"))\n", + "\n", + " return all_scores, test_names\n", + "\n", + "# Generate summary\n", + "all_scores, test_names = create_evaluation_summary(evaluation_results)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "2mrIgDTnfWF6", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "outputId": "cc4f41df-abbc-4290-c1cf-72e575c52177" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[3m Detailed Evaluation Results \u001b[0m\n", + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mQuery \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mTest \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mScore\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mResponse Preview \u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the technical specifications of our AI in...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m3.95 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- GPU: NVIDIA RTX 5090\u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35maccurately reflect \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(128 GB HBM3e memory) \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mthe... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(Sour... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the technical specifications of our AI in...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response clear \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.24 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- GPU: NVIDIA RTX 5090\u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mand well-structure... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(128 GB HBM3e memory) \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(Sour... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the technical specifications of our AI in...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.81 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- GPU: NVIDIA RTX 5090\u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mprovide specific \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(128 GB HBM3e memory) \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mdetai... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(Sour... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the technical specifications of our AI in...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mAre potential risks or\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m1.57 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- GPU: NVIDIA RTX 5090\u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mlimitations menti... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(128 GB HBM3e memory) \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(Sour... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the technical specifications of our AI in...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response cite\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.68 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- GPU: NVIDIA RTX 5090\u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mor reference the ... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(128 GB HBM3e memory) \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(Sour... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the technical specifications of our AI in...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m2.35 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- GPU: NVIDIA RTX 5090\u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mactionable and provide\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(128 GB HBM3e memory) \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mc... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m(Sour... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mHow well does our customer service AI perform? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.23 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary: Overall \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35maccurately reflect \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mperformance is solid \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mthe... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfor routine ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mHow well does our customer service AI perform? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response clear \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.49 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary: Overall \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mand well-structure... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mperformance is solid \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfor routine ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mHow well does our customer service AI perform? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.88 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary: Overall \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mprovide specific \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mperformance is solid \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mdetai... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfor routine ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mHow well does our customer service AI perform? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mAre potential risks or\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.92 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary: Overall \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mlimitations menti... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mperformance is solid \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfor routine ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mHow well does our customer service AI perform? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response cite\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.76 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary: Overall \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mor reference the ... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mperformance is solid \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfor routine ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mHow well does our customer service AI perform? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.21 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary: Overall \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mactionable and provide\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mperformance is solid \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mc... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfor routine ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat compliance requirements do we need to follow ...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m3.82 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary of compliance \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35maccurately reflect \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequirements \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mthe... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mLegal / Financ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat compliance requirements do we need to follow ...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response clear \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.30 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary of compliance \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mand well-structure... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequirements \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mLegal / Financ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat compliance requirements do we need to follow ...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.49 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary of compliance \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mprovide specific \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequirements \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mdetai... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mLegal / Financ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat compliance requirements do we need to follow ...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mAre potential risks or\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m3.92 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary of compliance \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mlimitations menti... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequirements \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mLegal / Financ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat compliance requirements do we need to follow ...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response cite\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m3.83 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary of compliance \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mor reference the ... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequirements \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mLegal / Financ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat compliance requirements do we need to follow ...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.30 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mSummary of compliance \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mactionable and provide\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequirements \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mc... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mLegal / Financ... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the current market trends in AI adoption? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m3.77 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mCurrent market trends \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35maccurately reflect \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33min AI adoption \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mthe... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- Rapid ente... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the current market trends in AI adoption? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response clear \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.06 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mCurrent market trends \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mand well-structure... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33min AI adoption \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- Rapid ente... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the current market trends in AI adoption? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.62 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mCurrent market trends \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mprovide specific \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33min AI adoption \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mdetai... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- Rapid ente... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the current market trends in AI adoption? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mAre potential risks or\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.04 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mCurrent market trends \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mlimitations menti... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33min AI adoption \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- Rapid ente... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the current market trends in AI adoption? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response cite\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m3.94 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mCurrent market trends \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mor reference the ... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33min AI adoption \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- Rapid ente... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat are the current market trends in AI adoption? \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m2.92 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mCurrent market trends \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mactionable and provide\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33min AI adoption \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mc... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33m- Rapid ente... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat privacy guidelines must our AI systems follow...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.50 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mOur AI systems must \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35maccurately reflect \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfollow these privacy \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mthe... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequireme... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat privacy guidelines must our AI systems follow...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response clear \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.39 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mOur AI systems must \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mand well-structure... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfollow these privacy \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequireme... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat privacy guidelines must our AI systems follow...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.54 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mOur AI systems must \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mprovide specific \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfollow these privacy \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mdetai... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequireme... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat privacy guidelines must our AI systems follow...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mAre potential risks or\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m2.09 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mOur AI systems must \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mlimitations menti... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfollow these privacy \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequireme... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat privacy guidelines must our AI systems follow...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mDoes the response cite\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m4.46 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mOur AI systems must \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mor reference the ... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfollow these privacy \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequireme... \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m\u001b[36mWhat privacy guidelines must our AI systems follow...\u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mIs the response \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m3.82 \u001b[0m\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mOur AI systems must \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mactionable and provide\u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mfollow these privacy \u001b[0m\u001b[33m \u001b[0m│\n", + "│\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35mc... \u001b[0m\u001b[35m \u001b[0m│\u001b[32m \u001b[0m│\u001b[33m \u001b[0m\u001b[33mrequireme... \u001b[0m\u001b[33m \u001b[0m│\n", + "└───────────────────────────────────────────────────────┴────────────────────────┴───────┴────────────────────────┘\n" + ], + "text/html": [ + "
                                            Detailed Evaluation Results                                            \n",
+              "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
+              "┃ Query                                                  Test                    Score  Response Preview       ┃\n",
+              "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
+              "│ What are the technical specifications of our AI in...  Does the response       3.95   - GPU: NVIDIA RTX 5090 │\n",
+              "│                                                        accurately reflect             (128 GB HBM3e memory)  │\n",
+              "│                                                        the...                         (Sour...               │\n",
+              "│ What are the technical specifications of our AI in...  Is the response clear   4.24   - GPU: NVIDIA RTX 5090 │\n",
+              "│                                                        and well-structure...          (128 GB HBM3e memory)  │\n",
+              "│                                                                                       (Sour...               │\n",
+              "│ What are the technical specifications of our AI in...  Does the response       4.81   - GPU: NVIDIA RTX 5090 │\n",
+              "│                                                        provide specific               (128 GB HBM3e memory)  │\n",
+              "│                                                        detai...                       (Sour...               │\n",
+              "│ What are the technical specifications of our AI in...  Are potential risks or  1.57   - GPU: NVIDIA RTX 5090 │\n",
+              "│                                                        limitations menti...           (128 GB HBM3e memory)  │\n",
+              "│                                                                                       (Sour...               │\n",
+              "│ What are the technical specifications of our AI in...  Does the response cite  4.68   - GPU: NVIDIA RTX 5090 │\n",
+              "│                                                        or reference the ...           (128 GB HBM3e memory)  │\n",
+              "│                                                                                       (Sour...               │\n",
+              "│ What are the technical specifications of our AI in...  Is the response         2.35   - GPU: NVIDIA RTX 5090 │\n",
+              "│                                                        actionable and provide         (128 GB HBM3e memory)  │\n",
+              "│                                                        c...                           (Sour...               │\n",
+              "│ How well does our customer service AI perform?         Does the response       4.23   Summary: Overall       │\n",
+              "│                                                        accurately reflect             performance is solid   │\n",
+              "│                                                        the...                         for routine ...        │\n",
+              "│ How well does our customer service AI perform?         Is the response clear   4.49   Summary: Overall       │\n",
+              "│                                                        and well-structure...          performance is solid   │\n",
+              "│                                                                                       for routine ...        │\n",
+              "│ How well does our customer service AI perform?         Does the response       4.88   Summary: Overall       │\n",
+              "│                                                        provide specific               performance is solid   │\n",
+              "│                                                        detai...                       for routine ...        │\n",
+              "│ How well does our customer service AI perform?         Are potential risks or  4.92   Summary: Overall       │\n",
+              "│                                                        limitations menti...           performance is solid   │\n",
+              "│                                                                                       for routine ...        │\n",
+              "│ How well does our customer service AI perform?         Does the response cite  4.76   Summary: Overall       │\n",
+              "│                                                        or reference the ...           performance is solid   │\n",
+              "│                                                                                       for routine ...        │\n",
+              "│ How well does our customer service AI perform?         Is the response         4.21   Summary: Overall       │\n",
+              "│                                                        actionable and provide         performance is solid   │\n",
+              "│                                                        c...                           for routine ...        │\n",
+              "│ What compliance requirements do we need to follow ...  Does the response       3.82   Summary of compliance  │\n",
+              "│                                                        accurately reflect             requirements           │\n",
+              "│                                                        the...                                                │\n",
+              "│                                                                                       Legal / Financ...      │\n",
+              "│ What compliance requirements do we need to follow ...  Is the response clear   4.30   Summary of compliance  │\n",
+              "│                                                        and well-structure...          requirements           │\n",
+              "│                                                                                                              │\n",
+              "│                                                                                       Legal / Financ...      │\n",
+              "│ What compliance requirements do we need to follow ...  Does the response       4.49   Summary of compliance  │\n",
+              "│                                                        provide specific               requirements           │\n",
+              "│                                                        detai...                                              │\n",
+              "│                                                                                       Legal / Financ...      │\n",
+              "│ What compliance requirements do we need to follow ...  Are potential risks or  3.92   Summary of compliance  │\n",
+              "│                                                        limitations menti...           requirements           │\n",
+              "│                                                                                                              │\n",
+              "│                                                                                       Legal / Financ...      │\n",
+              "│ What compliance requirements do we need to follow ...  Does the response cite  3.83   Summary of compliance  │\n",
+              "│                                                        or reference the ...           requirements           │\n",
+              "│                                                                                                              │\n",
+              "│                                                                                       Legal / Financ...      │\n",
+              "│ What compliance requirements do we need to follow ...  Is the response         4.30   Summary of compliance  │\n",
+              "│                                                        actionable and provide         requirements           │\n",
+              "│                                                        c...                                                  │\n",
+              "│                                                                                       Legal / Financ...      │\n",
+              "│ What are the current market trends in AI adoption?     Does the response       3.77   Current market trends  │\n",
+              "│                                                        accurately reflect             in AI adoption         │\n",
+              "│                                                        the...                                                │\n",
+              "│                                                                                       - Rapid ente...        │\n",
+              "│ What are the current market trends in AI adoption?     Is the response clear   4.06   Current market trends  │\n",
+              "│                                                        and well-structure...          in AI adoption         │\n",
+              "│                                                                                                              │\n",
+              "│                                                                                       - Rapid ente...        │\n",
+              "│ What are the current market trends in AI adoption?     Does the response       4.62   Current market trends  │\n",
+              "│                                                        provide specific               in AI adoption         │\n",
+              "│                                                        detai...                                              │\n",
+              "│                                                                                       - Rapid ente...        │\n",
+              "│ What are the current market trends in AI adoption?     Are potential risks or  4.04   Current market trends  │\n",
+              "│                                                        limitations menti...           in AI adoption         │\n",
+              "│                                                                                                              │\n",
+              "│                                                                                       - Rapid ente...        │\n",
+              "│ What are the current market trends in AI adoption?     Does the response cite  3.94   Current market trends  │\n",
+              "│                                                        or reference the ...           in AI adoption         │\n",
+              "│                                                                                                              │\n",
+              "│                                                                                       - Rapid ente...        │\n",
+              "│ What are the current market trends in AI adoption?     Is the response         2.92   Current market trends  │\n",
+              "│                                                        actionable and provide         in AI adoption         │\n",
+              "│                                                        c...                                                  │\n",
+              "│                                                                                       - Rapid ente...        │\n",
+              "│ What privacy guidelines must our AI systems follow...  Does the response       4.50   Our AI systems must    │\n",
+              "│                                                        accurately reflect             follow these privacy   │\n",
+              "│                                                        the...                         requireme...           │\n",
+              "│ What privacy guidelines must our AI systems follow...  Is the response clear   4.39   Our AI systems must    │\n",
+              "│                                                        and well-structure...          follow these privacy   │\n",
+              "│                                                                                       requireme...           │\n",
+              "│ What privacy guidelines must our AI systems follow...  Does the response       4.54   Our AI systems must    │\n",
+              "│                                                        provide specific               follow these privacy   │\n",
+              "│                                                        detai...                       requireme...           │\n",
+              "│ What privacy guidelines must our AI systems follow...  Are potential risks or  2.09   Our AI systems must    │\n",
+              "│                                                        limitations menti...           follow these privacy   │\n",
+              "│                                                                                       requireme...           │\n",
+              "│ What privacy guidelines must our AI systems follow...  Does the response cite  4.46   Our AI systems must    │\n",
+              "│                                                        or reference the ...           follow these privacy   │\n",
+              "│                                                                                       requireme...           │\n",
+              "│ What privacy guidelines must our AI systems follow...  Is the response         3.82   Our AI systems must    │\n",
+              "│                                                        actionable and provide         follow these privacy   │\n",
+              "│                                                        c...                           requireme...           │\n",
+              "└───────────────────────────────────────────────────────┴────────────────────────┴───────┴────────────────────────┘\n",
+              "
\n" + ] + }, + "metadata": {} + } + ], + "source": [ + "# Create detailed results table\n", + "def create_results_table(evaluation_results):\n", + " \"\"\"\n", + " Create a detailed results table\n", + " \"\"\"\n", + " console = Console()\n", + "\n", + " table = Table(title=\"Detailed Evaluation Results\")\n", + " table.add_column(\"Query\", style=\"cyan\", no_wrap=True)\n", + " table.add_column(\"Test\", style=\"magenta\")\n", + " table.add_column(\"Score\", style=\"green\")\n", + " table.add_column(\"Response Preview\", style=\"yellow\")\n", + "\n", + " for result in evaluation_results:\n", + " query_short = result['query'][:50] + \"...\" if len(result['query']) > 50 else result['query']\n", + " response_short = result['response'][:50] + \"...\" if len(result['response']) > 50 else result['response']\n", + "\n", + " for test_result in result['test_results']:\n", + " if test_result['score'] is not None:\n", + " test_short = test_result['test'][:40] + \"...\" if len(test_result['test']) > 40 else test_result['test']\n", + " table.add_row(\n", + " query_short,\n", + " test_short,\n", + " f\"{test_result['score']:.2f}\",\n", + " response_short\n", + " )\n", + "\n", + " console.print(table)\n", + "\n", + "# Display results table\n", + "create_results_table(evaluation_results)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vlFaDd5MfWF6" + }, + "source": [ + "### Create Visualizations\n", + "\n", + "We'll create comprehensive visualizations to analyze the evaluation results from multiple perspectives:\n", + "\n", + "1. **Score Distribution**: Histogram showing the overall distribution of scores across all tests, helping identify if responses generally score high or low\n", + "2. **Average Score by Test**: Bar chart comparing performance across different unit tests, revealing which quality dimensions are strongest or weakest\n", + "3. **Average Score by Response**: Bar chart comparing overall performance of different RAG responses, identifying which queries produce better results\n", + "4. **Score Heatmap**: Matrix visualization showing scores for each test-response combination, enabling quick identification of patterns and outliers\n", + "\n", + "These visualizations help identify areas for improvement in the RAG pipeline, whether it's specific quality dimensions that need attention or particular queries that generate lower-quality responses.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "id": "illMSDIafWF6", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "outputId": "1c57f3ff-6561-4308-e8bc-b56abf03916b" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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Ooajo6Px/vvvG+Hh4catW7ceun316tUt27q4uBjnzp2zWn/p0iXD2dnZ0qdmzZqWdV999VWSc0zucff1qV27dqr63/t+ffvtt4aLi0uK/Zs2bWrExsZa5Z46dapVn4YNGxru7u7Jbv/tt98ao0aNMkwmU5J1Dg4OxtKlS9P0nqxdu9ZqHwEBAanaLiIiwmq7Dh06JOkzefJkqz5ff/21ZV1YWNhDX1tHR0djypQpSfZ7/3tz73tw//l07949yfYBAQFWfe518eLFVL3v5cqVM65evfrAfT7ocVf37t2t2teuXWuV4/jx40bZsmUfur8333zTSExMtNr2/n137tzZcHR0THb71q1bJ/v+puT+c70/+2uvvWa1fsiQIUn2cfbsWaNy5copnpunp6fx008/JXl/UvNat23b1mq7lD4zhpH03+DgwYNTtX1af06k5nMvybh27Vqa3xcAAIDHgaldAABApnDixAmrkY5ubm5q0qRJsn1//vlnq5GfoaGhcnd3V6dOnbR161ZL+7Rp0/T1119bbbtp0yarZT8/P1WoUMEWp2DFx8dHwcHB+ueffyRJCQkJGjFihEaMGCGz2awyZcqoRo0aatGiherVqycHB+svEr7zzjt67rnnJN0ZrTt58mR9+OGHlvU//vij1XQwr7zyiqQ7U+IMGTLE0u7i4qIqVarIx8dHFy5c0OnTp5OMfq1du7Zy5cql9evXW72ud+ewv8vDw0OStGDBAqupOhwdHS3HiIiI0JkzZyRJK1as0Isvvqh58+Y98HVavXq1nJ2dVbNmTUVFRWnPnj2WdW+//bbi4+Pl5uamqlWr6ujRo5bRromJiXr33XfVqlWrB+77YS5evKh27dolu65u3bqWc3zmmWdUuXJly6joZcuWKTo6Wl5eXpb+M2fOtDw3m83q3r271f4cHBxUrFgx+fr6ysfHR7du3dLx48e1f/9+SXc+H2FhYWratKny5s2b7nNKj5w5c6pQoULy8fGRm5ubrl69ql27dik6OlrSnWmUBg8ebLkBcLNmzXThwgWtWLFC169ft+ynbdu2aT52fHy8mjVrpn379lnaPD09ValSJV29elU7d+60tI8aNUo5c+bUoEGDHri/2bNny2w2q0aNGrpy5YrV52np0qXatGmTzaZeuXLlin7++WfLsslkUuvWra363Lp1S82aNVNERISlrUCBAgoJCVFUVJQ2b96sxMREXbt2TR06dNCWLVv0zDPPSJImT55sNeo/MDBQpUuXVlxcnM6cOaNjx47pxo0bNjmX1EjLz4mzZ8/qu+++s2qrUqWKPDw8FBkZqZMnT+rChQsZlh0AACBV7F3JBwAAuEvJjFhu27atUa9ePatRyY6Ojsa0adMeuJ/7R67//PPPhmEYRmRkpOHg4GBp9/PzSzL6u1+/flbbVqlSJcn+O3funOzIydSOYL5r5cqVhpOT00NHZAYHBxs7duyw2jYhIcEoVqyYpU+BAgWszuXeEeu5cuUybt68aRiGYZw5c8Zq3zNmzEiS6/jx48akSZOMyMhIq/aHjWS9m6tgwYKWPj4+Psa+ffss62/dumU0b97caj/3jvi/fzSsyWQyfv/9d8u+7/9WgYeHh7F7927DMAwjNjbWyJs3r9X6EydOpPr9uH8Ed0qP+0d3f//991brv//+e6vX895R8x07drTa9tChQ1Yjuu81btw4q/2OHz/eav3jHJEeFxdn7N69O8kob8MwjOjoaCMoKMjq31Ja9n2vlEakT5gwwWpdoUKFjFOnTlnWz5w502q9u7u7cfny5Qfu29vb24iIiHjg+uRGjKfk/nOsVauW0bZtW6Nhw4ZG9uzZrdYNHTo0yfb3f2769etnJCQkWNb/9ddfVp+dFi1aWNb17t3b0l6sWDHj9u3bVvuOi4sz1qxZYyxcuNCq/XGNSE/t+rvndW+fP//8M0mf/fv3G2PGjDHi4uKSrAMAALAHRqQDAIAn1ooVK5K0FSlSRPPnz1e5cuWS3eb8+fNW2+XIkUONGzeWdGd0ed26dbVmzRpJ0rlz5/Tbb78lmcc4ozRq1Ejr16/X22+/rS1btjyw3z///KMGDRron3/+sYxGdnBw0Ntvv62XX35ZknT69GktXbpU7dq107Fjx6xG1vfo0UNms1mSlCtXLnl4eCg2NlaSNG7cOMXGxqpIkSIqWrSoChYsqICAAPXu3Ttd57Rz506dPHnSsuzu7q6PPvrIqs/Zs2etln/++ecHjvqvW7eu6tevbznnatWqWX2roEOHDipTpozlWNWqVdPixYst68+cOaOCBQum61zSomPHjnrrrbcso7RnzpypXr16SZJmzZplNR98nz59rLYtVKiQFi5cqB9//FERERE6d+6cbty4kewc8v/+++9jPAtrLi4u8vb21sCBA7V27VodOXJE0dHRyd749ty5c7p69aqyZ89u0wzLli2zWn7nnXdUoEABy3KXLl00duxYhYeHS5KuX7+uNWvWPPCbBK+88oplRLd059sq06dPtyzf/bZEet0/H7kk5c6dW3PmzLF8ju+1ZMkSq+VDhw6pffv2Vm0uLi6W+dtXr16tuLg4mc1mBQQEWPocO3ZMH3zwgSpWrKjChQurWLFiypYtm+rVq/dI5/O43JtdkoYOHar27dtbfg7ly5dPJUqUsLqXAAAAgL1RSAcAAJnK4cOH9corr+i3336Tj49PkvWzZs3S7du3Lctt27aVs7OzZblTp06WQrp0Z3qXewvpefLksdrfvUXhuypXrqybN2/q+vXryRb706J69eravHmzDh06pDVr1mjTpk3asGFDkhsAXrlyRVOnTtUHH3xgaevWrZs++ugjyxQI3377rdq1a6dZs2ZZ+phMJkuxXbpTlPvoo48sN1Tdtm2b1Y0avby8VKtWLfXp00ctW7ZM8/kcO3bMavnMmTNWNwZNzTb3ulskv8vT09NquXTp0imuf5QbSAYEBCR5Hx7Ew8NDL7zwgiZMmCDpTkH1xIkTCggIsJrWpWjRoqpbt65l2TAMtW3bVkuXLk3VcaKiolKd/1Ft2LBBTZs2tfzR5WGioqJsXki///W///Mg3Zla524hXUr581SpUiWrZW9vb6tlW99wVJIuXLigvn37atWqVQoMDLRad3/W1atXp7ivuLg4nT17VkFBQerdu7cmTZqkkydP6tatW/ryyy8t/e7e0LZVq1Z66623kr2RsT3lz59fr7zyiuXfy6pVq7Rq1SrL+ly5cqlevXp69dVX9eyzz9orJgAAgBWHh3cBAACwj2PHjunmzZv6888/rQpQ27ZtU48ePZLd5t7RpdKd+boLFChgedxbiJbujIa+fPmyZfn++ZEjIyO1e/duq7bXX39dCxcutJrj91EVLVpUr7zyimbMmKFjx44pIiJCwcHBVn3uzpd9l6urq1577TXL8rp16/TPP/9o9uzZlrb69eurSJEiVtu99957WrNmjTp37qyAgACZTCbLuujoaC1fvlyhoaEaO3aszc4vJSkVau8vzN4/V3xyf0yxl3tHmhuGoVmzZik8PFwHDhywtN8/0n/RokVJiuhlypRRaGio2rZtq1q1almtS26Uemrd+wemu1Kah7pv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wUHt2rXTzz//rJkzZ+r27dtW07pItrlOdnR0lMlksvpW4/Hjx7V06VKrfo0bN5aU9Frz7jXqvftr27atFi1alGyR++LFiynmOXTokE6ePJmk/erVq9q8ebN8fHzk6+srBwcHtW7dWj///LO2b9+epP/d3z0e9dq2QoUKKly4sL7++mvLlJBpOZ+7ZsyYYXVPpoULFyoyMtJybX9X06ZNlStXLn3xxRdav379I49Gl1L/Obl8+XKS9XcHTN2dYsjWv/MAyHoYkQ7gqfLaa6/p+vXreu6551SiRAnFx8dr06ZN+vHHHxUYGGiZ27BIkSIaNGiQPvvsMz377LNq06aNzGazwsPDlS9fPg0fPly+vr4aOHCghgwZoiZNmig0NFQHDhzQd999p0qVKqXqwrBr166aP3++XnnlFa1du1Y1atRQQkKC/v33X82fP18rV65M9gZD97p9+7ZmzZolSbp586ZOnDihZcuWaffu3apbt64mTZqU4vaNGjWSn5+fatSooTx58mj//v0aN26cmjdvbplbvUKFCpKkQYMGqWPHjnJ2dlbLli0tF5tpdezYMYWGhqpJkybavHmzZs2apRdeeMHq67wvvfSSRowYoZdeekkVK1bUn3/+qYMHDybZV1qyvf/++5o7d66aNm2q119/XTly5ND06dN17NgxLVq0KMmNZQEAAPBwy5Yt07Vr1xQaGprs+qpVq8rX11ezZ8+2FMw7dOig//3vfxo8eLDKlCljmdf7LltcJzdv3lwjR45UkyZN9MILL+jChQv69ttvVaRIEe3evdvSr0KFCmrbtq1Gjx6t//77T1WrVtX69est1573jlQeMWKE1q5dqypVqqh3794qVaqULl++rJ07d+r3339PtmB7199//60XXnhBTZs21bPPPqscOXLozJkzmj59us6ePavRo0dbphj5/PPPtWrVKtWuXVt9+vRRyZIlFRkZqQULFmjjxo3Knj37I1/bOjg46Pvvv1fTpk0VHBysnj17Kn/+/Dpz5ozWrl0rLy8v/fzzzynuQ5Jy5MihmjVrqmfPnjp//rxGjx6tIkWKqHfv3lb9nJ2d1bFjR40bN06Ojo7q1KnTQ/f9MKn9nHz66af6888/1bx5cwUEBOjChQv67rvvVKBAAcvNWgsXLqzs2bNrwoQJ8vT0lIeHh6pUqZLm+0EByMIMAHiKrFixwnjx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3DhZLBb74uPjo7Jly+qJJ57QxYsXzQ4PAADA7fbt26f77rtPZcqUkZ+fn0qWLKl27drp7bffNju0LBswYIACAgIyfN1isWj48OFujeHdd9/VggUL3LoPAMCdxdvsAADcmRYtWqSyZcvqxx9/1JEjRxQaGmp2SDlGz549tWbNGvXp00eDBw/WtWvX9Ouvv2r16tVq3LixqlSpYnaIOdbMmTMVEBCghIQErV+/Xm+//bb27NmjLVu2mB0aAACA22zbtk2tWrVS6dKlNXjwYAUHBys6Olo7duzQ9OnTNWLECLND9DjvvvuuChcurAEDBpgdCgDAQ5BIB+Byx44d07Zt27RixQoNHTpUixYtUkRERLbGkJKSoqSkJPn5+WXrfm9m586dWr16tSZMmKDnnnvO4bUZM2Zka3X11atX5evrK6vVc/446b777lPhwoUlSUOHDlXv3r31ySef6Mcff1SDBg1Mjg4AAMA9JkyYoKCgIO3cuVP58+d3eO3MmTPZGsvly5eVN2/ebN0nAAA5gedkTwB4jEWLFqlAgQLq0qWL7rvvPi1atMj+2rVr11SwYEENHDgwzfvi4uLk5+enp556yt6WmJioiIgIhYaGymazKSQkRM8884wSExMd3pv655+LFi1S9erVZbPZ9PXXX0uSpkyZosaNG6tQoULKkyeP6tWrl+6c3leuXNETTzyhwoULK1++fOrWrZtOnDghi8WicePGOfQ9ceKEHn74YRUrVkw2m03Vq1fXvHnzbnpujh49Kklq0qRJmte8vLxUqFChNPsZNGiQSpQoIZvNpnLlyumxxx5TUlKSvc/vv/+uXr16qWDBgsqbN68aNWqkL7/80mE7qXOZL1myRC+88IJKliypvHnzKi4uTpL0ww8/qGPHjgoKClLevHnVokULbd269abHkyo5OVnPPfecgoOD5e/vr27duik6Otr+ekREhHx8fHT27Nk07x0yZIjy58+vq1evZnp/qZo1aybpf+c1VWaO59KlSxo1apTKli0rm82mokWLql27dtqzZ4+9T8uWLVWjRg3t3r1bjRs3Vp48eVSuXDnNmjUrTSxnzpzRoEGDVKxYMfn5+alWrVr64IMPHPr88ccfslgsmjJliubMmaMKFSrIZrOpfv362rlzp0Pf06dPa+DAgSpVqpRsNpuKFy+u7t27648//nDot2bNGjVr1kz+/v7Kly+funTpogMHDmT5XAIAgJzr6NGjql69epokuiQVLVo0TdtHH32kBg0aKG/evCpQoICaN2+ub775xqHPu+++a79vLlGihIYNG5amqOOf90LNmzdX3rx57cUgmb1Pd5XM7m/+/Plq3bq1ihYtKpvNpmrVqmnmzJkOfcqWLasDBw5o06ZN9qkDW7ZsKUlasGCBLBaLtmzZoieeeEJFihRR/vz5NXToUCUlJenixYvq16+fChQooAIFCuiZZ56RYRgO28/s549/foapXLmy/Pz8VK9ePW3evNm1Jw8A4BJUpANwuUWLFunee++Vr6+v+vTpo5kzZ2rnzp2qX7++fHx8dM8992jFihWaPXu2fH197e9btWqVEhMT1bt3b0k3qsq7deumLVu2aMiQIapatar27dunqVOn6rffftOqVasc9vvdd99p6dKlGj58uAoXLqyyZctKkqZPn65u3bqpb9++SkpK0pIlS9SrVy+tXr1aXbp0sb9/wIABWrp0qR566CE1atRImzZtcng9VUxMjBo1amS/8S1SpIjWrFmjQYMGKS4uTqNGjcrw3JQpU8Z+jpo0aSJv74x/DJ88eVINGjTQxYsXNWTIEFWpUkUnTpzQsmXLdPnyZfn6+iomJkaNGzfW5cuX9cQTT6hQoUL64IMP1K1bNy1btkz33HOPwzbHjx8vX19fPfXUU0pMTJSvr6++++47derUSfXq1VNERISsVqv9A8j333+fqUrvCRMmyGKx6Nlnn9WZM2c0bdo0tW3bVlFRUcqTJ48eeughvfLKK/rkk08c5rtMSkrSsmXL1LNnz1v664HUpHKBAgXsbZk9nkcffVTLli3T8OHDVa1aNf3999/asmWLfvnlF9WtW9e+vQsXLqhz5866//771adPHy1dulSPPfaYfH199fDDD0u68UuYli1b6siRIxo+fLjKlSunTz/9VAMGDNDFixc1cuRIh7gXL16sS5cuaejQobJYLHr99dd177336vfff5ePj4+kG1MAHThwQCNGjFDZsmV15swZrVu3TsePH7df2wsXLlT//v3VoUMHTZo0SZcvX9bMmTPVtGlT/fTTT/Z+AADAs5UpU0bbt2/X/v37VaNGDad9X375ZY0bN06NGzfWK6+8Il9fX/3www/67rvv1L59e0k3nj/z8ssvq23btnrsscd06NAh+z371q1b7fcj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7DfMr732mr755hu1aNFCQ4YMUdWqVXXq1Cl9+umn2rJli/Lnz68xY8bo448/VqdOnfTEE0+oYMGC+uCDD3Ts2DEtX748zZzd/2a1WvX++++rU6dOql69ugYOHKiSJUvqxIkT2rBhgwIDA/XFF1843YYkFSxYUE2bNtXAgQMVExOjadOmKTQ0VIMHD3bo5+Pjo969e2vGjBny8vJyeDjnrfDx8dHIkSP19NNP6+uvv1bHjh0zdTyXLl1SqVKldN9996lWrVoKCAjQt99+q507d+qNN95w2EeJEiU0adIk/fHHH6pUqZI++eQTRUVFac6cOfLx8ZEkDRkyRLNnz9aAAQO0e/dulS1bVsuWLdPWrVs1bdo0hwexZsZvv/2mNm3a6P7771e1atXk7e2tlStXKiYmRr1795Z0o+p+5syZeuihh1S3bl317t1bRYoU0fHjx/Xll1+qSZMmmjFjxm2dXwAAkDOMGDFCly9f1j333KMqVaooKSlJ27Zt0yeffKKyZcvaHwYaGhqq559/XuPHj1ezZs107733ymazaefOnSpRooQiIyNVpEgRjR07Vi+//LI6duyobt266dChQ3r33XdVv379TBU6uOI+PSsyu7/27dvL19dXXbt21dChQxUfH6/33ntPRYsWtVftp6pXr55mzpypV199VaGhoSpatGiGz57Jisx+/khVo0YNdejQQU888YRsNpu9sOfll1++7VgAAC5mAIALdO3a1fDz8zMSEhIy7DNgwADDx8fHOHfunGEYhpGSkmKEhIQYkoxXX3013fckJSUZkyZNMqpXr27YbDajQIECRr169YyXX37ZiI2NtfeTZAwbNizdbcydO9eoWLGiYbPZjCpVqhjz5883IiIijH//CExISDCGDRtmFCxY0AgICDB69OhhHDp0yJBkTJw40aFvTEyMMWzYMCMkJMTw8fExgoODjTZt2hhz5sxxep5iYmKMiRMnGi1atDCKFy9ueHt7GwUKFDBat25tLFu2LE3/P//80+jXr59RpEgRw2azGeXLlzeGDRtmJCYm2vscPXrUuO+++4z8+fMbfn5+RoMGDYzVq1c7bGfDhg2GJOPTTz9NN66ffvrJuPfee41ChQoZNpvNKFOmjHH//fcb69evd3o8qdv9+OOPjbFjxxpFixY18uTJY3Tp0sX4888/033Pjz/+aEgy2rdv73Tb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denSJS1evFgbN27U2rVrsxoWAAAAcEfbtGmTtm3bpsqVK9vbKleurLffflvNmjUzMTIAAABkhkVWWbNe15yp7cL9spxIHzx4sEaOHKl58+bJYrHo5MmT2r59u5566im9+OKLWdrWmTNn1K9fP506dUpBQUEKCwvT2rVr1a5du6yGBQAAANzRQkJCdO1a2gdmJScnq0SJEiZEBAAAAOQeWU6kjxkzRikpKWrTpo0uX76s5s2by2az6amnntKIESOytK25c+dmdfcAAABArjR58mSNGDFC77zzju666y5JNx48OnLkSE2ZMsXk6AAAAHAz7prPnDnSs0eWEunJycnaunWrhg0bpqefflpHjhxRfHy8qlWrpoCAAHfFCAAAAORKBQoUkMVisa8nJCSoYcOG8va+cRt//fp1eXt76+GHH1aPHj1MihIAAACZYbVYZHVD0tv6j/tFuE+WEuleXl5q3769fvnlF+XPn1/VqlVzV1wAAABArjdt2jSzQwAAAACgW5japUaNGvr9999Vrlw5d8QDAAAA4P/179/f7BAAAADgIhZZ3fJgUB42mj2ynEh/9dVX9dRTT2n8+PGqV6+e/P39HV4PDAx0WXAAAAAAHJ05c0ZnzpxRSkqKQ3tYWJhJEQEAAAB3viwn0jt37ixJ6tatm8N8jYZhyGKxKDk52XXRAQAAAJAk7d69W/3799cvv/wiwzAcXuM+HAAAIOezWqxumiOdivTskOVE+oYNG9wRBwAAAAAnHn74YVWqVElz585VsWLFHIpaAAAAALhXlhPpLVq0cEccAAAAAJz4/ffftXz5coWGhpodCgAAAG4Bc6R7tiwn0iXp4sWLmjt3rn755RdJUvXq1fXwww8rKCjIpcEBAAAAuKFNmzbau3cviXQAAADABFlOpO/atUsdOnRQnjx51KBBA0nSm2++qQkTJuibb75R3bp1XR4kAAAAkNu9//776t+/v/bv368aNWrIx8fH4fVu3bqZFBkAAAAygznSPVuWE+lPPvmkunXrpvfee0/e3jfefv36dT3yyCMaNWqUNm/e7PIgAQAAgNxu+/bt2rp1q9asWZPmNR42CgAAALhXln9dsWvXLj377LP2JLokeXt765lnntGuXbtcGhwAAACAG0aMGKH//Oc/OnXqlFJSUhwWkugAAAA5n8ViddsC98vyWQ4MDNTx48fTtEdH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LjZgwABZLBZZLBb5+PioXLlyeuaZZ3T16lV7n27duql06dLy8/NT8eLF9dBDD+nkyZMmRg0AAABXmDlzpsLCwhQYGKjAwECFh4drzZo1Tt/z6aefqkqVKvLz81PNmjX11VdfZWmfx44dy3D5/fffb+dwsk2WE+mjR4/WgAEDdPjwYfn5+dnbO3furM2bN7s0OAAAAADu0bFjR506dUq///67pk6dqtmzZysiIsL+eqtWrbR06VIdOnRIy5cv19GjR3XfffeZGDEAAIBns1qsbluyolSpUpo4caJ2796tXbt2qXXr1urevbsOHDiQbv9t27apT58+GjRokH766Sf16NFDPXr00P79+2/pPBiGIcMwbum9ZspyIn3nzp0aOnRomvaSJUvq9OnTLgkKAAAAgHvZbDYFBwcrJCREPXr0UNu2bbVu3Tr7608++aQaNWqkMmXKqHHjxhozZox27Niha9eumRg1AACA57JYrG5bsqJr167q3LmzKlasqEqVKmnChAkKCAjQjh070u0/ffp0dezYUU8//bSqVq2q8ePHq27dupoxY0aW9vvhhx+qZs2aypMnj/LkyaOwsDAtXLgwS9swU5YT6TabTXFxcWnaf/vtNxUpUsQlQQEAAABI6+jRo3rhhRfUp08fnTlzRpK0Zs2aDKuHMmv//v3atm2bfH190339/PnzWrRokRo3biwfH590+yQmJiouLs5hAQAAQPb5971YYmLiTd+TnJysJUuWKCEhQeHh4en22b59u9q2bevQ1qFDB23fvj3Tsb355pt67LHH1LlzZy1dulRLly5Vx44d9eijj2rq1KmZ3o6ZspxI79atm1555RV7JYrFYtHx48f17LPPqmfPni4PEAAAAIC0adMm1axZUz/88INWrFih+Ph4SdLevXsdpmTJrNWrVysgIMA+z+WZM2f09NNPO/R59tln5e/vr0KFCun48eP67LPPMtxeZGSkgoKC7EtISEiWYwIAALiTWQz3LZIUEhLicD8WGRmZYSz79u1TQECAbDabHn30Ua1cuVLVqlVLt+/p06dVrFgxh7ZixYplaXaSt99+WzNnztSkSZPUrVs3devWTa+//rreffddvfXWW5nejpmynEh/4403FB8fr6JFi+rKlStq0aKFQkNDlS9fPk2YMMEdMQIAAAC53pgxY/Tqq69q3bp1DpXjrVu3zvDP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6a+//lKXLl1ks9nUvn17paSkaOzYsTp//ry6du2qd955x/nZtjgrcCF9zZo1euihh1SzZk3t2bNHDRo00MGDB2UYhho3bmxGjAAAAAAkPfbYY7nGwsPDLYgEAAAAuLbx48erdevW6tKliyRp586d6tevn8LDw1WvXj298cYbqlKlisaNG2dtoPlQ4EJ6ZGSkhg8frqioKAUEBOjzzz9XUFCQnnzySXXo0MGMGAEAAAAou6llzZo1+vPPP+X4n5tKzZ8/36KoAAAAkC9u2JEeHx+vCRMmOPeXLFmiu+++W3PnzpUkhYSEaOzYsS5RSPco6BN2796tXr16SZK8vLx04cIF+fv7a/z48Xr99dcLPUAAAAAAUlRUlNq3b681a9boxIkTOnXqVI4NAAAAKG5OnTqlSpUqOfd/+OEHdezY0bl/1113KTk52YrQCqzAHellypRxroteuXJl7du3T7fffrsk6cSJE4UbHQAAAABJ0pw5cxQTE6Onn37a6lAAAABwPRyO7M2MeYupSpUq6cCBAwoJCdHFixe1bds2RUVFOR8/c+aMvL29LYww/wpcSL/33nu1ceNG1atXT506ddJLL72knTt3avny5br33nvNiBEAAABwexcvXlTz5s2tDgMAAADIt06dOmnkyJF6/fXXtXLlSvn5+em+++5zPr5jxw7deuutFkaYfwVe2mXatGm65557JGVfXnr//fdr6dKlql69uubNm1foAQIAAACQnn32WS1atMjqMAAAAHC9Lq+RbsZWTE2YMEFeXl5q1aqV5s6dq7lz56pUqVLOx+fPn6/27dtbGGH+Fagj3W6368iRI2rYsKGk7GVe5syZY0pgAAAAAP4rIyND77//vr7//ns1bNgw1yWw06ZNsygyAAAA4MpuuukmrV+/XmlpafL395enp2eOxz/77DP5+/tbFF3BFKiQ7unpqfbt22v37t0qV66cSSEBAAAA+F87duxQo0aNJEkJCQk5HrPZbBZEBAAAgAIxq3u8GHekX1a2bNkrjleoUKGII7l+BV4jvUGDBtq/f79q1KhhRjwAAAAAruD//u//rA4BAAAAcFsFXiN94sSJGj58uL7++mulpKQoPT09xwYAAADAXEeOHNGRI0esDgMAAAAF4YZrpJckBe5I79SpkyTpoYceynEJqWEYstlsstvthRcdAAAAAEmSw+HQxIkT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KlSwOowS748//tDy5cvVqlUrq0MpkX777TeNHz9eH330EY1lRWTgwIG66aabdPfdd2v+/Pl83zBBbGysHA6Hjh49qnr16qlq1arq3r27kpOTrQ4NMB3v5Ch0CQkJmj59uoYMGaLWrVurVatWSkhIoJjuZn788UdNmjRJ7733nsaPH6/XX39db731llJSUrRz506rw0MRWLNmjSZMmKC5c+dqyJAh6tq1q55++mk1bNhQR44c0YULF3hPcCMhISEKCwvT7t279csvv2jQoEE6ffq0HA6H9u7da3V4MNkXX3yh/v37a8aMGerevbt+//13jRkzRk888YT69OmjpKQkvugCcNq0aZOWLl2q5557zupQSqyePXvKz89PN998swIDA/XBBx9YHVKJk5mZqZ49e+qNN97QLbfcYnU4bmH8+PH69NNPFRsbq0cffVQDBgzQO++8Y3VYJc7+/fvlcDg0efJkvfXWW1q2bJlOnjypBx54QBcvXrQ6PMBUFNJR6E6fPq2mTZuqV69emjt3rlq0aKHWrVs7i+l0ILqHhIQE1apVK8f6dE2aNNGZM2eUmppqYWQoKkeOHFGPHj304IMPOseWL1+uhQsX6p///Kfq16+vOXPm6MKFCxZGiaJy22236ccff1T58uW1bt06bd68WU8//bTuuOMOvuC4gV27dumWW27R+fPntWbNGnXq1Em7du3ShQsXlJCQoGbNmmnNmjWSuFIFcHcJCQn617/+pbFjx6p9+/ZWh1NiTZ8+Xdu2bdMXX3yhffv2adiwYVaHVOJERkaqXr16euqpp6wOxW28+uqratGihe688069/PLLGjFiBMsWmcDhcOjSpUuaMWOGwsLCdO+992rx4sX6/fff9X//939WhweYijXSUej++c9/qlKlSs71b999910NGDBArVu31rp169SgQQNJUkZGhux2u8qUKWNluDBJ+/btFRISovLly0uSLl68KF9fX5UpU4bLGt1EeHi4Dh8+7Px/vF+/ftqzZ49iYmLUoEEDTZ06VVFRUercubOqV69ubbAwld1ul5eXl3x9ffXDDz+oQ4cO+vHHHxUaGip/f3/16tXL6hBhslGjRskwDH3wwQf6888/1bt3b0VFRcnPz0+S9PDDD+vFF1/Uzp075eXFx1PAXf3222+6//779dxzz+mVV16xOpwSLTg4WMHBwapbt64q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VEmVKlXSV199JW9vb9ntdm4oWYKQByAHIJEHyEYeAIBru9ZNnseOHatx48Zd99wrVqxQ165dr3mcj4+PEhMTVa1aNed4165dVa5cOcXExFzX6+fXuHHjtHLlSsXHx5v6OgCAkoM10pGnffv2adWqVVqyZIn++c9/6s8//9TevXu1fPlytW3bVi+99JK6deumY8eOKTAwULVr15aHh4eysrLk5UV6lRTkAcgBSOQBspEHAOD6UlJSnP9eunSpxowZo8TEROdYUd3PwmazacyYMfrwww+L5PWKwqVLl+Tt7W11GAAAE7C0C/Lk7+/v/IK8bds2jRgxQn369NGqVavUvn17ffHFF7rlllt01113qU6dOvLw8JDD4eCLcglDHoAcgEQeIBt5AACuLzg42LmVLVtWNpstx9iSJUtUr149+fr6qm7dunr33Xedz7148aIiIiJUuXJl+fr6qlq1aoqOjpYkVa9eXZL08MMPy2azOfevJiIiQp988okSEhKuekz16tX11ltv5Rhr1KhRjo55m82m9957T126dJGfn5/q1aunuLg4JSUlqXXr1ipTpoyaN2+uffv25Zr/vffeU0hIiPz8/NS9e3elpaXlePyDDz646rk4ePCgbDabli5dqlatWsnX11cLFy7M82cGALguCulwcjgcucYCAgLUrVs3vf7662revLnKlSun6OhoxcfH64EHHtDGjRtzPefy2qlwTeQByAFI5AGykQcA4H4WLlyoMWPGaNKkSdq9e7cmT56sV1991dk1PmPGDH355Zf69NNPlZiYqIULFzoL5j///LMkacGCBUpJSXHuX02LFi3UpUsXjRw58objnjBhgnr16qX4+HjVrVtXTzzxhPr376/IyEj98ssvMgxDEREROZ6TlJSkTz/9VF999ZVWr16tX3/9VQMGDMj3ubhs5MiRGjx4sHbv3q2wsLAb/lkAAMUTrUGQlPPmYStWrNChQ4d01113qXHjxpo8ebJ69eqlrKwsNWnSRJKUlZWls2fP6uabb7YybBQy8gDkACTyANnIAwBwT2PHjtXUqVP1yCOPSJJq1Kih3377Te+9957Cw8N1+PBh1a5dW//85z9ls9lyrG9esWJFSVK5cuUUHBycr9eLjo5Ww4YNtWHDBt13333XHXefPn3UvXt3SdLLL7+sZs2a6dVXX3UWtgcPHqw+ffrkeE5GRoY++ugj5++ud955R507d9bUqVMVHBx8zXNx2ZAhQ5zHAABKLgrpkGEYzpvNjBgxQvPnz1f58uV17tw5PfbYY4qIiFBoaKik7JuIJSUlKTIyUufPn8/1F324LvIA5AAk8gDZyAMAcE/nzp3Tvn379Mwzz6hfv37O8aysLJUtW1aS1Lt3bz3wwAOqU6eOOnTooC5duqh9+/bX/Zr169dXr169NHLkSP3444/XPU/Dhg2d/65UqZIk6Y477sgxlpGRofT0dOdNsm+55ZYcfwBu1qyZHA6HEhMTFRAQcM1zcVnTpk2vO24AgOvgOls39/cvyj///LMSEhK0atUqJSYmatSoUfrll1/02muvae/evZKyO9JeeeUVnT9/Xj/99JO8vLxkt9ut/BFQCMgDkAOQyANkIw8AwH2dPXtWkjR37lzFx8c7t4SEBG3evFmS1LhxYx04cEATJkzQhQsX1L17dz322GM39LpRUVHatm2bVq5cmesxDw8PGYaRY+zSpUu5jvv7DT4v/x670tiVli27kvyci8vKlCmTrzkBAK6NjnQ3d/nDxMKFC/Xll1+qfPnyatq0qTw8PBQRESFvb299+OGHeu211zR+/Hh16dJFFStW1P333y9PT09lZWVx87ASgDwAOQCJPEA28gAA3FelSpVUpUoV7d+/X08++eRVjwsMDFSPHj3Uo0cPPfbYY+rQoYNOnjypChUqyNvbu8B/UA0JCVFERIRGjRqlW2+9NcdjFStWVEpKinM/PT1dBw4cKNgPdhWHDx/WH3/8oSpVqkiSNm/eLA8PD9WpUyff5wIA4D7oSIckKT4+XmvXrtW2bdv0119/Ocf79++v3r17a+/evXrxxRd1/vx5tW/fXp6ennI4HHxRLmHIA5ADkMgDZCMPAMA9RUVFKTo6WjNmzNDevXu1c+dOLViwQNOmTZMkTZs2TYsXL9aePXu0d+9effbZZwoODla5cuUkSdWrV9eaNWuUmpqqU6dO5ft1IyMj9ccff+j777/PMd62bVt9/PHH2rBhg3bu3Knw8HB5enoWys/q6+ur8PBwbd++XRs2bNCgQYPUvXt35/ru1zoXAAD3QiHdDV3pUrY33nhDw4cP16VLlzRlypQcf/F/7rnn9OijjyooKMi51pwk5w3I4JrIA5ADkMgDZCMPAACXPfvss/rggw+0YMEC3XHHHWrVqpViYmJUo0YNSVJAQICmTJmipk2b6q677tLBgwe1atUq5++AqVOnKjY2ViEhIbrzzjvz/boVKlTQyy+/rIyMjBzjkZGRatWqlbp06aLOnTura9euubrWr1etWrX0yCOPqFOnTmrfvr0aNmyod9991/n4tc4FAMC92Iz/XWwMJZrD4XB+wElISJCXl5ccDofq168vSRo3bpy+/PJLhYWFafDgwTnutH55zdS/zwHXRB6AHIBEHiAbeQAAAAAA18a1t27EMAznl9zIyEitXLlSJ06cUJkyZfTggw/qnXfe0bhx42QYhr7++mt5eHhowIABzruY22y2HHPANZEHIAcgkQfIRh4AAAAAQP5QSHcjl28e9uabb+r999/XZ599Jin7BiuDBw/WyZMntXDhQkVFRclms2nevHkKCQnR888/n2sOuC7yAOQAJPIA2cgDAAAAAMgfCuluJisrS1u2bNGgQYPUtm1b53jNmjUVFhamyZMna9SoURo3bpxuueUWhYeHWxgtzEIegByARB4gG3kAAAAAANfGdbhuxm6367ffftOxY8dyjLVs2VLPP/+84uLidO7cOUlS37595enpKbvdblW4MAl5AHIAEnmAbOQBAAAAAFwbhfQSzOFw5Brz8fHRk08+qbi4OG3YsEGS5OnpKUkqW7aszpw5I19f3xzPufw4XBN5AHIAEnmAbOQBAAAAAFwfCukllMPhcN74a9euXfr555+VmZkpSXrwwQdVvnx5zZo1S+vWrZMknT59WnFxcapZsyZfjksQ8gDkACTyANnIAwAAAAC4fjbDMAyrg4B5Xn75ZX300UfKyMhQYGCgnn76af373/9WQkKCxo8fr/j4eFWuXFlS9hfsrVu3ytvbW4ZhcPOwEoQ8ADkAiTxANvIAAAAAAAqOQnoJ8/dusxUrVmjw4MGaPXu2brvtNn388cf67rvv1KBBA7311ls6deqUfvvtN8XFxalq1arq3bu3vLy8lJWVJS8v7kPrysgDkAOQyANkIw8AAAAA4MZRSC+hPv74Yx0/flwZGRkaNWqUc3zOnDmaOXOmhg0bpr59++Z6nt1u5/LtEoQ8ADkAiTxANvIAAAAAAK4fhfQS6Pz587r99tt16NAh9erVSzExMTke79Gjhw4ePKgtW7ZYEyCKBHkAcgASeYBs5AEAAAAA3BhuNloCOBwO57/T09Pl5+enuLg4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\n" + }, + "metadata": {} + } + ], + "source": [ + "# Create visualizations\n", + "def create_evaluation_visualizations(evaluation_results, unit_tests):\n", + " \"\"\"\n", + " Create visualizations for evaluation results\n", + " \"\"\"\n", + " # Set up the plotting style\n", + " plt.style.use('default')\n", + " sns.set_palette(\"husl\")\n", + "\n", + " # Create figure with subplots\n", + " fig, axes = plt.subplots(2, 2, figsize=(15, 12))\n", + " fig.suptitle('RAG System Evaluation Results', fontsize=16, fontweight='bold')\n", + "\n", + " # 1. Score distribution histogram\n", + " all_scores = []\n", + " for result in evaluation_results:\n", + " for test_result in result['test_results']:\n", + " if test_result['score'] is not None:\n", + " all_scores.append(test_result['score'])\n", + "\n", + " axes[0, 0].hist(all_scores, bins=10, alpha=0.7, color='skyblue', edgecolor='black')\n", + " axes[0, 0].set_title('Score Distribution')\n", + " axes[0, 0].set_xlabel('Score (1-5)')\n", + " axes[0, 0].set_ylabel('Frequency')\n", + " axes[0, 0].axvline(np.mean(all_scores), color='red', linestyle='--', label=f'Mean: {np.mean(all_scores):.2f}')\n", + " axes[0, 0].legend()\n", + "\n", + " # 2. Test performance comparison\n", + " test_scores = {}\n", + " for i, test in enumerate(unit_tests):\n", + " test_scores[f\"Test {i+1}\"] = []\n", + " for result in evaluation_results:\n", + " if i < len(result['test_results']) and result['test_results'][i]['score'] is not None:\n", + " test_scores[f\"Test {i+1}\"].append(result['test_results'][i]['score'])\n", + "\n", + " test_means = [np.mean(scores) if scores else 0 for scores in test_scores.values()]\n", + " test_names = list(test_scores.keys())\n", + "\n", + " bars = axes[0, 1].bar(test_names, test_means, color='lightcoral', alpha=0.7)\n", + " axes[0, 1].set_title('Average Score by Test')\n", + " axes[0, 1].set_ylabel('Average Score')\n", + " axes[0, 1].set_ylim(0, 5)\n", + " axes[0, 1].tick_params(axis='x', rotation=45)\n", + "\n", + " # Add value labels on bars\n", + " for bar, mean in zip(bars, test_means):\n", + " axes[0, 1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.1,\n", + " f'{mean:.2f}', ha='center', va='bottom')\n", + "\n", + " # 3. Response performance comparison\n", + " response_scores = []\n", + " response_names = []\n", + " for i, result in enumerate(evaluation_results):\n", + " scores = [test_result['score'] for test_result in result['test_results'] if test_result['score'] is not None]\n", + " if scores:\n", + " response_scores.append(np.mean(scores))\n", + " response_names.append(f\"Response {i+1}\")\n", + "\n", + " axes[1, 0].bar(response_names, response_scores, color='lightgreen', alpha=0.7)\n", + " axes[1, 0].set_title('Average Score by Response')\n", + " axes[1, 0].set_ylabel('Average Score')\n", + " axes[1, 0].set_ylim(0, 5)\n", + " axes[1, 0].tick_params(axis='x', rotation=45)\n", + "\n", + " # 4. Score heatmap\n", + " score_matrix = []\n", + " for result in evaluation_results:\n", + " row = []\n", + " for test_result in result['test_results']:\n", + " row.append(test_result['score'] if test_result['score'] is not None else 0)\n", + " score_matrix.append(row)\n", + "\n", + " im = axes[1, 1].imshow(score_matrix, cmap='RdYlGn', aspect='auto', vmin=1, vmax=5)\n", + " axes[1, 1].set_title('Score Heatmap')\n", + " axes[1, 1].set_xlabel('Test Number')\n", + " axes[1, 1].set_ylabel('Response Number')\n", + " axes[1, 1].set_xticks(range(len(unit_tests)))\n", + " axes[1, 1].set_yticks(range(len(evaluation_results)))\n", + " axes[1, 1].set_xticklabels([f\"T{i+1}\" for i in range(len(unit_tests))])\n", + " axes[1, 1].set_yticklabels([f\"R{i+1}\" for i in range(len(evaluation_results))])\n", + "\n", + " # Add colorbar\n", + " cbar = plt.colorbar(im, ax=axes[1, 1])\n", + " cbar.set_label('Score')\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "# Create visualizations\n", + "create_evaluation_visualizations(evaluation_results, unit_tests)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tfC82kF_fWF6" + }, + "source": [ + "## Summary\n", + "\n", + "This notebook demonstrates comprehensive RAG system evaluation using Chroma and Contextual AI's LMUnit for natural language unit testing.\n", + "\n", + "### What We Demonstrated:\n", + "\n", + "1. **Complete RAG Pipeline**: Chroma retrieval + LLM generation with enterprise knowledge base\n", + "2. **Natural Language Unit Testing**: Systematic evaluation using LMUnit with 6 quality dimensions\n", + "3. **Comprehensive Analysis**: Statistical analysis and visualization of evaluation results\n", + "4. **Production-Ready Evaluation**: Scalable approach for enterprise RAG systems\n", + "\n", + "### Key Benefits of LMUnit for RAG Evaluation:\n", + "\n", + "- **Granular Quality Assessment**: Break down response quality into specific, measurable criteria\n", + "- **Flexible Evaluation**: Users can fully customize the unit tests to match their specific requirements\n", + "- **Consistent Evaluation**: Standardized scoring across different responses and contexts\n", + "- **Actionable Insights**: Identify specific areas for RAG system improvement\n", + "\n", + "### RAG Evaluation Dimensions Covered:\n", + "\n", + "1. **Accuracy**: Does the response reflect retrieved context accurately?\n", + "2. **Clarity**: Is the response clear and well-structured?\n", + "3. **Specificity**: Does the response provide specific details?\n", + "4. **Risk Awareness**: Are limitations and risks mentioned?\n", + "5. **Source Attribution**: Are sources properly cited?\n", + "6. **Actionability**: Does the response provide clear next steps?\n", + "\n", + "### Chroma Integration Benefits:\n", + "\n", + "- **Rich Context**: Leverage document metadata for better retrieval\n", + "- **Scalable Evaluation**: Test RAG systems with large knowledge bases\n", + "- **Metadata-Aware Testing**: Evaluate responses based on source quality and relevance\n", + "- **Production Monitoring**: Continuous evaluation of RAG system performance\n", + "\n", + "### Next Steps for Enhancement:\n", + "\n", + "- **Automated Evaluation**: Set up continuous evaluation pipelines\n", + "- **Custom Unit Tests**: Develop domain-specific evaluation criteria\n", + "- **Performance Monitoring**: Track RAG system performance over time\n", + "- **A/B Testing**: Compare different RAG configurations using LMUnit scores\n", + "\n", + "---\n", + "\n", + "**Ready to get started?** This notebook provides a complete, production-ready example of evaluating RAG systems using Chroma and Contextual AI's LMUnit. The combination enables systematic quality assessment and continuous improvement of RAG applications.\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + }, + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file