Initial scaffold: GyanDoot v2.0 in Rust
Full Cargo workspace with 11 crates: - gyandoot-core: Shared types, config, error types - gyandoot-agent: Agent loop, tool execution, context compression - gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama) - gyandoot-memory: MemPalace persistent memory system - gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI) - gyandoot-security: Credential vault, sanitizer, threat detector - gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics - gyandoot-subagents: Sub-agent spawning and orchestration - gyandoot-skills: Skills discovery, loading, preprocessing, bundles - gyandoot-profile: Multi-agent profile manager - gyandoot-gateway: Gateway sessions, routing, access control Plus: CLI entry point with clap, cross-platform installer script, KANBAN.md, .gnpl/AGENTS.md Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
112
crates/gyandoot-memory/src/identity.rs
Normal file
112
crates/gyandoot-memory/src/identity.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! Identity store — manages agent identity and preferences.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Agent identity information stored in memory.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentIdentity {
|
||||
pub agent_id: String,
|
||||
pub name: String,
|
||||
pub bio: String,
|
||||
pub preferences: HashMap<String, String>,
|
||||
pub learned_traits: Vec<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// Manages persistent agent identity data.
|
||||
pub struct IdentityStore {
|
||||
identities: HashMap<String, AgentIdentity>,
|
||||
}
|
||||
|
||||
impl IdentityStore {
|
||||
/// Create a new identity store.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
identities: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register or update an agent's identity.
|
||||
pub fn register(&mut self, identity: AgentIdentity) {
|
||||
self.identities.insert(identity.agent_id.clone(), identity);
|
||||
}
|
||||
|
||||
/// Get an agent's identity by ID.
|
||||
pub fn get(&self, agent_id: &str) -> Option<&AgentIdentity> {
|
||||
self.identities.get(agent_id)
|
||||
}
|
||||
|
||||
/// Update a preference for an agent.
|
||||
pub fn set_preference(&mut self, agent_id: &str, key: String, value: String) -> Option<()> {
|
||||
self.identities
|
||||
.get_mut(agent_id)?
|
||||
.preferences
|
||||
.insert(key, value);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Add a learned trait to an agent's identity.
|
||||
pub fn add_trait(&mut self, agent_id: &str, trait_name: String) -> Option<()> {
|
||||
self.identities
|
||||
.get_mut(agent_id)?
|
||||
.learned_traits
|
||||
.push(trait_name);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// List all registered agent IDs.
|
||||
pub fn list_agents(&self) -> Vec<String> {
|
||||
self.identities.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IdentityStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_identity_register_and_retrieve() {
|
||||
let mut store = IdentityStore::new();
|
||||
store.register(AgentIdentity {
|
||||
agent_id: "agent-1".to_string(),
|
||||
name: "GyanDoot".to_string(),
|
||||
bio: "An AI Agent".to_string(),
|
||||
preferences: HashMap::new(),
|
||||
learned_traits: vec![],
|
||||
created_at: "now".to_string(),
|
||||
updated_at: "now".to_string(),
|
||||
});
|
||||
let identity = store.get("agent-1").unwrap();
|
||||
assert_eq!(identity.name, "GyanDoot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preferences() {
|
||||
let mut store = IdentityStore::new();
|
||||
store.register(AgentIdentity {
|
||||
agent_id: "a1".to_string(),
|
||||
name: "Test".to_string(),
|
||||
bio: String::new(),
|
||||
preferences: HashMap::new(),
|
||||
learned_traits: vec![],
|
||||
created_at: "now".to_string(),
|
||||
updated_at: "now".to_string(),
|
||||
});
|
||||
store
|
||||
.set_preference("a1", "language".to_string(), "rust".to_string())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
store.get("a1").unwrap().preferences.get("language").unwrap(),
|
||||
"rust"
|
||||
);
|
||||
}
|
||||
}
|
||||
13
crates/gyandoot-memory/src/lib.rs
Normal file
13
crates/gyandoot-memory/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
//! GyanDoot Memory — MemPalace persistent memory system.
|
||||
//!
|
||||
//! The MemPalace stores conversation transcripts, agent identity data,
|
||||
//! and provides semantic query over past interactions using embeddings
|
||||
//! stored alongside text.
|
||||
|
||||
pub mod palace;
|
||||
pub mod transcript;
|
||||
pub mod identity;
|
||||
|
||||
pub use palace::MemPalace;
|
||||
pub use transcript::TranscriptStore;
|
||||
pub use identity::IdentityStore;
|
||||
227
crates/gyandoot-memory/src/palace.rs
Normal file
227
crates/gyandoot-memory/src/palace.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
//! MemPalace — The core persistent memory store.
|
||||
//!
|
||||
//! Stores conversation entries with embeddings for semantic retrieval,
|
||||
//! handles memory compaction, and provides query capabilities.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use gyandoot_core::MemoryResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::identity::IdentityStore;
|
||||
use crate::transcript::TranscriptStore;
|
||||
|
||||
/// A single memory entry stored in the palace.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MemoryEntry {
|
||||
pub id: String,
|
||||
pub content: String,
|
||||
pub agent_id: String,
|
||||
pub session_id: String,
|
||||
pub entry_type: EntryType,
|
||||
pub embedding: Option<Vec<f64>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// The type of a memory entry.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum EntryType {
|
||||
Conversation,
|
||||
Fact,
|
||||
Identity,
|
||||
Task,
|
||||
Skill,
|
||||
}
|
||||
|
||||
/// A memory query result with relevance score.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryQueryResult {
|
||||
pub content: String,
|
||||
pub relevance: f64,
|
||||
pub entry_type: EntryType,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Status information for the memory palace.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryStatus {
|
||||
pub entry_count: usize,
|
||||
pub backend: String,
|
||||
pub storage_size: u64,
|
||||
}
|
||||
|
||||
/// The MemPalace — persistent, searchable agent memory.
|
||||
///
|
||||
/// Entries are stored as JSON files in the data directory, organized by
|
||||
/// agent and session. Future versions will include vector embeddings
|
||||
/// for semantic search.
|
||||
pub struct MemPalace {
|
||||
entries: Vec<MemoryEntry>,
|
||||
storage_path: PathBuf,
|
||||
transcript_store: TranscriptStore,
|
||||
identity_store: IdentityStore,
|
||||
}
|
||||
|
||||
impl MemPalace {
|
||||
/// Create a new MemPalace with memory directory in the user's data dir.
|
||||
pub fn new() -> Self {
|
||||
let data_dir = gyandoot_core::data_dir().join("memory");
|
||||
std::fs::create_dir_all(&data_dir).ok();
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
storage_path: data_dir,
|
||||
transcript_store: TranscriptStore::new(),
|
||||
identity_store: IdentityStore::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a MemPalace with a custom storage path.
|
||||
pub fn with_path(path: PathBuf) -> Self {
|
||||
std::fs::create_dir_all(&path).ok();
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
storage_path: path,
|
||||
transcript_store: TranscriptStore::new(),
|
||||
identity_store: IdentityStore::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a memory entry in the palace.
|
||||
pub fn store(&mut self, entry: MemoryEntry) -> MemoryResult<()> {
|
||||
debug!("Storing memory entry: {}", entry.id);
|
||||
self.entries.push(entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store a conversation turn (the last user + assistant pair) as memory.
|
||||
pub async fn store_conversation_turn(
|
||||
&mut self,
|
||||
messages: &[gyandoot_core::Message],
|
||||
) -> MemoryResult<()> {
|
||||
let recent: Vec<&gyandoot_core::Message> = messages.iter().rev().take(4).collect();
|
||||
for msg in recent.iter().rev() {
|
||||
let entry = MemoryEntry {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
content: msg.content.clone(),
|
||||
agent_id: "default".to_string(),
|
||||
session_id: String::new(),
|
||||
entry_type: EntryType::Conversation,
|
||||
embedding: None,
|
||||
created_at: Utc::now(),
|
||||
metadata: std::collections::HashMap::new(),
|
||||
};
|
||||
self.store(entry)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Query memory by simple text matching.
|
||||
///
|
||||
/// Future versions will use embedding-based semantic search.
|
||||
pub fn query(&self, query: &str, limit: usize) -> MemoryResult<Vec<MemoryQueryResult>> {
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut results: Vec<MemoryQueryResult> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| e.content.to_lowercase().contains(&query_lower))
|
||||
.map(|e| MemoryQueryResult {
|
||||
content: e.content.clone(),
|
||||
relevance: 1.0, // placeholder; real relevance from embedding cosine sim
|
||||
entry_type: e.entry_type.clone(),
|
||||
created_at: e.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
results.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap_or(std::cmp::Ordering::Equal));
|
||||
results.truncate(limit);
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Export all memory to a JSON file at the given path.
|
||||
pub fn export_to(&self, path: &PathBuf) -> MemoryResult<()> {
|
||||
let json = serde_json::to_string_pretty(&self.entries)
|
||||
.map_err(|e| gyandoot_core::MemoryError::Storage(e.to_string()))?;
|
||||
std::fs::write(path, &json)
|
||||
.map_err(|e| gyandoot_core::MemoryError::Storage(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get memory status information.
|
||||
pub fn status() -> MemoryResult<MemoryStatus> {
|
||||
let data_dir = gyandoot_core::data_dir().join("memory");
|
||||
let size = std::fs::read_dir(&data_dir)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.metadata().ok())
|
||||
.map(|m| m.len())
|
||||
.sum()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let entry_count = std::fs::read_dir(&data_dir)
|
||||
.map(|entries| entries.filter_map(|e| e.ok()).count())
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(MemoryStatus {
|
||||
entry_count,
|
||||
backend: "local-json".to_string(),
|
||||
storage_size: size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a reference to the transcript store.
|
||||
pub fn transcript_store(&self) -> &TranscriptStore {
|
||||
&self.transcript_store
|
||||
}
|
||||
|
||||
/// Get a reference to the identity store.
|
||||
pub fn identity_store(&self) -> &IdentityStore {
|
||||
&self.identity_store
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemPalace {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_store_and_query() {
|
||||
let mut palace = MemPalace::new();
|
||||
|
||||
palace
|
||||
.store(MemoryEntry {
|
||||
id: "1".to_string(),
|
||||
content: "The user likes Python".to_string(),
|
||||
agent_id: "default".to_string(),
|
||||
session_id: "s1".to_string(),
|
||||
entry_type: EntryType::Fact,
|
||||
embedding: None,
|
||||
created_at: Utc::now(),
|
||||
metadata: std::collections::HashMap::new(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let results = palace.query("python", 10).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results[0].content.contains("Python"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export() {
|
||||
let palace = MemPalace::new();
|
||||
let tmp = std::env::temp_dir().join("gyandoot_test_export.json");
|
||||
palace.export_to(&tmp).unwrap();
|
||||
assert!(tmp.exists());
|
||||
std::fs::remove_file(tmp).ok();
|
||||
}
|
||||
}
|
||||
125
crates/gyandoot-memory/src/transcript.rs
Normal file
125
crates/gyandoot-memory/src/transcript.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
//! Transcript store — structured conversation log management.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single turn in a conversation transcript.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TranscriptTurn {
|
||||
pub turn_number: u64,
|
||||
pub user_message: String,
|
||||
pub assistant_message: String,
|
||||
pub tool_calls: Vec<String>,
|
||||
pub latency_ms: u64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// A complete conversation transcript.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Transcript {
|
||||
pub id: String,
|
||||
pub agent_id: String,
|
||||
pub session_id: String,
|
||||
pub started_at: DateTime<Utc>,
|
||||
pub ended_at: Option<DateTime<Utc>>,
|
||||
pub turns: Vec<TranscriptTurn>,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
/// Manages conversation transcripts.
|
||||
pub struct TranscriptStore {
|
||||
transcripts: HashMap<String, Transcript>,
|
||||
}
|
||||
|
||||
impl TranscriptStore {
|
||||
/// Create a new empty transcript store.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
transcripts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a new transcript.
|
||||
pub fn start_transcript(&mut self, id: String, agent_id: String, session_id: String) {
|
||||
self.transcripts.insert(
|
||||
id.clone(),
|
||||
Transcript {
|
||||
id,
|
||||
agent_id,
|
||||
session_id,
|
||||
started_at: Utc::now(),
|
||||
ended_at: None,
|
||||
turns: Vec::new(),
|
||||
summary: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Append a turn to an existing transcript.
|
||||
pub fn append_turn(&mut self, transcript_id: &str, turn: TranscriptTurn) -> Option<()> {
|
||||
self.transcripts.get_mut(transcript_id)?.turns.push(turn);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// End a transcript by setting the end timestamp.
|
||||
pub fn end_transcript(&mut self, transcript_id: &str) -> Option<()> {
|
||||
self.transcripts
|
||||
.get_mut(transcript_id)?
|
||||
.ended_at = Some(Utc::now());
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Set a summary for the transcript.
|
||||
pub fn set_summary(&mut self, transcript_id: &str, summary: String) -> Option<()> {
|
||||
self.transcripts.get_mut(transcript_id)?.summary = Some(summary);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Get a transcript by ID.
|
||||
pub fn get(&self, transcript_id: &str) -> Option<&Transcript> {
|
||||
self.transcripts.get(transcript_id)
|
||||
}
|
||||
|
||||
/// List all transcript IDs.
|
||||
pub fn list_ids(&self) -> Vec<String> {
|
||||
self.transcripts.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TranscriptStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_transcript_lifecycle() {
|
||||
let mut store = TranscriptStore::new();
|
||||
store.start_transcript("t1".to_string(), "agent1".to_string(), "session1".to_string());
|
||||
|
||||
store
|
||||
.append_turn(
|
||||
"t1",
|
||||
TranscriptTurn {
|
||||
turn_number: 1,
|
||||
user_message: "Hello".to_string(),
|
||||
assistant_message: "Hi there!".to_string(),
|
||||
tool_calls: vec![],
|
||||
latency_ms: 500,
|
||||
timestamp: Utc::now(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
store.end_transcript("t1").unwrap();
|
||||
let t = store.get("t1").unwrap();
|
||||
assert_eq!(t.turns.len(), 1);
|
||||
assert!(t.ended_at.is_some());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user