From 741dc9289ec2b4d6042a74eddf471a957a96adc6 Mon Sep 17 00:00:00 2001 From: Sushant Gautam Date: Tue, 13 Oct 2026 22:13:18 +0200 Subject: [PATCH] feat(seed): seed a versioned system prompt the demo agent uses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup_local now creates a v1 SystemPrompt at /prompts/ ("Acme Retail Support Refund Assistant") and points the demo "Support Refund Assistant" agent's defaults.system_prompt at that exact version content, so the agent editor's picker auto-preselects it (name radio + summary "v1 · latest", textarea pre-filled). Idempotent: re-runs reuse the existing same-named prompt. Also keeps the name-row radio in sync with the selected version in both pickers so a server-side latest preselect shows the name radio checked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infra/seed.py | 26 ++++++++++++++++++++++++- infra/tests/test_seed_demo_agent.py | 19 ++++++++++++++++++ templates/agents/agent_detail.html | 7 ++++++- templates/partials/run_settings_js.html | 7 ++++++- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/infra/seed.py b/infra/seed.py index 92251a33..d147a76c 100644 --- a/infra/seed.py +++ b/infra/seed.py @@ -428,14 +428,38 @@ def seed_demo_agent(project, user, log=logger.info) -> dict[str, str]: tool.external_id = tool_external_id tool.save(update_fields=["external_id", "updated_at"]) + # Seed a predefined system prompt (v1) at /prompts/ using the demo agent's + # instructions as its v1 content, then point the agent at that same text. + # Because the agent's text matches the version exactly, the agent editor's + # picker auto-preselects it. Idempotent: reuses an existing same-named prompt. + from prompts.models import SystemPrompt + from prompts.services import create_prompt + meta = DEMO_AGENT + # Seed a predefined system prompt (v1) at /prompts/ using the demo agent's + # instructions as its v1 content, then point the agent at that same text. + # Because the agent's text matches the version exactly, the agent editor's + # picker auto-preselects it. Idempotent: reuses an existing same-named prompt. + prompt_name = "Acme Retail Support Refund Assistant" + prompt = SystemPrompt.objects.filter(project=project, name=prompt_name).first() + if prompt is None or not prompt.latest: + prompt = create_prompt( + project=project, + name=prompt_name, + description="Instructions for the demo Support Refund Assistant agent.", + content=meta["system_prompt"], + note="Seeded", + user=user, + ) + log(f"Seeded system prompt '{prompt.name}' (v1).") + agent, created = Agent.objects.get_or_create( project=project, name=meta["name"], defaults={ "description": meta["description"], "base_model": base_model, - "system_prompt": meta["system_prompt"], + "system_prompt": prompt.latest.content, "retrieval_settings": { "search_mode": "hybrid", "top_k": 5, diff --git a/infra/tests/test_seed_demo_agent.py b/infra/tests/test_seed_demo_agent.py index f2da4556..072b1629 100644 --- a/infra/tests/test_seed_demo_agent.py +++ b/infra/tests/test_seed_demo_agent.py @@ -69,6 +69,25 @@ def test_is_idempotent(self): self.assertEqual(Tool.objects.filter(project=self.project).count(), 1) self.assertEqual(Agent.objects.get(project=self.project).retrieval_settings["top_k"], 5) + def test_seeds_a_versioned_prompt_the_agent_uses(self): + from prompts.models import SystemPrompt + + seed_demo_agent(self.project, self.user) + + # A predefined prompt (v1) exists at /prompts/. + prompt = SystemPrompt.objects.get( + project=self.project, name="Acme Retail Support Refund Assistant" + ) + self.assertEqual(prompt.latest.version, 1) + # The agent's instructions match that version exactly, so the picker + # auto-preselects it and deletion of the prompt is blocked while in use. + agent = Agent.objects.get(project=self.project, name=DEMO_AGENT["name"]) + self.assertEqual(agent.system_prompt, prompt.latest.content) + # Idempotent: re-running does not create a second prompt or bump v2. + seed_demo_agent(self.project, self.user) + self.assertEqual(SystemPrompt.objects.filter(project=self.project).count(), 1) + self.assertEqual(prompt.versions.count(), 1) + def test_skips_when_no_model_exists(self): self.model.delete() status = seed_demo_agent(self.project, self.user) diff --git a/templates/agents/agent_detail.html b/templates/agents/agent_detail.html index 0e3ecdbd..7b72c74d 100644 --- a/templates/agents/agent_detail.html +++ b/templates/agents/agent_detail.html @@ -471,15 +471,20 @@

Expose r.checked); fieldset.querySelectorAll('[data-prompt-row]').forEach(row => { const rs = [...row.querySelectorAll('input[name="system_prompt_version"]')]; const rowChecked = rs.find(r => r.checked); + const nameRadio = row.querySelector('[data-row-radio]'); const summary = row.querySelector('[data-prompt-summary]'); + if (!rs.length) return; + // The name radio means "latest": keep it checked whenever the row's pick + // is the latest version, regardless of which element the browser marked. + if (nameRadio) nameRadio.checked = !!rowChecked && rowChecked.hasAttribute('data-latest'); if (!summary) return; if (rowChecked) summary.textContent = `v${rowChecked.dataset.version}` + (rowChecked.hasAttribute('data-latest') ? ' · latest' : ' · older version'); else { const latest = rs.find(r => 'latest' in r.dataset); summary.textContent = latest ? `v${latest.dataset.version} · latest` : ''; } }); + const checked = allRadios().find(r => r.checked); if (checked && ta.value !== checked.dataset.content) ta.value = checked.dataset.content || ''; } allRadios().forEach(r => r.addEventListener('change', syncAll)); diff --git a/templates/partials/run_settings_js.html b/templates/partials/run_settings_js.html index b83f1d0d..f43c6ad5 100644 --- a/templates/partials/run_settings_js.html +++ b/templates/partials/run_settings_js.html @@ -16,11 +16,15 @@ const allRadios = () => [...fieldset.querySelectorAll('input[name="system_prompt_version"]')]; function syncAll() { - const checked = allRadios().find(r => r.checked); fieldset.querySelectorAll('[data-prompt-row]').forEach(row => { const rs = [...row.querySelectorAll('input[name="system_prompt_version"]')]; const rowChecked = rs.find(r => r.checked); + const nameRadio = row.querySelector('[data-row-radio]'); const summary = row.querySelector('[data-prompt-summary]'); + if (!rs.length) return; + // The name radio means "latest". It stays checked whenever the row's pick + // is the latest version, regardless of which element the browser marked. + if (nameRadio) nameRadio.checked = !!rowChecked && rowChecked.hasAttribute('data-latest'); if (!summary) return; if (rowChecked) summary.textContent = `v${rowChecked.dataset.version}` + (rowChecked.hasAttribute('data-latest') ? ' · latest' : ' · older version'); else { @@ -28,6 +32,7 @@ summary.textContent = latest ? `v${latest.dataset.version} · latest` : ''; } }); + const checked = allRadios().find(r => r.checked); if (checked && ta.value !== checked.dataset.content) ta.value = checked.dataset.content || ''; }