From be12b882252ede6ba9cbd8228de4b1b022ce50eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:48:12 +0000 Subject: [PATCH 1/9] Initial plan From fa314c7a7b817ea76968d5b3a05358b8ae61b2d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:57:25 +0000 Subject: [PATCH 2/9] Add 01-rag-from-scratch project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete RAG from scratch implementation with: - 6-step pipeline: load → chunk → embed → index → retrieve → generate - HuggingFace all-MiniLM-L6-v2 embeddings (free, local, no API key) - FAISS vector store with disk persistence - OpenAI + Ollama LLM support - argparse CLI with single-question and interactive modes - Heavily commented, beginner-friendly code throughout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- 01-rag-from-scratch/.env.example | 5 + 01-rag-from-scratch/README.md | 307 ++++++++++++++++ 01-rag-from-scratch/data/sample_docs/.gitkeep | 0 01-rag-from-scratch/main.py | 327 ++++++++++++++++++ 01-rag-from-scratch/requirements.txt | 9 + 01-rag-from-scratch/src/__init__.py | 5 + 01-rag-from-scratch/src/chunker.py | 107 ++++++ 01-rag-from-scratch/src/document_loader.py | 125 +++++++ 01-rag-from-scratch/src/embedder.py | 114 ++++++ 01-rag-from-scratch/src/generator.py | 167 +++++++++ 01-rag-from-scratch/src/retriever.py | 117 +++++++ 01-rag-from-scratch/src/vector_store.py | 159 +++++++++ 12 files changed, 1442 insertions(+) create mode 100644 01-rag-from-scratch/.env.example create mode 100644 01-rag-from-scratch/README.md create mode 100644 01-rag-from-scratch/data/sample_docs/.gitkeep create mode 100644 01-rag-from-scratch/main.py create mode 100644 01-rag-from-scratch/requirements.txt create mode 100644 01-rag-from-scratch/src/__init__.py create mode 100644 01-rag-from-scratch/src/chunker.py create mode 100644 01-rag-from-scratch/src/document_loader.py create mode 100644 01-rag-from-scratch/src/embedder.py create mode 100644 01-rag-from-scratch/src/generator.py create mode 100644 01-rag-from-scratch/src/retriever.py create mode 100644 01-rag-from-scratch/src/vector_store.py diff --git a/01-rag-from-scratch/.env.example b/01-rag-from-scratch/.env.example new file mode 100644 index 0000000..f4f0169 --- /dev/null +++ b/01-rag-from-scratch/.env.example @@ -0,0 +1,5 @@ +# OpenAI API Key (required if using OpenAI as LLM) +OPENAI_API_KEY=your_openai_api_key_here + +# Optional: Ollama base URL (if running locally, no API key needed) +# OLLAMA_BASE_URL=http://localhost:11434 diff --git a/01-rag-from-scratch/README.md b/01-rag-from-scratch/README.md new file mode 100644 index 0000000..fbaa37b --- /dev/null +++ b/01-rag-from-scratch/README.md @@ -0,0 +1,307 @@ +# RAG from Scratch 🔍 + +A beginner-friendly implementation of Retrieval-Augmented Generation (RAG) built step-by-step using LangChain, FAISS, and HuggingFace embeddings. Every file is heavily commented to explain *why* each piece exists, not just *what* it does. + +--- + +## What is RAG and Why Does It Matter? + +**The problem with plain LLMs:** Large Language Models like GPT-4 are trained on data up to a certain cutoff date, and they have no knowledge of *your* private documents — your company's policy manuals, your research papers, your product documentation. If you ask GPT-4 "What is the refund policy in our internal handbook?", it simply doesn't know. + +**What RAG does:** RAG (Retrieval-Augmented Generation) solves this by giving the LLM access to your documents *at query time*. Instead of retraining the model (expensive, slow), you store your documents in a searchable vector database. When a user asks a question, you retrieve the most relevant passages and include them in the LLM's prompt. The LLM reads those passages and answers *based on your documents*. + +**Why it matters:** RAG is currently the dominant architecture for production AI Q&A systems. It's cost-effective (no retraining), updatable (just add documents to the database), and auditable (you can see exactly which document chunks informed each answer). Understanding RAG from scratch gives you the foundation to build everything from customer support bots to internal knowledge assistants. + +--- + +## Architecture + +``` +YOUR DOCUMENTS (PDF / TXT / DOCX) + │ + ▼ + ┌─────────────┐ + │ 1. LOAD │ Read files from disk into LangChain Document objects + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 2. CHUNK │ Split large docs into ~500-char overlapping pieces + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 3. EMBED │ Convert each chunk → 384-dim vector (all-MiniLM-L6-v2) + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 4. INDEX │ Store vectors in FAISS (saved to disk for reuse) + └──────┬──────┘ + │ + │ USER QUESTION + │ │ + │ ▼ + │ ┌─────────────┐ + │ │ 5. EMBED │ Embed question → vector + │ └──────┬──────┘ + │ │ + ▼ ▼ + ┌─────────────────────────────────────┐ + │ FAISS SIMILARITY SEARCH │ Find top-k most similar chunks + └──────────────────┬──────────────────┘ + │ + ▼ + TOP-k RELEVANT CHUNKS + │ + ▼ + ┌─────────────────────────────────────┐ + │ 6. GENERATE (LLM + Prompt) │ LLM reads chunks + question + └──────────────────┬──────────────────┘ + │ + ▼ + GROUNDED ANSWER ✅ +``` + +--- + +## Tech Stack + +| Component | Library / Tool | Purpose | +|-------------------|-----------------------------------------|------------------------------------------| +| Document loading | `langchain-community` loaders | Read PDF, TXT, DOCX files | +| Text splitting | `langchain` RecursiveCharacterTextSplitter | Split docs into overlapping chunks | +| Embeddings | `sentence-transformers` (HuggingFace) | Convert text → vectors (free, local) | +| Vector database | `faiss-cpu` | Fast similarity search over embeddings | +| LLM | OpenAI GPT-3.5/4 or local Ollama | Generate answers from retrieved context | +| Orchestration | `langchain` RetrievalQA chain | Tie retrieval + generation together | +| Env management | `python-dotenv` | Load API keys from `.env` file | + +--- + +## Step-by-Step Setup + +### 1. Create and activate a virtual environment + +```bash +python -m venv venv +source venv/bin/activate # macOS / Linux +# venv\Scripts\activate # Windows +``` + +### 2. Install dependencies + +```bash +pip install -r requirements.txt +``` + +> ⏱️ First install may take a few minutes. `faiss-cpu` and `sentence-transformers` are the largest packages. + +### 3. Configure your API key + +```bash +cp .env.example .env +``` + +Open `.env` and replace `your_openai_api_key_here` with your actual key from [platform.openai.com](https://platform.openai.com/api-keys). + +``` +OPENAI_API_KEY=sk-...your-key-here... +``` + +> 💡 **No OpenAI account?** Use a local model with Ollama — see [Using Ollama](#using-ollama-no-api-key-needed) below. + +### 4. Add your documents + +Drop any `.pdf`, `.txt`, or `.docx` files into: + +``` +data/sample_docs/ +``` + +The more documents you add, the more the system can answer. Start with a few text files to test. + +### 5. Run it! + +```bash +# Interactive mode — asks questions in a loop +python main.py + +# Single question mode +python main.py --question "What are the main topics in these documents?" + +# Debug mode — shows retrieved chunks and full LLM prompt +python main.py --debug --question "What is the refund policy?" +``` + +--- + +## How to Add Your Own Documents + +Just drop files into `data/sample_docs/`. The loader automatically detects file types: + +| File type | Support | Notes | +|-----------|---------|-------| +| `.pdf` | ✅ | Each page becomes a separate Document | +| `.txt` | ✅ | Entire file is one Document | +| `.docx` | ✅ | Entire file is one Document | +| `.csv` | ❌ | Not supported (yet) | + +**After adding new documents**, delete the cached FAISS index so it gets rebuilt: + +```bash +rm -rf faiss_index/ +python main.py +``` + +--- + +## How to Verify the LLM Uses Your Documents + +This is the most important test for any RAG system — make sure it's actually reading *your* documents and not falling back on general knowledge. + +**Step 1:** Put a document with a very specific, obscure fact in `data/sample_docs/`. For example, create `test.txt` containing: + +``` +The Zorbax Protocol was established in 2019 by Dr. Eleanor Voss. +The protocol requires three phases: initialization, calibration, and review. +``` + +**Step 2:** Ask the system about it: +```bash +python main.py --question "Who established the Zorbax Protocol?" +``` + +**Expected good result:** +``` +Answer: Dr. Eleanor Voss established the Zorbax Protocol in 2019. +Sources: data/sample_docs/test.txt +``` + +**Step 3:** Ask about something NOT in any document: +```bash +python main.py --question "What is the capital of Australia?" +``` + +**Expected good result:** +``` +Answer: I don't know based on the provided documents. +``` + +If the second answer returns "Canberra" (from general knowledge), the system is hallucinating — check that your prompt template in `src/generator.py` is being applied correctly. + +--- + +## Using Ollama (No API Key Needed) + +[Ollama](https://ollama.com) lets you run LLMs locally for free. + +```bash +# 1. Install Ollama: https://ollama.com +# 2. Pull a model +ollama pull llama3 # ~4GB download +ollama pull mistral # ~4GB download, often faster + +# 3. Run with Ollama +python main.py --model ollama/llama3 +python main.py --model ollama/mistral --question "Summarize the documents" +``` + +--- + +## Beginner Tips + +### What happens if chunk_size is too large or too small? + +| Setting | Effect | +|---------|--------| +| **chunk_size too large** (e.g., 2000) | Fewer chunks, less precise retrieval. The LLM receives a lot of text, most of which may be irrelevant to the question. | +| **chunk_size too small** (e.g., 50) | Thousands of tiny chunks. Each chunk lacks context — a sentence like "See the above section" becomes meaningless on its own. | +| **Sweet spot** (300–800 chars) | Roughly 1–2 paragraphs. Enough context to be meaningful, small enough to be precise. | + +### Why cosine similarity beats keyword search + +Traditional search (e.g., `grep`, SQL `LIKE`) requires exact word matches. Search for "car" and you won't find documents that say "automobile" or "vehicle". + +Semantic search (cosine similarity over embeddings) understands *meaning*: +- "car", "automobile", "vehicle", "sedan" → all have very similar embeddings +- You can ask "What's the fastest way to travel?" and find chunks about "high-speed rail" or "airplane travel" — no exact keyword overlap needed + +### What does k mean in top-k retrieval? + +`k` is the number of document chunks retrieved per question. + +- **k=1**: Only the single best match. Very precise but may miss relevant context. +- **k=3** (default): A good balance. Captures the primary answer + nearby supporting text. +- **k=10**: Comprehensive but may include loosely related chunks that dilute the LLM's focus. + +Use `--k 5` on the command line to experiment. If the LLM keeps saying "I don't know" on questions you know are in the docs, try increasing k. + +--- + +## Troubleshooting + +### `OPENAI_API_KEY is not set` +```bash +cp .env.example .env +# edit .env and add your key +``` + +### `No documents were loaded` +Make sure you have files in `data/sample_docs/`. Only `.pdf`, `.txt`, and `.docx` are supported. + +### `FileNotFoundError: data/sample_docs does not exist` +```bash +mkdir -p data/sample_docs +# then add your files +``` + +### `Error: Connection refused` (Ollama) +Make sure Ollama is running: +```bash +ollama serve +``` + +### `Model not found` (Ollama) +Pull the model first: +```bash +ollama pull llama3 +``` + +### Answers seem wrong or generic +1. Run with `--debug` to see which chunks are being retrieved +2. Check the sources printed after each answer — are they the right files? +3. Try deleting `faiss_index/` and rebuilding — you may have stale embeddings +4. Try increasing `--k` to retrieve more context + +### `pip install` fails on `faiss-cpu` +On some systems you may need to install build tools: +```bash +# Ubuntu/Debian +sudo apt-get install build-essential + +# macOS +xcode-select --install +``` + +--- + +## Project Structure + +``` +01-rag-from-scratch/ +├── README.md ← You are here +├── requirements.txt ← Python dependencies +├── .env.example ← Template for your API keys +├── main.py ← Entry point — ties all 6 steps together +├── data/ +│ └── sample_docs/ ← Drop your .pdf/.txt/.docx files here +└── src/ + ├── __init__.py ← Makes src/ a Python package + ├── document_loader.py ← Step 1: Load documents from disk + ├── chunker.py ← Step 2: Split documents into chunks + ├── embedder.py ← Step 3: Convert text to vectors + ├── vector_store.py ← Step 4: Store/search vectors with FAISS + ├── retriever.py ← Step 5: Retrieve relevant chunks + └── generator.py ← Step 6: Generate answers with LLM +``` diff --git a/01-rag-from-scratch/data/sample_docs/.gitkeep b/01-rag-from-scratch/data/sample_docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/01-rag-from-scratch/main.py b/01-rag-from-scratch/main.py new file mode 100644 index 0000000..1baac5e --- /dev/null +++ b/01-rag-from-scratch/main.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +# main.py +# +# RAG FROM SCRATCH — COMPLETE PIPELINE +# ====================================== +# This file ties together all 6 steps of the RAG (Retrieval-Augmented Generation) +# pipeline into a single runnable script. +# +# THE 6 STEPS: +# 1. LOAD → Read .pdf/.txt/.docx files from disk into LangChain Documents +# 2. CHUNK → Split large documents into smaller overlapping chunks +# 3. EMBED → Convert each chunk to a vector using a HuggingFace model +# 4. INDEX → Store all vectors in a FAISS index (saved to disk for reuse) +# 5. RETRIEVE → Given a user question, find the top-k most relevant chunks +# 6. GENERATE → Pass the question + retrieved chunks to an LLM for a grounded answer +# +# USAGE: +# # Single question mode: +# python main.py --question "What are the main topics in these documents?" +# +# # Interactive mode (loops until you type 'quit'): +# python main.py +# +# # Use a local Ollama model instead of OpenAI: +# python main.py --model ollama/llama3 +# +# # Debug mode (shows full prompt sent to LLM and retrieved chunks): +# python main.py --debug --question "What is the refund policy?" +# +# # Specify a different data folder or index location: +# python main.py --data-dir my_docs/ --index-path my_index/ + +import os +import argparse + +# python-dotenv loads KEY=VALUE pairs from your .env file into os.environ. +# This is the standard way to manage API keys without hardcoding them in source code. +from dotenv import load_dotenv + +# Import each step of our pipeline from the src/ package +from src.document_loader import load_documents +from src.chunker import chunk_documents +from src.embedder import get_embedding_model, embed_text +from src.vector_store import get_or_create_vector_store +from src.retriever import get_retriever, retrieve_chunks +from src.generator import build_qa_chain + + +def parse_args(): + """ + Parse command-line arguments. + + argparse is Python's built-in library for CLI argument handling. + It automatically generates --help text from the descriptions below. + """ + parser = argparse.ArgumentParser( + description="RAG from Scratch — Ask questions about your documents using AI.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python main.py + python main.py --question "What are the main topics?" + python main.py --model ollama/llama3 --question "Summarize the documents" + python main.py --debug --question "What is the refund policy?" + python main.py --data-dir /path/to/docs --index-path /path/to/index + """, + ) + + parser.add_argument( + "--data-dir", + default="data/sample_docs", + help="Path to folder containing .pdf, .txt, or .docx files. " + "Default: data/sample_docs", + ) + + parser.add_argument( + "--index-path", + default="faiss_index", + help="Path to save/load the FAISS vector index. " + "Default: faiss_index (created automatically on first run).", + ) + + parser.add_argument( + "--model", + default="gpt-3.5-turbo", + help="LLM to use for answer generation. " + "Options: gpt-3.5-turbo, gpt-4, ollama/llama3, ollama/mistral. " + "Default: gpt-3.5-turbo", + ) + + parser.add_argument( + "--question", + default=None, + help="A single question to answer and exit. " + "If omitted, starts an interactive Q&A loop.", + ) + + parser.add_argument( + "--debug", + action="store_true", + help="Enable debug mode: print the full prompt sent to LLM and " + "detailed chain steps.", + ) + + parser.add_argument( + "--k", + type=int, + default=3, + help="Number of chunks to retrieve per query (top-k). Default: 3.", + ) + + return parser.parse_args() + + +def run_pipeline(args): + """ + Execute the full RAG pipeline end-to-end. + + This function orchestrates all 6 steps, printing clear separators between + each phase so you can follow along and understand what's happening. + """ + + print("=" * 60) + print(" RAG FROM SCRATCH — PIPELINE STARTING") + print("=" * 60) + + # ------------------------------------------------------------------------- + # LOAD ENVIRONMENT VARIABLES + # ------------------------------------------------------------------------- + # .env is NOT committed to git (see .gitignore). Copy .env.example → .env + # and fill in your OPENAI_API_KEY before running with an OpenAI model. + load_dotenv() + + # Warn early if using OpenAI but the API key is missing + if not args.model.startswith("ollama/") and not os.getenv("OPENAI_API_KEY"): + print( + "\n⚠️ WARNING: OPENAI_API_KEY is not set in your environment.\n" + " Either:\n" + " 1. Copy .env.example to .env and add your API key, OR\n" + " 2. Use a local model with --model ollama/llama3\n" + ) + + # ------------------------------------------------------------------------- + # STEP 1: LOAD DOCUMENTS + # ------------------------------------------------------------------------- + print("\n" + "─" * 60) + print("STEP 1/6: Loading documents") + print("─" * 60) + print(f" Source directory: {args.data_dir}") + + documents = load_documents(args.data_dir) + + # If no documents were found, we can't continue — tell the user what to do + if not documents: + print( + "\n❌ No documents loaded. Please add .pdf, .txt, or .docx files to:\n" + f" {args.data_dir}\n" + "\nThen re-run: python main.py" + ) + return + + # ------------------------------------------------------------------------- + # STEP 2: CHUNK DOCUMENTS + # ------------------------------------------------------------------------- + print("\n" + "─" * 60) + print("STEP 2/6: Chunking documents") + print("─" * 60) + + chunks = chunk_documents( + documents, + chunk_size=500, # ~1-2 short paragraphs per chunk + chunk_overlap=50, # 50 chars of overlap to preserve context at boundaries + ) + + # ------------------------------------------------------------------------- + # STEP 3: LOAD EMBEDDING MODEL + # ------------------------------------------------------------------------- + print("\n" + "─" * 60) + print("STEP 3/6: Loading embedding model") + print("─" * 60) + print(" Model: all-MiniLM-L6-v2 (free, local, no API key needed)") + + embedding_model = get_embedding_model("all-MiniLM-L6-v2") + + # DEMO: Show what an embedding vector looks like (educational, not required) + if args.debug and chunks: + embed_text(chunks[0].page_content[:100], embedding_model) + + # ------------------------------------------------------------------------- + # STEP 4: BUILD OR LOAD VECTOR STORE + # ------------------------------------------------------------------------- + print("\n" + "─" * 60) + print("STEP 4/6: Building / loading FAISS vector store") + print("─" * 60) + print(f" Index location: {args.index_path}/") + print(f" Tip: Delete '{args.index_path}/' to force a full rebuild.") + + vector_store = get_or_create_vector_store( + chunks=chunks, + embedding_model=embedding_model, + path=args.index_path, + ) + + # ------------------------------------------------------------------------- + # STEP 5: SET UP RETRIEVER + # ------------------------------------------------------------------------- + print("\n" + "─" * 60) + print("STEP 5/6: Configuring retriever") + print("─" * 60) + print(f" Retrieval strategy: cosine similarity, top-k={args.k}") + + retriever = get_retriever(vector_store, k=args.k) + + # ------------------------------------------------------------------------- + # STEP 6: BUILD QA CHAIN (LLM + RETRIEVER) + # ------------------------------------------------------------------------- + print("\n" + "─" * 60) + print("STEP 6/6: Building QA chain (LLM + Retriever)") + print("─" * 60) + + qa_chain = build_qa_chain( + retriever=retriever, + model_name=args.model, + debug=args.debug, + ) + + print("\n" + "=" * 60) + print(" PIPELINE READY — Let's ask some questions!") + print("=" * 60) + + # ------------------------------------------------------------------------- + # Q&A PHASE: Single question or interactive loop + # ------------------------------------------------------------------------- + + if args.question: + # Single question mode — answer it and exit + ask_question(qa_chain, args.question, args.debug) + else: + # Interactive mode — keep asking until the user types 'quit' or 'exit' + print("\n💬 Interactive Q&A Mode") + print(" Type your question and press Enter.") + print(" Type 'quit' or 'exit' to stop.\n") + + # Sample question to get the user started + sample_question = "What are the main topics covered in these documents?" + print(f" 💡 Sample question: {sample_question}\n") + + while True: + try: + question = input("Your question: ").strip() + except (KeyboardInterrupt, EOFError): + # Handle Ctrl+C gracefully + print("\n\nGoodbye! 👋") + break + + if not question: + print(" (Please type a question, or 'quit' to exit)") + continue + + if question.lower() in ("quit", "exit", "q"): + print("Goodbye! 👋") + break + + ask_question(qa_chain, question, args.debug) + + +def ask_question(qa_chain, question: str, debug: bool = False): + """ + Ask a single question and print the answer with source attribution. + + Args: + qa_chain: The assembled RetrievalQA chain. + question (str): The question to ask. + debug (bool): If True, print source document details. + """ + + print(f"\n❓ Question: {question}") + print(" (Retrieving relevant chunks and generating answer...)\n") + + try: + # .invoke() runs the full chain: + # question → embed → FAISS search → retrieve chunks → fill prompt → LLM → answer + result = qa_chain.invoke({"query": question}) + + # The result dict has: + # result["result"] → the LLM's answer string + # result["source_documents"] → list of Document objects used as context + answer = result["result"] + source_docs = result.get("source_documents", []) + + print(f"💡 Answer:\n{answer}") + + # Show which source documents contributed to this answer + if source_docs: + print("\n📚 Sources used:") + seen_sources = set() + for doc in source_docs: + source = doc.metadata.get("source", "unknown") + page = doc.metadata.get("page", "") + page_info = f", page {page}" if page != "" else "" + source_key = f"{source}{page_info}" + + # Deduplicate — a source file may appear multiple times (different chunks) + if source_key not in seen_sources: + print(f" • {source_key}") + seen_sources.add(source_key) + + # In debug mode, show the actual chunk text used + if debug: + print(f" Context: {doc.page_content[:150]}...") + + except Exception as e: + print(f"\n❌ Error generating answer: {e}") + print( + "\nCommon causes:\n" + " • Missing OPENAI_API_KEY (check your .env file)\n" + " • Ollama not running (start with: ollama serve)\n" + " • Model not pulled (run: ollama pull llama3)\n" + " • Network connectivity issues\n" + ) + + print() # blank line for readability between questions + + +if __name__ == "__main__": + args = parse_args() + run_pipeline(args) diff --git a/01-rag-from-scratch/requirements.txt b/01-rag-from-scratch/requirements.txt new file mode 100644 index 0000000..89a9dc3 --- /dev/null +++ b/01-rag-from-scratch/requirements.txt @@ -0,0 +1,9 @@ +langchain==0.1.20 +langchain-community==0.0.38 +langchain-openai==0.1.6 +faiss-cpu==1.8.0 +sentence-transformers==2.7.0 +pypdf==4.2.0 +python-docx==1.1.2 +openai==1.30.1 +python-dotenv==1.0.1 diff --git a/01-rag-from-scratch/src/__init__.py b/01-rag-from-scratch/src/__init__.py new file mode 100644 index 0000000..369aae7 --- /dev/null +++ b/01-rag-from-scratch/src/__init__.py @@ -0,0 +1,5 @@ +# src/__init__.py +# Makes the src/ directory a Python package so we can do: +# from src.document_loader import load_documents +# from src.chunker import chunk_documents +# etc. from main.py diff --git a/01-rag-from-scratch/src/chunker.py b/01-rag-from-scratch/src/chunker.py new file mode 100644 index 0000000..bf4933d --- /dev/null +++ b/01-rag-from-scratch/src/chunker.py @@ -0,0 +1,107 @@ +# src/chunker.py +# +# STEP 2 OF THE RAG PIPELINE: CHUNKING DOCUMENTS +# +# WHY DO WE SPLIT DOCUMENTS INTO CHUNKS? +# ---------------------------------------- +# Large Language Models (LLMs) have a "context window" — a hard limit on how much +# text they can receive in a single prompt. For example, GPT-3.5-turbo has a ~16k +# token limit (roughly 12,000 words). If your document is a 200-page PDF, you +# CANNOT send the whole thing to the LLM at once. +# +# Even if you could, it's wasteful: most of the document is irrelevant to any +# given question. We only want to send the 2-3 paragraphs that are actually useful. +# +# The solution: split documents into small "chunks", embed each chunk as a vector, +# store them in a vector database, and at query time retrieve ONLY the most relevant +# chunks to include in the LLM prompt. +# +# WHAT IS chunk_overlap AND WHY DOES IT MATTER? +# ----------------------------------------------- +# Imagine a document with this text: +# "...the policy expires on December 31st. Renewal must be submitted 30 days..." +# +# Without overlap, if the split happens between "December 31st." and "Renewal", +# one chunk ends with an incomplete thought and the other starts mid-context. +# With overlap (e.g., 50 characters), the second chunk will start a bit before +# "Renewal", capturing "...policy expires on December 31st. Renewal..." — giving +# the LLM enough context to understand the sentence properly. +# +# HOW DOES RecursiveCharacterTextSplitter WORK? +# ----------------------------------------------- +# It tries to split text in a smart order of preference: +# 1. Split on paragraph breaks ("\n\n") first — preserves paragraph structure +# 2. If still too long, split on newlines ("\n") — preserves line structure +# 3. If still too long, split on spaces (" ") — preserves word boundaries +# 4. Last resort: split on individual characters — avoids going over limit +# +# This is smarter than a naive "split every N characters" approach because it +# tries to keep semantically coherent units together. +# +# TUNING chunk_size: +# ------------------- +# Too LARGE (e.g., 2000): Fewer chunks, retrieval is less precise. +# The LLM may receive a lot of irrelevant text. +# +# Too SMALL (e.g., 50): Many chunks, each missing context. +# A sentence like "See section above" becomes meaningless alone. +# +# Sweet spot: 300–800 characters (roughly 1-2 short paragraphs). +# Default here is 500 with 50-character overlap — a good starting point. + +from langchain.text_splitter import RecursiveCharacterTextSplitter + + +def chunk_documents( + documents: list, + chunk_size: int = 500, + chunk_overlap: int = 50, +) -> list: + """ + Split a list of LangChain Documents into smaller chunks. + + Each chunk is itself a LangChain Document object, inheriting the metadata + of the original document (so we still know which file each chunk came from). + + Args: + documents (list): List of LangChain Document objects (from document_loader). + chunk_size (int): Maximum number of characters per chunk. Default: 500. + chunk_overlap (int): Number of characters to overlap between consecutive chunks. + Helps preserve context at boundaries. Default: 50. + + Returns: + list: A (usually much longer) list of smaller LangChain Document objects. + + Example: + chunks = chunk_documents(documents, chunk_size=500, chunk_overlap=50) + print(f"Created {len(chunks)} chunks") + print(chunks[0].page_content) # first chunk's text + print(chunks[0].metadata) # same metadata as parent document + """ + + print(f"\n📐 Chunking {len(documents)} document(s)...") + print(f" Settings: chunk_size={chunk_size}, chunk_overlap={chunk_overlap}") + + # Create the splitter with our chosen settings. + # separators: the list of strings it will try to split on, in order of preference. + splitter = RecursiveCharacterTextSplitter( + chunk_size=chunk_size, + chunk_overlap=chunk_overlap, + # Try these separators in order — paragraph breaks → newlines → spaces → characters + separators=["\n\n", "\n", " ", ""], + # length_function: how to measure "size". len() counts characters. + # You could swap this for a token counter if you want chunk_size in tokens. + length_function=len, + ) + + # split_documents() handles the full list at once and preserves metadata. + # It returns a new list of Document objects — one per chunk. + chunks = splitter.split_documents(documents) + + print(f"✅ Created {len(chunks)} chunks from {len(documents)} document(s)") + print( + f" Average chunk size: " + f"~{sum(len(c.page_content) for c in chunks) // max(len(chunks), 1)} characters" + ) + + return chunks diff --git a/01-rag-from-scratch/src/document_loader.py b/01-rag-from-scratch/src/document_loader.py new file mode 100644 index 0000000..2123be4 --- /dev/null +++ b/01-rag-from-scratch/src/document_loader.py @@ -0,0 +1,125 @@ +# src/document_loader.py +# +# STEP 1 OF THE RAG PIPELINE: LOADING DOCUMENTS +# +# Before we can answer questions about your documents, we first need to READ them. +# This module handles loading different file types (.pdf, .txt, .docx) into a +# common format that LangChain can work with. +# +# What is a LangChain "Document" object? +# ---------------------------------------- +# LangChain uses a Document object to represent a piece of text. It has two fields: +# +# document.page_content → the actual text string (e.g., "The capital of France is Paris...") +# document.metadata → a dict with info about where the text came from +# e.g., {"source": "data/sample_docs/report.pdf", "page": 2} +# +# Why use Documents instead of plain strings? +# Because we want to keep track of WHERE each piece of text came from. +# When the LLM answers a question, we can tell the user "this answer came from page 3 of report.pdf" +# — that's only possible if we preserve the metadata through the pipeline. + +import os +from pathlib import Path + +# LangChain community loaders for different file types. +# These loaders know how to read each format and return a list of Document objects. +from langchain_community.document_loaders import ( + PyPDFLoader, # Reads PDF files — returns one Document per page + TextLoader, # Reads plain .txt files — returns one Document per file + Docx2txtLoader, # Reads .docx (Word) files — returns one Document per file +) + + +def load_documents(data_dir: str) -> list: + """ + Load all supported documents from a directory. + + Walks through the given directory, finds all .pdf, .txt, and .docx files, + loads each one using the appropriate loader, and returns a flat list of + LangChain Document objects. + + Args: + data_dir (str): Path to the folder containing your documents. + e.g., "data/sample_docs" + + Returns: + list: A list of LangChain Document objects. Each document has: + - page_content: the text extracted from the file + - metadata: dict containing at minimum {"source": } + + Example: + documents = load_documents("data/sample_docs") + print(documents[0].page_content) # prints raw text + print(documents[0].metadata) # prints {"source": "data/sample_docs/report.pdf", "page": 0} + """ + + # Convert to a Path object for easier cross-platform file handling + data_path = Path(data_dir) + + # Make sure the directory actually exists before trying to read it + if not data_path.exists(): + raise FileNotFoundError( + f"Data directory '{data_dir}' does not exist. " + f"Please create it and add some .pdf, .txt, or .docx files." + ) + + all_documents = [] # We'll collect all Document objects here + + # Map each file extension to its corresponding LangChain loader class. + # This makes it easy to add new file types later — just add an entry here. + loader_map = { + ".pdf": PyPDFLoader, + ".txt": TextLoader, + ".docx": Docx2txtLoader, + } + + # Walk through every file in the directory (and subdirectories) + for file_path in sorted(data_path.rglob("*")): + + # Skip directories — we only want files + if not file_path.is_file(): + continue + + # Skip hidden files (e.g., .gitkeep, .DS_Store) + if file_path.name.startswith("."): + continue + + file_ext = file_path.suffix.lower() # e.g., ".pdf", ".txt", ".docx" + + # Check if we have a loader for this file type + if file_ext not in loader_map: + # Unsupported file type — skip it with a warning + print(f" ⚠️ Skipping unsupported file type: {file_path.name} ({file_ext})") + continue + + print(f" 📄 Loading: {file_path.name}") + + try: + # Instantiate the appropriate loader with the file path + loader_class = loader_map[file_ext] + loader = loader_class(str(file_path)) + + # .load() returns a list of Document objects. + # For PDFs, each page becomes its own Document. + # For TXT/DOCX, the whole file is usually one Document. + documents = loader.load() + + print(f" → Loaded {len(documents)} document chunk(s)") + all_documents.extend(documents) + + except Exception as e: + # Don't crash the whole pipeline if one file fails to load. + # Print the error and continue with the remaining files. + print(f" ❌ Failed to load {file_path.name}: {e}") + continue + + if len(all_documents) == 0: + print( + f"\n⚠️ No documents were loaded from '{data_dir}'.\n" + f" Add some .pdf, .txt, or .docx files and try again." + ) + else: + print(f"\n✅ Total documents loaded: {len(all_documents)}") + + return all_documents diff --git a/01-rag-from-scratch/src/embedder.py b/01-rag-from-scratch/src/embedder.py new file mode 100644 index 0000000..30532aa --- /dev/null +++ b/01-rag-from-scratch/src/embedder.py @@ -0,0 +1,114 @@ +# src/embedder.py +# +# STEP 3 OF THE RAG PIPELINE: EMBEDDING TEXT INTO VECTORS +# +# WHAT ARE EMBEDDINGS? +# ---------------------- +# An "embedding" is a way to represent text as a list of numbers (a vector). +# The key insight is that semantically similar text produces numerically similar vectors. +# +# For example, these two sentences will have very similar vectors: +# "The cat sat on the mat." +# "A feline rested on the rug." +# +# Even though they share no keywords, an embedding model understands they mean +# the same thing. This is the magic that makes semantic search work! +# +# WHY all-MiniLM-L6-v2? +# ----------------------- +# We use the "all-MiniLM-L6-v2" model from HuggingFace for several reasons: +# +# ✅ FREE — no API key required, runs entirely on your local machine +# ✅ FAST — it's a small, distilled model (only ~80MB to download) +# ✅ GOOD QUALITY — despite its size, it scores well on semantic benchmarks +# ✅ 384 DIMENSIONS — each piece of text becomes a list of 384 numbers +# +# Alternative: OpenAI's text-embedding-ada-002 is more powerful but costs money +# and requires an API key. For learning, the free HuggingFace model is perfect. +# +# WHAT DOES "384 DIMENSIONS" MEAN? +# ---------------------------------- +# Each text string gets converted to a list of 384 floating-point numbers. +# Think of it as a point in 384-dimensional space. Similar texts are "close" +# to each other in this space; unrelated texts are "far apart." +# +# WHY COSINE SIMILARITY? +# ----------------------- +# To find which chunks are most relevant to a query, we compare their vectors. +# We use "cosine similarity" which measures the angle between two vectors: +# - Score of 1.0 = identical direction = very similar meaning +# - Score of 0.0 = perpendicular = unrelated +# - Score of -1.0 = opposite direction = opposite meaning (rare in practice) +# +# Cosine similarity is preferred over Euclidean distance because it's insensitive +# to the magnitude of the vectors — only the direction matters. + +from langchain_community.embeddings import HuggingFaceEmbeddings + + +def get_embedding_model(model_name: str = "all-MiniLM-L6-v2") -> HuggingFaceEmbeddings: + """ + Load a HuggingFace sentence-transformer embedding model. + + The first time you call this, it will download the model (~80MB) from + HuggingFace Hub and cache it locally. Subsequent calls use the cache. + + Args: + model_name (str): HuggingFace model name. Default: "all-MiniLM-L6-v2" + Other options: "all-mpnet-base-v2" (higher quality, slower) + + Returns: + HuggingFaceEmbeddings: A LangChain-compatible embedding model object. + Call model.embed_documents([...]) or model.embed_query("...") + """ + + print(f"\n🔢 Loading embedding model: '{model_name}'") + print(f" (First run will download ~80MB — subsequent runs use cache)") + + # model_kwargs: passed directly to the underlying sentence-transformers library + # device="cpu" means we run on CPU — change to "cuda" if you have a GPU + embedding_model = HuggingFaceEmbeddings( + model_name=model_name, + model_kwargs={"device": "cpu"}, + # encode_kwargs: controls how the model encodes text into vectors + # normalize_embeddings=True ensures vectors have length 1.0, + # which makes cosine similarity equivalent to dot product (faster computation) + encode_kwargs={"normalize_embeddings": True}, + ) + + print(f"✅ Embedding model loaded successfully") + return embedding_model + + +def embed_text(text: str, model) -> list: + """ + Embed a single string and show what the resulting vector looks like. + + This is a teaching/demo function — it helps beginners see that "embedding" + just means converting text into a list of numbers. + + Args: + text (str): Any string to embed. + model: A loaded HuggingFaceEmbeddings (or compatible) model. + + Returns: + list: A list of 384 floats representing the text's meaning as a vector. + + Example: + model = get_embedding_model() + vector = embed_text("Hello world", model) + # Prints: Vector shape: 384 dimensions + # Prints: First 5 values: [0.023, -0.041, 0.118, ...] + """ + + # embed_query() is the LangChain method for embedding a single string. + # (embed_documents() is for embedding a list of strings all at once — more efficient.) + vector = model.embed_query(text) + + # Show the learner what a vector actually looks like + print(f"\n🔍 Embedding demo for: '{text[:60]}{'...' if len(text) > 60 else ''}'") + print(f" Vector shape: {len(vector)} dimensions") + print(f" First 5 values: {[round(v, 4) for v in vector[:5]]}") + print(f" (Each number encodes a tiny aspect of the text's meaning)") + + return vector diff --git a/01-rag-from-scratch/src/generator.py b/01-rag-from-scratch/src/generator.py new file mode 100644 index 0000000..a917be0 --- /dev/null +++ b/01-rag-from-scratch/src/generator.py @@ -0,0 +1,167 @@ +# src/generator.py +# +# STEP 6 OF THE RAG PIPELINE: GENERATING THE ANSWER WITH AN LLM +# +# WHAT DOES RetrievalQA DO? +# -------------------------- +# RetrievalQA is a LangChain "chain" that combines two things: +# 1. A retriever (which fetches relevant chunks from FAISS) +# 2. An LLM (which reads those chunks and generates an answer) +# +# It handles the "stuffing" step: it takes the retrieved Document objects, +# extracts their page_content, concatenates them into a {context} block, +# and injects that into our prompt template before calling the LLM. +# +# WHY THE "ONLY USE CONTEXT" INSTRUCTION PREVENTS HALLUCINATION: +# --------------------------------------------------------------- +# LLMs are trained on massive datasets and have general knowledge baked in. +# Without explicit instructions, an LLM might answer from its training data +# instead of your documents — which defeats the entire purpose of RAG. +# +# The system instruction "answer ONLY based on the following context" tells +# the LLM to restrict itself to what we provide. The fallback phrase +# "I don't know based on the provided documents" prevents the LLM from +# making things up when the answer truly isn't in the documents. +# +# This is the most important prompt engineering technique in RAG systems. +# +# WHAT IS THE SYSTEM PROMPT PATTERN? +# ------------------------------------ +# A "prompt template" is a string with placeholder variables (like {context} +# and {question}) that get filled in at runtime. This lets us: +# - Set the LLM's behavior with clear instructions at the top +# - Inject the retrieved context dynamically for each query +# - Ask the user's question at the end +# +# The resulting filled-in prompt is what actually gets sent to the LLM API. +# With debug=True, you can print this full prompt to see exactly what the LLM receives. + +from langchain_openai import ChatOpenAI +from langchain.chains import RetrievalQA +from langchain.prompts import PromptTemplate + + +# The prompt template instructs the LLM to stay grounded in the provided context. +# {context} will be replaced by the retrieved chunks (as a single text block). +# {question} will be replaced by the user's question. +RAG_PROMPT_TEMPLATE = """You are a helpful assistant. Answer the question based ONLY on the following context. +If the answer is not in the context, say "I don't know based on the provided documents." +Do not use your general knowledge. + +Context: +{context} + +Question: {question} + +Answer:""" + + +def build_qa_chain( + retriever, + model_name: str = "gpt-3.5-turbo", + debug: bool = False, +): + """ + Build a RetrievalQA chain that combines document retrieval with LLM generation. + + This is the final assembly step of the RAG pipeline: + User question + → retriever fetches top-k relevant chunks from FAISS + → chunks are injected into the prompt template as {context} + → LLM reads context + question and generates a grounded answer + + Args: + retriever: A LangChain retriever (from retriever.py). + model_name (str): LLM to use. Options: + - "gpt-3.5-turbo" (OpenAI, requires OPENAI_API_KEY) + - "gpt-4" (OpenAI, more powerful, costs more) + - "ollama/llama3" (local Ollama, no API key needed) + - "ollama/mistral" (local Ollama, no API key needed) + debug (bool): If True, prints the full prompt sent to the LLM. + Useful for understanding what the LLM actually receives. + + Returns: + RetrievalQA: A runnable chain. Call chain.invoke({"query": "your question"}) + to get an answer dict with keys "query", "result", "source_documents". + + Example: + chain = build_qa_chain(retriever, model_name="gpt-3.5-turbo", debug=True) + result = chain.invoke({"query": "What is the refund policy?"}) + print(result["result"]) + """ + + print(f"\n🤖 Building QA chain with model: '{model_name}'") + + # ------------------------------------------------------------------------- + # SELECT THE LLM BASED ON model_name + # ------------------------------------------------------------------------- + if model_name.startswith("ollama/"): + # Ollama runs LLMs locally on your machine — no API key, no cost. + # Install Ollama from https://ollama.com and pull a model: + # ollama pull llama3 + # ollama pull mistral + # + # The model_name format is "ollama/" e.g. "ollama/llama3" + import os + from langchain_community.llms import Ollama + + # Extract the model tag after the "ollama/" prefix + ollama_model = model_name.split("/", 1)[1] + base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + + print(f" Using local Ollama model '{ollama_model}' at {base_url}") + llm = Ollama(model=ollama_model, base_url=base_url) + + else: + # OpenAI models (gpt-3.5-turbo, gpt-4, gpt-4o, etc.) + # Requires OPENAI_API_KEY to be set in your .env file. + # + # temperature=0 means "deterministic" — the LLM always picks the highest + # probability token. For Q&A this is ideal; you want consistent, factual + # answers rather than creative variation. + llm = ChatOpenAI( + model_name=model_name, + temperature=0, # 0 = deterministic/factual, 1 = more creative/varied + ) + print(f" Using OpenAI model '{model_name}' (ensure OPENAI_API_KEY is set)") + + # ------------------------------------------------------------------------- + # BUILD THE PROMPT TEMPLATE + # ------------------------------------------------------------------------- + prompt = PromptTemplate( + template=RAG_PROMPT_TEMPLATE, + input_variables=["context", "question"], # placeholders to fill at runtime + ) + + # If debug mode is on, show the template so learners can see the structure + if debug: + print("\n🐛 DEBUG: Prompt template being used:") + print("-" * 60) + print(RAG_PROMPT_TEMPLATE) + print("-" * 60) + + # ------------------------------------------------------------------------- + # ASSEMBLE THE RetrievalQA CHAIN + # ------------------------------------------------------------------------- + # chain_type="stuff" means: take all retrieved chunks, "stuff" them all into + # the context at once. This works well for small k values (k=3 to k=5). + # + # Other chain_type options: + # "map_reduce" — summarize each chunk separately, then combine (handles many chunks) + # "refine" — iteratively refine the answer chunk by chunk (slower but thorough) + # "map_rerank" — score each chunk separately and pick the best answer + # + # For most use cases with k<=5, "stuff" is the simplest and most effective. + qa_chain = RetrievalQA.from_chain_type( + llm=llm, + chain_type="stuff", + retriever=retriever, + return_source_documents=True, # include source docs in the result dict + chain_type_kwargs={ + "prompt": prompt, + "verbose": debug, # if debug=True, LangChain will print internal chain steps + }, + ) + + print(f"✅ QA chain ready") + return qa_chain diff --git a/01-rag-from-scratch/src/retriever.py b/01-rag-from-scratch/src/retriever.py new file mode 100644 index 0000000..e673204 --- /dev/null +++ b/01-rag-from-scratch/src/retriever.py @@ -0,0 +1,117 @@ +# src/retriever.py +# +# STEP 5 OF THE RAG PIPELINE: RETRIEVING RELEVANT CHUNKS +# +# WHAT DOES "RETRIEVAL" DO IN THE RAG PIPELINE? +# ----------------------------------------------- +# At this point we have: +# - All our document chunks stored as vectors in FAISS +# - A user's question +# +# The retriever's job is to: +# 1. Embed the question using the SAME embedding model we used for the chunks +# 2. Search FAISS for the k most similar chunk vectors to the question vector +# 3. Return those k chunks (as Document objects) so the LLM can read them +# +# The LLM never reads the whole document database — it only reads these k chunks. +# This is what makes RAG efficient and precise. +# +# WHAT IS k IN TOP-k RETRIEVAL? +# -------------------------------- +# k is the number of chunks we retrieve. Think of it as: +# "Give me the top 3 most relevant paragraphs from my documents." +# +# k=1: Very focused. Only the single best match. May miss related info. +# k=3: A good balance. Captures the main answer + nearby context. (default) +# k=10: Comprehensive but may include loosely related chunks that confuse the LLM. +# +# Rule of thumb: Start with k=3 and increase if the LLM says "I don't know" +# on questions you KNOW are in your documents. +# +# WHY COSINE SIMILARITY BEATS KEYWORD SEARCH: +# -------------------------------------------- +# Traditional search (like grep or SQL LIKE) requires exact keyword matches. +# If your document says "automobile" and you search for "car", you get nothing. +# +# Semantic (vector) search understands meaning: +# "car" → very similar vector to "automobile", "vehicle", "sedan" +# +# This means you can ask questions in natural language and still find relevant +# chunks even when the exact words don't match. This is crucial for Q&A systems +# where users phrase questions differently than how documents are written. + + +def get_retriever(vector_store, k: int = 3): + """ + Create a LangChain retriever from a FAISS vector store. + + A LangChain "retriever" is a standardized interface that wraps the vector store + and exposes a simple .invoke(query) method. This makes it easy to plug into + LangChain chains (like RetrievalQA in generator.py). + + Args: + vector_store: A FAISS vector store (from vector_store.py). + k (int): How many chunks to retrieve per query. Default: 3. + Increase if answers are missing info; decrease if too noisy. + + Returns: + A LangChain VectorStoreRetriever object. + + Example: + retriever = get_retriever(vector_store, k=3) + docs = retriever.invoke("What is the refund policy?") + """ + + # as_retriever() wraps the FAISS store in a Retriever interface. + # search_type="similarity" uses cosine similarity (since we normalized embeddings). + # Other options: "mmr" (Maximal Marginal Relevance — reduces redundancy among results) + retriever = vector_store.as_retriever( + search_type="similarity", + search_kwargs={"k": k}, # retrieve top-k most similar chunks + ) + + print(f"\n🔎 Retriever configured (top-k={k}, search_type=similarity)") + return retriever + + +def retrieve_chunks(question: str, retriever) -> list: + """ + Retrieve the most relevant document chunks for a given question. + + Also prints the retrieved chunks so learners can inspect what gets passed + to the LLM. This transparency is key to understanding and debugging RAG. + + Args: + question (str): The user's question in natural language. + retriever: A LangChain retriever (from get_retriever()). + + Returns: + list: A list of LangChain Document objects — the most relevant chunks. + Each has .page_content (the text) and .metadata (source file, page, etc.) + + Example: + chunks = retrieve_chunks("What is the refund policy?", retriever) + for chunk in chunks: + print(chunk.page_content) + print(chunk.metadata["source"]) + """ + + print(f"\n🔍 Retrieving relevant chunks for: '{question}'") + + # .invoke() embeds the question and runs the similarity search + relevant_chunks = retriever.invoke(question) + + print(f"\n📋 Top {len(relevant_chunks)} retrieved chunk(s):") + print("-" * 60) + + for i, chunk in enumerate(relevant_chunks, 1): + source = chunk.metadata.get("source", "unknown") + page = chunk.metadata.get("page", "") + page_info = f" (page {page})" if page != "" else "" + + print(f"\n[Chunk {i}] Source: {source}{page_info}") + print(f"Content preview: {chunk.page_content[:200]}...") + + print("-" * 60) + + return relevant_chunks diff --git a/01-rag-from-scratch/src/vector_store.py b/01-rag-from-scratch/src/vector_store.py new file mode 100644 index 0000000..472a3a0 --- /dev/null +++ b/01-rag-from-scratch/src/vector_store.py @@ -0,0 +1,159 @@ +# src/vector_store.py +# +# STEP 4 OF THE RAG PIPELINE: STORING VECTORS IN FAISS +# +# WHAT IS FAISS? +# --------------- +# FAISS (Facebook AI Similarity Search) is an open-source library developed by +# Meta (Facebook) Research. It is specifically designed for one task: +# +# Given a query vector, quickly find the most similar vectors in a large collection. +# +# This is called "Approximate Nearest Neighbor" (ANN) search. Doing this naively +# (comparing the query against every stored vector one by one) would be too slow +# at scale. FAISS builds an *index* — a special data structure that lets it find +# the top-k similar vectors in milliseconds, even across millions of documents. +# +# HOW FAISS WORKS (CONCEPTUALLY): +# --------------------------------- +# 1. During indexing: FAISS takes all your chunk vectors and organizes them into +# a spatial data structure (e.g., an inverted file index or HNSW graph). +# 2. During search: Given a query vector, FAISS navigates the data structure to +# find the nearest neighbors without checking every single vector. +# +# For our use case (hundreds to thousands of chunks), FAISS is near-instant. +# It really shines at millions of vectors, but it's a great habit to use from day one. +# +# WHY SAVE THE INDEX TO DISK? +# ----------------------------- +# Embedding documents takes time (each chunk must be processed by the neural network). +# If we re-ran embedding every time we started the app, we'd waste seconds/minutes +# on every run even when the documents haven't changed. +# +# By saving the FAISS index to disk, we only embed once. On subsequent runs, we +# load the pre-built index from disk in milliseconds. +# +# The saved index consists of two files: +# faiss_index/index.faiss → the actual vector index (binary) +# faiss_index/index.pkl → metadata mapping (which chunk belongs to which vector) + +import os +from langchain_community.vectorstores import FAISS + + +def create_vector_store(chunks: list, embedding_model) -> FAISS: + """ + Embed all chunks and build a FAISS vector store from them. + + This is the "indexing" phase — it calls the embedding model once per chunk + (or in batches) and stores all resulting vectors in a FAISS index. + + Args: + chunks (list): List of LangChain Document objects (from chunker.py). + embedding_model: A loaded HuggingFaceEmbeddings model (from embedder.py). + + Returns: + FAISS: An in-memory FAISS vector store ready for similarity search. + """ + + print(f"\n🗄️ Building FAISS vector store from {len(chunks)} chunks...") + print(f" (Embedding each chunk — this may take a moment on first run)") + + # FAISS.from_documents() does two things in one call: + # 1. Calls embedding_model.embed_documents() on all chunks + # 2. Builds the FAISS index from the resulting vectors + vector_store = FAISS.from_documents( + documents=chunks, + embedding=embedding_model, + ) + + print(f"✅ Vector store created with {len(chunks)} vectors") + return vector_store + + +def save_vector_store(vector_store: FAISS, path: str = "faiss_index") -> None: + """ + Persist the FAISS index to disk so we don't have to re-embed next time. + + Saves two files: + {path}/index.faiss — the binary vector index + {path}/index.pkl — the document metadata mapping + + Args: + vector_store (FAISS): The in-memory FAISS vector store to save. + path (str): Directory path where index files will be written. + Default: "faiss_index" + """ + + # Create the directory if it doesn't exist yet + os.makedirs(path, exist_ok=True) + + # LangChain's FAISS wrapper handles the actual serialization + vector_store.save_local(path) + + print(f"💾 Vector store saved to '{path}/'") + print(f" Files: {path}/index.faiss, {path}/index.pkl") + + +def load_vector_store(path: str, embedding_model) -> FAISS: + """ + Load a previously saved FAISS index from disk. + + Args: + path (str): Directory path where index files are stored. + embedding_model: The SAME embedding model used when the index was created. + IMPORTANT: If you use a different model, the vectors won't + match and search results will be nonsense. + + Returns: + FAISS: The loaded vector store, ready for similarity search. + """ + + print(f"\n📂 Loading existing FAISS index from '{path}/'...") + + # allow_dangerous_deserialization=True is required because FAISS uses pickle + # under the hood. This is safe as long as you trust the source of the index file + # (which you do, since you created it yourself). + vector_store = FAISS.load_local( + folder_path=path, + embeddings=embedding_model, + allow_dangerous_deserialization=True, + ) + + print(f"✅ Vector store loaded from disk") + return vector_store + + +def get_or_create_vector_store( + chunks: list, + embedding_model, + path: str = "faiss_index", +) -> FAISS: + """ + Convenience function: load index from disk if it exists, otherwise build it. + + This is the function you'll call in main.py. It implements a simple cache: + - If '{path}/index.faiss' exists → load it (fast, skips re-embedding) + - Otherwise → embed all chunks and build a new index, then save it + + Args: + chunks (list): List of LangChain Document chunks. + embedding_model: Loaded embedding model. + path (str): Path to save/load the FAISS index. Default: "faiss_index" + + Returns: + FAISS: Ready-to-use vector store. + """ + + # Check if a saved index already exists on disk + index_file = os.path.join(path, "index.faiss") + + if os.path.exists(index_file): + print(f"\n♻️ Found existing FAISS index at '{path}/' — loading from disk") + print(f" (Skipping re-embedding. Delete '{path}/' to force rebuild.)") + return load_vector_store(path, embedding_model) + else: + print(f"\n🆕 No existing index found at '{path}/' — building from scratch") + vector_store = create_vector_store(chunks, embedding_model) + save_vector_store(vector_store, path) + return vector_store From 41ee007453d62e77dba751561277a1f0fcdba4ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:02:16 +0000 Subject: [PATCH 3/9] Add 02-legal-ai-assistant project Complete RAG-powered legal contract analysis tool with: - document_parser.py: PDF/DOCX parsing with section detection - indexer.py: FAISS vector indexing (same pattern as Project 1) - summarizer.py: executive summary with structured JSON output - clause_extractor.py: named clause extraction + plain-English translation - risk_analyzer.py: HIGH/MEDIUM/LOW risk scoring with emoji indicators - conflict_detector.py: internal contradiction detection (with disclaimers) - qa_chain.py: grounded RAG Q&A with mandatory section citations - main.py: full CLI pipeline using Rich for formatted output - prompts/: summary, clause, and risk prompt templates - README.md: setup guide, architecture diagram, limitations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- 02-legal-ai-assistant/.env.example | 8 + 02-legal-ai-assistant/README.md | 225 +++++++++++ .../data/sample_contracts/.gitkeep | 0 02-legal-ai-assistant/main.py | 377 ++++++++++++++++++ .../prompts/clause_prompt.txt | 24 ++ 02-legal-ai-assistant/prompts/risk_prompt.txt | 24 ++ .../prompts/summary_prompt.txt | 14 + 02-legal-ai-assistant/requirements.txt | 11 + 02-legal-ai-assistant/src/__init__.py | 0 02-legal-ai-assistant/src/clause_extractor.py | 150 +++++++ .../src/conflict_detector.py | 174 ++++++++ 02-legal-ai-assistant/src/document_parser.py | 216 ++++++++++ 02-legal-ai-assistant/src/indexer.py | 158 ++++++++ 02-legal-ai-assistant/src/qa_chain.py | 121 ++++++ 02-legal-ai-assistant/src/risk_analyzer.py | 161 ++++++++ 02-legal-ai-assistant/src/summarizer.py | 146 +++++++ 16 files changed, 1809 insertions(+) create mode 100644 02-legal-ai-assistant/.env.example create mode 100644 02-legal-ai-assistant/README.md create mode 100644 02-legal-ai-assistant/data/sample_contracts/.gitkeep create mode 100644 02-legal-ai-assistant/main.py create mode 100644 02-legal-ai-assistant/prompts/clause_prompt.txt create mode 100644 02-legal-ai-assistant/prompts/risk_prompt.txt create mode 100644 02-legal-ai-assistant/prompts/summary_prompt.txt create mode 100644 02-legal-ai-assistant/requirements.txt create mode 100644 02-legal-ai-assistant/src/__init__.py create mode 100644 02-legal-ai-assistant/src/clause_extractor.py create mode 100644 02-legal-ai-assistant/src/conflict_detector.py create mode 100644 02-legal-ai-assistant/src/document_parser.py create mode 100644 02-legal-ai-assistant/src/indexer.py create mode 100644 02-legal-ai-assistant/src/qa_chain.py create mode 100644 02-legal-ai-assistant/src/risk_analyzer.py create mode 100644 02-legal-ai-assistant/src/summarizer.py diff --git a/02-legal-ai-assistant/.env.example b/02-legal-ai-assistant/.env.example new file mode 100644 index 0000000..d28df29 --- /dev/null +++ b/02-legal-ai-assistant/.env.example @@ -0,0 +1,8 @@ +# OpenAI API Key (required - GPT-4 recommended for legal analysis accuracy) +OPENAI_API_KEY=your_openai_api_key_here + +# Model to use (gpt-4 recommended for accuracy, gpt-3.5-turbo for cost savings) +OPENAI_MODEL=gpt-4 + +# Optional: Anthropic Claude API (alternative to OpenAI) +# ANTHROPIC_API_KEY=your_anthropic_key_here diff --git a/02-legal-ai-assistant/README.md b/02-legal-ai-assistant/README.md new file mode 100644 index 0000000..3580048 --- /dev/null +++ b/02-legal-ai-assistant/README.md @@ -0,0 +1,225 @@ +# Legal AI Assistant + +> ⚠️ **DISCLAIMER: This tool is for educational purposes only. It does NOT constitute legal advice. Always consult a qualified attorney before making any legal or business decisions.** + +A Retrieval-Augmented Generation (RAG) pipeline that helps you understand contracts by extracting key clauses, flagging risks, detecting internal conflicts, and answering natural-language questions — all grounded in the actual document text. + +--- + +## What the Tool Does + +- **Parses** PDF and DOCX contract files into structured text with section detection +- **Indexes** the document into a FAISS vector store for semantic search +- **Summarises** the contract: parties, type, effective date, duration, key obligations +- **Extracts clauses**: indemnification, limitation of liability, termination, governing law, IP ownership, confidentiality +- **Analyses risks**: flags HIGH / MEDIUM / LOW risk patterns with plain-English explanations +- **Detects conflicts**: surfaces internal contradictions between clauses +- **Answers questions**: grounded Q&A with mandatory section citations + +--- + +## Supported Document Types + +| Format | Extension | Notes | +|--------|-----------|-------| +| PDF | `.pdf` | Text-based PDFs only. Scanned PDFs require OCR pre-processing. | +| Word | `.docx` | Supports Heading styles for better section detection. | + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ main.py (CLI) │ +└──────────────┬──────────────────────────────────────────────────┘ + │ + ┌──────────▼──────────┐ + │ document_parser.py │ PDF / DOCX → full_text + sections + └──────────┬──────────┘ + │ + ┌──────────▼──────────┐ + │ indexer.py │ text → chunks → HuggingFace embeddings → FAISS + └──────────┬──────────┘ + │ + ┌───────┴────────┐ + │ OpenAI GPT-4 │ (all LLM calls below use this) + └───────┬────────┘ + │ + ┌──────────▼──────────┐ ┌──────────────────────┐ + │ summarizer.py │ │ clause_extractor.py │ + └─────────────────────┘ └──────────┬───────────┘ + │ + ┌──────────▼──────────┐ + │ risk_analyzer.py │ + └──────────┬──────────┘ + │ + ┌──────────▼──────────┐ + │ conflict_detector.py │ + └─────────────────────┘ + ┌─────────────────────┐ + │ qa_chain.py │ FAISS retriever + custom legal prompt + └─────────────────────┘ +``` + +--- + +## Setup + +### 1. Clone / navigate to the project + +```bash +cd 02-legal-ai-assistant +``` + +### 2. Create a virtual environment + +```bash +python -m venv venv +source venv/bin/activate # macOS/Linux +venv\Scripts\activate # Windows +``` + +### 3. Install dependencies + +```bash +pip install -r requirements.txt +``` + +### 4. Configure environment variables + +```bash +cp .env.example .env +# Edit .env and add your OpenAI API key +``` + +```env +OPENAI_API_KEY=sk-... +OPENAI_MODEL=gpt-4 # or gpt-3.5-turbo for lower cost +``` + +### 5. Add a contract file + +Place a PDF or DOCX contract in `data/sample_contracts/` or any other path. + +--- + +## How to Run + +### Full analysis (default) + +```bash +python main.py --file data/sample_contracts/service_agreement.pdf +``` + +### Use a cheaper model (faster, less accurate) + +```bash +python main.py --file contract.pdf --model gpt-3.5-turbo +``` + +### Skip risk analysis and conflict detection + +```bash +python main.py --file contract.pdf --skip-risks --skip-conflicts +``` + +### Ask a single question and exit + +```bash +python main.py --file contract.pdf --question "What are my termination rights?" +``` + +### Interactive Q&A after analysis + +```bash +python main.py --file contract.pdf --interactive +``` + +--- + +## Sample Questions to Ask + +``` +What are my termination rights? +Who owns IP I create during the contract? +What is the liability cap? +How does auto-renewal work? +What information must I keep confidential and for how long? +Which court has jurisdiction over disputes? +Can the company change the terms without my consent? +What happens to my work if the contract is terminated early? +``` + +--- + +## Output Sections Explained + +| Section | What it shows | +|---------|---------------| +| **Executive Summary** | Parties, contract type, effective date, duration, key obligations, plain-English overview | +| **Key Clauses** | Table of named clause types with their section references and plain-English translations | +| **Risk Analysis** | 🔴 HIGH / 🟡 MEDIUM / 🟢 LOW risks with explanations and fair alternatives | +| **Conflict Detection** | Internal contradictions between clauses (e.g. mismatched notice periods) | +| **Q&A** | Grounded answers with mandatory section citations | + +--- + +## Limitations + +> These are not bugs — they are inherent limitations of the technology. + +1. **Cannot reliably detect all conflicts.** The LLM may miss conflicts requiring deep legal expertise or flag false positives. Every flagged conflict must be manually verified. + +2. **PDF extraction may miss some formatting.** Tables lose column alignment, scanned PDFs produce no text, and footnotes may appear mid-sentence. Complex formatting in PDFs will degrade extraction quality. + +3. **LLM can misinterpret complex legal language.** Highly technical, jurisdiction-specific, or archaic legal terms may be interpreted incorrectly. The model is not a lawyer. + +4. **Context window limits truncate long contracts.** Summary and clause extraction are capped at 8 000–12 000 characters. Very long contracts (100+ pages) will have their later sections underweighted. + +5. **Embeddings may not capture domain-specific meaning.** The `all-MiniLM-L6-v2` model was not trained on legal text specifically; niche legal terms may not retrieve optimally. + +6. **Always verify with a qualified attorney.** This tool helps you know WHAT to look for and WHERE to look. It does not replace professional legal review. + +--- + +## Project Structure + +``` +02-legal-ai-assistant/ +├── README.md ← this file +├── requirements.txt +├── .env.example +├── data/ +│ └── sample_contracts/ ← place your PDF/DOCX files here +├── src/ +│ ├── __init__.py +│ ├── document_parser.py ← PDF/DOCX → structured text +│ ├── indexer.py ← text → FAISS vector index +│ ├── summarizer.py ← executive summary generation +│ ├── clause_extractor.py ← named clause extraction +│ ├── risk_analyzer.py ← HIGH/MEDIUM/LOW risk scoring +│ ├── conflict_detector.py ← internal contradiction detection +│ └── qa_chain.py ← RAG Q&A chain +├── prompts/ +│ ├── summary_prompt.txt +│ ├── clause_prompt.txt +│ └── risk_prompt.txt +└── main.py ← CLI entry point +``` + +--- + +## Dependencies + +| Package | Purpose | +|---------|---------| +| `langchain` + `langchain-community` + `langchain-openai` | LLM orchestration and RAG chains | +| `faiss-cpu` | Local vector similarity search | +| `sentence-transformers` | HuggingFace embedding model (runs locally) | +| `pypdf` | PDF text extraction | +| `python-docx` | DOCX parsing | +| `openai` | OpenAI API client | +| `python-dotenv` | `.env` file loading | +| `pydantic` | Data validation | +| `rich` | Formatted terminal output | diff --git a/02-legal-ai-assistant/data/sample_contracts/.gitkeep b/02-legal-ai-assistant/data/sample_contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/02-legal-ai-assistant/main.py b/02-legal-ai-assistant/main.py new file mode 100644 index 0000000..40a9d10 --- /dev/null +++ b/02-legal-ai-assistant/main.py @@ -0,0 +1,377 @@ +""" +main.py — Legal AI Assistant Entry Point + +Full analysis pipeline for legal contracts: + 1. Parse document → structured text + sections + 2. Index for RAG → FAISS vector store + 3. Summarize → executive summary (parties, type, obligations) + 4. Extract clauses → indemnification, IP, termination, etc. + 5. Analyze risks → HIGH/MEDIUM/LOW risk flags + 6. Detect conflicts→ internal contradictions + 7. Q&A → answer specific questions or enter interactive mode + +Usage examples: + python main.py --file data/sample_contracts/service_agreement.pdf + python main.py --file contract.pdf --model gpt-3.5-turbo --skip-conflicts + python main.py --file contract.pdf --question "What are my termination rights?" + python main.py --file contract.pdf --interactive +""" + +import argparse +import os +import sys + +from dotenv import load_dotenv +from rich.console import Console +from rich.panel import Panel +from rich.rule import Rule +from rich.table import Table +from rich import box + +# --------------------------------------------------------------------------- +# Load environment variables from .env file +# --------------------------------------------------------------------------- +load_dotenv() + +console = Console() + + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Legal AI Assistant — contract analysis powered by LLMs + RAG", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python main.py --file contract.pdf + python main.py --file contract.pdf --model gpt-3.5-turbo --skip-risks + python main.py --file contract.pdf --question "Who owns the IP?" + python main.py --file contract.pdf --interactive + """, + ) + parser.add_argument( + "--file", + type=str, + help="Path to the contract file (PDF or DOCX). Required unless --interactive.", + ) + parser.add_argument( + "--model", + type=str, + default=os.getenv("OPENAI_MODEL", "gpt-4"), + help="OpenAI model to use (default: gpt-4).", + ) + parser.add_argument( + "--skip-risks", + action="store_true", + help="Skip the risk analysis step.", + ) + parser.add_argument( + "--skip-conflicts", + action="store_true", + help="Skip the conflict detection step.", + ) + parser.add_argument( + "--question", + type=str, + default=None, + help="Ask a single question about the contract and exit.", + ) + parser.add_argument( + "--interactive", + action="store_true", + help="Start an interactive Q&A loop after analysis.", + ) + return parser + + +# --------------------------------------------------------------------------- +# Rich display helpers +# --------------------------------------------------------------------------- + +def print_disclaimer() -> None: + """Print the mandatory legal disclaimer prominently.""" + console.print( + Panel( + "⚠️ [bold yellow]DISCLAIMER[/bold yellow]\n\n" + "This tool is for [bold]educational purposes only[/bold]. " + "It does [bold red]NOT[/bold red] constitute legal advice.\n" + "Always consult a qualified attorney before making any legal or business decisions.", + title="[bold red]LEGAL NOTICE[/bold red]", + border_style="red", + padding=(1, 4), + ) + ) + + +def print_section(title: str) -> None: + console.print(Rule(f"[bold cyan]{title}[/bold cyan]", style="cyan")) + + +def print_summary(summary: dict) -> None: + from src.summarizer import format_summary_output + console.print( + Panel( + format_summary_output(summary), + title="[bold green]Executive Summary[/bold green]", + border_style="green", + padding=(1, 2), + ) + ) + + +def print_clauses(clauses: list) -> None: + if not clauses: + console.print("[dim]No clauses extracted.[/dim]") + return + + table = Table( + title="Extracted Clauses", + box=box.ROUNDED, + show_lines=True, + style="blue", + ) + table.add_column("Type", style="bold cyan", no_wrap=True, min_width=20) + table.add_column("Section", style="dim", min_width=10) + table.add_column("Plain English", style="white") + + for clause in clauses: + table.add_row( + clause.get("clause_type", "").replace("_", " ").title(), + clause.get("section_reference", "Unknown"), + clause.get("plain_english", ""), + ) + + console.print(table) + + +def print_risks(risks: list) -> None: + from src.risk_analyzer import format_risk_output + console.print( + Panel( + format_risk_output(risks), + title="[bold red]Risk Analysis[/bold red]", + border_style="red", + padding=(1, 2), + ) + ) + + +def print_conflicts(conflicts: list) -> None: + from src.conflict_detector import format_conflicts_output + console.print( + Panel( + format_conflicts_output(conflicts), + title="[bold yellow]Conflict Detection[/bold yellow]", + border_style="yellow", + padding=(1, 2), + ) + ) + + +# --------------------------------------------------------------------------- +# Interactive Q&A loop +# --------------------------------------------------------------------------- + +def run_interactive_qa(qa_chain) -> None: + """ + Enter a REPL-style loop so the user can ask multiple questions about + the contract without re-running the full analysis each time. + """ + console.print( + Panel( + "Type your question and press [bold]Enter[/bold].\n" + "Type [bold]'exit'[/bold] or [bold]'quit'[/bold] to stop.\n\n" + "Sample questions:\n" + " • What are my termination rights?\n" + " • Who owns the IP I create?\n" + " • What is the liability cap?\n" + " • How does auto-renewal work?\n" + " • What information must I keep confidential?", + title="[bold cyan]Interactive Q&A Mode[/bold cyan]", + border_style="cyan", + padding=(1, 2), + ) + ) + + from src.qa_chain import ask_question + + while True: + try: + question = console.input("\n[bold cyan]Question >[/bold cyan] ").strip() + except (KeyboardInterrupt, EOFError): + console.print("\n[dim]Exiting Q&A mode.[/dim]") + break + + if not question: + continue + if question.lower() in ("exit", "quit", "q"): + console.print("[dim]Exiting Q&A mode.[/dim]") + break + + with console.status("[bold green]Thinking...[/bold green]"): + answer = ask_question(question, qa_chain) + + console.print( + Panel( + answer, + title="[bold green]Answer[/bold green]", + border_style="green", + padding=(1, 2), + ) + ) + + +# --------------------------------------------------------------------------- +# Main pipeline +# --------------------------------------------------------------------------- + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + + # Validate API key + api_key = os.getenv("OPENAI_API_KEY") + if not api_key or api_key == "your_openai_api_key_here": + console.print( + "[bold red]ERROR:[/bold red] OPENAI_API_KEY not set. " + "Copy .env.example to .env and add your key." + ) + sys.exit(1) + + # Validate file argument + if not args.file: + console.print( + "[bold red]ERROR:[/bold red] --file is required. " + "Provide a path to a PDF or DOCX contract." + ) + parser.print_help() + sys.exit(1) + + if not os.path.exists(args.file): + console.print(f"[bold red]ERROR:[/bold red] File not found: {args.file}") + sys.exit(1) + + # ── Startup banner ────────────────────────────────────────────────────── + console.print( + Panel( + "[bold white]Legal AI Assistant[/bold white]\n" + f"[dim]Contract:[/dim] {os.path.basename(args.file)}\n" + f"[dim]Model :[/dim] {args.model}", + border_style="white", + padding=(1, 4), + ) + ) + print_disclaimer() + + # ── Import modules here to keep startup fast for --help ───────────────── + from langchain_openai import ChatOpenAI + from src.document_parser import parse_legal_document + from src.indexer import index_document, get_retriever + from src.summarizer import generate_summary + from src.clause_extractor import extract_clauses + from src.risk_analyzer import analyze_risks + from src.conflict_detector import detect_conflicts + from src.qa_chain import build_qa_chain, ask_question + + # Initialise LLM + llm = ChatOpenAI( + model=args.model, + temperature=0, # deterministic output for legal analysis + openai_api_key=api_key, + ) + + # ── Step 1: Parse document ─────────────────────────────────────────────── + print_section("Step 1 — Parsing Document") + with console.status("[bold green]Parsing document...[/bold green]"): + doc = parse_legal_document(args.file) + + console.print( + f" ✅ Parsed [bold]{doc['file_name']}[/bold] — " + f"{doc['page_count']} page(s), {len(doc['sections'])} section(s) detected" + ) + + # ── Step 2: Index for RAG ──────────────────────────────────────────────── + print_section("Step 2 — Building Vector Index") + index_path = f"legal_index_{os.path.splitext(doc['file_name'])[0]}" + with console.status("[bold green]Indexing document...[/bold green]"): + vector_store = index_document(args.file, index_path=index_path) + retriever = get_retriever(vector_store, k=4) + console.print(f" ✅ Index built at [bold]{index_path}/[/bold]") + + # ── Step 3: Executive Summary ──────────────────────────────────────────── + print_section("Step 3 — Executive Summary") + with console.status("[bold green]Generating summary...[/bold green]"): + summary = generate_summary(doc["full_text"], llm) + print_summary(summary) + + # ── Step 4: Clause Extraction ──────────────────────────────────────────── + print_section("Step 4 — Key Clause Extraction") + with console.status("[bold green]Extracting clauses...[/bold green]"): + clauses = extract_clauses(doc["full_text"], llm) + console.print(f" ✅ {len(clauses)} clause(s) extracted") + print_clauses(clauses) + + # ── Step 5: Risk Analysis ──────────────────────────────────────────────── + if not args.skip_risks: + print_section("Step 5 — Risk Analysis") + with console.status("[bold green]Analyzing risks...[/bold green]"): + risks = analyze_risks(clauses, llm) + console.print(f" ✅ {len(risks)} risk(s) identified") + print_risks(risks) + else: + console.print("[dim]Risk analysis skipped (--skip-risks).[/dim]") + risks = [] + + # ── Step 6: Conflict Detection ─────────────────────────────────────────── + if not args.skip_conflicts: + print_section("Step 6 — Conflict Detection") + with console.status("[bold green]Detecting conflicts...[/bold green]"): + conflicts = detect_conflicts(clauses, llm) + console.print(f" ✅ {len(conflicts)} potential conflict(s) found") + print_conflicts(conflicts) + else: + console.print("[dim]Conflict detection skipped (--skip-conflicts).[/dim]") + + # ── Step 7: Q&A ────────────────────────────────────────────────────────── + qa_chain = build_qa_chain(retriever, llm) + + if args.question: + # Single question mode — answer and exit + print_section("Q&A — Single Question") + with console.status("[bold green]Thinking...[/bold green]"): + answer = ask_question(args.question, qa_chain) + console.print(f"\n[bold cyan]Q:[/bold cyan] {args.question}") + console.print( + Panel( + answer, + title="[bold green]Answer[/bold green]", + border_style="green", + padding=(1, 2), + ) + ) + + elif args.interactive: + print_section("Step 7 — Interactive Q&A") + run_interactive_qa(qa_chain) + + else: + console.print( + "\n[dim]Tip: run with [bold]--interactive[/bold] to ask follow-up questions, " + "or [bold]--question \"...[/bold]\" for a single query.[/dim]" + ) + + console.print( + Panel( + "✅ Analysis complete.\n\n" + "[bold yellow]Reminder:[/bold yellow] Always verify findings with a qualified attorney.", + border_style="green", + padding=(1, 2), + ) + ) + + +if __name__ == "__main__": + main() diff --git a/02-legal-ai-assistant/prompts/clause_prompt.txt b/02-legal-ai-assistant/prompts/clause_prompt.txt new file mode 100644 index 0000000..a808ac6 --- /dev/null +++ b/02-legal-ai-assistant/prompts/clause_prompt.txt @@ -0,0 +1,24 @@ +You are a legal analyst specializing in contract review. Extract specific clause types from the following contract text. + +Contract text: +{contract_text} + +Identify and extract the following clause types if present: +- indemnification: Who must protect whom from losses +- limitation_of_liability: Caps on damages one party can claim +- termination: Conditions under which the contract can be ended +- governing_law: Which jurisdiction's laws govern the contract +- ip_ownership: Who owns intellectual property created under the contract +- confidentiality: Obligations to keep information secret + +For each clause found, respond with a JSON array: +[ + {{ + "clause_type": "indemnification", + "original_text": "The exact text from the contract", + "plain_english": "What this means in simple terms", + "section_reference": "Section number if identifiable, else 'Unknown'" + }} +] + +If a clause type is not found, omit it from the array. diff --git a/02-legal-ai-assistant/prompts/risk_prompt.txt b/02-legal-ai-assistant/prompts/risk_prompt.txt new file mode 100644 index 0000000..d382cd7 --- /dev/null +++ b/02-legal-ai-assistant/prompts/risk_prompt.txt @@ -0,0 +1,24 @@ +You are a legal risk analyst. Review the following contract clauses and identify risks. + +Contract clauses: +{clauses_text} + +For each risky clause, provide a risk assessment. Common risk patterns to look for: +- Unlimited liability (one party bears all risk with no cap) +- One-sided termination rights (only one party can terminate) +- Vague language (ambiguous terms that could be interpreted broadly) +- Auto-renewal traps (contracts that automatically renew without notice) +- Broad IP assignment (company owns everything you create, even on personal time) +- Non-compete clauses (restrictions on future employment) +- Unilateral modification rights (one party can change terms without consent) + +Respond with a JSON array: +[ + {{ + "clause_summary": "Brief description of the clause", + "risk_level": "HIGH | MEDIUM | LOW", + "risk_type": "Type of risk (e.g., unlimited_liability, one_sided_termination)", + "explanation": "Why this is risky and what a fair version would look like", + "original_text_excerpt": "The specific text that raises the concern" + }} +] diff --git a/02-legal-ai-assistant/prompts/summary_prompt.txt b/02-legal-ai-assistant/prompts/summary_prompt.txt new file mode 100644 index 0000000..4dfd70c --- /dev/null +++ b/02-legal-ai-assistant/prompts/summary_prompt.txt @@ -0,0 +1,14 @@ +You are a legal analyst. Read the following contract text and produce a structured executive summary. + +Contract text: +{contract_text} + +Respond ONLY with a JSON object in this exact format: +{{ + "parties": ["Party 1 name and role", "Party 2 name and role"], + "contract_type": "Type of contract (e.g., NDA, Service Agreement, Employment Contract)", + "effective_date": "Date the contract takes effect, or 'Not specified'", + "duration": "Contract duration or expiration, or 'Not specified'", + "key_obligations": ["Obligation 1", "Obligation 2", "Obligation 3"], + "summary": "Plain-English summary of the contract in 2-3 sentences, max 100 words" +}} diff --git a/02-legal-ai-assistant/requirements.txt b/02-legal-ai-assistant/requirements.txt new file mode 100644 index 0000000..6f52047 --- /dev/null +++ b/02-legal-ai-assistant/requirements.txt @@ -0,0 +1,11 @@ +langchain==0.1.20 +langchain-community==0.0.38 +langchain-openai==0.1.6 +faiss-cpu==1.8.0 +sentence-transformers==2.7.0 +pypdf==4.2.0 +python-docx==1.1.2 +openai==1.30.1 +python-dotenv==1.0.1 +pydantic==2.7.1 +rich==13.7.1 diff --git a/02-legal-ai-assistant/src/__init__.py b/02-legal-ai-assistant/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/02-legal-ai-assistant/src/clause_extractor.py b/02-legal-ai-assistant/src/clause_extractor.py new file mode 100644 index 0000000..d1a120e --- /dev/null +++ b/02-legal-ai-assistant/src/clause_extractor.py @@ -0,0 +1,150 @@ +""" +clause_extractor.py — Legal Clause Extraction + +Identifies and extracts specific, named clause types from a contract and +translates them into plain English. This is the "translation layer" between +dense legal prose and actionable information. + +Clause type reference: + indemnification — Party A must pay for losses caused to Party B. + e.g. "Vendor shall indemnify Client against all claims + arising from Vendor's performance of the Services." + → "The vendor must cover any lawsuits or losses the + client suffers because of the vendor's work." + + limitation_of_liability — Maximum damages one party can claim. + e.g. "In no event shall either party's liability + exceed the fees paid in the prior 3 months." + → "Neither side can sue for more than 3 months of + contract payments." + + termination — How and when the contract can be ended early. + e.g. "Either party may terminate with 30 days notice." + → "Either side can cancel with a month's warning." + + governing_law — Which state/country's courts have jurisdiction. + e.g. "This Agreement shall be governed by the laws + of the State of New York." + → "Disputes go to New York courts." + + ip_ownership — Who owns code, inventions, or designs created during + the contract. + e.g. "All work product created by Contractor shall + be deemed works made for hire owned by Company." + → "Everything you build belongs to the company." + + confidentiality — What information must be kept secret and for how long. + e.g. "Each party agrees to keep Confidential + Information secret for 5 years after termination." + → "Both sides must keep secrets for 5 years after + the contract ends." +""" + +import json +import re +from pathlib import Path + +from langchain.schema import HumanMessage + + +# --------------------------------------------------------------------------- +# Prompt loading +# --------------------------------------------------------------------------- + +def _load_clause_prompt() -> str: + """Load the clause extraction prompt from prompts/clause_prompt.txt.""" + prompt_path = Path(__file__).parent.parent / "prompts" / "clause_prompt.txt" + with open(prompt_path, "r", encoding="utf-8") as f: + return f.read() + + +# --------------------------------------------------------------------------- +# Core function +# --------------------------------------------------------------------------- + +def extract_clauses(contract_text: str, llm) -> list[dict]: + """ + Extract named clause types from a contract and provide plain-English + translations. + + Parameters + ---------- + contract_text : str — full or truncated contract text + llm : LLM — any LangChain-compatible chat model + + Returns + ------- + list of dicts, each with: + clause_type : str — one of the six named types above + original_text : str — verbatim text from the contract + plain_english : str — plain-language explanation + section_reference: str — section number or "Unknown" + + Returns an empty list if extraction fails or no clauses are found. + """ + prompt_template = _load_clause_prompt() + + # Use up to 12 000 chars — clause extraction needs more context than summary + # because clauses may appear anywhere across a long document. + truncated_text = contract_text[:12000] + if len(contract_text) > 12000: + truncated_text += "\n\n[... document truncated ...]" + + prompt = prompt_template.format(contract_text=truncated_text) + response = llm.invoke([HumanMessage(content=prompt)]) + raw_content = response.content if hasattr(response, "content") else str(response) + + # Strip markdown code fences if present + clean = raw_content.strip() + if clean.startswith("```"): + clean = re.sub(r"^```(?:json)?\s*", "", clean, flags=re.MULTILINE) + clean = re.sub(r"```\s*$", "", clean, flags=re.MULTILINE) + clean = clean.strip() + + try: + clauses = json.loads(clean) + # Ensure we always return a list + if isinstance(clauses, dict): + clauses = [clauses] + return clauses + except json.JSONDecodeError: + # Return empty list rather than crashing — downstream code handles this + print(f"[ClauseExtractor] WARNING: Could not parse LLM response as JSON.\n{raw_content[:300]}") + return [] + + +# --------------------------------------------------------------------------- +# Display formatting +# --------------------------------------------------------------------------- + +def format_clauses_output(clauses: list) -> str: + """ + Format extracted clauses as a human-readable string for terminal output. + + Parameters + ---------- + clauses : list — result from extract_clauses() + + Returns + ------- + str — multi-line formatted clause list + """ + if not clauses: + return "No clauses extracted (or extraction failed)." + + lines = [] + for i, clause in enumerate(clauses, start=1): + clause_type = clause.get("clause_type", "unknown").replace("_", " ").title() + section = clause.get("section_reference", "Unknown") + plain = clause.get("plain_english", "") + original = clause.get("original_text", "") + + lines.append(f"[{i}] {clause_type} (Section: {section})") + lines.append(f" Plain English: {plain}") + # Truncate long original text for display purposes + if len(original) > 200: + original = original[:200] + "..." + lines.append(f" Original Text: {original}") + lines.append("") # blank line between clauses + + return "\n".join(lines) diff --git a/02-legal-ai-assistant/src/conflict_detector.py b/02-legal-ai-assistant/src/conflict_detector.py new file mode 100644 index 0000000..97d15fb --- /dev/null +++ b/02-legal-ai-assistant/src/conflict_detector.py @@ -0,0 +1,174 @@ +""" +conflict_detector.py — Contract Clause Conflict Detection + +Compares extracted clauses against each other to surface internal +contradictions — places where one part of the contract conflicts with +another part. These inconsistencies are a common source of disputes. + +⚠️ IMPORTANT DISCLAIMER ⚠️ +──────────────────────────────────────────────────────────────────────────── +LLM-based conflict detection is NOT 100% reliable. The model may: + • Miss conflicts that require deep legal domain expertise to spot. + • Flag "conflicts" that are actually intentional or legally complementary. + • Fail on highly technical or jurisdiction-specific language. + +This tool helps you KNOW WHAT TO LOOK FOR and directs your attention to +potentially problematic areas. It is NOT a substitute for a qualified +attorney's review. Always verify flagged conflicts with a licensed lawyer +before making any legal or business decisions. +──────────────────────────────────────────────────────────────────────────── + +Common conflict patterns this module targets: + + 1. NOTICE PERIOD MISMATCH + Termination clause: "30 days written notice required." + Payment clause: "Invoices are due 60 days after notice of termination." + → You're legally required to pay for 60 days but can only terminate in 30. + + 2. CONFIDENTIALITY vs DEFINITION CONFLICT + Definition section: "Confidential Information means only written materials + marked CONFIDENTIAL." + Confidentiality clause: "All information disclosed, including oral + communications, is confidential." + → The definition is narrower than what the clause protects. + + 3. TERMINATION vs AUTO-RENEWAL + Termination clause: "Either party may terminate on 30 days notice." + Auto-renewal clause: "This Agreement auto-renews annually unless notice + is given 90 days before expiry." + → You need 90 days notice for auto-renewal but only 30 for termination — + which governs if the contract expires and auto-renews in 35 days? + + 4. IP OWNERSHIP vs CONFIDENTIALITY + IP clause: "All work product is owned by Company and may be used freely." + Confidentiality clause: "All work product is Confidential Information + and must not be disclosed." + → Company claims ownership AND confidentiality — can they publish your work? +""" + +import json +import re + +from langchain.schema import HumanMessage + + +# --------------------------------------------------------------------------- +# LLM prompt (inline — short enough not to warrant a separate .txt file) +# --------------------------------------------------------------------------- + +_CONFLICT_PROMPT = """ +You are a legal contract analyst. Review the following extracted contract clauses +and identify any internal conflicts or contradictions between them. + +Extracted clauses: +{clauses_json} + +Look for conflicts such as: +- Different notice periods for the same event mentioned in two different clauses +- A definition that contradicts how a term is used elsewhere +- A termination clause that conflicts with an auto-renewal clause +- An IP ownership clause that contradicts a confidentiality clause +- Different liability caps stated in different sections +- Inconsistent governing law references + +Respond with a JSON array. If no conflicts are found, return an empty array []. +[ + {{ + "conflict_type": "Short name for the type of conflict (e.g. notice_period_mismatch)", + "clause_a": "Description or quote from the first clause", + "clause_b": "Description or quote from the conflicting clause", + "description": "Plain-English explanation of why these clauses conflict and the practical impact" + }} +] +""".strip() + + +# --------------------------------------------------------------------------- +# Core function +# --------------------------------------------------------------------------- + +def detect_conflicts(clauses: list[dict], llm) -> list[dict]: + """ + Use an LLM to compare extracted clauses for internal contradictions. + + Parameters + ---------- + clauses : list[dict] — output from clause_extractor.extract_clauses() + llm : LLM — any LangChain-compatible chat model + + Returns + ------- + list of dicts, each with: + conflict_type : str — short category label + clause_a : str — description/quote from first clause + clause_b : str — description/quote from conflicting clause + description : str — plain-English explanation of the conflict + + Returns an empty list if no conflicts are found or detection fails. + + ⚠️ See module docstring for reliability limitations. + """ + if not clauses: + return [] + + clauses_json = json.dumps(clauses, indent=2) + prompt = _CONFLICT_PROMPT.format(clauses_json=clauses_json) + + response = llm.invoke([HumanMessage(content=prompt)]) + raw_content = response.content if hasattr(response, "content") else str(response) + + # Strip markdown code fences + clean = raw_content.strip() + if clean.startswith("```"): + clean = re.sub(r"^```(?:json)?\s*", "", clean, flags=re.MULTILINE) + clean = re.sub(r"```\s*$", "", clean, flags=re.MULTILINE) + clean = clean.strip() + + try: + conflicts = json.loads(clean) + if isinstance(conflicts, dict): + conflicts = [conflicts] + return conflicts + except json.JSONDecodeError: + print(f"[ConflictDetector] WARNING: Could not parse LLM response as JSON.\n{raw_content[:300]}") + return [] + + +# --------------------------------------------------------------------------- +# Display formatting +# --------------------------------------------------------------------------- + +def format_conflicts_output(conflicts: list) -> str: + """ + Format detected conflicts as a human-readable terminal string. + + Parameters + ---------- + conflicts : list — result from detect_conflicts() + + Returns + ------- + str — formatted conflict report + """ + if not conflicts: + return "⚪ No internal conflicts detected." + + lines = [ + "⚠️ The following potential conflicts were detected.", + " Verify each finding with a qualified attorney before acting on it.", + "", + ] + + for i, conflict in enumerate(conflicts, start=1): + conflict_type = conflict.get("conflict_type", "unknown").replace("_", " ").title() + clause_a = conflict.get("clause_a", "") + clause_b = conflict.get("clause_b", "") + description = conflict.get("description", "") + + lines.append(f"⚡ [{i}] {conflict_type}") + lines.append(f" Clause A : {clause_a}") + lines.append(f" Clause B : {clause_b}") + lines.append(f" Impact : {description}") + lines.append("") + + return "\n".join(lines) diff --git a/02-legal-ai-assistant/src/document_parser.py b/02-legal-ai-assistant/src/document_parser.py new file mode 100644 index 0000000..5ead578 --- /dev/null +++ b/02-legal-ai-assistant/src/document_parser.py @@ -0,0 +1,216 @@ +""" +document_parser.py — Legal Document Parser + +Handles loading and parsing of PDF and DOCX contract files into structured text. + +Why section structure matters for legal documents: + Legal contracts are highly cross-referential. A clause in Section 8 may say + "subject to Section 4.2", so knowing WHICH section a piece of text belongs to + is critical for accurate clause extraction and conflict detection. If we just + dumped all text together we'd lose those structural anchors. + +Limitations of PDF text extraction: + PyPDF (and most PDF parsers) extract raw text by reading the PDF's character + stream. This means: + - Tables lose their column alignment and become garbled rows of text. + - Headers/footers repeat on every page, creating noise. + - Some scanned PDFs produce no text at all (OCR required separately). + - Footnotes sometimes appear inline mid-sentence rather than at the bottom. + These limitations mean downstream analysis must tolerate imperfect text. + +Why legal document structure is important for accurate clause extraction: + LLMs asked to find "the termination clause" perform significantly better when + the prompt includes section headings because headings act as semantic anchors. + Without them, the model may conflate a termination clause buried in an exhibit + with the main termination provisions, leading to inaccurate summaries. +""" + +import os +import re +from typing import Optional + +# PyPDFLoader uses pypdf under the hood — handles multi-page PDFs gracefully +from langchain_community.document_loaders import PyPDFLoader + +# python-docx for Microsoft Word (.docx) files +from docx import Document as DocxDocument + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + +def _detect_heading(line: str) -> bool: + """ + Heuristic: a line is treated as a section heading if it matches any of: + 1. Numbered section — "1.", "1.1", "2.3.4", etc. + 2. ALL-CAPS line — "INDEMNIFICATION", "LIMITATION OF LIABILITY" + 3. Trailing colon — "Governing Law:", "Notice:" + These patterns cover the vast majority of standard contract heading styles. + """ + line = line.strip() + if not line: + return False + # Pattern 1: numbered section (e.g. "1.", "2.1", "10.3.2") + if re.match(r"^\d+(\.\d+)*\.?\s+\S", line): + return True + # Pattern 2: all-caps (ignoring punctuation/spaces, at least 3 chars of alpha) + alpha_only = re.sub(r"[^A-Za-z]", "", line) + if len(alpha_only) >= 3 and alpha_only == alpha_only.upper(): + return True + # Pattern 3: ends with colon + if line.endswith(":"): + return True + return False + + +def _split_into_sections(full_text: str) -> list[dict]: + """ + Walk through lines of text and group them into sections based on headings. + Returns a list of section dicts. Each dict has: + heading — the heading text (or "Preamble" for leading content) + content — the body text under that heading + page_num — approximate page number (estimated by form-feed character '\x0c') + """ + sections = [] + current_heading = "Preamble" + current_lines: list[str] = [] + page_num = 1 + + for line in full_text.splitlines(): + # pypdf uses form-feed (\x0c) as a page separator + if "\x0c" in line: + page_num += line.count("\x0c") + line = line.replace("\x0c", "") + + if _detect_heading(line): + # Save the previous section before starting a new one + if current_lines: + sections.append({ + "heading": current_heading, + "content": "\n".join(current_lines).strip(), + "page_num": page_num, + }) + current_heading = line.strip() + current_lines = [] + else: + current_lines.append(line) + + # Don't forget the last section + if current_lines: + sections.append({ + "heading": current_heading, + "content": "\n".join(current_lines).strip(), + "page_num": page_num, + }) + + return sections + + +# --------------------------------------------------------------------------- +# PDF parsing +# --------------------------------------------------------------------------- + +def _parse_pdf(file_path: str) -> dict: + """ + Load a PDF using LangChain's PyPDFLoader (backed by pypdf). + Each LangChain Document corresponds to one PDF page. + We concatenate all pages and then split by detected headings. + """ + loader = PyPDFLoader(file_path) + pages = loader.load() # list of langchain Document objects, one per page + + full_text = "\n".join(page.page_content for page in pages) + page_count = len(pages) + + sections = _split_into_sections(full_text) + + return { + "full_text": full_text, + "sections": sections, + "file_name": os.path.basename(file_path), + "page_count": page_count, + } + + +# --------------------------------------------------------------------------- +# DOCX parsing +# --------------------------------------------------------------------------- + +def _parse_docx(file_path: str) -> dict: + """ + Load a DOCX file using python-docx. + Word documents store paragraphs with an explicit style name; we use + "Heading" styles as section delimiters when present, falling back to + the same heuristic used for PDFs. + """ + doc = DocxDocument(file_path) + + full_lines: list[str] = [] + for para in doc.paragraphs: + full_lines.append(para.text) + + full_text = "\n".join(full_lines) + + # Approximate page count: Word doesn't expose pages easily via python-docx; + # we use a rough heuristic (every ~40 paragraphs ≈ 1 page). + page_count = max(1, len(doc.paragraphs) // 40) + + sections = _split_into_sections(full_text) + + return { + "full_text": full_text, + "sections": sections, + "file_name": os.path.basename(file_path), + "page_count": page_count, + } + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +def parse_legal_document(file_path: str) -> dict: + """ + Parse a legal document (PDF or DOCX) into structured text. + + Parameters + ---------- + file_path : str + Absolute or relative path to the contract file. + + Returns + ------- + dict with keys: + full_text : str — complete raw text of the document + sections : list[dict] — list of {heading, content, page_num} dicts + file_name : str — basename of the file + page_count : int — number of pages (PDF) or estimated pages (DOCX) + + Raises + ------ + ValueError if the file type is not supported. + FileNotFoundError if the file does not exist. + """ + if not os.path.exists(file_path): + raise FileNotFoundError(f"Contract file not found: {file_path}") + + ext = os.path.splitext(file_path)[1].lower() + + if ext == ".pdf": + return _parse_pdf(file_path) + elif ext in (".docx", ".doc"): + return _parse_docx(file_path) + else: + raise ValueError( + f"Unsupported file type '{ext}'. Supported types: .pdf, .docx" + ) + + +def extract_full_text(file_path: str) -> str: + """ + Convenience wrapper: parse a document and return only the full text string. + Useful when callers don't need the structured section breakdown. + """ + result = parse_legal_document(file_path) + return result["full_text"] diff --git a/02-legal-ai-assistant/src/indexer.py b/02-legal-ai-assistant/src/indexer.py new file mode 100644 index 0000000..1172b33 --- /dev/null +++ b/02-legal-ai-assistant/src/indexer.py @@ -0,0 +1,158 @@ +""" +indexer.py — Vector-Store Indexing for Legal Documents + +This module is intentionally similar to the RAG indexer from Project 1 +(01-rag-from-scratch). The same pattern — chunk → embed → store in FAISS — +works equally well for legal documents. The only difference is that legal +chunks benefit from slightly larger sizes because legal sentences are long +and context-dependent (a 512-token chunk mid-clause may miss the crucial +subject from the sentence above). + +Reuse note: + If you already built a FAISS index in Project 1, the load_index() and + get_retriever() helpers here are identical. The legal domain just requires + a different source document and potentially different chunk sizes. +""" + +import os + +# HuggingFaceEmbeddings runs locally — no API key needed for embedding. +# We default to "all-MiniLM-L6-v2" which is fast and good for semantic search. +from langchain_community.embeddings import HuggingFaceEmbeddings +from langchain_community.vectorstores import FAISS + +# RecursiveCharacterTextSplitter tries to split on paragraphs, then sentences, +# then words — preserving as much semantic context as possible per chunk. +from langchain.text_splitter import RecursiveCharacterTextSplitter + +from langchain_community.document_loaders import PyPDFLoader +from langchain.schema import Document + + +# --------------------------------------------------------------------------- +# Chunking configuration +# --------------------------------------------------------------------------- + +# Legal sentences are verbose; 1200-char chunks with 200-char overlap keeps +# clauses intact while still providing sufficient retrieval granularity. +CHUNK_SIZE = 1200 +CHUNK_OVERLAP = 200 + +# Embedding model — same as Project 1, works well for legal text +EMBEDDING_MODEL = "all-MiniLM-L6-v2" + + +def _get_embeddings() -> HuggingFaceEmbeddings: + """Return a HuggingFaceEmbeddings instance (downloaded on first call).""" + return HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL) + + +def _chunk_text(full_text: str) -> list[Document]: + """ + Split raw contract text into overlapping chunks suitable for embedding. + + The RecursiveCharacterTextSplitter cascades through separators: + ["\n\n", "\n", " ", ""] — so it prefers to break at paragraph boundaries, + then line breaks, then spaces. This keeps sentences from being split in + the middle of a legal obligation where the subject is at the start and + the verb is at the end. + """ + splitter = RecursiveCharacterTextSplitter( + chunk_size=CHUNK_SIZE, + chunk_overlap=CHUNK_OVERLAP, + separators=["\n\n", "\n", ". ", " ", ""], + ) + # Wrap in a single Document so the splitter returns Document objects + docs = splitter.create_documents([full_text]) + return docs + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +def index_document(file_path: str, index_path: str = "legal_faiss_index") -> FAISS: + """ + Parse, chunk, embed, and persist a contract document to a FAISS index. + + Parameters + ---------- + file_path : str — path to the PDF/DOCX contract + index_path : str — directory where the FAISS index will be saved + + Returns + ------- + FAISS vector store ready for similarity search. + """ + # --- Step 1: Load raw text --- + # Use PyPDFLoader for PDFs; for DOCX we read via document_parser then wrap + ext = os.path.splitext(file_path)[1].lower() + if ext == ".pdf": + loader = PyPDFLoader(file_path) + raw_docs = loader.load() + full_text = "\n".join(d.page_content for d in raw_docs) + else: + # For non-PDF files, fall back to document_parser's full_text + from src.document_parser import extract_full_text + full_text = extract_full_text(file_path) + + # --- Step 2: Chunk --- + chunks = _chunk_text(full_text) + print(f"[Indexer] Created {len(chunks)} chunks from '{os.path.basename(file_path)}'") + + # --- Step 3: Embed & build FAISS index --- + embeddings = _get_embeddings() + vector_store = FAISS.from_documents(chunks, embeddings) + + # --- Step 4: Persist to disk --- + os.makedirs(index_path, exist_ok=True) + vector_store.save_local(index_path) + print(f"[Indexer] Index saved to '{index_path}'") + + return vector_store + + +def load_index(index_path: str = "legal_faiss_index") -> FAISS: + """ + Load a previously saved FAISS index from disk. + + Parameters + ---------- + index_path : str — directory containing the saved FAISS index files + + Returns + ------- + FAISS vector store ready for similarity search. + """ + if not os.path.exists(index_path): + raise FileNotFoundError( + f"No FAISS index found at '{index_path}'. " + "Run index_document() first to create the index." + ) + embeddings = _get_embeddings() + vector_store = FAISS.load_local( + index_path, + embeddings, + allow_dangerous_deserialization=True, # required by newer LangChain versions + ) + print(f"[Indexer] Loaded index from '{index_path}'") + return vector_store + + +def get_retriever(vector_store: FAISS, k: int = 4): + """ + Wrap a FAISS vector store as a LangChain retriever. + + Parameters + ---------- + vector_store : FAISS — the in-memory or loaded vector store + k : int — number of chunks to retrieve per query (default 4) + + Returns + ------- + A LangChain BaseRetriever that can be plugged into any chain. + + Note: k=4 is a good balance for legal Q&A — enough context to answer most + clause-level questions without exceeding typical context-window limits. + """ + return vector_store.as_retriever(search_kwargs={"k": k}) diff --git a/02-legal-ai-assistant/src/qa_chain.py b/02-legal-ai-assistant/src/qa_chain.py new file mode 100644 index 0000000..1c8cdb6 --- /dev/null +++ b/02-legal-ai-assistant/src/qa_chain.py @@ -0,0 +1,121 @@ +""" +qa_chain.py — Retrieval-Augmented Q&A for Legal Documents + +Builds a RAG Q&A chain that answers questions about a contract by: + 1. Retrieving the most relevant chunks from the FAISS index + 2. Sending those chunks + the user's question to the LLM + 3. Requiring the model to cite the specific section it's referencing + +Why source citation is critical in legal Q&A: + Unlike a general knowledge chatbot, a legal assistant's answers directly + influence decisions with real financial and legal consequences. If a user + asks "Can I terminate in 30 days?" and the model answers "Yes" without + citing the source clause, the user cannot verify whether that answer is + based on the actual contract or a hallucination. Forcing the model to + cite sections: + • Lets the user cross-check against the original document. + • Makes hallucinations easier to spot (the cited section won't exist). + • Builds appropriate trust — the user knows WHAT to verify, not just + WHETHER to trust. + + This is fundamentally different from Q&A over, say, a technical manual, + where a wrong answer is inconvenient. A wrong answer about a contract + clause can result in a breach of contract, lawsuit, or financial loss. +""" + +from langchain.chains import RetrievalQA +from langchain.prompts import PromptTemplate + + +# --------------------------------------------------------------------------- +# Custom legal Q&A prompt +# --------------------------------------------------------------------------- + +# The prompt explicitly: +# 1. Restricts answers to provided context (reduces hallucination) +# 2. Requires section citations (enables user verification) +# 3. Provides a safe fallback for out-of-scope questions +# 4. Includes the "not legal advice" disclaimer in every answer +_LEGAL_QA_TEMPLATE = """You are a legal document assistant. Answer questions about the contract based ONLY on the provided context. +Always cite the specific section or clause you are referencing. +If the answer is not in the provided context, say "This information is not found in the provided contract." + +Important: This is for informational purposes only, not legal advice. + +Context from contract: +{context} + +Question: {question} + +Answer (include section references):""" + +_QA_PROMPT = PromptTemplate( + template=_LEGAL_QA_TEMPLATE, + input_variables=["context", "question"], +) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +def build_qa_chain(retriever, llm) -> RetrievalQA: + """ + Construct a RetrievalQA chain grounded in the indexed contract. + + Parameters + ---------- + retriever : BaseRetriever — from indexer.get_retriever() + llm : LLM — any LangChain-compatible chat model + + Returns + ------- + RetrievalQA chain ready to accept questions via .invoke() or .run() + + The chain uses "stuff" document combination strategy — it concatenates + retrieved chunks into a single context block. For very long contracts + "map_reduce" or "refine" strategies may be preferable, but "stuff" is + the most reliable for faithfully citing specific text. + """ + qa_chain = RetrievalQA.from_chain_type( + llm=llm, + chain_type="stuff", + retriever=retriever, + return_source_documents=True, # lets callers show which chunks were used + chain_type_kwargs={"prompt": _QA_PROMPT}, + ) + return qa_chain + + +def ask_question(question: str, qa_chain) -> str: + """ + Ask a natural-language question about the indexed contract. + + Parameters + ---------- + question : str — the user's question (e.g. "What are my termination rights?") + qa_chain : RetrievalQA — built by build_qa_chain() + + Returns + ------- + str — the model's answer with section citations + + The returned string always includes the LLM's answer. If source documents + were returned they are appended as a "Sources" footer so users can quickly + locate the referenced passage in the original document. + """ + result = qa_chain.invoke({"query": question}) + + answer = result.get("result", "No answer returned.") + + # Append source chunk references if available — helps users locate the + # exact passage that was used to generate the answer. + source_docs = result.get("source_documents", []) + if source_docs: + answer += "\n\n─── Sources (retrieved chunks) ───" + for i, doc in enumerate(source_docs, start=1): + # Show the first 150 chars of each source chunk as a reference hint + snippet = doc.page_content[:150].replace("\n", " ").strip() + answer += f"\n [{i}] ...{snippet}..." + + return answer diff --git a/02-legal-ai-assistant/src/risk_analyzer.py b/02-legal-ai-assistant/src/risk_analyzer.py new file mode 100644 index 0000000..2c2f0b2 --- /dev/null +++ b/02-legal-ai-assistant/src/risk_analyzer.py @@ -0,0 +1,161 @@ +""" +risk_analyzer.py — Contract Risk Analysis + +Scores extracted clauses for potential risks and explains WHY each clause +is risky and what a fair alternative would look like. + +Example of HIGH-RISK vs STANDARD clause language: + + HIGH RISK (indemnification): + "Employee agrees to indemnify, defend, and hold harmless Company and + its officers, directors, and employees from any and all claims, + losses, or damages, including those arising from Company's own + negligence or intentional misconduct." + → This is dangerous: the employee bears the cost of the company's + OWN mistakes. + + STANDARD (indemnification): + "Each party shall indemnify and hold harmless the other party for + losses arising directly from that party's own negligence or + willful misconduct." + → Fair: each side is responsible only for their own actions. + + HIGH RISK (IP ownership): + "Employee hereby assigns to Company all inventions, discoveries, + and works of authorship conceived or reduced to practice at any + time during employment, whether or not related to Company's business + and whether or not made during working hours." + → "At any time" + "whether or not related" = company owns your + weekend side projects. + + STANDARD (IP ownership): + "Employee assigns to Company inventions that relate to Company's + business or are developed using Company resources or during + working hours." + → Scoped to actual work-related output. + +Risk levels: + HIGH 🔴 — Potential for significant financial or legal harm; seek + attorney review before signing. + MEDIUM 🟡 — Unusual or one-sided term; negotiate if possible. + LOW 🟢 — Minor concern; worth noting but unlikely to cause harm. +""" + +import json +import re +from pathlib import Path + +from langchain.schema import HumanMessage + + +# --------------------------------------------------------------------------- +# Prompt loading +# --------------------------------------------------------------------------- + +def _load_risk_prompt() -> str: + """Load the risk analysis prompt from prompts/risk_prompt.txt.""" + prompt_path = Path(__file__).parent.parent / "prompts" / "risk_prompt.txt" + with open(prompt_path, "r", encoding="utf-8") as f: + return f.read() + + +# --------------------------------------------------------------------------- +# Core function +# --------------------------------------------------------------------------- + +def analyze_risks(clauses: list[dict], llm) -> list[dict]: + """ + Analyze extracted clauses for legal and financial risks. + + Parameters + ---------- + clauses : list[dict] — output from clause_extractor.extract_clauses() + llm : LLM — any LangChain-compatible chat model + + Returns + ------- + list of dicts, each with: + clause_summary : str — brief description of the risky clause + risk_level : str — "HIGH", "MEDIUM", or "LOW" + risk_type : str — category (e.g. "unlimited_liability") + explanation : str — why it's risky + what fair looks like + original_text_excerpt: str — the specific concerning text + + Returns an empty list if analysis fails or no risks are found. + """ + if not clauses: + return [] + + prompt_template = _load_risk_prompt() + + # Serialize clauses to a readable text block for the prompt + clauses_text = json.dumps(clauses, indent=2) + + prompt = prompt_template.format(clauses_text=clauses_text) + response = llm.invoke([HumanMessage(content=prompt)]) + raw_content = response.content if hasattr(response, "content") else str(response) + + # Strip markdown fences + clean = raw_content.strip() + if clean.startswith("```"): + clean = re.sub(r"^```(?:json)?\s*", "", clean, flags=re.MULTILINE) + clean = re.sub(r"```\s*$", "", clean, flags=re.MULTILINE) + clean = clean.strip() + + try: + risks = json.loads(clean) + if isinstance(risks, dict): + risks = [risks] + return risks + except json.JSONDecodeError: + print(f"[RiskAnalyzer] WARNING: Could not parse LLM response as JSON.\n{raw_content[:300]}") + return [] + + +# --------------------------------------------------------------------------- +# Display formatting +# --------------------------------------------------------------------------- + +# Emoji indicators for risk levels — visible at a glance in terminal output +_RISK_EMOJI = { + "HIGH": "🔴", + "MEDIUM": "🟡", + "LOW": "🟢", +} + + +def format_risk_output(risks: list) -> str: + """ + Format risk analysis results as a human-readable terminal string. + + Parameters + ---------- + risks : list — result from analyze_risks() + + Returns + ------- + str — multi-line formatted risk report with emoji indicators + """ + if not risks: + return "No risks identified (or risk analysis failed)." + + lines = [] + for i, risk in enumerate(risks, start=1): + level = risk.get("risk_level", "UNKNOWN").upper() + emoji = _RISK_EMOJI.get(level, "⚪") + risk_type = risk.get("risk_type", "unknown").replace("_", " ").title() + summary = risk.get("clause_summary", "") + explanation = risk.get("explanation", "") + excerpt = risk.get("original_text_excerpt", "") + + lines.append(f"{emoji} [{i}] {level} RISK — {risk_type}") + lines.append(f" Clause : {summary}") + lines.append(f" Why : {explanation}") + if excerpt: + # Truncate long excerpts for readability + if len(excerpt) > 200: + excerpt = excerpt[:200] + "..." + lines.append(f" Text : \"{excerpt}\"") + lines.append("") + + return "\n".join(lines) diff --git a/02-legal-ai-assistant/src/summarizer.py b/02-legal-ai-assistant/src/summarizer.py new file mode 100644 index 0000000..57b1343 --- /dev/null +++ b/02-legal-ai-assistant/src/summarizer.py @@ -0,0 +1,146 @@ +""" +summarizer.py — Contract Executive Summary Generator + +Sends contract text to an LLM with a structured prompt and parses the JSON +response into a Python dict for downstream use and display. + +Example transformation: + Before (raw contract language): + "This Agreement shall commence on the Effective Date and shall continue + for a period of one (1) year unless sooner terminated..." + + After (plain-English summary field): + "This is a one-year service agreement between Acme Corp and Beta LLC. + Acme will provide software development services in exchange for monthly + payments. Either party may terminate with 30 days written notice." + +The structured JSON output (parties, contract_type, key_obligations, etc.) +makes it easy to build dashboards, comparison tools, or automated alerts on +top of this module without re-parsing free-form text. +""" + +import json +import os +import re +from pathlib import Path + +from langchain.schema import HumanMessage + + +# --------------------------------------------------------------------------- +# Prompt loading +# --------------------------------------------------------------------------- + +def _load_summary_prompt() -> str: + """Load the summary prompt template from prompts/summary_prompt.txt.""" + prompt_path = Path(__file__).parent.parent / "prompts" / "summary_prompt.txt" + with open(prompt_path, "r", encoding="utf-8") as f: + return f.read() + + +# --------------------------------------------------------------------------- +# Core function +# --------------------------------------------------------------------------- + +def generate_summary(contract_text: str, llm) -> dict: + """ + Generate a structured executive summary of a contract. + + Parameters + ---------- + contract_text : str — full text (or a representative excerpt) of the contract + llm : LLM — any LangChain-compatible chat model (e.g. ChatOpenAI) + + Returns + ------- + dict with keys: + parties : list[str] + contract_type : str + effective_date : str + duration : str + key_obligations : list[str] + summary : str + + Falls back to {"raw_response": } if JSON parsing fails, so callers + always receive a dict even when the model returns malformed output. + + Note: We cap input at 8000 characters. Most consumer LLMs have a ~4k-token + context window for GPT-3.5 or ~8k for GPT-4. 8000 chars ≈ 2000 tokens, + leaving room for the prompt itself and the response. + """ + prompt_template = _load_summary_prompt() + + # Truncate to avoid exceeding model context limits + truncated_text = contract_text[:8000] + if len(contract_text) > 8000: + truncated_text += "\n\n[... document truncated for summary ...]" + + prompt = prompt_template.format(contract_text=truncated_text) + + # Invoke the LLM — works with both .invoke() (newer LangChain) and direct call + response = llm.invoke([HumanMessage(content=prompt)]) + raw_content = response.content if hasattr(response, "content") else str(response) + + # --- Parse JSON response --- + # The prompt instructs the model to return ONLY JSON, but it occasionally + # wraps it in ```json ... ``` markdown fences — strip those first. + clean = raw_content.strip() + if clean.startswith("```"): + # Remove opening fence (```json or ```) + clean = re.sub(r"^```(?:json)?\s*", "", clean, flags=re.MULTILINE) + # Remove closing fence + clean = re.sub(r"```\s*$", "", clean, flags=re.MULTILINE) + clean = clean.strip() + + try: + return json.loads(clean) + except json.JSONDecodeError: + # Graceful fallback: return raw text so the caller can still display something + return {"raw_response": raw_content} + + +# --------------------------------------------------------------------------- +# Display formatting +# --------------------------------------------------------------------------- + +def format_summary_output(summary: dict) -> str: + """ + Format a summary dict as a human-readable string for terminal output. + + Parameters + ---------- + summary : dict — result from generate_summary() + + Returns + ------- + str — multi-line formatted summary + """ + if "raw_response" in summary: + return f"[Raw LLM response — JSON parsing failed]\n\n{summary['raw_response']}" + + lines = [] + + contract_type = summary.get("contract_type", "Unknown") + lines.append(f"Contract Type : {contract_type}") + + parties = summary.get("parties", []) + if parties: + lines.append("Parties :") + for p in parties: + lines.append(f" • {p}") + + lines.append(f"Effective Date: {summary.get('effective_date', 'Not specified')}") + lines.append(f"Duration : {summary.get('duration', 'Not specified')}") + + obligations = summary.get("key_obligations", []) + if obligations: + lines.append("Key Obligations:") + for ob in obligations: + lines.append(f" • {ob}") + + plain_summary = summary.get("summary", "") + if plain_summary: + lines.append(f"\nSummary:\n {plain_summary}") + + return "\n".join(lines) + From fcfc4cac36615e2778617816e02b09f77240da86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:06:58 +0000 Subject: [PATCH 4/9] Add 03-research-agent project Complete ReAct research agent pipeline including: - paper_parser.py: LLM-based PDF metadata extraction (PaperMetadata Pydantic model) - paper_indexer.py: FAISS vector index with per-paper metadata filtering - tools/search_tool.py: semantic search LangChain Tool - tools/summary_tool.py: paper summary LangChain Tool - tools/compare_tool.py: LLM-powered two-paper comparison Tool - agent.py: ZERO_SHOT_REACT_DESCRIPTION AgentExecutor with verbose ReAct loop - gap_analyzer.py: cross-paper synthesis and research gap identification - report_generator.py: structured Markdown report writer - main.py: CLI with --query, --report, --interactive flags - README.md: agent concepts, ReAct loop, architecture diagram, sample queries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- 03-research-agent/.env.example | 11 + 03-research-agent/README.md | 191 +++++++++++++++++ 03-research-agent/data/papers/.gitkeep | 0 03-research-agent/main.py | 222 ++++++++++++++++++++ 03-research-agent/requirements.txt | 10 + 03-research-agent/src/__init__.py | 2 + 03-research-agent/src/agent.py | 145 +++++++++++++ 03-research-agent/src/gap_analyzer.py | 185 ++++++++++++++++ 03-research-agent/src/paper_indexer.py | 180 ++++++++++++++++ 03-research-agent/src/paper_parser.py | 188 +++++++++++++++++ 03-research-agent/src/report_generator.py | 201 ++++++++++++++++++ 03-research-agent/src/tools/__init__.py | 2 + 03-research-agent/src/tools/compare_tool.py | 133 ++++++++++++ 03-research-agent/src/tools/search_tool.py | 90 ++++++++ 03-research-agent/src/tools/summary_tool.py | 96 +++++++++ 15 files changed, 1656 insertions(+) create mode 100644 03-research-agent/.env.example create mode 100644 03-research-agent/README.md create mode 100644 03-research-agent/data/papers/.gitkeep create mode 100644 03-research-agent/main.py create mode 100644 03-research-agent/requirements.txt create mode 100644 03-research-agent/src/__init__.py create mode 100644 03-research-agent/src/agent.py create mode 100644 03-research-agent/src/gap_analyzer.py create mode 100644 03-research-agent/src/paper_indexer.py create mode 100644 03-research-agent/src/paper_parser.py create mode 100644 03-research-agent/src/report_generator.py create mode 100644 03-research-agent/src/tools/__init__.py create mode 100644 03-research-agent/src/tools/compare_tool.py create mode 100644 03-research-agent/src/tools/search_tool.py create mode 100644 03-research-agent/src/tools/summary_tool.py diff --git a/03-research-agent/.env.example b/03-research-agent/.env.example new file mode 100644 index 0000000..c5a27fa --- /dev/null +++ b/03-research-agent/.env.example @@ -0,0 +1,11 @@ +# OpenAI API Key (required) +OPENAI_API_KEY=your_openai_api_key_here + +# Model to use (gpt-4 recommended for research synthesis) +OPENAI_MODEL=gpt-4 + +# Optional: Anthropic Claude (alternative) +# ANTHROPIC_API_KEY=your_anthropic_key_here + +# Papers directory +PAPERS_DIR=data/papers diff --git a/03-research-agent/README.md b/03-research-agent/README.md new file mode 100644 index 0000000..0624998 --- /dev/null +++ b/03-research-agent/README.md @@ -0,0 +1,191 @@ +# 03 — Research Agent + +> **"Like a research assistant who can look up papers, take notes, and compare findings — rather than just answering one question."** + +## What is an AI Agent? + +A regular LLM call is a single prompt → single response. You hand the model some text and it writes back. That's it. + +An **AI agent** is different: it has access to **tools** — functions it can call to look things up, compute things, or take actions — and it decides *dynamically* which tools to use based on each new sub-goal. + +Think of the difference between: +- 🤖 **Simple LLM**: You ask "what does Paper A say about transformers?" and the model guesses from its training data. +- 🕵️ **Research Agent**: You ask the same question, and the agent *looks it up*, reads the relevant sections, possibly *compares* them to Paper B, and synthesises an answer with citations. + +## How This Differs from Simple RAG + +| Simple RAG | Research Agent | +|---|---| +| Embed documents → vector DB | Same | +| User query → nearest chunks → LLM answer | **Agent plans which tools to call** | +| Single retrieval step | **Multi-step: search → summarise → compare** | +| No memory between steps | **Observations from each step feed the next** | +| Good for Q&A | Good for synthesis, comparison, gap analysis | + +In simple RAG, the pipeline is fixed: retrieve then answer. In an agent, the LLM itself decides the pipeline at runtime. + +## The ReAct Loop Explained + +**ReAct = Reason + Act**. The agent alternates between thinking and doing: + +``` +Thought : I need to find papers about attention mechanisms. +Action : search_papers +Input : attention mechanism self-attention +Observation: [Result 1] Paper: "Attention Is All You Need" … + +Thought : I found the relevant paper. Now I'll get its full summary. +Action : summarize_paper +Input : Attention Is All You Need +Observation: Title: Attention Is All You Need, Authors: Vaswani et al. … + +Thought : I have enough to answer the question. +Final Answer: The paper "Attention Is All You Need" introduced … +``` + +Each **Observation** is the tool's output, appended to the agent's context. The agent re-reads the growing context at each step to decide what to do next. + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ main.py │ +│ (CLI: --query / --report / --interactive) │ +└──────────────────────┬──────────────────────────────┘ + │ + ┌────────────▼────────────┐ + │ Research Agent │ ← agent.py + │ (ReAct loop + LLM) │ + └──┬──────────┬───────────┘ + │ │ + ┌─────────▼──┐ ┌───▼────────────┐ ┌─────────────────┐ + │search_tool │ │ summary_tool │ │ compare_tool │ + │(FAISS │ │ (PaperMetadata │ │ (LLM comparison│ + │ semantic │ │ lookup) │ │ of two papers)│ + │ search) │ └───────┬────────┘ └────────┬────────┘ + └─────┬──────┘ │ │ + │ ┌───────▼────────────────────▼──────┐ + │ │ PaperMetadata objects │ + │ │ (from paper_parser.py) │ + │ └───────────────────────────────────┘ + ┌─────▼──────┐ + │ FAISS index│ ← paper_indexer.py + │ (chunked │ + │ PDFs + │ + │ metadata) │ + └────────────┘ + + Gap Analysis (--report): + paper_metadata → gap_analyzer.py → LLM synthesis → report_generator.py → .md file +``` + +## Setup + +```bash +# 1. Clone / navigate to the project +cd 03-research-agent + +# 2. Create and activate a virtual environment +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate + +# 3. Install dependencies +pip install -r requirements.txt + +# 4. Configure environment +cp .env.example .env +# Edit .env and add your OPENAI_API_KEY + +# 5. Add research papers +# Copy your .pdf files into data/papers/ +``` + +## How to Add Papers + +Place any number of **.pdf** files into `data/papers/`. The pipeline will: +1. Extract text and LLM-parse metadata (title, authors, abstract, methodology, findings, limitations). +2. Chunk the full text and embed it into a FAISS vector index. +3. Make both the metadata and the full text available to the agent's tools. + +**Tips:** +- Use papers that are topically related for better gap analysis. +- 3–10 papers is the sweet spot. More than 20 may hit the LLM's context limit during gap analysis. +- Scanned PDFs without OCR will produce empty or garbled text — use PDFs with selectable text. + +## Running the Agent + +```bash +# Ask a single question and exit +python main.py --query "What methodologies are used across these papers?" + +# Start an interactive Q&A session +python main.py --interactive + +# Generate a gap analysis report +python main.py --topic "transformer models" --report + +# All options +python main.py --papers-dir data/papers \ + --topic "BERT fine-tuning" \ + --model gpt-4 \ + --report \ + --output reports/bert_gaps.md +``` + +## Sample Queries + +These questions showcase the agent's multi-step reasoning: + +``` +"What methodologies are used across these papers?" +"Which papers agree on X, and which contradict each other?" +"What are the main gaps in this research area?" +"Summarise the paper on [topic] and compare it to [other paper]." +"Which paper has the strongest experimental design?" +"What datasets are most commonly used?" +"Are there any contradictions between the papers' findings?" +``` + +## How to Interpret the Gap Analysis + +The gap analysis report has six sections: + +| Section | What it means | +|---|---| +| **Common Themes** | Topics / findings that appear in multiple papers — the consensus view | +| **Contradictions** | Where papers disagree — potential areas of ongoing debate | +| **Missing Experiments** | Experiments that logically follow from the existing work but haven't been done | +| **Missing Populations** | Groups, languages, contexts, or demographics not yet studied | +| **Methodological Gaps** | Approaches not used in any paper (e.g., "no longitudinal study exists") | +| **Suggested Next Steps** | Concrete research directions derived from all of the above | + +> ⚠️ **Always verify the output.** LLMs can hallucinate contradictions or invent plausible-sounding but non-existent gaps. Treat the gap analysis as a *first draft* to refine with domain expertise. + +## Limitations + +1. **LLMs can hallucinate citations** — the agent might confidently say "Paper X found Y" when it did not. Always check claims against the original PDF. + +2. **Gap analysis may miss domain-specific context** — a gap that is obvious to a domain expert ("nobody used technique Z") requires domain knowledge the LLM may not have. + +3. **Works best with 3–10 papers on the same topic** — fewer papers means less to synthesise; more papers risks exceeding the context window during gap analysis. + +4. **PDF extraction quality varies** — scanned PDFs, multi-column layouts, and heavy use of figures degrade text extraction. The LLM falls back gracefully but metadata may be incomplete. + +5. **The agent may loop or over-call tools** — the `max_iterations=8` safety cap prevents infinite loops but may cut off complex multi-paper comparisons. + +## How to Extend + +### Adding a new tool + +1. Create `src/tools/my_tool.py` with a `create_my_tool(…) -> Tool` function. +2. Import and instantiate it in `src/agent.py` inside `create_research_agent`. +3. Add it to the `tools` list passed to `initialize_agent`. + +The agent will automatically start using the new tool based on its description — no other changes needed. + +### Ideas for new tools + +- **`cite_tool`** — generate a BibTeX entry for a paper from its metadata. +- **`timeline_tool`** — order papers chronologically and show how the field evolved. +- **`keyword_tool`** — extract and rank keywords across all papers. +- **`arxiv_tool`** — search arXiv for papers related to the indexed collection. diff --git a/03-research-agent/data/papers/.gitkeep b/03-research-agent/data/papers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/03-research-agent/main.py b/03-research-agent/main.py new file mode 100644 index 0000000..4ed8d8b --- /dev/null +++ b/03-research-agent/main.py @@ -0,0 +1,222 @@ +""" +main.py +------- +Entry point for the 03-research-agent pipeline. + +Usage examples +-------------- +# Parse all PDFs and start an interactive research Q&A session +python main.py --papers-dir data/papers --interactive + +# Ask the agent a single question and exit +python main.py --papers-dir data/papers --query "What methodologies are used across these papers?" + +# Generate a full gap analysis report +python main.py --papers-dir data/papers --topic "transformer models" --report + +# Combine: generate a report and also run an interactive session +python main.py --papers-dir data/papers --topic "NLP" --report --interactive + +# Save the report to a specific file +python main.py --papers-dir data/papers --topic "BERT fine-tuning" --report --output reports/bert.md +""" + +import argparse +import os +import sys +from pathlib import Path + +from dotenv import load_dotenv + +# --------------------------------------------------------------------------- +# Load environment variables from .env before any other imports that might +# need OPENAI_API_KEY (e.g., langchain_openai) +# --------------------------------------------------------------------------- +load_dotenv() + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="main.py", + description="AI Research Agent — analyse a collection of research PDFs.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "--papers-dir", + default=os.getenv("PAPERS_DIR", "data/papers"), + metavar="DIR", + help="Directory containing *.pdf files (default: data/papers)", + ) + parser.add_argument( + "--topic", + default="Research Analysis", + metavar="TOPIC", + help="Research topic label used in the report title (default: 'Research Analysis')", + ) + parser.add_argument( + "--model", + default=os.getenv("OPENAI_MODEL", "gpt-4"), + metavar="MODEL", + help="OpenAI model name (default: gpt-4)", + ) + parser.add_argument( + "--query", + default=None, + metavar="QUESTION", + help="Ask the agent a single question and exit.", + ) + parser.add_argument( + "--report", + action="store_true", + help="Run gap analysis and generate a Markdown report.", + ) + parser.add_argument( + "--output", + default=None, + metavar="PATH", + help="Output file path for the Markdown report (only used with --report).", + ) + parser.add_argument( + "--interactive", + action="store_true", + help="Start an interactive Q&A session with the agent.", + ) + return parser + + +def _check_api_key() -> None: + """Exit early with a clear error if the OpenAI key is missing.""" + if not os.getenv("OPENAI_API_KEY"): + print( + "[main] ERROR: OPENAI_API_KEY environment variable is not set.\n" + " Copy .env.example to .env and add your key.", + file=sys.stderr, + ) + sys.exit(1) + + +def main() -> None: + parser = _build_parser() + args = parser.parse_args() + + _check_api_key() + + # ------------------------------------------------------------------ + # Lazy imports so startup is fast when there are argument errors + # ------------------------------------------------------------------ + from langchain_openai import ChatOpenAI + + from src.agent import create_research_agent, run_agent + from src.gap_analyzer import analyze_gaps, format_gap_analysis + from src.paper_indexer import index_papers + from src.paper_parser import parse_all_papers + from src.report_generator import generate_report + + # ------------------------------------------------------------------ + # Step 1: Validate papers directory + # ------------------------------------------------------------------ + papers_dir = Path(args.papers_dir) + if not papers_dir.exists(): + print(f"[main] ERROR: Papers directory '{papers_dir}' does not exist.", file=sys.stderr) + sys.exit(1) + + pdf_count = len(list(papers_dir.glob("*.pdf"))) + if pdf_count == 0: + print( + f"[main] ERROR: No PDF files found in '{papers_dir}'.\n" + " Add research papers as .pdf files and try again.", + file=sys.stderr, + ) + sys.exit(1) + + print(f"[main] Found {pdf_count} PDF file(s) in '{papers_dir}'.") + + # ------------------------------------------------------------------ + # Step 2: Initialise LLM + # ------------------------------------------------------------------ + print(f"[main] Using model: {args.model}") + llm = ChatOpenAI( + model=args.model, + temperature=0, # deterministic output for research tasks + openai_api_key=os.environ["OPENAI_API_KEY"], + ) + + # ------------------------------------------------------------------ + # Step 3: Parse all papers with LLM + # ------------------------------------------------------------------ + print("\n[main] === Step 1/3: Parsing papers ===") + paper_metadata = parse_all_papers(args.papers_dir, llm) + + if not paper_metadata: + print("[main] ERROR: No papers were successfully parsed.", file=sys.stderr) + sys.exit(1) + + print(f"[main] Parsed {len(paper_metadata)} paper(s).") + + # ------------------------------------------------------------------ + # Step 4: Index papers in FAISS + # ------------------------------------------------------------------ + print("\n[main] === Step 2/3: Indexing papers in FAISS ===") + vector_store = index_papers(args.papers_dir) + + # ------------------------------------------------------------------ + # Step 5: Create the research agent + # ------------------------------------------------------------------ + print("\n[main] === Step 3/3: Building research agent ===") + agent = create_research_agent(vector_store, paper_metadata, llm) + print("[main] Agent ready.\n") + + # ------------------------------------------------------------------ + # Step 6a: Generate report (--report) + # ------------------------------------------------------------------ + if args.report: + print("[main] Running gap analysis…") + gaps = analyze_gaps(paper_metadata, llm) + + print(format_gap_analysis(gaps)) + + report = generate_report( + paper_metadata_list=paper_metadata, + gap_analysis=gaps, + topic=args.topic, + output_path=args.output, + ) + print(f"[main] Report generated ({len(report)} characters).") + + # ------------------------------------------------------------------ + # Step 6b: Single query (--query) + # ------------------------------------------------------------------ + if args.query: + run_agent(args.query, agent) + + # ------------------------------------------------------------------ + # Step 6c: Interactive session (--interactive) + # ------------------------------------------------------------------ + if args.interactive: + print("\n[main] Entering interactive mode. Type 'exit' or 'quit' to stop.\n") + while True: + try: + user_input = input("You: ").strip() + except (EOFError, KeyboardInterrupt): + print("\n[main] Exiting.") + break + + if not user_input: + continue + if user_input.lower() in {"exit", "quit", "q"}: + print("[main] Goodbye!") + break + + run_agent(user_input, agent) + + # If no action flag was given, print help + if not args.report and not args.query and not args.interactive: + parser.print_help() + print( + "\n[main] No action specified. Use --query, --report, or --interactive." + ) + + +if __name__ == "__main__": + main() diff --git a/03-research-agent/requirements.txt b/03-research-agent/requirements.txt new file mode 100644 index 0000000..4b9fb18 --- /dev/null +++ b/03-research-agent/requirements.txt @@ -0,0 +1,10 @@ +langchain==0.1.20 +langchain-community==0.0.38 +langchain-openai==0.1.6 +faiss-cpu==1.8.0 +sentence-transformers==2.7.0 +pypdf==4.2.0 +openai==1.30.1 +python-dotenv==1.0.1 +pydantic==2.7.1 +arxiv==2.1.0 diff --git a/03-research-agent/src/__init__.py b/03-research-agent/src/__init__.py new file mode 100644 index 0000000..2c12f6c --- /dev/null +++ b/03-research-agent/src/__init__.py @@ -0,0 +1,2 @@ +# src/__init__.py +# Makes 'src' a Python package so modules can be imported as src.paper_parser, etc. diff --git a/03-research-agent/src/agent.py b/03-research-agent/src/agent.py new file mode 100644 index 0000000..beb7326 --- /dev/null +++ b/03-research-agent/src/agent.py @@ -0,0 +1,145 @@ +""" +src/agent.py +------------ +Wires together the tools and LLM into a LangChain ReAct agent. + +WHAT IS THE REACT LOOP? +------------------------ +ReAct (Reason + Act) is a prompting strategy where the LLM alternates between: + + Thought – the model reasons about what to do next + Action – the model picks a tool and writes an input for it + Observation – the tool runs and its output is appended to the prompt + … repeat until … + Final Answer – the model decides it has enough information + +Example: + Thought : I need to find papers about transformers. I'll search. + Action : search_papers + Action Input: transformer self-attention mechanism + Observation : [Result 1] Paper: "Attention Is All You Need" … + Thought : I found a relevant paper. Now I'll summarize it. + Action : summarize_paper + Action Input: Attention Is All You Need + Observation : Title: Attention Is All You Need … + Final Answer: The paper "Attention Is All You Need" introduced … + +HOW THE AGENT SEES THE TOOLS +------------------------------ +The agent receives a text-formatted list of tool names and descriptions in its +system prompt. It never sees function signatures or source code. This is why +precise tool descriptions are critical: they are the agent's entire API docs. + +WHY verbose=True IS IMPORTANT FOR LEARNING +------------------------------------------- +With verbose=True LangChain prints every Thought / Action / Observation to +stdout. You can watch the agent's reasoning unfold in real time. This is +invaluable for understanding why the agent chose a particular tool, and for +debugging when it makes the wrong choice. + +THE DIFFERENCE BETWEEN AN AGENT AND A SIMPLE LLM CALL +-------------------------------------------------------- +A simple LLM call is a single prompt → single response. The LLM cannot fetch +new information mid-response. An agent can: + - Decide which tool to call based on intermediate results + - Retry with a different query if the first search returns nothing + - Chain multiple tool calls (search → summarize → compare) + - Stop early if the first observation already answers the question + +WHAT "ZERO SHOT" MEANS +------------------------ +ZERO_SHOT_REACT_DESCRIPTION means the agent needs zero examples (shots) in its +prompt. It figures out when and how to use each tool purely from the tool +description. This keeps the prompt short and avoids the need to curate +few-shot examples for every new tool. +""" + +from langchain.agents import AgentExecutor, AgentType, initialize_agent +from langchain_community.vectorstores import FAISS + +from src.tools.compare_tool import create_compare_tool +from src.tools.search_tool import create_search_tool +from src.tools.summary_tool import create_summary_tool + +# System prompt injected as the agent's persona and behavioural guidelines. +# The prefix is prepended to the auto-generated ReAct prompt that lists tools. +_AGENT_PREFIX = """You are an AI research assistant. You have access to a collection of research papers. +Use the available tools to answer questions about the research literature. +Always cite your sources by mentioning which paper a piece of information comes from. +Think step by step about which tools to use.""" + + +def create_research_agent( + vector_store: FAISS, + paper_metadata: list, + llm, +) -> AgentExecutor: + """Build and return a fully configured ReAct research agent. + + Parameters + ---------- + vector_store : FAISS + Populated FAISS index (from paper_indexer.index_papers). + paper_metadata : list[PaperMetadata] + List of parsed paper metadata objects. + llm : + Any LangChain chat model (e.g., ChatOpenAI). + + Returns + ------- + AgentExecutor + The runnable agent. Call agent.run(query) to use it. + """ + # Build a title → metadata dict for the summary and compare tools + paper_metadata_dict = {pm.title: pm for pm in paper_metadata} + + # Instantiate each tool + search_tool = create_search_tool(vector_store) + summary_tool = create_summary_tool(paper_metadata_dict, llm) + compare_tool = create_compare_tool(paper_metadata_dict, llm) + + tools = [search_tool, summary_tool, compare_tool] + + # initialize_agent wraps the LLM + tools in a ReAct prompt loop. + # ZERO_SHOT_REACT_DESCRIPTION: no few-shot examples, tool selection driven + # entirely by the description strings we provided above. + agent = initialize_agent( + tools=tools, + llm=llm, + agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, + verbose=True, # print Thought/Action/Observation to stdout + handle_parsing_errors=True, # recover gracefully from malformed tool calls + agent_kwargs={"prefix": _AGENT_PREFIX}, + max_iterations=8, # safety cap to prevent infinite loops + ) + + return agent + + +def run_agent(query: str, agent: AgentExecutor) -> str: + """Run a single query through the research agent. + + Parameters + ---------- + query : str + The user's question or instruction. + agent : AgentExecutor + The agent built by :func:`create_research_agent`. + + Returns + ------- + str + The agent's final answer. + """ + print(f"\n{'='*60}") + print(f"Query: {query}") + print(f"{'='*60}\n") + + result = agent.run(query) + + print(f"\n{'='*60}") + print("Final Answer:") + print(result) + print(f"{'='*60}\n") + + return result diff --git a/03-research-agent/src/gap_analyzer.py b/03-research-agent/src/gap_analyzer.py new file mode 100644 index 0000000..fe98de7 --- /dev/null +++ b/03-research-agent/src/gap_analyzer.py @@ -0,0 +1,185 @@ +""" +src/gap_analyzer.py +-------------------- +Uses an LLM to synthesise research gaps across a collection of papers. + +THIS IS PROMPTED REASONING, NOT DATABASE LOGIC +------------------------------------------------ +Traditional literature review tools use citation graphs, keyword co-occurrence +matrices, or statistical topic models to find gaps. We use a different +approach: we feed all paper summaries to an LLM and ask it to reason about +what is missing. + +Advantages: + - No need for structured metadata like citation counts or MeSH terms. + - Can catch conceptual gaps ("nobody studied X in context Y") that keyword + matching would miss. + - Works on any research domain without domain-specific pre-processing. + +Disadvantages (see LIMITATIONS below): + - The LLM may hallucinate gaps or themes that are not actually present. + - Subtle contradictions buried in technical detail may be missed. + - The quality of the output is bounded by the quality of the summaries. + +WHY SYNTHESIS REQUIRES READING ALL PAPERS, NOT JUST SEARCHING +--------------------------------------------------------------- +Semantic search retrieves chunks relevant to a specific query. Gap analysis +is a meta-level task: it needs to observe the DISTRIBUTION of topics across +the entire corpus. If only 3 of 8 papers mention "dataset bias" we cannot +detect that gap by searching for "bias" — we need to compare absence vs +presence across all papers simultaneously. + +LIMITATIONS +----------- +1. The LLM may invent plausible-sounding but false contradictions. +2. Gaps requiring deep domain expertise (e.g., specific biochemical pathways) + may be missed or mischaracterised. +3. This prompt works best with 3–10 papers on the same topic. With 20+ + papers the concatenated summaries may exceed the context window. +4. Results should always be reviewed by a domain expert before acting on them. +""" + +import json + + +# --------------------------------------------------------------------------- +# Synthesis prompt +# --------------------------------------------------------------------------- + +_GAP_ANALYSIS_PROMPT = """You are analyzing a collection of research papers on a topic. + +Here are summaries of all the papers: +{all_summaries} + +Based on these papers, provide a research gap analysis with: +1. common_themes: What topics/findings appear across multiple papers? +2. contradictions: Where do papers disagree or contradict each other? +3. missing_experiments: What experiments have NOT been done that would be valuable? +4. missing_populations: What groups or contexts haven't been studied? +5. methodological_gaps: What methodological approaches are missing? +6. suggested_next_steps: 3-5 specific research directions worth pursuing + +Respond with JSON only.""" + + +def analyze_gaps(paper_metadata_list: list, llm) -> dict: + """Run a cross-paper synthesis prompt and return structured gap analysis. + + Parameters + ---------- + paper_metadata_list : list[PaperMetadata] + All parsed papers to analyse. + llm : + Any LangChain chat model. + + Returns + ------- + dict with keys: common_themes, contradictions, missing_experiments, + missing_populations, methodological_gaps, suggested_next_steps + """ + if not paper_metadata_list: + return { + "common_themes": [], + "contradictions": [], + "missing_experiments": [], + "missing_populations": [], + "methodological_gaps": [], + "suggested_next_steps": [], + "error": "No papers provided for gap analysis.", + } + + # Build a human-readable summary block for each paper + summary_blocks = [] + for pm in paper_metadata_list: + authors_str = ", ".join(pm.authors) if pm.authors else "Unknown" + findings_str = ( + "\n ".join(f"• {f}" for f in pm.key_findings) + if pm.key_findings + else "(not extracted)" + ) + limitations_str = ( + "\n ".join(f"• {l}" for l in pm.limitations) + if pm.limitations + else "(not extracted)" + ) + block = ( + f"--- Paper: {pm.title} ---\n" + f"Authors : {authors_str}\n" + f"Year : {pm.year or 'Unknown'}\n" + f"Methodology: {pm.methodology or 'Not extracted'}\n" + f"Key Findings:\n {findings_str}\n" + f"Limitations:\n {limitations_str}" + ) + summary_blocks.append(block) + + all_summaries = "\n\n".join(summary_blocks) + prompt = _GAP_ANALYSIS_PROMPT.format(all_summaries=all_summaries) + + print("[gap_analyzer] Running synthesis prompt across all papers…") + response = llm.invoke(prompt) + raw = response.content if hasattr(response, "content") else str(response) + + # Strip markdown code fences if present + raw = raw.strip() + if raw.startswith("```"): + raw = raw.split("```", 2)[1] + if raw.startswith("json"): + raw = raw[4:] + raw = raw.rsplit("```", 1)[0].strip() + + try: + gaps = json.loads(raw) + except json.JSONDecodeError as exc: + print(f"[gap_analyzer] Warning: could not parse JSON response: {exc}") + # Return the raw text under a fallback key so nothing is lost + gaps = { + "common_themes": [], + "contradictions": [], + "missing_experiments": [], + "missing_populations": [], + "methodological_gaps": [], + "suggested_next_steps": [], + "raw_response": raw, + } + + return gaps + + +def format_gap_analysis(gaps: dict) -> str: + """Format a gap analysis dict as a human-readable string for console display. + + Parameters + ---------- + gaps : dict + Output of :func:`analyze_gaps`. + + Returns + ------- + str + """ + def _fmt_list(items) -> str: + if not items: + return " (none identified)" + if isinstance(items, list): + return "\n".join(f" • {item}" for item in items) + return f" {items}" + + sections = [ + ("Common Themes", gaps.get("common_themes", [])), + ("Contradictions", gaps.get("contradictions", [])), + ("Missing Experiments", gaps.get("missing_experiments", [])), + ("Missing Populations", gaps.get("missing_populations", [])), + ("Methodological Gaps", gaps.get("methodological_gaps", [])), + ("Suggested Next Steps", gaps.get("suggested_next_steps", [])), + ] + + lines = ["=" * 60, "RESEARCH GAP ANALYSIS", "=" * 60] + for heading, items in sections: + lines.append(f"\n{heading}:") + lines.append(_fmt_list(items)) + + if "raw_response" in gaps: + lines.append("\n[Raw LLM response — JSON parsing failed]") + lines.append(gaps["raw_response"]) + + return "\n".join(lines) diff --git a/03-research-agent/src/paper_indexer.py b/03-research-agent/src/paper_indexer.py new file mode 100644 index 0000000..57d508d --- /dev/null +++ b/03-research-agent/src/paper_indexer.py @@ -0,0 +1,180 @@ +""" +src/paper_indexer.py +-------------------- +Embeds research papers and stores them in a FAISS vector index. + +HOW METADATA TAGGING WORKS IN FAISS (via LangChain) +----------------------------------------------------- +LangChain's FAISS wrapper stores a Python dict alongside each embedded chunk. +When you call `FAISS.from_documents(docs)`, each Document's `.metadata` dict +is persisted verbatim next to its vector. At search time, every returned +Document carries its original metadata, so you can read `doc.metadata["source"]` +to know which paper a chunk came from. + +METADATA FILTERING: "SEARCH ONLY WITHIN PAPER X" +-------------------------------------------------- +FAISS itself does not support SQL-style WHERE clauses — it returns the k nearest +vectors globally. We implement per-paper filtering post-hoc: run the query +across the full index, then discard results whose `doc.metadata["source"]` does +not match the requested paper title. This is simple and correct for small +collections (< a few thousand chunks). For larger collections a dedicated +vector DB (Pinecone, Weaviate, Qdrant) with native metadata filters is better. + +WHY INDEX ALL PAPERS TOGETHER? +------------------------------- +A single shared index enables cross-paper queries like "which papers discuss +attention mechanisms?". If each paper had its own index you would have to +query N indexes and merge results manually. The trade-off is that per-paper +filtering requires a post-search step, but that cost is negligible at the +scale of a typical research collection (3-50 papers). +""" + +import os +from pathlib import Path + +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_community.document_loaders import PyPDFLoader +from langchain_community.embeddings import HuggingFaceEmbeddings +from langchain_community.vectorstores import FAISS + +# --------------------------------------------------------------------------- +# Embedding model +# --------------------------------------------------------------------------- +# "all-MiniLM-L6-v2" is a fast, lightweight model (80 MB) that works well for +# semantic similarity on academic text and runs entirely locally (no API key). +_EMBEDDING_MODEL = "all-MiniLM-L6-v2" + +# Chunk parameters: 1 000 chars with 200-char overlap. +# Research paragraphs average ~500-800 chars, so a 1 000-char window usually +# captures a complete idea. Overlap prevents a sentence at a chunk boundary +# from being split across two embeddings. +_CHUNK_SIZE = 1000 +_CHUNK_OVERLAP = 200 + + +def index_papers( + papers_dir: str, + index_path: str = "papers_faiss_index", +) -> FAISS: + """Load all PDFs in *papers_dir*, chunk them, embed them, and build a FAISS index. + + Each chunk's metadata contains: + source – paper title derived from filename (used for filtering) + file_path – absolute path to the source PDF + chunk_id – sequential integer within that paper + + Parameters + ---------- + papers_dir : str + Directory containing *.pdf files. + index_path : str + Directory where the FAISS index will be saved to disk. + + Returns + ------- + FAISS + The populated vector store. + """ + papers_path = Path(papers_dir) + pdf_files = sorted(papers_path.glob("*.pdf")) + + if not pdf_files: + raise FileNotFoundError(f"No PDF files found in '{papers_dir}'.") + + splitter = RecursiveCharacterTextSplitter( + chunk_size=_CHUNK_SIZE, + chunk_overlap=_CHUNK_OVERLAP, + separators=["\n\n", "\n", " ", ""], # prefer paragraph → line → word splits + ) + + all_docs = [] + for pdf in pdf_files: + print(f"[paper_indexer] Loading: {pdf.name}") + loader = PyPDFLoader(str(pdf)) + pages = loader.load() + + # Derive a human-readable source label from the filename + paper_title = pdf.stem.replace("_", " ").replace("-", " ") + + chunks = splitter.split_documents(pages) + for i, chunk in enumerate(chunks): + # Enrich metadata — LangChain's PyPDFLoader already adds 'source' + # and 'page'; we add our own fields on top. + chunk.metadata["source"] = paper_title + chunk.metadata["file_path"] = str(pdf) + chunk.metadata["chunk_id"] = i + + all_docs.extend(chunks) + print(f"[paper_indexer] → {len(chunks)} chunk(s)") + + print(f"[paper_indexer] Embedding {len(all_docs)} total chunks…") + embeddings = HuggingFaceEmbeddings(model_name=_EMBEDDING_MODEL) + vector_store = FAISS.from_documents(all_docs, embeddings) + + # Persist to disk so we can reload without re-embedding + vector_store.save_local(index_path) + print(f"[paper_indexer] Index saved to '{index_path}'.") + + return vector_store + + +def load_index(index_path: str = "papers_faiss_index") -> FAISS: + """Load a previously saved FAISS index from disk. + + Parameters + ---------- + index_path : str + Directory where the index was saved by :func:`index_papers`. + + Returns + ------- + FAISS + """ + embeddings = HuggingFaceEmbeddings(model_name=_EMBEDDING_MODEL) + vector_store = FAISS.load_local( + index_path, + embeddings, + allow_dangerous_deserialization=True, # required by LangChain ≥ 0.1 + ) + print(f"[paper_indexer] Loaded index from '{index_path}'.") + return vector_store + + +def search_papers( + query: str, + vector_store: FAISS, + k: int = 5, + paper_filter: str = None, +) -> list: + """Semantic search over the FAISS index. + + Parameters + ---------- + query : str + Natural-language search query. + vector_store : FAISS + The populated vector store. + k : int + Number of results to return (before optional filtering). + paper_filter : str or None + If provided, only return chunks whose metadata["source"] contains + this string (case-insensitive). This implements per-paper search. + + Returns + ------- + list[Document] + Matching document chunks, each with .page_content and .metadata. + """ + # Retrieve more candidates when filtering so we still get k results after + # dropping non-matching papers + fetch_k = k * 4 if paper_filter else k + results = vector_store.similarity_search(query, k=fetch_k) + + if paper_filter: + filter_lower = paper_filter.lower() + results = [ + doc for doc in results + if filter_lower in doc.metadata.get("source", "").lower() + ] + + return results[:k] diff --git a/03-research-agent/src/paper_parser.py b/03-research-agent/src/paper_parser.py new file mode 100644 index 0000000..6b3c2ad --- /dev/null +++ b/03-research-agent/src/paper_parser.py @@ -0,0 +1,188 @@ +""" +src/paper_parser.py +------------------- +Parses research PDFs into structured metadata using an LLM. + +WHY STRUCTURED EXTRACTION INSTEAD OF RAW TEXT? +------------------------------------------------ +Storing raw text is simple, but it makes downstream tasks hard: + - Comparing papers requires knowing WHERE the methodology lives. + - Gap analysis needs to see key_findings from every paper side-by-side. + - Fuzzy title search works better when the title is its own field. + +By asking the LLM to fill a fixed schema once (at index time), every later +operation (compare, summarise, gap-analyse) can just read Python attributes +instead of re-searching the raw text. + +WHY ONLY THE FIRST 3 PAGES FOR METADATA? +----------------------------------------- +Research papers place title, authors, and abstract on page 1, sometimes +spilling to page 2. Page 3 occasionally contains the introduction which +gives methodology context. Beyond page 3 we are in body / results / tables +territory — the LLM prompt would be dominated by noisy content and would +exceed the context window for no gain. + +HOW MESSY PDF FORMATTING AFFECTS EXTRACTION +--------------------------------------------- +PDFs are layout-first, not text-first. Common problems: + - Multi-column layouts produce garbled word order when extracted linearly. + - Footnotes and headers are interspersed with body text. + - Figures and tables appear as blank space or gibberish characters. + - Hyphenated line-breaks split words across lines. + +We mitigate this by: + 1. Limiting extraction to the first 3 000 characters (header area). + 2. Using an LLM instead of regexes — LLMs are robust to mild formatting noise. + 3. Falling back to the filename as title when the LLM cannot parse the text. +""" + +import json +import os +from pathlib import Path +from typing import Optional + +from langchain_community.document_loaders import PyPDFLoader +from pydantic import BaseModel, Field + + +class PaperMetadata(BaseModel): + """Structured representation of a research paper's key metadata. + + Fields are intentionally coarse-grained (e.g., 'methodology' is a + 1-2 sentence description) so the LLM can fill them reliably even when + the PDF formatting is messy. + """ + + title: str = Field(description="Full title of the paper") + authors: list[str] = Field(default_factory=list, description="List of author names") + year: Optional[str] = Field(default=None, description="Publication year if found") + abstract: Optional[str] = Field(default=None, description="Full abstract text") + methodology: Optional[str] = Field( + default=None, + description="1-2 sentence description of the research methodology", + ) + key_findings: list[str] = Field( + default_factory=list, + description="3-5 main findings from the paper", + ) + limitations: list[str] = Field( + default_factory=list, + description="Limitations acknowledged by the authors", + ) + file_path: str = Field(description="Absolute or relative path to the source PDF") + + +# --------------------------------------------------------------------------- +# Extraction prompt +# --------------------------------------------------------------------------- + +_EXTRACTION_PROMPT = """Extract the following from this research paper text: +- title: Full paper title +- authors: List of author names +- year: Publication year (if found) +- abstract: Full abstract text +- methodology: Brief description of research methodology (1-2 sentences) +- key_findings: List of 3-5 main findings +- limitations: List of limitations mentioned by authors + +Paper text (first 3000 chars): +{text} + +Respond with JSON only.""" + + +def parse_paper(file_path: str, llm) -> PaperMetadata: + """Load a single PDF and extract structured metadata using an LLM. + + Steps + ----- + 1. Load all pages with PyPDFLoader. + 2. Concatenate text from the first 3 pages and truncate to 3 000 chars. + 3. Ask the LLM to fill the extraction schema (JSON response). + 4. Parse the JSON into a PaperMetadata object. + 5. On any failure, fall back to filename-derived title with empty fields. + + Parameters + ---------- + file_path : str + Path to the PDF file. + llm : + Any LangChain chat model (e.g., ChatOpenAI). + + Returns + ------- + PaperMetadata + """ + loader = PyPDFLoader(file_path) + pages = loader.load() + + # Combine text from first 3 pages only — metadata lives here + excerpt = "\n".join(p.page_content for p in pages[:3])[:3000] + + prompt = _EXTRACTION_PROMPT.format(text=excerpt) + + try: + response = llm.invoke(prompt) + # Handle both string responses and AIMessage objects + raw = response.content if hasattr(response, "content") else str(response) + + # Strip markdown code fences if the LLM wraps JSON in ```json ... ``` + raw = raw.strip() + if raw.startswith("```"): + raw = raw.split("```", 2)[1] + if raw.startswith("json"): + raw = raw[4:] + raw = raw.rsplit("```", 1)[0].strip() + + data = json.loads(raw) + return PaperMetadata( + title=data.get("title", Path(file_path).stem), + authors=data.get("authors", []), + year=str(data.get("year")) if data.get("year") else None, + abstract=data.get("abstract"), + methodology=data.get("methodology"), + key_findings=data.get("key_findings", []), + limitations=data.get("limitations", []), + file_path=file_path, + ) + + except Exception as exc: + # Graceful degradation: use the filename as title, leave everything else blank. + # This means the paper can still be searched even if LLM extraction failed. + print(f"[paper_parser] Warning: could not extract metadata from '{file_path}': {exc}") + return PaperMetadata( + title=Path(file_path).stem, + file_path=file_path, + ) + + +def parse_all_papers(papers_dir: str, llm) -> list[PaperMetadata]: + """Parse every PDF found in *papers_dir* and return a list of PaperMetadata. + + Parameters + ---------- + papers_dir : str + Directory that contains *.pdf files (non-recursive). + llm : + Any LangChain chat model. + + Returns + ------- + list[PaperMetadata] + One entry per successfully located PDF. Empty list if no PDFs found. + """ + papers_path = Path(papers_dir) + pdf_files = sorted(papers_path.glob("*.pdf")) + + if not pdf_files: + print(f"[paper_parser] No PDF files found in '{papers_dir}'.") + return [] + + results: list[PaperMetadata] = [] + for pdf in pdf_files: + print(f"[paper_parser] Parsing: {pdf.name}") + metadata = parse_paper(str(pdf), llm) + results.append(metadata) + + print(f"[paper_parser] Parsed {len(results)} paper(s).") + return results diff --git a/03-research-agent/src/report_generator.py b/03-research-agent/src/report_generator.py new file mode 100644 index 0000000..99bbddc --- /dev/null +++ b/03-research-agent/src/report_generator.py @@ -0,0 +1,201 @@ +""" +src/report_generator.py +------------------------ +Generates a structured Markdown report from parsed paper metadata and gap analysis. + +HOW OUTPUT PARSERS CAN ENFORCE STRUCTURE +----------------------------------------- +Here we build the Markdown manually using Python f-strings. An alternative +approach is to use LangChain's StructuredOutputParser or PydanticOutputParser: + + 1. Define a Pydantic model with all report sections as fields. + 2. Attach the parser's format_instructions to the LLM prompt. + 3. The LLM fills the model; the parser deserialises it. + +This is valuable when the *content* of each section needs to be LLM-generated +(e.g., "write a paragraph summarising the common themes"). For our report the +content comes from already-structured dicts (PaperMetadata, gap analysis dict), +so simple string formatting is cleaner and faster — no extra LLM call needed. + +The general lesson: use output parsers when you need the LLM to produce +structured data; use string formatting when you already have structured data +and just need to render it. +""" + +import os +from datetime import datetime +from pathlib import Path + + +def generate_report( + paper_metadata_list: list, + gap_analysis: dict, + topic: str, + output_path: str = None, +) -> str: + """Generate a full Markdown research report and optionally save it to disk. + + Parameters + ---------- + paper_metadata_list : list[PaperMetadata] + All parsed papers. + gap_analysis : dict + Output of gap_analyzer.analyze_gaps(). + topic : str + Human-readable topic label used in the report title. + output_path : str or None + If provided, the report is written to this path. + If None, a timestamped filename is used automatically. + + Returns + ------- + str + The complete Markdown report as a string. + """ + now = datetime.now() + timestamp = now.strftime("%Y-%m-%d %H:%M") + file_timestamp = now.strftime("%Y%m%d_%H%M%S") + + # ------------------------------------------------------------------ + # Helper utilities + # ------------------------------------------------------------------ + + def _list_section(items) -> str: + """Render a list of strings as a Markdown bullet list.""" + if not items: + return "_None identified._\n" + return "\n".join(f"- {item}" for item in items) + "\n" + + def _numbered_section(items) -> str: + """Render a list of strings as a Markdown numbered list.""" + if not items: + return "_None identified._\n" + return "\n".join(f"{i}. {item}" for i, item in enumerate(items, 1)) + "\n" + + # ------------------------------------------------------------------ + # Section: Title & preamble + # ------------------------------------------------------------------ + lines = [ + f"# Research Literature Analysis: {topic}", + "", + f"**Generated:** {timestamp} ", + f"**Papers analysed:** {len(paper_metadata_list)}", + "", + ] + + # ------------------------------------------------------------------ + # Section: Overview + # ------------------------------------------------------------------ + lines += [ + "## Overview", + "", + f"This report analyses **{len(paper_metadata_list)}** research paper(s) on the topic of **{topic}**.", + "", + "### Papers in this collection", + "", + ] + for pm in paper_metadata_list: + authors_str = ", ".join(pm.authors[:3]) if pm.authors else "Unknown" + if len(pm.authors) > 3: + authors_str += " et al." + year_str = f" ({pm.year})" if pm.year else "" + lines.append(f"- **{pm.title}**{year_str} — {authors_str}") + lines.append("") + + # ------------------------------------------------------------------ + # Section: Individual Paper Summaries + # ------------------------------------------------------------------ + lines += ["## Individual Paper Summaries", ""] + + for pm in paper_metadata_list: + authors_str = ", ".join(pm.authors) if pm.authors else "Unknown" + lines += [ + f"### {pm.title}", + "", + f"**Authors:** {authors_str} ", + f"**Year:** {pm.year or 'Unknown'} ", + f"**File:** `{Path(pm.file_path).name}`", + "", + ] + if pm.abstract: + lines += ["**Abstract:**", "", pm.abstract, ""] + if pm.methodology: + lines += [f"**Methodology:** {pm.methodology}", ""] + if pm.key_findings: + lines += ["**Key Findings:**", ""] + lines += [f"- {f}" for f in pm.key_findings] + lines.append("") + if pm.limitations: + lines += ["**Limitations:**", ""] + lines += [f"- {l}" for l in pm.limitations] + lines.append("") + lines.append("---") + lines.append("") + + # ------------------------------------------------------------------ + # Section: Cross-Paper Analysis + # ------------------------------------------------------------------ + lines += ["## Cross-Paper Analysis", ""] + + lines += ["### Common Themes", ""] + lines.append(_list_section(gap_analysis.get("common_themes", []))) + + lines += ["### Contradictions Between Papers", ""] + lines.append(_list_section(gap_analysis.get("contradictions", []))) + + # ------------------------------------------------------------------ + # Section: Research Gaps + # ------------------------------------------------------------------ + lines += ["## Research Gaps", ""] + + lines += ["### Missing Experiments", ""] + lines.append(_list_section(gap_analysis.get("missing_experiments", []))) + + lines += ["### Under-Studied Populations or Contexts", ""] + lines.append(_list_section(gap_analysis.get("missing_populations", []))) + + lines += ["### Methodological Gaps", ""] + lines.append(_list_section(gap_analysis.get("methodological_gaps", []))) + + # ------------------------------------------------------------------ + # Section: Suggested Next Steps + # ------------------------------------------------------------------ + lines += ["## Suggested Next Steps", ""] + lines.append(_numbered_section(gap_analysis.get("suggested_next_steps", []))) + + # ------------------------------------------------------------------ + # Section: Paper Index + # ------------------------------------------------------------------ + lines += ["## Paper Index", ""] + lines += ["| # | Title | File |", "|---|-------|------|"] + for i, pm in enumerate(paper_metadata_list, 1): + fname = Path(pm.file_path).name + lines.append(f"| {i} | {pm.title} | `{fname}` |") + lines.append("") + + # ------------------------------------------------------------------ + # Footer + # ------------------------------------------------------------------ + lines += [ + "---", + "", + "_Report generated by the 03-research-agent pipeline. " + "Always verify findings against the original papers — " + "LLMs can hallucinate citations and misrepresent content._", + "", + ] + + report = "\n".join(lines) + + # ------------------------------------------------------------------ + # Save to disk + # ------------------------------------------------------------------ + if output_path is None: + output_path = f"research_report_{file_timestamp}.md" + + out = Path(output_path) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(report, encoding="utf-8") + print(f"[report_generator] Report saved to '{output_path}'.") + + return report diff --git a/03-research-agent/src/tools/__init__.py b/03-research-agent/src/tools/__init__.py new file mode 100644 index 0000000..ba6e12c --- /dev/null +++ b/03-research-agent/src/tools/__init__.py @@ -0,0 +1,2 @@ +# src/tools/__init__.py +# Makes 'src/tools' a Python package. diff --git a/03-research-agent/src/tools/compare_tool.py b/03-research-agent/src/tools/compare_tool.py new file mode 100644 index 0000000..000dfed --- /dev/null +++ b/03-research-agent/src/tools/compare_tool.py @@ -0,0 +1,133 @@ +""" +src/tools/compare_tool.py +-------------------------- +LangChain Tool that uses an LLM to compare two papers' methodologies and findings. + +WHERE AGENTS SHINE: MULTI-STEP REASONING ACROSS DOCUMENTS +---------------------------------------------------------- +Simple RAG retrieves the nearest chunks to a query and returns them. A +comparison task is inherently multi-step: + 1. Identify paper A and paper B by name. + 2. Retrieve full metadata for each. + 3. Synthesise similarities, differences, and contradictions. + 4. Produce a structured answer. + +An agent can chain these steps autonomously. Without the agent layer you +would need to hard-code this pipeline. With an agent the user can simply ask +"Compare Paper A and Paper B" and the agent decides to call this tool. + +HOW THE REACT AGENT USES THIS TOOL +------------------------------------ +A typical agent trace might look like: + + Thought : The user wants to compare "Transformer" and "BERT". + I should use the compare_papers tool. + Action : compare_papers + Action Input: Transformer vs BERT + Observation: [structured comparison returned by this tool] + Thought : I now have the comparison. I can answer the user. + Final Answer: … + +The agent learns the input format ("A vs B") solely from the tool description — +no examples are needed. + +INPUT FORMAT +------------ +The tool expects: " vs <title of paper 2>" +The separator " vs " (with spaces) is chosen because it is unambiguous and +unlikely to appear in a paper title. +""" + +from langchain.tools import Tool + +# Comparison prompt template +_COMPARE_PROMPT = """Compare these two research papers: + +Paper 1: {title1} +Methodology: {methodology1} +Key Findings: {findings1} + +Paper 2: {title2} +Methodology: {methodology2} +Key Findings: {findings2} + +Provide a structured comparison covering: +1. Methodological similarities and differences +2. Agreements in findings +3. Contradictions in findings +4. Which paper's approach is stronger and why""" + + +def create_compare_tool(paper_metadata_dict: dict, llm) -> Tool: + """Build a LangChain Tool that compares two papers using an LLM. + + Parameters + ---------- + paper_metadata_dict : dict + Maps paper title (str) → PaperMetadata object. + llm : + Any LangChain chat model used to generate the comparison narrative. + + Returns + ------- + Tool + """ + + def _find_paper(query: str): + """Case-insensitive substring match against known paper titles.""" + q = query.strip().lower() + for title, meta in paper_metadata_dict.items(): + if q in title.lower(): + return meta + return None + + def _compare(input_str: str) -> str: + """Parse 'Paper A vs Paper B', retrieve metadata, call LLM to compare.""" + # Parse the two titles from the 'X vs Y' format + if " vs " not in input_str: + return ( + "Invalid input format. Please use: 'Paper Title A vs Paper Title B'. " + f"Got: '{input_str}'" + ) + + parts = input_str.split(" vs ", maxsplit=1) + title_a, title_b = parts[0].strip(), parts[1].strip() + + paper_a = _find_paper(title_a) + paper_b = _find_paper(title_b) + + # Report clearly which lookups failed so the agent can retry + if paper_a is None and paper_b is None: + return f"Could not find papers matching '{title_a}' or '{title_b}'." + if paper_a is None: + return f"Could not find a paper matching '{title_a}'." + if paper_b is None: + return f"Could not find a paper matching '{title_b}'." + + # Format findings lists as readable strings for the prompt + def fmt_findings(meta) -> str: + if not meta.key_findings: + return "Not extracted" + return "; ".join(meta.key_findings) + + prompt = _COMPARE_PROMPT.format( + title1=paper_a.title, + methodology1=paper_a.methodology or "Not extracted", + findings1=fmt_findings(paper_a), + title2=paper_b.title, + methodology2=paper_b.methodology or "Not extracted", + findings2=fmt_findings(paper_b), + ) + + response = llm.invoke(prompt) + return response.content if hasattr(response, "content") else str(response) + + return Tool( + name="compare_papers", + description=( + "Compare two research papers' methodologies and findings. " + "Input: two paper titles separated by ' vs ' " + "(e.g., 'Paper A vs Paper B')" + ), + func=_compare, + ) diff --git a/03-research-agent/src/tools/search_tool.py b/03-research-agent/src/tools/search_tool.py new file mode 100644 index 0000000..ab1a985 --- /dev/null +++ b/03-research-agent/src/tools/search_tool.py @@ -0,0 +1,90 @@ +""" +src/tools/search_tool.py +------------------------ +LangChain Tool that lets the agent do semantic search over indexed papers. + +WHAT IS A LANGCHAIN TOOL? +-------------------------- +A Tool is a Python function wrapped in a thin object that carries three things: + 1. name – a short identifier (e.g., "search_papers") + 2. description – a plain-English explanation of WHEN and HOW to use the tool + 3. func – the actual callable that receives a string and returns a string + +The agent never inspects the function's source code. It only reads the +name + description to decide whether to call the tool. + +HOW THE AGENT DECIDES WHEN TO USE THIS TOOL +-------------------------------------------- +During each reasoning step the ReAct agent compares its current sub-goal +(e.g., "I need to find papers about attention") to every tool's description. +If "search_papers" says "Search across all indexed research papers …" that +is an obvious match. Poor descriptions cause the agent to either skip a +useful tool or call the wrong one. + +THE INPUT/OUTPUT CONTRACT +-------------------------- + Input – a plain string (the search query). + Output – a plain string that the agent reads as an observation. + +LangChain enforces this contract: whatever your func returns is converted to +str and injected into the agent's prompt as the "Observation:" line. + +WHY TOOL DESCRIPTIONS MUST BE PRECISE +--------------------------------------- +The agent is stateless — it has no memory of tool internals. If the +description says "search papers" without clarifying the expected input format, +the agent might pass a JSON object or a question instead of a keyword query, +producing poor results. Explicit examples in the description (like "Input: a +search query string") dramatically improve reliability. +""" + +from langchain.tools import Tool + +from src.paper_indexer import search_papers + + +def create_search_tool(vector_store) -> Tool: + """Build and return a LangChain Tool that searches the FAISS index. + + Parameters + ---------- + vector_store : FAISS + The populated FAISS vector store built by paper_indexer.index_papers(). + + Returns + ------- + Tool + Ready-to-use LangChain Tool instance. + """ + + def _search(query: str) -> str: + """Internal function called by the agent with a plain query string.""" + docs = search_papers(query, vector_store, k=3) + + if not docs: + return "No relevant passages found for that query." + + parts = [] + for i, doc in enumerate(docs, start=1): + source = doc.metadata.get("source", "Unknown paper") + # page is 0-indexed in PyPDFLoader; add 1 for human readability + page = doc.metadata.get("page", 0) + 1 + snippet = doc.page_content.strip()[:400] # keep response concise + parts.append( + f"[Result {i}]\n" + f" Paper : {source}\n" + f" Page : {page}\n" + f" Text : {snippet}…" + ) + + return "\n\n".join(parts) + + return Tool( + name="search_papers", + description=( + "Search across all indexed research papers. " + "Use this to find relevant information, methodologies, or findings. " + "Input: a search query string." + ), + func=_search, + ) diff --git a/03-research-agent/src/tools/summary_tool.py b/03-research-agent/src/tools/summary_tool.py new file mode 100644 index 0000000..b4e4fe9 --- /dev/null +++ b/03-research-agent/src/tools/summary_tool.py @@ -0,0 +1,96 @@ +""" +src/tools/summary_tool.py +------------------------- +LangChain Tool that returns a structured summary of a single named paper. + +TOOL PATTERN +------------ +This tool is a good example of the lookup pattern: + - Input : a (possibly partial) paper title provided by the agent. + - Process: find the matching PaperMetadata object, format its fields. + - Output : a formatted string the agent can quote in its final answer. + +The agent uses this tool when it already knows the paper's name and wants +detailed information about it, as opposed to the search_papers tool which +is used when the agent is still looking for relevant papers. + +INPUT PARSING: FUZZY TITLE MATCHING +------------------------------------- +We do a case-insensitive substring match: a paper titled +"Attention Is All You Need" will match inputs like "attention", "all you need", +or the full title. This is intentional — the agent's input may be an +approximation if it inferred the title from a previous search result. + +If multiple papers match, we return the first one (alphabetical order from +the dict). For a production system you might use fuzzy matching (e.g., +rapidfuzz) or ask the agent to be more specific. +""" + +from langchain.tools import Tool + + +def create_summary_tool(paper_metadata_dict: dict, llm) -> Tool: + """Build a LangChain Tool that summarises a specific paper by title. + + Parameters + ---------- + paper_metadata_dict : dict + Maps paper title (str) → PaperMetadata object. + Built in main.py as {pm.title: pm for pm in paper_metadata_list}. + llm : + Unused here but accepted for API consistency with other tool factories. + + Returns + ------- + Tool + """ + + def _summarize(title_query: str) -> str: + """Find a paper by (partial) title and return a formatted summary.""" + query_lower = title_query.strip().lower() + + # Fuzzy match: find the first paper whose title contains the query + match = None + for title, meta in paper_metadata_dict.items(): + if query_lower in title.lower(): + match = meta + break + + if match is None: + available = ", ".join(paper_metadata_dict.keys()) or "none" + return ( + f"No paper found matching '{title_query}'. " + f"Available papers: {available}" + ) + + # Format the metadata as a readable summary + authors_str = ", ".join(match.authors) if match.authors else "Unknown" + findings_str = ( + "\n".join(f" • {f}" for f in match.key_findings) + if match.key_findings + else " (not extracted)" + ) + limitations_str = ( + "\n".join(f" • {l}" for l in match.limitations) + if match.limitations + else " (not extracted)" + ) + + return ( + f"Title : {match.title}\n" + f"Authors : {authors_str}\n" + f"Year : {match.year or 'Unknown'}\n" + f"Abstract : {match.abstract or 'Not available'}\n\n" + f"Methodology: {match.methodology or 'Not extracted'}\n\n" + f"Key Findings:\n{findings_str}\n\n" + f"Limitations:\n{limitations_str}" + ) + + return Tool( + name="summarize_paper", + description=( + "Get a structured summary of a specific research paper. " + "Input: the paper title (or a unique part of it)." + ), + func=_summarize, + ) From a946216dacdaaea66153101b8f34e8bf1dd8f484 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:11:42 +0000 Subject: [PATCH 5/9] Add 04-multimodal-rag project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-index multimodal RAG pipeline for PDF documents: - multimodal_parser: pdfplumber extracts text blocks, PNG images, and tables - text_indexer: FAISS index for text chunks (all-MiniLM-L6-v2) - image_processor: GPT-4V base64 captioning with graceful fallback - image_indexer: FAISS index over image captions with image_path metadata - table_processor: LLM converts 2-D tables to prose descriptions + CSV export - table_indexer: FAISS index over table descriptions with csv_path metadata - query_router: LLM classifies query → TEXT / IMAGE / TABLE / ALL - multi_retriever: fetches from relevant indexes, interleaves and de-duplicates - generator: modality-labelled prompt → GPT-4 final answer - main.py: argparse CLI with --skip-images, --skip-tables, --interactive flags Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- 04-multimodal-rag/.env.example | 12 + 04-multimodal-rag/README.md | 235 ++++++++++++++++++ .../data/extracted/images/.gitkeep | 0 .../data/extracted/tables/.gitkeep | 0 04-multimodal-rag/data/sample_docs/.gitkeep | 0 04-multimodal-rag/main.py | 214 ++++++++++++++++ 04-multimodal-rag/requirements.txt | 12 + 04-multimodal-rag/src/__init__.py | 2 + 04-multimodal-rag/src/generator.py | 98 ++++++++ 04-multimodal-rag/src/image_indexer.py | 124 +++++++++ 04-multimodal-rag/src/image_processor.py | 168 +++++++++++++ 04-multimodal-rag/src/multi_retriever.py | 169 +++++++++++++ 04-multimodal-rag/src/multimodal_parser.py | 163 ++++++++++++ 04-multimodal-rag/src/query_router.py | 114 +++++++++ 04-multimodal-rag/src/table_indexer.py | 123 +++++++++ 04-multimodal-rag/src/table_processor.py | 162 ++++++++++++ 04-multimodal-rag/src/text_indexer.py | 106 ++++++++ 17 files changed, 1702 insertions(+) create mode 100644 04-multimodal-rag/.env.example create mode 100644 04-multimodal-rag/README.md create mode 100644 04-multimodal-rag/data/extracted/images/.gitkeep create mode 100644 04-multimodal-rag/data/extracted/tables/.gitkeep create mode 100644 04-multimodal-rag/data/sample_docs/.gitkeep create mode 100644 04-multimodal-rag/main.py create mode 100644 04-multimodal-rag/requirements.txt create mode 100644 04-multimodal-rag/src/__init__.py create mode 100644 04-multimodal-rag/src/generator.py create mode 100644 04-multimodal-rag/src/image_indexer.py create mode 100644 04-multimodal-rag/src/image_processor.py create mode 100644 04-multimodal-rag/src/multi_retriever.py create mode 100644 04-multimodal-rag/src/multimodal_parser.py create mode 100644 04-multimodal-rag/src/query_router.py create mode 100644 04-multimodal-rag/src/table_indexer.py create mode 100644 04-multimodal-rag/src/table_processor.py create mode 100644 04-multimodal-rag/src/text_indexer.py diff --git a/04-multimodal-rag/.env.example b/04-multimodal-rag/.env.example new file mode 100644 index 0000000..ac45447 --- /dev/null +++ b/04-multimodal-rag/.env.example @@ -0,0 +1,12 @@ +# OpenAI API Key (required — GPT-4V used for image understanding) +OPENAI_API_KEY=your_openai_api_key_here + +# Model for text generation +OPENAI_MODEL=gpt-4 + +# Vision model for image captioning (GPT-4V) +VISION_MODEL=gpt-4-vision-preview + +# Paths for extracted content +IMAGES_OUTPUT_DIR=data/extracted/images +TABLES_OUTPUT_DIR=data/extracted/tables diff --git a/04-multimodal-rag/README.md b/04-multimodal-rag/README.md new file mode 100644 index 0000000..ab66956 --- /dev/null +++ b/04-multimodal-rag/README.md @@ -0,0 +1,235 @@ +# 04 — Multimodal RAG + +A Retrieval-Augmented Generation system that understands **text, images, and tables** inside PDF documents. + +> A text-only RAG can't answer *"What does the architecture diagram show?"* — but this system can. +> It extracts every content type from the document, builds a dedicated search index per modality, +> routes each query to the right index, and generates an answer that explicitly cites whether the +> information came from a paragraph, a chart, or a data table. + +--- + +## What "Multimodal" Means + +| Query | Text-only RAG | This System | +|---|---|---| +| "Explain the authentication flow" | ✅ Can answer | ✅ Can answer | +| "What does the flowchart in section 3 show?" | ❌ Cannot answer | ✅ Captions image, answers from description | +| "What was Q4 revenue?" | ⚠️ Only if table was also in prose | ✅ Extracts table, generates description | +| "Summarise all key findings" | ✅ Partial | ✅ Draws from text + images + tables | + +--- + +## Architecture + +``` +PDF Document + │ + ▼ +┌────────────────────┐ +│ multimodal_parser │ ── pdfplumber extracts text / tables / images +└────────┬───────────┘ + │ + ┌────┴─────────────────────────────────┐ + ▼ ▼ ▼ +┌─────────┐ ┌────────────┐ ┌─────────────────┐ +│ Text │ │ Images │ │ Tables │ +│ Blocks │ │ (PNG files)│ │ (list of lists) │ +└────┬────┘ └─────┬──────┘ └────────┬────────┘ + │ │ │ + │ GPT-4V caption LLM description + │ │ │ + ▼ ▼ ▼ +┌──────────┐ ┌────────────┐ ┌──────────────────┐ +│ FAISS │ │ FAISS │ │ FAISS │ +│ Text Idx │ │ Image Idx │ │ Table Idx │ +└────┬─────┘ └─────┬──────┘ └────────┬─────────┘ + └─────────────┴─────────────────── ┘ + │ + ┌──────┴──────┐ + │ Query Router│ ── classifies query → TEXT / IMAGE / TABLE / ALL + └──────┬──────┘ + │ + ┌──────┴──────┐ + │Multi-Retriev│ ── fetches top-k from relevant indexes, merges + └──────┬──────┘ + │ + ┌──────┴──────┐ + │ Generator │ ── GPT-4 builds final answer from mixed context + └─────────────┘ +``` + +--- + +## Model Comparison + +| Modality | Model | Why | +|---|---|---| +| Text | all-MiniLM-L6-v2 | Fast, free, runs locally, strong retrieval quality | +| Images | GPT-4V (`gpt-4-vision-preview`) | Understands visual content — charts, diagrams, photos | +| Tables | GPT-3.5 / GPT-4 | Strong at structured data reasoning; converts rows to prose | +| Generation | GPT-4 | Best reasoning across mixed text / image / table context | + +**Alternative (cost-free images):** [LLaVA](https://ollama.com/library/llava) via Ollama runs locally and produces comparable captions without API charges. + +--- + +## Setup + +### 1. Clone and enter the project +```bash +cd 04-multimodal-rag +``` + +### 2. Create and activate a virtual environment +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +``` + +### 3. Install dependencies +```bash +pip install -r requirements.txt +``` + +### 4. Configure environment variables +```bash +cp .env.example .env +# Edit .env and set OPENAI_API_KEY +``` + +### 5. Add a PDF document +```bash +cp /path/to/your/document.pdf data/sample_docs/ +``` + +--- + +## Usage + +### Ask a single question +```bash +python main.py --file data/sample_docs/annual_report.pdf \ + --query "What was Q4 revenue?" +``` + +### Skip image captioning during development (saves GPT-4V cost) +```bash +python main.py --file data/sample_docs/annual_report.pdf \ + --query "Summarise the key findings" \ + --skip-images +``` + +### Skip both images and tables (fastest, text-only mode) +```bash +python main.py --file data/sample_docs/report.pdf \ + --query "What is the company's strategy?" \ + --skip-images --skip-tables +``` + +### Interactive Q&A loop +```bash +python main.py --file data/sample_docs/annual_report.pdf --interactive +``` + +### Use a different model +```bash +python main.py --file data/sample_docs/report.pdf \ + --query "Describe the architecture diagram" \ + --model gpt-4o \ + --vision-model gpt-4o +``` + +### Full CLI reference +``` +--file Path to PDF document (required) +--query Question to answer +--model Text generation model (default: gpt-4) +--vision-model Vision model for image captioning (default: gpt-4-vision-preview) +--skip-images Skip GPT-4V image captioning +--skip-tables Skip LLM table description generation +--interactive Interactive Q&A loop after indexing +``` + +--- + +## Cost Considerations + +> ⚠️ **GPT-4V calls cost more. Use `--skip-images` during development.** + +Approximate costs per document (GPT-4V "high" detail, ~1024×1024 images): +- Each image ≈ 765 input tokens ≈ **$0.008–$0.01** at current pricing +- A 50-page document with 20 images ≈ **$0.15–$0.20** in image captioning alone +- Captions are generated once and cached; re-running queries does not re-caption + +**Cost optimisation tips:** +1. `--skip-images` — bypass GPT-4V entirely during development +2. Pre-generate captions once, save to JSON, reload on subsequent runs +3. Use LLaVA locally (free) for development, GPT-4V for production +4. Use `gpt-3.5-turbo` for table descriptions (cheaper, still good at structured data) + +--- + +## What This Can vs Cannot Answer + +### CAN answer (multimodal RAG) +- "What does the flowchart in section 3 show?" → image caption search +- "What was Q3 revenue according to the table?" → table description search +- "Describe the network architecture diagram" → image caption search +- "What were the year-over-year growth percentages?" → table search +- "Explain the data pipeline shown in figure 2" → image + text combined + +### CANNOT answer (text-only RAG) +- "What does the flowchart in section 3 show?" — no image content indexed +- "What was Q3 revenue?" — only if the number also appeared in prose + +--- + +## Comparison with Project 1 (Basic RAG) + +| Feature | Project 1 (Basic RAG) | Project 4 (Multimodal RAG) | +|---|---|---| +| Content types | Text only | Text + Images + Tables | +| Indexes | 1 FAISS index | 3 FAISS indexes | +| Embedding model | all-MiniLM-L6-v2 | all-MiniLM-L6-v2 (same) | +| LLM calls | Generation only | Captioning + Table desc + Classification + Generation | +| Query routing | None (always searches) | Router classifies query → selects relevant index(es) | +| Image understanding | ❌ | ✅ GPT-4V captions | +| Table understanding | ❌ | ✅ LLM-generated descriptions | +| Cost | Low (local embeddings) | Medium–High (GPT-4V for images) | +| Complexity | Low | High | + +**New concepts introduced in this project:** +- Multimodal document parsing (pdfplumber) +- Vision model integration (GPT-4V via base64 image encoding) +- Multiple specialised FAISS indexes (one per modality) +- Query routing / intent classification +- Cross-modality result merging and ranking +- Modality-aware generation prompts + +--- + +## Project Structure + +``` +04-multimodal-rag/ +├── README.md +├── requirements.txt +├── .env.example +├── data/ +│ ├── sample_docs/ ← Put your PDF files here +│ └── extracted/ +│ ├── images/ ← PNG files extracted from PDFs +│ └── tables/ ← CSV files extracted from PDFs +├── src/ +│ ├── multimodal_parser.py ← PDF → text + images + tables +│ ├── text_indexer.py ← FAISS index for text chunks +│ ├── image_processor.py ← GPT-4V image captioning +│ ├── image_indexer.py ← FAISS index for image captions +│ ├── table_processor.py ← LLM table → prose description +│ ├── table_indexer.py ← FAISS index for table descriptions +│ ├── query_router.py ← Classify query → modality(ies) +│ ├── multi_retriever.py ← Fetch + merge results from indexes +│ └── generator.py ← Build prompt + call LLM +└── main.py ← CLI entry point +``` diff --git a/04-multimodal-rag/data/extracted/images/.gitkeep b/04-multimodal-rag/data/extracted/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/04-multimodal-rag/data/extracted/tables/.gitkeep b/04-multimodal-rag/data/extracted/tables/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/04-multimodal-rag/data/sample_docs/.gitkeep b/04-multimodal-rag/data/sample_docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/04-multimodal-rag/main.py b/04-multimodal-rag/main.py new file mode 100644 index 0000000..4d01078 --- /dev/null +++ b/04-multimodal-rag/main.py @@ -0,0 +1,214 @@ +""" +main.py — Multimodal RAG Pipeline +----------------------------------- +Orchestrates the full multimodal retrieval-augmented generation pipeline: + 1. Parse PDF → extract text, images, tables + 2. Index each modality in its own FAISS vector store + 3. Route a user query to the relevant index(es) + 4. Retrieve top-k results + 5. Generate a grounded answer + +Usage examples +-------------- +# Full pipeline (index + query) +python main.py --file data/sample_docs/annual_report.pdf --query "What was Q4 revenue?" + +# Skip image captioning to save GPT-4V cost during development +python main.py --file data/sample_docs/annual_report.pdf --query "Summarise the findings" --skip-images + +# Interactive mode — ask multiple questions after indexing once +python main.py --file data/sample_docs/annual_report.pdf --interactive +""" + +import argparse +import os +import sys + +from dotenv import load_dotenv +from langchain_openai import ChatOpenAI +from openai import OpenAI + +from src.multimodal_parser import parse_document +from src.text_indexer import index_text_chunks +from src.image_processor import process_all_images +from src.image_indexer import index_image_captions +from src.table_processor import process_all_tables +from src.table_indexer import index_table_descriptions +from src.query_router import classify_query +from src.multi_retriever import retrieve_all, merge_and_rank_results +from src.generator import generate_answer + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Multimodal RAG: answer questions over text, images, and tables in a PDF." + ) + parser.add_argument( + "--file", + required=True, + help="Path to the PDF document to process.", + ) + parser.add_argument( + "--query", + default=None, + help="Question to answer. Required unless --interactive is set.", + ) + parser.add_argument( + "--model", + default=None, + help="OpenAI model for text generation (default: env OPENAI_MODEL or gpt-4).", + ) + parser.add_argument( + "--vision-model", + default=None, + dest="vision_model", + help="OpenAI vision model for image captioning (default: gpt-4-vision-preview).", + ) + parser.add_argument( + "--skip-images", + action="store_true", + dest="skip_images", + help="Skip GPT-4V image captioning (saves cost/time during development).", + ) + parser.add_argument( + "--skip-tables", + action="store_true", + dest="skip_tables", + help="Skip LLM-based table description generation.", + ) + parser.add_argument( + "--interactive", + action="store_true", + help="After indexing, enter an interactive Q&A loop.", + ) + return parser + + +def answer_query( + query: str, + llm, + text_index, + image_index, + table_index, +) -> str: + """Route → retrieve → generate for a single query.""" + print(f"\n[main] Query: {query}") + + query_types = classify_query(query, llm) + print(f"[main] Router selected modalities: {[qt.value for qt in query_types]}") + + raw_results = retrieve_all( + query=query, + query_types=query_types, + text_index=text_index, + image_index=image_index, + table_index=table_index, + k=3, + ) + + ranked_results = merge_and_rank_results(raw_results) + print(f"[main] Retrieved {len(ranked_results)} result(s) after merge/de-dup.") + + answer = generate_answer(query, ranked_results, llm) + return answer + + +def main() -> None: + load_dotenv() + + parser = build_arg_parser() + args = parser.parse_args() + + # ── Validate arguments ──────────────────────────────────────────────────── + if not args.interactive and args.query is None: + parser.error("--query is required unless --interactive is set.") + + if not os.path.isfile(args.file): + print(f"[main] ERROR: File not found: {args.file}") + sys.exit(1) + + # ── Resolve model names ─────────────────────────────────────────────────── + text_model = args.model or os.getenv("OPENAI_MODEL", "gpt-4") + vision_model = args.vision_model or os.getenv("VISION_MODEL", "gpt-4-vision-preview") + images_dir = os.getenv("IMAGES_OUTPUT_DIR", "data/extracted/images") + tables_dir = os.getenv("TABLES_OUTPUT_DIR", "data/extracted/tables") + + # ── Initialise clients ──────────────────────────────────────────────────── + openai_api_key = os.getenv("OPENAI_API_KEY") + if not openai_api_key: + print("[main] ERROR: OPENAI_API_KEY is not set. Copy .env.example to .env and fill it in.") + sys.exit(1) + + llm = ChatOpenAI(model=text_model, openai_api_key=openai_api_key) + openai_client = OpenAI(api_key=openai_api_key) + + # ── Step 1: Parse document ──────────────────────────────────────────────── + print(f"\n[main] Parsing document: {args.file}") + doc = parse_document(args.file, images_dir=images_dir, tables_dir=tables_dir) + + print( + f"[main] Found {len(doc.text_blocks)} text blocks, " + f"{len(doc.image_paths)} images, " + f"{len(doc.tables)} tables." + ) + + # ── Step 2: Index text ──────────────────────────────────────────────────── + text_index = None + if doc.text_blocks: + print(f"\n[main] Indexing {len(doc.text_blocks)} text blocks …") + text_index = index_text_chunks(doc.text_blocks, index_path="text_faiss_index") + else: + print("[main] No text blocks found — skipping text index.") + + # ── Step 3: Caption and index images ────────────────────────────────────── + image_index = None + if not args.skip_images and doc.image_paths: + print(f"\n[main] Captioning {len(doc.image_paths)} image(s) with {vision_model} …") + print(" ⚠️ GPT-4V calls cost more than text models.") + print(" Use --skip-images during development to avoid these charges.") + image_data = process_all_images(doc.image_paths, openai_client, vision_model) + print(f"\n[main] Indexing {len(image_data)} image caption(s) …") + image_index = index_image_captions(image_data, index_path="image_faiss_index") + elif args.skip_images: + print("\n[main] --skip-images set: skipping image captioning and indexing.") + else: + print("\n[main] No images found in document.") + + # ── Step 4: Process and index tables ───────────────────────────────────── + table_index = None + if not args.skip_tables and doc.tables: + print(f"\n[main] Processing {len(doc.tables)} table(s) …") + table_data = process_all_tables(doc.tables, llm, tables_dir=tables_dir) + print(f"[main] Indexing {len(table_data)} table description(s) …") + table_index = index_table_descriptions(table_data, index_path="table_faiss_index") + elif args.skip_tables: + print("\n[main] --skip-tables set: skipping table processing and indexing.") + else: + print("\n[main] No tables found in document.") + + # ── Step 5: Answer query / interactive loop ─────────────────────────────── + print("\n" + "─" * 60) + + if args.interactive: + print("[main] Interactive mode. Type 'quit' or 'exit' to stop.\n") + while True: + try: + query = input("Question: ").strip() + except (EOFError, KeyboardInterrupt): + print("\n[main] Exiting.") + break + if query.lower() in ("quit", "exit", "q"): + print("[main] Exiting.") + break + if not query: + continue + answer = answer_query(query, llm, text_index, image_index, table_index) + print(f"\nAnswer:\n{answer}\n") + print("─" * 60) + else: + answer = answer_query(args.query, llm, text_index, image_index, table_index) + print(f"\nAnswer:\n{answer}\n") + + +if __name__ == "__main__": + main() diff --git a/04-multimodal-rag/requirements.txt b/04-multimodal-rag/requirements.txt new file mode 100644 index 0000000..994aaa8 --- /dev/null +++ b/04-multimodal-rag/requirements.txt @@ -0,0 +1,12 @@ +langchain==0.1.20 +langchain-community==0.0.38 +langchain-openai==0.1.6 +faiss-cpu==1.8.0 +sentence-transformers==2.7.0 +openai==1.30.1 +python-dotenv==1.0.1 +unstructured[pdf]==0.13.7 +pdfplumber==0.11.1 +Pillow==10.3.0 +pandas==2.2.2 +base64 diff --git a/04-multimodal-rag/src/__init__.py b/04-multimodal-rag/src/__init__.py new file mode 100644 index 0000000..5a953af --- /dev/null +++ b/04-multimodal-rag/src/__init__.py @@ -0,0 +1,2 @@ +# src/__init__.py +# Multimodal RAG package — handles text, image, and table modalities diff --git a/04-multimodal-rag/src/generator.py b/04-multimodal-rag/src/generator.py new file mode 100644 index 0000000..dd04e3d --- /dev/null +++ b/04-multimodal-rag/src/generator.py @@ -0,0 +1,98 @@ +""" +generator.py +------------ +Builds a structured prompt from multimodal retrieved results and calls the +LLM to produce the final answer. + +The prompt explicitly labels each piece of context by modality ([TEXT], +[IMAGE DESCRIPTIONS], [TABLE DATA]) so the model can reason about the +*source* of information — e.g. "the bar chart (image) shows Q4 was highest, +while the revenue table confirms $1.2M." The model is instructed to +acknowledge which modality informed its answer, which improves transparency +and helps users verify the response against the source document. +""" + + +def generate_answer( + query: str, + retrieved_results: list[dict], + llm, + include_image_refs: bool = True, +) -> str: + """ + Generate a natural-language answer from multimodal retrieved context. + + Parameters + ---------- + query : The user's original question. + retrieved_results : Combined, ranked list from multi_retriever.merge_and_rank_results(). + llm : LangChain LLM / chat model. + include_image_refs: When True, append "See image: <path>" lines for any + image results so the user knows where to look. + + Returns + ------- + Formatted answer string. + """ + # ── Separate results by modality ───────────────────────────────────────── + text_chunks: list[str] = [] + image_captions: list[str] = [] + table_descriptions: list[str] = [] + image_refs: list[str] = [] + + for result in retrieved_results: + modality = result.get("modality", "text") + content = result.get("content", "").strip() + + if modality == "text": + text_chunks.append(content) + elif modality == "image": + image_captions.append(content) + if include_image_refs: + img_path = result.get("metadata", {}).get("image_path", "") + if img_path: + image_refs.append(img_path) + elif modality == "table": + table_descriptions.append(content) + + # ── Build context sections ──────────────────────────────────────────────── + text_section = "\n\n".join(text_chunks) if text_chunks else "No text context available." + image_section = "\n\n".join(image_captions) if image_captions else "No image context available." + table_section = "\n\n".join(table_descriptions) if table_descriptions else "No table context available." + + # ── Assemble prompt ─────────────────────────────────────────────────────── + prompt = f"""\ +Answer the following question based on the provided context from a document. +The context includes text, image descriptions, and table data. + +Context: +[TEXT] +{text_section} + +[IMAGE DESCRIPTIONS] +{image_section} + +[TABLE DATA] +{table_section} + +Question: {query} + +Answer (mention which type of content informed your answer — text/image/table):""" + + # ── Call the LLM ────────────────────────────────────────────────────────── + try: + if hasattr(llm, "invoke"): + response = llm.invoke(prompt) + answer = response.content if hasattr(response, "content") else str(response) + else: + answer = llm.predict(prompt) + answer = answer.strip() + except Exception as exc: + answer = f"[generator] LLM call failed: {exc}" + + # ── Append image references if requested ───────────────────────────────── + if include_image_refs and image_refs: + refs_block = "\n".join(f"See image: {path}" for path in image_refs) + answer = f"{answer}\n\n{refs_block}" + + return answer diff --git a/04-multimodal-rag/src/image_indexer.py b/04-multimodal-rag/src/image_indexer.py new file mode 100644 index 0000000..4cb286b --- /dev/null +++ b/04-multimodal-rag/src/image_indexer.py @@ -0,0 +1,124 @@ +""" +image_indexer.py +---------------- +Builds and queries a FAISS vector index over image *captions*. + +Key insight: we are searching text (captions), but returning image references. +The caption is the searchable representation; the metadata carries the file +path so callers can retrieve or display the actual image. This pattern — +"index the description, store the reference" — is the standard approach for +making non-text assets semantically searchable without specialised multimodal +embedding models. +""" + +from langchain_community.embeddings import HuggingFaceEmbeddings +from langchain_community.vectorstores import FAISS +from langchain.schema import Document + + +_EMBED_MODEL_NAME = "all-MiniLM-L6-v2" + + +def _get_embeddings() -> HuggingFaceEmbeddings: + return HuggingFaceEmbeddings(model_name=_EMBED_MODEL_NAME) + + +def index_image_captions( + image_data: list[dict], + index_path: str = "image_faiss_index", +) -> FAISS: + """ + Embed image captions and save a FAISS index to disk. + + Parameters + ---------- + image_data : List of dicts with keys "image_path" and "caption" + (as returned by image_processor.process_all_images()). + index_path : Directory where FAISS index files are written. + + Returns + ------- + A LangChain FAISS vector store whose documents are captions with + image_path stored in metadata. + """ + if not image_data: + raise ValueError("image_data is empty — nothing to index.") + + # page_content is the caption text that will be embedded and searched. + # metadata carries the image_path so we can return the file reference + # when this document is retrieved. + docs = [ + Document( + page_content=item["caption"], + metadata={ + "image_path": item["image_path"], + "image_type": item.get("image_type", "figure"), + "modality": "image", + }, + ) + for item in image_data + ] + + embeddings = _get_embeddings() + vector_store = FAISS.from_documents(docs, embeddings) + vector_store.save_local(index_path) + + print(f"[image_indexer] Indexed {len(docs)} image captions → '{index_path}'") + return vector_store + + +def load_image_index(index_path: str) -> FAISS: + """ + Load a previously saved FAISS image-caption index from disk. + + Parameters + ---------- + index_path : Directory path passed to index_image_captions(). + + Returns + ------- + A LangChain FAISS vector store. + """ + embeddings = _get_embeddings() + vector_store = FAISS.load_local( + index_path, embeddings, allow_dangerous_deserialization=True + ) + print(f"[image_indexer] Loaded image index from '{index_path}'") + return vector_store + + +def search_images( + query: str, + vector_store: FAISS, + k: int = 3, +) -> list[dict]: + """ + Retrieve the top-k image captions most relevant to a query. + + Parameters + ---------- + query : Natural language question or search string. + vector_store : A loaded or freshly-built FAISS image-caption index. + k : Number of results to return. + + Returns + ------- + List of dicts: + { + "caption" : str — the generated image description + "image_path" : str — path to the original image file + "image_type" : str — coarse type (chart, diagram, photo, …) + "score" : float — FAISS L2 distance (lower = more similar) + } + """ + raw_results = vector_store.similarity_search_with_score(query, k=k) + + return [ + { + "caption": doc.page_content, + "image_path": doc.metadata.get("image_path", ""), + "image_type": doc.metadata.get("image_type", "figure"), + "score": float(score), + } + for doc, score in raw_results + ] diff --git a/04-multimodal-rag/src/image_processor.py b/04-multimodal-rag/src/image_processor.py new file mode 100644 index 0000000..7b7b46d --- /dev/null +++ b/04-multimodal-rag/src/image_processor.py @@ -0,0 +1,168 @@ +""" +image_processor.py +------------------ +Converts image files into natural-language captions using GPT-4V (or any +compatible OpenAI vision model), making images semantically searchable. + +Why convert images to text captions? +-------------------------------------- +Semantic search engines (FAISS + sentence-transformers) operate in *text* +embedding space. A raw PNG file cannot be compared to a natural-language +query like "architecture diagram of the data pipeline." + +By asking GPT-4V to *describe* an image in detail, we produce a text string +that captures the visual content — labels, shapes, data, layout — in a form +that a sentence-transformer can embed and a user query can match against. + +How GPT-4V works +----------------- +GPT-4V (gpt-4-vision-preview) is a multimodal large language model that +accepts *both* text and images in the same prompt. Images are supplied as +base64-encoded strings inside a message with role "user". + +The base64 encoding pattern: + 1. Read the image file in binary mode. + 2. Encode with base64.b64encode(raw_bytes).decode("utf-8"). + 3. Pass as {"type": "image_url", "image_url": {"url": "data:image/png;base64,<b64>"}} + inside the messages list. + +Cost consideration ⚠️ +---------------------- +GPT-4V is significantly more expensive than text-only GPT models: + * A 1024×1024 image costs roughly 765 tokens at the "high" detail setting. + * Caption all images once, then **cache** the results to avoid re-captioning + on every run. The main pipeline serialises captions to disk for this reason. + +Alternative: LLaVA +------------------- +LLaVA (Large Language and Vision Assistant) is an open-source vision model +that runs locally with Ollama — zero API cost. Swap `caption_image` to call +`ollama.chat(model="llava", ...)` for a cost-free local alternative, at the +expense of some caption quality. +""" + +import base64 +import io + +from PIL import Image + + +def caption_image( + image_path: str, + openai_client, + vision_model: str = "gpt-4-vision-preview", +) -> dict: + """ + Generate a detailed text caption for a single image using GPT-4V. + + Parameters + ---------- + image_path : Path to the image file (PNG, JPEG, etc.). + openai_client : An initialised openai.OpenAI() client instance. + vision_model : OpenAI vision model identifier. + + Returns + ------- + dict with keys: + "image_path" — the original path (used as a reference in search results) + "caption" — the generated natural-language description + "image_type" — coarse type extracted from the caption (e.g. "chart") + """ + # ── Step 1: read and base64-encode the image ───────────────────────────── + with open(image_path, "rb") as f: + raw_bytes = f.read() + + # Normalise to PNG via PIL to ensure a consistent MIME type. + pil_img = Image.open(io.BytesIO(raw_bytes)).convert("RGB") + png_buffer = io.BytesIO() + pil_img.save(png_buffer, format="PNG") + b64_image = base64.b64encode(png_buffer.getvalue()).decode("utf-8") + + # ── Step 2: build the GPT-4V prompt ────────────────────────────────────── + # The data-URI scheme embeds the image directly in the JSON payload. + data_uri = f"data:image/png;base64,{b64_image}" + + prompt_text = ( + "Describe this image in detail for a document search system. " + "Include: what the image shows, any text visible, any data or statistics shown, " + "the type of visualization (chart, diagram, photo, etc.)." + ) + + try: + response = openai_client.chat.completions.create( + model=vision_model, + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": prompt_text}, + {"type": "image_url", "image_url": {"url": data_uri}}, + ], + } + ], + max_tokens=512, + ) + caption = response.choices[0].message.content.strip() + + # Derive a coarse image_type by scanning the caption for keywords. + image_type = _infer_image_type(caption) + + except Exception as exc: + # Graceful degradation: if GPT-4V is unavailable (quota, model access, + # or network issue) we return a placeholder so the pipeline keeps running. + # The placeholder still gets indexed; it just won't match queries well. + print(f" [image_processor] GPT-4V unavailable for '{image_path}': {exc}") + caption = f"[Image caption unavailable — {image_path}]" + image_type = "unknown" + + return { + "image_path": image_path, + "caption": caption, + "image_type": image_type, + } + + +def process_all_images( + image_paths: list[str], + openai_client, + vision_model: str = "gpt-4-vision-preview", +) -> list[dict]: + """ + Caption every image in the list and return combined results. + + Parameters + ---------- + image_paths : List of file paths returned by the multimodal parser. + openai_client : An initialised openai.OpenAI() client instance. + vision_model : OpenAI vision model identifier. + + Returns + ------- + List of caption dicts (same structure as caption_image() return value). + + Note: captioning is done sequentially to stay within rate limits. + For large document sets, consider batching with a short sleep between calls. + """ + results = [] + for idx, path in enumerate(image_paths, start=1): + print(f" [image_processor] Captioning image {idx}/{len(image_paths)}: {path}") + result = caption_image(path, openai_client, vision_model) + results.append(result) + return results + + +# ── Private helpers ────────────────────────────────────────────────────────── + + +def _infer_image_type(caption: str) -> str: + """Heuristically classify the image type from its caption text.""" + caption_lower = caption.lower() + if any(w in caption_lower for w in ("chart", "bar", "pie", "line graph", "plot")): + return "chart" + if any(w in caption_lower for w in ("diagram", "flowchart", "architecture", "uml")): + return "diagram" + if any(w in caption_lower for w in ("table", "matrix", "grid")): + return "table_image" + if any(w in caption_lower for w in ("photo", "photograph", "picture", "image of")): + return "photo" + return "figure" diff --git a/04-multimodal-rag/src/multi_retriever.py b/04-multimodal-rag/src/multi_retriever.py new file mode 100644 index 0000000..dac6c52 --- /dev/null +++ b/04-multimodal-rag/src/multi_retriever.py @@ -0,0 +1,169 @@ +""" +multi_retriever.py +------------------ +Queries one or more FAISS indexes in parallel based on the query types +returned by the router, then merges the results into a single ranked list. + +The challenge of ranking across modalities +------------------------------------------- +Each FAISS index returns an L2 distance score in the embedding space of +all-MiniLM-L6-v2 (384 dimensions). Because all three indexes use the *same* +embedding model, scores are theoretically comparable — but in practice: + + * The distribution of scores differs by modality (short captions tend to + have lower variance than long text chunks). + * A "0.3 score" for a text chunk may not be semantically equivalent to a + "0.3 score" for an image caption. + +Two ranking strategies are discussed here: + + Simple (implemented): interleave results — 1 text result, 1 image result, + 1 table result — so every modality is represented in the context, regardless + of raw score magnitude. Easy to implement, transparent to the user. + + Complex (alternative): normalise scores per-modality using min-max scaling, + then sort globally. More precise but can still suppress a modality entirely + if its scores are consistently higher (worse) than others. + +We use the simple interleaving approach and let the generator model weight +results contextually via its attention mechanism. + +De-duplication +-------------- +The same text snippet can theoretically appear in multiple indexes (e.g. a +table that was also mentioned verbatim in the text). We de-duplicate on +content string to avoid feeding the same information twice to the generator. +""" + +from langchain_community.vectorstores import FAISS + +from .query_router import QueryType +from .text_indexer import search_text +from .image_indexer import search_images +from .table_indexer import search_tables + + +def retrieve_all( + query: str, + query_types: list[QueryType], + text_index: FAISS | None, + image_index: FAISS | None, + table_index: FAISS | None, + k: int = 3, +) -> list[dict]: + """ + Retrieve top-k results from each relevant index and return a combined list. + + Parameters + ---------- + query : User's natural-language question. + query_types : List of QueryType values from the router. + text_index : Loaded FAISS text index (or None if not built). + image_index : Loaded FAISS image-caption index (or None if not built). + table_index : Loaded FAISS table-description index (or None if not built). + k : Number of results to fetch from each relevant index. + + Returns + ------- + List of result dicts: + { + "content" : str — the text content (chunk / caption / description) + "modality" : str — "text" | "image" | "table" + "metadata" : dict — index-specific metadata (image_path, csv_path, etc.) + "source" : str — human-readable source label + "score" : float + } + """ + results: list[dict] = [] + + if QueryType.TEXT in query_types and text_index is not None: + for doc, score in search_text(query, text_index, k=k): + results.append( + { + "content": doc.page_content, + "modality": "text", + "metadata": doc.metadata, + "source": f"text_chunk_{doc.metadata.get('chunk_id', '?')}", + "score": float(score), + } + ) + + if QueryType.IMAGE in query_types and image_index is not None: + for item in search_images(query, image_index, k=k): + results.append( + { + "content": item["caption"], + "modality": "image", + "metadata": { + "image_path": item["image_path"], + "image_type": item["image_type"], + }, + "source": item["image_path"], + "score": item["score"], + } + ) + + if QueryType.TABLE in query_types and table_index is not None: + for item in search_tables(query, table_index, k=k): + results.append( + { + "content": item["description"], + "modality": "table", + "metadata": { + "table_id": item["table_id"], + "csv_path": item["csv_path"], + "page": item["page"], + }, + "source": item["table_id"], + "score": item["score"], + } + ) + + return results + + +def merge_and_rank_results(results: list[dict]) -> list[dict]: + """ + De-duplicate and interleave results across modalities. + + De-duplication is done on content string (exact match). Ranking uses a + simple modality-interleaving strategy: we pick results round-robin from + text → image → table buckets so every modality is represented early in + the context window. + + Parameters + ---------- + results : Combined list from retrieve_all(). + + Returns + ------- + De-duplicated, interleaved list of result dicts. + """ + # De-duplicate on content string. + seen_content: set[str] = set() + unique: list[dict] = [] + for r in results: + if r["content"] not in seen_content: + seen_content.add(r["content"]) + unique.append(r) + + # Separate into modality buckets. + buckets: dict[str, list[dict]] = {"text": [], "image": [], "table": []} + for r in unique: + bucket_key = r["modality"] if r["modality"] in buckets else "text" + buckets[bucket_key].append(r) + + # Sort each bucket by ascending score (lower L2 = more similar). + for bucket in buckets.values(): + bucket.sort(key=lambda x: x["score"]) + + # Interleave: take one from each non-empty bucket in rotation. + merged: list[dict] = [] + order = ["text", "image", "table"] + max_len = max((len(b) for b in buckets.values()), default=0) + for i in range(max_len): + for modality in order: + if i < len(buckets[modality]): + merged.append(buckets[modality][i]) + + return merged diff --git a/04-multimodal-rag/src/multimodal_parser.py b/04-multimodal-rag/src/multimodal_parser.py new file mode 100644 index 0000000..c438816 --- /dev/null +++ b/04-multimodal-rag/src/multimodal_parser.py @@ -0,0 +1,163 @@ +""" +multimodal_parser.py +-------------------- +Parses a PDF document and extracts three distinct modalities: + 1. Text blocks — raw text per page, ready for embedding + 2. Images — saved as PNG files; need vision model captioning before embedding + 3. Tables — extracted as list-of-lists, converted to dict rows for downstream processing + +Why separate modalities before indexing? +----------------------------------------- +Each content type requires a completely different processing pipeline: + + Text → can be chunked and embedded directly with a sentence-transformer. + + Images → embedding raw pixel data is rarely useful for Q&A. Instead we use a + vision model (GPT-4V or LLaVA) to *describe* each image in plain English, + then embed that description. This bridges the "semantic gap" between a + pixel array and a natural-language query. + + Tables → 2-D structured data doesn't embed well as a flat string of cell values. + We convert each table into a short natural-language paragraph + ("Q1 revenue was $1 M, up 12 % year-over-year …") that a sentence- + transformer can compare against a user question. + +Limitations +----------- + * pdfplumber excels at text and table extraction from text-based PDFs. + * Image extraction relies on the PDF's internal XObject stream; quality varies. + Scanned PDFs with no embedded images will yield zero images here. + * Large tables spanning multiple pages may be split; downstream code should + handle partial tables gracefully. +""" + +import os +from dataclasses import dataclass, field +from pathlib import Path + +import pdfplumber +from PIL import Image + + +@dataclass +class ParsedDocument: + """Container for all content extracted from a single PDF.""" + + file_name: str + # One entry per page; each entry is the full text of that page. + text_blocks: list[str] = field(default_factory=list) + # Absolute/relative paths to saved PNG files extracted from the PDF. + image_paths: list[str] = field(default_factory=list) + # Each table is a dict with keys "rows" (list[list]) and "page" (int). + tables: list[dict] = field(default_factory=list) + + +def parse_document( + file_path: str, + images_dir: str = "data/extracted/images", + tables_dir: str = "data/extracted/tables", +) -> ParsedDocument: + """ + Open a PDF and extract text, images, and tables into a ParsedDocument. + + Parameters + ---------- + file_path : Path to the source PDF file. + images_dir : Directory where extracted PNG images are saved. + tables_dir : Directory where extracted tables are saved (CSV, handled downstream). + + Returns + ------- + ParsedDocument with text_blocks, image_paths, and tables populated. + """ + Path(images_dir).mkdir(parents=True, exist_ok=True) + Path(tables_dir).mkdir(parents=True, exist_ok=True) + + file_name = Path(file_path).stem + text_blocks: list[str] = [] + image_paths: list[str] = [] + tables: list[dict] = [] + + with pdfplumber.open(file_path) as pdf: + for page_num, page in enumerate(pdf.pages, start=1): + + # ── 1. TEXT ────────────────────────────────────────────────────────── + # extract_text() returns the full text of the page as a single string. + # We keep one block per page; callers can chunk further if needed. + page_text = page.extract_text() or "" + if page_text.strip(): + text_blocks.append(page_text.strip()) + + # ── 2. TABLES ──────────────────────────────────────────────────────── + # extract_tables() returns a list of tables; each table is a list of + # rows, and each row is a list of cell values (strings or None). + for table_idx, raw_table in enumerate(page.extract_tables()): + # Replace None cells with empty string to avoid downstream errors. + clean_rows = [ + [cell if cell is not None else "" for cell in row] + for row in raw_table + ] + tables.append( + { + "rows": clean_rows, # list[list[str]] + "page": page_num, + "table_index": table_idx, + } + ) + + # ── 3. IMAGES ──────────────────────────────────────────────────────── + # pdfplumber exposes raw image XObjects via page.images. + # Each entry is a dict with keys: "stream" (raw bytes), "x0", "y0", + # "x1", "y1", "width", "height", etc. + # We reconstruct a PIL Image from the raw stream and save as PNG. + for img_idx, img_meta in enumerate(page.images): + try: + raw_stream = img_meta.get("stream") + if raw_stream is None: + continue + + # The stream is a pdfplumber PDFStream object; get its raw data. + raw_data = ( + raw_stream.get_data() + if hasattr(raw_stream, "get_data") + else bytes(raw_stream) + ) + + # Attempt to open as a PIL Image (handles JPEG, PNG, etc.). + import io + try: + pil_img = Image.open(io.BytesIO(raw_data)) + pil_img = pil_img.convert("RGB") # normalise colour mode + except Exception: + # The raw bytes may be raw pixel data rather than an encoded + # image format. Fall back to using width/height from metadata. + width = int(img_meta.get("width", 100)) + height = int(img_meta.get("height", 100)) + pil_img = Image.frombytes("RGB", (width, height), raw_data) + + img_filename = f"{file_name}_page{page_num}_img{img_idx}.png" + img_save_path = os.path.join(images_dir, img_filename) + pil_img.save(img_save_path, format="PNG") + image_paths.append(img_save_path) + + except Exception as exc: + # Non-fatal: log and continue — a single bad image shouldn't + # abort extraction of the rest of the document. + print( + f" [parser] Could not extract image {img_idx} " + f"on page {page_num}: {exc}" + ) + + print( + f"[parser] '{file_name}': " + f"{len(text_blocks)} text blocks, " + f"{len(image_paths)} images, " + f"{len(tables)} tables extracted." + ) + + return ParsedDocument( + file_name=file_name, + text_blocks=text_blocks, + image_paths=image_paths, + tables=tables, + ) diff --git a/04-multimodal-rag/src/query_router.py b/04-multimodal-rag/src/query_router.py new file mode 100644 index 0000000..8217fbe --- /dev/null +++ b/04-multimodal-rag/src/query_router.py @@ -0,0 +1,114 @@ +""" +query_router.py +--------------- +Classifies an incoming user query to determine which content modalities +(text, image, table) are most likely to contain the answer, then routes +retrieval to the appropriate FAISS indexes. + +Why routing matters +-------------------- +Without routing every query would hit all three indexes, which: + * Wastes embedding / similarity-search compute. + * Inflates cost when GPT-4V-captioned image indexes are large. + * Dilutes the final context with irrelevant cross-modal results. + +By classifying upfront we retrieve *only* from relevant indexes, reducing +latency and cost while keeping the context focused. + +When to use ALL +---------------- +Complex questions (e.g. "Summarise the findings from section 2") often span +all content types. When the classifier is uncertain it returns ALL, which is +the safe default — it is better to over-search than to miss the answer. + +Parsing the LLM output +----------------------- +We ask the LLM to respond with a JSON object `{"types": [...]}` to make +parsing deterministic. If the response cannot be parsed as JSON we fall back +to ALL to maintain correctness at the cost of a broader search. +""" + +import json +import re +from enum import Enum + + +class QueryType(Enum): + TEXT = "TEXT" + IMAGE = "IMAGE" + TABLE = "TABLE" + ALL = "ALL" + + +_CLASSIFICATION_PROMPT = """\ +Classify this query to determine which type of document content would best answer it. + +Query: {query} + +Choose one or more from: +- TEXT: The answer is likely in text paragraphs +- IMAGE: The answer requires looking at a visual/diagram/photo +- TABLE: The answer requires numerical data from a table or chart +- ALL: Search all content types + +Common patterns: +- "show me", "what does X look like", "diagram of" → IMAGE +- "how many", "revenue", "statistics", "percentage", "trend" → TABLE +- "explain", "describe", "what is", "how does" → TEXT +- Complex questions → ALL + +Respond with JSON only: {{"types": ["TEXT", "TABLE"]}} +""" + + +def classify_query(query: str, llm) -> list[QueryType]: + """ + Ask the LLM to classify a user query by relevant content modality. + + Parameters + ---------- + query : The user's natural-language question. + llm : A LangChain LLM / chat model that supports .invoke() or .predict(). + + Returns + ------- + List of QueryType enum values indicating which indexes to search. + Falls back to [QueryType.ALL] on any parsing error. + """ + prompt = _CLASSIFICATION_PROMPT.format(query=query) + + try: + if hasattr(llm, "invoke"): + response = llm.invoke(prompt) + raw = response.content if hasattr(response, "content") else str(response) + else: + raw = llm.predict(prompt) + + # Extract JSON from the response — the model may wrap it in markdown fences. + json_match = re.search(r"\{.*?\}", raw, re.DOTALL) + if not json_match: + raise ValueError("No JSON object found in LLM response.") + + parsed = json.loads(json_match.group()) + type_strings: list[str] = parsed.get("types", ["ALL"]) + + query_types = [] + for t in type_strings: + t_upper = t.upper() + if t_upper == "ALL": + # ALL expands to all three specific types. + return [QueryType.TEXT, QueryType.IMAGE, QueryType.TABLE] + try: + query_types.append(QueryType(t_upper)) + except ValueError: + pass # Unknown type string — skip. + + if not query_types: + raise ValueError("No valid QueryType values parsed.") + + return query_types + + except Exception as exc: + # Fallback: search everything rather than potentially missing the answer. + print(f" [query_router] Classification failed ({exc}) — defaulting to ALL.") + return [QueryType.TEXT, QueryType.IMAGE, QueryType.TABLE] diff --git a/04-multimodal-rag/src/table_indexer.py b/04-multimodal-rag/src/table_indexer.py new file mode 100644 index 0000000..172f898 --- /dev/null +++ b/04-multimodal-rag/src/table_indexer.py @@ -0,0 +1,123 @@ +""" +table_indexer.py +---------------- +Builds and queries a FAISS vector index over natural-language table +descriptions produced by table_processor.py. + +The descriptions are embedded with the same all-MiniLM-L6-v2 model used for +text and image captions, giving us a single, consistent semantic space across +all three modalities. The metadata carries the table_id and csv_path so +callers can retrieve the exact CSV data when needed. +""" + +from langchain_community.embeddings import HuggingFaceEmbeddings +from langchain_community.vectorstores import FAISS +from langchain.schema import Document + + +_EMBED_MODEL_NAME = "all-MiniLM-L6-v2" + + +def _get_embeddings() -> HuggingFaceEmbeddings: + return HuggingFaceEmbeddings(model_name=_EMBED_MODEL_NAME) + + +def index_table_descriptions( + table_data: list[dict], + index_path: str = "table_faiss_index", +) -> FAISS: + """ + Embed table descriptions and save a FAISS index to disk. + + Parameters + ---------- + table_data : List of dicts with keys "table_id", "csv_path", + "description" (as returned by table_processor.process_all_tables()). + index_path : Directory where FAISS index files are written. + + Returns + ------- + A LangChain FAISS vector store whose documents are descriptions with + table_id and csv_path stored in metadata. + """ + if not table_data: + raise ValueError("table_data is empty — nothing to index.") + + docs = [ + Document( + page_content=item["description"], + metadata={ + "table_id": item["table_id"], + "csv_path": item["csv_path"], + "page": item.get("page", 0), + "modality": "table", + }, + ) + for item in table_data + ] + + embeddings = _get_embeddings() + vector_store = FAISS.from_documents(docs, embeddings) + vector_store.save_local(index_path) + + print(f"[table_indexer] Indexed {len(docs)} table descriptions → '{index_path}'") + return vector_store + + +def load_table_index(index_path: str) -> FAISS: + """ + Load a previously saved FAISS table-description index from disk. + + Parameters + ---------- + index_path : Directory path passed to index_table_descriptions(). + + Returns + ------- + A LangChain FAISS vector store. + """ + embeddings = _get_embeddings() + vector_store = FAISS.load_local( + index_path, embeddings, allow_dangerous_deserialization=True + ) + print(f"[table_indexer] Loaded table index from '{index_path}'") + return vector_store + + +def search_tables( + query: str, + vector_store: FAISS, + k: int = 3, +) -> list[dict]: + """ + Retrieve the top-k table descriptions most relevant to a query. + + Parameters + ---------- + query : Natural language question or search string. + vector_store : A loaded or freshly-built FAISS table index. + k : Number of results to return. + + Returns + ------- + List of dicts: + { + "description" : str — natural-language summary of the table + "table_id" : str — unique table identifier + "csv_path" : str — path to the raw CSV file + "page" : int — source page in the original document + "score" : float — FAISS L2 distance (lower = more similar) + } + """ + raw_results = vector_store.similarity_search_with_score(query, k=k) + + return [ + { + "description": doc.page_content, + "table_id": doc.metadata.get("table_id", ""), + "csv_path": doc.metadata.get("csv_path", ""), + "page": doc.metadata.get("page", 0), + "score": float(score), + } + for doc, score in raw_results + ] diff --git a/04-multimodal-rag/src/table_processor.py b/04-multimodal-rag/src/table_processor.py new file mode 100644 index 0000000..2ef01ce --- /dev/null +++ b/04-multimodal-rag/src/table_processor.py @@ -0,0 +1,162 @@ +""" +table_processor.py +------------------ +Converts extracted tables into natural-language descriptions suitable for +semantic search, while also persisting the raw data as CSV files. + +The challenge of searching tabular data semantically +----------------------------------------------------- +Tables are inherently 2-D structured objects. A flat string representation +like "Q1 | 1000000 | Q2 | 1200000" is syntactically correct but semantically +opaque to a sentence-transformer trained on prose. + +Why generate natural-language descriptions? +-------------------------------------------- +Text embedding models (and LLMs used for generation) understand sentences +like "Q1 revenue was $1 M, representing 12 % growth quarter-over-quarter" +far better than a raw CSV row. By asking an LLM to paraphrase a table, we +convert the structured 2-D data into a format that: + 1. Embeds meaningfully with all-MiniLM-L6-v2. + 2. Matches natural-language queries ("What were the Q1 sales figures?"). + 3. Can be injected verbatim into a generation prompt for the final answer. + +Why keep the raw CSV too? +-------------------------- +Natural-language descriptions are lossy — they summarise, not enumerate. +For exact queries ("What was the exact revenue in row 7, column 3?") or for +programmatic downstream use (pandas, Excel), the CSV is the ground truth. +We store both and surface whichever is appropriate. +""" + +import csv +import os +from pathlib import Path + + +def table_to_description(table: list[list], llm) -> str: + """ + Use an LLM to convert a raw table (list of rows) into a prose description. + + Parameters + ---------- + table : 2-D list where table[0] is typically the header row and + subsequent rows are data rows. Cell values are strings. + llm : A LangChain chat/LLM object that supports .invoke() or .predict(). + + Returns + ------- + A natural-language string describing the table's content and structure. + """ + if not table: + return "Empty table." + + # Format the table as a plain-text grid so the LLM can parse it easily. + table_str = _format_table_as_text(table) + + prompt = ( + "Convert this table to a natural language description for search purposes. " + "Describe what data the table contains, its structure, and key values.\n\n" + f"Table:\n{table_str}" + ) + + try: + # Support both .invoke() (LangChain ≥ 0.1) and .predict() (legacy). + if hasattr(llm, "invoke"): + response = llm.invoke(prompt) + # .invoke() may return a string or an AIMessage depending on the model. + description = response.content if hasattr(response, "content") else str(response) + else: + description = llm.predict(prompt) + return description.strip() + + except Exception as exc: + # Non-fatal fallback: return the raw text representation. + print(f" [table_processor] LLM unavailable for table description: {exc}") + return f"Table data:\n{table_str}" + + +def save_table_as_csv(table: list[list], output_path: str) -> None: + """ + Write a 2-D list to a CSV file. + + Parameters + ---------- + table : 2-D list of cell values. + output_path : Full file path for the output CSV (directory must exist). + """ + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerows(table) + + +def process_all_tables( + tables: list[dict], + llm, + tables_dir: str = "data/extracted/tables", +) -> list[dict]: + """ + Process every table extracted by the parser: save as CSV and generate a + natural-language description via the LLM. + + Parameters + ---------- + tables : List of table dicts as returned by multimodal_parser — + each has keys "rows" (list[list]) and "page" (int). + llm : LangChain LLM / chat model for description generation. + tables_dir : Directory where CSV files are written. + + Returns + ------- + List of dicts: + { + "table_id" : str — unique identifier, e.g. "table_p2_0" + "csv_path" : str — path to the saved CSV file + "description" : str — LLM-generated natural-language summary + "raw_table" : list[list] — original row data + "page" : int — source page number + } + """ + Path(tables_dir).mkdir(parents=True, exist_ok=True) + results = [] + + for idx, table_meta in enumerate(tables): + raw_rows = table_meta.get("rows", []) + page = table_meta.get("page", 0) + table_id = f"table_p{page}_{table_meta.get('table_index', idx)}" + + csv_filename = f"{table_id}.csv" + csv_path = os.path.join(tables_dir, csv_filename) + + # Persist raw data. + save_table_as_csv(raw_rows, csv_path) + + # Generate natural-language description. + print( + f" [table_processor] Describing table {idx + 1}/{len(tables)} " + f"(page {page}) …" + ) + description = table_to_description(raw_rows, llm) + + results.append( + { + "table_id": table_id, + "csv_path": csv_path, + "description": description, + "raw_table": raw_rows, + "page": page, + } + ) + + return results + + +# ── Private helpers ────────────────────────────────────────────────────────── + + +def _format_table_as_text(table: list[list]) -> str: + """Render a 2-D list as a plain-text grid with | separators.""" + lines = [] + for row in table: + lines.append(" | ".join(str(cell) for cell in row)) + return "\n".join(lines) diff --git a/04-multimodal-rag/src/text_indexer.py b/04-multimodal-rag/src/text_indexer.py new file mode 100644 index 0000000..88383e6 --- /dev/null +++ b/04-multimodal-rag/src/text_indexer.py @@ -0,0 +1,106 @@ +""" +text_indexer.py +--------------- +Builds and queries a FAISS vector index for plain-text chunks. + +Same embedding approach as Project 1 — this is the text modality index. + +We reuse the all-MiniLM-L6-v2 sentence-transformer model because: + * It is fast and runs fully locally (no API calls, no cost). + * Its 384-dimensional embeddings strike a good balance between quality + and memory / speed. + * It has proven strong retrieval performance on diverse Q&A benchmarks. + +The only difference from Project 1 is that this index is *one of three* +indexes in the multimodal pipeline. The query router decides whether to +hit this index, the image index, the table index, or all three. +""" + +from langchain_community.embeddings import HuggingFaceEmbeddings +from langchain_community.vectorstores import FAISS +from langchain.schema import Document + + +# Shared embedding model — instantiated once to avoid repeated model loading. +_EMBED_MODEL_NAME = "all-MiniLM-L6-v2" + + +def _get_embeddings() -> HuggingFaceEmbeddings: + """Return a HuggingFaceEmbeddings instance for all-MiniLM-L6-v2.""" + return HuggingFaceEmbeddings(model_name=_EMBED_MODEL_NAME) + + +def index_text_chunks( + text_blocks: list[str], + index_path: str = "text_faiss_index", +) -> FAISS: + """ + Embed a list of text strings and persist them as a FAISS index. + + Parameters + ---------- + text_blocks : Raw text strings (one per page, paragraph, or chunk). + index_path : Directory path where the FAISS index files are saved. + + Returns + ------- + A LangChain FAISS vector store ready for similarity search. + """ + if not text_blocks: + raise ValueError("text_blocks is empty — nothing to index.") + + # Wrap each string in a LangChain Document so we can store metadata. + # We record the chunk number so retrieved results can be traced back. + docs = [ + Document(page_content=block, metadata={"chunk_id": i, "modality": "text"}) + for i, block in enumerate(text_blocks) + ] + + embeddings = _get_embeddings() + + # FAISS.from_documents embeds all docs in a single batch and builds + # the index in memory, then we persist it to disk. + vector_store = FAISS.from_documents(docs, embeddings) + vector_store.save_local(index_path) + + print(f"[text_indexer] Indexed {len(docs)} text chunks → '{index_path}'") + return vector_store + + +def load_text_index(index_path: str) -> FAISS: + """ + Load a previously saved FAISS text index from disk. + + Parameters + ---------- + index_path : Directory path that was passed to index_text_chunks(). + + Returns + ------- + A LangChain FAISS vector store. + """ + embeddings = _get_embeddings() + vector_store = FAISS.load_local( + index_path, embeddings, allow_dangerous_deserialization=True + ) + print(f"[text_indexer] Loaded text index from '{index_path}'") + return vector_store + + +def search_text(query: str, vector_store: FAISS, k: int = 3) -> list: + """ + Retrieve the top-k most relevant text chunks for a query. + + Parameters + ---------- + query : Natural language question or search string. + vector_store : A loaded or freshly-built FAISS text index. + k : Number of results to return. + + Returns + ------- + List of (Document, score) tuples ordered by descending similarity. + Lower L2 distance = higher similarity in FAISS. + """ + results = vector_store.similarity_search_with_score(query, k=k) + return results From 1bf7214bb1099ab0cd6c88eb9a25235bb76d7673 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:16:39 +0000 Subject: [PATCH 6/9] Add 05-agentic-rag-realtime project Complete agentic RAG project with real-time tools: - FAISS knowledge base indexer (reused from Project 1) - Tool registry with 5 tools: RAG, finance (yfinance), Wikipedia, web search (Tavily), weather (OpenWeatherMap) - LangChain agent with OPENAI_FUNCTIONS / ReAct loop - Conversation memory (ConversationBufferWindowMemory k=5) - Response formatter with reasoning trace display - CLI with --query, --interactive, --no-memory, --verbose flags - Mock fallbacks for all optional-key tools - Comprehensive README with architecture diagram, Agentic vs Standard RAG comparison table, custom tool walkthrough, and troubleshooting guide Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- 05-agentic-rag-realtime/.env.example | 19 ++ 05-agentic-rag-realtime/README.md | 297 ++++++++++++++++++ .../data/knowledge_base/.gitkeep | 0 05-agentic-rag-realtime/main.py | 233 ++++++++++++++ 05-agentic-rag-realtime/requirements.txt | 11 + 05-agentic-rag-realtime/src/__init__.py | 2 + 05-agentic-rag-realtime/src/agent.py | 148 +++++++++ .../src/knowledge_indexer.py | 157 +++++++++ .../src/response_formatter.py | 138 ++++++++ 05-agentic-rag-realtime/src/tool_registry.py | 117 +++++++ 05-agentic-rag-realtime/src/tools/__init__.py | 2 + .../src/tools/finance_tool.py | 151 +++++++++ 05-agentic-rag-realtime/src/tools/rag_tool.py | 85 +++++ .../src/tools/weather_tool.py | 131 ++++++++ .../src/tools/web_search_tool.py | 138 ++++++++ .../src/tools/wiki_tool.py | 85 +++++ 16 files changed, 1714 insertions(+) create mode 100644 05-agentic-rag-realtime/.env.example create mode 100644 05-agentic-rag-realtime/README.md create mode 100644 05-agentic-rag-realtime/data/knowledge_base/.gitkeep create mode 100644 05-agentic-rag-realtime/main.py create mode 100644 05-agentic-rag-realtime/requirements.txt create mode 100644 05-agentic-rag-realtime/src/__init__.py create mode 100644 05-agentic-rag-realtime/src/agent.py create mode 100644 05-agentic-rag-realtime/src/knowledge_indexer.py create mode 100644 05-agentic-rag-realtime/src/response_formatter.py create mode 100644 05-agentic-rag-realtime/src/tool_registry.py create mode 100644 05-agentic-rag-realtime/src/tools/__init__.py create mode 100644 05-agentic-rag-realtime/src/tools/finance_tool.py create mode 100644 05-agentic-rag-realtime/src/tools/rag_tool.py create mode 100644 05-agentic-rag-realtime/src/tools/weather_tool.py create mode 100644 05-agentic-rag-realtime/src/tools/web_search_tool.py create mode 100644 05-agentic-rag-realtime/src/tools/wiki_tool.py diff --git a/05-agentic-rag-realtime/.env.example b/05-agentic-rag-realtime/.env.example new file mode 100644 index 0000000..1fa0393 --- /dev/null +++ b/05-agentic-rag-realtime/.env.example @@ -0,0 +1,19 @@ +# OpenAI API Key (required) +OPENAI_API_KEY=your_openai_api_key_here + +# Model to use (gpt-4 recommended for tool selection accuracy) +OPENAI_MODEL=gpt-4 + +# Web Search - Tavily API (free tier: 1000 requests/month) +# Sign up at: https://tavily.com +TAVILY_API_KEY=your_tavily_api_key_here + +# OR use SerpAPI instead +# SERPAPI_API_KEY=your_serpapi_key_here + +# OpenWeatherMap API (free tier: 60 calls/min) +# Sign up at: https://openweathermap.org/api +OPENWEATHERMAP_API_KEY=your_openweathermap_key_here + +# Knowledge base path +KNOWLEDGE_BASE_DIR=data/knowledge_base diff --git a/05-agentic-rag-realtime/README.md b/05-agentic-rag-realtime/README.md new file mode 100644 index 0000000..0f3171e --- /dev/null +++ b/05-agentic-rag-realtime/README.md @@ -0,0 +1,297 @@ +# Agentic RAG with Real-Time Tools + +A LangChain agent that decides **which tool to use** for every question — searching your internal documents, fetching live stock prices, current weather, Wikipedia articles, or the live web, depending on what the question needs. + +--- + +## Agentic RAG vs Standard RAG + +| Standard RAG | Agentic RAG | +|---|---| +| Always searches FAISS | Decides which tool(s) to use | +| One retrieval step | Multiple steps if needed | +| Only knows stored documents | Can fetch live data | +| Fast (single LLM call) | Slower (multi-step reasoning) | +| Deterministic path | Dynamic, question-driven path | + +**When to use Agentic RAG:** When users ask mixed questions that combine internal knowledge with live data (e.g. "How does today's AAPL price compare to our internal valuation model?"). + +**When to use Standard RAG:** High-volume, low-latency workloads where every question is about static documents. + +--- + +## Architecture + +``` +User Question + │ + ▼ +┌─────────────┐ +│ LLM Agent │ ← reads tool descriptions to decide what to call +└──────┬──────┘ + │ ReAct Loop: Reason → Act → Observe → Repeat + │ + ┌────┴────────────────────────────────────────┐ + │ Tool Registry │ + │ │ + │ ┌──────────────────┐ ┌─────────────────┐ │ + │ │ search_knowledge │ │ get_stock_data │ │ + │ │ _base │ │ (yfinance) │ │ + │ │ (FAISS index) │ └─────────────────┘ │ + │ └──────────────────┘ │ + │ ┌──────────────────┐ ┌─────────────────┐ │ + │ │ web_search │ │ get_weather │ │ + │ │ (Tavily API) │ │ (OpenWeather) │ │ + │ └──────────────────┘ └─────────────────┘ │ + │ ┌──────────────────┐ │ + │ │ search_wikipedia │ │ + │ │ (Wikipedia API) │ │ + │ └──────────────────┘ │ + └─────────────────────────────────────────────┘ + │ + ▼ + Final Answer + Sources +``` + +--- + +## How the Agent Decides Which Tool to Use + +The agent's LLM reads every tool's `name` and `description` string before responding. Here is the decision process for a typical question: + +**Question:** *"What is AAPL's current price and how does it compare to our internal forecast?"* + +``` +Step 1 — REASON: + "This question needs current stock data AND internal documents. + I should call get_stock_data first, then search_knowledge_base." + +Step 2 — ACT: get_stock_data("AAPL") +Step 3 — OBSERVE: "Stock: AAPL | Price: $182.50 | ..." + +Step 4 — REASON: + "Now I have the live price. I need the internal forecast from the KB." + +Step 5 — ACT: search_knowledge_base("AAPL valuation forecast") +Step 6 — OBSERVE: "Found in knowledge base: 1. Q3 forecast values AAPL at..." + +Step 7 — REASON: + "I have both pieces of information. I can now compose a full answer." + +Step 8 — FINAL ANSWER (no more tool calls needed) +``` + +The key insight: **the tool description IS the routing logic**. A clear description like *"Use this for questions about internal policies"* routes the agent correctly without any if/else code. + +--- + +## Setup + +### 1. Clone and install + +```bash +cd 05-agentic-rag-realtime +pip install -r requirements.txt +``` + +### 2. Configure API keys + +```bash +cp .env.example .env +# Edit .env with your keys +``` + +### API Key Guide + +| Service | Required? | Free Tier | Sign-up Link | +|---|---|---|---| +| **OpenAI** | ✅ Yes | Pay-per-use | [platform.openai.com](https://platform.openai.com) | +| **Tavily** (web search) | ❌ Optional | 1,000 searches/month | [tavily.com](https://tavily.com) | +| **OpenWeatherMap** | ❌ Optional | 60 calls/min | [openweathermap.org/api](https://openweathermap.org/api) | +| **yfinance** (finance) | ✅ Built-in | Unlimited* | No key needed | +| **Wikipedia** | ✅ Built-in | Unlimited | No key needed | + +*yfinance scrapes Yahoo Finance; data may be delayed 15 minutes. + +### 3. Add documents to the knowledge base (optional) + +```bash +# Drop .pdf or .txt files here: +data/knowledge_base/ +``` + +### 4. Run + +```bash +# Single query +python main.py --query "What is AAPL's current stock price?" + +# Interactive session +python main.py --interactive + +# Without conversation memory +python main.py --interactive --no-memory + +# Hide the reasoning trace +python main.py --query "Weather in Tokyo" --no-verbose +``` + +--- + +## Example Multi-Tool Queries + +### Finance + RAG +**Query:** *"What is AAPL price and how does it compare to our internal valuation?"* + +``` +Agent calls: get_stock_data("AAPL") → search_knowledge_base("AAPL valuation") +``` + +### Weather + RAG +**Query:** *"What's the weather in London and should we proceed per our event guidelines?"* + +``` +Agent calls: get_weather("London") → search_knowledge_base("event guidelines weather policy") +``` + +### Web Search + RAG +**Query:** *"What are latest AI news stories relevant to our strategy?"* + +``` +Agent calls: web_search("latest AI news 2024") → search_knowledge_base("AI strategy") +``` + +### Wikipedia + Finance +**Query:** *"What does Wikipedia say about transformer models and how is NVDA performing?"* + +``` +Agent calls: search_wikipedia("transformer neural network") → get_stock_data("NVDA") +``` + +--- + +## Cost and Rate Limits + +| Tool | Cost | Rate Limit | +|---|---|---| +| `search_knowledge_base` | Free (local FAISS) | Unlimited | +| `get_stock_data` | Free (yfinance) | ~2,000 req/hour* | +| `search_wikipedia` | Free | Unlimited | +| `web_search` | Free tier: 1,000/month | 1 req/sec | +| `get_weather` | Free tier: 60 calls/min | 1,000,000/month | +| OpenAI GPT-4 | ~$0.03/1K tokens | Depends on tier | +| OpenAI GPT-3.5 | ~$0.002/1K tokens | Depends on tier | + +*Yahoo Finance has unofficial rate limits; excessive calls may trigger temporary blocks. + +--- + +## How to Add a Custom Tool + +Adding a new tool requires three steps: write the function, wrap it in a `Tool`, and register it. + +### Step 1 — Write the function + +Create `src/tools/my_tool.py`: + +```python +from langchain.tools import Tool + +def my_custom_function(input_str: str) -> str: + # Your logic here — always string in, string out + return "Result: ..." + +def create_my_tool() -> Tool: + return Tool( + name="my_tool_name", + func=my_custom_function, + description=( + "What this tool does and when to use it. " + "Input: what to provide (be specific about format)." + ), + ) +``` + +**Rules for a good tool:** +- Function signature: always `(input_str: str) -> str` +- Never raise exceptions — catch errors and return a message string +- Description must say WHAT the tool does, WHEN to use it, and WHAT input format it expects + +### Step 2 — Register it in `tool_registry.py` + +```python +from src.tools.my_tool import create_my_tool + +def build_tool_registry(vector_store, config): + tools = [...] # existing tools + tools.append(create_my_tool()) + return tools +``` + +### Step 3 — Test it + +```bash +python main.py --query "A question that should trigger your new tool" +``` + +With `--verbose` (default) you'll see whether the agent picked your tool and what it returned. + +--- + +## Troubleshooting + +| Problem | Solution | +|---|---| +| `OPENAI_API_KEY not set` | Add key to `.env` file | +| Agent always uses `search_knowledge_base` | Knowledge base is empty — agent defaults to it. Add docs to `data/knowledge_base/` | +| `yfinance` returns `None` for price | Market may be closed; try a major ticker like AAPL | +| Web search returns mock message | Add `TAVILY_API_KEY` to `.env` | +| Weather returns mock data | Add `OPENWEATHERMAP_API_KEY` to `.env` | +| Agent uses wrong tool | Check tool descriptions in `src/tools/` — make them more specific | +| `FAISS` index error on reload | Delete `data/knowledge_base/.faiss_index/` and re-run | +| Agent loops > 8 times | Increase `max_iterations` in `src/agent.py` or simplify your query | +| `sentence-transformers` slow on first run | It downloads the model (~80 MB) once; subsequent runs are fast | + +### Changing the LLM + +```bash +# Use GPT-3.5 instead of GPT-4 (cheaper, slightly less accurate tool selection) +python main.py --model gpt-3.5-turbo --interactive +``` + +### Viewing the reasoning trace + +The `--verbose` flag (on by default) prints every Thought → Action → Observation cycle. This is the best way to debug unexpected answers: + +``` +Thought: I need current stock data for AAPL. +Action: get_stock_data +Action Input: AAPL +Observation: Stock: AAPL | Price: $182.50 | ... +Thought: I now have the price. Let me check the knowledge base for the internal valuation. +... +``` + +--- + +## Project Structure + +``` +05-agentic-rag-realtime/ +├── main.py # Entry point — pipeline + CLI +├── requirements.txt +├── .env.example # Copy to .env and fill in keys +├── data/ +│ └── knowledge_base/ # Drop .pdf and .txt files here +├── src/ +│ ├── knowledge_indexer.py # FAISS index builder (reused from Project 1) +│ ├── tool_registry.py # Assembles all tools into a list +│ ├── agent.py # LangChain agent with ReAct loop +│ ├── response_formatter.py # Formats output with sources and trace +│ └── tools/ +│ ├── rag_tool.py # Wraps FAISS search as a Tool +│ ├── finance_tool.py # yfinance stock data +│ ├── weather_tool.py # OpenWeatherMap current weather +│ ├── web_search_tool.py # Tavily live web search +│ └── wiki_tool.py # Wikipedia summaries +``` diff --git a/05-agentic-rag-realtime/data/knowledge_base/.gitkeep b/05-agentic-rag-realtime/data/knowledge_base/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/05-agentic-rag-realtime/main.py b/05-agentic-rag-realtime/main.py new file mode 100644 index 0000000..ba2c4af --- /dev/null +++ b/05-agentic-rag-realtime/main.py @@ -0,0 +1,233 @@ +""" +main.py — Agentic RAG with Real-Time Tools + +Entry point for the 05-agentic-rag-realtime project. Assembles the full +pipeline: knowledge base indexing → tool registry → agent → interactive Q&A. + +Usage examples: + # Single query + python main.py --query "What is AAPL's current stock price?" + + # Interactive multi-turn session + python main.py --interactive + + # Use a specific knowledge base directory + python main.py --kb-dir /path/to/docs --interactive + + # Disable conversation memory (stateless mode) + python main.py --interactive --no-memory + + # Hide the agent's reasoning trace + python main.py --query "Weather in Tokyo" --no-verbose +""" + +import argparse +import os +import sys + +from dotenv import load_dotenv + +# Load .env before importing project modules (they may read env vars at import time). +load_dotenv() + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _check_api_keys(config: dict) -> None: + """ + Print a startup banner showing which tools are ready / missing API keys. + This helps users quickly see what's available before running queries. + """ + openai_key = config.get("openai_api_key") + tavily_key = config.get("tavily_api_key") + owm_key = config.get("openweathermap_api_key") + + rag_status = "✅ RAG Tool ready" + finance_status = "✅ Finance Tool ready (yfinance — no key needed)" + wiki_status = "✅ Wikipedia Tool ready (no key needed)" + web_status = "✅ Web Search ready" if tavily_key else "❌ Web Search (no TAVILY_API_KEY)" + weather_status = "✅ Weather Tool ready" if owm_key else "⚠️ Weather Tool (mock mode — no OPENWEATHERMAP_API_KEY)" + openai_status = "✅ OpenAI connected" if openai_key else "❌ OpenAI (no OPENAI_API_KEY — required)" + + print("\n" + "=" * 60) + print(" Agentic RAG — Tool Availability") + print("=" * 60) + for status in [openai_status, rag_status, finance_status, wiki_status, web_status, weather_status]: + print(f" {status}") + print("=" * 60) + + if not openai_key: + print("\n[ERROR] OPENAI_API_KEY is required. Add it to your .env file.") + sys.exit(1) + + +def _print_example_queries() -> None: + """Print suggested example queries so new users know what to try.""" + print("\nExample queries:") + print(' • "What is the current price of AAPL?"') + print(' • "What\'s the weather in London today?"') + print(' • "Search Wikipedia for transformer neural networks"') + print(' • "What does our internal strategy document say about AI adoption?"') + print(' • "What is AAPL price and how does it compare to our internal valuation?"') + print(' • "What are latest AI news stories relevant to our strategy?"') + print() + + +def _build_config() -> dict: + """Read all configuration from environment variables and return as a dict.""" + return { + "openai_api_key": os.getenv("OPENAI_API_KEY", ""), + "openai_model": os.getenv("OPENAI_MODEL", "gpt-4"), + "tavily_api_key": os.getenv("TAVILY_API_KEY", ""), + "openweathermap_api_key": os.getenv("OPENWEATHERMAP_API_KEY", ""), + "domain_description": "internal company documents and knowledge base", + } + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Agentic RAG with real-time tools (finance, weather, web search, Wikipedia).", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "--kb-dir", + default=os.getenv("KNOWLEDGE_BASE_DIR", "data/knowledge_base"), + help="Directory containing .pdf and .txt files to index (default: data/knowledge_base)", + ) + parser.add_argument( + "--model", + default=None, + help="OpenAI model name to use (overrides OPENAI_MODEL env var, default: gpt-4)", + ) + parser.add_argument( + "--query", + default=None, + help="Run a single query and exit.", + ) + parser.add_argument( + "--interactive", + action="store_true", + help="Start an interactive multi-turn Q&A session.", + ) + parser.add_argument( + "--no-memory", + action="store_true", + help="Disable conversation memory (each query is independent).", + ) + verbose_group = parser.add_mutually_exclusive_group() + verbose_group.add_argument( + "--verbose", + dest="verbose", + action="store_true", + default=True, + help="Show the agent's reasoning trace (default: on).", + ) + verbose_group.add_argument( + "--no-verbose", + dest="verbose", + action="store_false", + help="Hide the agent's reasoning trace.", + ) + return parser.parse_args() + + +# --------------------------------------------------------------------------- +# Main pipeline +# --------------------------------------------------------------------------- + +def main() -> None: + args = _parse_args() + config = _build_config() + + # Allow --model to override the environment variable. + if args.model: + config["openai_model"] = args.model + + # --- Print startup banner --- + _check_api_keys(config) + + # --- Step 1: Index / load the knowledge base --- + print(f"\n[Setup] Indexing knowledge base from '{args.kb_dir}' …") + from src.knowledge_indexer import index_knowledge_base # noqa: PLC0415 + vector_store = index_knowledge_base( + kb_dir=args.kb_dir, + index_path=os.path.join(args.kb_dir, ".faiss_index"), + ) + + # --- Step 2: Build tool registry --- + print("[Setup] Building tool registry …") + from src.tool_registry import build_tool_registry, get_tool_descriptions # noqa: PLC0415 + tools = build_tool_registry(vector_store, config) + print(get_tool_descriptions(tools)) + + # --- Step 3: Instantiate the LLM --- + print(f"\n[Setup] Connecting to OpenAI model '{config['openai_model']}' …") + from langchain_openai import ChatOpenAI # noqa: PLC0415 + llm = ChatOpenAI( + model=config["openai_model"], + openai_api_key=config["openai_api_key"], + temperature=0, # deterministic tool selection + ) + + # --- Step 4: Create agent --- + use_memory = not args.no_memory + print(f"[Setup] Creating agent (memory={'on' if use_memory else 'off'}, verbose={args.verbose}) …") + from src.agent import create_agent, run_agent_query # noqa: PLC0415 + agent = create_agent(tools, llm, memory=use_memory, verbose=args.verbose) + + # --- Step 5: Run query/interactive loop --- + from src.response_formatter import ( # noqa: PLC0415 + format_response, + extract_tools_from_steps, + ) + + def _run_and_display(query: str) -> None: + """Run a single query and print the formatted response.""" + print(f"\n[Query] {query}\n") + try: + result = agent.invoke({"input": query}) + answer = result.get("output", str(result)) + steps = result.get("intermediate_steps", []) + tools_used = extract_tools_from_steps(steps) + except Exception as exc: + answer = f"Agent encountered an error: {exc}" + tools_used = [] + + print("\n" + format_response(answer, tools_used)) + + if args.query: + # Single-shot mode: run one query and exit. + _run_and_display(args.query) + + elif args.interactive: + # Interactive mode: loop until user types "quit" or "exit". + _print_example_queries() + print("Type 'quit' or 'exit' to end the session.\n") + + while True: + try: + user_input = input("You: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nGoodbye!") + break + + if not user_input: + continue + if user_input.lower() in {"quit", "exit", "q"}: + print("Goodbye!") + break + + _run_and_display(user_input) + + else: + # No mode selected — show help and example queries. + print("\nNo query mode selected. Use --query or --interactive.") + _print_example_queries() + print("Run with --help for all options.") + + +if __name__ == "__main__": + main() diff --git a/05-agentic-rag-realtime/requirements.txt b/05-agentic-rag-realtime/requirements.txt new file mode 100644 index 0000000..08694ad --- /dev/null +++ b/05-agentic-rag-realtime/requirements.txt @@ -0,0 +1,11 @@ +langchain==0.1.20 +langchain-community==0.0.38 +langchain-openai==0.1.6 +faiss-cpu==1.8.0 +sentence-transformers==2.7.0 +openai==1.30.1 +python-dotenv==1.0.1 +yfinance==0.2.38 +wikipedia==1.4.0 +requests==2.31.0 +tavily-python==0.3.3 diff --git a/05-agentic-rag-realtime/src/__init__.py b/05-agentic-rag-realtime/src/__init__.py new file mode 100644 index 0000000..6b4afd1 --- /dev/null +++ b/05-agentic-rag-realtime/src/__init__.py @@ -0,0 +1,2 @@ +# src/__init__.py +# Makes src/ a Python package so imports like `from src.agent import create_agent` work. diff --git a/05-agentic-rag-realtime/src/agent.py b/05-agentic-rag-realtime/src/agent.py new file mode 100644 index 0000000..dc511a9 --- /dev/null +++ b/05-agentic-rag-realtime/src/agent.py @@ -0,0 +1,148 @@ +""" +src/agent.py + +Assembles the LangChain agent executor that ties together the LLM, all tools, +and optional conversation memory. + +THE ReAct LOOP (Reason + Act): + Every time the agent receives a question it goes through repeated cycles: + 1. REASON — "What do I need to answer this? Which tool should I call?" + 2. ACT — Calls a tool with a specific input string. + 3. OBSERVE — Reads the tool's output. + 4. REPEAT — Reasons again with the new information; stops when confident. + + This is fundamentally different from standard RAG which does a single + FAISS search every time regardless of the question type. + +AGENT TYPES: + • OPENAI_FUNCTIONS (default when using GPT-3.5 / GPT-4): + Uses OpenAI's native function-calling API. The LLM is trained to emit + structured JSON for function calls, so tool invocation is very reliable. + Requires an OpenAI model that supports function calling. + + • ZERO_SHOT_REACT_DESCRIPTION (fallback): + Works with ANY LLM (Llama, Mistral, Claude, etc.). + The LLM reasons in plain text using a "Thought/Action/Observation" format. + Less reliable for tool selection but model-agnostic. + +MEMORY: + ConversationBufferWindowMemory(k=5) keeps the last 5 exchanges in context. + k=5 is a pragmatic choice: + • Enough to handle follow-up questions ("And what about MSFT?") + • Small enough not to overflow the context window on long conversations + Disable memory (--no-memory) for stateless single-query use cases. + +VERBOSE MODE: + verbose=True is essential for learning: you see every Thought → Action → + Observation cycle printed to stdout. In production set verbose=False. +""" + +from typing import List, Optional + +from langchain.agents import AgentExecutor, initialize_agent, AgentType +from langchain.memory import ConversationBufferWindowMemory +from langchain.schema import SystemMessage +from langchain.tools import Tool + + +# System prompt injected before every conversation. +# Specific instructions improve tool selection accuracy significantly. +_SYSTEM_PROMPT = """You are a knowledgeable assistant with access to multiple tools. +You can search internal documents, look up live data, and search the web. + +When answering: +1. First consider if you need real-time data (use web_search or get_stock_data) +2. Or if the question is about internal documents (use search_knowledge_base) +3. Or both (use multiple tools) + +Always cite which tools you used and where information came from. +Think step by step before deciding which tools to use.""" + + +def create_agent( + tools: List[Tool], + llm, + memory: bool = True, + verbose: bool = True, +) -> AgentExecutor: + """ + Build and return a LangChain AgentExecutor wired to the provided tools. + + Args: + tools: List of LangChain Tool objects from tool_registry. + llm: An instantiated LangChain LLM (e.g. ChatOpenAI). + memory: If True, adds a sliding-window conversation memory (k=5). + verbose: If True, prints the full reasoning trace to stdout. + + Returns: + A configured AgentExecutor ready to accept queries. + """ + # --- Memory --- + # ConversationBufferWindowMemory keeps only the last k exchanges so the + # context window doesn't grow unboundedly during long conversations. + mem: Optional[ConversationBufferWindowMemory] = None + if memory: + mem = ConversationBufferWindowMemory( + k=5, + memory_key="chat_history", + return_messages=True, + ) + + # --- Determine the best agent type --- + # OPENAI_FUNCTIONS is more reliable for tool selection because it uses + # OpenAI's native function-calling format instead of text-based reasoning. + # We detect whether we're talking to an OpenAI chat model by checking the + # class name — this avoids a hard dependency on langchain_openai at this level. + llm_class = type(llm).__name__ + is_openai_chat = "ChatOpenAI" in llm_class or "AzureChatOpenAI" in llm_class + + if is_openai_chat: + agent_type = AgentType.OPENAI_FUNCTIONS + # Inject the system message through agent_kwargs for OPENAI_FUNCTIONS agents. + agent_kwargs = { + "system_message": SystemMessage(content=_SYSTEM_PROMPT), + } + if mem: + agent_kwargs["extra_prompt_messages"] = [] # memory messages prepended automatically + else: + # ZERO_SHOT_REACT_DESCRIPTION works with any LLM via plain-text reasoning. + agent_type = AgentType.ZERO_SHOT_REACT_DESCRIPTION + agent_kwargs = {} + + agent_executor = initialize_agent( + tools=tools, + llm=llm, + agent=agent_type, + memory=mem, + agent_kwargs=agent_kwargs, + verbose=verbose, + # handle_parsing_errors=True prevents the agent from crashing when the + # LLM produces a malformed tool call; it retries with an error message. + handle_parsing_errors=True, + # max_iterations caps runaway loops — agent stops after N tool calls. + max_iterations=8, + ) + + return agent_executor + + +def run_agent_query(query: str, agent: AgentExecutor) -> str: + """ + Submit a query to the agent and return the final answer string. + + Wraps the AgentExecutor.invoke() call with error handling so the main + loop doesn't crash on unexpected LLM failures. + + Args: + query: The user's natural-language question. + agent: A configured AgentExecutor from create_agent(). + + Returns: + The agent's final answer as a plain string. + """ + try: + result = agent.invoke({"input": query}) + # AgentExecutor returns a dict; the final answer is under "output". + return result.get("output", str(result)) + except Exception as exc: + return f"Agent encountered an error: {exc}" diff --git a/05-agentic-rag-realtime/src/knowledge_indexer.py b/05-agentic-rag-realtime/src/knowledge_indexer.py new file mode 100644 index 0000000..805200e --- /dev/null +++ b/05-agentic-rag-realtime/src/knowledge_indexer.py @@ -0,0 +1,157 @@ +""" +src/knowledge_indexer.py + +This is the same RAG indexing pattern from Project 1, reused here as one of the +agent's tools. The key difference: in Project 1 this was the ONLY retrieval path; +here it is just one tool the agent may or may not call depending on the question. +""" + +import os +from typing import List + +from langchain_community.vectorstores import FAISS +from langchain_community.embeddings import HuggingFaceEmbeddings +from langchain_community.document_loaders import ( + PyPDFLoader, + TextLoader, + DirectoryLoader, +) +from langchain.text_splitter import RecursiveCharacterTextSplitter + + +# --------------------------------------------------------------------------- +# Embedding model — same lightweight model used in Project 1. +# Using a local sentence-transformers model avoids OpenAI embedding API calls +# and keeps costs at zero for the indexing step. +# --------------------------------------------------------------------------- +EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2" + + +def _get_embeddings() -> HuggingFaceEmbeddings: + """Return a cached HuggingFace embedding model instance.""" + return HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL_NAME) + + +def index_knowledge_base( + kb_dir: str, + index_path: str = "kb_faiss_index", +) -> FAISS: + """ + Load every .pdf and .txt file from kb_dir, chunk the text, build a FAISS + vector index, and persist it to disk. + + If the index already exists on disk it is loaded directly — re-indexing is + skipped so the agent starts up fast after the first run. + + Args: + kb_dir: Directory containing source documents (.pdf / .txt). + index_path: Path where the FAISS index folder will be saved. + + Returns: + A ready-to-query FAISS vector store. + """ + # If the index was already built, load it and return immediately. + if os.path.exists(index_path): + print(f"[Indexer] Loading existing FAISS index from '{index_path}'") + return load_index(index_path) + + print(f"[Indexer] Building new FAISS index from '{kb_dir}'") + + # --- Step 1: Load documents using two loaders — one for PDFs, one for TXTs --- + documents = [] + + pdf_loader = DirectoryLoader( + kb_dir, + glob="**/*.pdf", + loader_cls=PyPDFLoader, + silent_errors=True, + ) + txt_loader = DirectoryLoader( + kb_dir, + glob="**/*.txt", + loader_cls=TextLoader, + loader_kwargs={"encoding": "utf-8"}, + silent_errors=True, + ) + + for loader in (pdf_loader, txt_loader): + try: + docs = loader.load() + documents.extend(docs) + print(f"[Indexer] Loaded {len(docs)} pages/docs via {loader.__class__.__name__}") + except Exception as exc: + print(f"[Indexer] Warning: loader {loader.__class__.__name__} failed — {exc}") + + if not documents: + print(f"[Indexer] No documents found in '{kb_dir}'. Index will be empty.") + # Create a minimal placeholder doc so FAISS doesn't crash on an empty list. + from langchain.schema import Document + documents = [ + Document( + page_content="Knowledge base is empty. Add .pdf or .txt files to the knowledge_base/ folder.", + metadata={"source": "placeholder"}, + ) + ] + + # --- Step 2: Split documents into overlapping chunks --- + # chunk_size=500 tokens keeps chunks small enough for a single context window slot. + # chunk_overlap=50 ensures sentences cut at a boundary don't lose context. + splitter = RecursiveCharacterTextSplitter( + chunk_size=500, + chunk_overlap=50, + ) + chunks = splitter.split_documents(documents) + print(f"[Indexer] Split into {len(chunks)} chunks") + + # --- Step 3: Embed and index --- + embeddings = _get_embeddings() + vector_store = FAISS.from_documents(chunks, embeddings) + + # --- Step 4: Persist to disk --- + vector_store.save_local(index_path) + print(f"[Indexer] FAISS index saved to '{index_path}'") + + return vector_store + + +def load_index(index_path: str) -> FAISS: + """ + Load a previously saved FAISS index from disk. + + Args: + index_path: Path to the directory created by FAISS.save_local(). + + Returns: + Loaded FAISS vector store ready for similarity search. + """ + embeddings = _get_embeddings() + vector_store = FAISS.load_local( + index_path, + embeddings, + allow_dangerous_deserialization=True, # required since LangChain 0.1.x + ) + print(f"[Indexer] Loaded FAISS index from '{index_path}'") + return vector_store + + +def search_knowledge_base( + query: str, + vector_store: FAISS, + k: int = 3, +) -> List[str]: + """ + Perform a similarity search and return the top-k text chunks. + + The agent calls this via rag_tool.py. Returning plain strings (not Documents) + keeps the tool output easy to format and display. + + Args: + query: The natural-language search query. + vector_store: A loaded FAISS index. + k: Number of top results to return. + + Returns: + List of text strings — the retrieved chunks, most relevant first. + """ + docs = vector_store.similarity_search(query, k=k) + return [doc.page_content for doc in docs] diff --git a/05-agentic-rag-realtime/src/response_formatter.py b/05-agentic-rag-realtime/src/response_formatter.py new file mode 100644 index 0000000..219e8df --- /dev/null +++ b/05-agentic-rag-realtime/src/response_formatter.py @@ -0,0 +1,138 @@ +""" +src/response_formatter.py + +Formats the agent's raw output into a structured, readable display. + +WHY FORMAT RESPONSES? + • Transparency: users should know whether an answer came from live data + (could change in minutes) or stored documents (could be months old). + • Trust: showing which tools were used lets users verify accuracy. + • Debuggability: the reasoning trace (Thought → Action → Observation) is + an audit trail that reveals HOW the agent reached its conclusion. + +The box-drawing characters used here render well in any Unicode terminal. +""" + +from typing import List, Optional + + +# Width of the output box in characters. +_BOX_WIDTH = 56 + + +def format_response( + answer: str, + tools_used: List[str], + agent_steps: Optional[list] = None, +) -> str: + """ + Render the agent's answer inside a bordered box with a tools-used footer. + + Args: + answer: The final answer string from the agent. + tools_used: List of tool names that were called (e.g. ["get_stock_data"]). + agent_steps: Optional raw intermediate steps from AgentExecutor for the + full reasoning trace footer. + + Returns: + A multi-line formatted string ready to print to stdout. + """ + lines: List[str] = [] + + # ── Answer box ──────────────────────────────────────────────────────────── + lines.append("╔" + "═" * _BOX_WIDTH + "╗") + lines.append("║ ANSWER" + " " * (_BOX_WIDTH - 7) + "║") + lines.append("╚" + "═" * _BOX_WIDTH + "╝") + lines.append(answer) + lines.append("") + + # ── Tools / sources footer ───────────────────────────────────────────── + tools_str = ", ".join(tools_used) if tools_used else "none" + lines.append("┌" + "─" * _BOX_WIDTH + "┐") + + # Truncate tool list if it overflows the box width. + tools_line = f" Tools Used: {tools_str}" + if len(tools_line) > _BOX_WIDTH - 1: + tools_line = tools_line[: _BOX_WIDTH - 4] + "…" + lines.append("│" + tools_line.ljust(_BOX_WIDTH) + "│") + lines.append("└" + "─" * _BOX_WIDTH + "┘") + + return "\n".join(lines) + + +def extract_tools_from_steps(agent_steps: list) -> List[str]: + """ + Parse LangChain AgentExecutor intermediate steps to extract tool names. + + LangChain returns intermediate_steps as a list of (AgentAction, observation) + tuples. Each AgentAction has a `tool` attribute with the tool's name. + + Args: + agent_steps: The `intermediate_steps` value from AgentExecutor output. + + Returns: + Deduplicated list of tool names that were called, in call order. + """ + seen = set() + tools: List[str] = [] + + for step in agent_steps or []: + try: + # Each step is a (AgentAction, str) tuple. + action = step[0] + tool_name = getattr(action, "tool", None) + if tool_name and tool_name not in seen: + tools.append(tool_name) + seen.add(tool_name) + except (IndexError, TypeError): + # Malformed step — skip silently. + continue + + return tools + + +def format_agent_trace(agent_steps: list) -> str: + """ + Render the agent's full Thought → Action → Observation trace as text. + + This is the "reasoning audit trail": every decision the agent made is + visible here. Useful for debugging unexpected answers and for teaching + users how the agent works. + + Args: + agent_steps: The `intermediate_steps` list from AgentExecutor output. + + Returns: + A formatted multi-line string showing each reasoning step. + """ + if not agent_steps: + return "(No intermediate steps recorded)" + + lines: List[str] = ["── Agent Reasoning Trace ──"] + + for i, step in enumerate(agent_steps, start=1): + try: + action, observation = step[0], step[1] + tool_name = getattr(action, "tool", "unknown_tool") + tool_input = getattr(action, "tool_input", "") + log = getattr(action, "log", "").strip() + + lines.append(f"\nStep {i}:") + + # The `log` field contains the model's "Thought:" text for ReAct agents. + if log: + # Show only the Thought portion (first line) to keep it concise. + thought_line = log.split("\n")[0] + lines.append(f" Thought : {thought_line}") + + lines.append(f" Action : {tool_name}({tool_input!r})") + # Truncate very long observations for readability. + obs_str = str(observation) + if len(obs_str) > 300: + obs_str = obs_str[:300] + "…" + lines.append(f" Observation: {obs_str}") + + except Exception: + lines.append(f"\nStep {i}: (could not parse step)") + + return "\n".join(lines) diff --git a/05-agentic-rag-realtime/src/tool_registry.py b/05-agentic-rag-realtime/src/tool_registry.py new file mode 100644 index 0000000..2ef8a22 --- /dev/null +++ b/05-agentic-rag-realtime/src/tool_registry.py @@ -0,0 +1,117 @@ +""" +src/tool_registry.py + +Central registry that assembles all agent tools from their factory functions. + +WHY A REGISTRY? + The agent's LLM reads every tool's `name` and `description` when deciding + what to call. By building all tools in one place we can: + • Conditionally include/exclude tools based on available API keys. + • Replace real tools with mock/disabled stubs without touching agent.py. + • Easily add new tools in one location. + +GOOD TOOL DESCRIPTIONS: + Think of tool descriptions like function docstrings aimed at another LLM. + They should answer: + 1. WHAT the tool does. + 2. WHEN to use it (vs. other tools). + 3. WHAT INPUT FORMAT to provide. + Vague descriptions → the agent picks the wrong tool. + Overlapping descriptions → the agent gets confused about which to pick. + +NOTE: + The agent can ONLY use tools in this list — it cannot make up tools or call + functions not registered here. If a capability isn't listed, the agent will + either say it can't help or try to approximate with another tool. +""" + +from typing import List + +from langchain.tools import Tool +from langchain_community.vectorstores import FAISS + +from src.tools.rag_tool import create_rag_tool +from src.tools.web_search_tool import create_web_search_tool, create_mock_web_search_tool +from src.tools.finance_tool import create_finance_tool +from src.tools.weather_tool import create_weather_tool, create_mock_weather_tool +from src.tools.wiki_tool import create_wiki_tool + + +def build_tool_registry( + vector_store: FAISS, + config: dict, +) -> List[Tool]: + """ + Instantiate and return the full list of tools available to the agent. + + Each tool factory either creates a real (API-backed) tool or a mock/disabled + stub, depending on whether the relevant API key is present in `config`. + + Args: + vector_store: Loaded FAISS index for the RAG tool. + config: Dictionary with optional keys: + - tavily_api_key (str | None) + - openweathermap_api_key (str | None) + - domain_description (str) — what the KB contains + + Returns: + List of LangChain Tool objects, ordered roughly by expected call frequency. + """ + tools: List[Tool] = [] + + # --- 1. RAG / Knowledge Base Tool --- + # Always available — uses the local FAISS index, no external API needed. + domain = config.get("domain_description", "internal company documents") + rag = create_rag_tool(vector_store, domain_description=domain) + tools.append(rag) + + # --- 2. Finance Tool --- + # yfinance scrapes Yahoo Finance; no API key required. + finance = create_finance_tool() + tools.append(finance) + + # --- 3. Wikipedia Tool --- + # Free, no API key required. Useful for factual/encyclopaedic queries. + wiki = create_wiki_tool() + tools.append(wiki) + + # --- 4. Web Search Tool --- + # Requires a Tavily API key. Falls back to a disabled mock if not provided. + tavily_key = config.get("tavily_api_key") + if tavily_key: + web = create_web_search_tool(tavily_key) + else: + web = create_mock_web_search_tool() + tools.append(web) + + # --- 5. Weather Tool --- + # Requires an OpenWeatherMap API key. Falls back to mock data if not set. + owm_key = config.get("openweathermap_api_key") + if owm_key: + weather = create_weather_tool(owm_key) + else: + weather = create_mock_weather_tool() + tools.append(weather) + + return tools + + +def get_tool_descriptions(tools: List[Tool]) -> str: + """ + Return a formatted string listing every tool's name and description. + + Useful for displaying the agent's capabilities at startup or for debugging + which tools are available in the current session. + + Args: + tools: List of LangChain Tool objects from build_tool_registry(). + + Returns: + Multi-line string, one tool per line. + """ + lines = ["Available tools:"] + for tool in tools: + # Trim the description to one sentence for the summary display. + first_sentence = tool.description.split(".")[0] + "." + lines.append(f" • {tool.name}: {first_sentence}") + return "\n".join(lines) diff --git a/05-agentic-rag-realtime/src/tools/__init__.py b/05-agentic-rag-realtime/src/tools/__init__.py new file mode 100644 index 0000000..88af137 --- /dev/null +++ b/05-agentic-rag-realtime/src/tools/__init__.py @@ -0,0 +1,2 @@ +# src/tools/__init__.py +# Makes tools/ a sub-package. Individual tool modules are imported explicitly in tool_registry.py. diff --git a/05-agentic-rag-realtime/src/tools/finance_tool.py b/05-agentic-rag-realtime/src/tools/finance_tool.py new file mode 100644 index 0000000..e9f3b35 --- /dev/null +++ b/05-agentic-rag-realtime/src/tools/finance_tool.py @@ -0,0 +1,151 @@ +""" +src/tools/finance_tool.py + +Fetches live stock and financial data via yfinance (Yahoo Finance). + +This is a great example tool because it has: + • A concrete, unambiguous input format: the ticker symbol (AAPL, MSFT, …) + • A concrete, structured output: price, range, P/E, market cap + • No API key required — yfinance scrapes Yahoo Finance directly + +LIMITATIONS: + • Data may be delayed up to 15 minutes (Yahoo Finance's standard delay). + • Market cap and P/E are sourced from Yahoo Finance's "info" dict, which + can occasionally be None for smaller or recently-listed companies. + • For company-name inputs (e.g. "Apple") we do a best-effort ticker lookup + using yfinance's search; this may not always resolve correctly. +""" + +from langchain.tools import Tool + + +# Common company-name → ticker fallback mapping for the most-searched names. +# yfinance doesn't have a built-in name→ticker resolver so we keep a small +# local table for robustness. Users who pass valid tickers bypass this table. +_NAME_TO_TICKER = { + "apple": "AAPL", + "microsoft": "MSFT", + "google": "GOOGL", + "alphabet": "GOOGL", + "amazon": "AMZN", + "meta": "META", + "facebook": "META", + "tesla": "TSLA", + "nvidia": "NVDA", + "netflix": "NFLX", + "berkshire": "BRK-B", + "visa": "V", + "jpmorgan": "JPM", + "walmart": "WMT", +} + + +def _resolve_ticker(input_str: str) -> str: + """ + Best-effort conversion of a user's input to a valid ticker symbol. + + Priority: + 1. If the input looks like a ticker (short, uppercase, no spaces) use it. + 2. Check the local name→ticker mapping. + 3. Fall back to the input as-is (yfinance will fail if it's wrong). + """ + stripped = input_str.strip() + + # Heuristic: tickers are 1–5 uppercase letters (optionally with a dot/dash) + if len(stripped) <= 6 and stripped.replace("-", "").replace(".", "").isalpha(): + return stripped.upper() + + # Check local lookup table (case-insensitive) + lower = stripped.lower() + for name, ticker in _NAME_TO_TICKER.items(): + if name in lower: + return ticker + + # Last resort: return input uppercased and hope it's a valid ticker + return stripped.upper() + + +def create_finance_tool() -> Tool: + """ + Build a LangChain Tool that returns live stock data from Yahoo Finance. + + No API key is required. yfinance handles all HTTP communication internally. + + Returns: + A configured LangChain Tool for stock data lookups. + """ + + def get_stock_data(input_str: str) -> str: + """ + Fetch key financial metrics for a given ticker or company name. + + The agent passes the ticker or company name as a plain string. + We return a single formatted line so the agent can include it verbatim + in its response without further parsing. + """ + try: + import yfinance as yf # noqa: PLC0415 + + ticker_symbol = _resolve_ticker(input_str) + ticker = yf.Ticker(ticker_symbol) + + # fast_info is lighter-weight than the full .info dict and avoids + # some rate-limiting issues, but has fewer fields. + fast = ticker.fast_info + info = ticker.info # full metadata dict — may be slow on first call + + # Safely extract values; Yahoo Finance sometimes returns None. + price = fast.last_price + if price is None: + return ( + f"Could not find stock data for '{input_str}'. " + "Please provide a valid ticker symbol." + ) + + high_52w = fast.year_high + low_52w = fast.year_low + market_cap = fast.market_cap + pe_ratio = info.get("trailingPE") + + # --- Format market cap as a human-readable string --- + def _fmt_cap(cap) -> str: + if cap is None: + return "N/A" + if cap >= 1e12: + return f"${cap / 1e12:.2f}T" + if cap >= 1e9: + return f"${cap / 1e9:.2f}B" + if cap >= 1e6: + return f"${cap / 1e6:.2f}M" + return f"${cap:,.0f}" + + pe_str = f"{pe_ratio:.1f}" if pe_ratio else "N/A" + high_str = f"${high_52w:.2f}" if high_52w else "N/A" + low_str = f"${low_52w:.2f}" if low_52w else "N/A" + + return ( + f"Stock: {ticker_symbol} | " + f"Price: ${price:.2f} | " + f"52W High: {high_str} | " + f"52W Low: {low_str} | " + f"P/E: {pe_str} | " + f"Market Cap: {_fmt_cap(market_cap)}" + ) + + except Exception as exc: + # Catch-all so a yfinance network error doesn't crash the agent loop. + return ( + f"Could not find stock data for '{input_str}'. " + f"Error: {exc}. " + "Please provide a valid ticker symbol (e.g., AAPL, MSFT, GOOGL)." + ) + + return Tool( + name="get_stock_data", + func=get_stock_data, + description=( + "Get current stock/financial data for a publicly traded company. " + "Input: a stock ticker symbol (e.g., AAPL, MSFT, GOOGL) or company name. " + "Returns current price, 52-week range, P/E ratio, and market cap." + ), + ) diff --git a/05-agentic-rag-realtime/src/tools/rag_tool.py b/05-agentic-rag-realtime/src/tools/rag_tool.py new file mode 100644 index 0000000..85a37cc --- /dev/null +++ b/05-agentic-rag-realtime/src/tools/rag_tool.py @@ -0,0 +1,85 @@ +""" +src/tools/rag_tool.py + +Wraps the FAISS knowledge base search as a LangChain Tool so the agent can +call it alongside web search, finance, and weather tools. + +KEY CONCEPT — Tool Description is Everything: + The agent's LLM reads the `description` field to decide WHEN to call this + tool. A vague description like "search documents" leads to the agent using + the tool for every question. A specific description that says what the + knowledge base CONTAINS helps the agent make the right routing decision: + internal docs → RAG, live prices → finance_tool, current events → web_search. +""" + +from typing import List + +from langchain.tools import Tool +from langchain_community.vectorstores import FAISS + +from src.knowledge_indexer import search_knowledge_base + + +def create_rag_tool( + vector_store: FAISS, + domain_description: str = "internal company documents", +) -> Tool: + """ + Build a LangChain Tool that searches the FAISS knowledge base. + + Input/Output contract (required by LangChain Tools): + - Input: always a plain string — the search query the agent constructs. + - Output: always a plain string — the formatted retrieval results. + The agent cannot pass Python objects; everything is serialised to/from text. + + Args: + vector_store: Loaded FAISS index returned by knowledge_indexer. + domain_description: Short phrase describing WHAT is stored in the KB, + e.g. "Q3 financial forecasts and product roadmaps". + This is injected into the tool description so the + LLM knows exactly when to use it. + + Returns: + A configured LangChain Tool ready to add to the agent's tool list. + """ + + def _search(query: str) -> str: + """ + Inner function called by LangChain when the agent invokes this tool. + The agent provides `query` as a plain string. + """ + chunks: List[str] = search_knowledge_base(query, vector_store, k=3) + + if not chunks: + return "No relevant information found in knowledge base." + + # Format each retrieved chunk with a numbered label so the LLM can + # reference specific chunks in its final answer. + lines = ["Found in knowledge base:"] + for i, chunk in enumerate(chunks, start=1): + # Strip excess whitespace from the chunk to keep the context window tidy. + clean = " ".join(chunk.split()) + lines.append(f"{i}. {clean}") + + return "\n".join(lines) + + # --------------------------------------------------------------------------- + # The description is intentionally verbose: + # • "internal policies, product documentation, or stored knowledge" signals + # the agent to use this for anything that would appear in static docs. + # • Mentioning the domain_description further narrows the scope. + # • Ending with "Input: a search query string" sets the input format + # expectation clearly so the agent passes a plain query, not JSON. + # --------------------------------------------------------------------------- + description = ( + f"Search {domain_description} for relevant information. " + "Use this for questions about internal policies, product documentation, " + "or stored knowledge. " + "Input: a search query string." + ) + + return Tool( + name="search_knowledge_base", + func=_search, + description=description, + ) diff --git a/05-agentic-rag-realtime/src/tools/weather_tool.py b/05-agentic-rag-realtime/src/tools/weather_tool.py new file mode 100644 index 0000000..762c38b --- /dev/null +++ b/05-agentic-rag-realtime/src/tools/weather_tool.py @@ -0,0 +1,131 @@ +""" +src/tools/weather_tool.py + +Fetches current weather data from the OpenWeatherMap API. + +HOW THE AGENT PASSES PARAMETERS: + The agent's LLM reads the tool description, extracts the relevant entity + from the user's question (e.g. "London" from "What's the weather in London?"), + and passes it as the `city` string to this tool. The tool's job is simply to + accept that string and return a human-readable result. + +UNITS: + We use metric (°C, km/h) by default because it is universally understood. + If your users are in the US you can change `units=metric` to `units=imperial` + in the API URL to receive °F and mph. +""" + +import requests +from langchain.tools import Tool + + +def create_weather_tool(openweathermap_api_key: str) -> Tool: + """ + Build a LangChain Tool that returns current weather from OpenWeatherMap. + + Free tier: 60 API calls/minute, no credit card required. + Sign up at https://openweathermap.org/api + + Args: + openweathermap_api_key: A valid OpenWeatherMap API key. + + Returns: + A configured LangChain Tool for weather lookups. + """ + + def get_weather(city: str) -> str: + """ + Call the OpenWeatherMap "current weather" endpoint for a given city. + + The agent passes the city name exactly as it understands it from the + user's question — it may be "London", "New York, US", "Paris, FR", etc. + OpenWeatherMap accepts most common city formats. + + Args: + city: City name string provided by the agent. + + Returns: + A single human-readable weather summary string. + """ + city = city.strip() + url = ( + "https://api.openweathermap.org/data/2.5/weather" + f"?q={city}&appid={openweathermap_api_key}&units=metric" + ) + + try: + response = requests.get(url, timeout=10) + + # OpenWeatherMap returns 404 when the city name isn't recognised. + if response.status_code == 404: + return ( + f"Could not find weather for '{city}'. " + "Please check the city name (try adding the country code, " + "e.g. 'Paris, FR')." + ) + + response.raise_for_status() + data = response.json() + + # Extract fields — the API always returns these keys on success. + temp = data["main"]["temp"] + feels_like = data["main"]["feels_like"] + humidity = data["main"]["humidity"] + description = data["weather"][0]["description"].capitalize() + wind_speed_ms = data["wind"]["speed"] + # Convert m/s → km/h for a more intuitive display. + wind_kmh = wind_speed_ms * 3.6 + city_name = data.get("name", city) + country = data.get("sys", {}).get("country", "") + location = f"{city_name}, {country}" if country else city_name + + return ( + f"Weather in {location}: " + f"{temp:.1f}°C (feels like {feels_like:.1f}°C), " + f"{description}, " + f"Humidity: {humidity}%, " + f"Wind: {wind_kmh:.1f} km/h" + ) + + except requests.exceptions.Timeout: + return f"Weather service timed out for '{city}'. Please try again." + except Exception as exc: + return f"Could not retrieve weather for '{city}'. Error: {exc}" + + return Tool( + name="get_weather", + func=get_weather, + description=( + "Get current weather and forecast for any city. " + "Input: city name (e.g., 'London' or 'New York, US'). " + "Returns temperature, weather conditions, humidity, and wind speed." + ), + ) + + +def create_mock_weather_tool() -> Tool: + """ + Return a mock weather tool used when no OpenWeatherMap API key is set. + + The mock returns plausible-looking data so that the full agent pipeline can + be tested without any API keys. The response clearly labels itself as mock + data so it is never confused with real weather information. + """ + + def mock_weather(city: str) -> str: + city = city.strip() + return ( + f"[MOCK DATA — configure OPENWEATHERMAP_API_KEY for real weather] " + f"Weather in {city}: 18.0°C (feels like 17.0°C), " + f"Partly cloudy, Humidity: 65%, Wind: 14.0 km/h" + ) + + return Tool( + name="get_weather", + func=mock_weather, + description=( + "Get current weather and forecast for any city. " + "NOTE: Running in mock mode (no API key configured). " + "Input: city name (e.g., 'London' or 'New York, US')." + ), + ) diff --git a/05-agentic-rag-realtime/src/tools/web_search_tool.py b/05-agentic-rag-realtime/src/tools/web_search_tool.py new file mode 100644 index 0000000..0977b10 --- /dev/null +++ b/05-agentic-rag-realtime/src/tools/web_search_tool.py @@ -0,0 +1,138 @@ +""" +src/tools/web_search_tool.py + +Provides live web search capability via the Tavily API so the agent can answer +questions about current events, recent news, or anything not in the static +knowledge base. + +WHEN TO USE WEB SEARCH vs RAG: + • Current events / breaking news → web_search (RAG docs are static) + • Live prices, exchange rates, scores → dedicated tool (finance/weather) + • Recent product releases, news stories → web_search + • Internal company policies or strategy → search_knowledge_base (private data) + • Historical or stable reference info → search_knowledge_base (faster, no API cost) + +RATE LIMITS: + Tavily free tier allows 1,000 searches/month (~33/day). + Don't call this tool for every question — reserve it for questions that + genuinely require real-time or recent information. +""" + +from langchain.tools import Tool + + +def create_web_search_tool(tavily_api_key: str) -> Tool: + """ + Build a LangChain Tool that queries the Tavily web search API. + + Tavily is purpose-built for LLM agents — it returns clean text snippets + rather than raw HTML, which keeps the context window efficient. + + Args: + tavily_api_key: A valid Tavily API key from https://tavily.com. + + Returns: + A configured LangChain Tool for live web search. + """ + + def _web_search(query: str) -> str: + """ + Call the Tavily search API and format the top results as plain text. + + The agent provides `query` as whatever it decides to search for. + We return a structured string so the agent can extract the most + relevant details in its reasoning step. + """ + try: + # tavily-python provides a clean SDK around the REST API. + from tavily import TavilyClient # type: ignore + + client = TavilyClient(api_key=tavily_api_key) + + # max_results=3 keeps the context manageable while still giving + # the agent enough breadth to triangulate information. + response = client.search(query, max_results=3) + + results = response.get("results", []) + if not results: + return "Web search returned no results for that query." + + lines = [] + for r in results: + title = r.get("title", "No title") + snippet = r.get("content", r.get("snippet", "No snippet available")) + url = r.get("url", "") + lines.append(f"Title: {title}\nSnippet: {snippet}\nURL: {url}\n---") + + return "\n".join(lines) + + except ImportError: + # Fall back to a direct HTTP call if the SDK isn't installed. + return _tavily_http_fallback(query, tavily_api_key) + + except Exception as exc: + # Graceful degradation: the agent will see this message and can + # note in its response that web search was unavailable. + return f"Web search unavailable: {exc}" + + def _tavily_http_fallback(query: str, api_key: str) -> str: + """Direct HTTP fallback when the tavily-python package is missing.""" + import requests # noqa: PLC0415 + + try: + resp = requests.post( + "https://api.tavily.com/search", + json={"api_key": api_key, "query": query, "max_results": 3}, + timeout=10, + ) + resp.raise_for_status() + data = resp.json() + results = data.get("results", []) + if not results: + return "Web search returned no results." + lines = [] + for r in results: + lines.append( + f"Title: {r.get('title', '')}\n" + f"Snippet: {r.get('content', '')}\n" + f"URL: {r.get('url', '')}\n---" + ) + return "\n".join(lines) + except Exception as exc: + return f"Web search unavailable: {exc}" + + return Tool( + name="web_search", + func=_web_search, + description=( + "Search the live web for current information, news, or recent events. " + "Use this when the question requires up-to-date information not available " + "in static documents. " + "Input: a search query string." + ), + ) + + +def create_mock_web_search_tool() -> Tool: + """ + Fallback tool returned when no Tavily API key is configured. + + The agent will still receive a coherent message explaining why the tool + is unavailable, rather than raising an exception mid-reasoning. + """ + + def _mock_search(query: str) -> str: # noqa: ARG001 + return ( + "Web search is not configured. " + "Please add TAVILY_API_KEY to .env to enable live web search." + ) + + return Tool( + name="web_search", + func=_mock_search, + description=( + "Search the live web for current information, news, or recent events. " + "NOTE: Web search is currently disabled (no API key configured). " + "Input: a search query string." + ), + ) diff --git a/05-agentic-rag-realtime/src/tools/wiki_tool.py b/05-agentic-rag-realtime/src/tools/wiki_tool.py new file mode 100644 index 0000000..a2d07d9 --- /dev/null +++ b/05-agentic-rag-realtime/src/tools/wiki_tool.py @@ -0,0 +1,85 @@ +""" +src/tools/wiki_tool.py + +Provides Wikipedia lookups as a lightweight alternative to full web search. +Wikipedia is ideal for factual, encyclopaedic questions where live web crawling +isn't needed but the answer isn't in the internal knowledge base either. + +Advantages over web_search: + • No API key required. + • Zero cost — Wikipedia has no rate limits for this use case. + • Results are well-structured and factual (not SEO-optimised content). + +Use this before falling back to web_search for definitional or historical queries. +""" + +from langchain.tools import Tool + + +def create_wiki_tool() -> Tool: + """ + Build a LangChain Tool that searches Wikipedia for encyclopaedic information. + + Uses the `wikipedia` PyPI package which wraps the Wikipedia REST API. + + Returns: + A configured LangChain Tool for Wikipedia lookups. + """ + + def search_wikipedia(query: str) -> str: + """ + Search Wikipedia for the query and return a short summary. + + We return only the first 500 characters of the summary to keep the + context window usage low. The agent can always call the tool again + with a more specific query if it needs more detail. + + Args: + query: Search term or topic provided by the agent. + + Returns: + A plain-text summary from Wikipedia, or an error message. + """ + try: + import wikipedia # noqa: PLC0415 + + query = query.strip() + + # wikipedia.summary() can raise DisambiguationError when the query + # maps to multiple articles (e.g. "Python"). We handle this by + # picking the first suggestion automatically. + try: + summary = wikipedia.summary(query, sentences=4, auto_suggest=True) + except wikipedia.exceptions.DisambiguationError as e: + # Try the first suggested page instead of failing. + if e.options: + summary = wikipedia.summary(e.options[0], sentences=4) + else: + return f"Wikipedia: '{query}' is ambiguous. Please be more specific." + except wikipedia.exceptions.PageError: + return f"Wikipedia: No article found for '{query}'. Try a different search term." + + # Truncate to keep context window usage predictable. + if len(summary) > 800: + summary = summary[:800] + "…" + + return f"Wikipedia summary for '{query}':\n{summary}" + + except ImportError: + return ( + "Wikipedia tool is unavailable. " + "Install the 'wikipedia' package: pip install wikipedia" + ) + except Exception as exc: + return f"Wikipedia lookup failed for '{query}'. Error: {exc}" + + return Tool( + name="search_wikipedia", + func=search_wikipedia, + description=( + "Search Wikipedia for factual, encyclopaedic information about any topic. " + "Use this for definitions, historical facts, scientific concepts, or " + "general knowledge questions. No API key required. " + "Input: a topic or search term string." + ), + ) From b084de1c148a66e35bd2815546a2642dc475c8fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:17:47 +0000 Subject: [PATCH 7/9] Build all 5 GenAI learning projects as per spec Co-authored-by: nerdjerry <7092764+nerdjerry@users.noreply.github.com> --- .../__pycache__/main.cpython-312.pyc | Bin 0 -> 9730 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 134 bytes .../src/__pycache__/chunker.cpython-312.pyc | Bin 0 -> 2474 bytes .../document_loader.cpython-312.pyc | Bin 0 -> 3272 bytes .../src/__pycache__/embedder.cpython-312.pyc | Bin 0 -> 2865 bytes .../src/__pycache__/generator.cpython-312.pyc | Bin 0 -> 3694 bytes .../src/__pycache__/retriever.cpython-312.pyc | Bin 0 -> 3239 bytes .../__pycache__/vector_store.cpython-312.pyc | Bin 0 -> 4802 bytes .../__pycache__/main.cpython-312.pyc | Bin 0 -> 14661 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 136 bytes .../clause_extractor.cpython-312.pyc | Bin 0 -> 6758 bytes .../conflict_detector.cpython-312.pyc | Bin 0 -> 7817 bytes .../document_parser.cpython-312.pyc | Bin 0 -> 7743 bytes .../src/__pycache__/indexer.cpython-312.pyc | Bin 0 -> 5899 bytes .../src/__pycache__/qa_chain.cpython-312.pyc | Bin 0 -> 4494 bytes .../__pycache__/risk_analyzer.cpython-312.pyc | Bin 0 -> 6821 bytes .../__pycache__/summarizer.cpython-312.pyc | Bin 0 -> 5650 bytes .../__pycache__/main.cpython-312.pyc | Bin 0 -> 7387 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 132 bytes .../src/__pycache__/agent.cpython-312.pyc | Bin 0 -> 5448 bytes .../__pycache__/gap_analyzer.cpython-312.pyc | Bin 0 -> 8149 bytes .../__pycache__/paper_indexer.cpython-312.pyc | Bin 0 -> 6728 bytes .../__pycache__/paper_parser.cpython-312.pyc | Bin 0 -> 8050 bytes .../report_generator.cpython-312.pyc | Bin 0 -> 8386 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 138 bytes .../__pycache__/compare_tool.cpython-312.pyc | Bin 0 -> 5203 bytes .../__pycache__/search_tool.cpython-312.pyc | Bin 0 -> 3832 bytes .../__pycache__/summary_tool.cpython-312.pyc | Bin 0 -> 4349 bytes .../__pycache__/main.cpython-312.pyc | Bin 0 -> 9526 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 132 bytes .../src/__pycache__/generator.cpython-312.pyc | Bin 0 -> 3833 bytes .../__pycache__/image_indexer.cpython-312.pyc | Bin 0 -> 4538 bytes .../image_processor.cpython-312.pyc | Bin 0 -> 7366 bytes .../multi_retriever.cpython-312.pyc | Bin 0 -> 6576 bytes .../multimodal_parser.cpython-312.pyc | Bin 0 -> 6329 bytes .../__pycache__/query_router.cpython-312.pyc | Bin 0 -> 4611 bytes .../__pycache__/table_indexer.cpython-312.pyc | Bin 0 -> 4671 bytes .../table_processor.cpython-312.pyc | Bin 0 -> 6673 bytes .../__pycache__/text_indexer.cpython-312.pyc | Bin 0 -> 3990 bytes .../__pycache__/main.cpython-312.pyc | Bin 0 -> 9446 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 138 bytes .../src/__pycache__/agent.cpython-312.pyc | Bin 0 -> 5121 bytes .../knowledge_indexer.cpython-312.pyc | Bin 0 -> 5590 bytes .../response_formatter.cpython-312.pyc | Bin 0 -> 5573 bytes .../__pycache__/tool_registry.cpython-312.pyc | Bin 0 -> 4448 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 144 bytes .../__pycache__/finance_tool.cpython-312.pyc | Bin 0 -> 5178 bytes .../__pycache__/rag_tool.cpython-312.pyc | Bin 0 -> 3232 bytes .../__pycache__/weather_tool.cpython-312.pyc | Bin 0 -> 5231 bytes .../web_search_tool.cpython-312.pyc | Bin 0 -> 5678 bytes .../__pycache__/wiki_tool.cpython-312.pyc | Bin 0 -> 3441 bytes README.md | 168 ++++++++++++++++++ 52 files changed, 168 insertions(+) create mode 100644 01-rag-from-scratch/__pycache__/main.cpython-312.pyc create mode 100644 01-rag-from-scratch/src/__pycache__/__init__.cpython-312.pyc create mode 100644 01-rag-from-scratch/src/__pycache__/chunker.cpython-312.pyc create mode 100644 01-rag-from-scratch/src/__pycache__/document_loader.cpython-312.pyc create mode 100644 01-rag-from-scratch/src/__pycache__/embedder.cpython-312.pyc create mode 100644 01-rag-from-scratch/src/__pycache__/generator.cpython-312.pyc create mode 100644 01-rag-from-scratch/src/__pycache__/retriever.cpython-312.pyc create mode 100644 01-rag-from-scratch/src/__pycache__/vector_store.cpython-312.pyc create mode 100644 02-legal-ai-assistant/__pycache__/main.cpython-312.pyc create mode 100644 02-legal-ai-assistant/src/__pycache__/__init__.cpython-312.pyc create mode 100644 02-legal-ai-assistant/src/__pycache__/clause_extractor.cpython-312.pyc create mode 100644 02-legal-ai-assistant/src/__pycache__/conflict_detector.cpython-312.pyc create mode 100644 02-legal-ai-assistant/src/__pycache__/document_parser.cpython-312.pyc create mode 100644 02-legal-ai-assistant/src/__pycache__/indexer.cpython-312.pyc create mode 100644 02-legal-ai-assistant/src/__pycache__/qa_chain.cpython-312.pyc create mode 100644 02-legal-ai-assistant/src/__pycache__/risk_analyzer.cpython-312.pyc create mode 100644 02-legal-ai-assistant/src/__pycache__/summarizer.cpython-312.pyc create mode 100644 03-research-agent/__pycache__/main.cpython-312.pyc create mode 100644 03-research-agent/src/__pycache__/__init__.cpython-312.pyc create mode 100644 03-research-agent/src/__pycache__/agent.cpython-312.pyc create mode 100644 03-research-agent/src/__pycache__/gap_analyzer.cpython-312.pyc create mode 100644 03-research-agent/src/__pycache__/paper_indexer.cpython-312.pyc create mode 100644 03-research-agent/src/__pycache__/paper_parser.cpython-312.pyc create mode 100644 03-research-agent/src/__pycache__/report_generator.cpython-312.pyc create mode 100644 03-research-agent/src/tools/__pycache__/__init__.cpython-312.pyc create mode 100644 03-research-agent/src/tools/__pycache__/compare_tool.cpython-312.pyc create mode 100644 03-research-agent/src/tools/__pycache__/search_tool.cpython-312.pyc create mode 100644 03-research-agent/src/tools/__pycache__/summary_tool.cpython-312.pyc create mode 100644 04-multimodal-rag/__pycache__/main.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/__init__.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/generator.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/image_indexer.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/image_processor.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/multi_retriever.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/multimodal_parser.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/query_router.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/table_indexer.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/table_processor.cpython-312.pyc create mode 100644 04-multimodal-rag/src/__pycache__/text_indexer.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/__pycache__/main.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/__pycache__/__init__.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/__pycache__/agent.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/__pycache__/knowledge_indexer.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/__pycache__/response_formatter.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/__pycache__/tool_registry.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/__init__.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/finance_tool.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/rag_tool.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/weather_tool.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/web_search_tool.cpython-312.pyc create mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/wiki_tool.cpython-312.pyc create mode 100644 README.md diff --git a/01-rag-from-scratch/__pycache__/main.cpython-312.pyc b/01-rag-from-scratch/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09156204c4dd7a25434f9a9f2f0b8a52d732ca61 GIT binary patch literal 9730 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 literal 0 HcmV?d00001 diff --git a/01-rag-from-scratch/src/__pycache__/__init__.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e73f4b018d0adba582c8acf304fe98c99c48fc2e GIT binary patch literal 134 zcmX@j%ge<81WK;+Gxr1O#~=<m_{;(nna)tjpvmaBlA(wR$omXZ^Gj7v-@s6}C^20( zttdZNw>Y^du_QS|zqlw_KR!M)FS8^*Uaz3?7l%!5eoARhs$CH)P!}T*7lRldnHd=w Ii<p5d01R;*6#xJL literal 0 HcmV?d00001 diff --git a/01-rag-from-scratch/src/__pycache__/chunker.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/chunker.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f47f95cd2b0721199e129f3004d89ec9396891a6 GIT binary patch literal 2474 zcmZ`)Uu+ab7@xV_yWU;zyh;sFDLPV!ccvT_>Yp@iV=WMpmV(rfh{0yNJH1=j+r7-} z()PTX#$e)u4;YP*0HN^-Q%y|t!2}-qNaD*OO=vbe81zBjPFoZ0li%#^?j058Zf9n{ z{pS0A^ZUMU=I3-eiJ*-ixuO5H4WZw}OVASjWIYd)%Sb~SHc$zl!5Agrr^U4hjc5{; zG#Q>Kjm*Tg*v04>dA==|cqA6*u=~O(sydA87wBkFVTx)~_AZ^XC(DLz!`w}1mg<zK zX>&KJ79I2a6xS=%O*iJ&1<DMi?8X?i9cHSM4@nFHo_JqBjRia#`bN{R9qLulvM;}i zw8(WSc*CvwQWe8jxf;2OuFEYN(ZFIAS0e?aMZd;&B#?a@Tb3L11*s~XYYXJfvFUq3 zZb8@9f-dw1{^Kq3R@8~>(H2b2T_)`yKuBx9h^q2owCp<%^zHTlO^fZ&r<PjwcT<58 z5TslAsmhZm_l)urP?;pqyoeEn7&^C!HABV~vj8F0O>#JhAkj_RBD|y+24%#HAkQbg z)khSy*pLvN6W!+2m;qz|H$trGb5yl^!B&wn-PX+lv5S<HsI6#<t$5bJy^vW<FX*OX zkRXRhmRn?&5^n2;LC%}jESW9p;CV(jD4|UZs#2n4hFPVYk8gw(_<(0y>@~2*Nmgt( zHz0&${HZ(lL4&%m87`kPEJdRXe0b}jYi2mf0$IM{<I0>~a!SN>O4C3b>`o18g_CkX zWu6?SGm2x_1LU>7zO6R>N(s$QJjoVBC(xUw_AI3)Q7w~0h1yV{VZ;KoPacm^qYSVZ z<&=p<mT5yl6U8RemSbuP(<y%pt55MMuY_Az7Ss-q5t4Pd11U34N)Cj|u*?E*7Uo%y zPESphFvaGSQrV!*C^iz14Ed~wLCuD<-j;w20ja^ePZEf7Vd%;X0+yZGGs>u9Q;mFR zP&3<bkz4So+p|@@@%{b2v-z@8pi>@K5OyH82Xc}@C3zYzb0Rq0;;XqhNPe+Uv>w7m z0taHEKs*!%hiI63o+&2-<)5tI_~H^76-TS6rAj8$$?{x2pReo$gOk)2mBt5J38{2! z<s&CoBFW??hCgvpPG;hSWHIR6B&PsDu`R~k3{X?Bi&Ha>soJ__Iw{b2c<mDsa92tD zgn2bcDyddX9-t98_<^y88WC5*0QpD_4?nOc4&OuYtRIDL3}Ltes_1MNLJPQg*jDkm z@W2K&zb*TAzQUI=MxX7H(LDN2n#DOhk&Czzpm$S`#Vg`Q@_jB=xiCh^=HC2)zE}4H z!2P1$_j46Ct`-k)rXKKuMp?c*znK^;P?OG;*>Gi#C!PuuFF$Bls$%fre5g|d(;VQV zKhQsm=$B0YP5h;FY2@<6=M(okdhTZW|K(!KAHVn#ekAZ$+JPMZM&-Gf8}$;DiI6jK z{ka(~?%1YJSK>~|l}gH7F3N<dmgvPjU2z}B8d=9hWFavKVV$0_EgN9nAbkTKeha!q zwDCHMCTpqgYm-Y|S3jtA_uf2FOAfBeLl5LcEwy*)%+h$R@2!>HE16pI<f{D6MjUnI z*E)BvZSQ&5`N~ENb#-rK;vKQYWA&ZrneF$}`|hUqE$R2teTzqbmlL0kU+cVb`s%JL z>7}zj9{PU%hvC}6w^w>=$rG#cNpMk*;P%vmR0e3ed3rU|e=pU4Te>~E61z2a=h=I2 zj@?a-)!N7JjMXKacn@z#QZ!aiqSUs<<Ldw)LD=;TAobBfc*#ZG9R^%(z^-oQMO{t> z>@<j##lhuDa2$9ne1q6-SmyDx86PZJnq$ym)&nvTCq4un_{I3|B1U)=gX0~~uXXME mv#lR*Tg!CTB^ZD0=&VO!tjExEJL=*~J&t0@jRed*^Zx;pai_}w literal 0 HcmV?d00001 diff --git a/01-rag-from-scratch/src/__pycache__/document_loader.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/document_loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1706d7b8b682588fa9366b7791ec97307799555 GIT binary patch literal 3272 zcmai0TWs6b89o%L`^A=QwJ{tgr@ka+Ytv4LqMMMob7On;mbva6V2iWBU_@GyO_9Rk zVSSkt1VcA?fPoh6L*1o8JhZ@4bin%PJ?&x0Fkmlq?ZFw50zuc8Jx#3*Z17(8A4-(8 zq}dKYo}WDb`TqNV&Y$D)2!e5G@=jr<1EIgM4{r!{;_y5W?;-^$oQCGv?9;fSZ{Ei- zn%DTEFfSDS^ZsIBJ^=p&HK_R2(4wFO-Vo-)Yn&MTJA;}LIY*e4X<p===<M3;#MC82 zR#f6dE~~4wPA4Xe?CSHh@f}iRDlaGn;sl6FEuv?+x>{@s!NbFII12A~5pArM(Z_YV z!j-ut#`DPdxc+UnQNOpOaAkB0%)8BdyMM~%P{l_B9tUL--9cT)F;d~*=PNvo*Zv~} z^4dJQIV*yP>CF@9+NTIrgtD-dZ1Q<jLqBe_6F5@d|K_}uZqgp5_&s#OZH~ger@WQs zywugCJ<5|-@t6H&{&kM@&~9%{34FbL@Kmtuhj-|9Ln?<hms@s*pS1I~&(o7bVN~YJ zex_6<;6vpA?eQd+AM;rySPm|o_STzoL;p~V^ay>%<0ywbd7y8!8D9>SeM@~FS95Oi zgGKuSYC&pfuCy%=B1Doc65HMUN&Ywf9BSc6eI3#*)asA2Zw~b#+V5%8kI0!yxCJBj zB(Ey*au~20UgOXeq$Dcg8{xOO%@=Fb3b%q*`73CJ6FcN_D2xb(!Kx3-nublQR5A!v z6|5LptElSK#EZlzVi`jbs#$7~H7Vlm`Jt>Wo0#T_Vde5Lt2kG9UDca>LwK>EE2fJp zm6XLHEJ0xpVOdwO1n{e{uBqmbi>84FCRXKa9vixfEwiBKTu`}GB1VZ6WU6Auq>>s! ztsMY5UesiYwSq~pu~@T4)^iv0azV$?O<W_eaeWDtH|wg8lbo4xS())(i@}2?C1M7% z9)+plN?$ZIkchK}P63bUv}RbO=^Uwn!jM{rD&?dh+{?g6Ou1On)CI8F?5#QP2W~TK zQG)58!Q-AC_|lVYz>~~NUVLy~Zh@(3FpNubPF-MHLj*IfB@~)0hKI;m7G5`~ENCnS zj~FqGiz=0w<{5})mbRJ?t}cQjidGT-q}K#rWYyjbKF?4VO5nJ$O3lE@RoCVgFE{e( zf*VKXjaq^YKJFy3p%Fx0r-KdNtE1PXR@+1{xDUHbCMNO<W@i4c&7Ng!BO8s)E!I4y z&j?XjRj?%Pg_tr0!06G6D`>ra0zB1Bdwh2NduJg$s)==jxf~)Z;aN?UO%-Q}$`ZLi z-71h31)D}u#jWLGkk;yuM1Eah%kYNkS@*k@<8aR&bCUHe^azYU|K;{ScHYEzZn;n@ zu@trRW*M`<V7gXPGkD#H2ah??YIk>4@4WMdTlcUdgDrLhF@FHu&);_cLz1kgnrW8J zTl(^#DO&8<0j}GdcuEFvg_Q~`Ou*0J8RCOw3mPMeczVqk4HOfvsH9%U^&C9A{>`&) z6^;LYXShzIWDUzXs7A?RCq?A*_kV^j8&uX>q_~Yj$zZXAK#N08z>R|GB&R^*8G}w4 zmaa?^Vi3ZvIfPvS2s`DSfa$hB$4_#aaoq_P%mwB`N6_V>>O@Q`6Kbv$XdXDN#l^y^ z<JSx@%JI8N=<v`(9D&Jqcum!rWT-*K2~VzO)e<cjx)>orCMoRp9ab|=99nO)9-K(6 z&n*<?k`r!JmJ@PGz;j}ChO8!=rW2@r0iah=4yT&ze!{|88^=S^$mqFYBIkx#<qw-# zBGYVs#3b1fZ`(o*M=Gs32}U+GB4+@s7asE-T&r%PqfwMhzxAVc5;v!-0TfFgM7wvR z-TTp=2Z_`76Q{OTx0b4i6JO!r9iQe7dA==DMSM6?jiB_?H@|-rL8+bx$?gZq(_aRK zRN&@!j}qvqlUvhwUVZP?yBGIPj_#$;eT4@6>5VW5Xzw~`!@F&GzpeLvtkaeF=GZUB z4x;_L(f;kZ{iq1PA9kEP=;+(+=sV~TcRR$rj^{Q)jJ0zsvl|`wB9^o}$9C3!f8%#I z?6H|YUAJGmV$aUo^H=TEkN0EO0N{&I+-}F)UB5o{t5Y@}wG-#|L(f-*WH|k>dtf`i z+dZ;z>96T$KJ42rd?0~-8&hCS*XcXa_oBDr8`G|Yfe*8H#Se6Q;061I@7SqV_G8oc zV_gR^yc+}Lzr>!az63@c&2T6b-MFw9?Am(m)8MHGq4@p8i#wrD*1=yda}%!HCb=V? zc`SiqUAA!I$dAGayIuN?`N@U7k#Spk*-l*C4^2Fbo`BVV9rb{hzCuDceH1|Pl-()b z6@M#zB-!G(c2YazJKwNVFYU*^{aLK@-_<a19h>aJ^@pDEw#hTTduPH^Y5%>E?x}YF zAKU#vixJ0Xm`;#or&hRLE24f4&ZI0mSK(wS7A?I%*H|}lyLdf$%tz{pJPl%?9-s%h zN%Rpqgdd0u!0R}4zGx_xrjC(e_+%G$lMz*(<G8~i#6><sC;k}{INz5DUe&}^j_cZ* Qt0FdUUp{hYG6XyR18js_i~s-t literal 0 HcmV?d00001 diff --git a/01-rag-from-scratch/src/__pycache__/embedder.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/embedder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84583793f99f5226e2e848b587f3bfde6e02a06d GIT binary patch literal 2865 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 a{gYe#zBwcvmg?Ud=Zp2LL)-i&to{Sk4I#Mz literal 0 HcmV?d00001 diff --git a/01-rag-from-scratch/src/__pycache__/generator.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/generator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d16c42644be4bc125de9c434b930815bd579446 GIT binary patch literal 3694 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 literal 0 HcmV?d00001 diff --git a/01-rag-from-scratch/src/__pycache__/retriever.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/retriever.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9c566705c29f29784e4d00fe7cd136cb69ed742 GIT binary patch literal 3239 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 Bhv5JK literal 0 HcmV?d00001 diff --git a/01-rag-from-scratch/src/__pycache__/vector_store.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/vector_store.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63138824adaff420edfeace0694ef3de3072a6c0 GIT binary patch literal 4802 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 Rfh~==5bb`_1#h&i{{vNd6hi<2 literal 0 HcmV?d00001 diff --git a/02-legal-ai-assistant/__pycache__/main.cpython-312.pyc b/02-legal-ai-assistant/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7ae90eb3e2f8a06ef5f9d36d7e2094d45f2054d GIT binary patch literal 14661 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 E8}qI1bpQYW literal 0 HcmV?d00001 diff --git a/02-legal-ai-assistant/src/__pycache__/__init__.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e0373a2f74e346586f6117f8ab56bebf6886b45 GIT binary patch literal 136 zcmX@j%ge<81nO?{GePuY5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!QrFWrFw)IQO;60x zP0Z9yEH2J0E=kNQ(JwAa){l?R%*!l^kJl@x{Ka9Do1apelWJGQ3e?94#Kj=SM`lJw J#v*1Q3jqG}9Wnp_ literal 0 HcmV?d00001 diff --git a/02-legal-ai-assistant/src/__pycache__/clause_extractor.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/clause_extractor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7e3c1b1492ad7260beea15af1c7bacbbb24e29c GIT binary patch literal 6758 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 literal 0 HcmV?d00001 diff --git a/02-legal-ai-assistant/src/__pycache__/conflict_detector.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/conflict_detector.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8fd022f722809ead216862a462dc2cd73bd902dd GIT binary patch literal 7817 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 literal 0 HcmV?d00001 diff --git a/02-legal-ai-assistant/src/__pycache__/document_parser.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/document_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0080bc4501e7313847ac8856f9db27221924111 GIT binary patch literal 7743 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 literal 0 HcmV?d00001 diff --git a/02-legal-ai-assistant/src/__pycache__/indexer.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/indexer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a0fbf25417a890f449c665ee103d01689fde745 GIT binary patch literal 5899 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 CV^Vwo literal 0 HcmV?d00001 diff --git a/02-legal-ai-assistant/src/__pycache__/qa_chain.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/qa_chain.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f367a13e35b12a7641d07def2feb14779ad35925 GIT binary patch literal 4494 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 literal 0 HcmV?d00001 diff --git a/02-legal-ai-assistant/src/__pycache__/risk_analyzer.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/risk_analyzer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4dcc6d8fde0c1592947931fe1f5b7ae59ba40b5 GIT binary patch literal 6821 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 a(NH=to!soq@%!ebO%AUG^H20*eEc8tVm?X$ literal 0 HcmV?d00001 diff --git a/02-legal-ai-assistant/src/__pycache__/summarizer.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/summarizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e134c396f9b1a7a763aef9948ed137bf84490c7 GIT binary patch literal 5650 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 literal 0 HcmV?d00001 diff --git a/03-research-agent/__pycache__/main.cpython-312.pyc b/03-research-agent/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea5901a15d913eeb754a62fcf7265f4eae90f75c GIT binary patch literal 7387 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 literal 0 HcmV?d00001 diff --git a/03-research-agent/src/__pycache__/__init__.cpython-312.pyc b/03-research-agent/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9350b41bf9cc728d6aba52e603b21539c3d706e GIT binary patch literal 132 zcmX@j%ge<81pe;xGt+?dV-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K7F<Z(yujlv<pc zSd^Tho0y)OSE65Bl&l{gpP83g5+AQuQ2C3)CO1E&G$+-rh!v=Z5r~UHjE~HWjEqIh GKo$VGx*S9R literal 0 HcmV?d00001 diff --git a/03-research-agent/src/__pycache__/agent.cpython-312.pyc b/03-research-agent/src/__pycache__/agent.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1101a4e010d7a31305978f847bc773baee0f9fd GIT binary patch literal 5448 zcmbtY+ix6K8K1rQHt{;XBuz+ZIRv%Is_~{IP^wi6Oq_MR!m-VIA;hI@$1`WWlgwqA zi`N^iB|j7tq>4uLp{**^JRnpQ9(aTQftOURz#1h|RN$d+hD1X20rC6J+1<>hNeg0R z&*hx+edoLWzVGZGb2(MP_3v}<S~CX~<uCNnyN0_jcen8JreZ2<ikULgcFIYwrJc-L z#>uW_Q#6*bhn(THVey`|N1V~MQSm-xk2&g^D&B|fea`sWINnFra^@%>Hplpcsq%Dv z*xdJOc5S~o&L_<rPh)HnV-pygYVXfaMn4}@6gBXTlllgC!$PyI&L%GPg5~poh291a z8{B8O;lr~7rn@FvTs+S#H}sg!7Ik-HuAy5lt8mQ-ndGLb7Z$WCE3dNZLWxyM+FX?_ zE-$ZqYhaVaezU41KVIQ_;JNG>24;C?VL#UNJ*OF3?gk4&Uk~|4o3$Vl1cFc`if)J8 zcQF&Nbsn}jcZ;gZShe9r8;y|B_0i57V!7j)+-APm1iaV1C<HS-gmTREn9H|9P-1Tj zLOg`BX&IMECbESvMa#EnU)_a4nqXG<d}a}k<@JF3o4R1s+g(ukkxT1MEDIR+Co0mj z+2pRtO_GEZlUjq(M?1fUz)cPzBR8~c8o=<p<?1%m+@J;b^fr*{ncT2U2xWz=p$Ck+ zWCzFx*&HE|s+P8Nr)l#d81>9lWM$|t1*lt)uc<e=55a_yV0kwn1{f-^@-f?H0oQ$_ zQ2>jcy^7tjnk2nHQzqLKS%L{x+VxpI3=w=}Ylj;R-L(P-L`l;X*-I53M0Uu&`Z8M~ ziHhu~)+Z|mOoJ#Fy@)MA_M=_nBz3BL5tPwkLz{2vZYcFCuqCh6)gy8o-M6lA80-OU zdNWd?PwDeWIX<OC)e2#4_DF(<IJF4RAPh+5&(s)+=zC^lz;hHC>ca8`k#*X+(o&VJ zmP#@StINxatB*`SnTjgyC^8t<GPt!#k<CIR#jK2-5S!Rm5HhbWQ^3_75&b4Zz!|=U zJoOOXfCEIg8nlCuJKaz&Fjfu`VVlq=;Gn8UuFSK*+CcV31USf^z>9o?GXt3ll4=9F zfxlLxtu`T)6-Xc&+)oD@VE526bQ@vT;B7`jlt)6HW0VCUAdP7&WhA;06bPa&G6=Ko z1^mUTA8~@K^7)nJN>zj5=9epMv7}X&%1h@y$81R)n+0-)jX}QmF|z5yTaX3Yw)=^9 zvX?(62O>Z8)F3ng5U>Kpw*ic4>7mgWP)kA?fn$XHwt*@x!JExO%SqTzwcJhJj`Ve# z3y4BWAdy4B5Rt;T_n9)I0gDjs>OKq_**f4KaCbt07UDoP`FgamAqhEonmPJqIFHQT z@;xvF3(MfJF~R@Y^89?MQd*iTu`{LWg;HsWY4{W2sV$vl8e4_g7fS-m=d{Je|Ha&c zw%1r-5v~bVhy`0#kN~n`P(mvE=%>HHGBJXXYa(8yp?XxV>jB3(EUmh{mBdZvSmtbk zMsvH_0^bmn&a$%tdSOtD{6s|pa4rN{*8?OEkPZ>R0i&jcf(2I*wgrN=!b3zwi^4}| zrd6+_#sRK=98s`XNI00cZoz9^lm#)82H6IA8_-8Ek+s+XQ;X~%M4<fxc3k4MRKEvo z;>$pNMY`k#gVoS$G9bJCz@A>Bp6ND1%tVff>LVQwF#}5eZs|X|R;nzs)rIBiQFgwh zEj=1~R9ac1l{L{Q)XtVx=PKouYI%7HWdVC6Vv`C~z^-uLV_cRnQmo;H0h$E~I?~G$ zZR*yBgcVWd5vx>p8vuTIB|?yMz2~MT)<NYudJVB184iKo$Fv?4UFN(g1?ol<Huur? zP>I>}EC@$#>eW#rqJ~AS^VTd0d0%FvQ5OX$#AvgE`Z0Be`AnSCs3R+FaU%*nKOPgG z)pnD|la>pN({0qMn!vSuDju5G%B!n*nK9s$kk`82tPxWTPj`o<_j{wg#eoQfwiwpG zl2Q~x8iG!MjaH`c6h)rtK>*lC^(+W1YUrC-Vi-JtzSIU9o^5l9<|qw)GT8?d)b9~$ z6BuvmmMuVtq?5F=gXx7&zu&;>J{uc777Pg&6;#b`>Ft0S7-DUdif+9?>(@rKh-N|} z6J?;Ro`5n7<&26}q6vb`RJ+LI1r$EZy^Q{nH;IpU_L{?9CJSj1G=+G46RLWCEx?V( z6JnE^!$aNFLp>grX=!A-jXpilYb*49_YQ7vDxK6><@Jn_wiF}%@_t3x&U7-DF!SKm z$-I$%Z7Qp5XSXw9pI=A00Y}lbJroYfH$~}WJLwx~`L#V9jy!aHq%+hR?u=|?wnsyf zNnD-LPC66^7W%w2E=QAh97b?N2`74E77i!B&EDwlu|3uqTZJYm{BLJkN`6THJp?=x zStv)87}6k!l6qt##|+Pvd1*4)^!NQNVj5M08t)a|M{fbY6F34|`<M7kdVm#~$N@1S zNWCVi7WFD(98Nd+7CSC-TRKaOaK9j)-EK8cy&sby)AuHLQ@``l3bkS9yDomYztdv3 z7Xo||y326uMFR8IFCkFSY7k%Bb{N$Nd1-DtadblKiyA79CdYYUqi})&#=~VC2(&W9 z5>%)T1oq;+YYXu{02Y1zRZ5@8bw#d8vobbE1yO!r?zku{`5qL?-WLs5Jd8f8Zf$)$ z3U}v6;vtc$aRv&+LxMUU>9$SrLG<jV&0#^GDr$}UzUK$=xL8)Z+|vDxAf9mat(weW zY7X<GU`g$;PaF6YhCW4-PoeB*af=`SjFJ%NYNB<htyD_$<sal#pF-E)N4F^=PB)!+ zW-upfre%cjz61ou<4NR;$CIGxPZF<b+yY|Va~Wfwu)@h_Pg4URKyg-Rc`|v1jt_Rx zwzo#Yw9mk2ff&Aq+m5n#LOHFa{&Ylp@621L-hKY<=dV}Ze&Oa3?Pm56?)Qe3nWwJ* z@Pp}7JKwoA^YqPQ#Xrn_F!Sx5#et7Ixx;UrcyIjX_}6zz_f#eK#O}n*?)d&&6Nmmh zar*s<)4P)gcAq@D``DAe%H2Fz*gbUk-u}_t@J?y(fHFR{v-q!n%qowcNd;u|pB&Jh z$;Lw>9)#XT7e_FsDCCg@c7<K+QN+&{?<l`a-{_8{l*C-7qdamh{SIi8be+_tKDvoL z#Vvtz=>%sW(IYu$A0`WM@nI5vT8i{4Vb@bwT#3*s^qNFr7nCnw9HXg>^}O3nrvC=8 zBoiEvLw_JiQBZuGPB1OHCprLGE*3?im`dmnaRwyPl6}Dd7r9~`h#OM<D$>!a_{u|L z@s-cPlLrM9tRW$O62PfKc6?H-Pg3fY7he27t}7$orQWv4uH=%xW)$=3cu1U-;tX&q zpYgwpco2a}#Z@%S6fQ_i|4VqrV_4Nqv}5>g;XiP2+fnX4sSK%C&s^QS+PFIVVfOH? z?AX<F*GkvEaZ`Q#!|WI7^ZQp9uALE+cXHFOU;Np{YtaX}CwAulin&3;lOJY}5XIB4 zo_~OF-_*|eyVy`!%1_55wHgkXwOTxB<F_$DyX6*y5d)v376>P>E@u4&)U-Jfn!9#E zRyNUE1U~&(7(WJtDF_fQ48RnDW_<AeWCGp<v?d9YXeS}}3q+lz;wI23&fvu-^ZiEg zkK-Xs`^o4FC`*yezv-XC7^P95<F=PcrBZ)SKbOk>vzby-3#s>&bGMaKe^Z{jt$gja z^4#8NX6o6U+}`+!)YR_up}h>AcMcrh8^UvMR2iPRJ4SCmN*zf}Ut8T%@VNejc>M14 JJ^B(N{0IM0ZbkqA literal 0 HcmV?d00001 diff --git a/03-research-agent/src/__pycache__/gap_analyzer.cpython-312.pyc b/03-research-agent/src/__pycache__/gap_analyzer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6dbdc1e4c49a54c5095f7727d6cc214ac2965d04 GIT binary patch literal 8149 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 A7ytkO literal 0 HcmV?d00001 diff --git a/03-research-agent/src/__pycache__/paper_indexer.cpython-312.pyc b/03-research-agent/src/__pycache__/paper_indexer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc237eea48fbb114299bbadbb69f7c155606819a GIT binary patch literal 6728 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 C?@yip literal 0 HcmV?d00001 diff --git a/03-research-agent/src/__pycache__/paper_parser.cpython-312.pyc b/03-research-agent/src/__pycache__/paper_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1817e3948974171f89f61edac31cc2328e1375d6 GIT binary patch literal 8050 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 literal 0 HcmV?d00001 diff --git a/03-research-agent/src/__pycache__/report_generator.cpython-312.pyc b/03-research-agent/src/__pycache__/report_generator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a56840a0f8e7c641d60d6108ae3982a41277da1 GIT binary patch literal 8386 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 literal 0 HcmV?d00001 diff --git a/03-research-agent/src/tools/__pycache__/__init__.cpython-312.pyc b/03-research-agent/src/tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e47d6b1b6ed831736862209cac7fcfa4a529e7a GIT binary patch literal 138 zcmX@j%ge<81pe;xGwp!%V-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K76+Z(yujlv<pc zSd^Tho0y)OSE65Bl&oKppPy5#A0MBYmst`YuUAm{i^C>2KczG$)vkyYsFM+hi$RQ! M%#4hTMa)1J0EKfNf&c&j literal 0 HcmV?d00001 diff --git a/03-research-agent/src/tools/__pycache__/compare_tool.cpython-312.pyc b/03-research-agent/src/tools/__pycache__/compare_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8424900bbed241b848a760ad4e3b7c1822352c5 GIT binary patch literal 5203 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 literal 0 HcmV?d00001 diff --git a/03-research-agent/src/tools/__pycache__/search_tool.cpython-312.pyc b/03-research-agent/src/tools/__pycache__/search_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02f6ba853e2c6b9de9d01d12d2abff2005d94144 GIT binary patch literal 3832 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 YwxBBN$$uzswv$;kd0Ts`;2}5o4@{kDB>(^b literal 0 HcmV?d00001 diff --git a/03-research-agent/src/tools/__pycache__/summary_tool.cpython-312.pyc b/03-research-agent/src/tools/__pycache__/summary_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ac5fa8909696192476027a3eb2007ce0466a00f GIT binary patch literal 4349 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 literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/__pycache__/main.cpython-312.pyc b/04-multimodal-rag/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9800b9b79a9ff6bac3762e195c7d6a153ed22315 GIT binary patch literal 9526 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 literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/__init__.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8079aec9e4a4cdcb59d8d0305befe3c3a65918c6 GIT binary patch literal 132 zcmX@j%ge<81lb<*Gb4fYV-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K7F<Z(yRETbfgn znVX-In4?>in66)3l&l{gpP83g5+AQuQ2C3)1}ImWlWJGQ3e>|0#Kj=SM`lJw#v*1Q F3joLj9BTjo literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/generator.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/generator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0be4cfacaf8ceeba5e9774e8b0d58bf3b7cbb57 GIT binary patch literal 3833 zcmai1O>7&-6`tikmy{?{l4V=AJW?!KRAkDI96M%Qql#_0u6~l(DjG)>h!uB4uC&}` zXP1(wE(@Z_K`C57LW@d4YDDPCg;BsLdhoG_!U&KaBuj>vMGiuH>5-0eNL`@ln_ZG( zZ8;ramou|(-n{qb&G$Wi(a^vnXh&{*CfyGq^ba<uHh-n_tPeVCNJJkZKN7tnM>x?z z9K?A9=0zuQi7vv;_(b<d?hoC0Pt=z???8x85rt4eS7|Jp=TFx){+*m8iy9WNrqf(f z&ru@cELAgE9nZiI&g5iW%BZ3sV@h-?k$DiHM1yHqrHD8w$g+m@G~q`_$FQ!l)nYD5 z*c6_T6j&%I+5(|5o}YlJigL1;m8GPl%Xusd(*#ryA(_TmiNI2I1}9ZTCyP3s&SPtL zNzdcn$%$(pOicCjlQ+hOu3y7fuid(G^TvA<H{Kn;HPw$NCx+e`h1sErp{Xd=RLcTb zOk^-p0iy*?Rj@Fv=5)*6K26QhB<W+@Ny>~$GlDLG#NUg*!6cT7;V$OFv_Nq(El?fz zN*N(VKyzAM(D3``@PeS>w3JE{P4DLy(voaDO(8}~4*J40%zz^^63uA%<k{F8W3eub zSrlxuBn>vU0}oNvRbEKWDe8hu#1z4x5CT$j-nJ0LBrEVNqWu*aDFZ>BCvXZBLY);T zQIdI{9Zs6aSq<kjLNz>3s5Fzef&%B4RTYh3Atf*atVuf>SX7g_3{mtLZ@Pb)BQ$S@ zYX_gG9K0Ekl%$*!Nx}*VOwVYh2gGtzNxG^z3%@LFo$x%HhW8pup@It@H#{DAyasm_ zP5lX>g3s{Ha&_w}T@jM|KIAWJ1^+8#oT95LGt2gRq1~^H3%ubM9do@1(TL%ib-y46 zLxaCxv4KN(MnT$bNsk!r|0iuYHSP|1g?%0g)$BXSNAJG<g@6$NO}{j}2kscCv+s9c zpO4uG^1nAc2g=v`^Jf<_Tr@2DXT1k_?y725IFT;|4fiW0-F1?ILWALXrKG1$GFWI7 z8;szmNNhA3;2km=;oW2)c!%%$3n4$!s}P3_hZw0wjJ|0&W&;OU9I9$BG}Wza5}mb` zL8ECl`0`4H*8yDZW;E5bgq1Mt6s~^R|26h^3LD|?%&@TGq@9MBo)B9Pin4g<BV>55 zqH0Fg_HH)95C^}}{3*KYEWj?e&}&GKRBbqkXs8eon~ey3cND_6k=Rm*{5*2!E&F$Y zTR@A>+h~D{wh8}%6yq_p>o(-W9mk3Q7gLZ=%Yu^10fbcm`8xo<g1!LROx1e=6*5Ez z5V3~)^kEGxkYHl97Pladq0T7{t5ix^EC6yE!`GO#>LpCUaePJ1OiKz8`!V3y96+5c z0XFT^5a5i>Ks1&iG({2saS0|`#iib;tz4EfHDl~f9FKzYSJD7?nDNvA!*tz-VSCNX zh!)2`P7?)B&>ZQ<LN-ePP`hpsf-TUC<4ajVPhai=D;2<A;17sv-g54R&8?xesK%TX zc49HY4DvOAazcSKR4l9NTqW=~EkymFs9XAnEw0ibLN>KB@|Kj8?TG2Ca)#+*6l!{{ z(^Pa5nQqGpkRZAs3c6tOHh?ovOt(c?OAel2Iyw&Q$>_IA!@|5E0Z+>$w$u*aY-e8a zvBrCrhPst^hpa-tVw)LNmemC?8dviIm{tRifPfk8GDed`7*{v3(m<%ACq|>ykCnp% z1_Yz~E9QwrOHwJza!!K-gwwMvV~#_Buh<nvoVTkAeyNQ=u(05lLhzxkw&0h7JpX<r zo8vfl3oa_pSA6Sb+{vm4o4)mYcE6la^h%upZsno&sGc4xIGEuBR#*otE2DAKBPsLh z99cR(Su5YBtmI`S7oLFwAz~blKj-*lJ+Ckh9E9gt54=VY2fS;bDL~f(CnAGm81h?g zjRTGgxKN(^qZ4q$u@tCF?D0RKsKa!`&X_Ki7#d?>jGv#34V*c5dcPn%O@-8eMw0_I z^PswX?!Ux_i&=VksjKpVT~w<Ayd<k&yLLHNlSzPTtqorPMBf(C<BpMsu8sD46ZeL9 zP7H2!jQmSu)x<ACVJ^y>&J@v2H%s-b>9Oyl>6%p~#q_2HP0)2p8M0GGB&HMOOm7y- zSt+TT{%ea#Vr4?Kg)(<2i<0RCprC9AGr;1XNTzejoMtxKmm(?HxnoAG6G_zPQ}yCh zD<4)5Q^u_J?mj7#N?4aFXeri6k|l-NU~8@oZN=50$F7Y{U$s61H?6>#PO_LZdA3<% z8e%x7wZK*uti14|t<0Oy3xTIGm~DbbGa*xosN9b_aB-xByNctblRcZRL#1OK#UJiP zUi%s~xCfR!rRF1R@zwbH?7g1t=CjMbQsnSj-)i6b?R$>x$m_e2vs;m~+mXR#?_QvJ zW&UyCcq!Pn8|>H$cC0Tv37#r7w63>3ZosAB(cNI@R<Lv9)K0L^40V>;J3otm65p78 z(6!zE=5G7Nt@eu#`#*PVw~ws&%;xq|OUG_Y_f|{yM*2z1fTd=esp;4ac5el{H&Rc6 zv2r_@_!OfvZ?BwRdwccm^`GrDzrOkXpA<*G92{C1UK?2**=XEp8Q8oo7RSE4aKoP3 z#Bc1h3~s(F7stvTPlPK6k)yHLSVqneSN0%Bs2GBN(|+I46xa(iEuShQN8~0~KHk*$ zBd$1H?(w-Vd=-eS46gfj0-dF{qiaj6OB+4gZPDc)d=)+O;PAtXJJD+^AMCbvKW^>* z+u-MmUwrT5;l0+wYm=*!8;7=AdzXhx;kLD7tH*xbvFv%uBj3?aW=pL{)^4xfhK&CU zztb_Y-8!<&ms*alytRIAgI_l`!~M&iy+F7)_U$)e6m0z(aqbJH<}(kPAGkNe=YQq- zci9Ou&o%Zh@UNkvuHi%I(INkE%=zeqdpPQR6m>&?JjzW(J*GF2fEquMpvPFMxERNp zF6)-Krd3t8a)q+u;~6~2N~q_)OTZ~PnOvslV2+`UHUux&mE)c|f}E?|gYe0x{+k>Z nDFqtKPS)9LYhS;*x>R<vLD`3n94}h}WgdC_-vrn!WkB^`JI*VA literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/image_indexer.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/image_indexer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8410629d237495e233b2abffafd67ea27622f796 GIT binary patch literal 4538 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 CXU{eO literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/image_processor.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/image_processor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3f13a02dd050f8b2a65cdd66a01228daefcc16c GIT binary patch literal 7366 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 literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/multi_retriever.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/multi_retriever.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6551e8367d184f91224a893fa630fcaed02550e4 GIT binary patch literal 6576 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 literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/multimodal_parser.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/multimodal_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e95c0f1559acb4e67bfb0e35b4fb195cbf5b3ceb GIT binary patch literal 6329 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 literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/query_router.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/query_router.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71e90d61d8d0a4fb6575120b6302c2c891232bf7 GIT binary patch literal 4611 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 literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/table_indexer.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/table_indexer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9489953b45595e442d53cae87b5c5111ef0248f6 GIT binary patch literal 4671 zcmbtYO>7&-6`tkpQsk17Mg3T`rN&cPsl-@6h1wLYUAvGirH&)JP2@VL>R_?n9Z4%M zx$4YPGF=K#i=fRRfbFD!Z8U+6qz4st0UwK=d<>8a1sRaAZ~!CiB|r~UrKX5aeKWgD zQi}V(0C#3*-rvl;_q{j#xVc$F@H{zv$Np6dLVw~P(IaNyWBYA*-9#3eMwVp794Q~0 zj!C>PJ91u`RzzQM)O>t84t;gH$%>QMOw4L}Q<+XAk(IF$BuTUbAje9Q6wE3!aZ7ts zole)j6)OeQ%|P8|rGciIC{_z;0ossrCSmOZj(w%pT)Xjm2_elhE;&TE3l_OT=<q^G z8`|?|CyTaYF>Dkp{L><#Hi15VY3${xDZEHb&!t#!V|S5ITrj*MHJl;GD9jd(S%QUp z%_7XC_JU`-1%?->YZXmm;Y+1pT89ynFy?XthBfW{9KpLd3`#J`Um}(TTuZh$hdm%+ zMxJ29afZ&?1$*-B(B!v<7DsX3wTOd@3@B$@s(Iv!Cv3O#+#J&gj2OhT_M%;w1wvR2 zgPubMu?gnc%p(O4Gm<w7o^4{bV3?rBq%LC`Xam)h5U$2>Y!7y07-Md5o_L03cm}AT zVw1Ru;qF?(0+TK33kDd^T+IYYgfbv5U`jk-SOjX|AXg02!>6V$U?FU2judc#5Lo!I z_8Ir8oaA?&F3!$^$uEIr6JlH7gTaI<js-f2an~&7VI5an_im6M2Yp3aFuAbkk)gwX z+r99*i9GO(;PGNn@4j>|9GybB*f~(5iLKTPa16N^<?Mb&pE!GRVq8D_gYk(;{oL5u ziQl1ZF5&6G@W|J{Ig~Fto-MX+h#IpajG7~R9njyW&k|1$j+?KGHUobr{1^lmT0xIf z_|f3;N2wDP<pfQ^kRLiR-!NYj&M5ZTtl17kY50j1j>0N#qB3Hs`YNs3+I}IG(Kxym zGh?&JjJ+|0&~nV<eBvp`tQZeKXyvd~f_7dFNg5hUwc__e8>}WP@t$%&8GYY~y*;9! zWf`P5MJ)Hi&a&bqqp|r2K%xO?%W_$X<SeUYb)Ij~o~K+}k%YW`(WIq$nW$fuqx1X? zvQlLU-s$(%XiP$7bqaPsg5Pp%3FVrNk2zdmc!bLZuDvHr0V0?IjvV1b01TdsEt_42 zz~<aPG$=TX2hJ}(>u4iB18)$nkB{O>2uAGA0K4h2An-4f62rYwT=fp(-YBek2ZaQ# zqyF?Z;7BhXFc^<E@cw^-J^>Et5Hq?^!3J`<VIj8eRu~K$x2b@>5{3W;a0=isSniDN zaEOG_C2D&BCgH+g6N-1OT1XzllK_^d<_x>A8&(-$D+rl$8Ngu}&w=2EMFrc^uy^)e zu;EfHz~~x61q&+Xz!Ms_+hzc<3tkC-y7FsWaJ@N*Jq$-D7B%dr8g%-JX#f30wEMoY z0QgF|yZfq`&MCAFzGyptb@1^-2%4o_%ujQ29RQc3EzE@Ic{Tf!P<D`!-oP)5j)jXn z(4OUWpZ@Y4-2YVS&n12Bg5ea&1f?#eJRp4;phVyYRrqJi40ZE*Z7+R|d#moafX2SM zKy9!@c1R%?ryOc1j~idHJ(BkoU}C;jM<qWUBtKo85{m<A;B}NEjh_wtCEVYJ>__`x znlFX@5nd~3s}H63KW@oZ4veq2OjHsR)hy~f^m%709j|tv=FCnzN@g|>eC1Yt?ZDuZ z9(?!UTF>BS)Mk6`ByK0}?qAKUXOC?59NtQ*`&&L&6G^QqgM^jwDuQO`AZq*SdZu^f z%;Wa#MtlESd;hAkI=|ljot4Q=rFBE;UQ@bnoq446|5-^^(tW?3TJ8Ao$9G?;jGX@H z;794o`5#rZ3y+l7H&a~|rR$5oJ`XY(-`{sePj)C~Em^<_9~Ce=2c5=5QUf=s4yW_{ zQ1(1!RF*91e(YWdx{Uy7$z=pErMzE*E`X^Rq$Bm}Uql2<x(0YZ6c}XDc8km@2{^LC zuo8zc)ZoWq;liEbKS!wGU_~Bp!~rp|U@%^HL{Sr9h~A(P@Z|qOgCKOzL-IWY809YM z7C;A~uvV@GiZiH5t{S||x|-V?NPKg+alGJbTzNm`?g-J!(?}2}ngg^!T+rwEq%2~A z_Hv5D@M8RQz~2LBZx!SXaZ3ka`V^--4zCrolilTjhu7K<ZzkHVpLwY4$gz(2%H&QQ zsahrd;)D4I7awLS+Nnp%c;Ib8hF5vzSrV#!>9WbSM-Q(6`1kf;kl3a9Mt=;cz^h_V zz=X+oSuV?v<dtixDT_2;K<FaOEh~^tmHCh=QovgneTJ{j*HU+#)`HR?((_+%DgP5! zLxE8*8OpUJZZ*B9LZPvsAX8pemz6M~i$0|EMDYVyst6p&P4yDga~Fm#KMQH|E~H~h z9I^<dG52yq+z5Ed&av-bXLfOdoQyw5@wwolfNK{8m}lH^F<dAZF{lZ3G8k2u4Sc>4 zZGn9ZbDslu2I?6~*ql=ux&#*$uQArc+$D^=94HRnpuPxoGv&1fFhfzw_;P~M;NJx3 zuA1bikd5)r!WQG*1;<go2GA*T>fWmnE9UbCEpa1icZIr~Fr2JjZgQiXV!?*JisIOU z^l8u7gw{Eul>8-5fl;tdu8kT?;+Dayl{za=%7QIW3JEbZp1DN|*C0L*yDB8O)P_rl z;nZrAx)^3c%@N5tif7=gJh2BsoisVh;np(>AaMXt3=ZNrT&SSfVRqhj44Ml?EsHiM z(H9`VL^a_z)DxoE$f=Z<w0lbpRhW=d{hm;y?UnSvt#p2Y)WtmQ;;dQz>V}uE3QHmV z^V&$DjnM3;V7K(pQG6Ln0SXrb^rfWV94Zh;$LJ`~yw2&a!D|IoM^XEsjkaTJZO8sF z@$l73+p+bw@2|W9S-$&gf6pY-@vT<W-1XL}>z&nR)N^Q~`^Z}Nk<D!Po%HSWMs{E= zJFt52!E5W;Z$ERaqwD76jmeFUzO|0NyBAhpUGErq+<EZUi|=0CPQ{bjwhRiYD5=K3 z*hbJ5w%;E6#>v(=zsh9-Z<{e}#)3Zsa?)oURdcO=lg=w9UH6~kv1;-<73#Kp9{kWN z4c9B8;07oHdM{BByg{WDWSG{PtYK!(!ChO_Yn0!6L>}@L2RsDIE~xx{8G=aEO(L)O zGPtGKT*@y;B0-2e^fd8ZC@_l-IYD{a;c0-~hF4XVBxx(ANy^`{NE(wqL8G6b=c{ql z*<V4KYFv`OS2-}eji9ZjQP&GwYQL1-Z0)#y{sxp$(0|g}UgcxJ5>IV6@!okUBW3Tl zR}nO;o!h*r4x^TRTTOjZd^6Qjm7#so)>T!Z1-?Y8%4ezx6xZJB`}y=v6264#{{SLA B-f92< literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/table_processor.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/table_processor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d1fe059dcf6b72999bd14aa499e4a667fccc577 GIT binary patch literal 6673 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 literal 0 HcmV?d00001 diff --git a/04-multimodal-rag/src/__pycache__/text_indexer.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/text_indexer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d420b32f9547949e982d3207ef527b00a75ae001 GIT binary patch literal 3990 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 literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/__pycache__/main.cpython-312.pyc b/05-agentic-rag-realtime/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..771414120503c8f1dae356bfb0e50df5e409ebaf GIT binary patch literal 9446 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%0DhmghVcGO0P>X<jk|sA7F&6J-t$ShUbCuf@ zE3hbg#0iZ?(Iq1{;TRb*;(}@{vW*~R5)DxXp=%NgC$gfPz>$PiB^j%!U!q_^sL)Ok zB(Kw`ZFPDQF52}bBwnRRl>sD-Bd5kRCygg3FlJ+fbQBEZ>0U`r)M+Q(i#!a;I)iO7 zJfpW_p>;SCRWF!`ogsjik(P+Bj~ouets@~r<sz~GDTXkS?unm28OKARH=PmkV=|}k zr}4Nbxh&jpq-7BJNRv*Z*Fi2f$V_$rG`Ze}qXuqd-JG1{Oc)b!q|+Ilf#YFKr#gHk zfdjxQ1xywkVC5=~wsvqZs9+o%>-o$x&lm@Y#^a*ofZ!eYseb`&`v}|_U+d3~6$h3B z9Y0He_u`^F{x=FgEmXGc`l#c>j(OMoPZm905YlpnuAMB?n=!|f{7>Ay^zSeJ_QkvB zJ_(nY!7_d75A>1yE!~xdmUmx$^VQ{+@Qp*)58XI={pbSyk?%v_t*c8dM;|&JAy@J2 zLqBq0Vr9DbkM!C5{;doCTd5`glSPcME;3seoJ-7OWxCt!Y5(1WAIBCCoG$Gj`g~W3 zxlpE``vblEJ`<`0L)We@26t9AZ7Frd7B}@)LSgX1e6HfN;I6cG+_-xEYN=^QaR|e- z{@uSF`pv*6@zU<0&mS*6f92lu+~V_G>C#wfY`nxwlxgvg^r8FywwvJv$E~4H=+FG0 z`pf=v#rWS`4s_J~&LjWoSfSu`i~bwFRyxgv`pVOGGxW=jfA09gv-3-D;GVbrci#3t zdAiCTmRzVjRrVbHGT6Lgrvt6aTedG8dhhDJE&CR?>|5G$;2QG{Lxs93jct{N_InLm z7aO)NHEb*GI<nYsWVtbXZL-|Bt<o6&%HMf&wCwL$*-SOGJlqZ%rxYA4drmFadfEGh zXWzFEw^89O<*wdR%U;ku&>gTymm7E7Zw!Od3A8>^(J%S0Q2SsX^;uuXsj%bokPV)? zCy^il&_u*e%=2(0s37oE4&tL59|3siF5`a)3Q;gLMs|%ej?r{-?se}lL}b(~{QJRB z1}JUF9s@Or#F{wjBhOJ#LAkVWTzM7fKprXeWq827l+E^yJ#3>N#3)<i?<w!^DdvBv zu5VnF&Hq|pCD?8Y-5gk<;I(k+0lpsed2RlcHjmA9O?yDWYo#@6+j{MVd*SZIaQ6cW jJ|0ZiUa;BruT0x1+CN|ToumCe?Rov=ii4s%h_m|tHo~N| literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/__pycache__/__init__.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e7f6386b66145250ac4fc4ba329dc17004e91f5 GIT binary patch literal 138 zcmX@j%ge<81YMr<GqZv8V-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K76+Z(ypMn4X$f zl9{Yql$fqtl$w}Rl9`*TUtE-|A0MBYmst`YuUAm{i^C>2KczG$)vkyYsFM+hi$RQ! M%#4hTMa)1J0J29O#{d8T literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/__pycache__/agent.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/agent.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c617cae9b6ebb256b931ab399cda581f7d621dee GIT binary patch literal 5121 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 GdjA8Mdlsbt literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/__pycache__/knowledge_indexer.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/knowledge_indexer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a005e64a0c042bac46a1e9058fe246a0264e6893 GIT binary patch literal 5590 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 literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/__pycache__/response_formatter.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/response_formatter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01b6b59879a47debedf1811ad4e4a5495c6385b7 GIT binary patch literal 5573 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 GvHt*9KE31s literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/__pycache__/tool_registry.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/tool_registry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c17f62196364ba07973c635688c012fdca3e38c6 GIT binary patch literal 4448 zcma)9+ix6K8K2pmy?DJ|UlKdnq!kBRsJGbbG|)goTjaz}>eR7=?NDVAT90SW?v6c| z<(ye>Hd;%rgdmnGB_N~*D%8MJQFzE>9}s^4yf|`D(@~;ErG4Pdkn)lzzVFOs*C9rX zv@>VVIp5`cm*4lDzxMTIBzV4Y{sZI2fF%7L|AbE}7(BiOgZCs|DoMJm$4uFZm0~iE z<7V7aN{SdOX2MFAl46`NQ&zf^7UQJZV`WMitGCoE{-?~W)mQ2h<FuKx`b+)RKxshy z?=c6hq0$hHGc>99(gS*y#;B49`IV&J_fw^GP|t-kur^##^nREfX$=$x8?P#ol;KR9 za2>}iGg>vc%UZ>HE0Z}xZI`Jg2`7nLQ(dBRPOTM_a-y0hQLC_tAT1{q=2)<Y8jMs_ z&2?CdR2sJC8jj72nata7UM7=dVfx(6;?lz9?`H@+OEv1Vc!`tQ*$W^_)j6T-6ljqt zOfTE2MVE<c>ttD{Tw_KZIWCiCjoL(~nxPwZRd8#fey&3_(5y(v=@waO7$z<YU41PL zn>aQlbyL+SX;Rp1pAyuBe75;}a>lWB0|fw&7BOtiZ0K}?UJ>8mperh;I&dNCx@wpz z7=uh+oFQwp1?R+L`K%VGF9Z%{29!<1tr5%7)+Tg=BcV>X+gRZQ7n}xa-_UA8VKgkb zp_}2f$_-FT)pcUiX23EykLhSCsttxdH$Oi`mgeVY$<*}XnT45)OEdFxi~b>(fcYA6 zDuNiS7uk~&(^#XC!-(!^5D1_N_}Q>PPjtQQfO{Ew%2x~A*CHGgH(}l8P0B)3zEUJ_ zzd5;t;Sh*&Xk?t@xHvrr3#7p*F<dgb&WprnF1%C-kNvHH&&=G#OH1VJ{K5rf>X15f z)(xHd+kU848+7ksKKt1(g={}Q>xQ<*#s4OAAR17HPdX2wW~%i%ntYEn_A;namlMsg zE1)kNbp>73tQlHO=mOlq>~r%=)1i@rsK($T^K-M8#W|z+b87~go4XKkzry3gHZU!* zRPaEfF4W`1VIq)4Kt*{(aG#%$lz}SX4ho?@Qz0tsuB$7CX}A!#+<wUwBB(yrWr(I> znt~yPcmq#z74{1Gu)RjjI!cB7$76w2m~+LjR2T9D<2H1^Z@qsY&=q7)nT3PkcL|IN z-^AI;nZ-pI^lK3KE-f>)T1JvW%o_=&nslYismipPSn-Yq^Ozasrxt<Jfnd2}*s873 zkXtz21Tk<JpVi^LtU4>eXCM@BtQr39>^eoDDsy;IFNq#Ds$P;&x4~>JhDQt@fJrG1 zzsG-;5!b8IMyxHb!uaV2(f)Qoxe<4fMm%k)9sgrIT2b1u)kL_~`L>nY0Z&<S!&)NU z4mNBg_Mu92QT2q}BC6y*RLKt2K4*!v*+{ihEbI1$yxXaeb|Y<P+`e!=l9}E|W;&#k zrF~>ZXW8g!_khg)kaxQ$q}|AX%)xLzl9|~@W+tQqnXyHwkZk-09F#$vp0T;B+OB~? zjwQzTyvUeJ1L~W|(f}?22_w9R{6J_E@B+Dytfo}~2aXw4zaYu{r~_&&ZzdUqS~0$& zYHL(4h*Za%Q}<Q{!4NCM7@$&}LP-LAf`S7zPvkAiX4ItXpnF(*An5@XDaV2gMPb<w z=H)=UNmk_{I1|zx_NNG$byOX|L}V_|>hub!AP|6*g~@Y5$q<l<fI*`QRQ@w%hzhR) zXb6;WM0tjDVma(c47W$$apJ1$P|nI~-6+G}aA6e64r!A)DAEOChHzoecbqt2mLT&j zwH|T#f5~;nQVqM@h53R%k{1Ll`^^Mc;H}emWEXaG-_Z*K9lFkj2#7UxR<)~VYOs+Y z77}M=70`u`fq!)Y*34;CYf!*fT7+Jy!y4Eedll%M=uaBd)`Dv94gI%vSC1IBOBu8! zpp8{m?G~2kh6Uc{1({(oc)7h6^+uj;qe71thl0kENRtGZhxlTM8Z3)1L>-pK7k0S} zQ@+s4FvD36US0yhoO?-MM=uWe?xg}~_LL@ItCtBB0k?W_z;iF<G%a+pmkM0Qk!_bp zpA?G|$6g&5`DAG0z;hgv#f8>m;#u9PcR;ju0>z`Ql@VY%3P1iAcx_6%qf-9p=Jak_ z${oC3+A3`y9@{RS-|2g6^X!96{^r<6C+=lVZ=SiY^!$AGo!J`)?kU4xrltO`?c`qA zJQvO$x~Jq}Hv8O8@AI2e;q1YC$_VfsI{e;|UmV$fZfs|8eDnM#slf-i7jKnza>usf zpXUlYxo=*JhZM*iDEf!5uWqew=U>^$9p60nNh<eX=<BztJ43~-)aOGl?+lGyONA6D z`BCq{=ImoQjl@RarF9n^bd)Gbe}VyAHRP(iA-AO|>BmzNmZ#V%%s>6CMg^&}0tGAJ ztH<%$pvSJldm|12{frW&W9h=!dFpvz(i83Y`_gZtq9IG~o>e40xf+$E4oO=MWdb0l zR0;}+Z1<B3@DKn9S`SPSzYi5qj4-(8c7T@y0o+}Q93b8$P65INkU_Hzt#}KmBh-eg zMbt1Ec$;xSkjh->zX19W{&c$1s8)eUC>FMRc+*iC2#sn1<U4ZJ8jK<Ka>}{ih+};3 z#a)p)r^x>X4F4kajQAdfwz$hkT?llc0jV->8gQ}ktp%V1V-!c~(C6WbkKk7aCL_gt zZJx+QhW$zded^y_uEtK0Cvv8c@Or!K>LtL6hRcRwpQoVZJ!RD~?1Ca<(@UTf?kULD z%T)}<UFh-vwV-F?*gv5?*jJEx81To>-7fx|jlr4*KaP<L`Fljlk?kzmoW4IW{4Xh< zc>S94DF6J&`NG|N;p6<o-TcIz_)h-YcYbg;|DBIsxSN0dTJC<j?|ODCdt?2k{(<pp z<K~Y(jD7gV_RFWXhrjo?^lQ5*U;<f~9k_mS>*S3~JH1D@m80ydAa1U3$V-*W0IKD( zmvmcrC198=UcU*rt|6`$g~b@2#Us&d835U8*wA+u{kkf;NzO2Lydgj@pR5=*!anfk zI}1;Byd4DgMmn3in_q#}-HY8Va0j-%108b2Wnpmv@}8nY*%o-|#UWyZ<^LcFLkJH& zNxx<}dc&k|u#<2Mga=-R*KS;v<$uQVvhwc}l6?3Ji9D2!e<2k=l`cP&PJAj|d?+2; zO@3D%ygzhkHxA!N*@4{ze0P&lGIQ<4cS?^__-|bvlApVE@Rt6&BfAm|@5H~vk5J%0 D76|YK literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/__init__.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c3c78a5617646b3a393f0bf71e7dbd76cd9219b GIT binary patch literal 144 zcmX@j%ge<81YMr<Gpm90V-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K7O?Z(ypMn4X$f zl9{Yql$fqtl$w}Rl9`*TUtE-|Uy`4nQ>-5!pP83g5+AQuQ2C3)CO1E&G$+-rh!v=x S5r~UHjE~HWjEqIhKo$V0CLuil literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/finance_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/finance_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..271ead4ccb2634ca709cf4d89ab9d7d2e6c6eaa1 GIT binary patch literal 5178 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 Cce$GY literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/rag_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/rag_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f9c31aac93fde0fddf71a3bb1590ac0b465f060 GIT binary patch literal 3232 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 AH2?qr literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/weather_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/weather_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9f053cfcc07495170c14c33366bbd78877991b0 GIT binary patch literal 5231 zcmbVQO>7&-72f5pD3T&AMY8p0*`7$YWjYjP$98@cE3P6-v19$kk}KFnK(BX4;!4Y1 zW_H(#2rBR)Ko#^*8%<#ui4h)x!a;J-sqisPgI?MKR-D4@A_ao>lp9;SK#*Sg-YiK` za$K}sfHS-E^XAQ)_rC89e;<oQ1bDtX`=M?$3&P*<qw>h*mxnLF%S}NOE(w~b1q|_$ z2(@GcGSVeU6f~L2S{;?<WG(oPe5sDV2ef*44=xAdA$K_-2oam9{f=cBcK;GpoHS)A z{7Pn*Bau<+sA*~w)n$yD4k-^Hn&K#Aj#(K3<H=-}nzR1YaV1NJrp_iKku#ICWai8W z8G3bOVunl&O;3+ZLv?;=d}L<i{PaMCz%v80m3ffdYm>3Dal)vg*<2>7Ks0KrOwT&H zWhMxH%VCP@_}z>e^r`|H!Cc*0=EAD_xHe@V@J*N6IF%ft$@wJd9<xl%GP`}fyJyo1 zu8R6scBPxdA9JcZp5P)CQzKc$w!s9tLll4bWffMuOl*hg<~(sMkfB2l+RWb(zJlll z>k5GmThC;TW$5=Msw&MoFhy}H5^xbl9haFzA!#?Gn0;sm<%&U}-F6KJ%z0tr>`d8c zvy{LNNrr*~DmnD|`f!3QX8O}{a%CAzHmAT0a)qi2jsmqo2iMfEQf4a#DB_wLyn>Ee zl52E!jx1X)Lz&Db3WGT&H_L@->>#RQ5;d)u^ORf$IgUN(uY4J|UT%RcQl=}$WiGKi zAN+t^I6sEN7*#1~Kt5j|CAi&8Hl2)oi%vWd7d?3f4Cb|3;6~-6$SB#AlGRg-blD5~ zwo%2h&tp{wKM8&hPebuji~!MrH~-NBo9}b-sxSu;@lJF_bZ`cLG&wJ7b)N=5DYr}u z@p@$n7U1f?;_8M5M==I_9!@K;8`RG~GGCX+1A_bB);NGeLl+-qloCg$Y=As>1Y-jX zYCx=IbklWcf|wRj8P(vNRE41<yy@!j?VruGK5r5?OTbyuj+3<q`un$9DQU6!ez26k z))1Sw{lLPNw~nmXbASwytBRp(KM=MA>|Iv-Ja@<%X+uP{%sGAD1?9GzX3k>OATlg# z(aqYv{+^7n!tl7ZoL>wYTHyEaZ2+KOiZ_M4uuyxx{FDUYlXATR=bIPa7qq~;JD`+q z*R6<QVe>o}DtK5Pj}@G-{At}MARnKVKt8Y#^xtbsEx33{5Li&F=ki0hrD*|1hHJ<8 z?-gA{>~DB-|B95Cv~ULu&>~o_$kp!#{#{r7j`Hsye8>3s9lUJBase&0?Wt(6QV%nl z_}HhgTnXhvT5}GJyb{h!3ux?ZkJgeGs+Nd6Va!gwI_3vu?9y7RG8*y?tUe!svuMjV zyf56AOfer>iE8cnD71CtWv%mlS=*hj<8@cQ9$NS0gHZ0xhk4t++p%e(M{vsL13TWa zO=<XCXnMSDzre!4Y=ocJ)3+rxa8Z~R9AHTQf&S)|nAXkvd-5^RPR23X{*~C<u%=%x ziA%y;(nVoOj301+SBZR#nqi1e4u4ytnRIXCz;2)(Eo<q(LFoPpnFoLaqy&F2hY(7u z5?qBwkZ*7dF_jDjP6I{*Oa^8F04O6f03`pc@EL~@6t<n${(9{_oR(3R0q(2(mLS~| zbctNF*kXcQnC^z}DTV2Ff{dPr4`6gZHc{fXJt~}>Y?85TfFmoD0rKR^!VWWv13_KY zWh>O6EF3sMhO2uk^JA7-S9J|A1IP(C&*{EK&|^S@vW9>)&3WLzZFE>+YTM0Z6t-Mu z(9Qkio~^(nU}ADB0CK3r#HIh)<cUhEbL!2(T>U{Mo7J_!T<l;izkQ487qC;kqi3k) zI{JIyCb_O*%LR5YEjV@^n5?1^`ao}PqBlvV45|R}Lf)h<@*Qs;_aO&#Ry2+04d@7# zYhsgXX}|<LOX;niRc}1$$r+#@Pj+Y~>qX`$HSCn3FH$cAcF}<|mc9D20t^GmYpBVX zJb4Ke^y=&^rP}--|A0^QN1lw1=SlXm?bVmp_k>&^L2?bVeh!EHp9>6-oRmB|=VAy# zia`$X<q0lNTtrf6jKi#Gf}ANU15?5W5@Z&Zh1WWsl;rkRq(XXeRacP<GvEziP#!nE zxt*If<K~%%Ehlqi)6T%TOs9}NY_)X+=8Z6B0kKZRBd|{xmGweMppf_3o-|J#ufcW{ z$F);vVXuiPx=mAXR4KpNlNW5u^dc&FivygPqh5r*g$Wv@jbNV{|Kz>!NTo5}$m-G1 zlFJNS%g(|;>dEL=EDAjgftw+gF|?c)!$U~%Ge~7@FT_6=Gifi3Zjr*nV7qW&$gore z*YzY&fgz*WjZk1XY(Ce%KbGu2a-xq%k*@YJWge471C84M#6;d}09&NW=F1&|d_!?) z<=fBpPvY{1{c^Hq<bKrQ*WmTvT@y-OLetKl=ibfzEWaw3WHHuUY;L{YdVT5Mz7zNM zKDVAJNn&h7EJ;E*T8bP9kBMKm?Yo)3kuMCdH-FW3eDzEzC^WS{XzbZ&>?zn^HXi<_ z_1X2~>jz6h;Keu;yW%BDeyX_?6=J&{L=S944}2BvEw=2wIe2667pGQF!}Oi)Yrz{Q z3&D++-qq8^jy-F0x4H_(M#r($*NSbs*G6Hn*EZVvSI-nX_pUA9>c8{!MrY#A8ylT3 zJ?I?W=p4P@d1m!ov5OR1Z;jn)+vqyFI{r=D{(`;Hb{OWgcYPST6)Lpe*?GS`xq7zP z+Icg5BfaJpuH0{pKWOdSXzjbB-)+9%dUEyEKOcW-Exxw9@JeB+pl`Gt`K*KQuVwFA zdhJqy6<gZYr0a+8btUdxx+~pFoV+_)Y}tFguOx_MPjSzIwb$USx1-qcbfIT$M}d~Y z@>6{!DHz-F-J>RMrS6T!?gx#BHyRKBLlDDLVsY2;^_lh2djk{qS|-1YPJZ3kjvZsr zabjIt|MA^{&(zN@+-o`ar|7wV!BIfZ_x52pzYm@n9u$5PdgVA&za8v3eNy_OJA~!o z`qR%#UmO+j{rNg584l0=>QxHRzwNq*DLX_BTq%GT5a|>XKnwggNzR&C*BS6xf>Rp5 zY$Zsqf0ODZh>Nv+1h7WRm`@PbD1!iH0DqO}hBOn94FfQN@Xui`!EX&1JA{W)@?(i% z{^-ebuBq}!`#1og<}ijSeLTS9hwsQnV7yFN9EZ33fa|k<=xK$YeF6&Z>Mz!fi68Wo z1gt;m`6sU*T@{6BQ!&<AjJ22KK<st#Q8*M0mV{7U@F7eTR4gh_^@8#;wEU;*30zI& zm6}u*t|r1`SCgZa1jRQTBpVqE$faU2yl-N10Bn?o6a;P*+jt#s9gy1d+bG4`BmxSi zCR_~QE8IP8k~QGQsKc$Z4|90rD1Obi@q!B%5G3z87m_26JtV7VsiB)dVv5P-K?>pk z@S+yFlSX_;FDWl=m#@YZ#3U-PB!kSyP_6*0K^kLIV-E8mREf*0EbiZ6O&|cquOR+L zVF|b^DMr-@x(SX6NeVcQpAT@Le*Efz{=u&vNq#c_?ZpEx261V^9fw&z8J`?JM@|pT z3=vGhs`nx?IW;mdJ2C`+dz>F1no7amQ|Cr5@`$O`^$jw+$+TSJ06F?X^2m0YJ^DOv z1D1uP0MkU(uv~4qmT;dtQ6=1?$CF3={{ckS23p2NZUGy7M<~006pdTK{amsf47H8h z$e^~%208XYD6R>`#+Di?XzWD1ZAEZ9S&{;=K>+e_gdKsQKBnCKA2DTOa%N<JoOewV zoe;7SR09f)l0)F!6>qNET?0t@e{hIBgDUnx0k%2@RjsYspJW|&97ZnSXxt$%V{@=B z^1-o^0M$pw9%2O`UWSSego@!BsNiOa*L(G;lxC@^6l(_|UZVm3HNc-NkojOR+Z@Fh zdMHOHzU0S(Z&C!Z??WfS9IioMX7~>v9x+ERKz>pDd!SL2A9f33(_aKq3cfCi!F8!5 XV0~A9$m>!%&>;p3`yUBVak2jf!;00G literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/web_search_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/web_search_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5f0dec39beb469ceaea81305cca20af6b3ffcb1 GIT binary patch literal 5678 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 UUCW{^UktQLp@r0a0*#pSe_tsnga7~l literal 0 HcmV?d00001 diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/wiki_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/wiki_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..545e7cbb01855d630b0b4ab03998cc059be15000 GIT binary patch literal 3441 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 IXb#!`2Y>qIQvd(} literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..5274624 --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# GenAI Beginner Projects + +A hands-on learning path for developers new to Generative AI. Five self-contained projects that take you from basic RAG to agentic systems with real-time data — each building on the previous. + +--- + +## Why These 5 Projects? + +Most GenAI tutorials show you a hello-world demo and call it a day. These projects are different: + +- **Real code**, not toy examples — each project solves an actual use case +- **Step-by-step comments** explain *why*, not just *what* +- **Progressive complexity** — each project introduces exactly one new concept +- **Works with OpenAI or Ollama** — you're not gated by API costs + +--- + +## Prerequisites + +| Requirement | Notes | +|-------------|-------| +| Python 3.10+ | `python --version` to check | +| OpenAI API key | Or run Ollama locally for free | +| Git | For cloning the repo | +| 8 GB RAM minimum | For running local embedding models | + +--- + +## Project Map + +| # | Project | Difficulty | Key New Concept | One-Line Description | +|---|---------|-----------|----------------|----------------------| +| 1 | [RAG From Scratch](./01-rag-from-scratch/) | ⭐⭐ Beginner | Embeddings, vector search | Build a Q&A system over your own documents | +| 2 | [Legal AI Assistant](./02-legal-ai-assistant/) | ⭐⭐⭐ Beginner+ | Domain prompting, structured output | Analyze contracts for risks, clauses, and conflicts | +| 3 | [AI Research Agent](./03-research-agent/) | ⭐⭐⭐ Intermediate | Agents, multi-step reasoning | Synthesize multiple research papers and find gaps | +| 4 | [Multimodal RAG](./04-multimodal-rag/) | ⭐⭐⭐⭐ Intermediate | Vision models, multi-index | RAG that understands text, images, and tables | +| 5 | [Agentic RAG + Real-Time](./05-agentic-rag-realtime/) | ⭐⭐⭐⭐ Intermediate | Tool use, live data APIs | Agent that combines stored docs with live web/financial data | + +--- + +## Learning Path + +Follow the projects in order — each one adds exactly one new layer: + +``` +Project 1: RAG From Scratch + ↓ (adds domain-specific prompting) +Project 2: Legal AI Assistant + ↓ (adds agent framework + tools) +Project 3: AI Research Agent + ↓ (adds vision models + multi-index) +Project 4: Multimodal RAG + ↓ (adds live data tools + planning) +Project 5: Agentic RAG + Real-Time +``` + +**Skill progression:** +- After Project 1: You understand how RAG works and can build basic Q&A over documents +- After Project 2: You can write domain-specific prompts and structure LLM output as JSON +- After Project 3: You understand agents and can build multi-step reasoning systems +- After Project 4: You can handle documents with images and tables, not just text +- After Project 5: You can build production-grade agents that combine stored knowledge with live data + +--- + +## Quick Setup + +```bash +# 1. Clone the repo +git clone https://github.com/your-org/genai-beginner-projects.git +cd genai-beginner-projects + +# 2. Pick a project to start with +cd 01-rag-from-scratch + +# 3. Create a virtual environment (recommended — keeps dependencies isolated) +python -m venv venv +source venv/bin/activate # Mac/Linux +# venv\Scripts\activate # Windows + +# 4. Install dependencies +pip install -r requirements.txt + +# 5. Set up environment variables +cp .env.example .env +# Edit .env and add your API keys + +# 6. Run the project +python main.py --help +``` + +> **Tip:** Each project has its own `venv` and `requirements.txt`. You don't need to install everything at once. + +--- + +## Glossary + +Plain-English definitions for terms you'll encounter in these projects: + +| Term | Plain-English Definition | +|------|--------------------------| +| **RAG** | Retrieval-Augmented Generation — feeding relevant documents to an LLM before asking it a question, so it answers based on your data instead of guessing | +| **Embedding** | A list of numbers (a vector) that represents the meaning of a piece of text. Similar texts have similar vectors. | +| **Vector store** | A database optimized for finding similar vectors quickly. FAISS is a popular local option. | +| **FAISS** | Facebook AI Similarity Search — a library that stores vectors and finds the most similar ones very fast | +| **Agent** | An LLM that can take actions (like calling tools) to complete a goal, rather than just answering a single question | +| **Tool** | A function the agent can call — like searching the web, getting stock prices, or searching your documents | +| **Chain** | A sequence of LLM calls or operations linked together. LangChain helps you build these. | +| **Prompt template** | A reusable text structure with variables that gets filled in at runtime. Like a form letter for LLMs. | +| **Hallucination** | When an LLM confidently states something false. RAG reduces this by grounding answers in real documents. | +| **Chunk** | A small piece of a larger document (usually 300–1000 characters). Documents are split into chunks for embedding. | +| **Top-k retrieval** | Finding the k most similar chunks to a question. k=3 means: "find the 3 most relevant passages." | +| **ReAct** | Reason + Act — an agent pattern where the LLM thinks about what to do, does it, observes the result, and repeats | +| **LangChain** | A Python framework for building LLM applications. Provides building blocks for RAG, agents, chains, and more. | + +--- + +## Using Ollama (Free Local LLMs) + +Don't want to pay for OpenAI? Use Ollama to run LLMs on your own machine: + +```bash +# Install Ollama: https://ollama.com +curl -fsSL https://ollama.com/install.sh | sh + +# Pull a model +ollama pull llama3 + +# In any project, use: +python main.py --model ollama/llama3 +``` + +> **Note:** Local models require ~8 GB RAM for small models. They're slower than OpenAI but completely free. + +--- + +## Repository Structure + +``` +genai-beginner-projects/ +│ +├── README.md ← You are here +│ +├── 01-rag-from-scratch/ ← ⭐⭐ Build RAG from scratch +├── 02-legal-ai-assistant/ ← ⭐⭐⭐ Legal document analysis +├── 03-research-agent/ ← ⭐⭐⭐ AI research synthesis agent +├── 04-multimodal-rag/ ← ⭐⭐⭐⭐ Text + images + tables +└── 05-agentic-rag-realtime/ ← ⭐⭐⭐⭐ Live data + documents +``` + +Each project folder contains: +- `README.md` — what the project does, how to run it, what you'll learn +- `requirements.txt` — all Python dependencies pinned to specific versions +- `.env.example` — copy this to `.env` and fill in your API keys +- `main.py` — the entry point to run the project +- `src/` — well-commented source files organized by feature + +--- + +## Contributing + +Found a bug? Have an improvement? Open an issue or PR. + +When adding comments or documentation, remember the audience: developers with 3–4 years of experience who are new to GenAI. Explain the "why", not just the "what". + +--- + +*All projects support both OpenAI API and local Ollama models. You don't need to pay for API access to learn from these projects.* From ca9e847e8515ed1e478be88c3a0bf63ef8e95c8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:18:28 +0000 Subject: [PATCH 8/9] Add .gitignore and remove pycache files from tracking Co-authored-by: nerdjerry <7092764+nerdjerry@users.noreply.github.com> --- .gitignore | 52 ++++++++++++++++++ .../__pycache__/main.cpython-312.pyc | Bin 9730 -> 0 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 134 -> 0 bytes .../src/__pycache__/chunker.cpython-312.pyc | Bin 2474 -> 0 bytes .../document_loader.cpython-312.pyc | Bin 3272 -> 0 bytes .../src/__pycache__/embedder.cpython-312.pyc | Bin 2865 -> 0 bytes .../src/__pycache__/generator.cpython-312.pyc | Bin 3694 -> 0 bytes .../src/__pycache__/retriever.cpython-312.pyc | Bin 3239 -> 0 bytes .../__pycache__/vector_store.cpython-312.pyc | Bin 4802 -> 0 bytes .../__pycache__/main.cpython-312.pyc | Bin 14661 -> 0 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 136 -> 0 bytes .../clause_extractor.cpython-312.pyc | Bin 6758 -> 0 bytes .../conflict_detector.cpython-312.pyc | Bin 7817 -> 0 bytes .../document_parser.cpython-312.pyc | Bin 7743 -> 0 bytes .../src/__pycache__/indexer.cpython-312.pyc | Bin 5899 -> 0 bytes .../src/__pycache__/qa_chain.cpython-312.pyc | Bin 4494 -> 0 bytes .../__pycache__/risk_analyzer.cpython-312.pyc | Bin 6821 -> 0 bytes .../__pycache__/summarizer.cpython-312.pyc | Bin 5650 -> 0 bytes .../__pycache__/main.cpython-312.pyc | Bin 7387 -> 0 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 132 -> 0 bytes .../src/__pycache__/agent.cpython-312.pyc | Bin 5448 -> 0 bytes .../__pycache__/gap_analyzer.cpython-312.pyc | Bin 8149 -> 0 bytes .../__pycache__/paper_indexer.cpython-312.pyc | Bin 6728 -> 0 bytes .../__pycache__/paper_parser.cpython-312.pyc | Bin 8050 -> 0 bytes .../report_generator.cpython-312.pyc | Bin 8386 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 138 -> 0 bytes .../__pycache__/compare_tool.cpython-312.pyc | Bin 5203 -> 0 bytes .../__pycache__/search_tool.cpython-312.pyc | Bin 3832 -> 0 bytes .../__pycache__/summary_tool.cpython-312.pyc | Bin 4349 -> 0 bytes .../__pycache__/main.cpython-312.pyc | Bin 9526 -> 0 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 132 -> 0 bytes .../src/__pycache__/generator.cpython-312.pyc | Bin 3833 -> 0 bytes .../__pycache__/image_indexer.cpython-312.pyc | Bin 4538 -> 0 bytes .../image_processor.cpython-312.pyc | Bin 7366 -> 0 bytes .../multi_retriever.cpython-312.pyc | Bin 6576 -> 0 bytes .../multimodal_parser.cpython-312.pyc | Bin 6329 -> 0 bytes .../__pycache__/query_router.cpython-312.pyc | Bin 4611 -> 0 bytes .../__pycache__/table_indexer.cpython-312.pyc | Bin 4671 -> 0 bytes .../table_processor.cpython-312.pyc | Bin 6673 -> 0 bytes .../__pycache__/text_indexer.cpython-312.pyc | Bin 3990 -> 0 bytes .../__pycache__/main.cpython-312.pyc | Bin 9446 -> 0 bytes .../src/__pycache__/__init__.cpython-312.pyc | Bin 138 -> 0 bytes .../src/__pycache__/agent.cpython-312.pyc | Bin 5121 -> 0 bytes .../knowledge_indexer.cpython-312.pyc | Bin 5590 -> 0 bytes .../response_formatter.cpython-312.pyc | Bin 5573 -> 0 bytes .../__pycache__/tool_registry.cpython-312.pyc | Bin 4448 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 144 -> 0 bytes .../__pycache__/finance_tool.cpython-312.pyc | Bin 5178 -> 0 bytes .../__pycache__/rag_tool.cpython-312.pyc | Bin 3232 -> 0 bytes .../__pycache__/weather_tool.cpython-312.pyc | Bin 5231 -> 0 bytes .../web_search_tool.cpython-312.pyc | Bin 5678 -> 0 bytes .../__pycache__/wiki_tool.cpython-312.pyc | Bin 3441 -> 0 bytes 52 files changed, 52 insertions(+) create mode 100644 .gitignore delete mode 100644 01-rag-from-scratch/__pycache__/main.cpython-312.pyc delete mode 100644 01-rag-from-scratch/src/__pycache__/__init__.cpython-312.pyc delete mode 100644 01-rag-from-scratch/src/__pycache__/chunker.cpython-312.pyc delete mode 100644 01-rag-from-scratch/src/__pycache__/document_loader.cpython-312.pyc delete mode 100644 01-rag-from-scratch/src/__pycache__/embedder.cpython-312.pyc delete mode 100644 01-rag-from-scratch/src/__pycache__/generator.cpython-312.pyc delete mode 100644 01-rag-from-scratch/src/__pycache__/retriever.cpython-312.pyc delete mode 100644 01-rag-from-scratch/src/__pycache__/vector_store.cpython-312.pyc delete mode 100644 02-legal-ai-assistant/__pycache__/main.cpython-312.pyc delete mode 100644 02-legal-ai-assistant/src/__pycache__/__init__.cpython-312.pyc delete mode 100644 02-legal-ai-assistant/src/__pycache__/clause_extractor.cpython-312.pyc delete mode 100644 02-legal-ai-assistant/src/__pycache__/conflict_detector.cpython-312.pyc delete mode 100644 02-legal-ai-assistant/src/__pycache__/document_parser.cpython-312.pyc delete mode 100644 02-legal-ai-assistant/src/__pycache__/indexer.cpython-312.pyc delete mode 100644 02-legal-ai-assistant/src/__pycache__/qa_chain.cpython-312.pyc delete mode 100644 02-legal-ai-assistant/src/__pycache__/risk_analyzer.cpython-312.pyc delete mode 100644 02-legal-ai-assistant/src/__pycache__/summarizer.cpython-312.pyc delete mode 100644 03-research-agent/__pycache__/main.cpython-312.pyc delete mode 100644 03-research-agent/src/__pycache__/__init__.cpython-312.pyc delete mode 100644 03-research-agent/src/__pycache__/agent.cpython-312.pyc delete mode 100644 03-research-agent/src/__pycache__/gap_analyzer.cpython-312.pyc delete mode 100644 03-research-agent/src/__pycache__/paper_indexer.cpython-312.pyc delete mode 100644 03-research-agent/src/__pycache__/paper_parser.cpython-312.pyc delete mode 100644 03-research-agent/src/__pycache__/report_generator.cpython-312.pyc delete mode 100644 03-research-agent/src/tools/__pycache__/__init__.cpython-312.pyc delete mode 100644 03-research-agent/src/tools/__pycache__/compare_tool.cpython-312.pyc delete mode 100644 03-research-agent/src/tools/__pycache__/search_tool.cpython-312.pyc delete mode 100644 03-research-agent/src/tools/__pycache__/summary_tool.cpython-312.pyc delete mode 100644 04-multimodal-rag/__pycache__/main.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/__init__.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/generator.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/image_indexer.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/image_processor.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/multi_retriever.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/multimodal_parser.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/query_router.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/table_indexer.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/table_processor.cpython-312.pyc delete mode 100644 04-multimodal-rag/src/__pycache__/text_indexer.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/__pycache__/main.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/__pycache__/__init__.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/__pycache__/agent.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/__pycache__/knowledge_indexer.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/__pycache__/response_formatter.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/__pycache__/tool_registry.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/__init__.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/finance_tool.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/rag_tool.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/weather_tool.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/web_search_tool.cpython-312.pyc delete mode 100644 05-agentic-rag-realtime/src/tools/__pycache__/wiki_tool.cpython-312.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c487f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.pyc +*.pyo + +# Virtual environments +venv/ +.venv/ +env/ +.env + +# Environment files (keep .env.example, ignore .env) +.env +!.env.example + +# FAISS indexes (generated at runtime) +faiss_index/ +legal_faiss_index/ +papers_faiss_index/ +text_faiss_index/ +image_faiss_index/ +table_faiss_index/ +kb_faiss_index/ +*_faiss_index/ + +# Generated reports +research_report_*.md + +# Extracted content (generated at runtime) +data/extracted/images/*.png +data/extracted/images/*.jpg +data/extracted/tables/*.csv + +# Jupyter +.ipynb_checkpoints/ +*.ipynb + +# macOS +.DS_Store + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo + +# Distribution / packaging +dist/ +build/ +*.egg-info/ diff --git a/01-rag-from-scratch/__pycache__/main.cpython-312.pyc b/01-rag-from-scratch/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 09156204c4dd7a25434f9a9f2f0b8a52d732ca61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9730 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 diff --git a/01-rag-from-scratch/src/__pycache__/__init__.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index e73f4b018d0adba582c8acf304fe98c99c48fc2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmX@j%ge<81WK;+Gxr1O#~=<m_{;(nna)tjpvmaBlA(wR$omXZ^Gj7v-@s6}C^20( zttdZNw>Y^du_QS|zqlw_KR!M)FS8^*Uaz3?7l%!5eoARhs$CH)P!}T*7lRldnHd=w Ii<p5d01R;*6#xJL diff --git a/01-rag-from-scratch/src/__pycache__/chunker.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/chunker.cpython-312.pyc deleted file mode 100644 index f47f95cd2b0721199e129f3004d89ec9396891a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2474 zcmZ`)Uu+ab7@xV_yWU;zyh;sFDLPV!ccvT_>Yp@iV=WMpmV(rfh{0yNJH1=j+r7-} z()PTX#$e)u4;YP*0HN^-Q%y|t!2}-qNaD*OO=vbe81zBjPFoZ0li%#^?j058Zf9n{ z{pS0A^ZUMU=I3-eiJ*-ixuO5H4WZw}OVASjWIYd)%Sb~SHc$zl!5Agrr^U4hjc5{; zG#Q>Kjm*Tg*v04>dA==|cqA6*u=~O(sydA87wBkFVTx)~_AZ^XC(DLz!`w}1mg<zK zX>&KJ79I2a6xS=%O*iJ&1<DMi?8X?i9cHSM4@nFHo_JqBjRia#`bN{R9qLulvM;}i zw8(WSc*CvwQWe8jxf;2OuFEYN(ZFIAS0e?aMZd;&B#?a@Tb3L11*s~XYYXJfvFUq3 zZb8@9f-dw1{^Kq3R@8~>(H2b2T_)`yKuBx9h^q2owCp<%^zHTlO^fZ&r<PjwcT<58 z5TslAsmhZm_l)urP?;pqyoeEn7&^C!HABV~vj8F0O>#JhAkj_RBD|y+24%#HAkQbg z)khSy*pLvN6W!+2m;qz|H$trGb5yl^!B&wn-PX+lv5S<HsI6#<t$5bJy^vW<FX*OX zkRXRhmRn?&5^n2;LC%}jESW9p;CV(jD4|UZs#2n4hFPVYk8gw(_<(0y>@~2*Nmgt( zHz0&${HZ(lL4&%m87`kPEJdRXe0b}jYi2mf0$IM{<I0>~a!SN>O4C3b>`o18g_CkX zWu6?SGm2x_1LU>7zO6R>N(s$QJjoVBC(xUw_AI3)Q7w~0h1yV{VZ;KoPacm^qYSVZ z<&=p<mT5yl6U8RemSbuP(<y%pt55MMuY_Az7Ss-q5t4Pd11U34N)Cj|u*?E*7Uo%y zPESphFvaGSQrV!*C^iz14Ed~wLCuD<-j;w20ja^ePZEf7Vd%;X0+yZGGs>u9Q;mFR zP&3<bkz4So+p|@@@%{b2v-z@8pi>@K5OyH82Xc}@C3zYzb0Rq0;;XqhNPe+Uv>w7m z0taHEKs*!%hiI63o+&2-<)5tI_~H^76-TS6rAj8$$?{x2pReo$gOk)2mBt5J38{2! z<s&CoBFW??hCgvpPG;hSWHIR6B&PsDu`R~k3{X?Bi&Ha>soJ__Iw{b2c<mDsa92tD zgn2bcDyddX9-t98_<^y88WC5*0QpD_4?nOc4&OuYtRIDL3}Ltes_1MNLJPQg*jDkm z@W2K&zb*TAzQUI=MxX7H(LDN2n#DOhk&Czzpm$S`#Vg`Q@_jB=xiCh^=HC2)zE}4H z!2P1$_j46Ct`-k)rXKKuMp?c*znK^;P?OG;*>Gi#C!PuuFF$Bls$%fre5g|d(;VQV zKhQsm=$B0YP5h;FY2@<6=M(okdhTZW|K(!KAHVn#ekAZ$+JPMZM&-Gf8}$;DiI6jK z{ka(~?%1YJSK>~|l}gH7F3N<dmgvPjU2z}B8d=9hWFavKVV$0_EgN9nAbkTKeha!q zwDCHMCTpqgYm-Y|S3jtA_uf2FOAfBeLl5LcEwy*)%+h$R@2!>HE16pI<f{D6MjUnI z*E)BvZSQ&5`N~ENb#-rK;vKQYWA&ZrneF$}`|hUqE$R2teTzqbmlL0kU+cVb`s%JL z>7}zj9{PU%hvC}6w^w>=$rG#cNpMk*;P%vmR0e3ed3rU|e=pU4Te>~E61z2a=h=I2 zj@?a-)!N7JjMXKacn@z#QZ!aiqSUs<<Ldw)LD=;TAobBfc*#ZG9R^%(z^-oQMO{t> z>@<j##lhuDa2$9ne1q6-SmyDx86PZJnq$ym)&nvTCq4un_{I3|B1U)=gX0~~uXXME mv#lR*Tg!CTB^ZD0=&VO!tjExEJL=*~J&t0@jRed*^Zx;pai_}w diff --git a/01-rag-from-scratch/src/__pycache__/document_loader.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/document_loader.cpython-312.pyc deleted file mode 100644 index f1706d7b8b682588fa9366b7791ec97307799555..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3272 zcmai0TWs6b89o%L`^A=QwJ{tgr@ka+Ytv4LqMMMob7On;mbva6V2iWBU_@GyO_9Rk zVSSkt1VcA?fPoh6L*1o8JhZ@4bin%PJ?&x0Fkmlq?ZFw50zuc8Jx#3*Z17(8A4-(8 zq}dKYo}WDb`TqNV&Y$D)2!e5G@=jr<1EIgM4{r!{;_y5W?;-^$oQCGv?9;fSZ{Ei- zn%DTEFfSDS^ZsIBJ^=p&HK_R2(4wFO-Vo-)Yn&MTJA;}LIY*e4X<p===<M3;#MC82 zR#f6dE~~4wPA4Xe?CSHh@f}iRDlaGn;sl6FEuv?+x>{@s!NbFII12A~5pArM(Z_YV z!j-ut#`DPdxc+UnQNOpOaAkB0%)8BdyMM~%P{l_B9tUL--9cT)F;d~*=PNvo*Zv~} z^4dJQIV*yP>CF@9+NTIrgtD-dZ1Q<jLqBe_6F5@d|K_}uZqgp5_&s#OZH~ger@WQs zywugCJ<5|-@t6H&{&kM@&~9%{34FbL@Kmtuhj-|9Ln?<hms@s*pS1I~&(o7bVN~YJ zex_6<;6vpA?eQd+AM;rySPm|o_STzoL;p~V^ay>%<0ywbd7y8!8D9>SeM@~FS95Oi zgGKuSYC&pfuCy%=B1Doc65HMUN&Ywf9BSc6eI3#*)asA2Zw~b#+V5%8kI0!yxCJBj zB(Ey*au~20UgOXeq$Dcg8{xOO%@=Fb3b%q*`73CJ6FcN_D2xb(!Kx3-nublQR5A!v z6|5LptElSK#EZlzVi`jbs#$7~H7Vlm`Jt>Wo0#T_Vde5Lt2kG9UDca>LwK>EE2fJp zm6XLHEJ0xpVOdwO1n{e{uBqmbi>84FCRXKa9vixfEwiBKTu`}GB1VZ6WU6Auq>>s! ztsMY5UesiYwSq~pu~@T4)^iv0azV$?O<W_eaeWDtH|wg8lbo4xS())(i@}2?C1M7% z9)+plN?$ZIkchK}P63bUv}RbO=^Uwn!jM{rD&?dh+{?g6Ou1On)CI8F?5#QP2W~TK zQG)58!Q-AC_|lVYz>~~NUVLy~Zh@(3FpNubPF-MHLj*IfB@~)0hKI;m7G5`~ENCnS zj~FqGiz=0w<{5})mbRJ?t}cQjidGT-q}K#rWYyjbKF?4VO5nJ$O3lE@RoCVgFE{e( zf*VKXjaq^YKJFy3p%Fx0r-KdNtE1PXR@+1{xDUHbCMNO<W@i4c&7Ng!BO8s)E!I4y z&j?XjRj?%Pg_tr0!06G6D`>ra0zB1Bdwh2NduJg$s)==jxf~)Z;aN?UO%-Q}$`ZLi z-71h31)D}u#jWLGkk;yuM1Eah%kYNkS@*k@<8aR&bCUHe^azYU|K;{ScHYEzZn;n@ zu@trRW*M`<V7gXPGkD#H2ah??YIk>4@4WMdTlcUdgDrLhF@FHu&);_cLz1kgnrW8J zTl(^#DO&8<0j}GdcuEFvg_Q~`Ou*0J8RCOw3mPMeczVqk4HOfvsH9%U^&C9A{>`&) z6^;LYXShzIWDUzXs7A?RCq?A*_kV^j8&uX>q_~Yj$zZXAK#N08z>R|GB&R^*8G}w4 zmaa?^Vi3ZvIfPvS2s`DSfa$hB$4_#aaoq_P%mwB`N6_V>>O@Q`6Kbv$XdXDN#l^y^ z<JSx@%JI8N=<v`(9D&Jqcum!rWT-*K2~VzO)e<cjx)>orCMoRp9ab|=99nO)9-K(6 z&n*<?k`r!JmJ@PGz;j}ChO8!=rW2@r0iah=4yT&ze!{|88^=S^$mqFYBIkx#<qw-# zBGYVs#3b1fZ`(o*M=Gs32}U+GB4+@s7asE-T&r%PqfwMhzxAVc5;v!-0TfFgM7wvR z-TTp=2Z_`76Q{OTx0b4i6JO!r9iQe7dA==DMSM6?jiB_?H@|-rL8+bx$?gZq(_aRK zRN&@!j}qvqlUvhwUVZP?yBGIPj_#$;eT4@6>5VW5Xzw~`!@F&GzpeLvtkaeF=GZUB z4x;_L(f;kZ{iq1PA9kEP=;+(+=sV~TcRR$rj^{Q)jJ0zsvl|`wB9^o}$9C3!f8%#I z?6H|YUAJGmV$aUo^H=TEkN0EO0N{&I+-}F)UB5o{t5Y@}wG-#|L(f-*WH|k>dtf`i z+dZ;z>96T$KJ42rd?0~-8&hCS*XcXa_oBDr8`G|Yfe*8H#Se6Q;061I@7SqV_G8oc zV_gR^yc+}Lzr>!az63@c&2T6b-MFw9?Am(m)8MHGq4@p8i#wrD*1=yda}%!HCb=V? zc`SiqUAA!I$dAGayIuN?`N@U7k#Spk*-l*C4^2Fbo`BVV9rb{hzCuDceH1|Pl-()b z6@M#zB-!G(c2YazJKwNVFYU*^{aLK@-_<a19h>aJ^@pDEw#hTTduPH^Y5%>E?x}YF zAKU#vixJ0Xm`;#or&hRLE24f4&ZI0mSK(wS7A?I%*H|}lyLdf$%tz{pJPl%?9-s%h zN%Rpqgdd0u!0R}4zGx_xrjC(e_+%G$lMz*(<G8~i#6><sC;k}{INz5DUe&}^j_cZ* Qt0FdUUp{hYG6XyR18js_i~s-t diff --git a/01-rag-from-scratch/src/__pycache__/embedder.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/embedder.cpython-312.pyc deleted file mode 100644 index 84583793f99f5226e2e848b587f3bfde6e02a06d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2865 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 a{gYe#zBwcvmg?Ud=Zp2LL)-i&to{Sk4I#Mz diff --git a/01-rag-from-scratch/src/__pycache__/generator.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/generator.cpython-312.pyc deleted file mode 100644 index 6d16c42644be4bc125de9c434b930815bd579446..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3694 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 diff --git a/01-rag-from-scratch/src/__pycache__/retriever.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/retriever.cpython-312.pyc deleted file mode 100644 index f9c566705c29f29784e4d00fe7cd136cb69ed742..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3239 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 Bhv5JK diff --git a/01-rag-from-scratch/src/__pycache__/vector_store.cpython-312.pyc b/01-rag-from-scratch/src/__pycache__/vector_store.cpython-312.pyc deleted file mode 100644 index 63138824adaff420edfeace0694ef3de3072a6c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4802 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 Rfh~==5bb`_1#h&i{{vNd6hi<2 diff --git a/02-legal-ai-assistant/__pycache__/main.cpython-312.pyc b/02-legal-ai-assistant/__pycache__/main.cpython-312.pyc deleted file mode 100644 index a7ae90eb3e2f8a06ef5f9d36d7e2094d45f2054d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14661 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 E8}qI1bpQYW diff --git a/02-legal-ai-assistant/src/__pycache__/__init__.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 4e0373a2f74e346586f6117f8ab56bebf6886b45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136 zcmX@j%ge<81nO?{GePuY5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!QrFWrFw)IQO;60x zP0Z9yEH2J0E=kNQ(JwAa){l?R%*!l^kJl@x{Ka9Do1apelWJGQ3e?94#Kj=SM`lJw J#v*1Q3jqG}9Wnp_ diff --git a/02-legal-ai-assistant/src/__pycache__/clause_extractor.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/clause_extractor.cpython-312.pyc deleted file mode 100644 index e7e3c1b1492ad7260beea15af1c7bacbbb24e29c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6758 zcmb7JU2GiJb-uHIv%e&@GMD-@bwfF}RuNZ<<=8PH*B~W2QcTk-B{#5ac|F{@yF1kE z%w}d*Bp1to4@E&W1uQiNNFWKyM+Wi(0~DwO1Za!4fPoa~(quqlsscvpJ{af&V+BZ{ zr+)X&%#xBN${lLw=iWQ#p7Y&v&v(w~H<?V5;P{W%{!U9CAmqRKLvTd>n|qVEStKgC zN>oV=>C#n6TtinwxQ6v`Epjy?@xF*2t;Md!a35uIIi$w^GIBMp%1p*{f+Y_^PC25+ z(V9{dEFnfk)FhtMv!Q&d@ryVi$&#)#Y*u75j-`|w(<;<w>F?%0q+_h2==7ZErK4W2 zW*W)lMU@$jR@RtJ6+@-|i#E0Etc2#$NopuHrczHZb!O`rS+>j?RcOgH#5DZPB<>7b zR~&RZRaT>#;h40pE1E$^jf$?>)dIaz)ohA?7(fSt7oY~!m04yD&`IV@GiD^Alg+3u zbgo+_?zwI-2xFB=ok4inv}y`}E+mt_SzI@2u`;umQDQ?$ibFG0Rx`LEVlZzKcwAB} zXO<4rTEli|U74kjM|IP-p?--&s8Xo!jT$KkQR3aNoLHe!paXxw3>5<Gs-o+iIcI^l z26U-X0ScIhcLGPPX73?Tu{2vVDjvq3+~;gsXO^%ShEcOD0KCks8(N9kg@HY!{qC>+ zmJVD2+&4T8g}o*72D1R^EO4K;8=7O&9qX|+d&q<RCC@0kQ3ldB)=c0PXywfVji*iP zI#d&~bgia2VgZU~xu|Q(q^4`YpL^yT%8XWP)TpY|lnT%{4Tc5bt5Z@853VQUf6<_Z zNf{=0{6%0I46!^((55rz9=5TW665O*w8{*c)D=y|Vv2^kr2(Qdv<8>2ehtXS{rPQ8 zWx^1<0jUsCGc5**Ff9-U<n~aC10HNM_-PFm^6AAJ4zp^SvE$g>IXR>k%xSS<rmM`; zz9WG{I>{(}0fsWg(r2HH?dUEfX$@N?n3mCLbkQ@Xv39dI7Qlf!KG_?6c^zb+Q;F%G zEX4zuyN_ZS*am^A74EXUp$M1dcH-cs{-COrs?>&au~Q|pfp|2FmvLvosjJEjMyDFs zP^u<k$CHf}5o)+%F~*VheF=zp^8>U=*bfKc8)uJI3$`fNi|>qaHcj95HiW19qt0Tl zp4aSpg9p)y$s<tkqHj_FhgL6|(+0L*RjUW0JUC2>4vv7zPQrQljzEyKPx2kEGFz+g zPK!Y=m8yWosO(_|w>0pbCu28^@WyvNMaWhgC8skFFgxe_FtiXsa*s)Yw#wmGg)kc~ zhb_dnPF2I!;bx0fIA_-C2w`8t-{3j3PL;>hS+hYW8=4LQOgD`Rm<T8fBLiVVG1y$j z#szeWh*KP54_?Iqs3H<`SUviH!q-_H&cXmJqCBWdsEb|5CwVe(qY8Z3i+KunJ)zch z#_Alljv{+Nc5#<Vz7|}A@6cK7aS(eMTeh=pIcGkRGDb}7Y`#T-Kc`5cjVBuP|FG;m zR0DFmy@-b%9>2ce>Ct#zawC@%r;2O(LZhY_Z!p^i3%D@=*|3Zf9~B%i+=TJF_iLOM zi6aFaPAIs$<F`#kf|i}@quNxdnIc3D@$(#U<Y07*s9`noQS=U<565@NoYa!065iA9 z^Bw=8gnW(v-SKsi$YpYje4E(PbVwp^6A6E_v6=AOWLnD0&EYXq@tDTXtNgr+`xJIh z4c;VjEbrbv<&7$Oj|FGODY&xV*WCA&PFJ#;(S}nV{4RivBN;MPc>j1lYH@qqc(GX4 zbXF|7F*qH(+>MxZX1EcA1vMXXQ=a5v9V_EXWt*Es>HSlMQ>ULF6iL~jq75pxts(9j z&MDg}1q$xYJKg<79aa_}P|w;2VK3rmkK;5?KF{VpocT#%GyB|H_PNb$VJ%x&J-C(~ zYQJ)6EqiJH^}C78!u6#yD}5V@Z*LPRdSLa>x4L^ixN-Bwk7xcE#`7O}<Cpw&-^MDE zk9&r*;eXCXhY!ncCWu`{2a1(CZh~Qasf9Crh}PZC6fEdm2u^^6{ScW8JH8tb(jtM2 zsiEuV39+KBuo`Y=;6CA2XevnhAv`!*;g2H0J97~@QOC)G5rjA$$7*q>^PRXN1$T2% zhnpsjR&>Y7taK|nl@5A#u13=74xU;ad2_K=Y>LDG(9yA2Zpl;oAL_)TDhGO4?>W6e z|5Prx+qtT7HPMotr-GI#6h#joA0_Yjn{!Tnf;5yl{~u}4eoyNX#5o*jGj$}m+qt$n zcFx6}qd|Ks?$F>q*xXjQ6|YEh@m8E~a_3OcKQ$2C?OdNBR!$<$H-i>6-HO4lGKa`d z$<IRpvJ$C~Qs`|2jcMWsiLS<v;F?KJJ)>T|_BAUp9zbsJv{MvcgMRTP1nnxhY}8F$ zHcA}Sci;yvof+&LqW6IfM2C=QS%L`54RAeJz_A(zDl0H@FcLH~<A`^4y~Zc-k0ClX z_J(+u*BxVuQ8`ydRWw-QH124;s4G>$t~Desx~JNN;D878@&qI9@T7_-bx3R&XHTAq zdKDj^&ks4GAQbcOXwJ(EKRdIv86Q%ws#PYDWQrO1JEo;okX-8`cJPKB8AvE5p>s{x z>ILfE9M=&c7NE$y>i0nr-rDGZp#i=_*^LG-lkdC@w&giSai^l~!0&faqfwg#vtuC! z-ZHKm$c_BUz=saG2xTA>6nY#ptsK<%oM+04ri;Q4m6Hb;G6iH<Wu(!nm$Zy)pA`sc z_9v6?6bc2Znx%$Fi+7eC&DZjhm4$nQYh>;PAQX#-fM)pp_up^!yodAgSBIu-)5yR2 zu6@Gg1EXbKsn|^!eQ0{7IsDE!@oB>Mbkn*<e=s~TesTPJL&$*|NZ3$a@DvH30=OSp z%&sG=#diUle*f~D<AtPl5@9pHU+{|?)<I`6FHLh}$b!x5%;I6km8*)aIF2PWbi?px zH;R*1catD96b;;w+>pi4*O+t@Z@e{j<>J`I@liLzEpVfp(rh=$Yjn<jCFVxBAG+OK zjq{A>-J_OeLM%!~!^tNEHqc0~x!pdoI#?6_rNr&(04cl-d@Q)%jR*F*DNC6y`l!0m zlFk$ZC=0A?o6`n&D{)vo+`ew?KK|v$XI<Mmh1OyG>_Ol&PyTS6^qpAGeQW;B?KsID zTYhnQwB37Re*CVSTzF+&KDadcsr-$tc&>fu>{|TnLf2=RBb%9HYnfxKg?8rHdS-b3 zwXN>#2hE$!pPgNeZ5)27edxRE-7n8yxcltjO7-K!#<L^0j&EiMKFtpNd-#(JzsbzM zz8xc(1Dolm*V0d~r;pB$ZprCQxo=JGTZ(ST{daq^i?Q3WcHfDWk=5w0C;su>U%k80 z^ZkXW-*KzC{QPp`???V&?3ZI3J;Mvpt)AY+)a}$yG7Hh|NFtiOo6RkrxP4;jt@Z4& z1^M$-@2$$uoQ>2oTYdevw1w|&b>|kk{&Y9hy_q_?mOA<+2}iS^$20AoiT369+Qm}) z{Zjj~+MZzTuJT5_vemusuM^uc2Hm&0Q~%2g`SbncU;CdOmBarQlW?}MBS^{TXxt)3 z<0o>7KcRIIIf5<CN)@DxJ1Hu1iO@&kfIrTYYriGrU4-|!5b}obR42a>^2AAvsL^2j zqw)$Qn{ucB5jjPN)gzCQ53Ua0pF<Y&NF9=Dsxv0`m@(;HW8_CC$!ew*`YBPnS`nPP zTTz^Qe%@hQi9F`5eXZDo9JwnC>b$LHMIIJ!$?ASJdx)q{HG$vU<231Q$q&kW%-pFv z{t8RvZ#E-D%}pW4d-!N|GPKJi-`DtG^o!{In)o&u#9B^{-iGDTN}*L==nV2)TYSpo z(T9KT<811E&jGVG9DD@$dJOH09`ZswuTl{_8oFOVJQNO&=l7=8fZuBnpbhF8$kL0V zg5t$gVjf{;^W^vrbNCa%*yFIoe+p=ZsrkYe98ezmMVxh09wtRjy{_Ew*vplQ_a*A? z`wI2Bvd?dBMlfeUsus^j-OK}AeP4Q~8M;QBX-bb@_BeEi-k0*tr$n5*wEL6v5dGp4 zNEmO1L1g}bH+|x#S6~HyGntp&1OwMHiwEs+g*mQlF(`uQA9XZGN5QJpQ9`L!AJ-=` zWokw~E+PdO11Q?Aq`5KAWa|tcBl|XjC%8gxJg~vWXMEy?bV|LT-s`PF5YvmEdswey z;0%5?Dolj--yb09{af9ye3tH-AHCb#zcjs~Z1kS~lBA;tZzdL`g%`JSM;51UPc1*a znpn>bZRSSSawGqA=9BPxZfr9*v6h=y&s|waZDkMefZBfg^lE-Rdv-zI>d7r8Zzq>t zT^U*LIfV$k|KQ@#?V%-e#aiG0{6hS*c;<uj&GfAwF6~<iFFn6>ed*fD$<^sq?c*P| zdxtmTBU|2pcHg&FMps^H_nz5^zwmjgYhmWcuPmMUG}XV=cXT<jy#LmZR-$Wtg9{gS zjlZxm)b4$8BmR=#c5C9d@dMijVbb;ylJ5E7<(n_xdTTv(xE(os4^WVN#+8djBr3(C z^$k2*{WzgkMk%XnlV1JX!_zJ#o0?IuOZcw}MWhB6FAlu)A;PmI>lyyc=~Dpx3u2A& zd(N`=<7bhkHgxu9)&w58W80T-+73&S^!w0pDRS=$3jXwelEi<J1OG$v+g%aqz|!S= r1efjHX(_vWW}D!$8d<&a%j7+NyKPAQ(vjsCwh1mP=l;MiV#faibSMq3 diff --git a/02-legal-ai-assistant/src/__pycache__/conflict_detector.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/conflict_detector.cpython-312.pyc deleted file mode 100644 index 8fd022f722809ead216862a462dc2cd73bd902dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7817 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 diff --git a/02-legal-ai-assistant/src/__pycache__/document_parser.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/document_parser.cpython-312.pyc deleted file mode 100644 index a0080bc4501e7313847ac8856f9db27221924111..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7743 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 diff --git a/02-legal-ai-assistant/src/__pycache__/indexer.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/indexer.cpython-312.pyc deleted file mode 100644 index 5a0fbf25417a890f449c665ee103d01689fde745..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5899 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 CV^Vwo diff --git a/02-legal-ai-assistant/src/__pycache__/qa_chain.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/qa_chain.cpython-312.pyc deleted file mode 100644 index f367a13e35b12a7641d07def2feb14779ad35925..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4494 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 diff --git a/02-legal-ai-assistant/src/__pycache__/risk_analyzer.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/risk_analyzer.cpython-312.pyc deleted file mode 100644 index f4dcc6d8fde0c1592947931fe1f5b7ae59ba40b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6821 zcmb7JYiu0Xb)MPxi`><Uq$EnIM=QnB#^hS^1F~qxhGbEeY>HASse<*$YPd7p9ddSN zF%MC@T_XHZ6jV|`RAYc_;=oLPF_nHWK!CVFfTm~*1W168ArZ1u6);@)hyT%03K#m- z@7$T$r3^|zN8CGi?&I8Z&pG#;?_B++qa(rL`Q7P{l>h1Bxc{OL{|S4QM;0n`oWxz_ zBwh-r{8gU416Koh2i0ITbT!0NUq}sCBUdA+hvn#4K#Kfz=xR)g%5gcd2Q<*?)y6_n z41Gx{E+wR-l=?7uH6?Y(aW*d`b)v7M9>}NdH=`VvFcfp5C~BfwZ^%ZWRu?{<{y=zJ z*DOOUS;7c<g%i|kDrO>av7{TaDaf~3uPg~ARkTgCjOm6@(=A!E6j2q3rYVS;gpoB> zR5Vk#a^`(O7E2XxMnW;!s5%=pSrIKk6vjlw5Jc6I4NbI^Te2{z+o~j}x;`PO%7k1< zBnEGb)tV{``j~L$>^o=rN6ub+Pq<|Y7cZR{K6PT`lt*4rMXhX$WqBZx5b)4gfsH+^ zXp&sjlrg1*HFPaMzy{Eu+#pEnby*g~vLRzrmM-|C>wSVGkI9<U#~7~Y;Hx4URaG`k zNUK^CHMW%&F+s6RaEWDP!^C98kV}?MRVEg1B?!*Yt7vlNAgU^DK&hI2Z9=MULc)Cl zBrJ(l><M*6E2*}mXl22w=q4D3#OaGs^Vm6r^(VD99ZfE)N?F!Qke&ewOj$ZqD^wM; zq-&C0vI@D@_)q`l?}SSgs0n`(q$wM^Z4O{PS&)4>jmn|{F`=oXn-(d>tut{PGF!9+ z;rx|hTE-G5WD{y6ako_VOqGzy)I`Io3ucAH(^AMbdZ5L16fi|L14h|q!4+dmQm#VO zx+^k^43JH7r6xQftTvHJMODY_XGrWcOnAUGbW@RJ5*`jrP1j6il+0G66(N3b30sGL z1e`LMp)ZszhI;k_^}~89N^M&!#YW{(7>;QwWt=crYs(Ospl;D=1ZlE9DEO9aD6pOO zX2=jHbpt0AiW6;E5?c$pgpJ_%Nn|I647)%T)NAAsV9x>_R;}J9WGPiykZgl2(6x;! zmhKZKD>Bmt$Y{C+QmSaVq}nDrYQu1Gh}zT`TTjlciV&r@0C7wURdm}hzhbYs6O1!~ z7|IDR2)XS!$O+%XCfzd&5t~9CwAu^^ld?R4qb@KF)(m|dN7q+S+iL%;ezsAJqWMaJ z1wf3hNVceM>!I;QLuwEW2td9it9lI*B$<_4+J=FF{}LP)2D(yeZ{}Izf;T5>P%RW| z!x7NneqT+xZPjcGhn2+ul@>Q$XUU9I5H>&h;O8t_TxdCdGHtR(=3X^R7*jM+D^W;5 zhbos*A+PzmU_!)gCdnP(5iNK~O|C;Mw-k9&7?nX!b_r_b0vmsB@YLDM=dkuiKW1xR z)@+kl2Yp?W`$=WkkxXmg4C@F-7Ea+9f)kDjj-X3r==>F05LAA`sGL(Y5MYLHXs?qT ztP0sIiFwqv1Y4u{gR=`+l_=t|Z4?rpQxqT0^G@i3XjSk|ow2K;b`Ay&N9{xm*|H6- zM5FvCf=Up-N54cd$637JVFmo!eXngi<hQin+qFr2BMATypmK_{5Y^qM$w@&e^dNkn z*2D8EH^n#k@wof7wY=|rlmIK}-+hl4p1a5$;Er-8KN;Y;_c<Q_>XF;Q_qj<vA8ouo zq>GR?+%83YI0Y9cz*FFKLHiWj+`9Q9jVQVw1?#p|aH3vMBmI>whc_Bw+ZyYC4FZiJ zVCfX;hVx;A_Tj{e#W6*di$x~_j}1rTgkZ*+6Eb8`$_JdJOSf2qMjU?3q^$_T<G#X+ zM_%e@HrOvJ{d71Hjx_5<(<r&jZ2>{fj-raK6}NC^>;Ro(_?g2frn%3uJs;fueqlX( zXeE1SJzH4G7MAv`WCxbNIlPh`o__CPykll!{^iA8Yw@F-93S4j^sS9_=I*UKx4w7# z%OJkLq{cr|@z{joxQ{a@vcZ4OhEME`I#CZ-m0C7|0i;)EIX^(_9M|N`KpFaJTad5C zset8aoa45$7uW^=AmHnGD%cE+w`>M$cuzC<An04-RA_s<v50Z@G(!(U{^&CA*QUZ& zM{7h_3j1pssb+Y*)9-1&wS+}tW=}KRq7rFF#&`Pd?RRUx(PnhKyWQ^BV8`A`GQMr? z@>|Du`?dC4ib}C&)B;F%pYdM5)_y;T-}lbrRP@hKOZ@++f%|9fF3xSy=e{;aeythx z_c;}_o@=$nEWxk0<Q{Cs%KTKU8KZOE_Bi7?e`Nc8kTZIC&icCFA|;y<35i56_e1VS z0e?lFD|4m5`>?}F&hrbM_8(xN339M3b7%lDZksT;0fFLuJ5JDv^JDD=(GaULoVkk* z`dbg9?WZ0C$PG;EnxvGh8_b!zfy?zi?x3Q#zHSt}TG5+*IL~OQYL(a(285xZbF7Af z5+Oq7LqNZ8SCGZ@Gtgi$Bv+#FUDYL7b<yky^Yl+J>QV-8gs+G_ATgE#O)6HUopyLE zns&7+8g*~+0l~Bk#=$618>Y)<$xv#9=X{t=lB)aZl+QDBSS(iiEqJ$y<tTLL`h=Vt zMo~l9%OM)JaX+JA)obz=0$V5`YeHBBpNHi_xgg{yGFBD9x}vIxql&6n^_(kHn$OZd zO6I_J>uIwyS*asW0_1l0@&XVeq{`AjO8QKe^)WHhWZh88z!PfGf)b%pWL7n+O^m{j zS(8hIJ^WBk@ta0Jp(7}jU@B4yG8_U2t-yt)RR*Ip-A`fxK*n@ilU(5NIXPs6r3mFA znne-cvn0k9zN0`9lv)|&a2mmzH*YpF-$r@(t$}e9`Q}^K&2KnFB0Q#wWwQ}QADRv} z-n=#fjd~}>xFK9QF*1C1_?-dZJj#LzK_lJ)dXR~FNd>UDDZG2}{BR*Ltn|Uz<#)0; z<%DQ0CoI|3n(0IUqpG4sOyANcWP{w86Rn6Q0GGk|bb>H9CyYX=ISIr?!!pTAoq!>u z&mMK+=PnOjIy-cBc+d$`Xfz$9@JQ?tLQAp}BJ+0A#K|cc@md}<3>^e($O)``+{n_3 zL7Xxt?L}vc5(U_jK|U9eoFN7$=I_f%8scQp6QvU_sj`UekeQe!;g^;%85x@2;l*wi zuubDA+D_tU?uSUGxi1cLshu0?9W(LIdJe7j<fqU7DZ+K_o*TF~FkfC2*LEJ69{QwX z-+IS^m5u{T`<6QntaiLPeR?CEz1z6c`0$OT$lBgx%e}9yrjJjbdH8()V&&ub+Vdx8 z53gr)zt84=8~pW|-*imBw;AC&cCV+twvzhVYU;V^!HsBYJ-TZpx@$hX7X6Edne1HT zUSxUKHx^GWg?~BnPuKtP`da4QnXuO}+gNyM!T$NlUkv?hXf1PMCcKg9o=e_Ke!pWT zycvpyvkx;}bBTM2`Bzsn`!OroJu82BX)T%C=<c0;XJ&9C-8GZ?--pTcdh)rI<a2-I z0+<r(Sk9bZzHn{%`pxAVH<vGn%jZXzJ4<UZ3F7!}d^3zek4@6fzZ}b-+Ry##{^ti1 z!GDYMC~Yzm!uVe0qaXW~pX7s1h&+iCas7xBgbOi_V0_67xTqoo;Ku;VJWdS*EPS0W z1BA9A3!q2<FoRANo>SZ`f8!v>UB~xS0Kg<T-U2f22r8ve8%T`%_h{{It_2H17J+T{ zY)7XVY6IP=@Ke?TFz~tC4mi^tTdThXhWu5zHZPH<(8v9pKYuFvlrbG!#>Ad7rgO`f zIKWy9cC~qFhJVOO>1F_BrWr!HqZvUcZ-ED%+kT#uZAPCA`A-?Qvl;u!xVZ6xmG(Jk z#_xO9A$3ih;y5GLj7!~8S1%{^G%#rDDSS)2nsIQu`$sJ}`czI+{t7%S>2D8(IBCy# z3sCxeaM-6m?jo1(wZB9Ui!!H~pZrQP1D9m0xXgp66@?0=cSw;%Nu>K6N;D}XQM5*^ z@ou)Bja!%#Lqui4hcJe_fQwh2@M8ccjIF0(3m+m>!JpEv;vR+Wmf-2>mW7r9=^9d5 zfm}I4o<6S7{TbbS;;M&{ZWM<7NQcZB_eEn0<{kzrVDp~{jIxw3d`=pq;KgEu<K7-K zyx~MI55G5zdp?_v?598Y2PaM>iUcK`sI5(CxN33u;$vQLI-kTm1|f1MxdkjZ-P;EH zMnJgM2;2~C@+|IsE(x%aX5rv%b{9=>$7{F(ctOvA@cFMXVYm@26bg+DYr6zg;P&Mn z^Y}~T<4#=G?5b=aDmlTjY&l`OR+9}US)`le;^4XS@1Av{5UDC6<b^H8Qjy||7^6uD zZj^$K>x!07Fi(guNf%oVuOKspAn;9BC{CE^!5E+|M?KJIyv4peNMo|yF)pcaBV5+i ztecU#2-moTS>M6W9CX9nZZ5TBBmLGVsm|%ahdcMpUtZk5wzKaMmkIB=6P@8_UfDnh ziQS9M?^~#^W{$3Bj;&;lt!9o>4B9)FzL#DIFW9S@e#9O`$20fN%wJo~uXeq%-u1>x z*Bc*S{MCWguG8yXLn~cFt6jr0@r`WH+~IqNm-oJ~II@~OG85hC>7A?GtISU=j;{6` zok_L^ytH(3HG6C(`bn(gZt6~IK0G@)Kf17EA&8hKFI-uCcd5QKvD|%fE%x??J7o4d z3zG}Va`%z7*wJ>=UoBLZyI)z0z54IT>}Toh?6L1P=S#m&@89U%zi@Ej<@x@_udnpJ zI2+wa?YKL1XJ~nE-{O_Uvr8k(dtO^h9rx*s{2{h`b3dfAd5}wG?jFB$eD?Bca_@3z z?;}W)%Xd1_Vo}md#iG%RZzG2S*A=)=P?b^l5+XzY!GIh<(F$g%A_KWov@|IB@&Fhe zZ{rYs6B;qzp@J0lQ_i?db;2!=V{agZuvPg@;~GBcY?)&yHiJCRe-X&@p+|p-E0@%N zaPj}-cK<h*-|P(WyXRki#NoBsQ{c1nuPlWAeqfVBb<tWn_0z^9s%~mLm+s!^?84s{ a(NH=to!soq@%!ebO%AUG^H20*eEc8tVm?X$ diff --git a/02-legal-ai-assistant/src/__pycache__/summarizer.cpython-312.pyc b/02-legal-ai-assistant/src/__pycache__/summarizer.cpython-312.pyc deleted file mode 100644 index 3e134c396f9b1a7a763aef9948ed137bf84490c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5650 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 diff --git a/03-research-agent/__pycache__/main.cpython-312.pyc b/03-research-agent/__pycache__/main.cpython-312.pyc deleted file mode 100644 index ea5901a15d913eeb754a62fcf7265f4eae90f75c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7387 zcmcIJYfl_kmQ~%=uj(hw8w^}yY-o>ZFeKRwb{^P3LdFIHMmzD&Ol{FsK)0*AI=8BA zcMTn(Xh!SoW))i+$;@V>IA10n*&p}`^L2NlT?jp*T|>0WtXHe~M%IqB%BMZ|Ru$cC zV~z7+%A)V<yzaT@o_o&mzlFm*17GW#pQeA)%`pE(2F~ZQJCANc=QblTml=tbTnc-c zrLF6-i?;5|ZfM;Km-SrsumpxvyjkC6AIrEH*>}$-11lbx&BPq?)ppq>d4IuO4oJM@ zgO-QZ4{cBmNr5W^41@hZCs?IZbQN8Rp!f&`<g;Qrm&_OWLF?lub2=`fyqeDGXj#Qb zUzO4D=pdFgS;VQ;L2*Tfm3%rcE9sn^<oQJnrjUGH%;ptYbLcqaj-fdbYcdiQ1<g%N zX-LdTNYh2ELk|HH8H*`BeN{#d2lV!daiqzbmR56ozNoLNIb;io1_$$EUdGy>l*ULB zb#cfV!$J)yo<D}hwJTI~OI#6Y>D-DUqjw9krUPLrhI~D(e-93T3!3Tu>s3)lSsCP# zR7G7$%djUbqk<+&NK9c>(+FeOvL)JUy+oWh<Q!~N2R)$W0;pm|%!3G`Qq<BK>^!gH z?<0w>=F=(Et79>z!5L*`jIydED_Soo!lnjH&#T!bIHNHHN9J(1R_XRdMN<(j5M9(A z#P`>|Ezit_xvBt9AIX9Ur%cSrmO)oR8(LmYrI%qh%V|aa9*1GGJU2Oi2~czLpkBzq z5!dlo3wpkw+w8QVB^m3<ti=C`T%4H%YjSg<z6xznQAJ6RR9((pHN9BY3pkgm-Y$mm z!N3iFkN%w{7g>j2>&rM-+?jxuTxtDaoZ$|-2OZt(bnm<G+3O`{jmZR@`D!b1de9kV zLQb#RN*>93&z3^+N&fo*ha&qdio6t*LQ?oSAR~W<cT|cU@NSUe&q1+KVx%Ui`8gn4 zq*kfze)~P5cle&Un+~AZDIIwZid{glyPo0#ljxZ-e71WjoCQYX(`erIb!eRWx<9V} z%D$`Q`leJxk-=ku4^fw^PK)@qWP&rfJXTlCpd@Q4oX!(4GfJ+gsDq~yQi`Z)rdQ6V z!E+d)T6bphbK`T9^9#bn^t=(Q@olD?%)C62#=uqu50p}KIyh~xc62hCmzL?((GVJt z<Ylp-=wr2>F_CbaK6}CRf!h+VirC~<WhHO;1_$XiG8(EowtbT6ximXBecrfuuwq>} zMO=~<%h#oIb(cA<gL|!!q5lAL+@{%|ARMWqGDEX-lQZMf!o}H%$qR;OC9e;@V0x_4 zvDv(wgJYnOC?{s?=u%KhiTY#H|MueK!lmii86)w$K0j&r9XDz68t7CObX<^);dyX| zb)K0#RyEG^qKL0Z>b0EhQj!Um=_Rk1n!`~r9YWUfIpA8xFTH7;pS3&?@on%N=&J`W z>JyaEfSOZ^mR_%=0nU*xku<_}Z<{f^aM<Hn^JXGSfcRPZ_9YWO(=XzcJb8xA$T(gp zWZ?y;uQE0}=f!IiRa<=%%wo#1R?{bdQ%nhh84@K)02&n2bg5v%D>Ctx;5@yQFxH4q zMBjBLhn@-+hV1KQhJ~d<T9E_`10%pV0e@OA;I1=!;pW|N-*&jK5^kw9&F{6L-L|va zZD;qQEsq19h<ANzKg4)~yIkuw*IMzn?8V}@1GfSjr|yP#VlP!<&6V&Q`vJz&_L%W_ zymhb*mDsULthEyU$+G|)V#&MxJFyW8JMt{pO0208>!^fZeg;6i678&nU)%RlSvZh| zuaP@W(OW9v`DdWF9)cc<uU~uws$%M%CGvF8XUU2$|J<{5&wjSF)4GSyA|E_e2ai$4 zZ_U;I!UJVLE-a&yXOMmw8uL0MtCcv1KFbt6MNh(OoSB3G3qjZjrcI3!yr%*~Bx4Mb zWqO%95c{@Yk&6gop==rgzuZdFjMyGxd~RC!(d0Wu{%?pG{S-~k&(F?}q54$>wwT6h zj#xGV2bUI?;Kl*DoT@`CsV8}4edpDD5hVdSX$Pzbq7X8+A|gpbMYVtt2$TFR1PJ8f z{)wn<CgH_Lpv7JAV|wx!!cEhy72zgmx&-?%ITB6&mcd5}R1blQ2&q*$bwv>KX#oi0 zQ!x2s_|sm3pLOP8F#OREKltHR^YL=<#GXIA?_$H_?8Anp+a0$$zV&ed{xQ!4LLd1* z@ZW6s$KZaL3H4LzbvAGin!E?+SR0x+(LN0<h?P9?9jvoeYjEah4&qYWS+3-weYfJt zdQ0xCuf*B;i9Z`Cd9r-TOUE81m<^R6UxDDqtAw+WlArc{N;DfQ1;BQWIqcWiOrO)Q zwvO$T_%%M$U!8Y)j%3;kdcv8L{A>Kjj1>69t)Fz}GDOKweE0bp8~%Z_m`OUlYAXdD zIY8pm&b&VA^gy;3oqn~ILJroGZ7H}GlvtX9)yf^Fa!39`a#!^eT?>MKVw79MCmu&S z*jp7Q4(CDa@rmza<`Y*H-o>oB4u_Li{bdI=Gv@TFt<=~KgZoWR7*`6_@NTZ-;6L0l z)<VwdmBMRbZMX)%<u7vXRo%2!ZD1|z$O;;5`^4>x>U7y&odu2g4&RRhQ;O8=q{Epd z8ULDlEwUDdecQLT6s>{mtU>~P60kn;;h)w(AE`nET@R&d`}0z)3I}W6wU`6zv{aS= z;KYE_t52|jA<Vx1^z8jECjlunR5?i9HM(qYIDjsDYIO+|&pk~MH-dcJ;cIJA$2uTE zU^5ekV82}>1#&pEOaT7rZ#eMMQD-GH?aWnM$NQAxYjKVJuzM|@nXRrmJqJoUc9p@k zcHZaEW|<sM_Y$v_`@>0){E(B>UOIfwTH^4XYDXZ2eA|(x+B$O8+3NACOhv}}cZ|{B za<KFZPH)-O$KXgEZf_O$9p*#f8gt$K4s(r7oS4DHxF9Ix&qI;Kde|XGZcbqtY99i& z^D2P`re*+Y4^|goLbMuU$O6RQn%QWjOk^N9WfCA8O6Mf`I>e8p1_a|6jU)xAEmh~D zPP`86NLj@|^?}12MMh9SveT)4qoE&3DwJ_ZumG>PrYA9ZqA&?g4G+z+jN|oGW>iwm zvNNw`wE%en<U{?&SpPw&F%H3mvtzp!ghUP1WNak6AjLLuh1B4Tu+3tMk~10(I1FeB zi@K{nX*AhTizH04a-=bY$@2=Cp2y0rg1Vdq6^T&DfRJB>bV^J+VTdun+Z0}T<rTD` z%XxHqXmpI0=?F!;pazS^>8JK=FJ6<O;H4E(DX7wsau(DJNp>M6jfjmhN5{zEEqbtn zq%rao;}ODmnw)73XVBx+`1Ha8q9QznGfHsIkvxLn94MHY6h(0qWF_xlc~UXS7oaxF z*s?*EN0aB;Oz)5Dzk%evk?v0*Su#n$1qE!u4AmN;>7jYB8MN{|FgtzKXm<!xgF8qV zB{X1!QEF8r6;5KXN#mTe9jLO(5JRri72e3<CDEl~UPk>Sn(RjqCHKEuAUz0EAydsK zdBgLLS{Oq{?0^(N0;S}u<hd}}cQFZuOpc%#QCqS%R8?9k%14b!2MY|?%3Pfa9Go6n zgsSD>AkC{!9i*VAh!k}>XflzY$(zaB_YbdVG`cloikMr09j9}GWp$WDS!O`PsU$ri zJOpDrOeSMw@<6u+#Ly(-!Xsp@fvj1FL9aY4k|4~&vF0gjj9(zrO$6M!?t*>&Rg6if zhN)6OPU0~Hxb#qbAvavZB*?^28)q<i@GYge6**^e^whZ(MP0%qGNN(6>9aEoQXeF4 z1)qT>(`%>u9;+4+tSLg9o^%eXkETb{p%7&HCugT7N!nvJ{75b?sUnu9i4EXF9$*v| zt`w+~q!j|G%_JHzu~?IXtXrc>tZJ^RC#n>x0&BBV1SJ3^At41m1QHZQ$(oURqc%fV zspAu9;luP;zL<l9&@gfOCJ&WtEKuRKMwqZOr`pEKZ#a^W;hzCW3jVYm@ckr<@AwB{ zeX0^{*bN@p4j$PFcCSx7<l<$n>03yHPyG)!_MhCjuejt_-082l7a+xnx7?I&tGCph zh7;>=Rf5eHUOCteInh>Zc(Y?Ov~~94R%E8U_~YG0VS7>dz5j3b{6Q-*`Y`w~*7&Q5 zjgC)ye%*7ob*KIGPSeOvY;=9P;)~rJfB!w8Qi+UizO{LFpJ88Tr&wshQ_sq?fE*XN zad9KD(Ye+4;$~*^C%=nt@e^fk5+wETTOo9(>GO`yI_`wF_`x!le84wVBJqzhA7m;` z&3moww~brIR`1Am>*!-27xb>5e;g-h{EKDor3cN&??|7kpQ)SLPV<@dnXiNGWQ&pU zFWmpZ{|o<n|3dAqK#ZAx9^H6vr!%?LF?841;>XI|8A}TOwKDhmgT|ga+~?8HqC1Vl z>lf^WSIgXM4?;)poZ5{1&b<?wSfAYE0$cpiGS>@L2!H7Be*XT?Dbr-xKlC8lxiNG1 z{7!WE2KUe(-i{o-)3+1pFZ&0+Za@{3th9Gk;w`sN-a1)nJMwAp*FmVm#Q7WE#~~&V z*@`9ZAG^DJ?+16gHnUriiL!sv;$XaQJn%Q%>?3#^a@pTUr+0m=U;0`p>q}+-A}DR& z#f4k;x_a;Y)yF^E?K-{Pb$X|3^d|oe&op&cVr`XZ`);&nJKD1oJ-T&#Y&$x(7i->X z=`Y6ypw{zssQG5*&hc_+V84@zHtr)P6#MAR-=EpgKQ;cz*zDUKIrrtrxr#rs>u=xo zx0n5$#12B2zID3-{Jw`7J<o0!H_rUu<4&fztK2=b)j0gWj0@x;JQC&D$p^9KeGg20 zr^Ugz|Fy0CLJ#vrPsfF`?k~=;@N4=7f!MzwBwU!3Q)Uuj+yO0aB|o%QK)j3TBTs~q zUa~xq*ZN-u%n_hA@`K0^$y(`6B4kO$QxGu8RIAm5@BC-sc_=9ItC-w2Qj^dY;b-5? zvg|jmW|n)DWZ2jrn7|(x{wt>KznSiDLJS-FMR?yk#Kv!4-)&89w<aGkFnA=fezxgR d%Qcqe_jNbJg>D#My4xRc{$Eb*!?1(0{ePiCH<bVY diff --git a/03-research-agent/src/__pycache__/__init__.cpython-312.pyc b/03-research-agent/src/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index c9350b41bf9cc728d6aba52e603b21539c3d706e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132 zcmX@j%ge<81pe;xGt+?dV-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K7F<Z(yujlv<pc zSd^Tho0y)OSE65Bl&l{gpP83g5+AQuQ2C3)CO1E&G$+-rh!v=Z5r~UHjE~HWjEqIh GKo$VGx*S9R diff --git a/03-research-agent/src/__pycache__/agent.cpython-312.pyc b/03-research-agent/src/__pycache__/agent.cpython-312.pyc deleted file mode 100644 index d1101a4e010d7a31305978f847bc773baee0f9fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5448 zcmbtY+ix6K8K1rQHt{;XBuz+ZIRv%Is_~{IP^wi6Oq_MR!m-VIA;hI@$1`WWlgwqA zi`N^iB|j7tq>4uLp{**^JRnpQ9(aTQftOURz#1h|RN$d+hD1X20rC6J+1<>hNeg0R z&*hx+edoLWzVGZGb2(MP_3v}<S~CX~<uCNnyN0_jcen8JreZ2<ikULgcFIYwrJc-L z#>uW_Q#6*bhn(THVey`|N1V~MQSm-xk2&g^D&B|fea`sWINnFra^@%>Hplpcsq%Dv z*xdJOc5S~o&L_<rPh)HnV-pygYVXfaMn4}@6gBXTlllgC!$PyI&L%GPg5~poh291a z8{B8O;lr~7rn@FvTs+S#H}sg!7Ik-HuAy5lt8mQ-ndGLb7Z$WCE3dNZLWxyM+FX?_ zE-$ZqYhaVaezU41KVIQ_;JNG>24;C?VL#UNJ*OF3?gk4&Uk~|4o3$Vl1cFc`if)J8 zcQF&Nbsn}jcZ;gZShe9r8;y|B_0i57V!7j)+-APm1iaV1C<HS-gmTREn9H|9P-1Tj zLOg`BX&IMECbESvMa#EnU)_a4nqXG<d}a}k<@JF3o4R1s+g(ukkxT1MEDIR+Co0mj z+2pRtO_GEZlUjq(M?1fUz)cPzBR8~c8o=<p<?1%m+@J;b^fr*{ncT2U2xWz=p$Ck+ zWCzFx*&HE|s+P8Nr)l#d81>9lWM$|t1*lt)uc<e=55a_yV0kwn1{f-^@-f?H0oQ$_ zQ2>jcy^7tjnk2nHQzqLKS%L{x+VxpI3=w=}Ylj;R-L(P-L`l;X*-I53M0Uu&`Z8M~ ziHhu~)+Z|mOoJ#Fy@)MA_M=_nBz3BL5tPwkLz{2vZYcFCuqCh6)gy8o-M6lA80-OU zdNWd?PwDeWIX<OC)e2#4_DF(<IJF4RAPh+5&(s)+=zC^lz;hHC>ca8`k#*X+(o&VJ zmP#@StINxatB*`SnTjgyC^8t<GPt!#k<CIR#jK2-5S!Rm5HhbWQ^3_75&b4Zz!|=U zJoOOXfCEIg8nlCuJKaz&Fjfu`VVlq=;Gn8UuFSK*+CcV31USf^z>9o?GXt3ll4=9F zfxlLxtu`T)6-Xc&+)oD@VE526bQ@vT;B7`jlt)6HW0VCUAdP7&WhA;06bPa&G6=Ko z1^mUTA8~@K^7)nJN>zj5=9epMv7}X&%1h@y$81R)n+0-)jX}QmF|z5yTaX3Yw)=^9 zvX?(62O>Z8)F3ng5U>Kpw*ic4>7mgWP)kA?fn$XHwt*@x!JExO%SqTzwcJhJj`Ve# z3y4BWAdy4B5Rt;T_n9)I0gDjs>OKq_**f4KaCbt07UDoP`FgamAqhEonmPJqIFHQT z@;xvF3(MfJF~R@Y^89?MQd*iTu`{LWg;HsWY4{W2sV$vl8e4_g7fS-m=d{Je|Ha&c zw%1r-5v~bVhy`0#kN~n`P(mvE=%>HHGBJXXYa(8yp?XxV>jB3(EUmh{mBdZvSmtbk zMsvH_0^bmn&a$%tdSOtD{6s|pa4rN{*8?OEkPZ>R0i&jcf(2I*wgrN=!b3zwi^4}| zrd6+_#sRK=98s`XNI00cZoz9^lm#)82H6IA8_-8Ek+s+XQ;X~%M4<fxc3k4MRKEvo z;>$pNMY`k#gVoS$G9bJCz@A>Bp6ND1%tVff>LVQwF#}5eZs|X|R;nzs)rIBiQFgwh zEj=1~R9ac1l{L{Q)XtVx=PKouYI%7HWdVC6Vv`C~z^-uLV_cRnQmo;H0h$E~I?~G$ zZR*yBgcVWd5vx>p8vuTIB|?yMz2~MT)<NYudJVB184iKo$Fv?4UFN(g1?ol<Huur? zP>I>}EC@$#>eW#rqJ~AS^VTd0d0%FvQ5OX$#AvgE`Z0Be`AnSCs3R+FaU%*nKOPgG z)pnD|la>pN({0qMn!vSuDju5G%B!n*nK9s$kk`82tPxWTPj`o<_j{wg#eoQfwiwpG zl2Q~x8iG!MjaH`c6h)rtK>*lC^(+W1YUrC-Vi-JtzSIU9o^5l9<|qw)GT8?d)b9~$ z6BuvmmMuVtq?5F=gXx7&zu&;>J{uc777Pg&6;#b`>Ft0S7-DUdif+9?>(@rKh-N|} z6J?;Ro`5n7<&26}q6vb`RJ+LI1r$EZy^Q{nH;IpU_L{?9CJSj1G=+G46RLWCEx?V( z6JnE^!$aNFLp>grX=!A-jXpilYb*49_YQ7vDxK6><@Jn_wiF}%@_t3x&U7-DF!SKm z$-I$%Z7Qp5XSXw9pI=A00Y}lbJroYfH$~}WJLwx~`L#V9jy!aHq%+hR?u=|?wnsyf zNnD-LPC66^7W%w2E=QAh97b?N2`74E77i!B&EDwlu|3uqTZJYm{BLJkN`6THJp?=x zStv)87}6k!l6qt##|+Pvd1*4)^!NQNVj5M08t)a|M{fbY6F34|`<M7kdVm#~$N@1S zNWCVi7WFD(98Nd+7CSC-TRKaOaK9j)-EK8cy&sby)AuHLQ@``l3bkS9yDomYztdv3 z7Xo||y326uMFR8IFCkFSY7k%Bb{N$Nd1-DtadblKiyA79CdYYUqi})&#=~VC2(&W9 z5>%)T1oq;+YYXu{02Y1zRZ5@8bw#d8vobbE1yO!r?zku{`5qL?-WLs5Jd8f8Zf$)$ z3U}v6;vtc$aRv&+LxMUU>9$SrLG<jV&0#^GDr$}UzUK$=xL8)Z+|vDxAf9mat(weW zY7X<GU`g$;PaF6YhCW4-PoeB*af=`SjFJ%NYNB<htyD_$<sal#pF-E)N4F^=PB)!+ zW-upfre%cjz61ou<4NR;$CIGxPZF<b+yY|Va~Wfwu)@h_Pg4URKyg-Rc`|v1jt_Rx zwzo#Yw9mk2ff&Aq+m5n#LOHFa{&Ylp@621L-hKY<=dV}Ze&Oa3?Pm56?)Qe3nWwJ* z@Pp}7JKwoA^YqPQ#Xrn_F!Sx5#et7Ixx;UrcyIjX_}6zz_f#eK#O}n*?)d&&6Nmmh zar*s<)4P)gcAq@D``DAe%H2Fz*gbUk-u}_t@J?y(fHFR{v-q!n%qowcNd;u|pB&Jh z$;Lw>9)#XT7e_FsDCCg@c7<K+QN+&{?<l`a-{_8{l*C-7qdamh{SIi8be+_tKDvoL z#Vvtz=>%sW(IYu$A0`WM@nI5vT8i{4Vb@bwT#3*s^qNFr7nCnw9HXg>^}O3nrvC=8 zBoiEvLw_JiQBZuGPB1OHCprLGE*3?im`dmnaRwyPl6}Dd7r9~`h#OM<D$>!a_{u|L z@s-cPlLrM9tRW$O62PfKc6?H-Pg3fY7he27t}7$orQWv4uH=%xW)$=3cu1U-;tX&q zpYgwpco2a}#Z@%S6fQ_i|4VqrV_4Nqv}5>g;XiP2+fnX4sSK%C&s^QS+PFIVVfOH? z?AX<F*GkvEaZ`Q#!|WI7^ZQp9uALE+cXHFOU;Np{YtaX}CwAulin&3;lOJY}5XIB4 zo_~OF-_*|eyVy`!%1_55wHgkXwOTxB<F_$DyX6*y5d)v376>P>E@u4&)U-Jfn!9#E zRyNUE1U~&(7(WJtDF_fQ48RnDW_<AeWCGp<v?d9YXeS}}3q+lz;wI23&fvu-^ZiEg zkK-Xs`^o4FC`*yezv-XC7^P95<F=PcrBZ)SKbOk>vzby-3#s>&bGMaKe^Z{jt$gja z^4#8NX6o6U+}`+!)YR_up}h>AcMcrh8^UvMR2iPRJ4SCmN*zf}Ut8T%@VNejc>M14 JJ^B(N{0IM0ZbkqA diff --git a/03-research-agent/src/__pycache__/gap_analyzer.cpython-312.pyc b/03-research-agent/src/__pycache__/gap_analyzer.cpython-312.pyc deleted file mode 100644 index 6dbdc1e4c49a54c5095f7727d6cc214ac2965d04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8149 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 A7ytkO diff --git a/03-research-agent/src/__pycache__/paper_indexer.cpython-312.pyc b/03-research-agent/src/__pycache__/paper_indexer.cpython-312.pyc deleted file mode 100644 index bc237eea48fbb114299bbadbb69f7c155606819a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6728 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 C?@yip diff --git a/03-research-agent/src/__pycache__/paper_parser.cpython-312.pyc b/03-research-agent/src/__pycache__/paper_parser.cpython-312.pyc deleted file mode 100644 index 1817e3948974171f89f61edac31cc2328e1375d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8050 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 diff --git a/03-research-agent/src/__pycache__/report_generator.cpython-312.pyc b/03-research-agent/src/__pycache__/report_generator.cpython-312.pyc deleted file mode 100644 index 1a56840a0f8e7c641d60d6108ae3982a41277da1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8386 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 diff --git a/03-research-agent/src/tools/__pycache__/__init__.cpython-312.pyc b/03-research-agent/src/tools/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 0e47d6b1b6ed831736862209cac7fcfa4a529e7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmX@j%ge<81pe;xGwp!%V-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K76+Z(yujlv<pc zSd^Tho0y)OSE65Bl&oKppPy5#A0MBYmst`YuUAm{i^C>2KczG$)vkyYsFM+hi$RQ! M%#4hTMa)1J0EKfNf&c&j diff --git a/03-research-agent/src/tools/__pycache__/compare_tool.cpython-312.pyc b/03-research-agent/src/tools/__pycache__/compare_tool.cpython-312.pyc deleted file mode 100644 index a8424900bbed241b848a760ad4e3b7c1822352c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5203 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 diff --git a/03-research-agent/src/tools/__pycache__/search_tool.cpython-312.pyc b/03-research-agent/src/tools/__pycache__/search_tool.cpython-312.pyc deleted file mode 100644 index 02f6ba853e2c6b9de9d01d12d2abff2005d94144..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3832 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 YwxBBN$$uzswv$;kd0Ts`;2}5o4@{kDB>(^b diff --git a/03-research-agent/src/tools/__pycache__/summary_tool.cpython-312.pyc b/03-research-agent/src/tools/__pycache__/summary_tool.cpython-312.pyc deleted file mode 100644 index 9ac5fa8909696192476027a3eb2007ce0466a00f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4349 zcmbtYT}&L;6`t8YU|?Be2jm3eUQk@v7#3T$s}vasiLr~ZF|x5LTTQpa%w2Y1c4j>@ z3*IeAR%^A66sb;?)>v+Z8&#?!r&862wvT!5i@}zl^JJwj?c26?q{dJE&YfYw4pfzL zrQQ2??)kspJ-dHRCKU<oPp4Om<)kG2ohF+t77m`ifx&G_m##><tVc|_5V;bOB|XZc zdW=VMF+F}Gb|qF$r0Yt*!Xd?Vw0_UFO}F1I6$+|T&eAMXEGvDxHRY^o&Agje4U0{I zmU(&AV-EL9j^#3yxt>$fFyT52Su#6E>xMOBa%QOo>=e}^cQT4Hb^iQWHaRjiHF{y< z1&PX3-f$V|!qBU1#<q3Fuc?J1cz~am=ghS2xl$3lJdZopfWlZG8?%ZfkFfz9=_%T- zYfPJEP&l5Un(06+^9&D6ijF;R=+I@lEV!vN-10Jl$)saz+;s<7&aiZ`Sx`MK4+GdF zDSnQ7s;+t}v!`dd=Jm3i?SO(Ao{J;g)ZKv3`I1N4kTL;<M^5n^VnhZAkxWx9cC}=C zoWY|33rjVbYPk!6k7|@9m%}GD$1LPI9bl^Ia8)m}Im=$4eb`1?cDusaV5GAJ)$&|L zhu($>9Skeb$FMD?PTSDYfEB9C>>`{70?!tFT&_A=J}cOT(OAeET3$g+pbYqu3R|XN z+4T(56rmw2AqJ^o@_EP)w1R7tv5Cn`QwaNoi(?a~2iU1gSFT)UQ)5$SN7=cNsdvAF z)fbXsw@mC)zRQ_z!@Zj7@;<|Ix#b$3G0%~D)1jk9=qLojiXiy9(lvslu{?6Xn9D|> z@@2ckCOFr-*aBG~j=@m~pvz2Sj<YVxGjXJ=7ZcEwam1n{y$b9kH{?oK7WtK7Or$Ve zVp!ohMD_FQA8q*y5fTC*tjwm750;`9k(bvDNGH{d9K{#W;tt&>k`N?s&an$*lEdc> zy9BR`K*7~+$wU$>V>wp9$e`O2Mo$>j%NBTeR|9<w$Mu+Pan_@n#k@MrJwt<<wxe^0 zkrai5>W1c}VbLj=Nm6YcSDX}d%dW=@!Vv{yCht*JzzLN~IdCB5Ko8H%WO@}xEgCv) zrNJG!Hn-&mN;jBaupI=h$TcHpXc^_7glTEnj}glF`-B<ULU{GEq>`TqU`LCD@JC%7 zpC~?0PvYyR1ctJQQPA|r);JftDdl86dZTG6;?W7wbU@@{<kQ51aOI)|sQj@kN$->l zQ-}3Cp<e)W23=18U1&j=a0D*|IEV^En^YYjAOOo1)2-AN(;bWie2}eTn;4u^i|+PZ zW<5ysH2eG~Ki?6Y2#JD522e5~GKK|&r~CfHVj&~68(_tPxUJ|w@{%5=;d)B}C(|r2 zg4&>6vIGp|IU+X=7*REi7YXI);*U&@G0nz}M2=Wm8TZP|GaLDYyF$R8Q#H?a3{H?h z>@Eavh!}@{5o3zK82F7s*X-32cghY0T0>xJtr??z_&og>`S1(*wzMeCZr=|HAB6pE zE#~0@y3M^MS(le0?>EenkeAvf%}MFKc1ijkWGzLPVvF)(^nY=jmt6USR~KVTc#bVO zCM`y;AJQYUbuTf;6jB?$y1FU#L1@u6*%c4_Qe2NMN_Qn>ZTznE>)1tbo)v|{i#(k6 zou%zey(z_#^~6#FG)*sZqnXpC*X^XgUVnph*x|&<nt!+PZ&<1WHO20wwkv-{@ZaIY zF8u5DMo8H65_?SHwWh@czLVJB`_=u>$3)Q#idS}}NP%JxDB5<VXaU9E#X5ZNTdYT% znt>~?REAGcfwB2C0V#TFrAmPF^mYdr2dd!B7Bxgrt7RrCkzVG<EC6`r)d@R<g`8c& zF@g7lzulGA?hG55$J4J)*WQBx=GR@asARC7%gPJ)i0;Kve4c(2U*nPt3|*AoLr&sW zNQ*MGkNi@;Ez8mejWMY#|311Pe-bGrK+N#Ieat34k<t-Ak~!?lIhUZBvFCl6{=-N6 z@IHx$s!vot{ks;Q&l^sl8spcB&QN7QLv`cv><jhj3Dd?i<ql=SWuw9~wYA#WQOI^F zCjXHBb6r|%9lm#UIk9|oS^LObZ5{sdDOJrsO0~$97RGuk+e3lrIRGWDr@yX498~C% zLphp`I4N|#Y=*vYT4;;X?d#Zf2IsfiHniQgp)VZ*!b);Vl)AuHDv2x2%1Ft}+m2hC zUq(2@?&dyKP4GZKpPYbC)Xl`Hq!a~}Z{F5z)1E1VpV+VYtg=T@#(9}hP9yK!0fkPS zH426&su?V#oBTMHWko-3+Nf^*xPkJ)^<%TPVfnE+UUvODHPF#-!Zb@2I5G1Z%`HZL zT-**nR)+e1y&5X*H{xO4R4=VKgspy~cr3D^IB_YQ1GEvNQo?Vjnddi#g0s-jZ`u?r zW~pt$X~Ch)aS3HZ=P`C&gw<EJ60*|$e`fAsVzvZaN0L5i>s)W^UTy1s)Rtav>tAi_ zf7Ev5W@9xWwY1)hZ?tFD+mEfbAA8jPt$$0=hPL;|Z;swNwvl>mnXk;<nY&w9OC8$S z-*GGcw>_=ZIuKM9sj21WAl6nI?le5?I=<dDu-Y~7sO!X|{ex@!Pk+{SbL`39L-)t; z4^}1l+XpueX6}twqjJlUYBZW`t~N<W-}-akANy|Uw~Y^td-2~Tf0KONmig?m&}mQW zda83R)me>7$(Emuy+4L)b$@idcWAYD=yB`t<EG&!EpJroq=wdNvqZ#fEycDG4-xU; zYVY9V)}hBuLm}~g5T`npM_0!0j4zW~>5h$#?%Jqx<;<NkYaK`LrysT-!|rkHcCJj^ znON%>yl+2jAHv!&t*v+Tt#<USb-ewDZ#--t!RkBmfsF%iE`J}x-q$x??|s<OS4}38 z{ncoFgYpznko;6w%2jyqtEVz^n&S@a{;PJg*xQ7*0E08Z577Zp<BDP`EZloU1zoZX zq$Nll6yH?ir!(m&a2%DP->7p}!xf?W+K=T*mIg45QqmDFr=ueG{3z-$Cryb%t3O~Y zNWXnMq?~gE6pTm-)RALV)*V-V8?8#{K8=1s-Lng_)U;<Kd7v7Tlf%fi=Kar-bxnz? zR2NS;Z{plUI_}qHv%0Nivwq6NUjQ2Y1(2ai&K0WpF{)FAGy#0#m{99^{fPp8GMIeG sISy7-!MF$Ug*=nxe??xCV_&=_$(_~2+p?UvKUI~e|I4vwq8Dub3uEEG3jhEB diff --git a/04-multimodal-rag/__pycache__/main.cpython-312.pyc b/04-multimodal-rag/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 9800b9b79a9ff6bac3762e195c7d6a153ed22315..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9526 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 diff --git a/04-multimodal-rag/src/__pycache__/__init__.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 8079aec9e4a4cdcb59d8d0305befe3c3a65918c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132 zcmX@j%ge<81lb<*Gb4fYV-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K7F<Z(yRETbfgn znVX-In4?>in66)3l&l{gpP83g5+AQuQ2C3)1}ImWlWJGQ3e>|0#Kj=SM`lJw#v*1Q F3joLj9BTjo diff --git a/04-multimodal-rag/src/__pycache__/generator.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/generator.cpython-312.pyc deleted file mode 100644 index b0be4cfacaf8ceeba5e9774e8b0d58bf3b7cbb57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3833 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 diff --git a/04-multimodal-rag/src/__pycache__/image_indexer.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/image_indexer.cpython-312.pyc deleted file mode 100644 index 8410629d237495e233b2abffafd67ea27622f796..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4538 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 CXU{eO diff --git a/04-multimodal-rag/src/__pycache__/image_processor.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/image_processor.cpython-312.pyc deleted file mode 100644 index b3f13a02dd050f8b2a65cdd66a01228daefcc16c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7366 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 diff --git a/04-multimodal-rag/src/__pycache__/multi_retriever.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/multi_retriever.cpython-312.pyc deleted file mode 100644 index 6551e8367d184f91224a893fa630fcaed02550e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6576 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 diff --git a/04-multimodal-rag/src/__pycache__/multimodal_parser.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/multimodal_parser.cpython-312.pyc deleted file mode 100644 index e95c0f1559acb4e67bfb0e35b4fb195cbf5b3ceb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6329 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 diff --git a/04-multimodal-rag/src/__pycache__/query_router.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/query_router.cpython-312.pyc deleted file mode 100644 index 71e90d61d8d0a4fb6575120b6302c2c891232bf7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4611 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 diff --git a/04-multimodal-rag/src/__pycache__/table_indexer.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/table_indexer.cpython-312.pyc deleted file mode 100644 index 9489953b45595e442d53cae87b5c5111ef0248f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4671 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 B-f92< diff --git a/04-multimodal-rag/src/__pycache__/table_processor.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/table_processor.cpython-312.pyc deleted file mode 100644 index 9d1fe059dcf6b72999bd14aa499e4a667fccc577..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6673 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 diff --git a/04-multimodal-rag/src/__pycache__/text_indexer.cpython-312.pyc b/04-multimodal-rag/src/__pycache__/text_indexer.cpython-312.pyc deleted file mode 100644 index d420b32f9547949e982d3207ef527b00a75ae001..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3990 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 diff --git a/05-agentic-rag-realtime/__pycache__/main.cpython-312.pyc b/05-agentic-rag-realtime/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 771414120503c8f1dae356bfb0e50df5e409ebaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9446 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%0DhmghVcGO0P>X<jk|sA7F&6J-t$ShUbCuf@ zE3hbg#0iZ?(Iq1{;TRb*;(}@{vW*~R5)DxXp=%NgC$gfPz>$PiB^j%!U!q_^sL)Ok zB(Kw`ZFPDQF52}bBwnRRl>sD-Bd5kRCygg3FlJ+fbQBEZ>0U`r)M+Q(i#!a;I)iO7 zJfpW_p>;SCRWF!`ogsjik(P+Bj~ouets@~r<sz~GDTXkS?unm28OKARH=PmkV=|}k zr}4Nbxh&jpq-7BJNRv*Z*Fi2f$V_$rG`Ze}qXuqd-JG1{Oc)b!q|+Ilf#YFKr#gHk zfdjxQ1xywkVC5=~wsvqZs9+o%>-o$x&lm@Y#^a*ofZ!eYseb`&`v}|_U+d3~6$h3B z9Y0He_u`^F{x=FgEmXGc`l#c>j(OMoPZm905YlpnuAMB?n=!|f{7>Ay^zSeJ_QkvB zJ_(nY!7_d75A>1yE!~xdmUmx$^VQ{+@Qp*)58XI={pbSyk?%v_t*c8dM;|&JAy@J2 zLqBq0Vr9DbkM!C5{;doCTd5`glSPcME;3seoJ-7OWxCt!Y5(1WAIBCCoG$Gj`g~W3 zxlpE``vblEJ`<`0L)We@26t9AZ7Frd7B}@)LSgX1e6HfN;I6cG+_-xEYN=^QaR|e- z{@uSF`pv*6@zU<0&mS*6f92lu+~V_G>C#wfY`nxwlxgvg^r8FywwvJv$E~4H=+FG0 z`pf=v#rWS`4s_J~&LjWoSfSu`i~bwFRyxgv`pVOGGxW=jfA09gv-3-D;GVbrci#3t zdAiCTmRzVjRrVbHGT6Lgrvt6aTedG8dhhDJE&CR?>|5G$;2QG{Lxs93jct{N_InLm z7aO)NHEb*GI<nYsWVtbXZL-|Bt<o6&%HMf&wCwL$*-SOGJlqZ%rxYA4drmFadfEGh zXWzFEw^89O<*wdR%U;ku&>gTymm7E7Zw!Od3A8>^(J%S0Q2SsX^;uuXsj%bokPV)? zCy^il&_u*e%=2(0s37oE4&tL59|3siF5`a)3Q;gLMs|%ej?r{-?se}lL}b(~{QJRB z1}JUF9s@Or#F{wjBhOJ#LAkVWTzM7fKprXeWq827l+E^yJ#3>N#3)<i?<w!^DdvBv zu5VnF&Hq|pCD?8Y-5gk<;I(k+0lpsed2RlcHjmA9O?yDWYo#@6+j{MVd*SZIaQ6cW jJ|0ZiUa;BruT0x1+CN|ToumCe?Rov=ii4s%h_m|tHo~N| diff --git a/05-agentic-rag-realtime/src/__pycache__/__init__.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 0e7f6386b66145250ac4fc4ba329dc17004e91f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmX@j%ge<81YMr<GqZv8V-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K76+Z(ypMn4X$f zl9{Yql$fqtl$w}Rl9`*TUtE-|A0MBYmst`YuUAm{i^C>2KczG$)vkyYsFM+hi$RQ! M%#4hTMa)1J0J29O#{d8T diff --git a/05-agentic-rag-realtime/src/__pycache__/agent.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/agent.cpython-312.pyc deleted file mode 100644 index c617cae9b6ebb256b931ab399cda581f7d621dee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5121 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 GdjA8Mdlsbt diff --git a/05-agentic-rag-realtime/src/__pycache__/knowledge_indexer.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/knowledge_indexer.cpython-312.pyc deleted file mode 100644 index a005e64a0c042bac46a1e9058fe246a0264e6893..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5590 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 diff --git a/05-agentic-rag-realtime/src/__pycache__/response_formatter.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/response_formatter.cpython-312.pyc deleted file mode 100644 index 01b6b59879a47debedf1811ad4e4a5495c6385b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5573 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 GvHt*9KE31s diff --git a/05-agentic-rag-realtime/src/__pycache__/tool_registry.cpython-312.pyc b/05-agentic-rag-realtime/src/__pycache__/tool_registry.cpython-312.pyc deleted file mode 100644 index c17f62196364ba07973c635688c012fdca3e38c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4448 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 D76|YK diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/__init__.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 1c3c78a5617646b3a393f0bf71e7dbd76cd9219b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 144 zcmX@j%ge<81YMr<Gpm90V-N=&d}aZPOlPQM&}8&m$xy@u<b4LI`K7O?Z(ypMn4X$f zl9{Yql$fqtl$w}Rl9`*TUtE-|Uy`4nQ>-5!pP83g5+AQuQ2C3)CO1E&G$+-rh!v=x S5r~UHjE~HWjEqIhKo$V0CLuil diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/finance_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/finance_tool.cpython-312.pyc deleted file mode 100644 index 271ead4ccb2634ca709cf4d89ab9d7d2e6c6eaa1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5178 zcmb7ITWl298J^jhy?QSO+gxmHpI``V@UFSkNeYD27#m2iaj}~Ol4>=cne~o6JF`7A zi?Ov!tv0GmR0)Vuu@OZ<QB+Zp2Ojg7DpFOcie&0aY^Hsv(!NCM!zK`^r7!)?%&xr< z(#x^;oPW+a^Pk)QfB$#p_pw-nLvl{Or)}%txWBT8MoQF$`)43r;}q^3r|^nT=kvaE zKAuwqMO6Iw3n)SSg_N)oQCgI!5>w(z;+OGrg3_vr$_7=;ips_}#dChhzN}AagB)0B zPj(dl>EpPFMdbn8G<9nrs~M6Zs~Pr`E-XYMqpB_EREy}^d6ihUDbEqfP>9!0lXRj; zwnWZr5?QFfOLm`?a;8Z}JypqcBr=uLEP{WD%urRbiF!fG7j%`dQOL9^OGQg1noV+& zbudDZK3{sD3=!EhWUAU~KPehgep;I;nnma_3Pqb_O`4bNgT&53jV8~jlvoS-Y3SQM zG&FIlpNvh8PW404uaZyHXxX$V+eNA>#4Os-c#ssRCSxor88av*21fcxUZQiVO=PLC zrexeCLlY;-oVq}$`lF)8G<?4F&ie8#nMwuh*dx0@6m*nr9XopJ<k-onp{bLn$0yep zdyMU3URuCz5JlCc1!yf6ut8+sAjxY+(T2uHH+GkW^T1h1RPkQHPB->$Y;E4|X<;oy zqBuXZNM&fwQZxVP{Ge>RW@OFnn5b;`llhz`=UjCJt2c-#%aWy;hNSE69LG&VWoJOF zJgTty5V1hjz!FosX4x1;HuD9^&{S)EUZd#dRWGD)q&ReTm{^&bo=KDKLj_pH_9U6d zCYZ36Y1Oh*Y8K<zjYHN=bFS!}1|daDGiKIyo^6OKQ8{-ARsm~c+i93Kk@R_K!NT+` zQ-=}DCiRXY9r=Q}SCV(cDHx(7GQ)91W;l-j`03Nf@h~)YZX8u3Q$y%9c?$XXE5}$q zdg`@B|Isrqr;a-OD~|Be#F!(zIyRMzJAR3+!U^X!nVOcFwVl9>Y0jW3B<Y2mG_9gW z%1eu;;fQ(FmYh&llGSM}%<<!xb;$`B=QTx>oS>oFSzWu}gr`+HXXRi+j(A?P@XBl< zZ_*jbaDwxa4l}i#u)CKT7`79D*A%HC^L6-Og@~Vk-~Cb4UgJvKY;!B?fpps|xH50E zzHTb<x9dF>ey)?_==VzetiRE^UMfCCxD7O6>51)PQ+v?s*PJ)TrmfH)Zf^19M*Ad( z_LgsNFZ=9hqjk0cpp6QY4L!%WAI3QD4a~SKl*N)z@}1`_{^u<vQ3kY5V#EI1Jd*z6 z9&C;4CP$l06}WX?rOaK3nS~hx-6)LH11`8U@KVw>rD4(@ZUH8l_fAmFq?*0pK?AGV zmnJ8(F3x!hm<KRCo>QTy4t=|=9DD>gStzI!o(lXjOkx#aS5}gZ4|MgskZIhXCc`-u zNY21hH)WtWbH>kq_I6!e9wrRWT7Tm}nvBBF$+X1YGMi>2St+>M?lthA*U&91X*$ue z7!=E%r=~Ge$EGg=IDm(I%0ZS+h8#bvRVz4xuELP1TF^0JCkO*)j^g;;<v4!boL8xn z$czt-jbx@yXQoaLzdUlr@oTo4w~~I^4W&-VRaH<Gnq-e6Tagp+7GyDVWh*BS52Oe7 z4yL3TAeAPks5Fy8MAB_7uMT``6vWVV@W^=j=w9gCgP-*aWJ_FaBiFk1@{ce5cr`(m zPCN*3p_b+6e_F2ae+X`^<nHyP@Ajl`S$B@E_B?m5=f%4{FMce1d}g)h#J!%0yFC+s z>^W0SY`%Q)(#6$8?@Fk*7T{tXm&Y%SuST~mjr@7bu6tWjcekWgw+wv6`TU)iqRZlP zw%U@oGWgaD)wZo~jn(|9u0^<L`{g5-j;ywHt%zOsF)>bNStGGM5b(Qy1R1ylZ#P;v z#kVelDF|<hWo{h+6tTo9{tpAU+1p5yTr#-$chBmJny$bMPa$Rw=OoP_3`t=g2=X2V zJ;EGZuJFy^Q#bvl$O@Bc1lRN$zJneZijFwQEb+wD)C3D9`J$o8lFfWgGi;T*5vs26 zjEjPGO2JOwr4(842o|&-o3k6tTz=!u(u*_o(B)bf?ZMg~fz&uE+VH#oTNhHp9yfaw zJh|I-@GSeD44Wn2S`VQl%rXagEHQAJ4Lw$g$Fjd9&br+3(YA7+6j%x<LW#T1DdG(Q zfGN(dLE?JfauD@w4UeUA2=%RxRpPN6F8OEKdLK)Qe{BU3C1AHVn!z)Ju%wn!go>qz z61pxLyb>-)OF<e?BBh|xa$UUPbL(QIkXsjOs*5Ypk`F70l>)3*iI-w{N|fTrTT4;o z8+=@;r4(+Aex17!o#gsBdsAZuJ2=`_PSkf|V<~}m+HUw|-&t6EIdL&DSuf}LdG3O6 zmYe63?b6l;q%(_g>90%n))_BiHkhL2(jPEd5M!f(!|U|MMbx?-GDTB>S{HRlL9#3u zaT9TU>@6ZOrVK2EJ26me2I5!Mx?iCOBR?R~FkCQUM!0khq|EZr*&B;dH_edsqT=#v zM5Sr?N<PA7#1cl(K)--)1=B#3nAhwa=rG9^DdtBpSA?<V@hJ=N<BPq+W)Y6h*cJ9g zsb35{_r_{=E$-fxCKI}fNCaAKg3YjC^`xlhL~M#^fk7rlzUg5CMsq#PfWS!062HI$ zjeK)&fQUrc$tu1@kO4)6h4@`t=afKGYhbN4SZ@tAwT9MO!>rZS7hzTjjB6QxD2xvb z&0ysB-|5UBeaP>4D5Upi9e#?{aq6d^4mtc$S6+HE4S8%)>`(8_{+dxr0e1$Axjvx- z$emCoo3}HlTzmm{8p&2O_3M&%)jI5kVe3eGMzulVaLvlF-Au35H(ocP%*rEM;;L<% zt{kd%5tNl3Zy&05_f)K#Q|~U`>RUm$kyg6)uC(uEjeTz)^cpAMUAWa&F>if;rE6fN zec(T~ZSAZasMwWfu3TJc-AVUh#6=;!FH4_=M4w~X^Q<m7$tHV{EVhsmBQLUgknC8D z;bCz9tK@_>lS5my{&>oq$5V*)92kL=fK<P#3`+%6L|`@}l$sRs(EnyZ-rb>}G~fDM z@}5j5qfV$nOx%#|1g6}pq7%+amYrc29!GTZh|Vh98=w<jz%3$^!_@IQ6Tv^6h<6oa zC+G+`Bu8LGJ3MkhRtqfpBwIZi;#<f%F^?r=JYq-F*e6G1BhsTx^n1gE*a(@y{qs%` z12BP(vgS}h%}^H6C=1k10FP`auRD|6?o1HW3$n$4+V$n8%rm+8qqelT=l?LgacH$p z!G1w*TAvHY{}g@dZ`=0VI=Q;-+2yfn$L4Eiub!=xS36S6qqPv%-g&RB?`~V)@-gs{ zYlp5LT0RC6)3Np1>8q!gN2-bT<?>%P4c^i2yk6t@A^tG(j>9!U*su|-Wy`i}7q4Di zK2hD&c`bi6zkIy9wfnu^cYBv#s&4MOcH!!Uo6*(HspS*3KsfgF2W|H`6kl%Q_8h2$ zZnj;$_>q5k^t04pWkcopo5Pj#-Ob652A5BKwr78Zuk5<nSLwdHX&3q&$AsECE2&B^ z22XauZEm(z_CS8RyV||8@)D$7+p638D&K{)b4ztg&y@?1NbhI8yKY5Rdk?Jie5cmp z-|%cr2*%?7c)%FnS6m<*XL|SFN#5x~?-x3&o!ylVNWEwHn&8`ThW}a+WAT60{OI}6 z+KFBL$xzF%$o(chG>GC8F+5C!Phz3rF5#07p2=N)<n#yVx%j)|?At>T*Hy#zAh!N} zX6?HRBP|FO#c5rWaWMy!Dr*<-1>76h?fM`K6)v&y%yR8J3A=MY$#2+QzJ5vfq8O<Y zskOPd2HX!1PtB|9T+<g5h%nrK_oIX7Fznk&Iw{Z;76d0K8nV-(sQ5;rd2ZvQC(uF} z&?k9!+0Cfg_5_oquc7ZT{454au+R1v{qYa_Y8;9W`|h*i!BHO<ZLP+-tFf+{=!>1^ zAB02EK#dFe11^+}CxcEflTl1LlW`I{*sILgYnpwKvfRngXQ14h8zcMfp!6`4*r=|9 zJq#Vrn@UkvkI>gq$H3l_k<|pA=fCu|^5WM!IR2?xoQt<tH*BuPJF2b7+pE!y)z(eb zSR0cQ9n}q6s?kKXwF7?gWp|>@x0I;u*u-zESTzpCP3t}@YWpX7K2Vu@z_HSu#J>Sg Cce$GY diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/rag_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/rag_tool.cpython-312.pyc deleted file mode 100644 index 8f9c31aac93fde0fddf71a3bb1590ac0b465f060..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3232 zcmai0UuYc18K1qo-8-G6+E!#NTBn+*N$N|_tKd)yiZNUqr4m+UCsHs~YEGlwxx1D2 zb}uutvQM1A522MI5L#TU;)eLO*bjc}OWp&0F(d)8w2(OD!Ecw^f#Ror-|XI<tcZ4D zXJ&W)ee?T%zweuWjE)uwJoo26@je|T<Qx3xJ^8fp_!2a35tpnG*Kjkw(afx53~Xop zY%{l#)9svZHmwy)w@p9aEUXj^GDO^A*W#9&=R<CR=W3Qa^ntljY!^!-(UlA#MJb$V z6^6c?7OY;ymfdO>i<bp!Nvay0zEzoDTA~|4c-7}_ozpcYIhC9Vr$L#dj4rUCe!juH zfG)#qDnmVu)p?+(!-As2e4l!XGCvIJ(sMbz%Gc5r%CzPMEO2<4vcQE723rcM4YG^H z3$wqa=PxdvpS`q9A6~ziZ15(Rj__K_3j^v&I=jh5TQ$6(K2xOdEQ8H@^Pfw)u<#oy zICGPeth$4ltFSTmUD%1b+(Dx*&&@94r#2VXlX^7`2&EOe$?6e@v4g#RZ-djR6bUzU zq9&YJPEkMEa6b%@1oM*hz&c!$)<Qu!noi%3xWvQQxQ%RqH8sz9W+&BPib~d&^eVOh zBE8lDp8!zHmHEXb+TecbyTl1i28N<5g;#GVD#A#CIr`iZ_Dx{(0>wqZd<rL#^x^OS zMBl0Wx=ej<lhc;)9G?74-KB|ArcNXTx)}}vN7u8!x77p_yZ8tRTr%R^f+rQ$ND!z^ z%_(RcPexYzC#fR4;*m}6s8CcT6g(bt!zQq+x)1x<5?n<haDc)6$AnfEUM-hDpkNHu z?{qVRb}hF<Y9O8uMz%9<u4CLLuK96xJF9TEeq5`Q>16Na@1}p2NU0EAgb$7T8<7Xh z`2q10FKJ-_Scvyw(g9W@OXq`Dq^2)M3M%S^ffCG7^pxQ5N1os=U27lEl`JKG!pWzT z=`$2GbrosBXv;?dN+r;<C>7j4<t7jeXof#hQHB8wj2tCxZA-FbV$00`SrTa5WJ-aB z-~r+BO<;`RGV*(iEf2O~K@ck1Vp7sepiq!&xV8?Sm9Hnv*P=xUOD;U-dlFVv;dD)e zO{(}cl|oe!bvZcuerf;!`dJ8>3v&~=y};$y&^q8p0_1>Qx{r&&v42Rd8M@R61(@9s z80Zqxts$zH=PJwET4be50d9o!!W-p2h^aZxRNSuHbn5Nb(m)62MCo&fNe~Riyp{-E z@CC?{xlIVVQv>UsSt$KjhRB3gcmYDG?SP<#mgumuPb6t1{-4%ifJAsC<=23Kej6eT zhl7PQg2Q_~7rdj<+~<V~a<=BxBShjr0>FR^bPFkSb$sm;4+65b5j+hG<9T9Jb5jg~ z>Kr2#A66E;&%yinAryZ$ZV}b*?T{OmYuwB9w7G3`3~98pZmw=@X9gPX7_NEGx|>Fb zH_4S5LVgEd+u7}$n_tIhap39X)_W;;U(emNeK)mW+w5f4(clA5Ckt_;&@pe5kF&k8 z2Kn%8j<`dI?w09|g6LA*`cMOcpv(sWq`+DfI6B2)K1z5zz&UYJUuj;j8E)v91H?dx z2NW8}5qP!<c}~(*jrnSly!XeA!K_CZ_aHtS8(Xg~h9D*%gbs1u3ljZf>aB$luoqO& zmzTa8*jp2C9o{qZ2oc2aFBKE&sX@0gHhpRoOQt}7$3r}T94eUNac({I0vOoxJr!H5 z)#8C$%8OwfW;u;0*pRX5djXfR;l-x@0~Ms+K%fsL76>M#*Pl+?)35&Ww2pnAa~iHW zr-7or@|t}5(A!U(PczsSbzr38)&AA!tRI3u<T<;S<8ZHQQ2ggQc~T_LpSl0i-%oya z?2D5ZcC4?D9N&9xwp$=$<p)R0JLW$pUbyq}edCLXGyf*pp;vCq?NoN6uf|W@n)~D2 z?z^A7_m}r>U-^9e7dx|iBgZ~kxUsNX*?nzy?auK#`Q6DcM@rp1OzIZN(eaPkH`=?$ zKR+`0pfLFu_9k2TrQ|-gbyMFf4)d2y-vveEy+#k|T{<8Jt-*Z&{FtWyR_J?<$7Q)6 zn0i+mhIA}bxVPxM><%1n_wo^YHsvJg$kyI1W#e4Hn&4Vwc09Z<P?a)q7KVx(9Ho@g z{u2dgi|4U;0Sbv-tKdv=5*qQ518xyNgTE(m+#D3(imp9o(wnkNp!%!)9;@yQ86JBu z_TqzMFLg=w<x@}$o$6+ZdGyJT$jH&X(HHkd$M?o3pX75RR+r>VOZ*fTTPzt{KkkF$ zgv}NU+IUAwZhNI+AWIcKtJT<2?H1$-f!c|W_;4$B@K$W2Af-NBO#6SUuv9Y&Jk_?7 zjHWS_0>ctqi-cqMWmQeHmWEMG!2o^UzY%SqM&xPXY!kAG&(DchpdWQ6&qC478iw&* zX2i&Sf07sz-xB&Sa^_p|E1=Qj>))B@3~O)fL^lidR}(+!;&;~~R`E$5T6*>W0+t`X AH2?qr diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/weather_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/weather_tool.cpython-312.pyc deleted file mode 100644 index d9f053cfcc07495170c14c33366bbd78877991b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5231 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 XV0~A9$m>!%&>;p3`yUBVak2jf!;00G diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/web_search_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/web_search_tool.cpython-312.pyc deleted file mode 100644 index a5f0dec39beb469ceaea81305cca20af6b3ffcb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5678 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 UUCW{^UktQLp@r0a0*#pSe_tsnga7~l diff --git a/05-agentic-rag-realtime/src/tools/__pycache__/wiki_tool.cpython-312.pyc b/05-agentic-rag-realtime/src/tools/__pycache__/wiki_tool.cpython-312.pyc deleted file mode 100644 index 545e7cbb01855d630b0b4ab03998cc059be15000..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3441 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 IXb#!`2Y>qIQvd(} From 9c280296811792a80e292487ce6889ef0bd4ee62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 01:54:39 +0000 Subject: [PATCH 9/9] Fix: remove base64 stdlib entry from multimodal-rag requirements.txt Co-authored-by: nerdjerry <7092764+nerdjerry@users.noreply.github.com> --- 04-multimodal-rag/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/04-multimodal-rag/requirements.txt b/04-multimodal-rag/requirements.txt index 994aaa8..72c8ada 100644 --- a/04-multimodal-rag/requirements.txt +++ b/04-multimodal-rag/requirements.txt @@ -9,4 +9,3 @@ unstructured[pdf]==0.13.7 pdfplumber==0.11.1 Pillow==10.3.0 pandas==2.2.2 -base64