Skip to content

Commit 45a691a

Browse files
authored
Merge pull request #20 from jenish-jain/refactoring
Refactoring
2 parents 8f7a490 + 174ec80 commit 45a691a

30 files changed

Lines changed: 4309 additions & 407 deletions

cmd/agent/commands/buildindex.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package commands
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"intern/internal/indexer"
8+
9+
logger "github.com/jenish-jain/logger"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
// BuildIndexCmd represents the build-index command
14+
var BuildIndexCmd = &cobra.Command{
15+
Use: "build-index",
16+
Short: "Build file index for smart context selection",
17+
Long: `Build or update the file index for intelligent context selection during code analysis.`,
18+
RunE: buildIndex,
19+
}
20+
21+
func buildIndex(cmd *cobra.Command, args []string) error {
22+
logger.Info("Building file index for smart context selection...")
23+
24+
// Load config to get repository path
25+
_, repoPaths, err := InitPartialDependencies()
26+
if err != nil {
27+
logger.Error("Failed to initialize dependencies: %v", err)
28+
return err
29+
}
30+
31+
repoRoot := repoPaths.Root()
32+
33+
// Check if repository exists
34+
if _, err := os.Stat(repoRoot); os.IsNotExist(err) {
35+
logger.Error("Repository not found", "path", repoRoot)
36+
logger.Info("Make sure to clone the repository first or set WORKING_DIR correctly in your config")
37+
return err
38+
}
39+
40+
logger.Info("Indexing repository", "path", repoRoot)
41+
42+
// Build or update index incrementally
43+
idx := indexer.New(repoRoot)
44+
fileIndex, wasUpdated, err := idx.RebuildIfStale()
45+
if err != nil {
46+
logger.Error("Failed to build index", "error", err)
47+
return err
48+
}
49+
50+
if !wasUpdated {
51+
logger.Info("Index is already up to date")
52+
indexPath := filepath.Join(repoRoot, indexer.IndexDirName, indexer.IndexFileName)
53+
logger.Info("Using existing index", "path", indexPath)
54+
return nil
55+
}
56+
57+
logger.Info("Index built successfully", "files", len(fileIndex.Files), "modules", len(fileIndex.Modules))
58+
59+
// Save index
60+
if err := idx.SaveIndex(fileIndex); err != nil {
61+
logger.Error("Failed to save index", "error", err)
62+
return err
63+
}
64+
65+
indexPath := filepath.Join(repoRoot, indexer.IndexDirName, indexer.IndexFileName)
66+
logger.Info("Index saved successfully", "path", indexPath)
67+
68+
// Show some statistics
69+
categoryCounts := make(map[string]int)
70+
for _, meta := range fileIndex.Files {
71+
categoryCounts[meta.Category]++
72+
}
73+
74+
logger.Info("Index statistics:")
75+
for category, count := range categoryCounts {
76+
logger.Info(" - "+category, "count", count)
77+
}
78+
79+
return nil
80+
}

cmd/agent/commands/helpers.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"os"
6+
7+
"intern/internal/config"
8+
"intern/internal/errors"
9+
"intern/internal/orchestrator"
10+
"intern/internal/provider"
11+
"intern/internal/repository"
12+
"intern/internal/repository/github"
13+
"intern/internal/ticketing"
14+
jiraraw "intern/internal/ticketing/jira-raw"
15+
16+
logger "github.com/jenish-jain/logger"
17+
)
18+
19+
// Dependencies holds all initialized dependencies for the application
20+
type Dependencies struct {
21+
Config *config.Config
22+
JiraClient interface{} // JIRA ticketing client
23+
TicketingSvc *ticketing.Service
24+
GitHubClient repository.RepositoryClient
25+
RepoSvc *repository.RepositoryService
26+
State *orchestrator.State
27+
Agent interface{} // AI agent (provider-agnostic)
28+
Coordinator *orchestrator.Coordinator
29+
RepoPaths *repository.RepositoryPath
30+
}
31+
32+
// State represents the agent state (wrapper for orchestrator.State)
33+
type State struct {
34+
*orchestrator.State
35+
}
36+
37+
// InitDependencies initializes all dependencies for the agent
38+
func InitDependencies(ctx context.Context) (*Dependencies, error) {
39+
// Load config
40+
cfg, err := config.LoadConfig()
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
// Initialize JIRA client
46+
jiraClient, err := jiraraw.NewRawClient(cfg.JiraURL, cfg.JiraEmail, cfg.JiraAPIToken)
47+
if err != nil {
48+
logger.Error("Failed to init JIRA client: %v", err)
49+
return nil, err
50+
}
51+
52+
// Health check for JIRA
53+
if err := jiraClient.HealthCheck(context.Background()); err != nil {
54+
logger.Error("JIRA health check failed: %v", err)
55+
return nil, err
56+
}
57+
58+
// Create ticketing service
59+
ticketingSvc := ticketing.NewService(jiraClient)
60+
61+
// Create repository path manager
62+
workingDir := cfg.WorkingDir
63+
if workingDir == "" {
64+
workingDir = "./workspace"
65+
}
66+
67+
repoPaths, err := repository.NewRepositoryPath(workingDir, cfg.GitHubRepo)
68+
if err != nil {
69+
logger.Error("Failed to create repository path manager", "error", err)
70+
return nil, err
71+
}
72+
73+
// Initialize GitHub client and repository service
74+
githubClient := github.NewClient(cfg.GitHubToken, cfg.GitHubOwner, cfg.GitHubRepo, repoPaths)
75+
repoSvc := repository.NewRepositoryService(githubClient)
76+
77+
// Load state
78+
stateFile := "agent_state.jsonc"
79+
state := orchestrator.NewState(stateFile)
80+
81+
// Load existing state if available
82+
// Only ignore "file not found" error (expected on first run)
83+
// Fail on other errors (permission denied, corrupted file, etc.)
84+
if err := state.Load(); err != nil && !os.IsNotExist(err) {
85+
stateErr := errors.NewStateLoadError(err, stateFile)
86+
logger.Error("Failed to load state file", stateErr.LogFields())
87+
logger.Info("State file may be corrupted. Delete %s to reset state.", stateFile)
88+
return nil, err
89+
}
90+
91+
// Initialize AI agent based on configured provider
92+
agent, err := provider.NewAgent(cfg)
93+
if err != nil {
94+
logger.Error("Failed to initialize AI agent: %v", err)
95+
return nil, err
96+
}
97+
logger.Info("Initialized AI provider", "provider", cfg.AIProvider)
98+
99+
// Create coordinator
100+
coordinator := orchestrator.NewCoordinator(ticketingSvc, repoSvc, agent, cfg, state, repoPaths)
101+
102+
return &Dependencies{
103+
Config: cfg,
104+
JiraClient: jiraClient,
105+
TicketingSvc: ticketingSvc,
106+
GitHubClient: githubClient,
107+
RepoSvc: repoSvc,
108+
State: state,
109+
Agent: agent,
110+
Coordinator: coordinator,
111+
RepoPaths: repoPaths,
112+
}, nil
113+
}
114+
115+
// InitPartialDependencies initializes only config and repository paths for lightweight commands
116+
func InitPartialDependencies() (*config.Config, *repository.RepositoryPath, error) {
117+
cfg, err := config.LoadConfig()
118+
if err != nil {
119+
return nil, nil, err
120+
}
121+
122+
workingDir := cfg.WorkingDir
123+
if workingDir == "" {
124+
workingDir = "./workspace"
125+
}
126+
127+
repoPaths, err := repository.NewRepositoryPath(workingDir, cfg.GitHubRepo)
128+
if err != nil {
129+
logger.Error("Failed to create repository path manager", "error", err)
130+
return nil, nil, err
131+
}
132+
133+
return cfg, repoPaths, nil
134+
}
135+
136+
// NewState creates a new state instance
137+
func NewState(filePath string) *State {
138+
return &State{
139+
State: orchestrator.NewState(filePath),
140+
}
141+
}

cmd/agent/commands/init.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package commands
2+
3+
import (
4+
"os"
5+
6+
logger "github.com/jenish-jain/logger"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
// InitCmd represents the init command
11+
var InitCmd = &cobra.Command{
12+
Use: "init",
13+
Short: "Initialize sample configuration files",
14+
Long: `Create sample config.yaml, .env.example, and agent_state.jsonc files.`,
15+
RunE: initConfig,
16+
}
17+
18+
func initConfig(cmd *cobra.Command, args []string) error {
19+
logger.Info("Creating sample configuration files...")
20+
21+
envContent := `JIRA_URL="https://company.atlassian.net"
22+
JIRA_EMAIL="ai-agent@company.com"
23+
JIRA_API_TOKEN="your-jira-api-token"
24+
JIRA_PROJECT_KEY="PROJ"
25+
JIRA_TRANSITION_TO_DO="11"
26+
JIRA_TRANSITION_IN_PROGRESS="21"
27+
JIRA_TRANSITION_DONE="31"
28+
29+
GITHUB_TOKEN="your-github-token"
30+
GITHUB_OWNER="company"
31+
GITHUB_REPO="main-repo"
32+
33+
# AI Provider Configuration
34+
# Options: "anthropic" (cloud API) or "ollama" (local LLM)
35+
AI_PROVIDER="anthropic"
36+
37+
# Anthropic Configuration (required if AI_PROVIDER=anthropic)
38+
ANTHROPIC_API_KEY="your-anthropic-api-key"
39+
40+
# Ollama Configuration (required if AI_PROVIDER=ollama)
41+
# Make sure Ollama is running locally: https://ollama.ai
42+
OLLAMA_BASE_URL="http://localhost:11434"
43+
OLLAMA_MODEL="qwen2.5-coder:7b" # Options: qwen2.5-coder:7b, deepseek-coder:6.7b, codellama:13b
44+
45+
AGENT_USERNAME="ai-intern"
46+
POLLING_INTERVAL="30s"
47+
MAX_CONCURRENT_TICKETS=1
48+
49+
WORKING_DIR="./workspace" # Will be ./workspace/{GITHUB_REPO} automatically
50+
BASE_BRANCH="master"
51+
BRANCH_PREFIX="feature/"
52+
53+
CONTEXT_MAX_FILES=40
54+
CONTEXT_MAX_BYTES=32
55+
CONTEXT_CACHE_ENABLED=true # Enable context caching for better performance
56+
CONTEXT_CACHE_TTL=1h # Cache time-to-live (e.g., "1h", "30m")
57+
58+
PLAN_MAX_FILES=10
59+
ALLOWED_WRITE_DIRS="internal,cmd,pkg,docs,config,."
60+
61+
# Self-Healing Configuration
62+
SELF_HEAL_ENABLED=false # Enable AI-powered self-healing for failed quality gates
63+
SELF_HEAL_MAX_ATTEMPTS=3 # Maximum healing attempts (default: 3)
64+
SELF_HEAL_ON_TESTS=true # Retry on test failures
65+
SELF_HEAL_ON_VET=true # Retry on vet failures
66+
SELF_HEAL_ON_BUILD=false # Retry on build failures (usually not needed for Go)
67+
68+
# Operational Mode
69+
DRY_RUN=false # If true, process tickets but don't create PRs (preview mode)
70+
71+
# Metrics Configuration
72+
METRICS_ENABLED=false # Enable HTTP metrics server with Prometheus format
73+
METRICS_PORT=9090 # Port for metrics server (default: 9090)
74+
# Access metrics at http://localhost:9090/metrics (Prometheus format)
75+
# Access dashboard at http://localhost:9090/ (web UI)
76+
# Access health check at http://localhost:9090/health
77+
`
78+
79+
if err := os.WriteFile(".env.example", []byte(envContent), 0644); err != nil {
80+
logger.Error("Failed to write .env.example: %v", err)
81+
return err
82+
}
83+
84+
stateContent := `{"processed":{}}`
85+
if err := os.WriteFile("agent_state.jsonc", []byte(stateContent), 0644); err != nil {
86+
logger.Error("Failed to write agent_state.jsonc: %v", err)
87+
return err
88+
}
89+
90+
logger.Info("Sample config.yaml, .env.example, and agent_state.jsonc created.")
91+
return nil
92+
}

cmd/agent/commands/metrics.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"intern/internal/orchestrator"
9+
10+
logger "github.com/jenish-jain/logger"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
// MetricsCmd represents the metrics command
15+
var MetricsCmd = &cobra.Command{
16+
Use: "metrics",
17+
Short: "Show detailed metrics summary",
18+
Long: `Display comprehensive metrics about agent performance, costs, and processing statistics.`,
19+
RunE: showMetrics,
20+
}
21+
22+
func showMetrics(cmd *cobra.Command, args []string) error {
23+
logger.Info("Loading metrics...")
24+
25+
// Load config to find metrics file
26+
_, repoPaths, err := InitPartialDependencies()
27+
if err != nil {
28+
logger.Error("Failed to initialize dependencies: %v", err)
29+
return err
30+
}
31+
32+
metricsPath := filepath.Join(repoPaths.Root(), ".ai-intern", "metrics.json")
33+
34+
// Check if metrics file exists
35+
if _, err := os.Stat(metricsPath); os.IsNotExist(err) {
36+
logger.Error("No metrics file found", "path", metricsPath)
37+
fmt.Println("\nNo metrics available yet. Run the agent to generate metrics.")
38+
return fmt.Errorf("metrics file not found")
39+
}
40+
41+
// Load metrics
42+
output, err := orchestrator.LoadMetrics(metricsPath)
43+
if err != nil {
44+
logger.Error("Failed to load metrics", "error", err)
45+
return err
46+
}
47+
48+
// Print metrics summary
49+
fmt.Println("\n=== AI Intern Agent Metrics ===")
50+
fmt.Printf("\nRun Metadata:\n")
51+
fmt.Printf(" Timestamp: %s\n", output.RunMetadata.Timestamp)
52+
fmt.Printf(" Duration: %.1f seconds\n", output.RunMetadata.DurationSeconds)
53+
fmt.Printf(" Version: %s\n", output.RunMetadata.AgentVersion)
54+
55+
fmt.Printf("\nSummary:\n")
56+
fmt.Printf(" Tickets Processed: %d\n", output.Summary.TicketsProcessed)
57+
fmt.Printf(" PRs Created: %d\n", output.Summary.PRsCreated)
58+
fmt.Printf(" Tickets Failed: %d\n", output.Summary.TicketsFailed)
59+
60+
fmt.Printf("\nCost Metrics:\n")
61+
fmt.Printf(" Total Cost: $%.2f\n", output.Summary.TotalCost)
62+
fmt.Printf(" Avg Cost/Ticket: $%.3f\n", output.Summary.AvgCostPerTicket)
63+
fmt.Printf(" Input Tokens: %d\n", output.Summary.TotalInputTokens)
64+
fmt.Printf(" Output Tokens: %d\n", output.Summary.TotalOutputTokens)
65+
66+
fmt.Printf("\nContext Strategy:\n")
67+
fmt.Printf(" Smart Context: %d\n", output.Summary.SmartContextUsed)
68+
fmt.Printf(" Simple Context: %d\n", output.Summary.SimpleContextUsed)
69+
70+
fmt.Printf("\nPerformance:\n")
71+
fmt.Printf(" Avg Time/Ticket: %.1f seconds\n", output.Summary.AvgTimePerTicket)
72+
fmt.Printf(" Files Changed: %d\n", output.Summary.TotalFilesChanged)
73+
74+
fmt.Println("\n=== End Metrics ===")
75+
return nil
76+
}

0 commit comments

Comments
 (0)