~cytrogen/evi-run

0a538df3d09185427872e44bf81bded93517f818 — Bendy 6 months ago
Initial commit
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">

![evi.run Logo](https://img.shields.io/badge/evi-run-blue?style=flat-square&logo=robot&logoColor=white)
[![Python](https://img.shields.io/badge/Python-3.9+-blue?style=flat-square&logo=python&logoColor=white)](https://python.org)
[![OpenAI](https://img.shields.io/badge/OpenAI-Agents_SDK-green?style=flat-square&logo=openai&logoColor=white)](https://openai.github.io/openai-agents-python/)
[![Telegram](https://img.shields.io/badge/Telegram-Bot_API-blue?style=flat-square&logo=telegram&logoColor=white)](https://core.telegram.org/bots/api)
[![Docker](https://img.shields.io/badge/Docker-Compose-blue?style=flat-square&logo=docker&logoColor=white)](https://docker.com)

[![Python CI](https://github.com/pipedude/evi-run/workflows/Python%20CI/badge.svg)](https://github.com/pipedude/evi-run/actions)
[![Docker Build](https://github.com/pipedude/evi-run/workflows/Docker%20Build%20&%20Publish/badge.svg)](https://github.com/pipedude/evi-run/actions)
[![Release](https://github.com/pipedude/evi-run/workflows/Release/badge.svg)](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  => bot/dialogs/menu.py +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('&lt;blockquote expandable&gt;', '<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