Skip to content

Commit eeb8e59

Browse files
abossardCopilot
andcommitted
feat: add 'Improve my Prompt' button to Agent Fabric
LLM-powered prompt improvement following 2025 best practices: - Backend: /api/workbench/improve-prompt endpoint + service method that rewrites prompts with clear role, goals, numbered steps, tool references, output format, and constraints - Frontend: '✨ Improve my Prompt' button below the system prompt textarea, disabled when empty, replaces prompt with improved version - 4 Playwright E2E tests with before/after screenshots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6531c0f commit eeb8e59

5 files changed

Lines changed: 225 additions & 0 deletions

File tree

backend/agent_builder/routes.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,21 @@ async def workbench_suggest_schema():
166166
return _error_response(exc)
167167

168168

169+
@agent_builder_bp.route("/api/workbench/improve-prompt", methods=["POST"])
170+
async def workbench_improve_prompt():
171+
"""Improve a system prompt using LLM best practices."""
172+
try:
173+
data = await request.get_json()
174+
result = await _workbench_service.improve_prompt(
175+
name=data.get("name", ""),
176+
description=data.get("description", ""),
177+
system_prompt=data.get("system_prompt", ""),
178+
)
179+
return jsonify(result), 200
180+
except Exception as exc:
181+
return _error_response(exc)
182+
183+
169184
# ---------------------------------------------------------------------------
170185
# Agent CRUD
171186
# ---------------------------------------------------------------------------

backend/agent_builder/service.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,44 @@
4545
# CALCULATIONS (pure — no side effects, no I/O)
4646
# ============================================================================
4747

48+
def _build_improve_prompt_request(
49+
name: str, description: str, system_prompt: str,
50+
all_tools: list[dict[str, Any]],
51+
) -> str:
52+
"""Build the LLM prompt for improving an agent system prompt. Pure calculation."""
53+
context_parts = []
54+
if name:
55+
context_parts.append(f"Agent name: {name}")
56+
if description:
57+
context_parts.append(f"Description: {description}")
58+
agent_context = "\n".join(context_parts) if context_parts else "(not provided)"
59+
60+
tool_descriptions = "\n".join(
61+
f" - {t['name']}: {t['description'][:120]}" for t in all_tools
62+
)
63+
64+
return (
65+
"You are an expert prompt engineer. Improve the following system prompt for an AI agent.\n\n"
66+
"## Agent Context\n"
67+
f"{agent_context}\n\n"
68+
"## Available Tools\n"
69+
f"{tool_descriptions}\n\n"
70+
"## Current Prompt\n"
71+
f"{system_prompt}\n\n"
72+
"## Instructions\n"
73+
"Rewrite the prompt following these modern best practices:\n"
74+
"1. Start with a clear role definition (\"You are a...\").\n"
75+
"2. State the goal in one sentence.\n"
76+
"3. List concrete steps the agent should follow (numbered).\n"
77+
"4. Reference specific tool names the agent should use and when.\n"
78+
"5. Define the expected output format clearly.\n"
79+
"6. Add constraints: what NOT to do, edge cases to handle.\n"
80+
"7. Keep it concise — reasoning models work best with clear, direct instructions.\n"
81+
"8. Use structured sections (##) for readability.\n\n"
82+
"Return ONLY the improved prompt text. No wrapping, no explanation, no markdown fences."
83+
)
84+
85+
4886
def _build_suggest_prompt(
4987
name: str, description: str, system_prompt: str,
5088
all_tools: list[dict[str, Any]], tool_names_list: list[str],
@@ -226,6 +264,35 @@ async def suggest_schema(
226264

227265
return _parse_suggest_response(raw, tool_names_list)
228266

267+
# ------------------------------------------------------------------
268+
# Prompt improvement (LLM call)
269+
# ------------------------------------------------------------------
270+
271+
async def improve_prompt(
272+
self,
273+
name: str,
274+
description: str,
275+
system_prompt: str,
276+
) -> dict[str, str]:
277+
"""
278+
Ask the LLM to improve an agent's system prompt.
279+
280+
Action: calls LLM. Returns { improved_prompt: str }.
281+
"""
282+
all_tools = self.list_tools()
283+
prompt = _build_improve_prompt_request(name, description, system_prompt, all_tools)
284+
285+
from langchain_core.messages import HumanMessage
286+
response = await self.llm.ainvoke([HumanMessage(content=prompt)])
287+
improved = (response.content or "").strip()
288+
289+
# Strip markdown fences if LLM wraps it anyway
290+
if improved.startswith("```"):
291+
lines = improved.split("\n")
292+
improved = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:])
293+
294+
return {"improved_prompt": improved}
295+
229296
# ------------------------------------------------------------------
230297
# Validation helpers (calculations)
231298
# ------------------------------------------------------------------

frontend/src/features/workbench/AgentCreateForm.jsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { useEffect, useState } from 'react'
1515
import {
1616
createWorkbenchAgent,
17+
improvePrompt,
1718
suggestOutputSchema,
1819
updateWorkbenchAgent,
1920
} from '../../services/api'
@@ -164,6 +165,7 @@ export default function AgentCreateForm({ tools, onAgentCreated, initialData })
164165
return ''
165166
})
166167
const [suggestingSchema, setSuggestingSchema] = useState(false)
168+
const [improvingPrompt, setImprovingPrompt] = useState(false)
167169
const [submitting, setSubmitting] = useState(false)
168170
const [error, setError] = useState('')
169171

@@ -247,6 +249,25 @@ export default function AgentCreateForm({ tools, onAgentCreated, initialData })
247249
}
248250
}
249251

252+
const handleImprovePrompt = async () => {
253+
setImprovingPrompt(true)
254+
setError('')
255+
try {
256+
const resp = await improvePrompt({
257+
name: formData.name.trim(),
258+
description: formData.description.trim(),
259+
systemPrompt: formData.systemPrompt.trim(),
260+
})
261+
if (resp.improved_prompt) {
262+
setFormData((prev) => ({ ...prev, systemPrompt: resp.improved_prompt }))
263+
}
264+
} catch (err) {
265+
setError(err?.message || 'Failed to improve prompt')
266+
} finally {
267+
setImprovingPrompt(false)
268+
}
269+
}
270+
250271
const handleSubmit = async () => {
251272
setError('')
252273
if (!validateForm()) return
@@ -406,6 +427,13 @@ export default function AgentCreateForm({ tools, onAgentCreated, initialData })
406427
/>
407428
</Field>
408429
{fieldErrors.systemPrompt && <Text>{fieldErrors.systemPrompt}</Text>}
430+
<Button
431+
data-testid="workbench-improve-prompt-button"
432+
disabled={improvingPrompt || !formData.systemPrompt.trim()}
433+
onClick={handleImprovePrompt}
434+
>
435+
{improvingPrompt ? '✨ Improving...' : '✨ Improve my Prompt'}
436+
</Button>
409437
<Field label="Output schema" hint="Define the structured output format with display widgets">
410438
<SchemaEditor
411439
value={outputSchema}

frontend/src/services/api.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,17 @@ export async function suggestOutputSchema({ name = "", description = "", systemP
387387
});
388388
}
389389

390+
export async function improvePrompt({ name = "", description = "", systemPrompt = "" } = {}) {
391+
return fetchJSON(`${API_BASE_URL}/workbench/improve-prompt`, {
392+
method: "POST",
393+
body: JSON.stringify({
394+
name,
395+
description,
396+
system_prompt: systemPrompt,
397+
}),
398+
});
399+
}
400+
390401
export async function updateWorkbenchAgent(agentId, agentData) {
391402
return fetchJSON(`${API_BASE_URL}/workbench/agents/${agentId}`, {
392403
method: "PUT",

tests/e2e/improve-prompt.spec.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
const APP_URL = process.env.E2E_APP_URL || "http://localhost:3001";
4+
5+
async function goToCreateTab(page) {
6+
await page.goto(`${APP_URL}/workbench`, { waitUntil: "load" });
7+
await expect(page.getByTestId("workbench-page-title")).toBeVisible();
8+
await page.getByTestId("workbench-tab-create").click();
9+
await expect(page.getByTestId("workbench-create-agent-button")).toBeVisible();
10+
}
11+
12+
test.describe("Improve Prompt button", () => {
13+
test("button is visible and disabled when prompt is empty", async ({ page }) => {
14+
await goToCreateTab(page);
15+
16+
const btn = page.getByTestId("workbench-improve-prompt-button");
17+
await expect(btn).toBeVisible();
18+
await expect(btn).toBeDisabled();
19+
});
20+
21+
test("button enables when prompt has text", async ({ page }) => {
22+
await goToCreateTab(page);
23+
24+
const btn = page.getByTestId("workbench-improve-prompt-button");
25+
await expect(btn).toBeDisabled();
26+
27+
await page.getByTestId("workbench-agent-system-prompt-input").fill("list all tickets");
28+
await expect(btn).toBeEnabled();
29+
});
30+
31+
test("button works with mocked API", async ({ page }) => {
32+
// Mock the improve-prompt endpoint
33+
await page.route("**/api/workbench/improve-prompt", async (route) => {
34+
await route.fulfill({
35+
status: 200,
36+
contentType: "application/json",
37+
body: JSON.stringify({
38+
improved_prompt:
39+
"You are a ticket analysis agent.\n\n## Goal\nList and summarize all open tickets.\n\n## Steps\n1. Use csv_list_tickets to retrieve all tickets.\n2. Filter by status.\n3. Produce a summary.\n\n## Output\nReturn a markdown summary.",
40+
}),
41+
});
42+
});
43+
44+
await goToCreateTab(page);
45+
46+
// Type a basic prompt
47+
const promptInput = page.getByTestId("workbench-agent-system-prompt-input");
48+
await promptInput.fill("list all tickets");
49+
50+
// Click improve
51+
const btn = page.getByTestId("workbench-improve-prompt-button");
52+
await btn.click();
53+
54+
// Prompt should be replaced with the improved version
55+
await expect(promptInput).toHaveValue(/You are a ticket analysis agent/);
56+
await expect(promptInput).toHaveValue(/## Steps/);
57+
});
58+
59+
test("template + improve workflow screenshot", async ({ page }) => {
60+
// Mock the improve endpoint
61+
await page.route("**/api/workbench/improve-prompt", async (route) => {
62+
await route.fulfill({
63+
status: 200,
64+
contentType: "application/json",
65+
body: JSON.stringify({
66+
improved_prompt:
67+
"You are an expert Knowledge Base author specializing in IT support.\n\n## Goal\nCreate a comprehensive, reusable KBA from patterns found across related tickets.\n\n## Steps\n1. Use csv_search_tickets_with_details to find tickets matching the user's topic.\n2. Identify common symptoms across tickets.\n3. Determine the root cause from resolution notes.\n4. Write step-by-step resolution instructions.\n\n## Output Format\n- **Title**: Concise, searchable\n- **Symptoms**: Bullet list of what users experience\n- **Cause**: Root cause explanation\n- **Resolution**: Numbered steps\n- **Related Tickets**: INC numbers analyzed\n\n## Constraints\n- Only reference tickets you actually retrieved.\n- Do not fabricate resolution steps.",
68+
}),
69+
});
70+
});
71+
72+
await goToCreateTab(page);
73+
74+
// Select a template first
75+
const templateSelect = page.getByTestId("workbench-template-select");
76+
await templateSelect.click();
77+
await page.getByText("KBA from Multiple Tickets").click();
78+
79+
// Wait for form to populate
80+
await expect(
81+
page.getByTestId("workbench-agent-system-prompt-input"),
82+
).not.toHaveValue("");
83+
84+
// Take screenshot before improvement
85+
await page.screenshot({
86+
path: "screenshot-improve-prompt-before.png",
87+
fullPage: true,
88+
});
89+
90+
// Click improve
91+
await page.getByTestId("workbench-improve-prompt-button").click();
92+
93+
// Wait for improved prompt
94+
await expect(
95+
page.getByTestId("workbench-agent-system-prompt-input"),
96+
).toHaveValue(/## Steps/);
97+
98+
// Take screenshot after improvement
99+
await page.screenshot({
100+
path: "screenshot-improve-prompt-after.png",
101+
fullPage: true,
102+
});
103+
});
104+
});

0 commit comments

Comments
 (0)