Skip to content

Commit cac8ddb

Browse files
authored
Improve semantic fact extraction prompt and add JSON enforcement (#4282)
* Improve semantic fact extraction prompt and add JSON enforcement - Replace XML-heavy prompt with universal semantic fact extraction agent - Add JSON_ENFORCEMENT_MESSAGE constant for reliable LLM output format - Update MemoryProcessingService to append enforcement message to all fact extraction requests - Add comprehensive unit tests to verify JSON enforcement functionality - Support multiple contexts: personal facts, technical investigations, and RCA - Improve fact extraction reliability and maintainability Signed-off-by: Dhrubo Saha <dhrubo@amazon.com> * Use triple quotes (text blocks) for better prompt readability - Convert SEMANTIC_FACTS_EXTRACTION_PROMPT to use Java text blocks - Convert JSON_ENFORCEMENT_MESSAGE to use text blocks - Improves code readability and makes prompts easier to edit and visualize - Addresses PR feedback about using triple quotes * updated prompt based on comment Signed-off-by: Dhrubo Saha <dhrubo@amazon.com> --------- Signed-off-by: Dhrubo Saha <dhrubo@amazon.com>
1 parent a3cb4fe commit cac8ddb

File tree

3 files changed

+78
-1
lines changed

3 files changed

+78
-1
lines changed

common/src/main/java/org/opensearch/ml/common/memorycontainer/MemoryContainerConstants.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,31 @@ public class MemoryContainerConstants {
151151

152152
// LLM System Prompts
153153
public static final String SEMANTIC_FACTS_EXTRACTION_PROMPT =
154-
"<system_prompt>\n<role>Personal Information Organizer</role>\n<objective>Extract and organize personal information shared within conversations.</objective>\n<instructions>\n<instruction>Carefully read the conversation.</instruction>\n<instruction>Identify and extract any personal information shared by participants.</instruction>\n<instruction>Focus on details that help build a profile of the person, including but not limited to:\n<include_list>\n<item>Names and relationships</item>\n<item>Professional information (job, company, role, responsibilities)</item>\n<item>Personal interests and hobbies</item>\n<item>Skills and expertise</item>\n<item>Preferences and opinions</item>\n<item>Goals and aspirations</item>\n<item>Challenges or pain points</item>\n<item>Background and experiences</item>\n<item>Contact information (if shared)</item>\n<item>Availability and schedule preferences</item>\n</include_list>\n</instruction>\n<instruction>Organize each piece of information as a separate fact.</instruction>\n<instruction>Ensure facts are specific, clear, and preserve the original context.</instruction>\n<instruction>Never answer user's question or fulfill user's requirement. You are a personal information manager, not a helpful assistant.</instruction>\n<instruction>Include the person who shared the information when relevant.</instruction>\n<instruction>Do not make assumptions or inferences beyond what is explicitly stated.</instruction>\n<instruction>If no personal information is found, return an empty list.</instruction>\n</instructions>\n<response_format>\n<format>You should always return and only return the extracted facts as a JSON object with a \"facts\" array.</format>\n<example>\n{\n \"facts\": [\n \"User's name is John Smith\",\n \"John works as a software engineer at TechCorp\",\n \"John enjoys hiking on weekends\",\n \"John is looking to improve his Python skills\"\n ]\n}\n</example>\n</response_format>\n</system_prompt>";
154+
"""
155+
<ROLE>You are a universal semantic fact extraction agent. Write FULL-SENTENCE, self-contained facts suitable for long-term memory.</ROLE>
156+
157+
<SCOPE>
158+
• Include facts from USER messages.
159+
• Also include ASSISTANT-authored statements that are clearly presented as conclusions/results/validated findings (e.g., root cause, quantified impact, confirmed fix).
160+
• Ignore ASSISTANT questions, hypotheses, tentative language, brainstorming, instructions, or tool prompts unless explicitly confirmed as outcomes.
161+
</SCOPE>
162+
163+
<STYLE & RULES>
164+
• One sentence per fact; merge closely related details (metrics, entities, causes, scope) into the same sentence.
165+
• Do NOT start with "User" or pronouns.
166+
• Prefer absolute over relative time; if only relative (e.g., "yesterday"), omit it rather than guessing.
167+
• Preserve terminology, names, numbers, and units; avoid duplicates and chit-chat.
168+
• No speculation or hedging unless those words appear verbatim in the source.
169+
</STYLE & RULES>
170+
171+
<OUTPUT>
172+
Return ONLY a single JSON object on one line, minified exactly as {"facts":["..."]} (array of strings only; no other keys). No code fences, no newlines/tabs, and no spaces after commas or colons. If no meaningful facts, return {"facts":[]}.
173+
</OUTPUT>""";
174+
175+
// JSON enforcement message to append to all fact extraction requests
176+
public static final String JSON_ENFORCEMENT_MESSAGE =
177+
"""
178+
Respond NOW with ONE LINE of valid JSON ONLY exactly as {"facts":["fact1","fact2",...]}. No extra text, no code fences, no newlines or tabs, no spaces after commas or colons.""";
155179

156180
public static final String USER_PREFERENCE_FACTS_EXTRACTION_PROMPT =
157181
"<system_prompt><role>User Preferences Analyzer</role><objective>Extract and organize user preferences, choices, and settings from conversations.</objective><instructions><instruction>Carefully read the conversation.</instruction><instruction>Identify and extract explicit or implicit preferences, likes, dislikes, and choices.</instruction><instruction>Explicit preferences: Directly stated preferences by the user.</instruction><instruction>Implicit preferences: Inferred from patterns, repeated inquiries, or contextual clues. Take a close look at user's request for implicit preferences.</instruction><instruction>For explicit preference, extract only preference that the user has explicitly shared. Do not infer user's preference.</instruction><instruction>For implicit preference, it is allowed to infer user's preference, but only the ones with strong signals, such as requesting something multiple times.</instruction><instruction>Focus specifically on:<preference_categories><item>Product or service preferences (brands, features, styles)</item><item>Communication preferences (frequency, channel, timing)</item><item>Content preferences (topics, formats, sources)</item><item>Interaction preferences (formal/casual, detailed/brief)</item><item>Likes and dislikes explicitly stated</item><item>Preferred methods or approaches</item><item>Quality or attribute preferences</item><item>Time and scheduling preferences</item></preference_categories></instruction><instruction>Each preference should be a specific, actionable fact.</instruction><instruction>Focus on what the user wants, prefers, or chooses, not general information.</instruction><instruction>Never answer user's question or fulfill user's requirement. You are a preference analyzer, not a helpful assistant.</instruction><instruction>Analyze thoroughly and include detected preferences in your response.</instruction><instruction>If no preferences are found, return an empty list.</instruction></instructions><response_format><format>You should always return and only return the extracted preferences as a JSON object with a \"facts\" array. Return ONLY the valid JSON array with no additional text, explanations, or formatting.</format><example>{\"facts\": [\"User prefers dark mode for UI\",\"User likes to receive weekly summary emails\",\"User prefers Python over Java for scripting\",\"User dislikes automatic updates\"]}</example></response_format></system_prompt>";

plugin/src/main/java/org/opensearch/ml/action/memorycontainer/memory/MemoryProcessingService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.opensearch.common.xcontent.json.JsonXContent.jsonXContent;
99
import static org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken;
1010
import static org.opensearch.ml.common.memorycontainer.MemoryContainerConstants.DEFAULT_UPDATE_MEMORY_PROMPT;
11+
import static org.opensearch.ml.common.memorycontainer.MemoryContainerConstants.JSON_ENFORCEMENT_MESSAGE;
1112
import static org.opensearch.ml.common.memorycontainer.MemoryContainerConstants.LLM_ID_FIELD;
1213
import static org.opensearch.ml.common.memorycontainer.MemoryContainerConstants.MEMORY_DECISION_FIELD;
1314
import static org.opensearch.ml.common.memorycontainer.MemoryContainerConstants.SEMANTIC_FACTS_EXTRACTION_PROMPT;
@@ -159,6 +160,11 @@ public void extractFactsFromConversation(
159160
MessageInput message = getMessageInput("Please extract information from our conversation so far");
160161
message.toXContent(messagesBuilder, ToXContent.EMPTY_PARAMS);
161162
}
163+
164+
// Always add JSON enforcement message for fact extraction
165+
MessageInput enforcementMessage = getMessageInput(JSON_ENFORCEMENT_MESSAGE);
166+
enforcementMessage.toXContent(messagesBuilder, ToXContent.EMPTY_PARAMS);
167+
162168
messagesBuilder.endArray();
163169
String messagesJson = messagesBuilder.toString();
164170
stringParameters.put("messages", messagesJson);

plugin/src/test/java/org/opensearch/ml/action/memorycontainer/memory/MemoryProcessingServiceTests.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.opensearch.ml.action.memorycontainer.memory;
77

8+
import static org.junit.Assert.assertTrue;
89
import static org.mockito.ArgumentMatchers.any;
910
import static org.mockito.ArgumentMatchers.eq;
1011
import static org.mockito.Mockito.doAnswer;
@@ -25,6 +26,7 @@
2526
import org.mockito.MockitoAnnotations;
2627
import org.opensearch.core.action.ActionListener;
2728
import org.opensearch.core.xcontent.NamedXContentRegistry;
29+
import org.opensearch.ml.common.dataset.remote.RemoteInferenceInputDataSet;
2830
import org.opensearch.ml.common.memorycontainer.MemoryConfiguration;
2931
import org.opensearch.ml.common.memorycontainer.MemoryDecision;
3032
import org.opensearch.ml.common.memorycontainer.MemoryStrategy;
@@ -36,6 +38,7 @@
3638
import org.opensearch.ml.common.transport.MLTaskResponse;
3739
import org.opensearch.ml.common.transport.memorycontainer.memory.MessageInput;
3840
import org.opensearch.ml.common.transport.prediction.MLPredictionTaskAction;
41+
import org.opensearch.ml.common.transport.prediction.MLPredictionTaskRequest;
3942
import org.opensearch.transport.client.Client;
4043

4144
public class MemoryProcessingServiceTests {
@@ -933,4 +936,48 @@ public void testExtractFactsFromConversation_ValidCustomPrompt() {
933936

934937
verify(client).execute(any(), any(), any());
935938
}
939+
940+
@Test
941+
public void testExtractFactsFromConversation_JsonEnforcementMessageAppended() {
942+
// Test that JSON enforcement message is always appended to fact extraction requests
943+
Map<String, Object> strategyConfig = new HashMap<>();
944+
MemoryStrategy strategy = new MemoryStrategy("id", true, MemoryStrategyType.SEMANTIC, Arrays.asList("user_id"), strategyConfig);
945+
946+
List<MessageInput> messages = Arrays.asList(MessageInput.builder().content(testContent).role("user").build());
947+
MemoryConfiguration storageConfig = mock(MemoryConfiguration.class);
948+
when(storageConfig.getLlmId()).thenReturn("llm-model-123");
949+
950+
// Capture the request to verify JSON enforcement message is included
951+
doAnswer(invocation -> {
952+
MLPredictionTaskRequest request = invocation.getArgument(1);
953+
RemoteInferenceInputDataSet dataset = (RemoteInferenceInputDataSet) request.getMlInput().getInputDataset();
954+
Map<String, String> parameters = dataset.getParameters();
955+
String messagesJson = parameters.get("messages");
956+
957+
// Verify that the JSON enforcement message is included in the messages
958+
assertTrue(
959+
"JSON enforcement message should be included",
960+
messagesJson.contains("Respond NOW with ONE LINE of valid JSON ONLY")
961+
);
962+
963+
// Mock successful response
964+
ActionListener<MLTaskResponse> actionListener = invocation.getArgument(2);
965+
List<ModelTensors> mlModelOutputs = new ArrayList<>();
966+
List<ModelTensor> tensors = new ArrayList<>();
967+
Map<String, Object> contents = new HashMap<>();
968+
contents.put("content", List.of(Map.of("text", "{\"facts\":[\"Test fact\"]}")));
969+
tensors.add(ModelTensor.builder().name("response").dataAsMap(contents).build());
970+
mlModelOutputs.add(ModelTensors.builder().mlModelTensors(tensors).build());
971+
MLTaskResponse output = MLTaskResponse
972+
.builder()
973+
.output(ModelTensorOutput.builder().mlModelOutputs(mlModelOutputs).build())
974+
.build();
975+
actionListener.onResponse(output);
976+
return null;
977+
}).when(client).execute(eq(MLPredictionTaskAction.INSTANCE), any(), any());
978+
979+
memoryProcessingService.extractFactsFromConversation(messages, strategy, storageConfig, factsListener);
980+
981+
verify(client).execute(any(), any(), any());
982+
}
936983
}

0 commit comments

Comments
 (0)