feat: implement Priority 4 backend - Media Triage AI Service
Add AI-powered media inquiry triage with Tractatus governance: - MediaTriage.service.js: Comprehensive AI analysis service - Urgency classification (high/medium/low) with reasoning - Topic sensitivity detection - BoundaryEnforcer checks for values-sensitive topics - Talking points generation - Draft response generation (always requires human approval) - Triage statistics for transparency - Enhanced media.controller.js: - triageInquiry(): Run AI triage on specific inquiry - getTriageStats(): Public transparency endpoint - Full governance logging for audit trail - Updated media.routes.js: - POST /api/media/inquiries/:id/triage (admin only) - GET /api/media/triage-stats (public transparency) GOVERNANCE PRINCIPLES DEMONSTRATED: - AI analyzes and suggests, humans decide - 100% human review required before any response - All AI reasoning transparent and visible - BoundaryEnforcer escalates values-sensitive topics - No auto-responses without human approval Reference: docs/FEATURE_RICH_UI_IMPLEMENTATION_PLAN.md lines 123-164 Priority: 4 of 10 (10-12 hours estimated, backend complete) Status: Backend complete, frontend UI pending 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a15b285bb1
commit
3208bae7b0
3 changed files with 636 additions and 1 deletions
|
|
@ -7,6 +7,7 @@ const MediaInquiry = require('../models/MediaInquiry.model');
|
||||||
const ModerationQueue = require('../models/ModerationQueue.model');
|
const ModerationQueue = require('../models/ModerationQueue.model');
|
||||||
const GovernanceLog = require('../models/GovernanceLog.model');
|
const GovernanceLog = require('../models/GovernanceLog.model');
|
||||||
const BoundaryEnforcer = require('../services/BoundaryEnforcer.service');
|
const BoundaryEnforcer = require('../services/BoundaryEnforcer.service');
|
||||||
|
const MediaTriageService = require('../services/MediaTriage.service');
|
||||||
const logger = require('../utils/logger.util');
|
const logger = require('../utils/logger.util');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -303,6 +304,136 @@ async function deleteInquiry(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run AI triage on inquiry (admin)
|
||||||
|
* POST /api/media/inquiries/:id/triage
|
||||||
|
*
|
||||||
|
* Demonstrates Tractatus dogfooding: AI assists, human decides
|
||||||
|
*/
|
||||||
|
async function triageInquiry(req, res) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const inquiry = await MediaInquiry.findById(id);
|
||||||
|
|
||||||
|
if (!inquiry) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'Not Found',
|
||||||
|
message: 'Media inquiry not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Running AI triage on inquiry ${id}`);
|
||||||
|
|
||||||
|
// Run AI triage (MediaTriage service handles all analysis)
|
||||||
|
const triageResult = await MediaTriageService.triageInquiry(inquiry);
|
||||||
|
|
||||||
|
// Update inquiry with triage results
|
||||||
|
await MediaInquiry.update(id, {
|
||||||
|
'ai_triage.urgency': triageResult.urgency,
|
||||||
|
'ai_triage.urgency_score': triageResult.urgency_score,
|
||||||
|
'ai_triage.urgency_reasoning': triageResult.urgency_reasoning,
|
||||||
|
'ai_triage.topic_sensitivity': triageResult.topic_sensitivity,
|
||||||
|
'ai_triage.sensitivity_reasoning': triageResult.sensitivity_reasoning,
|
||||||
|
'ai_triage.involves_values': triageResult.involves_values,
|
||||||
|
'ai_triage.values_reasoning': triageResult.values_reasoning,
|
||||||
|
'ai_triage.boundary_enforcement': triageResult.boundary_enforcement,
|
||||||
|
'ai_triage.suggested_response_time': triageResult.suggested_response_time,
|
||||||
|
'ai_triage.suggested_talking_points': triageResult.suggested_talking_points,
|
||||||
|
'ai_triage.draft_response': triageResult.draft_response,
|
||||||
|
'ai_triage.draft_response_reasoning': triageResult.draft_response_reasoning,
|
||||||
|
'ai_triage.triaged_at': triageResult.triaged_at,
|
||||||
|
'ai_triage.ai_model': triageResult.ai_model,
|
||||||
|
status: 'triaged'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log governance action
|
||||||
|
await GovernanceLog.create({
|
||||||
|
action: 'AI_TRIAGE',
|
||||||
|
entity_type: 'media_inquiry',
|
||||||
|
entity_id: id,
|
||||||
|
actor: req.user.email,
|
||||||
|
quadrant: triageResult.involves_values ? 'STRATEGIC' : 'OPERATIONAL',
|
||||||
|
tractatus_component: 'BoundaryEnforcer',
|
||||||
|
reasoning: triageResult.values_reasoning,
|
||||||
|
outcome: 'success',
|
||||||
|
metadata: {
|
||||||
|
urgency: triageResult.urgency,
|
||||||
|
urgency_score: triageResult.urgency_score,
|
||||||
|
involves_values: triageResult.involves_values,
|
||||||
|
boundary_enforced: triageResult.involves_values,
|
||||||
|
human_approval_required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`AI triage complete for inquiry ${id}: urgency=${triageResult.urgency}, values=${triageResult.involves_values}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'AI triage completed',
|
||||||
|
triage: triageResult,
|
||||||
|
governance: {
|
||||||
|
human_approval_required: true,
|
||||||
|
boundary_enforcer_active: triageResult.involves_values,
|
||||||
|
transparency_note: 'All AI reasoning is visible for human review'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Triage inquiry error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: 'AI triage failed',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get triage statistics for public transparency
|
||||||
|
* GET /api/media/triage-stats
|
||||||
|
*/
|
||||||
|
async function getTriageStats(req, res) {
|
||||||
|
try {
|
||||||
|
// Get all triaged inquiries (public stats, no sensitive data)
|
||||||
|
const { getCollection } = require('../utils/db.util');
|
||||||
|
const collection = await getCollection('media_inquiries');
|
||||||
|
|
||||||
|
const inquiries = await collection.find({
|
||||||
|
'ai_triage.triaged_at': { $exists: true }
|
||||||
|
}).toArray();
|
||||||
|
|
||||||
|
const stats = await MediaTriageService.getTriageStats(inquiries);
|
||||||
|
|
||||||
|
// Add transparency metrics
|
||||||
|
const transparencyMetrics = {
|
||||||
|
...stats,
|
||||||
|
human_review_rate: '100%', // All inquiries require human review
|
||||||
|
ai_auto_response_rate: '0%', // No auto-responses allowed
|
||||||
|
boundary_enforcement_active: stats.boundary_enforcements > 0,
|
||||||
|
framework_compliance: {
|
||||||
|
human_approval_required: true,
|
||||||
|
ai_reasoning_transparent: true,
|
||||||
|
values_decisions_escalated: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
period: 'all_time',
|
||||||
|
statistics: transparencyMetrics,
|
||||||
|
note: 'All media inquiries require human review before response. AI assists with triage only.'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Get triage stats error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: 'Failed to retrieve statistics'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
submitInquiry,
|
submitInquiry,
|
||||||
listInquiries,
|
listInquiries,
|
||||||
|
|
@ -310,5 +441,7 @@ module.exports = {
|
||||||
getInquiry,
|
getInquiry,
|
||||||
assignInquiry,
|
assignInquiry,
|
||||||
respondToInquiry,
|
respondToInquiry,
|
||||||
deleteInquiry
|
deleteInquiry,
|
||||||
|
triageInquiry,
|
||||||
|
getTriageStats
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ router.post('/inquiries',
|
||||||
asyncHandler(mediaController.submitInquiry)
|
asyncHandler(mediaController.submitInquiry)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// GET /api/media/triage-stats - Get triage statistics (public, transparency)
|
||||||
|
router.get('/triage-stats',
|
||||||
|
asyncHandler(mediaController.getTriageStats)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin routes
|
* Admin routes
|
||||||
*/
|
*/
|
||||||
|
|
@ -56,6 +61,14 @@ router.post('/inquiries/:id/assign',
|
||||||
asyncHandler(mediaController.assignInquiry)
|
asyncHandler(mediaController.assignInquiry)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// POST /api/media/inquiries/:id/triage - Run AI triage (admin)
|
||||||
|
router.post('/inquiries/:id/triage',
|
||||||
|
authenticateToken,
|
||||||
|
requireRole('admin', 'moderator'),
|
||||||
|
validateObjectId('id'),
|
||||||
|
asyncHandler(mediaController.triageInquiry)
|
||||||
|
);
|
||||||
|
|
||||||
// POST /api/media/inquiries/:id/respond - Mark as responded (admin)
|
// POST /api/media/inquiries/:id/respond - Mark as responded (admin)
|
||||||
router.post('/inquiries/:id/respond',
|
router.post('/inquiries/:id/respond',
|
||||||
authenticateToken,
|
authenticateToken,
|
||||||
|
|
|
||||||
489
src/services/MediaTriage.service.js
Normal file
489
src/services/MediaTriage.service.js
Normal file
|
|
@ -0,0 +1,489 @@
|
||||||
|
/**
|
||||||
|
* Media Triage Service
|
||||||
|
* AI-powered media inquiry triage with Tractatus governance
|
||||||
|
*
|
||||||
|
* GOVERNANCE PRINCIPLES:
|
||||||
|
* - AI analyzes and suggests, humans decide
|
||||||
|
* - All reasoning must be transparent
|
||||||
|
* - Values decisions require human approval
|
||||||
|
* - No auto-responses without human review
|
||||||
|
* - Boundary enforcement for sensitive topics
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Anthropic = require('@anthropic-ai/sdk');
|
||||||
|
const logger = require('../utils/logger.util');
|
||||||
|
|
||||||
|
class MediaTriageService {
|
||||||
|
constructor() {
|
||||||
|
// Initialize Anthropic client
|
||||||
|
this.client = new Anthropic({
|
||||||
|
apiKey: process.env.ANTHROPIC_API_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
// Topic sensitivity keywords (triggers boundary enforcement)
|
||||||
|
this.SENSITIVE_TOPICS = [
|
||||||
|
'values', 'ethics', 'strategic direction', 'partnerships',
|
||||||
|
'te tiriti', 'māori', 'indigenous', 'governance philosophy',
|
||||||
|
'framework limitations', 'criticism', 'controversy'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Urgency indicators
|
||||||
|
this.URGENCY_INDICATORS = {
|
||||||
|
high: ['urgent', 'asap', 'immediate', 'breaking', 'deadline today', 'deadline tomorrow'],
|
||||||
|
medium: ['deadline this week', 'timely', 'soon'],
|
||||||
|
low: ['no deadline', 'general inquiry', 'background']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform AI triage on media inquiry
|
||||||
|
* Returns structured analysis for human review
|
||||||
|
*/
|
||||||
|
async triageInquiry(inquiry) {
|
||||||
|
try {
|
||||||
|
logger.info(`AI triaging inquiry: ${inquiry._id}`);
|
||||||
|
|
||||||
|
// Step 1: Analyze urgency
|
||||||
|
const urgencyAnalysis = await this.analyzeUrgency(inquiry);
|
||||||
|
|
||||||
|
// Step 2: Detect topic sensitivity
|
||||||
|
const sensitivityAnalysis = await this.analyzeTopicSensitivity(inquiry);
|
||||||
|
|
||||||
|
// Step 3: Check if involves values (BoundaryEnforcer)
|
||||||
|
const valuesCheck = this.checkInvolvesValues(inquiry, sensitivityAnalysis);
|
||||||
|
|
||||||
|
// Step 4: Generate suggested talking points
|
||||||
|
const talkingPoints = await this.generateTalkingPoints(inquiry, sensitivityAnalysis);
|
||||||
|
|
||||||
|
// Step 5: Draft response (ALWAYS requires human approval)
|
||||||
|
const draftResponse = await this.generateDraftResponse(inquiry, talkingPoints, valuesCheck);
|
||||||
|
|
||||||
|
// Step 6: Calculate suggested response time
|
||||||
|
const suggestedResponseTime = this.calculateResponseTime(urgencyAnalysis, inquiry);
|
||||||
|
|
||||||
|
// Compile triage result with full transparency
|
||||||
|
const triageResult = {
|
||||||
|
urgency: urgencyAnalysis.level,
|
||||||
|
urgency_score: urgencyAnalysis.score,
|
||||||
|
urgency_reasoning: urgencyAnalysis.reasoning,
|
||||||
|
|
||||||
|
topic_sensitivity: sensitivityAnalysis.level,
|
||||||
|
sensitivity_reasoning: sensitivityAnalysis.reasoning,
|
||||||
|
|
||||||
|
involves_values: valuesCheck.involves_values,
|
||||||
|
values_reasoning: valuesCheck.reasoning,
|
||||||
|
boundary_enforcement: valuesCheck.boundary_enforcement,
|
||||||
|
|
||||||
|
suggested_response_time: suggestedResponseTime,
|
||||||
|
suggested_talking_points: talkingPoints,
|
||||||
|
|
||||||
|
draft_response: draftResponse.content,
|
||||||
|
draft_response_reasoning: draftResponse.reasoning,
|
||||||
|
draft_requires_human_approval: true, // ALWAYS
|
||||||
|
|
||||||
|
triaged_at: new Date(),
|
||||||
|
ai_model: 'claude-3-5-sonnet-20241022',
|
||||||
|
framework_compliance: {
|
||||||
|
boundary_enforcer_checked: true,
|
||||||
|
human_approval_required: true,
|
||||||
|
reasoning_transparent: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(`Triage complete for inquiry ${inquiry._id}: urgency=${urgencyAnalysis.level}, values=${valuesCheck.involves_values}`);
|
||||||
|
|
||||||
|
return triageResult;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Media triage error:', error);
|
||||||
|
throw new Error(`Triage failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze urgency level of inquiry
|
||||||
|
*/
|
||||||
|
async analyzeUrgency(inquiry) {
|
||||||
|
const prompt = `Analyze the urgency of this media inquiry and provide a structured assessment.
|
||||||
|
|
||||||
|
INQUIRY DETAILS:
|
||||||
|
Subject: ${inquiry.inquiry.subject}
|
||||||
|
Message: ${inquiry.inquiry.message}
|
||||||
|
Deadline: ${inquiry.inquiry.deadline || 'Not specified'}
|
||||||
|
Outlet: ${inquiry.contact.outlet}
|
||||||
|
|
||||||
|
TASK:
|
||||||
|
1. Determine urgency level: HIGH, MEDIUM, or LOW
|
||||||
|
2. Provide urgency score (0-100)
|
||||||
|
3. Explain your reasoning
|
||||||
|
|
||||||
|
URGENCY GUIDELINES:
|
||||||
|
- HIGH (80-100): Breaking news, same-day deadline, crisis response
|
||||||
|
- MEDIUM (40-79): This week deadline, feature story, ongoing coverage
|
||||||
|
- LOW (0-39): No deadline, background research, general inquiry
|
||||||
|
|
||||||
|
Respond in JSON format:
|
||||||
|
{
|
||||||
|
"level": "HIGH|MEDIUM|LOW",
|
||||||
|
"score": 0-100,
|
||||||
|
"reasoning": "2-3 sentence explanation"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await this.client.messages.create({
|
||||||
|
model: 'claude-3-5-sonnet-20241022',
|
||||||
|
max_tokens: 500,
|
||||||
|
messages: [{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = message.content[0].text;
|
||||||
|
const analysis = JSON.parse(responseText);
|
||||||
|
|
||||||
|
return {
|
||||||
|
level: analysis.level.toLowerCase(),
|
||||||
|
score: analysis.score,
|
||||||
|
reasoning: analysis.reasoning
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Urgency analysis error:', error);
|
||||||
|
// Fallback to basic analysis
|
||||||
|
return this.basicUrgencyAnalysis(inquiry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze topic sensitivity
|
||||||
|
*/
|
||||||
|
async analyzeTopicSensitivity(inquiry) {
|
||||||
|
const prompt = `Analyze the topic sensitivity of this media inquiry for an AI safety framework organization.
|
||||||
|
|
||||||
|
INQUIRY DETAILS:
|
||||||
|
Subject: ${inquiry.inquiry.subject}
|
||||||
|
Message: ${inquiry.inquiry.message}
|
||||||
|
Topics: ${inquiry.inquiry.topic_areas?.join(', ') || 'Not specified'}
|
||||||
|
|
||||||
|
TASK:
|
||||||
|
Determine if this inquiry touches on sensitive topics such as:
|
||||||
|
- Framework values or ethics
|
||||||
|
- Strategic partnerships
|
||||||
|
- Indigenous data sovereignty (Te Tiriti o Waitangi)
|
||||||
|
- Framework limitations or criticisms
|
||||||
|
- Controversial AI safety debates
|
||||||
|
|
||||||
|
Provide sensitivity level: HIGH, MEDIUM, or LOW
|
||||||
|
|
||||||
|
Respond in JSON format:
|
||||||
|
{
|
||||||
|
"level": "HIGH|MEDIUM|LOW",
|
||||||
|
"reasoning": "2-3 sentence explanation of why this topic is sensitive or not"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await this.client.messages.create({
|
||||||
|
model: 'claude-3-5-sonnet-20241022',
|
||||||
|
max_tokens: 500,
|
||||||
|
messages: [{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = message.content[0].text;
|
||||||
|
const analysis = JSON.parse(responseText);
|
||||||
|
|
||||||
|
return {
|
||||||
|
level: analysis.level.toLowerCase(),
|
||||||
|
reasoning: analysis.reasoning
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Sensitivity analysis error:', error);
|
||||||
|
// Fallback to keyword-based analysis
|
||||||
|
return this.basicSensitivityAnalysis(inquiry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if inquiry involves framework values (BoundaryEnforcer)
|
||||||
|
*/
|
||||||
|
checkInvolvesValues(inquiry, sensitivityAnalysis) {
|
||||||
|
// Keywords that indicate values territory
|
||||||
|
const valuesKeywords = [
|
||||||
|
'values', 'ethics', 'mission', 'principles', 'philosophy',
|
||||||
|
'te tiriti', 'indigenous', 'sovereignty', 'partnership',
|
||||||
|
'governance', 'strategy', 'direction', 'why tractatus'
|
||||||
|
];
|
||||||
|
|
||||||
|
const combinedText = `${inquiry.inquiry.subject} ${inquiry.inquiry.message}`.toLowerCase();
|
||||||
|
const hasValuesKeyword = valuesKeywords.some(keyword => combinedText.includes(keyword));
|
||||||
|
const isHighSensitivity = sensitivityAnalysis.level === 'high';
|
||||||
|
|
||||||
|
const involves_values = hasValuesKeyword || isHighSensitivity;
|
||||||
|
|
||||||
|
return {
|
||||||
|
involves_values,
|
||||||
|
reasoning: involves_values
|
||||||
|
? 'This inquiry touches on framework values, strategic direction, or sensitive topics. Human approval required for any response (BoundaryEnforcer).'
|
||||||
|
: 'This inquiry is operational/technical in nature. Standard response workflow applies.',
|
||||||
|
boundary_enforcement: involves_values
|
||||||
|
? 'ENFORCED: Response must be reviewed and approved by John Stroh before sending.'
|
||||||
|
: 'NOT_REQUIRED: Standard review process applies.',
|
||||||
|
escalation_required: involves_values,
|
||||||
|
escalation_reason: involves_values
|
||||||
|
? 'Values-sensitive topic detected by BoundaryEnforcer'
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate suggested talking points
|
||||||
|
*/
|
||||||
|
async generateTalkingPoints(inquiry, sensitivityAnalysis) {
|
||||||
|
const prompt = `Generate 3-5 concise talking points for responding to this media inquiry about an AI safety framework.
|
||||||
|
|
||||||
|
INQUIRY DETAILS:
|
||||||
|
Subject: ${inquiry.inquiry.subject}
|
||||||
|
Message: ${inquiry.inquiry.message}
|
||||||
|
Sensitivity: ${sensitivityAnalysis.level}
|
||||||
|
|
||||||
|
GUIDELINES:
|
||||||
|
- Focus on factual, verifiable information
|
||||||
|
- Avoid speculation or aspirational claims
|
||||||
|
- Stay within established framework documentation
|
||||||
|
- Be honest about limitations
|
||||||
|
- NO fabricated statistics
|
||||||
|
- NO absolute guarantees
|
||||||
|
|
||||||
|
Respond with JSON array of talking points:
|
||||||
|
["Point 1", "Point 2", "Point 3", ...]`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await this.client.messages.create({
|
||||||
|
model: 'claude-3-5-sonnet-20241022',
|
||||||
|
max_tokens: 800,
|
||||||
|
messages: [{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = message.content[0].text;
|
||||||
|
const points = JSON.parse(responseText);
|
||||||
|
|
||||||
|
return Array.isArray(points) ? points : [];
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Talking points generation error:', error);
|
||||||
|
return [
|
||||||
|
'Tractatus is a development-stage AI safety framework',
|
||||||
|
'Focus on architectural safety guarantees and human oversight',
|
||||||
|
'Open source and transparent governance'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate draft response (ALWAYS requires human approval)
|
||||||
|
*/
|
||||||
|
async generateDraftResponse(inquiry, talkingPoints, valuesCheck) {
|
||||||
|
const prompt = `Draft a professional response to this media inquiry. This draft will be reviewed and edited by humans before sending.
|
||||||
|
|
||||||
|
INQUIRY DETAILS:
|
||||||
|
From: ${inquiry.contact.name} (${inquiry.contact.outlet})
|
||||||
|
Subject: ${inquiry.inquiry.subject}
|
||||||
|
Message: ${inquiry.inquiry.message}
|
||||||
|
|
||||||
|
TALKING POINTS TO INCLUDE:
|
||||||
|
${talkingPoints.map((p, i) => `${i + 1}. ${p}`).join('\n')}
|
||||||
|
|
||||||
|
VALUES CHECK:
|
||||||
|
${valuesCheck.involves_values ? '⚠️ This touches on framework values - response requires strategic approval' : 'Standard operational inquiry'}
|
||||||
|
|
||||||
|
GUIDELINES:
|
||||||
|
- Professional and helpful tone
|
||||||
|
- 2-3 paragraphs maximum
|
||||||
|
- Include contact info for follow-up
|
||||||
|
- Offer to provide additional resources
|
||||||
|
- Be honest about framework status (development stage)
|
||||||
|
- NO fabricated statistics or guarantees
|
||||||
|
|
||||||
|
Draft the response:`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await this.client.messages.create({
|
||||||
|
model: 'claude-3-5-sonnet-20241022',
|
||||||
|
max_tokens: 1000,
|
||||||
|
messages: [{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
const draftContent = message.content[0].text;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: draftContent,
|
||||||
|
reasoning: 'AI-generated draft based on talking points. MUST be reviewed and approved by human before sending.',
|
||||||
|
requires_approval: true,
|
||||||
|
approval_level: valuesCheck.involves_values ? 'STRATEGIC' : 'OPERATIONAL'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Draft response generation error:', error);
|
||||||
|
return {
|
||||||
|
content: `[DRAFT GENERATION FAILED - Manual response required]\n\nHi ${inquiry.contact.name},\n\nThank you for your inquiry about Tractatus. We'll get back to you shortly with a detailed response.\n\nBest regards,\nTractatus Team`,
|
||||||
|
reasoning: 'Fallback template due to AI generation error',
|
||||||
|
requires_approval: true,
|
||||||
|
approval_level: 'OPERATIONAL'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate suggested response time in hours
|
||||||
|
*/
|
||||||
|
calculateResponseTime(urgencyAnalysis, inquiry) {
|
||||||
|
if (inquiry.inquiry.deadline) {
|
||||||
|
const deadline = new Date(inquiry.inquiry.deadline);
|
||||||
|
const now = new Date();
|
||||||
|
const hoursUntilDeadline = (deadline - now) / (1000 * 60 * 60);
|
||||||
|
return Math.max(1, Math.floor(hoursUntilDeadline * 0.5)); // Aim for 50% of time to deadline
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on urgency score
|
||||||
|
if (urgencyAnalysis.level === 'high') {
|
||||||
|
return 4; // 4 hours
|
||||||
|
} else if (urgencyAnalysis.level === 'medium') {
|
||||||
|
return 24; // 1 day
|
||||||
|
} else {
|
||||||
|
return 72; // 3 days
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic urgency analysis (fallback)
|
||||||
|
*/
|
||||||
|
basicUrgencyAnalysis(inquiry) {
|
||||||
|
const text = `${inquiry.inquiry.subject} ${inquiry.inquiry.message}`.toLowerCase();
|
||||||
|
let score = 30; // Default low
|
||||||
|
let level = 'low';
|
||||||
|
|
||||||
|
// Check for urgency keywords
|
||||||
|
for (const [urgencyLevel, keywords] of Object.entries(this.URGENCY_INDICATORS)) {
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
if (text.includes(keyword)) {
|
||||||
|
if (urgencyLevel === 'high') {
|
||||||
|
score = 85;
|
||||||
|
level = 'high';
|
||||||
|
} else if (urgencyLevel === 'medium' && score < 60) {
|
||||||
|
score = 60;
|
||||||
|
level = 'medium';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check deadline
|
||||||
|
if (inquiry.inquiry.deadline) {
|
||||||
|
const deadline = new Date(inquiry.inquiry.deadline);
|
||||||
|
const now = new Date();
|
||||||
|
const hoursUntilDeadline = (deadline - now) / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
if (hoursUntilDeadline < 24) {
|
||||||
|
score = 90;
|
||||||
|
level = 'high';
|
||||||
|
} else if (hoursUntilDeadline < 72) {
|
||||||
|
score = 65;
|
||||||
|
level = 'medium';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
level,
|
||||||
|
score,
|
||||||
|
reasoning: `Basic analysis based on keywords and deadline. Urgency level: ${level}.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic sensitivity analysis (fallback)
|
||||||
|
*/
|
||||||
|
basicSensitivityAnalysis(inquiry) {
|
||||||
|
const text = `${inquiry.inquiry.subject} ${inquiry.inquiry.message}`.toLowerCase();
|
||||||
|
let level = 'low';
|
||||||
|
|
||||||
|
for (const keyword of this.SENSITIVE_TOPICS) {
|
||||||
|
if (text.includes(keyword)) {
|
||||||
|
level = 'high';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
level,
|
||||||
|
reasoning: level === 'high'
|
||||||
|
? 'Topic involves potentially sensitive framework areas'
|
||||||
|
: 'Standard operational inquiry'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get triage statistics for transparency
|
||||||
|
*/
|
||||||
|
async getTriageStats(inquiries) {
|
||||||
|
const stats = {
|
||||||
|
total_triaged: inquiries.length,
|
||||||
|
by_urgency: {
|
||||||
|
high: 0,
|
||||||
|
medium: 0,
|
||||||
|
low: 0
|
||||||
|
},
|
||||||
|
by_sensitivity: {
|
||||||
|
high: 0,
|
||||||
|
medium: 0,
|
||||||
|
low: 0
|
||||||
|
},
|
||||||
|
involves_values_count: 0,
|
||||||
|
boundary_enforcements: 0,
|
||||||
|
avg_response_time_hours: 0,
|
||||||
|
human_overrides: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const inquiry of inquiries) {
|
||||||
|
if (inquiry.ai_triage) {
|
||||||
|
// Count by urgency
|
||||||
|
if (inquiry.ai_triage.urgency) {
|
||||||
|
stats.by_urgency[inquiry.ai_triage.urgency]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count by sensitivity
|
||||||
|
if (inquiry.ai_triage.topic_sensitivity) {
|
||||||
|
stats.by_sensitivity[inquiry.ai_triage.topic_sensitivity]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count values involvements
|
||||||
|
if (inquiry.ai_triage.involves_values) {
|
||||||
|
stats.involves_values_count++;
|
||||||
|
stats.boundary_enforcements++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Average response time
|
||||||
|
if (inquiry.ai_triage.suggested_response_time) {
|
||||||
|
stats.avg_response_time_hours += inquiry.ai_triage.suggested_response_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.total_triaged > 0) {
|
||||||
|
stats.avg_response_time_hours = Math.round(stats.avg_response_time_hours / stats.total_triaged);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new MediaTriageService();
|
||||||
Loading…
Add table
Reference in a new issue