use crate::cognition::drive::InternalDrive;
use crate::cognition::knowledge::{KnowledgeIndex, KnowledgeSource, ingest as knowledge_ingest};
use crate::cognition::learning::engine::LearningEngine;
use crate::cognition::learning::q_table::{ActionKey, QTable};
use crate::cognition::learning::store::LearningStore;
use crate::traits::agent::Agent;
use crate::types::{
Capability, ContextManager, Knowledge, Message, Planner, Profile, Reflection, Status, Task,
TaskScheduler, ThinkResult, Tool,
};
use anyhow::Result;
use lmm::predict::TextPredictor;
use std::borrow::Cow;
use std::collections::HashSet;
#[cfg(feature = "net")]
use duckduckgo::browser::Browser;
#[cfg(feature = "net")]
use duckduckgo::user_agents::get as get_ua;
use crate::cognition::r#loop::ThinkLoop;
use crate::cognition::search::SearchOracle;
#[derive(Debug, Clone, Default)]
pub struct LmmAgent {
pub id: String,
pub persona: String,
pub behavior: String,
pub status: Status,
pub memory: Vec<Message>,
pub long_term_memory: Vec<Message>,
pub knowledge: Knowledge,
pub tools: Vec<Tool>,
pub planner: Option<Planner>,
pub reflection: Option<Reflection>,
pub scheduler: Option<TaskScheduler>,
pub profile: Profile,
pub context: ContextManager,
pub capabilities: HashSet<Capability>,
pub tasks: Vec<Task>,
pub knowledge_index: KnowledgeIndex,
pub learning_engine: Option<LearningEngine>,
pub internal_drive: InternalDrive,
}
#[derive(Default)]
pub struct LmmAgentBuilder {
id: Option<String>,
persona: Option<String>,
behavior: Option<String>,
status: Option<Status>,
memory: Option<Vec<Message>>,
long_term_memory: Option<Vec<Message>>,
knowledge: Option<Knowledge>,
tools: Option<Vec<Tool>>,
planner: Option<Option<Planner>>,
reflection: Option<Option<Reflection>>,
scheduler: Option<Option<TaskScheduler>>,
profile: Option<Profile>,
context: Option<ContextManager>,
capabilities: Option<HashSet<Capability>>,
tasks: Option<Vec<Task>>,
knowledge_index: Option<KnowledgeIndex>,
learning_engine: Option<Option<LearningEngine>>,
internal_drive: Option<InternalDrive>,
}
impl LmmAgentBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn persona(mut self, persona: impl Into<String>) -> Self {
self.persona = Some(persona.into());
self
}
pub fn behavior(mut self, behavior: impl Into<String>) -> Self {
self.behavior = Some(behavior.into());
self
}
pub fn status(mut self, status: Status) -> Self {
self.status = Some(status);
self
}
pub fn memory(mut self, memory: Vec<Message>) -> Self {
self.memory = Some(memory);
self
}
pub fn long_term_memory(mut self, ltm: Vec<Message>) -> Self {
self.long_term_memory = Some(ltm);
self
}
pub fn knowledge(mut self, knowledge: Knowledge) -> Self {
self.knowledge = Some(knowledge);
self
}
pub fn tools(mut self, tools: Vec<Tool>) -> Self {
self.tools = Some(tools);
self
}
pub fn planner(mut self, planner: impl Into<Option<Planner>>) -> Self {
self.planner = Some(planner.into());
self
}
pub fn reflection(mut self, reflection: impl Into<Option<Reflection>>) -> Self {
self.reflection = Some(reflection.into());
self
}
pub fn scheduler(mut self, scheduler: impl Into<Option<TaskScheduler>>) -> Self {
self.scheduler = Some(scheduler.into());
self
}
pub fn profile(mut self, profile: Profile) -> Self {
self.profile = Some(profile);
self
}
pub fn context(mut self, context: ContextManager) -> Self {
self.context = Some(context);
self
}
pub fn capabilities(mut self, capabilities: HashSet<Capability>) -> Self {
self.capabilities = Some(capabilities);
self
}
pub fn tasks(mut self, tasks: Vec<Task>) -> Self {
self.tasks = Some(tasks);
self
}
pub fn knowledge_index(mut self, index: KnowledgeIndex) -> Self {
self.knowledge_index = Some(index);
self
}
pub fn learning_engine(mut self, engine: impl Into<Option<LearningEngine>>) -> Self {
self.learning_engine = Some(engine.into());
self
}
pub fn internal_drive(mut self, drive: InternalDrive) -> Self {
self.internal_drive = Some(drive);
self
}
pub fn build(self) -> LmmAgent {
let persona = self
.persona
.expect("LmmAgentBuilder: `persona` is required");
let behavior = self
.behavior
.expect("LmmAgentBuilder: `behavior` is required");
let profile = self.profile.unwrap_or_else(|| Profile {
name: behavior.clone().into(),
traits: vec![],
behavior_script: None,
});
LmmAgent {
id: self.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
persona,
behavior,
status: self.status.unwrap_or_default(),
memory: self.memory.unwrap_or_default(),
long_term_memory: self.long_term_memory.unwrap_or_default(),
knowledge: self.knowledge.unwrap_or_default(),
tools: self.tools.unwrap_or_default(),
planner: self.planner.unwrap_or_else(|| Some(Planner::default())),
reflection: self
.reflection
.unwrap_or_else(|| Some(Reflection::default())),
scheduler: self
.scheduler
.unwrap_or_else(|| Some(TaskScheduler::default())),
profile,
context: self.context.unwrap_or_default(),
capabilities: self.capabilities.unwrap_or_default(),
tasks: self.tasks.unwrap_or_default(),
knowledge_index: self.knowledge_index.unwrap_or_default(),
learning_engine: self.learning_engine.unwrap_or(None),
internal_drive: self.internal_drive.unwrap_or_default(),
}
}
}
impl LmmAgent {
pub fn builder() -> LmmAgentBuilder {
LmmAgentBuilder::default()
}
pub fn new(
persona: std::borrow::Cow<'static, str>,
behavior: std::borrow::Cow<'static, str>,
) -> Self {
LmmAgent::builder()
.persona(persona.into_owned())
.behavior(behavior.into_owned())
.build()
}
pub fn add_message(&mut self, message: Message) {
self.memory.push(message);
}
pub fn add_ltm_message(&mut self, message: Message) {
self.long_term_memory.push(message);
}
pub fn complete_goal(&mut self, description_substr: &str) -> bool {
if let Some(plan) = self.planner.as_mut() {
for goal in &mut plan.current_plan {
if goal.description.contains(description_substr) {
goal.completed = true;
return true;
}
}
}
false
}
pub async fn generate(&mut self, request: &str) -> Result<String> {
if !self.knowledge_index.is_empty()
&& let Some(answer) = self.knowledge_index.answer(request, 5)
{
self.add_message(Message::new("user", request.to_string()));
self.add_message(Message::new("assistant", answer.clone()));
return Ok(answer);
}
#[cfg(feature = "net")]
let result = {
let corpus = self.search(request, 5).await.unwrap_or_default();
if let Some(sentence) = Self::best_sentence(&corpus, request) {
sentence
} else {
let seed = if corpus.is_empty() {
Self::domain_seed(request, &self.behavior)
} else {
format!("{request} {corpus}")
};
Self::symbolic_continuation(seed)
}
};
#[cfg(not(feature = "net"))]
let result = {
let seed = Self::domain_seed(request, &self.behavior);
Self::symbolic_continuation(seed)
};
self.add_message(Message::new("user", request.to_string()));
self.add_message(Message::new("assistant", result.clone()));
Ok(result)
}
fn domain_seed(request: &str, behavior: &str) -> String {
const STOP: &[&str] = &[
"a", "an", "the", "and", "or", "of", "to", "in", "is", "are", "be", "for", "on", "at",
"by", "as", "it", "its",
];
let domain_words: Vec<&str> = behavior
.split_whitespace()
.filter(|w| {
let lw = w.to_ascii_lowercase();
!STOP.contains(&lw.as_str()) && w.len() > 3
})
.take(6)
.collect();
let mut seed = request.to_string();
if !domain_words.is_empty() {
seed.push(' ');
seed.push_str(&domain_words.join(" "));
}
if seed.split_whitespace().count() < 2 {
seed.push_str(" and");
}
seed
}
fn symbolic_continuation(seed: String) -> String {
let mut predictor = TextPredictor::new(20, 40, 3);
if let Ok(lex) = lmm::lexicon::Lexicon::load_system() {
predictor = predictor.with_lexicon(lex);
}
predictor
.predict_continuation(&seed, 120)
.map(|c| format!("{} {}", seed.trim(), c.continuation.trim()))
.unwrap_or(seed)
}
#[cfg(feature = "net")]
fn best_sentence(corpus: &str, query: &str) -> Option<String> {
use std::collections::HashSet;
let query_tokens: HashSet<String> = query
.split_whitespace()
.map(|w| w.to_ascii_lowercase())
.collect();
corpus
.split(['.', '!', '?'])
.map(str::trim)
.filter(|s| s.split_whitespace().count() >= 5)
.map(|sentence| {
let sentence_tokens: HashSet<String> = sentence
.split_whitespace()
.map(|w| w.to_ascii_lowercase())
.collect();
let overlap = query_tokens.intersection(&sentence_tokens).count();
(overlap, sentence.to_string())
})
.filter(|(overlap, _)| *overlap >= 2)
.max_by_key(|(overlap, _)| *overlap)
.map(|(_, sentence)| sentence)
}
#[cfg(feature = "net")]
pub async fn search(&self, query: &str, limit: usize) -> Result<String> {
let browser = Browser::new();
let ua = get_ua("firefox").unwrap_or("Mozilla/5.0");
let results = browser.lite_search(query, "wt-wt", Some(limit), ua).await?;
let corpus = results
.iter()
.filter_map(|r| {
let snippet = r.snippet.trim();
if !snippet.is_empty() {
Some(snippet.to_string())
} else if !r.title.trim().is_empty() {
Some(r.title.trim().to_string())
} else {
None
}
})
.collect::<Vec<_>>()
.join(" ");
Ok(corpus)
}
#[cfg(not(feature = "net"))]
pub async fn search(&self, _query: &str, _limit: usize) -> Result<String> {
Ok(String::new())
}
pub async fn think(&mut self, goal: &str) -> Result<ThinkResult> {
self.think_with(goal, 10, 0.25, 1.0, 0.05).await
}
pub async fn think_with(
&mut self,
goal: &str,
max_iterations: usize,
convergence_threshold: f64,
k_proportional: f64,
k_integral: f64,
) -> Result<ThinkResult> {
self.status = Status::Thinking;
let mut oracle = SearchOracle::new(5);
let mut lp = ThinkLoop::new(
goal,
max_iterations,
convergence_threshold,
k_proportional,
k_integral,
);
let result = lp.run(&mut oracle).await;
if let Some(engine) = &mut self.learning_engine {
let mut prev_state = QTable::state_key(goal);
for signal in &result.signals {
let next_state = QTable::state_key(&signal.observation);
let action = engine.recommend_action(prev_state, goal, signal.step);
engine.record_step(signal, prev_state, action, next_state);
prev_state = next_state;
}
let avg_reward = if result.steps > 0 {
result.signals.iter().map(|s| s.reward).sum::<f64>() / result.steps as f64
} else {
0.0
};
engine.end_of_episode(&lp.cold, &mut self.knowledge_index, goal, avg_reward);
}
for entry in lp.cold.all() {
self.long_term_memory
.push(Message::new("think", entry.content.clone()));
}
self.add_message(Message::new("think:goal", goal.to_string()));
self.add_message(Message::new(
"think:result",
format!(
"converged={} steps={} error={:.3}",
result.converged, result.steps, result.final_error
),
));
self.status = Status::Completed;
Ok(result)
}
pub async fn ingest(&mut self, source: KnowledgeSource) -> Result<usize> {
knowledge_ingest(&mut self.knowledge_index, source).await
}
pub fn query_knowledge(&self, question: &str, top_k: usize) -> Vec<String> {
self.knowledge_index
.query(question, top_k)
.into_iter()
.map(|c| c.text.clone())
.collect()
}
pub fn answer_from_knowledge(&self, question: &str) -> Option<String> {
self.knowledge_index.answer(question, 5)
}
pub fn save_learning(&self, path: &std::path::Path) -> Result<()> {
if let Some(engine) = &self.learning_engine {
LearningStore::save(engine, path)
} else {
Ok(())
}
}
pub fn load_learning(&mut self, path: &std::path::Path) -> Result<()> {
let engine = LearningStore::load(path)?;
self.learning_engine = Some(engine);
Ok(())
}
pub fn recall_learned(&mut self, query: &str, step: usize) -> Option<ActionKey> {
let engine = self.learning_engine.as_mut()?;
let state = QTable::state_key(query);
Some(engine.recommend_action(state, query, step))
}
pub fn attribute_causes(
&self,
graph: &lmm::causal::CausalGraph,
outcome_var: &str,
) -> anyhow::Result<crate::cognition::attribution::AttributionReport> {
crate::cognition::attribution::CausalAttributor::attribute(graph, outcome_var)
.map_err(|e| anyhow::anyhow!("{e}"))
}
pub fn form_hypotheses(
&self,
graph: &lmm::causal::CausalGraph,
observed: &std::collections::HashMap<String, f64>,
max_hypotheses: usize,
) -> anyhow::Result<Vec<crate::cognition::hypothesis::Hypothesis>> {
let r#gen = crate::cognition::hypothesis::HypothesisGenerator::new(0.05, max_hypotheses);
r#gen
.generate(graph, observed)
.map_err(|e| anyhow::anyhow!("{e}"))
}
pub fn drive_state(&mut self) -> crate::cognition::drive::DriveState {
self.internal_drive.tick()
}
pub fn record_residual(&mut self, magnitude: f64) {
self.internal_drive.record_residual(magnitude);
}
pub fn record_incoherence(&mut self, magnitude: f64) {
self.internal_drive.record_incoherence(magnitude);
}
pub fn record_contradiction(&mut self) {
self.internal_drive.record_contradiction();
}
}
impl Agent for LmmAgent {
fn new(persona: Cow<'static, str>, behavior: Cow<'static, str>) -> Self {
LmmAgent::new(persona, behavior)
}
fn update(&mut self, status: Status) {
self.status = status;
}
fn persona(&self) -> &str {
&self.persona
}
fn behavior(&self) -> &str {
&self.behavior
}
fn status(&self) -> &Status {
&self.status
}
fn memory(&self) -> &Vec<Message> {
&self.memory
}
fn tools(&self) -> &Vec<Tool> {
&self.tools
}
fn knowledge(&self) -> &Knowledge {
&self.knowledge
}
fn planner(&self) -> Option<&Planner> {
self.planner.as_ref()
}
fn profile(&self) -> &Profile {
&self.profile
}
fn reflection(&self) -> Option<&Reflection> {
self.reflection.as_ref()
}
fn scheduler(&self) -> Option<&TaskScheduler> {
self.scheduler.as_ref()
}
fn capabilities(&self) -> &HashSet<Capability> {
&self.capabilities
}
fn context(&self) -> &ContextManager {
&self.context
}
fn tasks(&self) -> &Vec<Task> {
&self.tasks
}
fn memory_mut(&mut self) -> &mut Vec<Message> {
&mut self.memory
}
fn planner_mut(&mut self) -> Option<&mut Planner> {
self.planner.as_mut()
}
fn context_mut(&mut self) -> &mut ContextManager {
&mut self.context
}
}