hope / queue.py
Cunt1257's picture
Upload 3 files
0af083b verified
Raw
History Blame Contribute Delete
6.82 kB
import asyncio
from datetime import datetime, timezone
from typing import Dict, List, Optional
from .models import Job, JobStatus, Pipeline, PipelineStep
from src.agents import executor, registry, AgentContext
from src.agents.base import AgentStatus
from src.config import settings
from src.telemetry import collector
# Per-agent-type concurrency weight: fraction of max_concurrent_agents slots reserved
# Values > 1.0 mean the type can use more than its fair share; < 1.0 means restricted
AGENT_TYPE_WEIGHTS: Dict[str, float] = {
"echo": 1.0,
"compute": 2.0, # compute-heavy, more slots
"sleep": 0.5, # low priority, fewer slots
"assistant": 1.5,
"router": 1.5,
}
_PRIORITY_BOOST: Dict[str, float] = {
"vip": settings.priority_boost_vip,
"high": settings.priority_boost_high,
"normal": settings.priority_boost_normal,
"low": settings.priority_boost_low,
}
_DECAY: float = settings.weight_decay
def _effective_weight(job: Job, attempt: int = 0) -> float:
"""Compute scheduling weight. Higher = executed sooner."""
boost = _PRIORITY_BOOST.get(job.priority, 1.0)
type_weight = AGENT_TYPE_WEIGHTS.get(job.agent_type, 1.0)
decay = _DECAY ** attempt
return job.weight * boost * type_weight * decay
class JobQueue:
def __init__(self):
self._jobs: Dict[str, Job] = {}
self._queue: asyncio.PriorityQueue = asyncio.PriorityQueue()
self._pipelines: Dict[str, Pipeline] = {}
self._worker_task: Optional[asyncio.Task] = None
# ── Job management ──────────────────────────────────────────────
def enqueue(self, job: Job) -> Job:
job.weight = _effective_weight(job)
self._jobs[job.job_id] = job
priority_score = -job.weight # negate so higher weight = dequeued first
self._queue.put_nowait((priority_score, job.created_at.timestamp(), job.job_id))
collector.emit_raw("job.queued", {"job_id": job.job_id, "weight": job.weight,
"priority": job.priority, "agent_type": job.agent_type})
return job
def get_job(self, job_id: str) -> Optional[Job]:
return self._jobs.get(job_id)
def list_jobs(self, status: Optional[JobStatus] = None, limit: int = 100) -> List[Job]:
jobs = list(self._jobs.values())
if status:
jobs = [j for j in jobs if j.status == status]
return sorted(jobs, key=lambda j: j.created_at, reverse=True)[:limit]
def cancel_job(self, job_id: str) -> bool:
job = self._jobs.get(job_id)
if job and job.status == JobStatus.QUEUED:
job.status = JobStatus.CANCELLED
return True
return False
# ── Pipeline management ─────────────────────────────────────────
def enqueue_pipeline(self, pipeline: Pipeline) -> Pipeline:
self._pipelines[pipeline.pipeline_id] = pipeline
asyncio.create_task(self._run_pipeline(pipeline))
return pipeline
def get_pipeline(self, pipeline_id: str) -> Optional[Pipeline]:
return self._pipelines.get(pipeline_id)
def list_pipelines(self, limit: int = 50) -> List[Pipeline]:
return sorted(self._pipelines.values(), key=lambda p: p.created_at, reverse=True)[:limit]
# ── Worker ──────────────────────────────────────────────────────
async def start(self) -> None:
if self._worker_task is None or self._worker_task.done():
self._worker_task = asyncio.create_task(self._worker_loop())
async def stop(self) -> None:
if self._worker_task:
self._worker_task.cancel()
async def _worker_loop(self) -> None:
while True:
_, _, job_id = await self._queue.get()
job = self._jobs.get(job_id)
if not job or job.status == JobStatus.CANCELLED:
self._queue.task_done()
continue
asyncio.create_task(self._execute_job(job))
self._queue.task_done()
async def _execute_job(self, job: Job) -> None:
job.status = JobStatus.RUNNING
job.started_at = datetime.now(timezone.utc)
try:
agent = registry.create(job.agent_type)
ctx = AgentContext(agent_id=agent.agent_id)
ctx.metadata["job_id"] = job.job_id
if job.retry:
result = await executor.run_with_retry(agent, ctx,
retries=job.retries, **job.kwargs)
else:
result = await executor.run(agent, ctx, **job.kwargs)
job.result = result.output
job.error = result.error
job.duration_ms = result.duration_ms
job.status = (JobStatus.COMPLETED if result.status == AgentStatus.COMPLETED
else JobStatus.FAILED)
except Exception as exc:
job.status = JobStatus.FAILED
job.error = str(exc)
finally:
job.completed_at = datetime.now(timezone.utc)
collector.emit_raw(f"job.{job.status.value}",
{"job_id": job.job_id, "duration_ms": job.duration_ms})
async def _run_pipeline(self, pipeline: Pipeline) -> None:
pipeline.status = JobStatus.RUNNING
previous_output = None
for i, step in enumerate(pipeline.steps):
try:
agent = registry.create(step.agent_type)
ctx = AgentContext(agent_id=agent.agent_id)
ctx.metadata["pipeline_id"] = pipeline.pipeline_id
ctx.metadata["step"] = i
kwargs = dict(step.kwargs)
if step.pass_output and previous_output is not None:
kwargs["previous_output"] = previous_output
result = await executor.run(agent, ctx, **kwargs)
pipeline.results.append(result.output)
previous_output = result.output
if result.status != AgentStatus.COMPLETED:
pipeline.status = JobStatus.FAILED
pipeline.error = f"Step {i} ({step.agent_type}) failed: {result.error}"
return
except Exception as exc:
pipeline.status = JobStatus.FAILED
pipeline.error = f"Step {i} ({step.agent_type}) exception: {exc}"
return
pipeline.status = JobStatus.COMPLETED
pipeline.completed_at = datetime.now(timezone.utc)
job_queue = JobQueue()