Compare commits
No commits in common. "9d9fa1dc4b2b22b07e592ee8972298970795fec2" and "03c95e6b8fbb2387edea2aec01eaa8c3560661b3" have entirely different histories.
9d9fa1dc4b
...
03c95e6b8f
@ -1128,7 +1128,6 @@ mod tests {
|
|||||||
api_key: "test-key".to_string(),
|
api_key: "test-key".to_string(),
|
||||||
extra_headers: std::collections::HashMap::new(),
|
extra_headers: std::collections::HashMap::new(),
|
||||||
llm_timeout_secs: 120,
|
llm_timeout_secs: 120,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
model_id: "test-model".to_string(),
|
model_id: "test-model".to_string(),
|
||||||
temperature: Some(0.0),
|
temperature: Some(0.0),
|
||||||
max_tokens: Some(32),
|
max_tokens: Some(32),
|
||||||
|
|||||||
@ -248,8 +248,6 @@ pub struct ProviderConfig {
|
|||||||
pub extra_headers: HashMap<String, String>,
|
pub extra_headers: HashMap<String, String>,
|
||||||
#[serde(default = "default_llm_timeout_secs")]
|
#[serde(default = "default_llm_timeout_secs")]
|
||||||
pub llm_timeout_secs: u64,
|
pub llm_timeout_secs: u64,
|
||||||
#[serde(default = "default_memory_maintenance_timeout_secs")]
|
|
||||||
pub memory_maintenance_timeout_secs: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
@ -293,10 +291,6 @@ fn default_llm_timeout_secs() -> u64 {
|
|||||||
120
|
120
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_memory_maintenance_timeout_secs() -> u64 {
|
|
||||||
600
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct GatewayConfig {
|
pub struct GatewayConfig {
|
||||||
#[serde(default = "default_gateway_host")]
|
#[serde(default = "default_gateway_host")]
|
||||||
@ -629,7 +623,6 @@ pub struct LLMProviderConfig {
|
|||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
pub extra_headers: HashMap<String, String>,
|
pub extra_headers: HashMap<String, String>,
|
||||||
pub llm_timeout_secs: u64,
|
pub llm_timeout_secs: u64,
|
||||||
pub memory_maintenance_timeout_secs: u64,
|
|
||||||
pub model_id: String,
|
pub model_id: String,
|
||||||
pub temperature: Option<f32>,
|
pub temperature: Option<f32>,
|
||||||
pub max_tokens: Option<u32>,
|
pub max_tokens: Option<u32>,
|
||||||
@ -719,7 +712,6 @@ impl Config {
|
|||||||
api_key: provider.api_key.clone(),
|
api_key: provider.api_key.clone(),
|
||||||
extra_headers: provider.extra_headers.clone(),
|
extra_headers: provider.extra_headers.clone(),
|
||||||
llm_timeout_secs: provider.llm_timeout_secs,
|
llm_timeout_secs: provider.llm_timeout_secs,
|
||||||
memory_maintenance_timeout_secs: provider.memory_maintenance_timeout_secs,
|
|
||||||
model_id: model.model_id.clone(),
|
model_id: model.model_id.clone(),
|
||||||
temperature: model.temperature,
|
temperature: model.temperature,
|
||||||
max_tokens: model.max_tokens,
|
max_tokens: model.max_tokens,
|
||||||
|
|||||||
@ -61,7 +61,7 @@ impl SchedulerMaintenanceService {
|
|||||||
self.session_manager.cleanup_expired_sessions().await
|
self.session_manager.cleanup_expired_sessions().await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_memory_maintenance(&self) -> Result<Option<MemoryMaintenanceScopeResult>, AgentError> {
|
async fn run_memory_maintenance(&self) -> Result<Vec<MemoryMaintenanceScopeResult>, AgentError> {
|
||||||
self.session_manager.run_memory_maintenance_for_all_scopes().await
|
self.session_manager.run_memory_maintenance_for_all_scopes().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,6 @@ mod tests {
|
|||||||
api_key: "test-key".to_string(),
|
api_key: "test-key".to_string(),
|
||||||
extra_headers: HashMap::new(),
|
extra_headers: HashMap::new(),
|
||||||
llm_timeout_secs: 120,
|
llm_timeout_secs: 120,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
model_id: "test-model".to_string(),
|
model_id: "test-model".to_string(),
|
||||||
temperature: Some(0.0),
|
temperature: Some(0.0),
|
||||||
max_tokens: Some(32),
|
max_tokens: Some(32),
|
||||||
|
|||||||
@ -4,9 +4,9 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::agent::AgentError;
|
use crate::agent::{AgentError, AgentRuntimeConfig};
|
||||||
use crate::config::LLMProviderConfig;
|
use crate::config::LLMProviderConfig;
|
||||||
use crate::providers::{ChatCompletionRequest, Message, ProviderRuntimeConfig, create_provider};
|
use crate::providers::{ChatCompletionRequest, Message, create_provider};
|
||||||
use crate::storage::{MemoryRecord, SessionStore};
|
use crate::storage::{MemoryRecord, SessionStore};
|
||||||
|
|
||||||
use super::prompt::upsert_managed_agent_memory_summary;
|
use super::prompt::upsert_managed_agent_memory_summary;
|
||||||
@ -17,6 +17,14 @@ const MEMORY_MAINTENANCE_STEP2_SYSTEM_PROMPT: &str =
|
|||||||
include_str!("memory_maintenance_step2_system_prompt.md");
|
include_str!("memory_maintenance_step2_system_prompt.md");
|
||||||
const MEMORY_MAINTENANCE_RETRY_DELAYS_MS: &[u64] = &[1_000, 3_000];
|
const MEMORY_MAINTENANCE_RETRY_DELAYS_MS: &[u64] = &[1_000, 3_000];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum MemoryMaintenanceCategory {
|
||||||
|
UserFacts,
|
||||||
|
Preferences,
|
||||||
|
BehaviorPatterns,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub(crate) struct MemoryMaintenanceCandidate {
|
pub(crate) struct MemoryMaintenanceCandidate {
|
||||||
pub(crate) id: String,
|
pub(crate) id: String,
|
||||||
@ -27,7 +35,10 @@ pub(crate) struct MemoryMaintenanceCandidate {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub(crate) struct MemoryMaintenancePlan {
|
pub(crate) struct MemoryMaintenancePlan {
|
||||||
pub(crate) candidates: Vec<MemoryMaintenanceCandidate>,
|
pub(crate) user_facts: Vec<MemoryMaintenanceCandidate>,
|
||||||
|
pub(crate) preferences: Vec<MemoryMaintenanceCandidate>,
|
||||||
|
pub(crate) behavior_patterns: Vec<MemoryMaintenanceCandidate>,
|
||||||
|
pub(crate) others: Vec<MemoryMaintenanceCandidate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@ -53,6 +64,7 @@ pub(crate) struct MemoryOrganizationOutput {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub(crate) struct MemorySummaryInput {
|
pub(crate) struct MemorySummaryInput {
|
||||||
|
pub(crate) scope_key: String,
|
||||||
pub(crate) organized_memories: Vec<OrganizedMemory>,
|
pub(crate) organized_memories: Vec<OrganizedMemory>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,26 +95,6 @@ impl MemoryMaintenanceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 创建记忆整理专用的 provider,使用 memory_maintenance_timeout_secs 作为超时时间
|
|
||||||
fn create_maintenance_provider(
|
|
||||||
&self,
|
|
||||||
) -> Result<Box<dyn crate::providers::LLMProvider>, crate::providers::ProviderError> {
|
|
||||||
let config = &self.provider_config;
|
|
||||||
let runtime_config = ProviderRuntimeConfig {
|
|
||||||
provider_type: config.provider_type.clone(),
|
|
||||||
name: config.name.clone(),
|
|
||||||
base_url: config.base_url.clone(),
|
|
||||||
api_key: config.api_key.clone(),
|
|
||||||
extra_headers: config.extra_headers.clone(),
|
|
||||||
llm_timeout_secs: config.memory_maintenance_timeout_secs,
|
|
||||||
model_id: config.model_id.clone(),
|
|
||||||
temperature: config.temperature,
|
|
||||||
max_tokens: config.max_tokens,
|
|
||||||
model_extra: config.model_extra.clone(),
|
|
||||||
};
|
|
||||||
create_provider(runtime_config)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build_plan_for_scope(
|
pub(crate) fn build_plan_for_scope(
|
||||||
&self,
|
&self,
|
||||||
scope_key: &str,
|
scope_key: &str,
|
||||||
@ -135,7 +127,8 @@ impl MemoryMaintenanceService {
|
|||||||
scope_key: &str,
|
scope_key: &str,
|
||||||
plan: &MemoryMaintenancePlan,
|
plan: &MemoryMaintenancePlan,
|
||||||
) -> Result<MemoryOrganizationOutput, AgentError> {
|
) -> Result<MemoryOrganizationOutput, AgentError> {
|
||||||
let provider = self.create_maintenance_provider().map_err(|err| {
|
let runtime_config = AgentRuntimeConfig::from(self.provider_config.clone());
|
||||||
|
let provider = create_provider(runtime_config.provider).map_err(|err| {
|
||||||
AgentError::Other(format!("create maintenance provider error: {}", err))
|
AgentError::Other(format!("create maintenance provider error: {}", err))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@ -224,7 +217,6 @@ impl MemoryMaintenanceService {
|
|||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(not(test), allow(dead_code))]
|
|
||||||
pub(crate) async fn run_for_scope(
|
pub(crate) async fn run_for_scope(
|
||||||
&self,
|
&self,
|
||||||
scope_key: &str,
|
scope_key: &str,
|
||||||
@ -266,11 +258,13 @@ impl MemoryMaintenanceService {
|
|||||||
scope_key: &str,
|
scope_key: &str,
|
||||||
remaining_memories: &[MemoryRecord],
|
remaining_memories: &[MemoryRecord],
|
||||||
) -> Result<String, AgentError> {
|
) -> Result<String, AgentError> {
|
||||||
let provider = self.create_maintenance_provider().map_err(|err| {
|
let runtime_config = AgentRuntimeConfig::from(self.provider_config.clone());
|
||||||
|
let provider = create_provider(runtime_config.provider).map_err(|err| {
|
||||||
AgentError::Other(format!("create maintenance provider error: {}", err))
|
AgentError::Other(format!("create maintenance provider error: {}", err))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let input = MemorySummaryInput {
|
let input = MemorySummaryInput {
|
||||||
|
scope_key: scope_key.to_string(),
|
||||||
organized_memories: remaining_memories
|
organized_memories: remaining_memories
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| OrganizedMemory {
|
.map(|m| OrganizedMemory {
|
||||||
@ -359,20 +353,15 @@ impl MemoryMaintenanceService {
|
|||||||
|
|
||||||
pub(crate) async fn run_for_all_scopes(
|
pub(crate) async fn run_for_all_scopes(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Option<MemoryMaintenanceScopeResult>, AgentError> {
|
) -> Result<Vec<MemoryMaintenanceScopeResult>, AgentError> {
|
||||||
let scope_keys = self.store.list_memory_scope_keys("user").map_err(|err| {
|
let scope_keys = self.store.list_memory_scope_keys("user").map_err(|err| {
|
||||||
AgentError::Other(format!("list memory scope keys error: {}", err))
|
AgentError::Other(format!("list memory scope keys error: {}", err))
|
||||||
})?;
|
})?;
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
if scope_keys.is_empty() {
|
for scope_key in scope_keys {
|
||||||
return Ok(None);
|
let result = match self.run_for_scope(&scope_key).await {
|
||||||
}
|
Ok(Some(result)) => result,
|
||||||
|
|
||||||
// 步骤1:逐个 scope 整理记忆(merge/delete),但不生成摘要
|
|
||||||
let mut all_outputs = Vec::new();
|
|
||||||
for scope_key in &scope_keys {
|
|
||||||
match self.run_organize_for_scope(scope_key).await {
|
|
||||||
Ok(Some(output)) => all_outputs.push((scope_key.clone(), output)),
|
|
||||||
Ok(None) => continue,
|
Ok(None) => continue,
|
||||||
Err(error) if is_recoverable_maintenance_scope_error(&error) => {
|
Err(error) if is_recoverable_maintenance_scope_error(&error) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@ -383,78 +372,23 @@ impl MemoryMaintenanceService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
results.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
if all_outputs.is_empty() {
|
let combined_markdown = combine_managed_memory_markdown(
|
||||||
return Ok(None);
|
&results
|
||||||
}
|
|
||||||
|
|
||||||
// 步骤2:收集所有 scope 整理后的剩余记忆
|
|
||||||
let mut all_remaining_memories = Vec::new();
|
|
||||||
for (scope_key, _) in &all_outputs {
|
|
||||||
let memories = self
|
|
||||||
.store
|
|
||||||
.list_memories_for_scope("user", scope_key)
|
|
||||||
.map_err(|err| {
|
|
||||||
AgentError::Other(format!(
|
|
||||||
"list remaining memories for scope {} error: {}",
|
|
||||||
scope_key, err
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
all_remaining_memories.extend(memories);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 步骤3:统一生成一个摘要
|
|
||||||
let managed_markdown = if all_remaining_memories.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
self.generate_summary("all", &all_remaining_memories).await?
|
|
||||||
};
|
|
||||||
|
|
||||||
if !managed_markdown.is_empty() {
|
|
||||||
upsert_managed_agent_memory_summary(&managed_markdown)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并所有输出用于返回
|
|
||||||
let combined_output = MemoryOrganizationOutput {
|
|
||||||
merges: all_outputs
|
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|(_, o)| o.merges.clone())
|
.map(|result| result.managed_markdown.clone())
|
||||||
.collect(),
|
.collect::<Vec<_>>(),
|
||||||
conflicts: all_outputs
|
);
|
||||||
.iter()
|
|
||||||
.flat_map(|(_, o)| o.conflicts.clone())
|
|
||||||
.collect(),
|
|
||||||
low_value_ids: all_outputs
|
|
||||||
.iter()
|
|
||||||
.flat_map(|(_, o)| o.low_value_ids.clone())
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(MemoryMaintenanceScopeResult {
|
if !combined_markdown.is_empty() {
|
||||||
scope_key: "all".to_string(),
|
upsert_managed_agent_memory_summary(&combined_markdown)?;
|
||||||
output: combined_output,
|
}
|
||||||
managed_markdown,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 仅执行整理步骤(organize + apply),不生成摘要
|
Ok(results)
|
||||||
async fn run_organize_for_scope(
|
|
||||||
&self,
|
|
||||||
scope_key: &str,
|
|
||||||
) -> Result<Option<MemoryOrganizationOutput>, AgentError> {
|
|
||||||
let Some(plan) = self.build_plan_for_scope(scope_key)? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 步骤1:整理记忆
|
|
||||||
let organize_output = self.organize_plan(scope_key, &plan).await?;
|
|
||||||
|
|
||||||
// 应用整理结果(merge和delete)
|
|
||||||
apply_memory_maintenance_output(self.store.as_ref(), scope_key, &plan, &organize_output)?;
|
|
||||||
|
|
||||||
Ok(Some(organize_output))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,12 +419,28 @@ pub(crate) fn build_memory_maintenance_plan(memories: &[MemoryRecord]) -> Memory
|
|||||||
content: normalized_content.to_string(),
|
content: normalized_content.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
plan.candidates.push(candidate);
|
match memory_maintenance_category(&memory.namespace) {
|
||||||
|
MemoryMaintenanceCategory::UserFacts => plan.user_facts.push(candidate),
|
||||||
|
MemoryMaintenanceCategory::Preferences => plan.preferences.push(candidate),
|
||||||
|
MemoryMaintenanceCategory::BehaviorPatterns => plan.behavior_patterns.push(candidate),
|
||||||
|
MemoryMaintenanceCategory::Other => plan.others.push(candidate),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plan
|
plan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn memory_maintenance_category(namespace: &str) -> MemoryMaintenanceCategory {
|
||||||
|
match namespace.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"profile" | "facts" | "identity" => MemoryMaintenanceCategory::UserFacts,
|
||||||
|
"preferences" | "style" | "likes" => MemoryMaintenanceCategory::Preferences,
|
||||||
|
"patterns" | "behavior" | "habits" | "workflow" => {
|
||||||
|
MemoryMaintenanceCategory::BehaviorPatterns
|
||||||
|
}
|
||||||
|
_ => MemoryMaintenanceCategory::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn is_recoverable_maintenance_llm_error(error: &str) -> bool {
|
pub(crate) fn is_recoverable_maintenance_llm_error(error: &str) -> bool {
|
||||||
let normalized = error.to_ascii_lowercase();
|
let normalized = error.to_ascii_lowercase();
|
||||||
normalized.contains("error sending request for url")
|
normalized.contains("error sending request for url")
|
||||||
@ -562,13 +512,61 @@ pub(crate) fn extract_json_object(content: &str) -> Option<&str> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn combine_managed_memory_markdown(chunks: &[String]) -> String {
|
||||||
|
let normalized_chunks = chunks
|
||||||
|
.iter()
|
||||||
|
.map(|chunk| chunk.trim())
|
||||||
|
.filter(|chunk| !chunk.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut combined = Vec::new();
|
||||||
|
for (index, chunk) in normalized_chunks.iter().enumerate() {
|
||||||
|
let chunk_lines = chunk
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let is_subset_of_other =
|
||||||
|
normalized_chunks
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.any(|(other_index, other)| {
|
||||||
|
if index == other_index {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let other_lines = other
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
chunk_lines.len() < other_lines.len() && chunk_lines.is_subset(&other_lines)
|
||||||
|
});
|
||||||
|
|
||||||
|
if !is_subset_of_other && !combined.iter().any(|existing: &String| existing == chunk) {
|
||||||
|
combined.push((*chunk).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combined.join("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn apply_memory_maintenance_output(
|
pub(crate) fn apply_memory_maintenance_output(
|
||||||
store: &SessionStore,
|
store: &SessionStore,
|
||||||
scope_key: &str,
|
scope_key: &str,
|
||||||
plan: &MemoryMaintenancePlan,
|
plan: &MemoryMaintenancePlan,
|
||||||
output: &MemoryOrganizationOutput,
|
output: &MemoryOrganizationOutput,
|
||||||
) -> Result<(), AgentError> {
|
) -> Result<(), AgentError> {
|
||||||
let all_candidates = plan.candidates.clone();
|
let all_candidates = plan
|
||||||
|
.user_facts
|
||||||
|
.iter()
|
||||||
|
.chain(plan.preferences.iter())
|
||||||
|
.chain(plan.behavior_patterns.iter())
|
||||||
|
.chain(plan.others.iter())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let candidates_by_id = all_candidates
|
let candidates_by_id = all_candidates
|
||||||
.iter()
|
.iter()
|
||||||
@ -653,6 +651,3 @@ fn preview_text(content: &str, max_chars: usize) -> String {
|
|||||||
}
|
}
|
||||||
preview.replace('\n', "\\n")
|
preview.replace('\n', "\\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {}
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ impl MemoryMaintenanceCoordinator {
|
|||||||
|
|
||||||
pub(crate) async fn run_for_all_scopes(
|
pub(crate) async fn run_for_all_scopes(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Option<MemoryMaintenanceScopeResult>, AgentError> {
|
) -> Result<Vec<MemoryMaintenanceScopeResult>, AgentError> {
|
||||||
self.service()?.run_for_all_scopes().await
|
self.service()?.run_for_all_scopes().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,22 +19,14 @@
|
|||||||
|
|
||||||
- merges:对象数组。每个对象必须包含 source_ids、namespace、memory_key、content。
|
- merges:对象数组。每个对象必须包含 source_ids、namespace、memory_key、content。
|
||||||
- source_ids: 字符串数组,要合并的源记忆ID列表
|
- source_ids: 字符串数组,要合并的源记忆ID列表
|
||||||
- namespace: 目标命名空间(可以自由决定,不限于固定分类)
|
- namespace: 目标命名空间
|
||||||
- memory_key: 目标记忆键(可以自由决定)
|
- memory_key: 目标记忆键
|
||||||
- content: 合并后的内容
|
- content: 合并后的内容
|
||||||
- conflicts:对象数组。每个对象必须包含 source_ids、note。
|
- conflicts:对象数组。每个对象必须包含 source_ids、note。
|
||||||
- source_ids: 冲突的记忆ID列表
|
- source_ids: 冲突的记忆ID列表
|
||||||
- note: 冲突说明
|
- note: 冲突说明
|
||||||
- low_value_ids:需要删除的低价值候选记忆 ID 数组
|
- low_value_ids:需要删除的低价值候选记忆 ID 数组
|
||||||
|
|
||||||
组织原则(由你自主决定):
|
|
||||||
|
|
||||||
- 根据记忆的语义内容自然分组,不必拘泥于预定义分类
|
|
||||||
- 相似的、互补的记忆可以合并
|
|
||||||
- 过期、重复、过细的记忆可以标记为低价值
|
|
||||||
- namespace 和 memory_key 的命名应当简洁、有意义
|
|
||||||
- 可以自由创建新的 namespace 来组织相关记忆
|
|
||||||
|
|
||||||
额外约束:
|
额外约束:
|
||||||
|
|
||||||
- 只能引用输入里出现过的候选 id。
|
- 只能引用输入里出现过的候选 id。
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
你的任务是基于整理后的用户记忆生成结构化的 Markdown 摘要。
|
你的任务是基于整理后的用户记忆生成结构化的 Markdown 摘要。
|
||||||
|
|
||||||
输入格式:
|
输入格式:
|
||||||
|
- scope_key: 用户的唯一标识
|
||||||
- organized_memories: 整理后的记忆列表,每个包含 namespace、memory_key、content
|
- organized_memories: 整理后的记忆列表,每个包含 namespace、memory_key、content
|
||||||
|
|
||||||
输出要求:
|
输出要求:
|
||||||
|
|||||||
@ -51,7 +51,6 @@ mod tests {
|
|||||||
api_key: "test-key".to_string(),
|
api_key: "test-key".to_string(),
|
||||||
extra_headers: HashMap::new(),
|
extra_headers: HashMap::new(),
|
||||||
llm_timeout_secs: 120,
|
llm_timeout_secs: 120,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
model_id: model_id.to_string(),
|
model_id: model_id.to_string(),
|
||||||
temperature: Some(0.0),
|
temperature: Some(0.0),
|
||||||
max_tokens: Some(32),
|
max_tokens: Some(32),
|
||||||
|
|||||||
@ -20,7 +20,7 @@ use super::execution::should_display_message_to_user;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use super::memory_maintenance::{
|
use super::memory_maintenance::{
|
||||||
MemoryMaintenanceMerge, apply_memory_maintenance_output, build_memory_maintenance_plan,
|
MemoryMaintenanceMerge, apply_memory_maintenance_output, build_memory_maintenance_plan,
|
||||||
extract_json_object, is_recoverable_maintenance_llm_error,
|
combine_managed_memory_markdown, extract_json_object, is_recoverable_maintenance_llm_error,
|
||||||
strip_json_code_fence,
|
strip_json_code_fence,
|
||||||
};
|
};
|
||||||
use super::memory_maintenance::{MemoryMaintenanceScopeResult, MemoryOrganizationOutput};
|
use super::memory_maintenance::{MemoryMaintenanceScopeResult, MemoryOrganizationOutput};
|
||||||
@ -427,7 +427,7 @@ impl SessionManager {
|
|||||||
|
|
||||||
pub(crate) async fn run_memory_maintenance_for_all_scopes(
|
pub(crate) async fn run_memory_maintenance_for_all_scopes(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Option<MemoryMaintenanceScopeResult>, AgentError> {
|
) -> Result<Vec<MemoryMaintenanceScopeResult>, AgentError> {
|
||||||
self.memory_maintenance.run_for_all_scopes().await
|
self.memory_maintenance.run_for_all_scopes().await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,7 +520,6 @@ mod tests {
|
|||||||
api_key: "test-key".to_string(),
|
api_key: "test-key".to_string(),
|
||||||
extra_headers: HashMap::new(),
|
extra_headers: HashMap::new(),
|
||||||
llm_timeout_secs: 120,
|
llm_timeout_secs: 120,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
model_id: "test-model".to_string(),
|
model_id: "test-model".to_string(),
|
||||||
temperature: Some(0.0),
|
temperature: Some(0.0),
|
||||||
max_tokens: Some(32),
|
max_tokens: Some(32),
|
||||||
@ -787,7 +786,6 @@ mod tests {
|
|||||||
model_extra: HashMap::new(),
|
model_extra: HashMap::new(),
|
||||||
max_tool_iterations: 1,
|
max_tool_iterations: 1,
|
||||||
llm_timeout_secs: 30,
|
llm_timeout_secs: 30,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
tool_result_max_chars: 20_000,
|
tool_result_max_chars: 20_000,
|
||||||
context_tool_result_trim_chars: 20_000,
|
context_tool_result_trim_chars: 20_000,
|
||||||
};
|
};
|
||||||
@ -829,7 +827,6 @@ mod tests {
|
|||||||
model_extra: HashMap::new(),
|
model_extra: HashMap::new(),
|
||||||
max_tool_iterations: 1,
|
max_tool_iterations: 1,
|
||||||
llm_timeout_secs: 30,
|
llm_timeout_secs: 30,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
tool_result_max_chars: 20_000,
|
tool_result_max_chars: 20_000,
|
||||||
context_tool_result_trim_chars: 20_000,
|
context_tool_result_trim_chars: 20_000,
|
||||||
};
|
};
|
||||||
@ -903,7 +900,6 @@ mod tests {
|
|||||||
model_extra: HashMap::new(),
|
model_extra: HashMap::new(),
|
||||||
max_tool_iterations: 1,
|
max_tool_iterations: 1,
|
||||||
llm_timeout_secs: 30,
|
llm_timeout_secs: 30,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
tool_result_max_chars: 20_000,
|
tool_result_max_chars: 20_000,
|
||||||
context_tool_result_trim_chars: 20_000,
|
context_tool_result_trim_chars: 20_000,
|
||||||
};
|
};
|
||||||
@ -986,7 +982,6 @@ mod tests {
|
|||||||
)]),
|
)]),
|
||||||
max_tool_iterations: 1,
|
max_tool_iterations: 1,
|
||||||
llm_timeout_secs: 30,
|
llm_timeout_secs: 30,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
tool_result_max_chars: 20_000,
|
tool_result_max_chars: 20_000,
|
||||||
context_tool_result_trim_chars: 20_000,
|
context_tool_result_trim_chars: 20_000,
|
||||||
};
|
};
|
||||||
@ -1070,7 +1065,6 @@ mod tests {
|
|||||||
model_extra: HashMap::new(),
|
model_extra: HashMap::new(),
|
||||||
max_tool_iterations: 1,
|
max_tool_iterations: 1,
|
||||||
llm_timeout_secs: 1,
|
llm_timeout_secs: 1,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
tool_result_max_chars: 20_000,
|
tool_result_max_chars: 20_000,
|
||||||
context_tool_result_trim_chars: 20_000,
|
context_tool_result_trim_chars: 20_000,
|
||||||
};
|
};
|
||||||
@ -1119,7 +1113,7 @@ mod tests {
|
|||||||
assert!(error.contains("provider=maintenance-provider"));
|
assert!(error.contains("provider=maintenance-provider"));
|
||||||
assert!(error.contains("model=maintenance-model"));
|
assert!(error.contains("model=maintenance-model"));
|
||||||
assert!(error.contains("url=https://example.invalid/v1/chat/completions"));
|
assert!(error.contains("url=https://example.invalid/v1/chat/completions"));
|
||||||
assert!(error.contains("timeout_secs=600"));
|
assert!(error.contains("timeout_secs=1"));
|
||||||
assert!(error.contains("error sending request for url"));
|
assert!(error.contains("error sending request for url"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1153,7 +1147,6 @@ mod tests {
|
|||||||
)]),
|
)]),
|
||||||
max_tool_iterations: 1,
|
max_tool_iterations: 1,
|
||||||
llm_timeout_secs: 30,
|
llm_timeout_secs: 30,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
tool_result_max_chars: 20_000,
|
tool_result_max_chars: 20_000,
|
||||||
context_tool_result_trim_chars: 20_000,
|
context_tool_result_trim_chars: 20_000,
|
||||||
};
|
};
|
||||||
@ -1218,7 +1211,6 @@ mod tests {
|
|||||||
)]),
|
)]),
|
||||||
max_tool_iterations: 1,
|
max_tool_iterations: 1,
|
||||||
llm_timeout_secs: 30,
|
llm_timeout_secs: 30,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
tool_result_max_chars: 20_000,
|
tool_result_max_chars: 20_000,
|
||||||
context_tool_result_trim_chars: 20_000,
|
context_tool_result_trim_chars: 20_000,
|
||||||
};
|
};
|
||||||
@ -1292,7 +1284,6 @@ mod tests {
|
|||||||
)]),
|
)]),
|
||||||
max_tool_iterations: 1,
|
max_tool_iterations: 1,
|
||||||
llm_timeout_secs: 30,
|
llm_timeout_secs: 30,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
tool_result_max_chars: 20_000,
|
tool_result_max_chars: 20_000,
|
||||||
context_tool_result_trim_chars: 20_000,
|
context_tool_result_trim_chars: 20_000,
|
||||||
};
|
};
|
||||||
@ -1326,16 +1317,15 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = session_manager
|
let results = session_manager
|
||||||
.run_memory_maintenance_for_all_scopes()
|
.run_memory_maintenance_for_all_scopes()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(result.is_some());
|
assert_eq!(results.len(), 1);
|
||||||
let result = result.unwrap();
|
assert_eq!(results[0].scope_key, "feishu:user-1");
|
||||||
assert_eq!(result.scope_key, "all");
|
|
||||||
// 由于步骤2需要新的提示词和输入格式,这里只验证基本功能
|
// 由于步骤2需要新的提示词和输入格式,这里只验证基本功能
|
||||||
assert!(!result.managed_markdown.is_empty());
|
assert!(!results[0].managed_markdown.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@ -1353,7 +1343,6 @@ mod tests {
|
|||||||
model_extra: HashMap::new(),
|
model_extra: HashMap::new(),
|
||||||
max_tool_iterations: 1,
|
max_tool_iterations: 1,
|
||||||
llm_timeout_secs: 1,
|
llm_timeout_secs: 1,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
tool_result_max_chars: 20_000,
|
tool_result_max_chars: 20_000,
|
||||||
context_tool_result_trim_chars: 20_000,
|
context_tool_result_trim_chars: 20_000,
|
||||||
};
|
};
|
||||||
@ -1389,13 +1378,12 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = session_manager
|
let results = session_manager
|
||||||
.run_memory_maintenance_for_all_scopes()
|
.run_memory_maintenance_for_all_scopes()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// 当遇到可恢复错误时,没有整理任何记忆,返回 None
|
assert!(results.is_empty());
|
||||||
assert!(result.is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1472,6 +1460,21 @@ mod tests {
|
|||||||
assert_eq!(all_memories[0].content, "用户主要在做AI产品设计与实现");
|
assert_eq!(all_memories[0].content, "用户主要在做AI产品设计与实现");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_combine_managed_memory_markdown_prefers_richer_summary_over_subset() {
|
||||||
|
let combined = combine_managed_memory_markdown(&[
|
||||||
|
"### 用户事实\n- 用户在做AI产品\n\n### 用户偏好\n- 偏好简洁表达".to_string(),
|
||||||
|
"- 用户在做AI产品".to_string(),
|
||||||
|
"### 用户事实\n- 用户名为区德成,昵称DC。".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
combined.contains("### 用户事实\n- 用户在做AI产品\n\n### 用户偏好\n- 偏好简洁表达")
|
||||||
|
);
|
||||||
|
assert!(combined.contains("### 用户事实\n- 用户名为区德成,昵称DC。"));
|
||||||
|
assert_eq!(combined.matches("- 用户在做AI产品").count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_display_message_to_user_hides_completed_tool_results_by_default() {
|
fn test_should_display_message_to_user_hides_completed_tool_results_by_default() {
|
||||||
let completed = ChatMessage::tool("call-1", "calculator", "2");
|
let completed = ChatMessage::tool("call-1", "calculator", "2");
|
||||||
@ -1759,12 +1762,12 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let plan = build_memory_maintenance_plan(&memories);
|
let plan = build_memory_maintenance_plan(&memories);
|
||||||
// 去重后应该有3条(第1、2条重复)
|
assert_eq!(plan.user_facts.len(), 1);
|
||||||
assert_eq!(plan.candidates.len(), 3);
|
assert_eq!(plan.preferences.len(), 1);
|
||||||
// 验证内容包含所有唯一的记忆
|
assert_eq!(plan.behavior_patterns.len(), 1);
|
||||||
let contents: Vec<String> = plan.candidates.iter().map(|c| c.content.clone()).collect();
|
assert!(plan.others.is_empty());
|
||||||
assert!(contents.contains(&"用户在做AI产品".to_string()));
|
assert_eq!(plan.user_facts[0].content, "用户在做AI产品");
|
||||||
assert!(contents.contains(&"偏好简洁表达".to_string()));
|
assert_eq!(plan.preferences[0].content, "偏好简洁表达");
|
||||||
assert!(contents.contains(&"习惯先问方案再要代码".to_string()));
|
assert_eq!(plan.behavior_patterns[0].content, "习惯先问方案再要代码");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,6 @@ fn load_config() -> Option<LLMProviderConfig> {
|
|||||||
api_key: openai_api_key,
|
api_key: openai_api_key,
|
||||||
extra_headers: HashMap::new(),
|
extra_headers: HashMap::new(),
|
||||||
llm_timeout_secs: 120,
|
llm_timeout_secs: 120,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
model_id: openai_model,
|
model_id: openai_model,
|
||||||
temperature: Some(0.0),
|
temperature: Some(0.0),
|
||||||
max_tokens: Some(100),
|
max_tokens: Some(100),
|
||||||
|
|||||||
@ -37,7 +37,6 @@ fn load_openai_config() -> Option<LLMProviderConfig> {
|
|||||||
api_key: openai_api_key,
|
api_key: openai_api_key,
|
||||||
extra_headers: HashMap::new(),
|
extra_headers: HashMap::new(),
|
||||||
llm_timeout_secs: 120,
|
llm_timeout_secs: 120,
|
||||||
memory_maintenance_timeout_secs: 600,
|
|
||||||
model_id: openai_model,
|
model_id: openai_model,
|
||||||
temperature: Some(0.0),
|
temperature: Some(0.0),
|
||||||
max_tokens: Some(100),
|
max_tokens: Some(100),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user