1use std::sync::Arc;
8
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use behest_core::error::ContextError;
14use behest_provider::{ChatRequest, Message, ModelName, ToolChoice, ToolSpec};
15
16pub type ContextResult<T> = std::result::Result<T, ContextError>;
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct ContextInput {
22 pub user_message: Option<String>,
24 pub session_id: Option<String>,
26 pub metadata: Value,
28}
29
30impl ContextInput {
31 #[must_use]
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 #[must_use]
39 pub fn with_user_message(mut self, message: impl Into<String>) -> Self {
40 self.user_message = Some(message.into());
41 self
42 }
43
44 #[must_use]
46 pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
47 self.session_id = Some(session_id.into());
48 self
49 }
50
51 #[must_use]
53 pub fn with_metadata(mut self, metadata: Value) -> Self {
54 self.metadata = metadata;
55 self
56 }
57}
58
59#[derive(Debug, Clone, Default)]
61pub struct ContextOutput {
62 messages: Vec<Message>,
63}
64
65impl ContextOutput {
66 #[must_use]
68 pub fn new() -> Self {
69 Self::default()
70 }
71
72 #[must_use]
74 pub fn from_messages(messages: Vec<Message>) -> Self {
75 Self { messages }
76 }
77
78 #[must_use]
80 pub fn messages(&self) -> &[Message] {
81 &self.messages
82 }
83
84 #[must_use]
86 pub fn messages_mut(&mut self) -> &mut Vec<Message> {
87 &mut self.messages
88 }
89
90 #[must_use]
92 pub fn into_messages(self) -> Vec<Message> {
93 self.messages
94 }
95
96 pub fn extend(&mut self, messages: impl IntoIterator<Item = Message>) {
98 self.messages.extend(messages);
99 }
100
101 #[must_use]
103 pub fn into_request(self, model: ModelName) -> ChatRequest {
104 ChatRequest {
105 model,
106 messages: self.messages,
107 tools: Vec::new(),
108 tool_choice: ToolChoice::default(),
109 response_format: None,
110 temperature: None,
111 top_p: None,
112 max_output_tokens: None,
113 stop: Vec::new(),
114 metadata: Value::Null,
115 }
116 }
117
118 #[must_use]
120 pub fn into_request_with_tools(self, model: ModelName, tools: &[ToolSpec]) -> ChatRequest {
121 ChatRequest {
122 model,
123 messages: self.messages,
124 tools: tools.to_vec(),
125 tool_choice: ToolChoice::default(),
126 response_format: None,
127 temperature: None,
128 top_p: None,
129 max_output_tokens: None,
130 stop: Vec::new(),
131 metadata: Value::Null,
132 }
133 }
134}
135
136#[async_trait]
138pub trait ContextAdapter: Send + Sync {
139 fn name(&self) -> &str;
141
142 async fn produce(&self, input: &ContextInput) -> ContextResult<Vec<Message>>;
144}
145
146pub struct StaticAdapter {
148 name: String,
149 messages: Vec<Message>,
150}
151
152impl StaticAdapter {
153 #[must_use]
155 pub fn system(text: impl Into<String>) -> Self {
156 Self {
157 name: "system".to_owned(),
158 messages: vec![Message::system_text(text)],
159 }
160 }
161
162 #[must_use]
164 pub fn user(text: impl Into<String>) -> Self {
165 Self {
166 name: "user".to_owned(),
167 messages: vec![Message::user_text(text)],
168 }
169 }
170
171 #[must_use]
173 pub fn messages(name: impl Into<String>, messages: Vec<Message>) -> Self {
174 Self {
175 name: name.into(),
176 messages,
177 }
178 }
179}
180
181#[async_trait]
182impl ContextAdapter for StaticAdapter {
183 fn name(&self) -> &str {
184 &self.name
185 }
186
187 async fn produce(&self, _input: &ContextInput) -> ContextResult<Vec<Message>> {
188 Ok(self.messages.clone())
189 }
190}
191
192pub struct FunctionAdapter<F> {
194 name: String,
195 handler: F,
196}
197
198impl<F, Fut> FunctionAdapter<F>
199where
200 F: Fn(ContextInput) -> Fut + Send + Sync + 'static,
201 Fut: std::future::Future<Output = ContextResult<Vec<Message>>> + Send + 'static,
202{
203 #[must_use]
205 pub fn new(name: impl Into<String>, handler: F) -> Self {
206 Self {
207 name: name.into(),
208 handler,
209 }
210 }
211}
212
213#[async_trait]
214impl<F, Fut> ContextAdapter for FunctionAdapter<F>
215where
216 F: Fn(ContextInput) -> Fut + Send + Sync + 'static,
217 Fut: std::future::Future<Output = ContextResult<Vec<Message>>> + Send + 'static,
218{
219 fn name(&self) -> &str {
220 &self.name
221 }
222
223 async fn produce(&self, input: &ContextInput) -> ContextResult<Vec<Message>> {
224 (self.handler)(input.clone()).await
225 }
226}
227
228#[derive(Clone, Default)]
233pub struct ContextFactory {
234 adapters: Vec<Arc<dyn ContextAdapter>>,
235}
236
237impl ContextFactory {
238 #[must_use]
240 pub fn new() -> Self {
241 Self::default()
242 }
243
244 pub fn register<A>(&mut self, adapter: A)
246 where
247 A: ContextAdapter + 'static,
248 {
249 self.adapters.push(Arc::new(adapter));
250 }
251
252 pub fn register_arc(&mut self, adapter: Arc<dyn ContextAdapter>) {
254 self.adapters.push(adapter);
255 }
256
257 #[must_use]
259 pub fn len(&self) -> usize {
260 self.adapters.len()
261 }
262
263 #[must_use]
265 pub fn is_empty(&self) -> bool {
266 self.adapters.is_empty()
267 }
268
269 pub fn adapter_names(&self) -> impl Iterator<Item = &str> {
271 self.adapters.iter().map(|a| a.name())
272 }
273
274 pub async fn build(&self, input: &ContextInput) -> ContextResult<ContextOutput> {
280 let mut output = ContextOutput::new();
281
282 for adapter in &self.adapters {
283 let messages =
284 adapter
285 .produce(input)
286 .await
287 .map_err(|e| ContextError::AdapterFailed {
288 adapter: adapter.name().to_owned(),
289 message: e.to_string(),
290 })?;
291 output.extend(messages);
292 }
293
294 Ok(output)
295 }
296
297 pub async fn build_request(
307 &self,
308 input: &ContextInput,
309 model: ModelName,
310 tool_specs: Option<&[ToolSpec]>,
311 ) -> ContextResult<ChatRequest> {
312 let output = self.build(input).await?;
313
314 let request = if let Some(specs) = tool_specs {
315 output.into_request_with_tools(model, specs)
316 } else {
317 output.into_request(model)
318 };
319
320 Ok(request)
321 }
322}
323
324#[cfg(test)]
325#[allow(clippy::unwrap_used)]
326mod tests {
327 use super::*;
328 use serde_json::json;
329
330 #[test]
331 fn context_input_should_support_builder_pattern() {
332 let input = ContextInput::new()
333 .with_user_message("Hello")
334 .with_session_id("session_123")
335 .with_metadata(json!({"key": "value"}));
336
337 assert_eq!(input.user_message, Some("Hello".to_owned()));
338 assert_eq!(input.session_id, Some("session_123".to_owned()));
339 assert_eq!(input.metadata, json!({"key": "value"}));
340 }
341
342 #[test]
343 fn context_output_should_be_empty_when_new() {
344 let output = ContextOutput::new();
345 assert!(output.messages().is_empty());
346 }
347
348 #[test]
349 fn context_output_should_extend_messages() {
350 let mut output = ContextOutput::new();
351 output.extend(vec![
352 Message::system_text("System"),
353 Message::user_text("User"),
354 ]);
355
356 assert_eq!(output.messages().len(), 2);
357 }
358
359 #[test]
360 fn context_output_should_convert_to_request() {
361 let output = ContextOutput::from_messages(vec![
362 Message::system_text("System"),
363 Message::user_text("User"),
364 ]);
365
366 let request = output.into_request(ModelName::new("gpt-4"));
367
368 assert_eq!(request.model.as_str(), "gpt-4");
369 assert_eq!(request.messages.len(), 2);
370 assert!(request.tools.is_empty());
371 }
372
373 #[test]
374 fn context_output_should_convert_to_request_with_tools() {
375 let output = ContextOutput::from_messages(vec![Message::user_text("Hello")]);
376 let tools = vec![ToolSpec::new("echo", "Echo tool", json!({}))];
377
378 let request = output.into_request_with_tools(ModelName::new("gpt-4"), &tools);
379
380 assert_eq!(request.tools.len(), 1);
381 assert_eq!(request.tools[0].name, "echo");
382 }
383
384 #[test]
385 fn context_factory_should_be_empty_when_new() {
386 let factory = ContextFactory::new();
387 assert!(factory.is_empty());
388 assert_eq!(factory.len(), 0);
389 }
390
391 #[test]
392 fn context_factory_should_register_adapters() {
393 let mut factory = ContextFactory::new();
394 factory.register(StaticAdapter::system("System prompt"));
395 factory.register(StaticAdapter::user("User message"));
396
397 assert_eq!(factory.len(), 2);
398 }
399
400 #[test]
401 fn context_factory_should_list_adapter_names() {
402 let mut factory = ContextFactory::new();
403 factory.register(StaticAdapter::system("System"));
404 factory.register(StaticAdapter::user("User"));
405
406 let names: Vec<&str> = factory.adapter_names().collect();
407 assert_eq!(names, vec!["system", "user"]);
408 }
409
410 #[tokio::test]
411 async fn context_factory_should_build_output_in_order() {
412 let mut factory = ContextFactory::new();
413 factory.register(StaticAdapter::system("First"));
414 factory.register(StaticAdapter::user("Second"));
415
416 let input = ContextInput::new();
417 let output = factory.build(&input).await.unwrap();
418
419 assert_eq!(output.messages().len(), 2);
420 }
421
422 #[tokio::test]
423 async fn context_factory_should_build_request_with_tools() {
424 let mut factory = ContextFactory::new();
425 factory.register(StaticAdapter::system("You are helpful."));
426
427 let input = ContextInput::new().with_user_message("Hello");
428 let tools = vec![ToolSpec::new("echo", "Echo tool", json!({}))];
429 let request = factory
430 .build_request(&input, ModelName::new("gpt-4"), Some(&tools))
431 .await
432 .unwrap();
433
434 assert_eq!(request.messages.len(), 1);
435 assert_eq!(request.tools.len(), 1);
436 }
437
438 #[tokio::test]
439 async fn static_adapter_should_produce_system_message() {
440 let adapter = StaticAdapter::system("You are a helpful assistant.");
441 let input = ContextInput::new();
442 let messages = adapter.produce(&input).await.unwrap();
443
444 assert_eq!(messages.len(), 1);
445 assert!(matches!(messages[0], Message::System { .. }));
446 }
447
448 #[tokio::test]
449 async fn static_adapter_should_produce_user_message() {
450 let adapter = StaticAdapter::user("Hello");
451 let input = ContextInput::new();
452 let messages = adapter.produce(&input).await.unwrap();
453
454 assert_eq!(messages.len(), 1);
455 assert!(matches!(messages[0], Message::User { .. }));
456 }
457
458 #[tokio::test]
459 async fn function_adapter_should_invoke_handler() {
460 let adapter = FunctionAdapter::new("custom", |input: ContextInput| async move {
461 let msg = input.user_message.unwrap_or_default();
462 Ok(vec![Message::user_text(format!("Echo: {msg}"))])
463 });
464
465 let input = ContextInput::new().with_user_message("test");
466 let messages = adapter.produce(&input).await.unwrap();
467
468 assert_eq!(messages.len(), 1);
469 }
470}