SysOps: manual — 2026-07-30 09:33 UTC
This commit is contained in:
@@ -10,6 +10,12 @@ class Settings:
|
||||
OLLAMA_MODEL: str = os.getenv("OLLAMA_MODEL", "qwen3:8b")
|
||||
TOOLS_API_URL: str = os.getenv("TOOLS_API_URL", "http://tools-api:8700")
|
||||
HERMAN_ORCHESTRATOR_URL: str = os.getenv("HERMAN_ORCHESTRATOR_URL", "http://10.4.7.19:8090")
|
||||
# OpenRouter (optioneel — key ook via Instellingen → AI / LLM)
|
||||
OPENROUTER_API_KEY: str = os.getenv("OPENROUTER_API_KEY", "")
|
||||
OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "deepseek/deepseek-v4-flash")
|
||||
OPENROUTER_BASE_URL: str = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
|
||||
# orchestrator | router | auto (auto = orchestrator, fallback naar standaard LLM provider)
|
||||
HERMAN_LLM_BACKEND: str = os.getenv("HERMAN_LLM_BACKEND", "auto")
|
||||
WHISPER_API_URL: str = os.getenv("WHISPER_API_URL", "http://10.4.7.19:8877/v1/audio/transcriptions")
|
||||
WHISPER_MODEL: str = os.getenv("WHISPER_MODEL", "Systran/faster-whisper-base")
|
||||
WHISPER_LANGUAGE: str = os.getenv("WHISPER_LANGUAGE", "nl")
|
||||
|
||||
@@ -276,6 +276,39 @@ async def api_tenders(country: Optional[str] = None, status: Optional[str] = "op
|
||||
return await _proxy("GET", "/tenders", params=params)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get("/api/export-intel/settings")
|
||||
async def api_export_intel_settings_get():
|
||||
return await _proxy("GET", "/settings")
|
||||
|
||||
|
||||
@router.put("/api/export-intel/settings")
|
||||
async def api_export_intel_settings_put(request: Request):
|
||||
body = await request.json()
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
r = await client.put(f"{TOOLS}/export-intel/settings", json=body)
|
||||
return r.json()
|
||||
|
||||
|
||||
@router.post("/api/export-intel/sync/places")
|
||||
async def api_sync_places(request: Request):
|
||||
body = {}
|
||||
if request.headers.get("content-type", "").startswith("application/json"):
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
async with httpx.AsyncClient(timeout=7200.0) as client:
|
||||
r = await client.post(f"{TOOLS}/export-intel/sync/places", json=body)
|
||||
return r.json()
|
||||
|
||||
|
||||
@router.get("/api/export-intel/sync/status")
|
||||
async def api_sync_status():
|
||||
return await _proxy("GET", "/sync/status")
|
||||
|
||||
@router.post("/api/export-intel/sync/{kind}")
|
||||
async def api_sync(kind: str, request: Request):
|
||||
body = {}
|
||||
@@ -284,7 +317,7 @@ async def api_sync(kind: str, request: Request):
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
if kind not in ("contacts", "caterers", "distributors", "customers", "tenders", "all", "world", "region"):
|
||||
if kind not in ("contacts", "caterers", "distributors", "customers", "tenders", "all", "world", "region", "missing", "places"):
|
||||
return {"error": "unknown sync kind"}
|
||||
timeout = 7200.0 if kind in ("world", "region", "all") else 600.0
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
|
||||
@@ -406,6 +406,7 @@ def llm_presets() -> dict[str, Any]:
|
||||
|
||||
@settings_router.get("/llm")
|
||||
def llm_list() -> dict[str, Any]:
|
||||
llm_router.ensure_openrouter_placeholder()
|
||||
items = llm_router.list_providers()
|
||||
default = next((i for i in items if i.get("is_default")), items[0] if items else None)
|
||||
return {"providers": items, "default": default, "presets": llm_router.list_presets()}
|
||||
@@ -415,7 +416,7 @@ def llm_list() -> dict[str, Any]:
|
||||
def llm_create(body: LlmProviderBody) -> dict[str, Any]:
|
||||
preset = llm_router.LLM_PRESETS.get(body.provider_type, {})
|
||||
base_url = (body.api_base_url or preset.get("api_base_url") or "").strip() or None
|
||||
model = body.model or (preset.get("models") or ["deepseek-chat"])[0]
|
||||
model = body.model or preset.get("default_model") or (preset.get("models") or ["deepseek-chat"])[0]
|
||||
if body.is_default:
|
||||
execute("UPDATE llm_providers SET is_default = FALSE, updated_at = NOW()")
|
||||
row = fetch_one(
|
||||
|
||||
@@ -119,6 +119,40 @@ def _agent_steps(delegated: list[str], routing_reason: str = "", extra: list[dic
|
||||
return steps
|
||||
|
||||
|
||||
|
||||
async def _chat_via_llm_router(message: str, channel: str) -> dict[str, Any]:
|
||||
from app.services import llm_router
|
||||
|
||||
system = (
|
||||
"Je bent Herman, de AI-assistent van Foodlinkk B2B (halal vlees, export, CRM). "
|
||||
"Antwoord in het Nederlands, bondig, actiegericht. Geef concrete vervolgstappen waar nuttig."
|
||||
)
|
||||
messages = [
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": message},
|
||||
]
|
||||
reply, meta = await llm_router.chat_messages(messages, timeout=120.0)
|
||||
provider = meta.get("provider_label") or "LLM"
|
||||
model = meta.get("model") or ""
|
||||
await _log_event(
|
||||
"herman",
|
||||
"llm_chat",
|
||||
f"Herman via {provider}",
|
||||
message[:2000],
|
||||
{"provider": meta, "channel": channel},
|
||||
channel=channel,
|
||||
)
|
||||
return {
|
||||
"agent": "herman",
|
||||
"agent_label": f"Herman · {provider}",
|
||||
"reply": reply,
|
||||
"delegated_agents": ["herman"],
|
||||
"routing_reason": f"Cloud LLM · {provider}" + (f" · {model}" if model else ""),
|
||||
"agent_steps": _agent_steps([], f"Via {provider}"),
|
||||
"llm_provider": meta,
|
||||
}
|
||||
|
||||
|
||||
async def chat(
|
||||
message: str,
|
||||
channel: str = "cockpit",
|
||||
@@ -344,6 +378,17 @@ async def chat(
|
||||
"reply": f"Kon geen afbeelding genereren: {exc}",
|
||||
}
|
||||
|
||||
backend = (settings.HERMAN_LLM_BACKEND or "auto").strip().lower()
|
||||
if backend in ("router", "openrouter", "llm"):
|
||||
try:
|
||||
return await _chat_via_llm_router(message, channel)
|
||||
except Exception as exc:
|
||||
return {
|
||||
"agent": "herman",
|
||||
"agent_label": "Herman",
|
||||
"reply": f"LLM provider niet beschikbaar: {exc}. Vul OpenRouter key in via Instellingen → AI / LLM.",
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=620.0) as client:
|
||||
r = await client.post(
|
||||
@@ -373,6 +418,19 @@ async def chat(
|
||||
"crm_loaded": data.get("crm_loaded", False),
|
||||
}
|
||||
except Exception as exc:
|
||||
if backend == "auto":
|
||||
try:
|
||||
return await _chat_via_llm_router(message, channel)
|
||||
except Exception as llm_exc:
|
||||
return {
|
||||
"agent": "herman",
|
||||
"agent_label": "Herman",
|
||||
"reply": (
|
||||
f"Orchestrator offline ({exc}). "
|
||||
f"Cloud LLM ook niet beschikbaar ({llm_exc}). "
|
||||
"Voeg OpenRouter API key toe: Instellingen → AI / LLM."
|
||||
),
|
||||
}
|
||||
return {
|
||||
"agent": "herman",
|
||||
"agent_label": "Herman",
|
||||
|
||||
@@ -48,13 +48,17 @@ LLM_PRESETS: dict[str, dict[str, Any]] = {
|
||||
"label": "OpenRouter",
|
||||
"api_base_url": "https://openrouter.ai/api/v1",
|
||||
"models": [
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"deepseek/deepseek-v4-flash:free",
|
||||
"deepseek/deepseek-chat",
|
||||
"deepseek/deepseek-r1",
|
||||
"google/gemini-2.0-flash-exp:free",
|
||||
"deepseek/deepseek-r1:free",
|
||||
"meta-llama/llama-3.3-70b-instruct:free",
|
||||
],
|
||||
"default_model": "deepseek/deepseek-v4-flash",
|
||||
"needs_key": True,
|
||||
"signup_url": "https://openrouter.ai/",
|
||||
"hint": "Veel gratis modellen via één API.",
|
||||
"signup_url": "https://openrouter.ai/keys",
|
||||
"hint": "Eén API key · DeepSeek V4 Flash via openrouter.ai. Vul key in Instellingen → AI.",
|
||||
},
|
||||
"mistral": {
|
||||
"label": "Mistral AI",
|
||||
@@ -74,6 +78,52 @@ LLM_PRESETS: dict[str, dict[str, Any]] = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
OPENROUTER_PLACEHOLDER_LABEL = "OpenRouter · DeepSeek V4 Flash"
|
||||
|
||||
|
||||
def ensure_openrouter_placeholder() -> None:
|
||||
"""Seed inactive OpenRouter row so user can fill API key later in Settings."""
|
||||
row = fetch_one(
|
||||
"SELECT id FROM llm_providers WHERE provider_type = 'openrouter' LIMIT 1"
|
||||
)
|
||||
if row:
|
||||
return
|
||||
execute(
|
||||
"""INSERT INTO llm_providers (
|
||||
label, provider_type, api_base_url, api_key, model,
|
||||
is_active, is_default, extra_config, updated_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb, NOW())""",
|
||||
(
|
||||
OPENROUTER_PLACEHOLDER_LABEL,
|
||||
"openrouter",
|
||||
"https://openrouter.ai/api/v1",
|
||||
"",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
False,
|
||||
False,
|
||||
json.dumps({"referer": "https://foodlinkk.local", "title": "Foodlinkk Command Center"}),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _env_openrouter_provider() -> dict[str, Any] | None:
|
||||
from app.config import settings as app_settings
|
||||
|
||||
key = (app_settings.OPENROUTER_API_KEY or "").strip()
|
||||
if not key:
|
||||
return None
|
||||
return {
|
||||
"id": -1,
|
||||
"label": "OpenRouter (environment)",
|
||||
"provider_type": "openrouter",
|
||||
"api_base_url": (app_settings.OPENROUTER_BASE_URL or "https://openrouter.ai/api/v1").rstrip("/"),
|
||||
"api_key": key,
|
||||
"model": app_settings.OPENROUTER_MODEL or "deepseek/deepseek-v4-flash",
|
||||
"extra_config": {"referer": "https://foodlinkk.local", "title": "Foodlinkk Command Center"},
|
||||
}
|
||||
|
||||
|
||||
def list_presets() -> list[dict[str, Any]]:
|
||||
out = []
|
||||
for key, meta in LLM_PRESETS.items():
|
||||
@@ -127,6 +177,9 @@ def get_provider(provider_id: int | None = None) -> dict[str, Any] | None:
|
||||
def resolve_provider(provider_id: int | None = None) -> dict[str, Any]:
|
||||
row = get_provider(provider_id)
|
||||
if not row:
|
||||
env_or = _env_openrouter_provider()
|
||||
if env_or:
|
||||
return env_or
|
||||
return {
|
||||
"id": 0,
|
||||
"label": "Ollama lokaal",
|
||||
@@ -136,6 +189,19 @@ def resolve_provider(provider_id: int | None = None) -> dict[str, Any]:
|
||||
"model": settings.OLLAMA_MODEL,
|
||||
"extra_config": {},
|
||||
}
|
||||
env_or = _env_openrouter_provider()
|
||||
ptype = (row.get("provider_type") or "").lower()
|
||||
if env_or and ptype == "openrouter" and not (row.get("api_key") or "").strip():
|
||||
merged = dict(row)
|
||||
merged["api_key"] = env_or["api_key"]
|
||||
if not (merged.get("model") or "").strip():
|
||||
merged["model"] = env_or["model"]
|
||||
return merged
|
||||
if env_or and ptype == "ollama" and row.get("is_default") and not (row.get("api_key") or "").strip():
|
||||
# Optional: prefer env OpenRouter over default Ollama when key is set
|
||||
from app.config import settings as app_settings
|
||||
if (app_settings.HERMAN_LLM_BACKEND or "").lower() in ("router", "openrouter"):
|
||||
return env_or
|
||||
return row
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user