Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P1-1: tracker.rs record_learning 改为通过 MemoryEntry 存储
(之前用 store_metadata 存但 get_timeline 用 find_by_prefix 读,
两条路径不交叉,timeline 永远返回空)
P2-4: extractor.rs 移除未使用的 _llm_driver 绑定,改为 is_none() 检查
P2-5: lib.rs 模块文档更新,反映实际 17 个模块而非原始 4 个
profile_updater.rs: 添加注释说明只收集 update_field 支持的字段
测试: zclaw-growth 137 tests, zclaw-runtime 87 tests, 0 failures
222 lines
6.9 KiB
Rust
222 lines
6.9 KiB
Rust
//! Growth Tracker - Tracks agent growth metrics and evolution
|
|
//!
|
|
//! This module provides the `GrowthTracker` which monitors and records
|
|
//! the evolution of an agent's capabilities and knowledge over time.
|
|
|
|
use crate::types::{GrowthStats, MemoryType};
|
|
use crate::viking_adapter::VikingAdapter;
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use zclaw_types::{AgentId, Result};
|
|
|
|
/// Growth Tracker - tracks agent growth metrics
|
|
pub struct GrowthTracker {
|
|
/// OpenViking adapter for storage
|
|
viking: Arc<VikingAdapter>,
|
|
}
|
|
|
|
impl GrowthTracker {
|
|
/// Create a new growth tracker
|
|
pub fn new(viking: Arc<VikingAdapter>) -> Self {
|
|
Self { viking }
|
|
}
|
|
|
|
/// Get current growth statistics for an agent
|
|
pub async fn get_stats(&self, agent_id: &AgentId) -> Result<GrowthStats> {
|
|
// Query all memories for the agent
|
|
let memories = self.viking.find_by_prefix(&format!("agent://{}", agent_id)).await?;
|
|
|
|
let mut stats = GrowthStats::default();
|
|
stats.total_memories = memories.len();
|
|
|
|
for memory in &memories {
|
|
match memory.memory_type {
|
|
MemoryType::Preference => stats.preference_count += 1,
|
|
MemoryType::Knowledge => stats.knowledge_count += 1,
|
|
MemoryType::Experience => stats.experience_count += 1,
|
|
MemoryType::Session => stats.sessions_processed += 1,
|
|
}
|
|
}
|
|
|
|
// Get last learning time from metadata
|
|
let meta: Option<AgentMetadata> = self.viking
|
|
.get_metadata(&format!("agent://{}", agent_id))
|
|
.await?;
|
|
|
|
if let Some(meta) = meta {
|
|
stats.last_learning_time = meta.last_learning_time;
|
|
}
|
|
|
|
Ok(stats)
|
|
}
|
|
|
|
/// Record a learning event
|
|
pub async fn record_learning(
|
|
&self,
|
|
agent_id: &AgentId,
|
|
session_id: &str,
|
|
memories_extracted: usize,
|
|
) -> Result<()> {
|
|
let event = LearningEvent {
|
|
agent_id: agent_id.to_string(),
|
|
session_id: session_id.to_string(),
|
|
memories_extracted,
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
// Store learning event as MemoryEntry so get_timeline can find it via find_by_prefix
|
|
let event_uri = format!("agent://{}/events/{}", agent_id, session_id);
|
|
let content = serde_json::to_string(&event)?;
|
|
let entry = crate::types::MemoryEntry {
|
|
uri: event_uri,
|
|
memory_type: MemoryType::Session,
|
|
content,
|
|
keywords: vec![agent_id.to_string(), session_id.to_string()],
|
|
importance: 5,
|
|
access_count: 0,
|
|
created_at: event.timestamp,
|
|
last_accessed: event.timestamp,
|
|
overview: None,
|
|
abstract_summary: None,
|
|
};
|
|
self.viking.store(&entry).await?;
|
|
|
|
// Update last learning time via metadata
|
|
self.viking
|
|
.store_metadata(
|
|
&format!("agent://{}", agent_id),
|
|
&AgentMetadata {
|
|
last_learning_time: Some(Utc::now()),
|
|
total_learning_events: None,
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
tracing::info!(
|
|
"[GrowthTracker] Recorded learning event: agent={}, session={}, memories={}",
|
|
agent_id,
|
|
session_id,
|
|
memories_extracted
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get growth timeline for an agent
|
|
pub async fn get_timeline(&self, agent_id: &AgentId) -> Result<Vec<LearningEvent>> {
|
|
let memories = self
|
|
.viking
|
|
.find_by_prefix(&format!("agent://{}/events/", agent_id))
|
|
.await?;
|
|
|
|
// Parse events from stored memory content
|
|
let mut timeline = Vec::new();
|
|
for memory in memories {
|
|
if let Ok(event) = serde_json::from_str::<LearningEvent>(&memory.content) {
|
|
timeline.push(event);
|
|
}
|
|
}
|
|
|
|
// Sort by timestamp descending
|
|
timeline.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
|
|
|
Ok(timeline)
|
|
}
|
|
|
|
/// Calculate growth velocity (memories per day)
|
|
pub async fn get_growth_velocity(&self, agent_id: &AgentId) -> Result<f64> {
|
|
let timeline = self.get_timeline(agent_id).await?;
|
|
|
|
if timeline.is_empty() {
|
|
return Ok(0.0);
|
|
}
|
|
|
|
// Get first and last event
|
|
let first = timeline.iter().min_by_key(|e| e.timestamp);
|
|
let last = timeline.iter().max_by_key(|e| e.timestamp);
|
|
|
|
match (first, last) {
|
|
(Some(first), Some(last)) => {
|
|
let days = (last.timestamp - first.timestamp).num_days().max(1) as f64;
|
|
let total_memories: usize = timeline.iter().map(|e| e.memories_extracted).sum();
|
|
Ok(total_memories as f64 / days)
|
|
}
|
|
_ => Ok(0.0),
|
|
}
|
|
}
|
|
|
|
/// Get memory distribution by category
|
|
pub async fn get_memory_distribution(
|
|
&self,
|
|
agent_id: &AgentId,
|
|
) -> Result<HashMap<String, usize>> {
|
|
let memories = self.viking.find_by_prefix(&format!("agent://{}", agent_id)).await?;
|
|
|
|
let mut distribution = HashMap::new();
|
|
for memory in memories {
|
|
*distribution.entry(memory.memory_type.to_string()).or_insert(0) += 1;
|
|
}
|
|
|
|
Ok(distribution)
|
|
}
|
|
}
|
|
|
|
/// Learning event record
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct LearningEvent {
|
|
/// Agent ID
|
|
pub agent_id: String,
|
|
/// Session ID where learning occurred
|
|
pub session_id: String,
|
|
/// Number of memories extracted
|
|
pub memories_extracted: usize,
|
|
/// Event timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
/// Agent metadata stored in OpenViking
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AgentMetadata {
|
|
/// Last learning time
|
|
pub last_learning_time: Option<DateTime<Utc>>,
|
|
/// Total learning events (computed)
|
|
pub total_learning_events: Option<usize>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_learning_event_serialization() {
|
|
let event = LearningEvent {
|
|
agent_id: "test-agent".to_string(),
|
|
session_id: "test-session".to_string(),
|
|
memories_extracted: 5,
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let json = serde_json::to_string(&event).unwrap();
|
|
let parsed: LearningEvent = serde_json::from_str(&json).unwrap();
|
|
|
|
assert_eq!(parsed.agent_id, event.agent_id);
|
|
assert_eq!(parsed.memories_extracted, event.memories_extracted);
|
|
}
|
|
|
|
#[test]
|
|
fn test_agent_metadata_serialization() {
|
|
let meta = AgentMetadata {
|
|
last_learning_time: Some(Utc::now()),
|
|
total_learning_events: Some(10),
|
|
};
|
|
|
|
let json = serde_json::to_string(&meta).unwrap();
|
|
let parsed: AgentMetadata = serde_json::from_str(&json).unwrap();
|
|
|
|
assert!(parsed.last_learning_time.is_some());
|
|
assert_eq!(parsed.total_learning_events, Some(10));
|
|
}
|
|
}
|