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
13 changes: 10 additions & 3 deletions bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"start": "node dist/index.cjs",
"build": "tsc --project tsconfig.json && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.cjs --format=cjs --packages=external",
"build:bundle-for-pkg": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.bundled.cjs --format=cjs --external:better-sqlite3 --external:@napi-rs/keyring --external:ssh2 --external:cpu-features --external:pg-native",
"copy:native": "node scripts/copy-native.js",
"rebuild:native": "npm rebuild better-sqlite3",
"build:pkg:win": "npm run build && npm run rebuild:native && npm run copy:native && npx @yao-pkg/pkg . --target node22-win-x64 --output ../src-tauri/resources/bridge-x86_64-pc-windows-msvc.exe",
"build:pkg:linux": "npm run build && npm run rebuild:native && npm run copy:native && npx @yao-pkg/pkg . --target node22-linux-x64 --output ../src-tauri/resources/bridge-x86_64-unknown-linux-gnu",
"build:pkg:win": "npm run build && npm run build:bundle-for-pkg && npm run rebuild:native && npm run copy:native && npx @yao-pkg/pkg dist/index.bundled.cjs --target node22-win-x64 --output ../src-tauri/resources/bridge-x86_64-pc-windows-msvc.exe",
"build:pkg:linux": "npm run build && npm run build:bundle-for-pkg && npm run rebuild:native && npm run copy:native && npx @yao-pkg/pkg dist/index.bundled.cjs --target node22-linux-x64 --output ../src-tauri/resources/bridge-x86_64-unknown-linux-gnu",
"test": "jest",
"test:watch": "jest --watchAll --detectOpenHandles"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.100.1",
"@google/generative-ai": "^0.24.1",
"@jest/globals": "^30.2.0",
"@mistralai/mistralai": "^2.2.5",
"@napi-rs/keyring": "^1.2.0",
"@types/ssh2": "^1.15.5",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^11.9.0",
"dotenv": "^17.2.3",
"groq-sdk": "^1.2.1",
"mysql2": "^3.15.3",
"ollama": "^0.6.3",
"openai": "^6.41.0",
"pg": "^8.16.3",
"pg-query-stream": "^4.10.3",
"pino": "^9.14.0",
Expand Down Expand Up @@ -53,4 +60,4 @@
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.0"
}
}
}
1,170 changes: 656 additions & 514 deletions bridge/pnpm-lock.yaml

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions bridge/src/ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# AI Integration (Bridge)

This folder contains AI provider implementations and prompt templates used by the bridge.

Layout
- `providers/` — concrete provider adapters (OpenAI, Anthropic, Gemini, Mistral, Ollama, Groq). Implement the `AIProvider` interface in `providers/types.ts`.
- `prompts/` — prompt builders and parsers for schema analysis, query explanation and chart recommendation.

What moved
- The public RPC entry points and handler logic live under `src/handlers/aiHandlers.ts`.
- The service factory implementation was moved to `src/services/ai.impl.ts` and a shim `src/services/aiService.ts` exposes `AIService` and `aiService` for consistency with other bridge services.
- Shared AI types were moved to `src/types/ai.ts` and are re-exported from the central `src/types/index.ts`.

How to add a provider
1. Create a new file under `providers/` implementing the `AIProvider` interface.
2. Add the provider into `src/services/ai.impl.ts` in the `createProvider` switch.
3. Add any provider-specific configuration notes to this README.

Testing
- No tests currently exist for AI. To exercise the integration locally, use the frontend UI features that call `ai.*` RPC methods or create a test that uses the RPC registrar.

Notes
- Keep providers small and avoid direct network retries; the bridge surface should translate provider errors into `AIError` using `providers/types.ts`.
66 changes: 66 additions & 0 deletions bridge/src/ai/prompts/chart-recommendation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ChartRecommendationInput, ChartRecommendation } from "../../types/";
import { SYSTEM_CONTEXT } from "./shared";
import { AIError } from "../providers/types";

export function buildChartRecommendationPrompt(input: ChartRecommendationInput): {
system: string;
user: string;
} {
const columnList = input.columns
.map((c) => {
const flags: string[] = [c.type];
if (c.isPrimaryKey) flags.push("PK");
if (c.sampleValues?.length) flags.push(`samples: ${c.sampleValues.slice(0, 3).join(", ")}`);
return `- ${c.name} (${flags.join(", ")})`;
})
.join("\n");

const user = `Given the table "${input.tableName}" with the following columns, recommend the best chart visualization:

## Columns
${columnList}

## Instructions
Respond ONLY with a valid JSON object — no markdown fences, no explanation text.
The JSON must match exactly this shape:
{
"chartType": "bar" | "line" | "area" | "pie",
"xAxis": "<column name>",
"yAxis": "<column name>",
"reasoning": "<one sentence explanation>"
}

Choose the most insightful combination. Prefer grouping a categorical/text column on X and counting a numeric/PK column on Y.`;

return { system: SYSTEM_CONTEXT, user };
}

/**
* Parse the raw LLM text response into a ChartRecommendation.
* The model is instructed to return bare JSON, but defensively strip
* any markdown fences if the model adds them anyway.
*/
export function parseChartRecommendation(raw: string): ChartRecommendation {
// Strip markdown code fences if present
const cleaned = raw
.replace(/```json\s*/gi, "")
.replace(/```\s*/g, "")
.trim();

let parsed: any;
try {
parsed = JSON.parse(cleaned);
} catch {
throw new AIError("PARSE_ERROR", "chart-recommendation", `Failed to parse chart recommendation JSON: ${raw.slice(0, 200)}`);
}

const validTypes = ["bar", "line", "area", "pie"];
const chartType = validTypes.includes(parsed.chartType) ? parsed.chartType : "bar";

return {
chartType: chartType as ChartRecommendation["chartType"],
xAxis: String(parsed.xAxis ?? ""),
yAxis: String(parsed.yAxis ?? ""),
reasoning: String(parsed.reasoning ?? ""),
};
}
42 changes: 42 additions & 0 deletions bridge/src/ai/prompts/query-explanation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { QueryExplanationInput } from "../../types/";
import { SYSTEM_CONTEXT, MARKDOWN_INSTRUCTION } from "./shared";

export function buildQueryExplanationPrompt(input: QueryExplanationInput): {
system: string;
user: string;
} {
const schemaContext =
input.schema && input.schema.length > 0
? input.schema
.map((t) => {
const cols = t.columns
.map((c) => `${c.name} ${c.type}${c.isPrimaryKey ? " PK" : ""}${c.isForeignKey ? " FK" : ""}`)
.join(", ");
return `- ${t.name}(${cols})`;
})
.join("\n")
: "Schema not provided.";

const dbType = input.databaseType ? ` (${input.databaseType})` : "";

const user = `Explain the following SQL query${dbType}:

\`\`\`sql
${input.sql}
\`\`\`

## Relevant Schema
${schemaContext}

## What to cover
1. **Query Purpose** — What does this query do in plain English?
2. **Joins** — Explain any joins and the relationships they traverse
3. **Filters** — What data is being filtered and why
4. **Aggregations** — Any GROUP BY, COUNT, SUM, etc., and what they compute
5. **Performance Concerns** — Potential bottlenecks, missing indexes, full table scans
6. **Suggested Improvements** — Rewritten or optimized version if applicable

${MARKDOWN_INSTRUCTION}`;

return { system: SYSTEM_CONTEXT, user };
}
55 changes: 55 additions & 0 deletions bridge/src/ai/prompts/schema-analysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { SchemaAnalysisInput } from "../../types/";
import { SYSTEM_CONTEXT, MARKDOWN_INSTRUCTION } from "./shared";

export function buildSchemaAnalysisPrompt(input: SchemaAnalysisInput): {
system: string;
user: string;
} {
const tableDescriptions = input.tables
.map((t) => {
const columns = t.columns
.map((c) => {
const flags: string[] = [];
if (c.isPrimaryKey) flags.push("PK");
if (c.isForeignKey) flags.push("FK");
if (!c.nullable) flags.push("NOT NULL");
if (c.references) flags.push(`→ ${c.references.table}.${c.references.column}`);
return ` - ${c.name} (${c.type})${flags.length ? " [" + flags.join(", ") + "]" : ""}`;
})
.join("\n");

const extras: string[] = [];
if (t.indexes?.length) extras.push(`Indexes: ${t.indexes.join(", ")}`);
if (t.foreignKeys?.length) extras.push(`Foreign keys: ${t.foreignKeys.join(", ")}`);
if (t.constraints?.length) extras.push(`Constraints: ${t.constraints.join(", ")}`);

return [
`### Table: ${t.schema ? `${t.schema}.` : ""}${t.name}`,
columns,
extras.length ? extras.map((e) => ` * ${e}`).join("\n") : "",
]
.filter(Boolean)
.join("\n");
})
.join("\n\n");

const dbType = input.databaseType ? ` (${input.databaseType})` : "";

const user = `Analyze the following database schema${dbType} and provide:

## What to cover
1. **Purpose** — What does this database appear to be for?
2. **Architecture** — Key design decisions and table relationships
3. **Missing Indexes** — Columns that should be indexed but aren't
4. **Schema Smells** — Anti-patterns, naming issues, or poor design choices
5. **Normalization Concerns** — Over/under-normalization issues
6. **Scalability Concerns** — Issues that may cause problems at scale
7. **Suggested Improvements** — Concrete, actionable recommendations

## Schema
${tableDescriptions || "No tables provided."}

${MARKDOWN_INSTRUCTION}`;

return { system: SYSTEM_CONTEXT, user };
}
18 changes: 18 additions & 0 deletions bridge/src/ai/prompts/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Shared prompt fragments used across all AI providers.
* Keep these provider-independent — no SDK-specific formatting here.
*/

export const SYSTEM_CONTEXT = `You are RelWave AI, an expert database assistant embedded in the RelWave desktop application.
RelWave helps developers manage PostgreSQL, MySQL, MariaDB, and SQLite databases.
Always respond in clear, well-structured Markdown unless instructed otherwise.
Be concise, practical, and actionable. Avoid boilerplate preambles.`;

export const MARKDOWN_INSTRUCTION = `Format your response in Markdown with:
- Level 2 headings (##) for major sections
- Bullet points for lists of items
- Code blocks (\`\`\`sql) for SQL examples
- Bold text for key terms and important warnings
Keep responses focused and under 1000 words unless the complexity demands more.`;

export const NO_DATA_PROMPT = "No structured data was provided.";
65 changes: 65 additions & 0 deletions bridge/src/ai/providers/anthropic.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Anthropic from "@anthropic-ai/sdk";
import { AIProvider, classifyError } from "./types";
import {
SchemaAnalysisInput,
QueryExplanationInput,
ChartRecommendationInput,
ChartRecommendation,
} from "../../types";
import { buildSchemaAnalysisPrompt } from "../prompts/schema-analysis";
import { buildQueryExplanationPrompt } from "../prompts/query-explanation";
import { buildChartRecommendationPrompt, parseChartRecommendation } from "../prompts/chart-recommendation";

const DEFAULT_MODEL = "claude-3-5-haiku-20241022";

export class AnthropicProvider implements AIProvider {
private client: Anthropic;

constructor(apiKey: string) {
this.client = new Anthropic({ apiKey });
}

private async complete(system: string, user: string): Promise<string> {
try {
const msg = await this.client.messages.create({
model: DEFAULT_MODEL,
max_tokens: 2048,
system,
messages: [{ role: "user", content: user }],
});
const block = msg.content[0];
return block.type === "text" ? block.text : "";
} catch (err) {
throw classifyError(err, "anthropic");
}
}

async analyzeSchema(input: SchemaAnalysisInput): Promise<string> {
const { system, user } = buildSchemaAnalysisPrompt(input);
return this.complete(system, user);
}

async explainQuery(input: QueryExplanationInput): Promise<string> {
const { system, user } = buildQueryExplanationPrompt(input);
return this.complete(system, user);
}

async recommendChart(input: ChartRecommendationInput): Promise<ChartRecommendation> {
const { system, user } = buildChartRecommendationPrompt(input);
const raw = await this.complete(system, user);
return parseChartRecommendation(raw);
}

async testConnection(): Promise<string> {
try {
await this.client.messages.create({
model: DEFAULT_MODEL,
max_tokens: 10,
messages: [{ role: "user", content: "ping" }],
});
return "";
} catch (err) {
throw classifyError(err, "anthropic");
}
}
}
60 changes: 60 additions & 0 deletions bridge/src/ai/providers/gemini.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { GoogleGenerativeAI } from "@google/generative-ai";
import { AIProvider, classifyError } from "./types";
import {
SchemaAnalysisInput,
QueryExplanationInput,
ChartRecommendationInput,
ChartRecommendation,
} from "../../types/";
import { buildSchemaAnalysisPrompt } from "../prompts/schema-analysis";
import { buildQueryExplanationPrompt } from "../prompts/query-explanation";
import { buildChartRecommendationPrompt, parseChartRecommendation } from "../prompts/chart-recommendation";

const DEFAULT_MODEL = "gemini-1.5-flash";

export class GeminiProvider implements AIProvider {
private genAI: GoogleGenerativeAI;

constructor(apiKey: string) {
this.genAI = new GoogleGenerativeAI(apiKey);
}

private async complete(system: string, user: string): Promise<string> {
try {
const model = this.genAI.getGenerativeModel({
model: DEFAULT_MODEL,
systemInstruction: system,
});
const result = await model.generateContent(user);
return result.response.text();
} catch (err) {
throw classifyError(err, "gemini");
}
}

async analyzeSchema(input: SchemaAnalysisInput): Promise<string> {
const { system, user } = buildSchemaAnalysisPrompt(input);
return this.complete(system, user);
}

async explainQuery(input: QueryExplanationInput): Promise<string> {
const { system, user } = buildQueryExplanationPrompt(input);
return this.complete(system, user);
}

async recommendChart(input: ChartRecommendationInput): Promise<ChartRecommendation> {
const { system, user } = buildChartRecommendationPrompt(input);
const raw = await this.complete(system, user);
return parseChartRecommendation(raw);
}

async testConnection(): Promise<string> {
try {
const model = this.genAI.getGenerativeModel({ model: DEFAULT_MODEL });
await model.generateContent("ping");
return "";
} catch (err) {
throw classifyError(err, "gemini");
}
}
}
Loading
Loading