1use objectiveai_sdk::agent::completions::message::{File, ImageUrl, InputAudio, VideoUrl};
43use serde::Serialize;
44
45use crate::db::{Error, Pool};
46
47use super::row::{MessageTable, RowValue};
48use super::shadow::WriteOp;
49
50pub async fn update_agent_token_usage(
53 pool: &Pool,
54 agent_instance_hierarchy: &str,
55 total_tokens: i64,
56) -> Result<(), Error> {
57 sqlx::query(
58 "INSERT INTO objectiveai.agent_token_usage \
59 (agent_instance_hierarchy, total_tokens) \
60 VALUES ($1, $2) \
61 ON CONFLICT (agent_instance_hierarchy) \
62 DO UPDATE SET total_tokens = EXCLUDED.total_tokens",
63 )
64 .bind(agent_instance_hierarchy)
65 .bind(total_tokens)
66 .execute(&**pool)
67 .await?;
68 Ok(())
69}
70
71pub async fn write_value<'a>(
73 pool: &Pool,
74 op: WriteOp,
75 value: &RowValue<'a>,
76 timestamp: i64,
77) -> Result<(), Error> {
78 match op {
79 WriteOp::Skip => Ok(()),
80 WriteOp::Insert => insert_value(pool, value, timestamp).await,
81 WriteOp::Update => update_value(pool, value).await,
82 }
83}
84
85async fn insert_value<'a>(
86 pool: &Pool,
87 value: &RowValue<'a>,
88 timestamp: i64,
89) -> Result<(), Error> {
90 if let RowValue::MessageQueueContent {
96 response_id,
97 agent_instance_hierarchy,
98 message_queue_content_id,
99 } = *value
100 {
101 return insert_message_queue_content_with_msg(
102 pool,
103 response_id,
104 agent_instance_hierarchy,
105 message_queue_content_id,
106 timestamp,
107 )
108 .await;
109 }
110
111 if let RowValue::ToolResponse { response_id, index, tool_call_id, .. } = *value {
117 sqlx::query(
118 "INSERT INTO objectiveai.tool_response (response_id, \"index\", tool_call_id) \
119 VALUES ($1, $2, $3)",
120 )
121 .bind(response_id)
122 .bind(index as i64)
123 .bind(tool_call_id)
124 .execute(&**pool)
125 .await?;
126 return Ok(());
127 }
128
129 if let RowValue::RequestMessageTool { response_id, index, tool_call_id, .. } = *value {
132 sqlx::query(
133 "INSERT INTO objectiveai.request_message_tool (response_id, \"index\", tool_call_id) \
134 VALUES ($1, $2, $3)",
135 )
136 .bind(response_id)
137 .bind(index as i64)
138 .bind(tool_call_id)
139 .execute(&**pool)
140 .await?;
141 return Ok(());
142 }
143
144 if let RowValue::RequestVectorChoice { response_id, choice_index, key, .. } = *value {
147 sqlx::query(
148 "INSERT INTO objectiveai.request_vector_choice (response_id, \"index\", key) \
149 VALUES ($1, $2, $3)",
150 )
151 .bind(response_id)
152 .bind(choice_index as i64)
153 .bind(key)
154 .execute(&**pool)
155 .await?;
156 return Ok(());
157 }
158
159 if let RowValue::ResponseVectorVote { response_id, agent_instance_hierarchy, vote } = *value {
163 sqlx::query(
164 "WITH data_ins AS (\
165 INSERT INTO objectiveai.response_vector_vote (response_id, agent_instance_hierarchy, vote) \
166 VALUES ($1, $2, $3) RETURNING response_id\
167 )\
168 INSERT INTO objectiveai.messages \
169 (response_id, \"table\", row_index, row_sub_index, \
170 agent_instance_hierarchy, \"timestamp\") \
171 SELECT $1, $4, NULL, NULL, $2, $5 FROM data_ins",
172 )
173 .bind(response_id)
174 .bind(agent_instance_hierarchy)
175 .bind(sqlx::types::Json(serde_json::to_value(vote)?))
176 .bind(MessageTable::ResponseVectorVote)
177 .bind(timestamp)
178 .execute(&**pool)
179 .await?;
180 return Ok(());
181 }
182
183 let mt = value.message_table();
184 let hier = value.agent_instance_hierarchy();
185 let row_index = value.row_index();
186 let row_sub_index = value.row_sub_index();
187 let response_id = value.response_id();
188
189 match *value {
190 RowValue::MessageQueueContent { .. } => unreachable!(
191 "MessageQueueContent handled by early-return branch above"
192 ),
193 RowValue::ToolResponse { .. } => unreachable!(
194 "ToolResponse handled by early-return branch above"
195 ),
196 RowValue::AssistantResponseRefusal { text, .. } => {
197 sqlx::query(
198 "WITH data_ins AS (\
199 INSERT INTO objectiveai.assistant_response_refusal (response_id, \"index\", text) \
200 VALUES ($1, $2, $3) RETURNING response_id\
201 )\
202 INSERT INTO objectiveai.messages \
203 (response_id, \"table\", row_index, row_sub_index, \
204 agent_instance_hierarchy, \"timestamp\") \
205 SELECT $1, $4, $5, $6, $7, $8 FROM data_ins",
206 )
207 .bind(response_id)
208 .bind(row_index)
209 .bind(text)
210 .bind(mt)
211 .bind(row_index)
212 .bind(row_sub_index)
213 .bind(hier)
214 .bind(timestamp)
215 .execute(&**pool)
216 .await?;
217 }
218 RowValue::AssistantResponseReasoning { text, .. } => {
219 sqlx::query(
220 "WITH data_ins AS (\
221 INSERT INTO objectiveai.assistant_response_reasoning (response_id, \"index\", text) \
222 VALUES ($1, $2, $3) RETURNING response_id\
223 )\
224 INSERT INTO objectiveai.messages \
225 (response_id, \"table\", row_index, row_sub_index, \
226 agent_instance_hierarchy, \"timestamp\") \
227 SELECT $1, $4, $5, $6, $7, $8 FROM data_ins",
228 )
229 .bind(response_id)
230 .bind(row_index)
231 .bind(text)
232 .bind(mt)
233 .bind(row_index)
234 .bind(row_sub_index)
235 .bind(hier)
236 .bind(timestamp)
237 .execute(&**pool)
238 .await?;
239 }
240 RowValue::AssistantResponseToolCalls {
241 tool_call_index, tool_call_id, function_name, arguments, ..
242 } => {
243 sqlx::query(
244 "WITH data_ins AS (\
245 INSERT INTO objectiveai.assistant_response_tool_calls \
246 (response_id, \"index\", tool_call_index, tool_call_id, function_name, arguments) \
247 VALUES ($1, $2, $3, $4, $5, $6) RETURNING response_id\
248 )\
249 INSERT INTO objectiveai.messages \
250 (response_id, \"table\", row_index, row_sub_index, \
251 agent_instance_hierarchy, \"timestamp\") \
252 SELECT $1, $7, $8, $9, $10, $11 FROM data_ins",
253 )
254 .bind(response_id)
255 .bind(row_index)
256 .bind(tool_call_index as i64)
257 .bind(tool_call_id)
258 .bind(function_name)
259 .bind(arguments)
260 .bind(mt)
261 .bind(row_index)
262 .bind(row_sub_index)
263 .bind(hier)
264 .bind(timestamp)
265 .execute(&**pool)
266 .await?;
267 }
268 RowValue::AssistantResponseContentText { text, .. } => {
269 insert_text_part_with_msg(pool, "objectiveai.assistant_response_content_text", value, text, timestamp).await?;
270 }
271 RowValue::ToolResponseContentText { text, .. } => {
272 insert_text_part_with_msg(pool, "objectiveai.tool_response_content_text", value, text, timestamp).await?;
273 }
274 RowValue::AssistantResponseContentImage { image_url, .. } => {
275 insert_image_part_with_msg(pool, "objectiveai.assistant_response_content_image", value, image_url, timestamp).await?;
276 }
277 RowValue::ToolResponseContentImage { image_url, .. } => {
278 insert_image_part_with_msg(pool, "objectiveai.tool_response_content_image", value, image_url, timestamp).await?;
279 }
280 RowValue::AssistantResponseContentAudio { input_audio, .. } => {
281 insert_audio_part_with_msg(pool, "objectiveai.assistant_response_content_audio", value, input_audio, timestamp).await?;
282 }
283 RowValue::ToolResponseContentAudio { input_audio, .. } => {
284 insert_audio_part_with_msg(pool, "objectiveai.tool_response_content_audio", value, input_audio, timestamp).await?;
285 }
286 RowValue::AssistantResponseContentVideo { video_url, .. } => {
287 insert_video_part_with_msg(pool, "objectiveai.assistant_response_content_video", value, video_url, timestamp).await?;
288 }
289 RowValue::ToolResponseContentVideo { video_url, .. } => {
290 insert_video_part_with_msg(pool, "objectiveai.tool_response_content_video", value, video_url, timestamp).await?;
291 }
292 RowValue::AssistantResponseContentFile { file, .. } => {
293 insert_file_part_with_msg(pool, "objectiveai.assistant_response_content_file", value, file, timestamp).await?;
294 }
295 RowValue::ToolResponseContentFile { file, .. } => {
296 insert_file_part_with_msg(pool, "objectiveai.tool_response_content_file", value, file, timestamp).await?;
297 }
298 RowValue::RequestMessageTool { .. }
299 | RowValue::RequestVectorChoice { .. }
300 | RowValue::ResponseVectorVote { .. } => unreachable!(
301 "RequestMessageTool / RequestVectorChoice / ResponseVectorVote handled by early-return branches above"
302 ),
303 RowValue::RequestMessageAssistantRefusal { text, .. } => {
305 sqlx::query(
306 "WITH data_ins AS (\
307 INSERT INTO objectiveai.request_message_assistant_refusal (response_id, \"index\", text) \
308 VALUES ($1, $2, $3) RETURNING response_id\
309 )\
310 INSERT INTO objectiveai.messages \
311 (response_id, \"table\", row_index, row_sub_index, \
312 agent_instance_hierarchy, \"timestamp\") \
313 SELECT $1, $4, $5, $6, $7, $8 FROM data_ins",
314 )
315 .bind(response_id).bind(row_index).bind(text)
316 .bind(mt).bind(row_index).bind(row_sub_index).bind(hier).bind(timestamp)
317 .execute(&**pool).await?;
318 }
319 RowValue::RequestMessageAssistantReasoning { text, .. } => {
320 sqlx::query(
321 "WITH data_ins AS (\
322 INSERT INTO objectiveai.request_message_assistant_reasoning (response_id, \"index\", text) \
323 VALUES ($1, $2, $3) RETURNING response_id\
324 )\
325 INSERT INTO objectiveai.messages \
326 (response_id, \"table\", row_index, row_sub_index, \
327 agent_instance_hierarchy, \"timestamp\") \
328 SELECT $1, $4, $5, $6, $7, $8 FROM data_ins",
329 )
330 .bind(response_id).bind(row_index).bind(text)
331 .bind(mt).bind(row_index).bind(row_sub_index).bind(hier).bind(timestamp)
332 .execute(&**pool).await?;
333 }
334 RowValue::RequestMessageAssistantToolCalls {
335 tool_call_index, tool_call_id, function_name, arguments, ..
336 } => {
337 sqlx::query(
338 "WITH data_ins AS (\
339 INSERT INTO objectiveai.request_message_assistant_tool_calls \
340 (response_id, \"index\", tool_call_index, tool_call_id, function_name, arguments) \
341 VALUES ($1, $2, $3, $4, $5, $6) RETURNING response_id\
342 )\
343 INSERT INTO objectiveai.messages \
344 (response_id, \"table\", row_index, row_sub_index, \
345 agent_instance_hierarchy, \"timestamp\") \
346 SELECT $1, $7, $8, $9, $10, $11 FROM data_ins",
347 )
348 .bind(response_id).bind(row_index).bind(tool_call_index as i64)
349 .bind(tool_call_id).bind(function_name).bind(arguments)
350 .bind(mt).bind(row_index).bind(row_sub_index).bind(hier).bind(timestamp)
351 .execute(&**pool).await?;
352 }
353 RowValue::RequestMessageUserContentText { text, .. } => {
355 insert_text_part_with_msg(pool, "objectiveai.request_message_user_content_text", value, text, timestamp).await?;
356 }
357 RowValue::RequestMessageUserContentImage { image_url, .. } => {
358 insert_image_part_with_msg(pool, "objectiveai.request_message_user_content_image", value, image_url, timestamp).await?;
359 }
360 RowValue::RequestMessageUserContentAudio { input_audio, .. } => {
361 insert_audio_part_with_msg(pool, "objectiveai.request_message_user_content_audio", value, input_audio, timestamp).await?;
362 }
363 RowValue::RequestMessageUserContentVideo { video_url, .. } => {
364 insert_video_part_with_msg(pool, "objectiveai.request_message_user_content_video", value, video_url, timestamp).await?;
365 }
366 RowValue::RequestMessageUserContentFile { file, .. } => {
367 insert_file_part_with_msg(pool, "objectiveai.request_message_user_content_file", value, file, timestamp).await?;
368 }
369 RowValue::RequestMessageAssistantContentText { text, .. } => {
370 insert_text_part_with_msg(pool, "objectiveai.request_message_assistant_content_text", value, text, timestamp).await?;
371 }
372 RowValue::RequestMessageAssistantContentImage { image_url, .. } => {
373 insert_image_part_with_msg(pool, "objectiveai.request_message_assistant_content_image", value, image_url, timestamp).await?;
374 }
375 RowValue::RequestMessageAssistantContentAudio { input_audio, .. } => {
376 insert_audio_part_with_msg(pool, "objectiveai.request_message_assistant_content_audio", value, input_audio, timestamp).await?;
377 }
378 RowValue::RequestMessageAssistantContentVideo { video_url, .. } => {
379 insert_video_part_with_msg(pool, "objectiveai.request_message_assistant_content_video", value, video_url, timestamp).await?;
380 }
381 RowValue::RequestMessageAssistantContentFile { file, .. } => {
382 insert_file_part_with_msg(pool, "objectiveai.request_message_assistant_content_file", value, file, timestamp).await?;
383 }
384 RowValue::RequestMessageToolContentText { text, .. } => {
385 insert_text_part_with_msg(pool, "objectiveai.request_message_tool_content_text", value, text, timestamp).await?;
386 }
387 RowValue::RequestMessageToolContentImage { image_url, .. } => {
388 insert_image_part_with_msg(pool, "objectiveai.request_message_tool_content_image", value, image_url, timestamp).await?;
389 }
390 RowValue::RequestMessageToolContentAudio { input_audio, .. } => {
391 insert_audio_part_with_msg(pool, "objectiveai.request_message_tool_content_audio", value, input_audio, timestamp).await?;
392 }
393 RowValue::RequestMessageToolContentVideo { video_url, .. } => {
394 insert_video_part_with_msg(pool, "objectiveai.request_message_tool_content_video", value, video_url, timestamp).await?;
395 }
396 RowValue::RequestMessageToolContentFile { file, .. } => {
397 insert_file_part_with_msg(pool, "objectiveai.request_message_tool_content_file", value, file, timestamp).await?;
398 }
399 RowValue::RequestVectorChoiceContentText { text, .. } => {
400 insert_text_part_with_msg(pool, "objectiveai.request_vector_choice_content_text", value, text, timestamp).await?;
401 }
402 RowValue::RequestVectorChoiceContentImage { image_url, .. } => {
403 insert_image_part_with_msg(pool, "objectiveai.request_vector_choice_content_image", value, image_url, timestamp).await?;
404 }
405 RowValue::RequestVectorChoiceContentAudio { input_audio, .. } => {
406 insert_audio_part_with_msg(pool, "objectiveai.request_vector_choice_content_audio", value, input_audio, timestamp).await?;
407 }
408 RowValue::RequestVectorChoiceContentVideo { video_url, .. } => {
409 insert_video_part_with_msg(pool, "objectiveai.request_vector_choice_content_video", value, video_url, timestamp).await?;
410 }
411 RowValue::RequestVectorChoiceContentFile { file, .. } => {
412 insert_file_part_with_msg(pool, "objectiveai.request_vector_choice_content_file", value, file, timestamp).await?;
413 }
414 }
415 Ok(())
416}
417
418async fn update_value<'a>(pool: &Pool, value: &RowValue<'a>) -> Result<(), Error> {
419 if matches!(value, RowValue::MessageQueueContent { .. }) {
423 return Ok(());
424 }
425
426 if let RowValue::ToolResponse { response_id, index, tool_call_id, .. } = *value {
434 sqlx::query(
435 "UPDATE objectiveai.tool_response SET tool_call_id = $1 \
436 WHERE response_id = $2 AND \"index\" = $3",
437 )
438 .bind(tool_call_id)
439 .bind(response_id)
440 .bind(index as i64)
441 .execute(&**pool)
442 .await?;
443 return Ok(());
444 }
445
446 if let RowValue::RequestMessageTool { response_id, index, tool_call_id, .. } = *value {
450 sqlx::query(
451 "UPDATE objectiveai.request_message_tool SET tool_call_id = $1 \
452 WHERE response_id = $2 AND \"index\" = $3",
453 )
454 .bind(tool_call_id).bind(response_id).bind(index as i64)
455 .execute(&**pool).await?;
456 return Ok(());
457 }
458 if let RowValue::RequestVectorChoice { response_id, choice_index, key, .. } = *value {
459 sqlx::query(
460 "UPDATE objectiveai.request_vector_choice SET key = $1 \
461 WHERE response_id = $2 AND \"index\" = $3",
462 )
463 .bind(key).bind(response_id).bind(choice_index as i64)
464 .execute(&**pool).await?;
465 return Ok(());
466 }
467 if let RowValue::ResponseVectorVote { response_id, agent_instance_hierarchy, vote } = *value {
468 sqlx::query(
469 "UPDATE objectiveai.response_vector_vote SET vote = $1 \
470 WHERE response_id = $2 AND agent_instance_hierarchy = $3",
471 )
472 .bind(sqlx::types::Json(serde_json::to_value(vote)?))
473 .bind(response_id).bind(agent_instance_hierarchy)
474 .execute(&**pool).await?;
475 return Ok(());
476 }
477
478 let mt = value.message_table();
479 let hier = value.agent_instance_hierarchy();
480 let row_index = value.row_index();
481 let row_sub_index = value.row_sub_index();
482 let response_id = value.response_id();
483
484 match *value {
485 RowValue::MessageQueueContent { .. } => unreachable!(
486 "MessageQueueContent handled by short-circuit above"
487 ),
488 RowValue::ToolResponse { .. } => unreachable!(
489 "ToolResponse handled by short-circuit above"
490 ),
491 RowValue::AssistantResponseRefusal { text, .. }
492 | RowValue::RequestMessageAssistantRefusal { text, .. } => {
493 let table = match *value {
494 RowValue::AssistantResponseRefusal { .. } => "objectiveai.assistant_response_refusal",
495 _ => "objectiveai.request_message_assistant_refusal",
496 };
497 let sql = format!("UPDATE {table} SET text = $A WHERE response_id = $RESP AND \"index\" = $RI");
498 run_update_with_downgrade(
499 pool, &sql,
500 response_id, row_index, row_sub_index, mt, hier,
501 &[("A", BindVal::Str(text))],
502 &[BindIdx::Resp, BindIdx::Ri],
503 ).await?;
504 }
505 RowValue::AssistantResponseReasoning { text, .. }
506 | RowValue::RequestMessageAssistantReasoning { text, .. } => {
507 let table = match *value {
508 RowValue::AssistantResponseReasoning { .. } => "objectiveai.assistant_response_reasoning",
509 _ => "objectiveai.request_message_assistant_reasoning",
510 };
511 let sql = format!("UPDATE {table} SET text = $A WHERE response_id = $RESP AND \"index\" = $RI");
512 run_update_with_downgrade(
513 pool, &sql,
514 response_id, row_index, row_sub_index, mt, hier,
515 &[("A", BindVal::Str(text))],
516 &[BindIdx::Resp, BindIdx::Ri],
517 ).await?;
518 }
519 RowValue::AssistantResponseToolCalls { tool_call_index, tool_call_id, function_name, arguments, .. }
520 | RowValue::RequestMessageAssistantToolCalls { tool_call_index, tool_call_id, function_name, arguments, .. } => {
521 let table = match *value {
522 RowValue::AssistantResponseToolCalls { .. } => "objectiveai.assistant_response_tool_calls",
523 _ => "objectiveai.request_message_assistant_tool_calls",
524 };
525 let sql = format!(
526 "UPDATE {table} SET tool_call_id = $A, function_name = $B, arguments = $C \
527 WHERE response_id = $RESP AND \"index\" = $RI AND tool_call_index = $RSI"
528 );
529 run_update_with_downgrade(
530 pool, &sql,
531 response_id, row_index, row_sub_index, mt, hier,
532 &[("A", BindVal::Str(tool_call_id)), ("B", BindVal::Str(function_name)), ("C", BindVal::Str(arguments))],
533 &[BindIdx::Resp, BindIdx::Ri, BindIdx::Rsi],
534 ).await?;
535 let _ = tool_call_index;
536 }
537 RowValue::AssistantResponseContentText { text, .. }
538 | RowValue::ToolResponseContentText { text, .. }
539 | RowValue::RequestMessageUserContentText { text, .. }
540 | RowValue::RequestMessageAssistantContentText { text, .. }
541 | RowValue::RequestMessageToolContentText { text, .. }
542 | RowValue::RequestVectorChoiceContentText { text, .. } => {
543 let table = match *value {
544 RowValue::AssistantResponseContentText { .. } => "objectiveai.assistant_response_content_text",
545 RowValue::ToolResponseContentText { .. } => "objectiveai.tool_response_content_text",
546 RowValue::RequestMessageUserContentText { .. } => "objectiveai.request_message_user_content_text",
547 RowValue::RequestMessageAssistantContentText { .. } => "objectiveai.request_message_assistant_content_text",
548 RowValue::RequestMessageToolContentText { .. } => "objectiveai.request_message_tool_content_text",
549 _ => "objectiveai.request_vector_choice_content_text",
550 };
551 let sql = format!(
552 "UPDATE {table} SET text = $A \
553 WHERE response_id = $RESP AND \"index\" = $RI AND part_index = $RSI"
554 );
555 run_update_with_downgrade(
556 pool, &sql,
557 response_id, row_index, row_sub_index, mt, hier,
558 &[("A", BindVal::Str(text))],
559 &[BindIdx::Resp, BindIdx::Ri, BindIdx::Rsi],
560 ).await?;
561 }
562 RowValue::AssistantResponseContentImage { image_url, .. }
563 | RowValue::ToolResponseContentImage { image_url, .. }
564 | RowValue::RequestMessageUserContentImage { image_url, .. }
565 | RowValue::RequestMessageAssistantContentImage { image_url, .. }
566 | RowValue::RequestMessageToolContentImage { image_url, .. }
567 | RowValue::RequestVectorChoiceContentImage { image_url, .. } => {
568 let table = match *value {
569 RowValue::AssistantResponseContentImage { .. } => "objectiveai.assistant_response_content_image",
570 RowValue::ToolResponseContentImage { .. } => "objectiveai.tool_response_content_image",
571 RowValue::RequestMessageUserContentImage { .. } => "objectiveai.request_message_user_content_image",
572 RowValue::RequestMessageAssistantContentImage { .. } => "objectiveai.request_message_assistant_content_image",
573 RowValue::RequestMessageToolContentImage { .. } => "objectiveai.request_message_tool_content_image",
574 _ => "objectiveai.request_vector_choice_content_image",
575 };
576 let detail = image_url.detail.as_ref().and_then(|d| serde_json::to_string(d).ok());
577 let sql = format!(
578 "UPDATE {table} SET url = $A, detail = $B \
579 WHERE response_id = $RESP AND \"index\" = $RI AND part_index = $RSI"
580 );
581 run_update_with_downgrade(
582 pool, &sql,
583 response_id, row_index, row_sub_index, mt, hier,
584 &[("A", BindVal::Str(image_url.url.as_str())), ("B", BindVal::OptString(detail))],
585 &[BindIdx::Resp, BindIdx::Ri, BindIdx::Rsi],
586 ).await?;
587 }
588 RowValue::AssistantResponseContentAudio { input_audio, .. }
589 | RowValue::ToolResponseContentAudio { input_audio, .. }
590 | RowValue::RequestMessageUserContentAudio { input_audio, .. }
591 | RowValue::RequestMessageAssistantContentAudio { input_audio, .. }
592 | RowValue::RequestMessageToolContentAudio { input_audio, .. }
593 | RowValue::RequestVectorChoiceContentAudio { input_audio, .. } => {
594 let table = match *value {
595 RowValue::AssistantResponseContentAudio { .. } => "objectiveai.assistant_response_content_audio",
596 RowValue::ToolResponseContentAudio { .. } => "objectiveai.tool_response_content_audio",
597 RowValue::RequestMessageUserContentAudio { .. } => "objectiveai.request_message_user_content_audio",
598 RowValue::RequestMessageAssistantContentAudio { .. } => "objectiveai.request_message_assistant_content_audio",
599 RowValue::RequestMessageToolContentAudio { .. } => "objectiveai.request_message_tool_content_audio",
600 _ => "objectiveai.request_vector_choice_content_audio",
601 };
602 let sql = format!(
603 "UPDATE {table} SET data = $A, format = $B \
604 WHERE response_id = $RESP AND \"index\" = $RI AND part_index = $RSI"
605 );
606 run_update_with_downgrade(
607 pool, &sql,
608 response_id, row_index, row_sub_index, mt, hier,
609 &[
610 ("A", BindVal::Str(input_audio.data.as_str())),
611 ("B", BindVal::Str(input_audio.format.as_str())),
612 ],
613 &[BindIdx::Resp, BindIdx::Ri, BindIdx::Rsi],
614 ).await?;
615 }
616 RowValue::AssistantResponseContentVideo { video_url, .. }
617 | RowValue::ToolResponseContentVideo { video_url, .. }
618 | RowValue::RequestMessageUserContentVideo { video_url, .. }
619 | RowValue::RequestMessageAssistantContentVideo { video_url, .. }
620 | RowValue::RequestMessageToolContentVideo { video_url, .. }
621 | RowValue::RequestVectorChoiceContentVideo { video_url, .. } => {
622 let table = match *value {
623 RowValue::AssistantResponseContentVideo { .. } => "objectiveai.assistant_response_content_video",
624 RowValue::ToolResponseContentVideo { .. } => "objectiveai.tool_response_content_video",
625 RowValue::RequestMessageUserContentVideo { .. } => "objectiveai.request_message_user_content_video",
626 RowValue::RequestMessageAssistantContentVideo { .. } => "objectiveai.request_message_assistant_content_video",
627 RowValue::RequestMessageToolContentVideo { .. } => "objectiveai.request_message_tool_content_video",
628 _ => "objectiveai.request_vector_choice_content_video",
629 };
630 let sql = format!(
631 "UPDATE {table} SET url = $A \
632 WHERE response_id = $RESP AND \"index\" = $RI AND part_index = $RSI"
633 );
634 run_update_with_downgrade(
635 pool, &sql,
636 response_id, row_index, row_sub_index, mt, hier,
637 &[("A", BindVal::Str(video_url.url.as_str()))],
638 &[BindIdx::Resp, BindIdx::Ri, BindIdx::Rsi],
639 ).await?;
640 }
641 RowValue::AssistantResponseContentFile { file, .. }
642 | RowValue::ToolResponseContentFile { file, .. }
643 | RowValue::RequestMessageUserContentFile { file, .. }
644 | RowValue::RequestMessageAssistantContentFile { file, .. }
645 | RowValue::RequestMessageToolContentFile { file, .. }
646 | RowValue::RequestVectorChoiceContentFile { file, .. } => {
647 let table = match *value {
648 RowValue::AssistantResponseContentFile { .. } => "objectiveai.assistant_response_content_file",
649 RowValue::ToolResponseContentFile { .. } => "objectiveai.tool_response_content_file",
650 RowValue::RequestMessageUserContentFile { .. } => "objectiveai.request_message_user_content_file",
651 RowValue::RequestMessageAssistantContentFile { .. } => "objectiveai.request_message_assistant_content_file",
652 RowValue::RequestMessageToolContentFile { .. } => "objectiveai.request_message_tool_content_file",
653 _ => "objectiveai.request_vector_choice_content_file",
654 };
655 let sql = format!(
656 "UPDATE {table} SET file_data = $A, file_id = $B, filename = $C, file_url = $D \
657 WHERE response_id = $RESP AND \"index\" = $RI AND part_index = $RSI"
658 );
659 run_update_with_downgrade(
660 pool, &sql,
661 response_id, row_index, row_sub_index, mt, hier,
662 &[
663 ("A", BindVal::OptStr(file.file_data.as_deref())),
664 ("B", BindVal::OptStr(file.file_id.as_deref())),
665 ("C", BindVal::OptStr(file.filename.as_deref())),
666 ("D", BindVal::OptStr(file.file_url.as_deref())),
667 ],
668 &[BindIdx::Resp, BindIdx::Ri, BindIdx::Rsi],
669 ).await?;
670 }
671 RowValue::RequestMessageTool { .. }
672 | RowValue::RequestVectorChoice { .. }
673 | RowValue::ResponseVectorVote { .. } => unreachable!(
674 "RequestMessageTool / RequestVectorChoice / ResponseVectorVote handled by early-return branches above"
675 ),
676 }
677 Ok(())
678}
679
680async fn insert_text_part_with_msg<'a>(
683 pool: &Pool,
684 table: &str,
685 value: &RowValue<'a>,
686 text: &str,
687 timestamp: i64,
688) -> Result<(), Error> {
689 let sql = format!(
690 "WITH data_ins AS (\
691 INSERT INTO {table} (response_id, \"index\", part_index, text) \
692 VALUES ($1, $2, $3, $4) RETURNING response_id\
693 )\
694 INSERT INTO objectiveai.messages \
695 (response_id, \"table\", row_index, row_sub_index, \
696 agent_instance_hierarchy, \"timestamp\") \
697 SELECT $1, $5, $6, $7, $8, $9 FROM data_ins"
698 );
699 sqlx::query(&sql)
700 .bind(value.response_id())
701 .bind(value.row_index())
702 .bind(value.row_sub_index())
703 .bind(text)
704 .bind(value.message_table())
705 .bind(value.row_index())
706 .bind(value.row_sub_index())
707 .bind(value.agent_instance_hierarchy())
708 .bind(timestamp)
709 .execute(&**pool)
710 .await?;
711 Ok(())
712}
713
714async fn insert_image_part_with_msg<'a>(
715 pool: &Pool,
716 table: &str,
717 value: &RowValue<'a>,
718 image: &ImageUrl,
719 timestamp: i64,
720) -> Result<(), Error> {
721 let detail = image.detail.as_ref().and_then(|d| serde_json::to_string(d).ok());
722 let sql = format!(
723 "WITH data_ins AS (\
724 INSERT INTO {table} (response_id, \"index\", part_index, url, detail) \
725 VALUES ($1, $2, $3, $4, $5) RETURNING response_id\
726 )\
727 INSERT INTO objectiveai.messages \
728 (response_id, \"table\", row_index, row_sub_index, \
729 agent_instance_hierarchy, \"timestamp\") \
730 SELECT $1, $6, $7, $8, $9, $10 FROM data_ins"
731 );
732 sqlx::query(&sql)
733 .bind(value.response_id())
734 .bind(value.row_index())
735 .bind(value.row_sub_index())
736 .bind(image.url.as_str())
737 .bind(detail)
738 .bind(value.message_table())
739 .bind(value.row_index())
740 .bind(value.row_sub_index())
741 .bind(value.agent_instance_hierarchy())
742 .bind(timestamp)
743 .execute(&**pool)
744 .await?;
745 Ok(())
746}
747
748async fn insert_audio_part_with_msg<'a>(
749 pool: &Pool,
750 table: &str,
751 value: &RowValue<'a>,
752 audio: &InputAudio,
753 timestamp: i64,
754) -> Result<(), Error> {
755 let sql = format!(
756 "WITH data_ins AS (\
757 INSERT INTO {table} (response_id, \"index\", part_index, data, format) \
758 VALUES ($1, $2, $3, $4, $5) RETURNING response_id\
759 )\
760 INSERT INTO objectiveai.messages \
761 (response_id, \"table\", row_index, row_sub_index, \
762 agent_instance_hierarchy, \"timestamp\") \
763 SELECT $1, $6, $7, $8, $9, $10 FROM data_ins"
764 );
765 sqlx::query(&sql)
766 .bind(value.response_id())
767 .bind(value.row_index())
768 .bind(value.row_sub_index())
769 .bind(audio.data.as_str())
770 .bind(audio.format.as_str())
771 .bind(value.message_table())
772 .bind(value.row_index())
773 .bind(value.row_sub_index())
774 .bind(value.agent_instance_hierarchy())
775 .bind(timestamp)
776 .execute(&**pool)
777 .await?;
778 Ok(())
779}
780
781async fn insert_message_queue_content_with_msg(
795 pool: &Pool,
796 response_id: &str,
797 agent_instance_hierarchy: &str,
798 message_queue_content_id: i64,
799 timestamp: i64,
800) -> Result<(), Error> {
801 sqlx::query(
802 "WITH content AS (\
803 SELECT id, kind, message_queue_id \
804 FROM objectiveai.message_queue_contents \
805 WHERE id = $1 \
806 ), \
807 flip AS (\
808 UPDATE objectiveai.message_queue \
809 SET active = FALSE \
810 WHERE id = (SELECT message_queue_id FROM content) \
811 AND active = TRUE \
812 RETURNING id \
813 ) \
814 INSERT INTO objectiveai.messages \
815 (response_id, \"table\", row_index, row_sub_index, \
816 agent_instance_hierarchy, \"timestamp\") \
817 SELECT $2, \
818 CASE (SELECT kind FROM content) \
819 WHEN 'text' THEN 'message_queue_text'::objectiveai.message_table \
820 WHEN 'image' THEN 'message_queue_image'::objectiveai.message_table \
821 WHEN 'audio' THEN 'message_queue_audio'::objectiveai.message_table \
822 WHEN 'video' THEN 'message_queue_video'::objectiveai.message_table \
823 WHEN 'file' THEN 'message_queue_file'::objectiveai.message_table \
824 END, \
825 $1, NULL, $3, $4 \
826 FROM content",
827 )
828 .bind(message_queue_content_id)
829 .bind(response_id)
830 .bind(agent_instance_hierarchy)
831 .bind(timestamp)
832 .execute(&**pool)
833 .await?;
834 Ok(())
835}
836
837async fn insert_video_part_with_msg<'a>(
838 pool: &Pool,
839 table: &str,
840 value: &RowValue<'a>,
841 video: &VideoUrl,
842 timestamp: i64,
843) -> Result<(), Error> {
844 let sql = format!(
845 "WITH data_ins AS (\
846 INSERT INTO {table} (response_id, \"index\", part_index, url) \
847 VALUES ($1, $2, $3, $4) RETURNING response_id\
848 )\
849 INSERT INTO objectiveai.messages \
850 (response_id, \"table\", row_index, row_sub_index, \
851 agent_instance_hierarchy, \"timestamp\") \
852 SELECT $1, $5, $6, $7, $8, $9 FROM data_ins"
853 );
854 sqlx::query(&sql)
855 .bind(value.response_id())
856 .bind(value.row_index())
857 .bind(value.row_sub_index())
858 .bind(video.url.as_str())
859 .bind(value.message_table())
860 .bind(value.row_index())
861 .bind(value.row_sub_index())
862 .bind(value.agent_instance_hierarchy())
863 .bind(timestamp)
864 .execute(&**pool)
865 .await?;
866 Ok(())
867}
868
869async fn insert_file_part_with_msg<'a>(
870 pool: &Pool,
871 table: &str,
872 value: &RowValue<'a>,
873 file: &File,
874 timestamp: i64,
875) -> Result<(), Error> {
876 let sql = format!(
877 "WITH data_ins AS (\
878 INSERT INTO {table} (response_id, \"index\", part_index, file_data, file_id, filename, file_url) \
879 VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING response_id\
880 )\
881 INSERT INTO objectiveai.messages \
882 (response_id, \"table\", row_index, row_sub_index, \
883 agent_instance_hierarchy, \"timestamp\") \
884 SELECT $1, $8, $9, $10, $11, $12 FROM data_ins"
885 );
886 sqlx::query(&sql)
887 .bind(value.response_id())
888 .bind(value.row_index())
889 .bind(value.row_sub_index())
890 .bind(file.file_data.as_deref())
891 .bind(file.file_id.as_deref())
892 .bind(file.filename.as_deref())
893 .bind(file.file_url.as_deref())
894 .bind(value.message_table())
895 .bind(value.row_index())
896 .bind(value.row_sub_index())
897 .bind(value.agent_instance_hierarchy())
898 .bind(timestamp)
899 .execute(&**pool)
900 .await?;
901 Ok(())
902}
903
904#[derive(Clone, Copy)]
913enum BindIdx {
914 Resp,
915 Ri,
916 Rsi,
917}
918
919enum BindVal<'a> {
920 Str(&'a str),
921 OptStr(Option<&'a str>),
922 OptString(Option<String>),
923 Bool(bool),
924}
925
926#[allow(clippy::too_many_arguments)]
927async fn run_update_with_downgrade<'a>(
928 pool: &Pool,
929 update_sql_template: &str,
930 response_id: &str,
931 row_index: i64,
932 row_sub_index: Option<i64>,
933 message_table: MessageTable,
934 agent_instance_hierarchy: &str,
935 extra_binds: &[(&str, BindVal<'a>)],
936 update_where_binds: &[BindIdx],
937) -> Result<(), Error> {
938 let mut sql = update_sql_template.to_string();
947 let mut pos = 1usize;
948
949 let mut resp_pos: Option<usize> = None;
954 for slot in update_where_binds {
955 let idx = pos;
956 pos += 1;
957 match slot {
958 BindIdx::Resp => {
959 sql = sql.replace("$RESP", &format!("${idx}"));
960 resp_pos = Some(idx);
961 }
962 BindIdx::Ri => {
963 sql = sql.replace("$RI", &format!("${idx}"));
964 }
965 BindIdx::Rsi => {
966 sql = sql.replace("$RSI", &format!("${idx}"));
967 }
968 }
969 }
970 let resp_pos = resp_pos.expect("Resp bind required");
971
972 for (token, _val) in extra_binds {
974 let idx = pos;
975 pos += 1;
976 sql = sql.replace(&format!("${token}"), &format!("${idx}"));
977 }
978
979 let mt_pos = pos; pos += 1;
980 let ri_for_msg_pos = pos; pos += 1;
981 let rsi_for_msg_pos = pos; pos += 1;
982 let hier_pos = pos;
983
984 let final_sql = format!(
985 "WITH \
986 data_upd AS ({sql} RETURNING response_id),\
987 msg AS (\
988 SELECT \"index\" AS msg_index FROM objectiveai.messages \
989 WHERE response_id = ${resp_pos} \
990 AND \"table\" = ${mt_pos} \
991 AND row_index IS NOT DISTINCT FROM ${ri_for_msg_pos} \
992 AND row_sub_index IS NOT DISTINCT FROM ${rsi_for_msg_pos}\
993 )\
994 UPDATE objectiveai.messages_queue \
995 SET read_index = msg.msg_index - 1 \
996 FROM msg, data_upd \
997 WHERE spawned_agent_instance_hierarchy = ${hier_pos} \
998 AND read_index >= msg.msg_index",
999 );
1000
1001 let mut q = sqlx::query(&final_sql);
1002 for slot in update_where_binds {
1004 q = match slot {
1005 BindIdx::Resp => q.bind(response_id),
1006 BindIdx::Ri => q.bind(row_index),
1007 BindIdx::Rsi => q.bind(row_sub_index),
1008 };
1009 }
1010 for (_, val) in extra_binds {
1012 q = match val {
1013 BindVal::Str(s) => q.bind(*s),
1014 BindVal::OptStr(s) => q.bind(*s),
1015 BindVal::OptString(s) => q.bind(s.clone()),
1016 BindVal::Bool(b) => q.bind(*b),
1017 };
1018 }
1019 q = q.bind(message_table);
1021 q = q.bind(row_index);
1022 q = q.bind(row_sub_index);
1023 q = q.bind(agent_instance_hierarchy);
1024
1025 q.execute(&**pool).await?;
1026 Ok(())
1027}
1028
1029#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1034pub enum Tier {
1035 Agent,
1036 Vector,
1037 Function,
1038}
1039
1040impl Tier {
1041 pub fn request_table(self) -> &'static str {
1042 match self {
1043 Tier::Agent => "objectiveai.agent_completion_requests",
1044 Tier::Vector => "objectiveai.vector_completion_requests",
1045 Tier::Function => "objectiveai.function_execution_requests",
1046 }
1047 }
1048 pub fn response_table(self) -> &'static str {
1049 match self {
1050 Tier::Agent => "objectiveai.agent_completion_responses",
1051 Tier::Vector => "objectiveai.vector_completion_responses",
1052 Tier::Function => "objectiveai.function_execution_responses",
1053 }
1054 }
1055 pub fn request_message_table(self) -> MessageTable {
1058 match self {
1059 Tier::Agent => MessageTable::AgentCompletionRequest,
1060 Tier::Vector => MessageTable::VectorCompletionRequest,
1061 Tier::Function => MessageTable::FunctionExecutionRequest,
1062 }
1063 }
1064}
1065
1066pub async fn insert_request_blob<P: Serialize>(
1074 pool: &Pool,
1075 tier: Tier,
1076 response_id: &str,
1077 params: &P,
1078 sender_agent_instance_hierarchy: &str,
1079 timestamp: i64,
1080) -> Result<(), Error> {
1081 let body = serde_json::to_value(params)?;
1082 let sql = format!(
1083 "INSERT INTO {table} \
1084 (response_id, body, created_at, sender_agent_instance_hierarchy) \
1085 VALUES ($1, $2, $3, $4)",
1086 table = tier.request_table()
1087 );
1088 sqlx::query(&sql)
1089 .bind(response_id)
1090 .bind(sqlx::types::Json(body))
1091 .bind(timestamp)
1092 .bind(sender_agent_instance_hierarchy)
1093 .execute(&**pool)
1094 .await?;
1095 Ok(())
1096}
1097
1098pub async fn insert_request_messages_row(
1107 pool: &Pool,
1108 tier: Tier,
1109 response_id: &str,
1110 agent_instance_hierarchy: &str,
1111 timestamp: i64,
1112) -> Result<(), Error> {
1113 sqlx::query(
1114 "INSERT INTO objectiveai.messages \
1115 (response_id, \"table\", row_index, row_sub_index, \
1116 agent_instance_hierarchy, \"timestamp\") \
1117 VALUES ($1, $2, NULL, NULL, $3, $4)",
1118 )
1119 .bind(response_id)
1120 .bind(tier.request_message_table())
1121 .bind(agent_instance_hierarchy)
1122 .bind(timestamp)
1123 .execute(&**pool)
1124 .await?;
1125 Ok(())
1126}
1127
1128pub async fn insert_response_blob<C: Serialize>(
1133 pool: &Pool,
1134 tier: Tier,
1135 response_id: &str,
1136 chunk: &C,
1137 created_at: i64,
1138) -> Result<(), Error> {
1139 let body = serde_json::to_value(chunk)?;
1140 let sql = format!(
1141 "INSERT INTO {table} (response_id, body, created_at) VALUES ($1, $2, $3)",
1142 table = tier.response_table()
1143 );
1144 sqlx::query(&sql)
1145 .bind(response_id)
1146 .bind(sqlx::types::Json(body))
1147 .bind(created_at)
1148 .execute(&**pool)
1149 .await?;
1150 Ok(())
1151}
1152
1153pub async fn update_response_blob<C: Serialize>(
1155 pool: &Pool,
1156 tier: Tier,
1157 response_id: &str,
1158 chunk: &C,
1159 created_at: i64,
1160) -> Result<(), Error> {
1161 let body = serde_json::to_value(chunk)?;
1162 let sql = format!(
1163 "UPDATE {table} SET body = $2, created_at = $3 WHERE response_id = $1",
1164 table = tier.response_table()
1165 );
1166 sqlx::query(&sql)
1167 .bind(response_id)
1168 .bind(sqlx::types::Json(body))
1169 .bind(created_at)
1170 .execute(&**pool)
1171 .await?;
1172 Ok(())
1173}