File size: 8,034 Bytes
1171cba
 
649d2d5
1171cba
 
 
649d2d5
1171cba
a037cf8
41018f6
b1de6d2
41018f6
 
7715973
 
41018f6
64fd9b7
41018f6
7715973
a7ef914
40a908e
78bd110
41018f6
b1de6d2
 
78bd110
8ea6d3a
78bd110
 
41018f6
78bd110
 
 
 
 
8ea6d3a
78bd110
 
41018f6
 
 
 
 
 
 
 
8ea6d3a
a037cf8
41018f6
a037cf8
 
41018f6
a037cf8
 
41018f6
 
 
 
a037cf8
 
 
 
41018f6
7715973
a037cf8
 
41018f6
7715973
8ea6d3a
40a908e
 
 
 
 
 
b1de6d2
 
41018f6
b1de6d2
41018f6
 
b1de6d2
40a908e
a7ef914
 
41018f6
8ea6d3a
41018f6
a7ef914
41018f6
 
 
 
 
 
 
8ea6d3a
78bd110
a7ef914
41018f6
a7ef914
64fd9b7
41018f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7715973
41018f6
 
 
 
 
 
 
 
7715973
41018f6
 
 
 
 
 
 
64fd9b7
 
41018f6
 
 
 
 
 
 
 
 
 
 
 
7715973
41018f6
 
 
7715973
41018f6
 
7715973
41018f6
 
 
 
 
7715973
41018f6
 
 
 
 
 
 
7715973
41018f6
64fd9b7
 
a037cf8
41018f6
40a908e
 
41018f6
40a908e
41018f6
 
 
 
40a908e
41018f6
 
 
 
40a908e
 
 
 
 
 
 
 
41018f6
 
 
 
 
 
40a908e
26ad320
649d2d5
 
 
 
 
 
 
 
 
 
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
from app.storage import DATA_DIR, INDEX_DIR, HISTORY_JSON

from app.storage import DATA_DIR, INDEX_DIR, HISTORY_JSON



# app/api.py


import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List

import faiss
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from pydantic import BaseModel, Field

from .rag_system import SimpleRAG, UPLOAD_DIR, INDEX_DIR

__version__ = "1.3.2"

app = FastAPI(title="RAG API", version=__version__)

# в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ CORS в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],          # tighten if needed
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ Core singleton & metrics в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ
rag = SimpleRAG()

METRICS: Dict[str, Any] = {
    "questions_answered": 0,
    "avg_ms": 0.0,
    "last7_questions": [5, 8, 12, 7, 15, 11, 9],  # placeholder sample
    "last_added_chunks": 0,
}
HISTORY: List[Dict[str, Any]] = []  # [{"question":..., "timestamp":...}]

# в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ Models в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ
class UploadResponse(BaseModel):
    message: str
    filename: str
    chunks_added: int
    total_chunks: int

class AskRequest(BaseModel):
    question: str = Field(min_length=3)
    top_k: int = 5
    # Optional routing hint: "all" (default) or "last"
    scope: str = Field(default="all", pattern="^(all|last)$")

class AskResponse(BaseModel):
    answer: str
    contexts: List[str]
    used_top_k: int

class HistoryResponse(BaseModel):
    total_chunks: int
    history: List[Dict[str, Any]]

# в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ Routes в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ
@app.get("/")
def root():
    return RedirectResponse(url="/docs")

@app.get("/health")
def health():
    return {
        "status": "ok",
        "version": __version__,
        "summarizer": "extractive_en + translate + keyword_fallback",
        "faiss_ntotal": getattr(rag.index, "ntotal", 0),
        "model_dim": getattr(rag, "embed_dim", None),
    }

@app.get("/debug/translate")
def debug_translate():
    """
    Simple smoke test for the AZ→EN translator pipeline (if available).
    """
    try:
        from transformers import pipeline  # type: ignore
        tr = pipeline(
            "translation",
            model="Helsinki-NLP/opus-mt-az-en",
            cache_dir=str(rag.cache_dir),
            device=-1,
        )
        out = tr("SЙ™nЙ™d tЙ™miri vЙ™ quraЕџdД±rД±lmasД± ilЙ™ baДџlД± iЕџlЙ™r gГ¶rГјlГјb.", max_length=80)[0]["translation_text"]
        return {"ok": True, "example_out": out}
    except Exception as e:
        return {"ok": False, "error": str(e)}

@app.post("/upload_pdf", response_model=UploadResponse)
def upload_pdf(file: UploadFile = File(...)):
    """
    Accepts a PDF, extracts text, embeds, and adds to FAISS index.
    """
    name = file.filename or "uploaded.pdf"
    if not name.lower().endswith(".pdf"):
        raise HTTPException(status_code=400, detail="Only .pdf files are accepted.")

    dest = UPLOAD_DIR / name
    try:
        # Save whole file to disk
        data = file.file.read()
        if not data:
            raise HTTPException(status_code=400, detail="Empty file.")
        dest.write_bytes(data)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to save PDF: {e}")

    try:
        added = rag.add_pdf(dest)
        if added == 0:
            raise HTTPException(status_code=400, detail="No extractable text found (likely a scanned PDF).")
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Indexing failed: {e}")

    METRICS["last_added_chunks"] = int(added)
    return UploadResponse(
        message="indexed",
        filename=name,
        chunks_added=added,
        total_chunks=len(rag.chunks),
    )

@app.post("/ask_question", response_model=AskResponse)
def ask_question(req: AskRequest):
    """
    Retrieves top_k contexts and synthesizes an extractive answer.
    Supports optional scope hint: "all" or "last".
    """
    q = (req.question or "").strip()
    if len(q) < 3:
        raise HTTPException(status_code=400, detail="Question is too short.")

    start = time.perf_counter()

    # Prefer calling with scope if rag_system supports it; otherwise fallback.
    try:
        pairs = rag.search(q, k=req.top_k, scope=req.scope)  # type: ignore[arg-type]
    except TypeError:
        pairs = rag.search(q, k=req.top_k)

    contexts = [t for (t, _) in pairs]
    answer = rag.synthesize_answer(q, contexts, max_sentences=4)

    # metrics
    elapsed_ms = (time.perf_counter() - start) * 1000.0
    METRICS["questions_answered"] += 1
    n = METRICS["questions_answered"]
    METRICS["avg_ms"] = (METRICS["avg_ms"] * (n - 1) + elapsed_ms) / n

    # history (cap to last 200)
    HISTORY.append({
        "question": q,
        "timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds"),
    })
    if len(HISTORY) > 200:
        del HISTORY[: len(HISTORY) - 200]

    return AskResponse(answer=answer, contexts=contexts, used_top_k=int(req.top_k))

@app.get("/get_history", response_model=HistoryResponse)
def get_history():
    return {"total_chunks": len(rag.chunks), "history": HISTORY[-50:]}

@app.get("/stats")
def stats():
    return {
        "documents_indexed": len(list(UPLOAD_DIR.glob("*.pdf"))),
        "questions_answered": METRICS["questions_answered"],
        "avg_ms": round(float(METRICS["avg_ms"]), 2),
        "last7_questions": METRICS.get("last7_questions", []),
        "total_chunks": len(rag.chunks),
        "faiss_ntotal": getattr(rag.index, "ntotal", 0),
        "model_dim": getattr(rag, "embed_dim", None),
        "last_added_chunks": METRICS.get("last_added_chunks", 0),
        "version": __version__,
    }

@app.post("/reset_index")
def reset_index():
    try:
        rag.index = faiss.IndexFlatIP(rag.embed_dim)
        rag.chunks = []
        rag.last_added = []
        # remove persisted files if present
        (INDEX_DIR / "faiss.index").unlink(missing_ok=True)
        (INDEX_DIR / "meta.npy").unlink(missing_ok=True)
        # persist empty state
        rag._persist()
        return {"message": "index reset", "ntotal": getattr(rag.index, "ntotal", 0)}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.on_event("startup")
async def _ensure_dirs():
    try:
        INDEX_DIR.mkdir(parents=True, exist_ok=True)
        # HISTORY_JSON parent is DATA_DIR
        HISTORY_JSON.parent.mkdir(parents=True, exist_ok=True)
    except Exception:
        # boot-un dayanmasının qarşısını alaq
        pass