Files
foodlinkk-command-center/tools-api/app/research.py
T
2026-06-09 10:41:13 +00:00

203 lines
7.6 KiB
Python

"""Research pipeline: data providers, snapshots, briefs."""
from __future__ import annotations
import json
import urllib.request
from datetime import date, datetime, timedelta, timezone
from typing import Any
from fastapi import APIRouter, HTTPException
from app.db import execute, execute_returning, fetch_all, fetch_one, json_param
from app.middleware import log_agent_event
router = APIRouter(prefix="/research", tags=["research"])
def _json_safe(obj: Any) -> Any:
if isinstance(obj, dict):
return {k: _json_safe(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_json_safe(x) for x in obj]
if hasattr(obj, "isoformat"):
return obj.isoformat()
if type(obj).__name__ == "Decimal":
return float(obj)
return obj
def _http_get(url: str, timeout: int = 30) -> dict:
with urllib.request.urlopen(url, timeout=timeout) as resp:
return json.loads(resp.read().decode())
@router.get("/providers")
def list_providers() -> dict[str, Any]:
rows = fetch_all("SELECT * FROM data_providers ORDER BY name")
return {"items": [dict(r) for r in rows]}
@router.post("/providers/{provider_id}/refresh")
def refresh_provider(provider_id: int) -> dict[str, Any]:
prov = fetch_one("SELECT * FROM data_providers WHERE id = %s", (provider_id,))
if not prov:
raise HTTPException(404, "Provider not found")
name = prov["name"]
payload: dict[str, Any] = {}
count = 0
if name == "crm":
clients = fetch_all("SELECT id, name, stage FROM clients ORDER BY updated_at DESC LIMIT 20")
deals = fetch_all(
"SELECT id, title, value, stage, next_action, deadline FROM deals ORDER BY updated_at DESC LIMIT 20"
)
payload = {
"clients": _json_safe([dict(c) for c in clients]),
"deals": _json_safe([dict(d) for d in deals]),
}
count = len(clients) + len(deals)
elif name == "weather":
url = (
"https://api.open-meteo.com/v1/forecast?"
"latitude=52.37&longitude=4.89&daily=temperature_2m_max,precipitation_sum&timezone=Europe%2FAmsterdam&forecast_days=7"
)
payload = _http_get(url)
count = len(payload.get("daily", {}).get("time", []))
for i, day in enumerate(payload.get("daily", {}).get("time", [])[:7]):
temps = payload["daily"].get("temperature_2m_max", [])
prec = payload["daily"].get("precipitation_sum", [])
execute(
"""INSERT INTO weather_data (region, city, date, temperature_c, precipitation_mm, weather_condition, data_source)
VALUES ('Noord-Holland', 'Amsterdam', %s, %s, %s, 'forecast', 'open-meteo')""",
(day, temps[i] if i < len(temps) else None, prec[i] if i < len(prec) else None),
)
elif name == "social":
rows = fetch_all(
"SELECT platform, text, sentiment_score, created_at FROM social_mentions ORDER BY created_at DESC LIMIT 30"
)
payload = _json_safe({"mentions": [dict(r) for r in rows]})
count = len(rows)
elif name == "retail_manual":
rows = fetch_all("SELECT id, name, chain, city, partnership_status FROM supermarkets ORDER BY id")
payload = _json_safe({"stores": [dict(r) for r in rows]})
count = len(rows)
elif name == "cbs":
from app import retail_enrichment
status = retail_enrichment.enrichment_status()
batch = retail_enrichment.enrich_batch(limit=30, offset=0)
payload = _json_safe({"status": status, "batch": batch})
count = batch.get("ok", 0)
elif name == "pdok":
from app.connectors import pdok as pdok_conn
sample = fetch_all(
"SELECT DISTINCT postcode FROM supermarkets WHERE postcode <> '0000AA' LIMIT 5"
)
lookups = [pdok_conn.lookup_postcode(r["postcode"]) for r in sample]
payload = _json_safe({"lookups": [x for x in lookups if x]})
count = len(payload.get("lookups", []))
else:
payload = {"status": "noop"}
snap = execute_returning(
"""INSERT INTO data_snapshots (provider_id, payload, record_count)
VALUES (%s, %s, %s) RETURNING id, fetched_at""",
(provider_id, json_param(payload), count),
)
execute(
"UPDATE data_providers SET last_fetch_at = NOW(), last_status = 'ok' WHERE id = %s",
(provider_id,),
)
log_agent_event(
agent_name="research",
event_type="data_refresh",
title=f"Provider {name} refreshed",
metadata={"provider_id": provider_id, "records": count},
)
return {"provider": name, "snapshot_id": snap["id"], "record_count": count}
@router.get("/briefs")
def list_briefs(limit: int = 20) -> dict[str, Any]:
rows = fetch_all(
"SELECT * FROM research_briefs ORDER BY generated_at DESC LIMIT %s",
(limit,),
)
return {"items": [dict(r) for r in rows]}
def _build_brief(domain: str, title: str, summary: str, findings: list[dict]) -> dict:
row = execute_returning(
"""INSERT INTO research_briefs (domain, title, summary, key_findings, expires_at)
VALUES (%s, %s, %s, %s, %s) RETURNING *""",
(
domain,
title,
summary,
json_param(findings),
datetime.now(timezone.utc) + timedelta(days=1),
),
)
return dict(row)
@router.post("/run")
def run_research() -> dict[str, Any]:
providers = fetch_all("SELECT id, name FROM data_providers WHERE is_active = true")
snapshot_ids = []
for p in providers:
result = refresh_provider(int(p["id"]))
snapshot_ids.append(result.get("snapshot_id"))
clients_n = fetch_one("SELECT COUNT(*) AS c FROM clients")["c"]
deals = fetch_all("SELECT title, value, stage FROM deals WHERE stage NOT IN ('won','lost')")
pipeline = sum(float(d.get("value") or 0) for d in deals)
stores = fetch_one("SELECT COUNT(*) AS c FROM supermarkets")["c"]
mentions = fetch_one("SELECT COUNT(*) AS c FROM social_mentions WHERE created_at > NOW() - interval '7 days'")["c"]
crm_brief = _build_brief(
"crm",
f"CRM snapshot {date.today()}",
f"{clients_n} klanten, pipeline €{pipeline:,.0f}, {len(deals)} actieve deals.",
[{"finding": f"Pipeline €{pipeline:,.0f}", "relevance": "high"}],
)
retail_brief = _build_brief(
"retail",
f"Retail NL {date.today()}",
f"{stores} supermarkten in database, partnerships actief/proposal gemapt.",
[{"finding": f"{stores} locaties geladen", "relevance": "medium"}],
)
social_brief = _build_brief(
"social",
f"Social week {date.today()}",
f"{mentions} mentions afgelopen 7 dagen.",
[{"finding": f"{mentions} mentions", "relevance": "medium"}],
)
log_agent_event(
agent_name="research",
event_type="research_run",
title="Full research cycle completed",
metadata={"briefs": 3, "snapshots": len(snapshot_ids)},
)
from app.connectors.project_assets import register_agent_output
for brief, domain in [
(crm_brief, "crm"),
(retail_brief, "retail"),
(social_brief, "social"),
]:
try:
register_agent_output(
asset_type="research_brief",
title=brief.get("title") or f"Research {domain}",
ref_id=str(brief.get("id")),
payload={"domain": domain, "summary": brief.get("summary"), "brief_id": brief.get("id")},
source_agent="research",
)
except Exception:
pass
return {
"snapshots": snapshot_ids,
"briefs": [crm_brief["id"], retail_brief["id"], social_brief["id"]],
}