use std::collections::{BTreeMap, BTreeSet, HashMap};
use bevy_ecs::prelude::*;
use bevy_ecs::reflect::{AppTypeRegistry, ReflectComponent};
use bevy_log::warn;
use bevy_reflect::prelude::ReflectDefault;
use bevy_reflect::serde::TypedReflectDeserializer;
use bevy_reflect::{Reflect, ReflectFromReflect, TypeRegistration, TypeRegistry};
use rig_core::completion::Message;
use rig_core::conversation::{Aside, AsideId, Author};
use rig_core::effect::Decider;
use rig_core::message::{CallId, UserContent};
use rig_core::tool::ToolResult;
use rig_memory::Session;
use serde::de::DeserializeSeed;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use web_time::SystemTime;
use super::agent::{Agent, AgentId, CallOf, Conversation, Notice, SpawnedBy, Turn, TurnOf};
use super::effects::Effects;
use super::inbox::{Unsent, kept};
use super::session_log::{
Opened, ReflectSaved, SessionLog, SessionRestored, SessionStore, open_session,
};
use super::stage::{Admitted, Decision, Effect, ForAgent, Holds, Outcome, Settled, Started};
use super::tools::{Footprint, ToolRequest};
use super::turn::{TurnCall, not_finished};
pub(crate) const RESTART: &str = "the session restarted";
const CALL_MARK: &str = "rig.call";
#[derive(Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
enum CallMark {
Started { call: CallId },
Settled { call: CallId, result: ToolResult },
}
impl CallMark {
fn call(&self) -> &CallId {
match self {
Self::Started { call } | Self::Settled { call, .. } => call,
}
}
}
fn marks(conversation: &rig_core::conversation::Conversation) -> Vec<(AsideId, CallMark)> {
let marks = conversation
.asides()
.filter(|(_, aside)| aside.kind == CALL_MARK);
marks
.filter_map(|(id, aside)| Some((id, CallMark::deserialize(&aside.value).ok()?)))
.collect()
}
pub(crate) fn mark_started(conversation: &mut rig_core::conversation::Conversation, call: &CallId) {
let after = conversation.entries().next_back().map(|entry| entry.id);
let mark = CallMark::Started { call: call.clone() };
let value = serde_json::to_value(mark).unwrap_or_default();
let aside = Aside::new(CALL_MARK, Author::rig(), value).after(after);
let had = marks(conversation)
.into_iter()
.find(|(_, mark)| mark.call() == call);
if let Some((id, _)) = had {
conversation.replace_aside(id, aside);
} else {
conversation.add_aside(aside);
}
}
pub(crate) fn mark_settled(
conversation: &mut rig_core::conversation::Conversation,
call: &CallId,
result: &ToolResult,
) {
let had = marks(conversation)
.into_iter()
.find(|(_, mark)| mark.call() == call);
let mark = CallMark::Settled {
call: call.clone(),
result: result.clone(),
};
let value = serde_json::to_value(mark).unwrap_or_default();
match had {
Some((_, CallMark::Settled { .. })) => {}
Some((id, CallMark::Started { .. })) => {
if let Some(aside) = conversation.aside(id).cloned() {
conversation.replace_aside(id, Aside { value, ..aside });
}
}
None => {
let after = conversation.entries().next_back().map(|entry| entry.id);
conversation.add_aside(Aside::new(CALL_MARK, Author::rig(), value).after(after));
}
}
}
pub(crate) fn commit_results(
conversation: &mut rig_core::conversation::Conversation,
results: Message,
) {
let answered: Vec<CallId> = match &results {
Message::User { content } => content
.iter()
.filter_map(|item| match item {
UserContent::ToolResult(result) => Some(result.call.clone()),
_ => None,
})
.collect(),
_ => Vec::new(),
};
conversation.push(results);
for (id, mark) in marks(conversation) {
if answered.contains(mark.call()) {
conversation.remove_aside(id);
}
}
}
#[derive(Component, Clone, Debug)]
pub enum Interrupted {
Started,
Settled(ToolResult),
}
pub(crate) fn answer_interrupted(
mut calls: Query<
(&Interrupted, Option<&Footprint>, &mut Decision),
(
With<Admitted>,
Without<Outcome>,
Without<Started>,
Without<Settled>,
Without<Holds>,
),
>,
) {
for (interrupted, footprint, mut decision) in &mut calls {
if decision.is_decided() {
continue;
}
let result = match interrupted {
Interrupted::Settled(result) => result.clone(),
Interrupted::Started => not_finished(RESTART, true, footprint),
};
decision.decide(Decider::plugin("rig-ecs"), result);
}
}
#[derive(Resource)]
pub(crate) struct RestoredAgents(Vec<(usize, Entity)>);
#[derive(Component, Reflect, Clone, Copy, Debug, Default)]
#[reflect(Component, Default)]
pub struct Restored;
pub(crate) fn restore_session(world: &mut World) {
let session = match world.remove_resource::<Opened>() {
Some(Opened(session)) => session,
None => {
let Some(SessionStore(store)) = world.remove_resource::<SessionStore>() else {
return;
};
if let Err(failure) = open_session(world, store) {
let refused = format!(
"The session cannot be resumed, so nothing of it is changed and this run is \
not saved: {failure}."
);
world.write_message(Notice::error(None, refused));
return;
}
match world.remove_resource::<Opened>() {
Some(Opened(session)) => session,
None => return,
}
}
};
let Some(SessionLog(log)) = world.get_resource::<SessionLog>().cloned() else {
return;
};
let Session {
agents,
saved: state,
repairs,
} = session;
let mut notices: Vec<Notice> = repairs
.iter()
.map(|repair| Notice::warn(None, format!("While resuming the session: {repair}.")))
.collect();
let registry = world.get_resource::<AppTypeRegistry>().cloned();
let registry = registry.unwrap_or_default();
let registry = registry.read();
let reader = SavedReader::new(world);
let skipped = |what: &str, key: &str, failure: String| {
let skipped =
format!("Skipped saved {what} `{key}`: {failure}; its next save replaces it.");
Notice::error(None, skipped)
};
for (key, value) in &state {
let restored = reader
.read(®istry, key, value)
.and_then(|(component, value)| {
let mut resource = resource_entity(world, component)?;
component.insert(&mut resource, value.as_partial_reflect(), ®istry);
Ok(())
});
if let Err(failure) = restored {
notices.push(skipped("resource", key, failure));
}
}
let mut entities = HashMap::new();
let mut links = Vec::new();
for agent in agents {
let conversation = Conversation(agent.conversation);
let id = AgentId(agent.id);
let mut read = Vec::new();
for (key, value) in &agent.saved {
match reader.read(®istry, key, value) {
Ok(value) => read.push(value),
Err(failure) => notices.push(skipped("component", key, failure)),
}
}
let mut spawned = world.spawn((Name::new("agent"), Agent, id.clone(), conversation));
for (component, value) in read {
component.insert(&mut spawned, value.as_partial_reflect(), ®istry);
}
let entity = spawned.id();
entities.insert(id.0, entity);
links.push((entity, agent.parent));
}
for (child, parent) in &links {
if let Some(&parent) = parent.as_ref().and_then(|parent| entities.get(parent))
&& let Ok(mut child) = world.get_entity_mut(*child)
{
child.insert(SpawnedBy(parent));
}
}
let mut parents = world.query::<&SpawnedBy>();
let parents = parents.query(world);
let bound = links.len();
let restored = links
.into_iter()
.map(|(entity, _)| {
let depth = parents.iter_ancestors(entity).take(bound).count();
(depth, entity)
})
.collect();
world.flush();
log.start();
world.insert_resource(RestoredAgents(restored));
world.insert_resource(SessionRestored { repairs });
for notice in notices {
world.write_message(notice);
}
}
#[derive(Resource, Default, Debug, Clone, PartialEq, Eq)]
pub struct SavedKeys {
keys: BTreeMap<String, String>,
paths: BTreeMap<String, String>,
listed: BTreeSet<String>,
}
impl SavedKeys {
pub fn add(&mut self, path: &str, key: &str) -> Result<(), String> {
match (self.paths.get(key), self.keys.get(path)) {
(Some(taken), _) if taken != path => {
Err(format!("the saved key `{key}` is taken by `{taken}`"))
}
(_, Some(other)) if other != key => {
Err(format!("`{path}` is saved under `{other}` already"))
}
_ => {
self.keys.insert(path.to_owned(), key.to_owned());
self.paths.insert(key.to_owned(), path.to_owned());
Ok(())
}
}
}
pub fn list(&mut self, key: &str) {
self.listed.insert(key.to_owned());
}
pub fn key(&self, path: &str) -> Option<&str> {
self.keys.get(path).map(String::as_str)
}
pub fn path(&self, key: &str) -> Option<&str> {
self.paths.get(key).map(String::as_str)
}
pub fn is_listed(&self, key: &str) -> bool {
self.listed.contains(key)
}
pub fn listed(&self) -> &BTreeSet<String> {
&self.listed
}
}
pub(crate) struct SavedReader {
keys: SavedKeys,
}
impl SavedReader {
pub(crate) fn new(world: &World) -> Self {
Self {
keys: world
.get_resource::<SavedKeys>()
.cloned()
.unwrap_or_default(),
}
}
pub(crate) fn read<'r>(
&self,
registry: &'r TypeRegistry,
key: &str,
saved: &Value,
) -> Result<(&'r ReflectComponent, Box<dyn Reflect>), String> {
let (registration, component) = saved_type(self.path(key), registry)?;
Ok((component, reflected(registration, saved, registry)?))
}
pub(crate) fn path<'k>(&'k self, key: &'k str) -> &'k str {
self.keys.path(key).unwrap_or(key)
}
}
fn reflected(
registration: &TypeRegistration,
value: &Value,
registry: &TypeRegistry,
) -> Result<Box<dyn Reflect>, String> {
let reflected = TypedReflectDeserializer::new(registration, registry)
.deserialize(value)
.map_err(|failure| format!("its saved value no longer fits: {failure}"))?;
registration
.data::<ReflectFromReflect>()
.and_then(|from| from.from_reflect(&*reflected))
.ok_or_else(|| "its saved value no longer fits".to_owned())
}
pub(crate) fn saved_type<'r>(
path: &str,
registry: &'r TypeRegistry,
) -> Result<(&'r TypeRegistration, &'r ReflectComponent), String> {
let registration = registry
.get_with_type_path(path)
.filter(|registration| registration.data::<ReflectSaved>().is_some())
.ok_or("no plugin saves it in this build")?;
let component = registration.data::<ReflectComponent>();
let component = component.ok_or("it is not a reflected component")?;
Ok((registration, component))
}
pub(crate) fn resource_entity<'w>(
world: &'w mut World,
component: &ReflectComponent,
) -> Result<EntityWorldMut<'w>, String> {
let id = component.register_component(world);
let entity = match world.resource_entities().get(id) {
Some(entity) => entity,
None => world.spawn_empty().id(),
};
world
.get_entity_mut(entity)
.map_err(|failure| failure.to_string())
}
pub(crate) fn reconcile(world: &mut World) {
let Some(RestoredAgents(mut agents)) = world.remove_resource::<RestoredAgents>() else {
return;
};
agents.sort_by_key(|&(depth, _)| depth);
for (_, agent) in agents {
if let Err(error) = world.run_system_cached_with(carry_on_restored, agent) {
warn!("could not reconcile a restored agent: {error}");
}
}
}
fn carry_on_restored(
In(agent): In<Entity>,
mut agents: Query<&mut Conversation>,
effects: Res<Effects>,
mut commands: Commands,
) {
let Ok(mut conversation) = agents.get_mut(agent) else {
return;
};
let pending = conversation.pending_calls();
let idle = pending.is_empty() && !conversation.awaits_model();
let mut marked = HashMap::new();
for (id, mark) in marks(&conversation) {
if pending.iter().any(|call| &call.id == mark.call()) {
marked.insert(mark.call().clone(), mark);
} else {
conversation.remove_aside(id);
}
}
if !pending.is_empty() {
let reply = effects.next_id();
let state = Turn {
reply: Some(reply),
..Turn::default()
};
let turn = commands.spawn((TurnOf(agent), state, Restored)).id();
for call in pending {
let interrupted = marked.remove(&call.id).map(|mark| match mark {
CallMark::Started { .. } => Interrupted::Started,
CallMark::Settled { result, .. } => Interrupted::Settled(result),
});
let mut restored = commands.spawn((
ToolRequest { call },
Effect::with_parent_id(Some(reply)),
CallOf(turn),
ForAgent(agent),
Restored,
));
if let Some(interrupted) = interrupted {
restored.insert(interrupted);
}
}
} else if conversation.awaits_model() {
let turn = commands.spawn((TurnOf(agent), Restored)).id();
commands.spawn((TurnCall, CallOf(turn), ForAgent(agent), Restored));
}
let at = SystemTime::now();
for (mark, message) in kept(&conversation) {
let mut message =
commands.spawn((message, ForAgent(agent), Started { at }, Restored, mark));
if idle {
message.insert(Unsent { ended: None });
}
}
}
#[cfg(test)]
mod tests;