diff --git a/ui.py b/ui.py
index 7fc7736..9db31ec 100644
--- a/ui.py
+++ b/ui.py
@@ -1,7 +1,7 @@
"""
Streamlit UI для brojs-agent.
-Запуск: streamlit run ui.py
+Запуск: python -m streamlit run ui.py
"""
import asyncio
import json
@@ -68,12 +68,16 @@ st.markdown("""
.thinking {
color: #64748b; font-style: italic; font-size: .82em; padding: 4px 0;
}
-.status-ok { background:#052e16; border:1px solid #22c55e; color:#4ade80; padding:10px 16px; border-radius:8px; }
-.status-warn { background:#1c1917; border:1px solid #f59e0b; color:#fbbf24; padding:10px 16px; border-radius:8px; }
-.status-err { background:#1c0a0a; border:1px solid #ef4444; color:#f87171; padding:10px 16px; border-radius:8px; }
-
-.chat-user { background:#1e3a5f; border-radius:12px 12px 2px 12px; padding:10px 14px; margin:6px 0; }
-.chat-agent { background:#1a1a2e; border-radius:12px 12px 12px 2px; padding:10px 14px; margin:6px 0; }
+.status-ok { background:#052e16; border:1px solid #22c55e; color:#4ade80;
+ padding:10px 16px; border-radius:8px; }
+.status-warn { background:#1c1917; border:1px solid #f59e0b; color:#fbbf24;
+ padding:10px 16px; border-radius:8px; }
+.status-err { background:#1c0a0a; border:1px solid #ef4444; color:#f87171;
+ padding:10px 16px; border-radius:8px; }
+.chat-user { background:#1e3a5f; border-radius:12px 12px 2px 12px;
+ padding:10px 14px; margin:6px 0; }
+.chat-agent { background:#1a1a2e; border-radius:12px 12px 12px 2px;
+ padding:10px 14px; margin:6px 0; }
""", unsafe_allow_html=True)
@@ -89,84 +93,118 @@ st.markdown("""
""", unsafe_allow_html=True)
# ---------------------------------------------------------------------------
-# Кэш агента
+# Кэш агента — загружается один раз при первом обращении
# ---------------------------------------------------------------------------
-@st.cache_resource(show_spinner="Инициализация агента (~30с)...")
+@st.cache_resource(show_spinner="⏳ Инициализация агента (подключение к MCP)...")
def get_agent():
from src.agent.agent import homework_direct_agent
return homework_direct_agent
-@st.cache_resource(show_spinner="Загрузка pipeline...")
+@st.cache_resource(show_spinner="⏳ Загрузка pipeline...")
def get_pipeline():
from src.agent.graph.pipeline import pipeline
return pipeline
# ---------------------------------------------------------------------------
-# Callback — перехватывает события агента и шлёт в очередь
+# Сборщик событий агента (синхронный — не нужна очередь)
# ---------------------------------------------------------------------------
-_SOLVE_TOOLS = {"validate_teacher_comment", "generate_code_solution"}
+_SOLVE_TOOLS = {"validate_teacher_comment", "generate_code_solution"}
_JOURNAL_PREFIX = "mcp__journal-bh-professor__"
-class AgentCallback(BaseCallbackHandler):
- def __init__(self, q: queue.Queue):
- self.q = q
+class AgentEventCollector(BaseCallbackHandler):
+ """Накапливает события агента в список во время синхронного вызова."""
+
+ def __init__(self) -> None:
+ self.events: list[dict] = []
def _ts(self) -> str:
return datetime.now().strftime("%H:%M:%S")
+ def _add(self, ev: dict) -> None:
+ self.events.append(ev)
+
def on_tool_start(self, serialized, input_str, **kwargs):
name = serialized.get("name", "?")
try:
args = json.loads(str(input_str)) if isinstance(input_str, str) else input_str
except Exception:
args = {}
- self.q.put({"t": "tool_start", "ts": self._ts(), "name": name, "args": args})
+ self._add({"t": "tool_start", "ts": self._ts(), "name": name, "args": args})
def on_tool_end(self, output, **kwargs):
- self.q.put({"t": "tool_end", "ts": self._ts(), "output": str(output)[:300]})
+ self._add({"t": "tool_end", "ts": self._ts(), "output": str(output)[:300]})
def on_tool_error(self, error, **kwargs):
- self.q.put({"t": "tool_error", "ts": self._ts(), "msg": str(error)[:200]})
+ self._add({"t": "tool_error", "ts": self._ts(), "msg": str(error)[:200]})
def on_llm_start(self, *a, **kw):
- self.q.put({"t": "thinking", "ts": self._ts()})
+ self._add({"t": "thinking", "ts": self._ts()})
def on_llm_end(self, response, **kwargs):
try:
text = response.generations[0][0].text[:120]
- self.q.put({"t": "llm_end", "ts": self._ts(), "preview": text})
+ self._add({"t": "llm_end", "ts": self._ts(), "preview": text})
except Exception:
pass
# ---------------------------------------------------------------------------
-# Запуск агента в фоне
+# Запуск агента — блокирующий вызов в отдельном потоке
+# (избегаем конфликтов с event loop Streamlit)
# ---------------------------------------------------------------------------
-def _run_agent_thread(
- agent, messages, config, q: queue.Queue, cb: AgentCallback,
- stop_flag: threading.Event,
-):
+def _invoke_agent(
+ agent, messages: dict, config: dict
+) -> tuple[dict | None, list[dict], str | None]:
+ """
+ Запускает агента синхронно. Блокирует поток до завершения.
+ Возвращает (result, events, error_message).
+ """
from src.agent.middlewares.retry_on_rate_limit import set_ui_event_queue
- set_ui_event_queue(q)
- 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)})
- finally:
- set_ui_event_queue(None)
- if not stop_flag.is_set():
+
+ collector = AgentEventCollector()
+ # Очередь для rate-limit событий из middleware
+ rl_queue: queue.Queue = queue.Queue()
+ set_ui_event_queue(rl_queue)
+
+ result: dict | None = None
+ error: str | None = None
+
+ def _thread_fn() -> None:
+ nonlocal result, error
+
+ async def _inner() -> None:
+ nonlocal result, error
+ try:
+ r = await agent.ainvoke(messages, {**config, "callbacks": [collector]})
+ result = r
+ except Exception as e:
+ error = str(e)
+
asyncio.run(_inner())
+ t = threading.Thread(target=_thread_fn, daemon=True)
+ t.start()
+ t.join() # ждём завершения (UI показывает spinner)
+
+ set_ui_event_queue(None)
+
+ # Переносим rate-limit события в список collector'а
+ while not rl_queue.empty():
+ try:
+ collector.events.append(rl_queue.get_nowait())
+ except Exception:
+ break
+
+ return result, collector.events, error
+
# ---------------------------------------------------------------------------
-# Рендер одного события в лог
+# Рендер одного события в HTML
# ---------------------------------------------------------------------------
def _render_event(ev: dict) -> str:
@@ -177,28 +215,19 @@ def _render_event(ev: dict) -> str:
return f'
💭 {ts} модель думает...
'
if kind == "tool_start":
- name = ev["name"]
- args = ev.get("args", {})
+ name = ev["name"]
+ args = ev.get("args", {})
short = name.replace(_JOURNAL_PREFIX, "mcp::")
- # Определяем тип инструмента
- if name in _SOLVE_TOOLS:
- cls = "tool-subagent"
- icon = "🧠"
- label = f"[субагент] {short}"
- elif "gitea" in name:
- cls = "tool-call"
- icon = "📦"
- label = short
- elif "mcp::" in short or "journal" in name:
- cls = "tool-call"
- icon = "📡"
- label = short
- else:
- cls = "tool-call"
- icon = "🔧"
- label = short
- # Показываем ключевые аргументы
+ if name in _SOLVE_TOOLS:
+ cls, icon, label = "tool-subagent", "🧠", f"[субагент] {short}"
+ elif "gitea" in name:
+ cls, icon, label = "tool-call", "📦", short
+ elif "mcp::" in short or "journal" in name:
+ cls, icon, label = "tool-call", "📡", short
+ else:
+ cls, icon, label = "tool-call", "🔧", short
+
hint = ""
for key in ("taskId", "path", "repo", "repo_name", "name"):
if key in args:
@@ -216,12 +245,12 @@ def _render_event(ev: dict) -> str:
return f'⚠ {msg}
'
if kind == "rate_limit_wait":
- name = ev.get("name", "?")
- pause = ev.get("pause", 30)
- attempt = ev.get("attempt", 1)
- mx = ev.get("max", 5)
+ name, pause, attempt, mx = (
+ ev.get("name", "?"), ev.get("pause", 30),
+ ev.get("attempt", 1), ev.get("max", 5),
+ )
return (f''
- f'⏳ {ts} 429 rate limit — {name} · жду {pause}с '
+ f'⏳ {ts} 429 rate limit — {name} · ждал {pause}с '
f'(попытка {attempt}/{mx})
')
if kind == "rate_limit_retry":
@@ -237,11 +266,22 @@ def _render_event(ev: dict) -> str:
return ""
+def _show_events(events: list[dict], expanded: bool = False) -> None:
+ if not events:
+ return
+ with st.expander(f"🔍 Лог инструментов ({len(events)} событий)", expanded=expanded):
+ html = "".join(_render_event(e) for e in events[-80:])
+ st.markdown(f'{html}
',
+ unsafe_allow_html=True)
+
+
# ---------------------------------------------------------------------------
# Вкладки
# ---------------------------------------------------------------------------
-tab_chat, tab_pipeline, tab_status = st.tabs(["💬 Чат с агентом", "⚡ Pipeline", "📊 Статус заданий"])
+tab_chat, tab_pipeline, tab_status = st.tabs(
+ ["💬 Чат с агентом", "⚡ Pipeline", "📊 Статус заданий"]
+)
# ══════════════════════════════════════════════════════════════════════════
@@ -249,164 +289,87 @@ tab_chat, tab_pipeline, tab_status = st.tabs(["💬 Чат с агентом", "
# ══════════════════════════════════════════════════════════════════════════
with tab_chat:
- st.caption("Общайся с агентом: задай вопрос, попроси решить задание или разобрать ситуацию.")
+ st.caption("Пиши агенту напрямую. Пока он работает — показывается spinner.")
- # Инициализация session state
- for key, default in [
- ("chat_history", []),
- ("chat_events", []),
- ("chat_thread_id", f"ui-{int(time.time())}"),
- ("chat_running", False),
- ("_chat_last_event_t", time.time()),
- ]:
- if key not in st.session_state:
- st.session_state[key] = default
+ if "chat_history" not in st.session_state:
+ st.session_state.chat_history = []
+ if "chat_events" not in st.session_state:
+ st.session_state.chat_events = []
+ if "chat_thread_id" not in st.session_state:
+ st.session_state.chat_thread_id = f"ui-{int(time.time())}"
- # Показываем историю
+ # История сообщений
for msg in st.session_state.chat_history:
role, text = msg["role"], msg["text"]
if role == "user":
- st.markdown(f'👤 {text}
', unsafe_allow_html=True)
+ st.markdown(f'👤 {text}
',
+ unsafe_allow_html=True)
else:
- st.markdown(f'🤖 {text}
', unsafe_allow_html=True)
-
- # ── АГЕНТ РАБОТАЕТ ──────────────────────────────────────────────────────
- if st.session_state.chat_running:
- thread: threading.Thread = st.session_state.get("_chat_thread")
- evq: queue.Queue = st.session_state.get("_chat_queue")
-
- # Считываем новые события из очереди
- new_events: list[dict] = []
- final_result = None
- fatal = None
-
- if evq:
- while not evq.empty():
- ev = evq.get_nowait()
- if ev["t"] == "done":
- final_result = ev["result"]
- elif ev["t"] == "fatal":
- fatal = ev["msg"]
- else:
- new_events.append(ev)
-
- if new_events:
- st.session_state.chat_events.extend(new_events)
- st.session_state["_chat_last_event_t"] = time.time()
-
- # Агент завершил работу?
- agent_done = (final_result is not None or fatal is not None
- or (thread is not None and not thread.is_alive() and
- (evq is None or evq.empty())))
-
- if agent_done:
- st.session_state.chat_running = False
- if fatal:
- st.session_state.chat_history.append(
- {"role": "agent", "text": f"⚠️ Ошибка: {fatal}"})
- elif final_result:
- msgs = final_result.get("messages", [])
- last = msgs[-1] if msgs else None
- reply = last.content if last and hasattr(last, "content") else "Готово."
- st.session_state.chat_history.append({"role": "agent", "text": reply})
- st.rerun()
-
- # Показываем накопленные события
- if st.session_state.chat_events:
- html = "".join(_render_event(e) for e in st.session_state.chat_events[-80:])
- st.markdown(
- f'{html}
',
- unsafe_allow_html=True,
- )
-
- # Статус
- idle = time.time() - st.session_state["_chat_last_event_t"]
- if idle > 15:
- st.markdown(
- f''
- f'⏳ Агент работает... ({int(idle)}с без событий — возможно rate limit)
',
- unsafe_allow_html=True,
- )
- else:
- st.markdown('💭 Агент думает...
',
+ st.markdown(f'🤖 {text}
',
unsafe_allow_html=True)
- # Кнопка Стоп — работает, потому что НЕ внутри while-цикла
- if st.button("🛑 Остановить", key="chat_stop_btn"):
- stop_flag = st.session_state.get("_chat_stop_flag")
- if stop_flag:
- stop_flag.set()
- st.session_state.chat_running = False
- st.session_state.chat_history.append(
- {"role": "agent", "text": "Остановлено пользователем."})
- st.rerun()
+ # Лог предыдущего запроса
+ _show_events(st.session_state.chat_events)
+
+ # Поле ввода
+ col_input, col_btn = st.columns([5, 1])
+ with col_input:
+ user_input = st.text_input(
+ "Сообщение",
+ placeholder='"Реши задание 6a1864f7..." или "Какие задания у меня есть?"',
+ label_visibility="collapsed",
+ key="chat_input",
+ )
+ with col_btn:
+ send = st.button("Отправить", use_container_width=True, type="primary")
+
+ if send and user_input.strip():
+ msg_text = user_input.strip()
+ st.session_state.chat_history.append({"role": "user", "text": msg_text})
+ st.session_state.chat_events = []
+
+ lc_messages = [
+ HumanMessage(content=m["text"]) if m["role"] == "user"
+ else AIMessage(content=m["text"])
+ for m in st.session_state.chat_history
+ ]
+ config = {"configurable": {"thread_id": st.session_state.chat_thread_id}}
+
+ try:
+ agent = get_agent()
+ except Exception as e:
+ st.error(f"Ошибка инициализации агента: {e}")
+ st.stop()
+
+ with st.spinner("🤖 Агент работает... (это может занять несколько минут)"):
+ result, events, error = _invoke_agent(
+ agent, {"messages": lc_messages}, config
+ )
+
+ st.session_state.chat_events = events
+
+ if error:
+ st.session_state.chat_history.append(
+ {"role": "agent", "text": f"⚠️ Ошибка: {error}"}
+ )
+ elif result:
+ msgs = result.get("messages", [])
+ last = msgs[-1] if msgs else None
+ reply = last.content if last and hasattr(last, "content") else "Готово."
+ st.session_state.chat_history.append({"role": "agent", "text": reply})
+ else:
+ st.session_state.chat_history.append(
+ {"role": "agent", "text": "Агент завершил работу без ответа."}
+ )
- # Следующая итерация опроса через 0.5с
- time.sleep(0.5)
st.rerun()
- # ── ВВОД (агент не работает) ─────────────────────────────────────────────
- else:
- # Лог прошлого запроса
- if st.session_state.chat_events:
- with st.expander(
- f"🔍 Лог инструментов ({len(st.session_state.chat_events)} событий)",
- expanded=False,
- ):
- html = "".join(_render_event(e) for e in st.session_state.chat_events[-80:])
- st.markdown(f'{html}
',
- unsafe_allow_html=True)
-
- col_input, col_btn = st.columns([5, 1])
- with col_input:
- user_input = st.text_input(
- "Сообщение",
- placeholder='Например: "Реши задание 6a1864f7..." или "Какие задания у меня есть?"',
- label_visibility="collapsed",
- key="chat_input",
- )
- with col_btn:
- send = st.button("Отправить", use_container_width=True, type="primary")
-
- if send and user_input.strip():
- msg_text = user_input.strip()
- st.session_state.chat_history.append({"role": "user", "text": msg_text})
- st.session_state.chat_events = []
-
- lc_messages = [
- HumanMessage(content=m["text"]) if m["role"] == "user"
- else AIMessage(content=m["text"])
- for m in st.session_state.chat_history
- ]
- config = {"configurable": {"thread_id": st.session_state.chat_thread_id}}
- agent = get_agent()
- evq = queue.Queue()
- stop_flag = threading.Event()
- cb = AgentCallback(evq)
-
- thread = threading.Thread(
- target=_run_agent_thread,
- args=(agent, {"messages": lc_messages}, config, evq, cb, stop_flag),
- daemon=True,
- )
-
- # Сохраняем в session_state — переживут rerun
- st.session_state["_chat_thread"] = thread
- st.session_state["_chat_queue"] = evq
- st.session_state["_chat_stop_flag"] = stop_flag
- st.session_state["_chat_last_event_t"] = time.time()
- st.session_state.chat_running = True
-
- thread.start()
- st.rerun() # → переходим в ветку "АГЕНТ РАБОТАЕТ"
-
- if st.session_state.chat_history:
- if st.button("🗑 Очистить чат"):
- st.session_state.chat_history = []
- st.session_state.chat_events = []
- st.session_state.chat_thread_id = f"ui-{int(time.time())}"
- st.rerun()
+ if st.session_state.chat_history:
+ if st.button("🗑 Очистить чат"):
+ st.session_state.chat_history = []
+ st.session_state.chat_events = []
+ st.session_state.chat_thread_id = f"ui-{int(time.time())}"
+ st.rerun()
# ══════════════════════════════════════════════════════════════════════════
@@ -414,12 +377,17 @@ with tab_chat:
# ══════════════════════════════════════════════════════════════════════════
with tab_pipeline:
- st.caption("Автоматически решает все todo-задания курса по очереди.")
+ st.caption("Запусти конкретное задание по ID или все todo-задания сразу.")
+
+ if "pipe_events" not in st.session_state:
+ st.session_state.pipe_events = []
+ if "pipe_result_text" not in st.session_state:
+ st.session_state.pipe_result_text = ""
col1, col2 = st.columns([3, 1])
with col1:
task_id_input = st.text_input(
- "Task ID (оставь пустым — решить все todo)",
+ "Task ID",
placeholder="6a1864f7fd30e81cf3146d65",
label_visibility="visible",
)
@@ -427,140 +395,49 @@ with tab_pipeline:
st.write("")
run_btn = st.button("▶ Запустить", type="primary", use_container_width=True)
- # Инициализация pipeline session state
- for key, default in [
- ("pipe_running", False),
- ("pipe_events", []),
- ("_pipe_last_event_t", time.time()),
- ("_pipe_task_id", ""),
- ("_pipe_result", None),
- ("_pipe_fatal", None),
- ]:
- if key not in st.session_state:
- st.session_state[key] = default
+ # Показываем результат предыдущего запуска
+ if st.session_state.pipe_result_text:
+ st.markdown(st.session_state.pipe_result_text, unsafe_allow_html=True)
- if run_btn and task_id_input.strip():
- task_id = task_id_input.strip()
- repo_url = f"https://git.brojs.ru/{GITEA_OWNER}/task-{task_id}"
- prompt = f"Реши задание taskId={task_id} курса 698b49da77cb6d4d2e43ce78"
- config = {"configurable": {"thread_id": f"pipe-{task_id}-{int(time.time())}"}}
- messages = {"messages": [HumanMessage(content=prompt)]}
- agent = get_agent()
+ _show_events(st.session_state.pipe_events)
- evq2 = queue.Queue()
- stop_flag2 = threading.Event()
- cb2 = AgentCallback(evq2)
-
- thread2 = threading.Thread(
- target=_run_agent_thread,
- args=(agent, messages, config, evq2, cb2, stop_flag2),
- daemon=True,
- )
-
- st.session_state["_pipe_thread"] = thread2
- st.session_state["_pipe_queue"] = evq2
- st.session_state["_pipe_stop_flag"] = stop_flag2
- st.session_state["_pipe_task_id"] = task_id
- st.session_state["_pipe_last_event_t"] = time.time()
- st.session_state.pipe_events = []
- st.session_state.pipe_running = True
- st.session_state["_pipe_result"] = None
- st.session_state["_pipe_fatal"] = None
-
- thread2.start()
- st.rerun()
-
- elif run_btn and not task_id_input.strip():
- st.info("Введи Task ID выше или используй раздел «Запустить все todo-задания»")
-
- # ── PIPELINE РАБОТАЕТ ────────────────────────────────────────────────────
- if st.session_state.pipe_running:
- thread2 = st.session_state.get("_pipe_thread")
- evq2 = st.session_state.get("_pipe_queue")
- task_id = st.session_state.get("_pipe_task_id", "")
-
- new_ev2: list[dict] = []
- final2 = fatal2 = None
-
- if evq2:
- while not evq2.empty():
- ev = evq2.get_nowait()
- if ev["t"] == "done":
- final2 = ev["result"]
- elif ev["t"] == "fatal":
- fatal2 = ev["msg"]
- else:
- new_ev2.append(ev)
-
- if new_ev2:
- st.session_state.pipe_events.extend(new_ev2)
- st.session_state["_pipe_last_event_t"] = time.time()
-
- pipe_done = (final2 is not None or fatal2 is not None
- or (thread2 is not None and not thread2.is_alive()
- and (evq2 is None or evq2.empty())))
-
- if pipe_done:
- st.session_state.pipe_running = False
- st.session_state["_pipe_result"] = final2
- st.session_state["_pipe_fatal"] = fatal2
- st.rerun()
-
- # Живой лог
- if st.session_state.pipe_events:
- html = "".join(_render_event(e) for e in st.session_state.pipe_events[-60:])
- st.markdown(
- f'{html}
',
- unsafe_allow_html=True,
- )
-
- idle2 = time.time() - st.session_state["_pipe_last_event_t"]
- if idle2 > 15:
- st.markdown(
- f''
- f'⏳ Агент работает... ({int(idle2)}с — возможно rate limit)
',
- unsafe_allow_html=True)
+ if run_btn:
+ if not task_id_input.strip():
+ st.warning("Введи Task ID")
else:
- st.markdown(f'💭 Решаю {task_id[:8]}...
',
- unsafe_allow_html=True)
+ task_id = task_id_input.strip()
+ repo_url = f"https://git.brojs.ru/{GITEA_OWNER}/task-{task_id}"
+ prompt = f"Реши задание taskId={task_id} курса 698b49da77cb6d4d2e43ce78"
+ config = {"configurable": {"thread_id": f"pipe-{task_id}-{int(time.time())}"}}
+ messages = {"messages": [HumanMessage(content=prompt)]}
+
+ st.session_state.pipe_events = []
+ st.session_state.pipe_result_text = ""
+
+ try:
+ agent = get_agent()
+ except Exception as e:
+ st.error(f"Ошибка инициализации агента: {e}")
+ st.stop()
+
+ with st.spinner(f"🤖 Решаю задание {task_id[:8]}..."):
+ result, events, error = _invoke_agent(agent, messages, config)
+
+ st.session_state.pipe_events = events
+
+ if error:
+ st.session_state.pipe_result_text = (
+ f'❌ Ошибка: {error[:400]}
'
+ )
+ else:
+ st.session_state.pipe_result_text = (
+ f''
+ )
- if st.button("🛑 Остановить", key="pipe_stop_btn"):
- sf = st.session_state.get("_pipe_stop_flag")
- if sf:
- sf.set()
- st.session_state.pipe_running = False
- st.session_state["_pipe_fatal"] = "Остановлено пользователем"
st.rerun()
- time.sleep(0.5)
- st.rerun()
-
- # ── РЕЗУЛЬТАТ ────────────────────────────────────────────────────────────
- elif st.session_state.get("_pipe_fatal") or st.session_state.get("_pipe_result"):
- task_id = st.session_state.get("_pipe_task_id", "")
- repo_url = f"https://git.brojs.ru/{GITEA_OWNER}/task-{task_id}"
- fatal2 = st.session_state.get("_pipe_fatal")
- final2 = st.session_state.get("_pipe_result")
-
- if fatal2:
- st.markdown(f'❌ {fatal2[:300]}
',
- unsafe_allow_html=True)
- else:
- st.markdown(
- f'',
- unsafe_allow_html=True)
-
- if st.session_state.pipe_events:
- with st.expander(
- f"🔍 Лог ({len(st.session_state.pipe_events)} событий)", expanded=False
- ):
- html = "".join(_render_event(e) for e in st.session_state.pipe_events[-80:])
- st.markdown(f'{html}
',
- unsafe_allow_html=True)
-
st.divider()
st.subheader("Запустить все todo-задания")
if st.button("⚡ Запустить pipeline для всех заданий", use_container_width=True):
@@ -601,19 +478,20 @@ with tab_status:
from src.agent.mcp_client import load_journal_toolsets
async def _fetch():
- j = load_journal_toolsets()
- # Инструменты имеют префикс mcp__journal-bh-professor__
+ 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)
+ t = tools.get(full_name) or next(
+ (v for k, v in tools.items() if "tasks_list" in k), None
+ )
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())}")
+ 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)
+ 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
@@ -643,7 +521,7 @@ with tab_status:
title = t.get("title", t.get("name", ""))
counts[status] = counts.get(status, 0) + 1
rows.append({
- "": STATUS_EMOJI.get(status, "❓"),
+ "": STATUS_EMOJI.get(status, "❓"),
"Статус": status,
"ID": tid[:12] + "...",
"Название": title,
@@ -655,6 +533,6 @@ with tab_status:
st.divider()
cols = st.columns(len(counts))
for col, (s, n) in zip(cols, counts.items()):
- col.metric(f"{STATUS_EMOJI.get(s,'❓')} {s}", n)
+ col.metric(f"{STATUS_EMOJI.get(s, '❓')} {s}", n)
else:
st.info("Нажми «Обновить статусы» чтобы загрузить данные.")