"""
Демо-режим 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("""
""", 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'',
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'',
unsafe_allow_html=True,
)