refactor: extract execution prompt assembly

This commit is contained in:
kris
2026-04-02 22:32:19 +08:00
parent e348d6cc5d
commit 384dd570de
7 changed files with 415 additions and 157 deletions

View File

@@ -21,6 +21,9 @@ import {
} from "@/lib/boss-data";
import type { AiProvider, DispatchPlanTarget, Project, ProjectAgentControls, ReasoningEffort } from "@/lib/boss-data";
import { canInlineAttachmentText, extractAttachmentTextExcerpt } from "@/lib/boss-attachments";
import { resolveRuntimeRelevantMemories } from "@/lib/execution/memory-resolver";
import type { RelevantMemory } from "@/lib/execution/memory-resolver";
import { buildExecutionPrompt } from "@/lib/execution/prompt-assembler";
import { readAliyunOssObjectBuffer } from "@/lib/boss-storage-aliyun-oss";
import { readServerFileAttachmentBuffer } from "@/lib/boss-storage-server-file";
import {
@@ -90,8 +93,11 @@ export async function resolveMasterAgentExecutionConfig(
const promptPolicy = getMasterAgentPromptPolicyView(state);
const userPrompt = getUserMasterPromptView(state, resolvedAccountId);
const memoryScope = listUserMasterMemoriesView(state, resolvedAccountId, { includeArchived: false });
const projectMemories = selectRelevantProjectMemories(memoryScope, projectId, requestText);
const userMemories = selectRelevantUserMemories(memoryScope, requestText);
const { projectMemories, userMemories } = resolveRuntimeRelevantMemories({
projectId,
requestText,
memories: memoryScope,
});
const touchedMemoryIds = [...projectMemories, ...userMemories].map((memory) => memory.memoryId);
if (touchedMemoryIds.length > 0) {
await touchUserMasterMemories(touchedMemoryIds, resolvedAccountId);
@@ -109,84 +115,17 @@ export async function resolveMasterAgentExecutionConfig(
userPrompt,
projectMemories,
userMemories,
executionPrompt: buildMasterAgentExecutionPrompt({
state,
projectId,
requestText: requestText ?? "",
currentSessionExpiresAt: undefined,
agentControls: scopedAgentControls,
accountId: resolvedAccountId,
promptPolicy,
userPrompt,
executionPrompt: buildExecutionPrompt({
globalPrompt: promptPolicy?.globalPrompt ?? null,
userPrompt: userPrompt?.content ?? null,
conversationPrompt: scopedAgentControls?.promptOverride ?? null,
projectMemories,
userMemories,
requestText: requestText ?? "",
}),
};
}
function selectRelevantProjectMemories(
memories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
projectId: string,
requestText?: string,
) {
const projectScoped = memories.filter((memory) => memory.scope === "project");
if (projectId !== "master-agent") {
return projectScoped.filter((memory) => memory.projectId === projectId);
}
if (projectScoped.length === 0) {
return [];
}
const lowered = requestText?.trim().toLowerCase() ?? "";
if (!lowered) {
return projectScoped.slice(0, 6);
}
const matched = projectScoped.filter((memory) => {
const haystacks = [memory.title, memory.content, ...(memory.tags ?? [])]
.map((value) => value.toLowerCase());
return haystacks.some((value) => lowered.includes(value) || value.includes(lowered));
});
return (matched.length > 0 ? matched : projectScoped).slice(0, 6);
}
function selectRelevantUserMemories(
memories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
requestText?: string,
) {
const globalScoped = memories.filter((memory) => memory.scope === "global");
if (globalScoped.length === 0) {
return [];
}
const lowered = requestText?.trim().toLowerCase() ?? "";
const prioritized = [...globalScoped].sort((left, right) => {
const leftPriority =
left.memoryType === "workflow_rule" || left.memoryType === "user_preference" ? 1 : 0;
const rightPriority =
right.memoryType === "workflow_rule" || right.memoryType === "user_preference" ? 1 : 0;
if (leftPriority !== rightPriority) {
return rightPriority - leftPriority;
}
const leftTime = Date.parse(left.lastUsedAt ?? left.updatedAt ?? left.createdAt) || 0;
const rightTime = Date.parse(right.lastUsedAt ?? right.updatedAt ?? right.createdAt) || 0;
return rightTime - leftTime;
});
if (!lowered) {
return prioritized.slice(0, 8);
}
const matched = prioritized.filter((memory) => {
const haystacks = [memory.title, memory.content, ...(memory.tags ?? [])]
.map((value) => value.toLowerCase());
return haystacks.some((value) => lowered.includes(value) || value.includes(lowered));
});
return (matched.length > 0 ? matched : prioritized).slice(0, 8);
}
function buildAgentControlsDigest(agentControls?: ProjectAgentControls | null) {
if (!agentControls) {
return "当前对话覆盖:无";
@@ -200,63 +139,28 @@ function buildAgentControlsDigest(agentControls?: ProjectAgentControls | null) {
].join(" ");
}
function buildPromptPolicyDigest(promptPolicy: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>) {
return promptPolicy?.globalPrompt?.trim()
? [`管理员全局主提示词:`, promptPolicy.globalPrompt.trim()].join("\n")
: "管理员全局主提示词:无";
}
function buildUserPromptDigest(userPrompt: Awaited<ReturnType<typeof getUserMasterPromptView>>) {
return userPrompt?.content?.trim()
? [`用户私有主提示词:`, userPrompt.content.trim()].join("\n")
: "用户私有主提示词:无";
}
function buildMemoryDigest(title: string, memories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>) {
if (memories.length === 0) {
return `${title}:无`;
}
return [
`${title}`,
...memories.map((memory) => {
const meta = [
memory.scope === "project" && memory.projectId ? `projectId=${memory.projectId}` : null,
memory.memoryType ? `type=${memory.memoryType}` : null,
memory.tags?.length ? `tags=${memory.tags.join("|")}` : null,
]
.filter(Boolean)
.join(" · ");
return meta
? `- ${memory.title}${meta}${memory.content}`
: `- ${memory.title}${memory.content}`;
}),
].join("\n");
}
function buildMasterAgentExecutionPrompt(params: {
state: Awaited<ReturnType<typeof readState>>;
projectId: string;
requestText: string;
currentSessionExpiresAt?: string;
agentControls?: ProjectAgentControls | null;
accountId: string;
promptPolicy: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
projectMemories: RelevantMemory[];
userMemories: RelevantMemory[];
}) {
return [
buildMasterAgentInstructions(),
buildPromptPolicyDigest(params.promptPolicy),
buildUserPromptDigest(params.userPrompt),
params.agentControls?.promptOverride?.trim()
? ["当前对话附加提示词:", params.agentControls.promptOverride.trim()].join("\n")
: "当前对话附加提示词:无",
buildMemoryDigest("项目记忆", params.projectMemories),
buildMemoryDigest("用户记忆", params.userMemories),
buildExecutionPrompt({
globalPrompt: params.promptPolicy?.globalPrompt ?? "",
userPrompt: params.userPrompt?.content ?? "",
conversationPrompt: params.agentControls?.promptOverride ?? "",
projectMemories: params.projectMemories,
userMemories: params.userMemories,
requestText: params.requestText,
}),
buildAgentControlsDigest(params.agentControls),
"",
buildRuntimeDigest(params.state, params.requestText, params.currentSessionExpiresAt, params.agentControls),
buildRuntimeDigest(params.state, params.requestText, params.currentSessionExpiresAt),
].join("\n\n");
}
@@ -292,7 +196,6 @@ function buildRuntimeDigest(
state: Awaited<ReturnType<typeof readState>>,
requestText: string,
currentSessionExpiresAt?: string,
agentControls?: ProjectAgentControls | null,
) {
const recentMessages = state.projects
.find((project) => project.id === "master-agent")
@@ -331,7 +234,6 @@ function buildRuntimeDigest(
`登录会话策略:成功登录后默认保持 ${Math.round(AUTH_SESSION_TTL_MS / 24 / 60 / 60_000)} 天。`,
"Cookie Max-Age2592000 秒。",
currentSessionExpiresAt ? `当前请求会话到期时间:${currentSessionExpiresAt}` : undefined,
buildAgentControlsDigest(agentControls),
]
.filter(Boolean)
.join("\n");
@@ -499,8 +401,8 @@ async function replyViaOpenAiAccount(params: {
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
projectMemories?: RelevantMemory[];
userMemories?: RelevantMemory[];
}) {
if (!params.account?.apiKey?.trim() || !isApiCompatibleProvider(params.account.provider)) {
throw new Error("OPENAI_ACCOUNT_NOT_CONFIGURED");
@@ -548,31 +450,29 @@ async function generateApiProviderReply(params: {
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
projectMemories?: RelevantMemory[];
userMemories?: RelevantMemory[];
}) {
const state = await readState();
const effectiveProjectMemories =
params.projectMemories && params.projectMemories.length > 0
? params.projectMemories
: selectRelevantProjectMemories(
listUserMasterMemoriesView(state, params.userPrompt?.account ?? state.user.account, {
: resolveRuntimeRelevantMemories({
projectId: "master-agent",
requestText: params.requestText,
memories: listUserMasterMemoriesView(state, params.userPrompt?.account ?? state.user.account, {
includeArchived: false,
}),
"master-agent",
params.requestText,
);
}).projectMemories;
let response: Response;
const config = apiProviderConfig(params.provider);
const requestBody: Record<string, unknown> = {
model: params.model,
instructions: buildMasterAgentExecutionPrompt({
state,
projectId: "master-agent",
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
accountId: "master-agent",
promptPolicy: params.promptPolicy ?? null,
userPrompt: params.userPrompt ?? null,
projectMemories: effectiveProjectMemories,
@@ -638,16 +538,14 @@ function buildMasterOpenAiReplyPrompt(
agentControls?: ProjectAgentControls | null,
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>,
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>,
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
projectMemories?: RelevantMemory[],
userMemories?: RelevantMemory[],
) {
return buildMasterAgentExecutionPrompt({
state,
projectId: "master-agent",
requestText,
currentSessionExpiresAt,
agentControls,
accountId: "master-agent",
promptPolicy: promptPolicy ?? null,
userPrompt: userPrompt ?? null,
projectMemories: projectMemories ?? [],
@@ -667,8 +565,8 @@ async function queueAndStartOpenAiMasterAgentReply(params: {
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
projectMemories?: RelevantMemory[];
userMemories?: RelevantMemory[];
}) {
const timer = setTimeout(() => {
void (async () => {
@@ -727,8 +625,8 @@ async function enqueueOpenAiMasterAgentReply(params: {
agentControls?: ProjectAgentControls | null;
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
projectMemories?: RelevantMemory[];
userMemories?: RelevantMemory[];
}) {
const state = await readState();
const task = await queueMasterAgentTask({
@@ -866,16 +764,14 @@ function buildMasterCodexNodePrompt(
agentControls?: ProjectAgentControls | null,
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>,
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>,
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
projectMemories?: RelevantMemory[],
userMemories?: RelevantMemory[],
) {
return buildMasterAgentExecutionPrompt({
state,
projectId: "master-agent",
requestText,
currentSessionExpiresAt,
agentControls,
accountId: "master-agent",
promptPolicy: promptPolicy ?? null,
userPrompt: userPrompt ?? null,
projectMemories: projectMemories ?? [],

View File

@@ -0,0 +1,88 @@
import type { MasterAgentMemory } from "@/lib/boss-data";
export type RelevantMemory = Pick<
MasterAgentMemory,
"memoryId" | "scope" | "projectId" | "title" | "content" | "tags" | "memoryType" | "lastUsedAt" | "updatedAt" | "createdAt"
>;
export function resolveRelevantMemories(input: {
projectId: string;
requestText?: string;
memories: RelevantMemory[];
}) {
const lowered = input.requestText?.trim().toLowerCase() ?? "";
const projectScoped = input.memories.filter((memory) => {
if (memory.scope !== "project") {
return false;
}
if (input.projectId !== "master-agent") {
return memory.projectId === input.projectId;
}
return true;
});
const projectMemories =
input.projectId !== "master-agent"
? projectScoped.slice(0, 6)
: !lowered
? projectScoped.slice(0, 6)
: projectScoped
.filter((memory) => {
const haystacks = [memory.projectId, memory.title, memory.content, ...(memory.tags ?? [])]
.filter((value): value is string => Boolean(value))
.map((value) => value.toLowerCase());
return haystacks.some((value) => lowered.includes(value) || value.includes(lowered));
})
.slice(0, 6);
const userMemories = input.memories.filter((memory) => memory.scope === "global").slice(0, 8);
return {
projectMemories,
userMemories,
};
}
function prioritizeRuntimeMemories(memories: RelevantMemory[]) {
const globalMemories = memories
.filter((memory) => memory.scope === "global")
.sort((left, right) => {
const leftPriority =
left.memoryType === "workflow_rule" || left.memoryType === "user_preference" ? 1 : 0;
const rightPriority =
right.memoryType === "workflow_rule" || right.memoryType === "user_preference" ? 1 : 0;
if (leftPriority !== rightPriority) {
return rightPriority - leftPriority;
}
const leftTime = Date.parse(left.lastUsedAt ?? left.updatedAt ?? left.createdAt) || 0;
const rightTime = Date.parse(right.lastUsedAt ?? right.updatedAt ?? right.createdAt) || 0;
return rightTime - leftTime;
});
const projectMemories = memories.filter((memory) => memory.scope === "project");
return [...globalMemories, ...projectMemories];
}
export function resolveRuntimeRelevantMemories(input: {
projectId: string;
requestText?: string;
memories: RelevantMemory[];
}) {
const prioritizedMemories = prioritizeRuntimeMemories(input.memories);
const resolved = resolveRelevantMemories({
projectId: input.projectId,
requestText: input.requestText,
memories: prioritizedMemories,
});
if (input.projectId === "master-agent" && input.requestText?.trim() && resolved.projectMemories.length === 0) {
return {
projectMemories: prioritizedMemories.filter((memory) => memory.scope === "project").slice(0, 6),
userMemories: resolved.userMemories,
};
}
return resolved;
}
export const resolveRelevantMemoriesForTesting = resolveRelevantMemories;
export const resolveRuntimeRelevantMemoriesForTesting = resolveRuntimeRelevantMemories;

View File

@@ -0,0 +1,48 @@
import type { MasterAgentMemory } from "@/lib/boss-data";
type PromptMemory = Pick<MasterAgentMemory, "projectId" | "title" | "content" | "tags">;
function buildProjectMemorySection(memories: PromptMemory[]) {
if (memories.length === 0) {
return null;
}
return [
"项目记忆:",
...memories.map((memory) => `- [${memory.projectId ?? "unknown"}] ${memory.title}: ${memory.content}`),
].join("\n");
}
function buildUserMemorySection(memories: PromptMemory[]) {
if (memories.length === 0) {
return null;
}
return [
"用户通用记忆:",
...memories.map((memory) => `- ${memory.title}: ${memory.content}`),
].join("\n");
}
export function buildExecutionPrompt(input: {
globalPrompt?: string | null;
userPrompt?: string | null;
conversationPrompt?: string | null;
projectMemories: PromptMemory[];
userMemories: PromptMemory[];
requestText: string;
}) {
return [
input.globalPrompt?.trim() ? `管理员全局主提示词:\n${input.globalPrompt.trim()}` : null,
input.userPrompt?.trim() ? `用户私有主提示词:\n${input.userPrompt.trim()}` : null,
input.conversationPrompt?.trim() ? `当前对话附加提示词:\n${input.conversationPrompt.trim()}` : null,
buildProjectMemorySection(input.projectMemories),
buildUserMemorySection(input.userMemories),
`当前消息:\n${input.requestText}`,
]
.filter((value): value is string => value !== null)
.join("\n\n");
}
export const buildExecutionPromptForTesting = buildExecutionPrompt;
export { resolveRelevantMemoriesForTesting } from "@/lib/execution/memory-resolver";