use hirn_core::HirnResult;
use hirn_core::timestamp::Timestamp;
use hirn_core::types::{AgentId, Namespace};
use crate::activation::ActivationMode;
use crate::db::HirnDB;
use crate::ql::context::{ContextConfig, ContextFormat, ThinkResult};
use crate::ql::results::ScoredMemory;
use crate::recall::LayerFilter;
use crate::retrieval::explanation::{ThinkExplanation, build_think_explanation};
use crate::scoring::ScoringWeights;
pub struct ThinkBuilder<'a> {
db: &'a HirnDB,
actor_id: AgentId,
query: Vec<f32>,
query_text: Option<String>,
hybrid: bool,
limit: usize,
threshold: Option<f32>,
layer_filter: LayerFilter,
namespace: Option<Namespace>,
after: Option<Timestamp>,
before: Option<Timestamp>,
weights: Option<ScoringWeights>,
activation_mode: ActivationMode,
activation_depth: Option<usize>,
context_config: Option<ContextConfig>,
budget: Option<usize>,
format: Option<ContextFormat>,
}
impl<'a> ThinkBuilder<'a> {
pub(crate) fn new(db: &'a HirnDB, query_embedding: Vec<f32>) -> Self {
Self {
db,
actor_id: AgentId::well_known("anonymous"),
query: query_embedding,
query_text: None,
hybrid: false,
limit: 50,
threshold: None,
layer_filter: LayerFilter::All,
namespace: None,
after: None,
before: None,
weights: None,
activation_mode: ActivationMode::None,
activation_depth: None,
context_config: None,
budget: None,
format: None,
}
}
pub fn limit(mut self, k: usize) -> Self {
self.limit = k;
self
}
pub fn threshold(mut self, min: f32) -> Self {
self.threshold = Some(min);
self
}
pub fn query_text(mut self, text: impl Into<String>) -> Self {
self.query_text = Some(text.into());
self.hybrid = true;
self
}
pub fn hybrid(mut self, enable: bool) -> Self {
self.hybrid = enable;
self
}
pub fn episodic_only(mut self) -> Self {
self.layer_filter = LayerFilter::EpisodicOnly;
self
}
pub fn semantic_only(mut self) -> Self {
self.layer_filter = LayerFilter::SemanticOnly;
self
}
pub fn namespace(mut self, ns: Namespace) -> Self {
self.namespace = Some(ns);
self
}
pub fn agent_id(mut self, actor_id: AgentId) -> Self {
self.actor_id = actor_id;
self
}
pub fn after(mut self, ts: Timestamp) -> Self {
self.after = Some(ts);
self
}
pub fn before(mut self, ts: Timestamp) -> Self {
self.before = Some(ts);
self
}
pub fn weights(mut self, w: ScoringWeights) -> Self {
self.weights = Some(w);
self
}
pub fn activation(mut self, mode: ActivationMode) -> Self {
self.activation_mode = mode;
self
}
pub fn depth(mut self, d: usize) -> Self {
self.activation_depth = Some(d);
self
}
pub fn budget(mut self, tokens: usize) -> Self {
self.budget = Some(tokens);
self
}
pub fn format(mut self, fmt: ContextFormat) -> Self {
self.format = Some(fmt);
self
}
pub fn preview_package_limits(mut self, max_previews: usize, max_chars: usize) -> Self {
let mut config = self
.context_config
.unwrap_or_else(|| ContextConfig::from_hirn_config(self.db.config()));
config.max_resource_previews_per_entry = max_previews;
config.max_resource_preview_chars = max_chars;
self.context_config = Some(config);
self
}
pub fn context_config(mut self, config: ContextConfig) -> Self {
self.context_config = Some(config);
self
}
pub async fn execute(self) -> HirnResult<ThinkResult> {
self.execute_with_explanation()
.await
.map(|(result, _)| result)
}
pub async fn execute_with_explanation(self) -> HirnResult<(ThinkResult, ThinkExplanation)> {
let start = std::time::Instant::now();
let mut recall = self
.db
.recall(self.query)
.limit(self.limit)
.agent_id(self.actor_id.as_str());
if let Some(threshold) = self.threshold {
recall = recall.threshold(threshold);
}
recall = match self.layer_filter {
LayerFilter::EpisodicOnly => recall.episodic_only(),
LayerFilter::SemanticOnly => recall.semantic_only(),
LayerFilter::ProceduralOnly => recall.procedural_only(),
LayerFilter::All => recall,
};
if let Some(namespace) = self.namespace {
recall = recall.namespace(namespace);
}
if let Some(after) = self.after {
recall = recall.after(after);
}
if let Some(before) = self.before {
recall = recall.before(before);
}
if let Some(weights) = self.weights {
recall = recall.weights(weights);
}
recall = recall.activation(self.activation_mode);
if let Some(depth) = self.activation_depth {
recall = recall.depth(depth);
}
if let Some(query_text) = self.query_text {
recall = recall.query_text(query_text);
}
recall = recall.hybrid(self.hybrid);
let (recall_results, retrieval_explanation) = recall.execute_with_explanation().await?;
let scored: Vec<ScoredMemory> = recall_results
.into_iter()
.map(|rr| ScoredMemory {
record: rr.record,
revision: rr.revision,
score: rr.composite_score,
score_breakdown: rr.score_breakdown,
resource_evidence: rr.resource_evidence,
resource_preview_packages: rr.resource_preview_packages,
resource_score_attribution: rr.resource_score_attribution,
})
.collect();
let mut config = self
.context_config
.unwrap_or_else(|| ContextConfig::from_hirn_config(self.db.config()));
if let Some(budget) = self.budget {
config.token_budget = budget;
}
if let Some(fmt) = self.format {
config.output_format = fmt;
}
let visible_namespaces = self.namespace.as_ref().map(std::slice::from_ref);
let mut result = crate::ql::context::assemble_think_context(
self.db,
&self.actor_id,
&scored,
&config,
visible_namespaces,
None,
None,
)
.await?;
result.query_time_ms = start.elapsed().as_secs_f64() * 1000.0;
let explanation =
build_think_explanation(retrieval_explanation, &result, config.token_budget);
Ok((result, explanation))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::SemanticUpdate;
use hirn_core::semantic::SemanticRecord;
use hirn_core::types::{AgentId, KnowledgeType, Namespace, Origin};
use tempfile::tempdir;
async fn test_db() -> HirnDB {
let dir = tempdir().unwrap();
let db_path = dir.path().join("db");
let lance_path = dir.path().join("lance");
let mut config = hirn_core::HirnConfig::default();
config.db_path = db_path;
config.embedding_dimensions = hirn_core::EmbeddingDimension::new_const(3);
let storage: std::sync::Arc<dyn hirn_storage::PhysicalStore> = hirn_storage::HirnDb::open(
hirn_storage::HirnDbConfig::local(lance_path.to_str().unwrap()),
)
.await
.unwrap()
.store_arc();
let db = HirnDB::open_with_config(config, storage).await.unwrap();
std::mem::forget(dir);
db
}
#[tokio::test(flavor = "multi_thread")]
async fn think_uses_current_semantic_heads() {
let db = test_db().await;
let agent = AgentId::new("think-test").unwrap();
let namespace = Namespace::new("think-current").unwrap();
let original = SemanticRecord::builder()
.concept("deployment-strategy")
.knowledge_type(KnowledgeType::Propositional)
.description("outdated rollout plan")
.embedding(vec![1.0, 0.0, 0.0])
.confidence(0.8)
.agent_id(agent)
.origin(Origin::Consolidation)
.namespace(namespace)
.build()
.unwrap();
let original_id = db.store_semantic(original).await.unwrap();
db.semantic()
.correct(
original_id,
SemanticUpdate {
description: Some("current rollout plan".into()),
reason: Some("refresh current semantic head for think()".into()),
..SemanticUpdate::with_metadata(agent, original_id)
},
)
.await
.unwrap();
let result = db
.recall_view()
.think(vec![1.0, 0.0, 0.0])
.semantic_only()
.namespace(namespace)
.limit(5)
.execute()
.await
.unwrap();
assert!(
result.context.contains("current rollout plan"),
"think() should use the current semantic head"
);
assert!(
!result.context.contains("outdated rollout plan"),
"think() should not expose superseded semantic content"
);
}
}