1use sim_codec_chat::{model_error_expr, model_response_expr};
4use sim_kernel::{Cx, Error, Expr, Result, Symbol};
5use sim_lib_agent_runner_core::{ModelCard, ModelRequest, ModelResponse, ModelRunner};
6use std::collections::BTreeMap;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct FakeVisionFixture {
11 pub caption: String,
13 pub fields: Vec<(String, String)>,
15}
16
17impl FakeVisionFixture {
18 pub fn new(caption: impl Into<String>) -> Self {
20 Self {
21 caption: caption.into(),
22 fields: Vec::new(),
23 }
24 }
25
26 pub fn with_field(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
28 self.fields.push((name.into(), value.into()));
29 self
30 }
31}
32
33#[derive(Clone, Debug)]
35pub struct FakeVisionRunner {
36 model: String,
37 fixtures: BTreeMap<String, FakeVisionFixture>,
38}
39
40impl FakeVisionRunner {
41 pub fn new(model: impl Into<String>) -> Self {
43 Self {
44 model: model.into(),
45 fixtures: BTreeMap::new(),
46 }
47 }
48
49 pub fn with_fixture(
51 mut self,
52 reference: impl Into<String>,
53 fixture: FakeVisionFixture,
54 ) -> Result<Self> {
55 let reference = reference.into();
56 validate_synthetic_reference(&reference)?;
57 self.fixtures.insert(reference, fixture);
58 Ok(self)
59 }
60
61 pub fn fixture(&self, reference: &str) -> Option<&FakeVisionFixture> {
63 self.fixtures.get(reference)
64 }
65}
66
67impl ModelRunner for FakeVisionRunner {
68 fn card(&self) -> ModelCard {
69 model_card(
70 Symbol::qualified("runner", "fake-vision"),
71 self.model.clone(),
72 Symbol::new("fake-vision"),
73 ["image"],
74 ["text", "fields"],
75 )
76 }
77
78 fn infer(&self, _cx: &mut Cx, request: ModelRequest) -> Result<ModelResponse> {
79 let Some(reference) = request_reference(&request, &["image", "image-ref", "fixture"])
80 else {
81 return error_response(
82 Symbol::qualified("runner", "fake-vision"),
83 &self.model,
84 "fake vision request missing synthetic image reference",
85 );
86 };
87 if let Err(error) = validate_synthetic_reference(reference) {
88 return error_response(
89 Symbol::qualified("runner", "fake-vision"),
90 &self.model,
91 error.to_string(),
92 );
93 }
94 let Some(fixture) = self.fixtures.get(reference) else {
95 return error_response(
96 Symbol::qualified("runner", "fake-vision"),
97 &self.model,
98 format!("fake vision fixture not found: {reference}"),
99 );
100 };
101 response_from_parts(
102 Symbol::qualified("runner", "fake-vision"),
103 &self.model,
104 vec![
105 text_part(&fixture.caption),
106 string_part("fixture", "reference", reference),
107 fields_part(&fixture.fields),
108 ],
109 )
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct FakeAsrSegment {
116 pub start_ms: u64,
118 pub end_ms: u64,
120 pub text: String,
122}
123
124impl FakeAsrSegment {
125 pub fn new(start_ms: u64, end_ms: u64, text: impl Into<String>) -> Result<Self> {
127 if end_ms < start_ms {
128 return Err(Error::Eval(
129 "fake ASR segment end must be at or after start".to_owned(),
130 ));
131 }
132 Ok(Self {
133 start_ms,
134 end_ms,
135 text: text.into(),
136 })
137 }
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct FakeAsrFixture {
143 pub transcript: String,
145 pub locale: String,
147 pub segments: Vec<FakeAsrSegment>,
149}
150
151impl FakeAsrFixture {
152 pub fn new(transcript: impl Into<String>, locale: impl Into<String>) -> Self {
154 Self {
155 transcript: transcript.into(),
156 locale: locale.into(),
157 segments: Vec::new(),
158 }
159 }
160
161 pub fn with_segment(mut self, segment: FakeAsrSegment) -> Self {
163 self.segments.push(segment);
164 self
165 }
166}
167
168#[derive(Clone, Debug)]
170pub struct FakeAsrRunner {
171 model: String,
172 fixtures: BTreeMap<String, FakeAsrFixture>,
173}
174
175impl FakeAsrRunner {
176 pub fn new(model: impl Into<String>) -> Self {
178 Self {
179 model: model.into(),
180 fixtures: BTreeMap::new(),
181 }
182 }
183
184 pub fn with_fixture(
186 mut self,
187 reference: impl Into<String>,
188 fixture: FakeAsrFixture,
189 ) -> Result<Self> {
190 let reference = reference.into();
191 validate_synthetic_reference(&reference)?;
192 self.fixtures.insert(reference, fixture);
193 Ok(self)
194 }
195
196 pub fn fixture(&self, reference: &str) -> Option<&FakeAsrFixture> {
198 self.fixtures.get(reference)
199 }
200}
201
202impl ModelRunner for FakeAsrRunner {
203 fn card(&self) -> ModelCard {
204 model_card(
205 Symbol::qualified("runner", "fake-asr"),
206 self.model.clone(),
207 Symbol::new("fake-asr"),
208 ["audio"],
209 ["text", "transcript"],
210 )
211 }
212
213 fn infer(&self, _cx: &mut Cx, request: ModelRequest) -> Result<ModelResponse> {
214 let Some(reference) = request_reference(&request, &["audio", "audio-ref", "fixture"])
215 else {
216 return error_response(
217 Symbol::qualified("runner", "fake-asr"),
218 &self.model,
219 "fake ASR request missing synthetic audio reference",
220 );
221 };
222 if let Err(error) = validate_synthetic_reference(reference) {
223 return error_response(
224 Symbol::qualified("runner", "fake-asr"),
225 &self.model,
226 error.to_string(),
227 );
228 }
229 let Some(fixture) = self.fixtures.get(reference) else {
230 return error_response(
231 Symbol::qualified("runner", "fake-asr"),
232 &self.model,
233 format!("fake ASR fixture not found: {reference}"),
234 );
235 };
236 response_from_parts(
237 Symbol::qualified("runner", "fake-asr"),
238 &self.model,
239 vec![
240 text_part(&fixture.transcript),
241 string_part("fixture", "reference", reference),
242 transcript_part(fixture),
243 ],
244 )
245 }
246}
247
248#[derive(Clone, Debug, PartialEq)]
250pub struct FakeSensorFrame {
251 pub sequence: u64,
253 pub readings: Vec<(String, f64)>,
255}
256
257impl FakeSensorFrame {
258 pub fn new(sequence: u64, readings: Vec<(String, f64)>) -> Result<Self> {
260 if readings.iter().any(|(_, value)| !value.is_finite()) {
261 return Err(Error::Eval(
262 "fake sensor frame readings must be finite".to_owned(),
263 ));
264 }
265 Ok(Self { sequence, readings })
266 }
267
268 pub fn as_expr(&self) -> Expr {
270 Expr::Map(vec![
271 number_entry("sequence", self.sequence),
272 (
273 sym("readings"),
274 Expr::Map(
275 self.readings
276 .iter()
277 .map(|(name, value)| {
278 (
279 Expr::String(name.clone()),
280 number_literal("f64", value.to_string()),
281 )
282 })
283 .collect(),
284 ),
285 ),
286 ])
287 }
288}
289
290#[derive(Clone, Debug, PartialEq)]
292pub struct FakeSensorStream {
293 frames: Vec<FakeSensorFrame>,
294 cursor: usize,
295}
296
297impl FakeSensorStream {
298 pub fn new(frames: Vec<FakeSensorFrame>) -> Result<Self> {
300 if frames.is_empty() {
301 return Err(Error::Eval(
302 "fake sensor stream needs at least one frame".to_owned(),
303 ));
304 }
305 Ok(Self { frames, cursor: 0 })
306 }
307
308 pub fn next_frame(&mut self) -> Option<FakeSensorFrame> {
310 let frame = self.frames.get(self.cursor).cloned()?;
311 self.cursor += 1;
312 Some(frame)
313 }
314
315 pub fn reset(&mut self) {
317 self.cursor = 0;
318 }
319
320 pub fn frames(&self) -> &[FakeSensorFrame] {
322 &self.frames
323 }
324}
325
326impl Iterator for FakeSensorStream {
327 type Item = FakeSensorFrame;
328
329 fn next(&mut self) -> Option<Self::Item> {
330 self.next_frame()
331 }
332}
333
334fn validate_synthetic_reference(reference: &str) -> Result<()> {
335 if reference.starts_with("fixture:") || reference.starts_with("synthetic:") {
336 return Ok(());
337 }
338 Err(Error::Eval(
339 "fake multimodal fixtures require fixture: or synthetic: references".to_owned(),
340 ))
341}
342
343fn request_reference<'a>(request: &'a ModelRequest, keys: &[&str]) -> Option<&'a str> {
344 expr_reference(&request.task, keys).or_else(|| {
345 request
346 .messages
347 .iter()
348 .find_map(|expr| expr_reference(expr, keys))
349 })
350}
351
352fn expr_reference<'a>(expr: &'a Expr, keys: &[&str]) -> Option<&'a str> {
353 match expr {
354 Expr::String(text) if is_fixture_reference(text) => Some(text.as_str()),
355 Expr::Map(entries) => entries.iter().find_map(|(key, value)| {
356 if key_matches(key, keys)
357 && let Expr::String(reference) = value
358 {
359 return Some(reference.as_str());
360 }
361 expr_reference(value, keys)
362 }),
363 Expr::List(items) | Expr::Vector(items) => {
364 items.iter().find_map(|item| expr_reference(item, keys))
365 }
366 _ => None,
367 }
368}
369
370fn is_fixture_reference(text: &str) -> bool {
371 text.starts_with("fixture:") || text.starts_with("synthetic:")
372}
373
374fn key_matches(expr: &Expr, keys: &[&str]) -> bool {
375 match expr {
376 Expr::Symbol(symbol) => {
377 let name = symbol.as_qualified_str();
378 keys.iter().any(|key| *key == name)
379 }
380 Expr::String(text) => keys.iter().any(|key| *key == text),
381 _ => false,
382 }
383}
384
385fn response_from_parts(runner: Symbol, model: &str, parts: Vec<Expr>) -> Result<ModelResponse> {
386 ModelResponse::try_from(model_response_expr(
387 runner,
388 model,
389 parts,
390 Symbol::new("stop"),
391 ))
392}
393
394fn error_response(
395 runner: Symbol,
396 model: &str,
397 message: impl Into<String>,
398) -> Result<ModelResponse> {
399 ModelResponse::try_from(model_error_expr(runner, model, message))
400}
401
402fn model_card<const IN: usize, const OUT: usize>(
403 runner: Symbol,
404 model: String,
405 provider: Symbol,
406 modalities_in: [&str; IN],
407 modalities_out: [&str; OUT],
408) -> ModelCard {
409 let mut card = ModelCard::new(runner, model, provider, Symbol::new("local"));
410 card.extra = vec![
411 symbol_list_entry("modalities-in", modalities_in),
412 symbol_list_entry("modalities-out", modalities_out),
413 bool_entry("supports-stream", false),
414 bool_entry("supports-tools", false),
415 bool_entry("supports-json", true),
416 bool_entry("supports-shape", false),
417 symbol_entry("health", "ok"),
418 ];
419 card
420}
421
422use sim_codec_chat::text_part;
423
424fn string_part(kind: &str, key: &str, value: &str) -> Expr {
425 Expr::Map(vec![symbol_entry("type", kind), string_entry(key, value)])
426}
427
428fn fields_part(fields: &[(String, String)]) -> Expr {
429 Expr::Map(vec![
430 symbol_entry("type", "fields"),
431 (
432 sym("fields"),
433 Expr::Map(
434 fields
435 .iter()
436 .map(|(key, value)| (Expr::String(key.clone()), Expr::String(value.clone())))
437 .collect(),
438 ),
439 ),
440 ])
441}
442
443fn transcript_part(fixture: &FakeAsrFixture) -> Expr {
444 Expr::Map(vec![
445 symbol_entry("type", "transcript"),
446 string_entry("locale", &fixture.locale),
447 (
448 sym("segments"),
449 Expr::List(fixture.segments.iter().map(segment_expr).collect()),
450 ),
451 ])
452}
453
454fn segment_expr(segment: &FakeAsrSegment) -> Expr {
455 Expr::Map(vec![
456 number_entry("start-ms", segment.start_ms),
457 number_entry("end-ms", segment.end_ms),
458 string_entry("text", &segment.text),
459 ])
460}
461
462fn symbol_list_entry<const N: usize>(key: &str, values: [&str; N]) -> (Expr, Expr) {
463 (
464 Expr::Symbol(Symbol::new(key)),
465 Expr::List(
466 values
467 .into_iter()
468 .map(|value| Expr::Symbol(Symbol::new(value)))
469 .collect(),
470 ),
471 )
472}
473
474fn bool_entry(key: &str, value: bool) -> (Expr, Expr) {
475 (sym(key), Expr::Bool(value))
476}
477
478fn number_entry(key: &str, value: u64) -> (Expr, Expr) {
479 (sym(key), number_literal("i64", value.to_string()))
480}
481
482fn symbol_entry(key: &str, value: &str) -> (Expr, Expr) {
483 (sym(key), Expr::Symbol(Symbol::new(value)))
484}
485
486fn string_entry(key: &str, value: &str) -> (Expr, Expr) {
487 (sym(key), Expr::String(value.to_owned()))
488}
489
490use sim_value::build::sym;
491
492fn number_literal(domain: &str, canonical: String) -> Expr {
493 Expr::Number(sim_kernel::NumberLiteral {
494 domain: Symbol::qualified("numbers", domain),
495 canonical,
496 })
497}