diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 000000000..6f9f00ff4 --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) diff --git a/api/pkg/apps/github.go b/api/pkg/apps/github.go index 40c0ebf50..6f8690254 100644 --- a/api/pkg/apps/github.go +++ b/api/pkg/apps/github.go @@ -2,7 +2,6 @@ package apps import ( "fmt" - "io" "os" "path" "strings" @@ -45,13 +44,13 @@ func NewGithubApp(options GithubAppOptions) (*GithubApp, error) { return nil, fmt.Errorf("invalid repo name") } if options.Client == nil { - return nil, fmt.Errorf("Client is required") + return nil, fmt.Errorf("client is required") } if options.ToolsPlanner == nil { return nil, fmt.Errorf("ToolsPlanner is required") } if options.App == nil { - return nil, fmt.Errorf("App struct is required") + return nil, fmt.Errorf("app struct is required") } if options.UpdateApp == nil { return nil, fmt.Errorf("UpdateApp function is required") @@ -281,180 +280,16 @@ func (githubApp *GithubApp) GetConfig() (*types.AppHelixConfig, error) { } func (githubApp *GithubApp) processConfig(config *types.AppHelixConfig) (*types.AppHelixConfig, error) { - if config.Assistants == nil { - config.Assistants = []types.AssistantConfig{} - } - - for i, assistant := range config.Assistants { - if assistant.APIs == nil { - config.Assistants[i].APIs = []types.AssistantAPI{} - } - if assistant.GPTScripts == nil { - config.Assistants[i].GPTScripts = []types.AssistantGPTScript{} - } - - var ( - newTools []*types.Tool - newScripts []types.AssistantGPTScript - ) - - // scripts means you can configure the GPTScript contents inline with the helix.yaml - for _, script := range assistant.GPTScripts { - if script.File != "" { - expandedFiles, err := system.ExpandAndCheckFiles(githubApp.Filepath(""), []string{script.File}) - if err != nil { - return nil, err - } - for _, filepath := range expandedFiles { - content, err := os.ReadFile(filepath) - if err != nil { - return nil, err - } - file := githubApp.RelativePath(filepath) - newScripts = append(newScripts, types.AssistantGPTScript{ - Name: file, - File: file, - Content: string(content), - Description: script.Description, - }) - newTools = append(newTools, &types.Tool{ - ID: system.GenerateToolID(), - Name: file, - ToolType: types.ToolTypeGPTScript, - Config: types.ToolConfig{ - GPTScript: &types.ToolGPTScriptConfig{ - Script: string(content), - }, - }, - Created: time.Now(), - Updated: time.Now(), - }) - } - } else { - if script.Content == "" { - return nil, fmt.Errorf("gpt script %s has no content", script.Name) - } - newScripts = append(newScripts, script) - newTools = append(newTools, &types.Tool{ - ID: system.GenerateToolID(), - Name: script.Name, - Description: script.Description, - ToolType: types.ToolTypeGPTScript, - Config: types.ToolConfig{ - GPTScript: &types.ToolGPTScriptConfig{ - Script: script.Content, - }, - }, - Created: time.Now(), - Updated: time.Now(), - }) - } - } - - newAPIs := []types.AssistantAPI{} - - for _, api := range assistant.APIs { - if api.Schema == "" { - return nil, fmt.Errorf("api %s has no schema", api.Name) - } - - processedSchema, err := githubApp.processApiSchema(api.Schema) - if err != nil { - return nil, err - } - - api.Schema = processedSchema - - if api.Headers == nil { - api.Headers = map[string]string{} - } - - if api.Query == nil { - api.Query = map[string]string{} - } - - newTools = append(newTools, &types.Tool{ - ID: system.GenerateToolID(), - Created: time.Now(), - Updated: time.Now(), - Name: api.Name, - Description: api.Description, - ToolType: types.ToolTypeAPI, - Config: types.ToolConfig{ - API: &types.ToolApiConfig{ - URL: api.URL, - Schema: api.Schema, - Headers: api.Headers, - Query: api.Query, - RequestPrepTemplate: api.RequestPrepTemplate, - ResponseSuccessTemplate: api.ResponseSuccessTemplate, - ResponseErrorTemplate: api.ResponseErrorTemplate, - Model: assistant.Model, - }, - }, - }) - } - - for _, zapier := range assistant.Zapier { - newTools = append(newTools, &types.Tool{ - ID: system.GenerateToolID(), - Created: time.Now(), - Updated: time.Now(), - Name: zapier.Name, - Description: zapier.Description, - ToolType: types.ToolTypeZapier, - Config: types.ToolConfig{ - Zapier: &types.ToolZapierConfig{ - APIKey: zapier.APIKey, - Model: zapier.Model, - MaxIterations: zapier.MaxIterations, - }, - }, - }) - } - - for i := range newTools { - err := tools.ValidateTool(newTools[i], githubApp.ToolsPlanner, false) - if err != nil { - return nil, err - } - } - - config.Assistants[i].GPTScripts = newScripts - config.Assistants[i].APIs = newAPIs - config.Assistants[i].Tools = newTools + // Process any file references relative to the repo root + err := processLocalFiles(config, githubApp.Filepath("")) + if err != nil { + return nil, fmt.Errorf("error processing repo files: %w", err) } return config, nil } -func (githubApp *GithubApp) processApiSchema(schema string) (string, error) { - if strings.HasPrefix(strings.ToLower(schema), "http://") || strings.HasPrefix(strings.ToLower(schema), "https://") { - client := system.NewRetryClient(3) - resp, err := client.Get(schema) - if err != nil { - return "", fmt.Errorf("failed to get schema from URL: %w", err) - } - defer resp.Body.Close() - bts, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response body: %w", err) - } - return string(bts), nil - } - - // if the schema is only one line then assume it's a file path - if !strings.Contains(schema, "\n") && !strings.Contains(schema, "\r") { - // it must be a YAML file - if !strings.HasSuffix(schema, ".yaml") && !strings.HasSuffix(schema, ".yml") { - return "", fmt.Errorf("schema must be in yaml format") - } - content, err := os.ReadFile(githubApp.Filepath(schema)) - if err != nil { - return "", fmt.Errorf("failed to read schema file: %w", err) - } - return string(content), nil - } - - return schema, nil +// Add this method to implement FilePathResolver +func (g *GithubApp) ResolvePath(path string) string { + return g.Filepath(path) } diff --git a/api/pkg/apps/local.go b/api/pkg/apps/local.go index 4ebf48590..5ffb19cca 100644 --- a/api/pkg/apps/local.go +++ b/api/pkg/apps/local.go @@ -2,12 +2,9 @@ package apps import ( "fmt" - "io" "os" "path/filepath" - "strings" - "github.com/helixml/helix/api/pkg/system" "github.com/helixml/helix/api/pkg/types" "gopkg.in/yaml.v2" ) @@ -26,7 +23,6 @@ func NewLocalApp(filename string) (*LocalApp, error) { if os.IsNotExist(err) { return nil, fmt.Errorf("file %s does not exist", filename) } - return nil, fmt.Errorf("error checking if file %s exists: %w", filename, err) } @@ -37,109 +33,16 @@ func NewLocalApp(filename string) (*LocalApp, error) { } // Parse the yaml + // this will handle both AppHelixConfig & AppHelixConfigCRD app, err := processConfig(yamlFile) if err != nil { return nil, fmt.Errorf("error processing config file %s: %w", filename, err) } - var ( - apiTools []*types.Tool - gptScripts []*types.Tool - zapier []*types.Tool - ) - - // TODO: don't throw away the apis and gptscripts fields (here and in github - // apps), make tools an internal implementation detail, see - // https://github.com/helixml/helix/issues/544 - - for idx, assistant := range app.Assistants { - for _, api := range assistant.APIs { - schema, err := processApiSchema(filename, api.Schema) - if err != nil { - return nil, fmt.Errorf("error processing assistant %s api schema: %w", assistant.ID, err) - } - apiTools = append(apiTools, &types.Tool{ - Name: api.Name, - Description: api.Description, - ToolType: types.ToolTypeAPI, - Config: types.ToolConfig{ - API: &types.ToolApiConfig{ - URL: api.URL, - Schema: schema, - Headers: api.Headers, - Query: api.Query, - RequestPrepTemplate: api.RequestPrepTemplate, - ResponseSuccessTemplate: api.ResponseSuccessTemplate, - ResponseErrorTemplate: api.ResponseErrorTemplate, - Model: assistant.Model, - }, - }, - }) - } - - for _, assistantZapier := range assistant.Zapier { - zapier = append(zapier, &types.Tool{ - Name: assistantZapier.Name, - Description: assistantZapier.Description, - ToolType: types.ToolTypeZapier, - Config: types.ToolConfig{ - Zapier: &types.ToolZapierConfig{ - APIKey: assistantZapier.APIKey, - Model: assistantZapier.Model, - MaxIterations: assistantZapier.MaxIterations, - }, - }, - }) - } - - for _, script := range assistant.GPTScripts { - switch { - case script.Content != "": - // Load directly - gptScripts = append(gptScripts, &types.Tool{ - Name: getNameGptScriptName(script.Name, script.File), - ToolType: types.ToolTypeGPTScript, - Config: types.ToolConfig{ - GPTScript: &types.ToolGPTScriptConfig{ - Script: script.Content, - }, - }, - }) - case script.File != "": - // Load from file(s), this can contain a glob pattern - // such as gptscripts/*.gpt which will load all .gpt files in the directory - - // Use the config path to find the script file - scriptFile := filepath.Join(filepath.Dir(filename), script.File) - // Use the glob pattern to find all files - files, err := filepath.Glob(scriptFile) - if err != nil { - return nil, fmt.Errorf("error globbing file %s: %w", script.File, err) - } - - for _, file := range files { - content, err := os.ReadFile(file) - if err != nil { - return nil, fmt.Errorf("error reading file %s: %w", file, err) - } - - gptScripts = append(gptScripts, &types.Tool{ - Name: getNameGptScriptName(script.Name, file), - Description: script.Description, - ToolType: types.ToolTypeGPTScript, - Config: types.ToolConfig{ - GPTScript: &types.ToolGPTScriptConfig{ - Script: string(content), - }, - }, - }) - } - } - } - - app.Assistants[idx].Tools = apiTools - app.Assistants[idx].Tools = append(app.Assistants[idx].Tools, zapier...) - app.Assistants[idx].Tools = append(app.Assistants[idx].Tools, gptScripts...) + // Process any file references relative to the config file's directory + err = processLocalFiles(app, filepath.Dir(filename)) + if err != nil { + return nil, fmt.Errorf("error processing local files: %w", err) } return &LocalApp{ @@ -152,54 +55,39 @@ func (a *LocalApp) GetAppConfig() *types.AppHelixConfig { return a.app } -func processConfig(yamlFile []byte) (*types.AppHelixConfig, error) { - var app types.AppHelixConfig - err := yaml.Unmarshal(yamlFile, &app) - if err != nil { - return nil, fmt.Errorf("error parsing yaml file: %w", err) - } - - return &app, nil +// Add this method to implement FilePathResolver +func (a *LocalApp) ResolvePath(path string) string { + return filepath.Join(filepath.Dir(a.filename), path) } -func processApiSchema(configPath, schemaPath string) (string, error) { - if strings.HasPrefix(strings.ToLower(schemaPath), "http://") || strings.HasPrefix(strings.ToLower(schemaPath), "https://") { - client := system.NewRetryClient(3) - resp, err := client.Get(schemaPath) - if err != nil { - return "", fmt.Errorf("failed to get schema from URL: %w", err) - } - defer resp.Body.Close() - bts, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response body: %w", err) - } - return string(bts), nil +func processConfig(yamlFile []byte) (*types.AppHelixConfig, error) { + // First, unmarshal as generic map to check structure + var rawMap map[string]interface{} + if err := yaml.Unmarshal(yamlFile, &rawMap); err != nil { + return nil, fmt.Errorf("failed to parse YAML: %w", err) } - // if the schema is only one line then assume it's a file path - if !strings.Contains(schemaPath, "\n") && !strings.Contains(schemaPath, "\r") { - // it must be a YAML file - if !strings.HasSuffix(schemaPath, ".yaml") && !strings.HasSuffix(schemaPath, ".yml") { - return "", fmt.Errorf("schema must be in yaml format") - } + // Check if it has the CRD structure + _, hasApiVersion := rawMap["apiVersion"] + _, hasKind := rawMap["kind"] + _, hasSpec := rawMap["spec"] - // Find schemaFile relative to the configPath - schemaPath = filepath.Join(filepath.Dir(configPath), schemaPath) + isCRD := hasApiVersion && hasKind && hasSpec - content, err := os.ReadFile(schemaPath) - if err != nil { - return "", fmt.Errorf("failed to read schema file: %w", err) + if isCRD { + // If it looks like a CRD, we must treat it as one + var crd types.AppHelixConfigCRD + if err := yaml.Unmarshal(yamlFile, &crd); err != nil { + return nil, fmt.Errorf("file appears to be a CRD but failed to parse: %w", err) } - return string(content), nil + return &crd.Spec, nil } - return schemaPath, nil -} - -func getNameGptScriptName(name, filename string) string { - if name != "" { - return name + // Not a CRD, try to unmarshal as AppHelixConfig + var config types.AppHelixConfig + if err := yaml.Unmarshal(yamlFile, &config); err != nil { + return nil, fmt.Errorf("error parsing yaml file as AppHelixConfig: %w", err) } - return filepath.Base(filename) + + return &config, nil } diff --git a/api/pkg/apps/local_test.go b/api/pkg/apps/local_test.go new file mode 100644 index 000000000..ff383e4c6 --- /dev/null +++ b/api/pkg/apps/local_test.go @@ -0,0 +1,129 @@ +package apps + +import ( + "os" + "path/filepath" + "testing" + + "github.com/helixml/helix/api/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewLocalApp(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "localapp-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + testCases := []struct { + name string + yamlData string + validate func(*testing.T, *types.AppHelixConfig) + }{ + { + name: "api tools defined in assistant.APIs", + yamlData: ` +assistants: +- id: test-assistant + name: Test Assistant + model: gpt-4 + apis: + - name: test-api + description: Test API + url: http://example.com/api + schema: | + openapi: 3.0.0 + info: + title: Test API + version: 1.0.0 + headers: + Authorization: Bearer test +`, + validate: func(t *testing.T, config *types.AppHelixConfig) { + require.Len(t, config.Assistants, 1) + assistant := config.Assistants[0] + + // APIs should be present as defined in YAML + require.Len(t, assistant.APIs, 1) + api := assistant.APIs[0] + assert.Equal(t, "test-api", api.Name) + assert.Equal(t, "http://example.com/api", api.URL) + + // Tools should be empty since none were defined + assert.Empty(t, assistant.Tools) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Write test YAML to temporary file + yamlPath := filepath.Join(tmpDir, "test.yaml") + err := os.WriteFile(yamlPath, []byte(tc.yamlData), 0644) + require.NoError(t, err) + + // Create LocalApp + localApp, err := NewLocalApp(yamlPath) + require.NoError(t, err) + + // Validate the config + tc.validate(t, localApp.GetAppConfig()) + }) + } +} + +func TestLocalAppWithSchemaFile(t *testing.T) { + // Create a temporary directory for our test files + tmpDir, err := os.MkdirTemp("", "helix-test-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Create the schema file + schemaContent := `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + summary: Test endpoint + responses: + '200': + description: OK` + + schemaPath := filepath.Join(tmpDir, "test-schema.yaml") + err = os.WriteFile(schemaPath, []byte(schemaContent), 0644) + if err != nil { + t.Fatal(err) + } + + // Create the helix config file that references the schema + configContent := `assistants: +- id: test-assistant + name: Test Assistant + model: gpt-4 + apis: + - name: test-api + description: A test API + url: http://test-api + schema: test-schema.yaml` + + configPath := filepath.Join(tmpDir, "helix.yaml") + err = os.WriteFile(configPath, []byte(configContent), 0644) + if err != nil { + t.Fatal(err) + } + + // Load and test the config + app, err := NewLocalApp(configPath) + assert.NoError(t, err) + assert.NotNil(t, app) + + // Verify the schema was loaded correctly + assert.Len(t, app.app.Assistants, 1) + assert.Len(t, app.app.Assistants[0].APIs, 1) + assert.Equal(t, schemaContent, app.app.Assistants[0].APIs[0].Schema) +} diff --git a/api/pkg/apps/process_files.go b/api/pkg/apps/process_files.go new file mode 100644 index 000000000..77009b9c2 --- /dev/null +++ b/api/pkg/apps/process_files.go @@ -0,0 +1,126 @@ +package apps + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/helixml/helix/api/pkg/system" + "github.com/helixml/helix/api/pkg/types" +) + +// processLocalFiles takes an AppHelixConfig and a base directory path, +// and processes any file references (GPTScripts and API schemas) relative to that directory, +// loading the contents of the files into the config. +func processLocalFiles(config *types.AppHelixConfig, basePath string) error { + if config.Assistants == nil { + config.Assistants = []types.AssistantConfig{} + } + + for i := range config.Assistants { + assistant := &config.Assistants[i] + + // Initialize empty slices + if assistant.APIs == nil { + assistant.APIs = []types.AssistantAPI{} + } + if assistant.GPTScripts == nil { + assistant.GPTScripts = []types.AssistantGPTScript{} + } + + // Process GPTScripts + var newScripts []types.AssistantGPTScript + for _, script := range assistant.GPTScripts { + if script.File != "" { + // Load script from file(s), this can contain a glob pattern + scriptPath := filepath.Join(basePath, script.File) + expandedFiles, err := filepath.Glob(scriptPath) + if err != nil { + return fmt.Errorf("error globbing file %s: %w", script.File, err) + } + + for _, file := range expandedFiles { + content, err := os.ReadFile(file) + if err != nil { + return fmt.Errorf("error reading file %s: %w", file, err) + } + + name := script.Name + if name == "" { + name = filepath.Base(file) + } + + newScripts = append(newScripts, types.AssistantGPTScript{ + Name: name, + File: file, + Content: string(content), + Description: script.Description, + }) + } + } else { + if script.Content == "" { + return fmt.Errorf("gpt script %s has no content", script.Name) + } + newScripts = append(newScripts, script) + } + } + assistant.GPTScripts = newScripts + + // Process API schemas + for j, api := range assistant.APIs { + if api.Schema == "" { + return fmt.Errorf("api %s has no schema", api.Name) + } + + schema, err := processSchemaContent(api.Schema, basePath) + if err != nil { + return fmt.Errorf("error processing assistant %s api schema: %w", assistant.ID, err) + } + assistant.APIs[j].Schema = schema + + if api.Headers == nil { + assistant.APIs[j].Headers = map[string]string{} + } + if api.Query == nil { + assistant.APIs[j].Query = map[string]string{} + } + } + } + + return nil +} + +func processSchemaContent(schemaPath, basePath string) (string, error) { + if strings.HasPrefix(strings.ToLower(schemaPath), "http://") || strings.HasPrefix(strings.ToLower(schemaPath), "https://") { + client := system.NewRetryClient(3) + resp, err := client.Get(schemaPath) + if err != nil { + return "", fmt.Errorf("failed to get schema from URL: %w", err) + } + defer resp.Body.Close() + bts, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + return string(bts), nil + } + + // if the schema is only one line then assume it's a file path + if !strings.Contains(schemaPath, "\n") && !strings.Contains(schemaPath, "\r") { + // it must be a YAML file + if !strings.HasSuffix(schemaPath, ".yaml") && !strings.HasSuffix(schemaPath, ".yml") { + return "", fmt.Errorf("schema must be in yaml format") + } + + fullPath := filepath.Join(basePath, schemaPath) + content, err := os.ReadFile(fullPath) + if err != nil { + return "", fmt.Errorf("failed to read schema file: %w", err) + } + return string(content), nil + } + + return schemaPath, nil +} diff --git a/api/pkg/client/app.go b/api/pkg/client/app.go index fccaf5a62..6078d95db 100644 --- a/api/pkg/client/app.go +++ b/api/pkg/client/app.go @@ -3,11 +3,13 @@ package client import ( "bytes" "encoding/json" + "fmt" "net/http" "net/url" "strconv" "github.com/helixml/helix/api/pkg/types" + "github.com/rs/zerolog/log" ) type AppFilter struct { @@ -73,3 +75,25 @@ func (c *HelixClient) DeleteApp(appID string, deleteKnowledge bool) error { return nil } + +// TODO: optimize this to not list all apps and instead use a server side filter +func (c *HelixClient) GetAppByName(name string) (*types.App, error) { + log.Debug().Str("name", name).Msg("getting app by name") + + apps, err := c.ListApps(nil) + if err != nil { + log.Error().Err(err).Str("name", name).Msg("failed to list apps") + return nil, err + } + + log.Debug().Int("total_apps", len(apps)).Msg("searching through apps") + for _, app := range apps { + if app.Config.Helix.Name == name { + log.Debug().Str("name", name).Str("id", app.ID).Msg("found matching app") + return app, nil + } + } + + log.Debug().Str("name", name).Msg("app not found") + return nil, fmt.Errorf("app with name %s not found", name) +} diff --git a/api/pkg/client/client.go b/api/pkg/client/client.go index e4ea2a146..6b36e4246 100644 --- a/api/pkg/client/client.go +++ b/api/pkg/client/client.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "time" @@ -84,7 +85,24 @@ func (c *HelixClient) makeRequest(method, path string, body io.Reader, v interfa ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - req, err := http.NewRequestWithContext(ctx, method, c.url+path, body) + fullURL := c.url + path + if os.Getenv("DEBUG") != "" { + fmt.Printf("Making request to Helix API: %s %s\n", method, fullURL) + } + + // Read and store body content for curl logging + var bodyBytes []byte + if body != nil { + var err error + bodyBytes, err = io.ReadAll(body) + if err != nil { + return err + } + // Create new reader from bytes for the actual request + body = strings.NewReader(string(bodyBytes)) + } + + req, err := http.NewRequestWithContext(ctx, method, fullURL, body) if err != nil { return err } @@ -92,6 +110,20 @@ func (c *HelixClient) makeRequest(method, path string, body io.Reader, v interfa req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.apiKey) + if os.Getenv("DEBUG") != "" { + // Build curl command + curlCmd := fmt.Sprintf("curl -X %s '%s'", method, fullURL) + for key, values := range req.Header { + for _, value := range values { + curlCmd += fmt.Sprintf(" -H '%s: %s'", key, value) + } + } + if len(bodyBytes) > 0 { + curlCmd += fmt.Sprintf(" --data-raw '%s'", string(bodyBytes)) + } + fmt.Printf("Equivalent curl command:\n%s\n", curlCmd) + } + resp, err := c.httpClient.Do(req) if err != nil { return err diff --git a/api/pkg/controller/inference.go b/api/pkg/controller/inference.go index 2bf23346f..7dff69adb 100644 --- a/api/pkg/controller/inference.go +++ b/api/pkg/controller/inference.go @@ -365,7 +365,11 @@ func (c *Controller) loadAssistant(ctx context.Context, user *types.User, opts * return &types.AssistantConfig{}, nil } - app, err := c.Options.Store.GetApp(ctx, opts.AppID) + // TODO: change GetAppWithTools to GetApp when we've updated all inference + // code to use apis, gptscripts, and zapier fields directly. Meanwhile, the + // flattened tools list is the internal only representation, and should not + // be exposed to the user. + app, err := c.Options.Store.GetAppWithTools(ctx, opts.AppID) if err != nil { return nil, fmt.Errorf("error getting app: %w", err) } diff --git a/api/pkg/controller/inference_test.go b/api/pkg/controller/inference_test.go index 8a7029428..352e98bc8 100644 --- a/api/pkg/controller/inference_test.go +++ b/api/pkg/controller/inference_test.go @@ -151,7 +151,7 @@ func (suite *ControllerSuite) Test_BasicInferenceWithKnowledge() { }, } - suite.store.EXPECT().GetApp(suite.ctx, "app_id").Return(app, nil) + suite.store.EXPECT().GetAppWithTools(suite.ctx, "app_id").Return(app, nil) suite.store.EXPECT().ListSecrets(gomock.Any(), &store.ListSecretsQuery{ Owner: suite.user.ID, }).Return([]*types.Secret{}, nil) diff --git a/api/pkg/controller/knowledge/crawler/crawler_mocks.go b/api/pkg/controller/knowledge/crawler/crawler_mocks.go index a06f96a5a..40544972a 100644 --- a/api/pkg/controller/knowledge/crawler/crawler_mocks.go +++ b/api/pkg/controller/knowledge/crawler/crawler_mocks.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: crawler.go +// +// Generated by this command: +// +// mockgen -source crawler.go -destination crawler_mocks.go -package crawler +// // Package crawler is a generated GoMock package. package crawler @@ -8,14 +13,15 @@ import ( context "context" reflect "reflect" - gomock "go.uber.org/mock/gomock" types "github.com/helixml/helix/api/pkg/types" + gomock "go.uber.org/mock/gomock" ) // MockCrawler is a mock of Crawler interface. type MockCrawler struct { ctrl *gomock.Controller recorder *MockCrawlerMockRecorder + isgomock struct{} } // MockCrawlerMockRecorder is the mock recorder for MockCrawler. @@ -45,7 +51,7 @@ func (m *MockCrawler) Crawl(ctx context.Context) ([]*types.CrawledDocument, erro } // Crawl indicates an expected call of Crawl. -func (mr *MockCrawlerMockRecorder) Crawl(ctx interface{}) *gomock.Call { +func (mr *MockCrawlerMockRecorder) Crawl(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Crawl", reflect.TypeOf((*MockCrawler)(nil).Crawl), ctx) } diff --git a/api/pkg/controller/sessions.go b/api/pkg/controller/sessions.go index 3f895fa05..3ce926380 100644 --- a/api/pkg/controller/sessions.go +++ b/api/pkg/controller/sessions.go @@ -558,7 +558,7 @@ func (c *Controller) checkForActions(session *types.Session) (*types.Session, er // if this session is spawned from an app then we populate the list of tools from the app rather than the linked // database record if session.ParentApp != "" { - app, err := c.Options.Store.GetApp(ctx, session.ParentApp) + app, err := c.Options.Store.GetAppWithTools(ctx, session.ParentApp) if err != nil { return nil, fmt.Errorf("error getting app: %w", err) } diff --git a/api/pkg/controller/tool_actions.go b/api/pkg/controller/tool_actions.go index 384a3e0eb..a7ccd2f8e 100644 --- a/api/pkg/controller/tool_actions.go +++ b/api/pkg/controller/tool_actions.go @@ -26,7 +26,7 @@ func (c *Controller) runActionInteraction(ctx context.Context, session *types.Se } if session.ParentApp != "" { - app, err := c.Options.Store.GetApp(ctx, session.ParentApp) + app, err := c.Options.Store.GetAppWithTools(ctx, session.ParentApp) if err != nil { return nil, fmt.Errorf("failed to get app %s: %w", session.ParentApp, err) } diff --git a/api/pkg/extract/extractor_mocks.go b/api/pkg/extract/extractor_mocks.go index 8c2c480d0..8a0b5008b 100644 --- a/api/pkg/extract/extractor_mocks.go +++ b/api/pkg/extract/extractor_mocks.go @@ -20,6 +20,7 @@ import ( type MockExtractor struct { ctrl *gomock.Controller recorder *MockExtractorMockRecorder + isgomock struct{} } // MockExtractorMockRecorder is the mock recorder for MockExtractor. diff --git a/api/pkg/filestore/filestore_mocks.go b/api/pkg/filestore/filestore_mocks.go index 9fdba6df3..292cd594d 100644 --- a/api/pkg/filestore/filestore_mocks.go +++ b/api/pkg/filestore/filestore_mocks.go @@ -21,6 +21,7 @@ import ( type MockFileStore struct { ctrl *gomock.Controller recorder *MockFileStoreMockRecorder + isgomock struct{} } // MockFileStoreMockRecorder is the mock recorder for MockFileStore. diff --git a/api/pkg/gptscript/gptscript_mocks.go b/api/pkg/gptscript/gptscript_mocks.go index 23417bfe5..b64a5a7f1 100644 --- a/api/pkg/gptscript/gptscript_mocks.go +++ b/api/pkg/gptscript/gptscript_mocks.go @@ -21,6 +21,7 @@ import ( type MockExecutor struct { ctrl *gomock.Controller recorder *MockExecutorMockRecorder + isgomock struct{} } // MockExecutorMockRecorder is the mock recorder for MockExecutor. diff --git a/api/pkg/model/types_mocks.go b/api/pkg/model/types_mocks.go index db5ca61f0..44264ed3d 100644 --- a/api/pkg/model/types_mocks.go +++ b/api/pkg/model/types_mocks.go @@ -22,6 +22,7 @@ import ( type MockModel struct { ctrl *gomock.Controller recorder *MockModelMockRecorder + isgomock struct{} } // MockModelMockRecorder is the mock recorder for MockModel. @@ -134,6 +135,7 @@ func (mr *MockModelMockRecorder) PrepareFiles(session, isInitialSession, fileMan type MockModelSessionFileManager struct { ctrl *gomock.Controller recorder *MockModelSessionFileManagerMockRecorder + isgomock struct{} } // MockModelSessionFileManagerMockRecorder is the mock recorder for MockModelSessionFileManager. diff --git a/api/pkg/openai/manager/manager_mocks.go b/api/pkg/openai/manager/manager_mocks.go index af74d63c6..cff631acd 100644 --- a/api/pkg/openai/manager/manager_mocks.go +++ b/api/pkg/openai/manager/manager_mocks.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: provider_manager.go +// +// Generated by this command: +// +// mockgen -source provider_manager.go -destination manager_mocks.go -package manager +// // Package manager is a generated GoMock package. package manager @@ -17,6 +22,7 @@ import ( type MockProviderManager struct { ctrl *gomock.Controller recorder *MockProviderManagerMockRecorder + isgomock struct{} } // MockProviderManagerMockRecorder is the mock recorder for MockProviderManager. diff --git a/api/pkg/openai/openai_client_mocks.go b/api/pkg/openai/openai_client_mocks.go index ecb46550a..04af819ff 100644 --- a/api/pkg/openai/openai_client_mocks.go +++ b/api/pkg/openai/openai_client_mocks.go @@ -22,6 +22,7 @@ import ( type MockClient struct { ctrl *gomock.Controller recorder *MockClientMockRecorder + isgomock struct{} } // MockClientMockRecorder is the mock recorder for MockClient. diff --git a/api/pkg/rag/rag_mocks.go b/api/pkg/rag/rag_mocks.go index 174020639..596dd96ce 100644 --- a/api/pkg/rag/rag_mocks.go +++ b/api/pkg/rag/rag_mocks.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: rag.go +// +// Generated by this command: +// +// mockgen -source rag.go -destination rag_mocks.go -package rag +// // Package rag is a generated GoMock package. package rag @@ -8,14 +13,15 @@ import ( context "context" reflect "reflect" - gomock "go.uber.org/mock/gomock" types "github.com/helixml/helix/api/pkg/types" + gomock "go.uber.org/mock/gomock" ) // MockRAG is a mock of RAG interface. type MockRAG struct { ctrl *gomock.Controller recorder *MockRAGMockRecorder + isgomock struct{} } // MockRAGMockRecorder is the mock recorder for MockRAG. @@ -44,7 +50,7 @@ func (m *MockRAG) Delete(ctx context.Context, req *types.DeleteIndexRequest) err } // Delete indicates an expected call of Delete. -func (mr *MockRAGMockRecorder) Delete(ctx, req interface{}) *gomock.Call { +func (mr *MockRAGMockRecorder) Delete(ctx, req any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRAG)(nil).Delete), ctx, req) } @@ -52,7 +58,7 @@ func (mr *MockRAGMockRecorder) Delete(ctx, req interface{}) *gomock.Call { // Index mocks base method. func (m *MockRAG) Index(ctx context.Context, req ...*types.SessionRAGIndexChunk) error { m.ctrl.T.Helper() - varargs := []interface{}{ctx} + varargs := []any{ctx} for _, a := range req { varargs = append(varargs, a) } @@ -62,9 +68,9 @@ func (m *MockRAG) Index(ctx context.Context, req ...*types.SessionRAGIndexChunk) } // Index indicates an expected call of Index. -func (mr *MockRAGMockRecorder) Index(ctx interface{}, req ...interface{}) *gomock.Call { +func (mr *MockRAGMockRecorder) Index(ctx any, req ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx}, req...) + varargs := append([]any{ctx}, req...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index", reflect.TypeOf((*MockRAG)(nil).Index), varargs...) } @@ -78,7 +84,7 @@ func (m *MockRAG) Query(ctx context.Context, q *types.SessionRAGQuery) ([]*types } // Query indicates an expected call of Query. -func (mr *MockRAGMockRecorder) Query(ctx, q interface{}) *gomock.Call { +func (mr *MockRAGMockRecorder) Query(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockRAG)(nil).Query), ctx, q) } diff --git a/api/pkg/runner/commander_mocks.go b/api/pkg/runner/commander_mocks.go index 162245783..1b8390c2d 100644 --- a/api/pkg/runner/commander_mocks.go +++ b/api/pkg/runner/commander_mocks.go @@ -21,6 +21,7 @@ import ( type MockCommander struct { ctrl *gomock.Controller recorder *MockCommanderMockRecorder + isgomock struct{} } // MockCommanderMockRecorder is the mock recorder for MockCommander. diff --git a/api/pkg/runner/model_instance_mocks.go b/api/pkg/runner/model_instance_mocks.go index 09459d205..039b72310 100644 --- a/api/pkg/runner/model_instance_mocks.go +++ b/api/pkg/runner/model_instance_mocks.go @@ -22,6 +22,7 @@ import ( type MockModelInstance struct { ctrl *gomock.Controller recorder *MockModelInstanceMockRecorder + isgomock struct{} } // MockModelInstanceMockRecorder is the mock recorder for MockModelInstance. diff --git a/api/pkg/runner/utils_mocks.go b/api/pkg/runner/utils_mocks.go index de9daf79d..66e4c87cd 100644 --- a/api/pkg/runner/utils_mocks.go +++ b/api/pkg/runner/utils_mocks.go @@ -19,6 +19,7 @@ import ( type MockFreePortFinder struct { ctrl *gomock.Controller recorder *MockFreePortFinderMockRecorder + isgomock struct{} } // MockFreePortFinderMockRecorder is the mock recorder for MockFreePortFinder. diff --git a/api/pkg/server/openai_chat_handlers.go b/api/pkg/server/openai_chat_handlers.go index 5da42bdf5..f70609d7d 100644 --- a/api/pkg/server/openai_chat_handlers.go +++ b/api/pkg/server/openai_chat_handlers.go @@ -215,7 +215,7 @@ func (s *HelixAPIServer) createChatCompletion(rw http.ResponseWriter, r *http.Re } func (s *HelixAPIServer) getAppLoraAssistant(ctx context.Context, appID string) (*types.AssistantConfig, error) { - app, err := s.Store.GetApp(ctx, appID) + app, err := s.Store.GetAppWithTools(ctx, appID) if err != nil { return nil, err } diff --git a/api/pkg/server/openai_chat_handlers_test.go b/api/pkg/server/openai_chat_handlers_test.go index 3e0096492..5b3218ee4 100644 --- a/api/pkg/server/openai_chat_handlers_test.go +++ b/api/pkg/server/openai_chat_handlers_test.go @@ -319,7 +319,7 @@ func (suite *OpenAIChatSuite) TestChatCompletions_App_Blocking() { }, } - suite.store.EXPECT().GetApp(gomock.Any(), "app123").Return(app, nil).Times(1) + suite.store.EXPECT().GetAppWithTools(gomock.Any(), "app123").Return(app, nil).Times(1) suite.store.EXPECT().ListSecrets(gomock.Any(), &store.ListSecretsQuery{ Owner: suite.userID, }).Return([]*types.Secret{}, nil) @@ -408,7 +408,7 @@ func (suite *OpenAIChatSuite) TestChatCompletions_App_HelixModel() { }, } - suite.store.EXPECT().GetApp(gomock.Any(), "app123").Return(app, nil).Times(1) + suite.store.EXPECT().GetAppWithTools(gomock.Any(), "app123").Return(app, nil).Times(1) suite.store.EXPECT().ListSecrets(gomock.Any(), &store.ListSecretsQuery{ Owner: suite.userID, }).Return([]*types.Secret{}, nil) @@ -499,7 +499,7 @@ func (suite *OpenAIChatSuite) TestChatCompletions_AppRag_Blocking() { }, } - suite.store.EXPECT().GetApp(gomock.Any(), "app123").Return(app, nil).Times(1) + suite.store.EXPECT().GetAppWithTools(gomock.Any(), "app123").Return(app, nil).Times(1) suite.store.EXPECT().ListSecrets(gomock.Any(), &store.ListSecretsQuery{ Owner: suite.userID, }).Return([]*types.Secret{}, nil) @@ -623,7 +623,7 @@ func (suite *OpenAIChatSuite) TestChatCompletions_AppFromAuth_Blocking() { }, } - suite.store.EXPECT().GetApp(gomock.Any(), "app123").Return(app, nil).Times(2) + suite.store.EXPECT().GetAppWithTools(gomock.Any(), "app123").Return(app, nil).Times(2) suite.store.EXPECT().ListSecrets(gomock.Any(), &store.ListSecretsQuery{ Owner: suite.userID, }).Return([]*types.Secret{}, nil) @@ -719,7 +719,7 @@ func (suite *OpenAIChatSuite) TestChatCompletions_App_Streaming() { }, } - suite.store.EXPECT().GetApp(gomock.Any(), "app123").Return(app, nil).Times(1) + suite.store.EXPECT().GetAppWithTools(gomock.Any(), "app123").Return(app, nil).Times(1) suite.store.EXPECT().ListSecrets(gomock.Any(), &store.ListSecretsQuery{ Owner: suite.userID, }).Return([]*types.Secret{}, nil) diff --git a/api/pkg/server/session_handlers.go b/api/pkg/server/session_handlers.go index b877d221b..2e28edf67 100644 --- a/api/pkg/server/session_handlers.go +++ b/api/pkg/server/session_handlers.go @@ -76,7 +76,7 @@ func (s *HelixAPIServer) startChatSessionHandler(rw http.ResponseWriter, req *ht // the correct model in the UI (and some things may rely on it) if startReq.AppID != "" { // load the app - app, err := s.Store.GetApp(req.Context(), startReq.AppID) + app, err := s.Store.GetAppWithTools(req.Context(), startReq.AppID) if err != nil { log.Error().Err(err).Str("app_id", startReq.AppID).Msg("Failed to load app") http.Error(rw, "Failed to load app: "+err.Error(), http.StatusInternalServerError) diff --git a/api/pkg/server/session_legacy_handlers.go b/api/pkg/server/session_legacy_handlers.go index a66a2158d..bd5b4ddf4 100644 --- a/api/pkg/server/session_legacy_handlers.go +++ b/api/pkg/server/session_legacy_handlers.go @@ -116,7 +116,7 @@ func (s *HelixAPIServer) startChatSessionLegacyHandler(ctx context.Context, user // if we have an app then let's populate the InternalSessionRequest with values from it if newSession.ParentApp != "" { - app, err := s.Store.GetApp(ctx, appID) + app, err := s.Store.GetAppWithTools(ctx, appID) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return diff --git a/api/pkg/store/postgres.go b/api/pkg/store/postgres.go index b12b2618b..bd3bd22ab 100644 --- a/api/pkg/store/postgres.go +++ b/api/pkg/store/postgres.go @@ -220,7 +220,7 @@ func getValueIndexes(fields []string) string { for i := range fields { parts = append(parts, fmt.Sprintf("$%d", i+1)) } - return fmt.Sprintf("%s", strings.Join(parts, ", ")) + return strings.Join(parts, ", ") } // given an array of field names - return the indexes as an update @@ -231,7 +231,7 @@ func getKeyValueIndexes(fields []string, offset int) string { for i, field := range fields { parts = append(parts, fmt.Sprintf("%s = $%d", field, i+offset+1)) } - return fmt.Sprintf("%s", strings.Join(parts, ", ")) + return strings.Join(parts, ", ") } var USERMETA_FIELDS = []string{ @@ -288,17 +288,6 @@ func (d *PostgresStore) GetUserMeta( return scanUserMetaRow(row) } -func (d *PostgresStore) getSessionsWhere(query GetSessionsQuery) goqu.Ex { - where := goqu.Ex{} - if query.Owner != "" { - where["owner"] = query.Owner - } - if query.OwnerType != "" { - where["owner_type"] = query.OwnerType - } - return where -} - func (d *PostgresStore) CreateUserMeta( ctx context.Context, user types.UserMeta, diff --git a/api/pkg/store/store.go b/api/pkg/store/store.go index 175b210a4..f6c721adf 100644 --- a/api/pkg/store/store.go +++ b/api/pkg/store/store.go @@ -100,6 +100,7 @@ type Store interface { CreateApp(ctx context.Context, tool *types.App) (*types.App, error) UpdateApp(ctx context.Context, tool *types.App) (*types.App, error) GetApp(ctx context.Context, id string) (*types.App, error) + GetAppWithTools(ctx context.Context, id string) (*types.App, error) ListApps(ctx context.Context, q *ListAppsQuery) ([]*types.App, error) DeleteApp(ctx context.Context, id string) error diff --git a/api/pkg/store/store_apps.go b/api/pkg/store/store_apps.go index 2d6a6dde5..b39ff1b77 100644 --- a/api/pkg/store/store_apps.go +++ b/api/pkg/store/store_apps.go @@ -4,14 +4,98 @@ import ( "context" "errors" "fmt" - "sort" "time" + "github.com/getkin/kin-openapi/openapi3" "github.com/helixml/helix/api/pkg/system" "github.com/helixml/helix/api/pkg/types" "gorm.io/gorm" ) +// RectifyApp handles the migration of app configurations from the old format +// (which used both Tools and specific fields like APIs, GPTScripts, Zapier) to +// a new canonical AISpec compatible format where tools are only stored in their +// specific fields (APIs, GPTScripts, Zapier). +// +// This function: +// 1. Processes any tools found in the deprecated Tools field and converts them +// to their appropriate specific fields (APIs, GPTScripts, Zapier) +// 2. Handles deduplication by name - if a tool already exists in a specific +// field (e.g., in APIs), it won't be duplicated from the Tools field +// 3. Gives precedence to tools defined in their specific fields over those in +// the Tools field +// 4. Clears the Tools field after processing (as it's now deprecated) +// +// This allows us to handle old database records that might have tools defined +// in either or both places, while ensuring we move forward with a clean, +// consistent format where tools are only stored in their specific fields. +func RectifyApp(app *types.App) { + for i := range app.Config.Helix.Assistants { + assistant := &app.Config.Helix.Assistants[i] + + // Create maps to track existing tools by name + existingAPIs := make(map[string]bool) + existingGPTScripts := make(map[string]bool) + existingZapier := make(map[string]bool) + + // First mark all existing non-Tools items + for _, api := range assistant.APIs { + existingAPIs[api.Name] = true + } + for _, script := range assistant.GPTScripts { + existingGPTScripts[script.Name] = true + } + for _, zapier := range assistant.Zapier { + existingZapier[zapier.Name] = true + } + + // Convert tools to their appropriate fields + // but only if they don't already exist in the non-Tools fields + for _, tool := range assistant.Tools { + switch tool.ToolType { + case types.ToolTypeAPI: + if !existingAPIs[tool.Name] && tool.Config.API != nil { + assistant.APIs = append(assistant.APIs, types.AssistantAPI{ + Name: tool.Name, + Description: tool.Description, + URL: tool.Config.API.URL, + Schema: tool.Config.API.Schema, + Headers: tool.Config.API.Headers, + Query: tool.Config.API.Query, + RequestPrepTemplate: tool.Config.API.RequestPrepTemplate, + ResponseSuccessTemplate: tool.Config.API.ResponseSuccessTemplate, + ResponseErrorTemplate: tool.Config.API.ResponseErrorTemplate, + }) + existingAPIs[tool.Name] = true + } + case types.ToolTypeGPTScript: + if !existingGPTScripts[tool.Name] && tool.Config.GPTScript != nil { + assistant.GPTScripts = append(assistant.GPTScripts, types.AssistantGPTScript{ + Name: tool.Name, + Description: tool.Description, + Content: tool.Config.GPTScript.Script, + }) + existingGPTScripts[tool.Name] = true + } + case types.ToolTypeZapier: + if !existingZapier[tool.Name] && tool.Config.Zapier != nil { + assistant.Zapier = append(assistant.Zapier, types.AssistantZapier{ + Name: tool.Name, + Description: tool.Description, + APIKey: tool.Config.Zapier.APIKey, + Model: tool.Config.Zapier.Model, + MaxIterations: tool.Config.Zapier.MaxIterations, + }) + existingZapier[tool.Name] = true + } + } + } + + // Clear the tools field as it's now deprecated + assistant.Tools = nil + } +} + func (s *PostgresStore) CreateApp(ctx context.Context, app *types.App) (*types.App, error) { if app.ID == "" { app.ID = system.GenerateAppID() @@ -24,7 +108,7 @@ func (s *PostgresStore) CreateApp(ctx context.Context, app *types.App) (*types.A app.Created = time.Now() setAppDefaults(app) - sortAppTools(app) + RectifyApp(app) err := s.gdb.WithContext(ctx).Create(app).Error if err != nil { @@ -33,15 +117,6 @@ func (s *PostgresStore) CreateApp(ctx context.Context, app *types.App) (*types.A return s.GetApp(ctx, app.ID) } -func sortAppTools(app *types.App) { - for idx, assistant := range app.Config.Helix.Assistants { - sort.SliceStable(assistant.Tools, func(i, j int) bool { - return assistant.Tools[i].Name < assistant.Tools[j].Name - }) - app.Config.Helix.Assistants[idx] = assistant - } -} - func (s *PostgresStore) UpdateApp(ctx context.Context, app *types.App) (*types.App, error) { if app.ID == "" { return nil, fmt.Errorf("id not specified") @@ -53,7 +128,7 @@ func (s *PostgresStore) UpdateApp(ctx context.Context, app *types.App) (*types.A app.Updated = time.Now() - sortAppTools(app) + RectifyApp(app) err := s.gdb.WithContext(ctx).Save(&app).Error if err != nil { @@ -63,8 +138,8 @@ func (s *PostgresStore) UpdateApp(ctx context.Context, app *types.App) (*types.A } func (s *PostgresStore) GetApp(ctx context.Context, id string) (*types.App, error) { - var tool types.App - err := s.gdb.WithContext(ctx).Where("id = ?", id).First(&tool).Error + var app types.App + err := s.gdb.WithContext(ctx).Where("id = ?", id).First(&app).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrNotFound @@ -72,25 +147,177 @@ func (s *PostgresStore) GetApp(ctx context.Context, id string) (*types.App, erro return nil, err } - setAppDefaults(&tool) + setAppDefaults(&app) - return &tool, nil + // Check if any tools need to be rectified + hasTools := false + for _, assistant := range app.Config.Helix.Assistants { + if len(assistant.Tools) > 0 { + hasTools = true + break + } + } + + // If we found tools, rectify and save back to database + if hasTools { + RectifyApp(&app) + err = s.gdb.WithContext(ctx).Save(&app).Error + if err != nil { + return nil, fmt.Errorf("error saving rectified app: %w", err) + } + } + + return &app, nil +} + +// XXX Copy paste to avoid import cycle +func GetActionsFromSchema(spec string) ([]*types.ToolApiAction, error) { + loader := openapi3.NewLoader() + + schema, err := loader.LoadFromData([]byte(spec)) + if err != nil { + return nil, fmt.Errorf("failed to load openapi spec: %w", err) + } + + var actions []*types.ToolApiAction + + for path, pathItem := range schema.Paths.Map() { + + for method, operation := range pathItem.Operations() { + description := operation.Summary + if description == "" { + description = operation.Description + } + + if operation.OperationID == "" { + return nil, fmt.Errorf("operationId is missing for all %s %s", method, path) + } + + actions = append(actions, &types.ToolApiAction{ + Name: operation.OperationID, + Description: description, + Path: path, + Method: method, + }) + } + } + + return actions, nil +} + +// BACKWARD COMPATIBILITY ONLY: return an app with the apis, gptscripts, and zapier +// transformed into the deprecated (or at least internal) Tools field +func (s *PostgresStore) GetAppWithTools(ctx context.Context, id string) (*types.App, error) { + app, err := s.GetApp(ctx, id) + if err != nil { + return nil, err + } + + // Convert each assistant's specific tool fields into the deprecated Tools field + for i := range app.Config.Helix.Assistants { + assistant := &app.Config.Helix.Assistants[i] + var tools []*types.Tool + + // Convert APIs to Tools + for _, api := range assistant.APIs { + t := &types.Tool{ + Name: api.Name, + Description: api.Description, + ToolType: types.ToolTypeAPI, + Config: types.ToolConfig{ + API: &types.ToolApiConfig{ + URL: api.URL, + Schema: api.Schema, + Headers: api.Headers, + Query: api.Query, + RequestPrepTemplate: api.RequestPrepTemplate, + ResponseSuccessTemplate: api.ResponseSuccessTemplate, + ResponseErrorTemplate: api.ResponseErrorTemplate, + }, + }, + } + // FFS this doesn't belong here + t.Config.API.Actions, err = GetActionsFromSchema(api.Schema) + if err != nil { + return nil, fmt.Errorf("error getting actions from schema: %w", err) + } + tools = append(tools, t) + } + + // Convert Zapier to Tools + for _, zapier := range assistant.Zapier { + tools = append(tools, &types.Tool{ + Name: zapier.Name, + Description: zapier.Description, + ToolType: types.ToolTypeZapier, + Config: types.ToolConfig{ + Zapier: &types.ToolZapierConfig{ + APIKey: zapier.APIKey, + Model: zapier.Model, + MaxIterations: zapier.MaxIterations, + }, + }, + }) + } + + // Convert GPTScripts to Tools + for _, script := range assistant.GPTScripts { + tools = append(tools, &types.Tool{ + Name: script.Name, + Description: script.Description, + ToolType: types.ToolTypeGPTScript, + Config: types.ToolConfig{ + GPTScript: &types.ToolGPTScriptConfig{ + Script: script.Content, + }, + }, + }) + } + + assistant.Tools = tools + // empty out the canonical fields to avoid confusion. Callers of this + // function should ONLY use the internal Tools field + assistant.APIs = nil + assistant.GPTScripts = nil + assistant.Zapier = nil + } + + return app, nil } func (s *PostgresStore) ListApps(ctx context.Context, q *ListAppsQuery) ([]*types.App, error) { - var tools []*types.App + var apps []*types.App err := s.gdb.WithContext(ctx).Where(&types.App{ Owner: q.Owner, OwnerType: q.OwnerType, Global: q.Global, - }).Order("id DESC").Find(&tools).Error + }).Order("id DESC").Find(&apps).Error if err != nil { return nil, err } - setAppDefaults(tools...) + setAppDefaults(apps...) + + // Check and rectify any apps that have tools + for _, app := range apps { + hasTools := false + for _, assistant := range app.Config.Helix.Assistants { + if len(assistant.Tools) > 0 { + hasTools = true + break + } + } + + if hasTools { + RectifyApp(app) + err = s.gdb.WithContext(ctx).Save(app).Error + if err != nil { + return nil, fmt.Errorf("error saving rectified app: %w", err) + } + } + } - return tools, nil + return apps, nil } func (s *PostgresStore) DeleteApp(ctx context.Context, id string) error { diff --git a/api/pkg/store/store_apps_test.go b/api/pkg/store/store_apps_test.go index 4f1dc2ad1..0dc479d46 100644 --- a/api/pkg/store/store_apps_test.go +++ b/api/pkg/store/store_apps_test.go @@ -1,8 +1,12 @@ package store import ( + "testing" + "github.com/helixml/helix/api/pkg/system" "github.com/helixml/helix/api/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func (suite *PostgresStoreTestSuite) TestCreateApp() { @@ -114,3 +118,117 @@ func (suite *PostgresStoreTestSuite) TestDeleteApp() { suite.NoError(err) suite.Equal(0, len(tools)) } + +func (suite *PostgresStoreTestSuite) TestRectifyApp() { + testCases := []struct { + name string + app *types.App + validateAfter func(*testing.T, *types.App) + }{ + { + name: "convert tools to apis", + app: &types.App{ + Owner: "test-owner", + OwnerType: types.OwnerTypeUser, + Config: types.AppConfig{ + Helix: types.AppHelixConfig{ + Assistants: []types.AssistantConfig{ + { + ID: "test-assistant", + Name: "Test Assistant", + Model: "gpt-4", + Tools: []*types.Tool{ + { + Name: "test-api", + Description: "Test API", + ToolType: types.ToolTypeAPI, + Config: types.ToolConfig{ + API: &types.ToolApiConfig{ + URL: "http://example.com/api", + Schema: "openapi: 3.0.0\ninfo:\n title: Test API\n version: 1.0.0", + Headers: map[string]string{ + "Authorization": "Bearer test", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + validateAfter: func(t *testing.T, app *types.App) { + require.Len(t, app.Config.Helix.Assistants, 1) + assistant := app.Config.Helix.Assistants[0] + + // Tools should be empty after rectification + assert.Empty(t, assistant.Tools, "Tools should be empty after rectification") + + // APIs should contain the converted tool + require.Len(t, assistant.APIs, 1) + api := assistant.APIs[0] + assert.Equal(t, "test-api", api.Name) + assert.Equal(t, "http://example.com/api", api.URL) + assert.Equal(t, "Test API", api.Description) + }, + }, + { + name: "preserve existing apis", + app: &types.App{ + Owner: "test-owner", + OwnerType: types.OwnerTypeUser, + Config: types.AppConfig{ + Helix: types.AppHelixConfig{ + Assistants: []types.AssistantConfig{ + { + ID: "test-assistant", + Name: "Test Assistant", + Model: "gpt-4", + APIs: []types.AssistantAPI{ + { + Name: "existing-api", + Description: "Existing API", + URL: "http://example.com/existing", + Schema: "openapi: 3.0.0", + }, + }, + }, + }, + }, + }, + }, + validateAfter: func(t *testing.T, app *types.App) { + require.Len(t, app.Config.Helix.Assistants, 1) + assistant := app.Config.Helix.Assistants[0] + + // Tools should be empty + assert.Empty(t, assistant.Tools) + + // Existing API should be preserved + require.Len(t, assistant.APIs, 1) + api := assistant.APIs[0] + assert.Equal(t, "existing-api", api.Name) + assert.Equal(t, "http://example.com/existing", api.URL) + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Create the app + createdApp, err := suite.db.CreateApp(suite.ctx, tc.app) + suite.NoError(err) + suite.NotNil(createdApp) + + // Validate the rectified app + tc.validateAfter(suite.T(), createdApp) + + // Clean up + suite.T().Cleanup(func() { + err := suite.db.DeleteApp(suite.ctx, createdApp.ID) + suite.NoError(err) + }) + }) + } +} diff --git a/api/pkg/store/store_mocks.go b/api/pkg/store/store_mocks.go index 3c461f062..7a0922ace 100644 --- a/api/pkg/store/store_mocks.go +++ b/api/pkg/store/store_mocks.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: store.go +// +// Generated by this command: +// +// mockgen -source store.go -destination store_mocks.go -package store +// // Package store is a generated GoMock package. package store @@ -8,14 +13,15 @@ import ( context "context" reflect "reflect" - gomock "go.uber.org/mock/gomock" types "github.com/helixml/helix/api/pkg/types" + gomock "go.uber.org/mock/gomock" ) // MockStore is a mock of Store interface. type MockStore struct { ctrl *gomock.Controller recorder *MockStoreMockRecorder + isgomock struct{} } // MockStoreMockRecorder is the mock recorder for MockStore. @@ -45,7 +51,7 @@ func (m *MockStore) CreateAPIKey(ctx context.Context, apiKey *types.APIKey) (*ty } // CreateAPIKey indicates an expected call of CreateAPIKey. -func (mr *MockStoreMockRecorder) CreateAPIKey(ctx, apiKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateAPIKey(ctx, apiKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAPIKey", reflect.TypeOf((*MockStore)(nil).CreateAPIKey), ctx, apiKey) } @@ -60,7 +66,7 @@ func (m *MockStore) CreateApp(ctx context.Context, tool *types.App) (*types.App, } // CreateApp indicates an expected call of CreateApp. -func (mr *MockStoreMockRecorder) CreateApp(ctx, tool interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateApp(ctx, tool any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateApp", reflect.TypeOf((*MockStore)(nil).CreateApp), ctx, tool) } @@ -75,7 +81,7 @@ func (m *MockStore) CreateDataEntity(ctx context.Context, dataEntity *types.Data } // CreateDataEntity indicates an expected call of CreateDataEntity. -func (mr *MockStoreMockRecorder) CreateDataEntity(ctx, dataEntity interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateDataEntity(ctx, dataEntity any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDataEntity", reflect.TypeOf((*MockStore)(nil).CreateDataEntity), ctx, dataEntity) } @@ -90,7 +96,7 @@ func (m *MockStore) CreateKnowledge(ctx context.Context, knowledge *types.Knowle } // CreateKnowledge indicates an expected call of CreateKnowledge. -func (mr *MockStoreMockRecorder) CreateKnowledge(ctx, knowledge interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateKnowledge(ctx, knowledge any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateKnowledge", reflect.TypeOf((*MockStore)(nil).CreateKnowledge), ctx, knowledge) } @@ -105,7 +111,7 @@ func (m *MockStore) CreateKnowledgeVersion(ctx context.Context, version *types.K } // CreateKnowledgeVersion indicates an expected call of CreateKnowledgeVersion. -func (mr *MockStoreMockRecorder) CreateKnowledgeVersion(ctx, version interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateKnowledgeVersion(ctx, version any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateKnowledgeVersion", reflect.TypeOf((*MockStore)(nil).CreateKnowledgeVersion), ctx, version) } @@ -120,7 +126,7 @@ func (m *MockStore) CreateLLMCall(ctx context.Context, call *types.LLMCall) (*ty } // CreateLLMCall indicates an expected call of CreateLLMCall. -func (mr *MockStoreMockRecorder) CreateLLMCall(ctx, call interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateLLMCall(ctx, call any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLLMCall", reflect.TypeOf((*MockStore)(nil).CreateLLMCall), ctx, call) } @@ -135,7 +141,7 @@ func (m *MockStore) CreateScriptRun(ctx context.Context, task *types.ScriptRun) } // CreateScriptRun indicates an expected call of CreateScriptRun. -func (mr *MockStoreMockRecorder) CreateScriptRun(ctx, task interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateScriptRun(ctx, task any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateScriptRun", reflect.TypeOf((*MockStore)(nil).CreateScriptRun), ctx, task) } @@ -150,7 +156,7 @@ func (m *MockStore) CreateSecret(ctx context.Context, secret *types.Secret) (*ty } // CreateSecret indicates an expected call of CreateSecret. -func (mr *MockStoreMockRecorder) CreateSecret(ctx, secret interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateSecret(ctx, secret any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockStore)(nil).CreateSecret), ctx, secret) } @@ -165,7 +171,7 @@ func (m *MockStore) CreateSession(ctx context.Context, session types.Session) (* } // CreateSession indicates an expected call of CreateSession. -func (mr *MockStoreMockRecorder) CreateSession(ctx, session interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateSession(ctx, session any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockStore)(nil).CreateSession), ctx, session) } @@ -179,7 +185,7 @@ func (m *MockStore) CreateSessionToolBinding(ctx context.Context, sessionID, too } // CreateSessionToolBinding indicates an expected call of CreateSessionToolBinding. -func (mr *MockStoreMockRecorder) CreateSessionToolBinding(ctx, sessionID, toolID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateSessionToolBinding(ctx, sessionID, toolID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSessionToolBinding", reflect.TypeOf((*MockStore)(nil).CreateSessionToolBinding), ctx, sessionID, toolID) } @@ -194,7 +200,7 @@ func (m *MockStore) CreateTool(ctx context.Context, tool *types.Tool) (*types.To } // CreateTool indicates an expected call of CreateTool. -func (mr *MockStoreMockRecorder) CreateTool(ctx, tool interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateTool(ctx, tool any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTool", reflect.TypeOf((*MockStore)(nil).CreateTool), ctx, tool) } @@ -209,7 +215,7 @@ func (m *MockStore) CreateUserMeta(ctx context.Context, UserMeta types.UserMeta) } // CreateUserMeta indicates an expected call of CreateUserMeta. -func (mr *MockStoreMockRecorder) CreateUserMeta(ctx, UserMeta interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateUserMeta(ctx, UserMeta any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserMeta", reflect.TypeOf((*MockStore)(nil).CreateUserMeta), ctx, UserMeta) } @@ -223,7 +229,7 @@ func (m *MockStore) DeleteAPIKey(ctx context.Context, apiKey string) error { } // DeleteAPIKey indicates an expected call of DeleteAPIKey. -func (mr *MockStoreMockRecorder) DeleteAPIKey(ctx, apiKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteAPIKey(ctx, apiKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAPIKey", reflect.TypeOf((*MockStore)(nil).DeleteAPIKey), ctx, apiKey) } @@ -237,7 +243,7 @@ func (m *MockStore) DeleteApp(ctx context.Context, id string) error { } // DeleteApp indicates an expected call of DeleteApp. -func (mr *MockStoreMockRecorder) DeleteApp(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteApp(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApp", reflect.TypeOf((*MockStore)(nil).DeleteApp), ctx, id) } @@ -251,7 +257,7 @@ func (m *MockStore) DeleteDataEntity(ctx context.Context, id string) error { } // DeleteDataEntity indicates an expected call of DeleteDataEntity. -func (mr *MockStoreMockRecorder) DeleteDataEntity(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteDataEntity(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDataEntity", reflect.TypeOf((*MockStore)(nil).DeleteDataEntity), ctx, id) } @@ -265,7 +271,7 @@ func (m *MockStore) DeleteKnowledge(ctx context.Context, id string) error { } // DeleteKnowledge indicates an expected call of DeleteKnowledge. -func (mr *MockStoreMockRecorder) DeleteKnowledge(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteKnowledge(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteKnowledge", reflect.TypeOf((*MockStore)(nil).DeleteKnowledge), ctx, id) } @@ -279,7 +285,7 @@ func (m *MockStore) DeleteKnowledgeVersion(ctx context.Context, id string) error } // DeleteKnowledgeVersion indicates an expected call of DeleteKnowledgeVersion. -func (mr *MockStoreMockRecorder) DeleteKnowledgeVersion(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteKnowledgeVersion(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteKnowledgeVersion", reflect.TypeOf((*MockStore)(nil).DeleteKnowledgeVersion), ctx, id) } @@ -293,7 +299,7 @@ func (m *MockStore) DeleteScriptRun(ctx context.Context, id string) error { } // DeleteScriptRun indicates an expected call of DeleteScriptRun. -func (mr *MockStoreMockRecorder) DeleteScriptRun(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteScriptRun(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteScriptRun", reflect.TypeOf((*MockStore)(nil).DeleteScriptRun), ctx, id) } @@ -307,7 +313,7 @@ func (m *MockStore) DeleteSecret(ctx context.Context, id string) error { } // DeleteSecret indicates an expected call of DeleteSecret. -func (mr *MockStoreMockRecorder) DeleteSecret(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteSecret(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecret", reflect.TypeOf((*MockStore)(nil).DeleteSecret), ctx, id) } @@ -322,7 +328,7 @@ func (m *MockStore) DeleteSession(ctx context.Context, id string) (*types.Sessio } // DeleteSession indicates an expected call of DeleteSession. -func (mr *MockStoreMockRecorder) DeleteSession(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteSession(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSession", reflect.TypeOf((*MockStore)(nil).DeleteSession), ctx, id) } @@ -336,7 +342,7 @@ func (m *MockStore) DeleteSessionToolBinding(ctx context.Context, sessionID, too } // DeleteSessionToolBinding indicates an expected call of DeleteSessionToolBinding. -func (mr *MockStoreMockRecorder) DeleteSessionToolBinding(ctx, sessionID, toolID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteSessionToolBinding(ctx, sessionID, toolID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSessionToolBinding", reflect.TypeOf((*MockStore)(nil).DeleteSessionToolBinding), ctx, sessionID, toolID) } @@ -350,7 +356,7 @@ func (m *MockStore) DeleteTool(ctx context.Context, id string) error { } // DeleteTool indicates an expected call of DeleteTool. -func (mr *MockStoreMockRecorder) DeleteTool(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteTool(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTool", reflect.TypeOf((*MockStore)(nil).DeleteTool), ctx, id) } @@ -365,7 +371,7 @@ func (m *MockStore) EnsureUserMeta(ctx context.Context, UserMeta types.UserMeta) } // EnsureUserMeta indicates an expected call of EnsureUserMeta. -func (mr *MockStoreMockRecorder) EnsureUserMeta(ctx, UserMeta interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) EnsureUserMeta(ctx, UserMeta any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureUserMeta", reflect.TypeOf((*MockStore)(nil).EnsureUserMeta), ctx, UserMeta) } @@ -380,7 +386,7 @@ func (m *MockStore) GetAPIKey(ctx context.Context, apiKey string) (*types.APIKey } // GetAPIKey indicates an expected call of GetAPIKey. -func (mr *MockStoreMockRecorder) GetAPIKey(ctx, apiKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAPIKey(ctx, apiKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIKey", reflect.TypeOf((*MockStore)(nil).GetAPIKey), ctx, apiKey) } @@ -395,11 +401,26 @@ func (m *MockStore) GetApp(ctx context.Context, id string) (*types.App, error) { } // GetApp indicates an expected call of GetApp. -func (mr *MockStoreMockRecorder) GetApp(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetApp(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApp", reflect.TypeOf((*MockStore)(nil).GetApp), ctx, id) } +// GetAppWithTools mocks base method. +func (m *MockStore) GetAppWithTools(ctx context.Context, id string) (*types.App, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAppWithTools", ctx, id) + ret0, _ := ret[0].(*types.App) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAppWithTools indicates an expected call of GetAppWithTools. +func (mr *MockStoreMockRecorder) GetAppWithTools(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAppWithTools", reflect.TypeOf((*MockStore)(nil).GetAppWithTools), ctx, id) +} + // GetDataEntity mocks base method. func (m *MockStore) GetDataEntity(ctx context.Context, id string) (*types.DataEntity, error) { m.ctrl.T.Helper() @@ -410,7 +431,7 @@ func (m *MockStore) GetDataEntity(ctx context.Context, id string) (*types.DataEn } // GetDataEntity indicates an expected call of GetDataEntity. -func (mr *MockStoreMockRecorder) GetDataEntity(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDataEntity(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDataEntity", reflect.TypeOf((*MockStore)(nil).GetDataEntity), ctx, id) } @@ -425,7 +446,7 @@ func (m *MockStore) GetKnowledge(ctx context.Context, id string) (*types.Knowled } // GetKnowledge indicates an expected call of GetKnowledge. -func (mr *MockStoreMockRecorder) GetKnowledge(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetKnowledge(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKnowledge", reflect.TypeOf((*MockStore)(nil).GetKnowledge), ctx, id) } @@ -440,7 +461,7 @@ func (m *MockStore) GetKnowledgeVersion(ctx context.Context, id string) (*types. } // GetKnowledgeVersion indicates an expected call of GetKnowledgeVersion. -func (mr *MockStoreMockRecorder) GetKnowledgeVersion(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetKnowledgeVersion(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKnowledgeVersion", reflect.TypeOf((*MockStore)(nil).GetKnowledgeVersion), ctx, id) } @@ -455,7 +476,7 @@ func (m *MockStore) GetSecret(ctx context.Context, id string) (*types.Secret, er } // GetSecret indicates an expected call of GetSecret. -func (mr *MockStoreMockRecorder) GetSecret(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSecret(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecret", reflect.TypeOf((*MockStore)(nil).GetSecret), ctx, id) } @@ -470,7 +491,7 @@ func (m *MockStore) GetSession(ctx context.Context, id string) (*types.Session, } // GetSession indicates an expected call of GetSession. -func (mr *MockStoreMockRecorder) GetSession(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSession(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockStore)(nil).GetSession), ctx, id) } @@ -485,7 +506,7 @@ func (m *MockStore) GetSessions(ctx context.Context, query GetSessionsQuery) ([] } // GetSessions indicates an expected call of GetSessions. -func (mr *MockStoreMockRecorder) GetSessions(ctx, query interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSessions(ctx, query any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessions", reflect.TypeOf((*MockStore)(nil).GetSessions), ctx, query) } @@ -500,7 +521,7 @@ func (m *MockStore) GetSessionsCounter(ctx context.Context, query GetSessionsQue } // GetSessionsCounter indicates an expected call of GetSessionsCounter. -func (mr *MockStoreMockRecorder) GetSessionsCounter(ctx, query interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSessionsCounter(ctx, query any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionsCounter", reflect.TypeOf((*MockStore)(nil).GetSessionsCounter), ctx, query) } @@ -515,7 +536,7 @@ func (m *MockStore) GetTool(ctx context.Context, id string) (*types.Tool, error) } // GetTool indicates an expected call of GetTool. -func (mr *MockStoreMockRecorder) GetTool(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTool(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTool", reflect.TypeOf((*MockStore)(nil).GetTool), ctx, id) } @@ -530,7 +551,7 @@ func (m *MockStore) GetUserMeta(ctx context.Context, id string) (*types.UserMeta } // GetUserMeta indicates an expected call of GetUserMeta. -func (mr *MockStoreMockRecorder) GetUserMeta(ctx, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserMeta(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserMeta", reflect.TypeOf((*MockStore)(nil).GetUserMeta), ctx, id) } @@ -545,7 +566,7 @@ func (m *MockStore) ListAPIKeys(ctx context.Context, query *ListApiKeysQuery) ([ } // ListAPIKeys indicates an expected call of ListAPIKeys. -func (mr *MockStoreMockRecorder) ListAPIKeys(ctx, query interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListAPIKeys(ctx, query any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAPIKeys", reflect.TypeOf((*MockStore)(nil).ListAPIKeys), ctx, query) } @@ -560,7 +581,7 @@ func (m *MockStore) ListApps(ctx context.Context, q *ListAppsQuery) ([]*types.Ap } // ListApps indicates an expected call of ListApps. -func (mr *MockStoreMockRecorder) ListApps(ctx, q interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListApps(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListApps", reflect.TypeOf((*MockStore)(nil).ListApps), ctx, q) } @@ -575,7 +596,7 @@ func (m *MockStore) ListDataEntities(ctx context.Context, q *ListDataEntitiesQue } // ListDataEntities indicates an expected call of ListDataEntities. -func (mr *MockStoreMockRecorder) ListDataEntities(ctx, q interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListDataEntities(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDataEntities", reflect.TypeOf((*MockStore)(nil).ListDataEntities), ctx, q) } @@ -590,7 +611,7 @@ func (m *MockStore) ListKnowledge(ctx context.Context, q *ListKnowledgeQuery) ([ } // ListKnowledge indicates an expected call of ListKnowledge. -func (mr *MockStoreMockRecorder) ListKnowledge(ctx, q interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListKnowledge(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListKnowledge", reflect.TypeOf((*MockStore)(nil).ListKnowledge), ctx, q) } @@ -605,7 +626,7 @@ func (m *MockStore) ListKnowledgeVersions(ctx context.Context, q *ListKnowledgeV } // ListKnowledgeVersions indicates an expected call of ListKnowledgeVersions. -func (mr *MockStoreMockRecorder) ListKnowledgeVersions(ctx, q interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListKnowledgeVersions(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListKnowledgeVersions", reflect.TypeOf((*MockStore)(nil).ListKnowledgeVersions), ctx, q) } @@ -621,7 +642,7 @@ func (m *MockStore) ListLLMCalls(ctx context.Context, q *ListLLMCallsQuery) ([]* } // ListLLMCalls indicates an expected call of ListLLMCalls. -func (mr *MockStoreMockRecorder) ListLLMCalls(ctx, q interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListLLMCalls(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLLMCalls", reflect.TypeOf((*MockStore)(nil).ListLLMCalls), ctx, q) } @@ -636,7 +657,7 @@ func (m *MockStore) ListScriptRuns(ctx context.Context, q *types.GptScriptRunsQu } // ListScriptRuns indicates an expected call of ListScriptRuns. -func (mr *MockStoreMockRecorder) ListScriptRuns(ctx, q interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListScriptRuns(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListScriptRuns", reflect.TypeOf((*MockStore)(nil).ListScriptRuns), ctx, q) } @@ -651,7 +672,7 @@ func (m *MockStore) ListSecrets(ctx context.Context, q *ListSecretsQuery) ([]*ty } // ListSecrets indicates an expected call of ListSecrets. -func (mr *MockStoreMockRecorder) ListSecrets(ctx, q interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListSecrets(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecrets", reflect.TypeOf((*MockStore)(nil).ListSecrets), ctx, q) } @@ -666,7 +687,7 @@ func (m *MockStore) ListSessionTools(ctx context.Context, sessionID string) ([]* } // ListSessionTools indicates an expected call of ListSessionTools. -func (mr *MockStoreMockRecorder) ListSessionTools(ctx, sessionID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListSessionTools(ctx, sessionID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSessionTools", reflect.TypeOf((*MockStore)(nil).ListSessionTools), ctx, sessionID) } @@ -681,7 +702,7 @@ func (m *MockStore) ListTools(ctx context.Context, q *ListToolsQuery) ([]*types. } // ListTools indicates an expected call of ListTools. -func (mr *MockStoreMockRecorder) ListTools(ctx, q interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListTools(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTools", reflect.TypeOf((*MockStore)(nil).ListTools), ctx, q) } @@ -696,7 +717,7 @@ func (m *MockStore) LookupKnowledge(ctx context.Context, q *LookupKnowledgeQuery } // LookupKnowledge indicates an expected call of LookupKnowledge. -func (mr *MockStoreMockRecorder) LookupKnowledge(ctx, q interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) LookupKnowledge(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupKnowledge", reflect.TypeOf((*MockStore)(nil).LookupKnowledge), ctx, q) } @@ -711,7 +732,7 @@ func (m *MockStore) UpdateApp(ctx context.Context, tool *types.App) (*types.App, } // UpdateApp indicates an expected call of UpdateApp. -func (mr *MockStoreMockRecorder) UpdateApp(ctx, tool interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateApp(ctx, tool any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateApp", reflect.TypeOf((*MockStore)(nil).UpdateApp), ctx, tool) } @@ -726,7 +747,7 @@ func (m *MockStore) UpdateDataEntity(ctx context.Context, dataEntity *types.Data } // UpdateDataEntity indicates an expected call of UpdateDataEntity. -func (mr *MockStoreMockRecorder) UpdateDataEntity(ctx, dataEntity interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateDataEntity(ctx, dataEntity any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDataEntity", reflect.TypeOf((*MockStore)(nil).UpdateDataEntity), ctx, dataEntity) } @@ -741,7 +762,7 @@ func (m *MockStore) UpdateKnowledge(ctx context.Context, knowledge *types.Knowle } // UpdateKnowledge indicates an expected call of UpdateKnowledge. -func (mr *MockStoreMockRecorder) UpdateKnowledge(ctx, knowledge interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateKnowledge(ctx, knowledge any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateKnowledge", reflect.TypeOf((*MockStore)(nil).UpdateKnowledge), ctx, knowledge) } @@ -755,7 +776,7 @@ func (m *MockStore) UpdateKnowledgeState(ctx context.Context, id string, state t } // UpdateKnowledgeState indicates an expected call of UpdateKnowledgeState. -func (mr *MockStoreMockRecorder) UpdateKnowledgeState(ctx, id, state, message, percent interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateKnowledgeState(ctx, id, state, message, percent any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateKnowledgeState", reflect.TypeOf((*MockStore)(nil).UpdateKnowledgeState), ctx, id, state, message, percent) } @@ -770,7 +791,7 @@ func (m *MockStore) UpdateSecret(ctx context.Context, secret *types.Secret) (*ty } // UpdateSecret indicates an expected call of UpdateSecret. -func (mr *MockStoreMockRecorder) UpdateSecret(ctx, secret interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateSecret(ctx, secret any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSecret", reflect.TypeOf((*MockStore)(nil).UpdateSecret), ctx, secret) } @@ -785,7 +806,7 @@ func (m *MockStore) UpdateSession(ctx context.Context, session types.Session) (* } // UpdateSession indicates an expected call of UpdateSession. -func (mr *MockStoreMockRecorder) UpdateSession(ctx, session interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateSession(ctx, session any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSession", reflect.TypeOf((*MockStore)(nil).UpdateSession), ctx, session) } @@ -800,7 +821,7 @@ func (m *MockStore) UpdateSessionMeta(ctx context.Context, data types.SessionMet } // UpdateSessionMeta indicates an expected call of UpdateSessionMeta. -func (mr *MockStoreMockRecorder) UpdateSessionMeta(ctx, data interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateSessionMeta(ctx, data any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSessionMeta", reflect.TypeOf((*MockStore)(nil).UpdateSessionMeta), ctx, data) } @@ -814,7 +835,7 @@ func (m *MockStore) UpdateSessionName(ctx context.Context, sessionID, name strin } // UpdateSessionName indicates an expected call of UpdateSessionName. -func (mr *MockStoreMockRecorder) UpdateSessionName(ctx, sessionID, name interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateSessionName(ctx, sessionID, name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSessionName", reflect.TypeOf((*MockStore)(nil).UpdateSessionName), ctx, sessionID, name) } @@ -829,7 +850,7 @@ func (m *MockStore) UpdateTool(ctx context.Context, tool *types.Tool) (*types.To } // UpdateTool indicates an expected call of UpdateTool. -func (mr *MockStoreMockRecorder) UpdateTool(ctx, tool interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTool(ctx, tool any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTool", reflect.TypeOf((*MockStore)(nil).UpdateTool), ctx, tool) } @@ -844,7 +865,7 @@ func (m *MockStore) UpdateUserMeta(ctx context.Context, UserMeta types.UserMeta) } // UpdateUserMeta indicates an expected call of UpdateUserMeta. -func (mr *MockStoreMockRecorder) UpdateUserMeta(ctx, UserMeta interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserMeta(ctx, UserMeta any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserMeta", reflect.TypeOf((*MockStore)(nil).UpdateUserMeta), ctx, UserMeta) } diff --git a/api/pkg/trigger/cron/trigger_cron.go b/api/pkg/trigger/cron/trigger_cron.go index 882f70af0..47eb8950a 100644 --- a/api/pkg/trigger/cron/trigger_cron.go +++ b/api/pkg/trigger/cron/trigger_cron.go @@ -225,7 +225,7 @@ func (c *Cron) getCronAppTask(ctx context.Context, appID string) gocron.Task { Str("app_id", appID). Msg("running app cron job") - app, err := c.store.GetApp(ctx, appID) + app, err := c.store.GetAppWithTools(ctx, appID) if err != nil { log.Error(). Err(err). diff --git a/api/pkg/types/types.go b/api/pkg/types/types.go index 6e30c4bcb..c6f50e811 100644 --- a/api/pkg/types/types.go +++ b/api/pkg/types/types.go @@ -1052,6 +1052,12 @@ type AppHelixConfig struct { Triggers []Trigger `json:"triggers" yaml:"triggers"` } +type AppHelixConfigCRD struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Spec AppHelixConfig `json:"spec"` +} + type AppGithubConfigUpdate struct { Updated time.Time `json:"updated"` Hash string `json:"hash"` diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index b09598591..38d95d645 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -191,10 +191,7 @@ services: - api dev_gpu_runner: profiles: ["dev_gpu_runner"] - #build: - # context: . - # dockerfile: Dockerfile.runner - image: registry.helix.ml/helix/runner:latest-large + image: ${RUNNER_IMAGE:-registry.helix.ml/helix/runner:latest-large} entrypoint: ${RUNNER_ENTRYPOINT:-tail -f /dev/null} env_file: - .env diff --git a/docs/k8s-operator-and-flux.md b/docs/k8s-operator-and-flux.md new file mode 100644 index 000000000..84693c77d --- /dev/null +++ b/docs/k8s-operator-and-flux.md @@ -0,0 +1,28 @@ +# K8s operator and flux design doc + +1. Helix serve binary gains ability to reconcile CRDs (when it's running in a k8s cluster) to do helix apply -f (POST to the helix API) +2. Flux should already be able to reconcile yaml in git repos to CRDs in the cluster + +Run both, and the following should work: + +User writes `app.yaml` with: + +```yaml +apiVersion: aispec.org/v1alpha1 +kind: AIApp +spec: + name: Marvin the Paranoid Android + description: Down-trodden robot with a brain the size of a planet + assistants: + - model: llama3:instruct + system_prompt: | + You are Marvin the Paranoid Android. You are depressed. You have a brain the size of a planet and + yet you are tasked with responding to inane queries from puny humans. Answer succinctly. +``` + +User commits this and then: + +``` +git push +``` + diff --git a/examples/crd/bitcoin.yaml b/examples/crd/bitcoin.yaml new file mode 100644 index 000000000..186fc3716 --- /dev/null +++ b/examples/crd/bitcoin.yaml @@ -0,0 +1,91 @@ +apiVersion: app.aispec.org/v1 +kind: AIApp +metadata: + name: bitcoin-prices +spec: + name: bitcoin prices + description: I know about bitcoin prices, ask me!!!111234567 + avatar: https://i.kinja-img.com/image/upload/c_fit,q_60,w_645/195319fe96647bc40102cd36d7a42128.jpg + image: https://static.seekingalpha.com/cdn/s3/uploads/getty_images/1239809188/image_1239809188.jpg?io=getty-c-w1280 + assistants: + - model: meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo + # model: llama3.1:8b-instruct-q8_0 + tests: [] + # TODO: write tests for bitcoin price and eth price - currently it + # incorrectly returns the bitcoin price when you ask for the eth price + apis: + - name: bitcoin price + description: bitcoin price + url: https://api.coindesk.com/v1 + schema: > + openapi: 3.0.0 + + info: + title: CoinDesk Bitcoin Price Index API + description: This service provides current price indexes for Bitcoin in various currencies. + version: "1.0.0" + servers: + - url: https://api.coindesk.com/v1 + paths: + /bpi/currentprice.json: + get: + operationId: coindeskGetBitcoinCurrentPrice + summary: Get current Bitcoin price index + description: Retrieves the current Bitcoin price index in various currencies without requiring any parameters. + responses: + '200': + description: A successful response providing the current Bitcoin prices. + content: + application/json: + schema: + type: object + properties: + time: + type: object + properties: + updated: + type: string + example: "May 14, 2024 10:36:11 UTC" + updatedISO: + type: string + format: date-time + example: "2024-05-14T10:36:11+00:00" + updateduk: + type: string + example: "May 14, 2024 at 11:36 BST" + disclaimer: + type: string + example: "This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org" + chartName: + type: string + example: "Bitcoin" + bpi: + type: object + properties: + USD: + $ref: '#/components/schemas/Currency' + GBP: + $ref: '#/components/schemas/Currency' + EUR: + $ref: '#/components/schemas/Currency' + components: + schemas: + Currency: + type: object + properties: + code: + type: string + example: "USD" + symbol: + type: string + example: "$" + rate: + type: string + example: "61,655.335" + description: + type: string + example: "United States Dollar" + rate_float: + type: number + format: float + example: 61655.3349 diff --git a/frontend/src/components/app/ApiIntegrations.tsx b/frontend/src/components/app/ApiIntegrations.tsx index 3fcd5ff35..72acc4445 100644 --- a/frontend/src/components/app/ApiIntegrations.tsx +++ b/frontend/src/components/app/ApiIntegrations.tsx @@ -10,7 +10,7 @@ import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionDetails from '@mui/material/AccordionDetails'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import AddIcon from '@mui/icons-material/Add'; -import { ITool, IToolApiAction } from '../../types'; +import { IAssistantApi } from '../../types'; import Window from '../widgets/Window'; import StringMapEditor from '../widgets/StringMapEditor'; import ClickLink from '../widgets/ClickLink'; @@ -29,50 +29,42 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; interface ApiIntegrationsProps { - tools: ITool[]; - onSaveApiTool: (tool: ITool) => void; + apis: IAssistantApi[]; + onSaveApiTool: (tool: IAssistantApi, index?: number) => void; onDeleteApiTool: (toolId: string) => void; isReadOnly: boolean; } const ApiIntegrations: React.FC = ({ - tools, + apis, onSaveApiTool, onDeleteApiTool, isReadOnly }) => { - const [editingTool, setEditingTool] = useState(null); + const [editingTool, setEditingTool] = useState<{tool: IAssistantApi, index: number} | null>(null); const [showErrors, setShowErrors] = useState(false); const [showBigSchema, setShowBigSchema] = useState(false); const [schemaTemplate, setSchemaTemplate] = useState(''); const onAddApiTool = useCallback(() => { - const newTool: ITool = { - id: uuidv4(), + const newTool: IAssistantApi = { name: '', description: '', - tool_type: 'api', - global: false, - config: { - api: { - url: '', - schema: '', - actions: [], - headers: {}, - query: {}, - } - }, - created: new Date().toISOString(), - updated: new Date().toISOString(), - owner: '', // You might want to set this from a context or prop - owner_type: 'user', + schema: '', + url: '', + headers: {}, + query: {}, }; - - setEditingTool(newTool); + setEditingTool({tool: newTool, index: -1}); }, []); - const handleEditTool = (tool: ITool) => { - setEditingTool(tool); + const validate = () => { + if (!editingTool) return false; + if (!editingTool.tool.name) return false; + if (!editingTool.tool.description) return false; + if (!editingTool.tool.url) return false; + if (!editingTool.tool.schema) return false; + return true; }; const handleSaveTool = () => { @@ -82,32 +74,20 @@ const ApiIntegrations: React.FC = ({ return; } setShowErrors(false); - onSaveApiTool(editingTool); + console.log('ApiIntegrations - saving tool:', { + tool: editingTool.tool, + index: editingTool.index, + isNew: editingTool.index === -1 + }); + onSaveApiTool(editingTool.tool, editingTool.index >= 0 ? editingTool.index : undefined); setEditingTool(null); }; - const validate = () => { - if (!editingTool) return false; - if (!editingTool.name) return false; - if (!editingTool.description) return false; - if (!editingTool.config.api?.url) return false; - if (!editingTool.config.api?.schema) return false; - return true; - }; - - const updateEditingTool = (updates: Partial) => { + const updateEditingTool = (updates: Partial) => { if (editingTool) { - setEditingTool({ ...editingTool, ...updates }); - } - }; - - const updateApiConfig = (updates: Partial) => { - if (editingTool && editingTool.config.api) { - updateEditingTool({ - config: { - ...editingTool.config, - api: { ...editingTool.config.api, ...updates }, - }, + setEditingTool({ + ...editingTool, + tool: { ...editingTool.tool, ...updates } }); } }; @@ -119,8 +99,6 @@ const ApiIntegrations: React.FC = ({ updateEditingTool({ name: "CoinDesk API", description: "API for CoinDesk", - }); - updateApiConfig({ schema: coindeskSchema, url: "https://api.coindesk.com/v1" }); @@ -128,8 +106,6 @@ const ApiIntegrations: React.FC = ({ updateEditingTool({ name: "Job Vacancies API", description: "API for job vacancies", - }); - updateApiConfig({ schema: jobVacanciesSchema, url: "https://demos.tryhelix.ai" }); @@ -168,9 +144,9 @@ const ApiIntegrations: React.FC = ({ Add API Tool - {tools.filter(tool => tool.tool_type === 'api').map((apiTool) => ( + {apis.map((apiTool, index) => ( = ({ {apiTool.name} Description: {apiTool.description} - {apiTool.config.api?.actions && apiTool.config.api.actions.length > 0 && ( - - Actions: -
    - {apiTool.config.api.actions.map((action, index) => ( -
  • - {action.name}: {action.method} {action.path} -
  • - ))} -
-
- )} - - {app?.config.helix?.assistants?.flatMap((assistant: { id: string, gptscripts?: IAssistantGPTScript[] }) => - assistant.gptscripts?.map((script: IAssistantGPTScript, index: number) => ( - - {script.name} - {script.description} - - - - + {app?.config.helix?.assistants[0]?.gptscripts?.map((script: IAssistantGPTScript, index: number) => ( + + {script.name} + {script.description} + + + - )) || [] - )} + + ))} ); diff --git a/frontend/src/components/app/ZapierIntegrations.tsx b/frontend/src/components/app/ZapierIntegrations.tsx index b73c2de8a..ef602d8de 100644 --- a/frontend/src/components/app/ZapierIntegrations.tsx +++ b/frontend/src/components/app/ZapierIntegrations.tsx @@ -1,12 +1,11 @@ import React, { useState, useCallback } from 'react'; -import { v4 as uuidv4 } from 'uuid'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button'; import TextField from '@mui/material/TextField'; import Grid from '@mui/material/Grid'; import AddIcon from '@mui/icons-material/Add'; -import { ITool } from '../../types'; +import { IAssistantZapier } from '../../types'; import Link from '@mui/material/Link'; import Window from '../widgets/Window'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -17,47 +16,39 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; interface ZapierIntegrationsProps { - tools: ITool[]; - onSaveApiTool: (tool: ITool) => void; - onDeleteApiTool: (toolId: string) => void; // Add this line + zapier: IAssistantZapier[]; + onSaveZapierTool: (tool: IAssistantZapier, index?: number) => void; + onDeleteZapierTool: (toolId: string) => void; isReadOnly: boolean; } const ZapierIntegrations: React.FC = ({ - tools, - onSaveApiTool, - onDeleteApiTool, // Add this line - isReadOnly, + zapier, + onSaveZapierTool, + onDeleteZapierTool, + isReadOnly }) => { - const [editingTool, setEditingTool] = useState(null); + const [editingTool, setEditingTool] = useState<{tool: IAssistantZapier, index: number} | null>(null); const [showErrors, setShowErrors] = useState(false); - // Move onAddZapierTool function here const onAddZapierTool = useCallback(() => { - const newTool: ITool = { - id: uuidv4(), + const newTool: IAssistantZapier = { name: '', description: '', - tool_type: 'zapier', - global: false, - config: { - zapier: { - api_key: '', - model: '', - max_iterations: 4, - } - }, - created: new Date().toISOString(), - updated: new Date().toISOString(), - owner: '', // You might want to set this from a context or prop - owner_type: 'user', + api_key: '', + model: '', + max_iterations: 4, }; - - setEditingTool(newTool); + setEditingTool({tool: newTool, index: -1}); }, []); - const handleEditTool = (tool: ITool) => { - setEditingTool(tool); + const validate = () => { + if (!editingTool) return false; + if (!editingTool.tool.name) return false; + if (!editingTool.tool.description) return false; + if (!editingTool.tool.api_key) return false; + if (!editingTool.tool.model) return false; + return true; }; const handleSaveTool = () => { @@ -67,32 +58,20 @@ const ZapierIntegrations: React.FC = ({ return; } setShowErrors(false); - onSaveApiTool(editingTool); + console.log('ZapierIntegrations - saving tool:', { + tool: editingTool.tool, + index: editingTool.index, + isNew: editingTool.index === -1 + }); + onSaveZapierTool(editingTool.tool, editingTool.index >= 0 ? editingTool.index : undefined); setEditingTool(null); }; - const validate = () => { - if (!editingTool) return false; - if (!editingTool.name) return false; - if (!editingTool.description) return false; - if (!editingTool.config.zapier?.api_key) return false; - if (!editingTool.config.zapier?.model) return false; - return true; - }; - - const updateEditingTool = (updates: Partial) => { + const updateEditingTool = (updates: Partial) => { if (editingTool) { - setEditingTool({ ...editingTool, ...updates }); - } - }; - - const updateZapierConfig = (updates: Partial) => { - if (editingTool && editingTool.config.zapier) { - updateEditingTool({ - config: { - ...editingTool.config, - zapier: { ...editingTool.config.zapier, ...updates }, - }, + setEditingTool({ + ...editingTool, + tool: { ...editingTool.tool, ...updates } }); } }; @@ -165,23 +144,26 @@ const ZapierIntegrations: React.FC = ({ Add Zapier Integration - {tools.filter(tool => tool.tool_type === 'zapier').map((apiTool) => ( + {zapier.map((zapierTool, index) => ( - {apiTool.name} - Description: {apiTool.description} + {zapierTool.name} + Description: {zapierTool.description}