1mod sandbox;
53mod stubs;
54
55#[cfg(feature = "code-mode")]
56mod monty;
57#[cfg(feature = "code-pool")]
58mod pool;
59
60use std::collections::BTreeSet;
61use std::sync::Arc;
62use std::time::Duration;
63
64use gemini_genai_rs::prelude::FunctionDeclaration;
65use serde_json::{Value, json};
66
67#[cfg(feature = "code-mode")]
68pub use monty::InProcess;
69#[cfg(feature = "code-pool")]
70pub use pool::MontyPool;
71pub use sandbox::{CodeSandbox, Execution, Suspension, Unavailable};
72pub use stubs::{arguments, parameters, python_stub, without_redefinitions};
73
74pub const DEFAULT_TOOL: &str = "run_code";
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct CodeLimits {
80 pub calls: usize,
82 pub cpu: Duration,
85 pub wall: Duration,
88 pub output_chars: usize,
92 pub memory: usize,
96}
97
98impl Default for CodeLimits {
99 fn default() -> Self {
100 Self {
101 calls: 20,
102 cpu: Duration::from_secs(2),
103 wall: Duration::from_secs(30),
104 output_chars: 2_000,
105 memory: 64 * 1024 * 1024,
106 }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct CodeFunction {
113 pub name: String,
115 pub returns: Option<String>,
120 pub direct: Option<bool>,
125}
126
127#[derive(Clone)]
130pub struct CodeMode {
131 tool: String,
132 functions: Vec<CodeFunction>,
133 limits: CodeLimits,
134 direct: bool,
135 sandbox: Arc<dyn CodeSandbox>,
136}
137
138impl std::fmt::Debug for CodeMode {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("CodeMode")
141 .field("tool", &self.tool)
142 .field("functions", &self.functions)
143 .field("limits", &self.limits)
144 .field("direct", &self.direct)
145 .finish_non_exhaustive()
146 }
147}
148
149impl Default for CodeMode {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155impl CodeMode {
156 pub fn new() -> Self {
160 Self {
161 tool: DEFAULT_TOOL.to_string(),
162 functions: Vec::new(),
163 limits: CodeLimits::default(),
164 direct: true,
165 sandbox: default_sandbox(),
166 }
167 }
168
169 #[must_use]
171 pub fn function(mut self, name: impl Into<String>) -> Self {
172 self.push(CodeFunction {
173 name: name.into(),
174 returns: None,
175 direct: None,
176 });
177 self
178 }
179
180 #[must_use]
184 pub fn only_in_code(mut self, name: impl Into<String>) -> Self {
185 self.push(CodeFunction {
186 name: name.into(),
187 returns: None,
188 direct: Some(false),
189 });
190 self
191 }
192
193 #[must_use]
195 pub fn function_returning(
196 mut self,
197 name: impl Into<String>,
198 returns: impl Into<String>,
199 ) -> Self {
200 self.push(CodeFunction {
201 name: name.into(),
202 returns: Some(returns.into()),
203 direct: None,
204 });
205 self
206 }
207
208 #[must_use]
210 pub fn functions<I, S>(mut self, names: I) -> Self
211 where
212 I: IntoIterator<Item = S>,
213 S: Into<String>,
214 {
215 for name in names {
216 self = self.function(name);
217 }
218 self
219 }
220
221 #[must_use]
223 pub fn tool_name(mut self, name: impl Into<String>) -> Self {
224 self.tool = name.into();
225 self
226 }
227
228 #[must_use]
230 pub fn limits(mut self, limits: CodeLimits) -> Self {
231 self.limits = limits;
232 self
233 }
234
235 #[must_use]
238 pub fn direct(mut self, direct: bool) -> Self {
239 self.direct = direct;
240 self
241 }
242
243 #[must_use]
245 pub fn sandbox(mut self, sandbox: Arc<dyn CodeSandbox>) -> Self {
246 self.sandbox = sandbox;
247 self
248 }
249
250 fn push(&mut self, function: CodeFunction) {
251 match self.functions.iter_mut().find(|f| f.name == function.name) {
252 Some(existing) => {
253 if function.returns.is_some() {
254 existing.returns = function.returns;
255 }
256 if function.direct.is_some() {
257 existing.direct = function.direct;
258 }
259 }
260 None => self.functions.push(function),
261 }
262 }
263
264 pub fn tool(&self) -> &str {
266 &self.tool
267 }
268
269 pub fn function_list(&self) -> &[CodeFunction] {
271 &self.functions
272 }
273
274 pub fn limit(&self) -> &CodeLimits {
276 &self.limits
277 }
278
279 pub fn is_direct(&self) -> bool {
281 self.direct
282 }
283
284 pub fn sandbox_ref(&self) -> &Arc<dyn CodeSandbox> {
286 &self.sandbox
287 }
288
289 pub fn exposes(&self, name: &str) -> bool {
291 self.functions.iter().any(|f| f.name == name)
292 }
293
294 pub fn hides(&self, name: &str) -> bool {
296 self.functions
297 .iter()
298 .find(|f| f.name == name)
299 .is_some_and(|f| !f.direct.unwrap_or(self.direct))
300 }
301
302 pub fn declaration<'a>(
306 &self,
307 catalog: impl IntoIterator<Item = &'a FunctionDeclaration>,
308 offered: impl Fn(&str) -> bool,
309 ) -> Option<FunctionDeclaration> {
310 let catalog: Vec<&FunctionDeclaration> = catalog.into_iter().collect();
311 let listed: Vec<&CodeFunction> = self
312 .functions
313 .iter()
314 .filter(|f| offered(&f.name))
315 .filter(|f| catalog.iter().any(|d| d.name == f.name))
316 .collect();
317 let stubs: Vec<String> = listed
318 .iter()
319 .filter_map(|f| {
320 catalog
321 .iter()
322 .find(|d| d.name == f.name)
323 .map(|d| python_stub(d, f.returns.as_deref()))
324 })
325 .collect();
326 if stubs.is_empty() {
327 return None;
328 }
329 let when = if listed.iter().any(|f| !self.hides(&f.name)) {
330 "Use it whenever the answer needs arithmetic, dates, counting, sorting \
331 or comparing over what a function returns, or several calls: call \
332 the function inside the program rather than calling the tool first \
333 and copying its result in. Call a tool directly only when its result \
334 is the answer as it stands."
335 } else {
336 "Use it to call the functions below and to work out arithmetic, \
337 dates, counting or sorting over what they return."
338 };
339 Some(FunctionDeclaration {
340 name: self.tool.clone(),
341 description: format!(
342 "Run a short Python program and get back the value of its last \
343 expression, and anything it prints. {when} Do the whole request \
344 in one program. Each function's docstring says what it returns: \
345 rely on it rather than printing results to look at them. End the \
346 program with an expression holding only the values you need to \
347 answer. The functions are already defined: call them directly; \
348 never import, define or mock them. Keep judgments of meaning out \
349 of the program: when a choice depends on what something is (which \
350 medication treats a condition), return the candidates and decide \
351 yourself. date.weekday() counts Monday as 0. A call the conversation does \
352 not allow yet raises RuntimeError with the reason: tell the \
353 caller what is needed instead of retrying. The program can \
354 import datetime, math, json and re.\n\n{}",
355 stubs.join("\n")
356 ),
357 parameters: Some(json!({
358 "type": "object",
359 "properties": {
360 "code": {"type": "string", "description": "The Python program."}
361 },
362 "required": ["code"],
363 })),
364 behavior: None,
365 })
366 }
367
368 pub fn offered_functions<'a>(
371 &self,
372 catalog: impl IntoIterator<Item = &'a FunctionDeclaration>,
373 offered: impl Fn(&str) -> bool,
374 ) -> BTreeSet<String> {
375 catalog
376 .into_iter()
377 .filter(|d| self.exposes(&d.name) && offered(&d.name))
378 .map(|d| d.name.clone())
379 .collect()
380 }
381
382 pub fn response(&self, result: Result<Value, String>, printed: &str) -> Value {
386 let limit = self.limits.output_chars;
387 let mut response = match result {
388 Ok(value) => {
389 let size = value.to_string().chars().count();
390 if size > limit {
391 json!({
392 "error": format!(
393 "The program's result is {size} characters, over the \
394 limit of {limit}. End it with only the values you \
395 need to answer."
396 )
397 })
398 } else {
399 json!({ "result": value })
400 }
401 }
402 Err(error) => json!({ "error": error }),
403 };
404 let printed = printed.trim();
407 let size = printed.chars().count();
408 if size > limit {
409 return json!({
410 "error": format!(
411 "The program printed {size} characters, over the limit of {limit}, \
412 so none of it is shown. Print only the values you need: compute \
413 the answer in the program."
414 )
415 });
416 }
417 if !printed.is_empty() {
418 response["printed"] = Value::String(printed.to_string());
419 }
420 response
421 }
422}
423
424fn default_sandbox() -> Arc<dyn CodeSandbox> {
425 #[cfg(feature = "code-mode")]
426 {
427 Arc::new(InProcess)
428 }
429 #[cfg(not(feature = "code-mode"))]
430 {
431 Arc::new(Unavailable)
432 }
433}
434
435impl CodeMode {
436 pub async fn start(&self, program: &str, catalog: &[FunctionDeclaration]) -> Program {
444 let names: Vec<String> = self.functions.iter().map(|f| f.name.clone()).collect();
445 let declarations = catalog
446 .iter()
447 .filter(|d| self.exposes(&d.name))
448 .cloned()
449 .collect();
450 let (execution, first) = match without_redefinitions(program, &names) {
451 Ok(source) => self.sandbox.start(&source, &names, &self.limits).await,
452 Err(reason) => (sandbox::ended(), Suspension::failed(reason)),
453 };
454 let mut program = Program {
455 mode: self.clone(),
456 execution,
457 declarations,
458 pending: None,
459 finished: None,
460 calls: Vec::new(),
461 };
462 program.advance(first).await;
463 program
464 }
465}
466
467pub struct Program {
470 mode: CodeMode,
471 execution: Box<dyn Execution>,
472 declarations: Vec<FunctionDeclaration>,
473 pending: Option<(String, Value)>,
475 finished: Option<(Result<Value, String>, String)>,
477 calls: Vec<(String, bool)>,
479}
480
481impl std::fmt::Debug for Program {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 f.debug_struct("Program")
484 .field("pending", &self.pending)
485 .field("finished", &self.finished)
486 .field("calls", &self.calls)
487 .finish_non_exhaustive()
488 }
489}
490
491impl Program {
492 pub fn call(&self) -> Option<(&str, &Value)> {
495 self.pending
496 .as_ref()
497 .map(|(name, args)| (name.as_str(), args))
498 }
499
500 pub async fn answer(&mut self, result: Result<Value, String>) {
504 let Some((name, _)) = self.pending.take() else {
505 return;
506 };
507 self.calls.push((name, result.is_ok()));
508 let next = self.execution.resume(result).await;
509 self.advance(next).await;
510 }
511
512 pub fn abort(&mut self, error: impl Into<String>) {
514 self.pending = None;
515 self.finished = Some((Err(error.into()), String::new()));
516 }
517
518 pub fn is_finished(&self) -> bool {
520 self.finished.is_some()
521 }
522
523 pub fn calls(&self) -> &[(String, bool)] {
525 &self.calls
526 }
527
528 pub fn response(&self) -> Option<Value> {
532 self.finished
533 .as_ref()
534 .map(|(result, printed)| self.mode.response(result.clone(), printed))
535 }
536
537 async fn advance(&mut self, mut next: Suspension) {
541 loop {
542 match next {
543 Suspension::Finished { result, printed } => {
544 self.finished = Some((result, printed));
545 return;
546 }
547 Suspension::Call {
548 function,
549 positional,
550 keywords,
551 } => {
552 let limit = self.mode.limits.calls;
553 if self.calls.len() >= limit {
554 self.abort(format!(
555 "The program made more than {limit} function calls. \
556 Do the work in fewer calls."
557 ));
558 return;
559 }
560 let named = match self.declarations.iter().find(|d| d.name == function) {
561 Some(declaration) => arguments(declaration, positional, keywords),
562 None => Err(format!("NameError: name '{function}' is not defined")),
563 };
564 match named {
565 Ok(args) => {
566 self.pending = Some((function, args));
567 return;
568 }
569 Err(reason) => {
570 self.calls.push((function, false));
571 next = self.execution.resume(Err(reason)).await;
572 }
573 }
574 }
575 }
576 }
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583
584 #[test]
585 fn an_over_limit_printout_is_refused_whole() {
586 let mode = CodeMode::new().limits(CodeLimits {
587 output_chars: 10,
588 ..CodeLimits::default()
589 });
590 let response = mode.response(Ok(Value::Null), "0123456789abc");
591 assert!(
592 response.get("printed").is_none(),
593 "nothing partial is shown"
594 );
595 let error = response["error"].as_str().unwrap();
596 assert!(error.contains("printed 13 characters"), "{error}");
597 assert_eq!(
598 mode.response(Ok(json!(1)), "short"),
599 json!({ "result": 1, "printed": "short" })
600 );
601 }
602}