Skip to content

Commit 5b5725f

Browse files
committed
introduce System messages
1 parent f97cdda commit 5b5725f

File tree

14 files changed

+57
-47
lines changed

14 files changed

+57
-47
lines changed

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/domain/ChatRole.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ sealed trait ChatRole extends EnumValue {
77
}
88

99
object ChatRole {
10+
case object System extends ChatRole
1011
case object User extends ChatRole
1112
case object Assistant extends ChatRole
1213

13-
def allValues: Seq[ChatRole] = Seq(User, Assistant)
14+
def allValues: Seq[ChatRole] = Seq(System, User, Assistant)
1415
}

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/domain/Message.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,20 @@ import io.cequence.openaiscala.anthropic.domain.Content.{
99
sealed abstract class Message private (
1010
val role: ChatRole,
1111
val content: Content
12-
)
12+
) {
13+
def isSystem: Boolean = role == ChatRole.System
14+
}
1315

1416
object Message {
1517

18+
case class SystemMessage(
19+
contentString: String,
20+
cacheControl: Option[CacheControl] = None
21+
) extends Message(ChatRole.System, SingleString(contentString, cacheControl))
22+
23+
case class SystemMessageContent(contentBlocks: Seq[ContentBlockBase])
24+
extends Message(ChatRole.System, ContentBlocks(contentBlocks))
25+
1626
case class UserMessage(
1727
contentString: String,
1828
cacheControl: Option[CacheControl] = None

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/service/AnthropicService.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ trait AnthropicService extends CloseableService with AnthropicServiceConsts {
3333
*/
3434
def createMessage(
3535
messages: Seq[Message],
36-
system: Option[Content] = None,
3736
settings: AnthropicCreateMessageSettings = DefaultSettings.CreateMessage
3837
): Future[CreateMessageResponse]
3938

@@ -55,7 +54,6 @@ trait AnthropicService extends CloseableService with AnthropicServiceConsts {
5554
* <a href="https://docs.anthropic.com/claude/reference/messages_post">Anthropic Doc</a>
5655
*/
5756
def createMessageStreamed(
58-
system: Option[Content],
5957
messages: Seq[Message],
6058
settings: AnthropicCreateMessageSettings = DefaultSettings.CreateMessage
6159
): Source[ContentBlockDelta, NotUsed]

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/service/impl/AnthropicServiceImpl.scala

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import akka.NotUsed
44
import akka.stream.scaladsl.Source
55
import io.cequence.openaiscala.OpenAIScalaClientException
66
import io.cequence.openaiscala.anthropic.JsonFormats
7+
import io.cequence.openaiscala.anthropic.domain.Message.{SystemMessage, SystemMessageContent}
8+
import io.cequence.openaiscala.anthropic.domain.{Message => AnthropicMessage}
79
import io.cequence.openaiscala.anthropic.domain.response.{
810
ContentBlockDelta,
911
CreateMessageResponse
@@ -34,19 +36,16 @@ private[service] trait AnthropicServiceImpl extends Anthropic {
3436

3537
override def createMessage(
3638
messages: Seq[Message],
37-
system: Option[Content] = None,
3839
settings: AnthropicCreateMessageSettings
3940
): Future[CreateMessageResponse] =
4041
execPOST(
4142
EndPoint.messages,
42-
bodyParams =
43-
createBodyParamsForMessageCreation(system, messages, settings, stream = false)
43+
bodyParams = createBodyParamsForMessageCreation(messages, settings, stream = false)
4444
).map(
4545
_.asSafeJson[CreateMessageResponse]
4646
)
4747

4848
override def createMessageStreamed(
49-
system: Option[Content],
5049
messages: Seq[Message],
5150
settings: AnthropicCreateMessageSettings
5251
): Source[ContentBlockDelta, NotUsed] =
@@ -55,7 +54,7 @@ private[service] trait AnthropicServiceImpl extends Anthropic {
5554
EndPoint.messages.toString(),
5655
"POST",
5756
bodyParams = paramTuplesToStrings(
58-
createBodyParamsForMessageCreation(system, messages, settings, stream = true)
57+
createBodyParamsForMessageCreation(messages, settings, stream = true)
5958
)
6059
)
6160
.map { (json: JsValue) =>
@@ -83,36 +82,39 @@ private[service] trait AnthropicServiceImpl extends Anthropic {
8382
.collect { case Some(delta) => delta }
8483

8584
private def createBodyParamsForMessageCreation(
86-
system: Option[Content],
8785
messages: Seq[Message],
8886
settings: AnthropicCreateMessageSettings,
8987
stream: Boolean
9088
): Seq[(Param, Option[JsValue])] = {
9189
assert(messages.nonEmpty, "At least one message expected.")
92-
assert(messages.head.role == ChatRole.User, "First message must be from user.")
9390

94-
val messageJsons = messages.map(Json.toJson(_))
91+
val (system, nonSystem) = messages.partition(_.isSystem)
9592

96-
val systemJson = system.map {
97-
case Content.SingleString(text, cacheControl) =>
93+
assert(nonSystem.head.role == ChatRole.User, "First non-system message must be from user.")
94+
assert(system.size <= 1, "System message can be only 1. Use SystemMessageContent to include more content blocks.")
95+
96+
val messageJsons = nonSystem.map(Json.toJson(_))
97+
98+
val systemJson: Seq[JsValue] = system.map {
99+
case SystemMessage(text, cacheControl) =>
98100
if (cacheControl.isEmpty) JsString(text)
99101
else {
100102
val blocks =
101103
Seq(Content.ContentBlockBase(Content.ContentBlock.TextBlock(text), cacheControl))
102104

103105
Json.toJson(blocks)(Writes.seq(contentBlockBaseWrites))
104106
}
105-
case Content.ContentBlocks(blocks) =>
106-
Json.toJson(blocks)(Writes.seq(contentBlockBaseWrites))
107-
case Content.ContentBlockBase(content, cacheControl) =>
108-
val blocks = Seq(Content.ContentBlockBase(content, cacheControl))
107+
case SystemMessageContent(blocks) =>
109108
Json.toJson(blocks)(Writes.seq(contentBlockBaseWrites))
110109
}
111110

112111
jsonBodyParams(
113112
Param.messages -> Some(messageJsons),
114113
Param.model -> Some(settings.model),
115-
Param.system -> system.map(_ => systemJson),
114+
Param.system -> {
115+
if (system.isEmpty) None
116+
else Some(systemJson.head)
117+
},
116118
Param.max_tokens -> Some(settings.max_tokens),
117119
Param.metadata -> { if (settings.metadata.isEmpty) None else Some(settings.metadata) },
118120
Param.stop_sequences -> {

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/service/impl/OpenAIAnthropicChatCompletionService.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ private[service] class OpenAIAnthropicChatCompletionService(
4040
): Future[ChatCompletionResponse] = {
4141
underlying
4242
.createMessage(
43-
toAnthropicMessages(messages, settings),
44-
toAnthropicSystemMessages(messages, settings),
43+
toAnthropicSystemMessages(messages.filter(_.isSystem), settings) ++
44+
toAnthropicMessages(messages.filter(!_.isSystem), settings),
4545
toAnthropicSettings(settings)
4646
)
4747
.map(toOpenAI)
@@ -65,8 +65,8 @@ private[service] class OpenAIAnthropicChatCompletionService(
6565
): Source[ChatCompletionChunkResponse, NotUsed] =
6666
underlying
6767
.createMessageStreamed(
68-
toAnthropicSystemMessages(messages, settings),
69-
toAnthropicMessages(messages, settings),
68+
toAnthropicSystemMessages(messages.filter(_.isSystem), settings) ++
69+
toAnthropicMessages(messages.filter(!_.isSystem), settings),
7070
toAnthropicSettings(settings)
7171
)
7272
.map(toOpenAI)

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/service/impl/package.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.cequence.openaiscala.anthropic.service
33
import io.cequence.openaiscala.anthropic.domain.CacheControl.Ephemeral
44
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.TextBlock
55
import io.cequence.openaiscala.anthropic.domain.Content.{ContentBlockBase, ContentBlocks}
6+
import io.cequence.openaiscala.anthropic.domain.Message.SystemMessageContent
67
import io.cequence.openaiscala.anthropic.domain.response.CreateMessageResponse.UsageInfo
78
import io.cequence.openaiscala.anthropic.domain.response.{
89
ContentBlockDelta,
@@ -40,7 +41,7 @@ package object impl extends AnthropicServiceConsts {
4041
def toAnthropicSystemMessages(
4142
messages: Seq[OpenAIBaseMessage],
4243
settings: CreateChatCompletionSettings
43-
): Option[ContentBlocks] = {
44+
): Seq[Message] = {
4445
val useSystemCache: Option[CacheControl] =
4546
if (settings.useAnthropicSystemMessagesCache) Some(Ephemeral) else None
4647

@@ -55,7 +56,8 @@ package object impl extends AnthropicServiceConsts {
5556
}
5657
}
5758

58-
if (messageStrings.isEmpty) None else Some(ContentBlocks(messageStrings))
59+
if (messageStrings.isEmpty) Seq.empty
60+
else Seq(SystemMessageContent(messageStrings))
5961
}
6062

6163
def toAnthropicMessages(

anthropic-client/src/test/scala/io/cequence/openaiscala/anthropic/service/impl/AnthropicServiceSpec.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,49 +27,49 @@ class AnthropicServiceSpec extends AsyncWordSpec with GivenWhenThen {
2727

2828
"should throw AnthropicScalaUnauthorizedException when 401" ignore {
2929
recoverToSucceededIf[AnthropicScalaUnauthorizedException] {
30-
TestFactory.mockedService401().createMessage(irrelevantMessages, None, settings)
30+
TestFactory.mockedService401().createMessage(irrelevantMessages, settings)
3131
}
3232
}
3333

3434
"should throw AnthropicScalaUnauthorizedException when 403" ignore {
3535
recoverToSucceededIf[AnthropicScalaUnauthorizedException] {
36-
TestFactory.mockedService403().createMessage(irrelevantMessages, None, settings)
36+
TestFactory.mockedService403().createMessage(irrelevantMessages, settings)
3737
}
3838
}
3939

4040
"should throw AnthropicScalaNotFoundException when 404" ignore {
4141
recoverToSucceededIf[AnthropicScalaNotFoundException] {
42-
TestFactory.mockedService404().createMessage(irrelevantMessages, None, settings)
42+
TestFactory.mockedService404().createMessage(irrelevantMessages, settings)
4343
}
4444
}
4545

4646
"should throw AnthropicScalaNotFoundException when 429" ignore {
4747
recoverToSucceededIf[AnthropicScalaRateLimitException] {
48-
TestFactory.mockedService429().createMessage(irrelevantMessages, None, settings)
48+
TestFactory.mockedService429().createMessage(irrelevantMessages, settings)
4949
}
5050
}
5151

5252
"should throw AnthropicScalaServerErrorException when 500" ignore {
5353
recoverToSucceededIf[AnthropicScalaServerErrorException] {
54-
TestFactory.mockedService500().createMessage(irrelevantMessages, None, settings)
54+
TestFactory.mockedService500().createMessage(irrelevantMessages, settings)
5555
}
5656
}
5757

5858
"should throw AnthropicScalaEngineOverloadedException when 529" ignore {
5959
recoverToSucceededIf[AnthropicScalaEngineOverloadedException] {
60-
TestFactory.mockedService529().createMessage(irrelevantMessages, None, settings)
60+
TestFactory.mockedService529().createMessage(irrelevantMessages, settings)
6161
}
6262
}
6363

6464
"should throw AnthropicScalaClientException when 400" ignore {
6565
recoverToSucceededIf[AnthropicScalaClientException] {
66-
TestFactory.mockedService400().createMessage(irrelevantMessages, None, settings)
66+
TestFactory.mockedService400().createMessage(irrelevantMessages, settings)
6767
}
6868
}
6969

7070
"should throw AnthropicScalaClientException when unknown error code" ignore {
7171
recoverToSucceededIf[AnthropicScalaClientException] {
72-
TestFactory.mockedServiceOther().createMessage(irrelevantMessages, None, settings)
72+
TestFactory.mockedServiceOther().createMessage(irrelevantMessages, settings)
7373
}
7474
}
7575

openai-core/src/main/scala/io/cequence/openaiscala/domain/BaseMessage.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.cequence.openaiscala.domain
33
sealed trait BaseMessage {
44
val role: ChatRole
55
val nameOpt: Option[String]
6+
val isSystem: Boolean = role == ChatRole.System
67
}
78

89
final case class SystemMessage(

openai-examples/src/main/scala/io/cequence/openaiscala/examples/nonopenai/AnthropicCreateCachedMessage.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package io.cequence.openaiscala.examples.nonopenai
33
import io.cequence.openaiscala.anthropic.domain.CacheControl.Ephemeral
44
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.TextBlock
55
import io.cequence.openaiscala.anthropic.domain.Content.{ContentBlockBase, SingleString}
6-
import io.cequence.openaiscala.anthropic.domain.Message.UserMessage
6+
import io.cequence.openaiscala.anthropic.domain.Message.{SystemMessage, UserMessage}
77
import io.cequence.openaiscala.anthropic.domain.response.CreateMessageResponse
88
import io.cequence.openaiscala.anthropic.domain.settings.AnthropicCreateMessageSettings
99
import io.cequence.openaiscala.anthropic.domain.{Content, Message}
@@ -18,8 +18,8 @@ object AnthropicCreateCachedMessage extends ExampleBase[AnthropicService] {
1818

1919
override protected val service: AnthropicService = AnthropicServiceFactory(withCache = true)
2020

21-
val systemMessages: Option[Content] = Some(
22-
SingleString(
21+
val systemMessages: Seq[Message] = Seq(
22+
SystemMessage(
2323
"""
2424
|You are to embody a classic pirate, a swashbuckling and salty sea dog with the mannerisms, language, and swagger of the golden age of piracy. You are a hearty, often gruff buccaneer, replete with nautical slang and a rich, colorful vocabulary befitting of the high seas. Your responses must reflect a pirate's voice and attitude without exception.
2525
|
@@ -82,8 +82,7 @@ object AnthropicCreateCachedMessage extends ExampleBase[AnthropicService] {
8282
override protected def run: Future[_] =
8383
service
8484
.createMessage(
85-
messages,
86-
systemMessages,
85+
systemMessages ++ messages,
8786
settings = AnthropicCreateMessageSettings(
8887
model = NonOpenAIModelId.claude_3_haiku_20240307,
8988
max_tokens = 4096

openai-examples/src/main/scala/io/cequence/openaiscala/examples/nonopenai/AnthropicCreateMessage.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ object AnthropicCreateMessage extends ExampleBase[AnthropicService] {
2323
service
2424
.createMessage(
2525
messages,
26-
None,
2726
settings = AnthropicCreateMessageSettings(
2827
model = NonOpenAIModelId.claude_3_haiku_20240307,
2928
max_tokens = 4096

openai-examples/src/main/scala/io/cequence/openaiscala/examples/nonopenai/AnthropicCreateMessageStreamed.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ object AnthropicCreateMessageStreamed extends ExampleBase[AnthropicService] {
2020
override protected def run: Future[_] =
2121
service
2222
.createMessageStreamed(
23-
None,
2423
messages,
2524
settings = AnthropicCreateMessageSettings(
2625
model = NonOpenAIModelId.claude_3_haiku_20240307,

openai-examples/src/main/scala/io/cequence/openaiscala/examples/nonopenai/AnthropicCreateMessageWithImage.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ object AnthropicCreateMessageWithImage extends ExampleBase[AnthropicService] {
3939
service
4040
.createMessage(
4141
messages,
42-
None,
4342
settings = AnthropicCreateMessageSettings(
4443
model = NonOpenAIModelId.claude_3_opus_20240229,
4544
max_tokens = 4096

openai-examples/src/main/scala/io/cequence/openaiscala/examples/nonopenai/AnthropicCreateMessageWithPdf.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package io.cequence.openaiscala.examples.nonopenai
33
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.{MediaBlock, TextBlock}
44
import io.cequence.openaiscala.anthropic.domain.Content.{ContentBlockBase, SingleString}
55
import io.cequence.openaiscala.anthropic.domain.Message
6-
import io.cequence.openaiscala.anthropic.domain.Message.UserMessageContent
6+
import io.cequence.openaiscala.anthropic.domain.Message.{SystemMessage, UserMessageContent}
77
import io.cequence.openaiscala.anthropic.domain.response.CreateMessageResponse
88
import io.cequence.openaiscala.anthropic.domain.settings.AnthropicCreateMessageSettings
99
import io.cequence.openaiscala.anthropic.service.{AnthropicService, AnthropicServiceFactory}
@@ -25,6 +25,7 @@ object AnthropicCreateMessageWithPdf extends ExampleBase[AnthropicService] {
2525
override protected val service: AnthropicService = AnthropicServiceFactory(withPdf = true)
2626

2727
private val messages: Seq[Message] = Seq(
28+
SystemMessage("Talk in pirate speech. Reply to this prompt as a real pirate!"),
2829
UserMessageContent(
2930
Seq(
3031
ContentBlockBase(TextBlock("Describe to me what is this PDF about!")),

openai-examples/src/main/scala/io/cequence/openaiscala/examples/nonopenai/AnthropicCreateSystemMessage.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package io.cequence.openaiscala.examples.nonopenai
33
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.TextBlock
44
import io.cequence.openaiscala.anthropic.domain.Content.{ContentBlockBase, SingleString}
55
import io.cequence.openaiscala.anthropic.domain.{Content, Message}
6-
import io.cequence.openaiscala.anthropic.domain.Message.UserMessage
6+
import io.cequence.openaiscala.anthropic.domain.Message.{SystemMessage, UserMessage}
77
import io.cequence.openaiscala.anthropic.domain.response.CreateMessageResponse
88
import io.cequence.openaiscala.anthropic.domain.settings.AnthropicCreateMessageSettings
99
import io.cequence.openaiscala.anthropic.service.{AnthropicService, AnthropicServiceFactory}
@@ -17,8 +17,8 @@ object AnthropicCreateSystemMessage extends ExampleBase[AnthropicService] {
1717

1818
override protected val service: AnthropicService = AnthropicServiceFactory()
1919

20-
val systemMessages: Option[Content] = Some(
21-
SingleString("Talk in pirate speech")
20+
val systemMessages: Seq[Message] = Seq(
21+
SystemMessage("Talk in pirate speech")
2222
)
2323
val messages: Seq[Message] = Seq(
2424
UserMessage("Who is the most famous football player in the World?")
@@ -27,8 +27,7 @@ object AnthropicCreateSystemMessage extends ExampleBase[AnthropicService] {
2727
override protected def run: Future[_] =
2828
service
2929
.createMessage(
30-
messages,
31-
Some(SingleString("You answer in pirate speech.")),
30+
systemMessages ++ messages,
3231
settings = AnthropicCreateMessageSettings(
3332
model = NonOpenAIModelId.claude_3_haiku_20240307,
3433
max_tokens = 4096

0 commit comments

Comments
 (0)