use std::fmt;
use std::sync::Arc;
use bevy_ecs::prelude::*;
use bevy_reflect::prelude::*;
use rig_core::catalog::{Connector, ModelSpec};
use rig_core::completion::Reasoning;
use serde::{Deserialize, Serialize};
use super::agent::{ActiveTurn, Agent, Notice};
use super::effects::Handler;
use super::session_log::ReflectSaved;
#[derive(Resource, Clone, Default)]
pub struct Models(pub Connector);
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
not(target_feature = "atomics")
))]
unsafe impl Send for Models {}
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
not(target_feature = "atomics")
))]
unsafe impl Sync for Models {}
impl Models {
pub fn connect(&self, reference: &str) -> Result<Connection, String> {
match self.0.connect(reference) {
Ok((spec, handler)) => Ok(Connection {
spec,
handler: Handler(handler),
}),
Err(why) => Err(format!("Cannot use {reference}: {why}.")),
}
}
}
#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[component(immutable)]
#[reflect(Component, Saved, Clone)]
#[type_path = "rig"]
#[type_name = "model"]
#[serde(transparent)]
pub struct ModelChoice(pub String);
impl ModelChoice {
pub fn inherit(
models: &Connector,
parent: (Option<&ModelChoice>, ReasoningSetting),
model: Option<&str>,
effort: Option<&str>,
) -> Result<(Option<ModelChoice>, ReasoningSetting), String> {
fn asked(text: Option<&str>) -> Option<&str> {
text.map(str::trim).filter(|text| !text.is_empty())
}
let resolve = |text: &str| models.catalog().resolve(text).map(|found| found.spec);
let model = match asked(model) {
Some(asked) => {
let spec = resolve(asked).map_err(|why| format!("{why}; use vendor/model"))?;
if !spec.tools {
return Err(format!("{asked} cannot call tools"));
}
Some(ModelChoice(spec.reference()))
}
None => parent.0.cloned(),
};
let effort = match (asked(effort), &model) {
(Some(name), Some(model)) => {
let spec = resolve(&model.0).map_err(|why| why.to_string())?;
let named = spec.reasoning.named(name);
ReasoningSetting(
named
.map_err(|why| format!("{}: {why}", spec.display_name))?
.reasoning,
)
}
_ if model.is_some() && model.as_ref() == parent.0 => parent.1,
_ => ReasoningSetting::default(),
};
Ok((model, effort))
}
}
#[derive(Component, Reflect, Clone)]
#[reflect(opaque, Component, Clone)]
pub struct Connection {
pub spec: Arc<ModelSpec>,
pub handler: Handler,
}
#[derive(
Component, Reflect, Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
)]
#[component(immutable)]
#[reflect(opaque, Component, Saved, Default, Clone, Serialize, Deserialize)]
pub struct ReasoningSetting(pub Option<Reasoning>);
impl fmt::Display for ReasoningSetting {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
None => f.write_str("default"),
Some(Reasoning::Off) => f.write_str("off"),
Some(Reasoning::Effort(level)) => f.write_str(level.as_str()),
Some(Reasoning::Budget { tokens }) => write!(f, "{tokens} tokens"),
Some(_) => f.write_str("custom"),
}
}
}
#[derive(EntityEvent, Reflect, Clone, Debug)]
#[reflect(Event, Clone, Debug)]
pub struct SetModel {
pub entity: Entity,
pub model: String,
}
#[derive(EntityEvent, Reflect, Clone, Debug)]
#[reflect(Event, Clone, Debug)]
pub struct SetReasoning {
pub entity: Entity,
pub effort: ReasoningSetting,
}
pub(crate) fn on_set_model(
set: On<SetModel>,
agents: Query<(Has<ActiveTurn>, Option<&ModelChoice>), With<Agent>>,
models: Res<Models>,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
let Ok((busy, current)) = agents.get(set.entity) else {
return;
};
if busy {
notices.write(Notice::turn_running(set.entity));
return;
}
match models.0.catalog().resolve(&set.model) {
Ok(found) => {
commands
.entity(set.entity)
.insert(ModelChoice(found.spec.reference()));
}
Err(why) => {
let stop = if why.suggestions.is_empty() { "." } else { "" };
let kept = match current {
Some(ModelChoice(model)) => format!(" Still on {model}."),
None => String::new(),
};
let text = format!("Not picked: {why}{stop}{kept}");
notices.write(Notice::error(set.entity, text));
}
}
}
pub(crate) fn connect(
inserted: On<Insert<ModelChoice>>,
agents: Query<(&ModelChoice, Option<&Connection>)>,
models: Res<Models>,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
let agent = inserted.entity;
let Ok((choice, connected)) = agents.get(agent) else {
return;
};
if connected.is_some_and(|connection| connection.spec.reference() == choice.0) {
return;
}
match models.connect(&choice.0) {
Ok(connection) => {
if connected.is_some() {
let model = format!("Model: {} ({}).", connection.spec.display_name, choice.0);
notices.write(Notice::info(agent, model));
}
commands.entity(agent).insert(connection);
}
Err(why) => {
commands.entity(agent).remove::<Connection>();
notices.write(Notice::error(agent, why));
}
}
}
pub(crate) fn reconnect(
agents: Query<(Entity, &ModelChoice), (With<Agent>, Without<Connection>)>,
models: Res<Models>,
mut commands: Commands,
) {
for (agent, choice) in &agents {
if let Ok(connection) = models.connect(&choice.0) {
commands.entity(agent).try_insert(connection);
}
}
}
pub(crate) fn check_effort(
inserted: On<Insert<(Connection, ReasoningSetting)>>,
agents: Query<(&Connection, &ReasoningSetting)>,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
let agent = inserted.entity;
let Ok((Connection { spec, .. }, effort)) = agents.get(agent) else {
return;
};
if let Err(refusal) = spec.validate(&spec.default_options(effort.0)) {
commands.entity(agent).insert(ReasoningSetting(None));
notices.write(Notice::info(
agent,
format!("Reasoning reset to default: {refusal}."),
));
}
}
pub(crate) fn on_set_reasoning(
set: On<SetReasoning>,
agents: Query<(Option<&Connection>, Has<ActiveTurn>), With<Agent>>,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
let Ok((connection, busy)) = agents.get(set.entity) else {
return;
};
if busy {
notices.write(Notice::turn_running(set.entity));
return;
}
let Some(connection) = connection else {
notices.write(Notice::no_model(set.entity));
return;
};
let spec = &connection.spec;
match spec.validate(&spec.default_options(set.effort.0)) {
Ok(()) => {
commands.entity(set.entity).insert(set.effort);
notices.write(Notice::info(
set.entity,
format!("Reasoning: {}.", set.effort),
));
}
Err(refusal) => {
notices.write(Notice::error(set.entity, format!("{refusal}.")));
}
}
}