Skip to content
Merged

Dev #13

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
# ── 基础设施 (Infrastructure) ────────────────────────────────────────
# NATS_URL=nats://127.0.0.1:4222
# REDIS_ADDR=127.0.0.1:6379
# REDIS_URL=redis://127.0.0.1:6379/0
# AGENTHUB_DISTRIBUTED_CACHE_ENABLED=true
# AGENTHUB_MEMORY_EVENTS_ENABLED=true
# DATABASE_DSN=postgres://agenthub:agenthub@localhost:5432/agenthub?sslmode=disable

# ── 安全 (Security) ──────────────────────────────────────────────────
Expand Down Expand Up @@ -35,6 +38,13 @@
# OPENAI_COMPATIBLE_BASE_URL=http://127.0.0.1:11434/v1
# OPENAI_COMPATIBLE_API_KEY=not-needed

# 国产模型精确 token 统计(指向本地 tokenizer.json 或其目录)
# AGENTHUB_TOKENIZER_QWEN_PATH=/models/qwen/tokenizer.json
# AGENTHUB_TOKENIZER_DEEPSEEK_PATH=/models/deepseek/tokenizer.json
# AGENTHUB_TOKENIZER_DOUBAO_PATH=/models/doubao/tokenizer.json
# AGENTHUB_TOKENIZER_ZHIPU_PATH=/models/glm/tokenizer.json
# AGENTHUB_TOKENIZER_MOONSHOT_PATH=/models/kimi/tokenizer.json

# vLLM 本地推理(优先级高于 OPENAI_COMPATIBLE_*)
# 启用 vLLM 服务:docker compose -f deploy/docker-compose.platform.yml --profile vllm up
# 调用方式:model 字段传 vllm-<hf-model-id>,如 vllm-Qwen/Qwen2.5-7B-Instruct
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ __pycache__/
# ─── Frontend ─────────────────────────────────────────────────────
frontend/node_modules/
frontend/.next/
frontend/.next-*/
frontend/out/
frontend/.env*.local

Expand Down
133 changes: 103 additions & 30 deletions app/api/admin/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@

from __future__ import annotations

import json

from fastapi import APIRouter, Depends, HTTPException

from app.db.init_db import now
from app.db.session import aexecute
from app.schemas.common import AgentRouteActiveRequest, AgentRouteRequest
from app.schemas.dag import DAGConfig
from app.schemas.common import AgentRouteActiveRequest
from app.schemas.workflow import AgentRouteRequest, WorkflowDraftRequest, WorkflowValidationRequest
from app.services.agent_route_service import agent_route_service
from app.services.auth_service import get_current_user, require_admin, write_audit
from app.services.template_engine import template_engine
from app.services.context_summary_cache import context_summary_cache
from app.services.workflow_contract import validate_workflow_contract
from app.services.workflow_draft_service import workflow_draft_service
from app.services.workflow_errors import WorkflowVersionConflict

router = APIRouter(prefix="/workflows", tags=["admin-workflows"])

Expand All @@ -44,6 +44,59 @@ async def list_workflows(user: dict = Depends(get_current_user)) -> list[dict]:
return await agent_route_service.list_routes(_uid(user))


@router.post("/validate")
async def validate_workflow(data: WorkflowValidationRequest, user: dict = Depends(get_current_user)) -> dict:
"""Validate and normalize an editor graph without persisting it."""
require_admin(user)
result = validate_workflow_contract(data.nodes, data.edges, schema_version=data.schemaVersion)
return result.model_dump(mode="json")


@router.get("/drafts")
async def list_workflow_drafts(user: dict = Depends(get_current_user)) -> list[dict]:
require_admin(user)
return await workflow_draft_service.list_drafts(_uid(user))


@router.get("/drafts/{draft_key}")
async def get_workflow_draft(draft_key: str, user: dict = Depends(get_current_user)) -> dict:
require_admin(user)
draft = await workflow_draft_service.get_draft(_uid(user), draft_key)
if not draft:
raise HTTPException(status_code=404, detail="Workflow draft not found")
return draft


@router.put("/drafts/{draft_key}")
async def save_workflow_draft(
draft_key: str, data: WorkflowDraftRequest, user: dict = Depends(get_current_user),
) -> dict:
require_admin(user)
try:
return await workflow_draft_service.save_draft(_uid(user), draft_key, data)
except WorkflowVersionConflict as exc:
raise _version_conflict(exc, "workflow_draft_version_conflict") from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc


@router.delete("/drafts/{draft_key}")
async def delete_workflow_draft(draft_key: str, user: dict = Depends(get_current_user)) -> dict:
require_admin(user)
if not await workflow_draft_service.delete_draft(_uid(user), draft_key):
raise HTTPException(status_code=404, detail="Workflow draft not found")
return {"status": "success", "draftKey": draft_key}


@router.get("/{route_id}")
async def get_workflow(route_id: int, user: dict = Depends(get_current_user)) -> dict:
require_admin(user)
route = await agent_route_service.get_route(route_id, _uid(user))
if not route:
raise HTTPException(status_code=404, detail="Workflow not found")
return route


# ── CREATE ────────────────────────────────────────────────────────────────


Expand All @@ -54,7 +107,15 @@ async def create_workflow(data: AgentRouteRequest, user: dict = Depends(get_curr
uid = _uid(user)
try:
route = await agent_route_service.create_route(
uid, data.name, data.description, data.triggerKeywords, data.nodes, data.isDefault,
uid,
data.name,
data.description,
data.triggerKeywords,
data.nodes,
edges=data.edges,
is_default=data.isDefault,
active=data.active,
schema_version=data.schemaVersion,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
Expand All @@ -75,36 +136,47 @@ async def update_workflow(route_id: int, data: AgentRouteRequest, user: dict = D
require_admin(user)
uid = _uid(user)

existing = await agent_route_service.get_route(route_id, uid)
if not existing:
raise HTTPException(status_code=404, detail="Workflow not found")

dag = DAGConfig(total=len(data.nodes), completed=0, nodes=data.nodes)
template_engine.validate(dag)

if data.isDefault:
await aexecute("UPDATE agent_routes SET is_default = 0 WHERE user_id = $1", uid)
await aexecute(
"UPDATE agent_routes SET name = $1, description = $2, trigger_keywords = $3, "
"nodes_json = $4, is_default = $5, updated_at = $6 WHERE id = $7 AND user_id = $8",
data.name,
data.description,
json.dumps(data.triggerKeywords, ensure_ascii=False),
json.dumps(data.nodes, ensure_ascii=False),
1 if data.isDefault else 0,
now(),
route_id,
uid,
)

route = await agent_route_service.get_route(route_id, uid)
if data.version < 1:
raise HTTPException(status_code=428, detail="Workflow version is required for updates")
try:
route = await agent_route_service.update_route(
route_id,
uid,
name=data.name,
description=data.description,
trigger_keywords=data.triggerKeywords,
nodes=data.nodes,
edges=data.edges,
is_default=data.isDefault,
active=data.active,
schema_version=data.schemaVersion,
expected_version=data.version,
)
except WorkflowVersionConflict as exc:
raise _version_conflict(exc, "workflow_version_conflict") from exc
except LookupError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
audit_id = write_audit(
user["id"], "admin", "workflow_update", "L2", "approve",
{"routeId": route_id, "name": data.name},
)
return {"status": "success", "route": route, "auditId": audit_id}


def _version_conflict(exc: WorkflowVersionConflict, code: str) -> HTTPException:
return HTTPException(
status_code=409,
detail={
"code": code,
"message": str(exc),
"expectedVersion": exc.expected_version,
"currentVersion": exc.current_version,
},
)


# ── DELETE ────────────────────────────────────────────────────────────────


Expand All @@ -119,6 +191,7 @@ async def delete_workflow(route_id: int, user: dict = Depends(get_current_user))
raise HTTPException(status_code=404, detail="Workflow not found")

await aexecute("DELETE FROM agent_routes WHERE id = $1 AND user_id = $2", route_id, uid)
context_summary_cache.invalidate("route", uid)

audit_id = write_audit(
user["id"], "admin", "workflow_delete", "L2", "approve",
Expand Down
9 changes: 9 additions & 0 deletions app/api/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ async def create_agent(data: AgentCreateRequest, user: dict = Depends(get_curren
)
except Exception as exc:
raise HTTPException(status_code=400, detail="Agent 已存在或参数无效") from exc
from app.services.context_summary_cache import context_summary_cache
context_summary_cache.invalidate("agent", str(user["id"]))
context_summary_cache.invalidate("agent", "shared")
audit_id = write_audit(user["id"], agent_id, "agent_create", "L2", "approve", {**data.model_dump(), "apiKey": "***" if data.apiKey else ""})
return {"status": "success", "agentId": agent_id, "auditId": audit_id}

Expand All @@ -319,6 +322,9 @@ async def delete_agent(agent_id: str, user: dict = Depends(get_current_user)) ->
)
if not deleted:
raise HTTPException(status_code=404, detail="Agent 不存在")
from app.services.context_summary_cache import context_summary_cache
context_summary_cache.invalidate("agent", str(user["id"]))
context_summary_cache.invalidate("agent", "shared")
audit_id = write_audit(user["id"], agent_id, "agent_delete", "L2", "approve", {"agentId": agent_id})
return {"status": "success", "agentId": agent_id, "auditId": audit_id}

Expand Down Expand Up @@ -368,6 +374,9 @@ async def update_agent(agent_id: str, data: AgentUpdateRequest, user: dict = Dep
avatar_url, tags_json, data.baseUrl.strip(), config_json, agent_id, user["id"],
)

from app.services.context_summary_cache import context_summary_cache
context_summary_cache.invalidate("agent", str(user["id"]))
context_summary_cache.invalidate("agent", "shared")
audit_id = write_audit(user["id"], agent_id, "agent_update", "L2", "approve", {**data.model_dump(), "apiKey": "***" if data.apiKey else ""})
return {"status": "success", "agentId": agent_id, "auditId": audit_id}

Expand Down
Loading
Loading