chore(repo): initialize Cowork Local Gitea repository
CI / test (push) Canceled after 0s

This commit is contained in:
thanhnv
2026-08-09 20:12:05 +07:00
commit 414eaddca3
192 changed files with 48160 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
"""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:
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:
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:
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:
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:
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]:
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]:
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:
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"