# 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](#architecture-overview) - [Adding a New LLM Provider](#adding-a-new-llm-provider) - [Adding a Vector Database Integration](#adding-a-vector-database-integration) - [Adding New Workers/Tasks](#adding-new-workerstasks) - [Adding MCP Tools](#adding-mcp-tools) - [Testing Guidelines](#testing-guidelines) - [Code Style and Best Practices](#code-style-and-best-practices) --- ## 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`: ```java 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: ```java 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 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 ```java 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`: ```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 ```java 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: ```java @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`): ```java @Data @NoArgsConstructor @AllArgsConstructor public class YourDBConfig implements VectorDBConfig { 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: ```java 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 embeddings, Map metadata) { // Implement logic to store embeddings } @Override public List search(String indexName, String namespace, List 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: ```java @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 ```java 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 ```java 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 ```java 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: ```json { "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 ```java // Unit test method format void test__() // Examples: void testGetModel_WithValidProvider_ReturnsModel() void testGetModel_WithInvalidProvider_ThrowsException() ``` ### Running Tests ```bash # 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 ```bash 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 ```bash ./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: ```java @ConditionalOnProperty( prefix = "conductor.ai.your-feature", name = "enabled", havingValue = "true" ) ``` ### Parameter Mapping For workers with dynamic parameters, use `@JsonAnySetter`: ```java @JsonAnySetter public void setAdditionalProperty(String key, Object value) { additionalProperties.put(key, value); } ``` ### Resource Cleanup Implement `DisposableBean` for cleanup: ```java @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.