diff options
Diffstat (limited to 'packages/web/src/server/api/routers/memory.ts')
| -rw-r--r-- | packages/web/src/server/api/routers/memory.ts | 142 |
1 files changed, 135 insertions, 7 deletions
diff --git a/packages/web/src/server/api/routers/memory.ts b/packages/web/src/server/api/routers/memory.ts index e6d206b..eebe0f8 100644 --- a/packages/web/src/server/api/routers/memory.ts +++ b/packages/web/src/server/api/routers/memory.ts @@ -1,9 +1,21 @@ -import { SupabaseProjectStore, SupabaseStore } from "@imemio/sdk"; +import { EmbeddingService, SupabaseProjectStore, SupabaseStore } from "@imemio/sdk"; import type { SupabaseClient } from "@supabase/supabase-js"; +import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { env } from "~/env"; import { createClient } from "~/lib/supabase/server"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; +function getEmbeddingService(): EmbeddingService | null { + const openaiApiKey = env.OPENAI_API_KEY; + + if (!openaiApiKey) { + return null; + } + + return new EmbeddingService({ apiKey: openaiApiKey }); +} + const defaultProjectName = "default"; async function getOrCreateDefaultProject( @@ -47,14 +59,53 @@ export const memoryRouter = createTRPCRouter({ return memories; }), - create: protectedProcedure - .input(z.object({ content: z.string().min(1) })) - .mutation(async ({ context, input }) => { + listWithProjects: protectedProcedure + .input( + z.object({ + projectId: z.string().optional(), + sortOrder: z.enum(["asc", "desc"]).optional().default("desc"), + }), + ) + .query(async ({ ctx: context, input }) => { const supabaseClient = await createClient(); - const projectId = await getOrCreateDefaultProject( + const memoryStore = new SupabaseStore(supabaseClient, context.user.id); + const projectStore = new SupabaseProjectStore( supabaseClient, context.user.id, ); + const memories = await memoryStore.list( + input.projectId ? { projectId: input.projectId } : undefined, + ); + const projectsResult = await projectStore.list(); + const projects = projectsResult.success ? projectsResult.value : []; + const projectMap = new Map(projects.map((project) => [project.id, project])); + const memoriesWithProjects = memories.map((memory) => ({ + ...memory, + project: projectMap.get(memory.projectId) ?? null, + })); + const sortedMemories = memoriesWithProjects.sort((memoryA, memoryB) => { + const comparison = + new Date(memoryA.createdAt).getTime() - + new Date(memoryB.createdAt).getTime(); + + return input.sortOrder === "desc" ? -comparison : comparison; + }); + + return { memories: sortedMemories, projects }; + }), + + create: protectedProcedure + .input( + z.object({ + content: z.string().min(1), + projectId: z.string().optional(), + }), + ) + .mutation(async ({ ctx: context, input }) => { + const supabaseClient = await createClient(); + const projectId = + input.projectId ?? + (await getOrCreateDefaultProject(supabaseClient, context.user.id)); const memoryStore = new SupabaseStore(supabaseClient, context.user.id); const memory = await memoryStore.create({ content: input.content, @@ -64,17 +115,94 @@ export const memoryRouter = createTRPCRouter({ return memory; }), + update: protectedProcedure + .input( + z.object({ + id: z.string(), + content: z.string().min(1).optional(), + projectId: z.string().optional(), + }), + ) + .mutation(async ({ ctx: context, input }) => { + const supabaseClient = await createClient(); + const memoryStore = new SupabaseStore(supabaseClient, context.user.id); + const updateData: { content?: string; projectId?: string } = {}; + + if (input.content !== undefined) { + updateData.content = input.content; + } + + if (input.projectId !== undefined) { + updateData.projectId = input.projectId; + } + + const updateResult = await memoryStore.update(input.id, updateData); + + if (!updateResult.success) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Memory not found: ${input.id}`, + }); + } + + return updateResult.value; + }), + delete: protectedProcedure .input(z.object({ id: z.string() })) - .mutation(async ({ context, input }) => { + .mutation(async ({ ctx: context, input }) => { const supabaseClient = await createClient(); const memoryStore = new SupabaseStore(supabaseClient, context.user.id); const deleteResult = await memoryStore.delete(input.id); if (!deleteResult.success) { - throw new Error(`Memory not found: ${input.id}`); + throw new TRPCError({ + code: "NOT_FOUND", + message: `Memory not found: ${input.id}`, + }); } return { success: true }; }), + + search: protectedProcedure + .input( + z.object({ + query: z.string().min(1), + projectId: z.string().optional(), + limit: z.number().optional().default(20), + }), + ) + .query(async ({ ctx: context, input }) => { + const embeddingService = getEmbeddingService(); + + if (!embeddingService) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: + "Search is not available. Configure OPENAI_API_KEY to enable semantic search.", + }); + } + + const supabaseClient = await createClient(); + const memoryStore = new SupabaseStore(supabaseClient, context.user.id); + const projectStore = new SupabaseProjectStore( + supabaseClient, + context.user.id, + ); + const queryEmbedding = await embeddingService.generate(input.query); + const searchResults = await memoryStore.search(queryEmbedding, { + projectId: input.projectId, + limit: input.limit, + }); + const projectsResult = await projectStore.list(); + const projects = projectsResult.success ? projectsResult.value : []; + const projectMap = new Map(projects.map((project) => [project.id, project])); + const resultsWithProjects = searchResults.map((result) => ({ + ...result, + project: projectMap.get(result.projectId) ?? null, + })); + + return { results: resultsWithProjects, projects }; + }), }); |