blob: 86dd7a5a33c260ec977addcbb8f9f8aaec755d42 [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 {
65 BranchPrefix string `yaml:"branch_prefix"`
66 CommitMessageTemplate string `yaml:"commit_message_template"`
67 PRTemplate string `yaml:"pr_template"`
68}
69
70// LoadConfig loads configuration from a YAML file
71func LoadConfig(configPath string) (*Config, error) {
72 // Read the config file
73 data, err := os.ReadFile(configPath)
74 if err != nil {
75 return nil, fmt.Errorf("failed to read config file: %w", err)
76 }
77
78 // Parse YAML
79 var config Config
80 if err := yaml.Unmarshal(data, &config); err != nil {
81 return nil, fmt.Errorf("failed to parse config YAML: %w", err)
82 }
83
84 // Apply defaults
85 config = applyDefaults(config)
86
87 // Validate configuration
88 if err := validateConfig(config); err != nil {
89 return nil, fmt.Errorf("invalid configuration: %w", err)
90 }
91
92 return &config, nil
93}
94
95// LoadConfigWithEnvOverrides loads config with environment variable overrides
96func LoadConfigWithEnvOverrides(configPath string) (*Config, error) {
97 config, err := LoadConfig(configPath)
98 if err != nil {
99 return nil, err
100 }
101
102 // Override with environment variables if present
103 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
104 config.OpenAI.APIKey = apiKey
105 }
106 if githubToken := os.Getenv("GITHUB_TOKEN"); githubToken != "" {
107 config.GitHub.Token = githubToken
108 }
109 if owner := os.Getenv("GITHUB_OWNER"); owner != "" {
110 config.GitHub.Owner = owner
111 }
112 if repo := os.Getenv("GITHUB_REPO"); repo != "" {
113 config.GitHub.Repo = repo
114 }
iomodo578f5042025-07-28 20:46:02 +0400115 if gerritUsername := os.Getenv("GERRIT_USERNAME"); gerritUsername != "" {
116 config.Gerrit.Username = gerritUsername
117 }
118 if gerritPassword := os.Getenv("GERRIT_PASSWORD"); gerritPassword != "" {
119 config.Gerrit.Password = gerritPassword
120 }
121 if gerritBaseURL := os.Getenv("GERRIT_BASE_URL"); gerritBaseURL != "" {
122 config.Gerrit.BaseURL = gerritBaseURL
123 }
124 if gerritProject := os.Getenv("GERRIT_PROJECT"); gerritProject != "" {
125 config.Gerrit.Project = gerritProject
126 }
user5a7d60d2025-07-27 21:22:04 +0400127
128 // Re-validate after env overrides
129 if err := validateConfig(*config); err != nil {
130 return nil, fmt.Errorf("invalid configuration after env overrides: %w", err)
131 }
132
133 return config, nil
134}
135
136// applyDefaults applies default values to configuration
137func applyDefaults(config Config) Config {
138 // OpenAI defaults
139 if config.OpenAI.Model == "" {
140 config.OpenAI.Model = "gpt-4"
141 }
142 if config.OpenAI.BaseURL == "" {
143 config.OpenAI.BaseURL = "https://api.openai.com/v1"
144 }
145 if config.OpenAI.Timeout == 0 {
146 config.OpenAI.Timeout = 30 * time.Second
147 }
148 if config.OpenAI.MaxRetries == 0 {
149 config.OpenAI.MaxRetries = 3
150 }
151
152 // Tasks defaults
153 if config.Tasks.StoragePath == "" {
iomodo50598c62025-07-27 22:06:32 +0400154 config.Tasks.StoragePath = "../operations/"
user5a7d60d2025-07-27 21:22:04 +0400155 }
156 if config.Tasks.CompletedPath == "" {
iomodo50598c62025-07-27 22:06:32 +0400157 config.Tasks.CompletedPath = "../operations/completed/"
user5a7d60d2025-07-27 21:22:04 +0400158 }
159
160 // Git defaults
161 if config.Git.BranchPrefix == "" {
162 config.Git.BranchPrefix = "task/"
163 }
164 if config.Git.CommitMessageTemplate == "" {
165 config.Git.CommitMessageTemplate = "Task {task_id}: {task_title}\n\n{solution}\n\nGenerated by Staff AI Agent: {agent_name}"
166 }
167 if config.Git.PRTemplate == "" {
168 config.Git.PRTemplate = `## Task: {task_title}
169
170**Task ID:** {task_id}
171**Agent:** {agent_name}
172**Priority:** {priority}
173
174### Description
175{task_description}
176
177### Solution
178{solution}
179
180### Files Changed
181{files_changed}
182
183---
184*Generated by Staff AI Multi-Agent System*`
185 }
186
187 // Agent defaults
188 for i := range config.Agents {
189 if config.Agents[i].Model == "" {
190 config.Agents[i].Model = config.OpenAI.Model
191 }
192 }
193
194 return config
195}
196
197// validateConfig validates the configuration
198func validateConfig(config Config) error {
199 // Validate OpenAI config
200 if config.OpenAI.APIKey == "" {
201 return fmt.Errorf("openai.api_key is required")
202 }
203 if config.OpenAI.Model == "" {
204 return fmt.Errorf("openai.model is required")
205 }
206
iomodo578f5042025-07-28 20:46:02 +0400207 // Validate that at least one Git provider is configured
208 hasGitHub := config.GitHub.Token != "" && config.GitHub.Owner != "" && config.GitHub.Repo != ""
209 hasGerrit := config.Gerrit.Username != "" && config.Gerrit.Password != "" && config.Gerrit.BaseURL != "" && config.Gerrit.Project != ""
210
211 if !hasGitHub && !hasGerrit {
212 return fmt.Errorf("either GitHub or Gerrit configuration is required")
user5a7d60d2025-07-27 21:22:04 +0400213 }
iomodo578f5042025-07-28 20:46:02 +0400214
215 // Validate GitHub config if provided
216 if config.GitHub.Token != "" {
217 if config.GitHub.Owner == "" {
218 return fmt.Errorf("github.owner is required when github.token is provided")
219 }
220 if config.GitHub.Repo == "" {
221 return fmt.Errorf("github.repo is required when github.token is provided")
222 }
user5a7d60d2025-07-27 21:22:04 +0400223 }
iomodo578f5042025-07-28 20:46:02 +0400224
225 // Validate Gerrit config if provided
226 if config.Gerrit.Username != "" {
227 if config.Gerrit.Password == "" {
228 return fmt.Errorf("gerrit.password is required when gerrit.username is provided")
229 }
230 if config.Gerrit.BaseURL == "" {
231 return fmt.Errorf("gerrit.base_url is required when gerrit.username is provided")
232 }
233 if config.Gerrit.Project == "" {
234 return fmt.Errorf("gerrit.project is required when gerrit.username is provided")
235 }
user5a7d60d2025-07-27 21:22:04 +0400236 }
237
238 // Validate agents
239 if len(config.Agents) == 0 {
240 return fmt.Errorf("at least one agent must be configured")
241 }
242
243 for i, agent := range config.Agents {
244 if agent.Name == "" {
245 return fmt.Errorf("agent[%d].name is required", i)
246 }
247 if agent.Role == "" {
248 return fmt.Errorf("agent[%d].role is required", i)
249 }
250 if agent.SystemPromptFile == "" {
251 return fmt.Errorf("agent[%d].system_prompt_file is required", i)
252 }
253 }
254
255 return nil
256}
257
258// GetAgentByName returns an agent config by name
259func (c *Config) GetAgentByName(name string) (*AgentConfig, error) {
260 for _, agent := range c.Agents {
261 if agent.Name == name {
262 return &agent, nil
263 }
264 }
265 return nil, fmt.Errorf("agent not found: %s", name)
266}
267
268// ListAgentNames returns a list of all configured agent names
269func (c *Config) ListAgentNames() []string {
270 names := make([]string, len(c.Agents))
271 for i, agent := range c.Agents {
272 names[i] = agent.Name
273 }
274 return names
iomodo50598c62025-07-27 22:06:32 +0400275}
iomodo578f5042025-07-28 20:46:02 +0400276
277// HasGitHubConfig returns true if GitHub is properly configured
278func (c *Config) HasGitHubConfig() bool {
279 return c.GitHub.Token != "" && c.GitHub.Owner != "" && c.GitHub.Repo != ""
280}
281
282// HasGerritConfig returns true if Gerrit is properly configured
283func (c *Config) HasGerritConfig() bool {
284 return c.Gerrit.Username != "" && c.Gerrit.Password != "" && c.Gerrit.BaseURL != "" && c.Gerrit.Project != ""
285}
286
287// GetPrimaryGitProvider returns the primary git provider type ("github" or "gerrit")
288// If both are configured, GitHub takes precedence for backward compatibility
289func (c *Config) GetPrimaryGitProvider() string {
290 if c.HasGitHubConfig() {
291 return "github"
292 }
293 if c.HasGerritConfig() {
294 return "gerrit"
295 }
296 return ""
297}