use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[non_exhaustive]
pub struct Message {
pub(crate) role: String,
pub(crate) content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) tool_calls: Option<Vec<Value>>,
}
impl Message {
#[must_use]
pub fn user(content: impl Into<String>) -> Message {
Message {
role: "user".into(),
content: content.into(),
tool_call_id: None,
tool_calls: None,
}
}
#[must_use]
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Message {
Message {
role: "tool".into(),
content: content.into(),
tool_call_id: Some(tool_call_id.into()),
tool_calls: None,
}
}
#[must_use]
pub fn assistant(content: impl Into<String>) -> Message {
Message {
role: "assistant".into(),
content: content.into(),
tool_call_id: None,
tool_calls: None,
}
}
#[must_use]
pub(crate) fn assistant_tool_calls(raw_tool_calls: Vec<Value>) -> Message {
Message {
role: "assistant".into(),
content: String::new(),
tool_call_id: None,
tool_calls: Some(raw_tool_calls),
}
}
#[must_use]
pub fn role(&self) -> &str {
&self.role
}
#[must_use]
pub fn content(&self) -> &str {
&self.content
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[non_exhaustive]
pub struct ToolSchema {
pub(crate) name: String,
pub(crate) description: String,
pub(crate) parameters: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub(crate) enum ToolSchemaError {
#[error("invalid tool wire name {name:?}: {reason}")]
#[non_exhaustive]
InvalidName {
name: String,
reason: &'static str,
},
#[error("tool {name:?} parameters schema must be a JSON object")]
#[non_exhaustive]
NonObjectSchema {
name: String,
},
}
impl ToolSchema {
pub(crate) fn new(
name: impl Into<String>,
description: impl Into<String>,
parameters: Value,
) -> std::result::Result<ToolSchema, ToolSchemaError> {
let name = name.into();
if name.is_empty() {
return Err(ToolSchemaError::InvalidName {
name,
reason: "must not be empty",
});
}
if !name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b'-'))
{
return Err(ToolSchemaError::InvalidName {
name,
reason: "may contain only [A-Za-z0-9_.-]",
});
}
if !parameters.is_object() {
return Err(ToolSchemaError::NonObjectSchema { name });
}
Ok(ToolSchema {
name,
description: description.into(),
parameters,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ToolCall {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) arguments: Value,
}
impl ToolCall {
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn arguments(&self) -> ToolArguments<'_> {
ToolArguments {
value: &self.arguments,
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ToolArguments<'a> {
value: &'a Value,
}
impl ToolArguments<'_> {
#[must_use]
pub fn to_json_string(&self) -> String {
self.value.to_string()
}
#[must_use]
pub fn is_empty(&self) -> bool {
match self.value {
Value::Null => true,
Value::Object(map) => map.is_empty(),
_ => false,
}
}
#[must_use]
pub fn contains(&self, key: &str) -> bool {
self.value
.as_object()
.is_some_and(|map| map.contains_key(key))
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.value
.as_object()
.into_iter()
.flat_map(|map| map.keys().map(String::as_str))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CompletionResult {
Text(String),
ToolCalls(Vec<ToolCall>),
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Completion {
pub(crate) result: CompletionResult,
pub(crate) finish_reason: Option<String>,
pub(crate) reasoning_content: Option<String>,
pub(crate) request_body: Value,
pub(crate) response_body: Value,
}
impl Completion {
#[must_use]
pub fn result(&self) -> &CompletionResult {
&self.result
}
#[must_use]
pub fn finish_reason(&self) -> Option<&str> {
self.finish_reason.as_deref()
}
#[must_use]
pub fn reasoning_content(&self) -> Option<&str> {
self.reasoning_content.as_deref()
}
}