diff --git a/Makefile b/Makefile index 188612c..c08531e 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,10 @@ completion: ## Install shell completion @echo "Installed completion for $(SHELL_NAME)" @echo "Restart your shell or source the completion file to use it" +test: ## Run tests + @echo "Running tests..." + go test -v ./... + clean: ## Clean build files @echo "Cleaning..." go clean diff --git a/go.mod b/go.mod index 557b30e..2cfdcf1 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/exp/shiny v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/image v0.18.0 // indirect + golang.org/x/image v0.22.0 // indirect golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sys v0.20.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6e16dc7..aafad2b 100644 --- a/go.sum +++ b/go.sum @@ -12,12 +12,12 @@ golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo= golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E= golang.org/x/exp/shiny v0.0.0-20241009180824-f66d83c29e7c h1:jTMrjjZRcSH/BDxWhXCP6OWsfVgmnwI7J+F4/nyVXaU= golang.org/x/exp/shiny v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= +golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/cmd/generate.go b/src/cmd/generate.go index 383c9f6..6a04b46 100644 --- a/src/cmd/generate.go +++ b/src/cmd/generate.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "strings" @@ -100,11 +101,13 @@ func runGenerate(cmd *cobra.Command, args []string) error { finalPrompt = string(content) } + cfg := config.NewClientConfig() + // Create request body reqBody := OllamaRequest{ - Model: config.OllamaModel, + Model: cfg.OllamaConfig.Model, Prompt: finalPrompt, - Stream: config.OllamaStream, + Stream: cfg.OllamaConfig.Stream, } jsonData, err := json.Marshal(reqBody) @@ -112,10 +115,15 @@ func runGenerate(cmd *cobra.Command, args []string) error { return err } - url := config.OllamaURL + "/generate" + endpoint := "/api/generate" + + ollamaURL, err := url.JoinPath(cfg.OllamaConfig.Host, endpoint) + if err != nil { + return err + } // Make POST request to Ollama API - resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) + resp, err := http.Post(ollamaURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return err } diff --git a/src/config/client_config.go b/src/config/client_config.go new file mode 100644 index 0000000..20ee9e7 --- /dev/null +++ b/src/config/client_config.go @@ -0,0 +1,21 @@ +package config + +type ClientConfig struct { + OllamaConfig OllamaConfig `json:"ollama"` +} + +type OllamaConfig struct { + Host string `json:"host"` + Model string `json:"model"` + Stream bool `json:"stream"` +} + +func NewClientConfig() *ClientConfig { + return &ClientConfig{ + OllamaConfig: OllamaConfig{ + Host: getEnvAsString("OLLAMA_HOST", "http://localhost:11434/"), + Model: getEnvAsString("OLLAMA_MODEL", "llama3"), + Stream: getEnvAsBool("OLLAMA_STREAM", true), + }, + } +} diff --git a/src/config/config.go b/src/config/config.go index 98deae8..11b0e8a 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -39,29 +39,10 @@ tags: --- This is an example prompt.` -const EnvPrefix string = "GRIMOIRE" - var ( - OllamaModel string = getEnv(EnvPrefix+"_OLLAMA_MODEL", "llama3") - OllamaURL string = getEnv(EnvPrefix+"_OLLAMA_URL", "http://localhost:11434/api") - OllamaStream bool = getEnvAsBool(EnvPrefix+"_OLLAMA_STREAM", true) - Editor string = getEnv(EnvPrefix+"_EDITOR", "vim") + Editor string = getEnvAsString("EDITOR", "vim") ) -func getEnv(key, defaultValue string) string { - if value, exists := os.LookupEnv(key); exists { - return value - } - return defaultValue -} - -func getEnvAsBool(key string, defaultValue bool) bool { - if value, exists := os.LookupEnv(key); exists { - return value == "true" - } - return defaultValue -} - func GetConfigDir() string { home, err := os.UserHomeDir() if err != nil { diff --git a/src/config/env_config.go b/src/config/env_config.go new file mode 100644 index 0000000..b30779b --- /dev/null +++ b/src/config/env_config.go @@ -0,0 +1,34 @@ +package config + +import "os" + +const EnvPrefix string = "GRIMOIRE_" + +type EnvError struct { + Key string +} + +func (e *EnvError) Error() string { + return "environment variable not found: " + e.Key +} + +func getEnvAsString(key, defaultValue string) string { + if value, exists := os.LookupEnv(EnvPrefix + key); exists { + return value + } + return defaultValue +} + +func getEnvAsApiKey(key string) (string, error) { + if value, exists := os.LookupEnv(EnvPrefix + key); exists { + return value, nil + } + return "", &EnvError{key} +} + +func getEnvAsBool(key string, defaultValue bool) bool { + if value, exists := os.LookupEnv(EnvPrefix + key); exists { + return value == "true" + } + return defaultValue +} diff --git a/src/config/env_config_test.go b/src/config/env_config_test.go new file mode 100644 index 0000000..d984b52 --- /dev/null +++ b/src/config/env_config_test.go @@ -0,0 +1,51 @@ +package config + +import ( + "os" + "testing" +) + +func TestGetEnvAsString(t *testing.T) { + os.Setenv("GRIMOIRE_TEST_STRING", "test_value") + defer os.Unsetenv("GRIMOIRE_TEST_STRING") + + value := getEnvAsString("TEST_STRING", "default_value") + if value != "test_value" { + t.Errorf("Expected 'test_value', got '%s'", value) + } + + value = getEnvAsString("NON_EXISTENT_KEY", "default_value") + if value != "default_value" { + t.Errorf("Expected 'default_value', got '%s'", value) + } +} + +func TestGetEnvAsApiKey(t *testing.T) { + os.Setenv("GRIMOIRE_TEST_API_KEY", "api_key_value") + defer os.Unsetenv("GRIMOIRE_TEST_API_KEY") + + value, err := getEnvAsApiKey("TEST_API_KEY") + if err != nil || value != "api_key_value" { + t.Errorf("Expected 'api_key_value', got '%s' with error '%v'", value, err) + } + + value, err = getEnvAsApiKey("NON_EXISTENT_KEY") + if err == nil || value != "" { + t.Errorf("Expected error and empty value, got '%s' with error '%v'", value, err) + } +} + +func TestGetEnvAsBool(t *testing.T) { + os.Setenv("GRIMOIRE_TEST_BOOL", "true") + defer os.Unsetenv("GRIMOIRE_TEST_BOOL") + + value := getEnvAsBool("TEST_BOOL", false) + if value != true { + t.Errorf("Expected 'true', got '%v'", value) + } + + value = getEnvAsBool("NON_EXISTENT_KEY", false) + if value != false { + t.Errorf("Expected 'false', got '%v'", value) + } +} diff --git a/src/interfaces/generate.go b/src/interfaces/generate.go new file mode 100644 index 0000000..64d8dbf --- /dev/null +++ b/src/interfaces/generate.go @@ -0,0 +1,6 @@ +package interfaces + +type Generate interface { + Generate(prompt string) (string, error) + GenerateStream(prompt string) (<-chan string, <-chan error) +} diff --git a/src/internal/prompt/types.go b/src/internal/prompt/types.go deleted file mode 100644 index e75d261..0000000 --- a/src/internal/prompt/types.go +++ /dev/null @@ -1,30 +0,0 @@ -package prompt - -import ( - "time" -) - -// PromptMetadata represents the YAML front matter in prompt files -type PromptMetadata struct { - Title string `yaml:"title"` - Model string `yaml:"model"` - Description string `yaml:"description"` - Input string `yaml:"input"` - Output string `yaml:"output"` - Version string `yaml:"version"` - Updated time.Time `yaml:"updated"` - Author string `yaml:"author"` - Email string `yaml:"email"` - Tags []string `yaml:"tags"` -} - -type FileMetadata struct { - Name string `yaml:"name"` - Path string `yaml:"path"` -} - -type Prompt struct { - FileMetadata FileMetadata - PromptMetadata PromptMetadata - Content string -}