Skip to main content

pe_core/
formatter.rs

1//! Message formatter trait -- decouples wire format from LLM provider.
2//!
3//! Each LLM provider (OpenAI, Anthropic, custom) has its own message format.
4//! The `MessageFormatter` trait handles translation so providers only deal
5//! with their native format.
6//!
7//! # Example
8//!
9//! ```
10//! use pe_core::formatter::MessageFormatter;
11//! use pe_core::openai_formatter::OpenAiFormatter;
12//! use pe_core::{Message, ToolSchema, LlmResponse};
13//!
14//! let formatter = OpenAiFormatter;
15//! let messages = vec![Message::human("Hello"), Message::system("Be helpful")];
16//! let wire = formatter.format_messages(&messages).unwrap();
17//! assert!(wire.is_array());
18//! ```
19
20use crate::error::PeError;
21use crate::llm::{LlmResponse, ToolSchema};
22use crate::message::Message;
23
24/// Trait for converting between library Message types and provider-specific wire formats.
25///
26/// Each LLM provider has its own message format. The formatter handles
27/// the translation so providers only deal with their native format.
28///
29/// Implementors produce `serde_json::Value` so the provider HTTP layer can
30/// embed the result directly into request bodies without further conversion.
31pub trait MessageFormatter: Send + Sync + 'static {
32    /// Provider name for debugging and logging.
33    ///
34    /// # Example
35    ///
36    /// ```
37    /// use pe_core::openai_formatter::OpenAiFormatter;
38    /// use pe_core::formatter::MessageFormatter;
39    ///
40    /// assert_eq!(OpenAiFormatter.name(), "openai");
41    /// ```
42    fn name(&self) -> &str;
43
44    /// Convert library messages to the provider's wire format (as JSON Value).
45    ///
46    /// Returns an array of message objects in the provider's expected format.
47    /// Unknown or unsupported message variants are silently skipped.
48    fn format_messages(&self, messages: &[Message]) -> Result<serde_json::Value, PeError>;
49
50    /// Convert tool schemas to the provider's tool format (as JSON Value).
51    ///
52    /// Returns an array of tool definition objects. Returns an empty array
53    /// for empty input.
54    fn format_tools(&self, tools: &[ToolSchema]) -> Result<serde_json::Value, PeError>;
55
56    /// Parse a provider response back into library types.
57    ///
58    /// Takes the raw JSON response body from the provider and extracts
59    /// the AI message, usage metadata, and provider-specific metadata.
60    fn parse_response(&self, raw: &serde_json::Value) -> Result<LlmResponse, PeError>;
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    /// Verify the trait is object-safe (can be used as `Box<dyn MessageFormatter>`).
68    #[test]
69    fn test_message_formatter_is_object_safe() {
70        fn _accepts_boxed(_f: Box<dyn MessageFormatter>) {}
71        // Compiles = object-safe. No runtime assertion needed.
72    }
73}