Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: Build, publish and deploy docker

on:
push:
branches: [ 'main' ]
tags:
- 'v*'

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
name: Build and push
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=test,enable=true
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

deploy-testing:
name: Deploy Testing
needs: build-and-push-image
runs-on: [ self-hosted, Linux, testing ]
environment:
name: Testing
env:
CONTAINER_NAME: com_profcomff_tgbot_mark_test
permissions:
packages: read
steps:
- name: Pull new version
run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test
- name: Migrate DB
run: |
docker run \
--rm \
--network=web \
--env DB_DSN='${{ secrets.DB_DSN }}' \
--env BOT_TOKEN='${{ secrets.BOT_TOKEN }}' \
--env BASE_URL='${{ vars.BASE_URL }}' \
--env WEBHOOK_PATH='${{ vars.WEBHOOK_PATH }}' \
--env QDRANT_API_KEY='${{ secrets.QDRANT_API_KEY }}' \
--env GIGA_KEY='${{ secrets.GIGA_KEY }}' \
--env HOST='${{ vars.HOST }}' \
--env PORT='${{ vars.PORT }}' \
--name ${{ env.CONTAINER_NAME }}_migration \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test \
alembic upgrade head
- name: Run new version
run: |
docker stop ${{ env.CONTAINER_NAME }} || true && docker rm ${{ env.CONTAINER_NAME }} || true
docker run \
--detach \
--network=web \
--restart on-failure:3 \
--env DB_DSN='${{ secrets.DB_DSN }}' \
--env BOT_TOKEN='${{ secrets.BOT_TOKEN }}' \
--env BASE_URL='${{ vars.BASE_URL }}' \
--env WEBHOOK_PATH='${{ vars.WEBHOOK_PATH }}' \
--env QDRANT_API_KEY='${{ secrets.QDRANT_API_KEY }}' \
--env GIGA_KEY='${{ secrets.GIGA_KEY }}' \
--env HOST='${{ vars.HOST }}' \
--env PORT='${{ vars.PORT }}' \
--name ${{ env.CONTAINER_NAME }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test

deploy-production:
name: Deploy Production
needs: build-and-push-image
if: startsWith(github.ref, 'refs/tags/v')
runs-on: [ self-hosted, Linux, production ]
environment:
name: Production
env:
CONTAINER_NAME: com_profcomff_tgbot_mark
permissions:
packages: read
steps:
- name: Pull new version
run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Migrate DB
run: |
docker run \
--rm \
--network=web \
--env DB_DSN='${{ secrets.DB_DSN }}' \
--env BOT_TOKEN='${{ secrets.BOT_TOKEN }}' \
--env BASE_URL='${{ vars.BASE_URL }}' \
--env WEBHOOK_PATH='${{ vars.WEBHOOK_PATH }}' \
--env QDRANT_API_KEY='${{ secrets.QDRANT_API_KEY }}' \
--env GIGA_KEY='${{ secrets.GIGA_KEY }}' \
--env HOST='${{ vars.HOST }}' \
--env PORT='${{ vars.PORT }}' \
--name ${{ env.CONTAINER_NAME }}_migration \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
alembic upgrade head
- name: Run new version
run: |
docker stop ${{ env.CONTAINER_NAME }} || true && docker rm ${{ env.CONTAINER_NAME }} || true
docker run \
--detach \
--network=web \
--restart always \
--env DB_DSN='${{ secrets.DB_DSN }}' \
--env BOT_TOKEN='${{ secrets.BOT_TOKEN }}' \
--env BASE_URL='${{ vars.BASE_URL }}' \
--env WEBHOOK_PATH='${{ vars.WEBHOOK_PATH }}' \
--env QDRANT_API_KEY='${{ secrets.QDRANT_API_KEY }}' \
--env GIGA_KEY='${{ secrets.GIGA_KEY }}' \
--env GIGA_KEY='${{ secrets.GIGA_KEY }}' \
--env HOST='${{ vars.HOST }}' \
--env PORT='${{ vars.PORT }}' \
--name ${{ env.CONTAINER_NAME }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
20 changes: 11 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
FROM python:3.11.13-slim

# Установка переменных окружения
ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION} \
APP_NAME=answer \
APP_MODULE=${APP_NAME}.routes.base:app \
PYTHONPATH=/app \
NLTK_DATA=/app/nltk_data
NLTK_DATA=/app/nltk_data \
CA_BUNDLE_URL=https://gu-st.ru/content/Other/doc/russiantrustedca.pem

# Установка системных зависимостей и загрузка данных NLTK
WORKDIR /app

# Установка системных зависимостей (включая curl для runtime)
RUN apt-get update && \
apt-get install -y --no-install-recommends wget && \
apt-get install -y --no-install-recommends wget curl ca-certificates && \
mkdir -p /app/nltk_data && \
pip install nltk && \
python -c "import nltk; \
nltk.download('stopwords', download_dir='/app/nltk_data'); \
nltk.download('punkt_tab', download_dir='/app/nltk_data'); \
nltk.download('punkt', download_dir='/app/nltk_data')" && \
apt-get remove -y wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Копирование зависимостей и установка
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Копирование исходного кода
COPY . .

# Проверка структуры проекта (после COPY)
RUN ls -lR /app
# Entrypoint для загрузки сертификата
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

CMD ["uvicorn", "answer.routes.base:app", "--host", "0.0.0.0", "--port", "8000"]
ENTRYPOINT ["/entrypoint.sh"]
CMD ["uvicorn", "answer.routes.base:app", "--host", "0.0.0.0", "--port", "8000"]
48 changes: 12 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,63 +22,39 @@

1. Перейдите в папку проекта

2. Установите сертификаты Минцифры (для работы Гигачат API)
```console
cd llm
curl -o russian_trusted_root_ca.crt "https://gu-st.ru/content/Other/doc/russiantrustedca.pem"
```

3. Создайте виртуальное окружение и активируйте его:
2. Создайте виртуальное окружение и активируйте его:
```console
python3 -m venv venv
source ./venv/bin/activate # На MacOS и Linux
venv\Scripts\activate # На Windows
```

4. Установите зависимости
3. Установите зависимости
```console
pip install -r requirements.txt
```
```console
python -m nltk.downloader punkt_tab
```

5. Установите переменные окружения
4. Установите переменные окружения
```console
# Ключ для доступа к бд
export QDRANT_API_KEY="qdrant_api_key"

# Ключ для гигачат API
export GIGA_KEY_PATH="gigakey.txt"

export SERVICE_ACCOUNT_ID="FROM YAGPT"

export KEY_ID="FROM YAGPT"

export PRIVATE_KEY="FROM YAGPT"

EXPORT BOT_TOKEN="FROM YAGPT"
```

6. Запустите приложение
5. Запустите приложение
```console
python -m answer
```

## Запуск через Docker
```console
# Установка сертификатов
cd llm
curl -o russian_trusted_root_ca.crt "https://gu-st.ru/content/Other/doc/russiantrustedca.pem"

# Сборка образа
docker build -t my-fastapi-langchain .

# Поднятие контейнера
docker run -d \
-p 127.0.0.1:8000:8000 \
--name my-fastapi-langchain \
-v "/Локальный/путь/до/chroma_db:/app/chroma_db" \
-v "/Локальный/путь/до/gigakey.txt:/app/gigakey.txt:ro" \
-e CHROMA_DIR="/app/chroma_db" \
-e GIGA_KEY_PATH="/app/gigakey.txt" \
-e APP_MODULE="answer.routes.base:app" \
-e PYTHONPATH="/app" \
my-fastapi-langchain
```


## ENV-file description
- `DB_DSN=postgresql://postgres@localhost:5432/postgres` – Данные для подключения к БД
22 changes: 21 additions & 1 deletion answer/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import asyncio
import logging

import uvicorn
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.types import BotCommand, BotCommandScopeDefault, Update
from fastapi import Request

from answer.routes.base import app
from answer.settings import Settings, get_settings


settings: Settings = get_settings()
logger = logging.getLogger(__name__)

if __name__ == '__main__':
uvicorn.run(app)
try:
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger.info("Starting FastAPI app with bot integration")

uvicorn.run(app, host=settings.HOST, port=settings.PORT, log_level="info")
except (KeyboardInterrupt, SystemExit):
logger.info("Application stopped")
Empty file added answer/bot/__init__.py
Empty file.
58 changes: 58 additions & 0 deletions answer/bot/tg_bot/initialisation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import logging

from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import BotCommand, BotCommandScopeDefault

from answer.handlers.ask_bot import router as router_ask_bot
from answer.handlers.info import router as info_router
from answer.handlers.start import start_router
from answer.settings import Settings, get_settings


settings: Settings = get_settings()
logger = logging.getLogger(__name__)

storage = MemoryStorage()

bot: Bot = Bot(token=settings.BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp: Dispatcher = Dispatcher(storage=storage)

dp.include_router(start_router)
dp.include_router(info_router)
dp.include_router(router_ask_bot)


async def setup_bot():
logger.info("Setting up bot commands and webhook")

commands = [BotCommand(command='start', description='Старт')]
await bot.set_my_commands(commands, BotCommandScopeDefault())
await bot.set_webhook(f"{settings.BASE_URL}{settings.WEBHOOK_PATH}")

logger.info("Bot setup completed")


async def bot_startup():
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger.info("Starting FastAPI app with bot integration")

await setup_bot()

logger.info("Bot initialized and webhook set")
return bot, dp


async def bot_shutdown():
if bot:
await bot.delete_webhook(drop_pending_updates=True)
await bot.session.close()

logger.info("Bot shutdown completed")


def get_bot_objects(): # вот это здорово придумал конечно))
"""Возвращает объекты бота и диспетчера"""
return bot, dp
Empty file added answer/handlers/__init__.py
Empty file.
Loading