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