/** * Runs one audit against an IT Glue record. * * Phase 4 supported only Application (flexible_asset) audits, all-time * scoped. Phase 4.1 generalizes: * - assetType: 'flexible_asset' | 'configuration' * - ticketScopeAnalysisId: when set, ticket evidence is the single source * analysis only ("ticket-first" mode). * * Flow: * 1. buildAssetAuditContext() — pulls everything; redacted. * 2. callLLMStage() — single shot using the deep-analysis tier model for * the requested provider (Sonnet for Anthropic, V4 Pro for OpenRouter). * 3. insertAssetAudit() — persists the row, including ticket linkage. */ import { callLLMStage } from '@/lib/services/llm/call'; import { type Provider, stageModelsFor, } from '@/lib/services/llm/models'; import { AssetAuditResponse } from '@/lib/types/analyzer'; import { buildAssetAuditContext, type AuditAssetType, } from './data-builder'; import { getSystemPrompt, buildAssetAuditUserPayload } from './prompt'; import { insertAssetAudit, insertFailedAssetAudit, } from './persistence'; const STAGE_MAX_TOKENS = 8_000; export interface RunAssetAuditInput { assetType: AuditAssetType; assetId: number | string; generatedByUserId: string | null; provider?: Provider; /** Phase 4.1: when set, ticket evidence narrows to just this analysis. */ ticketScopeAnalysisId?: string; } export interface RunAssetAuditResult { auditId: string; status: 'complete' | 'failed'; ticketCount: number; errorMessage?: string; } export async function runAssetAudit( input: RunAssetAuditInput ): Promise { const provider: Provider = input.provider ?? 'anthropic'; const model = stageModelsFor(provider).deep_analysis; const ctx = await buildAssetAuditContext({ assetType: input.assetType, assetId: input.assetId, ticketScopeAnalysisId: input.ticketScopeAnalysisId, }); const ticketCount = ctx.ticket_evidence.length; const assetSnapshot = { id: ctx.asset.id, name: ctx.asset.name, organization_id: ctx.asset.organization_id, organization_name: ctx.asset.organization_name, asset_type: ctx.asset_type, type_id: ctx.type_id, type_name: ctx.type_name, fields: ctx.asset.traits, }; const triggeredByTicketNumber = ctx.ticket_scope?.ticket_number ?? null; const triggeredByAnalysisId = ctx.ticket_scope?.analysis_id ?? null; const { payload } = buildAssetAuditUserPayload(ctx); const systemPrompt = getSystemPrompt(ctx); try { const result = await callLLMStage({ model, system: systemPrompt, user: payload, schema: AssetAuditResponse, maxTokens: STAGE_MAX_TOKENS, }); const inserted = await insertAssetAudit({ asset_type: ctx.asset_type, asset_id: ctx.asset.id, asset_type_id: ctx.type_id, organization_id: ctx.asset.organization_id, generated_by_user_id: input.generatedByUserId, provider, model_used: model, asset_snapshot: assetSnapshot, ticket_count: ticketCount, response: result.data, estimated_cost_usd: result.estimated_cost_usd, total_input_tokens: result.usage.input_tokens, total_output_tokens: result.usage.output_tokens, triggered_by_ticket_number: triggeredByTicketNumber, triggered_by_analysis_id: triggeredByAnalysisId, }); return { auditId: inserted.id, status: 'complete', ticketCount, }; } catch (err) { const message = err instanceof Error ? err.message : String(err); const failed = await insertFailedAssetAudit({ asset_type: ctx.asset_type, asset_id: ctx.asset.id, asset_type_id: ctx.type_id, organization_id: ctx.asset.organization_id, generated_by_user_id: input.generatedByUserId, provider, model_used: model, asset_snapshot: assetSnapshot, ticket_count: ticketCount, error_message: message, triggered_by_ticket_number: triggeredByTicketNumber, triggered_by_analysis_id: triggeredByAnalysisId, }); return { auditId: failed.id, status: 'failed', ticketCount, errorMessage: message, }; } }