1use crate::error::MockError;
2use mockito;
3use potato_agent::agents::provider::{gemini::GenerateContentResponse, openai::OpenAIChatResponse};
4use serde_json;
5
6use pyo3::prelude::*;
7
8pub const OPENAI_CHAT_COMPLETION_RESPONSE: &str =
9 include_str!("assets/openai/openai_chat_completion_response.json");
10
11pub const OPENAI_CHAT_STRUCTURED_RESPONSE: &str =
12 include_str!("assets/openai/chat_completion_structured_response.json");
13
14pub const OPENAI_CHAT_STRUCTURED_SCORE_RESPONSE: &str =
15 include_str!("assets/openai/chat_completion_structured_score_response.json");
16
17pub const OPENAI_CHAT_STRUCTURED_RESPONSE_PARAMS: &str =
18 include_str!("assets/openai/chat_completion_structured_response_params.json");
19
20pub const OPENAI_CHAT_STRUCTURED_TASK_OUTPUT: &str =
21 include_str!("assets/openai/chat_completion_structured_task_output.json");
22
23pub const GEMINI_CHAT_COMPLETION_RESPONSE: &str =
24 include_str!("assets/gemini/chat_completion.json");
25
26pub const GEMINI_CHAT_COMPLETION_RESPONSE_WITH_SCORE: &str =
27 include_str!("assets/gemini/chat_completion_with_score.json");
28
29pub struct LLMApiMock {
30 pub url: String,
31 pub server: mockito::ServerGuard,
32}
33
34impl LLMApiMock {
35 pub fn new() -> Self {
36 let mut server = mockito::Server::new();
37 let chat_msg_response: OpenAIChatResponse =
39 serde_json::from_str(OPENAI_CHAT_COMPLETION_RESPONSE).unwrap();
40 let chat_structured_response: OpenAIChatResponse =
41 serde_json::from_str(OPENAI_CHAT_STRUCTURED_RESPONSE).unwrap();
42 let chat_structured_score_response: OpenAIChatResponse =
43 serde_json::from_str(OPENAI_CHAT_STRUCTURED_SCORE_RESPONSE).unwrap();
44 let chat_structured_response_params: OpenAIChatResponse =
45 serde_json::from_str(OPENAI_CHAT_STRUCTURED_RESPONSE_PARAMS).unwrap();
46 let chat_structured_task_output: OpenAIChatResponse =
47 serde_json::from_str(OPENAI_CHAT_STRUCTURED_TASK_OUTPUT).unwrap();
48
49 let gemini_chat_response: GenerateContentResponse =
51 serde_json::from_str(GEMINI_CHAT_COMPLETION_RESPONSE).unwrap();
52 let gemini_chat_response_with_score: GenerateContentResponse =
53 serde_json::from_str(GEMINI_CHAT_COMPLETION_RESPONSE_WITH_SCORE).unwrap();
54
55 server
56 .mock("POST", "/chat/completions")
57 .match_body(mockito::Matcher::PartialJson(serde_json::json!({
58 "response_format": {
59 "type": "json_schema",
60 "json_schema": {
61 "name": "Parameters",
62 "schema": {
63 "$schema": "https://json-schema.org/draft/2020-12/schema",
64 "properties": {
65 "variable1": {
66 "format": "int32",
67 "type": "integer"
68 },
69 "variable2": {
70 "format": "int32",
71 "type": "integer"
72 }
73 },
74 "required": [
75 "variable1",
76 "variable2"
77 ],
78 "title": "Parameters",
79 "type": "object"
80 },
81 "strict": true
82 }
83
84 }
85 })))
86 .expect(usize::MAX)
87 .with_status(200)
88 .with_header("content-type", "application/json")
89 .with_body(serde_json::to_string(&chat_structured_response_params).unwrap())
90 .create();
91
92 server
93 .mock("POST", "/chat/completions")
94 .match_body(mockito::Matcher::PartialJson(serde_json::json!({
95 "response_format": {
96 "type": "json_schema",
97 "json_schema": {
98 "name": "TaskOutput",
99 }
100 }
101 })))
102 .expect(usize::MAX)
103 .with_status(200)
104 .with_header("content-type", "application/json")
105 .with_body(serde_json::to_string(&chat_structured_task_output).unwrap())
106 .create();
107
108 server
109 .mock("POST", "/chat/completions")
110 .match_body(mockito::Matcher::PartialJson(serde_json::json!({
111 "response_format": {
112 "type": "json_schema",
113 "json_schema": {
114 "name": "Score",
115 }
116 }
117 })))
118 .expect(usize::MAX)
119 .with_status(200)
120 .with_header("content-type", "application/json")
121 .with_body(serde_json::to_string(&chat_structured_score_response).unwrap())
122 .create();
123
124 server
125 .mock("POST", "/chat/completions")
126 .match_body(mockito::Matcher::Regex(
127 r#".*"name"\s*:\s*"Score".*"#.to_string(),
128 ))
129 .expect(usize::MAX)
130 .with_status(200)
131 .with_header("content-type", "application/json")
132 .with_body(serde_json::to_string(&chat_structured_score_response).unwrap())
133 .create();
134
135 server
136 .mock("POST", "/chat/completions")
137 .match_body(mockito::Matcher::PartialJson(serde_json::json!({
138 "response_format": {
139 "type": "json_schema"
140 }
141 })))
142 .expect(usize::MAX)
143 .with_status(200)
144 .with_header("content-type", "application/json")
145 .with_body(serde_json::to_string(&chat_structured_response).unwrap())
146 .create();
147
148 server
150 .mock(
151 "POST",
152 mockito::Matcher::Regex(r".*/.*:generateContent$".to_string()),
153 )
154 .match_header("x-goog-api-key", mockito::Matcher::Any)
155 .match_header("content-type", "application/json")
156 .match_body(mockito::Matcher::PartialJson(serde_json::json!({
157 "contents": [
158 {
159 "parts": [
160 {
161 "text": "You are a helpful assistant"
162 }
163 ]
164 }
165 ]
166 })))
167 .expect(usize::MAX) .with_status(200)
169 .with_header("content-type", "application/json")
170 .with_body(serde_json::to_string(&gemini_chat_response).unwrap())
171 .create();
172
173 server
175 .mock(
176 "POST",
177 mockito::Matcher::Regex(r".*/.*:generateContent$".to_string()),
178 )
179 .match_header("x-goog-api-key", mockito::Matcher::Any)
180 .match_header("content-type", "application/json")
181 .match_body(mockito::Matcher::PartialJson(serde_json::json!({
182 "generation_config": {
183 "responseMimeType": "application/json"
184 }
185 })))
186 .expect(usize::MAX)
187 .with_status(200)
188 .with_header("content-type", "application/json")
189 .with_body(serde_json::to_string(&gemini_chat_response_with_score).unwrap())
190 .create();
191
192 server
194 .mock("POST", "/chat/completions")
195 .expect(usize::MAX)
196 .with_status(200)
197 .with_header("content-type", "application/json")
198 .with_body(serde_json::to_string(&chat_msg_response).unwrap())
199 .create();
200
201 Self {
202 url: server.url(),
203 server,
204 }
205 }
206}
207
208impl Default for LLMApiMock {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214#[pyclass]
215#[allow(dead_code)]
216pub struct LLMTestServer {
217 openai_server: Option<LLMApiMock>,
218}
219
220#[pymethods]
221impl LLMTestServer {
222 #[new]
223 pub fn new() -> Self {
224 LLMTestServer {
225 openai_server: None,
226 }
227 }
228
229 pub fn start_mock_server(&mut self) -> Result<(), MockError> {
230 let llm_server = LLMApiMock::new();
231 println!("Mock LLM Server started at {}", llm_server.url);
232 self.openai_server = Some(llm_server);
233 Ok(())
234 }
235
236 pub fn stop_mock_server(&mut self) {
237 if let Some(server) = self.openai_server.take() {
238 drop(server);
239 std::env::remove_var("OPENAI_API_URL");
240 std::env::remove_var("OPENAI_API_KEY");
241 }
242 println!("Mock LLM Server stopped");
243 }
244
245 pub fn set_env_vars_for_client(&self) -> Result<(), MockError> {
246 {
247 std::env::set_var("APP_ENV", "dev_client");
248 std::env::set_var("OPENAI_API_KEY", "test_key");
249 std::env::set_var("GEMINI_API_KEY", "gemini");
250 std::env::set_var(
251 "OPENAI_API_URL",
252 self.openai_server.as_ref().unwrap().url.clone(),
253 );
254 std::env::set_var(
255 "GEMINI_API_URL",
256 self.openai_server.as_ref().unwrap().url.clone(),
257 );
258 Ok(())
259 }
260 }
261
262 pub fn start_server(&mut self) -> Result<(), MockError> {
263 self.cleanup()?;
264
265 println!("Starting Mock GenAI Server...");
266 self.start_mock_server()?;
267 self.set_env_vars_for_client()?;
268
269 std::env::set_var("APP_ENV", "dev_server");
271
272 Ok(())
273 }
274
275 pub fn stop_server(&mut self) -> Result<(), MockError> {
276 self.cleanup()?;
277
278 Ok(())
279 }
280
281 pub fn remove_env_vars_for_client(&self) -> Result<(), MockError> {
282 std::env::remove_var("OPENAI_API_URI");
283 std::env::remove_var("OPENAI_API_KEY");
284 std::env::remove_var("GEMINI_API_KEY");
285 std::env::remove_var("GEMINI_API_URL");
286 Ok(())
287 }
288
289 fn cleanup(&self) -> Result<(), MockError> {
290 self.remove_env_vars_for_client()?;
292
293 Ok(())
294 }
295
296 fn __enter__(mut self_: PyRefMut<Self>) -> Result<PyRefMut<Self>, MockError> {
297 self_.start_server()?;
298
299 Ok(self_)
300 }
301
302 fn __exit__(
303 &mut self,
304 _exc_type: PyObject,
305 _exc_value: PyObject,
306 _traceback: PyObject,
307 ) -> Result<(), MockError> {
308 self.stop_server()
309 }
310}
311
312impl Default for LLMTestServer {
313 fn default() -> Self {
314 Self::new()
315 }
316}