Skip to content

Commit f82ecf8

Browse files
committed
Introduce new feature for storage and retrieval memory records from external databases. Introduce RAG strategies with nodeRetrieveFromMemoryAndAugment and memoryExtractionStrategy with nodeExtractAndSaveMemoryRecord. Introduce interfaces MemoryRecordRepository and ScopedMemoryRecordRepository for working with external databases and an implementation for pgvector (PGVectorMemoryRecordRepository).
1 parent d5bc207 commit f82ecf8

File tree

17 files changed

+4124
-0
lines changed

17 files changed

+4124
-0
lines changed

agents/agents-features/agents-features-memory/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ kotlin {
1515
api(project(":agents:agents-core"))
1616
api(project(":prompt:prompt-markdown"))
1717
api(project(":rag:rag-base"))
18+
api(project(":rag:vector-storage"))
1819

1920
api(libs.kotlinx.serialization.json)
2021
api(libs.ktor.client.content.negotiation)
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package ai.koog.agents.memory.feature
2+
3+
import ai.koog.agents.core.agent.context.AIAgentContext
4+
import ai.koog.agents.core.agent.context.featureOrThrow
5+
import ai.koog.agents.core.agent.entity.AIAgentStorageKey
6+
import ai.koog.agents.core.agent.entity.createStorageKey
7+
import ai.koog.agents.core.annotation.InternalAgentsApi
8+
import ai.koog.agents.core.feature.AIAgentFunctionalFeature
9+
import ai.koog.agents.core.feature.AIAgentGraphFeature
10+
import ai.koog.agents.core.feature.config.FeatureConfig
11+
import ai.koog.agents.core.feature.pipeline.AIAgentFunctionalPipeline
12+
import ai.koog.agents.core.feature.pipeline.AIAgentGraphPipeline
13+
import ai.koog.agents.core.feature.pipeline.AIAgentPipeline
14+
import ai.koog.agents.memory.repositories.ChatMessageRepository
15+
import ai.koog.agents.memory.repositories.NoChatMessageRepository
16+
import ai.koog.agents.memory.repositories.NoMemoryRecordRepository
17+
import ai.koog.rag.vector.database.MemoryRecord
18+
import ai.koog.rag.vector.database.MemoryRecordRepository
19+
import ai.koog.rag.vector.database.SimilaritySearchRequest
20+
import ai.koog.rag.vector.database.records
21+
import io.github.oshai.kotlinlogging.KotlinLogging
22+
import kotlinx.serialization.json.Json
23+
import kotlinx.serialization.json.JsonPrimitive
24+
import kotlinx.serialization.serializer
25+
import kotlin.collections.emptyMap
26+
27+
/**
28+
* Memory feature that incorporates persistent storage of message history and memory records (documents) in vector databases
29+
*/
30+
@OptIn(InternalAgentsApi::class)
31+
public class Memory2(
32+
private val chatMessageRepository: ChatMessageRepository,//for future use
33+
private val memoryRecordRepository: MemoryRecordRepository
34+
) {
35+
36+
/**
37+
* Configuration for the Memory2 feature.
38+
*
39+
* This class allows configuring:
40+
* - The message repository
41+
* - The record repository
42+
*/
43+
public class Config : FeatureConfig() {
44+
/**
45+
* The provider that handles the actual storage and retrieval of chat messages.
46+
* Defaults to [NoChatMessageRepository], which doesn't store anything.
47+
*/
48+
public var chatMessageRepository: ChatMessageRepository = NoChatMessageRepository
49+
50+
/**
51+
* The provider that handles the actual storage and retrieval of memory records.
52+
* Defaults to [NoMemoryRecordRepository], which doesn't store anything.
53+
*/
54+
public var memoryRecordRepository: MemoryRecordRepository = NoMemoryRecordRepository
55+
}
56+
57+
/**
58+
* Companion object implementing agent feature, handling [Memory2] creation and installation.
59+
*/
60+
public companion object Feature : AIAgentGraphFeature<Config, Memory2>, AIAgentFunctionalFeature<Config, Memory2> {
61+
private val logger = KotlinLogging.logger { }
62+
63+
override val key: AIAgentStorageKey<Memory2> = createStorageKey<Memory2>("ai-agent-memory-feature")
64+
65+
override fun createInitialConfig(): Config = Config()
66+
67+
/**
68+
* Create a feature implementation using the provided configuration.
69+
*/
70+
private fun createFeature(
71+
config: Config,
72+
pipeline: AIAgentPipeline,//TODO: should we pipeline.interceptStrategyStarting?
73+
): Memory2 {
74+
return Memory2(config.chatMessageRepository, config.memoryRecordRepository)
75+
}
76+
77+
override fun install(
78+
config: Config,
79+
pipeline: AIAgentGraphPipeline,
80+
): Memory2 = createFeature(config, pipeline)
81+
82+
override fun install(
83+
config: Config,
84+
pipeline: AIAgentFunctionalPipeline,
85+
): Memory2 = createFeature(config, pipeline)
86+
}
87+
88+
/**
89+
* Stores string contents in the memory record repository.
90+
*
91+
* @param memoryRecordsContents The list of raw strings to store.
92+
* @param typeMetadataFieldName The field name in metadata for storing a simple name of the original class.
93+
* @param typeMetadataValue The simple name of the class of saved content. Null by default for raw strings.
94+
*/
95+
@PublishedApi
96+
internal suspend fun storeRawMemoryContent(
97+
memoryRecordsContents: List<String>,
98+
typeMetadataFieldName: String = "class_name",
99+
typeMetadataValue: String? = null
100+
) {
101+
if (memoryRecordsContents.isEmpty()) {
102+
return
103+
}
104+
105+
val metadata = if (typeMetadataValue != null) {
106+
mapOf(typeMetadataFieldName to JsonPrimitive(typeMetadataValue))
107+
} else {
108+
emptyMap()
109+
}
110+
111+
val memoryRecords = memoryRecordsContents.map { MemoryRecord(content = it, metadata = metadata) }
112+
113+
logger.debug { "Storing ${memoryRecordsContents.size} memory record(s)" }
114+
val batchOperationResult = memoryRecordRepository.add(memoryRecords)
115+
logger.debug { "batchOperationResult: $batchOperationResult" }
116+
}
117+
118+
/**
119+
* Stores memory records with type information in the memory record repository.
120+
*
121+
* @param T The type of the content to store. Must be serializable.
122+
* @param contents The list of typed content to store.
123+
* @param json The JSON serializer to use. Defaults to a lenient configuration.
124+
*/
125+
public suspend inline fun <reified T> store(
126+
contents: List<T>,
127+
json: Json = Json { ignoreUnknownKeys = true }
128+
) {
129+
if (contents.isEmpty()) {
130+
return
131+
}
132+
133+
val serializer = serializer<T>()
134+
val jsonStrings = contents.map { content ->
135+
json.encodeToString(serializer, content)
136+
}
137+
138+
storeRawMemoryContent(memoryRecordsContents = jsonStrings, typeMetadataValue = T::class.simpleName)
139+
//T::class.qualifiedName is not supported in Kotlin/JS
140+
}
141+
142+
143+
/**
144+
* Internal method that searches for memory records matching the given query.
145+
*
146+
* @param query The search query string.
147+
* @param topK The maximum number of results to return.
148+
* @param scoreThreshold The minimum score threshold for results (default 0.0).
149+
* @param filterExpression Metadata filter expression.
150+
* @return A list of strings from the memory repository that matches the search criteria.
151+
*/
152+
@PublishedApi
153+
internal suspend fun searchRawMemoryContent(
154+
query: String,
155+
topK: Int,
156+
scoreThreshold: Double = 0.0,
157+
filterExpression: String? = null
158+
): List<String> {
159+
logger.debug { "Searching memory records with query: ${query.shortened()}" }
160+
161+
val memoryRecords = memoryRecordRepository.search(
162+
SimilaritySearchRequest(
163+
query,
164+
topK,
165+
scoreThreshold,
166+
filterExpression
167+
)
168+
).records()
169+
170+
return memoryRecords.map { it.content }
171+
}
172+
173+
/**
174+
* Searches for typed memory records matching the given query.
175+
*
176+
* This function searches for memory records and deserializes their content
177+
* to the specified type. Records that fail to deserialize are skipped.
178+
*
179+
* @param T The type to deserialize the content to. Must be serializable.
180+
* @param query The search query string.
181+
* @param topK The maximum number of results to return.
182+
* @param scoreThreshold The minimum score threshold for results (default 0.0).
183+
* @param filterExpression Metadata filter expression.
184+
* @param json The JSON deserializer to use. Defaults to a lenient configuration.
185+
* @return A list of deserialized content matching the search criteria.
186+
*/
187+
public suspend inline fun <reified T> search(
188+
query: String,
189+
topK: Int,
190+
scoreThreshold: Double = 0.0,
191+
filterExpression: String? = null,
192+
json: Json = Json { ignoreUnknownKeys = true }
193+
): List<T> {
194+
val memoryRecordContents =
195+
searchRawMemoryContent(query, topK, scoreThreshold, filterExpression) // TODO: should filter by type
196+
val serializer = serializer<T>()
197+
198+
return memoryRecordContents.mapNotNull { content ->
199+
try {
200+
json.decodeFromString(serializer, content)
201+
} catch (e: Exception) {
202+
null // TODO: log warning about unserialized records
203+
}
204+
}
205+
}
206+
}
207+
208+
/**
209+
* Utility function to shorten a string for display purposes.
210+
* Takes the first line and truncates it to 100 characters.
211+
*/
212+
private fun String.shortened() = lines().first().take(100) + "..."
213+
214+
/**
215+
* Extension function to access the Memory2 feature from a AIAgentStageContext.
216+
*
217+
* This provides a convenient way to access memory operations within agent nodes.
218+
*
219+
* Example usage:
220+
* ```kotlin
221+
* val rememberUserPreference by node {
222+
* // Access memory directly
223+
* val memory = stageContext.memory2()
224+
* // Use memory operations...
225+
* }
226+
* ```
227+
*
228+
* @return The Memory2 instance for this agent context
229+
*/
230+
public fun AIAgentContext.memory2(): Memory2 = featureOrThrow(Memory2)
231+
232+
/**
233+
* Extension function to perform memory operations within a AIAgentStageContext.
234+
*
235+
* This provides a convenient way to use memory operations within agent nodes
236+
* with a more concise syntax using the `withMemory` block.
237+
*
238+
* Example usage:
239+
* ```kotlin
240+
* val loadUserPreferences by node {
241+
* // Use memory operations in a block
242+
* stageContext.withMemory2 {
243+
* loadMemoryRecordsToChat(
244+
* searchQuery = = "User's preferred programming language")
245+
* )
246+
* }
247+
* }
248+
* ```
249+
*
250+
* @param action The memory operations to perform
251+
* @return The result of the action
252+
*/
253+
public suspend fun <T> AIAgentContext.withMemory2(action: suspend Memory2.() -> T): T = memory2().action()

0 commit comments

Comments
 (0)