Skip to content

Commit a1f32d1

Browse files
committed
Backport Skills API support fixes for 1.1.x branch
- Add missing ContentBlock.Type enum values for Skills responses: BASH_CODE_EXECUTION_TOOL_RESULT, TEXT_EDITOR_CODE_EXECUTION_TOOL_RESULT, SERVER_TOOL_USE - Change ContentBlock.content field from String to Object to support complex nested structures in Skills responses - Add ContentBlock.filename field for Skills-generated files - Add contentAsString() convenience method with migration guidance - Add serializeContent() helper in AnthropicChatModel - Store anthropic-response in ChatResponseMetadata for SkillsResponseHelper to extract file IDs - Update StreamHelper constructor calls for new ContentBlock signature - Add Skills documentation to anthropic-chat.adoc Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
1 parent b7a36bb commit a1f32d1

File tree

5 files changed

+884
-21
lines changed

5 files changed

+884
-21
lines changed

models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,8 @@ private ChatResponse toChatResponse(ChatCompletionResponse chatCompletion, Usage
381381
.usage(usage)
382382
.keyValue("stop-reason", chatCompletion.stopReason())
383383
.keyValue("stop-sequence", chatCompletion.stopSequence())
384-
.keyValue("type", chatCompletion.type());
384+
.keyValue("type", chatCompletion.type())
385+
.keyValue("anthropic-response", chatCompletion);
385386

386387
// Add citation metadata if citations were found
387388
if (citationContext.hasCitations()) {
@@ -711,11 +712,21 @@ else if (this.defaultOptions.getSkillContainer() != null) {
711712
return request;
712713
}
713714

715+
private static String serializeContent(Object content) {
716+
if (content == null) {
717+
return null;
718+
}
719+
if (content instanceof String s) {
720+
return s;
721+
}
722+
return JsonParser.toJson(content);
723+
}
724+
714725
private static ContentBlock cacheAwareContentBlock(ContentBlock contentBlock, MessageType messageType,
715726
CacheEligibilityResolver cacheEligibilityResolver) {
716727
String basisForLength = switch (contentBlock.type()) {
717728
case TEXT, TEXT_DELTA -> contentBlock.text();
718-
case TOOL_RESULT -> contentBlock.content();
729+
case TOOL_RESULT -> serializeContent(contentBlock.content());
719730
case TOOL_USE -> JsonParser.toJson(contentBlock.input());
720731
case THINKING, THINKING_DELTA -> contentBlock.thinking();
721732
case REDACTED_THINKING -> contentBlock.data();

models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,7 +1155,7 @@ public record ContentBlock(
11551155

11561156
// tool_result response only
11571157
@JsonProperty("tool_use_id") String toolUseId,
1158-
@JsonProperty("content") String content,
1158+
@JsonProperty("content") Object content,
11591159

11601160
// Thinking only
11611161
@JsonProperty("signature") String signature,
@@ -1172,8 +1172,9 @@ public record ContentBlock(
11721172
@JsonProperty("context") String context,
11731173
@JsonProperty("citations") Object citations, // Can be CitationsConfig for requests or List<CitationResponse> for responses
11741174

1175-
// File content block from Skills
1176-
@JsonProperty("file_id") String fileId
1175+
// File fields (for Skills-generated files)
1176+
@JsonProperty("file_id") String fileId,
1177+
@JsonProperty("filename") String filename
11771178
) {
11781179
// @formatter:on
11791180

@@ -1192,7 +1193,7 @@ public ContentBlock(String mediaType, String data) {
11921193
* @param source The source of the content.
11931194
*/
11941195
public ContentBlock(Type type, Source source) {
1195-
this(type, source, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
1196+
this(type, source, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
11961197
null);
11971198
}
11981199

@@ -1202,7 +1203,7 @@ public ContentBlock(Type type, Source source) {
12021203
*/
12031204
public ContentBlock(Source source) {
12041205
this(Type.IMAGE, source, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
1205-
null);
1206+
null, null);
12061207
}
12071208

12081209
/**
@@ -1211,12 +1212,12 @@ public ContentBlock(Source source) {
12111212
*/
12121213
public ContentBlock(String text) {
12131214
this(Type.TEXT, null, text, null, null, null, null, null, null, null, null, null, null, null, null, null,
1214-
null);
1215+
null, null);
12151216
}
12161217

12171218
public ContentBlock(String text, CacheControl cache) {
12181219
this(Type.TEXT, null, text, null, null, null, null, null, null, null, null, null, cache, null, null, null,
1219-
null);
1220+
null, null);
12201221
}
12211222

12221223
// Tool result
@@ -1228,7 +1229,7 @@ public ContentBlock(String text, CacheControl cache) {
12281229
*/
12291230
public ContentBlock(Type type, String toolUseId, String content) {
12301231
this(type, null, null, null, null, null, null, toolUseId, content, null, null, null, null, null, null, null,
1231-
null);
1232+
null, null);
12321233
}
12331234

12341235
/**
@@ -1240,7 +1241,7 @@ public ContentBlock(Type type, String toolUseId, String content) {
12401241
*/
12411242
public ContentBlock(Type type, Source source, String text, Integer index) {
12421243
this(type, source, text, index, null, null, null, null, null, null, null, null, null, null, null, null,
1243-
null);
1244+
null, null);
12441245
}
12451246

12461247
// Tool use input JSON delta streaming
@@ -1252,7 +1253,8 @@ public ContentBlock(Type type, Source source, String text, Integer index) {
12521253
* @param input The input of the tool use.
12531254
*/
12541255
public ContentBlock(Type type, String id, String name, Map<String, Object> input) {
1255-
this(type, null, null, null, id, name, input, null, null, null, null, null, null, null, null, null, null);
1256+
this(type, null, null, null, id, name, input, null, null, null, null, null, null, null, null, null, null,
1257+
null);
12561258
}
12571259

12581260
/**
@@ -1266,13 +1268,26 @@ public ContentBlock(Type type, String id, String name, Map<String, Object> input
12661268
public ContentBlock(Source source, String title, String context, boolean citationsEnabled,
12671269
CacheControl cacheControl) {
12681270
this(Type.DOCUMENT, source, null, null, null, null, null, null, null, null, null, null, cacheControl, title,
1269-
context, citationsEnabled ? new CitationsConfig(true) : null, null);
1271+
context, citationsEnabled ? new CitationsConfig(true) : null, null, null);
12701272
}
12711273

12721274
public static ContentBlockBuilder from(ContentBlock contentBlock) {
12731275
return new ContentBlockBuilder(contentBlock);
12741276
}
12751277

1278+
/**
1279+
* Returns the content as a String if it is a String, null otherwise.
1280+
* <p>
1281+
* Note: The {@link #content()} field was changed from {@code String} to
1282+
* {@code Object} to support Skills responses which may return complex nested
1283+
* structures. If you are using the low-level API and previously relied on
1284+
* {@code content()} returning a String, use this method instead.
1285+
* @return the content as String, or null if content is not a String
1286+
*/
1287+
public String contentAsString() {
1288+
return this.content instanceof String s ? s : null;
1289+
}
1290+
12761291
/**
12771292
* The ContentBlock type.
12781293
*/
@@ -1349,10 +1364,35 @@ public enum Type {
13491364
REDACTED_THINKING("redacted_thinking"),
13501365

13511366
/**
1352-
* File content block from Skills.
1367+
* File content block representing a file generated by Skills. Used in
1368+
* {@link org.springframework.ai.anthropic.SkillsResponseHelper} to extract
1369+
* file IDs for downloading generated documents.
13531370
*/
13541371
@JsonProperty("file")
1355-
FILE("file");
1372+
FILE("file"),
1373+
1374+
/**
1375+
* Bash code execution tool result returned in Skills responses. Observed in
1376+
* actual API responses where file IDs are nested within this content block.
1377+
* Required for JSON deserialization.
1378+
*/
1379+
@JsonProperty("bash_code_execution_tool_result")
1380+
BASH_CODE_EXECUTION_TOOL_RESULT("bash_code_execution_tool_result"),
1381+
1382+
/**
1383+
* Text editor code execution tool result returned in Skills responses.
1384+
* Observed in actual API responses. Required for JSON deserialization.
1385+
*/
1386+
@JsonProperty("text_editor_code_execution_tool_result")
1387+
TEXT_EDITOR_CODE_EXECUTION_TOOL_RESULT("text_editor_code_execution_tool_result"),
1388+
1389+
/**
1390+
* Server-side tool use returned in Skills responses. Observed in actual API
1391+
* responses when Skills invoke server-side tools. Required for JSON
1392+
* deserialization.
1393+
*/
1394+
@JsonProperty("server_tool_use")
1395+
SERVER_TOOL_USE("server_tool_use");
13561396

13571397
public final String value;
13581398

@@ -1426,7 +1466,7 @@ public static class ContentBlockBuilder {
14261466

14271467
private String toolUseId;
14281468

1429-
private String content;
1469+
private Object content;
14301470

14311471
private String signature;
14321472

@@ -1444,6 +1484,8 @@ public static class ContentBlockBuilder {
14441484

14451485
private String fileId;
14461486

1487+
private String filename;
1488+
14471489
public ContentBlockBuilder(ContentBlock contentBlock) {
14481490
this.type = contentBlock.type;
14491491
this.source = contentBlock.source;
@@ -1462,6 +1504,7 @@ public ContentBlockBuilder(ContentBlock contentBlock) {
14621504
this.context = contentBlock.context;
14631505
this.citations = contentBlock.citations;
14641506
this.fileId = contentBlock.fileId;
1507+
this.filename = contentBlock.filename;
14651508
}
14661509

14671510
public ContentBlockBuilder type(Type type) {
@@ -1504,7 +1547,7 @@ public ContentBlockBuilder toolUseId(String toolUseId) {
15041547
return this;
15051548
}
15061549

1507-
public ContentBlockBuilder content(String content) {
1550+
public ContentBlockBuilder content(Object content) {
15081551
this.content = content;
15091552
return this;
15101553
}
@@ -1534,10 +1577,15 @@ public ContentBlockBuilder fileId(String fileId) {
15341577
return this;
15351578
}
15361579

1580+
public ContentBlockBuilder filename(String filename) {
1581+
this.filename = filename;
1582+
return this;
1583+
}
1584+
15371585
public ContentBlock build() {
15381586
return new ContentBlock(this.type, this.source, this.text, this.index, this.id, this.name, this.input,
15391587
this.toolUseId, this.content, this.signature, this.thinking, this.data, this.cacheControl,
1540-
this.title, this.context, this.citations, this.fileId);
1588+
this.title, this.context, this.citations, this.fileId, this.filename);
15411589
}
15421590

15431591
}

models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ else if (event.type().equals(EventType.CONTENT_BLOCK_START)) {
165165
else if (contentBlockStartEvent.contentBlock() instanceof ContentBlockThinking thinkingBlock) {
166166
ContentBlock cb = new ContentBlock(Type.THINKING, null, null, contentBlockStartEvent.index(), null,
167167
null, null, null, null, thinkingBlock.signature(), thinkingBlock.thinking(), null, null, null,
168-
null, null, null);
168+
null, null, null, null);
169169
contentBlockReference.get().withType(event.type().name()).withContent(List.of(cb));
170170
}
171171
else {
@@ -182,12 +182,13 @@ else if (event.type().equals(EventType.CONTENT_BLOCK_DELTA)) {
182182
}
183183
else if (contentBlockDeltaEvent.delta() instanceof ContentBlockDeltaThinking thinking) {
184184
ContentBlock cb = new ContentBlock(Type.THINKING_DELTA, null, null, contentBlockDeltaEvent.index(),
185-
null, null, null, null, null, null, thinking.thinking(), null, null, null, null, null, null);
185+
null, null, null, null, null, null, thinking.thinking(), null, null, null, null, null, null,
186+
null);
186187
contentBlockReference.get().withType(event.type().name()).withContent(List.of(cb));
187188
}
188189
else if (contentBlockDeltaEvent.delta() instanceof ContentBlockDeltaSignature sig) {
189190
ContentBlock cb = new ContentBlock(Type.SIGNATURE_DELTA, null, null, contentBlockDeltaEvent.index(),
190-
null, null, null, null, null, sig.signature(), null, null, null, null, null, null, null);
191+
null, null, null, null, null, sig.signature(), null, null, null, null, null, null, null, null);
191192
contentBlockReference.get().withType(event.type().name()).withContent(List.of(cb));
192193
}
193194
else {

0 commit comments

Comments
 (0)