""" Демо-режим 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(""" """, unsafe_allow_html=True) st.markdown("""
🤖 BroJS Agent
Автоматическое выполнение заданий курса KFU-26-1 · platform.brojs.ru
""", unsafe_allow_html=True) st.markdown('
🎬 DEMO-режим — реальные API не вызываются, показывает как выглядит интерфейс в работе
', 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" ({file_count} файлов)" if key == "gitea_write_file" and file_count > 0 else "" ) parts.append(f'
{_ICON[sv]} {label}{extra}
') 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'
{ts} ' f'🤔 модель думает...
' ) 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" {path}" if path else "" logs.append( f'
{ts} ' f'🔧 {short}{finfo}
' ) dirty = True elif ev["t"] == "tool_end": out = ev["output"][:140].replace("<", "<").replace(">", ">") logs.append( f'
{ts} ' f'↩ {out}
' ) 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( '
' + "".join(logs[-60:]) + '
', 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'
📄 {last_path} ' f'({len(files)} файлов загружено)
', 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'
✓ Задание сдано! ' f'Открыть репозиторий →
', unsafe_allow_html=True, )