fix: чистый backend/ из acc-server
This commit is contained in:
parent
f260a63302
commit
55743703b0
13 changed files with 3 additions and 258 deletions
|
|
@ -1,252 +0,0 @@
|
|||
"""Agent Control Center — Control Plane API (FastAPI)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
app = FastAPI(title="Agent Control Center", version="0.1.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
HERMES_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
|
||||
MEMORY_DIR = HERMES_HOME / "acc-memory"
|
||||
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
WORKER_PROFILES = ["worker-code", "worker-fast", "worker-research", "worker-review"]
|
||||
|
||||
runs_db: dict[str, dict[str, Any]] = {}
|
||||
projects_db: list[dict[str, Any]] = [
|
||||
{"id": "proj-1", "key": "ACC", "name": "Agent Control Center", "status": "active"},
|
||||
{"id": "proj-2", "key": "OPS", "name": "Operations", "status": "active"},
|
||||
]
|
||||
|
||||
UI_BUILD_DIR = Path(__file__).resolve().parents[2] / "frontend" / "dist"
|
||||
if UI_BUILD_DIR.exists():
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
app.mount("/ui", StaticFiles(directory=UI_BUILD_DIR, html=True), name="ui")
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────
|
||||
|
||||
def now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def hermes_memory_path() -> Path:
|
||||
return MEMORY_DIR / "MEMORY.md"
|
||||
|
||||
|
||||
def read_memory() -> str:
|
||||
path = hermes_memory_path()
|
||||
if path.exists():
|
||||
return path.read_text(encoding="utf-8")
|
||||
return ""
|
||||
|
||||
|
||||
def write_memory(content: str) -> None:
|
||||
hermes_memory_path().write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
# ── Models ───────────────────────────────────────────────
|
||||
|
||||
class RunCreate(BaseModel):
|
||||
profile: str = Field(..., description="Worker profile, e.g. worker-code")
|
||||
prompt: str = Field(..., min_length=1)
|
||||
context: str | None = None
|
||||
|
||||
|
||||
class RunOut(BaseModel):
|
||||
id: str
|
||||
profile: str
|
||||
prompt: str
|
||||
status: str
|
||||
created_at: str
|
||||
updated_at: str
|
||||
output: str | None = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class HealthOut(BaseModel):
|
||||
profile: str
|
||||
current_model: str | None
|
||||
provider: str | None
|
||||
status: str
|
||||
latency_ms: int | None = None
|
||||
|
||||
|
||||
class MemoryPut(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
# ── Endpoints: runs ──────────────────────────────────────
|
||||
|
||||
@app.post("/api/v1/runs", response_model=RunOut)
|
||||
def create_run(payload: RunCreate) -> dict[str, Any]:
|
||||
if payload.profile not in WORKER_PROFILES:
|
||||
raise HTTPException(status_code=422, detail=f"Unknown profile: {payload.profile}")
|
||||
|
||||
run_id = str(uuid.uuid4())
|
||||
run = {
|
||||
"id": run_id,
|
||||
"profile": payload.profile,
|
||||
"prompt": payload.prompt,
|
||||
"status": "queued",
|
||||
"created_at": now_iso(),
|
||||
"updated_at": now_iso(),
|
||||
"output": None,
|
||||
"error": None,
|
||||
}
|
||||
runs_db[run_id] = run
|
||||
|
||||
# Dispatch via hermes chat -q (fire-and-forget background-friendly sync here)
|
||||
command = ["hermes", "-p", payload.profile, "chat", "-q", payload.prompt]
|
||||
try:
|
||||
run["status"] = "running"
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
run["output"] = result.stdout.strip() or "(empty response)"
|
||||
if result.returncode != 0:
|
||||
run["status"] = "failed"
|
||||
run["error"] = result.stderr.strip() or "hermes exit non-zero"
|
||||
else:
|
||||
run["status"] = "succeeded"
|
||||
except subprocess.TimeoutExpired:
|
||||
run["status"] = "failed"
|
||||
run["error"] = "hermes timeout after 300s"
|
||||
except Exception as exc:
|
||||
run["status"] = "failed"
|
||||
run["error"] = f"dispatch error: {exc}"
|
||||
run["updated_at"] = now_iso()
|
||||
return run
|
||||
|
||||
|
||||
@app.get("/api/v1/runs")
|
||||
def list_runs() -> dict[str, Any]:
|
||||
return {"data": list(runs_db.values()), "meta": {"request_id": str(uuid.uuid4())}}
|
||||
|
||||
|
||||
@app.get("/api/v1/runs/{run_id}")
|
||||
def get_run(run_id: str) -> dict[str, Any]:
|
||||
run = runs_db.get(run_id)
|
||||
if not run:
|
||||
raise HTTPException(status_code=404, detail="Run not found")
|
||||
return {"data": run, "meta": {"request_id": str(uuid.uuid4())}}
|
||||
|
||||
|
||||
@app.delete("/api/v1/runs/{run_id}")
|
||||
def cancel_run(run_id: str) -> dict[str, Any]:
|
||||
run = runs_db.get(run_id)
|
||||
if not run:
|
||||
raise HTTPException(status_code=404, detail="Run not found")
|
||||
if run["status"] in ("succeeded", "failed", "cancelled"):
|
||||
raise HTTPException(status_code=409, detail=f"Run already terminal: {run['status']}")
|
||||
run["status"] = "cancelled"
|
||||
run["updated_at"] = now_iso()
|
||||
return {"data": run, "meta": {"request_id": str(uuid.uuid4())}}
|
||||
|
||||
|
||||
# ── Endpoints: agents / health ───────────────────────────
|
||||
|
||||
@app.post("/api/v1/agents/health")
|
||||
def agents_health() -> dict[str, Any]:
|
||||
health = []
|
||||
for profile in WORKER_PROFILES:
|
||||
current_model = None
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["hermes", "config", "get", "model.default", "-p", profile],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
current_model = r.stdout.strip() or None
|
||||
except Exception:
|
||||
current_model = None
|
||||
|
||||
provider = current_model.split("/")[0] if current_model else None
|
||||
status = "unknown"
|
||||
latency_ms = None
|
||||
try:
|
||||
start = datetime.now(timezone.utc)
|
||||
r = subprocess.run(
|
||||
["hermes", "-p", profile, "chat", "-q", "hi"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
latency_ms = int((datetime.now(timezone.utc) - start).total_seconds() * 1000)
|
||||
status = "ok" if r.returncode == 0 else "error"
|
||||
except subprocess.TimeoutExpired:
|
||||
status = "timeout"
|
||||
latency_ms = 15000
|
||||
except Exception:
|
||||
status = "offline"
|
||||
|
||||
health.append(
|
||||
{
|
||||
"profile": profile,
|
||||
"current_model": current_model,
|
||||
"provider": provider,
|
||||
"status": status,
|
||||
"latency_ms": latency_ms,
|
||||
}
|
||||
)
|
||||
return {"data": health, "meta": {"request_id": str(uuid.uuid4())}}
|
||||
|
||||
|
||||
# ── Endpoints: projects ──────────────────────────────────
|
||||
|
||||
@app.get("/api/v1/projects")
|
||||
def list_projects() -> dict[str, Any]:
|
||||
return {"data": projects_db, "meta": {"request_id": str(uuid.uuid4())}}
|
||||
|
||||
|
||||
# ── Endpoints: memory ────────────────────────────────────
|
||||
|
||||
@app.get("/api/v1/memory/{key}")
|
||||
def get_memory(key: str) -> dict[str, Any]:
|
||||
if key != "global":
|
||||
raise HTTPException(status_code=404, detail="Only key='global' is supported in MVP")
|
||||
return {
|
||||
"data": {"key": key, "content": read_memory()},
|
||||
"meta": {"request_id": str(uuid.uuid4())},
|
||||
}
|
||||
|
||||
|
||||
@app.put("/api/v1/memory/{key}")
|
||||
def put_memory(key: str, payload: MemoryPut) -> dict[str, Any]:
|
||||
if key != "global":
|
||||
raise HTTPException(status_code=404, detail="Only key='global' is supported in MVP")
|
||||
write_memory(payload.content)
|
||||
return {
|
||||
"data": {"key": key, "content": payload.content, "updated_at": now_iso()},
|
||||
"meta": {"request_id": str(uuid.uuid4())},
|
||||
}
|
||||
|
||||
|
||||
# ── Root / docs ──────────────────────────────────────────
|
||||
|
||||
@app.get("/api/v1/healthz")
|
||||
def healthz() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
fastapi>=0.110.0
|
||||
uvicorn[standard]>=0.29.0
|
||||
pydantic>=2.6.0
|
||||
fastapi>=0.110
|
||||
uvicorn[standard]>=0.27
|
||||
pydantic>=2.6
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
fastapi>=0.110
|
||||
uvicorn[standard]>=0.27
|
||||
pydantic>=2.6
|
||||
Loading…
Reference in a new issue