gitdataai/libs/agent/perception/vector.rs
2026-04-14 19:02:01 +08:00

164 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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.01.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.01.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()
})
}
}