1mod evaluation;
4mod evaluation_scorers;
5mod evaluation_store;
6mod retrieval_evaluation;
7
8use std::{
9 collections::VecDeque,
10 sync::{Arc, Mutex},
11};
12
13use runifold_core::{
14 Budget, BudgetTracker, CapabilitySet, EventFactory, InMemoryJournal, LifecycleEvent,
15 RunContext, RunEvent, RunEventKind,
16};
17use runifold_model::{
18 Model, ModelCallContext, ModelCapabilities, ModelError, ModelErrorKind, ModelEventStream,
19 ModelFuture, ModelRef, ModelRequest, ModelResponse, ModelStreamAccumulator, ModelStreamEvent,
20};
21use serde_json::Value;
22
23pub use evaluation::{
24 EvaluationCase, EvaluationCaseId, EvaluationCaseResult, EvaluationDataset, EvaluationError,
25 EvaluationFailure, EvaluationFailureStage, EvaluationFuture, EvaluationMetrics,
26 EvaluationOutput, EvaluationReport, EvaluationRunner, EvaluationScore, EvaluationScoreSummary,
27 EvaluationScorer, EvaluationTarget, FnScorer, JsonExactMatchScorer, MetricRegression,
28 RegressionComparison, RegressionPolicy, ScoreValue,
29};
30pub use evaluation_scorers::{
31 JsonRule, JsonRuleScorer, JudgeRubric, ModelJudgeScorer, TokenOverlapScorer, WeightedJsonRule,
32};
33pub use evaluation_store::{EvaluationRepository, EvaluationStoreError, FileEvaluationRepository};
34pub use retrieval_evaluation::{
35 RetrievalCaseMetrics, RetrievalEvaluationCase, RetrievalEvaluationError,
36 RetrievalEvaluationReport, RetrievalEvaluationRunner,
37};
38
39type ModelScript = Result<Vec<ModelStreamEvent>, ModelError>;
40type ModelScriptQueue = Arc<Mutex<VecDeque<ModelScript>>>;
41
42#[derive(Debug)]
44pub struct RunScenario {
45 context: RunContext,
46 events: EventFactory,
47 journal: InMemoryJournal,
48}
49
50impl RunScenario {
51 pub fn new(budget: Budget) -> Self {
53 let context = RunContext::root(BudgetTracker::new(budget), CapabilitySet::new());
54 let events = EventFactory::new(context.run_id(), context.parent_run_id());
55 Self {
56 context,
57 events,
58 journal: InMemoryJournal::new(),
59 }
60 }
61
62 pub const fn context(&self) -> &RunContext {
64 &self.context
65 }
66
67 pub fn start(&self) -> RunEvent {
69 let event = self
70 .events
71 .emit(RunEventKind::Lifecycle(LifecycleEvent::Started), None);
72 self.journal.push(event.clone());
73 event
74 }
75
76 pub fn complete(&self, output: Value, caused_by: &RunEvent) -> RunEvent {
78 let event = self.events.emit(
79 RunEventKind::Lifecycle(LifecycleEvent::Completed { output }),
80 Some(caused_by.meta.event_id),
81 );
82 self.journal.push(event.clone());
83 event
84 }
85
86 pub fn recorded_events(&self) -> Vec<RunEvent> {
88 self.journal.events()
89 }
90}
91
92#[derive(Clone, Debug, Default)]
94pub struct ModelScenario {
95 events: Vec<ModelStreamEvent>,
96}
97
98impl ModelScenario {
99 pub fn new() -> Self {
101 Self::default()
102 }
103
104 #[must_use]
106 pub fn then(mut self, event: ModelStreamEvent) -> Self {
107 self.events.push(event);
108 self
109 }
110
111 pub fn response(self) -> Result<ModelResponse, ModelError> {
118 let mut accumulator = ModelStreamAccumulator::new();
119 for event in self.events {
120 if let Some(response) = accumulator.push(event)? {
121 return Ok(response);
122 }
123 }
124 Err(ModelError::local(
125 runifold_model::ModelErrorKind::StreamState,
126 "scripted model stream did not complete",
127 ))
128 }
129}
130
131#[derive(Clone, Debug, Default)]
136pub struct ScriptedModel {
137 capabilities: ModelCapabilities,
138 scripts: ModelScriptQueue,
139 requests: Arc<Mutex<Vec<ModelRequest>>>,
140 contexts: Arc<Mutex<Vec<ModelCallContext>>>,
141}
142
143impl ScriptedModel {
144 pub fn new() -> Self {
146 Self::default()
147 }
148
149 #[must_use]
151 pub fn with_capabilities(mut self, capabilities: ModelCapabilities) -> Self {
152 self.capabilities = capabilities;
153 self
154 }
155
156 pub fn enqueue(&self, events: impl IntoIterator<Item = ModelStreamEvent>) {
158 self.scripts()
159 .push_back(Ok(events.into_iter().collect::<Vec<_>>()));
160 }
161
162 pub fn enqueue_error(&self, error: ModelError) {
164 self.scripts().push_back(Err(error));
165 }
166
167 pub fn recorded_requests(&self) -> Vec<ModelRequest> {
169 self.requests
170 .lock()
171 .unwrap_or_else(std::sync::PoisonError::into_inner)
172 .clone()
173 }
174
175 pub fn recorded_contexts(&self) -> Vec<ModelCallContext> {
177 self.contexts
178 .lock()
179 .unwrap_or_else(std::sync::PoisonError::into_inner)
180 .clone()
181 }
182
183 fn scripts(&self) -> std::sync::MutexGuard<'_, VecDeque<ModelScript>> {
184 self.scripts
185 .lock()
186 .unwrap_or_else(std::sync::PoisonError::into_inner)
187 }
188}
189
190impl Model for ScriptedModel {
191 fn capabilities<'a>(
192 &'a self,
193 _model: &'a ModelRef,
194 ) -> ModelFuture<'a, Result<ModelCapabilities, ModelError>> {
195 let capabilities = self.capabilities.clone();
196 Box::pin(async move { Ok(capabilities) })
197 }
198
199 fn stream(
200 &self,
201 request: ModelRequest,
202 context: ModelCallContext,
203 ) -> ModelFuture<'_, Result<ModelEventStream, ModelError>> {
204 self.requests
205 .lock()
206 .unwrap_or_else(std::sync::PoisonError::into_inner)
207 .push(request);
208 self.contexts
209 .lock()
210 .unwrap_or_else(std::sync::PoisonError::into_inner)
211 .push(context);
212 let script = self.scripts().pop_front().unwrap_or_else(|| {
213 Err(ModelError::local(
214 ModelErrorKind::Protocol,
215 "scripted model has no queued invocation",
216 ))
217 });
218 Box::pin(async move {
219 let events = script?;
220 Ok(
221 Box::pin(futures_util::stream::iter(events.into_iter().map(Ok)))
222 as ModelEventStream,
223 )
224 })
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use std::collections::BTreeMap;
231
232 use runifold_core::Budget;
233 use runifold_model::{
234 ContentBlockKind, ContentPart, FinishReason, Message, Model, ModelCallContext,
235 ModelErrorKind, ModelRef, ModelRequest, ModelStreamEvent,
236 };
237
238 use super::{ModelScenario, RunScenario, ScriptedModel};
239
240 #[test]
241 fn scenario_records_a_causal_lifecycle() {
242 let scenario = RunScenario::new(Budget::default());
243 let started = scenario.start();
244 let completed = scenario.complete(serde_json::json!({"ok": true}), &started);
245 let events = scenario.recorded_events();
246
247 assert_eq!(events, vec![started.clone(), completed.clone()]);
248 assert_eq!(completed.meta.caused_by, Some(started.meta.event_id));
249 }
250
251 #[test]
252 fn model_scenario_accumulates_a_response() {
253 let response = ModelScenario::new()
254 .then(ModelStreamEvent::ResponseStarted {
255 id: Some("test-response".into()),
256 model: ModelRef::new("test", "scripted"),
257 })
258 .then(ModelStreamEvent::ContentBlockStarted {
259 index: 0,
260 kind: ContentBlockKind::Text,
261 })
262 .then(ModelStreamEvent::TextDelta {
263 index: 0,
264 text: "hello".into(),
265 })
266 .then(ModelStreamEvent::ContentBlockCompleted { index: 0 })
267 .then(ModelStreamEvent::ResponseCompleted {
268 finish_reason: FinishReason::Stop,
269 provider_metadata: BTreeMap::new(),
270 })
271 .response()
272 .unwrap();
273
274 assert_eq!(response.content, vec![ContentPart::text("hello")]);
275 }
276
277 #[test]
278 fn scripted_model_uses_the_same_stream_accumulator_as_real_adapters() {
279 let model = ScriptedModel::new();
280 model.enqueue([
281 ModelStreamEvent::ResponseStarted {
282 id: Some("test-response".into()),
283 model: ModelRef::new("test", "scripted"),
284 },
285 ModelStreamEvent::ContentPartCompleted {
286 index: 0,
287 part: ContentPart::text("hello"),
288 },
289 ModelStreamEvent::ResponseCompleted {
290 finish_reason: FinishReason::Stop,
291 provider_metadata: BTreeMap::new(),
292 },
293 ]);
294 let request = ModelRequest::new(ModelRef::new("test", "scripted"), Message::user("hello"));
295
296 let response = futures_executor::block_on(model.invoke(request, ModelCallContext::new()))
297 .expect("script should complete");
298
299 assert_eq!(response.content, vec![ContentPart::text("hello")]);
300 }
301
302 #[test]
303 fn scripted_model_invocation_observes_preexisting_cancellation() {
304 let model = ScriptedModel::new();
305 model.enqueue([]);
306 let request = ModelRequest::new(ModelRef::new("test", "scripted"), Message::user("hello"));
307 let context = ModelCallContext::new();
308 context.cancellation().cancel();
309
310 let error = futures_executor::block_on(model.invoke(request, context))
311 .expect_err("cancelled invocation must fail");
312
313 assert_eq!(error.kind, ModelErrorKind::Cancelled);
314 }
315}