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