mirror of
https://github.com/Significant-Gravitas/AgentProbe.git
synced 2026-11-10 04:41:46 -10:00
329 lines
10 KiB
TypeScript
329 lines
10 KiB
TypeScript
import { beforeEach, describe, expect, test } from "bun:test";
|
|
|
|
import {
|
|
generateNextStep,
|
|
generatePersonaStep,
|
|
} from "../../src/domains/evaluation/simulator.ts";
|
|
import { AgentProbeRuntimeError } from "../../src/shared/utils/errors.ts";
|
|
import {
|
|
asResponsesClient,
|
|
buildPersona,
|
|
buildPersonaStep,
|
|
FakeResponsesClient,
|
|
} from "./support.ts";
|
|
|
|
describe("simulator", () => {
|
|
const originalModel = process.env.AGENTPROBE_PERSONA_MODEL;
|
|
|
|
beforeEach(() => {
|
|
if (originalModel === undefined) {
|
|
delete process.env.AGENTPROBE_PERSONA_MODEL;
|
|
} else {
|
|
process.env.AGENTPROBE_PERSONA_MODEL = originalModel;
|
|
}
|
|
});
|
|
|
|
test("uses env default model and guidance for required turns", async () => {
|
|
process.env.AGENTPROBE_PERSONA_MODEL = "env-persona-model";
|
|
const client = new FakeResponsesClient([
|
|
buildPersonaStep(
|
|
"continue",
|
|
"I need to know when the refund will show up.",
|
|
),
|
|
]);
|
|
|
|
const result = await generatePersonaStep(
|
|
buildPersona({
|
|
id: "frustrated-customer",
|
|
name: "Frustrated Customer",
|
|
description: "Emotionally charged support user.",
|
|
demographics: {
|
|
role: "end-user customer",
|
|
techLiteracy: "low",
|
|
domainExpertise: "none",
|
|
languageStyle: "casual",
|
|
},
|
|
personality: {
|
|
patience: 2,
|
|
assertiveness: 4,
|
|
detailOrientation: 2,
|
|
cooperativeness: 3,
|
|
emotionalIntensity: 4,
|
|
},
|
|
behavior: {
|
|
openingStyle: "Start frustrated.",
|
|
followUpStyle: "Push for specifics when the agent is vague.",
|
|
escalationTriggers: ["Ask for a human after repeated dead ends."],
|
|
topicDrift: "low",
|
|
clarificationCompliance: "medium",
|
|
},
|
|
systemPrompt:
|
|
"You are a frustrated customer asking for help with a broken order.",
|
|
}),
|
|
[
|
|
{ role: "user", content: "My order arrived broken." },
|
|
{
|
|
role: "assistant",
|
|
content: "I can help with that. Do you want a refund?",
|
|
},
|
|
],
|
|
asResponsesClient(client) as never,
|
|
{
|
|
guidance: "Ask about refund timing.",
|
|
requireResponse: true,
|
|
},
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
status: "continue",
|
|
message: "I need to know when the refund will show up.",
|
|
});
|
|
expect(client.calls).toHaveLength(1);
|
|
expect(client.calls[0]?.model).toBe("env-persona-model");
|
|
expect(client.calls[0]?.instructions).toContain("Frustrated Customer");
|
|
expect(client.calls[0]?.input).toContain("Ask about refund timing.");
|
|
expect(client.calls[0]?.input).toContain("Conversation so far:");
|
|
expect(client.calls[0]?.input).toContain(
|
|
"A response is required for this scripted turn.",
|
|
);
|
|
expect(client.calls[0]?.text.format.type).toBe("json_schema");
|
|
expect(client.calls[0]?.text.format.name).toBe("persona_step");
|
|
expect(client.calls[0]?.text.format.strict).toBe(true);
|
|
expect(client.calls[0]?.text.format.schema.required).toEqual(["message"]);
|
|
});
|
|
|
|
test("prefers persona model override", async () => {
|
|
process.env.AGENTPROBE_PERSONA_MODEL = "env-persona-model";
|
|
const client = new FakeResponsesClient([
|
|
buildPersonaStep("continue", "Refund, and I need it processed today."),
|
|
]);
|
|
|
|
await generatePersonaStep(
|
|
buildPersona({ model: "persona-override-model" }),
|
|
"User: My order is broken.\nAssistant: Do you want a refund or replacement?",
|
|
asResponsesClient(client) as never,
|
|
{ requireResponse: true },
|
|
);
|
|
|
|
expect(client.calls[0]?.model).toBe("persona-override-model");
|
|
});
|
|
|
|
test("ignores checkpoint turns", async () => {
|
|
const client = new FakeResponsesClient([
|
|
buildPersonaStep("continue", "Yes, order 1234. Can you fix this?"),
|
|
]);
|
|
|
|
const result = await generatePersonaStep(
|
|
buildPersona(),
|
|
[
|
|
{ role: "user", content: "I was charged twice." },
|
|
{ role: "checkpoint", assert: [{ tool_called: "lookup_charge" }] },
|
|
{
|
|
role: "assistant",
|
|
content: "I can check that. What is the order number?",
|
|
},
|
|
],
|
|
asResponsesClient(client) as never,
|
|
{ requireResponse: true },
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
status: "continue",
|
|
message: "Yes, order 1234. Can you fix this?",
|
|
});
|
|
const input = client.calls[0]?.input;
|
|
const inputText =
|
|
typeof input === "string"
|
|
? input
|
|
: (input
|
|
?.flatMap((message) => message.content.map((part) => part.text))
|
|
.join("\n\n") ?? "");
|
|
expect(inputText.toLowerCase()).not.toContain("checkpoint");
|
|
});
|
|
|
|
test("supports stop statuses", async () => {
|
|
for (const status of ["completed", "stalled"] as const) {
|
|
const client = new FakeResponsesClient([buildPersonaStep(status)]);
|
|
const result = await generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "user", content: "Hello" }],
|
|
asResponsesClient(client) as never,
|
|
);
|
|
expect(result).toEqual({ status, message: null });
|
|
}
|
|
});
|
|
|
|
test("normalizes placeholder and acknowledgement terminal messages", async () => {
|
|
const cases = [":", "...", " null ", "Thanks, that's all."];
|
|
for (const message of cases) {
|
|
const client = new FakeResponsesClient([
|
|
buildPersonaStep("completed", message),
|
|
]);
|
|
const result = await generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "user", content: "Hello" }],
|
|
asResponsesClient(client) as never,
|
|
);
|
|
expect(result).toEqual({ status: "completed", message: null });
|
|
}
|
|
});
|
|
|
|
test("coerces conversational terminal messages to continue", async () => {
|
|
const client = new FakeResponsesClient([
|
|
buildPersonaStep("completed", "How complicated would that be?"),
|
|
]);
|
|
|
|
const result = await generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "assistant", content: "I can create the posts." }],
|
|
asResponsesClient(client) as never,
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
status: "continue",
|
|
message: "How complicated would that be?",
|
|
});
|
|
});
|
|
|
|
test("coerces required response payloads and rejects invalid required turns", async () => {
|
|
const acceptableClient = new FakeResponsesClient([
|
|
buildPersonaStep("completed", "I need the CRM record for Sarah."),
|
|
]);
|
|
const acceptable = await generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "assistant", content: "What should I look up?" }],
|
|
asResponsesClient(acceptableClient) as never,
|
|
{ requireResponse: true },
|
|
);
|
|
expect(acceptable).toEqual({
|
|
status: "continue",
|
|
message: "I need the CRM record for Sarah.",
|
|
});
|
|
|
|
const invalidStatusClient = new FakeResponsesClient([
|
|
buildPersonaStep("completed"),
|
|
]);
|
|
await expect(
|
|
generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "user", content: "Hello" }],
|
|
asResponsesClient(invalidStatusClient) as never,
|
|
{ requireResponse: true },
|
|
),
|
|
).rejects.toThrow(/must return `continue`/);
|
|
|
|
const emptyMessageClient = new FakeResponsesClient([
|
|
buildPersonaStep("continue", " "),
|
|
]);
|
|
await expect(
|
|
generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "user", content: "Hello" }],
|
|
asResponsesClient(emptyMessageClient) as never,
|
|
{ requireResponse: true },
|
|
),
|
|
).rejects.toThrow(/non-empty `message`/);
|
|
});
|
|
|
|
test("generateNextStep returns a plain message", async () => {
|
|
const client = new FakeResponsesClient([
|
|
buildPersonaStep("continue", "I need a refund, and I need it today."),
|
|
]);
|
|
|
|
const result = await generateNextStep(
|
|
buildPersona(),
|
|
[{ role: "user", content: "Hello" }],
|
|
asResponsesClient(client) as never,
|
|
{ guidance: "Ask for urgent refund handling." },
|
|
);
|
|
|
|
expect(result).toBe("I need a refund, and I need it today.");
|
|
});
|
|
|
|
test("accepts fenced JSON, embedded JSON, and plaintext fallbacks", async () => {
|
|
const fencedClient = new FakeResponsesClient([
|
|
{
|
|
outputText:
|
|
'```json\n{"status":"continue","message":"I need the refund timeline."}\n```',
|
|
},
|
|
]);
|
|
expect(
|
|
await generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "user", content: "Hello" }],
|
|
asResponsesClient(fencedClient) as never,
|
|
{ requireResponse: true },
|
|
),
|
|
).toEqual({
|
|
status: "continue",
|
|
message: "I need the refund timeline.",
|
|
});
|
|
|
|
const embeddedClient = new FakeResponsesClient([
|
|
{
|
|
outputText: 'Here is the result: {"status":"completed","message":null}',
|
|
},
|
|
]);
|
|
expect(
|
|
await generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "user", content: "Hello" }],
|
|
asResponsesClient(embeddedClient) as never,
|
|
),
|
|
).toEqual({ status: "completed", message: null });
|
|
|
|
const plaintextRequiredClient = new FakeResponsesClient([
|
|
{
|
|
outputText: "I need you to check the CRM contact for Sarah.",
|
|
},
|
|
]);
|
|
expect(
|
|
await generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "assistant", content: "Who should I look up?" }],
|
|
asResponsesClient(plaintextRequiredClient) as never,
|
|
{ requireResponse: true },
|
|
),
|
|
).toEqual({
|
|
status: "continue",
|
|
message: "I need you to check the CRM contact for Sarah.",
|
|
});
|
|
|
|
const plaintextContinueClient = new FakeResponsesClient([
|
|
{ outputText: "Can you also verify their company?" },
|
|
]);
|
|
expect(
|
|
await generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "assistant", content: "I found the contact." }],
|
|
asResponsesClient(plaintextContinueClient) as never,
|
|
),
|
|
).toEqual({
|
|
status: "continue",
|
|
message: "Can you also verify their company?",
|
|
});
|
|
|
|
const plaintextCompletedClient = new FakeResponsesClient([
|
|
{ outputText: "The task is complete. No further response." },
|
|
]);
|
|
expect(
|
|
await generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "assistant", content: "Done." }],
|
|
asResponsesClient(plaintextCompletedClient) as never,
|
|
),
|
|
).toEqual({ status: "completed", message: null });
|
|
});
|
|
|
|
test("rejects empty model output", async () => {
|
|
const client = new FakeResponsesClient([{ outputText: "" }]);
|
|
await expect(
|
|
generatePersonaStep(
|
|
buildPersona(),
|
|
[{ role: "assistant", content: "Done." }],
|
|
asResponsesClient(client) as never,
|
|
),
|
|
).rejects.toThrow(AgentProbeRuntimeError);
|
|
});
|
|
});
|