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")]
488impl From<Vec<crate::mcp::tool::ContentBlock>> for RichContent {
489 fn from(blocks: Vec<crate::mcp::tool::ContentBlock>) -> Self {
490 let parts: Vec<RichContentPart> =
491 blocks.into_iter().map(Into::into).collect();
492 RichContent::from(parts)
493 }
494}
495
496#[derive(
498 Debug,
499 Clone,
500 PartialEq,
501 Serialize,
502 Deserialize,
503 JsonSchema,
504 arbitrary::Arbitrary,
505)]
506#[serde(untagged)]
507#[schemars(rename = "agent.completions.message.RichContentExpression")]
508pub enum RichContentExpression {
509 #[schemars(title = "Text")]
511 Text(String),
512 #[schemars(title = "Parts")]
514 Parts(
515 Vec<functions::expression::WithExpression<RichContentPartExpression>>,
516 ),
517}
518
519impl RichContentExpression {
520 pub fn compile(
522 self,
523 params: &functions::expression::Params,
524 ) -> Result<RichContent, functions::expression::ExpressionError> {
525 match self {
526 RichContentExpression::Text(text) => Ok(RichContent::Text(text)),
527 RichContentExpression::Parts(parts) => {
528 let mut compiled_parts = Vec::with_capacity(parts.len());
529 for part in parts {
530 match part.compile_one_or_many(params)? {
531 functions::expression::OneOrMany::One(one_part) => {
532 compiled_parts.push(one_part.compile(params)?);
533 }
534 functions::expression::OneOrMany::Many(many_parts) => {
535 for part in many_parts {
536 compiled_parts.push(part.compile(params)?);
537 }
538 }
539 }
540 }
541 Ok(RichContent::Parts(compiled_parts))
542 }
543 }
544 }
545}
546
547impl From<RichContent> for RichContentExpression {
548 fn from(content: RichContent) -> Self {
549 match content {
550 RichContent::Text(text) => RichContentExpression::Text(text),
551 RichContent::Parts(parts) => RichContentExpression::Parts(
552 parts
553 .into_iter()
554 .map(RichContentPartExpression::from)
555 .map(WithExpression::Value)
556 .collect(),
557 ),
558 }
559 }
560}
561
562impl FromStarlarkValue for RichContentExpression {
563 fn from_starlark_value(
564 value: &StarlarkValue,
565 ) -> Result<Self, ExpressionError> {
566 if let Ok(Some(s)) = <&str as UnpackValue>::unpack_value(*value) {
567 return Ok(RichContentExpression::Text(s.to_owned()));
568 }
569 let parts = Vec::<WithExpression<RichContentPartExpression>>::from_starlark_value(value)?;
570 Ok(RichContentExpression::Parts(parts))
571 }
572}
573
574#[derive(
576 Debug,
577 Clone,
578 Hash,
579 PartialEq,
580 Eq,
581 Serialize,
582 Deserialize,
583 JsonSchema,
584 arbitrary::Arbitrary,
585)]
586#[serde(tag = "type", rename_all = "snake_case")]
587#[schemars(rename = "agent.completions.message.RichContentPart")]
588pub enum RichContentPart {
589 #[schemars(title = "Text")]
591 Text { text: String },
592 #[schemars(title = "ImageUrl")]
594 ImageUrl { image_url: ImageUrl },
595 #[schemars(title = "InputAudio")]
597 InputAudio { input_audio: InputAudio },
598 #[schemars(title = "InputVideo")]
600 InputVideo { video_url: VideoUrl },
601 #[schemars(title = "VideoUrl")]
603 VideoUrl { video_url: VideoUrl },
604 #[schemars(title = "File")]
606 File { file: File },
607}
608
609impl RichContentPart {
610 pub fn prepare(&mut self) {
612 match self {
613 RichContentPart::Text { .. } => {}
614 RichContentPart::ImageUrl { image_url } => {
615 image_url.prepare();
616 }
617 RichContentPart::InputAudio { .. } => {}
618 RichContentPart::InputVideo { .. } => {}
619 RichContentPart::VideoUrl { .. } => {}
620 RichContentPart::File { file } => {
621 file.prepare();
622 }
623 }
624 }
625
626 pub fn is_empty(&self) -> bool {
628 match self {
629 RichContentPart::Text { text } => text.is_empty(),
630 RichContentPart::ImageUrl { image_url } => image_url.is_empty(),
631 RichContentPart::InputAudio { input_audio } => {
632 input_audio.is_empty()
633 }
634 RichContentPart::InputVideo { video_url } => video_url.is_empty(),
635 RichContentPart::VideoUrl { video_url } => video_url.is_empty(),
636 RichContentPart::File { file } => file.is_empty(),
637 }
638 }
639}
640
641impl ToStarlarkValue for RichContentPart {
642 fn to_starlark_value<'v>(
643 &self,
644 heap: &'v StarlarkHeap,
645 ) -> StarlarkValue<'v> {
646 match self {
647 RichContentPart::Text { text } => heap.alloc(StarlarkAllocDict([
648 ("type", "text".to_starlark_value(heap)),
649 ("text", text.to_starlark_value(heap)),
650 ])),
651 RichContentPart::ImageUrl { image_url } => {
652 heap.alloc(StarlarkAllocDict([
653 ("type", "image_url".to_starlark_value(heap)),
654 ("image_url", image_url.to_starlark_value(heap)),
655 ]))
656 }
657 RichContentPart::InputAudio { input_audio } => {
658 heap.alloc(StarlarkAllocDict([
659 ("type", "input_audio".to_starlark_value(heap)),
660 ("input_audio", input_audio.to_starlark_value(heap)),
661 ]))
662 }
663 RichContentPart::InputVideo { video_url } => {
664 heap.alloc(StarlarkAllocDict([
665 ("type", "input_video".to_starlark_value(heap)),
666 ("video_url", video_url.to_starlark_value(heap)),
667 ]))
668 }
669 RichContentPart::VideoUrl { video_url } => {
670 heap.alloc(StarlarkAllocDict([
671 ("type", "video_url".to_starlark_value(heap)),
672 ("video_url", video_url.to_starlark_value(heap)),
673 ]))
674 }
675 RichContentPart::File { file } => heap.alloc(StarlarkAllocDict([
676 ("type", "file".to_starlark_value(heap)),
677 ("file", file.to_starlark_value(heap)),
678 ])),
679 }
680 }
681}
682
683impl FromStarlarkValue for RichContentPart {
684 fn from_starlark_value(
685 value: &StarlarkValue,
686 ) -> Result<Self, ExpressionError> {
687 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
688 ExpressionError::StarlarkConversionError(
689 "RichContentPart: expected dict".into(),
690 )
691 })?;
692 let mut typ = None;
694 for (k, v) in dict.iter() {
695 if let Ok(Some("type")) = <&str as UnpackValue>::unpack_value(k) {
696 typ = Some(
697 <&str as UnpackValue>::unpack_value(v)
698 .map_err(|e| {
699 ExpressionError::StarlarkConversionError(
700 e.to_string(),
701 )
702 })?
703 .ok_or_else(|| {
704 ExpressionError::StarlarkConversionError(
705 "RichContentPart: expected string type".into(),
706 )
707 })?,
708 );
709 break;
710 }
711 }
712 let typ = typ.ok_or_else(|| {
713 ExpressionError::StarlarkConversionError(
714 "RichContentPart: missing type".into(),
715 )
716 })?;
717 let payload_key = match typ {
719 "text" => "text",
720 "image_url" => "image_url",
721 "input_audio" => "input_audio",
722 "input_video" | "video_url" => "video_url",
723 "file" => "file",
724 _ => {
725 return Err(ExpressionError::StarlarkConversionError(format!(
726 "RichContentPart: unknown type: {}",
727 typ
728 )));
729 }
730 };
731 let mut payload = None;
732 for (k, v) in dict.iter() {
733 if let Ok(Some(key)) = <&str as UnpackValue>::unpack_value(k) {
734 if key == payload_key {
735 payload = Some(v);
736 break;
737 }
738 }
739 }
740 let v = payload.ok_or_else(|| {
741 ExpressionError::StarlarkConversionError(format!(
742 "RichContentPart: missing {}",
743 payload_key
744 ))
745 })?;
746 match typ {
747 "text" => Ok(RichContentPart::Text {
748 text: String::from_starlark_value(&v)?,
749 }),
750 "image_url" => Ok(RichContentPart::ImageUrl {
751 image_url: ImageUrl::from_starlark_value(&v)?,
752 }),
753 "input_audio" => Ok(RichContentPart::InputAudio {
754 input_audio: InputAudio::from_starlark_value(&v)?,
755 }),
756 "input_video" => Ok(RichContentPart::InputVideo {
757 video_url: VideoUrl::from_starlark_value(&v)?,
758 }),
759 "video_url" => Ok(RichContentPart::VideoUrl {
760 video_url: VideoUrl::from_starlark_value(&v)?,
761 }),
762 "file" => Ok(RichContentPart::File {
763 file: File::from_starlark_value(&v)?,
764 }),
765 _ => unreachable!(),
766 }
767 }
768}
769
770#[derive(
772 Debug,
773 Clone,
774 PartialEq,
775 Serialize,
776 Deserialize,
777 JsonSchema,
778 arbitrary::Arbitrary,
779)]
780#[serde(tag = "type", rename_all = "snake_case")]
781#[schemars(rename = "agent.completions.message.RichContentPartExpression")]
782pub enum RichContentPartExpression {
783 #[schemars(title = "Text")]
784 Text {
785 text: functions::expression::WithExpression<String>,
786 },
787 #[schemars(title = "ImageUrl")]
788 ImageUrl {
789 image_url: functions::expression::WithExpression<ImageUrl>,
790 },
791 #[schemars(title = "InputAudio")]
792 InputAudio {
793 input_audio: functions::expression::WithExpression<InputAudio>,
794 },
795 #[schemars(title = "InputVideo")]
796 InputVideo {
797 video_url: functions::expression::WithExpression<VideoUrl>,
798 },
799 #[schemars(title = "VideoUrl")]
800 VideoUrl {
801 video_url: functions::expression::WithExpression<VideoUrl>,
802 },
803 #[schemars(title = "File")]
804 File {
805 file: functions::expression::WithExpression<File>,
806 },
807}
808
809impl RichContentPartExpression {
810 pub fn compile(
812 self,
813 params: &functions::expression::Params,
814 ) -> Result<RichContentPart, functions::expression::ExpressionError> {
815 match self {
816 RichContentPartExpression::Text { text } => {
817 let text = text.compile_one(params)?;
818 Ok(RichContentPart::Text { text })
819 }
820 RichContentPartExpression::ImageUrl { image_url } => {
821 let image_url = image_url.compile_one(params)?;
822 Ok(RichContentPart::ImageUrl { image_url })
823 }
824 RichContentPartExpression::InputAudio { input_audio } => {
825 let input_audio = input_audio.compile_one(params)?;
826 Ok(RichContentPart::InputAudio { input_audio })
827 }
828 RichContentPartExpression::InputVideo { video_url } => {
829 let video_url = video_url.compile_one(params)?;
830 Ok(RichContentPart::InputVideo { video_url })
831 }
832 RichContentPartExpression::VideoUrl { video_url } => {
833 let video_url = video_url.compile_one(params)?;
834 Ok(RichContentPart::VideoUrl { video_url })
835 }
836 RichContentPartExpression::File { file } => {
837 let file = file.compile_one(params)?;
838 Ok(RichContentPart::File { file })
839 }
840 }
841 }
842}
843
844impl From<RichContentPart> for RichContentPartExpression {
845 fn from(part: RichContentPart) -> Self {
846 match part {
847 RichContentPart::Text { text } => RichContentPartExpression::Text {
848 text: WithExpression::Value(text),
849 },
850 RichContentPart::ImageUrl { image_url } => {
851 RichContentPartExpression::ImageUrl {
852 image_url: WithExpression::Value(image_url),
853 }
854 }
855 RichContentPart::InputAudio { input_audio } => {
856 RichContentPartExpression::InputAudio {
857 input_audio: WithExpression::Value(input_audio),
858 }
859 }
860 RichContentPart::InputVideo { video_url } => {
861 RichContentPartExpression::InputVideo {
862 video_url: WithExpression::Value(video_url),
863 }
864 }
865 RichContentPart::VideoUrl { video_url } => {
866 RichContentPartExpression::VideoUrl {
867 video_url: WithExpression::Value(video_url),
868 }
869 }
870 RichContentPart::File { file } => RichContentPartExpression::File {
871 file: WithExpression::Value(file),
872 },
873 }
874 }
875}
876
877impl FromStarlarkValue for RichContentPartExpression {
878 fn from_starlark_value(
879 value: &StarlarkValue,
880 ) -> Result<Self, ExpressionError> {
881 let part = RichContentPart::from_starlark_value(value)?;
882 match part {
883 RichContentPart::Text { text } => {
884 Ok(RichContentPartExpression::Text {
885 text: WithExpression::Value(text),
886 })
887 }
888 RichContentPart::ImageUrl { image_url } => {
889 Ok(RichContentPartExpression::ImageUrl {
890 image_url: WithExpression::Value(image_url),
891 })
892 }
893 RichContentPart::InputAudio { input_audio } => {
894 Ok(RichContentPartExpression::InputAudio {
895 input_audio: WithExpression::Value(input_audio),
896 })
897 }
898 RichContentPart::InputVideo { video_url } => {
899 Ok(RichContentPartExpression::InputVideo {
900 video_url: WithExpression::Value(video_url),
901 })
902 }
903 RichContentPart::VideoUrl { video_url } => {
904 Ok(RichContentPartExpression::VideoUrl {
905 video_url: WithExpression::Value(video_url),
906 })
907 }
908 RichContentPart::File { file } => {
909 Ok(RichContentPartExpression::File {
910 file: WithExpression::Value(file),
911 })
912 }
913 }
914 }
915}
916
917#[derive(
919 Debug,
920 Clone,
921 Hash,
922 PartialEq,
923 Eq,
924 Serialize,
925 Deserialize,
926 JsonSchema,
927 arbitrary::Arbitrary,
928)]
929#[schemars(rename = "agent.completions.message.ImageUrl")]
930pub struct ImageUrl {
931 pub url: String,
933 #[serde(skip_serializing_if = "Option::is_none")]
935 #[schemars(extend("omitempty" = true))]
936 pub detail: Option<ImageUrlDetail>,
937}
938
939impl ImageUrl {
940 pub fn prepare(&mut self) {
942 if matches!(self.detail, Some(ImageUrlDetail::Auto)) {
943 self.detail = None;
944 }
945 }
946
947 pub fn is_empty(&self) -> bool {
949 self.url.is_empty() && self.detail.is_none()
950 }
951
952 pub fn file_content(&self) -> Option<super::FileContent<'_>> {
956 let (mime, payload) = crate::data_url::parse_data_url(&self.url)?;
957 Some(super::FileContent {
958 content: payload,
959 extension: super::file_content::mime_to_ext(mime),
960 })
961 }
962}
963
964#[cfg(feature = "mcp")]
967impl From<crate::mcp::tool::ImageContent> for ImageUrl {
968 fn from(image: crate::mcp::tool::ImageContent) -> Self {
969 Self {
970 url: format!("data:{};base64,{}", image.mime_type, image.data),
971 detail: None,
972 }
973 }
974}
975
976impl ToStarlarkValue for ImageUrl {
977 fn to_starlark_value<'v>(
978 &self,
979 heap: &'v StarlarkHeap,
980 ) -> StarlarkValue<'v> {
981 heap.alloc(StarlarkAllocDict([
982 ("url", self.url.to_starlark_value(heap)),
983 ("detail", self.detail.to_starlark_value(heap)),
984 ]))
985 }
986}
987
988impl FromStarlarkValue for ImageUrl {
989 fn from_starlark_value(
990 value: &StarlarkValue,
991 ) -> Result<Self, ExpressionError> {
992 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
993 ExpressionError::StarlarkConversionError(
994 "ImageUrl: expected dict".into(),
995 )
996 })?;
997 let mut url = None;
998 let mut detail = None;
999 for (k, v) in dict.iter() {
1000 let key = <&str as UnpackValue>::unpack_value(k)
1001 .map_err(|e| {
1002 ExpressionError::StarlarkConversionError(e.to_string())
1003 })?
1004 .ok_or_else(|| {
1005 ExpressionError::StarlarkConversionError(
1006 "ImageUrl: expected string key".into(),
1007 )
1008 })?;
1009 match key {
1010 "url" => url = Some(String::from_starlark_value(&v)?),
1011 "detail" => {
1012 detail = Option::<ImageUrlDetail>::from_starlark_value(&v)?
1013 }
1014 _ => {}
1015 }
1016 if url.is_some() && detail.is_some() {
1017 break;
1018 }
1019 }
1020 Ok(ImageUrl {
1021 url: url.ok_or_else(|| {
1022 ExpressionError::StarlarkConversionError(
1023 "ImageUrl: missing url".into(),
1024 )
1025 })?,
1026 detail,
1027 })
1028 }
1029}
1030
1031#[derive(
1033 Debug,
1034 Clone,
1035 Copy,
1036 Hash,
1037 PartialEq,
1038 Eq,
1039 Serialize,
1040 Deserialize,
1041 JsonSchema,
1042 arbitrary::Arbitrary,
1043)]
1044#[schemars(rename = "agent.completions.message.ImageUrlDetail")]
1045pub enum ImageUrlDetail {
1046 #[schemars(title = "Auto")]
1048 #[serde(rename = "auto")]
1049 Auto,
1050 #[schemars(title = "Low")]
1052 #[serde(rename = "low")]
1053 Low,
1054 #[schemars(title = "High")]
1056 #[serde(rename = "high")]
1057 High,
1058}
1059
1060impl ToStarlarkValue for ImageUrlDetail {
1061 fn to_starlark_value<'v>(
1062 &self,
1063 heap: &'v StarlarkHeap,
1064 ) -> StarlarkValue<'v> {
1065 match self {
1066 ImageUrlDetail::Auto => "auto".to_starlark_value(heap),
1067 ImageUrlDetail::Low => "low".to_starlark_value(heap),
1068 ImageUrlDetail::High => "high".to_starlark_value(heap),
1069 }
1070 }
1071}
1072
1073impl FromStarlarkValue for ImageUrlDetail {
1074 fn from_starlark_value(
1075 value: &StarlarkValue,
1076 ) -> Result<Self, ExpressionError> {
1077 let s = <&str as UnpackValue>::unpack_value(*value)
1078 .map_err(|e| {
1079 ExpressionError::StarlarkConversionError(e.to_string())
1080 })?
1081 .ok_or_else(|| {
1082 ExpressionError::StarlarkConversionError(
1083 "ImageUrlDetail: expected string".into(),
1084 )
1085 })?;
1086 match s {
1087 "auto" => Ok(ImageUrlDetail::Auto),
1088 "low" => Ok(ImageUrlDetail::Low),
1089 "high" => Ok(ImageUrlDetail::High),
1090 _ => Err(ExpressionError::StarlarkConversionError(format!(
1091 "ImageUrlDetail: unknown value: {}",
1092 s
1093 ))),
1094 }
1095 }
1096}
1097
1098#[derive(
1100 Debug,
1101 Clone,
1102 Hash,
1103 PartialEq,
1104 Eq,
1105 Serialize,
1106 Deserialize,
1107 JsonSchema,
1108 arbitrary::Arbitrary,
1109)]
1110#[schemars(rename = "agent.completions.message.InputAudio")]
1111pub struct InputAudio {
1112 pub data: String,
1114 pub format: String,
1116}
1117
1118impl InputAudio {
1119 pub fn is_empty(&self) -> bool {
1121 self.data.is_empty() && self.format.is_empty()
1122 }
1123
1124 pub fn file_content(&self) -> Option<super::FileContent<'_>> {
1129 if self.data.is_empty() {
1130 return None;
1131 }
1132 Some(super::FileContent {
1133 content: &self.data,
1134 extension: if self.format.is_empty() {
1135 "bin"
1136 } else {
1137 &self.format
1138 },
1139 })
1140 }
1141}
1142
1143#[cfg(feature = "mcp")]
1145impl From<crate::mcp::tool::AudioContent> for InputAudio {
1146 fn from(audio: crate::mcp::tool::AudioContent) -> Self {
1147 Self {
1148 data: audio.data,
1149 format: audio.mime_type,
1150 }
1151 }
1152}
1153
1154impl ToStarlarkValue for InputAudio {
1155 fn to_starlark_value<'v>(
1156 &self,
1157 heap: &'v StarlarkHeap,
1158 ) -> StarlarkValue<'v> {
1159 heap.alloc(StarlarkAllocDict([
1160 ("data", self.data.to_starlark_value(heap)),
1161 ("format", self.format.to_starlark_value(heap)),
1162 ]))
1163 }
1164}
1165
1166impl FromStarlarkValue for InputAudio {
1167 fn from_starlark_value(
1168 value: &StarlarkValue,
1169 ) -> Result<Self, ExpressionError> {
1170 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
1171 ExpressionError::StarlarkConversionError(
1172 "InputAudio: expected dict".into(),
1173 )
1174 })?;
1175 let mut data = None;
1176 let mut format = None;
1177 for (k, v) in dict.iter() {
1178 let key = <&str as UnpackValue>::unpack_value(k)
1179 .map_err(|e| {
1180 ExpressionError::StarlarkConversionError(e.to_string())
1181 })?
1182 .ok_or_else(|| {
1183 ExpressionError::StarlarkConversionError(
1184 "InputAudio: expected string key".into(),
1185 )
1186 })?;
1187 match key {
1188 "data" => data = Some(String::from_starlark_value(&v)?),
1189 "format" => format = Some(String::from_starlark_value(&v)?),
1190 _ => {}
1191 }
1192 if data.is_some() && format.is_some() {
1193 break;
1194 }
1195 }
1196 Ok(InputAudio {
1197 data: data.unwrap_or_default(),
1198 format: format.unwrap_or_default(),
1199 })
1200 }
1201}
1202
1203#[derive(
1205 Debug,
1206 Clone,
1207 Hash,
1208 PartialEq,
1209 Eq,
1210 Serialize,
1211 Deserialize,
1212 JsonSchema,
1213 arbitrary::Arbitrary,
1214)]
1215#[schemars(rename = "agent.completions.message.VideoUrl")]
1216pub struct VideoUrl {
1217 pub url: String,
1219}
1220
1221impl VideoUrl {
1222 pub fn is_empty(&self) -> bool {
1224 self.url.is_empty()
1225 }
1226
1227 pub fn file_content(&self) -> Option<super::FileContent<'_>> {
1231 let (mime, payload) = crate::data_url::parse_data_url(&self.url)?;
1232 Some(super::FileContent {
1233 content: payload,
1234 extension: super::file_content::mime_to_ext(mime),
1235 })
1236 }
1237}
1238
1239impl ToStarlarkValue for VideoUrl {
1240 fn to_starlark_value<'v>(
1241 &self,
1242 heap: &'v StarlarkHeap,
1243 ) -> StarlarkValue<'v> {
1244 heap.alloc(StarlarkAllocDict([(
1245 "url",
1246 self.url.to_starlark_value(heap),
1247 )]))
1248 }
1249}
1250
1251impl FromStarlarkValue for VideoUrl {
1252 fn from_starlark_value(
1253 value: &StarlarkValue,
1254 ) -> Result<Self, ExpressionError> {
1255 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
1256 ExpressionError::StarlarkConversionError(
1257 "VideoUrl: expected dict".into(),
1258 )
1259 })?;
1260 let mut url = None;
1261 for (k, v) in dict.iter() {
1262 let key = <&str as UnpackValue>::unpack_value(k)
1263 .map_err(|e| {
1264 ExpressionError::StarlarkConversionError(e.to_string())
1265 })?
1266 .ok_or_else(|| {
1267 ExpressionError::StarlarkConversionError(
1268 "VideoUrl: expected string key".into(),
1269 )
1270 })?;
1271 if key == "url" {
1272 url = Some(String::from_starlark_value(&v)?);
1273 }
1274 }
1275 Ok(VideoUrl {
1276 url: url.ok_or_else(|| {
1277 ExpressionError::StarlarkConversionError(
1278 "VideoUrl: missing url".into(),
1279 )
1280 })?,
1281 })
1282 }
1283}
1284
1285#[derive(
1287 Debug,
1288 Clone,
1289 Hash,
1290 PartialEq,
1291 Eq,
1292 Serialize,
1293 Deserialize,
1294 JsonSchema,
1295 arbitrary::Arbitrary,
1296)]
1297#[schemars(rename = "agent.completions.message.File")]
1298pub struct File {
1299 #[serde(skip_serializing_if = "Option::is_none")]
1301 #[schemars(extend("omitempty" = true))]
1302 pub file_data: Option<String>,
1303 #[serde(skip_serializing_if = "Option::is_none")]
1305 #[schemars(extend("omitempty" = true))]
1306 pub file_id: Option<String>,
1307 #[serde(skip_serializing_if = "Option::is_none")]
1309 #[schemars(extend("omitempty" = true))]
1310 pub filename: Option<String>,
1311 #[serde(skip_serializing_if = "Option::is_none")]
1313 #[schemars(extend("omitempty" = true))]
1314 pub file_url: Option<String>,
1315}
1316
1317impl File {
1318 pub fn prepare(&mut self) {
1320 if self.file_data.as_ref().is_some_and(String::is_empty) {
1321 self.file_data = None;
1322 }
1323 if self.file_id.as_ref().is_some_and(String::is_empty) {
1324 self.file_id = None;
1325 }
1326 if self.filename.as_ref().is_some_and(String::is_empty) {
1327 self.filename = None;
1328 }
1329 if self.file_url.as_ref().is_some_and(String::is_empty) {
1330 self.file_url = None;
1331 }
1332 }
1333
1334 pub fn is_empty(&self) -> bool {
1336 self.file_data.is_none()
1337 && self.file_id.is_none()
1338 && self.filename.is_none()
1339 && self.file_url.is_none()
1340 }
1341
1342 pub fn file_content(&self) -> Option<super::FileContent<'_>> {
1346 let data = self.file_data.as_deref()?;
1347 if data.is_empty() {
1348 return None;
1349 }
1350 let ext = self
1351 .filename
1352 .as_deref()
1353 .and_then(|name| name.rsplit_once('.'))
1354 .map(|(_, ext)| ext)
1355 .unwrap_or("bin");
1356 Some(super::FileContent {
1357 content: data,
1358 extension: ext,
1359 })
1360 }
1361}
1362
1363impl ToStarlarkValue for File {
1364 fn to_starlark_value<'v>(
1365 &self,
1366 heap: &'v StarlarkHeap,
1367 ) -> StarlarkValue<'v> {
1368 heap.alloc(StarlarkAllocDict([
1369 ("file_data", self.file_data.to_starlark_value(heap)),
1370 ("file_id", self.file_id.to_starlark_value(heap)),
1371 ("filename", self.filename.to_starlark_value(heap)),
1372 ("file_url", self.file_url.to_starlark_value(heap)),
1373 ]))
1374 }
1375}
1376
1377impl FromStarlarkValue for File {
1378 fn from_starlark_value(
1379 value: &StarlarkValue,
1380 ) -> Result<Self, ExpressionError> {
1381 let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
1382 ExpressionError::StarlarkConversionError(
1383 "File: expected dict".into(),
1384 )
1385 })?;
1386 let mut file_data = None;
1387 let mut file_id = None;
1388 let mut filename = None;
1389 let mut file_url = None;
1390 for (k, v) in dict.iter() {
1391 let key = <&str as UnpackValue>::unpack_value(k)
1392 .map_err(|e| {
1393 ExpressionError::StarlarkConversionError(e.to_string())
1394 })?
1395 .ok_or_else(|| {
1396 ExpressionError::StarlarkConversionError(
1397 "File: expected string key".into(),
1398 )
1399 })?;
1400 match key {
1401 "file_data" => {
1402 file_data = Option::<String>::from_starlark_value(&v)?
1403 }
1404 "file_id" => {
1405 file_id = Option::<String>::from_starlark_value(&v)?
1406 }
1407 "filename" => {
1408 filename = Option::<String>::from_starlark_value(&v)?
1409 }
1410 "file_url" => {
1411 file_url = Option::<String>::from_starlark_value(&v)?
1412 }
1413 _ => {}
1414 }
1415 }
1416 Ok(File {
1417 file_data,
1418 file_id,
1419 filename,
1420 file_url,
1421 })
1422 }
1423}
1424
1425crate::functions::expression::impl_from_special_unsupported!(
1426 RichContentExpression,
1427 RichContentPartExpression,
1428 ImageUrl,
1429 InputAudio,
1430 VideoUrl,
1431 File,
1432);
1433
1434impl crate::functions::expression::FromSpecial
1435 for Vec<crate::functions::expression::WithExpression<RichContentExpression>>
1436{
1437 fn from_special(
1438 _special: &crate::functions::expression::Special,
1439 _params: &crate::functions::expression::Params,
1440 ) -> Result<Self, crate::functions::expression::ExpressionError> {
1441 Err(crate::functions::expression::ExpressionError::UnsupportedSpecial)
1442 }
1443}