potato_type/traits.rs
1use crate::tools::ToolCallInfo;
2use crate::{
3 error::TypeError,
4 prompt::{MessageNum, ModelSettings, ResponseContent},
5 tools::AgentToolDefinition,
6 Provider,
7};
8use potato_util::utils::TokenLogProbs;
9use pyo3::prelude::*;
10use pyo3::types::PyList;
11use regex::Regex;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::sync::OnceLock;
15
16pub static VAR_REGEX: OnceLock<Regex> = OnceLock::new();
17pub fn get_var_regex() -> &'static Regex {
18 VAR_REGEX.get_or_init(|| Regex::new(r"\$?\{([a-zA-Z_][a-zA-Z0-9_]*)\}").unwrap())
19}
20use crate::prompt::builder::ProviderRequest;
21/// Core trait that all message types must implement
22pub trait PromptMessageExt:
23 Send + Sync + Clone + Serialize + for<'de> Deserialize<'de> + PartialEq
24{
25 /// Bind a variable in the message content, returning a new instance
26 fn bind(&self, name: &str, value: &str) -> Result<Self, TypeError>
27 where
28 Self: Sized;
29
30 /// Bind a variable in-place
31 fn bind_mut(&mut self, name: &str, value: &str) -> Result<(), TypeError>;
32
33 /// Extract variables from the message content
34 fn extract_variables(&self) -> Vec<String>;
35
36 fn from_text(content: String, role: &str) -> Result<Self, TypeError>;
37}
38
39/// Core trait that must be implemented for all request types
40pub trait RequestAdapter {
41 /// Returns all messages in the request
42 fn messages(&self) -> &[MessageNum];
43 /// Returns a mutable reference to the messages in the request
44 fn messages_mut(&mut self) -> &mut Vec<MessageNum>;
45 /// Returns all system instructions in the request
46 fn system_instructions(&self) -> Vec<&MessageNum>;
47 /// Returns the response JSON schema if set
48 fn response_json_schema(&self) -> Option<&Value>;
49 /// Inserts a message at the specified index (or at the start if None)
50 fn insert_message(&mut self, message: MessageNum, idx: Option<usize>) {
51 self.messages_mut().insert(idx.unwrap_or(0), message);
52 }
53 /// Prepends system instructions to the messages
54 fn preprend_system_instructions(&mut self, messages: Vec<MessageNum>) -> Result<(), TypeError>;
55
56 /// Returns the system instructions as a Python list
57 /// # Arguments
58 /// * `py` - The Python GIL token
59 /// # Returns
60 /// Returns a Python list of system instruction messages
61 fn get_py_system_instructions<'py>(
62 &self,
63 py: Python<'py>,
64 ) -> Result<Bound<'py, PyList>, TypeError>;
65 /// Returns the model settings for the request (python object)
66 fn model_settings<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError>;
67
68 /// Converts the request to a JSON value for sending to the provider
69 fn to_request_body(&self) -> Result<Value, TypeError>;
70 /// Checks if the request matches the given provider
71 fn match_provider(&self, provider: &Provider) -> bool;
72 /// Builds a provider-specific request enum from the given parameters
73 /// The ProviderRequest enum encapsulates all supported provider request types and is an
74 /// attribute of the Prompt struct. ProviderRequest is built on instantiation of the Prompt
75 fn build_provider_enum(
76 messages: Vec<MessageNum>,
77 system_instructions: Vec<MessageNum>,
78 model: String,
79 settings: ModelSettings,
80 response_json_schema: Option<Value>,
81 ) -> Result<ProviderRequest, TypeError>;
82
83 /// Sets the response JSON schema for the request
84 /// Typically used as part of workflows when adding tasks
85 fn set_response_json_schema(&mut self, response_json_schema: Option<Value>) -> ();
86
87 /// Adds tools to the request
88 fn add_tools(&mut self, tools: Vec<AgentToolDefinition>) -> Result<(), TypeError>;
89}
90
91pub trait ResponseAdapter {
92 /// Returns a string representation of the response
93 fn __str__(&self) -> String;
94
95 /// Checks if the response is empty
96 fn is_empty(&self) -> bool;
97
98 /// Converts the response to a Python object
99 fn to_bound_py_object<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError>;
100
101 /// Returns the response ID
102 fn id(&self) -> &str;
103
104 /// Converts the response to a vector of MessageNum
105 fn to_message_num(&self) -> Result<Vec<MessageNum>, TypeError>;
106
107 // Get the token usage as a Python object
108 fn usage<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError>;
109
110 /// Retrieves the first content choice from the response
111 fn get_content(&self) -> ResponseContent;
112
113 /// Retrieves the log probabilities from the response
114 fn get_log_probs(&self) -> Vec<TokenLogProbs>;
115
116 /// Returns the structured output of the response
117 /// For all response types the flow is as follows:
118 /// 1. Check if the response has content (string/text)
119 /// 2. If no content, return Python None
120 /// 3. If content exists, check if an output_type/model is provided
121 /// 4. If output_type/model is provided, attempt to convert the content to that type
122 /// 5. If conversion fails, attempt to construct a generic Python object from the content
123 /// 6. If no output_type/model is provided, return the content as a generic Python object
124 /// # Arguments
125 /// * `py`: The Python GIL token
126 /// * `output_type`: An optional Python type/model to convert the content into. This can be a pydantic model or any object
127 /// that implements model_validate_json that can parse from a JSON string.
128 /// # Returns
129 /// * `Result<Bound<'py, PyAny>, TypeError>`: The structured output as a Python object or an error
130 fn structured_output<'py>(
131 &self,
132 py: Python<'py>,
133 output_type: Option<&Bound<'py, PyAny>>,
134 ) -> Result<Bound<'py, PyAny>, TypeError>;
135
136 /// Returns the structured output value as a serde_json::Value
137 fn structured_output_value(&self) -> Option<Value>;
138
139 /// Returns any tool calls made in the response, if applicable
140 fn tool_call_output(&self) -> Option<Value>;
141
142 /// Returns the output text of the response if available
143 fn response_text(&self) -> String;
144
145 /// Extracts tool calls from the response, if any.
146 /// Returns None if the response contains no tool calls.
147 fn extract_tool_calls(&self) -> Option<Vec<crate::tools::ToolCall>> {
148 None
149 }
150 fn model_name(&self) -> Option<&str>;
151
152 fn finish_reason(&self) -> Option<&str>;
153
154 fn input_tokens(&self) -> Option<i64>;
155
156 fn output_tokens(&self) -> Option<i64>;
157
158 fn total_tokens(&self) -> Option<i64>;
159
160 fn get_tool_calls(&self) -> Vec<ToolCallInfo>;
161}
162
163pub trait MessageResponseExt {
164 fn to_message_num(&self) -> Result<MessageNum, TypeError>;
165}
166
167pub trait MessageFactory: Sized {
168 fn from_text(content: String, role: &str) -> Result<Self, TypeError>;
169}
170
171/// Trait for converting between different provider message formats
172///
173/// This trait enables conversion of messages between different LLM provider formats
174/// (e.g., Anthropic MessageParam ↔ Google GeminiContent ↔ OpenAI ChatMessage).
175///
176/// Currently focused on text content conversion, with support for other content
177/// types planned for future implementation.
178pub trait MessageConversion {
179 /// Convert this message to an Anthropic MessageParam
180 ///
181 /// # Errors
182 /// Returns `TypeError::UnsupportedConversion` if the message contains
183 /// content types that cannot be represented in Anthropic's format
184 fn to_anthropic_message(
185 &self,
186 ) -> Result<crate::anthropic::v1::request::MessageParam, TypeError>;
187
188 /// Convert this message to a Google GeminiContent
189 ///
190 /// # Errors
191 /// Returns `TypeError::UnsupportedConversion` if the message contains
192 /// content types that cannot be represented in Google's format
193 fn to_google_message(
194 &self,
195 ) -> Result<crate::google::v1::generate::request::GeminiContent, TypeError>;
196
197 /// Convert this message to an OpenAI ChatMessage
198 ///
199 /// # Errors
200 /// Returns `TypeError::UnsupportedConversion` if the message contains
201 /// content types that cannot be represented in OpenAI's format
202 fn to_openai_message(&self)
203 -> Result<crate::openai::v1::chat::request::ChatMessage, TypeError>;
204}