1use crate::detail::ffi::*;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum ItemType {
23 Bytes,
25 Tensor,
27 Text,
29 Message,
31 Image,
33 Audio,
35 SpeechSegment,
37 SpeechResult,
39 ToolCall,
41 ToolResult,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
47pub enum TextKind {
48 #[default]
50 Default,
51 Reasoning,
53 OpenAiJson,
56}
57
58impl TextKind {
59 pub(crate) fn to_native(self) -> flTextItemType {
60 match self {
61 TextKind::Default => FOUNDRY_LOCAL_TEXT_ITEM_TYPE_DEFAULT,
62 TextKind::Reasoning => FOUNDRY_LOCAL_TEXT_ITEM_TYPE_REASONING,
63 TextKind::OpenAiJson => FOUNDRY_LOCAL_TEXT_ITEM_TYPE_OPENAI_JSON,
64 }
65 }
66
67 pub(crate) fn from_native(value: flTextItemType) -> TextKind {
68 match value {
69 FOUNDRY_LOCAL_TEXT_ITEM_TYPE_REASONING => TextKind::Reasoning,
70 FOUNDRY_LOCAL_TEXT_ITEM_TYPE_OPENAI_JSON => TextKind::OpenAiJson,
71 _ => TextKind::Default,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
78pub enum MessageRole {
79 #[default]
81 None,
82 System,
84 User,
86 Assistant,
88 Tool,
90 Developer,
92}
93
94impl MessageRole {
95 pub(crate) fn to_native(self) -> flMessageRole {
96 match self {
97 MessageRole::None => FOUNDRY_LOCAL_ROLE_NONE,
98 MessageRole::System => FOUNDRY_LOCAL_ROLE_SYSTEM,
99 MessageRole::User => FOUNDRY_LOCAL_ROLE_USER,
100 MessageRole::Assistant => FOUNDRY_LOCAL_ROLE_ASSISTANT,
101 MessageRole::Tool => FOUNDRY_LOCAL_ROLE_TOOL,
102 MessageRole::Developer => FOUNDRY_LOCAL_ROLE_DEVELOPER,
103 }
104 }
105
106 pub(crate) fn from_native(value: flMessageRole) -> MessageRole {
107 match value {
108 FOUNDRY_LOCAL_ROLE_SYSTEM => MessageRole::System,
109 FOUNDRY_LOCAL_ROLE_USER => MessageRole::User,
110 FOUNDRY_LOCAL_ROLE_ASSISTANT => MessageRole::Assistant,
111 FOUNDRY_LOCAL_ROLE_TOOL => MessageRole::Tool,
112 FOUNDRY_LOCAL_ROLE_DEVELOPER => MessageRole::Developer,
113 _ => MessageRole::None,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
120#[allow(missing_docs)]
121pub enum TensorDataType {
122 #[default]
123 Undefined,
124 Float,
125 Uint8,
126 Int8,
127 Uint16,
128 Int16,
129 Int32,
130 Int64,
131 String,
132 Bool,
133 Float16,
134 Double,
135 Uint32,
136 Uint64,
137 Complex64,
138 Complex128,
139 BFloat16,
140 Float8E4M3FN,
141 Float8E4M3FNUZ,
142 Float8E5M2,
143 Float8E5M2FNUZ,
144 Uint4,
145 Int4,
146 Float4E2M1,
147 Float8E8M0,
148}
149
150impl TensorDataType {
151 pub(crate) fn to_native(self) -> flTensorDataType {
152 match self {
153 TensorDataType::Undefined => FOUNDRY_LOCAL_TENSOR_UNDEFINED,
154 TensorDataType::Float => FOUNDRY_LOCAL_TENSOR_FLOAT,
155 TensorDataType::Uint8 => FOUNDRY_LOCAL_TENSOR_UINT8,
156 TensorDataType::Int8 => FOUNDRY_LOCAL_TENSOR_INT8,
157 TensorDataType::Uint16 => FOUNDRY_LOCAL_TENSOR_UINT16,
158 TensorDataType::Int16 => FOUNDRY_LOCAL_TENSOR_INT16,
159 TensorDataType::Int32 => FOUNDRY_LOCAL_TENSOR_INT32,
160 TensorDataType::Int64 => FOUNDRY_LOCAL_TENSOR_INT64,
161 TensorDataType::String => FOUNDRY_LOCAL_TENSOR_STRING,
162 TensorDataType::Bool => FOUNDRY_LOCAL_TENSOR_BOOL,
163 TensorDataType::Float16 => FOUNDRY_LOCAL_TENSOR_FLOAT16,
164 TensorDataType::Double => FOUNDRY_LOCAL_TENSOR_DOUBLE,
165 TensorDataType::Uint32 => FOUNDRY_LOCAL_TENSOR_UINT32,
166 TensorDataType::Uint64 => FOUNDRY_LOCAL_TENSOR_UINT64,
167 TensorDataType::Complex64 => FOUNDRY_LOCAL_TENSOR_COMPLEX64,
168 TensorDataType::Complex128 => FOUNDRY_LOCAL_TENSOR_COMPLEX128,
169 TensorDataType::BFloat16 => FOUNDRY_LOCAL_TENSOR_BFLOAT16,
170 TensorDataType::Float8E4M3FN => FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FN,
171 TensorDataType::Float8E4M3FNUZ => FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FNUZ,
172 TensorDataType::Float8E5M2 => FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2,
173 TensorDataType::Float8E5M2FNUZ => FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2FNUZ,
174 TensorDataType::Uint4 => FOUNDRY_LOCAL_TENSOR_UINT4,
175 TensorDataType::Int4 => FOUNDRY_LOCAL_TENSOR_INT4,
176 TensorDataType::Float4E2M1 => FOUNDRY_LOCAL_TENSOR_FLOAT4E2M1,
177 TensorDataType::Float8E8M0 => FOUNDRY_LOCAL_TENSOR_FLOAT8E8M0,
178 }
179 }
180
181 pub(crate) fn from_native(value: flTensorDataType) -> TensorDataType {
182 match value {
183 FOUNDRY_LOCAL_TENSOR_FLOAT => TensorDataType::Float,
184 FOUNDRY_LOCAL_TENSOR_UINT8 => TensorDataType::Uint8,
185 FOUNDRY_LOCAL_TENSOR_INT8 => TensorDataType::Int8,
186 FOUNDRY_LOCAL_TENSOR_UINT16 => TensorDataType::Uint16,
187 FOUNDRY_LOCAL_TENSOR_INT16 => TensorDataType::Int16,
188 FOUNDRY_LOCAL_TENSOR_INT32 => TensorDataType::Int32,
189 FOUNDRY_LOCAL_TENSOR_INT64 => TensorDataType::Int64,
190 FOUNDRY_LOCAL_TENSOR_STRING => TensorDataType::String,
191 FOUNDRY_LOCAL_TENSOR_BOOL => TensorDataType::Bool,
192 FOUNDRY_LOCAL_TENSOR_FLOAT16 => TensorDataType::Float16,
193 FOUNDRY_LOCAL_TENSOR_DOUBLE => TensorDataType::Double,
194 FOUNDRY_LOCAL_TENSOR_UINT32 => TensorDataType::Uint32,
195 FOUNDRY_LOCAL_TENSOR_UINT64 => TensorDataType::Uint64,
196 FOUNDRY_LOCAL_TENSOR_COMPLEX64 => TensorDataType::Complex64,
197 FOUNDRY_LOCAL_TENSOR_COMPLEX128 => TensorDataType::Complex128,
198 FOUNDRY_LOCAL_TENSOR_BFLOAT16 => TensorDataType::BFloat16,
199 FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FN => TensorDataType::Float8E4M3FN,
200 FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FNUZ => TensorDataType::Float8E4M3FNUZ,
201 FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2 => TensorDataType::Float8E5M2,
202 FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2FNUZ => TensorDataType::Float8E5M2FNUZ,
203 FOUNDRY_LOCAL_TENSOR_UINT4 => TensorDataType::Uint4,
204 FOUNDRY_LOCAL_TENSOR_INT4 => TensorDataType::Int4,
205 FOUNDRY_LOCAL_TENSOR_FLOAT4E2M1 => TensorDataType::Float4E2M1,
206 FOUNDRY_LOCAL_TENSOR_FLOAT8E8M0 => TensorDataType::Float8E8M0,
207 _ => TensorDataType::Undefined,
208 }
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
214pub enum SpeechSegmentKind {
215 #[default]
217 None,
218 Partial,
220 Final,
222}
223
224impl SpeechSegmentKind {
225 pub(crate) fn from_native(value: flSpeechSegmentKind) -> SpeechSegmentKind {
226 match value {
227 FOUNDRY_LOCAL_SPEECH_SEGMENT_PARTIAL => SpeechSegmentKind::Partial,
228 FOUNDRY_LOCAL_SPEECH_SEGMENT_FINAL => SpeechSegmentKind::Final,
229 _ => SpeechSegmentKind::None,
230 }
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub enum MediaSource {
237 Data(Vec<u8>),
239 Uri(String),
241}
242
243#[derive(Debug, Clone, PartialEq)]
248pub struct Message {
249 pub role: MessageRole,
251 pub content: Vec<Item>,
253 pub name: Option<String>,
255}
256
257impl Message {
258 pub fn new(role: MessageRole, content: impl Into<Vec<Item>>) -> Self {
260 Self {
261 role,
262 content: content.into(),
263 name: None,
264 }
265 }
266
267 pub fn with_name(mut self, name: impl Into<String>) -> Self {
269 self.name = Some(name.into());
270 self
271 }
272
273 pub fn is_simple_text(&self) -> bool {
275 matches!(self.content.as_slice(), [Item::Text { .. }])
276 }
277
278 pub fn text(&self) -> String {
282 let mut out = String::new();
283 for part in &self.content {
284 if let Item::Text { text, .. } = part {
285 out.push_str(text);
286 }
287 }
288 out
289 }
290}
291
292#[derive(Debug, Clone, PartialEq)]
295pub struct Tensor {
296 pub data_type: TensorDataType,
298 pub shape: Vec<i64>,
300 pub data: Vec<u8>,
302}
303
304impl Tensor {
305 pub fn as_f32(&self) -> Option<Vec<f32>> {
310 if self.data_type != TensorDataType::Float || self.data.len() % 4 != 0 {
311 return None;
312 }
313 Some(
314 self.data
315 .chunks_exact(4)
316 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
317 .collect(),
318 )
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct Image {
325 pub source: MediaSource,
327 pub format: Option<String>,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct Audio {
334 pub source: MediaSource,
336 pub format: Option<String>,
338 pub sample_rate: i32,
340 pub channels: i32,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct ToolCall {
347 pub call_id: String,
349 pub name: String,
351 pub arguments: String,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct ToolResult {
358 pub call_id: String,
360 pub result: String,
362}
363
364#[derive(Debug, Clone, PartialEq)]
366pub struct SpeechWord {
367 pub text: String,
369 pub start_time_ms: Option<i64>,
371 pub end_time_ms: Option<i64>,
373 pub confidence: Option<f32>,
375 pub speaker_id: Option<String>,
377}
378
379#[derive(Debug, Clone, PartialEq)]
381pub struct SpeechSegment {
382 pub kind: SpeechSegmentKind,
384 pub text: String,
386 pub start_time_ms: Option<i64>,
388 pub end_time_ms: Option<i64>,
390 pub utterance_start: bool,
392 pub words: Vec<SpeechWord>,
394 pub language: Option<String>,
396}
397
398#[derive(Debug, Clone, PartialEq)]
400pub struct SpeechResult {
401 pub text: String,
403 pub language: Option<String>,
405 pub duration_ms: Option<i64>,
407 pub segments: Vec<Item>,
409}
410
411#[derive(Debug, Clone, PartialEq)]
418pub enum Item {
419 Text {
421 text: String,
423 kind: TextKind,
425 },
426 Message(Message),
428 Bytes(Vec<u8>),
430 Tensor(Tensor),
432 Image(Image),
434 Audio(Audio),
436 ToolCall(ToolCall),
438 ToolResult(ToolResult),
440 SpeechSegment(SpeechSegment),
442 SpeechResult(SpeechResult),
444}
445
446impl Item {
447 pub fn item_type(&self) -> ItemType {
449 match self {
450 Item::Text { .. } => ItemType::Text,
451 Item::Message(_) => ItemType::Message,
452 Item::Bytes(_) => ItemType::Bytes,
453 Item::Tensor(_) => ItemType::Tensor,
454 Item::Image(_) => ItemType::Image,
455 Item::Audio(_) => ItemType::Audio,
456 Item::ToolCall(_) => ItemType::ToolCall,
457 Item::ToolResult(_) => ItemType::ToolResult,
458 Item::SpeechSegment(_) => ItemType::SpeechSegment,
459 Item::SpeechResult(_) => ItemType::SpeechResult,
460 }
461 }
462
463 pub fn text(text: impl Into<String>) -> Self {
467 Item::Text {
468 text: text.into(),
469 kind: TextKind::Default,
470 }
471 }
472
473 pub fn reasoning(text: impl Into<String>) -> Self {
475 Item::Text {
476 text: text.into(),
477 kind: TextKind::Reasoning,
478 }
479 }
480
481 pub fn message(role: MessageRole, content: impl Into<Vec<Item>>) -> Self {
485 Item::Message(Message::new(role, content))
486 }
487
488 pub fn system_message(content: impl Into<Vec<Item>>) -> Self {
490 Item::message(MessageRole::System, content)
491 }
492
493 pub fn user_message(content: impl Into<Vec<Item>>) -> Self {
495 Item::message(MessageRole::User, content)
496 }
497
498 pub fn assistant_message(content: impl Into<Vec<Item>>) -> Self {
500 Item::message(MessageRole::Assistant, content)
501 }
502
503 pub fn developer_message(content: impl Into<Vec<Item>>) -> Self {
505 Item::message(MessageRole::Developer, content)
506 }
507
508 pub fn tool_message(content: impl Into<Vec<Item>>) -> Self {
510 Item::message(MessageRole::Tool, content)
511 }
512
513 pub fn bytes(data: impl Into<Vec<u8>>) -> Self {
517 Item::Bytes(data.into())
518 }
519
520 pub fn tensor(
522 data_type: TensorDataType,
523 shape: impl Into<Vec<i64>>,
524 data: impl Into<Vec<u8>>,
525 ) -> Self {
526 Item::Tensor(Tensor {
527 data_type,
528 shape: shape.into(),
529 data: data.into(),
530 })
531 }
532
533 pub fn float_tensor(shape: impl Into<Vec<i64>>, data: &[f32]) -> Self {
535 let mut bytes = Vec::with_capacity(data.len() * 4);
536 for f in data {
537 bytes.extend_from_slice(&f.to_le_bytes());
538 }
539 Item::Tensor(Tensor {
540 data_type: TensorDataType::Float,
541 shape: shape.into(),
542 data: bytes,
543 })
544 }
545
546 pub fn image_data(data: impl Into<Vec<u8>>, format: Option<impl Into<String>>) -> Self {
550 Item::Image(Image {
551 source: MediaSource::Data(data.into()),
552 format: format.map(Into::into),
553 })
554 }
555
556 pub fn image_uri(uri: impl Into<String>, format: Option<impl Into<String>>) -> Self {
558 Item::Image(Image {
559 source: MediaSource::Uri(uri.into()),
560 format: format.map(Into::into),
561 })
562 }
563
564 pub fn audio_data(
566 data: impl Into<Vec<u8>>,
567 format: Option<impl Into<String>>,
568 sample_rate: i32,
569 channels: i32,
570 ) -> Self {
571 Item::Audio(Audio {
572 source: MediaSource::Data(data.into()),
573 format: format.map(Into::into),
574 sample_rate,
575 channels,
576 })
577 }
578
579 pub fn audio_uri(
581 uri: impl Into<String>,
582 format: Option<impl Into<String>>,
583 sample_rate: i32,
584 channels: i32,
585 ) -> Self {
586 Item::Audio(Audio {
587 source: MediaSource::Uri(uri.into()),
588 format: format.map(Into::into),
589 sample_rate,
590 channels,
591 })
592 }
593
594 pub fn tool_call(
598 call_id: impl Into<String>,
599 name: impl Into<String>,
600 arguments: impl Into<String>,
601 ) -> Self {
602 Item::ToolCall(ToolCall {
603 call_id: call_id.into(),
604 name: name.into(),
605 arguments: arguments.into(),
606 })
607 }
608
609 pub fn tool_result(call_id: impl Into<String>, result: impl Into<String>) -> Self {
611 Item::ToolResult(ToolResult {
612 call_id: call_id.into(),
613 result: result.into(),
614 })
615 }
616
617 pub fn as_text(&self) -> Option<&str> {
621 match self {
622 Item::Text { text, .. } => Some(text),
623 _ => None,
624 }
625 }
626
627 pub fn as_message(&self) -> Option<&Message> {
629 match self {
630 Item::Message(m) => Some(m),
631 _ => None,
632 }
633 }
634
635 pub fn as_tensor(&self) -> Option<&Tensor> {
637 match self {
638 Item::Tensor(t) => Some(t),
639 _ => None,
640 }
641 }
642
643 pub fn as_tool_call(&self) -> Option<&ToolCall> {
645 match self {
646 Item::ToolCall(c) => Some(c),
647 _ => None,
648 }
649 }
650
651 pub fn as_speech_result(&self) -> Option<&SpeechResult> {
653 match self {
654 Item::SpeechResult(r) => Some(r),
655 _ => None,
656 }
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663
664 #[test]
665 fn item_type_matches_variant() {
666 assert_eq!(Item::text("hi").item_type(), ItemType::Text);
667 assert_eq!(Item::bytes(vec![1, 2]).item_type(), ItemType::Bytes);
668 assert_eq!(
669 Item::tool_call("c", "f", "{}").item_type(),
670 ItemType::ToolCall
671 );
672 }
673
674 #[test]
675 fn message_helpers() {
676 let m = Item::user_message(vec![Item::text("hello"), Item::text(" world")]);
677 let msg = m.as_message().unwrap();
678 assert_eq!(msg.role, MessageRole::User);
679 assert!(!msg.is_simple_text());
680 assert_eq!(msg.text(), "hello world");
681
682 let simple = Message::new(MessageRole::System, vec![Item::text("x")]);
683 assert!(simple.is_simple_text());
684 }
685
686 #[test]
687 fn float_tensor_round_trips_bytes() {
688 let values = [1.0f32, -2.5, 3.25];
689 let item = Item::float_tensor(vec![3], &values);
690 let t = item.as_tensor().unwrap();
691 assert_eq!(t.data_type, TensorDataType::Float);
692 assert_eq!(t.shape, vec![3]);
693 assert_eq!(t.as_f32().unwrap(), values);
694 }
695
696 #[test]
697 fn native_enum_mappings_round_trip() {
698 for kind in [TextKind::Default, TextKind::Reasoning, TextKind::OpenAiJson] {
699 assert_eq!(TextKind::from_native(kind.to_native()), kind);
700 }
701 for role in [
702 MessageRole::None,
703 MessageRole::System,
704 MessageRole::User,
705 MessageRole::Assistant,
706 MessageRole::Tool,
707 MessageRole::Developer,
708 ] {
709 assert_eq!(MessageRole::from_native(role.to_native()), role);
710 }
711 for dt in [
712 TensorDataType::Undefined,
713 TensorDataType::Float,
714 TensorDataType::Int64,
715 TensorDataType::BFloat16,
716 TensorDataType::Float8E8M0,
717 ] {
718 assert_eq!(TensorDataType::from_native(dt.to_native()), dt);
719 }
720 }
721}