feat: 添加记忆维护的错误处理逻辑,优化传输错误的上下文信息
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
891830779f
commit
04fc2c0710
@ -238,8 +238,18 @@ impl MemoryMaintenanceService {
|
|||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
for scope_key in scope_keys {
|
for scope_key in scope_keys {
|
||||||
let Some(output) = self.run_for_scope(&scope_key).await? else {
|
let output = match self.run_for_scope(&scope_key).await {
|
||||||
|
Ok(Some(output)) => output,
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(error) if is_recoverable_maintenance_scope_error(&error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
scope_key = %scope_key,
|
||||||
|
error = %error,
|
||||||
|
"Memory maintenance skipped scope after recoverable model failure"
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
|
|
||||||
results.push(MemoryMaintenanceScopeResult { scope_key, output });
|
results.push(MemoryMaintenanceScopeResult { scope_key, output });
|
||||||
@ -319,6 +329,10 @@ pub(crate) fn is_recoverable_maintenance_llm_error(error: &str) -> bool {
|
|||||||
|| normalized.contains("timeout")
|
|| normalized.contains("timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_recoverable_maintenance_scope_error(error: &AgentError) -> bool {
|
||||||
|
is_recoverable_maintenance_llm_error(&error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn strip_json_code_fence(content: &str) -> &str {
|
pub(crate) fn strip_json_code_fence(content: &str) -> &str {
|
||||||
let trimmed = content.trim();
|
let trimmed = content.trim();
|
||||||
if let Some(rest) = trimmed.strip_prefix("```json") {
|
if let Some(rest) = trimmed.strip_prefix("```json") {
|
||||||
|
|||||||
@ -1059,6 +1059,67 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_summarize_memory_maintenance_transport_error_includes_provider_context() {
|
||||||
|
let provider_config = LLMProviderConfig {
|
||||||
|
provider_type: "openai".to_string(),
|
||||||
|
name: "maintenance-provider".to_string(),
|
||||||
|
base_url: "https://example.invalid/v1".to_string(),
|
||||||
|
api_key: "test-key".to_string(),
|
||||||
|
extra_headers: HashMap::new(),
|
||||||
|
model_id: "maintenance-model".to_string(),
|
||||||
|
temperature: Some(0.0),
|
||||||
|
max_tokens: Some(256),
|
||||||
|
context_window_tokens: None,
|
||||||
|
model_extra: HashMap::new(),
|
||||||
|
max_tool_iterations: 1,
|
||||||
|
llm_timeout_secs: 1,
|
||||||
|
tool_result_max_chars: 20_000,
|
||||||
|
context_tool_result_trim_chars: 20_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
let session_manager = SessionManager::new(
|
||||||
|
4,
|
||||||
|
100,
|
||||||
|
false,
|
||||||
|
"Asia/Shanghai".to_string(),
|
||||||
|
provider_config.clone(),
|
||||||
|
HashMap::from([("default".to_string(), provider_config)]),
|
||||||
|
Arc::new(SkillRuntime::default()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
session_manager
|
||||||
|
.store()
|
||||||
|
.put_memory(&crate::storage::MemoryUpsert {
|
||||||
|
scope_kind: "user".to_string(),
|
||||||
|
scope_key: "feishu:user-1".to_string(),
|
||||||
|
namespace: "profile".to_string(),
|
||||||
|
memory_key: "work".to_string(),
|
||||||
|
content: "用户在做AI产品".to_string(),
|
||||||
|
source_type: "message".to_string(),
|
||||||
|
source_session_id: None,
|
||||||
|
source_message_id: None,
|
||||||
|
source_message_seq: None,
|
||||||
|
source_channel_name: None,
|
||||||
|
source_chat_id: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let error = session_manager
|
||||||
|
.summarize_memory_maintenance_for_scope("feishu:user-1")
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
assert!(error.contains("memory maintenance model error: transport error:"));
|
||||||
|
assert!(error.contains("provider=maintenance-provider"));
|
||||||
|
assert!(error.contains("model=maintenance-model"));
|
||||||
|
assert!(error.contains("url=https://example.invalid/v1/chat/completions"));
|
||||||
|
assert!(error.contains("timeout_secs=1"));
|
||||||
|
assert!(error.contains("error sending request for url"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_summarize_memory_maintenance_retries_recoverable_provider_errors() {
|
async fn test_summarize_memory_maintenance_retries_recoverable_provider_errors() {
|
||||||
let mock_response_content = serde_json::to_string(&json!({
|
let mock_response_content = serde_json::to_string(&json!({
|
||||||
@ -1249,6 +1310,63 @@ mod tests {
|
|||||||
assert!(results.is_empty());
|
assert!(results.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_run_memory_maintenance_for_all_scopes_skips_recoverable_transport_failures() {
|
||||||
|
let provider_config = LLMProviderConfig {
|
||||||
|
provider_type: "openai".to_string(),
|
||||||
|
name: "maintenance-provider".to_string(),
|
||||||
|
base_url: "https://example.invalid/v1".to_string(),
|
||||||
|
api_key: "test-key".to_string(),
|
||||||
|
extra_headers: HashMap::new(),
|
||||||
|
model_id: "maintenance-model".to_string(),
|
||||||
|
temperature: Some(0.0),
|
||||||
|
max_tokens: Some(256),
|
||||||
|
context_window_tokens: None,
|
||||||
|
model_extra: HashMap::new(),
|
||||||
|
max_tool_iterations: 1,
|
||||||
|
llm_timeout_secs: 1,
|
||||||
|
tool_result_max_chars: 20_000,
|
||||||
|
context_tool_result_trim_chars: 20_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
let session_manager = SessionManager::new(
|
||||||
|
4,
|
||||||
|
100,
|
||||||
|
false,
|
||||||
|
"Asia/Shanghai".to_string(),
|
||||||
|
provider_config.clone(),
|
||||||
|
HashMap::from([("default".to_string(), provider_config)]),
|
||||||
|
Arc::new(SkillRuntime::default()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for scope_key in ["feishu:user-1", "feishu:user-2"] {
|
||||||
|
session_manager
|
||||||
|
.store()
|
||||||
|
.put_memory(&crate::storage::MemoryUpsert {
|
||||||
|
scope_kind: "user".to_string(),
|
||||||
|
scope_key: scope_key.to_string(),
|
||||||
|
namespace: "profile".to_string(),
|
||||||
|
memory_key: "work".to_string(),
|
||||||
|
content: format!("{} 在做AI产品", scope_key),
|
||||||
|
source_type: "message".to_string(),
|
||||||
|
source_session_id: None,
|
||||||
|
source_message_id: None,
|
||||||
|
source_message_seq: None,
|
||||||
|
source_channel_name: None,
|
||||||
|
source_chat_id: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = session_manager
|
||||||
|
.run_memory_maintenance_for_all_scopes(None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(results.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_apply_memory_maintenance_output_merges_and_deletes_low_value_records() {
|
fn test_apply_memory_maintenance_output_merges_and_deletes_low_value_records() {
|
||||||
let store = SessionStore::in_memory().unwrap();
|
let store = SessionStore::in_memory().unwrap();
|
||||||
|
|||||||
@ -23,6 +23,23 @@ fn format_error_chain(error: &(dyn std::error::Error + 'static)) -> String {
|
|||||||
details.join("\ncaused by: ")
|
details.join("\ncaused by: ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_transport_error_context(
|
||||||
|
provider_name: &str,
|
||||||
|
model_id: &str,
|
||||||
|
url: &str,
|
||||||
|
timeout_secs: u64,
|
||||||
|
error: &(dyn std::error::Error + 'static),
|
||||||
|
) -> String {
|
||||||
|
format!(
|
||||||
|
"transport error: provider={} model={} url={} timeout_secs={} details={}",
|
||||||
|
provider_name,
|
||||||
|
model_id,
|
||||||
|
url,
|
||||||
|
timeout_secs,
|
||||||
|
format_error_chain(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_content_blocks(blocks: &[ContentBlock]) -> Value {
|
fn convert_content_blocks(blocks: &[ContentBlock]) -> Value {
|
||||||
if blocks.len() == 1 {
|
if blocks.len() == 1 {
|
||||||
if let ContentBlock::Text { text } = &blocks[0] {
|
if let ContentBlock::Text { text } = &blocks[0] {
|
||||||
@ -294,7 +311,25 @@ impl LLMProvider for OpenAIProvider {
|
|||||||
req_builder = req_builder.header(key.as_str(), value.as_str());
|
req_builder = req_builder.header(key.as_str(), value.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = req_builder.json(&body).send().await?;
|
let resp = req_builder.json(&body).send().await.map_err(|err| {
|
||||||
|
let error_context = format_transport_error_context(
|
||||||
|
&self.name,
|
||||||
|
&self.model_id,
|
||||||
|
&url,
|
||||||
|
self.llm_timeout_secs,
|
||||||
|
&err,
|
||||||
|
);
|
||||||
|
tracing::error!(
|
||||||
|
provider = %self.name,
|
||||||
|
model = %self.model_id,
|
||||||
|
url = %url,
|
||||||
|
base_url = %self.base_url,
|
||||||
|
timeout_secs = self.llm_timeout_secs,
|
||||||
|
error = %error_context,
|
||||||
|
"OpenAI-compatible API transport request failed"
|
||||||
|
);
|
||||||
|
error_context
|
||||||
|
})?;
|
||||||
|
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
let text = resp.text().await?;
|
let text = resp.text().await?;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user