Skip to main content

potato_type/
lib.rs

1pub mod error;
2
3pub use crate::error::TypeError;
4use pyo3::prelude::*;
5use schemars::JsonSchema;
6use serde::de::Error;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::any::type_name;
10use std::fmt;
11use std::fmt::Display;
12use std::path::{Path, PathBuf};
13use tracing::error;
14pub mod anthropic;
15pub mod common;
16pub mod google;
17pub mod openai;
18pub mod prompt;
19pub mod spec;
20pub mod tools;
21pub mod traits;
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
24#[pyclass(from_py_object)]
25pub enum Model {
26    Undefined,
27}
28
29impl Model {
30    pub fn as_str(&self) -> &str {
31        match self {
32            Model::Undefined => "undefined",
33        }
34    }
35
36    pub fn from_string(s: &str) -> Result<Self, TypeError> {
37        match s.to_lowercase().as_str() {
38            "undefined" => Ok(Model::Undefined),
39            _ => Err(TypeError::UnknownModelError(s.to_string())),
40        }
41    }
42}
43
44pub enum Common {
45    Undefined,
46}
47
48impl Common {
49    pub fn as_str(&self) -> &str {
50        match self {
51            Common::Undefined => "undefined",
52        }
53    }
54}
55
56impl Display for Common {
57    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58        write!(f, "{}", self.as_str())
59    }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
63#[pyclass(from_py_object, eq, eq_int)]
64pub enum Provider {
65    OpenAI,
66    Gemini,
67    Google,
68    Vertex,
69    Anthropic,
70    GoogleAdk,
71    Undefined, // Added Undefined for better error handling
72}
73
74impl Provider {
75    pub fn from_string(s: &str) -> Result<Self, TypeError> {
76        match s.to_lowercase().as_str() {
77            "openai" => Ok(Provider::OpenAI),
78            "gemini" => Ok(Provider::Gemini),
79            "google" => Ok(Provider::Google),
80            "vertex" => Ok(Provider::Vertex),
81            "anthropic" => Ok(Provider::Anthropic),
82            "google_adk" => Ok(Provider::GoogleAdk),
83            "undefined" => Ok(Provider::Undefined), // Handle undefined case
84            _ => Err(TypeError::UnknownProviderError(s.to_string())),
85        }
86    }
87
88    /// Extract provider from a PyAny object
89    ///
90    /// # Arguments
91    /// * `provider` - PyAny object
92    ///
93    /// # Returns
94    /// * `Result<Provider, AgentError>` - Result
95    ///
96    /// # Errors
97    /// * `AgentError` - Error
98    pub fn extract_provider(provider: &Bound<'_, PyAny>) -> Result<Provider, TypeError> {
99        match provider.is_instance_of::<Provider>() {
100            true => Ok(provider.extract::<Provider>().inspect_err(|e| {
101                error!("Failed to extract provider: {}", e);
102            })?),
103            false => {
104                let provider = provider.extract::<String>().unwrap();
105                Ok(Provider::from_string(&provider).inspect_err(|e| {
106                    error!("Failed to convert string to provider: {}", e);
107                })?)
108            }
109        }
110    }
111
112    pub fn as_str(&self) -> &str {
113        match self {
114            Provider::OpenAI => "openai",
115            Provider::Gemini => "gemini",
116            Provider::Vertex => "vertex",
117            Provider::Google => "google",
118            Provider::Anthropic => "anthropic",
119            Provider::GoogleAdk => "google_adk",
120            Provider::Undefined => "undefined", // Added Undefined case
121        }
122    }
123}
124
125impl Display for Provider {
126    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
127        write!(f, "{}", self.as_str())
128    }
129}
130
131#[pyclass(from_py_object, eq, eq_int)]
132#[derive(Debug, PartialEq, Clone)]
133pub enum SaveName {
134    Prompt,
135}
136
137#[pymethods]
138impl SaveName {
139    #[staticmethod]
140    pub fn from_string(s: &str) -> Option<Self> {
141        match s {
142            "prompt" => Some(SaveName::Prompt),
143
144            _ => None,
145        }
146    }
147
148    pub fn as_string(&self) -> &str {
149        match self {
150            SaveName::Prompt => "prompt",
151        }
152    }
153
154    pub fn __str__(&self) -> String {
155        self.to_string()
156    }
157}
158
159impl Display for SaveName {
160    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
161        write!(f, "{}", self.as_string())
162    }
163}
164
165impl AsRef<Path> for SaveName {
166    fn as_ref(&self) -> &Path {
167        match self {
168            SaveName::Prompt => Path::new("prompt"),
169        }
170    }
171}
172
173// impl PathBuf: From<SaveName>
174impl From<SaveName> for PathBuf {
175    fn from(save_name: SaveName) -> Self {
176        PathBuf::from(save_name.as_ref())
177    }
178}
179
180/// A trait for structured output types that can be used with potatohead prompts agents and workflows.
181///
182/// # Example
183/// ```rust
184/// use potatohead_macro::StructureOutput;
185/// use serde::{Serialize, Deserialize};
186/// use schemars::JsonSchema;
187///
188/// #[derive(Serialize, Deserialize, JsonSchema)]
189/// struct MyOutput {
190///     message: String,
191///     value: i32,
192/// }
193///
194/// impl StructuredOutput for MyOutput {}
195///
196/// let schema = MyOutput::get_structured_output_schema();
197/// ```
198pub trait StructuredOutput: for<'de> serde::Deserialize<'de> + JsonSchema {
199    fn type_name() -> &'static str {
200        type_name::<Self>().rsplit("::").next().unwrap_or("Unknown")
201    }
202
203    /// Validates and deserializes a JSON value into its struct type.
204    ///
205    /// # Arguments
206    /// * `value` - The JSON value to deserialize
207    ///
208    /// # Returns
209    /// * `Result<Self, serde_json::Error>` - The deserialized value or error
210    fn model_validate_json_value(value: &Value) -> Result<Self, serde_json::Error> {
211        match &value {
212            Value::String(json_str) => Self::model_validate_json_str(json_str),
213            Value::Object(_) => {
214                // Content is already a JSON object
215                serde_json::from_value(value.clone())
216            }
217            _ => {
218                // If the value is not a string or object, we cannot deserialize it
219                Err(Error::custom("Expected a JSON string or object"))
220            }
221        }
222    }
223
224    fn model_validate_json_str(value: &str) -> Result<Self, serde_json::Error> {
225        serde_json::from_str(value)
226    }
227
228    /// Generates an OpenAI-compatible JSON schema.
229    ///
230    /// # Returns
231    /// * `Value` - The JSON schema wrapped in OpenAI's format
232    fn get_structured_output_schema() -> Value {
233        let schema = ::schemars::schema_for!(Self);
234        schema.into()
235    }
236    // add fallback parsing logic
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
240#[pyclass(from_py_object)]
241pub enum SettingsType {
242    GoogleChat,
243    OpenAIChat,
244    ModelSettings,
245    Anthropic,
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_provider_google_adk_round_trip() {
254        let p = Provider::from_string("google_adk").unwrap();
255        assert_eq!(p, Provider::GoogleAdk);
256        assert_eq!(p.as_str(), "google_adk");
257    }
258
259    #[test]
260    fn test_provider_all_variants_round_trip() {
261        for (s, variant) in [
262            ("openai", Provider::OpenAI),
263            ("gemini", Provider::Gemini),
264            ("google", Provider::Google),
265            ("vertex", Provider::Vertex),
266            ("anthropic", Provider::Anthropic),
267            ("google_adk", Provider::GoogleAdk),
268            ("undefined", Provider::Undefined),
269        ] {
270            let parsed = Provider::from_string(s).unwrap();
271            assert_eq!(parsed, variant);
272            assert_eq!(parsed.as_str(), s);
273        }
274    }
275
276    #[test]
277    fn test_provider_unknown_string_errors() {
278        assert!(Provider::from_string("not_a_provider").is_err());
279    }
280}