switchyard_libsy/core/
processor.rs1use crate::Result;
5use async_trait::async_trait;
6use switchyard_protocol::{AggLlmResponse, Decision, Request, Signals};
7
8pub enum Event<'a> {
14 Request(&'a mut Request),
16 Signal(&'a Signals),
18 Decision {
23 request: &'a mut Request,
25 decision: &'a dyn Decision,
27 },
28 ModelResponse(&'a AggLlmResponse),
30}
31
32#[async_trait]
34pub trait Processor<S = ()>: Send + Sync {
35 async fn process(&self, state: &mut S, event: Event<'_>) -> Result<()>;
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use std::collections::HashMap;
44 use switchyard_protocol::{text_request, text_response};
45
46 type TestState = HashMap<&'static str, u32>;
47
48 fn event_key(event: &Event<'_>) -> &'static str {
50 match event {
51 Event::Request(_) => "requests",
52 Event::Signal(_) => "signals",
53 Event::Decision { .. } => "decisions",
54 Event::ModelResponse(_) => "model_responses",
55 }
56 }
57
58 fn count(state: &TestState, key: &'static str) -> u32 {
60 state.get(key).copied().unwrap_or_default()
61 }
62
63 struct CountingProcessor;
65
66 #[async_trait]
67 impl Processor<TestState> for CountingProcessor {
68 async fn process(&self, state: &mut TestState, event: Event<'_>) -> Result<()> {
69 *state.entry(event_key(&event)).or_default() += 1;
70 Ok(())
71 }
72 }
73
74 struct TestDecision;
76
77 impl Decision for TestDecision {
78 fn selected_model(&self) -> &str {
79 "test/model"
80 }
81 fn reasoning(&self) -> Option<&str> {
82 None
83 }
84 fn as_any(&self) -> &dyn std::any::Any {
85 self
86 }
87 }
88
89 fn request() -> Request {
90 Request {
91 llm_request: text_request(Some("auto".to_string()), "hi"),
92 raw_request: None,
93 metadata: None,
94 }
95 }
96
97 #[tokio::test]
98 async fn processor_tallies_each_event_variant_into_state() -> Result<()> {
99 let processor = CountingProcessor;
100 let mut state = TestState::default();
101 let mut req = request();
102 let response = text_response(None, "ok");
103 let decision = TestDecision;
104 let signals = Signals {};
105
106 processor
108 .process(&mut state, Event::Request(&mut req))
109 .await?;
110 processor
111 .process(&mut state, Event::ModelResponse(&response))
112 .await?;
113 processor
114 .process(
115 &mut state,
116 Event::Decision {
117 request: &mut req,
118 decision: &decision,
119 },
120 )
121 .await?;
122 processor
123 .process(&mut state, Event::Signal(&signals))
124 .await?;
125
126 assert_eq!(count(&state, "requests"), 1);
127 assert_eq!(count(&state, "signals"), 1);
128 assert_eq!(count(&state, "decisions"), 1);
129 assert_eq!(count(&state, "model_responses"), 1);
130 Ok(())
131 }
132
133 #[tokio::test]
134 async fn process_accumulates_state_across_repeated_events() -> Result<()> {
135 let processor = CountingProcessor;
136 let mut state = TestState::default();
137 let mut req = request();
138
139 for _ in 0..3 {
140 processor
141 .process(&mut state, Event::Request(&mut req))
142 .await?;
143 }
144
145 assert_eq!(count(&state, "requests"), 3);
146 Ok(())
147 }
148
149 struct RewritingProcessor;
151
152 #[async_trait]
153 impl Processor for RewritingProcessor {
154 async fn process(&self, _state: &mut (), event: Event<'_>) -> Result<()> {
155 match event {
156 Event::Request(request) | Event::Decision { request, .. } => {
157 request.llm_request.model = Some("rewritten".to_string());
158 }
159 _ => {}
160 }
161 Ok(())
162 }
163 }
164
165 #[tokio::test]
166 async fn processor_rewrites_the_request_in_place() -> Result<()> {
167 let mut state = ();
168 let mut req = request();
169 assert_eq!(req.requested_model(), Some("auto"));
170
171 RewritingProcessor
172 .process(&mut state, Event::Request(&mut req))
173 .await?;
174
175 assert_eq!(req.requested_model(), Some("rewritten"));
177 Ok(())
178 }
179}