Skip to content

Commit 54093be

Browse files
committed
DocumentBlock for Anthropic PDF and CacheControl
1 parent fb2f54e commit 54093be

File tree

4 files changed

+220
-39
lines changed

4 files changed

+220
-39
lines changed

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/JsonFormats.scala

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package io.cequence.openaiscala.anthropic
22

33
import io.cequence.openaiscala.anthropic.domain.CacheControl.Ephemeral
4-
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.{ImageBlock, TextBlock}
4+
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.{
5+
DocumentBlock,
6+
ImageBlock,
7+
TextBlock
8+
}
59
import io.cequence.openaiscala.anthropic.domain.Content.{
610
ContentBlock,
711
ContentBlockBase,
@@ -35,6 +39,30 @@ trait JsonFormats {
3539
JsonUtil.enumFormat[ChatRole](ChatRole.allValues: _*)
3640
implicit lazy val usageInfoFormat: Format[UsageInfo] = Json.format[UsageInfo]
3741

42+
implicit lazy val cacheControlFormat: Format[CacheControl] = new Format[CacheControl] {
43+
def reads(json: JsValue): JsResult[CacheControl] = json match {
44+
case JsObject(Seq(("type", JsString("ephemeral")))) => JsSuccess(CacheControl.Ephemeral)
45+
case _ => JsError("Invalid cache control")
46+
}
47+
48+
def writes(cacheControl: CacheControl): JsValue = cacheControl match {
49+
case CacheControl.Ephemeral => Json.obj("type" -> "ephemeral")
50+
}
51+
}
52+
53+
implicit lazy val cacheControlOptionFormat: Format[Option[CacheControl]] =
54+
new Format[Option[CacheControl]] {
55+
def reads(json: JsValue): JsResult[Option[CacheControl]] = json match {
56+
case JsNull => JsSuccess(None)
57+
case _ => cacheControlFormat.reads(json).map(Some(_))
58+
}
59+
60+
def writes(option: Option[CacheControl]): JsValue = option match {
61+
case None => JsNull
62+
case Some(cacheControl) => cacheControlFormat.writes(cacheControl)
63+
}
64+
}
65+
3866
implicit lazy val userMessageFormat: Format[UserMessage] = Json.format[UserMessage]
3967
implicit lazy val userMessageContentFormat: Format[UserMessageContent] =
4068
Json.format[UserMessageContent]
@@ -59,25 +87,8 @@ trait JsonFormats {
5987
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
6088
Json.writes[TextBlock]
6189
}
62-
// implicit lazy val imageBlockWrites: Writes[ImageBlock] =
63-
// (block: ImageBlock) =>
64-
// Json.obj(
65-
// "type" -> "image",
66-
// "source" -> Json.obj(
67-
// "type" -> block.`type`,
68-
// "media_type" -> block.mediaType,
69-
// "data" -> block.data
70-
// )
71-
// )
72-
73-
implicit lazy val contentBlockWrites: Writes[ContentBlockBase] = {
74-
case ContentBlockBase(tb: TextBlock, None) =>
75-
Json.obj("type" -> "text") ++ Json.toJson(tb)(textBlockWrites).as[JsObject]
76-
case ContentBlockBase(tb: TextBlock, Some(Ephemeral)) =>
77-
Json.obj("type" -> "text", "cache_control" -> "ephemeral") ++ Json
78-
.toJson(tb)(textBlockWrites)
79-
.as[JsObject]
80-
case ContentBlockBase(block: ImageBlock, None) =>
90+
implicit lazy val imageBlockWrites: Writes[ImageBlock] =
91+
(block: ImageBlock) =>
8192
Json.obj(
8293
"type" -> "image",
8394
"source" -> Json.obj(
@@ -86,16 +97,35 @@ trait JsonFormats {
8697
"data" -> block.data
8798
)
8899
)
89-
case ContentBlockBase(block: ImageBlock, Some(Ephemeral)) =>
100+
implicit lazy val documentBlockWrites: Writes[DocumentBlock] =
101+
(block: DocumentBlock) =>
90102
Json.obj(
91-
"type" -> "image",
92-
"cache_control" -> "ephemeral",
103+
"type" -> "document",
93104
"source" -> Json.obj(
94105
"type" -> block.`type`,
95106
"media_type" -> block.mediaType,
96107
"data" -> block.data
97108
)
98109
)
110+
111+
private def cacheControlToJsObject(maybeCacheControl: Option[CacheControl]): JsObject =
112+
maybeCacheControl.fold(Json.obj())(cc => Json.obj("cache_control" -> Json.toJson(cc)))
113+
114+
implicit lazy val contentBlockWrites: Writes[ContentBlockBase] = {
115+
case ContentBlockBase(textBlock @ TextBlock(_), cacheControl) =>
116+
Json.obj("type" -> "text") ++
117+
Json.toJson(textBlock)(textBlockWrites).as[JsObject] ++
118+
cacheControlToJsObject(cacheControl)
119+
case ContentBlockBase(imageBlock @ ImageBlock(_, _, _), maybeCacheControl) =>
120+
Json.toJson(imageBlock)(imageBlockWrites).as[JsObject] ++
121+
cacheControlToJsObject(maybeCacheControl)
122+
case ContentBlockBase(documentBlock @ DocumentBlock(_, _, _), maybeCacheControl) =>
123+
Json.toJson(documentBlock)(documentBlockWrites).as[JsObject] ++
124+
cacheControlToJsObject(maybeCacheControl) ++
125+
maybeCacheControl
126+
.map(cc => Json.toJson(cc)(cacheControlFormat.writes))
127+
.getOrElse(Json.obj())
128+
99129
}
100130

101131
implicit lazy val contentBlockReads: Reads[ContentBlockBase] =
@@ -117,20 +147,33 @@ trait JsonFormats {
117147
data <- (source \ "data").validate[String]
118148
cacheControl <- (json \ "cache_control").validateOpt[CacheControl]
119149
} yield ContentBlockBase(ImageBlock(`type`, mediaType, data), cacheControl)
150+
151+
case "document" =>
152+
for {
153+
source <- (json \ "source").validate[JsObject]
154+
`type` <- (source \ "type").validate[String]
155+
mediaType <- (source \ "media_type").validate[String]
156+
data <- (source \ "data").validate[String]
157+
cacheControl <- (json \ "cache_control").validateOpt[CacheControl]
158+
} yield ContentBlockBase(DocumentBlock(`type`, mediaType, data), cacheControl)
159+
120160
case _ => JsError("Unsupported or invalid content block")
121161
}
122162
}
123163

124164
// CacheControl Reads and Writes
125-
implicit lazy val cacheControlReads: Reads[CacheControl] = Reads[CacheControl] {
126-
case JsString("ephemeral") => JsSuccess(CacheControl.Ephemeral)
127-
case JsNull | JsUndefined() => JsSuccess(null)
128-
case _ => JsError("Invalid cache control")
129-
}
130-
131-
implicit lazy val cacheControlWrites: Writes[CacheControl] = Writes[CacheControl] {
132-
case CacheControl.Ephemeral => JsString("ephemeral")
133-
}
165+
// implicit lazy val cacheControlReads: Reads[Option[CacheControl]] =
166+
// Reads[Option[CacheControl]] {
167+
// case JsObject(Seq("type", JsString("ephemeral"))) =>
168+
// JsSuccess(Some(CacheControl.Ephemeral))
169+
// case JsNull | JsUndefined() => JsSuccess(None)
170+
// case _ => JsError("Invalid cache control")
171+
// }
172+
//
173+
// implicit lazy val cacheControlWrites: Writes[CacheControl] =
174+
// Writes[CacheControl] { case CacheControl.Ephemeral =>
175+
// Json.obj("cache_control" -> Json.obj("type" -> "ephemeral"))
176+
// }
134177

135178
implicit lazy val contentReads: Reads[Content] = new Reads[Content] {
136179
def reads(json: JsValue): JsResult[Content] = json match {

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,98 @@ object Content {
3131
object ContentBlock {
3232
case class TextBlock(text: String) extends ContentBlock
3333

34+
case class MediaBlock(
35+
`type`: String,
36+
encoding: String,
37+
mediaType: String,
38+
data: String
39+
) extends ContentBlock
40+
41+
object MediaBlock {
42+
def pdf(
43+
data: String,
44+
cacheControl: Option[CacheControl] = None
45+
): ContentBlockBase =
46+
ContentBlockBase(
47+
MediaBlock("document", "base64", "application/pdf", data),
48+
cacheControl
49+
)
50+
51+
def image(
52+
mediaType: String
53+
)(
54+
data: String,
55+
cacheControl: Option[CacheControl] = None
56+
): ContentBlockBase =
57+
ContentBlockBase(MediaBlock("image", "base64", mediaType, data), cacheControl)
58+
59+
def jpeg(
60+
data: String,
61+
cacheControl: Option[CacheControl] = None
62+
): ContentBlockBase = image("image/jpeg")(data, cacheControl)
63+
64+
def png(
65+
data: String,
66+
cacheControl: Option[CacheControl] = None
67+
): ContentBlockBase = image("image/png")(data, cacheControl)
68+
69+
def gif(
70+
data: String,
71+
cacheControl: Option[CacheControl] = None
72+
): ContentBlockBase = image("image/gif")(data, cacheControl)
73+
74+
def webp(
75+
data: String,
76+
cacheControl: Option[CacheControl] = None
77+
): ContentBlockBase = image("image/webp")(data, cacheControl)
78+
}
79+
3480
case class ImageBlock(
3581
`type`: String,
3682
mediaType: String,
3783
data: String
3884
) extends ContentBlock
3985

86+
case class DocumentBlock(
87+
`type`: String,
88+
mediaType: String,
89+
data: String
90+
) extends ContentBlock
91+
92+
object DocumentBlock {
93+
def pdf(
94+
data: String,
95+
cacheControl: Option[CacheControl]
96+
): ContentBlockBase =
97+
ContentBlockBase(DocumentBlock("base64", "application/pdf", data), cacheControl)
98+
}
99+
100+
object ImageBlock {
101+
def jpeg(
102+
data: String,
103+
cacheControl: Option[CacheControl]
104+
): ContentBlockBase =
105+
ContentBlockBase(ImageBlock("base64", "image/jpeg", data), cacheControl)
106+
107+
def png(
108+
data: String,
109+
cacheControl: Option[CacheControl]
110+
): ContentBlockBase =
111+
ContentBlockBase(ImageBlock("base64", "image/png", data), cacheControl)
112+
113+
def gif(
114+
data: String,
115+
cacheControl: Option[CacheControl]
116+
): ContentBlockBase =
117+
ContentBlockBase(ImageBlock("base64", "image/gif", data), cacheControl)
118+
119+
def webp(
120+
data: String,
121+
cacheControl: Option[CacheControl]
122+
): ContentBlockBase =
123+
ContentBlockBase(ImageBlock("base64", "image/webp", data), cacheControl)
124+
}
125+
40126
// TODO: check PDF
41127
}
42128
}

anthropic-client/src/test/scala/io/cequence/openaiscala/anthropic/JsonFormatsSpec.scala

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ package io.cequence.openaiscala.anthropic
22

33
import io.cequence.openaiscala.anthropic.JsonFormatsSpec.JsonPrintMode
44
import io.cequence.openaiscala.anthropic.JsonFormatsSpec.JsonPrintMode.{Compact, Pretty}
5-
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.{ImageBlock, TextBlock}
5+
import io.cequence.openaiscala.anthropic.domain.CacheControl.Ephemeral
6+
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.{
7+
ImageBlock,
8+
MediaBlock,
9+
TextBlock
10+
}
611
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlockBase
712
import io.cequence.openaiscala.anthropic.domain.Message
813
import io.cequence.openaiscala.anthropic.domain.Message.{
@@ -80,11 +85,55 @@ class JsonFormatsSpec extends AnyWordSpecLike with Matchers with JsonFormats {
8085
"serialize and deserialize a message with an image content" in {
8186
val userMessage =
8287
UserMessageContent(
83-
Seq(ContentBlockBase(ImageBlock("base64", "image/jpeg", "/9j/4AAQSkZJRg...")))
88+
Seq(
89+
ContentBlockBase(MediaBlock("image", "base64", "image/jpeg", "/9j/4AAQSkZJRg..."))
90+
)
8491
)
8592
testCodec[Message](userMessage, expectedImageContentJson, Pretty)
8693
}
8794

95+
// TEST CACHING
96+
"serialize and deserialize Cache control" should {
97+
"serialize and deserialize arbitrary (first) user message with caching" in {
98+
val userMessage =
99+
UserMessageContent(
100+
Seq(
101+
ContentBlockBase(TextBlock("Hello, world!"), Some(Ephemeral)),
102+
ContentBlockBase(TextBlock("How are you?"))
103+
)
104+
)
105+
val json =
106+
"""{"role":"user","content":[{"type":"text","text":"Hello, world!","cache_control":"ephemeral"},{"type":"text","text":"How are you?"}]}"""
107+
testCodec[Message](userMessage, json)
108+
}
109+
110+
"serialize and deserialize arbitrary (second) user message with caching" in {
111+
val userMessage =
112+
UserMessageContent(
113+
Seq(
114+
ContentBlockBase(TextBlock("Hello, world!")),
115+
ContentBlockBase(TextBlock("How are you?"), Some(Ephemeral))
116+
)
117+
)
118+
val json =
119+
"""{"role":"user","content":[{"type":"text","text":"Hello, world!"},{"type":"text","text":"How are you?","cache_control":"ephemeral"}]}"""
120+
testCodec[Message](userMessage, json)
121+
}
122+
123+
"serialize and deserialize arbitrary (first) image content with caching" in {
124+
val userMessage =
125+
UserMessageContent(
126+
Seq(
127+
ImageBlock.jpeg("Hello, world!", Some(Ephemeral)),
128+
ContentBlockBase(TextBlock("How are you?"))
129+
)
130+
)
131+
val json =
132+
"""{"role":"user","content":[{"type":"text","text":"Hello, world!","cache_control":"ephemeral"},{"type":"text","text":"How are you?"}]}"""
133+
testCodec[Message](userMessage, json)
134+
}
135+
}
136+
88137
}
89138

90139
private def testCodec[A](
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package io.cequence.openaiscala.examples
22

3+
import java.io.File
34
import scala.concurrent.Future
45
object CreateAudioTranscription extends Example {
56

6-
private val audioFile = getClass.getResource("/wolfgang.mp3").getFile
7+
private val audioFile: String = Option(
8+
getClass.getClassLoader.getResource("question-last-164421.mp3")
9+
).map(_.getFile).getOrElse(throw new RuntimeException("Audio file not found"))
710

8-
override protected def run: Future[Unit] =
11+
override protected def run: Future[Unit] = {
912
service
10-
.createAudioTranscription(
11-
new java.io.File(audioFile)
12-
)
13+
.createAudioTranscription(new File(audioFile))
1314
.map(response => println(response.text))
15+
// Future.successful(())
16+
}
1417
}

0 commit comments

Comments
 (0)