use std::sync::Arc;
use async_trait::async_trait;
use converge_pack::{
AgentEffect, Context, ContextFact, ContextKey, ExecutionIdentity, FactPayload,
ProvenanceSource, Suggestor, TextPayload,
};
use serde::{Deserialize, Serialize};
use crate::core::{KnowledgeBase, KnowledgeEntry, SearchOptions};
use crate::provenance::MNEMOS_PROVENANCE;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KnowledgeHitPayload {
pub source: String,
pub query: String,
pub title: String,
pub content: String,
pub score: f32,
pub execution_identity: ExecutionIdentity,
}
impl FactPayload for KnowledgeHitPayload {
const FAMILY: &'static str = "mnemos.knowledge.hit";
const VERSION: u16 = 2;
}
const MNEMOS_BACKEND: &str = "mnemos-knowledge-base-v1";
#[derive(serde::Serialize)]
struct MnemosRetrievalConfig<'a> {
query: &'a str,
max_results: usize,
}
fn retrieval_execution_identity(query: &str, max_results: usize) -> ExecutionIdentity {
let config = MnemosRetrievalConfig { query, max_results };
ExecutionIdentity::non_native(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
MNEMOS_BACKEND,
ExecutionIdentity::runtime_config_from_typed(&config),
)
}
pub struct KnowledgeRetrievalSuggestor {
kb: Arc<KnowledgeBase>,
max_results: usize,
}
impl KnowledgeRetrievalSuggestor {
pub fn new(kb: Arc<KnowledgeBase>) -> Self {
Self { kb, max_results: 5 }
}
pub fn with_max_results(mut self, n: usize) -> Self {
self.max_results = n;
self
}
}
#[async_trait]
impl Suggestor for KnowledgeRetrievalSuggestor {
fn name(&self) -> &'static str {
"knowledge-retrieval"
}
fn dependencies(&self) -> &[ContextKey] {
&[ContextKey::Seeds]
}
fn accepts(&self, ctx: &dyn Context) -> bool {
ctx.has(ContextKey::Seeds) && !ctx.has(ContextKey::Hypotheses)
}
fn provenance(&self) -> &'static str {
MNEMOS_PROVENANCE.as_str()
}
async fn execute(&self, ctx: &dyn Context) -> AgentEffect {
async move {
let seeds = ctx.get(ContextKey::Seeds);
let mut proposals = Vec::new();
for seed in seeds {
let Some(query) = seed.text() else {
continue;
};
let options = SearchOptions {
limit: self.max_results,
..SearchOptions::default()
};
if let Ok(results) = self.kb.search(query, options).await {
let identity = retrieval_execution_identity(query, self.max_results);
for (i, result) in results.into_iter().enumerate() {
let payload = KnowledgeHitPayload {
source: "knowledge-base".to_string(),
query: query.to_string(),
title: result.entry.title,
content: result.entry.content,
score: result.score,
execution_identity: identity.clone(),
};
proposals.push(
MNEMOS_PROVENANCE
.proposed_fact(
ContextKey::Hypotheses,
format!("kb-{}-{}", seed.id(), i),
payload,
)
.with_confidence(f64::from(result.score)),
);
}
}
}
AgentEffect::with_proposals(proposals)
}
.await
}
}
pub struct KnowledgeStoreSuggestor {
kb: Arc<KnowledgeBase>,
}
impl KnowledgeStoreSuggestor {
pub fn new(kb: Arc<KnowledgeBase>) -> Self {
Self { kb }
}
}
#[async_trait]
impl Suggestor for KnowledgeStoreSuggestor {
fn name(&self) -> &'static str {
"knowledge-store"
}
fn dependencies(&self) -> &[ContextKey] {
&[ContextKey::Evaluations]
}
fn accepts(&self, ctx: &dyn Context) -> bool {
ctx.has(ContextKey::Evaluations)
&& !ctx
.get(ContextKey::Seeds)
.iter()
.any(|f| f.id().starts_with("stored-"))
}
fn provenance(&self) -> &'static str {
MNEMOS_PROVENANCE.as_str()
}
async fn execute(&self, ctx: &dyn Context) -> AgentEffect {
async move {
let evaluations = ctx.get(ContextKey::Evaluations);
let mut proposals = Vec::new();
for eval in evaluations {
let Some(content) = fact_content_for_store(eval) else {
continue;
};
let entry = KnowledgeEntry::new(eval.id().as_str(), content)
.with_category("convergence-result")
.with_tags(vec!["auto-stored", "formation-output"]);
if self.kb.add_entry(entry).await.is_ok() {
proposals.push(MNEMOS_PROVENANCE.proposed_fact(
ContextKey::Seeds,
format!("stored-{}", eval.id()),
TextPayload::new(format!(
"stored evaluation {} in knowledge base",
eval.id()
)),
));
}
}
AgentEffect::with_proposals(proposals)
}
.await
}
}
fn fact_content_for_store(fact: &ContextFact) -> Option<String> {
fact.text().map(str::to_string).or_else(|| {
fact.to_wire()
.ok()
.map(|wire| wire.payload.payload.to_string())
})
}