Muthukumarank commited on
Commit
8b4090b
Β·
verified Β·
1 Parent(s): 634e0c3

Add multi-agent-system/agent-timeline.ts

Browse files
Files changed (1) hide show
  1. multi-agent-system/agent-timeline.ts +129 -0
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
+ }