mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
refactor(node-runtime): extract prompt-builder to @openchatlab/node-runtime
Move buildSystemPrompt from electron/main/ai/agent/ to shared package. i18n t() is now injected via BuildSystemPromptOptions parameter. Types OwnerInfo, MentionedMember, SkillContext are shared. Electron prompt-builder.ts injects its own t() and delegates. Added 8 unit tests with mock translation function. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
d1942fb18f
commit
0ac90030cb
5 files changed
+239
-93
No files matched your search
@@ -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, unknown>): 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,
|
||||
})
|
||||
}
|
||||
@@ -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<string, unknown>)?.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<string, unknown>)?.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]'))
|
||||
})
|
||||
})
|
||||
@@ -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, unknown>) => 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, unknown>): 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}`
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user