فهرست منبع

feat: add prompt workspace persistence

zhenyu.hu 3 هفته پیش
والد
کامیت
ee2334f5c8

+ 197 - 5
src/agent_lab/presentation/static/app.js

@@ -2,32 +2,38 @@ const messagesEl = document.querySelector("#messages");
 const statusEl = document.querySelector("#connection-status");
 const chatForm = document.querySelector("#chat-form");
 const userMessage = document.querySelector("#user-message");
+const promptSetName = document.querySelector("#prompt-set-name");
+const savedPromptSets = document.querySelector("#saved-prompt-sets");
 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 PROMPT_SETS_STORAGE_KEY = "agent-lab.prompt-sets.v1";
 
 let socket = null;
 let activeAssistant = null;
 let runStartedAt = 0;
 let firstTokenAt = 0;
+let pendingEnabledTools = null;
 
 document.querySelector("#add-system-prompt").addEventListener("click", () => {
-  const textarea = document.createElement("textarea");
-  textarea.className = "system-prompt";
-  textarea.rows = 4;
-  systemPrompts.append(textarea);
+  systemPrompts.append(createSystemPrompt(""));
 });
 
 document.querySelector("#add-pre-message").addEventListener("click", () => {
-  preMessages.append(preMessageTemplate.content.cloneNode(true));
+  preMessages.append(createPreMessage({ role: "user", content: "" }));
 });
 
+document.querySelector("#save-prompt-set").addEventListener("click", savePromptSet);
+document.querySelector("#load-prompt-set").addEventListener("click", loadPromptSet);
+document.querySelector("#delete-prompt-set").addEventListener("click", deletePromptSet);
+
 chatForm.addEventListener("submit", (event) => {
   event.preventDefault();
   runDebugSession();
 });
 
+refreshPromptSetSelector();
 loadTools();
 
 async function loadTools() {
@@ -63,6 +69,192 @@ function renderTools(tools) {
     label.append(checkbox, ` ${tool.name}`);
     toolList.append(label);
   });
+
+  if (pendingEnabledTools) {
+    applySelectedTools(pendingEnabledTools);
+    pendingEnabledTools = null;
+  }
+}
+
+function savePromptSet() {
+  const name = promptSetName.value.trim();
+  if (!name) {
+    statusEl.textContent = "Set name required";
+    return;
+  }
+
+  const store = readPromptSets();
+  store.sets[name] = buildPromptSet();
+  writePromptSets(store);
+  refreshPromptSetSelector(name);
+  statusEl.textContent = "Prompt set saved";
+}
+
+function loadPromptSet() {
+  const name = savedPromptSets.value;
+  if (!name) {
+    statusEl.textContent = "Select a prompt set";
+    return;
+  }
+
+  const promptSet = readPromptSets().sets[name];
+  if (!promptSet) {
+    statusEl.textContent = "Prompt set not found";
+    refreshPromptSetSelector();
+    return;
+  }
+
+  promptSetName.value = name;
+  restorePromptSet(promptSet);
+  statusEl.textContent = "Prompt set loaded";
+}
+
+function deletePromptSet() {
+  const name = savedPromptSets.value;
+  if (!name) {
+    statusEl.textContent = "Select a prompt set";
+    return;
+  }
+
+  const store = readPromptSets();
+  delete store.sets[name];
+  writePromptSets(store);
+  if (promptSetName.value.trim() === name) {
+    promptSetName.value = "";
+  }
+  refreshPromptSetSelector();
+  statusEl.textContent = "Prompt set deleted";
+}
+
+function buildPromptSet() {
+  return {
+    system_prompts: [...document.querySelectorAll(".system-prompt")]
+      .map((input) => input.value.trim())
+      .filter(Boolean),
+    pre_messages: [...document.querySelectorAll(".pre-message")]
+      .map((row) => ({
+        role: row.querySelector(".pre-role").value,
+        content: row.querySelector(".pre-content").value.trim(),
+      }))
+      .filter((message) => message.content),
+    chat_agent: {
+      model: document.querySelector("#model").value.trim(),
+      temperature: Number(document.querySelector("#temperature").value),
+      max_tokens: Number(document.querySelector("#max-tokens").value),
+    },
+    event_agent: {
+      enabled_tools: selectedTools(),
+      max_event_loops: Number(document.querySelector("#max-event-loops").value),
+    },
+  };
+}
+
+function restorePromptSet(promptSet) {
+  restoreSystemPrompts(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;
+
+  const eventAgent = promptSet.event_agent || {};
+  document.querySelector("#max-event-loops").value = eventAgent.max_event_loops ?? 3;
+  const enabledTools = eventAgent.enabled_tools || [];
+  pendingEnabledTools = applySelectedTools(enabledTools) ? null : enabledTools;
+}
+
+function restoreSystemPrompts(prompts) {
+  systemPrompts.textContent = "";
+  const values = prompts.length ? prompts : [""];
+  values.forEach((prompt) => {
+    systemPrompts.append(createSystemPrompt(prompt));
+  });
+}
+
+function restorePreMessages(messages) {
+  preMessages.textContent = "";
+  messages.forEach((message) => {
+    preMessages.append(createPreMessage(message));
+  });
+}
+
+function createSystemPrompt(value) {
+  const textarea = document.createElement("textarea");
+  textarea.className = "system-prompt";
+  textarea.rows = 4;
+  textarea.value = value;
+  return textarea;
+}
+
+function createPreMessage(message) {
+  const fragment = preMessageTemplate.content.cloneNode(true);
+  fragment.querySelector(".pre-role").value = message.role || "user";
+  fragment.querySelector(".pre-content").value = message.content || "";
+  return fragment;
+}
+
+function applySelectedTools(toolNames) {
+  const checkboxes = [...toolList.querySelectorAll(".tool-toggle")];
+  if (!checkboxes.length) {
+    return false;
+  }
+
+  const selected = new Set(toolNames);
+  checkboxes.forEach((checkbox) => {
+    checkbox.checked = selected.has(checkbox.value);
+  });
+  return true;
+}
+
+function refreshPromptSetSelector(selectedName = "") {
+  const previousName = selectedName || savedPromptSets.value;
+  const names = Object.keys(readPromptSets().sets).sort();
+  savedPromptSets.textContent = "";
+
+  const placeholder = document.createElement("option");
+  placeholder.value = "";
+  placeholder.textContent = names.length ? "Select a set" : "No saved sets";
+  savedPromptSets.append(placeholder);
+
+  names.forEach((name) => {
+    const option = document.createElement("option");
+    option.value = name;
+    option.textContent = name;
+    savedPromptSets.append(option);
+  });
+
+  if (names.includes(previousName)) {
+    savedPromptSets.value = previousName;
+  }
+}
+
+function readPromptSets() {
+  try {
+    const rawValue = localStorage.getItem(PROMPT_SETS_STORAGE_KEY);
+    if (!rawValue) {
+      return { sets: {} };
+    }
+
+    const parsed = JSON.parse(rawValue);
+    if (
+      !parsed ||
+      typeof parsed !== "object" ||
+      !parsed.sets ||
+      typeof parsed.sets !== "object" ||
+      Array.isArray(parsed.sets)
+    ) {
+      return { sets: {} };
+    }
+
+    return parsed;
+  } catch {
+    return { sets: {} };
+  }
+}
+
+function writePromptSets(store) {
+  localStorage.setItem(PROMPT_SETS_STORAGE_KEY, JSON.stringify(store));
 }
 
 function runDebugSession() {

+ 14 - 0
src/agent_lab/presentation/static/index.html

@@ -14,6 +14,20 @@
 
     <main class="layout">
       <aside class="panel controls">
+        <section>
+          <h2>Prompt Workspace</h2>
+          <label>Name <input id="prompt-set-name" placeholder="default debugger" /></label>
+          <label>
+            Saved Set
+            <select id="saved-prompt-sets"></select>
+          </label>
+          <div class="button-row">
+            <button id="save-prompt-set" type="button">Save</button>
+            <button id="load-prompt-set" type="button">Load</button>
+            <button id="delete-prompt-set" type="button">Delete</button>
+          </div>
+        </section>
+
         <section>
           <div class="section-head">
             <h2>System Prompts</h2>

+ 9 - 1
src/agent_lab/presentation/static/styles.css

@@ -99,6 +99,13 @@ h2 {
   gap: 10px;
 }
 
+.button-row {
+  display: grid;
+  gap: 8px;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  margin-top: 10px;
+}
+
 label {
   display: grid;
   gap: 6px;
@@ -223,7 +230,8 @@ textarea {
 
   .stats,
   .composer,
-  .pre-message {
+  .pre-message,
+  .button-row {
     grid-template-columns: 1fr;
   }
 }

+ 19 - 0
tests/test_websocket_api.py

@@ -362,6 +362,25 @@ def test_static_tools_ui_is_dynamic_and_not_hardcoded_to_handoff_note_checkbox()
     assert "fetch" in js
 
 
+def test_static_prompt_workspace_controls_are_available():
+    html = Path("src/agent_lab/presentation/static/index.html").read_text()
+
+    assert 'id="prompt-set-name"' in html
+    assert 'id="saved-prompt-sets"' in html
+    assert 'id="save-prompt-set"' in html
+    assert 'id="load-prompt-set"' in html
+    assert 'id="delete-prompt-set"' in html
+
+
+def test_static_prompt_workspace_uses_stable_storage_hooks():
+    js = Path("src/agent_lab/presentation/static/app.js").read_text()
+
+    assert "agent-lab.prompt-sets.v1" in js
+    assert "function savePromptSet()" in js
+    assert "function loadPromptSet()" in js
+    assert "function deletePromptSet()" in js
+
+
 @pytest.mark.asyncio
 async def test_openai_chat_client_omits_stream_options_when_usage_disabled():
     captured_payloads: list[dict] = []