fix: full fresh acra.py with Jina web search, no duckduckgo
Browse files
acra.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
| 1 |
-
import os
|
| 2 |
from google import genai
|
| 3 |
from google.genai import types
|
| 4 |
from db import supabase
|
| 5 |
from classifier_inference import classify_query
|
| 6 |
-
from duckduckgo_search import DDGS
|
| 7 |
from typing import List
|
| 8 |
|
| 9 |
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
|
|
@@ -34,29 +33,35 @@ def adaptive_chunk(text, max_tok=512):
|
|
| 34 |
return chunks or [text]
|
| 35 |
|
| 36 |
def web_search(query: str, max_results: int = 5) -> List[dict]:
|
| 37 |
-
"""Web search via Jina AI β no API key, works from cloud
|
| 38 |
-
import httpx
|
| 39 |
try:
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
| 45 |
if r.status_code != 200:
|
| 46 |
-
print(f"Jina
|
| 47 |
return []
|
| 48 |
-
data
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
except Exception as e:
|
| 56 |
print(f"Web search error: {e}")
|
| 57 |
return []
|
| 58 |
|
| 59 |
-
|
| 60 |
def decompose(query):
|
| 61 |
r = client.models.generate_content(model=GEN_MODEL,
|
| 62 |
contents=f"Decompose into 2-4 simpler sub-queries. Numbered list only.\n\nQuery: {query}")
|
|
@@ -68,9 +73,8 @@ def compress(query, chunks):
|
|
| 68 |
numbered = "\n\n".join(f"[{i+1}]\n{c}" for i, c in enumerate(chunks))
|
| 69 |
r = client.models.generate_content(model=GEN_MODEL, contents=(
|
| 70 |
f"You have {len(chunks)} text chunks and a query.\n"
|
| 71 |
-
f"For each chunk
|
| 72 |
-
f"Reply
|
| 73 |
-
f"[1] <extracted sentences or EMPTY>\n[2] <...> etc.\n\n"
|
| 74 |
f"Query: {query}\n\nChunks:\n{numbered}"))
|
| 75 |
import re
|
| 76 |
out = []
|
|
@@ -88,14 +92,14 @@ def vsearch(query, namespace, user_id, k):
|
|
| 88 |
}).execute().data or [])
|
| 89 |
|
| 90 |
PROMPTS = {
|
| 91 |
-
0: "Answer
|
| 92 |
1: "Answer using ONLY the context. Be concise.\n\nContext:\n{ctx}\n\nQuestion: {q}\nAnswer:",
|
| 93 |
2: "Synthesize the context step by step.\n\nContext:\n{ctx}\n\nQuestion: {q}\nAnswer:",
|
| 94 |
3: "Use chain-of-thought reasoning.\n\nContext:\n{ctx}\n\nQuestion: {q}\nAnswer:",
|
| 95 |
}
|
| 96 |
WEB_PROMPT = (
|
| 97 |
"Answer the question using ONLY the web search results below.\n"
|
| 98 |
-
"Be factual and concise.
|
| 99 |
"Web results:\n{ctx}\n\nQuestion: {q}\nAnswer:"
|
| 100 |
)
|
| 101 |
|
|
@@ -117,42 +121,42 @@ async def query_pipeline(query, namespace, top_k, rerank, user_id, use_web=False
|
|
| 117 |
level = cls["level"]
|
| 118 |
k = DEPTH[level]
|
| 119 |
|
| 120 |
-
#
|
| 121 |
if use_web:
|
| 122 |
-
|
| 123 |
-
if not
|
| 124 |
-
return {"answer": "No web results found
|
| 125 |
-
"
|
| 126 |
ctx = "\n\n---\n\n".join(
|
| 127 |
-
f"Source: {h['title']}\n{h['snippet']}" for h in
|
| 128 |
r = client.models.generate_content(model=GEN_MODEL,
|
| 129 |
contents=WEB_PROMPT.format(ctx=ctx, q=query))
|
| 130 |
return {
|
| 131 |
"answer": r.text.strip(),
|
| 132 |
"sources": [{"content": h["snippet"][:200],
|
| 133 |
"metadata": {"title": h["title"], "url": h["url"]},
|
| 134 |
-
"score": 1.0, "source": "web"} for h in
|
| 135 |
"complexity": cls,
|
| 136 |
"retrieval_source": "web",
|
| 137 |
}
|
| 138 |
|
| 139 |
-
#
|
| 140 |
if level == 0:
|
| 141 |
-
|
| 142 |
-
if
|
| 143 |
-
ctx = "\n\n---\n\n".join(h["content"] for h in
|
| 144 |
r = client.models.generate_content(model=GEN_MODEL, contents=(
|
| 145 |
f"Use the context if it contains a relevant answer. "
|
| 146 |
f"Otherwise answer from your own knowledge.\n\n"
|
| 147 |
f"Context:\n{ctx}\n\nQuestion: {query}\nAnswer:"))
|
| 148 |
-
top_score =
|
| 149 |
return {
|
| 150 |
"answer": r.text.strip(),
|
| 151 |
"sources": [{"content": h["content"][:200],
|
| 152 |
"metadata": h.get("metadata", {}),
|
| 153 |
"score": h.get("similarity", 0),
|
| 154 |
"source": "local"}
|
| 155 |
-
for h in
|
| 156 |
"complexity": cls,
|
| 157 |
"retrieval_source": "local" if top_score > 0.5 else "model_knowledge",
|
| 158 |
}
|
|
@@ -161,7 +165,7 @@ async def query_pipeline(query, namespace, top_k, rerank, user_id, use_web=False
|
|
| 161 |
return {"answer": r.text.strip(), "sources": [],
|
| 162 |
"complexity": cls, "retrieval_source": "model_knowledge"}
|
| 163 |
|
| 164 |
-
#
|
| 165 |
hits = []
|
| 166 |
if level == 3:
|
| 167 |
seen = set()
|
|
@@ -172,7 +176,6 @@ async def query_pipeline(query, namespace, top_k, rerank, user_id, use_web=False
|
|
| 172 |
hits = vsearch(query, namespace, user_id, k)
|
| 173 |
|
| 174 |
if not hits:
|
| 175 |
-
# Auto-fallback to web if no local docs found
|
| 176 |
web_hits = web_search(query, max_results=k)
|
| 177 |
if not web_hits:
|
| 178 |
return {"answer": "Nothing found locally or on the web.",
|
|
@@ -197,8 +200,7 @@ async def query_pipeline(query, namespace, top_k, rerank, user_id, use_web=False
|
|
| 197 |
"sources": [{"content": h["content"][:200], "metadata": h.get("metadata", {}),
|
| 198 |
"score": h.get("similarity", 0), "source": "local"}
|
| 199 |
for h in hits[:len(lc)]],
|
| 200 |
-
"complexity": cls,
|
| 201 |
-
"retrieval_source": "local",
|
| 202 |
}
|
| 203 |
|
| 204 |
async def run_acra_pipeline(mode, **kw):
|
|
|
|
| 1 |
+
import os, httpx
|
| 2 |
from google import genai
|
| 3 |
from google.genai import types
|
| 4 |
from db import supabase
|
| 5 |
from classifier_inference import classify_query
|
|
|
|
| 6 |
from typing import List
|
| 7 |
|
| 8 |
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
|
|
|
|
| 33 |
return chunks or [text]
|
| 34 |
|
| 35 |
def web_search(query: str, max_results: int = 5) -> List[dict]:
|
| 36 |
+
"""Web search via Jina AI s.jina.ai β no API key needed, works from cloud."""
|
|
|
|
| 37 |
try:
|
| 38 |
+
import urllib.parse
|
| 39 |
+
encoded = urllib.parse.quote(query)
|
| 40 |
+
r = httpx.get(
|
| 41 |
+
f"https://s.jina.ai/?q={encoded}",
|
| 42 |
+
headers={"Accept": "application/json", "X-Respond-With": "no-content"},
|
| 43 |
+
timeout=20.0,
|
| 44 |
+
follow_redirects=True
|
| 45 |
+
)
|
| 46 |
if r.status_code != 200:
|
| 47 |
+
print(f"Jina returned {r.status_code}: {r.text[:200]}")
|
| 48 |
return []
|
| 49 |
+
data = r.json()
|
| 50 |
+
items = data.get("data", [])
|
| 51 |
+
results = []
|
| 52 |
+
for item in items[:max_results]:
|
| 53 |
+
snippet = item.get("description") or item.get("content","")
|
| 54 |
+
if snippet:
|
| 55 |
+
results.append({
|
| 56 |
+
"title": item.get("title", ""),
|
| 57 |
+
"snippet": snippet[:500],
|
| 58 |
+
"url": item.get("url", "")
|
| 59 |
+
})
|
| 60 |
+
return results
|
| 61 |
except Exception as e:
|
| 62 |
print(f"Web search error: {e}")
|
| 63 |
return []
|
| 64 |
|
|
|
|
| 65 |
def decompose(query):
|
| 66 |
r = client.models.generate_content(model=GEN_MODEL,
|
| 67 |
contents=f"Decompose into 2-4 simpler sub-queries. Numbered list only.\n\nQuery: {query}")
|
|
|
|
| 73 |
numbered = "\n\n".join(f"[{i+1}]\n{c}" for i, c in enumerate(chunks))
|
| 74 |
r = client.models.generate_content(model=GEN_MODEL, contents=(
|
| 75 |
f"You have {len(chunks)} text chunks and a query.\n"
|
| 76 |
+
f"For each chunk extract ONLY sentences relevant to the query.\n"
|
| 77 |
+
f"Reply as [1] <text or EMPTY> [2] <text or EMPTY> etc.\n\n"
|
|
|
|
| 78 |
f"Query: {query}\n\nChunks:\n{numbered}"))
|
| 79 |
import re
|
| 80 |
out = []
|
|
|
|
| 92 |
}).execute().data or [])
|
| 93 |
|
| 94 |
PROMPTS = {
|
| 95 |
+
0: "Answer from your knowledge:\n\n{q}",
|
| 96 |
1: "Answer using ONLY the context. Be concise.\n\nContext:\n{ctx}\n\nQuestion: {q}\nAnswer:",
|
| 97 |
2: "Synthesize the context step by step.\n\nContext:\n{ctx}\n\nQuestion: {q}\nAnswer:",
|
| 98 |
3: "Use chain-of-thought reasoning.\n\nContext:\n{ctx}\n\nQuestion: {q}\nAnswer:",
|
| 99 |
}
|
| 100 |
WEB_PROMPT = (
|
| 101 |
"Answer the question using ONLY the web search results below.\n"
|
| 102 |
+
"Be factual and concise.\n\n"
|
| 103 |
"Web results:\n{ctx}\n\nQuestion: {q}\nAnswer:"
|
| 104 |
)
|
| 105 |
|
|
|
|
| 121 |
level = cls["level"]
|
| 122 |
k = DEPTH[level]
|
| 123 |
|
| 124 |
+
# use_web=True: skip ALL local retrieval, pure Jina web search
|
| 125 |
if use_web:
|
| 126 |
+
hits = web_search(query, max_results=6)
|
| 127 |
+
if not hits:
|
| 128 |
+
return {"answer": "No web results found.", "sources": [],
|
| 129 |
+
"complexity": cls, "retrieval_source": "none"}
|
| 130 |
ctx = "\n\n---\n\n".join(
|
| 131 |
+
f"Source: {h['title']}\nURL: {h['url']}\n{h['snippet']}" for h in hits)
|
| 132 |
r = client.models.generate_content(model=GEN_MODEL,
|
| 133 |
contents=WEB_PROMPT.format(ctx=ctx, q=query))
|
| 134 |
return {
|
| 135 |
"answer": r.text.strip(),
|
| 136 |
"sources": [{"content": h["snippet"][:200],
|
| 137 |
"metadata": {"title": h["title"], "url": h["url"]},
|
| 138 |
+
"score": 1.0, "source": "web"} for h in hits],
|
| 139 |
"complexity": cls,
|
| 140 |
"retrieval_source": "web",
|
| 141 |
}
|
| 142 |
|
| 143 |
+
# L0: check docs first, fall back to model knowledge
|
| 144 |
if level == 0:
|
| 145 |
+
doc_hits = vsearch(query, namespace, user_id, 2)
|
| 146 |
+
if doc_hits:
|
| 147 |
+
ctx = "\n\n---\n\n".join(h["content"] for h in doc_hits)
|
| 148 |
r = client.models.generate_content(model=GEN_MODEL, contents=(
|
| 149 |
f"Use the context if it contains a relevant answer. "
|
| 150 |
f"Otherwise answer from your own knowledge.\n\n"
|
| 151 |
f"Context:\n{ctx}\n\nQuestion: {query}\nAnswer:"))
|
| 152 |
+
top_score = doc_hits[0].get("similarity", 0)
|
| 153 |
return {
|
| 154 |
"answer": r.text.strip(),
|
| 155 |
"sources": [{"content": h["content"][:200],
|
| 156 |
"metadata": h.get("metadata", {}),
|
| 157 |
"score": h.get("similarity", 0),
|
| 158 |
"source": "local"}
|
| 159 |
+
for h in doc_hits if h.get("similarity", 0) > 0.5],
|
| 160 |
"complexity": cls,
|
| 161 |
"retrieval_source": "local" if top_score > 0.5 else "model_knowledge",
|
| 162 |
}
|
|
|
|
| 165 |
return {"answer": r.text.strip(), "sources": [],
|
| 166 |
"complexity": cls, "retrieval_source": "model_knowledge"}
|
| 167 |
|
| 168 |
+
# L1-L3: local vector retrieval
|
| 169 |
hits = []
|
| 170 |
if level == 3:
|
| 171 |
seen = set()
|
|
|
|
| 176 |
hits = vsearch(query, namespace, user_id, k)
|
| 177 |
|
| 178 |
if not hits:
|
|
|
|
| 179 |
web_hits = web_search(query, max_results=k)
|
| 180 |
if not web_hits:
|
| 181 |
return {"answer": "Nothing found locally or on the web.",
|
|
|
|
| 200 |
"sources": [{"content": h["content"][:200], "metadata": h.get("metadata", {}),
|
| 201 |
"score": h.get("similarity", 0), "source": "local"}
|
| 202 |
for h in hits[:len(lc)]],
|
| 203 |
+
"complexity": cls, "retrieval_source": "local",
|
|
|
|
| 204 |
}
|
| 205 |
|
| 206 |
async def run_acra_pipeline(mode, **kw):
|