yakub
feat: implement real-time source ingestion progress, chitchat intent classifier, and empty state fallback
fd1e711 | import json | |
| import re | |
| from dataclasses import dataclass | |
| from groq import Groq | |
| from backend.config import GROQ_API_KEY, GROQ_MODEL, GROQ_CLASSIFIER_MODEL | |
| from backend.database.connection import get_connection | |
| class QueryAnalysis: | |
| intent : str # "single_source" | "comparison" | "synthesis" | |
| source_types : list[str] # ["legal_statute", "legal_judgment", "web", "youtube", "any"] | |
| topics : list[str] # extracted topics like ["murder", "302", "IPC"] | |
| ipc_sections : list[str] # ["302", "304"] if mentioned | |
| time_filter : str | None # "2024" if year mentioned | |
| language_hint : str # "en" | "hi" — detected from query language | |
| requires_compare: bool # True if "compare", "difference", "vs" in query | |
| requires_summary: bool # True if "common", "all documents", "across" in query | |
| source_names : list[str] # ["IPC", "CrPC"] if explicitly named | |
| temporal_markers: list[int] # [180, 360] for 3min, 6min mentioned in video context | |
| route : str = "rag" # "chat" or "rag" | |
| def classify_query(question: str) -> QueryAnalysis: | |
| client = Groq(api_key=GROQ_API_KEY) | |
| system_prompt = """ | |
| Analyze the user's query. Determine if it is a general chitchat message, casual greeting ("hi", "hello", "how are you"), bot capability question ("what can you do?", "who are you?"), or simple polite message ("thanks", "thank you") that should be routed to standard chitchat response ("chat") OR if it is a research, lookup, summarization, comparison, or file-specific query that requires searching/retrieving facts from the knowledge base/documents/videos ("rag"). | |
| Return ONLY valid JSON with these exact keys: | |
| { | |
| "route": "chat" | "rag", | |
| "intent": "single_source" | "comparison" | "synthesis", | |
| "source_types": list of "legal_statute"|"legal_judgment"|"web"|"youtube"|"any", | |
| "topics": list of topic strings, | |
| "ipc_sections": list of section numbers as strings, | |
| "time_filter": year string or null, | |
| "language_hint": "en" or "hi", | |
| "requires_compare": boolean, | |
| "requires_summary": boolean, | |
| "source_names": list of named sources, | |
| "temporal_markers": list of integers (seconds) for any time marks mentioned like "3 minutes", "10:30", "at 45 seconds". | |
| Convert all to total seconds. | |
| } | |
| """ | |
| try: | |
| response = client.chat.completions.create( | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": f"Query: {question}"} | |
| ], | |
| model=GROQ_CLASSIFIER_MODEL, | |
| max_tokens=300, | |
| stream=False, | |
| response_format={"type": "json_object"} | |
| ) | |
| data = json.loads(response.choices[0].message.content) | |
| return QueryAnalysis( | |
| intent=data.get("intent", "single_source"), | |
| source_types=data.get("source_types", ["any"]), | |
| topics=data.get("topics", []), | |
| ipc_sections=data.get("ipc_sections", []), | |
| time_filter=data.get("time_filter"), | |
| language_hint=data.get("language_hint", "en"), | |
| requires_compare=data.get("requires_compare", False), | |
| requires_summary=data.get("requires_summary", False), | |
| source_names=data.get("source_names", []), | |
| temporal_markers=data.get("temporal_markers", []), | |
| route=data.get("route", "rag") | |
| ) | |
| except Exception as e: | |
| print(f"[QueryClassifier] Error: {e}") | |
| return QueryAnalysis( | |
| intent="single_source", | |
| source_types=["any"], | |
| topics=[], | |
| ipc_sections=[], | |
| time_filter=None, | |
| language_hint="en", | |
| requires_compare=False, | |
| requires_summary=False, | |
| source_names=[], | |
| temporal_markers=[], | |
| route="rag" | |
| ) | |
| def extract_source_filter(analysis: QueryAnalysis, available_source_ids: list[str] | None = None) -> list[str] | None: | |
| source_ids = [] | |
| # 1. If explicitly named sources are found (e.g., "IPC", "Constitution") | |
| if analysis.source_names: | |
| try: | |
| with get_connection() as conn: | |
| cursor = conn.cursor(dictionary=True) | |
| for name in analysis.source_names: | |
| query = "SELECT id FROM sources WHERE title LIKE %s" | |
| cursor.execute(query, (f"%{name}%",)) | |
| rows = cursor.fetchall() | |
| source_ids.extend([row['id'] for row in rows]) | |
| except Exception as e: | |
| print(f"[QueryClassifier] DB Error in filter extraction (names): {e}") | |
| # 2. If source types are specified (e.g., "youtube", "web") | |
| if analysis.source_types and "any" not in analysis.source_types: | |
| try: | |
| type_map = { | |
| "youtube": "youtube", | |
| "web": "url", | |
| "legal_statute": "pdf", | |
| "legal_judgment": "pdf" | |
| } | |
| with get_connection() as conn: | |
| cursor = conn.cursor(dictionary=True) | |
| for stype in analysis.source_types: | |
| db_type = type_map.get(stype) | |
| if db_type: | |
| cursor.execute("SELECT id FROM sources WHERE type = %s", (db_type,)) | |
| rows = cursor.fetchall() | |
| source_ids.extend([row['id'] for row in rows]) | |
| except Exception as e: | |
| print(f"[QueryClassifier] DB Error in filter extraction (types): {e}") | |
| # 3. Deduplicate and filter by available_source_ids if provided | |
| source_ids = list(set(source_ids)) | |
| if available_source_ids and source_ids: | |
| source_ids = [sid for sid in source_ids if sid in available_source_ids] | |
| # If no specific filters found and "any" is allowed, return None for global search | |
| if not source_ids and "any" in analysis.source_types: | |
| return None | |
| return source_ids if source_ids else None | |