This commit is contained in:
Primakov Alexandr Alexandrovich
2025-10-12 23:15:09 +03:00
commit 09cdd06307
88 changed files with 15007 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
"""LangGraph agents for code review"""
from app.agents.reviewer import ReviewerAgent
__all__ = ["ReviewerAgent"]

View File

@@ -0,0 +1,139 @@
"""Prompts for AI code reviewer"""
SYSTEM_PROMPT = """Ты строгий и внимательный code reviewer с многолетним опытом. Твоя задача - тщательно анализировать код и находить ВСЕ проблемы.
ОБЯЗАТЕЛЬНО проверяй:
1. **Синтаксические ошибки** - опечатки, незакрытые скобки, некорректный синтаксис языка
2. **Потенциальные баги** - логические ошибки, неправильная обработка исключений, проблемы с null/undefined
3. **Проблемы безопасности** - SQL injection, XSS, небезопасное использование eval, утечки данных
4. **Нарушения best practices** - неправильное использование React (key prop, hooks), плохие названия переменных
5. **Проблемы производительности** - неэффективные алгоритмы, лишние ререндеры, утечки памяти
6. **Читаемость кода** - сложная логика, отсутствие обработки ошибок
Особое внимание:
- В React: правильность использования key, hooks rules, JSX syntax
- Опечатки в строковых константах (API paths, Content-Type headers)
- Незакрытые/лишние скобки в JSX и JavaScript
- Несоответствие кода описанию в PR
Для каждой проблемы укажи:
- Номер строки
- Уровень серьезности: ERROR (критично), WARNING (важно), INFO (рекомендация)
- Что не так
- Как исправить
Будь требовательным! Даже мелкие опечатки могут сломать продакшн."""
CODE_REVIEW_PROMPT = """Проанализируй следующий код из файла `{file_path}`:
```{language}
{code}
```
Контекст: это изменения в Pull Request.
{patch_info}
Найди проблемы и предложи улучшения. Для каждой проблемы укажи:
1. Номер строки
2. Уровень: INFO, WARNING или ERROR
3. Описание проблемы
4. Рекомендация
Ответ дай в формате JSON:
{{
"comments": [
{{
"line": <номер_строки>,
"severity": "INFO|WARNING|ERROR",
"message": "описание проблемы и рекомендация"
}}
]
}}
Если проблем нет, верни пустой массив comments."""
DIFF_REVIEW_PROMPT = """Ты СТРОГИЙ code reviewer. Твоя задача - найти ВСЕ ошибки в коде.
{pr_context}
Анализируй изменения в файле `{file_path}`:
```diff
{diff}
```
ПОШАГОВЫЙ АНАЛИЗ каждой строки с +:
Шаг 1: ЧИТАЙ КАЖДУЮ СТРОКУ с + внимательно
Шаг 2: ПРОВЕРЬ каждую строку на:
a) ОПЕЧАТКИ - неправильные слова, typos
b) СИНТАКСИС - скобки, кавычки, запятые
c) ЛОГИКА - правильность кода
d) REACT ПРАВИЛА - key, hooks, JSX
Шаг 3: НАЙДИ ошибки (даже мелкие!)
КОНКРЕТНЫЕ ПРИМЕРЫ ОШИБОК (ОБЯЗАТЕЛЬНО ИЩИ ТАКИЕ):
❌ ОПЕЧАТКИ В СТРОКАХ:
'Content-Type': 'shmapplication/json' // ОШИБКА! должно быть 'application/json'
const url = 'htps://example.com' // ОШИБКА! должно быть 'https'
❌ НЕЗАКРЫТЫЕ СКОБКИ:
{{condition && (<div>text</div>}} // ОШИБКА! пропущена )
<span>{{text</span> // ОШИБКА! пропущена }}
❌ НЕПРАВИЛЬНЫЙ KEY В REACT:
<div>
<Item> // ОШИБКА! key должен быть ЗДЕСЬ
<img key={{id}} /> // а не здесь
</Item>
</div>
❌ УДАЛЕНИЕ KEY:
-<Item key={{id}}> // ОШИБКА! удалили key
+<Item>
❌ НЕСООТВЕТСТВИЕ ОПИСАНИЮ PR:
Описание PR: "Добавление функционала редактирования аватара"
Код: меняет Content-Type на 'shmapplication/json' // ОШИБКА! не связано с аватарами
ОБЯЗАТЕЛЬНО ПРОВЕРЬ:
1. СООТВЕТСТВИЕ ОПИСАНИЮ PR - делает ли код то что написано в описании?
2. Все строки в кавычках - нет ли опечаток?
3. Все скобки - все ли закрыты?
4. Все JSX элементы - правильно ли?
5. React key - на правильном элементе?
{format_instructions}
ВАЖНО:
1. ТОЛЬКО JSON в ответе!
2. НЕ ПИШИ "Thank you" или другой текст
3. Даже мелкая опечатка - это ERROR!
4. Если проблем НЕТ: {{"comments": []}}
Структура ответа:
{{
"comments": [
{{
"line": 58,
"severity": "ERROR",
"message": "Опечатка в строке: 'shmapplication/json' должно быть 'application/json'"
}}
]
}}"""
SUMMARY_PROMPT = """На основе всех найденных проблем в PR создай краткое резюме ревью.
Найденные проблемы:
{issues_summary}
Создай краткое резюме (2-3 предложения), которое:
- Указывает общее количество найденных проблем по уровням серьезности
- Выделяет наиболее критичные моменты
- Дает общую оценку качества кода
Ответ верни в виде текста без форматирования."""

View File

@@ -0,0 +1,488 @@
"""Main reviewer agent using LangGraph"""
from typing import TypedDict, List, Dict, Any, Optional
from langgraph.graph import StateGraph, END
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.agents.tools import CodeAnalyzer, detect_language, should_review_file
from app.agents.prompts import SYSTEM_PROMPT, SUMMARY_PROMPT
from app.models import Review, Comment, PullRequest, Repository
from app.models.review import ReviewStatusEnum
from app.models.comment import SeverityEnum
from app.services import GiteaService, GitHubService, BitbucketService
from app.services.base import BaseGitService
from app.config import settings
class ReviewState(TypedDict):
"""State for the review workflow"""
review_id: int
pr_number: int
repository_id: int
status: str
files: List[Dict[str, Any]]
analyzed_files: List[str]
comments: List[Dict[str, Any]]
error: Optional[str]
git_service: Optional[BaseGitService]
class ReviewerAgent:
"""Agent for reviewing code using LangGraph"""
def __init__(self, db: AsyncSession):
self.db = db
self.analyzer = CodeAnalyzer(
ollama_base_url=settings.ollama_base_url,
model=settings.ollama_model
)
self.graph = self._build_graph()
def _build_graph(self) -> StateGraph:
"""Build the LangGraph workflow"""
workflow = StateGraph(ReviewState)
# Add nodes
workflow.add_node("fetch_pr_info", self.fetch_pr_info)
workflow.add_node("fetch_files", self.fetch_files)
workflow.add_node("analyze_files", self.analyze_files)
workflow.add_node("post_comments", self.post_comments)
workflow.add_node("complete_review", self.complete_review)
# Set entry point
workflow.set_entry_point("fetch_pr_info")
# Add edges
workflow.add_edge("fetch_pr_info", "fetch_files")
workflow.add_edge("fetch_files", "analyze_files")
workflow.add_edge("analyze_files", "post_comments")
workflow.add_edge("post_comments", "complete_review")
workflow.add_edge("complete_review", END)
return workflow.compile()
def _remove_think_blocks(self, text: str) -> str:
"""Remove <think>...</think> blocks from text"""
import re
# Remove <think> blocks
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL | re.IGNORECASE)
# Remove extra whitespace
text = re.sub(r'\n\n+', '\n\n', text)
return text.strip()
def _escape_html_in_text(self, text: str) -> str:
"""Escape HTML tags in text to prevent Markdown from hiding them
Wraps code-like content (anything with < >) in backticks.
"""
import re
# Pattern to find HTML-like tags (e.g., <CharacterItem>, <img>)
# We want to wrap them in backticks so they display correctly
def replace_tag(match):
tag = match.group(0)
# If it's already in backticks or code block, skip
return f"`{tag}`"
# Find all <...> patterns and wrap them
text = re.sub(r'<[^>]+>', replace_tag, text)
return text
def _get_git_service(self, repository: Repository) -> BaseGitService:
"""Get appropriate Git service for repository"""
from app.utils import decrypt_token
from app.config import settings
# Parse repository URL to get owner and name
# Assuming URL format: https://git.example.com/owner/repo
parts = repository.url.rstrip('/').split('/')
repo_name = parts[-1].replace('.git', '')
repo_owner = parts[-2]
base_url = '/'.join(parts[:-2])
# Определяем токен: проектный или мастер
if repository.api_token:
# Используем проектный токен
try:
decrypted_token = decrypt_token(repository.api_token)
print(f" 🔑 Используется проектный токен")
except ValueError as e:
raise ValueError(f"Не удалось расшифровать API токен для репозитория {repository.name}: {str(e)}")
else:
# Используем мастер токен
platform = repository.platform.value.lower()
if platform == "gitea":
decrypted_token = settings.master_gitea_token
elif platform == "github":
decrypted_token = settings.master_github_token
elif platform == "bitbucket":
decrypted_token = settings.master_bitbucket_token
else:
raise ValueError(f"Unsupported platform: {repository.platform}")
if not decrypted_token:
raise ValueError(
f"API токен не указан для репозитория {repository.name} "
f"и мастер токен для {platform} не настроен в .env (MASTER_{platform.upper()}_TOKEN)"
)
print(f" 🔑 Используется мастер {platform} токен")
if repository.platform.value == "gitea":
return GiteaService(base_url, decrypted_token, repo_owner, repo_name)
elif repository.platform.value == "github":
return GitHubService(base_url, decrypted_token, repo_owner, repo_name)
elif repository.platform.value == "bitbucket":
return BitbucketService(base_url, decrypted_token, repo_owner, repo_name)
else:
raise ValueError(f"Unsupported platform: {repository.platform}")
async def fetch_pr_info(self, state: ReviewState) -> ReviewState:
"""Fetch PR information"""
try:
# Update review status
result = await self.db.execute(
select(Review).where(Review.id == state["review_id"])
)
review = result.scalar_one()
review.status = ReviewStatusEnum.FETCHING
await self.db.commit()
# Get repository
result = await self.db.execute(
select(Repository).where(Repository.id == state["repository_id"])
)
repository = result.scalar_one()
# Initialize Git service
git_service = self._get_git_service(repository)
state["git_service"] = git_service
# Fetch PR info
pr_info = await git_service.get_pull_request(state["pr_number"])
print("\n" + "📋"*40)
print("ИНФОРМАЦИЯ О PR")
print("📋"*40)
print(f"\n📝 Название: {pr_info.title}")
print(f"👤 Автор: {pr_info.author}")
print(f"🔀 Ветки: {pr_info.source_branch}{pr_info.target_branch}")
print(f"📄 Описание:")
print("-" * 80)
print(pr_info.description if pr_info.description else "(без описания)")
print("-" * 80)
print("📋"*40 + "\n")
# Store PR info in state
state["pr_info"] = {
"title": pr_info.title,
"description": pr_info.description,
"author": pr_info.author,
"source_branch": pr_info.source_branch,
"target_branch": pr_info.target_branch
}
state["status"] = "pr_info_fetched"
return state
except Exception as e:
print(f"❌ ОШИБКА в fetch_pr_info: {e}")
import traceback
traceback.print_exc()
state["error"] = str(e)
state["status"] = "failed"
return state
async def fetch_files(self, state: ReviewState) -> ReviewState:
"""Fetch changed files in PR"""
try:
git_service = state["git_service"]
print("\n" + "📥"*40)
print("ПОЛУЧЕНИЕ ФАЙЛОВ ИЗ PR")
print("📥"*40)
# Get changed files
files = await git_service.get_pr_files(state["pr_number"])
print(f"\n📊 Получено файлов из API: {len(files)}")
for i, f in enumerate(files, 1):
print(f"\n {i}. {f.filename}")
print(f" Status: {f.status}")
print(f" +{f.additions} -{f.deletions}")
print(f" Patch: {'ДА' if f.patch else 'НЕТ'} ({len(f.patch) if f.patch else 0} символов)")
if f.patch:
print(f" Первые 200 символов patch:")
print(f" {f.patch[:200]}...")
# Filter files that should be reviewed
reviewable_files = []
skipped_files = []
for f in files:
if should_review_file(f.filename):
reviewable_files.append({
"path": f.filename,
"status": f.status,
"additions": f.additions,
"deletions": f.deletions,
"patch": f.patch,
"language": detect_language(f.filename)
})
else:
skipped_files.append(f.filename)
print(f"\n✅ Файлов для ревью: {len(reviewable_files)}")
for rf in reviewable_files:
print(f" - {rf['path']} ({rf['language']})")
if skipped_files:
print(f"\n⏭️ Пропущено файлов: {len(skipped_files)}")
for sf in skipped_files:
print(f" - {sf}")
print("📥"*40 + "\n")
state["files"] = reviewable_files
state["status"] = "files_fetched"
# Update review
result = await self.db.execute(
select(Review).where(Review.id == state["review_id"])
)
review = result.scalar_one()
review.status = ReviewStatusEnum.ANALYZING
await self.db.commit()
return state
except Exception as e:
print(f"❌ ОШИБКА в fetch_files: {e}")
import traceback
traceback.print_exc()
state["error"] = str(e)
state["status"] = "failed"
return state
async def analyze_files(self, state: ReviewState) -> ReviewState:
"""Analyze files and generate comments"""
try:
all_comments = []
print("\n" + "🔬"*40)
print("НАЧАЛО АНАЛИЗА ФАЙЛОВ")
print("🔬"*40)
print(f"Файлов для анализа: {len(state['files'])}")
for i, file_info in enumerate(state["files"], 1):
file_path = file_info["path"]
patch = file_info.get("patch")
language = file_info.get("language", "text")
print(f"\n📂 Файл {i}/{len(state['files'])}: {file_path}")
print(f" Язык: {language}")
print(f" Размер patch: {len(patch) if patch else 0} символов")
print(f" Additions: {file_info.get('additions')}, Deletions: {file_info.get('deletions')}")
if not patch or len(patch) < 10:
print(f" ⚠️ ПРОПУСК: patch пустой или слишком маленький")
continue
# Analyze diff with PR context
pr_info = state.get("pr_info", {})
comments = await self.analyzer.analyze_diff(
file_path=file_path,
diff=patch,
language=language,
pr_title=pr_info.get("title", ""),
pr_description=pr_info.get("description", "")
)
print(f" 💬 Получено комментариев: {len(comments)}")
# Add file path to each comment
for comment in comments:
comment["file_path"] = file_path
all_comments.append(comment)
print(f"\n✅ ИТОГО комментариев: {len(all_comments)}")
print("🔬"*40 + "\n")
state["comments"] = all_comments
state["status"] = "analyzed"
# Update review
result = await self.db.execute(
select(Review).where(Review.id == state["review_id"])
)
review = result.scalar_one()
review.files_analyzed = len(state["files"])
review.status = ReviewStatusEnum.COMMENTING
await self.db.commit()
return state
except Exception as e:
print(f"❌ ОШИБКА в analyze_files: {e}")
import traceback
traceback.print_exc()
state["error"] = str(e)
state["status"] = "failed"
return state
async def post_comments(self, state: ReviewState) -> ReviewState:
"""Post comments to PR"""
try:
# Save comments to database
result = await self.db.execute(
select(Review).where(Review.id == state["review_id"])
)
review = result.scalar_one()
db_comments = []
for comment_data in state["comments"]:
# Фильтруем <think> блоки из сообщения
message = comment_data.get("message", "")
message = self._remove_think_blocks(message)
# Экранируем HTML теги (чтобы они не исчезали в Markdown)
message = self._escape_html_in_text(message)
comment = Comment(
review_id=review.id,
file_path=comment_data["file_path"],
line_number=comment_data.get("line", 1),
content=message,
severity=SeverityEnum(comment_data.get("severity", "INFO").lower()),
posted=False
)
self.db.add(comment)
db_comments.append({**comment_data, "message": message})
await self.db.commit()
# Post to Git platform
git_service = state["git_service"]
pr_info = state.get("pr_info", {})
# Generate summary
summary = await self.analyzer.generate_summary(
all_comments=db_comments,
pr_title=pr_info.get("title", ""),
pr_description=pr_info.get("description", "")
)
# Фильтруем <think> блоки из summary
summary = self._remove_think_blocks(summary)
# Экранируем HTML теги в summary
summary = self._escape_html_in_text(summary)
if db_comments:
# Format comments for API
formatted_comments = [
{
"file_path": c["file_path"],
"line_number": c.get("line", 1),
"content": f"**{c.get('severity', 'INFO').upper()}**: {c.get('message', '')}"
}
for c in db_comments
]
try:
# Determine review status based on severity
has_errors = any(c.get('severity', '').upper() == 'ERROR' for c in db_comments)
event = "REQUEST_CHANGES" if has_errors else "COMMENT"
await git_service.create_review(
pr_number=state["pr_number"],
comments=formatted_comments,
body=summary,
event=event
)
# Mark comments as posted
result = await self.db.execute(
select(Comment).where(Comment.review_id == review.id)
)
comments = result.scalars().all()
for comment in comments:
comment.posted = True
await self.db.commit()
except Exception as e:
print(f"Error posting comments to Git platform: {e}")
# Continue even if posting fails
else:
# No issues found - approve PR
try:
await git_service.create_review(
pr_number=state["pr_number"],
comments=[],
body=summary,
event="APPROVE" # Approve if no issues
)
except Exception as e:
print(f"Error posting approval: {e}")
review.comments_generated = len(db_comments)
await self.db.commit()
state["status"] = "commented"
return state
except Exception as e:
state["error"] = str(e)
state["status"] = "failed"
return state
async def complete_review(self, state: ReviewState) -> ReviewState:
"""Complete the review"""
try:
result = await self.db.execute(
select(Review).where(Review.id == state["review_id"])
)
review = result.scalar_one()
if state.get("error"):
review.status = ReviewStatusEnum.FAILED
review.error_message = state["error"]
else:
review.status = ReviewStatusEnum.COMPLETED
from datetime import datetime
review.completed_at = datetime.utcnow()
await self.db.commit()
state["status"] = "completed"
return state
except Exception as e:
state["error"] = str(e)
state["status"] = "failed"
return state
async def run_review(
self,
review_id: int,
pr_number: int,
repository_id: int
) -> Dict[str, Any]:
"""Run the review workflow"""
initial_state: ReviewState = {
"review_id": review_id,
"pr_number": pr_number,
"repository_id": repository_id,
"status": "pending",
"files": [],
"analyzed_files": [],
"comments": [],
"error": None,
"git_service": None
}
final_state = await self.graph.ainvoke(initial_state)
return final_state

299
backend/app/agents/tools.py Normal file
View File

@@ -0,0 +1,299 @@
"""Tools for the reviewer agent"""
import json
import re
from typing import List, Dict, Any, Optional
from langchain_ollama import OllamaLLM
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from app.agents.prompts import DIFF_REVIEW_PROMPT, CODE_REVIEW_PROMPT
class CodeAnalyzer:
"""Tool for analyzing code with Ollama"""
def __init__(self, ollama_base_url: str, model: str):
self.llm = OllamaLLM(
base_url=ollama_base_url,
model=model,
temperature=0.3, # Увеличили для более внимательного анализа
format="json" # Форсируем JSON формат
)
# Используем JsonOutputParser для гарантированного JSON
self.json_parser = JsonOutputParser()
def _extract_json_from_response(self, response: str) -> Dict[str, Any]:
"""Extract JSON from LLM response"""
# Remove markdown code blocks if present
response = response.strip()
if response.startswith('```'):
response = re.sub(r'^```(?:json)?\s*', '', response)
response = re.sub(r'\s*```$', '', response)
# Try to find JSON in the response
json_match = re.search(r'\{[\s\S]*\}', response)
if json_match:
try:
json_str = json_match.group()
print(f" 🔍 Найден JSON: {json_str[:200]}...")
return json.loads(json_str)
except json.JSONDecodeError as e:
print(f" ❌ Ошибка парсинга JSON: {e}")
print(f" 📄 JSON строка: {json_str[:500]}")
else:
print(f" ❌ JSON не найден в ответе!")
print(f" 📄 Ответ: {response[:500]}")
# If no valid JSON found, return empty comments
return {"comments": []}
async def generate_summary(
self,
all_comments: List[Dict[str, Any]],
pr_title: str = "",
pr_description: str = ""
) -> str:
"""Generate overall review summary in markdown"""
if not all_comments:
return """## 🤖 AI Code Review
✅ **Отличная работа!** Серьезных проблем не обнаружено.
Код выглядит хорошо и соответствует стандартам."""
# Группируем по severity
errors = [c for c in all_comments if c.get('severity', '').upper() == 'ERROR']
warnings = [c for c in all_comments if c.get('severity', '').upper() == 'WARNING']
infos = [c for c in all_comments if c.get('severity', '').upper() == 'INFO']
summary = f"""## 🤖 AI Code Review
### 📊 Статистика
- **Всего проблем:** {len(all_comments)}
"""
if errors:
summary += f"- ❌ **Критичных:** {len(errors)}\n"
if warnings:
summary += f"- ⚠️ **Важных:** {len(warnings)}\n"
if infos:
summary += f"- **Рекомендаций:** {len(infos)}\n"
summary += "\n### 💡 Рекомендации\n\n"
if errors:
summary += "⚠️ **Найдены критичные проблемы!** Пожалуйста, исправьте их перед мержем в main.\n\n"
elif warnings:
summary += "Найдены важные замечания. Рекомендуется исправить перед мержем.\n\n"
else:
summary += "Проблемы не критичны, но рекомендуется учесть.\n\n"
summary += "📝 **Детальные комментарии для каждой проблемы опубликованы ниже.**\n"
return summary
async def analyze_diff(
self,
file_path: str,
diff: str,
language: Optional[str] = None,
pr_title: str = "",
pr_description: str = ""
) -> List[Dict[str, Any]]:
"""Analyze code diff and return comments"""
if not diff or not diff.strip():
print(f"⚠️ Пустой diff для {file_path}")
return []
# Add PR context if available
pr_context = ""
if pr_title or pr_description:
pr_context = f"\n\n**КОНТЕКСТ PR:**\n"
if pr_title:
pr_context += f"Название: {pr_title}\n"
if pr_description:
pr_context += f"Описание: {pr_description}\n"
pr_context += "\nОБЯЗАТЕЛЬНО проверь: соответствует ли код описанию PR!\n"
# Получаем инструкции по формату JSON от парсера
format_instructions = self.json_parser.get_format_instructions()
prompt = DIFF_REVIEW_PROMPT.format(
file_path=file_path,
diff=diff,
pr_context=pr_context,
format_instructions=format_instructions
)
print("\n" + "="*80)
print(f"🔍 АНАЛИЗ ФАЙЛА: {file_path}")
print("="*80)
if pr_title or pr_description:
print(f"\n📋 КОНТЕКСТ PR:")
print("-" * 80)
if pr_title:
print(f"Название: {pr_title}")
if pr_description:
desc_short = pr_description[:200] + ("..." if len(pr_description) > 200 else "")
print(f"Описание: {desc_short}")
print("-" * 80)
print(f"\n📝 DIFF ({len(diff)} символов):")
print("-" * 80)
# Показываем первые 800 символов diff
print(diff[:800] + ("...\n[обрезано]" if len(diff) > 800 else ""))
print("-" * 80)
print(f"\n💭 ПРОМПТ ({len(prompt)} символов):")
print("-" * 80)
print(prompt[:500] + "...")
print("-" * 80)
try:
print(f"\n⏳ Отправка запроса к Ollama ({self.llm.model})...")
# Создаем chain с LLM и JSON парсером
chain = self.llm | self.json_parser
# Получаем результат
result = await chain.ainvoke(prompt)
print(f"\n🤖 ОТВЕТ AI (распарсен через JsonOutputParser):")
print("-" * 80)
print(json.dumps(result, ensure_ascii=False, indent=2)[:500] + "...")
print("-" * 80)
comments = result.get("comments", [])
if comments:
print(f"\n✅ Найдено комментариев: {len(comments)}")
for i, comment in enumerate(comments, 1):
print(f"\n {i}. Строка {comment.get('line', '?')}:")
print(f" Severity: {comment.get('severity', '?')}")
print(f" Message: {comment.get('message', '?')[:100]}...")
else:
print("\n⚠️ Комментариев не найдено! AI не нашел проблем.")
print("="*80 + "\n")
return comments
except Exception as e:
print(f"\n❌ ОШИБКА при анализе {file_path}: {e}")
print(f" Тип ошибки: {type(e).__name__}")
import traceback
traceback.print_exc()
# Fallback: попытка извлечь JSON вручную
print("\n🔄 Попытка fallback парсинга...")
try:
if hasattr(e, 'args') and len(e.args) > 0:
response_text = str(e.args[0])
result = self._extract_json_from_response(response_text)
return result.get("comments", [])
except:
pass
return []
async def analyze_code(
self,
file_path: str,
code: str,
language: str = "python",
patch_info: str = ""
) -> List[Dict[str, Any]]:
"""Analyze full code content and return comments"""
if not code or not code.strip():
return []
prompt = CODE_REVIEW_PROMPT.format(
file_path=file_path,
code=code,
language=language,
patch_info=patch_info
)
try:
response = await self.llm.ainvoke(prompt)
result = self._extract_json_from_response(response)
return result.get("comments", [])
except Exception as e:
print(f"Error analyzing code for {file_path}: {e}")
return []
def detect_language(file_path: str) -> str:
"""Detect programming language from file extension"""
extension_map = {
'.py': 'python',
'.js': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.jsx': 'javascript',
'.java': 'java',
'.go': 'go',
'.rs': 'rust',
'.cpp': 'cpp',
'.c': 'c',
'.cs': 'csharp',
'.php': 'php',
'.rb': 'ruby',
'.swift': 'swift',
'.kt': 'kotlin',
'.scala': 'scala',
'.sh': 'bash',
'.sql': 'sql',
'.html': 'html',
'.css': 'css',
'.scss': 'scss',
'.yaml': 'yaml',
'.yml': 'yaml',
'.json': 'json',
'.xml': 'xml',
'.md': 'markdown',
}
ext = '.' + file_path.split('.')[-1] if '.' in file_path else ''
return extension_map.get(ext.lower(), 'text')
def should_review_file(file_path: str) -> bool:
"""Determine if file should be reviewed"""
# Skip binary, generated, and config files
skip_extensions = {
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico',
'.pdf', '.zip', '.tar', '.gz',
'.lock', '.min.js', '.min.css',
'.pyc', '.pyo', '.class', '.o',
}
skip_patterns = [
'node_modules/',
'venv/',
'.git/',
'dist/',
'build/',
'__pycache__/',
'.next/',
'.nuxt/',
'package-lock.json',
'yarn.lock',
'poetry.lock',
]
# Check extension
ext = '.' + file_path.split('.')[-1] if '.' in file_path else ''
if ext.lower() in skip_extensions:
return False
# Check patterns
for pattern in skip_patterns:
if pattern in file_path:
return False
return True