feat: initial TaskPulse release
AI Agent 定时任务跟踪、监控、状态看板系统。 - FastAPI + Vue3 单体应用 - AI Agent 一键接入(批量注册 API) - 暗色主题 Dashboard - 后台调度器 + 超时告警 - 飞书/邮件/Webhook 多渠道通知 - Base URL 可配置 - Gunicorn 生产部署
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""API __init__ + shared dependencies."""
|
||||
|
||||
from fastapi import Header, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Agent
|
||||
from app.services import AgentService
|
||||
|
||||
|
||||
async def verify_api_key(
|
||||
authorization: str = Header("", alias="Authorization"),
|
||||
db: AsyncSession = None, # injected via Depends in router
|
||||
) -> Agent:
|
||||
"""Dependency: verify API Key in Authorization header."""
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
|
||||
api_key = authorization[7:]
|
||||
svc = AgentService(db)
|
||||
agent = await svc.get_by_api_key(api_key)
|
||||
if agent is None:
|
||||
raise HTTPException(status_code=401, detail="Invalid API Key")
|
||||
if agent.status != "active":
|
||||
raise HTTPException(status_code=403, detail="Agent is inactive")
|
||||
return agent
|
||||
@@ -0,0 +1,87 @@
|
||||
"""API router — Agent management."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.schemas import AgentBatchRegister, AgentBatchRegisterResult, AgentCreate, AgentHeartbeat, AgentOut, AgentUpdate
|
||||
from app.services import AgentService, TaskService
|
||||
|
||||
router = APIRouter(prefix="/api/agents", tags=["agents"])
|
||||
|
||||
|
||||
@router.post("", response_model=AgentOut, status_code=201)
|
||||
async def register_agent(body: AgentCreate, db: AsyncSession = Depends(get_db)):
|
||||
svc = AgentService(db)
|
||||
agent = await svc.create(name=body.name, description=body.description)
|
||||
task_count = await svc.get_task_count(agent.id)
|
||||
return AgentOut(**{**agent.__dict__, "task_count": task_count})
|
||||
|
||||
|
||||
@router.get("", response_model=list[AgentOut])
|
||||
async def list_agents(db: AsyncSession = Depends(get_db)):
|
||||
svc = AgentService(db)
|
||||
agents = await svc.list_all()
|
||||
result = []
|
||||
for a in agents:
|
||||
cnt = await svc.get_task_count(a.id)
|
||||
result.append(AgentOut(**{**a.__dict__, "task_count": cnt}))
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{agent_id}", response_model=AgentOut)
|
||||
async def get_agent(agent_id: int, db: AsyncSession = Depends(get_db)):
|
||||
svc = AgentService(db)
|
||||
agent = await svc.get(agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(404, "Agent not found")
|
||||
cnt = await svc.get_task_count(agent.id)
|
||||
return AgentOut(**{**agent.__dict__, "task_count": cnt})
|
||||
|
||||
|
||||
@router.put("/{agent_id}", response_model=AgentOut)
|
||||
async def update_agent(agent_id: int, body: AgentUpdate, db: AsyncSession = Depends(get_db)):
|
||||
svc = AgentService(db)
|
||||
agent = await svc.update(agent_id, **body.model_dump(exclude_none=True))
|
||||
if not agent:
|
||||
raise HTTPException(404, "Agent not found")
|
||||
cnt = await svc.get_task_count(agent.id)
|
||||
return AgentOut(**{**agent.__dict__, "task_count": cnt})
|
||||
|
||||
|
||||
@router.post("/{agent_id}/heartbeat", response_model=AgentOut)
|
||||
async def heartbeat(agent_id: int, body: AgentHeartbeat, db: AsyncSession = Depends(get_db)):
|
||||
svc = AgentService(db)
|
||||
agent = await svc.heartbeat(agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(404, "Agent not found")
|
||||
cnt = await svc.get_task_count(agent.id)
|
||||
return AgentOut(**{**agent.__dict__, "task_count": cnt})
|
||||
|
||||
|
||||
# ── Batch registration: AI agent self-registers with all its tasks ─────
|
||||
|
||||
@router.post("/register-with-tasks", response_model=AgentBatchRegisterResult, status_code=201)
|
||||
async def register_agent_with_tasks(body: AgentBatchRegister, db: AsyncSession = Depends(get_db)):
|
||||
"""AI Agent 一次性注册自己 + 所有定时任务。"""
|
||||
agent_svc = AgentService(db)
|
||||
task_svc = TaskService(db)
|
||||
|
||||
agent = await agent_svc.create(name=body.name, description=body.description)
|
||||
tasks_created = 0
|
||||
for t in body.tasks:
|
||||
await task_svc.create(
|
||||
agent_id=agent.id,
|
||||
name=t.name,
|
||||
cron_expression=t.cron_expression,
|
||||
description=t.description,
|
||||
grace_period=t.grace_period,
|
||||
)
|
||||
tasks_created += 1
|
||||
|
||||
await db.commit()
|
||||
task_count = await agent_svc.get_task_count(agent.id)
|
||||
return AgentBatchRegisterResult(
|
||||
agent=AgentOut(**{**agent.__dict__, "task_count": task_count}),
|
||||
tasks_created=tasks_created,
|
||||
)
|
||||
@@ -0,0 +1,115 @@
|
||||
"""API router — Dashboard overview."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Agent, Alert, ScheduledTask, TaskExecution
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||
|
||||
|
||||
class DashboardSummary(BaseModel):
|
||||
total_agents: int
|
||||
active_agents: int
|
||||
total_tasks: int
|
||||
active_tasks: int
|
||||
total_executions: int
|
||||
recent_executions_ok: int
|
||||
recent_executions_failed: int
|
||||
pending_alerts: int
|
||||
|
||||
|
||||
class DashboardTaskItem(BaseModel):
|
||||
id: int
|
||||
agent_name: str
|
||||
name: str
|
||||
cron_expression: str
|
||||
status: str
|
||||
last_run_at: datetime | None = None
|
||||
last_run_result: str | None = None
|
||||
next_run_at: datetime | None = None
|
||||
total_run_count: int = 0
|
||||
|
||||
|
||||
@router.get("/summary", response_model=DashboardSummary)
|
||||
async def dashboard_summary(db: AsyncSession = Depends(get_db)):
|
||||
# Agents
|
||||
total_agents = (await db.execute(select(func.count(Agent.id)))).scalar() or 0
|
||||
active_agents = (
|
||||
await db.execute(select(func.count(Agent.id)).where(Agent.status == "active"))
|
||||
).scalar() or 0
|
||||
|
||||
# Tasks
|
||||
total_tasks = (await db.execute(select(func.count(ScheduledTask.id)))).scalar() or 0
|
||||
active_tasks = (
|
||||
await db.execute(
|
||||
select(func.count(ScheduledTask.id)).where(ScheduledTask.status == "active")
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
# Executions (last 24h)
|
||||
since = datetime.utcnow() - timedelta(hours=24)
|
||||
total_execs = (
|
||||
await db.execute(
|
||||
select(func.count(TaskExecution.id)).where(TaskExecution.created_at >= since)
|
||||
)
|
||||
).scalar() or 0
|
||||
ok_execs = (
|
||||
await db.execute(
|
||||
select(func.count(TaskExecution.id)).where(
|
||||
TaskExecution.created_at >= since, TaskExecution.status == "success"
|
||||
)
|
||||
)
|
||||
).scalar() or 0
|
||||
failed_execs = (
|
||||
await db.execute(
|
||||
select(func.count(TaskExecution.id)).where(
|
||||
TaskExecution.created_at >= since, TaskExecution.status == "failed"
|
||||
)
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
# Alerts
|
||||
pending_alerts = (
|
||||
await db.execute(
|
||||
select(func.count(Alert.id)).where(Alert.status == "pending")
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
return DashboardSummary(
|
||||
total_agents=total_agents,
|
||||
active_agents=active_agents,
|
||||
total_tasks=total_tasks,
|
||||
active_tasks=active_tasks,
|
||||
total_executions=total_execs,
|
||||
recent_executions_ok=ok_execs,
|
||||
recent_executions_failed=failed_execs,
|
||||
pending_alerts=pending_alerts,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tasks", response_model=list[DashboardTaskItem])
|
||||
async def dashboard_tasks(db: AsyncSession = Depends(get_db)):
|
||||
"""Return all tasks with agent name for the unified view."""
|
||||
stmt = select(ScheduledTask).order_by(ScheduledTask.id)
|
||||
tasks = (await db.execute(stmt)).scalars().all()
|
||||
result = []
|
||||
for t in tasks:
|
||||
agent = await db.get(Agent, t.agent_id)
|
||||
result.append(DashboardTaskItem(
|
||||
id=t.id,
|
||||
agent_name=agent.name if agent else "",
|
||||
name=t.name,
|
||||
cron_expression=t.cron_expression,
|
||||
status=t.status,
|
||||
last_run_at=t.last_run_at,
|
||||
last_run_result=t.last_run_result,
|
||||
next_run_at=t.next_run_at,
|
||||
total_run_count=t.total_run_count or 0,
|
||||
))
|
||||
return result
|
||||
@@ -0,0 +1,70 @@
|
||||
"""API router — TaskExecution (reporting + query)."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.schemas import ExecutionOut, ExecutionReport
|
||||
from app.services import ExecutionService, TaskService, AgentService
|
||||
|
||||
router = APIRouter(prefix="/api/tasks/{task_id}/executions", tags=["executions"])
|
||||
|
||||
|
||||
@router.post("", response_model=ExecutionOut, status_code=201)
|
||||
async def report_execution(task_id: int, body: ExecutionReport,
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
"""Agent reports execution result for a task."""
|
||||
# Verify task exists
|
||||
task_svc = TaskService(db)
|
||||
task = await task_svc.get(task_id)
|
||||
if not task:
|
||||
raise HTTPException(404, "Task not found")
|
||||
|
||||
exec_svc = ExecutionService(db)
|
||||
record = await exec_svc.report_result(
|
||||
task_id=task_id,
|
||||
agent_id=task.agent_id,
|
||||
status=body.status,
|
||||
finished_at=body.finished_at,
|
||||
duration_ms=body.duration_ms,
|
||||
result=body.result,
|
||||
log=body.log,
|
||||
error_message=body.error_message,
|
||||
)
|
||||
|
||||
# Update task's last_run info
|
||||
await task_svc.mark_run(task_id, success=(body.status == "success"))
|
||||
await db.commit()
|
||||
return ExecutionOut(**record.__dict__)
|
||||
|
||||
|
||||
@router.get("", response_model=list[ExecutionOut])
|
||||
async def list_executions(
|
||||
task_id: int,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
exec_svc = ExecutionService(db)
|
||||
records = await exec_svc.get_executions(task_id, limit=limit, offset=offset)
|
||||
return [ExecutionOut(**r.__dict__) for r in records]
|
||||
|
||||
|
||||
@router.get("/recent", response_model=list[ExecutionOut])
|
||||
async def recent_executions(
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get recent executions across all tasks (for dashboard)."""
|
||||
exec_svc = ExecutionService(db)
|
||||
records = await exec_svc.get_recent_executions(limit=limit)
|
||||
return [ExecutionOut(**r.__dict__) for r in records]
|
||||
|
||||
|
||||
@router.get("/{execution_id}", response_model=ExecutionOut)
|
||||
async def get_execution(execution_id: int, db: AsyncSession = Depends(get_db)):
|
||||
exec_svc = ExecutionService(db)
|
||||
record = await exec_svc.get_execution(execution_id)
|
||||
if not record:
|
||||
raise HTTPException(404, "Execution not found")
|
||||
return ExecutionOut(**record.__dict__)
|
||||
@@ -0,0 +1,158 @@
|
||||
"""API router — Notifications & Alerts."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.schemas import (
|
||||
AlertAcknowledge,
|
||||
AlertOut,
|
||||
AlertRuleCreate,
|
||||
AlertRuleOut,
|
||||
NotificationChannelCreate,
|
||||
NotificationChannelOut,
|
||||
NotificationChannelUpdate,
|
||||
)
|
||||
from app.models import Alert, AlertRule, NotificationChannel, ScheduledTask
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["notifications"])
|
||||
|
||||
|
||||
# ── Notification Channels ─────────────────────────────────────────────
|
||||
|
||||
@router.post("/notification-channels", response_model=NotificationChannelOut, status_code=201)
|
||||
async def create_channel(body: NotificationChannelCreate, db: AsyncSession = Depends(get_db)):
|
||||
channel = NotificationChannel(
|
||||
name=body.name,
|
||||
channel_type=body.channel_type,
|
||||
config=body.config,
|
||||
)
|
||||
db.add(channel)
|
||||
await db.flush()
|
||||
await db.refresh(channel)
|
||||
await db.commit()
|
||||
return NotificationChannelOut(**channel.__dict__)
|
||||
|
||||
|
||||
@router.get("/notification-channels", response_model=list[NotificationChannelOut])
|
||||
async def list_channels(db: AsyncSession = Depends(get_db)):
|
||||
from sqlalchemy import select
|
||||
result = await db.execute(select(NotificationChannel).order_by(NotificationChannel.id))
|
||||
channels = result.scalars().all()
|
||||
return [NotificationChannelOut(**c.__dict__) for c in channels]
|
||||
|
||||
|
||||
@router.put("/notification-channels/{channel_id}", response_model=NotificationChannelOut)
|
||||
async def update_channel(channel_id: int, body: NotificationChannelUpdate,
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
channel = await db.get(NotificationChannel, channel_id)
|
||||
if not channel:
|
||||
raise HTTPException(404, "Channel not found")
|
||||
for k, v in body.model_dump(exclude_none=True).items():
|
||||
setattr(channel, k, v)
|
||||
await db.flush()
|
||||
await db.commit()
|
||||
return NotificationChannelOut(**channel.__dict__)
|
||||
|
||||
|
||||
@router.delete("/notification-channels/{channel_id}", status_code=204)
|
||||
async def delete_channel(channel_id: int, db: AsyncSession = Depends(get_db)):
|
||||
channel = await db.get(NotificationChannel, channel_id)
|
||||
if not channel:
|
||||
raise HTTPException(404, "Channel not found")
|
||||
await db.delete(channel)
|
||||
await db.flush()
|
||||
await db.commit()
|
||||
|
||||
|
||||
# ── Alert Rules ───────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/alert-rules", response_model=AlertRuleOut, status_code=201)
|
||||
async def create_alert_rule(body: AlertRuleCreate, db: AsyncSession = Depends(get_db)):
|
||||
# Verify task & channel exist
|
||||
task = await db.get(ScheduledTask, body.task_id)
|
||||
if not task:
|
||||
raise HTTPException(404, "Task not found")
|
||||
channel = await db.get(NotificationChannel, body.channel_id)
|
||||
if not channel:
|
||||
raise HTTPException(404, "Channel not found")
|
||||
|
||||
rule = AlertRule(
|
||||
task_id=body.task_id,
|
||||
channel_id=body.channel_id,
|
||||
alert_type=body.alert_type,
|
||||
)
|
||||
db.add(rule)
|
||||
await db.flush()
|
||||
await db.refresh(rule)
|
||||
await db.commit()
|
||||
return AlertRuleOut(**{**rule.__dict__, "channel_name": channel.name})
|
||||
|
||||
|
||||
@router.get("/alert-rules", response_model=list[AlertRuleOut])
|
||||
async def list_alert_rules(task_id: int | None = Query(None), db: AsyncSession = Depends(get_db)):
|
||||
from sqlalchemy import select
|
||||
stmt = select(AlertRule)
|
||||
if task_id:
|
||||
stmt = stmt.where(AlertRule.task_id == task_id)
|
||||
result = await db.execute(stmt)
|
||||
rules = result.scalars().all()
|
||||
output = []
|
||||
for r in rules:
|
||||
ch = await db.get(NotificationChannel, r.channel_id)
|
||||
output.append(AlertRuleOut(**{**r.__dict__, "channel_name": ch.name if ch else ""}))
|
||||
return output
|
||||
|
||||
|
||||
@router.delete("/alert-rules/{rule_id}", status_code=204)
|
||||
async def delete_alert_rule(rule_id: int, db: AsyncSession = Depends(get_db)):
|
||||
rule = await db.get(AlertRule, rule_id)
|
||||
if not rule:
|
||||
raise HTTPException(404, "Alert rule not found")
|
||||
await db.delete(rule)
|
||||
await db.flush()
|
||||
await db.commit()
|
||||
|
||||
|
||||
# ── Alerts ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/alerts", response_model=list[AlertOut])
|
||||
async def list_alerts(
|
||||
status: str | None = Query(None),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
from sqlalchemy import select, desc
|
||||
stmt = select(Alert).order_by(desc(Alert.id)).offset(offset).limit(limit)
|
||||
if status:
|
||||
stmt = stmt.where(Alert.status == status)
|
||||
result = await db.execute(stmt)
|
||||
alerts = result.scalars().all()
|
||||
output = []
|
||||
for a in alerts:
|
||||
task_name = ""
|
||||
if a.task_id:
|
||||
task = await db.get(ScheduledTask, a.task_id)
|
||||
task_name = task.name if task else ""
|
||||
output.append(AlertOut(**{**a.__dict__, "task_name": task_name}))
|
||||
return output
|
||||
|
||||
|
||||
@router.post("/alerts/{alert_id}/acknowledge", response_model=AlertOut)
|
||||
async def acknowledge_alert(alert_id: int, body: AlertAcknowledge,
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
from datetime import datetime
|
||||
alert = await db.get(Alert, alert_id)
|
||||
if not alert:
|
||||
raise HTTPException(404, "Alert not found")
|
||||
alert.status = "acknowledged"
|
||||
alert.acknowledged_at = datetime.utcnow()
|
||||
await db.flush()
|
||||
await db.commit()
|
||||
|
||||
task_name = ""
|
||||
if alert.task_id:
|
||||
task = await db.get(ScheduledTask, alert.task_id)
|
||||
task_name = task.name if task else ""
|
||||
return AlertOut(**{**alert.__dict__, "task_name": task_name})
|
||||
@@ -0,0 +1,77 @@
|
||||
"""API router — System configuration (base_url, etc.)."""
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.system_config import SystemConfig
|
||||
|
||||
router = APIRouter(prefix="/api/system", tags=["system"])
|
||||
|
||||
|
||||
class SystemConfigOut(BaseModel):
|
||||
base_url: str = ""
|
||||
scheduler_check_interval: int = 60
|
||||
|
||||
|
||||
class SystemConfigUpdate(BaseModel):
|
||||
base_url: str = ""
|
||||
|
||||
|
||||
@router.get("/config", response_model=SystemConfigOut)
|
||||
async def get_system_config(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
"""返回系统配置。base_url 默认从请求 host 推断,也可从数据库读取用户配置。"""
|
||||
# Try to get from DB first
|
||||
stmt = select(SystemConfig).where(SystemConfig.key == "base_url")
|
||||
result = await db.execute(stmt)
|
||||
row = result.scalar_one_or_none()
|
||||
|
||||
if row and row.value:
|
||||
base_url = row.value
|
||||
else:
|
||||
# Infer from request
|
||||
scheme = request.headers.get("x-forwarded-proto", request.url.scheme)
|
||||
host = request.headers.get("host", request.url.hostname)
|
||||
base_url = f"{scheme}://{host}"
|
||||
|
||||
return SystemConfigOut(
|
||||
base_url=base_url,
|
||||
scheduler_check_interval=60,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/config", response_model=SystemConfigOut)
|
||||
async def update_system_config(body: SystemConfigUpdate, request: Request,
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
"""更新系统配置并持久化到数据库。"""
|
||||
# Upsert base_url
|
||||
stmt = select(SystemConfig).where(SystemConfig.key == "base_url")
|
||||
result = await db.execute(stmt)
|
||||
row = result.scalar_one_or_none()
|
||||
|
||||
if body.base_url:
|
||||
if row:
|
||||
row.value = body.base_url
|
||||
else:
|
||||
row = SystemConfig(key="base_url", value=body.base_url,
|
||||
description="系统公网访问地址")
|
||||
db.add(row)
|
||||
else:
|
||||
# If clearing, remove from DB so we fall back to request host
|
||||
if row:
|
||||
await db.delete(row)
|
||||
|
||||
await db.flush()
|
||||
await db.commit()
|
||||
|
||||
# Return current effective config
|
||||
scheme = request.headers.get("x-forwarded-proto", request.url.scheme)
|
||||
host = request.headers.get("host", request.url.hostname)
|
||||
effective_base = body.base_url or f"{scheme}://{host}"
|
||||
|
||||
return SystemConfigOut(
|
||||
base_url=effective_base,
|
||||
scheduler_check_interval=60,
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
"""API router — Task management."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.schemas import TaskCreate, TaskOut, TaskUpdate
|
||||
from app.services import TaskService, AgentService
|
||||
|
||||
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
|
||||
|
||||
|
||||
@router.post("", response_model=TaskOut, status_code=201)
|
||||
async def create_task(body: TaskCreate, agent_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Create a task for a specific agent. Pass agent_id as query param."""
|
||||
svc = TaskService(db)
|
||||
agent_svc = AgentService(db)
|
||||
agent = await agent_svc.get(agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(404, "Agent not found")
|
||||
task = await svc.create(
|
||||
agent_id=agent_id,
|
||||
name=body.name,
|
||||
cron_expression=body.cron_expression,
|
||||
description=body.description,
|
||||
grace_period=body.grace_period,
|
||||
)
|
||||
return TaskOut(**{**task.__dict__, "agent_name": agent.name})
|
||||
|
||||
|
||||
@router.get("", response_model=list[TaskOut])
|
||||
async def list_tasks(agent_id: int | None = None, db: AsyncSession = Depends(get_db)):
|
||||
"""List all tasks, optionally filtered by agent_id."""
|
||||
svc = TaskService(db)
|
||||
agent_svc = AgentService(db)
|
||||
if agent_id:
|
||||
tasks = await svc.list_by_agent(agent_id)
|
||||
else:
|
||||
tasks = await svc.list_all()
|
||||
result = []
|
||||
for t in tasks:
|
||||
agent = await agent_svc.get(t.agent_id)
|
||||
result.append(TaskOut(**{**t.__dict__, "agent_name": agent.name if agent else ""}))
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{task_id}", response_model=TaskOut)
|
||||
async def get_task(task_id: int, db: AsyncSession = Depends(get_db)):
|
||||
svc = TaskService(db)
|
||||
agent_svc = AgentService(db)
|
||||
task = await svc.get(task_id)
|
||||
if not task:
|
||||
raise HTTPException(404, "Task not found")
|
||||
agent = await agent_svc.get(task.agent_id)
|
||||
return TaskOut(**{**task.__dict__, "agent_name": agent.name if agent else ""})
|
||||
|
||||
|
||||
@router.put("/{task_id}", response_model=TaskOut)
|
||||
async def update_task(task_id: int, body: TaskUpdate, db: AsyncSession = Depends(get_db)):
|
||||
svc = TaskService(db)
|
||||
agent_svc = AgentService(db)
|
||||
task = await svc.update(task_id, **body.model_dump(exclude_none=True))
|
||||
if not task:
|
||||
raise HTTPException(404, "Task not found")
|
||||
agent = await agent_svc.get(task.agent_id)
|
||||
return TaskOut(**{**task.__dict__, "agent_name": agent.name if agent else ""})
|
||||
|
||||
|
||||
@router.delete("/{task_id}", status_code=204)
|
||||
async def delete_task(task_id: int, db: AsyncSession = Depends(get_db)):
|
||||
svc = TaskService(db)
|
||||
ok = await svc.delete(task_id)
|
||||
if not ok:
|
||||
raise HTTPException(404, "Task not found")
|
||||
Reference in New Issue
Block a user