init
This commit is contained in:
commit
d27156d152
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
__pycache__
|
||||||
134
Target.md
Normal file
134
Target.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
https://www.perplexity.ai/search/sozdai-requiremmnts-txt-rkA2KeVJR1y0gtlrZpwecA
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
доработай чтобы удовлетворить всем требованиям
|
||||||
|
|
||||||
|
`какие есть открытые агенты, доступные на декстопе вин 10, к которым можно подсоединить как внешние (типа gemini по api) так и внутренние (развернутые в ollama) llm, и чтобы можно было задавать кастомные промпты, типо сделать запрос через curl и получить ссылки на изображения внутри json взять их их и вывести все 4 в чате чтобы визульано было видно что получено, сделать запрос по полученной ссылке на изображения, проанализировать изображение и вывести промпт чтобы сгенерировтаь подобное - а промпт например подать запрос по удаленному эндпоинту. или запросить в некой папке got --diff staged и создать сообщение на основе этого для коммита. или например аналзировать формируемый где-то субтитры видеозвонка и на сонове того брать последние 50000 символов и пропускать через промпт выдавая на выходе предложения по дальнейшему развитию игры в днд`. если нужно задай вначале все необходимые вопросы и не приступай к генерации пока не получишь всю нужную информацию. Если нужно а я не даю, продолжай требовать её
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
1. Ollama не будет использоваться, только внешние модели пока что `MODELS = {
|
||||||
|
"gemini-2.0-flash": {
|
||||||
|
"name": "gemini-2.0-flash",
|
||||||
|
"provider": "openai",
|
||||||
|
"baseUrl": "https://render-service-gsu7.onrender.com/g/v1beta",
|
||||||
|
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision"],
|
||||||
|
},
|
||||||
|
"gemini-2.5-flash": {
|
||||||
|
"name": "gemini-2.5-flash",
|
||||||
|
"provider": "openai",
|
||||||
|
"baseUrl": "https://render-service-gsu7.onrender.com/g/v1beta",
|
||||||
|
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision"],
|
||||||
|
},
|
||||||
|
"gemini-2.5-flash-preview-05-20": {
|
||||||
|
"name": "gemini-2.5-flash-preview-05-20",
|
||||||
|
"provider": "openai",
|
||||||
|
"baseUrl": "https://render-service-gsu7.onrender.com/g/v1beta",
|
||||||
|
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision"],
|
||||||
|
},
|
||||||
|
"mistral-small-latest": {
|
||||||
|
"name": "mistral-small-latest",
|
||||||
|
"provider": "mistralai",
|
||||||
|
"baseUrl": "https://render-service-gsu7.onrender.com/m",
|
||||||
|
"apiKey": "Q0m29fvxBY0Cfdj4sjHaKqccy1NjonLW",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision"],
|
||||||
|
},
|
||||||
|
}`, заложи заглушку, которую потом можно развить под ollama (типо пустой словарь соединений с моделями).
|
||||||
|
|
||||||
|
1. Веб интерфейс не слабее chatgpt или perlexity, с историями диалогов, с о средней панелью (левая история, правая чат, а средняя - граф) с графом как на flowith, сделай его через react flow если нет лучше альтернативы. суть в том, что активный чат - это выбранная ветка на графе, и новое сообщение в чате порождает новый лист в графе, а граф перестраивается с использованием библиотеки dagre. активный нод в графе - ыбранный последнее сообщение в правой панели, например, если выбрать промужточное на графе (щелкнуть лкм по ноду), то сообщения в правой панели выведутся от корня графа вплоть до щелкнутого сообщения. и если спроить, то будет добален ноду новый ребенок и дальше новое ветвления с постоянным обновлением посдвечиваемого синими цветом активного листа согласно последнему сообщению(сообщения пользователя и llm разными цветми контура нода). я пробовал streamlit и он показался оцень примитивным, возможно, я не прав.
|
||||||
|
Соответсвтенно хранятся одержимое нодов графа в чем-то и при отправке сообщения нужно использовать память (формировать её?) но не в лоб как в дипсик, чтобы она глупо конкатенировалась, а суммаризацией, возможно либо с отсечкой по кол-ву токенов последних - предложи сначала варианты, какие есть. т.е. для новой ветки формируемый промпт будет разным (отправляемыая история).
|
||||||
|
1. Нужно бесплатное решение - предложи варианты. и то, что будет работать на gtx 1060 ti .
|
||||||
|
2. подробное описание и стиль как минимум. предложи, что ещё?
|
||||||
|
3. сначала предложи вариант платформы для звонка, (варианты), которые позволяю непрерывно извлекать уже сформированные на данный момент звонка субтитры и передавать в систему (промежуточные субтитры).
|
||||||
|
4. необходимость запустить код в obsidiab ````dataviewjs
|
||||||
|
const defaultlocations = ["Фаэрун"];
|
||||||
|
const defaultCurrentDate = "1491/09/29";
|
||||||
|
const defaultStartDate = "1490/01/01";
|
||||||
|
const defaultEndDate = "1493/3/30";
|
||||||
|
const defaultPcsParticipationOnly = false;
|
||||||
|
const defaultIncludeRegionsParticipants = false;
|
||||||
|
const fullWidth = true;
|
||||||
|
|
||||||
|
const scriptPath = "scripts/timeline-widget";
|
||||||
|
await dv.view(scriptPath, { arg1: dv, arg2: defaultlocations, arg3: defaultCurrentDate, arg4: defaultStartDate, arg5: defaultEndDate, arg6: defaultPcsParticipationOnly, arg7: defaultIncludeRegionsParticipants, arg8: fullWidth });
|
||||||
|
|
||||||
|
```` и считать результат. ВОзможность собирать данные из obsidian.
|
||||||
|
|
||||||
|
Также этот эндпоинт на удаленном ресурсе (остаь пока там произвольную константу для url), который имеет этот ресурс для получения изображения по промпту : `def _process_image_generation(prompt: str):
|
||||||
|
"""
|
||||||
|
Вспомогательная функция для обработки логики генерации изображений.
|
||||||
|
Возвращает кортеж (image_urls, error_message).
|
||||||
|
"""
|
||||||
|
image_urls = []
|
||||||
|
error_message = None
|
||||||
|
|
||||||
|
# При каждой генерации изображений создаем новый объект BingArt
|
||||||
|
# с помощью функции load_cookies, чтобы убедиться, что куки актуальны.
|
||||||
|
# Это позволяет избежать проблем с устаревшими сессиями.
|
||||||
|
# Важно: close_session() вызывается в finally, чтобы гарантировать закрытие сессии.
|
||||||
|
local_bing_art = BingArt(
|
||||||
|
auth_cookie_U=cookies.get('_U'),
|
||||||
|
auth_cookie_KievRPSSecAuth=cookies.get('KievRPSSecAuth'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if prompt:
|
||||||
|
results = local_bing_art.generate_images(prompt)
|
||||||
|
if results and 'images' in results:
|
||||||
|
# Фильтруем и обрезаем до 4 изображений
|
||||||
|
image_urls = [
|
||||||
|
img['url'] for i, img in enumerate(results['images'])
|
||||||
|
if i % 2 == 0
|
||||||
|
][:4]
|
||||||
|
else:
|
||||||
|
error_message = "Не удалось получить изображения."
|
||||||
|
else:
|
||||||
|
error_message = "Коммандер, введи промпт!"
|
||||||
|
except Exception as e:
|
||||||
|
error_message = f"Произошла ошибка: {str(e)}"
|
||||||
|
app.logger.error(f"Ошибка при генерации изображения: {e}",
|
||||||
|
exc_info=True)
|
||||||
|
finally:
|
||||||
|
# Важно: закрываем сессию BingArt после использования
|
||||||
|
local_bing_art.close_session()
|
||||||
|
|
||||||
|
return image_urls, error_message
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/generate-images', methods=['GET', 'POST'])
|
||||||
|
def generate_images():
|
||||||
|
"""
|
||||||
|
Генерация изображений с использованием bingart.
|
||||||
|
Поддерживает как POST (form data), так и GET (query parameter) запросы.
|
||||||
|
"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
prompt = request.form.get('prompt')
|
||||||
|
else: # GET request
|
||||||
|
prompt = request.args.get('prompt')
|
||||||
|
|
||||||
|
session['prompt'] = prompt # Сохраняем промпт в сессии
|
||||||
|
|
||||||
|
image_urls, error_message = _process_image_generation(prompt)
|
||||||
|
|
||||||
|
if error_message:
|
||||||
|
session['error_message'] = error_message
|
||||||
|
return jsonify({'error': error_message}), 500
|
||||||
|
|
||||||
|
session['image_urls'] = image_urls
|
||||||
|
return jsonify({'image_urls': image_urls})`
|
||||||
|
|
||||||
|
7. Думаю, они должны предлагаться как команды по слешу, но эти команды должны предлагаться исходя из контекста. но также должны быть автодополнение - когда слеш начинаешь писать, и предлагается автодополнение. Предлоижи сначала варианты.
|
||||||
|
|
||||||
|
храниться они будут в отдельной папке в подпапках, а при начале письма подставляются в виде `<имя папки>/<ввод пользователя><вариант продолжения N>`
|
||||||
|
и все варианты должны предлагаться в некотором popup-окне или лучше.
|
||||||
|
8. Русский.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
217
Target2.md
Normal file
217
Target2.md
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
Для реализации вашего комплексного агента с учётом всех требований предлагаю следующий план и архитектуру. Ниже — ключевые моменты, архитектурные решения и рекомендации, а в конце — пример кода для базового LLM-клиента с заглушкой под Ollama и интеграцией Gemini через LangChain.
|
||||||
|
|
||||||
|
## 1. Поддержка только внешних моделей (Gemini и Mistral), заглушка под Ollama
|
||||||
|
|
||||||
|
- В `MODELS` оставляем только внешние модели, как вы указали.
|
||||||
|
- Создаём пустой словарь `LOCAL_MODELS = {}` для будущей интеграции Ollama.
|
||||||
|
- Для Gemini используем **официальный LangChain-пакет `langchain-google-genai`** (рекомендуется, т.к. поддерживает чат, vision, структурированные ответы).
|
||||||
|
- Для Mistral — пока простой HTTP-клиент (зависит от API).
|
||||||
|
|
||||||
|
## 2. Веб-интерфейс (React + React Flow + Dagre)
|
||||||
|
|
||||||
|
- Три панели:
|
||||||
|
- Левая — история диалогов (ветки графа)
|
||||||
|
- Средняя — визуализация графа сообщений (React Flow + Dagre для layout)
|
||||||
|
- Правая — чат с выбранной веткой (последовательность сообщений от корня до выбранного узла)
|
||||||
|
- Активный чат — выбранный лист графа. Новое сообщение добавляет новый лист (ребёнок) в граф.
|
||||||
|
- При клике на узел графа показываем в чате цепочку сообщений от корня до этого узла.
|
||||||
|
- Ноды разного цвета: пользователь — один цвет, LLM — другой.
|
||||||
|
- Для хранения истории и графа — локальное хранилище (IndexedDB) или backend (например, FastAPI + БД).
|
||||||
|
- Для суммаризации истории и формирования промптов:
|
||||||
|
- Использовать **суммаризацию через LLM** (например, Gemini) — для сжатия истории до лимита токенов.
|
||||||
|
- Альтернативно — скользящее окно последних сообщений с ограничением по токенам.
|
||||||
|
- Можно комбинировать: последние N сообщений + суммаризация более старых.
|
||||||
|
|
||||||
|
## 3. Бесплатное решение и работа на GTX 1060 Ti
|
||||||
|
|
||||||
|
- Для LLM — использовать только внешние API (Gemini, Mistral), чтобы не нагружать локальную видеокарту.
|
||||||
|
- Для визуализации — React + React Flow — лёгкие и бесплатные.
|
||||||
|
- Для анализа изображений — использовать возможности Gemini Vision или внешние бесплатные API (например, CLIP + локальный сервер, если потребуется).
|
||||||
|
- Для git — локальные команды через subprocess (git diff) — бесплатно.
|
||||||
|
- Для субтитров — интеграция с выбранной платформой звонков (см. ниже).
|
||||||
|
|
||||||
|
## 4. Описание и стиль
|
||||||
|
|
||||||
|
- Подробные комментарии в коде.
|
||||||
|
- Документация API.
|
||||||
|
- UI с подсказками и автодополнением.
|
||||||
|
- Локализация на русский.
|
||||||
|
|
||||||
|
## 5. Платформа для видеозвонков с субтитрами
|
||||||
|
|
||||||
|
Варианты:
|
||||||
|
|
||||||
|
| Платформа | Особенности субтитров | API для извлечения субтитров |
|
||||||
|
|-------------------|-----------------------------------------------|-----------------------------------------------|
|
||||||
|
| Zoom | Автоматические субтитры, можно получать через API | Есть Webhooks и API для получения транскриптов |
|
||||||
|
| Microsoft Teams | Автоматические субтитры, API ограничены | Можно использовать Graph API для записи |
|
||||||
|
| Google Meet | Субтитры в реальном времени, API нет | Можно использовать сторонние решения для захвата |
|
||||||
|
| Jitsi | Открытый исходный код, можно интегрировать субтитры | Можно реализовать собственный плагин |
|
||||||
|
|
||||||
|
Рекомендация: **Zoom** — наиболее зрелая платформа с API для субтитров.
|
||||||
|
|
||||||
|
## 6. Интеграция с Obsidian
|
||||||
|
|
||||||
|
- Можно запускать `dataviewjs` скрипты через Obsidian API.
|
||||||
|
- Для интеграции в агент — сделать отдельный узел, который запускает скрипт и получает результат.
|
||||||
|
- Для удалённого эндпоинта генерации изображений — сделать HTTP-клиент, который отправляет промпт и получает JSON с 4 ссылками.
|
||||||
|
|
||||||
|
## 7. Команды с автодополнением и контекстом
|
||||||
|
|
||||||
|
- Ввод команд через слеш `/` в поле ввода.
|
||||||
|
- Автодополнение реализуется через локальный индекс команд (хранить в папках).
|
||||||
|
- При вводе `/` показывать popup с командами, фильтровать по введённому тексту.
|
||||||
|
- Команды могут подставляться в виде `/`.
|
||||||
|
- Можно использовать библиотеку для автокомплита (например, React Autosuggest).
|
||||||
|
|
||||||
|
## 8. Язык — русский
|
||||||
|
|
||||||
|
# Резюме и дальнейшие шаги
|
||||||
|
|
||||||
|
- Для веб-интерфейса рекомендую React + React Flow + Dagre. Можно использовать готовые шаблоны и расширять их.
|
||||||
|
- Для суммаризации истории — использовать Gemini для генерации кратких резюме, комбинировать с оконным ограничением по токенам.
|
||||||
|
- Для субтитров — рекомендую рассмотреть Zoom API или Jitsi с кастомной интеграцией.
|
||||||
|
- Для git diff — использовать Python `subprocess` с `git diff --staged`.
|
||||||
|
- Для автодополнения команд — React Autosuggest или аналогичные библиотеки.
|
||||||
|
- Для генерации и анализа изображений — использовать Gemini Vision API и ваш удалённый эндпоинт с bingart.
|
||||||
|
- Вся логика с графом сообщений и ветвлениями — на фронтенде и backend (FastAPI или Node.js).
|
||||||
|
|
||||||
|
Если хотите, могу помочь с примером React-приложения для визуализации графа и чата или с backend для интеграции git и субтитров.
|
||||||
|
|
||||||
|
Жду ваших комментариев или уточнений для следующего шага!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
1. ОК
|
||||||
|
2. хранение через indexed db.
|
||||||
|
3.давай комбинацию
|
||||||
|
gemini vision - ок пока, добавь заглушку для расширения.
|
||||||
|
Пока для Google Meet и Microsoft TEams.
|
||||||
|
7. React Autosuggest - годится
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Ваши уточнения приняты, предлагаю архитектуру и ключевые технические решения для вашего агента, а также примерный план реализации фронтенда с React Flow + Dagre и IndexedDB, и backend-структуру.
|
||||||
|
|
||||||
|
## Архитектура и ключевые решения
|
||||||
|
|
||||||
|
| Требование | Решение / Технология |
|
||||||
|
|-----------------------------------|-----------------------------------------------------------------------------------------------------------|
|
||||||
|
| **LLM** | Внешние модели Gemini и Mistral через API, заглушка под Ollama (пока пустая). |
|
||||||
|
| **Веб-интерфейс** | React + React Flow для графа + Dagre для автоматического layout, React Autosuggest для автодополнения. |
|
||||||
|
| **Хранение истории и графа** | IndexedDB (через библиотеку `idb` или `dexie.js`) для локального хранения диалогов и графа. |
|
||||||
|
| **Визуализация графа** | React Flow с динамическим пересчётом layout через Dagre (пример из [1][2][4]). |
|
||||||
|
| **Чат с историей и ветвлениями** | Правая панель — чат, показывающий цепочку сообщений от корня до выбранного узла графа. |
|
||||||
|
| **Суммаризация истории** | Комбинация: последние N сообщений + вызов Gemini для суммаризации старой истории (с учётом лимита токенов).|
|
||||||
|
| **Анализ изображений** | Использовать Gemini Vision API, заглушка для расширения. |
|
||||||
|
| **Платформы для субтитров** | Google Meet и Microsoft Teams (API для получения транскриптов, интеграция через polling или webhook). |
|
||||||
|
| **Git интеграция** | Локальный запуск `git diff --staged` через Python subprocess, формирование сообщения коммита. |
|
||||||
|
| **Генерация изображений** | Вызов удалённого эндпоинта с bingart (пример функции из вашего запроса). |
|
||||||
|
| **Команды с автодополнением** | React Autosuggest, команды хранятся в IndexedDB, показываются в popup при вводе `/`. |
|
||||||
|
| **Язык интерфейса** | Русский. |
|
||||||
|
|
||||||
|
## Краткое описание React Flow + Dagre для графа
|
||||||
|
|
||||||
|
- Используйте пример из [1], где через dagre вычисляется layout для узлов и рёбер.
|
||||||
|
- При добавлении нового сообщения создаётся новый узел и ребро к предыдущему.
|
||||||
|
- При клике на узел — обновляется чат справа, показывая цепочку сообщений от корня до выбранного узла.
|
||||||
|
- Используйте разные цвета и стили для нодов пользователя и LLM.
|
||||||
|
- Для автодополнения команд — React Autosuggest, данные команд загружать из IndexedDB.
|
||||||
|
|
||||||
|
## IndexedDB для хранения
|
||||||
|
|
||||||
|
- Используйте `dexie.js` или `idb` для удобной работы с IndexedDB.
|
||||||
|
- Структура БД:
|
||||||
|
- Таблица `dialogs` — id, название, дата создания.
|
||||||
|
- Таблица `nodes` — id, dialog_id, parent_id, content, role (user/llm), timestamp.
|
||||||
|
- Таблица `commands` — id, folder, command_text, description.
|
||||||
|
|
||||||
|
## Рекомендации по дальнейшей реализации
|
||||||
|
|
||||||
|
- **Frontend**:
|
||||||
|
- Создайте React-приложение с тремя панелями.
|
||||||
|
- Используйте React Flow с Dagre для графа (примеры из [1][2][4]).
|
||||||
|
- Реализуйте IndexedDB для хранения истории и команд.
|
||||||
|
- Подключите React Autosuggest для автодополнения команд.
|
||||||
|
|
||||||
|
- **Backend**:
|
||||||
|
- API для вызова LLM (Gemini).
|
||||||
|
- API для git diff, генерации сообщений коммита.
|
||||||
|
- API для получения субтитров (интеграция с Google Meet / MS Teams).
|
||||||
|
- API для генерации и анализа изображений (bingart).
|
||||||
|
|
||||||
|
- **Суммаризация истории**:
|
||||||
|
- При отправке нового сообщения формируйте промпт из последних N сообщений + краткое резюме старой истории через LLM.
|
||||||
|
|
||||||
|
Если хотите, могу помочь с примером React-компонента для графа на React Flow + Dagre или примером IndexedDB-схемы. Также могу подготовить пример автодополнения команд через React Autosuggest.
|
||||||
|
|
||||||
|
Жду ваших пожеланий по следующему шагу!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
а langgrpah - здесь нужен? потому что я хочу, чтобы по слешу не просто промпты поставлялись, а целые действия выполнялись. Где-то промпт присовокупляется к запросу (но промпт не подставляется, т.е. пользователь не видит), а где неявносначала выполняется скрипт генерации с рандомизацией, а его вывод + запрос подается на вход llm через api
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
LangGraph — это именно тот инструмент, который вам нужен для реализации описанной логики с «целыми действиями» по слеш-командам, где:
|
||||||
|
|
||||||
|
- не просто подставляются промпты, а
|
||||||
|
- запускаются сложные workflow с несколькими узлами (например, генерация с рандомизацией, вызов внешних API, анализ изображений, git diff и т.д.),
|
||||||
|
- с хранением состояния и ветвлением диалога в графе,
|
||||||
|
- с возможностью динамически добавлять новые узлы и ветви,
|
||||||
|
- и с контролем, какие промпты и когда подмешиваются к запросам (причём прозрачно для пользователя).
|
||||||
|
|
||||||
|
### Почему LangGraph подходит именно для вашей задачи
|
||||||
|
|
||||||
|
- **Графовая модель**: вы описываете логику как набор узлов (actions, LLM вызовы, скрипты), связанных рёбрами — именно это и есть LangGraph[1][3][5].
|
||||||
|
- **Вызов функций и внешних API**: LangGraph позволяет в узлах выполнять произвольный код (скрипты, запросы), а результат подмешивать к промптам или использовать отдельно[3][4].
|
||||||
|
- **Состояние и память**: хранит состояние диалога, можно реализовать суммаризацию, ветвления, переключение контекста[1][5].
|
||||||
|
- **Мультиагентность и сложные сценарии**: можно объединять разные LLM, инструменты, API, что идеально для ваших слеш-команд с разными действиями[2][6].
|
||||||
|
- **Поддержка потоковой генерации и трассировки**: удобно для интерактивных чатов с визуализацией и отладкой[5].
|
||||||
|
- **Расширяемость под Ollama и другие LLM**: можно подключать любые модели и переключаться между ними[6].
|
||||||
|
|
||||||
|
### Как это будет работать в вашем случае
|
||||||
|
|
||||||
|
1. Пользователь вводит в чат команду с `/` — фронтенд (React) через автодополнение предлагает варианты.
|
||||||
|
2. По выбору команды LangGraph запускает соответствующий узел или цепочку узлов:
|
||||||
|
- например, сначала запускает скрипт генерации с рандомизацией (не показывая пользователю),
|
||||||
|
- потом подмешивает результат в промпт,
|
||||||
|
- вызывает LLM через API,
|
||||||
|
- получает ответ,
|
||||||
|
- анализирует изображения,
|
||||||
|
- при необходимости вызывает git diff, субтитры и т.д.
|
||||||
|
3. Каждый шаг — отдельный узел графа с состоянием, что позволяет легко отлаживать и расширять логику.
|
||||||
|
4. Визуализация графа в React Flow отражает структуру диалога и действия агента.
|
||||||
|
5. Активный узел и ветвь подсвечиваются, чат показывает цепочку сообщений от корня до текущего узла.
|
||||||
|
|
||||||
|
### Итог
|
||||||
|
|
||||||
|
- **LangGraph — это не просто вызов LLM, а полноценный оркестратор сложных агентных workflow с хранением состояния и ветвлением.**
|
||||||
|
- Он идеально подходит, если нужно, чтобы по слеш-командам выполнялись не только промпты, а целые сценарии с вызовами функций, API, анализом и генерацией.
|
||||||
|
- Простые случаи (только промпты) можно реализовать и без него, но для вашей задачи с динамическими действиями и состоянием — LangGraph значительно упростит поддержку и масштабирование.
|
||||||
|
|
||||||
|
### Рекомендация
|
||||||
|
|
||||||
|
Используйте LangGraph как основу backend-логики для вашего агента. Реализуйте узлы для:
|
||||||
|
|
||||||
|
- генерации промптов с рандомизацией,
|
||||||
|
- вызова LLM (Gemini и Mistral),
|
||||||
|
- анализа изображений (Gemini Vision или кастомные API),
|
||||||
|
- git diff и обработки субтитров,
|
||||||
|
- управления состоянием диалога и ветвлениями.
|
||||||
|
|
||||||
|
Фронтенд будет взаимодействовать с LangGraph через API, получать состояние графа и визуализировать его.
|
||||||
|
|
||||||
|
Если хотите, могу помочь с примером простого LangGraph workflow, который реализует слеш-команду с генерацией и вызовом LLM, или с архитектурой API для интеграции с React.
|
||||||
|
|
||||||
|
**Кратко:** Да, LangGraph нужен и рекомендован для вашей задачи, чтобы управлять сложными действиями, а не просто подставлять промпты.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`ConversationSummaryMemory, ConversationSummaryBufferMemory - позволяет задать окно (в токенах), в рамках которого мы сохраняем диалоги в неизменной форме, а при превышении - суммаризируем.
|
||||||
|
|
||||||
|
ConversationBufferWindowMemory - сохраняет только последние k диалогов.
|
||||||
|
|
||||||
|
ConversationKGMemory - использует граф знаний для сохранения памяти.
|
||||||
|
|
||||||
|
ConversationEntityMemory - сохраняет в памяти знания об определенном объекте.` здесь не использовать что-то из этого? как вообще с графом быть?
|
||||||
118
TechSpecification.md
Normal file
118
TechSpecification.md
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
## ТЗ: Интерактивный Агент с LLM и Визуализацией Графа Диалогов
|
||||||
|
|
||||||
|
**Цель:** Создать десктопное приложение-агента, позволяющее взаимодействовать с внешними LLM (Gemini, Mistral) через API, выполнять сложные действия по слеш-командам и визуализировать историю диалогов в виде графа.
|
||||||
|
|
||||||
|
**1. LLM:**
|
||||||
|
|
||||||
|
* Использовать только внешние модели: Gemini (через LangChain), Mistral (через HTTP API).
|
||||||
|
* Заглушка для Ollama (пустой словарь `LOCAL_MODELS`).
|
||||||
|
|
||||||
|
**2. Веб-интерфейс (React):**
|
||||||
|
|
||||||
|
* Три панели:
|
||||||
|
* История диалогов (различные графы запросов), по умолчанию слева.
|
||||||
|
* Визуализация активного графа сообщений (React Flow + Dagre), по умолчанию по центру.
|
||||||
|
* Чат с выбранной веткой активного графа, по умолчанию справа.
|
||||||
|
* Активный чат = выбранный лист графа.
|
||||||
|
* Клик по ноду = показ сообщений от корня до нода.
|
||||||
|
* Разные цвета для нодов пользователя и LLM.
|
||||||
|
|
||||||
|
**3. Хранение:**
|
||||||
|
|
||||||
|
* Бэкенд хранит историю графов запросов.
|
||||||
|
* IndexedDB (dexie.js или idb) для временного хранения активного графа на фронтенде (опционально).
|
||||||
|
|
||||||
|
**4. Суммаризация истории:**
|
||||||
|
|
||||||
|
* Комбинация: последние N сообщений + суммаризация старой истории через LLM (Gemini).
|
||||||
|
|
||||||
|
**5. Анализ изображений:**
|
||||||
|
|
||||||
|
* Gemini Vision API, заглушка для расширения.
|
||||||
|
|
||||||
|
**6. Платформы субтитров:**
|
||||||
|
|
||||||
|
* Google Meet и Microsoft Teams: возможность получения текущих, не финальных субтитров (API или polling/webhook).
|
||||||
|
|
||||||
|
**7. Генерация изображений:**
|
||||||
|
|
||||||
|
* Вызов удаленного эндпоинта bingart.
|
||||||
|
|
||||||
|
Эндпоинт генерации изображений
|
||||||
|
|
||||||
|
* **URL:** `<URL_удаленного_ресурса>/generate-images`
|
||||||
|
* **Метод:** GET или POST.
|
||||||
|
* **Параметры:**
|
||||||
|
* `prompt` (строка): Текст запроса для генерации изображения. Передается либо как query parameter в GET-запросе, либо как form data в POST-запросе.
|
||||||
|
* **Результат (JSON):**
|
||||||
|
* `image_urls` (массив строк): Список URL сгенерированных изображений (до 4 штук).
|
||||||
|
* `error` (строка, опционально): Сообщение об ошибке, если генерация не удалась.
|
||||||
|
|
||||||
|
Пример успешного ответа:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"image_urls": [
|
||||||
|
"url1",
|
||||||
|
"url2",
|
||||||
|
"url3",
|
||||||
|
"url4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример ответа с ошибкой:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Не удалось получить изображения."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**8. Команды:**
|
||||||
|
|
||||||
|
* Слеш-команды (`/`) с автодополнением (React Autosuggest).
|
||||||
|
* Команды хранятся в базе данных на бэкенде.
|
||||||
|
* Запуск workflow через LangGraph.
|
||||||
|
|
||||||
|
**9. LangGraph:**
|
||||||
|
|
||||||
|
* Оркестрация сложных workflow по слеш-командам.
|
||||||
|
* Узлы: генерация, LLM, анализ изображений, субтитры, управление состоянием.
|
||||||
|
|
||||||
|
**10. Реализация:**
|
||||||
|
|
||||||
|
* **Frontend:** React, React Flow, Dagre, React Autosuggest.
|
||||||
|
* **Backend:** API для LLM, субтитров, bingart, LangGraph, база данных (хранение истории графов).
|
||||||
|
|
||||||
|
**11. Язык:**
|
||||||
|
|
||||||
|
* Русский.
|
||||||
|
|
||||||
|
**Типичный workflow:**
|
||||||
|
|
||||||
|
1. Пользователь вводит `/команда`.
|
||||||
|
2. Frontend отправляет запрос на бэкенд.
|
||||||
|
3. Бэкенд запускает LangGraph workflow, соответствующий команде:
|
||||||
|
* Подставляет релевантный команде промпт (если необходимо).
|
||||||
|
* Вызывает LLM (Gemini или Mistral).
|
||||||
|
* Анализирует изображения (если необходимо).
|
||||||
|
* Запрашивает текущие субтитры (если необходимо в рамках команды, это одна из команд).
|
||||||
|
* Запрашивает генерацию изображений на удалённом эндпоинте и ждёт ответа.
|
||||||
|
* Обрабатывает результаты и формирует ответ.
|
||||||
|
4. Бэкенд сохраняет новый граф запроса в базе данных (или обновляет существующий).
|
||||||
|
5. Бэкенд отправляет данные активного графа на фронтенд.
|
||||||
|
6. Frontend отображает граф и чат.
|
||||||
|
|
||||||
|
**Приоритеты:**
|
||||||
|
|
||||||
|
1. Базовая функциональность чата с Gemini/Mistral и визуализацией графа.
|
||||||
|
2. Реализация слеш-команд через LangGraph.
|
||||||
|
3. Интеграция с Google Meet/MS Teams (получение текущих субтитров).
|
||||||
|
|
||||||
|
**Использовать:**
|
||||||
|
|
||||||
|
* React Flow (с примерами Dagre).
|
||||||
|
* LangGraph.
|
||||||
|
* dexie.js или idb (опционально, для временного хранения на фронтенде).
|
||||||
|
* React Autosuggest.
|
||||||
73
app/api.py
Normal file
73
app/api.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
"""
|
||||||
|
Этот файл содержит API endpoints, созданные с использованием Flask.
|
||||||
|
Он обеспечивает взаимодействие с агентом через HTTP запросы,
|
||||||
|
включая обработку сообщений, управление графами и получение истории.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
from workflows import run_agent, graph_history_manager
|
||||||
|
|
||||||
|
app2 = Flask(__name__)
|
||||||
|
CORS(
|
||||||
|
app2
|
||||||
|
) # Разрешаем CORS для всех доменов (в production нужно настроить более строго)
|
||||||
|
|
||||||
|
|
||||||
|
@app2.route('/api/chat', methods=['POST'])
|
||||||
|
def chat():
|
||||||
|
"""API endpoint для обработки сообщений и возврата ответа."""
|
||||||
|
data = request.get_json()
|
||||||
|
message = data.get("message")
|
||||||
|
graph_id = data.get("graph_id")
|
||||||
|
parent_node_id = data.get(
|
||||||
|
"parent_node_id") # Получаем parent_node_id из запроса
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return jsonify({"error": "Сообщение не может быть пустым."}, 400)
|
||||||
|
|
||||||
|
result = run_agent(message, graph_id,
|
||||||
|
parent_node_id) # Передаем parent_node_id в run_agent
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@app2.route('/api/graphs', methods=['GET'])
|
||||||
|
def get_graphs():
|
||||||
|
"""API endpoint для получения списка графов."""
|
||||||
|
graphs = graph_history_manager.get_all_graphs_summary()
|
||||||
|
print("get_graphs response:", graphs) # Добавлено логирование
|
||||||
|
return jsonify(graphs)
|
||||||
|
|
||||||
|
|
||||||
|
@app2.route('/api/graphs/<graph_id>', methods=['GET'])
|
||||||
|
def get_graph_by_id(graph_id):
|
||||||
|
"""API endpoint для получения данных конкретного графа по ID."""
|
||||||
|
graph_data = graph_history_manager.get_graph(graph_id)
|
||||||
|
if graph_data:
|
||||||
|
return jsonify(graph_data)
|
||||||
|
else:
|
||||||
|
return jsonify({"error": f"Граф с ID {graph_id} не найден."}, 404)
|
||||||
|
|
||||||
|
|
||||||
|
@app2.route('/api/graphs/<graph_id>', methods=['DELETE'])
|
||||||
|
def delete_graph(graph_id):
|
||||||
|
"""API endpoint для удаления графа."""
|
||||||
|
if not graph_id:
|
||||||
|
return jsonify({"error": "Не указан ID графа для удаления."}, 400)
|
||||||
|
|
||||||
|
if graph_history_manager.delete_graph(graph_id):
|
||||||
|
return jsonify({"message": f"Граф {graph_id} успешно удален."})
|
||||||
|
else:
|
||||||
|
return jsonify({"error": f"Не удалось удалить граф {graph_id}."}, 500)
|
||||||
|
|
||||||
|
|
||||||
|
@app2.route('/api/messages/<graph_id>/<node_id>', methods=['GET'])
|
||||||
|
def get_messages_from_root_to_node(graph_id, node_id):
|
||||||
|
"""API endpoint для получения сообщений от корня до выбранной ноды."""
|
||||||
|
graph_data = graph_history_manager.get_graph(graph_id)
|
||||||
|
if not graph_data:
|
||||||
|
return jsonify({"error": "Граф не найден."}, 404)
|
||||||
|
|
||||||
|
messages = graph_history_manager.get_messages_from_root_to_node(
|
||||||
|
graph_data, node_id)
|
||||||
|
return jsonify(messages)
|
||||||
190
app/graph_history_manager.py
Normal file
190
app/graph_history_manager.py
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
"""
|
||||||
|
Этот модуль управляет сохранением и извлечением истории графов
|
||||||
|
запросов, используя SQLite базу данных для хранения данных.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
class GraphHistoryManager:
|
||||||
|
"""
|
||||||
|
Менеджер для хранения истории графов запросов в SQLite.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db_path="graph_history.db"):
|
||||||
|
self.db_path = db_path
|
||||||
|
|
||||||
|
def _get_connection(self):
|
||||||
|
"""Получает соединение с базой данных."""
|
||||||
|
return sqlite3.connect(self.db_path)
|
||||||
|
|
||||||
|
def _create_table(self, conn):
|
||||||
|
"""Создает таблицу для хранения графов, если она не существует."""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS graphs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
messages TEXT,
|
||||||
|
graph_nodes TEXT,
|
||||||
|
graph_edges TEXT,
|
||||||
|
current_node_id TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def _get_max_graph_id(self, conn):
|
||||||
|
"""Получает максимальный ID графа из базы данных."""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT MAX(CAST(SUBSTR(id, 7) AS INTEGER)) FROM graphs WHERE id LIKE 'graph_%'"
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()[0]
|
||||||
|
return result if result is not None else 0
|
||||||
|
|
||||||
|
def save_graph(self, graph_data: Any) -> str:
|
||||||
|
"""Сохраняет или обновляет данные графа в базе данных и возвращает ID."""
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
graph_id = graph_data.get("id")
|
||||||
|
if not graph_id:
|
||||||
|
max_graph_id = self._get_max_graph_id(conn)
|
||||||
|
next_graph_id = max_graph_id + 1
|
||||||
|
graph_id = f"graph_{next_graph_id}"
|
||||||
|
graph_data["id"] = graph_id
|
||||||
|
|
||||||
|
# Преобразуем структуры данных в JSON-строки для хранения в SQLite
|
||||||
|
messages_json = json.dumps(graph_data.get("messages", []))
|
||||||
|
graph_nodes_json = json.dumps(graph_data.get("graph_nodes", []))
|
||||||
|
graph_edges_json = json.dumps(graph_data.get("graph_edges", []))
|
||||||
|
current_node_id = graph_data.get("current_node_id", "")
|
||||||
|
|
||||||
|
# Проверяем, существует ли уже запись с таким ID
|
||||||
|
cursor.execute("SELECT id FROM graphs WHERE id = ?", (graph_id, ))
|
||||||
|
existing_record = cursor.fetchone()
|
||||||
|
|
||||||
|
if existing_record:
|
||||||
|
# Обновляем существующую запись
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE graphs SET
|
||||||
|
messages = ?,
|
||||||
|
graph_nodes = ?,
|
||||||
|
graph_edges = ?,
|
||||||
|
current_node_id = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", (messages_json, graph_nodes_json, graph_edges_json,
|
||||||
|
current_node_id, graph_id))
|
||||||
|
else:
|
||||||
|
# Вставляем новую запись
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO graphs (id, messages, graph_nodes, graph_edges, current_node_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""", (graph_id, messages_json, graph_nodes_json,
|
||||||
|
graph_edges_json, current_node_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f"Граф сохранен/обновлен: {graph_id}")
|
||||||
|
return graph_id
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_graph(self, graph_id: str) -> Optional[Any]:
|
||||||
|
"""Получает данные графа по ID из базы данных."""
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT messages, graph_nodes, graph_edges, current_node_id FROM graphs WHERE id = ?",
|
||||||
|
(graph_id, ))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
messages_json, graph_nodes_json, graph_edges_json, current_node_id = result
|
||||||
|
# Преобразуем JSON-строки обратно в структуры данных Python
|
||||||
|
messages = json.loads(messages_json)
|
||||||
|
graph_nodes = json.loads(graph_nodes_json)
|
||||||
|
graph_edges = json.loads(graph_edges_json)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": graph_id,
|
||||||
|
"messages": messages,
|
||||||
|
"graph_nodes": graph_nodes,
|
||||||
|
"graph_edges": graph_edges,
|
||||||
|
"current_node_id": current_node_id
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_all_graphs_summary(self) -> List[Dict[str, str]]:
|
||||||
|
"""Возвращает краткий список всех сохраненных графов."""
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT id, messages FROM graphs")
|
||||||
|
graphs_data = cursor.fetchall()
|
||||||
|
return [{
|
||||||
|
"id":
|
||||||
|
gid,
|
||||||
|
"first_message":
|
||||||
|
json.loads(messages)[0].get("content", "No content")
|
||||||
|
if messages else "No content"
|
||||||
|
} for gid, messages in graphs_data]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_messages_from_root_to_node(
|
||||||
|
self, graph_data: Dict[str, Any],
|
||||||
|
target_node_id: str) -> List[Dict[str, str]]:
|
||||||
|
"""Получает список сообщений от корневого узла до указанного узла."""
|
||||||
|
nodes = graph_data.get("graph_nodes", [])
|
||||||
|
edges = graph_data.get("graph_edges", [])
|
||||||
|
messages = graph_data.get("messages", [])
|
||||||
|
|
||||||
|
# Создаем словарь для быстрого поиска родительских узлов
|
||||||
|
parent_map = {edge['target']: edge['source'] for edge in edges}
|
||||||
|
|
||||||
|
# Функция для рекурсивного подъема по дереву до корневого узла
|
||||||
|
def get_path_to_root(node_id: str) -> List[str]:
|
||||||
|
path = [node_id]
|
||||||
|
while node_id in parent_map:
|
||||||
|
node_id = parent_map[node_id]
|
||||||
|
path.append(node_id)
|
||||||
|
return path[::-1] # Инвертируем, чтобы получить путь от корня
|
||||||
|
|
||||||
|
# Получаем путь от корня до целевого узла
|
||||||
|
path_to_root = get_path_to_root(target_node_id)
|
||||||
|
|
||||||
|
# Собираем сообщения, соответствующие узлам в пути
|
||||||
|
messages_for_path = []
|
||||||
|
node_ids_in_path = set(path_to_root)
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
if "node_id" in message and message["node_id"] in node_ids_in_path:
|
||||||
|
messages_for_path.append({
|
||||||
|
"role": message["role"],
|
||||||
|
"content": message["content"]
|
||||||
|
})
|
||||||
|
|
||||||
|
return messages_for_path
|
||||||
|
|
||||||
|
def delete_graph(self, graph_id: str) -> bool:
|
||||||
|
"""Удаляет граф из базы данных."""
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute("DELETE FROM graphs WHERE id = ?", (graph_id, ))
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при удалении графа: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
196
app/llm_client.py
Normal file
196
app/llm_client.py
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
"""
|
||||||
|
Этот модуль предоставляет инструменты для взаимодействия с различными LLM,
|
||||||
|
включая OpenAI (Gemini) и MistralAI, обеспечивая унифицированный интерфейс.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
|
|
||||||
|
# --- Конфигурация LLM ---
|
||||||
|
# Пустой словарь для локальных моделей (Ollama)
|
||||||
|
LOCAL_MODELS: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Конфигурация внешних моделей
|
||||||
|
MODELS: Dict[str, Dict[str, Any]] = {
|
||||||
|
"gemini-2.0-flash": {
|
||||||
|
"name": "gemini-2.0-flash",
|
||||||
|
"provider": "openai", # Изменено на "openai"
|
||||||
|
"model_name": "gemini-2.0-flash", # Добавлено имя модели для LangChain
|
||||||
|
"apiBase": "https://render-service-gsu7.onrender.com/g/v1beta",
|
||||||
|
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision"],
|
||||||
|
},
|
||||||
|
"gemini-2.5-flash": {
|
||||||
|
"name": "gemini-2.5-flash",
|
||||||
|
"provider": "openai", # Изменено на "openai"
|
||||||
|
"model_name": "gemini-2.5-flash", # Добавлено имя модели для LangChain
|
||||||
|
"apiBase": "https://render-service-gsu7.onrender.com/g/v1beta",
|
||||||
|
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision"],
|
||||||
|
},
|
||||||
|
"gemini-2.5-flash-preview-05-20": {
|
||||||
|
"name": "gemini-2.5-flash-preview-05-20",
|
||||||
|
"provider": "openai", # Изменено на "openai"
|
||||||
|
"model_name":
|
||||||
|
"gemini-2.5-flash-preview-05-20", # Добавлено имя модели для LangChain
|
||||||
|
"apiBase": "https://render-service-gsu7.onrender.com/g/v1beta",
|
||||||
|
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision"],
|
||||||
|
},
|
||||||
|
"mistral-small-latest": {
|
||||||
|
"name": "mistral-small-latest",
|
||||||
|
"provider": "mistralai",
|
||||||
|
"baseUrl": "https://render-service-gsu7.onrender.com/m",
|
||||||
|
"apiKey": "Q0m29fvxBY0Cfdj4sjHaKqccy1NjonLW",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLLM:
|
||||||
|
"""
|
||||||
|
Класс-обертка для взаимодействия с различными LLM.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any]):
|
||||||
|
self.name = config["name"]
|
||||||
|
self.provider = config["provider"]
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
if self.provider == "openai":
|
||||||
|
# Инициализация Gemini через OpenAI API
|
||||||
|
from openai import OpenAI
|
||||||
|
self.client = OpenAI(api_key=config["apiKey"],
|
||||||
|
base_url=config["apiBase"])
|
||||||
|
self.model_name = config["model_name"]
|
||||||
|
self.stream = config["stream"]
|
||||||
|
|
||||||
|
elif self.provider == "mistralai":
|
||||||
|
# Для Mistral используем прямые HTTP-запросы
|
||||||
|
self.base_url = config["baseUrl"]
|
||||||
|
self.api_key = config["apiKey"]
|
||||||
|
self.stream = config.get("stream", False)
|
||||||
|
self.capabilities = config.get("capabilities", [])
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Неизвестный провайдер LLM: {self.provider}")
|
||||||
|
|
||||||
|
def invoke(self, messages: List[Any]) -> str:
|
||||||
|
"""
|
||||||
|
Отправляет запрос к LLM и возвращает ответ.
|
||||||
|
Messages могут быть строкой или списком объектов Langchain Message.
|
||||||
|
"""
|
||||||
|
if self.provider == "openai":
|
||||||
|
# OpenAI ожидает список сообщений в определенном формате
|
||||||
|
openai_messages = []
|
||||||
|
if isinstance(messages, str):
|
||||||
|
openai_messages.append({"role": "user", "content": messages})
|
||||||
|
elif isinstance(messages, list):
|
||||||
|
for msg in messages:
|
||||||
|
if isinstance(msg, HumanMessage):
|
||||||
|
openai_messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": msg.content
|
||||||
|
})
|
||||||
|
elif isinstance(msg, SystemMessage):
|
||||||
|
openai_messages.append({
|
||||||
|
"role": "system",
|
||||||
|
"content": msg.content
|
||||||
|
})
|
||||||
|
else: # Предполагаем, что это другие типы сообщений Langchain или словари
|
||||||
|
openai_messages.append({
|
||||||
|
"role":
|
||||||
|
msg.type if hasattr(msg, 'type') else 'user',
|
||||||
|
"content":
|
||||||
|
msg.content
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.client.chat.completions.create(
|
||||||
|
model=self.model_name,
|
||||||
|
messages=openai_messages,
|
||||||
|
stream=self.stream,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.stream:
|
||||||
|
# Handle streaming responses
|
||||||
|
collected_chunks = []
|
||||||
|
collected_messages = []
|
||||||
|
for chunk in response:
|
||||||
|
collected_chunks.append(chunk)
|
||||||
|
chunk_message = chunk.choices[0].delta.content
|
||||||
|
if chunk_message is not None:
|
||||||
|
collected_messages.append(chunk_message)
|
||||||
|
full_reply_content = ''.join(collected_messages)
|
||||||
|
return full_reply_content
|
||||||
|
else:
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при вызове OpenAI API: {e}")
|
||||||
|
raise
|
||||||
|
elif self.provider == "mistralai":
|
||||||
|
# Отступы исправлены
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
# Формируем сообщения для Mistral API
|
||||||
|
mistral_messages = []
|
||||||
|
if isinstance(messages, str):
|
||||||
|
mistral_messages.append({"role": "user", "content": messages})
|
||||||
|
elif isinstance(messages, list):
|
||||||
|
for msg in messages:
|
||||||
|
if isinstance(msg, HumanMessage):
|
||||||
|
mistral_messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": msg.content
|
||||||
|
})
|
||||||
|
elif isinstance(msg, SystemMessage):
|
||||||
|
mistral_messages.append({
|
||||||
|
"role": "system",
|
||||||
|
"content": msg.content
|
||||||
|
})
|
||||||
|
else: # Предполагаем, что это другие типы сообщений Langchain или словари
|
||||||
|
mistral_messages.append({
|
||||||
|
"role":
|
||||||
|
msg.type if hasattr(msg, 'type') else 'user',
|
||||||
|
"content":
|
||||||
|
msg.content
|
||||||
|
})
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": self.name,
|
||||||
|
"messages": mistral_messages,
|
||||||
|
"stream": self.stream,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(self.base_url,
|
||||||
|
json=payload,
|
||||||
|
headers=headers)
|
||||||
|
response.raise_for_status(
|
||||||
|
) # Вызывает исключение для HTTP ошибок
|
||||||
|
data = response.json()
|
||||||
|
# Извлекаем контент ответа
|
||||||
|
return data["choices"][0]["message"]["content"]
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Ошибка при вызове Mistral API: {e}")
|
||||||
|
raise
|
||||||
|
except KeyError:
|
||||||
|
print(f"Неверный формат ответа от Mistral API: {data}")
|
||||||
|
raise ValueError("Неверный формат ответа от Mistral API")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_llm(name: str) -> CustomLLM:
|
||||||
|
"""
|
||||||
|
Возвращает экземпляр CustomLLM для заданной модели.
|
||||||
|
"""
|
||||||
|
config = MODELS.get(name)
|
||||||
|
if not config:
|
||||||
|
raise ValueError(f"Модель '{name}' не сконфигурирована.")
|
||||||
|
return CustomLLM(config)
|
||||||
39
app/models.py
Normal file
39
app/models.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""
|
||||||
|
Определения моделей данных (pydantic) для представления состояния агента LangGraph.
|
||||||
|
Включают схему для отслеживания хода выполнения диалога и визуализации графа.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from langchain_core.pydantic_v1 import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
# Определяем состояние нашего агента (LangGraph)
|
||||||
|
class AgentState(BaseModel):
|
||||||
|
"""Состояние графа агента."""
|
||||||
|
input: str = Field(description="Исходный запрос пользователя.")
|
||||||
|
command: Optional[str] = Field(None,
|
||||||
|
description="Распознанная слеш-команда.")
|
||||||
|
command_args: List[str] = Field([], description="Аргументы команды.")
|
||||||
|
chat_history: List[Dict[str, str]] = Field(
|
||||||
|
[],
|
||||||
|
description=
|
||||||
|
"Полная история сообщений в текущей ветке диалога. Формат: [{'role': 'user/assistant', 'content': 'message'}]"
|
||||||
|
)
|
||||||
|
llm_response: Optional[str] = Field(None, description="Ответ от LLM.")
|
||||||
|
image_urls: List[str] = Field(
|
||||||
|
[], description="URL сгенерированных изображений.")
|
||||||
|
analysis_result: Optional[str] = Field(
|
||||||
|
None, description="Результат анализа изображения.")
|
||||||
|
subtitles: Optional[str] = Field(None, description="Полученные субтитры.")
|
||||||
|
summarized_history_text: Optional[str] = Field(
|
||||||
|
None, description="Суммированная история.")
|
||||||
|
error: Optional[str] = Field(None, description="Сообщение об ошибке.")
|
||||||
|
# Для визуализации графа (имитация узлов и ребер)
|
||||||
|
graph_nodes: List[Dict[str, Any]] = Field(
|
||||||
|
[], description="Узлы для визуализации графа.")
|
||||||
|
graph_edges: List[Dict[str, Any]] = Field(
|
||||||
|
[], description="Ребра для визуализации графа.")
|
||||||
|
current_node_id: str = Field(
|
||||||
|
"start", description="ID текущего узла графа для фронтенда.")
|
||||||
|
parent_node_id: Optional[str] = Field(None,
|
||||||
|
description="ID родительского узла.")
|
||||||
469
app/nodes.py
Normal file
469
app/nodes.py
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
"""
|
||||||
|
Этот файл содержит узлы для графа состояний агента.
|
||||||
|
Каждый узел выполняет определенную функцию, такую как
|
||||||
|
разбор команд, вызов LLM или выполнение сервисов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
|
|
||||||
|
from llm_client import get_llm
|
||||||
|
from services import ImageGenerationService, SubtitleService, ImageAnalysisService, SummarizationService
|
||||||
|
from models import AgentState
|
||||||
|
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class CommandManager:
|
||||||
|
"""
|
||||||
|
Менеджер для хранения и получения слеш-команд.
|
||||||
|
В реальном приложении команды будут храниться в БД.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._commands = {
|
||||||
|
"help": {
|
||||||
|
"description": "Показывает список доступных команд.",
|
||||||
|
"workflow": "help_workflow"
|
||||||
|
},
|
||||||
|
"imagine": {
|
||||||
|
"description":
|
||||||
|
"Генерирует изображения по промпту. Использование: /imagine [prompt]",
|
||||||
|
"workflow": "image_generation_workflow"
|
||||||
|
},
|
||||||
|
"analyze": {
|
||||||
|
"description":
|
||||||
|
"Анализирует изображение по URL и промпту. Использование: /analyze [url] [prompt]",
|
||||||
|
"workflow": "image_analysis_workflow"
|
||||||
|
},
|
||||||
|
"subtitles_meet": {
|
||||||
|
"description": "Получает текущие субтитры из Google Meet.",
|
||||||
|
"workflow": "get_meet_subtitles_workflow"
|
||||||
|
},
|
||||||
|
"subtitles_teams": {
|
||||||
|
"description": "Получает текущие субтитры из MS Teams.",
|
||||||
|
"workflow": "get_teams_subtitles_workflow"
|
||||||
|
},
|
||||||
|
"summarize": {
|
||||||
|
"description": "Суммирует текущую историю диалога.",
|
||||||
|
"workflow": "summarize_history_workflow"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"description": "Ведет обычный диалог с LLM.",
|
||||||
|
"workflow": "chat_workflow"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_commands(self) -> Dict[str, Any]:
|
||||||
|
"""Возвращает все доступные команды."""
|
||||||
|
return self._commands
|
||||||
|
|
||||||
|
def get_command_workflow(self, command_name: str) -> Optional[str]:
|
||||||
|
"""Возвращает имя workflow для заданной команды."""
|
||||||
|
return self._commands.get(command_name, {}).get("workflow")
|
||||||
|
|
||||||
|
|
||||||
|
# Инициализация сервисов (пример, убедитесь, что они доступны в этом файле или импортированы)
|
||||||
|
command_manager = CommandManager()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_command_node(state: AgentState) -> AgentState:
|
||||||
|
"""Парсит вход пользователя на предмет слеш-команды."""
|
||||||
|
print("Выполняется узел: parse_command_node")
|
||||||
|
user_input = state.input.strip()
|
||||||
|
|
||||||
|
# Добавляем узел пользователя в граф для фронтенда
|
||||||
|
user_node_id = f"user_{len(state.graph_nodes) + 1}"
|
||||||
|
state.graph_nodes.append({
|
||||||
|
"id": user_node_id,
|
||||||
|
"type": "user",
|
||||||
|
"data": {
|
||||||
|
"label":
|
||||||
|
user_input[:30] + "..." if len(user_input) > 30 else user_input
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if state.parent_node_id:
|
||||||
|
state.graph_edges.append({
|
||||||
|
"id": f"e{state.parent_node_id}-{user_node_id}",
|
||||||
|
"source": state.parent_node_id,
|
||||||
|
"target": user_node_id
|
||||||
|
})
|
||||||
|
state.parent_node_id = user_node_id
|
||||||
|
state.current_node_id = user_node_id
|
||||||
|
|
||||||
|
state.chat_history.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": user_input,
|
||||||
|
"node_id": user_node_id # Сохраняем ID узла
|
||||||
|
})
|
||||||
|
|
||||||
|
if user_input.startswith('/'):
|
||||||
|
parts = user_input[1:].split(' ', 1)
|
||||||
|
command_name = parts[0].lower()
|
||||||
|
command_args = parts[1].split(' ') if len(parts) > 1 else []
|
||||||
|
if command_name in command_manager.get_commands():
|
||||||
|
state.command = command_name
|
||||||
|
state.command_args = command_args
|
||||||
|
print(
|
||||||
|
f"Команда распознана: /{state.command} с аргументами: {state.command_args}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
state.error = f"Неизвестная команда: /{command_name}. Введите /help для списка команд."
|
||||||
|
print(state.error)
|
||||||
|
state.command = "error" # Специальный флаг для обработки ошибок команд
|
||||||
|
else:
|
||||||
|
state.command = "chat" # Обычный чат
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def execute_command_node(state: AgentState) -> AgentState:
|
||||||
|
"""Выполняет соответствующий workflow для команды."""
|
||||||
|
print(
|
||||||
|
f"Выполняется узел: execute_command_node для команды: {state.command}")
|
||||||
|
# Этот узел будет выступать в роли маршрутизатора
|
||||||
|
# Фактическая логика команды будет в отдельных узлах, вызываемых по условию
|
||||||
|
return state # Просто передаем состояние дальше, чтобы маршрутизатор смог выбрать следующий узел
|
||||||
|
|
||||||
|
|
||||||
|
# Предполагаем, что используем Gemini для основного LLM и суммаризации/анализа
|
||||||
|
# В реальной системе можно выбрать динамически
|
||||||
|
DEFAULT_LLM_NAME = "gemini-2.5-flash"
|
||||||
|
main_llm = get_llm(DEFAULT_LLM_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def call_llm_node(state: AgentState) -> AgentState:
|
||||||
|
"""Вызывает LLM для обработки обычного диалога или генерации ответа."""
|
||||||
|
print("Выполняется узел: call_llm_node (для обычного чата)")
|
||||||
|
try:
|
||||||
|
# Для обычного чата LLM использует всю историю
|
||||||
|
messages_for_llm = []
|
||||||
|
for msg in state.chat_history:
|
||||||
|
if msg["role"] == "user":
|
||||||
|
messages_for_llm.append(HumanMessage(content=msg["content"]))
|
||||||
|
elif msg["role"] == "assistant":
|
||||||
|
messages_for_llm.append(SystemMessage(content=msg["content"]))
|
||||||
|
|
||||||
|
response = main_llm.invoke(messages_for_llm)
|
||||||
|
|
||||||
|
# Добавляем узел LLM в граф для фронтенда
|
||||||
|
llm_node_id = f"llm_{len(state.graph_nodes) + 1}"
|
||||||
|
state.graph_nodes.append({
|
||||||
|
"id": llm_node_id,
|
||||||
|
"type": "llm",
|
||||||
|
"data": {
|
||||||
|
"label":
|
||||||
|
response[:30] + "..." if len(response) > 30 else response
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.graph_edges.append({
|
||||||
|
"id": f"e{state.parent_node_id}-{llm_node_id}",
|
||||||
|
"source": state.parent_node_id,
|
||||||
|
"target": llm_node_id
|
||||||
|
})
|
||||||
|
state.parent_node_id = llm_node_id
|
||||||
|
state.current_node_id = llm_node_id
|
||||||
|
|
||||||
|
state.chat_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": response,
|
||||||
|
"node_id": llm_node_id # Сохраняем ID узла
|
||||||
|
})
|
||||||
|
state.llm_response = response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
state.error = f"Ошибка при вызове LLM: {e}"
|
||||||
|
state.llm_response = f"Произошла ошибка: {e}"
|
||||||
|
print(state.error)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
image_gen_service = ImageGenerationService(
|
||||||
|
base_url="https://render-service-gsu7.onrender.com/g2"
|
||||||
|
) # TODO: Замените на реальный URL
|
||||||
|
|
||||||
|
|
||||||
|
def generate_images_node(state: AgentState) -> AgentState:
|
||||||
|
"""Генерирует изображения по промпту."""
|
||||||
|
print("Выполняется узел: generate_images_node")
|
||||||
|
prompt = " ".join(state.command_args)
|
||||||
|
if not prompt:
|
||||||
|
state.error = "Для команды /imagine требуется промпт. Пример: /imagine кошка на луне"
|
||||||
|
state.llm_response = state.error
|
||||||
|
return state
|
||||||
|
|
||||||
|
try:
|
||||||
|
urls = image_gen_service.generate_images(prompt)
|
||||||
|
state.image_urls = urls
|
||||||
|
|
||||||
|
# Формируем текст ответа с использованием Markdown для отображения изображений
|
||||||
|
image_markdown = "\n".join([
|
||||||
|
f'<a href="{url}"><img src="{url}" alt="image{i}" style="max-width: 300px; max-height: 300px;"></a>'
|
||||||
|
for i, url in enumerate(urls)
|
||||||
|
])
|
||||||
|
response_text = f"Сгенерировано {len(urls)} изображений:\n{image_markdown}"
|
||||||
|
state.llm_response = response_text
|
||||||
|
|
||||||
|
# Добавляем узел генерации изображений в граф
|
||||||
|
img_gen_node_id = f"img_gen_{len(state.graph_nodes) + 1}"
|
||||||
|
state.graph_nodes.append({
|
||||||
|
"id": img_gen_node_id,
|
||||||
|
"type": "tool",
|
||||||
|
"data": {
|
||||||
|
"label": "Генерация изображений",
|
||||||
|
"details": prompt
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.graph_edges.append({
|
||||||
|
"id": f"e{state.parent_node_id}-{img_gen_node_id}",
|
||||||
|
"source": state.parent_node_id,
|
||||||
|
"target": img_gen_node_id
|
||||||
|
})
|
||||||
|
state.parent_node_id = img_gen_node_id
|
||||||
|
state.current_node_id = img_gen_node_id
|
||||||
|
|
||||||
|
state.llm_response = "Сгенерированы изображения."
|
||||||
|
state.chat_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": response_text,
|
||||||
|
"node_id": img_gen_node_id # Привязываем к ID узла
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
state.error = f"Ошибка генерации изображений: {e}"
|
||||||
|
state.llm_response = f"Произошла ошибка при генерации изображений: {e}"
|
||||||
|
print(state.error)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
image_analysis_service = ImageAnalysisService(
|
||||||
|
llm=get_llm("gemini-2.5-flash")) # Gemini для анализа изображений
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_image_node(state: AgentState) -> AgentState:
|
||||||
|
"""Анализирует изображение по URL или данным."""
|
||||||
|
print("Выполняется узел: analyze_image_node")
|
||||||
|
if len(state.command_args) < 2:
|
||||||
|
state.error = "Для команды /analyze требуется URL изображения и промпт. Пример: /analyze [url] 'что на изображении?'"
|
||||||
|
state.llm_response = state.error
|
||||||
|
return state
|
||||||
|
|
||||||
|
image_url = state.command_args[0]
|
||||||
|
prompt = " ".join(state.command_args[1:])
|
||||||
|
|
||||||
|
try:
|
||||||
|
analysis_result = image_analysis_service.analyze_image(
|
||||||
|
image_url, prompt)
|
||||||
|
state.analysis_result = analysis_result
|
||||||
|
state.llm_response = f"Результат анализа изображения '{image_url}': {analysis_result}"
|
||||||
|
state.chat_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": state.llm_response
|
||||||
|
})
|
||||||
|
|
||||||
|
# Добавляем узел анализа изображения в граф
|
||||||
|
img_analysis_node_id = f"img_analysis_{len(state.graph_nodes) + 1}"
|
||||||
|
state.graph_nodes.append({
|
||||||
|
"id": img_analysis_node_id,
|
||||||
|
"type": "tool",
|
||||||
|
"data": {
|
||||||
|
"label": "Анализ изображения",
|
||||||
|
"details": f"URL: {image_url}, Промпт: {prompt}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.graph_edges.append({
|
||||||
|
"id": f"e{state.parent_node_id}-{img_analysis_node_id}",
|
||||||
|
"source": state.parent_node_id,
|
||||||
|
"target": img_analysis_node_id
|
||||||
|
})
|
||||||
|
state.parent_node_id = img_analysis_node_id
|
||||||
|
state.current_node_id = img_analysis_node_id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
state.error = f"Ошибка анализа изображения: {e}"
|
||||||
|
state.llm_response = f"Произошла ошибка при анализе изображения: {e}"
|
||||||
|
print(state.error)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
subtitle_service = SubtitleService()
|
||||||
|
|
||||||
|
|
||||||
|
def get_meet_subtitles_node(state: AgentState) -> AgentState:
|
||||||
|
"""Получает субтитры из Google Meet."""
|
||||||
|
print("Выполняется узел: get_meet_subtitles_node")
|
||||||
|
try:
|
||||||
|
subtitles = subtitle_service.get_google_meet_subtitles()
|
||||||
|
state.subtitles = subtitles
|
||||||
|
state.llm_response = f"Текущие субтитры из Google Meet: {subtitles}"
|
||||||
|
state.chat_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": state.llm_response
|
||||||
|
})
|
||||||
|
|
||||||
|
# Добавляем узел субтитров Meet в граф
|
||||||
|
meet_subtitles_node_id = f"meet_subtitles_{len(state.graph_nodes) + 1}"
|
||||||
|
state.graph_nodes.append({
|
||||||
|
"id": meet_subtitles_node_id,
|
||||||
|
"type": "tool",
|
||||||
|
"data": {
|
||||||
|
"label": "Субтитры Google Meet",
|
||||||
|
"details": subtitles[:30] + "..." if subtitles else ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.graph_edges.append({
|
||||||
|
"id": f"e{state.parent_node_id}-{meet_subtitles_node_id}",
|
||||||
|
"source": state.parent_node_id,
|
||||||
|
"target": meet_subtitles_node_id
|
||||||
|
})
|
||||||
|
state.parent_node_id = meet_subtitles_node_id
|
||||||
|
state.current_node_id = meet_subtitles_node_id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
state.error = f"Ошибка получения субтитров Google Meet: {e}"
|
||||||
|
state.llm_response = f"Произошла ошибка при получении субтитров Google Meet: {e}"
|
||||||
|
print(state.error)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def get_teams_subtitles_node(state: AgentState) -> AgentState:
|
||||||
|
"""Получает субтитры из MS Teams."""
|
||||||
|
print("Выполняется узел: get_teams_subtitles_node")
|
||||||
|
try:
|
||||||
|
subtitles = subtitle_service.get_ms_teams_subtitles()
|
||||||
|
state.subtitles = subtitles
|
||||||
|
state.llm_response = f"Текущие субтитры из MS Teams: {subtitles}"
|
||||||
|
state.chat_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": state.llm_response
|
||||||
|
})
|
||||||
|
|
||||||
|
# Добавляем узел субтитров Teams в граф
|
||||||
|
teams_subtitles_node_id = f"teams_subtitles_{len(state.graph_nodes) + 1}"
|
||||||
|
state.graph_nodes.append({
|
||||||
|
"id": teams_subtitles_node_id,
|
||||||
|
"type": "tool",
|
||||||
|
"data": {
|
||||||
|
"label": "Субтитры MS Teams",
|
||||||
|
"details": subtitles[:30] + "..." if subtitles else ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.graph_edges.append({
|
||||||
|
"id": f"e{state.parent_node_id}-{teams_subtitles_node_id}",
|
||||||
|
"source": state.parent_node_id,
|
||||||
|
"target": teams_subtitles_node_id
|
||||||
|
})
|
||||||
|
state.parent_node_id = teams_subtitles_node_id
|
||||||
|
state.current_node_id = teams_subtitles_node_id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
state.error = f"Ошибка получения субтитров MS Teams: {e}"
|
||||||
|
state.llm_response = f"Произошла ошибка при получении субтитров MS Teams: {e}"
|
||||||
|
print(state.error)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
summarization_service = SummarizationService(
|
||||||
|
llm=get_llm("gemini-2.5-flash")) # Gemini для суммаризации
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_history_node(state: AgentState) -> AgentState:
|
||||||
|
"""Суммирует историю диалога."""
|
||||||
|
print("Выполняется узел: summarize_history_node")
|
||||||
|
try:
|
||||||
|
# Для суммаризации используем полную историю, которую ведет AgentState
|
||||||
|
summarized_text = summarization_service.summarize_history(
|
||||||
|
state.chat_history)
|
||||||
|
state.summarized_history_text = summarized_text
|
||||||
|
state.llm_response = f"История диалога суммирована:\n{summarized_text}"
|
||||||
|
state.chat_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": state.llm_response
|
||||||
|
})
|
||||||
|
|
||||||
|
# Добавляем узел суммаризации в граф
|
||||||
|
summary_node_id = f"summary_{len(state.graph_nodes) + 1}"
|
||||||
|
state.graph_nodes.append({
|
||||||
|
"id": summary_node_id,
|
||||||
|
"type": "tool",
|
||||||
|
"data": {
|
||||||
|
"label": "Суммаризация истории",
|
||||||
|
"details":
|
||||||
|
summarized_text[:30] + "..." if summarized_text else ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.graph_edges.append({
|
||||||
|
"id": f"e{state.parent_node_id}-{summary_node_id}",
|
||||||
|
"source": state.parent_node_id,
|
||||||
|
"target": summary_node_id
|
||||||
|
})
|
||||||
|
state.parent_node_id = summary_node_id
|
||||||
|
state.current_node_id = summary_node_id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
state.error = f"Ошибка суммаризации истории: {e}"
|
||||||
|
state.llm_response = f"Произошла ошибка при суммаризации истории: {e}"
|
||||||
|
print(state.error)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def handle_error_node(state: AgentState) -> AgentState:
|
||||||
|
"""Обрабатывает ошибки команд."""
|
||||||
|
print(f"Выполняется узел: handle_error_node. Ошибка: {state.error}")
|
||||||
|
state.llm_response = state.error
|
||||||
|
state.chat_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": state.llm_response
|
||||||
|
})
|
||||||
|
|
||||||
|
# Добавляем узел ошибки в граф
|
||||||
|
error_node_id = f"error_{len(state.graph_nodes) + 1}"
|
||||||
|
state.graph_nodes.append({
|
||||||
|
"id": error_node_id,
|
||||||
|
"type": "error",
|
||||||
|
"data": {
|
||||||
|
"label": "Ошибка",
|
||||||
|
"details": state.error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.graph_edges.append({
|
||||||
|
"id": f"e{state.parent_node_id}-{error_node_id}",
|
||||||
|
"source": state.parent_node_id,
|
||||||
|
"target": error_node_id
|
||||||
|
})
|
||||||
|
state.parent_node_id = error_node_id
|
||||||
|
state.current_node_id = error_node_id
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def help_node(state: AgentState) -> AgentState:
|
||||||
|
"""Предоставляет информацию о доступных командах."""
|
||||||
|
print("Выполняется узел: help_node")
|
||||||
|
commands_info = command_manager.get_commands()
|
||||||
|
help_text = "Доступные команды:\n"
|
||||||
|
for cmd, info in commands_info.items():
|
||||||
|
help_text += f"/{cmd}: {info['description']}\n"
|
||||||
|
state.llm_response = help_text
|
||||||
|
state.chat_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": state.llm_response
|
||||||
|
})
|
||||||
|
|
||||||
|
# Добавляем узел справки в граф
|
||||||
|
help_node_id = f"help_{len(state.graph_nodes) + 1}"
|
||||||
|
state.graph_nodes.append({
|
||||||
|
"id": help_node_id,
|
||||||
|
"type": "info",
|
||||||
|
"data": {
|
||||||
|
"label": "Справка",
|
||||||
|
"details": "Список команд"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.graph_edges.append({
|
||||||
|
"id": f"e{state.parent_node_id}-{help_node_id}",
|
||||||
|
"source": state.parent_node_id,
|
||||||
|
"target": help_node_id
|
||||||
|
})
|
||||||
|
state.parent_node_id = help_node_id
|
||||||
|
state.current_node_id = help_node_id
|
||||||
|
|
||||||
|
return state
|
||||||
158
app/services.py
Normal file
158
app/services.py
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
"""
|
||||||
|
Этот файл содержит классы сервисов для выполнения различных задач:
|
||||||
|
- Получение субтитров,
|
||||||
|
- Анализ изображений,
|
||||||
|
- Суммаризация текста,
|
||||||
|
- Генерация изображений.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from llm_client import CustomLLM
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# --- Сервисы для внешних операций ---
|
||||||
|
|
||||||
|
|
||||||
|
class SubtitleService:
|
||||||
|
"""
|
||||||
|
Сервис для получения текущих субтитров. Заглушки.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_google_meet_subtitles(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Получает текущие, не финальные субтитры из Google Meet.
|
||||||
|
(Реализация требует интеграции с Google Workspace API или другим методом.)
|
||||||
|
"""
|
||||||
|
print("Заглушка: Получение субтитров из Google Meet...")
|
||||||
|
# TODO: Реальная интеграция с Google Meet API
|
||||||
|
return "Это текущие субтитры из Google Meet: 'Привет, как дела?'"
|
||||||
|
|
||||||
|
def get_ms_teams_subtitles(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Получает текущие, не финальные субтитры из Microsoft Teams.
|
||||||
|
(Реализация требует интеграции с Microsoft Graph API или другим методом.)
|
||||||
|
"""
|
||||||
|
print("Заглушка: Получение субтитров из Microsoft Teams...")
|
||||||
|
# TODO: Реальная интеграция с Microsoft Teams API
|
||||||
|
return "Это текущие субтитры из MS Teams: 'Отлично, спасибо!'"
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAnalysisService:
|
||||||
|
"""
|
||||||
|
Сервис для анализа изображений с использованием Gemini Vision API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, llm: CustomLLM):
|
||||||
|
self.llm = llm # Ожидаем, что это будет экземпляр Gemini LLM
|
||||||
|
|
||||||
|
def analyze_image(self, image_url_or_bytes: Any, prompt: str) -> str:
|
||||||
|
"""
|
||||||
|
Анализирует изображение с помощью Gemini Vision.
|
||||||
|
`image_url_or_bytes` может быть URL, путем к файлу или байтами изображения.
|
||||||
|
LangChain Gemini Vision принимает это как часть сообщения.
|
||||||
|
"""
|
||||||
|
if "vision" not in self.llm.config.get("capabilities", []):
|
||||||
|
raise ValueError(
|
||||||
|
"Для анализа изображений требуется LLM с поддержкой 'vision' (например, Gemini)."
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Анализ изображения: {image_url_or_bytes} с промптом: {prompt}")
|
||||||
|
|
||||||
|
# LangChain GeminiVision ожидает контент в определенном формате
|
||||||
|
# Для простоты, здесь заглушка, имитирующая обработку изображения.
|
||||||
|
# В реальной реализации, image_url_or_bytes нужно будет соответствующим образом загрузить/передать.
|
||||||
|
# Пример: [HumanMessage(content=[{"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": image_url_or_bytes}}])]
|
||||||
|
# Или для локальных файлов: [{"type": "image_bytes", "image_bytes": {"data": base64_encoded_bytes}}]
|
||||||
|
|
||||||
|
# Поскольку у нас нет реального механизма загрузки изображения здесь,
|
||||||
|
# имитируем ответ LLM для демо-целей.
|
||||||
|
# В реальном приложении:
|
||||||
|
# messages = [
|
||||||
|
# HumanMessage(
|
||||||
|
# content=[
|
||||||
|
# {"type": "text", "text": prompt},
|
||||||
|
# {"type": "image_url", "image_url": {"url": image_url_or_bytes}} # Или image_bytes
|
||||||
|
# ]
|
||||||
|
# )
|
||||||
|
# ]
|
||||||
|
# return self.llm.invoke(messages)
|
||||||
|
|
||||||
|
return f"Заглушка анализа изображения: Изображение '{image_url_or_bytes}' содержит (по результатам анализа Gemini): 'яркий пейзаж с горами и рекой' по запросу '{prompt}'."
|
||||||
|
|
||||||
|
|
||||||
|
class SummarizationService:
|
||||||
|
"""
|
||||||
|
Сервис для суммаризации истории диалога с использованием Gemini LLM.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, llm: CustomLLM):
|
||||||
|
self.llm = llm # Ожидаем, что это будет экземпляр Gemini LLM
|
||||||
|
|
||||||
|
def summarize_history(self,
|
||||||
|
history: List[Dict[str, str]],
|
||||||
|
num_recent_messages: int = 5) -> str:
|
||||||
|
"""
|
||||||
|
Суммирует старую историю диалога с использованием LLM.
|
||||||
|
Комбинация: последние N сообщений + суммаризация старой истории.
|
||||||
|
"""
|
||||||
|
if self.llm.provider != "google":
|
||||||
|
print(
|
||||||
|
"Предупреждение: Суммаризация рекомендуется с Gemini для лучшего качества."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Последние N сообщений
|
||||||
|
recent_messages = history[-num_recent_messages:]
|
||||||
|
old_history = history[:-num_recent_messages]
|
||||||
|
|
||||||
|
summarized_old_history = ""
|
||||||
|
if old_history:
|
||||||
|
# Формируем промпт для суммаризации старой истории
|
||||||
|
old_messages_text = "\n".join(
|
||||||
|
[f"{msg['role']}: {msg['content']}" for msg in old_history])
|
||||||
|
summary_prompt = f"Суммируй следующий диалог максимально кратко, сохранив ключевые моменты и темы:\n---\n{old_messages_text}\n---"
|
||||||
|
try:
|
||||||
|
summarized_old_history = self.llm.invoke(summary_prompt)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при суммаризации старой истории: {e}")
|
||||||
|
summarized_old_history = "Ошибка суммаризации старой истории."
|
||||||
|
|
||||||
|
# Объединяем суммированную старую историю с последними сообщениями
|
||||||
|
full_context = []
|
||||||
|
if summarized_old_history:
|
||||||
|
full_context.append(
|
||||||
|
f"Предшествующая история (краткое содержание): {summarized_old_history}"
|
||||||
|
)
|
||||||
|
for msg in recent_messages:
|
||||||
|
full_context.append(f"{msg['role']}: {msg['content']}")
|
||||||
|
|
||||||
|
return "\n".join(full_context)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageGenerationService:
|
||||||
|
"""
|
||||||
|
Сервис для генерации изображений через удаленный эндпоинт (bingart).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str):
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
def generate_images(self, prompt: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Отправляет запрос на генерацию изображений.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/generate-images"
|
||||||
|
try:
|
||||||
|
# Для простоты используем GET, как указано в ТЗ, хотя POST с body более типичен для prompt
|
||||||
|
response = requests.get(url, params={"prompt": prompt})
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
if "error" in data:
|
||||||
|
raise Exception(
|
||||||
|
f"Ошибка генерации изображений: {data['error']}")
|
||||||
|
return data.get("image_urls", [])
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Ошибка при вызове сервиса генерации изображений: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Не удалось получить изображения: {e}")
|
||||||
|
raise
|
||||||
147
app/workflows.py
Normal file
147
app/workflows.py
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
"""
|
||||||
|
Определяет логику рабочих процессов агента,
|
||||||
|
используя LangGraph для управления состояниями
|
||||||
|
и переходами между узлами обработки.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
from graph_history_manager import GraphHistoryManager
|
||||||
|
from models import AgentState
|
||||||
|
from nodes import parse_command_node, execute_command_node, call_llm_node, generate_images_node, analyze_image_node, get_meet_subtitles_node, get_teams_subtitles_node, summarize_history_node, handle_error_node, help_node
|
||||||
|
# --- LangGraph: Построение графа ---
|
||||||
|
|
||||||
|
graph_history_manager = GraphHistoryManager()
|
||||||
|
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
|
# Добавляем узлы
|
||||||
|
workflow.add_node("parse_command", parse_command_node)
|
||||||
|
workflow.add_node("execute_command",
|
||||||
|
execute_command_node) # Это по сути роутер
|
||||||
|
workflow.add_node("call_llm", call_llm_node)
|
||||||
|
workflow.add_node("generate_images", generate_images_node)
|
||||||
|
workflow.add_node("analyze_image", analyze_image_node)
|
||||||
|
workflow.add_node("get_meet_subtitles", get_meet_subtitles_node)
|
||||||
|
workflow.add_node("get_teams_subtitles", get_teams_subtitles_node)
|
||||||
|
workflow.add_node("summarize_history", summarize_history_node)
|
||||||
|
workflow.add_node("handle_error", handle_error_node)
|
||||||
|
workflow.add_node("help", help_node)
|
||||||
|
|
||||||
|
# Устанавливаем точку входа
|
||||||
|
workflow.set_entry_point("parse_command")
|
||||||
|
|
||||||
|
|
||||||
|
# Определяем маршрутизацию после parse_command
|
||||||
|
def route_command(state: AgentState) -> str:
|
||||||
|
"""Маршрутизирует выполнение на основе распознанной команды."""
|
||||||
|
if state.error:
|
||||||
|
return "handle_error" # Если была ошибка парсинга команды
|
||||||
|
elif state.command == "imagine":
|
||||||
|
return "generate_images"
|
||||||
|
elif state.command == "analyze":
|
||||||
|
return "analyze_image"
|
||||||
|
elif state.command == "subtitles_meet":
|
||||||
|
return "get_meet_subtitles"
|
||||||
|
elif state.command == "subtitles_teams":
|
||||||
|
return "get_teams_subtitles"
|
||||||
|
elif state.command == "summarize":
|
||||||
|
return "summarize_history"
|
||||||
|
elif state.command == "help":
|
||||||
|
return "help"
|
||||||
|
elif state.command == "chat":
|
||||||
|
return "call_llm"
|
||||||
|
elif state.command == "error": # Команда была, но не известная
|
||||||
|
return "handle_error"
|
||||||
|
else:
|
||||||
|
# Fallback на LLM, если команда не распознана или пуста (хотя parse_command должен был бы ее поймать)
|
||||||
|
return "call_llm"
|
||||||
|
|
||||||
|
|
||||||
|
workflow.add_conditional_edges(
|
||||||
|
"parse_command", # Откуда
|
||||||
|
route_command, # Функция, определяющая куда идти
|
||||||
|
{
|
||||||
|
"generate_images": "generate_images",
|
||||||
|
"analyze_image": "analyze_image",
|
||||||
|
"get_meet_subtitles": "get_meet_subtitles",
|
||||||
|
"get_teams_subtitles": "get_teams_subtitles",
|
||||||
|
"summarize_history": "summarize_history",
|
||||||
|
"help": "help",
|
||||||
|
"call_llm": "call_llm",
|
||||||
|
"handle_error":
|
||||||
|
"handle_error", # Для ошибок, найденных в parse_command
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Все узлы операций (кроме ошибок) ведут к END после завершения
|
||||||
|
workflow.add_edge("call_llm", END)
|
||||||
|
workflow.add_edge("generate_images", END)
|
||||||
|
workflow.add_edge("analyze_image", END)
|
||||||
|
workflow.add_edge("get_meet_subtitles", END)
|
||||||
|
workflow.add_edge("get_teams_subtitles", END)
|
||||||
|
workflow.add_edge("summarize_history", END)
|
||||||
|
workflow.add_edge("help", END)
|
||||||
|
workflow.add_edge("handle_error",
|
||||||
|
END) # Ошибка - это тоже конечный пункт для текущего запроса
|
||||||
|
|
||||||
|
# Компилируем граф
|
||||||
|
app = workflow.compile()
|
||||||
|
|
||||||
|
|
||||||
|
def run_agent(user_input: str,
|
||||||
|
existing_graph_id: Optional[str] = None,
|
||||||
|
parent_node_id: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Запускает агента с заданным пользовательским вводом.
|
||||||
|
Может продолжить существующий граф по graph_id.
|
||||||
|
Возвращает обновленное состояние и ID графа.
|
||||||
|
"""
|
||||||
|
initial_state = AgentState(input=user_input, parent_node_id=parent_node_id)
|
||||||
|
|
||||||
|
# Если есть существующий ID графа, загружаем его историю
|
||||||
|
if existing_graph_id:
|
||||||
|
existing_graph_data = graph_history_manager.get_graph(
|
||||||
|
existing_graph_id)
|
||||||
|
if existing_graph_data:
|
||||||
|
print(f"Продолжаю существующий граф {existing_graph_id}")
|
||||||
|
initial_state.chat_history = existing_graph_data.get(
|
||||||
|
"messages", [])
|
||||||
|
initial_state.graph_nodes = existing_graph_data.get(
|
||||||
|
"graph_nodes", [])
|
||||||
|
initial_state.graph_edges = existing_graph_data.get(
|
||||||
|
"graph_edges", [])
|
||||||
|
initial_state.parent_node_id = parent_node_id or existing_graph_data.get(
|
||||||
|
"current_node_id")
|
||||||
|
|
||||||
|
# Запускаем граф
|
||||||
|
result = app.invoke(initial_state)
|
||||||
|
final_state = AgentState(**result)
|
||||||
|
|
||||||
|
# Сохраняем обновленное состояние графа
|
||||||
|
graph_data_to_save = {
|
||||||
|
"id": existing_graph_id,
|
||||||
|
"messages": final_state.chat_history,
|
||||||
|
"graph_nodes": final_state.graph_nodes,
|
||||||
|
"graph_edges": final_state.graph_edges,
|
||||||
|
"current_node_id": final_state.current_node_id
|
||||||
|
}
|
||||||
|
new_graph_id = graph_history_manager.save_graph(graph_data_to_save)
|
||||||
|
|
||||||
|
# Формируем ответ для фронтенда
|
||||||
|
response_data = {
|
||||||
|
"graph_id": new_graph_id,
|
||||||
|
"messages": final_state.chat_history,
|
||||||
|
"llm_response": final_state.llm_response,
|
||||||
|
"image_urls": final_state.image_urls,
|
||||||
|
"analysis_result": final_state.analysis_result,
|
||||||
|
"subtitles": final_state.subtitles,
|
||||||
|
"summarized_history_text": final_state.summarized_history_text,
|
||||||
|
"error": final_state.error,
|
||||||
|
"graph_visualization_data": {
|
||||||
|
"nodes": final_state.graph_nodes,
|
||||||
|
"edges": final_state.graph_edges,
|
||||||
|
"current_node_id": final_state.current_node_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response_data
|
||||||
BIN
graph_history.db
Normal file
BIN
graph_history.db
Normal file
Binary file not shown.
8
requirements.txt
Normal file
8
requirements.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
langgraph
|
||||||
|
langchain
|
||||||
|
langchain_openai
|
||||||
|
|
||||||
|
langchain_google_genai
|
||||||
|
|
||||||
|
flask
|
||||||
|
flask_cors
|
||||||
21
run.py
Normal file
21
run.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# ----------------------------------------------- Imports ------------------------------------------------------------
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# ----------------------------------------------- Main ------------------------------------------------------------
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Добавляем папку app в Python path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
|
||||||
|
from app.api import app2
|
||||||
|
|
||||||
|
print("🚀 Запуск LLM Agent Backend сервера...")
|
||||||
|
print("📍 API будет доступно на: http://localhost:5000/api")
|
||||||
|
print("📊 Список графов: http://localhost:5000/api/graphs")
|
||||||
|
print("💬 Чат эндпоинт: http://localhost:5000/api/chat")
|
||||||
|
print("\n⚠️ Для остановки нажмите Ctrl+C\n")
|
||||||
|
|
||||||
|
app2.run(
|
||||||
|
debug=True,
|
||||||
|
port=5000,
|
||||||
|
host='localhost'
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user