102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
from fastapi import APIRouter, HTTPException, Request
|
|
|
|
from app.services.recipe_importer import normalize_recipe_from_url
|
|
from app.services.repository import repo
|
|
from app.services.scoring import extract_ingredients, extract_tags, score_recipe
|
|
from app.services.store import JsonStore
|
|
from app.services.recipe_search import recipe_search
|
|
|
|
|
|
router = APIRouter(prefix="/api/search", tags=["search"])
|
|
store = JsonStore()
|
|
MIN_HEALTHY_RESULTS = 5
|
|
|
|
|
|
def _build_suggestion(*, query: str, url: str, source_title: str, source_snippet: str, payload: dict) -> dict:
|
|
profile = repo.get_profile()
|
|
score, reasons = score_recipe(query, payload, profile)
|
|
return {
|
|
"id": store.new_id("suggestion"),
|
|
"title": payload.get("name") or source_title or url,
|
|
"url": url,
|
|
"source_query": query,
|
|
"source_title": source_title or url,
|
|
"source_snippet": source_snippet,
|
|
"payload": payload,
|
|
"ingredients": extract_ingredients(payload),
|
|
"tags": extract_tags(payload),
|
|
"score": score,
|
|
"score_reasons": reasons,
|
|
"status": "suggested",
|
|
"created_at": store.now(),
|
|
"updated_at": store.now(),
|
|
"kitchenowl_status": "not-imported",
|
|
"ready_for_auto_import": bool(profile.get("auto_import_enabled") and score >= int(profile.get("auto_import_threshold", 96))),
|
|
}
|
|
|
|
|
|
@router.post("")
|
|
async def run_search(request: Request):
|
|
payload = await request.json()
|
|
query = str(payload.get("query") or "").strip()
|
|
if not query:
|
|
raise HTTPException(status_code=400, detail="Search query is required.")
|
|
limit = int(payload.get("limit") or 6)
|
|
candidate_queries = recipe_search._candidate_queries(query)
|
|
results = recipe_search.search(query, limit=limit)
|
|
failed_search = None
|
|
if len(results) < MIN_HEALTHY_RESULTS:
|
|
attention_type = "no-results" if not results else "low-result-count"
|
|
notes = "No recipe suggestions returned from the configured search lane." if not results else f"Search returned only {len(results)} unique recipes; more coverage needed."
|
|
existing = next(
|
|
(
|
|
item for item in repo.list_failed_searches()
|
|
if str(item.get("query") or "").strip().lower() == query.lower() and str(item.get("status") or "open").startswith("open")
|
|
),
|
|
None,
|
|
)
|
|
failed_search = {
|
|
"id": str((existing or {}).get("id") or store.new_id("failed-search")),
|
|
"query": query,
|
|
"candidate_queries": candidate_queries,
|
|
"created_at": str((existing or {}).get("created_at") or store.now()),
|
|
"updated_at": store.now(),
|
|
"status": "open",
|
|
"attention_type": attention_type,
|
|
"result_count": len(results),
|
|
"sample_titles": [str(item.get("title") or "") for item in results[:4]],
|
|
"notes": notes,
|
|
}
|
|
if existing:
|
|
repo.update_failed_search(str(existing.get("id")), failed_search)
|
|
else:
|
|
repo.log_failed_search(failed_search)
|
|
return {"status": "ok", "query": query, "results": results, "failed_search": failed_search, "needs_attention": bool(failed_search)}
|
|
|
|
|
|
@router.post("/url")
|
|
async def ingest_url(request: Request):
|
|
payload = await request.json()
|
|
url = str(payload.get("url") or "").strip()
|
|
if not url.startswith("http://") and not url.startswith("https://"):
|
|
raise HTTPException(status_code=400, detail="Recipe URL must start with http:// or https://")
|
|
try:
|
|
normalized = normalize_recipe_from_url(url, timeout=20)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=400, detail=f"Could not normalize recipe link: {exc}") from exc
|
|
suggestion = _build_suggestion(
|
|
query="direct recipe link",
|
|
url=url,
|
|
source_title=url,
|
|
source_snippet="Recipe URL pasted directly into Doris Kitchen.",
|
|
payload=normalized,
|
|
)
|
|
saved = repo.upsert_suggestion(suggestion)
|
|
return {"status": "ok", "url": url, "suggestion": saved}
|
|
|
|
|
|
@router.post("/seed")
|
|
async def seed_queue():
|
|
results = recipe_search.seed_queries()
|
|
return {"status": "ok", "seeded": results, "count": len(results)}
|