improve: better prompts and rate-limit retry in pipeline
prompts.py: - Added detailed technical patterns for deepagents, FastMCP, LangGraph, HumanInTheLoop, RAG with Qdrant, stream mode, text game - LLM always via OpenRouter (never hub.pull/Ollama/hardcode) - FastMCP correct pattern (module-level, NOT inside class) - create_agent not compatible with AgentExecutor - documented - DuckDuckGo search pattern (no API key needed) pipeline.py: - Added _invoke_with_retry: auto-retry on 429 rate limit (up to 5x, 90s backoff) - Added TASK_PAUSE (15s) between tasks to reduce rate limit pressure - Progress logging: per-task status messages - Imported asyncio and re Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
"""LangGraph pipeline: последовательно выполняет все незакрытые задания курса."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
from typing import TypedDict
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
@@ -80,12 +82,25 @@ _CODING_KW = [
|
||||
"code", "напиши", "реализуй", "python", "langchain", "langgraph",
|
||||
"агент", "agent", "граф", "graph", "файл", "функц", "программ",
|
||||
"скрипт", "алгоритм", "библиотек", "api", "сервер", "модуль", "класс",
|
||||
# дополнительные ключевые слова для курса KFU-26-1
|
||||
"ai", "llm", "rag", "mcp", "stream", "human", "interrupt", "middleware",
|
||||
"память", "игра", "текст", "fluency", "практическ", "создай", "создайт",
|
||||
"задание", "deep", "search", "поиск",
|
||||
]
|
||||
|
||||
# Задания, которые точно не требуют кода (теория, чтение)
|
||||
_NON_CODING_TITLES = []
|
||||
|
||||
|
||||
def _is_coding(task: TaskInfo) -> bool:
|
||||
title = (task.get("title") or "").lower()
|
||||
return any(kw in title for kw in _CODING_KW)
|
||||
if any(nc in title for nc in _NON_CODING_TITLES):
|
||||
return False
|
||||
# Если хотя бы одно кодинговое слово — берём задание
|
||||
if any(kw in title for kw in _CODING_KW):
|
||||
return True
|
||||
# Для этого курса все задания — программирование, берём всё
|
||||
return True
|
||||
|
||||
|
||||
async def _task_text(task_id: str) -> str:
|
||||
@@ -162,10 +177,39 @@ def _fix_prompt(task: TaskInfo, repo_name: str, v: dict) -> str:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Узлы графа
|
||||
# Вспомогательное: retry при rate-limit 429
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MAX_RETRIES = 2
|
||||
RATE_LIMIT_RETRIES = 5 # сколько раз повторять при 429
|
||||
RATE_LIMIT_PAUSE = 90 # секунд ожидания перед повтором
|
||||
TASK_PAUSE = 15 # пауза между заданиями (снижает давление на rate limit)
|
||||
|
||||
|
||||
def _is_rate_limit(exc: Exception) -> bool:
|
||||
"""Проверяет, является ли исключение ошибкой rate-limit (429)."""
|
||||
msg = str(exc)
|
||||
return "429" in msg or "rate" in msg.lower() or "rate_limit" in msg.lower()
|
||||
|
||||
|
||||
async def _invoke_with_retry(agent, messages, config):
|
||||
"""Вызывает агента с автоматическим retry при 429."""
|
||||
for attempt in range(1, RATE_LIMIT_RETRIES + 1):
|
||||
try:
|
||||
return await agent.ainvoke(messages, config)
|
||||
except Exception as e:
|
||||
if _is_rate_limit(e) and attempt < RATE_LIMIT_RETRIES:
|
||||
wait = RATE_LIMIT_PAUSE * attempt
|
||||
print(f"[pipeline] Rate limit (попытка {attempt}/{RATE_LIMIT_RETRIES}), "
|
||||
f"жду {wait}с...")
|
||||
await asyncio.sleep(wait)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Узлы графа
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def fetch_tasks(state: PipelineState) -> dict:
|
||||
@@ -225,7 +269,11 @@ async def process_one_task(state: PipelineState) -> dict:
|
||||
agent_to_use = homework_direct_agent
|
||||
|
||||
try:
|
||||
result = await agent_to_use.ainvoke(
|
||||
print(f"[pipeline] Задание {task_id[:8]} — {'пересдача' if is_rework else 'первая сдача'}: "
|
||||
f"{task.get('title','')[:50]}")
|
||||
|
||||
result = await _invoke_with_retry(
|
||||
agent_to_use,
|
||||
{"messages": [HumanMessage(content=prompt)]},
|
||||
{"configurable": {"thread_id": f"pipeline-task-{task_id}"}},
|
||||
)
|
||||
@@ -243,12 +291,14 @@ async def process_one_task(state: PipelineState) -> dict:
|
||||
while _needs_retry(verification) and retries < MAX_RETRIES:
|
||||
retries += 1
|
||||
fix_msg = _fix_prompt(task, repo_name, verification)
|
||||
result = await agent_to_use.ainvoke(
|
||||
result = await _invoke_with_retry(
|
||||
agent_to_use,
|
||||
{"messages": [HumanMessage(content=fix_msg)]},
|
||||
{"configurable": {"thread_id": f"pipeline-task-{task_id}-retry-{retries}"}},
|
||||
)
|
||||
verification = await _verify_repo(repo_name)
|
||||
|
||||
print(f"[pipeline] Задание {task_id[:8]} — OK (retries={retries})")
|
||||
results.append({
|
||||
"task_id": task_id,
|
||||
"status": "done",
|
||||
@@ -259,8 +309,13 @@ async def process_one_task(state: PipelineState) -> dict:
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"[pipeline] Задание {task_id[:8]} — ОШИБКА: {e}")
|
||||
errors.append(f"Задание {task_id} ({'rework' if is_rework else 'new'}): {e}")
|
||||
|
||||
# Пауза между заданиями чтобы не перегружать rate limit
|
||||
print(f"[pipeline] Пауза {TASK_PAUSE}с перед следующим заданием...")
|
||||
await asyncio.sleep(TASK_PAUSE)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"current_index": state["current_index"] + 1,
|
||||
|
||||
Reference in New Issue
Block a user