Przeglądaj źródła

feat: consolidate chat agent prompt list

zhenyu.hu 3 tygodni temu
rodzic
commit
7a4647ac63

+ 25 - 0
docs/plans/todo-24-chat-agent-prompt-list.md

@@ -0,0 +1,25 @@
+# Todo 24 ChatAgent Prompt List Plan
+
+**Status:** done
+
+## Goal
+
+Put all ChatAgent prompts into one ordered list so configuration is not split between a standalone ChatAgent system prompt, generated event instructions, and prompt-config system prompts.
+
+## Why
+
+ChatAgent depends on the event-instruction prompt to know when and how to emit event names. Keeping that prompt outside the editable prompt sequence makes ordering unclear and makes debugging prompt effects harder.
+
+## Scope
+
+- Replace the standalone ChatAgent System Prompt field and Available Events Prompt preview with one prompt list.
+- Include the generated EventAgent event-rule prompt as a fixed item in that list.
+- Allow custom prompt items to be added, deleted, and reordered by dragging.
+- Serialize `system_prompts` from the ordered list and stop sending a separate `chat_agent.system_prompt`.
+- Preserve existing prompt-set save/load behavior for user-created prompts.
+
+## Verification
+
+- Static tests failed before implementation and pass after implementation.
+- `uv run pytest` passed: 51 tests, 1 existing Starlette deprecation warning.
+- Browser check confirmed the ChatAgent config shows one prompt list, supports add/delete/reorder controls, includes the generated event-rule prompt in the list, and no longer shows separate ChatAgent prompt/event-preview fields.

+ 1 - 0
docs/plans/todos.md

@@ -50,3 +50,4 @@
 | 21 | done | `docs/plans/todo-21-event-agent-context-summary.md` | Make EventAgent config part of parameter generation context and enqueue an aggregated tool-result summary for the next ChatAgent round. | `uv run pytest` passes (`50 passed`, one existing Starlette deprecation warning). |
 | 22 | done | `docs/plans/todo-22-text-event-protocol.md` | Replace ChatAgent tool calls with streamed text event parsing, inject generated event descriptions as a system message, and harden Agent config buttons. | `uv run pytest` passes (`50 passed`, one existing Starlette deprecation warning), plus local browser click check. |
 | 23 | done | `docs/plans/todo-23-agent-prompt-and-event-llm.md` | Move Prompt Config into ChatAgent config, show available-event prompt by default, and make EventAgent use LLM tool calls to generate tool arguments. | `uv run pytest` passes (`51 passed`, one existing Starlette deprecation warning); changed Python modules compile; local server returned page/static assets/tools API. |
+| 24 | done | `docs/plans/todo-24-chat-agent-prompt-list.md` | Put all ChatAgent prompts, including generated EventAgent event rules, into one ordered prompt list with add/delete/drag controls. | `uv run pytest` passes (`51 passed`, one existing Starlette deprecation warning), plus local browser prompt-list check. |

+ 182 - 34
src/agent_lab/presentation/static/app.js

@@ -22,10 +22,9 @@ let elapsedTimer = 0;
 let hasBackendRoundStats = false;
 let pendingEnabledTools = null;
 let availableTools = [];
-let generatedChatPrompt = "";
 
 bindClick("#add-system-prompt", () => {
-  systemPrompts.append(createSystemPrompt(""));
+  systemPrompts.append(createSystemPrompt({ content: "" }));
 });
 
 bindClick("#add-pre-message", () => {
@@ -55,6 +54,7 @@ chatForm.addEventListener("submit", (event) => {
   runDebugSession();
 });
 
+initializePromptList();
 refreshPromptSetSelector();
 loadTools();
 
@@ -91,7 +91,7 @@ function renderTools(tools) {
   if (!tools.length) {
     toolList.textContent = "No tools available";
     updateToolCount();
-    updateChatSystemPromptDefault(tools);
+    updateEventPromptItem(tools);
     return;
   }
 
@@ -110,7 +110,7 @@ function renderTools(tools) {
     checkbox.checked = true;
     checkbox.addEventListener("change", () => {
       updateToolCount();
-      updateChatSystemPromptDefault(availableTools);
+      updateEventPromptItem(availableTools);
     });
 
     label.append(checkbox, ` ${tool.name}`);
@@ -132,7 +132,7 @@ function renderTools(tools) {
     pendingEnabledTools = null;
   }
   updateToolCount();
-  updateChatSystemPromptDefault(tools);
+  updateEventPromptItem(tools);
 }
 
 function formatRequiredParameters(tool) {
@@ -147,7 +147,7 @@ function setAllTools(checked) {
     checkbox.checked = checked;
   });
   updateToolCount();
-  updateChatSystemPromptDefault(availableTools);
+  updateEventPromptItem(availableTools);
 }
 
 function updateToolCount() {
@@ -208,8 +208,9 @@ function deletePromptSet() {
 
 function buildPromptSet() {
   return {
-    system_prompts: [...document.querySelectorAll(".system-prompt")]
-      .map((input) => input.value.trim())
+    prompt_items: buildPromptItems(),
+    system_prompts: buildCustomPromptItems()
+      .map((item) => item.content)
       .filter(Boolean),
     pre_messages: [...document.querySelectorAll(".pre-message")]
       .map((row) => ({
@@ -221,7 +222,7 @@ function buildPromptSet() {
       model: document.querySelector("#model").value.trim(),
       temperature: Number(document.querySelector("#temperature").value),
       max_tokens: Number(document.querySelector("#max-tokens").value),
-      system_prompt: document.querySelector("#chat-system-prompt").value.trim(),
+      system_prompt: "",
       extra_body: buildExtraBody("chat"),
     },
     event_agent: {
@@ -237,14 +238,13 @@ function buildPromptSet() {
 }
 
 function restorePromptSet(promptSet) {
-  restoreSystemPrompts(promptSet.system_prompts || []);
+  restoreSystemPrompts(promptSet.prompt_items || promptSet.system_prompts || []);
   restorePreMessages(promptSet.pre_messages || []);
 
   const chatAgent = promptSet.chat_agent || {};
   document.querySelector("#model").value = chatAgent.model || "";
   document.querySelector("#temperature").value = chatAgent.temperature ?? 0.2;
   document.querySelector("#max-tokens").value = chatAgent.max_tokens ?? 800;
-  document.querySelector("#chat-system-prompt").value = chatAgent.system_prompt || "";
   restoreExtraBody("chat", chatAgent.extra_body);
 
   const eventAgent = promptSet.event_agent || {};
@@ -256,7 +256,7 @@ function restorePromptSet(promptSet) {
   document.querySelector("#max-event-loops").value = eventAgent.max_event_loops ?? 1;
   const enabledTools = eventAgent.enabled_tools || [];
   pendingEnabledTools = applySelectedTools(enabledTools) ? null : enabledTools;
-  updateChatSystemPromptDefault(availableTools);
+  updateEventPromptItem(availableTools);
 }
 
 function buildExtraBody(prefix) {
@@ -286,10 +286,11 @@ function restoreExtraBody(prefix, extraBody = {}) {
 
 function restoreSystemPrompts(prompts) {
   systemPrompts.textContent = "";
-  const values = prompts.length ? prompts : [""];
-  values.forEach((prompt) => {
+  const promptItems = normalizePromptItems(prompts);
+  promptItems.forEach((prompt) => {
     systemPrompts.append(createSystemPrompt(prompt));
   });
+  ensureEventPromptItem();
 }
 
 function restorePreMessages(messages) {
@@ -299,12 +300,59 @@ function restorePreMessages(messages) {
   });
 }
 
-function createSystemPrompt(value) {
+function createSystemPrompt(prompt) {
+  const promptItem = normalizePromptItem(prompt);
+  const item = document.createElement("div");
+  item.className = promptItem.prompt_kind === "event_agent_system"
+    ? "prompt-item event-prompt"
+    : "prompt-item";
+  item.dataset.promptKind = promptItem.prompt_kind;
+  item.draggable = true;
+
+  const head = document.createElement("div");
+  head.className = "prompt-item-head";
+
+  const title = document.createElement("span");
+  title.textContent = promptItem.prompt_kind === "event_agent_system"
+    ? "EventAgent System Prompt"
+    : "Custom Prompt";
+
+  const actions = document.createElement("div");
+  actions.className = "prompt-actions";
+
+  const moveUp = document.createElement("button");
+  moveUp.className = "move-prompt-up";
+  moveUp.type = "button";
+  moveUp.textContent = "Up";
+  moveUp.addEventListener("click", () => movePromptItem(item, -1));
+
+  const moveDown = document.createElement("button");
+  moveDown.className = "move-prompt-down";
+  moveDown.type = "button";
+  moveDown.textContent = "Down";
+  moveDown.addEventListener("click", () => movePromptItem(item, 1));
+
+  actions.append(moveUp, moveDown);
+  if (promptItem.prompt_kind !== "event_agent_system") {
+    const deleteButton = document.createElement("button");
+    deleteButton.className = "delete-system-prompt";
+    deleteButton.type = "button";
+    deleteButton.textContent = "Delete";
+    deleteButton.addEventListener("click", () => item.remove());
+    actions.append(deleteButton);
+  }
+
+  head.append(title, actions);
+
   const textarea = document.createElement("textarea");
   textarea.className = "system-prompt";
-  textarea.rows = 4;
-  textarea.value = value;
-  return textarea;
+  textarea.rows = promptItem.prompt_kind === "event_agent_system" ? 7 : 4;
+  textarea.value = promptItem.content;
+  textarea.readOnly = promptItem.prompt_kind === "event_agent_system";
+
+  item.append(head, textarea);
+  bindPromptDrag(item);
+  return item;
 }
 
 function createPreMessage(message) {
@@ -325,10 +373,66 @@ function applySelectedTools(toolNames) {
     checkbox.checked = selected.has(checkbox.value);
   });
   updateToolCount();
-  updateChatSystemPromptDefault(availableTools);
+  updateEventPromptItem(availableTools);
   return true;
 }
 
+function initializePromptList() {
+  [...systemPrompts.querySelectorAll(".prompt-item")].forEach((item) => {
+    bindPromptItemControls(item);
+    bindPromptDrag(item);
+  });
+  ensureEventPromptItem();
+}
+
+function buildPromptItems() {
+  return [...systemPrompts.querySelectorAll(".prompt-item")]
+    .map((item) => ({
+      prompt_kind: item.dataset.promptKind || "custom",
+      content: item.querySelector(".system-prompt").value.trim(),
+    }))
+    .filter((item) => item.prompt_kind === "event_agent_system" || item.content);
+}
+
+function buildCustomPromptItems() {
+  return buildPromptItems().filter((item) => item.prompt_kind !== "event_agent_system");
+}
+
+function collectSystemPrompts() {
+  return buildPromptItems()
+    .map((item) => item.content)
+    .filter(Boolean);
+}
+
+function normalizePromptItems(prompts) {
+  const items = Array.isArray(prompts) ? prompts : [];
+  const normalized = items
+    .map((prompt) => normalizePromptItem(prompt))
+    .filter((prompt) => prompt.prompt_kind === "event_agent_system" || prompt.content);
+  if (!normalized.some((prompt) => prompt.prompt_kind === "event_agent_system")) {
+    normalized.push({ prompt_kind: "event_agent_system", content: "" });
+  }
+  if (!normalized.some((prompt) => prompt.prompt_kind === "custom")) {
+    normalized.unshift({ prompt_kind: "custom", content: "" });
+  }
+  return normalized;
+}
+
+function normalizePromptItem(prompt) {
+  if (prompt && typeof prompt === "object" && !Array.isArray(prompt)) {
+    return {
+      prompt_kind: prompt.prompt_kind === "event_agent_system"
+        ? "event_agent_system"
+        : "custom",
+      content: prompt.content || "",
+    };
+  }
+  return {
+    prompt_kind: "custom",
+    content: prompt || "",
+  };
+}
+
 function buildAvailableEventsPrompt(tools) {
   const eventLines = tools.map((tool) => `- ${tool.name}: ${tool.description || "No description"}`);
   if (!eventLines.length) {
@@ -346,24 +450,70 @@ function buildAvailableEventsPrompt(tools) {
   ].join("\n");
 }
 
-function updateChatSystemPromptDefault(tools) {
+function updateEventPromptItem(tools) {
+  const eventPrompt = ensureEventPromptItem();
   const selected = new Set(selectedTools());
   const prompt = buildAvailableEventsPrompt(
     tools.filter((tool) => selected.has(tool.name)),
   );
-  const systemPrompt = document.querySelector("#chat-system-prompt");
-  const preview = document.querySelector("#chat-event-prompt-preview");
-  if (preview) {
-    preview.value = prompt;
+  eventPrompt.querySelector(".system-prompt").value = prompt;
+}
+
+function ensureEventPromptItem() {
+  const existing = systemPrompts.querySelector('[data-prompt-kind="event_agent_system"]');
+  if (existing) {
+    return existing;
   }
-  if (!systemPrompt) {
-    generatedChatPrompt = prompt;
-    return;
+
+  const item = createSystemPrompt({
+    prompt_kind: "event_agent_system",
+    content: "",
+  });
+  systemPrompts.append(item);
+  return item;
+}
+
+function bindPromptItemControls(item) {
+  const moveUp = item.querySelector(".move-prompt-up");
+  if (moveUp) {
+    moveUp.addEventListener("click", () => movePromptItem(item, -1));
+  }
+  const moveDown = item.querySelector(".move-prompt-down");
+  if (moveDown) {
+    moveDown.addEventListener("click", () => movePromptItem(item, 1));
+  }
+  const deleteButton = item.querySelector(".delete-system-prompt");
+  if (deleteButton) {
+    deleteButton.addEventListener("click", () => item.remove());
   }
-  if (!systemPrompt.value.trim() || systemPrompt.value === generatedChatPrompt) {
-    systemPrompt.value = prompt;
+}
+
+function movePromptItem(item, direction) {
+  if (direction < 0 && item.previousElementSibling) {
+    systemPrompts.insertBefore(item, item.previousElementSibling);
+  }
+  if (direction > 0 && item.nextElementSibling) {
+    systemPrompts.insertBefore(item.nextElementSibling, item);
   }
-  generatedChatPrompt = prompt;
+}
+
+function bindPromptDrag(item) {
+  item.addEventListener("dragstart", () => {
+    item.classList.add("dragging");
+  });
+  item.addEventListener("dragend", () => {
+    item.classList.remove("dragging");
+  });
+  item.addEventListener("dragover", (event) => {
+    event.preventDefault();
+    const dragging = systemPrompts.querySelector(".prompt-item.dragging");
+    if (!dragging || dragging === item) {
+      return;
+    }
+    const rect = item.getBoundingClientRect();
+    const afterMidpoint = event.clientY > rect.top + rect.height / 2;
+    systemPrompts.insertBefore(dragging, afterMidpoint ? item.nextSibling : item);
+  });
 }
 
 function refreshPromptSetSelector(selectedName = "") {
@@ -453,9 +603,7 @@ function runDebugSession() {
 function buildRequest(submittedMessage = userMessage.value) {
   return {
     user_message: submittedMessage,
-    system_prompts: [...document.querySelectorAll(".system-prompt")]
-      .map((input) => input.value.trim())
-      .filter(Boolean),
+    system_prompts: collectSystemPrompts(),
     pre_messages: [...document.querySelectorAll(".pre-message")]
       .map((row) => ({
         role: row.querySelector(".pre-role").value,
@@ -466,7 +614,7 @@ function buildRequest(submittedMessage = userMessage.value) {
       model: document.querySelector("#model").value.trim(),
       temperature: Number(document.querySelector("#temperature").value),
       max_tokens: Number(document.querySelector("#max-tokens").value),
-      system_prompt: document.querySelector("#chat-system-prompt").value.trim(),
+      system_prompt: "",
       extra_body: buildExtraBody("chat"),
     },
     event_agent: {

+ 24 - 6
src/agent_lab/presentation/static/index.html

@@ -64,8 +64,6 @@
           <label>Model <input id="model" placeholder="Backend default" /></label>
           <label>Temperature <input id="temperature" type="number" min="0" max="2" step="0.1" value="0.2" /></label>
           <label>Max Tokens <input id="max-tokens" type="number" min="1" step="1" value="800" /></label>
-          <label>System Prompt <textarea id="chat-system-prompt" rows="5" placeholder="Optional ChatAgent system prompt"></textarea></label>
-          <label>Available Events Prompt <textarea id="chat-event-prompt-preview" rows="7" readonly></textarea></label>
           <fieldset>
             <legend>Extra Body</legend>
             <label class="checkbox"><input id="chat-thinking-disabled" type="checkbox" checked /> Disable thinking</label>
@@ -88,11 +86,31 @@
 
         <section>
           <div class="section-head">
-            <h2>System Prompts</h2>
+            <h2>Prompt List</h2>
             <button id="add-system-prompt" type="button">Add</button>
           </div>
-          <div id="system-prompts" class="stack">
-            <textarea class="system-prompt" rows="4">You are a debugging assistant.</textarea>
+          <div id="system-prompts" class="prompt-list stack">
+            <div class="prompt-item" draggable="true" data-prompt-kind="custom">
+              <div class="prompt-item-head">
+                <span>Custom Prompt</span>
+                <div class="prompt-actions">
+                  <button class="move-prompt-up" type="button">Up</button>
+                  <button class="move-prompt-down" type="button">Down</button>
+                  <button class="delete-system-prompt" type="button">Delete</button>
+                </div>
+              </div>
+              <textarea class="system-prompt" rows="4">You are a debugging assistant.</textarea>
+            </div>
+            <div class="prompt-item event-prompt" draggable="true" data-prompt-kind="event_agent_system">
+              <div class="prompt-item-head">
+                <span>EventAgent System Prompt</span>
+                <div class="prompt-actions">
+                  <button class="move-prompt-up" type="button">Up</button>
+                  <button class="move-prompt-down" type="button">Down</button>
+                </div>
+              </div>
+              <textarea class="system-prompt" rows="7" readonly></textarea>
+            </div>
           </div>
         </section>
 
@@ -139,6 +157,6 @@
       </div>
     </template>
 
-    <script src="/static/app.js?v=20260704-text-events"></script>
+    <script src="/static/app.js?v=20260706-prompt-list"></script>
   </body>
 </html>

+ 39 - 0
src/agent_lab/presentation/static/styles.css

@@ -177,6 +177,45 @@ textarea {
   grid-template-columns: 96px 1fr;
 }
 
+.prompt-item {
+  border: 1px solid #d7dee5;
+  border-radius: 6px;
+  cursor: grab;
+  padding: 10px;
+}
+
+.prompt-item.dragging {
+  opacity: 0.55;
+}
+
+.prompt-item-head {
+  align-items: center;
+  display: flex;
+  gap: 10px;
+  justify-content: space-between;
+  margin-bottom: 8px;
+}
+
+.prompt-item-head span {
+  color: #52616f;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.prompt-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.prompt-actions button {
+  padding: 5px 8px;
+}
+
+.event-prompt textarea {
+  background: #f7f9fb;
+}
+
 .workspace {
   display: grid;
   grid-template-rows: minmax(0, 1fr) auto auto;

+ 17 - 5
tests/test_websocket_api.py

@@ -475,14 +475,27 @@ def test_static_prompt_workspace_controls_are_available_in_chat_agent_config():
     assert 'id="pre-messages"' in chat_dialog_html
 
 
-def test_static_chat_agent_prompt_defaults_to_available_event_instructions():
+def test_static_chat_agent_prompt_is_a_single_sortable_list_with_event_instructions():
     html = Path("src/agent_lab/presentation/static/index.html").read_text()
     js = Path("src/agent_lab/presentation/static/app.js").read_text()
+    css = Path("src/agent_lab/presentation/static/styles.css").read_text()
 
-    assert 'id="chat-system-prompt"' in html
+    assert 'id="chat-system-prompt"' not in html
+    assert 'id="chat-event-prompt-preview"' not in html
+    assert 'class="prompt-list stack"' in html
+    assert 'class="prompt-item"' in html
     assert "function buildAvailableEventsPrompt(" in js
     assert "Available events:" in js
-    assert "updateChatSystemPromptDefault(tools)" in js
+    assert "function updateEventPromptItem(" in js
+    assert "function createSystemPrompt(" in js
+    assert "function movePromptItem(" in js
+    assert 'draggable = true' in js
+    assert "delete-system-prompt" in js
+    assert 'prompt_kind: "event_agent_system"' in js
+    assert "system_prompts: collectSystemPrompts()" in js
+    assert 'system_prompt: ""' in js
+    assert ".prompt-item" in css
+    assert ".prompt-item.dragging" in css
     assert "#open-prompt-config" not in js
     assert "promptDialog" not in js
 
@@ -522,7 +535,6 @@ def test_static_agent_config_controls_are_aligned_for_chat_and_event_agents():
         "model",
         "temperature",
         "max-tokens",
-        "chat-system-prompt",
         "chat-thinking-disabled",
         "chat-enable-search",
         "chat-forced-search",
@@ -538,7 +550,7 @@ def test_static_agent_config_controls_are_aligned_for_chat_and_event_agents():
 
     assert 'extra_body: buildExtraBody("chat")' in js
     assert 'extra_body: buildExtraBody("event")' in js
-    assert 'system_prompt: document.querySelector("#chat-system-prompt")' in js
+    assert 'system_prompt: ""' in js
     assert 'system_prompt: document.querySelector("#event-system-prompt")' in js