File size: 4,697 Bytes
b1f401e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/usr/bin/env python3
"""
scripts/ingest_sample.py

Ingest sample documents to test the RAG pipeline end-to-end.
Run from the project root: python scripts/ingest_sample.py
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from dotenv import load_dotenv
load_dotenv()

from backend.ingestion.web_loader import WebLoader
from backend.ingestion.chunker import SmartChunker
from backend.retrieval.vector_store import get_vector_store
from backend.retrieval.bm25_retriever import get_bm25_retriever

SAMPLE_URLS = [
    "https://en.wikipedia.org/wiki/Retrieval-augmented_generation",
    "https://en.wikipedia.org/wiki/Large_language_model",
    "https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)",
]

SAMPLE_TEXTS = [
    {
        "title": "RAG Overview",
        "text": """
Retrieval-Augmented Generation (RAG) is an AI framework that improves the quality of
responses generated by large language models (LLMs) by grounding them in external
knowledge sources. Instead of relying solely on parametric knowledge baked into model
weights, RAG retrieves relevant documents at query time and provides them as context.

The main components of a RAG system are:
1. Document ingestion: Loading, chunking, and embedding documents into a vector store.
2. Retrieval: Using semantic search (and optionally keyword search) to find relevant chunks.
3. Generation: Providing retrieved context to an LLM to generate a grounded answer.

Key advantages of RAG:
- Reduces hallucination by grounding answers in retrieved facts
- Allows knowledge updates without retraining the model
- Enables citation of sources for transparency
- More cost-effective than fine-tuning for knowledge injection
        """,
    },
    {
        "title": "Hybrid Search Explained",
        "text": """
Hybrid search combines dense (semantic) retrieval with sparse (keyword) retrieval.
Dense retrieval uses embedding models to encode text into high-dimensional vectors,
enabling semantic similarity matching. Sparse retrieval uses algorithms like BM25
to match based on term frequency and inverse document frequency.

Reciprocal Rank Fusion (RRF) is the standard method for combining ranked lists from
multiple retrieval systems. The formula is: RRF(d) = sum(1 / (k + rank(d))) where
k=60 is a smoothing constant. RRF is rank-based (not score-based), making it robust
to score distribution differences between retrieval systems.

Re-ranking with cross-encoders (like Cohere Rerank) further improves precision by
scoring each query-document pair jointly, rather than independently. This is the
retrieve-then-rerank pattern used by production systems at Notion, Perplexity, and You.com.
        """,
    },
]


def ingest_urls():
    print("\n=== Ingesting URLs ===")
    loader = WebLoader()
    vs = get_vector_store()
    bm25 = get_bm25_retriever()

    for url in SAMPLE_URLS:
        print(f"  Fetching: {url}")
        try:
            chunks = loader.load_url_sync(url)
            vs.add_chunks(chunks)
            bm25.add_chunks(chunks)
            print(f"  ✓ Ingested {len(chunks)} chunks")
        except Exception as e:
            print(f"  ✗ Failed: {e}")


def ingest_texts():
    print("\n=== Ingesting Sample Texts ===")
    chunker = SmartChunker()
    vs = get_vector_store()
    bm25 = get_bm25_retriever()

    import hashlib
    for item in SAMPLE_TEXTS:
        source_id = "txt_" + hashlib.sha256(item["title"].encode()).hexdigest()[:12]
        chunks = chunker.chunk_text(
            text=item["text"],
            source_id=source_id,
            source_type="text",
            source_name=item["title"],
        )
        vs.add_chunks(chunks)
        bm25.add_chunks(chunks)
        print(f"  ✓ '{item['title']}' → {len(chunks)} chunks")


def test_query():
    print("\n=== Testing Query Pipeline ===")
    from backend.agents.research_agent import get_research_agent
    agent = get_research_agent()

    query = "What is hybrid search and why is it better than pure semantic search?"
    print(f"  Query: {query}")

    result = agent.query(query)
    print(f"\n  Answer:\n{result.answer[:500]}...")
    print(f"\n  Citations:")
    for c in result.citations[:3]:
        print(f"    - {c.source_name} (score: {c.relevance_score:.3f})")


if __name__ == "__main__":
    print("RAG Research Agent — Sample Ingestion Script")
    print("=" * 50)

    ingest_texts()   # Always works (no network needed)
    ingest_urls()    # Requires internet

    test_query()     # Requires OPENAI_API_KEY

    print("\n✅ Done! Your knowledge base is ready.")
    print("   Start the API: uvicorn backend.api.main:app --reload")