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