blob: baceb040e799a04e7eb61589d457585a53f2255f [file] [log] [blame]
user5a7d60d2025-07-27 21:22:04 +04001package config
2
3import (
4 "fmt"
5 "os"
6 "time"
7
8 "gopkg.in/yaml.v3"
9)
10
11// Config represents the Staff MVP configuration
12type Config struct {
iomodo50598c62025-07-27 22:06:32 +040013 OpenAI OpenAIConfig `yaml:"openai"`
14 GitHub GitHubConfig `yaml:"github"`
iomodo578f5042025-07-28 20:46:02 +040015 Gerrit GerritConfig `yaml:"gerrit"`
user5a7d60d2025-07-27 21:22:04 +040016 Agents []AgentConfig `yaml:"agents"`
17 Tasks TasksConfig `yaml:"tasks"`
18 Git GitConfig `yaml:"git"`
19}
20
21// OpenAIConfig represents OpenAI provider configuration
22type OpenAIConfig struct {
23 APIKey string `yaml:"api_key"`
24 Model string `yaml:"model"`
25 BaseURL string `yaml:"base_url"`
26 Timeout time.Duration `yaml:"timeout"`
27 MaxRetries int `yaml:"max_retries"`
28}
29
30// GitHubConfig represents GitHub integration configuration
31type GitHubConfig struct {
32 Token string `yaml:"token"`
33 Owner string `yaml:"owner"`
34 Repo string `yaml:"repo"`
35}
36
iomodo578f5042025-07-28 20:46:02 +040037// GerritConfig represents Gerrit integration configuration
38type GerritConfig struct {
39 Username string `yaml:"username"`
40 Password string `yaml:"password"` // HTTP password or API token
41 BaseURL string `yaml:"base_url"` // Gerrit server URL (e.g. https://gerrit.example.com)
42 Project string `yaml:"project"` // Gerrit project name
43}
44
user5a7d60d2025-07-27 21:22:04 +040045// AgentConfig represents individual agent configuration
46type AgentConfig struct {
47 Name string `yaml:"name"`
48 Role string `yaml:"role"`
49 Model string `yaml:"model"`
50 SystemPromptFile string `yaml:"system_prompt_file"`
iomodo50598c62025-07-27 22:06:32 +040051 Capabilities []string `yaml:"capabilities"` // For auto-assignment
52 TaskTypes []string `yaml:"task_types"` // Types of tasks this agent handles
53 MaxTokens *int `yaml:"max_tokens"` // Model-specific token limits
54 Temperature *float64 `yaml:"temperature"` // Model creativity setting
user5a7d60d2025-07-27 21:22:04 +040055}
56
57// TasksConfig represents task management configuration
58type TasksConfig struct {
59 StoragePath string `yaml:"storage_path"`
60 CompletedPath string `yaml:"completed_path"`
61}
62
63// GitConfig represents Git operation configuration
64type GitConfig struct {
iomodoa53240a2025-07-30 17:33:35 +040065 RepoPath string `yaml:"repo_path"`
iomodoe7477eb2025-07-30 20:26:30 +040066 WorkspacePath string `yaml:"workspace_path"`
user5a7d60d2025-07-27 21:22:04 +040067 CommitMessageTemplate string `yaml:"commit_message_template"`
68 PRTemplate string `yaml:"pr_template"`
69}
70
71// LoadConfig loads configuration from a YAML file
72func LoadConfig(configPath string) (*Config, error) {
73 // Read the config file
74 data, err := os.ReadFile(configPath)
75 if err != nil {
76 return nil, fmt.Errorf("failed to read config file: %w", err)
77 }
78
79 // Parse YAML
80 var config Config
81 if err := yaml.Unmarshal(data, &config); err != nil {
82 return nil, fmt.Errorf("failed to parse config YAML: %w", err)
83 }
84
85 // Apply defaults
86 config = applyDefaults(config)
87
88 // Validate configuration
89 if err := validateConfig(config); err != nil {
90 return nil, fmt.Errorf("invalid configuration: %w", err)
91 }
92
93 return &config, nil
94}
95
96// LoadConfigWithEnvOverrides loads config with environment variable overrides
97func LoadConfigWithEnvOverrides(configPath string) (*Config, error) {
98 config, err := LoadConfig(configPath)
99 if err != nil {
100 return nil, err
101 }
102
103 // Override with environment variables if present
104 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
105 config.OpenAI.APIKey = apiKey
106 }
107 if githubToken := os.Getenv("GITHUB_TOKEN"); githubToken != "" {
108 config.GitHub.Token = githubToken
109 }
110 if owner := os.Getenv("GITHUB_OWNER"); owner != "" {
111 config.GitHub.Owner = owner
112 }
113 if repo := os.Getenv("GITHUB_REPO"); repo != "" {
114 config.GitHub.Repo = repo
115 }
iomodo578f5042025-07-28 20:46:02 +0400116 if gerritUsername := os.Getenv("GERRIT_USERNAME"); gerritUsername != "" {
117 config.Gerrit.Username = gerritUsername
118 }
119 if gerritPassword := os.Getenv("GERRIT_PASSWORD"); gerritPassword != "" {
120 config.Gerrit.Password = gerritPassword
121 }
122 if gerritBaseURL := os.Getenv("GERRIT_BASE_URL"); gerritBaseURL != "" {
123 config.Gerrit.BaseURL = gerritBaseURL
124 }
125 if gerritProject := os.Getenv("GERRIT_PROJECT"); gerritProject != "" {
126 config.Gerrit.Project = gerritProject
127 }
user5a7d60d2025-07-27 21:22:04 +0400128
129 // Re-validate after env overrides
130 if err := validateConfig(*config); err != nil {
131 return nil, fmt.Errorf("invalid configuration after env overrides: %w", err)
132 }
133
134 return config, nil
135}
136
137// applyDefaults applies default values to configuration
138func applyDefaults(config Config) Config {
139 // OpenAI defaults
140 if config.OpenAI.Model == "" {
141 config.OpenAI.Model = "gpt-4"
142 }
143 if config.OpenAI.BaseURL == "" {
144 config.OpenAI.BaseURL = "https://api.openai.com/v1"
145 }
146 if config.OpenAI.Timeout == 0 {
147 config.OpenAI.Timeout = 30 * time.Second
148 }
149 if config.OpenAI.MaxRetries == 0 {
150 config.OpenAI.MaxRetries = 3
151 }
152
153 // Tasks defaults
154 if config.Tasks.StoragePath == "" {
iomodo50598c62025-07-27 22:06:32 +0400155 config.Tasks.StoragePath = "../operations/"
user5a7d60d2025-07-27 21:22:04 +0400156 }
157 if config.Tasks.CompletedPath == "" {
iomodo50598c62025-07-27 22:06:32 +0400158 config.Tasks.CompletedPath = "../operations/completed/"
user5a7d60d2025-07-27 21:22:04 +0400159 }
160
161 // Git defaults
user5a7d60d2025-07-27 21:22:04 +0400162 if config.Git.CommitMessageTemplate == "" {
163 config.Git.CommitMessageTemplate = "Task {task_id}: {task_title}\n\n{solution}\n\nGenerated by Staff AI Agent: {agent_name}"
164 }
165 if config.Git.PRTemplate == "" {
166 config.Git.PRTemplate = `## Task: {task_title}
167
168**Task ID:** {task_id}
169**Agent:** {agent_name}
170**Priority:** {priority}
171
172### Description
173{task_description}
174
175### Solution
176{solution}
177
178### Files Changed
179{files_changed}
180
181---
182*Generated by Staff AI Multi-Agent System*`
183 }
184
185 // Agent defaults
186 for i := range config.Agents {
187 if config.Agents[i].Model == "" {
188 config.Agents[i].Model = config.OpenAI.Model
189 }
190 }
191
192 return config
193}
194
195// validateConfig validates the configuration
196func validateConfig(config Config) error {
197 // Validate OpenAI config
198 if config.OpenAI.APIKey == "" {
199 return fmt.Errorf("openai.api_key is required")
200 }
201 if config.OpenAI.Model == "" {
202 return fmt.Errorf("openai.model is required")
203 }
204
iomodo578f5042025-07-28 20:46:02 +0400205 // Validate that at least one Git provider is configured
206 hasGitHub := config.GitHub.Token != "" && config.GitHub.Owner != "" && config.GitHub.Repo != ""
207 hasGerrit := config.Gerrit.Username != "" && config.Gerrit.Password != "" && config.Gerrit.BaseURL != "" && config.Gerrit.Project != ""
iomodoa53240a2025-07-30 17:33:35 +0400208
iomodo578f5042025-07-28 20:46:02 +0400209 if !hasGitHub && !hasGerrit {
210 return fmt.Errorf("either GitHub or Gerrit configuration is required")
user5a7d60d2025-07-27 21:22:04 +0400211 }
iomodoa53240a2025-07-30 17:33:35 +0400212
iomodo578f5042025-07-28 20:46:02 +0400213 // Validate GitHub config if provided
214 if config.GitHub.Token != "" {
215 if config.GitHub.Owner == "" {
216 return fmt.Errorf("github.owner is required when github.token is provided")
217 }
218 if config.GitHub.Repo == "" {
219 return fmt.Errorf("github.repo is required when github.token is provided")
220 }
user5a7d60d2025-07-27 21:22:04 +0400221 }
iomodoa53240a2025-07-30 17:33:35 +0400222
iomodo578f5042025-07-28 20:46:02 +0400223 // Validate Gerrit config if provided
224 if config.Gerrit.Username != "" {
225 if config.Gerrit.Password == "" {
226 return fmt.Errorf("gerrit.password is required when gerrit.username is provided")
227 }
228 if config.Gerrit.BaseURL == "" {
229 return fmt.Errorf("gerrit.base_url is required when gerrit.username is provided")
230 }
231 if config.Gerrit.Project == "" {
232 return fmt.Errorf("gerrit.project is required when gerrit.username is provided")
233 }
user5a7d60d2025-07-27 21:22:04 +0400234 }
235
236 // Validate agents
237 if len(config.Agents) == 0 {
238 return fmt.Errorf("at least one agent must be configured")
239 }
240
241 for i, agent := range config.Agents {
242 if agent.Name == "" {
243 return fmt.Errorf("agent[%d].name is required", i)
244 }
245 if agent.Role == "" {
246 return fmt.Errorf("agent[%d].role is required", i)
247 }
248 if agent.SystemPromptFile == "" {
249 return fmt.Errorf("agent[%d].system_prompt_file is required", i)
250 }
251 }
252
253 return nil
254}
255
256// GetAgentByName returns an agent config by name
257func (c *Config) GetAgentByName(name string) (*AgentConfig, error) {
258 for _, agent := range c.Agents {
259 if agent.Name == name {
260 return &agent, nil
261 }
262 }
263 return nil, fmt.Errorf("agent not found: %s", name)
264}
265
266// ListAgentNames returns a list of all configured agent names
267func (c *Config) ListAgentNames() []string {
268 names := make([]string, len(c.Agents))
269 for i, agent := range c.Agents {
270 names[i] = agent.Name
271 }
272 return names
iomodo50598c62025-07-27 22:06:32 +0400273}
iomodo578f5042025-07-28 20:46:02 +0400274
275// HasGitHubConfig returns true if GitHub is properly configured
276func (c *Config) HasGitHubConfig() bool {
277 return c.GitHub.Token != "" && c.GitHub.Owner != "" && c.GitHub.Repo != ""
278}
279
280// HasGerritConfig returns true if Gerrit is properly configured
281func (c *Config) HasGerritConfig() bool {
282 return c.Gerrit.Username != "" && c.Gerrit.Password != "" && c.Gerrit.BaseURL != "" && c.Gerrit.Project != ""
283}
284
285// GetPrimaryGitProvider returns the primary git provider type ("github" or "gerrit")
286// If both are configured, GitHub takes precedence for backward compatibility
287func (c *Config) GetPrimaryGitProvider() string {
288 if c.HasGitHubConfig() {
289 return "github"
290 }
291 if c.HasGerritConfig() {
292 return "gerrit"
293 }
294 return ""
295}