Spaces:
Configuration error
Configuration error
Add multi-agent-system/agent-timeline.ts
Browse files
multi-agent-system/agent-timeline.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
* AGENT 2: TIMELINE AGENT
|
| 4 |
+
* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
+
*
|
| 6 |
+
* Provider: Local engine (deterministic) + Gemini Flash Lite (time extraction)
|
| 7 |
+
*
|
| 8 |
+
* Builds chronological event sequence, detects gaps, clusters events.
|
| 9 |
+
* Timestamps MUST be accurate β no AI hallucination allowed.
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
import { AgentResult, Finding, EvidenceItem } from './config';
|
| 13 |
+
import { callGemini } from './api-clients';
|
| 14 |
+
|
| 15 |
+
export class TimelineAgent {
|
| 16 |
+
id = 'timeline-agent';
|
| 17 |
+
name = 'Timeline Reconstruction Agent';
|
| 18 |
+
|
| 19 |
+
async analyze(evidence: EvidenceItem[], reportText?: string): Promise<AgentResult> {
|
| 20 |
+
const start = Date.now();
|
| 21 |
+
const findings: Finding[] = [];
|
| 22 |
+
|
| 23 |
+
if (!evidence || evidence.length === 0) {
|
| 24 |
+
return {
|
| 25 |
+
agentId: this.id, agentName: this.name, status: 'partial',
|
| 26 |
+
confidence: 0.2, findings: [],
|
| 27 |
+
metadata: { reason: 'No evidence items provided' },
|
| 28 |
+
executionTimeMs: Date.now() - start,
|
| 29 |
+
modelUsed: 'none', apiProvider: 'local',
|
| 30 |
+
};
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Step 1: Sort events chronologically (LOCAL β deterministic)
|
| 34 |
+
const sorted = [...evidence]
|
| 35 |
+
.filter(e => e.timestamp)
|
| 36 |
+
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
| 37 |
+
|
| 38 |
+
// Step 2: Extract additional time references from report text
|
| 39 |
+
let extractedTimes: string[] = [];
|
| 40 |
+
if (reportText) {
|
| 41 |
+
try {
|
| 42 |
+
const timeResponse = await callGemini(
|
| 43 |
+
`Extract ALL time references from this text. Return JSON array of strings with timestamps:
|
| 44 |
+
{"times": ["March 14, 2024 06:30 PM - body found", "09:00 AM - examination", etc]}
|
| 45 |
+
|
| 46 |
+
TEXT: ${reportText.slice(0, 2000)}`,
|
| 47 |
+
{ model: 'lite', jsonMode: true, temperature: 0 }
|
| 48 |
+
);
|
| 49 |
+
const parsed = JSON.parse(timeResponse);
|
| 50 |
+
extractedTimes = parsed.times || [];
|
| 51 |
+
} catch (e) {
|
| 52 |
+
// Time extraction failed β continue with evidence timestamps only
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Step 3: Detect timeline gaps (LOCAL β mathematical)
|
| 57 |
+
for (let i = 0; i < sorted.length - 1; i++) {
|
| 58 |
+
const t1 = new Date(sorted[i].timestamp).getTime();
|
| 59 |
+
const t2 = new Date(sorted[i + 1].timestamp).getTime();
|
| 60 |
+
const gapMinutes = (t2 - t1) / 60000;
|
| 61 |
+
|
| 62 |
+
if (gapMinutes > 30) {
|
| 63 |
+
const severity = gapMinutes > 480 ? 'CRITICAL' : gapMinutes > 120 ? 'HIGH' : 'MODERATE';
|
| 64 |
+
findings.push({
|
| 65 |
+
id: `timeline-gap-${i}`, type: 'TIMELINE_GAP',
|
| 66 |
+
content: `${Math.round(gapMinutes)} minute gap (${(gapMinutes / 60).toFixed(1)}h) between "${sorted[i].details}" and "${sorted[i + 1].details}"`,
|
| 67 |
+
confidence: 0.95, severity,
|
| 68 |
+
evidence: [sorted[i].timestamp, sorted[i + 1].timestamp],
|
| 69 |
+
relatedEntities: [sorted[i].source, sorted[i + 1].source],
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Step 4: Detect rapid event clusters (LOCAL β mathematical)
|
| 75 |
+
for (let i = 0; i < sorted.length - 1; i++) {
|
| 76 |
+
const t1 = new Date(sorted[i].timestamp).getTime();
|
| 77 |
+
const t2 = new Date(sorted[i + 1].timestamp).getTime();
|
| 78 |
+
const diffMin = (t2 - t1) / 60000;
|
| 79 |
+
|
| 80 |
+
if (diffMin > 0 && diffMin <= 5 && sorted[i].source !== sorted[i + 1].source) {
|
| 81 |
+
findings.push({
|
| 82 |
+
id: `timeline-cluster-${i}`, type: 'EVENT_CLUSTER',
|
| 83 |
+
content: `Rapid sequence (${diffMin.toFixed(1)}min): ${sorted[i].source} β ${sorted[i + 1].source}`,
|
| 84 |
+
confidence: 0.88, severity: 'HIGH',
|
| 85 |
+
evidence: [sorted[i].details, sorted[i + 1].details],
|
| 86 |
+
relatedEntities: [sorted[i].source, sorted[i + 1].source],
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Step 5: Calculate total timespan
|
| 92 |
+
if (sorted.length >= 2) {
|
| 93 |
+
const spanHours = (new Date(sorted[sorted.length - 1].timestamp).getTime() - new Date(sorted[0].timestamp).getTime()) / 3600000;
|
| 94 |
+
findings.push({
|
| 95 |
+
id: 'timeline-span', type: 'TIMELINE_SPAN',
|
| 96 |
+
content: `Evidence spans ${spanHours.toFixed(1)} hours across ${sorted.length} events from ${new Set(sorted.map(s => s.source)).size} sources`,
|
| 97 |
+
confidence: 1.0, severity: 'INFO',
|
| 98 |
+
evidence: [sorted[0].timestamp, sorted[sorted.length - 1].timestamp],
|
| 99 |
+
relatedEntities: [],
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Step 6: Identify critical incident window
|
| 104 |
+
const clusters = findings.filter(f => f.type === 'EVENT_CLUSTER');
|
| 105 |
+
if (clusters.length > 0) {
|
| 106 |
+
findings.push({
|
| 107 |
+
id: 'timeline-incident-window', type: 'INCIDENT_WINDOW',
|
| 108 |
+
content: `Critical incident window identified: ${clusters.length} rapid event clusters detected β likely corresponds to time of crime`,
|
| 109 |
+
confidence: 0.85, severity: 'CRITICAL',
|
| 110 |
+
evidence: clusters.map(c => c.content),
|
| 111 |
+
relatedEntities: ['victim', 'suspect', 'scene'],
|
| 112 |
+
});
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
return {
|
| 116 |
+
agentId: this.id, agentName: this.name, status: 'completed',
|
| 117 |
+
confidence: 0.92, findings,
|
| 118 |
+
metadata: {
|
| 119 |
+
totalEvents: sorted.length,
|
| 120 |
+
gaps: findings.filter(f => f.type === 'TIMELINE_GAP').length,
|
| 121 |
+
clusters: findings.filter(f => f.type === 'EVENT_CLUSTER').length,
|
| 122 |
+
extractedTimes,
|
| 123 |
+
sortedTimeline: sorted.map(e => ({ time: e.timestamp, source: e.source, event: e.details })),
|
| 124 |
+
},
|
| 125 |
+
executionTimeMs: Date.now() - start,
|
| 126 |
+
modelUsed: 'local-engine + gemini-2.5-flash-lite', apiProvider: 'local + gemini',
|
| 127 |
+
};
|
| 128 |
+
}
|
| 129 |
+
}
|