diff --git a/Dockerfile b/Dockerfile index ceb6b50f9..2904742a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ ENV PGDATA=/data/postgresql COPY --from=build-pgvector /usr/lib/postgresql17/vector.so /usr/lib/postgresql17/ COPY --from=build-pgvector /usr/share/postgresql17/extension/vector* /usr/share/postgresql17/extension/ -RUN apk add --no-cache git python-3.13 py3.13-pip npm bash tini procps libreoffice docker +RUN apk add --no-cache git python-3.13 py3.13-pip npm bash tini procps libreoffice docker perl-utils COPY --chmod=0755 /tools/package-chrome.sh / RUN /package-chrome.sh && rm /package-chrome.sh @@ -58,6 +58,7 @@ EXPOSE 22 ENV PATH=$PATH:/usr/lib/libreoffice/program ENV HOME=/data ENV XDG_CACHE_HOME=/data/cache +ENV OBOT_SERVER_AGENTS_DIR=/agents ENV OBOT_SERVER_ENCRYPTION_CONFIG_FILE=/encryption.yaml ENV BAAAH_THREADINESS=20 ENV TERM=vt100 diff --git a/go.mod b/go.mod index c53f9ab14..f072cb252 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/gptscript-ai/chat-completion-client v0.0.0-20241219123536-85c44096bc10 github.com/gptscript-ai/cmd v0.0.0-20250122115124-a3d65e9d2432 github.com/gptscript-ai/go-gptscript v0.9.6-0.20241216211344-79a66826cf82 - github.com/gptscript-ai/gptscript v0.9.6-0.20250120172457-3f876b2ef42b + github.com/gptscript-ai/gptscript v0.9.6-0.20250122033232-c02c4cbaa1cc github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de github.com/mhale/smtpd v0.8.3 github.com/obot-platform/kinm v0.0.0-20250116162656-270198b40c6d diff --git a/go.sum b/go.sum index 4e6c387ea..2ae3e8537 100644 --- a/go.sum +++ b/go.sum @@ -302,8 +302,8 @@ github.com/gptscript-ai/cmd v0.0.0-20250122115124-a3d65e9d2432 h1:cJh/Hl1HFd1qLp github.com/gptscript-ai/cmd v0.0.0-20250122115124-a3d65e9d2432/go.mod h1:DJAo1xTht1LDkNYFNydVjTHd576TC7MlpsVRl3oloVw= github.com/gptscript-ai/go-gptscript v0.9.6-0.20241216211344-79a66826cf82 h1:BEN268Z92gqeDc51XVvWdJWdQ47BuuWH3MUysHzilfI= github.com/gptscript-ai/go-gptscript v0.9.6-0.20241216211344-79a66826cf82/go.mod h1:/FVuLwhz+sIfsWUgUHWKi32qT0i6+IXlUlzs70KKt/Q= -github.com/gptscript-ai/gptscript v0.9.6-0.20250120172457-3f876b2ef42b h1:xOnA8rGg3iGjTpvWUtuw8IzPFey5gsivi1CUNtZp4Gc= -github.com/gptscript-ai/gptscript v0.9.6-0.20250120172457-3f876b2ef42b/go.mod h1:eBrKu1mmZ4tLPoHJJD1xT/Ogm5K7Oue14xk54e+yEZw= +github.com/gptscript-ai/gptscript v0.9.6-0.20250122033232-c02c4cbaa1cc h1:ez6q7fMTmATleVP/d7xKw81oRiymozLNbCxYHmTIstI= +github.com/gptscript-ai/gptscript v0.9.6-0.20250122033232-c02c4cbaa1cc/go.mod h1:eBrKu1mmZ4tLPoHJJD1xT/Ogm5K7Oue14xk54e+yEZw= github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6 h1:vkgNZVWQgbE33VD3z9WKDwuu7B/eJVVMMPM62ixfCR8= github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6/go.mod h1:frrl/B+ZH3VSs3Tqk2qxEIIWTONExX3tuUa4JsVnqx4= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= diff --git a/pkg/api/authz/authz.go b/pkg/api/authz/authz.go index 62731db63..0a9a2f3c7 100644 --- a/pkg/api/authz/authz.go +++ b/pkg/api/authz/authz.go @@ -28,9 +28,8 @@ var staticRules = map[string][]string{ "/admin/", "/{$}", "/{agent}", - "/images/", + "/user/images/", "/_app/", - "/static/", // Allow access to the oauth2 endpoints "/oauth2/", diff --git a/pkg/api/static/static.go b/pkg/api/static/static.go new file mode 100644 index 000000000..7504ff592 --- /dev/null +++ b/pkg/api/static/static.go @@ -0,0 +1,39 @@ +package static + +import ( + "fmt" + "net/http" + "os" + "strings" +) + +func Wrap(next http.Handler, dir string) (http.Handler, error) { + mux := http.NewServeMux() + mux.Handle("/", next) + + fs := http.FileServer(http.Dir(dir)) + + entries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("failed to read directory: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() { + mux.Handle("GET /"+entry.Name()+"/", fs) + continue + } + + if strings.HasPrefix(entry.Name(), ".") { + continue + } + + mux.Handle("GET /"+entry.Name(), fs) + + if entry.Name() == "index.html" { + mux.Handle("GET /{$}", fs) + } + } + + return mux, nil +} diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 1a3b1a386..fed5b74a3 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -34,7 +34,7 @@ func New(services *services.Services) (*Controller, error) { } func (c *Controller) PreStart(ctx context.Context) error { - if err := data.Data(ctx, c.services.StorageClient); err != nil { + if err := data.Data(ctx, c.services.StorageClient, c.services.AgentsDir); err != nil { return fmt.Errorf("failed to apply data: %w", err) } return nil diff --git a/pkg/controller/data/agent.go b/pkg/controller/data/agent.go index e6697088e..b163af050 100644 --- a/pkg/controller/data/agent.go +++ b/pkg/controller/data/agent.go @@ -3,12 +3,22 @@ package data import ( "context" _ "embed" + "errors" + "fmt" + "io/fs" + "os" + "path" + "path/filepath" "slices" + "strings" + "github.com/adrg/xdg" "github.com/obot-platform/obot/apiclient/types" + "github.com/obot-platform/obot/pkg/alias" "github.com/obot-platform/obot/pkg/api/handlers" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" + "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -18,7 +28,76 @@ import ( //go:embed agent.yaml var agentBytes []byte -func addAgent(ctx context.Context, k kclient.Client) error { +func addAgents(ctx context.Context, k kclient.Client, agentDir string) error { + if err := addAutoAgents(ctx, k, agentDir); err != nil { + return err + } + return addDefaultAgent(ctx, k, agentDir) +} + +func addAutoAgents(ctx context.Context, k kclient.Client, agentDir string) error { + var err error + if agentDir == "" { + agentDir, err = xdg.ConfigFile(path.Join("obot", "agents")) + if err != nil { + return fmt.Errorf("failed to get agent dir: %w", err) + } + } + + files, err := os.ReadDir(agentDir) + if errors.Is(err, fs.ErrNotExist) { + return nil + } + + for _, file := range files { + if file.IsDir() || !strings.HasSuffix(file.Name(), ".yaml") { + continue + } + + data, err := os.ReadFile(filepath.Join(agentDir, file.Name())) + if err != nil { + return fmt.Errorf("failed to read agent file %s: %w", file.Name(), err) + } + + var manifest types.AgentManifest + if err := yaml.Unmarshal(data, &manifest); err != nil { + return fmt.Errorf("failed to unmarshal agent file %s: %w", file.Name(), err) + } + + if manifest.Alias == "" { + return fmt.Errorf("agent file %s is missing an alias", file.Name()) + } + + var agent v1.Agent + if err := alias.Get(ctx, k, &agent, system.DefaultNamespace, manifest.Alias); apierrors.IsNotFound(err) { + if err := k.Create(ctx, &v1.Agent{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: system.AgentPrefix, + Namespace: system.DefaultNamespace, + }, + Spec: v1.AgentSpec{ + Manifest: manifest, + }, + }); err != nil { + return fmt.Errorf("failed to create agent %s: %w", manifest.Alias, err) + } + continue + } else if err != nil { + return fmt.Errorf("failed to get agent %s: %w", manifest.Alias, err) + } + + if !equality.Semantic.DeepEqual(agent.Spec.Manifest, manifest) { + agent.Spec.Manifest = manifest + if err := k.Update(ctx, &agent); err != nil { + return fmt.Errorf("failed to update agent %s: %w", manifest.Alias, err) + } + } + } + + return nil +} + +func addDefaultAgent(ctx context.Context, k kclient.Client, agentDir string) error { var agent v1.Agent if err := yaml.Unmarshal(agentBytes, &agent); err != nil { return err @@ -26,6 +105,17 @@ func addAgent(ctx context.Context, k kclient.Client) error { var existing v1.Agent if err := k.Get(ctx, kclient.ObjectKey{Namespace: agent.Namespace, Name: agent.Name}, &existing); apierrors.IsNotFound(err) { + if agentDir != "" { + // If the agent dir is set, it's assumed they don't want the default, so only add it if there are zero agents + var agents v1.AgentList + if err := k.List(ctx, &agents); err != nil { + return fmt.Errorf("failed to list agents: %w", err) + } + if len(agents.Items) > 0 { + return nil + } + } + if err := k.Create(ctx, &agent); err != nil { return err } @@ -53,6 +143,12 @@ func addAgent(ctx context.Context, k kclient.Client) error { modified, existing.Spec.Manifest.DefaultThreadTools = addTool(modified, &existing, existing.Spec.Manifest.DefaultThreadTools, agent.Spec.Manifest.DefaultThreadTools) modified, existing.Spec.Manifest.AvailableThreadTools = addTool(modified, &existing, existing.Spec.Manifest.AvailableThreadTools, agent.Spec.Manifest.AvailableThreadTools) + // migrate from the old icon path + if existing.Spec.Manifest.Icons != nil && existing.Spec.Manifest.Icons.Icon == "/images/obot-icon-blue.svg" { + existing.Spec.Manifest.Icons = agent.Spec.Manifest.Icons + modified = true + } + if modified { return k.Update(ctx, &existing) } diff --git a/pkg/controller/data/agent.yaml b/pkg/controller/data/agent.yaml index 37f0bbd15..8368982d8 100644 --- a/pkg/controller/data/agent.yaml +++ b/pkg/controller/data/agent.yaml @@ -8,9 +8,9 @@ spec: name: Obot description: Default Assistant icons: - icon: /images/obot-icon-blue.svg - collapsed: /images/obot-logo-blue-black-text.svg - collapsedDark: /images/obot-logo-blue-white-text.svg + icon: /user/images/obot-icon-blue.svg + collapsed: /user/images/obot-logo-blue-black-text.svg + collapsedDark: /user/images/obot-logo-blue-white-text.svg prompt: | You are an AI assistant developed by Acorn Labs named Obot. You are described as follows: diff --git a/pkg/controller/data/data.go b/pkg/controller/data/data.go index 4c14f456d..2afb0d280 100644 --- a/pkg/controller/data/data.go +++ b/pkg/controller/data/data.go @@ -16,7 +16,7 @@ var defaultModelsData []byte //go:embed default-model-aliases.yaml var defaultModelAliasesData []byte -func Data(ctx context.Context, c kclient.Client) error { +func Data(ctx context.Context, c kclient.Client, agentDir string) error { var defaultModels v1.ModelList if err := yaml.Unmarshal(defaultModelsData, &defaultModels); err != nil { return err @@ -45,5 +45,5 @@ func Data(ctx context.Context, c kclient.Client) error { } } - return addAgent(ctx, c) + return addAgents(ctx, c, agentDir) } diff --git a/pkg/credstores/credstore.go b/pkg/credstores/credstore.go index 4b494a341..dc747dce0 100644 --- a/pkg/credstores/credstore.go +++ b/pkg/credstores/credstore.go @@ -30,7 +30,7 @@ func Init(ctx context.Context, toolRegistries []string, dsn string, opts Options case strings.HasPrefix(dsn, "postgres://"): return setupPostgres(toolRegistries, dsn) default: - return "", nil, fmt.Errorf("unsupported database for credentials %s", dsn) + return "", nil, fmt.Errorf("unsupported database for credentials %s", strings.Split(dsn, "://")[0]) } } diff --git a/pkg/server/server.go b/pkg/server/server.go index 0244f65e1..279f580de 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -7,6 +7,7 @@ import ( "github.com/obot-platform/obot/logger" "github.com/obot-platform/obot/pkg/api/router" + "github.com/obot-platform/obot/pkg/api/static" "github.com/obot-platform/obot/pkg/controller" "github.com/obot-platform/obot/pkg/services" "github.com/rs/cors" @@ -41,6 +42,13 @@ func Run(ctx context.Context, c services.Config) error { return err } + if c.StaticDir != "" { + handler, err = static.Wrap(handler, c.StaticDir) + if err != nil { + return err + } + } + context.AfterFunc(ctx, func() { log.Fatalf("Interrupted, exiting") }) diff --git a/pkg/services/config.go b/pkg/services/config.go index c4911dfe6..138c15456 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -68,6 +68,8 @@ type Config struct { EnableAuthentication bool `usage:"Enable authentication" default:"false"` EnableBootstrapUser bool `usage:"Enables the bootstrap user, regardless of configured auth providers" default:"true"` AuthAdminEmails []string `usage:"Emails of admin users"` + AgentsDir string `usage:"The directory to auto load agents on start (default $XDG_CONFIG_HOME/.obot/agents)"` + StaticDir string `usage:"The directory to serve static files from"` // Sendgrid webhook SendgridWebhookUsername string `usage:"The username for the sendgrid webhook to authenticate with"` @@ -98,6 +100,7 @@ type Services struct { Bootstrapper *bootstrap.Bootstrap KnowledgeSetIngestionLimit int SupportDocker bool + AgentsDir string // Use basic auth for sendgrid webhook, if being set SendgridWebhookUsername string @@ -390,6 +393,7 @@ func New(ctx context.Context, config Config) (*Services, error) { ProxyManager: proxyManager, ProviderDispatcher: providerDispatcher, Bootstrapper: bootstrapper, + AgentsDir: config.AgentsDir, }, nil } diff --git a/ui/user/src/lib/auth.ts b/ui/user/src/lib/auth.ts index 7619fe61a..6c6c61af0 100644 --- a/ui/user/src/lib/auth.ts +++ b/ui/user/src/lib/auth.ts @@ -1,13 +1,13 @@ export type AuthProvider = { - configured: boolean - icon?: string - name: string - namespace: string - id: string -} + configured: boolean; + icon?: string; + name: string; + namespace: string; + id: string; +}; export async function listAuthProviders(): Promise { - const resp = await fetch('/api/auth-providers') - const data = await resp.json() - return data.items.filter((provider: AuthProvider) => provider.configured); + const resp = await fetch('/api/auth-providers'); + const data = await resp.json(); + return data.items.filter((provider: AuthProvider) => provider.configured); } diff --git a/ui/user/src/lib/stores/currentassistant.ts b/ui/user/src/lib/stores/currentassistant.ts index 5d5829b8c..c1a018b66 100644 --- a/ui/user/src/lib/stores/currentassistant.ts +++ b/ui/user/src/lib/stores/currentassistant.ts @@ -26,12 +26,18 @@ function assignSelected(currentAssistants: Assistant[], selectedName: string): A } const res = currentAssistants.find((value) => value.current); if (!res && selectedName) { - ChatService.getAssistant(selectedName).then((assistant) => { - if (assistant) { - assistant.current = true; - store.set(assistant); - } - }); + ChatService.getAssistant(selectedName) + .then((assistant) => { + if (assistant) { + assistant.current = true; + store.set(assistant); + } + }) + .catch((error) => { + if (String(error).includes('404')) { + window.location.href = '/'; + } + }); return def; } return res ?? def; diff --git a/ui/user/src/routes/+page.svelte b/ui/user/src/routes/+page.svelte index b342ec070..413cda687 100644 --- a/ui/user/src/routes/+page.svelte +++ b/ui/user/src/routes/+page.svelte @@ -7,9 +7,9 @@ import { Book } from '$lib/icons'; import { loadedAssistants } from '$lib/stores'; import { listAuthProviders, type AuthProvider } from '$lib/auth'; - import {onMount} from "svelte" + import { onMount } from 'svelte'; - let authProviders: AuthProvider[] = $state([]) + let authProviders: AuthProvider[] = $state([]); onMount(async () => { authProviders = await listAuthProviders(); @@ -50,18 +50,18 @@ class="icon-button text-white hover:text-blue-50" > {#if $darkMode} - GitHub + GitHub {:else} - GitHub + GitHub {/if}
{#if $darkMode} - obot icon + obot icon {:else} - obot icon + obot icon {/if}
@@ -69,24 +69,33 @@ {#each authProviders as provider} { - window.location.href = '/oauth2/start?rd=' + window.location.pathname + '&obot-auth-provider=' + provider.namespace + '/' + provider.id; + window.location.href = + '/oauth2/start?rd=' + + window.location.pathname + + '&obot-auth-provider=' + + provider.namespace + + '/' + + provider.id; }} rel="external" href={`/oauth2/start?obot-auth-provider=${provider.namespace}/${provider.id}&rd=/`} class="group flex items-center gap-1 rounded-full bg-black p-2 px-8 text-lg font-semibold text-white dark:bg-white dark:text-black" > - {#if provider.icon} - {provider.name} - Login with {provider.name} - {/if} + {#if provider.icon} + {provider.name} + Login with {provider.name} + {/if} {/each} {#if authProviders.length === 0} -

No auth providers configured. Please configure at least one auth provider in the admin panel.

+

+ No auth providers configured. Please configure at least one auth provider in the admin + panel. +

{/if}
diff --git a/ui/user/src/routes/login_complete/+page.svelte b/ui/user/src/routes/login_complete/+page.svelte index acfd5facf..13a96cf46 100644 --- a/ui/user/src/routes/login_complete/+page.svelte +++ b/ui/user/src/routes/login_complete/+page.svelte @@ -3,7 +3,11 @@
- Assistant logo + Assistant logo

All Set!

You can close this window and return to the application diff --git a/ui/user/static/images/github-mark/github-mark-white.svg b/ui/user/static/user/images/github-mark/github-mark-white.svg similarity index 100% rename from ui/user/static/images/github-mark/github-mark-white.svg rename to ui/user/static/user/images/github-mark/github-mark-white.svg diff --git a/ui/user/static/images/github-mark/github-mark.svg b/ui/user/static/user/images/github-mark/github-mark.svg similarity index 100% rename from ui/user/static/images/github-mark/github-mark.svg rename to ui/user/static/user/images/github-mark/github-mark.svg diff --git a/ui/user/static/images/obot-icon-blue.svg b/ui/user/static/user/images/obot-icon-blue.svg similarity index 100% rename from ui/user/static/images/obot-icon-blue.svg rename to ui/user/static/user/images/obot-icon-blue.svg diff --git a/ui/user/static/images/obot-icon-white.svg b/ui/user/static/user/images/obot-icon-white.svg similarity index 100% rename from ui/user/static/images/obot-icon-white.svg rename to ui/user/static/user/images/obot-icon-white.svg diff --git a/ui/user/static/images/obot-logo-blue-black-text.svg b/ui/user/static/user/images/obot-logo-blue-black-text.svg similarity index 100% rename from ui/user/static/images/obot-logo-blue-black-text.svg rename to ui/user/static/user/images/obot-logo-blue-black-text.svg diff --git a/ui/user/static/images/obot-logo-blue-white-text.svg b/ui/user/static/user/images/obot-logo-blue-white-text.svg similarity index 100% rename from ui/user/static/images/obot-logo-blue-white-text.svg rename to ui/user/static/user/images/obot-logo-blue-white-text.svg