import { Injectable, Logger } from '@nestjs/common'; import { and, asc, eq, gte, isNull, lt, ne } from 'drizzle-orm'; import { db } from '../db/client'; import { organizations, researchBriefs, savedSegments, tasks } from '../db/schema'; import { AiGatewayService } from '../ai-gateway/ai-gateway.service'; import type { AuthenticatedSession } from '../auth/tenant.guard'; const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; const BRIEFING_TEMPLATE_VERSION = 'daily-briefing@v1'; /** * Blueprint 17 "Daily Briefing": Scheduler -> collect tasks/deadlines -> deterministic * priority -> AI explanation -> save -> notify. Aici implementam doar partea * deterministica (colectare + prioritizare); explicatia AI vine cu AI Gateway-ul * (blueprint 14), inca neconstruit -- nu simulam text generat de AI. */ @Injectable() export class BriefingService { private readonly logger = new Logger(BriefingService.name); constructor(private readonly aiGateway: AiGatewayService) {} async today(session: AuthenticatedSession) { const now = new Date(); const weekAgo = new Date(now.getTime() - SEVEN_DAYS_MS); const weekFromNow = new Date(now.getTime() + SEVEN_DAYS_MS); const activeStatuses = [ne(tasks.status, 'done'), ne(tasks.status, 'cancelled')] as const; const [overdueTasks, upcomingTasks, newOrganizations, recentSegments, recentBriefs] = await Promise.all([ db.query.tasks.findMany({ where: and( eq(tasks.tenantId, session.tenantId), isNull(tasks.deletedAt), ...activeStatuses, lt(tasks.dueAt, now), ), orderBy: asc(tasks.dueAt), }), db.query.tasks.findMany({ where: and( eq(tasks.tenantId, session.tenantId), isNull(tasks.deletedAt), ...activeStatuses, gte(tasks.dueAt, now), lt(tasks.dueAt, weekFromNow), ), orderBy: [asc(tasks.priority), asc(tasks.dueAt)], }), db.query.organizations.findMany({ where: and( eq(organizations.tenantId, session.tenantId), isNull(organizations.deletedAt), gte(organizations.createdAt, weekAgo), ), }), db.query.savedSegments.findMany({ where: and(eq(savedSegments.tenantId, session.tenantId), gte(savedSegments.createdAt, weekAgo)), }), db.query.researchBriefs.findMany({ where: and(eq(researchBriefs.tenantId, session.tenantId), gte(researchBriefs.createdAt, weekAgo)), }), ]); const weekInReview = { newOrganizations: newOrganizations.length, newSegments: recentSegments.length, newResearchBriefs: recentBriefs.length, }; return { generatedAt: now.toISOString(), overdueTasks, upcomingTasks, weekInReview, aiExplanation: await this.explain(session, overdueTasks, upcomingTasks, weekInReview), }; } /** * Narativul AI peste datele deja prioritizate determinist (blueprint 17: * "deterministic priority -> AI explanation"). AI-ul explica, nu decide * ordinea. Esecul nu strica briefingul -- intoarce null, iar UI arata doar * partea deterministica. */ private async explain( session: AuthenticatedSession, overdueTasks: { title: string; dueAt: Date | null }[], upcomingTasks: { title: string; dueAt: Date | null }[], weekInReview: { newOrganizations: number; newSegments: number; newResearchBriefs: number }, ): Promise { if (overdueTasks.length === 0 && upcomingTasks.length === 0) { return null; } const formatTask = (task: { title: string; dueAt: Date | null }) => `- ${task.title}${task.dueAt ? ` (termen ${task.dueAt.toISOString().slice(0, 10)})` : ''}`; const state = [ `Restante (${overdueTasks.length}):`, ...overdueTasks.slice(0, 10).map(formatTask), `Urmatoarele 7 zile (${upcomingTasks.length}):`, ...upcomingTasks.slice(0, 10).map(formatTask), `Saptamana: ${weekInReview.newOrganizations} companii noi, ${weekInReview.newSegments} segmente, ${weekInReview.newResearchBriefs} research briefs.`, ].join('\n'); try { const result = await this.aiGateway.complete({ tenantId: session.tenantId, requestedByUserId: session.userId, purpose: 'daily_briefing', actionClass: 'read_only', allowedCategories: ['tasks', 'deadlines', 'organizations'], deniedCategories: ['health', 'identity_documents'], model: 'gpt-4o-mini', templateVersion: BRIEFING_TEMPLATE_VERSION, systemPrompt: 'Esti chief of staff. Scrii 2-3 propozitii in romana, direct, fara introduceri. ' + 'Folosesti exclusiv datele primite; nu inventezi taskuri sau termene. ' + 'Spui ce merita atentie azi si de ce.', userPrompt: `Rezuma situatia de azi pe baza starii de mai jos:\n\n${state}`, maxCostUsd: 0.05, }); return result.content; } catch (error) { this.logger.warn(`Daily briefing AI explanation unavailable: ${(error as Error).message}`); return null; } } }