feat: Jon Snow Phase 2 — FastAPI orchestrator with LiteLLM brain
OpenAI-compatible API at :8900. Intent classifier routes status queries to FAST_MODEL (Ollama), task submissions to Plane, planning to SMART_MODEL. Reads agent-os logs for status context. Phase 3: approval gate + execution. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+112
@@ -0,0 +1,112 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger("jon-snow.tools")
|
||||
|
||||
PLANE_URL = os.getenv("PLANE_URL", "http://172.27.40.3:8095")
|
||||
PLANE_WORKSPACE = os.getenv("PLANE_WORKSPACE", "nxm")
|
||||
PLANE_API_KEY = os.getenv("PLANE_API_KEY", "")
|
||||
|
||||
AGENT_FULL_NAMES = [
|
||||
"hodor-gateway", "bran-changelog", "varys-monitor", "sam-research",
|
||||
"raven-notify", "qyburn-coder", "citadel-mcp", "jon-snow",
|
||||
]
|
||||
|
||||
SHORT_TO_FULL = {
|
||||
"hodor": "hodor-gateway",
|
||||
"bran": "bran-changelog",
|
||||
"varys": "varys-monitor",
|
||||
"sam": "sam-research",
|
||||
"raven": "raven-notify",
|
||||
"qyburn": "qyburn-coder",
|
||||
"citadel": "citadel-mcp",
|
||||
"jon": "jon-snow",
|
||||
}
|
||||
|
||||
|
||||
def get_all_agent_status(agent_os_dir: Path) -> str:
|
||||
logs_dir = agent_os_dir / "logs"
|
||||
if not logs_dir.exists():
|
||||
return "Agent log directory not found."
|
||||
|
||||
lines = []
|
||||
for full_name in AGENT_FULL_NAMES:
|
||||
log_file = logs_dir / full_name / "last-run.json"
|
||||
if log_file.exists():
|
||||
try:
|
||||
data = json.loads(log_file.read_text())
|
||||
ts = data.get("timestamp", "unknown")[:19].replace("T", " ")
|
||||
status = data.get("status", "unknown")
|
||||
result = data.get("result", "")[:120]
|
||||
icon = "OK" if status == "success" else "FAIL"
|
||||
lines.append(f"- **{full_name}**: [{icon}] {ts} UTC — {result}")
|
||||
except Exception as e:
|
||||
lines.append(f"- **{full_name}**: error reading log ({e})")
|
||||
else:
|
||||
lines.append(f"- **{full_name}**: no log (never run or not yet deployed)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def get_agent_output(sites_dir: Path, agent_name: str) -> str | None:
|
||||
short_name = agent_name if agent_name in SHORT_TO_FULL else next(
|
||||
(k for k, v in SHORT_TO_FULL.items() if v == agent_name), agent_name
|
||||
)
|
||||
output_file = sites_dir / short_name / "last-output.md"
|
||||
if not output_file.exists():
|
||||
return None
|
||||
content = output_file.read_text()
|
||||
return content[:3000]
|
||||
|
||||
|
||||
async def get_plane_projects() -> list[dict]:
|
||||
headers = {"X-Api-Key": PLANE_API_KEY}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
f"{PLANE_URL}/api/v1/workspaces/{PLANE_WORKSPACE}/projects/",
|
||||
headers=headers, timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json().get("results", [])
|
||||
|
||||
|
||||
def _resolve_project(projects: list[dict], hint: str | None) -> dict:
|
||||
if hint:
|
||||
hint_lower = hint.lower()
|
||||
for p in projects:
|
||||
if hint_lower in p["name"].lower():
|
||||
return p
|
||||
# Default: Agent Ecosystem
|
||||
for p in projects:
|
||||
if "agent" in p["name"].lower():
|
||||
return p
|
||||
return projects[0]
|
||||
|
||||
|
||||
async def create_plane_issue(title: str, project_hint: str | None = None) -> dict:
|
||||
projects = await get_plane_projects()
|
||||
if not projects:
|
||||
raise ValueError("No Plane projects found")
|
||||
|
||||
project = _resolve_project(projects, project_hint)
|
||||
|
||||
headers = {"X-Api-Key": PLANE_API_KEY, "Content-Type": "application/json"}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"{PLANE_URL}/api/v1/workspaces/{PLANE_WORKSPACE}/projects/{project['id']}/issues/",
|
||||
headers=headers,
|
||||
json={"name": title[:255], "priority": "none"},
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
issue = resp.json()
|
||||
|
||||
return {
|
||||
"sequence_id": issue.get("sequence_id", "?"),
|
||||
"title": title[:80],
|
||||
"project": project["name"],
|
||||
}
|
||||
Reference in New Issue
Block a user