my-ai-chat-framework
v4.0.0
Published
A lightweight AI chat framework with plugin system, unified message format, and tool calling support.
Maintainers
Readme
🤖 My AI Chat Framework
A lightweight, modular AI chat framework with plugin system, unified message format, and tool calling support.
⚠️ Note: This project is created for learning purposes and is AI‑generated. It is not intended for production use. No backward compatibility is guaranteed. Use at your own risk.
✨ Features
- Lightweight Core – ~300 lines, easy to understand and extend.
- Plugin System – Add features (tool calling, reasoning, custom adapters) without touching the core.
- Unified Message Format – Consistent data structure across all components.
- Multi‑Environment – Builds ES module, UMD, and CommonJS for browser & Node.js.
- No External Dependencies – Uses native
fetch(Node 18+ & modern browsers). - Tool Calling – Built‑in plugin to handle function calls from AI models.
- Streaming – Full support for real‑time responses.
- Flexible Configuration – Supports both flat and nested
modelParamsstructure. - Event‑Driven – Built‑in EventEmitter for
message,sending,error,stream-progressevents. - Pipeline Stages (v3.0) –
chat.pipe()to hook custom logic atbeforeSend/afterSendwithout touching the core. - Custom Error Classes –
APIError,NetworkError,ConfigurationError,ParsingErrorfor fine‑grained error handling.
🧱 Architecture
src/
├── index.js # Public entry point (re-exports)
├── core/
│ ├── ChatService.js # Main service: config, pipeline, send/stream, plugin hosting
│ ├── Pipeline.js # Sequential pipeline (v3.0: ctx flows through stages)
│ ├── MessageStore.js # In-memory message list with CRUD helpers
│ ├── SystemPromptStore.js # Toggleable system prompts
│ ├── EventEmitter.js # Minimal pub/sub (on/off/emit)
│ └── Errors.js # Custom error classes
├── adapters/
│ └── openai.js # OpenAI‑compatible API adapter (protocol via assertAdapter)
├── plugins/
│ ├── tool-calling.js # Tool calling plugin (afterSend stage, auto‑detect & loop)
│ └── model-registry.js # Model capability table (beforeSend stage)
└── utils/
├── MessageFormatter.js # Message format conversion (registrable)
├── typeCheck.js # Type checking helpers
└── url.js # URL joining utility
tests/ # node:test suites (core / pipeline / integration)Data flow:
User calls chat.send(input)
→ ChatService._request() → ctx flows through stages:
→ prepareInput (add user msg, merge config, emit 'sending')
→ beforeSend (internal hook + user beforeSend stages)
→ autoContinue / buildRequest (→ request body)
→ send (adapter.send / stream + retry + placeholder)
→ afterSend (user stages + tool-calling loop)
→ returns result, emitting 'message' along the wayWith toolCallingPlugin, the flow loops: response → detect tool_calls → execute tools → add tool results → sendExisting → repeat (max 5 iterations).
📦 Installation
npm install my-ai-chat-framework🚀 Quick Start
Basic Usage (Flat Configuration)
import { ChatService, openaiAdapter, toolCallingPlugin } from 'my-ai-chat-framework';
const chat = new ChatService({
apiKey: 'your-api-key',
baseUrl: 'https://api.deepseek.com', // optional, defaults to OpenAI
model: 'deepseek-chat',
temperature: 0.7,
maxTokens: 2000
});
chat.use(openaiAdapter);
chat.use(toolCallingPlugin);
chat.on('message', msg => console.log(msg.content));
await chat.send('Hello!');Factory Mode (Owned Configuration, Recommended)
import { ChatService, createOpenAIAdapter, createToolCallingPlugin } from 'my-ai-chat-framework';
// transport + request defaults belong to the adapter
const adapter = createOpenAIAdapter({
apiKey: 'your-api-key',
baseUrl: 'https://api.deepseek.com',
modelParams: { temperature: 0.8, maxTokens: 2000 }
});
const chat = new ChatService({
adapter, // injected at construction (same as chat.use(adapter))
model: 'deepseek-chat',
system: 'You are a helpful assistant'
});
chat.use(createToolCallingPlugin({ timeout: 30000 })); // plugin options
// per-request override (this request only)
await chat.send('Hello', { temperature: 0.2 });⚠️ The default exports
openaiAdapter/toolCallingPlugin/modelRegistryPluginare module-level singletons — installing one on multipleChatServiceinstances will clobber shared state. Use the factories (createXxx) for multiple instances.
Using modelParams (Recommended for Many Parameters)
const chat = new ChatService({
apiKey: 'your-api-key',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat', // still at top level for convenience
modelParams: { // optional parameters grouped
temperature: 0.8,
maxTokens: 1500,
reasoningEffort: 'medium' // for deepseek-reasoner
}
});Registering a Tool
chat.plugins['tool-calling'].registerTool('get_weather', 'Get current weather for a city',
async (args) => {
// args = { city: 'Beijing' }
return `Weather in ${args.city}: 22°C, sunny`;
},
{ // parameter schema (optional but recommended)
city: { type: 'string', description: 'City name', required: true }
}
);
await chat.send('What\'s the weather in Beijing?');
// → AI calls get_weather, framework executes it, AI responds with weather info🔌 Plugins & Adapters
openaiAdapter
Converts internal messages to OpenAI‑compatible format. Supports:
apiUrl– full URL (highest priority)baseUrl+path– base domain + API path- Defaults to
https://api.openai.com/v1/chat/completions
| Config field | Type | Default | Description |
|-------------|------|---------|-------------|
| apiKey | string | required | Bearer token for Authorization header |
| apiUrl | string | – | Full request URL (overrides baseUrl+path) |
| baseUrl | string | https://api.openai.com | API base domain |
| path | string | /v1/chat/completions | API endpoint path |
toolCallingPlugin
Detects tool_calls in assistant responses, executes registered tools, feeds results back, and continues the conversation (up to maxIterations = 5).
chat.plugins['tool-calling'].registerTool(name, description, executor, parameters?)– register a tool- Automatically injects
toolrole messages into the conversation - Recovers from tool execution errors gracefully (logs error, returns error message to model)
🔧 Extending with Pipeline (v3.0)
// Inject a role card before every request
chat.pipe({ name: 'role-card', phase: 'beforeSend', async run(ctx) {
ctx.messages.add({ role: 'system', content: 'You are a tsundere CEO.' });
}});
// Post-process the reply
chat.pipe({ name: 'stamp', phase: 'afterSend', run(ctx) {
if (ctx.result?.content) ctx.result.content += ' — ' + Date.now();
}});
chat.unpipe('role-card'); // remove
console.log(chat.pipelineStages); // inspect stage orderSee docs/DEVELOPER.md \u201cWrite a Stage\u201d for the full guide.
📡 Events (EventEmitter)
ChatService extends EventEmitter. Subscribe with chat.on(event, handler):
| Event | Payload | When |
|-------|---------|------|
| sending | { addUser, userInput, timestamp } | Before each request |
| message | { role, content, ... } | Full assistant message received |
| stream-progress | chunk object | Each streaming chunk arrives |
| error | { error, timestamp } | Any error during request |
chat.on('sending', ({ userInput }) => console.log('Sending:', userInput));
chat.on('message', msg => console.log('Got:', msg.content));
chat.on('error', ({ error }) => console.error('Error:', error.message));
// on() returns an unsubscribe function
const unsubscribe = chat.on('message', handler);
unsubscribe(); // stop listening🧩 ChatService API
| Method | Returns | Description |
|--------|---------|-------------|
| chat.send(userInput) | Promise<Message> | Send a message, get reply (non‑streaming) |
| chat.stream(userInput, onProgress, onDone) | Promise<Message> | Send a message, get streaming reply |
| chat.sendExisting() | Promise<Message> | Re‑send current messages without adding user input |
| chat.sendExistingStream(onProgress, onDone) | Promise<Message> | Same as above, streaming |
| chat.use(plugin) | this | Install a plugin/adapter |
| chat.setAdapter(adapter) | void | Manually set the adapter |
| chat.on(event, handler) | unsubscribe function | Subscribe to events |
| chat.plugins['tool-calling'].registerTool(name, desc, fn, params?) | this | Register a tool (requires toolCallingPlugin) |
| chat.messages | MessageStore | Access the message store directly |
🗄️ MessageStore API
| Method | Description |
|--------|-------------|
| add(message) | Add a raw message object |
| addUser(content, meta?) | Add a user message |
| addAssistant(content, meta?) | Add an assistant message |
| addSystem(content, meta?) | Add a system message |
| addTool(content, toolCallId, meta?) | Add a tool result message |
| addOnceAssistant(content, options?) | Add a one-shot assistant guide message (defaults to _ephemeral: true + prefix: true; options: { reasoningContent, prefix }) |
| getAll() | Return a shallow copy of all messages |
| getLast() | Return the last message (or null) |
| clear() | Remove all messages |
| undoToLastAssistant() | Remove messages after the last assistant message |
| undoToPreviousUser(id) | Remove messages from the message with id (inclusive) back to, but excluding, the previous user message — useful before resending |
Message format:
{
id: string, // auto‑generated if not provided
role: 'user' | 'assistant' | 'system' | 'tool',
content: string,
toolCalls?: Array, // assistant messages with tool calls
toolCallId?: string, // tool messages
reasoningContent?: string, // deepseek-reasoner
timestamp?: number,
metadata?: any
}❌ Error Handling
The framework throws typed errors for different failure modes:
| Error Class | .name | When |
|------------|---------|------|
| APIError | 'APIError' | Non‑2xx HTTP responses (401, 429, 500, etc.) |
| NetworkError | 'NetworkError' | fetch failures, connection timeouts |
| ConfigurationError | 'ConfigurationError' | Missing required config |
| ParsingError | 'ParsingError' | Malformed API response |
import { APIError, NetworkError, ConfigurationError, ParsingError } from 'my-ai-chat-framework';
try {
await chat.send('Hello');
} catch (error) {
if (error instanceof APIError) {
console.error(`API ${error.statusCode}: ${error.message}`);
} else if (error instanceof NetworkError) {
console.error('Network issue:', error.message);
}
}📚 Configuration Reference
new ChatService(config) accepts:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | required | Your API key |
| baseUrl | string | 'https://api.openai.com' | API base URL (used with path) |
| path | string | '/v1/chat/completions' | API path (used with baseUrl) |
| apiUrl | string | – | Full API URL (overrides baseUrl+path) |
| model | string | required | Model name (e.g., deepseek-chat) |
| modelParams | object | {} | Grouped model parameters (see below) |
| temperature | number | 0.7 | Sampling temperature (0–2) |
| maxTokens | number | 2000 | Max tokens to generate |
| reasoningEffort | string | – | For deepseek-reasoner: 'low', 'medium', 'high' |
Both flat and
modelParamsstyles work.modelParamstakes precedence over top‑level values.
modelParams
modelParams groups all model-related parameters. Supported fields:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| model | string | – | Model name (overrides top-level model) |
| temperature | number | 0.7 | Sampling temperature (0–2) |
| maxTokens | number | 2000 | Max tokens to generate |
| reasoningEffort | string | – | For deepseek-reasoner: 'low', 'medium', 'high' |
| topP | number | – | Nucleus sampling (0–1) |
| frequencyPenalty | number | – | Penalize frequent tokens (-2.0–2.0) |
| presencePenalty | number | – | Penalize repeated tokens (-2.0–2.0) |
| stop | string | string[] | – | Stop sequences |
| responseFormat | object | – | e.g., { type: 'json_object' } |
| seed | number | – | For reproducible results |
Naming convention: CamelCase fields (maxTokens, topP) are managed by the adapter (converted to API format). Any unknown field is passed through as‑is—use the provider's native naming (e.g., snake_case for OpenAI).
const chat = new ChatService({
apiKey: 'your-api-key',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
modelParams: {
temperature: 0.8,
maxTokens: 1500,
topP: 0.9,
frequencyPenalty: 0.5,
// unknown fields pass through directly (use API's native names):
logprobs: true,
top_logprobs: 5
}
});🧪 Testing
# 1. Create a .env file with your API key
echo "DEEPSEEK_API_KEY=sk-xxxxx" > .env
# 2. Run the test
npm testUnit tests: npm test (tests/core.test.js + tests/pipeline.test.js, no API key needed). Integration test: npm run test:integration (tests/integration.test.js, requires DEEPSEEK_API_KEY in .env).
🛠️ Development
git clone https://github.com/your-username/my-ai-chat-framework.git
cd my-ai-chat-framework
npm install
# Dev mode (watching)
npm run dev
# Build the library (ES + UMD + CJS)
npm run build
# Run tests
npm test📄 License
MIT
