blob: c51bd43916885c97ee26085aba551174f116f966 [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"`
user5a7d60d2025-07-27 21:22:04 +040066 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
user5a7d60d2025-07-27 21:22:04 +0400161 if config.Git.CommitMessageTemplate == "" {
162 config.Git.CommitMessageTemplate = "Task {task_id}: {task_title}\n\n{solution}\n\nGenerated by Staff AI Agent: {agent_name}"
163 }
164 if config.Git.PRTemplate == "" {
165 config.Git.PRTemplate = `## Task: {task_title}
166
167**Task ID:** {task_id}
168**Agent:** {agent_name}
169**Priority:** {priority}
170
171### Description
172{task_description}
173
174### Solution
175{solution}
176
177### Files Changed
178{files_changed}
179
180---
181*Generated by Staff AI Multi-Agent System*`
182 }
183
184 // Agent defaults
185 for i := range config.Agents {
186 if config.Agents[i].Model == "" {
187 config.Agents[i].Model = config.OpenAI.Model
188 }
189 }
190
191 return config
192}
193
194// validateConfig validates the configuration
195func validateConfig(config Config) error {
196 // Validate OpenAI config
197 if config.OpenAI.APIKey == "" {
198 return fmt.Errorf("openai.api_key is required")
199 }
200 if config.OpenAI.Model == "" {
201 return fmt.Errorf("openai.model is required")
202 }
203
iomodo578f5042025-07-28 20:46:02 +0400204 // Validate that at least one Git provider is configured
205 hasGitHub := config.GitHub.Token != "" && config.GitHub.Owner != "" && config.GitHub.Repo != ""
206 hasGerrit := config.Gerrit.Username != "" && config.Gerrit.Password != "" && config.Gerrit.BaseURL != "" && config.Gerrit.Project != ""
iomodoa53240a2025-07-30 17:33:35 +0400207
iomodo578f5042025-07-28 20:46:02 +0400208 if !hasGitHub && !hasGerrit {
209 return fmt.Errorf("either GitHub or Gerrit configuration is required")
user5a7d60d2025-07-27 21:22:04 +0400210 }
iomodoa53240a2025-07-30 17:33:35 +0400211
iomodo578f5042025-07-28 20:46:02 +0400212 // Validate GitHub config if provided
213 if config.GitHub.Token != "" {
214 if config.GitHub.Owner == "" {
215 return fmt.Errorf("github.owner is required when github.token is provided")
216 }
217 if config.GitHub.Repo == "" {
218 return fmt.Errorf("github.repo is required when github.token is provided")
219 }
user5a7d60d2025-07-27 21:22:04 +0400220 }
iomodoa53240a2025-07-30 17:33:35 +0400221
iomodo578f5042025-07-28 20:46:02 +0400222 // Validate Gerrit config if provided
223 if config.Gerrit.Username != "" {
224 if config.Gerrit.Password == "" {
225 return fmt.Errorf("gerrit.password is required when gerrit.username is provided")
226 }
227 if config.Gerrit.BaseURL == "" {
228 return fmt.Errorf("gerrit.base_url is required when gerrit.username is provided")
229 }
230 if config.Gerrit.Project == "" {
231 return fmt.Errorf("gerrit.project is required when gerrit.username is provided")
232 }
user5a7d60d2025-07-27 21:22:04 +0400233 }
234
235 // Validate agents
236 if len(config.Agents) == 0 {
237 return fmt.Errorf("at least one agent must be configured")
238 }
239
240 for i, agent := range config.Agents {
241 if agent.Name == "" {
242 return fmt.Errorf("agent[%d].name is required", i)
243 }
244 if agent.Role == "" {
245 return fmt.Errorf("agent[%d].role is required", i)
246 }
247 if agent.SystemPromptFile == "" {
248 return fmt.Errorf("agent[%d].system_prompt_file is required", i)
249 }
250 }
251
252 return nil
253}
254
255// GetAgentByName returns an agent config by name
256func (c *Config) GetAgentByName(name string) (*AgentConfig, error) {
257 for _, agent := range c.Agents {
258 if agent.Name == name {
259 return &agent, nil
260 }
261 }
262 return nil, fmt.Errorf("agent not found: %s", name)
263}
264
265// ListAgentNames returns a list of all configured agent names
266func (c *Config) ListAgentNames() []string {
267 names := make([]string, len(c.Agents))
268 for i, agent := range c.Agents {
269 names[i] = agent.Name
270 }
271 return names
iomodo50598c62025-07-27 22:06:32 +0400272}
iomodo578f5042025-07-28 20:46:02 +0400273
274// HasGitHubConfig returns true if GitHub is properly configured
275func (c *Config) HasGitHubConfig() bool {
276 return c.GitHub.Token != "" && c.GitHub.Owner != "" && c.GitHub.Repo != ""
277}
278
279// HasGerritConfig returns true if Gerrit is properly configured
280func (c *Config) HasGerritConfig() bool {
281 return c.Gerrit.Username != "" && c.Gerrit.Password != "" && c.Gerrit.BaseURL != "" && c.Gerrit.Project != ""
282}
283
284// GetPrimaryGitProvider returns the primary git provider type ("github" or "gerrit")
285// If both are configured, GitHub takes precedence for backward compatibility
286func (c *Config) GetPrimaryGitProvider() string {
287 if c.HasGitHubConfig() {
288 return "github"
289 }
290 if c.HasGerritConfig() {
291 return "gerrit"
292 }
293 return ""
294}