Skip to main content

objectiveai_sdk/agent/completions/message/
assistant_message.rs

1//! Assistant message and tool call types.
2
3use super::rich_content::{RichContent, RichContentExpression};
4use crate::functions;
5use functions::expression::{
6    ExpressionError, FromStarlarkValue, WithExpression,
7};
8use serde::{Deserialize, Serialize};
9use starlark::values::dict::DictRef as StarlarkDictRef;
10use starlark::values::{UnpackValue, Value as StarlarkValue};
11use schemars::JsonSchema;
12
13/// An assistant message (model's previous response).
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, arbitrary::Arbitrary)]
15#[schemars(rename = "agent.completions.message.AssistantMessage")]
16pub struct AssistantMessage {
17    /// The message content, if any.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    #[schemars(extend("omitempty" = true))]
20    pub content: Option<RichContent>,
21    /// Optional name for the assistant.
22    #[serde(skip_serializing_if = "Option::is_none")]
23    #[schemars(extend("omitempty" = true))]
24    pub name: Option<String>,
25    /// Refusal message if the model declined to respond.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    #[schemars(extend("omitempty" = true))]
28    pub refusal: Option<String>,
29    /// Tool calls made by the assistant.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    #[schemars(extend("omitempty" = true))]
32    pub tool_calls: Option<Vec<AssistantToolCall>>,
33    /// Reasoning content from models that support chain-of-thought.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    #[schemars(extend("omitempty" = true))]
36    pub reasoning: Option<String>,
37}
38
39impl AssistantMessage {
40    /// Prepares the message by normalizing content and optional fields.
41    pub fn prepare(&mut self) {
42        if let Some(content) = &mut self.content {
43            content.prepare();
44            if content.is_empty() {
45                self.content = None;
46            }
47        }
48        if self.name.as_ref().is_some_and(String::is_empty) {
49            self.name = None;
50        }
51        if self.refusal.as_ref().is_some_and(String::is_empty) {
52            self.refusal = None;
53        }
54        if let Some(tool_calls) = &mut self.tool_calls {
55            tool_calls.retain(|tool_call| !tool_call.is_empty());
56            if tool_calls.is_empty() {
57                self.tool_calls = None;
58            }
59        }
60        if self.reasoning.as_ref().is_some_and(String::is_empty) {
61            self.reasoning = None;
62        }
63    }
64}
65
66impl FromStarlarkValue for AssistantMessage {
67    fn from_starlark_value(
68        value: &StarlarkValue,
69    ) -> Result<Self, ExpressionError> {
70        let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
71            ExpressionError::StarlarkConversionError(
72                "AssistantMessage: expected dict".into(),
73            )
74        })?;
75        let mut content = None;
76        let mut name = None;
77        let mut refusal = None;
78        let mut tool_calls = None;
79        let mut reasoning = None;
80        for (k, v) in dict.iter() {
81            let key = <&str as UnpackValue>::unpack_value(k)
82                .map_err(|e| {
83                    ExpressionError::StarlarkConversionError(e.to_string())
84                })?
85                .ok_or_else(|| {
86                    ExpressionError::StarlarkConversionError(
87                        "AssistantMessage: expected string key".into(),
88                    )
89                })?;
90            match key {
91                "content" => {
92                    content = Option::<RichContent>::from_starlark_value(&v)?
93                }
94                "name" => name = Option::<String>::from_starlark_value(&v)?,
95                "refusal" => {
96                    refusal = Option::<String>::from_starlark_value(&v)?
97                }
98                "tool_calls" => {
99                    tool_calls =
100                        Option::<Vec<AssistantToolCall>>::from_starlark_value(
101                            &v,
102                        )?
103                }
104                "reasoning" => {
105                    reasoning = Option::<String>::from_starlark_value(&v)?
106                }
107                _ => {}
108            }
109        }
110        Ok(AssistantMessage {
111            content,
112            name,
113            refusal,
114            tool_calls,
115            reasoning,
116        })
117    }
118}
119
120/// Expression variant of [`AssistantMessage`] for dynamic content.
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, arbitrary::Arbitrary)]
122#[schemars(rename = "agent.completions.message.AssistantMessageExpression")]
123pub struct AssistantMessageExpression {
124    /// The content expression.
125    #[serde(default, skip_serializing_if = "functions::expression::WithExpression::is_none")]
126    #[schemars(with = "Option<functions::expression::WithExpression<RichContentExpression>>", extend("omitempty" = true))]
127    pub content:
128        functions::expression::WithExpression<Option<RichContentExpression>>,
129    #[serde(default, skip_serializing_if = "functions::expression::WithExpression::is_none")]
130    #[schemars(with = "Option<functions::expression::WithExpression<String>>", extend("omitempty" = true))]
131    pub name: functions::expression::WithExpression<Option<String>>,
132    #[serde(default, skip_serializing_if = "functions::expression::WithExpression::is_none")]
133    #[schemars(with = "Option<functions::expression::WithExpression<String>>", extend("omitempty" = true))]
134    pub refusal: functions::expression::WithExpression<Option<String>>,
135    #[serde(default, skip_serializing_if = "functions::expression::WithExpression::is_none")]
136    #[schemars(with = "Option<functions::expression::WithExpression<Vec<functions::expression::WithExpression<AssistantToolCallExpression>>>>", extend("omitempty" = true))]
137    pub tool_calls:
138        functions::expression::WithExpression<
139            Option<
140                Vec<
141                    functions::expression::WithExpression<
142                        AssistantToolCallExpression,
143                    >,
144                >,
145            >,
146        >,
147    #[serde(default, skip_serializing_if = "functions::expression::WithExpression::is_none")]
148    #[schemars(with = "Option<functions::expression::WithExpression<String>>", extend("omitempty" = true))]
149    pub reasoning:
150        functions::expression::WithExpression<Option<String>>,
151}
152
153impl AssistantMessageExpression {
154    /// Compiles the expression into a concrete [`AssistantMessage`].
155    pub fn compile(
156        self,
157        params: &functions::expression::Params,
158    ) -> Result<AssistantMessage, functions::expression::ExpressionError> {
159        let content = self
160            .content
161            .compile_one(params)?
162            .map(|content| content.compile(params))
163            .transpose()?;
164        let name = self.name.compile_one(params)?;
165        let refusal = self.refusal.compile_one(params)?;
166        let tool_calls = self
167            .tool_calls
168            .compile_one(params)?
169            .map(|tool_calls| {
170                let mut compiled_tool_calls =
171                    Vec::with_capacity(tool_calls.len());
172                for tool_call in tool_calls {
173                    match tool_call.compile_one_or_many(params)? {
174                        functions::expression::OneOrMany::One(
175                            one_tool_call,
176                        ) => {
177                            compiled_tool_calls
178                                .push(one_tool_call.compile(params)?);
179                        }
180                        functions::expression::OneOrMany::Many(
181                            many_tool_calls,
182                        ) => {
183                            for tool_call in many_tool_calls {
184                                compiled_tool_calls
185                                    .push(tool_call.compile(params)?);
186                            }
187                        }
188                    }
189                }
190                Ok::<_, functions::expression::ExpressionError>(
191                    compiled_tool_calls,
192                )
193            })
194            .transpose()?;
195        let reasoning = self.reasoning.compile_one(params)?;
196        Ok(AssistantMessage {
197            content,
198            name,
199            refusal,
200            tool_calls,
201            reasoning,
202        })
203    }
204}
205
206impl FromStarlarkValue for AssistantMessageExpression {
207    fn from_starlark_value(
208        value: &StarlarkValue,
209    ) -> Result<Self, ExpressionError> {
210        let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
211            ExpressionError::StarlarkConversionError(
212                "AssistantMessageExpression: expected dict".into(),
213            )
214        })?;
215        let mut content = WithExpression::Value(None);
216        let mut name = WithExpression::Value(None);
217        let mut refusal = WithExpression::Value(None);
218        let mut tool_calls = WithExpression::Value(None);
219        let mut reasoning = WithExpression::Value(None);
220        for (k, v) in dict.iter() {
221            let key = <&str as UnpackValue>::unpack_value(k)
222                .map_err(|e| {
223                    ExpressionError::StarlarkConversionError(e.to_string())
224                })?
225                .ok_or_else(|| {
226                    ExpressionError::StarlarkConversionError(
227                        "AssistantMessageExpression: expected string key"
228                            .into(),
229                    )
230                })?;
231            match key {
232                "content" => {
233                    content = WithExpression::Value(if v.is_none() {
234                        None
235                    } else {
236                        Some(RichContentExpression::from_starlark_value(&v)?)
237                    });
238                }
239                "name" => {
240                    name = WithExpression::Value(if v.is_none() {
241                        None
242                    } else {
243                        Some(String::from_starlark_value(&v)?)
244                    });
245                }
246                "refusal" => {
247                    refusal = WithExpression::Value(if v.is_none() {
248                        None
249                    } else {
250                        Some(String::from_starlark_value(&v)?)
251                    });
252                }
253                "tool_calls" => {
254                    tool_calls = WithExpression::Value(if v.is_none() {
255                        None
256                    } else {
257                        Some(Vec::<WithExpression<AssistantToolCallExpression>>::from_starlark_value(&v)?)
258                    });
259                }
260                "reasoning" => {
261                    reasoning = WithExpression::Value(if v.is_none() {
262                        None
263                    } else {
264                        Some(String::from_starlark_value(&v)?)
265                    });
266                }
267                _ => {}
268            }
269        }
270        Ok(AssistantMessageExpression {
271            content,
272            name,
273            refusal,
274            tool_calls,
275            reasoning,
276        })
277    }
278}
279
280/// A tool call made by the assistant.
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, arbitrary::Arbitrary)]
282#[serde(tag = "type", rename_all = "snake_case")]
283#[schemars(rename = "agent.completions.message.AssistantToolCall")]
284pub enum AssistantToolCall {
285    /// A function call with an ID and function details.
286    Function {
287        /// The unique ID of this tool call.
288        id: String,
289        /// The function being called.
290        function: AssistantToolCallFunction,
291    },
292}
293
294impl AssistantToolCall {
295    /// Returns `true` if all fields are empty.
296    pub fn is_empty(&self) -> bool {
297        match self {
298            AssistantToolCall::Function { id, function } => {
299                id.is_empty() && function.is_empty()
300            }
301        }
302    }
303}
304
305impl From<AssistantToolCallDelta> for AssistantToolCall {
306    fn from(tc: AssistantToolCallDelta) -> Self {
307        AssistantToolCall::Function {
308            id: tc.id.unwrap_or_default(),
309            function: tc.function.unwrap_or_default().into(),
310        }
311    }
312}
313
314impl FromStarlarkValue for AssistantToolCall {
315    fn from_starlark_value(
316        value: &StarlarkValue,
317    ) -> Result<Self, ExpressionError> {
318        let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
319            ExpressionError::StarlarkConversionError(
320                "AssistantToolCall: expected dict".into(),
321            )
322        })?;
323        let mut id = None;
324        let mut function = None;
325        for (k, v) in dict.iter() {
326            let key = <&str as UnpackValue>::unpack_value(k)
327                .map_err(|e| {
328                    ExpressionError::StarlarkConversionError(e.to_string())
329                })?
330                .ok_or_else(|| {
331                    ExpressionError::StarlarkConversionError(
332                        "AssistantToolCall: expected string key".into(),
333                    )
334                })?;
335            match key {
336                "id" => id = Some(String::from_starlark_value(&v)?),
337                "function" => {
338                    function = Some(
339                        AssistantToolCallFunction::from_starlark_value(&v)?,
340                    )
341                }
342                _ => {}
343            }
344            if id.is_some() && function.is_some() {
345                break;
346            }
347        }
348        Ok(AssistantToolCall::Function {
349            id: id.unwrap_or_default(),
350            function: function.ok_or_else(|| {
351                ExpressionError::StarlarkConversionError(
352                    "AssistantToolCall: missing function".into(),
353                )
354            })?,
355        })
356    }
357}
358
359/// Expression variant of [`AssistantToolCall`] for dynamic content.
360#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, arbitrary::Arbitrary)]
361#[serde(tag = "type", rename_all = "snake_case")]
362#[schemars(rename = "agent.completions.message.AssistantToolCallExpression")]
363pub enum AssistantToolCallExpression {
364    /// A function call expression.
365    Function {
366        /// The tool call ID expression.
367        id: functions::expression::WithExpression<String>,
368        /// The function expression.
369        function: functions::expression::WithExpression<
370            AssistantToolCallFunctionExpression,
371        >,
372    },
373}
374
375impl AssistantToolCallExpression {
376    /// Compiles the expression into a concrete [`AssistantToolCall`].
377    pub fn compile(
378        self,
379        params: &functions::expression::Params,
380    ) -> Result<AssistantToolCall, functions::expression::ExpressionError> {
381        match self {
382            AssistantToolCallExpression::Function { id, function } => {
383                let id = id.compile_one(params)?;
384                let function = function.compile_one(params)?.compile(params)?;
385                Ok(AssistantToolCall::Function { id, function })
386            }
387        }
388    }
389}
390
391impl FromStarlarkValue for AssistantToolCallExpression {
392    fn from_starlark_value(
393        value: &StarlarkValue,
394    ) -> Result<Self, ExpressionError> {
395        let call = AssistantToolCall::from_starlark_value(value)?;
396        match call {
397            AssistantToolCall::Function { id, function } => {
398                Ok(AssistantToolCallExpression::Function {
399                    id: WithExpression::Value(id),
400                    function: WithExpression::Value(
401                        AssistantToolCallFunctionExpression {
402                            name: WithExpression::Value(function.name),
403                            arguments: WithExpression::Value(
404                                function.arguments,
405                            ),
406                        },
407                    ),
408                })
409            }
410        }
411    }
412}
413
414/// Details of a function call made by the assistant.
415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, arbitrary::Arbitrary)]
416#[schemars(rename = "agent.completions.message.AssistantToolCallFunction")]
417pub struct AssistantToolCallFunction {
418    /// The name of the function to call.
419    pub name: String,
420    /// The arguments to pass to the function, as a JSON string.
421    pub arguments: String,
422}
423
424impl AssistantToolCallFunction {
425    /// Returns `true` if both name and arguments are empty.
426    pub fn is_empty(&self) -> bool {
427        self.name.is_empty() && self.arguments.is_empty()
428    }
429}
430
431impl From<AssistantToolCallFunctionDelta> for AssistantToolCallFunction {
432    fn from(f: AssistantToolCallFunctionDelta) -> Self {
433        Self {
434            name: f.name.unwrap_or_default(),
435            arguments: f.arguments.unwrap_or_default(),
436        }
437    }
438}
439
440impl FromStarlarkValue for AssistantToolCallFunction {
441    fn from_starlark_value(
442        value: &StarlarkValue,
443    ) -> Result<Self, ExpressionError> {
444        let dict = StarlarkDictRef::from_value(*value).ok_or_else(|| {
445            ExpressionError::StarlarkConversionError(
446                "AssistantToolCallFunction: expected dict".into(),
447            )
448        })?;
449        let mut name = None;
450        let mut arguments = None;
451        for (k, v) in dict.iter() {
452            let key = <&str as UnpackValue>::unpack_value(k)
453                .map_err(|e| {
454                    ExpressionError::StarlarkConversionError(e.to_string())
455                })?
456                .ok_or_else(|| {
457                    ExpressionError::StarlarkConversionError(
458                        "AssistantToolCallFunction: expected string key".into(),
459                    )
460                })?;
461            match key {
462                "name" => name = Some(String::from_starlark_value(&v)?),
463                "arguments" => {
464                    arguments = Some(String::from_starlark_value(&v)?)
465                }
466                _ => {}
467            }
468            if name.is_some() && arguments.is_some() {
469                break;
470            }
471        }
472        Ok(AssistantToolCallFunction {
473            name: name.ok_or_else(|| {
474                ExpressionError::StarlarkConversionError(
475                    "AssistantToolCallFunction: missing name".into(),
476                )
477            })?,
478            arguments: arguments.ok_or_else(|| {
479                ExpressionError::StarlarkConversionError(
480                    "AssistantToolCallFunction: missing arguments".into(),
481                )
482            })?,
483        })
484    }
485}
486
487/// A tool call delta in a streaming response.
488#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, arbitrary::Arbitrary)]
489#[schemars(rename = "agent.completions.message.AssistantToolCallDelta")]
490pub struct AssistantToolCallDelta {
491    /// The index of this tool call.
492    #[arbitrary(with = crate::arbitrary_util::arbitrary_u64)]
493    pub index: u64,
494    /// The type of tool call (always "function").
495    #[serde(skip_serializing_if = "Option::is_none")]
496    #[schemars(extend("omitempty" = true))]
497    pub r#type: Option<AssistantToolCallType>,
498    /// The unique ID of this tool call.
499    #[serde(skip_serializing_if = "Option::is_none")]
500    #[schemars(extend("omitempty" = true))]
501    pub id: Option<String>,
502    /// The function call details.
503    #[serde(skip_serializing_if = "Option::is_none")]
504    #[schemars(extend("omitempty" = true))]
505    pub function: Option<AssistantToolCallFunctionDelta>,
506}
507
508impl AssistantToolCallDelta {
509    /// Accumulates another tool call into this one.
510    pub fn push(&mut self, other: &AssistantToolCallDelta) {
511        if self.r#type.is_none() {
512            self.r#type = other.r#type;
513        }
514        if self.id.is_none() {
515            self.id = other.id.clone();
516        }
517        match (&mut self.function, &other.function) {
518            (Some(self_function), Some(other_function)) => {
519                self_function.push(other_function);
520            }
521            (None, Some(other_function)) => {
522                self.function = Some(other_function.clone());
523            }
524            _ => {}
525        }
526    }
527}
528
529/// The type of tool call.
530#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default, JsonSchema, arbitrary::Arbitrary)]
531#[schemars(rename = "agent.completions.message.AssistantToolCallType")]
532pub enum AssistantToolCallType {
533    /// A function call.
534    #[serde(rename = "function")]
535    #[default]
536    Function,
537}
538
539/// Function call details in a streaming tool call.
540#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, arbitrary::Arbitrary)]
541#[schemars(rename = "agent.completions.message.AssistantToolCallFunctionDelta")]
542pub struct AssistantToolCallFunctionDelta {
543    /// The function name (only present in the first delta).
544    #[serde(skip_serializing_if = "Option::is_none")]
545    #[schemars(extend("omitempty" = true))]
546    pub name: Option<String>,
547    /// The arguments being streamed (accumulated across deltas).
548    #[serde(skip_serializing_if = "Option::is_none")]
549    #[schemars(extend("omitempty" = true))]
550    pub arguments: Option<String>,
551}
552
553impl AssistantToolCallFunctionDelta {
554    /// Accumulates another function call delta into this one.
555    pub fn push(&mut self, other: &AssistantToolCallFunctionDelta) {
556        if self.name.is_none() {
557            self.name = other.name.clone();
558        }
559        match (&mut self.arguments, &other.arguments) {
560            (Some(self_arguments), Some(other_arguments)) => {
561                self_arguments.push_str(other_arguments);
562            }
563            (None, Some(other_arguments)) => {
564                self.arguments = Some(other_arguments.clone());
565            }
566            _ => {}
567        }
568    }
569}
570
571/// Expression variant of [`AssistantToolCallFunction`] for dynamic content.
572#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, arbitrary::Arbitrary)]
573#[schemars(rename = "agent.completions.message.AssistantToolCallFunctionExpression")]
574pub struct AssistantToolCallFunctionExpression {
575    /// The function name expression.
576    pub name: functions::expression::WithExpression<String>,
577    /// The arguments expression.
578    pub arguments: functions::expression::WithExpression<String>,
579}
580
581impl AssistantToolCallFunctionExpression {
582    /// Compiles the expression into a concrete [`AssistantToolCallFunction`].
583    pub fn compile(
584        self,
585        params: &functions::expression::Params,
586    ) -> Result<AssistantToolCallFunction, functions::expression::ExpressionError>
587    {
588        let name = self.name.compile_one(params)?;
589        let arguments = self.arguments.compile_one(params)?;
590        Ok(AssistantToolCallFunction { name, arguments })
591    }
592}
593
594impl FromStarlarkValue for AssistantToolCallFunctionExpression {
595    fn from_starlark_value(
596        value: &StarlarkValue,
597    ) -> Result<Self, ExpressionError> {
598        let f = AssistantToolCallFunction::from_starlark_value(value)?;
599        Ok(AssistantToolCallFunctionExpression {
600            name: WithExpression::Value(f.name),
601            arguments: WithExpression::Value(f.arguments),
602        })
603    }
604}
605
606crate::functions::expression::impl_from_special_unsupported!(
607    AssistantToolCallExpression,
608    AssistantToolCallFunctionExpression,
609);
610
611impl crate::functions::expression::FromSpecial
612    for Vec<
613        crate::functions::expression::WithExpression<
614            AssistantToolCallExpression,
615        >,
616    >
617{
618    fn from_special(
619        _special: &crate::functions::expression::Special,
620        _params: &crate::functions::expression::Params,
621    ) -> Result<Self, crate::functions::expression::ExpressionError> {
622        Err(crate::functions::expression::ExpressionError::UnsupportedSpecial)
623    }
624}