164 lines
5.5 KiB
Rust
164 lines
5.5 KiB
Rust
//! 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<SkillContext> {
|
||
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<MemoryContext> {
|
||
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()
|
||
})
|
||
}
|
||
}
|