From ef83aa4c4fb9d0dd05ff852c33bef5e239507a36 Mon Sep 17 00:00:00 2001 From: ckelly17 Date: Tue, 3 Nov 2026 07:12:35 +0000 Subject: [PATCH 1/5] feat: run a pre-chat tool before a room's first message A deployment can name one toolbox tool to run when a room is set up and before its first message. The result is stored as a hidden note and acknowledgement, so the model starts with it in context and the UI does not show it. - Agent harness: AGENT_DEFAULT_TOOLS_PRE_CHAT_TOOL (plus optional _ARGS and _MAX_CHARS), looked up in the AGENT_DEFAULT_TOOLS_MCP_ID toolbox and skipped when the agent turns that tool off. - Playground: playground.preChatTool in the active theme map, applied by AskPlayground and returned to the client as preChatMessages. - Both run through PreChatContext: access-checked MCP execution, skipped for missing, disabled or ask tools, truncated, and never failing the chat. - Room auto-naming skips platform-generated messages. --- docs/agents/mcp_tools.md | 19 ++ src/prerna/engine/impl/model/Room.java | 44 ++- .../reactors/AskPlaygroundReactor.java | 6 + .../agent/runtime/PlatformAgentTools.java | 31 ++- .../reactor/agent/runtime/PreChatContext.java | 251 ++++++++++++++++++ .../agent/runtime/SemossAgentHarness.java | 8 + src/prerna/reactor/model/AskRoomReactor.java | 21 ++ src/prerna/theme/PlaygroundThemeUtils.java | 67 +++++ .../runtime/PreChatContextUnitTests.java | 86 ++++++ .../theme/PlaygroundThemeUtilsUnitTests.java | 63 +++++ 10 files changed, 592 insertions(+), 4 deletions(-) create mode 100644 src/prerna/reactor/agent/runtime/PreChatContext.java create mode 100644 test/prerna/reactor/agent/runtime/PreChatContextUnitTests.java create mode 100644 test/prerna/theme/PlaygroundThemeUtilsUnitTests.java diff --git a/docs/agents/mcp_tools.md b/docs/agents/mcp_tools.md index f066c6e5294..934e82c5c29 100644 --- a/docs/agents/mcp_tools.md +++ b/docs/agents/mcp_tools.md @@ -218,6 +218,24 @@ The runtime reads `_meta` from `pixel_mcp.json` and `py_mcp.json` as written, so See [run lifecycle](agent_runs.md) for approval states and [the harness](semoss_harness.md#tools-and-execution-policy) for the built-in tool groups. +## Pre-chat tool + +One toolbox tool can run once, when a room is set up and before its first message. Its result is stored at the start of the conversation as a hidden note and acknowledgement (`visible=false`, `platformGenerated`, ornament `preChatTool`): the model reads it on every later turn and the UI does not show it. A typical choice is a discovery tool such as ECU's `GetRoomResources`, so the model starts with the room's documents, skills and toolboxes in context instead of spending its first step calling the tool. + +| Surface | Configured by | +| --- | --- | +| `RunAgent` (native `semoss` harness) | Deployment properties, only when `AGENT_DEFAULT_TOOLS_MCP_ID` replaces the default tools: `AGENT_DEFAULT_TOOLS_PRE_CHAT_TOOL` (tool name in that toolbox), optional `AGENT_DEFAULT_TOOLS_PRE_CHAT_ARGS` (JSON object) and `AGENT_DEFAULT_TOOLS_PRE_CHAT_MAX_CHARS`. Skipped when the agent turns default tools off or lists the tool in `tool_policy.default_tools.disabled`, and for PPTX workflow runs. | +| `AskPlayground` | `playground.preChatTool` in the active theme map: `{"toolboxId": "", "tool": "GetRoomResources", "args": {}, "maxChars": 60000}`. The stored pair is returned as `preChatMessages`. | + +Both use [PreChatContext](../../src/prerna/reactor/agent/runtime/PreChatContext.java): + +- The tool runs through `MCPUtility.executeTool`, so the user's access to the toolbox is checked and `SMSS_FUNCTION_NAME` applies. +- A tool that is missing, `disabled` or `ask` is skipped; nothing is shown to approve before the first message. +- The insight must be unbound or bound to the room (chat mode binds it with `SetRoomForInsight`); an unbound insight is bound to the room first. +- The result is cut to `maxChars` (default 60,000) and wrapped in `` with a note telling the model it need not call the tool again unless something changed. +- Any failure is logged and skipped, and the chat continues as it would without the setting. The tool stays available, so the model can still call it itself. +- It runs once per room. Files added later are not reflected until the model calls the tool again. + ## Pitfalls - Use lowercase execution values in hand-written JSON. The hide filter is case-sensitive. @@ -238,4 +256,5 @@ See [run lifecycle](agent_runs.md) for approval states and [the harness](semoss_ | Room tool assembly and alias lookup | [Room](../../src/prerna/engine/impl/model/Room.java) | | Deferred tools | [DeferredAgentTools](../../src/prerna/reactor/agent/run/DeferredAgentTools.java) | | Platform tool defaults | [PlatformAgentTools](../../src/prerna/reactor/agent/runtime/PlatformAgentTools.java) | +| Pre-chat tool | [PreChatContext](../../src/prerna/reactor/agent/runtime/PreChatContext.java), [PlaygroundThemeUtils](../../src/prerna/theme/PlaygroundThemeUtils.java) | | Approval pauses | [HarnessToolExecutor](../../src/prerna/reactor/agent/runtime/HarnessToolExecutor.java), [AgentToolDecisionHandler](../../src/prerna/reactor/agent/mcp/AgentToolDecisionHandler.java) | diff --git a/src/prerna/engine/impl/model/Room.java b/src/prerna/engine/impl/model/Room.java index 6c68c407be8..c927acf95af 100644 --- a/src/prerna/engine/impl/model/Room.java +++ b/src/prerna/engine/impl/model/Room.java @@ -95,6 +95,9 @@ public class Room implements Serializable { private static final Logger classLogger = LogManager.getLogger(Room.class); + /** Ornament naming the tool whose result a pre-chat hidden pair holds. */ + public static final String PRE_CHAT_TOOL_ORNAMENT = "preChatTool"; + private static final Gson GSON = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) .disableHtmlEscaping().create(); @@ -401,7 +404,7 @@ public ResponseMessage ask(InputMessage msg, IModelEngine modelEngine, String pa // Try to infer/set roomName if missing if (prevRoomName == null || prevRoomName.trim().isEmpty()) { for (AbstractMessage m : this.messages) { - if (m instanceof InputMessage) { + if (m instanceof InputMessage && !m.isPlatformGenerated()) { InputMessage im = (InputMessage) m; String prompt = im.getInputUIPrompt(); if (prompt != null && !prompt.trim().isEmpty()) { @@ -497,7 +500,7 @@ public ResponseMessage commitPrebuiltTurn(InputMessage msg, IModelEngine modelEn String prevRoomName = roomName; if (prevRoomName == null || prevRoomName.trim().isEmpty()) { for (AbstractMessage m : messages) { - if (m instanceof InputMessage) { + if (m instanceof InputMessage && !m.isPlatformGenerated()) { String prompt = ((InputMessage) m).getInputUIPrompt(); if (prompt != null && !prompt.trim().isEmpty()) { roomName = prompt.substring(0, Math.min(prompt.length(), 100)); @@ -645,6 +648,43 @@ public ResponseMessage appendHarnessResponse(String text, String parentMessageId } } + /** + * Stores a pre-chat tool result as a hidden note and acknowledgement at the + * start of the conversation. The model sees the pair on every later turn; the + * UI does not show it. Nothing is added when the room already has messages, + * checked again under the room lock against the latest stored history. + * + * @param note model-facing text holding the tool result + * @param toolName tool that produced it, recorded as an ornament + * @param modelEngine model associated with the hidden input + * @param insight caller insight, used for the user id when persisting + * @return the appended pair, or an empty list when nothing was added + */ + public List appendPreChatContext(String note, String toolName, IModelEngine modelEngine, + Insight insight) { + ReentrantLock lock = getMessageLock(); + lock.lock(); + try { + String userId = insight.getUser().getPrimaryLoginToken().getId(); + try (RoomMessageStore.RoomMutationLock ignored = RoomMessageStore.acquireMutationLock(this)) { + this.insight = insight; + RoomMessageStore.refreshFromLatestProjection(this, userId); + if (!messages.isEmpty()) { + return new ArrayList<>(); + } + List added = new ArrayList<>(); + MessageUtils.appendHiddenPair(this, modelEngine, note, null, added); + for (AbstractMessage message : added) { + message.setOrnament(PRE_CHAT_TOOL_ORNAMENT, toolName); + } + RoomMessageStore.persist(this, userId); + return added; + } + } finally { + lock.unlock(); + } + } + /** * Flags a message so provider requests replace tool payloads at and above it * with a placeholder. Stored history is unchanged. Returns false when the diff --git a/src/prerna/playground/reactors/AskPlaygroundReactor.java b/src/prerna/playground/reactors/AskPlaygroundReactor.java index 4bf87f8ed92..83cfb6049eb 100644 --- a/src/prerna/playground/reactors/AskPlaygroundReactor.java +++ b/src/prerna/playground/reactors/AskPlaygroundReactor.java @@ -28,6 +28,7 @@ package prerna.playground.reactors; import prerna.playground.PlaygroundUtils; +import prerna.reactor.agent.runtime.PreChatContext; import prerna.reactor.model.AskRoomReactor; import prerna.theme.PlaygroundThemeUtils; @@ -43,6 +44,11 @@ protected boolean shouldHideSystemMessages() { return PlaygroundThemeUtils.hidePlaygroundSystemMessages(); } + @Override + protected PreChatContext.Spec getPreChatTool() { + return PlaygroundThemeUtils.getPlaygroundPreChatTool(); + } + @Override public String getReactorDescription() { return "This method is used to run an LLM text-generation call (Playground) returns both input and response message objects."; diff --git a/src/prerna/reactor/agent/runtime/PlatformAgentTools.java b/src/prerna/reactor/agent/runtime/PlatformAgentTools.java index 46717c2117c..13fcff4a6d0 100644 --- a/src/prerna/reactor/agent/runtime/PlatformAgentTools.java +++ b/src/prerna/reactor/agent/runtime/PlatformAgentTools.java @@ -39,6 +39,7 @@ import prerna.engine.api.IEngine; import prerna.engine.api.ToolExecutionResult; import prerna.reactor.agent.AgentRunContext; +import prerna.reactor.agent.config.AgentConfig; import prerna.reactor.agent.mcp.MCPUtility; import prerna.reactor.agent.mcp.MCPUtility.MCPExecution; import prerna.reactor.agent.mcp.RunMCPToolReactor; @@ -55,6 +56,9 @@ public final class PlatformAgentTools { private static final String PARAM_TOOLS = "tools"; private static final String PROP_DEFAULT_TOOLS_MCP_ID = "AGENT_DEFAULT_TOOLS_MCP_ID"; private static final String PROP_DEFAULT_TOOLS_MCP_PROJECT_ID = "AGENT_DEFAULT_TOOLS_MCP_PROJECT_ID"; + private static final String PROP_PRE_CHAT_TOOL = "AGENT_DEFAULT_TOOLS_PRE_CHAT_TOOL"; + private static final String PROP_PRE_CHAT_ARGS = "AGENT_DEFAULT_TOOLS_PRE_CHAT_ARGS"; + private static final String PROP_PRE_CHAT_MAX_CHARS = "AGENT_DEFAULT_TOOLS_PRE_CHAT_MAX_CHARS"; private static final Set DEFERRED_DEFAULT_TOOLS = Set.of("InspectPptx", "MultiEdit"); private static final Map PLATFORM_TOOLS = @@ -90,6 +94,29 @@ static List> resolveDefaultTools(Map paramMa return resolved; } + /** + * The tool to run before a room's first message, from + * {@code AGENT_DEFAULT_TOOLS_PRE_CHAT_TOOL}. Only set when the deployment + * replaces the default tools ({@code AGENT_DEFAULT_TOOLS_MCP_ID}); the tool is + * looked up in that toolbox. Returns {@code null} when unset, or when this + * agent turns default tools off or disables that tool. + */ + static PreChatContext.Spec preChatSpec(AgentConfig agentConfig) { + String mcpId = getDefaultToolsMcpId(); + String tool = Utility.getDIHelperProperty(PROP_PRE_CHAT_TOOL); + tool = tool != null ? tool.trim() : null; + if (mcpId == null || tool == null || tool.isEmpty()) { + return null; + } + if (agentConfig != null + && (!agentConfig.useDefaultAgentTools() || agentConfig.getDisabledDefaultTools().contains(tool))) { + return null; + } + return PreChatContext.Spec.of(mcpId, tool, + PreChatContext.parseArgs(Utility.getDIHelperProperty(PROP_PRE_CHAT_ARGS)), + PreChatContext.parseInt(Utility.getDIHelperProperty(PROP_PRE_CHAT_MAX_CHARS))); + } + static ToolExecutionResult policyDenialResult(String toolName, AgentRunContext ctx) { if (toolName == null || toolName.trim().isEmpty() || ctx == null) { return null; @@ -169,7 +196,7 @@ private static boolean useDefaultAgentTools(Map paramMap) { return !("false".equals(normalized) || "0".equals(normalized) || "no".equals(normalized)); } - private static String getDefaultToolsMcpId() { + static String getDefaultToolsMcpId() { String id = Utility.getDIHelperProperty(PROP_DEFAULT_TOOLS_MCP_ID); if (id == null || id.trim().isEmpty()) { id = Utility.getDIHelperProperty(PROP_DEFAULT_TOOLS_MCP_PROJECT_ID); @@ -206,7 +233,7 @@ private static List> getMcpToolDefinitions(String mcpId) { return tools; } - private static JSONObject findMcpTool(String mcpId, String toolName) { + static JSONObject findMcpTool(String mcpId, String toolName) { JSONObject toolMap = getMcpToolsJson(mcpId); JSONArray arr = toolMap != null ? toolMap.optJSONArray(PARAM_TOOLS) : null; if (arr == null) { diff --git a/src/prerna/reactor/agent/runtime/PreChatContext.java b/src/prerna/reactor/agent/runtime/PreChatContext.java new file mode 100644 index 00000000000..b29ecb5c87f --- /dev/null +++ b/src/prerna/reactor/agent/runtime/PreChatContext.java @@ -0,0 +1,251 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.agent.runtime; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.JSONObject; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import prerna.engine.api.IModelEngine; +import prerna.engine.impl.model.Room; +import prerna.engine.impl.model.message.AbstractMessage; +import prerna.om.Insight; +import prerna.reactor.agent.mcp.MCPUtility; +import prerna.reactor.agent.mcp.MCPUtility.MCPExecution; +import prerna.sablecc2.om.nounmeta.NounMetadata; + +/** + * Runs one toolbox tool when a room is set up, before its first message, and + * stores the result as a hidden note the model reads but the UI does not show. + *

+ * Two surfaces use it: + *

    + *
  • the {@code semoss} agent harness, when the deployment replaces the default + * agent tools ({@code AGENT_DEFAULT_TOOLS_MCP_ID}) and names a pre-chat tool + * ({@code AGENT_DEFAULT_TOOLS_PRE_CHAT_TOOL});
  • + *
  • Playground chat, from {@code playground.preChatTool} in the active theme + * map.
  • + *
+ * The tool runs through {@link MCPUtility#executeTool}, so the caller's access to + * the toolbox is checked and the toolbox's own {@code SMSS_FUNCTION_NAME} mapping + * applies. A tool that is missing, {@code disabled} or {@code ask} is skipped: + * nothing is shown to approve before the first message. Every failure is logged + * and skipped, so the chat carries on as it would without the feature, and the + * model can still call the tool itself. + */ +public final class PreChatContext { + + private static final Logger logger = LogManager.getLogger(PreChatContext.class); + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); + + /** Result characters kept when the configuration does not set a limit. */ + public static final int DEFAULT_MAX_CHARS = 60_000; + + private PreChatContext() { + } + + /** + * Which tool to run and with what arguments. + * + * @param toolboxId engine or project id of the toolbox (MCP) + * @param tool tool name as listed by the toolbox + * @param args tool arguments, never {@code null} + * @param maxChars result characters kept in the hidden note + */ + public record Spec(String toolboxId, String tool, Map args, int maxChars) { + + public Spec { + args = args == null ? Collections.emptyMap() : Collections.unmodifiableMap(new LinkedHashMap<>(args)); + maxChars = maxChars > 0 ? maxChars : DEFAULT_MAX_CHARS; + } + + /** + * Builds a spec, or returns {@code null} when the toolbox or tool is blank. + */ + public static Spec of(String toolboxId, String tool, Map args, Integer maxChars) { + toolboxId = trimToNull(toolboxId); + tool = trimToNull(tool); + if (toolboxId == null || tool == null) { + return null; + } + return new Spec(toolboxId, tool, args, maxChars == null ? 0 : maxChars); + } + } + + /** + * Runs the tool and stores its result when the room has no messages yet. + * + * @param room room about to receive its first message + * @param insight caller insight; must be unbound or bound to {@code room} + * @param modelEngine model the first message will be sent to + * @param spec tool to run; {@code null} does nothing + * @return the hidden pair that was stored, or an empty list + */ + public static List injectIfFirstMessage(Room room, Insight insight, IModelEngine modelEngine, + Spec spec) { + if (spec == null || room == null || insight == null || modelEngine == null + || !room.getMessages().isEmpty()) { + return new ArrayList<>(); + } + try { + // Room-scoped tools read the room from the insight. In chat mode the client + // binds it with SetRoomForInsight; bind it here if it has not, and never run + // against a different room. + String boundRoomId = trimToNull(insight.getRoomId()); + if (boundRoomId == null) { + insight.setRoomForInsight(room); + } else if (!boundRoomId.equals(room.getId())) { + logger.warn("PreChatContext: skipped {} for room={}: insight is bound to room={}", spec.tool(), + room.getId(), boundRoomId); + return new ArrayList<>(); + } + + JSONObject tool = PlatformAgentTools.findMcpTool(spec.toolboxId(), spec.tool()); + if (tool == null) { + logger.warn("PreChatContext: skipped room={}: tool {} is missing or disabled in toolbox {}", + room.getId(), spec.tool(), spec.toolboxId()); + return new ArrayList<>(); + } + JSONObject meta = tool.optJSONObject("_meta"); + if (meta != null && MCPExecution.ASK.getValue().equalsIgnoreCase(meta.optString(MCPUtility.SMSS_MCP_EXECUTION))) { + logger.warn("PreChatContext: skipped room={}: tool {} needs user approval", room.getId(), spec.tool()); + return new ArrayList<>(); + } + String functionName = meta != null ? meta.optString(MCPUtility.SMSS_FUNCTION_NAME, spec.tool()) + : spec.tool(); + + long start = System.currentTimeMillis(); + Object raw = MCPUtility.executeTool(spec.toolboxId(), functionName, new LinkedHashMap<>(spec.args()), + insight); + String output = stringify(raw); + if (output == null || output.isBlank() || output.startsWith("Error:")) { + logger.warn("PreChatContext: skipped room={}: tool {} returned no usable result", room.getId(), + spec.tool()); + return new ArrayList<>(); + } + + List added = room.appendPreChatContext(note(spec.tool(), output, spec.maxChars()), + spec.tool(), modelEngine, insight); + logger.info("PreChatContext: room={} tool={} chars={} stored={} ms={}", room.getId(), spec.tool(), + output.length(), !added.isEmpty(), System.currentTimeMillis() - start); + return added; + } catch (Exception e) { + logger.warn("PreChatContext: skipped {} for room={}: {}", spec.tool(), room.getId(), e.getMessage(), e); + return new ArrayList<>(); + } + } + + /** + * Wraps a tool result for the model, truncating it to {@code maxChars}. + */ + static String note(String toolName, String output, int maxChars) { + boolean truncated = output.length() > maxChars; + String body = truncated ? output.substring(0, maxChars) : output; + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append(body); + if (truncated) { + sb.append("\n[truncated: ").append(output.length() - maxChars).append(" more characters]"); + } + sb.append("\n\n\n"); + sb.append(""" + The platform ran %s when this chat started, before the user's first message; its result is above. \ + Treat it as if you had called the tool yourself. Don't call %s again unless the user adds or \ + mentions new material, asks what is available, or you need something this result doesn't cover.\ + """.formatted(toolName, toolName)); + if (truncated) { + sb.append(" The result was truncated, so call ").append(toolName) + .append(" if you need the part that was cut."); + } + sb.append(" Do not mention this note to the user."); + return sb.toString(); + } + + static String stringify(Object raw) { + if (raw instanceof NounMetadata noun) { + raw = noun.getValue(); + } + if (raw == null) { + return null; + } + if (raw instanceof String s) { + return s; + } + return GSON.toJson(raw); + } + + /** + * Parses a JSON object of tool arguments; blank or invalid input yields an + * empty map. + */ + static Map parseArgs(String json) { + json = trimToNull(json); + if (json == null) { + return Collections.emptyMap(); + } + try { + return new JSONObject(json).toMap(); + } catch (Exception e) { + logger.warn("PreChatContext: ignoring invalid tool arguments: {}", e.getMessage()); + return Collections.emptyMap(); + } + } + + static Integer parseInt(String value) { + value = trimToNull(value); + if (value == null) { + return null; + } + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + } + + private static String trimToNull(String s) { + if (s == null) { + return null; + } + s = s.trim(); + return s.isEmpty() ? null : s; + } + + private static String xmlEscape(String s) { + return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); + } +} diff --git a/src/prerna/reactor/agent/runtime/SemossAgentHarness.java b/src/prerna/reactor/agent/runtime/SemossAgentHarness.java index d5b9bec0e7f..152b9875563 100644 --- a/src/prerna/reactor/agent/runtime/SemossAgentHarness.java +++ b/src/prerna/reactor/agent/runtime/SemossAgentHarness.java @@ -359,6 +359,14 @@ public AgentHarnessResult execute(AgentRunContext ctx) throws Exception { if (compactionOutcome == AutoCompactionOutcome.CONTEXT_WINDOW_UNAVAILABLE) { autoCompactionContextWarningLogged = true; } + // A deployment that replaces the default tools can name one to run before the + // room's first message; its result is stored as a hidden note for the model. + // Like compaction, it is not a message produced by this run, so it sits above + // runMessageStartIndex. + if (!focusedEdit && !agentConfig.hasPptxWorkflow() && room.getMessages().isEmpty()) { + PreChatContext.injectIfFirstMessage(room, ctx.getInsight(), ctx.getModelEngine(), + PlatformAgentTools.preChatSpec(agentConfig)); + } // Compaction messages are implementation history, not messages produced by this // run. Start run tagging after any automatic compaction messages. runMessageStartIndex = room.getMessages().size(); diff --git a/src/prerna/reactor/model/AskRoomReactor.java b/src/prerna/reactor/model/AskRoomReactor.java index f0546cc09dd..e4a20465535 100644 --- a/src/prerna/reactor/model/AskRoomReactor.java +++ b/src/prerna/reactor/model/AskRoomReactor.java @@ -46,6 +46,7 @@ import prerna.engine.impl.model.message.MessageUtils; import prerna.engine.impl.model.message.ResponseMessage; import prerna.reactor.AbstractReactor; +import prerna.reactor.agent.runtime.PreChatContext; import prerna.sablecc2.om.PixelDataType; import prerna.sablecc2.om.ReactorKeysEnum; import prerna.sablecc2.om.nounmeta.NounMetadata; @@ -104,6 +105,10 @@ public NounMetadata execute() { if (projectIdOverride != null) { room.setProjectId(projectIdOverride); } + // A prebuilt turn was produced by the client, so nothing runs ahead of it. + List preChatMessages = responseParts == null + ? PreChatContext.injectIfFirstMessage(room, this.insight, modelEngine, getPreChatTool()) + : new ArrayList<>(); String givenSystemPrompt = room.getSystemPromptForModel(); List copiedMedia = RoomUtils.copyFilesToRoomFolder(inputMedia, room, insight); @@ -141,6 +146,14 @@ public NounMetadata execute() { extraMessagesList.add(pair); } pixelReturn.put("extraMessages", extraMessagesList); + if (!preChatMessages.isEmpty()) { + // hidden note and acknowledgement stored ahead of this turn + List> preChatList = new ArrayList<>(); + for (AbstractMessage message : preChatMessages) { + preChatList.add(jsonToMap(MessageUtils.toJsonWithImage(message))); + } + pixelReturn.put("preChatMessages", preChatList); + } return new NounMetadata(pixelReturn, PixelDataType.MAP); } @@ -153,6 +166,14 @@ protected String getProjectIdOverride() { return null; } + /** + * Returns the tool to run once, before a new room's first message, or + * {@code null} for none. Its result is stored as a hidden note for the model. + */ + protected PreChatContext.Spec getPreChatTool() { + return null; + } + /** * Returns whether system content should be removed from the client response. */ diff --git a/src/prerna/theme/PlaygroundThemeUtils.java b/src/prerna/theme/PlaygroundThemeUtils.java index b8d30459c11..cafc2e9fbac 100644 --- a/src/prerna/theme/PlaygroundThemeUtils.java +++ b/src/prerna/theme/PlaygroundThemeUtils.java @@ -34,12 +34,14 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.json.JSONObject; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import prerna.engine.api.IRDBMSEngine; +import prerna.reactor.agent.runtime.PreChatContext; import prerna.util.Constants; import prerna.util.SystemEngineRegistry; @@ -53,6 +55,7 @@ public class PlaygroundThemeUtils extends AbstractThemeUtils { private static volatile String cachedGlobalSystemPrompt = null; private static volatile Map cachedSystemPromptVars = null; private static volatile boolean cachedHideSystemMessages = false; + private static volatile PreChatContext.Spec cachedPreChatTool = null; private static volatile boolean cacheInitialized = false; private PlaygroundThemeUtils() { @@ -99,6 +102,32 @@ public static boolean hidePlaygroundSystemMessages() { return cachedHideSystemMessages; } + /** + * Returns {@code playground.preChatTool} from the currently active theme, or + * {@code null} when it is not set or names no toolbox or tool. Playground runs + * this tool once, before a room's first message, and gives its result to the + * model as a hidden note. + *

+ * Expected JSON shape ({@code args} and {@code maxChars} are optional): + * + *

+	 * {
+	 *   "playground": {
+	 *     "preChatTool": {
+	 *       "toolboxId": "<engine or project id>",
+	 *       "tool": "GetRoomResources",
+	 *       "args": {},
+	 *       "maxChars": 60000
+	 *     }
+	 *   }
+	 * }
+	 * 
+ */ + public static PreChatContext.Spec getPlaygroundPreChatTool() { + ensureCacheLoaded(); + return cachedPreChatTool; + } + /** * Refreshes the in-memory cache from the active theme. */ @@ -114,6 +143,7 @@ private static void refreshCache() { cachedGlobalSystemPrompt = null; cachedSystemPromptVars = null; cachedHideSystemMessages = false; + cachedPreChatTool = null; cacheInitialized = false; } } @@ -129,6 +159,7 @@ private static void parseThemeMap(String themeMapJson) { cachedGlobalSystemPrompt = null; cachedSystemPromptVars = new LinkedHashMap<>(); cachedHideSystemMessages = false; + cachedPreChatTool = null; if (themeMapJson == null) { return; } @@ -143,6 +174,8 @@ private static void parseThemeMap(String themeMapJson) { cachedHideSystemMessages = getBooleanValue(playground, "hide_system_messages", getBooleanValue(playground, "hideSystemMessages", false)); + cachedPreChatTool = parsePreChatTool(playground.get("preChatTool")); + JsonElement globalSystemPromptElem = playground.get("globalSystemPrompt"); if (globalSystemPromptElem != null && globalSystemPromptElem.isJsonPrimitive()) { cachedGlobalSystemPrompt = StringUtils.trimToNull(globalSystemPromptElem.getAsString()); @@ -169,6 +202,40 @@ private static void parseThemeMap(String themeMapJson) { } } + /** + * Reads a {@code preChatTool} object; anything missing a toolbox id or tool + * name yields {@code null}. + */ + static PreChatContext.Spec parsePreChatTool(JsonElement elem) { + if (elem == null || !elem.isJsonObject()) { + return null; + } + JsonObject obj = elem.getAsJsonObject(); + Map args = null; + JsonElement argsElem = obj.get("args"); + if (argsElem != null && argsElem.isJsonObject()) { + args = new JSONObject(argsElem.toString()).toMap(); + } + Integer maxChars = null; + JsonElement maxElem = obj.get("maxChars"); + try { + if (maxElem != null && maxElem.isJsonPrimitive() && maxElem.getAsJsonPrimitive().isNumber()) { + maxChars = maxElem.getAsInt(); + } + } catch (NumberFormatException e) { + // out of range: use the default + } + return PreChatContext.Spec.of(getStringValue(obj, "toolboxId"), getStringValue(obj, "tool"), args, maxChars); + } + + private static String getStringValue(JsonObject obj, String key) { + JsonElement elem = obj.get(key); + if (elem == null || !elem.isJsonPrimitive()) { + return null; + } + return StringUtils.trimToNull(elem.getAsString()); + } + private static boolean getBooleanValue(JsonObject obj, String key, boolean defaultValue) { JsonElement elem = obj.get(key); if (elem == null || elem.isJsonNull() || !elem.isJsonPrimitive()) { diff --git a/test/prerna/reactor/agent/runtime/PreChatContextUnitTests.java b/test/prerna/reactor/agent/runtime/PreChatContextUnitTests.java new file mode 100644 index 00000000000..4cb1ccabe78 --- /dev/null +++ b/test/prerna/reactor/agent/runtime/PreChatContextUnitTests.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.agent.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.nounmeta.NounMetadata; + +class PreChatContextUnitTests { + + @Test + void specNeedsToolboxAndTool() { + assertNull(PreChatContext.Spec.of(null, "GetRoomResources", null, null)); + assertNull(PreChatContext.Spec.of("ecu", " ", null, null)); + PreChatContext.Spec spec = PreChatContext.Spec.of(" ecu ", " GetRoomResources ", null, null); + assertEquals("ecu", spec.toolboxId()); + assertEquals("GetRoomResources", spec.tool()); + assertTrue(spec.args().isEmpty()); + assertEquals(PreChatContext.DEFAULT_MAX_CHARS, spec.maxChars()); + assertEquals(PreChatContext.DEFAULT_MAX_CHARS, PreChatContext.Spec.of("e", "t", null, -5).maxChars()); + assertEquals(10, PreChatContext.Spec.of("e", "t", Map.of("a", 1), 10).maxChars()); + } + + @Test + void parsesArgsAndLimits() { + assertEquals(Map.of("limit", 5), PreChatContext.parseArgs("{\"limit\": 5}")); + assertTrue(PreChatContext.parseArgs("not json").isEmpty()); + assertTrue(PreChatContext.parseArgs(" ").isEmpty()); + assertEquals(42, PreChatContext.parseInt(" 42 ")); + assertNull(PreChatContext.parseInt("lots")); + } + + @Test + void noteWrapsAndTruncates() { + String whole = PreChatContext.note("GetRoomResources", "{\"room_files\":[]}", 100); + assertTrue(whole.startsWith("\n{\"room_files\":[]}\n")); + assertTrue(whole.contains("Don't call GetRoomResources again")); + assertFalse(whole.contains("truncated")); + + String cut = PreChatContext.note("T", "abcdefghij", 4); + assertTrue(cut.startsWith("\nabcd\n[truncated: 6 more characters]")); + assertTrue(cut.contains("The result was truncated")); + } + + @Test + void stringifiesToolOutput() { + assertEquals("text", PreChatContext.stringify("text")); + assertEquals("ok", PreChatContext.stringify(new NounMetadata("ok", PixelDataType.CONST_STRING))); + assertEquals("[1,2]", PreChatContext.stringify(List.of(1, 2))); + assertNull(PreChatContext.stringify(null)); + } +} diff --git a/test/prerna/theme/PlaygroundThemeUtilsUnitTests.java b/test/prerna/theme/PlaygroundThemeUtilsUnitTests.java new file mode 100644 index 00000000000..94510c543fe --- /dev/null +++ b/test/prerna/theme/PlaygroundThemeUtilsUnitTests.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.theme; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.google.gson.JsonParser; + +import prerna.reactor.agent.runtime.PreChatContext; + +class PlaygroundThemeUtilsUnitTests { + + @Test + void readsPreChatTool() { + PreChatContext.Spec spec = PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString( + "{\"toolboxId\":\"ecu\",\"tool\":\"GetRoomResources\",\"args\":{\"mode\":\"brief\"},\"maxChars\":500}")); + assertEquals("ecu", spec.toolboxId()); + assertEquals("GetRoomResources", spec.tool()); + assertEquals(Map.of("mode", "brief"), spec.args()); + assertEquals(500, spec.maxChars()); + } + + @Test + void ignoresIncompleteOrMalformedPreChatTool() { + assertNull(PlaygroundThemeUtils.parsePreChatTool(null)); + assertNull(PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString("\"GetRoomResources\""))); + assertNull(PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString("{\"tool\":\"GetRoomResources\"}"))); + PreChatContext.Spec spec = PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString( + "{\"toolboxId\":\"ecu\",\"tool\":\"T\",\"args\":[1],\"maxChars\":\"big\"}")); + assertEquals(Map.of(), spec.args()); + assertEquals(PreChatContext.DEFAULT_MAX_CHARS, spec.maxChars()); + } +} From cc5e9e8c8c25d299977479722ec1bac1a9aa8736 Mon Sep 17 00:00:00 2001 From: ckelly17 Date: Tue, 3 Nov 2026 07:19:48 +0000 Subject: [PATCH 2/5] feat: fall back to playground default tools for the pre-chat toolbox When playground.preChatTool has no toolboxId, look for the tool in the theme's playground.defaultTools (the toolboxes the UI attaches to every new room), in order, and run it from the first toolbox that has it. --- docs/agents/mcp_tools.md | 2 +- .../reactor/agent/runtime/PreChatContext.java | 62 ++++++++++++++----- src/prerna/theme/PlaygroundThemeUtils.java | 30 +++++++-- .../runtime/PreChatContextUnitTests.java | 5 +- .../theme/PlaygroundThemeUtilsUnitTests.java | 24 +++++-- 5 files changed, 94 insertions(+), 29 deletions(-) diff --git a/docs/agents/mcp_tools.md b/docs/agents/mcp_tools.md index 934e82c5c29..74c74deb882 100644 --- a/docs/agents/mcp_tools.md +++ b/docs/agents/mcp_tools.md @@ -225,7 +225,7 @@ One toolbox tool can run once, when a room is set up and before its first messag | Surface | Configured by | | --- | --- | | `RunAgent` (native `semoss` harness) | Deployment properties, only when `AGENT_DEFAULT_TOOLS_MCP_ID` replaces the default tools: `AGENT_DEFAULT_TOOLS_PRE_CHAT_TOOL` (tool name in that toolbox), optional `AGENT_DEFAULT_TOOLS_PRE_CHAT_ARGS` (JSON object) and `AGENT_DEFAULT_TOOLS_PRE_CHAT_MAX_CHARS`. Skipped when the agent turns default tools off or lists the tool in `tool_policy.default_tools.disabled`, and for PPTX workflow runs. | -| `AskPlayground` | `playground.preChatTool` in the active theme map: `{"toolboxId": "", "tool": "GetRoomResources", "args": {}, "maxChars": 60000}`. The stored pair is returned as `preChatMessages`. | +| `AskPlayground` | `playground.preChatTool` in the active theme map: `{"tool": "GetRoomResources", "toolboxId": "", "args": {}, "maxChars": 60000}`. Without `toolboxId`, the tool is looked up in `playground.defaultTools` (the toolboxes the UI attaches to every new room), in order. The stored pair is returned as `preChatMessages`. | Both use [PreChatContext](../../src/prerna/reactor/agent/runtime/PreChatContext.java): diff --git a/src/prerna/reactor/agent/runtime/PreChatContext.java b/src/prerna/reactor/agent/runtime/PreChatContext.java index b29ecb5c87f..402afc41f99 100644 --- a/src/prerna/reactor/agent/runtime/PreChatContext.java +++ b/src/prerna/reactor/agent/runtime/PreChatContext.java @@ -81,28 +81,48 @@ private PreChatContext() { /** * Which tool to run and with what arguments. * - * @param toolboxId engine or project id of the toolbox (MCP) - * @param tool tool name as listed by the toolbox - * @param args tool arguments, never {@code null} - * @param maxChars result characters kept in the hidden note + * @param toolboxIds engine or project ids of the toolboxes (MCPs) to look in, + * in order; the first one that provides {@code tool} runs it + * @param tool tool name as listed by the toolbox + * @param args tool arguments, never {@code null} + * @param maxChars result characters kept in the hidden note */ - public record Spec(String toolboxId, String tool, Map args, int maxChars) { + public record Spec(List toolboxIds, String tool, Map args, int maxChars) { public Spec { + toolboxIds = List.copyOf(toolboxIds); args = args == null ? Collections.emptyMap() : Collections.unmodifiableMap(new LinkedHashMap<>(args)); maxChars = maxChars > 0 ? maxChars : DEFAULT_MAX_CHARS; } /** - * Builds a spec, or returns {@code null} when the toolbox or tool is blank. + * Builds a spec for one toolbox, or returns {@code null} when the toolbox or + * tool is blank. */ public static Spec of(String toolboxId, String tool, Map args, Integer maxChars) { - toolboxId = trimToNull(toolboxId); + return anyOf(toolboxId == null ? List.of() : java.util.Arrays.asList(toolboxId), tool, args, maxChars); + } + + /** + * Builds a spec that looks for the tool in each toolbox in turn, or returns + * {@code null} when no toolbox id is given or the tool is blank. Blank and + * repeated ids are dropped. + */ + public static Spec anyOf(List toolboxIds, String tool, Map args, Integer maxChars) { tool = trimToNull(tool); - if (toolboxId == null || tool == null) { + java.util.Set ids = new java.util.LinkedHashSet<>(); + if (toolboxIds != null) { + for (String id : toolboxIds) { + id = trimToNull(id); + if (id != null) { + ids.add(id); + } + } + } + if (ids.isEmpty() || tool == null) { return null; } - return new Spec(toolboxId, tool, args, maxChars == null ? 0 : maxChars); + return new Spec(new ArrayList<>(ids), tool, args, maxChars == null ? 0 : maxChars); } } @@ -134,10 +154,22 @@ public static List injectIfFirstMessage(Room room, Insight insi return new ArrayList<>(); } - JSONObject tool = PlatformAgentTools.findMcpTool(spec.toolboxId(), spec.tool()); + String toolboxId = null; + JSONObject tool = null; + for (String candidate : spec.toolboxIds()) { + try { + tool = PlatformAgentTools.findMcpTool(candidate, spec.tool()); + } catch (Exception e) { + logger.warn("PreChatContext: cannot read toolbox {}: {}", candidate, e.getMessage()); + } + if (tool != null) { + toolboxId = candidate; + break; + } + } if (tool == null) { - logger.warn("PreChatContext: skipped room={}: tool {} is missing or disabled in toolbox {}", - room.getId(), spec.tool(), spec.toolboxId()); + logger.warn("PreChatContext: skipped room={}: tool {} is missing or disabled in toolboxes {}", + room.getId(), spec.tool(), spec.toolboxIds()); return new ArrayList<>(); } JSONObject meta = tool.optJSONObject("_meta"); @@ -149,7 +181,7 @@ public static List injectIfFirstMessage(Room room, Insight insi : spec.tool(); long start = System.currentTimeMillis(); - Object raw = MCPUtility.executeTool(spec.toolboxId(), functionName, new LinkedHashMap<>(spec.args()), + Object raw = MCPUtility.executeTool(toolboxId, functionName, new LinkedHashMap<>(spec.args()), insight); String output = stringify(raw); if (output == null || output.isBlank() || output.startsWith("Error:")) { @@ -160,8 +192,8 @@ public static List injectIfFirstMessage(Room room, Insight insi List added = room.appendPreChatContext(note(spec.tool(), output, spec.maxChars()), spec.tool(), modelEngine, insight); - logger.info("PreChatContext: room={} tool={} chars={} stored={} ms={}", room.getId(), spec.tool(), - output.length(), !added.isEmpty(), System.currentTimeMillis() - start); + logger.info("PreChatContext: room={} toolbox={} tool={} chars={} stored={} ms={}", room.getId(), toolboxId, + spec.tool(), output.length(), !added.isEmpty(), System.currentTimeMillis() - start); return added; } catch (Exception e) { logger.warn("PreChatContext: skipped {} for room={}: {}", spec.tool(), room.getId(), e.getMessage(), e); diff --git a/src/prerna/theme/PlaygroundThemeUtils.java b/src/prerna/theme/PlaygroundThemeUtils.java index cafc2e9fbac..359548369c8 100644 --- a/src/prerna/theme/PlaygroundThemeUtils.java +++ b/src/prerna/theme/PlaygroundThemeUtils.java @@ -27,8 +27,10 @@ *******************************************************************************/ package prerna.theme; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; @@ -114,7 +116,7 @@ public static boolean hidePlaygroundSystemMessages() { * { * "playground": { * "preChatTool": { - * "toolboxId": "<engine or project id>", + * "toolboxId": "<engine or project id; defaults to defaultTools>", * "tool": "GetRoomResources", * "args": {}, * "maxChars": 60000 @@ -174,7 +176,7 @@ private static void parseThemeMap(String themeMapJson) { cachedHideSystemMessages = getBooleanValue(playground, "hide_system_messages", getBooleanValue(playground, "hideSystemMessages", false)); - cachedPreChatTool = parsePreChatTool(playground.get("preChatTool")); + cachedPreChatTool = parsePreChatTool(playground.get("preChatTool"), playground.get("defaultTools")); JsonElement globalSystemPromptElem = playground.get("globalSystemPrompt"); if (globalSystemPromptElem != null && globalSystemPromptElem.isJsonPrimitive()) { @@ -203,14 +205,30 @@ private static void parseThemeMap(String themeMapJson) { } /** - * Reads a {@code preChatTool} object; anything missing a toolbox id or tool - * name yields {@code null}. + * Reads a {@code preChatTool} object. Without its own {@code toolboxId}, the + * tool is looked up in the theme's {@code defaultTools} (the toolboxes the UI + * attaches to every new room), in order. Anything with no tool name or no + * toolbox to look in yields {@code null}. */ - static PreChatContext.Spec parsePreChatTool(JsonElement elem) { + static PreChatContext.Spec parsePreChatTool(JsonElement elem, JsonElement defaultTools) { if (elem == null || !elem.isJsonObject()) { return null; } JsonObject obj = elem.getAsJsonObject(); + List toolboxIds = new ArrayList<>(); + String toolboxId = getStringValue(obj, "toolboxId"); + if (toolboxId != null) { + toolboxIds.add(toolboxId); + } else if (defaultTools != null && defaultTools.isJsonArray()) { + for (JsonElement tool : defaultTools.getAsJsonArray()) { + if (tool != null && tool.isJsonObject()) { + String id = getStringValue(tool.getAsJsonObject(), "id"); + if (id != null) { + toolboxIds.add(id); + } + } + } + } Map args = null; JsonElement argsElem = obj.get("args"); if (argsElem != null && argsElem.isJsonObject()) { @@ -225,7 +243,7 @@ static PreChatContext.Spec parsePreChatTool(JsonElement elem) { } catch (NumberFormatException e) { // out of range: use the default } - return PreChatContext.Spec.of(getStringValue(obj, "toolboxId"), getStringValue(obj, "tool"), args, maxChars); + return PreChatContext.Spec.anyOf(toolboxIds, getStringValue(obj, "tool"), args, maxChars); } private static String getStringValue(JsonObject obj, String key) { diff --git a/test/prerna/reactor/agent/runtime/PreChatContextUnitTests.java b/test/prerna/reactor/agent/runtime/PreChatContextUnitTests.java index 4cb1ccabe78..8ba12c0d017 100644 --- a/test/prerna/reactor/agent/runtime/PreChatContextUnitTests.java +++ b/test/prerna/reactor/agent/runtime/PreChatContextUnitTests.java @@ -47,12 +47,15 @@ void specNeedsToolboxAndTool() { assertNull(PreChatContext.Spec.of(null, "GetRoomResources", null, null)); assertNull(PreChatContext.Spec.of("ecu", " ", null, null)); PreChatContext.Spec spec = PreChatContext.Spec.of(" ecu ", " GetRoomResources ", null, null); - assertEquals("ecu", spec.toolboxId()); + assertEquals(List.of("ecu"), spec.toolboxIds()); assertEquals("GetRoomResources", spec.tool()); assertTrue(spec.args().isEmpty()); assertEquals(PreChatContext.DEFAULT_MAX_CHARS, spec.maxChars()); assertEquals(PreChatContext.DEFAULT_MAX_CHARS, PreChatContext.Spec.of("e", "t", null, -5).maxChars()); assertEquals(10, PreChatContext.Spec.of("e", "t", Map.of("a", 1), 10).maxChars()); + assertEquals(List.of("a", "b"), + PreChatContext.Spec.anyOf(java.util.Arrays.asList(" a ", null, "", "b", "a"), "t", null, null).toolboxIds()); + assertNull(PreChatContext.Spec.anyOf(List.of(), "t", null, null)); } @Test diff --git a/test/prerna/theme/PlaygroundThemeUtilsUnitTests.java b/test/prerna/theme/PlaygroundThemeUtilsUnitTests.java index 94510c543fe..830f124f73c 100644 --- a/test/prerna/theme/PlaygroundThemeUtilsUnitTests.java +++ b/test/prerna/theme/PlaygroundThemeUtilsUnitTests.java @@ -30,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -43,20 +44,31 @@ class PlaygroundThemeUtilsUnitTests { @Test void readsPreChatTool() { PreChatContext.Spec spec = PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString( - "{\"toolboxId\":\"ecu\",\"tool\":\"GetRoomResources\",\"args\":{\"mode\":\"brief\"},\"maxChars\":500}")); - assertEquals("ecu", spec.toolboxId()); + "{\"toolboxId\":\"ecu\",\"tool\":\"GetRoomResources\",\"args\":{\"mode\":\"brief\"},\"maxChars\":500}"), + JsonParser.parseString("[{\"id\":\"other\"}]")); + assertEquals(List.of("ecu"), spec.toolboxIds()); assertEquals("GetRoomResources", spec.tool()); assertEquals(Map.of("mode", "brief"), spec.args()); assertEquals(500, spec.maxChars()); } + @Test + void fallsBackToThemeDefaultTools() { + PreChatContext.Spec spec = PlaygroundThemeUtils.parsePreChatTool( + JsonParser.parseString("{\"tool\":\"GetRoomResources\"}"), + JsonParser.parseString( + "[{\"type\":\"PROJECT\",\"id\":\"ecu\",\"name\":\"ECU\"},{\"id\":\" \"},\"bad\",{\"id\":\"other\"}]")); + assertEquals(List.of("ecu", "other"), spec.toolboxIds()); + assertEquals("GetRoomResources", spec.tool()); + } + @Test void ignoresIncompleteOrMalformedPreChatTool() { - assertNull(PlaygroundThemeUtils.parsePreChatTool(null)); - assertNull(PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString("\"GetRoomResources\""))); - assertNull(PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString("{\"tool\":\"GetRoomResources\"}"))); + assertNull(PlaygroundThemeUtils.parsePreChatTool(null, null)); + assertNull(PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString("\"GetRoomResources\""), null)); + assertNull(PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString("{\"tool\":\"GetRoomResources\"}"), null)); PreChatContext.Spec spec = PlaygroundThemeUtils.parsePreChatTool(JsonParser.parseString( - "{\"toolboxId\":\"ecu\",\"tool\":\"T\",\"args\":[1],\"maxChars\":\"big\"}")); + "{\"toolboxId\":\"ecu\",\"tool\":\"T\",\"args\":[1],\"maxChars\":\"big\"}"), null); assertEquals(Map.of(), spec.args()); assertEquals(PreChatContext.DEFAULT_MAX_CHARS, spec.maxChars()); } From 22d99b6337ba3b436ff083ba8d6e4aa1f9ff4e8c Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 4 Nov 2026 21:51:42 -0800 Subject: [PATCH 3/5] feat: pre-chat tool run from deployment standard --- src/prerna/engine/impl/model/Room.java | 40 ------ .../reactor/agent/runtime/PreChatContext.java | 127 +++++++++--------- .../agent/runtime/SemossAgentHarness.java | 19 +-- src/prerna/reactor/model/AskRoomReactor.java | 30 ++--- 4 files changed, 90 insertions(+), 126 deletions(-) diff --git a/src/prerna/engine/impl/model/Room.java b/src/prerna/engine/impl/model/Room.java index c927acf95af..e30b41e6308 100644 --- a/src/prerna/engine/impl/model/Room.java +++ b/src/prerna/engine/impl/model/Room.java @@ -95,9 +95,6 @@ public class Room implements Serializable { private static final Logger classLogger = LogManager.getLogger(Room.class); - /** Ornament naming the tool whose result a pre-chat hidden pair holds. */ - public static final String PRE_CHAT_TOOL_ORNAMENT = "preChatTool"; - private static final Gson GSON = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) .disableHtmlEscaping().create(); @@ -648,43 +645,6 @@ public ResponseMessage appendHarnessResponse(String text, String parentMessageId } } - /** - * Stores a pre-chat tool result as a hidden note and acknowledgement at the - * start of the conversation. The model sees the pair on every later turn; the - * UI does not show it. Nothing is added when the room already has messages, - * checked again under the room lock against the latest stored history. - * - * @param note model-facing text holding the tool result - * @param toolName tool that produced it, recorded as an ornament - * @param modelEngine model associated with the hidden input - * @param insight caller insight, used for the user id when persisting - * @return the appended pair, or an empty list when nothing was added - */ - public List appendPreChatContext(String note, String toolName, IModelEngine modelEngine, - Insight insight) { - ReentrantLock lock = getMessageLock(); - lock.lock(); - try { - String userId = insight.getUser().getPrimaryLoginToken().getId(); - try (RoomMessageStore.RoomMutationLock ignored = RoomMessageStore.acquireMutationLock(this)) { - this.insight = insight; - RoomMessageStore.refreshFromLatestProjection(this, userId); - if (!messages.isEmpty()) { - return new ArrayList<>(); - } - List added = new ArrayList<>(); - MessageUtils.appendHiddenPair(this, modelEngine, note, null, added); - for (AbstractMessage message : added) { - message.setOrnament(PRE_CHAT_TOOL_ORNAMENT, toolName); - } - RoomMessageStore.persist(this, userId); - return added; - } - } finally { - lock.unlock(); - } - } - /** * Flags a message so provider requests replace tool payloads at and above it * with a placeholder. Stored history is unchanged. Returns false when the diff --git a/src/prerna/reactor/agent/runtime/PreChatContext.java b/src/prerna/reactor/agent/runtime/PreChatContext.java index 402afc41f99..0bbdc0490e1 100644 --- a/src/prerna/reactor/agent/runtime/PreChatContext.java +++ b/src/prerna/reactor/agent/runtime/PreChatContext.java @@ -127,78 +127,79 @@ public static Spec anyOf(List toolboxIds, String tool, Map