mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 12:07:01 +08:00
test: 暂存代码
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import type { AiSearchAgentToolName, AiSearchAgentTraceItem } from '../../shared/ai-search'
|
||||
|
||||
export const MAX_AGENT_TOOL_CALLS = 5
|
||||
|
||||
export type AgentAction =
|
||||
| { action: 'tool'; tool: AiSearchAgentToolName; arguments: Record<string, unknown> }
|
||||
| { action: 'finalize'; reason: string }
|
||||
|
||||
export interface AgentToolResult {
|
||||
summary: Record<string, unknown>
|
||||
candidateCount: number
|
||||
/** A host-owned coverage signal, never supplied by the model. */
|
||||
finalizeReason?: string
|
||||
}
|
||||
|
||||
export interface ControlledSearchAgentOptions {
|
||||
question: string
|
||||
scopeLabel: string
|
||||
rangeLabel: string
|
||||
maxToolCalls?: number
|
||||
decide: (prompt: string) => Promise<string | undefined>
|
||||
execute: (action: Extract<AgentAction, { action: 'tool' }>) => Promise<AgentToolResult>
|
||||
onTrace: (item: Omit<AiSearchAgentTraceItem, 'sequence'>) => void
|
||||
}
|
||||
|
||||
export interface ControlledSearchAgentResult {
|
||||
status: 'finalized' | 'exhausted' | 'invalid'
|
||||
toolCalls: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
const TOOL_NAMES = new Set<AiSearchAgentToolName>([
|
||||
'search_conversations',
|
||||
'search_people',
|
||||
'search_messages',
|
||||
'get_conversation_messages',
|
||||
'get_messages_by_time',
|
||||
'get_message_context'
|
||||
])
|
||||
|
||||
const parseAction = (value: string | undefined): AgentAction | null => {
|
||||
if (!value) return null
|
||||
const match = value.match(/\{[\s\S]*\}/)
|
||||
if (!match) return null
|
||||
try {
|
||||
const parsed = JSON.parse(match[0]) as Record<string, unknown>
|
||||
if (parsed.action === 'finalize' && typeof parsed.reason === 'string' && parsed.reason.trim()) {
|
||||
return { action: 'finalize', reason: parsed.reason.trim().slice(0, 240) }
|
||||
}
|
||||
if (
|
||||
parsed.action === 'tool' &&
|
||||
typeof parsed.tool === 'string' &&
|
||||
TOOL_NAMES.has(parsed.tool as AiSearchAgentToolName) &&
|
||||
parsed.arguments &&
|
||||
typeof parsed.arguments === 'object' &&
|
||||
!Array.isArray(parsed.arguments)
|
||||
) {
|
||||
return {
|
||||
action: 'tool',
|
||||
tool: parsed.tool as AiSearchAgentToolName,
|
||||
arguments: parsed.arguments as Record<string, unknown>
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Invalid model output is rejected by the caller and triggers legacy fallback.
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const agentSystemPrompt = (
|
||||
question: string,
|
||||
scopeLabel: string,
|
||||
rangeLabel: string
|
||||
): string => `你是 WechatExplorer 的受控本地聊天搜索代理,只负责决定下一步检索,不回答用户问题。
|
||||
用户问题:${question}
|
||||
允许范围:${scopeLabel};时间范围:${rangeLabel}。
|
||||
|
||||
你只能输出一个 JSON 对象,不能输出 Markdown、解释、代码、SQL、文件路径或任何系统操作。
|
||||
唯一合法格式:
|
||||
{"action":"tool","tool":"search_people|search_conversations|search_messages|get_conversation_messages|get_messages_by_time|get_message_context","arguments":{...}}
|
||||
或:
|
||||
{"action":"finalize","reason":"已有足够证据"}
|
||||
|
||||
规则:
|
||||
- 只能使用此前 Tool 返回的 conversationRef/messageRef;不得猜测或创建引用。
|
||||
- 问“我和某人最近聊了什么”时,优先 search_people 或 search_conversations,再 get_conversation_messages;不要把联系人名当消息关键词。
|
||||
- 搜索会话没有结果时,可根据结果自行尝试更短或更自然的名称表达,但最多五次 Tool 调用。
|
||||
- Tool 结果不足时可以改 Tool 或查询策略;结果充分时 finalize。
|
||||
- 不要请求全部聊天记录;遵守 Tool 返回的受限结果。`
|
||||
|
||||
const traceArguments = (
|
||||
argumentsValue: Record<string, unknown>
|
||||
): Record<string, string | number | boolean> => {
|
||||
const result: Record<string, string | number | boolean> = {}
|
||||
if (typeof argumentsValue.query === 'string') result.query = argumentsValue.query.slice(0, 80)
|
||||
if (typeof argumentsValue.limit === 'number') result.limit = argumentsValue.limit
|
||||
if (typeof argumentsValue.startTime === 'number') result.startTime = argumentsValue.startTime
|
||||
if (typeof argumentsValue.endTime === 'number') result.endTime = argumentsValue.endTime
|
||||
if (typeof argumentsValue.conversationRef === 'string') result.target = '已选择会话'
|
||||
if (typeof argumentsValue.messageRef === 'string') result.context = '已选择消息'
|
||||
return result
|
||||
}
|
||||
|
||||
export async function runControlledSearchAgent(
|
||||
options: ControlledSearchAgentOptions
|
||||
): Promise<ControlledSearchAgentResult> {
|
||||
let toolCalls = 0
|
||||
let previousResult = '尚未执行 Tool。'
|
||||
options.onTrace({ event: 'agentStart', label: '开始规划本次本地检索' })
|
||||
|
||||
const maxToolCalls = options.maxToolCalls || MAX_AGENT_TOOL_CALLS
|
||||
while (toolCalls < maxToolCalls) {
|
||||
const decisionStartedAt = Date.now()
|
||||
const decisionInput = `${agentSystemPrompt(options.question, options.scopeLabel, options.rangeLabel)}\n\n上一次 Tool 结果:${previousResult}`
|
||||
const output = await options.decide(decisionInput)
|
||||
const decisionElapsedMs = Date.now() - decisionStartedAt
|
||||
const action = parseAction(output)
|
||||
if (!action) return { status: 'invalid', toolCalls, reason: 'Agent 返回的控制协议无效' }
|
||||
if (action.action === 'finalize') {
|
||||
options.onTrace({
|
||||
event: 'agentDecision',
|
||||
label: 'Agent 判断现有结果足够',
|
||||
decision: action.reason,
|
||||
decisionInput: decisionInput.slice(0, 8_000),
|
||||
elapsedMs: decisionElapsedMs
|
||||
})
|
||||
return { status: 'finalized', toolCalls, reason: action.reason }
|
||||
}
|
||||
|
||||
options.onTrace({
|
||||
event: 'agentDecision',
|
||||
label: 'Agent 选择下一次检索',
|
||||
toolName: action.tool,
|
||||
elapsedMs: decisionElapsedMs,
|
||||
decisionInput: decisionInput.slice(0, 8_000)
|
||||
})
|
||||
toolCalls += 1
|
||||
options.onTrace({
|
||||
event: 'toolCallStart',
|
||||
label: '正在执行本地检索',
|
||||
toolName: action.tool,
|
||||
arguments: traceArguments(action.arguments)
|
||||
})
|
||||
const toolStartedAt = Date.now()
|
||||
try {
|
||||
const result = await options.execute(action)
|
||||
const elapsedMs = Date.now() - toolStartedAt
|
||||
options.onTrace({
|
||||
event: 'toolCallEnd',
|
||||
label: '本地检索完成',
|
||||
toolName: action.tool,
|
||||
resultCount: result.candidateCount,
|
||||
elapsedMs
|
||||
})
|
||||
previousResult = JSON.stringify(result.summary)
|
||||
if (result.finalizeReason) {
|
||||
options.onTrace({
|
||||
event: 'agentDecision',
|
||||
label: '本地资料已覆盖所选时间范围,可直接整理回答',
|
||||
decision: result.finalizeReason,
|
||||
elapsedMs: 0
|
||||
})
|
||||
return { status: 'finalized', toolCalls, reason: result.finalizeReason }
|
||||
}
|
||||
} catch (error) {
|
||||
const elapsedMs = Date.now() - toolStartedAt
|
||||
const message = error instanceof Error ? error.message : '本次本地检索不可用'
|
||||
options.onTrace({
|
||||
event: 'toolCallEnd',
|
||||
label: '本地检索未返回结果',
|
||||
toolName: action.tool,
|
||||
resultCount: 0,
|
||||
elapsedMs,
|
||||
decision: message.slice(0, 160)
|
||||
})
|
||||
previousResult = JSON.stringify({ error: message.slice(0, 160), results: [] })
|
||||
}
|
||||
}
|
||||
options.onTrace({
|
||||
event: 'agentDecision',
|
||||
label: '已达到本次检索上限',
|
||||
decision: `最多允许 ${maxToolCalls} 次本地检索`
|
||||
})
|
||||
return { status: 'exhausted', toolCalls, reason: '已达到本次检索上限' }
|
||||
}
|
||||
Reference in New Issue
Block a user