diff --git a/README.md b/README.md index 03fc9e20..29b03701 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/Sources/MCP/Server/Prompts.swift b/Sources/MCP/Server/Prompts.swift index 24e5a007..c194b28a 100644 --- a/Sources/MCP/Server/Prompts.swift +++ b/Sources/MCP/Server/Prompts.swift @@ -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. diff --git a/Sources/MCP/Server/Sampling.swift b/Sources/MCP/Server/Sampling.swift index 29704443..46563985 100644 --- a/Sources/MCP/Server/Sampling.swift +++ b/Sources/MCP/Server/Sampling.swift @@ -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 { diff --git a/Tests/MCPTests/PromptTests.swift b/Tests/MCPTests/PromptTests.swift index d0d7f112..20e5c279 100644 --- a/Tests/MCPTests/PromptTests.swift +++ b/Tests/MCPTests/PromptTests.swift @@ -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) + } } diff --git a/Tests/MCPTests/SamplingTests.swift b/Tests/MCPTests/SamplingTests.swift index f10964e5..8606809e 100644 --- a/Tests/MCPTests/SamplingTests.swift +++ b/Tests/MCPTests/SamplingTests.swift @@ -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(