Skip to content

Commit b7a36bb

Browse files
markpollacksobychacko
authored andcommitted
Add Claude Skills integration with Files API support
Integrate Anthropic's pre-built Skills for document generation. Skills enable Claude to create actual downloadable files rather than just describing them. Supported skills: - XLSX: Generate Excel spreadsheets - PPTX: Create PowerPoint presentations - DOCX: Generate Word documents - PDF: Create PDF files Example usage: AnthropicChatOptions.builder() .anthropicSkill(AnthropicSkill.XLSX) .build() Generated files can be downloaded via the Files API using SkillsResponseHelper to extract file IDs from responses. Breaking change: - ContentBlock.content type changed from String to Object to support nested JSON structures in Skills responses Signed-off-by: Mark Pollack <mark.pollack@broadcom.com>
1 parent 8cc4ea4 commit b7a36bb

File tree

7 files changed

+1044
-39
lines changed

7 files changed

+1044
-39
lines changed

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,14 @@ Prompt buildRequestPrompt(Prompt prompt) {
583583
else if (this.defaultOptions.getCitationDocuments() != null) {
584584
requestOptions.setCitationDocuments(this.defaultOptions.getCitationDocuments());
585585
}
586+
587+
// Merge skillContainer that is Json-ignored
588+
if (runtimeOptions.getSkillContainer() != null) {
589+
requestOptions.setSkillContainer(runtimeOptions.getSkillContainer());
590+
}
591+
else if (this.defaultOptions.getSkillContainer() != null) {
592+
requestOptions.setSkillContainer(this.defaultOptions.getSkillContainer());
593+
}
586594
}
587595
else {
588596
requestOptions.setHttpHeaders(this.defaultOptions.getHttpHeaders());
@@ -591,6 +599,7 @@ else if (this.defaultOptions.getCitationDocuments() != null) {
591599
requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks());
592600
requestOptions.setToolContext(this.defaultOptions.getToolContext());
593601
requestOptions.setCitationDocuments(this.defaultOptions.getCitationDocuments());
602+
requestOptions.setSkillContainer(this.defaultOptions.getSkillContainer());
594603
}
595604

596605
ToolCallingChatOptions.validateToolCallbacks(requestOptions.getToolCallbacks());
@@ -649,6 +658,56 @@ ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
649658
requestOptions.setHttpHeaders(headers);
650659
}
651660

661+
// Add Skills container from options if present
662+
AnthropicApi.SkillContainer skillContainer = null;
663+
if (requestOptions != null && requestOptions.getSkillContainer() != null) {
664+
skillContainer = requestOptions.getSkillContainer();
665+
}
666+
else if (this.defaultOptions.getSkillContainer() != null) {
667+
skillContainer = this.defaultOptions.getSkillContainer();
668+
}
669+
670+
if (skillContainer != null) {
671+
request = ChatCompletionRequest.from(request).container(skillContainer).build();
672+
673+
// Skills require the code_execution tool to be enabled
674+
// Add it if not already present
675+
List<AnthropicApi.Tool> existingTools = request.tools() != null ? new ArrayList<>(request.tools())
676+
: new ArrayList<>();
677+
boolean hasCodeExecution = existingTools.stream().anyMatch(tool -> "code_execution".equals(tool.name()));
678+
679+
if (!hasCodeExecution) {
680+
existingTools.add(new AnthropicApi.Tool(AnthropicApi.CODE_EXECUTION_TOOL_TYPE, "code_execution", null,
681+
null, null));
682+
request = ChatCompletionRequest.from(request).tools(existingTools).build();
683+
}
684+
685+
// Skills require three beta headers: skills, code-execution, and files-api
686+
Map<String, String> headers = requestOptions != null ? new HashMap<>(requestOptions.getHttpHeaders())
687+
: new HashMap<>();
688+
String requiredBetas = AnthropicApi.BETA_SKILLS + "," + AnthropicApi.BETA_CODE_EXECUTION + ","
689+
+ AnthropicApi.BETA_FILES_API;
690+
String existingBeta = headers.get("anthropic-beta");
691+
if (existingBeta != null) {
692+
if (!existingBeta.contains(AnthropicApi.BETA_SKILLS)) {
693+
existingBeta = existingBeta + "," + AnthropicApi.BETA_SKILLS;
694+
}
695+
if (!existingBeta.contains(AnthropicApi.BETA_CODE_EXECUTION)) {
696+
existingBeta = existingBeta + "," + AnthropicApi.BETA_CODE_EXECUTION;
697+
}
698+
if (!existingBeta.contains(AnthropicApi.BETA_FILES_API)) {
699+
existingBeta = existingBeta + "," + AnthropicApi.BETA_FILES_API;
700+
}
701+
headers.put("anthropic-beta", existingBeta);
702+
}
703+
else {
704+
headers.put("anthropic-beta", requiredBetas);
705+
}
706+
if (requestOptions != null) {
707+
requestOptions.setHttpHeaders(headers);
708+
}
709+
}
710+
652711
return request;
653712
}
654713

@@ -845,7 +904,8 @@ private List<AnthropicApi.Tool> addCacheToLastTool(List<AnthropicApi.Tool> tools
845904
AnthropicApi.Tool tool = tools.get(i);
846905
if (i == tools.size() - 1) {
847906
// Add cache control to last tool
848-
tool = new AnthropicApi.Tool(tool.name(), tool.description(), tool.inputSchema(), cacheControl);
907+
tool = new AnthropicApi.Tool(tool.type(), tool.name(), tool.description(), tool.inputSchema(),
908+
cacheControl);
849909
cacheEligibilityResolver.useCacheBlock();
850910
}
851911
modifiedTools.add(tool);

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

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ public void setCacheOptions(AnthropicCacheOptions cacheOptions) {
115115
@JsonIgnore
116116
private Map<String, String> httpHeaders = new HashMap<>();
117117

118+
/**
119+
* Container for Claude Skills to make available in this request.
120+
* Skills are collections of instructions, scripts, and resources that
121+
* extend Claude's capabilities for specific domains.
122+
* Maximum of 8 skills per request.
123+
*/
124+
@JsonIgnore
125+
private AnthropicApi.SkillContainer skillContainer;
126+
118127
// @formatter:on
119128

120129
public static Builder builder() {
@@ -141,9 +150,18 @@ public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions)
141150
.cacheOptions(fromOptions.getCacheOptions())
142151
.citationDocuments(fromOptions.getCitationDocuments() != null
143152
? new ArrayList<>(fromOptions.getCitationDocuments()) : null)
153+
.skillContainer(fromOptions.getSkillContainer())
144154
.build();
145155
}
146156

157+
public AnthropicApi.SkillContainer getSkillContainer() {
158+
return this.skillContainer;
159+
}
160+
161+
public void setSkillContainer(AnthropicApi.SkillContainer skillContainer) {
162+
this.skillContainer = skillContainer;
163+
}
164+
147165
@Override
148166
public String getModel() {
149167
return this.model;
@@ -351,15 +369,16 @@ public boolean equals(Object o) {
351369
&& Objects.equals(this.toolContext, that.toolContext)
352370
&& Objects.equals(this.httpHeaders, that.httpHeaders)
353371
&& Objects.equals(this.cacheOptions, that.cacheOptions)
354-
&& Objects.equals(this.citationDocuments, that.citationDocuments);
372+
&& Objects.equals(this.citationDocuments, that.citationDocuments)
373+
&& Objects.equals(this.skillContainer, that.skillContainer);
355374
}
356375

357376
@Override
358377
public int hashCode() {
359378
return Objects.hash(this.model, this.maxTokens, this.metadata, this.stopSequences, this.temperature, this.topP,
360379
this.topK, this.toolChoice, this.thinking, this.toolCallbacks, this.toolNames,
361380
this.internalToolExecutionEnabled, this.toolContext, this.httpHeaders, this.cacheOptions,
362-
this.citationDocuments);
381+
this.citationDocuments, this.skillContainer);
363382
}
364383

365384
public static final class Builder {
@@ -501,6 +520,80 @@ public Builder addCitationDocument(CitationDocument document) {
501520
return this;
502521
}
503522

523+
/**
524+
* Set the Skills container for this request.
525+
* @param skillContainer Container with skills to make available
526+
* @return Builder for method chaining
527+
*/
528+
public Builder skillContainer(AnthropicApi.SkillContainer skillContainer) {
529+
this.options.setSkillContainer(skillContainer);
530+
return this;
531+
}
532+
533+
/**
534+
* Add a single skill to the request. Creates a SkillContainer if one doesn't
535+
* exist.
536+
* @param skill Skill to add
537+
* @return Builder for method chaining
538+
*/
539+
public Builder skill(AnthropicApi.Skill skill) {
540+
Assert.notNull(skill, "Skill cannot be null");
541+
if (this.options.skillContainer == null) {
542+
this.options.skillContainer = AnthropicApi.SkillContainer.builder().skill(skill).build();
543+
}
544+
else {
545+
// Rebuild container with additional skill
546+
List<AnthropicApi.Skill> existingSkills = new ArrayList<>(this.options.skillContainer.skills());
547+
existingSkills.add(skill);
548+
this.options.skillContainer = new AnthropicApi.SkillContainer(existingSkills);
549+
}
550+
return this;
551+
}
552+
553+
/**
554+
* Add an Anthropic pre-built skill (xlsx, pptx, docx, pdf).
555+
* @param anthropicSkill Pre-built Anthropic skill to add
556+
* @return Builder for method chaining
557+
*/
558+
public Builder anthropicSkill(AnthropicApi.AnthropicSkill anthropicSkill) {
559+
Assert.notNull(anthropicSkill, "AnthropicSkill cannot be null");
560+
return skill(anthropicSkill.toSkill());
561+
}
562+
563+
/**
564+
* Add an Anthropic pre-built skill with specific version.
565+
* @param anthropicSkill Pre-built Anthropic skill to add
566+
* @param version Version of the skill (e.g., "latest", "20251013")
567+
* @return Builder for method chaining
568+
*/
569+
public Builder anthropicSkill(AnthropicApi.AnthropicSkill anthropicSkill, String version) {
570+
Assert.notNull(anthropicSkill, "AnthropicSkill cannot be null");
571+
Assert.hasText(version, "Version cannot be empty");
572+
return skill(anthropicSkill.toSkill(version));
573+
}
574+
575+
/**
576+
* Add a custom skill by ID.
577+
* @param skillId Custom skill ID
578+
* @return Builder for method chaining
579+
*/
580+
public Builder customSkill(String skillId) {
581+
Assert.hasText(skillId, "Skill ID cannot be empty");
582+
return skill(new AnthropicApi.Skill(AnthropicApi.SkillType.CUSTOM, skillId));
583+
}
584+
585+
/**
586+
* Add a custom skill with specific version.
587+
* @param skillId Custom skill ID
588+
* @param version Version of the skill
589+
* @return Builder for method chaining
590+
*/
591+
public Builder customSkill(String skillId, String version) {
592+
Assert.hasText(skillId, "Skill ID cannot be empty");
593+
Assert.hasText(version, "Version cannot be empty");
594+
return skill(new AnthropicApi.Skill(AnthropicApi.SkillType.CUSTOM, skillId, version));
595+
}
596+
504597
public AnthropicChatOptions build() {
505598
this.options.validateCitationConsistency();
506599
return this.options;

0 commit comments

Comments
 (0)