1use crate::anthropic::v1::response::{
2 ResponseContentBlockInner, TextCitation, WebSearchToolResultBlockContent,
3};
4use crate::common::get_image_media_types;
5use crate::google::v1::generate::request::{DataNum, GeminiContent, Part};
6use crate::openai::v1::chat::request::{ChatMessage, ContentPart, TextContentPart};
7use crate::prompt::builder::ProviderRequest;
8use crate::prompt::MessageNum;
9use crate::prompt::ModelSettings;
10use crate::tools::AgentToolDefinition;
11use crate::traits::get_var_regex;
12use crate::traits::{MessageConversion, MessageFactory, PromptMessageExt, RequestAdapter};
13use crate::TypeError;
14use crate::{Provider, SettingsType};
15use potato_util::{PyHelperFuncs, UtilError};
16use pyo3::prelude::*;
17use pyo3::types::PyList;
18use pyo3::IntoPyObjectExt;
19use pythonize::{depythonize, pythonize};
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use std::collections::HashSet;
23
24pub const BASE64_TYPE: &str = "base64";
26pub const URL_TYPE: &str = "url";
27pub const EPHEMERAL_TYPE: &str = "ephemeral";
28pub const IMAGE_TYPE: &str = "image";
29pub const TEXT_TYPE: &str = "text";
30pub const DOCUMENT_TYPE: &str = "document";
31pub const DOCUMENT_BASE64_PDF_TYPE: &str = "application/pdf";
32pub const DOCUMENT_PLAIN_TEXT_TYPE: &str = "text/plain";
33pub const WEB_SEARCH_RESULT_TYPE: &str = "web_search_result";
34pub const SEARCH_TYPE: &str = "search_result";
35pub const THINKING_TYPE: &str = "thinking";
36pub const REDACTED_THINKING_TYPE: &str = "redacted_thinking";
37pub const TOOL_USE_TYPE: &str = "tool_use";
38pub const TOOL_RESULT_TYPE: &str = "tool_result";
39pub const WEB_SEARCH_TOOL_RESULT_TYPE: &str = "web_search_tool_result";
40pub const SERVER_TOOL_USE_TYPE: &str = "server_tool_use";
41
42pub const CHAR_LOCATION_TYPE: &str = "char_location";
44pub const PAGE_LOCATION_TYPE: &str = "page_location";
45pub const CONTENT_BLOCK_LOCATION_TYPE: &str = "content_block_location";
46pub const WEB_SEARCH_RESULT_LOCATION_TYPE: &str = "web_search_result_location";
47pub const SEARCH_RESULT_LOCATION_TYPE: &str = "search_result_location";
48pub const WEB_SEARCH_TOOL_RESULT_ERROR_TYPE: &str = "web_search_tool_result_error";
49
50#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
51#[pyclass(from_py_object)]
52pub struct CitationCharLocationParam {
53 #[pyo3(get, set)]
54 pub cited_text: String,
55 #[pyo3(get, set)]
56 pub document_index: i32,
57 #[pyo3(get, set)]
58 pub document_title: String,
59 #[pyo3(get, set)]
60 pub end_char_index: i32,
61 #[pyo3(get, set)]
62 pub start_char_index: i32,
63 #[pyo3(get)]
64 #[serde(rename = "type")]
65 pub r#type: String,
66}
67
68#[pymethods]
69impl CitationCharLocationParam {
70 #[new]
71 pub fn new(
72 cited_text: String,
73 document_index: i32,
74 document_title: String,
75 end_char_index: i32,
76 start_char_index: i32,
77 ) -> Self {
78 Self {
79 cited_text,
80 document_index,
81 document_title,
82 end_char_index,
83 start_char_index,
84 r#type: CHAR_LOCATION_TYPE.to_string(),
85 }
86 }
87}
88
89#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
90#[pyclass(from_py_object)]
91pub struct CitationPageLocationParam {
92 #[pyo3(get, set)]
93 pub cited_text: String,
94 #[pyo3(get, set)]
95 pub document_index: i32,
96 #[pyo3(get, set)]
97 pub document_title: String,
98 #[pyo3(get, set)]
99 pub end_page_number: i32,
100 #[pyo3(get, set)]
101 pub start_page_number: i32,
102 #[pyo3(get)]
103 #[serde(rename = "type")]
104 pub r#type: String,
105}
106
107#[pymethods]
108impl CitationPageLocationParam {
109 #[new]
110 pub fn new(
111 cited_text: String,
112 document_index: i32,
113 document_title: String,
114 end_page_number: i32,
115 start_page_number: i32,
116 ) -> Self {
117 Self {
118 cited_text,
119 document_index,
120 document_title,
121 end_page_number,
122 start_page_number,
123 r#type: PAGE_LOCATION_TYPE.to_string(),
124 }
125 }
126}
127
128#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
129#[pyclass(from_py_object)]
130pub struct CitationContentBlockLocationParam {
131 #[pyo3(get, set)]
132 pub cited_text: String,
133 #[pyo3(get, set)]
134 pub document_index: i32,
135 #[pyo3(get, set)]
136 pub document_title: String,
137 #[pyo3(get, set)]
138 pub end_block_index: i32,
139 #[pyo3(get, set)]
140 pub start_block_index: i32,
141 #[pyo3(get)]
142 #[serde(rename = "type")]
143 pub r#type: String,
144}
145
146#[pymethods]
147impl CitationContentBlockLocationParam {
148 #[new]
149 pub fn new(
150 cited_text: String,
151 document_index: i32,
152 document_title: String,
153 end_block_index: i32,
154 start_block_index: i32,
155 ) -> Self {
156 Self {
157 cited_text,
158 document_index,
159 document_title,
160 end_block_index,
161 start_block_index,
162 r#type: CONTENT_BLOCK_LOCATION_TYPE.to_string(),
163 }
164 }
165}
166
167#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
168#[pyclass(from_py_object)]
169pub struct CitationWebSearchResultLocationParam {
170 #[pyo3(get, set)]
171 pub cited_text: String,
172 #[pyo3(get, set)]
173 pub encrypted_index: String,
174 #[pyo3(get, set)]
175 pub title: String,
176 #[pyo3(get)]
177 #[serde(rename = "type")]
178 pub r#type: String,
179 #[pyo3(get, set)]
180 pub url: String,
181}
182
183#[pymethods]
184impl CitationWebSearchResultLocationParam {
185 #[new]
186 pub fn new(cited_text: String, encrypted_index: String, title: String, url: String) -> Self {
187 Self {
188 cited_text,
189 encrypted_index,
190 title,
191 r#type: WEB_SEARCH_RESULT_LOCATION_TYPE.to_string(),
192 url,
193 }
194 }
195}
196
197#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
198#[pyclass(from_py_object)]
199pub struct CitationSearchResultLocationParam {
200 #[pyo3(get, set)]
201 pub cited_text: String,
202 #[pyo3(get, set)]
203 pub end_block_index: i32,
204 #[pyo3(get, set)]
205 pub search_result_index: i32,
206 #[pyo3(get, set)]
207 pub source: String,
208 #[pyo3(get, set)]
209 pub start_block_index: i32,
210 #[pyo3(get, set)]
211 pub title: String,
212 #[pyo3(get)]
213 #[serde(rename = "type")]
214 pub r#type: String,
215}
216
217#[pymethods]
218impl CitationSearchResultLocationParam {
219 #[new]
220 pub fn new(
221 cited_text: String,
222 end_block_index: i32,
223 search_result_index: i32,
224 source: String,
225 start_block_index: i32,
226 title: String,
227 ) -> Self {
228 Self {
229 cited_text,
230 end_block_index,
231 search_result_index,
232 source,
233 start_block_index,
234 title,
235 r#type: SEARCH_RESULT_LOCATION_TYPE.to_string(),
236 }
237 }
238}
239
240#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
242#[serde(untagged)]
243pub enum TextCitationParam {
244 CharLocation(CitationCharLocationParam),
245 PageLocation(CitationPageLocationParam),
246 ContentBlockLocation(CitationContentBlockLocationParam),
247 WebSearchResultLocation(CitationWebSearchResultLocationParam),
248 SearchResultLocation(CitationSearchResultLocationParam),
249}
250
251#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
252#[pyclass(from_py_object)]
253pub struct TextBlockParam {
254 #[pyo3(get, set)]
255 pub text: String,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 #[pyo3(get, set)]
258 pub cache_control: Option<CacheControl>,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub citations: Option<TextCitationParam>,
261 #[pyo3(get)]
262 #[serde(rename = "type")]
263 pub r#type: String,
264}
265
266fn parse_text_citation(cit: &Bound<'_, PyAny>) -> Result<TextCitationParam, TypeError> {
267 if cit.is_instance_of::<CitationCharLocationParam>() {
268 Ok(TextCitationParam::CharLocation(
269 cit.extract::<CitationCharLocationParam>()?,
270 ))
271 } else if cit.is_instance_of::<CitationPageLocationParam>() {
272 Ok(TextCitationParam::PageLocation(
273 cit.extract::<CitationPageLocationParam>()?,
274 ))
275 } else if cit.is_instance_of::<CitationContentBlockLocationParam>() {
276 Ok(TextCitationParam::ContentBlockLocation(
277 cit.extract::<CitationContentBlockLocationParam>()?,
278 ))
279 } else if cit.is_instance_of::<CitationWebSearchResultLocationParam>() {
280 Ok(TextCitationParam::WebSearchResultLocation(
281 cit.extract::<CitationWebSearchResultLocationParam>()?,
282 ))
283 } else if cit.is_instance_of::<CitationSearchResultLocationParam>() {
284 Ok(TextCitationParam::SearchResultLocation(
285 cit.extract::<CitationSearchResultLocationParam>()?,
286 ))
287 } else {
288 Err(TypeError::InvalidInput(
289 "Invalid citation type provided".to_string(),
290 ))
291 }
292}
293#[pymethods]
294impl TextBlockParam {
295 #[new]
296 #[pyo3(signature = (text, cache_control=None, citations=None))]
297 pub fn new(
298 text: String,
299 cache_control: Option<CacheControl>,
300 citations: Option<&Bound<'_, PyAny>>,
301 ) -> Result<Self, TypeError> {
302 let citations = if let Some(cit) = citations {
303 Some(parse_text_citation(cit)?)
304 } else {
305 None
306 };
307 Ok(Self {
308 text,
309 cache_control,
310 citations,
311 r#type: TEXT_TYPE.to_string(),
312 })
313 }
314}
315
316impl TextBlockParam {
317 pub fn new_rs(
318 text: String,
319 cache_control: Option<CacheControl>,
320 citations: Option<TextCitationParam>,
321 ) -> Self {
322 Self {
323 text,
324 cache_control,
325 citations,
326 r#type: TEXT_TYPE.to_string(),
327 }
328 }
329}
330
331#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
332#[pyclass(from_py_object)]
333pub struct Base64ImageSource {
334 #[pyo3(get, set)]
335 pub media_type: String,
336 #[pyo3(get, set)]
337 pub data: String,
338 #[pyo3(get)]
339 #[serde(rename = "type")]
340 pub r#type: String,
341}
342
343#[pymethods]
344impl Base64ImageSource {
345 #[new]
346 pub fn new(media_type: String, data: String) -> Result<Self, TypeError> {
347 if !get_image_media_types().contains(media_type.as_str()) {
349 return Err(TypeError::InvalidMediaType(media_type));
350 }
351 Ok(Self {
352 media_type,
353 data,
354 r#type: BASE64_TYPE.to_string(),
355 })
356 }
357}
358
359#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
360#[pyclass(from_py_object)]
361pub struct UrlImageSource {
362 #[pyo3(get, set)]
363 pub url: String,
364 #[pyo3(get)]
365 #[serde(rename = "type")]
366 pub r#type: String,
367}
368
369#[pymethods]
370impl UrlImageSource {
371 #[new]
372 pub fn new(url: String) -> Self {
373 Self {
374 url,
375 r#type: URL_TYPE.to_string(),
376 }
377 }
378}
379
380#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
381#[serde(untagged)] pub enum ImageSource {
383 Base64(Base64ImageSource),
384 Url(UrlImageSource),
385}
386
387#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
388#[pyclass(from_py_object)]
389pub struct ImageBlockParam {
390 pub source: ImageSource,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 #[pyo3(get, set)]
393 pub cache_control: Option<CacheControl>,
394 #[pyo3(get)]
395 #[serde(rename = "type")]
396 pub r#type: String,
397}
398
399#[pymethods]
400impl ImageBlockParam {
401 #[new]
402 #[pyo3(signature = (source, cache_control=None))]
403 pub fn new(
404 source: &Bound<'_, PyAny>,
405 cache_control: Option<CacheControl>,
406 ) -> Result<Self, TypeError> {
407 let source: ImageSource = if source.is_instance_of::<Base64ImageSource>() {
408 ImageSource::Base64(source.extract::<Base64ImageSource>()?)
409 } else {
410 ImageSource::Url(source.extract::<UrlImageSource>()?)
411 };
412 Ok(Self {
413 source,
414 cache_control,
415 r#type: IMAGE_TYPE.to_string(),
416 })
417 }
418
419 #[getter]
420 pub fn source<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError> {
421 match &self.source {
422 ImageSource::Base64(base64) => {
423 let py_obj = base64.clone().into_bound_py_any(py)?;
424 Ok(py_obj.clone())
425 }
426 ImageSource::Url(url) => {
427 let py_obj = url.clone().into_bound_py_any(py)?;
428 Ok(py_obj.clone())
429 }
430 }
431 }
432}
433
434#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
435#[pyclass(from_py_object)]
436pub struct Base64PDFSource {
437 #[pyo3(get, set)]
438 pub media_type: String,
439 #[pyo3(get, set)]
440 pub data: String,
441 #[pyo3(get)]
442 #[serde(rename = "type")]
443 pub r#type: String,
444}
445
446#[pymethods]
447impl Base64PDFSource {
448 #[new]
449 pub fn new(data: String) -> Result<Self, TypeError> {
450 Ok(Self {
451 media_type: DOCUMENT_BASE64_PDF_TYPE.to_string(),
452 data,
453 r#type: BASE64_TYPE.to_string(),
454 })
455 }
456}
457
458#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
459#[pyclass(from_py_object)]
460pub struct UrlPDFSource {
461 #[pyo3(get, set)]
462 pub url: String,
463 #[pyo3(get)]
464 #[serde(rename = "type")]
465 pub r#type: String,
466}
467
468#[pymethods]
469impl UrlPDFSource {
470 #[new]
471 pub fn new(url: String) -> Self {
472 Self {
473 url,
474 r#type: URL_TYPE.to_string(),
475 }
476 }
477}
478
479#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
480#[pyclass(from_py_object)]
481pub struct PlainTextSource {
482 #[pyo3(get, set)]
483 pub media_type: String,
484 #[pyo3(get, set)]
485 pub data: String,
486 #[pyo3(get)]
487 #[serde(rename = "type")]
488 pub r#type: String,
489}
490
491#[pymethods]
492impl PlainTextSource {
493 #[new]
494 pub fn new(data: String) -> Self {
495 Self {
496 media_type: DOCUMENT_PLAIN_TEXT_TYPE.to_string(),
497 data,
498 r#type: TEXT_TYPE.to_string(),
499 }
500 }
501}
502
503#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
504#[pyclass(from_py_object)]
505pub struct CitationsConfigParams {
506 #[pyo3(get, set)]
507 pub enabled: Option<bool>,
508}
509
510#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
511#[serde(untagged)]
512pub enum DocumentSource {
513 Base64(Base64PDFSource),
514 Url(UrlPDFSource),
515 Text(PlainTextSource),
516}
517
518#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
519#[pyclass(from_py_object)]
520pub struct DocumentBlockParam {
521 pub source: DocumentSource,
522 #[serde(skip_serializing_if = "Option::is_none")]
523 #[pyo3(get, set)]
524 pub cache_control: Option<CacheControl>,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 #[pyo3(get, set)]
527 pub title: Option<String>,
528 #[serde(skip_serializing_if = "Option::is_none")]
529 #[pyo3(get, set)]
530 pub context: Option<String>,
531 #[serde(rename = "type")]
532 #[pyo3(get, set)]
533 pub r#type: String,
534 #[serde(skip_serializing_if = "Option::is_none")]
535 #[pyo3(get, set)]
536 pub citations: Option<CitationsConfigParams>,
537}
538
539#[pymethods]
540impl DocumentBlockParam {
541 #[new]
542 #[pyo3(signature = (source, cache_control=None, title=None, context=None, citations=None))]
543 pub fn new(
544 source: &Bound<'_, PyAny>,
545 cache_control: Option<CacheControl>,
546 title: Option<String>,
547 context: Option<String>,
548 citations: Option<CitationsConfigParams>,
549 ) -> Result<Self, TypeError> {
550 let source: DocumentSource = if source.is_instance_of::<Base64PDFSource>() {
551 DocumentSource::Base64(source.extract::<Base64PDFSource>()?)
552 } else if source.is_instance_of::<UrlPDFSource>() {
553 DocumentSource::Url(source.extract::<UrlPDFSource>()?)
554 } else {
555 DocumentSource::Text(source.extract::<PlainTextSource>()?)
556 };
557
558 Ok(Self {
559 source,
560 cache_control,
561 title,
562 context,
563 r#type: DOCUMENT_TYPE.to_string(),
564 citations,
565 })
566 }
567}
568
569#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
570#[pyclass(from_py_object)]
571pub struct SearchResultBlockParam {
572 #[pyo3(get, set)]
573 pub content: Vec<TextBlockParam>,
574 #[pyo3(get, set)]
575 pub source: String,
576 #[pyo3(get, set)]
577 pub title: String,
578 #[serde(rename = "type")]
579 #[pyo3(get, set)]
580 pub r#type: String,
581 #[pyo3(get, set)]
582 pub cache_control: Option<CacheControl>,
583 #[pyo3(get, set)]
584 pub citations: Option<CitationsConfigParams>,
585}
586
587#[pymethods]
588impl SearchResultBlockParam {
589 #[new]
590 #[pyo3(signature = (content, source, title, cache_control=None, citations=None))]
591 pub fn new(
592 content: Vec<TextBlockParam>,
593 source: String,
594 title: String,
595 cache_control: Option<CacheControl>,
596 citations: Option<CitationsConfigParams>,
597 ) -> Self {
598 Self {
599 content,
600 source,
601 title,
602 r#type: SEARCH_TYPE.to_string(),
603 cache_control,
604 citations,
605 }
606 }
607}
608
609#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
610#[pyclass(from_py_object)]
611pub struct ThinkingBlockParam {
612 #[pyo3(get, set)]
613 pub thinking: String,
614 #[pyo3(get, set)]
615 pub signature: Option<String>,
616 #[pyo3(get, set)]
617 #[serde(rename = "type")]
618 pub r#type: String,
619}
620
621#[pymethods]
622impl ThinkingBlockParam {
623 #[new]
624 #[pyo3(signature = (thinking, signature=None))]
625 pub fn new(thinking: String, signature: Option<String>) -> Self {
626 Self {
627 thinking,
628 signature,
629 r#type: THINKING_TYPE.to_string(),
630 }
631 }
632}
633
634#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
635#[pyclass(from_py_object)]
636pub struct RedactedThinkingBlockParam {
637 #[pyo3(get, set)]
638 pub data: String,
639 #[pyo3(get, set)]
640 #[serde(rename = "type")]
641 pub r#type: String,
642}
643
644#[pymethods]
645impl RedactedThinkingBlockParam {
646 #[new]
647 pub fn new(data: String) -> Self {
648 Self {
649 data,
650 r#type: REDACTED_THINKING_TYPE.to_string(),
651 }
652 }
653}
654
655#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
656#[pyclass(from_py_object)]
657pub struct ToolUseBlockParam {
658 #[pyo3(get, set)]
659 pub id: String,
660 #[pyo3(get, set)]
661 pub name: String,
662 pub input: Value,
663 #[serde(skip_serializing_if = "Option::is_none")]
664 #[pyo3(get, set)]
665 pub cache_control: Option<CacheControl>,
666 #[pyo3(get, set)]
667 #[serde(rename = "type")]
668 pub r#type: String,
669}
670
671#[pymethods]
672impl ToolUseBlockParam {
673 #[new]
674 #[pyo3(signature = (id, name, input, cache_control=None))]
675 pub fn new(
676 id: String,
677 name: String,
678 input: &Bound<'_, PyAny>,
679 cache_control: Option<CacheControl>,
680 ) -> Result<Self, TypeError> {
681 let input_value = depythonize(input)?;
682 Ok(Self {
683 id,
684 name,
685 input: input_value,
686 cache_control,
687 r#type: TOOL_USE_TYPE.to_string(),
688 })
689 }
690
691 #[getter]
692 pub fn input<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError> {
693 let py_dict = pythonize(py, &self.input)?;
694 Ok(py_dict)
695 }
696}
697
698#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
699#[serde(untagged)]
700pub enum ToolResultContentEnum {
701 Text(Vec<TextBlockParam>),
702 Image(Vec<ImageBlockParam>),
703 Document(Vec<DocumentBlockParam>),
704 SearchResult(Vec<SearchResultBlockParam>),
705}
706
707#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
708#[pyclass(from_py_object)]
709pub struct ToolResultBlockParam {
710 #[pyo3(get, set)]
711 pub tool_use_id: String,
712 #[serde(skip_serializing_if = "Option::is_none")]
713 pub is_error: Option<bool>,
714 #[serde(skip_serializing_if = "Option::is_none")]
715 #[pyo3(get, set)]
716 pub cache_control: Option<CacheControl>,
717 #[pyo3(get, set)]
718 #[serde(rename = "type")]
719 pub r#type: String,
720 #[serde(skip_serializing_if = "Option::is_none")]
721 pub content: Option<ToolResultContentEnum>,
722}
723
724fn extract_all_blocks<T>(blocks: Vec<Bound<'_, PyAny>>) -> Result<Vec<T>, TypeError>
726where
727 T: for<'a, 'py> FromPyObject<'a, 'py>,
728{
729 blocks
730 .into_iter()
731 .map(|block| {
732 block
733 .extract::<T>()
734 .map_err(|_| TypeError::Error("Failed to extract block".to_string()))
735 })
736 .collect()
737}
738
739#[pymethods]
740impl ToolResultBlockParam {
741 #[new]
742 #[pyo3(signature = (
743 tool_use_id,
744 is_error=None,
745 cache_control=None,
746 content=None
747 ))]
748 pub fn new(
749 tool_use_id: String,
750 is_error: Option<bool>,
751 cache_control: Option<CacheControl>,
752 content: Option<Vec<Bound<'_, PyAny>>>,
753 ) -> Result<Self, TypeError> {
754 let content_enum = match content {
755 None => None,
756 Some(blocks) if blocks.is_empty() => None,
757
758 Some(blocks) => {
759 let first_block = &blocks[0];
760
761 if first_block.is_instance_of::<TextBlockParam>() {
762 Some(ToolResultContentEnum::Text(extract_all_blocks(blocks)?))
763 } else if first_block.is_instance_of::<ImageBlockParam>() {
764 Some(ToolResultContentEnum::Image(extract_all_blocks(blocks)?))
765 } else if first_block.is_instance_of::<DocumentBlockParam>() {
766 Some(ToolResultContentEnum::Document(extract_all_blocks(blocks)?))
767 } else if first_block.is_instance_of::<SearchResultBlockParam>() {
768 Some(ToolResultContentEnum::SearchResult(extract_all_blocks(
769 blocks,
770 )?))
771 } else {
772 return Err(TypeError::InvalidInput(
773 "Unsupported content block type".to_string(),
774 ));
775 }
776 }
777 };
778
779 Ok(Self {
780 tool_use_id,
781 is_error,
782 cache_control,
783 r#type: TOOL_RESULT_TYPE.to_string(),
784 content: content_enum,
785 })
786 }
787
788 #[getter]
789 pub fn content<'py>(&self, py: Python<'py>) -> Result<Option<Bound<'py, PyAny>>, TypeError> {
790 match &self.content {
791 None => Ok(None),
792 Some(ToolResultContentEnum::Text(blocks)) => {
793 let py_list = blocks
794 .iter()
795 .map(|block| block.clone().into_bound_py_any(py))
796 .collect::<Result<Vec<_>, _>>()?;
797 Ok(Some(py_list.into_bound_py_any(py)?))
798 }
799 Some(ToolResultContentEnum::Image(blocks)) => {
800 let py_list = blocks
801 .iter()
802 .map(|block| block.clone().into_bound_py_any(py))
803 .collect::<Result<Vec<_>, _>>()?;
804 Ok(Some(py_list.into_bound_py_any(py)?))
805 }
806 Some(ToolResultContentEnum::Document(blocks)) => {
807 let py_list = blocks
808 .iter()
809 .map(|block| block.clone().into_bound_py_any(py))
810 .collect::<Result<Vec<_>, _>>()?;
811 Ok(Some(py_list.into_bound_py_any(py)?))
812 }
813 Some(ToolResultContentEnum::SearchResult(blocks)) => {
814 let py_list = blocks
815 .iter()
816 .map(|block| block.clone().into_bound_py_any(py))
817 .collect::<Result<Vec<_>, _>>()?;
818 Ok(Some(py_list.into_bound_py_any(py)?))
819 }
820 }
821 }
822}
823
824#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
825#[pyclass(from_py_object)]
826pub struct ServerToolUseBlockParam {
827 #[pyo3(get, set)]
828 pub id: String,
829 #[pyo3(get, set)]
830 pub name: String,
831 pub input: Value,
832 #[serde(skip_serializing_if = "Option::is_none")]
833 pub cache_control: Option<CacheControl>,
834 #[pyo3(get, set)]
835 #[serde(rename = "type")]
836 pub r#type: String,
837}
838
839#[pymethods]
840impl ServerToolUseBlockParam {
841 #[new]
842 #[pyo3(signature = (id, name, input, cache_control=None))]
843 pub fn new(
844 id: String,
845 name: String,
846 input: &Bound<'_, PyAny>,
847 cache_control: Option<CacheControl>,
848 ) -> Result<Self, TypeError> {
849 let input_value = depythonize(input)?;
850 Ok(Self {
851 id,
852 name,
853 input: input_value,
854 cache_control,
855 r#type: SERVER_TOOL_USE_TYPE.to_string(),
856 })
857 }
858 #[getter]
859 pub fn input<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError> {
860 let py_dict = pythonize(py, &self.input)?;
861 Ok(py_dict)
862 }
863}
864
865#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
866#[pyclass(from_py_object)]
867pub struct WebSearchResultBlockParam {
868 #[pyo3(get, set)]
869 pub encrypted_content: String,
870 #[pyo3(get, set)]
871 pub title: String,
872 #[pyo3(get, set)]
873 pub url: String,
874 #[pyo3(get, set)]
875 pub page_agent: Option<String>,
876 #[pyo3(get, set)]
877 #[serde(rename = "type")]
878 pub r#type: String,
879}
880
881#[pymethods]
882impl WebSearchResultBlockParam {
883 #[new]
884 #[pyo3(signature = (encrypted_content, title, url, page_agent=None))]
885 pub fn new(
886 encrypted_content: String,
887 title: String,
888 url: String,
889 page_agent: Option<String>,
890 ) -> Self {
891 Self {
892 encrypted_content,
893 title,
894 url,
895 page_agent,
896 r#type: WEB_SEARCH_RESULT_TYPE.to_string(),
897 }
898 }
899}
900
901#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
902#[pyclass(from_py_object)]
903pub struct WebSearchToolResultBlockParam {
904 #[pyo3(get, set)]
905 pub tool_use_id: String,
906 #[pyo3(get, set)]
907 pub content: Vec<WebSearchResultBlockParam>,
908 #[serde(skip_serializing_if = "Option::is_none")]
909 #[pyo3(get, set)]
910 pub cache_control: Option<CacheControl>,
911 #[pyo3(get, set)]
912 #[serde(rename = "type")]
913 pub r#type: String,
914}
915
916#[pymethods]
917impl WebSearchToolResultBlockParam {
918 #[new]
919 #[pyo3(signature = (tool_use_id, content, cache_control=None))]
920 pub fn new(
921 tool_use_id: String,
922 content: Vec<WebSearchResultBlockParam>,
923 cache_control: Option<CacheControl>,
924 ) -> Self {
925 Self {
926 tool_use_id,
927 content,
928 cache_control,
929 r#type: WEB_SEARCH_TOOL_RESULT_TYPE.to_string(),
930 }
931 }
932}
933
934#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
935#[serde(untagged)]
936pub(crate) enum ContentBlock {
937 Text(TextBlockParam),
938 Image(ImageBlockParam),
939 Document(DocumentBlockParam),
940 SearchResult(SearchResultBlockParam),
941 Thinking(ThinkingBlockParam),
942 RedactedThinking(RedactedThinkingBlockParam),
943 ToolUse(ToolUseBlockParam),
944 ToolResult(ToolResultBlockParam),
945 ServerToolUse(ServerToolUseBlockParam),
946 WebSearchResult(WebSearchResultBlockParam),
947}
948
949macro_rules! try_extract_content_block {
950 ($block:expr, $($variant:ident => $type:ty),+ $(,)?) => {{
951 $(
952 if $block.is_instance_of::<$type>() {
953 return Ok(Self {
954 inner: ContentBlock::$variant($block.extract::<$type>()?),
955 });
956 }
957 )+
958 return Err(TypeError::InvalidInput(
959 "Unsupported content block type".to_string(),
960 ));
961 }};
962}
963
964#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
965pub struct ContentBlockParam {
966 #[serde(flatten)]
967 pub(crate) inner: ContentBlock,
968}
969
970impl ContentBlockParam {
971 pub fn new(block: &Bound<'_, PyAny>) -> Result<Self, TypeError> {
972 try_extract_content_block!(
973 block,
974 Text => TextBlockParam,
975 Image => ImageBlockParam,
976 Document => DocumentBlockParam,
977 SearchResult => SearchResultBlockParam,
978 Thinking => ThinkingBlockParam,
979 RedactedThinking => RedactedThinkingBlockParam,
980 ToolUse => ToolUseBlockParam,
981 ToolResult => ToolResultBlockParam,
982 ServerToolUse => ServerToolUseBlockParam,
983 WebSearchResult => WebSearchResultBlockParam,
984 )
985 }
986
987 pub fn to_pyobject<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError> {
990 match &self.inner {
991 ContentBlock::Text(block) => Ok(block.clone().into_bound_py_any(py)?.clone()),
992 ContentBlock::Image(block) => Ok(block.clone().into_bound_py_any(py)?.clone()),
993 ContentBlock::Document(block) => Ok(block.clone().into_bound_py_any(py)?.clone()),
994 ContentBlock::SearchResult(block) => Ok(block.clone().into_bound_py_any(py)?.clone()),
995 ContentBlock::Thinking(block) => Ok(block.clone().into_bound_py_any(py)?.clone()),
996 ContentBlock::RedactedThinking(block) => {
997 Ok(block.clone().into_bound_py_any(py)?.clone())
998 }
999 ContentBlock::ToolUse(block) => Ok(block.clone().into_bound_py_any(py)?.clone()),
1000 ContentBlock::ToolResult(block) => Ok(block.clone().into_bound_py_any(py)?.clone()),
1001 ContentBlock::ServerToolUse(block) => Ok(block.clone().into_bound_py_any(py)?.clone()),
1002 ContentBlock::WebSearchResult(block) => {
1003 Ok(block.clone().into_bound_py_any(py)?.clone())
1004 }
1005 }
1006 }
1007}
1008
1009impl ContentBlockParam {
1010 pub(crate) fn from_response_content_block(
1022 block: &ResponseContentBlockInner,
1023 ) -> Result<Self, TypeError> {
1024 match block {
1025 ResponseContentBlockInner::Text(text_block) => {
1026 let citations = text_block.citations.as_ref().and_then(|cits| {
1029 cits.first().map(|cit| match cit {
1030 TextCitation::CharLocation(c) => {
1031 TextCitationParam::CharLocation(CitationCharLocationParam {
1032 cited_text: c.cited_text.clone(),
1033 document_index: c.document_index,
1034 document_title: c.document_title.clone(),
1035 end_char_index: c.end_char_index,
1036 start_char_index: c.start_char_index,
1037 r#type: c.r#type.clone(),
1038 })
1039 }
1040 TextCitation::PageLocation(c) => {
1041 TextCitationParam::PageLocation(CitationPageLocationParam {
1042 cited_text: c.cited_text.clone(),
1043 document_index: c.document_index,
1044 document_title: c.document_title.clone(),
1045 end_page_number: c.end_page_number,
1046 start_page_number: c.start_page_number,
1047 r#type: c.r#type.clone(),
1048 })
1049 }
1050 TextCitation::ContentBlockLocation(c) => {
1051 TextCitationParam::ContentBlockLocation(
1052 CitationContentBlockLocationParam {
1053 cited_text: c.cited_text.clone(),
1054 document_index: c.document_index,
1055 document_title: c.document_title.clone(),
1056 end_block_index: c.end_block_index,
1057 start_block_index: c.start_block_index,
1058 r#type: c.r#type.clone(),
1059 },
1060 )
1061 }
1062 TextCitation::WebSearchResultLocation(c) => {
1063 TextCitationParam::WebSearchResultLocation(
1064 CitationWebSearchResultLocationParam {
1065 cited_text: c.cited_text.clone(),
1066 encrypted_index: c.encrypted_index.clone(),
1067 title: c.title.clone(),
1068 r#type: c.r#type.clone(),
1069 url: c.url.clone(),
1070 },
1071 )
1072 }
1073 TextCitation::SearchResultLocation(c) => {
1074 TextCitationParam::SearchResultLocation(
1075 CitationSearchResultLocationParam {
1076 cited_text: c.cited_text.clone(),
1077 end_block_index: c.end_block_index,
1078 search_result_index: c.search_result_index,
1079 source: c.source.clone(),
1080 start_block_index: c.start_block_index,
1081 title: c.title.clone(),
1082 r#type: c.r#type.clone(),
1083 },
1084 )
1085 }
1086 })
1087 });
1088
1089 Ok(Self {
1090 inner: ContentBlock::Text(TextBlockParam {
1091 text: text_block.text.clone(),
1092 cache_control: None,
1093 citations,
1094 r#type: text_block.r#type.clone(),
1095 }),
1096 })
1097 }
1098 ResponseContentBlockInner::Thinking(thinking_block) => Ok(Self {
1099 inner: ContentBlock::Thinking(ThinkingBlockParam {
1100 thinking: thinking_block.thinking.clone(),
1101 signature: thinking_block.signature.clone(),
1102 r#type: thinking_block.r#type.clone(),
1103 }),
1104 }),
1105 ResponseContentBlockInner::RedactedThinking(redacted_thinking_block) => Ok(Self {
1106 inner: ContentBlock::RedactedThinking(RedactedThinkingBlockParam {
1107 data: redacted_thinking_block.data.clone(),
1108 r#type: redacted_thinking_block.r#type.clone(),
1109 }),
1110 }),
1111 ResponseContentBlockInner::ToolUse(tool_use_block) => Ok(Self {
1112 inner: ContentBlock::ToolUse(ToolUseBlockParam {
1113 id: tool_use_block.id.clone(),
1114 name: tool_use_block.name.clone(),
1115 input: tool_use_block.input.clone(),
1116 cache_control: None,
1117 r#type: tool_use_block.r#type.clone(),
1118 }),
1119 }),
1120 ResponseContentBlockInner::ServerToolUse(server_tool_use_block) => Ok(Self {
1121 inner: ContentBlock::ServerToolUse(ServerToolUseBlockParam {
1122 id: server_tool_use_block.id.clone(),
1123 name: server_tool_use_block.name.clone(),
1124 input: server_tool_use_block.input.clone(),
1125 cache_control: None,
1126 r#type: server_tool_use_block.r#type.clone(),
1127 }),
1128 }),
1129 ResponseContentBlockInner::WebSearchToolResult(web_search_tool_result_block) => {
1130 match &web_search_tool_result_block.content {
1131 WebSearchToolResultBlockContent::Results(results) => {
1132 let first_result = results.first().ok_or_else(|| {
1134 TypeError::InvalidInput(
1135 "WebSearchToolResult must contain at least one result".to_string(),
1136 )
1137 })?;
1138
1139 Ok(Self {
1140 inner: ContentBlock::WebSearchResult(WebSearchResultBlockParam {
1141 encrypted_content: first_result.encrypted_content.clone(),
1142 title: first_result.title.clone(),
1143 url: first_result.url.clone(),
1144 page_agent: first_result.page_age.clone(), r#type: first_result.r#type.clone(),
1146 }),
1147 })
1148 }
1149 WebSearchToolResultBlockContent::Error(_) => Err(TypeError::InvalidInput(
1150 "Cannot convert WebSearchToolResult error to ContentBlockParam".to_string(),
1151 )),
1152 }
1153 }
1154 }
1155 }
1156}
1157
1158#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1159#[pyclass(from_py_object)]
1160pub struct MessageParam {
1161 pub content: Vec<ContentBlockParam>,
1162 #[pyo3(get)]
1163 pub role: String,
1164}
1165
1166#[pymethods]
1167impl MessageParam {
1168 #[new]
1175 pub fn new(content: &Bound<'_, PyAny>, role: String) -> Result<Self, TypeError> {
1176 let content: Vec<ContentBlockParam> = if content.is_instance_of::<pyo3::types::PyString>() {
1177 let text = content.extract::<String>()?;
1178 let text_block = TextBlockParam::new(text, None, None)?;
1179 let content_block =
1180 ContentBlockParam::new(&text_block.into_bound_py_any(content.py())?)?;
1181 vec![content_block]
1182 } else if content.is_instance_of::<pyo3::types::PyList>() {
1183 let content_list = content.extract::<Vec<Bound<'_, PyAny>>>()?;
1184 let mut blocks = Vec::new();
1185 for item in content_list {
1186 let content_block = ContentBlockParam::new(&item)?;
1187 blocks.push(content_block);
1188 }
1189 blocks
1190 } else {
1191 let content_block = ContentBlockParam::new(content)?;
1193 vec![content_block]
1194 };
1195
1196 Ok(Self { content, role })
1197 }
1198
1199 #[getter]
1200 fn content<'py>(&self, py: Python<'py>) -> Result<Vec<Bound<'py, PyAny>>, TypeError> {
1201 self.content
1202 .iter()
1203 .map(|block| block.to_pyobject(py))
1204 .collect()
1205 }
1206
1207 #[pyo3(name = "bind")]
1208 fn bind_py(&self, name: &str, value: &str) -> Result<Self, TypeError> {
1209 self.bind(name, value)
1210 }
1211
1212 #[pyo3(name = "bind_mut")]
1213 fn bind_mut_py(&mut self, name: &str, value: &str) -> Result<(), TypeError> {
1214 self.bind_mut(name, value)
1215 }
1216
1217 pub fn __str__(&self) -> String {
1218 PyHelperFuncs::__str__(self)
1219 }
1220
1221 pub fn model_dump<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError> {
1222 let json = serde_json::to_value(self)?;
1224 Ok(pythonize(py, &json)?)
1225 }
1226
1227 #[getter]
1229 pub fn text(&self) -> String {
1230 self.content
1231 .iter()
1232 .find_map(|part| {
1233 if let ContentBlock::Text(text_part) = &part.inner {
1234 Some(text_part.text.clone())
1235 } else {
1236 None
1237 }
1238 })
1239 .unwrap_or_default()
1240 }
1241}
1242
1243impl PromptMessageExt for MessageParam {
1244 fn bind_mut(&mut self, name: &str, value: &str) -> Result<(), TypeError> {
1245 let regex = get_var_regex();
1246
1247 for part in &mut self.content {
1248 if let ContentBlock::Text(text_part) = &mut part.inner {
1249 text_part.text = regex
1250 .replace_all(&text_part.text, |caps: ®ex::Captures| {
1251 if &caps[1] == name {
1252 value.to_string()
1253 } else {
1254 caps.get(0)
1255 .map_or_else(|| "".to_string(), |m| m.as_str().to_string())
1256 }
1257 })
1258 .to_string();
1259 }
1260 }
1261
1262 Ok(())
1263 }
1264
1265 fn bind(&self, name: &str, value: &str) -> Result<Self, TypeError>
1266 where
1267 Self: Sized,
1268 {
1269 let mut new_message = self.clone();
1270 new_message.bind_mut(name, value)?;
1271 Ok(new_message)
1272 }
1273
1274 fn extract_variables(&self) -> Vec<String> {
1275 let mut variables = HashSet::new();
1276
1277 let regex = get_var_regex();
1279
1280 for part in &self.content {
1282 if let ContentBlock::Text(text_part) = &part.inner {
1283 for captures in regex.captures_iter(&text_part.text) {
1284 if let Some(name) = captures.get(1) {
1285 variables.insert(name.as_str().to_string());
1286 }
1287 }
1288 }
1289 }
1290
1291 variables.into_iter().collect()
1293 }
1294
1295 fn from_text(content: String, role: &str) -> Result<Self, TypeError> {
1296 Ok(Self {
1297 role: role.to_string(),
1298 content: vec![ContentBlockParam {
1299 inner: ContentBlock::Text(TextBlockParam::new_rs(content, None, None)),
1300 }],
1301 })
1302 }
1303
1304 fn extract_media_variables(&self) -> Vec<String> {
1305 let mut out = HashSet::new();
1306 let regex = crate::traits::get_media_regex();
1307 for part in &self.content {
1308 if let ContentBlock::Text(t) = &part.inner {
1309 for cap in regex.captures_iter(&t.text) {
1310 if let Some(name) = cap.get(1) {
1311 out.insert(name.as_str().to_string());
1312 }
1313 }
1314 }
1315 }
1316 out.into_iter().collect()
1317 }
1318
1319 fn split_media_placeholders(&mut self) -> Result<(), TypeError> {
1320 let regex = crate::traits::get_media_regex();
1321 let mut new_content: Vec<ContentBlockParam> = Vec::with_capacity(self.content.len());
1322 for part in self.content.drain(..) {
1323 match part.inner {
1324 ContentBlock::Text(t) if regex.is_match(&t.text) => {
1325 let cache = t.cache_control.clone();
1326 let citations = t.citations.clone();
1327 let pieces = crate::traits::split_text_on_media(&t.text, regex);
1328 let mut first = true;
1329 for s in pieces {
1330 if s.is_empty() {
1331 continue;
1332 }
1333 let block = TextBlockParam::new_rs(
1334 s,
1335 if first { cache.clone() } else { None },
1336 if first { citations.clone() } else { None },
1337 );
1338 new_content.push(ContentBlockParam {
1339 inner: ContentBlock::Text(block),
1340 });
1341 first = false;
1342 }
1343 }
1344 other => new_content.push(ContentBlockParam { inner: other }),
1345 }
1346 }
1347 self.content = new_content;
1348 Ok(())
1349 }
1350
1351 fn bind_media_mut(
1352 &mut self,
1353 token: &str,
1354 media: &crate::prompt::media::MediaRef,
1355 _provider: &crate::Provider,
1356 ) -> Result<bool, TypeError> {
1357 use crate::prompt::media::{MediaKind, MediaSource};
1358
1359 let mut replaced = false;
1360 let mut new_content: Vec<ContentBlockParam> = Vec::with_capacity(self.content.len());
1361 for part in self.content.drain(..) {
1362 match &part.inner {
1363 ContentBlock::Text(t) if t.text.trim() == token => {
1364 let new_inner = match (media.kind, &media.source) {
1365 (MediaKind::Image, MediaSource::Url { url, .. }) => {
1366 ContentBlock::Image(ImageBlockParam {
1367 source: ImageSource::Url(UrlImageSource::new(url.clone())),
1368 cache_control: None,
1369 r#type: IMAGE_TYPE.to_string(),
1370 })
1371 }
1372 (MediaKind::Image, MediaSource::Base64 { mime_type, data }) => {
1373 ContentBlock::Image(ImageBlockParam {
1374 source: ImageSource::Base64(Base64ImageSource {
1375 media_type: mime_type.clone(),
1376 data: data.clone(),
1377 r#type: BASE64_TYPE.to_string(),
1378 }),
1379 cache_control: None,
1380 r#type: IMAGE_TYPE.to_string(),
1381 })
1382 }
1383 (MediaKind::Document, MediaSource::Url { url, .. }) => {
1384 ContentBlock::Document(DocumentBlockParam {
1385 source: DocumentSource::Url(UrlPDFSource::new(url.clone())),
1386 cache_control: None,
1387 title: None,
1388 context: None,
1389 r#type: DOCUMENT_TYPE.to_string(),
1390 citations: None,
1391 })
1392 }
1393 (MediaKind::Document, MediaSource::Base64 { mime_type, data }) => {
1394 ContentBlock::Document(DocumentBlockParam {
1395 source: DocumentSource::Base64(Base64PDFSource {
1396 media_type: mime_type.clone(),
1397 data: data.clone(),
1398 r#type: BASE64_TYPE.to_string(),
1399 }),
1400 cache_control: None,
1401 title: None,
1402 context: None,
1403 r#type: DOCUMENT_TYPE.to_string(),
1404 citations: None,
1405 })
1406 }
1407 };
1408 new_content.push(ContentBlockParam { inner: new_inner });
1409 replaced = true;
1410 }
1411 ContentBlock::Text(t) if t.text.contains(token) => {
1412 return Err(TypeError::MediaPlaceholderNotIsolated {
1413 name: crate::traits::extract_token_name(token),
1414 });
1415 }
1416 _ => new_content.push(part),
1417 }
1418 }
1419 self.content = new_content;
1420 Ok(replaced)
1421 }
1422}
1423
1424impl MessageParam {
1425 pub fn to_text_block_param(&self) -> Result<TextBlockParam, TypeError> {
1427 if self.content.len() != 1 {
1428 return Err(TypeError::InvalidInput(
1429 "MessageParam must contain exactly one content block to convert to TextBlockParam"
1430 .to_string(),
1431 ));
1432 }
1433
1434 match &self.content[0].inner {
1435 ContentBlock::Text(text_block) => Ok(text_block.clone()),
1436 _ => Err(TypeError::InvalidInput(
1437 "Content block is not of type TextBlockParam".to_string(),
1438 )),
1439 }
1440 }
1441}
1442
1443impl MessageFactory for MessageParam {
1444 fn from_text(content: String, role: &str) -> Result<Self, TypeError> {
1445 let text_block = TextBlockParam::new_rs(content, None, None);
1446 let content_block = ContentBlockParam {
1447 inner: ContentBlock::Text(text_block),
1448 };
1449
1450 Ok(Self {
1451 role: role.to_string(),
1452 content: vec![content_block],
1453 })
1454 }
1455}
1456
1457impl MessageConversion for MessageParam {
1458 fn to_anthropic_message(&self) -> Result<Self, TypeError> {
1459 Err(TypeError::CantConvertSelf)
1461 }
1462
1463 fn to_google_message(
1464 &self,
1465 ) -> Result<crate::google::v1::generate::request::GeminiContent, TypeError> {
1466 let mut parts = Vec::new();
1468
1469 for content_block in &self.content {
1470 match &content_block.inner {
1471 ContentBlock::Text(text_block) => {
1472 parts.push(Part {
1473 data: DataNum::Text(text_block.text.clone()),
1474 thought: None,
1475 thought_signature: None,
1476 part_metadata: None,
1477 media_resolution: None,
1478 video_metadata: None,
1479 });
1480 }
1481 _ => {
1482 return Err(TypeError::UnsupportedConversion(
1483 "Only text content blocks are currently supported for conversion"
1484 .to_string(),
1485 ));
1486 }
1487 }
1488 }
1489
1490 if parts.is_empty() {
1491 return Err(TypeError::UnsupportedConversion(
1492 "Message contains no text content to convert".to_string(),
1493 ));
1494 }
1495
1496 Ok(GeminiContent {
1497 role: self.role.clone(),
1498 parts,
1499 })
1500 }
1501
1502 fn to_openai_message(
1503 &self,
1504 ) -> Result<crate::openai::v1::chat::request::ChatMessage, TypeError> {
1505 let mut content_parts = Vec::new();
1507
1508 for content_block in &self.content {
1509 match &content_block.inner {
1510 ContentBlock::Text(text_block) => {
1511 content_parts.push(ContentPart::Text(TextContentPart::new(
1512 text_block.text.clone(),
1513 )));
1514 }
1515 _ => {
1516 return Err(TypeError::UnsupportedConversion(
1517 "Only text content blocks are currently supported for conversion"
1518 .to_string(),
1519 ));
1520 }
1521 }
1522 }
1523
1524 if content_parts.is_empty() {
1525 return Err(TypeError::UnsupportedConversion(
1526 "Message contains no text content to convert".to_string(),
1527 ));
1528 }
1529
1530 Ok(ChatMessage {
1531 role: self.role.clone(),
1532 content: content_parts,
1533 name: None,
1534 })
1535 }
1536}
1537
1538#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1539#[pyclass(from_py_object)]
1540pub struct Metadata {
1541 #[serde(skip_serializing_if = "Option::is_none")]
1542 pub user_id: Option<String>,
1543}
1544
1545#[pymethods]
1546impl Metadata {
1547 #[new]
1548 #[pyo3(signature = (user_id=None))]
1549 pub fn new(user_id: Option<String>) -> Self {
1550 Self { user_id }
1551 }
1552}
1553
1554#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1555#[pyclass(from_py_object)]
1556pub struct CacheControl {
1557 #[serde(rename = "type")]
1558 pub cache_type: String, #[serde(skip_serializing_if = "Option::is_none")]
1560 pub ttl: Option<String>, }
1562
1563#[pymethods]
1564impl CacheControl {
1565 #[new]
1566 #[pyo3(signature = (cache_type, ttl=None))]
1567 pub fn new(cache_type: String, ttl: Option<String>) -> Self {
1568 Self { cache_type, ttl }
1569 }
1570}
1571
1572#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1573#[pyclass(from_py_object, name = "AnthropicTool")]
1574pub struct Tool {
1575 pub name: String,
1576 #[serde(skip_serializing_if = "Option::is_none")]
1577 pub description: Option<String>,
1578 pub input_schema: Value,
1579 #[serde(skip_serializing_if = "Option::is_none")]
1580 pub cache_control: Option<CacheControl>,
1581}
1582
1583#[pymethods]
1584impl Tool {
1585 #[new]
1586 #[pyo3(signature = (
1587 name,
1588 input_schema,
1589 description=None,
1590 cache_control=None
1591 ))]
1592 pub fn new(
1593 name: String,
1594 input_schema: &Bound<'_, PyAny>,
1595 description: Option<String>,
1596 cache_control: Option<CacheControl>,
1597 ) -> Result<Self, UtilError> {
1598 Ok(Self {
1599 name,
1600 description,
1601 input_schema: depythonize(input_schema)?,
1602 cache_control,
1603 })
1604 }
1605}
1606
1607impl Tool {
1608 pub fn from_tool_agent_tool_definition(tool: &AgentToolDefinition) -> Result<Self, TypeError> {
1609 Ok(Self {
1610 name: tool.name.clone(),
1611 description: Some(tool.description.clone()),
1612 input_schema: tool.parameters.clone(),
1613 cache_control: None,
1614 })
1615 }
1616}
1617
1618#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1619#[pyclass(from_py_object, name = "AnthropicThinkingConfig")]
1620pub struct ThinkingConfig {
1621 #[pyo3(get)]
1622 pub r#type: String,
1623
1624 #[serde(skip_serializing_if = "Option::is_none")]
1625 #[pyo3(get)]
1626 pub budget_tokens: Option<i32>,
1627}
1628
1629#[pymethods]
1630impl ThinkingConfig {
1631 #[new]
1632 #[pyo3(signature = (r#type, budget_tokens=None))]
1633 pub fn new(r#type: String, budget_tokens: Option<i32>) -> Self {
1634 Self {
1635 r#type,
1636 budget_tokens,
1637 }
1638 }
1639}
1640
1641#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1642#[pyclass(from_py_object, name = "AnthropicToolChoice")]
1643pub struct ToolChoice {
1644 #[pyo3(get)]
1645 pub r#type: String, #[serde(skip_serializing_if = "Option::is_none")]
1648 #[pyo3(get)]
1649 disable_parallel_tool_use: Option<bool>,
1650
1651 #[serde(skip_serializing_if = "Option::is_none")]
1652 #[pyo3(get)]
1653 pub name: Option<String>,
1654}
1655
1656#[pymethods]
1657impl ToolChoice {
1658 #[new]
1659 #[pyo3(signature = (r#type, disable_parallel_tool_use=None, name=None))]
1660 pub fn new(
1661 r#type: String,
1662 disable_parallel_tool_use: Option<bool>,
1663 name: Option<String>,
1664 ) -> Result<Self, UtilError> {
1665 match name {
1666 Some(_) if r#type != "tool" => {
1667 return Err(UtilError::PyError(
1668 "ToolChoice name can only be set if type is 'tool'".to_string(),
1669 ))
1670 }
1671 None if r#type == "tool" => {
1672 return Err(UtilError::PyError(
1673 "ToolChoice of type 'tool' requires a name".to_string(),
1674 ))
1675 }
1676 _ => {}
1677 }
1678
1679 Ok(Self {
1680 r#type,
1681 disable_parallel_tool_use,
1682 name,
1683 })
1684 }
1685}
1686
1687#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1688#[pyclass(from_py_object)]
1689#[serde(default)]
1690pub struct AnthropicSettings {
1691 #[pyo3(get)]
1692 pub max_tokens: i32,
1693
1694 #[serde(skip_serializing_if = "Option::is_none")]
1695 #[pyo3(get)]
1696 pub metadata: Option<Metadata>,
1697
1698 #[serde(skip_serializing_if = "Option::is_none")]
1699 #[pyo3(get)]
1700 pub service_tier: Option<String>,
1701
1702 #[serde(skip_serializing_if = "Option::is_none")]
1703 #[pyo3(get)]
1704 pub stop_sequences: Option<Vec<String>>,
1705
1706 #[serde(skip_serializing_if = "Option::is_none")]
1707 #[pyo3(get)]
1708 pub stream: Option<bool>,
1709
1710 #[serde(skip_serializing_if = "Option::is_none")]
1711 #[pyo3(get)]
1712 pub system: Option<String>,
1713
1714 #[serde(skip_serializing_if = "Option::is_none")]
1715 #[pyo3(get)]
1716 pub temperature: Option<f32>,
1717
1718 #[serde(skip_serializing_if = "Option::is_none")]
1719 #[pyo3(get)]
1720 pub thinking: Option<ThinkingConfig>,
1721
1722 #[serde(skip_serializing_if = "Option::is_none")]
1723 #[pyo3(get)]
1724 pub tool_choice: Option<ToolChoice>,
1725
1726 #[serde(skip_serializing_if = "Option::is_none")]
1727 #[pyo3(get)]
1728 pub tools: Option<Vec<Tool>>,
1729
1730 #[serde(skip_serializing_if = "Option::is_none")]
1731 #[pyo3(get)]
1732 pub top_k: Option<i32>,
1733
1734 #[serde(skip_serializing_if = "Option::is_none")]
1735 #[pyo3(get)]
1736 pub top_p: Option<f32>,
1737
1738 #[serde(skip_serializing_if = "Option::is_none")]
1739 pub extra_body: Option<Value>,
1740}
1741
1742impl Default for AnthropicSettings {
1743 fn default() -> Self {
1744 Self {
1745 max_tokens: 4096,
1746 metadata: None,
1747 service_tier: None,
1748 stop_sequences: None,
1749 stream: Some(false),
1750 system: None,
1751 temperature: None,
1752 thinking: None,
1753 top_k: None,
1754 top_p: None,
1755 tools: None,
1756 tool_choice: None,
1757 extra_body: None,
1758 }
1759 }
1760}
1761
1762#[pymethods]
1763impl AnthropicSettings {
1764 #[new]
1765 #[pyo3(signature = (
1766 max_tokens=4096,
1767 metadata=None,
1768 service_tier=None,
1769 stop_sequences=None,
1770 stream=None,
1771 system =None,
1772 temperature=None,
1773 thinking=None,
1774 top_k=None,
1775 top_p=None,
1776 tools=None,
1777 tool_choice=None,
1778 extra_body=None
1779 ))]
1780 #[allow(clippy::too_many_arguments)]
1781 pub fn new(
1782 max_tokens: i32,
1783 metadata: Option<Metadata>,
1784 service_tier: Option<String>,
1785 stop_sequences: Option<Vec<String>>,
1786 stream: Option<bool>,
1787 system: Option<String>,
1788 temperature: Option<f32>,
1789 thinking: Option<ThinkingConfig>,
1790 top_k: Option<i32>,
1791 top_p: Option<f32>,
1792 tools: Option<Vec<Tool>>,
1793 tool_choice: Option<ToolChoice>,
1794 extra_body: Option<&Bound<'_, PyAny>>,
1795 ) -> Result<Self, UtilError> {
1796 let extra = match extra_body {
1797 Some(obj) => Some(depythonize(obj)?),
1798 None => None,
1799 };
1800
1801 Ok(Self {
1802 max_tokens,
1803 metadata,
1804 service_tier,
1805 stop_sequences,
1806 stream,
1807 system,
1808 temperature,
1809 thinking,
1810 top_k,
1811 top_p,
1812 tools,
1813 tool_choice,
1814 extra_body: extra,
1815 })
1816 }
1817
1818 pub fn __str__(&self) -> String {
1819 PyHelperFuncs::__str__(self)
1820 }
1821
1822 pub fn model_dump<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError> {
1823 let json = serde_json::to_value(self)?;
1825 Ok(pythonize(py, &json)?)
1826 }
1827
1828 pub fn settings_type(&self) -> SettingsType {
1829 SettingsType::Anthropic
1830 }
1831}
1832
1833impl AnthropicSettings {
1834 pub fn add_tools(&mut self, tools: Vec<AgentToolDefinition>) -> Result<(), TypeError> {
1835 let current_tools = self.tools.get_or_insert_with(Vec::new);
1836
1837 for tool in tools {
1838 let tool_param = Tool::from_tool_agent_tool_definition(&tool)?;
1839 current_tools.push(tool_param);
1840 }
1841
1842 Ok(())
1843 }
1844}
1845
1846#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1847pub struct AnthropicMessageRequestV1 {
1848 pub model: String,
1849 pub messages: Vec<MessageNum>,
1850 pub system: Vec<MessageNum>,
1851 #[serde(flatten)]
1852 pub settings: AnthropicSettings,
1853 #[serde(skip_serializing_if = "Option::is_none")]
1854 pub output_format: Option<Value>,
1855}
1856
1857pub(crate) fn create_structured_output_schema(json_schema: &Value) -> Value {
1858 serde_json::json!({
1859 "type": "json_schema",
1860 "schema": json_schema,
1861
1862 })
1863}
1864
1865impl RequestAdapter for AnthropicMessageRequestV1 {
1866 fn messages_mut(&mut self) -> &mut Vec<MessageNum> {
1867 &mut self.messages
1868 }
1869 fn messages(&self) -> &[MessageNum] {
1870 &self.messages
1871 }
1872 fn system_instructions(&self) -> Vec<&MessageNum> {
1873 self.system.iter().collect()
1874 }
1875 fn response_json_schema(&self) -> Option<&Value> {
1876 let schema = self.output_format.as_ref();
1877
1878 schema.and_then(|of| of.get("schema"))
1884 }
1885
1886 fn preprend_system_instructions(&mut self, messages: Vec<MessageNum>) -> Result<(), TypeError> {
1887 let mut combined = messages;
1888 combined.append(&mut self.system);
1889 self.system = combined;
1890 Ok(())
1891 }
1892
1893 fn get_py_system_instructions<'py>(
1894 &self,
1895 py: Python<'py>,
1896 ) -> Result<Bound<'py, PyList>, TypeError> {
1897 let py_system_instructions = PyList::empty(py);
1898 for system_msg in &self.system {
1899 py_system_instructions.append(system_msg.to_bound_py_object(py)?)?;
1900 }
1901
1902 Ok(py_system_instructions)
1903 }
1904
1905 fn model_settings<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError> {
1906 let settings = self.settings.clone();
1907 Ok(settings.into_bound_py_any(py)?)
1908 }
1909
1910 fn to_request_body(&self) -> Result<Value, TypeError> {
1911 Ok(serde_json::to_value(self)?)
1912 }
1913 fn match_provider(&self, provider: &Provider) -> bool {
1914 *provider == Provider::Anthropic
1915 }
1916 fn build_provider_enum(
1917 messages: Vec<MessageNum>,
1918 system_instructions: Vec<MessageNum>,
1919 model: String,
1920 settings: ModelSettings,
1921 response_json_schema: Option<Value>,
1922 ) -> Result<ProviderRequest, TypeError> {
1923 let anthropic_settings = match settings {
1924 ModelSettings::AnthropicChat(s) => s,
1925 _ => AnthropicSettings::default(),
1926 };
1927
1928 let output_format =
1929 response_json_schema.map(|json_schema| create_structured_output_schema(&json_schema));
1930
1931 Ok(ProviderRequest::AnthropicV1(AnthropicMessageRequestV1 {
1932 model,
1933 messages,
1934 system: system_instructions,
1935 settings: anthropic_settings,
1936 output_format,
1937 }))
1938 }
1939
1940 fn set_response_json_schema(&mut self, response_json_schema: Option<Value>) {
1941 self.output_format =
1942 response_json_schema.map(|json_schema| create_structured_output_schema(&json_schema));
1943 }
1944
1945 fn add_tools(&mut self, tools: Vec<AgentToolDefinition>) -> Result<(), TypeError> {
1946 self.settings.add_tools(tools)
1947 }
1948}
1949
1950#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1951#[pyclass(from_py_object)]
1952pub struct SystemPrompt {
1953 #[pyo3(get)]
1954 #[serde(flatten)]
1955 pub content: Vec<TextBlockParam>,
1956}
1957
1958#[pymethods]
1959impl SystemPrompt {
1960 #[new]
1969 pub fn new(content: &Bound<'_, PyAny>) -> Result<Self, TypeError> {
1970 let content_blocks: Vec<TextBlockParam> =
1971 if content.is_instance_of::<pyo3::types::PyString>() {
1972 let text = content.extract::<String>()?;
1973 let text_block = TextBlockParam::new(text, None, None)?;
1974 vec![text_block]
1975 } else if content.is_instance_of::<pyo3::types::PyList>() {
1976 let content_list = content.extract::<Vec<Bound<'_, PyAny>>>()?;
1977 let mut blocks = Vec::new();
1978 for item in content_list {
1979 let text_block = item.extract::<TextBlockParam>().map_err(|_| {
1980 TypeError::InvalidInput(
1981 "All items in the list must be TextBlockParam".to_string(),
1982 )
1983 })?;
1984 blocks.push(text_block);
1985 }
1986 blocks
1987 } else {
1988 return Err(TypeError::InvalidInput(
1989 "Content must be either a string or a list of TextBlockParam".to_string(),
1990 ));
1991 };
1992
1993 Ok(Self {
1994 content: content_blocks,
1995 })
1996 }
1997}