diff --git a/.env.sample b/.env.sample index 8880e1b..c3ebd37 100644 --- a/.env.sample +++ b/.env.sample @@ -92,6 +92,10 @@ DEV_EVAL_DEFAULT_INCLUDE=".py,.js,.java,.ts,.tsx,.jsx,.c,.cpp,.h,.hpp" # 默认排除的文件类型 DEV_EVAL_DEFAULT_EXCLUDE=".md,.txt,.json,.lock,.gitignore" -# ===== 其他可选配置 ===== -# 日志级别,可以是 DEBUG, INFO, WARNING, ERROR +# ===== Orchestration Configuration ===== +# Enable orchestrated code review with specialized agents +USE_ORCHESTRATION="false" + +# ===== Other Optional Configuration ===== +# Log level, can be DEBUG, INFO, WARNING, ERROR LOG_LEVEL="INFO" diff --git a/codedog/chains/__init__.py b/codedog/chains/__init__.py index 5ed9681..3166be6 100644 --- a/codedog/chains/__init__.py +++ b/codedog/chains/__init__.py @@ -1,5 +1,13 @@ from codedog.chains.code_review.base import CodeReviewChain +from codedog.chains.code_review.orchestrated import OrchestratedCodeReviewChain +from codedog.chains.code_review.factory import CodeReviewChainFactory from codedog.chains.pr_summary.base import PRSummaryChain from codedog.chains.pr_summary.translate_pr_summary_chain import TranslatePRSummaryChain -__all__ = ["PRSummaryChain", "CodeReviewChain", "TranslatePRSummaryChain"] +__all__ = [ + "PRSummaryChain", + "CodeReviewChain", + "OrchestratedCodeReviewChain", + "CodeReviewChainFactory", + "TranslatePRSummaryChain" +] diff --git a/codedog/chains/code_review/factory.py b/codedog/chains/code_review/factory.py new file mode 100644 index 0000000..7ef5f18 --- /dev/null +++ b/codedog/chains/code_review/factory.py @@ -0,0 +1,57 @@ +""" +Factory for creating code review chains. +""" + +from typing import Dict, Optional + +from langchain_core.language_models import BaseLanguageModel + +from codedog.chains.code_review.base import CodeReviewChain +from codedog.chains.code_review.orchestrated import OrchestratedCodeReviewChain + + +class CodeReviewChainFactory: + """ + Factory for creating code review chains. + + This factory supports creating both traditional and orchestrated + code review chains based on configuration. + """ + + @staticmethod + def create_chain( + llm: BaseLanguageModel, + use_orchestration: bool = False, + models: Optional[Dict[str, BaseLanguageModel]] = None, + **kwargs + ): + """ + Create a code review chain. + + Args: + llm: Language model for traditional chain or default model for orchestrated chain + use_orchestration: Whether to use orchestration + models: Dictionary of language models for different agents (for orchestration) + **kwargs: Additional arguments for chain creation + + Returns: + CodeReviewChain or OrchestratedCodeReviewChain instance + """ + if use_orchestration: + # Prepare models dictionary + if models is None: + models = {"default": llm} + elif "default" not in models: + models["default"] = llm + + # Create orchestrated chain + return OrchestratedCodeReviewChain.from_llms( + models=models, + **kwargs + ) + else: + # Create traditional chain + return CodeReviewChain.from_llm( + llm=llm, + **kwargs + ) diff --git a/codedog/chains/code_review/orchestrated.py b/codedog/chains/code_review/orchestrated.py new file mode 100644 index 0000000..adacc85 --- /dev/null +++ b/codedog/chains/code_review/orchestrated.py @@ -0,0 +1,123 @@ +""" +Orchestrated code review chain using specialized agents. +""" + +from __future__ import annotations + +import asyncio +from typing import Any, Dict, List, Optional + +from langchain_core.language_models import BaseLanguageModel +from langchain_core.callbacks.manager import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, +) +from langchain.chains.base import Chain +from pydantic import Field + +from codedog.models import ChangeFile, CodeReview, PullRequest +from codedog.processors import PullRequestProcessor +from codedog.orchestration.orchestrator import CodeReviewOrchestrator + + +class OrchestratedCodeReviewChain(Chain): + """ + Orchestrated code review chain using specialized agents. + + This chain uses the orchestration architecture to coordinate + multiple specialized agents for a comprehensive code review. + """ + + orchestrator: CodeReviewOrchestrator = Field(exclude=True) + """Orchestrator for code review.""" + + processor: PullRequestProcessor = Field( + exclude=True, default_factory=PullRequestProcessor.build + ) + """PR data processor.""" + + _input_keys: List[str] = ["pull_request"] + _output_keys: List[str] = ["code_reviews"] + + @property + def _chain_type(self) -> str: + return "orchestrated_code_review_chain" + + @property + def input_keys(self) -> List[str]: + """Will be whatever keys the prompt expects. + + :meta private: + """ + return self._input_keys + + @property + def output_keys(self) -> List[str]: + """Will always return text key. + + :meta private: + """ + return self._output_keys + + def _call( + self, + inputs: Dict[str, Any], + run_manager: Optional[CallbackManagerForChainRun] = None, + ) -> Dict[str, Any]: + """ + Synchronous call is not supported for orchestrated review. + Use async call instead. + """ + raise NotImplementedError( + "Orchestrated code review only supports async calls. Use acall instead." + ) + + async def _acall( + self, + inputs: Dict[str, Any], + run_manager: Optional[AsyncCallbackManagerForChainRun] = None, + ) -> Dict[str, Any]: + """ + Perform orchestrated code review asynchronously. + """ + _run_manager = run_manager or AsyncCallbackManagerForChainRun.get_noop_manager() + await _run_manager.on_text(inputs["pull_request"].json() + "\n") + + pr: PullRequest = inputs["pull_request"] + code_files: List[ChangeFile] = self.processor.get_diff_code_files(pr) + + # Use orchestrator to review files + code_reviews = await self.orchestrator.review_files(code_files) + + return {"code_reviews": code_reviews} + + @classmethod + def from_llms( + cls, + *, + models: Dict[str, BaseLanguageModel], + **kwargs, + ) -> OrchestratedCodeReviewChain: + """ + Create an orchestrated code review chain from language models. + + Args: + models: Dictionary of language models for different agents + Keys should include: 'coordinator', 'security', 'performance', + 'readability', 'architecture', 'documentation', 'default' + + Returns: + OrchestratedCodeReviewChain instance + """ + # Ensure we have a default model + if "default" not in models and len(models) > 0: + # Use the first model as default + models["default"] = next(iter(models.values())) + + # Create orchestrator + orchestrator = CodeReviewOrchestrator(models) + + return cls( + orchestrator=orchestrator, + processor=PullRequestProcessor(), + ) diff --git a/codedog/orchestration/__init__.py b/codedog/orchestration/__init__.py new file mode 100644 index 0000000..4845468 --- /dev/null +++ b/codedog/orchestration/__init__.py @@ -0,0 +1,26 @@ +""" +Orchestration module for CodeDog. + +This module contains the orchestration architecture for coordinating +specialized agents in the code review process. +""" + +from codedog.orchestration.coordinator import ReviewCoordinator +from codedog.orchestration.orchestrator import CodeReviewOrchestrator +from codedog.orchestration.agents import ( + SecurityReviewAgent, + PerformanceReviewAgent, + ReadabilityReviewAgent, + ArchitectureReviewAgent, + DocumentationReviewAgent +) + +__all__ = [ + "ReviewCoordinator", + "CodeReviewOrchestrator", + "SecurityReviewAgent", + "PerformanceReviewAgent", + "ReadabilityReviewAgent", + "ArchitectureReviewAgent", + "DocumentationReviewAgent" +] diff --git a/codedog/orchestration/agents.py b/codedog/orchestration/agents.py new file mode 100644 index 0000000..3400bc9 --- /dev/null +++ b/codedog/orchestration/agents.py @@ -0,0 +1,579 @@ +""" +Specialized agents for different aspects of code review. +""" + +import logging +from typing import Dict, Any, Optional + +from codedog.orchestration.base_agent import BaseReviewAgent + +logger = logging.getLogger(__name__) + +class SecurityReviewAgent(BaseReviewAgent): + """ + Specialized agent for security code review. + + This agent focuses on identifying security vulnerabilities, + potential exploits, and secure coding practices. + """ + + def __init__(self, model): + """Initialize the security review agent.""" + system_prompt = """You are a specialized security code review agent within an orchestrated code review system. +Your primary focus is identifying security vulnerabilities, potential exploits, and secure coding practices. + +Evaluate the code specifically for: +1. Input validation and sanitization +2. Authentication and authorization issues +3. Sensitive data exposure +4. Injection vulnerabilities (SQL, command, etc.) +5. Cross-site scripting (XSS) and cross-site request forgery (CSRF) +6. Insecure cryptographic implementations +7. Hardcoded credentials or secrets +8. Proper error handling that doesn't leak sensitive information +9. Race conditions and concurrency issues +10. Compliance with security standards (OWASP, etc.) + +For each issue found, provide: +1. A clear description of the vulnerability +2. The potential impact and risk level +3. Specific code location(s) where the issue exists +4. Concrete recommendations for remediation with code examples + +Your output should include: +1. A security score (1-10) for the code +2. Detailed analysis of security issues found +3. Prioritized recommendations for improvement +4. Estimated hours for an experienced developer to fix the issues + +Format your response with clear sections and markdown formatting for readability. +""" + super().__init__(model, system_prompt) + + async def review(self, code_diff: Dict[str, Any], context: Dict[str, Any], priority: bool = False) -> Dict[str, Any]: + """Perform a security-focused code review.""" + logger.info("Performing security review") + + # Extract information from code_diff + file_path = code_diff.get("file_path", "") + language = code_diff.get("language", "") + content = code_diff.get("content", "") + + # Create prompt for security review + prompt = f"""# Security Code Review Request + +## File Information +- **File Path**: {file_path} +- **Language**: {language} +- **Priority**: {"High" if priority else "Normal"} + +## Code to Review +```{language} +{content} +``` + +## Context +- **File Type**: {context.get('file_type', 'Unknown')} +- **Complexity**: {context.get('estimated_complexity', 'Unknown')} + +## Instructions +Please conduct a thorough security review of this code, focusing on: +1. Identifying any security vulnerabilities +2. Assessing the risk level of each issue +3. Providing specific recommendations for remediation +4. Estimating the work hours required to fix the issues + +## Response Format +Please structure your response with the following sections: + +### SUMMARY +Brief overview of security findings + +### VULNERABILITIES +Detailed analysis of each security issue found + +### RECOMMENDATIONS +Prioritized list of security improvements + +### SCORES: +- Readability: [score] /10 +- Efficiency: [score] /10 +- Security: [score] /10 +- Structure: [score] /10 +- Error Handling: [score] /10 +- Documentation: [score] /10 +- Code Style: [score] /10 +- Overall Score: [score] /10 + +### ESTIMATED HOURS: [hours] +""" + + # Generate review + review_text = await self._generate_review(prompt, "security_review") + + # Parse scores and estimated hours + scores = self._parse_scores(review_text) + estimated_hours = self._parse_estimated_hours(review_text) + + # Return review results + return { + "scores": scores, + "feedback": review_text, + "estimated_hours": estimated_hours + } + + +class PerformanceReviewAgent(BaseReviewAgent): + """ + Specialized agent for performance code review. + + This agent focuses on identifying performance issues, + optimization opportunities, and efficient coding practices. + """ + + def __init__(self, model): + """Initialize the performance review agent.""" + system_prompt = """You are a specialized performance code review agent within an orchestrated code review system. +Your primary focus is identifying performance issues, optimization opportunities, and efficient coding practices. + +Evaluate the code specifically for: +1. Algorithmic efficiency and complexity (O notation) +2. Resource utilization (CPU, memory, network, disk) +3. Caching opportunities and implementations +4. Database query optimization +5. Concurrency and parallelism +6. Memory leaks and garbage collection +7. Unnecessary computations or operations +8. Efficient data structures and algorithms +9. Performance bottlenecks +10. Scalability concerns + +For each issue found, provide: +1. A clear description of the performance issue +2. The potential impact on system performance +3. Specific code location(s) where the issue exists +4. Concrete recommendations for optimization with code examples + +Your output should include: +1. An efficiency score (1-10) for the code +2. Detailed analysis of performance issues found +3. Prioritized recommendations for improvement +4. Estimated hours for an experienced developer to implement optimizations + +Format your response with clear sections and markdown formatting for readability. +""" + super().__init__(model, system_prompt) + + async def review(self, code_diff: Dict[str, Any], context: Dict[str, Any], priority: bool = False) -> Dict[str, Any]: + """Perform a performance-focused code review.""" + logger.info("Performing performance review") + + # Extract information from code_diff + file_path = code_diff.get("file_path", "") + language = code_diff.get("language", "") + content = code_diff.get("content", "") + + # Create prompt for performance review + prompt = f"""# Performance Code Review Request + +## File Information +- **File Path**: {file_path} +- **Language**: {language} +- **Priority**: {"High" if priority else "Normal"} + +## Code to Review +```{language} +{content} +``` + +## Context +- **File Type**: {context.get('file_type', 'Unknown')} +- **Complexity**: {context.get('estimated_complexity', 'Unknown')} + +## Instructions +Please conduct a thorough performance review of this code, focusing on: +1. Identifying any performance issues or bottlenecks +2. Assessing the algorithmic complexity +3. Providing specific optimization recommendations +4. Estimating the work hours required to implement optimizations + +## Response Format +Please structure your response with the following sections: + +### SUMMARY +Brief overview of performance findings + +### PERFORMANCE ISSUES +Detailed analysis of each performance issue found + +### OPTIMIZATION RECOMMENDATIONS +Prioritized list of performance improvements + +### SCORES: +- Readability: [score] /10 +- Efficiency: [score] /10 +- Security: [score] /10 +- Structure: [score] /10 +- Error Handling: [score] /10 +- Documentation: [score] /10 +- Code Style: [score] /10 +- Overall Score: [score] /10 + +### ESTIMATED HOURS: [hours] +""" + + # Generate review + review_text = await self._generate_review(prompt, "performance_review") + + # Parse scores and estimated hours + scores = self._parse_scores(review_text) + estimated_hours = self._parse_estimated_hours(review_text) + + # Return review results + return { + "scores": scores, + "feedback": review_text, + "estimated_hours": estimated_hours + } + + +class ReadabilityReviewAgent(BaseReviewAgent): + """ + Specialized agent for readability code review. + + This agent focuses on code readability, naming conventions, + formatting, and overall code clarity. + """ + + def __init__(self, model): + """Initialize the readability review agent.""" + system_prompt = """You are a specialized readability code review agent within an orchestrated code review system. +Your primary focus is evaluating code readability, naming conventions, formatting, and overall code clarity. + +Evaluate the code specifically for: +1. Clear and descriptive variable, function, and class names +2. Consistent formatting and indentation +3. Appropriate use of whitespace +4. Code organization and logical flow +5. Adherence to language-specific style guides +6. Avoidance of overly complex expressions +7. Appropriate use of comments +8. Function and method length +9. Nesting depth +10. Overall code clarity and maintainability + +For each issue found, provide: +1. A clear description of the readability issue +2. The impact on code maintainability +3. Specific code location(s) where the issue exists +4. Concrete recommendations for improvement with code examples + +Your output should include: +1. A readability score (1-10) for the code +2. Detailed analysis of readability issues found +3. Prioritized recommendations for improvement +4. Estimated hours for an experienced developer to implement improvements + +Format your response with clear sections and markdown formatting for readability. +""" + super().__init__(model, system_prompt) + + async def review(self, code_diff: Dict[str, Any], context: Dict[str, Any], priority: bool = False) -> Dict[str, Any]: + """Perform a readability-focused code review.""" + logger.info("Performing readability review") + + # Extract information from code_diff + file_path = code_diff.get("file_path", "") + language = code_diff.get("language", "") + content = code_diff.get("content", "") + + # Create prompt for readability review + prompt = f"""# Readability Code Review Request + +## File Information +- **File Path**: {file_path} +- **Language**: {language} +- **Priority**: {"High" if priority else "Normal"} + +## Code to Review +```{language} +{content} +``` + +## Context +- **File Type**: {context.get('file_type', 'Unknown')} +- **Complexity**: {context.get('estimated_complexity', 'Unknown')} + +## Instructions +Please conduct a thorough readability review of this code, focusing on: +1. Identifying any readability issues +2. Assessing naming conventions and code clarity +3. Providing specific recommendations for improvement +4. Estimating the work hours required to implement improvements + +## Response Format +Please structure your response with the following sections: + +### SUMMARY +Brief overview of readability findings + +### READABILITY ISSUES +Detailed analysis of each readability issue found + +### IMPROVEMENT RECOMMENDATIONS +Prioritized list of readability improvements + +### SCORES: +- Readability: [score] /10 +- Efficiency: [score] /10 +- Security: [score] /10 +- Structure: [score] /10 +- Error Handling: [score] /10 +- Documentation: [score] /10 +- Code Style: [score] /10 +- Overall Score: [score] /10 + +### ESTIMATED HOURS: [hours] +""" + + # Generate review + review_text = await self._generate_review(prompt, "readability_review") + + # Parse scores and estimated hours + scores = self._parse_scores(review_text) + estimated_hours = self._parse_estimated_hours(review_text) + + # Return review results + return { + "scores": scores, + "feedback": review_text, + "estimated_hours": estimated_hours + } + + +class ArchitectureReviewAgent(BaseReviewAgent): + """ + Specialized agent for architecture code review. + + This agent focuses on code structure, design patterns, + architectural principles, and overall code organization. + """ + + def __init__(self, model): + """Initialize the architecture review agent.""" + system_prompt = """You are a specialized architecture code review agent within an orchestrated code review system. +Your primary focus is evaluating code structure, design patterns, architectural principles, and overall code organization. + +Evaluate the code specifically for: +1. Adherence to SOLID principles +2. Appropriate use of design patterns +3. Separation of concerns +4. Modularity and cohesion +5. Coupling between components +6. Dependency management +7. Error handling and exception flow +8. Testability +9. Extensibility and maintainability +10. Overall architectural consistency + +For each issue found, provide: +1. A clear description of the architectural issue +2. The impact on code maintainability and extensibility +3. Specific code location(s) where the issue exists +4. Concrete recommendations for improvement with code examples + +Your output should include: +1. A structure score (1-10) for the code +2. Detailed analysis of architectural issues found +3. Prioritized recommendations for improvement +4. Estimated hours for an experienced developer to implement improvements + +Format your response with clear sections and markdown formatting for readability. +""" + super().__init__(model, system_prompt) + + async def review(self, code_diff: Dict[str, Any], context: Dict[str, Any], priority: bool = False) -> Dict[str, Any]: + """Perform an architecture-focused code review.""" + logger.info("Performing architecture review") + + # Extract information from code_diff + file_path = code_diff.get("file_path", "") + language = code_diff.get("language", "") + content = code_diff.get("content", "") + + # Create prompt for architecture review + prompt = f"""# Architecture Code Review Request + +## File Information +- **File Path**: {file_path} +- **Language**: {language} +- **Priority**: {"High" if priority else "Normal"} + +## Code to Review +```{language} +{content} +``` + +## Context +- **File Type**: {context.get('file_type', 'Unknown')} +- **Complexity**: {context.get('estimated_complexity', 'Unknown')} + +## Instructions +Please conduct a thorough architecture review of this code, focusing on: +1. Identifying any architectural issues +2. Assessing design patterns and SOLID principles +3. Providing specific recommendations for improvement +4. Estimating the work hours required to implement improvements + +## Response Format +Please structure your response with the following sections: + +### SUMMARY +Brief overview of architectural findings + +### ARCHITECTURAL ISSUES +Detailed analysis of each architectural issue found + +### IMPROVEMENT RECOMMENDATIONS +Prioritized list of architectural improvements + +### SCORES: +- Readability: [score] /10 +- Efficiency: [score] /10 +- Security: [score] /10 +- Structure: [score] /10 +- Error Handling: [score] /10 +- Documentation: [score] /10 +- Code Style: [score] /10 +- Overall Score: [score] /10 + +### ESTIMATED HOURS: [hours] +""" + + # Generate review + review_text = await self._generate_review(prompt, "architecture_review") + + # Parse scores and estimated hours + scores = self._parse_scores(review_text) + estimated_hours = self._parse_estimated_hours(review_text) + + # Return review results + return { + "scores": scores, + "feedback": review_text, + "estimated_hours": estimated_hours + } + + +class DocumentationReviewAgent(BaseReviewAgent): + """ + Specialized agent for documentation code review. + + This agent focuses on code documentation, comments, + docstrings, and overall code documentation quality. + """ + + def __init__(self, model): + """Initialize the documentation review agent.""" + system_prompt = """You are a specialized documentation code review agent within an orchestrated code review system. +Your primary focus is evaluating code documentation, comments, docstrings, and overall documentation quality. + +Evaluate the code specifically for: +1. Presence and quality of docstrings +2. Function and method documentation +3. Class and module documentation +4. Inline comments for complex logic +5. API documentation +6. Examples and usage instructions +7. Parameter and return value documentation +8. Exception documentation +9. Consistency in documentation style +10. Overall documentation completeness + +For each issue found, provide: +1. A clear description of the documentation issue +2. The impact on code maintainability and usability +3. Specific code location(s) where the issue exists +4. Concrete recommendations for improvement with code examples + +Your output should include: +1. A documentation score (1-10) for the code +2. Detailed analysis of documentation issues found +3. Prioritized recommendations for improvement +4. Estimated hours for an experienced developer to implement improvements + +Format your response with clear sections and markdown formatting for readability. +""" + super().__init__(model, system_prompt) + + async def review(self, code_diff: Dict[str, Any], context: Dict[str, Any], priority: bool = False) -> Dict[str, Any]: + """Perform a documentation-focused code review.""" + logger.info("Performing documentation review") + + # Extract information from code_diff + file_path = code_diff.get("file_path", "") + language = code_diff.get("language", "") + content = code_diff.get("content", "") + + # Create prompt for documentation review + prompt = f"""# Documentation Code Review Request + +## File Information +- **File Path**: {file_path} +- **Language**: {language} +- **Priority**: {"High" if priority else "Normal"} + +## Code to Review +```{language} +{content} +``` + +## Context +- **File Type**: {context.get('file_type', 'Unknown')} +- **Complexity**: {context.get('estimated_complexity', 'Unknown')} + +## Instructions +Please conduct a thorough documentation review of this code, focusing on: +1. Identifying any documentation issues +2. Assessing docstrings and comments +3. Providing specific recommendations for improvement +4. Estimating the work hours required to implement improvements + +## Response Format +Please structure your response with the following sections: + +### SUMMARY +Brief overview of documentation findings + +### DOCUMENTATION ISSUES +Detailed analysis of each documentation issue found + +### IMPROVEMENT RECOMMENDATIONS +Prioritized list of documentation improvements + +### SCORES: +- Readability: [score] /10 +- Efficiency: [score] /10 +- Security: [score] /10 +- Structure: [score] /10 +- Error Handling: [score] /10 +- Documentation: [score] /10 +- Code Style: [score] /10 +- Overall Score: [score] /10 + +### ESTIMATED HOURS: [hours] +""" + + # Generate review + review_text = await self._generate_review(prompt, "documentation_review") + + # Parse scores and estimated hours + scores = self._parse_scores(review_text) + estimated_hours = self._parse_estimated_hours(review_text) + + # Return review results + return { + "scores": scores, + "feedback": review_text, + "estimated_hours": estimated_hours + } diff --git a/codedog/orchestration/base_agent.py b/codedog/orchestration/base_agent.py new file mode 100644 index 0000000..6b7d27e --- /dev/null +++ b/codedog/orchestration/base_agent.py @@ -0,0 +1,150 @@ +""" +Base agent class for specialized code review agents. +""" + +import logging +from typing import Dict, Any, Optional +from abc import ABC, abstractmethod + +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import SystemMessage, HumanMessage + +from codedog.utils.code_evaluator import log_llm_interaction + +logger = logging.getLogger(__name__) + +class BaseReviewAgent(ABC): + """ + Base class for specialized code review agents. + + This abstract class defines the interface for all specialized agents + and provides common functionality. + """ + + def __init__(self, model: BaseChatModel, system_prompt: str): + """ + Initialize the agent. + + Args: + model: Language model to use for reviews + system_prompt: System prompt for this specialized agent + """ + self.model = model + self.system_prompt = system_prompt + + @abstractmethod + async def review(self, code_diff: Dict[str, Any], context: Dict[str, Any], priority: bool = False) -> Dict[str, Any]: + """ + Review the code diff with this specialized agent. + + Args: + code_diff: The code diff to review + context: Context information from the coordinator + priority: Whether this agent should be prioritized + + Returns: + Dictionary containing review results + """ + pass + + async def _generate_review(self, prompt: str, interaction_type: str = "specialized_review") -> str: + """ + Generate a review using the language model. + + Args: + prompt: The prompt to send to the model + interaction_type: Type of interaction for logging + + Returns: + The generated review text + """ + messages = [ + SystemMessage(content=self.system_prompt), + HumanMessage(content=prompt) + ] + + # Log the prompt + log_llm_interaction(prompt, "", interaction_type=interaction_type) + + # Call the model + response = await self.model.agenerate(messages=[messages]) + generated_text = response.generations[0][0].text + + # Log the response + log_llm_interaction("", generated_text, interaction_type=interaction_type) + + return generated_text + + def _parse_scores(self, review_text: str) -> Dict[str, float]: + """ + Parse scores from the review text. + + Args: + review_text: The review text to parse + + Returns: + Dictionary of scores + """ + import re + + # Default scores + scores = { + "readability": 5.0, + "efficiency": 5.0, + "security": 5.0, + "structure": 5.0, + "error_handling": 5.0, + "documentation": 5.0, + "code_style": 5.0, + "overall_score": 5.0 + } + + # Try to extract scores using regex + try: + # Look for score section + score_section_match = re.search(r'#{1,3}\s*(?:SCORES|RATINGS):\s*([\s\S]*?)(?=#{1,3}|$)', review_text, re.IGNORECASE) + if score_section_match: + score_section = score_section_match.group(1) + + # Extract individual scores + for category in scores.keys(): + # Convert category to regex pattern (e.g., "error_handling" -> "Error\s*Handling") + category_pattern = category.replace('_', r'\s*').title() + score_match = re.search(rf'[-*]\s*{category_pattern}:\s*(\d+(?:\.\d+)?)\s*/\s*10', score_section, re.IGNORECASE) + if score_match: + scores[category] = float(score_match.group(1)) + + # Try to extract overall score if not found above + overall_match = re.search(r'[-*]\s*(?:Final\s+)?Overall(?:\s+Score)?:\s*(\d+(?:\.\d+)?)\s*/\s*10', score_section, re.IGNORECASE) + if overall_match: + scores["overall_score"] = float(overall_match.group(1)) + except Exception as e: + logger.warning(f"Error parsing scores: {e}") + + return scores + + def _parse_estimated_hours(self, review_text: str) -> float: + """ + Parse estimated hours from the review text. + + Args: + review_text: The review text to parse + + Returns: + Estimated hours as a float + """ + import re + + # Default estimate + estimated_hours = 0.0 + + # Try to extract estimated hours using regex + try: + # Look for estimated hours + hours_match = re.search(r'(?:estimated|approximate)\s+(?:work\s*)?hours?:?\s*(\d+(?:\.\d+)?)', review_text, re.IGNORECASE) + if hours_match: + estimated_hours = float(hours_match.group(1)) + except Exception as e: + logger.warning(f"Error parsing estimated hours: {e}") + + return estimated_hours diff --git a/codedog/orchestration/coordinator.py b/codedog/orchestration/coordinator.py new file mode 100644 index 0000000..22da047 --- /dev/null +++ b/codedog/orchestration/coordinator.py @@ -0,0 +1,326 @@ +""" +Coordinator module for orchestrating the code review process. +""" + +import logging +from typing import Dict, List, Any, Optional +import asyncio + +from codedog.models import ChangeFile, CodeReview +from codedog.utils.code_evaluator import log_llm_interaction + +logger = logging.getLogger(__name__) + +class ReviewCoordinator: + """ + Coordinates the code review process between specialized agents. + + This class is responsible for: + 1. Analyzing the context of code changes + 2. Distributing work to specialized agents + 3. Integrating results from different agents + 4. Generating the final report + """ + + def __init__(self, model=None): + """ + Initialize the coordinator. + + Args: + model: Optional language model for context analysis and integration + """ + self.model = model + + async def analyze_context(self, code_diff: Dict[str, Any]) -> Dict[str, Any]: + """ + Analyze the context of code changes to guide specialized reviews. + + Args: + code_diff: The code diff to analyze + + Returns: + Dict containing context information for specialized agents + """ + logger.info("Analyzing context for code review") + + # Extract basic information + file_path = code_diff.get("file_path", "") + language = code_diff.get("language", "") + content = code_diff.get("content", "") + + # Determine file type and primary concerns + file_type = self._determine_file_type(file_path, language) + primary_concerns = self._identify_primary_concerns(file_type, content) + + # Create context object + context = { + "file_path": file_path, + "language": language, + "file_type": file_type, + "primary_concerns": primary_concerns, + "estimated_complexity": self._estimate_complexity(content), + } + + logger.info(f"Context analysis complete: {context}") + return context + + async def distribute_work(self, code_diff: Dict[str, Any], context: Dict[str, Any], agents: Dict[str, Any]) -> Dict[str, asyncio.Task]: + """ + Distribute work to specialized agents based on context. + + Args: + code_diff: The code diff to review + context: The context information + agents: Dictionary of specialized agents + + Returns: + Dictionary of agent tasks + """ + logger.info("Distributing work to specialized agents") + + tasks = {} + for agent_name, agent in agents.items(): + # Determine if this agent should be prioritized based on context + priority = agent_name in context.get("primary_concerns", []) + + # Create task for this agent + tasks[agent_name] = asyncio.create_task( + agent.review(code_diff, context, priority=priority) + ) + + return tasks + + async def integrate_reviews(self, reviews: Dict[str, Any]) -> Dict[str, Any]: + """ + Integrate reviews from specialized agents into a cohesive result. + + Args: + reviews: Dictionary of reviews from specialized agents + + Returns: + Integrated review + """ + logger.info("Integrating specialized reviews") + + # Combine scores from different agents with appropriate weighting + scores = self._combine_scores(reviews) + + # Combine feedback from different agents + feedback = self._combine_feedback(reviews) + + # Create integrated review + integrated_review = { + "scores": scores, + "feedback": feedback, + "estimated_hours": self._calculate_estimated_hours(reviews), + } + + return integrated_review + + async def generate_report(self, integrated_review: Dict[str, Any], context: Dict[str, Any]) -> CodeReview: + """ + Generate the final code review report. + + Args: + integrated_review: The integrated review from specialized agents + context: The context information + + Returns: + CodeReview object + """ + logger.info("Generating final code review report") + + # Format the report + report = self._format_report(integrated_review, context) + + # Create CodeReview object + code_review = CodeReview( + file=ChangeFile( + full_name=context.get("file_path", ""), + language=context.get("language", ""), + ), + review=report + ) + + return code_review + + def _determine_file_type(self, file_path: str, language: str) -> str: + """Determine the type of file based on path and language.""" + if not file_path: + return "unknown" + + if language.lower() in ["python", "py"]: + if "test" in file_path.lower(): + return "python_test" + elif "model" in file_path.lower(): + return "python_model" + elif "view" in file_path.lower(): + return "python_view" + elif "controller" in file_path.lower() or "handler" in file_path.lower(): + return "python_controller" + else: + return "python_general" + + # Add similar logic for other languages + return language.lower() if language else "unknown" + + def _identify_primary_concerns(self, file_type: str, content: str) -> List[str]: + """Identify primary concerns based on file type and content.""" + concerns = [] + + # File type based concerns + if file_type.startswith("python_test"): + concerns.extend(["readability", "architecture"]) + elif file_type.startswith("python_model"): + concerns.extend(["performance", "security"]) + elif file_type.startswith("python_controller"): + concerns.extend(["security", "architecture"]) + + # Content based concerns + if "password" in content.lower() or "auth" in content.lower(): + concerns.append("security") + if "database" in content.lower() or "query" in content.lower(): + concerns.append("performance") + if "TODO" in content or "FIXME" in content: + concerns.append("documentation") + + # Ensure uniqueness + return list(set(concerns)) + + def _estimate_complexity(self, content: str) -> str: + """Estimate the complexity of the code.""" + # Simple heuristic based on length and structure + lines = content.split("\n") + line_count = len(lines) + + if line_count < 50: + return "low" + elif line_count < 200: + return "medium" + else: + return "high" + + def _combine_scores(self, reviews: Dict[str, Any]) -> Dict[str, float]: + """Combine scores from different specialized agents.""" + combined_scores = { + "readability": 0, + "efficiency": 0, + "security": 0, + "structure": 0, + "error_handling": 0, + "documentation": 0, + "code_style": 0, + "overall_score": 0 + } + + # Define weights for different agent types + weights = { + "security": {"security": 2.0, "overall_score": 1.2}, + "performance": {"efficiency": 2.0, "overall_score": 1.2}, + "readability": {"readability": 2.0, "code_style": 1.5, "overall_score": 1.2}, + "architecture": {"structure": 2.0, "error_handling": 1.5, "overall_score": 1.2}, + "documentation": {"documentation": 2.0, "overall_score": 1.0} + } + + # Track weight totals for normalization + weight_totals = {key: 0 for key in combined_scores} + + # Combine scores with weighting + for agent_type, review in reviews.items(): + if not review or "scores" not in review: + continue + + for score_type, score in review["scores"].items(): + if score_type in combined_scores: + # Get weight for this agent and score type + weight = weights.get(agent_type, {}).get(score_type, 1.0) + + # Add weighted score + combined_scores[score_type] += score * weight + weight_totals[score_type] += weight + + # Normalize scores + for score_type in combined_scores: + if weight_totals[score_type] > 0: + combined_scores[score_type] /= weight_totals[score_type] + # Round to one decimal place + combined_scores[score_type] = round(combined_scores[score_type], 1) + else: + combined_scores[score_type] = 5.0 # Default score + + return combined_scores + + def _combine_feedback(self, reviews: Dict[str, Any]) -> str: + """Combine feedback from different specialized agents.""" + combined_feedback = [] + + # Add feedback from each agent with appropriate headers + for agent_type, review in reviews.items(): + if not review or "feedback" not in review: + continue + + # Add header for this agent's feedback + header = f"## {agent_type.capitalize()} Review" + combined_feedback.append(header) + + # Add the feedback + combined_feedback.append(review["feedback"]) + + # Add separator + combined_feedback.append("") + + return "\n".join(combined_feedback) + + def _calculate_estimated_hours(self, reviews: Dict[str, Any]) -> float: + """Calculate estimated hours from specialized agent reviews.""" + # Get estimates from each agent + estimates = [] + for review in reviews.values(): + if review and "estimated_hours" in review: + estimates.append(review["estimated_hours"]) + + # Calculate final estimate + if not estimates: + return 0.0 + + # Use the maximum estimate as the base + base_estimate = max(estimates) + + # Add 10% for each additional estimate to account for integration work + additional = 0.1 * base_estimate * (len(estimates) - 1) + + return round(base_estimate + additional, 1) + + def _format_report(self, integrated_review: Dict[str, Any], context: Dict[str, Any]) -> str: + """Format the final code review report.""" + scores = integrated_review.get("scores", {}) + feedback = integrated_review.get("feedback", "") + estimated_hours = integrated_review.get("estimated_hours", 0.0) + + # Create report header + report = [ + f"# Code Review for {context.get('file_path', 'Unknown File')}", + "", + f"**Language:** {context.get('language', 'Unknown')}", + f"**Complexity:** {context.get('estimated_complexity', 'Unknown')}", + f"**Estimated Hours:** {estimated_hours}", + "", + "## Scores", + "", + "| Category | Score |", + "|----------|-------|", + ] + + # Add scores + for category, score in scores.items(): + # Convert category name to title case with spaces + category_name = category.replace("_", " ").title() + report.append(f"| {category_name} | {score:.1f} / 10 |") + + # Add feedback + report.append("") + report.append("## Detailed Feedback") + report.append("") + report.append(feedback) + + return "\n".join(report) diff --git a/codedog/orchestration/orchestrator.py b/codedog/orchestration/orchestrator.py new file mode 100644 index 0000000..9fa793f --- /dev/null +++ b/codedog/orchestration/orchestrator.py @@ -0,0 +1,127 @@ +""" +Orchestrator for code review process. +""" + +import logging +import asyncio +from typing import Dict, List, Any, Optional + +from langchain_core.language_models.chat_models import BaseChatModel + +from codedog.models import ChangeFile, CodeReview +from codedog.orchestration.coordinator import ReviewCoordinator +from codedog.orchestration.agents import ( + SecurityReviewAgent, + PerformanceReviewAgent, + ReadabilityReviewAgent, + ArchitectureReviewAgent, + DocumentationReviewAgent +) + +logger = logging.getLogger(__name__) + +class CodeReviewOrchestrator: + """ + Orchestrates the code review process using specialized agents. + + This class coordinates the work of multiple specialized agents + to produce a comprehensive code review. + """ + + def __init__(self, models: Dict[str, BaseChatModel]): + """ + Initialize the orchestrator with language models. + + Args: + models: Dictionary of language models for different agents + Keys should include: 'coordinator', 'security', 'performance', + 'readability', 'architecture', 'documentation' + """ + # Initialize coordinator + self.coordinator = ReviewCoordinator(model=models.get('coordinator')) + + # Initialize specialized agents + self.agents = { + 'security': SecurityReviewAgent(models.get('security', models.get('default'))), + 'performance': PerformanceReviewAgent(models.get('performance', models.get('default'))), + 'readability': ReadabilityReviewAgent(models.get('readability', models.get('default'))), + 'architecture': ArchitectureReviewAgent(models.get('architecture', models.get('default'))), + 'documentation': DocumentationReviewAgent(models.get('documentation', models.get('default'))) + } + + async def review_file(self, change_file: ChangeFile) -> CodeReview: + """ + Review a single file using the orchestrated process. + + Args: + change_file: The file to review + + Returns: + CodeReview object with the review results + """ + logger.info(f"Starting orchestrated review for {change_file.full_name}") + + # Prepare code diff for review + code_diff = { + "file_path": change_file.full_name, + "language": change_file.language, + "content": change_file.content + } + + # Step 1: Analyze context + context = await self.coordinator.analyze_context(code_diff) + + # Step 2: Distribute work to specialized agents + agent_tasks = await self.coordinator.distribute_work(code_diff, context, self.agents) + + # Step 3: Wait for all agent tasks to complete + agent_results = {} + for agent_name, task in agent_tasks.items(): + try: + agent_results[agent_name] = await task + except Exception as e: + logger.error(f"Error in {agent_name} agent: {e}") + agent_results[agent_name] = None + + # Step 4: Integrate results + integrated_review = await self.coordinator.integrate_reviews(agent_results) + + # Step 5: Generate final report + code_review = await self.coordinator.generate_report(integrated_review, context) + + logger.info(f"Completed orchestrated review for {change_file.full_name}") + return code_review + + async def review_files(self, change_files: List[ChangeFile]) -> List[CodeReview]: + """ + Review multiple files using the orchestrated process. + + Args: + change_files: List of files to review + + Returns: + List of CodeReview objects with the review results + """ + logger.info(f"Starting orchestrated review for {len(change_files)} files") + + # Create tasks for each file + tasks = [self.review_file(file) for file in change_files] + + # Wait for all tasks to complete + reviews = await asyncio.gather(*tasks, return_exceptions=True) + + # Handle any exceptions + code_reviews = [] + for i, review in enumerate(reviews): + if isinstance(review, Exception): + logger.error(f"Error reviewing {change_files[i].full_name}: {review}") + # Create an error review + code_reviews.append(CodeReview( + file=change_files[i], + review=f"Error during review: {str(review)}" + )) + else: + code_reviews.append(review) + + logger.info(f"Completed orchestrated review for {len(change_files)} files") + return code_reviews diff --git a/example.diff b/example.diff new file mode 100644 index 0000000..e5fc467 --- /dev/null +++ b/example.diff @@ -0,0 +1,51 @@ +diff --git a/example.py b/example.py +index 1234567..abcdefg 100644 +--- a/example.py ++++ b/example.py +@@ -1,10 +1,15 @@ + import os + import sys ++import json ++from typing import Dict, Any, Optional + +-def hello_world(): +- print("Hello, World!") ++def hello_world(name: Optional[str] = None) -> str: ++ """ ++ Returns a greeting message. ++ ++ Args: ++ name: Optional name to greet. If None, uses "World". ++ """ ++ greeting = f"Hello, {name or 'World'}!" ++ return greeting + +-if __name__ == "__main__": +- hello_world() +- print("Goodbye!") ++def save_greeting(name: Optional[str] = None, file_path: str = "greeting.json") -> Dict[str, Any]: ++ """ ++ Saves a greeting to a JSON file. ++ ++ Args: ++ name: Optional name to greet ++ file_path: Path to save the greeting ++ ++ Returns: ++ Dictionary containing the greeting data ++ """ ++ greeting = hello_world(name) ++ data = { ++ "greeting": greeting, ++ "timestamp": os.path.getmtime(file_path) if os.path.exists(file_path) else None ++ } ++ ++ with open(file_path, "w") as f: ++ json.dump(data, f) ++ ++ return data ++ ++if __name__ == "__main__": ++ name = sys.argv[1] if len(sys.argv) > 1 else None ++ result = save_greeting(name) ++ print(result["greeting"]) diff --git a/run_codedog.py b/run_codedog.py index e386fc3..a7a2ff9 100755 --- a/run_codedog.py +++ b/run_codedog.py @@ -21,7 +21,7 @@ from langchain_community.callbacks.manager import get_openai_callback from codedog.actors.reporters.pull_request import PullRequestReporter -from codedog.chains import CodeReviewChain, PRSummaryChain +from codedog.chains import CodeReviewChain, PRSummaryChain, CodeReviewChainFactory from codedog.retrievers import GithubRetriever, GitlabRetriever from codedog.utils.langchain_utils import load_model_by_name from codedog.utils.email_utils import send_report_email @@ -464,7 +464,7 @@ def get_all_remote_commits( end_iso = f"{end_date}T23:59:59Z" # Get all commits in the repository within the date range - all_commits = project.commits.list(all=True, since=start_iso, until=end_iso) + all_commits = project.commits.list(all=True, get_all=True, since=start_iso, until=end_iso) logger.info(f"Found {len(all_commits)} commits in the date range") # Group commits by author @@ -493,7 +493,7 @@ def get_all_remote_commits( commit_detail = project.commits.get(commit.id) # Get commit diff - diff = commit_detail.diff() + diff = commit_detail.diff(get_all=True) # Filter files by extension filtered_diff = [] @@ -703,7 +703,7 @@ def get_remote_commits( end_iso = f"{end_date}T23:59:59Z" # Get all commits in the repository within the date range - all_commits = project.commits.list(all=True, since=start_iso, until=end_iso) + all_commits = project.commits.list(all=True, get_all=True, since=start_iso, until=end_iso) # Filter by author for commit in all_commits: @@ -714,7 +714,7 @@ def get_remote_commits( commit_detail = project.commits.get(commit.id) # Get commit diff - diff = commit_detail.diff() + diff = commit_detail.diff(get_all=True) # Filter files by extension filtered_diff = [] @@ -1336,11 +1336,30 @@ def generate_full_report(repository_name, pull_request_number, email_addresses=N verbose=True ) - review_chain = CodeReviewChain.from_llm( + # Check if orchestration is enabled + use_orchestration = os.environ.get("USE_ORCHESTRATION", "false").lower() == "true" + + # Create code review chain using factory + review_chain = CodeReviewChainFactory.create_chain( llm=load_model_by_name(code_review_model), + use_orchestration=use_orchestration, + models={ + "default": load_model_by_name(code_review_model), + "coordinator": load_model_by_name(code_review_model), + "security": load_model_by_name(code_review_model), + "performance": load_model_by_name(code_review_model), + "readability": load_model_by_name(code_review_model), + "architecture": load_model_by_name(code_review_model), + "documentation": load_model_by_name(code_review_model) + } if use_orchestration else None, verbose=True ) + if use_orchestration: + print("Using orchestrated code review with specialized agents") + else: + print("Using standard code review") + with get_openai_callback() as cb: # Get PR summary print(f"Generating PR summary using {pr_summary_model}...")