feat: implement YAML-based configuration, .env support, and customizable prompt templates for AI analysis
This commit is contained in:
+116
-12
@@ -5,18 +5,72 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type AppConfigFile struct {
|
||||
Server struct {
|
||||
Port string `yaml:"port"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
} `yaml:"server"`
|
||||
AITrigger struct {
|
||||
AnalysisThreshold float64 `yaml:"analysis_threshold"`
|
||||
} `yaml:"ai_trigger"`
|
||||
Prompts struct {
|
||||
SystemPromptPath string `yaml:"system_prompt_path"`
|
||||
UserPromptPath string `yaml:"user_prompt_path"`
|
||||
} `yaml:"prompts"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
DBPath string
|
||||
TargetConfig string
|
||||
FilterConfig string
|
||||
LogLevel string
|
||||
LLMProvider string // "gemini", "deepseek", "mock"
|
||||
GeminiAPIKey string
|
||||
DeepSeekAPIKey string
|
||||
DeepSeekBaseURL string
|
||||
Port string
|
||||
DBPath string
|
||||
TargetConfig string
|
||||
FilterConfig string
|
||||
AppConfig string
|
||||
LogLevel string
|
||||
LLMProvider string // "gemini", "deepseek", "mock"
|
||||
GeminiAPIKey string
|
||||
DeepSeekAPIKey string
|
||||
DeepSeekBaseURL string
|
||||
AnalysisThreshold float64
|
||||
SystemPromptPath string
|
||||
UserPromptPath string
|
||||
SystemPrompt string
|
||||
UserPrompt string
|
||||
}
|
||||
|
||||
func init() {
|
||||
LoadEnvFile(".env")
|
||||
}
|
||||
|
||||
// LoadEnvFile parses a simple KEY=VALUE .env file if it exists without overriding existing env vars
|
||||
func LoadEnvFile(filenames ...string) {
|
||||
if len(filenames) == 0 {
|
||||
filenames = []string{".env"}
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
val = strings.Trim(val, `"'`)
|
||||
if os.Getenv(key) == "" {
|
||||
os.Setenv(key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterFlags(flags *pflag.FlagSet) *Config {
|
||||
@@ -26,21 +80,71 @@ func RegisterFlags(flags *pflag.FlagSet) *Config {
|
||||
flags.StringVarP(&cfg.DBPath, "db", "d", getEnvOrDefault("DB_PATH", "data/db/hireme.db"), "Path to database file")
|
||||
flags.StringVarP(&cfg.TargetConfig, "target-config", "c", getEnvOrDefault("TARGET_CONFIG", "data/config/targets.yaml"), "Path to targets YAML file")
|
||||
flags.StringVarP(&cfg.FilterConfig, "filter-config", "f", getEnvOrDefault("FILTER_CONFIG", "data/config/filters.yaml"), "Path to filters YAML file")
|
||||
flags.StringVar(&cfg.AppConfig, "app-config", getEnvOrDefault("APP_CONFIG", "data/config/config.yaml"), "Path to app YAML config file")
|
||||
flags.StringVar(&cfg.LogLevel, "log-level", getEnvOrDefault("LOG_LEVEL", "info"), "Logging level (trace, debug, info, error)")
|
||||
flags.StringVarP(&cfg.LLMProvider, "llm-provider", "l", getEnvOrDefault("LLM_PROVIDER", ""), "Active LLM Provider (gemini, deepseek, mock)")
|
||||
flags.StringVar(&cfg.GeminiAPIKey, "gemini-api-key", os.Getenv("GEMINI_API_KEY"), "Gemini API Key")
|
||||
flags.StringVar(&cfg.DeepSeekAPIKey, "deepseek-api-key", os.Getenv("DEEPSEEK_API_KEY"), "DeepSeek API Key")
|
||||
flags.StringVar(&cfg.DeepSeekBaseURL, "deepseek-base-url", getEnvOrDefault("DEEPSEEK_BASE_URL", "https://api.deepseek.com"), "DeepSeek Base URL")
|
||||
flags.Float64Var(&cfg.AnalysisThreshold, "analysis-threshold", 25.0, "Threshold score from filter to trigger LLM analysis")
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (c *Config) Normalize() {
|
||||
// Re-check env vars in case .env was loaded after initial flag binding
|
||||
if c.DeepSeekAPIKey == "" {
|
||||
c.DeepSeekAPIKey = os.Getenv("DEEPSEEK_API_KEY")
|
||||
}
|
||||
if c.GeminiAPIKey == "" {
|
||||
c.GeminiAPIKey = os.Getenv("GEMINI_API_KEY")
|
||||
}
|
||||
|
||||
// Load data/config/config.yaml if present
|
||||
if c.AppConfig != "" {
|
||||
if data, err := os.ReadFile(c.AppConfig); err == nil {
|
||||
var appCfg AppConfigFile
|
||||
if err := yaml.Unmarshal(data, &appCfg); err == nil {
|
||||
if appCfg.Server.Port != "" && c.Port == "8080" {
|
||||
c.Port = appCfg.Server.Port
|
||||
}
|
||||
if appCfg.Server.LogLevel != "" && c.LogLevel == "info" {
|
||||
c.LogLevel = appCfg.Server.LogLevel
|
||||
}
|
||||
if appCfg.AITrigger.AnalysisThreshold > 0 {
|
||||
c.AnalysisThreshold = appCfg.AITrigger.AnalysisThreshold
|
||||
}
|
||||
if appCfg.Prompts.SystemPromptPath != "" {
|
||||
c.SystemPromptPath = appCfg.Prompts.SystemPromptPath
|
||||
}
|
||||
if appCfg.Prompts.UserPromptPath != "" {
|
||||
c.UserPromptPath = appCfg.Prompts.UserPromptPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.SystemPromptPath == "" {
|
||||
c.SystemPromptPath = "data/config/prompts/system_prompt.txt"
|
||||
}
|
||||
if c.UserPromptPath == "" {
|
||||
c.UserPromptPath = "data/config/prompts/user_prompt.txt"
|
||||
}
|
||||
|
||||
// Load prompt contents from files
|
||||
if promptData, err := os.ReadFile(c.SystemPromptPath); err == nil {
|
||||
c.SystemPrompt = strings.TrimSpace(string(promptData))
|
||||
}
|
||||
if userPromptData, err := os.ReadFile(c.UserPromptPath); err == nil {
|
||||
c.UserPrompt = strings.TrimSpace(string(userPromptData))
|
||||
}
|
||||
|
||||
// Determine LLM provider
|
||||
if c.LLMProvider == "" {
|
||||
if c.GeminiAPIKey != "" {
|
||||
c.LLMProvider = "gemini"
|
||||
} else if c.DeepSeekAPIKey != "" {
|
||||
if c.DeepSeekAPIKey != "" {
|
||||
c.LLMProvider = "deepseek"
|
||||
} else if c.GeminiAPIKey != "" {
|
||||
c.LLMProvider = "gemini"
|
||||
} else {
|
||||
c.LLMProvider = "mock"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigLoadEnvAndAppConfig(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
envPath := filepath.Join(tempDir, ".env")
|
||||
configYAMLPath := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
_ = os.WriteFile(envPath, []byte("TEST_SECRET_KEY=12345\nDEEPSEEK_API_KEY=test-sk-key\n"), 0644)
|
||||
_ = os.WriteFile(configYAMLPath, []byte(`
|
||||
server:
|
||||
port: "9090"
|
||||
log_level: "debug"
|
||||
ai_trigger:
|
||||
analysis_threshold: 35.0
|
||||
prompts:
|
||||
system_prompt_path: "sys.txt"
|
||||
`), 0644)
|
||||
|
||||
LoadEnvFile(envPath)
|
||||
if os.Getenv("TEST_SECRET_KEY") != "12345" {
|
||||
t.Errorf("expected TEST_SECRET_KEY to be 12345, got %q", os.Getenv("TEST_SECRET_KEY"))
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
Port: "8080",
|
||||
LogLevel: "info",
|
||||
AppConfig: configYAMLPath,
|
||||
}
|
||||
cfg.Normalize()
|
||||
|
||||
if cfg.Port != "9090" {
|
||||
t.Errorf("expected port 9090 from config.yaml, got %s", cfg.Port)
|
||||
}
|
||||
if cfg.AnalysisThreshold != 35.0 {
|
||||
t.Errorf("expected analysis threshold 35.0, got %f", cfg.AnalysisThreshold)
|
||||
}
|
||||
if cfg.LLMProvider != "deepseek" {
|
||||
t.Errorf("expected LLMProvider to be deepseek, got %s", cfg.LLMProvider)
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
)
|
||||
|
||||
type FilterConfig struct {
|
||||
HardKeywords []string `yaml:"hard_keywords"`
|
||||
ScoringWeights []filter.KeywordWeight `yaml:"scoring_weights"`
|
||||
HardKeywords []string `yaml:"hard_keywords"`
|
||||
AnalysisThreshold float64 `yaml:"analysis_threshold"`
|
||||
ScoringWeights []filter.KeywordWeight `yaml:"scoring_weights"`
|
||||
}
|
||||
|
||||
func LoadFilterConfig(path string) (*FilterConfig, error) {
|
||||
@@ -20,6 +21,8 @@ func LoadFilterConfig(path string) (*FilterConfig, error) {
|
||||
}
|
||||
|
||||
var cfg FilterConfig
|
||||
// Default threshold if not specified
|
||||
cfg.AnalysisThreshold = 25.0
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal filter config: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user