feat(monitoring): add shared SSE in-flight monitor with draggable dock

This commit is contained in:
Hyu
2026-10-10 21:30:03 +08:00
parent d2ec53ec91
commit 38aa06e149
24 changed files with 1289 additions and 14 deletions
@@ -59,6 +59,31 @@ class MonitoringRouterGroup(group.RouterGroup):
return self.success(data=metrics)
@self.route('/in-flight/stream', methods=['GET'], permission=Permission.RESOURCE_VIEW)
async def stream_inflight(request_context: RequestContext):
from .....utils.inflight import inflight_hub
from ...service.tenant import require_workspace_uuid
import json
workspace = require_workspace_uuid(request_context)
previous = []
async def snapshot():
nonlocal previous
result = await self.ap.monitoring_service.get_inflight_snapshot(request_context, previous)
previous = [row for row in result['items'] if row['status_group'] in {'running', 'queued'}]
return result
async def frames():
async for frame in inflight_hub.watch(workspace, snapshot):
yield 'data: ' + json.dumps(frame, ensure_ascii=False) + '\n\n'
response = quart.Response(frames(), content_type='text/event-stream')
response.headers['Cache-Control'] = 'no-cache, no-store'
response.headers['X-Accel-Buffering'] = 'no'
response.timeout = None
return response
@self.route('/executions', methods=['GET'], permission=Permission.RESOURCE_VIEW)
async def get_executions(request_context: RequestContext) -> str:
"""Unified execution list across agent runs and pipeline queries."""
+58 -2
View File
@@ -127,8 +127,13 @@ def _workspace_transaction(method):
tenant_uow = getattr(self.ap.persistence_mgr, 'tenant_uow', None)
if callable(tenant_uow):
async with tenant_uow(workspace_uuid):
return await method(self, context, *args, **kwargs)
return await method(self, context, *args, **kwargs)
result = await method(self, context, *args, **kwargs)
else:
result = await method(self, context, *args, **kwargs)
if method.__name__ in {'record_message', 'update_message_status'}:
from ....utils.inflight import inflight_hub
inflight_hub.notify(workspace_uuid)
return result
return wrapped
@@ -1533,6 +1538,7 @@ class MonitoringService(ExecutionDetailsMixin):
end_time: datetime.datetime | None = None,
limit: int = 50,
offset: int = 0,
include_summary: bool = True,
) -> dict:
"""List executions from both object types (agent runs and pipeline queries)."""
workspace_uuid = require_workspace_uuid(context)
@@ -1633,6 +1639,8 @@ class MonitoringService(ExecutionDetailsMixin):
by_key[(kind, item['id'])] = item
page = [by_key[(ref.source, ref.id)] for ref in refs if (ref.source, ref.id) in by_key]
await self.enrich_execution_rows(workspace_uuid, page)
if not include_summary:
return {'items': page, 'total': total, 'has_more': total > offset + len(page)}
return {
'items': page,
'total': total,
@@ -1661,6 +1669,54 @@ class MonitoringService(ExecutionDetailsMixin):
},
}
async def get_inflight_snapshot(self, context: TenantContext, previous: list[dict] | None = None) -> dict:
"""Bounded Workspace snapshot without dashboard aggregates or token payloads."""
from ....utils.inflight import inflight_hub
workspace = require_workspace_uuid(context)
active = await self.get_executions(context, statuses=['running', 'queued'], limit=100, include_summary=False)
recent = await self.get_executions(
context, start_time=datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=30),
limit=100, include_summary=False,
)
items = {(r['source'], r['id']): r for r in recent['items'] + active['items'] if r['source'] != 'event'}
# Keep the actual terminal state of older runs that just left the active set.
for kind, model, column, scope in [
('agent', persistence_agent_run.AgentRun, persistence_agent_run.AgentRun.run_id, persistence_agent_run.AgentRun.workspace_id),
('pipeline', persistence_monitoring.MonitoringMessage, persistence_monitoring.MonitoringMessage.id, persistence_monitoring.MonitoringMessage.workspace_uuid),
]:
missing = [r['id'] for r in (previous or [])[:100] if r['source'] == kind and (kind, r['id']) not in items]
if missing:
records = await self._execution_rows(workspace, sqlalchemy.select(model).where(scope == workspace, column.in_(missing)))
names = await self._agent_names(workspace, records) if kind == 'agent' else {}
for record in records:
row = self._serialize_agent_execution(record, names) if kind == 'agent' else self._serialize_pipeline_execution(record)
items[(kind, row['id'])] = row
rows = list(items.values())
Run = persistence_agent_run.AgentRun
Message = persistence_monitoring.MonitoringMessage
agent_ids = [r['id'] for r in rows if r['source'] == 'agent']
pipeline_ids = [r['id'] for r in rows if r['source'] == 'pipeline']
linked = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(Run.run_id, Message.id.label('message_id')).outerjoin(
Message, sqlalchemy.and_(Message.workspace_uuid == workspace, Message.run_id == Run.run_id)
).where(Run.workspace_id == workspace, sqlalchemy.or_(Run.run_id.in_(agent_ids), Message.id.in_(pipeline_ids)))
)
identities = {r.run_id: ('pipeline', r.message_id) if r.message_id in pipeline_ids else ('agent', r.run_id) for r in linked.all()}
inflight_hub.track(workspace, identities)
Event = persistence_agent_run.AgentRunEvent
latest = sqlalchemy.select(Event.run_id, sqlalchemy.func.max(Event.sequence).label('seq')).where(
Event.run_id.in_(identities)
).group_by(Event.run_id).subquery()
phases = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(Event.run_id, Event.type).join(latest, sqlalchemy.and_(Event.run_id == latest.c.run_id, Event.sequence == latest.c.seq))
)
for run_id, event_type in phases.all():
row = items.get(identities[run_id])
if row is not None:
row['progress_event'] = event_type
return {'items': rows, 'active_total': active['total'], 'truncated': active['has_more'] or recent['has_more']}
async def get_token_statistics(
self,
context: TenantContext,