## 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:
@@ -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
|
||||
Reference in New Issue
Block a user