feat(server): add workbench API, DSH planner worker and RAG pipeline
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Cross-process progress event bus.
|
||||
|
||||
The legacy bus was process-local, which worked when FastAPI spawned content
|
||||
generation threads. With an external DSH worker, events are persisted in
|
||||
SQLite and replayed by SSE/polling; the in-memory deque remains a fast path
|
||||
for events emitted inside the API process.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger("yawen.progress")
|
||||
_LOCK = threading.Lock()
|
||||
_QUEUES: dict[int, deque] = {}
|
||||
_MAX_PER_USER = 200
|
||||
|
||||
|
||||
def publish(user_id: int, event: str, **fields: Any) -> None:
|
||||
item = {"event": event, "ts": time.time(), **fields}
|
||||
uid = int(user_id or 0)
|
||||
with _LOCK:
|
||||
targets = list(_QUEUES.keys()) if uid == 0 else [uid]
|
||||
for target in targets:
|
||||
q = _QUEUES.get(target)
|
||||
if q is not None:
|
||||
q.append(item)
|
||||
while len(q) > _MAX_PER_USER:
|
||||
q.popleft()
|
||||
try:
|
||||
from sqlmodel import Session
|
||||
from .db import engine
|
||||
from .models import ProgressEvent
|
||||
with Session(engine) as session:
|
||||
session.add(ProgressEvent(user_id=uid, event=str(event)[:80], payload=item))
|
||||
session.commit()
|
||||
except Exception as exc: # progress must never block production
|
||||
logger.debug("persist progress event failed: %s", exc)
|
||||
|
||||
|
||||
def register(user_id: int) -> deque:
|
||||
with _LOCK:
|
||||
q = _QUEUES.get(int(user_id))
|
||||
if q is None:
|
||||
q = deque(maxlen=_MAX_PER_USER)
|
||||
_QUEUES[int(user_id)] = q
|
||||
return q
|
||||
|
||||
|
||||
def unregister(user_id: int) -> None:
|
||||
with _LOCK:
|
||||
_QUEUES.pop(int(user_id), None)
|
||||
|
||||
|
||||
def list_events(user_id: int, after_ts: float = 0.0, limit: int = 200) -> list[dict]:
|
||||
uid = int(user_id or 0)
|
||||
events: list[dict] = []
|
||||
try:
|
||||
from sqlmodel import Session, select
|
||||
from .db import engine
|
||||
from .models import ProgressEvent
|
||||
with Session(engine) as session:
|
||||
rows = session.exec(
|
||||
select(ProgressEvent)
|
||||
.where(ProgressEvent.user_id.in_([0, uid]))
|
||||
.order_by(ProgressEvent.id.desc())
|
||||
.limit(500)
|
||||
).all()
|
||||
events = [dict(row.payload or {}) for row in reversed(rows)]
|
||||
except Exception:
|
||||
events = []
|
||||
with _LOCK:
|
||||
q = _QUEUES.get(uid)
|
||||
if q is not None:
|
||||
events.extend(list(q))
|
||||
result: list[dict] = []
|
||||
seen: set[tuple] = set()
|
||||
for event in events:
|
||||
if float(event.get("ts") or 0.0) <= float(after_ts or 0.0):
|
||||
continue
|
||||
key = (event.get("event"), event.get("ts"), event.get("campaign_id"),
|
||||
event.get("segment_id"), event.get("title"))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
result.append(event)
|
||||
return result[-limit:]
|
||||
|
||||
|
||||
def snapshot(user_id: int, after_ts: float = 0.0, limit: int = 200) -> list[dict]:
|
||||
return list_events(user_id, after_ts, limit=limit)
|
||||
Reference in New Issue
Block a user