mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(ai): build extended data snapshots
This commit is contained in:
1 parent
383af0b13a
commit
718735a748
11 files changed
+201
-28
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user