Files
memind/rain-service/llm-analysis.mjs
T
john be464a5b8d
Memind CI / Test, build, and release guards (push) Has been cancelled
Add Rain V0 for MeInput full-range chat analysis and delivery tooling.
Introduce rain-service orchestration, browser-safe chat skill filtering, MeInput
adapter helpers, and verify/deploy scripts so Rain mode can summarize recent input
without Memory V2 pollution.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 13:49:40 +08:00

119 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function stripRainSkillPrefix(text) {
let next = String(text ?? '').trim();
next = next.replace(/^【Rain[^】]*】\s*/u, '');
next = next.replace(/^请使用\s+rain\s+技能[:]\s*/iu, '');
if (/^请描述要分析的时间区间/u.test(next)) {
const marker = '我的问题是:';
const idx = next.indexOf(marker);
if (idx >= 0) next = next.slice(idx + marker.length);
}
return next.trim();
}
function parseRainLlmJson(raw) {
const text = String(raw ?? '').trim();
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
const candidate = fenced?.[1]?.trim() || text;
try {
return JSON.parse(candidate);
} catch {
return null;
}
}
/**
* @param {{ llmProviderService: object, userQuery: string, meinputBlock: string, timeRangeLabel: string, recordCount: number }} input
*/
export async function runRainLlmAnalysis(input) {
const userQuery = stripRainSkillPrefix(input.userQuery);
const system = [
'你是 TKMind Rain 分析层。只能依据【MeInput 原始输入】块中的内容做归纳,禁止引用或编造长期记忆、聊天历史、日程等外部信息。',
'输出必须是单个 JSON 对象,不要 markdown,不要代码围栏,字段如下:',
'{"needs_clarification":boolean,"clarification_question":string|null,"user_goal":string,"meinput_analysis":string,"suggested_next_steps":string[],"user_reply":string}',
'- needs_clarification=true 时:clarification_question 必填,user_reply 用自然语言向用户追问;meinput_analysis 可为空。',
'- needs_clarification=false 时:meinput_analysis 按时间线归纳用户在各 App 的输入活动;user_reply 是可直接展示给用户的中文回复(含区间说明);suggested_next_steps 供下游 Agent 参考(如生成报告页、继续追问)。',
'- 不要把内部排序分数、source 字段名暴露给用户。',
].join('\n');
const user = [
`时间区间:${input.timeRangeLabel}`,
`记录条数:${input.recordCount}`,
'',
input.meinputBlock,
'',
`用户诉求:${userQuery || '请总结我在上述区间的输入活动'}`,
].join('\n');
const completion = await input.llmProviderService.createChatCompletion({
messages: [
{ role: 'system', content: system },
{ role: 'user', content: user },
],
});
if (!completion?.ok) {
const err = new Error(completion?.message ?? 'Rain LLM 分析失败');
err.code = 'RAIN_LLM_FAILED';
throw err;
}
const parsed = parseRainLlmJson(completion.reply);
if (!parsed || typeof parsed !== 'object') {
return {
needs_clarification: false,
clarification_question: null,
user_goal: userQuery || '回顾 MeInput 输入',
meinput_analysis: String(completion.reply ?? '').trim(),
suggested_next_steps: [],
user_reply: String(completion.reply ?? '').trim(),
raw: completion.reply,
};
}
return {
needs_clarification: Boolean(parsed.needs_clarification),
clarification_question: parsed.clarification_question ?? null,
user_goal: String(parsed.user_goal ?? userQuery ?? '').trim(),
meinput_analysis: String(parsed.meinput_analysis ?? '').trim(),
suggested_next_steps: Array.isArray(parsed.suggested_next_steps)
? parsed.suggested_next_steps.map((s) => String(s).trim()).filter(Boolean)
: [],
user_reply: String(parsed.user_reply ?? '').trim(),
raw: completion.reply,
};
}
export function buildRainGooseHandoffText({
userQuery,
timeRangeLabel,
recordCount,
analysis,
userGoal,
suggestedNextSteps = [],
}) {
const steps =
suggestedNextSteps.length > 0
? suggestedNextSteps.map((s) => `- ${s}`).join('\n')
: '- (无明确工具动作,先给用户文字总结)';
return [
'[Rain · MeInput 分析简报]',
'以下简报由 Rain 分析层基于 MeInput 全量原始输入生成。请据此决定如何回复用户、是否调用工具或 skill;不要重复询问时间区间。',
'',
`时间区间:${timeRangeLabel}`,
`原始记录条数:${recordCount}`,
`用户诉求:${userGoal || stripRainSkillPrefix(userQuery)}`,
'',
'【分析归纳】',
analysis || '(无)',
'',
'【建议下一步】',
steps,
'',
'【用户原始问题】',
stripRainSkillPrefix(userQuery),
].join('\n');
}
export { stripRainSkillPrefix };