From 8b04b2407bfc5a29e7e5b3fe846f14855bb9583f Mon Sep 17 00:00:00 2001 From: Matthew Bellew Date: Wed, 11 Feb 2026 13:25:41 -0800 Subject: [PATCH 1/3] McpService.clear() /clear command --- .../labkey/api/mcp/AbstractAgentAction.java | 30 ++++++++++- api/src/org/labkey/api/mcp/McpService.java | 2 + core/src/org/labkey/core/CoreController.java | 1 - .../org/labkey/core/mpc/McpServiceImpl.java | 50 +++++++++++++++++-- .../query/controllers/QueryController.java | 7 ++- 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/api/src/org/labkey/api/mcp/AbstractAgentAction.java b/api/src/org/labkey/api/mcp/AbstractAgentAction.java index 91dddcc7eaf..7706780c961 100644 --- a/api/src/org/labkey/api/mcp/AbstractAgentAction.java +++ b/api/src/org/labkey/api/mcp/AbstractAgentAction.java @@ -3,6 +3,7 @@ import com.google.genai.errors.ClientException; import com.google.genai.errors.ServerException; import jakarta.servlet.http.HttpSession; +import org.apache.commons.lang3.StringUtils; import org.json.JSONObject; import org.labkey.api.action.ReadOnlyApiAction; import org.labkey.api.util.HtmlString; @@ -32,11 +33,39 @@ protected ChatClient getChat() return chatSession; } + protected String handleEscape(String prompt) + { + prompt = StringUtils.trimToEmpty(prompt); + switch (prompt) + { + case "/clear" -> + { + ChatClient chatSession = getChat(); // CONSIDER: getChat(boolean ifStarted) + if (null != chatSession) + McpService.get().close(getViewContext().getSession(), chatSession); + return "OK, let's start over."; + } + } + return null; + } + @Override public Object execute(PromptForm form, BindException errors) throws Exception { try (var mcpPush = McpContext.withContext(getViewContext())) { + String prompt = form.getPrompt(); + + String escapeResponse = handleEscape(prompt); + if (null != escapeResponse) + { + return new JSONObject(Map.of( + "contentType", "text/plain", + "response", escapeResponse, + "success", Boolean.TRUE)); + } + + // call getChat() after handleEscape() ChatClient chatSession = getChat(); if (null == chatSession) return new JSONObject(Map.of( @@ -44,7 +73,6 @@ public Object execute(PromptForm form, BindException errors) throws Exception "response", "Service is not ready yet", "success", Boolean.FALSE)); - String prompt = form.getPrompt(); McpService.MessageResponse response = McpService.get().sendMessage(chatSession, prompt); var ret = new JSONObject(Map.of("success", Boolean.TRUE)); if (!HtmlString.isBlank(response.html())) diff --git a/api/src/org/labkey/api/mcp/McpService.java b/api/src/org/labkey/api/mcp/McpService.java index 164e8b81f0d..3f50a29bcaa 100644 --- a/api/src/org/labkey/api/mcp/McpService.java +++ b/api/src/org/labkey/api/mcp/McpService.java @@ -76,6 +76,8 @@ default void register(McpProvider mcp) ChatClient getChat(HttpSession session, String agentName, Supplier systemPromptSupplier); + void close(HttpSession session, ChatClient chat); + record MessageResponse(String contentType, String text, HtmlString html) {} /** get consolidated response (good for many text oriented agents/use-cases) */ diff --git a/core/src/org/labkey/core/CoreController.java b/core/src/org/labkey/core/CoreController.java index 3f78eb63b95..d80b090fa49 100644 --- a/core/src/org/labkey/core/CoreController.java +++ b/core/src/org/labkey/core/CoreController.java @@ -21,7 +21,6 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.apache.commons.beanutils.ConversionException; -import org.apache.commons.beanutils.ConvertUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.apache.xmlbeans.XmlObject; diff --git a/core/src/org/labkey/core/mpc/McpServiceImpl.java b/core/src/org/labkey/core/mpc/McpServiceImpl.java index 422d0140223..f5ac5a36343 100644 --- a/core/src/org/labkey/core/mpc/McpServiceImpl.java +++ b/core/src/org/labkey/core/mpc/McpServiceImpl.java @@ -46,6 +46,7 @@ import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.google.genai.GoogleGenAiChatModel; import org.springframework.ai.google.genai.GoogleGenAiChatOptions; @@ -306,7 +307,8 @@ public ChatClient getChat(HttpSession session, String agentName, Supplier + String sessionKey = ChatClient.class.getName() + "#" + agentName; + return SessionHelper.getAttribute(session, sessionKey, () -> { String systemPrompt = systemPromptSupplier.get(); String conversationId = session.getId() + ":" + agentName; @@ -326,14 +328,57 @@ public ChatClient getChat(HttpSession session, String agentName, Supplier systemPromptSupplier); EmbeddingModel createEmbeddingModel(); } diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 0d14f2a7b27..8df85d47210 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -8880,9 +8880,14 @@ public Object execute(SqlPromptForm form, BindException errors) throws Exception try (var mcpPush = McpContext.withContext(getViewContext())) { + String prompt = form.getPrompt(); + + String replacePrompt = handleEscape(prompt); + if (null != replacePrompt) + prompt = replacePrompt; + // TODO when/how to do we reset or isolate different chat sessions, e.g. if two SQL windows are open concurrently? ChatClient chatSession = getChat(); - String prompt = form.getPrompt(); List responses; SqlResponse sqlResponse; From e81695f00fe7884dd544dd4e14a19598419f615c Mon Sep 17 00:00:00 2001 From: Matthew Bellew Date: Wed, 11 Feb 2026 13:28:12 -0800 Subject: [PATCH 2/3] McpService.clear() /clear command --- .../org/labkey/query/controllers/QueryController.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 8df85d47210..cab3754e726 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -8882,9 +8882,14 @@ public Object execute(SqlPromptForm form, BindException errors) throws Exception { String prompt = form.getPrompt(); - String replacePrompt = handleEscape(prompt); - if (null != replacePrompt) - prompt = replacePrompt; + String escapeResponse = handleEscape(prompt); + if (null != escapeResponse) + { + return new JSONObject(Map.of( + "contentType", "text/plain", + "response", escapeResponse, + "success", Boolean.TRUE)); + } // TODO when/how to do we reset or isolate different chat sessions, e.g. if two SQL windows are open concurrently? ChatClient chatSession = getChat(); From a50d62232300e7f0d18c5a31c38bc28fa2f6d45f Mon Sep 17 00:00:00 2001 From: Matthew Bellew Date: Wed, 11 Feb 2026 13:50:31 -0800 Subject: [PATCH 3/3] getChat(boolean create) --- .../labkey/api/mcp/AbstractAgentAction.java | 8 +-- api/src/org/labkey/api/mcp/McpService.java | 7 ++- .../org/labkey/core/mpc/McpServiceImpl.java | 62 +++++++++++-------- .../query/controllers/QueryController.java | 5 +- 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/api/src/org/labkey/api/mcp/AbstractAgentAction.java b/api/src/org/labkey/api/mcp/AbstractAgentAction.java index 7706780c961..159695afe78 100644 --- a/api/src/org/labkey/api/mcp/AbstractAgentAction.java +++ b/api/src/org/labkey/api/mcp/AbstractAgentAction.java @@ -26,10 +26,10 @@ public abstract class AbstractAgentAction extends ReadOnly protected abstract String getServicePrompt(); - protected ChatClient getChat() + protected ChatClient getChat(boolean create) { HttpSession session = getViewContext().getRequest().getSession(true); - ChatClient chatSession = McpService.get().getChat(session, getAgentName(), this::getServicePrompt); + ChatClient chatSession = McpService.get().getChat(session, getAgentName(), this::getServicePrompt, create); return chatSession; } @@ -40,7 +40,7 @@ protected String handleEscape(String prompt) { case "/clear" -> { - ChatClient chatSession = getChat(); // CONSIDER: getChat(boolean ifStarted) + ChatClient chatSession = getChat(false); // CONSIDER: getChat(boolean ifStarted) if (null != chatSession) McpService.get().close(getViewContext().getSession(), chatSession); return "OK, let's start over."; @@ -66,7 +66,7 @@ public Object execute(PromptForm form, BindException errors) throws Exception } // call getChat() after handleEscape() - ChatClient chatSession = getChat(); + ChatClient chatSession = getChat(true); if (null == chatSession) return new JSONObject(Map.of( "contentType", "text/plain", diff --git a/api/src/org/labkey/api/mcp/McpService.java b/api/src/org/labkey/api/mcp/McpService.java index 3f50a29bcaa..67854a58c61 100644 --- a/api/src/org/labkey/api/mcp/McpService.java +++ b/api/src/org/labkey/api/mcp/McpService.java @@ -74,7 +74,12 @@ default void register(McpProvider mcp) @Override ToolCallback @NonNull [] getToolCallbacks(); - ChatClient getChat(HttpSession session, String agentName, Supplier systemPromptSupplier); + default ChatClient getChat(HttpSession session, String agentName, Supplier systemPromptSupplier) + { + return getChat(session, agentName, systemPromptSupplier, true); + } + + ChatClient getChat(HttpSession session, String agentName, Supplier systemPromptSupplier, boolean createIfNotExists); void close(HttpSession session, ChatClient chat); diff --git a/core/src/org/labkey/core/mpc/McpServiceImpl.java b/core/src/org/labkey/core/mpc/McpServiceImpl.java index f5ac5a36343..11e7b93a3a3 100644 --- a/core/src/org/labkey/core/mpc/McpServiceImpl.java +++ b/core/src/org/labkey/core/mpc/McpServiceImpl.java @@ -300,41 +300,49 @@ public void shutdownStarted() } } - @Override - public ChatClient getChat(HttpSession session, String agentName, Supplier systemPromptSupplier) + public ChatClient getChat(HttpSession session, String agentName, Supplier systemPromptSupplier, boolean createIfNotExists) { if (!serverReady) return null; String sessionKey = ChatClient.class.getName() + "#" + agentName; - return SessionHelper.getAttribute(session, sessionKey, () -> + if (createIfNotExists) { - String systemPrompt = systemPromptSupplier.get(); - String conversationId = session.getId() + ":" + agentName; - List advisors = new ArrayList<>(); - - ChatMemory chatMemory = MessageWindowChatMemory.builder() - .maxMessages(100) - .chatMemoryRepository(chatMemoryRepository) - .build(); - - MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory) - .conversationId(conversationId) - .build(); - advisors.add(chatMemoryAdvisor); - - VectorStore vs = getVectorStore(); - if (null != vs) - advisors.add(QuestionAnswerAdvisor.builder(vs).build()); + return SessionHelper.getAttribute(session, sessionKey, () -> + { + var springClient = createSpringChat(session, agentName, systemPromptSupplier); + return new _ChatClient(springClient, sessionKey); + }); + } + return SessionHelper.getAttribute(session, sessionKey, null); + } - ChatClient ret = ChatClient.builder(modelProvider.getChatModel()) - .defaultOptions(modelProvider.getChatOptions()) - .defaultAdvisors(advisors) - .defaultSystem(systemPrompt) - .build(); - return new _ChatClient(ret, sessionKey); - }); + private ChatClient createSpringChat(HttpSession session, String agentName, Supplier systemPromptSupplier) + { + String systemPrompt = systemPromptSupplier.get(); + String conversationId = session.getId() + ":" + agentName; + List advisors = new ArrayList<>(); + + ChatMemory chatMemory = MessageWindowChatMemory.builder() + .maxMessages(100) + .chatMemoryRepository(chatMemoryRepository) + .build(); + + MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory) + .conversationId(conversationId) + .build(); + advisors.add(chatMemoryAdvisor); + + VectorStore vs = getVectorStore(); + if (null != vs) + advisors.add(QuestionAnswerAdvisor.builder(vs).build()); + + return ChatClient.builder(modelProvider.getChatModel()) + .defaultOptions(modelProvider.getChatOptions()) + .defaultAdvisors(advisors) + .defaultSystem(systemPrompt) + .build(); } private class _ChatClient implements ChatClient diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index cab3754e726..5a3c88f2edc 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -8887,18 +8887,19 @@ public Object execute(SqlPromptForm form, BindException errors) throws Exception { return new JSONObject(Map.of( "contentType", "text/plain", - "response", escapeResponse, + "text", escapeResponse, "success", Boolean.TRUE)); } // TODO when/how to do we reset or isolate different chat sessions, e.g. if two SQL windows are open concurrently? - ChatClient chatSession = getChat(); + ChatClient chatSession = getChat(true); List responses; SqlResponse sqlResponse; if (isBlank(prompt)) { return new JSONObject(Map.of( + "contentType", "text/plain", "text", "🤷", "success", Boolean.TRUE)); }