File size: 4,470 Bytes
e8e3115
 
aaa4ec9
 
 
1d1b426
 
 
e8e3115
 
aaa4ec9
 
 
 
 
 
 
 
 
 
 
 
 
 
e8e3115
aaa4ec9
e8e3115
aaa4ec9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8e3115
 
 
aaa4ec9
 
 
e8e3115
aaa4ec9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8e3115
 
 
 
 
 
 
 
 
 
 
 
 
 
aaa4ec9
 
 
 
 
 
 
 
 
 
 
e8e3115
aaa4ec9
e8e3115
 
 
 
aaa4ec9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8e3115
aaa4ec9
 
e8e3115
 
 
 
 
 
 
aaa4ec9
 
 
 
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import time

from fastapi import FastAPI, BackgroundTasks, HTTPException
from fastapi.middleware.cors import CORSMiddleware

from api.utils import get_namespace_id, logger
from api.crawler import crawl_website
from api.schemas import ChatRequest, IngestRequest
from api.config import vs, flash, system_instruction
from google.genai import types

app = FastAPI(title="RAG Chatbot API", version="1.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=False,
    allow_methods=["*"],
    allow_headers=["*"],
)


def background_ingest_task(url: str, namespace_id: str):
    logger.info(f"Background crawl STARTED for: {url}")
    start = time.perf_counter()
    try:
        crawler_gen = crawl_website(url, limit=50)

        buffer = []
        for source_url, chunks in crawler_gen:
            for chunk in chunks:
                buffer.append({"text": chunk, "source": source_url})

                if len(buffer) >= 50:
                    vs.batch_upsert(buffer, namespace_id)
                    buffer = []

        if buffer:
            vs.batch_upsert(buffer, namespace_id)

        logger.info(f"Background crawl COMPLETED for {namespace_id}")
    except Exception as e:
        logger.error(f"Background task failed for {url}: {e}")

    end = time.perf_counter()
    logger.info(f"Time elapsed: {end - start}")


@app.get("/")
def check_health():
    return {"status": "online", "system": "RAG-Chatbot v1.0"}


@app.post("/check")
def check_endpoint(req: IngestRequest):
    url_str = str(req.url)
    namespace_id = get_namespace_id(url_str)

    try:
        stats = vs.index.describe_index_stats()
        exists = namespace_id in stats.namespaces

        count = 0
        if exists:
            count = stats.namespaces[namespace_id].vector_count

        return {
            "exists": exists and count > 0,
            "namespace": namespace_id,
            "vector_count": count,
        }
    except Exception as e:
        logger.error(f"Check failed: {e}")
        return {"exists": False, "error": str(e)}


@app.post("/ingest")
def ingest_endpoint(req: IngestRequest, background_tasks: BackgroundTasks):
    url_str = str(req.url)
    namespace_id = get_namespace_id(url_str)

    background_tasks.add_task(background_ingest_task, url_str, namespace_id)
    logger.info(f"Background ingest dispatched: {url_str} -> {namespace_id}")

    return {
        "status": "processing",
        "message": "Ingestion started in background.",
        "namespace": namespace_id,
    }


# Add this near your other endpoints
@app.post("/reset")
def reset_endpoint(req: IngestRequest):
    url_str = str(req.url)
    namespace_id = get_namespace_id(url_str)

    success = vs.delete_namespace(namespace_id)

    if success:
        return {"status": "success", "message": f"Memory wiped for {url_str}"}
    else:
        raise HTTPException(status_code=500, detail="Failed to delete namespace")


@app.post("/chat")
def chat_endpoint(req: ChatRequest):
    url_str = str(req.url)
    namespace_id = get_namespace_id(url_str)

    # 1. Retrieval
    results = vs.query_namespace(req.message, namespace_id)

    contexts = []
    sources = set()

    logger.info(f"\n--- DEBUG: RETRIEVED FOR '{req.message}' ---")
    if results and results.matches:
        for i, match in enumerate(results.matches):
            logger.info(
                f"[{i}] Score: {match.score:.4f} | Text: {match.metadata.get('text', '')[:100]}..."
            )
            if match.metadata:
                text = match.metadata.get("text", "")
                src = match.metadata.get("source", None)
                if text:
                    contexts.append(text)
                if src:
                    sources.add(src)

    if not contexts:
        return {
            "answer": "I haven't learned this website yet. Please click 'Train' first!",
            "sources": [],
        }

    # 2. Prompting
    context = "\n\n".join(contexts[:5])

    try:
        response = flash.models.generate_content(
            model="gemini-2.0-flash",
            config=types.GenerateContentConfig(
                system_instruction=system_instruction(url_str, context)
            ),
            contents=req.message,
        )
        return {"answer": response.text, "sources": list(sources)}
    except Exception as e:
        logger.error(f"LLM Error: {e}")
        raise HTTPException(status_code=500, detail="AI Service Error")