1use crate::functions;
4use functions::expression::{
5 ExpressionError, FromStarlarkValue, ToStarlarkValue, WithExpression,
6};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use starlark::values::dict::{
10 AllocDict as StarlarkAllocDict, DictRef as StarlarkDictRef,
11};
12use starlark::values::{
13 Heap as StarlarkHeap, UnpackValue, Value as StarlarkValue,
14};
15
16#[derive(
18 Debug,
19 Clone,
20 PartialEq,
21 Serialize,
22 Deserialize,
23 JsonSchema,
24 arbitrary::Arbitrary,
25)]
26#[serde(untagged)]
27#[schemars(rename = "agent.completions.message.RichContent")]
28pub enum RichContent {
29 #[schemars(title = "Text")]
31 Text(String),
32 #[schemars(title = "Parts")]
34 Parts(Vec<RichContentPart>),
35}
36
37impl RichContent {
38 pub fn push(&mut self, other: &RichContent) {
39 match (&mut *self, other) {
40 (RichContent::Text(self_text), RichContent::Text(other_text)) => {
41 self_text.push_str(&other_text);
42 }
43 (RichContent::Text(self_text), RichContent::Parts(other_parts)) => {
44 let mut parts = Vec::with_capacity(1 + other_parts.len());
45 parts.push(RichContentPart::Text {
46 text: std::mem::take(self_text),
47 });
48 parts.extend(other_parts.iter().cloned());
49 *self = RichContent::Parts(parts);
50 }
51 (RichContent::Parts(self_parts), RichContent::Text(other_text)) => {
52 self_parts.push(RichContentPart::Text {
53 text: other_text.clone(),
54 });
55 }
56 (
57 RichContent::Parts(self_parts),
58 RichContent::Parts(other_parts),
59 ) => {
60 self_parts.extend(other_parts.iter().cloned());
61 }
62 }
63 }
64
65 pub fn prepare(&mut self) {
70 let parts = match self {
72 RichContent::Text(_) => return,
73 RichContent::Parts(parts) => parts,
74 };
75
76 parts.iter_mut().for_each(RichContentPart::prepare);
78
79 let mut final_parts = Vec::with_capacity(parts.len());
81 let mut buffer: Option<String> = None;
82 for part in parts.drain(..) {
83 match part {
84 part if part.is_empty() => continue,
85 RichContentPart::Text { text } => {
86 if let Some(buffer) = &mut buffer {
87 buffer.push_str(&text);
88 } else {
89 buffer = Some(text);
90 }
91 }
92 part => {
93 if let Some(buffer) = buffer.take() {
94 final_parts
95 .push(RichContentPart::Text { text: buffer });
96 }
97 final_parts.push(part);
98 }
99 }
100 }
101 if let Some(buffer) = buffer.take() {
102 final_parts.push(RichContentPart::Text { text: buffer });
103 }
104
105 if final_parts.len() == 1
107 && matches!(&final_parts[0], RichContentPart::Text { .. })
108 {
109 match final_parts.into_iter().next() {
110 Some(RichContentPart::Text { text }) => {
111 *self = RichContent::Text(text);
112 }
113 _ => unreachable!(),
114 }
115 } else {
116 *self = RichContent::Parts(final_parts);
117 }
118 }
119
120 pub fn is_empty(&self) -> bool {
122 match self {
123 RichContent::Text(text) => text.is_empty(),
124 RichContent::Parts(parts) => parts.is_empty(),
125 }
126 }
127
128 pub fn id(&self) -> String {
130 let mut hasher = twox_hash::XxHash3_128::with_seed(0);
131 hasher.write(serde_json::to_string(self).unwrap().as_bytes());
132 format!("{:0>22}", base62::encode(hasher.finish_128()))
133 }
134
135 pub fn validate_text_or_image_only(&self) -> Result<(), String> {
143 match self {
144 RichContent::Text(_) => Ok(()),
145 RichContent::Parts(parts) => {
146 for (idx, part) in parts.iter().enumerate() {
147 match part {
148 RichContentPart::Text { .. }
149 | RichContentPart::ImageUrl { .. } => {}
150 RichContentPart::InputAudio { .. } => {
151 return Err(format!(
152 "part[{idx}] has unsupported media type `input_audio`; only text and image parts are allowed"
153 ));
154 }
155 RichContentPart::InputVideo { .. } => {
156 return Err(format!(
157 "part[{idx}] has unsupported media type `input_video`; only text and image parts are allowed"
158 ));
159 }
160 RichContentPart::VideoUrl { .. } => {
161 return Err(format!(
162 "part[{idx}] has unsupported media type `video_url`; only text and image parts are allowed"
163 ));
164 }
165 RichContentPart::File { .. } => {
166 return Err(format!(
167 "part[{idx}] has unsupported media type `file`; only text and image parts are allowed"
168 ));
169 }
170 }
171 }
172 Ok(())
173 }
174 }
175 }
176}
177
178impl FromStarlarkValue for RichContent {
179 fn from_starlark_value(
180 value: &StarlarkValue,
181 ) -> Result<Self, ExpressionError> {
182 if let Ok(Some(s)) = <&str as UnpackValue>::unpack_value(*value) {
183 return Ok(RichContent::Text(s.to_owned()));
184 }
185 let parts = Vec::<RichContentPart>::from_starlark_value(value)?;
186 Ok(RichContent::Parts(parts))
187 }
188}
189
190impl From<Vec<RichContentPart>> for RichContent {
195 fn from(parts: Vec<RichContentPart>) -> Self {
196 if parts.is_empty() {
197 return RichContent::Text(String::new());
198 }
199 let all_text = parts
200 .iter()
201 .all(|p| matches!(p, RichContentPart::Text { .. }));
202 if all_text {
203 let joined = parts
204 .into_iter()
205 .filter_map(|p| match p {
206 RichContentPart::Text { text } => Some(text),
207 _ => None,
208 })
209 .collect::<Vec<_>>()
210 .join("\n\n");
211 RichContent::Text(joined)
212 } else {
213 RichContent::Parts(parts)
214 }
215 }
216}
217
218impl RichContentPart {
219 pub fn from_text_or_data_url(text: String) -> Self {
226 match crate::data_url::parse_data_url(&text) {
227 Some((mime, payload)) => {
228 Self::from_blob(mime, payload.to_string(), None)
229 }
230 None => RichContentPart::Text { text },
231 }
232 }
233
234 pub fn from_blob(
247 mime: &str,
248 blob: String,
249 filename: Option<String>,
250 ) -> Self {
251 if mime.starts_with("image/") {
252 RichContentPart::ImageUrl {
253 image_url: ImageUrl {
254 url: format!("data:{};base64,{}", mime, blob),
255 detail: None,
256 },
257 }
258 } else if mime.starts_with("audio/") {
259 RichContentPart::InputAudio {
260 input_audio: InputAudio {
261 data: blob,
262 format: mime.to_string(),
263 },
264 }
265 } else if mime.starts_with("video/") {
266 RichContentPart::InputVideo {
267 video_url: VideoUrl {
268 url: format!("data:{};base64,{}", mime, blob),
269 },
270 }
271 } else {
272 RichContentPart::File {
273 file: File {
274 file_data: Some(blob),
275 filename,
276 file_id: None,
277 file_url: None,
278 },
279 }
280 }
281 }
282}
283
284impl RichContent {
285 pub fn from_text_or_data_url(text: String) -> Self {
290 Self::from(vec![RichContentPart::from_text_or_data_url(text)])
291 }
292}
293
294#[cfg(feature = "mcp")]
314impl From<crate::mcp::shared::ResourceContentsUnion> for RichContentPart {
315 fn from(contents: crate::mcp::shared::ResourceContentsUnion) -> Self {
316 use crate::mcp::shared::ResourceContentsUnion;
317 match contents {
318 ResourceContentsUnion::Text(text) => {
319 RichContentPart::Text { text: text.text }
320 }
321 ResourceContentsUnion::Blob(blob) => {
322 let mime = blob
323 .base
324 .mime_type
325 .as_deref()
326 .unwrap_or("application/octet-stream");
327 let filename = blob
328 .base
329 .uri
330 .rsplit('/')
331 .next()
332 .filter(|s| !s.is_empty())
333 .map(String::from);
334 RichContentPart::from_blob(mime, blob.blob, filename)
335 }
336 }
337 }
338}
339
340#[cfg(feature = "mcp")]
357fn meta_string(
358 meta: &Option<indexmap::IndexMap<String, serde_json::Value>>,
359 key: &str,
360) -> Option<String> {
361 meta.as_ref()?
362 .get(key)
363 .and_then(|v| v.as_str().map(String::from))
364}
365
366#[cfg(feature = "mcp")]
370fn meta_image_detail(
371 meta: &Option<indexmap::IndexMap<String, serde_json::Value>>,
372 key: &str,
373) -> Option<ImageUrlDetail> {
374 let v = meta.as_ref()?.get(key)?.clone();
375 serde_json::from_value::<ImageUrlDetail>(v).ok()
376}
377
378#[cfg(feature = "mcp")]
382fn decode_text_no_marker(
383 text: String,
384 meta: &Option<indexmap::IndexMap<String, serde_json::Value>>,
385) -> RichContentPart {
386 if let Some((mime, payload)) = crate::data_url::parse_data_url(&text) {
387 let filename = meta_string(meta, "objectiveai/filename");
388 RichContentPart::from_blob(mime, payload.to_string(), filename)
389 } else {
390 RichContentPart::Text { text }
391 }
392}
393
394#[cfg(feature = "mcp")]
395impl From<crate::mcp::tool::ContentBlock> for RichContentPart {
396 fn from(block: crate::mcp::tool::ContentBlock) -> Self {
397 use crate::mcp::tool::ContentBlock;
398 const META_KIND: &str = "objectiveai/kind";
402 const META_IMAGE_DETAIL: &str = "objectiveai/image_detail";
403 const META_FILENAME: &str = "objectiveai/filename";
404 const KIND_IMAGE_URL_REMOTE: &str = "image_url_remote";
405 const KIND_INPUT_VIDEO_REMOTE: &str = "input_video_remote";
406 const KIND_VIDEO_URL: &str = "video_url";
407 const KIND_FILE_URL: &str = "file_url";
408 const KIND_FILE_ID: &str = "file_id";
409
410 match block {
411 ContentBlock::Text(t) => {
412 let kind = meta_string(&t._meta, META_KIND);
416 if let Some(kind) = kind {
417 return match kind.as_str() {
418 KIND_IMAGE_URL_REMOTE => {
419 let detail =
420 meta_image_detail(&t._meta, META_IMAGE_DETAIL);
421 RichContentPart::ImageUrl {
422 image_url: ImageUrl {
423 url: t.text,
424 detail,
425 },
426 }
427 }
428 KIND_INPUT_VIDEO_REMOTE => {
429 RichContentPart::InputVideo {
430 video_url: VideoUrl { url: t.text },
431 }
432 }
433 KIND_VIDEO_URL => RichContentPart::VideoUrl {
434 video_url: VideoUrl { url: t.text },
435 },
436 KIND_FILE_URL => RichContentPart::File {
437 file: File {
438 file_data: None,
439 filename: meta_string(&t._meta, META_FILENAME),
440 file_id: None,
441 file_url: Some(t.text),
442 },
443 },
444 KIND_FILE_ID => RichContentPart::File {
445 file: File {
446 file_data: None,
447 filename: meta_string(&t._meta, META_FILENAME),
448 file_id: Some(t.text),
449 file_url: None,
450 },
451 },
452 _ => decode_text_no_marker(t.text, &t._meta),
455 };
456 }
457 decode_text_no_marker(t.text, &t._meta)
462 }
463 ContentBlock::Image(i) => {
464 let detail = meta_image_detail(&i._meta, META_IMAGE_DETAIL);
465 let mut image_url: ImageUrl = i.into();
466 image_url.detail = detail;
467 RichContentPart::ImageUrl { image_url }
468 }
469 ContentBlock::Audio(a) => RichContentPart::InputAudio {
470 input_audio: a.into(),
471 },
472 ContentBlock::EmbeddedResource(embedded) => {
473 embedded.resource.into()
474 }
475 block @ ContentBlock::ResourceLink(_) => RichContentPart::Text {
476 text: serde_json::to_string(&block).unwrap_or_default(),
477 },
478 }
479 }
480}
481
482#[cfg(feature = "mcp")]
487impl From<Vec<crate::mcp::tool::ContentBlock>> for RichContent {
488 fn from(blocks: Vec<crate::mcp::tool::ContentBlock>) -> Self {
489 let parts: Vec<RichContentPart> =
490 blocks.into_iter().map(Into::into).collect();
491 RichContent::from(parts)
492 }
493}
494
495#[derive(
497 Debug,
498 Clone,
499 PartialEq,
500 Serialize,
501 Deserialize,
502 JsonSchema,
503 arbitrary::Arbitrary,
504)]
505#[serde(untagged)]
506#[schemars(rename = "agent.completions.message.RichContentExpression")]
507pub enum RichContentExpression {
508 #[schemars(title = "Text")]
510 Text(String),
511 #[schemars(title = "Parts")]
513 Parts(
514 Vec<functions::expression::WithExpression<RichContentPartExpression>>,
515 ),
516}
517
518impl RichContentExpression {
519 pub fn compile(
521 self,
522 params: &functions::expression::Params,
523 ) -> Result<RichContent, functions::expression::ExpressionError> {
524 match self {
525 RichContentExpression::Text(text) => Ok(RichContent::Text(text)),
526 RichContentExpression::Parts(parts) => {
527 let mut compiled_parts = Vec::with_capacity(parts.len());
528 for part in parts {
529 match part.compile_one_or_many(params)? {
530 functions::expression::OneOrMany::One(one_part) => {
531 compiled_parts.push(one_part.compile(params)?);
532 }
533 functions::expression::OneOrMany::Many(many_parts) => {
534 for part in many_parts {
535 compiled_parts.push(part.compile(params)?);
536 }
537 }
538 }
539 }
540 Ok(RichContent::Parts(compiled_parts))
541 }
542 }
543 }
544}
545
546impl From<RichContent> for RichContentExpression {
547 fn from(content: RichContent) -> Self {
548 match content {
549 RichContent::Text(text) => RichContentExpression::Text(text),
550 RichContent::Parts(parts) => RichContentExpression::Parts(
551 parts
552 .into_iter()
553 .map(RichContentPartExpression::from)
554 .map(WithExpression::Value)
555 .collect(),
556 ),
557 }
558 }
559}
560
561impl FromStarlarkValue for RichContentExpression {
562 fn from_starlark_value(
563 value: &StarlarkValue,
564 ) -> Result<Self, ExpressionError> {
565 if let Ok(Some(s)) = <&str as UnpackValue>::unpack_value(*value) {
566 return Ok(RichContentExpression::Text(s.to_owned()));
567 }
568 let parts = Vec::<WithExpression<RichContentPartExpression>>::from_starlark_value(value)?;
569 Ok(RichContentExpression::Parts(parts))
570 }
571}
572
573#[derive(
575 Debug,
576 Clone,
577 Hash,
578 PartialEq,
579 Eq,
580 Serialize,
581 Deserialize,
582 JsonSchema,
583 arbitrary::Arbitrary,
584)]
585#[serde(tag = "type", rename_all = "snake_case")]
586#[schemars(rename = "agent.completions.message.RichContentPart")]
587pub enum RichContentPart {
588 #[schemars(title = "Text")]
590 Text { text: String },
591 #[schemars(title = "ImageUrl")]
593 ImageUrl { image_url: ImageUrl },
594 #[schemars(title = "InputAudio")]
596 InputAudio { input_audio: InputAudio },
597 #[schemars(title = "InputVideo")]
599 InputVideo { video_url: VideoUrl },
600 #[schemars(title = "VideoUrl")]
602 VideoUrl { video_url: VideoUrl },
603 #[schemars(title = "File")]
605 File { file: File },
606}
607
608impl RichContentPart {
609 pub fn prepare(&mut self) {
611 match self {
612 RichContentPart::Text { .. } => {}
613 RichContentPart::ImageUrl { image_url } => {
614 image_url.prepare();
615 }
616 RichContentPart::InputAudio { .. } => {}
617 RichContentPart::InputVideo { .. } => {}
618 RichContentPart::VideoUrl { .. } => {}
619 RichContentPart::File { file } => {
620 file.prepare();
621 }
622 }
623 }
624
625 pub fn is_empty(&self) -> bool {
627 match self {
628 RichContentPart::Text { text } => text.is_empty(),
629 RichContentPart::ImageUrl { image_url } => image_url.is_empty(),
630 RichContentPart::InputAudio { input_audio } => {
631 input_audio.is_empty()
632 }
633 RichContentPart::InputVideo { video_url } => video_url.is_empty(),
634 RichContentPart::VideoUrl { video_url } => video_url.is_empty(),
635 RichContentPart::File { file } => file.is_empty(),
636 }
637 }
638}
639
640impl ToStarlarkValue for RichContentPart {
641 fn to_starlark_value<'v>(
642 &self,
643 heap: &'v StarlarkHeap,
644 ) -> StarlarkValue<'v> {
645 match self {
646 RichContentPart::Text { text } => heap.alloc(StarlarkAllocDict([
647 ("type", "text".to_starlark_value(heap)),
648 ("text", text.to_starlark_value(heap)),
649 ])),
650 RichContentPart::ImageUrl { image_url } => {
651 heap.alloc(StarlarkAllocDict([
652 ("type", "image_url".to_starlark_value(heap)),
653 ("image_url", image_url.to_starlark_value(heap)),
654 ]))
655 }
656 RichContentPart::InputAudio { input_audio } => {
657 heap.alloc(StarlarkAllocDict([
658 ("type", "input_audio".to_starlark_value(heap)),
659 ("input_audio", input_audio.to_starlark_value(heap)),
660 ]))
661 }
662 RichContentPart::InputVideo { video_url } => {
663 heap.alloc(StarlarkAllocDict([
664 ("type", "input_video".to_starlark_value(heap)),
665 ("video_url", video_url.to_starlark_value(heap)),
666 ]))
667 }
668 RichContentPart::VideoUrl { video_url } => {
669 heap.alloc(StarlarkAllocDict([
670 ("type", "video_url".to_starlark_value(heap)),
671 ("video_url", video_url.to_starlark_value(heap)),
672 ]))
673 }
674 RichContentPart::File { file } => heap.alloc(StarlarkAllocDict([
675 ("type", "file".to_starlark_value(heap)),
676 ("file", file.to_starlark_value(heap)),
677 ])),
678 }
679 }
680}
681
682impl FromStarlarkValue for RichContentPart {
683 fn from_starlark_value(
684 value: &StarlarkValue,
685 ) -> Result<Self, ExpressionError> {
686 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
687 ExpressionError::StarlarkConversionError(
688 "RichContentPart: expected dict".into(),
689 )
690 })?;
691 let mut typ = None;
693 for (k, v) in dict.iter() {
694 if let Ok(Some("type")) = <&str as UnpackValue>::unpack_value(k) {
695 typ = Some(
696 <&str as UnpackValue>::unpack_value(v)
697 .map_err(|e| {
698 ExpressionError::StarlarkConversionError(
699 e.to_string(),
700 )
701 })?
702 .ok_or_else(|| {
703 ExpressionError::StarlarkConversionError(
704 "RichContentPart: expected string type".into(),
705 )
706 })?,
707 );
708 break;
709 }
710 }
711 let typ = typ.ok_or_else(|| {
712 ExpressionError::StarlarkConversionError(
713 "RichContentPart: missing type".into(),
714 )
715 })?;
716 let payload_key = match typ {
718 "text" => "text",
719 "image_url" => "image_url",
720 "input_audio" => "input_audio",
721 "input_video" | "video_url" => "video_url",
722 "file" => "file",
723 _ => {
724 return Err(ExpressionError::StarlarkConversionError(format!(
725 "RichContentPart: unknown type: {}",
726 typ
727 )));
728 }
729 };
730 let mut payload = None;
731 for (k, v) in dict.iter() {
732 if let Ok(Some(key)) = <&str as UnpackValue>::unpack_value(k) {
733 if key == payload_key {
734 payload = Some(v);
735 break;
736 }
737 }
738 }
739 let v = payload.ok_or_else(|| {
740 ExpressionError::StarlarkConversionError(format!(
741 "RichContentPart: missing {}",
742 payload_key
743 ))
744 })?;
745 match typ {
746 "text" => Ok(RichContentPart::Text {
747 text: String::from_starlark_value(&v)?,
748 }),
749 "image_url" => Ok(RichContentPart::ImageUrl {
750 image_url: ImageUrl::from_starlark_value(&v)?,
751 }),
752 "input_audio" => Ok(RichContentPart::InputAudio {
753 input_audio: InputAudio::from_starlark_value(&v)?,
754 }),
755 "input_video" => Ok(RichContentPart::InputVideo {
756 video_url: VideoUrl::from_starlark_value(&v)?,
757 }),
758 "video_url" => Ok(RichContentPart::VideoUrl {
759 video_url: VideoUrl::from_starlark_value(&v)?,
760 }),
761 "file" => Ok(RichContentPart::File {
762 file: File::from_starlark_value(&v)?,
763 }),
764 _ => unreachable!(),
765 }
766 }
767}
768
769#[derive(
771 Debug,
772 Clone,
773 PartialEq,
774 Serialize,
775 Deserialize,
776 JsonSchema,
777 arbitrary::Arbitrary,
778)]
779#[serde(tag = "type", rename_all = "snake_case")]
780#[schemars(rename = "agent.completions.message.RichContentPartExpression")]
781pub enum RichContentPartExpression {
782 #[schemars(title = "Text")]
783 Text {
784 text: functions::expression::WithExpression<String>,
785 },
786 #[schemars(title = "ImageUrl")]
787 ImageUrl {
788 image_url: functions::expression::WithExpression<ImageUrl>,
789 },
790 #[schemars(title = "InputAudio")]
791 InputAudio {
792 input_audio: functions::expression::WithExpression<InputAudio>,
793 },
794 #[schemars(title = "InputVideo")]
795 InputVideo {
796 video_url: functions::expression::WithExpression<VideoUrl>,
797 },
798 #[schemars(title = "VideoUrl")]
799 VideoUrl {
800 video_url: functions::expression::WithExpression<VideoUrl>,
801 },
802 #[schemars(title = "File")]
803 File {
804 file: functions::expression::WithExpression<File>,
805 },
806}
807
808impl RichContentPartExpression {
809 pub fn compile(
811 self,
812 params: &functions::expression::Params,
813 ) -> Result<RichContentPart, functions::expression::ExpressionError> {
814 match self {
815 RichContentPartExpression::Text { text } => {
816 let text = text.compile_one(params)?;
817 Ok(RichContentPart::Text { text })
818 }
819 RichContentPartExpression::ImageUrl { image_url } => {
820 let image_url = image_url.compile_one(params)?;
821 Ok(RichContentPart::ImageUrl { image_url })
822 }
823 RichContentPartExpression::InputAudio { input_audio } => {
824 let input_audio = input_audio.compile_one(params)?;
825 Ok(RichContentPart::InputAudio { input_audio })
826 }
827 RichContentPartExpression::InputVideo { video_url } => {
828 let video_url = video_url.compile_one(params)?;
829 Ok(RichContentPart::InputVideo { video_url })
830 }
831 RichContentPartExpression::VideoUrl { video_url } => {
832 let video_url = video_url.compile_one(params)?;
833 Ok(RichContentPart::VideoUrl { video_url })
834 }
835 RichContentPartExpression::File { file } => {
836 let file = file.compile_one(params)?;
837 Ok(RichContentPart::File { file })
838 }
839 }
840 }
841}
842
843impl From<RichContentPart> for RichContentPartExpression {
844 fn from(part: RichContentPart) -> Self {
845 match part {
846 RichContentPart::Text { text } => RichContentPartExpression::Text {
847 text: WithExpression::Value(text),
848 },
849 RichContentPart::ImageUrl { image_url } => {
850 RichContentPartExpression::ImageUrl {
851 image_url: WithExpression::Value(image_url),
852 }
853 }
854 RichContentPart::InputAudio { input_audio } => {
855 RichContentPartExpression::InputAudio {
856 input_audio: WithExpression::Value(input_audio),
857 }
858 }
859 RichContentPart::InputVideo { video_url } => {
860 RichContentPartExpression::InputVideo {
861 video_url: WithExpression::Value(video_url),
862 }
863 }
864 RichContentPart::VideoUrl { video_url } => {
865 RichContentPartExpression::VideoUrl {
866 video_url: WithExpression::Value(video_url),
867 }
868 }
869 RichContentPart::File { file } => RichContentPartExpression::File {
870 file: WithExpression::Value(file),
871 },
872 }
873 }
874}
875
876impl FromStarlarkValue for RichContentPartExpression {
877 fn from_starlark_value(
878 value: &StarlarkValue,
879 ) -> Result<Self, ExpressionError> {
880 let part = RichContentPart::from_starlark_value(value)?;
881 match part {
882 RichContentPart::Text { text } => {
883 Ok(RichContentPartExpression::Text {
884 text: WithExpression::Value(text),
885 })
886 }
887 RichContentPart::ImageUrl { image_url } => {
888 Ok(RichContentPartExpression::ImageUrl {
889 image_url: WithExpression::Value(image_url),
890 })
891 }
892 RichContentPart::InputAudio { input_audio } => {
893 Ok(RichContentPartExpression::InputAudio {
894 input_audio: WithExpression::Value(input_audio),
895 })
896 }
897 RichContentPart::InputVideo { video_url } => {
898 Ok(RichContentPartExpression::InputVideo {
899 video_url: WithExpression::Value(video_url),
900 })
901 }
902 RichContentPart::VideoUrl { video_url } => {
903 Ok(RichContentPartExpression::VideoUrl {
904 video_url: WithExpression::Value(video_url),
905 })
906 }
907 RichContentPart::File { file } => {
908 Ok(RichContentPartExpression::File {
909 file: WithExpression::Value(file),
910 })
911 }
912 }
913 }
914}
915
916#[derive(
918 Debug,
919 Clone,
920 Hash,
921 PartialEq,
922 Eq,
923 Serialize,
924 Deserialize,
925 JsonSchema,
926 arbitrary::Arbitrary,
927)]
928#[schemars(rename = "agent.completions.message.ImageUrl")]
929pub struct ImageUrl {
930 pub url: String,
932 #[serde(skip_serializing_if = "Option::is_none")]
934 #[schemars(extend("omitempty" = true))]
935 pub detail: Option<ImageUrlDetail>,
936}
937
938impl ImageUrl {
939 pub fn prepare(&mut self) {
941 if matches!(self.detail, Some(ImageUrlDetail::Auto)) {
942 self.detail = None;
943 }
944 }
945
946 pub fn is_empty(&self) -> bool {
948 self.url.is_empty() && self.detail.is_none()
949 }
950
951 pub fn file_content(&self) -> Option<super::FileContent<'_>> {
955 let (mime, payload) = crate::data_url::parse_data_url(&self.url)?;
956 Some(super::FileContent {
957 content: payload,
958 extension: super::file_content::mime_to_ext(mime),
959 })
960 }
961}
962
963#[cfg(feature = "mcp")]
966impl From<crate::mcp::tool::ImageContent> for ImageUrl {
967 fn from(image: crate::mcp::tool::ImageContent) -> Self {
968 Self {
969 url: format!("data:{};base64,{}", image.mime_type, image.data),
970 detail: None,
971 }
972 }
973}
974
975impl ToStarlarkValue for ImageUrl {
976 fn to_starlark_value<'v>(
977 &self,
978 heap: &'v StarlarkHeap,
979 ) -> StarlarkValue<'v> {
980 heap.alloc(StarlarkAllocDict([
981 ("url", self.url.to_starlark_value(heap)),
982 ("detail", self.detail.to_starlark_value(heap)),
983 ]))
984 }
985}
986
987impl FromStarlarkValue for ImageUrl {
988 fn from_starlark_value(
989 value: &StarlarkValue,
990 ) -> Result<Self, ExpressionError> {
991 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
992 ExpressionError::StarlarkConversionError(
993 "ImageUrl: expected dict".into(),
994 )
995 })?;
996 let mut url = None;
997 let mut detail = None;
998 for (k, v) in dict.iter() {
999 let key = <&str as UnpackValue>::unpack_value(k)
1000 .map_err(|e| {
1001 ExpressionError::StarlarkConversionError(e.to_string())
1002 })?
1003 .ok_or_else(|| {
1004 ExpressionError::StarlarkConversionError(
1005 "ImageUrl: expected string key".into(),
1006 )
1007 })?;
1008 match key {
1009 "url" => url = Some(String::from_starlark_value(&v)?),
1010 "detail" => {
1011 detail = Option::<ImageUrlDetail>::from_starlark_value(&v)?
1012 }
1013 _ => {}
1014 }
1015 if url.is_some() && detail.is_some() {
1016 break;
1017 }
1018 }
1019 Ok(ImageUrl {
1020 url: url.ok_or_else(|| {
1021 ExpressionError::StarlarkConversionError(
1022 "ImageUrl: missing url".into(),
1023 )
1024 })?,
1025 detail,
1026 })
1027 }
1028}
1029
1030#[derive(
1032 Debug,
1033 Clone,
1034 Copy,
1035 Hash,
1036 PartialEq,
1037 Eq,
1038 Serialize,
1039 Deserialize,
1040 JsonSchema,
1041 arbitrary::Arbitrary,
1042)]
1043#[schemars(rename = "agent.completions.message.ImageUrlDetail")]
1044pub enum ImageUrlDetail {
1045 #[schemars(title = "Auto")]
1047 #[serde(rename = "auto")]
1048 Auto,
1049 #[schemars(title = "Low")]
1051 #[serde(rename = "low")]
1052 Low,
1053 #[schemars(title = "High")]
1055 #[serde(rename = "high")]
1056 High,
1057}
1058
1059impl ToStarlarkValue for ImageUrlDetail {
1060 fn to_starlark_value<'v>(
1061 &self,
1062 heap: &'v StarlarkHeap,
1063 ) -> StarlarkValue<'v> {
1064 match self {
1065 ImageUrlDetail::Auto => "auto".to_starlark_value(heap),
1066 ImageUrlDetail::Low => "low".to_starlark_value(heap),
1067 ImageUrlDetail::High => "high".to_starlark_value(heap),
1068 }
1069 }
1070}
1071
1072impl FromStarlarkValue for ImageUrlDetail {
1073 fn from_starlark_value(
1074 value: &StarlarkValue,
1075 ) -> Result<Self, ExpressionError> {
1076 let s = <&str as UnpackValue>::unpack_value(*value)
1077 .map_err(|e| {
1078 ExpressionError::StarlarkConversionError(e.to_string())
1079 })?
1080 .ok_or_else(|| {
1081 ExpressionError::StarlarkConversionError(
1082 "ImageUrlDetail: expected string".into(),
1083 )
1084 })?;
1085 match s {
1086 "auto" => Ok(ImageUrlDetail::Auto),
1087 "low" => Ok(ImageUrlDetail::Low),
1088 "high" => Ok(ImageUrlDetail::High),
1089 _ => Err(ExpressionError::StarlarkConversionError(format!(
1090 "ImageUrlDetail: unknown value: {}",
1091 s
1092 ))),
1093 }
1094 }
1095}
1096
1097#[derive(
1099 Debug,
1100 Clone,
1101 Hash,
1102 PartialEq,
1103 Eq,
1104 Serialize,
1105 Deserialize,
1106 JsonSchema,
1107 arbitrary::Arbitrary,
1108)]
1109#[schemars(rename = "agent.completions.message.InputAudio")]
1110pub struct InputAudio {
1111 pub data: String,
1113 pub format: String,
1115}
1116
1117impl InputAudio {
1118 pub fn is_empty(&self) -> bool {
1120 self.data.is_empty() && self.format.is_empty()
1121 }
1122
1123 pub fn file_content(&self) -> Option<super::FileContent<'_>> {
1128 if self.data.is_empty() {
1129 return None;
1130 }
1131 Some(super::FileContent {
1132 content: &self.data,
1133 extension: if self.format.is_empty() {
1134 "bin"
1135 } else {
1136 &self.format
1137 },
1138 })
1139 }
1140}
1141
1142#[cfg(feature = "mcp")]
1144impl From<crate::mcp::tool::AudioContent> for InputAudio {
1145 fn from(audio: crate::mcp::tool::AudioContent) -> Self {
1146 Self {
1147 data: audio.data,
1148 format: audio.mime_type,
1149 }
1150 }
1151}
1152
1153impl ToStarlarkValue for InputAudio {
1154 fn to_starlark_value<'v>(
1155 &self,
1156 heap: &'v StarlarkHeap,
1157 ) -> StarlarkValue<'v> {
1158 heap.alloc(StarlarkAllocDict([
1159 ("data", self.data.to_starlark_value(heap)),
1160 ("format", self.format.to_starlark_value(heap)),
1161 ]))
1162 }
1163}
1164
1165impl FromStarlarkValue for InputAudio {
1166 fn from_starlark_value(
1167 value: &StarlarkValue,
1168 ) -> Result<Self, ExpressionError> {
1169 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
1170 ExpressionError::StarlarkConversionError(
1171 "InputAudio: expected dict".into(),
1172 )
1173 })?;
1174 let mut data = None;
1175 let mut format = None;
1176 for (k, v) in dict.iter() {
1177 let key = <&str as UnpackValue>::unpack_value(k)
1178 .map_err(|e| {
1179 ExpressionError::StarlarkConversionError(e.to_string())
1180 })?
1181 .ok_or_else(|| {
1182 ExpressionError::StarlarkConversionError(
1183 "InputAudio: expected string key".into(),
1184 )
1185 })?;
1186 match key {
1187 "data" => data = Some(String::from_starlark_value(&v)?),
1188 "format" => format = Some(String::from_starlark_value(&v)?),
1189 _ => {}
1190 }
1191 if data.is_some() && format.is_some() {
1192 break;
1193 }
1194 }
1195 Ok(InputAudio {
1196 data: data.unwrap_or_default(),
1197 format: format.unwrap_or_default(),
1198 })
1199 }
1200}
1201
1202#[derive(
1204 Debug,
1205 Clone,
1206 Hash,
1207 PartialEq,
1208 Eq,
1209 Serialize,
1210 Deserialize,
1211 JsonSchema,
1212 arbitrary::Arbitrary,
1213)]
1214#[schemars(rename = "agent.completions.message.VideoUrl")]
1215pub struct VideoUrl {
1216 pub url: String,
1218}
1219
1220impl VideoUrl {
1221 pub fn is_empty(&self) -> bool {
1223 self.url.is_empty()
1224 }
1225
1226 pub fn file_content(&self) -> Option<super::FileContent<'_>> {
1230 let (mime, payload) = crate::data_url::parse_data_url(&self.url)?;
1231 Some(super::FileContent {
1232 content: payload,
1233 extension: super::file_content::mime_to_ext(mime),
1234 })
1235 }
1236}
1237
1238impl ToStarlarkValue for VideoUrl {
1239 fn to_starlark_value<'v>(
1240 &self,
1241 heap: &'v StarlarkHeap,
1242 ) -> StarlarkValue<'v> {
1243 heap.alloc(StarlarkAllocDict([(
1244 "url",
1245 self.url.to_starlark_value(heap),
1246 )]))
1247 }
1248}
1249
1250impl FromStarlarkValue for VideoUrl {
1251 fn from_starlark_value(
1252 value: &StarlarkValue,
1253 ) -> Result<Self, ExpressionError> {
1254 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
1255 ExpressionError::StarlarkConversionError(
1256 "VideoUrl: expected dict".into(),
1257 )
1258 })?;
1259 let mut url = None;
1260 for (k, v) in dict.iter() {
1261 let key = <&str as UnpackValue>::unpack_value(k)
1262 .map_err(|e| {
1263 ExpressionError::StarlarkConversionError(e.to_string())
1264 })?
1265 .ok_or_else(|| {
1266 ExpressionError::StarlarkConversionError(
1267 "VideoUrl: expected string key".into(),
1268 )
1269 })?;
1270 if key == "url" {
1271 url = Some(String::from_starlark_value(&v)?);
1272 }
1273 }
1274 Ok(VideoUrl {
1275 url: url.ok_or_else(|| {
1276 ExpressionError::StarlarkConversionError(
1277 "VideoUrl: missing url".into(),
1278 )
1279 })?,
1280 })
1281 }
1282}
1283
1284#[derive(
1286 Debug,
1287 Clone,
1288 Hash,
1289 PartialEq,
1290 Eq,
1291 Serialize,
1292 Deserialize,
1293 JsonSchema,
1294 arbitrary::Arbitrary,
1295)]
1296#[schemars(rename = "agent.completions.message.File")]
1297pub struct File {
1298 #[serde(skip_serializing_if = "Option::is_none")]
1300 #[schemars(extend("omitempty" = true))]
1301 pub file_data: Option<String>,
1302 #[serde(skip_serializing_if = "Option::is_none")]
1304 #[schemars(extend("omitempty" = true))]
1305 pub file_id: Option<String>,
1306 #[serde(skip_serializing_if = "Option::is_none")]
1308 #[schemars(extend("omitempty" = true))]
1309 pub filename: Option<String>,
1310 #[serde(skip_serializing_if = "Option::is_none")]
1312 #[schemars(extend("omitempty" = true))]
1313 pub file_url: Option<String>,
1314}
1315
1316impl File {
1317 pub fn prepare(&mut self) {
1319 if self.file_data.as_ref().is_some_and(String::is_empty) {
1320 self.file_data = None;
1321 }
1322 if self.file_id.as_ref().is_some_and(String::is_empty) {
1323 self.file_id = None;
1324 }
1325 if self.filename.as_ref().is_some_and(String::is_empty) {
1326 self.filename = None;
1327 }
1328 if self.file_url.as_ref().is_some_and(String::is_empty) {
1329 self.file_url = None;
1330 }
1331 }
1332
1333 pub fn is_empty(&self) -> bool {
1335 self.file_data.is_none()
1336 && self.file_id.is_none()
1337 && self.filename.is_none()
1338 && self.file_url.is_none()
1339 }
1340
1341 pub fn file_content(&self) -> Option<super::FileContent<'_>> {
1345 let data = self.file_data.as_deref()?;
1346 if data.is_empty() {
1347 return None;
1348 }
1349 let ext = self
1350 .filename
1351 .as_deref()
1352 .and_then(|name| name.rsplit_once('.'))
1353 .map(|(_, ext)| ext)
1354 .unwrap_or("bin");
1355 Some(super::FileContent {
1356 content: data,
1357 extension: ext,
1358 })
1359 }
1360}
1361
1362impl ToStarlarkValue for File {
1363 fn to_starlark_value<'v>(
1364 &self,
1365 heap: &'v StarlarkHeap,
1366 ) -> StarlarkValue<'v> {
1367 heap.alloc(StarlarkAllocDict([
1368 ("file_data", self.file_data.to_starlark_value(heap)),
1369 ("file_id", self.file_id.to_starlark_value(heap)),
1370 ("filename", self.filename.to_starlark_value(heap)),
1371 ("file_url", self.file_url.to_starlark_value(heap)),
1372 ]))
1373 }
1374}
1375
1376impl FromStarlarkValue for File {
1377 fn from_starlark_value(
1378 value: &StarlarkValue,
1379 ) -> Result<Self, ExpressionError> {
1380 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
1381 ExpressionError::StarlarkConversionError(
1382 "File: expected dict".into(),
1383 )
1384 })?;
1385 let mut file_data = None;
1386 let mut file_id = None;
1387 let mut filename = None;
1388 let mut file_url = None;
1389 for (k, v) in dict.iter() {
1390 let key = <&str as UnpackValue>::unpack_value(k)
1391 .map_err(|e| {
1392 ExpressionError::StarlarkConversionError(e.to_string())
1393 })?
1394 .ok_or_else(|| {
1395 ExpressionError::StarlarkConversionError(
1396 "File: expected string key".into(),
1397 )
1398 })?;
1399 match key {
1400 "file_data" => {
1401 file_data = Option::<String>::from_starlark_value(&v)?
1402 }
1403 "file_id" => {
1404 file_id = Option::<String>::from_starlark_value(&v)?
1405 }
1406 "filename" => {
1407 filename = Option::<String>::from_starlark_value(&v)?
1408 }
1409 "file_url" => {
1410 file_url = Option::<String>::from_starlark_value(&v)?
1411 }
1412 _ => {}
1413 }
1414 }
1415 Ok(File {
1416 file_data,
1417 file_id,
1418 filename,
1419 file_url,
1420 })
1421 }
1422}
1423
1424crate::functions::expression::impl_from_special_unsupported!(
1425 RichContentExpression,
1426 RichContentPartExpression,
1427 ImageUrl,
1428 InputAudio,
1429 VideoUrl,
1430 File,
1431);
1432
1433impl crate::functions::expression::FromSpecial
1434 for Vec<crate::functions::expression::WithExpression<RichContentExpression>>
1435{
1436 fn from_special(
1437 _special: &crate::functions::expression::Special,
1438 _params: &crate::functions::expression::Params,
1439 ) -> Result<Self, crate::functions::expression::ExpressionError> {
1440 Err(crate::functions::expression::ExpressionError::UnsupportedSpecial)
1441 }
1442}