feat(ai): build extended data snapshots

This commit is contained in:
digua committed 2026-06-10 00:15:25 +08:00
1 parent 383af0b13a
commit 718735a748
11 files changed
+201 -28

No files matched your search

+2 -13
View File
@@ -9,6 +9,7 @@ import {
SkillManager,
buildSkillMenuWithBuiltinChart,
createActivateSkillTool,
createDataSnapshotFromOverview,
getAllowedBuiltinToolsForChartAutoSkill,
getChartCapabilitySkill,
getSkillConfigWithBuiltinChart,
@@ -166,19 +167,7 @@ export function createCliRunAgentStream(
let dataSnapshot: DataSnapshot | undefined
if (db) {
try {
const overview = getChatOverview(db, 5)
if (overview) {
dataSnapshot = {
name: overview.name,
platform: overview.platform,
type: overview.type,
totalMessages: overview.totalMessages,
totalMembers: overview.totalMembers,
firstMessageTs: overview.firstMessageTs,
lastMessageTs: overview.lastMessageTs,
capturedAt: Math.floor(Date.now() / 1000),
}
}
dataSnapshot = createDataSnapshotFromOverview(getChatOverview(db, 10))
} catch {
// non-fatal
}
+2 -13
View File
@@ -13,6 +13,7 @@ import {
CHART_CAPABILITY_ANALYSIS_TOOLS,
checkAndCompress,
createCompressionLlmAdapter,
createDataSnapshotFromOverview,
formatAIError,
getAllowedBuiltinToolsForChartAutoSkill,
getChartCapabilitySkill,
@@ -214,19 +215,7 @@ export function createElectronRunAgentStream(): (
let dataSnapshot: ToolContext['dataSnapshot'] | undefined
try {
const overview = await workerManager.getChatOverview(sessionId, 5)
if (overview) {
dataSnapshot = {
name: overview.name,
platform: overview.platform,
type: overview.type,
totalMessages: overview.totalMessages,
totalMembers: overview.totalMembers,
firstMessageTs: overview.firstMessageTs,
lastMessageTs: overview.lastMessageTs,
capturedAt: Math.floor(Date.now() / 1000),
}
}
dataSnapshot = createDataSnapshotFromOverview(await workerManager.getChatOverview(sessionId, 10))
} catch (error) {
aiLogger.warn('AgentStream', `Failed to load data snapshot: ${requestId}`, { error: String(error) })
}
@@ -160,6 +160,7 @@ export function getChatOverview(sessionId: string, topN: number = 10) {
const cacheDir = getCacheDir()
const overview = resolveOverview(db, sessionId, cacheDir)
const summaryCount = getSummaryCount(coreDb)
let membersCache = getCache<MembersCache>(sessionId, CACHE_KEY_MEMBERS, cacheDir)
if (!membersCache) {
@@ -187,5 +188,6 @@ export function getChatOverview(sessionId: string, topN: number = 10) {
firstMessageTs: overview.firstMessageTs,
lastMessageTs: overview.lastMessageTs,
topMembers,
summaryCount,
}
}
@@ -432,6 +432,7 @@ export async function getChatOverview(
firstMessageTs: number | null
lastMessageTs: number | null
topMembers: Array<{ id: number; name: string; count: number }>
summaryCount: number
} | null> {
return sendToWorker('getChatOverview', { sessionId, topN })
}
@@ -10,7 +10,13 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import type { SessionMeta, SessionOverview } from '../session-queries'
import { buildSessionInfo, getSessionInfo, getSummaryCount, getLastPlatformMessageId } from '../session-queries'
import {
buildSessionInfo,
getChatOverview,
getSessionInfo,
getSummaryCount,
getLastPlatformMessageId,
} from '../session-queries'
import type { DatabaseAdapter } from '../../interfaces'
// ==================== Mock helpers ====================
@@ -141,6 +147,38 @@ describe('getSessionInfo', () => {
})
})
describe('getChatOverview', () => {
it('returns summaryCount together with overview and top members', () => {
const db = createMockDb({
'FROM meta': () => ({
name: 'Chat',
platform: 'wechat',
type: 'group',
imported_at: 1700000000,
group_id: null,
group_avatar: null,
owner_id: null,
}),
'FROM message msg': () => ({ count: 42 }),
'FROM member\n WHERE': () => ({ count: 2 }),
'MIN(ts)': () => ({ v: 1600000000 }),
'MAX(ts)': () => ({ v: 1700000000 }),
sqlite_master: () => ({ cnt: 1 }),
'FROM segment': () => ({ count: 5 }),
'FROM member m': () => [
{ memberId: 1, platformId: 'u1', name: 'Alice', avatar: null, messageCount: 30 },
{ memberId: 2, platformId: 'u2', name: 'Bob', avatar: null, messageCount: 12 },
],
})
const overview = getChatOverview(db, 1)
assert.ok(overview)
assert.equal(overview.summaryCount, 5)
assert.deepEqual(overview.topMembers, [{ id: 1, name: 'Alice', count: 30 }])
})
})
describe('getSummaryCount', () => {
it('returns 0 when segment table does not exist', () => {
const db = createMockDb({
@@ -194,6 +194,7 @@ export interface ChatOverviewData {
firstMessageTs: number | null
lastMessageTs: number | null
topMembers: Array<{ id: number; name: string; count: number }>
summaryCount: number
}
export interface SessionPreviewMessage {
@@ -252,6 +253,7 @@ export function getChatOverview(db: DatabaseAdapter, topN: number = 10): ChatOve
const overview = getSessionOverview(db)
const members = getMemberActivity(db)
const summaryCount = getSummaryCount(db)
const topMembers = members.slice(0, topN).map((m) => ({
id: m.memberId,
@@ -268,6 +270,7 @@ export function getChatOverview(db: DatabaseAdapter, topN: number = 10): ChatOve
firstMessageTs: overview.firstMessageTs,
lastMessageTs: overview.lastMessageTs,
topMembers,
summaryCount,
}
}
@@ -0,0 +1,100 @@
/**
* Tests for DataSnapshot assembly.
*
* Run: npx tsx --test packages/node-runtime/src/ai/agent/__tests__/data-snapshot.test.ts
*/
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { createDataSnapshotFromOverview } from '../data-snapshot'
describe('createDataSnapshotFromOverview', () => {
it('maps overview members and summary count into an extended data snapshot', () => {
const snapshot = createDataSnapshotFromOverview({
name: 'Team Chat',
platform: 'wechat',
type: 'group',
totalMessages: 200,
totalMembers: 3,
firstMessageTs: 1735689600,
lastMessageTs: 1767225599,
summaryCount: 7,
topMembers: [
{ id: 1, name: 'Alice', count: 120 },
{ id: 2, name: 'Bob', count: 80 },
],
})
assert.deepEqual(snapshot, {
version: 2,
name: 'Team Chat',
platform: 'wechat',
type: 'group',
totalMessages: 200,
totalMembers: 3,
firstMessageTs: 1735689600,
lastMessageTs: 1767225599,
activeMemberHints: [
{ memberId: 1, displayName: 'Alice', messageCount: 120, share: 60 },
{ memberId: 2, displayName: 'Bob', messageCount: 80, share: 40 },
],
segmentSummaries: { availableCount: 7 },
})
})
it('keeps all supplied members for small groups and caps large groups at ten hints', () => {
const topMembers = Array.from({ length: 12 }, (_, index) => ({
id: index + 1,
name: `Member ${index + 1}`,
count: 10,
}))
const smallGroup = createDataSnapshotFromOverview({
name: 'Small',
platform: 'wechat',
type: 'group',
totalMessages: 120,
totalMembers: 10,
firstMessageTs: null,
lastMessageTs: null,
topMembers: topMembers.slice(0, 10),
})
const largeGroup = createDataSnapshotFromOverview({
name: 'Large',
platform: 'wechat',
type: 'group',
totalMessages: 120,
totalMembers: 12,
firstMessageTs: null,
lastMessageTs: null,
topMembers,
})
assert.ok(smallGroup?.activeMemberHints)
assert.ok(largeGroup?.activeMemberHints)
assert.equal(smallGroup.activeMemberHints.length, 10)
assert.equal(largeGroup.activeMemberHints.length, 10)
assert.equal(largeGroup.activeMemberHints.at(-1)?.memberId, 10)
})
it('returns a usable snapshot without member hints when overview has no topMembers', () => {
const snapshot = createDataSnapshotFromOverview({
name: 'No Cache',
platform: 'wechat',
type: 'group',
totalMessages: 0,
totalMembers: 0,
firstMessageTs: null,
lastMessageTs: null,
})
assert.ok(snapshot?.segmentSummaries)
assert.equal(snapshot.segmentSummaries.availableCount, 0)
assert.deepEqual(snapshot?.activeMemberHints, [])
})
it('returns undefined for missing overview', () => {
assert.equal(createDataSnapshotFromOverview(null), undefined)
assert.equal(createDataSnapshotFromOverview(undefined), undefined)
})
})
@@ -0,0 +1,46 @@
import type { DataSnapshot } from './prompt-builder'
export interface ChatOverviewForSnapshot {
name: string
platform: string
type: string
totalMessages: number
totalMembers: number
firstMessageTs: number | null
lastMessageTs: number | null
topMembers?: Array<{ id: number; name: string; count: number }>
summaryCount?: number
}
function roundShare(value: number): number {
if (!Number.isFinite(value)) return 0
return Math.round(value * 10) / 10
}
export function createDataSnapshotFromOverview(
overview: ChatOverviewForSnapshot | null | undefined
): DataSnapshot | undefined {
if (!overview) return undefined
const topMembers = overview.topMembers ?? []
return {
version: 2,
name: overview.name,
platform: overview.platform,
type: overview.type,
totalMessages: overview.totalMessages,
totalMembers: overview.totalMembers,
firstMessageTs: overview.firstMessageTs,
lastMessageTs: overview.lastMessageTs,
activeMemberHints: topMembers.slice(0, 10).map((member) => ({
memberId: member.id,
displayName: member.name,
messageCount: member.count,
share: overview.totalMessages > 0 ? roundShare((member.count / overview.totalMessages) * 100) : 0,
})),
segmentSummaries: {
availableCount: overview.summaryCount ?? 0,
},
}
}
@@ -4,6 +4,8 @@ export { DEFAULT_MAX_TOOL_ROUNDS } from './constants'
export { createLlmRouteDecider, decideRequestRoute } from './router'
export type { LlmRouteDecider, RequestRoute, RouteDecision, RouteDecisionSource, RouterInput } from './routing-types'
export { buildPlanGuidance, createAnalysisPlanner, createPlanContentBlock } from './planner'
export { createDataSnapshotFromOverview } from './data-snapshot'
export type { ChatOverviewForSnapshot } from './data-snapshot'
export type {
AnalysisPlanIntent,
AnalysisPlanner,
+2
View File
@@ -102,6 +102,7 @@ export type { AgentCoreOptions, AgentCoreEvent, AgentCoreResult, AgentTokenUsage
export { DEFAULT_MAX_TOOL_ROUNDS, createLlmRouteDecider, decideRequestRoute, runAgentCore } from './agent'
export type { LlmRouteDecider, RequestRoute, RouteDecision, RouteDecisionSource, RouterInput } from './agent'
export { buildPlanGuidance, createAnalysisPlanner, createPlanContentBlock } from './agent'
export { createDataSnapshotFromOverview } from './agent'
export type {
AnalysisPlanIntent,
AnalysisPlanner,
@@ -111,6 +112,7 @@ export type {
PlannerInput,
PlanContentBlock,
PlanDraftContentBlock,
ChatOverviewForSnapshot,
} from './agent'
// Agent Event Handler
+2 -1
View File
@@ -207,7 +207,7 @@ export type { AIChat, AIMessage, AIMessageRole, ContentBlock, TokenUsageData, AI
export type { AgentCoreOptions, AgentCoreEvent, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './ai'
export { DEFAULT_MAX_TOOL_ROUNDS, createLlmRouteDecider, decideRequestRoute, runAgentCore } from './ai'
export type { LlmRouteDecider, RequestRoute, RouteDecision, RouteDecisionSource, RouterInput } from './ai'
export { buildPlanGuidance, createAnalysisPlanner, createPlanContentBlock } from './ai'
export { buildPlanGuidance, createAnalysisPlanner, createDataSnapshotFromOverview, createPlanContentBlock } from './ai'
export type {
AnalysisPlanIntent,
AnalysisPlanner,
@@ -217,6 +217,7 @@ export type {
PlannerInput,
PlanContentBlock,
PlanDraftContentBlock,
ChatOverviewForSnapshot,
} from './ai'
// Summary generation