Skip to content

Commit 0e0ba18

Browse files
committed
Add prompt-improvement briefs to eval loop
1 parent ab544a8 commit 0e0ba18

11 files changed

Lines changed: 239 additions & 7 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ coverage
1010
*.tsbuildinfo
1111
packages/integrations/*.manual-eval-report.json
1212
packages/integrations/manual-eval-report.json
13+
packages/integrations/*.prompt-improvement.md

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,33 @@ Atlas is a chat-first brain-dump scheduler MVP. A user sends freeform text, the
3232
- `pnpm db:test:reset`: reset the local `atlas_test` database schema for integration reruns
3333
- `pnpm db:test:stop`: stop the local Homebrew Postgres test service
3434

35+
## Prompt Improvement Loop
36+
37+
Atlas treats prompt changes as product behavior changes. Use the live eval harness to iterate on prompts deliberately instead of editing blind.
38+
39+
Recommended loop:
40+
41+
1. Edit the owning prompt, schema, or parser in `packages/integrations` and `packages/core` together when the contract changes.
42+
2. Run the narrowest relevant live eval first:
43+
- `pnpm eval:planner`
44+
- `pnpm eval:turn-router`
45+
- `pnpm eval:router-confirmation`
46+
- `pnpm eval:conversation-context`
47+
- `pnpm eval:confirmed-mutation-recovery`
48+
3. Inspect the suite-specific report written under `packages/integrations/*.manual-eval-report.json`.
49+
4. If the suite fails, inspect the generated prompt-improvement brief under `packages/integrations/*.prompt-improvement.md` and use it as the starting point for the next prompt revision.
50+
5. Tighten the prompt or contract based on the actual failing model output.
51+
6. Run `pnpm eval:all` before merging to confirm the full prompt surface still passes together.
52+
53+
Notes:
54+
55+
- `pnpm eval:all` writes the canonical consolidated report to `packages/integrations/manual-eval-report.json`.
56+
- Single-suite evals write suite-specific reports such as `packages/integrations/conversation-context.manual-eval-report.json`.
57+
- Failing suites also write suite-specific prompt-improvement briefs such as `packages/integrations/conversation-context.prompt-improvement.md`.
58+
- Generated eval reports are ignored by git and should not be committed.
59+
- Generated prompt-improvement briefs are also ignored by git and should not be committed.
60+
- Live evals help judge prompt quality, but they do not replace deterministic tests and schema validation.
61+
3562
## Local Integration DB
3663

3764
Atlas supports an explicit local Postgres workflow for integration tests on macOS with Homebrew `postgresql@16`.

packages/integrations/src/manual/all.eval.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { runConfirmedMutationRecoveryEvalSuite } from "./confirmed-mutation-reco
44
import { runConversationContextEvalSuite } from "./conversation-context.eval-suite";
55
import { runPlannerEvalSuite } from "./planner.eval-suite";
66
import { runRouterConfirmationEvalSuite } from "./router-confirmation.eval-suite";
7-
import { ensureManualEvalEnv, buildEvalReport, writeEvalReport } from "./shared";
7+
import {
8+
ensureManualEvalEnv,
9+
buildEvalReport,
10+
writeEvalReport,
11+
writePromptImprovementBriefsForFailures
12+
} from "./shared";
813
import { runTurnRouterEvalSuite } from "./turn-router.eval-suite";
914

1015
beforeAll(() => {
@@ -23,6 +28,7 @@ describe.sequential("manual prompt eval loop", () => {
2328

2429
const report = buildEvalReport(suites);
2530
const reportPath = await writeEvalReport(report);
31+
const briefPaths = await writePromptImprovementBriefsForFailures(suites);
2632

2733
console.table(
2834
suites.map((suite) => ({
@@ -34,6 +40,9 @@ describe.sequential("manual prompt eval loop", () => {
3440
}))
3541
);
3642
console.log(`Manual eval report written to ${reportPath}`);
43+
if (briefPaths.length > 0) {
44+
console.log(`Prompt improvement briefs written to ${briefPaths.join(", ")}`);
45+
}
3746

3847
expect(report.failedCases).toBe(0);
3948
}, 300_000);

packages/integrations/src/manual/confirmed-mutation-recovery.eval.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22

33
import { runConfirmedMutationRecoveryEvalSuite } from "./confirmed-mutation-recovery.eval-suite";
4-
import { ensureManualEvalEnv } from "./shared";
4+
import {
5+
ensureManualEvalEnv,
6+
writePromptImprovementBrief,
7+
writeSuiteEvalReport
8+
} from "./shared";
59

610
beforeAll(() => {
711
ensureManualEvalEnv();
@@ -10,6 +14,9 @@ beforeAll(() => {
1014
describe.sequential("manual confirmed-mutation recovery eval", () => {
1115
it("checks curated recovery cases against the live OpenAI prompt", async () => {
1216
const suite = await runConfirmedMutationRecoveryEvalSuite();
17+
const reportPath = await writeSuiteEvalReport(suite);
18+
const briefPath =
19+
suite.failed > 0 ? await writePromptImprovementBrief(suite) : null;
1320

1421
console.table(
1522
suite.cases.map((testCase) => ({
@@ -23,6 +30,10 @@ describe.sequential("manual confirmed-mutation recovery eval", () => {
2330
error: testCase.error ?? ""
2431
}))
2532
);
33+
console.log(`Manual eval report written to ${reportPath}`);
34+
if (briefPath) {
35+
console.log(`Prompt improvement brief written to ${briefPath}`);
36+
}
2637

2738
expect(suite.failed).toBe(0);
2839
}, 120_000);

packages/integrations/src/manual/conversation-context.eval.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22

33
import { runConversationContextEvalSuite } from "./conversation-context.eval-suite";
4-
import { ensureManualEvalEnv, writeSuiteEvalReport } from "./shared";
4+
import {
5+
ensureManualEvalEnv,
6+
writePromptImprovementBrief,
7+
writeSuiteEvalReport
8+
} from "./shared";
59

610
beforeAll(() => {
711
ensureManualEvalEnv();
@@ -11,6 +15,8 @@ describe.sequential("manual conversation context eval", () => {
1115
it("checks curated continuity cases against the live OpenAI prompts", async () => {
1216
const suite = await runConversationContextEvalSuite();
1317
const reportPath = await writeSuiteEvalReport(suite);
18+
const briefPath =
19+
suite.failed > 0 ? await writePromptImprovementBrief(suite) : null;
1420

1521
console.table(
1622
suite.cases.map((testCase) => ({
@@ -23,6 +29,9 @@ describe.sequential("manual conversation context eval", () => {
2329
}))
2430
);
2531
console.log(`Manual eval report written to ${reportPath}`);
32+
if (briefPath) {
33+
console.log(`Prompt improvement brief written to ${briefPath}`);
34+
}
2635

2736
expect(suite.failed).toBe(0);
2837
}, 120_000);

packages/integrations/src/manual/planner.eval.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22

33
import { runPlannerEvalSuite } from "./planner.eval-suite";
4-
import { ensureManualEvalEnv } from "./shared";
4+
import {
5+
ensureManualEvalEnv,
6+
writePromptImprovementBrief,
7+
writeSuiteEvalReport
8+
} from "./shared";
59

610
beforeAll(() => {
711
ensureManualEvalEnv();
@@ -10,6 +14,9 @@ beforeAll(() => {
1014
describe.sequential("manual planner eval", () => {
1115
it("checks curated planner timing cases against the live OpenAI prompt", async () => {
1216
const suite = await runPlannerEvalSuite();
17+
const reportPath = await writeSuiteEvalReport(suite);
18+
const briefPath =
19+
suite.failed > 0 ? await writePromptImprovementBrief(suite) : null;
1320

1421
console.table(
1522
suite.cases.map((testCase) => ({
@@ -22,6 +29,10 @@ describe.sequential("manual planner eval", () => {
2229
error: testCase.error ?? ""
2330
}))
2431
);
32+
console.log(`Manual eval report written to ${reportPath}`);
33+
if (briefPath) {
34+
console.log(`Prompt improvement brief written to ${briefPath}`);
35+
}
2536

2637
expect(suite.failed).toBe(0);
2738
}, 120_000);

packages/integrations/src/manual/router-confirmation.eval.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22

33
import { runRouterConfirmationEvalSuite } from "./router-confirmation.eval-suite";
4-
import { ensureManualEvalEnv } from "./shared";
4+
import {
5+
ensureManualEvalEnv,
6+
writePromptImprovementBrief,
7+
writeSuiteEvalReport
8+
} from "./shared";
59

610
beforeAll(() => {
711
ensureManualEvalEnv();
@@ -10,6 +14,9 @@ beforeAll(() => {
1014
describe.sequential("manual router confirmation eval", () => {
1115
it("checks curated confirmation cases against the live OpenAI prompt", async () => {
1216
const suite = await runRouterConfirmationEvalSuite();
17+
const reportPath = await writeSuiteEvalReport(suite);
18+
const briefPath =
19+
suite.failed > 0 ? await writePromptImprovementBrief(suite) : null;
1320

1421
console.table(
1522
suite.cases.map((testCase) => ({
@@ -22,6 +29,10 @@ describe.sequential("manual router confirmation eval", () => {
2229
error: testCase.error ?? ""
2330
}))
2431
);
32+
console.log(`Manual eval report written to ${reportPath}`);
33+
if (briefPath) {
34+
console.log(`Prompt improvement brief written to ${briefPath}`);
35+
}
2536

2637
expect(suite.failed).toBe(0);
2738
}, 120_000);

packages/integrations/src/manual/shared.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { mkdir, writeFile } from "node:fs/promises";
22
import path from "node:path";
33

4+
import { confirmedMutationRecoverySystemPrompt } from "../prompts/confirmed-mutation-recovery";
5+
import { conversationMemorySummarySystemPrompt } from "../prompts/conversation-memory-summary";
6+
import { conversationResponseSystemPrompt } from "../prompts/conversation-response";
7+
import { inboxPlannerSystemPrompt } from "../prompts/planner";
8+
import { turnRouterSystemPrompt } from "../prompts/turn-router";
9+
410
export type EvalCaseResult = {
511
name: string;
612
pass: boolean;
@@ -26,6 +32,20 @@ export type EvalReport = {
2632
suites: EvalSuiteResult[];
2733
};
2834

35+
const SUITE_PROMPTS: Record<string, string> = {
36+
planner: inboxPlannerSystemPrompt,
37+
"turn-router": turnRouterSystemPrompt,
38+
"router-confirmation": turnRouterSystemPrompt,
39+
"confirmed-mutation-recovery": confirmedMutationRecoverySystemPrompt,
40+
"conversation-context": [
41+
"Conversation Response Prompt:",
42+
conversationResponseSystemPrompt,
43+
"",
44+
"Conversation Memory Summary Prompt:",
45+
conversationMemorySummarySystemPrompt
46+
].join("\n")
47+
};
48+
2949
export function ensureManualEvalEnv() {
3050
if (!process.env.OPENAI_API_KEY?.trim()) {
3151
throw new Error("OPENAI_API_KEY is required to run the manual evals.");
@@ -78,3 +98,102 @@ export async function writeSuiteEvalReport(suite: EvalSuiteResult) {
7898

7999
return reportPath;
80100
}
101+
102+
function formatCaseDetails(details: Record<string, unknown>) {
103+
return Object.entries(details)
104+
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
105+
.join("\n");
106+
}
107+
108+
function buildPromptImprovementPrompt(suite: EvalSuiteResult) {
109+
const originalPrompt =
110+
SUITE_PROMPTS[suite.suiteName] ??
111+
"Prompt text unavailable for this suite. Inspect the owning prompt module directly.";
112+
113+
const failedCases = suite.cases.filter((testCase) => !testCase.pass);
114+
const testResults =
115+
failedCases.length === 0
116+
? "No failing cases."
117+
: failedCases
118+
.map(
119+
(testCase) =>
120+
[
121+
`Case: ${testCase.name}`,
122+
`Details:`,
123+
formatCaseDetails(testCase.details),
124+
testCase.error ? `Error: ${testCase.error}` : null
125+
]
126+
.filter(Boolean)
127+
.join("\n")
128+
)
129+
.join("\n\n---\n\n");
130+
131+
return [
132+
"You are optimizing a production prompt to improve eval pass rate without breaking its intended behavior.",
133+
"",
134+
"You will receive:",
135+
"- `Original prompt`: the current prompt text",
136+
"- `Test results`: failing cases, logs, model outputs, and any passing-context clues",
137+
"",
138+
"Your job:",
139+
"1. Diagnose why the prompt underperformed.",
140+
"2. Identify the smallest prompt changes likely to improve behavior.",
141+
"3. Rewrite the prompt to improve test performance while preserving the original contract.",
142+
"4. Avoid brittle edits that only patch one example.",
143+
"",
144+
"Working rules:",
145+
"- Preserve the original intent, role, and task boundary.",
146+
"- Prefer minimal but high-leverage edits.",
147+
"- Do not overfit to one failure string.",
148+
"- Generalize from the failure pattern.",
149+
"- Keep successful existing behavior intact unless the failures show a real conflict.",
150+
"- If the test failure suggests a schema or evaluator problem rather than a prompt problem, say so briefly before proposing prompt edits.",
151+
"",
152+
"Output exactly in this format:",
153+
"",
154+
"Improved prompt:",
155+
"\"\"\"",
156+
"<full revised prompt>",
157+
"\"\"\"",
158+
"",
159+
"Explanation:",
160+
"- <short bullet explaining the main failure pattern>",
161+
"- <short bullet explaining the key prompt changes>",
162+
"- <short bullet explaining why the revision should generalize better>",
163+
"",
164+
"Original prompt:",
165+
"\"\"\"",
166+
originalPrompt,
167+
"\"\"\"",
168+
"",
169+
"Test results:",
170+
"\"\"\"",
171+
testResults,
172+
"\"\"\""
173+
].join("\n");
174+
}
175+
176+
export async function writePromptImprovementBrief(suite: EvalSuiteResult) {
177+
const briefPath =
178+
process.env.ATLAS_PROMPT_IMPROVEMENT_PATH ??
179+
path.resolve(process.cwd(), `${suite.suiteName}.prompt-improvement.md`);
180+
181+
await mkdir(path.dirname(briefPath), {
182+
recursive: true
183+
});
184+
await writeFile(briefPath, buildPromptImprovementPrompt(suite), "utf8");
185+
186+
return briefPath;
187+
}
188+
189+
export async function writePromptImprovementBriefsForFailures(
190+
suites: EvalSuiteResult[]
191+
) {
192+
const failedSuites = suites.filter((suite) => suite.failed > 0);
193+
194+
const briefPaths = await Promise.all(
195+
failedSuites.map((suite) => writePromptImprovementBrief(suite))
196+
);
197+
198+
return briefPaths;
199+
}

packages/integrations/src/manual/turn-router.eval.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22

3-
import { ensureManualEvalEnv } from "./shared";
3+
import {
4+
ensureManualEvalEnv,
5+
writePromptImprovementBrief,
6+
writeSuiteEvalReport
7+
} from "./shared";
48
import { runTurnRouterEvalSuite } from "./turn-router.eval-suite";
59

610
beforeAll(() => {
@@ -10,6 +14,9 @@ beforeAll(() => {
1014
describe.sequential("manual turn router eval", () => {
1115
it("checks curated routing cases against the live OpenAI prompt", async () => {
1216
const suite = await runTurnRouterEvalSuite();
17+
const reportPath = await writeSuiteEvalReport(suite);
18+
const briefPath =
19+
suite.failed > 0 ? await writePromptImprovementBrief(suite) : null;
1320

1421
console.table(
1522
suite.cases.map((testCase) => ({
@@ -22,6 +29,10 @@ describe.sequential("manual turn router eval", () => {
2229
error: testCase.error ?? ""
2330
}))
2431
);
32+
console.log(`Manual eval report written to ${reportPath}`);
33+
if (briefPath) {
34+
console.log(`Prompt improvement brief written to ${briefPath}`);
35+
}
2536

2637
expect(suite.failed).toBe(0);
2738
}, 60_000);

0 commit comments

Comments
 (0)