From f016e2ae5d97301105193439f772f247ca8ed88f Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Sun, 22 Dec 2024 15:33:34 -0500 Subject: [PATCH] feat: add support for authenticating on agents and workflows Agents and workflows can have tools that require credentials. To authenticate these credentials, this change adds the ability to track which tools need which credentials, whether those credentials exist, and the ability to create and delete them. Signed-off-by: Donnie Adams --- apiclient/types/agent.go | 15 +- apiclient/types/toolreference.go | 2 +- apiclient/types/workflow.go | 10 +- apiclient/types/zz_generated.deepcopy.go | 47 ++++++ pkg/api/handlers/agent.go | 151 +++++++++++++++-- pkg/api/handlers/emailreceiver.go | 2 +- pkg/api/handlers/model.go | 2 +- pkg/api/handlers/toolreferences.go | 2 +- pkg/api/handlers/webhooks.go | 2 +- pkg/api/handlers/workflows.go | 111 ++++++++++--- pkg/api/router/router.go | 6 +- pkg/controller/generationed/generationed.go | 20 +++ pkg/controller/handlers/agents/agents.go | 4 +- pkg/controller/handlers/alias/alias.go | 21 ++- .../knowledgesource/knowledgesource.go | 4 +- .../handlers/oauthapp/oauthapplogin.go | 6 +- pkg/controller/handlers/toolinfo/toolinfo.go | 75 +++++++++ .../handlers/toolreference/toolreference.go | 112 +++++++++++-- pkg/controller/handlers/workflow/oauth.go | 4 +- pkg/controller/routes.go | 22 ++- pkg/storage/apis/otto.otto8.ai/v1/agent.go | 39 +++-- pkg/storage/apis/otto.otto8.ai/v1/alias.go | 3 +- .../otto.otto8.ai/v1/defaultmodelalias.go | 8 +- .../apis/otto.otto8.ai/v1/emailaddress.go | 13 +- .../apis/otto.otto8.ai/v1/generationed.go | 8 + pkg/storage/apis/otto.otto8.ai/v1/model.go | 18 ++- pkg/storage/apis/otto.otto8.ai/v1/oauthapp.go | 4 +- pkg/storage/apis/otto.otto8.ai/v1/run.go | 2 + pkg/storage/apis/otto.otto8.ai/v1/tool.go | 12 ++ .../apis/otto.otto8.ai/v1/toolreference.go | 6 +- pkg/storage/apis/otto.otto8.ai/v1/webhook.go | 10 +- pkg/storage/apis/otto.otto8.ai/v1/workflow.go | 32 ++-- .../otto.otto8.ai/v1/zz_generated.deepcopy.go | 24 +++ .../openapi/generated/openapi_generated.go | 153 ++++++++++++++++-- 34 files changed, 811 insertions(+), 139 deletions(-) create mode 100644 pkg/controller/generationed/generationed.go create mode 100644 pkg/controller/handlers/toolinfo/toolinfo.go create mode 100644 pkg/storage/apis/otto.otto8.ai/v1/generationed.go create mode 100644 pkg/storage/apis/otto.otto8.ai/v1/tool.go diff --git a/apiclient/types/agent.go b/apiclient/types/agent.go index b8d7e80a0..def450a76 100644 --- a/apiclient/types/agent.go +++ b/apiclient/types/agent.go @@ -11,9 +11,13 @@ import ( type Agent struct { Metadata AgentManifest - AliasAssigned *bool `json:"aliasAssigned,omitempty"` - AuthStatus map[string]OAuthAppLoginAuthStatus `json:"authStatus,omitempty"` - TextEmbeddingModel string `json:"textEmbeddingModel,omitempty"` + AliasAssigned *bool `json:"aliasAssigned,omitempty"` + AuthStatus map[string]OAuthAppLoginAuthStatus `json:"authStatus,omitempty"` + // ToolInfo provides information about the tools for this agent, like which credentials they use and whether that + // credential has been created. This is a pointer so that we can distinguish between an empty map (no tool information) + // and nil (tool information not processed yet). + ToolInfo *map[string]ToolInfo `json:"toolInfo,omitempty"` + TextEmbeddingModel string `json:"textEmbeddingModel,omitempty"` } type AgentList List[Agent] @@ -56,3 +60,8 @@ func (m AgentManifest) GetParams() *openapi3.Schema { return gptscript.ObjectSchema(args...) } + +type ToolInfo struct { + CredentialNames []string `json:"credentialNames,omitempty"` + Authorized bool `json:"authorized"` +} diff --git a/apiclient/types/toolreference.go b/apiclient/types/toolreference.go index bd2aa37e2..41849c608 100644 --- a/apiclient/types/toolreference.go +++ b/apiclient/types/toolreference.go @@ -25,7 +25,7 @@ type ToolReference struct { Error string `json:"error,omitempty"` Builtin bool `json:"builtin,omitempty"` Description string `json:"description,omitempty"` - Credential string `json:"credential,omitempty"` + Credentials []string `json:"credential,omitempty"` Params map[string]string `json:"params,omitempty"` } diff --git a/apiclient/types/workflow.go b/apiclient/types/workflow.go index a351101c5..02e181529 100644 --- a/apiclient/types/workflow.go +++ b/apiclient/types/workflow.go @@ -5,9 +5,13 @@ import "strings" type Workflow struct { Metadata WorkflowManifest - AliasAssigned *bool `json:"aliasAssigned,omitempty"` - AuthStatus map[string]OAuthAppLoginAuthStatus `json:"authStatus,omitempty"` - TextEmbeddingModel string `json:"textEmbeddingModel,omitempty"` + AliasAssigned *bool `json:"aliasAssigned,omitempty"` + AuthStatus map[string]OAuthAppLoginAuthStatus `json:"authStatus,omitempty"` + // ToolInfo provides information about the tools for this workflow, like which credentials they use and whether that + // credential has been created. This is a pointer so that we can distinguish between an empty map (no tool information) + // and nil (tool information not processed yet). + ToolInfo *map[string]ToolInfo `json:"toolInfo,omitempty"` + TextEmbeddingModel string `json:"textEmbeddingModel,omitempty"` } type WorkflowList List[Workflow] diff --git a/apiclient/types/zz_generated.deepcopy.go b/apiclient/types/zz_generated.deepcopy.go index 4503fc416..ca1ea4f17 100644 --- a/apiclient/types/zz_generated.deepcopy.go +++ b/apiclient/types/zz_generated.deepcopy.go @@ -25,6 +25,17 @@ func (in *Agent) DeepCopyInto(out *Agent) { (*out)[key] = *val.DeepCopy() } } + if in.ToolInfo != nil { + in, out := &in.ToolInfo, &out.ToolInfo + *out = new(map[string]ToolInfo) + if **in != nil { + in, out := *in, *out + *out = make(map[string]ToolInfo, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Agent. @@ -1693,6 +1704,26 @@ func (in *ToolCall) DeepCopy() *ToolCall { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ToolInfo) DeepCopyInto(out *ToolInfo) { + *out = *in + if in.CredentialNames != nil { + in, out := &in.CredentialNames, &out.CredentialNames + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ToolInfo. +func (in *ToolInfo) DeepCopy() *ToolInfo { + if in == nil { + return nil + } + out := new(ToolInfo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ToolInput) DeepCopyInto(out *ToolInput) { *out = *in @@ -1720,6 +1751,11 @@ func (in *ToolReference) DeepCopyInto(out *ToolReference) { *out = *in in.Metadata.DeepCopyInto(&out.Metadata) out.ToolReferenceManifest = in.ToolReferenceManifest + if in.Credentials != nil { + in, out := &in.Credentials, &out.Credentials + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.Params != nil { in, out := &in.Params, &out.Params *out = make(map[string]string, len(*in)) @@ -1941,6 +1977,17 @@ func (in *Workflow) DeepCopyInto(out *Workflow) { (*out)[key] = *val.DeepCopy() } } + if in.ToolInfo != nil { + in, out := &in.ToolInfo, &out.ToolInfo + *out = new(map[string]ToolInfo) + if **in != nil { + in, out := *in, *out + *out = make(map[string]ToolInfo, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Workflow. diff --git a/pkg/api/handlers/agent.go b/pkg/api/handlers/agent.go index efd7eae3a..11de00f79 100644 --- a/pkg/api/handlers/agent.go +++ b/pkg/api/handlers/agent.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "errors" "fmt" "net/http" @@ -11,6 +12,7 @@ import ( "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/alias" "github.com/obot-platform/obot/pkg/api" + "github.com/obot-platform/obot/pkg/invoke" "github.com/obot-platform/obot/pkg/render" v1 "github.com/obot-platform/obot/pkg/storage/apis/otto.otto8.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -21,19 +23,97 @@ import ( type AgentHandler struct { gptscript *gptscript.GPTScript + invoker *invoke.Invoker serverURL string // This is currently a hack to access the workflow handler workflowHandler *WorkflowHandler } -func NewAgentHandler(gClient *gptscript.GPTScript, serverURL string) *AgentHandler { +func NewAgentHandler(gClient *gptscript.GPTScript, invoker *invoke.Invoker, serverURL string) *AgentHandler { return &AgentHandler{ serverURL: serverURL, gptscript: gClient, - workflowHandler: NewWorkflowHandler(gClient, serverURL, nil), + invoker: invoker, + workflowHandler: NewWorkflowHandler(gClient, serverURL, invoker), } } +func (a *AgentHandler) Authenticate(req api.Context) (err error) { + var ( + id = req.PathValue("id") + agent v1.Agent + tools []string + ) + + if err := req.Read(&tools); err != nil { + return fmt.Errorf("failed to read tools from request body: %w", err) + } + + if len(tools) == 0 { + return types.NewErrBadRequest("no tools provided for authentication") + } + + if err := req.Get(&agent, id); err != nil { + return err + } + + resp, err := runAuthForAgent(req.Context(), req.Storage, a.invoker, agent.DeepCopy(), tools) + defer func() { + resp.Close() + if kickErr := kickAgent(req.Context(), req.Storage, &agent); kickErr != nil && err == nil { + err = fmt.Errorf("failed to update agent status: %w", kickErr) + } + }() + + req.ResponseWriter.Header().Set("X-Otto-Thread-Id", resp.Thread.Name) + return req.WriteEvents(resp.Events) +} + +func (a *AgentHandler) DeAuthenticate(req api.Context) error { + var ( + id = req.PathValue("id") + agent v1.Agent + tools []string + ) + + if err := req.Read(&tools); err != nil { + return fmt.Errorf("failed to read tools from request body: %w", err) + } + + if len(tools) == 0 { + return types.NewErrBadRequest("no tools provided for de-authentication") + } + + if err := req.Get(&agent, id); err != nil { + return err + } + + var ( + errs []error + toolRef v1.ToolReference + ) + for _, tool := range tools { + if err := req.Get(&toolRef, tool); err != nil { + errs = append(errs, err) + continue + } + + if toolRef.Status.Tool != nil { + for _, cred := range toolRef.Status.Tool.CredentialNames { + if err := a.gptscript.DeleteCredential(req.Context(), id, cred); err != nil && !strings.HasSuffix(err.Error(), "credential not found") { + errs = append(errs, err) + } + } + } + } + + if err := kickAgent(req.Context(), req.Storage, &agent); err != nil { + errs = append(errs, fmt.Errorf("failed to update agent status: %w", err)) + } + + return errors.Join(errs...) +} + func (a *AgentHandler) Update(req api.Context) error { var ( id = req.PathValue("id") @@ -140,9 +220,13 @@ func convertAgent(agent v1.Agent, textEmbeddingModel, baseURL string) (*types.Ag links = []string{"invoke", baseURL + "/invoke/" + alias} } - var aliasAssigned *bool - if agent.Generation == agent.Status.AliasObservedGeneration { + var ( + aliasAssigned *bool + toolInfos *map[string]types.ToolInfo + ) + if agent.Generation == agent.Status.ObservedGeneration { aliasAssigned = &agent.Status.AliasAssigned + toolInfos = &agent.Status.ToolInfo } return &types.Agent{ @@ -150,6 +234,7 @@ func convertAgent(agent v1.Agent, textEmbeddingModel, baseURL string) (*types.Ag AgentManifest: agent.Spec.Manifest, AliasAssigned: aliasAssigned, AuthStatus: agent.Status.AuthStatus, + ToolInfo: toolInfos, TextEmbeddingModel: textEmbeddingModel, }, nil } @@ -218,6 +303,7 @@ func (a *AgentHandler) ByID(req api.Context) error { if err != nil { return err } + return req.WriteCreated(resp) } @@ -658,21 +744,12 @@ func (a *AgentHandler) EnsureCredentialForKnowledgeSource(req api.Context) error return req.WriteCreated(resp) } - // if auth is already authenticated, then don't continue. - if authStatus.Authenticated { - resp, err := convertAgent(agent, knowledgeSet.Status.TextEmbeddingModel, req.APIBaseURL) - if err != nil { - return err - } - return req.WriteCreated(resp) - } - - credentialTool, err := v1.CredentialTool(req.Context(), req.Storage, req.Namespace(), ref) + credentialTools, err := v1.CredentialTools(req.Context(), req.Storage, req.Namespace(), ref) if err != nil { return err } - if credentialTool == "" { + if len(credentialTools) == 0 { // The only way to get here is if the controller hasn't set the field yet. if agent.Status.AuthStatus == nil { agent.Status.AuthStatus = make(map[string]types.OAuthAppLoginAuthStatus) @@ -770,3 +847,47 @@ func MetadataFrom(obj kclient.Object, linkKV ...string) types.Metadata { } return m } + +func runAuthForAgent(ctx context.Context, c kclient.WithWatch, invoker *invoke.Invoker, agent *v1.Agent, tools []string) (*invoke.Response, error) { + credentials := make([]string, 0, len(tools)) + + var toolRef v1.ToolReference + for _, tool := range tools { + if err := c.Get(ctx, kclient.ObjectKey{Namespace: agent.Namespace, Name: tool}, &toolRef); err != nil { + return nil, err + } + + if toolRef.Status.Tool == nil { + return nil, types.NewErrHttp(http.StatusTooEarly, fmt.Sprintf("tool %q is not ready", tool)) + } + + credentials = append(credentials, toolRef.Status.Tool.Credentials...) + + // Reset the fields we care about so that we can use the same variable for the whole loop. + toolRef.Status.Tool = nil + } + + agent.Spec.Manifest.Prompt = "#!sys.echo\nDONE" + agent.Spec.Manifest.Tools = tools + agent.Spec.Manifest.AvailableThreadTools = nil + agent.Spec.Manifest.DefaultThreadTools = nil + agent.Spec.Credentials = credentials + + return invoker.Agent(ctx, c, agent, "", invoke.Options{ + Synchronous: true, + ThreadCredentialScope: new(bool), + }) +} + +func kickAgent(ctx context.Context, c kclient.Client, agent *v1.Agent) error { + if agent.Annotations[v1.AgentSyncAnnotation] != "" { + delete(agent.Annotations, v1.AgentSyncAnnotation) + } else { + if agent.Annotations == nil { + agent.Annotations = make(map[string]string) + } + agent.Annotations[v1.AgentSyncAnnotation] = "true" + } + + return c.Update(ctx, agent) +} diff --git a/pkg/api/handlers/emailreceiver.go b/pkg/api/handlers/emailreceiver.go index 108606f6d..2762e1af4 100644 --- a/pkg/api/handlers/emailreceiver.go +++ b/pkg/api/handlers/emailreceiver.go @@ -82,7 +82,7 @@ func convertEmailReceiver(emailReceiver v1.EmailReceiver, hostname string) *type manifest := emailReceiver.Spec.EmailReceiverManifest var aliasAssigned *bool - if emailReceiver.Generation == emailReceiver.Status.AliasObservedGeneration { + if emailReceiver.Generation == emailReceiver.Status.ObservedGeneration { aliasAssigned = &emailReceiver.Status.AliasAssigned } er := &types.EmailReceiver{ diff --git a/pkg/api/handlers/model.go b/pkg/api/handlers/model.go index cad4343c3..c6f55a66f 100644 --- a/pkg/api/handlers/model.go +++ b/pkg/api/handlers/model.go @@ -160,7 +160,7 @@ func convertModel(ctx context.Context, c kclient.Client, model v1.Model) (types. } var aliasAssigned *bool - if model.Generation == model.Status.AliasObservedGeneration { + if model.Generation == model.Status.ObservedGeneration { aliasAssigned = &model.Status.AliasAssigned } diff --git a/pkg/api/handlers/toolreferences.go b/pkg/api/handlers/toolreferences.go index d606404c8..6a7f31419 100644 --- a/pkg/api/handlers/toolreferences.go +++ b/pkg/api/handlers/toolreferences.go @@ -48,7 +48,7 @@ func convertToolReference(toolRef v1.ToolReference) types.ToolReference { tf.Name = toolRef.Status.Tool.Name tf.Description = toolRef.Status.Tool.Description tf.Metadata.Metadata = toolRef.Status.Tool.Metadata - tf.Credential = toolRef.Status.Tool.Credential + tf.Credentials = toolRef.Status.Tool.Credentials } return tf diff --git a/pkg/api/handlers/webhooks.go b/pkg/api/handlers/webhooks.go index c012fb714..97b718bad 100644 --- a/pkg/api/handlers/webhooks.go +++ b/pkg/api/handlers/webhooks.go @@ -141,7 +141,7 @@ func convertWebhook(webhook v1.Webhook, urlPrefix string) *types.Webhook { } var aliasAssigned *bool - if webhook.Generation == webhook.Status.AliasObservedGeneration { + if webhook.Generation == webhook.Status.ObservedGeneration { aliasAssigned = &webhook.Status.AliasAssigned } diff --git a/pkg/api/handlers/workflows.go b/pkg/api/handlers/workflows.go index b3271ef38..2fa4595da 100644 --- a/pkg/api/handlers/workflows.go +++ b/pkg/api/handlers/workflows.go @@ -1,6 +1,8 @@ package handlers import ( + "context" + "errors" "fmt" "strings" @@ -36,8 +38,17 @@ func (a *WorkflowHandler) Authenticate(req api.Context) error { var ( id = req.PathValue("id") workflow v1.Workflow + tools []string ) + if err := req.Read(&tools); err != nil { + return fmt.Errorf("failed to read tools from request body: %w", err) + } + + if len(tools) == 0 { + return types.NewErrBadRequest("no tools provided for authentication") + } + if err := req.Get(&workflow, id); err != nil { return err } @@ -47,19 +58,65 @@ func (a *WorkflowHandler) Authenticate(req api.Context) error { return err } - agent.Spec.Manifest.Prompt = "#!sys.echo\nDONE" + resp, err := runAuthForAgent(req.Context(), req.Storage, a.invoker, agent, tools) + defer func() { + resp.Close() + if kickErr := kickWorkflow(req.Context(), req.Storage, &workflow); kickErr != nil && err == nil { + err = fmt.Errorf("failed to update workflow status: %w", kickErr) + } + }() - resp, err := a.invoker.Agent(req.Context(), req.Storage, agent, "", invoke.Options{ - Synchronous: true, - ThreadCredentialScope: new(bool), - }) - if err != nil { + req.ResponseWriter.Header().Set("X-Otto-Thread-Id", resp.Thread.Name) + return req.WriteEvents(resp.Events) +} + +func (a *WorkflowHandler) DeAuthenticate(req api.Context) error { + var ( + id = req.PathValue("id") + wf v1.Workflow + tools []string + ) + + if err := req.Read(&tools); err != nil { + return fmt.Errorf("failed to read tools from request body: %w", err) + } + + if len(tools) == 0 { + return types.NewErrBadRequest("no tools provided for de-authentication") + } + + if err := req.Get(&wf, id); err != nil { return err } - defer resp.Close() - req.ResponseWriter.Header().Set("X-Otto-Thread-Id", resp.Thread.Name) - return req.WriteEvents(resp.Events) + var ( + errs []error + toolRef v1.ToolReference + ) + for _, tool := range tools { + if err := req.Get(&toolRef, tool); err != nil { + errs = append(errs, err) + continue + } + + if toolRef.Status.Tool != nil { + for _, cred := range toolRef.Status.Tool.CredentialNames { + if err := a.gptscript.DeleteCredential(req.Context(), id, cred); err != nil && !strings.HasSuffix(err.Error(), "credential not found") { + errs = append(errs, err) + } + } + + // Reset the value we care about so the same variable can be used. + // This ensures that the value we read on the next iteration is pulled from the database. + toolRef.Status.Tool = nil + } + } + + if err := kickWorkflow(req.Context(), req.Storage, &wf); err != nil { + errs = append(errs, fmt.Errorf("failed to update workflow status: %w", err)) + } + + return errors.Join(errs...) } func (a *WorkflowHandler) Update(req api.Context) error { @@ -176,9 +233,13 @@ func convertWorkflow(workflow v1.Workflow, textEmbeddingModel, baseURL string) ( links = []string{"invoke", baseURL + "/invoke/" + alias} } - var aliasAssigned *bool - if workflow.Generation == workflow.Status.AliasObservedGeneration { + var ( + aliasAssigned *bool + toolInfos *map[string]types.ToolInfo + ) + if workflow.Generation == workflow.Status.ObservedGeneration { aliasAssigned = &workflow.Status.AliasAssigned + toolInfos = &workflow.Status.ToolInfo } return &types.Workflow{ @@ -186,6 +247,7 @@ func convertWorkflow(workflow v1.Workflow, textEmbeddingModel, baseURL string) ( WorkflowManifest: workflow.Spec.Manifest, AliasAssigned: aliasAssigned, AuthStatus: workflow.Status.AuthStatus, + ToolInfo: toolInfos, TextEmbeddingModel: textEmbeddingModel, }, nil } @@ -276,22 +338,12 @@ func (a *WorkflowHandler) EnsureCredentialForKnowledgeSource(req api.Context) er return req.WriteCreated(resp) } - // if auth is already authenticated, then don't continue. - if authStatus.Authenticated { - resp, err := convertWorkflow(wf, knowledgeSet.Status.TextEmbeddingModel, req.APIBaseURL) - if err != nil { - return err - } - - return req.WriteCreated(resp) - } - - credentialTool, err := v1.CredentialTool(req.Context(), req.Storage, req.Namespace(), ref) + credentialTools, err := v1.CredentialTools(req.Context(), req.Storage, req.Namespace(), ref) if err != nil { return err } - if credentialTool == "" { + if len(credentialTools) == 0 { // The only way to get here is if the controller hasn't set the field yet. if wf.Status.AuthStatus == nil { wf.Status.AuthStatus = make(map[string]types.OAuthAppLoginAuthStatus) @@ -403,3 +455,16 @@ func (a *WorkflowHandler) Script(req api.Context) error { return req.Write(script) } + +func kickWorkflow(ctx context.Context, c kclient.Client, wf *v1.Workflow) error { + if wf.Annotations[v1.WorkflowSyncAnnotation] != "" { + delete(wf.Annotations, v1.WorkflowSyncAnnotation) + } else { + if wf.Annotations == nil { + wf.Annotations = make(map[string]string) + } + wf.Annotations[v1.WorkflowSyncAnnotation] = "true" + } + + return c.Update(ctx, wf) +} diff --git a/pkg/api/router/router.go b/pkg/api/router/router.go index 9d8bb76a9..1ffa632c6 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -11,7 +11,7 @@ import ( func Router(services *services.Services) (http.Handler, error) { mux := services.APIServer - agents := handlers.NewAgentHandler(services.GPTClient, services.ServerURL) + agents := handlers.NewAgentHandler(services.GPTClient, services.Invoker, services.ServerURL) assistants := handlers.NewAssistantHandler(services.Invoker, services.Events, services.GPTClient) tasks := handlers.NewTaskHandler(services.Invoker, services.Events) workflows := handlers.NewWorkflowHandler(services.GPTClient, services.ServerURL, services.Invoker) @@ -41,9 +41,12 @@ func Router(services *services.Services) (http.Handler, error) { mux.HandleFunc("GET /api/agents/{id}/script.gpt", agents.Script) mux.HandleFunc("GET /api/agents/{id}/script/tool.gpt", agents.Script) mux.HandleFunc("POST /api/agents", agents.Create) + mux.HandleFunc("POST /api/agents/{id}/authenticate", agents.Authenticate) + mux.HandleFunc("POST /api/agents/{id}/deauthenticate", agents.DeAuthenticate) mux.HandleFunc("PUT /api/agents/{id}", agents.Update) mux.HandleFunc("DELETE /api/agents/{id}", agents.Delete) mux.HandleFunc("POST /api/agents/{id}/oauth-credentials/{ref}/login", agents.EnsureCredentialForKnowledgeSource) + mux.HandleFunc("POST /api/agents/{id}/auth", agents.EnsureCredentialForKnowledgeSource) // Assistants mux.HandleFunc("GET /api/assistants", assistants.List) @@ -130,6 +133,7 @@ func Router(services *services.Services) (http.Handler, error) { mux.HandleFunc("GET /api/workflows/{id}/script/tool.gpt", workflows.Script) mux.HandleFunc("POST /api/workflows", workflows.Create) mux.HandleFunc("POST /api/workflows/{id}/authenticate", workflows.Authenticate) + mux.HandleFunc("POST /api/workflows/{id}/deauthenticate", workflows.DeAuthenticate) mux.HandleFunc("PUT /api/workflows/{id}", workflows.Update) mux.HandleFunc("DELETE /api/workflows/{id}", workflows.Delete) mux.HandleFunc("POST /api/workflows/{id}/oauth-credentials/{ref}/login", workflows.EnsureCredentialForKnowledgeSource) diff --git a/pkg/controller/generationed/generationed.go b/pkg/controller/generationed/generationed.go new file mode 100644 index 000000000..1aa3ad769 --- /dev/null +++ b/pkg/controller/generationed/generationed.go @@ -0,0 +1,20 @@ +package generationed + +import ( + "github.com/obot-platform/nah/pkg/router" + v1 "github.com/obot-platform/obot/pkg/storage/apis/otto.otto8.ai/v1" +) + +// UpdateObservedGeneration should be the last handler that runs on such an object to ensure +// that updating of the observed generation only happens if no error occurs. +func UpdateObservedGeneration(req router.Request, resp router.Response) error { + if req.Object == nil { + return nil + } + + if errored, ok := resp.Attributes()["generation:errored"]; !ok || errored != true { + req.Object.(v1.Generationed).SetObservedGeneration(req.Object.GetGeneration()) + } + + return nil +} diff --git a/pkg/controller/handlers/agents/agents.go b/pkg/controller/handlers/agents/agents.go index f1d5e998e..253baf037 100644 --- a/pkg/controller/handlers/agents/agents.go +++ b/pkg/controller/handlers/agents/agents.go @@ -88,11 +88,11 @@ func BackPopulateAuthStatus(req router.Request, _ router.Response) error { agent.Status.AuthStatus = make(map[string]types.OAuthAppLoginAuthStatus) for _, login := range logins.Items { var required *bool - credentialTool, err := v1.CredentialTool(req.Ctx, req.Client, agent.Namespace, login.Spec.ToolReference) + credentialTools, err := v1.CredentialTools(req.Ctx, req.Client, agent.Namespace, login.Spec.ToolReference) if err != nil { login.Status.External.Error = fmt.Sprintf("failed to get credential tool for knowledge source [%s]: %v", agent.Name, err) } else { - required = &[]bool{credentialTool != ""}[0] + required = &[]bool{len(credentialTools) > 0}[0] updateRequired = updateRequired || login.Status.External.Required == nil || *login.Status.External.Required != *required login.Status.External.Required = required } diff --git a/pkg/controller/handlers/alias/alias.go b/pkg/controller/handlers/alias/alias.go index c42f8f3d4..2ef89eeef 100644 --- a/pkg/controller/handlers/alias/alias.go +++ b/pkg/controller/handlers/alias/alias.go @@ -23,13 +23,23 @@ func matches(alias *v1.Alias, obj kclient.Object) bool { alias.Spec.TargetKind == obj.GetObjectKind().GroupVersionKind().Kind } -func AssignAlias(req router.Request, _ router.Response) error { +// AssignAlias will check the requested alias to see if it is already assigned to another object. +// If it is not, then the alias is assigned to the currently processing object. +// This handler should be used with the generationed.UpdateObservedGeneration to ensure that the processing +// is correctly reported to through the API. +func AssignAlias(req router.Request, resp router.Response) (err error) { + defer func() { + if err != nil { + resp.Attributes()["generation:errored"] = true + } + }() + aliasable := req.Object.(v1.Aliasable) if aliasable.GetAliasName() == "" { - if aliasable.IsAssigned() || aliasable.GetGeneration() != aliasable.GetAliasObservedGeneration() { + if aliasable.IsAssigned() || aliasable.GetGeneration() != aliasable.GetObservedGeneration() { aliasable.SetAssigned(false) - aliasable.SetAliasObservedGeneration(aliasable.GetGeneration()) + aliasable.SetObservedGeneration(aliasable.GetGeneration()) return req.Client.Status().Update(req.Ctx, req.Object) } @@ -57,13 +67,12 @@ func AssignAlias(req router.Request, _ router.Response) error { TargetKind: gvk.Kind, }, } - if err := create.IfNotExists(req.Ctx, req.Client, alias); err != nil { + if err = create.IfNotExists(req.Ctx, req.Client, alias); err != nil { return err } - if assigned := matches(alias, req.Object); assigned != aliasable.IsAssigned() || aliasable.GetGeneration() != aliasable.GetAliasObservedGeneration() { + if assigned := matches(alias, req.Object); assigned != aliasable.IsAssigned() { aliasable.SetAssigned(assigned) - aliasable.SetAliasObservedGeneration(aliasable.GetGeneration()) return req.Client.Status().Update(req.Ctx, req.Object) } diff --git a/pkg/controller/handlers/knowledgesource/knowledgesource.go b/pkg/controller/handlers/knowledgesource/knowledgesource.go index 5aab48334..7d6a7f16a 100644 --- a/pkg/controller/handlers/knowledgesource/knowledgesource.go +++ b/pkg/controller/handlers/knowledgesource/knowledgesource.go @@ -242,7 +242,7 @@ func (k *Handler) Sync(req router.Request, _ router.Response) error { toolReferenceName := string(sourceType) + "-data-source" - credentialTool, err := v1.CredentialTool(req.Ctx, req.Client, source.Namespace, toolReferenceName) + credentialTools, err := v1.CredentialTools(req.Ctx, req.Client, source.Namespace, toolReferenceName) if err != nil { return err } @@ -252,7 +252,7 @@ func (k *Handler) Sync(req router.Request, _ router.Response) error { return err } - if credentialTool != "" && (authStatus.Required == nil || (*authStatus.Required && !authStatus.Authenticated)) { + if len(credentialTools) > 0 && (authStatus.Required == nil || (*authStatus.Required && !authStatus.Authenticated)) { return nil } diff --git a/pkg/controller/handlers/oauthapp/oauthapplogin.go b/pkg/controller/handlers/oauthapp/oauthapplogin.go index 496d607a4..b8b269632 100644 --- a/pkg/controller/handlers/oauthapp/oauthapplogin.go +++ b/pkg/controller/handlers/oauthapp/oauthapplogin.go @@ -33,8 +33,8 @@ func (h *LoginHandler) RunTool(req router.Request, _ router.Response) error { return nil } - credentialTool, err := v1.CredentialTool(req.Ctx, req.Client, login.Namespace, login.Spec.ToolReference) - if err != nil || credentialTool == "" { + credentialTools, err := v1.CredentialTools(req.Ctx, req.Client, login.Namespace, login.Spec.ToolReference) + if err != nil || len(credentialTools) == 0 { return err } @@ -59,7 +59,7 @@ func (h *LoginHandler) RunTool(req router.Request, _ router.Response) error { task, err := h.invoker.SystemTask(req.Ctx, &thread, []gptscript.ToolDef{ { - Credentials: []string{credentialTool}, + Credentials: credentialTools, Instructions: "#!sys.echo DONE", }, }, "", invoke.SystemTaskOptions{ diff --git a/pkg/controller/handlers/toolinfo/toolinfo.go b/pkg/controller/handlers/toolinfo/toolinfo.go new file mode 100644 index 000000000..93281b152 --- /dev/null +++ b/pkg/controller/handlers/toolinfo/toolinfo.go @@ -0,0 +1,75 @@ +package toolinfo + +import ( + "fmt" + + "github.com/gptscript-ai/go-gptscript" + "github.com/obot-platform/nah/pkg/router" + "github.com/obot-platform/obot/apiclient/types" + v1 "github.com/obot-platform/obot/pkg/storage/apis/otto.otto8.ai/v1" + apierror "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/sets" +) + +type Handler struct { + gptscript *gptscript.GPTScript +} + +func New(gptscript *gptscript.GPTScript) *Handler { + return &Handler{ + gptscript: gptscript, + } +} + +// SetToolInfoStatus will set the tool information for the object. This includes credential information, +// and whether those credentials exist. +// This handler should be used with the generationed.UpdateObservedGeneration to ensure that the processing +// is correctly reported to through the API. +func (h *Handler) SetToolInfoStatus(req router.Request, resp router.Response) (err error) { + defer func() { + if err != nil { + resp.Attributes()["generation:errored"] = true + } + }() + + // Get all the credentials that exist in the expected context. + creds, err := h.gptscript.ListCredentials(req.Ctx, gptscript.ListCredentialsOptions{ + CredentialContexts: []string{req.Name}, + }) + if err != nil { + return err + } + credsSet := make(sets.Set[string], len(creds)) + for _, cred := range creds { + credsSet.Insert(cred.ToolName) + } + + obj := req.Object.(v1.ToolUser) + tools := obj.GetTools() + toolInfos := make(map[string]types.ToolInfo, len(tools)) + + var toolRef v1.ToolReference + for _, tool := range tools { + if err := req.Get(&toolRef, req.Namespace, tool); apierror.IsNotFound(err) { + continue + } else if err != nil { + return err + } else if toolRef.Status.Tool == nil { + return fmt.Errorf("cannot determine credential status for tool %s: no tool status found", tool) + } + + toolInfos[tool] = types.ToolInfo{ + CredentialNames: toolRef.Status.Tool.CredentialNames, + Authorized: credsSet.HasAll(toolRef.Status.Tool.CredentialNames...), + } + + // Clear the field we care about in this loop. + // This allows us to use the same variable for the whole loop + // while ensuring that the value we care about is loaded correctly. + toolRef.Status.Tool.CredentialNames = nil + } + + obj.SetToolInfos(toolInfos) + + return nil +} diff --git a/pkg/controller/handlers/toolreference/toolreference.go b/pkg/controller/handlers/toolreference/toolreference.go index 7eb2dd0a0..bb02e9753 100644 --- a/pkg/controller/handlers/toolreference/toolreference.go +++ b/pkg/controller/handlers/toolreference/toolreference.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "path" + "slices" "strings" "time" @@ -251,17 +252,66 @@ func (h *Handler) Populate(req router.Request, resp router.Response) error { } } } - if len(tool.Credentials) == 1 { - if strings.HasPrefix(tool.Credentials[0], ".") { - toolName, _ := gtypes.SplitToolRef(toolRef.Spec.Reference) - refURL, err := url.Parse(toolName) - if err == nil { - refURL.Path = path.Join(refURL.Path, tool.Credentials[0]) - // Don't need to check the error because we are unescaping something that was produced directly from the url package. - toolRef.Status.Tool.Credential, _ = url.PathUnescape(refURL.String()) + + // The available tool references from this tool are the tool itself and any tool this tool exports. + toolRefs := make([]gptscript.ToolReference, 0, len(tool.Export)+1) + toolRefs = append(toolRefs, gptscript.ToolReference{ + Reference: toolRef.Spec.Reference, + ToolID: prg.EntryToolID, + }) + for _, exportedTool := range tool.Export { + toolRefs = append(toolRefs, tool.ToolMapping[exportedTool]...) + } + + toolRef.Status.Tool.Credentials = make([]string, 0, len(tool.Credentials)+len(tool.Export)) + toolRef.Status.Tool.CredentialNames = make([]string, 0, len(tool.Credentials)+len(tool.Export)) + for _, ref := range toolRefs { + t := prg.ToolSet[ref.ToolID] + for _, cred := range t.Credentials { + parsedCred := cred + credToolName, credSubTool := gtypes.SplitToolRef(cred) + if strings.HasPrefix(credToolName, ".") { + toolName, _ := gtypes.SplitToolRef(ref.Reference) + if !path.IsAbs(toolName) { + if !strings.HasPrefix(toolName, ".") { + toolName, _ = gtypes.SplitToolRef(toolRef.Spec.Reference) + } else { + toolName = path.Join(toolRef.Spec.Reference, toolName) + } + } + + refURL, err := url.Parse(toolName) + if err != nil { + continue + } + + refURL.Path = path.Join(refURL.Path, credToolName) + parsedCred = refURL.String() + if refURL.Host == "" { + // This is only a path, so url unescape it. + // No need to check the error here, we would have errored when parsing. + parsedCred, _ = url.PathUnescape(parsedCred) + } + + if credSubTool != "" { + parsedCred = fmt.Sprintf("%s from %s", credSubTool, parsedCred) + } + } + + if parsedCred != "" && !slices.Contains(toolRef.Status.Tool.Credentials, parsedCred) { + toolRef.Status.Tool.Credentials = append(toolRef.Status.Tool.Credentials, parsedCred) + } + + credNames, err := determineCredentialNames(prg, prg.ToolSet[ref.ToolID], cred) + if err != nil { + toolRef.Status.Error = err.Error() + } + + for _, n := range credNames { + if !slices.Contains(toolRef.Status.Tool.CredentialNames, n) { + toolRef.Status.Tool.CredentialNames = append(toolRef.Status.Tool.CredentialNames, n) + } } - } else { - toolRef.Status.Tool.Credential = tool.Credentials[0] } } @@ -460,3 +510,45 @@ func (h *Handler) CleanupModelProvider(req router.Request, _ router.Response) er func modelName(modelProviderName, modelName string) string { return name.SafeConcatName(system.ModelPrefix, modelProviderName, fmt.Sprintf("%x", sha256.Sum256([]byte(modelName)))) } + +func determineCredentialNames(prg *gptscript.Program, tool gptscript.Tool, toolName string) ([]string, error) { + toolName, alias, args, err := gtypes.ParseCredentialArgs(toolName, "") + if err != nil { + return nil, err + } + + if alias != "" { + return []string{alias}, nil + } + + if args == nil { + // This is a tool and not the credential format. Parse the tool from the program to determine the alias + toolNames := make([]string, 0, len(tool.Credentials)) + for _, cred := range tool.Credentials { + if cred == toolName { + if len(tool.ToolMapping[cred]) == 0 { + return nil, fmt.Errorf("cannot find credential name for tool %q", toolName) + } + + for _, ref := range tool.ToolMapping[cred] { + for _, c := range prg.ToolSet[ref.ToolID].ExportCredentials { + names, err := determineCredentialNames(prg, prg.ToolSet[ref.ToolID], c) + if err != nil { + return nil, err + } + + toolNames = append(toolNames, names...) + } + } + } + } + + if len(toolNames) > 0 { + return toolNames, nil + } + + return nil, fmt.Errorf("tool %q not found in program", toolName) + } + + return []string{toolName}, nil +} diff --git a/pkg/controller/handlers/workflow/oauth.go b/pkg/controller/handlers/workflow/oauth.go index 80a15e99b..55772a937 100644 --- a/pkg/controller/handlers/workflow/oauth.go +++ b/pkg/controller/handlers/workflow/oauth.go @@ -31,11 +31,11 @@ func BackPopulateAuthStatus(req router.Request, _ router.Response) error { } var required *bool - credentialTool, err := v1.CredentialTool(req.Ctx, req.Client, workflow.Namespace, login.Spec.ToolReference) + credentialTools, err := v1.CredentialTools(req.Ctx, req.Client, workflow.Namespace, login.Spec.ToolReference) if err != nil { login.Status.External.Error = fmt.Sprintf("failed to get credential tool for knowledge source [%s]: %v", workflow.Name, err) } else { - required = &[]bool{credentialTool != ""}[0] + required = &[]bool{len(credentialTools) > 0}[0] updateRequired = updateRequired || login.Status.External.Required == nil || *login.Status.External.Required != *required login.Status.External.Required = required } diff --git a/pkg/controller/routes.go b/pkg/controller/routes.go index e8b1e5661..f800ee8ed 100644 --- a/pkg/controller/routes.go +++ b/pkg/controller/routes.go @@ -2,6 +2,7 @@ package controller import ( "github.com/obot-platform/nah/pkg/handlers" + "github.com/obot-platform/obot/pkg/controller/generationed" "github.com/obot-platform/obot/pkg/controller/handlers/agents" "github.com/obot-platform/obot/pkg/controller/handlers/alias" "github.com/obot-platform/obot/pkg/controller/handlers/cleanup" @@ -13,6 +14,7 @@ import ( "github.com/obot-platform/obot/pkg/controller/handlers/oauthapp" "github.com/obot-platform/obot/pkg/controller/handlers/runs" "github.com/obot-platform/obot/pkg/controller/handlers/threads" + "github.com/obot-platform/obot/pkg/controller/handlers/toolinfo" "github.com/obot-platform/obot/pkg/controller/handlers/toolreference" "github.com/obot-platform/obot/pkg/controller/handlers/webhook" "github.com/obot-platform/obot/pkg/controller/handlers/workflow" @@ -37,6 +39,7 @@ func (c *Controller) setupRoutes() error { cronJobs := cronjob.New() oauthLogins := oauthapp.NewLogin(c.services.Invoker, c.services.ServerURL) knowledgesummary := knowledgesummary.NewHandler(c.services.GPTClient) + toolInfo := toolinfo.New(c.services.GPTClient) // Runs root.Type(&v1.Run{}).FinalizeFunc(v1.RunFinalizer, runs.DeleteRunState) @@ -59,6 +62,9 @@ func (c *Controller) setupRoutes() error { root.Type(&v1.Workflow{}).HandlerFunc(workflow.CreateWorkspaceAndKnowledgeSet) root.Type(&v1.Workflow{}).HandlerFunc(workflow.BackPopulateAuthStatus) root.Type(&v1.Workflow{}).HandlerFunc(cleanup.Cleanup) + root.Type(&v1.Workflow{}).HandlerFunc(alias.AssignAlias) + root.Type(&v1.Workflow{}).HandlerFunc(toolInfo.SetToolInfoStatus) + root.Type(&v1.Workflow{}).HandlerFunc(generationed.UpdateObservedGeneration) // WorkflowExecutions root.Type(&v1.WorkflowExecution{}).HandlerFunc(cleanup.Cleanup) @@ -68,6 +74,9 @@ func (c *Controller) setupRoutes() error { // Agents root.Type(&v1.Agent{}).HandlerFunc(agents.CreateWorkspaceAndKnowledgeSet) root.Type(&v1.Agent{}).HandlerFunc(agents.BackPopulateAuthStatus) + root.Type(&v1.Agent{}).HandlerFunc(alias.AssignAlias) + root.Type(&v1.Agent{}).HandlerFunc(toolInfo.SetToolInfoStatus) + root.Type(&v1.Agent{}).HandlerFunc(generationed.UpdateObservedGeneration) // Uploads root.Type(&v1.KnowledgeSource{}).HandlerFunc(cleanup.Cleanup) @@ -75,17 +84,22 @@ func (c *Controller) setupRoutes() error { root.Type(&v1.KnowledgeSource{}).HandlerFunc(knowledgesource.Reschedule) root.Type(&v1.KnowledgeSource{}).HandlerFunc(knowledgesource.Sync) - // ToolReference + // ToolReferences root.Type(&v1.ToolReference{}).HandlerFunc(toolRef.BackPopulateModels) root.Type(&v1.ToolReference{}).HandlerFunc(toolRef.Populate) root.Type(&v1.ToolReference{}).FinalizeFunc(v1.ToolReferenceFinalizer, toolRef.CleanupModelProvider) - // Reference - root.Type(&v1.Agent{}).HandlerFunc(alias.AssignAlias) + // EmailReceivers root.Type(&v1.EmailReceiver{}).HandlerFunc(alias.AssignAlias) - root.Type(&v1.Workflow{}).HandlerFunc(alias.AssignAlias) + root.Type(&v1.EmailReceiver{}).HandlerFunc(generationed.UpdateObservedGeneration) + + // Models root.Type(&v1.Model{}).HandlerFunc(alias.AssignAlias) + root.Type(&v1.Model{}).HandlerFunc(generationed.UpdateObservedGeneration) + + // DefaultModelAliases root.Type(&v1.DefaultModelAlias{}).HandlerFunc(alias.AssignAlias) + root.Type(&v1.DefaultModelAlias{}).HandlerFunc(generationed.UpdateObservedGeneration) // Knowledge files root.Type(&v1.KnowledgeFile{}).HandlerFunc(cleanup.Cleanup) diff --git a/pkg/storage/apis/otto.otto8.ai/v1/agent.go b/pkg/storage/apis/otto.otto8.ai/v1/agent.go index 42c7f71de..6173fec84 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/agent.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/agent.go @@ -2,6 +2,7 @@ package v1 import ( "context" + "slices" "github.com/obot-platform/nah/pkg/fields" "github.com/obot-platform/obot/apiclient/types" @@ -12,6 +13,7 @@ import ( var ( _ fields.Fields = (*Agent)(nil) _ Aliasable = (*Agent)(nil) + _ Generationed = (*Agent)(nil) ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -36,12 +38,24 @@ func (a *Agent) SetAssigned(assigned bool) { a.Status.AliasAssigned = assigned } -func (a *Agent) GetAliasObservedGeneration() int64 { - return a.Status.AliasObservedGeneration +func (a *Agent) GetObservedGeneration() int64 { + return a.Status.ObservedGeneration } -func (a *Agent) SetAliasObservedGeneration(gen int64) { - a.Status.AliasObservedGeneration = gen +func (a *Agent) SetObservedGeneration(gen int64) { + a.Status.ObservedGeneration = gen +} + +func (a *Agent) GetTools() []string { + return slices.Concat(a.Spec.Manifest.Tools, a.Spec.Manifest.DefaultThreadTools, a.Spec.Manifest.AvailableThreadTools) +} + +func (a *Agent) GetToolInfos() map[string]types.ToolInfo { + return a.Status.ToolInfo +} + +func (a *Agent) SetToolInfos(toolInfos map[string]types.ToolInfo) { + a.Status.ToolInfo = toolInfos } func (a *Agent) Has(field string) bool { @@ -74,11 +88,12 @@ type AgentSpec struct { } type AgentStatus struct { - KnowledgeSetNames []string `json:"knowledgeSetNames,omitempty"` - WorkspaceName string `json:"workspaceName,omitempty"` - AliasAssigned bool `json:"aliasAssigned,omitempty"` - AuthStatus map[string]types.OAuthAppLoginAuthStatus `json:"authStatus,omitempty"` - AliasObservedGeneration int64 `json:"aliasProcessed,omitempty"` + KnowledgeSetNames []string `json:"knowledgeSetNames,omitempty"` + WorkspaceName string `json:"workspaceName,omitempty"` + AliasAssigned bool `json:"aliasAssigned,omitempty"` + AuthStatus map[string]types.OAuthAppLoginAuthStatus `json:"authStatus,omitempty"` + ToolInfo map[string]types.ToolInfo `json:"toolInfo,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -90,12 +105,12 @@ type AgentList struct { Items []Agent `json:"items"` } -func CredentialTool(ctx context.Context, c kclient.Client, namespace string, toolReferenceName string) (string, error) { +func CredentialTools(ctx context.Context, c kclient.Client, namespace string, toolReferenceName string) ([]string, error) { var toolReference ToolReference err := c.Get(ctx, kclient.ObjectKey{Namespace: namespace, Name: toolReferenceName}, &toolReference) if err != nil || toolReference.Status.Tool == nil { - return "", err + return nil, err } - return toolReference.Status.Tool.Credential, nil + return toolReference.Status.Tool.Credentials, nil } diff --git a/pkg/storage/apis/otto.otto8.ai/v1/alias.go b/pkg/storage/apis/otto.otto8.ai/v1/alias.go index 1ec7868a6..d37b46f2d 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/alias.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/alias.go @@ -44,11 +44,10 @@ type AliasSpec struct { type Aliasable interface { kclient.Object + Generationed GetAliasName() string SetAssigned(bool) IsAssigned() bool - GetAliasObservedGeneration() int64 - SetAliasObservedGeneration(int64) } // +k8s:deepcopy-gen=false diff --git a/pkg/storage/apis/otto.otto8.ai/v1/defaultmodelalias.go b/pkg/storage/apis/otto.otto8.ai/v1/defaultmodelalias.go index a983ea92b..10a642f79 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/defaultmodelalias.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/defaultmodelalias.go @@ -5,6 +5,10 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +var ( + _ Generationed = (*DefaultModelAlias)(nil) +) + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type DefaultModelAlias struct { @@ -29,11 +33,11 @@ func (a *DefaultModelAlias) GetAliasScope() string { return "Model" } -func (a *DefaultModelAlias) GetAliasObservedGeneration() int64 { +func (a *DefaultModelAlias) GetObservedGeneration() int64 { return a.Generation } -func (a *DefaultModelAlias) SetAliasObservedGeneration(int64) {} +func (a *DefaultModelAlias) SetObservedGeneration(int64) {} type DefaultModelAliasSpec struct { Manifest types.DefaultModelAliasManifest `json:"manifest"` diff --git a/pkg/storage/apis/otto.otto8.ai/v1/emailaddress.go b/pkg/storage/apis/otto.otto8.ai/v1/emailaddress.go index a59e9fb68..d69a5f713 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/emailaddress.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/emailaddress.go @@ -11,6 +11,7 @@ import ( var ( _ Aliasable = (*EmailReceiver)(nil) + _ Generationed = (*EmailReceiver)(nil) _ fields.Fields = (*EmailReceiver)(nil) ) @@ -52,12 +53,12 @@ func (in *EmailReceiver) IsAssigned() bool { return in.Status.AliasAssigned } -func (in *EmailReceiver) GetAliasObservedGeneration() int64 { - return in.Status.AliasObservedGeneration +func (in *EmailReceiver) GetObservedGeneration() int64 { + return in.Status.ObservedGeneration } -func (in *EmailReceiver) SetAliasObservedGeneration(gen int64) { - in.Status.AliasObservedGeneration = gen +func (in *EmailReceiver) SetObservedGeneration(gen int64) { + in.Status.ObservedGeneration = gen } func (*EmailReceiver) GetColumns() [][]string { @@ -85,8 +86,8 @@ type EmailReceiverSpec struct { } type EmailReceiverStatus struct { - AliasAssigned bool `json:"aliasAssigned,omitempty"` - AliasObservedGeneration int64 `json:"aliasProcessed,omitempty"` + AliasAssigned bool `json:"aliasAssigned,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/storage/apis/otto.otto8.ai/v1/generationed.go b/pkg/storage/apis/otto.otto8.ai/v1/generationed.go new file mode 100644 index 000000000..3b395657f --- /dev/null +++ b/pkg/storage/apis/otto.otto8.ai/v1/generationed.go @@ -0,0 +1,8 @@ +package v1 + +// +k8s:deepcopy-gen=false + +type Generationed interface { + GetObservedGeneration() int64 + SetObservedGeneration(int64) +} diff --git a/pkg/storage/apis/otto.otto8.ai/v1/model.go b/pkg/storage/apis/otto.otto8.ai/v1/model.go index 3263828c8..c58b6a5ae 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/model.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/model.go @@ -6,7 +6,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var _ fields.Fields = (*Model)(nil) +var ( + _ fields.Fields = (*Model)(nil) + _ Aliasable = (*Model)(nil) + _ Generationed = (*Model)(nil) +) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -48,12 +52,12 @@ func (m *Model) SetAssigned(assigned bool) { m.Status.AliasAssigned = assigned } -func (m *Model) GetAliasObservedGeneration() int64 { - return m.Status.AliasObservedGeneration +func (m *Model) GetObservedGeneration() int64 { + return m.Status.ObservedGeneration } -func (m *Model) SetAliasObservedGeneration(gen int64) { - m.Status.AliasObservedGeneration = gen +func (m *Model) SetObservedGeneration(gen int64) { + m.Status.ObservedGeneration = gen } type ModelSpec struct { @@ -61,8 +65,8 @@ type ModelSpec struct { } type ModelStatus struct { - AliasAssigned bool `json:"aliasAssigned,omitempty"` - AliasObservedGeneration int64 `json:"aliasProcessed,omitempty"` + AliasAssigned bool `json:"aliasAssigned,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/storage/apis/otto.otto8.ai/v1/oauthapp.go b/pkg/storage/apis/otto.otto8.ai/v1/oauthapp.go index dc5abf4e5..0ead375a5 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/oauthapp.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/oauthapp.go @@ -34,11 +34,11 @@ func (r *OAuthApp) IsAssigned() bool { return true } -func (r *OAuthApp) GetAliasObservedGeneration() int64 { +func (r *OAuthApp) GetObservedGeneration() int64 { return r.Generation } -func (r *OAuthApp) SetAliasObservedGeneration(int64) {} +func (r *OAuthApp) SetObservedGeneration(int64) {} func (r *OAuthApp) Has(field string) bool { return r.Get(field) != "" diff --git a/pkg/storage/apis/otto.otto8.ai/v1/run.go b/pkg/storage/apis/otto.otto8.ai/v1/run.go index f51398e61..c9c1c2f25 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/run.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/run.go @@ -15,6 +15,8 @@ const ( ToolReferenceFinalizer = "otto.otto8.ai/tool-reference" ModelProviderSyncAnnotation = "otto8.ai/model-provider-sync" + WorkflowSyncAnnotation = "otto8.ai/workflow-sync" + AgentSyncAnnotation = "otto8.ai/agent-sync" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/storage/apis/otto.otto8.ai/v1/tool.go b/pkg/storage/apis/otto.otto8.ai/v1/tool.go new file mode 100644 index 000000000..e7eb8e3c4 --- /dev/null +++ b/pkg/storage/apis/otto.otto8.ai/v1/tool.go @@ -0,0 +1,12 @@ +package v1 + +import "github.com/obot-platform/obot/apiclient/types" + +// +k8s:deepcopy-gen=false + +type ToolUser interface { + Generationed + GetTools() []string + GetToolInfos() map[string]types.ToolInfo + SetToolInfos(map[string]types.ToolInfo) +} diff --git a/pkg/storage/apis/otto.otto8.ai/v1/toolreference.go b/pkg/storage/apis/otto.otto8.ai/v1/toolreference.go index 90e3a1459..49842d364 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/toolreference.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/toolreference.go @@ -60,7 +60,11 @@ type ToolShortDescription struct { Description string `json:"description,omitempty"` Params map[string]string `json:"params,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` - Credential string `json:"credential,omitempty"` + // Credentials are all the credentials for this tool, including for tools exported by this tool. + Credentials []string `json:"credentials,omitempty"` + // CredentialNames are the names of the credentials for each tool. This is different from the Credentials field + // because these names could be aliases and identifies which tools have the same credential. + CredentialNames []string `json:"credentialNames,omitempty"` } type ToolReferenceStatus struct { diff --git a/pkg/storage/apis/otto.otto8.ai/v1/webhook.go b/pkg/storage/apis/otto.otto8.ai/v1/webhook.go index 4b7f3b7ca..0426974f1 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/webhook.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/webhook.go @@ -52,12 +52,12 @@ func (w *Webhook) IsAssigned() bool { return w.Status.AliasAssigned } -func (w *Webhook) GetAliasObservedGeneration() int64 { - return w.Status.AliasObservedGeneration +func (w *Webhook) GetObservedGeneration() int64 { + return w.Status.ObservedGeneration } -func (w *Webhook) SetAliasObservedGeneration(gen int64) { - w.Status.AliasObservedGeneration = gen +func (w *Webhook) SetObservedGeneration(gen int64) { + w.Status.ObservedGeneration = gen } func (*Webhook) GetColumns() [][]string { @@ -89,7 +89,7 @@ type WebhookSpec struct { type WebhookStatus struct { AliasAssigned bool `json:"aliasAssigned,omitempty"` LastSuccessfulRunCompleted *metav1.Time `json:"lastSuccessfulRunCompleted,omitempty"` - AliasObservedGeneration int64 `json:"aliasProcessed,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/storage/apis/otto.otto8.ai/v1/workflow.go b/pkg/storage/apis/otto.otto8.ai/v1/workflow.go index c9bb93890..7ae51079f 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/workflow.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/workflow.go @@ -10,6 +10,7 @@ import ( var ( _ Aliasable = (*Workflow)(nil) + _ Generationed = (*Workflow)(nil) _ AliasScoped = (*Workflow)(nil) _ fields.Fields = (*Workflow)(nil) ) @@ -58,12 +59,24 @@ func (in *Workflow) GetAliasScope() string { return "Agent" } -func (in *Workflow) GetAliasObservedGeneration() int64 { - return in.Status.AliasObservedGeneration +func (in *Workflow) GetObservedGeneration() int64 { + return in.Status.ObservedGeneration } -func (in *Workflow) SetAliasObservedGeneration(gen int64) { - in.Status.AliasObservedGeneration = gen +func (in *Workflow) SetObservedGeneration(gen int64) { + in.Status.ObservedGeneration = gen +} + +func (in *Workflow) GetTools() []string { + return slices.Concat(in.Spec.Manifest.Tools, in.Spec.Manifest.DefaultThreadTools, in.Spec.Manifest.AvailableThreadTools) +} + +func (in *Workflow) GetToolInfos() map[string]types.ToolInfo { + return in.Status.ToolInfo +} + +func (in *Workflow) SetToolInfos(toolInfos map[string]types.ToolInfo) { + in.Status.ToolInfo = toolInfos } type WorkflowSpec struct { @@ -86,11 +99,12 @@ func (in *Workflow) DeleteRefs() []Ref { } type WorkflowStatus struct { - WorkspaceName string `json:"workspaceName,omitempty"` - KnowledgeSetNames []string `json:"knowledgeSetNames,omitempty"` - AliasAssigned bool `json:"aliasAssigned,omitempty"` - AuthStatus map[string]types.OAuthAppLoginAuthStatus `json:"authStatus,omitempty"` - AliasObservedGeneration int64 `json:"aliasProcessed,omitempty"` + WorkspaceName string `json:"workspaceName,omitempty"` + KnowledgeSetNames []string `json:"knowledgeSetNames,omitempty"` + AliasAssigned bool `json:"aliasAssigned,omitempty"` + AuthStatus map[string]types.OAuthAppLoginAuthStatus `json:"authStatus,omitempty"` + ToolInfo map[string]types.ToolInfo `json:"toolInfo,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/storage/apis/otto.otto8.ai/v1/zz_generated.deepcopy.go b/pkg/storage/apis/otto.otto8.ai/v1/zz_generated.deepcopy.go index d0e8a6e5e..38d318f8b 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/zz_generated.deepcopy.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/zz_generated.deepcopy.go @@ -120,6 +120,13 @@ func (in *AgentStatus) DeepCopyInto(out *AgentStatus) { (*out)[key] = *val.DeepCopy() } } + if in.ToolInfo != nil { + in, out := &in.ToolInfo, &out.ToolInfo + *out = make(map[string]types.ToolInfo, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentStatus. @@ -1601,6 +1608,16 @@ func (in *ToolShortDescription) DeepCopyInto(out *ToolShortDescription) { (*out)[key] = val } } + if in.Credentials != nil { + in, out := &in.Credentials, &out.Credentials + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.CredentialNames != nil { + in, out := &in.CredentialNames, &out.CredentialNames + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ToolShortDescription. @@ -1910,6 +1927,13 @@ func (in *WorkflowStatus) DeepCopyInto(out *WorkflowStatus) { (*out)[key] = *val.DeepCopy() } } + if in.ToolInfo != nil { + in, out := &in.ToolInfo, &out.ToolInfo + *out = make(map[string]types.ToolInfo, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowStatus. diff --git a/pkg/storage/openapi/generated/openapi_generated.go b/pkg/storage/openapi/generated/openapi_generated.go index c6e98663f..467b7b8ba 100644 --- a/pkg/storage/openapi/generated/openapi_generated.go +++ b/pkg/storage/openapi/generated/openapi_generated.go @@ -89,6 +89,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/obot-platform/obot/apiclient/types.ThreadManifest": schema_obot_platform_obot_apiclient_types_ThreadManifest(ref), "github.com/obot-platform/obot/apiclient/types.Time": schema_obot_platform_obot_apiclient_types_Time(ref), "github.com/obot-platform/obot/apiclient/types.ToolCall": schema_obot_platform_obot_apiclient_types_ToolCall(ref), + "github.com/obot-platform/obot/apiclient/types.ToolInfo": schema_obot_platform_obot_apiclient_types_ToolInfo(ref), "github.com/obot-platform/obot/apiclient/types.ToolInput": schema_obot_platform_obot_apiclient_types_ToolInput(ref), "github.com/obot-platform/obot/apiclient/types.ToolReference": schema_obot_platform_obot_apiclient_types_ToolReference(ref), "github.com/obot-platform/obot/apiclient/types.ToolReferenceList": schema_obot_platform_obot_apiclient_types_ToolReferenceList(ref), @@ -292,6 +293,21 @@ func schema_obot_platform_obot_apiclient_types_Agent(ref common.ReferenceCallbac }, }, }, + "toolInfo": { + SchemaProps: spec.SchemaProps{ + Description: "ToolInfo provides information about the tools for this agent, like which credentials they use and whether that credential has been created. This is a pointer so that we can distinguish between an empty map (no tool information) and nil (tool information not processed yet).", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/obot-platform/obot/apiclient/types.ToolInfo"), + }, + }, + }, + }, + }, "textEmbeddingModel": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -303,7 +319,7 @@ func schema_obot_platform_obot_apiclient_types_Agent(ref common.ReferenceCallbac }, }, Dependencies: []string{ - "github.com/obot-platform/obot/apiclient/types.AgentManifest", "github.com/obot-platform/obot/apiclient/types.Metadata", "github.com/obot-platform/obot/apiclient/types.OAuthAppLoginAuthStatus"}, + "github.com/obot-platform/obot/apiclient/types.AgentManifest", "github.com/obot-platform/obot/apiclient/types.Metadata", "github.com/obot-platform/obot/apiclient/types.OAuthAppLoginAuthStatus", "github.com/obot-platform/obot/apiclient/types.ToolInfo"}, } } @@ -3497,6 +3513,40 @@ func schema_obot_platform_obot_apiclient_types_ToolCall(ref common.ReferenceCall } } +func schema_obot_platform_obot_apiclient_types_ToolInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "credentialNames": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "authorized": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"authorized"}, + }, + }, + } +} + func schema_obot_platform_obot_apiclient_types_ToolInput(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -3586,8 +3636,16 @@ func schema_obot_platform_obot_apiclient_types_ToolReference(ref common.Referenc }, "credential": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, }, }, "params": { @@ -3998,6 +4056,21 @@ func schema_obot_platform_obot_apiclient_types_Workflow(ref common.ReferenceCall }, }, }, + "toolInfo": { + SchemaProps: spec.SchemaProps{ + Description: "ToolInfo provides information about the tools for this workflow, like which credentials they use and whether that credential has been created. This is a pointer so that we can distinguish between an empty map (no tool information) and nil (tool information not processed yet).", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/obot-platform/obot/apiclient/types.ToolInfo"), + }, + }, + }, + }, + }, "textEmbeddingModel": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -4009,7 +4082,7 @@ func schema_obot_platform_obot_apiclient_types_Workflow(ref common.ReferenceCall }, }, Dependencies: []string{ - "github.com/obot-platform/obot/apiclient/types.Metadata", "github.com/obot-platform/obot/apiclient/types.OAuthAppLoginAuthStatus", "github.com/obot-platform/obot/apiclient/types.WorkflowManifest"}, + "github.com/obot-platform/obot/apiclient/types.Metadata", "github.com/obot-platform/obot/apiclient/types.OAuthAppLoginAuthStatus", "github.com/obot-platform/obot/apiclient/types.ToolInfo", "github.com/obot-platform/obot/apiclient/types.WorkflowManifest"}, } } @@ -4609,7 +4682,21 @@ func schema_storage_apis_ottootto8ai_v1_AgentStatus(ref common.ReferenceCallback }, }, }, - "aliasProcessed": { + "toolInfo": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/obot-platform/obot/apiclient/types.ToolInfo"), + }, + }, + }, + }, + }, + "observedGeneration": { SchemaProps: spec.SchemaProps{ Type: []string{"integer"}, Format: "int64", @@ -4619,7 +4706,7 @@ func schema_storage_apis_ottootto8ai_v1_AgentStatus(ref common.ReferenceCallback }, }, Dependencies: []string{ - "github.com/obot-platform/obot/apiclient/types.OAuthAppLoginAuthStatus"}, + "github.com/obot-platform/obot/apiclient/types.OAuthAppLoginAuthStatus", "github.com/obot-platform/obot/apiclient/types.ToolInfo"}, } } @@ -5222,7 +5309,7 @@ func schema_storage_apis_ottootto8ai_v1_EmailReceiverStatus(ref common.Reference Format: "", }, }, - "aliasProcessed": { + "observedGeneration": { SchemaProps: spec.SchemaProps{ Type: []string{"integer"}, Format: "int64", @@ -6156,7 +6243,7 @@ func schema_storage_apis_ottootto8ai_v1_ModelStatus(ref common.ReferenceCallback Format: "", }, }, - "aliasProcessed": { + "observedGeneration": { SchemaProps: spec.SchemaProps{ Type: []string{"integer"}, Format: "int64", @@ -7451,10 +7538,34 @@ func schema_storage_apis_ottootto8ai_v1_ToolShortDescription(ref common.Referenc }, }, }, - "credential": { + "credentials": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Description: "Credentials are all the credentials for this tool, including for tools exported by this tool.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "credentialNames": { + SchemaProps: spec.SchemaProps{ + Description: "CredentialNames are the names of the credentials for each tool. This is different from the Credentials field because these names could be aliases and identifies which tools have the same credential.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, }, }, }, @@ -7656,7 +7767,7 @@ func schema_storage_apis_ottootto8ai_v1_WebhookStatus(ref common.ReferenceCallba Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), }, }, - "aliasProcessed": { + "observedGeneration": { SchemaProps: spec.SchemaProps{ Type: []string{"integer"}, Format: "int64", @@ -8101,7 +8212,21 @@ func schema_storage_apis_ottootto8ai_v1_WorkflowStatus(ref common.ReferenceCallb }, }, }, - "aliasProcessed": { + "toolInfo": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/obot-platform/obot/apiclient/types.ToolInfo"), + }, + }, + }, + }, + }, + "observedGeneration": { SchemaProps: spec.SchemaProps{ Type: []string{"integer"}, Format: "int64", @@ -8111,7 +8236,7 @@ func schema_storage_apis_ottootto8ai_v1_WorkflowStatus(ref common.ReferenceCallb }, }, Dependencies: []string{ - "github.com/obot-platform/obot/apiclient/types.OAuthAppLoginAuthStatus"}, + "github.com/obot-platform/obot/apiclient/types.OAuthAppLoginAuthStatus", "github.com/obot-platform/obot/apiclient/types.ToolInfo"}, } }