llm-worker 0.2.1

A library for building autonomous LLM-powered systems
Documentation
//! Anthropic リクエスト生成

use serde::Serialize;

use crate::llm_client::{
    Request,
    types::{ContentPart, Message, MessageContent, Role, ToolDefinition},
};

use super::AnthropicScheme;

/// Anthropic APIへのリクエストボディ
#[derive(Debug, Serialize)]
pub(crate) struct AnthropicRequest {
    pub model: String,
    pub max_tokens: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub system: Option<String>,
    pub messages: Vec<AnthropicMessage>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tools: Vec<AnthropicTool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_p: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_k: Option<u32>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub stop_sequences: Vec<String>,
    pub stream: bool,
}

/// Anthropic メッセージ
#[derive(Debug, Serialize)]
pub(crate) struct AnthropicMessage {
    pub role: String,
    pub content: AnthropicContent,
}

/// Anthropic コンテンツ
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub(crate) enum AnthropicContent {
    Text(String),
    Parts(Vec<AnthropicContentPart>),
}

/// Anthropic コンテンツパーツ
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
pub(crate) enum AnthropicContentPart {
    #[serde(rename = "text")]
    Text { text: String },
    #[serde(rename = "tool_use")]
    ToolUse {
        id: String,
        name: String,
        input: serde_json::Value,
    },
    #[serde(rename = "tool_result")]
    ToolResult {
        tool_use_id: String,
        content: String,
    },
}

/// Anthropic ツール定義
#[derive(Debug, Serialize)]
pub(crate) struct AnthropicTool {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    pub input_schema: serde_json::Value,
}

impl AnthropicScheme {
    /// RequestからAnthropicのリクエストボディを構築
    pub(crate) fn build_request(&self, model: &str, request: &Request) -> AnthropicRequest {
        let messages = request
            .messages
            .iter()
            .map(|m| self.convert_message(m))
            .collect();

        let tools = request.tools.iter().map(|t| self.convert_tool(t)).collect();

        AnthropicRequest {
            model: model.to_string(),
            max_tokens: request.config.max_tokens.unwrap_or(4096),
            system: request.system_prompt.clone(),
            messages,
            tools,
            temperature: request.config.temperature,
            top_p: request.config.top_p,
            top_k: request.config.top_k,
            stop_sequences: request.config.stop_sequences.clone(),
            stream: true,
        }
    }

    fn convert_message(&self, message: &Message) -> AnthropicMessage {
        let role = match message.role {
            Role::User => "user",
            Role::Assistant => "assistant",
        };

        let content = match &message.content {
            MessageContent::Text(text) => AnthropicContent::Text(text.clone()),
            MessageContent::ToolResult {
                tool_use_id,
                content,
            } => AnthropicContent::Parts(vec![AnthropicContentPart::ToolResult {
                tool_use_id: tool_use_id.clone(),
                content: content.clone(),
            }]),
            MessageContent::Parts(parts) => {
                let converted: Vec<_> = parts
                    .iter()
                    .map(|p| match p {
                        ContentPart::Text { text } => {
                            AnthropicContentPart::Text { text: text.clone() }
                        }
                        ContentPart::ToolUse { id, name, input } => AnthropicContentPart::ToolUse {
                            id: id.clone(),
                            name: name.clone(),
                            input: input.clone(),
                        },
                        ContentPart::ToolResult {
                            tool_use_id,
                            content,
                        } => AnthropicContentPart::ToolResult {
                            tool_use_id: tool_use_id.clone(),
                            content: content.clone(),
                        },
                    })
                    .collect();
                AnthropicContent::Parts(converted)
            }
        };

        AnthropicMessage {
            role: role.to_string(),
            content,
        }
    }

    fn convert_tool(&self, tool: &ToolDefinition) -> AnthropicTool {
        AnthropicTool {
            name: tool.name.clone(),
            description: tool.description.clone(),
            input_schema: tool.input_schema.clone(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_build_simple_request() {
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .system("You are a helpful assistant.")
            .user("Hello!");

        let anthropic_req = scheme.build_request("claude-sonnet-4-20250514", &request);

        assert_eq!(anthropic_req.model, "claude-sonnet-4-20250514");
        assert_eq!(
            anthropic_req.system,
            Some("You are a helpful assistant.".to_string())
        );
        assert_eq!(anthropic_req.messages.len(), 1);
        assert!(anthropic_req.stream);
    }

    #[test]
    fn test_build_request_with_tool() {
        let scheme = AnthropicScheme::new();
        let request = Request::new().user("What's the weather?").tool(
            ToolDefinition::new("get_weather")
                .description("Get current weather")
                .input_schema(serde_json::json!({
                    "type": "object",
                    "properties": {
                        "location": { "type": "string" }
                    },
                    "required": ["location"]
                })),
        );

        let anthropic_req = scheme.build_request("claude-sonnet-4-20250514", &request);

        assert_eq!(anthropic_req.tools.len(), 1);
        assert_eq!(anthropic_req.tools[0].name, "get_weather");
    }
}