Skip to content

Commit f4ac35c

Browse files
mingshlZhangxunmt
andauthored
[3.4 Feature Branch] Introduce hook and context management to OpenSearch Agents (#4432)
* add hooks in ml-commons (#4326) Signed-off-by: Xun Zhang <[email protected]> * initiate context management api with hook implementation (#4345) * initiate context management api with hook implementation Signed-off-by: Mingshi Liu <[email protected]> * apply spotless Signed-off-by: Mingshi Liu <[email protected]> --------- Signed-off-by: Mingshi Liu <[email protected]> * Add Context Manager to PER (#4379) * add pre_llm hook to per agent Signed-off-by: Mingshi Liu <[email protected]> change context management passing from query parameters to payload Signed-off-by: Mingshi Liu <[email protected]> pass hook registery into PER Signed-off-by: Mingshi Liu <[email protected]> apply spotless Signed-off-by: Mingshi Liu <[email protected]> initiate context management api with hook implementation Signed-off-by: Mingshi Liu <[email protected]> * add comment Signed-off-by: Mingshi Liu <[email protected]> * format Signed-off-by: Mingshi Liu <[email protected]> * add validation Signed-off-by: Mingshi Liu <[email protected]> --------- Signed-off-by: Mingshi Liu <[email protected]> * add inner create context management to agent register api Signed-off-by: Mingshi Liu <[email protected]> * add code coverage Signed-off-by: Mingshi Liu <[email protected]> * allow context management hook register in during agent execute Signed-off-by: Mingshi Liu <[email protected]> * add code coverage Signed-off-by: Mingshi Liu <[email protected]> * add more code coverage Signed-off-by: Mingshi Liu <[email protected]> * add validation check Signed-off-by: Mingshi Liu <[email protected]> * adapt to inplace update for context Signed-off-by: Mingshi Liu <[email protected]> * Allow context management inline create in register agent without storing in index (#4403) * allow inline create context management without storing in agent register Signed-off-by: Mingshi Liu <[email protected]> * make ML_COMMONS_MULTI_TENANCY_ENABLED default is false Signed-off-by: Mingshi Liu <[email protected]> --------- Signed-off-by: Mingshi Liu <[email protected]> * Update the POST_TOOL hook emit saving to agentic memory (#4408) * Fix POST_TOOL hook interaction updates and add tenant ID support Signed-off-by: Mingshi Liu <[email protected]> - Fix POST_TOOL hook to return full ContextManagerContext like PRE_LLM hook - Update MLChatAgentRunner to properly handle interaction updates from POST_TOOL hook - Ensure interactions list and tmpParameters.INTERACTIONS stay synchronized - Add tenant ID support to MLPredictionTaskRequest in ModelGuardrail and SummarizationManager Signed-off-by: Mingshi Liu <[email protected]> * fix error message escaping Signed-off-by: Mingshi Liu <[email protected]> * consolicate post_hook logic Signed-off-by: Mingshi Liu <[email protected]> --------- Signed-off-by: Mingshi Liu <[email protected]> * bump supported version to 3.4 Signed-off-by: Mingshi Liu <[email protected]> --------- Signed-off-by: Xun Zhang <[email protected]> Signed-off-by: Mingshi Liu <[email protected]> Co-authored-by: Xun Zhang <[email protected]>
1 parent 285b6e2 commit f4ac35c

File tree

89 files changed

+9840
-1551
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+9840
-1551
lines changed

common/src/main/java/org/opensearch/ml/common/CommonValue.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class CommonValue {
5454
public static final String TASK_POLLING_JOB_INDEX = ".ml_commons_task_polling_job";
5555
public static final String MCP_SESSION_MANAGEMENT_INDEX = ".plugins-ml-mcp-session-management";
5656
public static final String MCP_TOOLS_INDEX = ".plugins-ml-mcp-tools";
57+
public static final String ML_CONTEXT_MANAGEMENT_TEMPLATES_INDEX = ".plugins-ml-context-management-templates";
5758
// index created in 3.1 to track all ml jobs created via job scheduler
5859
public static final String ML_JOBS_INDEX = ".plugins-ml-jobs";
5960
public static final Set<String> stopWordsIndices = ImmutableSet.of(".plugins-ml-stop-words");
@@ -76,6 +77,7 @@ public class CommonValue {
7677
public static final String ML_LONG_MEMORY_HISTORY_INDEX_MAPPING_PATH = "index-mappings/ml_memory_long_term_history.json";
7778
public static final String ML_MCP_SESSION_MANAGEMENT_INDEX_MAPPING_PATH = "index-mappings/ml_mcp_session_management.json";
7879
public static final String ML_MCP_TOOLS_INDEX_MAPPING_PATH = "index-mappings/ml_mcp_tools.json";
80+
public static final String ML_CONTEXT_MANAGEMENT_TEMPLATES_INDEX_MAPPING_PATH = "index-mappings/ml_context_management_templates.json";
7981
public static final String ML_JOBS_INDEX_MAPPING_PATH = "index-mappings/ml_jobs.json";
8082
public static final String ML_INDEX_INSIGHT_CONFIG_INDEX_MAPPING_PATH = "index-mappings/ml_index_insight_config.json";
8183
public static final String ML_INDEX_INSIGHT_STORAGE_INDEX_MAPPING_PATH = "index-mappings/ml_index_insight_storage.json";

common/src/main/java/org/opensearch/ml/common/agent/MLAgent.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.opensearch.ml.common.CommonValue;
3131
import org.opensearch.ml.common.MLAgentType;
3232
import org.opensearch.ml.common.MLModel;
33+
import org.opensearch.ml.common.contextmanager.ContextManagementTemplate;
3334
import org.opensearch.telemetry.metrics.tags.Tags;
3435

3536
import lombok.Builder;
@@ -51,13 +52,16 @@ public class MLAgent implements ToXContentObject, Writeable {
5152
public static final String LAST_UPDATED_TIME_FIELD = "last_updated_time";
5253
public static final String APP_TYPE_FIELD = "app_type";
5354
public static final String IS_HIDDEN_FIELD = "is_hidden";
55+
public static final String CONTEXT_MANAGEMENT_NAME_FIELD = "context_management_name";
56+
public static final String CONTEXT_MANAGEMENT_FIELD = "context_management";
5457
private static final String LLM_INTERFACE_FIELD = "_llm_interface";
5558
private static final String TAG_VALUE_UNKNOWN = "unknown";
5659
private static final String TAG_MEMORY_TYPE = "memory_type";
5760

5861
public static final int AGENT_NAME_MAX_LENGTH = 128;
5962

6063
private static final Version MINIMAL_SUPPORTED_VERSION_FOR_HIDDEN_AGENT = CommonValue.VERSION_2_13_0;
64+
private static final Version MINIMAL_SUPPORTED_VERSION_FOR_CONTEXT_MANAGEMENT = CommonValue.VERSION_3_3_0;
6165

6266
private String name;
6367
private String type;
@@ -71,6 +75,8 @@ public class MLAgent implements ToXContentObject, Writeable {
7175
private Instant lastUpdateTime;
7276
private String appType;
7377
private Boolean isHidden;
78+
private String contextManagementName;
79+
private ContextManagementTemplate contextManagement;
7480
private final String tenantId;
7581

7682
@Builder(toBuilder = true)
@@ -86,6 +92,8 @@ public MLAgent(
8692
Instant lastUpdateTime,
8793
String appType,
8894
Boolean isHidden,
95+
String contextManagementName,
96+
ContextManagementTemplate contextManagement,
8997
String tenantId
9098
) {
9199
this.name = name;
@@ -100,6 +108,8 @@ public MLAgent(
100108
this.appType = appType;
101109
// is_hidden field isn't going to be set by user. It will be set by the code.
102110
this.isHidden = isHidden;
111+
this.contextManagementName = contextManagementName;
112+
this.contextManagement = contextManagement;
103113
this.tenantId = tenantId;
104114
validate();
105115
}
@@ -128,6 +138,17 @@ private void validate() {
128138
}
129139
}
130140
}
141+
validateContextManagement();
142+
}
143+
144+
private void validateContextManagement() {
145+
if (contextManagementName != null && contextManagement != null) {
146+
throw new IllegalArgumentException("Cannot specify both context_management_name and context_management");
147+
}
148+
149+
if (contextManagement != null && !contextManagement.isValid()) {
150+
throw new IllegalArgumentException("Invalid context management configuration");
151+
}
131152
}
132153

133154
private void validateMLAgentType(String agentType) {
@@ -171,6 +192,12 @@ public MLAgent(StreamInput input) throws IOException {
171192
if (streamInputVersion.onOrAfter(MINIMAL_SUPPORTED_VERSION_FOR_HIDDEN_AGENT)) {
172193
isHidden = input.readOptionalBoolean();
173194
}
195+
if (streamInputVersion.onOrAfter(MINIMAL_SUPPORTED_VERSION_FOR_CONTEXT_MANAGEMENT)) {
196+
contextManagementName = input.readOptionalString();
197+
if (input.readBoolean()) {
198+
contextManagement = new ContextManagementTemplate(input);
199+
}
200+
}
174201
this.tenantId = streamInputVersion.onOrAfter(VERSION_2_19_0) ? input.readOptionalString() : null;
175202
validate();
176203
}
@@ -214,6 +241,15 @@ public void writeTo(StreamOutput out) throws IOException {
214241
if (streamOutputVersion.onOrAfter(MINIMAL_SUPPORTED_VERSION_FOR_HIDDEN_AGENT)) {
215242
out.writeOptionalBoolean(isHidden);
216243
}
244+
if (streamOutputVersion.onOrAfter(MINIMAL_SUPPORTED_VERSION_FOR_CONTEXT_MANAGEMENT)) {
245+
out.writeOptionalString(contextManagementName);
246+
if (contextManagement != null) {
247+
out.writeBoolean(true);
248+
contextManagement.writeTo(out);
249+
} else {
250+
out.writeBoolean(false);
251+
}
252+
}
217253
if (streamOutputVersion.onOrAfter(VERSION_2_19_0)) {
218254
out.writeOptionalString(tenantId);
219255
}
@@ -256,6 +292,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
256292
if (isHidden != null) {
257293
builder.field(MLModel.IS_HIDDEN_FIELD, isHidden);
258294
}
295+
if (contextManagementName != null) {
296+
builder.field(CONTEXT_MANAGEMENT_NAME_FIELD, contextManagementName);
297+
}
298+
if (contextManagement != null) {
299+
builder.field(CONTEXT_MANAGEMENT_FIELD, contextManagement);
300+
}
259301
if (tenantId != null) {
260302
builder.field(TENANT_ID_FIELD, tenantId);
261303
}
@@ -283,6 +325,8 @@ private static MLAgent parseCommonFields(XContentParser parser, boolean parseHid
283325
Instant lastUpdateTime = null;
284326
String appType = null;
285327
boolean isHidden = false;
328+
String contextManagementName = null;
329+
ContextManagementTemplate contextManagement = null;
286330
String tenantId = null;
287331

288332
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
@@ -329,6 +373,12 @@ private static MLAgent parseCommonFields(XContentParser parser, boolean parseHid
329373
if (parseHidden)
330374
isHidden = parser.booleanValue();
331375
break;
376+
case CONTEXT_MANAGEMENT_NAME_FIELD:
377+
contextManagementName = parser.text();
378+
break;
379+
case CONTEXT_MANAGEMENT_FIELD:
380+
contextManagement = ContextManagementTemplate.parse(parser);
381+
break;
332382
case TENANT_ID_FIELD:
333383
tenantId = parser.textOrNull();
334384
break;
@@ -351,6 +401,8 @@ private static MLAgent parseCommonFields(XContentParser parser, boolean parseHid
351401
.lastUpdateTime(lastUpdateTime)
352402
.appType(appType)
353403
.isHidden(isHidden)
404+
.contextManagementName(contextManagementName)
405+
.contextManagement(contextManagement)
354406
.tenantId(tenantId)
355407
.build();
356408
}
@@ -384,4 +436,47 @@ public Tags getTags() {
384436

385437
return tags;
386438
}
439+
440+
/**
441+
* Check if this agent has context management configuration
442+
* @return true if agent has either context management name or inline configuration
443+
*/
444+
public boolean hasContextManagement() {
445+
return contextManagementName != null || contextManagement != null;
446+
}
447+
448+
/**
449+
* Get the effective context management configuration for this agent.
450+
* This method prioritizes inline configuration over template reference.
451+
* Note: Template resolution requires external service call and should be handled by the caller.
452+
*
453+
* @return the inline context management configuration, or null if using template reference or no configuration
454+
*/
455+
public ContextManagementTemplate getInlineContextManagement() {
456+
return contextManagement;
457+
}
458+
459+
/**
460+
* Check if this agent uses a context management template reference
461+
* @return true if agent references a context management template by name
462+
*/
463+
public boolean hasContextManagementTemplate() {
464+
return contextManagementName != null;
465+
}
466+
467+
/**
468+
* Check if this agent has inline context management configuration
469+
* @return true if agent has inline context management configuration
470+
*/
471+
public boolean hasInlineContextManagement() {
472+
return contextManagement != null;
473+
}
474+
475+
/**
476+
* Get the context management template name if this agent references one
477+
* @return the template name, or null if no template reference
478+
*/
479+
public String getContextManagementTemplateName() {
480+
return contextManagementName;
481+
}
387482
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.ml.common.contextmanager;
7+
8+
/**
9+
* Interface for activation rules that determine when a context manager should execute.
10+
* Activation rules evaluate runtime conditions based on the current context state.
11+
*/
12+
public interface ActivationRule {
13+
14+
/**
15+
* Evaluate whether the activation condition is met.
16+
* @param context the current context state
17+
* @return true if the condition is met and the manager should activate, false otherwise
18+
*/
19+
boolean evaluate(ContextManagerContext context);
20+
21+
/**
22+
* Get a description of this activation rule for logging and debugging.
23+
* @return a human-readable description of the rule
24+
*/
25+
String getDescription();
26+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.ml.common.contextmanager;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
import lombok.extern.log4j.Log4j2;
13+
14+
/**
15+
* Factory class for creating activation rules from configuration.
16+
* Supports creating rules from configuration maps and combining multiple rules.
17+
*/
18+
@Log4j2
19+
public class ActivationRuleFactory {
20+
21+
public static final String TOKENS_EXCEED_KEY = "tokens_exceed";
22+
public static final String MESSAGE_COUNT_EXCEED_KEY = "message_count_exceed";
23+
24+
/**
25+
* Create activation rules from a configuration map.
26+
* @param activationConfig the configuration map containing rule definitions
27+
* @return a list of activation rules, or empty list if no valid rules found
28+
*/
29+
public static List<ActivationRule> createRules(Map<String, Object> activationConfig) {
30+
List<ActivationRule> rules = new ArrayList<>();
31+
32+
if (activationConfig == null || activationConfig.isEmpty()) {
33+
return rules;
34+
}
35+
36+
// Create tokens_exceed rule
37+
if (activationConfig.containsKey(TOKENS_EXCEED_KEY)) {
38+
try {
39+
Object tokenValue = activationConfig.get(TOKENS_EXCEED_KEY);
40+
int tokenThreshold = parseIntegerValue(tokenValue, TOKENS_EXCEED_KEY);
41+
if (tokenThreshold > 0) {
42+
rules.add(new TokensExceedRule(tokenThreshold));
43+
log.debug("Created TokensExceedRule with threshold: {}", tokenThreshold);
44+
} else {
45+
throw new IllegalArgumentException("Invalid token threshold value: " + tokenValue + ". Must be positive integer.");
46+
}
47+
} catch (Exception e) {
48+
log.error("Failed to create TokensExceedRule: {}", e.getMessage());
49+
}
50+
}
51+
52+
// Create message_count_exceed rule
53+
if (activationConfig.containsKey(MESSAGE_COUNT_EXCEED_KEY)) {
54+
try {
55+
Object messageValue = activationConfig.get(MESSAGE_COUNT_EXCEED_KEY);
56+
int messageThreshold = parseIntegerValue(messageValue, MESSAGE_COUNT_EXCEED_KEY);
57+
if (messageThreshold > 0) {
58+
rules.add(new MessageCountExceedRule(messageThreshold));
59+
log.debug("Created MessageCountExceedRule with threshold: {}", messageThreshold);
60+
} else {
61+
throw new IllegalArgumentException(
62+
"Invalid message count threshold value: " + messageValue + ". Must be positive integer."
63+
);
64+
}
65+
} catch (Exception e) {
66+
log.error("Failed to create MessageCountExceedRule: {}", e.getMessage());
67+
}
68+
}
69+
70+
return rules;
71+
}
72+
73+
/**
74+
* Create a composite rule that requires ALL rules to be satisfied (AND logic).
75+
* @param rules the list of rules to combine
76+
* @return a composite rule, or null if the list is empty
77+
*/
78+
public static ActivationRule createCompositeRule(List<ActivationRule> rules) {
79+
if (rules == null || rules.isEmpty()) {
80+
return null;
81+
}
82+
83+
if (rules.size() == 1) {
84+
return rules.get(0);
85+
}
86+
87+
return new CompositeActivationRule(rules);
88+
}
89+
90+
/**
91+
* Parse an integer value from configuration, handling various input types.
92+
* @param value the value to parse
93+
* @param fieldName the field name for error reporting
94+
* @return the parsed integer value
95+
* @throws IllegalArgumentException if the value cannot be parsed
96+
*/
97+
private static int parseIntegerValue(Object value, String fieldName) {
98+
if (value instanceof Integer) {
99+
return (Integer) value;
100+
} else if (value instanceof Number) {
101+
return ((Number) value).intValue();
102+
} else if (value instanceof String) {
103+
try {
104+
return Integer.parseInt((String) value);
105+
} catch (NumberFormatException e) {
106+
throw new IllegalArgumentException("Invalid integer value for " + fieldName + ": " + value);
107+
}
108+
} else {
109+
throw new IllegalArgumentException("Unsupported value type for " + fieldName + ": " + value.getClass().getSimpleName());
110+
}
111+
}
112+
113+
/**
114+
* Composite activation rule that implements AND logic for multiple rules.
115+
*/
116+
private static class CompositeActivationRule implements ActivationRule {
117+
private final List<ActivationRule> rules;
118+
119+
public CompositeActivationRule(List<ActivationRule> rules) {
120+
this.rules = new ArrayList<>(rules);
121+
}
122+
123+
@Override
124+
public boolean evaluate(ContextManagerContext context) {
125+
// All rules must evaluate to true (AND logic)
126+
for (ActivationRule rule : rules) {
127+
if (!rule.evaluate(context)) {
128+
return false;
129+
}
130+
}
131+
return true;
132+
}
133+
134+
@Override
135+
public String getDescription() {
136+
StringBuilder sb = new StringBuilder();
137+
sb.append("composite_rule: [");
138+
for (int i = 0; i < rules.size(); i++) {
139+
if (i > 0) {
140+
sb.append(" AND ");
141+
}
142+
sb.append(rules.get(i).getDescription());
143+
}
144+
sb.append("]");
145+
return sb.toString();
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)