blob: 421e861ffe91b9bc57f0c7b33589780b847b4651 [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"`
user5a7d60d2025-07-27 21:22:04 +040015 Agents []AgentConfig `yaml:"agents"`
16 Tasks TasksConfig `yaml:"tasks"`
17 Git GitConfig `yaml:"git"`
18}
19
20// OpenAIConfig represents OpenAI provider configuration
21type OpenAIConfig struct {
22 APIKey string `yaml:"api_key"`
23 Model string `yaml:"model"`
24 BaseURL string `yaml:"base_url"`
25 Timeout time.Duration `yaml:"timeout"`
26 MaxRetries int `yaml:"max_retries"`
27}
28
29// GitHubConfig represents GitHub integration configuration
30type GitHubConfig struct {
31 Token string `yaml:"token"`
32 Owner string `yaml:"owner"`
33 Repo string `yaml:"repo"`
34}
35
36// AgentConfig represents individual agent configuration
37type AgentConfig struct {
38 Name string `yaml:"name"`
39 Role string `yaml:"role"`
40 Model string `yaml:"model"`
41 SystemPromptFile string `yaml:"system_prompt_file"`
iomodo50598c62025-07-27 22:06:32 +040042 Capabilities []string `yaml:"capabilities"` // For auto-assignment
43 TaskTypes []string `yaml:"task_types"` // Types of tasks this agent handles
44 MaxTokens *int `yaml:"max_tokens"` // Model-specific token limits
45 Temperature *float64 `yaml:"temperature"` // Model creativity setting
user5a7d60d2025-07-27 21:22:04 +040046}
47
48// TasksConfig represents task management configuration
49type TasksConfig struct {
50 StoragePath string `yaml:"storage_path"`
51 CompletedPath string `yaml:"completed_path"`
52}
53
54// GitConfig represents Git operation configuration
55type GitConfig struct {
56 BranchPrefix string `yaml:"branch_prefix"`
57 CommitMessageTemplate string `yaml:"commit_message_template"`
58 PRTemplate string `yaml:"pr_template"`
59}
60
61// LoadConfig loads configuration from a YAML file
62func LoadConfig(configPath string) (*Config, error) {
63 // Read the config file
64 data, err := os.ReadFile(configPath)
65 if err != nil {
66 return nil, fmt.Errorf("failed to read config file: %w", err)
67 }
68
69 // Parse YAML
70 var config Config
71 if err := yaml.Unmarshal(data, &config); err != nil {
72 return nil, fmt.Errorf("failed to parse config YAML: %w", err)
73 }
74
75 // Apply defaults
76 config = applyDefaults(config)
77
78 // Validate configuration
79 if err := validateConfig(config); err != nil {
80 return nil, fmt.Errorf("invalid configuration: %w", err)
81 }
82
83 return &config, nil
84}
85
86// LoadConfigWithEnvOverrides loads config with environment variable overrides
87func LoadConfigWithEnvOverrides(configPath string) (*Config, error) {
88 config, err := LoadConfig(configPath)
89 if err != nil {
90 return nil, err
91 }
92
93 // Override with environment variables if present
94 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
95 config.OpenAI.APIKey = apiKey
96 }
97 if githubToken := os.Getenv("GITHUB_TOKEN"); githubToken != "" {
98 config.GitHub.Token = githubToken
99 }
100 if owner := os.Getenv("GITHUB_OWNER"); owner != "" {
101 config.GitHub.Owner = owner
102 }
103 if repo := os.Getenv("GITHUB_REPO"); repo != "" {
104 config.GitHub.Repo = repo
105 }
106
107 // Re-validate after env overrides
108 if err := validateConfig(*config); err != nil {
109 return nil, fmt.Errorf("invalid configuration after env overrides: %w", err)
110 }
111
112 return config, nil
113}
114
115// applyDefaults applies default values to configuration
116func applyDefaults(config Config) Config {
117 // OpenAI defaults
118 if config.OpenAI.Model == "" {
119 config.OpenAI.Model = "gpt-4"
120 }
121 if config.OpenAI.BaseURL == "" {
122 config.OpenAI.BaseURL = "https://api.openai.com/v1"
123 }
124 if config.OpenAI.Timeout == 0 {
125 config.OpenAI.Timeout = 30 * time.Second
126 }
127 if config.OpenAI.MaxRetries == 0 {
128 config.OpenAI.MaxRetries = 3
129 }
130
131 // Tasks defaults
132 if config.Tasks.StoragePath == "" {
iomodo50598c62025-07-27 22:06:32 +0400133 config.Tasks.StoragePath = "../operations/"
user5a7d60d2025-07-27 21:22:04 +0400134 }
135 if config.Tasks.CompletedPath == "" {
iomodo50598c62025-07-27 22:06:32 +0400136 config.Tasks.CompletedPath = "../operations/completed/"
user5a7d60d2025-07-27 21:22:04 +0400137 }
138
139 // Git defaults
140 if config.Git.BranchPrefix == "" {
141 config.Git.BranchPrefix = "task/"
142 }
143 if config.Git.CommitMessageTemplate == "" {
144 config.Git.CommitMessageTemplate = "Task {task_id}: {task_title}\n\n{solution}\n\nGenerated by Staff AI Agent: {agent_name}"
145 }
146 if config.Git.PRTemplate == "" {
147 config.Git.PRTemplate = `## Task: {task_title}
148
149**Task ID:** {task_id}
150**Agent:** {agent_name}
151**Priority:** {priority}
152
153### Description
154{task_description}
155
156### Solution
157{solution}
158
159### Files Changed
160{files_changed}
161
162---
163*Generated by Staff AI Multi-Agent System*`
164 }
165
166 // Agent defaults
167 for i := range config.Agents {
168 if config.Agents[i].Model == "" {
169 config.Agents[i].Model = config.OpenAI.Model
170 }
171 }
172
173 return config
174}
175
176// validateConfig validates the configuration
177func validateConfig(config Config) error {
178 // Validate OpenAI config
179 if config.OpenAI.APIKey == "" {
180 return fmt.Errorf("openai.api_key is required")
181 }
182 if config.OpenAI.Model == "" {
183 return fmt.Errorf("openai.model is required")
184 }
185
186 // Validate GitHub config
187 if config.GitHub.Token == "" {
188 return fmt.Errorf("github.token is required")
189 }
190 if config.GitHub.Owner == "" {
191 return fmt.Errorf("github.owner is required")
192 }
193 if config.GitHub.Repo == "" {
194 return fmt.Errorf("github.repo is required")
195 }
196
197 // Validate agents
198 if len(config.Agents) == 0 {
199 return fmt.Errorf("at least one agent must be configured")
200 }
201
202 for i, agent := range config.Agents {
203 if agent.Name == "" {
204 return fmt.Errorf("agent[%d].name is required", i)
205 }
206 if agent.Role == "" {
207 return fmt.Errorf("agent[%d].role is required", i)
208 }
209 if agent.SystemPromptFile == "" {
210 return fmt.Errorf("agent[%d].system_prompt_file is required", i)
211 }
212 }
213
214 return nil
215}
216
217// GetAgentByName returns an agent config by name
218func (c *Config) GetAgentByName(name string) (*AgentConfig, error) {
219 for _, agent := range c.Agents {
220 if agent.Name == name {
221 return &agent, nil
222 }
223 }
224 return nil, fmt.Errorf("agent not found: %s", name)
225}
226
227// ListAgentNames returns a list of all configured agent names
228func (c *Config) ListAgentNames() []string {
229 names := make([]string, len(c.Agents))
230 for i, agent := range c.Agents {
231 names[i] = agent.Name
232 }
233 return names
iomodo50598c62025-07-27 22:06:32 +0400234}