no more forbidden things

This commit is contained in:
Pewter71 2026-05-26 23:43:07 +03:00
parent b6274f7106
commit d037834938
5 changed files with 195 additions and 210 deletions

View file

@ -1,15 +1,14 @@
# Qwen Telegram Bot # Qwen Console App
Telegram-бот на Python с четырьмя функциями: Консольное приложение на Python с четырьмя функциями:
- 📷 **описание изображений**присылаешь фото, бот возвращает текстовое описание через vision-модель Qwen - 📷 **описание изображений**указываешь путь к файлу, приложение возвращает текстовое описание через vision-модель Qwen
- 📊 **генерация графиков** — по текстовому запросу бот просит LLM написать код на matplotlib, выполняет его и отправляет PNG - 📊 **генерация графиков** — по текстовому запросу LLM пишет код на matplotlib, приложение выполняет его и сохраняет PNG
- 🏛 **архитектурные схемы** — по описанию системы LLM генерирует DOT, Graphviz рендерит это в PNG - 🏛 **архитектурные схемы** — по описанию системы LLM генерирует DOT, Graphviz рендерит это в PNG
- ♻️ **воспроизведение с фото**кидаешь фото графика или архитектурной схемы, бот распознаёт тип, извлекает структуру и отрисовывает заново в своём стиле - ♻️ **воспроизведение с фото**указываешь путь к фото графика или схемы, приложение распознаёт тип и отрисовывает заново
## Стек ## Стек
- [python-telegram-bot](https://docs.python-telegram-bot.org/) — асинхронный клиент Telegram Bot API
- [openai](https://github.com/openai/openai-python) SDK — работа с любым OpenAI-совместимым провайдером (OpenRouter, DashScope, vLLM и др.) - [openai](https://github.com/openai/openai-python) SDK — работа с любым OpenAI-совместимым провайдером (OpenRouter, DashScope, vLLM и др.)
- [matplotlib](https://matplotlib.org/) + numpy — рендеринг графиков - [matplotlib](https://matplotlib.org/) + numpy — рендеринг графиков
- [Graphviz](https://graphviz.org/) + [python-graphviz](https://github.com/xflr6/graphviz) — рендеринг архитектурных схем из DOT - [Graphviz](https://graphviz.org/) + [python-graphviz](https://github.com/xflr6/graphviz) — рендеринг архитектурных схем из DOT
@ -19,11 +18,10 @@ Telegram-бот на Python с четырьмя функциями:
- Python **3.11+** - Python **3.11+**
- установленный `uv` ([инструкция](https://docs.astral.sh/uv/getting-started/installation/)) - установленный `uv` ([инструкция](https://docs.astral.sh/uv/getting-started/installation/))
- **бинарь Graphviz** в `$PATH` (нужен для команды `/arch`): - **бинарь Graphviz** в `$PATH` (нужен для команды `arch`):
- Linux: `sudo apt install graphviz` - Linux: `sudo apt install graphviz`
- macOS: `brew install graphviz` - macOS: `brew install graphviz`
- Windows: [скачать с graphviz.org](https://graphviz.org/download/) - Windows: [скачать с graphviz.org](https://graphviz.org/download/)
- токен Telegram-бота от [@BotFather](https://t.me/BotFather)
- API-ключ провайдера, поддерживающего мультимодальную модель Qwen - API-ключ провайдера, поддерживающего мультимодальную модель Qwen
## Установка ## Установка
@ -44,57 +42,71 @@ uv sync
cp .env.example .env cp .env.example .env
``` ```
> ⚠️ Для функции описания изображений модель должна быть **мультимодальной (VL)**. Текстовая модель упадёт при попытке передать ей картинку. Переменные окружения:
| Переменная | Обязательна | По умолчанию | Описание |
| --------------------- | ----------- | ----------------------------------------- | ------------------------------------------- |
| `LLM_API_KEY` | ✅ | — | API-ключ провайдера |
| `LLM_BASE_URL` | ❌ | `https://llm.lambda.coredump.ru/v1` | Base URL OpenAI-совместимого API |
| `LLM_MODEL` | ❌ | `qwen3.5-122b` | Имя модели |
| `LLM_DISABLE_THINKING`| ❌ | `false` | Отключить reasoning (для Qwen3/DeepSeek-R1) |
> ⚠️ Для команды `describe` и `repro` модель должна быть **мультимодальной (VL)**. Текстовая модель упадёт при попытке передать ей картинку.
## Запуск ## Запуск
```bash ```bash
uv run bot.py uv run src/app.py
``` ```
Бот запускается в режиме long-polling и пишет лог в stdout. Приложение запускается в интерактивном режиме и ждёт команд.
## Использование ## Использование
| Действие | Что делает бот |
| ------------------------------------ | ----------------------------------------------------------- |
| `/start` или `/help` | Показать справку |
| Отправить фото (без подписи-команды) | Вернуть описание изображения |
| `/plot <описание>` | Сгенерировать и прислать matplotlib-график |
| `/arch <описание>` | Нарисовать архитектурную схему через Graphviz |
| `/repro` + фото | Распознать график/схему и отрисовать её заново |
Примеры запросов к `/plot`:
``` ```
/plot sin(x) и cos(x) на отрезке [0, 2π] >>> <команда> <аргумент>
/plot затухающая синусоида на [0, 10]
/plot столбчатая диаграмма продаж по месяцам с рандомными значениями
/plot гистограмма нормального распределения, 1000 сэмплов
``` ```
Примеры запросов к `/arch`: | Команда | Что делает |
| ------------------------- | ------------------------------------------------------------- |
| `describe <путь>` | Описать изображение |
| `plot <описание>` | Сгенерировать matplotlib-график → сохраняется в `plot.png` |
| `arch <описание>` | Нарисовать архитектурную схему → сохраняется в `arch.png` |
| `repro <путь>` | Воспроизвести график/схему с картинки → сохраняется в `repro.png` |
| `help` | Показать справку |
| `exit` / `quit` / Ctrl+C | Выйти из приложения |
Примеры для `plot`:
``` ```
/arch React-фронт, FastAPI-бэкенд, Postgres и Redis за nginx >>> plot sin(x) и cos(x) на отрезке [0, 2π]
/arch микросервисы: auth, orders, payments. Общаются через RabbitMQ, у каждого своя БД Postgres, Prometheus собирает метрики >>> plot затухающая синусоида на [0, 10]
/arch телеграм-бот, принимает вебхуки, складывает задачи в очередь Celery с Redis-брокером, воркеры пишут результаты в S3 >>> plot столбчатая диаграмма продаж по месяцам с рандомными значениями
>>> plot гистограмма нормального распределения, 1000 сэмплов
``` ```
### Как пользоваться `/repro` Примеры для `arch`:
Два варианта: ```
>>> arch React-фронт, FastAPI-бэкенд, Postgres и Redis за nginx
>>> arch микросервисы: auth, orders, payments. Общаются через RabbitMQ, у каждого своя БД Postgres, Prometheus собирает метрики
>>> arch телеграм-бот, принимает вебхуки, складывает задачи в очередь Celery с Redis-брокером, воркеры пишут результаты в S3
```
1. **Прикрепить фото с подписью** `/repro` — бот сразу обработает его. Пример для `repro`:
2. **Ответить** `/repro` на сообщение с фото (reply-to-message).
Бот классифицирует изображение как график (`plot`) либо архитектурную схему (`architecture`), извлекает структуру через vision-модель, генерирует для неё matplotlib-код или DOT и отрисовывает заново. Это не попиксельная копия — это реконструкция: бот старается передать тот же тип графика/те же компоненты и связи, но стиль будет его собственный. ```
>>> repro chart.png
>>> repro /home/user/screenshots/arch_diagram.jpg
```
Результат `repro` — это не попиксельная копия, а реконструкция: приложение старается передать тот же тип графика / те же компоненты и связи, но стиль будет его собственный.
## Структура проекта ## Структура проекта
``` ```
. .
├── bot.py # основной скрипт: конфиг, LLM-клиент, хэндлеры Telegram ├── app.py # основной скрипт: конфиг, LLM-клиент, консольный цикл
├── pyproject.toml # зависимости и метаданные проекта для uv ├── pyproject.toml # зависимости и метаданные проекта для uv
├── uv.lock # фиксация версий (создаётся после uv sync) ├── uv.lock # фиксация версий (создаётся после uv sync)
├── .env.example # шаблон переменных окружения ├── .env.example # шаблон переменных окружения
@ -116,7 +128,7 @@ uv run <команда> # выполнить команду в окруж
Архитектурные схемы безопаснее: Graphviz парсит DOT как данные, а не исполняет его как код, поэтому инъекция через промпт там в худшем случае даст кривую или огромную картинку. Архитектурные схемы безопаснее: Graphviz парсит DOT как данные, а не исполняет его как код, поэтому инъекция через промпт там в худшем случае даст кривую или огромную картинку.
Для публичного бота стоит изолировать исполнение `/plot`, например: Для использования в более критичной среде стоит изолировать исполнение `plot`:
- отдельный воркер-процесс с `resource.setrlimit` (CPU, память, время) - отдельный воркер-процесс с `resource.setrlimit` (CPU, память, время)
- Docker-контейнер без сети и с read-only FS - Docker-контейнер без сети и с read-only FS
@ -125,9 +137,9 @@ uv run <команда> # выполнить команду в окруж
## Возможные улучшения ## Возможные улучшения
- webhook вместо polling для продакшена - история команд (readline / prompt_toolkit)
- очередь задач (arq / Celery) для тяжёлого рендеринга - автодополнение путей к файлам по Tab
- флаг `--open` для автоматического открытия PNG после генерации
- кэш описаний по хэшу изображения - кэш описаний по хэшу изображения
- вывод сгенерированного кода графика в подписи к картинке (дебаг-режим) - вывод сгенерированного кода графика в режиме отладки (`--debug`)
- rate-limiting на пользователя
- тесты для `render_plot` с фиксированными промптами - тесты для `render_plot` с фиксированными промптами

View file

@ -1,5 +1,4 @@
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
LLM_API_KEY=your_api_key LLM_API_KEY=your_api_key

View file

@ -1,10 +1,9 @@
[project] [project]
name = "qwen-telegram-bot" name = "qwen-console-app"
version = "0.1.0" version = "0.1.0"
description = "Telegram-бот: описание изображений и генерация графиков через Qwen" description = "Консольное приложение: описание изображений и генерация графиков через Qwen"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"python-telegram-bot>=21.0",
"openai>=1.40.0", "openai>=1.40.0",
"python-dotenv>=1.0.0", "python-dotenv>=1.0.0",
"matplotlib>=3.7", "matplotlib>=3.7",

View file

@ -1,9 +1,11 @@
""" """
Telegram-бот с четырьмя функциями: Консольное приложение с четырьмя функциями:
1) получает изображение возвращает его описание (vision-модель Qwen); 1) describe <путь_к_картинке> описание изображения (vision-модель Qwen);
2) /plot <описание> matplotlib-график; 2) plot <описание> matplotlib-график (сохраняется в plot.png);
3) /arch <описание системы> архитектурная схема через Graphviz; 3) arch <описание системы> архитектурная схема через Graphviz (arch.png);
4) /repro (в подписи или ответе к фото) воспроизводит график/схему с картинки. 4) repro <путь_к_картинке> воспроизводит график/схему с картинки (repro.png).
Команды вводятся в интерактивном режиме. Для выхода: exit / quit / Ctrl+C.
""" """
import os import os
@ -15,42 +17,31 @@ import logging
import asyncio import asyncio
import matplotlib import matplotlib
matplotlib.use("Agg") # без GUI-бэкенда, мы только сохраняем в буфер matplotlib.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
import graphviz import graphviz
from dotenv import load_dotenv from dotenv import load_dotenv
from openai import AsyncOpenAI from openai import AsyncOpenAI
from telegram import Update
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
ContextTypes,
filters,
)
# ---------- конфиг ---------- # ---------- конфиг ----------
load_dotenv() load_dotenv()
TELEGRAM_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") LLM_API_KEY = os.getenv("LLM_API_KEY")
LLM_API_KEY = os.getenv("LLM_API_KEY") LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://llm.lambda.coredump.ru/v1")
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://llm.lambda.coredump.ru/v1") LLM_MODEL = os.getenv("LLM_MODEL", "qwen3.5-122b")
LLM_MODEL = os.getenv("LLM_MODEL", "qwen3.5-122b")
# Отключить thinking у reasoning-моделей (актуально для Qwen3/DeepSeek-R1 и т.п.),
# чтобы все токены шли в финальный ответ, а не в <think>-блок.
DISABLE_THINKING = os.getenv("LLM_DISABLE_THINKING", "false").lower() in ("1", "true", "yes") DISABLE_THINKING = os.getenv("LLM_DISABLE_THINKING", "false").lower() in ("1", "true", "yes")
if not TELEGRAM_TOKEN or not LLM_API_KEY: if not LLM_API_KEY:
raise RuntimeError("В .env должны быть заданы TELEGRAM_BOT_TOKEN и LLM_API_KEY") raise RuntimeError("В .env должна быть задана переменная LLM_API_KEY")
logging.basicConfig( logging.basicConfig(
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s", format="%(asctime)s | %(levelname)s | %(message)s",
level=logging.INFO, level=logging.WARNING, # в консоли не засоряем вывод INFO-логами
) )
logger = logging.getLogger("qwen-bot") logger = logging.getLogger("qwen-app")
client = AsyncOpenAI(api_key=LLM_API_KEY, base_url=LLM_BASE_URL) client = AsyncOpenAI(api_key=LLM_API_KEY, base_url=LLM_BASE_URL)
@ -95,7 +86,7 @@ async def describe_image(image_bytes: bytes) -> str:
], ],
} }
], ],
**_chat_kwargs(max_tokens=1024), **_chat_kwargs(max_tokens=8192),
) )
return _extract_content(response).strip() return _extract_content(response).strip()
@ -149,7 +140,7 @@ def render_plot(code: str) -> bytes:
Выполняет сгенерированный код и возвращает PNG-байты. Выполняет сгенерированный код и возвращает PNG-байты.
ВНИМАНИЕ: exec используется в ограниченном неймспейсе, но это не настоящая ВНИМАНИЕ: exec используется в ограниченном неймспейсе, но это не настоящая
песочница. Для публичного бота лучше изолировать исполнение (отдельный песочница. Для публичного приложения лучше изолировать исполнение (отдельный
процесс с resource-лимитами, Docker, nsjail и т. п.). процесс с resource-лимитами, Docker, nsjail и т. п.).
""" """
plt.close("all") plt.close("all")
@ -200,7 +191,7 @@ async def generate_arch_dot(description: str) -> str:
{"role": "system", "content": ARCH_SYSTEM_PROMPT}, {"role": "system", "content": ARCH_SYSTEM_PROMPT},
{"role": "user", "content": description}, {"role": "user", "content": description},
], ],
**_chat_kwargs(max_tokens=8192), # DOT + возможный reasoning могут съесть много **_chat_kwargs(max_tokens=8192),
) )
return _strip_code_fences(_extract_content(response)) return _strip_code_fences(_extract_content(response))
@ -275,145 +266,144 @@ async def analyze_and_reproduce(image_bytes: bytes) -> tuple[str, str, str]:
) )
# ---------- Telegram-хэндлеры ---------- # ---------- вспомогательные утилиты ----------
async def cmd_start(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None: def read_image(path: str) -> bytes:
await update.message.reply_text( """Читает файл изображения с диска."""
"Привет! Я умею:\n" if not os.path.isfile(path):
"📷 описывать фото — просто пришли картинку\n" raise FileNotFoundError(f"Файл не найден: {path}")
"📊 строить графики — команда /plot <описание>\n" with open(path, "rb") as f:
"🏛 рисовать архитектурные схемы — команда /arch <описание системы>\n" return f.read()
"♻️ воспроизводить график/схему с фото — пришли фото с подписью /repro "
"или ответь /repro на сообщение с фото\n\n"
"Примеры:\n" def save_png(png: bytes, filename: str) -> str:
" /plot sin(x) и cos(x) на отрезке [0, 2π]\n" """Сохраняет PNG-байты в файл рядом со скриптом и возвращает путь."""
" /arch React-фронт, FastAPI-бэкенд, Postgres и Redis за nginx" out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
with open(out_path, "wb") as f:
f.write(png)
return out_path
def print_help() -> None:
print(
"\nДоступные команды:\n"
" describe <путь> — описать изображение\n"
" plot <описание> — построить график (сохраняется в plot.png)\n"
" arch <описание> — нарисовать архитектурную схему (arch.png)\n"
" repro <путь> — воспроизвести график/схему с картинки (repro.png)\n"
" help — эта справка\n"
" exit / quit — выход\n"
) )
async def on_photo(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None: # ---------- обработчики команд ----------
await update.message.chat.send_action("typing")
photo = update.message.photo[-1] # берём самый большой размер
tg_file = await photo.get_file()
image_bytes = bytes(await tg_file.download_as_bytearray())
try:
text = await describe_image(image_bytes)
await update.message.reply_text(text)
except Exception as e:
logger.exception("Ошибка при описании изображения")
await update.message.reply_text(f"Не смог разобрать изображение: {e}")
async def handle_describe(arg: str) -> None:
async def cmd_plot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not arg:
request = " ".join(context.args).strip() print("Укажи путь к картинке. Пример: describe photo.jpg")
if not request:
await update.message.reply_text(
"Опиши, какой график построить. Пример:\n/plot затухающая синусоида на [0, 10]"
)
return return
print("⏳ Анализирую изображение...")
await update.message.chat.send_action("upload_photo") image_bytes = read_image(arg)
try: text = await describe_image(image_bytes)
code = await generate_plot_code(request) print(f"\n{text}\n")
logger.info("Сгенерирован код:\n%s", code)
png = await asyncio.to_thread(render_plot, code)
await update.message.reply_photo(photo=png, caption=request[:1024])
except Exception as e:
logger.exception("Ошибка при построении графика")
await update.message.reply_text(f"Не получилось построить график: {e}")
async def cmd_arch(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def handle_plot(arg: str) -> None:
description = " ".join(context.args).strip() if not arg:
if not description: print("Опиши, какой график построить. Пример: plot sin(x) на [0, 2π]")
await update.message.reply_text( return
print("⏳ Генерирую код и строю график...")
code = await generate_plot_code(arg)
logger.debug("Сгенерирован код:\n%s", code)
png = await asyncio.to_thread(render_plot, code)
path = save_png(png, "plot.png")
print(f"✅ График сохранён: {path}\n")
async def handle_arch(arg: str) -> None:
if not arg:
print(
"Опиши архитектуру системы. Пример:\n" "Опиши архитектуру системы. Пример:\n"
"/arch React-фронт, FastAPI-бэкенд с воркерами Celery, Postgres, Redis и S3" " arch React-фронт, FastAPI-бэкенд, Postgres и Redis за nginx"
) )
return return
print("⏳ Генерирую архитектурную схему...")
await update.message.chat.send_action("upload_photo") dot = await generate_arch_dot(arg)
try: logger.debug("Сгенерирован DOT:\n%s", dot)
dot = await generate_arch_dot(description) png = await asyncio.to_thread(render_architecture, dot)
logger.info("Сгенерирован DOT:\n%s", dot) path = save_png(png, "arch.png")
png = await asyncio.to_thread(render_architecture, dot) print(f"✅ Схема сохранена: {path}\n")
await update.message.reply_photo(photo=png, caption=description[:1024])
except graphviz.ExecutableNotFound:
logger.exception("Нет бинаря graphviz")
await update.message.reply_text(
"На сервере не установлен Graphviz. Поставь его: `apt install graphviz` или `brew install graphviz`."
)
except Exception as e:
logger.exception("Ошибка при построении архитектуры")
await update.message.reply_text(f"Не получилось построить схему: {e}")
async def cmd_repro(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None: async def handle_repro(arg: str) -> None:
""" if not arg:
Воспроизводит график/схему с приложенного фото. print("Укажи путь к картинке. Пример: repro chart.png")
Работает двумя способами: return
1) фото с подписью "/repro" print("⏳ Анализирую и воспроизвожу...")
2) команда "/repro" в ответ (reply) на сообщение с фото image_bytes = read_image(arg)
""" kind, code, notes = await analyze_and_reproduce(image_bytes)
msg = update.message logger.debug("repro type=%s notes=%s\ncode:\n%s", kind, notes, code)
if msg.photo:
source_msg = msg if kind == "plot":
elif msg.reply_to_message and msg.reply_to_message.photo: png = await asyncio.to_thread(render_plot, code)
source_msg = msg.reply_to_message path = save_png(png, "repro.png")
label = f"📊 {notes}" if notes else "📊 Воспроизведённый график"
print(f"{label}\n Сохранён: {path}\n")
elif kind == "architecture":
png = await asyncio.to_thread(render_architecture, code)
path = save_png(png, "repro.png")
label = f"🏛 {notes}" if notes else "🏛 Воспроизведённая схема"
print(f"{label}\n Сохранена: {path}\n")
else: else:
await msg.reply_text( msg = "Это не похоже ни на график, ни на архитектурную схему."
"Пришли фото с подписью /repro или ответь /repro на сообщение с фото." if notes:
) msg += f"\n{notes}"
return print(f" {msg}\n")
await msg.chat.send_action("upload_photo")
tg_file = await source_msg.photo[-1].get_file()
image_bytes = bytes(await tg_file.download_as_bytearray())
try:
kind, code, notes = await analyze_and_reproduce(image_bytes)
logger.info("repro type=%s notes=%s\ncode:\n%s", kind, notes, code)
if kind == "plot":
png = await asyncio.to_thread(render_plot, code)
caption = f"📊 {notes}" if notes else "📊 Воспроизведённый график"
elif kind == "architecture":
png = await asyncio.to_thread(render_architecture, code)
caption = f"🏛 {notes}" if notes else "🏛 Воспроизведённая схема"
else:
await msg.reply_text(
"Это не похоже ни на график, ни на архитектурную схему."
+ (f"\n\n{notes}" if notes else "")
)
return
await msg.reply_photo(photo=png, caption=caption[:1024])
except graphviz.ExecutableNotFound:
logger.exception("Нет бинаря graphviz")
await msg.reply_text(
"На сервере не установлен Graphviz. Поставь его: `apt install graphviz` / "
"`brew install graphviz` / `winget install graphviz`."
)
except Exception as e:
logger.exception("Ошибка при воспроизведении")
await msg.reply_text(f"Не получилось воспроизвести: {e}")
# ---------- запуск ---------- # ---------- главный цикл ----------
def main() -> None: async def main() -> None:
app = Application.builder().token(TELEGRAM_TOKEN).build() print("🤖 Qwen Console App")
app.add_handler(CommandHandler(["start", "help"], cmd_start)) print_help()
app.add_handler(CommandHandler("plot", cmd_plot))
app.add_handler(CommandHandler("arch", cmd_arch)) while True:
app.add_handler(CommandHandler("repro", cmd_repro)) try:
# фото без подписи-команды → описание (иначе команда уже обработана выше) line = input(">>> ").strip()
app.add_handler( except (EOFError, KeyboardInterrupt):
MessageHandler(filters.PHOTO & ~filters.CaptionRegex(r"^/"), on_photo) print("\nВыход.")
) break
logger.info("Бот запущен")
app.run_polling(allowed_updates=Update.ALL_TYPES) if not line:
continue
parts = line.split(maxsplit=1)
cmd = parts[0].lower()
arg = parts[1].strip() if len(parts) > 1 else ""
try:
if cmd in ("exit", "quit"):
print("Выход.")
break
elif cmd == "help":
print_help()
elif cmd == "describe":
await handle_describe(arg)
elif cmd == "plot":
await handle_plot(arg)
elif cmd == "arch":
await handle_arch(arg)
elif cmd == "repro":
await handle_repro(arg)
else:
print(f"Неизвестная команда: {cmd!r}. Введи help для справки.")
except FileNotFoundError as e:
print(f"{e}")
except graphviz.ExecutableNotFound:
print("❌ Graphviz не установлен. Установи: apt install graphviz / brew install graphviz / winget install graphviz")
except Exception as e:
print(f"❌ Ошибка: {e}")
if __name__ == "__main__": if __name__ == "__main__":
main() asyncio.run(main())

17
uv.lock generated
View file

@ -852,20 +852,7 @@ wheels = [
] ]
[[package]] [[package]]
name = "python-telegram-bot" name = "qwen-console-app"
version = "22.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpcore", marker = "python_full_version >= '3.14'" },
{ name = "httpx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/25/2258161b1069e66d6c39c0a602dbe57461d4767dc0012539970ea40bc9d6/python_telegram_bot-22.7.tar.gz", hash = "sha256:784b59ea3852fe4616ad63b4a0264c755637f5d725e87755ecdee28300febf61", size = 1516454, upload-time = "2026-03-16T09:36:03.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/f7/0e2f89dd62f45d46d4ea0d8aec5893ce5b37389638db010c117f46f11450/python_telegram_bot-22.7-py3-none-any.whl", hash = "sha256:d72eed532cf763758cd9331b57a6d790aff0bb4d37d8f4e92149436fe21c6475", size = 745365, upload-time = "2026-03-16T09:36:01.498Z" },
]
[[package]]
name = "qwen-telegram-bot"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
@ -874,7 +861,6 @@ dependencies = [
{ name = "numpy" }, { name = "numpy" },
{ name = "openai" }, { name = "openai" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "python-telegram-bot" },
] ]
[package.metadata] [package.metadata]
@ -884,7 +870,6 @@ requires-dist = [
{ name = "numpy", specifier = ">=1.24" }, { name = "numpy", specifier = ">=1.24" },
{ name = "openai", specifier = ">=1.40.0" }, { name = "openai", specifier = ">=1.40.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" }, { name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "python-telegram-bot", specifier = ">=21.0" },
] ]
[[package]] [[package]]