@operor/analytics 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +224 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +571 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
- package/src/AnalyticsCollector.test.ts +344 -0
- package/src/AnalyticsCollector.ts +148 -0
- package/src/InMemoryAnalyticsStore.test.ts +495 -0
- package/src/InMemoryAnalyticsStore.ts +250 -0
- package/src/SQLiteAnalyticsStore.test.ts +554 -0
- package/src/SQLiteAnalyticsStore.ts +367 -0
- package/src/index.ts +4 -0
- package/src/types.ts +149 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +10 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type {
|
|
3
|
+
AnalyticsStore,
|
|
4
|
+
AnalyticsEvent,
|
|
5
|
+
AnalyticsSummary,
|
|
6
|
+
MessageVolumeData,
|
|
7
|
+
AgentStatsData,
|
|
8
|
+
CustomerStatsData,
|
|
9
|
+
KbStatsData,
|
|
10
|
+
ConversationSession,
|
|
11
|
+
TimeRange,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
|
|
14
|
+
const SESSION_GAP_MS = 30 * 60 * 1000; // 30 minutes
|
|
15
|
+
|
|
16
|
+
export class SQLiteAnalyticsStore implements AnalyticsStore {
|
|
17
|
+
private db: Database.Database;
|
|
18
|
+
|
|
19
|
+
constructor(dbPath: string = './analytics.db') {
|
|
20
|
+
this.db = new Database(dbPath);
|
|
21
|
+
this.db.pragma('journal_mode = WAL');
|
|
22
|
+
this.db.pragma('foreign_keys = ON');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async initialize(): Promise<void> {
|
|
26
|
+
this.db.exec(`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS message_events (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
timestamp INTEGER NOT NULL,
|
|
30
|
+
channel TEXT NOT NULL,
|
|
31
|
+
customer_phone TEXT NOT NULL,
|
|
32
|
+
customer_id TEXT NOT NULL,
|
|
33
|
+
agent_name TEXT NOT NULL,
|
|
34
|
+
intent TEXT NOT NULL DEFAULT '',
|
|
35
|
+
intent_confidence REAL NOT NULL DEFAULT 0,
|
|
36
|
+
response_time_ms INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
llm_cost REAL NOT NULL DEFAULT 0,
|
|
38
|
+
kb_top_score REAL NOT NULL DEFAULT 0,
|
|
39
|
+
kb_is_faq_match INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
kb_result_count INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
tool_calls_count INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
tool_calls_json TEXT,
|
|
43
|
+
message_length INTEGER NOT NULL DEFAULT 0,
|
|
44
|
+
response_length INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
is_new_customer INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
was_escalated INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
conversation_id TEXT,
|
|
48
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON message_events(timestamp);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_events_agent ON message_events(agent_name);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_events_customer ON message_events(customer_phone);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_events_conversation ON message_events(conversation_id);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
customer_id TEXT NOT NULL,
|
|
60
|
+
customer_phone TEXT NOT NULL,
|
|
61
|
+
channel TEXT NOT NULL,
|
|
62
|
+
agent_id TEXT NOT NULL,
|
|
63
|
+
started_at INTEGER NOT NULL,
|
|
64
|
+
ended_at INTEGER NOT NULL,
|
|
65
|
+
message_count INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
tool_call_count INTEGER NOT NULL DEFAULT 0,
|
|
67
|
+
total_duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
68
|
+
total_cost REAL NOT NULL DEFAULT 0,
|
|
69
|
+
resolved INTEGER NOT NULL DEFAULT 0,
|
|
70
|
+
resolution_type TEXT
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS daily_metrics (
|
|
74
|
+
date TEXT NOT NULL,
|
|
75
|
+
metric TEXT NOT NULL,
|
|
76
|
+
channel TEXT NOT NULL DEFAULT '',
|
|
77
|
+
agent_id TEXT NOT NULL DEFAULT '',
|
|
78
|
+
value REAL NOT NULL DEFAULT 0,
|
|
79
|
+
PRIMARY KEY (date, metric, channel, agent_id)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS analytics_meta (
|
|
83
|
+
key TEXT PRIMARY KEY,
|
|
84
|
+
value TEXT NOT NULL
|
|
85
|
+
);
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async close(): Promise<void> {
|
|
90
|
+
this.db.close();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async recordMessageEvent(event: AnalyticsEvent): Promise<void> {
|
|
94
|
+
this.db.prepare(`
|
|
95
|
+
INSERT INTO message_events
|
|
96
|
+
(timestamp, channel, customer_phone, customer_id, agent_name, intent,
|
|
97
|
+
intent_confidence, response_time_ms, llm_cost, kb_top_score, kb_is_faq_match,
|
|
98
|
+
kb_result_count, tool_calls_count, tool_calls_json, message_length,
|
|
99
|
+
response_length, is_new_customer, was_escalated, conversation_id,
|
|
100
|
+
prompt_tokens, completion_tokens)
|
|
101
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
102
|
+
`).run(
|
|
103
|
+
event.timestamp,
|
|
104
|
+
event.channel,
|
|
105
|
+
event.customerPhone,
|
|
106
|
+
event.customerId,
|
|
107
|
+
event.agentName,
|
|
108
|
+
event.intent,
|
|
109
|
+
event.intentConfidence,
|
|
110
|
+
event.responseTimeMs,
|
|
111
|
+
event.llmCost,
|
|
112
|
+
event.kbTopScore,
|
|
113
|
+
event.kbIsFaqMatch ? 1 : 0,
|
|
114
|
+
event.kbResultCount,
|
|
115
|
+
event.toolCallsCount,
|
|
116
|
+
event.toolCallsJson,
|
|
117
|
+
event.messageLength,
|
|
118
|
+
event.responseLength,
|
|
119
|
+
event.isNewCustomer ? 1 : 0,
|
|
120
|
+
event.wasEscalated ? 1 : 0,
|
|
121
|
+
event.conversationId,
|
|
122
|
+
event.promptTokens,
|
|
123
|
+
event.completionTokens,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async getOrCreateConversation(
|
|
128
|
+
customerPhone: string,
|
|
129
|
+
channel: string,
|
|
130
|
+
agentName: string,
|
|
131
|
+
): Promise<string> {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const cutoff = now - SESSION_GAP_MS;
|
|
134
|
+
|
|
135
|
+
// Find an active conversation within the session gap
|
|
136
|
+
const existing = this.db.prepare(`
|
|
137
|
+
SELECT id FROM conversations
|
|
138
|
+
WHERE customer_phone = ? AND channel = ? AND agent_id = ? AND ended_at > ?
|
|
139
|
+
ORDER BY ended_at DESC LIMIT 1
|
|
140
|
+
`).get(customerPhone, channel, agentName, cutoff) as any;
|
|
141
|
+
|
|
142
|
+
if (existing) {
|
|
143
|
+
return existing.id as string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Create new conversation
|
|
147
|
+
const id = crypto.randomUUID();
|
|
148
|
+
this.db.prepare(`
|
|
149
|
+
INSERT INTO conversations (id, customer_id, customer_phone, channel, agent_id, started_at, ended_at)
|
|
150
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
151
|
+
`).run(id, '', customerPhone, channel, agentName, now, now);
|
|
152
|
+
|
|
153
|
+
return id;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async updateConversationStats(
|
|
157
|
+
conversationId: string,
|
|
158
|
+
event: AnalyticsEvent,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
this.db.prepare(`
|
|
161
|
+
UPDATE conversations SET
|
|
162
|
+
customer_id = CASE WHEN customer_id = '' THEN ? ELSE customer_id END,
|
|
163
|
+
ended_at = ?,
|
|
164
|
+
message_count = message_count + 1,
|
|
165
|
+
tool_call_count = tool_call_count + ?,
|
|
166
|
+
total_duration_ms = ? - started_at,
|
|
167
|
+
total_cost = total_cost + ?
|
|
168
|
+
WHERE id = ?
|
|
169
|
+
`).run(
|
|
170
|
+
event.customerId,
|
|
171
|
+
event.timestamp,
|
|
172
|
+
event.toolCallsCount,
|
|
173
|
+
event.timestamp,
|
|
174
|
+
event.llmCost,
|
|
175
|
+
conversationId,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async getSummary(range?: TimeRange): Promise<AnalyticsSummary> {
|
|
180
|
+
const where = range ? 'WHERE timestamp >= ? AND timestamp <= ?' : '';
|
|
181
|
+
const params = range ? [range.from, range.to] : [];
|
|
182
|
+
|
|
183
|
+
const row = this.db.prepare(`
|
|
184
|
+
SELECT
|
|
185
|
+
COUNT(*) as total,
|
|
186
|
+
AVG(response_time_ms) as avg_response_time,
|
|
187
|
+
COUNT(DISTINCT customer_phone) as unique_customers,
|
|
188
|
+
SUM(CASE WHEN is_new_customer = 1 THEN 1 ELSE 0 END) as new_customers,
|
|
189
|
+
SUM(CASE WHEN kb_is_faq_match = 1 THEN 1 ELSE 0 END) as faq_hits,
|
|
190
|
+
SUM(CASE WHEN kb_top_score >= 0.7 THEN 1 ELSE 0 END) as kb_answered,
|
|
191
|
+
SUM(CASE WHEN kb_top_score < 0.3 THEN 1 ELSE 0 END) as no_answer
|
|
192
|
+
FROM message_events ${where}
|
|
193
|
+
`).get(...params) as any;
|
|
194
|
+
|
|
195
|
+
const total = row.total || 0;
|
|
196
|
+
|
|
197
|
+
// Peak day
|
|
198
|
+
const peakRow = this.db.prepare(`
|
|
199
|
+
SELECT date(timestamp / 1000, 'unixepoch') as day, COUNT(*) as cnt
|
|
200
|
+
FROM message_events ${where}
|
|
201
|
+
GROUP BY day ORDER BY cnt DESC LIMIT 1
|
|
202
|
+
`).get(...params) as any;
|
|
203
|
+
|
|
204
|
+
// Agent breakdown
|
|
205
|
+
const agentRows = this.db.prepare(`
|
|
206
|
+
SELECT agent_name, COUNT(*) as cnt
|
|
207
|
+
FROM message_events ${where}
|
|
208
|
+
GROUP BY agent_name
|
|
209
|
+
`).all(...params) as any[];
|
|
210
|
+
|
|
211
|
+
const agentBreakdown: Record<string, number> = {};
|
|
212
|
+
for (const r of agentRows) {
|
|
213
|
+
agentBreakdown[r.agent_name] = r.cnt;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Days in range for avg calculation
|
|
217
|
+
const daysRow = this.db.prepare(`
|
|
218
|
+
SELECT COUNT(DISTINCT date(timestamp / 1000, 'unixepoch')) as days
|
|
219
|
+
FROM message_events ${where}
|
|
220
|
+
`).get(...params) as any;
|
|
221
|
+
const days = daysRow.days || 1;
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
totalMessages: total,
|
|
225
|
+
avgPerDay: total / days,
|
|
226
|
+
peakDay: peakRow?.day ?? null,
|
|
227
|
+
kbAnsweredPct: total > 0 ? (row.kb_answered / total) * 100 : 0,
|
|
228
|
+
llmFallbackPct: total > 0 ? ((total - row.kb_answered) / total) * 100 : 0,
|
|
229
|
+
noAnswerPct: total > 0 ? (row.no_answer / total) * 100 : 0,
|
|
230
|
+
avgResponseTime: row.avg_response_time || 0,
|
|
231
|
+
faqHitRate: total > 0 ? (row.faq_hits / total) * 100 : 0,
|
|
232
|
+
uniqueCustomers: row.unique_customers || 0,
|
|
233
|
+
newCustomers: row.new_customers || 0,
|
|
234
|
+
returningCustomers: (row.unique_customers || 0) - (row.new_customers || 0),
|
|
235
|
+
agentBreakdown,
|
|
236
|
+
pendingReviews: 0, // populated externally from copilot
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async getMessageVolume(range?: TimeRange, groupBy: 'day' | 'hour' = 'day'): Promise<MessageVolumeData[]> {
|
|
241
|
+
const where = range ? 'WHERE timestamp >= ? AND timestamp <= ?' : '';
|
|
242
|
+
const params = range ? [range.from, range.to] : [];
|
|
243
|
+
|
|
244
|
+
const dateExpr = groupBy === 'hour'
|
|
245
|
+
? "strftime('%Y-%m-%d %H:00', timestamp / 1000, 'unixepoch')"
|
|
246
|
+
: "date(timestamp / 1000, 'unixepoch')";
|
|
247
|
+
|
|
248
|
+
const rows = this.db.prepare(`
|
|
249
|
+
SELECT ${dateExpr} as date, COUNT(*) as count
|
|
250
|
+
FROM message_events ${where}
|
|
251
|
+
GROUP BY date ORDER BY date
|
|
252
|
+
`).all(...params) as any[];
|
|
253
|
+
|
|
254
|
+
return rows.map(r => ({ date: r.date, count: r.count }));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async getAgentStats(range?: TimeRange): Promise<AgentStatsData[]> {
|
|
258
|
+
const where = range ? 'WHERE timestamp >= ? AND timestamp <= ?' : '';
|
|
259
|
+
const params = range ? [range.from, range.to] : [];
|
|
260
|
+
|
|
261
|
+
const rows = this.db.prepare(`
|
|
262
|
+
SELECT
|
|
263
|
+
agent_name,
|
|
264
|
+
COUNT(*) as message_count,
|
|
265
|
+
AVG(response_time_ms) as avg_response_time,
|
|
266
|
+
AVG(intent_confidence) as avg_confidence,
|
|
267
|
+
SUM(CASE WHEN was_escalated = 1 THEN 1 ELSE 0 END) as escalation_count,
|
|
268
|
+
SUM(tool_calls_count) as tool_call_count
|
|
269
|
+
FROM message_events ${where}
|
|
270
|
+
GROUP BY agent_name
|
|
271
|
+
`).all(...params) as any[];
|
|
272
|
+
|
|
273
|
+
return rows.map(r => ({
|
|
274
|
+
agentName: r.agent_name,
|
|
275
|
+
messageCount: r.message_count,
|
|
276
|
+
avgResponseTimeMs: r.avg_response_time || 0,
|
|
277
|
+
avgConfidence: r.avg_confidence || 0,
|
|
278
|
+
escalationCount: r.escalation_count || 0,
|
|
279
|
+
toolCallCount: r.tool_call_count || 0,
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async getCustomerStats(range?: TimeRange, limit = 50): Promise<CustomerStatsData[]> {
|
|
284
|
+
const where = range ? 'WHERE timestamp >= ? AND timestamp <= ?' : '';
|
|
285
|
+
const params: any[] = range ? [range.from, range.to, limit] : [limit];
|
|
286
|
+
|
|
287
|
+
const rows = this.db.prepare(`
|
|
288
|
+
SELECT
|
|
289
|
+
customer_id,
|
|
290
|
+
customer_phone,
|
|
291
|
+
COUNT(*) as message_count,
|
|
292
|
+
MIN(timestamp) as first_seen,
|
|
293
|
+
MAX(timestamp) as last_seen,
|
|
294
|
+
MAX(is_new_customer) as is_new
|
|
295
|
+
FROM message_events ${where}
|
|
296
|
+
GROUP BY customer_phone
|
|
297
|
+
ORDER BY message_count DESC
|
|
298
|
+
LIMIT ?
|
|
299
|
+
`).all(...params) as any[];
|
|
300
|
+
|
|
301
|
+
return rows.map(r => ({
|
|
302
|
+
customerId: r.customer_id,
|
|
303
|
+
customerPhone: r.customer_phone,
|
|
304
|
+
messageCount: r.message_count,
|
|
305
|
+
firstSeen: r.first_seen,
|
|
306
|
+
lastSeen: r.last_seen,
|
|
307
|
+
isNew: !!r.is_new,
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async getKbStats(range?: TimeRange): Promise<KbStatsData> {
|
|
312
|
+
const where = range ? 'WHERE timestamp >= ? AND timestamp <= ?' : '';
|
|
313
|
+
const params = range ? [range.from, range.to] : [];
|
|
314
|
+
|
|
315
|
+
const row = this.db.prepare(`
|
|
316
|
+
SELECT
|
|
317
|
+
COUNT(*) as total,
|
|
318
|
+
SUM(CASE WHEN kb_is_faq_match = 1 THEN 1 ELSE 0 END) as faq_hits,
|
|
319
|
+
AVG(kb_top_score) as avg_score
|
|
320
|
+
FROM message_events ${where}
|
|
321
|
+
`).get(...params) as any;
|
|
322
|
+
|
|
323
|
+
const total = row.total || 0;
|
|
324
|
+
|
|
325
|
+
const topHits = await this.getTopFaqHits(10, range);
|
|
326
|
+
const topMisses = await this.getTopMisses(10, range);
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
totalQueries: total,
|
|
330
|
+
faqHits: row.faq_hits || 0,
|
|
331
|
+
faqHitRate: total > 0 ? ((row.faq_hits || 0) / total) * 100 : 0,
|
|
332
|
+
avgTopScore: row.avg_score || 0,
|
|
333
|
+
topFaqHits: topHits,
|
|
334
|
+
topMisses: topMisses,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async getTopFaqHits(limit = 10, range?: TimeRange): Promise<Array<{ intent: string; count: number }>> {
|
|
339
|
+
const where = range
|
|
340
|
+
? 'WHERE kb_is_faq_match = 1 AND timestamp >= ? AND timestamp <= ?'
|
|
341
|
+
: 'WHERE kb_is_faq_match = 1';
|
|
342
|
+
const params: any[] = range ? [range.from, range.to, limit] : [limit];
|
|
343
|
+
|
|
344
|
+
const rows = this.db.prepare(`
|
|
345
|
+
SELECT intent, COUNT(*) as count
|
|
346
|
+
FROM message_events ${where}
|
|
347
|
+
GROUP BY intent ORDER BY count DESC LIMIT ?
|
|
348
|
+
`).all(...params) as any[];
|
|
349
|
+
|
|
350
|
+
return rows.map(r => ({ intent: r.intent, count: r.count }));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async getTopMisses(limit = 10, range?: TimeRange): Promise<Array<{ intent: string; count: number; avgScore: number }>> {
|
|
354
|
+
const where = range
|
|
355
|
+
? 'WHERE kb_top_score < 0.5 AND kb_top_score > 0 AND timestamp >= ? AND timestamp <= ?'
|
|
356
|
+
: 'WHERE kb_top_score < 0.5 AND kb_top_score > 0';
|
|
357
|
+
const params: any[] = range ? [range.from, range.to, limit] : [limit];
|
|
358
|
+
|
|
359
|
+
const rows = this.db.prepare(`
|
|
360
|
+
SELECT intent, COUNT(*) as count, AVG(kb_top_score) as avg_score
|
|
361
|
+
FROM message_events ${where}
|
|
362
|
+
GROUP BY intent ORDER BY count DESC LIMIT ?
|
|
363
|
+
`).all(...params) as any[];
|
|
364
|
+
|
|
365
|
+
return rows.map(r => ({ intent: r.intent, count: r.count, avgScore: r.avg_score }));
|
|
366
|
+
}
|
|
367
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics types for Operor
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** A single message event recorded by the analytics collector */
|
|
6
|
+
export interface AnalyticsEvent {
|
|
7
|
+
id?: number;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
channel: string;
|
|
10
|
+
customerPhone: string;
|
|
11
|
+
customerId: string;
|
|
12
|
+
agentName: string;
|
|
13
|
+
intent: string;
|
|
14
|
+
intentConfidence: number;
|
|
15
|
+
responseTimeMs: number;
|
|
16
|
+
llmCost: number;
|
|
17
|
+
kbTopScore: number;
|
|
18
|
+
kbIsFaqMatch: boolean;
|
|
19
|
+
kbResultCount: number;
|
|
20
|
+
toolCallsCount: number;
|
|
21
|
+
toolCallsJson: string | null;
|
|
22
|
+
messageLength: number;
|
|
23
|
+
responseLength: number;
|
|
24
|
+
isNewCustomer: boolean;
|
|
25
|
+
wasEscalated: boolean;
|
|
26
|
+
conversationId: string | null;
|
|
27
|
+
promptTokens: number;
|
|
28
|
+
completionTokens: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Aggregated summary for a time range */
|
|
32
|
+
export interface AnalyticsSummary {
|
|
33
|
+
totalMessages: number;
|
|
34
|
+
avgPerDay: number;
|
|
35
|
+
peakDay: string | null;
|
|
36
|
+
kbAnsweredPct: number;
|
|
37
|
+
llmFallbackPct: number;
|
|
38
|
+
noAnswerPct: number;
|
|
39
|
+
avgResponseTime: number;
|
|
40
|
+
faqHitRate: number;
|
|
41
|
+
uniqueCustomers: number;
|
|
42
|
+
newCustomers: number;
|
|
43
|
+
returningCustomers: number;
|
|
44
|
+
agentBreakdown: Record<string, number>;
|
|
45
|
+
pendingReviews: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Message volume data point (for charts) */
|
|
49
|
+
export interface MessageVolumeData {
|
|
50
|
+
date: string;
|
|
51
|
+
count: number;
|
|
52
|
+
channel?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Per-agent stats */
|
|
56
|
+
export interface AgentStatsData {
|
|
57
|
+
agentName: string;
|
|
58
|
+
messageCount: number;
|
|
59
|
+
avgResponseTimeMs: number;
|
|
60
|
+
avgConfidence: number;
|
|
61
|
+
escalationCount: number;
|
|
62
|
+
toolCallCount: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Per-customer stats */
|
|
66
|
+
export interface CustomerStatsData {
|
|
67
|
+
customerId: string;
|
|
68
|
+
customerPhone: string;
|
|
69
|
+
messageCount: number;
|
|
70
|
+
firstSeen: number;
|
|
71
|
+
lastSeen: number;
|
|
72
|
+
isNew: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Knowledge Base stats from analytics */
|
|
76
|
+
export interface KbStatsData {
|
|
77
|
+
totalQueries: number;
|
|
78
|
+
faqHits: number;
|
|
79
|
+
faqHitRate: number;
|
|
80
|
+
avgTopScore: number;
|
|
81
|
+
topFaqHits: Array<{ intent: string; count: number }>;
|
|
82
|
+
topMisses: Array<{ intent: string; count: number; avgScore: number }>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A conversation session (gap-based) */
|
|
86
|
+
export interface ConversationSession {
|
|
87
|
+
id: string;
|
|
88
|
+
customerId: string;
|
|
89
|
+
customerPhone: string;
|
|
90
|
+
channel: string;
|
|
91
|
+
agentId: string;
|
|
92
|
+
startedAt: number;
|
|
93
|
+
endedAt: number;
|
|
94
|
+
messageCount: number;
|
|
95
|
+
toolCallCount: number;
|
|
96
|
+
totalDurationMs: number;
|
|
97
|
+
totalCost: number;
|
|
98
|
+
resolved: boolean;
|
|
99
|
+
resolutionType: string | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Time range filter for queries */
|
|
103
|
+
export interface TimeRange {
|
|
104
|
+
from: number;
|
|
105
|
+
to: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Store interface for analytics persistence */
|
|
109
|
+
export interface AnalyticsStore {
|
|
110
|
+
initialize(): Promise<void>;
|
|
111
|
+
close(): Promise<void>;
|
|
112
|
+
|
|
113
|
+
/** Record a message event */
|
|
114
|
+
recordMessageEvent(event: AnalyticsEvent): Promise<void>;
|
|
115
|
+
|
|
116
|
+
/** Get or create a conversation session */
|
|
117
|
+
getOrCreateConversation(
|
|
118
|
+
customerPhone: string,
|
|
119
|
+
channel: string,
|
|
120
|
+
agentName: string,
|
|
121
|
+
): Promise<string>;
|
|
122
|
+
|
|
123
|
+
/** Update conversation stats after a message */
|
|
124
|
+
updateConversationStats(
|
|
125
|
+
conversationId: string,
|
|
126
|
+
event: AnalyticsEvent,
|
|
127
|
+
): Promise<void>;
|
|
128
|
+
|
|
129
|
+
/** Get aggregated summary for a time range */
|
|
130
|
+
getSummary(range?: TimeRange): Promise<AnalyticsSummary>;
|
|
131
|
+
|
|
132
|
+
/** Get message volume over time */
|
|
133
|
+
getMessageVolume(range?: TimeRange, groupBy?: 'day' | 'hour'): Promise<MessageVolumeData[]>;
|
|
134
|
+
|
|
135
|
+
/** Get per-agent stats */
|
|
136
|
+
getAgentStats(range?: TimeRange): Promise<AgentStatsData[]>;
|
|
137
|
+
|
|
138
|
+
/** Get per-customer stats */
|
|
139
|
+
getCustomerStats(range?: TimeRange, limit?: number): Promise<CustomerStatsData[]>;
|
|
140
|
+
|
|
141
|
+
/** Get KB-related stats */
|
|
142
|
+
getKbStats(range?: TimeRange): Promise<KbStatsData>;
|
|
143
|
+
|
|
144
|
+
/** Get top FAQ hits */
|
|
145
|
+
getTopFaqHits(limit?: number, range?: TimeRange): Promise<Array<{ intent: string; count: number }>>;
|
|
146
|
+
|
|
147
|
+
/** Get top misses (low KB score) */
|
|
148
|
+
getTopMisses(limit?: number, range?: TimeRange): Promise<Array<{ intent: string; count: number; avgScore: number }>>;
|
|
149
|
+
}
|
package/tsconfig.json
ADDED