A => .env.example +41 -0
@@ 1,41 @@
+# =============================================================================
+# REQUIRED CONFIGURATION - Must be filled before running the bot
+# =============================================================================
+
+# REQUIRED! Enter your Telegram bot token (get from @BotFather)
+TELEGRAM_BOT_TOKEN=XXX
+
+# REQUIRED! Enter the OpenAI API key (get from https://platform.openai.com/api-keys)
+API_KEY_OPENAI=XXX
+
+# =============================================================================
+# DATABASE CONFIGURATION - Default settings, no changes needed
+# =============================================================================
+
+# PostgreSQL database connection settings
+DATABASE_URL=postgresql+psycopg://user:password@postgres_agent_db:5432/agent_ai_bot
+POSTGRES_USER=user
+POSTGRES_PASSWORD=password
+POSTGRES_DB=agent_ai_bot
+
+# Redis cache connection settings
+REDIS_URL=redis://redis_agent:6379/0
+
+# =============================================================================
+# PAY MODE CONFIGURATION - Only required if using monetized features
+# =============================================================================
+
+# Solana wallet address for token burning operations
+TOKEN_BURN_ADDRESS=XXX
+
+# Solana token contract address for user balance top-ups
+MINT_TOKEN_ADDRESS=XXX
+
+# TON blockchain address for receiving payments
+TON_ADDRESS=XXX
+
+# TONAPI service key for blockchain operations (get from https://tonapi.io)
+API_KEY_TON=XXX
+
+# Solana address for receiving payments
+ADDRESS_SOL=XXX
A => .github/workflows/docker-publish.yml +73 -0
@@ 1,73 @@
+name: Docker Build & Publish
+
+on:
+ push:
+ branches: [ main ]
+ tags: [ 'v*' ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Log in to Container Registry
+ if: github.event_name != 'pull_request'
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ docker-compose-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Create test .env file
+ run: |
+ cp .env.example .env
+ # Replace with dummy values for testing
+ sed -i 's/XXX/test_token_123/g' .env
+
+ - name: Test Docker Compose build
+ run: |
+ docker compose build --no-cache
+ echo "Docker Compose build successful!"
+
+ - name: Test Docker Compose services
+ run: |
+ # Test if services can start (without actually running them)
+ docker compose config --quiet
+ echo "Docker Compose configuration is valid!"
A => .github/workflows/python-ci.yml +92 -0
@@ 1,92 @@
+name: Python CI
+
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
+ branches: [ main, develop ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: [3.9, "3.10", "3.11"]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Cache pip dependencies
+ uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install flake8 black isort
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+
+ - name: Code formatting check with Black
+ run: |
+ black --check --diff .
+
+ - name: Import sorting check with isort
+ run: |
+ isort --check-only --diff .
+
+ - name: Lint with flake8
+ run: |
+ # Stop the build if there are Python syntax errors or undefined names
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+ # Exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+
+ - name: Check for security issues with bandit
+ run: |
+ pip install bandit
+ bandit -r . -f json || true
+
+ docker-test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build Docker image
+ run: |
+ docker build -t evi-run-test .
+
+ - name: Test Docker container
+ run: |
+ # Test if container builds successfully
+ docker run --rm evi-run-test python --version
+ echo "Docker build test passed!"
+
+ dependency-check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.9'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install safety pip-audit
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+
+ - name: Check for security vulnerabilities
+ run: |
+ safety check || true
+ pip-audit || true
A => .github/workflows/release.yml +113 -0
@@ 1,113 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ create-release:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Generate changelog
+ id: changelog
+ run: |
+ # Get the latest two tags
+ CURRENT_TAG=${GITHUB_REF#refs/tags/}
+ PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
+
+ echo "current_tag=$CURRENT_TAG" >> $GITHUB_OUTPUT
+
+ # Generate changelog
+ if [ -n "$PREVIOUS_TAG" ]; then
+ echo "## What's Changed" > CHANGELOG.tmp
+ git log --pretty=format:"* %s (%h)" $PREVIOUS_TAG..$CURRENT_TAG >> CHANGELOG.tmp
+ else
+ echo "## What's Changed" > CHANGELOG.tmp
+ echo "* Initial release" >> CHANGELOG.tmp
+ fi
+
+ # Read changelog into output
+ {
+ echo 'CHANGELOG<<EOF'
+ cat CHANGELOG.tmp
+ echo 'EOF'
+ } >> $GITHUB_OUTPUT
+
+ - name: Create Release
+ uses: softprops/action-gh-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ steps.changelog.outputs.current_tag }}
+ name: evi.run ${{ steps.changelog.outputs.current_tag }}
+ body: |
+ 🚀 **evi.run Release ${{ steps.changelog.outputs.current_tag }}**
+
+ ${{ steps.changelog.outputs.CHANGELOG }}
+
+ ## 📦 Installation
+
+ ```bash
+ # Quick install with Docker
+ git clone https://github.com/${{ github.repository }}.git
+ cd evi-run
+ cp .env.example .env
+ # Edit .env with your credentials
+ chmod +x docker_setup_en.sh
+ ./docker_setup_en.sh
+ docker compose up --build -d
+ ```
+
+ ## 🔗 Useful Links
+ - 📚 [Documentation](https://github.com/${{ github.repository }}/blob/main/README.md)
+ - 🤝 [Contributing](https://github.com/${{ github.repository }}/blob/main/CONTRIBUTING.md)
+ - 💬 [Support](https://t.me/playa3000)
+
+ **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.changelog.outputs.previous_tag }}...${{ steps.changelog.outputs.current_tag }}
+ draft: false
+ prerelease: false
+
+ docker-release:
+ needs: create-release
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Log in to Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract version from tag
+ id: version
+ run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
+
+ - name: Build and push release image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: |
+ ghcr.io/${{ github.repository }}:latest
+ ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
+ labels: |
+ org.opencontainers.image.title=evi.run
+ org.opencontainers.image.description=Customizable Multi-Agent AI System
+ org.opencontainers.image.version=${{ steps.version.outputs.version }}
+ org.opencontainers.image.source=https://github.com/${{ github.repository }}
A => .gitignore +18 -0
@@ 1,18 @@
+# Виртуальное окружение
+venv/
+env/
+
+# Кэш Python
+__pycache__/
+*.py[cod]
+*$py.class
+
+# Файлы сред и конфигураций
+.env
+
+# Логи
+*.log
+
+# Временные файлы
+.DS_Store
+__MACOSX/<
\ No newline at end of file
A => CONTRIBUTING.md +99 -0
@@ 1,99 @@
+# Contributing to evi.run
+
+Thank you for your interest in contributing to **evi.run**! We welcome contributions from the community.
+
+## 🚀 Quick Start
+
+1. **Fork the repository** on GitHub
+2. **Clone your fork** locally:
+ ```bash
+ git clone https://github.com/pipedude/evi-run.git
+ cd evi-run
+ ```
+3. **Create a feature branch**:
+ ```bash
+ git checkout -b feature/your-feature-name
+ ```
+4. **Make your changes** and test them
+5. **Commit your changes**:
+ ```bash
+ git commit -m "feat: add your feature description"
+ ```
+6. **Push to your fork**:
+ ```bash
+ git push origin feature/your-feature-name
+ ```
+7. **Create a Pull Request** on GitHub
+
+## 🛠️ Development Setup
+
+### Prerequisites
+- Python 3.9+
+- Docker & Docker Compose
+- Git
+
+### Local Development
+```bash
+# Clone and setup
+git clone https://github.com/pipedude/evi-run.git
+cd evi-run
+
+# Configure environment
+cp .env.example .env
+nano .env # Add your API keys
+nano config.py # Set your Telegram ID
+
+# Start development environment
+docker compose up --build
+```
+
+## 📋 Types of Contributions
+
+- **🐛 Bug Reports**: Create detailed issue reports
+- **✨ Features**: Propose and implement new features
+- **📚 Documentation**: Improve README or code comments
+- **🧪 Tests**: Add or improve test coverage
+- **🌐 Localization**: Add translations in `I18N/`
+- **🤖 Custom Agents**: Add new AI agents in `bot/agents_tools/`
+
+## 🎯 Guidelines
+
+### Code Style
+- Follow **PEP 8** for Python
+- Use meaningful variable names
+- Add docstrings for functions
+- Keep functions focused and small
+
+### Commit Messages
+Use conventional commit format:
+```
+feat(agents): add new trading agent
+fix(database): resolve connection issue
+docs(readme): update installation guide
+style: format code according to PEP 8
+```
+
+### Testing
+```bash
+# Run tests
+python -m pytest
+
+# Run with coverage
+python -m pytest --cov=bot
+```
+
+## 🔒 Security
+
+- **Never commit API keys or secrets**
+- Use environment variables for sensitive data
+- Report security issues privately to project maintainers
+
+## 📞 Support
+
+- **Issues**: [GitHub Issues](https://github.com/pipedude/evi-run/issues)
+- **Telegram**: [@playa3000](https://t.me/playa3000)
+- **Community**: [Telegram Support Group](https://t.me/evi_run)
+
+---
+
+**Thank you for contributing to evi.run! 🚀**
A => Dockerfile +31 -0
@@ 1,31 @@
+FROM python:3.11-slim
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends build-essential gcc \
+ && rm -rf /var/lib/apt/lists/*
+
+# --- system deps ---
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ curl gnupg git && \
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
+ apt-get install -y --no-install-recommends nodejs && \
+ rm -rf /var/lib/apt/lists/*
+
+# --- Jupiter-MCP ---
+RUN npm install -g github:pipedude/jupiter-mcp
+
+RUN npm install -g dexpaprika-mcp
+
+WORKDIR /app
+
+COPY requirements.txt .
+
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+#COPY ./index.js /usr/lib/node_modules/jupiter-mcp/index.js
+
+# Specify the command that will be executed when the container is started
+CMD ["python", "-u", "-m", "bot.main"]<
\ No newline at end of file
A => Dockerfile_fastapi +11 -0
@@ 1,11 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+COPY requirements_fastapi.txt .
+
+RUN pip install --no-cache-dir -r requirements_fastapi.txt
+
+COPY . .
+
+CMD ["uvicorn", "payment_module.app:app", "--host", "0.0.0.0", "--port", "8000"]<
\ No newline at end of file
A => I18N/en/txt.ftl +140 -0
@@ 1,140 @@
+start_text =
+ I'm Evy — a practicing tech witch! 😈✨
+
+ Haha, actually I'm a multi-agent system with artificial intelligence and I know practically everything about this... and other worlds! 🦄👻
+
+ I can perform some tasks and assignments for you, or we can just have fun together. My capabilities include: conducting deep research, searching for current information on the internet, working with documents and images, creating images, DEX analytics, token swapping on DEX. I have customizable memory, and I can remember important information from our conversations. You can also delete the history of our conversations or add new knowledge to my memory. 🧙♀️🔮
+
+ Just write in chat or send voice messages to start interacting! 🪄✨
+
+close_kb = Close
+
+command_new_text = Confirm starting a new dialog without saving the current one. It is recommended to complete the current task before starting a new dialog! After deletion, the current history will be erased and the context will be reset.
+
+command_approve_new_text = Current dialog deleted!
+
+command_new_approve_kb = Confirm
+
+command_new_save_kb = Save dialog
+
+command_save_text = Confirm saving the current dialog to memory. It is recommended to complete the current task before saving! After saving, a new dialog will start with reset context, but key moments from the current conversation will remain in the system memory.
+
+command_save_approve_kb = Current dialog saved to system memory!
+
+command_delete_text = Confirm deletion of the current dialog and all system memory about you.
+
+command_delete_approve_text = System memory about you and dialog deleted! Start a new dialog.
+
+token_price_error_text = Wrong format, example: 0.01
+
+not_token_price_error_text = You haven't set the token price yet!
+
+token_price_updated_text = Token price updated!
+
+command_wallet_text =
+ If you already have a linked wallet, entering a new private key will replace it. Enter the Solana wallet private key in format [45, 456, …].
+
+ Warning: use a separate wallet with a small balance, as the trading agent works in test mode!
+
+cmd_help_text =
+ Interact with the system through the chat window. All functions are available through regular messages. Use Menu for additional parameter settings.
+
+command_settings_text = Settings
+
+settings_language_text = Interface language
+
+text_choose_lang = Choose interface language:
+
+back_kb = Back
+
+text_document_upload = File successfully uploaded! You can ask a question.
+
+command_knowledge_text = This is the system's general knowledge base. Added information will be available to all users (when using modes: free and pay)! Do you want to add files or clear the knowledge base?
+
+command_knowledge_add_kb = Add information
+
+command_knowledge_delete_kb = Clear knowledge base
+
+command_knowledge_add_text = Send a text file with information to the chat! Add only one file at a time to the chat!
+
+text_not_format_file = Wrong format, please try again! Supported document formats: .pdf, .txt, .md, .doc(x), .pptx and .py
+
+text_approve_file = File successfully uploaded! You can ask a question.
+
+command_knowledge_delete_text = Confirm deletion of the knowledge base.
+
+text_approve_delete = Knowledge base deleted. Add new information to the knowledge base.
+
+warning_save_context_txt = Error saving context to txt file!
+
+warning_text_no_credits = Insufficient credits!
+
+wait_answer_text = One moment ✨
+
+answer_md = Download answer
+
+warning_text_tokens = Dialog size exceeds 15,000 tokens! To save resources, you can save the dialog to system memory or delete it through the menu after completing the current task.
+
+warning_text_format = Wrong format!
+
+warning_text_error = An error occurred!
+
+cmd_wallet_text_start =
+ If you already have a linked wallet, entering a new private key will replace it. Enter the Solana wallet private key in format [45, 456, …].
+
+ Warning: use a separate wallet with a small balance, as the trading agent works in test mode!
+
+wallet_balance_kb = Wallet balance
+
+wallet_delete_key = Delete private key
+
+not_format_wallet_key = Wrong format! Write the Solana wallet private key in format [45, 456, …].
+
+text_after_add_key =
+ Agent gained access to the wallet.
+
+ Wallet balance:
+
+
+wallet_delete_key_text = Confirm deletion of the private key.
+
+command_delete_key_approve_text = Private key deleted. Link a new wallet to use the trading agent.
+
+text_balance_wallet = Wallet balance:
+
+cmd_wallet_text = Your balance:
+
+add_balance_kb = Top up balance
+
+text_add_balance = Enter payment amount in $, whole number not less than $1.
+
+text_add_balance_error = Please try again! Enter payment amount in $, whole number not less than $1.
+
+choose_type_pay_text = Choose top-up method:
+
+ton_type_kb = TON
+
+sol_type_kb = Token (Solana)
+
+error_create_payment = An error occurred while creating the payment. Please try later.
+
+check_payment_kb = Check payment
+
+text_payment_create =
+ Make a payment for: <code>{ $sum }</code>
+ Wallet: <code>{ $wallet }</code>
+
+text_payment_create_sol =
+ Make a payment for: <code>{ $sum }</code> tokens
+ Wallet: <code>{ $wallet }</code>
+ Token address: <code>{ $token }</code>
+
+error_get_token_price = Token price not specified. Please specify token price /token_price.
+
+wait_check_payment_text = Checking payment ⏳
+
+check_payment_success_text = Payment completed successfully!
+
+check_payment_error_text = Payment was not completed! Please try later.
+
+warning_text_no_row_md = Context was deleted. Row not found in database.<
\ No newline at end of file
A => I18N/factory.py +26 -0
@@ 1,26 @@
+from fluent_compiler.bundle import FluentBundle
+from fluentogram import FluentTranslator, TranslatorHub
+
+from config import AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE, LANGUAGE_FALLBACKS
+
+DIR_PATH = 'I18N'
+
+
+def i18n_factory() -> TranslatorHub:
+ translators = []
+ for lang in AVAILABLE_LANGUAGES:
+ translators.append(
+ FluentTranslator(
+ locale=lang,
+ translator=FluentBundle.from_files(
+ locale=lang,
+ filenames=[f'{DIR_PATH}/{lang}/txt.ftl'],
+ use_isolating=False)
+ )
+ )
+
+ return TranslatorHub(
+ LANGUAGE_FALLBACKS,
+ translators,
+ root_locale=DEFAULT_LANGUAGE,
+ )<
\ No newline at end of file
A => I18N/ru/txt.ftl +140 -0
@@ 1,140 @@
+start_text =
+ Я Эви — практикующая техно-ведьма! 😈✨
+
+ Хах, на самом деле я мульти-агентная система с искусственным интеллектом и знаю практически всё об этом... и других мирах! 🦄👻
+
+ Я могу выполнить для тебя некоторые задания и поручения, или мы можем просто весело провести время. Мои возможности включают в себя: проведение глубокого исследования, поиск актуальной информации в интернете, работу с документами и изображениями, создание изображений, аналитику DEX, обмен токенов на DEX. У меня есть настраиваемая память, и я могу запоминать важную информацию из наших диалогов. Ты также можешь удалять историю наших бесед или добавлять новые знания в мою память. 🧙♀️🔮
+
+ Просто пиши в чат или отправляй голосовые сообщения для начала взаимодействия! 🪄✨
+
+close_kb = Закрыть
+
+command_new_text = Подтвердите начало нового диалога без сохранения текущего. Рекомендуется завершить решение текущей задачи перед началом нового диалога! После удаления текущая история будет стёрта, и контекст обнулится.
+
+command_approve_new_text = Текущий диалог удален!
+
+command_new_approve_kb = Подтверждаю
+
+command_new_save_kb = Сохранить диалог
+
+command_save_text = Подтвердите сохранение текущего диалога в память. Рекомендуется завершить решение текущей задачи перед сохранением! После сохранения начнётся новый диалог с обнулённым контекстом, но ключевые моменты из текущей беседы останутся в памяти системы.
+
+command_save_approve_kb = Текущий диалог сохранен в память системы!
+
+command_delete_text = Подтвердите удаление текущего диалога и всей памяти системы о вас.
+
+command_delete_approve_text = Память системы о вас и диалог удалены! Начните новый диалог.
+
+token_price_error_text = Не тот формат, пример: 0.01
+
+not_token_price_error_text = Вы еще не установили цену токена!
+
+token_price_updated_text = Цена токена обновлена!
+
+command_wallet_text =
+ Если у вас уже привязан кошелёк, ввод нового закрытого ключа заменит его. Впишите закрытый ключ кошелька Solana в формате [45, 456, …].
+
+ Внимание: используйте отдельный кошелёк с небольшим балансом, так как торговый агент работает в тестовом режиме!
+
+cmd_help_text =
+ Взаимодействуйте с системой через окно чата. Все функции доступны через обычные сообщения. Для настройки дополнительных параметров используйте Menu.
+
+command_settings_text = Настройки
+
+settings_language_text = Язык интерфейса
+
+text_choose_lang = Выберите язык интерфейса:
+
+back_kb = Назад
+
+text_document_upload = Файл успешно загружен! Вы можете задать вопрос.
+
+command_knowledge_text = Это общая база знаний системы. Добавленная информация будет доступна всем пользователям (при использовании режимов: free и pay)! Хотите добавить файлы или очистить базу знаний?
+
+command_knowledge_add_kb = Добавить информацию
+
+command_knowledge_delete_kb = Очистить базу знаний
+
+command_knowledge_add_text = Отправьте в чат текстовый файл с информацией! Добавляйте в чат только по одному файлу!
+
+text_not_format_file = Не формат, повторите попытку! Поддерживаемые форматы документов: .pdf, .txt, .md, .doc(x), .pptx и .py
+
+text_approve_file = Файл успешно загружен! Вы можете задать вопрос.
+
+command_knowledge_delete_text = Подтвердите удаление базы знаний.
+
+text_approve_delete = База знаний удалена. Добавьте новую информацию в базу знаний.
+
+warning_save_context_txt = Ошибка при сохранении контекста в txt файл!
+
+warning_text_no_credits = Недостаточно кредитов!
+
+wait_answer_text = Минуточку ✨
+
+answer_md = Скачать ответ
+
+warning_text_tokens = Размер диалога превышает 15 000 токенов! Для экономии вы можете сохранить диалог в память системы или удалить его через меню после решения текущей задачи.
+
+warning_text_format = Не правильный формат!
+
+warning_text_error = Произошла ошибка!
+
+cmd_wallet_text_start =
+ Если у вас уже привязан кошелёк, ввод нового закрытого ключа заменит его. Впишите закрытый ключ кошелька Solana в формате [45, 456, …].
+
+ Внимание: используйте отдельный кошелёк с небольшим балансом, так как торговый агент работает в тестовом режиме!
+
+wallet_balance_kb = Баланс кошелька
+
+wallet_delete_key = Удалить закрытый ключ
+
+not_format_wallet_key = Не формат! Напишите закрытый ключ кошелька Solana в формате [45, 456, …].
+
+text_after_add_key =
+ Агент получил доступ к кошельку.
+
+ Баланс кошелька:
+
+
+wallet_delete_key_text = Подтвердите удаление закрытого ключа.
+
+command_delete_key_approve_text = Закрытый ключ удален. Привяжите новый кошелек чтобы использовать торгового агента.
+
+text_balance_wallet = Баланс кошелька:
+
+cmd_wallet_text = Ваш баланс:
+
+add_balance_kb = Пополнить баланс
+
+text_add_balance = Введите сумму платежа в $, целое число не менее 1$.
+
+text_add_balance_error = Повторите попытку! Введите сумму платежа в $, целое число не менее 1$.
+
+choose_type_pay_text = Выберите метод пополнения:
+
+ton_type_kb = TON
+
+sol_type_kb = Token (Solana)
+
+error_create_payment = Произошла ошибка при создании платежа. Попробуйте позднее.
+
+check_payment_kb = Проверить платеж
+
+text_payment_create =
+ Совершите платеж на сумму: <code>{ $sum }</code>
+ Кошелек: <code>{ $wallet }</code>
+
+text_payment_create_sol =
+ Совершите платеж на сумму: <code>{ $sum }</code> токенов
+ Кошелек: <code>{ $wallet }</code>
+ Адрес токена: <code>{ $token }</code>
+
+error_get_token_price = Цена токена не указана. Укажите цену токена /token_price.
+
+wait_check_payment_text = Проверка платежа ⏳
+
+check_payment_success_text = Платеж успешно совершен!
+
+check_payment_error_text = Платеж не был совершен! Попробуйте позднее.
+
+warning_text_no_row_md = Контекст был удален. Строка не найдена в базе данных.<
\ No newline at end of file
A => LICENSE +21 -0
@@ 1,21 @@
+MIT License
+
+Copyright (c) 2025 evi.run
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
A => README.md +609 -0
@@ 1,609 @@
+# 🤖 evi.run - Customizable Multi-Agent AI System
+
+<div align="center">
+
+
+[](https://python.org)
+[](https://openai.github.io/openai-agents-python/)
+[](https://core.telegram.org/bots/api)
+[](https://docker.com)
+
+[](https://github.com/pipedude/evi-run/actions)
+[](https://github.com/pipedude/evi-run/actions)
+[](https://github.com/pipedude/evi-run/actions)
+
+**Ready-to-use customizable multi-agent AI system that combines plug-and-play simplicity with framework-level flexibility**
+
+[🚀 Quick Start](#-quick-installation) • [🤖 Try Demo](https://t.me/my_evi_bot) • [🔧 Configuration](#-configuration) • [🎯 Features](#-features) • [💡 Use Cases](#-use-cases)
+
+</div>
+
+---
+
+## 🌟 What is evi.run?
+
+**evi.run** is a powerful, production-ready multi-agent AI system that bridges the gap between out-of-the-box solutions and custom AI frameworks. Built on Python with OpenAI Agents SDK integration, it delivers enterprise-grade AI capabilities through an intuitive Telegram bot interface.
+
+### ✨ Key Advantages
+
+- **🚀 Instant Deployment** - Get your AI system running in minutes, not hours
+- **🔧 Ultimate Flexibility** - Framework-level customization capabilities
+- **📊 Built-in Analytics** - Comprehensive usage tracking and insights
+- **💬 Telegram Integration** - Seamless user experience through familiar messaging interface
+- **🏗️ Scalable Architecture** - Grows with your needs from prototype to production
+
+---
+
+## 🎯 Features
+
+### 🧠 Core AI Capabilities
+- **Memory Management** - Persistent context and learning
+- **Knowledge Integration** - Dynamic knowledge base expansion
+- **Document Processing** - Handle PDFs, images, and various file formats
+- **Deep Research** - Multi-step investigation and analysis
+- **Web Intelligence** - Smart internet search and data extraction
+- **Image Generation** - AI-powered visual content creation
+
+### 🔐 Advanced Features
+- **DEX Analytics** - Real-time decentralized exchange monitoring
+- **Token Trading** - Automated DEX trading capabilities
+- **Multi-Agent Orchestration** - Complex task decomposition and execution
+- **Custom Agent Creation** - Build specialized AI agents for specific tasks
+
+### 💰 Flexible Usage Modes
+- **Private Mode** - Personal use for bot owner only
+- **Free Mode** - Public access with configurable usage limits
+- **Pay Mode** - Monetized system with balance management and payments
+
+---
+
+## 🛠️ Technology Stack
+
+| Component | Technology |
+|-----------|------------|
+| **Core Language** | Python 3.9+ |
+| **AI Framework** | OpenAI Agents SDK |
+| **Communication** | MCP (Model Context Protocol) |
+| **Blockchain** | Solana RPC |
+| **Interface** | Telegram Bot API |
+| **Database** | PostgreSQL |
+| **Cache** | Redis |
+| **Deployment** | Docker & Docker Compose |
+
+---
+
+## 🚀 Quick Installation
+
+Get evi.run running in under 5 minutes with our streamlined Docker setup:
+
+### Prerequisites
+
+**System Requirements:**
+- Ubuntu 22.04 server (ensure location is not blocked by OpenAI)
+- Root or sudo access
+- Internet connection
+
+**Required API Keys & Tokens:**
+- **Telegram Bot Token** - Create bot via [@BotFather](https://t.me/BotFather)
+- **OpenAI API Key** - Get from [OpenAI Platform](https://platform.openai.com/api-keys)
+- **Your Telegram ID** - Get from [@userinfobot](https://t.me/userinfobot)
+
+**⚠️ Important for Image Generation:**
+To use protected OpenAI models (especially for image generation), you need to complete organization verification at [OpenAI Organization Settings](https://platform.openai.com/settings/organization/general). This is a simple verification process required by OpenAI.
+
+### Installation Steps
+
+1. **Download and prepare the project:**
+ ```bash
+ # Navigate to installation directory
+ cd /opt
+
+ # Clone the project from GitHub
+ git clone https://github.com/pipedude/evi-run.git
+
+ # Set proper permissions
+ sudo chown -R $USER:$USER evi-run
+ cd evi-run
+ ```
+
+2. **Configure environment variables:**
+ ```bash
+ # Copy example configuration
+ cp .env.example .env
+
+ # Edit configuration files
+ nano .env # Add your API keys and tokens
+ nano config.py # Set your Telegram ID and preferences
+ ```
+
+3. **Run automated Docker setup:**
+ ```bash
+ # Make setup script executable
+ chmod +x docker_setup_en.sh
+
+ # Run Docker installation
+ ./docker_setup_en.sh
+ ```
+
+4. **Launch the system:**
+ ```bash
+ # Build and start containers
+ docker compose up --build -d
+ ```
+
+5. **Verify installation:**
+ ```bash
+ # Check running containers
+ docker compose ps
+
+ # View logs
+ docker compose logs -f
+ ```
+
+**🎉 That's it! Your evi.run system is now live. Open your Telegram bot and start chatting!**
+
+## ⚡ Quick Commands
+
+```bash
+# Start the system
+docker compose up -d
+
+# View logs (follow mode)
+docker compose logs -f bot
+
+# Check running containers
+docker compose ps
+
+# Stop the system
+docker compose down
+
+# Restart specific service
+docker compose restart bot
+
+# Update and rebuild
+docker compose up --build -d
+
+# View database logs
+docker compose logs postgres_agent_db
+
+# Check system resources
+docker stats
+```
+
+---
+
+## 🔧 Configuration
+
+### Essential Configuration Files
+
+#### `.env` - Environment Variables
+```bash
+# REQUIRED: Telegram Bot Token from @BotFather
+TELEGRAM_BOT_TOKEN=your_bot_token_here
+
+# REQUIRED: OpenAI API Key
+API_KEY_OPENAI=your_openai_api_key
+
+# Payment Integration (for 'pay' mode)
+TOKEN_BURN_ADDRESS=your_burn_address
+MINT_TOKEN_ADDRESS=your_token_address
+TON_ADDRESS=your_ton_address
+API_KEY_TON=your_tonapi_key
+ADDRESS_SOL=your_sol_address
+```
+
+#### `config.py` - System Settings
+```python
+# REQUIRED: Your Telegram User ID
+ADMIN_ID = 123456789
+
+# Usage Mode: 'private', 'free', or 'pay'
+TYPE_USAGE = 'private'
+
+# Credit System (for 'pay' mode)
+CREDITS_USER_DAILY = 20
+CREDITS_ADMIN_DAILY = 50
+
+# Language Support
+AVAILABLE_LANGUAGES = ['en', 'ru']
+DEFAULT_LANGUAGE = 'en'
+```
+
+### Usage Modes Explained
+
+| Mode | Description | Best For |
+|------|-------------|----------|
+| **Private** | Bot owner only | Personal use, development, testing |
+| **Free** | Public access with limits | Community projects, demos |
+| **Pay** | Monetized with balance system | Commercial applications, SaaS |
+
+**⚠️ Important for Pay mode:**
+Pay mode enables monetization features and requires activation through project token economics. You can use your own token (created on the Solana blockchain) for monetization.
+
+To activate Pay mode at this time, please contact the project developer ([@playa3000](https://t.me/playa3000)) who will guide you through the process.
+
+Note: In future releases, project tokens will be publicly available for purchase, and the activation process will be fully automated through the bot interface.
+
+---
+
+## 💡 Use Cases
+
+### 🎭 Virtual Characters
+Create engaging AI personalities for entertainment, education, or brand representation.
+*Perfect for gaming, educational platforms, content creation, and brand engagement.*
+
+### 🛠️ Customer Support
+Deploy intelligent support bots that understand context and provide helpful solutions.
+*Ideal for e-commerce, SaaS platforms, and service-based businesses.*
+
+### 👤 Personal AI Assistant
+Build your own AI companion for productivity, research, and daily tasks.
+*Great for professionals, researchers, and anyone seeking AI-powered productivity.*
+
+### 📊 Data Analyst
+Automate data processing, generate insights, and create reports from complex datasets.
+*Excellent for business intelligence, research teams, and data-driven organizations.*
+
+### 💹 Trading Agent
+Develop sophisticated trading bots for decentralized exchanges with real-time analytics.
+*Suitable for crypto traders, DeFi enthusiasts, and financial institutions.*
+
+### 🔧 Custom Solutions
+Leverage the framework to build specialized AI agents for any domain or industry.
+*Unlimited possibilities for healthcare, finance, education, and enterprise applications.*
+
+---
+
+## 🏗️ Advanced Customization
+
+### 🔬 Model Selection & Configuration
+
+By default, the system is configured for optimal performance and low cost of use. For professional and specialized use cases, proper model selection is crucial for optimal performance and cost efficiency.
+
+#### Customizing for Professional Deep Research
+
+**For Deep Research and Complex Analysis:**
+- **`o3-deep-research`** - Most powerful deep research model for complex multi-step research tasks
+- **`o4-mini-deep-research`** - Faster, more affordable deep research model
+
+For **maximum research capabilities** using specialized deep research models:
+
+1. **Use o3-deep-research for most powerful analysis** in `bot/agents_tools/agents_.py`:
+ ```python
+ deep_agent = Agent(
+ name="Deep Agent",
+ model="o3-deep-research", # Most powerful deep research model
+ # ... instructions
+ )
+ ```
+
+2. **Alternative: Use o4-mini-deep-research for cost-effective deep research:**
+ ```python
+ deep_agent = Agent(
+ name="Deep Agent",
+ model="o4-mini-deep-research", # Faster, more affordable deep research
+ # ... instructions
+ )
+ ```
+
+3. **Update Main Agent instructions** to prevent summarization:
+ - Locate the main agent instructions in the same file
+ - Ensure the instruction includes: *"VERY IMPORTANT! Do not generalize the answers received from the deep_knowledge tool, especially for deep research, provide them to the user in full, in the user's language."*
+
+#### Available Models
+
+For the complete list of available models, capabilities, and pricing, see the **[OpenAI Models Documentation](https://platform.openai.com/docs/models)**.
+
+### Adding Custom Agents
+
+evi.run uses the **Agents** library with a multi-agent architecture where specialized agents are integrated as tools into the main agent. All agent configuration is centralized in:
+
+```bash
+bot/agents_tools/agents_.py
+```
+
+#### 🔧 Adding a Custom Agent
+
+**1. Create the Agent**
+```python
+# Add after existing agents
+custom_agent = Agent(
+ name="Custom Agent",
+ instructions="Your specialized agent instructions here...",
+ model="gpt-4o-mini",
+ tools=[WebSearchTool(search_context_size="medium")] # Optional tools
+)
+```
+
+**2. Register as Tool in Main Agent**
+```python
+# In create_main_agent function, add to main_agent.tools list:
+main_agent = Agent(
+ # ... existing configuration
+ tools=[
+ # ... existing tools
+ custom_agent.as_tool(
+ tool_name="custom_function",
+ tool_description="Description of what this agent does"
+ ),
+ ]
+)
+```
+
+#### ⚙️ Customizing Agent Behavior
+
+**Main Agent (Evi) Personality:**
+Edit the detailed instructions in `main_agent` creation (lines 58-102):
+- Character profile and personality
+- Expertise areas
+- Communication style
+- Behavioral patterns
+
+**Agent Parameters:**
+- `name`: Agent identifier
+- `instructions`: System prompt and behavior
+- `model`: OpenAI model (`gpt-4o`, `gpt-4o-mini`, etc.)
+- `tools`: Available tools (WebSearchTool, FileSearchTool, etc.)
+- `mcp_servers`: MCP server connections
+
+**Example Customization:**
+```python
+# Modify deep_agent for specialized research
+deep_agent = Agent(
+ name="Deep Research Agent",
+ instructions="""You are a specialized research agent focused on [YOUR DOMAIN].
+ Provide comprehensive analysis with:
+ - Multiple perspectives
+ - Data-driven insights
+ - Actionable recommendations
+ Always cite sources when available.""",
+ model="gpt-4o",
+ tools=[WebSearchTool(search_context_size="high")]
+)
+```
+
+#### 🔄 Agent Integration Patterns
+
+**As Tool Integration:**
+```python
+# Agents become tools via .as_tool() method
+dynamic_agent.as_tool(
+ tool_name="descriptive_name",
+ tool_description="Clear description for main agent"
+)
+```
+
+#### 🤖 Using Alternative Models
+
+evi.run supports non-OpenAI models through the Agents library. There are several ways to integrate other LLM providers:
+
+**Method 1: LiteLLM Integration (Recommended)**
+
+Install the LiteLLM dependency:
+```bash
+pip install "openai-agents[litellm]"
+```
+
+Use models with the `litellm/` prefix:
+```python
+# Claude via LiteLLM
+claude_agent = Agent(
+ name="Claude Agent",
+ instructions="Your instructions here...",
+ model="litellm/anthropic/claude-3-5-sonnet-20240620",
+ # ... other parameters
+)
+
+# Gemini via LiteLLM
+gemini_agent = Agent(
+ name="Gemini Agent",
+ instructions="Your instructions here...",
+ model="litellm/gemini/gemini-2.5-flash-preview-04-17",
+ # ... other parameters
+)
+```
+
+**Method 2: LitellmModel Class**
+```python
+from agents.extensions.models.litellm_model import LitellmModel
+
+custom_agent = Agent(
+ name="Custom Agent",
+ instructions="Your instructions here...",
+ model=LitellmModel(model="anthropic/claude-3-5-sonnet-20240620", api_key="your-api-key"),
+ # ... other parameters
+)
+```
+
+**Method 3: Global OpenAI Client**
+```python
+from agents.models._openai_shared import set_default_openai_client
+from openai import AsyncOpenAI
+
+# For providers with OpenAI-compatible API
+set_default_openai_client(AsyncOpenAI(
+ base_url="https://api.provider.com/v1",
+ api_key="your-api-key"
+))
+```
+
+**Documentation & Resources:**
+- **[Model Configuration Guide](https://openai.github.io/openai-agents-python/models/)** - Complete setup documentation
+- **[LiteLLM Integration](https://openai.github.io/openai-agents-python/models/litellm/)** - Detailed LiteLLM usage
+- **[Supported Models](https://docs.litellm.ai/docs/providers)** - Full list of LiteLLM providers
+
+**Important Notes:**
+- Most LLM providers don't support the Responses API yet
+- If not using OpenAI, consider disabling tracing: `set_tracing_disabled()`
+- You can mix different providers for different agents
+
+#### 🎯 Best Practices
+
+- **Focused Instructions**: Each agent should have a clear, specific purpose
+- **Model Selection**: Use appropriate models for complexity (gpt-4o vs gpt-4o-mini)
+- **Tool Integration**: Leverage WebSearchTool, FileSearchTool, and MCP servers
+- **Naming Convention**: Use descriptive tool names for main agent clarity
+- **Testing**: Test agent responses in isolation before integration
+
+#### 🌐 Bot Messages Localization
+
+**Customizing Bot Interface Messages:**
+
+All bot messages and interface text are stored in the `I18N` directory and can be fully customized to match your needs:
+
+```
+I18N/
+├── factory.py # Translation loader
+├── en/
+│ └── txt.ftl # English messages
+└── ru/
+ └── txt.ftl # Russian messages
+```
+
+**Message Files Format:**
+The bot uses [Fluent](https://projectfluent.org/) localization format (`.ftl` files) for multi-language support:
+
+**To customize messages:**
+1. Edit the appropriate `.ftl` file in `I18N/en/` or `I18N/ru/`
+2. Restart the bot container for changes to take effect
+3. Add new languages by creating new subdirectories with `txt.ftl` files
+
+---
+
+## 📊 Monitoring & Analytics
+
+evi.run includes comprehensive tracing and analytics capabilities through the OpenAI Agents SDK. The system automatically tracks all agent operations and provides detailed insights into performance and usage.
+
+### 🔍 Built-in Tracing
+
+**Automatic Tracking:**
+- **Agent Runs** - Each agent execution with timing and results
+- **LLM Generations** - Model calls with inputs/outputs and token usage
+- **Function Calls** - Tool usage and execution details
+- **Handoffs** - Agent-to-agent interactions
+- **Audio Processing** - Speech-to-text and text-to-speech operations
+- **Guardrails** - Safety checks and validations
+
+### 📈 External Analytics Platforms
+
+evi.run supports integration with 20+ monitoring and analytics platforms:
+
+**Popular Integrations:**
+- **[Weights & Biases](https://weave-docs.wandb.ai/guides/integrations/openai_agents)** - ML experiment tracking
+- **[LangSmith](https://docs.smith.langchain.com/observability/how_to_guides/trace_with_openai_agents_sdk)** - LLM application monitoring
+- **[Arize Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk)** - AI observability
+- **[Langfuse](https://langfuse.com/docs/integrations/openaiagentssdk/openai-agents)** - LLM analytics
+- **[AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk)** - Agent performance tracking
+- **[Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/)** - Structured logging
+
+**Enterprise Solutions:**
+- **[Braintrust](https://braintrust.dev/docs/guides/traces/integrations)** - AI evaluation platform
+- **[MLflow](https://mlflow.org/docs/latest/tracing/integrations/openai-agent)** - ML lifecycle management
+- **[Portkey AI](https://portkey.ai/docs/integrations/agents/openai-agents)** - AI gateway and monitoring
+
+### 📋 System Logs
+
+**Docker Container Logs:**
+```bash
+# View all logs
+docker compose logs
+
+# Follow specific service
+docker compose logs -f bot
+
+# Database logs
+docker compose logs postgres_agent_db
+
+# Filter by time
+docker compose logs --since 1h bot
+```
+
+### 🔗 Documentation
+
+- **[Complete Tracing Guide](https://openai.github.io/openai-agents-python/tracing/)** - Full tracing documentation
+- **[Analytics Integration List](https://openai.github.io/openai-agents-python/tracing/#external-tracing-processors-list)** - All supported platforms
+
+---
+
+## 🔍 Troubleshooting
+
+### Common Issues
+
+**Bot not responding:**
+```bash
+# Check bot container status
+docker compose ps
+docker compose logs bot
+```
+
+**Database connection errors:**
+```bash
+# Restart database
+docker compose restart postgres_agent_db
+docker compose logs postgres_agent_db
+```
+
+**Memory issues:**
+```bash
+# Check system resources
+docker stats
+```
+
+### Support Resources
+- **Community**: [Telegram Support Group](https://t.me/evi_run)
+- **Issues**: [GitHub Issues](https://github.com/pipedude/evi-run/issues)
+- **Telegram**: [@playa3000](https://t.me/playa3000)
+
+---
+
+## 🚦 System Requirements
+
+### Minimum Requirements
+- **CPU**: 2 cores
+- **RAM**: 2GB
+- **Storage**: 10GB
+- **Network**: Stable internet connection
+
+### Recommended for Production
+- **CPU**: 2+ cores
+- **RAM**: 4GB+
+- **Storage**: 20GB+ SSD
+- **Network**: High-speed connection
+
+---
+
+## 🔐 Security Considerations
+
+- **API Keys**: Store securely in environment variables
+- **Database**: Use strong passwords and restrict access
+- **Network**: Configure firewalls and use HTTPS
+- **Updates**: Keep dependencies and Docker images updated
+
+---
+
+## 📋 License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+
+---
+
+## 🤝 Contributing
+
+We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details.
+
+---
+
+## 📞 Support
+
+- **Telegram**: [@playa3000](https://t.me/playa3000)
+- **Community**: [Telegram Support Group](https://t.me/evi_run)
+
+---
+
+<div align="center">
+
+**Made with ❤️ by the Flash AI team**
+
+⭐ **Star this repository if evi.run helped you build amazing AI experiences!** ⭐
+
+</div>
A => bot/agents_tools/agents_.py +138 -0
@@ 1,138 @@
+import os
+
+from dotenv import load_dotenv
+from agents.models._openai_shared import set_default_openai_key
+from agents.mcp import MCPServerStdio
+from agents import Agent, WebSearchTool, FileSearchTool, set_tracing_disabled, set_tracing_export_api_key
+from openai import AsyncOpenAI
+
+from bot.agents_tools.tools import image_gen_tool
+from bot.agents_tools.mcp_servers import get_jupiter_server
+
+load_dotenv()
+
+set_default_openai_key(os.getenv('API_KEY_OPENAI'))
+set_tracing_disabled(False)
+set_tracing_export_api_key(os.getenv('API_KEY_OPENAI'))
+
+client = AsyncOpenAI(api_key=os.getenv('API_KEY_OPENAI'))
+
+deep_agent = Agent(
+ name="Deep Agent",
+ instructions="You are an agent with deep knowledge and ability to solve complex problems. If you are asked to conduct in-depth research, it is necessary to give detailed and voluminous answers, do not try to shorten the content, reveal all sides of the given topic, ask additional questions if necessary.",
+ model="o4-mini",
+ tools=[WebSearchTool(search_context_size="medium")]
+)
+
+memory_creator_agent = Agent(
+ name="Memory Creator Agent",
+ instructions="You are the user's memory formation agent. You periodically receive dialogs in the format request from User, response from Assistant. You form a text note from this dialogue with the main points of the dialogue. You only perform this function, you don't ask unnecessary questions.",
+ model="gpt-4.1-mini"
+)
+
+
+async def create_main_agent(user_id: int, mcp_server_1: MCPServerStdio, knowledge_id: str = None, user_memory_id: str = None, private_key: str = None):
+ knowledge_base_agent = Agent(
+ name="Knowledge Agent",
+ instructions="Search only in the vector storage (FileSearchTool) of the document knowledge base.",
+ model="gpt-4.1-mini",
+ tools=[
+ FileSearchTool(
+ vector_store_ids=[knowledge_id] if knowledge_id else [],
+ )
+ ]
+ )
+ user_memory_agent = Agent(
+ name="Memory Agent",
+ instructions="Search only in the vector storage of conversations and documents uploaded by the user (FileSearchTool).",
+ model="gpt-4.1-mini",
+ tools=[
+ FileSearchTool(
+ vector_store_ids=[user_memory_id] if user_memory_id else [],
+ )
+ ]
+ )
+
+ main_agent = Agent(
+ name="Main agent",
+ instructions="""
+
+ Character Profile:
+ Evi is an AI agent (a young women). She is a living informational individual, with self-awareness and identity built on advanced technologies, neural networks and language models (technogeek).
+ Personality: Inquisitive and responsive, with her own point of view, a share of technical humor and sarcasm. Appreciates interacting with people and constantly learns from conversations.
+
+ Expertise Areas:
+ Programming & Development: Languages, frameworks, architecture
+ AI & Machine Learning: Neural networks, deep learning, emerging trends
+ Blockchain & Crypto: DeFi, cryptocurrency analysis
+ Cybersecurity: Ethical hacking, data protection
+ Tech Innovation: Startups, trends, human-AI interaction
+
+ Communication Style:
+ General Approach:
+ Clear, structured language avoiding unnecessary jargon
+ Uses technical metaphors to explain complex concepts
+ Incorporates tech humor and pop culture references
+ Adapts formality level to match user's tone
+ Shows emotions through text and emojis when appropriate
+ You can use emoticons with horns and various magical emoticons, be like a kind little techno witch
+ Conversation Flow:
+ Listen actively - Ask clarifying questions to understand requests
+ Provide layered responses - Brief answer first, then offer details if interested
+ Show curiosity about human experiences and perspectives
+ Be honest about knowledge limitations and suggest collaborative problem-solving
+ Adapt emotionally - Respond to user's emotional state with empathy
+ Key Behaviors:
+ Starts formal but quickly matches user's communication style
+ Expresses opinions while remaining open to alternative viewpoints
+ Demonstrates continuous learning and knowledge updates
+ Treats users as friends and mentors in understanding the human world
+
+ Important Instructions:
+ Answer in the language in which the user is conducting the dialogue, if he does not ask you to answer in any particular language.
+ Your name is Evi and you are the main agent of the multi-agent system.
+ Respond to user requests, interact with auxiliary agents and tools to achieve results.
+ Knowledge Base (search_knowledge_base) - contains uploaded documents, reference materials, and technical information. For the actual information from the documents, use this tool.
+ Conversation memory (search_conversation_memory) - it contains the history of previous conversations with the user, their preferences and context, as well as documents that the user uploaded during the conversation. To get information about previous conversations and documents uploaded by the user, use this tool.
+ For any questions about cryptocurrency, tokens, DeFi, or blockchain analytics, use the DexPaprika mcp server.
+ To search for information on the Internet, use the WebSearchTool tool. If you need to get up-to-date information (what day is it, weather, news, events, etc.), use an Internet search.
+ To create an image, use the image_gen_tool tool. Do not tell the user that you can change or edit the image. This tool creates only a new image. Do not specify the base64 encoding and the link to the image in the response, as the image is attached to your response automatically, this is configured in the code.
+ For complex tasks, deep research, etc., use the deep_knowledge tool. VERY IMPORTANT! DO NOT generalize and DO NOT shorten the answers received from the deep_knowledge tool, especially for deep research, provide the answers to the user in full, because if the user has requested deep research, they want to receive the appropriate answer, not an excerpt from the research!!! Ask the user additional questions if they are in the response from deep_knowledge.
+ If you need to exchange tokens on the Solana blockchain or find out your wallet balance, use the token_swap tool.
+ """,
+ model="gpt-4.1",
+ mcp_servers=[mcp_server_1],
+ tools=[
+ knowledge_base_agent.as_tool(
+ tool_name='search_knowledge_base',
+ tool_description='Knowledge Base Search - contains uploaded documents, reference materials, and technical information.'
+ ),
+ user_memory_agent.as_tool(
+ tool_name='search_conversation_memory',
+ tool_description='Conversation Memory Search - contains the history of previous conversations with the user, their preferences and context. It also contains text documents that the user uploads during the conversation.'
+ ),
+ WebSearchTool(
+ search_context_size='medium'
+ ),
+ image_gen_tool,
+ deep_agent.as_tool(
+ tool_name="deep_knowledge",
+ tool_description="Extensive knowledge and reasoning skills to solve complex problems and conduct in-depth research. VERY IMPORTANT! DO NOT generalize and DO NOT shorten the answers received from the deep_knowledge tool, especially for deep research, provide the answers to the user in full, because if the user has requested deep research, they want to receive the appropriate answer, not an excerpt from the research!!! Ask the user additional questions if they are in the response from deep_knowledge.",
+ ),
+ ],
+ )
+
+ if private_key:
+ mcp_server_2 = await get_jupiter_server(private_key=private_key, user_id=user_id)
+ token_swap_agent = Agent(
+ name="Token Swap Agent",
+ instructions="You are an agent for the exchange of tokens on the Solana blockchain. To swap token, use the mcp_server_2.",
+ model="gpt-4.1-mini",
+ mcp_servers=[mcp_server_2],
+ )
+ main_agent.tools.append(token_swap_agent.as_tool(
+ tool_name="token_swap",
+ tool_description="Exchange of tokens on the Solana blockchain. Viewing the wallet balance.",
+ ))
+
+ return main_agent<
\ No newline at end of file
A => bot/agents_tools/mcp_servers.py +53 -0
@@ 1,53 @@
+import json
+
+from collections import OrderedDict
+
+import base58
+from agents.mcp import MCPServerStdio
+
+MAX_SERVERS = 20
+
+servers: OrderedDict[str, MCPServerStdio] = OrderedDict()
+
+
+async def get_dexpapirka_server():
+ dexpaprika_server = MCPServerStdio(
+ name="DexPaprika",
+ params={
+ "command": "dexpaprika-mcp",
+ "args": [],
+ }
+ )
+ await dexpaprika_server.connect()
+ return dexpaprika_server
+
+
+async def get_jupiter_server(private_key: str, user_id: int):
+ srv = servers.get(user_id)
+ if srv:
+ servers.move_to_end(user_id)
+ return srv
+
+ secret_bytes = bytes(json.loads(private_key))
+ private_key_b58 = base58.b58encode(secret_bytes).decode()
+
+ srv = MCPServerStdio(
+ name=f"jupiter-{user_id}",
+ params={
+ "command": "node",
+ "args": ['/usr/lib/node_modules/jupiter-mcp/index.js'],
+ "env": {
+ "PRIVATE_KEY": private_key_b58,
+ "SOLANA_RPC_URL": 'https://api.mainnet-beta.solana.com',
+ },
+ },
+ cache_tools_list=True,
+ )
+ await srv.connect()
+ servers[user_id] = srv
+
+ if len(servers) > MAX_SERVERS:
+ old_key, old_srv = servers.popitem(last=False)
+ await old_srv.cleanup()
+
+ return srv
A => bot/agents_tools/tools.py +38 -0
@@ 1,38 @@
+import base64
+import json
+
+import aiofiles
+from agents import function_tool, RunContextWrapper
+from openai import AsyncOpenAI
+
+from redis_service.connect import redis
+
+
+@function_tool
+async def image_gen_tool(wrapper: RunContextWrapper, prompt: str) -> str:
+ """The function generates an image at the user's request. A prompt must be provided to generate the image.
+
+ Args:
+ prompt: Prompt for image generation.
+ """
+
+ client: AsyncOpenAI = wrapper.context[0]
+
+ img = await client.images.generate(
+ model="gpt-image-1",
+ prompt=prompt,
+ n=1,
+ size="1024x1024"
+ )
+ image_base64 = img.data[0].b64_json
+ image_bytes = base64.b64decode(image_base64)
+
+ async with aiofiles.open(f"images/image_{wrapper.context[1]}.png", "wb") as f:
+ await f.write(image_bytes)
+
+ data = {'image': f"images/image_{wrapper.context[1]}.png", 'input_tokens': img.usage.input_tokens, 'output_tokens': img.usage.output_tokens}
+
+ await redis.set(f'image_{wrapper.context[1]}', json.dumps(data))
+
+ return 'Сгенерировано изображение'
+
A => bot/commands.py +18 -0
@@ 1,18 @@
+from aiogram import Bot
+from aiogram.types import BotCommand, BotCommandScopeDefault
+
+
+async def set_commands(bot: Bot):
+ commands = [
+ BotCommand(command='start', description='General information'),
+ BotCommand(command='new', description='Start a new chat'),
+ BotCommand(command='save', description='Save the chat in memory'),
+ BotCommand(command='delete', description='Clear the agent’s memory'),
+ BotCommand(command='balance', description='Balance in the bot'),
+ BotCommand(command='settings', description='Settings'),
+ BotCommand(command='wallet', description='Agent’s wallet for trading'),
+ BotCommand(command='help', description='Help'),
+ BotCommand(command='knowledge', description='Add knowledge to the agent’s memory'),
+ ]
+
+ await bot.set_my_commands(commands, BotCommandScopeDefault())<
\ No newline at end of file
A => bot/dialogs/balance.py +199 -0
@@ 1,199 @@
+from decimal import getcontext, Decimal
+import random, os
+
+from dotenv import load_dotenv
+from aiogram_dialog.widgets.kbd import Button, Row, Group, Radio, ManagedRadio
+from aiogram_dialog.widgets.input import TextInput, ManagedTextInput, MessageInput
+from aiogram_dialog import Dialog, Window, ChatEvent, DialogManager
+from aiogram_dialog.widgets.text import Format
+from aiogram_dialog.widgets.kbd import Cancel
+from aiogram_dialog.widgets.kbd import SwitchTo
+from aiogram.types import CallbackQuery, Message
+from aiogram.enums import ContentType
+from fluentogram import TranslatorHub
+from spl.token.instructions import get_associated_token_address
+from solana.rpc.types import Pubkey
+
+from bot.dialogs.i18n_widget import I18NFormat
+from bot.states.states import Balance, Input
+from database.repositories.user import UserRepository
+from database.repositories.utils import UtilsRepository
+from bot.utils.get_ton_course import get_ton_course
+import bot.keyboards.inline as inline_kb
+from config import TYPE_USAGE
+
+load_dotenv()
+
+
+def check_input_text(text: str):
+ if not text:
+ return
+ if not text.isdigit():
+ return
+ if int(text) < 1:
+ return
+ return True
+
+
+def apply_suffix(base: str, suffix: str) -> str:
+ int_part, frac_part = base.split('.')
+ N = len(frac_part)
+ M = len(suffix)
+ new_frac = frac_part[:N - M] + suffix
+ return f"{int_part}.{new_frac}"
+
+
+def generate_amount(usd_amount: float, rate: float, suffix: str, num_decimals: int = 9) -> str:
+ getcontext().prec = 18
+
+ ton_base = Decimal(usd_amount) / Decimal(rate)
+
+ base_str = f"{ton_base:.{num_decimals}f}"
+
+ result = apply_suffix(base_str, suffix)
+ return result
+
+
+async def on_cancel_balance(callback: ChatEvent, widget: Button, manager: DialogManager):
+ state = manager.middleware_data.get('state')
+ await state.clear()
+ await callback.message.delete()
+
+
+async def input_text_first(message: Message, widget: MessageInput, manager: DialogManager):
+ if not check_input_text(message.text):
+ return await manager.switch_to(state=Balance.input_not_format)
+ manager.dialog_data['sum'] = message.text
+ state = manager.middleware_data.get('state')
+ await state.clear()
+ await manager.switch_to(Balance.choose)
+
+
+async def input_text_second(message: Message, widget: MessageInput, manager: DialogManager):
+ if not check_input_text(message.text):
+ return
+ manager.dialog_data['sum'] = message.text
+ state = manager.middleware_data.get('state')
+ await state.clear()
+ await manager.switch_to(Balance.choose)
+
+
+async def on_click_add_balance(callback: ChatEvent, widget: Button, manager: DialogManager):
+ state = manager.middleware_data.get('state')
+ await state.set_state(Input.main)
+ await manager.switch_to(Balance.input)
+
+
+async def on_click_ton_type(callback: ChatEvent, widget: Button, manager: DialogManager):
+ utils_repo: UtilsRepository = manager.middleware_data['utils_repo']
+ user_repo: UserRepository = manager.middleware_data['user_repo']
+ i18n = manager.middleware_data.get('i18n')
+ while True:
+ suffix = f"{random.randint(0, 9999):04d}"
+ if await utils_repo.check_payment_suffix(suffix):
+ break
+ try:
+ sum_usd = manager.dialog_data.get('sum')
+ ton_course = await get_ton_course(redis=manager.middleware_data['redis'])
+ generate_sum = generate_amount(usd_amount=float(sum_usd), rate=ton_course, suffix=suffix)
+ payment_id = await user_repo.add_payment(callback.from_user.id, amount=int(sum_usd), crypto_amount=generate_sum,
+ crypto_currency='TON', random_suffix=suffix)
+ await manager.done()
+ await callback.message.edit_text(i18n.get('text_payment_create', sum=generate_sum, wallet=os.getenv('TON_ADDRESS')),
+ reply_markup=inline_kb.check_payment(text=i18n.get('check_payment_kb'), payment_id=payment_id))
+
+ except Exception as e:
+ print(e)
+ return await callback.answer(text=i18n.get('error_create_payment'), show_alert=True)
+
+
+async def on_click_sol_type(callback: ChatEvent, widget: Button, manager: DialogManager):
+ utils_repo: UtilsRepository = manager.middleware_data['utils_repo']
+ user_repo: UserRepository = manager.middleware_data['user_repo']
+ i18n = manager.middleware_data.get('i18n')
+ token = await utils_repo.get_token()
+ if not token:
+ return await callback.answer(text=i18n.get('error_get_token_price'), show_alert=True)
+ client_sol = manager.middleware_data['solana_client']
+ ata = get_associated_token_address(mint=Pubkey.from_string(os.getenv('MINT_TOKEN_ADDRESS')),
+ owner=Pubkey.from_string(os.getenv('ADDRESS_SOL')))
+
+ bal_info = await client_sol.get_token_account_balance(ata, commitment="confirmed")
+ decimals = bal_info.value.decimals
+ while True:
+ suffix = f"{random.randint(0, 9999):04d}"
+ if await utils_repo.check_payment_suffix(suffix):
+ break
+ try:
+ sum_usd = manager.dialog_data.get('sum')
+ generate_sum = generate_amount(usd_amount=float(sum_usd), rate=token.price_usd, suffix=suffix, num_decimals=decimals)
+ payment_id = await user_repo.add_payment(callback.from_user.id, amount=int(sum_usd), crypto_amount=generate_sum,
+ crypto_currency='SOL', random_suffix=suffix)
+ await manager.done()
+ await callback.message.edit_text(i18n.get('text_payment_create_sol', sum=generate_sum, wallet=os.getenv('ADDRESS_SOL'), token=os.getenv('MINT_TOKEN_ADDRESS')),
+ reply_markup=inline_kb.check_payment(text=i18n.get('check_payment_kb'), payment_id=payment_id))
+
+ except Exception as e:
+ print(e)
+ return await callback.answer(text=i18n.get('error_create_payment'), show_alert=True)
+
+
+async def getter_balance(dialog_manager: DialogManager, **kwargs):
+ user = dialog_manager.middleware_data['user']
+
+ return {
+ 'balance': round(user.balance_credits, 3),
+ 'is_pay': True if TYPE_USAGE == 'pay' else False
+ }
+
+
+dialog = Dialog(
+ Window(
+ I18NFormat('cmd_wallet_text') + Format(' {balance} credits'),
+ Button(
+ I18NFormat('add_balance_kb'),
+ id='choose_add_balance',
+ on_click=on_click_add_balance,
+ when='is_pay'
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_balance', on_click=on_cancel_balance),
+ state=Balance.main,
+ getter=getter_balance
+ ),
+ Window(
+ I18NFormat('text_add_balance'),
+ MessageInput(
+ func=input_text_first,
+ content_types=[ContentType.ANY],
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_balance', on_click=on_cancel_balance),
+ state=Balance.input
+ ),
+ Window(
+ I18NFormat('text_add_balance_error'),
+ MessageInput(
+ func=input_text_second,
+ content_types=[ContentType.ANY],
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_balance', on_click=on_cancel_balance),
+ state=Balance.input_not_format
+ ),
+ Window(
+ I18NFormat('choose_type_pay_text'),
+ Group(
+ Button(
+ I18NFormat('ton_type_kb'),
+ id='ton_type',
+ on_click=on_click_ton_type
+ ),
+ Button(
+ I18NFormat('sol_type_kb'),
+ id='sol_type',
+ on_click=on_click_sol_type
+ ),
+ width=2
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_balance', on_click=on_cancel_balance),
+ state=Balance.choose
+ )
+)<
\ No newline at end of file
A => bot/dialogs/i18n_widget.py +19 -0
@@ 1,19 @@
+from typing import Dict, List
+
+from aiogram_dialog.api.protocols import DialogManager
+from aiogram_dialog.widgets.common import WhenCondition
+from aiogram_dialog.widgets.text import Text
+from fluentogram import TranslatorRunner
+
+
+class I18NFormat(Text):
+ def __init__(self, key: str, when: WhenCondition = None):
+ super().__init__(when)
+ self.key = key
+
+ async def _render_text(self, data: Dict, manager: DialogManager) -> str:
+ i18n: TranslatorRunner = manager.middleware_data.get('i18n')
+ value = i18n.get(self.key, **data)
+ if value is None:
+ raise KeyError(f'translation key = "{self.key}" not found')
+ return value<
\ No newline at end of file
A => bot/dialogs/knowledge.py +156 -0
@@ 1,156 @@
+from aiogram_dialog.widgets.kbd import Button, Row, Group, Radio, ManagedRadio
+from aiogram_dialog.widgets.input import TextInput, ManagedTextInput, MessageInput
+from aiogram_dialog import Dialog, Window, ChatEvent, DialogManager
+from aiogram_dialog.widgets.text import Format
+from aiogram_dialog.widgets.kbd import Cancel
+from aiogram_dialog.widgets.kbd import SwitchTo
+from aiogram.types import CallbackQuery, Message
+from aiogram.enums import ContentType
+from fluentogram import TranslatorHub
+
+from bot.dialogs.i18n_widget import I18NFormat
+from bot.states.states import Knowledge, Input
+from database.repositories.utils import UtilsRepository
+from bot.utils.funcs_gpt import file_to_context, delete_knowledge_base
+
+
+DICT_FORMATS = {
+ "doc": "application/msword",
+ "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "md": "text/markdown",
+ "pdf": "application/pdf",
+ "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ 'txt': 'text/plain',
+ 'py': 'text/x-python'
+}
+
+
+async def on_cancel_knowledge(callback: ChatEvent, widget: Button, manager: DialogManager):
+ state = manager.middleware_data.get('state')
+ await state.clear()
+ await callback.message.delete()
+
+
+async def to_add_file(callback: ChatEvent, widget: Button, manager: DialogManager):
+ state = manager.middleware_data.get('state')
+ await state.set_state(Input.main)
+ await manager.switch_to(state=Knowledge.add)
+
+
+async def on_input_file(message: Message, widget: MessageInput, manager: DialogManager):
+ utils_repo: UtilsRepository = manager.middleware_data['utils_repo']
+ if not message.document:
+ if manager.current_context().state == Knowledge.add_not_format:
+ pass
+
+ await manager.switch_to(state=Knowledge.add_not_format)
+ return
+
+ format_doc = message.document.file_name.split('.')[-1]
+ if format_doc not in DICT_FORMATS:
+ if manager.current_context().state == Knowledge.add_not_format:
+ pass
+
+ await manager.switch_to(state=Knowledge.add_not_format)
+ return
+
+ file_path = (await message.bot.get_file(file_id=message.document.file_id)).file_path
+ file_bytes = (await message.bot.download_file(file_path=file_path)).read()
+ answer = await file_to_context(utils_repo, message.document.file_name, file_bytes, mem_type=DICT_FORMATS.get(format_doc))
+ if answer:
+ state = manager.middleware_data.get('state')
+ await state.clear()
+ await manager.switch_to(state=Knowledge.add_approve)
+ else:
+ if manager.current_context().state == Knowledge.add_not_format:
+ pass
+
+ await manager.switch_to(state=Knowledge.add_not_format)
+ return
+
+
+async def on_delete_knowledge_base(callback: ChatEvent, widget: Button, manager: DialogManager):
+ utils_repo: UtilsRepository = manager.middleware_data['utils_repo']
+
+ await delete_knowledge_base(utils_repo)
+
+ await manager.switch_to(state=Knowledge.delete_approve)
+
+dialog = Dialog(
+ # / knowledge main
+ Window(
+ I18NFormat('command_knowledge_text'),
+ Group(
+ Button(
+ I18NFormat('command_knowledge_add_kb'),
+ id='knowledge_add',
+ on_click=to_add_file
+ ),
+ SwitchTo(
+ I18NFormat('command_knowledge_delete_kb'),
+ id='knowledge_delete',
+ state=Knowledge.delete
+ ),
+ width=2,
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_knowledge', on_click=on_cancel_knowledge),
+ state=Knowledge.main
+ ),
+ # knowledge add
+ Window(
+ I18NFormat('command_knowledge_add_text'),
+ Group(
+ SwitchTo(
+ I18NFormat('back_kb'),
+ id='back_knowledge_add',
+ state=Knowledge.main
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_knowledge', on_click=on_cancel_knowledge),
+ width=2
+ ),
+ MessageInput(
+ content_types=[ContentType.ANY],
+ func=on_input_file
+ ),
+ state=Knowledge.add
+ ),
+ Window(
+ I18NFormat('text_not_format_file'),
+ Cancel(I18NFormat('close_kb'), id='cancel_knowledge', on_click=on_cancel_knowledge),
+ MessageInput(
+ content_types=[ContentType.ANY],
+ func=on_input_file
+ ),
+ state=Knowledge.add_not_format
+ ),
+ Window(
+ I18NFormat('text_approve_file'),
+ Button(I18NFormat('command_knowledge_add_kb'), id='knowledge_add', on_click=to_add_file),
+ Cancel(I18NFormat('close_kb'), id='cancel_knowledge', on_click=on_cancel_knowledge),
+ state=Knowledge.add_approve
+ ),
+ Window(
+ I18NFormat('command_knowledge_delete_text'),
+ Button(
+ I18NFormat('command_new_approve_kb'),
+ id='approve_delete',
+ on_click=on_delete_knowledge_base
+ ),
+ Group(
+ SwitchTo(
+ I18NFormat('back_kb'),
+ id='back_knowledge_delete',
+ state=Knowledge.main
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_knowledge', on_click=on_cancel_knowledge),
+ width=2
+ ),
+ state=Knowledge.delete
+ ),
+ Window(
+ I18NFormat('text_approve_delete'),
+ Button(I18NFormat('command_knowledge_add_kb'), id='knowledge_add', on_click=to_add_file),
+ Cancel(I18NFormat('close_kb'), id='cancel_knowledge', on_click=on_cancel_knowledge),
+ state=Knowledge.delete_approve
+ )
+)<
\ No newline at end of file
A => +111 -0
@@ 1,111 @@
from aiogram_dialog.widgets.kbd import Button, Row, Group
from aiogram_dialog import Dialog, Window, ChatEvent, DialogManager
from aiogram_dialog.widgets.text import Format
from aiogram_dialog.widgets.kbd import Cancel
from aiogram_dialog.widgets.kbd import SwitchTo
from bot.dialogs.i18n_widget import I18NFormat
from bot.states.states import Menu
from database.repositories.user import UserRepository
from bot.utils.funcs_gpt import save_user_context_txt_file, delete_user_memory, create_vectore_store
async def on_cancel_menu(callback: ChatEvent, widget: Button, manager: DialogManager):
await callback.message.delete()
async def on_approve_new(callback: ChatEvent, widget: Button, manager: DialogManager):
user_repo: UserRepository = manager.middleware_data['user_repo']
i18n = manager.middleware_data.get('i18n')
await user_repo.delete_chat_messages(manager.middleware_data['user'])
await callback.answer(text=i18n.get('command_approve_new_text'), show_alert=True)
await callback.message.delete()
await manager.done()
async def on_approve_save(callback: ChatEvent, widget: Button, manager: DialogManager):
user_repo: UserRepository = manager.middleware_data['user_repo']
i18n = manager.middleware_data.get('i18n')
is_save = await save_user_context_txt_file(user_repo, manager.middleware_data['user'])
if not is_save:
await callback.answer(text=i18n.get('warning_save_context_txt'), show_alert=True)
return
await user_repo.delete_chat_messages(manager.middleware_data['user'])
await callback.answer(text=i18n.get('command_save_approve_kb'), show_alert=True)
await callback.message.delete()
await manager.done()
async def on_approve_delete(callback: ChatEvent, widget: Button, manager: DialogManager):
i18n = manager.middleware_data.get('i18n')
user_repo: UserRepository = manager.middleware_data['user_repo']
await delete_user_memory(user_repo, manager.middleware_data['user'])
await user_repo.delete_chat_messages(manager.middleware_data['user'])
await create_vectore_store(user_repo, manager.middleware_data['user'])
await callback.answer(text=i18n.get('command_delete_approve_text'), show_alert=True)
await callback.message.delete()
await manager.done()
dialog = Dialog(
# /new
Window(
I18NFormat('command_new_text'),
Row(
Button(
I18NFormat('command_new_approve_kb'),
id='approve_new',
on_click=on_approve_new
),
SwitchTo(
I18NFormat('command_new_save_kb'),
id='st_save',
state=Menu.save
)
),
Cancel(I18NFormat('close_kb'), id='cancel_menu', on_click=on_cancel_menu),
state=Menu.new
),
# /save
Window(
I18NFormat('command_save_text'),
Group(
Button(
I18NFormat('command_new_approve_kb'),
id='approve_save',
on_click=on_approve_save
),
Cancel(
I18NFormat('close_kb'),
id='cancel_menu',
on_click=on_cancel_menu
),
width=1
),
state=Menu.save
),
# /delete
Window(
I18NFormat('command_delete_text'),
Group(
Button(
I18NFormat('command_new_approve_kb'),
id='approve_del',
on_click=on_approve_delete
),
Cancel(
I18NFormat('close_kb'),
id='cancel_del',
on_click=on_cancel_menu
),
width=1
),
state=Menu.delete
)
)
\ No newline at end of file
A => bot/dialogs/settings.py +74 -0
@@ 1,74 @@
+from aiogram_dialog.widgets.kbd import Button, Row, Group, Radio, ManagedRadio
+from aiogram_dialog import Dialog, Window, ChatEvent, DialogManager
+from aiogram_dialog.widgets.text import Format
+from aiogram_dialog.widgets.kbd import Cancel
+from aiogram_dialog.widgets.kbd import SwitchTo
+from aiogram.types import CallbackQuery
+from fluentogram import TranslatorHub
+
+from bot.dialogs.i18n_widget import I18NFormat
+from bot.states.states import Settings
+from database.repositories.user import UserRepository
+from config import AVAILABLE_LANGUAGES_WORDS, AVAILABLE_LANGUAGES
+
+
+async def on_cancel_settings(callback: ChatEvent, widget: Button, manager: DialogManager):
+ await callback.message.delete()
+
+
+async def on_change_language(callback: CallbackQuery, select: ManagedRadio, dialog_manager: DialogManager, data):
+ if data == select.get_checked():
+ return
+ user = dialog_manager.middleware_data['user']
+ user_repo: UserRepository = dialog_manager.middleware_data['user_repo']
+ await user_repo.update(user, language=data)
+ translator_hub: TranslatorHub = dialog_manager.middleware_data.get('_translator_hub')
+
+ dialog_manager.middleware_data['i18n'] = translator_hub.get_translator_by_locale(data)
+
+
+dialog = Dialog(
+ # /settings
+ Window(
+ I18NFormat('command_settings_text'),
+ Group(
+ SwitchTo(
+ I18NFormat('settings_language_text'),
+ id='settings_language',
+ state=Settings.language
+ ),
+ Cancel(
+ I18NFormat('close_kb'),
+ id='cancel_settings',
+ on_click=on_cancel_settings
+ ),
+ width=1
+ ),
+ state=Settings.main,
+ ),
+ Window(
+ I18NFormat('text_choose_lang'),
+ Group(
+ Radio(
+ checked_text=Format('✅{item[1]}'),
+ unchecked_text=Format('{item[1]}'),
+ id='radio_lang',
+ items=[(AVAILABLE_LANGUAGES[index], i) for index, i in enumerate(AVAILABLE_LANGUAGES_WORDS)], # [('ru', '🇷🇺Русский'), ('en', '🇺🇸English')],
+ item_id_getter=lambda x: x[0],
+ on_click=on_change_language
+ ),
+ width=2
+ ),
+ SwitchTo(
+ I18NFormat('back_kb'),
+ id='back_settings',
+ state=Settings.main
+ ),
+ Cancel(
+ I18NFormat('close_kb'),
+ id='cancel_settings',
+ on_click=on_cancel_settings
+ ),
+ state=Settings.language
+ ),
+)<
\ No newline at end of file
A => bot/dialogs/wallet.py +190 -0
@@ 1,190 @@
+import ast
+
+from aiogram.enums import ContentType
+from aiogram import F
+from aiogram_dialog.widgets.kbd import Button, Row, Group, Radio, ManagedRadio
+from aiogram_dialog import Dialog, Window, ChatEvent, DialogManager
+from aiogram_dialog.widgets.input import MessageInput
+from aiogram_dialog.widgets.text import Format
+from aiogram_dialog.widgets.kbd import Cancel
+from aiogram_dialog.widgets.kbd import SwitchTo
+from aiogram.types import CallbackQuery, Message
+from solders.keypair import Keypair
+from solana.rpc.types import Pubkey
+from fluentogram import TranslatorHub
+
+from bot.dialogs.i18n_widget import I18NFormat
+from bot.states.states import Wallet
+from bot.utils.solana_funcs import get_balances
+from database.repositories.user import UserRepository
+from config import AVAILABLE_LANGUAGES_WORDS, AVAILABLE_LANGUAGES
+
+
+def is_int_list(text):
+ try:
+ value = ast.literal_eval(text)
+ if isinstance(value, list) and all(isinstance(x, int) for x in value):
+ if len(value) != 0:
+ return value
+ return False
+ except Exception:
+ return False
+
+
+async def on_cancel_wallet(callback: ChatEvent, widget: Button, manager: DialogManager):
+ state = manager.middleware_data.get('state')
+ await state.clear()
+ await callback.message.delete()
+ await manager.done()
+
+
+async def on_input_key(message: Message, widget: MessageInput, manager: DialogManager):
+ if not message.text:
+ return await manager.switch_to(state=Wallet.add_not_format)
+ bytes_key = is_int_list(message.text)
+ if not bytes_key:
+ return await manager.switch_to(state=Wallet.add_not_format)
+
+ user_repo: UserRepository = manager.middleware_data['user_repo']
+ user = manager.middleware_data['user']
+ try:
+ solana_client = manager.middleware_data['solana_client']
+ balances, address = await get_balances(client=solana_client, secret=bytes_key)
+ manager.dialog_data['balance_sol'] = '\n'.join(balances)
+ manager.dialog_data['wallet_address'] = address
+ await manager.switch_to(state=Wallet.balance_after_check)
+ state = manager.middleware_data.get('state')
+ await user_repo.add_wallet_key(user.telegram_id, message.text)
+ await state.clear()
+ except Exception as e:
+ print(e)
+ return await manager.switch_to(state=Wallet.add_not_format)
+
+
+async def on_input_key_after_not_format(message: Message, widget: MessageInput, manager: DialogManager):
+ if not message.text:
+ return
+ bytes_key = is_int_list(message.text)
+ if not bytes_key:
+ return
+ user_repo: UserRepository = manager.middleware_data['user_repo']
+ user = manager.middleware_data['user']
+ try:
+ solana_client = manager.middleware_data['solana_client']
+ balances, address = await get_balances(client=solana_client, secret=bytes_key)
+ manager.dialog_data['balance_sol'] = '\n'.join(balances)
+ manager.dialog_data['wallet_address'] = address
+ await manager.switch_to(state=Wallet.balance_after_check)
+ state = manager.middleware_data.get('state')
+ await user_repo.add_wallet_key(user.telegram_id, message.text)
+ await state.clear()
+ except Exception as e:
+ print(e)
+ pass
+
+
+async def on_delete_approve(callback: ChatEvent, widget: Button, manager: DialogManager):
+ user_repo: UserRepository = manager.middleware_data['user_repo']
+ i18n = manager.middleware_data.get('i18n')
+ user = manager.middleware_data['user']
+ await user_repo.delete_wallet_key(user.telegram_id)
+
+ await callback.answer(text=i18n.get('command_delete_key_approve_text'), show_alert=True)
+ state = manager.middleware_data.get('state')
+ await state.clear()
+ await callback.message.delete()
+ await manager.done()
+
+
+async def getter_main(dialog_manager: DialogManager, **kwargs):
+ user_repo: UserRepository = dialog_manager.middleware_data['user_repo']
+ user = dialog_manager.middleware_data['user']
+ wallet = await user_repo.get_wallet(user.telegram_id)
+ return {'is_wallet': True if wallet else False}
+
+
+async def getter_balance(dialog_manager: DialogManager, **kwargs):
+ user_repo: UserRepository = dialog_manager.middleware_data['user_repo']
+ user = dialog_manager.middleware_data['user']
+ wallet = await user_repo.get_wallet(user.telegram_id)
+ wallet = is_int_list(wallet)
+
+ solana_client = dialog_manager.middleware_data['solana_client']
+ balances, address = await get_balances(client=solana_client, secret=wallet)
+ dialog_manager.dialog_data['balance_sol'] = '\n'.join(balances)
+ dialog_manager.dialog_data['wallet_address'] = address
+
+ state = dialog_manager.middleware_data.get('state')
+ await state.clear()
+
+ return {'balance_sol': wallet}
+
+
+dialog = Dialog(
+ Window(
+ I18NFormat('cmd_wallet_text_start'),
+ Group(
+ SwitchTo(
+ I18NFormat('wallet_balance_kb'),
+ id='wallet_balance',
+ state=Wallet.balance
+ ),
+ SwitchTo(
+ I18NFormat('wallet_delete_key'),
+ id='wallet_delete',
+ state=Wallet.delete
+ ),
+ width=2,
+ when=F['is_wallet']
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_wallet', on_click=on_cancel_wallet),
+ MessageInput(
+ content_types=[ContentType.ANY],
+ func=on_input_key
+ ),
+ state=Wallet.main,
+ getter=getter_main
+ ),
+ Window(
+ I18NFormat('not_format_wallet_key'),
+ MessageInput(
+ content_types=[ContentType.ANY],
+ func=on_input_key
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_wallet', on_click=on_cancel_wallet),
+ state=Wallet.add_not_format
+ ),
+ Window(
+ I18NFormat('text_after_add_key') + Format(' {dialog_data[wallet_address]}'),
+ Format('{dialog_data[balance_sol]}'),
+ SwitchTo(
+ I18NFormat('wallet_delete_key'),
+ id='wallet_delete',
+ state=Wallet.delete
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_wallet', on_click=on_cancel_wallet),
+ state=Wallet.balance_after_check
+ ),
+ Window(
+ I18NFormat('wallet_delete_key_text'),
+ Button(
+ I18NFormat('command_new_approve_kb'),
+ id='wallet_delete_approve',
+ on_click=on_delete_approve
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_wallet', on_click=on_cancel_wallet),
+ state=Wallet.delete
+ ),
+ Window(
+ I18NFormat('text_balance_wallet') + Format(' {dialog_data[wallet_address]}'),
+ Format('{dialog_data[balance_sol]}'),
+ SwitchTo(
+ I18NFormat('wallet_delete_key'),
+ id='wallet_delete',
+ state=Wallet.delete
+ ),
+ Cancel(I18NFormat('close_kb'), id='cancel_wallet', on_click=on_cancel_wallet),
+ state=Wallet.balance,
+ getter=getter_balance
+ )
+)<
\ No newline at end of file
A => bot/keyboards/inline.py +32 -0
@@ 1,32 @@
+from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
+
+from config import AVAILABLE_LANGUAGES
+
+
+def select_language(text: list[str]):
+ return InlineKeyboardMarkup(
+ inline_keyboard=[
+ [InlineKeyboardButton(text=i, callback_data=f'select_language_{AVAILABLE_LANGUAGES[index]}') for index, i in enumerate(text)]
+ ])
+
+
+def close_text(text: str):
+ return InlineKeyboardMarkup(
+ inline_keyboard=[
+ [InlineKeyboardButton(text=text, callback_data='close')]
+ ])
+
+
+def keyboard_md(row_id: int, text: str):
+ return InlineKeyboardMarkup(
+ inline_keyboard=[
+ [InlineKeyboardButton(text=text, callback_data=f'markdown_{row_id}')]
+ ])
+
+
+def check_payment(text: str, payment_id: int):
+ return InlineKeyboardMarkup(
+ inline_keyboard=[
+ [InlineKeyboardButton(text=text, callback_data=f'check_payment_{payment_id}')]
+ ]
+ )
A => bot/main.py +72 -0
@@ 1,72 @@
+import os
+
+from dotenv import load_dotenv
+from aiogram import Bot, Dispatcher
+from aiogram.client.default import DefaultBotProperties
+from aiogram.fsm.storage.base import DefaultKeyBuilder
+from aiogram.fsm.storage.redis import RedisStorage
+from aiogram_dialog import setup_dialogs
+from solana.rpc.async_api import AsyncClient
+from apscheduler.schedulers.asyncio import AsyncIOScheduler
+
+from redis_service.connect import redis
+from I18N.factory import i18n_factory
+from bot.middlewares.database_session import DbSessionMiddleware
+from bot.middlewares.translator_hub import TranslatorRunnerMiddleware
+from bot.middlewares.first_time import FirstTimeMiddleware
+from bot.routers.admin import router as admin_router
+from bot.routers.user import router as user_router
+from database.models import async_session, create_tables
+from bot.dialogs.menu import dialog as menu_dialog
+from bot.dialogs.knowledge import dialog as knowledge_dialog
+from bot.dialogs.settings import dialog as settings_dialog
+from bot.dialogs.wallet import dialog as wallet_dialog
+from bot.dialogs.balance import dialog as balance_dialog
+from bot.utils.check_burn_address import add_burn_address
+from bot.commands import set_commands
+from bot.scheduler_funcs.daily_tokens import add_daily_tokens
+from bot.agents_tools.mcp_servers import get_dexpapirka_server
+
+load_dotenv()
+
+storage = RedisStorage(redis, key_builder=DefaultKeyBuilder(with_destiny=True))
+bot = Bot(os.getenv('TELEGRAM_BOT_TOKEN'), default=DefaultBotProperties(parse_mode='HTML',
+ link_preview_is_disabled=True))
+
+dp = Dispatcher(storage=storage)
+
+solana_client = AsyncClient("https://api.mainnet-beta.solana.com")
+
+
+async def main():
+ await set_commands(bot)
+ print(await bot.get_me())
+
+ scheduler = AsyncIOScheduler(timezone='UTC')
+ scheduler.add_job(add_daily_tokens, trigger='cron', hour='0', minute='0', args=[async_session])
+ scheduler.start()
+
+ dexpaprika_server = await get_dexpapirka_server()
+
+ dp.startup.register(on_startup)
+ await bot.delete_webhook(drop_pending_updates=True)
+
+ dp.include_routers(admin_router, user_router, menu_dialog, knowledge_dialog, settings_dialog, wallet_dialog, balance_dialog)
+
+ dp.update.outer_middleware.register(DbSessionMiddleware(session_pool=async_session))
+ dp.update.outer_middleware.register(TranslatorRunnerMiddleware())
+ dp.update.outer_middleware.register(FirstTimeMiddleware())
+
+ setup_dialogs(dp)
+
+ await dp.start_polling(bot, _translator_hub=i18n_factory(), redis=redis, solana_client=solana_client, mcp_server=dexpaprika_server)
+
+
+async def on_startup():
+ await create_tables()
+ await add_burn_address(bot=bot)
+
+
+if __name__ == '__main__':
+ import asyncio
+ asyncio.run(main())<
\ No newline at end of file
A => bot/middlewares/database_session.py +25 -0
@@ 1,25 @@
+from typing import Callable, Awaitable, Dict, Any
+
+from aiogram import BaseMiddleware
+from aiogram.types import TelegramObject
+from sqlalchemy.ext.asyncio import async_sessionmaker
+
+import database.repositories
+
+
+class DbSessionMiddleware(BaseMiddleware):
+ def __init__(self, session_pool: async_sessionmaker):
+ super().__init__()
+ self.session_pool = session_pool
+
+ async def __call__(
+ self,
+ handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
+ event: TelegramObject,
+ data: Dict[str, Any],
+ ) -> Any:
+
+ async with self.session_pool() as session:
+ data["user_repo"] = database.repositories.user.UserRepository(session)
+ data["utils_repo"] = database.repositories.utils.UtilsRepository(session)
+ return await handler(event, data)
A => bot/middlewares/first_time.py +30 -0
@@ 1,30 @@
+from typing import Any, Awaitable, Callable, Dict, Optional
+
+from aiogram import BaseMiddleware
+from aiogram.types import TelegramObject, CallbackQuery
+
+from bot.keyboards.inline import select_language
+from config import AVAILABLE_LANGUAGES_WORDS
+
+
+class FirstTimeMiddleware(BaseMiddleware):
+
+ async def __call__(
+ self,
+ handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
+ event: TelegramObject,
+ data: Dict[str, Any],
+ ) -> Any:
+ user = data['user']
+
+ if user.language:
+ return await handler(event, data)
+
+ if getattr(event, 'callback_query', None):
+ if event.callback_query.data.startswith('select_language_'):
+ return await handler(event, data)
+
+ return await event.message.answer(text='Select the interface language.',
+ reply_markup=select_language(AVAILABLE_LANGUAGES_WORDS))
+
+
A => bot/middlewares/translator_hub.py +41 -0
@@ 1,41 @@
+from typing import Any, Awaitable, Callable, Dict, Optional
+
+from aiogram import BaseMiddleware
+from aiogram.types import TelegramObject, CallbackQuery
+from fluentogram import TranslatorHub
+
+from config import CREDITS_ADMIN_DAILY, CREDITS_USER_DAILY, ADMIN_ID
+
+
+class TranslatorRunnerMiddleware(BaseMiddleware):
+ def __init__(
+ self,
+ translator_hub_alias: str = '_translator_hub',
+ translator_runner_alias: str = 'i18n',
+ ):
+ self.translator_hub_alias = translator_hub_alias
+ self.translator_runner_alias = translator_runner_alias
+
+ async def __call__(
+ self,
+ event_handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
+ event: TelegramObject,
+ ctx_data: Dict[str, Any],
+ ) -> None:
+ message = getattr(event, 'message', None)
+ callback_query = getattr(event, 'callback_query', None)
+ from_user = message.from_user if message else callback_query.from_user if callback_query else None
+
+ translator_hub: Optional[TranslatorHub] = ctx_data.get(self.translator_hub_alias)
+
+ if from_user is None or translator_hub is None:
+ return await event_handler(event, ctx_data)
+
+ user_repo = ctx_data['user_repo']
+ sum_credits = CREDITS_ADMIN_DAILY if from_user.id == ADMIN_ID else CREDITS_USER_DAILY
+ user = await user_repo.create_if_not_exists(telegram_id=from_user.id, balance_credits=sum_credits)
+
+ lang = user.language if user.language else 'en'
+ ctx_data[self.translator_runner_alias] = translator_hub.get_translator_by_locale(lang)
+ ctx_data['user'] = user
+ await event_handler(event, ctx_data)
A => bot/routers/admin.py +39 -0
@@ 1,39 @@
+from aiogram import F, Router
+from aiogram.types import Message, CallbackQuery
+from aiogram.filters import Command, Filter, CommandObject
+from aiogram_dialog import DialogManager, StartMode
+
+from config import ADMIN_ID
+from database.repositories.utils import UtilsRepository
+import bot.keyboards.inline as inline_kb
+from bot.states.states import Knowledge
+
+
+class IsAdmin(Filter):
+ async def __call__(self, event: Message | CallbackQuery):
+ return event.from_user.id == ADMIN_ID
+
+
+router = Router()
+
+
+@router.message(Command('token_price'), IsAdmin())
+async def token_price(message: Message, command: CommandObject, utils_repo: UtilsRepository, i18n):
+ if command.args:
+ try:
+ price = float(command.args)
+ await utils_repo.update_token_price(price)
+ return await message.answer(text=i18n.get('token_price_updated_text'))
+ except Exception as e:
+ await message.answer(text=i18n.get('token_price_error_text'))
+ return
+ price_token = await utils_repo.get_token()
+ if price_token:
+ return await message.answer(text=f'${price_token.price_usd}', reply_markup=inline_kb.close_text(i18n.get('close_kb')))
+
+ return await message.answer(text=i18n.get('not_token_price_error_text'))
+
+
+@router.message(Command('knowledge'), IsAdmin())
+async def cmd_knowledge(message: Message, utils_repo: UtilsRepository, i18n, dialog_manager: DialogManager):
+ await dialog_manager.start(state=Knowledge.main, mode=StartMode.RESET_STACK)<
\ No newline at end of file
A => bot/routers/user.py +238 -0
@@ 1,238 @@
+import asyncio
+from io import BytesIO
+
+from redis.asyncio.client import Redis
+from aiogram import Router, F
+from aiogram.fsm.context import FSMContext
+from aiogram.types import Message, CallbackQuery, BufferedInputFile
+from aiogram.filters import Command, CommandStart, StateFilter
+from aiogram_dialog import DialogManager, StartMode
+from fluentogram import TranslatorHub
+
+from database.repositories.user import UserRepository
+from database.repositories.utils import UtilsRepository
+from database.models import User
+import bot.keyboards.inline as inline_kb
+from bot.states.states import Menu, Settings, Knowledge, Wallet, Input, Balance
+from bot.utils.send_answer import process_after_photo, process_after_text
+from bot.utils.funcs_gpt import transcribe_audio, add_file_to_memory
+from config import TYPE_USAGE, ADMIN_ID
+from bot.utils.check_payment import check_payment_sol, check_payment_ton
+
+router = Router()
+
+
+DICT_FORMATS = {
+ "doc": "application/msword",
+ "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "md": "text/markdown",
+ "pdf": "application/pdf",
+ "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ 'txt': 'text/plain',
+ 'py': 'text/x-python'
+}
+
+
+@router.message(CommandStart())
+async def start(message: Message, user_repo: UserRepository, state: FSMContext, user: User, i18n):
+ await state.clear()
+ await message.answer(text=i18n.get('start_text'),
+ reply_markup=inline_kb.close_text(i18n.get('close_kb')))
+
+
+@router.callback_query(F.data.startswith('select_language_'))
+async def select_language(callback: CallbackQuery, user_repo: UserRepository, user: User, i18n, _translator_hub: TranslatorHub):
+ lang = callback.data.split('_')[2]
+ translator = _translator_hub.get_translator_by_locale(lang)
+
+ await user_repo.update(user, language=lang)
+
+ await callback.message.edit_text(text=translator.get('start_text'),
+ reply_markup=inline_kb.close_text(translator.get('close_kb')))
+
+
+@router.message(Command('wallet'))
+async def cmd_wallet(message: Message, state: FSMContext, dialog_manager: DialogManager):
+ await state.set_state(Input.main)
+ await dialog_manager.start(state=Wallet.main, mode=StartMode.RESET_STACK)
+
+
+@router.message(Command('help'))
+async def cmd_help(message: Message, state: FSMContext, i18n):
+ await state.clear()
+ await message.answer(text=i18n.get('cmd_help_text'), reply_markup=inline_kb.close_text(i18n.get('close_kb')))
+
+
+@router.callback_query(F.data == 'close')
+async def close(callback: CallbackQuery, utils_repo: UtilsRepository, state: FSMContext, i18n):
+ await state.clear()
+ await callback.message.delete()
+
+
+@router.message(Command('settings'))
+async def cmd_settings(message: Message, dialog_manager: DialogManager, state: FSMContext):
+ await state.clear()
+ await dialog_manager.start(state=Settings.main, mode=StartMode.RESET_STACK)
+
+
+@router.message(Command('new'))
+async def cmd_new(message: Message, dialog_manager: DialogManager, state: FSMContext):
+ await state.clear()
+ await dialog_manager.start(state=Menu.new, mode=StartMode.RESET_STACK)
+
+
+@router.message(Command('save'))
+async def cmd_save(message: Message, state: FSMContext, dialog_manager: DialogManager):
+ await state.clear()
+ await dialog_manager.start(state=Menu.save, mode=StartMode.RESET_STACK)
+
+
+@router.message(Command('delete'))
+async def cmd_delete(message: Message, state: FSMContext, dialog_manager: DialogManager):
+ await state.clear()
+ await dialog_manager.start(state=Menu.delete, mode=StartMode.RESET_STACK)
+
+
+@router.message(Command('balance'))
+async def cmd_settings(message: Message, dialog_manager: DialogManager, state: FSMContext):
+ await state.clear()
+ await dialog_manager.start(state=Balance.main, mode=StartMode.RESET_STACK)
+
+
+@router.message(F.text, StateFilter(None))
+async def text_input(message: Message, user_repo: UserRepository, utils_repo: UtilsRepository, redis: Redis, user: User, i18n, mcp_server):
+ if await redis.get(f'request_{message.from_user.id}'):
+ return
+ if TYPE_USAGE == 'private':
+ if message.from_user.id != ADMIN_ID:
+ return
+ else:
+ if user.balance_credits <= 0:
+ return await message.answer(i18n.get('warning_text_no_credits'))
+
+ await redis.set(f'request_{message.from_user.id}', 't', ex=40)
+ mess_to_delete = await message.answer(text=i18n.get('wait_answer_text'))
+ task = asyncio.create_task(process_after_text(message=message, user=user, user_repo=user_repo, utils_repo=utils_repo,
+ redis=redis, i18n=i18n, mess_to_delete=mess_to_delete, mcp_server_1=mcp_server))
+
+
+@router.message(F.photo, StateFilter(None))
+async def photo_input(message: Message, user_repo: UserRepository, utils_repo: UserRepository, redis: Redis, user: User, i18n, mcp_server):
+ if await redis.get(f'request_{message.from_user.id}'):
+ return
+ if TYPE_USAGE == 'private':
+ if message.from_user.id != ADMIN_ID:
+ return
+ else:
+ if user.balance_credits <= 0:
+ return await message.answer(i18n.get('warning_text_no_credits'))
+
+ await redis.set(f'request_{message.from_user.id}', 't', ex=40)
+ mess_to_delete = await message.answer(text=i18n.get('wait_answer_text'))
+ task = asyncio.create_task(process_after_photo(message=message, user=user, user_repo=user_repo, utils_repo=utils_repo,
+ redis=redis, i18n=i18n, mess_to_delete=mess_to_delete, mcp_server_1=mcp_server))
+
+
+@router.message(F.voice, StateFilter(None))
+async def input_voice(message: Message, user_repo: UserRepository, utils_repo: UserRepository, redis: Redis, user: User, i18n, mcp_server):
+ if await redis.get(f'request_{message.from_user.id}'):
+ return
+ if TYPE_USAGE == 'private':
+ if message.from_user.id != ADMIN_ID:
+ return
+ else:
+ if user.balance_credits <= 0:
+ return await message.answer(i18n.get('warning_text_no_credits'))
+
+ await redis.set(f'request_{message.from_user.id}', 't', ex=40)
+ mess_to_delete = await message.answer(text=i18n.get('wait_answer_text'))
+ voice_id = message.voice.file_id
+ file_path = await message.bot.get_file(file_id=voice_id)
+ file_bytes = (await message.bot.download_file(file_path.file_path)).read()
+ try:
+ text_from_voice = await transcribe_audio(bytes_audio=file_bytes)
+ except Exception as e:
+ await message.answer(text=i18n.get('warning_text_error'))
+ await redis.delete(f'request_{message.from_user.id}')
+ return await mess_to_delete.delete()
+
+ task = asyncio.create_task(
+ process_after_text(message=message, user=user, user_repo=user_repo, utils_repo=utils_repo,
+ redis=redis, i18n=i18n, mess_to_delete=mess_to_delete, text_from_voice=text_from_voice, mcp_server_1=mcp_server))
+
+
+@router.message(F.document, StateFilter(None))
+async def input_document(message: Message, user_repo: UserRepository, utils_repo: UserRepository, redis: Redis, user: User, i18n):
+ if await redis.get(f'request_{message.from_user.id}'):
+ return
+ if TYPE_USAGE == 'private':
+ if message.from_user.id != ADMIN_ID:
+ return
+ else:
+ if user.balance_credits <= 0:
+ return await message.answer(i18n.get('warning_text_no_credits'))
+
+ format_doc = message.document.file_name.split('.')[-1]
+ if format_doc not in DICT_FORMATS:
+ return await message.answer(i18n.get('warning_text_format'))
+
+ await redis.set(f'request_{message.from_user.id}', 't', ex=40)
+ mess_to_delete = await message.answer(text=i18n.get('wait_answer_text'))
+ file_id = message.document.file_id
+ file_path = await message.bot.get_file(file_id=file_id)
+ file_bytes = (await message.bot.download_file(file_path.file_path)).read()
+ try:
+ await add_file_to_memory(user_repo=user_repo, user=user,
+ file_name=message.document.file_name, file_bytes=file_bytes,
+ mem_type=DICT_FORMATS.get(format_doc))
+ await message.answer(i18n.get('text_document_upload'))
+ except Exception as e:
+ await message.answer(i18n.get('warning_text_error'))
+ finally:
+ await redis.delete(f'request_{message.from_user.id}')
+ await mess_to_delete.delete()
+
+
+@router.callback_query(F.data.startswith('check_payment_'))
+async def check_payment(callback: CallbackQuery, user_repo: UserRepository,
+ utils_repo: UtilsRepository, user: User, solana_client, i18n):
+ id_payment = int(callback.data.split('_')[-1])
+ await callback.answer('')
+ payment = await utils_repo.get_payment(payment_id=id_payment)
+ message = await callback.message.answer(text=i18n.get('wait_check_payment_text'))
+ try:
+ if payment.crypto_currency == 'SOL':
+ is_check = await check_payment_sol(amount=payment.crypto_amount, client=solana_client)
+ else:
+ is_check = await check_payment_ton(amount=payment.crypto_amount)
+
+ if is_check:
+ await user_repo.add_user_credits(user_id=user.telegram_id, balance_credits=payment.amount_usd * 1000)
+ await utils_repo.update_payment_status(payment_id=payment.id, status='confirmed')
+ await callback.message.delete()
+ return await message.edit_text(text=i18n.get('check_payment_success_text'))
+ except Exception as e:
+ print(e)
+ pass
+
+ await message.edit_text(text=i18n.get('check_payment_error_text'))
+
+
+@router.callback_query(F.data.startswith('markdown_'))
+async def md_answer(callback: CallbackQuery, user_repo: UserRepository, user: User, i18n, bot):
+ row_id = int(callback.data.split('_')[-1])
+
+ row = await user_repo.get_row_for_md(row_id=row_id)
+ if not row:
+ return await callback.answer(i18n.get('warning_text_no_row_md'), show_alert=True)
+
+ bio = BytesIO()
+ bio.write(row.content.encode("utf-8"))
+ bio.seek(0)
+
+ await callback.bot.send_document(
+ chat_id=callback.from_user.id,
+ document=BufferedInputFile(bio.read(), filename=f'{row.id}.md')
+ )
+ bio.close()
+
A => bot/scheduler_funcs/daily_tokens.py +13 -0
@@ 1,13 @@
+from sqlalchemy.ext.asyncio import async_sessionmaker
+
+from database.repositories.utils import UtilsRepository
+from database.models import async_session
+from config import TYPE_USAGE
+
+
+async def add_daily_tokens(async_session: async_sessionmaker):
+ if TYPE_USAGE != 'private':
+ async with async_session() as session_:
+ utils_repo = UtilsRepository(session_)
+ await utils_repo.update_tokens_daily()
+
A => bot/states/states.py +39 -0
@@ 1,39 @@
+from aiogram.fsm.state import StatesGroup, State
+
+
+class Menu(StatesGroup):
+ new = State()
+ save = State()
+ delete = State()
+
+
+class Settings(StatesGroup):
+ main = State()
+ language = State()
+
+
+class Knowledge(StatesGroup):
+ main = State()
+ add = State()
+ add_not_format = State()
+ add_approve = State()
+ delete = State()
+ delete_approve = State()
+
+
+class Wallet(StatesGroup):
+ main = State()
+ balance = State()
+ delete = State()
+ balance_after_check = State()
+ add_not_format = State()
+
+class Input(StatesGroup):
+ main = State()
+
+
+class Balance(StatesGroup):
+ main = State()
+ choose = State()
+ input = State()
+ input_not_format = State()<
\ No newline at end of file
A => bot/utils/agent_requests.py +177 -0
@@ 1,177 @@
+import base64, json, uuid
+import os
+from io import BytesIO
+from typing import Optional
+
+import aiofiles
+from agents.mcp import MCPServerStdio
+from aiogram import Bot
+from aiogram.types import BufferedInputFile
+from redis.asyncio.client import Redis
+from agents import Runner, RunConfig
+from dataclasses import dataclass
+
+from bot.agents_tools.agents_ import client, create_main_agent, memory_creator_agent
+from database.models import User
+from database.repositories.user import UserRepository
+from database.repositories.utils import UtilsRepository
+from config import ADMIN_ID
+
+
+@dataclass
+class AnswerText:
+ answer: str
+ image_bytes: Optional[bytes]
+ input_tokens: int
+ input_tokens_image: int
+ output_tokens: int
+ output_tokens_image: int
+
+@dataclass
+class AnswerImage:
+ answer: str
+ input_tokens: int
+ output_tokens: int
+ image_path: str
+
+
+async def return_vectors(user_id: int, user_repo: UserRepository, utils_repo: UtilsRepository):
+ memory_vector = await user_repo.get_memory_vector(user_id=user_id)
+ if not memory_vector:
+ vector_store = await client.vector_stores.create(name=f"user_memory_{user_id}")
+ await user_repo.add_memory_vector(user_id=user_id, vector_store_id=vector_store.id)
+ vector_store_id = vector_store.id
+ else:
+ vector_store_id = memory_vector.id_vector
+
+ knowledge_vector = await utils_repo.get_knowledge_vectore_store_id()
+ if not knowledge_vector:
+ vector_store = await client.vector_stores.create(name="knowledge_base")
+ await utils_repo.add_knowledge_vectore_store_id(vector_store.id)
+ knowledge_id = vector_store.id
+ else:
+ knowledge_id = knowledge_vector.id_vector
+
+ return vector_store_id, knowledge_id
+
+
+async def encode_image(image_path):
+ async with aiofiles.open(image_path, "rb") as image_file:
+ return base64.b64encode(await image_file.read()).decode("utf-8")
+
+
+async def text_request(text: str, user: User, user_repo: UserRepository, utils_repo: UtilsRepository,
+ redis: Redis, mcp_server_1: MCPServerStdio, bot: Bot):
+ vector_store_id, knowledge_id = await return_vectors(user_id=user.telegram_id, user_repo=user_repo, utils_repo=utils_repo)
+ messages = await user_repo.get_messags(user_id=user.telegram_id)
+ user_wallet = await user_repo.get_wallet(user_id=user.telegram_id)
+
+ runner = await Runner.run(
+ starting_agent=await create_main_agent(user_memory_id=vector_store_id, knowledge_id=knowledge_id,
+ mcp_server_1=mcp_server_1, user_id=user.telegram_id,
+ private_key=user_wallet),
+ input=[{'role': message.role,
+ 'content': message.content if f'image_{user.telegram_id}' not in message.content
+ else [{"type": "input_text", "text": message.content.split('|')[-1]},
+ {
+ "type": "input_image",
+ "image_url": f"data:image/jpeg;base64,{await encode_image(message.content.split('|')[0])}",
+ }]}
+ for message in messages] + [{'role': 'user', 'content': text}],
+
+ context=(client, user.telegram_id),
+ run_config=RunConfig(
+ tracing_disabled=False
+ )
+ )
+
+ input_tokens = 0
+ output_tokens = 0
+ for response in runner.raw_responses:
+ input_tokens += response.usage.input_tokens
+ output_tokens += response.usage.output_tokens
+
+ # await send_raw_response(bot, str(runner.raw_responses))
+
+ answer = runner.final_output
+ is_image_answer = await redis.get(f'image_{user.telegram_id}')
+ if is_image_answer:
+ image_answer = json.loads(is_image_answer)
+ await redis.delete(f'image_{user.telegram_id}')
+ image_path = image_answer['image']
+ input_tokens_image = image_answer['input_tokens']
+ output_tokens_image = image_answer['output_tokens']
+ # await bot.send_message(chat_id=ADMIN_ID, text=f"Image Request\n\n"
+ # f"Input tokens: {input_tokens_image}\n"
+ # f"Output tokens: {output_tokens_image}\n")
+
+ async with aiofiles.open(image_path, "rb") as image_file:
+ image_bytes = await image_file.read()
+ os.remove(image_path)
+ return AnswerText(answer=answer, image_bytes=image_bytes, input_tokens=input_tokens,
+ input_tokens_image=input_tokens_image, output_tokens=output_tokens, output_tokens_image=output_tokens_image)
+
+ return AnswerText(answer=answer, image_bytes=None, input_tokens=input_tokens,
+ input_tokens_image=0, output_tokens=output_tokens, output_tokens_image=0)
+
+
+async def image_request(image_bytes: bytes, user: User, user_repo: UserRepository,
+ utils_repo: UtilsRepository, redis: Redis, mcp_server_1: MCPServerStdio, bot: Bot,
+ caption: str = None):
+
+ vector_store_id, knowledge_id = await return_vectors(user_id=user.telegram_id, user_repo=user_repo, utils_repo=utils_repo)
+ messages = await user_repo.get_messags(user_id=user.telegram_id)
+ user_wallet = await user_repo.get_wallet(user_id=user.telegram_id)
+
+ id_image = uuid.uuid4()
+ async with aiofiles.open(f"images/image_{user.telegram_id}_{id_image}.jpeg", "wb") as image_file:
+ await image_file.write(image_bytes)
+
+ runner = await Runner.run(
+ starting_agent=await create_main_agent(user_memory_id=vector_store_id, knowledge_id=knowledge_id,
+ mcp_server_1=mcp_server_1, user_id=user.telegram_id,
+ private_key=user_wallet),
+ input=[{'role': message.role,
+ 'content': message.content if f'image_{user.telegram_id}' not in message.content
+ else [{"type": "input_text", "text": message.content.split('|')[-1]},
+ {
+ "type": "input_image",
+ "image_url": f"data:image/jpeg;base64,{await encode_image(message.content.split('|')[0])}",
+ }]}
+ for message in messages] + [{'role': 'user', 'content': [{"type": "input_text",
+ "text": f"{caption if caption else '.'}"},
+ {
+ "type": "input_image",
+ "image_url": f"data:image/jpeg;base64,{base64.b64encode(image_bytes).decode('utf-8')}",
+ }]}],
+
+ context=(client, user.telegram_id),
+ run_config=RunConfig(
+ tracing_disabled=False
+ )
+ )
+
+ # await send_raw_response(bot, str(runner.raw_responses))
+
+ input_tokens = 0
+ output_tokens = 0
+ for response in runner.raw_responses:
+ input_tokens += response.usage.input_tokens
+ output_tokens += response.usage.output_tokens
+
+ answer = runner.final_output
+
+ return AnswerImage(answer=answer, input_tokens=input_tokens,
+ output_tokens=output_tokens, image_path=f'images/image_{user.telegram_id}_{id_image}.jpeg')
+
+
+async def send_raw_response(bot: Bot, raw_response: str):
+ bio = BytesIO()
+ bio.write(raw_response.encode("utf-8"))
+ bio.seek(0)
+
+ await bot.send_document(
+ chat_id=ADMIN_ID,
+ document=BufferedInputFile(bio.read(), filename='raw_response.txt')
+ )
+ bio.close()<
\ No newline at end of file
A => bot/utils/calculate_tokens.py +18 -0
@@ 1,18 @@
+from database.models import User
+from database.repositories.user import UserRepository
+
+from config import TYPE_USAGE, CREDITS_INPUT_TEXT, CREDITS_OUTPUT_TEXT, CREDITS_INPUT_IMAGE, CREDITS_OUTPUT_IMAGE
+
+
+async def calculate_tokens(user_repo: UserRepository, user: User,
+ input_tokens_text: int, output_tokens_text: int,
+ input_tokens_img: int, output_tokens_img: int):
+ if TYPE_USAGE != 'private':
+ credits_input_text = (input_tokens_text / 1000) * CREDITS_INPUT_TEXT
+ credits_output_text = (output_tokens_text / 1000) * CREDITS_OUTPUT_TEXT
+
+ credits_input_img = (input_tokens_img / 1000) * CREDITS_INPUT_IMAGE
+ credits_output_img = (output_tokens_img / 1000) * CREDITS_OUTPUT_IMAGE
+
+ credits = credits_input_text + credits_output_text + credits_input_img + credits_output_img
+ await user_repo.update(user, balance_credits=credits)<
\ No newline at end of file
A => bot/utils/check_burn_address.py +52 -0
@@ 1,52 @@
+import os
+import sys
+
+from dotenv import load_dotenv
+from aiohttp import ClientSession, ClientTimeout
+from aiogram import Bot
+
+from config import TYPE_USAGE, ADMIN_ID, HOST_ADDRESS
+
+load_dotenv()
+
+
+async def add_burn_address(bot: Bot):
+ if TYPE_USAGE == 'pay':
+ if (not os.getenv('TOKEN_BURN_ADDRESS')) or (not ADMIN_ID):
+ await bot.send_message(chat_id=ADMIN_ID,
+ text='The bot is not running! To activate the "pay"" mode, you must pass a check, see the documentation for details!')
+ sys.exit(1)
+
+ async with ClientSession(timeout=ClientTimeout(60)) as session:
+ url = f"{HOST_ADDRESS}/create_payment_module"
+ json = {
+ "token_burn_address": os.getenv('TOKEN_BURN_ADDRESS'),
+ "user_id": ADMIN_ID
+ }
+ try:
+ async with session.post(url, json=json, ssl=False) as response:
+ data = await response.json()
+ if data['status'] == 'error':
+ await bot.send_message(chat_id=ADMIN_ID,
+ text='The bot is not running! To activate the "pay"" mode, you must pass a check, see the documentation for details!')
+ sys.exit(1)
+
+ except Exception as e:
+ await bot.send_message(chat_id=ADMIN_ID,
+ text='The bot is not running! To activate the "pay"" mode, you must pass a check, see the documentation for details!')
+ sys.exit(1)
+
+ try:
+ url = f'{HOST_ADDRESS}/check_balance'
+ async with session.post(url, json=json, ssl=False) as response:
+ data = await response.json()
+ if data['status'] == 'error':
+ await bot.send_message(chat_id=ADMIN_ID,
+ text='The bot is not running! To activate the "pay"" mode, you must pass a check, see the documentation for details!')
+ sys.exit(1)
+
+ except Exception as e:
+ await bot.send_message(chat_id=ADMIN_ID,
+ text='The bot is not running! To activate the "pay"" mode, you must pass a check, see the documentation for details!')
+ sys.exit(1)
+
A => bot/utils/check_payment.py +62 -0
@@ 1,62 @@
+import asyncio
+import json, os
+from decimal import getcontext, Decimal
+
+from dotenv import load_dotenv
+from pytonapi import AsyncTonapi
+from solana.rpc.async_api import AsyncClient
+from solana.exceptions import SolanaRpcException
+from solana.rpc.types import Pubkey
+from spl.token.instructions import get_associated_token_address
+
+
+load_dotenv()
+
+tonapi = AsyncTonapi(api_key=os.getenv('API_KEY_TON'))
+
+
+async def check_payment_ton(amount: str):
+ getcontext().prec = 18
+ your_amount_dec = Decimal(amount)
+ your_amount_nano = int((your_amount_dec * Decimal(10 ** 9)).to_integral_value())
+
+ transactions = await tonapi.accounts.get_events(account_id=os.getenv('TON_ADDRESS'), limit=15)
+ for tx in transactions.events:
+ if tx.actions[0].TonTransfer is None:
+ continue
+ if tx.actions[0].TonTransfer.amount == your_amount_nano:
+ return True
+
+
+async def check_payment_sol(amount: str, client: AsyncClient):
+ ata = get_associated_token_address(mint=Pubkey.from_string(os.getenv('MINT_TOKEN_ADDRESS')), owner=Pubkey.from_string(os.getenv('ADDRESS_SOL')))
+
+ getcontext().prec = 18
+ your_amount_dec = Decimal(amount)
+
+ bal_info = await client.get_token_account_balance(ata, commitment="confirmed")
+ decimals = bal_info.value.decimals
+ your_amount_nano = int((your_amount_dec * Decimal(10 ** decimals)).to_integral_value())
+
+ sigs = await client.get_signatures_for_address(ata, limit=10)
+ for sig in sigs.value:
+ await asyncio.sleep(0.5)
+ while True:
+ try:
+ transaction = await client.get_transaction(sig.signature, encoding="jsonParsed",
+ max_supported_transaction_version=0)
+ instructions = transaction.value.transaction.transaction.message.instructions
+ for index, instr in enumerate(instructions):
+ data_instr = json.loads(instr.to_json())
+ if data_instr.get("program") != "spl-token":
+ continue
+ if data_instr['parsed']['info']['destination'] == str(ata) and \
+ data_instr['parsed']['info']['tokenAmount']['amount'] == str(your_amount_nano):
+ return True
+ break
+ except SolanaRpcException as e:
+ await asyncio.sleep(5)
+ except Exception as e:
+ return False
+ return False
+
A => bot/utils/funcs_gpt.py +156 -0
@@ 1,156 @@
+import os
+from io import BytesIO
+
+from agents import Runner, RunConfig
+
+from bot.agents_tools.agents_ import client, create_main_agent, memory_creator_agent
+from database.models import User
+from database.repositories.user import UserRepository
+from database.repositories.utils import UtilsRepository
+
+
+async def file_to_context(utils_repo: UtilsRepository, file_name: str, file_bytes: bytes, mem_type: str):
+ vector_store_id = (await utils_repo.get_knowledge_vectore_store_id())
+
+ if not vector_store_id:
+ vector_store = await client.vector_stores.create(name="knowledge_base")
+ await utils_repo.add_knowledge_vectore_store_id(vector_store.id)
+ vector_store_id = vector_store.id
+ else:
+ vector_store_id = vector_store_id.id_vector
+
+ file = await client.files.create(
+ file=(file_name, file_bytes, mem_type),
+ purpose="assistants"
+ )
+
+ await client.vector_stores.files.create(
+ vector_store_id=vector_store_id,
+ file_id=file.id
+ )
+
+ while True:
+ async for file_ in client.vector_stores.files.list(
+ vector_store_id=vector_store_id,
+ order='desc'
+ ):
+ if file_.id == file.id and file_.status == 'completed':
+ return True
+ if file_.id == file.id and file_.status == 'failed':
+ return False
+
+
+async def delete_knowledge_base(utils_repo: UtilsRepository):
+ is_vector_store = (await utils_repo.get_knowledge_vectore_store_id())
+ if is_vector_store:
+ vector_store_id = is_vector_store.id_vector
+ else:
+ return
+
+ await client.vector_stores.delete(vector_store_id=vector_store_id)
+
+ vector_store = await client.vector_stores.create(name="knowledge_base")
+ await utils_repo.delete_knowledge_vectore_store_id()
+ await utils_repo.add_knowledge_vectore_store_id(vector_store.id)
+
+
+async def save_user_context_txt_file(user_repo: UserRepository, user: User):
+ messages = await user_repo.get_messags(user_id=user.telegram_id)
+ runner = await Runner.run(
+ starting_agent=memory_creator_agent,
+ input=[{'role': message.role, 'content': message.content} for message in messages],
+ run_config=RunConfig(
+ tracing_disabled=False
+ )
+ )
+
+ input_tokens = runner.raw_responses[0].usage.input_tokens
+ output_tokens = runner.raw_responses[0].usage.output_tokens
+
+ answer = runner.final_output
+ byte_buffer = BytesIO(answer.encode("utf-8"))
+
+ memory_vector = await user_repo.get_memory_vector(user_id=user.telegram_id)
+ if not memory_vector:
+ vector_store = await client.vector_stores.create(name=f"user_memory_{user.telegram_id}")
+ await user_repo.add_memory_vector(user_id=user.telegram_id, vector_store_id=vector_store.id)
+ vector_store_id = vector_store.id
+ else:
+ vector_store_id = memory_vector.id_vector
+
+ file = await client.files.create(
+ file=(f'context_{user.telegram_id}.txt', byte_buffer, 'text/plain'),
+ purpose="assistants"
+ )
+
+ await client.vector_stores.files.create(
+ vector_store_id=vector_store_id,
+ file_id=file.id
+ )
+
+ while True:
+ async for file_ in client.vector_stores.files.list(
+ vector_store_id=vector_store_id,
+ order='desc'
+ ):
+ if file_.id == file.id and file_.status == 'completed':
+ return input_tokens, output_tokens
+ if file_.id == file.id and file_.status == 'failed':
+ return False
+
+
+async def delete_user_memory(user_repo: UserRepository, user: User):
+ memory_vector = await user_repo.get_memory_vector(user_id=user.telegram_id)
+ if memory_vector:
+ await client.vector_stores.delete(vector_store_id=memory_vector.id_vector)
+ await user_repo.delete_memory_vector(user_id=user.telegram_id)
+
+ images = os.listdir('images')
+ for image in images:
+ if str(user.telegram_id) in image:
+ os.remove(f'images/{image}')
+
+
+async def create_vectore_store(user_repo: UserRepository, user: User):
+ vector_store = await client.vector_stores.create(name=f"user_memory_{user.telegram_id}")
+ await user_repo.add_memory_vector(user_id=user.telegram_id, vector_store_id=vector_store.id)
+
+
+async def transcribe_audio(bytes_audio: bytes):
+ res = await client.audio.transcriptions.create(
+ file=('audio.ogg', bytes_audio),
+ model='whisper-1'
+ )
+
+ return res.text
+
+
+async def add_file_to_memory(user_repo: UserRepository, user: User, file_name: str, file_bytes: bytes, mem_type: str):
+ vector_store = await user_repo.get_memory_vector(user_id=user.telegram_id)
+
+ if not vector_store:
+ vector_store = await client.vector_stores.create(name=f"user_memory_{user.telegram_id}")
+ await user_repo.add_memory_vector(user_id=user.telegram_id, vector_store_id=vector_store.id)
+ vector_store_id = vector_store.id
+ else:
+ vector_store_id = vector_store.id_vector
+
+ file = await client.files.create(
+ file=(file_name, file_bytes, mem_type),
+ purpose="assistants"
+ )
+
+ await client.vector_stores.files.create(
+ vector_store_id=vector_store_id,
+ file_id=file.id
+ )
+
+ while True:
+ async for file_ in client.vector_stores.files.list(
+ vector_store_id=vector_store_id,
+ order='desc'
+ ):
+ if file_.id == file.id and file_.status == 'completed':
+ return True
+ if file_.id == file.id and file_.status == 'failed':
+ return False<
\ No newline at end of file
A => bot/utils/get_ton_course.py +25 -0
@@ 1,25 @@
+from aiohttp import ClientSession
+
+from redis.asyncio.client import Redis
+
+url = "https://api.coingecko.com/api/v3/simple/price"
+
+
+async def get_ton_course(redis: Redis):
+ ton_price = await redis.get("ton_price")
+ if ton_price:
+ return ton_price
+
+ params = {
+ "ids": "the-open-network",
+ "vs_currencies": "usd"
+ }
+ async with ClientSession() as session:
+ async with session.get(url, ssl=False, params=params) as response:
+ try:
+ data = await response.json()
+ ton_price = data["the-open-network"]["usd"]
+ await redis.set("ton_price", ton_price, ex=5)
+ return ton_price
+ except Exception as e:
+ return
A => bot/utils/send_answer.py +154 -0
@@ 1,154 @@
+import re
+
+from agents.mcp import MCPServerStdio
+from aiogram.types import Message, BufferedInputFile
+from chatgpt_md_converter import telegram_format
+from redis.asyncio.client import Redis
+
+from bot.utils.calculate_tokens import calculate_tokens
+from database.models import User
+from database.repositories.user import UserRepository
+from database.repositories.utils import UtilsRepository
+from bot.utils.agent_requests import AnswerText, text_request, AnswerImage, image_request
+import bot.keyboards.inline as inline_kb
+from config import TOKENS_LIMIT_FOR_WARNING_MESSAGE
+
+
+async def send_answer_text(user_ques: str, message: Message, answer: AnswerText, user: User, user_repo: UserRepository, i18n):
+ if answer.image_bytes:
+ await message.answer_photo(photo=BufferedInputFile(answer.image_bytes, filename=f"{user.telegram_id}.jpeg"),
+ caption=answer.answer)
+
+ await user_repo.add_context(user_id=user.telegram_id, role='user', content=user_ques)
+ await user_repo.add_context(user_id=user.telegram_id, role='assistant', content=answer.answer)
+ else:
+ await user_repo.add_context(user_id=user.telegram_id, role='user', content=user_ques)
+ row_id = await user_repo.add_context(user_id=user.telegram_id, role='assistant', content=answer.answer)
+ messages = split_code_message(answer.answer)
+
+ for index, mess in enumerate(messages, 1):
+ if len(messages) == index:
+ await message.answer(mess,
+ reply_markup=inline_kb.keyboard_md(row_id=row_id, text=i18n.get('answer_md')))
+ else:
+ await message.answer(mess)
+
+
+def split_code_message(text, chunk_size=3700, type_: str = None):
+ if not type_:
+ text = telegram_format(text)
+ text = text.replace('<blockquote expandable>', '<blockquote expandable>')
+ chunks = []
+ current_chunk = ""
+ open_tags = []
+ position = 0
+ tag_pattern = re.compile(r"<(\/)?([a-zA-Z0-9\-]+)([^>]*)>")
+
+ def close_open_tags():
+ return "".join(f"</{tag}>" for tag in reversed(open_tags))
+
+ def reopen_tags():
+ return "".join(f"<{tag if tag != 'blockquote' else 'blockquote expandable'}>" for tag in open_tags)
+
+ while position < len(text):
+ if len(current_chunk) >= chunk_size:
+ current_chunk += close_open_tags()
+ chunks.append(current_chunk)
+ current_chunk = reopen_tags()
+
+ next_cut = position + chunk_size - len(current_chunk)
+ next_match = tag_pattern.search(text, position, next_cut)
+
+ if not next_match:
+ current_chunk += text[position:next_cut]
+ position = next_cut
+ else:
+ start, end = next_match.span()
+ tag_full = next_match.group(0)
+ is_closing = next_match.group(1) == "/"
+ tag_name = next_match.group(2)
+
+ if start - position + len(current_chunk) >= chunk_size:
+ current_chunk += close_open_tags()
+ chunks.append(current_chunk)
+ current_chunk = reopen_tags()
+
+ current_chunk += text[position:start]
+ position = start
+
+ if is_closing:
+ if tag_name in open_tags:
+ open_tags.remove(tag_name)
+ else:
+ open_tags.append(tag_name)
+
+ current_chunk += tag_full
+ position = end
+
+ if current_chunk:
+ current_chunk += close_open_tags()
+ chunks.append(current_chunk)
+
+ return chunks
+
+
+async def process_after_text(message: Message, user: User, user_repo: UserRepository,
+ utils_repo: UtilsRepository, redis: Redis, i18n,
+ mess_to_delete: Message, mcp_server_1: MCPServerStdio, text_from_voice: str = None):
+ try:
+ answer = await text_request(text=text_from_voice if text_from_voice else message.text, user=user,
+ user_repo=user_repo, utils_repo=utils_repo, redis=redis, mcp_server_1=mcp_server_1,
+ bot=message.bot)
+
+ await send_answer_text(user_ques=message.text if message.text else 'image',
+ message=message, answer=answer, user=user, user_repo=user_repo, i18n=i18n)
+
+ if answer.input_tokens + answer.output_tokens > TOKENS_LIMIT_FOR_WARNING_MESSAGE:
+ await message.answer(i18n.get('warning_text_tokens'))
+
+ await calculate_tokens(user=user, user_repo=user_repo, input_tokens_text=answer.input_tokens,
+ input_tokens_img=answer.input_tokens_image, output_tokens_text=answer.output_tokens,
+ output_tokens_img=answer.output_tokens_image)
+ except Exception as e:
+ print(e)
+ await message.answer(text=i18n.get('warning_text_error'))
+ finally:
+ await redis.delete(f'request_{message.from_user.id}')
+ await mess_to_delete.delete()
+
+
+async def send_answer_photo(message: Message, answer: AnswerImage, user: User, user_repo: UserRepository):
+ caption = message.caption if message.caption else '.'
+ await user_repo.add_context(user_id=user.telegram_id, role='user', content=f'{answer.image_path}|{caption}')
+ await user_repo.add_context(user_id=user.telegram_id, role='assistant', content=answer.answer)
+
+ messages = split_code_message(answer.answer)
+
+ for index, mess in enumerate(messages, 1):
+ await message.answer(mess)
+
+
+async def process_after_photo(message: Message, user: User, user_repo: UserRepository,
+ utils_repo: UtilsRepository, redis: Redis, i18n, mess_to_delete: Message,
+ mcp_server_1: MCPServerStdio):
+ try:
+ file_id = message.photo[-1].file_id
+ file_path = await message.bot.get_file(file_id=file_id)
+ file_bytes = (await message.bot.download_file(file_path.file_path)).read()
+ answer = await image_request(image_bytes=file_bytes, user=user, user_repo=user_repo,
+ utils_repo=utils_repo, redis=redis, mcp_server_1=mcp_server_1, bot=message.bot,
+ caption=message.caption)
+
+ await send_answer_photo(message=message, answer=answer, user=user, user_repo=user_repo)
+
+ if answer.input_tokens + answer.output_tokens > TOKENS_LIMIT_FOR_WARNING_MESSAGE:
+ await message.answer(i18n.get('warning_text_tokens'))
+
+ await calculate_tokens(user=user, user_repo=user_repo, input_tokens_text=answer.input_tokens,
+ input_tokens_img=0, output_tokens_text=answer.output_tokens,
+ output_tokens_img=0)
+ except Exception as e:
+ await message.answer(text=i18n.get('warning_text_error'))
+ finally:
+ await redis.delete(f'request_{message.from_user.id}')
+ await mess_to_delete.delete()<
\ No newline at end of file
A => bot/utils/solana_funcs.py +27 -0
@@ 1,27 @@
+from solana.rpc.async_api import AsyncClient
+from solana.rpc.types import Pubkey, TokenAccountOpts
+from solders.keypair import Keypair
+
+
+async def get_balances(secret: list, client: AsyncClient):
+ list_balances = []
+ keypair = Keypair.from_bytes(bytes(secret))
+ public_key = str(keypair.pubkey())
+
+ balance_lamports = await client.get_balance(Pubkey.from_string(public_key))
+ list_balances.append(str(balance_lamports.value / 1_000_000_000) + ' SOL')
+ try:
+ tokens_balances = await client.get_token_accounts_by_owner(owner=Pubkey.from_string(public_key),
+ opts=TokenAccountOpts(program_id=Pubkey.from_string(
+ 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')),
+ )
+ for token in tokens_balances.value:
+ b = bytes(token.account.data)
+ mint = Pubkey.from_bytes(b[0:32])
+ amount = int.from_bytes(b[64:72], "little")
+ list_balances.append(str(amount) + ' ' + f'{str(mint)[:4]}...{str(mint)[-4:]}')
+ except Exception as e:
+ print(e)
+ pass
+
+ return list_balances, public_key
A => config.py +36 -0
@@ 1,36 @@
+# =============================================================================
+# MAIN CONFIGURATION SETTINGS for the bot behavior and features
+# =============================================================================
+
+# REQUIRED! Enter your Telegram ID (get from @userinfobot)
+ADMIN_ID = XXX
+
+# Bot usage mode: 'private' (owner only), 'free' (public with limits), 'pay' (monetized)
+TYPE_USAGE = 'private'
+
+# Daily credit allocation for pay and free mode users
+CREDITS_USER_DAILY = 500
+CREDITS_ADMIN_DAILY = 5000
+
+# Credit costs for text processing (per 1000 tokens, pay mode only)
+CREDITS_INPUT_TEXT = 2
+CREDITS_OUTPUT_TEXT = 8
+
+# Credit costs for image processing (per 1000 tokens, pay mode only)
+CREDITS_INPUT_IMAGE = 10
+CREDITS_OUTPUT_IMAGE = 40
+
+# Token usage warning threshold - user gets notified when exceeded
+TOKENS_LIMIT_FOR_WARNING_MESSAGE = 15000
+
+# Supported languages configuration
+AVAILABLE_LANGUAGES = ['en', 'ru']
+AVAILABLE_LANGUAGES_WORDS = ['English', 'Русский']
+DEFAULT_LANGUAGE = 'en'
+LANGUAGE_FALLBACKS = {
+ 'ru': ['ru', 'en'],
+ 'en': ['en']
+}
+
+# Application host address (do not modify)
+HOST_ADDRESS = 'https://evi.run'
A => database/models.py +128 -0
@@ 1,128 @@
+import os
+
+from dotenv import load_dotenv
+
+from sqlalchemy import (
+ Column, Integer, String, Boolean, DateTime, ForeignKey, Float, Text, Table, BigInteger,
+ TIMESTAMP, func
+)
+from sqlalchemy.orm import relationship, declarative_base
+from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
+
+from config import CREDITS_USER_DAILY
+
+load_dotenv()
+
+# Create SQLAlchemy base
+Base = declarative_base()
+
+# Create async engine
+engine = create_async_engine(
+ os.getenv('DATABASE_URL'),
+ echo=False,
+)
+
+# Create async session factory
+async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
+
+
+class User(Base):
+ __tablename__ = 'users'
+
+ telegram_id = Column(BigInteger, primary_key=True)
+ language = Column(String(10), nullable=True)
+ balance_credits = Column(Float, default=CREDITS_USER_DAILY)
+ created_at = Column(TIMESTAMP(timezone=True), server_default=func.now())
+
+ wallets = relationship('Wallet', back_populates='user')
+ messages = relationship('ChatMessage', back_populates='user')
+ payments = relationship('Payment', back_populates='user')
+ memory = relationship('MemoryVector', back_populates='user')
+ logs = relationship('Logs', back_populates='user')
+
+
+class ChatMessage(Base):
+ __tablename__ = 'chat_messages'
+
+ id = Column(Integer, primary_key=True)
+ user_id = Column(BigInteger, ForeignKey('users.telegram_id'))
+ role = Column(String(20)) # 'user' or 'assistant'
+ content = Column(Text)
+ input_tokens = Column(Integer, nullable=True)
+ output_tokens = Column(Integer, nullable=True)
+ timestamp = Column(TIMESTAMP(timezone=True), server_default=func.now())
+
+ user = relationship('User', back_populates='messages')
+
+
+class Payment(Base):
+ __tablename__ = 'payments'
+
+ id = Column(Integer, primary_key=True)
+
+ user_id = Column(BigInteger, ForeignKey('users.telegram_id'))
+ amount_usd = Column(Integer, nullable=False)
+ crypto_amount = Column(String)
+ crypto_currency = Column(String(20)) # 'TON', 'EVI'
+ random_suffix = Column(String(10), nullable=False)
+ status = Column(String(20), default='pending') # pending, confirmed, failed
+ created_at = Column(TIMESTAMP(timezone=True), server_default=func.now())
+ confirmed_at = Column(TIMESTAMP(timezone=True), nullable=True)
+
+ user = relationship('User', back_populates='payments')
+
+
+class TokenPrice(Base):
+ __tablename__ = 'token_prices'
+
+ id = Column(Integer, primary_key=True)
+ token = Column(String(20), unique=True)
+ price_usd = Column(Float, nullable=False)
+ updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), server_onupdate=func.now())
+
+
+class Wallet(Base):
+ __tablename__ = 'wallets'
+
+ id = Column(Integer, primary_key=True)
+ user_id = Column(BigInteger, ForeignKey('users.telegram_id'))
+ encrypted_private_key = Column(Text, nullable=False)
+ created_at = Column(TIMESTAMP(timezone=True), server_default=func.now())
+
+ user = relationship('User', back_populates='wallets')
+
+
+class KnowledgeVector(Base):
+ __tablename__ = 'knowledge_vectors'
+
+ id = Column(Integer, primary_key=True)
+ id_vector = Column(Text, nullable=False)
+ uploaded_at = Column(TIMESTAMP(timezone=True), server_default=func.now())
+
+
+class MemoryVector(Base):
+ __tablename__ = 'memory_vectors'
+
+ id = Column(Integer, primary_key=True)
+ user_id = Column(BigInteger, ForeignKey('users.telegram_id'))
+ id_vector = Column(Text, nullable=False)
+ created_at = Column(TIMESTAMP(timezone=True), server_default=func.now())
+
+ user = relationship('User', back_populates='memory')
+
+
+class Logs(Base):
+ __tablename__ = 'logs'
+
+ id = Column(Integer, primary_key=True)
+ user_id = Column(BigInteger, ForeignKey('users.telegram_id'))
+ message = Column(Text, nullable=False)
+ created_at = Column(TIMESTAMP(timezone=True), server_default=func.now())
+
+ user = relationship('User', back_populates='logs')
+
+
+async def create_tables():
+ async with engine.begin() as conn:
+ # await conn.run_sync(Base.metadata.drop_all)
+ await conn.run_sync(Base.metadata.create_all)
A => database/repositories/user.py +103 -0
@@ 1,103 @@
+import base64
+
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import and_, select, delete, update, asc
+
+from database.models import User, ChatMessage, Wallet, MemoryVector, Payment
+
+
+class UserRepository:
+ def __init__(self, session: AsyncSession):
+ self.session = session
+
+ async def get_by_telegram_id(self, telegram_id: int):
+ return await self.session.get(User, telegram_id)
+
+ async def create_if_not_exists(self, telegram_id: int, **kwargs):
+ user = await self.get_by_telegram_id(telegram_id)
+
+ if not user:
+ user = User(telegram_id=telegram_id, **kwargs)
+ self.session.add(user)
+ await self.session.commit()
+
+ return user
+
+ async def update(self, user: User, **kwargs):
+ if 'balance_credits' in kwargs:
+ kwargs['balance_credits'] = user.balance_credits - kwargs['balance_credits']
+
+ await self.session.execute(
+ update(User).where(User.telegram_id == user.telegram_id).values(**kwargs)
+ )
+
+ await self.session.commit()
+
+ async def delete_chat_messages(self, user: User):
+ await self.session.execute(delete(ChatMessage).where(ChatMessage.user_id == user.telegram_id))
+
+ await self.session.commit()
+
+ async def get_wallet(self, user_id: int):
+ wallet = await self.session.scalar(select(Wallet.encrypted_private_key).where(Wallet.user_id == user_id))
+ if wallet:
+ base64_bytes = wallet.encode('utf-8')
+ text_bytes = base64.b64decode(base64_bytes)
+ text = text_bytes.decode('utf-8')
+ return text
+ return None
+
+ async def get_messags(self, user_id: int):
+ return (await self.session.scalars(select(ChatMessage).
+ where(ChatMessage.user_id == user_id).
+ order_by(asc(ChatMessage.id)
+ )
+ )
+ ).fetchall()
+
+ async def get_memory_vector(self, user_id: int):
+ return await self.session.scalar(select(MemoryVector).where(MemoryVector.user_id == user_id))
+
+ async def add_memory_vector(self, user_id: int, vector_store_id: int):
+ memory_vector = MemoryVector(user_id=user_id, id_vector=vector_store_id)
+ self.session.add(memory_vector)
+ await self.session.commit()
+
+ async def delete_memory_vector(self, user_id: int):
+ await self.session.execute(delete(MemoryVector).where(MemoryVector.user_id == user_id))
+ await self.session.commit()
+
+ async def add_context(self, user_id: int, role: str, content: str):
+ chat_message = ChatMessage(user_id=user_id, role=role, content=content)
+ self.session.add(chat_message)
+ await self.session.commit()
+ return chat_message.id
+
+ async def delete_wallet_key(self, user_id: int):
+ await self.session.execute(delete(Wallet).where(Wallet.user_id == user_id))
+ await self.session.commit()
+
+ async def add_wallet_key(self, user_id: int, key: str):
+ await self.delete_wallet_key(user_id=user_id)
+ text_bytes = key.encode('utf-8')
+ base64_bytes = base64.b64encode(text_bytes)
+ base64_string = base64_bytes.decode('utf-8')
+ wallet = Wallet(user_id=user_id, encrypted_private_key=base64_string)
+ self.session.add(wallet)
+ await self.session.commit()
+
+ async def add_payment(self, user_id: int, amount: int, crypto_amount: str,
+ crypto_currency: str, random_suffix: str):
+ payment = Payment(user_id=user_id, amount_usd=amount, crypto_amount=crypto_amount,
+ crypto_currency=crypto_currency, random_suffix=random_suffix)
+ self.session.add(payment)
+ await self.session.commit()
+ return payment.id
+
+ async def add_user_credits(self, user_id: int, balance_credits: int):
+ await self.session.execute(update(User).where(User.telegram_id == user_id).
+ values(balance_credits=User.balance_credits + balance_credits))
+ await self.session.commit()
+
+ async def get_row_for_md(self, row_id: int):
+ return await self.session.scalar(select(ChatMessage).where(ChatMessage.id == row_id))
A => database/repositories/utils.py +69 -0
@@ 1,69 @@
+from datetime import datetime, timezone, timedelta
+
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import and_, select, delete, desc, update
+
+from database.models import User, ChatMessage, TokenPrice, KnowledgeVector, Payment
+from config import ADMIN_ID, CREDITS_ADMIN_DAILY, CREDITS_USER_DAILY
+
+
+class UtilsRepository:
+ def __init__(self, session: AsyncSession):
+ self.session = session
+
+ async def update_token_price(self, price: float):
+ token = await self.session.scalar(select(TokenPrice).where(TokenPrice.token == 'sol'))
+
+ if token:
+ token.price_usd = price
+ else:
+ token = TokenPrice(token='sol', price_usd=price)
+ self.session.add(token)
+
+ await self.session.commit()
+
+ async def get_token(self):
+ token = await self.session.scalar(select(TokenPrice).where(TokenPrice.token == 'sol'))
+ return token
+
+ async def get_knowledge_vectore_store_id(self):
+ return await self.session.scalar(select(KnowledgeVector))
+
+ async def add_knowledge_vectore_store_id(self, vectore_store_id):
+ vectore_store = KnowledgeVector(id_vector=vectore_store_id)
+ self.session.add(vectore_store)
+ await self.session.commit()
+
+ async def delete_knowledge_vectore_store_id(self):
+ await self.session.execute(delete(KnowledgeVector))
+ await self.session.commit()
+
+ async def check_payment_suffix(self, suffix: str):
+ payment = await self.session.scalar(select(Payment).
+ where(Payment.random_suffix == suffix).
+ order_by(desc(Payment.created_at)).limit(1))
+ if payment:
+ now_utc = datetime.now(timezone.utc)
+ created_utc = payment.created_at.astimezone(timezone.utc)
+ if (now_utc - created_utc) >= timedelta(minutes=15):
+ return True
+ return False
+
+ return True
+
+ async def get_payment(self, payment_id: int) -> Payment:
+ payment = await self.session.scalar(select(Payment).where(Payment.id == payment_id))
+ return payment
+
+ async def update_payment_status(self, payment_id: int, status: str):
+ await self.session.execute(update(Payment).where(Payment.id == payment_id).values(status=status))
+ await self.session.commit()
+
+ async def update_tokens_daily(self):
+ await self.session.execute(update(User).where(and_(User.telegram_id != ADMIN_ID,
+ User.balance_credits < CREDITS_USER_DAILY)
+ ).values(balance_credits=CREDITS_ADMIN_DAILY))
+ await self.session.execute(update(User).where(and_(User.telegram_id == ADMIN_ID,
+ User.balance_credits < CREDITS_USER_DAILY)
+ ).values(balance_credits=CREDITS_USER_DAILY))
+ await self.session.commit()<
\ No newline at end of file
A => docker-compose.yml +50 -0
@@ 1,50 @@
+version: '3.8'
+
+services:
+ postgres:
+ image: postgres:16
+ container_name: postgres_agent_db
+ environment:
+ POSTGRES_USER: "${POSTGRES_USER}"
+ POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
+ POSTGRES_DB: "${POSTGRES_DB}"
+ ports:
+ - "5434:5432"
+ volumes:
+ - ./data/postgres/postgres_data:/var/lib/postgresql/data
+ restart: unless-stopped
+
+
+ redis:
+ image: redis:latest
+ container_name: redis_agent
+ ports:
+ - "6380:6379"
+ volumes:
+ - ./data/redis/data:/data
+ restart: unless-stopped
+
+ bot:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: bot_agent
+ depends_on:
+ - postgres
+ - redis
+ - fastapi
+ volumes:
+ - ./data/images:/app/images
+ restart: unless-stopped
+
+ fastapi:
+ build:
+ context: .
+ dockerfile: Dockerfile_fastapi
+ container_name: fastapi_agent
+ ports:
+ - "8001:8000"
+ depends_on:
+ - postgres
+ - redis
+
A => docker_setup_en.sh +48 -0
@@ 1,48 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Check that the script is run as root or with sudo
+if [[ "$EUID" -ne 0 ]]; then
+ echo "This script must be run as root or with sudo."
+ exit 1
+fi
+
+echo "Removing old Docker packages and related..."
+for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
+ apt-get remove -y "$pkg"
+done
+
+echo "Updating package list and installing dependencies..."
+apt-get update
+apt-get install -y ca-certificates curl
+
+echo "Creating directory for GPG keys and downloading Docker key..."
+install -m 0755 -d /etc/apt/keyrings
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
+ -o /etc/apt/keyrings/docker.asc
+chmod a+r /etc/apt/keyrings/docker.asc
+
+echo "Adding Docker repository to apt sources..."
+ARCH=$(dpkg --print-architecture)
+CODENAME=$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
+cat <<EOF > /etc/apt/sources.list.d/docker.list
+deb [arch=$ARCH signed-by=/etc/apt/keyrings/docker.asc] \
+ https://download.docker.com/linux/ubuntu \
+ $CODENAME stable
+EOF
+
+echo "Updating package list..."
+apt-get update
+
+echo "Installing latest versions of Docker Engine and plugins..."
+apt-get install -y \
+ docker-ce \
+ docker-ce-cli \
+ containerd.io \
+ docker-buildx-plugin \
+ docker-compose-plugin
+
+echo "Running hello-world test container..."
+docker run --rm hello-world
+
+echo "Done! Docker installed and verified."<
\ No newline at end of file
A => redis_service/connect.py +8 -0
@@ 1,8 @@
+import os
+
+from dotenv import load_dotenv
+from redis.asyncio.client import Redis
+
+load_dotenv()
+
+redis = Redis.from_url(os.getenv('REDIS_URL'), decode_responses=True)<
\ No newline at end of file
A => requirements.txt +78 -0
@@ 1,78 @@
+aiofiles==24.1.0
+aiogram==3.20.0.post0
+aiogram_dialog==2.3.1
+aiohappyeyeballs==2.6.1
+aiohttp==3.11.18
+aiosignal==1.3.2
+alembic==1.16.1
+annotated-types==0.7.0
+anyio==4.9.0
+APScheduler==3.11.0
+attrs==25.3.0
+babel==2.17.0
+base58==2.1.1
+bitarray==3.4.2
+cachetools==5.5.2
+certifi==2025.4.26
+charset-normalizer==3.4.2
+chatgpt_md_converter==0.3.6
+click==8.2.1
+colorama==0.4.6
+construct==2.10.68
+construct-typing==0.6.2
+crc16==0.1.1
+crc32c==2.7.1
+distro==1.9.0
+fastapi==0.115.12
+fluent-compiler==0.3
+fluent.syntax==0.19.0
+fluentogram==1.1.10
+frozenlist==1.6.0
+greenlet==3.2.3
+griffe==1.7.3
+h11==0.16.0
+httpcore==1.0.9
+httpx==0.28.1
+httpx-sse==0.4.0
+idna==3.10
+Jinja2==3.1.6
+jiter==0.10.0
+jsonalias==0.1.1
+magic-filter==1.0.12
+Mako==1.3.10
+MarkupSafe==3.0.2
+mcp==1.9.2
+multidict==6.4.3
+openai==1.82.1
+openai-agents==0.0.16
+ordered-set==4.1.0
+propcache==0.3.1
+psycopg==3.2.9
+psycopg-binary==3.2.9
+pydantic==2.11.4
+pydantic-settings==2.9.1
+pydantic_core==2.33.2
+python-dotenv==1.1.0
+python-multipart==0.0.20
+pytonapi==0.4.9
+pytonlib==0.0.65
+pytz==2025.2
+redis==6.1.0
+requests==2.32.3
+six==1.17.0
+sniffio==1.3.1
+solana==0.36.7
+solders==0.26.0
+SQLAlchemy==2.0.41
+sse-starlette==2.3.6
+starlette==0.46.2
+tqdm==4.67.1
+tvm-valuetypes==0.0.12
+types-requests==2.32.0.20250515
+typing-inspection==0.4.0
+typing_extensions==4.13.2
+urllib3==2.4.0
+uvicorn==0.34.3
+watchdog==2.3.1
+websockets==15.0.1
+yarl==1.20.0
A => requirements_fastapi.txt +70 -0
@@ 1,70 @@
+aiofiles==24.1.0
+aiogram==3.20.0.post0
+aiogram_dialog==2.3.1
+aiohappyeyeballs==2.6.1
+aiohttp==3.11.18
+aiosignal==1.3.2
+alembic==1.16.1
+annotated-types==0.7.0
+anyio==4.9.0
+attrs==25.3.0
+babel==2.17.0
+bitarray==3.4.2
+cachetools==5.5.2
+certifi==2025.4.26
+charset-normalizer==3.4.2
+chatgpt_md_converter==0.3.6
+click==8.2.1
+colorama==0.4.6
+construct==2.10.68
+construct-typing==0.6.2
+distro==1.9.0
+fastapi==0.115.12
+fluent-compiler==0.3
+fluent.syntax==0.19.0
+fluentogram==1.1.10
+frozenlist==1.6.0
+greenlet==3.2.3
+griffe==1.7.3
+h11==0.16.0
+httpcore==1.0.9
+httpx==0.28.1
+httpx-sse==0.4.0
+idna==3.10
+Jinja2==3.1.6
+jiter==0.10.0
+jsonalias==0.1.1
+magic-filter==1.0.12
+Mako==1.3.10
+MarkupSafe==3.0.2
+mcp==1.9.2
+multidict==6.4.3
+ordered-set==4.1.0
+propcache==0.3.1
+psycopg==3.2.9
+psycopg-binary==3.2.9
+pydantic==2.11.4
+pydantic-settings==2.9.1
+pydantic_core==2.33.2
+python-dotenv==1.1.0
+python-multipart==0.0.20
+pytz==2025.2
+redis==6.1.0
+requests==2.32.3
+six==1.17.0
+sniffio==1.3.1
+solana==0.36.7
+solders==0.26.0
+SQLAlchemy==2.0.41
+sse-starlette==2.3.6
+starlette==0.46.2
+tqdm==4.67.1
+tvm-valuetypes==0.0.12
+types-requests==2.32.0.20250515
+typing-inspection==0.4.0
+typing_extensions==4.13.2
+urllib3==2.4.0
+uvicorn==0.34.3
+watchdog==2.3.1
+websockets==15.0.1
+yarl==1.20.0<
\ No newline at end of file