From d27156d15218c8ac2a667c677aa9ee28a4588dba Mon Sep 17 00:00:00 2001 From: dimitrievgs Date: Fri, 12 Sep 2025 00:26:19 +0300 Subject: [PATCH] init --- .gitignore | 6 + Target.md | 134 ++++++++++ Target2.md | 217 ++++++++++++++++ TechSpecification.md | 118 +++++++++ app/api.py | 73 ++++++ app/graph_history_manager.py | 190 ++++++++++++++ app/llm_client.py | 196 +++++++++++++++ app/models.py | 39 +++ app/nodes.py | 469 +++++++++++++++++++++++++++++++++++ app/services.py | 158 ++++++++++++ app/workflows.py | 147 +++++++++++ graph_history.db | Bin 0 -> 286720 bytes requirements.txt | 8 + run.py | 21 ++ 14 files changed, 1776 insertions(+) create mode 100644 .gitignore create mode 100644 Target.md create mode 100644 Target2.md create mode 100644 TechSpecification.md create mode 100644 app/api.py create mode 100644 app/graph_history_manager.py create mode 100644 app/llm_client.py create mode 100644 app/models.py create mode 100644 app/nodes.py create mode 100644 app/services.py create mode 100644 app/workflows.py create mode 100644 graph_history.db create mode 100644 requirements.txt create mode 100644 run.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2275c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +venv/ +env/ + +node_modules/ + +__pycache__ \ No newline at end of file diff --git a/Target.md b/Target.md new file mode 100644 index 0000000..33026cd --- /dev/null +++ b/Target.md @@ -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. Русский. + +--- + diff --git a/Target2.md b/Target2.md new file mode 100644 index 0000000..319be52 --- /dev/null +++ b/Target2.md @@ -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 - сохраняет в памяти знания об определенном объекте.` здесь не использовать что-то из этого? как вообще с графом быть? \ No newline at end of file diff --git a/TechSpecification.md b/TechSpecification.md new file mode 100644 index 0000000..8c85e7f --- /dev/null +++ b/TechSpecification.md @@ -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:** `/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. diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..763b096 --- /dev/null +++ b/app/api.py @@ -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/', 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/', 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//', 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) diff --git a/app/graph_history_manager.py b/app/graph_history_manager.py new file mode 100644 index 0000000..d0ff3da --- /dev/null +++ b/app/graph_history_manager.py @@ -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() diff --git a/app/llm_client.py b/app/llm_client.py new file mode 100644 index 0000000..30cdc52 --- /dev/null +++ b/app/llm_client.py @@ -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) diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..71cb887 --- /dev/null +++ b/app/models.py @@ -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 родительского узла.") diff --git a/app/nodes.py b/app/nodes.py new file mode 100644 index 0000000..5948292 --- /dev/null +++ b/app/nodes.py @@ -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'image{i}' + 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 diff --git a/app/services.py b/app/services.py new file mode 100644 index 0000000..7cd4eb5 --- /dev/null +++ b/app/services.py @@ -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 diff --git a/app/workflows.py b/app/workflows.py new file mode 100644 index 0000000..ff214ca --- /dev/null +++ b/app/workflows.py @@ -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 diff --git a/graph_history.db b/graph_history.db new file mode 100644 index 0000000000000000000000000000000000000000..bfc11bcab2e40e3e011affdb1068d7788f4ff28e GIT binary patch literal 286720 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�(x| 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 Je=+&}{{x$&