Skip to content

Commit b4ecc6d

Browse files
committed
Rewrite Memory2 feature to work with interceptors
1 parent f82ecf8 commit b4ecc6d

File tree

20 files changed

+1657
-2198
lines changed

20 files changed

+1657
-2198
lines changed

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/feature/ContextualPromptExecutor.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,26 @@ public class ContextualPromptExecutor(
3838
@OptIn(ExperimentalUuidApi::class)
3939
val eventId = Uuid.random().toString()
4040

41-
logger.debug { "Executing LLM call (event id: $eventId, prompt: $prompt, tools: [${tools.joinToString { it.name }}])" }
41+
val promptBeforeInterceptors = context.llm.prompt // because onLLMCallStarting might change context.llm.prompt
42+
43+
logger.debug { "Starting LLM call (event id: $eventId, prompt: $prompt, tools: [${tools.joinToString { it.name }}])" }
4244
context.pipeline.onLLMCallStarting(eventId, context.executionInfo, context.runId, prompt, model, tools, context)
4345

44-
val responses = executor.execute(prompt, model, tools)
46+
val modifiedPrompt = if (context.llm.prompt !== promptBeforeInterceptors) {
47+
logger.debug { "Executing LLM call with modified prompt (event id: $eventId, prompt: $prompt, tools: [${tools.joinToString { it.name }}])" }
48+
context.llm.prompt
49+
} else {
50+
prompt
51+
}
52+
53+
val responses = executor.execute(modifiedPrompt, model, tools)
4554

4655
logger.trace { "Finished LLM call (event id: $eventId) with responses: [${responses.joinToString { "${it.role}: ${it.content}" }}]" }
4756
context.pipeline.onLLMCallCompleted(
4857
eventId,
4958
context.executionInfo,
5059
context.runId,
51-
prompt,
60+
modifiedPrompt,
5261
model,
5362
tools,
5463
responses,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package ai.koog.agents.memory.feature
2+
3+
/**
4+
* Defines how retrieved context should be inserted into the prompt.
5+
*/
6+
public enum class ContextInsertionMode {
7+
/**
8+
* Insert context as a system message at the beginning of the prompt.
9+
*/
10+
SYSTEM_MESSAGE,
11+
12+
/**
13+
* Insert context as a user message before the last user message.
14+
*/
15+
USER_MESSAGE_BEFORE_LAST,
16+
17+
/**
18+
* Augment the last user message by prepending context to it.
19+
* Useful for RAG scenarios where context should be part of the user's question.
20+
*/
21+
AUGMENT_LAST_USER_MESSAGE
22+
}

agents/agents-features/agents-features-memory/src/commonMain/kotlin/ai/koog/agents/memory/feature/Memory2.kt

Lines changed: 175 additions & 190 deletions
Large diffs are not rendered by default.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package ai.koog.agents.memory.feature
2+
3+
import ai.koog.rag.vector.database.HybridSearchRequest
4+
import ai.koog.rag.vector.database.KeywordSearchRequest
5+
import ai.koog.rag.vector.database.MemoryRecord
6+
import ai.koog.rag.vector.database.MemoryRecordRepository
7+
import ai.koog.rag.vector.database.ScoredMemoryRecord
8+
import ai.koog.rag.vector.database.SimilaritySearchRequest
9+
import kotlin.jvm.JvmStatic
10+
11+
/**
12+
* Retriever of memory records during prompt augmentation.
13+
*
14+
* This is a functional interface (SAM) that can be implemented as a lambda
15+
* in both Kotlin and Java. It provides flexibility in how memory searches
16+
* are performed while maintaining type safety.
17+
*
18+
* Pre-built implementations are available for common search types:
19+
* - [SimilarityRecordRetriever] - Vector similarity search (semantic search)
20+
* - [KeywordRecordRetriever] - Full-text/keyword search
21+
* - [HybridRecordRetriever] - Combined vector and keyword search
22+
*
23+
* ### Usage Examples
24+
*
25+
* **Using pre-built retrievers (Kotlin):**
26+
* ```kotlin
27+
* // Similarity search with default parameters
28+
* val retriever = MemoryRecordRetriever.similarity()
29+
*
30+
* // Keyword search with custom topK
31+
* val keywordRetriever = MemoryRecordRetriever.keyword(topK = 5)
32+
*
33+
* // Hybrid search with custom alpha (balance between vector and keyword)
34+
* val hybridRetriever = MemoryRecordRetriever.hybrid(
35+
* topK = 10,
36+
* similarityThreshold = 0.7,
37+
* alpha = 0.6
38+
* )
39+
* ```
40+
*
41+
* **Custom implementation as lambda (Kotlin):**
42+
* ```kotlin
43+
* val customRetriever = MemoryRecordRetriever { repository, query ->
44+
* repository.search(SimilaritySearchRequest(
45+
* query = query,
46+
* topK = 5,
47+
* similarityThreshold = 0.8
48+
* ))
49+
* }
50+
* ```
51+
*
52+
* **Using pre-built retrievers (Java):**
53+
* ```java
54+
* // Similarity search with default parameters
55+
* MemoryRecordRetriever retriever = MemoryRecordRetriever.similarity();
56+
*
57+
* // Keyword search with custom topK
58+
* MemoryRecordRetriever keywordRetriever = MemoryRecordRetriever.keyword(5, 0.0, null);
59+
*
60+
* // Hybrid search with custom parameters
61+
* MemoryRecordRetriever hybridRetriever = MemoryRecordRetriever.hybrid(10, 0.7, 0.6, null);
62+
* ```
63+
*
64+
* **Custom implementation as lambda (Java):**
65+
* ```java
66+
* MemoryRecordRetriever customRetriever = (repository, query) ->
67+
* repository.search(new SimilaritySearchRequest(query, 5, 0.8, null));
68+
* ```
69+
*/
70+
public fun interface MemoryRecordRetriever {
71+
/**
72+
* Searches the repository for relevant memory records.
73+
*
74+
* @param repository The memory record repository to search
75+
* @param query The user's query string (typically the last user message content)
76+
* @return List of scored memory records, sorted by relevance
77+
*/
78+
public suspend fun retrieve(
79+
repository: MemoryRecordRepository,
80+
query: String
81+
): List<ScoredMemoryRecord<MemoryRecord>>
82+
83+
/**
84+
* Pre-defined retrievers.
85+
*/
86+
public companion object {
87+
/**
88+
* Creates a similarity search mode with the given parameters.
89+
* Uses vector similarity (semantic) search.
90+
*
91+
* @param topK Maximum number of results to return
92+
* @param similarityThreshold Minimum similarity score (0.0 to 1.0)
93+
* @param filterExpression Optional metadata filter expression
94+
* @return A configured similarity search mode
95+
*/
96+
@JvmStatic
97+
public fun similarity(
98+
topK: Int = 10,
99+
similarityThreshold: Double = 0.0,
100+
filterExpression: String? = null
101+
): MemoryRecordRetriever = SimilarityRecordRetriever(topK, similarityThreshold, filterExpression)
102+
103+
/**
104+
* Creates a keyword search mode with the given parameters.
105+
* Uses full-text/keyword matching.
106+
*
107+
* @param topK Maximum number of results to return
108+
* @param similarityThreshold Minimum similarity score (0.0 to 1.0)
109+
* @param filterExpression Optional metadata filter expression
110+
* @return A configured keyword search mode
111+
*/
112+
@JvmStatic
113+
public fun keyword(
114+
topK: Int = 10,
115+
similarityThreshold: Double = 0.0,
116+
filterExpression: String? = null
117+
): MemoryRecordRetriever = KeywordRecordRetriever(topK, similarityThreshold, filterExpression)
118+
119+
/**
120+
* Creates a hybrid search mode with the given parameters.
121+
* Combines vector similarity and keyword search.
122+
*
123+
* @param topK Maximum number of results to return
124+
* @param similarityThreshold Minimum similarity score (0.0 to 1.0)
125+
* @param alpha Balance between vector (0.0) and keyword (1.0) search. Default 0.5 for equal weight.
126+
* @param filterExpression Optional metadata filter expression
127+
* @return A configured hybrid search mode
128+
*/
129+
@JvmStatic
130+
public fun hybrid(
131+
topK: Int = 10,
132+
similarityThreshold: Double = 0.0,
133+
alpha: Double = 0.5,
134+
filterExpression: String? = null
135+
): MemoryRecordRetriever = HybridRecordRetriever(topK, similarityThreshold, alpha, filterExpression)
136+
}
137+
}
138+
139+
/**
140+
* Similarity search mode using vector embeddings for semantic search.
141+
*
142+
* This mode converts the query to a vector embedding and finds records
143+
* with similar embeddings in the vector store.
144+
*
145+
* @property topK Maximum number of results to return
146+
* @property similarityThreshold Minimum similarity score (0.0 to 1.0)
147+
* @property filterExpression Optional metadata filter expression for pre-filtering
148+
*/
149+
public class SimilarityRecordRetriever(
150+
public val topK: Int = 10,
151+
public val similarityThreshold: Double = 0.0,
152+
public val filterExpression: String? = null
153+
) : MemoryRecordRetriever {
154+
override suspend fun retrieve(
155+
repository: MemoryRecordRepository,
156+
query: String
157+
): List<ScoredMemoryRecord<MemoryRecord>> =
158+
repository.search(SimilaritySearchRequest(query, topK, similarityThreshold, filterExpression))
159+
}
160+
161+
/**
162+
* Keyword search mode using full-text/lexical matching.
163+
*
164+
* This mode uses traditional text matching instead of vector similarity,
165+
* which can be useful for exact term matching or when semantic search
166+
* is not needed.
167+
*
168+
* @property topK Maximum number of results to return
169+
* @property similarityThreshold Minimum similarity score (0.0 to 1.0)
170+
* @property filterExpression Optional metadata filter expression for pre-filtering
171+
*/
172+
public class KeywordRecordRetriever(
173+
public val topK: Int = 10,
174+
public val similarityThreshold: Double = 0.0,
175+
public val filterExpression: String? = null
176+
) : MemoryRecordRetriever {
177+
override suspend fun retrieve(
178+
repository: MemoryRecordRepository,
179+
query: String
180+
): List<ScoredMemoryRecord<MemoryRecord>> =
181+
repository.search(KeywordSearchRequest(query, topK, similarityThreshold, filterExpression))
182+
}
183+
184+
/**
185+
* Hybrid search mode combining vector similarity and keyword search.
186+
*
187+
* This mode balances semantic understanding (vector search) with exact
188+
* term matching (keyword search), often providing better results than
189+
* either approach alone.
190+
*
191+
* @property topK Maximum number of results to return
192+
* @property similarityThreshold Minimum similarity score (0.0 to 1.0)
193+
* @property alpha Balance between vector (0.0) and keyword (1.0) search. Default 0.5 for equal weight.
194+
* @property filterExpression Optional metadata filter expression for pre-filtering
195+
*/
196+
public class HybridRecordRetriever(
197+
public val topK: Int = 10,
198+
public val similarityThreshold: Double = 0.0,
199+
public val alpha: Double = 0.5,
200+
public val filterExpression: String? = null
201+
) : MemoryRecordRetriever {
202+
init {
203+
require(alpha in 0.0..1.0) { "Alpha must be between 0.0 and 1.0, got $alpha" }
204+
}
205+
206+
override suspend fun retrieve(
207+
repository: MemoryRecordRepository,
208+
query: String
209+
): List<ScoredMemoryRecord<MemoryRecord>> =
210+
repository.search(HybridSearchRequest(query, null, alpha, topK, similarityThreshold, filterExpression))
211+
}

0 commit comments

Comments
 (0)