File size: 11,658 Bytes
18e58fa | 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 220 221 222 223 224 225 226 227 228 229 230 231 232 | import os
import sys
import json
from dotenv import load_dotenv
load_dotenv()
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from embeddings.embedder import search
from sentence_transformers import CrossEncoder
from api.utils import detect_jurisdiction, auto_context_depth, tier_sources, strip_think_tags
# βββ Reranker ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_reranker = None
def get_reranker():
global _reranker
if _reranker is None:
print("Initializing Reranker (Lazy)...")
_reranker = CrossEncoder('BAAI/bge-reranker-base')
return _reranker
# βββ Provider Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434/api/chat")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3:latest")
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
OPENROUTER_MODEL = "meta-llama/llama-3.3-70b-instruct:free"
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
GROQ_MODEL = "llama-3.3-70b-versatile"
LLM_PROVIDER = os.environ.get("LLM_PROVIDER", "openrouter")
# βββ System Prompt ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SYSTEM_PROMPT = """You are LexRAG, an expert AI counsel for UAE and Indian law, taxation, and accounting.
RULES:
1. If context documents are provided and relevant, answer ONLY from them. Cite source title and jurisdiction.
2. If context is insufficient or the question is off-topic, answer helpfully using your knowledge. Prefix ALL such paragraphs with [INDEPENDENT ANALYSIS].
3. Be concise and structured. Use bullet points for lists of rules or rates.
4. Always end your response with a one-line tag: JURISDICTION: India | UAE | Both | General
5. Never fabricate statute numbers or case names."""
# βββ Retrieval + Reranking ββββββββββββββββββββββββββββββββββββββββββββββββββββ
def search_and_rerank(question: str, jurisdiction: str = None, top_k: int = 5) -> list:
filters = {}
if jurisdiction and jurisdiction != "Both":
filters["jurisdiction"] = [jurisdiction, "Both"]
initial = search(question, top_k=20, filters=filters if filters else None)
if not initial:
return []
try:
pairs = [[question, d["text"]] for d in initial]
scores = get_reranker().predict(pairs)
for i, s in enumerate(scores):
initial[i]["rerank_score"] = float(s)
ranked = sorted(initial, key=lambda x: x["rerank_score"], reverse=True)
return ranked[:top_k]
except Exception as e:
print(f"Rerank error: {e}")
return initial[:top_k]
# βββ Prompt Builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def build_prompt(query: str, context_docs: list, history: list = None, confidence: str = "GROUNDED") -> str:
if context_docs:
ctx = "\n\n---\n\n".join([
f"[Source: {d['source']} | Jurisdiction: {d['jurisdiction']} | Date: {d.get('date','')}]\n"
f"Title: {d['doc_title']}\n\n{d['text']}"
for d in context_docs
])
else:
ctx = "No relevant documents found."
hist_str = ""
if history:
hist_str = "CONVERSATION HISTORY:\n" + "\n".join(
f"{h['role'].upper()}: {h['content']}" for h in history
) + "\n\n"
fallback = ""
if confidence == "SYNTHESIZED":
fallback = "\nNote: No strong document matches found. Provide an independent analysis based on your knowledge and mark paragraphs with [INDEPENDENT ANALYSIS].\n"
return f"""{hist_str}CONTEXT DOCUMENTS:
{ctx}
{fallback}
QUESTION: {query}"""
# βββ Streaming Generators ββββββββββββββββββββββββββββββββββββββββββββββββββββ
import httpx
async def stream_groq(messages: list, model: str = None):
model = model or GROQ_MODEL
in_think = False
async with httpx.AsyncClient(timeout=120.0) as client:
async with client.stream(
"POST", "https://api.groq.com/openai/v1/chat/completions",
headers={"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"},
json={"model": model, "messages": messages, "stream": True}
) as resp:
if resp.status_code != 200:
err_body = await resp.aread()
try: err_json = json.loads(err_body)
except: err_json = {"error": {"message": err_body.decode()}}
msg = err_json.get("error", {}).get("message", "Unknown Groq error")
raise Exception(f"Groq API Error ({resp.status_code}): {msg}")
async for line in resp.aiter_lines():
if not line.startswith("data: "): continue
data = line[6:]
if data.strip() == "[DONE]": break
try:
pdata = json.loads(data)
if "error" in pdata:
raise Exception(f"Groq Stream Error: {pdata['error'].get('message', 'Unknown')}")
token = pdata["choices"][0]["delta"].get("content", "")
if not token: continue
clean, in_think = strip_think_tags(token, in_think)
if clean: yield clean
except Exception as e:
if "Stream Error" in str(e) or "API Error" in str(e): raise e
pass
async def stream_openrouter(messages: list, model: str = None):
model = model or OPENROUTER_MODEL
in_think = False
async with httpx.AsyncClient(timeout=120.0) as client:
async with client.stream(
"POST", "https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json"},
json={"model": model, "messages": messages, "stream": True}
) as resp:
if resp.status_code != 200:
err_body = await resp.aread()
try: err_json = json.loads(err_body)
except: err_json = {"error": {"message": err_body.decode()}}
msg = err_json.get("error", {}).get("message", "Unknown OpenRouter error")
raise Exception(f"OpenRouter API Error ({resp.status_code}): {msg}")
async for line in resp.aiter_lines():
if not line.startswith("data: "): continue
data = line[6:]
if data.strip() == "[DONE]": break
try:
pdata = json.loads(data)
if "error" in pdata:
raise Exception(f"OpenRouter Stream Error: {pdata['error'].get('message', 'Unknown')}")
token = pdata["choices"][0]["delta"].get("content", "")
if not token: continue
clean, in_think = strip_think_tags(token, in_think)
if clean: yield clean
except Exception as e:
if "Stream Error" in str(e) or "API Error" in str(e): raise e
pass
async def stream_ollama(messages: list, model: str = None):
model = model or OLLAMA_MODEL
async with httpx.AsyncClient(timeout=180.0) as client:
async with client.stream(
"POST", OLLAMA_URL,
json={"model": model, "messages": messages, "stream": True}
) as resp:
if resp.status_code != 200:
err_body = await resp.aread()
raise Exception(f"Ollama API Error ({resp.status_code}): {err_body.decode()}")
async for line in resp.aiter_lines():
if not line.strip():
continue
try:
chunk = json.loads(line)
token = chunk.get("message", {}).get("content", "")
if token: yield token
if chunk.get("done"): break
except Exception:
pass
async def stream_provider(messages: list, provider: str, model: str = None):
if provider == "groq":
async for t in stream_groq(messages, model): yield t
elif provider == "openrouter":
async for t in stream_openrouter(messages, model): yield t
elif provider == "ollama":
async for t in stream_ollama(messages, model): yield t
else:
async for t in stream_groq(messages, model): yield t
# βββ Legacy sync query (CLI) ββββββββββββββββββββββββββββββββββββββββββββββββββ
def query_rag(question: str, jurisdiction: str = None, source_type: str = None,
top_k: int = None, provider: str = None, session_id: str = "default") -> dict:
from api.memory import save_message, get_history
from api.utils import parse_citations
provider = provider or LLM_PROVIDER
top_k = top_k or auto_context_depth(question)
jurisdiction = jurisdiction or detect_jurisdiction(question)
docs = search_and_rerank(question, jurisdiction, top_k)
confidence = tier_sources(docs)
history = get_history(session_id, limit=5)
prompt = build_prompt(question, docs, history, confidence)
messages = [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt}]
try:
import httpx as _h
if provider == "groq":
r = _h.Client(timeout=60).post(
"https://api.groq.com/openai/v1/chat/completions",
headers={"Authorization": f"Bearer {GROQ_API_KEY}"},
json={"model": GROQ_MODEL, "messages": messages}
).json()["choices"][0]["message"]["content"]
elif provider == "openrouter":
r = _h.Client(timeout=60).post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {OPENROUTER_API_KEY}"},
json={"model": OPENROUTER_MODEL, "messages": messages}
).json()["choices"][0]["message"]["content"]
else:
r = "Provider not supported in sync mode."
r = parse_citations(r)
except Exception as e:
r = f"Error: {e}"
save_message(session_id, "user", question)
save_message(session_id, "assistant", r, sources=docs, provider=provider)
return {"answer": r, "sources": [{"title": d["doc_title"], "source": d["source"],
"jurisdiction": d["jurisdiction"], "type": d["source_type"], "url": d["url"],
"score": round(d.get("rerank_score", 0), 3)} for d in docs],
"context_used": len(docs), "provider": provider, "session_id": session_id,
"confidence": confidence, "jurisdiction": jurisdiction}
if __name__ == "__main__":
r = query_rag("What is the GST rate on online gaming contest entry fees in India?")
print("ANSWER:", r["answer"][:300])
print("CONFIDENCE:", r["confidence"])
print("JURISDICTION:", r["jurisdiction"]) |