//! Vector-based skill and memory awareness using Qdrant embeddings. //! //! Leverages semantic similarity search to find relevant skills and conversation //! memories based on vector embeddings. This is more powerful than keyword matching //! because it captures semantic meaning, not just surface-level word overlap. //! //! - **VectorActiveAwareness**: Searches skills by semantic similarity when the user //! sends a message, finding skills relevant to the conversation topic. //! //! - **VectorPassiveAwareness**: Searches past conversation memories to provide relevant //! historical context when similar topics arise, based on tool-call patterns. use async_openai::types::chat::{ ChatCompletionRequestMessage, ChatCompletionRequestSystemMessage, ChatCompletionRequestSystemMessageContent, }; use crate::embed::EmbedService; use crate::perception::SkillContext; /// Maximum relevant memories to inject. const MAX_MEMORY_RESULTS: usize = 3; /// Minimum similarity score (0.0–1.0) for memories. const MIN_MEMORY_SCORE: f32 = 0.72; /// Maximum skills to return from vector search. const MAX_SKILL_RESULTS: usize = 3; /// Minimum similarity score for skills. const MIN_SKILL_SCORE: f32 = 0.70; /// Vector-based active skill awareness — semantic search for relevant skills. /// /// When the user sends a message, this awareness mode searches the Qdrant skill index /// for skills whose content is semantically similar to the message, even if no keywords /// match directly. This captures intent beyond explicit skill mentions. #[derive(Debug, Clone)] pub struct VectorActiveAwareness { pub max_skills: usize, pub min_score: f32, } impl Default for VectorActiveAwareness { fn default() -> Self { Self { max_skills: MAX_SKILL_RESULTS, min_score: MIN_SKILL_SCORE, } } } impl VectorActiveAwareness { /// Search for skills semantically relevant to the user's input. /// /// Uses Qdrant vector search within the given project to find skills whose /// embedded content is similar to `query`. Only returns results above `min_score`. pub async fn detect( &self, embed_service: &EmbedService, query: &str, project_uuid: &str, ) -> Vec { let results = match embed_service .search_skills(query, project_uuid, self.max_skills) .await { Ok(results) => results, Err(_) => return Vec::new(), }; results .into_iter() .filter(|r| r.score >= self.min_score) .map(|r| { let name = r .payload .extra .as_ref() .and_then(|v| v.get("name")) .and_then(|v| v.as_str()) .unwrap_or("skill") .to_string(); SkillContext { label: format!("[Vector] Skill: {}", name), content: format!( "[Relevant skill (score {:.2})]\n{}", r.score, r.payload.text ), } }) .collect() } } /// Vector-based passive memory awareness — retrieve relevant past context. /// /// When the agent encounters a topic (via tool-call or observation), this awareness /// searches past conversation messages to find semantically similar prior discussions. /// This gives the agent memory of how similar situations were handled before. #[derive(Debug, Clone)] pub struct VectorPassiveAwareness { pub max_memories: usize, pub min_score: f32, } impl Default for VectorPassiveAwareness { fn default() -> Self { Self { max_memories: MAX_MEMORY_RESULTS, min_score: MIN_MEMORY_SCORE, } } } impl VectorPassiveAwareness { /// Search for past conversation messages semantically similar to the current context. /// /// Uses Qdrant to find memories within the same room that share semantic similarity /// with the given query (usually the current input or a tool-call description). /// High-scoring results suggest prior discussions on this topic. pub async fn detect( &self, embed_service: &EmbedService, query: &str, room_id: &str, ) -> Vec { let results = match embed_service .search_memories(query, room_id, self.max_memories) .await { Ok(results) => results, Err(_) => return Vec::new(), }; results .into_iter() .filter(|r| r.score >= self.min_score) .map(|r| MemoryContext { score: r.score, content: r.payload.text, }) .collect() } } /// A retrieved memory entry from vector search. #[derive(Debug, Clone)] pub struct MemoryContext { /// Similarity score (0.0–1.0). pub score: f32, /// The text of the past conversation message. pub content: String, } impl MemoryContext { /// Format as a system message for injection into the agent context. pub fn to_system_message(self) -> ChatCompletionRequestMessage { ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { content: ChatCompletionRequestSystemMessageContent::Text(format!( "[Relevant memory (score {:.2})]\n{}", self.score, self.content )), ..Default::default() }) } }