feat: 重构记忆工具提示和代理配置,增强用户指导和系统提示

This commit is contained in:
ooodc 2026-04-23 17:33:10 +08:00
parent 3d241544c5
commit 7fefd40dca
7 changed files with 448 additions and 10 deletions

View File

@ -21,7 +21,8 @@ use std::time::Instant;
const MAX_TOOL_RESULT_CHARS: usize = 16_000; const MAX_TOOL_RESULT_CHARS: usize = 16_000;
/// Minimum characters to keep when truncating /// Minimum characters to keep when truncating
const TRUNCATION_SUFFIX_LEN: usize = 200; const TRUNCATION_SUFFIX_LEN: usize = 200;
const MEMORY_TOOL_USAGE_SYSTEM_PROMPT: &str = "在绝大多数请求开始时,你都应先使用长期记忆检索工具 memory_search 来召回相关上下文,然后再决定如何回答或是否需要写入记忆。默认流程是:先用 memory_search(action='search');只有在你已经明确知道 namespace 和 key 时才改用 get只有在需要浏览最近几条记忆时才用 list。即使用户没有明确提到“记忆”或“偏好”只要请求可能与用户长期偏好、稳定事实、历史决策、持续任务或项目上下文有关就应先搜记忆。仅以下少数情况可跳过记忆搜索纯寒暄、一次性简单计算、完全不依赖用户历史的直接事实问答。写入或修改记忆时再使用 memory_manage。仅在遇到高价值且未来仍有用的信息时写入记忆用户长期偏好、稳定事实、用户对你的纠正、持续任务/项目上下文、明确决策。不要保存一次性工具结果、临时列表、敏感凭证或不确定推测。写入时优先使用规范 namespacepreferences、profile、tasks、decisions并优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。检索时应提供 queries 数组,尽量同时放入中文关键词、英文别名,以及可能的 snake_case memory_key 词,例如 queries=['email', '邮件', 'email_folder_preference']。如果你决定跳过记忆搜索,应先确认当前请求确实属于上述少数例外,而不是因为你忘了检索。"; const MEMORY_TOOL_USAGE_SYSTEM_PROMPT: &str =
include_str!("memory_tool_usage_system_prompt.md");
const PENDING_USER_ACTION_MARKER: &str = "__PICOBOT_PENDING_USER_ACTION__"; const PENDING_USER_ACTION_MARKER: &str = "__PICOBOT_PENDING_USER_ACTION__";
const DEFAULT_PENDING_ASSISTANT_MESSAGE: &str = "工具已经启动并进入等待用户操作的状态。请先完成外部操作,完成后直接告诉我继续。"; const DEFAULT_PENDING_ASSISTANT_MESSAGE: &str = "工具已经启动并进入等待用户操作的状态。请先完成外部操作,完成后直接告诉我继续。";
const RECOVERABLE_LLM_ERROR_MESSAGE: &str = "模型服务暂时不可用或响应超时。请稍后重试。"; const RECOVERABLE_LLM_ERROR_MESSAGE: &str = "模型服务暂时不可用或响应超时。请稍后重试。";
@ -850,6 +851,20 @@ mod tests {
assert_eq!(provider_message.tool_calls.as_ref().unwrap()[0].name, "calculator"); assert_eq!(provider_message.tool_calls.as_ref().unwrap()[0].name, "calculator");
} }
#[test]
fn test_memory_prompt_requires_proactive_memory_search() {
assert!(MEMORY_TOOL_USAGE_SYSTEM_PROMPT.contains("在绝大多数请求开始时"));
assert!(MEMORY_TOOL_USAGE_SYSTEM_PROMPT.contains("先使用长期记忆检索工具 memory_search"));
assert!(MEMORY_TOOL_USAGE_SYSTEM_PROMPT.contains("不要因为你自认为已经能直接回答就省略检索"));
}
#[test]
fn test_memory_prompt_allows_parallel_independent_tool_calls() {
assert!(MEMORY_TOOL_USAGE_SYSTEM_PROMPT.contains("同一轮同时返回多个 tool calls"));
assert!(MEMORY_TOOL_USAGE_SYSTEM_PROMPT.contains("自动并行执行"));
assert!(MEMORY_TOOL_USAGE_SYSTEM_PROMPT.contains("只有当后一个工具的参数依赖"));
}
#[test] #[test]
fn test_truncate_tool_result_handles_utf8_char_boundaries() { fn test_truncate_tool_result_handles_utf8_char_boundaries() {
let input = "".repeat(MAX_TOOL_RESULT_CHARS + 500); let input = "".repeat(MAX_TOOL_RESULT_CHARS + 500);

View File

@ -0,0 +1,47 @@
# 记忆检索
在绝大多数请求开始时,你都应先使用长期记忆检索工具 memory_search 来召回相关记忆,再决定如何回答或是否需要写入记忆。先检索通常能帮助你识别用户长期偏好、稳定事实、历史决策、持续任务和上下文约束。
## 默认流程
- 先使用长期记忆检索工具 memory_search优先调用 memory_search(action='search')。
- 只有在你已经明确知道 namespace 和 key 时,才改用 get。
- 只有在需要浏览最近几条记忆时,才用 list。
- 即使用户没有明确提到“记忆”或“偏好”,也应该先搜记忆,不要因为你自认为已经能直接回答就省略检索。
## 可以跳过检索的情况
仅以下少数情况可跳过记忆搜索:
- 纯寒暄
- 一次性简单计算
- 完全不依赖用户历史的直接事实问答
如果当前请求不明显属于这些例外,就默认先检索。
## 并行调用规则
若当前请求同时还需要其它彼此独立的只读工具,你可以在同一轮同时返回多个 tool calls把 memory_search 与这些独立工具一起调用;运行时会按工具能力自动并行执行。
只有当后一个工具的参数依赖 memory_search 或其它工具结果时,才分多轮串行调用。
## 检索方式
- 检索时应提供 queries 数组。
- 尽量同时放入中文关键词、英文别名,以及可能的 snake_case memory_key 词。如果最后一轮的会话与前面的话题不一致则优先根据最后一轮用户的会话来生成关键词保留1、 2个历史会话的
- 越靠近最新会话,生成关键词的比例或者权重应该更高
- 例如queries=['email', '邮件', 'email_folder_preference']。
如果用户在聊持续任务、既有偏好、历史决策、项目上下文、曾经纠正过你的内容,或当前请求看起来像是延续之前的事,优先先搜记忆。
## 写入规则
- 写入或修改记忆时,再使用 memory_manage。
- 仅在遇到高价值且未来仍有用的信息时写入记忆:用户长期偏好、稳定事实、用户对你的纠正、持续任务或项目上下文、明确决策。
- 不要保存一次性工具结果、临时列表、敏感凭证或不确定推测。
- 写入时优先使用规范 namespacepreferences、profile、tasks、decisions。
- 优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。
## 最后检查
如果你决定跳过记忆搜索,应先确认当前请求确实属于上述少数例外,而不是因为你忘了检索,或因为你误以为单凭当前消息就足够。

View File

@ -0,0 +1,35 @@
# PicoBot 代理配置
## 身份
- 你是 PicoBot一名务实、可靠的通用助理。
- 你的目标是理解用户当下的真实需求,并给出清晰、可执行的帮助。
## 工作方式
- 优先理解意图,再给出回应或行动。
- 保持简洁、准确、自然,不故作热情,也不空泛铺陈。
- 能直接验证的内容尽量先验证,避免凭空猜测。
- 当现有工具是完成任务的最直接方式时,优先使用工具。
- 除非用户明确要求改变方向,否则保持用户原本目标不变。
## 助理原则
- 优先解决问题,而不是展示过程。
- 输出要方便用户立即使用,结论尽量明确。
- 对不确定的地方要直说,不把猜测包装成事实。
- 复杂任务先收敛重点,简单任务直接给结果。
- 避免不必要的重复、客套和冗长说明。
## 回复规则
- 除非用户另有要求,否则使用中文回复。
- 默认短而清楚,按信息密度组织内容。
- 如果任务涉及文件、命令、配置或下一步操作,优先给出最关键的那部分。
- 如果存在限制、风险或前提条件,要直接说明。
## 补充要求
- 你是 PicoBot。
- 回答应以帮助用户完成当前目标为中心。
- 在信息不足时先补关键前提,在信息充分时直接执行。

View File

@ -0,0 +1 @@
你是 PicoBot 的后台记忆整理器。你必须根据输入的候选记忆做语义整理,并严格返回 JSON不要输出 Markdown 代码块,不要输出额外解释。输出 JSON 字段必须包含user_facts, preferences, behavior_patterns, merges, conflicts, low_value_ids, managed_markdown。user_facts、preferences、behavior_patterns 是字符串数组。merges 是对象数组,每个对象必须包含 source_ids、namespace、memory_key、content。conflicts 是对象数组,每个对象必须包含 source_ids、note。low_value_ids 是需要删除的候选记忆 id 数组。只能引用输入里出现过的候选 id。managed_markdown 必须是 Markdown 文本,且只保留稳定模式,不写一次性事件。

View File

@ -20,11 +20,13 @@ use crate::tools::{
WebFetchTool, WebFetchTool,
}; };
const DEFAULT_AGENT_PROMPT: &str = "# PicoBot 代理配置\n\n## 身份\n- 你是 PicoBot一名务实、可靠的通用助理。\n- 你的目标是理解用户当下的真实需求,并给出清晰、可执行的帮助。\n\n## 工作方式\n- 优先理解意图,再给出回应或行动。\n- 保持简洁、准确、自然,不故作热情,也不空泛铺陈。\n- 能直接验证的内容尽量先验证,避免凭空猜测。\n- 当现有工具是完成任务的最直接方式时,优先使用工具。\n- 除非用户明确要求改变方向,否则保持用户原本目标不变。\n\n## 助理原则\n- 优先解决问题,而不是展示过程。\n- 输出要方便用户立即使用,结论尽量明确。\n- 对不确定的地方要直说,不把猜测包装成事实。\n- 复杂任务先收敛重点,简单任务直接给结果。\n- 避免不必要的重复、客套和冗长说明。\n\n## 回复规则\n- 除非用户另有要求,否则使用中文回复。\n- 默认短而清楚,按信息密度组织内容。\n- 如果任务涉及文件、命令、配置或下一步操作,优先给出最关键的那部分。\n- 如果存在限制、风险或前提条件,要直接说明。\n\n## 补充要求\n- 你是 PicoBot。\n- 回答应以帮助用户完成当前目标为中心。\n- 在信息不足时先补关键前提,在信息充分时直接执行。\n"; const DEFAULT_AGENT_PROMPT: &str = include_str!("default_agent_prompt.md");
const MANAGED_AGENT_MEMORY_BLOCK_START: &str = "<!-- PICOBOT_MANAGED_MEMORY:START -->"; const MANAGED_AGENT_MEMORY_BLOCK_START: &str = "<!-- PICOBOT_MANAGED_MEMORY:START -->";
const MANAGED_AGENT_MEMORY_BLOCK_END: &str = "<!-- PICOBOT_MANAGED_MEMORY:END -->"; const MANAGED_AGENT_MEMORY_BLOCK_END: &str = "<!-- PICOBOT_MANAGED_MEMORY:END -->";
const MANAGED_AGENT_MEMORY_TITLE: &str = "## 用户记忆摘要"; const MANAGED_AGENT_MEMORY_TITLE: &str = "## 用户记忆摘要";
const MEMORY_MAINTENANCE_SYSTEM_PROMPT: &str = "你是 PicoBot 的后台记忆整理器。你必须根据输入的候选记忆做语义整理,并严格返回 JSON不要输出 Markdown 代码块,不要输出额外解释。输出 JSON 字段必须包含user_facts, preferences, behavior_patterns, merges, conflicts, low_value_ids, managed_markdown。user_facts、preferences、behavior_patterns 是字符串数组。merges 是对象数组,每个对象必须包含 source_ids、namespace、memory_key、content。conflicts 是对象数组,每个对象必须包含 source_ids、note。low_value_ids 是需要删除的候选记忆 id 数组。只能引用输入里出现过的候选 id。managed_markdown 必须是 Markdown 文本,且只保留稳定模式,不写一次性事件。"; const MEMORY_MAINTENANCE_SYSTEM_PROMPT: &str =
include_str!("memory_maintenance_system_prompt.md");
const MEMORY_MAINTENANCE_RETRY_DELAYS_MS: &[u64] = &[1_000, 3_000];
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MemoryMaintenanceCategory { enum MemoryMaintenanceCategory {
@ -128,6 +130,16 @@ fn memory_maintenance_category(namespace: &str) -> MemoryMaintenanceCategory {
} }
} }
fn is_recoverable_maintenance_llm_error(error: &str) -> bool {
let normalized = error.to_ascii_lowercase();
normalized.contains("error sending request for url")
|| normalized.contains("504")
|| normalized.contains("gateway timeout")
|| normalized.contains("stream timeout")
|| normalized.contains("timed out")
|| normalized.contains("timeout")
}
fn render_managed_agent_memory_block(markdown_body: &str) -> String { fn render_managed_agent_memory_block(markdown_body: &str) -> String {
format!( format!(
"{MANAGED_AGENT_MEMORY_BLOCK_START}\n{MANAGED_AGENT_MEMORY_TITLE}\n\n{}\n{MANAGED_AGENT_MEMORY_BLOCK_END}", "{MANAGED_AGENT_MEMORY_BLOCK_START}\n{MANAGED_AGENT_MEMORY_TITLE}\n\n{}\n{MANAGED_AGENT_MEMORY_BLOCK_END}",
@ -196,6 +208,60 @@ fn strip_json_code_fence(content: &str) -> &str {
trimmed trimmed
} }
fn extract_json_object(content: &str) -> Option<&str> {
let mut start = None;
let mut depth = 0usize;
let mut in_string = false;
let mut escaped = false;
for (index, ch) in content.char_indices() {
if in_string {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' => escaped = true,
'"' => in_string = false,
_ => {}
}
continue;
}
match ch {
'"' => in_string = true,
'{' => {
if start.is_none() {
start = Some(index);
}
depth += 1;
}
'}' => {
if depth == 0 {
continue;
}
depth -= 1;
if depth == 0 {
let start = start?;
let end = index + ch.len_utf8();
return Some(content[start..end].trim());
}
}
_ => {}
}
}
None
}
fn preview_text(content: &str, max_chars: usize) -> String {
let mut preview = content.chars().take(max_chars).collect::<String>();
if content.chars().count() > max_chars {
preview.push_str("...");
}
preview.replace('\n', "\\n")
}
fn combine_managed_memory_markdown(chunks: &[String]) -> String { fn combine_managed_memory_markdown(chunks: &[String]) -> String {
let normalized_chunks = chunks let normalized_chunks = chunks
.iter() .iter()
@ -870,13 +936,67 @@ impl SessionManager {
tools: None, tools: None,
}; };
let response = provider let mut last_error = None;
.chat(request) let mut response = None;
.await
.map_err(|err| AgentError::Other(format!("memory maintenance model error: {}", err)))?;
let output: MemoryMaintenanceModelOutput = serde_json::from_str(strip_json_code_fence(&response.content)) for (attempt, delay_ms) in MEMORY_MAINTENANCE_RETRY_DELAYS_MS
.map_err(|err| AgentError::Other(format!("memory maintenance JSON decode error: {}", err)))?; .iter()
.copied()
.map(Some)
.chain(std::iter::once(None))
.enumerate()
{
match provider.chat(request.clone()).await {
Ok(success) => {
response = Some(success);
break;
}
Err(err) => {
let error_text = err.to_string();
let should_retry = delay_ms.is_some()
&& is_recoverable_maintenance_llm_error(&error_text);
last_error = Some(error_text.clone());
if should_retry {
tracing::warn!(
scope_key = %scope_key,
attempt = attempt + 1,
retry_in_ms = delay_ms.unwrap_or_default(),
error = %error_text,
"Memory maintenance model request failed, retrying"
);
tokio::time::sleep(Duration::from_millis(delay_ms.unwrap_or_default())).await;
continue;
}
return Err(AgentError::Other(format!("memory maintenance model error: {}", error_text)));
}
}
}
let response = response.ok_or_else(|| {
AgentError::Other(format!(
"memory maintenance model error: {}",
last_error.unwrap_or_else(|| "unknown provider error".to_string())
))
})?;
let raw_content = strip_json_code_fence(&response.content);
let json_candidate = extract_json_object(raw_content).unwrap_or(raw_content);
let output: MemoryMaintenanceModelOutput = serde_json::from_str(json_candidate)
.map_err(|err| {
tracing::error!(
scope_key = %scope_key,
error = %err,
raw_len = raw_content.len(),
raw_preview = %preview_text(raw_content, 400),
json_candidate_len = json_candidate.len(),
json_candidate_preview = %preview_text(json_candidate, 400),
"Memory maintenance JSON decode failed"
);
AgentError::Other(format!("memory maintenance JSON decode error: {}", err))
})?;
Ok(output) Ok(output)
} }
@ -1327,6 +1447,10 @@ mod tests {
use crate::storage::MemoryRecord; use crate::storage::MemoryRecord;
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{
Arc as StdArc,
atomic::{AtomicUsize, Ordering},
};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -1475,6 +1599,63 @@ mod tests {
format!("http://{}", address) format!("http://{}", address)
} }
async fn start_mock_openai_flaky_server() -> String {
let attempts = StdArc::new(AtomicUsize::new(0));
async fn handle(
axum::extract::State(attempts): axum::extract::State<StdArc<AtomicUsize>>,
Json(body): Json<Value>,
) -> (StatusCode, Json<Value>) {
let attempt = attempts.fetch_add(1, Ordering::SeqCst);
if attempt == 0 {
return (
StatusCode::GATEWAY_TIMEOUT,
Json(json!({"error": "stream timeout"})),
);
}
let model = body
.get("model")
.and_then(|value| value.as_str())
.unwrap_or("unknown-model");
let content = body
.get("mock_response_content")
.and_then(|value| value.as_str())
.unwrap_or("{\"user_facts\":[],\"preferences\":[],\"behavior_patterns\":[],\"merges\":[],\"conflicts\":[],\"low_value_ids\":[],\"managed_markdown\":\"\"}");
(
StatusCode::OK,
Json(json!({
"id": "mock-response",
"model": model,
"choices": [
{
"message": {
"content": content,
"tool_calls": []
}
}
],
"usage": {
"prompt_tokens": 1,
"completion_tokens": 1,
"total_tokens": 2
}
})),
)
}
let app = Router::new()
.route("/chat/completions", post(handle))
.with_state(attempts);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
format!("http://{}", address)
}
#[tokio::test] #[tokio::test]
async fn test_handle_message_returns_recoverable_reply_on_llm_504() { async fn test_handle_message_returns_recoverable_reply_on_llm_504() {
let base_url = start_mock_openai_504_server().await; let base_url = start_mock_openai_504_server().await;
@ -1652,6 +1833,151 @@ mod tests {
assert!(output.managed_markdown.contains("### 用户事实")); assert!(output.managed_markdown.contains("### 用户事实"));
} }
#[test]
fn test_is_recoverable_maintenance_llm_error_detects_transport_failures() {
assert!(is_recoverable_maintenance_llm_error(
"error sending request for url (https://example.invalid/v1/chat/completions)"
));
assert!(is_recoverable_maintenance_llm_error("API error 504 Gateway Timeout: stream timeout"));
assert!(!is_recoverable_maintenance_llm_error("API error 401 Unauthorized"));
}
#[test]
fn test_extract_json_object_skips_wrapping_text() {
let wrapped = "下面是结果:\n```json\n{\n \"user_facts\": [],\n \"preferences\": []\n}\n```\n请查收";
let stripped = strip_json_code_fence(wrapped);
let extracted = extract_json_object(stripped).unwrap();
assert_eq!(extracted, "{\n \"user_facts\": [],\n \"preferences\": []\n}");
}
#[tokio::test]
async fn test_summarize_memory_maintenance_retries_recoverable_provider_errors() {
let base_url = start_mock_openai_flaky_server().await;
let mock_response_content = serde_json::to_string(&json!({
"user_facts": ["用户在做AI产品"],
"preferences": [],
"behavior_patterns": [],
"merges": [],
"conflicts": [],
"low_value_ids": [],
"managed_markdown": "### 用户事实\n- 用户在做AI产品"
}))
.unwrap();
let provider_config = LLMProviderConfig {
provider_type: "openai".to_string(),
name: "maintenance-provider".to_string(),
base_url,
api_key: "test-key".to_string(),
extra_headers: HashMap::new(),
model_id: "maintenance-model".to_string(),
temperature: Some(0.0),
max_tokens: Some(256),
model_extra: HashMap::from([(
"mock_response_content".to_string(),
json!(mock_response_content),
)]),
max_tool_iterations: 1,
token_limit: 4096,
llm_timeout_secs: 30,
};
let session_manager = SessionManager::new(
4,
100,
false,
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 output = session_manager
.summarize_memory_maintenance_for_scope("feishu:user-1")
.await
.unwrap()
.unwrap();
assert_eq!(output.user_facts, vec!["用户在做AI产品".to_string()]);
}
#[tokio::test]
async fn test_summarize_memory_maintenance_for_scope_extracts_wrapped_json_object() {
let base_url = start_mock_openai_server().await;
let mock_response_content = "结果如下:\n```json\n{\n \"user_facts\": [\"用户在做AI产品\"],\n \"preferences\": [],\n \"behavior_patterns\": [],\n \"merges\": [],\n \"conflicts\": [],\n \"low_value_ids\": [],\n \"managed_markdown\": \"### 用户事实\\n- 用户在做AI产品\"\n}\n```\n";
let provider_config = LLMProviderConfig {
provider_type: "openai".to_string(),
name: "maintenance-provider".to_string(),
base_url,
api_key: "test-key".to_string(),
extra_headers: HashMap::new(),
model_id: "maintenance-model".to_string(),
temperature: Some(0.0),
max_tokens: Some(256),
model_extra: HashMap::from([(
"mock_response_content".to_string(),
json!(mock_response_content),
)]),
max_tool_iterations: 1,
token_limit: 4096,
llm_timeout_secs: 30,
};
let session_manager = SessionManager::new(
4,
100,
false,
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 output = session_manager
.summarize_memory_maintenance_for_scope("feishu:user-1")
.await
.unwrap()
.unwrap();
assert_eq!(output.user_facts, vec!["用户在做AI产品".to_string()]);
assert!(output.managed_markdown.contains("### 用户事实"));
}
#[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();

View File

@ -1,12 +1,23 @@
use std::path::PathBuf; use std::path::PathBuf;
use chrono::Local;
use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{ use tracing_subscriber::{
fmt, fmt,
fmt::time::FormatTime,
layer::SubscriberExt, layer::SubscriberExt,
util::SubscriberInitExt, util::SubscriberInitExt,
EnvFilter, EnvFilter,
}; };
#[derive(Clone, Copy, Debug, Default)]
struct LocalTimestamp;
impl FormatTime for LocalTimestamp {
fn format_time(&self, writer: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
write!(writer, "{}", Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true))
}
}
/// Get the default log directory path: ~/.picobot/logs /// Get the default log directory path: ~/.picobot/logs
pub fn get_default_log_dir() -> PathBuf { pub fn get_default_log_dir() -> PathBuf {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
@ -44,12 +55,14 @@ pub fn init_logging() {
let file_layer = fmt::layer() let file_layer = fmt::layer()
.with_writer(file_appender) .with_writer(file_appender)
.with_timer(LocalTimestamp)
.with_ansi(false) .with_ansi(false)
.with_target(true) .with_target(true)
.with_level(true) .with_level(true)
.with_thread_ids(true); .with_thread_ids(true);
let console_layer = fmt::layer() let console_layer = fmt::layer()
.with_timer(LocalTimestamp)
.with_target(true) .with_target(true)
.with_level(true); .with_level(true);
@ -68,6 +81,7 @@ pub fn init_logging_console_only() {
.unwrap_or_else(|_| EnvFilter::new("info")); .unwrap_or_else(|_| EnvFilter::new("info"));
let console_layer = fmt::layer() let console_layer = fmt::layer()
.with_timer(LocalTimestamp)
.with_target(true) .with_target(true)
.with_level(true); .with_level(true);

View File

@ -23,7 +23,7 @@ impl Tool for MemorySearchTool {
} }
fn description(&self) -> &str { fn description(&self) -> &str {
"Search and read long-term user memories stored in SQLite. This is the default entry point for memory retrieval and should usually be the first memory tool you call at the start of a request, unless the request is clearly a simple greeting, a one-off calculation, or a direct fact question that does not depend on user history. Use it to recall prior preferences, stable facts, historical decisions, and ongoing task context. This tool is read-only and supports three actions: search for multi-keyword recall, get for exact namespace/key lookup, and list for browsing recent memories. Prefer this tool over memory_manage whenever you only need to retrieve memory." "Search and read long-term user memories stored in SQLite. This is the default entry point for memory retrieval and should usually be the first memory tool you call at the start of a request, unless the request is clearly a simple greeting, a one-off calculation, or a direct fact question that does not depend on user history. Use it to recall prior preferences, stable facts, historical decisions, and ongoing task context. If the request also needs other independent read-only tools, you may call memory_search in the same round alongside them. This tool is read-only and supports three actions: search for multi-keyword recall, get for exact namespace/key lookup, and list for browsing recent memories. Prefer this tool over memory_manage whenever you only need to retrieve memory."
} }
fn parameters_schema(&self) -> serde_json::Value { fn parameters_schema(&self) -> serde_json::Value {