Skip to content

Commit 57b13e1

Browse files
authored
Merge pull request #31 from naaa760/feat/repo-analysis-pr-creation
add : automated repo analysis and one-click PR creation for Watchflow rules
2 parents 0b55233 + 6d68ed5 commit 57b13e1

10 files changed

Lines changed: 702 additions & 655 deletions

File tree

docs/features.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ standards so teams can focus on building, increase trust, and move fast.
55

66
## Core Features
77

8+
### Repository Analysis → One-Click PR
9+
- Paste a repo URL, get diff-aware rule recommendations (structure, PR history, CONTRIBUTING).
10+
- Click “Proceed with PR” to auto-create `.watchflow/rules.yaml` on a branch with a ready-to-review PR body.
11+
- Supports GitHub App installations or user tokens; logs are structured and safe for ops visibility.
12+
813
### Context-Aware Rule Evaluation
914

1015
**Intelligent Context Analysis**

src/agents/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
from src.agents.engine_agent import RuleEngineAgent
1212
from src.agents.factory import get_agent
1313
from src.agents.feasibility_agent import RuleFeasibilityAgent
14+
from src.agents.repository_analysis_agent import RepositoryAnalysisAgent
1415

1516
__all__ = [
1617
"BaseAgent",
1718
"AgentResult",
1819
"RuleFeasibilityAgent",
1920
"RuleEngineAgent",
2021
"AcknowledgmentAgent",
22+
"RepositoryAnalysisAgent",
2123
"get_agent",
2224
]

src/agents/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from typing import Any, TypeVar
88

99
from src.core.utils.timeout import execute_with_timeout
10-
from src.integrations.providers import get_chat_model
1110

1211
logger = logging.getLogger(__name__)
1312

@@ -45,6 +44,9 @@ def __init__(self, max_retries: int = 3, retry_delay: float = 1.0, agent_name: s
4544
self.max_retries = max_retries
4645
self.retry_delay = retry_delay
4746
self.agent_name = agent_name
47+
# Lazy import to avoid circular imports and heavy initialization at module load.
48+
from src.integrations.providers import get_chat_model
49+
4850
self.llm = get_chat_model(agent=agent_name)
4951
self.graph = self._build_graph()
5052
logger.info(f"🔧 {self.__class__.__name__} initialized with max_retries={max_retries}, agent_name={agent_name}")
Lines changed: 33 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,180 +1,59 @@
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
46

5-
from langgraph.graph import END, START, StateGraph
7+
import time
68

79
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
1311
from src.agents.repository_analysis_agent.nodes import (
12+
_default_recommendations,
1413
analyze_contributing_guidelines,
1514
analyze_pr_history,
1615
analyze_repository_structure,
17-
generate_rule_recommendations,
1816
summarize_analysis,
1917
validate_recommendations,
2018
)
2119

22-
logger = logging.getLogger(__name__)
23-
2420

2521
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."""
4923

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
5727

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+
)
8235

8336
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)
8540

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)
12944

45+
latency_ms = int((time.perf_counter() - started_at) * 1000)
13046
return AgentResult(
131-
success=not has_errors,
132-
message=success_message,
47+
success=True,
48+
message="Repository analysis completed",
13349
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},
14051
)
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)
14654
return AgentResult(
14755
success=False,
148-
message=f"Repository analysis failed: {str(e)}",
56+
message=f"Repository analysis failed: {exc}",
14957
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},
18059
)

0 commit comments

Comments
 (0)