diff --git a/CLAUDE.md b/CLAUDE.md index 9ce8fb6110..a0b834d3f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -209,3 +209,85 @@ Preset theme overrides are defined in `packages/superdoc/src/assets/styles/helpe - Expose UI component-specific variables as `--sd-ui-{component}-*` so consumers can customize via CSS. **When writing copy or content:** see `brand/brand-guidelines.md` for voice, tone, and the dual-register pattern (developer vs. leader). Product name is always **SuperDoc** (capital S, capital D). + +## SD-2091: chooseTools() Investigation Findings (RESOLVED) + +### Root Cause + +The `chooseTools()` function wasn't returning mutation tools due to: + +1. **Default phase is `'read'`** — excludes `mutation`, `create`, `format` categories +2. **Wrong budget syntax** — `budget: 100` should be `budget: { maxTools: 100 }` +3. **Default maxTools is 12** — limits tools even when phase allows more + +### How chooseTools() Works + +**Location**: `packages/sdk/langs/node/src/tools.ts` + +The function filters tools based on phase, then applies budget limits: + +| Phase | Included Categories | Excluded Categories | +|-------|---------------------|---------------------| +| `read` (default) | introspection, query | mutation, create, format, comments, trackChanges, session | +| `mutate` | query, mutation, format, comments, create | session | +| `locate` | query | mutation, create, format, comments, trackChanges, session | +| `review` | query, trackChanges, comments | mutation, create, format, session | + +**Default budgets**: intent=12, operation=16 tools max. + +### Correct Usage for Mutation Tools + +**IMPORTANT:** The npm SDK (v1.0.0-alpha.44) has a different API than local development. + +```typescript +// npm SDK v1.0.0-alpha.44 API +const { tools, selected, meta } = await chooseTools({ + provider: 'openai', + mode: 'all', // 'essential' (default) or 'all' + groups: ['core', 'format', 'create'], // Additional groups to include + includeDiscoverTool: false, // Whether to include discover_tools meta-tool +}); + +// Filter out LLM-incompatible tools manually +const EXCLUDED = new Set(['apply_mutations', 'query_match', 'preview_mutations']); +const filteredTools = tools.filter((t) => !EXCLUDED.has(t.function.name)); +``` + +**Available groups:** `core`, `format`, `create`, `tables`, `sections`, `lists`, `comments`, `trackChanges`, `toc`, `images`, `history`, `session` + +**Note:** The local dev version has more sophisticated options (`taskContext.phase`, `budget`, `policy.forceInclude/forceExclude`) that aren't in the npm package yet. + +### Available Tools by Category (intent profile) + +| Category | Tools | +|----------|-------| +| mutation | `insert_content`, `replace_content`, `delete_content`, `apply_mutations` | +| create | `create_paragraph`, `create_heading`, `create_section_break`, `create_table`, `create_table_of_contents`, `create_image` | +| query | `find_content`, `get_node`, `get_node_by_id`, `get_document_text`, `get_document_markdown`, `get_document_html`, `get_document_info`, `query_match`, `preview_mutations` | +| format | `format_bold`, `format_italic`, `format_underline`, + 30 more | + +### Tools With Complex Schemas (LLM Incompatible) + +These tools have discriminated union schemas that LLMs cannot construct correctly: + +| Tool | Issue | +|------|-------| +| `apply_mutations` | Complex `steps` array with discriminated union variants | +| `query_match` | Complex `select` parameter with multiple schema variants | +| `preview_mutations` | Same as apply_mutations | + +Use `forceExclude` to remove these from LLM tool sets. + +### Files Changed + +**examples/collaboration/superdoc-yjs/** +- `agent.ts` — Fixed `chooseTools()` configuration with correct budget syntax and forceInclude/forceExclude +- `package.json` — Uses npm packages (`superdoc@^1.18.2`, `@superdoc-dev/sdk@^1.0.0-alpha.44`) +- `src/App.vue` — Chat sidebar for agent communication +- `README.md` — Customer documentation + +### Remaining Work + +1. **Fix tool schema generation** for `apply_mutations` and `query_match` to be LLM-compatible +2. **Consider adding wrapper tools** with simpler schemas for complex operations +3. **Document chooseTools() gotchas** in SDK documentation diff --git a/examples/document-api/agentic-collaboration/.env.example b/examples/document-api/agentic-collaboration/.env.example new file mode 100644 index 0000000000..3e48acbf0a --- /dev/null +++ b/examples/document-api/agentic-collaboration/.env.example @@ -0,0 +1,6 @@ +# OpenAI API key (required for agent) +OPENAI_API_KEY=sk-your-key-here + +# Backend URL (set after Cloud Run deployment) +# Leave empty for local development (defaults to localhost:3050) +VITE_BACKEND_URL= diff --git a/examples/document-api/agentic-collaboration/.gitignore b/examples/document-api/agentic-collaboration/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/examples/document-api/agentic-collaboration/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/document-api/agentic-collaboration/README.md b/examples/document-api/agentic-collaboration/README.md new file mode 100644 index 0000000000..0697fcfa84 --- /dev/null +++ b/examples/document-api/agentic-collaboration/README.md @@ -0,0 +1,299 @@ +# SuperDoc Document Editing Agent + +A chat-based AI agent that can read and modify documents using the SuperDoc SDK. This example demonstrates: + +- Real-time chat interface with an AI document editing agent +- Agent uses `chooseTools()` to get LLM-compatible tool definitions +- Full agentic loop with tool calling via OpenAI +- Document edits broadcast to all clients via Yjs collaboration + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Vue Client (port 5173) │ +│ ┌──────────────────────────┐ ┌────────────────────────────────┐ │ +│ │ SuperDoc Editor │ │ Chat Sidebar │ │ +│ │ (document editing) │ │ - Send messages to agent │ │ +│ │ │ │ - See agent responses │ │ +│ │ │ │ - Agent status indicator │ │ +│ └──────────────────────────┘ └────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ │ + │ SuperDoc collaboration │ WebSocket /chat/:roomId + │ (document sync) │ (simple JSON messages) + ▼ ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Collaboration Server (port 3050) │ +│ Fastify + Yjs + WebSocket │ +│ /collaboration/:docId /chat/:roomId │ +└─────────────────────────────────────────────────────────────────────┘ + ▲ ▲ + │ SDK collaboration │ WebSocket /chat/:roomId + │ │ +┌─────────────────────────────────────────────────────────────────────┐ +│ AI Agent (Node.js) │ +│ - Connects to document via SDK (client.doc.open) │ +│ - Uses chooseTools() to get Document API tools │ +│ - Processes requests with OpenAI gpt-4o │ +│ - Executes tools via dispatchSuperDocTool() │ +│ - Edits broadcast to all clients automatically │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Prerequisites + +- **Node.js** 18+ +- **OpenAI API key** + +## Quick Start + +### 1. Install dependencies + +```bash +npm install +``` + +### 2. Configure environment + +```bash +cp .env.example .env +``` + +Edit `.env` and add your OpenAI API key: + +``` +OPENAI_API_KEY=sk-your-key-here +``` + +### 3. Run the example + +```bash +npm run dev +``` + +This starts: +- **Collaboration server** on `http://localhost:3050` +- **Vue client** on `http://localhost:5173` +- **AI agent** connected to the same document + +### 4. Try it out + +1. Open `http://localhost:5173` +2. Use the chat sidebar on the right to talk to the agent +3. Try commands like: + - "Add a heading that says 'Introduction'" + - "Insert a paragraph about AI" + - "Make the first line bold" + - "What's in this document?" + +## Project Structure + +| File | Description | +|------|-------------| +| `agent.ts` | AI agent with SDK integration and agentic loop | +| `server.ts` | Fastify server for collaboration + chat WebSocket | +| `src/App.vue` | Vue client with editor and chat sidebar | + +## SDK Usage + +### Connecting to a Document + +```typescript +import { createSuperDocClient } from '@superdoc-dev/sdk'; + +const client = createSuperDocClient(); +await client.connect(); + +await client.doc.open({ + collaboration: { + providerType: 'y-websocket', + url: 'ws://localhost:3050/collaboration', + documentId: 'my-doc', + }, +}); +``` + +### Getting Tools for LLM + +```typescript +import { chooseTools } from '@superdoc-dev/sdk'; + +const { tools } = await chooseTools({ + provider: 'openai', + mode: 'all', // Include mutation tools +}); + +// tools is an array of OpenAI-compatible tool definitions +``` + +### Executing Tools + +```typescript +import { dispatchSuperDocTool } from '@superdoc-dev/sdk'; + +// Insert content at end of document +await dispatchSuperDocTool(client, 'insert_content', { + value: '# New Heading\n\nSome paragraph text.', + type: 'markdown', +}); + +// Get document text +const text = await client.doc.getText({}); +``` + +### Agentic Loop + +```typescript +for (let i = 0; i < 10; i++) { + const response = await openai.chat.completions.create({ + model: 'gpt-4o', + messages, + tools, + tool_choice: i === 0 ? 'required' : 'auto', + }); + + const message = response.choices[0].message; + + if (!message.tool_calls?.length) { + return message.content; // Done + } + + // Execute each tool call + for (const call of message.tool_calls) { + const args = JSON.parse(call.function.arguments); + const result = await dispatchSuperDocTool(client, call.function.name, args); + messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) }); + } +} +``` + +## Available Scripts + +| Script | Description | +|--------|-------------| +| `npm run dev` | Run all components (server + client + agent) | +| `npm run dev:server` | Run only the collaboration server | +| `npm run dev:client` | Run only the Vue client | +| `npm run dev:agent` | Run only the AI agent | +| `npm run dev:client:remote` | Run local frontend against deployed backend | +| `npm run deploy:backend` | Deploy backend to Google Cloud Run | +| `npm run deploy:frontend` | Deploy frontend to Cloudflare Pages | + +## Deployment + +The demo can be deployed with the frontend on Cloudflare Pages and the backend on Google Cloud Run. + +### Prerequisites + +- **Google Cloud CLI** (`gcloud`) authenticated with a project +- **Wrangler CLI** (`npx wrangler`) authenticated with Cloudflare +- **OpenAI API key** in `.env` + +### Step 1: Deploy Backend (Cloud Run) + +The backend includes the collaboration server and AI agent in a single container. + +```bash +# Make sure .env has your OpenAI key +npm run deploy:backend +``` + +This will: +1. Build a Docker container with the server and agent +2. Push it to Google Container Registry +3. Deploy to Cloud Run with WebSocket support +4. Output the service URL (e.g., `https://superdoc-agent-demo-xxxxx.run.app`) + +### Step 2: Configure Frontend + +Add the backend URL to your `.env` file: + +```bash +# .env +OPENAI_API_KEY=sk-your-key-here +VITE_BACKEND_URL=https://superdoc-agent-demo-xxxxx.run.app +``` + +### Step 3: Deploy Frontend (Cloudflare Pages) + +```bash +npm run deploy:frontend +``` + +This will: +1. Build the Vue client with the backend URL baked in +2. Deploy to Cloudflare Pages (project: `document-api-agentic-demo`) + +The frontend will be available at: `https://document-api-agentic-demo.pages.dev` + +### Deployment Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Cloudflare Pages (Frontend) │ +│ https://document-api-agentic-demo.pages.dev │ +│ Vue + SuperDoc Editor │ +└─────────────────────────────────────────────────────────────────────┘ + │ + │ WebSocket (wss://) + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Google Cloud Run (Backend) │ +│ https://superdoc-agent-demo-xxxxx.run.app │ +│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Collaboration Server │ │ AI Agent │ │ +│ │ (Fastify + Yjs) │ │ (OpenAI + SuperDoc SDK) │ │ +│ └─────────────────────────┘ └─────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Environment Variables + +| Variable | Where | Description | +|----------|-------|-------------| +| `OPENAI_API_KEY` | Cloud Run | OpenAI API key for the agent | +| `VITE_BACKEND_URL` | Build time | Backend URL for frontend to connect to | + +### Testing Locally with Deployed Backend + +To test the local frontend against the deployed Cloud Run backend: + +```bash +# 1. Set the backend URL in .env +echo "VITE_BACKEND_URL=https://superdoc-agent-demo-xxxxx.run.app" >> .env + +# 2. Run only the frontend (no local server/agent) +npm run dev:client:remote +``` + +Open `http://localhost:5173` - the local Vue app will connect to the deployed backend. + +### Health Check + +The backend exposes a health endpoint for monitoring: + +```bash +curl https://superdoc-agent-demo-xxxxx.run.app/health +# Returns: {"status":"ok"} +``` + +## Troubleshooting + +### Agent shows "Offline" + +Make sure the agent is running. Check the terminal for errors. The agent needs a valid `OPENAI_API_KEY` in `.env`. + +### Tools not working + +Some tools have complex schemas and are excluded. The agent logs show which tools are available. + +### Edits not appearing + +Both client and agent connect to the same collaboration room. The SDK handles syncing edits automatically. + +## Learn More + +- [SuperDoc Documentation](https://docs.superdoc.dev) +- [Document API SDK Reference](https://docs.superdoc.dev/document-api) +- [Self-hosted Collaboration Guide](https://docs.superdoc.dev/guides/superdoc-yjs) diff --git a/examples/document-api/agentic-collaboration/agent/agent.ts b/examples/document-api/agentic-collaboration/agent/agent.ts new file mode 100644 index 0000000000..1af89971c9 --- /dev/null +++ b/examples/document-api/agentic-collaboration/agent/agent.ts @@ -0,0 +1,95 @@ +/** + * SuperDoc Document Editing Agent + * + * Edits documents using the SuperDoc SDK and OpenAI. + * Based on: https://docs.superdoc.dev/document-engine/ai-agents/llm-tools + */ + +import dotenv from 'dotenv'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +dotenv.config({ path: join(__dirname, '..', '.env') }); + +import OpenAI from 'openai'; +import { createSuperDocClient, chooseTools, dispatchSuperDocTool, getSystemPrompt } from '@superdoc-dev/sdk'; +import { Chat } from './chat.js'; + +const PORT = process.env.PORT || '3050'; +const COLLAB_URL = `ws://localhost:${PORT}/collaboration`; +const CHAT_URL = `ws://localhost:${PORT}/chat`; +const DOC_ID = process.argv[2] || 'superdoc-demo'; + +console.log('[Agent] Starting...'); + +// Connect and open document +const client = createSuperDocClient(); +await client.connect(); + +const doc = await client.open({ + collaboration: { + providerType: 'y-websocket', + url: COLLAB_URL, + documentId: DOC_ID, + }, +}); +console.log(`[Agent] Joined room: ${DOC_ID}`); + +// Load tools +const { tools } = await chooseTools({ provider: 'openai' }); +console.log(`[Agent] Loaded ${tools.length} tools`); + +// Set up OpenAI +const openai = new OpenAI(); +const conversationHistory: OpenAI.ChatCompletionMessageParam[] = [ + { role: 'system', content: await getSystemPrompt() }, +]; + +// Agent loop: process a user message and return a response +async function processMessage(userMessage: string): Promise { + conversationHistory.push({ role: 'user', content: userMessage }); + const messages: OpenAI.ChatCompletionMessageParam[] = [...conversationHistory]; + + while (true) { + const response = await openai.chat.completions.create({ + model: 'gpt-4o', + messages, + tools: tools as OpenAI.ChatCompletionTool[], + }); + + const message = response.choices[0].message; + messages.push(message); + + if (!message.tool_calls?.length) { + conversationHistory.push({ role: 'assistant', content: message.content || 'Done.' }); + return message.content || 'Done.'; + } + + for (const call of message.tool_calls) { + const result = await dispatchSuperDocTool( + doc, + call.function.name, + JSON.parse(call.function.arguments), + ); + messages.push({ + role: 'tool', + tool_call_id: call.id, + content: JSON.stringify(result), + }); + } + } +} + +// Connect to chat and serve messages +const chat = await new Chat(`${CHAT_URL}/${DOC_ID}`).connect(); +chat.serve(processMessage); + +console.log('[Agent] Ready. Press Ctrl+C to exit.'); + +process.on('SIGINT', async () => { + chat.close(); + await doc.close(); + await client.dispose(); + process.exit(0); +}); diff --git a/examples/document-api/agentic-collaboration/agent/chat.ts b/examples/document-api/agentic-collaboration/agent/chat.ts new file mode 100644 index 0000000000..63fff6c677 --- /dev/null +++ b/examples/document-api/agentic-collaboration/agent/chat.ts @@ -0,0 +1,64 @@ +/** + * Chat WebSocket client for agent communication. + */ + +import WebSocket from 'ws'; + +type MessageHandler = (userMessage: string) => Promise; + +export class Chat { + private url: string; + private ws!: WebSocket; + + constructor(url: string) { + this.url = url; + } + + connect(): Promise { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(this.url); + this.ws.on('open', () => resolve(this)); + this.ws.on('error', reject); + }); + } + + serve(handler: MessageHandler): this { + this.ws.on('message', async (data) => { + const msg = JSON.parse(data.toString()); + if (msg.type === 'message' && msg.message?.role === 'user') { + this.setStatus('thinking'); + try { + const response = await handler(msg.message.content); + this.send(response); + } catch (err) { + console.error('[Agent] Error:', err); + this.send('Sorry, I encountered an error.'); + } + this.setStatus('ready'); + } + }); + this.setStatus('ready'); + return this; + } + + send(content: string): this { + this.ws.send(JSON.stringify({ + type: 'message', + role: 'assistant', + id: `agent-${Date.now()}`, + content, + timestamp: Date.now(), + })); + return this; + } + + setStatus(status: string): this { + this.ws.send(JSON.stringify({ type: 'status', status })); + return this; + } + + close() { + this.setStatus('offline'); + this.ws.close(); + } +} diff --git a/examples/document-api/agentic-collaboration/agent/package.json b/examples/document-api/agentic-collaboration/agent/package.json new file mode 100644 index 0000000000..4187706d6e --- /dev/null +++ b/examples/document-api/agentic-collaboration/agent/package.json @@ -0,0 +1,20 @@ +{ + "name": "agentic-collaboration-agent", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "tsx watch agent.ts", + "start": "tsx agent.ts" + }, + "dependencies": { + "@superdoc-dev/sdk": "^1.0.0", + "dotenv": "^16.4.7", + "openai": "^4.78.0", + "ws": "^8.18.0", + "yjs": "13.6.19" + }, + "devDependencies": { + "tsx": "^4.20.6" + } +} diff --git a/examples/document-api/agentic-collaboration/client/index.html b/examples/document-api/agentic-collaboration/client/index.html new file mode 100644 index 0000000000..7df3e389fd --- /dev/null +++ b/examples/document-api/agentic-collaboration/client/index.html @@ -0,0 +1,17 @@ + + + + + + + SuperDoc - Agentic Document Editing Demo + + + + + + +
+ + + diff --git a/examples/document-api/agentic-collaboration/client/package.json b/examples/document-api/agentic-collaboration/client/package.json new file mode 100644 index 0000000000..caddcd80c7 --- /dev/null +++ b/examples/document-api/agentic-collaboration/client/package.json @@ -0,0 +1,19 @@ +{ + "name": "agentic-collaboration-client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "superdoc": "^1.20.0", + "vue": "^3.5.13", + "yjs": "13.6.19" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.2", + "vite": "^6.0.0" + } +} diff --git a/examples/document-api/agentic-collaboration/client/public/logo.webp b/examples/document-api/agentic-collaboration/client/public/logo.webp new file mode 100644 index 0000000000..f36a71b86d Binary files /dev/null and b/examples/document-api/agentic-collaboration/client/public/logo.webp differ diff --git a/examples/document-api/agentic-collaboration/client/public/sample-document.docx b/examples/document-api/agentic-collaboration/client/public/sample-document.docx new file mode 100644 index 0000000000..40e55aa3c9 Binary files /dev/null and b/examples/document-api/agentic-collaboration/client/public/sample-document.docx differ diff --git a/examples/document-api/agentic-collaboration/client/src/App.vue b/examples/document-api/agentic-collaboration/client/src/App.vue new file mode 100644 index 0000000000..333ff2371b --- /dev/null +++ b/examples/document-api/agentic-collaboration/client/src/App.vue @@ -0,0 +1,596 @@ + + + + + diff --git a/examples/document-api/agentic-collaboration/client/src/main.js b/examples/document-api/agentic-collaboration/client/src/main.js new file mode 100644 index 0000000000..2425c0f745 --- /dev/null +++ b/examples/document-api/agentic-collaboration/client/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/examples/document-api/agentic-collaboration/client/src/style.css b/examples/document-api/agentic-collaboration/client/src/style.css new file mode 100644 index 0000000000..7cb8f1e8a2 --- /dev/null +++ b/examples/document-api/agentic-collaboration/client/src/style.css @@ -0,0 +1,74 @@ +/* SuperDoc Agentic Demo - Global Styles */ + +:root { + /* Brand colors */ + --sd-blue-500: #3b82f6; + --sd-blue-600: #2563eb; + --sd-indigo-500: #6366f1; + --sd-indigo-600: #4f46e5; + --sd-green-500: #22c55e; + --sd-green-600: #16a34a; + + /* Neutrals */ + --sd-gray-50: #f8fafc; + --sd-gray-100: #f1f5f9; + --sd-gray-200: #e2e8f0; + --sd-gray-300: #cbd5e1; + --sd-gray-400: #94a3b8; + --sd-gray-500: #64748b; + --sd-gray-600: #475569; + --sd-gray-700: #334155; + --sd-gray-800: #1e293b; + --sd-gray-900: #0f172a; + + /* Typography */ + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.5; + font-weight: 400; + color: var(--sd-gray-800); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Import Inter font */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +a { + font-weight: 500; + color: var(--sd-blue-500); + text-decoration: none; + transition: color 0.15s ease; +} + +a:hover { + color: var(--sd-blue-600); +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: var(--sd-gray-50); +} + +h1, h2, h3, h4, h5, h6 { + line-height: 1.2; + font-weight: 600; + color: var(--sd-gray-800); +} + +#app { + width: 100%; +} + +p { + padding: 0; + margin: 0; +} + +/* Utility classes */ +.hidden { + display: none; +} diff --git a/examples/document-api/agentic-collaboration/client/vite.config.js b/examples/document-api/agentic-collaboration/client/vite.config.js new file mode 100644 index 0000000000..bbcf80cca9 --- /dev/null +++ b/examples/document-api/agentic-collaboration/client/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], +}) diff --git a/examples/document-api/agentic-collaboration/package.json b/examples/document-api/agentic-collaboration/package.json new file mode 100644 index 0000000000..b2f32f9ef7 --- /dev/null +++ b/examples/document-api/agentic-collaboration/package.json @@ -0,0 +1,17 @@ +{ + "name": "agentic-collaboration", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\" \"npm run dev:agent\"", + "dev:client": "npm run dev --prefix client", + "dev:server": "npm run dev --prefix server", + "dev:agent": "npm run dev --prefix agent", + "build": "npm run build --prefix client", + "install:all": "npm install && npm install --prefix client && npm install --prefix server && npm install --prefix agent" + }, + "devDependencies": { + "concurrently": "^9.0.0" + } +} diff --git a/examples/document-api/agentic-collaboration/server/package.json b/examples/document-api/agentic-collaboration/server/package.json new file mode 100644 index 0000000000..84132c8f12 --- /dev/null +++ b/examples/document-api/agentic-collaboration/server/package.json @@ -0,0 +1,20 @@ +{ + "name": "agentic-collaboration-server", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "tsx watch server.ts", + "start": "tsx server.ts" + }, + "dependencies": { + "@superdoc-dev/superdoc-yjs-collaboration": "^1.0.0", + "@fastify/websocket": "^11.1.0", + "dotenv": "^16.4.7", + "fastify": "^5.3.3", + "yjs": "13.6.19" + }, + "devDependencies": { + "tsx": "^4.20.6" + } +} diff --git a/examples/document-api/agentic-collaboration/server/server.ts b/examples/document-api/agentic-collaboration/server/server.ts new file mode 100644 index 0000000000..167eddf391 --- /dev/null +++ b/examples/document-api/agentic-collaboration/server/server.ts @@ -0,0 +1,165 @@ +import Fastify from 'fastify'; +import websocketPlugin from '@fastify/websocket'; +import { Doc as YDoc, encodeStateAsUpdate } from 'yjs'; +import type { WebSocket } from 'ws'; + +import { + CollaborationBuilder, + type CollaborationParams, + type UserContext, + type ServiceConfig +} from '@superdoc-dev/superdoc-yjs-collaboration'; + +// ============================================================================ +// Chat Types +// ============================================================================ + +interface ChatMessage { + id: string; + role: 'user' | 'assistant'; + content: string; + timestamp: number; +} + +interface ChatRoom { + messages: ChatMessage[]; + clients: Set; + agentStatus: string; +} + +const chatRooms = new Map(); + +function getChatRoom(roomId: string): ChatRoom { + if (!chatRooms.has(roomId)) { + chatRooms.set(roomId, { messages: [], clients: new Set(), agentStatus: 'offline' }); + } + return chatRooms.get(roomId)!; +} + +function broadcastToRoom(roomId: string, data: object, exclude?: WebSocket) { + const room = getChatRoom(roomId); + const msg = JSON.stringify(data); + for (const client of room.clients) { + if (client !== exclude && client.readyState === 1) client.send(msg); + } +} + +// ============================================================================ +// Collaboration Hooks +// ============================================================================ + +const handleConfig = (config: ServiceConfig): void => { + console.log('[Server] Collaboration service configured'); +}; + +const handleAuth = async ({ documentId }: CollaborationParams): Promise => { + console.log(`[Server] Auth for document: ${documentId}`); + return { + user: { userid: 'abc', username: 'testuser' }, + organizationid: 'someorg123', + custom: { someCustomKey: 'somevalue' } + }; +}; + +const handleLoad = async (params: CollaborationParams): Promise => { + console.log(`[Server] Loading document: ${params.documentId}`); + const ydoc = new YDoc(); + return encodeStateAsUpdate(ydoc); +}; + +const handleOnChange = async (params: CollaborationParams): Promise => { + // Quiet - too noisy +}; + +const handleAutoSave = async (params: CollaborationParams): Promise => { + // Quiet - too noisy +}; + +const SuperDocCollaboration = new CollaborationBuilder() + .withName('SuperDoc Collaboration service') + .withDebounce(2000) + .onConfigure(handleConfig) + .onLoad(handleLoad) + .onAuthenticate(handleAuth) + .onChange(handleOnChange) + .onAutoSave(handleAutoSave) + .build(); + +// ============================================================================ +// Server Setup +// ============================================================================ + +async function main() { + const fastify = Fastify({ logger: false }); + const port = parseInt(process.env.PORT || '3050', 10); + + // Register WebSocket plugin + await fastify.register(websocketPlugin); + + // Health check + fastify.get('/health', async () => ({ status: 'ok' })); + + // Collaboration WebSocket + fastify.get('/collaboration/:documentId', { websocket: true }, (socket, request) => { + const documentId = (request.params as { documentId: string }).documentId; + console.log(`[Server] Collaboration client connected: ${documentId}`); + SuperDocCollaboration.welcome(socket as any, request as any); + }); + + // Chat WebSocket + fastify.get('/chat/:roomId', { websocket: true }, (socket, request) => { + const roomId = (request.params as { roomId: string }).roomId; + const room = getChatRoom(roomId); + room.clients.add(socket); + console.log(`[Server] Chat client joined "${roomId}" (${room.clients.size} clients)`); + + // Send current state + socket.send(JSON.stringify({ type: 'init', messages: room.messages, agentStatus: room.agentStatus })); + + socket.on('message', (data) => { + try { + const msg = JSON.parse(data.toString()); + console.log(`[Server] Chat ${msg.type} from ${msg.role || 'system'}:`, msg.content?.slice(0, 50) || ''); + + if (msg.type === 'message') { + const chatMsg: ChatMessage = { + id: msg.id || `${msg.role}-${Date.now()}`, + role: msg.role, + content: msg.content, + timestamp: msg.timestamp || Date.now() + }; + room.messages.push(chatMsg); + console.log(`[Server] Broadcasting to ${room.clients.size - 1} other clients`); + broadcastToRoom(roomId, { type: 'message', message: chatMsg }, socket); + } else if (msg.type === 'status') { + room.agentStatus = msg.status; + broadcastToRoom(roomId, { type: 'status', status: msg.status }, socket); + } else if (msg.type === 'clear') { + room.messages = []; + broadcastToRoom(roomId, { type: 'clear' }); + } + } catch (e) { + console.error('[Server] Chat parse error:', e); + } + }); + + socket.on('close', () => { + room.clients.delete(socket); + console.log(`[Server] Chat client left "${roomId}" (${room.clients.size} clients)`); + }); + }); + + // Start server + await fastify.listen({ port, host: '0.0.0.0' }); + + console.log('[Server] ' + '='.repeat(50)); + console.log(`[Server] Listening at http://0.0.0.0:${port}`); + console.log(`[Server] Collaboration: ws://localhost:${port}/collaboration/:documentId`); + console.log(`[Server] Chat: ws://localhost:${port}/chat/:roomId`); + console.log('[Server] ' + '='.repeat(50)); +} + +main().catch((err) => { + console.error('[Server] Fatal error:', err); + process.exit(1); +});