1use serde_json::Value;
2use utoipa::ToSchema;
3
4use crate::prelude::*;
5
6#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
8#[sqlx(type_name = "tool_kind", rename_all = "kebab-case")]
9#[serde(rename_all = "snake_case")]
10pub enum ToolKind {
11 Function,
13 AzureAiSearch,
15 ClientTool,
18}
19
20impl ToolKind {
21 pub fn is_answered_by_client(self) -> bool {
24 matches!(self, Self::ClientTool)
25 }
26
27 pub fn is_provider_tool(self) -> bool {
31 match self {
32 ToolKind::AzureAiSearch => true,
33 ToolKind::Function | ToolKind::ClientTool => false,
34 }
35 }
36}
37
38#[derive(Clone, PartialEq, Deserialize, Serialize, Debug, ToSchema)]
39pub struct ChatbotConversationMessageToolCall {
40 pub id: Uuid,
41 pub created_at: DateTime<Utc>,
42 pub updated_at: DateTime<Utc>,
43 pub deleted_at: Option<DateTime<Utc>>,
44 pub chatbot_conversation_message_id: Uuid,
45 pub tool_name: String,
46 #[schema(value_type = String)]
51 pub tool_arguments: Value,
52 pub tool_call_id: String,
53 pub tool_kind: ToolKind,
54 pub response_id: String,
55}
56
57impl ChatbotConversationMessageToolCall {
58 pub fn new(
64 tool_call_id: String,
65 tool_name: String,
66 arguments: String,
67 tool_kind: ToolKind,
68 response_id: String,
69 ) -> Self {
70 Self {
71 tool_call_id,
72 tool_name,
73 tool_arguments: Value::String(arguments),
74 tool_kind,
75 response_id,
76 ..Default::default()
77 }
78 }
79
80 pub fn arguments_json(&self) -> String {
86 match &self.tool_arguments {
87 Value::String(json) => json.clone(),
88 parsed => parsed.to_string(),
89 }
90 }
91}
92
93impl Default for ChatbotConversationMessageToolCall {
94 fn default() -> Self {
95 Self {
96 id: Uuid::nil(),
97 created_at: Default::default(),
98 updated_at: Default::default(),
99 deleted_at: None,
100 chatbot_conversation_message_id: Uuid::nil(),
101 tool_name: Default::default(),
102 tool_arguments: Default::default(),
103 tool_call_id: Default::default(),
104 tool_kind: ToolKind::Function,
105 response_id: Default::default(),
106 }
107 }
108}
109
110pub async fn insert(
111 conn: &mut PgConnection,
112 input: ChatbotConversationMessageToolCall,
113 msg_id: Uuid,
114) -> ModelResult<ChatbotConversationMessageToolCall> {
115 let res = sqlx::query_as!(
116 ChatbotConversationMessageToolCall,
117 r#"
118INSERT INTO chatbot_conversation_message_tool_calls (
119 chatbot_conversation_message_id,
120 tool_name,
121 tool_arguments,
122 tool_call_id,
123 tool_kind,
124 response_id
125 )
126VALUES ($1, $2, $3, $4, $5, $6)
127RETURNING *
128 "#,
129 msg_id,
130 input.tool_name,
131 input.tool_arguments,
132 input.tool_call_id,
133 input.tool_kind as ToolKind,
134 input.response_id
135 )
136 .fetch_one(conn)
137 .await?;
138 Ok(res)
139}
140
141pub async fn get_by_id(
142 conn: &mut PgConnection,
143 id: Uuid,
144) -> ModelResult<ChatbotConversationMessageToolCall> {
145 let res = sqlx::query_as!(
146 ChatbotConversationMessageToolCall,
147 r#"
148SELECT *
149FROM chatbot_conversation_message_tool_calls
150WHERE id = $1
151 AND deleted_at IS NULL
152 "#,
153 id
154 )
155 .fetch_one(conn)
156 .await?;
157 Ok(res)
158}
159
160pub async fn get_by_message_id(
161 conn: &mut PgConnection,
162 msg_id: Uuid,
163) -> ModelResult<Option<ChatbotConversationMessageToolCall>> {
164 let res = sqlx::query_as!(
165 ChatbotConversationMessageToolCall,
166 r#"
167SELECT *
168FROM chatbot_conversation_message_tool_calls
169WHERE chatbot_conversation_message_id = $1
170 AND deleted_at IS NULL
171 "#,
172 msg_id
173 )
174 .fetch_optional(conn)
175 .await?;
176 Ok(res)
177}
178
179pub async fn get_by_conversation_and_tool_call_id(
184 conn: &mut PgConnection,
185 conversation_id: Uuid,
186 tool_call_id: &str,
187) -> ModelResult<Option<ChatbotConversationMessageToolCall>> {
188 let res = sqlx::query_as!(
189 ChatbotConversationMessageToolCall,
190 r#"
191SELECT ccmtc.*
192FROM chatbot_conversation_message_tool_calls AS ccmtc
193 JOIN chatbot_conversation_messages AS ccm ON ccm.id = ccmtc.chatbot_conversation_message_id
194WHERE ccm.conversation_id = $1
195 AND ccmtc.tool_call_id = $2
196 AND ccmtc.deleted_at IS NULL
197 AND ccm.deleted_at IS NULL
198 "#,
199 conversation_id,
200 tool_call_id
201 )
202 .fetch_optional(conn)
203 .await?;
204 Ok(res)
205}
206
207pub async fn delete(
208 conn: &mut PgConnection,
209 id: Uuid,
210) -> ModelResult<ChatbotConversationMessageToolCall> {
211 let res = sqlx::query_as!(
212 ChatbotConversationMessageToolCall,
213 r#"
214UPDATE chatbot_conversation_message_tool_calls
215SET deleted_at = NOW()
216WHERE id = $1
217 AND deleted_at IS NULL
218RETURNING *
219 "#,
220 id
221 )
222 .fetch_one(conn)
223 .await?;
224 Ok(res)
225}
226
227pub async fn get_unanswered_tool_calls_for_conversation(
237 conn: &mut PgConnection,
238 conversation_id: Uuid,
239) -> ModelResult<Vec<ChatbotConversationMessageToolCall>> {
240 let res = sqlx::query_as!(
241 ChatbotConversationMessageToolCall,
242 r#"
243SELECT *
244FROM chatbot_conversation_message_tool_calls AS ccmtc
245WHERE ccmtc.chatbot_conversation_message_id IN (
246 SELECT id
247 FROM chatbot_conversation_messages
248 WHERE conversation_id = $1
249 AND deleted_at IS NULL
250 )
251 AND ccmtc.deleted_at IS NULL
252 AND NOT EXISTS (
253 SELECT ccmto.id
254 FROM chatbot_conversation_message_tool_outputs AS ccmto
255 JOIN chatbot_conversation_messages AS ccm ON ccm.id = ccmto.chatbot_conversation_message_id
256 WHERE ccmto.tool_call_id = ccmtc.tool_call_id
257 AND ccm.conversation_id = $1
258 AND ccmto.deleted_at IS NULL
259 AND ccm.deleted_at IS NULL
260 )
261 "#,
262 conversation_id
263 )
264 .fetch_all(conn)
265 .await?;
266 Ok(res)
267}
268
269pub async fn lock_unanswered_for_execution(
278 conn: &mut PgConnection,
279 tool_call_id: Uuid,
280) -> ModelResult<()> {
281 let call = sqlx::query!(
282 r#"
283SELECT ccmtc.id, ccmtc.tool_call_id, ccm.conversation_id
284FROM chatbot_conversation_message_tool_calls AS ccmtc
285 JOIN chatbot_conversation_messages AS ccm ON ccm.id = ccmtc.chatbot_conversation_message_id
286WHERE ccmtc.id = $1
287 AND ccmtc.deleted_at IS NULL
288FOR UPDATE OF ccmtc
289 "#,
290 tool_call_id
291 )
292 .fetch_optional(&mut *conn)
293 .await?
294 .ok_or_else(|| {
295 model_err!(
296 RecordNotFound,
297 format!("No tool call {tool_call_id} to execute")
298 )
299 })?;
300
301 let already_answered = sqlx::query!(
304 r#"
305SELECT ccmto.id
306FROM chatbot_conversation_message_tool_outputs AS ccmto
307 JOIN chatbot_conversation_messages AS ccm ON ccm.id = ccmto.chatbot_conversation_message_id
308WHERE ccmto.tool_call_id = $1
309 AND ccm.conversation_id = $2
310 AND ccmto.deleted_at IS NULL
311 AND ccm.deleted_at IS NULL
312 "#,
313 call.tool_call_id,
314 call.conversation_id
315 )
316 .fetch_optional(conn)
317 .await?
318 .is_some();
319
320 if already_answered {
321 return Err(model_err!(
322 InvalidRequest,
323 format!("Tool call {tool_call_id} has already been answered")
324 ));
325 }
326 Ok(())
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
336 fn stored_arguments_come_back_as_the_json_text_the_model_wrote() {
337 let recorded = ChatbotConversationMessageToolCall {
338 tool_arguments: Value::String(r#"{"choices":["a","b"]}"#.to_string()),
339 ..Default::default()
340 };
341 assert_eq!(recorded.arguments_json(), r#"{"choices":["a","b"]}"#);
342
343 let as_an_object = ChatbotConversationMessageToolCall {
344 tool_arguments: serde_json::json!({ "choices": ["a", "b"] }),
345 ..Default::default()
346 };
347 assert_eq!(as_an_object.arguments_json(), r#"{"choices":["a","b"]}"#);
348 }
349}