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:
parent
fac4df6b1a
commit
3b505ee575
5 changed files with 680 additions and 171 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue