update agents skill design
This commit is contained in:
153
src/lib/agents/conversationInsights.js
Normal file
153
src/lib/agents/conversationInsights.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* What the stored conversations add up to.
|
||||
*
|
||||
* Pure selectors over the archive `history.js` returns: no reading, no React,
|
||||
* no knowledge of who is asking. Both the Insights view and the Conversation
|
||||
* Reviews view read through here, so a figure quoted in one and a list shown in
|
||||
* the other cannot describe different things.
|
||||
*
|
||||
* **Nothing is invented.** Every counter is derived from records that exist,
|
||||
* and a workspace with no conversations returns `empty: true` rather than a row
|
||||
* of zeros. A zero and an absence look identical on a dashboard and mean
|
||||
* completely different things — one says the agent was asked and did nothing,
|
||||
* the other says it has not been asked. The empty flag is what lets the view
|
||||
* say which.
|
||||
*/
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
const at = (record) => new Date(record?.updatedAt || 0).getTime();
|
||||
|
||||
/** Records for one agent, or all of them when no agent is named. */
|
||||
export function conversationsForAgent(records = [], agentId = null) {
|
||||
if (!agentId) return [...records];
|
||||
return records.filter((record) => record.agentId === agentId);
|
||||
}
|
||||
|
||||
/** Conversations nobody has rated yet — the queue a reviewer works through. */
|
||||
export const unratedConversations = (records = [], agentId = null) =>
|
||||
conversationsForAgent(records, agentId).filter((record) => !record.feedback);
|
||||
|
||||
/** Counts by a key each record contributes many of. */
|
||||
function tally(records, pick) {
|
||||
const counts = new Map();
|
||||
for (const record of records) {
|
||||
for (const value of pick(record) || []) {
|
||||
if (!value) continue;
|
||||
counts.set(value, (counts.get(value) || 0) + 1);
|
||||
}
|
||||
}
|
||||
return [...counts.entries()]
|
||||
.map(([id, count]) => ({ id, count }))
|
||||
.sort((a, b) => b.count - a.count || a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
/** Counts by a key each record contributes one of. */
|
||||
function tallyOne(records, pick) {
|
||||
const counts = new Map();
|
||||
for (const record of records) {
|
||||
const value = pick(record);
|
||||
if (!value) continue;
|
||||
counts.set(value, (counts.get(value) || 0) + 1);
|
||||
}
|
||||
return [...counts.entries()]
|
||||
.map(([id, count]) => ({ id, count }))
|
||||
.sort((a, b) => b.count - a.count || a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* The figures an Insights view reports.
|
||||
*
|
||||
* `since` windows the archive; `agentId` narrows it to one agent. Both are
|
||||
* optional, and neither invents a record that is not there.
|
||||
*/
|
||||
export function conversationStats(records = [], { agentId = null, since = null } = {}) {
|
||||
const scoped = conversationsForAgent(records, agentId)
|
||||
.filter((record) => (since ? at(record) >= new Date(since).getTime() : true));
|
||||
|
||||
if (!scoped.length) {
|
||||
return {
|
||||
empty: true,
|
||||
total: 0,
|
||||
turns: 0,
|
||||
averageTurns: 0,
|
||||
pages: 0,
|
||||
days: 0,
|
||||
activeDays: 0,
|
||||
feedback: { up: 0, down: 0, unrated: 0, score: null },
|
||||
byAgent: [],
|
||||
byPage: [],
|
||||
bySkill: [],
|
||||
byTool: [],
|
||||
byDay: [],
|
||||
};
|
||||
}
|
||||
|
||||
const turns = scoped.reduce((sum, record) => sum + (record.turns || 0), 0);
|
||||
|
||||
const up = scoped.filter((r) => r.feedback?.rating === 'up').length;
|
||||
const down = scoped.filter((r) => r.feedback?.rating === 'down').length;
|
||||
const rated = up + down;
|
||||
|
||||
/* Conversations per day, oldest first, over the days that actually have
|
||||
one. Padding out empty days would draw a chart mostly made of zeros and
|
||||
make a quiet week look like an outage. */
|
||||
const perDay = new Map();
|
||||
for (const record of scoped) {
|
||||
const day = new Date(at(record));
|
||||
day.setHours(0, 0, 0, 0);
|
||||
const key = day.toISOString().slice(0, 10);
|
||||
perDay.set(key, (perDay.get(key) || 0) + 1);
|
||||
}
|
||||
const byDay = [...perDay.entries()]
|
||||
.map(([day, count]) => ({ id: day, day, count }))
|
||||
.sort((a, b) => a.day.localeCompare(b.day));
|
||||
|
||||
const span = scoped.length
|
||||
? Math.max(1, Math.round((Math.max(...scoped.map(at)) - Math.min(...scoped.map(at))) / DAY) + 1)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
empty: false,
|
||||
total: scoped.length,
|
||||
turns,
|
||||
averageTurns: Math.round((turns / scoped.length) * 10) / 10,
|
||||
pages: new Set(scoped.map((r) => r.contextId).filter(Boolean)).size,
|
||||
/* Days the archive spans, and days anything was actually asked. Reporting
|
||||
only the first would make a busy afternoon look like a busy fortnight. */
|
||||
days: span,
|
||||
activeDays: byDay.length,
|
||||
feedback: {
|
||||
up,
|
||||
down,
|
||||
unrated: scoped.length - rated,
|
||||
/* Null rather than 0 when nothing is rated: a score of zero reads as
|
||||
unanimous disapproval. */
|
||||
score: rated ? Math.round((up / rated) * 100) : null,
|
||||
},
|
||||
byAgent: tallyOne(scoped, (r) => r.agentId),
|
||||
byPage: tallyOne(scoped, (r) => r.pageContext?.page || r.page),
|
||||
bySkill: tally(scoped, (r) => r.skillsUsed),
|
||||
byTool: tally(scoped, (r) => r.toolsUsed),
|
||||
byDay,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One conversation, reduced to what a review list shows.
|
||||
*
|
||||
* The thread itself is deliberately not included: a list renders forty of these
|
||||
* and only one is ever opened.
|
||||
*/
|
||||
export const reviewRow = (record) => ({
|
||||
id: record.id,
|
||||
title: record.title,
|
||||
agentId: record.agentId ?? null,
|
||||
page: record.pageContext?.page || record.page || '',
|
||||
route: record.pageContext?.route ?? null,
|
||||
turns: record.turns || 0,
|
||||
skillsUsed: record.skillsUsed || [],
|
||||
toolsUsed: record.toolsUsed || [],
|
||||
feedback: record.feedback ?? null,
|
||||
updatedAt: record.updatedAt,
|
||||
});
|
||||
Reference in New Issue
Block a user