switchyard_libsy/core/
processor.rs1use crate::Result;
5use crate::core::algorithm::Driver;
6use crate::core::state::State;
7use async_trait::async_trait;
8use switchyard_protocol::{AggLlmResponse, Category, ModelId, Request};
9
10pub enum Event<'a> {
16 Request {
18 request: &'a mut Request,
20 driver: &'a Driver,
22 },
23 Decision {
28 request: &'a mut Request,
30 selected_model_id: &'a ModelId,
32 category: Option<Category>,
35 driver: &'a Driver,
37 },
38 ModelResponse(&'a AggLlmResponse),
40}
41
42#[async_trait]
44pub trait Processor: Send + Sync {
45 async fn process(&self, state: &mut State, event: Event<'_>) -> Result<()>;
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use crate::core::state::StateValue;
54 use crate::core::testing::empty_driver;
55 use switchyard_protocol::{text_request, text_response};
56
57 fn event_key(event: &Event<'_>) -> &'static str {
59 match event {
60 Event::Request { .. } => "requests",
61 Event::Decision { .. } => "decisions",
62 Event::ModelResponse(_) => "model_responses",
63 }
64 }
65
66 fn count(state: &State, key: &'static str) -> u32 {
68 match state.extra.get(key) {
69 Some(StateValue::Count(n)) => *n,
70 _ => 0,
71 }
72 }
73
74 struct CountingProcessor;
76
77 #[async_trait]
78 impl Processor for CountingProcessor {
79 async fn process(&self, state: &mut State, event: Event<'_>) -> Result<()> {
80 let key = event_key(&event);
81 let next = count(state, key) + 1;
82 state.extra.insert(key.to_string(), StateValue::Count(next));
83 Ok(())
84 }
85 }
86
87 fn request() -> Request {
88 Request {
89 llm_request: text_request(Some("auto".to_string()), "hi"),
90 raw_request: None,
91 metadata: None,
92 }
93 }
94
95 #[tokio::test]
96 async fn processor_tallies_each_event_variant_into_state() -> Result<()> {
97 let processor = CountingProcessor;
98 let mut state = State::default();
99 let mut req = request();
100 let response = text_response(None, "ok");
101 let selected_model_id = ModelId::from("test/model");
102 processor
104 .process(
105 &mut state,
106 Event::Request {
107 request: &mut req,
108 driver: &empty_driver(),
109 },
110 )
111 .await?;
112 processor
113 .process(&mut state, Event::ModelResponse(&response))
114 .await?;
115 processor
116 .process(
117 &mut state,
118 Event::Decision {
119 request: &mut req,
120 selected_model_id: &selected_model_id,
121 category: None,
122 driver: &empty_driver(),
123 },
124 )
125 .await?;
126 assert_eq!(count(&state, "requests"), 1);
127 assert_eq!(count(&state, "decisions"), 1);
128 assert_eq!(count(&state, "model_responses"), 1);
129 Ok(())
130 }
131
132 #[tokio::test]
133 async fn process_accumulates_state_across_repeated_events() -> Result<()> {
134 let processor = CountingProcessor;
135 let mut state = State::default();
136 let mut req = request();
137
138 for _ in 0..3 {
139 processor
140 .process(
141 &mut state,
142 Event::Request {
143 request: &mut req,
144 driver: &empty_driver(),
145 },
146 )
147 .await?;
148 }
149
150 assert_eq!(count(&state, "requests"), 3);
151 Ok(())
152 }
153
154 struct RewritingProcessor;
156
157 #[async_trait]
158 impl Processor for RewritingProcessor {
159 async fn process(&self, _state: &mut State, event: Event<'_>) -> Result<()> {
160 match event {
161 Event::Request { request, .. } | Event::Decision { request, .. } => {
162 request.llm_request.model = Some("rewritten".to_string());
163 }
164 _ => {}
165 }
166 Ok(())
167 }
168 }
169
170 #[tokio::test]
171 async fn processor_rewrites_the_request_in_place() -> Result<()> {
172 let mut state = State::default();
173 let mut req = request();
174 assert_eq!(req.model_id(), Some("auto".into()));
175
176 RewritingProcessor
177 .process(
178 &mut state,
179 Event::Request {
180 request: &mut req,
181 driver: &empty_driver(),
182 },
183 )
184 .await?;
185
186 assert_eq!(req.model_id(), Some("rewritten".into()));
188 Ok(())
189 }
190}