refactor(memory): Timeline 按 session 隔离,拆分知识/摘要检索工具
- storage/memory: search_memories 和 search_memories_by_time 增加 session_id 过滤参数 - memory/manager: recall/recall_by_time 透传 session_id - tools: MemoryStoreTool/MemoryRecallTool 锁定 Knowledge 类别,移除 category 参数 - tools: 新增 TimelineRecallTool 用于检索会话摘要,支持可选 session_id 过滤 - tools: 输出格式化增加 session 信息显示 - tests: 新增 test_session_id_filter 验证会话级过滤
This commit is contained in:
parent
f9ae4b2c69
commit
cb1140e9be
@ -52,18 +52,21 @@ impl MemoryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Search memories by keyword query. Returns entries sorted by relevance.
|
/// Search memories by keyword query. Returns entries sorted by relevance.
|
||||||
|
/// When `session_id` is provided, results are filtered to that session.
|
||||||
pub async fn recall(
|
pub async fn recall(
|
||||||
&self,
|
&self,
|
||||||
query: &str,
|
query: &str,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
category: Option<MemoryCategory>,
|
category: Option<MemoryCategory>,
|
||||||
|
session_id: Option<&str>,
|
||||||
) -> Result<Vec<MemoryEntry>, crate::storage::StorageError> {
|
) -> Result<Vec<MemoryEntry>, crate::storage::StorageError> {
|
||||||
self.storage
|
self.storage
|
||||||
.search_memories(query, category.as_ref(), limit)
|
.search_memories(query, category.as_ref(), session_id, limit)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search memories by time range (Unix milliseconds).
|
/// Search memories by time range (Unix milliseconds).
|
||||||
|
/// When `session_id` is provided, results are filtered to that session.
|
||||||
pub async fn recall_by_time(
|
pub async fn recall_by_time(
|
||||||
&self,
|
&self,
|
||||||
since: i64,
|
since: i64,
|
||||||
@ -71,9 +74,10 @@ impl MemoryManager {
|
|||||||
query: Option<&str>,
|
query: Option<&str>,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
category: Option<MemoryCategory>,
|
category: Option<MemoryCategory>,
|
||||||
|
session_id: Option<&str>,
|
||||||
) -> Result<Vec<MemoryEntry>, crate::storage::StorageError> {
|
) -> Result<Vec<MemoryEntry>, crate::storage::StorageError> {
|
||||||
self.storage
|
self.storage
|
||||||
.search_memories_by_time(since, until, query, category.as_ref(), limit)
|
.search_memories_by_time(since, until, query, category.as_ref(), session_id, limit)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +88,7 @@ impl MemoryManager {
|
|||||||
|
|
||||||
/// Check if the memory system has any entries (for testing/health check).
|
/// Check if the memory system has any entries (for testing/health check).
|
||||||
pub async fn is_empty(&self) -> Result<bool, crate::storage::StorageError> {
|
pub async fn is_empty(&self) -> Result<bool, crate::storage::StorageError> {
|
||||||
self.recall("*", 1, None).await.map(|r| r.is_empty())
|
self.recall("*", 1, None, None).await.map(|r| r.is_empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +120,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let results = mm.recall("test memory", 10, None).await.unwrap();
|
let results = mm.recall("test memory", 10, None, None).await.unwrap();
|
||||||
assert_eq!(results.len(), 1);
|
assert_eq!(results.len(), 1);
|
||||||
assert_eq!(results[0].key, "test_key");
|
assert_eq!(results[0].key, "test_key");
|
||||||
assert_eq!(results[0].content, "This is a test memory");
|
assert_eq!(results[0].content, "This is a test memory");
|
||||||
@ -146,7 +150,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let results = mm.recall("updated", 10, None).await.unwrap();
|
let results = mm.recall("updated", 10, None, None).await.unwrap();
|
||||||
assert_eq!(results.len(), 1);
|
assert_eq!(results.len(), 1);
|
||||||
assert_eq!(results[0].content, "updated");
|
assert_eq!(results[0].content, "updated");
|
||||||
}
|
}
|
||||||
@ -166,7 +170,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
mm.forget("to_delete").await.unwrap();
|
mm.forget("to_delete").await.unwrap();
|
||||||
|
|
||||||
let results = mm.recall("deleted", 10, None).await.unwrap();
|
let results = mm.recall("deleted", 10, None, None).await.unwrap();
|
||||||
assert!(results.is_empty());
|
assert!(results.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,17 +198,60 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let know_results = mm
|
let know_results = mm
|
||||||
.recall("content", 10, Some(MemoryCategory::Knowledge))
|
.recall("content", 10, Some(MemoryCategory::Knowledge), None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(know_results.len(), 1);
|
assert_eq!(know_results.len(), 1);
|
||||||
assert_eq!(know_results[0].key, "knowledge_1");
|
assert_eq!(know_results[0].key, "knowledge_1");
|
||||||
|
|
||||||
let time_results = mm
|
let time_results = mm
|
||||||
.recall("content", 10, Some(MemoryCategory::Timeline))
|
.recall("content", 10, Some(MemoryCategory::Timeline), None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(time_results.len(), 1);
|
assert_eq!(time_results.len(), 1);
|
||||||
assert_eq!(time_results[0].key, "timeline_1");
|
assert_eq!(time_results[0].key, "timeline_1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_session_id_filter() {
|
||||||
|
let (mm, _dir) = setup_memory_manager().await;
|
||||||
|
|
||||||
|
// Store a timeline entry for session A
|
||||||
|
mm.store(
|
||||||
|
"tl_a",
|
||||||
|
"summary from session A",
|
||||||
|
MemoryCategory::Timeline,
|
||||||
|
Some("chan:chat:dialog_a"),
|
||||||
|
Some(0.5),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Store a timeline entry for session B
|
||||||
|
mm.store(
|
||||||
|
"tl_b",
|
||||||
|
"summary from session B",
|
||||||
|
MemoryCategory::Timeline,
|
||||||
|
Some("chan:chat:dialog_b"),
|
||||||
|
Some(0.5),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Recall without session_id — should get both
|
||||||
|
let all = mm
|
||||||
|
.recall("summary", 10, Some(MemoryCategory::Timeline), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(all.len(), 2);
|
||||||
|
|
||||||
|
// Recall scoped to session A — should get only tl_a
|
||||||
|
let scoped = mm
|
||||||
|
.recall("summary", 10, Some(MemoryCategory::Timeline), Some("chan:chat:dialog_a"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(scoped.len(), 1);
|
||||||
|
assert_eq!(scoped[0].key, "tl_a");
|
||||||
|
assert_eq!(scoped[0].session_id.as_deref(), Some("chan:chat:dialog_a"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1322,7 +1322,7 @@ impl SessionManager {
|
|||||||
let skills_prompt = self.skills_loader.build_skills_prompt();
|
let skills_prompt = self.skills_loader.build_skills_prompt();
|
||||||
|
|
||||||
// Fetch memory context
|
// Fetch memory context
|
||||||
let memory_context = match self.memory_manager.recall(content, 5, Some(crate::memory::MemoryCategory::Knowledge)).await {
|
let memory_context = match self.memory_manager.recall(content, 5, Some(crate::memory::MemoryCategory::Knowledge), None).await {
|
||||||
Ok(entries) if !entries.is_empty() => {
|
Ok(entries) if !entries.is_empty() => {
|
||||||
Some(entries.iter()
|
Some(entries.iter()
|
||||||
.map(|e| format!("- {}: {}", e.key, e.content))
|
.map(|e| format!("- {}: {}", e.key, e.content))
|
||||||
|
|||||||
@ -56,6 +56,7 @@ impl super::Storage {
|
|||||||
&self,
|
&self,
|
||||||
query: &str,
|
query: &str,
|
||||||
category: Option<&MemoryCategory>,
|
category: Option<&MemoryCategory>,
|
||||||
|
session_id: Option<&str>,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<MemoryEntry>, StorageError> {
|
) -> Result<Vec<MemoryEntry>, StorageError> {
|
||||||
// Build FTS5 query: segment with jieba, wrap each term in quotes, join with OR
|
// Build FTS5 query: segment with jieba, wrap each term in quotes, join with OR
|
||||||
@ -76,7 +77,7 @@ impl super::Storage {
|
|||||||
m.session_id, m.created_at, m.updated_at
|
m.session_id, m.created_at, m.updated_at
|
||||||
FROM memory_fts f
|
FROM memory_fts f
|
||||||
JOIN memories m ON f.rowid = m.rowid
|
JOIN memories m ON f.rowid = m.rowid
|
||||||
WHERE memory_fts MATCH ? AND (? IS NULL OR m.category = ?)
|
WHERE memory_fts MATCH ? AND (? IS NULL OR m.category = ?) AND (? IS NULL OR m.session_id = ?)
|
||||||
ORDER BY rank
|
ORDER BY rank
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
"#,
|
"#,
|
||||||
@ -84,6 +85,8 @@ impl super::Storage {
|
|||||||
.bind(&fts_query)
|
.bind(&fts_query)
|
||||||
.bind(category_filter)
|
.bind(category_filter)
|
||||||
.bind(category_filter)
|
.bind(category_filter)
|
||||||
|
.bind(session_id)
|
||||||
|
.bind(session_id)
|
||||||
.bind(limit as i64)
|
.bind(limit as i64)
|
||||||
.fetch_all(self.pool())
|
.fetch_all(self.pool())
|
||||||
.await?;
|
.await?;
|
||||||
@ -113,6 +116,7 @@ impl super::Storage {
|
|||||||
FROM memories
|
FROM memories
|
||||||
WHERE ({})
|
WHERE ({})
|
||||||
AND (? IS NULL OR category = ?)
|
AND (? IS NULL OR category = ?)
|
||||||
|
AND (? IS NULL OR session_id = ?)
|
||||||
ORDER BY importance DESC, updated_at DESC
|
ORDER BY importance DESC, updated_at DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
"#,
|
"#,
|
||||||
@ -127,6 +131,8 @@ impl super::Storage {
|
|||||||
query_builder = query_builder
|
query_builder = query_builder
|
||||||
.bind(category_filter)
|
.bind(category_filter)
|
||||||
.bind(category_filter)
|
.bind(category_filter)
|
||||||
|
.bind(session_id)
|
||||||
|
.bind(session_id)
|
||||||
.bind(limit as i64);
|
.bind(limit as i64);
|
||||||
|
|
||||||
let rows = query_builder.fetch_all(self.pool()).await?;
|
let rows = query_builder.fetch_all(self.pool()).await?;
|
||||||
@ -144,6 +150,7 @@ impl super::Storage {
|
|||||||
until: i64,
|
until: i64,
|
||||||
query: Option<&str>,
|
query: Option<&str>,
|
||||||
category: Option<&MemoryCategory>,
|
category: Option<&MemoryCategory>,
|
||||||
|
session_id: Option<&str>,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<MemoryEntry>, StorageError> {
|
) -> Result<Vec<MemoryEntry>, StorageError> {
|
||||||
let category_filter = category.map(|c| c.as_str());
|
let category_filter = category.map(|c| c.as_str());
|
||||||
@ -180,6 +187,7 @@ impl super::Storage {
|
|||||||
WHERE ({})
|
WHERE ({})
|
||||||
AND created_at >= ? AND created_at <= ?
|
AND created_at >= ? AND created_at <= ?
|
||||||
AND (? IS NULL OR category = ?)
|
AND (? IS NULL OR category = ?)
|
||||||
|
AND (? IS NULL OR session_id = ?)
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
"#,
|
"#,
|
||||||
@ -196,6 +204,8 @@ impl super::Storage {
|
|||||||
.bind(&until_dt)
|
.bind(&until_dt)
|
||||||
.bind(category_filter)
|
.bind(category_filter)
|
||||||
.bind(category_filter)
|
.bind(category_filter)
|
||||||
|
.bind(session_id)
|
||||||
|
.bind(session_id)
|
||||||
.bind(limit as i64);
|
.bind(limit as i64);
|
||||||
|
|
||||||
query_builder.fetch_all(self.pool()).await?
|
query_builder.fetch_all(self.pool()).await?
|
||||||
@ -207,6 +217,7 @@ impl super::Storage {
|
|||||||
FROM memories
|
FROM memories
|
||||||
WHERE created_at >= ? AND created_at <= ?
|
WHERE created_at >= ? AND created_at <= ?
|
||||||
AND (? IS NULL OR category = ?)
|
AND (? IS NULL OR category = ?)
|
||||||
|
AND (? IS NULL OR session_id = ?)
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
"#,
|
"#,
|
||||||
@ -215,6 +226,8 @@ impl super::Storage {
|
|||||||
.bind(&until_dt)
|
.bind(&until_dt)
|
||||||
.bind(category_filter)
|
.bind(category_filter)
|
||||||
.bind(category_filter)
|
.bind(category_filter)
|
||||||
|
.bind(session_id)
|
||||||
|
.bind(session_id)
|
||||||
.bind(limit as i64)
|
.bind(limit as i64)
|
||||||
.fetch_all(self.pool())
|
.fetch_all(self.pool())
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
@ -24,7 +24,7 @@ impl Tool for MemoryStoreTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"Store a fact, preference, or insight into long-term memory. \
|
"Store a fact, preference, or insight into long-term knowledge memory. \
|
||||||
Use this when the user shares important information you should remember. \
|
Use this when the user shares important information you should remember. \
|
||||||
Provide a descriptive key (e.g., 'user_prefers_python', 'project_auth_approach') \
|
Provide a descriptive key (e.g., 'user_prefers_python', 'project_auth_approach') \
|
||||||
and the full content to remember."
|
and the full content to remember."
|
||||||
@ -46,11 +46,6 @@ impl Tool for MemoryStoreTool {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The full content of the memory entry."
|
"description": "The full content of the memory entry."
|
||||||
},
|
},
|
||||||
"category": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["knowledge", "timeline"],
|
|
||||||
"description": "Memory category. Use 'knowledge' for facts/preferences/insights, 'timeline' for conversation summaries."
|
|
||||||
},
|
|
||||||
"importance": {
|
"importance": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Importance score 0.0-1.0. Higher = more important. Use 0.8+ for critical facts, 0.5 for general info."
|
"description": "Importance score 0.0-1.0. Higher = more important. Use 0.8+ for critical facts, 0.5 for general info."
|
||||||
@ -71,16 +66,10 @@ impl Tool for MemoryStoreTool {
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: content"))?;
|
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: content"))?;
|
||||||
|
|
||||||
let category = args
|
|
||||||
.get("category")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.and_then(MemoryCategory::from_str)
|
|
||||||
.unwrap_or(MemoryCategory::Knowledge);
|
|
||||||
|
|
||||||
let importance = args.get("importance").and_then(|v| v.as_f64());
|
let importance = args.get("importance").and_then(|v| v.as_f64());
|
||||||
|
|
||||||
self.memory
|
self.memory
|
||||||
.store(key, content, category, None, importance)
|
.store(key, content, MemoryCategory::Knowledge, None, importance)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(ToolResult {
|
Ok(ToolResult {
|
||||||
@ -110,8 +99,8 @@ impl Tool for MemoryRecallTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"Search and retrieve entries from long-term memory using keyword matching. \
|
"Search and retrieve entries from long-term knowledge memory using keyword matching. \
|
||||||
Use this to recall previously stored facts, preferences, or conversation history. \
|
Use this to recall previously stored facts, preferences, or insights. \
|
||||||
IMPORTANT: query must be a space-separated list of RELEVANT KEYWORDS (not a question or sentence). \
|
IMPORTANT: query must be a space-separated list of RELEVANT KEYWORDS (not a question or sentence). \
|
||||||
Use multiple synonymous or related terms to increase recall. \
|
Use multiple synonymous or related terms to increase recall. \
|
||||||
Example: instead of 'what is the user location', use 'user location address city residence'. \
|
Example: instead of 'what is the user location', use 'user location address city residence'. \
|
||||||
@ -130,11 +119,6 @@ impl Tool for MemoryRecallTool {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Space-separated KEYWORDS for memory search (NOT a natural language question). Use multiple related terms for better recall, e.g. 'address city location residence'."
|
"description": "Space-separated KEYWORDS for memory search (NOT a natural language question). Use multiple related terms for better recall, e.g. 'address city location residence'."
|
||||||
},
|
},
|
||||||
"category": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["knowledge", "timeline"],
|
|
||||||
"description": "Filter by memory category. Omit to search all categories."
|
|
||||||
},
|
|
||||||
"since": {
|
"since": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Start of time range (Unix milliseconds)."
|
"description": "Start of time range (Unix milliseconds)."
|
||||||
@ -158,11 +142,6 @@ impl Tool for MemoryRecallTool {
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: query"))?;
|
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: query"))?;
|
||||||
|
|
||||||
let category = args
|
|
||||||
.get("category")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.and_then(MemoryCategory::from_str);
|
|
||||||
|
|
||||||
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
||||||
|
|
||||||
let entries = if args.get("since").is_some() || args.get("until").is_some() {
|
let entries = if args.get("since").is_some() || args.get("until").is_some() {
|
||||||
@ -172,10 +151,10 @@ impl Tool for MemoryRecallTool {
|
|||||||
.and_then(|v| v.as_i64())
|
.and_then(|v| v.as_i64())
|
||||||
.unwrap_or(chrono::Utc::now().timestamp_millis());
|
.unwrap_or(chrono::Utc::now().timestamp_millis());
|
||||||
self.memory
|
self.memory
|
||||||
.recall_by_time(since, until, Some(query), limit, category)
|
.recall_by_time(since, until, Some(query), limit, Some(MemoryCategory::Knowledge), None)
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
self.memory.recall(query, limit, category).await?
|
self.memory.recall(query, limit, Some(MemoryCategory::Knowledge), None).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
@ -189,10 +168,12 @@ impl Tool for MemoryRecallTool {
|
|||||||
let formatted = entries
|
let formatted = entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| {
|
.map(|e| {
|
||||||
|
let session = e.session_id.as_deref().map(|s| format!(" [session: {}]", s)).unwrap_or_default();
|
||||||
format!(
|
format!(
|
||||||
"- {} [{}] [importance: {:.1}]: {}",
|
"- {} [{}]{} [importance: {:.1}]: {}",
|
||||||
e.key,
|
e.key,
|
||||||
e.category.as_str(),
|
e.category.as_str(),
|
||||||
|
session,
|
||||||
e.importance,
|
e.importance,
|
||||||
e.content
|
e.content
|
||||||
)
|
)
|
||||||
@ -208,6 +189,119 @@ impl Tool for MemoryRecallTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── TimelineRecallTool ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct TimelineRecallTool {
|
||||||
|
memory: Arc<MemoryManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimelineRecallTool {
|
||||||
|
pub fn new(memory: Arc<MemoryManager>) -> Self {
|
||||||
|
Self { memory }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Tool for TimelineRecallTool {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"timeline_recall"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Search and retrieve conversation summaries from timeline memory. \
|
||||||
|
Use this to recall what was discussed in past sessions or earlier in the current session. \
|
||||||
|
Optionally filter by session_id to scope to a specific conversation. \
|
||||||
|
IMPORTANT: query must be a space-separated list of RELEVANT KEYWORDS (not a question or sentence)."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_only(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameters_schema(&self) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Space-separated KEYWORDS for timeline search (NOT a natural language question). Use multiple related terms for better recall."
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter to a specific session (format: channel:chat_id:dialog_id). Omit to search across all sessions."
|
||||||
|
},
|
||||||
|
"since": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Start of time range (Unix milliseconds)."
|
||||||
|
},
|
||||||
|
"until": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "End of time range (Unix milliseconds)."
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Max results to return (default 10)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||||
|
let query = args
|
||||||
|
.get("query")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: query"))?;
|
||||||
|
|
||||||
|
let session_id = args.get("session_id").and_then(|v| v.as_str());
|
||||||
|
|
||||||
|
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
||||||
|
|
||||||
|
let entries = if args.get("since").is_some() || args.get("until").is_some() {
|
||||||
|
let since = args.get("since").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||||
|
let until = args
|
||||||
|
.get("until")
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.unwrap_or(chrono::Utc::now().timestamp_millis());
|
||||||
|
self.memory
|
||||||
|
.recall_by_time(since, until, Some(query), limit, Some(MemoryCategory::Timeline), session_id)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
self.memory.recall(query, limit, Some(MemoryCategory::Timeline), session_id).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
if entries.is_empty() {
|
||||||
|
return Ok(ToolResult {
|
||||||
|
success: true,
|
||||||
|
output: "No matching timeline entries found.".to_string(),
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| {
|
||||||
|
let session = e.session_id.as_deref().map(|s| format!(" [session: {}]", s)).unwrap_or_default();
|
||||||
|
format!(
|
||||||
|
"- {} [{}]{} [importance: {:.1}]: {}",
|
||||||
|
e.key,
|
||||||
|
e.category.as_str(),
|
||||||
|
session,
|
||||||
|
e.importance,
|
||||||
|
e.content
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
Ok(ToolResult {
|
||||||
|
success: true,
|
||||||
|
output: format!("Found {} timeline entries:\n{}", entries.len(), formatted),
|
||||||
|
error: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── MemoryForgetTool ─────────────────────────────────────────────────
|
// ── MemoryForgetTool ─────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct MemoryForgetTool {
|
pub struct MemoryForgetTool {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ pub use file_read::FileReadTool;
|
|||||||
pub use file_write::FileWriteTool;
|
pub use file_write::FileWriteTool;
|
||||||
pub use get_skill::GetSkillTool;
|
pub use get_skill::GetSkillTool;
|
||||||
pub use http_request::HttpRequestTool;
|
pub use http_request::HttpRequestTool;
|
||||||
pub use memory::{MemoryForgetTool, MemoryRecallTool, MemoryStoreTool};
|
pub use memory::{MemoryForgetTool, MemoryRecallTool, MemoryStoreTool, TimelineRecallTool};
|
||||||
pub use registry::ToolRegistry;
|
pub use registry::ToolRegistry;
|
||||||
pub use schema::{CleaningStrategy, SchemaCleanr};
|
pub use schema::{CleaningStrategy, SchemaCleanr};
|
||||||
pub use send_message::SendMessageTool;
|
pub use send_message::SendMessageTool;
|
||||||
@ -57,6 +57,7 @@ pub fn create_default_tools(
|
|||||||
|
|
||||||
registry.register(MemoryStoreTool::new(memory.clone()));
|
registry.register(MemoryStoreTool::new(memory.clone()));
|
||||||
registry.register(MemoryRecallTool::new(memory.clone()));
|
registry.register(MemoryRecallTool::new(memory.clone()));
|
||||||
|
registry.register(TimelineRecallTool::new(memory.clone()));
|
||||||
registry.register(MemoryForgetTool::new(memory.clone()));
|
registry.register(MemoryForgetTool::new(memory.clone()));
|
||||||
|
|
||||||
registry
|
registry
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user