import { createHandler } from './_shared/middleware' import { adminDb } from './_shared/db' import { resolveTypeId, resolveAgentId, resolvePromptConfigId } from './_shared/resolve-ids' import { runAgent } from './_shared/index' import { create } from './admin-data' /** * @module custom_support-redaction * @layer custom/cortex * @stability custom * * KB Redaction Analysis — drives the RedactionReview page. * Runs content through the KB Redaction Agent and returns structured * redaction suggestions + a generalised rewrite ready for human review. * * Action: POST ?action=analyze * body: { ticket_id: string, content: string } * * Response: { analysis: RedactionAnalysis } * RedactionAnalysis matches the shape expected by RedactionReview.tsx: * { * original_content: string * redacted_content: string * suggestions: RedactionSuggestion[] * confidence_score: number * processing_metadata: { model_used, temperature, tokens_consumed } * } * * The agent message is saved in an internal analysis thread on the ticket * (thread_purpose: 'redaction_analysis') as an audit trail. */ async function handleAnalyze(ctx: any, body: any) { const { ticket_id, content } = body if (!ticket_id) throw new Error('ticket_id is required') if (!content) throw new Error('content is required') const [threadTypeId, agentId, promptConfigId] = await Promise.all([ resolveTypeId('thread', 'support_thread'), resolveAgentId('KB Redaction Agent'), resolvePromptConfigId('kb_generator'), ]) // Reuse existing redaction thread if one exists (idempotent re-analysis) const { data: existing } = await adminDb .from('threads') .select('id') .eq('target_type', 'items') .eq('target_id', ticket_id) .eq('visibility', 'internal') .eq('data->>thread_purpose', 'redaction_analysis') .maybeSingle() let threadId = existing?.id if (!threadId) { const thread = await create(ctx, { entity: 'threads', type_id: threadTypeId, target_type: 'items', target_id: ticket_id, visibility: 'internal', status: 'active', data: { thread_purpose: 'redaction_analysis', agent_id: agentId, prompt_config_id: promptConfigId, } }) if (!thread?.id) throw new Error('Failed to create redaction thread') threadId = thread.id } // Run the KB Redaction Agent — returns structured JSON in message content const agentMsg = await runAgent(threadId, content, ctx) // Parse JSON from agent response let analysis: any = null try { const raw = agentMsg?.content || '{}' const cleaned = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim() analysis = JSON.parse(cleaned) } catch { // If the agent didn't return valid JSON, return a minimal analysis // so the UI can still proceed with the original content unredacted analysis = { original_content: content, redacted_content: content, suggestions: [], confidence_score: 0.0, processing_metadata: { model_used: 'unknown', temperature: 0.1, tokens_consumed: 0, error: 'Agent response was not valid JSON', } } } // Ensure required fields are present for RedactionReview.tsx if (!analysis.original_content) analysis.original_content = content if (!analysis.suggestions) analysis.suggestions = [] if (typeof analysis.confidence_score !== 'number') analysis.confidence_score = 0.5 if (!analysis.processing_metadata) analysis.processing_metadata = { model_used: 'unknown', temperature: 0.1, tokens_consumed: 0 } return { analysis, analysisThreadId: threadId } } export const handler = createHandler(async (ctx, body) => { const action = (ctx as any).query?.action switch (action) { case 'analyze': return handleAnalyze(ctx, body) default: throw new Error(`Unknown action: ${action}. Use 'analyze'.`) } })