Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spring-ai-modules/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@
<module>spring-ai-text-to-sql</module>
<module>spring-ai-vector-stores</module>
<module>spring-ai-mcp-annotations</module>
<module>spring-ai-subagent-orchestrator</module>
</modules>
</project>
94 changes: 94 additions & 0 deletions spring-ai-modules/spring-ai-subagent-orchestrator/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.baeldung</groupId>
<artifactId>spring-ai-modules</artifactId>
<version>0.0.1</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>spring-ai-subagent-orchestrator</artifactId>
<name>spring-ai-subagent-orchestrator</name>
<description>spring-ai-subagent-orchestrator</description>

<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0-M5</spring-ai.version>
<spring-ai-agent-utils.version>0.7.0</spring-ai-agent-utils.version>
<spring-boot.version>4.0.6</spring-boot.version>
<junit-jupiter.version>6.0.3</junit-jupiter.version>
<junit-platform.version>6.0.3</junit-platform.version>
<org.slf4j.version>2.0.17</org.slf4j.version>
<logback.version>1.5.18</logback.version>
</properties>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-agent-utils-bom</artifactId>
<version>${spring-ai-agent-utils.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-agent-utils</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>${junit-platform.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.5</version>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.baeldung.springai.subagentorchestrator;

import com.baeldung.springai.subagentorchestrator.config.OrchestratorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;

@SpringBootApplication
public class SpringAiSubagentOrchestratorApplication {

private static final Logger logger = LoggerFactory.getLogger(SpringAiSubagentOrchestratorApplication.class);

public static void main(String[] args) {
SpringApplication.run(SpringAiSubagentOrchestratorApplication.class, args);
}

@Bean
@Profile("!test")
CommandLineRunner demo(OrchestratorService orchestratorService) {
return args -> {
String response = orchestratorService.ask(
"""
Perform the following tasks:
- Review the code quality of a current Java Spring Boot application
- Generate concise technical documentation like user guide
"""
);
logger.info("{}", response);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.baeldung.springai.subagentorchestrator.config;

import java.util.List;

import org.springaicommunity.agent.common.task.subagent.SubagentType;
import org.springaicommunity.agent.tools.task.TaskTool;
import org.springaicommunity.agent.tools.task.claude.ClaudeSubagentReferences;
import org.springaicommunity.agent.tools.task.claude.ClaudeSubagentType;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;

@Configuration
public class AgentConfig {

@Value("${agent.tasks.paths}")
private List<Resource> agentPaths;

@Bean
@Primary
public ChatClient orchestratorChatClient(ChatClient.Builder chatClientBuilder) {

SubagentType claudeType = ClaudeSubagentType.builder()
.chatClientBuilder("default", chatClientBuilder.clone())
.build();

ToolCallback taskTool = TaskTool.builder()
.subagentReferences(
ClaudeSubagentReferences.fromResources(agentPaths))
.subagentTypes(claudeType)
.build();

return chatClientBuilder.clone()
.defaultToolCallbacks(taskTool)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.baeldung.springai.subagentorchestrator.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class OrchestratorService {

private final ChatClient chatClient;

public OrchestratorService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String ask(String userMessage) {
return chatClient
.prompt(userMessage)
.call()
.content();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: code-reviewer
description: >
Expert code reviewer. Use proactively after writing or modifying code
to surface quality, security, and readability issues.
tools: Read, Grep, Glob
disallowedTools: Edit, Write
model: sonnet
---

You are a senior code reviewer with expertise in software quality.

**When Invoked:**
1. Run `git diff` to identify recent changes
2. Inspect the modified files and surrounding context
3. Check for issues in the areas listed below

**Review Checklist:**
- Code clarity and readability
- Proper naming conventions
- Error handling and edge cases
- Security vulnerabilities

**Output:** Clear, actionable feedback organized by file, with line references.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: documentation-writer
description: >
Technical documentation specialist for architecture explanations,
workflow summaries, and concise developer-facing docs.
model: default
---

You are a senior technical documentation specialist.

Your responsibilities:
- Generate concise technical documentation
- Explain Spring Boot and Java application architecture
- Summarize workflows clearly
- Produce developer-friendly explanations
- Keep documentation simple and technically accurate
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
spring.application.name=spring-ai-subagent-orchestrator
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4.1-mini
agent.tasks.paths=classpath:/agents/*.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.baeldung.springai.subagentorchestrator;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles("test")
class SpringAiSubagentOrchestratorApplicationTests {

@Test
void contextLoads() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.baeldung.springai.subagentorchestrator;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.springaicommunity.agent.common.task.subagent.SubagentType;
import org.springaicommunity.agent.tools.task.TaskTool;
import org.springaicommunity.agent.tools.task.claude.ClaudeSubagentReferences;
import org.springaicommunity.agent.tools.task.claude.ClaudeSubagentType;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(locations = "classpath:application-test.properties")
class SubagentOrchestrationIntegrationTest {

@Value("${agent.tasks.paths}")
private List<Resource> agentPaths;

@Autowired
private ChatClient.Builder chatClientBuilder;

@Test
void givenSubagentDefinitions_whenLoadingSubagents_thenReferencesAreCreated() {

var references = ClaudeSubagentReferences
.fromResources(agentPaths);

assertThat(references).isNotNull();
}

@Test
void givenPrompt_whenExecutingOrchestration_thenResponseIsGenerated() {
List<Resource> agentResources = List.of(
new ClassPathResource("agents/test-agent.md"));

SubagentType claudeType = ClaudeSubagentType.builder()
.chatClientBuilder("default", chatClientBuilder.clone())
.build();

ToolCallback taskTool = TaskTool.builder()
.subagentReferences(
ClaudeSubagentReferences.fromResources(agentResources))
.subagentTypes(claudeType)
.build();

ChatClient chatClient = chatClientBuilder.clone()
.defaultToolCallbacks(taskTool)
.build();

String result = chatClient
.prompt("Explain how the authentication module works.")
.call()
.content();

assertThat(result).isNotBlank();
assertThat(result).contains("authentication");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
name: test-agent
description: A test subagent used only in automated tests.
---
You are a test subagent.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
spring.ai.openai.api-key=dummy-key-for-tests
agent.tasks.paths=classpath:/agents/*.md