Improve ergonomics of prompt and sampling message initialization (#120)

* Improve ergonomics of prompt and sampling message initialization

* Deprecate previous / existing Message initializers
This commit is contained in:
Mattt 2025-05-26 05:25:02 -07:00 • committed by GitHub
commit 3b505ee575
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 680 additions and 171 deletions

View file

@ -571,9 +571,9 @@ server.withMethodHandler(GetPrompt.self) { params in
let description = "Job interview for \(position) position at \(company)"
let messages: [Prompt.Message] = [
.init(role: .user, content: .text(text: "You are an interviewer for the \(position) position at \(company).")),
.init(role: .user, content: .text(text: "Hello, I'm \(interviewee) and I'm here for the \(position) interview.")),
.init(role: .assistant, content: .text(text: "Hi \(interviewee), welcome to \(company)! I'd like to start by asking about your background and experience."))
.user("You are an interviewer for the \(position) position at \(company)."),
.user("Hello, I'm \(interviewee) and I'm here for the \(position) interview."),
.assistant("Hi \(interviewee), welcome to \(company)! I'd like to start by asking about your background and experience.")
]
return .init(description: description, messages: messages)
@ -609,7 +609,7 @@ let server = Server(
do {
let result = try await server.requestSampling(
messages: [
Sampling.Message(role: .user, content: .text("Analyze this data and suggest next steps"))
.user("Analyze this data and suggest next steps")
],
systemPrompt: "You are a helpful data analyst",
maxTokens: 150,

View file

@ -53,13 +53,33 @@ public struct Prompt: Hashable, Codable, Sendable {
/// The message content
public let content: Content
/// Creates a message with the specified role and content
@available(
*, deprecated, message: "Use static factory methods .user(_:) or .assistant(_:) instead"
)
public init(role: Role, content: Content) {
self.role = role
self.content = content
}
/// Private initializer for convenience methods to avoid deprecation warnings
private init(_role role: Role, _content content: Content) {
self.role = role
self.content = content
}
/// Creates a user message with the specified content
public static func user(_ content: Content) -> Message {
return Message(_role: .user, _content: content)
}
/// Creates an assistant message with the specified content
public static func assistant(_ content: Content) -> Message {
return Message(_role: .assistant, _content: content)
}
/// Content types for messages
public enum Content: Hashable, Codable, Sendable {
public enum Content: Hashable, Sendable {
/// Text content
case text(text: String)
/// Image content
@ -68,64 +88,6 @@ public struct Prompt: Hashable, Codable, Sendable {
case audio(data: String, mimeType: String)
/// Embedded resource content
case resource(uri: String, mimeType: String, text: String?, blob: String?)
private enum CodingKeys: String, CodingKey {
case type, text, data, mimeType, uri, blob
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
case .image(let data, let mimeType):
try container.encode("image", forKey: .type)
try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType)
case .audio(let data, let mimeType):
try container.encode("audio", forKey: .type)
try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType)
case .resource(let uri, let mimeType, let text, let blob):
try container.encode("resource", forKey: .type)
try container.encode(uri, forKey: .uri)
try container.encode(mimeType, forKey: .mimeType)
try container.encodeIfPresent(text, forKey: .text)
try container.encodeIfPresent(blob, forKey: .blob)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let text = try container.decode(String.self, forKey: .text)
self = .text(text: text)
case "image":
let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType)
self = .image(data: data, mimeType: mimeType)
case "audio":
let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType)
self = .audio(data: data, mimeType: mimeType)
case "resource":
let uri = try container.decode(String.self, forKey: .uri)
let mimeType = try container.decode(String.self, forKey: .mimeType)
let text = try container.decodeIfPresent(String.self, forKey: .text)
let blob = try container.decodeIfPresent(String.self, forKey: .blob)
self = .resource(uri: uri, mimeType: mimeType, text: text, blob: blob)
default:
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Unknown content type")
}
}
}
}
@ -156,6 +118,84 @@ public struct Prompt: Hashable, Codable, Sendable {
}
}
// MARK: - Codable
extension Prompt.Message.Content: Codable {
private enum CodingKeys: String, CodingKey {
case type, text, data, mimeType, uri, blob
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
case .image(let data, let mimeType):
try container.encode("image", forKey: .type)
try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType)
case .audio(let data, let mimeType):
try container.encode("audio", forKey: .type)
try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType)
case .resource(let uri, let mimeType, let text, let blob):
try container.encode("resource", forKey: .type)
try container.encode(uri, forKey: .uri)
try container.encode(mimeType, forKey: .mimeType)
try container.encodeIfPresent(text, forKey: .text)
try container.encodeIfPresent(blob, forKey: .blob)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let text = try container.decode(String.self, forKey: .text)
self = .text(text: text)
case "image":
let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType)
self = .image(data: data, mimeType: mimeType)
case "audio":
let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType)
self = .audio(data: data, mimeType: mimeType)
case "resource":
let uri = try container.decode(String.self, forKey: .uri)
let mimeType = try container.decode(String.self, forKey: .mimeType)
let text = try container.decodeIfPresent(String.self, forKey: .text)
let blob = try container.decodeIfPresent(String.self, forKey: .blob)
self = .resource(uri: uri, mimeType: mimeType, text: text, blob: blob)
default:
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Unknown content type")
}
}
}
// MARK: - ExpressibleByStringLiteral
extension Prompt.Message.Content: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .text(text: value)
}
}
// MARK: - ExpressibleByStringInterpolation
extension Prompt.Message.Content: ExpressibleByStringInterpolation {
public init(stringInterpolation: DefaultStringInterpolation) {
self = .text(text: String(stringInterpolation: stringInterpolation))
}
}
// MARK: -
/// To retrieve available prompts, clients send a `prompts/list` request.

View file

@ -21,54 +21,37 @@ public enum Sampling {
/// The message content
public let content: Content
/// Creates a message with the specified role and content
@available(
*, deprecated, message: "Use static factory methods .user(_:) or .assistant(_:) instead"
)
public init(role: Role, content: Content) {
self.role = role
self.content = content
}
/// Private initializer for convenience methods to avoid deprecation warnings
private init(_role role: Role, _content content: Content) {
self.role = role
self.content = content
}
/// Creates a user message with the specified content
public static func user(_ content: Content) -> Message {
return Message(_role: .user, _content: content)
}
/// Creates an assistant message with the specified content
public static func assistant(_ content: Content) -> Message {
return Message(_role: .assistant, _content: content)
}
/// Content types for sampling messages
public enum Content: Hashable, Codable, Sendable {
public enum Content: Hashable, Sendable {
/// Text content
case text(String)
/// Image content
case image(data: String, mimeType: String)
private enum CodingKeys: String, CodingKey {
case type, text, data, mimeType
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let text = try container.decode(String.self, forKey: .text)
self = .text(text)
case "image":
let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType)
self = .image(data: data, mimeType: mimeType)
default:
throw DecodingError.dataCorruptedError(
forKey: .type, in: container,
debugDescription: "Unknown sampling message content type")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
case .image(let data, let mimeType):
try container.encode("image", forKey: .type)
try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType)
}
}
}
}
@ -127,6 +110,65 @@ public enum Sampling {
}
}
// MARK: - Codable
extension Sampling.Message.Content: Codable {
private enum CodingKeys: String, CodingKey {
case type, text, data, mimeType
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let text = try container.decode(String.self, forKey: .text)
self = .text(text)
case "image":
let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType)
self = .image(data: data, mimeType: mimeType)
default:
throw DecodingError.dataCorruptedError(
forKey: .type, in: container,
debugDescription: "Unknown sampling message content type")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
case .image(let data, let mimeType):
try container.encode("image", forKey: .type)
try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType)
}
}
}
// MARK: - ExpressibleByStringLiteral
extension Sampling.Message.Content: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .text(value)
}
}
// MARK: - ExpressibleByStringInterpolation
extension Sampling.Message.Content: ExpressibleByStringInterpolation {
public init(stringInterpolation: DefaultStringInterpolation) {
self = .text(String(stringInterpolation: stringInterpolation))
}
}
// MARK: -
/// To request sampling from a client, servers send a `sampling/createMessage` request.
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/sampling#how-sampling-works
public enum CreateSamplingMessage: Method {

View file

@ -29,10 +29,7 @@ struct PromptTests {
@Test("Prompt Message encoding and decoding")
func testPromptMessageEncodingDecoding() throws {
let textMessage = Prompt.Message(
role: .user,
content: .text(text: "Hello, world!")
)
let textMessage: Prompt.Message = .user("Hello, world!")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
@ -134,9 +131,9 @@ struct PromptTests {
@Test("GetPrompt result validation")
func testGetPromptResult() throws {
let messages = [
Prompt.Message(role: .user, content: .text(text: "User message")),
Prompt.Message(role: .assistant, content: .text(text: "Assistant response")),
let messages: [Prompt.Message] = [
.user("User message"),
.assistant("Assistant response"),
]
let result = GetPrompt.Result(description: "Test description", messages: messages)
@ -203,4 +200,246 @@ struct PromptTests {
func testPromptListChangedNotification() throws {
#expect(PromptListChangedNotification.name == "notifications/prompts/list_changed")
}
@Test("Prompt Message factory methods")
func testPromptMessageFactoryMethods() throws {
// Test user message factory method
let userMessage: Prompt.Message = .user("Hello, world!")
#expect(userMessage.role == .user)
if case .text(let text) = userMessage.content {
#expect(text == "Hello, world!")
} else {
#expect(Bool(false), "Expected text content")
}
// Test assistant message factory method
let assistantMessage: Prompt.Message = .assistant("Hi there!")
#expect(assistantMessage.role == .assistant)
if case .text(let text) = assistantMessage.content {
#expect(text == "Hi there!")
} else {
#expect(Bool(false), "Expected text content")
}
// Test with image content
let imageMessage: Prompt.Message = .user(.image(data: "base64data", mimeType: "image/png"))
#expect(imageMessage.role == .user)
if case .image(let data, let mimeType) = imageMessage.content {
#expect(data == "base64data")
#expect(mimeType == "image/png")
} else {
#expect(Bool(false), "Expected image content")
}
// Test with audio content
let audioMessage: Prompt.Message = .assistant(
.audio(data: "base64audio", mimeType: "audio/wav"))
#expect(audioMessage.role == .assistant)
if case .audio(let data, let mimeType) = audioMessage.content {
#expect(data == "base64audio")
#expect(mimeType == "audio/wav")
} else {
#expect(Bool(false), "Expected audio content")
}
// Test with resource content
let resourceMessage: Prompt.Message = .user(
.resource(
uri: "file://test.txt", mimeType: "text/plain", text: "Sample text", blob: nil))
#expect(resourceMessage.role == .user)
if case .resource(let uri, let mimeType, let text, let blob) = resourceMessage.content {
#expect(uri == "file://test.txt")
#expect(mimeType == "text/plain")
#expect(text == "Sample text")
#expect(blob == nil)
} else {
#expect(Bool(false), "Expected resource content")
}
}
@Test("Prompt Content ExpressibleByStringLiteral")
func testPromptContentExpressibleByStringLiteral() throws {
// Test string literal assignment
let content: Prompt.Message.Content = "Hello from string literal"
if case .text(let text) = content {
#expect(text == "Hello from string literal")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in message creation
let message: Prompt.Message = .user("Direct string literal")
if case .text(let text) = message.content {
#expect(text == "Direct string literal")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in array context
let messages: [Prompt.Message] = [
.user("First message"),
.assistant("Second message"),
.user("Third message"),
]
#expect(messages.count == 3)
#expect(messages[0].role == .user)
#expect(messages[1].role == .assistant)
#expect(messages[2].role == .user)
}
@Test("Prompt Content ExpressibleByStringInterpolation")
func testPromptContentExpressibleByStringInterpolation() throws {
let userName = "Alice"
let position = "Software Engineer"
let company = "TechCorp"
// Test string interpolation
let content: Prompt.Message.Content =
"Hello \(userName), welcome to your \(position) interview at \(company)"
if case .text(let text) = content {
#expect(text == "Hello Alice, welcome to your Software Engineer interview at TechCorp")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in message creation with interpolation
let message: Prompt.Message = .user(
"Hi \(userName), I'm excited about the \(position) role at \(company)")
if case .text(let text) = message.content {
#expect(text == "Hi Alice, I'm excited about the Software Engineer role at TechCorp")
} else {
#expect(Bool(false), "Expected text content")
}
// Test complex interpolation
let skills = ["Swift", "Python", "JavaScript"]
let experience = 5
let interviewMessage: Prompt.Message = .assistant(
"I see you have \(experience) years of experience with \(skills.joined(separator: ", ")). That's impressive!"
)
if case .text(let text) = interviewMessage.content {
#expect(
text
== "I see you have 5 years of experience with Swift, Python, JavaScript. That's impressive!"
)
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("Prompt Message factory methods with string interpolation")
func testPromptMessageFactoryMethodsWithStringInterpolation() throws {
let candidateName = "Bob"
let position = "Data Scientist"
let company = "DataCorp"
let experience = 3
// Test user message with interpolation
let userMessage: Prompt.Message = .user(
"Hello, I'm \(candidateName) and I'm interviewing for the \(position) position")
#expect(userMessage.role == .user)
if case .text(let text) = userMessage.content {
#expect(text == "Hello, I'm Bob and I'm interviewing for the Data Scientist position")
} else {
#expect(Bool(false), "Expected text content")
}
// Test assistant message with interpolation
let assistantMessage: Prompt.Message = .assistant(
"Welcome \(candidateName)! Tell me about your \(experience) years of experience in data science"
)
#expect(assistantMessage.role == .assistant)
if case .text(let text) = assistantMessage.content {
#expect(text == "Welcome Bob! Tell me about your 3 years of experience in data science")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in conversation array
let conversation: [Prompt.Message] = [
.user("Hi, I'm \(candidateName) applying for \(position) at \(company)"),
.assistant("Welcome \(candidateName)! How many years of experience do you have?"),
.user("I have \(experience) years of experience in the field"),
.assistant(
"Great! \(experience) years is solid experience for a \(position) role at \(company)"
),
]
#expect(conversation.count == 4)
// Verify interpolated content
if case .text(let text) = conversation[2].content {
#expect(text == "I have 3 years of experience in the field")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("Prompt ergonomic API usage patterns")
func testPromptErgonomicAPIUsagePatterns() throws {
// Test various ergonomic usage patterns enabled by the new API
// Pattern 1: Simple interview conversation
let interviewConversation: [Prompt.Message] = [
.user("Tell me about yourself"),
.assistant("I'm a software engineer with 5 years of experience"),
.user("What's your biggest strength?"),
.assistant("I'm great at problem-solving and team collaboration"),
]
#expect(interviewConversation.count == 4)
// Pattern 2: Dynamic content with interpolation
let candidateName = "Sarah"
let role = "Product Manager"
let yearsExp = 7
let dynamicConversation: [Prompt.Message] = [
.user("Welcome \(candidateName) to the \(role) interview"),
.assistant("Thank you! I'm excited about this \(role) opportunity"),
.user("I see you have \(yearsExp) years of experience. Tell me about your background"),
.assistant(
"In my \(yearsExp) years as a \(role), I've led multiple successful product launches"
),
]
#expect(dynamicConversation.count == 4)
// Pattern 3: Mixed content types
let mixedContent: [Prompt.Message] = [
.user("Please review this design mockup"),
.assistant(.image(data: "design_mockup_data", mimeType: "image/png")),
.user("What do you think of the user flow?"),
.assistant(
"The design looks clean and intuitive. I particularly like the navigation structure."
),
]
#expect(mixedContent.count == 4)
// Verify content types
if case .text = mixedContent[0].content,
case .image = mixedContent[1].content,
case .text = mixedContent[2].content,
case .text = mixedContent[3].content
{
// All content types are correct
} else {
#expect(Bool(false), "Content types don't match expected pattern")
}
// Pattern 4: Encoding/decoding still works
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(interviewConversation)
let decoded = try decoder.decode([Prompt.Message].self, from: data)
#expect(decoded.count == 4)
#expect(decoded[0].role == .user)
#expect(decoded[1].role == .assistant)
#expect(decoded[2].role == .user)
#expect(decoded[3].role == .assistant)
}
}

View file

@ -20,10 +20,7 @@ struct SamplingTests {
let decoder = JSONDecoder()
// Test text content
let textMessage = Sampling.Message(
role: .user,
content: .text("Hello, world!")
)
let textMessage: Sampling.Message = .user("Hello, world!")
let textData = try encoder.encode(textMessage)
let decodedTextMessage = try decoder.decode(Sampling.Message.self, from: textData)
@ -36,10 +33,8 @@ struct SamplingTests {
}
// Test image content
let imageMessage = Sampling.Message(
role: .assistant,
content: .image(data: "base64imagedata", mimeType: "image/png")
)
let imageMessage: Sampling.Message = .assistant(
.image(data: "base64imagedata", mimeType: "image/png"))
let imageData = try encoder.encode(imageMessage)
let decodedImageMessage = try decoder.decode(Sampling.Message.self, from: imageData)
@ -112,10 +107,9 @@ struct SamplingTests {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let messages = [
Sampling.Message(role: .user, content: .text("What is the weather like?")),
Sampling.Message(
role: .assistant, content: .text("I need to check the weather for you.")),
let messages: [Sampling.Message] = [
.user("What is the weather like?"),
.assistant("I need to check the weather for you."),
]
let modelPreferences = Sampling.ModelPreferences(
@ -179,8 +173,8 @@ struct SamplingTests {
@Test("CreateMessage request creation")
func testCreateMessageRequest() throws {
let messages = [
Sampling.Message(role: .user, content: .text("Hello"))
let messages: [Sampling.Message] = [
.user("Hello")
]
let request = CreateSamplingMessage.request(
@ -260,8 +254,8 @@ struct SamplingTests {
try await server.start(transport: transport)
// Test that server can attempt to request sampling
let messages = [
Sampling.Message(role: .user, content: .text("Test message"))
let messages: [Sampling.Message] = [
.user("Test message")
]
do {
@ -293,7 +287,7 @@ struct SamplingTests {
encoder.outputFormatting = [.sortedKeys]
// Test text content JSON format
let textContent = Sampling.Message.Content.text("Hello")
let textContent: Sampling.Message.Content = .text("Hello")
let textData = try encoder.encode(textContent)
let textJSON = String(data: textData, encoding: .utf8)!
@ -301,7 +295,8 @@ struct SamplingTests {
#expect(textJSON.contains("\"text\":\"Hello\""))
// Test image content JSON format
let imageContent = Sampling.Message.Content.image(data: "base64data", mimeType: "image/png")
let imageContent: Sampling.Message.Content = .image(
data: "base64data", mimeType: "image/png")
let imageData = try encoder.encode(imageContent)
let imageJSON = String(data: imageData, encoding: .utf8)!
@ -334,6 +329,218 @@ struct SamplingTests {
#expect(decoded.speedPriority?.doubleValue == 1.0)
#expect(decoded.intelligencePriority?.doubleValue == 0.0)
}
@Test("Message factory methods")
func testMessageFactoryMethods() throws {
// Test user message factory method
let userMessage: Sampling.Message = .user("Hello, world!")
#expect(userMessage.role == .user)
if case .text(let text) = userMessage.content {
#expect(text == "Hello, world!")
} else {
#expect(Bool(false), "Expected text content")
}
// Test assistant message factory method
let assistantMessage: Sampling.Message = .assistant("Hi there!")
#expect(assistantMessage.role == .assistant)
if case .text(let text) = assistantMessage.content {
#expect(text == "Hi there!")
} else {
#expect(Bool(false), "Expected text content")
}
// Test with image content
let imageMessage: Sampling.Message = .user(
.image(data: "base64data", mimeType: "image/png"))
#expect(imageMessage.role == .user)
if case .image(let data, let mimeType) = imageMessage.content {
#expect(data == "base64data")
#expect(mimeType == "image/png")
} else {
#expect(Bool(false), "Expected image content")
}
}
@Test("Content ExpressibleByStringLiteral")
func testContentExpressibleByStringLiteral() throws {
// Test string literal assignment
let content: Sampling.Message.Content = "Hello from string literal"
if case .text(let text) = content {
#expect(text == "Hello from string literal")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in message creation
let message: Sampling.Message = .user("Direct string literal")
if case .text(let text) = message.content {
#expect(text == "Direct string literal")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in array context
let messages: [Sampling.Message] = [
.user("First message"),
.assistant("Second message"),
.user("Third message"),
]
#expect(messages.count == 3)
#expect(messages[0].role == .user)
#expect(messages[1].role == .assistant)
#expect(messages[2].role == .user)
}
@Test("Content ExpressibleByStringInterpolation")
func testContentExpressibleByStringInterpolation() throws {
let userName = "Alice"
let temperature = 72
let location = "San Francisco"
// Test string interpolation
let content: Sampling.Message.Content =
"Hello \(userName), the temperature in \(location) is \(temperature)°F"
if case .text(let text) = content {
#expect(text == "Hello Alice, the temperature in San Francisco is 72°F")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in message creation with interpolation
let message = Sampling.Message.user(
"Welcome \(userName)! Today's weather in \(location) is \(temperature)°F")
if case .text(let text) = message.content {
#expect(text == "Welcome Alice! Today's weather in San Francisco is 72°F")
} else {
#expect(Bool(false), "Expected text content")
}
// Test complex interpolation
let items = ["apples", "bananas", "oranges"]
let count = items.count
let listMessage: Sampling.Message = .assistant(
"You have \(count) items: \(items.joined(separator: ", "))")
if case .text(let text) = listMessage.content {
#expect(text == "You have 3 items: apples, bananas, oranges")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("Message factory methods with string interpolation")
func testMessageFactoryMethodsWithStringInterpolation() throws {
let customerName = "Bob"
let orderNumber = "ORD-12345"
let issueType = "delivery delay"
// Test user message with interpolation
let userMessage: Sampling.Message = .user(
"Hi, I'm \(customerName) and I have an issue with order \(orderNumber)")
#expect(userMessage.role == .user)
if case .text(let text) = userMessage.content {
#expect(text == "Hi, I'm Bob and I have an issue with order ORD-12345")
} else {
#expect(Bool(false), "Expected text content")
}
// Test assistant message with interpolation
let assistantMessage: Sampling.Message = .assistant(
"Hello \(customerName), I can help you with your \(issueType) issue for order \(orderNumber)"
)
#expect(assistantMessage.role == .assistant)
if case .text(let text) = assistantMessage.content {
#expect(
text
== "Hello Bob, I can help you with your delivery delay issue for order ORD-12345"
)
} else {
#expect(Bool(false), "Expected text content")
}
// Test in conversation array
let conversation: [Sampling.Message] = [
.user("Hello, I'm \(customerName)"),
.assistant("Hi \(customerName), how can I help you today?"),
.user("I have an issue with order \(orderNumber) - it's a \(issueType)"),
.assistant(
"I understand you're experiencing a \(issueType) with order \(orderNumber). Let me look into that for you."
),
]
#expect(conversation.count == 4)
// Verify interpolated content
if case .text(let text) = conversation[2].content {
#expect(text == "I have an issue with order ORD-12345 - it's a delivery delay")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("Ergonomic API usage patterns")
func testErgonomicAPIUsagePatterns() throws {
// Test various ergonomic usage patterns enabled by the new API
// Pattern 1: Simple conversation
let simpleConversation: [Sampling.Message] = [
.user("What's the weather like?"),
.assistant("I'd be happy to help you check the weather!"),
.user("Thanks!"),
]
#expect(simpleConversation.count == 3)
// Pattern 2: Dynamic content with interpolation
let productName = "Smart Thermostat"
let price = 199.99
let discount = 20
let salesConversation: [Sampling.Message] = [
.user("Tell me about the \(productName)"),
.assistant("The \(productName) is priced at $\(String(format: "%.2f", price))"),
.user("Do you have any discounts?"),
.assistant(
"Yes! We currently have a \(discount)% discount, bringing the price to $\(String(format: "%.2f", price * (1.0 - Double(discount)/100.0)))"
),
]
#expect(salesConversation.count == 4)
// Pattern 3: Mixed content types
let mixedContent: [Sampling.Message] = [
.user("Can you analyze this image?"),
.assistant(.image(data: "analysis_chart_data", mimeType: "image/png")),
.user("What does it show?"),
.assistant("The chart shows a clear upward trend in sales."),
]
#expect(mixedContent.count == 4)
// Verify content types
if case .text = mixedContent[0].content,
case .image = mixedContent[1].content,
case .text = mixedContent[2].content,
case .text = mixedContent[3].content
{
// All content types are correct
} else {
#expect(Bool(false), "Content types don't match expected pattern")
}
// Pattern 4: Encoding/decoding still works
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(simpleConversation)
let decoded = try decoder.decode([Sampling.Message].self, from: data)
#expect(decoded.count == 3)
#expect(decoded[0].role == .user)
#expect(decoded[1].role == .assistant)
#expect(decoded[2].role == .user)
}
}
@Suite("Sampling Integration Tests")
@ -454,15 +661,10 @@ struct SamplingIntegrationTests {
try await server.start(transport: transport)
// Test sampling request with comprehensive parameters
let messages = [
Sampling.Message(
role: .user,
content: .text("Analyze the following data and provide insights:")
),
Sampling.Message(
role: .user,
content: .text("Sales data: Q1: $100k, Q2: $150k, Q3: $200k, Q4: $180k")
),
let messages: [Sampling.Message] = [
.user("Analyze the following data and provide insights:"),
.user("Sales data: Q1: $100k, Q2: $150k, Q3: $200k, Q4: $180k"),
.user("Marketing data: Q1: $50k, Q2: $75k, Q3: $100k, Q4: $90k"),
]
let modelPreferences = Sampling.ModelPreferences(
@ -514,19 +716,14 @@ struct SamplingIntegrationTests {
)
func testSamplingMessageTypes() async throws {
// Test comprehensive message content types
let textMessage = Sampling.Message(
role: .user,
content: .text("What do you see in this data?")
)
let textMessage: Sampling.Message = .user("What do you see in this data?")
let imageMessage = Sampling.Message(
role: .user,
content: .image(
let imageMessage: Sampling.Message = .user(
.image(
data:
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
mimeType: "image/png"
)
)
))
// Test encoding/decoding of different message types
let encoder = JSONEncoder()
@ -625,8 +822,8 @@ struct SamplingIntegrationTests {
try await server.start(transport: transport)
// Test sampling request on server without sampling capability
let messages = [
Sampling.Message(role: .user, content: .text("Test message"))
let messages: [Sampling.Message] = [
.user("Test message")
]
do {
@ -658,11 +855,11 @@ struct SamplingIntegrationTests {
)
func testSamplingParameterValidation() async throws {
// Test parameter validation and edge cases
let validMessages = [
Sampling.Message(role: .user, content: .text("Valid message"))
let validMessages: [Sampling.Message] = [
.user("Valid message")
]
_ = [Sampling.Message]() // Test empty messages array
_ = [Sampling.Message]() // Test empty messages array.
// Test with valid parameters
let validParams = CreateSamplingMessage.Parameters(
@ -725,22 +922,16 @@ struct SamplingIntegrationTests {
// Test realistic sampling workflow scenarios
// Scenario 1: Data Analysis Request
let dataAnalysisMessages = [
Sampling.Message(
role: .user,
content: .text("Please analyze the following customer feedback data:")
),
Sampling.Message(
role: .user,
content: .text(
"""
Feedback Summary:
- 85% positive sentiment
- Top complaints: shipping delays (12%), product quality (8%)
- Top praise: customer service (45%), product features (40%)
- NPS Score: 72
""")
),
let dataAnalysisMessages: [Sampling.Message] = [
.user("Please analyze the following customer feedback data:"),
.user(
"""
Feedback Summary:
- 85% positive sentiment
- Top complaints: shipping delays (12%), product quality (8%)
- Top praise: customer service (45%), product features (40%)
- NPS Score: 72
"""),
]
let dataAnalysisParams = CreateSamplingMessage.Parameters(
@ -759,12 +950,9 @@ struct SamplingIntegrationTests {
)
// Scenario 2: Creative Content Generation
let creativeMessages = [
Sampling.Message(
role: .user,
content: .text(
"Write a compelling product description for a new smart home device.")
)
let creativeMessages: [Sampling.Message] = [
.user(
"Write a compelling product description for a new smart home device.")
]
let creativeParams = CreateSamplingMessage.Parameters(