Files
wehub-resource-sync a9cd7750f4
CI / unit-test (push) Has been cancelled
CI / detect-changes (push) Has been cancelled
CI / build (push) Has been cancelled
Publish docs via GitHub Pages / Deploy docs (push) Has been cancelled
CI / test-harness (push) Has been cancelled
CI / generate-e2e-matrix (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / build-ui (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
UI v2 Integration CI / E2E (Integration) (push) Has been cancelled
UI v2 CI / Lint, Format & Test (push) Has been cancelled
UI v2 CI / E2E (Mocked) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:56 +08:00

17 KiB

Contributing to Conductor AI Module

Thank you for your interest in contributing to the Conductor AI module! This guide will help you add new LLM providers, vector database integrations, workers, and other enhancements.

Table of Contents


Architecture Overview

The AI module is organized into several key packages:

org.conductoross.conductor.ai/
├── providers/           # LLM provider implementations (OpenAI, Anthropic, etc.)
├── vectordb/           # Vector database integrations (Pinecone, MongoDB, etc.)
├── video/              # Video generation abstractions (VideoModel, AsyncVideoModel, etc.)
├── tasks/              # Worker task definitions
│   ├── mapper/         # Input/output parameter mappers
│   └── worker/         # Worker implementations
├── mcp/                # Model Context Protocol implementation
├── models/             # Request/response models
└── document/           # Document readers and parsers

Key interfaces:

  • AIModel: Base interface for LLM providers
  • VideoModel: Functional interface for synchronous video generation (mirrors Spring AI's ImageModel)
  • AsyncVideoModel: Extends VideoModel with async polling via checkStatus(String jobId)
  • VectorDBProvider: Base interface for vector databases
  • @WorkerTask: Annotation for defining worker tasks

Adding a New LLM Provider

Step 1: Create Provider Package

Create a new package under providers/:

org.conductoross.conductor.ai.providers.yourprovider/
├── YourProvider.java          # Main provider implementation
└── YourProviderConfiguration.java  # Spring configuration

Step 2: Implement AIModel Interface

Create your provider class implementing AIModel:

package org.conductoross.conductor.ai.providers.yourprovider;

import org.conductoross.conductor.ai.AIModel;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.embedding.EmbeddingModel;

public class YourProvider implements AIModel {
    
    private final ChatModel chatModel;
    private final EmbeddingModel embeddingModel;
    
    public YourProvider(ChatModel chatModel, EmbeddingModel embeddingModel) {
        this.chatModel = chatModel;
        this.embeddingModel = embeddingModel;
    }
    
    @Override
    public String getModelProvider() {
        return "your_provider_name";  // Used in workflow definitions
    }
    
    @Override
    public ChatModel getChatModel() {
        return chatModel;
    }
    
    @Override
    public EmbeddingModel getEmbeddingModel() {
        return embeddingModel;
    }
}

Step 3: Create Configuration Class

Use @ConditionalOnProperty to ensure the provider only loads when configured:

package org.conductoross.conductor.ai.providers.yourprovider;

import org.conductoross.conductor.ai.ModelConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(YourProviderProperties.class)
@ConditionalOnProperty(prefix = "conductor.ai.your-provider", name = "api-key")
public class YourProviderConfiguration {
    
    @Bean
    public ModelConfiguration<YourProvider> yourProviderConfiguration(
            YourProviderProperties properties) {
        return () -> {
            // Initialize chat and embedding models
            ChatModel chatModel = // ... create from properties
            EmbeddingModel embeddingModel = // ... create from properties
            
            return new YourProvider(chatModel, embeddingModel);
        };
    }
}

Step 4: Create Properties Class

package org.conductoross.conductor.ai.providers.yourprovider;

import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;

@Data
@ConfigurationProperties(prefix = "conductor.ai.your-provider")
public class YourProviderProperties {
    private String apiKey;
    private String baseUrl = "https://api.yourprovider.com";
    private String model = "default-model";
    // Add other configuration properties
}

Step 5: Add Tests

Create YourProviderConfigurationTest.java:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class YourProviderConfigurationTest {
    
    @Test
    void testProviderLoadsWhenConfigured() {
        ApplicationContextRunner contextRunner =
                new ApplicationContextRunner()
                        .withConfiguration(
                                AutoConfigurations.of(YourProviderConfiguration.class))
                        .withPropertyValues(
                                "conductor.ai.your-provider.api-key=test-key");
        
        contextRunner.run(
                context -> {
                    assertThat(context).hasSingleBean(ModelConfiguration.class);
                });
    }
    
    @Test
    void testProviderDoesNotLoadWithoutApiKey() {
        ApplicationContextRunner contextRunner =
                new ApplicationContextRunner()
                        .withConfiguration(
                                AutoConfigurations.of(YourProviderConfiguration.class));
        
        contextRunner.run(
                context -> {
                    assertThat(context).doesNotHaveBean(ModelConfiguration.class);
                });
    }
}

Step 6: Add Video Generation Support (Optional)

If your provider supports video generation, implement video model support using the video/ package abstractions. Video generation is async by nature (submit a job, poll for results), so most providers will implement AsyncVideoModel.

6a. Create a Video Model Class

package org.conductoross.conductor.ai.providers.yourprovider;

import org.conductoross.conductor.ai.video.*;

public class YourVideoModel implements AsyncVideoModel {

    private final String apiKey;

    public YourVideoModel(String apiKey) {
        this.apiKey = apiKey;
    }

    @Override
    public VideoResponse call(VideoPrompt prompt) {
        // Submit video generation job to provider API
        // Return a VideoResponse with jobId in metadata
        VideoResponseMetadata metadata = new VideoResponseMetadata();
        metadata.put("jobId", submittedJobId);
        metadata.put("status", "PENDING");
        return new VideoResponse(List.of(), metadata);
    }

    @Override
    public VideoResponse checkStatus(String jobId) {
        // Poll provider API for job status
        // When complete, download video bytes and return Video objects
        // Set mimeType on each Video (e.g., "video/mp4", "image/webp" for thumbnails)
        Video video = new Video(videoUrl, null, "video/mp4");
        VideoGeneration generation = new VideoGeneration(video);

        VideoResponseMetadata metadata = new VideoResponseMetadata();
        metadata.put("jobId", jobId);
        metadata.put("status", "COMPLETED");
        return new VideoResponse(List.of(generation), metadata);
    }
}

6b. Wire Video Model into Your Provider

Override the video-related methods in your AIModel implementation:

@Override
public VideoModel getVideoModel() {
    if (videoModel == null) {
        videoModel = new YourVideoModel(apiKey);
    }
    return videoModel;
}

@Override
public LLMResponse generateVideo(VideoGenRequest request) {
    VideoOptions options = getVideoOptions(request);
    VideoPrompt prompt = new VideoPrompt(
        List.of(new VideoMessage(request.getPrompt())), options);
    VideoResponse response = getVideoModel().call(prompt);
    // Convert to LLMResponse with jobId
}

@Override
public LLMResponse checkVideoStatus(VideoGenRequest request) {
    AsyncVideoModel asyncModel = (AsyncVideoModel) getVideoModel();
    VideoResponse response = asyncModel.checkStatus(request.getJobId());
    // Convert to LLMResponse with media list
}

The video/ package mirrors Spring AI's Image* abstraction pattern:

  • VideoPrompt -> ImagePrompt (request wrapper)
  • VideoResponse -> ImageResponse (response wrapper)
  • VideoGeneration -> ImageGeneration (individual result)
  • Video -> Image (the actual media, with url, b64Json, and mimeType fields)
  • VideoOptions -> ImageOptions (generation parameters)

Step 7: Update Documentation

Add your provider to README.md under the supported providers section with configuration examples.


Adding a Vector Database Integration

Step 1: Create Config Class

Create a new configuration class in the database package (e.g., org.conductoross.conductor.ai.vectordb.yourdb):

@Data
@NoArgsConstructor
@AllArgsConstructor
public class YourDBConfig implements VectorDBConfig<YourVectorDB> {
    
    private String connectionString;
    // other properties
    
    @Override
    public YourVectorDB get() {
        throw new UnsupportedOperationException("Use get(String name) instead");
    }

    public YourVectorDB get(String name) {
        return new YourVectorDB(name, this);
    }
}

Step 2: Implement VectorDB Class

Extend the VectorDB abstract class:

public class YourVectorDB extends VectorDB {
    
    public static final String TYPE = "yourdb";
    private final YourDBConfig config;
    
    public YourVectorDB(String name, YourDBConfig config) {
        super(name, TYPE);
        this.config = config;
    }
    
    @Override
    public int updateEmbeddings(String indexName, String namespace, String doc, String parentDocId, String id, List<Float> embeddings, Map<String, Object> metadata) {
        // Implement logic to store embeddings
    }
    
    @Override
    public List<IndexedDoc> search(String indexName, String namespace, List<Float> embeddings, int maxResults) {
        // Implement logic to search embeddings
    }
}

Step 3: Register in VectorDBInstanceConfig

Add your database type to the createVectorDB method and the VectorDBInstance inner class in org.conductoross.conductor.ai.vectordb.VectorDBInstanceConfig.

Step 4: Add Integration Tests

Use Testcontainers for integration testing:

@Testcontainers
class YourVectorDBTest {
    
    @Container
    static GenericContainer<?> yourdb =
            new GenericContainer<>("yourdb:latest")
                    .withExposedPorts(1234);
    
    @Test
    void testStoreAndSearch() {
        // Test vector storage and similarity search
    }
}

Adding New Workers/Tasks

Step 1: Create Request Model

package org.conductoross.conductor.ai.model;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = false)
public class YourTaskRequest extends LLMWorkerInput {
    private String parameter1;
    private String parameter2;
    // Add task-specific parameters
}

Step 2: Create Worker Class

package org.conductoross.conductor.ai.tasks.worker;

import com.netflix.conductor.sdk.workflow.annotations.WorkerTask;
import org.conductoross.conductor.ai.model.YourTaskRequest;

@Component
public class YourWorker {
    
    private final YourService yourService;
    
    public YourWorker(YourService yourService) {
        this.yourService = yourService;
    }
    
    @WorkerTask("YOUR_TASK_NAME")
    public @OutputParam("result") YourTaskResult executeTask(YourTaskRequest request) {
        // Implement task logic
        return yourService.processRequest(request);
    }
}

Step 3: Add Task Tests

class YourWorkerTest {
    
    @Test
    void testTaskExecution() {
        YourWorker worker = new YourWorker(mockService);
        YourTaskRequest request = new YourTaskRequest();
        request.setParameter1("test");
        
        YourTaskResult result = worker.executeTask(request);
        
        assertNotNull(result);
        // Add assertions
    }
}

Adding MCP Tools

Model Context Protocol (MCP) allows external tools to be called from workflows.

Adding MCP Server Support

The MCPService already supports:

  • HTTP/SSE transports
  • stdio (local process) transports
  • Direct JSON-RPC fallback

To add a new MCP server:

  1. Deploy your MCP server (HTTP or local script)
  2. Use existing CALL_MCP_TOOL task in workflows:
{
  "name": "call_your_tool",
  "taskReferenceName": "your_tool",
  "type": "CALL_MCP_TOOL",
  "inputParameters": {
    "mcpServer": "http://localhost:3000",
    "methodName": "your_tool_name",
    "param1": "value1",
    "param2": "value2"
  }
}

Extending MCP Capabilities

To add new MCP-related features, modify:

  • MCPService.java - Core MCP communication logic
  • MCPWorkers.java - Worker task definitions
  • models/MCP*.java - Request/response models

Testing Guidelines

Unit Tests

  • Place in src/test/java mirroring the source structure
  • Use MockBean for Spring dependencies
  • Test individual methods and edge cases
  • Aim for 80%+ code coverage

Integration Tests

  • Use @SpringBootTest for full context testing
  • Use Testcontainers for external dependencies (databases, servers)
  • Test real interactions between components

Test Naming Convention

// Unit test method format
void test<MethodName>_<Scenario>_<ExpectedResult>()

// Examples:
void testGetModel_WithValidProvider_ReturnsModel()
void testGetModel_WithInvalidProvider_ThrowsException()

Running Tests

# Run all tests
./gradlew :conductor-ai:test

# Run specific test class
./gradlew :conductor-ai:test --tests YourProviderTest

# Run with coverage
./gradlew :conductor-ai:test jacocoTestReport

Code Style and Best Practices

Lombok Usage

Use Lombok annotations consistently:

  • @Data for simple POJOs
  • @Builder for complex object construction
  • @Slf4j for logging
  • @AllArgsConstructor / @NoArgsConstructor for constructors

Logging

  • Use SLF4J via @Slf4j
  • Log levels:
    • log.debug() - Detailed diagnostic information
    • log.info() - Important business events
    • log.warn() - Recoverable issues
    • log.error() - Errors requiring attention

Error Handling

  • Throw descriptive exceptions
  • Include context in error messages
  • Use try-catch for recoverable errors
  • Let unchecked exceptions propagate for programming errors

Configuration Properties

  • Use @ConfigurationProperties for type-safe configuration
  • Provide sensible defaults
  • Document all properties in javadoc
  • Use @ConditionalOnProperty to make features optional

Spring Beans

  • Prefer constructor injection over field injection
  • Use @Component for auto-detected beans
  • Use @Configuration for explicit bean definitions
  • Apply @ConditionalOnProperty for optional features

Documentation

  • Add Javadoc to all public classes and methods
  • Include usage examples in class-level Javadoc
  • Update README.md with new features
  • Provide workflow examples for new tasks

Development Workflow

1. Create a Feature Branch

git checkout -b feature/add-your-provider

2. Implement Your Changes

Follow the patterns above for your contribution type.

3. Write Tests

Ensure your code has comprehensive test coverage.

4. Run Tests and Checks

./gradlew :conductor-ai:test
./gradlew :conductor-ai:compileJava

5. Update Documentation

  • Update README.md with examples
  • Add Javadoc to new classes
  • Update this CONTRIBUTING.md if adding new patterns

6. Submit Pull Request

  • Provide clear description of changes
  • Reference any related issues
  • Include test results
  • Update changelog if applicable

Common Patterns

Conditional Bean Creation

Always use @ConditionalOnProperty for optional integrations:

@ConditionalOnProperty(
    prefix = "conductor.ai.your-feature",
    name = "enabled",
    havingValue = "true"
)

Parameter Mapping

For workers with dynamic parameters, use @JsonAnySetter:

@JsonAnySetter
public void setAdditionalProperty(String key, Object value) {
    additionalProperties.put(key, value);
}

Resource Cleanup

Implement DisposableBean for cleanup:

@Override
public void destroy() throws Exception {
    // Clean up resources
}

Getting Help

  • Check existing implementations in providers/ for examples
  • Review README.md for usage patterns
  • Look at test files for testing patterns
  • Open a GitHub issue for questions

License

By contributing, you agree that your contributions will be licensed under the Apache License 2.0.