Bladeren bron

feat: improve prompt and tool controls

zhenyu.hu 3 weken geleden
bovenliggende
commit
c11b0b053a

+ 36 - 0
docs/plans/todo-18-prompt-modal-tool-ui.md

@@ -0,0 +1,36 @@
+# Todo 18 Prompt Modal and Tool UI Plan
+
+**Status:** done
+
+## Goal
+
+Move prompt editing into a modal, expose matching ChatAgent/EventAgent controls, and make tools/audit activity visible in the frontend.
+
+## Design
+
+- Keep existing prompt-set, system prompt, and pre-message DOM ids so saved prompt behavior remains stable.
+- Add a top-level Prompt Config button that opens a native `<dialog>`.
+- Add matching ChatAgent/EventAgent controls for model, temperature, max tokens, system prompt, and `extra_body`.
+- Render tool metadata from `/api/tools` as selectable rows with description and required parameter names.
+- Render backend `audit` events in the transcript as lightweight chain logs.
+
+## Files
+
+- Modify: `src/agent_lab/presentation/static/index.html`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify: `src/agent_lab/presentation/static/styles.css`
+- Modify: `tests/test_websocket_api.py`
+- Modify: `docs/plans/todos.md`
+
+## Verification
+
+- Static tests prove modal hooks, matching agent controls, tool metadata UI, and audit handling are wired.
+- Full suite passes with `uv run pytest`.
+
+## Result
+
+- Moved prompt workspace controls into a native modal opened by `Prompt Config`.
+- Added matching ChatAgent/EventAgent fields for model, temperature, max tokens, system prompt, and extra body.
+- Rendered tool cards with descriptions, required parameter metadata, selected count, and select-all/clear actions.
+- Added frontend handling for backend `audit` events.
+- Verified with focused static tests, `uv run pytest` (`46 passed`), and a local browser check at `http://127.0.0.1:8001`.

+ 1 - 1
docs/plans/todos.md

@@ -44,5 +44,5 @@
 | 15 | done | `docs/plans/todo-15-static-package-data.md` | Include static UI assets in installed package builds. | `uv run pytest tests/test_packaging.py`; `uv run pytest`; `uv build`; wheel inspection. |
 | 16 | done | `docs/plans/todo-16-tool-error-isolation.md` | Convert EventAgent tool handler exceptions into structured tool-result errors instead of aborting the session. | `uv run pytest tests/test_event_agent.py tests/test_debug_runtime.py`; `uv run pytest`. |
 | 17 | done | `docs/plans/todo-17-agent-config-audit-tools.md` | Align ChatAgent/EventAgent config, add extra_body defaults, default EventAgent to one event round, add mock tools, audit events, and backend logging. | `uv run pytest` passes (`42 passed`, one existing Starlette deprecation warning). |
-| 18 | pending | `docs/plans/todo-18-prompt-modal-tool-ui.md` | Move prompt workspace into a modal and improve visible tool-management UI. | Static UI tests prove modal controls, tool metadata, and audit log handling exist. |
+| 18 | done | `docs/plans/todo-18-prompt-modal-tool-ui.md` | Move prompt workspace into a modal and improve visible tool-management UI. | `uv run pytest` passes (`46 passed`), plus local browser check confirms modal and tool UI render. |
 | 19 | pending | `docs/plans/todo-19-latency-oriented-polish.md` | Do one optimization pass focused on reducing perceived reply wait time and cleaning frontend/backend rough edges. | Full test suite plus focused UI/static checks pass. |

+ 110 - 2
src/agent_lab/presentation/static/app.js

@@ -8,6 +8,8 @@ const systemPrompts = document.querySelector("#system-prompts");
 const preMessages = document.querySelector("#pre-messages");
 const preMessageTemplate = document.querySelector("#pre-message-template");
 const toolList = document.querySelector("#tool-list");
+const toolCount = document.querySelector("#tool-count");
+const promptDialog = document.querySelector("#prompt-dialog");
 const PROMPT_SETS_STORAGE_KEY = "agent-lab.prompt-sets.v1";
 
 let socket = null;
@@ -28,6 +30,18 @@ document.querySelector("#add-pre-message").addEventListener("click", () => {
 document.querySelector("#save-prompt-set").addEventListener("click", savePromptSet);
 document.querySelector("#load-prompt-set").addEventListener("click", loadPromptSet);
 document.querySelector("#delete-prompt-set").addEventListener("click", deletePromptSet);
+document.querySelector("#open-prompt-config").addEventListener("click", () => {
+  promptDialog.showModal();
+});
+document.querySelector("#close-prompt-config").addEventListener("click", () => {
+  promptDialog.close();
+});
+document.querySelector("#select-all-tools").addEventListener("click", () => {
+  setAllTools(true);
+});
+document.querySelector("#clear-tools").addEventListener("click", () => {
+  setAllTools(false);
+});
 
 chatForm.addEventListener("submit", (event) => {
   event.preventDefault();
@@ -53,10 +67,14 @@ function renderTools(tools) {
   toolList.textContent = "";
   if (!tools.length) {
     toolList.textContent = "No tools available";
+    updateToolCount();
     return;
   }
 
   tools.forEach((tool) => {
+    const item = document.createElement("div");
+    item.className = "tool-card";
+
     const label = document.createElement("label");
     label.className = "checkbox";
     label.title = tool.description || tool.name;
@@ -66,15 +84,47 @@ function renderTools(tools) {
     checkbox.className = "tool-toggle";
     checkbox.value = tool.name;
     checkbox.checked = true;
+    checkbox.addEventListener("change", updateToolCount);
 
     label.append(checkbox, ` ${tool.name}`);
-    toolList.append(label);
+
+    const description = document.createElement("div");
+    description.className = "tool-description";
+    description.textContent = tool.description || "No description";
+
+    const required = document.createElement("div");
+    required.className = "tool-required";
+    required.textContent = formatRequiredParameters(tool);
+
+    item.append(label, description, required);
+    toolList.append(item);
   });
 
   if (pendingEnabledTools) {
     applySelectedTools(pendingEnabledTools);
     pendingEnabledTools = null;
   }
+  updateToolCount();
+}
+
+function formatRequiredParameters(tool) {
+  const required = tool.parameters && Array.isArray(tool.parameters.required)
+    ? tool.parameters.required
+    : [];
+  return required.length ? `Required: ${required.join(", ")}` : "No required parameters";
+}
+
+function setAllTools(checked) {
+  toolList.querySelectorAll(".tool-toggle").forEach((checkbox) => {
+    checkbox.checked = checked;
+  });
+  updateToolCount();
+}
+
+function updateToolCount() {
+  const selected = selectedTools().length;
+  const total = toolList.querySelectorAll(".tool-toggle").length;
+  toolCount.textContent = `${selected}/${total} selected`;
 }
 
 function savePromptSet() {
@@ -142,8 +192,15 @@ 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(),
+      extra_body: buildExtraBody("chat"),
     },
     event_agent: {
+      model: document.querySelector("#event-model").value.trim(),
+      temperature: Number(document.querySelector("#event-temperature").value),
+      max_tokens: Number(document.querySelector("#event-max-tokens").value),
+      system_prompt: document.querySelector("#event-system-prompt").value.trim(),
+      extra_body: buildExtraBody("event"),
       enabled_tools: selectedTools(),
       max_event_loops: Number(document.querySelector("#max-event-loops").value),
     },
@@ -158,13 +215,45 @@ function restorePromptSet(promptSet) {
   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 || {};
-  document.querySelector("#max-event-loops").value = eventAgent.max_event_loops ?? 3;
+  document.querySelector("#event-model").value = eventAgent.model || "";
+  document.querySelector("#event-temperature").value = eventAgent.temperature ?? 0.2;
+  document.querySelector("#event-max-tokens").value = eventAgent.max_tokens ?? 800;
+  document.querySelector("#event-system-prompt").value = eventAgent.system_prompt || "";
+  restoreExtraBody("event", eventAgent.extra_body);
+  document.querySelector("#max-event-loops").value = eventAgent.max_event_loops ?? 1;
   const enabledTools = eventAgent.enabled_tools || [];
   pendingEnabledTools = applySelectedTools(enabledTools) ? null : enabledTools;
 }
 
+function buildExtraBody(prefix) {
+  const thinkingDisabled = document.querySelector(`#${prefix}-thinking-disabled`).checked;
+  const enableSearch = document.querySelector(`#${prefix}-enable-search`).checked;
+  const forcedSearch = document.querySelector(`#${prefix}-forced-search`).checked;
+  const extraBody = {
+    enable_search: enableSearch,
+    search_options: {
+      forced_search: forcedSearch,
+    },
+  };
+  if (thinkingDisabled) {
+    extraBody.thinking = { type: "disabled" };
+  }
+  return extraBody;
+}
+
+function restoreExtraBody(prefix, extraBody = {}) {
+  document.querySelector(`#${prefix}-thinking-disabled`).checked =
+    !extraBody.thinking || extraBody.thinking.type === "disabled";
+  document.querySelector(`#${prefix}-enable-search`).checked =
+    Boolean(extraBody.enable_search);
+  document.querySelector(`#${prefix}-forced-search`).checked =
+    Boolean(extraBody.search_options && extraBody.search_options.forced_search);
+}
+
 function restoreSystemPrompts(prompts) {
   systemPrompts.textContent = "";
   const values = prompts.length ? prompts : [""];
@@ -205,6 +294,7 @@ function applySelectedTools(toolNames) {
   checkboxes.forEach((checkbox) => {
     checkbox.checked = selected.has(checkbox.value);
   });
+  updateToolCount();
   return true;
 }
 
@@ -296,8 +386,15 @@ function buildRequest() {
       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(),
+      extra_body: buildExtraBody("chat"),
     },
     event_agent: {
+      model: document.querySelector("#event-model").value.trim(),
+      temperature: Number(document.querySelector("#event-temperature").value),
+      max_tokens: Number(document.querySelector("#event-max-tokens").value),
+      system_prompt: document.querySelector("#event-system-prompt").value.trim(),
+      extra_body: buildExtraBody("event"),
       enabled_tools: selectedTools(),
       max_event_loops: Number(document.querySelector("#max-event-loops").value),
     },
@@ -327,6 +424,10 @@ function handleServerMessage(message) {
     updateRoundStats(message);
     return;
   }
+  if (message.type === "audit") {
+    appendLog("audit", formatAuditMessage(message));
+    return;
+  }
   if (message.type === "event") {
     appendLog("event", message.event.name);
     return;
@@ -345,6 +446,13 @@ function handleServerMessage(message) {
   }
 }
 
+function formatAuditMessage(message) {
+  const details = message.details && Object.keys(message.details).length
+    ? ` ${JSON.stringify(message.details)}`
+    : "";
+  return `${message.event}${details}`;
+}
+
 function appendAssistantDelta(content) {
   if (!firstTokenAt) {
     firstTokenAt = performance.now();

+ 70 - 32
src/agent_lab/presentation/static/index.html

@@ -9,13 +9,80 @@
   <body>
     <header class="topbar">
       <h1>Agent Lab</h1>
-      <div id="connection-status" class="status">Idle</div>
+      <div class="top-actions">
+        <button id="open-prompt-config" type="button">Prompt Config</button>
+        <div id="connection-status" class="status">Idle</div>
+      </div>
     </header>
 
     <main class="layout">
       <aside class="panel controls">
         <section>
+          <h2>Chat Agent</h2>
+          <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="4" placeholder="Optional ChatAgent system prompt"></textarea></label>
+          <fieldset>
+            <legend>Extra Body</legend>
+            <label class="checkbox"><input id="chat-thinking-disabled" type="checkbox" checked /> Disable thinking</label>
+            <label class="checkbox"><input id="chat-enable-search" type="checkbox" /> Enable search</label>
+            <label class="checkbox"><input id="chat-forced-search" type="checkbox" /> Forced search</label>
+          </fieldset>
+        </section>
+
+        <section>
+          <h2>Event Agent</h2>
+          <label>Model <input id="event-model" placeholder="Same as ChatAgent" /></label>
+          <label>Temperature <input id="event-temperature" type="number" min="0" max="2" step="0.1" value="0.2" /></label>
+          <label>Max Tokens <input id="event-max-tokens" type="number" min="1" step="1" value="800" /></label>
+          <label>System Prompt <textarea id="event-system-prompt" rows="4" placeholder="Optional EventAgent system prompt"></textarea></label>
+          <label>Max Loops <input id="max-event-loops" type="number" min="1" step="1" value="1" /></label>
+          <fieldset>
+            <legend>Extra Body</legend>
+            <label class="checkbox"><input id="event-thinking-disabled" type="checkbox" checked /> Disable thinking</label>
+            <label class="checkbox"><input id="event-enable-search" type="checkbox" /> Enable search</label>
+            <label class="checkbox"><input id="event-forced-search" type="checkbox" /> Forced search</label>
+          </fieldset>
+        </section>
+
+        <section>
+          <div class="section-head">
+            <h2>Tools <span id="tool-count">0 selected</span></h2>
+            <div class="button-row compact">
+              <button id="select-all-tools" type="button">All</button>
+              <button id="clear-tools" type="button">None</button>
+            </div>
+          </div>
+          <div id="tool-list" class="stack">Loading tools...</div>
+        </section>
+      </aside>
+
+      <section class="workspace">
+        <div id="messages" class="messages"></div>
+
+        <div class="stats">
+          <div><span>Tokens</span><strong id="stat-tokens">0</strong></div>
+          <div><span>Cached</span><strong id="stat-cached">0</strong></div>
+          <div><span>TTFT</span><strong id="stat-ttft">-</strong></div>
+          <div><span>Elapsed</span><strong id="stat-elapsed">-</strong></div>
+        </div>
+
+        <form id="chat-form" class="composer">
+          <textarea id="user-message" rows="3" placeholder="Send a debug message"></textarea>
+          <button type="submit">Run</button>
+        </form>
+      </section>
+    </main>
+
+    <dialog id="prompt-dialog" class="modal">
+      <form method="dialog" class="modal-shell">
+        <div class="modal-head">
           <h2>Prompt Workspace</h2>
+          <button id="close-prompt-config" type="button">Close</button>
+        </div>
+
+        <section>
           <label>Name <input id="prompt-set-name" placeholder="default debugger" /></label>
           <label>
             Saved Set
@@ -45,37 +112,8 @@
           </div>
           <div id="pre-messages" class="stack"></div>
         </section>
-
-        <section>
-          <h2>Chat Agent</h2>
-          <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>
-        </section>
-
-        <section>
-          <h2>Event Agent</h2>
-          <label>Max Loops <input id="max-event-loops" type="number" min="1" step="1" value="3" /></label>
-          <div id="tool-list" class="stack">Loading tools...</div>
-        </section>
-      </aside>
-
-      <section class="workspace">
-        <div id="messages" class="messages"></div>
-
-        <div class="stats">
-          <div><span>Tokens</span><strong id="stat-tokens">0</strong></div>
-          <div><span>Cached</span><strong id="stat-cached">0</strong></div>
-          <div><span>TTFT</span><strong id="stat-ttft">-</strong></div>
-          <div><span>Elapsed</span><strong id="stat-elapsed">-</strong></div>
-        </div>
-
-        <form id="chat-form" class="composer">
-          <textarea id="user-message" rows="3" placeholder="Send a debug message"></textarea>
-          <button type="submit">Run</button>
-        </form>
-      </section>
-    </main>
+      </form>
+    </dialog>
 
     <template id="pre-message-template">
       <div class="pre-message">

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

@@ -48,6 +48,12 @@ button:hover {
   margin: 0;
 }
 
+.top-actions {
+  align-items: center;
+  display: flex;
+  gap: 10px;
+}
+
 .status {
   background: #e0f2f1;
   border: 1px solid #b2dfdb;
@@ -106,18 +112,40 @@ h2 {
   margin-top: 10px;
 }
 
+.button-row.compact {
+  grid-template-columns: repeat(2, auto);
+  margin-top: 0;
+}
+
 label {
   display: grid;
   gap: 6px;
   margin-top: 10px;
 }
 
+fieldset {
+  border: 1px solid #d7dee5;
+  border-radius: 6px;
+  margin: 12px 0 0;
+  padding: 8px 10px 10px;
+}
+
+legend {
+  color: #52616f;
+  font-size: 12px;
+  padding: 0 4px;
+}
+
 .checkbox {
   align-items: center;
   display: flex;
   gap: 8px;
 }
 
+.checkbox input {
+  width: auto;
+}
+
 input,
 select,
 textarea {
@@ -181,12 +209,65 @@ textarea {
   border: 1px solid #bbf7d0;
 }
 
+.audit {
+  background: #eef2ff;
+  border: 1px solid #c7d2fe;
+  color: #3730a3;
+  font-size: 13px;
+}
+
 .error {
   background: #fef2f2;
   border: 1px solid #fecaca;
   color: #991b1b;
 }
 
+.tool-card {
+  border: 1px solid #e4e9ee;
+  border-radius: 6px;
+  padding: 10px;
+}
+
+.tool-description,
+.tool-required {
+  color: #52616f;
+  font-size: 12px;
+  line-height: 1.4;
+  margin-top: 6px;
+}
+
+#tool-count {
+  color: #697586;
+  font-size: 12px;
+  font-weight: 400;
+}
+
+.modal {
+  border: 0;
+  border-radius: 8px;
+  max-height: min(720px, calc(100vh - 48px));
+  max-width: min(760px, calc(100vw - 32px));
+  padding: 0;
+  width: 760px;
+}
+
+.modal::backdrop {
+  background: rgba(17, 24, 39, 0.36);
+}
+
+.modal-shell {
+  display: grid;
+  max-height: min(720px, calc(100vh - 48px));
+  overflow: auto;
+  padding: 16px;
+}
+
+.modal-head {
+  align-items: center;
+  display: flex;
+  justify-content: space-between;
+}
+
 .stats {
   border-top: 1px solid #e4e9ee;
   display: grid;

+ 61 - 0
tests/test_websocket_api.py

@@ -465,6 +465,67 @@ def test_static_prompt_workspace_controls_are_available():
     assert 'id="delete-prompt-set"' in html
 
 
+def test_static_prompt_workspace_is_opened_from_a_modal():
+    html = Path("src/agent_lab/presentation/static/index.html").read_text()
+    js = Path("src/agent_lab/presentation/static/app.js").read_text()
+
+    assert 'id="open-prompt-config"' in html
+    assert '<dialog id="prompt-dialog"' in html
+    assert 'id="close-prompt-config"' in html
+    assert "promptDialog.showModal()" in js
+    assert "promptDialog.close()" in js
+
+
+def test_static_agent_config_controls_are_aligned_for_chat_and_event_agents():
+    html = Path("src/agent_lab/presentation/static/index.html").read_text()
+    js = Path("src/agent_lab/presentation/static/app.js").read_text()
+
+    for control_id in [
+        "model",
+        "temperature",
+        "max-tokens",
+        "chat-system-prompt",
+        "chat-thinking-disabled",
+        "chat-enable-search",
+        "chat-forced-search",
+        "event-model",
+        "event-temperature",
+        "event-max-tokens",
+        "event-system-prompt",
+        "event-thinking-disabled",
+        "event-enable-search",
+        "event-forced-search",
+    ]:
+        assert f'id="{control_id}"' in html
+
+    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: document.querySelector("#event-system-prompt")' in js
+
+
+def test_static_tools_ui_shows_tool_metadata_and_selection_actions():
+    html = Path("src/agent_lab/presentation/static/index.html").read_text()
+    js = Path("src/agent_lab/presentation/static/app.js").read_text()
+
+    assert 'id="tool-count"' in html
+    assert 'id="select-all-tools"' in html
+    assert 'id="clear-tools"' in html
+    assert "tool-card" in js
+    assert "tool-description" in js
+    assert "tool-required" in js
+    assert "function updateToolCount()" in js
+
+
+def test_static_app_handles_audit_messages():
+    js = Path("src/agent_lab/presentation/static/app.js").read_text()
+    css = Path("src/agent_lab/presentation/static/styles.css").read_text()
+
+    assert 'message.type === "audit"' in js
+    assert 'appendLog("audit"' in js
+    assert ".audit" in css
+
+
 def test_static_prompt_workspace_uses_stable_storage_hooks():
     js = Path("src/agent_lab/presentation/static/app.js").read_text()