diff --git a/ui.py b/ui.py index bbcb50b..7fc7736 100644 --- a/ui.py +++ b/ui.py @@ -251,143 +251,162 @@ tab_chat, tab_pipeline, tab_status = st.tabs(["💬 Чат с агентом", " with tab_chat: st.caption("Общайся с агентом: задай вопрос, попроси решить задание или разобрать ситуацию.") - # История сообщений - 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())}" + # Инициализация 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 # Показываем историю for msg in st.session_state.chat_history: - role = msg["role"] - text = msg["text"] + role, text = msg["role"], msg["text"] if role == "user": st.markdown(f'
👤 {text}
', unsafe_allow_html=True) else: st.markdown(f'
🤖 {text}
', unsafe_allow_html=True) - # Лог событий (раскрывающийся) - 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 = [] - for m in st.session_state.chat_history: - if m["role"] == "user": - lc_messages.append(HumanMessage(content=m["text"])) - else: - lc_messages.append(AIMessage(content=m["text"])) - - config = {"configurable": {"thread_id": st.session_state.chat_thread_id}} - agent = get_agent() - - # Placeholders для обновления в реальном времени - events_ph = st.empty() - status_ph = st.empty() - - evq: queue.Queue = queue.Queue() - cb = AgentCallback(evq) - all_events: list[dict] = [] - stop_flag = threading.Event() - - thread = threading.Thread( - target=_run_agent_thread, - args=(agent, {"messages": lc_messages}, config, evq, cb, stop_flag), - daemon=True, - ) - thread.start() + # ── АГЕНТ РАБОТАЕТ ────────────────────────────────────────────────────── + 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 - last_event_t = time.time() - stop_ph = st.empty() - - while thread.is_alive() or not evq.empty(): - # Кнопка "Стоп" - if stop_ph.button("🛑 Остановить", key=f"stop_{int(time.time()*1000)}"): - stop_flag.set() - fatal = "Остановлено пользователем" - break - - changed = False + if evq: while not evq.empty(): ev = evq.get_nowait() - if ev["t"] in ("done", "fatal"): - if ev["t"] == "done": - final_result = ev["result"] - else: - fatal = ev["msg"] + if ev["t"] == "done": + final_result = ev["result"] + elif ev["t"] == "fatal": + fatal = ev["msg"] else: - all_events.append(ev) - last_event_t = time.time() - changed = True + new_events.append(ev) - # Показываем "ожидание" если нет событий >15с - idle = time.time() - last_event_t - if idle > 15 and thread.is_alive(): - status_ph.markdown( - f'
' - f'⏳ Агент работает... ({int(idle)}с без событий — ' - f'возможно ожидание rate limit)
', - unsafe_allow_html=True, - ) - else: - status_ph.empty() + if new_events: + st.session_state.chat_events.extend(new_events) + st.session_state["_chat_last_event_t"] = time.time() - if changed and all_events: - html = "".join(_render_event(e) for e in all_events[-60:]) - events_ph.markdown( - f'
{html}
', - unsafe_allow_html=True, - ) - time.sleep(0.3) + # Агент завершил работу? + 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()))) - stop_ph.empty() - events_ph.empty() - status_ph.empty() - st.session_state.chat_events = all_events + 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 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}) + # Показываем накопленные события + 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('
💭 Агент думает...
', + 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() + + # Следующая итерация опроса через 0.5с + time.sleep(0.5) 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() + # ── ВВОД (агент не работает) ───────────────────────────────────────────── + 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() # ══════════════════════════════════════════════════════════════════════════ @@ -408,91 +427,139 @@ with tab_pipeline: st.write("") run_btn = st.button("▶ Запустить", type="primary", use_container_width=True) - if run_btn: - result_ph = st.empty() - events_ph2 = st.empty() - agent = get_agent() + # Инициализация 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 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)]} - else: - result_ph.info("Pipeline для всех todo-заданий — используй раздел ниже") - st.stop() + 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() - evq2: queue.Queue = queue.Queue() - cb2 = AgentCallback(evq2) - all_events2: list[dict] = [] + 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() - final2 = None - fatal2 = None - last_event_t2 = time.time() - pipe_status_ph = st.empty() - pipe_stop_ph = st.empty() + elif run_btn and not task_id_input.strip(): + st.info("Введи Task ID выше или используй раздел «Запустить все todo-задания»") - with st.spinner(f"Агент решает {task_id[:8]}..."): - while thread2.is_alive() or not evq2.empty(): - if pipe_stop_ph.button("🛑 Остановить", key=f"pipe_stop_{int(time.time()*1000)}"): - stop_flag2.set() - fatal2 = "Остановлено пользователем" - break + # ── 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", "") - while not evq2.empty(): - ev = evq2.get_nowait() - if ev["t"] == "done": - final2 = ev["result"] - elif ev["t"] == "fatal": - fatal2 = ev["msg"] - else: - all_events2.append(ev) - last_event_t2 = time.time() + new_ev2: list[dict] = [] + final2 = fatal2 = None - idle2 = time.time() - last_event_t2 - if idle2 > 15 and thread2.is_alive(): - pipe_status_ph.markdown( - f'
' - f'⏳ Агент работает... ({int(idle2)}с — возможно rate limit)
', - unsafe_allow_html=True, - ) + 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: - pipe_status_ph.empty() + new_ev2.append(ev) - if all_events2: - html = "".join(_render_event(e) for e in all_events2[-50:]) - events_ph2.markdown( - f'
{html}
', - unsafe_allow_html=True, - ) - time.sleep(0.3) + if new_ev2: + st.session_state.pipe_events.extend(new_ev2) + st.session_state["_pipe_last_event_t"] = time.time() - pipe_stop_ph.empty() - pipe_status_ph.empty() + 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) + else: + st.markdown(f'
💭 Решаю {task_id[:8]}...
', + unsafe_allow_html=True) + + 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: - result_ph.markdown( - f'
❌ Ошибка: {fatal2[:300]}
', - unsafe_allow_html=True, - ) - elif final2: - result_ph.markdown( + st.markdown(f'
❌ {fatal2[:300]}
', + unsafe_allow_html=True) + else: + st.markdown( f'
✅ Готово! ' - f'Открыть репозиторий' - f'
', - unsafe_allow_html=True, - ) + f'' + 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-задания")