Add retry logic and stricter version detection for weak search results

Retries with a refined query when no version number appears alongside
release context words (latest/stable/released/current) in snippets.
Exposes retried flag in response and history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 06:05:06 +02:00
parent 371679d330
commit 0f1d8f4517
+30 -2
View File
@@ -1,6 +1,7 @@
import json import json
import logging import logging
import os import os
import re
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@@ -8,6 +9,9 @@ import httpx
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
VERSION_PATTERN = re.compile(r'\bv?\d+\.\d+[\.\d]*\b')
VERSION_CONTEXT = re.compile(r'\b(latest|stable|current|released?|new version)\b', re.IGNORECASE)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [sam] %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s [sam] %(message)s")
logger = logging.getLogger("sam") logger = logging.getLogger("sam")
@@ -51,6 +55,28 @@ async def search_web(query: str, max_results: int) -> list[dict]:
] ]
def _has_version(results: list[dict]) -> bool:
for r in results:
text = r.get("snippet", "") + " " + r.get("title", "")
if VERSION_PATTERN.search(text) and VERSION_CONTEXT.search(text):
return True
return False
async def search_with_retry(query: str, max_results: int) -> tuple[list[dict], bool]:
"""Search SearXNG, retrying with a refined query if no version number found in results.
Returns (results, retried)."""
results = await search_web(query, max_results)
if _has_version(results):
return results, False
retry_query = query + " latest release version changelog"
logger.info(f"no version found in results — retrying: '{retry_query}'")
retry_results = await search_web(retry_query, max_results)
seen_urls = {r["url"] for r in results}
merged = results + [r for r in retry_results if r["url"] not in seen_urls]
return merged[:max_results], True
async def ollama_synthesize(prompt: str, model: str) -> str: async def ollama_synthesize(prompt: str, model: str) -> str:
payload = { payload = {
"model": model, "model": model,
@@ -163,11 +189,11 @@ async def research(req: ResearchRequest):
logger.info(f"research: query='{req.query}' max_results={req.max_results} model={model}") logger.info(f"research: query='{req.query}' max_results={req.max_results} model={model}")
try: try:
results = await search_web(req.query, req.max_results) results, retried = await search_with_retry(req.query, req.max_results)
except httpx.HTTPError as e: except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"SearXNG error: {e}") raise HTTPException(status_code=502, detail=f"SearXNG error: {e}")
logger.info(f"got {len(results)} results from SearXNG") logger.info(f"got {len(results)} results from SearXNG (retried={retried})")
prompt = build_prompt(req.query, results, req.instructions) prompt = build_prompt(req.query, results, req.instructions)
@@ -195,6 +221,7 @@ async def research(req: ResearchRequest):
"query": req.query, "query": req.query,
"model": model, "model": model,
"summary_chars": len(summary), "summary_chars": len(summary),
"retried": retried,
} }
save_history(history_entry) save_history(history_entry)
(out_dir / "index.html").write_text(render_html(load_history())) (out_dir / "index.html").write_text(render_html(load_history()))
@@ -214,4 +241,5 @@ async def research(req: ResearchRequest):
"summary": summary, "summary": summary,
"model": model, "model": model,
"timestamp": timestamp, "timestamp": timestamp,
"retried": retried,
} }