Skip to content
Merged
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ root

`app` depends on `base` + all `providers/` + all `plugins/`. `ChatChannel` lives inside `app/`.

Each **provider** implements `AgentOnboardingProvider` (in `base`) and is auto-discovered by Spring. Each **plugin** is an optional Spring Boot auto-configuration module that contributes tools or channels.
Each **provider** implements `AgentOnboardingProvider` (in `base`) and self-registers via Spring Boot auto-configuration (`META-INF/spring/…AutoConfiguration.imports`) — same drop-in mechanism as plugins, independent of the app's component scan. Each **plugin** is an optional Spring Boot auto-configuration module that contributes tools or channels.

---

Expand Down Expand Up @@ -107,7 +107,7 @@ User/Agent → TaskManager.create()
| `MCP Tools` | `SyncMcpToolCallbackProvider` |
| `BraveWebSearchTool` | 15 results (only if Brave API key configured) |

**Supported LLM Providers** — each lives in its own `providers/<name>/` module and implements `AgentOnboardingProvider`:
**Supported LLM Providers** — each lives in its own `providers/<name>/` module and contributes its `AgentOnboardingProvider` through an `@AutoConfiguration` class registered in the module's `AutoConfiguration.imports`. The active model is selected centrally by `spring.ai.model.chat` (Spring AI), not per-provider; the default `unknown` disables all of them until onboarding sets it:

| Provider | Module | Default Model | API Key |
|---|---|---|---|
Expand Down
1 change: 1 addition & 0 deletions base/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id 'java-library'
id 'java-test-fixtures'
id("com.skillsjars.gradle-plugin") version "0.0.2"
}

Expand Down
30 changes: 0 additions & 30 deletions base/src/main/java/ai/javaclaw/providers/AgentProvider.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ai.javaclaw.testsupport;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public final class AutoConfigurationImportsTestSupport {

private static final String IMPORTS_RESOURCE =
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";

private AutoConfigurationImportsTestSupport() {
}

public static List<String> importedAutoConfigurations(Class<?> testClass) throws IOException {
URL resource = testClass.getClassLoader().getResource(IMPORTS_RESOURCE);
try {
Path path = Path.of(resource.toURI());
return Files.readAllLines(path).stream()
.map(String::trim)
.filter(line -> !line.isEmpty())
.toList();
} catch (URISyntaxException e) {
throw new UncheckedIOException(new IOException(e));
}
}
}
1 change: 1 addition & 0 deletions providers/anthropic/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-anthropic'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation testFixtures(project(':base'))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ai.javaclaw.providers.anthropic;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public class AnthropicAgentAutoConfiguration {

@Bean
@ConditionalOnMissingBean(AnthropicAgentOnboardingProvider.class)
public AgentOnboardingProvider anthropicAgentOnboardingProvider() {
return new AnthropicAgentOnboardingProvider();
}
Comment thread
rdehuyss marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package ai.javaclaw.providers.anthropic;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.springframework.stereotype.Component;

import java.util.Optional;

import static ai.javaclaw.providers.anthropic.AnthropticClaudeCodeConfiguration.CLAUDE_CODE_OATH_TOKEN_PLACEHOLDER;

@Component
public class AnthropicAgentOnboardingProvider implements AgentOnboardingProvider {
@Override
public String getId() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.anthropic.http.okhttp.SpringAiAnthropicHttpClient;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatProperties;
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicConnectionProperties;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@AutoConfiguration(before = AnthropicChatAutoConfiguration.class)
@ConditionalOnProperty(name = "spring.ai.anthropic.api-key", havingValue = AnthropticClaudeCodeConfiguration.CLAUDE_CODE_OATH_TOKEN_PLACEHOLDER)
public class AnthropticClaudeCodeConfiguration {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ai.javaclaw.providers.anthropic.AnthropicAgentAutoConfiguration
ai.javaclaw.providers.anthropic.AnthropticClaudeCodeConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ai.javaclaw.providers.anthropic;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import java.io.IOException;

import static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations;
import static org.assertj.core.api.Assertions.assertThat;

class AnthropicAgentAutoConfigurationTest {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AnthropicAgentAutoConfiguration.class));

@Test
void registersOnboardingProvider() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(AgentOnboardingProvider.class);
assertThat(context.getBean(AgentOnboardingProvider.class).getId()).isEqualTo("anthropic");
});
}

@Test
void bothAutoConfigurationsAreRegisteredViaImportsFile() throws IOException {
assertThat(importedAutoConfigurations(AnthropicAgentAutoConfigurationTest.class)).contains(
AnthropicAgentAutoConfiguration.class.getName(),
AnthropticClaudeCodeConfiguration.class.getName());
}
}
1 change: 1 addition & 0 deletions providers/google/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-google-genai'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation testFixtures(project(':base'))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ai.javaclaw.providers.google.genai;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public class GoogleGenAIAgentAutoConfiguration {

@Bean
@ConditionalOnMissingBean(GoogleGenAIAgentOnboardingProvider.class)
public AgentOnboardingProvider googleGenAIAgentOnboardingProvider() {
return new GoogleGenAIAgentOnboardingProvider();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package ai.javaclaw.providers.google.genai;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.springframework.stereotype.Component;

@Component
public class GoogleGenAIAgentOnboardingProvider implements AgentOnboardingProvider {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ai.javaclaw.providers.google.genai.GoogleGenAIAgentAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ai.javaclaw.providers.google.genai;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import java.io.IOException;

import static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations;
import static org.assertj.core.api.Assertions.assertThat;

class GoogleGenAIAgentAutoConfigurationTest {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GoogleGenAIAgentAutoConfiguration.class));

@Test
void registersOnboardingProvider() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(AgentOnboardingProvider.class);
assertThat(context.getBean(AgentOnboardingProvider.class).getId()).isEqualTo("google.genai");
});
}

@Test
void autoConfigurationIsRegisteredViaImportsFile() throws IOException {
assertThat(importedAutoConfigurations(GoogleGenAIAgentAutoConfigurationTest.class))
.contains(GoogleGenAIAgentAutoConfiguration.class.getName());
}
}
1 change: 1 addition & 0 deletions providers/ollama/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-ollama'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation testFixtures(project(':base'))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ai.javaclaw.providers.ollama;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public class OllamaAgentAutoConfiguration {

@Bean
@ConditionalOnMissingBean(OllamaAgentOnboardingProvider.class)
public AgentOnboardingProvider ollamaAgentOnboardingProvider() {
return new OllamaAgentOnboardingProvider();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package ai.javaclaw.providers.ollama;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.springframework.stereotype.Component;

@Component
public class OllamaAgentOnboardingProvider implements AgentOnboardingProvider {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ai.javaclaw.providers.ollama.OllamaAgentAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ai.javaclaw.providers.ollama;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import java.io.IOException;

import static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations;
import static org.assertj.core.api.Assertions.assertThat;

class OllamaAgentAutoConfigurationTest {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OllamaAgentAutoConfiguration.class));

@Test
void registersOnboardingProvider() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(AgentOnboardingProvider.class);
assertThat(context.getBean(AgentOnboardingProvider.class).getId()).isEqualTo("ollama");
});
}

@Test
void autoConfigurationIsRegisteredViaImportsFile() throws IOException {
assertThat(importedAutoConfigurations(OllamaAgentAutoConfigurationTest.class))
.contains(OllamaAgentAutoConfiguration.class.getName());
}
}
1 change: 1 addition & 0 deletions providers/openai/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-openai'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation testFixtures(project(':base'))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ai.javaclaw.providers.openai;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public class OpenAIAgentAutoConfiguration {

@Bean
@ConditionalOnMissingBean(OpenAIAgentOnboardingProvider.class)
public AgentOnboardingProvider openAIAgentOnboardingProvider() {
return new OpenAIAgentOnboardingProvider();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package ai.javaclaw.providers.openai;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.springframework.stereotype.Component;

@Component
public class OpenAIAgentOnboardingProvider implements AgentOnboardingProvider {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ai.javaclaw.providers.openai.OpenAIAgentAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ai.javaclaw.providers.openai;

import ai.javaclaw.onboarding.AgentOnboardingProvider;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import java.io.IOException;

import static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations;
import static org.assertj.core.api.Assertions.assertThat;

class OpenAIAgentAutoConfigurationTest {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OpenAIAgentAutoConfiguration.class));

@Test
void registersOnboardingProvider() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(AgentOnboardingProvider.class);
assertThat(context.getBean(AgentOnboardingProvider.class).getId()).isEqualTo("openai");
});
}

@Test
void autoConfigurationIsRegisteredViaImportsFile() throws IOException {
assertThat(importedAutoConfigurations(OpenAIAgentAutoConfigurationTest.class))
.contains(OpenAIAgentAutoConfiguration.class.getName());
}
}
Loading