From da4f64994f4e64c0f981c44102b1937411c1edf0 Mon Sep 17 00:00:00 2001 From: Drobysh Date: Sun, 20 Sep 2026 17:17:06 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20AI-=D0=B0=D1=83=D0=B4=D0=B8=D1=82=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=B0=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20Git?= =?UTF-8?q?ea=20Actions=20+=20DeepSeek=20V4.1=20Flash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ai-review.yml | 28 ++++ scripts/ai_review.py | 259 +++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 .gitea/workflows/ai-review.yml create mode 100644 scripts/ai_review.py diff --git a/.gitea/workflows/ai-review.yml b/.gitea/workflows/ai-review.yml new file mode 100644 index 0000000..261af51 --- /dev/null +++ b/.gitea/workflows/ai-review.yml @@ -0,0 +1,28 @@ +name: AI Code Review + +on: + # Кнопка "Run workflow" в UI Gitea + workflow_dispatch: + +jobs: + review: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: pip install httpx + + - name: Run AI review + env: + TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }} + BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + GITEA_INSTANCE: https://git.filwordspl.com + GITEA_REPO: Drobysh/filwordspl-backend + run: python scripts/ai_review.py diff --git a/scripts/ai_review.py b/scripts/ai_review.py new file mode 100644 index 0000000..4e78c1f --- /dev/null +++ b/scripts/ai_review.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +""" +AI Code Review for Filwords PL Backend + +Читает Python-файлы проекта, отправляет в DeepSeek V4.1 Flash (Together AI), +получает структурированное ТЗ и создаёт Issue в Gitea. + +Запускается через Gitea Actions (workflow_dispatch — вручную из UI). + +(20.09.2026) Первая версия — конвейер без tool-calling, предсказуемо по токенам. +""" + +import os +import sys +from pathlib import Path + +import httpx + + +# ============ Настройки ============ + +TOGETHER_API_KEY = os.environ.get("TOGETHER_API_KEY") +GITEA_BOT_TOKEN = os.environ.get("BOT_TOKEN") +GITEA_INSTANCE = os.environ.get("GITEA_INSTANCE", "https://git.filwordspl.com") +GITEA_REPO = os.environ.get("GITEA_REPO", "Drobysh/filwordspl-backend") + +MODEL = "deepseek-ai/DeepSeek-V4.1-Flash" +MAX_OUTPUT_TOKENS = 4000 + +# Что игнорировать при сборе кода +IGNORE_DIRS = { + "venv", ".venv", "__pycache__", ".git", ".gitea", + "tests", "archive", "uploads", "tts_cache", + "node_modules", "dist", +} +IGNORE_FILES = {"ai_review.py", "conftest.py"} + +# Расширения, которые читаем +INCLUDE_EXTENSIONS = {".py"} + + +# ============ Проверка окружения ============ + +def check_env() -> None: + missing = [] + if not TOGETHER_API_KEY: + missing.append("TOGETHER_API_KEY") + if not GITEA_BOT_TOKEN: + missing.append("BOT_TOKEN") + if missing: + print(f"❌ Отсутствуют переменные окружения: {', '.join(missing)}", file=sys.stderr) + sys.exit(1) + + +# ============ Сбор кода ============ + +def collect_code(repo_root: Path) -> str: + """Собирает все .py файлы в один текст с заголовками.""" + chunks: list[str] = [] + total_files = 0 + total_size = 0 + + for path in sorted(repo_root.rglob("*")): + if not path.is_file(): + continue + if path.suffix not in INCLUDE_EXTENSIONS: + continue + + # Пропускаем игнорируемые папки + rel = path.relative_to(repo_root) + if any(part in IGNORE_DIRS for part in rel.parts): + continue + if path.name in IGNORE_FILES: + continue + + try: + content = path.read_text(encoding="utf-8") + except Exception as e: + print(f"⚠️ Не прочитал {rel}: {e}", file=sys.stderr) + continue + + # Пропускаем слишком большие файлы (>100 KB) + if len(content) > 100_000: + print(f"⚠️ Пропущен большой файл: {rel} ({len(content)} символов)", file=sys.stderr) + continue + + chunks.append(f"### FILE: {rel}\n```python\n{content}\n```\n") + total_files += 1 + total_size += len(content) + + print(f"📂 Собрано файлов: {total_files}, символов: {total_size:,}") + return "\n".join(chunks) + + +# ============ Промпт для модели ============ + +SYSTEM_PROMPT = """Ты — опытный техлид и ревьюер Python-кода (FastAPI, asyncpg, PostgreSQL). + +Твоя задача — проанализировать код и выдать СТРУКТУРИРОВАННОЕ ТЗ для разработчика. + +ПРАВИЛА: +1. НЕ переписывай код. Только описывай, что не так и что сделать. +2. Каждый пункт — КОНКРЕТНЫЙ: файл → строка/функция → проблема → что сделать. +3. Без воды. Без общих слов. Без объяснений «что такое XSS» — только факт и фикс. +4. Приоритеты: 🔴 критично / 🟡 важно / 🟢 желательно. +5. Если проблем мало — лучше меньше пунктов, но точных. +6. Максимум 20 пунктов — только самое важное. + +ФОРМАТ ОТВЕТА (markdown): + +## 🔴 Критично + +### 1. [короткое название] +- **Файл:** `path/to/file.py:123` +- **Проблема:** что не так +- **Что сделать:** конкретное действие +- **Критерий:** как проверить, что исправлено + +## 🟡 Важно +(то же самое) + +## 🟢 Желательно +(то же самое) + +Если проблем нет — напиши: "✅ Критичных проблем не найдено". +""" + + +USER_PROMPT_TEMPLATE = """Проанализируй код проекта Filwords PL (backend). + +Это FastAPI + PostgreSQL + asyncpg. Есть: +- routers/ — эндпоинты +- services/ — бизнес-логика +- database.py — схема БД +- auth.py — HMAC-SHA256 initData + +Ищи: +1. Баги и логические ошибки +2. Проблемы безопасности (утечки, инъекции, неправильная валидация) +3. N+1 запросы к БД +4. Отсутствие обработки ошибок +5. Дублирование кода +6. Мёртвый код +7. Слабые места архитектуры + +Вот код: + +{code} +""" + + +# ============ Запрос к Together AI ============ + +def call_together(code: str) -> str: + """Отправляет код в DeepSeek V4.1 Flash, возвращает текст ответа.""" + print("🤖 Отправляю код в DeepSeek V4.1 Flash...") + + user_prompt = USER_PROMPT_TEMPLATE.format(code=code) + + response = httpx.post( + "https://api.together.xyz/v1/chat/completions", + headers={ + "Authorization": f"Bearer {TOGETHER_API_KEY}", + "Content-Type": "application/json", + }, + json={ + "model": MODEL, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": user_prompt}, + ], + "max_tokens": MAX_OUTPUT_TOKENS, + "temperature": 0.3, + }, + timeout=300.0, + ) + + if response.status_code != 200: + print(f"❌ Together AI вернул {response.status_code}: {response.text[:500]}", file=sys.stderr) + sys.exit(1) + + data = response.json() + result = data["choices"][0]["message"]["content"] + + usage = data.get("usage", {}) + print(f"✅ Ответ получен. Токены: вход={usage.get('prompt_tokens', 0)}, выход={usage.get('completion_tokens', 0)}") + + return result + + +# ============ Создание Issue в Gitea ============ + +def create_issue(title: str, body: str) -> str: + """Создаёт Issue в Gitea, возвращает URL.""" + print(f"📝 Создаю Issue в Gitea...") + + response = httpx.post( + f"{GITEA_INSTANCE}/api/v1/repos/{GITEA_REPO}/issues", + headers={ + "Authorization": f"token {GITEA_BOT_TOKEN}", + "Content-Type": "application/json", + }, + json={ + "title": title, + "body": body, + }, + timeout=30.0, + ) + + if response.status_code not in (200, 201): + print(f"❌ Gitea вернул {response.status_code}: {response.text[:500]}", file=sys.stderr) + sys.exit(1) + + data = response.json() + issue_url = data.get("html_url", "") + print(f"✅ Issue создан: {issue_url}") + return issue_url + + +# ============ Main ============ + +def main() -> None: + check_env() + + repo_root = Path(".").resolve() + print(f"📁 Репозиторий: {repo_root}") + + code = collect_code(repo_root) + if not code: + print("❌ Не найдено файлов для анализа", file=sys.stderr) + sys.exit(1) + + analysis = call_together(code) + + from datetime import datetime + date_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC") + title = f"🤖 AI Review: список задач ({date_str})" + + body = f"""# 🤖 AI Code Review + +**Дата:** {date_str} +**Модель:** `{MODEL}` +**Репозиторий:** `{GITEA_REPO}` + +--- + +{analysis} + +--- + +_Этот Issue создан автоматически. Исправления делаются вручную._ +""" + + create_issue(title, body) + print("🎉 Готово!") + + +if __name__ == "__main__": + main()