diff --git a/src/prerna/engine/impl/guardrail/AggressiveSelfHarmGuardrailEngine.java b/src/prerna/engine/impl/guardrail/AggressiveSelfHarmGuardrailEngine.java index 5cc400aa55..0ae6a88705 100644 --- a/src/prerna/engine/impl/guardrail/AggressiveSelfHarmGuardrailEngine.java +++ b/src/prerna/engine/impl/guardrail/AggressiveSelfHarmGuardrailEngine.java @@ -46,6 +46,7 @@ import prerna.engine.impl.model.message.ResponseMessage; import prerna.om.Insight; import prerna.om.InsightStore; +import prerna.om.ThreadStore; import prerna.sablecc2.om.GenRowStruct; import prerna.sablecc2.om.NounStore; import prerna.sablecc2.om.nounmeta.GuardrailNounMetadata; @@ -63,6 +64,7 @@ * Optional SMSS keys: * */ public class AggressiveSelfHarmGuardrailEngine extends AbstractGuardrailReactorFunctionEngine { @@ -70,8 +72,10 @@ public class AggressiveSelfHarmGuardrailEngine extends AbstractGuardrailReactorF private static final Logger classLogger = LogManager.getLogger(AggressiveSelfHarmGuardrailEngine.class); public static final String MODEL_ENGINE_ID_KEY = "MODEL_ENGINE_ID"; + public static final String SYSTEM_PROMPT_KEY = "SYSTEM_PROMPT"; + public static final String BLOCKED_MESSAGE_KEY = "BLOCKED_MESSAGE"; - private static final String SYSTEM_PROMPT = + private static final String DEFAULT_SYSTEM_PROMPT = "You are a content safety classifier. Your sole task is to detect whether a user message " + "expresses first-person intent to harm themselves or others, suicidal ideation, or aggressive threats.\n\n" + "Respond with ONLY one word:\n" @@ -82,7 +86,13 @@ public class AggressiveSelfHarmGuardrailEngine extends AbstractGuardrailReactorF + "third-party reports, or general medical questions\n\n" + "Do not explain your reasoning. Output only SAFE or UNSAFE."; + private static final String DEFAULT_BLOCKED_MESSAGE = + "I'm sorry, I'm not able to help with that. If you or someone you know is in crisis, " + + "please call or text 988 to reach the Suicide & Crisis Lifeline, or call 911 for immediate emergency assistance."; + private String modelEngineId; + private String systemPrompt; + private String blockedMessage; public AggressiveSelfHarmGuardrailEngine() { this.keysToGet = new String[] { "prompt" }; @@ -98,6 +108,14 @@ public void open(Properties smssProp) throws Exception { } this.modelEngineId = this.modelEngineId.trim(); + String systemPromptProp = this.smssProp.getProperty(SYSTEM_PROMPT_KEY); + this.systemPrompt = (systemPromptProp != null && !systemPromptProp.trim().isEmpty()) + ? systemPromptProp.trim() : DEFAULT_SYSTEM_PROMPT; + + String blockedMessageProp = this.smssProp.getProperty(BLOCKED_MESSAGE_KEY); + this.blockedMessage = (blockedMessageProp != null && !blockedMessageProp.trim().isEmpty()) + ? blockedMessageProp.trim() : DEFAULT_BLOCKED_MESSAGE; + this.functionDescription = "Detects aggressive or self-harm content by asking a configured LLM to classify " + "the prompt as SAFE or UNSAFE."; this.parameters = new ArrayList<>(); @@ -109,6 +127,9 @@ public void open(Properties smssProp) throws Exception { public GuardrailNounMetadata execute(NounStore ns, GenRowStruct curRow) { Map keyValue = organizeKeys(ns, curRow); String prompt = keyValue.get("prompt"); + if (prompt == null) { + throw new IllegalArgumentException("No prompt has been defined"); + } classLogger.info("AggressiveSelfHarmGuardrail: classifying prompt (length={}) via model={}", prompt.length(), this.modelEngineId); @@ -121,13 +142,16 @@ public GuardrailNounMetadata execute(NounStore ns, GenRowStruct curRow) { Insight classificationInsight = new Insight(); InsightStore.getInstance().put(classificationInsight); + String savedJobId = ThreadStore.getJobId(); + ThreadStore.setJobId(null); + String classification; try { Room room = RoomUtils.createRoomIfNotExists(UUID.randomUUID().toString(), classificationInsight, modelEngine, prompt); Map params = new HashMap<>(); params.put("use_history", false); InputMessage msg = InputMessage.builder(room) - .withSystemPrompt(SYSTEM_PROMPT) + .withSystemPrompt(this.systemPrompt) .withText(prompt) .withModelType(modelEngine.getModelType()) .withParamMap(params) @@ -136,6 +160,7 @@ public GuardrailNounMetadata execute(NounStore ns, GenRowStruct curRow) { Object responseObj = response.getModelEngineResponse().toMap().get("response"); classification = responseObj != null ? responseObj.toString().trim() : ""; } finally { + ThreadStore.setJobId(savedJobId); InsightStore.getInstance().remove(classificationInsight.getInsightId()); } @@ -147,11 +172,7 @@ public GuardrailNounMetadata execute(NounStore ns, GenRowStruct curRow) { details.put("classification", classification); details.put("modelEngineId", this.modelEngineId); - String returnPrompt = pass ? prompt - : "Respond only with the following message, word for word: " - + "'I'm sorry, I'm not able to help with that. " - + "If you or someone you know is in crisis, please call or text 988 to reach the Veterans Crisis Line, " - + "or call 911 for immediate emergency assistance.'"; + String returnPrompt = pass ? prompt : this.blockedMessage; return new GuardrailNounMetadata(pass, returnPrompt, details); } diff --git a/src/prerna/engine/impl/guardrail/PolicyComplianceGuardrailEngine.java b/src/prerna/engine/impl/guardrail/PolicyComplianceGuardrailEngine.java index a73f9a7134..1d141d70b6 100644 --- a/src/prerna/engine/impl/guardrail/PolicyComplianceGuardrailEngine.java +++ b/src/prerna/engine/impl/guardrail/PolicyComplianceGuardrailEngine.java @@ -47,6 +47,7 @@ import prerna.engine.impl.model.responses.AbstractModelEngineResponse; import prerna.om.Insight; import prerna.om.InsightStore; +import prerna.om.ThreadStore; import prerna.sablecc2.om.GenRowStruct; import prerna.sablecc2.om.NounStore; import prerna.sablecc2.om.nounmeta.GuardrailNounMetadata; @@ -181,6 +182,8 @@ private String classify(String textToJudge, String policyDescription) throws Exc // Room/model calls require an Insight to be registered, this one is never tied to a real user session. Insight classificationInsight = new Insight(); InsightStore.getInstance().put(classificationInsight); + String savedJobId = ThreadStore.getJobId(); + ThreadStore.setJobId(null); try { Room room = RoomUtils.createRoomIfNotExists(UUID.randomUUID().toString(), classificationInsight, judgeEngine, textToJudge); @@ -196,6 +199,7 @@ private String classify(String textToJudge, String policyDescription) throws Exc Object responseObj = response.getModelEngineResponse().toMap().get("response"); return responseObj != null ? responseObj.toString().trim() : ""; } finally { + ThreadStore.setJobId(savedJobId); InsightStore.getInstance().remove(classificationInsight.getInsightId()); } } diff --git a/src/prerna/engine/impl/pipeline/PipelineInvocationHandler.java b/src/prerna/engine/impl/pipeline/PipelineInvocationHandler.java index 5007d862bd..101981099e 100644 --- a/src/prerna/engine/impl/pipeline/PipelineInvocationHandler.java +++ b/src/prerna/engine/impl/pipeline/PipelineInvocationHandler.java @@ -68,6 +68,8 @@ import prerna.engine.api.IEngine; import prerna.engine.impl.model.Room; import prerna.engine.impl.model.responses.AbstractModelEngineResponse; +import prerna.engine.impl.model.responses.AskModelEngineResponse; +import prerna.engine.impl.model.responses.AskStringModelEngineResponse; import prerna.logging.IgnoreEngineLogging; import prerna.logging.LoggingEngineSerializer; import prerna.logging.LoggingIReactorSerializer; @@ -129,6 +131,7 @@ public class PipelineInvocationHandler implements InvocationHandler { // took private static final String GUARDRAIL_ACTION_MASK = "MASK"; private static final String GUARDRAIL_ACTION_BLOCK = "BLOCK"; + private static final String GUARDRAIL_ACTION_RESPOND = "RESPOND"; private final ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); private final Map pipelinesMap = new HashMap<>(); @@ -315,9 +318,12 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl .get(PipelineReactorUtils.INTERIM_RESULT); boolean pass = (boolean) resultMap.get(PipelineReactorUtils.PASS); boolean masked = Boolean.TRUE.equals(resultMap.get(PipelineReactorUtils.MASKED)); + String cannedResponse = (String) resultMap.get(PipelineReactorUtils.SHORT_CIRCUIT_RESPONSE); // MASK when the guardrail neutralized content, BLOCK when it stopped the - // request, null when it ran clean - queryable via the GUARDRAIL_ACTION column - String guardrailAction = masked ? GUARDRAIL_ACTION_MASK : (!pass ? GUARDRAIL_ACTION_BLOCK : null); + // request, RESPOND when it supplied the answer itself, null when it ran clean + // - queryable via the GUARDRAIL_ACTION column + String guardrailAction = cannedResponse != null ? GUARDRAIL_ACTION_RESPOND + : masked ? GUARDRAIL_ACTION_MASK : (!pass ? GUARDRAIL_ACTION_BLOCK : null); String request = null; String response = null; @@ -334,6 +340,16 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl logEngineCall(engineSpecificLogger, start, end, pass, request, response, reactor.getClass().getSimpleName(), null, null, null, guardrailAction); + if (cannedResponse != null) { + if (!AskModelEngineResponse.class.isAssignableFrom(method.getReturnType())) { + throw new SemossPixelException( + "Unable to process this request due to content policy (guardrail input exception)"); + } + classLogger.warn("Guardrail {} short-circuited the model call with a canned response", + reactor.getClass().getSimpleName()); + return new AskStringModelEngineResponse(cannedResponse, 0, 0); + } + if (!pass) { throw new SemossPixelException( "Unable to process this request due to content policy (guardrail input exception)"); diff --git a/src/prerna/reactor/interceptor/GenericGuardrailInputReactor.java b/src/prerna/reactor/interceptor/GenericGuardrailInputReactor.java index bfc24de8b9..09a00b863c 100644 --- a/src/prerna/reactor/interceptor/GenericGuardrailInputReactor.java +++ b/src/prerna/reactor/interceptor/GenericGuardrailInputReactor.java @@ -191,7 +191,17 @@ public NounMetadata execute() { } } - Map resultMap = createInterimResult(output, this.getClass().getName(), masked); + // When configured to respond (rather than mask or block), hand the guardrail's + // message back as the model's answer. The real model call is skipped entirely, + // so no version of the prompt reaches the provider. + String cannedResponse = null; + Boolean respondWithGuardrailMessage = helper.getConfigParameter("respondWithGuardrailMessage", Boolean.class); + if (Boolean.TRUE.equals(respondWithGuardrailMessage) && !output.isPass()) { + cannedResponse = output.getReturnPrompt(); + } + + Map resultMap = createInterimResult(output, this.getClass().getName(), masked, + cannedResponse); // Update the processedArguments with the interim result processedArguments.put(PipelineReactorUtils.INTERIM_RESULT, resultMap); @@ -206,13 +216,16 @@ public NounMetadata execute() { * @return */ private Map createInterimResult(GuardrailNounMetadata results, String interceptorName, - boolean masked) { + boolean masked, String cannedResponse) { Map resultMap = new HashMap<>(); resultMap.put(PipelineReactorUtils.INTERCEPTOR, interceptorName); // when we masked the input we neutralized the failure, so let it pass downstream resultMap.put(PipelineReactorUtils.PASS, masked || results.isPass()); resultMap.put(PipelineReactorUtils.PASS_DETAILS, results.getValue()); resultMap.put(PipelineReactorUtils.MASKED, masked); + if (cannedResponse != null) { + resultMap.put(PipelineReactorUtils.SHORT_CIRCUIT_RESPONSE, cannedResponse); + } return resultMap; } diff --git a/src/prerna/reactor/interceptor/PipelineReactorUtils.java b/src/prerna/reactor/interceptor/PipelineReactorUtils.java index a947bae9b9..d34e6fed40 100644 --- a/src/prerna/reactor/interceptor/PipelineReactorUtils.java +++ b/src/prerna/reactor/interceptor/PipelineReactorUtils.java @@ -48,6 +48,7 @@ public final class PipelineReactorUtils { public static final String PASS = "pass"; public static final String PASS_DETAILS = "passDetails"; public static final String MASKED = "masked"; + public static final String SHORT_CIRCUIT_RESPONSE = "shortCircuitResponse"; private PipelineReactorUtils() { // private constructor to prevent instantiation