mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(ai): guide planner with startup context
This commit is contained in:
1 parent
718735a748
commit
6594e586d3
5 files changed
+147
-6
No files matched your search
@@ -10,6 +10,19 @@ const baseInput = {
|
||||
availableTools: ['search_messages', 'get_time_stats', 'get_member_stats'],
|
||||
}
|
||||
|
||||
const extendedDataSnapshot = {
|
||||
version: 2 as const,
|
||||
name: 'Team Chat',
|
||||
platform: 'wechat',
|
||||
type: 'group',
|
||||
totalMessages: 1000,
|
||||
totalMembers: 12,
|
||||
firstMessageTs: 1735689600,
|
||||
lastMessageTs: 1767225599,
|
||||
activeMemberHints: [{ memberId: 3, displayName: 'Alice', messageCount: 300, share: 30 }],
|
||||
segmentSummaries: { availableCount: 8 },
|
||||
}
|
||||
|
||||
describe('createAnalysisPlanner', () => {
|
||||
it('parses valid JSON and filters suggested tools to the available set', async () => {
|
||||
const planner = createAnalysisPlanner({
|
||||
@@ -218,4 +231,35 @@ ${json}
|
||||
assert.match(capturedPrompt, /A concise user-facing analysis approach/)
|
||||
assert.match(capturedPrompt, /those details belong in <json>/)
|
||||
})
|
||||
|
||||
it('includes extended data snapshot context and reconnaissance strategy in the planner prompt', async () => {
|
||||
let capturedPrompt = ''
|
||||
const planner = createAnalysisPlanner({
|
||||
complete: async (prompt) => {
|
||||
capturedPrompt = prompt
|
||||
return JSON.stringify({
|
||||
title: '年度话题趋势分析',
|
||||
intent: 'trend',
|
||||
steps: [
|
||||
{
|
||||
goal: '先建立话题地图和发言规律',
|
||||
suggestedTools: ['search_messages', 'get_time_stats'],
|
||||
evidenceNeeded: '摘要、时间分布、成员活跃和代表消息',
|
||||
},
|
||||
],
|
||||
successCriteria: ['覆盖真实最近一年'],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
await planner({ ...baseInput, dataSnapshot: extendedDataSnapshot })
|
||||
|
||||
assert.match(capturedPrompt, /first_message_ts: 1735689600/)
|
||||
assert.match(capturedPrompt, /last_message_ts: 1767225599/)
|
||||
assert.match(capturedPrompt, /segment_summaries_available: 8/)
|
||||
assert.match(capturedPrompt, /member_id=3/)
|
||||
assert.match(capturedPrompt, /display_name=Alice/)
|
||||
assert.match(capturedPrompt, /lightweight reconnaissance step/i)
|
||||
assert.match(capturedPrompt, /topic\/member activity map/i)
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,19 @@ const baseInput = {
|
||||
availableTools: ['get_chat_overview', 'search_messages', 'get_member_stats'],
|
||||
}
|
||||
|
||||
const extendedDataSnapshot = {
|
||||
version: 2 as const,
|
||||
name: 'Team Chat',
|
||||
platform: 'wechat',
|
||||
type: 'group',
|
||||
totalMessages: 1000,
|
||||
totalMembers: 12,
|
||||
firstMessageTs: 1735689600,
|
||||
lastMessageTs: 1767225599,
|
||||
activeMemberHints: [{ memberId: 3, displayName: 'Alice', messageCount: 300, share: 30 }],
|
||||
segmentSummaries: { availableCount: 8 },
|
||||
}
|
||||
|
||||
describe('decideRequestRoute', () => {
|
||||
it('routes concept and help questions to direct response by rule', async () => {
|
||||
const concept = await decideRequestRoute({
|
||||
@@ -111,6 +124,37 @@ describe('decideRequestRoute', () => {
|
||||
assert.match(decision.reason, /No local data/)
|
||||
})
|
||||
|
||||
it('includes compact extended data snapshot context in the LLM fallback prompt', async () => {
|
||||
let capturedPrompt = ''
|
||||
const decider = createLlmRouteDecider({
|
||||
complete: async (prompt) => {
|
||||
capturedPrompt = prompt
|
||||
return '{"route":"planned_execution","confidence":0.8,"reason":"Needs trend analysis."}'
|
||||
},
|
||||
})
|
||||
|
||||
await decider(
|
||||
{
|
||||
...baseInput,
|
||||
userMessage: '帮我看一下这个情况。',
|
||||
dataSnapshot: extendedDataSnapshot,
|
||||
},
|
||||
{
|
||||
route: 'tool_assisted',
|
||||
confidence: 0.45,
|
||||
reason: 'Ambiguous request.',
|
||||
source: 'rule',
|
||||
}
|
||||
)
|
||||
|
||||
assert.match(capturedPrompt, /total_messages: 1000/)
|
||||
assert.match(capturedPrompt, /total_members: 12/)
|
||||
assert.match(capturedPrompt, /first_message_ts: 1735689600/)
|
||||
assert.match(capturedPrompt, /last_message_ts: 1767225599/)
|
||||
assert.match(capturedPrompt, /segment_summaries_available: 8/)
|
||||
assert.match(capturedPrompt, /active_member_hint_count: 1/)
|
||||
})
|
||||
|
||||
it('keeps the rule decision when LLM fallback output is invalid', async () => {
|
||||
const decider = createLlmRouteDecider({
|
||||
complete: async () => '{"route":"unknown","confidence":0.9,"reason":"bad"}',
|
||||
|
||||
@@ -17,6 +17,16 @@ function roundShare(value: number): number {
|
||||
return Math.round(value * 10) / 10
|
||||
}
|
||||
|
||||
function formatNullableTimestamp(timestamp: number | null | undefined): string {
|
||||
return typeof timestamp === 'number' && Number.isFinite(timestamp) ? String(timestamp) : 'null'
|
||||
}
|
||||
|
||||
function formatSharePercent(share: number): string {
|
||||
if (!Number.isFinite(share)) return '0%'
|
||||
const rounded = Math.round(share * 10) / 10
|
||||
return Number.isInteger(rounded) ? `${rounded}%` : `${rounded.toFixed(1)}%`
|
||||
}
|
||||
|
||||
export function createDataSnapshotFromOverview(
|
||||
overview: ChatOverviewForSnapshot | null | undefined
|
||||
): DataSnapshot | undefined {
|
||||
@@ -44,3 +54,43 @@ export function createDataSnapshotFromOverview(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDataSnapshotForRouter(dataSnapshot: DataSnapshot | undefined): string {
|
||||
if (!dataSnapshot) return '(none)'
|
||||
|
||||
return [
|
||||
`name: ${dataSnapshot.name}`,
|
||||
`platform: ${dataSnapshot.platform}`,
|
||||
`type: ${dataSnapshot.type}`,
|
||||
`total_messages: ${dataSnapshot.totalMessages}`,
|
||||
`total_members: ${dataSnapshot.totalMembers}`,
|
||||
`first_message_ts: ${formatNullableTimestamp(dataSnapshot.firstMessageTs)}`,
|
||||
`last_message_ts: ${formatNullableTimestamp(dataSnapshot.lastMessageTs)}`,
|
||||
`segment_summaries_available: ${dataSnapshot.segmentSummaries?.availableCount ?? 0}`,
|
||||
`active_member_hint_count: ${dataSnapshot.activeMemberHints?.length ?? 0}`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function formatDataSnapshotForPlanner(dataSnapshot: DataSnapshot | undefined): string {
|
||||
if (!dataSnapshot) return '(none)'
|
||||
|
||||
const memberHints = dataSnapshot.activeMemberHints ?? []
|
||||
const memberHintLines =
|
||||
memberHints.length > 0
|
||||
? memberHints
|
||||
.map(
|
||||
(member, index) =>
|
||||
`${index + 1}. member_id=${member.memberId} | display_name=${member.displayName} | messages=${member.messageCount} | share=${formatSharePercent(member.share)}`
|
||||
)
|
||||
.join('\n')
|
||||
: '(none)'
|
||||
|
||||
return `${formatDataSnapshotForRouter(dataSnapshot)}
|
||||
active_member_hints:
|
||||
${memberHintLines}
|
||||
notes:
|
||||
- member_id is a tool lookup hint; display_name may not be unique.
|
||||
- Active member hints reflect historical total message volume, not recent activity.
|
||||
- Use real current date for relative ranges; database bounds only describe data coverage.
|
||||
- Do not plan a tool call only to rediscover min/max timestamp.`
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
PlanContentBlock,
|
||||
PlannerInput,
|
||||
} from './planning-types'
|
||||
import { formatDataSnapshotForPlanner } from './data-snapshot'
|
||||
|
||||
export type PlannerCompletionResult = string | { text: string }
|
||||
|
||||
@@ -121,9 +122,7 @@ function resultToText(result: PlannerCompletionResult): string {
|
||||
}
|
||||
|
||||
function buildPlannerPrompt(input: PlannerInput): string {
|
||||
const dataSnapshot = input.dataSnapshot
|
||||
? `${input.dataSnapshot.name}, ${input.dataSnapshot.totalMessages} messages, ${input.dataSnapshot.totalMembers} members`
|
||||
: '(none)'
|
||||
const dataSnapshot = formatDataSnapshotForPlanner(input.dataSnapshot)
|
||||
const availableCapabilities =
|
||||
input.availableCapabilities && input.availableCapabilities.length > 0
|
||||
? input.availableCapabilities
|
||||
@@ -162,6 +161,11 @@ Limits:
|
||||
- Do not reveal hidden chain-of-thought; draft and plan are user-readable summaries only.
|
||||
- The plan is soft guidance, not a mandatory execution script.
|
||||
|
||||
Planning strategy:
|
||||
- For open-ended topic, community profile, influence, interaction-pattern, or multi-dimensional retrospective analysis, first plan a lightweight reconnaissance step to build a topic/member activity map before drawing conclusions.
|
||||
- Prefer segment summaries, keyword frequency, time distribution, member activity, and representative message retrieval when available.
|
||||
- Do not create a dedicated step whose only purpose is finding max timestamp or confirming latest message time.
|
||||
|
||||
Context:
|
||||
- locale: ${input.locale}
|
||||
- chatType: ${input.chatType}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LlmRouteDecider, RouteDecision, RouterInput } from './routing-types'
|
||||
import { formatDataSnapshotForRouter } from './data-snapshot'
|
||||
import {
|
||||
completeSimple,
|
||||
type Api as PiApi,
|
||||
@@ -77,9 +78,7 @@ function parseLlmDecision(rawText: string, usage?: RouteDecision['usage']): Rout
|
||||
|
||||
function buildLlmRouterPrompt(input: RouterInput, ruleDecision: RouteDecision): string {
|
||||
const toolNames = input.availableTools?.length ? input.availableTools.join(', ') : '(none)'
|
||||
const dataSnapshot = input.dataSnapshot
|
||||
? `${input.dataSnapshot.name}, ${input.dataSnapshot.totalMessages} messages, ${input.dataSnapshot.totalMembers} members`
|
||||
: '(none)'
|
||||
const dataSnapshot = formatDataSnapshotForRouter(input.dataSnapshot)
|
||||
|
||||
return `Classify the user's ChatLab request into exactly one route.
|
||||
|
||||
|
||||
Reference in new issue
Block a user