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