134 lines
3.8 KiB
Python
134 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
AutoJanet Intake Service
|
|
|
|
Accepts task submissions and creates Vikunja tasks with the appropriate
|
|
agent label so the dispatcher picks them up automatically.
|
|
|
|
POST /task
|
|
{
|
|
"title": "Add dark mode to the dashboard",
|
|
"description": "...", # optional
|
|
"role": "coder" # optional, defaults to "pm" (PM decomposes)
|
|
}
|
|
|
|
GET /health
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
import httpx
|
|
from fastapi import FastAPI, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", stream=sys.stdout)
|
|
log = logging.getLogger("intake")
|
|
|
|
VIKUNJA_BASE_URL = os.environ["VIKUNJA_BASE_URL"]
|
|
VIKUNJA_PM_TOKEN = os.environ["VIKUNJA_PM_TOKEN"]
|
|
VIKUNJA_PROJECT_ID = int(os.environ.get("VIKUNJA_PROJECT_ID", "78"))
|
|
VIKUNJA_TODO_BUCKET_ID = int(os.environ.get("VIKUNJA_TODO_BUCKET_ID", "116"))
|
|
VIKUNJA_VIEW_ID = int(os.environ.get("VIKUNJA_VIEW_ID", "114"))
|
|
|
|
# Label IDs for agent roles (from Vikunja)
|
|
ROLE_LABEL_IDS = {
|
|
"pm": 1,
|
|
"coder": 3,
|
|
"code-reviewer": 4,
|
|
"test-engineer": 5,
|
|
"devsecops": 6,
|
|
"secops": 7,
|
|
"sre": 8,
|
|
"kubernetes-pilot": 9,
|
|
"linux-admin": 10,
|
|
"systems-engineer": 11,
|
|
"networking": 12,
|
|
"dba": 13,
|
|
"prometheus-expert": 14,
|
|
"tofu-engineer": 15,
|
|
"release-manager": 16,
|
|
"doc-updater": 17,
|
|
"doc-writer": 18,
|
|
"technical-writer": 19,
|
|
"cost-optimizer": 20,
|
|
}
|
|
|
|
app = FastAPI(title="AutoJanet Intake", version="1.0.0")
|
|
|
|
VIKUNJA_HEADERS = {
|
|
"Authorization": f"Bearer {VIKUNJA_PM_TOKEN}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
|
|
class TaskRequest(BaseModel):
|
|
title: str
|
|
description: str = ""
|
|
role: str = "pm"
|
|
|
|
|
|
class TaskResponse(BaseModel):
|
|
task_id: int
|
|
title: str
|
|
role: str
|
|
url: str
|
|
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.post("/task", response_model=TaskResponse)
|
|
def submit_task(req: TaskRequest):
|
|
role = req.role.lower()
|
|
if role not in ROLE_LABEL_IDS:
|
|
raise HTTPException(status_code=400, detail=f"Unknown role '{role}'. Valid: {sorted(ROLE_LABEL_IDS)}")
|
|
|
|
label_id = ROLE_LABEL_IDS[role]
|
|
|
|
# Create the task
|
|
with httpx.Client(timeout=15) as client:
|
|
resp = client.put(
|
|
f"{VIKUNJA_BASE_URL}/api/v1/projects/{VIKUNJA_PROJECT_ID}/tasks",
|
|
headers=VIKUNJA_HEADERS,
|
|
json={
|
|
"title": req.title,
|
|
"description": req.description,
|
|
"labels": [{"id": label_id}],
|
|
},
|
|
)
|
|
if resp.status_code not in (200, 201):
|
|
log.error("Vikunja task creation failed: %s %s", resp.status_code, resp.text)
|
|
raise HTTPException(status_code=502, detail="Failed to create Vikunja task")
|
|
|
|
task = resp.json()
|
|
task_id = task["id"]
|
|
log.info("Created task %d: %s (role=%s)", task_id, req.title, role)
|
|
|
|
# Move to Todo bucket
|
|
bucket_resp = client.post(
|
|
f"{VIKUNJA_BASE_URL}/api/v1/projects/{VIKUNJA_PROJECT_ID}/views/{VIKUNJA_VIEW_ID}/buckets/{VIKUNJA_TODO_BUCKET_ID}/tasks",
|
|
headers=VIKUNJA_HEADERS,
|
|
json={"task_id": task_id},
|
|
)
|
|
if bucket_resp.status_code not in (200, 201):
|
|
log.warning("Failed to move task %d to Todo bucket: %s", task_id, bucket_resp.text)
|
|
|
|
# Set the label explicitly (belt and suspenders)
|
|
label_resp = client.put(
|
|
f"{VIKUNJA_BASE_URL}/api/v1/tasks/{task_id}/labels",
|
|
headers=VIKUNJA_HEADERS,
|
|
json={"label_id": label_id},
|
|
)
|
|
if label_resp.status_code not in (200, 201):
|
|
log.warning("Failed to set label on task %d: %s", task_id, label_resp.text)
|
|
|
|
return TaskResponse(
|
|
task_id=task_id,
|
|
title=req.title,
|
|
role=role,
|
|
url=f"https://tasks.ctz.fyi/projects/{VIKUNJA_PROJECT_ID}/tasks/{task_id}",
|
|
)
|