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
+63 -2
View File
@@ -18,6 +18,8 @@ existing ``core/skills.py`` registry.
"""
from __future__ import annotations
from ..infrastructure.persistence.json.atomic_json_file import AtomicJsonFile
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
@@ -52,6 +54,9 @@ RUN_MODES = ("auto", "plan", "manual")
def slugify(value: str) -> str:
"""Chuyển một chuỗi thành slug an toàn cho tên file: chỉ chữ/số/gạch, gộp gạch
liên tiếp. Rỗng thì trả về 'step' để không bao giờ sinh ra tên file trống.
"""
s = "".join(c if (c.isalnum() or c in "-_") else "-" for c in (value or "").strip().lower())
return "-".join(filter(None, s.split("-"))) or "step"
@@ -86,11 +91,13 @@ class Step:
@property
def is_parallel(self) -> bool:
"""Bước này có chạy nhiều sub-agent song song hay không."""
return self.variant == "parallel"
@dataclass
class Node:
"""Một node trên khung vẽ: id, toạ độ, và bước (:class:`Step`) mà nó đại diện."""
id: str
x: float = 0.0
y: float = 0.0
@@ -99,6 +106,7 @@ class Node:
@dataclass
class Edge:
"""Một cạnh nối hai node, quy định thứ tự chạy giữa chúng."""
id: str
source: str
target: str
@@ -106,6 +114,7 @@ class Edge:
@dataclass
class Workflow:
"""Một luồng Co4E: danh sách node, cạnh, và cờ đánh dấu đây có phải mẫu không."""
id: str
name: str = "Untitled flow"
is_template: bool = False
@@ -130,6 +139,10 @@ class CustomAgent:
# ---- (de)serialization ---------------------------------------------------
def step_from_dict(d: dict) -> Step:
"""Dựng :class:`Step` từ dict đọc trên đĩa.
Lọc bỏ khoá lạ để file luồng của phiên bản mới hơn không làm vỡ bản cũ.
"""
d = dict(d or {})
subs = d.pop("sub_agents", None) or []
known = Step().__dict__.keys()
@@ -143,11 +156,13 @@ def step_from_dict(d: dict) -> Step:
def node_from_dict(d: dict) -> Node:
"""Dựng :class:`Node` từ dict đọc trên đĩa."""
return Node(id=str(d.get("id", "")), x=float(d.get("x", 0) or 0),
y=float(d.get("y", 0) or 0), data=step_from_dict(d.get("data", {})))
def workflow_from_dict(d: dict) -> Workflow:
"""Dựng :class:`Workflow` từ dict đọc trên đĩa."""
return Workflow(
id=str(d.get("id", "")),
name=d.get("name", "Untitled flow"),
@@ -159,6 +174,7 @@ def workflow_from_dict(d: dict) -> Workflow:
def workflow_to_dict(wf: Workflow) -> dict:
"""Chuyển một luồng thành dict để ghi JSON."""
return {
"id": wf.id, "name": wf.name, "is_template": wf.is_template,
"nodes": [{"id": n.id, "x": n.x, "y": n.y, "data": _step_dict(n.data)} for n in wf.nodes],
@@ -167,16 +183,19 @@ def workflow_to_dict(wf: Workflow) -> dict:
def _step_dict(step: Step) -> dict:
"""Chuyển một bước thành dict; ``asdict`` đã tự chuyển ``sub_agents`` thành list dict."""
d = asdict(step)
# asdict already turns sub_agents into list[dict]
return d
def agent_to_dict(a: CustomAgent) -> dict:
"""Chuyển một agent tự tạo thành dict để ghi JSON."""
return asdict(a)
def agent_from_dict(d: dict) -> CustomAgent:
"""Dựng :class:`CustomAgent` từ dict, lọc bỏ khoá lạ."""
known = CustomAgent(id="").__dict__.keys()
d = {k: v for k, v in (d or {}).items() if k in known}
d.setdefault("id", "")
@@ -191,32 +210,43 @@ _counter = {"n": 0}
def _mint_id(prefix: str) -> str:
"""Sinh id tăng dần dạng ``<prefix>_000001``."""
_counter["n"] += 1
return f"{prefix}_{_counter['n']:06d}"
def new_node_id() -> str:
"""Id mới cho một node."""
return _mint_id("node")
def new_edge_id(source: str, target: str) -> str:
"""Id cạnh suy ra TỪ cặp nguồn/đích.
Cố ý không ngẫu nhiên: nhờ vậy nối lại đúng cặp node đó luôn cho ra cùng
một id, và không thể sinh ra hai cạnh trùng nhau.
"""
return f"e_{source}__{target}"
def new_workflow(name: str = "Untitled flow") -> Workflow:
"""Tạo một luồng rỗng với id mới."""
return Workflow(id=_mint_id("wf"), name=name)
def new_custom_agent(name: str = "") -> CustomAgent:
"""Tạo một agent tự tạo rỗng với id mới."""
return CustomAgent(id=_mint_id("agent"), name=name)
# ---- workflow store ------------------------------------------------------
def workflows_dir() -> Path:
"""Thư mục chứa file luồng."""
return WORKFLOWS_DIR
def list_workflows(directory: Optional[Path] = None) -> List[Workflow]:
"""Liệt kê mọi luồng đã lưu; thư mục chưa có thì trả list rỗng."""
directory = directory or WORKFLOWS_DIR
if not directory.exists():
return []
@@ -230,14 +260,18 @@ def list_workflows(directory: Optional[Path] = None) -> List[Workflow]:
def save_workflow(wf: Workflow, directory: Optional[Path] = None) -> Path:
"""Ghi một luồng ra ``<id>.json``, tự tạo thư mục nếu chưa có."""
directory = directory or WORKFLOWS_DIR
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{wf.id}.json"
path.write_text(json.dumps(workflow_to_dict(wf), ensure_ascii=False, indent=2), encoding="utf-8")
# Tiêu chí nghiệm thu A: mọi thao tác ghi tệp đi qua AtomicJsonFile. Trước
# đây ghi thẳng, nên tắt máy giữa lúc lưu là mất luôn workflow.
AtomicJsonFile(path).write(workflow_to_dict(wf))
return path
def get_workflow(wf_id: str, directory: Optional[Path] = None) -> Optional[Workflow]:
"""Đọc một luồng theo id; ``None`` nếu không có."""
directory = directory or WORKFLOWS_DIR
path = directory / f"{wf_id}.json"
if not path.exists():
@@ -276,6 +310,7 @@ def tr_copy_suffix() -> str:
def delete_workflow(wf_id: str, directory: Optional[Path] = None) -> None:
"""Xoá file luồng theo id; không có thì bỏ qua."""
directory = directory or WORKFLOWS_DIR
path = directory / f"{wf_id}.json"
if path.exists():
@@ -287,10 +322,12 @@ def delete_workflow(wf_id: str, directory: Optional[Path] = None) -> None:
# ---- custom-agent store --------------------------------------------------
def agents_dir() -> Path:
"""Thư mục chứa file agent tự tạo."""
return AGENTS_DIR
def list_custom_agents(directory: Optional[Path] = None) -> List[CustomAgent]:
"""Liệt kê mọi agent tự tạo; thư mục chưa có thì trả list rỗng."""
directory = directory or AGENTS_DIR
if not directory.exists():
return []
@@ -304,14 +341,16 @@ def list_custom_agents(directory: Optional[Path] = None) -> List[CustomAgent]:
def save_custom_agent(agent: CustomAgent, directory: Optional[Path] = None) -> Path:
"""Ghi một agent tự tạo ra ``<id>.json``."""
directory = directory or AGENTS_DIR
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{agent.id}.json"
path.write_text(json.dumps(agent_to_dict(agent), ensure_ascii=False, indent=2), encoding="utf-8")
AtomicJsonFile(path).write(agent_to_dict(agent))
return path
def delete_custom_agent(agent_id: str, directory: Optional[Path] = None) -> None:
"""Xoá file agent tự tạo theo id; không có thì bỏ qua."""
directory = directory or AGENTS_DIR
path = directory / f"{agent_id}.json"
if path.exists():
@@ -336,6 +375,11 @@ def compute_waves(nodes: List[Node], edges: List[Edge]) -> Dict[str, int]:
limit = len(nodes) + 1
def depth(nid: str, seen: frozenset) -> int:
"""Độ sâu của một node = lớp chạy của nó.
Có nhớ kết quả và chặn theo ``limit``: đồ thị có vòng sẽ khiến đệ quy chạy
mãi, nên gặp node đã thấy trong nhánh hiện tại thì dừng.
"""
if nid in wave:
return wave[nid]
if nid in seen or len(seen) > limit:
@@ -356,12 +400,14 @@ def connected_component_count(nodes: List[Node], edges: List[Edge]) -> int:
parent = {n.id: n.id for n in nodes}
def find(x):
"""Tìm gốc của một phần tử, kèm nén đường đi (union-find)."""
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a, b):
"""Gộp hai tập hợp lại làm một (union-find)."""
ra, rb = find(a), find(b)
if ra != rb:
parent[ra] = rb
@@ -375,6 +421,7 @@ def connected_component_count(nodes: List[Node], edges: List[Edge]) -> int:
# ---- run-stage compilation ----------------------------------------------
@dataclass
class RunStage:
"""Một chặng chạy: ứng với một node, hoặc một nhánh song song / bước gộp của nó."""
id: str # node id, or "<node>__p<i>" / "<node>__pjoin"
node_id: str # which canvas node this stage maps back onto
wave: int
@@ -391,6 +438,7 @@ PLAN_MODE_PREAMBLE = (
def build_skills_block(skills: List[str], skill_map: Dict[str, str]) -> str:
"""Ghép nội dung các skill được chọn thành một khối chèn vào prompt."""
parts = []
for name in skills or []:
content = (skill_map.get(name) or "").strip()
@@ -403,6 +451,9 @@ def build_skills_block(skills: List[str], skill_map: Dict[str, str]) -> str:
def _shared_prompt_parts(step: Step, skill_map: Dict[str, str], extra_context: str) -> str:
"""Phần prompt dùng chung cho cả ba loại chặng: chỉ dẫn của bước, khối skill,
và ngữ cảnh thêm từ các bước trước.
"""
parts = []
if step.instructions.strip():
parts.append(step.instructions.strip())
@@ -419,6 +470,7 @@ def _shared_prompt_parts(step: Step, skill_map: Dict[str, str], extra_context: s
def build_step_prompt(step: Step, skill_map: Dict[str, str], extra_context: str = "") -> str:
"""Prompt cho một bước chạy tuần tự bình thường."""
head = f'You are the {step.role} agent for the workflow step "{step.label}".'
body = _shared_prompt_parts(step, skill_map, extra_context)
return f"{head}\n{body}".strip()
@@ -426,6 +478,11 @@ def build_step_prompt(step: Step, skill_map: Dict[str, str], extra_context: str
def build_subagent_prompt(step: Step, sub: SubAgent, peers: List[str],
skill_map: Dict[str, str], extra_context: str = "") -> str:
"""Prompt cho một sub-agent chạy song song.
Nói rõ nó đang chạy CÙNG LÚC với những ai và phải ở trong phạm vi của mình —
không có câu đó, các sub-agent hay làm chồng việc của nhau.
"""
peer_txt = ", ".join(p for p in peers if p) or "peers"
head = (f'You are the "{sub.agent}" agent working concurrently (in parallel with '
f'{peer_txt}) on the workflow step "{step.label}". Stay within your own scope.')
@@ -439,6 +496,7 @@ def build_subagent_prompt(step: Step, sub: SubAgent, peers: List[str],
def build_join_prompt(step: Step, skill_map: Dict[str, str], extra_context: str = "") -> str:
"""Prompt cho bước gộp: hợp nhất đầu ra của các sub-agent thành một kết quả."""
head = (f'You are the coordinator for the parallel step "{step.label}". Consolidate the '
f"outputs of the sub-agents (provided above as prior outputs) into one coherent result.")
body = _shared_prompt_parts(step, skill_map, extra_context)
@@ -457,6 +515,9 @@ def compile_run_stages(nodes: List[Node], edges: List[Edge],
stages: List[RunStage] = []
def finalize(prompt: str, preset: str) -> tuple:
"""Chốt prompt của một chặng: áp phạm vi theo preset, và thêm lời mở đầu chế
độ lập kế hoạch nếu đang chạy ở chế độ đó.
"""
scope = PRESET_SCOPES.get(preset)
if plan_mode:
prompt = PLAN_MODE_PREAMBLE + prompt