sprint1: agent queue worker, proxy support, auto-mark-on-navigate-away, test fixes
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Agent Queue Worker — polls agent_queue for pending items and
|
||||
processes them against the local Qwen27B API (:8080).
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import aiosqlite
|
||||
import httpx
|
||||
|
||||
import db
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
QWEN_BASE_URL = os.environ.get("QWEN_BASE_URL", "http://localhost:8080")
|
||||
QWEN_MODEL = os.environ.get("QWEN_MODEL", "current")
|
||||
POLL_INTERVAL = int(os.environ.get("AGENT_POLL_INTERVAL", "30"))
|
||||
MAX_CONCURRENT = int(os.environ.get("AGENT_MAX_CONCURRENT", "3"))
|
||||
SYSTEM_PROMPT = os.environ.get(
|
||||
"AGENT_SYSTEM_PROMPT",
|
||||
"You are a helpful AI assistant. The user has watched a YouTube video and "
|
||||
"left you instructions. Read the video info below and respond to their "
|
||||
"prompt. Be concise and actionable."
|
||||
)
|
||||
|
||||
semaphore = asyncio.Semaphore(MAX_CONCURRENT)
|
||||
|
||||
|
||||
async def _call_qwen(prompt: str, video_title: str, video_context: str) -> str:
|
||||
"""Call the local Qwen27B API with video context + user prompt."""
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"## Video Title\n{video_title}\n\n"
|
||||
f"## Video Context\n{video_context}\n\n"
|
||||
f"## User's Request\n{prompt}"
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
http_proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
|
||||
https_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
|
||||
proxies = http_proxy or https_proxy
|
||||
|
||||
async with httpx.AsyncClient(proxy=proxies, timeout=180) as client:
|
||||
resp = await client.post(
|
||||
f"{QWEN_BASE_URL}/v1/chat/completions",
|
||||
json={
|
||||
"model": QWEN_MODEL,
|
||||
"messages": messages,
|
||||
"max_tokens": 2048,
|
||||
"temperature": 0.3,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
|
||||
|
||||
async def _process_item(item: dict) -> None:
|
||||
"""Process a single agent_queue item."""
|
||||
video_id = item["video_id"]
|
||||
user_prompt = item["user_prompt"]
|
||||
|
||||
async with aiosqlite.connect(db.DB_PATH) as conn:
|
||||
conn.row_factory = aiosqlite.Row
|
||||
|
||||
# Mark as processing
|
||||
await conn.execute(
|
||||
"UPDATE agent_queue SET status = 'processing' WHERE id = ?",
|
||||
(item["id"],),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
# Get video info
|
||||
cursor = await conn.execute(
|
||||
"SELECT title, channel_name, summary, transcript FROM videos WHERE video_id = ?",
|
||||
(video_id,),
|
||||
)
|
||||
video = await cursor.fetchone()
|
||||
|
||||
if not video:
|
||||
async with aiosqlite.connect(db.DB_PATH) as conn:
|
||||
await conn.execute(
|
||||
"UPDATE agent_queue SET status = 'failed', result = 'Video not found in DB' WHERE id = ?",
|
||||
(item["id"],),
|
||||
)
|
||||
await conn.commit()
|
||||
return
|
||||
|
||||
# Build video context from whatever we have
|
||||
context_parts = []
|
||||
if video["channel_name"]:
|
||||
context_parts.append(f"Channel: {video['channel_name']}")
|
||||
if video["summary"] and not video["summary"].startswith("📺 Short clip"):
|
||||
context_parts.append(f"Summary:\n{video['summary']}")
|
||||
if video["transcript"] and video["transcript"] != "No transcript available":
|
||||
context_parts.append(f"Transcript (first 4000 chars):\n{video['transcript'][:4000]}")
|
||||
video_context = "\n\n".join(context_parts) if context_parts else "No additional context available."
|
||||
|
||||
try:
|
||||
result = await _call_qwen(user_prompt, video["title"], video_context)
|
||||
status = "done"
|
||||
except Exception as e:
|
||||
log.warning(f"Qwen API call failed for item {item['id']}: {e}")
|
||||
result = f"Error: {e}"
|
||||
status = "failed"
|
||||
|
||||
async with aiosqlite.connect(db.DB_PATH) as conn:
|
||||
await conn.execute(
|
||||
"UPDATE agent_queue SET status = ?, result = ?, processed_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
(status, result, item["id"]),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def process_queue_loop() -> None:
|
||||
"""Main polling loop — runs forever."""
|
||||
log.info(
|
||||
f"Agent worker started: Qwen at {QWEN_BASE_URL}, "
|
||||
f"poll every {POLL_INTERVAL}s, max {MAX_CONCURRENT} concurrent"
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with aiosqlite.connect(db.DB_PATH) as conn:
|
||||
conn.row_factory = aiosqlite.Row
|
||||
cursor = await conn.execute(
|
||||
"SELECT id, video_id, user_prompt FROM agent_queue WHERE status = 'pending' ORDER BY created_at ASC LIMIT ?",
|
||||
(MAX_CONCURRENT,),
|
||||
)
|
||||
items = await cursor.fetchall()
|
||||
|
||||
if items:
|
||||
log.info(f"Agent queue: processing {len(items)} item(s)")
|
||||
tasks = [_process_item(dict(item)) for item in items]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Agent worker loop error: {e}")
|
||||
|
||||
await asyncio.sleep(POLL_INTERVAL)
|
||||
|
||||
|
||||
async def start_worker():
|
||||
"""Entry point — called from main.py startup."""
|
||||
task = asyncio.create_task(process_queue_loop())
|
||||
log.info("Agent queue worker scheduled")
|
||||
return task
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
async def main():
|
||||
from db import init_db
|
||||
await init_db()
|
||||
log.info("Agent worker running in foreground. Ctrl+C to stop.")
|
||||
await process_queue_loop()
|
||||
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user