1use std::fmt;
6use std::sync::Arc;
7
8use bevy_ecs::prelude::*;
9use bevy_reflect::prelude::*;
10use rig_core::catalog::{Connector, ModelSpec};
11use rig_core::completion::Reasoning;
12use serde::{Deserialize, Serialize};
13
14use super::agent::{ActiveTurn, Agent, Notice};
15use super::effects::Handler;
16use super::session_log::ReflectSaved;
17
18#[derive(Resource, Clone, Default)]
26pub struct Models(pub Connector);
27
28#[cfg(all(
31 target_arch = "wasm32",
32 target_os = "unknown",
33 not(target_feature = "atomics")
34))]
35unsafe impl Send for Models {}
36#[cfg(all(
38 target_arch = "wasm32",
39 target_os = "unknown",
40 not(target_feature = "atomics")
41))]
42unsafe impl Sync for Models {}
43
44impl Models {
45 pub fn connect(&self, reference: &str) -> Result<Connection, String> {
49 match self.0.connect(reference) {
50 Ok((spec, handler)) => Ok(Connection {
51 spec,
52 handler: Handler(handler),
53 }),
54 Err(why) => Err(format!("Cannot use {reference}: {why}.")),
55 }
56 }
57}
58
59#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
64#[component(immutable)]
65#[reflect(Component, Saved, Clone)]
66#[type_path = "rig"]
67#[type_name = "model"]
68#[serde(transparent)]
69pub struct ModelChoice(pub String);
70
71impl ModelChoice {
72 pub fn inherit(
77 models: &Connector,
78 parent: (Option<&ModelChoice>, ReasoningSetting),
79 model: Option<&str>,
80 effort: Option<&str>,
81 ) -> Result<(Option<ModelChoice>, ReasoningSetting), String> {
82 fn asked(text: Option<&str>) -> Option<&str> {
83 text.map(str::trim).filter(|text| !text.is_empty())
84 }
85 let resolve = |text: &str| models.catalog().resolve(text).map(|found| found.spec);
86 let model = match asked(model) {
87 Some(asked) => {
88 let spec = resolve(asked).map_err(|why| format!("{why}; use vendor/model"))?;
89 if !spec.tools {
90 return Err(format!("{asked} cannot call tools"));
91 }
92 Some(ModelChoice(spec.reference()))
93 }
94 None => parent.0.cloned(),
95 };
96 let effort = match (asked(effort), &model) {
97 (Some(name), Some(model)) => {
98 let spec = resolve(&model.0).map_err(|why| why.to_string())?;
99 let named = spec.reasoning.named(name);
100 ReasoningSetting(
101 named
102 .map_err(|why| format!("{}: {why}", spec.display_name))?
103 .reasoning,
104 )
105 }
106 _ if model.is_some() && model.as_ref() == parent.0 => parent.1,
107 _ => ReasoningSetting::default(),
108 };
109 Ok((model, effort))
110 }
111}
112
113#[derive(Component, Reflect, Clone)]
118#[reflect(opaque, Component, Clone)]
119pub struct Connection {
120 pub spec: Arc<ModelSpec>,
122 pub handler: Handler,
124}
125
126#[derive(
129 Component, Reflect, Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
130)]
131#[component(immutable)]
132#[reflect(opaque, Component, Saved, Default, Clone, Serialize, Deserialize)]
133pub struct ReasoningSetting(pub Option<Reasoning>);
134
135impl fmt::Display for ReasoningSetting {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 match self.0 {
139 None => f.write_str("default"),
140 Some(Reasoning::Off) => f.write_str("off"),
141 Some(Reasoning::Effort(level)) => f.write_str(level.as_str()),
142 Some(Reasoning::Budget { tokens }) => write!(f, "{tokens} tokens"),
143 Some(_) => f.write_str("custom"),
144 }
145 }
146}
147
148#[derive(EntityEvent, Reflect, Clone, Debug)]
150#[reflect(Event, Clone, Debug)]
151pub struct SetModel {
152 pub entity: Entity,
154 pub model: String,
156}
157
158#[derive(EntityEvent, Reflect, Clone, Debug)]
160#[reflect(Event, Clone, Debug)]
161pub struct SetReasoning {
162 pub entity: Entity,
164 pub effort: ReasoningSetting,
166}
167
168pub(crate) fn on_set_model(
172 set: On<SetModel>,
173 agents: Query<(Has<ActiveTurn>, Option<&ModelChoice>), With<Agent>>,
174 models: Res<Models>,
175 mut commands: Commands,
176 mut notices: MessageWriter<Notice>,
177) {
178 let Ok((busy, current)) = agents.get(set.entity) else {
179 return;
180 };
181 if busy {
182 notices.write(Notice::turn_running(set.entity));
183 return;
184 }
185 match models.0.catalog().resolve(&set.model) {
186 Ok(found) => {
187 commands
188 .entity(set.entity)
189 .insert(ModelChoice(found.spec.reference()));
190 }
191 Err(why) => {
192 let stop = if why.suggestions.is_empty() { "." } else { "" };
193 let kept = match current {
194 Some(ModelChoice(model)) => format!(" Still on {model}."),
195 None => String::new(),
196 };
197 let text = format!("Not picked: {why}{stop}{kept}");
198 notices.write(Notice::error(set.entity, text));
199 }
200 }
201}
202
203pub(crate) fn connect(
210 inserted: On<Insert<ModelChoice>>,
211 agents: Query<(&ModelChoice, Option<&Connection>)>,
212 models: Res<Models>,
213 mut commands: Commands,
214 mut notices: MessageWriter<Notice>,
215) {
216 let agent = inserted.entity;
217 let Ok((choice, connected)) = agents.get(agent) else {
218 return;
219 };
220 if connected.is_some_and(|connection| connection.spec.reference() == choice.0) {
221 return;
222 }
223 match models.connect(&choice.0) {
224 Ok(connection) => {
225 if connected.is_some() {
226 let model = format!("Model: {} ({}).", connection.spec.display_name, choice.0);
227 notices.write(Notice::info(agent, model));
228 }
229 commands.entity(agent).insert(connection);
230 }
231 Err(why) => {
232 commands.entity(agent).remove::<Connection>();
233 notices.write(Notice::error(agent, why));
234 }
235 }
236}
237
238pub(crate) fn reconnect(
244 agents: Query<(Entity, &ModelChoice), (With<Agent>, Without<Connection>)>,
245 models: Res<Models>,
246 mut commands: Commands,
247) {
248 for (agent, choice) in &agents {
249 if let Ok(connection) = models.connect(&choice.0) {
250 commands.entity(agent).try_insert(connection);
251 }
252 }
253}
254
255pub(crate) fn check_effort(
258 inserted: On<Insert<(Connection, ReasoningSetting)>>,
259 agents: Query<(&Connection, &ReasoningSetting)>,
260 mut commands: Commands,
261 mut notices: MessageWriter<Notice>,
262) {
263 let agent = inserted.entity;
264 let Ok((Connection { spec, .. }, effort)) = agents.get(agent) else {
265 return;
266 };
267 if let Err(refusal) = spec.validate(&spec.default_options(effort.0)) {
268 commands.entity(agent).insert(ReasoningSetting(None));
269 notices.write(Notice::info(
270 agent,
271 format!("Reasoning reset to default: {refusal}."),
272 ));
273 }
274}
275
276pub(crate) fn on_set_reasoning(
278 set: On<SetReasoning>,
279 agents: Query<(Option<&Connection>, Has<ActiveTurn>), With<Agent>>,
280 mut commands: Commands,
281 mut notices: MessageWriter<Notice>,
282) {
283 let Ok((connection, busy)) = agents.get(set.entity) else {
284 return;
285 };
286 if busy {
287 notices.write(Notice::turn_running(set.entity));
288 return;
289 }
290 let Some(connection) = connection else {
291 notices.write(Notice::no_model(set.entity));
292 return;
293 };
294 let spec = &connection.spec;
295 match spec.validate(&spec.default_options(set.effort.0)) {
296 Ok(()) => {
297 commands.entity(set.entity).insert(set.effort);
298 notices.write(Notice::info(
299 set.entity,
300 format!("Reasoning: {}.", set.effort),
301 ));
302 }
303 Err(refusal) => {
304 notices.write(Notice::error(set.entity, format!("{refusal}.")));
305 }
306 }
307}