forked from EduCraft/curriculum-project-hub
fix: label Feishu trigger senders in agent prompts
This commit is contained in:
+20
-14
@@ -187,6 +187,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
projectId,
|
projectId,
|
||||||
workspaceDir: project.workspaceDir,
|
workspaceDir: project.workspaceDir,
|
||||||
rt,
|
rt,
|
||||||
|
senderOpenId,
|
||||||
});
|
});
|
||||||
const promptForAgent = withFeishuTriggerContext(agentPrompt, feishuTriggerContext);
|
const promptForAgent = withFeishuTriggerContext(agentPrompt, feishuTriggerContext);
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
model,
|
model,
|
||||||
prompt: agentPrompt.slice(0, 200),
|
prompt: agentPrompt.slice(0, 200),
|
||||||
sender: await senderAuditMetadata(rt, senderOpenId),
|
sender: await senderAuditMetadata(rt, senderOpenId),
|
||||||
...(feishuTriggerContext === null ? {} : { feishuTriggerContext }),
|
feishuTriggerContext,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -836,18 +837,18 @@ interface BuildFeishuTriggerContextArgs {
|
|||||||
readonly projectId: string;
|
readonly projectId: string;
|
||||||
readonly workspaceDir: string;
|
readonly workspaceDir: string;
|
||||||
readonly rt: FeishuRuntime;
|
readonly rt: FeishuRuntime;
|
||||||
|
readonly senderOpenId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildFeishuTriggerContext(args: BuildFeishuTriggerContextArgs): Promise<Prisma.InputJsonObject | null> {
|
async function buildFeishuTriggerContext(args: BuildFeishuTriggerContextArgs): Promise<Prisma.InputJsonObject> {
|
||||||
const replyToMessageId = nonEmpty(args.msg.parent_id);
|
const replyToMessageId = nonEmpty(args.msg.parent_id);
|
||||||
const rootId = nonEmpty(args.msg.root_id);
|
const rootId = nonEmpty(args.msg.root_id);
|
||||||
const threadId = nonEmpty(args.msg.thread_id);
|
const threadId = nonEmpty(args.msg.thread_id);
|
||||||
if (replyToMessageId === undefined && rootId === undefined && threadId === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context: Record<string, Prisma.InputJsonValue> = {
|
const context: Record<string, Prisma.InputJsonValue> = {
|
||||||
trigger_message_id: args.msg.message_id,
|
trigger_message_id: args.msg.message_id,
|
||||||
|
chat_id: args.chatId,
|
||||||
|
sender: await senderAuditMetadata(args.rt, args.senderOpenId),
|
||||||
};
|
};
|
||||||
if (replyToMessageId !== undefined) context["reply_to_message_id"] = replyToMessageId;
|
if (replyToMessageId !== undefined) context["reply_to_message_id"] = replyToMessageId;
|
||||||
if (rootId !== undefined) context["root_id"] = rootId;
|
if (rootId !== undefined) context["root_id"] = rootId;
|
||||||
@@ -878,24 +879,29 @@ function parseJsonForMetadata(text: string): Prisma.InputJsonValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function withFeishuTriggerContext(prompt: string, context: Prisma.InputJsonObject | null): string {
|
function withFeishuTriggerContext(prompt: string, context: Prisma.InputJsonObject): string {
|
||||||
if (context === null) return prompt;
|
return `Feishu trigger context:\n${JSON.stringify(context, null, 2)}\n\nUser request from ${senderLabelFromContext(context)}:\n${prompt}`;
|
||||||
return `Feishu trigger context:\n${JSON.stringify(context, null, 2)}\n\nUser request:\n${prompt}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function agentRunMetadata(
|
function agentRunMetadata(
|
||||||
roleId: string,
|
roleId: string,
|
||||||
rawPrompt: string,
|
rawPrompt: string,
|
||||||
feishuTriggerContext: Prisma.InputJsonObject | null,
|
feishuTriggerContext: Prisma.InputJsonObject,
|
||||||
): Prisma.InputJsonObject {
|
): Prisma.InputJsonObject {
|
||||||
const metadata: Record<string, Prisma.InputJsonValue> = { roleId };
|
const metadata: Record<string, Prisma.InputJsonValue> = { roleId, rawPrompt, feishuTriggerContext };
|
||||||
if (feishuTriggerContext !== null) {
|
|
||||||
metadata["rawPrompt"] = rawPrompt;
|
|
||||||
metadata["feishuTriggerContext"] = feishuTriggerContext;
|
|
||||||
}
|
|
||||||
return metadata as Prisma.InputJsonObject;
|
return metadata as Prisma.InputJsonObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function senderLabelFromContext(context: Prisma.InputJsonObject): string {
|
||||||
|
const sender = context["sender"];
|
||||||
|
if (typeof sender !== "object" || sender === null || Array.isArray(sender)) return "unknown sender";
|
||||||
|
const senderObject = sender as Record<string, unknown>;
|
||||||
|
const name = senderObject["name"];
|
||||||
|
if (typeof name === "string" && name.trim() !== "") return name.trim();
|
||||||
|
const openId = senderObject["open_id"];
|
||||||
|
return typeof openId === "string" && openId.trim() !== "" ? openId.trim() : "unknown sender";
|
||||||
|
}
|
||||||
|
|
||||||
function auditDecision(decision: AuthorizationDecision): Record<string, unknown> {
|
function auditDecision(decision: AuthorizationDecision): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
allowed: decision.allowed,
|
allowed: decision.allowed,
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ function makeEvent(chatId: string, text: string, senderOpenId = "ou_test_user",
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectPromptFromSender(prompt: string | undefined, senderOpenId: string, rawPrompt: string): void {
|
||||||
|
expect(prompt).toContain("Feishu trigger context");
|
||||||
|
expect(prompt).toContain(`"open_id": "${senderOpenId}"`);
|
||||||
|
expect(prompt).toContain(`User request from ${senderOpenId}:\n${rawPrompt}`);
|
||||||
|
}
|
||||||
|
|
||||||
function createMockRunAgent(calls: RunRequest[] = []): TestRunner {
|
function createMockRunAgent(calls: RunRequest[] = []): TestRunner {
|
||||||
return async (req) => {
|
return async (req) => {
|
||||||
calls.push(req);
|
calls.push(req);
|
||||||
@@ -145,10 +151,42 @@ describe("trigger full lifecycle (integration)", () => {
|
|||||||
const runs = await prisma.agentRun.findMany();
|
const runs = await prisma.agentRun.findMany();
|
||||||
expect(runs).toHaveLength(1);
|
expect(runs).toHaveLength(1);
|
||||||
expect(runs[0]?.status).toBe("COMPLETED");
|
expect(runs[0]?.status).toBe("COMPLETED");
|
||||||
expect(runs[0]?.prompt).toBe("第一段\n第二段");
|
expectPromptFromSender(runs[0]?.prompt, "ou_test_user", "第一段\n第二段");
|
||||||
});
|
});
|
||||||
expect(runAgentCalls).toHaveLength(1);
|
expect(runAgentCalls).toHaveLength(1);
|
||||||
expect(runAgentCalls[0]?.prompt).toBe("第一段\n第二段");
|
expectPromptFromSender(runAgentCalls[0]?.prompt, "ou_test_user", "第一段\n第二段");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the project session shared while labeling each sender in the prompt", async () => {
|
||||||
|
await seedProject("proj-speaker", "chat-speaker");
|
||||||
|
await prisma.permissionGrant.create({
|
||||||
|
data: {
|
||||||
|
resourceType: "PROJECT",
|
||||||
|
resourceId: "proj-speaker",
|
||||||
|
principalType: "FEISHU_CHAT",
|
||||||
|
principalId: "chat-speaker",
|
||||||
|
role: "EDIT",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
|
||||||
|
|
||||||
|
await trigger(makeEvent("chat-speaker", "@_user_1 Alice 的需求", "ou_alice"), rt);
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(runAgentCalls).toHaveLength(1);
|
||||||
|
});
|
||||||
|
await vi.waitFor(async () => {
|
||||||
|
expect(await prisma.projectAgentLock.findMany()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
await trigger(makeEvent("chat-speaker", "@_user_1 Bob 的需求", "ou_bob"), rt);
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(runAgentCalls).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
expectPromptFromSender(runAgentCalls[0]?.prompt, "ou_alice", "Alice 的需求");
|
||||||
|
expectPromptFromSender(runAgentCalls[1]?.prompt, "ou_bob", "Bob 的需求");
|
||||||
|
const sessions = await prisma.agentSession.findMany();
|
||||||
|
expect(sessions).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("/new bypasses message batching", async () => {
|
it("/new bypasses message batching", async () => {
|
||||||
@@ -351,7 +389,7 @@ describe("trigger full lifecycle (integration)", () => {
|
|||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(runAgentCalls).toHaveLength(2);
|
expect(runAgentCalls).toHaveLength(2);
|
||||||
});
|
});
|
||||||
expect(runAgentCalls[1]?.prompt).toBe("第二个请求");
|
expectPromptFromSender(runAgentCalls[1]?.prompt, "ou_test_user", "第二个请求");
|
||||||
|
|
||||||
secondRun.resolve(completedRunResult("second done", "sdk-session-second"));
|
secondRun.resolve(completedRunResult("second done", "sdk-session-second"));
|
||||||
await vi.waitFor(async () => {
|
await vi.waitFor(async () => {
|
||||||
@@ -485,7 +523,7 @@ describe("trigger full lifecycle (integration)", () => {
|
|||||||
await vi.waitFor(async () => {
|
await vi.waitFor(async () => {
|
||||||
const runs = await prisma.agentRun.findMany();
|
const runs = await prisma.agentRun.findMany();
|
||||||
expect(runs).toHaveLength(1);
|
expect(runs).toHaveLength(1);
|
||||||
expect(runs[0]?.prompt).toBe("/unknown");
|
expectPromptFromSender(runs[0]?.prompt, "ou_test_user", "/unknown");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -531,7 +569,7 @@ describe("trigger full lifecycle (integration)", () => {
|
|||||||
await vi.waitFor(async () => {
|
await vi.waitFor(async () => {
|
||||||
const runs = await prisma.agentRun.findMany();
|
const runs = await prisma.agentRun.findMany();
|
||||||
expect(runs).toHaveLength(1);
|
expect(runs).toHaveLength(1);
|
||||||
expect(runs[0]?.prompt).toBe("写第三单元");
|
expectPromptFromSender(runs[0]?.prompt, "ou_test_user", "写第三单元");
|
||||||
expect(runs[0]?.metadata).toMatchObject({ roleId: "draft" });
|
expect(runs[0]?.metadata).toMatchObject({ roleId: "draft" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -651,7 +689,7 @@ describe("trigger full lifecycle (integration)", () => {
|
|||||||
expect(prompt).toContain("m-parent");
|
expect(prompt).toContain("m-parent");
|
||||||
expect(prompt).toContain("thread-1");
|
expect(prompt).toContain("thread-1");
|
||||||
expect(prompt).toContain("上一条需求");
|
expect(prompt).toContain("上一条需求");
|
||||||
expect(prompt).toContain("User request:\n继续这个改法");
|
expectPromptFromSender(prompt, "ou_test_user", "继续这个改法");
|
||||||
|
|
||||||
const run = await prisma.agentRun.findFirst();
|
const run = await prisma.agentRun.findFirst();
|
||||||
expect(run?.prompt).toBe(prompt);
|
expect(run?.prompt).toBe(prompt);
|
||||||
@@ -660,6 +698,8 @@ describe("trigger full lifecycle (integration)", () => {
|
|||||||
rawPrompt: "继续这个改法",
|
rawPrompt: "继续这个改法",
|
||||||
feishuTriggerContext: {
|
feishuTriggerContext: {
|
||||||
trigger_message_id: "m-child",
|
trigger_message_id: "m-child",
|
||||||
|
chat_id: "chat-13c",
|
||||||
|
sender: { open_id: "ou_test_user" },
|
||||||
reply_to_message_id: "m-parent",
|
reply_to_message_id: "m-parent",
|
||||||
root_id: "m-root",
|
root_id: "m-root",
|
||||||
thread_id: "thread-1",
|
thread_id: "thread-1",
|
||||||
|
|||||||
Reference in New Issue
Block a user