CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
134 lines
5.7 KiB
Python
134 lines
5.7 KiB
Python
"""Legacy dict -> typed :mod:`agent_event` translation (R04-T02).
|
|
|
|
Kept in its own module for two reasons. It is a **temporary compatibility
|
|
shim**: once R08-T01 turns ``ui/chat_panel.py::_on_event`` into an event
|
|
renderer that consumes typed events directly, nothing needs to parse dicts any
|
|
more and this whole file gets deleted — a deletion that stays trivial only while
|
|
it is isolated. And it keeps ``agent_event.py`` inside the 400-LOC limit the
|
|
architecture rules impose, without diluting either file's single job: one
|
|
declares the vocabulary, the other bridges it to the old wire format.
|
|
|
|
Serialisation the other way lives on the events themselves
|
|
(``AgentEvent.to_legacy_dict``), because an event has to be emittable without
|
|
anyone importing a codec.
|
|
|
|
SEAM · dựng 2026-08-23 · chưa nối dây (F-05)
|
|
------------------------------------------------------------
|
|
Được nối khi: ``presentation/chat`` dùng thẳng sự kiện có kiểu, không còn đọc dict cũ nữa.
|
|
Để dormant thì sao: Shim này để xoá, không để giữ. Còn nó thì khuôn dict cũ
|
|
vẫn là một hợp đồng phải duy trì.
|
|
|
|
Cổng ``scripts/check_orphan_modules.py`` đếm tuổi seam từ ngày trên
|
|
và nhắc khi quá ``SEAM_MAX_AGE_DAYS``. Đổi nội dung dòng đó thì cổng
|
|
đọc theo — đừng sửa ngày để làm im lời nhắc.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from .agent_event import (
|
|
AgentEvent,
|
|
AssistantMessageCompletedEvent,
|
|
ErrorEvent,
|
|
HistoryReadyEvent,
|
|
NOTICE_INFO,
|
|
NoticeEvent,
|
|
OutputsAddedEvent,
|
|
OutputsRemovedEvent,
|
|
PlanStep,
|
|
PlanUpdatedEvent,
|
|
ReasoningChunkEvent,
|
|
TextChunkEvent,
|
|
ToolCallFinishedEvent,
|
|
ToolCallStartedEvent,
|
|
ToolOutputChunkEvent,
|
|
ToolPreview,
|
|
TurnCompletedEvent,
|
|
)
|
|
|
|
|
|
def _plan_steps_from_legacy(raw: Any) -> Tuple[PlanStep, ...]:
|
|
"""Parse the legacy ``steps`` list, dropping anything unusable.
|
|
|
|
A step with no title cannot be rendered or ticked off, so it is discarded
|
|
instead of becoming a blank row in the Plan panel.
|
|
"""
|
|
if not isinstance(raw, list):
|
|
return ()
|
|
steps: List[PlanStep] = []
|
|
for item in raw:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
title = str(item.get("title", "")).strip()
|
|
if not title:
|
|
continue
|
|
steps.append(PlanStep(title=title, status=str(item.get("status", "pending"))))
|
|
return tuple(steps)
|
|
|
|
|
|
def _parse_tool_started(raw: Dict[str, Any]) -> ToolCallStartedEvent:
|
|
"""Rebuild a ``tool_proposed`` event, mapping ``id``/``args`` to typed names."""
|
|
args = raw.get("args")
|
|
return ToolCallStartedEvent(
|
|
call_id=str(raw.get("id", "")), name=str(raw.get("name", "")),
|
|
arguments=dict(args) if isinstance(args, dict) else {},
|
|
preview=ToolPreview.from_dict(raw.get("preview")),
|
|
)
|
|
|
|
|
|
def _parse_tool_finished(raw: Dict[str, Any]) -> ToolCallFinishedEvent:
|
|
"""Rebuild a ``tool_result`` event; the optional file keys may be absent."""
|
|
return ToolCallFinishedEvent(
|
|
call_id=str(raw.get("id", "")), name=str(raw.get("name", "")),
|
|
ok=bool(raw.get("ok", False)), output=str(raw.get("output", "")),
|
|
path=str(raw.get("path", "") or ""), produced=raw.get("produced") or (),
|
|
)
|
|
|
|
|
|
# One parser per wire name. A table (rather than an if/elif chain) keeps adding
|
|
# an event a single-line change and makes the supported set introspectable.
|
|
_PARSERS = {
|
|
TextChunkEvent.EVENT_TYPE: lambda raw: TextChunkEvent(delta=str(raw.get("delta", ""))),
|
|
ReasoningChunkEvent.EVENT_TYPE: lambda raw: ReasoningChunkEvent(
|
|
delta=str(raw.get("delta", ""))),
|
|
AssistantMessageCompletedEvent.EVENT_TYPE: lambda raw: AssistantMessageCompletedEvent(
|
|
content=str(raw.get("content", ""))),
|
|
ToolCallStartedEvent.EVENT_TYPE: _parse_tool_started,
|
|
ToolOutputChunkEvent.EVENT_TYPE: lambda raw: ToolOutputChunkEvent(
|
|
call_id=str(raw.get("id", "")), name=str(raw.get("name", "")),
|
|
delta=str(raw.get("delta", ""))),
|
|
ToolCallFinishedEvent.EVENT_TYPE: _parse_tool_finished,
|
|
PlanUpdatedEvent.EVENT_TYPE: lambda raw: PlanUpdatedEvent(
|
|
steps=_plan_steps_from_legacy(raw.get("steps"))),
|
|
NoticeEvent.EVENT_TYPE: lambda raw: NoticeEvent(
|
|
text=str(raw.get("text", "")), level=str(raw.get("level", NOTICE_INFO))),
|
|
OutputsAddedEvent.EVENT_TYPE: lambda raw: OutputsAddedEvent(paths=raw.get("paths") or ()),
|
|
OutputsRemovedEvent.EVENT_TYPE: lambda raw: OutputsRemovedEvent(paths=raw.get("paths") or ()),
|
|
HistoryReadyEvent.EVENT_TYPE: lambda raw: HistoryReadyEvent(
|
|
session_id=str(raw.get("session_id", ""))),
|
|
TurnCompletedEvent.EVENT_TYPE: lambda raw: TurnCompletedEvent(
|
|
final_text=str(raw.get("final_text", "")), steps_used=int(raw.get("steps_used", 0) or 0),
|
|
cancelled=bool(raw.get("cancelled", False)),
|
|
budget_exhausted=bool(raw.get("budget_exhausted", False))),
|
|
ErrorEvent.EVENT_TYPE: lambda raw: ErrorEvent(
|
|
message=str(raw.get("message", "")), recoverable=bool(raw.get("recoverable", False))),
|
|
}
|
|
|
|
|
|
def from_legacy_dict(payload: Any) -> Optional[AgentEvent]:
|
|
"""Parse an emitted dict into a typed event, or ``None`` if it isn't ours.
|
|
|
|
``None`` (rather than an exception) is the contract that makes incremental
|
|
adoption possible: a bridge sitting between the runtime and the widget can
|
|
type the events it recognises and forward everything else — Co4E's node
|
|
events, or anything a future emitter adds — completely untouched.
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return None
|
|
parser = _PARSERS.get(str(payload.get("type", "")))
|
|
return parser(payload) if parser is not None else None
|
|
|
|
|
|
__all__ = ["from_legacy_dict"]
|