diff --git a/.gitea/workflows/ai-review.yml b/.gitea/workflows/ai-review.yml index 9051870..261af51 100644 --- a/.gitea/workflows/ai-review.yml +++ b/.gitea/workflows/ai-review.yml @@ -1,6 +1,7 @@ name: AI Code Review on: + # Кнопка "Run workflow" в UI Gitea workflow_dispatch: jobs: diff --git a/scripts/ai_review.py b/scripts/ai_review.py index 7bd54b8..6d7c9b2 100644 --- a/scripts/ai_review.py +++ b/scripts/ai_review.py @@ -8,7 +8,8 @@ AI Code Review for Filwords PL Backend Запускается через Gitea Actions (workflow_dispatch — вручную из UI). (20.09.2026) Первая версия — конвейер без tool-calling, предсказуемо по токенам. -(20.09.2026, вечер) Фикс: tool_choice=none, безопасное извлечение content. +(20.09.2026, вечер) Фикс: max_tokens=16000, fallback на reasoning_content +(DeepSeek V4.1 Flash — reasoning-модель, тратит много токенов на размышления). """ import os @@ -27,7 +28,10 @@ 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 + +# 🔧 ФИКС (20.09.2026): было 4000 — DeepSeek V4.1 Flash тратит ~4000 на reasoning, +# не оставляя токенов на финальный ответ. 16000 хватает с запасом. +MAX_OUTPUT_TOKENS = 16000 # Что игнорировать при сборе кода IGNORE_DIRS = { @@ -71,7 +75,6 @@ def collect_code(repo_root: Path) -> str: 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 @@ -84,7 +87,6 @@ def collect_code(repo_root: Path) -> str: print(f"⚠️ Не прочитал {rel}: {e}", file=sys.stderr) continue - # Пропускаем слишком большие файлы if len(content) > MAX_FILE_SIZE: print(f"⚠️ Пропущен большой файл: {rel} ({len(content)} символов)", file=sys.stderr) continue @@ -176,7 +178,6 @@ def call_together(code: str) -> str: ], "max_tokens": MAX_OUTPUT_TOKENS, "temperature": 0.3, - "tool_choice": "none", }, timeout=300.0, ) @@ -189,18 +190,28 @@ def call_together(code: str) -> str: data = response.json() try: - result = data["choices"][0]["message"]["content"] - if result is None: - raise KeyError("content is None") + message = data["choices"][0]["message"] + result = message.get("content") + + # 🔧 ФИКС (20.09.2026): если content пустой — используем reasoning_content. + # DeepSeek V4.1 Flash — reasoning-модель, может уйти в размышления + # и не выдать финальный ответ. Тогда берём reasoning. + if not result: + result = message.get("reasoning_content") + + if not result: + raise KeyError("и content, и reasoning_content пустые") except (KeyError, IndexError, TypeError) as e: print(f"❌ Неожиданный формат ответа: {e}", file=sys.stderr) print(f"Полный ответ: {data}", file=sys.stderr) sys.exit(1) usage = data.get("usage", {}) + finish = data["choices"][0].get("finish_reason", "unknown") print( f"✅ Ответ получен. Токены: вход={usage.get('prompt_tokens', 0)}, " - f"выход={usage.get('completion_tokens', 0)}" + f"выход={usage.get('completion_tokens', 0)}, " + f"finish={finish}" ) return result