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