Загрузить файлы в «/»
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# 🤖 Агент решения домашних заданий
|
||||
|
||||
Берёт задания из журнала через **MCP**, решает их с помощью LLM, заливает на **git.brojs.ru** и отправляет ссылку преподавателю.
|
||||
Общаться с агентом можно через чат на `localhost:8123` — так же, как у преподавателя.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
homework_agent/
|
||||
├── agent.py # Граф агента + CLI (точка входа для langgraph dev)
|
||||
├── llm.py # Фабрика LLM: openai (пары) ↔ gigachat (дома)
|
||||
├── mcp_clients.py # Загрузка MCP-инструментов (journal + gitea)
|
||||
├── gitea_rest.py # REST fallback для Gitea (если uvx недоступен)
|
||||
├── langgraph.json # Конфиг для langgraph dev
|
||||
├── requirements.txt
|
||||
├── .env.example
|
||||
└── .env # ← создай сам, не коммить!
|
||||
```
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
# 1. Виртуальное окружение
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate # Windows
|
||||
# source .venv/bin/activate # Mac/Linux
|
||||
|
||||
# 2. Зависимости
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. uv (нужен для Gitea MCP, если будешь использовать)
|
||||
pip install uv
|
||||
```
|
||||
|
||||
## Настройка .env
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# открой .env и заполни значения
|
||||
```
|
||||
|
||||
### Токен журнала — `JOURNAL_MCP_PAT` (самое важное!)
|
||||
|
||||
Без него агент не видит задания. Спроси у преподавателя или найди сам:
|
||||
|
||||
1. Зайди на `https://platform.brojs.ru` (авторизуйся)
|
||||
2. Открой DevTools (`F12`) → вкладка **Network**
|
||||
3. Обнови страницу, кликни на любой запрос к `/jrnl-bh/api/...`
|
||||
4. В заголовках найди `Authorization: Bearer <токен>` — это и есть `JOURNAL_MCP_PAT`
|
||||
|
||||
### Токен Gitea — `GITEA_TOKEN`
|
||||
|
||||
1. Зайди на https://git.brojs.ru (авторизуйся)
|
||||
2. Аватарка → **Settings** → **Applications** → **Generate New Token**
|
||||
3. Поставь права: `repository: read & write`
|
||||
4. Скопируй в `.env` как `GITEA_TOKEN`
|
||||
5. Туда же `GITEA_USERNAME` — твой логин на git.brojs.ru
|
||||
|
||||
### Переключение LLM
|
||||
|
||||
**На парах (gpt-oss-20b):**
|
||||
```env
|
||||
LLM_PROVIDER=openai
|
||||
OPENAI_BASE_URL=https://89fe241d-b6f4-4e49-b3d5-9ed3c91dbe2f.tunnel4.com/v1
|
||||
OPENAI_MODEL=openi/gpt-oss-20b
|
||||
OPENAI_API_KEY=fake
|
||||
```
|
||||
> ⚠️ URL туннеля меняется каждый день — спрашивай актуальный у преподавателя
|
||||
|
||||
**Дома (GigaChat, бесплатно):**
|
||||
```env
|
||||
LLM_PROVIDER=gigachat
|
||||
GIGACHAT_CREDENTIALS=твои_credentials
|
||||
GIGACHAT_SCOPE=GIGACHAT_API_B2B
|
||||
```
|
||||
Получить credentials: https://developers.sber.ru/studio/workspaces/my-space/get-token/gigachat
|
||||
|
||||
## 🚀 Запуск чата (localhost:8123)
|
||||
|
||||
```bash
|
||||
langgraph dev
|
||||
```
|
||||
|
||||
Открой в браузере `http://localhost:8123` — появится чат. Можно писать:
|
||||
- *«покажи мои задания»*
|
||||
- *«реши задание с id=abc123»*
|
||||
- *«какие задания есть в курсе xyz?»*
|
||||
|
||||
## Запуск из консоли (без чата)
|
||||
|
||||
```bash
|
||||
# Показать список заданий
|
||||
python agent.py --list
|
||||
|
||||
# Решить конкретное задание
|
||||
python agent.py --task-id <id>
|
||||
|
||||
# Решить задания курса
|
||||
python agent.py --course-id <id>
|
||||
|
||||
# Посмотреть решение без отправки
|
||||
python agent.py --task-id <id> --dry-run
|
||||
```
|
||||
|
||||
## Как это работает
|
||||
|
||||
```
|
||||
Ты пишешь в чат
|
||||
↓
|
||||
agent_graph (LangGraph ReAct)
|
||||
↓ читает задания
|
||||
mcp__journal-mcp__tasks_list / task_get
|
||||
↓ пишет код (LLM)
|
||||
gpt-oss-20b / GigaChat
|
||||
↓ заливает на Gitea
|
||||
upload_solution → git.brojs.ru/username/dz
|
||||
↓ отправляет ссылку
|
||||
mcp__journal-mcp__submission_teacher_set_status
|
||||
```
|
||||
|
||||
## Частые проблемы
|
||||
|
||||
| Ошибка | Решение |
|
||||
|--------|---------|
|
||||
| `JOURNAL_MCP_PAT не задан` | Добавь токен в `.env` |
|
||||
| `FileNotFoundError: uvx` | `pip install uv` или работает REST fallback |
|
||||
| LLM не отвечает | Проверь `OPENAI_BASE_URL` — URL туннеля мог смениться |
|
||||
| `langgraph: command not found` | `pip install langgraph-cli[inmem]` |
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HW Agent</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Onest:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0d0f14; --surface: #14171f; --surface2: #1c2030;
|
||||
--border: rgba(255,255,255,0.07); --border2: rgba(255,255,255,0.12);
|
||||
--text: #e8eaf0; --muted: #6b7280;
|
||||
--accent: #7c6af5; --accent2: #5b4fd4;
|
||||
--green: #34d399; --amber: #fbbf24; --red: #f87171;
|
||||
--mono: 'JetBrains Mono', monospace; --sans: 'Onest', sans-serif;
|
||||
}
|
||||
body { background: var(--bg); color: var(--text); font-family: var(--sans); height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
.topbar { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-bottom: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
|
||||
.logo { font-family: var(--mono); font-size: 14px; font-weight: 600; color: var(--accent); display: flex; align-items: center; gap: 8px; }
|
||||
.logo-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 2s ease-in-out infinite; }
|
||||
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.5;transform:scale(0.8)} }
|
||||
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
||||
.status-badge { font-family: var(--mono); font-size: 11px; padding: 4px 10px; border-radius: 20px; border: 1px solid var(--border2); color: var(--muted); display: flex; align-items: center; gap: 5px; cursor: pointer; transition: all 0.2s; }
|
||||
.status-badge:hover { border-color: var(--accent); color: var(--text); }
|
||||
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--red); transition: background 0.3s; }
|
||||
.status-dot.connected { background: var(--green); }
|
||||
.new-thread-btn { font-family: var(--mono); font-size: 11px; padding: 4px 12px; border-radius: 6px; border: 1px solid var(--border2); background: transparent; color: var(--muted); cursor: pointer; transition: all 0.2s; }
|
||||
.new-thread-btn:hover { background: var(--surface2); color: var(--text); border-color: var(--accent); }
|
||||
|
||||
.main { display: flex; flex: 1; overflow: hidden; }
|
||||
|
||||
.sidebar { width: 220px; background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
|
||||
.sidebar-title { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; color: var(--muted); padding: 14px 16px 8px; text-transform: uppercase; }
|
||||
.quick-btn { display: flex; align-items: center; gap: 8px; padding: 9px 16px; font-size: 13px; color: var(--muted); cursor: pointer; transition: all 0.15s; border: none; background: none; text-align: left; width: 100%; font-family: var(--sans); }
|
||||
.quick-btn:hover { background: var(--surface2); color: var(--text); }
|
||||
.sidebar-sep { height: 1px; background: var(--border); margin: 8px 0; }
|
||||
.sidebar-bottom { margin-top: auto; padding: 12px 16px; font-family: var(--mono); font-size: 10px; color: var(--muted); }
|
||||
.model-tag { background: var(--surface2); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; margin-top: 6px; display: flex; align-items: center; gap: 5px; }
|
||||
|
||||
.chat-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.messages { flex: 1; overflow-y: auto; padding: 24px 28px; display: flex; flex-direction: column; gap: 20px; scroll-behavior: smooth; }
|
||||
.messages::-webkit-scrollbar { width: 4px; }
|
||||
.messages::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
|
||||
|
||||
.welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 12px; }
|
||||
.welcome h2 { font-size: 16px; font-weight: 500; color: var(--muted); }
|
||||
.welcome p { font-size: 13px; color: var(--muted); opacity: 0.6; font-family: var(--mono); }
|
||||
.suggestions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; justify-content: center; max-width: 500px; }
|
||||
.suggestion { padding: 6px 14px; background: var(--surface2); border: 1px solid var(--border2); border-radius: 20px; font-size: 12px; color: var(--muted); cursor: pointer; transition: all 0.15s; font-family: var(--mono); }
|
||||
.suggestion:hover { border-color: var(--accent); color: var(--accent); background: rgba(124,106,245,0.08); }
|
||||
|
||||
.msg { display: flex; gap: 12px; animation: fadeUp 0.2s ease-out; }
|
||||
.msg.user { flex-direction: row-reverse; }
|
||||
@keyframes fadeUp { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
|
||||
.avatar { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; flex-shrink: 0; font-family: var(--mono); }
|
||||
.avatar.ai { background: rgba(124,106,245,0.15); color: var(--accent); border: 1px solid rgba(124,106,245,0.3); }
|
||||
.avatar.user { background: var(--surface2); color: var(--muted); border: 1px solid var(--border2); }
|
||||
.bubble { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 12px 16px; font-size: 14px; line-height: 1.6; max-width: 680px; }
|
||||
.msg.user .bubble { background: var(--surface2); border-color: var(--border2); font-family: var(--mono); font-size: 13px; }
|
||||
|
||||
.repo-link { display: inline-flex; align-items: center; gap: 6px; background: rgba(52,211,153,0.08); border: 1px solid rgba(52,211,153,0.2); border-radius: 6px; padding: 8px 14px; margin: 6px 0; font-family: var(--mono); font-size: 12px; color: var(--green); text-decoration: none; transition: all 0.15s; word-break: break-all; display: block; }
|
||||
.repo-link:hover { background: rgba(52,211,153,0.15); }
|
||||
|
||||
.typing { display: flex; gap: 4px; align-items: center; }
|
||||
.typing span { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); animation: blink 1.2s ease-in-out infinite; }
|
||||
.typing span:nth-child(2){animation-delay:0.2s} .typing span:nth-child(3){animation-delay:0.4s}
|
||||
@keyframes blink { 0%,80%,100%{opacity:0.3;transform:scale(0.8)} 40%{opacity:1;transform:scale(1)} }
|
||||
|
||||
.input-area { padding: 16px 28px 20px; border-top: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
|
||||
.input-wrap { display: flex; gap: 10px; align-items: flex-end; background: var(--bg); border: 1px solid var(--border2); border-radius: 12px; padding: 10px 12px 10px 16px; transition: border-color 0.2s; }
|
||||
.input-wrap:focus-within { border-color: var(--accent); }
|
||||
textarea { flex: 1; background: transparent; border: none; outline: none; color: var(--text); font-family: var(--mono); font-size: 13px; line-height: 1.5; resize: none; max-height: 120px; min-height: 20px; }
|
||||
textarea::placeholder { color: var(--muted); }
|
||||
.send-btn { width: 32px; height: 32px; border-radius: 8px; background: var(--accent); border: none; color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s; flex-shrink: 0; font-size: 14px; }
|
||||
.send-btn:hover { background: var(--accent2); transform: scale(1.05); }
|
||||
.send-btn:disabled { background: var(--surface2); cursor: not-allowed; transform: none; }
|
||||
.input-hint { font-family: var(--mono); font-size: 10px; color: var(--muted); margin-top: 6px; padding: 0 4px; opacity: 0.5; }
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 100; backdrop-filter: blur(4px); }
|
||||
.modal-overlay.hidden { display: none; }
|
||||
.modal { background: var(--surface); border: 1px solid var(--border2); border-radius: 14px; padding: 24px; width: 440px; max-width: 90vw; }
|
||||
.modal h3 { font-family: var(--mono); font-size: 14px; color: var(--accent); margin-bottom: 16px; }
|
||||
.modal label { font-size: 12px; color: var(--muted); font-family: var(--mono); display: block; margin-bottom: 6px; }
|
||||
.modal input, .modal select { width: 100%; background: var(--bg); border: 1px solid var(--border2); border-radius: 8px; padding: 9px 12px; color: var(--text); font-family: var(--mono); font-size: 13px; outline: none; margin-bottom: 14px; }
|
||||
.modal input:focus, .modal select:focus { border-color: var(--accent); }
|
||||
.modal select option { background: var(--surface); }
|
||||
.modal-btns { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
|
||||
.btn-cancel { padding: 8px 16px; background: transparent; border: 1px solid var(--border2); border-radius: 8px; color: var(--muted); font-family: var(--mono); font-size: 12px; cursor: pointer; }
|
||||
.btn-cancel:hover { border-color: var(--border2); color: var(--text); }
|
||||
.btn-ok { padding: 8px 20px; background: var(--accent); border: none; border-radius: 8px; color: white; font-family: var(--mono); font-size: 12px; cursor: pointer; }
|
||||
.btn-ok:hover { background: var(--accent2); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal-overlay hidden" id="modalOverlay">
|
||||
<div class="modal">
|
||||
<h3 id="modalTitle">⚡ решить задание</h3>
|
||||
<label>название задания</label>
|
||||
<input type="text" id="modalInput" placeholder="например: текстовая игра на основе llm + interrupt" />
|
||||
<div id="reviseExtra" style="display:none">
|
||||
<label>что исправить (необязательно — агент прочитает комментарий сам)</label>
|
||||
<input type="text" id="modalComment" placeholder="или оставь пустым" />
|
||||
</div>
|
||||
<div class="modal-btns">
|
||||
<button class="btn-cancel" onclick="closeModal()">отмена</button>
|
||||
<button class="btn-ok" onclick="submitModal()">отправить ▶</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topbar">
|
||||
<div class="logo"><div class="logo-dot"></div>hw_agent</div>
|
||||
<div class="topbar-right">
|
||||
<div class="status-badge" id="statusBadge" onclick="checkConnection()">
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">connecting...</span>
|
||||
</div>
|
||||
<button class="new-thread-btn" onclick="newThread()">+ new thread</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-title">команды</div>
|
||||
<button class="quick-btn" onclick="quickSend('покажи список всех заданий курса KFU-26-1')">📋 список заданий</button>
|
||||
<button class="quick-btn" onclick="openModal('solve')">⚡ решить задание</button>
|
||||
<button class="quick-btn" onclick="openModal('revise')">✏️ переделать по комменту</button>
|
||||
<div class="sidebar-sep"></div>
|
||||
<div class="sidebar-title">другое</div>
|
||||
<button class="quick-btn" onclick="quickSend('покажи статус всех заданий')">📊 статус заданий</button>
|
||||
<button class="quick-btn" onclick="quickSend('какие инструменты у тебя есть?')">🔧 инструменты</button>
|
||||
<div class="sidebar-bottom">
|
||||
<div style="opacity:0.5">модель</div>
|
||||
<div class="model-tag"><span style="color:var(--accent)">▸</span><span id="modelName">gpt-oss-20b</span></div>
|
||||
<div style="margin-top:8px;opacity:0.5">api</div>
|
||||
<div class="model-tag" style="font-size:10px">localhost:2024</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-area">
|
||||
<div class="messages" id="messages">
|
||||
<div class="welcome" id="welcome">
|
||||
<div style="font-size:36px;opacity:0.3">⬡</div>
|
||||
<h2>homework agent</h2>
|
||||
<p>langgraph @ localhost:2024</p>
|
||||
<div class="suggestions">
|
||||
<div class="suggestion" onclick="quickSend('покажи список заданий курса KFU-26-1')">список заданий</div>
|
||||
<div class="suggestion" onclick="openModal('solve')">решить задание</div>
|
||||
<div class="suggestion" onclick="openModal('revise')">переделать по комменту</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<div class="input-wrap">
|
||||
<textarea id="input" placeholder="напиши агенту..." rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
|
||||
<button class="send-btn" id="sendBtn" onclick="sendMessage()">▶</button>
|
||||
</div>
|
||||
<div class="input-hint">enter — отправить · shift+enter — перенос строки</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = 'http://127.0.0.1:2024';
|
||||
const ASSISTANT_ID = '46958879-389b-529f-bca9-2baa098385d8';
|
||||
let threadId = null;
|
||||
let isRunning = false;
|
||||
let modalMode = 'solve';
|
||||
|
||||
// Modal
|
||||
function openModal(mode) {
|
||||
modalMode = mode;
|
||||
document.getElementById('modalTitle').textContent = mode === 'solve' ? '⚡ решить задание' : '✏️ переделать задание';
|
||||
document.getElementById('modalInput').placeholder = mode === 'solve'
|
||||
? 'например: Агент с RAG-памятью'
|
||||
: 'например: MCP-сервер для управления памятью агента';
|
||||
document.getElementById('reviseExtra').style.display = mode === 'revise' ? 'block' : 'none';
|
||||
document.getElementById('modalInput').value = '';
|
||||
document.getElementById('modalComment').value = '';
|
||||
document.getElementById('modalOverlay').classList.remove('hidden');
|
||||
setTimeout(() => document.getElementById('modalInput').focus(), 50);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modalOverlay').classList.add('hidden');
|
||||
}
|
||||
|
||||
function submitModal() {
|
||||
const name = document.getElementById('modalInput').value.trim();
|
||||
if (!name) { document.getElementById('modalInput').focus(); return; }
|
||||
closeModal();
|
||||
if (modalMode === 'solve') {
|
||||
quickSend(`реши задание "${name}"`);
|
||||
} else {
|
||||
const comment = document.getElementById('modalComment').value.trim();
|
||||
if (comment) {
|
||||
quickSend(`переделай задание "${name}" по комментарию: ${comment}`);
|
||||
} else {
|
||||
quickSend(`переделай задание "${name}" по комментарию преподавателя`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('modalInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); submitModal(); }
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
|
||||
document.getElementById('modalOverlay').addEventListener('click', e => {
|
||||
if (e.target === e.currentTarget) closeModal();
|
||||
});
|
||||
|
||||
// Connection
|
||||
async function checkConnection() {
|
||||
try {
|
||||
const r = await fetch(`${API}/assistants/${ASSISTANT_ID}`, {signal: AbortSignal.timeout(3000)});
|
||||
const dot = document.getElementById('statusDot');
|
||||
const txt = document.getElementById('statusText');
|
||||
if (r.ok) {
|
||||
dot.className = 'status-dot connected';
|
||||
txt.textContent = 'connected';
|
||||
} else {
|
||||
dot.className = 'status-dot';
|
||||
txt.textContent = 'error ' + r.status;
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('statusDot').className = 'status-dot';
|
||||
document.getElementById('statusText').textContent = 'offline';
|
||||
}
|
||||
}
|
||||
|
||||
async function newThread() {
|
||||
try {
|
||||
const r = await fetch(`${API}/threads`, {method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'});
|
||||
if (r.ok) { const d = await r.json(); threadId = d.thread_id; }
|
||||
else threadId = 'local-' + Date.now();
|
||||
} catch { threadId = 'local-' + Date.now(); }
|
||||
document.getElementById('messages').innerHTML = '';
|
||||
}
|
||||
|
||||
// Messages
|
||||
function hideWelcome() {
|
||||
const w = document.getElementById('welcome');
|
||||
if (w) w.remove();
|
||||
}
|
||||
|
||||
function addMessage(role, html) {
|
||||
hideWelcome();
|
||||
const msgs = document.getElementById('messages');
|
||||
const div = document.createElement('div');
|
||||
div.className = `msg ${role}`;
|
||||
const label = role === 'user' ? 'you' : 'ai';
|
||||
div.innerHTML = `<div class="avatar ${role}">${label}</div><div class="bubble">${html}</div>`;
|
||||
msgs.appendChild(div);
|
||||
msgs.scrollTop = msgs.scrollHeight;
|
||||
return div.querySelector('.bubble');
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
function formatAiText(text) {
|
||||
// Linkify git.brojs.ru URLs
|
||||
return escHtml(text).replace(
|
||||
/https:\/\/git\.brojs\.ru\/\S+/g,
|
||||
url => `<a class="repo-link" href="${url}" target="_blank">🔗 ${url}</a>`
|
||||
).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// Send
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById('input');
|
||||
const text = input.value.trim();
|
||||
if (!text || isRunning) return;
|
||||
|
||||
if (!threadId) await newThread();
|
||||
|
||||
input.value = '';
|
||||
autoResize(input);
|
||||
isRunning = true;
|
||||
document.getElementById('sendBtn').disabled = true;
|
||||
|
||||
addMessage('user', escHtml(text));
|
||||
const aiBubble = addMessage('ai', '<div class="typing"><span></span><span></span><span></span></div>');
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${API}/threads/${threadId}/runs/stream`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
assistant_id: ASSISTANT_ID,
|
||||
input: {messages: [{role: 'user', content: text}]},
|
||||
stream_mode: 'values'
|
||||
})
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
aiBubble.innerHTML = `<span style="color:var(--red)">❌ HTTP ${resp.status} — убедись что langgraph dev запущен</span>`;
|
||||
isRunning = false;
|
||||
document.getElementById('sendBtn').disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = resp.body.getReader();
|
||||
const dec = new TextDecoder();
|
||||
let buf = '';
|
||||
let lastAiText = '';
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
buf += dec.decode(value, {stream: true});
|
||||
|
||||
const lines = buf.split('\n');
|
||||
buf = lines.pop();
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
const raw = line.slice(6).trim();
|
||||
if (!raw || raw === '[DONE]') continue;
|
||||
try {
|
||||
const ev = JSON.parse(raw);
|
||||
// values mode: ev is the state object with messages array
|
||||
const msgs = ev.messages || (Array.isArray(ev) ? ev : null);
|
||||
if (msgs) {
|
||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
const m = msgs[i];
|
||||
const role = m.type || m.role;
|
||||
if (role === 'ai' || role === 'assistant') {
|
||||
const c = m.content;
|
||||
if (typeof c === 'string' && c.trim()) { lastAiText = c; break; }
|
||||
if (Array.isArray(c)) {
|
||||
const t = c.filter(x => x.type === 'text').map(x => x.text).join('');
|
||||
if (t.trim()) { lastAiText = t; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Show live update
|
||||
if (lastAiText) aiBubble.innerHTML = formatAiText(lastAiText);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastAiText) aiBubble.innerHTML = '<span style="color:var(--muted)">агент не вернул ответ — проверь логи в терминале</span>';
|
||||
else aiBubble.innerHTML = formatAiText(lastAiText);
|
||||
|
||||
} catch(e) {
|
||||
aiBubble.innerHTML = `<span style="color:var(--red)">❌ ${escHtml(e.message)}</span><br><span style="color:var(--muted);font-family:var(--mono);font-size:12px">запусти: langgraph dev</span>`;
|
||||
}
|
||||
|
||||
isRunning = false;
|
||||
document.getElementById('sendBtn').disabled = false;
|
||||
document.getElementById('messages').scrollTop = 99999;
|
||||
}
|
||||
|
||||
function quickSend(text) {
|
||||
document.getElementById('input').value = text;
|
||||
autoResize(document.getElementById('input'));
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
function handleKey(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
||||
}
|
||||
|
||||
function autoResize(el) {
|
||||
el.style.height = 'auto';
|
||||
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
|
||||
}
|
||||
|
||||
checkConnection();
|
||||
newThread();
|
||||
setInterval(checkConnection, 15000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"homework_agent": "src.agent:agent_graph"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
langchain==1.2.15
|
||||
langchain-core==1.3.0
|
||||
langchain-openai==1.1.9
|
||||
langchain-community>=0.3.0
|
||||
langchain-mcp-adapters==0.2.1
|
||||
langgraph==1.1.8
|
||||
langgraph-cli[inmem]==0.4.23
|
||||
fastmcp==3.1.0
|
||||
gigachat==0.2.0
|
||||
requests>=2.31.0
|
||||
beautifulsoup4>=4.12.0
|
||||
python-dotenv>=1.0.0
|
||||
pydantic>=2.0.0
|
||||
rich==14.3.3
|
||||
markdownify==1.2.2
|
||||
httpx>=0.27.0
|
||||
Reference in New Issue
Block a user