Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2025 Conductor Authors.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.
*/
package org.conductoross.conductor.ai.examples;

import java.util.function.Consumer;

import org.conductoross.conductor.ai.Agent;
import org.conductoross.conductor.ai.AgentRuntime;
import org.conductoross.conductor.ai.model.AgentResult;
import org.conductoross.conductor.ai.model.FeedbackEvent;
import org.conductoross.conductor.ai.model.OCGMemoryStore;
import org.conductoross.conductor.ai.model.SemanticMemory;

/**
* Example 118 — OCG-backed long-term memory with human good/bad feedback links.
*
* <p>Enable memory on an agent and the server-side compiler does two things
* automatically once the agent is deployed:
*
* <ul>
* <li>BEFORE a run: relevant past memories (scoped to this agent/user) are
* retrieved from OCG and injected into the prompt — no tool call needed.</li>
* <li>AFTER a run: the conversation is summarized (Claude-style: durable facts,
* not the raw transcript) by a small internal summarizer agent and saved back
* to OCG as a memory.</li>
* </ul>
*
* <p>Feedback is HUMAN-only. Agents never vote. Instead, the runtime hands a
* {@link FeedbackEvent} — including signed <i>capability URLs</i> (good/bad) — to the
* agent's {@code feedbackSink}. A human (e.g. a support engineer) clicks a link to
* mark the memory good or bad; the link skips auth (its signature is the
* authorization), so the clicker needs no OCG account. Here the sink just prints the
* URLs as they'd appear in a Zendesk ticket comment.
*
* <p>The SDK emits a {@code longTermMemory} block on the agent config so the server
* activates the feature; the OCG {@code credential} is a server-resolvable secret NAME
* ({@code OCG_PUBLIC_KEY}), never the raw client token.
*
* <p>Requires the OCG instance to be started with a feedback-link secret
* ({@code OCG_FEEDBACK_LINK_SECRET}) for the capability URLs to be minted.
*
* <pre>
* OCG_INSTANCE_URL=https://test.contextgraph.io \
* OCG_TOKEN=&lt;bearer-token&gt; \
* ./gradlew :conductor-ai-examples:run -PmainClass=org.conductoross.conductor.ai.examples.Example118OcgMemory
* </pre>
*/
public class Example118OcgMemory {

public static void main(String[] args) {
String ocgUrl = System.getenv().getOrDefault("OCG_INSTANCE_URL", "");
// Unlike the server-side OCG retrieval tools (which resolve a credential
// server-side), the memory store calls OCG directly, so it holds the bearer token.
String ocgToken = System.getenv("OCG_TOKEN");
if (ocgUrl.isEmpty()) {
System.err.println(
"Set OCG_INSTANCE_URL to your OCG instance, e.g. https://test.contextgraph.io");
return;
}

OCGMemoryStore store = OCGMemoryStore.builder()
.url(ocgUrl)
.agent("agent:support")
.user("user:alice")
.token(ocgToken)
.build();

// Deliver the good/bad links to a human. In production this would POST a
// comment to the Zendesk ticket; here we just print what would be sent.
Consumer<FeedbackEvent> zendeskSink = event -> {
System.out.println("\n--- would post to Zendesk ticket ---");
System.out.println("Saved memory: " + event.getMemoryKey());
System.out.println("Summary: " + event.getSummary());
if (event.getGoodUrl() != null) {
System.out.println(" Was this helpful? " + event.getGoodUrl());
System.out.println(" Not helpful: " + event.getBadUrl());
}
System.out.println("------------------------------------\n");
};

Agent agent = Agent.builder()
.name("support")
.model(Settings.LLM_MODEL)
.instructions(
"You are a customer support agent. Use any relevant context from "
+ "memory to personalize your answer. A memory labeled [bad] was "
+ "flagged by a human — treat it with suspicion.")
.semanticMemory(new SemanticMemory(store, 5, null))
.feedbackSink(zendeskSink)
.build();

try (AgentRuntime runtime = new AgentRuntime()) {
System.out.println("--- Turn 1 ---");
AgentResult turn1 =
runtime.run(agent, "Hi, I'm Alice. I'm on the Enterprise plan and prefer email.");
turn1.printResult();

System.out.println("\n--- Turn 2 (should recall Alice's plan from memory) ---");
AgentResult turn2 = runtime.run(agent, "What plan am I on again?");
turn2.printResult();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
Expand All @@ -25,9 +26,11 @@
import org.conductoross.conductor.ai.handoff.Handoff;
import org.conductoross.conductor.ai.internal.AgentRegistry;
import org.conductoross.conductor.ai.model.ConversationMemory;
import org.conductoross.conductor.ai.model.FeedbackEvent;
import org.conductoross.conductor.ai.model.GuardrailDef;
import org.conductoross.conductor.ai.model.PrefillToolCall;
import org.conductoross.conductor.ai.model.PromptTemplate;
import org.conductoross.conductor.ai.model.SemanticMemory;
import org.conductoross.conductor.ai.model.ToolDef;
import org.conductoross.conductor.ai.termination.TerminationCondition;

Expand Down Expand Up @@ -128,6 +131,21 @@ public class Agent {
private final List<String> maskedFields;
/** Token threshold for proactive context condensation. */
private final Integer contextWindowBudget;
/**
* OCG-backed long-term memory (see {@link org.conductoross.conductor.ai.model.OCGMemoryStore}).
* When the backing store is OCG-backed, the config serializer emits a
* {@code longTermMemory} block so the server-side compiler inlines retrieval
* (pre-loop) + distill/save/feedback (post-loop) steps on the deployed path.
*/
private final SemanticMemory semanticMemory;
/** Optional model override for the internal conversation summarizer (falls back to {@link #model}). */
private final String memorySummaryModel;
/**
* Optional sink that receives the good/bad capability links (a {@link FeedbackEvent})
* for a saved conversation memory, for out-of-band human delivery. When set, the
* serializer emits a {@code feedbackSink} worker ref.
*/
private final Consumer<FeedbackEvent> feedbackSink;

private Agent(Builder builder) {
this.name = builder.name;
Expand Down Expand Up @@ -195,6 +213,9 @@ private Agent(Builder builder) {
this.reasoningEffort = builder.reasoningEffort;
this.maskedFields = builder.maskedFields != null ? new ArrayList<>(builder.maskedFields) : null;
this.contextWindowBudget = builder.contextWindowBudget;
this.semanticMemory = builder.semanticMemory;
this.memorySummaryModel = builder.memorySummaryModel;
this.feedbackSink = builder.feedbackSink;
}

/**
Expand Down Expand Up @@ -440,6 +461,21 @@ public Integer getContextWindowBudget() {
return contextWindowBudget;
}

/** OCG-backed long-term memory handle, or {@code null} if unset. */
public SemanticMemory getSemanticMemory() {
return semanticMemory;
}

/** Model override for the conversation summarizer, or {@code null} to reuse {@link #getModel()}. */
public String getMemorySummaryModel() {
return memorySummaryModel;
}

/** Out-of-band feedback link sink, or {@code null} if unset. */
public Consumer<FeedbackEvent> getFeedbackSink() {
return feedbackSink;
}

public static Builder builder() {
return new Builder();
}
Expand Down Expand Up @@ -542,6 +578,9 @@ public static class Builder {
private String reasoningEffort;
private List<String> maskedFields;
private Integer contextWindowBudget;
private SemanticMemory semanticMemory;
private String memorySummaryModel;
private Consumer<FeedbackEvent> feedbackSink;

/** Set the agent name (required). Must match {@code ^[a-zA-Z_][a-zA-Z0-9_-]*$}. */
public Builder name(String name) {
Expand Down Expand Up @@ -1052,6 +1091,36 @@ public Builder contextWindowBudget(int contextWindowBudget) {
return this;
}

/**
* Attach OCG-backed long-term memory. When the backing store is an
* {@link org.conductoross.conductor.ai.model.OCGMemoryStore}, the config serializer
* emits a {@code longTermMemory} block so the server-side compiler activates
* pre-run retrieval and post-run distill/save on the deployed path.
*/
public Builder semanticMemory(SemanticMemory semanticMemory) {
this.semanticMemory = semanticMemory;
return this;
}

/**
* Override the model used by the internal conversation summarizer. Defaults to
* the agent's own {@link #model} when unset.
*/
public Builder memorySummaryModel(String memorySummaryModel) {
this.memorySummaryModel = memorySummaryModel;
return this;
}

/**
* Set the sink that receives the good/bad capability links (a
* {@link FeedbackEvent}) for a saved conversation memory, for out-of-band human
* delivery. When set, the serializer emits a {@code feedbackSink} worker ref.
*/
public Builder feedbackSink(Consumer<FeedbackEvent> feedbackSink) {
this.feedbackSink = feedbackSink;
return this;
}

/**
* Build the Agent.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
import org.conductoross.conductor.ai.handoff.OnTextMention;
import org.conductoross.conductor.ai.handoff.OnToolResult;
import org.conductoross.conductor.ai.model.GuardrailDef;
import org.conductoross.conductor.ai.model.OCGMemoryStore;
import org.conductoross.conductor.ai.model.PromptTemplate;
import org.conductoross.conductor.ai.model.SemanticMemory;
import org.conductoross.conductor.ai.model.ToolDef;
import org.conductoross.conductor.ai.plans.Context;
import org.slf4j.Logger;
Expand Down Expand Up @@ -262,6 +264,21 @@ private Map<String, Object> serializeAgent(Agent agent) {
agentMap.put("memory", memMap);
}

// Long-term (OCG-backed) memory. When present, the server-side compiler
// inlines retrieval (pre-loop) + distill/save/feedback (post-loop) steps
// so memory works on the deployed/webhook path — not just client run().
Map<String, Object> ltm = serializeLongTermMemory(agent);
if (ltm != null) {
agentMap.put("longTermMemory", ltm);
// feedbackSink delivers the human good/bad capability links out-of-band.
// Emit a worker ref so the compiled path can call the SDK's sink worker.
if (agent.getFeedbackSink() != null) {
Map<String, Object> feedbackSink = new LinkedHashMap<>();
feedbackSink.put("taskName", agent.getName() + "_feedback_sink");
agentMap.put("feedbackSink", feedbackSink);
}
}

// Termination condition
if (agent.getTermination() != null) {
agentMap.put("termination", agent.getTermination().toMap());
Expand Down Expand Up @@ -596,6 +613,51 @@ private Map<String, Object> serializeAgent(Agent agent) {
return agentMap;
}

/**
* Serialize an agent's OCG-backed semantic memory to a {@code LongTermMemoryConfig} map.
*
* <p>Returns {@code null} (no-op) unless the agent has a {@link SemanticMemory} whose
* store is an {@link OCGMemoryStore} (only OCG-backed stores compile server-side —
* they need a base url to call). Reads the OCG instance url, scope owner, user and
* scope off the store; the credential is a SERVER-resolvable secret NAME (e.g.
* {@code OCG_PUBLIC_KEY}) — never the raw client token. The summary model falls back
* to the agent's own model when not explicitly set. Null-valued keys are omitted.
*/
private Map<String, Object> serializeLongTermMemory(Agent agent) {
SemanticMemory sm = agent.getSemanticMemory();
if (sm == null) {
return null;
}
// Only OCG-backed stores compile server-side (need a base url to call).
if (!(sm.getStore() instanceof OCGMemoryStore)) {
return null;
}
OCGMemoryStore store = (OCGMemoryStore) sm.getStore();

String scope = store.getScope() != null && !store.getScope().isEmpty() ? store.getScope() : "agent";
String credential =
store.getCredential() != null && !store.getCredential().isEmpty() ? store.getCredential() : "OCG_PUBLIC_KEY";
String summaryModel = agent.getMemorySummaryModel() != null && !agent.getMemorySummaryModel().isEmpty()
? agent.getMemorySummaryModel()
: (agent.getModel() != null && !agent.getModel().isEmpty() ? agent.getModel() : null);

Map<String, Object> result = new LinkedHashMap<>();
putIfNotNull(result, "ocgUrl", store.getBaseUrl());
putIfNotNull(result, "credential", credential);
putIfNotNull(result, "agent", store.getAgent());
putIfNotNull(result, "user", store.getUser());
putIfNotNull(result, "scope", scope);
result.put("maxResults", sm.getMaxResults());
putIfNotNull(result, "summaryModel", summaryModel);
return result;
}

private static void putIfNotNull(Map<String, Object> map, String key, Object value) {
if (value != null) {
map.put(key, value);
}
}

private Map<String, Object> serializeTool(ToolDef tool, boolean agentStateful) {
Map<String, Object> toolMap = new LinkedHashMap<>();
toolMap.put("name", tool.getName());
Expand Down
Loading
Loading