Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,159 changes: 469 additions & 690 deletions package-lock.json

Large diffs are not rendered by default.

341 changes: 56 additions & 285 deletions packages/ai/README.md

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
"name": "@superdoc-dev/ai",
"version": "0.1.5",
"description": "AI integration package for SuperDoc",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
Expand All @@ -14,11 +21,15 @@
"keywords": [
"superdoc",
"ai",
"ai-builder",
"ai-actions",
"document",
"collaboration",
"llm",
"openai",
"anthropic"
"anthropic",
"tools",
"structured-outputs"
],
"license": "AGPL-3.0",
"peerDependencies": {
Expand Down
193 changes: 193 additions & 0 deletions packages/ai/src/ai-builder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# SuperDoc AI Builder

Low-level primitives for building custom AI workflows with SuperDoc.

## Overview

AI Builder provides the foundational components for creating AI-powered document editing experiences. Unlike AI Actions which offers pre-built operations, AI Builder gives you the tools to build custom workflows tailored to your specific needs.

**Alpha Status:** Currently supports Anthropic Claude only.

## Architecture

```
ai-builder/
├── types.ts # Core type definitions
├── executor.ts # Tool execution primitive (executeTool)
├── tools/ # Core tool implementations
│ ├── insertContent.ts
│ └── replaceContent.ts
├── providers/ # Provider-specific tool schemas
│ └── anthropic.ts # Anthropic Claude support
└── schema-generator/ # Schema generation from extensions
└── from-extensions.ts
```

## Quick Start

```typescript
import { executeTool, anthropicTools } from '@superdoc-dev/ai';
import Anthropic from '@anthropic-ai/sdk';

// Get tool definitions
const tools = anthropicTools(editor.extensionManager.extensions);

// Use with Anthropic SDK
const anthropic = new Anthropic({ apiKey: '...' });
const response = await anthropic.beta.messages.create({
model: 'claude-sonnet-4-5',
betas: ['structured-outputs-2025-11-13'],
tools,
messages: [{ role: 'user', content: 'Add a paragraph saying hello' }],
});

// Execute tool calls
for (const toolUse of response.content.filter((c) => c.type === 'tool_use')) {
await executeTool(toolUse.name, toolUse.input, editor);
}
```

## Core Concepts

### Tools

Tools are the basic operations that AI can perform on documents:

- **insertContent** - Insert content at selection, documentStart, or documentEnd
- **replaceContent** - Replace content in a specific range

Each tool executes with type-safe parameters and returns a structured result.

### Tool Execution

```typescript
import { executeTool } from '@superdoc-dev/ai';

// Execute a single tool
const result = await executeTool(
'insertContent',
{
position: 'selection',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }],
},
editor,
);

if (result.success) {
console.log('Content inserted successfully');
}
```

### Schema Generation

Generate tool definitions compatible with Anthropic Claude:

```typescript
import { anthropicTools } from '@superdoc-dev/ai';

// Get tool definitions from extensions
const tools = anthropicTools(editor.extensionManager.extensions, {
excludedNodes: ['bulletList', 'orderedList'],
excludedMarks: [],
strict: true,
});
```

## Available Tools

### insertContent

Insert content at a specific position.

**Parameters:**

- `position`: 'selection' | 'documentStart' | 'documentEnd'
- `content`: Array of ProseMirror nodes

```typescript
await executeTool(
'insertContent',
{
position: 'selection',
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Hello World' }],
},
],
},
editor,
);
```

### replaceContent

Replace content in a specific range.

**Parameters:**

- `from`: Start position (number)
- `to`: End position (number)
- `content`: Array of ProseMirror nodes

```typescript
await executeTool(
'replaceContent',
{
from: 0,
to: 100,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'New content' }],
},
],
},
editor,
);
```

## Anthropic Integration

AI Builder is optimized for Anthropic Claude with structured outputs:

```typescript
import { anthropicTools, executeTool } from '@superdoc-dev/ai';
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const tools = anthropicTools(editor.extensionManager.extensions);

async function processUserRequest(userMessage: string) {
const response = await anthropic.beta.messages.create({
model: 'claude-sonnet-4-5',
betas: ['structured-outputs-2025-11-13'],
tools,
messages: [{ role: 'user', content: userMessage }],
});

for (const block of response.content) {
if (block.type === 'tool_use') {
const result = await executeTool(block.name, block.input, editor);
console.log(`Tool ${block.name}:`, result.success ? 'Success' : 'Failed');
}
}
}

await processUserRequest('Add a paragraph saying "Hello World"');
```

## Comparison with AI Actions

| Feature | AI Builder | AI Actions |
| --------------- | -------------------- | -------------------- |
| **Use Case** | Custom workflows | Pre-built operations |
| **Complexity** | Low-level primitives | High-level methods |
| **Flexibility** | Maximum | Fixed operations |
| **Setup** | More code required | Minimal setup |
| **Best For** | Advanced use cases | Quick integration |

## Related

- [AI Actions](../ai-actions.ts) - High-level AI operations
- [SuperDoc Docs](https://docs.superdoc.dev/ai/ai-builder/overview)
118 changes: 118 additions & 0 deletions packages/ai/src/ai-builder/__tests__/helpers/enrichContent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, expect } from 'vitest';
import { enrichParagraphNodes } from '../../helpers/enrichContent';

describe('enrichParagraphNodes', () => {
it('should add default spacing to paragraph nodes without spacing', () => {
const input = [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }];

const result = enrichParagraphNodes(input);

expect(result[0].attrs).toBeDefined();
expect(result[0].attrs.spacing).toEqual({
after: null,
before: null,
line: null,
lineRule: 'auto',
});
});

it('should preserve existing spacing attributes', () => {
const input = [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Hello' }],
attrs: {
spacing: { after: 100, before: 50, line: 120, lineRule: 'exact' },
},
},
];

const result = enrichParagraphNodes(input);

expect(result[0].attrs.spacing).toEqual({
after: 100,
before: 50,
line: 120,
lineRule: 'exact',
});
});

it('should preserve other attrs while adding spacing', () => {
const input = [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Hello' }],
attrs: { styleId: 'Heading1' },
},
];

const result = enrichParagraphNodes(input);

expect(result[0].attrs.styleId).toBe('Heading1');
expect(result[0].attrs.spacing).toEqual({
after: null,
before: null,
line: null,
lineRule: 'auto',
});
});

it('should not affect non-paragraph nodes', () => {
const input = [
{ type: 'heading', level: 1, content: [{ type: 'text', text: 'Title' }] },
{ type: 'paragraph', content: [{ type: 'text', text: 'Content' }] },
];

const result = enrichParagraphNodes(input);

// Heading should remain unchanged
expect(result[0].type).toBe('heading');
expect(result[0].attrs).toBeUndefined();

// Paragraph should have spacing added
expect(result[1].type).toBe('paragraph');
expect(result[1].attrs.spacing).toBeDefined();
});

it('should handle empty arrays', () => {
const result = enrichParagraphNodes([]);
expect(result).toEqual([]);
});

it('should handle non-array input gracefully', () => {
const result = enrichParagraphNodes(null as any);
expect(result).toBeNull();
});

it('should not mutate original nodes', () => {
const input = [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }];

const original = JSON.parse(JSON.stringify(input));
const result = enrichParagraphNodes(input);

// Original should be unchanged
expect(input).toEqual(original);

// Result should have spacing
expect(result[0].attrs.spacing).toBeDefined();
});

it('should handle multiple paragraph nodes', () => {
const input = [
{ type: 'paragraph', content: [{ type: 'text', text: 'First' }] },
{ type: 'paragraph', content: [{ type: 'text', text: 'Second' }] },
{ type: 'paragraph', content: [{ type: 'text', text: 'Third' }] },
];

const result = enrichParagraphNodes(input);

result.forEach((node) => {
expect(node.attrs.spacing).toEqual({
after: null,
before: null,
line: null,
lineRule: 'auto',
});
});
});
});
Loading