From 4a78ca2fc90ca321ef3e04fef74d6e4b28939017 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 8 Mar 2026 18:58:33 -0300 Subject: [PATCH 1/9] feat(sdk): add AI integration examples and docs for Bedrock, Vertex, Vercel AI, LangChain Add minimal runnable examples (Node.js + Python) for four AI integrations: - AWS Bedrock: Converse API with tool use - Google Vertex AI: Gemini with function calling (includes schema sanitizer for const keyword) - Vercel AI SDK: generateText with automatic agentic loop - LangChain: LangGraph ReAct agent Also adds a full contract review demo and an Integrations docs page split into Cloud Platforms and Agent Frameworks sections. --- apps/docs/docs.json | 5 + .../ai-agents/integrations.mdx | 511 ++++++++++++++++++ apps/docs/scripts/validate-code-imports.ts | 5 + examples/README.md | 24 + examples/ai/README.md | 45 ++ examples/ai/bedrock/README.md | 41 ++ examples/ai/bedrock/contract.docx | Bin 0 -> 13854 bytes examples/ai/bedrock/index.py | 109 ++++ examples/ai/bedrock/index.ts | 109 ++++ examples/ai/bedrock/package.json | 15 + examples/ai/langchain/README.md | 51 ++ examples/ai/langchain/contract.docx | Bin 0 -> 79086 bytes examples/ai/langchain/index.py | 75 +++ examples/ai/langchain/index.ts | 72 +++ examples/ai/langchain/package.json | 18 + examples/ai/vercel-ai/README.md | 41 ++ examples/ai/vercel-ai/contract.docx | Bin 0 -> 79086 bytes examples/ai/vercel-ai/index.ts | 64 +++ examples/ai/vercel-ai/package.json | 17 + examples/ai/vertex/README.md | 42 ++ examples/ai/vertex/contract.docx | Bin 0 -> 79086 bytes examples/ai/vertex/index.py | 122 +++++ examples/ai/vertex/index.ts | 133 +++++ examples/ai/vertex/package.json | 15 + 24 files changed, 1514 insertions(+) create mode 100644 apps/docs/document-engine/ai-agents/integrations.mdx create mode 100644 examples/ai/README.md create mode 100644 examples/ai/bedrock/README.md create mode 100644 examples/ai/bedrock/contract.docx create mode 100644 examples/ai/bedrock/index.py create mode 100644 examples/ai/bedrock/index.ts create mode 100644 examples/ai/bedrock/package.json create mode 100644 examples/ai/langchain/README.md create mode 100644 examples/ai/langchain/contract.docx create mode 100644 examples/ai/langchain/index.py create mode 100644 examples/ai/langchain/index.ts create mode 100644 examples/ai/langchain/package.json create mode 100644 examples/ai/vercel-ai/README.md create mode 100644 examples/ai/vercel-ai/contract.docx create mode 100644 examples/ai/vercel-ai/index.ts create mode 100644 examples/ai/vercel-ai/package.json create mode 100644 examples/ai/vertex/README.md create mode 100644 examples/ai/vertex/contract.docx create mode 100644 examples/ai/vertex/index.py create mode 100644 examples/ai/vertex/index.ts create mode 100644 examples/ai/vertex/package.json diff --git a/apps/docs/docs.json b/apps/docs/docs.json index af1f72e509..0d442c4d47 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -115,6 +115,7 @@ "group": "AI Agents", "pages": [ "document-engine/ai-agents/llm-tools", + "document-engine/ai-agents/integrations", "document-engine/ai-agents/skills", "document-engine/ai-agents/mcp-server" ] @@ -318,6 +319,10 @@ "source": "/document-engine/mcp", "destination": "/document-engine/ai-agents/mcp-server" }, + { + "source": "/document-engine/ai-agents/providers", + "destination": "/document-engine/ai-agents/integrations" + }, { "source": "/core/superdoc/properties", "destination": "/core/superdoc/methods#properties" diff --git a/apps/docs/document-engine/ai-agents/integrations.mdx b/apps/docs/document-engine/ai-agents/integrations.mdx new file mode 100644 index 0000000000..e4282e101c --- /dev/null +++ b/apps/docs/document-engine/ai-agents/integrations.mdx @@ -0,0 +1,511 @@ +--- +title: Integrations +sidebarTitle: Integrations +tag: NEW +description: Connect SuperDoc tools to AWS Bedrock, Google Vertex AI, Vercel AI SDK, LangChain, and more +keywords: "ai integrations, ai providers, agent frameworks, aws bedrock, google vertex, vercel ai, langchain, openai, anthropic, tool use, function calling, superdoc sdk" +--- + +SuperDoc tools work with any LLM provider or agent framework that supports tool use. The SDK ships tool definitions in multiple formats — pick the one that matches your stack, write a conversation loop (or let the framework handle it), and dispatch tool calls through the SDK. + +Each example below opens a document, gives the model SuperDoc tools, and lets it review and edit the content. + + +LLM tools are in alpha. Tool names and schemas may change between releases. + + +## Cloud platforms + +Use SuperDoc tools with cloud AI platforms. You write the agentic loop and control the conversation directly. + + + + + + ```bash + npm install @superdoc-dev/sdk @aws-sdk/client-bedrock-runtime + ``` + + ```typescript + import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; + import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: './contract.docx' }); + + // Anthropic format → Bedrock toolSpec (3-line conversion) + const { tools } = await chooseTools({ provider: 'anthropic' }); + const toolConfig = { + tools: tools.map((t) => ({ + toolSpec: { + name: t.name, + description: t.description, + inputSchema: { json: t.input_schema }, + }, + })), + }; + + const bedrock = new BedrockRuntimeClient({ region: 'us-east-1' }); + const messages = [ + { role: 'user', content: [{ text: 'Review this contract.' }] }, + ]; + + while (true) { + const res = await bedrock.send(new ConverseCommand({ + modelId: 'amazon.nova-pro-v1:0', + messages, + system: [{ text: 'You edit .docx files using SuperDoc tools.' }], + toolConfig, + })); + + const output = res.output?.message; + if (!output) break; + messages.push(output); + + const toolUses = output.content?.filter((b) => b.toolUse) ?? []; + if (!toolUses.length) break; + + const results = []; + for (const block of toolUses) { + const { name, input, toolUseId } = block.toolUse; + const result = await dispatchSuperDocTool(client, name, input ?? {}); + // Bedrock requires json to be a plain object — wrap if needed + const json = (typeof result === 'object' && result !== null && !Array.isArray(result)) + ? result : { result }; + results.push({ toolResult: { toolUseId, content: [{ json }] } }); + } + messages.push({ role: 'user', content: results }); + } + + await client.doc.save({ inPlace: true }); + await client.dispose(); + ``` + + + ```bash + pip install superdoc-sdk boto3 + ``` + + ```python + import boto3 + from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool + + client = SuperDocClient() + client.connect() + client.doc.open(doc="./contract.docx") + + # Anthropic format → Bedrock toolSpec + result = choose_tools(provider="anthropic") + tool_config = { + "tools": [ + {"toolSpec": {"name": t["name"], "description": t["description"], + "inputSchema": {"json": t["input_schema"]}}} + for t in result["tools"] + ] + } + + bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") + messages = [{"role": "user", "content": [{"text": "Review this contract."}]}] + + while True: + response = bedrock.converse( + modelId="amazon.nova-pro-v1:0", + messages=messages, + system=[{"text": "You edit .docx files using SuperDoc tools."}], + toolConfig=tool_config, + ) + + output = response["output"]["message"] + messages.append(output) + + tool_uses = [b for b in output.get("content", []) if "toolUse" in b] + if not tool_uses: + break + + tool_results = [] + for block in tool_uses: + tu = block["toolUse"] + result = dispatch_superdoc_tool(client, tu["name"], tu.get("input", {})) + tool_results.append( + {"toolResult": {"toolUseId": tu["toolUseId"], "content": [{"json": result}]}} + ) + messages.append({"role": "user", "content": tool_results}) + + client.doc.save(in_place=True) + client.dispose() + ``` + + + + **Auth**: AWS credentials via `aws configure`, env vars, or IAM role. No API key needed. + + + + + + ```bash + npm install @superdoc-dev/sdk @google-cloud/vertexai + ``` + + ```typescript + import { VertexAI } from '@google-cloud/vertexai'; + import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: './contract.docx' }); + + // Vertex doesn't support all JSON Schema keywords — strip unsupported ones + function sanitizeSchema(obj) { + if (Array.isArray(obj)) return obj.map(sanitizeSchema); + if (typeof obj !== 'object' || obj === null) return obj; + const result = {}; + for (const [k, v] of Object.entries(obj)) { + if (k === 'const') continue; + result[k] = sanitizeSchema(v); + } + return result; + } + + // Generic format → Vertex function declarations + const { tools } = await chooseTools({ provider: 'generic' }); + const vertexTools = [{ + functionDeclarations: tools.map((t) => ({ + name: t.name, + description: t.description, + parameters: sanitizeSchema(t.parameters), + })), + }]; + + const vertexAI = new VertexAI({ project: 'your-project', location: 'us-central1' }); + const model = vertexAI.getGenerativeModel({ + model: 'gemini-2.5-pro', + tools: vertexTools, + systemInstruction: { role: 'system', parts: [{ text: 'You edit .docx files using SuperDoc tools.' }] }, + }); + + const chat = model.startChat(); + let response = await chat.sendMessage([{ text: 'Review this contract.' }]); + + while (true) { + const parts = response.response.candidates?.[0]?.content.parts ?? []; + const calls = parts.filter((p) => p.functionCall); + if (!calls.length) break; + + const results = []; + for (const part of calls) { + const { name, args } = part.functionCall; + const result = await dispatchSuperDocTool(client, name, args ?? {}); + results.push({ functionResponse: { name, response: result } }); + } + response = await chat.sendMessage(results); + } + + await client.doc.save({ inPlace: true }); + await client.dispose(); + ``` + + + ```bash + pip install superdoc-sdk google-cloud-aiplatform + ``` + + ```python + import vertexai + from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration, Part + from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool + + client = SuperDocClient() + client.connect() + client.doc.open(doc="./contract.docx") + + # Vertex doesn't support all JSON Schema keywords — strip unsupported ones + def sanitize_schema(obj): + if isinstance(obj, list): return [sanitize_schema(i) for i in obj] + if isinstance(obj, dict): return {k: sanitize_schema(v) for k, v in obj.items() if k != "const"} + return obj + + # Generic format → Vertex function declarations + result = choose_tools(provider="generic") + vertex_tools = [Tool(function_declarations=[ + FunctionDeclaration(name=t["name"], description=t["description"], parameters=sanitize_schema(t["parameters"])) + for t in result["tools"] + ])] + + vertexai.init(project="your-project", location="us-central1") + model = GenerativeModel( + "gemini-2.5-pro", + tools=vertex_tools, + system_instruction="You edit .docx files using SuperDoc tools.", + ) + chat = model.start_chat() + response = chat.send_message("Review this contract.") + + while True: + calls = [p for p in response.candidates[0].content.parts if p.function_call.name] + if not calls: + break + + responses = [] + for part in calls: + name = part.function_call.name + args = dict(part.function_call.args) if part.function_call.args else {} + result = dispatch_superdoc_tool(client, name, args) + responses.append(Part.from_function_response(name=name, response=result)) + response = chat.send_message(responses) + + client.doc.save(in_place=True) + client.dispose() + ``` + + + + **Auth**: `gcloud auth application-default login` or a service account key. + + + +## Agent frameworks + +Use SuperDoc tools with agent frameworks. The framework manages the agentic loop — you configure tools and let it run. + + + + ```bash + npm install @superdoc-dev/sdk ai @ai-sdk/openai zod + ``` + + ```typescript + import { generateText, tool } from 'ai'; + import { openai } from '@ai-sdk/openai'; + import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + import { z } from 'zod'; + + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: './contract.docx' }); + + const { tools: sdTools } = await chooseTools({ provider: 'vercel' }); + + // Wrap as Vercel AI tool() objects + const tools = {}; + for (const t of sdTools) { + tools[t.function.name] = tool({ + description: t.function.description, + parameters: z.object({}).passthrough(), + execute: async (args) => + dispatchSuperDocTool(client, t.function.name, args), + }); + } + + // generateText handles the agentic loop automatically + const result = await generateText({ + model: openai('gpt-4o'), + system: 'You edit .docx files using SuperDoc tools.', + prompt: 'Review this contract.', + tools, + maxSteps: 20, + }); + + console.log(result.text); + await client.doc.save({ inPlace: true }); + await client.dispose(); + ``` + + **Auth**: `OPENAI_API_KEY` env var. Swap `openai(...)` for `anthropic(...)`, `google(...)`, etc. + + + + + + ```bash + npm install @superdoc-dev/sdk @langchain/openai @langchain/core @langchain/langgraph zod + ``` + + ```typescript + import { ChatOpenAI } from '@langchain/openai'; + import { DynamicStructuredTool } from '@langchain/core/tools'; + import { createReactAgent } from '@langchain/langgraph/prebuilt'; + import { HumanMessage } from '@langchain/core/messages'; + import { z } from 'zod'; + import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: './contract.docx' }); + + const { tools: sdTools } = await chooseTools({ provider: 'generic' }); + + // Wrap as LangChain DynamicStructuredTool objects + const tools = sdTools.map( + (t) => new DynamicStructuredTool({ + name: t.name, + description: t.description, + schema: z.object({}).passthrough(), + func: async (args) => { + const result = await dispatchSuperDocTool(client, t.name, args); + return JSON.stringify(result); + }, + }), + ); + + const agent = createReactAgent({ + llm: new ChatOpenAI({ model: 'gpt-4o' }), + tools, + prompt: 'You edit .docx files using SuperDoc tools.', + }); + + const result = await agent.invoke({ + messages: [new HumanMessage('Review this contract.')], + }); + + console.log(result.messages.at(-1).content); + await client.doc.save({ inPlace: true }); + await client.dispose(); + ``` + + + ```bash + pip install superdoc-sdk langchain-openai langgraph + ``` + + ```python + import json + from langchain_openai import ChatOpenAI + from langchain_core.tools import StructuredTool + from langgraph.prebuilt import create_react_agent + from langchain_core.messages import HumanMessage + from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool + + client = SuperDocClient() + client.connect() + client.doc.open(doc="./contract.docx") + + result = choose_tools(provider="generic") + + # Wrap as LangChain StructuredTool objects + def make_tool(t): + def invoke(args: dict) -> str: + return json.dumps(dispatch_superdoc_tool(client, t["name"], args)) + return StructuredTool.from_function( + func=invoke, name=t["name"], description=t["description"], + ) + + tools = [make_tool(t) for t in result["tools"]] + + agent = create_react_agent( + model=ChatOpenAI(model="gpt-4o"), + tools=tools, + prompt="You edit .docx files using SuperDoc tools.", + ) + + result = agent.invoke( + {"messages": [HumanMessage(content="Review this contract.")]} + ) + print(result["messages"][-1].content) + + client.doc.save(in_place=True) + client.dispose() + ``` + + + + **Auth**: `OPENAI_API_KEY` env var. Swap `ChatOpenAI` for `ChatAnthropic`, `ChatGoogleGenerativeAI`, etc. + + + +## Tool format reference + +The SDK ships pre-formatted tools for each integration. The conversion is minimal: + +| Integration | Type | SDK format | Native shape | +|-------------|------|-----------|--------------| +| AWS Bedrock | Cloud platform | `anthropic` | Wrap in `{ toolSpec: { name, description, inputSchema: { json } } }` | +| Google Vertex AI | Cloud platform | `generic` | Use `{ name, description, parameters }` as `functionDeclarations` | +| Vercel AI SDK | Framework | `vercel` | Wrap in Vercel `tool()` with `z.object({}).passthrough()` | +| LangChain | Framework | `generic` | Wrap in `DynamicStructuredTool` | +| OpenAI | Direct API | `openai` | Pass directly — already in OpenAI format | +| Anthropic | Direct API | `anthropic` | Pass directly — already in Anthropic format | + +## The discover_tools pattern + +All provider examples above use **essential mode** (default) — 5 core tools plus `discover_tools`. When the model needs more tools (comments, formatting, tables, etc.), it calls `discover_tools` to load them dynamically. + +Handle this in your agentic loop by merging new tools into the conversation: + +```typescript +const result = await dispatchSuperDocTool(client, name, args); + +if (name === 'discover_tools' && result.tools) { + // Merge newly discovered tools into your provider's tool config + for (const t of result.tools) { + // ... add to your tools array in the provider's format + } +} +``` + +See the [LLM Tools guide](/document-engine/ai-agents/llm-tools#the-discover_tools-pattern) for details. + +## Tracked changes + +For contract review workflows, you typically want all edits to appear as tracked changes so a human can accept or reject them. Two approaches: + +### Instruct the model + +Tell the model to use `changeMode: "tracked"` in its `apply_mutations` calls: + +```markdown +## System prompt +All edits must use changeMode: "tracked" so they appear as +tracked changes for human review. +``` + +### Use the headless editor + +If you don't need agentic tool use — just want the model to suggest edits — use the headless editor with `documentMode: 'suggesting'`: + +```typescript +import { Editor } from 'superdoc/super-editor'; +import { readFile, writeFile } from 'node:fs/promises'; + +const docx = await readFile('./contract.docx'); +const editor = await Editor.open(docx, { documentMode: 'suggesting' }); + +// Get suggestions from your LLM (structured output, no tool use) +const suggestions = await getSuggestions(editor.state.doc.textContent); + +// Apply each suggestion as a tracked change +for (const s of suggestions) { + const matches = editor.commands.search(s.find, { highlight: false }); + if (!matches.length) continue; + + editor.commands.insertTrackedChange({ + from: matches[0].from, + to: matches[0].to, + text: s.replace, + user: { name: 'AI Reviewer', email: 'ai@example.com' }, + }); +} + +const result = await editor.exportDocx(); +await writeFile('./reviewed.docx', Buffer.from(result)); +editor.destroy(); +``` + +## Best practices + +- **Start with essential mode.** Load 5 tools + `discover_tools`. The model loads more groups when needed. This keeps token usage low. +- **Use `apply_mutations` for text edits.** It batches multiple rewrites in one call, reducing round trips. +- **Feed errors back.** When a tool call fails, return the error as a tool result. Most models self-correct on the next turn. +- **Pin your model version.** Use a specific model ID rather than an alias to avoid behavior changes between releases. + +## Example repository + +Complete, runnable examples for all cloud platforms and frameworks (Node.js and Python) are available at [`examples/ai/`](https://github.com/superdoc-dev/superdoc/tree/main/examples/ai). + +## Related + +- [LLM Tools](/document-engine/ai-agents/llm-tools) — tool selection, dispatch, and the full API +- [Skills](/document-engine/ai-agents/skills) — reusable prompt templates +- [MCP Server](/document-engine/ai-agents/mcp-server) — Model Context Protocol integration +- [SDKs](/document-engine/sdks) — typed Node.js and Python wrappers diff --git a/apps/docs/scripts/validate-code-imports.ts b/apps/docs/scripts/validate-code-imports.ts index 0601c995ca..81fceaee00 100644 --- a/apps/docs/scripts/validate-code-imports.ts +++ b/apps/docs/scripts/validate-code-imports.ts @@ -52,6 +52,8 @@ const EXACT_EXTERNAL_IMPORTS = new Set([ 'cors', 'pg', 'ioredis', + 'ai', + 'zod', ]); const PREFIX_EXTERNAL_IMPORTS = [ @@ -64,6 +66,9 @@ const PREFIX_EXTERNAL_IMPORTS = [ '@liveblocks/', '@fastify/', '@aws-sdk/', + '@ai-sdk/', + '@google-cloud/', + '@langchain/', 'next/', ]; diff --git a/examples/README.md b/examples/README.md index 1898c019fb..90736c6dcd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -22,6 +22,30 @@ Minimal, self-contained examples showing how to use SuperDoc. | [collaboration](./collaboration) | Real-time editing with various Yjs providers | [Guides](https://docs.superdoc.dev/guides) | | [headless](./headless) | Server-side AI redlining with Node.js | [AI Agents](https://docs.superdoc.dev/getting-started/ai-agents) | +## AI Integrations + +Connect SuperDoc's Document Engine to cloud AI platforms and agent frameworks. + +### Cloud Platforms + +| Integration | Description | Docs | +|-------------|-------------|------| +| [AWS Bedrock](./ai/bedrock) | Bedrock Converse API with tool use | [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) | +| [Google Vertex AI](./ai/vertex) | Gemini with function calling | [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) | + +### Agent Frameworks + +| Integration | Description | Docs | +|-------------|-------------|------| +| [Vercel AI SDK](./ai/vercel-ai) | Any model via the Vercel AI SDK | [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) | +| [LangChain](./ai/langchain) | LangGraph ReAct agent | [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) | + +### Demos + +| Example | Description | Docs | +|---------|-------------|------| +| [Contract Review](./ai/contract-review) | Full demo: agentic + headless contract review | [AI Agents](https://docs.superdoc.dev/getting-started/ai-agents) | + ## Running an example ```bash diff --git a/examples/ai/README.md b/examples/ai/README.md new file mode 100644 index 0000000000..c1091a93f5 --- /dev/null +++ b/examples/ai/README.md @@ -0,0 +1,45 @@ +# AI Integration Examples + +Give LLMs structured access to document operations. Each example connects SuperDoc's Document Engine to a cloud AI platform or agent framework — open a doc, let the model review and edit it with tools, save the result. + +**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) · [LLM Tools](https://docs.superdoc.dev/document-engine/ai-agents/llm-tools) + +## Cloud platforms + +You write the agentic loop and control the conversation directly. + +| Platform | Node.js | Python | Auth | +|----------|---------|--------|------| +| [AWS Bedrock](./bedrock) | `index.ts` | `index.py` | AWS credentials (`aws configure`) | +| [Google Vertex AI](./vertex) | `index.ts` | `index.py` | Google Cloud credentials (`gcloud auth application-default login`) | + +## Agent frameworks + +The framework manages the agentic loop — you configure tools and let it run. + +| Framework | Node.js | Python | Auth | +|-----------|---------|--------|------| +| [Vercel AI SDK](./vercel-ai) | `index.ts` | — | `OPENAI_API_KEY` | +| [LangChain](./langchain) | `index.ts` | `index.py` | `OPENAI_API_KEY` | + +## Full demo + +| Example | Description | Docs | +|---------|-------------|------| +| [contract-review](./contract-review) | AI-powered contract review with agentic and headless patterns | [AI Agents](https://docs.superdoc.dev/getting-started/ai-agents) | + +## Run + +```bash +# Node.js +cd bedrock +npm install +npx tsx index.ts contract.docx reviewed.docx + +# Python +cd bedrock +pip install superdoc-sdk boto3 +python index.py contract.docx reviewed.docx +``` + +Each integration needs different dependencies — see the README in each directory. diff --git a/examples/ai/bedrock/README.md b/examples/ai/bedrock/README.md new file mode 100644 index 0000000000..f5ff1ee405 --- /dev/null +++ b/examples/ai/bedrock/README.md @@ -0,0 +1,41 @@ +# SuperDoc + AWS Bedrock + +Agentic document editing using the Bedrock Converse API. + +**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) + +## Prerequisites + +- AWS credentials configured (`aws configure` or environment variables) +- Bedrock model access enabled in the [AWS console](https://console.aws.amazon.com/bedrock/) + +## Run + +### Node.js + +```bash +npm install +npx tsx index.ts contract.docx reviewed.docx +``` + +### Python + +```bash +pip install superdoc-sdk boto3 +python index.py contract.docx reviewed.docx +``` + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `AWS_REGION` | `us-east-1` | AWS region with Bedrock access | +| `BEDROCK_MODEL_ID` | `amazon.nova-pro-v1:0` | Any Bedrock model that supports tool use | + +## How it works + +1. Connects to SuperDoc via the SDK +2. Loads tool definitions in Anthropic format — the same format Bedrock's Converse API expects +3. Converts to Bedrock's `toolSpec` shape (3-line mapping) +4. Runs an agentic loop: the model calls SuperDoc tools to read, query, and edit the document +5. Saves the reviewed document diff --git a/examples/ai/bedrock/contract.docx b/examples/ai/bedrock/contract.docx new file mode 100644 index 0000000000000000000000000000000000000000..68f2475b14b9e3a851292697e2b924519dfadaf4 GIT binary patch literal 13854 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 I3_rj9A1!OL>;M1& literal 0 HcmV?d00001 diff --git a/examples/ai/bedrock/index.py b/examples/ai/bedrock/index.py new file mode 100644 index 0000000000..1e21c7e0cf --- /dev/null +++ b/examples/ai/bedrock/index.py @@ -0,0 +1,109 @@ +""" +SuperDoc + AWS Bedrock + +Minimal agentic loop: Claude on Bedrock uses SuperDoc tools +to review and edit a Word document. + +Usage: python index.py [input.docx] [output.docx] + +Requires: + - pip install superdoc-sdk boto3 + - AWS credentials configured (aws configure, env vars, or IAM role) +""" + +import sys +import os +import json +import boto3 +from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool + +MODEL_ID = os.environ.get("BEDROCK_MODEL_ID", "amazon.nova-pro-v1:0") +REGION = os.environ.get("AWS_REGION", "us-east-1") + + +def to_bedrock_tools(anthropic_tools): + """Convert SuperDoc Anthropic-format tools to Bedrock toolSpec shape.""" + return [ + { + "toolSpec": { + "name": t["name"], + "description": t["description"], + "inputSchema": {"json": t["input_schema"]}, + } + } + for t in anthropic_tools + ] + + +def main(): + args = sys.argv[1:] + input_path = args[0] if args else "contract.docx" + output_path = args[1] if len(args) > 1 else "reviewed.docx" + + # 1. Connect to SuperDoc + client = SuperDocClient() + client.connect() + client.doc.open(doc=input_path) + + # 2. Get tools in Anthropic format (Bedrock-compatible) + result = choose_tools(provider="anthropic") + tool_config = {"tools": to_bedrock_tools(result["tools"])} + + # 3. Agentic loop + bedrock = boto3.client("bedrock-runtime", region_name=REGION) + messages = [ + {"role": "user", "content": [{"text": "Review this contract. Fix vague language and one-sided terms."}]} + ] + + for _ in range(20): + response = bedrock.converse( + modelId=MODEL_ID, + messages=messages, + system=[{"text": "You edit .docx files using SuperDoc tools. Use tracked changes for all edits."}], + toolConfig=tool_config, + ) + + output = response["output"]["message"] + messages.append(output) + + tool_uses = [b for b in output.get("content", []) if "toolUse" in b] + if not tool_uses: + # Print final response + for b in output.get("content", []): + if "text" in b: + print(b["text"]) + break + + tool_results = [] + for block in tool_uses: + tool_use = block["toolUse"] + name = tool_use["name"] + print(f" Tool: {name}") + + try: + result = dispatch_superdoc_tool(client, name, tool_use.get("input", {})) + + # discover_tools returns new tools — merge them + if name == "discover_tools" and "tools" in result: + tool_config["tools"].extend(to_bedrock_tools(result["tools"])) + + # Bedrock requires json content to be a plain dict + json_result = result if isinstance(result, dict) else {"result": result} + tool_results.append( + {"toolResult": {"toolUseId": tool_use["toolUseId"], "content": [{"json": json_result}]}} + ) + except Exception as e: + tool_results.append( + {"toolResult": {"toolUseId": tool_use["toolUseId"], "content": [{"text": f"Error: {e}"}], "status": "error"}} + ) + + messages.append({"role": "user", "content": tool_results}) + + # 4. Save + client.doc.save(doc=output_path) + client.dispose() + print(f"\nSaved to {output_path}") + + +if __name__ == "__main__": + main() diff --git a/examples/ai/bedrock/index.ts b/examples/ai/bedrock/index.ts new file mode 100644 index 0000000000..4a777a6b78 --- /dev/null +++ b/examples/ai/bedrock/index.ts @@ -0,0 +1,109 @@ +/** + * SuperDoc + AWS Bedrock + * + * Minimal agentic loop: Claude on Bedrock uses SuperDoc tools + * to review and edit a Word document. + * + * Usage: npx tsx index.ts [input.docx] [output.docx] + * + * Requires: AWS credentials configured, Bedrock model access enabled. + */ + +import { + BedrockRuntimeClient, + ConverseCommand, + type ContentBlock, + type Message, + type Tool, +} from '@aws-sdk/client-bedrock-runtime'; +import { + createSuperDocClient, + chooseTools, + dispatchSuperDocTool, +} from '@superdoc-dev/sdk'; + +const MODEL_ID = process.env.BEDROCK_MODEL_ID ?? 'amazon.nova-pro-v1:0'; +const REGION = process.env.AWS_REGION ?? 'us-east-1'; + +async function main() { + const [inputPath = 'contract.docx', outputPath = 'reviewed.docx'] = process.argv.slice(2); + + // 1. Connect to SuperDoc and open the document + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: inputPath }); + + // 2. Get tools in Anthropic format (Bedrock-compatible) and convert to toolSpec shape + const { tools: sdTools } = await chooseTools({ provider: 'anthropic' }); + const toolConfig = { + tools: (sdTools as Array<{ name: string; description: string; input_schema: Record }>).map( + (t): Tool => ({ + toolSpec: { + name: t.name, + description: t.description, + inputSchema: { json: t.input_schema }, + }, + }), + ), + }; + + // 3. Agentic loop + const bedrock = new BedrockRuntimeClient({ region: REGION }); + const messages: Message[] = [ + { role: 'user', content: [{ text: 'Review this contract. Fix vague language and one-sided terms.' }] }, + ]; + + for (let turn = 0; turn < 20; turn++) { + const response = await bedrock.send( + new ConverseCommand({ + modelId: MODEL_ID, + messages, + system: [{ text: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.' }], + toolConfig, + }), + ); + + const output = response.output?.message; + if (!output) break; + messages.push(output); + + const toolUses = (output.content ?? []).filter((b): b is ContentBlock.ToolUseMember => 'toolUse' in b); + if (!toolUses.length) { + // Print final response + for (const b of output.content ?? []) if ('text' in b) console.log(b.text); + break; + } + + const results: ContentBlock[] = []; + for (const block of toolUses) { + const { name, input, toolUseId } = block.toolUse!; + console.log(` Tool: ${name}`); + try { + const result = await dispatchSuperDocTool(client, name!, (input ?? {}) as Record); + + // discover_tools returns new tools — merge them into toolConfig + if (name === 'discover_tools') { + for (const t of (result as { tools?: any[] }).tools ?? []) { + toolConfig.tools.push({ toolSpec: { name: t.name, description: t.description, inputSchema: { json: t.input_schema } } }); + } + } + + // Bedrock requires json content to be a plain object + const jsonResult = (typeof result === 'object' && result !== null && !Array.isArray(result)) + ? result as Record + : { result }; + results.push({ toolResult: { toolUseId, content: [{ json: jsonResult }] } } as ContentBlock); + } catch (err) { + results.push({ toolResult: { toolUseId, content: [{ text: `Error: ${(err as Error).message}` }], status: 'error' } } as ContentBlock); + } + } + messages.push({ role: 'user', content: results }); + } + + // 4. Save + await client.doc.save({ doc: outputPath }); + await client.dispose(); + console.log(`\nSaved to ${outputPath}`); +} + +main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/bedrock/package.json b/examples/ai/bedrock/package.json new file mode 100644 index 0000000000..e93b95d91c --- /dev/null +++ b/examples/ai/bedrock/package.json @@ -0,0 +1,15 @@ +{ + "name": "superdoc-bedrock", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@aws-sdk/client-bedrock-runtime": "^3.750.0", + "@superdoc-dev/sdk": "latest" + }, + "devDependencies": { + "tsx": "^4.21.0" + } +} diff --git a/examples/ai/langchain/README.md b/examples/ai/langchain/README.md new file mode 100644 index 0000000000..469dd538ef --- /dev/null +++ b/examples/ai/langchain/README.md @@ -0,0 +1,51 @@ +# SuperDoc + LangChain + +Agentic document editing using a LangGraph ReAct agent. + +**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) + +## Prerequisites + +- `OPENAI_API_KEY` environment variable (or swap the model) + +## Run + +### Node.js + +```bash +npm install +OPENAI_API_KEY=sk-... npx tsx index.ts contract.docx reviewed.docx +``` + +### Python + +```bash +pip install superdoc-sdk langchain-openai langgraph +OPENAI_API_KEY=sk-... python index.py contract.docx reviewed.docx +``` + +## Configuration + +The example uses OpenAI by default. Swap the model class to use any LangChain-compatible provider: + +```python +# OpenAI (default) +from langchain_openai import ChatOpenAI +model = ChatOpenAI(model="gpt-4o") + +# Anthropic +from langchain_anthropic import ChatAnthropic +model = ChatAnthropic(model="claude-sonnet-4-6-20250514") + +# Google +from langchain_google_genai import ChatGoogleGenerativeAI +model = ChatGoogleGenerativeAI(model="gemini-2.5-pro") +``` + +## How it works + +1. Connects to SuperDoc via the SDK +2. Loads tool definitions in generic format and wraps them as LangChain `StructuredTool` / `DynamicStructuredTool` objects +3. Creates a ReAct agent with `create_react_agent` +4. The agent calls SuperDoc tools to read, query, and edit the document +5. Saves the reviewed document diff --git a/examples/ai/langchain/contract.docx b/examples/ai/langchain/contract.docx new file mode 100644 index 0000000000000000000000000000000000000000..5b911d339926dd9eebe45bddf9e99267496193fa GIT binary patch literal 79086 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 str: + print(f" Tool: {tool_def['name']}") + result = dispatch_superdoc_tool(client, tool_def["name"], args) + return json.dumps(result) + + return StructuredTool.from_function( + func=invoke, + name=tool_def["name"], + description=tool_def["description"], + ) + + +def main(): + args = sys.argv[1:] + input_path = args[0] if args else "contract.docx" + output_path = args[1] if len(args) > 1 else "reviewed.docx" + + # 1. Connect to SuperDoc + client = SuperDocClient() + client.connect() + client.doc.open(doc=input_path) + + # 2. Get tools in generic format and wrap as LangChain tools + result = choose_tools(provider="generic") + tools = [make_superdoc_tool(client, t) for t in result["tools"]] + + # 3. Create a ReAct agent + model = ChatOpenAI(model="gpt-4o") + agent = create_react_agent( + model=model, + tools=tools, + prompt="You edit .docx files using SuperDoc tools. Use tracked changes for all edits.", + ) + + # 4. Run the agent + result = agent.invoke( + {"messages": [HumanMessage(content="Review this contract. Fix vague language and one-sided terms.")]} + ) + + last_message = result["messages"][-1] + print(last_message.content) + + # 5. Save + client.doc.save(doc=output_path) + client.dispose() + print(f"\nSaved to {output_path}") + + +if __name__ == "__main__": + main() diff --git a/examples/ai/langchain/index.ts b/examples/ai/langchain/index.ts new file mode 100644 index 0000000000..4e3ce9b8cb --- /dev/null +++ b/examples/ai/langchain/index.ts @@ -0,0 +1,72 @@ +/** + * SuperDoc + LangChain + * + * Minimal agentic loop: any LangChain-compatible model uses SuperDoc tools + * to review and edit a Word document. + * + * Usage: OPENAI_API_KEY=sk-... npx tsx index.ts [input.docx] [output.docx] + * + * Requires: OPENAI_API_KEY (or swap ChatOpenAI for ChatAnthropic, ChatGoogleGenerativeAI, etc.) + */ + +import { ChatOpenAI } from '@langchain/openai'; +import { DynamicStructuredTool } from '@langchain/core/tools'; +import { createReactAgent } from '@langchain/langgraph/prebuilt'; +import { HumanMessage } from '@langchain/core/messages'; +import { z } from 'zod'; +import { + createSuperDocClient, + chooseTools, + dispatchSuperDocTool, +} from '@superdoc-dev/sdk'; + +async function main() { + const [inputPath = 'contract.docx', outputPath = 'reviewed.docx'] = process.argv.slice(2); + + // 1. Connect to SuperDoc + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: inputPath }); + + // 2. Get tools in generic format and wrap as LangChain tools + const { tools: sdTools } = await chooseTools({ provider: 'generic' }); + + const langchainTools = ( + sdTools as Array<{ name: string; description: string; parameters: Record }> + ).map( + (t) => + new DynamicStructuredTool({ + name: t.name, + description: t.description, + schema: z.object({}).passthrough(), // Accept any params — SuperDoc SDK validates + func: async (args) => { + console.log(` Tool: ${t.name}`); + const result = await dispatchSuperDocTool(client, t.name, args as Record); + return JSON.stringify(result); + }, + }), + ); + + // 3. Create a ReAct agent + const model = new ChatOpenAI({ model: 'gpt-4o' }); + const agent = createReactAgent({ + llm: model, + tools: langchainTools, + prompt: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.', + }); + + // 4. Run the agent + const result = await agent.invoke({ + messages: [new HumanMessage('Review this contract. Fix vague language and one-sided terms.')], + }); + + const lastMessage = result.messages[result.messages.length - 1]; + console.log(lastMessage.content); + + // 5. Save + await client.doc.save({ doc: outputPath }); + await client.dispose(); + console.log(`\nSaved to ${outputPath}`); +} + +main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/langchain/package.json b/examples/ai/langchain/package.json new file mode 100644 index 0000000000..af11d98746 --- /dev/null +++ b/examples/ai/langchain/package.json @@ -0,0 +1,18 @@ +{ + "name": "superdoc-langchain", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@langchain/openai": "^0.4.0", + "@langchain/core": "^0.3.0", + "@langchain/langgraph": "^0.2.0", + "@superdoc-dev/sdk": "latest", + "zod": "^3.23.0" + }, + "devDependencies": { + "tsx": "^4.21.0" + } +} diff --git a/examples/ai/vercel-ai/README.md b/examples/ai/vercel-ai/README.md new file mode 100644 index 0000000000..462dc3c051 --- /dev/null +++ b/examples/ai/vercel-ai/README.md @@ -0,0 +1,41 @@ +# SuperDoc + Vercel AI SDK + +Agentic document editing using the Vercel AI SDK. The cleanest integration — `generateText` handles the agentic loop automatically. + +**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) + +## Prerequisites + +- `OPENAI_API_KEY` environment variable (or swap the provider) + +## Run + +```bash +npm install +OPENAI_API_KEY=sk-... npx tsx index.ts contract.docx reviewed.docx +``` + +## Configuration + +The example uses OpenAI by default. Swap the provider import to use any model Vercel AI supports: + +```typescript +// OpenAI (default) +import { openai } from '@ai-sdk/openai'; +model: openai('gpt-4o') + +// Anthropic +import { anthropic } from '@ai-sdk/anthropic'; +model: anthropic('claude-sonnet-4-6-20250514') + +// Google +import { google } from '@ai-sdk/google'; +model: google('gemini-2.5-pro') +``` + +## How it works + +1. Connects to SuperDoc via the SDK +2. Loads tool definitions in Vercel format and wraps them as `tool()` objects +3. Calls `generateText` with `maxSteps: 20` — the SDK handles the tool call loop +4. Saves the reviewed document diff --git a/examples/ai/vercel-ai/contract.docx b/examples/ai/vercel-ai/contract.docx new file mode 100644 index 0000000000000000000000000000000000000000..5b911d339926dd9eebe45bddf9e99267496193fa GIT binary patch literal 79086 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 = {}; + for (const t of sdTools as Array<{ type: string; function: { name: string; description: string; parameters: Record } }>) { + const fn = t.function; + vercelTools[fn.name] = tool({ + description: fn.description, + parameters: z.object({}).passthrough(), // Accept any params — SuperDoc SDK validates + execute: async (args) => { + console.log(` Tool: ${fn.name}`); + return dispatchSuperDocTool(client, fn.name, args as Record); + }, + }); + } + + // 3. Run with generateText — handles the agentic loop automatically + const result = await generateText({ + model: openai('gpt-4o'), + system: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.', + prompt: 'Review this contract. Fix vague language and one-sided terms.', + tools: vercelTools, + maxSteps: 20, + }); + + console.log(result.text); + + // 4. Save + await client.doc.save({ doc: outputPath }); + await client.dispose(); + console.log(`\nSaved to ${outputPath}`); +} + +main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/vercel-ai/package.json b/examples/ai/vercel-ai/package.json new file mode 100644 index 0000000000..b66706966f --- /dev/null +++ b/examples/ai/vercel-ai/package.json @@ -0,0 +1,17 @@ +{ + "name": "superdoc-vercel-ai", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "ai": "^4.0.0", + "@ai-sdk/openai": "^1.0.0", + "@superdoc-dev/sdk": "latest", + "zod": "^3.23.0" + }, + "devDependencies": { + "tsx": "^4.21.0" + } +} diff --git a/examples/ai/vertex/README.md b/examples/ai/vertex/README.md new file mode 100644 index 0000000000..c49d0293a6 --- /dev/null +++ b/examples/ai/vertex/README.md @@ -0,0 +1,42 @@ +# SuperDoc + Google Vertex AI + +Agentic document editing using Gemini on Vertex AI. + +**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) + +## Prerequisites + +- Google Cloud credentials (`gcloud auth application-default login` or a service account key) +- A Google Cloud project with Vertex AI API enabled + +## Run + +### Node.js + +```bash +npm install +GOOGLE_CLOUD_PROJECT=your-project npx tsx index.ts contract.docx reviewed.docx +``` + +### Python + +```bash +pip install superdoc-sdk google-cloud-aiplatform +GOOGLE_CLOUD_PROJECT=your-project python index.py contract.docx reviewed.docx +``` + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `GOOGLE_CLOUD_PROJECT` | `your-project-id` | Google Cloud project ID | +| `GOOGLE_CLOUD_LOCATION` | `us-central1` | Vertex AI region | +| `VERTEX_MODEL` | `gemini-2.5-pro` | Any Gemini model that supports function calling | + +## How it works + +1. Connects to SuperDoc via the SDK +2. Loads tool definitions in generic format and converts to Vertex `functionDeclarations` +3. Starts a chat with Gemini +4. Runs an agentic loop: the model calls SuperDoc tools to read, query, and edit the document +5. Saves the reviewed document diff --git a/examples/ai/vertex/contract.docx b/examples/ai/vertex/contract.docx new file mode 100644 index 0000000000000000000000000000000000000000..5b911d339926dd9eebe45bddf9e99267496193fa GIT binary patch literal 79086 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 1 else "reviewed.docx" + + # 1. Connect to SuperDoc + client = SuperDocClient() + client.connect() + client.doc.open(doc=input_path) + + # 2. Get tools in generic format and convert to Vertex shape + result = choose_tools(provider="generic") + vertex_tools = to_vertex_tools(result["tools"]) + + # 3. Set up Vertex AI + vertexai.init(project=PROJECT, location=LOCATION) + model = GenerativeModel( + MODEL, + tools=vertex_tools, + system_instruction="You edit .docx files using SuperDoc tools. Use tracked changes for all edits.", + ) + chat = model.start_chat() + + # 4. Agentic loop + response = chat.send_message("Review this contract. Fix vague language and one-sided terms.") + + for _ in range(20): + function_calls = [ + part for part in response.candidates[0].content.parts if part.function_call.name + ] + + if not function_calls: + # Print final response + for part in response.candidates[0].content.parts: + if part.text: + print(part.text) + break + + function_responses = [] + for part in function_calls: + name = part.function_call.name + args = dict(part.function_call.args) if part.function_call.args else {} + print(f" Tool: {name}") + + try: + result = dispatch_superdoc_tool(client, name, args) + + # discover_tools returns new tools — merge them + if name == "discover_tools" and "tools" in result: + for t in result["tools"]: + vertex_tools[0].function_declarations.append( + FunctionDeclaration( + name=t["name"], + description=t["description"], + parameters=sanitize_schema(t["parameters"]), + ) + ) + + function_responses.append( + Part.from_function_response(name=name, response=result) + ) + except Exception as e: + function_responses.append( + Part.from_function_response(name=name, response={"error": str(e)}) + ) + + response = chat.send_message(function_responses) + + # 5. Save + client.doc.save(doc=output_path) + client.dispose() + print(f"\nSaved to {output_path}") + + +if __name__ == "__main__": + main() diff --git a/examples/ai/vertex/index.ts b/examples/ai/vertex/index.ts new file mode 100644 index 0000000000..a87e98fc8b --- /dev/null +++ b/examples/ai/vertex/index.ts @@ -0,0 +1,133 @@ +/** + * SuperDoc + Google Vertex AI + * + * Minimal agentic loop: Gemini on Vertex AI uses SuperDoc tools + * to review and edit a Word document. + * + * Usage: npx tsx index.ts [input.docx] [output.docx] + * + * Requires: Google Cloud credentials configured (gcloud auth application-default login). + */ + +import { + VertexAI, + type FunctionDeclaration, + type Tool as VertexTool, + type Content, + type Part, +} from '@google-cloud/vertexai'; +import { + createSuperDocClient, + chooseTools, + dispatchSuperDocTool, +} from '@superdoc-dev/sdk'; + +const PROJECT = process.env.GOOGLE_CLOUD_PROJECT ?? 'your-project-id'; +const LOCATION = process.env.GOOGLE_CLOUD_LOCATION ?? 'us-central1'; +const MODEL = process.env.VERTEX_MODEL ?? 'gemini-2.5-pro'; + +/** Recursively strip JSON Schema keywords unsupported by Vertex AI (e.g. `const`). */ +function sanitizeSchema(obj: unknown): unknown { + if (Array.isArray(obj)) return obj.map(sanitizeSchema); + if (typeof obj !== 'object' || obj === null) return obj; + const result: Record = {}; + for (const [key, value] of Object.entries(obj as Record)) { + if (key === 'const') continue; + result[key] = sanitizeSchema(value); + } + return result; +} + +/** Convert SuperDoc generic-format tools to Vertex AI function declarations. */ +function toVertexTools( + sdTools: Array<{ name: string; description: string; parameters: Record }>, +): VertexTool[] { + const declarations: FunctionDeclaration[] = sdTools.map((t) => ({ + name: t.name, + description: t.description, + parameters: sanitizeSchema(t.parameters) as FunctionDeclaration['parameters'], + })); + return [{ functionDeclarations: declarations }]; +} + +async function main() { + const [inputPath = 'contract.docx', outputPath = 'reviewed.docx'] = process.argv.slice(2); + + // 1. Connect to SuperDoc + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: inputPath }); + + // 2. Get tools in generic format and convert to Vertex shape + const { tools: sdTools } = await chooseTools({ provider: 'generic' }); + const vertexTools = toVertexTools( + sdTools as Array<{ name: string; description: string; parameters: Record }>, + ); + + // 3. Set up Vertex AI + const vertexAI = new VertexAI({ project: PROJECT, location: LOCATION }); + const model = vertexAI.getGenerativeModel({ + model: MODEL, + tools: vertexTools, + systemInstruction: { role: 'system', parts: [{ text: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.' }] }, + }); + + const chat = model.startChat(); + + // 4. Agentic loop + let response = await chat.sendMessage([ + { text: 'Review this contract. Fix vague language and one-sided terms.' }, + ]); + + for (let turn = 0; turn < 20; turn++) { + const candidate = response.response.candidates?.[0]; + if (!candidate) break; + + const functionCalls = candidate.content.parts.filter((p) => p.functionCall); + if (!functionCalls.length) { + // Print final response + for (const part of candidate.content.parts) { + if (part.text) console.log(part.text); + } + break; + } + + const functionResponses: Part[] = []; + for (const part of functionCalls) { + const { name, args } = part.functionCall!; + console.log(` Tool: ${name}`); + try { + const result = await dispatchSuperDocTool(client, name, (args ?? {}) as Record); + + // discover_tools returns new tools — merge them + if (name === 'discover_tools') { + const newTools = (result as { tools?: any[] }).tools ?? []; + vertexTools[0].functionDeclarations!.push( + ...newTools.map((t: any) => ({ + name: t.name, + description: t.description, + parameters: sanitizeSchema(t.parameters), + })), + ); + } + + functionResponses.push({ + functionResponse: { name, response: result as object }, + }); + } catch (err) { + functionResponses.push({ + functionResponse: { name, response: { error: (err as Error).message } }, + }); + } + } + + response = await chat.sendMessage(functionResponses); + } + + // 5. Save + await client.doc.save({ doc: outputPath }); + await client.dispose(); + console.log(`\nSaved to ${outputPath}`); +} + +main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/vertex/package.json b/examples/ai/vertex/package.json new file mode 100644 index 0000000000..d6e705ba51 --- /dev/null +++ b/examples/ai/vertex/package.json @@ -0,0 +1,15 @@ +{ + "name": "superdoc-vertex", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@google-cloud/vertexai": "^1.9.0", + "@superdoc-dev/sdk": "latest" + }, + "devDependencies": { + "tsx": "^4.21.0" + } +} From f9dec93d0b8a6eab43c16757cbc0063c03fa50a6 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 8 Mar 2026 19:03:46 -0300 Subject: [PATCH 2/9] feat(sdk): add platform helpers for AI integrations SD-2165: Ship sanitizeToolSchemas, formatToolResult, formatToolError, and mergeDiscoveredTools helpers for both Node.js and Python SDKs. These eliminate boilerplate when integrating SuperDoc tools with cloud platforms (Bedrock, Vertex AI) and direct APIs (OpenAI, Anthropic). --- .../src/helpers/__tests__/platform.test.ts | 271 +++++++++++++++ .../sdk/langs/node/src/helpers/platform.ts | 322 ++++++++++++++++++ packages/sdk/langs/node/src/index.ts | 7 + .../sdk/langs/python/superdoc/__init__.py | 10 + .../langs/python/superdoc/helpers/__init__.py | 10 + .../langs/python/superdoc/helpers/platform.py | 262 ++++++++++++++ .../python/tests/test_platform_helpers.py | 244 +++++++++++++ 7 files changed, 1126 insertions(+) create mode 100644 packages/sdk/langs/node/src/helpers/__tests__/platform.test.ts create mode 100644 packages/sdk/langs/node/src/helpers/platform.ts create mode 100644 packages/sdk/langs/python/superdoc/helpers/platform.py create mode 100644 packages/sdk/langs/python/tests/test_platform_helpers.py diff --git a/packages/sdk/langs/node/src/helpers/__tests__/platform.test.ts b/packages/sdk/langs/node/src/helpers/__tests__/platform.test.ts new file mode 100644 index 0000000000..f1867ea12b --- /dev/null +++ b/packages/sdk/langs/node/src/helpers/__tests__/platform.test.ts @@ -0,0 +1,271 @@ +import { describe, expect, test } from 'bun:test'; +import { sanitizeToolSchemas, formatToolResult, formatToolError, mergeDiscoveredTools } from '../platform.js'; + +/* ------------------------------------------------------------------ */ +/* sanitizeToolSchemas */ +/* ------------------------------------------------------------------ */ + +describe('sanitizeToolSchemas', () => { + test('strips const keyword for vertex', () => { + const tools = [ + { + name: 'query_match', + parameters: { + type: 'object', + properties: { + matchKind: { const: 'text' }, + query: { type: 'string' }, + }, + }, + }, + ]; + + const result = sanitizeToolSchemas(tools, 'vertex'); + + expect(result[0].parameters.properties.matchKind).toEqual({}); + expect(result[0].parameters.properties.query).toEqual({ type: 'string' }); + }); + + test('strips const recursively in nested schemas', () => { + const tools = [ + { + name: 'test', + parameters: { + oneOf: [ + { properties: { kind: { const: 'a' }, value: { type: 'string' } } }, + { properties: { kind: { const: 'b' }, value: { type: 'number' } } }, + ], + }, + }, + ]; + + const result = sanitizeToolSchemas(tools, 'vertex'); + + expect(result[0].parameters.oneOf[0].properties.kind).toEqual({}); + expect(result[0].parameters.oneOf[1].properties.kind).toEqual({}); + expect(result[0].parameters.oneOf[0].properties.value).toEqual({ type: 'string' }); + }); + + test('does not mutate original tools', () => { + const tools = [{ name: 'test', parameters: { properties: { x: { const: 'a' } } } }]; + const original = JSON.stringify(tools); + + sanitizeToolSchemas(tools, 'vertex'); + + expect(JSON.stringify(tools)).toBe(original); + }); + + test('is a no-op for bedrock', () => { + const tools = [{ name: 'test', parameters: { properties: { x: { const: 'a' } } } }]; + const result = sanitizeToolSchemas(tools, 'bedrock'); + expect(result).toBe(tools); // same reference — no cloning needed + }); + + test('handles empty array', () => { + expect(sanitizeToolSchemas([], 'vertex')).toEqual([]); + }); +}); + +/* ------------------------------------------------------------------ */ +/* formatToolResult */ +/* ------------------------------------------------------------------ */ + +describe('formatToolResult', () => { + describe('bedrock', () => { + test('wraps object result in toolResult shape', () => { + const result = formatToolResult({ text: 'hello' }, { target: 'bedrock', toolUseId: 'tu-1' }); + expect(result).toEqual({ + toolResult: { toolUseId: 'tu-1', content: [{ json: { text: 'hello' } }] }, + }); + }); + + test('wraps array result in { result } wrapper', () => { + const result = formatToolResult([1, 2, 3], { target: 'bedrock', toolUseId: 'tu-1' }); + expect(result).toEqual({ + toolResult: { toolUseId: 'tu-1', content: [{ json: { result: [1, 2, 3] } }] }, + }); + }); + + test('wraps string result in { result } wrapper', () => { + const result = formatToolResult('hello', { target: 'bedrock', toolUseId: 'tu-1' }); + expect(result).toEqual({ + toolResult: { toolUseId: 'tu-1', content: [{ json: { result: 'hello' } }] }, + }); + }); + + test('wraps null result in { result } wrapper', () => { + const result = formatToolResult(null, { target: 'bedrock', toolUseId: 'tu-1' }); + expect(result).toEqual({ + toolResult: { toolUseId: 'tu-1', content: [{ json: { result: null } }] }, + }); + }); + }); + + describe('vertex', () => { + test('wraps in functionResponse shape', () => { + const result = formatToolResult({ data: 1 }, { target: 'vertex', name: 'get_text' }); + expect(result).toEqual({ + functionResponse: { name: 'get_text', response: { data: 1 } }, + }); + }); + }); + + describe('anthropic', () => { + test('wraps in tool_result shape', () => { + const result = formatToolResult({ ok: true }, { target: 'anthropic', toolUseId: 'tu-1' }); + expect(result).toEqual({ + type: 'tool_result', + tool_use_id: 'tu-1', + content: '{"ok":true}', + }); + }); + }); + + describe('openai', () => { + test('wraps in tool role message', () => { + const result = formatToolResult({ ok: true }, { target: 'openai', toolUseId: 'call-1', name: 'fn' }); + expect(result).toEqual({ + role: 'tool', + tool_call_id: 'call-1', + content: '{"ok":true}', + }); + }); + }); +}); + +/* ------------------------------------------------------------------ */ +/* formatToolError */ +/* ------------------------------------------------------------------ */ + +describe('formatToolError', () => { + test('bedrock error shape', () => { + const result = formatToolError(new Error('boom'), { target: 'bedrock', toolUseId: 'tu-1' }); + expect(result).toEqual({ + toolResult: { toolUseId: 'tu-1', content: [{ text: 'Error: boom' }], status: 'error' }, + }); + }); + + test('vertex error shape', () => { + const result = formatToolError('fail', { target: 'vertex', name: 'fn' }); + expect(result).toEqual({ + functionResponse: { name: 'fn', response: { error: 'fail' } }, + }); + }); + + test('anthropic error shape', () => { + const result = formatToolError(new Error('nope'), { target: 'anthropic', toolUseId: 'tu-1' }); + expect(result).toEqual({ + type: 'tool_result', + tool_use_id: 'tu-1', + content: 'Error: nope', + is_error: true, + }); + }); + + test('openai error shape', () => { + const result = formatToolError(new Error('bad'), { target: 'openai', toolUseId: 'call-1' }); + expect(result).toEqual({ + role: 'tool', + tool_call_id: 'call-1', + content: 'Error: bad', + }); + }); +}); + +/* ------------------------------------------------------------------ */ +/* mergeDiscoveredTools */ +/* ------------------------------------------------------------------ */ + +describe('mergeDiscoveredTools', () => { + const anthropicTools = [ + { name: 'add_comment', description: 'Add a comment', input_schema: { type: 'object' } }, + { name: 'format_bold', description: 'Bold text', input_schema: { type: 'object' } }, + ]; + + const genericTools = [ + { + name: 'add_comment', + description: 'Add a comment', + parameters: { type: 'object', properties: { kind: { const: 'inline' } } }, + }, + { name: 'format_bold', description: 'Bold text', parameters: { type: 'object' } }, + ]; + + describe('bedrock target', () => { + test('merges tools into bedrock toolConfig', () => { + const toolConfig = { tools: [{ toolSpec: { name: 'existing', description: 'x', inputSchema: { json: {} } } }] }; + const result = { tools: anthropicTools }; + + const count = mergeDiscoveredTools(toolConfig, result, { provider: 'anthropic', target: 'bedrock' }); + + expect(count).toBe(2); + expect(toolConfig.tools).toHaveLength(3); + expect(toolConfig.tools[1]).toEqual({ + toolSpec: { + name: 'add_comment', + description: 'Add a comment', + inputSchema: { json: { type: 'object' } }, + }, + }); + }); + + test('skips duplicate tools', () => { + const toolConfig = { + tools: [{ toolSpec: { name: 'add_comment', description: 'x', inputSchema: { json: {} } } }], + }; + const result = { tools: anthropicTools }; + + const count = mergeDiscoveredTools(toolConfig, result, { provider: 'anthropic', target: 'bedrock' }); + + expect(count).toBe(1); // only format_bold added, add_comment skipped + expect(toolConfig.tools).toHaveLength(2); + }); + }); + + describe('vertex target', () => { + test('merges tools and sanitizes schemas', () => { + const toolConfig = [{ functionDeclarations: [{ name: 'existing', description: 'x', parameters: {} }] }]; + const result = { tools: genericTools }; + + const count = mergeDiscoveredTools(toolConfig, result, { provider: 'generic', target: 'vertex' }); + + expect(count).toBe(2); + expect(toolConfig[0].functionDeclarations).toHaveLength(3); + // const keyword should be stripped + const addComment = toolConfig[0].functionDeclarations[1] as Record; + expect(JSON.stringify(addComment)).not.toContain('"const"'); + }); + }); + + describe('direct provider (no target)', () => { + test('merges into plain array', () => { + const toolConfig = [{ name: 'existing', description: 'x', input_schema: {} }] as unknown[]; + const result = { tools: anthropicTools }; + + const count = mergeDiscoveredTools(toolConfig, result, { provider: 'anthropic' }); + + expect(count).toBe(2); + expect(toolConfig).toHaveLength(3); + }); + }); + + describe('edge cases', () => { + test('returns 0 for empty discover result', () => { + const toolConfig = { tools: [] }; + const count = mergeDiscoveredTools(toolConfig, {}, { provider: 'anthropic', target: 'bedrock' }); + expect(count).toBe(0); + }); + + test('returns 0 for non-object discover result', () => { + const toolConfig = { tools: [] }; + const count = mergeDiscoveredTools(toolConfig, 'not an object', { provider: 'anthropic', target: 'bedrock' }); + expect(count).toBe(0); + }); + + test('returns 0 for null discover result', () => { + const toolConfig = { tools: [] }; + const count = mergeDiscoveredTools(toolConfig, null, { provider: 'anthropic', target: 'bedrock' }); + expect(count).toBe(0); + }); + }); +}); diff --git a/packages/sdk/langs/node/src/helpers/platform.ts b/packages/sdk/langs/node/src/helpers/platform.ts new file mode 100644 index 0000000000..9bdb8a5179 --- /dev/null +++ b/packages/sdk/langs/node/src/helpers/platform.ts @@ -0,0 +1,322 @@ +/** + * Platform helper methods for the Node SDK. + * + * These are hand-written convenience wrappers that handle platform-specific + * quirks when integrating SuperDoc tools with cloud AI platforms (Bedrock, + * Vertex AI) and direct APIs (OpenAI, Anthropic). They are NOT generated + * from the contract and will not be overwritten by `pnpm run generate:all`. + * + * Usage: + * ```ts + * import { chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + * import { sanitizeToolSchemas, formatToolResult, mergeDiscoveredTools } from '@superdoc-dev/sdk/helpers/platform'; + * + * // Vertex AI: strip unsupported JSON Schema keywords + * const { tools } = await chooseTools({ provider: 'generic' }); + * const sanitized = sanitizeToolSchemas(tools, 'vertex'); + * + * // Bedrock: format tool results in platform-native shape + * const result = await dispatchSuperDocTool(client, name, args); + * const formatted = formatToolResult(result, { target: 'bedrock', toolUseId }); + * + * // Merge discover_tools output into platform-native config + * mergeDiscoveredTools(toolConfig, discoverResult, { provider: 'anthropic', target: 'bedrock' }); + * ``` + */ + +import type { ToolProvider } from '../tools.js'; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +/** Cloud platforms that need schema or result sanitization. */ +export type PlatformTarget = 'bedrock' | 'vertex'; + +/** All targets that `formatToolResult` supports. */ +export type ResultTarget = 'bedrock' | 'vertex' | 'anthropic' | 'openai'; + +export interface FormatToolResultOptions { + target: ResultTarget; + /** Required for bedrock, anthropic, openai. */ + toolUseId?: string; + /** Required for vertex, openai. */ + name?: string; +} + +export interface MergeDiscoveredToolsOptions { + provider: ToolProvider; + target?: PlatformTarget; +} + +/* ------------------------------------------------------------------ */ +/* sanitizeToolSchemas */ +/* ------------------------------------------------------------------ */ + +/** + * JSON Schema keywords unsupported by each platform. + * Extend this map when new platform incompatibilities are discovered. + */ +const UNSUPPORTED_KEYWORDS: Record> = { + vertex: new Set(['const']), + bedrock: new Set(), // no-op currently — future-proof +}; + +/** + * Recursively strip JSON Schema keywords that the target platform doesn't support. + * + * Returns a new array — the original tools are not mutated. + */ +export function sanitizeToolSchemas(tools: T[], target: PlatformTarget): T[] { + const blocked = UNSUPPORTED_KEYWORDS[target]; + if (!blocked || blocked.size === 0) return tools; + return tools.map((t) => deepStripKeys(t, blocked) as T); +} + +function deepStripKeys(obj: unknown, blocked: Set): unknown { + if (Array.isArray(obj)) return obj.map((item) => deepStripKeys(item, blocked)); + if (typeof obj !== 'object' || obj === null) return obj; + + const result: Record = {}; + for (const [key, value] of Object.entries(obj as Record)) { + if (blocked.has(key)) continue; + result[key] = deepStripKeys(value, blocked); + } + return result; +} + +/* ------------------------------------------------------------------ */ +/* formatToolResult */ +/* ------------------------------------------------------------------ */ + +/** + * Wrap a raw `dispatchSuperDocTool` result in the platform-native shape + * expected by each provider's conversation API. + */ +export function formatToolResult(result: unknown, options: FormatToolResultOptions): unknown { + const { target, toolUseId, name } = options; + + switch (target) { + case 'bedrock': { + // Bedrock requires json content to be a plain object (not array or primitive) + const json = typeof result === 'object' && result !== null && !Array.isArray(result) ? result : { result }; + return { toolResult: { toolUseId, content: [{ json }] } }; + } + + case 'vertex': + return { functionResponse: { name, response: result } }; + + case 'anthropic': + return { + type: 'tool_result', + tool_use_id: toolUseId, + content: JSON.stringify(result), + }; + + case 'openai': + return { + role: 'tool', + tool_call_id: toolUseId, + content: JSON.stringify(result), + }; + + default: + return result; + } +} + +/** + * Format an error from a failed tool call in the platform-native error shape. + */ +export function formatToolError(error: unknown, options: FormatToolResultOptions): unknown { + const { target, toolUseId, name } = options; + const message = error instanceof Error ? error.message : String(error); + + switch (target) { + case 'bedrock': + return { + toolResult: { + toolUseId, + content: [{ text: `Error: ${message}` }], + status: 'error', + }, + }; + + case 'vertex': + return { functionResponse: { name, response: { error: message } } }; + + case 'anthropic': + return { + type: 'tool_result', + tool_use_id: toolUseId, + content: `Error: ${message}`, + is_error: true, + }; + + case 'openai': + return { + role: 'tool', + tool_call_id: toolUseId, + content: `Error: ${message}`, + }; + + default: + return { error: message }; + } +} + +/* ------------------------------------------------------------------ */ +/* mergeDiscoveredTools */ +/* ------------------------------------------------------------------ */ + +/** + * Extract newly discovered tools from a `discover_tools` result, convert them + * to the provider's native format, apply platform sanitization, and merge them + * into an existing tool configuration object. + * + * Mutates `toolConfig` in place. Returns the number of new tools added. + * + * Supported toolConfig shapes: + * - **bedrock**: `{ tools: [{ toolSpec: { name, description, inputSchema } }] }` + * - **vertex**: `[{ functionDeclarations: [...] }]` + * - **openai/anthropic/vercel/generic**: `unknown[]` (array of tool objects) + */ +export function mergeDiscoveredTools( + toolConfig: unknown, + discoverResult: unknown, + options: MergeDiscoveredToolsOptions, +): number { + const newTools = extractDiscoveredTools(discoverResult); + if (newTools.length === 0) return 0; + + const { provider, target } = options; + + // Collect existing tool names to avoid duplicates + const existingNames = collectExistingNames(toolConfig, target); + + let added = 0; + + for (const tool of newTools) { + const name = extractToolName(tool, provider); + if (!name || existingNames.has(name)) continue; + existingNames.add(name); + + const formatted = formatToolForConfig(tool, provider, target); + pushToConfig(toolConfig, formatted, target); + added++; + } + + return added; +} + +/** Pull the `tools` array from a discover_tools result. */ +function extractDiscoveredTools(result: unknown): unknown[] { + if (typeof result !== 'object' || result === null) return []; + const obj = result as Record; + if (Array.isArray(obj.tools)) return obj.tools; + return []; +} + +/** Extract a tool's name regardless of provider format. */ +function extractToolName(tool: unknown, provider: ToolProvider): string | null { + if (typeof tool !== 'object' || tool === null) return null; + const obj = tool as Record; + + // Anthropic / Generic: top-level name + if (typeof obj.name === 'string') return obj.name; + + // OpenAI / Vercel: nested under function.name + if (typeof obj.function === 'object' && obj.function !== null) { + const fn = obj.function as Record; + if (typeof fn.name === 'string') return fn.name; + } + + return null; +} + +/** Collect existing tool names from a platform-native config. */ +function collectExistingNames(toolConfig: unknown, target?: PlatformTarget): Set { + const names = new Set(); + + if (target === 'bedrock' && isRecord(toolConfig)) { + const tools = (toolConfig as Record).tools; + if (Array.isArray(tools)) { + for (const t of tools) { + const spec = isRecord(t) ? (t as Record).toolSpec : null; + if (isRecord(spec) && typeof (spec as Record).name === 'string') { + names.add((spec as Record).name as string); + } + } + } + } else if (target === 'vertex' && Array.isArray(toolConfig)) { + const decls = (toolConfig[0] as Record)?.functionDeclarations; + if (Array.isArray(decls)) { + for (const d of decls) { + if (isRecord(d) && typeof (d as Record).name === 'string') { + names.add((d as Record).name as string); + } + } + } + } else if (Array.isArray(toolConfig)) { + for (const t of toolConfig) { + if (isRecord(t)) { + const obj = t as Record; + if (typeof obj.name === 'string') names.add(obj.name); + if (isRecord(obj.function)) { + const fn = obj.function as Record; + if (typeof fn.name === 'string') names.add(fn.name); + } + } + } + } + + return names; +} + +/** Convert a discovered tool to the platform-native shape and push into config. */ +function formatToolForConfig(tool: unknown, provider: ToolProvider, target?: PlatformTarget): unknown { + const obj = tool as Record; + + if (target === 'bedrock') { + // Discovered tools from 'anthropic' provider: { name, description, input_schema } + return { + toolSpec: { + name: obj.name, + description: obj.description, + inputSchema: { json: obj.input_schema ?? obj.parameters }, + }, + }; + } + + if (target === 'vertex') { + // Discovered tools from 'generic' provider: { name, description, parameters } + const params = obj.parameters ?? obj.input_schema; + return { + name: obj.name, + description: obj.description, + parameters: params ? deepStripKeys(params, UNSUPPORTED_KEYWORDS.vertex) : params, + }; + } + + // For direct API providers (openai, anthropic, vercel, generic) — pass through as-is + return tool; +} + +/** Push a formatted tool into the platform-native config structure. */ +function pushToConfig(toolConfig: unknown, formatted: unknown, target?: PlatformTarget): void { + if (target === 'bedrock' && isRecord(toolConfig)) { + const tools = (toolConfig as Record).tools; + if (Array.isArray(tools)) tools.push(formatted); + } else if (target === 'vertex' && Array.isArray(toolConfig)) { + const first = toolConfig[0] as Record | undefined; + if (first && Array.isArray(first.functionDeclarations)) { + first.functionDeclarations.push(formatted); + } + } else if (Array.isArray(toolConfig)) { + toolConfig.push(formatted); + } +} + +function isRecord(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v); +} diff --git a/packages/sdk/langs/node/src/index.ts b/packages/sdk/langs/node/src/index.ts index ee8ed0ecd1..eb0c609618 100644 --- a/packages/sdk/langs/node/src/index.ts +++ b/packages/sdk/langs/node/src/index.ts @@ -40,5 +40,12 @@ export { resolveToolOperation, } from './tools.js'; export { SuperDocCliError } from './runtime/errors.js'; +export { formatToolError, formatToolResult, mergeDiscoveredTools, sanitizeToolSchemas } from './helpers/platform.js'; export type { InvokeOptions, OperationSpec, OperationParamSpec, SuperDocClientOptions } from './runtime/process.js'; export type { ToolChooserInput, ToolChooserMode, ToolGroup, ToolProvider } from './tools.js'; +export type { + FormatToolResultOptions, + MergeDiscoveredToolsOptions, + PlatformTarget, + ResultTarget, +} from './helpers/platform.js'; diff --git a/packages/sdk/langs/python/superdoc/__init__.py b/packages/sdk/langs/python/superdoc/__init__.py index 218a1131bb..00498856bf 100644 --- a/packages/sdk/langs/python/superdoc/__init__.py +++ b/packages/sdk/langs/python/superdoc/__init__.py @@ -1,6 +1,12 @@ from .client import AsyncSuperDocClient, SuperDocClient from .errors import SuperDocError from .skill_api import get_skill, install_skill, list_skills +from .helpers.platform import ( + format_tool_error, + format_tool_result, + merge_discovered_tools, + sanitize_tool_schemas, +) from .tools_api import ( choose_tools, dispatch_superdoc_tool, @@ -25,4 +31,8 @@ "choose_tools", "dispatch_superdoc_tool", "dispatch_superdoc_tool_async", + "format_tool_result", + "format_tool_error", + "merge_discovered_tools", + "sanitize_tool_schemas", ] diff --git a/packages/sdk/langs/python/superdoc/helpers/__init__.py b/packages/sdk/langs/python/superdoc/helpers/__init__.py index 473657d524..0b9967cdfb 100644 --- a/packages/sdk/langs/python/superdoc/helpers/__init__.py +++ b/packages/sdk/langs/python/superdoc/helpers/__init__.py @@ -17,6 +17,12 @@ unformat_strikethrough, unformat_underline, ) +from .platform import ( + format_tool_error, + format_tool_result, + merge_discovered_tools, + sanitize_tool_schemas, +) __all__ = [ "clear_bold", @@ -31,4 +37,8 @@ "unformat_italic", "unformat_underline", "unformat_strikethrough", + "format_tool_result", + "format_tool_error", + "merge_discovered_tools", + "sanitize_tool_schemas", ] diff --git a/packages/sdk/langs/python/superdoc/helpers/platform.py b/packages/sdk/langs/python/superdoc/helpers/platform.py new file mode 100644 index 0000000000..2d16f573a3 --- /dev/null +++ b/packages/sdk/langs/python/superdoc/helpers/platform.py @@ -0,0 +1,262 @@ +""" +Platform helper methods for the Python SDK. + +These are hand-written convenience wrappers that handle platform-specific +quirks when integrating SuperDoc tools with cloud AI platforms (Bedrock, +Vertex AI) and direct APIs (OpenAI, Anthropic). They are NOT generated +from the contract and will not be overwritten by codegen. + +Usage:: + + from superdoc import choose_tools, dispatch_superdoc_tool + from superdoc.helpers.platform import sanitize_tool_schemas, format_tool_result, merge_discovered_tools + + # Vertex AI: strip unsupported JSON Schema keywords + result = choose_tools(provider="generic") + sanitized = sanitize_tool_schemas(result["tools"], "vertex") + + # Bedrock: format tool results in platform-native shape + result = dispatch_superdoc_tool(client, name, args) + formatted = format_tool_result(result, target="bedrock", tool_use_id=tool_use_id) + + # Merge discover_tools output into platform-native config + merge_discovered_tools(tool_config, discover_result, provider="anthropic", target="bedrock") +""" + +from __future__ import annotations + +import json +from typing import Any, Dict, List, Literal, Optional, Set, Union + +PlatformTarget = Literal["bedrock", "vertex"] +ResultTarget = Literal["bedrock", "vertex", "anthropic", "openai"] +ToolProvider = Literal["openai", "anthropic", "vercel", "generic"] + +# JSON Schema keywords unsupported by each platform. +_UNSUPPORTED_KEYWORDS: Dict[PlatformTarget, Set[str]] = { + "vertex": {"const"}, + "bedrock": set(), +} + + +# ------------------------------------------------------------------ +# sanitize_tool_schemas +# ------------------------------------------------------------------ + + +def sanitize_tool_schemas(tools: List[Any], target: PlatformTarget) -> List[Any]: + """Recursively strip JSON Schema keywords that the target platform doesn't support. + + Returns a new list — the original tools are not mutated. + """ + blocked = _UNSUPPORTED_KEYWORDS.get(target) + if not blocked: + return tools + return [_deep_strip_keys(t, blocked) for t in tools] + + +def _deep_strip_keys(obj: Any, blocked: Set[str]) -> Any: + if isinstance(obj, list): + return [_deep_strip_keys(item, blocked) for item in obj] + if isinstance(obj, dict): + return {k: _deep_strip_keys(v, blocked) for k, v in obj.items() if k not in blocked} + return obj + + +# ------------------------------------------------------------------ +# format_tool_result +# ------------------------------------------------------------------ + + +def format_tool_result( + result: Any, + *, + target: ResultTarget, + tool_use_id: Optional[str] = None, + name: Optional[str] = None, +) -> Any: + """Wrap a raw ``dispatch_superdoc_tool`` result in the platform-native shape.""" + if target == "bedrock": + # Bedrock requires json content to be a plain dict + json_result = result if isinstance(result, dict) else {"result": result} + return {"toolResult": {"toolUseId": tool_use_id, "content": [{"json": json_result}]}} + + if target == "vertex": + return {"functionResponse": {"name": name, "response": result}} + + if target == "anthropic": + return { + "type": "tool_result", + "tool_use_id": tool_use_id, + "content": json.dumps(result), + } + + if target == "openai": + return { + "role": "tool", + "tool_call_id": tool_use_id, + "content": json.dumps(result), + } + + return result + + +def format_tool_error( + error: Union[Exception, str], + *, + target: ResultTarget, + tool_use_id: Optional[str] = None, + name: Optional[str] = None, +) -> Any: + """Format an error from a failed tool call in the platform-native error shape.""" + message = str(error) + + if target == "bedrock": + return { + "toolResult": { + "toolUseId": tool_use_id, + "content": [{"text": f"Error: {message}"}], + "status": "error", + } + } + + if target == "vertex": + return {"functionResponse": {"name": name, "response": {"error": message}}} + + if target == "anthropic": + return { + "type": "tool_result", + "tool_use_id": tool_use_id, + "content": f"Error: {message}", + "is_error": True, + } + + if target == "openai": + return { + "role": "tool", + "tool_call_id": tool_use_id, + "content": f"Error: {message}", + } + + return {"error": message} + + +# ------------------------------------------------------------------ +# merge_discovered_tools +# ------------------------------------------------------------------ + + +def merge_discovered_tools( + tool_config: Any, + discover_result: Any, + *, + provider: ToolProvider, + target: Optional[PlatformTarget] = None, +) -> int: + """Extract newly discovered tools, convert to platform format, and merge into config. + + Mutates ``tool_config`` in place. Returns the number of new tools added. + """ + new_tools = _extract_discovered_tools(discover_result) + if not new_tools: + return 0 + + existing_names = _collect_existing_names(tool_config, target) + added = 0 + + for tool in new_tools: + tool_name = _extract_tool_name(tool, provider) + if not tool_name or tool_name in existing_names: + continue + existing_names.add(tool_name) + + formatted = _format_tool_for_config(tool, provider, target) + _push_to_config(tool_config, formatted, target) + added += 1 + + return added + + +def _extract_discovered_tools(result: Any) -> List[Any]: + if isinstance(result, dict) and isinstance(result.get("tools"), list): + return result["tools"] + return [] + + +def _extract_tool_name(tool: Any, provider: ToolProvider) -> Optional[str]: + if not isinstance(tool, dict): + return None + # Anthropic / Generic: top-level name + if isinstance(tool.get("name"), str): + return tool["name"] + # OpenAI / Vercel: nested under function.name + fn = tool.get("function") + if isinstance(fn, dict) and isinstance(fn.get("name"), str): + return fn["name"] + return None + + +def _collect_existing_names(tool_config: Any, target: Optional[PlatformTarget]) -> Set[str]: + names: Set[str] = set() + + if target == "bedrock" and isinstance(tool_config, dict): + for t in tool_config.get("tools", []): + spec = t.get("toolSpec", {}) if isinstance(t, dict) else {} + if isinstance(spec.get("name"), str): + names.add(spec["name"]) + elif target == "vertex" and isinstance(tool_config, list) and tool_config: + decls = tool_config[0].get("functionDeclarations", []) if isinstance(tool_config[0], dict) else [] + for d in decls: + if isinstance(d, dict) and isinstance(d.get("name"), str): + names.add(d["name"]) + elif isinstance(tool_config, list): + for t in tool_config: + if isinstance(t, dict): + if isinstance(t.get("name"), str): + names.add(t["name"]) + fn = t.get("function") + if isinstance(fn, dict) and isinstance(fn.get("name"), str): + names.add(fn["name"]) + + return names + + +def _format_tool_for_config( + tool: Any, provider: ToolProvider, target: Optional[PlatformTarget] +) -> Any: + if not isinstance(tool, dict): + return tool + + if target == "bedrock": + return { + "toolSpec": { + "name": tool.get("name"), + "description": tool.get("description"), + "inputSchema": {"json": tool.get("input_schema") or tool.get("parameters")}, + } + } + + if target == "vertex": + params = tool.get("parameters") or tool.get("input_schema") + if params: + params = _deep_strip_keys(params, _UNSUPPORTED_KEYWORDS["vertex"]) + return { + "name": tool.get("name"), + "description": tool.get("description"), + "parameters": params, + } + + return tool + + +def _push_to_config(tool_config: Any, formatted: Any, target: Optional[PlatformTarget]) -> None: + if target == "bedrock" and isinstance(tool_config, dict): + tools = tool_config.get("tools") + if isinstance(tools, list): + tools.append(formatted) + elif target == "vertex" and isinstance(tool_config, list) and tool_config: + decls = tool_config[0].get("functionDeclarations") if isinstance(tool_config[0], dict) else None + if isinstance(decls, list): + decls.append(formatted) + elif isinstance(tool_config, list): + tool_config.append(formatted) diff --git a/packages/sdk/langs/python/tests/test_platform_helpers.py b/packages/sdk/langs/python/tests/test_platform_helpers.py new file mode 100644 index 0000000000..df0f1c96b1 --- /dev/null +++ b/packages/sdk/langs/python/tests/test_platform_helpers.py @@ -0,0 +1,244 @@ +"""Tests for platform helpers (sanitize, format, merge).""" + +import json +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from superdoc.helpers.platform import ( + format_tool_error, + format_tool_result, + merge_discovered_tools, + sanitize_tool_schemas, +) + + +# ------------------------------------------------------------------ +# sanitize_tool_schemas +# ------------------------------------------------------------------ + + +class TestSanitizeToolSchemas: + def test_strips_const_for_vertex(self): + tools = [ + { + "name": "query_match", + "parameters": { + "type": "object", + "properties": { + "matchKind": {"const": "text"}, + "query": {"type": "string"}, + }, + }, + } + ] + + result = sanitize_tool_schemas(tools, "vertex") + + assert result[0]["parameters"]["properties"]["matchKind"] == {} + assert result[0]["parameters"]["properties"]["query"] == {"type": "string"} + + def test_strips_const_recursively(self): + tools = [ + { + "name": "test", + "parameters": { + "oneOf": [ + {"properties": {"kind": {"const": "a"}, "value": {"type": "string"}}}, + {"properties": {"kind": {"const": "b"}, "value": {"type": "number"}}}, + ] + }, + } + ] + + result = sanitize_tool_schemas(tools, "vertex") + + assert result[0]["parameters"]["oneOf"][0]["properties"]["kind"] == {} + assert result[0]["parameters"]["oneOf"][1]["properties"]["kind"] == {} + + def test_does_not_mutate_original(self): + tools = [{"name": "test", "parameters": {"properties": {"x": {"const": "a"}}}}] + original = json.dumps(tools) + + sanitize_tool_schemas(tools, "vertex") + + assert json.dumps(tools) == original + + def test_noop_for_bedrock(self): + tools = [{"name": "test", "parameters": {"properties": {"x": {"const": "a"}}}}] + result = sanitize_tool_schemas(tools, "bedrock") + assert result is tools + + def test_empty_array(self): + assert sanitize_tool_schemas([], "vertex") == [] + + +# ------------------------------------------------------------------ +# format_tool_result +# ------------------------------------------------------------------ + + +class TestFormatToolResult: + def test_bedrock_object_result(self): + result = format_tool_result({"text": "hello"}, target="bedrock", tool_use_id="tu-1") + assert result == { + "toolResult": {"toolUseId": "tu-1", "content": [{"json": {"text": "hello"}}]} + } + + def test_bedrock_array_result_wrapped(self): + result = format_tool_result([1, 2, 3], target="bedrock", tool_use_id="tu-1") + assert result == { + "toolResult": {"toolUseId": "tu-1", "content": [{"json": {"result": [1, 2, 3]}}]} + } + + def test_bedrock_string_result_wrapped(self): + result = format_tool_result("hello", target="bedrock", tool_use_id="tu-1") + assert result == { + "toolResult": {"toolUseId": "tu-1", "content": [{"json": {"result": "hello"}}]} + } + + def test_bedrock_none_result_wrapped(self): + result = format_tool_result(None, target="bedrock", tool_use_id="tu-1") + assert result == { + "toolResult": {"toolUseId": "tu-1", "content": [{"json": {"result": None}}]} + } + + def test_vertex_result(self): + result = format_tool_result({"data": 1}, target="vertex", name="get_text") + assert result == {"functionResponse": {"name": "get_text", "response": {"data": 1}}} + + def test_anthropic_result(self): + result = format_tool_result({"ok": True}, target="anthropic", tool_use_id="tu-1") + assert result == { + "type": "tool_result", + "tool_use_id": "tu-1", + "content": '{"ok": true}', + } + + def test_openai_result(self): + result = format_tool_result({"ok": True}, target="openai", tool_use_id="call-1", name="fn") + assert result == { + "role": "tool", + "tool_call_id": "call-1", + "content": '{"ok": true}', + } + + +# ------------------------------------------------------------------ +# format_tool_error +# ------------------------------------------------------------------ + + +class TestFormatToolError: + def test_bedrock_error(self): + result = format_tool_error(Exception("boom"), target="bedrock", tool_use_id="tu-1") + assert result == { + "toolResult": { + "toolUseId": "tu-1", + "content": [{"text": "Error: boom"}], + "status": "error", + } + } + + def test_vertex_error(self): + result = format_tool_error("fail", target="vertex", name="fn") + assert result == {"functionResponse": {"name": "fn", "response": {"error": "fail"}}} + + def test_anthropic_error(self): + result = format_tool_error(Exception("nope"), target="anthropic", tool_use_id="tu-1") + assert result == { + "type": "tool_result", + "tool_use_id": "tu-1", + "content": "Error: nope", + "is_error": True, + } + + def test_openai_error(self): + result = format_tool_error(Exception("bad"), target="openai", tool_use_id="call-1") + assert result == {"role": "tool", "tool_call_id": "call-1", "content": "Error: bad"} + + +# ------------------------------------------------------------------ +# merge_discovered_tools +# ------------------------------------------------------------------ + + +class TestMergeDiscoveredTools: + anthropic_tools = [ + {"name": "add_comment", "description": "Add a comment", "input_schema": {"type": "object"}}, + {"name": "format_bold", "description": "Bold text", "input_schema": {"type": "object"}}, + ] + + generic_tools = [ + { + "name": "add_comment", + "description": "Add a comment", + "parameters": {"type": "object", "properties": {"kind": {"const": "inline"}}}, + }, + {"name": "format_bold", "description": "Bold text", "parameters": {"type": "object"}}, + ] + + def test_bedrock_merge(self): + tool_config = { + "tools": [{"toolSpec": {"name": "existing", "description": "x", "inputSchema": {"json": {}}}}] + } + result = {"tools": self.anthropic_tools} + + count = merge_discovered_tools(tool_config, result, provider="anthropic", target="bedrock") + + assert count == 2 + assert len(tool_config["tools"]) == 3 + assert tool_config["tools"][1] == { + "toolSpec": { + "name": "add_comment", + "description": "Add a comment", + "inputSchema": {"json": {"type": "object"}}, + } + } + + def test_bedrock_skips_duplicates(self): + tool_config = { + "tools": [{"toolSpec": {"name": "add_comment", "description": "x", "inputSchema": {"json": {}}}}] + } + result = {"tools": self.anthropic_tools} + + count = merge_discovered_tools(tool_config, result, provider="anthropic", target="bedrock") + + assert count == 1 + assert len(tool_config["tools"]) == 2 + + def test_vertex_merge_sanitizes_schemas(self): + tool_config = [{"functionDeclarations": [{"name": "existing", "description": "x", "parameters": {}}]}] + result = {"tools": self.generic_tools} + + count = merge_discovered_tools(tool_config, result, provider="generic", target="vertex") + + assert count == 2 + assert len(tool_config[0]["functionDeclarations"]) == 3 + add_comment = tool_config[0]["functionDeclarations"][1] + assert '"const"' not in json.dumps(add_comment) + + def test_plain_array_merge(self): + tool_config = [{"name": "existing", "description": "x", "input_schema": {}}] + result = {"tools": self.anthropic_tools} + + count = merge_discovered_tools(tool_config, result, provider="anthropic") + + assert count == 2 + assert len(tool_config) == 3 + + def test_empty_discover_result(self): + tool_config = {"tools": []} + count = merge_discovered_tools(tool_config, {}, provider="anthropic", target="bedrock") + assert count == 0 + + def test_non_object_discover_result(self): + tool_config = {"tools": []} + count = merge_discovered_tools(tool_config, "not an object", provider="anthropic", target="bedrock") + assert count == 0 + + def test_none_discover_result(self): + tool_config = {"tools": []} + count = merge_discovered_tools(tool_config, None, provider="anthropic", target="bedrock") + assert count == 0 From d5d661a1b9a5e1437d8d62d1395c28c48e21fcf9 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 8 Mar 2026 19:10:54 -0300 Subject: [PATCH 3/9] refactor(sdk): use platform helpers in examples and docs Replace inline tool conversion, result wrapping, and schema sanitization with the new SDK helpers (formatToolResult, formatToolError, sanitizeToolSchemas, mergeDiscoveredTools) across all integration examples and documentation. --- .../ai-agents/integrations.mdx | 108 +++++++----------- examples/ai/bedrock/index.py | 41 +++---- examples/ai/bedrock/index.ts | 30 ++--- examples/ai/vertex/index.py | 21 ++-- examples/ai/vertex/index.ts | 49 ++------ .../sdk/langs/node/src/helpers/platform.ts | 2 +- 6 files changed, 88 insertions(+), 163 deletions(-) diff --git a/apps/docs/document-engine/ai-agents/integrations.mdx b/apps/docs/document-engine/ai-agents/integrations.mdx index e4282e101c..242c781bdd 100644 --- a/apps/docs/document-engine/ai-agents/integrations.mdx +++ b/apps/docs/document-engine/ai-agents/integrations.mdx @@ -28,23 +28,19 @@ Use SuperDoc tools with cloud AI platforms. You write the agentic loop and contr ```typescript import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; - import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + import { + createSuperDocClient, chooseTools, dispatchSuperDocTool, + formatToolResult, mergeDiscoveredTools, + } from '@superdoc-dev/sdk'; const client = createSuperDocClient(); await client.connect(); await client.doc.open({ doc: './contract.docx' }); - // Anthropic format → Bedrock toolSpec (3-line conversion) + // Anthropic format → Bedrock toolSpec shape const { tools } = await chooseTools({ provider: 'anthropic' }); - const toolConfig = { - tools: tools.map((t) => ({ - toolSpec: { - name: t.name, - description: t.description, - inputSchema: { json: t.input_schema }, - }, - })), - }; + const toolConfig = { tools: [] }; + mergeDiscoveredTools(toolConfig, { tools }, { provider: 'anthropic', target: 'bedrock' }); const bedrock = new BedrockRuntimeClient({ region: 'us-east-1' }); const messages = [ @@ -70,10 +66,7 @@ Use SuperDoc tools with cloud AI platforms. You write the agentic loop and contr for (const block of toolUses) { const { name, input, toolUseId } = block.toolUse; const result = await dispatchSuperDocTool(client, name, input ?? {}); - // Bedrock requires json to be a plain object — wrap if needed - const json = (typeof result === 'object' && result !== null && !Array.isArray(result)) - ? result : { result }; - results.push({ toolResult: { toolUseId, content: [{ json }] } }); + results.push(formatToolResult(result, { target: 'bedrock', toolUseId })); } messages.push({ role: 'user', content: results }); } @@ -89,21 +82,19 @@ Use SuperDoc tools with cloud AI platforms. You write the agentic loop and contr ```python import boto3 - from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool + from superdoc import ( + SuperDocClient, choose_tools, dispatch_superdoc_tool, + format_tool_result, merge_discovered_tools, + ) client = SuperDocClient() client.connect() client.doc.open(doc="./contract.docx") - # Anthropic format → Bedrock toolSpec - result = choose_tools(provider="anthropic") - tool_config = { - "tools": [ - {"toolSpec": {"name": t["name"], "description": t["description"], - "inputSchema": {"json": t["input_schema"]}}} - for t in result["tools"] - ] - } + # Anthropic format → Bedrock toolSpec shape + sd_tools = choose_tools(provider="anthropic") + tool_config = {"tools": []} + merge_discovered_tools(tool_config, sd_tools, provider="anthropic", target="bedrock") bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") messages = [{"role": "user", "content": [{"text": "Review this contract."}]}] @@ -128,7 +119,7 @@ Use SuperDoc tools with cloud AI platforms. You write the agentic loop and contr tu = block["toolUse"] result = dispatch_superdoc_tool(client, tu["name"], tu.get("input", {})) tool_results.append( - {"toolResult": {"toolUseId": tu["toolUseId"], "content": [{"json": result}]}} + format_tool_result(result, target="bedrock", tool_use_id=tu["toolUseId"]) ) messages.append({"role": "user", "content": tool_results}) @@ -150,31 +141,23 @@ Use SuperDoc tools with cloud AI platforms. You write the agentic loop and contr ```typescript import { VertexAI } from '@google-cloud/vertexai'; - import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + import { + createSuperDocClient, chooseTools, dispatchSuperDocTool, + sanitizeToolSchemas, + } from '@superdoc-dev/sdk'; const client = createSuperDocClient(); await client.connect(); await client.doc.open({ doc: './contract.docx' }); - // Vertex doesn't support all JSON Schema keywords — strip unsupported ones - function sanitizeSchema(obj) { - if (Array.isArray(obj)) return obj.map(sanitizeSchema); - if (typeof obj !== 'object' || obj === null) return obj; - const result = {}; - for (const [k, v] of Object.entries(obj)) { - if (k === 'const') continue; - result[k] = sanitizeSchema(v); - } - return result; - } - - // Generic format → Vertex function declarations + // Generic format → Vertex function declarations (sanitized for Vertex compatibility) const { tools } = await chooseTools({ provider: 'generic' }); + const sanitized = sanitizeToolSchemas(tools, 'vertex'); const vertexTools = [{ - functionDeclarations: tools.map((t) => ({ + functionDeclarations: sanitized.map((t) => ({ name: t.name, description: t.description, - parameters: sanitizeSchema(t.parameters), + parameters: t.parameters, })), }]; @@ -214,23 +197,18 @@ Use SuperDoc tools with cloud AI platforms. You write the agentic loop and contr ```python import vertexai from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration, Part - from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool + from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool, sanitize_tool_schemas client = SuperDocClient() client.connect() client.doc.open(doc="./contract.docx") - # Vertex doesn't support all JSON Schema keywords — strip unsupported ones - def sanitize_schema(obj): - if isinstance(obj, list): return [sanitize_schema(i) for i in obj] - if isinstance(obj, dict): return {k: sanitize_schema(v) for k, v in obj.items() if k != "const"} - return obj - - # Generic format → Vertex function declarations + # Generic format → Vertex function declarations (sanitized for Vertex compatibility) result = choose_tools(provider="generic") + sanitized = sanitize_tool_schemas(result["tools"], "vertex") vertex_tools = [Tool(function_declarations=[ - FunctionDeclaration(name=t["name"], description=t["description"], parameters=sanitize_schema(t["parameters"])) - for t in result["tools"] + FunctionDeclaration(name=t["name"], description=t["description"], parameters=t["parameters"]) + for t in sanitized ])] vertexai.init(project="your-project", location="us-central1") @@ -418,14 +396,14 @@ Use SuperDoc tools with agent frameworks. The framework manages the agentic loop The SDK ships pre-formatted tools for each integration. The conversion is minimal: -| Integration | Type | SDK format | Native shape | -|-------------|------|-----------|--------------| -| AWS Bedrock | Cloud platform | `anthropic` | Wrap in `{ toolSpec: { name, description, inputSchema: { json } } }` | -| Google Vertex AI | Cloud platform | `generic` | Use `{ name, description, parameters }` as `functionDeclarations` | -| Vercel AI SDK | Framework | `vercel` | Wrap in Vercel `tool()` with `z.object({}).passthrough()` | -| LangChain | Framework | `generic` | Wrap in `DynamicStructuredTool` | -| OpenAI | Direct API | `openai` | Pass directly — already in OpenAI format | -| Anthropic | Direct API | `anthropic` | Pass directly — already in Anthropic format | +| Integration | Type | SDK format | SDK helpers | Native shape | +|-------------|------|-----------|-------------|--------------| +| AWS Bedrock | Cloud platform | `anthropic` | `mergeDiscoveredTools`, `formatToolResult`, `formatToolError` | `{ toolSpec: { name, description, inputSchema: { json } } }` | +| Google Vertex AI | Cloud platform | `generic` | `sanitizeToolSchemas`, `mergeDiscoveredTools` | `{ functionDeclarations: [...] }` | +| Vercel AI SDK | Framework | `vercel` | — | Wrap in Vercel `tool()` with `z.object({}).passthrough()` | +| LangChain | Framework | `generic` | — | Wrap in `DynamicStructuredTool` | +| OpenAI | Direct API | `openai` | `formatToolResult` | Pass directly | +| Anthropic | Direct API | `anthropic` | `formatToolResult` | Pass directly | ## The discover_tools pattern @@ -434,13 +412,13 @@ All provider examples above use **essential mode** (default) — 5 core tools pl Handle this in your agentic loop by merging new tools into the conversation: ```typescript +import { mergeDiscoveredTools } from '@superdoc-dev/sdk'; + const result = await dispatchSuperDocTool(client, name, args); -if (name === 'discover_tools' && result.tools) { - // Merge newly discovered tools into your provider's tool config - for (const t of result.tools) { - // ... add to your tools array in the provider's format - } +if (name === 'discover_tools') { + // Merge newly discovered tools — handles format conversion and deduplication + mergeDiscoveredTools(toolConfig, result, { provider: 'anthropic', target: 'bedrock' }); } ``` diff --git a/examples/ai/bedrock/index.py b/examples/ai/bedrock/index.py index 1e21c7e0cf..c34b122520 100644 --- a/examples/ai/bedrock/index.py +++ b/examples/ai/bedrock/index.py @@ -13,28 +13,20 @@ import sys import os -import json import boto3 -from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool +from superdoc import ( + SuperDocClient, + choose_tools, + dispatch_superdoc_tool, + format_tool_result, + format_tool_error, + merge_discovered_tools, +) MODEL_ID = os.environ.get("BEDROCK_MODEL_ID", "amazon.nova-pro-v1:0") REGION = os.environ.get("AWS_REGION", "us-east-1") -def to_bedrock_tools(anthropic_tools): - """Convert SuperDoc Anthropic-format tools to Bedrock toolSpec shape.""" - return [ - { - "toolSpec": { - "name": t["name"], - "description": t["description"], - "inputSchema": {"json": t["input_schema"]}, - } - } - for t in anthropic_tools - ] - - def main(): args = sys.argv[1:] input_path = args[0] if args else "contract.docx" @@ -45,9 +37,10 @@ def main(): client.connect() client.doc.open(doc=input_path) - # 2. Get tools in Anthropic format (Bedrock-compatible) - result = choose_tools(provider="anthropic") - tool_config = {"tools": to_bedrock_tools(result["tools"])} + # 2. Get tools in Anthropic format and convert to Bedrock toolSpec shape + sd_tools = choose_tools(provider="anthropic") + tool_config = {"tools": []} + merge_discovered_tools(tool_config, sd_tools, provider="anthropic", target="bedrock") # 3. Agentic loop bedrock = boto3.client("bedrock-runtime", region_name=REGION) @@ -84,17 +77,15 @@ def main(): result = dispatch_superdoc_tool(client, name, tool_use.get("input", {})) # discover_tools returns new tools — merge them - if name == "discover_tools" and "tools" in result: - tool_config["tools"].extend(to_bedrock_tools(result["tools"])) + if name == "discover_tools": + merge_discovered_tools(tool_config, result, provider="anthropic", target="bedrock") - # Bedrock requires json content to be a plain dict - json_result = result if isinstance(result, dict) else {"result": result} tool_results.append( - {"toolResult": {"toolUseId": tool_use["toolUseId"], "content": [{"json": json_result}]}} + format_tool_result(result, target="bedrock", tool_use_id=tool_use["toolUseId"]) ) except Exception as e: tool_results.append( - {"toolResult": {"toolUseId": tool_use["toolUseId"], "content": [{"text": f"Error: {e}"}], "status": "error"}} + format_tool_error(e, target="bedrock", tool_use_id=tool_use["toolUseId"]) ) messages.append({"role": "user", "content": tool_results}) diff --git a/examples/ai/bedrock/index.ts b/examples/ai/bedrock/index.ts index 4a777a6b78..e4e215c7ca 100644 --- a/examples/ai/bedrock/index.ts +++ b/examples/ai/bedrock/index.ts @@ -20,6 +20,9 @@ import { createSuperDocClient, chooseTools, dispatchSuperDocTool, + formatToolResult, + formatToolError, + mergeDiscoveredTools, } from '@superdoc-dev/sdk'; const MODEL_ID = process.env.BEDROCK_MODEL_ID ?? 'amazon.nova-pro-v1:0'; @@ -33,19 +36,10 @@ async function main() { await client.connect(); await client.doc.open({ doc: inputPath }); - // 2. Get tools in Anthropic format (Bedrock-compatible) and convert to toolSpec shape + // 2. Get tools in Anthropic format and convert to Bedrock toolSpec shape const { tools: sdTools } = await chooseTools({ provider: 'anthropic' }); - const toolConfig = { - tools: (sdTools as Array<{ name: string; description: string; input_schema: Record }>).map( - (t): Tool => ({ - toolSpec: { - name: t.name, - description: t.description, - inputSchema: { json: t.input_schema }, - }, - }), - ), - }; + const toolConfig = { tools: [] as Tool[] }; + mergeDiscoveredTools(toolConfig, { tools: sdTools }, { provider: 'anthropic', target: 'bedrock' }); // 3. Agentic loop const bedrock = new BedrockRuntimeClient({ region: REGION }); @@ -83,18 +77,12 @@ async function main() { // discover_tools returns new tools — merge them into toolConfig if (name === 'discover_tools') { - for (const t of (result as { tools?: any[] }).tools ?? []) { - toolConfig.tools.push({ toolSpec: { name: t.name, description: t.description, inputSchema: { json: t.input_schema } } }); - } + mergeDiscoveredTools(toolConfig, result, { provider: 'anthropic', target: 'bedrock' }); } - // Bedrock requires json content to be a plain object - const jsonResult = (typeof result === 'object' && result !== null && !Array.isArray(result)) - ? result as Record - : { result }; - results.push({ toolResult: { toolUseId, content: [{ json: jsonResult }] } } as ContentBlock); + results.push(formatToolResult(result, { target: 'bedrock', toolUseId }) as ContentBlock); } catch (err) { - results.push({ toolResult: { toolUseId, content: [{ text: `Error: ${(err as Error).message}` }], status: 'error' } } as ContentBlock); + results.push(formatToolError(err, { target: 'bedrock', toolUseId }) as ContentBlock); } } messages.push({ role: 'user', content: results }); diff --git a/examples/ai/vertex/index.py b/examples/ai/vertex/index.py index 3b899bdf4f..3fa3822260 100644 --- a/examples/ai/vertex/index.py +++ b/examples/ai/vertex/index.py @@ -15,31 +15,23 @@ import os from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration, Part import vertexai -from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool +from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool, sanitize_tool_schemas PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", "your-project-id") LOCATION = os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1") MODEL = os.environ.get("VERTEX_MODEL", "gemini-2.5-pro") -def sanitize_schema(obj): - """Recursively strip JSON Schema keywords unsupported by Vertex AI (e.g. `const`).""" - if isinstance(obj, list): - return [sanitize_schema(item) for item in obj] - if isinstance(obj, dict): - return {k: sanitize_schema(v) for k, v in obj.items() if k != "const"} - return obj - - def to_vertex_tools(generic_tools): """Convert SuperDoc generic-format tools to Vertex AI function declarations.""" + sanitized = sanitize_tool_schemas(generic_tools, "vertex") declarations = [ FunctionDeclaration( name=t["name"], description=t["description"], - parameters=sanitize_schema(t["parameters"]), + parameters=t["parameters"], ) - for t in generic_tools + for t in sanitized ] return [Tool(function_declarations=declarations)] @@ -93,12 +85,13 @@ def main(): # discover_tools returns new tools — merge them if name == "discover_tools" and "tools" in result: - for t in result["tools"]: + sanitized = sanitize_tool_schemas(result["tools"], "vertex") + for t in sanitized: vertex_tools[0].function_declarations.append( FunctionDeclaration( name=t["name"], description=t["description"], - parameters=sanitize_schema(t["parameters"]), + parameters=t["parameters"], ) ) diff --git a/examples/ai/vertex/index.ts b/examples/ai/vertex/index.ts index a87e98fc8b..04b4fde846 100644 --- a/examples/ai/vertex/index.ts +++ b/examples/ai/vertex/index.ts @@ -13,43 +13,20 @@ import { VertexAI, type FunctionDeclaration, type Tool as VertexTool, - type Content, type Part, } from '@google-cloud/vertexai'; import { createSuperDocClient, chooseTools, dispatchSuperDocTool, + sanitizeToolSchemas, + mergeDiscoveredTools, } from '@superdoc-dev/sdk'; const PROJECT = process.env.GOOGLE_CLOUD_PROJECT ?? 'your-project-id'; const LOCATION = process.env.GOOGLE_CLOUD_LOCATION ?? 'us-central1'; const MODEL = process.env.VERTEX_MODEL ?? 'gemini-2.5-pro'; -/** Recursively strip JSON Schema keywords unsupported by Vertex AI (e.g. `const`). */ -function sanitizeSchema(obj: unknown): unknown { - if (Array.isArray(obj)) return obj.map(sanitizeSchema); - if (typeof obj !== 'object' || obj === null) return obj; - const result: Record = {}; - for (const [key, value] of Object.entries(obj as Record)) { - if (key === 'const') continue; - result[key] = sanitizeSchema(value); - } - return result; -} - -/** Convert SuperDoc generic-format tools to Vertex AI function declarations. */ -function toVertexTools( - sdTools: Array<{ name: string; description: string; parameters: Record }>, -): VertexTool[] { - const declarations: FunctionDeclaration[] = sdTools.map((t) => ({ - name: t.name, - description: t.description, - parameters: sanitizeSchema(t.parameters) as FunctionDeclaration['parameters'], - })); - return [{ functionDeclarations: declarations }]; -} - async function main() { const [inputPath = 'contract.docx', outputPath = 'reviewed.docx'] = process.argv.slice(2); @@ -58,11 +35,16 @@ async function main() { await client.connect(); await client.doc.open({ doc: inputPath }); - // 2. Get tools in generic format and convert to Vertex shape + // 2. Get tools in generic format, sanitize for Vertex, and build declarations const { tools: sdTools } = await chooseTools({ provider: 'generic' }); - const vertexTools = toVertexTools( - sdTools as Array<{ name: string; description: string; parameters: Record }>, - ); + const sanitized = sanitizeToolSchemas(sdTools, 'vertex') as Array<{ name: string; description: string; parameters: Record }>; + const vertexTools: VertexTool[] = [{ + functionDeclarations: sanitized.map((t): FunctionDeclaration => ({ + name: t.name, + description: t.description, + parameters: t.parameters as FunctionDeclaration['parameters'], + })), + }]; // 3. Set up Vertex AI const vertexAI = new VertexAI({ project: PROJECT, location: LOCATION }); @@ -101,14 +83,7 @@ async function main() { // discover_tools returns new tools — merge them if (name === 'discover_tools') { - const newTools = (result as { tools?: any[] }).tools ?? []; - vertexTools[0].functionDeclarations!.push( - ...newTools.map((t: any) => ({ - name: t.name, - description: t.description, - parameters: sanitizeSchema(t.parameters), - })), - ); + mergeDiscoveredTools(vertexTools, result, { provider: 'generic', target: 'vertex' }); } functionResponses.push({ diff --git a/packages/sdk/langs/node/src/helpers/platform.ts b/packages/sdk/langs/node/src/helpers/platform.ts index 9bdb8a5179..1d8ecad951 100644 --- a/packages/sdk/langs/node/src/helpers/platform.ts +++ b/packages/sdk/langs/node/src/helpers/platform.ts @@ -274,7 +274,7 @@ function collectExistingNames(toolConfig: unknown, target?: PlatformTarget): Set } /** Convert a discovered tool to the platform-native shape and push into config. */ -function formatToolForConfig(tool: unknown, provider: ToolProvider, target?: PlatformTarget): unknown { +function formatToolForConfig(tool: unknown, _provider: ToolProvider, target?: PlatformTarget): unknown { const obj = tool as Record; if (target === 'bedrock') { From b20967a107813910ee190cc85b95f749ea6eeaae Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 8 Mar 2026 19:42:36 -0300 Subject: [PATCH 4/9] fix(sdk): handle discover_tools as meta-tool in examples discover_tools is a synthetic meta-tool not in tool-name-map.json, so dispatchSuperDocTool throws TOOL_NOT_FOUND. Fix by intercepting discover_tools before dispatch and handling it client-side via chooseTools. Framework examples (Vercel AI, LangChain) now use mode='all' since they can't inject tools mid-conversation. Also fix LangChain Python schema inference issue. --- .../document-engine/ai-agents/integrations.mdx | 18 ++++++++++++------ examples/ai/bedrock/index.py | 11 +++++++---- examples/ai/bedrock/index.ts | 11 ++++++++--- examples/ai/langchain/index.py | 8 +++++--- examples/ai/langchain/index.ts | 4 ++-- examples/ai/vercel-ai/index.ts | 4 ++-- examples/ai/vertex/index.py | 14 +++++++++----- examples/ai/vertex/index.ts | 11 ++++++++--- 8 files changed, 53 insertions(+), 28 deletions(-) diff --git a/apps/docs/document-engine/ai-agents/integrations.mdx b/apps/docs/document-engine/ai-agents/integrations.mdx index 242c781bdd..fb16e4bd99 100644 --- a/apps/docs/document-engine/ai-agents/integrations.mdx +++ b/apps/docs/document-engine/ai-agents/integrations.mdx @@ -409,19 +409,25 @@ The SDK ships pre-formatted tools for each integration. The conversion is minima All provider examples above use **essential mode** (default) — 5 core tools plus `discover_tools`. When the model needs more tools (comments, formatting, tables, etc.), it calls `discover_tools` to load them dynamically. -Handle this in your agentic loop by merging new tools into the conversation: +Handle this in your agentic loop. Since `discover_tools` is a meta-tool (not a document operation), intercept it before dispatching: ```typescript -import { mergeDiscoveredTools } from '@superdoc-dev/sdk'; - -const result = await dispatchSuperDocTool(client, name, args); +import { chooseTools, mergeDiscoveredTools } from '@superdoc-dev/sdk'; if (name === 'discover_tools') { - // Merge newly discovered tools — handles format conversion and deduplication - mergeDiscoveredTools(toolConfig, result, { provider: 'anthropic', target: 'bedrock' }); + // discover_tools is a meta-tool — handle client-side via chooseTools + const discovered = await chooseTools({ provider: 'anthropic', groups: args.groups }); + mergeDiscoveredTools(toolConfig, discovered, { provider: 'anthropic', target: 'bedrock' }); + result = discovered; +} else { + result = await dispatchSuperDocTool(client, name, args); } ``` + +Framework-managed examples (Vercel AI, LangChain) use `mode: 'all'` instead, since they can't inject new tools mid-conversation. + + See the [LLM Tools guide](/document-engine/ai-agents/llm-tools#the-discover_tools-pattern) for details. ## Tracked changes diff --git a/examples/ai/bedrock/index.py b/examples/ai/bedrock/index.py index c34b122520..f65340fb4b 100644 --- a/examples/ai/bedrock/index.py +++ b/examples/ai/bedrock/index.py @@ -74,11 +74,14 @@ def main(): print(f" Tool: {name}") try: - result = dispatch_superdoc_tool(client, name, tool_use.get("input", {})) - - # discover_tools returns new tools — merge them if name == "discover_tools": - merge_discovered_tools(tool_config, result, provider="anthropic", target="bedrock") + # discover_tools is a meta-tool — handle client-side via choose_tools + groups = tool_use.get("input", {}).get("groups") + discovered = choose_tools(provider="anthropic", groups=groups) + merge_discovered_tools(tool_config, discovered, provider="anthropic", target="bedrock") + result = discovered + else: + result = dispatch_superdoc_tool(client, name, tool_use.get("input", {})) tool_results.append( format_tool_result(result, target="bedrock", tool_use_id=tool_use["toolUseId"]) diff --git a/examples/ai/bedrock/index.ts b/examples/ai/bedrock/index.ts index e4e215c7ca..dc37e19b11 100644 --- a/examples/ai/bedrock/index.ts +++ b/examples/ai/bedrock/index.ts @@ -73,11 +73,16 @@ async function main() { const { name, input, toolUseId } = block.toolUse!; console.log(` Tool: ${name}`); try { - const result = await dispatchSuperDocTool(client, name!, (input ?? {}) as Record); + let result: unknown; - // discover_tools returns new tools — merge them into toolConfig if (name === 'discover_tools') { - mergeDiscoveredTools(toolConfig, result, { provider: 'anthropic', target: 'bedrock' }); + // discover_tools is a meta-tool — handle client-side via chooseTools + const groups = ((input ?? {}) as Record).groups as string[] | undefined; + const discovered = await chooseTools({ provider: 'anthropic', groups }); + mergeDiscoveredTools(toolConfig, discovered, { provider: 'anthropic', target: 'bedrock' }); + result = discovered; + } else { + result = await dispatchSuperDocTool(client, name!, (input ?? {}) as Record); } results.push(formatToolResult(result, { target: 'bedrock', toolUseId }) as ContentBlock); diff --git a/examples/ai/langchain/index.py b/examples/ai/langchain/index.py index f6469057e8..01b09ae2b2 100644 --- a/examples/ai/langchain/index.py +++ b/examples/ai/langchain/index.py @@ -23,15 +23,16 @@ def make_superdoc_tool(client, tool_def): """Wrap a SuperDoc tool definition as a LangChain StructuredTool.""" - def invoke(args: dict) -> str: + def invoke(**kwargs) -> str: print(f" Tool: {tool_def['name']}") - result = dispatch_superdoc_tool(client, tool_def["name"], args) + result = dispatch_superdoc_tool(client, tool_def["name"], kwargs) return json.dumps(result) return StructuredTool.from_function( func=invoke, name=tool_def["name"], description=tool_def["description"], + infer_schema=False, ) @@ -46,7 +47,8 @@ def main(): client.doc.open(doc=input_path) # 2. Get tools in generic format and wrap as LangChain tools - result = choose_tools(provider="generic") + # Use mode="all" — no discover_tools since the framework manages a fixed tool set + result = choose_tools(provider="generic", mode="all") tools = [make_superdoc_tool(client, t) for t in result["tools"]] # 3. Create a ReAct agent diff --git a/examples/ai/langchain/index.ts b/examples/ai/langchain/index.ts index 4e3ce9b8cb..ef91c07ced 100644 --- a/examples/ai/langchain/index.ts +++ b/examples/ai/langchain/index.ts @@ -28,8 +28,8 @@ async function main() { await client.connect(); await client.doc.open({ doc: inputPath }); - // 2. Get tools in generic format and wrap as LangChain tools - const { tools: sdTools } = await chooseTools({ provider: 'generic' }); + // 2. Get tools in generic format and wrap as LangChain tools (all tools — no discover_tools since the framework manages a fixed tool set) + const { tools: sdTools } = await chooseTools({ provider: 'generic', mode: 'all' }); const langchainTools = ( sdTools as Array<{ name: string; description: string; parameters: Record }> diff --git a/examples/ai/vercel-ai/index.ts b/examples/ai/vercel-ai/index.ts index c45b3a6d30..aac23c2bd9 100644 --- a/examples/ai/vercel-ai/index.ts +++ b/examples/ai/vercel-ai/index.ts @@ -27,8 +27,8 @@ async function main() { await client.connect(); await client.doc.open({ doc: inputPath }); - // 2. Get tools in Vercel AI format - const { tools: sdTools } = await chooseTools({ provider: 'vercel' }); + // 2. Get tools in Vercel AI format (all tools — no discover_tools since the framework manages a fixed tool set) + const { tools: sdTools } = await chooseTools({ provider: 'vercel', mode: 'all' }); // Convert SuperDoc tool definitions to Vercel AI `tool()` objects const vercelTools: Record> = {}; diff --git a/examples/ai/vertex/index.py b/examples/ai/vertex/index.py index 3fa3822260..bfbfbe6bf4 100644 --- a/examples/ai/vertex/index.py +++ b/examples/ai/vertex/index.py @@ -81,11 +81,12 @@ def main(): print(f" Tool: {name}") try: - result = dispatch_superdoc_tool(client, name, args) - - # discover_tools returns new tools — merge them - if name == "discover_tools" and "tools" in result: - sanitized = sanitize_tool_schemas(result["tools"], "vertex") + if name == "discover_tools": + # discover_tools is a meta-tool — handle client-side via choose_tools + groups = args.get("groups") + discovered = choose_tools(provider="generic", groups=groups) + new_tools = discovered.get("tools", []) + sanitized = sanitize_tool_schemas(new_tools, "vertex") for t in sanitized: vertex_tools[0].function_declarations.append( FunctionDeclaration( @@ -94,6 +95,9 @@ def main(): parameters=t["parameters"], ) ) + result = discovered + else: + result = dispatch_superdoc_tool(client, name, args) function_responses.append( Part.from_function_response(name=name, response=result) diff --git a/examples/ai/vertex/index.ts b/examples/ai/vertex/index.ts index 04b4fde846..9a8c15c7e0 100644 --- a/examples/ai/vertex/index.ts +++ b/examples/ai/vertex/index.ts @@ -79,11 +79,16 @@ async function main() { const { name, args } = part.functionCall!; console.log(` Tool: ${name}`); try { - const result = await dispatchSuperDocTool(client, name, (args ?? {}) as Record); + let result: unknown; - // discover_tools returns new tools — merge them if (name === 'discover_tools') { - mergeDiscoveredTools(vertexTools, result, { provider: 'generic', target: 'vertex' }); + // discover_tools is a meta-tool — handle client-side via chooseTools + const groups = ((args ?? {}) as Record).groups as string[] | undefined; + const discovered = await chooseTools({ provider: 'generic', groups }); + mergeDiscoveredTools(vertexTools, discovered, { provider: 'generic', target: 'vertex' }); + result = discovered; + } else { + result = await dispatchSuperDocTool(client, name, (args ?? {}) as Record); } functionResponses.push({ From 6423d3ded3c10d1e873418227942a42121994486 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 8 Mar 2026 21:31:00 -0300 Subject: [PATCH 5/9] fix(sdk): strip doc/sessionId in dispatchSuperDocTool and fix example save - dispatchSuperDocTool now strips doc/sessionId from args before dispatch, since the SDK client manages session targeting after doc.open() - Examples use absolute paths and copy-then-edit pattern to preserve the original document when saving - Fix ToolGroup type cast in discover_tools handling --- examples/ai/bedrock/index.py | 15 +++++++++------ examples/ai/bedrock/index.ts | 18 ++++++++++++------ examples/ai/langchain/index.py | 15 +++++++++------ examples/ai/langchain/index.ts | 15 ++++++++++----- examples/ai/vercel-ai/index.ts | 15 ++++++++++----- examples/ai/vertex/index.py | 15 +++++++++------ examples/ai/vertex/index.ts | 18 ++++++++++++------ packages/sdk/langs/node/src/tools.ts | 10 ++++++++-- .../sdk/langs/python/superdoc/tools_api.py | 8 ++++++++ 9 files changed, 87 insertions(+), 42 deletions(-) diff --git a/examples/ai/bedrock/index.py b/examples/ai/bedrock/index.py index f65340fb4b..26ca65f5b2 100644 --- a/examples/ai/bedrock/index.py +++ b/examples/ai/bedrock/index.py @@ -13,6 +13,8 @@ import sys import os +import shutil +from pathlib import Path import boto3 from superdoc import ( SuperDocClient, @@ -29,13 +31,14 @@ def main(): args = sys.argv[1:] - input_path = args[0] if args else "contract.docx" - output_path = args[1] if len(args) > 1 else "reviewed.docx" + input_path = str(Path(args[0] if args else "contract.docx").resolve()) + output_path = str(Path(args[1] if len(args) > 1 else "reviewed.docx").resolve()) - # 1. Connect to SuperDoc + # 1. Connect to SuperDoc — copy to output path so the original is preserved + shutil.copy2(input_path, output_path) client = SuperDocClient() client.connect() - client.doc.open(doc=input_path) + client.doc.open(doc=output_path) # 2. Get tools in Anthropic format and convert to Bedrock toolSpec shape sd_tools = choose_tools(provider="anthropic") @@ -93,8 +96,8 @@ def main(): messages.append({"role": "user", "content": tool_results}) - # 4. Save - client.doc.save(doc=output_path) + # 4. Save (in-place to the copy) + client.doc.save() client.dispose() print(f"\nSaved to {output_path}") diff --git a/examples/ai/bedrock/index.ts b/examples/ai/bedrock/index.ts index dc37e19b11..2567d935b6 100644 --- a/examples/ai/bedrock/index.ts +++ b/examples/ai/bedrock/index.ts @@ -9,6 +9,8 @@ * Requires: AWS credentials configured, Bedrock model access enabled. */ +import path from 'node:path'; +import { copyFileSync } from 'node:fs'; import { BedrockRuntimeClient, ConverseCommand, @@ -23,18 +25,22 @@ import { formatToolResult, formatToolError, mergeDiscoveredTools, + type ToolGroup, } from '@superdoc-dev/sdk'; const MODEL_ID = process.env.BEDROCK_MODEL_ID ?? 'amazon.nova-pro-v1:0'; const REGION = process.env.AWS_REGION ?? 'us-east-1'; async function main() { - const [inputPath = 'contract.docx', outputPath = 'reviewed.docx'] = process.argv.slice(2); + const [rawInput = 'contract.docx', rawOutput = 'reviewed.docx'] = process.argv.slice(2); + const inputPath = path.resolve(rawInput); + const outputPath = path.resolve(rawOutput); - // 1. Connect to SuperDoc and open the document + // 1. Connect to SuperDoc — copy to output path so the original is preserved + copyFileSync(inputPath, outputPath); const client = createSuperDocClient(); await client.connect(); - await client.doc.open({ doc: inputPath }); + await client.doc.open({ doc: outputPath }); // 2. Get tools in Anthropic format and convert to Bedrock toolSpec shape const { tools: sdTools } = await chooseTools({ provider: 'anthropic' }); @@ -77,7 +83,7 @@ async function main() { if (name === 'discover_tools') { // discover_tools is a meta-tool — handle client-side via chooseTools - const groups = ((input ?? {}) as Record).groups as string[] | undefined; + const groups = ((input ?? {}) as Record).groups as ToolGroup[] | undefined; const discovered = await chooseTools({ provider: 'anthropic', groups }); mergeDiscoveredTools(toolConfig, discovered, { provider: 'anthropic', target: 'bedrock' }); result = discovered; @@ -93,8 +99,8 @@ async function main() { messages.push({ role: 'user', content: results }); } - // 4. Save - await client.doc.save({ doc: outputPath }); + // 4. Save (in-place to the copy) + await client.doc.save(); await client.dispose(); console.log(`\nSaved to ${outputPath}`); } diff --git a/examples/ai/langchain/index.py b/examples/ai/langchain/index.py index 01b09ae2b2..970f1a195e 100644 --- a/examples/ai/langchain/index.py +++ b/examples/ai/langchain/index.py @@ -13,6 +13,8 @@ import sys import json +import shutil +from pathlib import Path from langchain_openai import ChatOpenAI from langchain_core.tools import StructuredTool from langgraph.prebuilt import create_react_agent @@ -38,13 +40,14 @@ def invoke(**kwargs) -> str: def main(): args = sys.argv[1:] - input_path = args[0] if args else "contract.docx" - output_path = args[1] if len(args) > 1 else "reviewed.docx" + input_path = str(Path(args[0] if args else "contract.docx").resolve()) + output_path = str(Path(args[1] if len(args) > 1 else "reviewed.docx").resolve()) - # 1. Connect to SuperDoc + # 1. Connect to SuperDoc — copy to output path so the original is preserved + shutil.copy2(input_path, output_path) client = SuperDocClient() client.connect() - client.doc.open(doc=input_path) + client.doc.open(doc=output_path) # 2. Get tools in generic format and wrap as LangChain tools # Use mode="all" — no discover_tools since the framework manages a fixed tool set @@ -67,8 +70,8 @@ def main(): last_message = result["messages"][-1] print(last_message.content) - # 5. Save - client.doc.save(doc=output_path) + # 5. Save (in-place to the copy) + client.doc.save() client.dispose() print(f"\nSaved to {output_path}") diff --git a/examples/ai/langchain/index.ts b/examples/ai/langchain/index.ts index ef91c07ced..58363bbb8f 100644 --- a/examples/ai/langchain/index.ts +++ b/examples/ai/langchain/index.ts @@ -9,6 +9,8 @@ * Requires: OPENAI_API_KEY (or swap ChatOpenAI for ChatAnthropic, ChatGoogleGenerativeAI, etc.) */ +import path from 'node:path'; +import { copyFileSync } from 'node:fs'; import { ChatOpenAI } from '@langchain/openai'; import { DynamicStructuredTool } from '@langchain/core/tools'; import { createReactAgent } from '@langchain/langgraph/prebuilt'; @@ -21,12 +23,15 @@ import { } from '@superdoc-dev/sdk'; async function main() { - const [inputPath = 'contract.docx', outputPath = 'reviewed.docx'] = process.argv.slice(2); + const [rawInput = 'contract.docx', rawOutput = 'reviewed.docx'] = process.argv.slice(2); + const inputPath = path.resolve(rawInput); + const outputPath = path.resolve(rawOutput); - // 1. Connect to SuperDoc + // 1. Connect to SuperDoc — copy to output path so the original is preserved + copyFileSync(inputPath, outputPath); const client = createSuperDocClient(); await client.connect(); - await client.doc.open({ doc: inputPath }); + await client.doc.open({ doc: outputPath }); // 2. Get tools in generic format and wrap as LangChain tools (all tools — no discover_tools since the framework manages a fixed tool set) const { tools: sdTools } = await chooseTools({ provider: 'generic', mode: 'all' }); @@ -63,8 +68,8 @@ async function main() { const lastMessage = result.messages[result.messages.length - 1]; console.log(lastMessage.content); - // 5. Save - await client.doc.save({ doc: outputPath }); + // 5. Save (in-place to the copy) + await client.doc.save(); await client.dispose(); console.log(`\nSaved to ${outputPath}`); } diff --git a/examples/ai/vercel-ai/index.ts b/examples/ai/vercel-ai/index.ts index aac23c2bd9..203cc0854f 100644 --- a/examples/ai/vercel-ai/index.ts +++ b/examples/ai/vercel-ai/index.ts @@ -10,6 +10,8 @@ * Anthropic, Google, Mistral, and others with the same interface). */ +import path from 'node:path'; +import { copyFileSync } from 'node:fs'; import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { @@ -20,12 +22,15 @@ import { import { z } from 'zod'; async function main() { - const [inputPath = 'contract.docx', outputPath = 'reviewed.docx'] = process.argv.slice(2); + const [rawInput = 'contract.docx', rawOutput = 'reviewed.docx'] = process.argv.slice(2); + const inputPath = path.resolve(rawInput); + const outputPath = path.resolve(rawOutput); - // 1. Connect to SuperDoc + // 1. Connect to SuperDoc — copy to output path so the original is preserved + copyFileSync(inputPath, outputPath); const client = createSuperDocClient(); await client.connect(); - await client.doc.open({ doc: inputPath }); + await client.doc.open({ doc: outputPath }); // 2. Get tools in Vercel AI format (all tools — no discover_tools since the framework manages a fixed tool set) const { tools: sdTools } = await chooseTools({ provider: 'vercel', mode: 'all' }); @@ -55,8 +60,8 @@ async function main() { console.log(result.text); - // 4. Save - await client.doc.save({ doc: outputPath }); + // 4. Save (in-place to the copy) + await client.doc.save(); await client.dispose(); console.log(`\nSaved to ${outputPath}`); } diff --git a/examples/ai/vertex/index.py b/examples/ai/vertex/index.py index bfbfbe6bf4..9f34fca8af 100644 --- a/examples/ai/vertex/index.py +++ b/examples/ai/vertex/index.py @@ -13,6 +13,8 @@ import sys import os +import shutil +from pathlib import Path from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration, Part import vertexai from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool, sanitize_tool_schemas @@ -38,13 +40,14 @@ def to_vertex_tools(generic_tools): def main(): args = sys.argv[1:] - input_path = args[0] if args else "contract.docx" - output_path = args[1] if len(args) > 1 else "reviewed.docx" + input_path = str(Path(args[0] if args else "contract.docx").resolve()) + output_path = str(Path(args[1] if len(args) > 1 else "reviewed.docx").resolve()) - # 1. Connect to SuperDoc + # 1. Connect to SuperDoc — copy to output path so the original is preserved + shutil.copy2(input_path, output_path) client = SuperDocClient() client.connect() - client.doc.open(doc=input_path) + client.doc.open(doc=output_path) # 2. Get tools in generic format and convert to Vertex shape result = choose_tools(provider="generic") @@ -109,8 +112,8 @@ def main(): response = chat.send_message(function_responses) - # 5. Save - client.doc.save(doc=output_path) + # 5. Save (in-place to the copy) + client.doc.save() client.dispose() print(f"\nSaved to {output_path}") diff --git a/examples/ai/vertex/index.ts b/examples/ai/vertex/index.ts index 9a8c15c7e0..4495cda794 100644 --- a/examples/ai/vertex/index.ts +++ b/examples/ai/vertex/index.ts @@ -9,6 +9,8 @@ * Requires: Google Cloud credentials configured (gcloud auth application-default login). */ +import path from 'node:path'; +import { copyFileSync } from 'node:fs'; import { VertexAI, type FunctionDeclaration, @@ -21,6 +23,7 @@ import { dispatchSuperDocTool, sanitizeToolSchemas, mergeDiscoveredTools, + type ToolGroup, } from '@superdoc-dev/sdk'; const PROJECT = process.env.GOOGLE_CLOUD_PROJECT ?? 'your-project-id'; @@ -28,12 +31,15 @@ const LOCATION = process.env.GOOGLE_CLOUD_LOCATION ?? 'us-central1'; const MODEL = process.env.VERTEX_MODEL ?? 'gemini-2.5-pro'; async function main() { - const [inputPath = 'contract.docx', outputPath = 'reviewed.docx'] = process.argv.slice(2); + const [rawInput = 'contract.docx', rawOutput = 'reviewed.docx'] = process.argv.slice(2); + const inputPath = path.resolve(rawInput); + const outputPath = path.resolve(rawOutput); - // 1. Connect to SuperDoc + // 1. Connect to SuperDoc — copy to output path so the original is preserved + copyFileSync(inputPath, outputPath); const client = createSuperDocClient(); await client.connect(); - await client.doc.open({ doc: inputPath }); + await client.doc.open({ doc: outputPath }); // 2. Get tools in generic format, sanitize for Vertex, and build declarations const { tools: sdTools } = await chooseTools({ provider: 'generic' }); @@ -83,7 +89,7 @@ async function main() { if (name === 'discover_tools') { // discover_tools is a meta-tool — handle client-side via chooseTools - const groups = ((args ?? {}) as Record).groups as string[] | undefined; + const groups = ((args ?? {}) as Record).groups as ToolGroup[] | undefined; const discovered = await chooseTools({ provider: 'generic', groups }); mergeDiscoveredTools(vertexTools, discovered, { provider: 'generic', target: 'vertex' }); result = discovered; @@ -104,8 +110,8 @@ async function main() { response = await chat.sendMessage(functionResponses); } - // 5. Save - await client.doc.save({ doc: outputPath }); + // 5. Save (in-place to the copy) + await client.doc.save(); await client.dispose(); console.log(`\nSaved to ${outputPath}`); } diff --git a/packages/sdk/langs/node/src/tools.ts b/packages/sdk/langs/node/src/tools.ts index 8de238486c..762f7d8416 100644 --- a/packages/sdk/langs/node/src/tools.ts +++ b/packages/sdk/langs/node/src/tools.ts @@ -428,7 +428,13 @@ export async function dispatchSuperDocTool( invalidArgument(`Tool arguments for ${toolName} must be an object.`); } - validateDispatchArgs(operationId, args); + // Strip doc/sessionId — the SDK client manages session targeting after doc.open(). + // Models fill these in because the tool schemas expose them, but passing them + // alongside an active session causes "stateless input.doc cannot be combined + // with a session target" errors. + const { doc: _doc, sessionId: _sid, ...cleanArgs } = args; + + validateDispatchArgs(operationId, cleanArgs); const method = resolveDocApiMethod(client, operationId); - return method(args, invokeOptions); + return method(cleanArgs, invokeOptions); } diff --git a/packages/sdk/langs/python/superdoc/tools_api.py b/packages/sdk/langs/python/superdoc/tools_api.py index 26cdce9b3d..299addbd5f 100644 --- a/packages/sdk/langs/python/superdoc/tools_api.py +++ b/packages/sdk/langs/python/superdoc/tools_api.py @@ -385,6 +385,11 @@ def dispatch_superdoc_tool( if not isinstance(payload, dict): raise SuperDocError('Tool arguments must be an object.', code='INVALID_ARGUMENT', details={'toolName': tool_name}) + # Strip doc/sessionId — the SDK client manages session targeting after doc.open(). + # Models fill these in because the tool schemas expose them, but passing them + # alongside an active session causes errors. + payload = {k: v for k, v in payload.items() if k not in ('doc', 'sessionId')} + _validate_dispatch_args(operation_id, payload) method = _resolve_doc_method(client, operation_id) @@ -413,6 +418,9 @@ async def dispatch_superdoc_tool_async( if not isinstance(payload, dict): raise SuperDocError('Tool arguments must be an object.', code='INVALID_ARGUMENT', details={'toolName': tool_name}) + # Strip doc/sessionId — same as sync version above. + payload = {k: v for k, v in payload.items() if k not in ('doc', 'sessionId')} + _validate_dispatch_args(operation_id, payload) method = _resolve_doc_method(client, operation_id) kwargs = dict(invoke_options or {}) From 0fab334d496c67800390e130b84062b28386fad6 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 8 Mar 2026 21:33:32 -0300 Subject: [PATCH 6/9] docs: fix framework tool mode and discover_tools pattern in docs - Vercel AI and LangChain snippets use mode: 'all' since frameworks can't handle discover_tools mid-conversation - LangChain Python uses **kwargs + infer_schema=False to fix schema inference - discover_tools pattern shows correct client-side interception via chooseTools instead of dispatchSuperDocTool --- .../document-engine/ai-agents/integrations.mdx | 14 +++++++++----- apps/docs/document-engine/ai-agents/llm-tools.mdx | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/docs/document-engine/ai-agents/integrations.mdx b/apps/docs/document-engine/ai-agents/integrations.mdx index fb16e4bd99..1cb34625a9 100644 --- a/apps/docs/document-engine/ai-agents/integrations.mdx +++ b/apps/docs/document-engine/ai-agents/integrations.mdx @@ -263,7 +263,8 @@ Use SuperDoc tools with agent frameworks. The framework manages the agentic loop await client.connect(); await client.doc.open({ doc: './contract.docx' }); - const { tools: sdTools } = await chooseTools({ provider: 'vercel' }); + // All tools — no discover_tools since the framework manages a fixed tool set + const { tools: sdTools } = await chooseTools({ provider: 'vercel', mode: 'all' }); // Wrap as Vercel AI tool() objects const tools = {}; @@ -312,7 +313,8 @@ Use SuperDoc tools with agent frameworks. The framework manages the agentic loop await client.connect(); await client.doc.open({ doc: './contract.docx' }); - const { tools: sdTools } = await chooseTools({ provider: 'generic' }); + // All tools — no discover_tools since the framework manages a fixed tool set + const { tools: sdTools } = await chooseTools({ provider: 'generic', mode: 'all' }); // Wrap as LangChain DynamicStructuredTool objects const tools = sdTools.map( @@ -359,14 +361,16 @@ Use SuperDoc tools with agent frameworks. The framework manages the agentic loop client.connect() client.doc.open(doc="./contract.docx") - result = choose_tools(provider="generic") + # All tools — no discover_tools since the framework manages a fixed tool set + result = choose_tools(provider="generic", mode="all") # Wrap as LangChain StructuredTool objects def make_tool(t): - def invoke(args: dict) -> str: - return json.dumps(dispatch_superdoc_tool(client, t["name"], args)) + def invoke(**kwargs) -> str: + return json.dumps(dispatch_superdoc_tool(client, t["name"], kwargs)) return StructuredTool.from_function( func=invoke, name=t["name"], description=t["description"], + infer_schema=False, ) tools = [make_tool(t) for t in result["tools"]] diff --git a/apps/docs/document-engine/ai-agents/llm-tools.mdx b/apps/docs/document-engine/ai-agents/llm-tools.mdx index 12ed982943..0edcd6a691 100644 --- a/apps/docs/document-engine/ai-agents/llm-tools.mdx +++ b/apps/docs/document-engine/ai-agents/llm-tools.mdx @@ -226,17 +226,21 @@ Tools are organized into 11 groups. In essential mode, the LLM can load any grou ## The discover_tools pattern -When the LLM needs tools beyond the essential set, it calls `discover_tools` with the groups it wants. Your agentic loop handles this like any other tool call — `dispatchSuperDocTool` returns the new tool definitions, and you merge them into the next request. +When the LLM needs tools beyond the essential set, it calls `discover_tools` with the groups it wants. Since `discover_tools` is a meta-tool (not a document operation), intercept it before `dispatchSuperDocTool` and handle it client-side via `chooseTools`: ```typescript +import { chooseTools } from '@superdoc-dev/sdk'; + for (const call of message.tool_calls) { - const result = await dispatchSuperDocTool( - client, call.function.name, JSON.parse(call.function.arguments), - ); + let result; + const args = JSON.parse(call.function.arguments); - // discover_tools returns new tool definitions — merge them if (call.function.name === 'discover_tools') { + // Meta-tool — resolve client-side, then merge new tools + result = await chooseTools({ provider: 'openai', groups: args.groups }); tools.push(...result.tools); + } else { + result = await dispatchSuperDocTool(client, call.function.name, args); } messages.push({ From bc905c3bc00f8b66ade839124ffbae4c0eeeae9d Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 8 Mar 2026 21:52:34 -0300 Subject: [PATCH 7/9] fix: use Claude Sonnet 4.6 as default Bedrock model and fix Python examples - Switch default Bedrock model from Nova Pro to Claude Sonnet 4.6 - Fix Python examples to use dict params instead of kwargs for SDK calls - Add venv setup to Python README instructions - Add venv/ to .gitignore - Update model references in docs and READMEs to Sonnet 4.6 --- .gitignore | 1 + apps/docs/document-engine/ai-agents/integrations.mdx | 4 ++-- examples/ai/bedrock/README.md | 3 ++- examples/ai/bedrock/index.py | 8 ++++---- examples/ai/bedrock/index.ts | 2 +- examples/ai/langchain/README.md | 3 ++- examples/ai/langchain/index.py | 4 ++-- examples/ai/vercel-ai/README.md | 2 +- examples/ai/vertex/README.md | 1 + examples/ai/vertex/index.py | 6 +++--- 10 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index fd2925178a..022da923b2 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,7 @@ devtools/visual-testing/pnpm-lock.yaml __pycache__/ *.pyc *.pyo +venv/ # Generated platform companion build metadata (do not commit) packages/sdk/langs/python/platforms/**/*.egg-info/ # Generated python packaging metadata (do not commit) diff --git a/apps/docs/document-engine/ai-agents/integrations.mdx b/apps/docs/document-engine/ai-agents/integrations.mdx index 1cb34625a9..dfc81f5ed8 100644 --- a/apps/docs/document-engine/ai-agents/integrations.mdx +++ b/apps/docs/document-engine/ai-agents/integrations.mdx @@ -49,7 +49,7 @@ Use SuperDoc tools with cloud AI platforms. You write the agentic loop and contr while (true) { const res = await bedrock.send(new ConverseCommand({ - modelId: 'amazon.nova-pro-v1:0', + modelId: 'us.anthropic.claude-sonnet-4-6', messages, system: [{ text: 'You edit .docx files using SuperDoc tools.' }], toolConfig, @@ -101,7 +101,7 @@ Use SuperDoc tools with cloud AI platforms. You write the agentic loop and contr while True: response = bedrock.converse( - modelId="amazon.nova-pro-v1:0", + modelId="us.anthropic.claude-sonnet-4-6", messages=messages, system=[{"text": "You edit .docx files using SuperDoc tools."}], toolConfig=tool_config, diff --git a/examples/ai/bedrock/README.md b/examples/ai/bedrock/README.md index f5ff1ee405..b1f0601843 100644 --- a/examples/ai/bedrock/README.md +++ b/examples/ai/bedrock/README.md @@ -21,6 +21,7 @@ npx tsx index.ts contract.docx reviewed.docx ### Python ```bash +python -m venv venv && source venv/bin/activate pip install superdoc-sdk boto3 python index.py contract.docx reviewed.docx ``` @@ -30,7 +31,7 @@ python index.py contract.docx reviewed.docx | Variable | Default | Description | |----------|---------|-------------| | `AWS_REGION` | `us-east-1` | AWS region with Bedrock access | -| `BEDROCK_MODEL_ID` | `amazon.nova-pro-v1:0` | Any Bedrock model that supports tool use | +| `BEDROCK_MODEL_ID` | `us.anthropic.claude-sonnet-4-6` | Any Bedrock model that supports tool use | ## How it works diff --git a/examples/ai/bedrock/index.py b/examples/ai/bedrock/index.py index 26ca65f5b2..707dd72d8b 100644 --- a/examples/ai/bedrock/index.py +++ b/examples/ai/bedrock/index.py @@ -25,7 +25,7 @@ merge_discovered_tools, ) -MODEL_ID = os.environ.get("BEDROCK_MODEL_ID", "amazon.nova-pro-v1:0") +MODEL_ID = os.environ.get("BEDROCK_MODEL_ID", "us.anthropic.claude-sonnet-4-6") REGION = os.environ.get("AWS_REGION", "us-east-1") @@ -38,10 +38,10 @@ def main(): shutil.copy2(input_path, output_path) client = SuperDocClient() client.connect() - client.doc.open(doc=output_path) + client.doc.open({"doc": output_path}) # 2. Get tools in Anthropic format and convert to Bedrock toolSpec shape - sd_tools = choose_tools(provider="anthropic") + sd_tools = choose_tools({"provider": "anthropic"}) tool_config = {"tools": []} merge_discovered_tools(tool_config, sd_tools, provider="anthropic", target="bedrock") @@ -80,7 +80,7 @@ def main(): if name == "discover_tools": # discover_tools is a meta-tool — handle client-side via choose_tools groups = tool_use.get("input", {}).get("groups") - discovered = choose_tools(provider="anthropic", groups=groups) + discovered = choose_tools({"provider": "anthropic", "groups": groups}) merge_discovered_tools(tool_config, discovered, provider="anthropic", target="bedrock") result = discovered else: diff --git a/examples/ai/bedrock/index.ts b/examples/ai/bedrock/index.ts index 2567d935b6..ec1b3059c4 100644 --- a/examples/ai/bedrock/index.ts +++ b/examples/ai/bedrock/index.ts @@ -28,7 +28,7 @@ import { type ToolGroup, } from '@superdoc-dev/sdk'; -const MODEL_ID = process.env.BEDROCK_MODEL_ID ?? 'amazon.nova-pro-v1:0'; +const MODEL_ID = process.env.BEDROCK_MODEL_ID ?? 'us.anthropic.claude-sonnet-4-6'; const REGION = process.env.AWS_REGION ?? 'us-east-1'; async function main() { diff --git a/examples/ai/langchain/README.md b/examples/ai/langchain/README.md index 469dd538ef..18a36098f8 100644 --- a/examples/ai/langchain/README.md +++ b/examples/ai/langchain/README.md @@ -20,6 +20,7 @@ OPENAI_API_KEY=sk-... npx tsx index.ts contract.docx reviewed.docx ### Python ```bash +python -m venv venv && source venv/bin/activate pip install superdoc-sdk langchain-openai langgraph OPENAI_API_KEY=sk-... python index.py contract.docx reviewed.docx ``` @@ -35,7 +36,7 @@ model = ChatOpenAI(model="gpt-4o") # Anthropic from langchain_anthropic import ChatAnthropic -model = ChatAnthropic(model="claude-sonnet-4-6-20250514") +model = ChatAnthropic(model="claude-sonnet-4-6-20250725") # Google from langchain_google_genai import ChatGoogleGenerativeAI diff --git a/examples/ai/langchain/index.py b/examples/ai/langchain/index.py index 970f1a195e..ecbfbced00 100644 --- a/examples/ai/langchain/index.py +++ b/examples/ai/langchain/index.py @@ -47,11 +47,11 @@ def main(): shutil.copy2(input_path, output_path) client = SuperDocClient() client.connect() - client.doc.open(doc=output_path) + client.doc.open({"doc": output_path}) # 2. Get tools in generic format and wrap as LangChain tools # Use mode="all" — no discover_tools since the framework manages a fixed tool set - result = choose_tools(provider="generic", mode="all") + result = choose_tools({"provider": "generic", "mode": "all"}) tools = [make_superdoc_tool(client, t) for t in result["tools"]] # 3. Create a ReAct agent diff --git a/examples/ai/vercel-ai/README.md b/examples/ai/vercel-ai/README.md index 462dc3c051..c7ffcded76 100644 --- a/examples/ai/vercel-ai/README.md +++ b/examples/ai/vercel-ai/README.md @@ -26,7 +26,7 @@ model: openai('gpt-4o') // Anthropic import { anthropic } from '@ai-sdk/anthropic'; -model: anthropic('claude-sonnet-4-6-20250514') +model: anthropic('claude-sonnet-4-6-20250725') // Google import { google } from '@ai-sdk/google'; diff --git a/examples/ai/vertex/README.md b/examples/ai/vertex/README.md index c49d0293a6..53799e73b4 100644 --- a/examples/ai/vertex/README.md +++ b/examples/ai/vertex/README.md @@ -21,6 +21,7 @@ GOOGLE_CLOUD_PROJECT=your-project npx tsx index.ts contract.docx reviewed.docx ### Python ```bash +python -m venv venv && source venv/bin/activate pip install superdoc-sdk google-cloud-aiplatform GOOGLE_CLOUD_PROJECT=your-project python index.py contract.docx reviewed.docx ``` diff --git a/examples/ai/vertex/index.py b/examples/ai/vertex/index.py index 9f34fca8af..114441c0d5 100644 --- a/examples/ai/vertex/index.py +++ b/examples/ai/vertex/index.py @@ -47,10 +47,10 @@ def main(): shutil.copy2(input_path, output_path) client = SuperDocClient() client.connect() - client.doc.open(doc=output_path) + client.doc.open({"doc": output_path}) # 2. Get tools in generic format and convert to Vertex shape - result = choose_tools(provider="generic") + result = choose_tools({"provider": "generic"}) vertex_tools = to_vertex_tools(result["tools"]) # 3. Set up Vertex AI @@ -87,7 +87,7 @@ def main(): if name == "discover_tools": # discover_tools is a meta-tool — handle client-side via choose_tools groups = args.get("groups") - discovered = choose_tools(provider="generic", groups=groups) + discovered = choose_tools({"provider": "generic", "groups": groups}) new_tools = discovered.get("tools", []) sanitized = sanitize_tool_schemas(new_tools, "vertex") for t in sanitized: From d151fa7ea4bf72ca109aa0b9ba2678927bce31cf Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 8 Mar 2026 22:13:51 -0300 Subject: [PATCH 8/9] refactor: scope AI integration examples and docs to Bedrock only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove Vertex, Vercel AI, and LangChain examples and docs — they'll be added in a follow-up PR once the intent-based tool layer lands. --- .../ai-agents/integrations.mdx | 466 ++++-------------- examples/ai/README.md | 11 +- examples/ai/langchain/README.md | 52 -- examples/ai/langchain/contract.docx | Bin 79086 -> 0 bytes examples/ai/langchain/index.py | 80 --- examples/ai/langchain/index.ts | 77 --- examples/ai/langchain/package.json | 18 - examples/ai/vercel-ai/README.md | 41 -- examples/ai/vercel-ai/contract.docx | Bin 79086 -> 0 bytes examples/ai/vercel-ai/index.ts | 69 --- examples/ai/vercel-ai/package.json | 17 - examples/ai/vertex/README.md | 43 -- examples/ai/vertex/contract.docx | Bin 79086 -> 0 bytes examples/ai/vertex/index.py | 122 ----- examples/ai/vertex/index.ts | 119 ----- examples/ai/vertex/package.json | 15 - 16 files changed, 91 insertions(+), 1039 deletions(-) delete mode 100644 examples/ai/langchain/README.md delete mode 100644 examples/ai/langchain/contract.docx delete mode 100644 examples/ai/langchain/index.py delete mode 100644 examples/ai/langchain/index.ts delete mode 100644 examples/ai/langchain/package.json delete mode 100644 examples/ai/vercel-ai/README.md delete mode 100644 examples/ai/vercel-ai/contract.docx delete mode 100644 examples/ai/vercel-ai/index.ts delete mode 100644 examples/ai/vercel-ai/package.json delete mode 100644 examples/ai/vertex/README.md delete mode 100644 examples/ai/vertex/contract.docx delete mode 100644 examples/ai/vertex/index.py delete mode 100644 examples/ai/vertex/index.ts delete mode 100644 examples/ai/vertex/package.json diff --git a/apps/docs/document-engine/ai-agents/integrations.mdx b/apps/docs/document-engine/ai-agents/integrations.mdx index dfc81f5ed8..aedd8fb4c8 100644 --- a/apps/docs/document-engine/ai-agents/integrations.mdx +++ b/apps/docs/document-engine/ai-agents/integrations.mdx @@ -2,8 +2,8 @@ title: Integrations sidebarTitle: Integrations tag: NEW -description: Connect SuperDoc tools to AWS Bedrock, Google Vertex AI, Vercel AI SDK, LangChain, and more -keywords: "ai integrations, ai providers, agent frameworks, aws bedrock, google vertex, vercel ai, langchain, openai, anthropic, tool use, function calling, superdoc sdk" +description: Connect SuperDoc tools to AWS Bedrock and other LLM providers +keywords: "ai integrations, ai providers, aws bedrock, openai, anthropic, tool use, function calling, superdoc sdk" --- SuperDoc tools work with any LLM provider or agent framework that supports tool use. The SDK ships tool definitions in multiple formats — pick the one that matches your stack, write a conversation loop (or let the framework handle it), and dispatch tool calls through the SDK. @@ -14,400 +14,118 @@ Each example below opens a document, gives the model SuperDoc tools, and lets it LLM tools are in alpha. Tool names and schemas may change between releases. -## Cloud platforms - -Use SuperDoc tools with cloud AI platforms. You write the agentic loop and control the conversation directly. +## AWS Bedrock - - - - ```bash - npm install @superdoc-dev/sdk @aws-sdk/client-bedrock-runtime - ``` - - ```typescript - import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; - import { - createSuperDocClient, chooseTools, dispatchSuperDocTool, - formatToolResult, mergeDiscoveredTools, - } from '@superdoc-dev/sdk'; - - const client = createSuperDocClient(); - await client.connect(); - await client.doc.open({ doc: './contract.docx' }); - - // Anthropic format → Bedrock toolSpec shape - const { tools } = await chooseTools({ provider: 'anthropic' }); - const toolConfig = { tools: [] }; - mergeDiscoveredTools(toolConfig, { tools }, { provider: 'anthropic', target: 'bedrock' }); - - const bedrock = new BedrockRuntimeClient({ region: 'us-east-1' }); - const messages = [ - { role: 'user', content: [{ text: 'Review this contract.' }] }, - ]; - - while (true) { - const res = await bedrock.send(new ConverseCommand({ - modelId: 'us.anthropic.claude-sonnet-4-6', - messages, - system: [{ text: 'You edit .docx files using SuperDoc tools.' }], - toolConfig, - })); - - const output = res.output?.message; - if (!output) break; - messages.push(output); - - const toolUses = output.content?.filter((b) => b.toolUse) ?? []; - if (!toolUses.length) break; - - const results = []; - for (const block of toolUses) { - const { name, input, toolUseId } = block.toolUse; - const result = await dispatchSuperDocTool(client, name, input ?? {}); - results.push(formatToolResult(result, { target: 'bedrock', toolUseId })); - } - messages.push({ role: 'user', content: results }); - } - - await client.doc.save({ inPlace: true }); - await client.dispose(); - ``` - - - ```bash - pip install superdoc-sdk boto3 - ``` - - ```python - import boto3 - from superdoc import ( - SuperDocClient, choose_tools, dispatch_superdoc_tool, - format_tool_result, merge_discovered_tools, - ) - - client = SuperDocClient() - client.connect() - client.doc.open(doc="./contract.docx") - - # Anthropic format → Bedrock toolSpec shape - sd_tools = choose_tools(provider="anthropic") - tool_config = {"tools": []} - merge_discovered_tools(tool_config, sd_tools, provider="anthropic", target="bedrock") - - bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") - messages = [{"role": "user", "content": [{"text": "Review this contract."}]}] - - while True: - response = bedrock.converse( - modelId="us.anthropic.claude-sonnet-4-6", - messages=messages, - system=[{"text": "You edit .docx files using SuperDoc tools."}], - toolConfig=tool_config, - ) - - output = response["output"]["message"] - messages.append(output) - - tool_uses = [b for b in output.get("content", []) if "toolUse" in b] - if not tool_uses: - break - - tool_results = [] - for block in tool_uses: - tu = block["toolUse"] - result = dispatch_superdoc_tool(client, tu["name"], tu.get("input", {})) - tool_results.append( - format_tool_result(result, target="bedrock", tool_use_id=tu["toolUseId"]) - ) - messages.append({"role": "user", "content": tool_results}) - - client.doc.save(in_place=True) - client.dispose() - ``` - - - - **Auth**: AWS credentials via `aws configure`, env vars, or IAM role. No API key needed. - - - - - - ```bash - npm install @superdoc-dev/sdk @google-cloud/vertexai - ``` - - ```typescript - import { VertexAI } from '@google-cloud/vertexai'; - import { - createSuperDocClient, chooseTools, dispatchSuperDocTool, - sanitizeToolSchemas, - } from '@superdoc-dev/sdk'; - - const client = createSuperDocClient(); - await client.connect(); - await client.doc.open({ doc: './contract.docx' }); - - // Generic format → Vertex function declarations (sanitized for Vertex compatibility) - const { tools } = await chooseTools({ provider: 'generic' }); - const sanitized = sanitizeToolSchemas(tools, 'vertex'); - const vertexTools = [{ - functionDeclarations: sanitized.map((t) => ({ - name: t.name, - description: t.description, - parameters: t.parameters, - })), - }]; - - const vertexAI = new VertexAI({ project: 'your-project', location: 'us-central1' }); - const model = vertexAI.getGenerativeModel({ - model: 'gemini-2.5-pro', - tools: vertexTools, - systemInstruction: { role: 'system', parts: [{ text: 'You edit .docx files using SuperDoc tools.' }] }, - }); - - const chat = model.startChat(); - let response = await chat.sendMessage([{ text: 'Review this contract.' }]); - - while (true) { - const parts = response.response.candidates?.[0]?.content.parts ?? []; - const calls = parts.filter((p) => p.functionCall); - if (!calls.length) break; - - const results = []; - for (const part of calls) { - const { name, args } = part.functionCall; - const result = await dispatchSuperDocTool(client, name, args ?? {}); - results.push({ functionResponse: { name, response: result } }); - } - response = await chat.sendMessage(results); - } - - await client.doc.save({ inPlace: true }); - await client.dispose(); - ``` - - - ```bash - pip install superdoc-sdk google-cloud-aiplatform - ``` - - ```python - import vertexai - from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration, Part - from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool, sanitize_tool_schemas - - client = SuperDocClient() - client.connect() - client.doc.open(doc="./contract.docx") - - # Generic format → Vertex function declarations (sanitized for Vertex compatibility) - result = choose_tools(provider="generic") - sanitized = sanitize_tool_schemas(result["tools"], "vertex") - vertex_tools = [Tool(function_declarations=[ - FunctionDeclaration(name=t["name"], description=t["description"], parameters=t["parameters"]) - for t in sanitized - ])] - - vertexai.init(project="your-project", location="us-central1") - model = GenerativeModel( - "gemini-2.5-pro", - tools=vertex_tools, - system_instruction="You edit .docx files using SuperDoc tools.", - ) - chat = model.start_chat() - response = chat.send_message("Review this contract.") - - while True: - calls = [p for p in response.candidates[0].content.parts if p.function_call.name] - if not calls: - break - - responses = [] - for part in calls: - name = part.function_call.name - args = dict(part.function_call.args) if part.function_call.args else {} - result = dispatch_superdoc_tool(client, name, args) - responses.append(Part.from_function_response(name=name, response=result)) - response = chat.send_message(responses) - - client.doc.save(in_place=True) - client.dispose() - ``` - - - - **Auth**: `gcloud auth application-default login` or a service account key. - - - -## Agent frameworks - -Use SuperDoc tools with agent frameworks. The framework manages the agentic loop — you configure tools and let it run. - - - + ```bash - npm install @superdoc-dev/sdk ai @ai-sdk/openai zod + npm install @superdoc-dev/sdk @aws-sdk/client-bedrock-runtime ``` ```typescript - import { generateText, tool } from 'ai'; - import { openai } from '@ai-sdk/openai'; - import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; - import { z } from 'zod'; + import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; + import { + createSuperDocClient, chooseTools, dispatchSuperDocTool, + formatToolResult, mergeDiscoveredTools, + } from '@superdoc-dev/sdk'; const client = createSuperDocClient(); await client.connect(); await client.doc.open({ doc: './contract.docx' }); - // All tools — no discover_tools since the framework manages a fixed tool set - const { tools: sdTools } = await chooseTools({ provider: 'vercel', mode: 'all' }); - - // Wrap as Vercel AI tool() objects - const tools = {}; - for (const t of sdTools) { - tools[t.function.name] = tool({ - description: t.function.description, - parameters: z.object({}).passthrough(), - execute: async (args) => - dispatchSuperDocTool(client, t.function.name, args), - }); + // Anthropic format → Bedrock toolSpec shape + const { tools } = await chooseTools({ provider: 'anthropic' }); + const toolConfig = { tools: [] }; + mergeDiscoveredTools(toolConfig, { tools }, { provider: 'anthropic', target: 'bedrock' }); + + const bedrock = new BedrockRuntimeClient({ region: 'us-east-1' }); + const messages = [ + { role: 'user', content: [{ text: 'Review this contract.' }] }, + ]; + + while (true) { + const res = await bedrock.send(new ConverseCommand({ + modelId: 'us.anthropic.claude-sonnet-4-6', + messages, + system: [{ text: 'You edit .docx files using SuperDoc tools.' }], + toolConfig, + })); + + const output = res.output?.message; + if (!output) break; + messages.push(output); + + const toolUses = output.content?.filter((b) => b.toolUse) ?? []; + if (!toolUses.length) break; + + const results = []; + for (const block of toolUses) { + const { name, input, toolUseId } = block.toolUse; + const result = await dispatchSuperDocTool(client, name, input ?? {}); + results.push(formatToolResult(result, { target: 'bedrock', toolUseId })); + } + messages.push({ role: 'user', content: results }); } - // generateText handles the agentic loop automatically - const result = await generateText({ - model: openai('gpt-4o'), - system: 'You edit .docx files using SuperDoc tools.', - prompt: 'Review this contract.', - tools, - maxSteps: 20, - }); - - console.log(result.text); await client.doc.save({ inPlace: true }); await client.dispose(); ``` - - **Auth**: `OPENAI_API_KEY` env var. Swap `openai(...)` for `anthropic(...)`, `google(...)`, etc. + + ```bash + pip install superdoc-sdk boto3 + ``` - - - - ```bash - npm install @superdoc-dev/sdk @langchain/openai @langchain/core @langchain/langgraph zod - ``` - - ```typescript - import { ChatOpenAI } from '@langchain/openai'; - import { DynamicStructuredTool } from '@langchain/core/tools'; - import { createReactAgent } from '@langchain/langgraph/prebuilt'; - import { HumanMessage } from '@langchain/core/messages'; - import { z } from 'zod'; - import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; - - const client = createSuperDocClient(); - await client.connect(); - await client.doc.open({ doc: './contract.docx' }); - - // All tools — no discover_tools since the framework manages a fixed tool set - const { tools: sdTools } = await chooseTools({ provider: 'generic', mode: 'all' }); - - // Wrap as LangChain DynamicStructuredTool objects - const tools = sdTools.map( - (t) => new DynamicStructuredTool({ - name: t.name, - description: t.description, - schema: z.object({}).passthrough(), - func: async (args) => { - const result = await dispatchSuperDocTool(client, t.name, args); - return JSON.stringify(result); - }, - }), - ); - - const agent = createReactAgent({ - llm: new ChatOpenAI({ model: 'gpt-4o' }), - tools, - prompt: 'You edit .docx files using SuperDoc tools.', - }); - - const result = await agent.invoke({ - messages: [new HumanMessage('Review this contract.')], - }); - - console.log(result.messages.at(-1).content); - await client.doc.save({ inPlace: true }); - await client.dispose(); - ``` - - - ```bash - pip install superdoc-sdk langchain-openai langgraph - ``` - - ```python - import json - from langchain_openai import ChatOpenAI - from langchain_core.tools import StructuredTool - from langgraph.prebuilt import create_react_agent - from langchain_core.messages import HumanMessage - from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool - - client = SuperDocClient() - client.connect() - client.doc.open(doc="./contract.docx") - - # All tools — no discover_tools since the framework manages a fixed tool set - result = choose_tools(provider="generic", mode="all") - - # Wrap as LangChain StructuredTool objects - def make_tool(t): - def invoke(**kwargs) -> str: - return json.dumps(dispatch_superdoc_tool(client, t["name"], kwargs)) - return StructuredTool.from_function( - func=invoke, name=t["name"], description=t["description"], - infer_schema=False, - ) - - tools = [make_tool(t) for t in result["tools"]] - - agent = create_react_agent( - model=ChatOpenAI(model="gpt-4o"), - tools=tools, - prompt="You edit .docx files using SuperDoc tools.", + ```python + import boto3 + from superdoc import ( + SuperDocClient, choose_tools, dispatch_superdoc_tool, + format_tool_result, merge_discovered_tools, + ) + + client = SuperDocClient() + client.connect() + client.doc.open(doc="./contract.docx") + + # Anthropic format → Bedrock toolSpec shape + sd_tools = choose_tools(provider="anthropic") + tool_config = {"tools": []} + merge_discovered_tools(tool_config, sd_tools, provider="anthropic", target="bedrock") + + bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") + messages = [{"role": "user", "content": [{"text": "Review this contract."}]}] + + while True: + response = bedrock.converse( + modelId="us.anthropic.claude-sonnet-4-6", + messages=messages, + system=[{"text": "You edit .docx files using SuperDoc tools."}], + toolConfig=tool_config, ) - result = agent.invoke( - {"messages": [HumanMessage(content="Review this contract.")]} - ) - print(result["messages"][-1].content) + output = response["output"]["message"] + messages.append(output) + + tool_uses = [b for b in output.get("content", []) if "toolUse" in b] + if not tool_uses: + break - client.doc.save(in_place=True) - client.dispose() - ``` - - + tool_results = [] + for block in tool_uses: + tu = block["toolUse"] + result = dispatch_superdoc_tool(client, tu["name"], tu.get("input", {})) + tool_results.append( + format_tool_result(result, target="bedrock", tool_use_id=tu["toolUseId"]) + ) + messages.append({"role": "user", "content": tool_results}) - **Auth**: `OPENAI_API_KEY` env var. Swap `ChatOpenAI` for `ChatAnthropic`, `ChatGoogleGenerativeAI`, etc. + client.doc.save(in_place=True) + client.dispose() + ``` -## Tool format reference - -The SDK ships pre-formatted tools for each integration. The conversion is minimal: - -| Integration | Type | SDK format | SDK helpers | Native shape | -|-------------|------|-----------|-------------|--------------| -| AWS Bedrock | Cloud platform | `anthropic` | `mergeDiscoveredTools`, `formatToolResult`, `formatToolError` | `{ toolSpec: { name, description, inputSchema: { json } } }` | -| Google Vertex AI | Cloud platform | `generic` | `sanitizeToolSchemas`, `mergeDiscoveredTools` | `{ functionDeclarations: [...] }` | -| Vercel AI SDK | Framework | `vercel` | — | Wrap in Vercel `tool()` with `z.object({}).passthrough()` | -| LangChain | Framework | `generic` | — | Wrap in `DynamicStructuredTool` | -| OpenAI | Direct API | `openai` | `formatToolResult` | Pass directly | -| Anthropic | Direct API | `anthropic` | `formatToolResult` | Pass directly | +**Auth**: AWS credentials via `aws configure`, env vars, or IAM role. No API key needed. ## The discover_tools pattern @@ -428,10 +146,6 @@ if (name === 'discover_tools') { } ``` - -Framework-managed examples (Vercel AI, LangChain) use `mode: 'all'` instead, since they can't inject new tools mid-conversation. - - See the [LLM Tools guide](/document-engine/ai-agents/llm-tools#the-discover_tools-pattern) for details. ## Tracked changes @@ -489,7 +203,7 @@ editor.destroy(); ## Example repository -Complete, runnable examples for all cloud platforms and frameworks (Node.js and Python) are available at [`examples/ai/`](https://github.com/superdoc-dev/superdoc/tree/main/examples/ai). +Complete, runnable examples are available at [`examples/ai/`](https://github.com/superdoc-dev/superdoc/tree/main/examples/ai). ## Related diff --git a/examples/ai/README.md b/examples/ai/README.md index c1091a93f5..029c780f2e 100644 --- a/examples/ai/README.md +++ b/examples/ai/README.md @@ -11,16 +11,6 @@ You write the agentic loop and control the conversation directly. | Platform | Node.js | Python | Auth | |----------|---------|--------|------| | [AWS Bedrock](./bedrock) | `index.ts` | `index.py` | AWS credentials (`aws configure`) | -| [Google Vertex AI](./vertex) | `index.ts` | `index.py` | Google Cloud credentials (`gcloud auth application-default login`) | - -## Agent frameworks - -The framework manages the agentic loop — you configure tools and let it run. - -| Framework | Node.js | Python | Auth | -|-----------|---------|--------|------| -| [Vercel AI SDK](./vercel-ai) | `index.ts` | — | `OPENAI_API_KEY` | -| [LangChain](./langchain) | `index.ts` | `index.py` | `OPENAI_API_KEY` | ## Full demo @@ -38,6 +28,7 @@ npx tsx index.ts contract.docx reviewed.docx # Python cd bedrock +python -m venv venv && source venv/bin/activate pip install superdoc-sdk boto3 python index.py contract.docx reviewed.docx ``` diff --git a/examples/ai/langchain/README.md b/examples/ai/langchain/README.md deleted file mode 100644 index 18a36098f8..0000000000 --- a/examples/ai/langchain/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# SuperDoc + LangChain - -Agentic document editing using a LangGraph ReAct agent. - -**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) - -## Prerequisites - -- `OPENAI_API_KEY` environment variable (or swap the model) - -## Run - -### Node.js - -```bash -npm install -OPENAI_API_KEY=sk-... npx tsx index.ts contract.docx reviewed.docx -``` - -### Python - -```bash -python -m venv venv && source venv/bin/activate -pip install superdoc-sdk langchain-openai langgraph -OPENAI_API_KEY=sk-... python index.py contract.docx reviewed.docx -``` - -## Configuration - -The example uses OpenAI by default. Swap the model class to use any LangChain-compatible provider: - -```python -# OpenAI (default) -from langchain_openai import ChatOpenAI -model = ChatOpenAI(model="gpt-4o") - -# Anthropic -from langchain_anthropic import ChatAnthropic -model = ChatAnthropic(model="claude-sonnet-4-6-20250725") - -# Google -from langchain_google_genai import ChatGoogleGenerativeAI -model = ChatGoogleGenerativeAI(model="gemini-2.5-pro") -``` - -## How it works - -1. Connects to SuperDoc via the SDK -2. Loads tool definitions in generic format and wraps them as LangChain `StructuredTool` / `DynamicStructuredTool` objects -3. Creates a ReAct agent with `create_react_agent` -4. The agent calls SuperDoc tools to read, query, and edit the document -5. Saves the reviewed document diff --git a/examples/ai/langchain/contract.docx b/examples/ai/langchain/contract.docx deleted file mode 100644 index 5b911d339926dd9eebe45bddf9e99267496193fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79086 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 str: - print(f" Tool: {tool_def['name']}") - result = dispatch_superdoc_tool(client, tool_def["name"], kwargs) - return json.dumps(result) - - return StructuredTool.from_function( - func=invoke, - name=tool_def["name"], - description=tool_def["description"], - infer_schema=False, - ) - - -def main(): - args = sys.argv[1:] - input_path = str(Path(args[0] if args else "contract.docx").resolve()) - output_path = str(Path(args[1] if len(args) > 1 else "reviewed.docx").resolve()) - - # 1. Connect to SuperDoc — copy to output path so the original is preserved - shutil.copy2(input_path, output_path) - client = SuperDocClient() - client.connect() - client.doc.open({"doc": output_path}) - - # 2. Get tools in generic format and wrap as LangChain tools - # Use mode="all" — no discover_tools since the framework manages a fixed tool set - result = choose_tools({"provider": "generic", "mode": "all"}) - tools = [make_superdoc_tool(client, t) for t in result["tools"]] - - # 3. Create a ReAct agent - model = ChatOpenAI(model="gpt-4o") - agent = create_react_agent( - model=model, - tools=tools, - prompt="You edit .docx files using SuperDoc tools. Use tracked changes for all edits.", - ) - - # 4. Run the agent - result = agent.invoke( - {"messages": [HumanMessage(content="Review this contract. Fix vague language and one-sided terms.")]} - ) - - last_message = result["messages"][-1] - print(last_message.content) - - # 5. Save (in-place to the copy) - client.doc.save() - client.dispose() - print(f"\nSaved to {output_path}") - - -if __name__ == "__main__": - main() diff --git a/examples/ai/langchain/index.ts b/examples/ai/langchain/index.ts deleted file mode 100644 index 58363bbb8f..0000000000 --- a/examples/ai/langchain/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * SuperDoc + LangChain - * - * Minimal agentic loop: any LangChain-compatible model uses SuperDoc tools - * to review and edit a Word document. - * - * Usage: OPENAI_API_KEY=sk-... npx tsx index.ts [input.docx] [output.docx] - * - * Requires: OPENAI_API_KEY (or swap ChatOpenAI for ChatAnthropic, ChatGoogleGenerativeAI, etc.) - */ - -import path from 'node:path'; -import { copyFileSync } from 'node:fs'; -import { ChatOpenAI } from '@langchain/openai'; -import { DynamicStructuredTool } from '@langchain/core/tools'; -import { createReactAgent } from '@langchain/langgraph/prebuilt'; -import { HumanMessage } from '@langchain/core/messages'; -import { z } from 'zod'; -import { - createSuperDocClient, - chooseTools, - dispatchSuperDocTool, -} from '@superdoc-dev/sdk'; - -async function main() { - const [rawInput = 'contract.docx', rawOutput = 'reviewed.docx'] = process.argv.slice(2); - const inputPath = path.resolve(rawInput); - const outputPath = path.resolve(rawOutput); - - // 1. Connect to SuperDoc — copy to output path so the original is preserved - copyFileSync(inputPath, outputPath); - const client = createSuperDocClient(); - await client.connect(); - await client.doc.open({ doc: outputPath }); - - // 2. Get tools in generic format and wrap as LangChain tools (all tools — no discover_tools since the framework manages a fixed tool set) - const { tools: sdTools } = await chooseTools({ provider: 'generic', mode: 'all' }); - - const langchainTools = ( - sdTools as Array<{ name: string; description: string; parameters: Record }> - ).map( - (t) => - new DynamicStructuredTool({ - name: t.name, - description: t.description, - schema: z.object({}).passthrough(), // Accept any params — SuperDoc SDK validates - func: async (args) => { - console.log(` Tool: ${t.name}`); - const result = await dispatchSuperDocTool(client, t.name, args as Record); - return JSON.stringify(result); - }, - }), - ); - - // 3. Create a ReAct agent - const model = new ChatOpenAI({ model: 'gpt-4o' }); - const agent = createReactAgent({ - llm: model, - tools: langchainTools, - prompt: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.', - }); - - // 4. Run the agent - const result = await agent.invoke({ - messages: [new HumanMessage('Review this contract. Fix vague language and one-sided terms.')], - }); - - const lastMessage = result.messages[result.messages.length - 1]; - console.log(lastMessage.content); - - // 5. Save (in-place to the copy) - await client.doc.save(); - await client.dispose(); - console.log(`\nSaved to ${outputPath}`); -} - -main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/langchain/package.json b/examples/ai/langchain/package.json deleted file mode 100644 index af11d98746..0000000000 --- a/examples/ai/langchain/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "superdoc-langchain", - "private": true, - "type": "module", - "scripts": { - "start": "tsx index.ts" - }, - "dependencies": { - "@langchain/openai": "^0.4.0", - "@langchain/core": "^0.3.0", - "@langchain/langgraph": "^0.2.0", - "@superdoc-dev/sdk": "latest", - "zod": "^3.23.0" - }, - "devDependencies": { - "tsx": "^4.21.0" - } -} diff --git a/examples/ai/vercel-ai/README.md b/examples/ai/vercel-ai/README.md deleted file mode 100644 index c7ffcded76..0000000000 --- a/examples/ai/vercel-ai/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# SuperDoc + Vercel AI SDK - -Agentic document editing using the Vercel AI SDK. The cleanest integration — `generateText` handles the agentic loop automatically. - -**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) - -## Prerequisites - -- `OPENAI_API_KEY` environment variable (or swap the provider) - -## Run - -```bash -npm install -OPENAI_API_KEY=sk-... npx tsx index.ts contract.docx reviewed.docx -``` - -## Configuration - -The example uses OpenAI by default. Swap the provider import to use any model Vercel AI supports: - -```typescript -// OpenAI (default) -import { openai } from '@ai-sdk/openai'; -model: openai('gpt-4o') - -// Anthropic -import { anthropic } from '@ai-sdk/anthropic'; -model: anthropic('claude-sonnet-4-6-20250725') - -// Google -import { google } from '@ai-sdk/google'; -model: google('gemini-2.5-pro') -``` - -## How it works - -1. Connects to SuperDoc via the SDK -2. Loads tool definitions in Vercel format and wraps them as `tool()` objects -3. Calls `generateText` with `maxSteps: 20` — the SDK handles the tool call loop -4. Saves the reviewed document diff --git a/examples/ai/vercel-ai/contract.docx b/examples/ai/vercel-ai/contract.docx deleted file mode 100644 index 5b911d339926dd9eebe45bddf9e99267496193fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79086 zcmeHwTZ|*ic^-9+WJgC_sz;hddwW@g9B*|RymS<0D>JMc?b~1Fbp^_WY`jpVH-B`ARsUhKu>wen*~TvEWiQs zCd5d-zpDCL-97AP&uXRYF1Sl}SAA9W-|DZw{;s}w>(vjwR-!-O`Q7QS)xTsu{k;#B zO7!_DT>I6YId`-b;mle@rYU`k^E*6G9 zQXTZPax))CTGjawWC}x|7_1sP_n@vGlQ~T&R|FuFlYn%F49H zm9aTm&Ol2dVhT)5Kh4QRA8B5FLR(noNV9Fwd}eHWcV?3b!OhhKOe^bUT4C^KKV0WdFSh|luGpZiC{6&=lqtRh|OuVzWL`y??@O; zJ`2PQyvz~zZk%=IlsHhA8gDbwVQ#t9W(!OR+JkeNjmW$@v;B&9b^p?IRO3>gX%DKn z?ETokz-(ndsO?wym;I-((>9giU-o-b)lx?eW=^^o__g2l=cxy0`p7bEbK)qEnAumT z_I@Q;WFJ%+Xrra2JGTTC{JDRs&(RB9Uqt`{EBaaD9IOf@ov(-bM2- z>_5Z=ZW+`h>dvpUv0FyILF9Xz`y&r(?d>Z5FnYPyzz|RVBAW5;D6v8z0_=vE%Y_=*J>qosJ4EG?j}*V1ds)FNG-#Q9gwFIq>r5VQ_qRG+Ghq?h{$$XUZM_*I$2OXN* zdsZRB7=m1_14#sgU+S$8x%Wh?!!TB@DWT0=_hTDlt8gCk^=rLatFmucz6J-mspdA5JLCr{)+9MrSArfRr9b4N2)5lS;Y)EdmqA25KCY*Z1?GdrF)*dgT zfcDrKPqSgV9IQ}XI2BXXo=UK?4Naw7%k8IqI|Cz>*=X~p15R(o?6LLqnCR?fAR^4OfIbGMi%3Rl&%wHiv_ zsK!CLifOpot~RQzY7?J2zBOSIIkKoSg70Z-j#s4`Eih4Mx^e5?w^*wYjD_xu&~M)8 zjcVyM;wX--U!lm-9##*^eYmFe{_$~{U#T6Gd$`isX&s6yb#$Kp*%enB-W9~zR(m6c z!-~{64z2)4)bdBMU*Qx?pl8p->Or%JqIc4=1N!YUni4IW8`Ba_3w*jLokGN%=+Crq zw8T1;Y{Cm*)P8Q1?1JsaoisdzJT@5(Nj5>n=-@KSqrK8?JBkq^;o9=pmF4k%spyJk z)D3v`{ZD`N)t@bu=tGM!$&tu8#lx1sV`H&bF)yzc@+6;X;<(xLr_`Q0zflow=F|LW z&3S8VsVhVsF&o#K6=JwU=F-K07Oyj31wdMr-O7#(1kuk7gsHt=lc8XJGD5X0)GKLl zYdRW%!4VL8B_XZpY($Y_2G`CtxLRHAQzsu%Lq^)kha`#9re3U&N_dRdIeLUzu3W3L z#+r^;fMcBs%@Iap?4p6At;7r>o?E?{Ozp{O8rs;0UZEyHLhmTJ9OPNJU zRC55brHdHiLYIwq3`HYdLM>-$Y3{J{KrmC}0*tTL;%lTAW1V8LGZ)0j(+$HMU2Eg; z4794jyS!6Fh%Ii@o30~WdCMnm0fEiEdh~j*;leS1v(cK-$B_iwvsZD8YHzvd18lxh z^s$OsB!&2J~d%N9}WU274i0C6#qp4HTt%UNfTV8*| z`SdS*c4<1l{Nd8^_qgLD-W(z-PBve8q0!LUWd?!T2#!=X28m za}{j*n3zr#w-?xYvdww1^sa9!7u|PAsFln9sPUr@oEUjnd>jW&%09te!9=o2vs-VwHG8EdJ7s7g$A5Vr%i zGc}hE#KJ+1Vrkm|t3lI~^X}!zqw_T>E4oaZaQd9c;E=R~)!G!oL`fR4b(Q`E8c3XL}fUvp^foiV{s)0l^SOW+wOo9np1M-i|1yLQm z4Jim25KK`wlvK9Do>H~y%FlpSAi>Ziy?CAE-B{A>qzt)u!eVfX0$PoA3k;<~{x`&~p17FVVj~`w1 zAFsjlE*QwSuDJkXLeyu(c%HJr$cY%SegpA_LctTdwiUMf4}xK0_GSmnH(*{!Teu7i zHADuuHS*>q5-mzYx+OBZwkts5DSQH&^^sP2HFuc>#X;{_t^%LR;WMBdG*_Q1_LAcN ztasjg7_bcLcnpa!VND?}GniGIscW%JXm2aMN9V^UN0?qtx@)PNM?Ne@g-~5<+Jah*&-A+N02PJp3U&&ctcs5qazctFY6l+DqqqBy&riU z%H}q#!U=&PGqfuv*yK+zE8`$%XRh6ne`2F^SE@B;MoI7(9stux{G`^zc;fkH!}4BW zirqt&`S>wyZ`B)(qYm39W0x)vqpMaMwQ951XA66w`f8@`<{bQvPeY_nL>}qv#M;tZA8ycv)E*#CfP|MvaBL~@@)$|G z*kJ*okbseIz^Sb;!L+J(?T>2B65GN=~&NOYe@<<`0- zTr#dK;ac!oh8iu)7@~V?upGK~x387uzsSev}*;gMC>EV0YNz*cxPg{!jLCik}4 zO{}#~^_hDE4+#utcZS`XW9kXqTh7OiZY)kRpHgu4>A5l@)hVKrt^jNEttSIk7FOJ_ z2K01-c7|kb3MODr;|i%bShh|bY+3*Uzs5-SWj}^jb#V`|b=<=*y2pN~BN`q`i!W4w ze?=+r#lMEs8h#C8@0LTB48I0>cdsGeg2-dy)}*z`31xIP`He=m z+dAr2JK}z8k8PVXQwH)r)+UFljWsKnh^$bra*J2~e7jw3*J{0;t>#XDcdOrNHn(18GvjwEmwq0cvZ@#1Haz07)w z#>QgJ4keP1ZGIVGNh41^_R$!3ntz$06nz-%1dENuqCGwWt8h`i2_x&4KaS1=2b}+}nukf`y=Rvq!0pFKgufR{xEbxmQV^Yb?j&WJ zf2y8iSa-U#@#B*GOxnkT#^rFJ*tqy14zcT)Pc>qP#S*mye`6LhlCxPOrNcT2<6*_) z$0a+4>}-ZS<4PC7X6&HS7iV{RhVf&Pe7XCS!B&oO-RaWBk4y5i?okHYvBSDU1|?@} zJsFf8qkEFUPVAu4W$;eVD4ti6FLR$VxRYmGceyn2G;=k1n8)qz44Mg!o@BkRMqV4w(D$K4pgW8U%r9s8C~E=pfroH^M8V% z&7D@O)@jywkS7)tUJ1A43=}=b|NIE!$jIJl8?Tm1^!d1Zf=J*j+zs}AagZId zi~TcuMPRs~yjV+0EsROKi_1%m&2oMDY&Ra9eVIn;#5`?&-@6?7A1@XepeD}noFRwt z*hD1WT-ln%g5AhVGFKPpZ*V&h(8GuY_}-Vl{*^(?@g0=$w3BH)q-Y5(QN`AaSC}?kB8LeopFYGX21;-6i(-9w zjc2o5hXy_iGe9;yUBoiq>R4cjOFh~V3tu$kJVU>S2W63Zh$KUOj9oL;+8T(s3G(OI zqjHo&?Pt>=_JJ2^SYyge$7Ua%Uw3^WcSjBX2WW-0uJ|QfrU+%}89pS5v$rpx?_+Ip z%8rv6P-eZ@o5aN@Cl`30kIolADDScm1}+Y?nf_28Bkej+a6Ux}`cvd4KS5TXfBlzX zzmcyhE{x12?FlwIZRR-|_PGCSr18_5;mxrV9?#if>%_99?l;cDvY&OqxFd*2NE`q? zorO&!1%<*GtpC|CSiHjyj5i*185un*m8~Tffp{)0vADo^sM?bu90#(vhpyq8mrtKC_yc=10`r(cAx~M$_|vEGueR>R3bZ2 zg2ZPBN)YJmKne1i1;{Z+N#YoobsDUt80(%apah&Co|^+Fi0kIS3F5msaDq5*4xAv~ zn*%3^`{uw2;=eg?f;wyl%-t|Ls|tQ1c~YC%ZF)u?=ds`<4tTGd!?9zV_;C`&0P;ur zH}4of3t)2`KF&ac8*nejx)GMRY7ZE5UTQM}n^V}%Jwc!8*N(u6oAZSyWJaE?-8 zV0e)kvkwIh)`cv_LU@v`E^&$(mc%%6YI7RSRJ`SDa~Q`|mBNh0DW3D&yi0_l9EwEc zO;whGowMDZf_j$Vqh(Qh)~N)SEHh4m2bLKp!Qsk`!#S7a%I1YDidv#~MG;NF6-7HP zsD;r^z!gP10aq051Y7~MyQCMvk$MLMf3*yezn)>zIdGd>hyf0MV~1F54&_E+y%#Pj z6uyqf4LD*v&X`5bSnT?;5bxT9D$M<7rjlaU`g~b{a}E@&IRpD5-;LsGc z9*)N?LX>e-5oc;4meB8K0S=Q7R)B{2V4NpK&`!cEqMvlay;6)}_D=BFT(FO$hk|Sl zIl!4&eD)3Mf1L@=deMXaY=P50bvtT9_`@sfVH~-yc)~$0n~&)N$K}oo;$D9XuP-r_ z0-cM8muIKSB~CQMS(>yry7_n+B0p-XI8YHw{Pa_8h^_Ta*Mh8zT`ZgDw?z%OTU2O? z^{t1q=nEQVPaGV#SqP2w3XZ5w2U8a@w=%%DwA-PHl!StYdV$Cf%N4+3*abljRJ_2w zC|1zZ-E`_YJ%&+;2*2&VC~{*sp980kTLCc7 zax>Kk;T7QGgzyUR9vDwCWso2zZCGaU(nEL!j5mZ=*m!$*X}hf_?$Bl*unSn&h+V+K zhg`r?k@KWy8m2W_8pTPChgcjH-98paM$=f%aARYA!%(!FTb;H zH(w`kjqB4ZMUpm2vYUeo%bXzCjg>ED&2PGP<6Wd^ccC6R$19B02(=XF0})uEJu5)^ z4(UlSyf~Y)S4{pR(^n~>^3sd2rka;umJ*Q?6ZHZ;~$ z%~sd-)ns zv(#Yj_Ii6!zl;|Tv-opR`s*xTdp&>?I9K#G<5-|ZhSV6r6E?IIhOBRZk{PnTB}&Fv z-y|httY@1N>t{1k$uOxZ>zN$Jm?XQA!E7b>BUNQ%?Uy`LWVBy0Kq@h0`y~V9vR^Vx zDlwAmmkg8Le#scA_A6FxWsK}nvbRu3bnwlW5;B&&X`+A_B? z=qF}XTNxmg7_!>R0I8IS=$oguGE6ElA~2rX$}qT+Oj7~FIGfUy>}o6bBfHwl7}?cU z#>lR=GDbGFmFnkHTNx%*QazOms;!KXDy$wxP;F(5R7s6hTL{!}o4xWLH}mBb(Yv_4BE%43jFU zp2`K)R>nvbRu3bnwlYSlq{gZ(gkdwal>t(TA*-znkV=V&zIkdZ!=w@;0^_NR43k}L zWejjCquRl3&QvG~tE5oEps;6>6wUseah1J6ds;!KX zDyd?JHR+K6evojsC~6W`9RykTfCJI3$H6E4gI;?h1C|@zO$Gy%*X`J$Y2q=HxJM`( zp!3=&`q0o#yhMF5y`@@_pn2G4HlSCyl4(c&QI9=wmiTZ|;2Fro(|6V#jCgp19ZA91 z5@b{}RSSYu852TPQ-wcRl~F!aHC5(=RVC-NnQD=PRiz2id(Cz1nOaOq`A#;QrWjKL ze4uxtB%9zs?HYFb@U~>BJ~V%=`ZA$Y4R@erna-)^I#5$4bE=UJ)NCoV!5JNFYh7Zs z71v5i5UG+G=s_nLB2_{I5Q1fH$W7LgwRx8eu{INvA=c(*GDJ4ECgSAaZ!%VPMkizC z>)WahXGk(uK%L_Us7I2&+W#>&re84Fj|GOH0Cmu+Qcj>{04IW9wF=C}-z zkK+=~1{{~M@^f6q%E@tgj?2t(86q>sWr)limm%_TT*BFa<1$u$j>}j%IWBvEnK>>) zWahXGk(uK%L_Us7I2&+W#>&re87n8p!Qb%WteKhPGDK#M%Mh74E<@zwxP-F-$7QVi z9G9_DInJUmd~ud*c@?OSb|NB@%HKduX(no>vNcdsvS`gzZU$=RSzZOI=UrX}Ao4D+ z0uT|`3sMu|0Hh(=hYJ9Cfm#`2Z6+qJ9oFY&GDJ4ECgP+kX@DR;f0MCNl{A3mGddY7 zqmsHnvd(4ZxD1h*<1)nB`kOqEd>og$vH{0sto$68v2bNA)r{!4Y%4Q!T!zTZaTy{r z$7P6o9G7r5;JA#HpW`xCPL9iSTxO2T5SckHLuBT-43UrH63zx3m$CA5T*k`Dami~> zceVpljW>_W5SckHLuBT-43UrH63zx3m$CA5T*k`DacK0Wo|_Djnd34|%zvNBgSjy3>bXEVgWBirwc$*?L%faDV2jzuosch?+lnpN$Q~INa=@EMAzt z+B5lCTcCp{W%L?!k1X-GB*)2;Ynh6YrTSX3|pIwsO&UzI}p12&%aHWZOoqKIIzRj7!xe#k_GtI$1&$oE?$_}hHr zW*n@>3Kfa=4~R&l7z(Z>_aFtShN87gf60^u{bL7f1m8Qp)am8SUCy%&twbl$O1MFf zqMVS<557ov6i--I72|e1ZxPr@6am+NDM|!(UPuv;eFQH0&s+8D9s0YD>K}2&Y^eIX ziF|jX8Ab7Z_XmXU9$D{Izwd$1BgCfEkvQ@`As{DSaOIts#9nU|4bO=}idRa!1&cWc za@dQn`-4xPa5q8@RX|n~Vloar_CZpg&~KJglNwD_SP|jD$erAE#d)28iZe7WETRg& z;C(gd*6N_lmKHqpIew;!hESlV8OE8)i{s@U$z=^~LV-p=)v6FOH;(Fq4Nb=}XDJ94 zJMk|W0`v`bi2tOKBoltlUehQZ@JS&4xk-CslA-Wk6ObSm8AzVozmQOmmNp19py+Zm z(i{rVMg>~jRBB4V21V>ztY&aQjtc}Q3!1o-68C)>h~;XOk+(cxn#9|BVZ3#NeKAHv z@HT2g{=eY$Whtgs^Wq(wSsD4mqjX99X%)sFH@f&@@#i^&-ZJuohscunfwWnFzj-5n#SQJf zSo}!UmwSkSWMmExRwXe9dB_DZ#|^T*Sj-72%lDrdJZzN2jGe-m;fA|jEM}x?%6-QY z?p}8B*iI62b_-*UyNG|Wm=n^JdtgQ=)5=SJ;-`m1X)ewbDP|vsfd#CbOm=$VJ8LUr z4vL@F6wPBA(JM-xIZM?LZ%80~_(2KLIMp8MdTc7vUEX(&BqqPtERAi;8~wFrdC6q~ ze;c73R;aj`fT&*xUGP;9P*LD@5N<{-M(~iwNp-_dJM_}Pyt_W(g|f4hvQ)LcOL^DJ-O22=AU^Ho zTQbe9xeFQFOFj(fx1W5tc0$j(E@+5vNkX!@O1sS@hBIrfXk^gpA6n+c>oR}cLzGA2 zB@o3-#0)G|h3P#Nn@z-SesMV8(^b{g-g_f=I-~cc(2JGhG&t2!ZC2^;I-8p!>9@Ga z(WhW6qc+#1Aiw3w(PPiuYV^D*7%$Yc<8$F<_(y5Ro{{oz-#u5!6rne>AdHUQJqge&5Ig>q4JDZxbf{q_YKT3gRAITF zh-19G2%2y$NFrEJM>h6sf-R<+Mtbv3CfbUn#^P`s*wtn3fQ^k(@s9e>9h4!#++m>I zNM_d9N)TJcxkC2d_V0Z+MXM<>Hs9TG&42ju684J`FQ|7rZ8mha9gRm&=oc$fz!Z^( zbj8gQl1!ya^5&C9usuOT**19cyW!mN97r2Aq5fH^Nrd!EY;Huo9m;i=F~W!rMbmE5 zwmrk^Dtqdps2px-RpDioCT{L{gbY2S7~M9H5;ahm>0`vkW}1<_>JS)V@x1{pj zYW;Ax5h~9L@$zk{e6!!})cc|Gtk9J7NiNOxijUSSGBJ&ZvBh`W?P}*pfcvE&4W4vS z6BC4HlVoj1&wElfAr-`f!}?qyYc;`3a*?IJa7`9t9zOns7?JsPi*ND^+&NsmcGDR;7y>G14X{|r+@m*FMjo#Uwr?Yzx7++{Nh){ zVaE(ARPUiW$79=mU;p;^zx3Vz@SDp2{FDFqx4-jmdDGGIcCh@n|L|Xb`_KL=4c-YB z|JIlP=-XfYqi_AYfAgLH{5vt|T}vGXi(l$9JPCZRt(3>+3RSh+!+@Thx5U(zeqrzL%R$xq*%lTD&9SJ`i`y~s(LI*F7;tZn*2%+bvP~RZCnSz z?+|Uy-c}AxBW~13+D*75bOUc;mxb<9GeTs1LtQ%REDrKgo#8d^K6Or2XByYZ!L2n4 zmh0ONIDMrVrqUndeeiL09$6S~0Q)T!)-9IpXU6R;T+(v%>p09))ii@8kIZWDL7gYE`)yqdvHNb_eu50)V3&LWp*u?}hL>+> zW=MVqx5h-(;*_jw%|a%&Yw5Alak#t+iT0_c8R`lT^J~hJlb9YHnu`$4{Z79*g;jZ| z#R=A%YFJk3H%?7}uY}#hm_`t?3W@AMyD}5{b9NgsJh$o`2ZkgepM&c`n)inmlrg3Y zkBkvqUtbr_b@+ggmsp}a!YU+KI1%+?O6QLFsPr4l`k3~iV_bs zd$d*^81`VDRN7ps6ol~`@CD{l)s@pYbBih=_WM<6a%I()vGpclrS;hW?;E=(}=HA(vX(wj;0IbM1*<(J+}oCCCpH{&}n@NFpQ!Gk?4^fOD0{7iJi7|K0>BSDgyR4E(fv$O6!J=EiQvIOZYEp*~DYr*( z&~f~qTi@j_V`h<@P9kPV0uWr}BSG}s;e zIkO?GZ?!rM!VBBEU6_>@t#gckK_+XHi8gW)t`Mbp0a$oGEdh9&;UuqL^!;n2{uU7&}`~WUAzy8v+muMXfy$%>Cc%Vt3tD8 zycG+z;7$V(TIQSN1HE!d44W35;koIKn%rI2)TOkbTK%nNqgmbBX>}W0 z-Bznn>$hsvqr>_aFtIq(S*^u)?T@jEVcd$ndv;ygzn{7LG}|L{Rx#QBJkK*=`}eha z^8US|!{~gzT|eG!>>jqab{pN}t>)3;&em?PeYkbh?sblik9w`0-QzEml^a&L*=_Wi z?f%YIyVmP%HQQC{{?6`Jr&+Ieo1N}XzuEnQaAPoXZaxGpKwd`BbrL1swKT(yBuf16 z|N6>D{_NFKi9Y|tO_bmT$L45B83>dru}xKT=1c59#?Am8>1|JSq}#BZ35nt{852m- zzs43D&zC^^J_h(1#8l`MYU^Z-=ln0(snvI9V{n!pi4n#$QL!}#Dt?6x!b{b<(lB|r zSBM@gk1MVZ^+NkVE3RM<*zeT#kz!gL4Fc!JxJ2+SrS<~ZKE?Hd$+yo2_LD0k%i>97 zMXSh8&`MbxKID?ZyOc_bW|6&EX~Tgemw}63Pbxv#jAl{&r@#N?yRW@cD$(bspz~1l z3(H*C75)q;f#g+n!qKqpihPE(^*l}&c~7<$9$Z=2>l-LyKJ|7-`8G+W4HF{3Z$(bk zV2Pfi(Azv8tgK+#V&?g%)jK+BRa;y2{%#MC1J({<)pt9XajMAgsrQa|yM5U51r?@~ z#c!NFW3j^4_Ji7XXS=#z@h|LG+(xD5U0d|VQXl7Qn@*Ybn?klvyH@M%Y&CcKyIcKE zv$?fXJH({YsUk`0xK?lMBzCRhvk~*k2VVJj$xp$nf609Mdmk#5=tII2{`0Fn=n3YG zcNvj?okS@L5UlXw;E&RO2c*j-OjF@@Klw9XdFSg8So-`#sM_BLtEr1cT%+&4^0BY| z1!~ggCquRW*FOj}I>O=man*x0ekAy#^gob>=+3L*4u9fHKllDQnxhZpD*Au^uUCgn ztI?7A1xqCiE%4{&ABvR6XqE3SzA^mjkCsaG`BZ(dv7D{Eq8gTvXp*WAv>mR5> = {}; - for (const t of sdTools as Array<{ type: string; function: { name: string; description: string; parameters: Record } }>) { - const fn = t.function; - vercelTools[fn.name] = tool({ - description: fn.description, - parameters: z.object({}).passthrough(), // Accept any params — SuperDoc SDK validates - execute: async (args) => { - console.log(` Tool: ${fn.name}`); - return dispatchSuperDocTool(client, fn.name, args as Record); - }, - }); - } - - // 3. Run with generateText — handles the agentic loop automatically - const result = await generateText({ - model: openai('gpt-4o'), - system: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.', - prompt: 'Review this contract. Fix vague language and one-sided terms.', - tools: vercelTools, - maxSteps: 20, - }); - - console.log(result.text); - - // 4. Save (in-place to the copy) - await client.doc.save(); - await client.dispose(); - console.log(`\nSaved to ${outputPath}`); -} - -main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/vercel-ai/package.json b/examples/ai/vercel-ai/package.json deleted file mode 100644 index b66706966f..0000000000 --- a/examples/ai/vercel-ai/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "superdoc-vercel-ai", - "private": true, - "type": "module", - "scripts": { - "start": "tsx index.ts" - }, - "dependencies": { - "ai": "^4.0.0", - "@ai-sdk/openai": "^1.0.0", - "@superdoc-dev/sdk": "latest", - "zod": "^3.23.0" - }, - "devDependencies": { - "tsx": "^4.21.0" - } -} diff --git a/examples/ai/vertex/README.md b/examples/ai/vertex/README.md deleted file mode 100644 index 53799e73b4..0000000000 --- a/examples/ai/vertex/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# SuperDoc + Google Vertex AI - -Agentic document editing using Gemini on Vertex AI. - -**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) - -## Prerequisites - -- Google Cloud credentials (`gcloud auth application-default login` or a service account key) -- A Google Cloud project with Vertex AI API enabled - -## Run - -### Node.js - -```bash -npm install -GOOGLE_CLOUD_PROJECT=your-project npx tsx index.ts contract.docx reviewed.docx -``` - -### Python - -```bash -python -m venv venv && source venv/bin/activate -pip install superdoc-sdk google-cloud-aiplatform -GOOGLE_CLOUD_PROJECT=your-project python index.py contract.docx reviewed.docx -``` - -## Configuration - -| Variable | Default | Description | -|----------|---------|-------------| -| `GOOGLE_CLOUD_PROJECT` | `your-project-id` | Google Cloud project ID | -| `GOOGLE_CLOUD_LOCATION` | `us-central1` | Vertex AI region | -| `VERTEX_MODEL` | `gemini-2.5-pro` | Any Gemini model that supports function calling | - -## How it works - -1. Connects to SuperDoc via the SDK -2. Loads tool definitions in generic format and converts to Vertex `functionDeclarations` -3. Starts a chat with Gemini -4. Runs an agentic loop: the model calls SuperDoc tools to read, query, and edit the document -5. Saves the reviewed document diff --git a/examples/ai/vertex/contract.docx b/examples/ai/vertex/contract.docx deleted file mode 100644 index 5b911d339926dd9eebe45bddf9e99267496193fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79086 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 1 else "reviewed.docx").resolve()) - - # 1. Connect to SuperDoc — copy to output path so the original is preserved - shutil.copy2(input_path, output_path) - client = SuperDocClient() - client.connect() - client.doc.open({"doc": output_path}) - - # 2. Get tools in generic format and convert to Vertex shape - result = choose_tools({"provider": "generic"}) - vertex_tools = to_vertex_tools(result["tools"]) - - # 3. Set up Vertex AI - vertexai.init(project=PROJECT, location=LOCATION) - model = GenerativeModel( - MODEL, - tools=vertex_tools, - system_instruction="You edit .docx files using SuperDoc tools. Use tracked changes for all edits.", - ) - chat = model.start_chat() - - # 4. Agentic loop - response = chat.send_message("Review this contract. Fix vague language and one-sided terms.") - - for _ in range(20): - function_calls = [ - part for part in response.candidates[0].content.parts if part.function_call.name - ] - - if not function_calls: - # Print final response - for part in response.candidates[0].content.parts: - if part.text: - print(part.text) - break - - function_responses = [] - for part in function_calls: - name = part.function_call.name - args = dict(part.function_call.args) if part.function_call.args else {} - print(f" Tool: {name}") - - try: - if name == "discover_tools": - # discover_tools is a meta-tool — handle client-side via choose_tools - groups = args.get("groups") - discovered = choose_tools({"provider": "generic", "groups": groups}) - new_tools = discovered.get("tools", []) - sanitized = sanitize_tool_schemas(new_tools, "vertex") - for t in sanitized: - vertex_tools[0].function_declarations.append( - FunctionDeclaration( - name=t["name"], - description=t["description"], - parameters=t["parameters"], - ) - ) - result = discovered - else: - result = dispatch_superdoc_tool(client, name, args) - - function_responses.append( - Part.from_function_response(name=name, response=result) - ) - except Exception as e: - function_responses.append( - Part.from_function_response(name=name, response={"error": str(e)}) - ) - - response = chat.send_message(function_responses) - - # 5. Save (in-place to the copy) - client.doc.save() - client.dispose() - print(f"\nSaved to {output_path}") - - -if __name__ == "__main__": - main() diff --git a/examples/ai/vertex/index.ts b/examples/ai/vertex/index.ts deleted file mode 100644 index 4495cda794..0000000000 --- a/examples/ai/vertex/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * SuperDoc + Google Vertex AI - * - * Minimal agentic loop: Gemini on Vertex AI uses SuperDoc tools - * to review and edit a Word document. - * - * Usage: npx tsx index.ts [input.docx] [output.docx] - * - * Requires: Google Cloud credentials configured (gcloud auth application-default login). - */ - -import path from 'node:path'; -import { copyFileSync } from 'node:fs'; -import { - VertexAI, - type FunctionDeclaration, - type Tool as VertexTool, - type Part, -} from '@google-cloud/vertexai'; -import { - createSuperDocClient, - chooseTools, - dispatchSuperDocTool, - sanitizeToolSchemas, - mergeDiscoveredTools, - type ToolGroup, -} from '@superdoc-dev/sdk'; - -const PROJECT = process.env.GOOGLE_CLOUD_PROJECT ?? 'your-project-id'; -const LOCATION = process.env.GOOGLE_CLOUD_LOCATION ?? 'us-central1'; -const MODEL = process.env.VERTEX_MODEL ?? 'gemini-2.5-pro'; - -async function main() { - const [rawInput = 'contract.docx', rawOutput = 'reviewed.docx'] = process.argv.slice(2); - const inputPath = path.resolve(rawInput); - const outputPath = path.resolve(rawOutput); - - // 1. Connect to SuperDoc — copy to output path so the original is preserved - copyFileSync(inputPath, outputPath); - const client = createSuperDocClient(); - await client.connect(); - await client.doc.open({ doc: outputPath }); - - // 2. Get tools in generic format, sanitize for Vertex, and build declarations - const { tools: sdTools } = await chooseTools({ provider: 'generic' }); - const sanitized = sanitizeToolSchemas(sdTools, 'vertex') as Array<{ name: string; description: string; parameters: Record }>; - const vertexTools: VertexTool[] = [{ - functionDeclarations: sanitized.map((t): FunctionDeclaration => ({ - name: t.name, - description: t.description, - parameters: t.parameters as FunctionDeclaration['parameters'], - })), - }]; - - // 3. Set up Vertex AI - const vertexAI = new VertexAI({ project: PROJECT, location: LOCATION }); - const model = vertexAI.getGenerativeModel({ - model: MODEL, - tools: vertexTools, - systemInstruction: { role: 'system', parts: [{ text: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.' }] }, - }); - - const chat = model.startChat(); - - // 4. Agentic loop - let response = await chat.sendMessage([ - { text: 'Review this contract. Fix vague language and one-sided terms.' }, - ]); - - for (let turn = 0; turn < 20; turn++) { - const candidate = response.response.candidates?.[0]; - if (!candidate) break; - - const functionCalls = candidate.content.parts.filter((p) => p.functionCall); - if (!functionCalls.length) { - // Print final response - for (const part of candidate.content.parts) { - if (part.text) console.log(part.text); - } - break; - } - - const functionResponses: Part[] = []; - for (const part of functionCalls) { - const { name, args } = part.functionCall!; - console.log(` Tool: ${name}`); - try { - let result: unknown; - - if (name === 'discover_tools') { - // discover_tools is a meta-tool — handle client-side via chooseTools - const groups = ((args ?? {}) as Record).groups as ToolGroup[] | undefined; - const discovered = await chooseTools({ provider: 'generic', groups }); - mergeDiscoveredTools(vertexTools, discovered, { provider: 'generic', target: 'vertex' }); - result = discovered; - } else { - result = await dispatchSuperDocTool(client, name, (args ?? {}) as Record); - } - - functionResponses.push({ - functionResponse: { name, response: result as object }, - }); - } catch (err) { - functionResponses.push({ - functionResponse: { name, response: { error: (err as Error).message } }, - }); - } - } - - response = await chat.sendMessage(functionResponses); - } - - // 5. Save (in-place to the copy) - await client.doc.save(); - await client.dispose(); - console.log(`\nSaved to ${outputPath}`); -} - -main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/vertex/package.json b/examples/ai/vertex/package.json deleted file mode 100644 index d6e705ba51..0000000000 --- a/examples/ai/vertex/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "superdoc-vertex", - "private": true, - "type": "module", - "scripts": { - "start": "tsx index.ts" - }, - "dependencies": { - "@google-cloud/vertexai": "^1.9.0", - "@superdoc-dev/sdk": "latest" - }, - "devDependencies": { - "tsx": "^4.21.0" - } -} From f2086bf571b654cc5ba0152b8fbd753560d9b7df Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 8 Mar 2026 22:16:52 -0300 Subject: [PATCH 9/9] feat(sdk): add Vertex, Vercel AI, and LangChain integration examples and docs Restore non-Bedrock AI integration examples split from the Bedrock PR. Includes Node.js and Python examples for each platform. --- .../ai-agents/integrations.mdx | 466 ++++++++++++++---- examples/ai/README.md | 11 +- examples/ai/langchain/README.md | 52 ++ examples/ai/langchain/contract.docx | Bin 0 -> 79086 bytes examples/ai/langchain/index.py | 80 +++ examples/ai/langchain/index.ts | 77 +++ examples/ai/langchain/package.json | 18 + examples/ai/vercel-ai/README.md | 41 ++ examples/ai/vercel-ai/contract.docx | Bin 0 -> 79086 bytes examples/ai/vercel-ai/index.ts | 69 +++ examples/ai/vercel-ai/package.json | 17 + examples/ai/vertex/README.md | 43 ++ examples/ai/vertex/contract.docx | Bin 0 -> 79086 bytes examples/ai/vertex/index.py | 122 +++++ examples/ai/vertex/index.ts | 119 +++++ examples/ai/vertex/package.json | 15 + 16 files changed, 1039 insertions(+), 91 deletions(-) create mode 100644 examples/ai/langchain/README.md create mode 100644 examples/ai/langchain/contract.docx create mode 100644 examples/ai/langchain/index.py create mode 100644 examples/ai/langchain/index.ts create mode 100644 examples/ai/langchain/package.json create mode 100644 examples/ai/vercel-ai/README.md create mode 100644 examples/ai/vercel-ai/contract.docx create mode 100644 examples/ai/vercel-ai/index.ts create mode 100644 examples/ai/vercel-ai/package.json create mode 100644 examples/ai/vertex/README.md create mode 100644 examples/ai/vertex/contract.docx create mode 100644 examples/ai/vertex/index.py create mode 100644 examples/ai/vertex/index.ts create mode 100644 examples/ai/vertex/package.json diff --git a/apps/docs/document-engine/ai-agents/integrations.mdx b/apps/docs/document-engine/ai-agents/integrations.mdx index aedd8fb4c8..dfc81f5ed8 100644 --- a/apps/docs/document-engine/ai-agents/integrations.mdx +++ b/apps/docs/document-engine/ai-agents/integrations.mdx @@ -2,8 +2,8 @@ title: Integrations sidebarTitle: Integrations tag: NEW -description: Connect SuperDoc tools to AWS Bedrock and other LLM providers -keywords: "ai integrations, ai providers, aws bedrock, openai, anthropic, tool use, function calling, superdoc sdk" +description: Connect SuperDoc tools to AWS Bedrock, Google Vertex AI, Vercel AI SDK, LangChain, and more +keywords: "ai integrations, ai providers, agent frameworks, aws bedrock, google vertex, vercel ai, langchain, openai, anthropic, tool use, function calling, superdoc sdk" --- SuperDoc tools work with any LLM provider or agent framework that supports tool use. The SDK ships tool definitions in multiple formats — pick the one that matches your stack, write a conversation loop (or let the framework handle it), and dispatch tool calls through the SDK. @@ -14,118 +14,400 @@ Each example below opens a document, gives the model SuperDoc tools, and lets it LLM tools are in alpha. Tool names and schemas may change between releases. -## AWS Bedrock +## Cloud platforms + +Use SuperDoc tools with cloud AI platforms. You write the agentic loop and control the conversation directly. - + + + + ```bash + npm install @superdoc-dev/sdk @aws-sdk/client-bedrock-runtime + ``` + + ```typescript + import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; + import { + createSuperDocClient, chooseTools, dispatchSuperDocTool, + formatToolResult, mergeDiscoveredTools, + } from '@superdoc-dev/sdk'; + + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: './contract.docx' }); + + // Anthropic format → Bedrock toolSpec shape + const { tools } = await chooseTools({ provider: 'anthropic' }); + const toolConfig = { tools: [] }; + mergeDiscoveredTools(toolConfig, { tools }, { provider: 'anthropic', target: 'bedrock' }); + + const bedrock = new BedrockRuntimeClient({ region: 'us-east-1' }); + const messages = [ + { role: 'user', content: [{ text: 'Review this contract.' }] }, + ]; + + while (true) { + const res = await bedrock.send(new ConverseCommand({ + modelId: 'us.anthropic.claude-sonnet-4-6', + messages, + system: [{ text: 'You edit .docx files using SuperDoc tools.' }], + toolConfig, + })); + + const output = res.output?.message; + if (!output) break; + messages.push(output); + + const toolUses = output.content?.filter((b) => b.toolUse) ?? []; + if (!toolUses.length) break; + + const results = []; + for (const block of toolUses) { + const { name, input, toolUseId } = block.toolUse; + const result = await dispatchSuperDocTool(client, name, input ?? {}); + results.push(formatToolResult(result, { target: 'bedrock', toolUseId })); + } + messages.push({ role: 'user', content: results }); + } + + await client.doc.save({ inPlace: true }); + await client.dispose(); + ``` + + + ```bash + pip install superdoc-sdk boto3 + ``` + + ```python + import boto3 + from superdoc import ( + SuperDocClient, choose_tools, dispatch_superdoc_tool, + format_tool_result, merge_discovered_tools, + ) + + client = SuperDocClient() + client.connect() + client.doc.open(doc="./contract.docx") + + # Anthropic format → Bedrock toolSpec shape + sd_tools = choose_tools(provider="anthropic") + tool_config = {"tools": []} + merge_discovered_tools(tool_config, sd_tools, provider="anthropic", target="bedrock") + + bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") + messages = [{"role": "user", "content": [{"text": "Review this contract."}]}] + + while True: + response = bedrock.converse( + modelId="us.anthropic.claude-sonnet-4-6", + messages=messages, + system=[{"text": "You edit .docx files using SuperDoc tools."}], + toolConfig=tool_config, + ) + + output = response["output"]["message"] + messages.append(output) + + tool_uses = [b for b in output.get("content", []) if "toolUse" in b] + if not tool_uses: + break + + tool_results = [] + for block in tool_uses: + tu = block["toolUse"] + result = dispatch_superdoc_tool(client, tu["name"], tu.get("input", {})) + tool_results.append( + format_tool_result(result, target="bedrock", tool_use_id=tu["toolUseId"]) + ) + messages.append({"role": "user", "content": tool_results}) + + client.doc.save(in_place=True) + client.dispose() + ``` + + + + **Auth**: AWS credentials via `aws configure`, env vars, or IAM role. No API key needed. + + + + + + ```bash + npm install @superdoc-dev/sdk @google-cloud/vertexai + ``` + + ```typescript + import { VertexAI } from '@google-cloud/vertexai'; + import { + createSuperDocClient, chooseTools, dispatchSuperDocTool, + sanitizeToolSchemas, + } from '@superdoc-dev/sdk'; + + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: './contract.docx' }); + + // Generic format → Vertex function declarations (sanitized for Vertex compatibility) + const { tools } = await chooseTools({ provider: 'generic' }); + const sanitized = sanitizeToolSchemas(tools, 'vertex'); + const vertexTools = [{ + functionDeclarations: sanitized.map((t) => ({ + name: t.name, + description: t.description, + parameters: t.parameters, + })), + }]; + + const vertexAI = new VertexAI({ project: 'your-project', location: 'us-central1' }); + const model = vertexAI.getGenerativeModel({ + model: 'gemini-2.5-pro', + tools: vertexTools, + systemInstruction: { role: 'system', parts: [{ text: 'You edit .docx files using SuperDoc tools.' }] }, + }); + + const chat = model.startChat(); + let response = await chat.sendMessage([{ text: 'Review this contract.' }]); + + while (true) { + const parts = response.response.candidates?.[0]?.content.parts ?? []; + const calls = parts.filter((p) => p.functionCall); + if (!calls.length) break; + + const results = []; + for (const part of calls) { + const { name, args } = part.functionCall; + const result = await dispatchSuperDocTool(client, name, args ?? {}); + results.push({ functionResponse: { name, response: result } }); + } + response = await chat.sendMessage(results); + } + + await client.doc.save({ inPlace: true }); + await client.dispose(); + ``` + + + ```bash + pip install superdoc-sdk google-cloud-aiplatform + ``` + + ```python + import vertexai + from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration, Part + from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool, sanitize_tool_schemas + + client = SuperDocClient() + client.connect() + client.doc.open(doc="./contract.docx") + + # Generic format → Vertex function declarations (sanitized for Vertex compatibility) + result = choose_tools(provider="generic") + sanitized = sanitize_tool_schemas(result["tools"], "vertex") + vertex_tools = [Tool(function_declarations=[ + FunctionDeclaration(name=t["name"], description=t["description"], parameters=t["parameters"]) + for t in sanitized + ])] + + vertexai.init(project="your-project", location="us-central1") + model = GenerativeModel( + "gemini-2.5-pro", + tools=vertex_tools, + system_instruction="You edit .docx files using SuperDoc tools.", + ) + chat = model.start_chat() + response = chat.send_message("Review this contract.") + + while True: + calls = [p for p in response.candidates[0].content.parts if p.function_call.name] + if not calls: + break + + responses = [] + for part in calls: + name = part.function_call.name + args = dict(part.function_call.args) if part.function_call.args else {} + result = dispatch_superdoc_tool(client, name, args) + responses.append(Part.from_function_response(name=name, response=result)) + response = chat.send_message(responses) + + client.doc.save(in_place=True) + client.dispose() + ``` + + + + **Auth**: `gcloud auth application-default login` or a service account key. + + + +## Agent frameworks + +Use SuperDoc tools with agent frameworks. The framework manages the agentic loop — you configure tools and let it run. + + + ```bash - npm install @superdoc-dev/sdk @aws-sdk/client-bedrock-runtime + npm install @superdoc-dev/sdk ai @ai-sdk/openai zod ``` ```typescript - import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; - import { - createSuperDocClient, chooseTools, dispatchSuperDocTool, - formatToolResult, mergeDiscoveredTools, - } from '@superdoc-dev/sdk'; + import { generateText, tool } from 'ai'; + import { openai } from '@ai-sdk/openai'; + import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + import { z } from 'zod'; const client = createSuperDocClient(); await client.connect(); await client.doc.open({ doc: './contract.docx' }); - // Anthropic format → Bedrock toolSpec shape - const { tools } = await chooseTools({ provider: 'anthropic' }); - const toolConfig = { tools: [] }; - mergeDiscoveredTools(toolConfig, { tools }, { provider: 'anthropic', target: 'bedrock' }); - - const bedrock = new BedrockRuntimeClient({ region: 'us-east-1' }); - const messages = [ - { role: 'user', content: [{ text: 'Review this contract.' }] }, - ]; - - while (true) { - const res = await bedrock.send(new ConverseCommand({ - modelId: 'us.anthropic.claude-sonnet-4-6', - messages, - system: [{ text: 'You edit .docx files using SuperDoc tools.' }], - toolConfig, - })); - - const output = res.output?.message; - if (!output) break; - messages.push(output); - - const toolUses = output.content?.filter((b) => b.toolUse) ?? []; - if (!toolUses.length) break; - - const results = []; - for (const block of toolUses) { - const { name, input, toolUseId } = block.toolUse; - const result = await dispatchSuperDocTool(client, name, input ?? {}); - results.push(formatToolResult(result, { target: 'bedrock', toolUseId })); - } - messages.push({ role: 'user', content: results }); + // All tools — no discover_tools since the framework manages a fixed tool set + const { tools: sdTools } = await chooseTools({ provider: 'vercel', mode: 'all' }); + + // Wrap as Vercel AI tool() objects + const tools = {}; + for (const t of sdTools) { + tools[t.function.name] = tool({ + description: t.function.description, + parameters: z.object({}).passthrough(), + execute: async (args) => + dispatchSuperDocTool(client, t.function.name, args), + }); } + // generateText handles the agentic loop automatically + const result = await generateText({ + model: openai('gpt-4o'), + system: 'You edit .docx files using SuperDoc tools.', + prompt: 'Review this contract.', + tools, + maxSteps: 20, + }); + + console.log(result.text); await client.doc.save({ inPlace: true }); await client.dispose(); ``` + + **Auth**: `OPENAI_API_KEY` env var. Swap `openai(...)` for `anthropic(...)`, `google(...)`, etc. - - ```bash - pip install superdoc-sdk boto3 - ``` - ```python - import boto3 - from superdoc import ( - SuperDocClient, choose_tools, dispatch_superdoc_tool, - format_tool_result, merge_discovered_tools, - ) - - client = SuperDocClient() - client.connect() - client.doc.open(doc="./contract.docx") - - # Anthropic format → Bedrock toolSpec shape - sd_tools = choose_tools(provider="anthropic") - tool_config = {"tools": []} - merge_discovered_tools(tool_config, sd_tools, provider="anthropic", target="bedrock") - - bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") - messages = [{"role": "user", "content": [{"text": "Review this contract."}]}] - - while True: - response = bedrock.converse( - modelId="us.anthropic.claude-sonnet-4-6", - messages=messages, - system=[{"text": "You edit .docx files using SuperDoc tools."}], - toolConfig=tool_config, - ) + + + + ```bash + npm install @superdoc-dev/sdk @langchain/openai @langchain/core @langchain/langgraph zod + ``` + + ```typescript + import { ChatOpenAI } from '@langchain/openai'; + import { DynamicStructuredTool } from '@langchain/core/tools'; + import { createReactAgent } from '@langchain/langgraph/prebuilt'; + import { HumanMessage } from '@langchain/core/messages'; + import { z } from 'zod'; + import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: './contract.docx' }); + + // All tools — no discover_tools since the framework manages a fixed tool set + const { tools: sdTools } = await chooseTools({ provider: 'generic', mode: 'all' }); + + // Wrap as LangChain DynamicStructuredTool objects + const tools = sdTools.map( + (t) => new DynamicStructuredTool({ + name: t.name, + description: t.description, + schema: z.object({}).passthrough(), + func: async (args) => { + const result = await dispatchSuperDocTool(client, t.name, args); + return JSON.stringify(result); + }, + }), + ); + + const agent = createReactAgent({ + llm: new ChatOpenAI({ model: 'gpt-4o' }), + tools, + prompt: 'You edit .docx files using SuperDoc tools.', + }); + + const result = await agent.invoke({ + messages: [new HumanMessage('Review this contract.')], + }); + + console.log(result.messages.at(-1).content); + await client.doc.save({ inPlace: true }); + await client.dispose(); + ``` + + + ```bash + pip install superdoc-sdk langchain-openai langgraph + ``` + + ```python + import json + from langchain_openai import ChatOpenAI + from langchain_core.tools import StructuredTool + from langgraph.prebuilt import create_react_agent + from langchain_core.messages import HumanMessage + from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool + + client = SuperDocClient() + client.connect() + client.doc.open(doc="./contract.docx") + + # All tools — no discover_tools since the framework manages a fixed tool set + result = choose_tools(provider="generic", mode="all") + + # Wrap as LangChain StructuredTool objects + def make_tool(t): + def invoke(**kwargs) -> str: + return json.dumps(dispatch_superdoc_tool(client, t["name"], kwargs)) + return StructuredTool.from_function( + func=invoke, name=t["name"], description=t["description"], + infer_schema=False, + ) - output = response["output"]["message"] - messages.append(output) + tools = [make_tool(t) for t in result["tools"]] - tool_uses = [b for b in output.get("content", []) if "toolUse" in b] - if not tool_uses: - break + agent = create_react_agent( + model=ChatOpenAI(model="gpt-4o"), + tools=tools, + prompt="You edit .docx files using SuperDoc tools.", + ) - tool_results = [] - for block in tool_uses: - tu = block["toolUse"] - result = dispatch_superdoc_tool(client, tu["name"], tu.get("input", {})) - tool_results.append( - format_tool_result(result, target="bedrock", tool_use_id=tu["toolUseId"]) - ) - messages.append({"role": "user", "content": tool_results}) + result = agent.invoke( + {"messages": [HumanMessage(content="Review this contract.")]} + ) + print(result["messages"][-1].content) - client.doc.save(in_place=True) - client.dispose() - ``` + client.doc.save(in_place=True) + client.dispose() + ``` + + + + **Auth**: `OPENAI_API_KEY` env var. Swap `ChatOpenAI` for `ChatAnthropic`, `ChatGoogleGenerativeAI`, etc. -**Auth**: AWS credentials via `aws configure`, env vars, or IAM role. No API key needed. +## Tool format reference + +The SDK ships pre-formatted tools for each integration. The conversion is minimal: + +| Integration | Type | SDK format | SDK helpers | Native shape | +|-------------|------|-----------|-------------|--------------| +| AWS Bedrock | Cloud platform | `anthropic` | `mergeDiscoveredTools`, `formatToolResult`, `formatToolError` | `{ toolSpec: { name, description, inputSchema: { json } } }` | +| Google Vertex AI | Cloud platform | `generic` | `sanitizeToolSchemas`, `mergeDiscoveredTools` | `{ functionDeclarations: [...] }` | +| Vercel AI SDK | Framework | `vercel` | — | Wrap in Vercel `tool()` with `z.object({}).passthrough()` | +| LangChain | Framework | `generic` | — | Wrap in `DynamicStructuredTool` | +| OpenAI | Direct API | `openai` | `formatToolResult` | Pass directly | +| Anthropic | Direct API | `anthropic` | `formatToolResult` | Pass directly | ## The discover_tools pattern @@ -146,6 +428,10 @@ if (name === 'discover_tools') { } ``` + +Framework-managed examples (Vercel AI, LangChain) use `mode: 'all'` instead, since they can't inject new tools mid-conversation. + + See the [LLM Tools guide](/document-engine/ai-agents/llm-tools#the-discover_tools-pattern) for details. ## Tracked changes @@ -203,7 +489,7 @@ editor.destroy(); ## Example repository -Complete, runnable examples are available at [`examples/ai/`](https://github.com/superdoc-dev/superdoc/tree/main/examples/ai). +Complete, runnable examples for all cloud platforms and frameworks (Node.js and Python) are available at [`examples/ai/`](https://github.com/superdoc-dev/superdoc/tree/main/examples/ai). ## Related diff --git a/examples/ai/README.md b/examples/ai/README.md index 029c780f2e..c1091a93f5 100644 --- a/examples/ai/README.md +++ b/examples/ai/README.md @@ -11,6 +11,16 @@ You write the agentic loop and control the conversation directly. | Platform | Node.js | Python | Auth | |----------|---------|--------|------| | [AWS Bedrock](./bedrock) | `index.ts` | `index.py` | AWS credentials (`aws configure`) | +| [Google Vertex AI](./vertex) | `index.ts` | `index.py` | Google Cloud credentials (`gcloud auth application-default login`) | + +## Agent frameworks + +The framework manages the agentic loop — you configure tools and let it run. + +| Framework | Node.js | Python | Auth | +|-----------|---------|--------|------| +| [Vercel AI SDK](./vercel-ai) | `index.ts` | — | `OPENAI_API_KEY` | +| [LangChain](./langchain) | `index.ts` | `index.py` | `OPENAI_API_KEY` | ## Full demo @@ -28,7 +38,6 @@ npx tsx index.ts contract.docx reviewed.docx # Python cd bedrock -python -m venv venv && source venv/bin/activate pip install superdoc-sdk boto3 python index.py contract.docx reviewed.docx ``` diff --git a/examples/ai/langchain/README.md b/examples/ai/langchain/README.md new file mode 100644 index 0000000000..18a36098f8 --- /dev/null +++ b/examples/ai/langchain/README.md @@ -0,0 +1,52 @@ +# SuperDoc + LangChain + +Agentic document editing using a LangGraph ReAct agent. + +**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) + +## Prerequisites + +- `OPENAI_API_KEY` environment variable (or swap the model) + +## Run + +### Node.js + +```bash +npm install +OPENAI_API_KEY=sk-... npx tsx index.ts contract.docx reviewed.docx +``` + +### Python + +```bash +python -m venv venv && source venv/bin/activate +pip install superdoc-sdk langchain-openai langgraph +OPENAI_API_KEY=sk-... python index.py contract.docx reviewed.docx +``` + +## Configuration + +The example uses OpenAI by default. Swap the model class to use any LangChain-compatible provider: + +```python +# OpenAI (default) +from langchain_openai import ChatOpenAI +model = ChatOpenAI(model="gpt-4o") + +# Anthropic +from langchain_anthropic import ChatAnthropic +model = ChatAnthropic(model="claude-sonnet-4-6-20250725") + +# Google +from langchain_google_genai import ChatGoogleGenerativeAI +model = ChatGoogleGenerativeAI(model="gemini-2.5-pro") +``` + +## How it works + +1. Connects to SuperDoc via the SDK +2. Loads tool definitions in generic format and wraps them as LangChain `StructuredTool` / `DynamicStructuredTool` objects +3. Creates a ReAct agent with `create_react_agent` +4. The agent calls SuperDoc tools to read, query, and edit the document +5. Saves the reviewed document diff --git a/examples/ai/langchain/contract.docx b/examples/ai/langchain/contract.docx new file mode 100644 index 0000000000000000000000000000000000000000..5b911d339926dd9eebe45bddf9e99267496193fa GIT binary patch literal 79086 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 str: + print(f" Tool: {tool_def['name']}") + result = dispatch_superdoc_tool(client, tool_def["name"], kwargs) + return json.dumps(result) + + return StructuredTool.from_function( + func=invoke, + name=tool_def["name"], + description=tool_def["description"], + infer_schema=False, + ) + + +def main(): + args = sys.argv[1:] + input_path = str(Path(args[0] if args else "contract.docx").resolve()) + output_path = str(Path(args[1] if len(args) > 1 else "reviewed.docx").resolve()) + + # 1. Connect to SuperDoc — copy to output path so the original is preserved + shutil.copy2(input_path, output_path) + client = SuperDocClient() + client.connect() + client.doc.open({"doc": output_path}) + + # 2. Get tools in generic format and wrap as LangChain tools + # Use mode="all" — no discover_tools since the framework manages a fixed tool set + result = choose_tools({"provider": "generic", "mode": "all"}) + tools = [make_superdoc_tool(client, t) for t in result["tools"]] + + # 3. Create a ReAct agent + model = ChatOpenAI(model="gpt-4o") + agent = create_react_agent( + model=model, + tools=tools, + prompt="You edit .docx files using SuperDoc tools. Use tracked changes for all edits.", + ) + + # 4. Run the agent + result = agent.invoke( + {"messages": [HumanMessage(content="Review this contract. Fix vague language and one-sided terms.")]} + ) + + last_message = result["messages"][-1] + print(last_message.content) + + # 5. Save (in-place to the copy) + client.doc.save() + client.dispose() + print(f"\nSaved to {output_path}") + + +if __name__ == "__main__": + main() diff --git a/examples/ai/langchain/index.ts b/examples/ai/langchain/index.ts new file mode 100644 index 0000000000..58363bbb8f --- /dev/null +++ b/examples/ai/langchain/index.ts @@ -0,0 +1,77 @@ +/** + * SuperDoc + LangChain + * + * Minimal agentic loop: any LangChain-compatible model uses SuperDoc tools + * to review and edit a Word document. + * + * Usage: OPENAI_API_KEY=sk-... npx tsx index.ts [input.docx] [output.docx] + * + * Requires: OPENAI_API_KEY (or swap ChatOpenAI for ChatAnthropic, ChatGoogleGenerativeAI, etc.) + */ + +import path from 'node:path'; +import { copyFileSync } from 'node:fs'; +import { ChatOpenAI } from '@langchain/openai'; +import { DynamicStructuredTool } from '@langchain/core/tools'; +import { createReactAgent } from '@langchain/langgraph/prebuilt'; +import { HumanMessage } from '@langchain/core/messages'; +import { z } from 'zod'; +import { + createSuperDocClient, + chooseTools, + dispatchSuperDocTool, +} from '@superdoc-dev/sdk'; + +async function main() { + const [rawInput = 'contract.docx', rawOutput = 'reviewed.docx'] = process.argv.slice(2); + const inputPath = path.resolve(rawInput); + const outputPath = path.resolve(rawOutput); + + // 1. Connect to SuperDoc — copy to output path so the original is preserved + copyFileSync(inputPath, outputPath); + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: outputPath }); + + // 2. Get tools in generic format and wrap as LangChain tools (all tools — no discover_tools since the framework manages a fixed tool set) + const { tools: sdTools } = await chooseTools({ provider: 'generic', mode: 'all' }); + + const langchainTools = ( + sdTools as Array<{ name: string; description: string; parameters: Record }> + ).map( + (t) => + new DynamicStructuredTool({ + name: t.name, + description: t.description, + schema: z.object({}).passthrough(), // Accept any params — SuperDoc SDK validates + func: async (args) => { + console.log(` Tool: ${t.name}`); + const result = await dispatchSuperDocTool(client, t.name, args as Record); + return JSON.stringify(result); + }, + }), + ); + + // 3. Create a ReAct agent + const model = new ChatOpenAI({ model: 'gpt-4o' }); + const agent = createReactAgent({ + llm: model, + tools: langchainTools, + prompt: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.', + }); + + // 4. Run the agent + const result = await agent.invoke({ + messages: [new HumanMessage('Review this contract. Fix vague language and one-sided terms.')], + }); + + const lastMessage = result.messages[result.messages.length - 1]; + console.log(lastMessage.content); + + // 5. Save (in-place to the copy) + await client.doc.save(); + await client.dispose(); + console.log(`\nSaved to ${outputPath}`); +} + +main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/langchain/package.json b/examples/ai/langchain/package.json new file mode 100644 index 0000000000..af11d98746 --- /dev/null +++ b/examples/ai/langchain/package.json @@ -0,0 +1,18 @@ +{ + "name": "superdoc-langchain", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@langchain/openai": "^0.4.0", + "@langchain/core": "^0.3.0", + "@langchain/langgraph": "^0.2.0", + "@superdoc-dev/sdk": "latest", + "zod": "^3.23.0" + }, + "devDependencies": { + "tsx": "^4.21.0" + } +} diff --git a/examples/ai/vercel-ai/README.md b/examples/ai/vercel-ai/README.md new file mode 100644 index 0000000000..c7ffcded76 --- /dev/null +++ b/examples/ai/vercel-ai/README.md @@ -0,0 +1,41 @@ +# SuperDoc + Vercel AI SDK + +Agentic document editing using the Vercel AI SDK. The cleanest integration — `generateText` handles the agentic loop automatically. + +**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) + +## Prerequisites + +- `OPENAI_API_KEY` environment variable (or swap the provider) + +## Run + +```bash +npm install +OPENAI_API_KEY=sk-... npx tsx index.ts contract.docx reviewed.docx +``` + +## Configuration + +The example uses OpenAI by default. Swap the provider import to use any model Vercel AI supports: + +```typescript +// OpenAI (default) +import { openai } from '@ai-sdk/openai'; +model: openai('gpt-4o') + +// Anthropic +import { anthropic } from '@ai-sdk/anthropic'; +model: anthropic('claude-sonnet-4-6-20250725') + +// Google +import { google } from '@ai-sdk/google'; +model: google('gemini-2.5-pro') +``` + +## How it works + +1. Connects to SuperDoc via the SDK +2. Loads tool definitions in Vercel format and wraps them as `tool()` objects +3. Calls `generateText` with `maxSteps: 20` — the SDK handles the tool call loop +4. Saves the reviewed document diff --git a/examples/ai/vercel-ai/contract.docx b/examples/ai/vercel-ai/contract.docx new file mode 100644 index 0000000000000000000000000000000000000000..5b911d339926dd9eebe45bddf9e99267496193fa GIT binary patch literal 79086 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 = {}; + for (const t of sdTools as Array<{ type: string; function: { name: string; description: string; parameters: Record } }>) { + const fn = t.function; + vercelTools[fn.name] = tool({ + description: fn.description, + parameters: z.object({}).passthrough(), // Accept any params — SuperDoc SDK validates + execute: async (args) => { + console.log(` Tool: ${fn.name}`); + return dispatchSuperDocTool(client, fn.name, args as Record); + }, + }); + } + + // 3. Run with generateText — handles the agentic loop automatically + const result = await generateText({ + model: openai('gpt-4o'), + system: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.', + prompt: 'Review this contract. Fix vague language and one-sided terms.', + tools: vercelTools, + maxSteps: 20, + }); + + console.log(result.text); + + // 4. Save (in-place to the copy) + await client.doc.save(); + await client.dispose(); + console.log(`\nSaved to ${outputPath}`); +} + +main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/vercel-ai/package.json b/examples/ai/vercel-ai/package.json new file mode 100644 index 0000000000..b66706966f --- /dev/null +++ b/examples/ai/vercel-ai/package.json @@ -0,0 +1,17 @@ +{ + "name": "superdoc-vercel-ai", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "ai": "^4.0.0", + "@ai-sdk/openai": "^1.0.0", + "@superdoc-dev/sdk": "latest", + "zod": "^3.23.0" + }, + "devDependencies": { + "tsx": "^4.21.0" + } +} diff --git a/examples/ai/vertex/README.md b/examples/ai/vertex/README.md new file mode 100644 index 0000000000..53799e73b4 --- /dev/null +++ b/examples/ai/vertex/README.md @@ -0,0 +1,43 @@ +# SuperDoc + Google Vertex AI + +Agentic document editing using Gemini on Vertex AI. + +**Docs:** [Integrations](https://docs.superdoc.dev/document-engine/ai-agents/integrations) + +## Prerequisites + +- Google Cloud credentials (`gcloud auth application-default login` or a service account key) +- A Google Cloud project with Vertex AI API enabled + +## Run + +### Node.js + +```bash +npm install +GOOGLE_CLOUD_PROJECT=your-project npx tsx index.ts contract.docx reviewed.docx +``` + +### Python + +```bash +python -m venv venv && source venv/bin/activate +pip install superdoc-sdk google-cloud-aiplatform +GOOGLE_CLOUD_PROJECT=your-project python index.py contract.docx reviewed.docx +``` + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `GOOGLE_CLOUD_PROJECT` | `your-project-id` | Google Cloud project ID | +| `GOOGLE_CLOUD_LOCATION` | `us-central1` | Vertex AI region | +| `VERTEX_MODEL` | `gemini-2.5-pro` | Any Gemini model that supports function calling | + +## How it works + +1. Connects to SuperDoc via the SDK +2. Loads tool definitions in generic format and converts to Vertex `functionDeclarations` +3. Starts a chat with Gemini +4. Runs an agentic loop: the model calls SuperDoc tools to read, query, and edit the document +5. Saves the reviewed document diff --git a/examples/ai/vertex/contract.docx b/examples/ai/vertex/contract.docx new file mode 100644 index 0000000000000000000000000000000000000000..5b911d339926dd9eebe45bddf9e99267496193fa GIT binary patch literal 79086 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 1 else "reviewed.docx").resolve()) + + # 1. Connect to SuperDoc — copy to output path so the original is preserved + shutil.copy2(input_path, output_path) + client = SuperDocClient() + client.connect() + client.doc.open({"doc": output_path}) + + # 2. Get tools in generic format and convert to Vertex shape + result = choose_tools({"provider": "generic"}) + vertex_tools = to_vertex_tools(result["tools"]) + + # 3. Set up Vertex AI + vertexai.init(project=PROJECT, location=LOCATION) + model = GenerativeModel( + MODEL, + tools=vertex_tools, + system_instruction="You edit .docx files using SuperDoc tools. Use tracked changes for all edits.", + ) + chat = model.start_chat() + + # 4. Agentic loop + response = chat.send_message("Review this contract. Fix vague language and one-sided terms.") + + for _ in range(20): + function_calls = [ + part for part in response.candidates[0].content.parts if part.function_call.name + ] + + if not function_calls: + # Print final response + for part in response.candidates[0].content.parts: + if part.text: + print(part.text) + break + + function_responses = [] + for part in function_calls: + name = part.function_call.name + args = dict(part.function_call.args) if part.function_call.args else {} + print(f" Tool: {name}") + + try: + if name == "discover_tools": + # discover_tools is a meta-tool — handle client-side via choose_tools + groups = args.get("groups") + discovered = choose_tools({"provider": "generic", "groups": groups}) + new_tools = discovered.get("tools", []) + sanitized = sanitize_tool_schemas(new_tools, "vertex") + for t in sanitized: + vertex_tools[0].function_declarations.append( + FunctionDeclaration( + name=t["name"], + description=t["description"], + parameters=t["parameters"], + ) + ) + result = discovered + else: + result = dispatch_superdoc_tool(client, name, args) + + function_responses.append( + Part.from_function_response(name=name, response=result) + ) + except Exception as e: + function_responses.append( + Part.from_function_response(name=name, response={"error": str(e)}) + ) + + response = chat.send_message(function_responses) + + # 5. Save (in-place to the copy) + client.doc.save() + client.dispose() + print(f"\nSaved to {output_path}") + + +if __name__ == "__main__": + main() diff --git a/examples/ai/vertex/index.ts b/examples/ai/vertex/index.ts new file mode 100644 index 0000000000..4495cda794 --- /dev/null +++ b/examples/ai/vertex/index.ts @@ -0,0 +1,119 @@ +/** + * SuperDoc + Google Vertex AI + * + * Minimal agentic loop: Gemini on Vertex AI uses SuperDoc tools + * to review and edit a Word document. + * + * Usage: npx tsx index.ts [input.docx] [output.docx] + * + * Requires: Google Cloud credentials configured (gcloud auth application-default login). + */ + +import path from 'node:path'; +import { copyFileSync } from 'node:fs'; +import { + VertexAI, + type FunctionDeclaration, + type Tool as VertexTool, + type Part, +} from '@google-cloud/vertexai'; +import { + createSuperDocClient, + chooseTools, + dispatchSuperDocTool, + sanitizeToolSchemas, + mergeDiscoveredTools, + type ToolGroup, +} from '@superdoc-dev/sdk'; + +const PROJECT = process.env.GOOGLE_CLOUD_PROJECT ?? 'your-project-id'; +const LOCATION = process.env.GOOGLE_CLOUD_LOCATION ?? 'us-central1'; +const MODEL = process.env.VERTEX_MODEL ?? 'gemini-2.5-pro'; + +async function main() { + const [rawInput = 'contract.docx', rawOutput = 'reviewed.docx'] = process.argv.slice(2); + const inputPath = path.resolve(rawInput); + const outputPath = path.resolve(rawOutput); + + // 1. Connect to SuperDoc — copy to output path so the original is preserved + copyFileSync(inputPath, outputPath); + const client = createSuperDocClient(); + await client.connect(); + await client.doc.open({ doc: outputPath }); + + // 2. Get tools in generic format, sanitize for Vertex, and build declarations + const { tools: sdTools } = await chooseTools({ provider: 'generic' }); + const sanitized = sanitizeToolSchemas(sdTools, 'vertex') as Array<{ name: string; description: string; parameters: Record }>; + const vertexTools: VertexTool[] = [{ + functionDeclarations: sanitized.map((t): FunctionDeclaration => ({ + name: t.name, + description: t.description, + parameters: t.parameters as FunctionDeclaration['parameters'], + })), + }]; + + // 3. Set up Vertex AI + const vertexAI = new VertexAI({ project: PROJECT, location: LOCATION }); + const model = vertexAI.getGenerativeModel({ + model: MODEL, + tools: vertexTools, + systemInstruction: { role: 'system', parts: [{ text: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.' }] }, + }); + + const chat = model.startChat(); + + // 4. Agentic loop + let response = await chat.sendMessage([ + { text: 'Review this contract. Fix vague language and one-sided terms.' }, + ]); + + for (let turn = 0; turn < 20; turn++) { + const candidate = response.response.candidates?.[0]; + if (!candidate) break; + + const functionCalls = candidate.content.parts.filter((p) => p.functionCall); + if (!functionCalls.length) { + // Print final response + for (const part of candidate.content.parts) { + if (part.text) console.log(part.text); + } + break; + } + + const functionResponses: Part[] = []; + for (const part of functionCalls) { + const { name, args } = part.functionCall!; + console.log(` Tool: ${name}`); + try { + let result: unknown; + + if (name === 'discover_tools') { + // discover_tools is a meta-tool — handle client-side via chooseTools + const groups = ((args ?? {}) as Record).groups as ToolGroup[] | undefined; + const discovered = await chooseTools({ provider: 'generic', groups }); + mergeDiscoveredTools(vertexTools, discovered, { provider: 'generic', target: 'vertex' }); + result = discovered; + } else { + result = await dispatchSuperDocTool(client, name, (args ?? {}) as Record); + } + + functionResponses.push({ + functionResponse: { name, response: result as object }, + }); + } catch (err) { + functionResponses.push({ + functionResponse: { name, response: { error: (err as Error).message } }, + }); + } + } + + response = await chat.sendMessage(functionResponses); + } + + // 5. Save (in-place to the copy) + await client.doc.save(); + await client.dispose(); + console.log(`\nSaved to ${outputPath}`); +} + +main().catch((e) => { console.error(e.message); process.exit(1); }); diff --git a/examples/ai/vertex/package.json b/examples/ai/vertex/package.json new file mode 100644 index 0000000000..d6e705ba51 --- /dev/null +++ b/examples/ai/vertex/package.json @@ -0,0 +1,15 @@ +{ + "name": "superdoc-vertex", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@google-cloud/vertexai": "^1.9.0", + "@superdoc-dev/sdk": "latest" + }, + "devDependencies": { + "tsx": "^4.21.0" + } +}