167 lines
5.5 KiB
Python
167 lines
5.5 KiB
Python
"""
|
|
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())
|