Changeset 0.5.0 (#35)
This commit is contained in:
@@ -1,482 +0,0 @@
|
||||
# 🏗️ Chat Switchboard Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Chat Switchboard is a **plugin-first, multi-model AI platform** with dual-mode operation:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ FRONTEND (Vanilla JS) │
|
||||
│ ├─ Managed Mode: Full backend features + auth │
|
||||
│ └─ Unmanaged Mode: LocalStorage only, offline │
|
||||
└─────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
│ │
|
||||
[HTTP/WS] [LocalStorage Only]
|
||||
│
|
||||
┌────────▼─────────────────────────────────────────────┐
|
||||
│ BACKEND (Go + PostgreSQL) │
|
||||
│ ├─ Core API (minimal, orchestration only) │
|
||||
│ ├─ Extension Manager │
|
||||
│ └─ WebSocket Server (real-time features) │
|
||||
└────────┬─────────────────────────────────────────────┘
|
||||
│
|
||||
├─ PostgreSQL (state, users, data)
|
||||
│
|
||||
└─ Extensions (modular features)
|
||||
├─ Chat Engine (Python)
|
||||
├─ Channels (Go)
|
||||
├─ RAG Engine (Python)
|
||||
├─ Workflow Builder (Go/Python)
|
||||
└─ Custom Extensions...
|
||||
```
|
||||
|
||||
## Operating Modes
|
||||
|
||||
### 🏠 Unmanaged Mode (LocalStorage)
|
||||
**Use Case:** Personal use, offline, privacy-focused
|
||||
|
||||
- ✅ No backend required
|
||||
- ✅ No account/auth needed
|
||||
- ✅ Works offline
|
||||
- ✅ All data in browser LocalStorage
|
||||
- ✅ Export/import for backup
|
||||
- ❌ No multi-device sync
|
||||
- ❌ No collaboration features
|
||||
- ❌ No server-side extensions
|
||||
|
||||
**Detection:**
|
||||
```javascript
|
||||
// src/js/state.js
|
||||
State.mode = State.settings.backendUrl ? 'managed' : 'unmanaged';
|
||||
```
|
||||
|
||||
### 🌐 Managed Mode (Full Backend)
|
||||
**Use Case:** Teams, collaboration, advanced features
|
||||
|
||||
- ✅ Multi-user with authentication
|
||||
- ✅ Real-time collaboration (WebSockets)
|
||||
- ✅ Server-side extensions (Python, Go)
|
||||
- ✅ Shared knowledge bases
|
||||
- ✅ Workflow orchestration
|
||||
- ✅ Usage analytics
|
||||
- ❌ Requires backend deployment
|
||||
|
||||
**Switching:**
|
||||
```javascript
|
||||
// User configures backend URL in settings
|
||||
State.settings.backendUrl = 'https://api.chatswitch.example.com';
|
||||
State.settings.backendToken = 'jwt_token_here';
|
||||
```
|
||||
|
||||
## Core Features (All Modes)
|
||||
|
||||
### 1. Chat (User → LLM)
|
||||
- Per-conversation model selection
|
||||
- Streaming responses
|
||||
- Message history
|
||||
- Export (Markdown, JSON, Plain Text)
|
||||
|
||||
**Managed Mode Additions:**
|
||||
- Multi-model auto-routing
|
||||
- Cost tracking
|
||||
- Shared chats
|
||||
- Server-side tool calling
|
||||
|
||||
### 2. Channels (User → User + AI)
|
||||
**Managed Mode Only**
|
||||
|
||||
- Public/private channels
|
||||
- @mentions for users and AI models
|
||||
- Threaded conversations
|
||||
- Reactions
|
||||
- Real-time updates (WebSocket)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
@alice What do you think about this design?
|
||||
@claude-3.5 Can you review this code?
|
||||
```
|
||||
|
||||
### 3. Notes & Knowledge Bases
|
||||
**Both Modes:**
|
||||
- Personal notes
|
||||
- Markdown editing
|
||||
- Tagging and folders
|
||||
|
||||
**Managed Mode Additions:**
|
||||
- Shared notes
|
||||
- Knowledge base collections
|
||||
- RAG (Retrieval Augmented Generation)
|
||||
- Vector embeddings (pgvector)
|
||||
- Semantic search
|
||||
|
||||
### 4. Workflows (🌟 UNIQUE FEATURE)
|
||||
**Managed Mode Only**
|
||||
|
||||
Visual workflow builder for multi-step AI operations:
|
||||
|
||||
```
|
||||
[User Input] → [Model A: Research] → [Model B: Summarize] → [Tool: Save to KB] → [Output]
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Research pipelines (search → summarize → extract → store)
|
||||
- Multi-model consensus (run prompt through 3 models, compare)
|
||||
- Data processing (extract → transform → analyze → report)
|
||||
- Content creation (outline → draft → edit → format)
|
||||
|
||||
**Why It's Unique:**
|
||||
- Competitors have single-shot chat
|
||||
- This enables **AI composition** - chain multiple models and tools
|
||||
- Shareable templates (marketplace potential)
|
||||
- Visual no-code builder
|
||||
|
||||
## Extension Architecture
|
||||
|
||||
### Extension Types
|
||||
|
||||
#### 1. Frontend Extensions (UI Only)
|
||||
**Both Modes** - Pure JavaScript, no backend needed
|
||||
|
||||
```javascript
|
||||
// extensions/token-counter/main.js
|
||||
window.ChatSwitchboard.registerExtension({
|
||||
name: 'token-counter',
|
||||
hooks: {
|
||||
onMessageSend: (message) => {
|
||||
const tokens = estimateTokens(message);
|
||||
showToast(`~${tokens} tokens`);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. Backend Extensions (Full Power)
|
||||
**Managed Mode Only** - Python, Go, Node.js
|
||||
|
||||
```
|
||||
extensions/
|
||||
├── web-search/
|
||||
│ ├── extension.json # Manifest
|
||||
│ ├── main.py # Python service
|
||||
│ └── requirements.txt
|
||||
```
|
||||
|
||||
**Communication:** HTTP/gRPC between Go core and extension processes
|
||||
|
||||
### Extension Protocol
|
||||
|
||||
```json
|
||||
// extension.json
|
||||
{
|
||||
"name": "web-search",
|
||||
"version": "1.0.0",
|
||||
"runtime": "python",
|
||||
"entry": "main.py",
|
||||
"port": 9001,
|
||||
"capabilities": ["tool"],
|
||||
"tools": [
|
||||
{
|
||||
"name": "search_web",
|
||||
"description": "Search the web for information",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Frontend
|
||||
- **Vanilla JavaScript** - No framework bloat
|
||||
- **CSS3** - Custom dark theme
|
||||
- **LocalStorage** - Offline-first
|
||||
- **WebSocket** - Real-time (managed mode)
|
||||
|
||||
### Backend (Managed Mode)
|
||||
- **Go** - Core API, routing, orchestration
|
||||
- **PostgreSQL** - Primary data store
|
||||
- **pgvector** - Vector embeddings for RAG
|
||||
- **Redis** - Session cache, WebSocket pub/sub (optional)
|
||||
- **Python** - AI/ML extensions (LangChain, embeddings)
|
||||
|
||||
### Infrastructure
|
||||
- **Docker** - Easy deployment
|
||||
- **nginx** - Reverse proxy
|
||||
- **Let's Encrypt** - TLS certificates
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Unmanaged Mode
|
||||
```
|
||||
User Input → Frontend State → LocalStorage
|
||||
↓
|
||||
API Provider (OpenAI, etc)
|
||||
↓
|
||||
Frontend State → LocalStorage
|
||||
```
|
||||
|
||||
### Managed Mode
|
||||
```
|
||||
User Input → Frontend → WebSocket → Backend
|
||||
↓
|
||||
PostgreSQL
|
||||
↓
|
||||
Extension Manager
|
||||
↓ ↓
|
||||
[Chat Engine] [RAG Engine]
|
||||
↓ ↓
|
||||
Model Router → API Provider
|
||||
↓
|
||||
WebSocket → Frontend
|
||||
```
|
||||
|
||||
## WebSocket Protocol
|
||||
|
||||
### Events (Managed Mode)
|
||||
|
||||
```javascript
|
||||
// Client → Server
|
||||
{
|
||||
type: 'chat.send',
|
||||
chatId: 'uuid',
|
||||
message: 'Hello',
|
||||
model: 'gpt-4'
|
||||
}
|
||||
|
||||
// Server → Client (streaming)
|
||||
{
|
||||
type: 'chat.stream',
|
||||
chatId: 'uuid',
|
||||
delta: 'Hello! ',
|
||||
done: false
|
||||
}
|
||||
|
||||
// Channel messages
|
||||
{
|
||||
type: 'channel.message',
|
||||
channelId: 'uuid',
|
||||
content: '@claude What do you think?',
|
||||
mentions: {users: [], models: ['claude-3.5']}
|
||||
}
|
||||
|
||||
// AI response in channel
|
||||
{
|
||||
type: 'channel.ai_response',
|
||||
channelId: 'uuid',
|
||||
model: 'claude-3.5',
|
||||
content: 'I think...',
|
||||
inReplyTo: 'message_uuid'
|
||||
}
|
||||
```
|
||||
|
||||
### Connection Management
|
||||
```go
|
||||
// server/websocket/hub.go
|
||||
type Hub struct {
|
||||
clients map[*Client]bool
|
||||
broadcast chan Message
|
||||
register chan *Client
|
||||
unregister chan *Client
|
||||
}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Unmanaged Mode
|
||||
- All sensitive data (API keys) in browser localStorage
|
||||
- User responsible for backups
|
||||
- No server-side attack surface
|
||||
|
||||
### Managed Mode
|
||||
- **Authentication:** JWT tokens
|
||||
- **Authorization:** RBAC (roles: user, admin, moderator)
|
||||
- **API Keys:** Encrypted at rest (pgcrypto)
|
||||
- **Rate Limiting:** Per-user, per-endpoint
|
||||
- **CORS:** Strict origin checking
|
||||
- **Webhooks:** HMAC signature verification
|
||||
- **Extensions:** Sandboxed execution
|
||||
|
||||
## Deployment
|
||||
|
||||
### Unmanaged Mode
|
||||
```bash
|
||||
# Build standalone
|
||||
./build.sh
|
||||
|
||||
# Serve
|
||||
python3 -m http.server 8080 --directory standalone
|
||||
# or upload index.html to any static host
|
||||
```
|
||||
|
||||
### Managed Mode
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker-compose up -d
|
||||
|
||||
# Services:
|
||||
# - postgres:5432
|
||||
# - backend:8080
|
||||
# - redis:6379 (optional)
|
||||
# - nginx:443
|
||||
```
|
||||
|
||||
## Scaling
|
||||
|
||||
### Horizontal Scaling (Managed)
|
||||
```
|
||||
┌─ Load Balancer (nginx)
|
||||
├─ Backend Instance 1 ─┐
|
||||
├─ Backend Instance 2 ─┼─ PostgreSQL (primary)
|
||||
└─ Backend Instance 3 ─┘
|
||||
│
|
||||
Redis (WebSocket sync)
|
||||
```
|
||||
|
||||
### Extension Scaling
|
||||
- Extensions are separate processes
|
||||
- Can run on different machines
|
||||
- Service discovery via extension registry
|
||||
|
||||
## Plugin Ecosystem (🌟 DIFFERENTIATOR)
|
||||
|
||||
### Core Principle
|
||||
**Everything is a plugin** (except minimal routing core)
|
||||
|
||||
| Feature | Implementation |
|
||||
|---------|---------------|
|
||||
| Chat | `extensions/chat-engine/` |
|
||||
| Channels | `extensions/channels/` |
|
||||
| Notes | `extensions/notes/` |
|
||||
| Knowledge Bases | `extensions/rag-engine/` |
|
||||
| Workflows | `extensions/workflows/` |
|
||||
|
||||
### Why This Matters
|
||||
1. **Proof of Extensibility** - If core features work as plugins, any feature can
|
||||
2. **Community Innovation** - Users build missing features
|
||||
3. **Customization** - Disable unwanted features
|
||||
4. **Marketplace Potential** - Monetize premium plugins
|
||||
|
||||
### Plugin Types
|
||||
|
||||
#### Official Plugins (Bundled)
|
||||
- Chat Engine
|
||||
- Channels
|
||||
- RAG/Knowledge
|
||||
- Web Search
|
||||
- Code Execution
|
||||
|
||||
#### Community Plugins (Marketplace)
|
||||
- Specialized tools
|
||||
- Industry-specific workflows
|
||||
- Integration plugins (Slack, Discord, etc)
|
||||
- Custom AI models
|
||||
|
||||
#### Enterprise Plugins (Premium)
|
||||
- SSO/SAML
|
||||
- Advanced analytics
|
||||
- Compliance logging
|
||||
- Custom model hosting
|
||||
|
||||
## Unique Features Summary
|
||||
|
||||
### 1. Workflow Builder (⭐⭐⭐)
|
||||
**What:** Visual DAG builder for multi-step AI operations
|
||||
**Why Unique:** No competitor has AI orchestration at this level
|
||||
**Use Case:** Research assistant = [Search] → [GPT-4 summarize] → [Claude verify] → [Save to KB]
|
||||
|
||||
### 2. Dual-Mode Operation (⭐⭐)
|
||||
**What:** Works offline (LocalStorage) or managed (full backend)
|
||||
**Why Unique:** Privacy-first with optional collaboration
|
||||
**Use Case:** Personal use → upgrade to team without migration
|
||||
|
||||
### 3. Plugin-First Architecture (⭐⭐⭐)
|
||||
**What:** Core features ARE plugins (dogfooding)
|
||||
**Why Unique:** Proves extensibility, enables marketplace
|
||||
**Use Case:** Build custom enterprise workflows as plugins
|
||||
|
||||
### 4. Multi-Model Auto-Routing (⭐⭐)
|
||||
**What:** Automatically route to best/cheapest model
|
||||
**Why Unique:** Most tools lock you to one provider
|
||||
**Use Case:** Simple questions → GPT-3.5 ($), complex → GPT-4 ($$$)
|
||||
|
||||
### 5. Channels with AI Participants (⭐)
|
||||
**What:** Slack-like channels where you @mention AI models
|
||||
**Why Unique:** Collaborative AI + human conversations
|
||||
**Use Case:** Team discusses design, @claude gives input, @gpt4 suggests alternatives
|
||||
|
||||
## Comparison Matrix
|
||||
|
||||
| Feature | Chat Switchboard | Open WebUI | ChatGPT | Claude Desktop |
|
||||
|---------|-----------------|------------|---------|----------------|
|
||||
| Multi-Model | ✅ | ✅ | ❌ | ❌ |
|
||||
| Plugin System | ✅⭐ | Limited | ❌ | ❌ |
|
||||
| Offline Mode | ✅ | ❌ | ❌ | Limited |
|
||||
| Workflows | ✅⭐ | ❌ | ❌ | ❌ |
|
||||
| Channels | ✅ | ✅ | ❌ | ❌ |
|
||||
| RAG/Knowledge | ✅ | ✅ | ❌ | Limited |
|
||||
| Self-Hosted | ✅ | ✅ | ❌ | ❌ |
|
||||
| Open Source | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
**Competitive Advantage:** Workflows + Plugin Architecture + Dual-Mode
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1: Foundation (Weeks 1-4)
|
||||
- ✅ Frontend core
|
||||
- ⬜ Backend API skeleton
|
||||
- ⬜ PostgreSQL integration
|
||||
- ⬜ WebSocket server
|
||||
- ⬜ Extension manager
|
||||
|
||||
### Phase 2: Core Features (Weeks 5-8)
|
||||
- ⬜ Chat engine (as plugin)
|
||||
- ⬜ Channels (as plugin)
|
||||
- ⬜ Notes (as plugin)
|
||||
- ⬜ Basic auth/users
|
||||
|
||||
### Phase 3: Unique Features (Weeks 9-12)
|
||||
- ⬜ RAG/Knowledge bases
|
||||
- ⬜ Workflow builder (visual editor)
|
||||
- ⬜ Model auto-routing
|
||||
- ⬜ Extension marketplace basics
|
||||
|
||||
### Phase 4: Polish (Weeks 13-16)
|
||||
- ⬜ Desktop app (Tauri)
|
||||
- ⬜ Mobile-responsive
|
||||
- ⬜ Documentation
|
||||
- ⬜ Example plugins (10+)
|
||||
- ⬜ Launch 🚀
|
||||
|
||||
## Contributing
|
||||
|
||||
Since core features are plugins, contributors can:
|
||||
1. Build new extensions (any language)
|
||||
2. Improve existing plugins (PRs welcome)
|
||||
3. Share workflows (template marketplace)
|
||||
4. Report bugs, suggest features
|
||||
|
||||
**Plugin Development:**
|
||||
```bash
|
||||
# Use template
|
||||
cp -r extensions/_template-python extensions/my-plugin
|
||||
cd extensions/my-plugin
|
||||
# Edit extension.json, main.py
|
||||
python main.py # Runs on port from manifest
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT - Build whatever you want, including commercial
|
||||
|
||||
---
|
||||
|
||||
**Questions?** See `/docs` for detailed guides or join discussions.
|
||||
@@ -1,698 +0,0 @@
|
||||
# ⚙️ Backend Features & Extensions Architecture
|
||||
|
||||
## Overview
|
||||
This document defines the **server-side architecture** for Chat Switchboard, focusing on persistent storage, multi-model routing, function/tool calling, and backend extension capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Core Backend Features
|
||||
|
||||
### 1. **Multi-Model Router**
|
||||
Route requests to different LLM providers based on model, capabilities, cost, or custom logic.
|
||||
|
||||
#### Architecture
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Frontend │
|
||||
└────────┬────────┘
|
||||
│ OpenAI-compatible API
|
||||
│
|
||||
┌────────▼────────────────────────────────────────┐
|
||||
│ Chat Switchboard Server │
|
||||
│ ┌──────────────────────────────────────────┐ │
|
||||
│ │ Model Router │ │
|
||||
│ │ - Route selection logic │ │
|
||||
│ │ - Fallback handling │ │
|
||||
│ │ - Load balancing │ │
|
||||
│ └──────────┬───────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────▼───────────────────────────────┐ │
|
||||
│ │ Provider Adapters │ │
|
||||
│ ├──────────────────────────────────────────┤ │
|
||||
│ │ OpenAI │ Anthropic │ Local │ Custom │ │
|
||||
│ └──────┬─────┴─────┬─────┴───┬───┴────┬────┘ │
|
||||
└─────────┼───────────┼─────────┼────────┼───────┘
|
||||
│ │ │ │
|
||||
OpenAI API Claude API Ollama Custom
|
||||
```
|
||||
|
||||
#### Implementation (`server/router/model_router.go`)
|
||||
```go
|
||||
type ModelRouter struct {
|
||||
providers map[string]Provider
|
||||
config *RouterConfig
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
Name() string
|
||||
Models() []string
|
||||
ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error)
|
||||
StreamCompletion(ctx context.Context, req *ChatRequest) (<-chan *ChatChunk, error)
|
||||
SupportsTools() bool
|
||||
}
|
||||
|
||||
type RouterConfig struct {
|
||||
DefaultProvider string
|
||||
FallbackChain []string
|
||||
ModelMapping map[string]string // model -> provider
|
||||
LoadBalancing bool
|
||||
CostOptimization bool
|
||||
}
|
||||
|
||||
func (r *ModelRouter) Route(model string, req *ChatRequest) (Provider, error) {
|
||||
// 1. Check explicit mapping
|
||||
if provider, ok := r.config.ModelMapping[model]; ok {
|
||||
return r.providers[provider], nil
|
||||
}
|
||||
|
||||
// 2. Query each provider for model support
|
||||
for _, provider := range r.providers {
|
||||
if provider.SupportsModel(model) {
|
||||
return provider, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback chain
|
||||
if len(r.config.FallbackChain) > 0 {
|
||||
return r.providers[r.config.FallbackChain[0]], nil
|
||||
}
|
||||
|
||||
return nil, ErrModelNotFound
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **Tool/Function Calling System**
|
||||
Enable LLMs to call external functions and tools.
|
||||
|
||||
#### Tool Registry (`server/tools/registry.go`)
|
||||
```go
|
||||
type Tool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
Handler ToolHandler
|
||||
}
|
||||
|
||||
type ToolHandler func(ctx context.Context, args map[string]interface{}) (interface{}, error)
|
||||
|
||||
type ToolRegistry struct {
|
||||
tools map[string]*Tool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Register(tool *Tool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.tools[tool.Name] = tool
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Execute(name string, args map[string]interface{}) (interface{}, error) {
|
||||
r.mu.RLock()
|
||||
tool, ok := r.tools[name]
|
||||
r.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil, ErrToolNotFound
|
||||
}
|
||||
|
||||
return tool.Handler(context.Background(), args)
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) GetOpenAIFunctions() []map[string]interface{} {
|
||||
// Convert tools to OpenAI function calling format
|
||||
functions := []map[string]interface{}{}
|
||||
for _, tool := range r.tools {
|
||||
functions = append(functions, map[string]interface{}{
|
||||
"name": tool.Name,
|
||||
"description": tool.Description,
|
||||
"parameters": tool.Parameters,
|
||||
})
|
||||
}
|
||||
return functions
|
||||
}
|
||||
```
|
||||
|
||||
#### Built-in Tools
|
||||
```go
|
||||
// Web Search Tool
|
||||
func init() {
|
||||
toolRegistry.Register(&Tool{
|
||||
Name: "web_search",
|
||||
Description: "Search the web for current information",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"query": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Search query",
|
||||
},
|
||||
},
|
||||
"required": []string{"query"},
|
||||
},
|
||||
Handler: func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
query := args["query"].(string)
|
||||
// Implement web search (DuckDuckGo, Google, etc.)
|
||||
results := performWebSearch(query)
|
||||
return results, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Code Execution Tool
|
||||
func init() {
|
||||
toolRegistry.Register(&Tool{
|
||||
Name: "execute_code",
|
||||
Description: "Execute code in a sandboxed environment",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"language": map[string]string{
|
||||
"type": "string",
|
||||
"enum": []string{"python", "javascript", "bash"},
|
||||
},
|
||||
"code": map[string]string{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": []string{"language", "code"},
|
||||
},
|
||||
Handler: func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
lang := args["language"].(string)
|
||||
code := args["code"].(string)
|
||||
|
||||
// Execute in Docker container or isolated runtime
|
||||
result := executeSandboxed(lang, code)
|
||||
return result, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### Function Calling Flow
|
||||
```go
|
||||
func (h *ChatHandler) HandleFunctionCalling(req *ChatRequest) (*ChatResponse, error) {
|
||||
// 1. Send initial request with tools
|
||||
req.Tools = h.toolRegistry.GetOpenAIFunctions()
|
||||
resp, err := h.provider.ChatCompletion(req)
|
||||
|
||||
// 2. Check if model wants to call a function
|
||||
if resp.FunctionCall != nil {
|
||||
// 3. Execute the function
|
||||
result, err := h.toolRegistry.Execute(
|
||||
resp.FunctionCall.Name,
|
||||
resp.FunctionCall.Arguments,
|
||||
)
|
||||
|
||||
// 4. Send function result back to model
|
||||
req.Messages = append(req.Messages, Message{
|
||||
Role: "function",
|
||||
Name: resp.FunctionCall.Name,
|
||||
Content: jsonStringify(result),
|
||||
})
|
||||
|
||||
// 5. Get final response
|
||||
return h.provider.ChatCompletion(req)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **Backend Extension System**
|
||||
Plugin architecture for server-side extensions.
|
||||
|
||||
#### Extension Interface (`server/extensions/extension.go`)
|
||||
```go
|
||||
type Extension interface {
|
||||
ID() string
|
||||
Name() string
|
||||
Version() string
|
||||
|
||||
// Lifecycle
|
||||
Initialize(ctx context.Context, config map[string]interface{}) error
|
||||
Shutdown() error
|
||||
|
||||
// Hooks
|
||||
OnChatRequest(ctx context.Context, req *ChatRequest) error
|
||||
OnChatResponse(ctx context.Context, resp *ChatResponse) error
|
||||
OnToolCall(ctx context.Context, tool string, args map[string]interface{}) error
|
||||
|
||||
// Optional: Provide custom tools
|
||||
RegisterTools() []*Tool
|
||||
|
||||
// Optional: Provide custom routes
|
||||
RegisterRoutes(router *gin.Engine)
|
||||
}
|
||||
|
||||
type BaseExtension struct {
|
||||
id string
|
||||
name string
|
||||
version string
|
||||
}
|
||||
|
||||
func (e *BaseExtension) ID() string { return e.id }
|
||||
func (e *BaseExtension) Name() string { return e.name }
|
||||
func (e *BaseExtension) Version() string { return e.version }
|
||||
|
||||
// Default no-op implementations
|
||||
func (e *BaseExtension) Initialize(ctx context.Context, config map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
func (e *BaseExtension) Shutdown() error { return nil }
|
||||
func (e *BaseExtension) OnChatRequest(ctx context.Context, req *ChatRequest) error {
|
||||
return nil
|
||||
}
|
||||
func (e *BaseExtension) OnChatResponse(ctx context.Context, resp *ChatResponse) error {
|
||||
return nil
|
||||
}
|
||||
func (e *BaseExtension) OnToolCall(ctx context.Context, tool string, args map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
func (e *BaseExtension) RegisterTools() []*Tool { return nil }
|
||||
func (e *BaseExtension) RegisterRoutes(router *gin.Engine) {}
|
||||
```
|
||||
|
||||
#### Extension Manager
|
||||
```go
|
||||
type ExtensionManager struct {
|
||||
extensions map[string]Extension
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *ExtensionManager) Load(ext Extension, config map[string]interface{}) error {
|
||||
if err := ext.Initialize(context.Background(), config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.extensions[ext.ID()] = ext
|
||||
m.mu.Unlock()
|
||||
|
||||
// Register any tools provided by extension
|
||||
for _, tool := range ext.RegisterTools() {
|
||||
toolRegistry.Register(tool)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ExtensionManager) ExecuteHook(hookName string, args ...interface{}) error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, ext := range m.extensions {
|
||||
switch hookName {
|
||||
case "OnChatRequest":
|
||||
if err := ext.OnChatRequest(args[0].(context.Context), args[1].(*ChatRequest)); err != nil {
|
||||
return err
|
||||
}
|
||||
case "OnChatResponse":
|
||||
if err := ext.OnChatResponse(args[0].(context.Context), args[1].(*ChatResponse)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. **Example Backend Extensions**
|
||||
|
||||
#### A. Logging Extension
|
||||
```go
|
||||
type LoggingExtension struct {
|
||||
BaseExtension
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewLoggingExtension() *LoggingExtension {
|
||||
return &LoggingExtension{
|
||||
BaseExtension: BaseExtension{
|
||||
id: "logging",
|
||||
name: "Request Logging",
|
||||
version: "1.0.0",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *LoggingExtension) OnChatRequest(ctx context.Context, req *ChatRequest) error {
|
||||
log.Printf("[CHAT] Model: %s, Messages: %d", req.Model, len(req.Messages))
|
||||
|
||||
// Store in database
|
||||
_, err := e.db.Exec(`
|
||||
INSERT INTO request_logs (user_id, model, message_count, timestamp)
|
||||
VALUES ($1, $2, $3, NOW())
|
||||
`, getUserID(ctx), req.Model, len(req.Messages))
|
||||
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Rate Limiting Extension
|
||||
```go
|
||||
type RateLimitExtension struct {
|
||||
BaseExtension
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
func (e *RateLimitExtension) OnChatRequest(ctx context.Context, req *ChatRequest) error {
|
||||
if !e.limiter.Allow() {
|
||||
return errors.New("rate limit exceeded")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
#### C. Cost Tracking Extension
|
||||
```go
|
||||
type CostTrackingExtension struct {
|
||||
BaseExtension
|
||||
pricing map[string]float64 // model -> price per 1K tokens
|
||||
}
|
||||
|
||||
func (e *CostTrackingExtension) OnChatResponse(ctx context.Context, resp *ChatResponse) error {
|
||||
model := resp.Model
|
||||
tokens := resp.Usage.TotalTokens
|
||||
|
||||
cost := (float64(tokens) / 1000.0) * e.pricing[model]
|
||||
|
||||
log.Printf("[COST] Model: %s, Tokens: %d, Cost: $%.4f", model, tokens, cost)
|
||||
|
||||
// Store in user account
|
||||
return e.updateUserBalance(getUserID(ctx), cost)
|
||||
}
|
||||
```
|
||||
|
||||
#### D. Content Moderation Extension
|
||||
```go
|
||||
type ModerationExtension struct {
|
||||
BaseExtension
|
||||
moderationAPI string
|
||||
}
|
||||
|
||||
func (e *ModerationExtension) OnChatRequest(ctx context.Context, req *ChatRequest) error {
|
||||
lastMsg := req.Messages[len(req.Messages)-1]
|
||||
|
||||
flagged, err := e.checkModeration(lastMsg.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if flagged {
|
||||
return errors.New("content violates policy")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. **Database Schema**
|
||||
|
||||
#### Core Tables (`migrations/001_initial.sql`)
|
||||
```sql
|
||||
-- Users
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- API Configurations (multiple API keys per user)
|
||||
CREATE TABLE api_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
endpoint VARCHAR(255) NOT NULL,
|
||||
api_key_encrypted TEXT NOT NULL,
|
||||
provider VARCHAR(50) NOT NULL, -- 'openai', 'anthropic', 'local', etc.
|
||||
is_default BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Chats
|
||||
CREATE TABLE chats (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(500),
|
||||
model VARCHAR(100),
|
||||
api_config_id UUID REFERENCES api_configs(id),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Messages
|
||||
CREATE TABLE messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
chat_id UUID REFERENCES chats(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL, -- 'system', 'user', 'assistant', 'function'
|
||||
content TEXT NOT NULL,
|
||||
name VARCHAR(100), -- for function messages
|
||||
function_call JSONB, -- { "name": "...", "arguments": "..." }
|
||||
metadata JSONB, -- extensible metadata
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- User Settings
|
||||
CREATE TABLE user_settings (
|
||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Extension Data (for extensions to store persistent data)
|
||||
CREATE TABLE extension_data (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
extension_id VARCHAR(100) NOT NULL,
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
key VARCHAR(255) NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(extension_id, user_id, key)
|
||||
);
|
||||
|
||||
-- Request Logs (for analytics)
|
||||
CREATE TABLE request_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
model VARCHAR(100),
|
||||
message_count INT,
|
||||
tokens_used INT,
|
||||
cost DECIMAL(10, 6),
|
||||
duration_ms INT,
|
||||
timestamp TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_chats_user_id ON chats(user_id);
|
||||
CREATE INDEX idx_messages_chat_id ON messages(chat_id);
|
||||
CREATE INDEX idx_request_logs_user_id ON request_logs(user_id);
|
||||
CREATE INDEX idx_request_logs_timestamp ON request_logs(timestamp);
|
||||
CREATE INDEX idx_extension_data_lookup ON extension_data(extension_id, user_id, key);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. **Provider Adapters**
|
||||
|
||||
#### OpenAI Provider (`server/providers/openai.go`)
|
||||
```go
|
||||
type OpenAIProvider struct {
|
||||
apiKey string
|
||||
endpoint string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
|
||||
// Standard OpenAI API implementation
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) StreamCompletion(ctx context.Context, req *ChatRequest) (<-chan *ChatChunk, error) {
|
||||
// SSE streaming implementation
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) SupportsTools() bool {
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
#### Anthropic Provider (`server/providers/anthropic.go`)
|
||||
```go
|
||||
type AnthropicProvider struct {
|
||||
apiKey string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (p *AnthropicProvider) ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
|
||||
// Convert to Anthropic message format
|
||||
anthropicReq := p.convertRequest(req)
|
||||
|
||||
// Call Anthropic API
|
||||
resp := p.callAPI(anthropicReq)
|
||||
|
||||
// Convert back to standard format
|
||||
return p.convertResponse(resp), nil
|
||||
}
|
||||
```
|
||||
|
||||
#### Local/Ollama Provider (`server/providers/ollama.go`)
|
||||
```go
|
||||
type OllamaProvider struct {
|
||||
endpoint string // http://localhost:11434
|
||||
}
|
||||
|
||||
func (p *OllamaProvider) ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
|
||||
// Call local Ollama instance
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. **API Endpoints**
|
||||
|
||||
#### Unified Chat Endpoint
|
||||
```go
|
||||
// POST /api/v1/chat/completions
|
||||
// OpenAI-compatible endpoint
|
||||
func (h *ChatHandler) CreateCompletion(c *gin.Context) {
|
||||
var req ChatRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Execute extension hooks
|
||||
if err := extensionManager.ExecuteHook("OnChatRequest", c.Request.Context(), &req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Route to appropriate provider
|
||||
provider, err := modelRouter.Route(req.Model, &req)
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{"error": "model not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle streaming vs non-streaming
|
||||
if req.Stream {
|
||||
h.streamResponse(c, provider, &req)
|
||||
} else {
|
||||
resp, err := provider.ChatCompletion(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Execute response hooks
|
||||
extensionManager.ExecuteHook("OnChatResponse", c.Request.Context(), resp)
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Tool Execution Endpoint
|
||||
```go
|
||||
// POST /api/v1/tools/execute
|
||||
func (h *ToolHandler) Execute(c *gin.Context) {
|
||||
var req struct {
|
||||
Tool string `json:"tool"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := toolRegistry.Execute(req.Tool, req.Args)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"result": result})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. **Configuration**
|
||||
|
||||
#### Server Config (`server/config.yaml`)
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
cors:
|
||||
allowed_origins: ["*"]
|
||||
|
||||
database:
|
||||
host: localhost
|
||||
port: 5432
|
||||
name: chat_switchboard
|
||||
user: postgres
|
||||
password: ${DB_PASSWORD}
|
||||
|
||||
providers:
|
||||
openai:
|
||||
endpoint: https://api.openai.com/v1
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
anthropic:
|
||||
endpoint: https://api.anthropic.com/v1
|
||||
api_key: ${ANTHROPIC_API_KEY}
|
||||
ollama:
|
||||
endpoint: http://localhost:11434
|
||||
|
||||
router:
|
||||
default_provider: openai
|
||||
fallback_chain:
|
||||
- openai
|
||||
- anthropic
|
||||
- ollama
|
||||
model_mapping:
|
||||
gpt-4: openai
|
||||
claude-3-opus: anthropic
|
||||
llama2: ollama
|
||||
|
||||
extensions:
|
||||
enabled:
|
||||
- logging
|
||||
- cost_tracking
|
||||
- rate_limiting
|
||||
logging:
|
||||
level: info
|
||||
cost_tracking:
|
||||
pricing:
|
||||
gpt-4: 0.03
|
||||
gpt-3.5-turbo: 0.002
|
||||
claude-3-opus: 0.015
|
||||
rate_limiting:
|
||||
requests_per_minute: 60
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
1. **Implement provider adapters** (`server/providers/`)
|
||||
2. **Build tool registry** (`server/tools/`)
|
||||
3. **Create extension manager** (`server/extensions/`)
|
||||
4. **Implement database layer** (`server/models/`)
|
||||
5. **Add authentication/JWT** (`server/middleware/`)
|
||||
6. **Build admin panel** for extension management
|
||||
7. **Create Docker setup** for easy deployment
|
||||
|
||||
---
|
||||
|
||||
**This backend architecture gives you a production-ready, infinitely extensible multi-model LLM router with function calling.**
|
||||
@@ -1,235 +0,0 @@
|
||||
# CI/CD Infrastructure Setup - Chat Switchboard
|
||||
|
||||
This document outlines the CI/CD infrastructure that has been set up for the Chat Switchboard project.
|
||||
|
||||
## Overview
|
||||
|
||||
The project uses Gitea Actions for CI/CD, configured with three primary workflows:
|
||||
|
||||
1. **Backend CI** - Go backend testing, linting, and building
|
||||
2. **Frontend CI** - JavaScript/CSS linting and standalone build
|
||||
3. **Docker CI** - Container image building and publishing
|
||||
|
||||
## Workflows
|
||||
|
||||
### Backend CI (`.gitea/workflows/backend.yml`)
|
||||
|
||||
**Triggers:**
|
||||
- Push to `server/**` or `go.mod` on `main` or `develop`
|
||||
- Pull requests targeting `main` or `develop`
|
||||
|
||||
**Jobs:**
|
||||
1. **test** - Runs Go tests with coverage
|
||||
2. **lint** - Runs golangci-lint
|
||||
3. **build** - Compiles Go binary (requires test & lint to pass)
|
||||
|
||||
**Features:**
|
||||
- Auto-initializes Go module if missing
|
||||
- Uploads coverage reports to Codecov
|
||||
- Builds with version tags from git
|
||||
- Caches Go modules for faster builds
|
||||
|
||||
### Frontend CI (`.gitea/workflows/frontend.yml`)
|
||||
|
||||
**Triggers:**
|
||||
- Push to `frontend/**`, `src/**`, or `build.sh` on `main` or `develop`
|
||||
- Pull requests targeting `main` or `develop`
|
||||
|
||||
**Jobs:**
|
||||
1. **lint** - ESLint for JavaScript, Prettier for CSS
|
||||
2. **build** - Generates standalone HTML via build.sh
|
||||
3. **validate** - Validates HTML structure
|
||||
|
||||
**Features:**
|
||||
- Auto-generates eslint config if missing
|
||||
- Validates build output structure
|
||||
- Uploads standalone build as artifact
|
||||
|
||||
### Docker CI (`.gitea/workflows/docker.yml`)
|
||||
|
||||
**Triggers:**
|
||||
- Push to `main` or `develop` with Dockerfile changes
|
||||
- Pull requests targeting `main` or `develop`
|
||||
- Tag creation (`v*` pattern)
|
||||
|
||||
**Jobs:**
|
||||
1. **build** - Builds and tests containers
|
||||
2. **metadata** - Generates image tags
|
||||
3. **push** - Pushes images to registry
|
||||
4. **manifest** - Creates multi-arch manifests (main branch only)
|
||||
|
||||
**Features:**
|
||||
- Multi-stage Docker builds
|
||||
- Cache from GitHub Actions cache
|
||||
- Auto-tagging based on git refs
|
||||
- Security headers in nginx config
|
||||
- Health checks for containers
|
||||
|
||||
## Docker Images
|
||||
|
||||
### Backend Image
|
||||
|
||||
**Location:** `server/Dockerfile`
|
||||
|
||||
**Build Process:**
|
||||
1. Multi-stage build (builder → runtime)
|
||||
2. Auto-generates version from git tags
|
||||
3. Non-root user for security
|
||||
4. Health check endpoint
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
docker build -t chat-switchboard-backend ./server
|
||||
docker run -p 8080:8080 chat-switchboard-backend
|
||||
```
|
||||
|
||||
### Frontend Image
|
||||
|
||||
**Location:** `Dockerfile.frontend`
|
||||
|
||||
**Build Process:**
|
||||
1. Builds standalone HTML in builder stage
|
||||
2. Serves via nginx in production stage
|
||||
3. Gzip compression enabled
|
||||
4. Security headers added
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
docker build -t chat-switchboard-frontend -f Dockerfile.frontend .
|
||||
docker run -p 80:80 chat-switchboard-frontend
|
||||
```
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
**Branches:**
|
||||
- `main` - Production branch
|
||||
- `develop` - Development branch (to be created)
|
||||
- Feature branches - Created from `develop`
|
||||
|
||||
**Workflow:**
|
||||
1. Create feature branch from `develop`
|
||||
2. Make changes and push
|
||||
3. Create PR to `develop`
|
||||
4. CI runs automatically on PR
|
||||
5. Merge to `develop` after review
|
||||
6. Create PR from `develop` to `main` for releases
|
||||
|
||||
## Getting Started
|
||||
|
||||
### For Developers
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://git.gobha.me/xcaliber/chat-switchboard.git
|
||||
cd chat-switchboard
|
||||
```
|
||||
|
||||
2. **Create feature branch**
|
||||
```bash
|
||||
git checkout -b feature/my-feature develop
|
||||
```
|
||||
|
||||
3. **Make changes and test locally**
|
||||
```bash
|
||||
# Backend
|
||||
cd server && go test ./...
|
||||
|
||||
# Frontend
|
||||
./build.sh
|
||||
```
|
||||
|
||||
4. **Push and create PR**
|
||||
```bash
|
||||
git push origin feature/my-feature
|
||||
# Create PR via web interface
|
||||
```
|
||||
|
||||
5. **CI will automatically run on your PR**
|
||||
|
||||
### For Maintainers
|
||||
|
||||
1. **Branch Protection (requires admin access)**
|
||||
- Go to Repository Settings → Branches
|
||||
- Add protection rule for `main` and `develop`
|
||||
- Require pull request reviews
|
||||
- Require status checks to pass
|
||||
- Select required CI checks
|
||||
|
||||
2. **Creating a Release**
|
||||
```bash
|
||||
# Create tag
|
||||
git tag -a v1.0.0 -m "Release v1.0.0"
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
This will:
|
||||
- Trigger Docker CI
|
||||
- Build and push images with version tag
|
||||
- Update `latest` tag on main branch
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Docker Registry
|
||||
|
||||
Images are pushed to GitHub Container Registry:
|
||||
- Backend: `ghcr.io/xcaliber/chat-switchboard-backend`
|
||||
- Frontend: `ghcr.io/xcaliber/chat-switchboard-frontend`
|
||||
|
||||
Authentication is handled via `GITHUB_TOKEN`.
|
||||
|
||||
## Monitoring & Logs
|
||||
|
||||
### Docker Health Checks
|
||||
|
||||
Both containers include health checks:
|
||||
- Backend: `/health` endpoint
|
||||
- Frontend: HTTP check on port 80
|
||||
|
||||
### CI Status
|
||||
|
||||
Check CI status in:
|
||||
- Gitea repository → Actions tab
|
||||
- Pull request checks section
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. Go module initialization fails**
|
||||
```bash
|
||||
cd server
|
||||
go mod init git.gobha.me/xcaliber/chat-switchboard
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
**2. Frontend build fails**
|
||||
```bash
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
```
|
||||
|
||||
**3. Docker build fails**
|
||||
```bash
|
||||
# Check if Dockerfile exists
|
||||
ls -la server/Dockerfile
|
||||
ls -la Dockerfile.frontend
|
||||
|
||||
# Build manually
|
||||
docker build -t test ./server
|
||||
```
|
||||
|
||||
### Viewing CI Logs
|
||||
|
||||
1. Go to Repository → Actions
|
||||
2. Select the workflow run
|
||||
3. View job logs for details
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements:
|
||||
- [ ] Semantic versioning automation
|
||||
- [ ] Automatic changelog generation
|
||||
- [ ] Integration tests in CI
|
||||
- [ ] Security scanning (Trivy, Snyk)
|
||||
- [ ] Performance benchmarks
|
||||
- [ ] Multi-arch builds (arm64/amd64)
|
||||
@@ -1,469 +0,0 @@
|
||||
# 🚀 Getting Started with Chat Switchboard
|
||||
|
||||
## What is Chat Switchboard?
|
||||
|
||||
Chat Switchboard is a **plugin-first, multi-model AI platform** that works in two modes:
|
||||
|
||||
- **🏠 Unmanaged Mode:** Runs entirely in your browser (like the current version)
|
||||
- **🌐 Managed Mode:** Full backend with collaboration, workflows, and advanced features
|
||||
|
||||
## Quick Start (Unmanaged Mode)
|
||||
|
||||
### 1. Download & Run
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://git.gobha.me/xcaliber/chat-switchboard.git
|
||||
cd chat-switchboard
|
||||
|
||||
# Build standalone version
|
||||
./build.sh
|
||||
|
||||
# Open in browser
|
||||
xdg-open standalone/index.html
|
||||
# or serve it
|
||||
python3 -m http.server 8080 --directory standalone
|
||||
```
|
||||
|
||||
### 2. Configure Your API
|
||||
|
||||
1. Click ⚙️ Settings
|
||||
2. Enter your API details:
|
||||
- **API Endpoint:** `https://api.openai.com/v1` (or OpenRouter, Venice.ai, etc.)
|
||||
- **API Key:** Your API key
|
||||
- **Model:** Select or type model name
|
||||
3. Click "Save Settings"
|
||||
|
||||
### 3. Start Chatting
|
||||
|
||||
- Type a message and press Enter
|
||||
- Switch models per-chat using the dropdown
|
||||
- Export conversations as Markdown/JSON
|
||||
|
||||
**That's it!** No backend needed, all data stays in your browser.
|
||||
|
||||
---
|
||||
|
||||
## Full Installation (Managed Mode)
|
||||
|
||||
For teams, collaboration, and advanced features like Workflows and Channels.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker & Docker Compose
|
||||
- PostgreSQL 15+
|
||||
- Go 1.21+ (for development)
|
||||
- Python 3.9+ (for AI extensions)
|
||||
|
||||
### 1. Clone & Configure
|
||||
|
||||
```bash
|
||||
git clone https://git.gobha.me/xcaliber/chat-switchboard.git
|
||||
cd chat-switchboard
|
||||
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
|
||||
# Edit configuration
|
||||
nano .env
|
||||
```
|
||||
|
||||
```bash
|
||||
# .env
|
||||
DATABASE_URL=postgres://user:pass@localhost:5432/chatswitch
|
||||
JWT_SECRET=your-secret-key-here
|
||||
PORT=8080
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
REDIS_URL=redis://localhost:6379
|
||||
```
|
||||
|
||||
### 2. Start Services
|
||||
|
||||
```bash
|
||||
# Start everything with Docker Compose
|
||||
docker-compose up -d
|
||||
|
||||
# Services:
|
||||
# - PostgreSQL (5432)
|
||||
# - Backend API (8080)
|
||||
# - Redis (6379)
|
||||
# - Frontend (3000)
|
||||
```
|
||||
|
||||
### 3. Run Migrations
|
||||
|
||||
```bash
|
||||
# Apply database schema
|
||||
docker-compose exec backend ./migrate up
|
||||
```
|
||||
|
||||
### 4. Create Admin User
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"email": "admin@example.com",
|
||||
"password": "secure-password",
|
||||
"role": "admin"
|
||||
}'
|
||||
```
|
||||
|
||||
### 5. Access Frontend
|
||||
|
||||
Open http://localhost:3000
|
||||
|
||||
1. Click "Connect to Backend"
|
||||
2. Enter backend URL: `http://localhost:8080`
|
||||
3. Login with your credentials
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Frontend (Vanilla JS) │
|
||||
│ - Manages UI state │
|
||||
│ - Switches modes (managed/local) │
|
||||
└─────────────┬───────────────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
[LocalStorage] [Backend API]
|
||||
│ │
|
||||
│ ┌─────────▼──────────┐
|
||||
│ │ Go Core │
|
||||
│ │ - Auth │
|
||||
│ │ - WebSocket │
|
||||
│ │ - Extension Mgr │
|
||||
│ └─────────┬──────────┘
|
||||
│ │
|
||||
│ ┌─────────┴──────────┐
|
||||
│ │ PostgreSQL │
|
||||
│ │ - Users, chats │
|
||||
│ │ - Channels, notes │
|
||||
│ │ - Vector search │
|
||||
│ └────────────────────┘
|
||||
│
|
||||
└─── Extensions (Python/Go) ───┐
|
||||
├─ Chat Engine │
|
||||
├─ RAG Engine │
|
||||
├─ Workflows │
|
||||
└─ Custom Tools │
|
||||
```
|
||||
|
||||
## Core Features
|
||||
|
||||
### 1. Chat (User → AI)
|
||||
|
||||
**Available in:** Both modes
|
||||
|
||||
- Multi-model support (OpenAI, Anthropic, Ollama, etc.)
|
||||
- Per-conversation model switching
|
||||
- Streaming responses
|
||||
- Message history
|
||||
- Export (Markdown, JSON, Plain Text)
|
||||
|
||||
**Managed mode additions:**
|
||||
- Auto-routing (smart model selection)
|
||||
- Cost tracking
|
||||
- Shared conversations
|
||||
- Server-side tool calling
|
||||
|
||||
### 2. Channels (User → User + AI)
|
||||
|
||||
**Available in:** Managed mode only
|
||||
|
||||
Multi-user chat rooms with AI participants:
|
||||
|
||||
```
|
||||
#general
|
||||
@alice: What do you think about this design?
|
||||
@claude: I'd suggest using a darker color scheme...
|
||||
@bob: Agreed! Also, @gpt4 can you review the code?
|
||||
@gpt4: I see a potential issue in line 42...
|
||||
```
|
||||
|
||||
- Public/private/DM channels
|
||||
- @mention users and AI models
|
||||
- Threaded conversations
|
||||
- Real-time updates (WebSocket)
|
||||
- Reactions and formatting
|
||||
|
||||
### 3. Notes & Knowledge Bases
|
||||
|
||||
**Available in:** Both modes (limited in unmanaged)
|
||||
|
||||
- Markdown notes with folders
|
||||
- Tagging and search
|
||||
- Linked notes (wiki-style)
|
||||
|
||||
**Managed mode additions:**
|
||||
- Shared notes
|
||||
- Knowledge base collections
|
||||
- RAG (vector search)
|
||||
- Semantic search with embeddings
|
||||
|
||||
### 4. Workflows 🌟 (UNIQUE FEATURE)
|
||||
|
||||
**Available in:** Managed mode only
|
||||
|
||||
Chain multiple AI models and tools into automated pipelines:
|
||||
|
||||
```
|
||||
[User Query] → [Web Search] → [GPT-4 Summarize]
|
||||
↓ ↓
|
||||
[Save to KB] [Claude Verify]
|
||||
↓
|
||||
[Generate Report]
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Research assistants
|
||||
- Code review pipelines
|
||||
- Content creation factories
|
||||
- Data processing
|
||||
- Multi-model consensus
|
||||
|
||||
See `docs/WORKFLOWS.md` for details.
|
||||
|
||||
## Extension System
|
||||
|
||||
### Frontend Extensions (Both Modes)
|
||||
|
||||
Simple JavaScript plugins that run in the browser:
|
||||
|
||||
```javascript
|
||||
// extensions/frontend/my-plugin/main.js
|
||||
window.ChatSwitchboard.registerExtension({
|
||||
name: 'my-plugin',
|
||||
hooks: {
|
||||
onMessageSend: (msg) => {
|
||||
console.log('Sending:', msg);
|
||||
return msg; // Can modify
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Backend Extensions (Managed Only)
|
||||
|
||||
Full-featured plugins in any language:
|
||||
|
||||
```python
|
||||
# extensions/backend/my-tool/main.py
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/tools/my_tool")
|
||||
async def my_tool(input: str):
|
||||
result = process(input)
|
||||
return {"result": result}
|
||||
```
|
||||
|
||||
See `docs/PLUGIN_SPEC.md` for complete guide.
|
||||
|
||||
## Development
|
||||
|
||||
### Frontend Development
|
||||
|
||||
```bash
|
||||
# Serve source files
|
||||
python3 -m http.server 8080 --directory src
|
||||
|
||||
# Build standalone
|
||||
./build.sh
|
||||
```
|
||||
|
||||
### Backend Development
|
||||
|
||||
```bash
|
||||
# Install Go dependencies
|
||||
go mod download
|
||||
|
||||
# Run backend
|
||||
go run server/main.go
|
||||
|
||||
# Or with air (hot reload)
|
||||
air
|
||||
```
|
||||
|
||||
### Extension Development
|
||||
|
||||
```bash
|
||||
# Create from template
|
||||
cp -r extensions/_template-python extensions/my-extension
|
||||
|
||||
# Edit files
|
||||
cd extensions/my-extension
|
||||
nano extension.json
|
||||
nano main.py
|
||||
|
||||
# Test locally
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Frontend Settings
|
||||
|
||||
Stored in localStorage (unmanaged) or user profile (managed):
|
||||
|
||||
- API endpoints and keys
|
||||
- Default model
|
||||
- Stream responses
|
||||
- System prompt
|
||||
- Temperature, max tokens, etc.
|
||||
|
||||
### Backend Configuration
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
server:
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
|
||||
database:
|
||||
url: ${DATABASE_URL}
|
||||
max_connections: 20
|
||||
|
||||
redis:
|
||||
url: ${REDIS_URL}
|
||||
enabled: true
|
||||
|
||||
extensions:
|
||||
directory: ./extensions/backend
|
||||
auto_load: true
|
||||
|
||||
security:
|
||||
jwt_secret: ${JWT_SECRET}
|
||||
jwt_expiry: 24h
|
||||
rate_limit: 100/minute
|
||||
```
|
||||
|
||||
## Switching Modes
|
||||
|
||||
### From Unmanaged → Managed
|
||||
|
||||
1. Deploy backend (see Full Installation above)
|
||||
2. In Settings, enter Backend URL
|
||||
3. Login/register
|
||||
4. Your local data will be **uploaded** on first sync
|
||||
|
||||
### From Managed → Unmanaged
|
||||
|
||||
1. Export your data (Settings → Export)
|
||||
2. Remove backend URL from settings
|
||||
3. Continue using locally
|
||||
|
||||
**Note:** Managed-only features (Channels, Workflows) won't work in unmanaged mode.
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Personal Use (Unmanaged)
|
||||
```
|
||||
Open standalone/index.html → Configure API → Chat
|
||||
```
|
||||
|
||||
### Team Collaboration (Managed)
|
||||
```
|
||||
Deploy backend → Create team channels → @mention AI models
|
||||
```
|
||||
|
||||
### Development (Contributing)
|
||||
```
|
||||
Fork repo → Create extension → Test locally → Submit PR
|
||||
```
|
||||
|
||||
### Self-Hosted (Privacy)
|
||||
```
|
||||
Deploy on your server → Configure domains → Invite team
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Frontend Issues
|
||||
|
||||
**"No settings found"**
|
||||
- Click Settings ⚙️ and configure API endpoint + key
|
||||
|
||||
**"API request failed"**
|
||||
- Check API endpoint URL (trailing slash?)
|
||||
- Verify API key is correct
|
||||
- Check browser console for errors
|
||||
|
||||
**"LocalStorage full"**
|
||||
- Export old chats
|
||||
- Delete unused chats
|
||||
- Clear browser data
|
||||
|
||||
### Backend Issues
|
||||
|
||||
**"Connection refused"**
|
||||
- Is backend running? `docker-compose ps`
|
||||
- Check firewall rules
|
||||
- Verify port 8080 is open
|
||||
|
||||
**"Database migration failed"**
|
||||
- Check PostgreSQL is running
|
||||
- Verify DATABASE_URL is correct
|
||||
- Run migrations manually: `./migrate up`
|
||||
|
||||
**"Extension not loading"**
|
||||
- Check extension.json is valid
|
||||
- Verify runtime is installed (python3, go, node)
|
||||
- Check logs: `docker-compose logs backend`
|
||||
|
||||
### WebSocket Issues
|
||||
|
||||
**"Real-time updates not working"**
|
||||
- Check WebSocket connection in browser console
|
||||
- Verify Redis is running (for multi-instance setups)
|
||||
- Check nginx WebSocket proxy config
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Explore Features:** Try Chat, Notes, and (if managed) Channels
|
||||
2. **Read Docs:**
|
||||
- `docs/ARCHITECTURE.md` - System design
|
||||
- `docs/WORKFLOWS.md` - Workflow system
|
||||
- `docs/PLUGIN_SPEC.md` - Extension development
|
||||
3. **Build Extensions:** Use templates in `extensions/`
|
||||
4. **Join Community:** Discord, GitHub Discussions
|
||||
5. **Contribute:** Submit PRs, report bugs, suggest features
|
||||
|
||||
## Resources
|
||||
|
||||
- **GitHub:** https://git.gobha.me/xcaliber/chat-switchboard
|
||||
- **Documentation:** `/docs` directory
|
||||
- **Examples:** `/examples` directory
|
||||
- **Extensions:** `/extensions` directory
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Is my data private?**
|
||||
A: In unmanaged mode, everything stays in your browser. In managed mode, you control the backend.
|
||||
|
||||
**Q: Can I use my own models?**
|
||||
A: Yes! Configure any OpenAI-compatible API (Ollama, LM Studio, etc.)
|
||||
|
||||
**Q: How do I migrate from ChatGPT/Claude?**
|
||||
A: Export your conversations, then import via our import tool (coming soon).
|
||||
|
||||
**Q: Can I run this offline?**
|
||||
A: Unmanaged mode works offline if you pre-load the page. For LLM calls, you need internet or local models (Ollama).
|
||||
|
||||
**Q: How much does it cost?**
|
||||
A: Chat Switchboard is free and open-source. You pay for API usage (OpenAI, etc.) or use free models (Ollama).
|
||||
|
||||
**Q: Can I sell extensions?**
|
||||
A: Yes! The marketplace (coming soon) will support paid extensions.
|
||||
|
||||
---
|
||||
|
||||
**Ready to switch? Start building! 🚀**
|
||||
@@ -1,445 +0,0 @@
|
||||
# 🎨 HTML/Frontend Extensions Architecture
|
||||
|
||||
## Overview
|
||||
This document defines the **client-side extension system** for Chat Switchboard. Extensions are HTML/JS/CSS modules that can hook into the application lifecycle, add UI elements, intercept messages, and extend functionality.
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Extension Interface
|
||||
|
||||
### Extension Manifest Structure
|
||||
```javascript
|
||||
{
|
||||
"id": "example-extension",
|
||||
"name": "Example Extension",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"description": "Does something cool",
|
||||
"type": "frontend", // or "hybrid" if it has backend components
|
||||
"permissions": ["ui", "messages", "storage"],
|
||||
"entrypoint": "extension.js",
|
||||
"styles": "extension.css",
|
||||
"hooks": {
|
||||
"onLoad": true,
|
||||
"onMessageSend": true,
|
||||
"onMessageReceive": true,
|
||||
"onModelSwitch": true
|
||||
},
|
||||
"ui": {
|
||||
"toolbar": true,
|
||||
"sidebar": false,
|
||||
"settings": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Extension API
|
||||
|
||||
### Core Extension Class
|
||||
```javascript
|
||||
class ChatSwitchboardExtension {
|
||||
constructor(manifest) {
|
||||
this.manifest = manifest;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
// Lifecycle hooks
|
||||
onLoad() {}
|
||||
onUnload() {}
|
||||
onEnable() {}
|
||||
onDisable() {}
|
||||
|
||||
// Message hooks
|
||||
async onMessageSend(message, context) {
|
||||
// Modify message before sending
|
||||
// return modified message or null to cancel
|
||||
return message;
|
||||
}
|
||||
|
||||
async onMessageReceive(message, context) {
|
||||
// Process received message
|
||||
// return modified message or original
|
||||
return message;
|
||||
}
|
||||
|
||||
async onMessageDisplay(message, element) {
|
||||
// Modify message DOM before display
|
||||
return element;
|
||||
}
|
||||
|
||||
// UI hooks
|
||||
onModelSwitch(oldModel, newModel) {}
|
||||
onChatSwitch(oldChatId, newChatId) {}
|
||||
onSettingsOpen() {}
|
||||
|
||||
// Render hooks
|
||||
renderToolbarButton() {
|
||||
// Return HTML string or DOM element
|
||||
return null;
|
||||
}
|
||||
|
||||
renderSidebarPanel() {
|
||||
return null;
|
||||
}
|
||||
|
||||
renderSettingsPanel() {
|
||||
return null;
|
||||
}
|
||||
|
||||
renderMessageAction(message) {
|
||||
// Add custom actions to message bubbles
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Extension Categories
|
||||
|
||||
### 1. **UI Extensions**
|
||||
Add visual elements and interface enhancements.
|
||||
|
||||
**Examples:**
|
||||
- Syntax highlighter themes
|
||||
- Message templates/snippets
|
||||
- Custom emoji pickers
|
||||
- Voice input buttons
|
||||
- Image/file attachments
|
||||
|
||||
**Hooks:** `renderToolbarButton`, `renderSidebarPanel`, `renderMessageAction`
|
||||
|
||||
---
|
||||
|
||||
### 2. **Message Processors**
|
||||
Transform messages before/after sending.
|
||||
|
||||
**Examples:**
|
||||
- Markdown preprocessor
|
||||
- Code formatter
|
||||
- Translation layer
|
||||
- Prompt templates
|
||||
- Token counter
|
||||
|
||||
**Hooks:** `onMessageSend`, `onMessageReceive`, `onMessageDisplay`
|
||||
|
||||
---
|
||||
|
||||
### 3. **Model Extensions**
|
||||
Enhance model selection and routing.
|
||||
|
||||
**Examples:**
|
||||
- Model performance stats
|
||||
- Cost calculator
|
||||
- Context window manager
|
||||
- Model recommendation engine
|
||||
- Fallback routing (if model fails, try another)
|
||||
|
||||
**Hooks:** `onModelSwitch`, access to `State.settings.model`
|
||||
|
||||
---
|
||||
|
||||
### 4. **Storage Extensions**
|
||||
Extend data persistence capabilities.
|
||||
|
||||
**Examples:**
|
||||
- Cloud sync (S3, Dropbox)
|
||||
- Export formats (PDF, DOCX)
|
||||
- Search indexing
|
||||
- Tagging system
|
||||
- Chat organization
|
||||
|
||||
**Permissions:** `storage`
|
||||
|
||||
---
|
||||
|
||||
### 5. **Tool/Function Extensions**
|
||||
Enable LLM function calling (critical for your use case).
|
||||
|
||||
**Examples:**
|
||||
- Web search tool
|
||||
- Calculator
|
||||
- Code execution sandbox
|
||||
- API caller
|
||||
- File system access
|
||||
|
||||
**Hooks:** `onFunctionCall`, `registerFunction`
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Extension Manager
|
||||
|
||||
### Core API
|
||||
```javascript
|
||||
// Global extension registry
|
||||
window.ExtensionManager = {
|
||||
extensions: new Map(),
|
||||
|
||||
// Register an extension
|
||||
register(manifest, ExtensionClass) {
|
||||
const ext = new ExtensionClass(manifest);
|
||||
this.extensions.set(manifest.id, ext);
|
||||
ext.onLoad();
|
||||
return ext;
|
||||
},
|
||||
|
||||
// Enable/disable
|
||||
enable(id) {},
|
||||
disable(id) {},
|
||||
unload(id) {},
|
||||
|
||||
// Hook execution
|
||||
async executeHook(hookName, ...args) {
|
||||
const results = [];
|
||||
for (const [id, ext] of this.extensions) {
|
||||
if (ext.enabled && ext[hookName]) {
|
||||
const result = await ext[hookName](...args);
|
||||
results.push({ id, result });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
// Get all extensions with specific capability
|
||||
getByPermission(permission) {
|
||||
return Array.from(this.extensions.values())
|
||||
.filter(ext => ext.manifest.permissions.includes(permission));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📂 Extension File Structure
|
||||
|
||||
```
|
||||
extensions/
|
||||
├── example-extension/
|
||||
│ ├── manifest.json
|
||||
│ ├── extension.js
|
||||
│ ├── extension.css (optional)
|
||||
│ ├── README.md
|
||||
│ └── assets/
|
||||
│ └── icon.svg
|
||||
```
|
||||
|
||||
### Loading Extensions
|
||||
```javascript
|
||||
async function loadExtension(path) {
|
||||
const manifest = await fetch(`${path}/manifest.json`).then(r => r.json());
|
||||
|
||||
// Load JS
|
||||
const module = await import(`${path}/${manifest.entrypoint}`);
|
||||
|
||||
// Load CSS if present
|
||||
if (manifest.styles) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = `${path}/${manifest.styles}`;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
// Register
|
||||
ExtensionManager.register(manifest, module.default);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security & Permissions
|
||||
|
||||
### Permission System
|
||||
```javascript
|
||||
const PERMISSIONS = {
|
||||
ui: "Modify user interface",
|
||||
messages: "Read and modify messages",
|
||||
storage: "Access local storage",
|
||||
network: "Make network requests",
|
||||
settings: "Access user settings",
|
||||
clipboard: "Access clipboard",
|
||||
notifications: "Show notifications"
|
||||
};
|
||||
```
|
||||
|
||||
### Sandboxing (Future)
|
||||
- Use iframes for untrusted extensions
|
||||
- Content Security Policy restrictions
|
||||
- API key scoping (extensions can't access main API key)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Example Extensions
|
||||
|
||||
### 1. Token Counter
|
||||
```javascript
|
||||
class TokenCounterExtension extends ChatSwitchboardExtension {
|
||||
onLoad() {
|
||||
this.tokenCount = 0;
|
||||
}
|
||||
|
||||
renderToolbarButton() {
|
||||
return `
|
||||
<button class="btn btn-small" id="tokenCounter">
|
||||
Tokens: <span id="tokenCount">0</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
async onMessageSend(message, context) {
|
||||
// Rough estimation: 1 token ≈ 4 chars
|
||||
this.tokenCount = Math.ceil(message.content.length / 4);
|
||||
document.getElementById('tokenCount').textContent = this.tokenCount;
|
||||
return message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Code Formatter
|
||||
```javascript
|
||||
class CodeFormatterExtension extends ChatSwitchboardExtension {
|
||||
async onMessageDisplay(message, element) {
|
||||
if (message.role === 'assistant') {
|
||||
// Find code blocks and apply Prism.js highlighting
|
||||
element.querySelectorAll('pre code').forEach(block => {
|
||||
Prism.highlightElement(block);
|
||||
});
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
renderMessageAction(message) {
|
||||
if (message.content.includes('```')) {
|
||||
return `
|
||||
<button class="btn btn-small" onclick="copyAllCode()">
|
||||
📋 Copy All Code
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Model Router (Smart Fallback)
|
||||
```javascript
|
||||
class ModelRouterExtension extends ChatSwitchboardExtension {
|
||||
async onMessageSend(message, context) {
|
||||
// If message is long, use high-context model
|
||||
if (message.content.length > 5000) {
|
||||
const originalModel = State.settings.model;
|
||||
State.settings.model = 'claude-3-opus-20240229'; // High context
|
||||
|
||||
// Restore after response
|
||||
context.onComplete = () => {
|
||||
State.settings.model = originalModel;
|
||||
};
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Integration with App
|
||||
|
||||
### Modify `app.js` to support hooks:
|
||||
```javascript
|
||||
async function sendMessage() {
|
||||
// ... existing code ...
|
||||
|
||||
// Execute hook before sending
|
||||
const hookResults = await ExtensionManager.executeHook(
|
||||
'onMessageSend',
|
||||
message,
|
||||
{ chatId: State.currentChatId }
|
||||
);
|
||||
|
||||
// Check if any extension cancelled the send
|
||||
if (hookResults.some(r => r.result === null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
for (const { result } of hookResults) {
|
||||
if (result && result !== message) {
|
||||
message = result;
|
||||
}
|
||||
}
|
||||
|
||||
// ... continue with API call ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Extension Discovery
|
||||
|
||||
### Extension Store (Future)
|
||||
```javascript
|
||||
{
|
||||
"extensions": [
|
||||
{
|
||||
"id": "web-search",
|
||||
"name": "Web Search Tool",
|
||||
"description": "Adds web search capability via function calling",
|
||||
"author": "Chat Switchboard Team",
|
||||
"version": "1.0.0",
|
||||
"downloadUrl": "https://extensions.switchboard/web-search.zip",
|
||||
"verified": true,
|
||||
"rating": 4.8,
|
||||
"downloads": 1250
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
1. **Implement ExtensionManager** in `src/js/extensions.js`
|
||||
2. **Add hook points** to existing code (app.js, ui.js, api.js)
|
||||
3. **Create example extensions** to validate API
|
||||
4. **Build extension settings UI** for enable/disable/configure
|
||||
5. **Document extension development** with starter template
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Extension Development Workflow
|
||||
|
||||
```bash
|
||||
# Create new extension
|
||||
mkdir extensions/my-extension
|
||||
cd extensions/my-extension
|
||||
|
||||
# Generate manifest
|
||||
cat > manifest.json << EOF
|
||||
{
|
||||
"id": "my-extension",
|
||||
"name": "My Extension",
|
||||
"version": "1.0.0",
|
||||
"entrypoint": "extension.js",
|
||||
"permissions": ["messages"]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create extension class
|
||||
cat > extension.js << EOF
|
||||
class MyExtension extends ChatSwitchboardExtension {
|
||||
onLoad() {
|
||||
console.log('My extension loaded!');
|
||||
}
|
||||
}
|
||||
export default MyExtension;
|
||||
EOF
|
||||
|
||||
# Test in dev mode
|
||||
# Extensions auto-load from /extensions/ directory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**This architecture makes Chat Switchboard infinitely extensible while keeping the core lean.**
|
||||
@@ -1,759 +0,0 @@
|
||||
# 🔌 Plugin Specification - Extension Development Guide
|
||||
|
||||
## Philosophy: Everything is a Plugin
|
||||
|
||||
**Core Principle:** If core features (Chat, Channels, Notes) are implemented as plugins, we prove that ANY feature can be a plugin.
|
||||
|
||||
```
|
||||
Minimal Core (Go) Extensions (Any Language)
|
||||
├── HTTP Router ├── Chat Engine (Python)
|
||||
├── WebSocket Hub ├── Channels (Go)
|
||||
├── Extension Manager ├── RAG Engine (Python)
|
||||
├── Auth/Users ├── Workflows (Go + Python)
|
||||
└── PostgreSQL └── Your Custom Plugin...
|
||||
```
|
||||
|
||||
## Plugin Types
|
||||
|
||||
### 1. Frontend Plugins (UI Only)
|
||||
**Language:** JavaScript
|
||||
**Runs:** In browser
|
||||
**Capabilities:** UI modifications, client-side logic
|
||||
**Modes:** Both managed and unmanaged
|
||||
|
||||
### 2. Backend Plugins (Full Power)
|
||||
**Language:** Python, Go, Node.js, Rust, etc.
|
||||
**Runs:** As separate process
|
||||
**Capabilities:** Tools, models, data processing, external APIs
|
||||
**Modes:** Managed only
|
||||
|
||||
## Frontend Plugin API
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
extensions/frontend/token-counter/
|
||||
├── manifest.json # Plugin metadata
|
||||
├── main.js # Entry point
|
||||
├── styles.css # Optional styles
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### manifest.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "token-counter",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"description": "Display token count for messages",
|
||||
"type": "frontend",
|
||||
"entry": "main.js",
|
||||
"permissions": [
|
||||
"message:read",
|
||||
"ui:inject"
|
||||
],
|
||||
"hooks": [
|
||||
"onMessageSend",
|
||||
"onMessageReceive",
|
||||
"onUIRender"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### main.js Example
|
||||
|
||||
```javascript
|
||||
// Frontend plugin template
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Plugin initialization
|
||||
window.ChatSwitchboard = window.ChatSwitchboard || {};
|
||||
window.ChatSwitchboard.plugins = window.ChatSwitchboard.plugins || [];
|
||||
|
||||
const TokenCounter = {
|
||||
name: 'token-counter',
|
||||
version: '1.0.0',
|
||||
|
||||
// Called when plugin loads
|
||||
init: function() {
|
||||
console.log('Token Counter plugin loaded');
|
||||
this.addUI();
|
||||
},
|
||||
|
||||
// Hook: Before message is sent
|
||||
hooks: {
|
||||
onMessageSend: function(message) {
|
||||
const tokens = this.estimateTokens(message.content);
|
||||
console.log(`Message has ~${tokens} tokens`);
|
||||
|
||||
// Show warning if too long
|
||||
if (tokens > 4000) {
|
||||
window.ChatSwitchboard.showToast(
|
||||
`⚠️ Long message: ${tokens} tokens`,
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
// Can modify message before sending
|
||||
return message;
|
||||
},
|
||||
|
||||
onMessageReceive: function(message) {
|
||||
// Process incoming messages
|
||||
return message;
|
||||
},
|
||||
|
||||
onUIRender: function() {
|
||||
// Inject UI elements
|
||||
this.updateTokenDisplay();
|
||||
}
|
||||
},
|
||||
|
||||
// Add token counter to UI
|
||||
addUI: function() {
|
||||
const inputArea = document.querySelector('.input-area');
|
||||
const counter = document.createElement('div');
|
||||
counter.id = 'token-counter';
|
||||
counter.className = 'plugin-token-counter';
|
||||
counter.textContent = '0 tokens';
|
||||
inputArea.appendChild(counter);
|
||||
|
||||
// Update on input
|
||||
const textarea = document.getElementById('messageInput');
|
||||
textarea.addEventListener('input', () => {
|
||||
const tokens = this.estimateTokens(textarea.value);
|
||||
counter.textContent = `${tokens} tokens`;
|
||||
});
|
||||
},
|
||||
|
||||
// Token estimation (rough approximation)
|
||||
estimateTokens: function(text) {
|
||||
return Math.ceil(text.length / 4);
|
||||
},
|
||||
|
||||
updateTokenDisplay: function() {
|
||||
const counter = document.getElementById('token-counter');
|
||||
const input = document.getElementById('messageInput');
|
||||
if (counter && input) {
|
||||
const tokens = this.estimateTokens(input.value);
|
||||
counter.textContent = `${tokens} tokens`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Register plugin
|
||||
window.ChatSwitchboard.plugins.push(TokenCounter);
|
||||
|
||||
// Auto-init when DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => TokenCounter.init());
|
||||
} else {
|
||||
TokenCounter.init();
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
### Available Hooks
|
||||
|
||||
```javascript
|
||||
// Message lifecycle
|
||||
onMessageSend(message) // Before sending to API
|
||||
onMessageReceive(message) // After receiving from API
|
||||
onMessageRender(element, message) // When rendering to DOM
|
||||
|
||||
// UI lifecycle
|
||||
onUIRender() // After UI updates
|
||||
onChatSwitch(chatId) // When switching chats
|
||||
onModelChange(model) // When model changes
|
||||
|
||||
// Extension points
|
||||
onToolCall(tool, params) // Before calling tool
|
||||
onToolResult(tool, result) // After tool returns
|
||||
|
||||
// Settings
|
||||
onSettingsOpen() // Settings modal opened
|
||||
onSettingsSave(settings) // Settings saved
|
||||
|
||||
// Files
|
||||
onFileUpload(file) // File uploaded
|
||||
onFileSelect(file) // File selected for chat
|
||||
```
|
||||
|
||||
### Plugin API Methods
|
||||
|
||||
```javascript
|
||||
// Toast notifications
|
||||
ChatSwitchboard.showToast(message, type); // type: success, error, warning, info
|
||||
|
||||
// Storage (scoped to plugin)
|
||||
ChatSwitchboard.storage.set(key, value);
|
||||
ChatSwitchboard.storage.get(key);
|
||||
ChatSwitchboard.storage.remove(key);
|
||||
|
||||
// UI manipulation
|
||||
ChatSwitchboard.ui.addButton(location, config);
|
||||
ChatSwitchboard.ui.addPanel(config);
|
||||
ChatSwitchboard.ui.addMenuItem(config);
|
||||
|
||||
// State access (read-only)
|
||||
ChatSwitchboard.state.getCurrentChat();
|
||||
ChatSwitchboard.state.getCurrentModel();
|
||||
ChatSwitchboard.state.getSettings();
|
||||
|
||||
// Events
|
||||
ChatSwitchboard.events.on(event, callback);
|
||||
ChatSwitchboard.events.off(event, callback);
|
||||
ChatSwitchboard.events.emit(event, data);
|
||||
```
|
||||
|
||||
## Backend Plugin API
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
extensions/backend/web-search/
|
||||
├── extension.json # Manifest
|
||||
├── main.py # Entry point (or main.go, server.js, etc)
|
||||
├── requirements.txt # Dependencies (Python)
|
||||
├── config.yaml # Configuration
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### extension.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "web-search",
|
||||
"version": "1.0.0",
|
||||
"author": "Chat Switchboard Team",
|
||||
"description": "Search the web using DuckDuckGo API",
|
||||
"type": "backend",
|
||||
"runtime": "python",
|
||||
"entry": "main.py",
|
||||
"port": 9001,
|
||||
"capabilities": ["tool"],
|
||||
"dependencies": {
|
||||
"python": ">=3.9",
|
||||
"packages": ["fastapi", "uvicorn", "duckduckgo-search"]
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"name": "search_web",
|
||||
"description": "Search the web for information",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query"
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"description": "Maximum results to return",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
},
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
"snippet": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"max_requests_per_minute": 30,
|
||||
"timeout_seconds": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python Plugin Template
|
||||
|
||||
```python
|
||||
# extensions/backend/web-search/main.py
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from duckduckgo_search import DDGS
|
||||
import uvicorn
|
||||
import os
|
||||
|
||||
app = FastAPI(title="Web Search Plugin")
|
||||
|
||||
# Load config
|
||||
PORT = int(os.getenv("PLUGIN_PORT", 9001))
|
||||
MAX_RESULTS = int(os.getenv("MAX_RESULTS", 5))
|
||||
|
||||
# Request/response models
|
||||
class SearchRequest(BaseModel):
|
||||
query: str
|
||||
max_results: int = 5
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
title: str
|
||||
url: str
|
||||
snippet: str
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
results: list[SearchResult]
|
||||
|
||||
# Tool endpoint
|
||||
@app.post("/tools/search_web", response_model=SearchResponse)
|
||||
async def search_web(request: SearchRequest):
|
||||
"""Search the web using DuckDuckGo"""
|
||||
try:
|
||||
ddgs = DDGS()
|
||||
results = ddgs.text(
|
||||
request.query,
|
||||
max_results=min(request.max_results, MAX_RESULTS)
|
||||
)
|
||||
|
||||
return SearchResponse(
|
||||
results=[
|
||||
SearchResult(
|
||||
title=r.get("title", ""),
|
||||
url=r.get("href", ""),
|
||||
snippet=r.get("body", "")
|
||||
)
|
||||
for r in results
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Health check
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "plugin": "web-search"}
|
||||
|
||||
# Plugin info
|
||||
@app.get("/info")
|
||||
async def info():
|
||||
return {
|
||||
"name": "web-search",
|
||||
"version": "1.0.0",
|
||||
"tools": ["search_web"]
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="127.0.0.1", port=PORT)
|
||||
```
|
||||
|
||||
### Go Plugin Template
|
||||
|
||||
```go
|
||||
// extensions/backend/file-ops/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type ReadFileRequest struct {
|
||||
Path string `json:"path" binding:"required"`
|
||||
}
|
||||
|
||||
type ReadFileResponse struct {
|
||||
Content string `json:"content"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// Tool endpoint
|
||||
r.POST("/tools/read_file", func(c *gin.Context) {
|
||||
var req ReadFileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(req.Path)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
info, _ := os.Stat(req.Path)
|
||||
|
||||
c.JSON(200, ReadFileResponse{
|
||||
Content: string(content),
|
||||
Size: info.Size(),
|
||||
})
|
||||
})
|
||||
|
||||
// Health check
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Start server
|
||||
port := os.Getenv("PLUGIN_PORT")
|
||||
if port == "" {
|
||||
port = "9002"
|
||||
}
|
||||
r.Run(":" + port)
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Manager (Backend Core)
|
||||
|
||||
### Go Extension Manager
|
||||
|
||||
```go
|
||||
// server/extensions/manager.go
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Runtime string `json:"runtime"`
|
||||
Entry string `json:"entry"`
|
||||
Port int `json:"port"`
|
||||
Tools []Tool `json:"tools"`
|
||||
Process *os.Process
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
extensions map[string]*Extension
|
||||
tools map[string]*Tool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
extensions: make(map[string]*Extension),
|
||||
tools: make(map[string]*Tool),
|
||||
}
|
||||
}
|
||||
|
||||
// Load all extensions from directory
|
||||
func (m *Manager) LoadExtensions(dir string) error {
|
||||
entries, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
extPath := filepath.Join(dir, entry.Name())
|
||||
if err := m.LoadExtension(extPath); err != nil {
|
||||
log.Printf("Failed to load extension %s: %v", entry.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load single extension
|
||||
func (m *Manager) LoadExtension(path string) error {
|
||||
// Read manifest
|
||||
manifestPath := filepath.Join(path, "extension.json")
|
||||
data, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ext Extension
|
||||
if err := json.Unmarshal(data, &ext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start extension process
|
||||
if err := m.startExtension(&ext, path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Register tools
|
||||
for i := range ext.Tools {
|
||||
ext.Tools[i].ExtensionName = ext.Name
|
||||
m.tools[ext.Tools[i].Name] = &ext.Tools[i]
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.extensions[ext.Name] = &ext
|
||||
m.mu.Unlock()
|
||||
|
||||
log.Printf("✅ Loaded extension: %s v%s", ext.Name, ext.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start extension process
|
||||
func (m *Manager) startExtension(ext *Extension, path string) error {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch ext.Runtime {
|
||||
case "python":
|
||||
cmd = exec.Command("python3", ext.Entry)
|
||||
case "go":
|
||||
cmd = exec.Command("go", "run", ext.Entry)
|
||||
case "node":
|
||||
cmd = exec.Command("node", ext.Entry)
|
||||
default:
|
||||
return fmt.Errorf("unsupported runtime: %s", ext.Runtime)
|
||||
}
|
||||
|
||||
cmd.Dir = path
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PLUGIN_PORT=%d", ext.Port))
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ext.Process = cmd.Process
|
||||
ext.BaseURL = fmt.Sprintf("http://localhost:%d", ext.Port)
|
||||
|
||||
// Wait for extension to be ready
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Call tool
|
||||
func (m *Manager) CallTool(name string, params map[string]interface{}) (interface{}, error) {
|
||||
m.mu.RLock()
|
||||
tool, exists := m.tools[name]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("tool not found: %s", name)
|
||||
}
|
||||
|
||||
ext := m.extensions[tool.ExtensionName]
|
||||
url := fmt.Sprintf("%s/tools/%s", ext.BaseURL, name)
|
||||
|
||||
// HTTP POST to extension
|
||||
body, _ := json.Marshal(params)
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// List all tools
|
||||
func (m *Manager) ListTools() []Tool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
tools := make([]Tool, 0, len(m.tools))
|
||||
for _, tool := range m.tools {
|
||||
tools = append(tools, *tool)
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
// Shutdown all extensions
|
||||
func (m *Manager) Shutdown() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for _, ext := range m.extensions {
|
||||
if ext.Process != nil {
|
||||
ext.Process.Kill()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extension Discovery
|
||||
|
||||
```go
|
||||
// server/main.go
|
||||
func main() {
|
||||
// ... existing setup ...
|
||||
|
||||
// Load extensions
|
||||
extManager := extensions.NewManager()
|
||||
if err := extManager.LoadExtensions("./extensions/backend"); err != nil {
|
||||
log.Fatal("Failed to load extensions:", err)
|
||||
}
|
||||
defer extManager.Shutdown()
|
||||
|
||||
// Expose extension tools via API
|
||||
r.GET("/api/tools", func(c *gin.Context) {
|
||||
tools := extManager.ListTools()
|
||||
c.JSON(200, tools)
|
||||
})
|
||||
|
||||
r.POST("/api/tools/:name", func(c *gin.Context) {
|
||||
toolName := c.Param("name")
|
||||
var params map[string]interface{}
|
||||
c.BindJSON(¶ms)
|
||||
|
||||
result, err := extManager.CallTool(toolName, params)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, result)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Development Workflow
|
||||
|
||||
### 1. Create from Template
|
||||
|
||||
```bash
|
||||
# Frontend plugin
|
||||
cp -r extensions/frontend/_template extensions/frontend/my-plugin
|
||||
cd extensions/frontend/my-plugin
|
||||
# Edit manifest.json, main.js
|
||||
|
||||
# Backend plugin (Python)
|
||||
cp -r extensions/backend/_template-python extensions/backend/my-plugin
|
||||
cd extensions/backend/my-plugin
|
||||
# Edit extension.json, main.py
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Test Locally
|
||||
|
||||
```bash
|
||||
# Backend plugin
|
||||
python main.py # Runs on port from extension.json
|
||||
|
||||
# Test tool
|
||||
curl -X POST http://localhost:9001/tools/my_tool \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"param": "value"}'
|
||||
```
|
||||
|
||||
### 3. Install in Chat Switchboard
|
||||
|
||||
```bash
|
||||
# Move to extensions directory
|
||||
mv my-plugin ../chat-switchboard/extensions/backend/
|
||||
|
||||
# Restart backend
|
||||
cd ../chat-switchboard
|
||||
docker-compose restart backend
|
||||
```
|
||||
|
||||
### 4. Publish to Marketplace
|
||||
|
||||
```bash
|
||||
# Package extension
|
||||
tar -czf my-plugin-1.0.0.tar.gz my-plugin/
|
||||
|
||||
# Upload to marketplace
|
||||
curl -X POST https://marketplace.chatswitch.io/api/publish \
|
||||
-F "file=@my-plugin-1.0.0.tar.gz" \
|
||||
-F "category=tools" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## Security & Sandboxing
|
||||
|
||||
### Backend Plugin Isolation
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml - Run plugins in containers
|
||||
services:
|
||||
plugin-web-search:
|
||||
build: ./extensions/backend/web-search
|
||||
ports:
|
||||
- "9001:9001"
|
||||
environment:
|
||||
- PLUGIN_PORT=9001
|
||||
networks:
|
||||
- extension-network
|
||||
restart: always
|
||||
```
|
||||
|
||||
### Permission System
|
||||
|
||||
```json
|
||||
// Plugins declare required permissions
|
||||
{
|
||||
"permissions": [
|
||||
"network:outbound", // Make external HTTP requests
|
||||
"storage:read", // Read from DB
|
||||
"storage:write", // Write to DB
|
||||
"file:read", // Read files
|
||||
"tool:call:*" // Call other tools
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Official Plugin List
|
||||
|
||||
### Bundled with Core
|
||||
|
||||
1. **chat-engine** (Python) - LLM conversation handling
|
||||
2. **channels** (Go) - Multi-user chat rooms
|
||||
3. **rag-engine** (Python) - Vector search, embeddings
|
||||
4. **workflows** (Go/Python) - Workflow orchestration
|
||||
5. **notes** (Go) - Note management
|
||||
|
||||
### Official Extensions
|
||||
|
||||
6. **web-search** (Python) - DuckDuckGo search
|
||||
7. **code-runner** (Python) - Sandboxed code execution
|
||||
8. **image-gen** (Python) - DALL-E/Stable Diffusion
|
||||
9. **file-ops** (Go) - File system operations
|
||||
10. **calculator** (Python) - Math evaluation
|
||||
|
||||
### Community Extensions (Example Ideas)
|
||||
|
||||
- **slack-integration** - Post to Slack channels
|
||||
- **github-integration** - Create issues, PRs
|
||||
- **email-sender** - Send emails via SMTP
|
||||
- **pdf-parser** - Extract text from PDFs
|
||||
- **scraper** - Web scraping tool
|
||||
- **translate** - Multi-language translation
|
||||
- **tts** - Text-to-speech
|
||||
- **stt** - Speech-to-text
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Read `docs/ARCHITECTURE.md` for system overview
|
||||
2. Check `extensions/_template-python/` for starter code
|
||||
3. Browse `extensions/backend/` for examples
|
||||
4. Join developer Discord for help
|
||||
|
||||
**Build something awesome! 🚀**
|
||||
@@ -1,360 +0,0 @@
|
||||
# 🚀 Quick Start for Developers
|
||||
|
||||
Get up and running with Chat Switchboard development in under 10 minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Go 1.21+** - Backend
|
||||
- **Python 3.11+** - Extensions
|
||||
- **PostgreSQL 15+** - Database
|
||||
- **Redis 7+** - WebSocket scaling
|
||||
- **Node.js 18+** (optional) - For frontend tooling
|
||||
- **Docker** (optional) - For containerized development
|
||||
|
||||
## 🏃 Quick Setup
|
||||
|
||||
### 1. Clone and Enter
|
||||
```bash
|
||||
git clone https://git.gobha.me/xcaliber/chat-switchboard.git
|
||||
cd chat-switchboard
|
||||
```
|
||||
|
||||
### 2. Start Services (Docker)
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
This starts:
|
||||
- PostgreSQL on `localhost:5432`
|
||||
- Redis on `localhost:6379`
|
||||
|
||||
### 3. Backend Setup
|
||||
```bash
|
||||
cd server
|
||||
go mod download
|
||||
cp .env.example .env
|
||||
# Edit .env with database credentials
|
||||
|
||||
# Run migrations
|
||||
go run cmd/migrate/main.go up
|
||||
|
||||
# Start server
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Backend runs on `http://localhost:8080`
|
||||
|
||||
### 4. Frontend Setup
|
||||
```bash
|
||||
cd src
|
||||
python3 -m http.server 3000
|
||||
```
|
||||
|
||||
Frontend runs on `http://localhost:3000`
|
||||
|
||||
### 5. Open Browser
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
Configure API settings to point to `http://localhost:8080`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Your First Contribution
|
||||
|
||||
### Step 1: Pick an Issue
|
||||
Browse [issues](https://git.gobha.me/xcaliber/chat-switchboard/issues) and pick one labeled:
|
||||
- 🟡 **LOW** priority
|
||||
- `good-first-issue`
|
||||
|
||||
### Step 2: Create Branch
|
||||
```bash
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b issue-XX-description
|
||||
```
|
||||
|
||||
### Step 3: Make Changes
|
||||
Edit files, test locally:
|
||||
```bash
|
||||
# Backend tests
|
||||
cd server
|
||||
go test ./...
|
||||
|
||||
# Frontend lint (if ESLint configured)
|
||||
cd src
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Step 4: Commit and Push
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "[Category] Description (refs #XX)"
|
||||
git push origin issue-XX-description
|
||||
```
|
||||
|
||||
### Step 5: Open PR
|
||||
Go to GitHub/Gitea and open PR:
|
||||
- **Base:** `develop`
|
||||
- **Title:** `[Category] Description (closes #XX)`
|
||||
- Fill in PR template
|
||||
|
||||
### Step 6: Wait for Review
|
||||
Maintainers will review and provide feedback.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Development Commands
|
||||
|
||||
### Backend
|
||||
```bash
|
||||
# Run server
|
||||
go run main.go
|
||||
|
||||
# Run tests
|
||||
go test ./...
|
||||
|
||||
# Run tests with coverage
|
||||
go test -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# Lint
|
||||
golangci-lint run
|
||||
|
||||
# Build
|
||||
go build -o bin/server main.go
|
||||
|
||||
# Run migrations
|
||||
go run cmd/migrate/main.go up
|
||||
go run cmd/migrate/main.go down
|
||||
```
|
||||
|
||||
### Frontend
|
||||
```bash
|
||||
# Serve for development
|
||||
python3 -m http.server 3000
|
||||
|
||||
# Build standalone
|
||||
./build.sh
|
||||
|
||||
# Lint (if configured)
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
# Start all services
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Restart a service
|
||||
docker-compose restart backend
|
||||
|
||||
# Stop all
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure Quick Reference
|
||||
|
||||
```
|
||||
chat-switchboard/
|
||||
├── server/ # Go backend
|
||||
│ ├── main.go # Entry point
|
||||
│ ├── handlers/ # API handlers
|
||||
│ ├── middleware/ # Auth, CORS, etc.
|
||||
│ ├── models/ # Database models
|
||||
│ ├── extensions/ # Extension manager
|
||||
│ ├── websocket/ # WebSocket hub
|
||||
│ └── workflows/ # Workflow engine
|
||||
├── src/ # Frontend
|
||||
│ ├── index.html
|
||||
│ ├── js/
|
||||
│ │ ├── app.js # Main app logic
|
||||
│ │ ├── state.js # State management
|
||||
│ │ ├── api.js # API calls
|
||||
│ │ └── ui.js # UI rendering
|
||||
│ └── css/
|
||||
├── extensions/ # Backend extensions
|
||||
│ ├── _template-python/
|
||||
│ ├── chat-engine/
|
||||
│ ├── web-search/
|
||||
│ └── calculator/
|
||||
├── migrations/ # Database migrations
|
||||
├── docs/ # Documentation
|
||||
│ ├── ARCHITECTURE.md
|
||||
│ ├── WORKFLOWS.md
|
||||
│ ├── PLUGIN_SPEC.md
|
||||
│ └── GETTING_STARTED.md
|
||||
├── ROADMAP.md # Development roadmap
|
||||
└── ISSUES.md # Issue workflow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐍 Creating Your First Extension
|
||||
|
||||
### 1. Copy Template
|
||||
```bash
|
||||
cp -r extensions/_template-python extensions/my-extension
|
||||
cd extensions/my-extension
|
||||
```
|
||||
|
||||
### 2. Edit Manifest
|
||||
```json
|
||||
{
|
||||
"name": "my-extension",
|
||||
"version": "1.0.0",
|
||||
"runtime": "python",
|
||||
"entry": "main.py",
|
||||
"port": 9001,
|
||||
"tools": [
|
||||
{
|
||||
"name": "my_tool",
|
||||
"description": "Does something cool",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "string",
|
||||
"description": "Input text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Implement Tool
|
||||
```python
|
||||
# main.py
|
||||
from fastapi import FastAPI
|
||||
import uvicorn
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/tools/my_tool")
|
||||
async def my_tool(input: str):
|
||||
# Your logic here
|
||||
result = input.upper() # Example
|
||||
return {"result": result}
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="127.0.0.1", port=9001)
|
||||
```
|
||||
|
||||
### 4. Test
|
||||
```bash
|
||||
python main.py
|
||||
# In another terminal:
|
||||
curl -X POST http://localhost:9001/tools/my_tool \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"input": "hello"}'
|
||||
```
|
||||
|
||||
### 5. Register with Backend
|
||||
Backend auto-discovers extensions in `/extensions` folder on startup.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Running Tests
|
||||
|
||||
### Backend Unit Tests
|
||||
```bash
|
||||
cd server
|
||||
go test ./handlers -v
|
||||
go test ./extensions -v
|
||||
go test ./workflows -v
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
```bash
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Run tests
|
||||
go test ./tests/integration -v
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
```bash
|
||||
# Install Playwright (one-time)
|
||||
npm install -D @playwright/test
|
||||
npx playwright install
|
||||
|
||||
# Run E2E tests
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Debugging
|
||||
|
||||
### Backend Debugging
|
||||
```bash
|
||||
# Run with delve debugger
|
||||
dlv debug main.go
|
||||
|
||||
# Or use VS Code launch.json
|
||||
# F5 to start debugging
|
||||
```
|
||||
|
||||
### Frontend Debugging
|
||||
- Open browser DevTools (F12)
|
||||
- Check Console for errors
|
||||
- Use Network tab for API calls
|
||||
|
||||
### Extension Debugging
|
||||
```bash
|
||||
# Run extension standalone
|
||||
cd extensions/my-extension
|
||||
python main.py
|
||||
|
||||
# Check logs
|
||||
tail -f logs/extension.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Essential Reading
|
||||
|
||||
Before you start coding:
|
||||
|
||||
1. **[ARCHITECTURE.md](ARCHITECTURE.md)** - System design
|
||||
2. **[PLUGIN_SPEC.md](PLUGIN_SPEC.md)** - Extension development
|
||||
3. **[ISSUES.md](../ISSUES.md)** - Contribution workflow
|
||||
4. **[ROADMAP.md](../ROADMAP.md)** - Project direction
|
||||
|
||||
---
|
||||
|
||||
## 💬 Getting Help
|
||||
|
||||
- **Issues:** [GitHub Issues](https://git.gobha.me/xcaliber/chat-switchboard/issues)
|
||||
- **Discussions:** (Coming soon)
|
||||
- **Discord:** (Coming soon)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Pro Tips
|
||||
|
||||
1. **Use `develop` branch** - Never commit directly to `main`
|
||||
2. **Small PRs** - Easier to review, faster to merge
|
||||
3. **Test locally** - Don't rely on CI to catch errors
|
||||
4. **Ask questions** - Better to ask than assume
|
||||
5. **Read existing code** - Learn patterns from implemented features
|
||||
|
||||
---
|
||||
|
||||
## 🎉 You're Ready!
|
||||
|
||||
Pick an issue and start coding. Welcome to the team! 🚀
|
||||
|
||||
**Next Steps:**
|
||||
1. Browse [open issues](https://git.gobha.me/xcaliber/chat-switchboard/issues)
|
||||
2. Read the [ROADMAP](../ROADMAP.md)
|
||||
3. Make your first PR!
|
||||
@@ -1,575 +0,0 @@
|
||||
# 🔄 Workflow System - The Killer Feature
|
||||
|
||||
## What Are Workflows?
|
||||
|
||||
Workflows let you **chain multiple AI models, tools, and data sources** into automated pipelines. Think Zapier/n8n but for LLMs.
|
||||
|
||||
### Why This Matters
|
||||
|
||||
**Current State (Competitors):**
|
||||
```
|
||||
User → Single Prompt → One Model → Response
|
||||
```
|
||||
|
||||
**Chat Switchboard Workflows:**
|
||||
```
|
||||
User Input → [Model A] → [Tool] → [Model B] → [Conditional Logic] → Final Output
|
||||
↓ logs ↓ saves ↓ verifies ↓ branches
|
||||
```
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### 1. Research Assistant
|
||||
**Problem:** Researching a topic requires multiple steps and models
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ User Query │
|
||||
└──────┬──────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ 1. Web Search Tool │ ← DuckDuckGo API
|
||||
└──────┬──────────────┘
|
||||
│ (top 5 results)
|
||||
↓
|
||||
┌─────────────────────────┐
|
||||
│ 2. GPT-4: Extract Facts │ ← Fast, cheap summarization
|
||||
└──────┬──────────────────┘
|
||||
│ (structured data)
|
||||
↓
|
||||
┌──────────────────────────┐
|
||||
│ 3. Claude: Verify Claims │ ← Accurate fact-checking
|
||||
└──────┬───────────────────┘
|
||||
│ (verified facts)
|
||||
↓
|
||||
┌──────────────────────────┐
|
||||
│ 4. Gemini: Write Report │ ← Long-form generation
|
||||
└──────┬───────────────────┘
|
||||
│ (final report)
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 5. Save to KB Tool │ ← Store for future reference
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
**Time Saved:** 30 minutes → 2 minutes
|
||||
**Cost Optimization:** Uses cheap models where possible
|
||||
|
||||
### 2. Code Review Pipeline
|
||||
```
|
||||
[GitHub PR] → [Fetch Code Tool] → [GPT-4: Find Bugs] → [Claude: Security Check]
|
||||
↓ ↓
|
||||
[Save Issues] [Create Report]
|
||||
↓
|
||||
[Post to Channel Tool]
|
||||
```
|
||||
|
||||
### 3. Multi-Model Consensus
|
||||
**Problem:** Get the "best" answer by combining multiple AI perspectives
|
||||
|
||||
```
|
||||
┌─[GPT-4]─┐
|
||||
│ │
|
||||
[User Question] ────┼─[Claude]─┼──→ [Voting Logic] ──→ [Best Answer]
|
||||
│ │
|
||||
└─[Gemini]┘
|
||||
```
|
||||
|
||||
**Use Case:** Medical advice, legal analysis, important decisions
|
||||
|
||||
### 4. Content Creation Factory
|
||||
```
|
||||
[Topic] → [GPT-4: Outline] → [Claude: Draft] → [Grammar Tool] → [SEO Tool] → [Publish]
|
||||
↓ ↓ ↓ ↓
|
||||
[Save Draft] [Version 1] [Version 2] [Final]
|
||||
```
|
||||
|
||||
### 5. Data Processing Pipeline
|
||||
```
|
||||
[CSV Upload] → [Python Tool: Parse] → [GPT-3.5: Categorize] → [Save to PostgreSQL]
|
||||
↓
|
||||
[Generate Report Tool]
|
||||
```
|
||||
|
||||
## Workflow Builder UI
|
||||
|
||||
### Visual Editor (Drag & Drop)
|
||||
|
||||
```
|
||||
+------------------+ +------------------+ +------------------+
|
||||
| [Web Search] |---->| [GPT-4 Node] |---->| [Save Note] |
|
||||
| | | | | |
|
||||
| Query: {input} | | Prompt: Summary | | Title: Research |
|
||||
+------------------+ +------------------+ +------------------+
|
||||
↓ ↓ ↓
|
||||
[5 results] [Markdown text] [Note ID]
|
||||
```
|
||||
|
||||
### Node Types
|
||||
|
||||
#### 1. Input Nodes
|
||||
- **User Input** - Text, file, form
|
||||
- **Trigger** - Schedule, webhook, event
|
||||
- **Variable** - Constants, config
|
||||
|
||||
#### 2. Model Nodes
|
||||
- **LLM Call** - Any configured model
|
||||
- **Model Router** - Auto-select best model
|
||||
- **Multi-Model** - Run parallel, compare
|
||||
|
||||
#### 3. Tool Nodes
|
||||
- **Web Search**
|
||||
- **Code Execution**
|
||||
- **API Call**
|
||||
- **Database Query**
|
||||
- **File Operations**
|
||||
- **Custom Extension Tools**
|
||||
|
||||
#### 4. Logic Nodes
|
||||
- **Conditional** - If/else branching
|
||||
- **Loop** - Iterate over data
|
||||
- **Merge** - Combine results
|
||||
- **Filter** - Data transformation
|
||||
|
||||
#### 5. Output Nodes
|
||||
- **Response** - Return to user
|
||||
- **Save** - Store in DB/KB
|
||||
- **Webhook** - External notification
|
||||
- **Channel Message** - Post to channel
|
||||
|
||||
## Workflow Definition (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "research-assistant",
|
||||
"name": "Research Assistant",
|
||||
"version": "1.0.0",
|
||||
"description": "Search, summarize, verify, and save research",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "input",
|
||||
"type": "input",
|
||||
"config": {
|
||||
"schema": {
|
||||
"query": {"type": "string", "required": true}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "search",
|
||||
"type": "tool",
|
||||
"tool": "web_search",
|
||||
"config": {
|
||||
"query": "{{input.query}}",
|
||||
"max_results": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "summarize",
|
||||
"type": "llm",
|
||||
"model": "gpt-4o-mini",
|
||||
"config": {
|
||||
"prompt": "Summarize these search results:\n{{search.results}}",
|
||||
"max_tokens": 500
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "verify",
|
||||
"type": "llm",
|
||||
"model": "claude-3.5-sonnet",
|
||||
"config": {
|
||||
"prompt": "Verify the accuracy of:\n{{summarize.content}}",
|
||||
"max_tokens": 300
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "save",
|
||||
"type": "tool",
|
||||
"tool": "save_to_kb",
|
||||
"config": {
|
||||
"kb_id": "research",
|
||||
"title": "{{input.query}}",
|
||||
"content": "{{verify.content}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "output",
|
||||
"type": "output",
|
||||
"config": {
|
||||
"response": "{{verify.content}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{"from": "input", "to": "search"},
|
||||
{"from": "search", "to": "summarize"},
|
||||
{"from": "summarize", "to": "verify"},
|
||||
{"from": "verify", "to": "save"},
|
||||
{"from": "save", "to": "output"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Execution Engine
|
||||
|
||||
### Go Workflow Runner
|
||||
|
||||
```go
|
||||
// server/workflows/executor.go
|
||||
package workflows
|
||||
|
||||
type Executor struct {
|
||||
workflow *Workflow
|
||||
context map[string]interface{}
|
||||
llmClient *LLMClient
|
||||
toolRegistry *ToolRegistry
|
||||
}
|
||||
|
||||
func (e *Executor) Run(input map[string]interface{}) (*Result, error) {
|
||||
e.context["input"] = input
|
||||
|
||||
// Topological sort to execute DAG in order
|
||||
sorted := e.workflow.TopologicalSort()
|
||||
|
||||
for _, node := range sorted {
|
||||
result, err := e.executeNode(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.context[node.ID] = result
|
||||
}
|
||||
|
||||
return e.context["output"], nil
|
||||
}
|
||||
|
||||
func (e *Executor) executeNode(node *Node) (interface{}, error) {
|
||||
switch node.Type {
|
||||
case "llm":
|
||||
return e.executeLLM(node)
|
||||
case "tool":
|
||||
return e.executeTool(node)
|
||||
case "conditional":
|
||||
return e.executeConditional(node)
|
||||
// ... other types
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) executeLLM(node *Node) (interface{}, error) {
|
||||
// Resolve template variables
|
||||
prompt := e.resolveTemplate(node.Config["prompt"])
|
||||
model := node.Config["model"]
|
||||
|
||||
// Call LLM
|
||||
response, err := e.llmClient.Chat(model, prompt)
|
||||
return response, err
|
||||
}
|
||||
```
|
||||
|
||||
### Variable Resolution
|
||||
|
||||
```go
|
||||
// Template: "Summarize: {{search.results}}"
|
||||
// Context: {"search": {"results": "..."}}
|
||||
// Result: "Summarize: ..."
|
||||
|
||||
func (e *Executor) resolveTemplate(template string) string {
|
||||
re := regexp.MustCompile(`\{\{([^}]+)\}\}`)
|
||||
return re.ReplaceAllStringFunc(template, func(match string) string {
|
||||
path := match[2:len(match)-2] // Remove {{}}
|
||||
return e.getFromContext(path)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Database Schema (Workflows)
|
||||
|
||||
```sql
|
||||
-- Workflow definitions
|
||||
CREATE TABLE workflows (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
name VARCHAR(200),
|
||||
graph JSONB NOT NULL, -- Node/edge structure
|
||||
input_schema JSONB,
|
||||
output_schema JSONB,
|
||||
is_public BOOLEAN DEFAULT false,
|
||||
tags TEXT[]
|
||||
);
|
||||
|
||||
-- Workflow executions (audit trail)
|
||||
CREATE TABLE workflow_executions (
|
||||
id UUID PRIMARY KEY,
|
||||
workflow_id UUID REFERENCES workflows(id),
|
||||
input_data JSONB,
|
||||
output_data JSONB,
|
||||
status VARCHAR(20), -- running, completed, failed
|
||||
steps JSONB, -- [{node_id, input, output, duration}]
|
||||
total_cost_usd NUMERIC(10, 6),
|
||||
total_time_ms INTEGER,
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## Workflow Marketplace
|
||||
|
||||
### Shareable Templates
|
||||
|
||||
```bash
|
||||
# Export workflow
|
||||
POST /api/workflows/{id}/export
|
||||
→ workflow.json file
|
||||
|
||||
# Import workflow
|
||||
POST /api/workflows/import
|
||||
← Upload workflow.json
|
||||
|
||||
# Publish to marketplace
|
||||
POST /api/marketplace/publish
|
||||
{
|
||||
"workflow_id": "uuid",
|
||||
"category": "research",
|
||||
"price": 0, // Free or paid
|
||||
"license": "MIT"
|
||||
}
|
||||
```
|
||||
|
||||
### Categories
|
||||
- 📊 Data Analysis
|
||||
- 🔍 Research
|
||||
- 📝 Content Creation
|
||||
- 💻 Code Review
|
||||
- 🎓 Education
|
||||
- 💼 Business Intelligence
|
||||
- 🔐 Security Audits
|
||||
|
||||
## Cost Optimization
|
||||
|
||||
### Smart Model Routing in Workflows
|
||||
|
||||
```javascript
|
||||
// Workflow node: Auto-select cheapest capable model
|
||||
{
|
||||
"id": "summarize",
|
||||
"type": "llm_auto",
|
||||
"config": {
|
||||
"task": "summarization",
|
||||
"max_cost": 0.01, // $0.01 max
|
||||
"min_quality": 0.8, // 80% quality threshold
|
||||
"prompt": "..."
|
||||
}
|
||||
}
|
||||
|
||||
// Engine selects:
|
||||
// GPT-4o-mini ($0.0001/token) ✅
|
||||
// Not GPT-4 ($0.03/token) - too expensive
|
||||
```
|
||||
|
||||
### Execution Cost Tracking
|
||||
|
||||
```sql
|
||||
-- Track workflow costs
|
||||
SELECT
|
||||
w.name,
|
||||
COUNT(we.id) as executions,
|
||||
AVG(we.total_cost_usd) as avg_cost,
|
||||
SUM(we.total_cost_usd) as total_cost
|
||||
FROM workflows w
|
||||
JOIN workflow_executions we ON w.id = we.workflow_id
|
||||
GROUP BY w.id
|
||||
ORDER BY total_cost DESC;
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### 1. Conditional Branching
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "quality_check",
|
||||
"type": "conditional",
|
||||
"config": {
|
||||
"condition": "{{verify.confidence}} > 0.8",
|
||||
"true_branch": "save",
|
||||
"false_branch": "human_review"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Loops
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "process_batch",
|
||||
"type": "loop",
|
||||
"config": {
|
||||
"items": "{{input.files}}",
|
||||
"body": "extract_data_node",
|
||||
"merge": "combine_results_node"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Parallel Execution
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "parallel_models",
|
||||
"type": "parallel",
|
||||
"config": {
|
||||
"branches": ["gpt4_node", "claude_node", "gemini_node"],
|
||||
"merge_strategy": "voting" // or "first", "all", "custom"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "api_call",
|
||||
"type": "tool",
|
||||
"config": {
|
||||
"tool": "external_api",
|
||||
"retry": 3,
|
||||
"timeout": 5000,
|
||||
"on_error": "fallback_node"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security & Limits
|
||||
|
||||
### Sandboxing
|
||||
- Code execution in isolated containers
|
||||
- API rate limiting per workflow
|
||||
- Cost caps per execution
|
||||
|
||||
### Permissions
|
||||
```sql
|
||||
-- Workflow ACL
|
||||
CREATE TABLE workflow_permissions (
|
||||
workflow_id UUID REFERENCES workflows(id),
|
||||
user_id UUID REFERENCES users(id),
|
||||
permission VARCHAR(20), -- read, execute, edit, admin
|
||||
UNIQUE(workflow_id, user_id)
|
||||
);
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
```
|
||||
POST /api/workflows # Create workflow
|
||||
GET /api/workflows # List user's workflows
|
||||
GET /api/workflows/{id} # Get workflow
|
||||
PUT /api/workflows/{id} # Update workflow
|
||||
DELETE /api/workflows/{id} # Delete workflow
|
||||
|
||||
POST /api/workflows/{id}/execute # Run workflow
|
||||
GET /api/workflows/{id}/executions # Execution history
|
||||
GET /api/executions/{id} # Execution details
|
||||
POST /api/executions/{id}/stop # Cancel execution
|
||||
|
||||
GET /api/marketplace/workflows # Browse public workflows
|
||||
POST /api/marketplace/install/{id} # Install from marketplace
|
||||
```
|
||||
|
||||
## Frontend UI (React/Vue Later, Start Simple)
|
||||
|
||||
### Workflow Editor Component
|
||||
|
||||
```javascript
|
||||
// src/js/workflow-editor.js
|
||||
class WorkflowEditor {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.nodes = [];
|
||||
this.edges = [];
|
||||
}
|
||||
|
||||
addNode(type, x, y) {
|
||||
const node = {
|
||||
id: generateId(),
|
||||
type: type,
|
||||
position: {x, y},
|
||||
config: {}
|
||||
};
|
||||
this.nodes.push(node);
|
||||
this.render();
|
||||
}
|
||||
|
||||
connectNodes(from, to) {
|
||||
this.edges.push({from, to});
|
||||
this.render();
|
||||
}
|
||||
|
||||
execute() {
|
||||
const workflow = this.toJSON();
|
||||
fetch('/api/workflows/execute', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(workflow)
|
||||
}).then(r => r.json()).then(result => {
|
||||
console.log('Workflow result:', result);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comparison with Alternatives
|
||||
|
||||
| Feature | Chat Switchboard | LangChain | n8n | Zapier |
|
||||
|---------|-----------------|-----------|-----|--------|
|
||||
| Visual Builder | ✅ | ❌ | ✅ | ✅ |
|
||||
| Multi-Model | ✅ | Limited | ❌ | ❌ |
|
||||
| Open Source | ✅ | ✅ | ✅ | ❌ |
|
||||
| Self-Hosted | ✅ | N/A | ✅ | ❌ |
|
||||
| Cost Tracking | ✅ | ❌ | ❌ | ✅ |
|
||||
| LLM-First | ✅ | ✅ | ❌ | ❌ |
|
||||
| No-Code | ✅ | ❌ | ✅ | ✅ |
|
||||
|
||||
**Unique Position:** LangChain for non-coders + n8n for LLMs
|
||||
|
||||
## Roadmap
|
||||
|
||||
### MVP (v1.0)
|
||||
- ✅ Basic DAG execution
|
||||
- ✅ LLM nodes (single model)
|
||||
- ✅ Tool nodes (web search, code exec)
|
||||
- ✅ Input/output nodes
|
||||
- ✅ Simple UI (form-based, not visual yet)
|
||||
|
||||
### v1.1
|
||||
- ⬜ Visual drag-drop editor
|
||||
- ⬜ Conditional logic
|
||||
- ⬜ Loops
|
||||
- ⬜ Cost tracking
|
||||
|
||||
### v1.2
|
||||
- ⬜ Template marketplace
|
||||
- ⬜ Workflow sharing
|
||||
- ⬜ Scheduled executions
|
||||
- ⬜ Webhook triggers
|
||||
|
||||
### v2.0
|
||||
- ⬜ Collaborative editing
|
||||
- ⬜ Version control (Git-like)
|
||||
- ⬜ A/B testing workflows
|
||||
- ⬜ Analytics dashboard
|
||||
|
||||
---
|
||||
|
||||
## Get Started
|
||||
|
||||
```bash
|
||||
# Create your first workflow
|
||||
curl -X POST https://api.chatswitch.example.com/api/workflows \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d @examples/research-assistant.json
|
||||
|
||||
# Execute it
|
||||
curl -X POST https://api.chatswitch.example.com/api/workflows/{id}/execute \
|
||||
-d '{"query": "Latest AI research"}'
|
||||
```
|
||||
|
||||
**This is the feature that sets Chat Switchboard apart.**
|
||||
Reference in New Issue
Block a user