1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//! Shared message conversion utilities for LLM adapters.
//!
//! Provides a [`MessageConverter`] trait that each adapter implements to supply
//! format-specific conversion logic, while the generic [`convert_messages`]
//! function handles the common iteration and pattern matching over
//! [`AgentMessage`] / [`LlmMessage`] variants.
use std::sync::Arc;
use serde_json::Value;
use crate::AgentTool;
use crate::types::{AgentMessage, AssistantMessage, LlmMessage, ToolResultMessage, UserMessage};
// ─── MessageConverter trait ─────────────────────────────────────────────────
/// Callbacks for provider-specific message conversion.
///
/// Each adapter implements this trait so the shared [`convert_messages`]
/// function can build the provider's message list without duplicating the
/// iteration / pattern-matching boilerplate.
pub trait MessageConverter {
/// The provider-specific message type (e.g. `OllamaMessage`, `OpenAiMessage`).
type Message;
/// Optionally produce a system message from the system prompt.
/// Return `None` if the provider handles system prompts out-of-band
/// (e.g. Anthropic uses a top-level `system` field).
fn system_message(system_prompt: &str) -> Option<Self::Message>;
/// Convert a user message.
fn user_message(user: &UserMessage) -> Self::Message;
/// Convert an assistant message.
fn assistant_message(assistant: &AssistantMessage) -> Self::Message;
/// Convert a tool-result message.
fn tool_result_message(result: &ToolResultMessage) -> Self::Message;
}
/// Generic message conversion that iterates [`AgentMessage`] values, skips
/// non-LLM variants, and delegates to the [`MessageConverter`] implementation
/// for format-specific construction.
pub fn convert_messages<C: MessageConverter>(
messages: &[AgentMessage],
system_prompt: &str,
) -> Vec<C::Message> {
let mut result = Vec::new();
if !system_prompt.is_empty()
&& let Some(sys) = C::system_message(system_prompt)
{
result.push(sys);
}
for msg in messages {
let AgentMessage::Llm(llm) = msg else {
continue;
};
match llm {
LlmMessage::User(user) => result.push(C::user_message(user)),
LlmMessage::Assistant(assistant) => result.push(C::assistant_message(assistant)),
LlmMessage::ToolResult(tool_result) => {
result.push(C::tool_result_message(tool_result));
}
}
}
result
}
// ─── Tool schema extraction ─────────────────────────────────────────────────
/// Common tool metadata extracted from [`AgentTool`] instances.
///
/// Used by adapters to avoid duplicating the `name` / `description` /
/// `parameters` mapping before wrapping in provider-specific types.
#[non_exhaustive]
pub struct ToolSchema {
pub name: String,
pub description: String,
pub parameters: Value,
}
impl ToolSchema {
/// Create a new tool schema record.
#[must_use]
pub fn new(name: impl Into<String>, description: impl Into<String>, parameters: Value) -> Self {
Self {
name: name.into(),
description: description.into(),
parameters,
}
}
}
/// Extract tool metadata from a slice of [`AgentTool`] trait objects.
pub fn extract_tool_schemas(tools: &[Arc<dyn AgentTool>]) -> Vec<ToolSchema> {
tools
.iter()
.map(|t| ToolSchema::new(t.name(), t.description(), t.parameters_schema().clone()))
.collect()
}
#[cfg(test)]
#[path = "convert_tests.rs"]
mod tests;