Skip to content

Commit b3555cc

Browse files
committed
Anthropic - web fetch tool result and examples
1 parent 26dac98 commit b3555cc

File tree

6 files changed

+331
-19
lines changed

6 files changed

+331
-19
lines changed

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

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.{
1616
TextsContentBlock,
1717
ThinkingBlock,
1818
ToolUseBlock,
19+
WebFetchToolResultBlock,
1920
WebSearchToolResultBlock
2021
}
2122
import io.cequence.openaiscala.anthropic.domain.Content.{
@@ -47,6 +48,7 @@ import io.cequence.openaiscala.anthropic.domain._
4748
import io.cequence.openaiscala.anthropic.domain.CodeExecutionToolResultContent.CodeExecutionErrorCode
4849
import io.cequence.openaiscala.anthropic.domain.BashCodeExecutionToolResultContent.BashCodeExecutionErrorCode
4950
import io.cequence.openaiscala.anthropic.domain.WebSearchToolResultContent.WebSearchErrorCode
51+
import io.cequence.openaiscala.anthropic.domain.WebFetchToolResultContent.WebFetchErrorCode
5052
import io.cequence.openaiscala.JsonFormats.jsonSchemaFormat
5153
import io.cequence.openaiscala.anthropic.domain.skills.{
5254
Container,
@@ -139,7 +141,7 @@ trait JsonFormats {
139141
// content block - raw - one to one with json
140142
implicit val textContentRawFormat: Format[TextContentRaw] = Json.format[TextContentRaw]
141143

142-
implicit val citationsFlagRawFormat: Format[CitationsFlagRaw] = Json.format[CitationsFlagRaw]
144+
implicit val citationsFlagRawFormat: Format[CitationsFlag] = Json.format[CitationsFlag]
143145

144146
implicit val sourceBlockRawFormat: Format[SourceBlockRaw] = {
145147
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
@@ -267,6 +269,58 @@ trait JsonFormats {
267269
Json.format[WebSearchToolResultBlock]
268270
}
269271

272+
implicit lazy val webFetchErrorCodeFormat: Format[WebFetchToolResultContent.WebFetchErrorCode] =
273+
JsonUtil.enumFormat[WebFetchToolResultContent.WebFetchErrorCode](
274+
WebFetchToolResultContent.WebFetchErrorCode.values: _*
275+
)
276+
277+
private implicit val webFetchSourceFormat: OFormat[WebFetchToolResultContent.Source] = {
278+
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
279+
Json.format[WebFetchToolResultContent.Source]
280+
}
281+
282+
private implicit val webFetchDocumentFormat: OFormat[WebFetchToolResultContent.Document] = {
283+
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
284+
formatWithType(Json.format[WebFetchToolResultContent.Document])
285+
}
286+
287+
private implicit val webFetchSuccessFormat: OFormat[WebFetchToolResultContent.Success] = {
288+
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
289+
formatWithType(Json.format[WebFetchToolResultContent.Success])
290+
}
291+
292+
private implicit val webFetchErrorFormat: Format[WebFetchToolResultContent.Error] = {
293+
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
294+
formatWithType(Json.format[WebFetchToolResultContent.Error])
295+
}
296+
297+
implicit lazy val webFetchToolResultContentReads: Reads[WebFetchToolResultContent] = {
298+
case obj: JsObject =>
299+
(obj \ "type").asOpt[String] match {
300+
case Some("web_fetch_tool_result_error") =>
301+
obj.validate[WebFetchToolResultContent.Error]
302+
case _ =>
303+
obj.validate[WebFetchToolResultContent.Success]
304+
}
305+
case _ =>
306+
JsError("Expected object for web fetch tool result content")
307+
}
308+
309+
implicit lazy val webFetchToolResultContentWrites: Writes[WebFetchToolResultContent] = {
310+
case success: WebFetchToolResultContent.Success =>
311+
Json.toJson(success)(webFetchSuccessFormat)
312+
case error: WebFetchToolResultContent.Error =>
313+
Json.toJson(error)(webFetchErrorFormat)
314+
}
315+
316+
implicit lazy val webFetchToolResultContentFormat: Format[WebFetchToolResultContent] =
317+
Format(webFetchToolResultContentReads, webFetchToolResultContentWrites)
318+
319+
private val webFetchToolResultBlockFormat: OFormat[WebFetchToolResultBlock] = {
320+
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
321+
Json.format[WebFetchToolResultBlock]
322+
}
323+
270324
private implicit val toolResultContentFormat: OFormat[MCPToolResultItem] = {
271325
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
272326
formatWithType(Json.using[Json.WithDefaultValues].format[MCPToolResultItem])
@@ -533,6 +587,9 @@ trait JsonFormats {
533587
case "web_search_tool_result" =>
534588
json.validate[WebSearchToolResultBlock](webSearchToolResultBlockFormat)
535589

590+
case "web_fetch_tool_result" =>
591+
json.validate[WebFetchToolResultBlock](webFetchToolResultBlockFormat)
592+
536593
case "mcp_tool_use" =>
537594
json.validate[McpToolUseBlock](mcpToolUseBlockFormat)
538595

@@ -613,6 +670,9 @@ trait JsonFormats {
613670
case x: WebSearchToolResultBlock =>
614671
Json.toJsObject(x)(webSearchToolResultBlockFormat)
615672

673+
case x: WebFetchToolResultBlock =>
674+
Json.toJsObject(x)(webFetchToolResultBlockFormat)
675+
616676
case x: McpToolUseBlock =>
617677
Json.toJsObject(x)(mcpToolUseBlockFormat)
618678

@@ -641,7 +701,7 @@ trait JsonFormats {
641701
),
642702
title = x.title,
643703
context = x.context,
644-
citations = if (x.citations.getOrElse(false)) Some(CitationsFlagRaw(true)) else None
704+
citations = if (x.citations.getOrElse(false)) Some(CitationsFlag(true)) else None
645705
)
646706
)(sourceContentBlockRawFormat)
647707

@@ -658,7 +718,7 @@ trait JsonFormats {
658718
),
659719
title = x.title,
660720
context = x.context,
661-
citations = if (x.citations.getOrElse(false)) Some(CitationsFlagRaw(true)) else None
721+
citations = if (x.citations.getOrElse(false)) Some(CitationsFlag(true)) else None
662722
)
663723
)(sourceContentBlockRawFormat)
664724

@@ -671,7 +731,7 @@ trait JsonFormats {
671731
),
672732
title = x.title,
673733
context = x.context,
674-
citations = if (x.citations.getOrElse(false)) Some(CitationsFlagRaw(true)) else None
734+
citations = if (x.citations.getOrElse(false)) Some(CitationsFlag(true)) else None
675735
)
676736
)(sourceContentBlockRawFormat)
677737
}

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -342,37 +342,36 @@ object WebSearchToolResultContent {
342342
}
343343
}
344344

345-
sealed trait WebFetchToolResultContent
345+
sealed trait WebFetchToolResultContent extends HasType
346346

347347
object WebFetchToolResultContent {
348348

349349
case class Success(
350-
document: Document,
350+
content: Document,
351351
url: String,
352352
retrievedAt: String
353-
) extends WebFetchToolResultContent
353+
) extends WebFetchToolResultContent {
354+
val `type`: String = "web_fetch_result"
355+
}
354356

355357
case class Error(
356358
errorCode: WebFetchErrorCode
357-
) extends WebFetchToolResultContent
358-
with HasType {
359+
) extends WebFetchToolResultContent {
359360
val `type`: String = "web_fetch_tool_result_error"
360361
}
361362

362363
case class Document(
363-
citations: Citations,
364+
citations: CitationsFlag,
364365
source: Source,
365366
title: String
366-
)
367-
368-
case class Citations(
369-
enabled: Boolean
370-
)
367+
) extends HasType {
368+
override val `type` = "document"
369+
}
371370

372371
case class Source(
373372
data: String,
374373
mediaType: String,
375-
`type`: String
374+
`type`: String // Allowed value: "text" and "base64"
376375
)
377376

378377
sealed trait WebFetchErrorCode extends EnumValue

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ case class SourceContentBlockRaw(
44
source: SourceBlockRaw,
55
title: Option[String] = None,
66
context: Option[String] = None,
7-
citations: Option[CitationsFlagRaw] = None
7+
citations: Option[CitationsFlag] = None
88
)
99

1010
case class SourceBlockRaw(
@@ -15,7 +15,7 @@ case class SourceBlockRaw(
1515
fileId: Option[String] = None
1616
)
1717

18-
case class CitationsFlagRaw(
18+
case class CitationsFlag(
1919
enabled: Boolean
2020
)
2121

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ object AnthropicServiceFactory extends AnthropicServiceConsts with EnvHelper {
3737
"output-128k-2025-02-19",
3838
"files-api-2025-04-14",
3939
"code-execution-2025-08-25",
40-
"mcp-client-2025-04-04"
40+
"mcp-client-2025-04-04",
41+
"web-fetch-2025-09-10"
4142
)
4243

4344
/**
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package io.cequence.openaiscala.examples.anthropic.tools
2+
3+
import io.cequence.openaiscala.anthropic.domain.Content.ContentBlock.WebFetchToolResultBlock
4+
import io.cequence.openaiscala.anthropic.domain.Message
5+
import io.cequence.openaiscala.anthropic.domain.Message.{SystemMessage, UserMessage}
6+
import io.cequence.openaiscala.anthropic.domain.WebFetchToolResultContent
7+
import io.cequence.openaiscala.anthropic.domain.settings.AnthropicCreateMessageSettings
8+
import io.cequence.openaiscala.anthropic.domain.tools.Tool
9+
import io.cequence.openaiscala.anthropic.service.{AnthropicService, AnthropicServiceFactory}
10+
import io.cequence.openaiscala.domain.NonOpenAIModelId
11+
import io.cequence.openaiscala.examples.ExampleBase
12+
13+
import scala.concurrent.Future
14+
15+
// requires `openai-scala-anthropic-client` as a dependency and `ANTHROPIC_API_KEY` environment variable to be set
16+
object AnthropicCreateMessageWithWebFetch extends ExampleBase[AnthropicService] {
17+
18+
override protected val service: AnthropicService = AnthropicServiceFactory()
19+
20+
private val model = NonOpenAIModelId.claude_sonnet_4_5_20250929
21+
22+
private val messages: Seq[Message] = Seq(
23+
SystemMessage("You are a helpful assistant with access to web fetch."),
24+
UserMessage(
25+
"Please fetch the content from https://docs.anthropic.com and summarize the main topics covered."
26+
)
27+
)
28+
29+
override protected def run: Future[_] =
30+
for {
31+
response <- service.createMessage(
32+
messages,
33+
settings = AnthropicCreateMessageSettings(
34+
model = model,
35+
max_tokens = 4096,
36+
tools = Seq(Tool.webFetch())
37+
)
38+
)
39+
40+
} yield {
41+
// Extract web fetch results
42+
val webFetchResults = response.blockContents.collect {
43+
case WebFetchToolResultBlock(content, toolUseId) =>
44+
(content, toolUseId)
45+
}
46+
47+
if (webFetchResults.nonEmpty) {
48+
println()
49+
println("=" * 60)
50+
println("Web Fetch Results:")
51+
println("=" * 60)
52+
println()
53+
54+
webFetchResults.foreach { case (content, toolUseId) =>
55+
println(s"Tool Use ID: $toolUseId")
56+
println()
57+
58+
content match {
59+
case WebFetchToolResultContent.Success(document, url, retrievedAt) =>
60+
println(s"Successfully fetched from: $url")
61+
println(s"Retrieved at: $retrievedAt")
62+
println()
63+
println(s"Document Title: ${document.title}")
64+
println(s"Citations Enabled: ${document.citations.enabled}")
65+
println()
66+
println(s"Source:")
67+
println(s" Type: ${document.source.`type`}")
68+
println(s" Media Type: ${document.source.mediaType}")
69+
println(
70+
s" Data (first 200 chars): ${document.source.data
71+
.take(200)}${if (document.source.data.length > 200) "..." else ""}"
72+
)
73+
println()
74+
75+
if (document.source.`type` == "base64") {
76+
println(
77+
"Note: The source data is base64-encoded and can be decoded to access the original content."
78+
)
79+
println()
80+
}
81+
82+
case WebFetchToolResultContent.Error(errorCode) =>
83+
println(s"Error fetching content: $errorCode")
84+
println()
85+
errorCode match {
86+
case WebFetchToolResultContent.WebFetchErrorCode.invalid_tool_input =>
87+
println("The provided input was invalid.")
88+
case WebFetchToolResultContent.WebFetchErrorCode.url_too_long =>
89+
println("The URL is too long.")
90+
case WebFetchToolResultContent.WebFetchErrorCode.url_not_allowed =>
91+
println("The URL is not in the allowed domains list.")
92+
case WebFetchToolResultContent.WebFetchErrorCode.url_not_accessible =>
93+
println("The URL could not be accessed.")
94+
case WebFetchToolResultContent.WebFetchErrorCode.unsupported_content_type =>
95+
println("The content type is not supported.")
96+
case WebFetchToolResultContent.WebFetchErrorCode.too_many_requests =>
97+
println("Too many requests were made.")
98+
case WebFetchToolResultContent.WebFetchErrorCode.max_uses_exceeded =>
99+
println("Maximum number of uses has been exceeded.")
100+
case WebFetchToolResultContent.WebFetchErrorCode.unavailable =>
101+
println("The web fetch service is currently unavailable.")
102+
}
103+
println()
104+
}
105+
106+
println("-" * 60)
107+
}
108+
}
109+
110+
// Display all content blocks
111+
println()
112+
println("=" * 60)
113+
println("Complete Response:")
114+
println("=" * 60)
115+
response.blockContents.zipWithIndex.foreach { case (blockContent, index) =>
116+
println(s"Block ${index + 1}:")
117+
println(blockContent)
118+
println("=" * 60)
119+
}
120+
121+
// Display text summary
122+
if (response.texts.nonEmpty) {
123+
println()
124+
println("=" * 60)
125+
println("Text Summary:")
126+
println("=" * 60)
127+
println(response.text)
128+
println("=" * 60)
129+
}
130+
131+
// Display citations if present
132+
if (response.citations.exists(_.nonEmpty)) {
133+
println()
134+
println("=" * 60)
135+
println("Citations:")
136+
println("=" * 60)
137+
response.citations.zipWithIndex.foreach { case (citationSeq, textIndex) =>
138+
if (citationSeq.nonEmpty) {
139+
println(s"For text block ${textIndex + 1}:")
140+
citationSeq.zipWithIndex.foreach { case (citation, citIndex) =>
141+
println(s" Citation ${citIndex + 1}:")
142+
println(s" Cited Text: ${citation.citedText}")
143+
println(s" Type: ${citation.`type`}")
144+
println()
145+
}
146+
}
147+
}
148+
println("=" * 60)
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)