1#![allow(missing_docs)]
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7pub type AnswersByToolCall = HashMap<String, HashMap<String, String>>;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Workspace {
16 pub id: String,
18 pub name: String,
20 #[serde(default)]
22 pub created_at: i64,
23 #[serde(default)]
25 pub updated_at: i64,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct WorkspacesResponse {
31 pub workspaces: Vec<Workspace>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Agent {
38 pub uid: String,
41 pub name: String,
43 #[serde(default)]
45 pub description: String,
46 #[serde(default)]
48 pub mode: String,
49 #[serde(default)]
51 pub icon: String,
52 #[serde(default)]
54 pub is_published: bool,
55 #[serde(default)]
57 pub published_at: i64,
58 #[serde(default)]
60 pub created_at: i64,
61 #[serde(default)]
63 pub updated_at: i64,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct AgentsResponse {
69 pub agents: Vec<Agent>,
71 #[serde(default)]
73 pub total: i32,
74}
75
76#[derive(Debug, Serialize, Default, Clone)]
78pub struct GetAgentsOptions {
79 #[serde(skip_serializing_if = "Option::is_none")]
80 page: Option<i32>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 limit: Option<i32>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 name: Option<String>,
85}
86
87impl GetAgentsOptions {
88 #[inline]
90 pub fn new() -> Self {
91 Default::default()
92 }
93
94 #[inline]
96 #[must_use]
97 pub fn page(self, page: i32) -> Self {
98 Self {
99 page: Some(page),
100 ..self
101 }
102 }
103
104 #[inline]
106 #[must_use]
107 pub fn limit(self, limit: i32) -> Self {
108 Self {
109 limit: Some(limit),
110 ..self
111 }
112 }
113
114 #[inline]
116 #[must_use]
117 pub fn name(self, name: impl Into<String>) -> Self {
118 Self {
119 name: Some(name.into()),
120 ..self
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum ConversationStatus {
129 Succeeded,
131 Interrupted,
134 Failed,
136 Stopped,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct Reference {
143 #[serde(default)]
145 pub index: i32,
146 #[serde(default)]
148 pub original_index: i32,
149 #[serde(default, rename = "type")]
151 pub ref_type: String,
152 #[serde(default)]
154 pub id: String,
155 #[serde(default)]
158 pub title: String,
159 #[serde(default)]
162 pub url: String,
163 #[serde(default)]
168 pub content: Option<serde_json::Value>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct Question {
174 pub question: String,
176 #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
178 pub options: Vec<QuestionOption>,
179 #[serde(default)]
181 pub multi_select: bool,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct QuestionOption {
187 #[serde(default)]
189 pub label: String,
190 #[serde(default)]
192 pub description: String,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct Interrupt {
199 pub node_id: String,
201 pub tool_call_id: String,
203 #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
205 pub questions: Vec<Question>,
206 #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
208 pub interactions: Vec<HumanInteraction>,
209 #[serde(default)]
211 pub message_id: i64,
212 #[serde(default)]
214 pub chat_id: i64,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct HumanInteraction {
220 #[serde(default)]
222 pub tool_call_id: String,
223 #[serde(default)]
225 pub interrupt_id: String,
226 #[serde(default, rename = "type")]
228 pub interaction_type: String,
229 #[serde(default)]
231 pub tool_name: String,
232 #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
234 pub questions: Vec<Question>,
235 #[serde(default)]
237 pub tool_args: serde_json::Value,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct AgentError {
243 #[serde(default)]
245 pub code: i32,
246 #[serde(default)]
248 pub message: String,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct ConversationResponse {
256 pub chat_uid: String,
259 #[serde(deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string")]
262 pub message_id: String,
263 pub status: ConversationStatus,
265 #[serde(default)]
267 pub answer: String,
268 #[serde(default)]
270 pub references: Option<Vec<Reference>>,
271 #[serde(default)]
274 pub further_questions: Option<Vec<String>>,
275 #[serde(default)]
277 pub elapsed_time: f64,
278 #[serde(default)]
280 pub interrupt: Option<Interrupt>,
281 #[serde(default)]
283 pub error: Option<AgentError>,
284}
285
286impl ConversationResponse {
287 pub(crate) fn from_stream_parts(
291 started: Option<(String, String)>,
292 payload: WorkflowFinishedPayload,
293 ) -> Self {
294 let (chat_uid, message_id) = started.unwrap_or_default();
295 let error = (payload.status == ConversationStatus::Failed).then_some(AgentError {
296 code: payload.error_code,
297 message: payload.error_message,
298 });
299 Self {
300 chat_uid,
301 message_id,
302 status: payload.status,
303 answer: payload.outputs.answer.unwrap_or_default(),
304 references: payload.outputs.references,
305 further_questions: payload.outputs.further_questions,
306 elapsed_time: payload.elapsed_time,
307 interrupt: None,
308 error,
309 }
310 }
311
312 pub(crate) fn from_stream_interrupt(
321 started: Option<(String, String)>,
322 interrupt: Interrupt,
323 ) -> Self {
324 let (chat_uid, message_id) = started.unwrap_or_default();
325 Self {
326 chat_uid,
327 message_id,
328 status: ConversationStatus::Interrupted,
329 answer: String::new(),
330 references: None,
331 further_questions: None,
332 elapsed_time: 0.0,
333 interrupt: Some(interrupt),
334 error: None,
335 }
336 }
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ChatStartedPayload {
342 pub chat_uid: String,
344 #[serde(deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string")]
348 pub message_id: String,
349 #[serde(default)]
351 pub chat_id: i64,
352 #[serde(default)]
354 pub error: String,
355 #[serde(default)]
357 pub error_message: String,
358}
359
360#[derive(Debug, Clone, Default, Serialize, Deserialize)]
363pub struct MessagePayload {
364 #[serde(default)]
366 pub text: String,
367 #[serde(default, rename = "type")]
370 pub message_type: String,
371 #[serde(default)]
375 pub key: String,
376 #[serde(default)]
378 pub started_at: i64,
379 #[serde(default)]
381 pub stage: String,
382 #[serde(default)]
385 pub stage_title: String,
386 #[serde(default)]
389 pub stage_finished_title: String,
390 #[serde(default)]
392 pub outputs: Option<serde_json::Value>,
393}
394
395#[derive(Debug, Clone, Default, Serialize, Deserialize)]
397pub struct WorkflowOutputs {
398 #[serde(default)]
400 pub answer: Option<String>,
401 #[serde(default)]
403 pub references: Option<Vec<Reference>>,
404 #[serde(default)]
407 pub further_questions: Option<Vec<String>>,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct WorkflowFinishedPayload {
415 pub status: ConversationStatus,
417 #[serde(default)]
419 pub elapsed_time: f64,
420 #[serde(default)]
422 pub outputs: WorkflowOutputs,
423 #[serde(default)]
425 pub error: String,
426 #[serde(default)]
428 pub error_code: i32,
429 #[serde(default)]
431 pub error_message: String,
432 #[serde(default)]
434 pub error_args: Option<serde_json::Value>,
435 #[serde(default)]
437 pub process_data: Vec<serde_json::Value>,
438}
439
440#[derive(Debug, Clone, Default, Serialize, Deserialize)]
442pub struct WorkflowStartedInputs {
443 #[serde(default)]
445 pub chat_id: i64,
446 #[serde(default)]
448 pub chat_uid: String,
449 #[serde(
452 default,
453 deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string"
454 )]
455 pub message_id: String,
456 #[serde(default)]
458 pub query: String,
459}
460
461#[derive(Debug, Clone, Default, Serialize, Deserialize)]
464pub struct WorkflowStartedPayload {
465 #[serde(default)]
467 pub hit_cache: bool,
468 #[serde(default)]
470 pub inputs: WorkflowStartedInputs,
471 #[serde(default)]
473 pub started_at: i64,
474 #[serde(default)]
476 pub workflow_id: i64,
477}
478
479#[derive(Debug, Clone, Default, Serialize, Deserialize)]
482pub struct ChatFinishedPayload {
483 #[serde(default)]
485 pub chat_id: i64,
486 #[serde(default)]
488 pub chat_uid: String,
489 #[serde(
492 default,
493 deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string"
494 )]
495 pub message_id: String,
496 #[serde(default)]
498 pub error: String,
499 #[serde(default)]
501 pub error_message: String,
502}
503
504#[derive(Debug, Clone, Default, Serialize, Deserialize)]
508pub struct ChatTitleUpdatedPayload {
509 #[serde(default)]
511 pub chat_id: i64,
512 #[serde(default)]
514 pub chat_uid: String,
515 #[serde(default)]
517 pub source: String,
518 #[serde(default)]
520 pub title: String,
521 #[serde(default)]
523 pub updated_at: i64,
524}
525
526#[derive(Debug, Clone, Default, Serialize, Deserialize)]
531pub struct ThinkingStartedPayload {
532 #[serde(default)]
534 pub started_at: i64,
535}
536
537#[derive(Debug, Clone, Default, Serialize, Deserialize)]
540pub struct ThinkingFinishedPayload {
541 #[serde(default)]
543 pub finished_at: i64,
544 #[serde(default)]
546 pub elapsed_time: i32,
547}
548
549#[derive(Debug, Clone, Default, Serialize, Deserialize)]
553pub struct NodeToolUseStartedPayload {
554 #[serde(default)]
556 pub tool_use_id: String,
557 #[serde(default)]
559 pub tool_name: String,
560 #[serde(default)]
563 pub tool_func_name: String,
564 #[serde(default)]
566 pub tool_args: String,
567 #[serde(default)]
570 pub tips: String,
571 #[serde(default)]
573 pub tip_chips: Vec<String>,
574 #[serde(default)]
577 pub iteration: i32,
578 #[serde(default)]
580 pub started_at: i64,
581}
582
583#[derive(Debug, Clone, Default, Serialize, Deserialize)]
586pub struct NodeToolUseOutputs {
587 #[serde(default)]
589 pub references: Option<Vec<Reference>>,
590 #[serde(default)]
592 pub reference_domains: Option<Vec<String>>,
593 #[serde(default)]
595 pub query: Option<String>,
596 #[serde(default)]
598 pub text: Option<String>,
599 #[serde(default)]
601 pub tool_args: Option<serde_json::Value>,
602 #[serde(default)]
604 pub data: Option<serde_json::Value>,
605}
606
607#[derive(Debug, Clone, Default, Serialize, Deserialize)]
610pub struct NodeToolUseFinishedPayload {
611 #[serde(default)]
613 pub tool_use_id: String,
614 #[serde(default)]
616 pub status: String,
617 #[serde(default)]
619 pub error: String,
620 #[serde(default)]
622 pub elapsed_time: f64,
623 #[serde(default)]
625 pub started_at: i64,
626 #[serde(default)]
628 pub tool_name: String,
629 #[serde(default)]
631 pub tool_func_name: String,
632 #[serde(default)]
634 pub tool_args: String,
635 #[serde(default)]
637 pub tool_type: String,
638 #[serde(default)]
640 pub tips: String,
641 #[serde(default)]
643 pub tip_chips: Vec<String>,
644 #[serde(default)]
646 pub iteration: i32,
647 #[serde(default)]
649 pub is_thinking: bool,
650 #[serde(default)]
652 pub outputs: NodeToolUseOutputs,
653}
654
655#[derive(Debug, Clone, Default, Serialize, Deserialize)]
659pub struct SubagentStartedPayload {
660 #[serde(default)]
662 pub node_id: String,
663 #[serde(default)]
665 pub tool_use_id: String,
666 #[serde(default)]
668 pub started_at: i64,
669 #[serde(default)]
671 pub goal: String,
672 #[serde(default)]
674 pub prompt: String,
675 #[serde(default)]
677 pub subagent_id: String,
678 #[serde(default)]
680 pub tools: Vec<serde_json::Value>,
681}
682
683#[derive(Debug, Clone, Default, Serialize, Deserialize)]
687pub struct SubagentProgressPayload {
688 #[serde(default)]
690 pub node_id: String,
691 #[serde(default)]
693 pub parent_tool_call_id: String,
694 #[serde(default)]
696 pub subagent_tool_name: String,
697 #[serde(default)]
699 pub subagent_tool_args: String,
700 #[serde(default)]
702 pub subagent_status: String,
703 #[serde(default)]
705 pub subagent_duration_ms: i64,
706 #[serde(default)]
708 pub subagent_iteration: i32,
709 #[serde(default)]
711 pub started_at: i64,
712}
713
714#[derive(Debug, Clone, Default, Serialize, Deserialize)]
716pub struct SubagentOutputs {
717 #[serde(default)]
719 pub goal: Option<String>,
720 #[serde(default)]
722 pub result: Option<String>,
723 #[serde(default)]
725 pub subagent_tools: Option<Vec<serde_json::Value>>,
726}
727
728#[derive(Debug, Clone, Default, Serialize, Deserialize)]
730pub struct SubagentFinishedPayload {
731 #[serde(default)]
733 pub node_id: String,
734 #[serde(default)]
736 pub tool_use_id: String,
737 #[serde(default)]
739 pub status: String,
740 #[serde(default)]
742 pub started_at: i64,
743 #[serde(default)]
745 pub elapsed_time: f64,
746 #[serde(default)]
748 pub error: String,
749 #[serde(default)]
752 pub outputs: SubagentOutputs,
753}
754
755#[derive(Debug, Clone, Default, Serialize, Deserialize)]
759pub struct AgentToolStartedPayload {
760 #[serde(default)]
762 pub node_id: String,
763 #[serde(default)]
765 pub tool_use_id: String,
766 #[serde(default)]
768 pub agent_tool_name: String,
769 #[serde(default)]
771 pub title: String,
772 #[serde(default)]
774 pub started_at: i64,
775 #[serde(default)]
777 pub tool_args: String,
778 #[serde(default)]
780 pub tool_name: String,
781 #[serde(default)]
783 pub tips: String,
784 #[serde(default)]
786 pub tip_chips: Vec<String>,
787 #[serde(default)]
789 pub is_thinking: bool,
790}
791
792#[derive(Debug, Clone, Default, Serialize, Deserialize)]
795pub struct AgentToolProgressPayload {
796 #[serde(default)]
798 pub node_id: String,
799 #[serde(default)]
801 pub parent_tool_call_id: String,
802 #[serde(default)]
804 pub agent_tool_name: String,
805 #[serde(default)]
807 pub inner_tool_name: String,
808 #[serde(default)]
810 pub inner_tool_args: String,
811 #[serde(default)]
813 pub status: String,
814 #[serde(default)]
816 pub duration_ms: i64,
817 #[serde(default)]
819 pub started_at: i64,
820 #[serde(default)]
822 pub is_thinking: bool,
823}
824
825#[derive(Debug, Clone, Default, Serialize, Deserialize)]
827pub struct AgentToolFinishedPayload {
828 #[serde(default)]
830 pub node_id: String,
831 #[serde(default)]
833 pub tool_use_id: String,
834 #[serde(default)]
836 pub agent_tool_name: String,
837 #[serde(default)]
839 pub status: String,
840 #[serde(default)]
842 pub started_at: i64,
843 #[serde(default)]
845 pub elapsed_time: f64,
846 #[serde(default)]
848 pub error: String,
849 #[serde(default)]
851 pub tool_args: String,
852 #[serde(default)]
854 pub outputs: Option<serde_json::Value>,
855 #[serde(default)]
857 pub tool_type: String,
858 #[serde(default)]
860 pub tips: String,
861 #[serde(default)]
863 pub tip_chips: Vec<String>,
864 #[serde(default)]
866 pub is_thinking: bool,
867}
868
869#[derive(Debug, Clone, Default, Serialize, Deserialize)]
873pub struct QueryMaskedPayload {
874 #[serde(default)]
876 pub raw_query: String,
877 #[serde(default)]
879 pub masked_query: String,
880}
881
882#[derive(Debug, Clone, Default, Serialize, Deserialize)]
885pub struct PlanChangedPayload {
886 #[serde(default)]
888 pub node_id: String,
889 #[serde(default)]
891 pub started_at: i64,
892 #[serde(default)]
894 pub outputs: Option<serde_json::Value>,
895 #[serde(default)]
898 pub tool_name: String,
899}
900
901#[derive(Debug, Clone, Default, Serialize, Deserialize)]
905pub struct ContextCompressStartedPayload {
906 #[serde(default)]
908 pub started_at: String,
909 #[serde(default)]
911 pub inputs: Option<serde_json::Value>,
912}
913
914#[derive(Debug, Clone, Default, Serialize, Deserialize)]
917pub struct ContextCompressFinishedPayload {
918 #[serde(default)]
920 pub created_at: String,
921 #[serde(default)]
923 pub inputs: Option<serde_json::Value>,
924 #[serde(default)]
926 pub outputs: Option<serde_json::Value>,
927}
928
929#[derive(Debug, Clone)]
957pub enum ConversationStreamEvent {
958 ChatStarted(ChatStartedPayload),
960 WorkflowStarted(WorkflowStartedPayload),
964 Message(MessagePayload),
966 Ping,
969 ThinkingStarted(ThinkingStartedPayload),
971 ThinkingFinished(ThinkingFinishedPayload),
973 NodeToolUseStarted(NodeToolUseStartedPayload),
975 NodeToolUseFinished(NodeToolUseFinishedPayload),
977 SubagentStarted(SubagentStartedPayload),
979 SubagentProgress(SubagentProgressPayload),
981 SubagentFinished(SubagentFinishedPayload),
983 AgentToolStarted(AgentToolStartedPayload),
985 AgentToolProgress(AgentToolProgressPayload),
987 AgentToolFinished(AgentToolFinishedPayload),
989 HumanInteractionRequired(ConversationResponse),
995 QueryMasked(QueryMaskedPayload),
997 PlanChanged(PlanChangedPayload),
999 ContextCompressStarted(ContextCompressStartedPayload),
1002 ContextCompressFinished(ContextCompressFinishedPayload),
1004 ChatFinished(ChatFinishedPayload),
1007 WorkflowFinished(ConversationResponse),
1015 ChatTitleUpdated(ChatTitleUpdatedPayload),
1019 Other {
1024 event: String,
1026 data: serde_json::Value,
1028 },
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034
1035 const SUCCEEDED_JSON: &str = r#"{
1038 "chat_uid": "ct_9f2c1a5b",
1039 "message_id": "42",
1040 "status": "succeeded",
1041 "answer": "Tesla (TSLA.US) recently...",
1042 "references": [
1043 { "index": 1, "title": "...", "url": "..." }
1044 ],
1045 "elapsed_time": 3.21
1046 }"#;
1047
1048 const INTERRUPTED_JSON: &str = r#"{
1050 "chat_uid": "ct_9f2c1a5b",
1051 "message_id": "43",
1052 "status": "interrupted",
1053 "answer": "",
1054 "references": null,
1055 "elapsed_time": 1.05,
1056 "interrupt": {
1057 "node_id": "n_ask_human",
1058 "tool_call_id": "call_abc123",
1059 "questions": [
1060 {
1061 "question": "Which time range would you like to check?",
1062 "options": [
1063 { "description": "Past week" },
1064 { "description": "Past month" }
1065 ],
1066 "multi_select": false
1067 }
1068 ],
1069 "message_id": 43,
1070 "chat_id": 1001
1071 }
1072 }"#;
1073
1074 #[test]
1075 fn deserialize_succeeded_conversation_response() {
1076 let resp: ConversationResponse = serde_json::from_str(SUCCEEDED_JSON).unwrap();
1077 assert_eq!(resp.chat_uid, "ct_9f2c1a5b");
1078 assert_eq!(resp.message_id, "42");
1079 assert_eq!(resp.status, ConversationStatus::Succeeded);
1080 assert_eq!(resp.answer, "Tesla (TSLA.US) recently...");
1081 assert_eq!(resp.references.as_ref().unwrap().len(), 1);
1082 assert_eq!(resp.references.as_ref().unwrap()[0].index, 1);
1083 assert!((resp.elapsed_time - 3.21).abs() < f64::EPSILON);
1084 assert!(resp.interrupt.is_none());
1085 assert!(resp.error.is_none());
1086 }
1087
1088 #[test]
1089 fn deserialize_interrupted_conversation_response() {
1090 let resp: ConversationResponse = serde_json::from_str(INTERRUPTED_JSON).unwrap();
1091 assert_eq!(resp.status, ConversationStatus::Interrupted);
1092 let interrupt = resp.interrupt.expect("interrupt");
1093 assert_eq!(interrupt.node_id, "n_ask_human");
1094 assert_eq!(interrupt.tool_call_id, "call_abc123");
1095 assert_eq!(interrupt.message_id, 43);
1096 assert_eq!(interrupt.chat_id, 1001);
1097 assert_eq!(interrupt.questions.len(), 1);
1098 assert_eq!(interrupt.questions[0].options.len(), 2);
1099 assert!(!interrupt.questions[0].multi_select);
1100 }
1101
1102 #[test]
1103 fn deserialize_chat_started_payload_with_numeric_message_id() {
1104 let json = r#"{"chat_uid":"ct_9f2c1a5b","message_id":42}"#;
1107 let payload: ChatStartedPayload = serde_json::from_str(json).unwrap();
1108 assert_eq!(payload.chat_uid, "ct_9f2c1a5b");
1109 assert_eq!(payload.message_id, "42");
1110 }
1111
1112 #[test]
1113 fn deserialize_message_payload() {
1114 let json = r#"{"text":"Tesla"}"#;
1115 let payload: MessagePayload = serde_json::from_str(json).unwrap();
1116 assert_eq!(payload.text, "Tesla");
1117 }
1118
1119 #[test]
1120 fn deserialize_message_payload_with_full_fields() {
1121 let json =
1123 r#"{"text":"Tesla","type":"answer","key":"n_llm_1:answer","started_at":1752048000}"#;
1124 let payload: MessagePayload = serde_json::from_str(json).unwrap();
1125 assert_eq!(payload.text, "Tesla");
1126 assert_eq!(payload.message_type, "answer");
1127 assert_eq!(payload.key, "n_llm_1:answer");
1128 assert_eq!(payload.started_at, 1752048000);
1129 }
1130
1131 #[test]
1132 fn deserialize_workflow_finished_payload() {
1133 let json = r#"{"status":"succeeded","elapsed_time":3.21,"outputs":{"answer":"Tesla (TSLA.US) recently...","further_questions":["What is Tesla's P/E?","How did Q3 deliveries look?"]}}"#;
1134 let payload: WorkflowFinishedPayload = serde_json::from_str(json).unwrap();
1135 assert_eq!(payload.status, ConversationStatus::Succeeded);
1136 assert!((payload.elapsed_time - 3.21).abs() < f64::EPSILON);
1137 assert_eq!(
1138 payload.outputs.answer.as_deref(),
1139 Some("Tesla (TSLA.US) recently...")
1140 );
1141 assert_eq!(
1142 payload.outputs.further_questions.as_deref(),
1143 Some(
1144 [
1145 "What is Tesla's P/E?".to_string(),
1146 "How did Q3 deliveries look?".to_string(),
1147 ]
1148 .as_slice()
1149 )
1150 );
1151
1152 let resp = ConversationResponse::from_stream_parts(
1153 Some(("ct_9f2c1a5b".to_string(), "42".to_string())),
1154 payload,
1155 );
1156 assert_eq!(resp.chat_uid, "ct_9f2c1a5b");
1157 assert_eq!(resp.message_id, "42");
1158 assert_eq!(resp.answer, "Tesla (TSLA.US) recently...");
1159 assert_eq!(resp.further_questions.as_ref().unwrap().len(), 2);
1161 assert!(resp.interrupt.is_none());
1162 assert!(resp.error.is_none());
1163 }
1164
1165 #[test]
1166 fn deserialize_workflow_finished_payload_with_failure() {
1167 let json = r#"{"status":"failed","elapsed_time":0.8,"error":"upstream timeout","error_code":500,"error_message":"Something went wrong, please try again"}"#;
1170 let payload: WorkflowFinishedPayload = serde_json::from_str(json).unwrap();
1171 assert_eq!(payload.status, ConversationStatus::Failed);
1172 assert_eq!(payload.error, "upstream timeout");
1173 assert_eq!(payload.error_code, 500);
1174 assert_eq!(
1175 payload.error_message,
1176 "Something went wrong, please try again"
1177 );
1178
1179 let resp = ConversationResponse::from_stream_parts(None, payload);
1180 assert_eq!(resp.status, ConversationStatus::Failed);
1181 let error = resp.error.expect("error");
1182 assert_eq!(error.code, 500);
1183 assert_eq!(error.message, "Something went wrong, please try again");
1184 }
1185
1186 #[test]
1187 fn conversation_response_from_stream_interrupt() {
1188 let json = r#"{"node_id":"n_ask_human","tool_call_id":"call_abc123","questions":[{"question":"Which time range would you like to check?","options":[{"description":"Past week"},{"description":"Past month"}],"multi_select":false}],"message_id":43,"chat_id":1001}"#;
1193 let interrupt: Interrupt = serde_json::from_str(json).unwrap();
1194
1195 let resp = ConversationResponse::from_stream_interrupt(
1196 Some(("ct_9f2c1a5b".to_string(), "43".to_string())),
1197 interrupt,
1198 );
1199 assert_eq!(resp.chat_uid, "ct_9f2c1a5b");
1200 assert_eq!(resp.message_id, "43");
1201 assert_eq!(resp.status, ConversationStatus::Interrupted);
1202 let interrupt = resp.interrupt.expect("interrupt");
1203 assert_eq!(interrupt.node_id, "n_ask_human");
1204 assert_eq!(interrupt.questions.len(), 1);
1205 }
1206
1207 #[test]
1208 fn deserialize_node_tool_use_finished_payload() {
1209 let json = r#"{"tool_use_id":"call_abc123","status":"succeeded","elapsed_time":1.42,"tool_name":"Web Search","tool_func_name":"web_search","tool_args":"{\"query\":\"TSLA stock news\"}","tool_type":"builtin","tips":"Searched the web","iteration":1,"is_thinking":true,"outputs":{"query":"TSLA stock news","references":[{"index":1,"title":"...","url":"..."}]}}"#;
1210 let payload: NodeToolUseFinishedPayload = serde_json::from_str(json).unwrap();
1211 assert_eq!(payload.tool_use_id, "call_abc123");
1212 assert_eq!(payload.status, "succeeded");
1213 assert_eq!(payload.tool_func_name, "web_search");
1214 assert!(payload.is_thinking);
1215 assert_eq!(payload.outputs.query.as_deref(), Some("TSLA stock news"));
1216 assert_eq!(payload.outputs.references.as_ref().unwrap().len(), 1);
1217 }
1218
1219 #[test]
1220 fn deserialize_reference_with_nested_content() {
1221 let json = r#"{"type":"NewsArticle","id":"295354885","index":1,"original_index":10,"content":{"source":"智通财经","description":"Jefferies cut Tesla's target.","published_at":"2026-08-10T03:45:02Z","source_url":"https://example.com/a","title":""}}"#;
1225 let r: Reference = serde_json::from_str(json).unwrap();
1226 assert_eq!(r.index, 1);
1227 assert_eq!(r.original_index, 10);
1228 assert_eq!(r.ref_type, "NewsArticle");
1229 assert_eq!(r.id, "295354885");
1230 let content = r.content.expect("content");
1231 assert_eq!(content["source"], "智通财经");
1232 assert_eq!(content["published_at"], "2026-08-10T03:45:02Z");
1233 }
1234
1235 #[test]
1236 fn deserialize_reference_flat_shape_still_works() {
1237 let r: Reference = serde_json::from_str(r#"{"index":1,"title":"t","url":"u"}"#).unwrap();
1239 assert_eq!(r.index, 1);
1240 assert_eq!(r.title, "t");
1241 assert_eq!(r.url, "u");
1242 assert!(r.content.is_none());
1243 assert_eq!(r.ref_type, "");
1244 }
1245
1246 #[test]
1247 fn deserialize_plan_changed_payload_picks_up_sibling_tool_name() {
1248 let mut payload: PlanChangedPayload =
1249 serde_json::from_str(r#"{"node_id":"n_plan","started_at":1752048000}"#).unwrap();
1250 payload.tool_name = "planner".to_string();
1253 assert_eq!(payload.node_id, "n_plan");
1254 assert_eq!(payload.tool_name, "planner");
1255 }
1256
1257 #[test]
1258 fn deserialize_workspaces_response() {
1259 let json = r#"{
1260 "workspaces": [
1261 { "id": "1001", "name": "My Workspace", "created_at": 1742000000, "updated_at": 1742001000 }
1262 ]
1263 }"#;
1264 let resp: WorkspacesResponse = serde_json::from_str(json).unwrap();
1265 assert_eq!(resp.workspaces.len(), 1);
1266 assert_eq!(resp.workspaces[0].id, "1001");
1267 }
1268
1269 #[test]
1270 fn deserialize_agents_response() {
1271 let json = r#"{
1272 "agents": [
1273 {
1274 "uid": "ag_7d3f9b2c",
1275 "name": "US Stock Analyst",
1276 "description": "Answers US stock questions with market and fundamental data",
1277 "mode": "chat",
1278 "icon": "https://cdn.longbridge.com/icons/agent.png",
1279 "is_published": true,
1280 "published_at": 1742000000,
1281 "created_at": 1741000000,
1282 "updated_at": 1742001000
1283 }
1284 ],
1285 "total": 12
1286 }"#;
1287 let resp: AgentsResponse = serde_json::from_str(json).unwrap();
1288 assert_eq!(resp.total, 12);
1289 assert_eq!(resp.agents[0].uid, "ag_7d3f9b2c");
1290 assert!(resp.agents[0].is_published);
1291 }
1292
1293 #[test]
1294 fn deserialize_interrupt_treats_null_questions_as_empty() {
1295 let interrupt: Interrupt = serde_json::from_str(
1296 r#"{"node_id":"approval","tool_call_id":"call-1","questions":null}"#,
1297 )
1298 .unwrap();
1299 assert!(interrupt.questions.is_empty());
1300 }
1301
1302 #[test]
1303 fn deserialize_question_treats_null_options_as_empty() {
1304 let question: Question =
1305 serde_json::from_str(r#"{"question":"Continue?","options":null}"#).unwrap();
1306 assert!(question.options.is_empty());
1307 }
1308
1309 #[test]
1310 fn deserialize_human_interaction_preserves_labels_and_answer_keys() {
1311 let interrupt: Interrupt = serde_json::from_str(
1312 r#"{
1313 "node_id":"ask",
1314 "tool_call_id":"call-1",
1315 "questions":[],
1316 "interactions":[{
1317 "tool_call_id":"call-1",
1318 "interrupt_id":"call-1",
1319 "type":"ask_human",
1320 "tool_name":"AskHuman",
1321 "questions":[{
1322 "question":"从哪个方向开始?",
1323 "options":[{"label":"看行情","description":"比较当前价格"}],
1324 "multi_select":false
1325 }],
1326 "tool_args":{}
1327 }]
1328 }"#,
1329 )
1330 .unwrap();
1331 assert_eq!(interrupt.interactions[0].interrupt_id, "call-1");
1332 assert_eq!(
1333 interrupt.interactions[0].questions[0].options[0].label,
1334 "看行情"
1335 );
1336 assert_eq!(
1337 interrupt.interactions[0].questions[0].options[0].description,
1338 "比较当前价格"
1339 );
1340 }
1341}