Hồi quy đã vá
-------------
F-12 Kéo–thả hoặc dán tệp vào ô chat ném NameError. R08 tách `_Input` sang
`chat_input_box.py` nhưng để `_paths_from_mime()` ở lại
`composer_widget.py`, nên hai hàm sự kiện Qt gọi một cái tên không tồn
tại. Bốn hàm dùng chung chuyển sang `composer_mime.py` — module thứ ba
là chỗ duy nhất không lặp lại được lỗi này. Đo lại: cả thả lẫn dán đều
gắn 1 tệp, khớp bản trước refactor.
F-01 Đổi provider thì bộ chọn model AI-Edit không làm gì. Hook cũ kiểm
`folder.ai_model_combo`, thuộc tính R08-T12 đã dời sang
`ai_panel.resolver`. Làm mới vô điều kiện, đúng như tab cũ: lần lấy đầu
tiên hỏng thì đổi provider chính là lúc phải thử lại.
F-07 Hàng chọn kỳ của Dashboard bị đẩy xuống dưới các thẻ số liệu. Hàng này
lọc CẢ BA thẻ con chứ không riêng biểu đồ, nên để nó nằm dưới là bắt
người dùng đọc con số trước khi thấy con số đó tính cho kỳ nào. Kèm
theo: `TokenUsageCardWidget` bị bỏ sót `setContentsMargins(0,0,0,0)`
mà hai thẻ con còn lại đã có, đẩy cả hàng thẻ lệch 9px.
`check_layout_geometry` nay khớp TỪNG BYTE với bản trước refactor.
F-11 Hai lớp khai trùng tên phương thức; Python giữ bản sau nên bản đầu là
mã chết. `co4e_tab.py::showEvent` bản đầu gọi `_narrow_guard.attach()`
và không bao giờ chạy.
Tách file (F-09)
----------------
Bốn file chạm trần 400 dòng, mỗi lần cắt ra một trách nhiệm thật:
graph_renderer.py -> graph_scene_builder.py + graph_export.py
co4e_workflow_service.py -> co4e_run_history.py
json_config_repository.py -> config_sections.py
agents_admin_tab.py -> shared/agent_kind_visuals.py
File cuối còn xoá 3 bản sao của hàm đã có trong `shared/formatters.py`,
giống hệt đến từng dòng — nay định dạng thời gian và avatar không lệch nhau
giữa các bảng Giám sát nữa.
Docstring
---------
41,6% -> 100% (3.478/3.478 định nghĩa production), kể cả module dormant và
phương thức dunder. Toàn bộ phần bổ sung viết bằng tiếng Việt; comment tiếng
Anh có sẵn giữ nguyên — dịch ngược là một đợt riêng.
Seam chưa nối dây (F-05)
------------------------
9 seam mang nhãn `SEAM · dựng <ngày>` kèm hai câu: được nối khi nào, và để
dormant thì hỏng gì. Ngày lấy từ lịch sử git, không phải hạn tự đặt. Gate O
đọc nhãn đó và nhắc khi quá 30 ngày.
859 test xanh · 4/4 cổng CASAN · 19/24 checker khớp từng byte bản cũ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
222 lines
10 KiB
Python
222 lines
10 KiB
Python
"""Admin-managed Agent catalog — named agent presets every machine shares.
|
|
|
|
An *admin agent* is defined once by the Admin (Monitoring → Agents Admin):
|
|
a name, the app function it performs (picked from a fixed droplist —
|
|
search / monitor / cowork / graphrag / schedule / security), optional extra
|
|
instructions, and the model to run on. The model defaults to each machine's
|
|
own Settings model when left empty; when the Admin pins one, every machine
|
|
runs that agent on the pinned model.
|
|
|
|
Storage mirrors ``accounts.py``'s pattern: one JSON per agent under
|
|
``<shared_dir>/agents_admin/`` so the catalog syncs across machines through
|
|
the same OneDrive/network share the accounts already use (the Admin edits,
|
|
other machines pick the change up next refresh once the share syncs). With
|
|
no shared folder configured it falls back to a local folder so the feature
|
|
still works single-machine.
|
|
|
|
This catalog is deliberately SEPARATE from ``custom_agents.py`` (per-user
|
|
Flow sub-agent presets stored locally): these are org-wide, admin-owned, and
|
|
selectable from the Cowork tab's Agent picker and the Schedule Task editor.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import uuid
|
|
from dataclasses import asdict, dataclass
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
from ..config import CONFIG_DIR
|
|
|
|
# The app functions an agent can be assigned to (droplist in the editor).
|
|
# Labels come from i18n keys ``agents_admin.kind.<kind>``.
|
|
TASK_KINDS = ("search", "monitor", "cowork", "graphrag", "schedule", "security", "help")
|
|
|
|
# Stable id for the built-in in-app Help assistant (the floating icon widget).
|
|
# Seeded once so the Admin can pick its provider/model in Agents Admin, while
|
|
# the widget always looks it up by this id.
|
|
HELP_AGENT_ID = "help-agent-builtin"
|
|
|
|
# Base instructions injected for each kind — the admin's own prompt (if any)
|
|
# is appended after these.
|
|
_KIND_PROMPTS = {
|
|
"search": ("You are a dedicated SEARCH agent: locate the requested information in the "
|
|
"provided files/folders/links and answer with precise findings and their "
|
|
"sources. Do not create files unless explicitly asked."),
|
|
"monitor": ("You are a dedicated MONITORING agent: review the provided logs/data for "
|
|
"anomalies, errors, security events and trends, and report a concise "
|
|
"status summary with anything needing attention first."),
|
|
"cowork": "",
|
|
"graphrag": ("You are a dedicated KNOWLEDGE agent: answer strictly from the project's "
|
|
"knowledge files/graph, citing which file each fact came from."),
|
|
"schedule": ("You are a dedicated TASK agent executing a scheduled job: complete the "
|
|
"task end-to-end without asking questions, and save the deliverable."),
|
|
"security": ("You are a dedicated SECURITY agent: review the given prompt/attachment/"
|
|
"command against the org security rules and decide whether it is safe to "
|
|
"allow. Reply strictly with the requested JSON verdict; err on the side of "
|
|
"blocking anything that could exfiltrate data or damage the system."),
|
|
"help": ("You are the in-app HELP assistant for this desktop application. Your ONLY job "
|
|
"is to help the user understand and use THIS app — its screens and features "
|
|
"(Dashboard, Schedule, Workspace with Cowork chat and the Co4E flow studio, "
|
|
"Monitoring, Connectors, Settings), how to get things done in it, and how to "
|
|
"troubleshoot using it. Be concise, friendly and practical.\n"
|
|
"STRICT RULES:\n"
|
|
"- Answer ONLY questions about using this app. If asked to do anything else "
|
|
"(write code for other purposes, do general research, chit-chat, run tasks, "
|
|
"act as a general assistant), politely decline and steer back to app help.\n"
|
|
"- You have no tools and cannot perform actions — you only explain and guide.\n"
|
|
"- ALWAYS reply in the SAME language the user wrote their message in, "
|
|
"regardless of the app's display language."),
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class AdminAgent:
|
|
"""Một agent chuyên trách do quản trị cấu hình: prompt riêng, provider và model riêng."""
|
|
agent_id: str
|
|
name: str
|
|
task_kind: str = "cowork"
|
|
prompt: str = "" # admin's extra instructions (appended to the kind's base)
|
|
provider: str = "" # "" = each machine's active provider
|
|
model: str = "" # "" = each machine's Settings model for that provider
|
|
enabled: bool = True
|
|
updated: str = ""
|
|
updated_by: str = ""
|
|
|
|
def effective_prompt(self) -> str:
|
|
"""Prompt hệ thống thật sự dùng: prompt mặc định theo loại việc, rồi tới phần
|
|
quản trị viết thêm.
|
|
"""
|
|
parts = [_KIND_PROMPTS.get(self.task_kind, ""), (self.prompt or "").strip()]
|
|
return "\n\n".join(p for p in parts if p)
|
|
|
|
|
|
def agents_admin_dir(shared_dir: str = "") -> Path:
|
|
"""Shared catalog folder when a shared dir is configured (cross-machine
|
|
sync), else a local fallback so the feature works single-machine too."""
|
|
if (shared_dir or "").strip():
|
|
return Path(shared_dir).expanduser() / "agents_admin"
|
|
return CONFIG_DIR / "agents_admin"
|
|
|
|
|
|
def _slug(name: str) -> str:
|
|
"""Định danh an toàn cho tên file, suy từ tên agent."""
|
|
s = re.sub(r"[^\w\-]+", "-", (name or "").strip().lower()).strip("-")
|
|
return s or "agent"
|
|
|
|
|
|
def new_agent(name: str, task_kind: str = "cowork", prompt: str = "",
|
|
provider: str = "", model: str = "", updated_by: str = "") -> AdminAgent:
|
|
"""Tạo một agent quản trị mới; loại việc lạ thì rơi về 'cowork'.
|
|
|
|
Id ghép slug với 6 ký tự ngẫu nhiên để hai agent trùng tên không đè file nhau.
|
|
"""
|
|
return AdminAgent(
|
|
agent_id=f"{_slug(name)}-{uuid.uuid4().hex[:6]}",
|
|
name=name.strip(), task_kind=task_kind if task_kind in TASK_KINDS else "cowork",
|
|
prompt=prompt, provider=provider, model=model, enabled=True,
|
|
updated=datetime.now().isoformat(timespec="seconds"), updated_by=updated_by,
|
|
)
|
|
|
|
|
|
def save_agent(agent: AdminAgent, directory: Path) -> Path:
|
|
"""Ghi một agent ra ``<agent_id>.json``."""
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{agent.agent_id}.json"
|
|
path.write_text(json.dumps(asdict(agent), ensure_ascii=False, indent=2), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def load_agent(agent_id: str, directory: Path) -> Optional[AdminAgent]:
|
|
"""Đọc một agent theo id; không có thì trả ``None``."""
|
|
path = directory / f"{agent_id}.json"
|
|
if not path.exists():
|
|
return None
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
known = {f for f in AdminAgent.__dataclass_fields__}
|
|
return AdminAgent(**{k: v for k, v in data.items() if k in known})
|
|
except (OSError, json.JSONDecodeError, TypeError):
|
|
return None
|
|
|
|
|
|
def list_agents(directory: Path, enabled_only: bool = False) -> List[AdminAgent]:
|
|
"""Liệt kê agent trong thư mục; ``enabled_only`` chỉ lấy agent đang bật."""
|
|
if not directory.exists():
|
|
return []
|
|
out: List[AdminAgent] = []
|
|
for path in sorted(directory.glob("*.json")):
|
|
agent = load_agent(path.stem, directory)
|
|
if agent is not None and (agent.enabled or not enabled_only):
|
|
out.append(agent)
|
|
out.sort(key=lambda a: a.name.lower())
|
|
return out
|
|
|
|
|
|
def ensure_help_agent(directory: Path) -> AdminAgent:
|
|
"""Return the built-in Help assistant, seeding it on first run so it shows
|
|
up in Agents Admin for the admin to pick a provider/model. Idempotent: an
|
|
existing entry (possibly with admin edits) is loaded and returned as-is —
|
|
only its immutable identity (id / kind) is guaranteed. The floating Help
|
|
widget always resolves the agent through this."""
|
|
existing = load_agent(HELP_AGENT_ID, directory)
|
|
if existing is not None:
|
|
return existing
|
|
agent = AdminAgent(
|
|
agent_id=HELP_AGENT_ID, name="App Help Assistant", task_kind="help",
|
|
prompt="", provider="", model="", enabled=True,
|
|
updated=datetime.now().isoformat(timespec="seconds"), updated_by="system",
|
|
)
|
|
try:
|
|
save_agent(agent, directory)
|
|
except OSError:
|
|
pass # read-only share — still usable in-memory this session
|
|
return agent
|
|
|
|
|
|
def delete_agent(agent_id: str, directory: Path) -> bool:
|
|
"""Xoá file agent; trả về ``True`` nếu có file để xoá."""
|
|
try:
|
|
(directory / f"{agent_id}.json").unlink()
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def build_agent_provider(ctx, agent: Optional[AdminAgent]):
|
|
"""The provider an admin agent runs on: its own pinned provider/model
|
|
when set, else the machine's active provider with its Settings model —
|
|
exactly the default the requirement asks for ("default là Model được
|
|
chọn trong setting")."""
|
|
if agent is None:
|
|
return ctx.build_active_provider()
|
|
return ctx.build_provider_for(agent.provider or None, agent.model or None)
|
|
|
|
|
|
def check_agent(ctx, agent: AdminAgent) -> tuple[bool, str]:
|
|
"""Best-effort OPERATIONAL health check for one admin agent: can its
|
|
EFFECTIVE provider (its pinned provider/model, or — when unset — the
|
|
machine's Settings provider/model) actually be reached right now?
|
|
|
|
Probes the provider's ``list_models()`` (the same lightweight call the
|
|
'Load models' button makes) rather than spending a real chat turn.
|
|
Returns ``(ok, message)`` and NEVER raises, so the UI can render a status
|
|
without a broken agent config taking the whole table down."""
|
|
if not agent.enabled:
|
|
return False, "disabled"
|
|
try:
|
|
provider = build_agent_provider(ctx, agent)
|
|
except Exception as exc: # noqa: BLE001 — a bad config must not crash the check
|
|
return False, f"config error: {exc}"
|
|
try:
|
|
models = provider.list_models()
|
|
except Exception as exc: # noqa: BLE001 — gateway unreachable / auth / TLS…
|
|
return False, str(exc)[:200]
|
|
if not models:
|
|
return False, "no models returned by provider"
|
|
if agent.model and agent.model not in models:
|
|
return True, f"reachable — note: pinned model '{agent.model}' not in provider's list"
|
|
return True, "reachable"
|