mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(ai): stream structured analysis plans
This commit is contained in:
1 parent
9ad012680d
commit
0801ccf940
21 files changed
+669
-36
No files matched your search
@@ -204,7 +204,17 @@ export async function runServerAgent(options: RunAgentOptions): Promise<void> {
|
||||
let effectiveSystemPrompt = systemPrompt
|
||||
if (routeDecision.route === 'planned_execution') {
|
||||
const planStartedAt = Date.now()
|
||||
const planner = createAnalysisPlanner({ piModel, apiKey: llmConfig.apiKey })
|
||||
const planner = createAnalysisPlanner({
|
||||
piModel,
|
||||
apiKey: llmConfig.apiKey,
|
||||
onPlanDelta: (delta) => onEvent({ type: 'plan_delta', planDelta: delta }),
|
||||
onThinkingDelta: (delta) => onEvent({ type: 'think', content: delta, thinkTag: 'thinking' }),
|
||||
onThinkingEnd: (durationMs) =>
|
||||
onEvent({ type: 'think', content: '', thinkTag: 'thinking', thinkDurationMs: durationMs }),
|
||||
onValidationDelta: (delta) => onEvent({ type: 'think', content: delta, thinkTag: 'plan_validation' }),
|
||||
onValidationEnd: (durationMs) =>
|
||||
onEvent({ type: 'think', content: '', thinkTag: 'plan_validation', thinkDurationMs: durationMs }),
|
||||
})
|
||||
const plan = await planner(routeInput, abortSignal)
|
||||
if (plan) {
|
||||
const planBlock = createPlanContentBlock(plan)
|
||||
@@ -221,6 +231,7 @@ export async function runServerAgent(options: RunAgentOptions): Promise<void> {
|
||||
elapsedMs: Date.now() - planStartedAt,
|
||||
route: routeDecision.route,
|
||||
})
|
||||
onEvent({ type: 'plan_skipped' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,8 @@ describe('runChatTurn', () => {
|
||||
source: 'rule',
|
||||
},
|
||||
})
|
||||
onEvent({ type: 'plan_delta', planDelta: '年度话题趋势\n' })
|
||||
onEvent({ type: 'plan_delta', planDelta: '1. 按季度检索\n' })
|
||||
onEvent({
|
||||
type: 'plan',
|
||||
plan: {
|
||||
@@ -227,12 +229,18 @@ describe('runChatTurn', () => {
|
||||
)
|
||||
|
||||
assert.equal(result.answer, '年度趋势如下。')
|
||||
assert.equal(result.events?.length, 4)
|
||||
assert.equal(result.events?.length, 6)
|
||||
assert.equal(result.events?.[0]?.type, 'route')
|
||||
assert.equal(result.events?.[0]?.routeDecision?.route, 'planned_execution')
|
||||
assert.equal(result.events?.[1]?.type, 'plan')
|
||||
assert.equal(result.contentBlocks?.[0]?.type, 'plan')
|
||||
assert.equal(result.contentBlocks?.[0]?.status, 'done')
|
||||
assert.equal(result.events?.[1]?.type, 'plan_delta')
|
||||
assert.equal(result.events?.[2]?.type, 'plan_delta')
|
||||
assert.equal(result.events?.[3]?.type, 'plan')
|
||||
const firstBlock = result.contentBlocks?.[0]
|
||||
assert.equal(firstBlock?.type, 'plan')
|
||||
if (firstBlock?.type === 'plan') {
|
||||
assert.equal(firstBlock.status, 'done')
|
||||
assert.equal(firstBlock.displayText, '年度话题趋势\n1. 按季度检索')
|
||||
}
|
||||
assert.deepEqual((aiChatManager as unknown as { __messages: unknown[] }).__messages, [
|
||||
{ aiChatId: 'ai_chat_1', role: 'user', content: '分析过去一年话题趋势' },
|
||||
{
|
||||
@@ -245,6 +253,34 @@ describe('runChatTurn', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('drops streamed plan drafts when planner validation is skipped', async () => {
|
||||
const stdout = new MemoryWritable()
|
||||
const aiChatManager = createAIChatManager()
|
||||
|
||||
const result = await runChatTurn(
|
||||
{ sessionId: 'session-1', question: '分析过去一年话题趋势', json: true, includeEvents: true },
|
||||
{
|
||||
dbManager: createDbManager(['session-1']),
|
||||
pathProvider: {} as never,
|
||||
aiChatManager,
|
||||
stdout,
|
||||
createRunAgentStream: () => async (_params, onEvent) => {
|
||||
onEvent({ type: 'plan_delta', planDelta: '无法校验的计划草稿\n' })
|
||||
onEvent({ type: 'plan_skipped' })
|
||||
onEvent({ type: 'content', content: '直接给出分析。' })
|
||||
onEvent({ type: 'done', isFinished: true })
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert.equal(result.events?.[0]?.type, 'plan_delta')
|
||||
assert.equal(result.events?.[1]?.type, 'plan_skipped')
|
||||
assert.deepEqual(
|
||||
result.contentBlocks?.map((block) => block.type),
|
||||
['text']
|
||||
)
|
||||
})
|
||||
|
||||
it('persists streamed thinking and answer text blocks for full CLI replay', async () => {
|
||||
const stdout = new MemoryWritable()
|
||||
const aiChatManager = createAIChatManager()
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
AgentStreamChunk,
|
||||
ContentBlock,
|
||||
PlanContentBlock,
|
||||
PlanDraftContentBlock,
|
||||
TokenUsageData,
|
||||
} from '@openchatlab/node-runtime'
|
||||
import type { ChartPayload, PathProvider } from '@openchatlab/core'
|
||||
@@ -211,6 +212,41 @@ export async function runChatTurn(
|
||||
hasReplayContentBlocks = true
|
||||
}
|
||||
|
||||
const appendPlanDraftBlock = (delta: string) => {
|
||||
if (!delta) return
|
||||
const lastBlock = contentBlocks[contentBlocks.length - 1]
|
||||
if (lastBlock?.type === 'plan_draft') {
|
||||
lastBlock.text += delta
|
||||
} else {
|
||||
contentBlocks.push({ type: 'plan_draft', version: 1, status: 'streaming', text: delta } as PlanDraftContentBlock)
|
||||
}
|
||||
hasReplayContentBlocks = true
|
||||
}
|
||||
|
||||
const removePlanDraftBlocks = () => {
|
||||
for (let index = contentBlocks.length - 1; index >= 0; index--) {
|
||||
if (contentBlocks[index]?.type === 'plan_draft') {
|
||||
contentBlocks.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const appendFinalPlanBlock = (plan: PlanContentBlock) => {
|
||||
const planBlock = JSON.parse(JSON.stringify(plan)) as PlanContentBlock
|
||||
for (let index = contentBlocks.length - 1; index >= 0; index--) {
|
||||
const block = contentBlocks[index]
|
||||
if (block?.type === 'plan_draft') {
|
||||
const displayText = block.text.trim()
|
||||
if (displayText) planBlock.displayText = displayText
|
||||
contentBlocks[index] = planBlock
|
||||
hasReplayContentBlocks = true
|
||||
return
|
||||
}
|
||||
}
|
||||
contentBlocks.push(planBlock)
|
||||
hasReplayContentBlocks = true
|
||||
}
|
||||
|
||||
await runAgentStream(
|
||||
{
|
||||
userMessage: options.question,
|
||||
@@ -226,12 +262,20 @@ export async function runChatTurn(
|
||||
events.push(JSON.parse(JSON.stringify(chunk)) as AgentStreamChunk)
|
||||
}
|
||||
if (chunk.type === 'error') {
|
||||
removePlanDraftBlocks()
|
||||
streamError = createAgentStreamError(chunk.error)
|
||||
return
|
||||
}
|
||||
if (chunk.type === 'plan_delta' && chunk.planDelta) {
|
||||
appendPlanDraftBlock(chunk.planDelta)
|
||||
return
|
||||
}
|
||||
if (chunk.type === 'plan' && chunk.plan) {
|
||||
hasReplayContentBlocks = true
|
||||
contentBlocks.push(JSON.parse(JSON.stringify(chunk.plan)) as PlanContentBlock)
|
||||
appendFinalPlanBlock(chunk.plan)
|
||||
return
|
||||
}
|
||||
if (chunk.type === 'plan_skipped') {
|
||||
removePlanDraftBlocks()
|
||||
return
|
||||
}
|
||||
if (chunk.type === 'think') {
|
||||
|
||||
Reference in new issue
Block a user