feat(beaver): 完成Task Team功能v1实现,重构后端架构支持统一内核
新增内部Task系统,包括验证、反馈门控机制,实现自动质量验证 (通过率>=0.75)和用户反馈闭环(satisfied/revise/abandon)。 实现Agent Team v1协调器,支持sequence/parallel/dag执行策略, sub-agent复用主AgentLoop,每个run使用独立memory snapshot。 建立Skill学习pipeline,包含draft/审核/发布/回滚完整生命周期, 通过Task验证通过且用户满意才生成学习候选。 重构目录结构,移除third_party依赖,建立统一engine内核, 所有agent共享运行时基础组件。 更新ContextBuilder清理provider消息字段,增强SkillContext版本管理, 集成TaskExecutionPlanner和TaskSkillResolver实现技能解析机制。
This commit is contained in:
@ -2,16 +2,30 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from beaver.engine.providers.registry import PROVIDERS, find_by_name
|
||||
from beaver.foundation.config import default_config_path, load_config
|
||||
from beaver.services.agent_service import AgentService
|
||||
from beaver.skills.learning import SkillLearningWorker, SkillLearningWorkerConfig
|
||||
|
||||
from .deps import get_agent_service
|
||||
from .schemas import WebChatRequest, WebChatResponse, WebErrorResponse, WebStatusResponse
|
||||
from .schemas import (
|
||||
WebChatFeedbackRequest,
|
||||
WebChatFeedbackResponse,
|
||||
WebChatRequest,
|
||||
WebChatResponse,
|
||||
WebErrorResponse,
|
||||
WebProviderConfigRequest,
|
||||
WebProviderConfigResponse,
|
||||
WebStatusResponse,
|
||||
)
|
||||
|
||||
try:
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
@ -50,6 +64,24 @@ except ModuleNotFoundError: # pragma: no cover - fallback for skeleton-only env
|
||||
|
||||
return decorator
|
||||
|
||||
def put(self, _path: str, **_kwargs: Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def patch(self, _path: str, **_kwargs: Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def delete(self, _path: str, **_kwargs: Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _app_lifespan(
|
||||
@ -82,9 +114,28 @@ async def _app_lifespan(
|
||||
else:
|
||||
attached_service.close()
|
||||
raise
|
||||
worker: SkillLearningWorker | None = None
|
||||
worker_task = None
|
||||
worker_config = SkillLearningWorkerConfig.from_env()
|
||||
if owns_service and worker_config.enabled:
|
||||
loaded = attached_service.create_loop().boot()
|
||||
worker = SkillLearningWorker(
|
||||
pipeline=loaded.skill_learning_pipeline, # type: ignore[arg-type]
|
||||
provider_bundle_factory=lambda: attached_service._make_provider_bundle_for_task(loaded, {}), # noqa: SLF001
|
||||
config=worker_config,
|
||||
)
|
||||
worker_task = asyncio.create_task(worker.run_forever())
|
||||
app.state.skill_learning_worker = worker
|
||||
app.state.skill_learning_worker_task = worker_task
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if worker is not None:
|
||||
worker.stop()
|
||||
if worker_task is not None:
|
||||
worker_task.cancel()
|
||||
with suppress(BaseException):
|
||||
await worker_task
|
||||
if owns_service and started:
|
||||
await attached_service.shutdown(
|
||||
timeout_seconds=shutdown_timeout_seconds,
|
||||
@ -133,6 +184,412 @@ def create_app(
|
||||
mode="running" if running else ("direct" if agent_service.has_loop else "idle"),
|
||||
)
|
||||
|
||||
@app.get("/api/status")
|
||||
async def status(request: Request) -> dict[str, Any]:
|
||||
agent_service = get_agent_service(request)
|
||||
loaded = agent_service.create_loop().boot()
|
||||
config = loaded.config
|
||||
config_path = config.config_path or default_config_path(workspace=loaded.workspace)
|
||||
|
||||
providers_status = []
|
||||
default_provider = config.resolve_provider_target().get("provider_name")
|
||||
for spec in PROVIDERS:
|
||||
provider_cfg = config.providers.get(spec.name)
|
||||
enabled = provider_cfg is not None
|
||||
api_key = provider_cfg.api_key if provider_cfg is not None else None
|
||||
api_base = provider_cfg.api_base if provider_cfg is not None else None
|
||||
if spec.is_oauth:
|
||||
has_key = enabled
|
||||
elif spec.is_local or spec.is_direct:
|
||||
has_key = bool(api_base)
|
||||
else:
|
||||
has_key = bool(api_key)
|
||||
providers_status.append(
|
||||
{
|
||||
"id": spec.name,
|
||||
"name": spec.label,
|
||||
"label": spec.label,
|
||||
"enabled": enabled,
|
||||
"active": default_provider == spec.name,
|
||||
"has_key": has_key,
|
||||
"api_key_masked": _mask_secret(api_key),
|
||||
"api_base": api_base or "",
|
||||
"default_api_base": spec.default_api_base,
|
||||
"detail": api_base or spec.default_api_base or "",
|
||||
"requires_api_key": not (spec.is_oauth or spec.is_local or spec.is_direct),
|
||||
"is_oauth": spec.is_oauth,
|
||||
"is_local": spec.is_local,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"config_path": str(config_path),
|
||||
"config_exists": config_path.exists(),
|
||||
"workspace": str(loaded.workspace),
|
||||
"workspace_exists": loaded.workspace.exists(),
|
||||
"model": config.default_model or agent_service.profile.default_model,
|
||||
"max_tokens": agent_service.profile.max_tokens,
|
||||
"temperature": agent_service.profile.temperature,
|
||||
"max_tool_iterations": agent_service.profile.max_tool_iterations,
|
||||
"providers": providers_status,
|
||||
"channels": [{"name": "web", "enabled": True}],
|
||||
"cron": {"enabled": False, "jobs": 0, "next_wake_at_ms": None},
|
||||
}
|
||||
|
||||
@app.post("/api/providers/{provider_name}/config", response_model=WebProviderConfigResponse)
|
||||
async def update_provider_config(
|
||||
provider_name: str,
|
||||
request: Request,
|
||||
payload: WebProviderConfigRequest,
|
||||
) -> WebProviderConfigResponse:
|
||||
spec = find_by_name(provider_name)
|
||||
if spec is None:
|
||||
raise HTTPException(status_code=404, detail=f"Unknown provider: {provider_name}")
|
||||
|
||||
agent_service = get_agent_service(request)
|
||||
config_path = agent_service.loader.config.config_path or default_config_path(workspace=agent_service.loader.workspace)
|
||||
raw = _read_config_json(config_path)
|
||||
providers = _ensure_dict(raw, "providers")
|
||||
agents = _ensure_dict(raw, "agents")
|
||||
defaults = _ensure_dict(agents, "defaults")
|
||||
|
||||
if not payload.enabled:
|
||||
providers.pop(spec.name, None)
|
||||
if _clean_text(defaults.get("provider")) == spec.name:
|
||||
defaults.pop("provider", None)
|
||||
else:
|
||||
current = providers.get(spec.name) if isinstance(providers.get(spec.name), dict) else {}
|
||||
provider_payload = dict(current)
|
||||
api_key = _clean_text(payload.api_key)
|
||||
api_base = _clean_text(payload.api_base)
|
||||
if api_key:
|
||||
provider_payload["apiKey"] = api_key
|
||||
elif "apiKey" not in provider_payload and "api_key" not in provider_payload:
|
||||
provider_payload.pop("apiKey", None)
|
||||
if api_base:
|
||||
provider_payload["apiBase"] = api_base
|
||||
elif spec.default_api_base and not provider_payload.get("apiBase") and not provider_payload.get("api_base"):
|
||||
provider_payload["apiBase"] = spec.default_api_base
|
||||
elif not api_base and not spec.default_api_base:
|
||||
provider_payload.pop("apiBase", None)
|
||||
if payload.request_timeout_seconds is not None:
|
||||
provider_payload["requestTimeoutSeconds"] = payload.request_timeout_seconds
|
||||
providers[spec.name] = provider_payload
|
||||
defaults["provider"] = spec.name
|
||||
model = _clean_text(payload.model)
|
||||
if model:
|
||||
defaults["model"] = model
|
||||
|
||||
_write_config_json(config_path, raw)
|
||||
_reload_agent_config(agent_service, config_path)
|
||||
return WebProviderConfigResponse(ok=True, provider=spec.name, enabled=payload.enabled)
|
||||
|
||||
@app.get("/api/sessions")
|
||||
async def list_sessions(request: Request) -> list[dict[str, Any]]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
session_manager = loaded.session_manager
|
||||
rows = session_manager.list_sessions_rich(limit=100, exclude_sources=["subagent"]) # type: ignore[union-attr]
|
||||
return [
|
||||
{
|
||||
"key": str(row.get("id")),
|
||||
"created_at": _iso_from_timestamp(row.get("started_at")),
|
||||
"updated_at": _iso_from_timestamp(row.get("last_active")),
|
||||
"path": str(row.get("id")),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
@app.post("/api/sessions/{session_id:path}")
|
||||
async def create_session(session_id: str, request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
session_manager = loaded.session_manager
|
||||
session = session_manager.get_or_create(session_id, source="web") # type: ignore[union-attr]
|
||||
return _session_detail(session_manager, session_id, session) # type: ignore[arg-type]
|
||||
|
||||
@app.get("/api/sessions/{session_id:path}/process")
|
||||
async def get_session_process(session_id: str, request: Request) -> dict[str, Any]:
|
||||
from beaver.services.process_service import SessionProcessProjector
|
||||
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
projector = SessionProcessProjector(
|
||||
loaded.session_manager,
|
||||
loaded.run_memory_store,
|
||||
)
|
||||
return projector.project(session_id)
|
||||
|
||||
@app.get("/api/sessions/{session_id:path}")
|
||||
async def get_session(session_id: str, request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
session_manager = loaded.session_manager
|
||||
session = session_manager.get_or_create(session_id, source="web") # type: ignore[union-attr]
|
||||
return _session_detail(session_manager, session_id, session) # type: ignore[arg-type]
|
||||
|
||||
@app.delete("/api/sessions/{session_id:path}")
|
||||
async def delete_session(session_id: str, request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
loaded.session_manager.end_session(session_id, "deleted") # type: ignore[union-attr]
|
||||
return {"ok": True}
|
||||
|
||||
@app.get("/api/agents")
|
||||
async def list_agents(request: Request) -> list[dict[str, Any]]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
return [_registered_agent_to_ui(agent) for agent in loaded.agent_registry.list_agents()] # type: ignore[union-attr]
|
||||
|
||||
@app.post("/api/agents")
|
||||
async def upsert_agent(request: Request, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
agent = loaded.agent_registry.upsert_agent(_agent_payload_from_ui(payload)) # type: ignore[union-attr]
|
||||
return _registered_agent_to_ui(agent)
|
||||
|
||||
@app.patch("/api/agents/{agent_id}")
|
||||
async def patch_agent(agent_id: str, request: Request, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
registry = loaded.agent_registry
|
||||
current = registry.get_agent(agent_id) # type: ignore[union-attr]
|
||||
if current is None:
|
||||
raise HTTPException(status_code=404, detail=f"Unknown agent: {agent_id}")
|
||||
merged = current.to_dict()
|
||||
merged.update(_agent_payload_from_ui(payload))
|
||||
merged["agent_id"] = agent_id
|
||||
agent = registry.upsert_agent(merged) # type: ignore[union-attr]
|
||||
return _registered_agent_to_ui(agent)
|
||||
|
||||
@app.post("/api/agents/{agent_id}/disable")
|
||||
async def disable_agent(agent_id: str, request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
try:
|
||||
agent = loaded.agent_registry.disable_agent(agent_id) # type: ignore[union-attr]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return _registered_agent_to_ui(agent)
|
||||
|
||||
@app.get("/api/skills")
|
||||
async def list_skills(request: Request) -> list[dict[str, Any]]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
skills = loaded.skills_loader.list_skills(filter_unavailable=False) # type: ignore[union-attr]
|
||||
return [
|
||||
{
|
||||
"name": record.name,
|
||||
"description": record.description,
|
||||
"source": "builtin" if record.source == "builtin" else "workspace",
|
||||
"available": loaded.skills_loader._record_available(record), # type: ignore[union-attr]
|
||||
"path": str(record.path),
|
||||
"agent_cards": [],
|
||||
}
|
||||
for record in skills
|
||||
]
|
||||
|
||||
@app.get("/api/skills/candidates")
|
||||
async def list_skill_candidates(request: Request, status: str | None = None) -> list[dict[str, Any]]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
return [item.to_dict() for item in loaded.skill_learning_pipeline.list_candidates(status=status)] # type: ignore[union-attr]
|
||||
|
||||
@app.get("/api/skills/candidates/{candidate_id}")
|
||||
async def get_skill_candidate(candidate_id: str, request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
try:
|
||||
return loaded.skill_learning_pipeline.get_candidate(candidate_id).to_dict() # type: ignore[union-attr]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
@app.post("/api/skills/candidates/{candidate_id}/draft")
|
||||
async def synthesize_skill_draft(candidate_id: str, request: Request) -> dict[str, Any]:
|
||||
agent_service = get_agent_service(request)
|
||||
loaded = agent_service.create_loop().boot()
|
||||
provider_bundle = agent_service._make_provider_bundle_for_task(loaded, {}) # noqa: SLF001
|
||||
try:
|
||||
draft = await loaded.skill_learning_pipeline.synthesize_draft( # type: ignore[union-attr]
|
||||
candidate_id,
|
||||
provider_bundle=provider_bundle,
|
||||
)
|
||||
loaded.skill_learning_pipeline.check_safety(draft.skill_name, draft.draft_id) # type: ignore[union-attr]
|
||||
await loaded.skill_learning_pipeline.evaluate_draft( # type: ignore[union-attr]
|
||||
candidate_id,
|
||||
draft.skill_name,
|
||||
draft.draft_id,
|
||||
provider_bundle=provider_bundle,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return draft.to_dict()
|
||||
|
||||
@app.post("/api/skills/candidates/{candidate_id}/regenerate")
|
||||
async def regenerate_skill_draft(candidate_id: str, request: Request) -> dict[str, Any]:
|
||||
agent_service = get_agent_service(request)
|
||||
loaded = agent_service.create_loop().boot()
|
||||
provider_bundle = agent_service._make_provider_bundle_for_task(loaded, {}) # noqa: SLF001
|
||||
try:
|
||||
draft = await loaded.skill_learning_pipeline.regenerate_draft( # type: ignore[union-attr]
|
||||
candidate_id,
|
||||
provider_bundle=provider_bundle,
|
||||
)
|
||||
loaded.skill_learning_pipeline.check_safety(draft.skill_name, draft.draft_id) # type: ignore[union-attr]
|
||||
await loaded.skill_learning_pipeline.evaluate_draft( # type: ignore[union-attr]
|
||||
candidate_id,
|
||||
draft.skill_name,
|
||||
draft.draft_id,
|
||||
provider_bundle=provider_bundle,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return draft.to_dict()
|
||||
|
||||
@app.post("/api/skills/learning/run-once")
|
||||
async def run_skill_learning_once(request: Request) -> dict[str, Any]:
|
||||
agent_service = get_agent_service(request)
|
||||
loaded = agent_service.create_loop().boot()
|
||||
worker = SkillLearningWorker(
|
||||
pipeline=loaded.skill_learning_pipeline, # type: ignore[arg-type]
|
||||
provider_bundle_factory=lambda: agent_service._make_provider_bundle_for_task(loaded, {}), # noqa: SLF001
|
||||
config=SkillLearningWorkerConfig.from_env(),
|
||||
)
|
||||
result = await worker.run_once()
|
||||
return result.to_dict()
|
||||
|
||||
@app.get("/api/skills/drafts")
|
||||
async def list_skill_drafts(request: Request) -> list[dict[str, Any]]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
results = []
|
||||
for item in loaded.skill_learning_pipeline.list_drafts(): # type: ignore[union-attr]
|
||||
safety = loaded.skill_learning_pipeline.get_safety_report(item.skill_name, item.draft_id) # type: ignore[union-attr]
|
||||
eval_report = loaded.skill_learning_pipeline.get_eval_report(item.skill_name, item.draft_id) # type: ignore[union-attr]
|
||||
results.append(
|
||||
{
|
||||
**item.to_dict(),
|
||||
"safety_report": safety.to_dict() if safety is not None else None,
|
||||
"eval_report": eval_report.to_dict() if eval_report is not None else None,
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
@app.get("/api/skills/{skill_name}/drafts/{draft_id}")
|
||||
async def get_skill_draft(skill_name: str, draft_id: str, request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
try:
|
||||
draft = loaded.skill_learning_pipeline.get_draft(skill_name, draft_id) # type: ignore[union-attr]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return {
|
||||
**draft.to_dict(),
|
||||
"reviews": [
|
||||
item.to_dict()
|
||||
for item in loaded.skill_learning_pipeline.reviews_for_draft(skill_name, draft_id) # type: ignore[union-attr]
|
||||
],
|
||||
"safety_report": (
|
||||
loaded.skill_learning_pipeline.get_safety_report(skill_name, draft_id).to_dict() # type: ignore[union-attr]
|
||||
if loaded.skill_learning_pipeline.get_safety_report(skill_name, draft_id) is not None # type: ignore[union-attr]
|
||||
else None
|
||||
),
|
||||
"eval_report": (
|
||||
loaded.skill_learning_pipeline.get_eval_report(skill_name, draft_id).to_dict() # type: ignore[union-attr]
|
||||
if loaded.skill_learning_pipeline.get_eval_report(skill_name, draft_id) is not None # type: ignore[union-attr]
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
@app.get("/api/skills/{skill_name}/drafts/{draft_id}/safety")
|
||||
async def get_skill_draft_safety(skill_name: str, draft_id: str, request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
report = loaded.skill_learning_pipeline.get_safety_report(skill_name, draft_id) # type: ignore[union-attr]
|
||||
if report is None:
|
||||
raise HTTPException(status_code=404, detail="Safety report not found")
|
||||
return report.to_dict()
|
||||
|
||||
@app.get("/api/skills/{skill_name}/drafts/{draft_id}/eval")
|
||||
async def get_skill_draft_eval(skill_name: str, draft_id: str, request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
report = loaded.skill_learning_pipeline.get_eval_report(skill_name, draft_id) # type: ignore[union-attr]
|
||||
if report is None:
|
||||
raise HTTPException(status_code=404, detail="Eval report not found")
|
||||
return report.to_dict()
|
||||
|
||||
@app.post("/api/skills/{skill_name}/drafts/{draft_id}/submit")
|
||||
async def submit_skill_draft(skill_name: str, draft_id: str, request: Request, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
try:
|
||||
review = loaded.skill_learning_pipeline.submit_review( # type: ignore[union-attr]
|
||||
skill_name,
|
||||
draft_id,
|
||||
requested_by=str((payload or {}).get("requested_by") or "web"),
|
||||
notes=str((payload or {}).get("notes") or ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return review.to_dict()
|
||||
|
||||
@app.post("/api/skills/{skill_name}/drafts/{draft_id}/approve")
|
||||
async def approve_skill_draft(skill_name: str, draft_id: str, request: Request, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
try:
|
||||
review = loaded.skill_learning_pipeline.approve( # type: ignore[union-attr]
|
||||
skill_name,
|
||||
draft_id,
|
||||
reviewer=str((payload or {}).get("reviewer") or "web"),
|
||||
notes=str((payload or {}).get("notes") or ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return review.to_dict()
|
||||
|
||||
@app.post("/api/skills/{skill_name}/drafts/{draft_id}/reject")
|
||||
async def reject_skill_draft(skill_name: str, draft_id: str, request: Request, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
try:
|
||||
review = loaded.skill_learning_pipeline.reject( # type: ignore[union-attr]
|
||||
skill_name,
|
||||
draft_id,
|
||||
reviewer=str((payload or {}).get("reviewer") or "web"),
|
||||
notes=str((payload or {}).get("notes") or ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return review.to_dict()
|
||||
|
||||
@app.post("/api/skills/{skill_name}/drafts/{draft_id}/publish")
|
||||
async def publish_skill_draft(skill_name: str, draft_id: str, request: Request, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
try:
|
||||
result = loaded.skill_learning_pipeline.publish( # type: ignore[union-attr]
|
||||
skill_name,
|
||||
draft_id,
|
||||
publisher=str((payload or {}).get("publisher") or "web"),
|
||||
notes=str((payload or {}).get("notes") or ""),
|
||||
confirm_high_risk=bool((payload or {}).get("confirm_high_risk")),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return result.to_dict()
|
||||
|
||||
@app.post("/api/skills/{skill_name}/disable")
|
||||
async def disable_skill(skill_name: str, request: Request, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
try:
|
||||
spec = loaded.skill_learning_pipeline.disable( # type: ignore[union-attr]
|
||||
skill_name,
|
||||
actor=str((payload or {}).get("actor") or "web"),
|
||||
reason=str((payload or {}).get("reason") or ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
return spec.to_dict()
|
||||
|
||||
@app.post("/api/skills/{skill_name}/rollback")
|
||||
async def rollback_skill(skill_name: str, request: Request, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
target_version = str(payload.get("target_version") or "").strip()
|
||||
if not target_version:
|
||||
raise HTTPException(status_code=400, detail="target_version is required")
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
try:
|
||||
spec = loaded.skill_learning_pipeline.rollback( # type: ignore[union-attr]
|
||||
skill_name,
|
||||
target_version,
|
||||
actor=str(payload.get("actor") or "web"),
|
||||
reason=str(payload.get("reason") or ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return spec.to_dict()
|
||||
|
||||
@app.post(
|
||||
"/api/chat",
|
||||
response_model=WebChatResponse,
|
||||
@ -191,11 +648,132 @@ def create_app(
|
||||
provider_name=result.provider_name,
|
||||
model=result.model,
|
||||
usage=result.usage,
|
||||
task_id=result.task_id,
|
||||
task_status=result.task_status,
|
||||
validation_result=result.validation_result,
|
||||
)
|
||||
|
||||
@app.post(
|
||||
"/api/chat/feedback",
|
||||
response_model=WebChatFeedbackResponse,
|
||||
responses={
|
||||
400: {"model": WebErrorResponse},
|
||||
404: {"model": WebErrorResponse},
|
||||
},
|
||||
)
|
||||
async def chat_feedback(request: Request, payload: WebChatFeedbackRequest) -> WebChatFeedbackResponse:
|
||||
agent_service = get_agent_service(request)
|
||||
try:
|
||||
result = await agent_service.submit_feedback(
|
||||
session_id=payload.session_id,
|
||||
run_id=payload.run_id,
|
||||
feedback_type=payload.feedback_type,
|
||||
comment=payload.comment,
|
||||
)
|
||||
except ValueError as exc:
|
||||
detail = str(exc)
|
||||
status_code = 404 if "No internal task" in detail else 400
|
||||
raise HTTPException(status_code=status_code, detail=detail) from exc
|
||||
|
||||
return WebChatFeedbackResponse(**result)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def _session_detail(session_manager: Any, session_id: str, session: dict[str, Any]) -> dict[str, Any]:
|
||||
messages = []
|
||||
for event in session_manager.get_messages_as_conversation(session_id):
|
||||
role = event.get("role")
|
||||
if role not in {"user", "assistant"}:
|
||||
continue
|
||||
messages.append(
|
||||
{
|
||||
"role": role,
|
||||
"content": event.get("content") or "",
|
||||
"timestamp": _iso_from_timestamp(event.get("timestamp")),
|
||||
"run_id": event.get("run_id"),
|
||||
"task_id": event.get("task_id"),
|
||||
"task_status": event.get("task_status"),
|
||||
"validation_status": event.get("validation_status"),
|
||||
"feedback_state": event.get("feedback_state"),
|
||||
"feedback_error": event.get("feedback_error"),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"key": session_id,
|
||||
"messages": messages,
|
||||
"created_at": _iso_from_timestamp(session.get("started_at")),
|
||||
"updated_at": _iso_from_timestamp(session.get("last_active")),
|
||||
}
|
||||
|
||||
|
||||
def _iso_from_timestamp(value: Any) -> str:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
if value in (None, ""):
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
try:
|
||||
return datetime.fromtimestamp(float(value), tz=timezone.utc).isoformat()
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
|
||||
|
||||
def _registered_agent_to_ui(agent: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"id": agent.agent_id,
|
||||
"name": agent.display_name or agent.name,
|
||||
"description": agent.description,
|
||||
"source": agent.source if agent.source in {"workspace", "skill", "builtin"} else "workspace",
|
||||
"kind": "specialist",
|
||||
"protocol": None,
|
||||
"endpoint": None,
|
||||
"base_url": None,
|
||||
"card_url": None,
|
||||
"auth_env": None,
|
||||
"auth_mode": "none",
|
||||
"auth_audience": None,
|
||||
"auth_scopes": [],
|
||||
"tags": list(agent.tags),
|
||||
"aliases": [agent.name],
|
||||
"metadata": {
|
||||
**dict(agent.metadata),
|
||||
"role": agent.role,
|
||||
"capabilities": list(agent.capabilities),
|
||||
"skill_names": list(agent.skill_names),
|
||||
"tool_hints": list(agent.tool_hints),
|
||||
"priority": agent.priority,
|
||||
"status": agent.status,
|
||||
},
|
||||
"support_streaming": False,
|
||||
}
|
||||
|
||||
|
||||
def _agent_payload_from_ui(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
metadata = dict(payload.get("metadata") or {})
|
||||
capabilities = payload.get("capabilities")
|
||||
if capabilities is None and isinstance(metadata.get("capabilities"), list):
|
||||
capabilities = metadata.get("capabilities")
|
||||
role = payload.get("role") or metadata.get("role") or payload.get("kind") or ""
|
||||
return {
|
||||
"agent_id": payload.get("agent_id") or payload.get("id") or payload.get("name"),
|
||||
"name": payload.get("name") or payload.get("id"),
|
||||
"display_name": payload.get("display_name") or payload.get("name") or payload.get("id"),
|
||||
"role": role,
|
||||
"description": payload.get("description") or "",
|
||||
"system_prompt": payload.get("system_prompt") or metadata.get("system_prompt") or "",
|
||||
"capabilities": capabilities or [],
|
||||
"skill_names": payload.get("skill_names") or metadata.get("skill_names") or [],
|
||||
"tool_hints": payload.get("tool_hints") or metadata.get("tool_hints") or [],
|
||||
"model": payload.get("model") or metadata.get("model"),
|
||||
"provider_name": payload.get("provider_name") or metadata.get("provider_name"),
|
||||
"tags": payload.get("tags") or [],
|
||||
"priority": payload.get("priority") or metadata.get("priority") or 0,
|
||||
"status": payload.get("status") or ("active" if payload.get("enabled", True) else "disabled"),
|
||||
"source": payload.get("source") or "workspace",
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
|
||||
def _model_dump(value: Any) -> dict[str, Any] | None:
|
||||
"""兼容 Pydantic v1/v2 的最小导出辅助。"""
|
||||
|
||||
@ -206,3 +784,52 @@ def _model_dump(value: Any) -> dict[str, Any] | None:
|
||||
if hasattr(value, "dict"):
|
||||
return value.dict(exclude_none=True)
|
||||
return dict(value)
|
||||
|
||||
|
||||
def _clean_text(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def _mask_secret(value: str | None) -> str:
|
||||
secret = _clean_text(value)
|
||||
if not secret:
|
||||
return ""
|
||||
if len(secret) <= 8:
|
||||
return "••••"
|
||||
return f"{secret[:4]}••••{secret[-4:]}"
|
||||
|
||||
|
||||
def _read_config_json(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"Config must be a JSON object: {path}")
|
||||
return data
|
||||
|
||||
|
||||
def _ensure_dict(parent: dict[str, Any], key: str) -> dict[str, Any]:
|
||||
value = parent.get(key)
|
||||
if not isinstance(value, dict):
|
||||
value = {}
|
||||
parent[key] = value
|
||||
return value
|
||||
|
||||
|
||||
def _write_config_json(path: Path, data: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = path.with_name(f"{path.name}.tmp")
|
||||
tmp_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
tmp_path.replace(path)
|
||||
|
||||
|
||||
def _reload_agent_config(agent_service: AgentService, config_path: Path) -> None:
|
||||
config = load_config(config_path=config_path)
|
||||
agent_service.loader.config = config
|
||||
loop = getattr(agent_service, "_loop", None)
|
||||
loaded = getattr(loop, "loaded", None) if loop is not None else None
|
||||
if loaded is not None:
|
||||
loaded.config = config
|
||||
|
||||
Reference in New Issue
Block a user