Packages

A well-typed, idiomatic Gleam client for Anthropic's Claude API with streaming support and tool use

Current section

Files

Jump to
anthropic_gleam src anthropic message.gleam
Raw

src/anthropic/message.gleam

//// Core message types for the Anthropic Messages API
////
//// This module defines the fundamental types for working with Claude's API,
//// including messages, content blocks, and their JSON serialization.
import gleam/dict.{type Dict}
import gleam/json.{type Json}
import gleam/list
import gleam/result
import gleam/string
// =============================================================================
// Role
// =============================================================================
/// Role of a message in a conversation
pub type Role {
/// Message from the user
User
/// Message from the assistant (Claude)
Assistant
}
/// Encode a Role to JSON string value
pub fn role_to_json(role: Role) -> Json {
case role {
User -> json.string("user")
Assistant -> json.string("assistant")
}
}
/// Convert a string to Role
pub fn role_from_string(str: String) -> Result(Role, String) {
case str {
"user" -> Ok(User)
"assistant" -> Ok(Assistant)
_ -> Error("Invalid role: " <> str)
}
}
/// Convert a Role to string
pub fn role_to_string(role: Role) -> String {
case role {
User -> "user"
Assistant -> "assistant"
}
}
// =============================================================================
// ImageSource
// =============================================================================
/// Source type for images
pub type ImageSourceType {
/// Base64 encoded image data
Base64
}
/// Image source for image content blocks
pub type ImageSource {
ImageSource(
/// Type of image source (currently only base64)
source_type: ImageSourceType,
/// MIME type of the image (e.g., "image/jpeg", "image/png", "image/gif", "image/webp")
media_type: String,
/// Base64 encoded image data
data: String,
)
}
/// Encode an ImageSource to JSON
pub fn image_source_to_json(source: ImageSource) -> Json {
let ImageSource(source_type: _, media_type: media_type, data: data) = source
json.object([
#("type", json.string("base64")),
#("media_type", json.string(media_type)),
#("data", json.string(data)),
])
}
/// Create an ImageSource from a dict (for JSON decoding)
pub fn image_source_from_dict(
dict: Dict(String, String),
) -> Result(ImageSource, String) {
use media_type <- result.try(
dict.get(dict, "media_type")
|> result.replace_error("Missing media_type field"),
)
use data <- result.try(
dict.get(dict, "data")
|> result.replace_error("Missing data field"),
)
Ok(ImageSource(source_type: Base64, media_type: media_type, data: data))
}
// =============================================================================
// ContentBlock
// =============================================================================
/// Content block in a message - represents different types of content
pub type ContentBlock {
/// Plain text content
TextBlock(text: String)
/// Image content with source data
ImageBlock(source: ImageSource)
/// Tool use request from the assistant
ToolUseBlock(
/// Unique identifier for this tool use
id: String,
/// Name of the tool being called
name: String,
/// JSON string of input arguments for the tool
input: String,
)
/// Result of a tool execution
ToolResultBlock(
/// ID of the tool use this is responding to
tool_use_id: String,
/// Content of the tool result (can be text or error)
content: String,
/// Whether this result represents an error (False for success, True for error)
is_error: Bool,
)
}
/// Encode a ContentBlock to JSON
pub fn content_block_to_json(block: ContentBlock) -> Json {
case block {
TextBlock(text: text) ->
json.object([#("type", json.string("text")), #("text", json.string(text))])
ImageBlock(source: source) ->
json.object([
#("type", json.string("image")),
#("source", image_source_to_json(source)),
])
ToolUseBlock(id: id, name: name, input: input) ->
json.object([
#("type", json.string("tool_use")),
#("id", json.string(id)),
#("name", json.string(name)),
#("input", json_string_to_raw_json(input)),
])
ToolResultBlock(
tool_use_id: tool_use_id,
content: content,
is_error: is_error,
) ->
json.object([
#("type", json.string("tool_result")),
#("tool_use_id", json.string(tool_use_id)),
#("content", json.string(content)),
#("is_error", json.bool(is_error)),
])
}
}
/// Get the type of a content block as a string
pub fn content_block_type(block: ContentBlock) -> String {
case block {
TextBlock(_) -> "text"
ImageBlock(_) -> "image"
ToolUseBlock(_, _, _) -> "tool_use"
ToolResultBlock(_, _, _) -> "tool_result"
}
}
// =============================================================================
// Message
// =============================================================================
/// A message in a conversation
pub type Message {
Message(
/// Role of the message sender
role: Role,
/// List of content blocks in the message
content: List(ContentBlock),
)
}
/// Encode a Message to JSON
pub fn message_to_json(message: Message) -> Json {
let Message(role: role, content: content) = message
json.object([
#("role", role_to_json(role)),
#("content", json.array(content, content_block_to_json)),
])
}
/// Encode a list of Messages to JSON
pub fn messages_to_json(messages: List(Message)) -> Json {
json.array(messages, message_to_json)
}
/// Get the text content from a message (concatenated)
pub fn message_text(message: Message) -> String {
message.content
|> list.filter_map(fn(block) {
case block {
TextBlock(text: text) -> Ok(text)
_ -> Error(Nil)
}
})
|> string.join("")
}
/// Check if a message contains tool use blocks
pub fn has_tool_use(message: Message) -> Bool {
list.any(message.content, fn(block) {
case block {
ToolUseBlock(_, _, _) -> True
_ -> False
}
})
}
/// Get all tool use blocks from a message
pub fn get_tool_uses(message: Message) -> List(ContentBlock) {
list.filter(message.content, fn(block) {
case block {
ToolUseBlock(_, _, _) -> True
_ -> False
}
})
}
// =============================================================================
// Convenience constructors
// =============================================================================
/// Create a user message with text content
///
/// This is a convenience function for the common case of creating a simple
/// text message from the user.
///
/// ## Example
///
/// ```gleam
/// let msg = user_message("Hello, Claude!")
/// // Equivalent to: Message(role: User, content: [TextBlock(text: "Hello, Claude!")])
/// ```
pub fn user_message(text_content: String) -> Message {
Message(role: User, content: [TextBlock(text: text_content)])
}
/// Create an assistant message with text content
///
/// This is a convenience function for the common case of creating a simple
/// text message from the assistant (useful for setting up conversation history).
///
/// ## Example
///
/// ```gleam
/// let msg = assistant_message("I'm here to help!")
/// // Equivalent to: Message(role: Assistant, content: [TextBlock(text: "I'm here to help!")])
/// ```
pub fn assistant_message(text_content: String) -> Message {
Message(role: Assistant, content: [TextBlock(text: text_content)])
}
// =============================================================================
// Raw JSON helpers
// =============================================================================
/// Convert a JSON string to a raw JSON value (for embedding pre-encoded JSON)
/// Uses the FFI to parse JSON and convert Erlang terms to Gleam Json
fn json_string_to_raw_json(json_string: String) -> Json {
ffi_json_string_to_gleam_json(json_string)
}
/// FFI function to parse JSON string and convert to Gleam Json type
@external(erlang, "anthropic_ffi", "json_string_to_gleam_json")
fn ffi_json_string_to_gleam_json(json_string: String) -> Json
// =============================================================================
// JSON String helpers
// =============================================================================
/// Convert a message to a JSON string
pub fn message_to_json_string(message: Message) -> String {
message
|> message_to_json
|> json.to_string
}
/// Convert a content block to a JSON string
pub fn content_block_to_json_string(block: ContentBlock) -> String {
block
|> content_block_to_json
|> json.to_string
}