File size: 6,307 Bytes
b2b6341
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
backend/api/evaluation.py

REST endpoints for RAGAS Evaluation
POST /eval/start   β€” kicks off evaluation as a background job
GET  /eval/status/{job_id} β€” polls job progress & results
GET  /eval/results/{job_id} β€” returns final results JSON
"""

from __future__ import annotations

import threading
import time
import uuid
from typing import Any

from fastapi import APIRouter, BackgroundTasks, HTTPException
from pydantic import BaseModel

router = APIRouter()

# ── In-memory job store (survives for the backend session) ───────────────────
# Maps job_id -> {"status": "running"|"done"|"error", "progress": {...}, "result": {...}}
_jobs: dict[str, dict[str, Any]] = {}


class EvalStartRequest(BaseModel):
    n_questions: int = 15  # How many test questions to evaluate (5-30 recommended)
    agentic_mode: bool = False  # Whether to use the deep research pipeline


# ─────────────────────────────────────────────────────────────────────────────
# Background worker
# ─────────────────────────────────────────────────────────────────────────────

def _run_eval_job(job_id: str, n_questions: int, use_agentic: bool = False):
    """Runs in a background thread. Updates _jobs[job_id] progressively."""
    _jobs[job_id]["status"] = "running"
    _jobs[job_id]["progress"] = {"current": 0, "total": n_questions, "message": "Starting..."}

    try:
        # Import here to avoid circular imports at module load time
        from backend.rag.evaluator import run_evaluation
        from backend.rag.multi_retriever import MultiSourceResult
        from backend.rag.generator import generate_answer, _build_citations
        from backend.api.query import _safe_classify, _do_retrieve
        from backend.rag.agent_workflow import run_agentic_workflow

        def retriever_fn(question: str) -> MultiSourceResult:
            analysis = _safe_classify(question)
            
            # Normal retrieval function
            def base_retriever(q, sids):
                return _do_retrieve(q, sids, analysis)

            if use_agentic:
                # Use the agentic research workflow
                multi_result, _ = run_agentic_workflow(
                    question=question,
                    retriever_fn=base_retriever,
                    source_ids=None,
                    is_legal=False # Default to false for generic eval
                )
                return multi_result
            else:
                # Direct retrieval
                return base_retriever(question, None)

        def generator_fn(question: str, multi_result: MultiSourceResult) -> str:
            try:
                result = generate_answer(question, multi_result, history=None)
                return result.answer
            except Exception as e:
                return f"[Generation error: {e}]"

        def progress_cb(current: int, total: int, msg: str):
            mode_prefix = "[DEEP] " if use_agentic else "[NORMAL] "
            _jobs[job_id]["progress"] = {
                "current": current,
                "total": total,
                "message": mode_prefix + msg,
            }

        result = run_evaluation(
            retriever_fn=retriever_fn,
            generator_fn=generator_fn,
            n_questions=n_questions,
            progress_cb=progress_cb,
        )

        _jobs[job_id]["status"] = "done"
        _jobs[job_id]["result"] = result
        _jobs[job_id]["progress"]["message"] = "Evaluation complete!"

    except Exception as e:
        print(f"[EvalAPI] Job {job_id[:8]} failed: {e}")
        _jobs[job_id]["status"] = "error"
        _jobs[job_id]["error"] = str(e)


# ─────────────────────────────────────────────────────────────────────────────
# Endpoints
# ─────────────────────────────────────────────────────────────────────────────

@router.post("/start")
def start_evaluation(req: EvalStartRequest):
    """
    POST /eval/start
    Kicks off a RAGAS evaluation run in the background.
    Returns a job_id to poll with GET /eval/status/{job_id}
    """
    # Limit concurrent jobs
    running = [j for j in _jobs.values() if j.get("status") == "running"]
    if running:
        raise HTTPException(
            status_code=409,
            detail="An evaluation is already running. Wait for it to finish.",
        )

    n = max(5, min(req.n_questions, 25))  # clamp 5-25 for Groq limits
    job_id = str(uuid.uuid4())
    _jobs[job_id] = {"status": "queued", "progress": {}, "result": None}

    t = threading.Thread(target=_run_eval_job, args=(job_id, n, req.agentic_mode), daemon=True)
    t.start()

    return {"job_id": job_id, "status": "queued", "n_questions": n}


@router.get("/status/{job_id}")
def get_eval_status(job_id: str):
    """
    GET /eval/status/{job_id}
    Returns current status, progress, and result (when done).
    Frontend polls this every 2 seconds.
    """
    if job_id not in _jobs:
        raise HTTPException(status_code=404, detail="Job not found")

    job = _jobs[job_id]
    response = {
        "job_id": job_id,
        "status": job.get("status"),
        "progress": job.get("progress", {}),
    }

    if job.get("status") == "done":
        response["result"] = job.get("result")

    if job.get("status") == "error":
        response["error"] = job.get("error")

    return response


@router.get("/jobs")
def list_jobs():
    """GET /eval/jobs β€” list all evaluation jobs (for debug)."""
    return [
        {
            "job_id": jid,
            "status": j.get("status"),
            "n_evaluated": j.get("result", {}).get("n_evaluated") if j.get("result") else None,
        }
        for jid, j in _jobs.items()
    ]