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
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ jobs:
- name: Build and Package on ${{ matrix.java }}
run: |
chmod 777 ./mvnw
./mvnw -B clean package -DskipTests -DskipITs \
-Dspotless.skip=true \
./mvnw -B clean verify -Dspotless.skip=true

result:
name: Build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
cache: maven

- name: Build Release Package
run: mvn clean package -DskipTests -Prelease -B
run: ./mvnw clean verify -Prelease -B

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
Expand Down
53 changes: 52 additions & 1 deletion consilens-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,43 @@ mvn clean install -pl consilens-ai -am

### Basic Usage

Production-oriented CLI entrypoint:

```bash
consilens ai config "compare mysql users with postgresql users by id" \
--no-llm \
--source-type mysql \
--source-url jdbc:mysql://localhost:3306/mydb \
--source-table users \
--source-user-env MYSQL_USER \
--source-password-env MYSQL_PASSWORD \
--target-type postgresql \
--target-url jdbc:postgresql://localhost:5432/mydb \
--target-table users \
--target-user-env PG_USER \
--target-password-env PG_PASSWORD \
--keys id \
--fields name,email,status \
--output diff.yaml

consilens ai explain -c diff.yaml
consilens diff --dry-run -c diff.yaml
consilens diff -c diff.yaml
consilens ai diagnose --result diff-records.json --analyzer rulebased --output diagnose.md
consilens ai providers
consilens ai providers --format json
consilens ai doctor --format json
```

The CLI path generates canonical Consilens YAML and validates it with the existing engine model. AI does not execute a real diff directly.
`ai diagnose` reads row-level diff evidence from a `json` `diff-record` sink; stats-only result files are not enough for pattern analysis.
The analyzer is loaded via SPI. Use `--analyzer <name>` or `CONSILENS_AI_ANALYZER`; the default is `rulebased`.
Use `--output` to persist the diagnosis report; otherwise it is printed to stdout.
Use `ai providers` to verify which analyzer and LLM backend plugins are visible on the runtime classpath; `--format json` is available for CI checks and scripts.
Use `ai doctor` as a production preflight check for SPI discovery, selected analyzer/backend wiring and required API key configuration. It is offline by default; add `--online` only when the deployment environment should verify backend reachability.

SDK/chat usage:

```java
// Initialize components
SessionContext session = SessionContext.builder()
Expand Down Expand Up @@ -71,7 +108,9 @@ consilens-ai/
│ ├── consilens-ai-llm-api/
│ └── consilens-ai-llm-plugins/
│ ├── consilens-ai-llm-noop/
│ └── consilens-ai-llm-ollama/
│ ├── consilens-ai-llm-ollama/
│ ├── consilens-ai-llm-openai/
│ └── consilens-ai-llm-deepseek/
└── consilens-ai-tool/ # Tool system
├── consilens-ai-tool-api/
└── consilens-ai-tool-plugins/consilens-ai-tool-defaults/
Expand All @@ -82,6 +121,8 @@ consilens-ai/
### DiffTool
Compares two database tables via JDBC and identifies all differences.

This tool is intended for SDK/demo usage. Production CLI flows should generate a YAML config and execute through `DiffService` / `DefaultCompareRuntime`.

**Example**: "Compare the orders table between production and staging"

### AnalyzeTool
Expand Down Expand Up @@ -124,6 +165,16 @@ Configure Ollama (local LLM):
LLMBackend backend = new OllamaBackend("http://localhost:11434");
```

Configure OpenAI:
```java
LLMBackend backend = new OpenAIBackend("https://api.openai.com/v1", "gpt-4.1-mini", System.getenv("OPENAI_API_KEY"));
```

Configure DeepSeek:
```java
LLMBackend backend = new DeepSeekBackend("https://api.deepseek.com", "deepseek-chat", System.getenv("DEEPSEEK_API_KEY"));
```

Or use no-op backend (fallback to rule-based):
```java
LLMBackend backend = new NoopBackend();
Expand Down
78 changes: 77 additions & 1 deletion consilens-ai/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,54 @@

## Quick Start

### Production CLI Flow

```bash
consilens ai config "compare users from mysql to postgresql by id" \
--no-llm \
--source-type mysql \
--source-url jdbc:mysql://localhost:3306/mydb \
--source-table users \
--source-user-env MYSQL_USER \
--source-password-env MYSQL_PASSWORD \
--target-type postgresql \
--target-url jdbc:postgresql://localhost:5432/mydb \
--target-table users \
--target-user-env PG_USER \
--target-password-env PG_PASSWORD \
--keys id \
--fields name,email,status \
--output diff.yaml

consilens ai explain -c diff.yaml
consilens diff --dry-run -c diff.yaml
consilens diff -c diff.yaml
consilens ai diagnose --result diff-records.json --analyzer rulebased --output diagnose.md
consilens ai providers
consilens ai providers --format json
consilens ai doctor --format json
```

For cloud LLMs, set `--backend openai` with `OPENAI_API_KEY`, or `--backend deepseek` with `DEEPSEEK_API_KEY`. `CONSILENS_AI_BACKEND`, `CONSILENS_AI_MODEL`, `CONSILENS_AI_BASE_URL` and `CONSILENS_AI_TIMEOUT` can provide environment defaults. The AI command produces structured configuration; real diff execution still goes through the existing deterministic engine.

`ai diagnose` requires diff evidence, not only summary statistics. Configure a `json` `diff-record` sink before running `consilens diff`:
The analyzer is selected via SPI with `--analyzer <name>` or `CONSILENS_AI_ANALYZER`; default: `rulebased`.
Use `--output` to write the report to a file; omit it to print to stdout.
Use `ai providers` to verify discovered analyzer and LLM backend providers before enabling a production task. Add `--format json` for CI checks and scripts.
Use `ai doctor` for production preflight checks. It verifies provider discovery, selected analyzer/backend creation and required API key configuration without network calls by default; add `--online` to verify backend reachability.

```yaml
result:
sinks:
- format: console
type: result
- format: json
type: diff-record
properties:
path: ./diff-records.json
pretty: true
```

### Basic Conversation

```java
Expand Down Expand Up @@ -32,20 +80,38 @@ String response = engine.chat(
System.out.println(response);
```

Cloud backend examples:

```java
LLMBackend openai = new OpenAIBackend(
"https://api.openai.com/v1",
"gpt-4.1-mini",
System.getenv("OPENAI_API_KEY")
);

LLMBackend deepseek = new DeepSeekBackend(
"https://api.deepseek.com",
"deepseek-chat",
System.getenv("DEEPSEEK_API_KEY")
);
```

## Common Use Cases

### 1. Compare Two Database Tables

```
User: "Compare the 'users' table between production and staging"

Response: The AI will:
SDK/demo response: The AI will:
1. Ask for connection details (URLs, credentials)
2. Execute the diff using DiffTool
3. Report the number of differences found
4. Store the diff result for further analysis
```

For production CLI usage, prefer `consilens ai config` followed by `consilens diff --dry-run` and `consilens diff`.

**Tool Input Schema** (DiffTool):
```json
{
Expand Down Expand Up @@ -78,6 +144,14 @@ Response: The AI will:
4. Provide explanations and recommendations
```

Production CLI:

```bash
consilens ai diagnose --result diff-records.json --analyzer rulebased --output diagnose.md
```

The input must be either a JSON array of diff records or an object containing a `differences` array. A stats-only `result` JSON file is rejected because it does not contain row-level evidence.

### 3. Generate Repair SQL

```
Expand Down Expand Up @@ -298,6 +372,8 @@ The system validates and sanitizes all inputs:
The system is running in fallback mode. Either:
- Start an Ollama server: `ollama serve`
- Configure an OllamaBackend with correct URL
- Set `OPENAI_API_KEY` and configure OpenAIBackend
- Set `DEEPSEEK_API_KEY` and configure DeepSeekBackend
- Or intentionally use NoopBackend for rule-based only

### "Unknown tool: consilens_diff"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.consilens.spi.PluginManager;

import java.util.Map;
import java.util.Set;

/**
* Manages {@link AIAnalyzer} instances loaded via SPI.
Expand Down Expand Up @@ -49,4 +50,11 @@ public AIAnalyzer create(String name) {
public AIAnalyzer create(String name, Map<String, ?> config) {
return pluginManager.create(name, config);
}

/**
* Returns analyzer provider names discovered from the classpath.
*/
public Set<String> supportedNames() {
return pluginManager.getSupportedKeys();
}
}
6 changes: 6 additions & 0 deletions consilens-ai/consilens-ai-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,11 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.consilens.ai.config;

import com.consilens.ai.config.model.AIConfigDraft;
import com.consilens.ai.config.model.AIConfigIssue;
import com.consilens.ai.config.model.DatasetDraft;
import com.consilens.ai.config.model.MappingDraft;

import java.util.ArrayList;
import java.util.List;

/**
* Validates the minimal structured draft accepted from AI or CLI hints.
*/
public class AIConfigDraftValidator {

public List<AIConfigIssue> validate(AIConfigDraft draft) {
List<AIConfigIssue> issues = new ArrayList<>();
if (draft == null) {
issues.add(error("", "AI_CONFIG_DRAFT_MISSING", "AI config draft is required"));
return issues;
}
validateDataset("source", draft.getSource(), issues);
validateDataset("target", draft.getTarget(), issues);
validateMapping(draft.getMapping(), issues);
return issues;
}

public boolean hasErrors(List<AIConfigIssue> issues) {
return issues != null && issues.stream()
.anyMatch(issue -> issue.getSeverity() == AIConfigIssue.Severity.ERROR);
}

private void validateDataset(String side, DatasetDraft dataset, List<AIConfigIssue> issues) {
if (dataset == null) {
issues.add(error(side, "AI_CONFIG_" + side.toUpperCase() + "_MISSING", side + " dataset is required"));
return;
}
require(dataset.getType(), side + ".type", "AI_CONFIG_DATASET_TYPE_MISSING",
side + " dataset type is required", issues);
require(dataset.getJdbcUrl(), side + ".jdbcUrl", "AI_CONFIG_JDBC_URL_MISSING",
side + " JDBC URL is required", issues);
if (!blank(dataset.getJdbcUrl()) && !dataset.getJdbcUrl().startsWith("jdbc:")) {
issues.add(error(side + ".jdbcUrl", "AI_CONFIG_JDBC_URL_INVALID",
side + " JDBC URL must start with jdbc:"));
}
String resourceType = blank(dataset.getResourceType()) ? "table" : dataset.getResourceType().trim();
if (!"table".equalsIgnoreCase(resourceType) && !"sql".equalsIgnoreCase(resourceType)) {
issues.add(error(side + ".resourceType", "AI_CONFIG_UNSUPPORTED_RESOURCE_TYPE",
side + " resourceType must be table or sql"));
}
if ("sql".equalsIgnoreCase(resourceType)) {
require(dataset.getQuery(), side + ".query", "AI_CONFIG_QUERY_MISSING",
side + " query is required when resourceType=sql", issues);
validateSql(side + ".query", dataset.getQuery(), issues);
} else {
require(dataset.getResourceName(), side + ".resourceName", "AI_CONFIG_RESOURCE_NAME_MISSING",
side + " table name is required", issues);
}
require(dataset.getUsernameEnv(), side + ".usernameEnv", "AI_CONFIG_USERNAME_ENV_MISSING",
side + " username env variable name is required", issues);
require(dataset.getPasswordEnv(), side + ".passwordEnv", "AI_CONFIG_PASSWORD_ENV_MISSING",
side + " password env variable name is required", issues);
}

private void validateMapping(MappingDraft mapping, List<AIConfigIssue> issues) {
if (mapping == null) {
issues.add(error("mapping", "AI_CONFIG_MAPPING_MISSING", "mapping is required"));
return;
}
if (mapping.getSourceKeys() == null || mapping.getSourceKeys().isEmpty()
|| mapping.getTargetKeys() == null || mapping.getTargetKeys().isEmpty()) {
issues.add(error("mapping.keys", "AI_CONFIG_KEY_MISSING",
"sourceKeys and targetKeys are required"));
return;
}
if (mapping.getSourceKeys().size() != mapping.getTargetKeys().size()) {
issues.add(error("mapping.keys", "AI_CONFIG_KEY_MAPPING_INVALID",
"sourceKeys and targetKeys must have the same size"));
}
if (mapping.getSourceFields() != null && !mapping.getSourceFields().isEmpty()
&& (mapping.getTargetFields() == null
|| mapping.getSourceFields().size() != mapping.getTargetFields().size())) {
issues.add(error("mapping.fields", "AI_CONFIG_FIELD_MAPPING_INVALID",
"sourceFields and targetFields must have the same size"));
}
}

private void validateSql(String path, String sql, List<AIConfigIssue> issues) {
if (blank(sql)) {
return;
}
String value = sql.trim();
if (!(value.regionMatches(true, 0, "select ", 0, 7)
|| value.regionMatches(true, 0, "with ", 0, 5))) {
issues.add(error(path, "AI_CONFIG_QUERY_INVALID", "query must start with SELECT or WITH"));
}
if (value.contains(";") || value.contains("--") || value.contains("/*") || value.contains("*/")) {
issues.add(error(path, "AI_CONFIG_QUERY_UNSAFE", "query contains disallowed SQL fragments"));
}
}

private void require(String value, String path, String code, String message, List<AIConfigIssue> issues) {
if (blank(value)) {
issues.add(error(path, code, message));
}
}

private boolean blank(String value) {
return value == null || value.trim().isEmpty();
}

private AIConfigIssue error(String path, String code, String message) {
return AIConfigIssue.builder()
.severity(AIConfigIssue.Severity.ERROR)
.path(path)
.code(code)
.message(message)
.build();
}
}
Loading
Loading