Feature/delta team/epic r04 (#7)
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>
This commit was merged in pull request #7.
This commit is contained in:
2026-08-31 05:15:13 +00:00
committed by gitea-admin
co-authored by anhtnm1 huongltt35 Nam Pham Dinh Thanh vudt15 Hiep Ha Van lamhv7
parent 86c27e2e79
commit f9f6bc01fd
496 changed files with 68421 additions and 19688 deletions
+174
View File
@@ -0,0 +1,174 @@
"""``Co4EWorkflowService`` giả — cho widget Co4E Studio (presentation/) và cho
test khác dùng khi service thật
(``application/workflows/co4e_workflow_service.py``) chưa được ``bootstrap.py``
lắp vào, hoặc khi test không muốn chạm đĩa/AI thật.
Chạy hoàn toàn trong bộ nhớ, đồng bộ, không cần ``runner`` thật (không
``AgentWorker``/``QThread`` nào được tạo): ``start()`` ghi nhận run ở trạng
thái "running" rồi đứng yên — muốn mô phỏng tiến trình thì test tự gọi
``deliver_event``/``mark_finished``/``mark_failed``, giống hệt cách
``tests/characterization/test_co4e_run_manager_behavior.py`` seed tay vào
``Co4ERunManager`` thật rồi gọi ``_on_event``/``_on_finished``/``_on_failed``.
Ví dụ dùng::
>>> from tests.fakes.fake_co4e_workflow_service import FakeCo4EWorkflowService
>>> class _Wf:
... id = "wf1"; name = "Flow"; nodes = []; edges = []
>>> svc = FakeCo4EWorkflowService()
>>> run_id = svc.start(_Wf())
>>> svc.started_workflows[0].id
'wf1'
>>> svc.runs()[0].status
'running'
>>> svc.mark_finished(run_id)
>>> svc.runs()[0].status
'done'
"""
from __future__ import annotations
from pathlib import Path
from typing import Callable, Dict, List, Optional
from cowork_local.domain.workflows.run_record import RunRecord
# Mirror dung gia tri cua STEP_DONE/STEP_ERROR/STEP_PLANNED (core/co4e.py) ma
# khong import core/ o day -- fake nay chi phu thuoc domain/, giu no nhe va
# nhanh de import trong test cua team khac.
_TERMINAL_NODE = {"done", "error", "planned"}
class FakeCo4EWorkflowService:
"""Bản giả của ``Co4EWorkflowService`` — cùng API công khai, ghi lại mọi
lời gọi để test khẳng định được "có gọi service không" và "gọi với gì"."""
def __init__(self):
self._runs: Dict[str, RunRecord] = {}
self._seq = 0
self._project_id: str = ""
self._output_root: Optional[Path] = None
self._changed_callbacks: List[Callable[[], None]] = []
self._event_callbacks: List[Callable[[str, dict], None]] = []
#: moi workflow da duoc start(), dung thu tu goi -- test khang dinh
#: "co goi service.start() khong" ma khong can thuc thi that.
self.started_workflows: list = []
self.stopped_run_ids: List[str] = []
self.removed_run_ids: List[str] = []
self.renamed: List[tuple] = []
# ---- callback thay Signal (giong Co4EWorkflowService that) -------------
def on_changed(self, cb: Callable[[], None]) -> None:
self._changed_callbacks.append(cb)
def on_event(self, cb: Callable[[str, dict], None]) -> None:
self._event_callbacks.append(cb)
def _emit_changed(self) -> None:
for cb in self._changed_callbacks:
cb()
def _emit_event(self, run_id: str, ev: dict) -> None:
for cb in self._event_callbacks:
cb(run_id, ev)
# ---- lifecycle ----------------------------------------------------
def start(self, wf, *, skill_map=None, plan_mode: bool = False, only_nodes=None,
seed_outputs=None, manual: bool = False, label: Optional[str] = None) -> str:
self._seq += 1
run_id = f"run{self._seq}"
nodes = getattr(wf, "nodes", None) or []
total = len(only_nodes) if only_nodes else len(nodes)
record = RunRecord(run_id, getattr(wf, "id", ""), label or getattr(wf, "name", ""),
total, plan_mode, manual, project_id=self._project_id)
self._runs[run_id] = record
self.started_workflows.append(wf)
self._emit_changed()
return run_id
# ---- hook gia lap tien trinh (goi TU TEST, khong phai tu runner that) --
def deliver_event(self, run_id: str, ev: dict) -> None:
"""Mo phong dung ``Co4EWorkflowService._on_event`` that."""
record = self._runs.get(run_id)
if record is not None and isinstance(ev, dict):
t = ev.get("type")
if t == "node_status":
record.node_status[ev.get("node_id")] = ev.get("status")
record.done = sum(1 for s in record.node_status.values() if s in _TERMINAL_NODE)
self._emit_changed()
elif t == "run_done":
if record.status == "running":
record.status = "done" if ev.get("ok", True) else "error"
self._emit_changed()
self._emit_event(run_id, ev)
def mark_finished(self, run_id: str) -> None:
record = self._runs.get(run_id)
if record is not None and record.status == "running":
record.status = "done"
self._emit_changed()
def mark_failed(self, run_id: str, err: str) -> None:
record = self._runs.get(run_id)
if record is not None:
record.status = "error"
record.error = str(err)
self._emit_event(run_id, {"type": "run_error", "error": str(err)})
self._emit_changed()
# ---- control --------------------------------------------------------
def stop(self, run_id: str) -> None:
record = self._runs.get(run_id)
if record is not None and record.running:
record.status = "stopped"
self.stopped_run_ids.append(run_id)
self._emit_changed()
def stop_all(self) -> None:
for run_id in [r for r, rec in self._runs.items() if self._belongs(rec)]:
self.stop(run_id)
def rename(self, run_id: str, new_name: str) -> None:
record = self._runs.get(run_id)
new_name = (new_name or "").strip()
if record is None or not new_name or new_name == record.name:
return
record.name = new_name
if record.wf is not None:
record.wf["name"] = new_name
self.renamed.append((run_id, new_name))
self._emit_changed()
def remove(self, run_id: str) -> None:
self._runs.pop(run_id, None)
self.removed_run_ids.append(run_id)
self._emit_changed()
def clear_finished(self) -> None:
for run_id in [r for r, rec in self._runs.items() if not rec.running and self._belongs(rec)]:
self._runs.pop(run_id, None)
self._emit_changed()
# ---- queries ----------------------------------------------------------
def _belongs(self, r: RunRecord) -> bool:
return getattr(r, "project_id", "") == self._project_id
def runs(self) -> List[RunRecord]:
return [r for r in self._runs.values() if self._belongs(r)]
def all_runs(self) -> List[RunRecord]:
return list(self._runs.values())
def get(self, run_id: str) -> Optional[RunRecord]:
return self._runs.get(run_id)
def active_count(self) -> int:
return sum(1 for r in self._runs.values() if r.running and self._belongs(r))
def set_current_project(self, project_id: str) -> None:
pid = project_id or ""
if pid != self._project_id:
self._project_id = pid
self._emit_changed()
def set_output_root(self, root) -> None:
self._output_root = Path(root) if root else None