diff --git a/apps/api/src/api.ts b/apps/api/src/api.ts index 84114aab..083a51c1 100644 --- a/apps/api/src/api.ts +++ b/apps/api/src/api.ts @@ -32,7 +32,6 @@ export const honoCtxToAppCtx = (c: Context): AppContext => { return { ...c.var, - params: { ...c.req.query(), ...c.req.param() }, envVars: envs, cookie: c.req.header("Cookie"), workspace: slug && root @@ -78,30 +77,10 @@ const createMCPHandlerFor = ( } return async (c: Context) => { - let srv = server; - const group = c.req.query("group"); - if (group) { - const serverGroup = new McpServer( - { name: "@deco/api", version: "1.0.0" }, - { capabilities: { tools: {} } }, - ); - - for (const tool of tools) { - if (tool.group === group) { - serverGroup.tool( - tool.name, - tool.description, - tool.schema.shape, - createAIHandler(tool.handler), - ); - } - } - srv = serverGroup; - } const transport = new HttpServerTransport(); startTime(c, "mcp-connect"); - await srv.connect(transport); + await server.connect(transport); endTime(c, "mcp-connect"); startTime(c, "mcp-handle-message"); diff --git a/apps/web/src/components/integrations/list/installed.tsx b/apps/web/src/components/integrations/list/installed.tsx index 08de18f9..aa8369db 100644 --- a/apps/web/src/components/integrations/list/installed.tsx +++ b/apps/web/src/components/integrations/list/installed.tsx @@ -32,11 +32,10 @@ import { IntegrationInfo } from "../../common/TableCells.tsx"; import { Header, IntegrationPageLayout } from "./breadcrumb.tsx"; import { IntegrationIcon } from "./common.tsx"; -const INTEGRATION_ID_DENYLIST = [ +const INTEGRATION_ID_DENYLIST = new Set([ "i:workspace-management", "i:user-management", - "i:knowledge-base", -]; +]); interface IntegrationActionsProps { onDelete: () => void; @@ -99,9 +98,7 @@ function IntegrationCard({
e.stopPropagation()}> - {!INTEGRATION_ID_DENYLIST.some((id) => - integration.id.startsWith(id) - ) && ( + {!INTEGRATION_ID_DENYLIST.has(integration.id) && ( onDelete(integration.id)} /> )}
@@ -226,9 +223,9 @@ function TableView( header: "", render: (integration) => (
e.stopPropagation()}> - {!INTEGRATION_ID_DENYLIST.some((id) => - integration.id.startsWith(id) - ) && onDelete(integration.id)} />} + {!INTEGRATION_ID_DENYLIST.has(integration.id) && ( + onDelete(integration.id)} /> + )}
), }, diff --git a/packages/ai/package.json b/packages/ai/package.json index 20910225..89801ece 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -5,7 +5,6 @@ "private": true, "dependencies": { "cloudflare": "^4.2.0", - "@deco/sdk": "^0.1.1", "@ai-sdk/anthropic": "^1.1.17", "@ai-sdk/google": "^1.1.25", "@ai-sdk/openai": "^1.2.5", diff --git a/packages/ai/src/agent.ts b/packages/ai/src/agent.ts index 4cae6937..f82c6a36 100644 --- a/packages/ai/src/agent.ts +++ b/packages/ai/src/agent.ts @@ -51,13 +51,8 @@ import { join } from "node:path/posix"; import process from "node:process"; import { pickCapybaraAvatar } from "./capybaras.ts"; import { mcpServerTools } from "./mcp.ts"; -import type { AgentMemoryConfig } from "@deco/sdk/memory"; -import { - AgentMemory, - buildMemoryId, - slugify, - toAlphanumericId, -} from "@deco/sdk/memory"; +import type { AgentMemoryConfig } from "./memory/memory.ts"; +import { AgentMemory, buildMemoryId } from "./memory/memory.ts"; import { createLLM } from "./models.ts"; import type { AIAgent as IIAgent, @@ -67,6 +62,7 @@ import type { ThreadQueryOptions, } from "./types.ts"; import { GenerateOptions } from "./types.ts"; +import { slugify, toAlphanumericId } from "./utils/slugify.ts"; import { AgentWallet } from "./wallet/index.ts"; const TURSO_AUTH_TOKEN_KEY = "turso-auth-token"; @@ -190,16 +186,8 @@ export class AIAgent extends BaseActor implements IIAgent { return path; } - public get embedder() { - const openai = createOpenAI({ - apiKey: this.env.OPENAI_API_KEY, - }); - return openai.embedding("text-embedding-3-small"); - } - createAppContext(metadata?: AgentMetadata): AppContext { return { - params: {}, envVars: this.env as any, db: this.db, user: metadata?.principal!, @@ -462,6 +450,9 @@ export class AIAgent extends BaseActor implements IIAgent { tokenLimit: number, ) { if (this.memoryId !== memoryId || !this.memory) { + const openai = createOpenAI({ + apiKey: this.env.OPENAI_API_KEY, + }); const tursoOrganization = this.env.TURSO_ORGANIZATION ?? "decoai"; const tokenStorage = this.env.TURSO_GROUP_DATABASE_TOKEN ?? { getToken: (memoryId: string) => { @@ -484,7 +475,7 @@ export class AIAgent extends BaseActor implements IIAgent { tursoOrganization, tokenStorage, processors: [new TokenLimiter({ limit: tokenLimit })], - embedder: this.embedder, + embedder: openai.embedding("text-embedding-3-small"), workspace: this.workspace, options: { semanticRecall: false, diff --git a/packages/ai/src/mcp.ts b/packages/ai/src/mcp.ts index a442cfa6..19d2a3f7 100644 --- a/packages/ai/src/mcp.ts +++ b/packages/ai/src/mcp.ts @@ -6,7 +6,6 @@ import type { MCPConnection, } from "@deco/sdk"; import { AppContext, fromWorkspaceString, MCPClient } from "@deco/sdk/mcp"; -import { slugify } from "@deco/sdk/memory"; import type { ToolAction } from "@mastra/core"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { @@ -20,6 +19,7 @@ import { getTools } from "./deco.ts"; import { getToolsForInnateIntegration } from "./storage/tools.ts"; import { createTool } from "./utils/createTool.ts"; import { jsonSchemaToModel } from "./utils/jsonSchemaToModel.ts"; +import { slugify } from "./utils/slugify.ts"; import { mapToolEntries } from "./utils/toolEntries.ts"; const ApiDecoChatURLs = [ diff --git a/packages/sdk/src/memory/libsql.ts b/packages/ai/src/memory/libsql.ts similarity index 100% rename from packages/sdk/src/memory/libsql.ts rename to packages/ai/src/memory/libsql.ts diff --git a/packages/sdk/src/memory/memory.ts b/packages/ai/src/memory/memory.ts similarity index 93% rename from packages/sdk/src/memory/memory.ts rename to packages/ai/src/memory/memory.ts index 40ab7cf5..2c572017 100644 --- a/packages/sdk/src/memory/memory.ts +++ b/packages/ai/src/memory/memory.ts @@ -1,11 +1,11 @@ -import type { Workspace } from "@deco/sdk/path"; import type { Client as LibSQLClient } from "@libsql/client"; import type { StorageThreadType } from "@mastra/core"; import type { SharedMemoryConfig } from "@mastra/core/memory"; import { Memory as MastraMemory } from "@mastra/memory"; -import { slugify, slugifyForDNS, toAlphanumericId } from "../mcp/slugify.ts"; +import type { Workspace } from "@deco/sdk/path"; +import { toAlphanumericId } from "../utils/slugify.ts"; import { LibSQLFactory, type LibSQLFactoryOpts } from "./libsql.ts"; -export { slugify, slugifyForDNS, toAlphanumericId }; + type CreateThreadOpts = Parameters[0]; interface WorkspaceMemoryConfig extends SharedMemoryConfig { @@ -15,7 +15,6 @@ interface WorkspaceMemoryConfig extends SharedMemoryConfig { interface CreateWorkspaceMemoryOpts extends LibSQLFactoryOpts, Omit { workspace: Workspace; - discriminator?: string; } export class WorkspaceMemory extends MastraMemory { @@ -29,10 +28,9 @@ export class WorkspaceMemory extends MastraMemory { tursoAdminToken, tursoOrganization, tokenStorage, - discriminator, ...opts }: CreateWorkspaceMemoryOpts) { - const memoryId = buildMemoryId(workspace, discriminator); + const memoryId = buildMemoryId(workspace); const libsqlFactory = new LibSQLFactory({ tursoAdminToken, diff --git a/packages/ai/src/triggers/outputTool.ts b/packages/ai/src/triggers/outputTool.ts index e07b9e6d..af70e4f7 100644 --- a/packages/ai/src/triggers/outputTool.ts +++ b/packages/ai/src/triggers/outputTool.ts @@ -6,8 +6,8 @@ import { zodToJsonSchema } from "zod-to-json-schema"; import type { AIAgent } from "../agent.ts"; import { mcpServerTools } from "../mcp.ts"; import type { Message } from "../types.ts"; +import { slugify } from "../utils/slugify.ts"; import type { Trigger } from "./trigger.ts"; -import { slugify } from "@deco/sdk/memory"; export interface RunOutputToolArgs { agent: ActorProxy; diff --git a/packages/ai/src/triggers/trigger.ts b/packages/ai/src/triggers/trigger.ts index f34621bc..b0706c51 100644 --- a/packages/ai/src/triggers/trigger.ts +++ b/packages/ai/src/triggers/trigger.ts @@ -123,7 +123,6 @@ export class Trigger { stub: this.state.stub as AppContext["stub"], workspace: fromWorkspaceString(this.workspace), cf: new Cloudflare({ apiToken: this.env.CF_API_TOKEN }), - params: {}, }); } diff --git a/packages/ai/src/utils/slugify.ts b/packages/ai/src/utils/slugify.ts new file mode 100644 index 00000000..782c2a42 --- /dev/null +++ b/packages/ai/src/utils/slugify.ts @@ -0,0 +1,33 @@ +import { createHash } from "node:crypto"; + +/** + * Transforms a string into a unique alphanumeric identifier. + * The resulting string will: + * 1. Only contain alphanumeric characters + * 2. Be unique for different inputs + * 3. Be deterministic (same input always produces same output) + * 4. Preserve some readability of the original string + */ +export function toAlphanumericId(input: string): string { + // First, convert the string to lowercase and remove all non-alphanumeric chars + const baseSlug = input.toLowerCase().replace(/[^a-z0-9]/g, ""); + + // Create a hash of the original input to ensure uniqueness + const hash = createHash("sha256") + .update(input) + .digest("hex") + .slice(0, 8); // Take first 8 chars of hash + + // Combine the cleaned string with the hash + // If baseSlug is empty, just return the hash + return baseSlug ? `${baseSlug}-${hash}` : hash; +} + +/** + * Slugifies a string by converting it to lowercase and replacing all non-alphanumeric characters with a dash. + * @param input The string to slugify. + * @returns The slugified string. + */ +export function slugify(input: string): string { + return input.toUpperCase().replace(/[^A-Z0-9]/g, "_"); +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index e605b169..37862afd 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -5,11 +5,6 @@ "private": true, "dependencies": { "@aws-sdk/s3-request-presigner": "^3.812.0", - "@ai-sdk/openai": "1.3.22", - "@mastra/core": "0.9.0", - "@mastra/mcp": "0.4.0", - "@mastra/memory": "0.3.0", - "@mastra/rag": "0.1.23", "@libsql/client": "^0.15.4", "@modelcontextprotocol/sdk": "1.11.4", "cloudflare": "^4.2.0", @@ -23,8 +18,6 @@ "react": "^19.1.0", "zod": "3.24.3", "@aws-sdk/client-s3": "^3.808.0", - "uuid": "^11.1.0", - "@tursodatabase/api": "^1.9.0", "@supabase/supabase-js": "^2.49.4", "@supabase/ssr": "^0.6.1", "@microlabs/otel-cf-workers": "^1.0.0-rc.49", @@ -46,7 +39,6 @@ "exports": { ".": "./src/index.ts", "./observability": "./src/observability/index.ts", - "./memory": "./src/memory/memory.ts", "./actors": "./src/actors/index.ts", "./cache": "./src/cache/index.ts", "./common": "./src/common/index.ts", diff --git a/packages/sdk/src/mcp/assertions.ts b/packages/sdk/src/mcp/assertions.ts index efe3e9cd..0ad5f5ac 100644 --- a/packages/sdk/src/mcp/assertions.ts +++ b/packages/sdk/src/mcp/assertions.ts @@ -6,22 +6,19 @@ import { UnauthorizedError, } from "../errors.ts"; import { AppContext } from "./context.ts"; -import { Workspace } from "../path.ts"; const getContextUser = (c: AppContext) => { assertHasUser(c); return c.user!; }; -type WithWorkspace = - & Omit - & { - workspace: { root: string; slug: string; value: Workspace }; - }; +type WithWorkspace = Omit & { + workspace: { root: string; slug: string; value: string }; +}; -export function assertHasWorkspace( - c: Pick | Pick, "workspace">, -): asserts c is WithWorkspace { +export function assertHasWorkspace( + c: Pick | Pick, +): asserts c is WithWorkspace { if (!c.workspace) { throw new NotFoundError(); } diff --git a/packages/sdk/src/mcp/context.ts b/packages/sdk/src/mcp/context.ts index 5585c349..b8a6cab7 100644 --- a/packages/sdk/src/mcp/context.ts +++ b/packages/sdk/src/mcp/context.ts @@ -9,7 +9,6 @@ import { AsyncLocalStorage } from "node:async_hooks"; import { z } from "zod"; export interface Vars { - params: Record; workspace?: { root: string; slug: string; @@ -59,17 +58,15 @@ const envSchema = z.object({ CF_DISPATCH_NAMESPACE: z.string().readonly(), CF_ACCOUNT_ID: z.string().readonly(), CF_API_TOKEN: z.string().readonly(), - CF_R2_ACCESS_KEY_ID: z.any().optional().readonly(), - CF_R2_SECRET_ACCESS_KEY: z.any().optional().readonly(), + CF_R2_ACCESS_KEY_ID: z.string().readonly(), + CF_R2_SECRET_ACCESS_KEY: z.string().readonly(), VITE_USE_LOCAL_BACKEND: z.any().optional().readonly(), SUPABASE_URL: z.string().readonly(), SUPABASE_SERVER_TOKEN: z.string().readonly(), TURSO_GROUP_DATABASE_TOKEN: z.string().readonly(), TURSO_ORGANIZATION: z.string().readonly(), - RESEND_API_KEY: z.any().optional().readonly(), + RESEND_API_KEY: z.string().readonly(), OPENROUTER_API_KEY: z.string().readonly(), - TURSO_ADMIN_TOKEN: z.any().optional().readonly(), - OPENAI_API_KEY: z.any().optional().readonly(), }); export const getEnv = (ctx: AppContext): EnvVars => @@ -101,19 +98,14 @@ export const createAIHandler = }; export interface ApiHandlerDefinition< - TAppContext extends AppContext = AppContext, TName extends string = string, T extends z.ZodType = z.ZodType, R extends object | boolean = object, - THandler extends ( + THandler extends (props: z.infer, c: AppContext) => Promise | R = ( props: z.infer, - c: TAppContext, - ) => Promise | R = ( - props: z.infer, - c: TAppContext, + c: AppContext, ) => Promise | R, > { - group?: string; name: TName; description: string; schema: T; @@ -128,41 +120,33 @@ export interface ApiHandler< props: z.infer, ) => Promise | R, > { - group?: string; name: TName; description: string; schema: T; handler: THandler; } -export const createApiHandlerFactory = < - TAppContext extends AppContext = AppContext, ->(contextFactory: (c: AppContext) => TAppContext, group?: string) => -< + +export const createApiHandler = < TName extends string = string, T extends z.ZodType = z.ZodType, R extends object | boolean = object | boolean, - THandler extends (props: z.infer, c: TAppContext) => Promise | R = ( + THandler extends (props: z.infer, c: AppContext) => Promise | R = ( props: z.infer, - c: TAppContext, + c: AppContext, ) => Promise | R, >( - definition: ApiHandlerDefinition, + definition: ApiHandlerDefinition, ): ApiHandler< TName, T, R, (props: Parameters[0]) => ReturnType > => ({ - group, ...definition, handler: (props: Parameters[0]): ReturnType => - definition.handler(props, contextFactory(State.getStore())) as ReturnType< - THandler - >, + definition.handler(props, State.getStore()) as ReturnType, }); -export const createApiHandler = createApiHandlerFactory((c) => c); - export type MCPDefinition = ApiHandler[]; const asyncLocalStorage = new AsyncLocalStorage(); diff --git a/packages/sdk/src/mcp/index.ts b/packages/sdk/src/mcp/index.ts index dfd9d2f3..0eb5e2d5 100644 --- a/packages/sdk/src/mcp/index.ts +++ b/packages/sdk/src/mcp/index.ts @@ -1,11 +1,10 @@ -export * from "../errors.ts"; export * from "./assertions.ts"; export * from "./context.ts"; +export * from "../errors.ts"; import * as agentsAPI from "./agents/api.ts"; import { ApiHandler, AppContext, State } from "./context.ts"; import * as hostingAPI from "./hosting/api.ts"; import * as integrationsAPI from "./integrations/api.ts"; -import * as knowledgeAPI from "./knowledge/api.ts"; import * as membersAPI from "./members/api.ts"; import * as profilesAPI from "./profiles/api.ts"; import { CreateStubHandlerOptions, MCPClientStub } from "./stub.ts"; @@ -66,11 +65,6 @@ export const WORKSPACE_TOOLS = [ triggersAPI.getWebhookTriggerUrl, triggersAPI.activateTrigger, triggersAPI.deactivateTrigger, - knowledgeAPI.createBase, - knowledgeAPI.deleteBase, - knowledgeAPI.forget, - knowledgeAPI.remember, - knowledgeAPI.search, fsAPI.listFiles, fsAPI.readFile, fsAPI.readFileMetadata, @@ -79,6 +73,7 @@ export const WORKSPACE_TOOLS = [ ] as const; export type WorkspaceTools = typeof WORKSPACE_TOOLS; + const global = createMCPToolsStub({ tools: GLOBAL_TOOLS, }); @@ -96,7 +91,6 @@ export const fromWorkspaceString = ( slug, }; }; - export const MCPClient = new Proxy( {} as typeof global & { forContext: ( diff --git a/packages/sdk/src/mcp/integrations/api.ts b/packages/sdk/src/mcp/integrations/api.ts index df82a6de..3deb67b3 100644 --- a/packages/sdk/src/mcp/integrations/api.ts +++ b/packages/sdk/src/mcp/integrations/api.ts @@ -24,7 +24,6 @@ import { } from "../assertions.ts"; import { createApiHandler } from "../context.ts"; import { NotFoundError } from "../index.ts"; -import { KNOWLEDGE_BASE_GROUP, listKnowledgeBases } from "../knowledge/api.ts"; const ensureStartingSlash = (path: string) => path.startsWith("/") ? path : `/${path}`; @@ -122,10 +121,7 @@ export const listTools = createApiHandler({ }, }); -const virtualIntegrationsFor = ( - workspace: string, - knowledgeBases: string[], -) => { +const virtualIntegrationsFor = (workspace: string) => { // Create a virtual User Management integration const userManagementIntegration = { id: formatId("i", "user-management"), @@ -139,7 +135,6 @@ const virtualIntegrationsFor = ( workspace, created_at: new Date().toISOString(), }; - const workspaceMcp = new URL(`${workspace}/mcp`, API_SERVER_URL); // Create a virtual Workspace Management integration const workspaceManagementIntegration = { @@ -148,7 +143,7 @@ const virtualIntegrationsFor = ( description: "Manage your agents, integrations and threads", connection: { type: "HTTP", - url: workspaceMcp.href, + url: new URL(`${workspace}/mcp`, API_SERVER_URL).href, }, icon: "https://assets.webdraw.app/uploads/deco-avocado-light.png", workspace, @@ -158,23 +153,6 @@ const virtualIntegrationsFor = ( return [ userManagementIntegration, workspaceManagementIntegration, - ...knowledgeBases.map((kb) => { - const url = new URL(workspaceMcp); - url.searchParams.set("group", KNOWLEDGE_BASE_GROUP); - url.searchParams.set("name", kb); - return { - id: formatId("i", `knowledge-base-${kb}`), - name: `${kb} (Knowledge Base)`, - description: "A knowledge base for your workspace", - connection: { - type: "HTTP", - url: url.href, - }, - icon: "https://assets.webdraw.app/uploads/deco-avocado-light.png", - workspace, - created_at: new Date().toISOString(), - }; - }), ]; }; export const listIntegrations = createApiHandler({ @@ -189,7 +167,6 @@ export const listIntegrations = createApiHandler({ _assertions, integrations, agents, - knowledgeBases, ] = await Promise.all([ assertUserHasAccessToWorkspace(c), c.db @@ -200,9 +177,6 @@ export const listIntegrations = createApiHandler({ .from("deco_chat_agents") .select("*") .ilike("workspace", workspace), - listKnowledgeBases.handler({}).catch(() => { - return { names: [] as string[] }; - }), ]); const error = integrations.error || agents.error; @@ -214,7 +188,7 @@ export const listIntegrations = createApiHandler({ } return [ - ...virtualIntegrationsFor(workspace, knowledgeBases.names), + ...virtualIntegrationsFor(workspace), ...integrations.data.map((item) => ({ ...item, id: formatId("i", item.id), @@ -248,11 +222,7 @@ export const getIntegration = createApiHandler({ } assertHasWorkspace(c); - const knowledgeBases = await listKnowledgeBases.handler({}); - const virtualIntegrations = virtualIntegrationsFor( - c.workspace.value, - knowledgeBases.names, - ); + const virtualIntegrations = virtualIntegrationsFor(c.workspace.value); if (virtualIntegrations.some((i) => i.id === id)) { return IntegrationSchema.parse({ diff --git a/packages/sdk/src/mcp/knowledge/api.ts b/packages/sdk/src/mcp/knowledge/api.ts deleted file mode 100644 index 9299b2ae..00000000 --- a/packages/sdk/src/mcp/knowledge/api.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { createOpenAI } from "@ai-sdk/openai"; -import { embed } from "ai"; -import { z } from "zod"; -import { InternalServerError } from "../../errors.ts"; -import { WorkspaceMemory } from "../../memory/memory.ts"; -import { - assertHasWorkspace, - assertUserHasAccessToWorkspace, -} from "../assertions.ts"; -import { - AppContext, - createApiHandler, - createApiHandlerFactory, -} from "../context.ts"; - -export interface KnowledgeBaseContext extends AppContext { - name: string; -} -export const KNOWLEDGE_BASE_GROUP = "knowledge_base"; -export const DEFAULT_KNOWLEDGE_BASE_NAME = "standard"; -const createKnowledgeBaseApiHandler = createApiHandlerFactory< - KnowledgeBaseContext ->((c) => ({ - ...c, - name: c.params.name ?? DEFAULT_KNOWLEDGE_BASE_NAME, -}), KNOWLEDGE_BASE_GROUP); - -const openAIEmbedder = (apiKey: string) => { - const openai = createOpenAI({ - apiKey, - }); - return openai.embedding("text-embedding-3-small"); -}; - -async function getVector(c: AppContext) { - assertHasWorkspace(c); - const mem = await WorkspaceMemory.create({ - workspace: c.workspace.value, - tursoAdminToken: c.envVars.TURSO_ADMIN_TOKEN, - tursoOrganization: c.envVars.TURSO_ORGANIZATION, - tokenStorage: c.envVars.TURSO_GROUP_DATABASE_TOKEN, - discriminator: KNOWLEDGE_BASE_GROUP, - }); - const vector = mem.vector; - if (!vector) { - throw new InternalServerError("Missing vector"); - } - return vector; -} - -const DEFAULT_DIMENSION = 1536; - -export const listKnowledgeBases = createApiHandler({ - name: "KNOWLEDGE_BASE_LIST", - description: "List all knowledge bases", - schema: z.object({}), - handler: async (_, c) => { - const vector = await getVector(c); - const names = await vector.listIndexes(); - // lazily create the default knowledge base - if (!names.includes(DEFAULT_KNOWLEDGE_BASE_NAME)) { - // create default knowledge base - await createBase.handler({ - name: DEFAULT_KNOWLEDGE_BASE_NAME, - }); - names.push(DEFAULT_KNOWLEDGE_BASE_NAME); - } - return { names }; - }, -}); - -export const deleteBase = createApiHandler({ - name: "KNOWLEDGE_BASE_DELETE", - description: "Delete a knowledge base", - schema: z.object({ - name: z.string().describe("The name of the knowledge base"), - }), - handler: async ({ name }, c) => { - await assertUserHasAccessToWorkspace(c); - const vector = await getVector(c); - await vector.deleteIndex(name); - }, -}); -export const createBase = createApiHandler({ - name: "KNOWLEDGE_BASE_CREATE", - description: "Create a knowledge base", - schema: z.object({ - name: z.string().describe("The name of the knowledge base"), - dimension: z.number().describe("The dimension of the knowledge base") - .optional(), - }), - handler: async ({ name, dimension }, c) => { - await assertUserHasAccessToWorkspace(c); - const vector = await getVector(c); - await vector.createIndex({ - indexName: name, - dimension: dimension ?? DEFAULT_DIMENSION, - }); - }, -}); - -export const forget = createKnowledgeBaseApiHandler({ - name: "KNOWLEDGE_BASE_FORGET", - description: "Forget something", - schema: z.object({ - docId: z.string().describe("The id of the content to forget"), - }), - handler: async ({ docId }, c) => { - await assertUserHasAccessToWorkspace(c); - const vector = await getVector(c); - await vector.deleteIndexById(c.name, docId); - }, -}); - -export const remember = createKnowledgeBaseApiHandler({ - name: "KNOWLEDGE_BASE_REMEMBER", - description: "Remember something", - schema: z.object({ - docId: z.string().optional().describe( - "The id of the content being remembered", - ), - content: z.string().describe("The content to remember"), - name: z.string().describe("The name of the knowledge base").optional(), - metadata: z.record(z.string(), z.string()).describe( - "The metadata to remember", - ).optional(), - }), - handler: async ({ content, metadata, docId: _id }, c) => { - await assertUserHasAccessToWorkspace(c); - if (!c.envVars.OPENAI_API_KEY) { - throw new InternalServerError("Missing OPENAI_API_KEY"); - } - - const vector = await getVector(c); - const docId = _id ?? crypto.randomUUID(); - const embedder = openAIEmbedder(c.envVars.OPENAI_API_KEY); - // Create embeddings using OpenAI - const { embedding } = await embed({ - model: embedder, - value: content, - }); - await vector.upsert(c.name, [embedding], [{ - id: docId, - metadata: { ...metadata ?? {}, content }, - }]); - - return { - docId, - }; - }, -}); - -export const search = createKnowledgeBaseApiHandler({ - name: "KNOWLEDGE_BASE_SEARCH", - description: "Search the knowledge base", - schema: z.object({ - query: z.string().describe("The query to search the knowledge base"), - topK: z.number().describe("The number of results to return").optional(), - content: z.boolean().describe("Whether to return the content").optional(), - }), - handler: async ({ query, topK }, c) => { - assertHasWorkspace(c); - await assertUserHasAccessToWorkspace(c); - const mem = await WorkspaceMemory.create({ - workspace: c.workspace.value, - tursoAdminToken: c.envVars.TURSO_ADMIN_TOKEN, - tursoOrganization: c.envVars.TURSO_ORGANIZATION, - tokenStorage: c.envVars.TURSO_GROUP_DATABASE_TOKEN, - discriminator: KNOWLEDGE_BASE_GROUP, // used to create a unique database for the knowledge base - }); - const vector = mem.vector; - if (!vector) { - throw new InternalServerError("Missing vector"); - } - if (!c.envVars.OPENAI_API_KEY) { - throw new InternalServerError("Missing OPENAI_API_KEY"); - } - - const indexName = c.name; - const embedder = openAIEmbedder(c.envVars.OPENAI_API_KEY); - const { embedding } = await embed({ - model: embedder, - value: query, - }); - - return await vector.query({ - indexName, - queryVector: embedding, - topK: topK ?? 1, - }); - }, -}); diff --git a/packages/sdk/src/mcp/slugify.ts b/packages/sdk/src/mcp/slugify.ts index e15bb03f..750577b2 100644 --- a/packages/sdk/src/mcp/slugify.ts +++ b/packages/sdk/src/mcp/slugify.ts @@ -1,5 +1,3 @@ -import { createHash } from "node:crypto"; - /** * Transforms a string into a unique alphanumeric identifier. * The resulting string will: @@ -8,15 +6,17 @@ import { createHash } from "node:crypto"; * 3. Be deterministic (same input always produces same output) * 4. Preserve some readability of the original string */ -export function toAlphanumericId(input: string): string { +export async function toAlphanumericId(input: string): Promise { // First, convert the string to lowercase and remove all non-alphanumeric chars const baseSlug = input.toLowerCase().replace(/[^a-z0-9]/g, ""); - // Create a hash of the original input to ensure uniqueness - const hash = createHash("sha256") - .update(input) - .digest("hex") - .slice(0, 8); // Take first 8 chars of hash + // Create a hash of the original input to ensure uniqueness using Web Crypto API + const encoder = new TextEncoder(); + const data = encoder.encode(input); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hash = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("") + .slice(0, 8); // Combine the cleaned string with the hash // If baseSlug is empty, just return the hash @@ -70,7 +70,7 @@ export async function generateUUIDv5( * @param name The name to slugify * @returns A slugified string */ -export const slugifyForDNS = (name: string) => { +export const slugify = (name: string) => { // Lowercase and replace all non-alphanumeric with hyphens let slug = name.toLowerCase().replace(/[^a-z0-9]/g, "-"); // Replace multiple hyphens with a single hyphen @@ -82,12 +82,3 @@ export const slugifyForDNS = (name: string) => { // If the result is empty, return a default value (e.g., "subdomain") return slug || "subdomain"; }; - -/** - * Slugifies a string by converting it to lowercase and replacing all non-alphanumeric characters with a dash. - * @param input The string to slugify. - * @returns The slugified string. - */ -export function slugify(input: string): string { - return input.toUpperCase().replace(/[^A-Z0-9]/g, "_"); -} diff --git a/packages/sdk/src/mcp/threads/api.ts b/packages/sdk/src/mcp/threads/api.ts index 42e3d1c9..60dd26c2 100644 --- a/packages/sdk/src/mcp/threads/api.ts +++ b/packages/sdk/src/mcp/threads/api.ts @@ -57,7 +57,7 @@ const createSQLClientFor = async ( organization: string, authToken: string, ) => { - const memoryId = toAlphanumericId( + const memoryId = await toAlphanumericId( `${workspace}/default`, ); const uniqueDbName = await generateUUIDv5(