|
1 | | -import logging |
2 | | -import time |
3 | | -from datetime import datetime |
| 1 | +""" |
| 2 | +RepositoryAnalysisAgent orchestrates repository signal gathering and rule generation. |
| 3 | +""" |
| 4 | + |
| 5 | +from __future__ import annotations |
4 | 6 |
|
5 | | -from langgraph.graph import END, START, StateGraph |
| 7 | +import time |
6 | 8 |
|
7 | 9 | from src.agents.base import AgentResult, BaseAgent |
8 | | -from src.agents.repository_analysis_agent.models import ( |
9 | | - RepositoryAnalysisRequest, |
10 | | - RepositoryAnalysisResponse, |
11 | | - RepositoryAnalysisState, |
12 | | -) |
| 10 | +from src.agents.repository_analysis_agent.models import RepositoryAnalysisRequest, RepositoryAnalysisState |
13 | 11 | from src.agents.repository_analysis_agent.nodes import ( |
| 12 | + _default_recommendations, |
14 | 13 | analyze_contributing_guidelines, |
15 | 14 | analyze_pr_history, |
16 | 15 | analyze_repository_structure, |
17 | | - generate_rule_recommendations, |
18 | 16 | summarize_analysis, |
19 | 17 | validate_recommendations, |
20 | 18 | ) |
21 | 19 |
|
22 | | -logger = logging.getLogger(__name__) |
23 | | - |
24 | 20 |
|
25 | 21 | class RepositoryAnalysisAgent(BaseAgent): |
26 | | - """ |
27 | | - Agent that analyzes GitHub repositories to generate Watchflow rule recommendations. |
28 | | -
|
29 | | - This agent performs multi-step analysis: |
30 | | - 1. Analyzes repository structure and features |
31 | | - 2. Parses contributing guidelines for patterns |
32 | | - 3. Reviews commit/PR patterns |
33 | | - 4. Generates rule recommendations with confidence scores |
34 | | - 5. Validates recommendations are valid YAML |
35 | | -
|
36 | | - Returns structured recommendations that can be directly used as Watchflow rules. |
37 | | - """ |
38 | | - |
39 | | - def __init__(self, max_retries: int = 3, timeout: float = 120.0): |
40 | | - super().__init__(max_retries=max_retries, agent_name="repository_analysis_agent") |
41 | | - self.timeout = timeout |
42 | | - |
43 | | - logger.info("Repository Analysis Agent initialized") |
44 | | - logger.info(f"Max retries: {max_retries}, Timeout: {timeout}s") |
45 | | - |
46 | | - def _build_graph(self) -> StateGraph: |
47 | | - """Build the LangGraph workflow for repository analysis.""" |
48 | | - workflow = StateGraph(RepositoryAnalysisState) |
| 22 | + """Agent that inspects a repository and proposes Watchflow rules.""" |
49 | 23 |
|
50 | | - # Add nodes |
51 | | - workflow.add_node("analyze_repository_structure", analyze_repository_structure) |
52 | | - workflow.add_node("analyze_pr_history", analyze_pr_history) |
53 | | - workflow.add_node("analyze_contributing_guidelines", analyze_contributing_guidelines) |
54 | | - workflow.add_node("generate_rule_recommendations", generate_rule_recommendations) |
55 | | - workflow.add_node("validate_recommendations", validate_recommendations) |
56 | | - workflow.add_node("summarize_analysis", summarize_analysis) |
| 24 | + def _build_graph(self): |
| 25 | + # Graph orchestration is handled procedurally in execute for clarity. |
| 26 | + return None |
57 | 27 |
|
58 | | - # Define workflow edges |
59 | | - workflow.add_edge(START, "analyze_repository_structure") |
60 | | - workflow.add_edge("analyze_repository_structure", "analyze_pr_history") |
61 | | - workflow.add_edge("analyze_pr_history", "analyze_contributing_guidelines") |
62 | | - workflow.add_edge("analyze_contributing_guidelines", "generate_rule_recommendations") |
63 | | - workflow.add_edge("generate_rule_recommendations", "validate_recommendations") |
64 | | - workflow.add_edge("validate_recommendations", "summarize_analysis") |
65 | | - workflow.add_edge("summarize_analysis", END) |
66 | | - |
67 | | - return workflow.compile() |
68 | | - |
69 | | - async def execute(self, repository_full_name: str, installation_id: int | None = None, **kwargs) -> AgentResult: |
70 | | - """ |
71 | | - Analyze a repository and generate rule recommendations. |
72 | | -
|
73 | | - Args: |
74 | | - repository_full_name: Full repository name (owner/repo) |
75 | | - installation_id: Optional GitHub App installation ID for private repos |
76 | | - **kwargs: Additional parameters |
77 | | -
|
78 | | - Returns: |
79 | | - AgentResult containing analysis results and recommendations |
80 | | - """ |
81 | | - start_time = time.time() |
| 28 | + async def execute(self, **kwargs) -> AgentResult: |
| 29 | + started_at = time.perf_counter() |
| 30 | + request = RepositoryAnalysisRequest(**kwargs) |
| 31 | + state = RepositoryAnalysisState( |
| 32 | + repository_full_name=request.repository_full_name, |
| 33 | + installation_id=request.installation_id, |
| 34 | + ) |
82 | 35 |
|
83 | 36 | try: |
84 | | - logger.info(f"Starting repository analysis for {repository_full_name}") |
| 37 | + await analyze_repository_structure(state) |
| 38 | + await analyze_pr_history(state, request.max_prs) |
| 39 | + await analyze_contributing_guidelines(state) |
85 | 40 |
|
86 | | - # Validate input |
87 | | - if not repository_full_name or "/" not in repository_full_name: |
88 | | - return AgentResult( |
89 | | - success=False, |
90 | | - message="Invalid repository name format. Expected 'owner/repo'", |
91 | | - data={}, |
92 | | - metadata={"execution_time_ms": 0}, |
93 | | - ) |
94 | | - |
95 | | - initial_state = RepositoryAnalysisState( |
96 | | - repository_full_name=repository_full_name, |
97 | | - installation_id=installation_id, |
98 | | - analysis_steps=[], |
99 | | - errors=[], |
100 | | - ) |
101 | | - |
102 | | - logger.info("Initial state prepared, starting analysis workflow") |
103 | | - |
104 | | - result = await self._execute_with_timeout(self.graph.ainvoke(initial_state), timeout=self.timeout) |
105 | | - |
106 | | - execution_time = time.time() - start_time |
107 | | - logger.info(f"Analysis completed in {execution_time:.2f}s") |
108 | | - |
109 | | - if isinstance(result, dict): |
110 | | - state = RepositoryAnalysisState(**result) |
111 | | - else: |
112 | | - state = result |
113 | | - |
114 | | - response = RepositoryAnalysisResponse( |
115 | | - repository_full_name=repository_full_name, |
116 | | - recommendations=state.recommendations, |
117 | | - analysis_summary=state.analysis_summary, |
118 | | - analyzed_at=datetime.now().isoformat(), |
119 | | - total_recommendations=len(state.recommendations), |
120 | | - ) |
121 | | - |
122 | | - # Check for errors |
123 | | - has_errors = len(state.errors) > 0 |
124 | | - success_message = f"Analysis completed successfully with {len(state.recommendations)} recommendations" |
125 | | - if has_errors: |
126 | | - success_message += f" ({len(state.errors)} errors encountered)" |
127 | | - |
128 | | - logger.info(f"Analysis result: {len(state.recommendations)} recommendations, {len(state.errors)} errors") |
| 41 | + state.recommendations = _default_recommendations(state) |
| 42 | + validate_recommendations(state) |
| 43 | + response = summarize_analysis(state, request) |
129 | 44 |
|
| 45 | + latency_ms = int((time.perf_counter() - started_at) * 1000) |
130 | 46 | return AgentResult( |
131 | | - success=not has_errors, |
132 | | - message=success_message, |
| 47 | + success=True, |
| 48 | + message="Repository analysis completed", |
133 | 49 | data={"analysis_response": response}, |
134 | | - metadata={ |
135 | | - "execution_time_ms": execution_time * 1000, |
136 | | - "recommendations_count": len(state.recommendations), |
137 | | - "errors_count": len(state.errors), |
138 | | - "analysis_steps": state.analysis_steps, |
139 | | - }, |
| 50 | + metadata={"execution_time_ms": latency_ms}, |
140 | 51 | ) |
141 | | - |
142 | | - except Exception as e: |
143 | | - execution_time = time.time() - start_time |
144 | | - logger.error(f"Error in repository analysis: {e}") |
145 | | - |
| 52 | + except Exception as exc: # noqa: BLE001 |
| 53 | + latency_ms = int((time.perf_counter() - started_at) * 1000) |
146 | 54 | return AgentResult( |
147 | 55 | success=False, |
148 | | - message=f"Repository analysis failed: {str(e)}", |
| 56 | + message=f"Repository analysis failed: {exc}", |
149 | 57 | data={}, |
150 | | - metadata={ |
151 | | - "execution_time_ms": execution_time * 1000, |
152 | | - "error_type": type(e).__name__, |
153 | | - }, |
154 | | - ) |
155 | | - |
156 | | - async def analyze_repository(self, request: RepositoryAnalysisRequest) -> RepositoryAnalysisResponse: |
157 | | - """ |
158 | | - Convenience method for analyzing a repository using the request model. |
159 | | -
|
160 | | - Args: |
161 | | - request: Repository analysis request |
162 | | -
|
163 | | - Returns: |
164 | | - Repository analysis response |
165 | | - """ |
166 | | - result = await self.execute( |
167 | | - repository_full_name=request.repository_full_name, |
168 | | - installation_id=request.installation_id, |
169 | | - ) |
170 | | - |
171 | | - if result.success and "analysis_response" in result.data: |
172 | | - return result.data["analysis_response"] |
173 | | - else: |
174 | | - return RepositoryAnalysisResponse( |
175 | | - repository_full_name=request.repository_full_name, |
176 | | - recommendations=[], |
177 | | - analysis_summary={"error": result.message}, |
178 | | - analyzed_at=datetime.now().isoformat(), |
179 | | - total_recommendations=0, |
| 58 | + metadata={"execution_time_ms": latency_ms}, |
180 | 59 | ) |
0 commit comments