Files
agentSber/ui_demo.py
T

363 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Демо-режим 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("""
<style>
.step-done {
padding: 8px 14px; border-radius: 8px; margin: 5px 0;
background: #0d2b1d; border-left: 4px solid #22c55e;
color: #4ade80; font-family: monospace; font-size: .88em;
}
.step-active {
padding: 8px 14px; border-radius: 8px; margin: 5px 0;
background: #2a1e00; border-left: 4px solid #f59e0b;
color: #fbbf24; font-family: monospace; font-size: .88em;
animation: pulse 1s infinite alternate;
}
.step-pending {
padding: 8px 14px; border-radius: 8px; margin: 5px 0;
background: #111827; border-left: 4px solid #1f2937;
color: #374151; font-family: monospace; font-size: .88em;
}
@keyframes pulse { from { opacity: 1; } to { opacity: 0.6; } }
.log-wrap {
background: #0b0f1a; border-radius: 10px; padding: 12px;
max-height: 340px; overflow-y: auto;
font-family: 'Courier New', monospace; font-size: .78em;
border: 1px solid #1f2937;
}
.log-line { padding: 3px 0; border-bottom: 1px solid #0f172a; }
.ts { color: #374151; }
.ttool { color: #818cf8; font-weight: bold; }
.tres { color: #34d399; }
.tthink { color: #6b7280; font-style: italic; }
.file-header {
background: #1e1b4b; border-radius: 6px 6px 0 0;
padding: 6px 14px; font-family: monospace;
font-size: .85em; color: #818cf8; border-bottom: 1px solid #312e81;
}
.agent-header {
background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 100%);
border-radius: 12px; padding: 20px 28px; margin-bottom: 20px;
border: 1px solid #312e81;
}
.agent-title { font-size: 1.8em; font-weight: bold; color: #e2e8f0; margin: 0; }
.agent-sub { color: #6b7280; font-size: .9em; margin-top: 4px; }
.badge-ok { background:#0d2b1d; border:1px solid #22c55e; color:#4ade80; padding:10px 18px; border-radius:8px; font-family:monospace; }
.demo-banner { background:#1e1b4b; border:1px solid #4f46e5; color:#a5b4fc; padding:8px 16px; border-radius:8px; font-size:.85em; margin-bottom:12px; }
</style>
""", unsafe_allow_html=True)
st.markdown("""
<div class="agent-header">
<div class="agent-title">🤖 BroJS Agent</div>
<div class="agent-sub">Автоматическое выполнение заданий курса KFU-26-1 · platform.brojs.ru</div>
</div>
""", unsafe_allow_html=True)
st.markdown('<div class="demo-banner">🎬 DEMO-режим — реальные API не вызываются, показывает как выглядит интерфейс в работе</div>', 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" <span style='opacity:.6'>({file_count} файлов)</span>"
if key == "gitea_write_file" and file_count > 0 else ""
)
parts.append(f'<div class="{_CLS[sv]}">{_ICON[sv]} {label}{extra}</div>')
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'<div class="log-line"><span class="ts">{ts}</span> '
f'<span class="tthink">🤔 модель думает...</span></div>'
)
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" <b style='color:#60a5fa'>{path}</b>" if path else ""
logs.append(
f'<div class="log-line"><span class="ts">{ts}</span> '
f'<span class="ttool">🔧 {short}</span>{finfo}</div>'
)
dirty = True
elif ev["t"] == "tool_end":
out = ev["output"][:140].replace("<", "&lt;").replace(">", "&gt;")
logs.append(
f'<div class="log-line"><span class="ts">{ts}</span> '
f'<span class="tres">↩ {out}</span></div>'
)
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(
'<div class="log-wrap">' + "".join(logs[-60:]) + '</div>',
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'<div class="file-header">📄 {last_path} '
f'<span style="opacity:.5">({len(files)} файлов загружено)</span></div>',
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'<div class="badge-ok">✓ Задание сдано! '
f'<a href="{url}" target="_blank" style="color:#4ade80">Открыть репозиторий →</a></div>',
unsafe_allow_html=True,
)