Skip to main content

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}