diff --git a/electron/main/ai/agent/prompt-builder.ts b/electron/main/ai/agent/prompt-builder.ts index 8b33ede4..bf253480 100644 --- a/electron/main/ai/agent/prompt-builder.ts +++ b/electron/main/ai/agent/prompt-builder.ts @@ -1,107 +1,29 @@ /** - * Agent 系统提示词构建 + * Agent system prompt builder — Electron adapter. + * Injects the Electron i18n `t` function into the shared builder. */ import { t as i18nT } from '../../i18n' -import type { OwnerInfo } from '../tools/types' -import type { SkillContext } from './types' -import type { ToolContext } from '../tools/types' +import { buildSystemPrompt as buildSystemPromptCore } from '@openchatlab/node-runtime' +import type { OwnerInfo, SkillContext, MentionedMember } from '@openchatlab/node-runtime' -function agentT(key: string, locale: string, options?: Record): string { - return i18nT(key, { lng: locale, ...options }) -} +export type { OwnerInfo, SkillContext, MentionedMember } -/** - * 获取系统锁定部分的提示词(策略说明、时间处理等) - * - * 工具定义通过 Function Calling 的 tools 参数传递给 LLM, - * 无需在 System Prompt 中重复描述。 - */ -function getLockedPromptSection( - chatType: 'group' | 'private', - ownerInfo?: OwnerInfo, - locale: string = 'zh-CN', - mentionedMembers?: ToolContext['mentionedMembers'] -): string { - const now = new Date() - const dateLocale = locale.startsWith('zh') ? 'zh-CN' : 'en-US' - const currentDate = now.toLocaleDateString(dateLocale, { - year: 'numeric', - month: 'long', - day: 'numeric', - weekday: 'long', - }) - - const isPrivate = chatType === 'private' - const chatContext = agentT(`ai.agent.chatContext.${chatType}`, locale) - - const ownerNote = ownerInfo - ? agentT('ai.agent.ownerNote', locale, { - displayName: ownerInfo.displayName, - platformId: ownerInfo.platformId, - chatContext, - }) - : '' - - const memberNote = isPrivate - ? agentT('ai.agent.memberNotePrivate', locale) - : agentT('ai.agent.memberNoteGroup', locale) - - const mentionedMembersNote = - mentionedMembers && mentionedMembers.length > 0 - ? `${agentT('ai.agent.mentionedMembersNote', locale)}\n${mentionedMembers - .map((member) => { - const aliasPart = member.aliases.length > 0 ? ` | aliases=${member.aliases.join(',')}` : '' - return `- member_id=${member.memberId} | mention=${member.mentionText} | display_name=${member.displayName} | platform_id=${member.platformId}${aliasPart}` - }) - .join('\n')}\n` - : '' - - const year = now.getFullYear() - - return `${agentT('ai.agent.currentDateIs', locale)} ${currentDate}。 -${ownerNote} -${mentionedMembersNote} -${memberNote} -${agentT('ai.agent.timeParamsIntro', locale)} -${agentT('ai.agent.defaultYearNote', locale, { year })} - -${agentT('ai.agent.responseInstruction', locale)}` -} - -function getFallbackRoleDefinition(chatType: 'group' | 'private', locale: string = 'zh-CN'): string { - return agentT(`ai.agent.fallbackRoleDefinition.${chatType}`, locale) -} - -/** - * 构建完整的系统提示词 - * - * 系统提示词优先使用当前助手配置;若助手不存在,再退回内置默认角色定义。 - * - * 最终格式:{助手系统提示词}\n\n{系统锁定段(日期/owner/时间参数/通用指引)} - */ export function buildSystemPrompt( chatType: 'group' | 'private' = 'group', assistantSystemPrompt?: string, ownerInfo?: OwnerInfo, locale: string = 'zh-CN', skillCtx?: SkillContext, - mentionedMembers?: ToolContext['mentionedMembers'] + mentionedMembers?: MentionedMember[] ): string { - const systemPrompt = assistantSystemPrompt || getFallbackRoleDefinition(chatType, locale) - const lockedSection = getLockedPromptSection(chatType, ownerInfo, locale, mentionedMembers) - - let skillSection = '' - if (skillCtx?.skillDef) { - skillSection = - `\n## ${agentT('ai.agent.currentTask', locale)}:${skillCtx.skillDef.name}\n` + - `${agentT('ai.agent.skillPriorityNote', locale)}\n` + - skillCtx.skillDef.prompt - } else if (skillCtx?.skillMenu) { - skillSection = `\n${skillCtx.skillMenu}` - } - - return `${systemPrompt}${skillSection} - -${lockedSection}` + return buildSystemPromptCore({ + t: i18nT, + chatType, + assistantSystemPrompt, + ownerInfo, + locale, + skillCtx, + mentionedMembers, + }) } diff --git a/packages/node-runtime/src/ai/agent/__tests__/prompt-builder.test.ts b/packages/node-runtime/src/ai/agent/__tests__/prompt-builder.test.ts new file mode 100644 index 00000000..182ca105 --- /dev/null +++ b/packages/node-runtime/src/ai/agent/__tests__/prompt-builder.test.ts @@ -0,0 +1,84 @@ +/** + * Tests for shared prompt-builder. + * + * Run: npx tsx --test packages/node-runtime/src/ai/agent/__tests__/prompt-builder.test.ts + */ + +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { buildSystemPrompt } from '../prompt-builder' +import type { TranslateFn } from '../prompt-builder' + +const mockT: TranslateFn = (key, options) => { + if (key.startsWith('ai.agent.fallbackRoleDefinition')) return `[fallback:${key}]` + if (key === 'ai.agent.currentDateIs') return 'Current date is' + if (key === 'ai.agent.ownerNote') return `Owner: ${(options as Record)?.displayName}` + if (key === 'ai.agent.memberNoteGroup') return '[member-group]' + if (key === 'ai.agent.memberNotePrivate') return '[member-private]' + if (key === 'ai.agent.timeParamsIntro') return '[time-params]' + if (key === 'ai.agent.defaultYearNote') return `[year:${(options as Record)?.year}]` + if (key === 'ai.agent.responseInstruction') return '[response-instruction]' + if (key === 'ai.agent.mentionedMembersNote') return 'Mentioned members:' + if (key === 'ai.agent.currentTask') return 'Current task' + if (key === 'ai.agent.skillPriorityNote') return '[skill-priority]' + return `[${key}]` +} + +describe('buildSystemPrompt', () => { + it('uses fallback role when no assistant prompt', () => { + const result = buildSystemPrompt({ t: mockT, chatType: 'group' }) + assert.ok(result.includes('[fallback:ai.agent.fallbackRoleDefinition.group]')) + }) + + it('uses assistant prompt when provided', () => { + const result = buildSystemPrompt({ t: mockT, assistantSystemPrompt: 'Custom assistant' }) + assert.ok(result.includes('Custom assistant')) + assert.ok(!result.includes('[fallback')) + }) + + it('includes owner info when provided', () => { + const result = buildSystemPrompt({ + t: mockT, + ownerInfo: { displayName: 'Alice', platformId: 'alice123' }, + }) + assert.ok(result.includes('Owner: Alice')) + }) + + it('includes member note for private chat', () => { + const result = buildSystemPrompt({ t: mockT, chatType: 'private' }) + assert.ok(result.includes('[member-private]')) + }) + + it('includes mentioned members', () => { + const result = buildSystemPrompt({ + t: mockT, + mentionedMembers: [{ memberId: 1, platformId: 'p1', displayName: 'Bob', aliases: ['B'], mentionText: '@Bob' }], + }) + assert.ok(result.includes('member_id=1')) + assert.ok(result.includes('aliases=B')) + }) + + it('includes skill definition when active', () => { + const result = buildSystemPrompt({ + t: mockT, + skillCtx: { skillDef: { name: 'Summarizer', prompt: 'Summarize the chat.' } }, + }) + assert.ok(result.includes('Current task')) + assert.ok(result.includes('Summarizer')) + assert.ok(result.includes('Summarize the chat.')) + }) + + it('includes skill menu when no active skill', () => { + const result = buildSystemPrompt({ + t: mockT, + skillCtx: { skillMenu: '[SKILL MENU TEXT]' }, + }) + assert.ok(result.includes('[SKILL MENU TEXT]')) + }) + + it('includes date and response instruction', () => { + const result = buildSystemPrompt({ t: mockT }) + assert.ok(result.includes('Current date is')) + assert.ok(result.includes('[response-instruction]')) + }) +}) diff --git a/packages/node-runtime/src/ai/agent/prompt-builder.ts b/packages/node-runtime/src/ai/agent/prompt-builder.ts new file mode 100644 index 00000000..419342a2 --- /dev/null +++ b/packages/node-runtime/src/ai/agent/prompt-builder.ts @@ -0,0 +1,126 @@ +/** + * Agent system prompt builder — shared implementation. + * + * The i18n translation function is injected via `t` parameter, + * making this module platform-agnostic. + */ + +export interface OwnerInfo { + platformId: string + displayName: string +} + +export interface MentionedMember { + memberId: number + platformId: string + displayName: string + aliases: string[] + mentionText: string +} + +export interface SkillContext { + skillDef?: { name: string; prompt: string } + skillMenu?: string +} + +export type TranslateFn = (key: string, options?: Record) => string + +export interface BuildSystemPromptOptions { + t: TranslateFn + chatType?: 'group' | 'private' + assistantSystemPrompt?: string + ownerInfo?: OwnerInfo + locale?: string + skillCtx?: SkillContext + mentionedMembers?: MentionedMember[] +} + +function agentT(t: TranslateFn, key: string, locale: string, options?: Record): string { + return t(key, { lng: locale, ...options }) +} + +function getLockedPromptSection( + t: TranslateFn, + chatType: 'group' | 'private', + ownerInfo: OwnerInfo | undefined, + locale: string, + mentionedMembers: MentionedMember[] | undefined +): string { + const now = new Date() + const dateLocale = locale.startsWith('zh') ? 'zh-CN' : 'en-US' + const currentDate = now.toLocaleDateString(dateLocale, { + year: 'numeric', + month: 'long', + day: 'numeric', + weekday: 'long', + }) + + const isPrivate = chatType === 'private' + const chatContext = agentT(t, `ai.agent.chatContext.${chatType}`, locale) + + const ownerNote = ownerInfo + ? agentT(t, 'ai.agent.ownerNote', locale, { + displayName: ownerInfo.displayName, + platformId: ownerInfo.platformId, + chatContext, + }) + : '' + + const memberNote = isPrivate + ? agentT(t, 'ai.agent.memberNotePrivate', locale) + : agentT(t, 'ai.agent.memberNoteGroup', locale) + + const mentionedMembersNote = + mentionedMembers && mentionedMembers.length > 0 + ? `${agentT(t, 'ai.agent.mentionedMembersNote', locale)}\n${mentionedMembers + .map((member) => { + const aliasPart = member.aliases.length > 0 ? ` | aliases=${member.aliases.join(',')}` : '' + return `- member_id=${member.memberId} | mention=${member.mentionText} | display_name=${member.displayName} | platform_id=${member.platformId}${aliasPart}` + }) + .join('\n')}\n` + : '' + + const year = now.getFullYear() + + return `${agentT(t, 'ai.agent.currentDateIs', locale)} ${currentDate}。 +${ownerNote} +${mentionedMembersNote} +${memberNote} +${agentT(t, 'ai.agent.timeParamsIntro', locale)} +${agentT(t, 'ai.agent.defaultYearNote', locale, { year })} + +${agentT(t, 'ai.agent.responseInstruction', locale)}` +} + +function getFallbackRoleDefinition(t: TranslateFn, chatType: 'group' | 'private', locale: string): string { + return agentT(t, `ai.agent.fallbackRoleDefinition.${chatType}`, locale) +} + +export function buildSystemPrompt(options: BuildSystemPromptOptions): string { + const { + t, + chatType = 'group', + assistantSystemPrompt, + ownerInfo, + locale = 'zh-CN', + skillCtx, + mentionedMembers, + } = options + + const systemPrompt = assistantSystemPrompt || getFallbackRoleDefinition(t, chatType, locale) + const lockedSection = getLockedPromptSection(t, chatType, ownerInfo, locale, mentionedMembers) + + let skillSection = '' + if (skillCtx?.skillDef) { + skillSection = + `\n## ${agentT(t, 'ai.agent.currentTask', locale)}:${skillCtx.skillDef.name}\n` + + `${agentT(t, 'ai.agent.skillPriorityNote', locale)}\n` + + skillCtx.skillDef.prompt + } else if (skillCtx?.skillMenu) { + skillSection = `\n${skillCtx.skillMenu}` + } + + return `${systemPrompt}${skillSection} + +${lockedSection}` +} diff --git a/packages/node-runtime/src/ai/index.ts b/packages/node-runtime/src/ai/index.ts index fe849a6b..e0a28692 100644 --- a/packages/node-runtime/src/ai/index.ts +++ b/packages/node-runtime/src/ai/index.ts @@ -62,6 +62,16 @@ export type { PreprocessingPipelineOptions, PreprocessingPipelineResult } from ' export type { AgentCoreOptions, AgentCoreEvent, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './agent' export { runAgentCore } from './agent' +// Agent Prompt Builder +export { buildSystemPrompt } from './agent/prompt-builder' +export type { + BuildSystemPromptOptions, + OwnerInfo, + MentionedMember, + SkillContext, + TranslateFn, +} from './agent/prompt-builder' + // LLM Model Builder export { buildPiModel, normalizeAnthropicBaseUrl, normalizeOpenAICompatibleBaseUrl } from './llm-builder' export type { PiModelConfig, BuildPiModelOptions } from './llm-builder' diff --git a/packages/node-runtime/src/index.ts b/packages/node-runtime/src/index.ts index 07d61d34..051203b8 100644 --- a/packages/node-runtime/src/index.ts +++ b/packages/node-runtime/src/index.ts @@ -88,6 +88,10 @@ export type { export type { AgentCoreOptions, AgentCoreEvent, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './ai' export { runAgentCore } from './ai' +// Agent Prompt Builder +export { buildSystemPrompt } from './ai' +export type { BuildSystemPromptOptions, OwnerInfo, MentionedMember, SkillContext, TranslateFn } from './ai' + // LLM Model Builder export { buildPiModel, normalizeAnthropicBaseUrl, normalizeOpenAICompatibleBaseUrl } from './ai' export type { PiModelConfig, BuildPiModelOptions } from './ai'