Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
This commit is contained in:
@@ -0,0 +1,750 @@
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.db import close_pool, execute_returning, fetch_all, fetch_one, init_pool
|
||||
from app.middleware import log_agent_event
|
||||
from app.packaging_routes import router as packaging_router
|
||||
from app.retail import router as retail_router
|
||||
from app.retail_360_routes import router as retail_360_router
|
||||
from app.research import router as research_router
|
||||
from app.recommendations import router as recommendations_router
|
||||
from app.ops_routes import router as ops_router
|
||||
from app.logging_middleware import AgentLoggingMiddleware
|
||||
from app import brain as brain_svc
|
||||
|
||||
|
||||
class BrainMessageIn(BaseModel):
|
||||
chat_id: int
|
||||
direction: str = Field(..., pattern="^(in|out)$")
|
||||
content_text: Optional[str] = None
|
||||
content_type: str = "text"
|
||||
role: str = "user"
|
||||
telegram_message_id: Optional[int] = None
|
||||
reply_to_db_id: Optional[int] = None
|
||||
agent_name: Optional[str] = None
|
||||
content_json: dict[str, Any] = Field(default_factory=dict)
|
||||
user_name: Optional[str] = None
|
||||
user_role: Optional[str] = None
|
||||
chat_type: str = "private"
|
||||
embed: bool = True
|
||||
|
||||
|
||||
class BrainEdgeIn(BaseModel):
|
||||
source_message_id: int
|
||||
edge_type: str
|
||||
target_message_id: Optional[int] = None
|
||||
target_entity_type: Optional[str] = None
|
||||
target_entity_id: Optional[int] = None
|
||||
weight: float = 1.0
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class BrainSearchIn(BaseModel):
|
||||
query: str = Field(..., min_length=2)
|
||||
chat_id: Optional[int] = None
|
||||
limit: int = Field(default=8, ge=1, le=30)
|
||||
|
||||
|
||||
from app.email_config import get_active_email_config
|
||||
from app.comfyui import fetch_image_bytes, generate_image, get_job, start_generation, QUALITY_PRESETS
|
||||
import json
|
||||
import os
|
||||
|
||||
DOC_INGEST_URL = os.getenv("DOC_INGEST_URL", "http://10.4.7.19:8750")
|
||||
|
||||
app = FastAPI(title="Foodlinkk Tools API", version="1.1.0")
|
||||
app.add_middleware(AgentLoggingMiddleware)
|
||||
app.include_router(retail_router)
|
||||
app.include_router(retail_360_router)
|
||||
app.include_router(research_router)
|
||||
app.include_router(recommendations_router)
|
||||
app.include_router(packaging_router)
|
||||
app.include_router(ops_router)
|
||||
|
||||
|
||||
class AgentEventCreate(BaseModel):
|
||||
agent_name: str = Field(..., max_length=64)
|
||||
event_type: str = Field(..., max_length=64)
|
||||
title: str = Field(..., max_length=255)
|
||||
body: Optional[str] = None
|
||||
agent_type: str = Field(default="openswarm", max_length=32)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
status: str = Field(default="completed", max_length=32)
|
||||
related_table: Optional[str] = Field(default=None, max_length=64)
|
||||
related_id: Optional[int] = None
|
||||
channel: str = Field(default="dashboard", max_length=32)
|
||||
|
||||
|
||||
def _serialize_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {}
|
||||
for key, value in row.items():
|
||||
if isinstance(value, (datetime, date)):
|
||||
out[key] = value.isoformat()
|
||||
elif isinstance(value, Decimal):
|
||||
out[key] = float(value)
|
||||
else:
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup() -> None:
|
||||
init_pool()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
def on_shutdown() -> None:
|
||||
close_pool()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
try:
|
||||
fetch_one("SELECT 1 AS ok")
|
||||
return {"status": "ok", "database": "connected"}
|
||||
except Exception as exc:
|
||||
return {"status": "degraded", "database": str(exc)}
|
||||
|
||||
|
||||
@app.post("/events")
|
||||
def create_event(payload: AgentEventCreate) -> dict[str, Any]:
|
||||
try:
|
||||
row = log_agent_event(**payload.model_dump())
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
return _serialize_row(row)
|
||||
|
||||
|
||||
@app.get("/events")
|
||||
def list_events(
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
status: Optional[str] = Query(default=None),
|
||||
agent_name: Optional[str] = Query(default=None),
|
||||
) -> dict[str, Any]:
|
||||
clauses: list[str] = []
|
||||
params: list[Any] = []
|
||||
if status:
|
||||
clauses.append("status = %s")
|
||||
params.append(status)
|
||||
if agent_name:
|
||||
clauses.append("agent_name = %s")
|
||||
params.append(agent_name)
|
||||
where = f"WHERE { AND .join(clauses)}" if clauses else ""
|
||||
params.append(limit)
|
||||
try:
|
||||
rows = fetch_all(
|
||||
f"""
|
||||
SELECT *
|
||||
FROM agent_events
|
||||
{where}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s
|
||||
""",
|
||||
tuple(params),
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
return {"events": [_serialize_row(r) for r in rows]}
|
||||
|
||||
|
||||
@app.post("/events/{event_id}/approve")
|
||||
def approve_event(event_id: int) -> dict[str, Any]:
|
||||
row = execute_returning(
|
||||
"""
|
||||
UPDATE agent_events
|
||||
SET status = %s, completed_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING *
|
||||
""",
|
||||
("approved", event_id),
|
||||
)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
return _serialize_row(row)
|
||||
|
||||
|
||||
@app.post("/events/{event_id}/reject")
|
||||
def reject_event(event_id: int) -> dict[str, Any]:
|
||||
row = execute_returning(
|
||||
"""
|
||||
UPDATE agent_events
|
||||
SET status = %s, completed_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING *
|
||||
""",
|
||||
("rejected", event_id),
|
||||
)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
return _serialize_row(row)
|
||||
|
||||
class BrowserTask(BaseModel):
|
||||
url: str
|
||||
task: str = Field(default="open")
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class HermanDelegate(BaseModel):
|
||||
message: str
|
||||
target_agent: Optional[str] = Field(default=None, max_length=64)
|
||||
|
||||
|
||||
@app.post("/browser/task")
|
||||
async def browser_task(payload: BrowserTask) -> dict[str, Any]:
|
||||
import httpx
|
||||
import os
|
||||
browser_url = os.getenv("BROWSER_USE_URL", "http://browser-agent:7790")
|
||||
result = {"ok": True, "browser_url": browser_url, "url": payload.url}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
resp = await client.post(f"{browser_url.rstrip('/')}/task", json=payload.model_dump())
|
||||
result["upstream_status"] = resp.status_code
|
||||
if resp.status_code < 500:
|
||||
try:
|
||||
result["upstream"] = resp.json()
|
||||
except Exception:
|
||||
result["upstream"] = resp.text[:500]
|
||||
except Exception as exc:
|
||||
result["upstream_error"] = str(exc)
|
||||
try:
|
||||
log_agent_event(
|
||||
agent_name="browser",
|
||||
event_type="browser_task",
|
||||
title=f"Browser task: {payload.url[:120]}",
|
||||
body=payload.task,
|
||||
metadata={"url": payload.url, **payload.metadata, **result},
|
||||
channel="tools-api",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/herman/delegate")
|
||||
def herman_delegate(payload: HermanDelegate) -> dict[str, Any]:
|
||||
agent = payload.target_agent or "herman"
|
||||
try:
|
||||
row = log_agent_event(
|
||||
agent_name=agent,
|
||||
agent_type="herman_delegate",
|
||||
event_type="delegate",
|
||||
title="Herman delegation",
|
||||
body=payload.message,
|
||||
metadata={"target_agent": agent},
|
||||
channel="herman",
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
return _serialize_row(row)
|
||||
|
||||
|
||||
@app.get("/knowledge/search")
|
||||
async def knowledge_search(q: str = Query(..., min_length=1), limit: int = Query(default=10, ge=1, le=50)) -> dict[str, Any]:
|
||||
import httpx
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
r = await client.get(f"{DOC_INGEST_URL.rstrip('/')}/search", params={"q": q, "limit": limit})
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
return {"query": q, "results": data.get("results", []), "source": "chroma"}
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
rows = fetch_all(
|
||||
"""
|
||||
SELECT id, title, source, doc_type, metadata, created_at
|
||||
FROM knowledge_documents
|
||||
WHERE title ILIKE %s OR source ILIKE %s
|
||||
ORDER BY updated_at DESC NULLS LAST
|
||||
LIMIT %s
|
||||
""",
|
||||
(f"%{q}%", f"%{q}%", limit),
|
||||
)
|
||||
except Exception:
|
||||
rows = []
|
||||
return {"query": q, "results": [_serialize_row(r) for r in rows], "source": "postgres"}
|
||||
|
||||
|
||||
@app.post("/knowledge/ingest")
|
||||
async def knowledge_ingest(force: bool = Query(default=False)) -> dict[str, Any]:
|
||||
import httpx
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=600.0) as client:
|
||||
r = await client.post(f"{DOC_INGEST_URL.rstrip('/')}/ingest/scan", params={"force": force})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@app.get("/knowledge/nas")
|
||||
async def knowledge_nas(limit: int = Query(default=50, ge=1, le=500)) -> dict[str, Any]:
|
||||
import httpx
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
r = await client.get(f"{DOC_INGEST_URL.rstrip('/')}/nas/list", params={"limit": limit})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
|
||||
# --- CRM / Email / LLM memory (2nd brain) ---
|
||||
|
||||
class EmailSend(BaseModel):
|
||||
to: list[str] = Field(..., min_length=1)
|
||||
subject: str = Field(..., max_length=500)
|
||||
body: str
|
||||
client_id: Optional[int] = None
|
||||
deal_id: Optional[int] = None
|
||||
cc: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class LlmMemoryCreate(BaseModel):
|
||||
category: str = Field(default="fact", max_length=64)
|
||||
subject: Optional[str] = Field(default=None, max_length=255)
|
||||
content: str
|
||||
client_id: Optional[int] = None
|
||||
deal_id: Optional[int] = None
|
||||
source: str = Field(default="herman", max_length=64)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
def _crm_safe_all(sql: str, params: tuple = ()) -> list[dict[str, Any]]:
|
||||
try:
|
||||
return fetch_all(sql, params or None)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
@app.get("/crm/context")
|
||||
def crm_context(days: int = Query(default=7, ge=1, le=30)) -> dict[str, Any]:
|
||||
"""Live PostgreSQL bundle for Herman / morning briefing."""
|
||||
pipeline = fetch_one(
|
||||
"SELECT COALESCE(SUM(value), 0) AS total, COUNT(*) AS cnt FROM deals WHERE stage NOT IN ('won', 'lost')"
|
||||
)
|
||||
clients = _crm_safe_all(
|
||||
"""SELECT id, name, contact, email, stage, sector, notes, mrr_estimate
|
||||
FROM clients ORDER BY updated_at DESC NULLS LAST, created_at DESC LIMIT 25"""
|
||||
)
|
||||
deals = _crm_safe_all(
|
||||
"""
|
||||
SELECT d.id, d.title, d.value, d.stage, d.next_action, d.deadline, d.agent_owner,
|
||||
c.name AS client_name, c.email AS client_email
|
||||
FROM deals d
|
||||
LEFT JOIN clients c ON c.id = d.client_id
|
||||
WHERE d.stage NOT IN ('won', 'lost')
|
||||
ORDER BY d.deadline ASC NULLS LAST, d.updated_at DESC NULLS LAST
|
||||
LIMIT 20
|
||||
"""
|
||||
)
|
||||
upcoming = _crm_safe_all(
|
||||
"""
|
||||
SELECT d.id, d.title, d.value, d.stage, d.deadline, d.next_action, c.name AS client_name
|
||||
FROM deals d
|
||||
LEFT JOIN clients c ON c.id = d.client_id
|
||||
WHERE d.deadline IS NOT NULL
|
||||
AND d.deadline <= CURRENT_DATE + make_interval(days => %s)
|
||||
AND d.stage NOT IN ('won', 'lost')
|
||||
ORDER BY d.deadline ASC
|
||||
LIMIT 15
|
||||
""",
|
||||
(days,),
|
||||
)
|
||||
calendar = _crm_safe_all(
|
||||
"""
|
||||
SELECT ce.id, ce.title, ce.starts_at, ce.ends_at, ce.location, ce.source,
|
||||
c.name AS client_name, d.title AS deal_title
|
||||
FROM calendar_events ce
|
||||
LEFT JOIN clients c ON c.id = ce.client_id
|
||||
LEFT JOIN deals d ON d.id = ce.deal_id
|
||||
WHERE ce.starts_at >= NOW() - INTERVAL '1 day'
|
||||
AND ce.starts_at <= NOW() + make_interval(days => %s)
|
||||
ORDER BY ce.starts_at ASC
|
||||
LIMIT 25
|
||||
""",
|
||||
(days,),
|
||||
)
|
||||
pending = _crm_safe_all(
|
||||
"""
|
||||
SELECT id, agent_name, title, event_type, created_at
|
||||
FROM agent_events WHERE status = 'needs_approval'
|
||||
ORDER BY created_at ASC LIMIT 10
|
||||
"""
|
||||
)
|
||||
emails_recent = _crm_safe_all(
|
||||
"""
|
||||
SELECT id, from_addr, subject, received_at, sent_at, direction, is_read, client_id
|
||||
FROM emails ORDER BY COALESCE(received_at, sent_at) DESC NULLS LAST LIMIT 15
|
||||
"""
|
||||
)
|
||||
memories = _crm_safe_all(
|
||||
"""
|
||||
SELECT id, category, subject, content, client_id, deal_id, source, updated_at
|
||||
FROM llm_memory ORDER BY updated_at DESC LIMIT 20
|
||||
"""
|
||||
)
|
||||
return {
|
||||
"generated_at": datetime.utcnow().isoformat() + "Z",
|
||||
"pipeline_eur": float(pipeline["total"]) if pipeline else 0.0,
|
||||
"active_deals": int(pipeline["cnt"]) if pipeline else 0,
|
||||
"clients_count": len(clients),
|
||||
"clients": [_serialize_row(c) for c in clients],
|
||||
"deals": [_serialize_row(d) for d in deals],
|
||||
"upcoming_deadlines": [_serialize_row(u) for u in upcoming],
|
||||
"calendar": [_serialize_row(c) for c in calendar],
|
||||
"pending_approvals": [_serialize_row(p) for p in pending],
|
||||
"recent_emails": [_serialize_row(e) for e in emails_recent],
|
||||
"llm_memory": [_serialize_row(m) for m in memories],
|
||||
"links": {
|
||||
"dashboard": "http://10.4.7.18:8600",
|
||||
"clients": "http://10.4.7.18:8600/clients",
|
||||
"deals": "http://10.4.7.18:8600/deals",
|
||||
"reports": "http://10.4.7.18:8600/reports",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.get("/crm/clients")
|
||||
def crm_clients(limit: int = Query(default=25, ge=1, le=100)) -> dict[str, Any]:
|
||||
rows = _crm_safe_all(
|
||||
"SELECT id, name, contact, email, stage, sector, notes FROM clients ORDER BY name LIMIT %s",
|
||||
(limit,),
|
||||
)
|
||||
return {"clients": [_serialize_row(r) for r in rows]}
|
||||
|
||||
|
||||
@app.get("/crm/deals/upcoming")
|
||||
def crm_deals_upcoming(days: int = Query(default=7, ge=1, le=60)) -> dict[str, Any]:
|
||||
rows = _crm_safe_all(
|
||||
"""
|
||||
SELECT d.id, d.title, d.value, d.stage, d.deadline, d.next_action, c.name AS client_name
|
||||
FROM deals d LEFT JOIN clients c ON c.id = d.client_id
|
||||
WHERE d.deadline IS NOT NULL AND d.deadline <= CURRENT_DATE + make_interval(days => %s)
|
||||
AND d.stage NOT IN ('won', 'lost')
|
||||
ORDER BY d.deadline ASC LIMIT 30
|
||||
""",
|
||||
(days,),
|
||||
)
|
||||
return {"days": days, "deals": [_serialize_row(r) for r in rows]}
|
||||
|
||||
|
||||
@app.get("/emails/recent")
|
||||
def emails_recent(limit: int = Query(default=20, ge=1, le=100)) -> dict[str, Any]:
|
||||
rows = _crm_safe_all(
|
||||
"""
|
||||
SELECT id, message_id, from_addr, to_addrs, subject, direction,
|
||||
received_at, sent_at, is_read, client_id
|
||||
FROM emails ORDER BY COALESCE(received_at, sent_at) DESC NULLS LAST LIMIT %s
|
||||
""",
|
||||
(limit,),
|
||||
)
|
||||
return {"emails": [_serialize_row(r) for r in rows]}
|
||||
|
||||
|
||||
@app.post("/emails/send")
|
||||
def emails_send(payload: EmailSend) -> dict[str, Any]:
|
||||
"""Verstuur e-mail via SMTP (configureer SMTP_* env vars). Log in PostgreSQL."""
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate, make_msgid
|
||||
import uuid
|
||||
|
||||
cfg = get_active_email_config()
|
||||
smtp_host = cfg["smtp_host"]
|
||||
smtp_port = cfg["smtp_port"]
|
||||
smtp_user = cfg["smtp_user"]
|
||||
smtp_pass = cfg["smtp_pass"]
|
||||
smtp_from = cfg["smtp_from"]
|
||||
|
||||
if not smtp_host or not smtp_from:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Geen email account geconfigureerd. Ga naar Settings → Email in het dashboard.",
|
||||
)
|
||||
|
||||
msg = MIMEText(payload.body, "plain", "utf-8")
|
||||
msg["Subject"] = payload.subject
|
||||
msg["From"] = smtp_from
|
||||
msg["To"] = ", ".join(payload.to)
|
||||
if payload.cc:
|
||||
msg["Cc"] = ", ".join(payload.cc)
|
||||
msg["Date"] = formatdate(localtime=True)
|
||||
message_id = make_msgid()
|
||||
msg["Message-ID"] = message_id
|
||||
|
||||
recipients = list(payload.to) + list(payload.cc)
|
||||
try:
|
||||
with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
|
||||
server.ehlo()
|
||||
if smtp_port == 587:
|
||||
server.starttls()
|
||||
if smtp_user and smtp_pass:
|
||||
server.login(smtp_user, smtp_pass)
|
||||
server.sendmail(smtp_from, recipients, msg.as_string())
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=f"SMTP send failed: {exc}") from exc
|
||||
|
||||
mid = message_id.strip("<>")
|
||||
try:
|
||||
row = execute_returning(
|
||||
"""
|
||||
INSERT INTO emails (
|
||||
message_id, client_id, deal_id, direction, from_addr, to_addrs,
|
||||
subject, body_text, sent_at, is_read
|
||||
) VALUES (%s, %s, %s, 'out', %s, %s, %s, %s, NOW(), TRUE)
|
||||
RETURNING id, message_id, subject, sent_at
|
||||
""",
|
||||
(mid, payload.client_id, payload.deal_id, smtp_from, payload.to, payload.subject, payload.body),
|
||||
)
|
||||
except Exception:
|
||||
row = {"message_id": mid, "subject": payload.subject}
|
||||
|
||||
try:
|
||||
log_agent_event(
|
||||
agent_name="herman",
|
||||
event_type="email_sent",
|
||||
title=f"Email: {payload.subject[:120]}",
|
||||
body=payload.body[:2000],
|
||||
metadata={"to": payload.to, "client_id": payload.client_id},
|
||||
channel="email",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"ok": True, "message_id": mid, "email": _serialize_row(row) if isinstance(row, dict) else row}
|
||||
|
||||
|
||||
@app.post("/llm/memory")
|
||||
def llm_memory_create(payload: LlmMemoryCreate) -> dict[str, Any]:
|
||||
row = execute_returning(
|
||||
"""
|
||||
INSERT INTO llm_memory (category, subject, content, client_id, deal_id, source, metadata)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb)
|
||||
RETURNING id, category, subject, content, created_at
|
||||
""",
|
||||
(
|
||||
payload.category,
|
||||
payload.subject,
|
||||
payload.content,
|
||||
payload.client_id,
|
||||
payload.deal_id,
|
||||
payload.source,
|
||||
json.dumps(payload.metadata),
|
||||
),
|
||||
)
|
||||
return {"ok": True, "memory": _serialize_row(row)}
|
||||
|
||||
|
||||
@app.get("/llm/memory/search")
|
||||
def llm_memory_search(q: str = Query(..., min_length=1), limit: int = Query(default=15, ge=1, le=50)) -> dict[str, Any]:
|
||||
rows = _crm_safe_all(
|
||||
"""
|
||||
SELECT id, category, subject, content, client_id, deal_id, source, updated_at
|
||||
FROM llm_memory
|
||||
WHERE content ILIKE %s OR subject ILIKE %s OR category ILIKE %s
|
||||
ORDER BY updated_at DESC LIMIT %s
|
||||
""",
|
||||
(f"%{q}%", f"%{q}%", f"%{q}%", limit),
|
||||
)
|
||||
return {"query": q, "results": [_serialize_row(r) for r in rows]}
|
||||
|
||||
@app.get("/settings/email/active")
|
||||
def settings_email_active() -> dict[str, Any]:
|
||||
cfg = get_active_email_config()
|
||||
return {
|
||||
"configured": bool(cfg.get("smtp_host") and cfg.get("smtp_from")),
|
||||
"source": cfg.get("source"),
|
||||
"label": cfg.get("label"),
|
||||
"from": cfg.get("smtp_from"),
|
||||
"account_id": cfg.get("account_id"),
|
||||
}
|
||||
|
||||
class ImageGenerateBody(BaseModel):
|
||||
prompt: str = Field(..., min_length=3, max_length=2000)
|
||||
width: int = Field(default=1024, ge=256, le=1024)
|
||||
height: int = Field(default=1024, ge=256, le=1024)
|
||||
steps: int = Field(default=28, ge=5, le=40)
|
||||
seed: Optional[int] = None
|
||||
quality: str = Field(default="hd", pattern="^(fast|hd|ultra|custom)$")
|
||||
|
||||
|
||||
class ImageStartBody(BaseModel):
|
||||
prompt: str = Field(..., min_length=3, max_length=2000)
|
||||
negative_prompt: str = Field(default="blurry, low quality, watermark, text, ugly, deformed", max_length=2000)
|
||||
quality: str = Field(default="hd", pattern="^(fast|hd|ultra)$")
|
||||
seed: Optional[int] = None
|
||||
|
||||
|
||||
@app.post("/images/generate/start")
|
||||
async def images_generate_start(body: ImageStartBody) -> dict[str, Any]:
|
||||
try:
|
||||
return await start_generation(
|
||||
body.prompt.strip(),
|
||||
negative=body.negative_prompt.strip(),
|
||||
quality=body.quality,
|
||||
seed=body.seed,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=f"ComfyUI: {exc}") from exc
|
||||
|
||||
|
||||
@app.get("/images/progress/{prompt_id}")
|
||||
async def images_progress(prompt_id: str) -> dict[str, Any]:
|
||||
job = get_job(prompt_id)
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Onbekende job")
|
||||
return job
|
||||
|
||||
|
||||
@app.get("/images/presets")
|
||||
def images_presets() -> dict[str, Any]:
|
||||
return {"presets": QUALITY_PRESETS}
|
||||
|
||||
|
||||
@app.post("/images/generate")
|
||||
async def images_generate(body: ImageGenerateBody) -> dict[str, Any]:
|
||||
try:
|
||||
result = await generate_image(
|
||||
body.prompt.strip(),
|
||||
width=body.width,
|
||||
height=body.height,
|
||||
steps=body.steps,
|
||||
seed=body.seed,
|
||||
quality=body.quality,
|
||||
)
|
||||
except TimeoutError as exc:
|
||||
raise HTTPException(status_code=504, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=f"ComfyUI: {exc}") from exc
|
||||
try:
|
||||
log_agent_event(
|
||||
agent_name="design",
|
||||
event_type="image_generated",
|
||||
title="ComfyUI foto gegenereerd",
|
||||
body=body.prompt[:500],
|
||||
metadata={"filename": result["filename"], "prompt_id": result["prompt_id"]},
|
||||
channel="cockpit",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True, **result}
|
||||
|
||||
|
||||
@app.get("/images/view")
|
||||
async def images_view(
|
||||
filename: str = Query(..., min_length=1),
|
||||
subfolder: str = Query(default=""),
|
||||
type: str = Query(default="output"),
|
||||
):
|
||||
from fastapi.responses import Response
|
||||
|
||||
try:
|
||||
data = await fetch_image_bytes(filename, subfolder, type)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
media = "image/png" if filename.lower().endswith(".png") else "image/jpeg"
|
||||
return Response(content=data, media_type=media)
|
||||
|
||||
@app.post("/brain/messages")
|
||||
async def brain_store_message(body: BrainMessageIn) -> dict[str, Any]:
|
||||
try:
|
||||
if body.embed and (body.content_text or "").strip():
|
||||
row = await brain_svc.store_message_with_embedding(
|
||||
body.chat_id,
|
||||
direction=body.direction,
|
||||
content_text=body.content_text,
|
||||
content_type=body.content_type,
|
||||
role=body.role,
|
||||
telegram_message_id=body.telegram_message_id,
|
||||
reply_to_db_id=body.reply_to_db_id,
|
||||
agent_name=body.agent_name,
|
||||
content_json=body.content_json,
|
||||
user_name=body.user_name,
|
||||
user_role=body.user_role,
|
||||
chat_type=body.chat_type,
|
||||
)
|
||||
else:
|
||||
row = brain_svc.store_message(
|
||||
body.chat_id,
|
||||
direction=body.direction,
|
||||
content_text=body.content_text,
|
||||
content_type=body.content_type,
|
||||
role=body.role,
|
||||
telegram_message_id=body.telegram_message_id,
|
||||
reply_to_db_id=body.reply_to_db_id,
|
||||
agent_name=body.agent_name,
|
||||
content_json=body.content_json,
|
||||
user_name=body.user_name,
|
||||
user_role=body.user_role,
|
||||
chat_type=body.chat_type,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
return _serialize_row(row)
|
||||
|
||||
|
||||
@app.post("/brain/edges")
|
||||
def brain_store_edge(body: BrainEdgeIn) -> dict[str, Any]:
|
||||
try:
|
||||
row = brain_svc.store_edge(
|
||||
body.source_message_id,
|
||||
body.edge_type,
|
||||
target_message_id=body.target_message_id,
|
||||
target_entity_type=body.target_entity_type,
|
||||
target_entity_id=body.target_entity_id,
|
||||
weight=body.weight,
|
||||
metadata=body.metadata,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
return _serialize_row(row)
|
||||
|
||||
|
||||
@app.get("/brain/graph/{chat_id}")
|
||||
def brain_graph(chat_id: int, limit: int = Query(default=50, ge=1, le=200)) -> dict[str, Any]:
|
||||
return brain_svc.get_graph(chat_id, limit=limit)
|
||||
|
||||
|
||||
@app.post("/brain/search")
|
||||
async def brain_search(body: BrainSearchIn) -> dict[str, Any]:
|
||||
try:
|
||||
results = await brain_svc.search_memory(body.query, chat_id=body.chat_id, limit=body.limit)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
return {"results": [_serialize_row(r) for r in results]}
|
||||
|
||||
# Append to tools-api main.py before end
|
||||
|
||||
@app.get("/brain/conversations")
|
||||
def brain_list_conversations(limit: int = Query(default=50, ge=1, le=200)) -> dict[str, Any]:
|
||||
rows = brain_svc.list_conversations(limit=limit)
|
||||
return {"items": [_serialize_row(r) for r in rows]}
|
||||
|
||||
|
||||
@app.get("/brain/feed")
|
||||
def brain_feed(
|
||||
chat_id: Optional[int] = Query(default=None),
|
||||
limit: int = Query(default=80, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> dict[str, Any]:
|
||||
rows = brain_svc.list_feed(chat_id=chat_id, limit=limit, offset=offset)
|
||||
return {"items": [_serialize_row(r) for r in rows]}
|
||||
|
||||
|
||||
@app.get("/brain/stats")
|
||||
def brain_stats() -> dict[str, Any]:
|
||||
data = brain_svc.get_dashboard_stats()
|
||||
return {
|
||||
"stats": _serialize_row(data.get("stats") or {}),
|
||||
"recent_messages": [_serialize_row(r) for r in data.get("recent_messages") or []],
|
||||
"agent_events": [_serialize_row(r) for r in data.get("agent_events") or []],
|
||||
}
|
||||
|
||||
|
||||
@app.get("/brain/graph")
|
||||
def brain_global_graph(limit: int = Query(default=100, ge=1, le=300)) -> dict[str, Any]:
|
||||
return brain_svc.get_global_graph(limit=limit)
|
||||
Reference in New Issue
Block a user