autojanet/intake/main.py
Zoë be03d042ad
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: bucket name is 'review' not 'in review'; add concurrency limits; intake service scaffold
2026-05-30 20:21:37 -07:00

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}",
)