Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions src/agents/repository_analysis_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from __future__ import annotations

import time


from src.agents.base import AgentResult, BaseAgent
from src.agents.repository_analysis_agent.models import RepositoryAnalysisRequest, RepositoryAnalysisState
Expand All @@ -25,35 +25,21 @@ def _build_graph(self):
# Graph orchestration is handled procedurally in execute for clarity.
return None

async def execute(self, **kwargs) -> AgentResult:
started_at = time.perf_counter()
request = RepositoryAnalysisRequest(**kwargs)
state = RepositoryAnalysisState(
repository_full_name=request.repository_full_name,
installation_id=request.installation_id,
)

try:
await analyze_repository_structure(state)
await analyze_pr_history(state, request.max_prs)
await analyze_contributing_guidelines(state)

state.recommendations = _default_recommendations(state)
validate_recommendations(state)
response = summarize_analysis(state, request)

latency_ms = int((time.perf_counter() - started_at) * 1000)
return AgentResult(
success=True,
message="Repository analysis completed",
data={"analysis_response": response},
metadata={"execution_time_ms": latency_ms},

)
except Exception as exc: # noqa: BLE001
latency_ms = int((time.perf_counter() - started_at) * 1000)
return AgentResult(
success=False,
message=f"Repository analysis failed: {exc}",
data={},
metadata={"execution_time_ms": latency_ms},
)
128 changes: 3 additions & 125 deletions src/agents/repository_analysis_agent/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime
from typing import Any


from pydantic import BaseModel, Field, field_validator, model_validator

Expand Down Expand Up @@ -30,154 +29,33 @@ def parse_github_repo_identifier(value: str) -> str:
repo = parts[gh_idx + 2] if len(parts) > gh_idx + 2 else ""
return f"{owner}/{repo}".rstrip("/").removesuffix(".git")

return raw.rstrip("/").removesuffix(".git")


class PullRequestSample(BaseModel):
"""Minimal PR snapshot used for recommendations."""

number: int
title: str
state: str
merged: bool = False
additions: int | None = None
deletions: int | None = None
changed_files: int | None = None


class RuleRecommendation(BaseModel):
"""A recommended Watchflow rule with confidence and reasoning."""

yaml_rule: str = Field(description="Valid Watchflow rule YAML content")
confidence: float = Field(description="Confidence score (0.0-1.0)", ge=0.0, le=1.0)
reasoning: str = Field(description="Short explanation of why this rule is recommended")
strategy_used: str = Field(description="Strategy used (static, hybrid, llm)")


class RepositoryFeatures(BaseModel):
"""Features and characteristics discovered in the repository."""

has_contributing: bool = Field(default=False, description="Has CONTRIBUTING.md file")
has_codeowners: bool = Field(default=False, description="Has CODEOWNERS file")
has_workflows: bool = Field(default=False, description="Has GitHub Actions workflows")
workflow_count: int = Field(default=0, description="Number of workflow files")
language: str | None = Field(default=None, description="Primary programming language")
contributor_count: int = Field(default=0, description="Number of contributors")
pr_count: int = Field(default=0, description="Number of pull requests")



class ContributingGuidelinesAnalysis(BaseModel):
"""Analysis of contributing guidelines content."""

content: str | None = Field(default=None, description="Full CONTRIBUTING.md content")
has_pr_template: bool = Field(default=False, description="Requires PR templates")
has_issue_template: bool = Field(default=False, description="Requires issue templates")
requires_tests: bool = Field(default=False, description="Requires tests for contributions")
requires_docs: bool = Field(default=False, description="Requires documentation updates")
code_style_requirements: list[str] = Field(default_factory=list, description="Code style requirements mentioned")
review_requirements: list[str] = Field(default_factory=list, description="Code review requirements mentioned")


class PullRequestPlan(BaseModel):
"""Plan for creating a PR with generated rules."""

branch_name: str = "watchflow/rules"
base_branch: str = "main"
commit_message: str = "chore: add Watchflow rules"
pr_title: str = "Add Watchflow rules"
pr_body: str = "This PR adds Watchflow rule recommendations."
file_path: str = ".watchflow/rules.yaml"


class RepositoryAnalysisRequest(BaseModel):
"""Request model for repository analysis."""

repository_url: str | None = Field(default=None, description="GitHub repository URL")
repository_full_name: str | None = Field(default=None, description="Full repository name (owner/repo)")
installation_id: int | None = Field(default=None, description="GitHub App installation ID")
max_prs: int = Field(default=10, ge=0, le=50, description="Max PRs to sample for analysis")

@field_validator("repository_full_name", mode="before")
@classmethod
def normalize_full_name(cls, value: str | None, info) -> str:
if value:
return parse_github_repo_identifier(value)
raw_url = info.data.get("repository_url")
return parse_github_repo_identifier(raw_url or "")

@field_validator("repository_url", mode="before")
@classmethod
def strip_url(cls, value: str | None) -> str | None:
return value.strip() if isinstance(value, str) else value

@model_validator(mode="after")
def populate_full_name(self) -> "RepositoryAnalysisRequest":
if not self.repository_full_name and self.repository_url:
self.repository_full_name = parse_github_repo_identifier(self.repository_url)
return self


class RepositoryAnalysisState(BaseModel):
"""State for the repository analysis workflow."""

repository_full_name: str
installation_id: int | None
pr_samples: list[PullRequestSample] = Field(default_factory=list)
repository_features: RepositoryFeatures = Field(default_factory=RepositoryFeatures)
contributing_analysis: ContributingGuidelinesAnalysis = Field(default_factory=ContributingGuidelinesAnalysis)
recommendations: list[RuleRecommendation] = Field(default_factory=list)
rules_yaml: str | None = None
pr_plan: PullRequestPlan | None = None
analysis_summary: dict[str, Any] = Field(default_factory=dict)
errors: list[str] = Field(default_factory=list)



class RepositoryAnalysisResponse(BaseModel):
"""Response model containing rule recommendations and PR plan."""

repository_full_name: str = Field(description="Repository that was analyzed")
rules_yaml: str = Field(description="Combined Watchflow rules YAML")
recommendations: list[RuleRecommendation] = Field(default_factory=list, description="Rule recommendations")
pr_plan: PullRequestPlan | None = Field(default=None, description="Suggested PR plan")
analysis_summary: dict[str, Any] = Field(default_factory=dict, description="Summary of analysis findings")
analyzed_at: datetime = Field(default_factory=datetime.utcnow, description="Timestamp of analysis")


class ProceedWithPullRequestRequest(BaseModel):
"""Request to create a PR with generated rules."""

repository_url: str | None = Field(default=None, description="GitHub repository URL")
repository_full_name: str | None = Field(default=None, description="Full repository name (owner/repo)")
installation_id: int | None = Field(default=None, description="GitHub App installation ID")
user_token: str | None = Field(default=None, description="User token for GitHub operations (optional)")
rules_yaml: str = Field(description="Rules YAML content to commit")
branch_name: str = Field(default="watchflow/rules", description="Branch to create or update")
base_branch: str = Field(default="main", description="Base branch for the PR")
commit_message: str = Field(default="chore: add Watchflow rules", description="Commit message")
pr_title: str = Field(default="Add Watchflow rules", description="Pull request title")
pr_body: str = Field(default="This PR adds Watchflow rule recommendations.", description="Pull request body")
file_path: str = Field(default=".watchflow/rules.yaml", description="Path to rules file in repo")

@field_validator("repository_full_name", mode="before")
@classmethod
def normalize_full_name(cls, value: str | None, info) -> str:
if value:
return parse_github_repo_identifier(value)
raw_url = info.data.get("repository_url")
return parse_github_repo_identifier(raw_url or "")

@model_validator(mode="after")
def populate_full_name(self) -> "ProceedWithPullRequestRequest":
if not self.repository_full_name and self.repository_url:
self.repository_full_name = parse_github_repo_identifier(self.repository_url)
return self


class ProceedWithPullRequestResponse(BaseModel):
"""Response after creating the PR."""

pull_request_url: str
branch_name: str
base_branch: str
file_path: str
commit_sha: str | None = None
Loading
Loading