1use std::collections::HashMap;
22
23use objectiveai_sdk::agent::completions::message::{File, ImageUrl, InputAudio, VideoUrl};
24use objectiveai_sdk::cli::websocket_agents_instances_listener::{
25 AgentInstanceEvent, AssistantResponsePart, ClientNotificationPart, PartContent,
26 RequestMessageUserPart, ToolResponsePart, VectorRequestChoicePart,
27};
28use sqlx::Row as _;
29
30use super::super::time::unix_to_rfc3339;
31use super::super::{Error, Pool};
32use super::read_all::{FROM_JOINS, MsgRow, SELECT_SHAPE, row_into_msg};
33use super::row::MessageTable;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37enum ContentKind {
38 Text,
39 Image,
40 Audio,
41 Video,
42 File,
43}
44
45enum Source {
47 Part { table: &'static str, kind: ContentKind },
49 IndexedText { table: &'static str },
52 ToolCalls { table: &'static str },
55 MessageQueue { table: &'static str, kind: ContentKind },
58 ErrorRow,
60 Vote,
62 Blob,
64}
65
66fn source(t: MessageTable) -> Source {
69 use ContentKind as K;
70 use MessageTable as T;
71 match t {
72 T::AgentCompletionRequest
73 | T::VectorCompletionRequest
74 | T::FunctionExecutionRequest => Source::Blob,
75 T::MessageQueueText => Source::MessageQueue {
76 table: "objectiveai.message_queue_texts",
77 kind: K::Text,
78 },
79 T::MessageQueueImage => Source::MessageQueue {
80 table: "objectiveai.message_queue_images",
81 kind: K::Image,
82 },
83 T::MessageQueueAudio => Source::MessageQueue {
84 table: "objectiveai.message_queue_audios",
85 kind: K::Audio,
86 },
87 T::MessageQueueVideo => Source::MessageQueue {
88 table: "objectiveai.message_queue_videos",
89 kind: K::Video,
90 },
91 T::MessageQueueFile => Source::MessageQueue {
92 table: "objectiveai.message_queue_files",
93 kind: K::File,
94 },
95 T::AssistantResponseRefusal => Source::IndexedText {
96 table: "objectiveai.assistant_response_refusal",
97 },
98 T::AssistantResponseReasoning => Source::IndexedText {
99 table: "objectiveai.assistant_response_reasoning",
100 },
101 T::AssistantResponseToolCalls => Source::ToolCalls {
102 table: "objectiveai.assistant_response_tool_calls",
103 },
104 T::AssistantResponseContentText => Source::Part {
105 table: "objectiveai.assistant_response_content_text",
106 kind: K::Text,
107 },
108 T::AssistantResponseContentImage => Source::Part {
109 table: "objectiveai.assistant_response_content_image",
110 kind: K::Image,
111 },
112 T::AssistantResponseContentAudio => Source::Part {
113 table: "objectiveai.assistant_response_content_audio",
114 kind: K::Audio,
115 },
116 T::AssistantResponseContentVideo => Source::Part {
117 table: "objectiveai.assistant_response_content_video",
118 kind: K::Video,
119 },
120 T::AssistantResponseContentFile => Source::Part {
121 table: "objectiveai.assistant_response_content_file",
122 kind: K::File,
123 },
124 T::ToolResponseContentText => Source::Part {
125 table: "objectiveai.tool_response_content_text",
126 kind: K::Text,
127 },
128 T::ToolResponseContentImage => Source::Part {
129 table: "objectiveai.tool_response_content_image",
130 kind: K::Image,
131 },
132 T::ToolResponseContentAudio => Source::Part {
133 table: "objectiveai.tool_response_content_audio",
134 kind: K::Audio,
135 },
136 T::ToolResponseContentVideo => Source::Part {
137 table: "objectiveai.tool_response_content_video",
138 kind: K::Video,
139 },
140 T::ToolResponseContentFile => Source::Part {
141 table: "objectiveai.tool_response_content_file",
142 kind: K::File,
143 },
144 T::RequestMessageUserContentText => Source::Part {
145 table: "objectiveai.request_message_user_content_text",
146 kind: K::Text,
147 },
148 T::RequestMessageUserContentImage => Source::Part {
149 table: "objectiveai.request_message_user_content_image",
150 kind: K::Image,
151 },
152 T::RequestMessageUserContentAudio => Source::Part {
153 table: "objectiveai.request_message_user_content_audio",
154 kind: K::Audio,
155 },
156 T::RequestMessageUserContentVideo => Source::Part {
157 table: "objectiveai.request_message_user_content_video",
158 kind: K::Video,
159 },
160 T::RequestMessageUserContentFile => Source::Part {
161 table: "objectiveai.request_message_user_content_file",
162 kind: K::File,
163 },
164 T::RequestMessageAssistantRefusal => Source::IndexedText {
165 table: "objectiveai.request_message_assistant_refusal",
166 },
167 T::RequestMessageAssistantReasoning => Source::IndexedText {
168 table: "objectiveai.request_message_assistant_reasoning",
169 },
170 T::RequestMessageAssistantToolCalls => Source::ToolCalls {
171 table: "objectiveai.request_message_assistant_tool_calls",
172 },
173 T::RequestMessageAssistantContentText => Source::Part {
174 table: "objectiveai.request_message_assistant_content_text",
175 kind: K::Text,
176 },
177 T::RequestMessageAssistantContentImage => Source::Part {
178 table: "objectiveai.request_message_assistant_content_image",
179 kind: K::Image,
180 },
181 T::RequestMessageAssistantContentAudio => Source::Part {
182 table: "objectiveai.request_message_assistant_content_audio",
183 kind: K::Audio,
184 },
185 T::RequestMessageAssistantContentVideo => Source::Part {
186 table: "objectiveai.request_message_assistant_content_video",
187 kind: K::Video,
188 },
189 T::RequestMessageAssistantContentFile => Source::Part {
190 table: "objectiveai.request_message_assistant_content_file",
191 kind: K::File,
192 },
193 T::RequestMessageToolContentText => Source::Part {
194 table: "objectiveai.request_message_tool_content_text",
195 kind: K::Text,
196 },
197 T::RequestMessageToolContentImage => Source::Part {
198 table: "objectiveai.request_message_tool_content_image",
199 kind: K::Image,
200 },
201 T::RequestMessageToolContentAudio => Source::Part {
202 table: "objectiveai.request_message_tool_content_audio",
203 kind: K::Audio,
204 },
205 T::RequestMessageToolContentVideo => Source::Part {
206 table: "objectiveai.request_message_tool_content_video",
207 kind: K::Video,
208 },
209 T::RequestMessageToolContentFile => Source::Part {
210 table: "objectiveai.request_message_tool_content_file",
211 kind: K::File,
212 },
213 T::RequestVectorChoiceContentText => Source::Part {
214 table: "objectiveai.request_vector_choice_content_text",
215 kind: K::Text,
216 },
217 T::RequestVectorChoiceContentImage => Source::Part {
218 table: "objectiveai.request_vector_choice_content_image",
219 kind: K::Image,
220 },
221 T::RequestVectorChoiceContentAudio => Source::Part {
222 table: "objectiveai.request_vector_choice_content_audio",
223 kind: K::Audio,
224 },
225 T::RequestVectorChoiceContentVideo => Source::Part {
226 table: "objectiveai.request_vector_choice_content_video",
227 kind: K::Video,
228 },
229 T::RequestVectorChoiceContentFile => Source::Part {
230 table: "objectiveai.request_vector_choice_content_file",
231 kind: K::File,
232 },
233 T::ResponseVectorVote => Source::Vote,
234 T::Error => Source::ErrorRow,
235 }
236}
237
238enum Fetched {
241 Media(PartContent),
243 Text(String),
245 ToolCall {
247 tool_call_id: String,
248 function_name: String,
249 arguments: String,
250 },
251}
252
253type ContentMap = HashMap<(MessageTable, String, i64, Option<i64>), Fetched>;
254
255fn media_content(kind: ContentKind, row: &sqlx::postgres::PgRow) -> Result<PartContent, Error> {
259 Ok(match kind {
260 ContentKind::Text => PartContent::Text {
261 text: row.try_get("text")?,
262 },
263 ContentKind::Image => {
264 let url: String = row.try_get("url")?;
265 let detail_str: Option<String> = row.try_get("detail")?;
266 let detail = match detail_str {
267 Some(s) => serde_json::from_value(serde_json::Value::String(s))?,
268 None => None,
269 };
270 PartContent::Image(ImageUrl { url, detail })
271 }
272 ContentKind::Audio => PartContent::Audio(InputAudio {
273 data: row.try_get("data")?,
274 format: row.try_get("format")?,
275 }),
276 ContentKind::Video => PartContent::Video(VideoUrl {
277 url: row.try_get("url")?,
278 }),
279 ContentKind::File => PartContent::File(File {
280 file_data: row.try_get("file_data")?,
281 file_id: row.try_get("file_id")?,
282 filename: row.try_get("filename")?,
283 file_url: row.try_get("file_url")?,
284 }),
285 })
286}
287
288fn media_columns(kind: ContentKind) -> &'static str {
290 match kind {
291 ContentKind::Text => "text",
292 ContentKind::Image => "url, detail",
293 ContentKind::Audio => "data, format",
294 ContentKind::Video => "url",
295 ContentKind::File => "file_data, file_id, filename, file_url",
296 }
297}
298
299fn assistant_media(delivered_at: String, content: PartContent) -> AssistantResponsePart {
301 match content {
302 PartContent::Text { text } => AssistantResponsePart::Text { delivered_at, text },
303 PartContent::Image(image) => AssistantResponsePart::Image {
304 delivered_at,
305 image,
306 },
307 PartContent::Audio(audio) => AssistantResponsePart::Audio {
308 delivered_at,
309 audio,
310 },
311 PartContent::Video(video) => AssistantResponsePart::Video {
312 delivered_at,
313 video,
314 },
315 PartContent::File(file) => AssistantResponsePart::File { delivered_at, file },
316 }
317}
318
319pub async fn read_conversation_page(
326 pool: &Pool,
327 agent_instance_hierarchy: &str,
328 after_id: Option<i64>,
329 limit: i64,
330) -> Result<(Vec<AgentInstanceEvent>, Option<i64>), Error> {
331 let sql = format!(
333 "{SELECT_SHAPE} {FROM_JOINS} \
334 WHERE m.agent_instance_hierarchy = $1 AND m.\"index\" > $2 \
335 ORDER BY m.\"index\" ASC \
336 LIMIT $3",
337 );
338 let rows = sqlx::query(&sql)
339 .bind(agent_instance_hierarchy)
340 .bind(after_id.unwrap_or(0))
341 .bind(limit)
342 .fetch_all(&**pool)
343 .await?;
344 let msgs = rows
345 .iter()
346 .map(row_into_msg)
347 .collect::<Result<Vec<MsgRow>, Error>>()?;
348 let next_after_id = if msgs.len() as i64 == limit {
349 msgs.last().map(|m| m.id)
350 } else {
351 None
352 };
353
354 let mut part_keys: HashMap<MessageTable, (Vec<String>, Vec<i64>, Vec<i64>)> = HashMap::new();
357 let mut text_keys: HashMap<MessageTable, (Vec<String>, Vec<i64>)> = HashMap::new();
358 let mut call_keys: HashMap<MessageTable, (Vec<String>, Vec<i64>, Vec<i64>)> = HashMap::new();
359 let mut queue_ids: HashMap<MessageTable, Vec<i64>> = HashMap::new();
360 let mut error_ids: Vec<i64> = Vec::new();
361 for msg in &msgs {
362 let rid = msg.response_id.clone().unwrap_or_default();
365 match source(msg.table_kind) {
366 Source::Part { .. } => {
367 let (Some(index), Some(part_index)) = (msg.row_index, msg.row_sub_index) else {
368 continue;
369 };
370 let entry = part_keys.entry(msg.table_kind).or_default();
371 entry.0.push(rid);
372 entry.1.push(index);
373 entry.2.push(part_index);
374 }
375 Source::IndexedText { .. } => {
376 let Some(index) = msg.row_index else { continue };
377 let entry = text_keys.entry(msg.table_kind).or_default();
378 entry.0.push(rid);
379 entry.1.push(index);
380 }
381 Source::ToolCalls { .. } => {
382 let (Some(index), Some(tool_call_index)) = (msg.row_index, msg.row_sub_index)
383 else {
384 continue;
385 };
386 let entry = call_keys.entry(msg.table_kind).or_default();
387 entry.0.push(rid);
388 entry.1.push(index);
389 entry.2.push(tool_call_index);
390 }
391 Source::MessageQueue { .. } => {
392 let Some(id) = msg.row_index else { continue };
393 queue_ids.entry(msg.table_kind).or_default().push(id);
394 }
395 Source::ErrorRow => {
396 let Some(id) = msg.row_index else { continue };
397 error_ids.push(id);
398 }
399 Source::Vote | Source::Blob => {}
400 }
401 }
402
403 let mut content: ContentMap = HashMap::new();
404 let mut errors_by_id: HashMap<i64, serde_json::Value> = HashMap::new();
405
406 for (kind_table, (rids, indices, part_indices)) in &part_keys {
407 let Source::Part { table, kind } = source(*kind_table) else {
408 unreachable!("grouped as Part above");
409 };
410 let sql = format!(
411 "SELECT t.response_id, t.\"index\" AS idx, t.part_index AS pidx, {} \
412 FROM {table} t \
413 JOIN unnest($1::text[], $2::bigint[], $3::bigint[]) AS k(rid, idx, pidx) \
414 ON t.response_id = k.rid AND t.\"index\" = k.idx AND t.part_index = k.pidx",
415 media_columns(kind),
416 );
417 for row in sqlx::query(&sql)
418 .bind(rids)
419 .bind(indices)
420 .bind(part_indices)
421 .fetch_all(&**pool)
422 .await?
423 {
424 let rid: String = row.try_get("response_id")?;
425 let idx: i64 = row.try_get("idx")?;
426 let pidx: i64 = row.try_get("pidx")?;
427 content.insert(
428 (*kind_table, rid, idx, Some(pidx)),
429 Fetched::Media(media_content(kind, &row)?),
430 );
431 }
432 }
433
434 for (kind_table, (rids, indices)) in &text_keys {
435 let Source::IndexedText { table } = source(*kind_table) else {
436 unreachable!("grouped as IndexedText above");
437 };
438 let sql = format!(
439 "SELECT t.response_id, t.\"index\" AS idx, t.text \
440 FROM {table} t \
441 JOIN unnest($1::text[], $2::bigint[]) AS k(rid, idx) \
442 ON t.response_id = k.rid AND t.\"index\" = k.idx",
443 );
444 for row in sqlx::query(&sql)
445 .bind(rids)
446 .bind(indices)
447 .fetch_all(&**pool)
448 .await?
449 {
450 let rid: String = row.try_get("response_id")?;
451 let idx: i64 = row.try_get("idx")?;
452 let text: String = row.try_get("text")?;
453 content.insert((*kind_table, rid, idx, None), Fetched::Text(text));
454 }
455 }
456
457 for (kind_table, (rids, indices, call_indices)) in &call_keys {
458 let Source::ToolCalls { table } = source(*kind_table) else {
459 unreachable!("grouped as ToolCalls above");
460 };
461 let sql = format!(
462 "SELECT t.response_id, t.\"index\" AS idx, t.tool_call_index AS tci, \
463 t.tool_call_id, t.function_name, t.arguments \
464 FROM {table} t \
465 JOIN unnest($1::text[], $2::bigint[], $3::bigint[]) AS k(rid, idx, tci) \
466 ON t.response_id = k.rid AND t.\"index\" = k.idx AND t.tool_call_index = k.tci",
467 );
468 for row in sqlx::query(&sql)
469 .bind(rids)
470 .bind(indices)
471 .bind(call_indices)
472 .fetch_all(&**pool)
473 .await?
474 {
475 let rid: String = row.try_get("response_id")?;
476 let idx: i64 = row.try_get("idx")?;
477 let tci: i64 = row.try_get("tci")?;
478 content.insert(
479 (*kind_table, rid, idx, Some(tci)),
480 Fetched::ToolCall {
481 tool_call_id: row.try_get("tool_call_id")?,
482 function_name: row.try_get("function_name")?,
483 arguments: row.try_get("arguments")?,
484 },
485 );
486 }
487 }
488
489 for (kind_table, ids) in &queue_ids {
490 let Source::MessageQueue { table, kind } = source(*kind_table) else {
491 unreachable!("grouped as MessageQueue above");
492 };
493 let sql = format!(
494 "SELECT t.id, {} FROM {table} t WHERE t.id = ANY($1::bigint[])",
495 media_columns(kind),
496 );
497 for row in sqlx::query(&sql).bind(ids).fetch_all(&**pool).await? {
498 let id: i64 = row.try_get("id")?;
499 for msg in msgs
502 .iter()
503 .filter(|m| m.table_kind == *kind_table && m.row_index == Some(id))
504 {
505 content.insert(
506 (
507 *kind_table,
508 msg.response_id.clone().unwrap_or_default(),
509 id,
510 None,
511 ),
512 Fetched::Media(media_content(kind, &row)?),
513 );
514 }
515 }
516 }
517
518 if !error_ids.is_empty() {
519 for row in
520 sqlx::query("SELECT id, error FROM objectiveai.errors WHERE id = ANY($1::bigint[])")
521 .bind(&error_ids)
522 .fetch_all(&**pool)
523 .await?
524 {
525 let id: i64 = row.try_get("id")?;
526 let error: serde_json::Value = row.try_get("error")?;
527 errors_by_id.insert(id, error);
528 }
529 }
530
531 let mut out = Vec::with_capacity(msgs.len());
534 for msg in &msgs {
535 let delivered_at = unix_to_rfc3339(msg.timestamp_delivered);
536 let aih = msg.agent_instance_hierarchy.clone();
537
538 if msg.table_kind == MessageTable::Error {
540 let Some(error) = msg.row_index.and_then(|id| errors_by_id.get(&id)) else {
541 continue; };
543 out.push(AgentInstanceEvent::Error {
544 agent_instance_hierarchy: aih,
545 response_id: msg.response_id.clone(),
546 error: error.clone(),
547 delivered_at,
548 });
549 continue;
550 }
551
552 let Some(response_id) = msg.response_id.clone() else {
554 continue;
555 };
556
557 match source(msg.table_kind) {
558 Source::Blob | Source::ErrorRow => continue,
559 Source::Vote => {
560 let Some(vote) = msg
561 .vote
562 .clone()
563 .and_then(|v| serde_json::from_value(v).ok())
564 else {
565 continue;
566 };
567 out.push(AgentInstanceEvent::VectorResponseVote {
568 agent_instance_hierarchy: aih,
569 response_id,
570 vote,
571 });
572 }
573 _ => {
574 let row_index = msg.row_index.unwrap_or(0);
575 let key = (
576 msg.table_kind,
577 response_id.clone(),
578 row_index,
579 msg.row_sub_index,
580 );
581 let Some(fetched) = content.get(&key) else {
582 continue;
585 };
586 let Some(event) = build_event(
587 msg,
588 aih,
589 response_id,
590 row_index,
591 delivered_at,
592 fetched,
593 ) else {
594 continue;
595 };
596 out.push(event);
597 }
598 }
599 }
600 Ok((out, next_after_id))
601}
602
603fn build_event(
607 msg: &MsgRow,
608 agent_instance_hierarchy: String,
609 response_id: String,
610 row_index: i64,
611 delivered_at: String,
612 fetched: &Fetched,
613) -> Option<AgentInstanceEvent> {
614 use MessageTable as T;
615 let row_sub_index = msg.row_sub_index;
616 Some(match msg.table_kind {
617 T::MessageQueueText
619 | T::MessageQueueImage
620 | T::MessageQueueAudio
621 | T::MessageQueueVideo
622 | T::MessageQueueFile => {
623 let Fetched::Media(content) = fetched else {
624 return None;
625 };
626 AgentInstanceEvent::ClientNotificationPart {
627 agent_instance_hierarchy,
628 response_id,
629 sender_agent_instance_hierarchy: msg
630 .sender_agent_instance_hierarchy
631 .clone()
632 .unwrap_or_default(),
633 message_queue_id: msg.message_queue_id?,
634 queued_at: unix_to_rfc3339(msg.timestamp_queued.unwrap_or_default()),
635 key: msg.message_queue_key.clone(),
636 row_index,
637 part: ClientNotificationPart {
638 delivered_at,
639 content: content.clone(),
640 },
641 }
642 }
643
644 T::AssistantResponseRefusal => AgentInstanceEvent::AssistantResponsePart {
646 agent_instance_hierarchy,
647 response_id,
648 row_index,
649 row_sub_index,
650 part: AssistantResponsePart::Refusal {
651 delivered_at,
652 text: fetched_text(fetched)?,
653 },
654 },
655 T::AssistantResponseReasoning => AgentInstanceEvent::AssistantResponsePart {
656 agent_instance_hierarchy,
657 response_id,
658 row_index,
659 row_sub_index,
660 part: AssistantResponsePart::Reasoning {
661 delivered_at,
662 text: fetched_text(fetched)?,
663 },
664 },
665 T::AssistantResponseToolCalls => AgentInstanceEvent::AssistantResponsePart {
666 agent_instance_hierarchy,
667 response_id,
668 row_index,
669 row_sub_index,
670 part: tool_call_part(fetched, delivered_at, row_sub_index)?,
671 },
672 T::AssistantResponseContentText
673 | T::AssistantResponseContentImage
674 | T::AssistantResponseContentAudio
675 | T::AssistantResponseContentVideo
676 | T::AssistantResponseContentFile => {
677 let Fetched::Media(content) = fetched else {
678 return None;
679 };
680 AgentInstanceEvent::AssistantResponsePart {
681 agent_instance_hierarchy,
682 response_id,
683 row_index,
684 row_sub_index,
685 part: assistant_media(delivered_at, content.clone()),
686 }
687 }
688
689 T::RequestMessageAssistantRefusal => AgentInstanceEvent::RequestMessageAssistantPart {
691 agent_instance_hierarchy,
692 response_id,
693 row_index,
694 row_sub_index,
695 part: AssistantResponsePart::Refusal {
696 delivered_at,
697 text: fetched_text(fetched)?,
698 },
699 },
700 T::RequestMessageAssistantReasoning => AgentInstanceEvent::RequestMessageAssistantPart {
701 agent_instance_hierarchy,
702 response_id,
703 row_index,
704 row_sub_index,
705 part: AssistantResponsePart::Reasoning {
706 delivered_at,
707 text: fetched_text(fetched)?,
708 },
709 },
710 T::RequestMessageAssistantToolCalls => AgentInstanceEvent::RequestMessageAssistantPart {
711 agent_instance_hierarchy,
712 response_id,
713 row_index,
714 row_sub_index,
715 part: tool_call_part(fetched, delivered_at, row_sub_index)?,
716 },
717 T::RequestMessageAssistantContentText
718 | T::RequestMessageAssistantContentImage
719 | T::RequestMessageAssistantContentAudio
720 | T::RequestMessageAssistantContentVideo
721 | T::RequestMessageAssistantContentFile => {
722 let Fetched::Media(content) = fetched else {
723 return None;
724 };
725 AgentInstanceEvent::RequestMessageAssistantPart {
726 agent_instance_hierarchy,
727 response_id,
728 row_index,
729 row_sub_index,
730 part: assistant_media(delivered_at, content.clone()),
731 }
732 }
733
734 T::ToolResponseContentText
736 | T::ToolResponseContentImage
737 | T::ToolResponseContentAudio
738 | T::ToolResponseContentVideo
739 | T::ToolResponseContentFile => {
740 let Fetched::Media(content) = fetched else {
741 return None;
742 };
743 AgentInstanceEvent::ToolResponsePart {
744 agent_instance_hierarchy,
745 response_id,
746 tool_call_id: msg.tool_call_id.clone()?,
747 row_index,
748 row_sub_index,
749 part: ToolResponsePart {
750 delivered_at,
751 content: content.clone(),
752 },
753 }
754 }
755
756 T::RequestMessageToolContentText
758 | T::RequestMessageToolContentImage
759 | T::RequestMessageToolContentAudio
760 | T::RequestMessageToolContentVideo
761 | T::RequestMessageToolContentFile => {
762 let Fetched::Media(content) = fetched else {
763 return None;
764 };
765 AgentInstanceEvent::RequestMessageToolPart {
766 agent_instance_hierarchy,
767 response_id,
768 tool_call_id: msg.tool_call_id.clone()?,
769 row_index,
770 row_sub_index,
771 part: ToolResponsePart {
772 delivered_at,
773 content: content.clone(),
774 },
775 }
776 }
777
778 T::RequestMessageUserContentText
780 | T::RequestMessageUserContentImage
781 | T::RequestMessageUserContentAudio
782 | T::RequestMessageUserContentVideo
783 | T::RequestMessageUserContentFile => {
784 let Fetched::Media(content) = fetched else {
785 return None;
786 };
787 AgentInstanceEvent::RequestMessageUserPart {
788 agent_instance_hierarchy,
789 response_id,
790 row_index,
791 row_sub_index,
792 part: RequestMessageUserPart {
793 delivered_at,
794 content: content.clone(),
795 },
796 }
797 }
798
799 T::RequestVectorChoiceContentText
801 | T::RequestVectorChoiceContentImage
802 | T::RequestVectorChoiceContentAudio
803 | T::RequestVectorChoiceContentVideo
804 | T::RequestVectorChoiceContentFile => {
805 let Fetched::Media(content) = fetched else {
806 return None;
807 };
808 AgentInstanceEvent::VectorRequestChoicePart {
809 agent_instance_hierarchy,
810 response_id,
811 key: msg.choice_key.clone()?,
812 choice_index: row_index,
813 part_index: row_sub_index?,
814 part: VectorRequestChoicePart {
815 delivered_at,
816 content: content.clone(),
817 },
818 }
819 }
820
821 T::AgentCompletionRequest
823 | T::VectorCompletionRequest
824 | T::FunctionExecutionRequest
825 | T::ResponseVectorVote
826 | T::Error => return None,
827 })
828}
829
830fn fetched_text(fetched: &Fetched) -> Option<String> {
831 match fetched {
832 Fetched::Text(text) => Some(text.clone()),
833 _ => None,
834 }
835}
836
837fn tool_call_part(
838 fetched: &Fetched,
839 delivered_at: String,
840 row_sub_index: Option<i64>,
841) -> Option<AssistantResponsePart> {
842 let Fetched::ToolCall {
843 tool_call_id,
844 function_name,
845 arguments,
846 } = fetched
847 else {
848 return None;
849 };
850 Some(AssistantResponsePart::ToolCall {
851 delivered_at,
852 function_name: function_name.clone(),
853 tool_call_id: tool_call_id.clone(),
854 tool_call_index: row_sub_index.unwrap_or(0),
855 arguments: arguments.clone(),
856 })
857}