diff --git a/docs/agent-memory.md b/docs/agent-memory.md new file mode 100644 index 000000000..279f85bfe --- /dev/null +++ b/docs/agent-memory.md @@ -0,0 +1,187 @@ +# Agent Memory System + +## Overview + +The BrowserPerceptiveAgent now includes a memory system that allows it to maintain context across interactions. This feature enables the agent to remember important information from previous steps and use it to make better decisions in future actions. + +## Architecture + +The memory system consists of three main components: + +### 1. ShortTermMemory +- **Purpose**: Stores recent memories using a sliding window approach +- **Capacity**: Configurable (default: 50 memories) +- **Retention**: Automatically removes oldest memories when capacity is reached +- **Use Case**: Maintaining context of recent actions and observations + +### 2. LongTermMemory +- **Purpose**: Stores important facts and information +- **Filtering**: Only stores memories above an importance threshold (default: 0.7) +- **Capacity**: Configurable (default: 200 memories) +- **Retention**: Removes least important memories when capacity is reached +- **Use Case**: Preserving critical information for long-running tasks + +### 3. CompositeMemory +- **Purpose**: Combines short-term and long-term memory +- **Deduplication**: Automatically handles duplicate memories across both stores +- **Retrieval**: Merges and ranks memories by relevance and importance +- **Use Case**: Provides unified interface for memory operations + +## Configuration + +Memory settings can be configured through `AgentConfig`: + +```kotlin +val config = AgentConfig( + enableMemory = true, // Enable/disable memory system + shortTermMemorySize = 50, // Max short-term memories + longTermMemorySize = 200, // Max long-term memories + longTermMemoryThreshold = 0.7, // Min importance for long-term storage + memoryRetrievalLimit = 10 // Max memories to include in prompts +) +``` + +## How It Works + +### 1. Memory Collection + +The agent automatically extracts and stores memories during execution: + +- **From LLM Responses**: The LLM can provide a "memory" field in its responses containing important observations +- **From Evaluations**: Previous action evaluations are stored as memories +- **From Goals**: Next goal statements are stored with high importance + +### 2. Memory Retrieval + +When generating actions, the agent: +1. Retrieves relevant memories based on the current instruction +2. Formats them for inclusion in the LLM prompt +3. Provides context to help the LLM make informed decisions + +### 3. Memory Persistence + +Memories are automatically saved and restored through the checkpoint system: +- Memories are included in checkpoint snapshots +- When restoring a session, memories are automatically loaded +- Enables resuming tasks with full context preservation + +## Usage Example + +### Basic Usage + +```kotlin +val agent = BrowserPerceptiveAgent(driver, session, config = AgentConfig( + enableMemory = true, + shortTermMemorySize = 50, + longTermMemorySize = 200 +)) + +// Memories are automatically collected and used +agent.resolve("Search for products and compare prices") +``` + +### Manual Memory Management + +```kotlin +// Add a memory manually +agent.memory.add( + content = "User prefers products under $100", + importance = 0.9, + metadata = mapOf("category" to "preference") +) + +// Retrieve relevant memories +val memories = agent.memory.retrieve(query = "price preference", limit = 5) + +// Get memory context for prompts +val context = agent.memory.getMemoryContext(query = "shopping", limit = 10) + +// Clear all memories +agent.memory.clear() +``` + +### Checkpoint Integration + +```kotlin +// Enable checkpointing to persist memories +val config = AgentConfig( + enableMemory = true, + enableCheckpointing = true, + checkpointIntervalSteps = 10 +) + +val agent = BrowserPerceptiveAgent(driver, session, config = config) + +// Memories are automatically saved in checkpoints +agent.resolve("Complex multi-step task") + +// Restore from checkpoint (includes memory state) +val checkpoint = agent.restoreFromCheckpoint(sessionId) +``` + +## Memory Importance Scoring + +Memories are assigned importance scores (0.0 to 1.0) based on context: + +- **1.0**: Task completion events +- **0.8**: Next goal statements +- **0.7**: Successful action memories +- **0.6**: Action evaluations +- **0.3**: Failed action memories + +Only memories above the `longTermMemoryThreshold` (default 0.7) are stored in long-term memory. + +## LLM Integration + +The memory field is included in the observation schema sent to the LLM: + +```json +{ + "elements": [ + { + "locator": "0,4", + "description": "Description of action", + "domain": "driver", + "method": "click", + "memory": "1-3 specific sentences describing this step and overall progress", + "evaluationPreviousGoal": "Analysis of previous action", + "nextGoal": "Clear statement of next goal" + } + ] +} +``` + +The LLM can populate the `memory` field with important observations that should be remembered. + +## Benefits + +1. **Context Preservation**: Maintains important information across multiple steps +2. **Improved Decisions**: LLM has access to relevant past information +3. **Task Continuity**: Can resume long-running tasks with full context +4. **Reduced Repetition**: Avoids re-learning information already discovered +5. **Better Planning**: Can track progress and make informed decisions + +## Best Practices + +1. **Configure Appropriately**: Adjust memory sizes based on task complexity +2. **Use Importance Wisely**: Assign higher importance to critical information +3. **Enable Checkpointing**: Combine with checkpoints for robust recovery +4. **Monitor Memory Usage**: Clear memories when starting unrelated tasks +5. **Provide Context**: Include relevant queries when retrieving memories + +## Performance Considerations + +- Memory retrieval is fast (O(n) with n = number of memories) +- Short-term memory uses a deque for efficient oldest-first removal +- Long-term memory uses a hash map for fast lookups and deduplication +- Memory snapshots are included in checkpoints (consider size when configuring) + +## Future Enhancements + +Potential improvements to the memory system: + +- Vector-based semantic search for more accurate retrieval +- Automatic importance scoring based on task outcomes +- Memory consolidation and summarization +- External memory storage (database, file system) +- Memory visualization and debugging tools diff --git a/docs/zh/agent-memory.md b/docs/zh/agent-memory.md new file mode 100644 index 000000000..31d8127f7 --- /dev/null +++ b/docs/zh/agent-memory.md @@ -0,0 +1,187 @@ +# 智能体记忆系统 + +## 概述 + +BrowserPerceptiveAgent 现在包含一个记忆系统,使其能够跨交互维护上下文。此功能使智能体能够记住先前步骤中的重要信息,并使用这些信息在未来的操作中做出更好的决策。 + +## 架构 + +记忆系统由三个主要组件组成: + +### 1. ShortTermMemory(短期记忆) +- **目的**:使用滑动窗口方法存储最近的记忆 +- **容量**:可配置(默认:50 条记忆) +- **保留策略**:达到容量时自动删除最旧的记忆 +- **使用场景**:维护最近操作和观察的上下文 + +### 2. LongTermMemory(长期记忆) +- **目的**:存储重要事实和信息 +- **过滤机制**:仅存储重要度高于阈值的记忆(默认:0.7) +- **容量**:可配置(默认:200 条记忆) +- **保留策略**:达到容量时删除最不重要的记忆 +- **使用场景**:为长期运行的任务保留关键信息 + +### 3. CompositeMemory(组合记忆) +- **目的**:结合短期和长期记忆 +- **去重功能**:自动处理两个存储中的重复记忆 +- **检索机制**:按相关性和重要性合并和排序记忆 +- **使用场景**:提供统一的记忆操作接口 + +## 配置 + +可以通过 `AgentConfig` 配置记忆设置: + +```kotlin +val config = AgentConfig( + enableMemory = true, // 启用/禁用记忆系统 + shortTermMemorySize = 50, // 最大短期记忆数量 + longTermMemorySize = 200, // 最大长期记忆数量 + longTermMemoryThreshold = 0.7, // 长期存储的最低重要度 + memoryRetrievalLimit = 10 // 提示中包含的最大记忆数量 +) +``` + +## 工作原理 + +### 1. 记忆收集 + +智能体在执行过程中自动提取和存储记忆: + +- **从 LLM 响应中**:LLM 可以在其响应中提供包含重要观察的"memory"字段 +- **从评估中**:先前操作的评估被存储为记忆 +- **从目标中**:下一步目标陈述以高重要度存储 + +### 2. 记忆检索 + +在生成操作时,智能体: +1. 根据当前指令检索相关记忆 +2. 格式化以包含在 LLM 提示中 +3. 提供上下文帮助 LLM 做出明智的决策 + +### 3. 记忆持久化 + +记忆通过检查点系统自动保存和恢复: +- 记忆包含在检查点快照中 +- 恢复会话时,记忆会自动加载 +- 支持在完整上下文下恢复任务 + +## 使用示例 + +### 基本使用 + +```kotlin +val agent = BrowserPerceptiveAgent(driver, session, config = AgentConfig( + enableMemory = true, + shortTermMemorySize = 50, + longTermMemorySize = 200 +)) + +// 记忆会自动收集和使用 +agent.resolve("搜索产品并比较价格") +``` + +### 手动记忆管理 + +```kotlin +// 手动添加记忆 +agent.memory.add( + content = "用户偏好 100 元以下的产品", + importance = 0.9, + metadata = mapOf("category" to "preference") +) + +// 检索相关记忆 +val memories = agent.memory.retrieve(query = "价格偏好", limit = 5) + +// 获取用于提示的记忆上下文 +val context = agent.memory.getMemoryContext(query = "购物", limit = 10) + +// 清除所有记忆 +agent.memory.clear() +``` + +### 检查点集成 + +```kotlin +// 启用检查点以持久化记忆 +val config = AgentConfig( + enableMemory = true, + enableCheckpointing = true, + checkpointIntervalSteps = 10 +) + +val agent = BrowserPerceptiveAgent(driver, session, config = config) + +// 记忆会自动保存在检查点中 +agent.resolve("复杂的多步骤任务") + +// 从检查点恢复(包括记忆状态) +val checkpoint = agent.restoreFromCheckpoint(sessionId) +``` + +## 记忆重要性评分 + +记忆根据上下文分配重要性分数(0.0 到 1.0): + +- **1.0**:任务完成事件 +- **0.8**:下一步目标陈述 +- **0.7**:成功操作的记忆 +- **0.6**:操作评估 +- **0.3**:失败操作的记忆 + +只有重要度高于 `longTermMemoryThreshold`(默认 0.7)的记忆才会存储在长期记忆中。 + +## LLM 集成 + +记忆字段包含在发送给 LLM 的观察模式中: + +```json +{ + "elements": [ + { + "locator": "0,4", + "description": "操作描述", + "domain": "driver", + "method": "click", + "memory": "描述此步骤和整体进度的 1-3 个具体句子", + "evaluationPreviousGoal": "对前一个操作的分析", + "nextGoal": "下一步目标的明确陈述" + } + ] +} +``` + +LLM 可以在 `memory` 字段中填充应该记住的重要观察。 + +## 优势 + +1. **上下文保留**:在多个步骤中维护重要信息 +2. **改进决策**:LLM 可以访问相关的过去信息 +3. **任务连续性**:可以在完整上下文下恢复长期运行的任务 +4. **减少重复**:避免重新学习已经发现的信息 +5. **更好的规划**:可以跟踪进度并做出明智的决策 + +## 最佳实践 + +1. **适当配置**:根据任务复杂度调整记忆大小 +2. **明智使用重要度**:为关键信息分配更高的重要度 +3. **启用检查点**:结合检查点实现稳健的恢复 +4. **监控记忆使用**:在开始不相关任务时清除记忆 +5. **提供上下文**:检索记忆时包含相关查询 + +## 性能考虑 + +- 记忆检索速度快(O(n),其中 n = 记忆数量) +- 短期记忆使用双端队列实现高效的最旧优先删除 +- 长期记忆使用哈希映射实现快速查找和去重 +- 记忆快照包含在检查点中(配置时考虑大小) + +## 未来增强 + +记忆系统的潜在改进: + +- 基于向量的语义搜索以实现更准确的检索 +- 基于任务结果的自动重要性评分 +- 记忆整合和总结 +- 外部记忆存储(数据库、文件系统) +- 记忆可视化和调试工具 diff --git a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/BrowserPerceptiveAgent.kt b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/BrowserPerceptiveAgent.kt index 468862bc0..eea53cdf8 100644 --- a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/BrowserPerceptiveAgent.kt +++ b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/BrowserPerceptiveAgent.kt @@ -5,6 +5,9 @@ import ai.platon.pulsar.agentic.ai.PromptBuilder import ai.platon.pulsar.agentic.ai.agent.InferenceEngine import ai.platon.pulsar.agentic.ai.agent.ObserveParams import ai.platon.pulsar.agentic.ai.agent.detail.* +import ai.platon.pulsar.agentic.ai.memory.CompositeMemory +import ai.platon.pulsar.agentic.ai.memory.LongTermMemory +import ai.platon.pulsar.agentic.ai.memory.ShortTermMemory import ai.platon.pulsar.agentic.ai.todo.ToDoManager import ai.platon.pulsar.agentic.ai.tta.ContextToAction import ai.platon.pulsar.agentic.ai.tta.DetailedActResult @@ -95,6 +98,12 @@ data class AgentConfig( val todoMaxProgressLines: Int = 200, val todoEnableAutoCheck: Boolean = true, val todoTagsFromToolCall: Boolean = true, + // --- Memory configuration --- + val enableMemory: Boolean = true, + val shortTermMemorySize: Int = 50, + val longTermMemorySize: Int = 200, + val longTermMemoryThreshold: Double = 0.7, + val memoryRetrievalLimit: Int = 10, ) open class BrowserPerceptiveAgent constructor( @@ -150,6 +159,14 @@ open class BrowserPerceptiveAgent constructor( CheckpointManager(baseDir.resolve("checkpoints")) } + // Memory system for maintaining context across interactions + val memory: CompositeMemory by lazy { + CompositeMemory( + shortTerm = ShortTermMemory(config.shortTermMemorySize), + longTerm = LongTermMemory(config.longTermMemoryThreshold, config.longTermMemorySize) + ) + } + val baseDir get() = toolExecutor.baseDir val startTime = Instant.now() @@ -387,8 +404,13 @@ open class BrowserPerceptiveAgent constructor( } else null context.screenshotB64 = screenshotB64 + // Get memory context if enabled + val memoryContext = if (config.enableMemory) { + memory.getMemoryContext(context.instruction, config.memoryRetrievalLimit) + } else "" + // Prepare messages for model - val messages = promptBuilder.buildResolveObserveMessageList(context, stateHistory) + val messages = promptBuilder.buildResolveObserveMessageList(context, stateHistory, memoryContext) return try { val action = cta.generate(messages, context) @@ -767,6 +789,8 @@ open class BrowserPerceptiveAgent constructor( stateManager.updateAgentState(context.agentState, detailedActResult) updateTodo(context, detailedActResult) + + saveMemory(detailedActResult) updatePerformanceMetrics(step, context.timestamp, true) @@ -897,6 +921,44 @@ open class BrowserPerceptiveAgent constructor( } } + private fun saveMemory(detailedActResult: DetailedActResult) { + if (!config.enableMemory) return + + val observeElement = detailedActResult.actionDescription.observeElement + + // Save the memory field if present + observeElement?.memory?.let { memoryContent -> + if (memoryContent.isNotBlank()) { + // Calculate importance based on success and context + val importance = when { + !detailedActResult.success -> 0.3 // Less important if failed + detailedActResult.actionDescription.isComplete -> 1.0 // Very important if task complete + else -> 0.7 // Normal importance for successful actions + } + + val metadata = mutableMapOf() + observeElement.method?.let { metadata["action"] = it } + observeElement.locator?.let { metadata["locator"] = it } + + memory.add(memoryContent, importance, metadata) + logger.debug("💾 memory.saved importance={} content={}", importance, memoryContent.take(50)) + } + } + + // Also save evaluation and next goal as memories if they exist + observeElement?.evaluationPreviousGoal?.let { evaluation -> + if (evaluation.isNotBlank()) { + memory.add("评估: $evaluation", 0.6) + } + } + + observeElement?.nextGoal?.let { nextGoal -> + if (nextGoal.isNotBlank()) { + memory.add("下一步目标: $nextGoal", 0.8) + } + } + } + /** * Classifies errors for appropriate retry strategies */ @@ -1163,6 +1225,7 @@ open class BrowserPerceptiveAgent constructor( instruction = context.instruction, targetUrl = context.targetUrl, recentStateHistory = stateHistory.takeLast(20).map { AgentStateSnapshot.from(it) }, + memorySnapshot = if (config.enableMemory) memory.createSnapshot() else null, totalSteps = performanceMetrics.totalSteps, successfulActions = performanceMetrics.successfulActions, failedActions = performanceMetrics.failedActions, @@ -1201,7 +1264,19 @@ open class BrowserPerceptiveAgent constructor( "💾 checkpoint.restored sid={} step={} age={}ms", sessionId.take(8), checkpoint.currentStep, checkpoint.age ) - // TODO: Restore state from checkpoint (implementation depends on requirements) + + // Restore memory state if available + if (config.enableMemory && checkpoint.memorySnapshot != null) { + memory.restoreFromSnapshot(checkpoint.memorySnapshot) + logger.info( + "💾 memory.restored sid={} shortTerm={} longTerm={}", + sessionId.take(8), + checkpoint.memorySnapshot.shortTermMemories.size, + checkpoint.memorySnapshot.longTermMemories.size + ) + } + + // TODO: Restore other state from checkpoint // This would involve: // - Restoring performance metrics // - Restoring circuit breaker state diff --git a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/PromptBuilder.kt b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/PromptBuilder.kt index 66cb9f0aa..bb79183ac 100644 --- a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/PromptBuilder.kt +++ b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/PromptBuilder.kt @@ -657,7 +657,11 @@ $AGENT_GUIDE_SYSTEM_PROMPT } } - fun buildResolveObserveMessageList(context: ExecutionContext, stateHistory: List): AgentMessageList { + fun buildResolveObserveMessageList( + context: ExecutionContext, + stateHistory: List, + memoryContext: String = "" + ): AgentMessageList { val instruction = context.instruction val messages = AgentMessageList() @@ -666,6 +670,12 @@ $AGENT_GUIDE_SYSTEM_PROMPT messages.addSystem(systemMsg) messages.addLast("user", buildUserRequestMessage(instruction), name = "user_request") messages.addUser(buildAgentStateHistoryMessage(stateHistory)) + + // Add memory context if available + if (memoryContext.isNotBlank()) { + messages.addUser(buildMemoryContextMessage(memoryContext)) + } + if (context.screenshotB64 != null) { messages.addUser(buildBrowserVisionInfo()) } @@ -751,6 +761,25 @@ $historyJson return msg } + fun buildMemoryContextMessage(memoryContext: String): String { + if (memoryContext.isBlank()) { + return "" + } + + val msg = """ +## 记忆 + +智能体在过往交互中积累的重要记忆信息。这些记忆可以帮助你更好地理解上下文和完成任务。 + +$memoryContext + +--- + + """.trimIndent() + + return msg + } + fun buildAgentStateMessage(state: AgentState): String { val message = """ ## 智能体状态 diff --git a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/agent/detail/CheckpointManager.kt b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/agent/detail/CheckpointManager.kt index 424cbbf27..8f7fd10d7 100644 --- a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/agent/detail/CheckpointManager.kt +++ b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/agent/detail/CheckpointManager.kt @@ -1,5 +1,6 @@ package ai.platon.pulsar.agentic.ai.agent.detail +import ai.platon.pulsar.agentic.ai.memory.MemorySnapshot import ai.platon.pulsar.skeleton.ai.AgentState import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.databind.ObjectMapper @@ -28,6 +29,9 @@ data class AgentCheckpoint( // State history (limited to avoid large files) val recentStateHistory: List, + // Memory snapshot + val memorySnapshot: MemorySnapshot? = null, + // Performance metrics val totalSteps: Int, val successfulActions: Int, diff --git a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/AgentMemory.kt b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/AgentMemory.kt new file mode 100644 index 000000000..2f3b96f9a --- /dev/null +++ b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/AgentMemory.kt @@ -0,0 +1,65 @@ +package ai.platon.pulsar.agentic.ai.memory + +import java.time.Instant + +/** + * Represents a single memory entry in the agent's memory system. + * + * @property content The actual content/text of the memory + * @property timestamp When this memory was created + * @property importance Priority/relevance score (0.0 to 1.0) + * @property metadata Additional context about this memory + */ +data class Memory( + val content: String, + val timestamp: Instant = Instant.now(), + val importance: Double = 0.5, + val metadata: Map = emptyMap() +) { + init { + require(importance in 0.0..1.0) { "Importance must be between 0.0 and 1.0" } + } +} + +/** + * Interface for agent memory systems. + * Provides storage and retrieval of memories to help the agent maintain context. + */ +interface AgentMemory { + /** + * Add a new memory to the storage. + * + * @param content The memory content + * @param importance Importance score (0.0 to 1.0), defaults to 0.5 + * @param metadata Additional metadata for this memory + */ + fun add(content: String, importance: Double = 0.5, metadata: Map = emptyMap()) + + /** + * Retrieve the most relevant memories based on the query. + * + * @param query The query to match against memories + * @param limit Maximum number of memories to return + * @return List of matching memories, ordered by relevance + */ + fun retrieve(query: String? = null, limit: Int = 10): List + + /** + * Get all memories in chronological order. + * + * @return All stored memories + */ + fun getAll(): List + + /** + * Clear all memories from storage. + */ + fun clear() + + /** + * Get the number of memories currently stored. + * + * @return Count of memories + */ + fun size(): Int +} diff --git a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/CompositeMemory.kt b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/CompositeMemory.kt new file mode 100644 index 000000000..c84cb3381 --- /dev/null +++ b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/CompositeMemory.kt @@ -0,0 +1,132 @@ +package ai.platon.pulsar.agentic.ai.memory + +/** + * Composite memory that combines short-term and long-term memory. + * Provides a unified interface for managing both types of memories. + * + * @property shortTerm Short-term memory for recent context + * @property longTerm Long-term memory for important facts + */ +class CompositeMemory( + val shortTerm: ShortTermMemory = ShortTermMemory(), + val longTerm: LongTermMemory = LongTermMemory() +) : AgentMemory { + + /** + * Add to both short-term and long-term memory. + * Long-term will filter based on its importance threshold. + */ + override fun add(content: String, importance: Double, metadata: Map) { + shortTerm.add(content, importance, metadata) + longTerm.add(content, importance, metadata) + } + + /** + * Retrieve from both memories and merge results. + * Prioritizes more important and relevant memories. + */ + override fun retrieve(query: String?, limit: Int): List { + val shortMemories = shortTerm.retrieve(query, limit) + val longMemories = longTerm.retrieve(query, limit) + + // Combine and deduplicate based on content + val combined = (shortMemories + longMemories) + .distinctBy { it.content.trim().lowercase() } + .sortedWith( + compareByDescending { it.importance } + .thenByDescending { it.timestamp } + ) + .take(limit) + + return combined + } + + /** + * Get all memories from both stores. + */ + override fun getAll(): List { + val shortMemories = shortTerm.getAll() + val longMemories = longTerm.getAll() + + return (shortMemories + longMemories) + .distinctBy { it.content.trim().lowercase() } + .sortedByDescending { it.timestamp } + } + + /** + * Clear both memory stores. + */ + override fun clear() { + shortTerm.clear() + longTerm.clear() + } + + /** + * Get total unique memory count across both stores. + */ + override fun size(): Int { + val allMemories = getAll() + return allMemories.size + } + + /** + * Get formatted memory context string for inclusion in prompts. + * + * @param query Optional query to filter relevant memories + * @param limit Maximum number of memories to include + * @return Formatted string containing memory context + */ + fun getMemoryContext(query: String? = null, limit: Int = 10): String { + val memories = retrieve(query, limit) + + if (memories.isEmpty()) { + return "" + } + + return buildString { + appendLine("## 记忆") + appendLine() + memories.forEachIndexed { index, memory -> + appendLine("${index + 1}. ${memory.content}") + } + } + } + + /** + * Create a snapshot of the memory state for checkpointing. + * + * @return A snapshot containing all memories + */ + fun createSnapshot(): MemorySnapshot { + return MemorySnapshot( + shortTermMemories = shortTerm.getAll(), + longTermMemories = longTerm.getAll() + ) + } + + /** + * Restore memory state from a snapshot. + * + * @param snapshot The snapshot to restore from + */ + fun restoreFromSnapshot(snapshot: MemorySnapshot) { + clear() + snapshot.shortTermMemories.forEach { memory -> + shortTerm.add(memory.content, memory.importance, memory.metadata) + } + snapshot.longTermMemories.forEach { memory -> + longTerm.add(memory.content, memory.importance, memory.metadata) + } + } +} + +/** + * Snapshot of memory state for serialization and restoration. + * + * @property shortTermMemories List of memories from short-term storage + * @property longTermMemories List of memories from long-term storage + */ +data class MemorySnapshot( + val shortTermMemories: List = emptyList(), + val longTermMemories: List = emptyList() +) diff --git a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/LongTermMemory.kt b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/LongTermMemory.kt new file mode 100644 index 000000000..b7eafbc5c --- /dev/null +++ b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/LongTermMemory.kt @@ -0,0 +1,89 @@ +package ai.platon.pulsar.agentic.ai.memory + +import java.util.concurrent.ConcurrentHashMap + +/** + * Long-term memory implementation that stores important facts. + * Retains memories based on importance threshold and supports keyword-based retrieval. + * + * @property importanceThreshold Minimum importance score to retain a memory (0.0 to 1.0) + * @property maxSize Maximum number of memories to retain + */ +class LongTermMemory( + private val importanceThreshold: Double = 0.7, + private val maxSize: Int = 200 +) : AgentMemory { + private val memories = ConcurrentHashMap() + + init { + require(importanceThreshold in 0.0..1.0) { "importanceThreshold must be between 0.0 and 1.0" } + require(maxSize > 0) { "maxSize must be positive" } + } + + override fun add(content: String, importance: Double, metadata: Map) { + // Only store memories that meet the importance threshold + if (importance < importanceThreshold) { + return + } + + val memory = Memory(content, importance = importance, metadata = metadata) + + synchronized(memories) { + // Use content as key to avoid duplicates + val key = content.trim().lowercase() + memories[key] = memory + + // If exceeding max size, remove least important memories + if (memories.size > maxSize) { + val toRemove = memories.size - maxSize + memories.values + .sortedBy { it.importance } + .take(toRemove) + .forEach { mem -> + memories.remove(mem.content.trim().lowercase()) + } + } + } + } + + override fun retrieve(query: String?, limit: Int): List { + val allMemories = memories.values.toList() + + return if (query.isNullOrBlank()) { + // Return most important memories + allMemories + .sortedByDescending { it.importance } + .take(limit) + } else { + // Rank by relevance and importance + val queryTerms = query.lowercase().split("\\s+".toRegex()) + allMemories + .map { memory -> + val content = memory.content.lowercase() + val matchCount = queryTerms.count { term -> content.contains(term) } + val relevanceScore = (matchCount.toDouble() / queryTerms.size) * memory.importance + memory to relevanceScore + } + .filter { (_, score) -> score > 0 } + .sortedByDescending { (_, score) -> score } + .take(limit) + .map { (memory, _) -> memory } + } + } + + override fun getAll(): List { + return memories.values + .sortedByDescending { it.importance } + .toList() + } + + override fun clear() { + synchronized(memories) { + memories.clear() + } + } + + override fun size(): Int { + return memories.size + } +} diff --git a/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/ShortTermMemory.kt b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/ShortTermMemory.kt new file mode 100644 index 000000000..725829bea --- /dev/null +++ b/pulsar-core/pulsar-agentic/src/main/kotlin/ai/platon/pulsar/agentic/ai/memory/ShortTermMemory.kt @@ -0,0 +1,65 @@ +package ai.platon.pulsar.agentic.ai.memory + +import java.util.concurrent.ConcurrentLinkedDeque + +/** + * Short-term memory implementation using a sliding window approach. + * Maintains recent memories with automatic cleanup of older entries. + * + * @property maxSize Maximum number of memories to retain + */ +class ShortTermMemory( + private val maxSize: Int = 50 +) : AgentMemory { + private val memories = ConcurrentLinkedDeque() + + init { + require(maxSize > 0) { "maxSize must be positive" } + } + + override fun add(content: String, importance: Double, metadata: Map) { + val memory = Memory(content, importance = importance, metadata = metadata) + + synchronized(memories) { + memories.addLast(memory) + + // Remove oldest if exceeding max size + while (memories.size > maxSize) { + memories.removeFirst() + } + } + } + + override fun retrieve(query: String?, limit: Int): List { + val allMemories = memories.toList() + + return if (query.isNullOrBlank()) { + // Return most recent memories + allMemories.takeLast(limit) + } else { + // Simple relevance: check if memory contains query terms + val queryTerms = query.lowercase().split("\\s+".toRegex()) + allMemories + .filter { memory -> + val content = memory.content.lowercase() + queryTerms.any { term -> content.contains(term) } + } + .sortedByDescending { it.importance } + .take(limit) + } + } + + override fun getAll(): List { + return memories.toList() + } + + override fun clear() { + synchronized(memories) { + memories.clear() + } + } + + override fun size(): Int { + return memories.size + } +} diff --git a/pulsar-core/pulsar-agentic/src/test/kotlin/ai/platon/pulsar/agentic/ai/memory/CompositeMemoryTest.kt b/pulsar-core/pulsar-agentic/src/test/kotlin/ai/platon/pulsar/agentic/ai/memory/CompositeMemoryTest.kt new file mode 100644 index 000000000..e8c99c9da --- /dev/null +++ b/pulsar-core/pulsar-agentic/src/test/kotlin/ai/platon/pulsar/agentic/ai/memory/CompositeMemoryTest.kt @@ -0,0 +1,146 @@ +package ai.platon.pulsar.agentic.ai.memory + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class CompositeMemoryTest { + + private lateinit var memory: CompositeMemory + + @BeforeEach + fun setup() { + memory = CompositeMemory( + shortTerm = ShortTermMemory(maxSize = 5), + longTerm = LongTermMemory(importanceThreshold = 0.7, maxSize = 10) + ) + } + + @Test + fun `Given high importance memory When add called Then should be in both stores`() { + memory.add("Important fact", 0.9) + + assertEquals(1, memory.shortTerm.size()) + assertEquals(1, memory.longTerm.size()) + } + + @Test + fun `Given low importance memory When add called Then should only be in short term`() { + memory.add("Minor detail", 0.5) + + assertEquals(1, memory.shortTerm.size()) + assertEquals(0, memory.longTerm.size()) + } + + @Test + fun `Given memories in both stores When retrieve called Then should merge and deduplicate`() { + memory.add("Shared memory", 0.8) + memory.add("Short term only", 0.5) + + val results = memory.retrieve(limit = 10) + + // Should have 2 unique memories + assertEquals(2, results.size) + assertTrue(results.any { it.content == "Shared memory" }) + assertTrue(results.any { it.content == "Short term only" }) + } + + @Test + fun `Given memories When getMemoryContext called Then should format for prompts`() { + memory.add("Visited product page", 0.8, mapOf("action" to "navigate")) + memory.add("Added item to cart", 0.9, mapOf("action" to "click")) + + val context = memory.getMemoryContext(limit = 10) + + assertTrue(context.contains("## 记忆")) + assertTrue(context.contains("Visited product page")) + assertTrue(context.contains("Added item to cart")) + } + + @Test + fun `Given empty memory When getMemoryContext called Then should return empty string`() { + val context = memory.getMemoryContext(limit = 10) + + assertEquals("", context) + } + + @Test + fun `Given memories When getMemoryContext with query Then should filter relevant memories`() { + memory.add("Search for products", 0.8) + memory.add("Navigate to cart", 0.7) + memory.add("Checkout process started", 0.9) + + val context = memory.getMemoryContext("cart", limit = 10) + + assertTrue(context.contains("Navigate to cart")) + assertFalse(context.contains("Search for products")) + } + + @Test + fun `Given memories When getAll called Then should return unique memories from both stores`() { + memory.add("Memory 1", 0.5) // Short term only + memory.add("Memory 2", 0.8) // Both stores + memory.add("Memory 3", 0.9) // Both stores + + val all = memory.getAll() + + assertEquals(3, all.size) + } + + @Test + fun `Given memories When clear called Then both stores should be cleared`() { + memory.add("Memory 1", 0.5) + memory.add("Memory 2", 0.8) + + assertTrue(memory.size() > 0) + + memory.clear() + + assertEquals(0, memory.size()) + assertEquals(0, memory.shortTerm.size()) + assertEquals(0, memory.longTerm.size()) + } + + @Test + fun `Given memories When size called Then should return unique count`() { + memory.add("Shared memory", 0.8) + memory.add("Another memory", 0.9) + + // Both are in both stores but should count as 2 unique + assertEquals(2, memory.size()) + } + + @Test + fun `Given memories When createSnapshot called Then should capture all memories`() { + memory.add("Short term only", 0.5) + memory.add("Both stores", 0.8) + memory.add("Another important", 0.9) + + val snapshot = memory.createSnapshot() + + assertEquals(3, snapshot.shortTermMemories.size) + assertEquals(2, snapshot.longTermMemories.size) + } + + @Test + fun `Given snapshot When restoreFromSnapshot called Then memories should be restored`() { + // Add initial memories + memory.add("Memory 1", 0.8) + memory.add("Memory 2", 0.9) + + // Create snapshot + val snapshot = memory.createSnapshot() + + // Clear memory + memory.clear() + assertEquals(0, memory.size()) + + // Restore from snapshot + memory.restoreFromSnapshot(snapshot) + + assertEquals(2, memory.size()) + val restored = memory.getAll() + assertTrue(restored.any { it.content == "Memory 1" }) + assertTrue(restored.any { it.content == "Memory 2" }) + } +} diff --git a/pulsar-core/pulsar-agentic/src/test/kotlin/ai/platon/pulsar/agentic/ai/memory/LongTermMemoryTest.kt b/pulsar-core/pulsar-agentic/src/test/kotlin/ai/platon/pulsar/agentic/ai/memory/LongTermMemoryTest.kt new file mode 100644 index 000000000..13cf22908 --- /dev/null +++ b/pulsar-core/pulsar-agentic/src/test/kotlin/ai/platon/pulsar/agentic/ai/memory/LongTermMemoryTest.kt @@ -0,0 +1,101 @@ +package ai.platon.pulsar.agentic.ai.memory + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class LongTermMemoryTest { + + private lateinit var memory: LongTermMemory + + @BeforeEach + fun setup() { + memory = LongTermMemory(importanceThreshold = 0.7, maxSize = 10) + } + + @Test + fun `Given memory above threshold When add called Then memory should be stored`() { + memory.add("Important memory", 0.8) + + assertEquals(1, memory.size()) + val memories = memory.getAll() + assertEquals("Important memory", memories[0].content) + } + + @Test + fun `Given memory below threshold When add called Then memory should not be stored`() { + memory.add("Unimportant memory", 0.5) + + assertEquals(0, memory.size()) + } + + @Test + fun `Given duplicate content When add called Then should update existing memory`() { + memory.add("Important fact", 0.8) + memory.add("Important fact", 0.9) + + assertEquals(1, memory.size()) + val memories = memory.getAll() + assertEquals(0.9, memories[0].importance, 0.01) + } + + @Test + fun `Given memory at max size When add called Then least important should be removed`() { + // Fill with high importance memories + repeat(10) { i -> + memory.add("Memory $i", 0.7 + (i * 0.01)) + } + + assertEquals(10, memory.size()) + + // Add one more with very high importance + memory.add("Very important memory", 1.0) + + // Still at max size + assertEquals(10, memory.size()) + + // Least important should be removed (Memory 0 with 0.7) + val memories = memory.getAll() + assertFalse(memories.any { it.content == "Memory 0" }) + assertTrue(memories.any { it.content == "Very important memory" }) + } + + @Test + fun `Given memories When retrieve with query Then should rank by relevance and importance`() { + memory.add("Search for products on Amazon", 0.9) + memory.add("Search results page loaded", 0.7) + memory.add("Click the add to cart button", 0.8) + + val results = memory.retrieve("search", limit = 10) + + assertEquals(2, results.size) + // Higher importance should come first when relevance is equal + assertEquals("Search for products on Amazon", results[0].content) + } + + @Test + fun `Given memories When retrieve without query Then should return by importance`() { + memory.add("Low priority task", 0.7) + memory.add("High priority task", 0.95) + memory.add("Medium priority task", 0.8) + + val results = memory.retrieve(null, limit = 3) + + assertEquals(3, results.size) + assertEquals("High priority task", results[0].content) + assertEquals("Medium priority task", results[1].content) + assertEquals("Low priority task", results[2].content) + } + + @Test + fun `Given memories When clear called Then all memories should be removed`() { + memory.add("Memory 1", 0.8) + memory.add("Memory 2", 0.9) + + assertEquals(2, memory.size()) + + memory.clear() + + assertEquals(0, memory.size()) + } +} diff --git a/pulsar-core/pulsar-agentic/src/test/kotlin/ai/platon/pulsar/agentic/ai/memory/ShortTermMemoryTest.kt b/pulsar-core/pulsar-agentic/src/test/kotlin/ai/platon/pulsar/agentic/ai/memory/ShortTermMemoryTest.kt new file mode 100644 index 000000000..9cea9784c --- /dev/null +++ b/pulsar-core/pulsar-agentic/src/test/kotlin/ai/platon/pulsar/agentic/ai/memory/ShortTermMemoryTest.kt @@ -0,0 +1,93 @@ +package ai.platon.pulsar.agentic.ai.memory + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertContains + +class ShortTermMemoryTest { + + private lateinit var memory: ShortTermMemory + + @BeforeEach + fun setup() { + memory = ShortTermMemory(maxSize = 5) + } + + @Test + fun `Given empty memory When add called Then memory should be stored`() { + memory.add("Test memory", 0.7) + + assertEquals(1, memory.size()) + val memories = memory.getAll() + assertEquals("Test memory", memories[0].content) + assertEquals(0.7, memories[0].importance, 0.01) + } + + @Test + fun `Given memory at max size When add called Then oldest should be removed`() { + // Fill to capacity + repeat(5) { i -> + memory.add("Memory $i", 0.5) + } + + assertEquals(5, memory.size()) + + // Add one more + memory.add("Memory 5", 0.5) + + // Still at max size + assertEquals(5, memory.size()) + + // Oldest should be removed + val memories = memory.getAll() + assertFalse(memories.any { it.content == "Memory 0" }) + assertTrue(memories.any { it.content == "Memory 5" }) + } + + @Test + fun `Given memories When retrieve with query Then should return matching memories`() { + memory.add("Search for products on Amazon", 0.8) + memory.add("Click the add to cart button", 0.6) + memory.add("Navigate to checkout page", 0.7) + + val results = memory.retrieve("cart", limit = 10) + + assertEquals(1, results.size) + assertContains(results[0].content, "cart") + } + + @Test + fun `Given memories When retrieve without query Then should return recent memories`() { + repeat(10) { i -> + memory.add("Memory $i", 0.5) + } + + val results = memory.retrieve(null, limit = 3) + + assertEquals(3, results.size) + // Should get the most recent (last 3 added, but size is 5, so it's 5-9) + assertTrue(results.any { it.content.contains("7") || it.content.contains("8") || it.content.contains("9") }) + } + + @Test + fun `Given memories When clear called Then all memories should be removed`() { + memory.add("Memory 1", 0.5) + memory.add("Memory 2", 0.5) + + assertEquals(2, memory.size()) + + memory.clear() + + assertEquals(0, memory.size()) + } + + @Test + fun `Given memories with metadata When add called Then metadata should be stored`() { + val metadata = mapOf("action" to "click", "locator" to "0,5") + memory.add("Clicked on button", 0.8, metadata) + + val memories = memory.getAll() + assertEquals(metadata, memories[0].metadata) + } +} diff --git a/pulsar-core/pulsar-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/ai/PerceptiveAgent.kt b/pulsar-core/pulsar-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/ai/PerceptiveAgent.kt index ad9a2cd14..ad9e51ea4 100644 --- a/pulsar-core/pulsar-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/ai/PerceptiveAgent.kt +++ b/pulsar-core/pulsar-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/ai/PerceptiveAgent.kt @@ -160,6 +160,7 @@ data class ObserveElement constructor( val currentPageContentSummary: String? = null, val evaluationPreviousGoal: String? = null, val nextGoal: String? = null, + val memory: String? = null, val modelResponse: String? = null,