feat: LLM-first architecture — agent orchestrates everything via solve_task tool

- cli.py: cmd_run() now uses agent.ainvoke() instead of pipeline
- ui.py: 'run all' button and chat use main agent; status tab fixed (thread + st.rerun)
- src/agent/runner_tools.py: new solve_task tool wrapping reliable single-task execution
- src/agent/agent.py: solve_task added to main agent tools
- src/agent/prompts.py: main_agent_instructions rewritten for LLM-first orchestration

LLM decides what to do and in what order; Python tools are just hands.
This commit is contained in:
2026-06-11 18:43:40 +03:00
parent 0a2b139f9b
commit ba83d0cfe4
5 changed files with 237 additions and 90 deletions
+73 -51
View File
@@ -94,14 +94,14 @@ st.markdown("""
@st.cache_resource(show_spinner="Инициализация агента (~30с)...")
def get_agent():
from src.agent.agent import homework_direct_agent
return homework_direct_agent
from src.agent.agent import agent
return agent
@st.cache_resource(show_spinner="Загрузка pipeline...")
def get_pipeline():
from src.agent.graph.pipeline import pipeline
return pipeline
@st.cache_resource(show_spinner="Загрузка главного агента...")
def get_main_agent():
from src.agent.agent import agent
return agent
# ---------------------------------------------------------------------------
# Callback — перехватывает события агента и шлёт в очередь
@@ -425,29 +425,45 @@ with tab_pipeline:
st.divider()
st.subheader("Запустить все todo-задания")
if st.button("⚡ Запустить pipeline для всех заданий", use_container_width=True):
pl = get_pipeline()
if st.button("⚡ Запустить агент для всех заданий", use_container_width=True):
main_ag = get_main_agent()
evq_pl = queue.Queue()
cb_pl = AgentCallback(evq_pl)
all_pl_events: list[dict] = []
_pl_state = {"result": None, "error": None}
def _run_pipeline():
run_prompt = (
"Выполни все задания со статусом todo в курсе KFU-26-1 "
"(courseId=698b49da77cb6d4d2e43ce78).\n\n"
"Шаги:\n"
"1. Получи список заданий через mcp__journal-bh-professor__tasks_list\n"
"2. Для каждого задания со статусом todo вызови solve_task(task_id=...)\n"
"3. Выполняй строго по одному заданию, жди результата перед следующим\n"
"4. Доложи итоговые результаты"
)
def _run_all():
async def _inner():
try:
_pl_state["result"] = await pl.ainvoke(
{"tasks": [], "current_index": 0, "results": [], "errors": []},
{"callbacks": [cb_pl]},
result = await main_ag.ainvoke(
{"messages": [HumanMessage(content=run_prompt)]},
{
"configurable": {"thread_id": f"ui-run-all-{int(time.time())}"},
"callbacks": [cb_pl],
},
)
_pl_state["result"] = result
evq_pl.put({"t": "done", "result": result})
except Exception as e:
_pl_state["error"] = str(e)
evq_pl.put({"t": "fatal", "msg": str(e)})
asyncio.run(_inner())
t_pl = threading.Thread(target=_run_pipeline, daemon=True)
t_pl = threading.Thread(target=_run_all, daemon=True)
t_pl.start()
events_pl_ph = st.empty()
with st.spinner("Pipeline работает... (может занять несколько минут)"):
with st.spinner("Агент-оркестратор работает... (LLM управляет всем)"):
while t_pl.is_alive() or not evq_pl.empty():
while not evq_pl.empty():
ev = evq_pl.get_nowait()
@@ -465,7 +481,7 @@ with tab_pipeline:
events_pl_ph.empty()
if all_pl_events:
with st.expander(f"🔍 Лог pipeline ({len(all_pl_events)} событий)", expanded=False):
with st.expander(f"🔍 Лог агента ({len(all_pl_events)} событий)", expanded=False):
html = "".join(_render_event(e) for e in all_pl_events[-80:])
st.markdown(f'<div style="max-height:300px;overflow-y:auto">{html}</div>',
unsafe_allow_html=True)
@@ -473,19 +489,13 @@ with tab_pipeline:
if _pl_state["error"]:
st.error(_pl_state["error"])
elif _pl_state["result"]:
results = _pl_state["result"].get("results", [])
errors = _pl_state["result"].get("errors", [])
md = [f"### Результат: {len(results)} заданий\n"]
for r in results:
tid = r.get("task_id", "")
url = f"https://git.brojs.ru/{GITEA_OWNER}/task-{tid}"
icon = "" if r.get("status") == "ok" else ""
md.append(f"- {icon} `{tid[:8]}...` — [{r.get('status','')}]({url})")
if errors:
md.append(f"\n**Ошибки ({len(errors)}):**")
for e in errors:
md.append(f"- {e}")
st.markdown("\n".join(md))
msgs = _pl_state["result"].get("messages", [])
last = msgs[-1] if msgs else None
reply = last.content if last and hasattr(last, "content") else "Готово."
st.markdown(
f'<div class="status-ok">✅ Агент завершил работу:<br>{reply[:600]}</div>',
unsafe_allow_html=True,
)
# ══════════════════════════════════════════════════════════════════════════
@@ -497,31 +507,43 @@ with tab_status:
if st.button("🔄 Обновить статусы", type="primary"):
with st.spinner("Загружаю статусы..."):
try:
from src.agent.mcp_client import load_journal_toolsets
from src.agent.mcp_client import load_journal_toolsets
async def _fetch():
j = load_journal_toolsets()
# Инструменты имеют префикс mcp__journal-bh-professor__
tools = {t.name: t for t in j.tasks_submissions_tools}
full_name = "mcp__journal-bh-professor__tasks_list"
t = tools.get(full_name)
if not t:
# fallback: ищем по любому имени содержащему tasks_list
t = next((v for k, v in tools.items() if "tasks_list" in k), None)
if not t:
st.warning(f"Инструмент tasks_list не найден. Доступны: {list(tools.keys())}")
return []
raw = await t.ainvoke({"courseId": "698b49da77cb6d4d2e43ce78"})
text = next((x["text"] for x in raw if x.get("type") == "text"), str(raw)) if isinstance(raw, list) else str(raw)
data = json.loads(text)
return data.get("tasks", data) if isinstance(data, dict) else data
async def _fetch():
j = load_journal_toolsets()
tools = {t.name: t for t in j.tasks_submissions_tools}
full_name = "mcp__journal-bh-professor__tasks_list"
t = tools.get(full_name) or next(
(v for k, v in tools.items() if "tasks_list" in k), None
)
if not t:
return [], f"tasks_list не найден. Доступны: {list(tools.keys())}"
raw = await t.ainvoke({"courseId": "698b49da77cb6d4d2e43ce78"})
text = next((x["text"] for x in raw if x.get("type") == "text"), str(raw)) if isinstance(raw, list) else str(raw)
data = json.loads(text)
return (data.get("tasks", data) if isinstance(data, dict) else data), None
items = asyncio.run(_fetch())
st.session_state["task_statuses"] = items
except Exception as e:
st.error(str(e))
items = []
_state = {"items": [], "error": None}
def _run_fetch():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
_state["items"], _state["error"] = loop.run_until_complete(_fetch())
except Exception as e:
_state["error"] = str(e)
finally:
loop.close()
t = threading.Thread(target=_run_fetch, daemon=True)
t.start()
t.join()
if _state["error"]:
st.error(_state["error"])
else:
st.session_state["task_statuses"] = _state["items"]
st.rerun()
items = st.session_state.get("task_statuses", [])