Skip to content
Open
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: 3 additions & 0 deletions packages/ai-adapter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
*.log
195 changes: 195 additions & 0 deletions packages/ai-adapter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# @embeddedchat/ai-adapter

Pluggable AI adapter layer for [EmbeddedChat](https://github.com/RocketChat/EmbeddedChat). Connect any local or cloud AI provider to add smart widget features — reply suggestions, context-aware prompts, and more.

## Architecture

```
Host App
├── Config
└── AI Adapter (optional) ──▶ AI Backend (OpenAI / Ollama / custom)
EmbeddedChat
├── React UI
├── API Layer ──▶ Rocket.Chat Server
└── Auth
```

The AI backend is **completely independent** of the Rocket.Chat server. EmbeddedChat has **zero dependency** on this package — the host app owns the entire AI integration.

## Installation

```bash
npm install @embeddedchat/ai-adapter
```

## Quick Start

```jsx
import { EmbeddedChat } from '@embeddedchat/react';
import { OpenAIAdapter } from '@embeddedchat/ai-adapter';

const adapter = new OpenAIAdapter({
apiKey: process.env.OPENAI_API_KEY,
tasks: {
chat: { model: 'gpt-4o-mini', systemPrompt: 'Be concise and helpful.' },
composer: {
model: 'gpt-4o',
systemPrompt: 'Return only the requested text transformation.',
temperature: 0,
},
replySuggestions: {
model: 'gpt-4o-mini',
systemPrompt: 'Return only short, natural replies for the current user.',
temperature: 0,
maxTokens: 90,
},
},
});

<EmbeddedChat
host="https://chat.example.com"
roomId="GENERAL"
aiAdapter={adapter}
/>
```

When `aiAdapter` is provided, EmbeddedChat adds a header-level **Catch up** action, automatic reply suggestions, and selection-based composer actions. Catch ups are stored locally as **You only** messages; they are never sent to Rocket.Chat.

When `aiAdapter` is **not** provided: zero UI changes, zero bundle size impact.

## Built-in Adapters

### OpenAIAdapter

```typescript
import { OpenAIAdapter } from '@embeddedchat/ai-adapter';

const adapter = new OpenAIAdapter({
Comment thread
Aryan-Verma-999 marked this conversation as resolved.
apiKey: 'sk-...', // optional if using a proxy via baseUrl
model: 'gpt-4o', // default: 'gpt-4o'
maxTokens: 500, // default: 500
baseUrl: 'https://api.openai.com/v1', // override for proxies
headers: { 'X-Custom-Key': '...' }, // extra headers forwarded to every request
assistantUsername: 'ai-bot', // RC username of the AI — maps its messages to 'assistant' role
tasks: { /* optional task-specific model and prompt configuration */ },
});
```

### GeminiAdapter

```typescript
import { GeminiAdapter } from '@embeddedchat/ai-adapter';

const adapter = new GeminiAdapter({
apiKey: 'AIza...', // optional if using a proxy via baseUrl
model: 'gemini-2.0-flash', // default
baseUrl: 'https://generativelanguage.googleapis.com', // override for proxies
headers: { 'X-Custom-Key': '...' }, // extra headers
assistantUsername: 'ai-bot', // RC username of the AI — maps its messages to 'model' role
});
```

### OllamaAdapter (local / self-hosted)

```typescript
import { OllamaAdapter } from '@embeddedchat/ai-adapter';

const adapter = new OllamaAdapter({
baseUrl: 'http://localhost:11434', // default
model: 'llama3', // default
headers: { 'X-Custom-Key': '...' }, // useful when Ollama is behind an auth proxy
assistantUsername: 'ai-bot', // RC username of the AI — maps its messages to 'assistant' role
});
```

No API key required for Ollama. Runs entirely on your own hardware — ideal for privacy-conscious deployments.

## Task configuration

Every built-in adapter accepts `tasks`. This lets one adapter select a model and system prompt for each EmbeddedChat task instead of keeping prompts inside provider implementations.

```typescript
const adapter = new OpenAIAdapter({
apiKey: 'sk-...',
model: 'gpt-4o-mini', // fallback for tasks without a model override
tasks: {
chat: {
systemPrompt: 'Answer clearly and concisely.',
},
composer: {
model: 'gpt-4o',
systemPrompt: 'Return only the transformed source text.',
temperature: 0,
},
replySuggestions: {
model: 'gpt-4o-mini',
systemPrompt: 'Return three short replies and no transcript labels.',
temperature: 0,
maxTokens: 90,
},
},
});
```

The available task keys are `chat`, `composer`, and `replySuggestions`. All task fields are optional. If a task has no configuration, the adapter uses its top-level model and no system prompt.

## Writing a Custom Adapter

Implement `IAIAdapter` or extend `BaseAIAdapter`:

```typescript
import { BaseAIAdapter, AIContext, AIResponse } from '@embeddedchat/ai-adapter';

export class MyCustomAdapter extends BaseAIAdapter {
name = 'My AI';

async sendPrompt(context: AIContext, message: string): Promise<AIResponse> {
const reply = await myAIService.chat(message);
return { text: reply };
}

async isAvailable(): Promise<boolean> {
return await myAIService.ping();
}
}
```

`BaseAIAdapter` provides a default `getSuggestions()` implementation that calls `sendPrompt()`. Override it for provider-specific optimisation.

## Interface

```typescript
interface IAIAdapter {
name: string;
sendPrompt(context: AIContext, message: string): Promise<AIResponse>;
getSuggestions?(conversation: Message[]): Promise<string[]>;
isAvailable(): Promise<boolean>;
}

interface AIContext {
roomId: string;
userId: string;
history: Message[];
metadata?: { federated?: boolean; task?: AITaskType };
}

type AITaskType = 'chat' | 'composer' | 'replySuggestions';

interface AITaskConfig {
model?: string;
systemPrompt?: string;
temperature?: number;
maxTokens?: number;
}

interface AIResponse {
text: string;
suggestions?: string[];
}
```

## License

MIT
39 changes: 39 additions & 0 deletions packages/ai-adapter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@embeddedchat/ai-adapter",
"version": "0.0.1",
"description": "Pluggable AI adapter layer for EmbeddedChat — connect any local or cloud AI provider",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup -c",
"dev": "rollup -c --watch",
"format": "prettier --write 'src/'",
"format:check": "prettier --check 'src/'"
},
"keywords": [
"embeddedchat",
"ai",
"adapter",
"rocketchat",
"openai",
"ollama"
],
"license": "MIT",
"devDependencies": {
"prettier": "^2.8.1",
"rollup": "^3.23.0",
"rollup-plugin-dts": "^6.0.1",
"rollup-plugin-esbuild": "^5.0.0",
"typescript": "^5.0.0"
}
}
32 changes: 32 additions & 0 deletions packages/ai-adapter/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import dts from 'rollup-plugin-dts';
import esbuild from 'rollup-plugin-esbuild';
import path from 'path';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const packageJson = require(path.resolve(__dirname, './package.json'));

const name = packageJson.main.replace(/\.(?:c?js)$/, '');

const bundle = (config) => ({
...config,
input: 'src/index.ts',
external: (id) => id[0] !== '.' && !path.isAbsolute(id),
});

export default [
bundle({
plugins: [esbuild()],
output: [
{ file: `${name}.cjs`, format: 'cjs', sourcemap: true },
{ file: `${name}.mjs`, format: 'es', sourcemap: true },
],
}),
bundle({
plugins: [dts()],
output: { file: `${name}.d.ts`, format: 'es' },
}),
];
Loading