import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Inject, NotFoundException, Param, Post, Query, UseGuards, } from '@nestjs/common'; import type { Memory } from '@mosaic/memory'; import { MEMORY } from './memory.tokens.js'; import { AuthGuard } from '../auth/auth.guard.js'; import { EmbeddingService } from './embedding.service.js'; import type { UpsertPreferenceDto, CreateInsightDto, SearchMemoryDto } from './memory.dto.js'; @Controller('api/memory') @UseGuards(AuthGuard) export class MemoryController { constructor( @Inject(MEMORY) private readonly memory: Memory, private readonly embeddings: EmbeddingService, ) {} // ─── Preferences ──────────────────────────────────────────────────── @Get('preferences') async listPreferences(@Query('userId') userId: string, @Query('category') category?: string) { if (category) { return this.memory.preferences.findByUserAndCategory( userId, category as Parameters[1], ); } return this.memory.preferences.findByUser(userId); } @Get('preferences/:key') async getPreference(@Query('userId') userId: string, @Param('key') key: string) { const pref = await this.memory.preferences.findByUserAndKey(userId, key); if (!pref) throw new NotFoundException('Preference not found'); return pref; } @Post('preferences') async upsertPreference(@Query('userId') userId: string, @Body() dto: UpsertPreferenceDto) { return this.memory.preferences.upsert({ userId, key: dto.key, value: dto.value, category: dto.category, source: dto.source, }); } @Delete('preferences/:key') @HttpCode(HttpStatus.NO_CONTENT) async removePreference(@Query('userId') userId: string, @Param('key') key: string) { const deleted = await this.memory.preferences.remove(userId, key); if (!deleted) throw new NotFoundException('Preference not found'); } // ─── Insights ─────────────────────────────────────────────────────── @Get('insights') async listInsights(@Query('userId') userId: string, @Query('limit') limit?: string) { return this.memory.insights.findByUser(userId, limit ? Number(limit) : undefined); } @Get('insights/:id') async getInsight(@Param('id') id: string) { const insight = await this.memory.insights.findById(id); if (!insight) throw new NotFoundException('Insight not found'); return insight; } @Post('insights') async createInsight(@Query('userId') userId: string, @Body() dto: CreateInsightDto) { const embedding = this.embeddings.available ? await this.embeddings.embed(dto.content) : undefined; return this.memory.insights.create({ userId, content: dto.content, source: dto.source, category: dto.category, metadata: dto.metadata, embedding: embedding ?? null, }); } @Delete('insights/:id') @HttpCode(HttpStatus.NO_CONTENT) async removeInsight(@Param('id') id: string) { const deleted = await this.memory.insights.remove(id); if (!deleted) throw new NotFoundException('Insight not found'); } // ─── Search ───────────────────────────────────────────────────────── @Post('search') async searchMemory(@Query('userId') userId: string, @Body() dto: SearchMemoryDto) { if (!this.embeddings.available) { return { query: dto.query, results: [], message: 'Semantic search requires OPENAI_API_KEY for embeddings', }; } const queryEmbedding = await this.embeddings.embed(dto.query); const results = await this.memory.insights.searchByEmbedding( userId, queryEmbedding, dto.limit ?? 10, dto.maxDistance ?? 0.8, ); return { query: dto.query, results }; } }