add ui, refactor agent, fix pipeline
This commit is contained in:
@@ -12,8 +12,6 @@ dependencies = [
|
|||||||
"pydantic-settings>=2.0.0",
|
"pydantic-settings>=2.0.0",
|
||||||
"python-dotenv>=1.0.0",
|
"python-dotenv>=1.0.0",
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
"markdownify>=0.13.0",
|
|
||||||
"tavily-python>=0.3.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -6,5 +6,3 @@ langgraph>=1.0.8
|
|||||||
pydantic-settings>=2.0.0
|
pydantic-settings>=2.0.0
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
httpx>=0.27.0
|
httpx>=0.27.0
|
||||||
markdownify>=0.13.0
|
|
||||||
tavily-python>=0.3.0
|
|
||||||
|
|||||||
+68
-39
@@ -6,11 +6,14 @@ from src.agent.agent import homework_direct_agent
|
|||||||
from src.agent.graph.pipeline import _invoke_with_retry, _get_journal_tool, _parse_text
|
from src.agent.graph.pipeline import _invoke_with_retry, _get_journal_tool, _parse_text
|
||||||
print("[2/3] Агент готов, запускаем задание...")
|
print("[2/3] Агент готов, запускаем задание...")
|
||||||
|
|
||||||
TASK_ID = "6a1855055db1b0a5ea224b8b"
|
# ── Настройки ────────────────────────────────────────────────────────────────
|
||||||
|
TASK_ID = "6a1867fa8a94f887e50d52bd" # ← taskId из platform.brojs.ru
|
||||||
OWNER = "KirillKutlakhmetov"
|
OWNER = "KirillKutlakhmetov"
|
||||||
REPO = f"task-{TASK_ID}"
|
REPO = f"task-{TASK_ID}"
|
||||||
URL = f"https://git.brojs.ru/{OWNER}/{REPO}"
|
URL = f"https://git.brojs.ru/{OWNER}/{REPO}"
|
||||||
|
|
||||||
|
# ── Вспомогательные функции ──────────────────────────────────────────────────
|
||||||
|
|
||||||
def _was_submitted(msgs: list) -> bool:
|
def _was_submitted(msgs: list) -> bool:
|
||||||
for m in msgs:
|
for m in msgs:
|
||||||
for tc in getattr(m, "tool_calls", []):
|
for tc in getattr(m, "tool_calls", []):
|
||||||
@@ -18,31 +21,72 @@ def _was_submitted(msgs: list) -> bool:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_rate_limit(e) -> bool:
|
||||||
|
full = repr(e)
|
||||||
|
if hasattr(e, 'exceptions'):
|
||||||
|
full += " ".join(repr(sub) for sub in e.exceptions)
|
||||||
|
return "429" in full or "rate" in full.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_not_found(e) -> bool:
|
||||||
|
full = repr(e)
|
||||||
|
if hasattr(e, 'exceptions'):
|
||||||
|
full += " ".join(repr(sub) for sub in e.exceptions)
|
||||||
|
return "not found" in full.lower() or "submission" in full.lower()
|
||||||
|
|
||||||
|
|
||||||
async def _force_submit(task_id: str):
|
async def _force_submit(task_id: str):
|
||||||
update = _get_journal_tool("task_update_answer")
|
"""Страховка: сдаёт задание если агент не сделал этого сам."""
|
||||||
submit = _get_journal_tool("task_submit")
|
task_get = _get_journal_tool("task_get")
|
||||||
|
update = _get_journal_tool("task_update_answer")
|
||||||
|
submit = _get_journal_tool("task_submit")
|
||||||
|
|
||||||
if not update or not submit:
|
if not update or not submit:
|
||||||
print("[!] journal-инструменты недоступны")
|
print("[!] journal-инструменты недоступны — сдать не удалось")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Сначала вызываем task_get — это инициализирует submission если его нет
|
||||||
|
if task_get:
|
||||||
|
try:
|
||||||
|
print("[AUTO] task_get (инициализация submission)...")
|
||||||
|
await task_get.ainvoke({"taskId": task_id})
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
except BaseException:
|
||||||
|
pass # не критично
|
||||||
|
|
||||||
for attempt in range(1, 6):
|
for attempt in range(1, 6):
|
||||||
try:
|
try:
|
||||||
print(f"[AUTO] task_update_answer (попытка {attempt})")
|
print(f"[AUTO] task_update_answer (попытка {attempt})...")
|
||||||
r1 = await update.ainvoke({"taskId": task_id, "answerType": "link", "content": URL})
|
r1 = await update.ainvoke({
|
||||||
print(f"[AUTO] update: {_parse_text(r1)[:120]}")
|
"taskId": task_id,
|
||||||
|
"answerType": "link",
|
||||||
|
"content": URL,
|
||||||
|
})
|
||||||
|
print(f"[AUTO] update OK: {_parse_text(r1)[:120]}")
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
print(f"[AUTO] task_submit")
|
|
||||||
|
print(f"[AUTO] task_submit...")
|
||||||
r2 = await submit.ainvoke({"taskId": task_id, "confirmSubmit": True})
|
r2 = await submit.ainvoke({"taskId": task_id, "confirmSubmit": True})
|
||||||
print(f"[AUTO] submit: {_parse_text(r2)[:120]}")
|
print(f"[AUTO] submit OK: {_parse_text(r2)[:120]}")
|
||||||
return
|
return
|
||||||
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
full = repr(e)
|
full = repr(e)
|
||||||
if hasattr(e, 'exceptions'):
|
if hasattr(e, 'exceptions'):
|
||||||
full += " ".join(repr(sub) for sub in e.exceptions)
|
full += " ".join(repr(sub) for sub in e.exceptions)
|
||||||
if "429" in full and attempt < 5:
|
if "429" in full and attempt < 5:
|
||||||
print(f"[AUTO] Rate limit, жду 70с...")
|
print(f"[AUTO] Rate limit 429 — жду 10с...")
|
||||||
await asyncio.sleep(70)
|
await asyncio.sleep(10)
|
||||||
|
elif attempt < 5:
|
||||||
|
print(f"[AUTO] Ошибка (попытка {attempt}): {full[:150]} — жду 10с...")
|
||||||
|
await asyncio.sleep(10)
|
||||||
else:
|
else:
|
||||||
raise
|
print(f"[!] Все попытки исчерпаны: {full[:200]}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ── Основной запуск ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
prompt = f"""Выполни задание из курса BroJS.
|
prompt = f"""Выполни задание из курса BroJS.
|
||||||
@@ -51,49 +95,33 @@ taskId: {TASK_ID}
|
|||||||
owner: {OWNER}
|
owner: {OWNER}
|
||||||
repo: {REPO}
|
repo: {REPO}
|
||||||
|
|
||||||
ШАГ 1. Прочитай задание полностью:
|
ШАГ 1. mcp__journal-bh-professor__task_text(taskId='{TASK_ID}')
|
||||||
mcp__journal-bh-professor__task_text(taskId='{TASK_ID}')
|
ШАГ 2. gitea_create_repo(name='{REPO}')
|
||||||
|
ШАГ 3. gitea_write_file — загрузи все файлы (main.py, requirements.txt, README.md + модули). Полный код, без заглушек.
|
||||||
ШАГ 2. Создай репозиторий:
|
ШАГ 4. mcp__journal-bh-professor__task_update_answer(taskId='{TASK_ID}', answerType='link', content='{URL}')
|
||||||
gitea_create_repo(name='{REPO}')
|
ШАГ 5. mcp__journal-bh-professor__task_submit(taskId='{TASK_ID}', confirmSubmit=True)"""
|
||||||
|
|
||||||
ШАГ 3. Напиши и загрузи все нужные файлы через gitea_write_file.
|
|
||||||
- Пиши полный рабочий код, без заглушек и TODO
|
|
||||||
- Минимум 4 файла: main.py, requirements.txt, README.md + модули по заданию
|
|
||||||
- Читай файлы репозитория только через gitea_get_file, не read_file
|
|
||||||
|
|
||||||
ШАГ 4. Отправь ответ:
|
|
||||||
mcp__journal-bh-professor__task_update_answer(
|
|
||||||
taskId='{TASK_ID}',
|
|
||||||
answerType='link',
|
|
||||||
content='{URL}'
|
|
||||||
)
|
|
||||||
|
|
||||||
ШАГ 5. Сдай задание (ОБЯЗАТЕЛЬНО):
|
|
||||||
mcp__journal-bh-professor__task_submit(
|
|
||||||
taskId='{TASK_ID}',
|
|
||||||
confirmSubmit=True
|
|
||||||
)"""
|
|
||||||
|
|
||||||
config = {"configurable": {"thread_id": f"hw-{TASK_ID}-{int(time.time())}"}}
|
config = {"configurable": {"thread_id": f"hw-{TASK_ID}-{int(time.time())}"}}
|
||||||
print(f"[3/3] Агент работает...")
|
print(f"[3/3] Агент работает...")
|
||||||
|
|
||||||
result = await _invoke_with_retry(
|
result = await _invoke_with_retry(
|
||||||
homework_direct_agent,
|
homework_direct_agent,
|
||||||
{"messages": [HumanMessage(content=prompt)]},
|
{"messages": [HumanMessage(content=prompt)]},
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
|
|
||||||
print("[ГОТОВО] Агент завершил работу.")
|
print("[ГОТОВО] Агент завершил работу.")
|
||||||
msgs = result.get("messages", [])
|
msgs = result.get("messages", [])
|
||||||
print(f"[{len(msgs)} сообщений]")
|
print(f"[{len(msgs)} сообщений]")
|
||||||
for i, m in enumerate(msgs):
|
for i, m in enumerate(msgs):
|
||||||
role = type(m).__name__
|
role = type(m).__name__
|
||||||
tcs = getattr(m, "tool_calls", [])
|
tcs = getattr(m, "tool_calls", [])
|
||||||
content_str = str(getattr(m, "content", ""))
|
text = str(getattr(m, "content", ""))
|
||||||
if tcs:
|
if tcs:
|
||||||
for tc in tcs:
|
for tc in tcs:
|
||||||
print(f" [{i}] {role} -> {tc['name']}({str(tc.get('args',''))[:120]})")
|
print(f" [{i}] {role} -> {tc['name']}({str(tc.get('args',''))[:120]})")
|
||||||
elif content_str.strip():
|
elif text.strip():
|
||||||
print(f" [{i}] {role}: {content_str[:200]}")
|
print(f" [{i}] {role}: {text[:200]}")
|
||||||
|
|
||||||
if not _was_submitted(msgs):
|
if not _was_submitted(msgs):
|
||||||
print("\n[!] Агент не вызвал task_submit — сдаю автоматически...")
|
print("\n[!] Агент не вызвал task_submit — сдаю автоматически...")
|
||||||
@@ -101,4 +129,5 @@ repo: {REPO}
|
|||||||
else:
|
else:
|
||||||
print("\n[OK] Задание сдано агентом самостоятельно.")
|
print("\n[OK] Задание сдано агентом самостоятельно.")
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|||||||
+7
-19
@@ -1,12 +1,10 @@
|
|||||||
"""Создание агентов: главный оркестратор, исполнитель ДЗ, агент пересдачи."""
|
"""Создание агентов: главный оркестратор, исполнитель ДЗ, агент пересдачи."""
|
||||||
from deepagents import create_deep_agent
|
from deepagents import create_deep_agent
|
||||||
from deepagents.backends import CompositeBackend, FilesystemBackend, LocalShellBackend
|
from deepagents.backends import LocalShellBackend
|
||||||
|
|
||||||
from src.agent.constants import (
|
from src.agent.constants import (
|
||||||
AGENT_WORKSPACE_DIR,
|
AGENT_WORKSPACE_DIR,
|
||||||
AGENTS_MD_VFS_PATH,
|
AGENTS_MD_VFS_PATH,
|
||||||
BUNDLED_SKILLS_DIR,
|
|
||||||
SKILLS_VFS_MOUNT,
|
|
||||||
ensure_agents_md_file,
|
ensure_agents_md_file,
|
||||||
)
|
)
|
||||||
from src.agent.gitea_tools import GITEA_TOOLS
|
from src.agent.gitea_tools import GITEA_TOOLS
|
||||||
@@ -19,7 +17,7 @@ from src.agent.prompts import (
|
|||||||
rework_instructions,
|
rework_instructions,
|
||||||
)
|
)
|
||||||
from src.agent.subagents import subagent_specs_without_tools
|
from src.agent.subagents import subagent_specs_without_tools
|
||||||
from src.agent.tools import GIT_TOOLS, WEB_TOOLS
|
from src.agent.tools import GIT_TOOLS
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Инициализация
|
# Инициализация
|
||||||
@@ -36,29 +34,19 @@ print(f"=== Загружено: journal={len(_journal_tools)}, gitea={len(GITEA_
|
|||||||
# Бэкенды (виртуальная файловая система агента)
|
# Бэкенды (виртуальная файловая система агента)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_workspace_backend = LocalShellBackend(
|
_composite_backend = LocalShellBackend(
|
||||||
root_dir=str(AGENT_WORKSPACE_DIR),
|
root_dir=str(AGENT_WORKSPACE_DIR),
|
||||||
virtual_mode=True,
|
virtual_mode=True,
|
||||||
inherit_env=True,
|
inherit_env=True,
|
||||||
)
|
)
|
||||||
_skills_backend = FilesystemBackend(
|
|
||||||
root_dir=str(BUNDLED_SKILLS_DIR),
|
|
||||||
virtual_mode=True,
|
|
||||||
)
|
|
||||||
_composite_backend = CompositeBackend(
|
|
||||||
default=_workspace_backend,
|
|
||||||
routes={SKILLS_VFS_MOUNT: _skills_backend},
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Наборы инструментов
|
# Наборы инструментов
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_homework_tools = [*GIT_TOOLS, *GITEA_TOOLS, *WEB_TOOLS, *_journal_tools]
|
_homework_tools = [*GIT_TOOLS, *GITEA_TOOLS, *_journal_tools]
|
||||||
_web_tools = WEB_TOOLS
|
|
||||||
|
|
||||||
_subagent_tool_map = {
|
_subagent_tool_map = {
|
||||||
"web_search": _web_tools,
|
|
||||||
"homework_doing": _homework_tools,
|
"homework_doing": _homework_tools,
|
||||||
"journal_bh_tasks_submissions": _journal_tools,
|
"journal_bh_tasks_submissions": _journal_tools,
|
||||||
}
|
}
|
||||||
@@ -71,13 +59,11 @@ _BUILTIN = {
|
|||||||
_gitea_names = {t.name for t in GITEA_TOOLS}
|
_gitea_names = {t.name for t in GITEA_TOOLS}
|
||||||
_journal_names = {t.name for t in _journal_tools}
|
_journal_names = {t.name for t in _journal_tools}
|
||||||
_git_names = {t.name for t in GIT_TOOLS}
|
_git_names = {t.name for t in GIT_TOOLS}
|
||||||
_web_names = {t.name for t in WEB_TOOLS}
|
|
||||||
|
|
||||||
_main_tool_names = _BUILTIN | _gitea_names
|
_main_tool_names = _BUILTIN | _gitea_names
|
||||||
|
|
||||||
_subagent_tool_names: dict[str, set[str]] = {
|
_subagent_tool_names: dict[str, set[str]] = {
|
||||||
"web_search": _BUILTIN | _web_names,
|
"homework_doing": _BUILTIN | _gitea_names | _journal_names | _git_names,
|
||||||
"homework_doing": _BUILTIN | _gitea_names | _journal_names | _git_names | _web_names,
|
|
||||||
"journal_bh_tasks_submissions": _BUILTIN | _journal_names,
|
"journal_bh_tasks_submissions": _BUILTIN | _journal_names,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +78,8 @@ def _make_subagent_middleware(name: str) -> list:
|
|||||||
return mw
|
return mw
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Субагенты с инструментами и middleware
|
# Субагенты с инструментами и middleware
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ from pathlib import Path
|
|||||||
PACKAGE_DIR = Path(__file__).resolve().parent
|
PACKAGE_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
AGENT_WORKSPACE_DIR = PACKAGE_DIR / "agent_workspace"
|
AGENT_WORKSPACE_DIR = PACKAGE_DIR / "agent_workspace"
|
||||||
BUNDLED_SKILLS_DIR = PACKAGE_DIR / "skills"
|
|
||||||
|
|
||||||
AGENTS_MD_FILENAME = "AGENTS.md"
|
AGENTS_MD_FILENAME = "AGENTS.md"
|
||||||
AGENTS_MD_VFS_PATH = "/AGENTS.md"
|
AGENTS_MD_VFS_PATH = "/AGENTS.md"
|
||||||
SKILLS_VFS_MOUNT = "/skills/"
|
|
||||||
|
|
||||||
# ID курса KFU-26-1 на platform.brojs.ru
|
# ID курса KFU-26-1 на platform.brojs.ru
|
||||||
COURSE_ID = "698b49da77cb6d4d2e43ce78"
|
COURSE_ID = "698b49da77cb6d4d2e43ce78"
|
||||||
|
|||||||
@@ -183,8 +183,8 @@ def _fix_prompt(task: TaskInfo, repo_name: str, v: dict) -> str:
|
|||||||
|
|
||||||
MAX_RETRIES = 2
|
MAX_RETRIES = 2
|
||||||
RATE_LIMIT_RETRIES = 5 # сколько раз повторять при 429
|
RATE_LIMIT_RETRIES = 5 # сколько раз повторять при 429
|
||||||
RATE_LIMIT_PAUSE = 90 # секунд ожидания перед повтором
|
RATE_LIMIT_PAUSE = 10 # секунд ожидания перед повтором
|
||||||
TASK_PAUSE = 15 # пауза между заданиями (снижает давление на rate limit)
|
TASK_PAUSE = 10 # пауза между заданиями (снижает давление на rate limit)
|
||||||
|
|
||||||
|
|
||||||
def _is_rate_limit(exc) -> bool:
|
def _is_rate_limit(exc) -> bool:
|
||||||
@@ -251,7 +251,7 @@ async def fetch_tasks(state: PipelineState) -> dict:
|
|||||||
if hasattr(e, "exceptions"):
|
if hasattr(e, "exceptions"):
|
||||||
full += " ".join(repr(sub) for sub in e.exceptions)
|
full += " ".join(repr(sub) for sub in e.exceptions)
|
||||||
if "429" in full and attempt < 5:
|
if "429" in full and attempt < 5:
|
||||||
wait = 90 * attempt
|
wait = 10 * attempt
|
||||||
print(f"[pipeline] fetch_tasks: rate limit, жду {wait}с (попытка {attempt}/5)...")
|
print(f"[pipeline] fetch_tasks: rate limit, жду {wait}с (попытка {attempt}/5)...")
|
||||||
await asyncio.sleep(wait)
|
await asyncio.sleep(wait)
|
||||||
else:
|
else:
|
||||||
@@ -314,8 +314,8 @@ async def _force_submit(task_id: str, repo_url: str) -> None:
|
|||||||
if hasattr(e, "exceptions"):
|
if hasattr(e, "exceptions"):
|
||||||
full += " ".join(repr(sub) for sub in e.exceptions)
|
full += " ".join(repr(sub) for sub in e.exceptions)
|
||||||
if "429" in full and attempt < 5:
|
if "429" in full and attempt < 5:
|
||||||
print(f"[pipeline] [AUTO] Rate limit, жду 70с...")
|
print(f"[pipeline] [AUTO] Rate limit, жду 10с...")
|
||||||
await asyncio.sleep(70)
|
await asyncio.sleep(10)
|
||||||
else:
|
else:
|
||||||
print(f"[pipeline] [AUTO] Ошибка при сдаче: {full[:200]}")
|
print(f"[pipeline] [AUTO] Ошибка при сдаче: {full[:200]}")
|
||||||
return
|
return
|
||||||
@@ -346,7 +346,7 @@ async def process_one_task(state: PipelineState) -> dict:
|
|||||||
f"4. В requirements.txt включай ТОЛЬКО пакеты из стека задания — не добавляй ничего лишнего.\n\n"
|
f"4. В requirements.txt включай ТОЛЬКО пакеты из стека задания — не добавляй ничего лишнего.\n\n"
|
||||||
f"ВАЖНО: в конце обязательно вызови task_submit(taskId='{task_id}', confirmSubmit=True)!"
|
f"ВАЖНО: в конце обязательно вызови task_submit(taskId='{task_id}', confirmSubmit=True)!"
|
||||||
)
|
)
|
||||||
agent_to_use = homework_direct_agent
|
agent_to_use = rework_agent
|
||||||
else:
|
else:
|
||||||
prompt = (
|
prompt = (
|
||||||
f"Выполни задание.\n\n"
|
f"Выполни задание.\n\n"
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class ValidateJournalWorkflowMiddleware(AgentMiddleware[AgentState[Any], Any]):
|
|||||||
return ToolMessage(
|
return ToolMessage(
|
||||||
content=(
|
content=(
|
||||||
f"Ошибка rate limit (429) от Journal API при вызове {tool_name}.\n"
|
f"Ошибка rate limit (429) от Journal API при вызове {tool_name}.\n"
|
||||||
"Подожди ~60 секунд и повтори этот же вызов."
|
"Подожди ~10 секунд и повтори этот же вызов."
|
||||||
),
|
),
|
||||||
tool_call_id=request.tool_call["id"],
|
tool_call_id=request.tool_call["id"],
|
||||||
name=tool_name,
|
name=tool_name,
|
||||||
|
|||||||
@@ -3,19 +3,9 @@ from src.agent.llm import llm
|
|||||||
from src.agent.prompts import (
|
from src.agent.prompts import (
|
||||||
homework_doing_instructions,
|
homework_doing_instructions,
|
||||||
journal_tasks_submissions_instructions,
|
journal_tasks_submissions_instructions,
|
||||||
research_instructions,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
subagent_specs_without_tools: list[dict] = [
|
subagent_specs_without_tools: list[dict] = [
|
||||||
{
|
|
||||||
"name": "web_search",
|
|
||||||
"description": (
|
|
||||||
"Ищет информацию в интернете, находит URL и открывает страницы, "
|
|
||||||
"чтобы извлекать факты только из реально прочитанного контента"
|
|
||||||
),
|
|
||||||
"model": llm,
|
|
||||||
"system_prompt": research_instructions,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "homework_doing",
|
"name": "homework_doing",
|
||||||
"description": (
|
"description": (
|
||||||
|
|||||||
@@ -0,0 +1,454 @@
|
|||||||
|
import asyncio
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import queue as q_mod
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import streamlit as st
|
||||||
|
from langchain_core.callbacks.base import BaseCallbackHandler
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
|
||||||
|
OWNER = "KirillKutlakhmetov"
|
||||||
|
|
||||||
|
# ── Page config ───────────────────────────────────────────────────────────────
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="BroJS Agent",
|
||||||
|
layout="wide",
|
||||||
|
initial_sidebar_state="collapsed",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── CSS ───────────────────────────────────────────────────────────────────────
|
||||||
|
st.markdown("""
|
||||||
|
<style>
|
||||||
|
/* Steps */
|
||||||
|
.step-done {
|
||||||
|
padding: 8px 14px; border-radius: 8px; margin: 5px 0;
|
||||||
|
background: #0d2b1d; border-left: 4px solid #22c55e;
|
||||||
|
color: #4ade80; font-family: monospace; font-size: .88em;
|
||||||
|
}
|
||||||
|
.step-active {
|
||||||
|
padding: 8px 14px; border-radius: 8px; margin: 5px 0;
|
||||||
|
background: #2a1e00; border-left: 4px solid #f59e0b;
|
||||||
|
color: #fbbf24; font-family: monospace; font-size: .88em;
|
||||||
|
animation: pulse 1s infinite alternate;
|
||||||
|
}
|
||||||
|
.step-pending {
|
||||||
|
padding: 8px 14px; border-radius: 8px; margin: 5px 0;
|
||||||
|
background: #111827; border-left: 4px solid #1f2937;
|
||||||
|
color: #374151; font-family: monospace; font-size: .88em;
|
||||||
|
}
|
||||||
|
@keyframes pulse { from { opacity: 1; } to { opacity: 0.6; } }
|
||||||
|
|
||||||
|
/* Log */
|
||||||
|
.log-wrap {
|
||||||
|
background: #0b0f1a; border-radius: 10px; padding: 12px;
|
||||||
|
max-height: 340px; overflow-y: auto;
|
||||||
|
font-family: 'Courier New', monospace; font-size: .78em;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
}
|
||||||
|
.log-line { padding: 3px 0; border-bottom: 1px solid #0f172a; }
|
||||||
|
.ts { color: #374151; }
|
||||||
|
.ttool { color: #818cf8; font-weight: bold; }
|
||||||
|
.tres { color: #34d399; }
|
||||||
|
.terr { color: #f87171; }
|
||||||
|
.tthink { color: #6b7280; font-style: italic; }
|
||||||
|
|
||||||
|
/* File tabs */
|
||||||
|
.file-header {
|
||||||
|
background: #1e1b4b; border-radius: 6px 6px 0 0;
|
||||||
|
padding: 6px 14px; font-family: monospace;
|
||||||
|
font-size: .85em; color: #818cf8; border-bottom: 1px solid #312e81;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.agent-header {
|
||||||
|
background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 100%);
|
||||||
|
border-radius: 12px; padding: 20px 28px; margin-bottom: 20px;
|
||||||
|
border: 1px solid #312e81;
|
||||||
|
}
|
||||||
|
.agent-title { font-size: 1.8em; font-weight: bold; color: #e2e8f0; margin: 0; }
|
||||||
|
.agent-sub { color: #6b7280; font-size: .9em; margin-top: 4px; }
|
||||||
|
|
||||||
|
/* Status badge */
|
||||||
|
.badge-ok { background:#0d2b1d; border:1px solid #22c55e; color:#4ade80; padding:10px 18px; border-radius:8px; font-family:monospace; }
|
||||||
|
.badge-warn{ background:#2a1e00; border:1px solid #f59e0b; color:#fbbf24; padding:10px 18px; border-radius:8px; font-family:monospace; }
|
||||||
|
.badge-err { background:#2d0f0f; border:1px solid #ef4444; color:#f87171; padding:10px 18px; border-radius:8px; font-family:monospace; }
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# ── Header ────────────────────────────────────────────────────────────────────
|
||||||
|
st.markdown("""
|
||||||
|
<div class="agent-header">
|
||||||
|
<div class="agent-title">BroJS Agent</div>
|
||||||
|
<div class="agent-sub">Автоматическое выполнение заданий курса KFU-26-1 · platform.brojs.ru</div>
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# ── Cache resources ───────────────────────────────────────────────────────────
|
||||||
|
@st.cache_resource(show_spinner="Инициализация агента и MCP-подключения (~30с)...")
|
||||||
|
def get_agent():
|
||||||
|
from src.agent.agent import homework_direct_agent
|
||||||
|
from src.agent.graph.pipeline import _invoke_with_retry
|
||||||
|
return homework_direct_agent, _invoke_with_retry
|
||||||
|
|
||||||
|
|
||||||
|
@st.cache_resource(show_spinner="Загрузка pipeline...")
|
||||||
|
def get_pipeline():
|
||||||
|
from src.agent.graph.pipeline import pipeline
|
||||||
|
return pipeline
|
||||||
|
|
||||||
|
|
||||||
|
# ── Callback (realtime events → queue) ───────────────────────────────────────
|
||||||
|
class UICallback(BaseCallbackHandler):
|
||||||
|
def __init__(self, q: q_mod.Queue):
|
||||||
|
self.q = q
|
||||||
|
|
||||||
|
def _parse_inputs(self, input_str: str, kwargs: dict) -> dict:
|
||||||
|
"""Пробуем достать структурированные inputs из kwargs или распарсить строку."""
|
||||||
|
inputs = kwargs.get("inputs") or {}
|
||||||
|
if inputs:
|
||||||
|
return inputs
|
||||||
|
try:
|
||||||
|
return json.loads(input_str)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return ast.literal_eval(input_str)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def on_tool_start(self, serialized, input_str, **kwargs):
|
||||||
|
name = serialized.get("name", "?")
|
||||||
|
inputs = self._parse_inputs(str(input_str), kwargs)
|
||||||
|
self.q.put({"t": "tool_start", "name": name, "inputs": inputs})
|
||||||
|
|
||||||
|
def on_tool_end(self, output, **kwargs):
|
||||||
|
self.q.put({"t": "tool_end", "output": str(output)[:500]})
|
||||||
|
|
||||||
|
def on_tool_error(self, error, **kwargs):
|
||||||
|
self.q.put({"t": "tool_error", "msg": str(error)[:300]})
|
||||||
|
|
||||||
|
def on_llm_start(self, *a, **kw):
|
||||||
|
self.q.put({"t": "thinking"})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Steps ─────────────────────────────────────────────────────────────────────
|
||||||
|
STEPS = [
|
||||||
|
("task_get", "Читаю состояние задания"),
|
||||||
|
("task_text", "Читаю текст задания"),
|
||||||
|
("gitea_create_repo", "Создаю репозиторий"),
|
||||||
|
("gitea_write_file", "Загружаю файлы"),
|
||||||
|
("task_update_answer", "Устанавливаю ответ"),
|
||||||
|
("task_submit", "Сдаю задание"),
|
||||||
|
]
|
||||||
|
|
||||||
|
_STEP_CLS = {"done": "step-done", "active": "step-active", "pending": "step-pending"}
|
||||||
|
_STEP_ICON = {"done": "✓", "active": "⟳", "pending": "○"}
|
||||||
|
|
||||||
|
|
||||||
|
def render_steps(states: dict, file_count: int = 0) -> str:
|
||||||
|
parts = []
|
||||||
|
for key, label in STEPS:
|
||||||
|
sv = states.get(key, "pending")
|
||||||
|
cls = _STEP_CLS[sv]
|
||||||
|
ico = _STEP_ICON[sv]
|
||||||
|
extra = f" <span style='opacity:.6'>({file_count} файлов)</span>" if key == "gitea_write_file" and file_count > 0 else ""
|
||||||
|
parts.append(f'<div class="{cls}">{ico} {label}{extra}</div>')
|
||||||
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def step_key_for(tool_name: str) -> str | None:
|
||||||
|
for key, _ in STEPS:
|
||||||
|
if key in tool_name:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ── Background runner ─────────────────────────────────────────────────────────
|
||||||
|
def run_agent_background(agent, messages, config, q: q_mod.Queue, cb):
|
||||||
|
async def _inner():
|
||||||
|
try:
|
||||||
|
result = await agent.ainvoke(messages, {**config, "callbacks": [cb]})
|
||||||
|
q.put({"t": "done", "result": result})
|
||||||
|
except Exception as e:
|
||||||
|
q.put({"t": "fatal", "msg": str(e)})
|
||||||
|
asyncio.run(_inner())
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Tabs
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
tab1, tab2 = st.tabs(["Одно задание", "Все задания (Pipeline)"])
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tab 1 — одно задание ──────────────────────────────────────────────────────
|
||||||
|
with tab1:
|
||||||
|
task_id_raw = st.text_input(
|
||||||
|
"Task ID",
|
||||||
|
placeholder="6a1867fa8a94f887e50d52bd",
|
||||||
|
help="Скопируй из URL на platform.brojs.ru",
|
||||||
|
label_visibility="visible",
|
||||||
|
)
|
||||||
|
go = st.button("Выполнить задание", type="primary", use_container_width=True)
|
||||||
|
|
||||||
|
if go:
|
||||||
|
task_id = task_id_raw.strip()
|
||||||
|
if not task_id:
|
||||||
|
st.warning("Введи Task ID")
|
||||||
|
else:
|
||||||
|
repo = f"task-{task_id}"
|
||||||
|
url = f"https://git.brojs.ru/{OWNER}/{repo}"
|
||||||
|
|
||||||
|
prompt = f"""Выполни задание из курса BroJS.
|
||||||
|
|
||||||
|
taskId: {task_id}
|
||||||
|
owner: {OWNER}
|
||||||
|
|
||||||
|
ШАГ 1. mcp__journal-bh-professor__task_get(taskId='{task_id}')
|
||||||
|
→ Смотри поле answer.content:
|
||||||
|
• Пусто → ПЕРВАЯ СДАЧА (шаг 2а)
|
||||||
|
• Ссылка на репо → ПЕРЕСДАЧА (шаг 2б)
|
||||||
|
→ Читай комментарии преподавателя — они в приоритете
|
||||||
|
|
||||||
|
ШАГ 2а (ПЕРВАЯ СДАЧА):
|
||||||
|
a. mcp__journal-bh-professor__task_text(taskId='{task_id}') — прочитай полный текст задания
|
||||||
|
b. gitea_create_repo(name='{repo}') — создай репозиторий
|
||||||
|
c. gitea_write_file — загрузи ВСЕ файлы (main.py, requirements.txt, README.md + модули). Полный код, без заглушек.
|
||||||
|
|
||||||
|
ШАГ 2б (ПЕРЕСДАЧА):
|
||||||
|
a. mcp__journal-bh-professor__task_text(taskId='{task_id}') — прочитай оригинальное задание
|
||||||
|
b. Сравни комментарий с требованиями:
|
||||||
|
• Противоречит заданию → task_comment с объяснением + цитата, НЕ меняй код
|
||||||
|
• Реальная ошибка → исправь через gitea_write_file
|
||||||
|
|
||||||
|
ШАГ 3. mcp__journal-bh-professor__task_update_answer(taskId='{task_id}', answerType='link', content='{url}')
|
||||||
|
ШАГ 4. mcp__journal-bh-professor__task_submit(taskId='{task_id}', confirmSubmit=True)"""
|
||||||
|
|
||||||
|
config = {"configurable": {"thread_id": f"ui-{task_id}-{int(time.time())}"}}
|
||||||
|
|
||||||
|
# ── UI placeholders ────────────────────────────────────────────
|
||||||
|
col_left, col_right = st.columns([1, 2])
|
||||||
|
with col_left:
|
||||||
|
st.markdown("**Pipeline**")
|
||||||
|
steps_ph = st.empty()
|
||||||
|
with col_right:
|
||||||
|
st.markdown("**Лог событий**")
|
||||||
|
log_ph = st.empty()
|
||||||
|
|
||||||
|
st.markdown("**Код (последний записанный файл)**")
|
||||||
|
code_header_ph = st.empty()
|
||||||
|
code_ph = st.empty()
|
||||||
|
status_ph = st.empty()
|
||||||
|
|
||||||
|
# ── Init ───────────────────────────────────────────────────────
|
||||||
|
step_states = {k: "pending" for k, _ in STEPS}
|
||||||
|
logs: list[str] = []
|
||||||
|
files: dict[str, str] = {}
|
||||||
|
file_count = 0
|
||||||
|
active_key = None
|
||||||
|
thinking_shown = False
|
||||||
|
|
||||||
|
steps_ph.markdown(render_steps(step_states), unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# ── Start thread ───────────────────────────────────────────────
|
||||||
|
update_q: q_mod.Queue = q_mod.Queue()
|
||||||
|
agent, _ = get_agent()
|
||||||
|
cb = UICallback(update_q)
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=run_agent_background,
|
||||||
|
args=(agent, {"messages": [HumanMessage(content=prompt)]}, config, update_q, cb),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
# ── Poll loop ──────────────────────────────────────────────────
|
||||||
|
final_result = None
|
||||||
|
fatal_error = None
|
||||||
|
|
||||||
|
while thread.is_alive() or not update_q.empty():
|
||||||
|
dirty = False
|
||||||
|
|
||||||
|
while not update_q.empty():
|
||||||
|
ev = update_q.get_nowait()
|
||||||
|
ts = datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
if ev["t"] == "thinking":
|
||||||
|
if not thinking_shown:
|
||||||
|
logs.append(
|
||||||
|
f'<div class="log-line">'
|
||||||
|
f'<span class="ts">{ts}</span> '
|
||||||
|
f'<span class="tthink">модель думает...</span>'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
thinking_shown = True
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
elif ev["t"] == "tool_start":
|
||||||
|
thinking_shown = False
|
||||||
|
name = ev["name"]
|
||||||
|
inputs = ev.get("inputs", {})
|
||||||
|
key = step_key_for(name)
|
||||||
|
|
||||||
|
if key:
|
||||||
|
if active_key and active_key != key:
|
||||||
|
step_states[active_key] = "done"
|
||||||
|
step_states[key] = "active"
|
||||||
|
active_key = key
|
||||||
|
|
||||||
|
if key == "gitea_write_file":
|
||||||
|
file_count += 1
|
||||||
|
path = inputs.get("path", "")
|
||||||
|
content = inputs.get("content", "")
|
||||||
|
if path and content:
|
||||||
|
files[path] = content
|
||||||
|
|
||||||
|
short = name.replace("mcp__journal-bh-professor__", "mcp::")
|
||||||
|
path = inputs.get("path", "")
|
||||||
|
finfo = f" <b style='color:#60a5fa'>{path}</b>" if path else ""
|
||||||
|
logs.append(
|
||||||
|
f'<div class="log-line">'
|
||||||
|
f'<span class="ts">{ts}</span> '
|
||||||
|
f'<span class="ttool">[tool] {short}</span>{finfo}'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
elif ev["t"] == "tool_end":
|
||||||
|
out = ev["output"][:140].replace("<", "<").replace(">", ">")
|
||||||
|
logs.append(
|
||||||
|
f'<div class="log-line">'
|
||||||
|
f'<span class="ts">{ts}</span> '
|
||||||
|
f'<span class="tres">-> {out}</span>'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
elif ev["t"] == "tool_error":
|
||||||
|
if active_key:
|
||||||
|
step_states[active_key] = "pending"
|
||||||
|
msg = ev["msg"][:140].replace("<", "<").replace(">", ">")
|
||||||
|
logs.append(
|
||||||
|
f'<div class="log-line">'
|
||||||
|
f'<span class="ts">{ts}</span> '
|
||||||
|
f'<span class="terr">[error] {msg}</span>'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
elif ev["t"] == "done":
|
||||||
|
final_result = ev["result"]
|
||||||
|
if active_key:
|
||||||
|
step_states[active_key] = "done"
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
elif ev["t"] == "fatal":
|
||||||
|
fatal_error = ev["msg"]
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
if dirty:
|
||||||
|
# Steps
|
||||||
|
steps_ph.markdown(
|
||||||
|
render_steps(step_states, file_count),
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
# Log
|
||||||
|
log_ph.markdown(
|
||||||
|
'<div class="log-wrap">' + "".join(logs[-60:]) + '</div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
# Code panel — показываем последний файл
|
||||||
|
if files:
|
||||||
|
last_path = list(files)[-1]
|
||||||
|
lang = "python" if last_path.endswith(".py") else (
|
||||||
|
"text" if last_path.endswith(".txt") else "markdown"
|
||||||
|
)
|
||||||
|
code_header_ph.markdown(
|
||||||
|
f'<div class="file-header">{last_path} '
|
||||||
|
f'<span style="opacity:.5">({len(files)} файлов загружено)</span></div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
code_ph.code(files[last_path], language=lang)
|
||||||
|
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
|
# ── Final ──────────────────────────────────────────────────────
|
||||||
|
if fatal_error:
|
||||||
|
status_ph.markdown(
|
||||||
|
f'<div class="badge-err">Ошибка: {fatal_error[:200]}</div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
elif final_result is not None:
|
||||||
|
msgs = final_result.get("messages", [])
|
||||||
|
submitted = any(
|
||||||
|
"task_submit" in tc.get("name", "")
|
||||||
|
for m in msgs
|
||||||
|
for tc in getattr(m, "tool_calls", [])
|
||||||
|
)
|
||||||
|
# финально помечаем все шаги
|
||||||
|
for k, _ in STEPS:
|
||||||
|
if step_states[k] in ("active", "done"):
|
||||||
|
step_states[k] = "done"
|
||||||
|
steps_ph.markdown(render_steps(step_states, file_count), unsafe_allow_html=True)
|
||||||
|
|
||||||
|
if submitted:
|
||||||
|
status_ph.markdown(
|
||||||
|
f'<div class="badge-ok">Задание сдано! '
|
||||||
|
f'<a href="{url}" target="_blank" style="color:#4ade80">'
|
||||||
|
f'Открыть репозиторий</a></div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
status_ph.markdown(
|
||||||
|
'<div class="badge-warn">Агент не вызвал task_submit — проверь лог</div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
status_ph.markdown(
|
||||||
|
'<div class="badge-warn">Агент завершился без результата</div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tab 2 — pipeline ──────────────────────────────────────────────────────────
|
||||||
|
with tab2:
|
||||||
|
st.info(
|
||||||
|
"Загружает **все незакрытые задания** курса KFU-26-1 и выполняет по очереди. "
|
||||||
|
"Может работать долго — не закрывай вкладку."
|
||||||
|
)
|
||||||
|
|
||||||
|
if st.button("Запустить Pipeline", type="primary", use_container_width=True):
|
||||||
|
pl = get_pipeline()
|
||||||
|
result_ph = st.empty()
|
||||||
|
with st.spinner("Pipeline работает..."):
|
||||||
|
try:
|
||||||
|
res = asyncio.run(pl.ainvoke({
|
||||||
|
"tasks": [], "current_index": 0, "results": [], "errors": [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
results = res.get("results", [])
|
||||||
|
errors = res.get("errors", [])
|
||||||
|
|
||||||
|
lines = [f"### Выполнено заданий: {len(results)}\n"]
|
||||||
|
for r in results:
|
||||||
|
tid = r.get("task_id", "")
|
||||||
|
repo_link = f"https://git.brojs.ru/{OWNER}/task-{tid}"
|
||||||
|
lines.append(
|
||||||
|
f"- `{tid[:8]}...` [{r.get('status','?')}] "
|
||||||
|
f"mode=**{r.get('mode','?')}** "
|
||||||
|
f"retries={r.get('retries', 0)} "
|
||||||
|
f"[→ репо]({repo_link})"
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
lines.append(f"\n### Ошибки ({len(errors)})")
|
||||||
|
for e in errors:
|
||||||
|
lines.append(f"- {e}")
|
||||||
|
|
||||||
|
result_ph.markdown("\n".join(lines))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result_ph.error(str(e))
|
||||||
+362
@@ -0,0 +1,362 @@
|
|||||||
|
"""
|
||||||
|
Демо-режим UI — симулирует работу агента без реальных API вызовов.
|
||||||
|
Запусти: streamlit run ui_demo.py
|
||||||
|
"""
|
||||||
|
import queue as q_mod
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
OWNER = "KirillKutlakhmetov"
|
||||||
|
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="BroJS Agent — DEMO",
|
||||||
|
page_icon="🤖",
|
||||||
|
layout="wide",
|
||||||
|
initial_sidebar_state="collapsed",
|
||||||
|
)
|
||||||
|
|
||||||
|
st.markdown("""
|
||||||
|
<style>
|
||||||
|
.step-done {
|
||||||
|
padding: 8px 14px; border-radius: 8px; margin: 5px 0;
|
||||||
|
background: #0d2b1d; border-left: 4px solid #22c55e;
|
||||||
|
color: #4ade80; font-family: monospace; font-size: .88em;
|
||||||
|
}
|
||||||
|
.step-active {
|
||||||
|
padding: 8px 14px; border-radius: 8px; margin: 5px 0;
|
||||||
|
background: #2a1e00; border-left: 4px solid #f59e0b;
|
||||||
|
color: #fbbf24; font-family: monospace; font-size: .88em;
|
||||||
|
animation: pulse 1s infinite alternate;
|
||||||
|
}
|
||||||
|
.step-pending {
|
||||||
|
padding: 8px 14px; border-radius: 8px; margin: 5px 0;
|
||||||
|
background: #111827; border-left: 4px solid #1f2937;
|
||||||
|
color: #374151; font-family: monospace; font-size: .88em;
|
||||||
|
}
|
||||||
|
@keyframes pulse { from { opacity: 1; } to { opacity: 0.6; } }
|
||||||
|
|
||||||
|
.log-wrap {
|
||||||
|
background: #0b0f1a; border-radius: 10px; padding: 12px;
|
||||||
|
max-height: 340px; overflow-y: auto;
|
||||||
|
font-family: 'Courier New', monospace; font-size: .78em;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
}
|
||||||
|
.log-line { padding: 3px 0; border-bottom: 1px solid #0f172a; }
|
||||||
|
.ts { color: #374151; }
|
||||||
|
.ttool { color: #818cf8; font-weight: bold; }
|
||||||
|
.tres { color: #34d399; }
|
||||||
|
.tthink { color: #6b7280; font-style: italic; }
|
||||||
|
|
||||||
|
.file-header {
|
||||||
|
background: #1e1b4b; border-radius: 6px 6px 0 0;
|
||||||
|
padding: 6px 14px; font-family: monospace;
|
||||||
|
font-size: .85em; color: #818cf8; border-bottom: 1px solid #312e81;
|
||||||
|
}
|
||||||
|
.agent-header {
|
||||||
|
background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 100%);
|
||||||
|
border-radius: 12px; padding: 20px 28px; margin-bottom: 20px;
|
||||||
|
border: 1px solid #312e81;
|
||||||
|
}
|
||||||
|
.agent-title { font-size: 1.8em; font-weight: bold; color: #e2e8f0; margin: 0; }
|
||||||
|
.agent-sub { color: #6b7280; font-size: .9em; margin-top: 4px; }
|
||||||
|
.badge-ok { background:#0d2b1d; border:1px solid #22c55e; color:#4ade80; padding:10px 18px; border-radius:8px; font-family:monospace; }
|
||||||
|
.demo-banner { background:#1e1b4b; border:1px solid #4f46e5; color:#a5b4fc; padding:8px 16px; border-radius:8px; font-size:.85em; margin-bottom:12px; }
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.markdown("""
|
||||||
|
<div class="agent-header">
|
||||||
|
<div class="agent-title">🤖 BroJS Agent</div>
|
||||||
|
<div class="agent-sub">Автоматическое выполнение заданий курса KFU-26-1 · platform.brojs.ru</div>
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.markdown('<div class="demo-banner">🎬 DEMO-режим — реальные API не вызываются, показывает как выглядит интерфейс в работе</div>', unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# ── Steps ─────────────────────────────────────────────────────────────────────
|
||||||
|
STEPS = [
|
||||||
|
("task_get", "📋 Читаю состояние задания"),
|
||||||
|
("task_text", "📄 Читаю текст задания"),
|
||||||
|
("gitea_create_repo", "📁 Создаю репозиторий"),
|
||||||
|
("gitea_write_file", "💾 Загружаю файлы"),
|
||||||
|
("task_update_answer", "🔗 Устанавливаю ответ"),
|
||||||
|
("task_submit", "🚀 Сдаю задание"),
|
||||||
|
]
|
||||||
|
_CLS = {"done": "step-done", "active": "step-active", "pending": "step-pending"}
|
||||||
|
_ICON = {"done": "✓", "active": "⟳", "pending": "○"}
|
||||||
|
|
||||||
|
|
||||||
|
def render_steps(states, file_count=0):
|
||||||
|
parts = []
|
||||||
|
for key, label in STEPS:
|
||||||
|
sv = states.get(key, "pending")
|
||||||
|
extra = (
|
||||||
|
f" <span style='opacity:.6'>({file_count} файлов)</span>"
|
||||||
|
if key == "gitea_write_file" and file_count > 0 else ""
|
||||||
|
)
|
||||||
|
parts.append(f'<div class="{_CLS[sv]}">{_ICON[sv]} {label}{extra}</div>')
|
||||||
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Фейковые события ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
FAKE_MAIN_PY = '''\
|
||||||
|
import os, asyncio
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
from langchain.agents import create_agent
|
||||||
|
from langchain.tools import tool
|
||||||
|
from langchain_qdrant import QdrantVectorStore
|
||||||
|
from qdrant_client import QdrantClient
|
||||||
|
from qdrant_client.models import Distance, VectorParams
|
||||||
|
|
||||||
|
llm = ChatOpenAI(
|
||||||
|
model="openai/gpt-oss-20b:free",
|
||||||
|
base_url="https://platform.brojs.ru/jrnl-bh/api/inference/v1",
|
||||||
|
api_key=os.getenv("JOURNAL_MCP_PAT"),
|
||||||
|
temperature=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
client = QdrantClient(":memory:")
|
||||||
|
client.create_collection(
|
||||||
|
"knowledge",
|
||||||
|
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
|
||||||
|
)
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def search_kb(query: str) -> str:
|
||||||
|
"""Search the knowledge base."""
|
||||||
|
results = vector_store.similarity_search(query, k=5)
|
||||||
|
if not results:
|
||||||
|
return "No relevant documents found."
|
||||||
|
return "\\n\\n".join(f"{i+1}. {d.page_content}" for i, d in enumerate(results))
|
||||||
|
|
||||||
|
agent = create_agent(
|
||||||
|
llm=llm,
|
||||||
|
tools=[search_kb],
|
||||||
|
system_prompt="You are a helpful RAG assistant.",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
result = await agent.ainvoke(
|
||||||
|
{"messages": [HumanMessage(content="What is LangChain?")]},
|
||||||
|
{"configurable": {"thread_id": "demo-1"}},
|
||||||
|
)
|
||||||
|
print(result["messages"][-1].content)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
'''
|
||||||
|
|
||||||
|
FAKE_REQUIREMENTS = """\
|
||||||
|
langchain-core>=0.3.0
|
||||||
|
langchain-openai>=0.3.0
|
||||||
|
langgraph>=0.2.0
|
||||||
|
langchain-qdrant>=0.1.0
|
||||||
|
qdrant-client>=1.7.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
FAKE_README = """\
|
||||||
|
# RAG-агент с Qdrant
|
||||||
|
|
||||||
|
AI-ассистент с векторным поиском через Qdrant.
|
||||||
|
|
||||||
|
## Стек
|
||||||
|
| Компонент | Технология |
|
||||||
|
|-----------|-----------|
|
||||||
|
| LLM | BroJS gpt-oss-20b |
|
||||||
|
| Векторное хранилище | Qdrant in-memory |
|
||||||
|
| Фреймворк | LangChain + LangGraph |
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
FAKE_EVENTS = [
|
||||||
|
{"delay": 0.5, "t": "thinking"},
|
||||||
|
{"delay": 1.0, "t": "tool_start", "name": "mcp__journal-bh-professor__task_get",
|
||||||
|
"inputs": {"taskId": "6a1867fa8a94f887e50d52bd"}},
|
||||||
|
{"delay": 1.2, "t": "tool_end", "output": '{"status": "todo", "answer": {"content": ""}, "comments": []}'},
|
||||||
|
{"delay": 0.4, "t": "thinking"},
|
||||||
|
{"delay": 0.8, "t": "tool_start", "name": "mcp__journal-bh-professor__task_text",
|
||||||
|
"inputs": {"taskId": "6a1867fa8a94f887e50d52bd"}},
|
||||||
|
{"delay": 1.1, "t": "tool_end", "output": "Создай RAG-агента с векторным хранилищем Qdrant и поиском по базе знаний..."},
|
||||||
|
{"delay": 0.6, "t": "thinking"},
|
||||||
|
{"delay": 1.5, "t": "tool_start", "name": "gitea_create_repo",
|
||||||
|
"inputs": {"name": "task-6a1867fa8a94f887e50d52bd", "private": False}},
|
||||||
|
{"delay": 0.9, "t": "tool_end", "output": "Репозиторий создан: https://git.brojs.ru/KirillKutlakhmetov/task-6a1867fa..."},
|
||||||
|
{"delay": 0.3, "t": "thinking"},
|
||||||
|
{"delay": 0.8, "t": "tool_start", "name": "gitea_write_file",
|
||||||
|
"inputs": {"repo": "task-6a1867fa...", "path": "main.py",
|
||||||
|
"content": FAKE_MAIN_PY, "message": "add main.py"}},
|
||||||
|
{"delay": 0.7, "t": "tool_end", "output": "Файл main.py создан в KirillKutlakhmetov/task-6a1867fa... (commit: a1b2c3d4)"},
|
||||||
|
{"delay": 0.5, "t": "tool_start", "name": "gitea_write_file",
|
||||||
|
"inputs": {"repo": "task-6a1867fa...", "path": "requirements.txt",
|
||||||
|
"content": FAKE_REQUIREMENTS, "message": "add requirements.txt"}},
|
||||||
|
{"delay": 0.6, "t": "tool_end", "output": "Файл requirements.txt создан в KirillKutlakhmetov/task-6a1867fa... (commit: b2c3d4e5)"},
|
||||||
|
{"delay": 0.5, "t": "tool_start", "name": "gitea_write_file",
|
||||||
|
"inputs": {"repo": "task-6a1867fa...", "path": "README.md",
|
||||||
|
"content": FAKE_README, "message": "add README.md"}},
|
||||||
|
{"delay": 0.6, "t": "tool_end", "output": "Файл README.md создан в KirillKutlakhmetov/task-6a1867fa... (commit: c3d4e5f6)"},
|
||||||
|
{"delay": 0.4, "t": "thinking"},
|
||||||
|
{"delay": 0.9, "t": "tool_start", "name": "mcp__journal-bh-professor__task_update_answer",
|
||||||
|
"inputs": {"taskId": "6a1867fa8a94f887e50d52bd", "answerType": "link",
|
||||||
|
"content": "https://git.brojs.ru/KirillKutlakhmetov/task-6a1867fa8a94f887e50d52bd"}},
|
||||||
|
{"delay": 0.8, "t": "tool_end", "output": '{"success": true, "message": "Answer updated"}'},
|
||||||
|
{"delay": 0.3, "t": "thinking"},
|
||||||
|
{"delay": 0.7, "t": "tool_start", "name": "mcp__journal-bh-professor__task_submit",
|
||||||
|
"inputs": {"taskId": "6a1867fa8a94f887e50d52bd", "confirmSubmit": True}},
|
||||||
|
{"delay": 1.0, "t": "tool_end", "output": '{"success": true, "message": "Task submitted for review"}'},
|
||||||
|
{"delay": 0.3, "t": "done"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_step_key(tool_name):
|
||||||
|
for key, _ in STEPS:
|
||||||
|
if key in tool_name:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def run_fake_agent(q: q_mod.Queue):
|
||||||
|
for ev in FAKE_EVENTS:
|
||||||
|
time.sleep(ev["delay"])
|
||||||
|
q.put(ev)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Layout ────────────────────────────────────────────────────────────────────
|
||||||
|
task_id = "6a1867fa8a94f887e50d52bd"
|
||||||
|
repo = f"task-{task_id}"
|
||||||
|
url = f"https://git.brojs.ru/{OWNER}/{repo}"
|
||||||
|
|
||||||
|
col_input, _ = st.columns([2, 1])
|
||||||
|
with col_input:
|
||||||
|
st.text_input("Task ID", value=task_id, disabled=True)
|
||||||
|
|
||||||
|
go = st.button("▶️ Выполнить задание (DEMO)", type="primary", use_container_width=True)
|
||||||
|
|
||||||
|
if go:
|
||||||
|
col_left, col_right = st.columns([1, 2])
|
||||||
|
with col_left:
|
||||||
|
st.markdown("**Pipeline**")
|
||||||
|
steps_ph = st.empty()
|
||||||
|
with col_right:
|
||||||
|
st.markdown("**Лог событий**")
|
||||||
|
log_ph = st.empty()
|
||||||
|
|
||||||
|
st.markdown("**Код (последний записанный файл)**")
|
||||||
|
code_header_ph = st.empty()
|
||||||
|
code_ph = st.empty()
|
||||||
|
status_ph = st.empty()
|
||||||
|
|
||||||
|
step_states = {k: "pending" for k, _ in STEPS}
|
||||||
|
logs: list[str] = []
|
||||||
|
files: dict[str, str] = {}
|
||||||
|
file_count = 0
|
||||||
|
active_key = None
|
||||||
|
thinking_shown = False
|
||||||
|
|
||||||
|
steps_ph.markdown(render_steps(step_states), unsafe_allow_html=True)
|
||||||
|
|
||||||
|
update_q: q_mod.Queue = q_mod.Queue()
|
||||||
|
t = threading.Thread(target=run_fake_agent, args=(update_q,), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
finished = False
|
||||||
|
while not finished:
|
||||||
|
dirty = False
|
||||||
|
while not update_q.empty():
|
||||||
|
ev = update_q.get_nowait()
|
||||||
|
ts = datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
if ev["t"] == "thinking":
|
||||||
|
if not thinking_shown:
|
||||||
|
logs.append(
|
||||||
|
f'<div class="log-line"><span class="ts">{ts}</span> '
|
||||||
|
f'<span class="tthink">🤔 модель думает...</span></div>'
|
||||||
|
)
|
||||||
|
thinking_shown = True
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
elif ev["t"] == "tool_start":
|
||||||
|
thinking_shown = False
|
||||||
|
name = ev["name"]
|
||||||
|
inputs = ev.get("inputs", {})
|
||||||
|
key = _fake_step_key(name)
|
||||||
|
|
||||||
|
if key:
|
||||||
|
if active_key and active_key != key:
|
||||||
|
step_states[active_key] = "done"
|
||||||
|
step_states[key] = "active"
|
||||||
|
active_key = key
|
||||||
|
|
||||||
|
if key == "gitea_write_file":
|
||||||
|
file_count += 1
|
||||||
|
path = inputs.get("path", "")
|
||||||
|
content = inputs.get("content", "")
|
||||||
|
if path and content:
|
||||||
|
files[path] = content
|
||||||
|
|
||||||
|
short = name.replace("mcp__journal-bh-professor__", "mcp::")
|
||||||
|
path = inputs.get("path", "")
|
||||||
|
finfo = f" <b style='color:#60a5fa'>{path}</b>" if path else ""
|
||||||
|
logs.append(
|
||||||
|
f'<div class="log-line"><span class="ts">{ts}</span> '
|
||||||
|
f'<span class="ttool">🔧 {short}</span>{finfo}</div>'
|
||||||
|
)
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
elif ev["t"] == "tool_end":
|
||||||
|
out = ev["output"][:140].replace("<", "<").replace(">", ">")
|
||||||
|
logs.append(
|
||||||
|
f'<div class="log-line"><span class="ts">{ts}</span> '
|
||||||
|
f'<span class="tres">↩ {out}</span></div>'
|
||||||
|
)
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
elif ev["t"] == "done":
|
||||||
|
if active_key:
|
||||||
|
step_states[active_key] = "done"
|
||||||
|
finished = True
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
if dirty:
|
||||||
|
steps_ph.markdown(render_steps(step_states, file_count), unsafe_allow_html=True)
|
||||||
|
log_ph.markdown(
|
||||||
|
'<div class="log-wrap">' + "".join(logs[-60:]) + '</div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
if files:
|
||||||
|
last_path = list(files)[-1]
|
||||||
|
lang = "python" if last_path.endswith(".py") else (
|
||||||
|
"text" if last_path.endswith(".txt") else "markdown"
|
||||||
|
)
|
||||||
|
code_header_ph.markdown(
|
||||||
|
f'<div class="file-header">📄 {last_path} '
|
||||||
|
f'<span style="opacity:.5">({len(files)} файлов загружено)</span></div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
code_ph.code(files[last_path], language=lang)
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# финал
|
||||||
|
for k, _ in STEPS:
|
||||||
|
step_states[k] = "done"
|
||||||
|
steps_ph.markdown(render_steps(step_states, file_count), unsafe_allow_html=True)
|
||||||
|
status_ph.markdown(
|
||||||
|
f'<div class="badge-ok">✓ Задание сдано! '
|
||||||
|
f'<a href="{url}" target="_blank" style="color:#4ade80">Открыть репозиторий →</a></div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user