1use crate::model::LlmResponse;
2use crate::types::Content;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8pub const KEY_PREFIX_APP: &str = "app:";
10pub const KEY_PREFIX_TEMP: &str = "temp:";
11pub const KEY_PREFIX_USER: &str = "user:";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Event {
17 pub id: String,
18 pub timestamp: DateTime<Utc>,
19 pub invocation_id: String,
20 #[serde(rename = "invocationId")]
22 pub invocation_id_camel: String,
23 pub branch: String,
24 pub author: String,
25 #[serde(flatten)]
28 pub llm_response: LlmResponse,
29 pub actions: EventActions,
30 #[serde(default)]
32 pub long_running_tool_ids: Vec<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub llm_request: Option<String>,
36 #[serde(rename = "gcp.vertex.agent.llm_request", skip_serializing_if = "Option::is_none")]
38 pub gcp_llm_request: Option<String>,
39 #[serde(rename = "gcp.vertex.agent.llm_response", skip_serializing_if = "Option::is_none")]
41 pub gcp_llm_response: Option<String>,
42}
43
44#[derive(Debug, Clone, Default, Serialize, Deserialize)]
45pub struct EventActions {
46 pub state_delta: HashMap<String, serde_json::Value>,
47 pub artifact_delta: HashMap<String, i64>,
48 pub skip_summarization: bool,
49 pub transfer_to_agent: Option<String>,
50 pub escalate: bool,
51}
52
53impl Event {
54 pub fn new(invocation_id: impl Into<String>) -> Self {
55 let invocation_id = invocation_id.into();
56 Self {
57 id: Uuid::new_v4().to_string(),
58 timestamp: Utc::now(),
59 invocation_id: invocation_id.clone(),
60 invocation_id_camel: invocation_id,
61 branch: String::new(),
62 author: String::new(),
63 llm_response: LlmResponse::default(),
64 actions: EventActions::default(),
65 long_running_tool_ids: Vec::new(),
66 llm_request: None,
67 gcp_llm_request: None,
68 gcp_llm_response: None,
69 }
70 }
71
72 pub fn with_id(id: impl Into<String>, invocation_id: impl Into<String>) -> Self {
75 let invocation_id = invocation_id.into();
76 Self {
77 id: id.into(),
78 timestamp: Utc::now(),
79 invocation_id: invocation_id.clone(),
80 invocation_id_camel: invocation_id,
81 branch: String::new(),
82 author: String::new(),
83 llm_response: LlmResponse::default(),
84 actions: EventActions::default(),
85 long_running_tool_ids: Vec::new(),
86 llm_request: None,
87 gcp_llm_request: None,
88 gcp_llm_response: None,
89 }
90 }
91
92 pub fn content(&self) -> Option<&Content> {
94 self.llm_response.content.as_ref()
95 }
96
97 pub fn set_content(&mut self, content: Content) {
99 self.llm_response.content = Some(content);
100 }
101
102 pub fn is_final_response(&self) -> bool {
113 if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
115 return true;
116 }
117
118 let has_function_calls = self.has_function_calls();
120 let has_function_responses = self.has_function_responses();
121 let is_partial = self.llm_response.partial;
122 let has_trailing_code_result = self.has_trailing_code_execution_result();
123
124 !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
125 }
126
127 fn has_function_calls(&self) -> bool {
129 if let Some(content) = &self.llm_response.content {
130 for part in &content.parts {
131 if matches!(part, crate::Part::FunctionCall { .. }) {
132 return true;
133 }
134 }
135 }
136 false
137 }
138
139 fn has_function_responses(&self) -> bool {
141 if let Some(content) = &self.llm_response.content {
142 for part in &content.parts {
143 if matches!(part, crate::Part::FunctionResponse { .. }) {
144 return true;
145 }
146 }
147 }
148 false
149 }
150
151 #[allow(clippy::match_like_matches_macro)]
155 fn has_trailing_code_execution_result(&self) -> bool {
156 false
163 }
164
165 pub fn function_call_ids(&self) -> Vec<String> {
168 let mut ids = Vec::new();
169 if let Some(content) = &self.llm_response.content {
170 for part in &content.parts {
171 if let crate::Part::FunctionCall { name, .. } = part {
172 ids.push(name.clone());
174 }
175 }
176 }
177 ids
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::Part;
185
186 #[test]
187 fn test_event_creation() {
188 let event = Event::new("inv-123");
189 assert_eq!(event.invocation_id, "inv-123");
190 assert!(!event.id.is_empty());
191 }
192
193 #[test]
194 fn test_event_actions_default() {
195 let actions = EventActions::default();
196 assert!(actions.state_delta.is_empty());
197 assert!(!actions.skip_summarization);
198 }
199
200 #[test]
201 fn test_state_prefixes() {
202 assert_eq!(KEY_PREFIX_APP, "app:");
203 assert_eq!(KEY_PREFIX_TEMP, "temp:");
204 assert_eq!(KEY_PREFIX_USER, "user:");
205 }
206
207 #[test]
208 fn test_is_final_response_no_content() {
209 let event = Event::new("inv-123");
210 assert!(event.is_final_response());
212 }
213
214 #[test]
215 fn test_is_final_response_text_only() {
216 let mut event = Event::new("inv-123");
217 event.llm_response.content = Some(Content {
218 role: "model".to_string(),
219 parts: vec![Part::Text { text: "Hello!".to_string() }],
220 });
221 assert!(event.is_final_response());
223 }
224
225 #[test]
226 fn test_is_final_response_with_function_call() {
227 let mut event = Event::new("inv-123");
228 event.llm_response.content = Some(Content {
229 role: "model".to_string(),
230 parts: vec![Part::FunctionCall {
231 name: "get_weather".to_string(),
232 args: serde_json::json!({"city": "NYC"}),
233 id: Some("call_123".to_string()),
234 }],
235 });
236 assert!(!event.is_final_response());
238 }
239
240 #[test]
241 fn test_is_final_response_with_function_response() {
242 let mut event = Event::new("inv-123");
243 event.llm_response.content = Some(Content {
244 role: "function".to_string(),
245 parts: vec![Part::FunctionResponse {
246 function_response: crate::FunctionResponseData {
247 name: "get_weather".to_string(),
248 response: serde_json::json!({"temp": 72}),
249 },
250 id: Some("call_123".to_string()),
251 }],
252 });
253 assert!(!event.is_final_response());
255 }
256
257 #[test]
258 fn test_is_final_response_partial() {
259 let mut event = Event::new("inv-123");
260 event.llm_response.partial = true;
261 event.llm_response.content = Some(Content {
262 role: "model".to_string(),
263 parts: vec![Part::Text { text: "Hello...".to_string() }],
264 });
265 assert!(!event.is_final_response());
267 }
268
269 #[test]
270 fn test_is_final_response_skip_summarization() {
271 let mut event = Event::new("inv-123");
272 event.actions.skip_summarization = true;
273 event.llm_response.content = Some(Content {
274 role: "function".to_string(),
275 parts: vec![Part::FunctionResponse {
276 function_response: crate::FunctionResponseData {
277 name: "tool".to_string(),
278 response: serde_json::json!({"result": "done"}),
279 },
280 id: Some("call_tool".to_string()),
281 }],
282 });
283 assert!(event.is_final_response());
285 }
286
287 #[test]
288 fn test_is_final_response_long_running_tool_ids() {
289 let mut event = Event::new("inv-123");
290 event.long_running_tool_ids = vec!["process_video".to_string()];
291 event.llm_response.content = Some(Content {
292 role: "model".to_string(),
293 parts: vec![Part::FunctionCall {
294 name: "process_video".to_string(),
295 args: serde_json::json!({"file": "video.mp4"}),
296 id: Some("call_process".to_string()),
297 }],
298 });
299 assert!(event.is_final_response());
301 }
302
303 #[test]
304 fn test_function_call_ids() {
305 let mut event = Event::new("inv-123");
306 event.llm_response.content = Some(Content {
307 role: "model".to_string(),
308 parts: vec![
309 Part::FunctionCall {
310 name: "get_weather".to_string(),
311 args: serde_json::json!({}),
312 id: Some("call_1".to_string()),
313 },
314 Part::Text { text: "I'll check the weather".to_string() },
315 Part::FunctionCall {
316 name: "get_time".to_string(),
317 args: serde_json::json!({}),
318 id: Some("call_2".to_string()),
319 },
320 ],
321 });
322
323 let ids = event.function_call_ids();
324 assert_eq!(ids.len(), 2);
325 assert!(ids.contains(&"get_weather".to_string()));
326 assert!(ids.contains(&"get_time".to_string()));
327 }
328
329 #[test]
330 fn test_function_call_ids_empty() {
331 let event = Event::new("inv-123");
332 let ids = event.function_call_ids();
333 assert!(ids.is_empty());
334 }
335}