From 6fca6958a4c6f58d2ad0a8e44d64bc87c057f0f1 Mon Sep 17 00:00:00 2001 From: xiaban <980892894@qq.com> Date: Tue, 7 Jul 2026 20:19:57 +0800 Subject: [PATCH] feat: add migration --- AGENTS.md | 1 + CLAUDE.md | 1 + skills/makers-migration/SKILL.md | 465 ++++++++++++++++++ .../references/api-route-to-makers.md | 188 +++++++ .../references/claude-agent-sdk-to-makers.md | 180 +++++++ .../references/crewai-to-makers.md | 135 +++++ .../references/deepagents-to-makers.md | 152 ++++++ .../references/langgraph-to-makers.md | 179 +++++++ .../references/openai-agents-to-makers.md | 161 ++++++ 9 files changed, 1462 insertions(+) create mode 100644 skills/makers-migration/SKILL.md create mode 100644 skills/makers-migration/references/api-route-to-makers.md create mode 100644 skills/makers-migration/references/claude-agent-sdk-to-makers.md create mode 100644 skills/makers-migration/references/crewai-to-makers.md create mode 100644 skills/makers-migration/references/deepagents-to-makers.md create mode 100644 skills/makers-migration/references/langgraph-to-makers.md create mode 100644 skills/makers-migration/references/openai-agents-to-makers.md diff --git a/AGENTS.md b/AGENTS.md index 5e502b3..971f5b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ When you need EdgeOne Makers platform development guidance, read the matching Sk | Task | Read | |------|------| | AI Agent development (DeepAgents, LangGraph, Claude SDK, OpenAI Agents, CrewAI) | skills/makers-agents/SKILL.md | +| Migrate existing agent project to EdgeOne Makers format | skills/makers-migration/SKILL.md | | Deploy project to EdgeOne | skills/makers-deploy/SKILL.md | | Edge Functions (V8 lightweight functions) | skills/makers-edge-functions/SKILL.md | | Cloud Functions (Node.js / Go / Python APIs) | skills/makers-cloud-functions/SKILL.md | diff --git a/CLAUDE.md b/CLAUDE.md index a4ca7f5..aa2e78a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,6 +5,7 @@ When you need EdgeOne Makers platform development guidance, read the matching Sk | Task | Read | |------|------| | AI Agent development (DeepAgents, LangGraph, Claude SDK, OpenAI Agents, CrewAI) | skills/makers-agents/SKILL.md | +| Migrate existing agent project to EdgeOne Makers format | skills/makers-migration/SKILL.md | | Deploy project to EdgeOne | skills/makers-deploy/SKILL.md | | Edge Functions (V8 lightweight functions) | skills/makers-edge-functions/SKILL.md | | Cloud Functions (Node.js / Go / Python APIs) | skills/makers-cloud-functions/SKILL.md | diff --git a/skills/makers-migration/SKILL.md b/skills/makers-migration/SKILL.md new file mode 100644 index 0000000..449d501 --- /dev/null +++ b/skills/makers-migration/SKILL.md @@ -0,0 +1,465 @@ +--- +name: makers-migration +description: >- + Migrate existing AI agent projects (LangChain, LangGraph, OpenAI Agents SDK, + Claude Agent SDK, CrewAI) to EdgeOne Makers platform conventions. + Use when the user wants to adapt a standard agent project to run on EdgeOne Makers, + convert Express/Next.js API routes to Makers handlers, or add platform capabilities + (context.tools, context.sandbox, context.store). + Do NOT trigger for new agent projects (use makers-agents instead). +metadata: + author: edgeone + version: "1.0.0" +--- + +# EdgeOne Makers Migration Guide + +Migrate existing AI agent projects to the **EdgeOne Makers** platform format. Covers structural conversion, API adaptation, and platform capability injection. + +--- + +## Migration Decision Tree + +``` +What type of project are you migrating? +├── Python project +│ ├── Using CrewAI → See §2 CrewAI +│ ├── Using LangChain/LangGraph/DeepAgents → See §3 LangGraph (Python) +│ ├── Using OpenAI Agents SDK → See §4 OpenAI Agents (Python) +│ └── Using Claude Agent SDK → See §5 Claude SDK (Python) +└── Node/TS project + ├── Using Express/Next.js API routes → See §6 Express → Makers + ├── Using LangGraph/DeepAgents → See §3 LangGraph (Node) + ├── Using OpenAI Agents SDK → See §4 OpenAI Agents (Node) + └── Using Claude Agent SDK → See §5 Claude SDK (Node) +``` + +--- + +## ⚠️ Migration Checklist (common to all frameworks) + +Before starting framework-specific changes, check these global items: + +- [ ] Create `edgeone.json` with correct `agents.framework` and `buildCommand`/`outputDirectory` +- [ ] Move backend code from Express routes / Next.js API routes into `agents/` directory +- [ ] Replace `process.env` / `os.environ` with `context.env` / `ctx.env` +- [ ] Replace `req.headers.get('x')` with `context.request.headers['x']` (Node) or plain dict access (Python) +- [ ] Replace `await req.json()` with `context.request.body` (already parsed) +- [ ] Replace direct model API calls (OpenAI, Anthropic) with `AI_GATEWAY_*` env vars +- [ ] Add SSE streaming for AI endpoints (replace `res.json()` / `return {"data": ...}`) +- [ ] Add `makers-conversation-id` header to frontend fetch calls +- [ ] Wire platform tools through `context.tools` instead of custom tool implementations +- [ ] Wire conversation history through `context.store` instead of in-memory or custom DB +- [ ] If using web_search, set `WSA_API_KEY` env var and use `context.tools.get("web_search")` +- [ ] Set up `edgeone makers dev` for local development + +--- + +## §1. Standard API Route → Makers Handler + +This is the most common migration pattern. Applies to Express/Next.js API routes, plain HTTP handlers, etc. + +### Node (Express/Next.js → Makers) + +```typescript +// ❌ Before: Next.js API route (app/api/chat/route.ts) +export async function POST(req: Request) { + const body = await req.json(); + const headers = req.headers; + const apiKey = process.env.OPENAI_API_KEY; + // ... LLM call ... + return Response.json({ data: result }); +} + +// ✅ After: Makers agent handler (agents/chat/index.ts) +export async function onRequest(context: any) { + const body = context.request.body; // already parsed + const conversationId = context.conversation_id; // auto-injected from header + const env = context.env; // context.env, never process.env + // ... LLM call via AI_GATEWAY_* ... + return new Response(JSON.stringify({ data: result }), { + headers: { 'Content-Type': 'application/json' }, + }); +} +``` + +### Python (Flask/FastAPI → Makers) + +```python +# ❌ Before: Flask route +@app.route('/chat', methods=['POST']) +def chat(): + body = request.get_json() + api_key = os.environ.get('OPENAI_API_KEY') + # ... LLM call ... + return jsonify({'data': result}) + +# ✅ After: Makers agent handler (agents/chat/index.py) +async def handler(ctx): + body = ctx.request.body + conversation_id = ctx.conversation_id + api_key = ctx.env.get("AI_GATEWAY_API_KEY") + # ... LLM call via AI_GATEWAY_* ... + return {"data": result} +``` + +--- + +## §2. CrewAI (Python) + +### Key changes + +| Before | After | +|--------|-------| +| `os.environ.get("OPENAI_API_KEY")` | `ctx.env.get("AI_GATEWAY_API_KEY")` | +| `LLM(provider="openai", ...)` — LiteLLM dispatch | `LLM(provider="openai", base_url=ctx.env["AI_GATEWAY_BASE_URL"], ...)` — bypass LiteLLM | +| `memory=True` on Crew | `memory=False` + use `ctx.store` | +| `verbose=True` | `verbose=False` (events go through `crewai_event_bus`) | +| `crew.kickoff()` (blocking) | `await asyncio.to_thread(crew.kickoff)` | +| Custom search tools | Use `ctx.tools.to_crewai_tools(BaseTool)` | +| Flask/FastAPI handler | `async def handler(ctx):` → `ctx.utils.stream_sse(gen())` | + +### edgeone.json + +```json +{ + "buildCommand": "", + "outputDirectory": "", + "agents": { + "framework": "crewai" + } +} +``` + +### Requirements + +```txt +crewai>=1.14.5 +openai>=1.50.0 +``` + +### Migration steps + +1. Replace Flask/FastAPI entry with `async def handler(ctx):` +2. Read env from `ctx.env`, never `os.environ` +3. Use `LLM(provider="openai", api_key=ctx.env["AI_GATEWAY_API_KEY"], base_url=ctx.env["AI_GATEWAY_BASE_URL"])` +4. Set `Crew(memory=False, verbose=False)` +5. Wrap `crew.kickoff()` in `asyncio.to_thread()` +6. Replace custom tools with `ctx.tools.to_crewai_tools(BaseTool)` +7. Return SSE via `ctx.utils.stream_sse(gen())` + +> See [makers-agents/skills/python-frameworks/crewai.md](../skills/makers-agents/references/python-frameworks/crewai.md) for the complete pattern. +> Detailed before/after: [references/crewai-to-makers.md](references/crewai-to-makers.md) + +--- + +## §3. LangGraph / DeepAgents (Node + Python) + +### Key changes + +| Before | After | +|--------|-------| +| Direct model creation (`new ChatOpenAI(...)`) | Use `AI_GATEWAY_*` for apiKey/baseURL | +| `MemorySaver` (in-memory checkpointer) | `context.store.langgraphCheckpointer` (persistent) | +| Custom tool functions | `context.tools.toLangChainTools(tool)` | +| `agent.stream()` | SSE via `createSSEResponse(gen, signal)` (Node) or `ctx.utils.stream_sse(gen())` (Python) | +| `thread_id` manual management | `thread_id = context.conversation_id` | + +### Node — edgeone.json + +```json +{ + "agents": { + "framework": "langgraph" + } +} +``` + +### Python — edgeone.json + +```json +{ + "buildCommand": "", + "outputDirectory": "", + "agents": { + "framework": "langgraph" + } +} +``` + +### Migration steps + +1. Move handler into `agents//index.ts` (or `.py`) +2. Replace model initialization: use `AI_GATEWAY_API_KEY` + `AI_GATEWAY_BASE_URL` +3. Replace checkpointer: `context.store.langgraphCheckpointer` instead of `MemorySaver` +4. Replace store: `context.store.langgraphStore` +5. Replace tools: `context.tools.toLangChainTools(tool)` instead of custom tool functions +6. Set `thread_id`: `{ configurable: { thread_id: context.conversation_id } }` +7. Replace response with SSE streaming pattern + +> Node: [makers-agents/skills/node-frameworks/langgraph.md](../skills/makers-agents/references/node-frameworks/langgraph.md) +> Python: [makers-agents/skills/python-frameworks/langgraph.md](../skills/makers-agents/references/python-frameworks/langgraph.md) +> DeepAgents: [makers-agents/skills/node-frameworks/deepagents.md](../skills/makers-agents/references/node-frameworks/deepagents.md) +> Detailed before/after: [references/langgraph-to-makers.md](references/langgraph-to-makers.md), [references/deepagents-to-makers.md](references/deepagents-to-makers.md) + +--- + +## §4. OpenAI Agents SDK (Node + Python) + +### Key changes + +| Before | After | +|--------|-------| +| `new OpenAI({ apiKey, baseURL })` | Read `AI_GATEWAY_*` from `context.env` / `ctx.env` | +| `Runner.run(agent, input, { tools })` | Tools from `context.tools.all()` (already OpenAI function format) | +| Session management | `context.store.openaiSession(convId)` (Node) | +| Express route response | SSE via `createSSEResponse(gen, signal)` (Node) or `ctx.utils.stream_sse(gen())` (Python) | +| Model name hardcoded | `ctx.env.AI_GATEWAY_MODEL || DEFAULT_MODEL` | + +### Node — edgeone.json + +```json +{ + "agents": { + "framework": "openai-agents-sdk" + } +} +``` + +### Python — edgeone.json + +```json +{ + "buildCommand": "", + "outputDirectory": "", + "agents": { + "framework": "openai-agents-sdk" + } +} +``` + +### Migration steps + +1. Move handler into `agents//index.ts` (or `.py`) +2. Create OpenAI client from `context.env` (not `process.env`) +3. Replace tools with `context.tools.all()` (returns OpenAI function tools) +4. Use `context.store.openaiSession(conversationId)` for session (Node) +5. Map stream events to SSE: `output_text_delta` → `ai_response`, `tool_called` → `tool_call` + +> Node: [makers-agents/skills/node-frameworks/openai-agents.md](../skills/makers-agents/references/node-frameworks/openai-agents.md) +> Python: [makers-agents/skills/python-frameworks/openai-agents.md](../skills/makers-agents/references/python-frameworks/openai-agents.md) +> Detailed before/after: [references/openai-agents-to-makers.md](references/openai-agents-to-makers.md) + +--- + +## §5. Claude Agent SDK (Node + Python) + +### Key changes + +| Before | After | +|--------|-------| +| `ANTHROPIC_API_KEY` env var | Mapped from `AI_GATEWAY_*` via `collectGatewayEnv()` | +| `process.env` | `context.env` injected into `query().options.env` | +| Custom MCP tools | `context.tools.toClaudeMcpServer()` | +| Session | `context.store.claudeSessionStore()` (Node) | +| Stdout EPIPE crash | Swallow `EPIPE` on `process.stdout` (Node) | +| No writable config dir | Set `CLAUDE_CONFIG_DIR=/tmp/claude-agent-sdk`, `CLAUDE_CODE_TMPDIR=/tmp` | + +### Node — edgeone.json + +```json +{ + "agents": { + "framework": "claude-agent-sdk" + } +} +``` + +### Python — edgeone.json + +```json +{ + "buildCommand": "", + "outputDirectory": "", + "agents": { + "framework": "claude-agent-sdk" + } +} +``` + +### Migration steps + +1. Move handler into `agents//index.ts` (or `.py`) +2. Map `AI_GATEWAY_*` → `ANTHROPIC_*` via `collectGatewayEnv(context.env)` +3. Inject env into `query({ options: { env: collectGatewayEnv(...) } })` +4. Replace MCP tools: `context.tools.toClaudeMcpServer('edgeone', { alwaysLoad: true })` +5. Node only: swallow `EPIPE` on `process.stdout` +6. Set writable config dirs: `CLAUDE_CONFIG_DIR=/tmp/claude-agent-sdk`, `CLAUDE_CODE_TMPDIR=/tmp` + +> Node: [makers-agents/skills/node-frameworks/claude-sdk.md](../skills/makers-agents/references/node-frameworks/claude-sdk.md) +> Python: [makers-agents/skills/python-frameworks/claude-sdk.md](../skills/makers-agents/references/python-frameworks/claude-sdk.md) +> Detailed before/after: [references/claude-agent-sdk-to-makers.md](references/claude-agent-sdk-to-makers.md) + +--- + +## §6. Express / Next.js API Routes (Node) + +General migration for any Express-based or Next.js API route agent. + +### The 7-step conversion + +| Step | Before | After | +|------|--------|-------| +| 1. File location | `app/api/chat/route.ts` or `server/routes/chat.ts` | `agents/chat/index.ts` | +| 2. Entry signature | `export async function POST(req)` or `app.post('/chat', handler)` | `export async function onRequest(context)` | +| 3. Body parsing | `await req.json()` | `context.request.body` (already parsed) | +| 4. Headers | `req.headers.get('x-foo')` | `context.request.headers['x-foo']` | +| 5. Abort signal | `req.signal` | `context.request.signal` (AbortSignal) | +| 6. Model access | `process.env.OPENAI_API_KEY` → direct call | `context.env.AI_GATEWAY_*` → AI Gateway | +| 7. Response | `res.json()` or `return Response.json()` | SSE stream via `createSSEResponse(gen, signal)` | + +### Example: Next.js API route → Makers + +```typescript +// ❌ Before: Next.js (app/api/chat/route.ts) +import { NextRequest } from 'next/server'; +import OpenAI from 'openai'; + +export async function POST(req: NextRequest) { + const { message } = await req.json(); + const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + const response = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: message }], + stream: true, + }); + // ... stream back as Response +} + +// ✅ After: EdgeOne Makers (agents/chat/index.ts) +import { createLogger, sseEvent, createSSEResponse } from '../_shared'; + +export async function onRequest(context: any) { + const { message } = context.request.body ?? {}; + if (!message) return new Response('Missing message', { status: 400 }); + + const signal = context.request.signal as AbortSignal; + + return createSSEResponse(async function* (sig) { + const response = await fetch(context.env.AI_GATEWAY_BASE_URL + '/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${context.env.AI_GATEWAY_API_KEY}`, + }, + body: JSON.stringify({ + model: context.env.AI_GATEWAY_MODEL || '@makers/deepseek-v4-flash', + messages: [{ role: 'user', content: message }], + stream: true, + }), + signal: sig, + }); + // ... proxy SSE chunks ... + yield 'data: [DONE]\n\n'; + }, signal); +} +``` + +--- + +## §7. Client-Side Migration + +### Frontend fetch calls + +```typescript +// ❌ Before: plain fetch without conversation-id +const response = await fetch('/api/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message }), +}); + +// ✅ After: with makers-conversation-id header +const conversationId = getOrCreateConversationId(); // crypto.randomUUID() + localStorage +const response = await fetch('/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'makers-conversation-id': conversationId, // ⭐ required for all AI endpoints + }, + body: JSON.stringify({ message }), +}); +``` + +### /stop endpoint + +```typescript +// ✅ Always pass conversation_id in body for /stop +await fetch('/stop', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ conversation_id: conversationId }), +}); +``` + +### SSE parsing + +```typescript +const reader = response.body!.getReader(); +const decoder = new TextDecoder(); +let buffer = ''; + +while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') return; + try { + const event = JSON.parse(data); + if (event.type === 'ai_response') { /* display text */ } + if (event.type === 'tool_call') { /* show tool call */ } + if (event.type === 'ping') { /* ignore heartbeat */ } + } catch { /* skip non-JSON */ } + } + } +} +``` + +--- + +## §8. Post-Migration Verification + +After migration, verify these items before deploying: + +- [ ] `edgeone makers dev` starts without errors +- [ ] `/chat` endpoint returns SSE stream (not JSON) +- [ ] AI responses work end-to-end (frontend → agent → model → frontend) +- [ ] `context.env` is used everywhere (grep for `process.env` / `os.environ` — none should remain) +- [ ] `edgeone.json` has correct `agents.framework` +- [ ] Platform tools (`context.tools`) work in at least one framework +- [ ] Conversation history persists across requests (via `context.store`) +- [ ] `/stop` endpoint cancels active runs +- [ ] Frontend sends `makers-conversation-id` header + +--- + +## See Also + +- Agent development guide: [makers-agents/SKILL.md](../makers-agents/SKILL.md) +- Platform conventions: [makers-agents/references/platform/](../makers-agents/references/platform/) +- CLI commands: [makers-cli/SKILL.md](../makers-cli/SKILL.md) +- Deploy guide: [makers-deploy/SKILL.md](../makers-deploy/SKILL.md) + +### Detailed before/after reference files + +- [references/api-route-to-makers.md](references/api-route-to-makers.md) — generic API route → Makers handler (no-framework fallback; Makers supports framework-less `onRequest`/`handler`) +- [references/langgraph-to-makers.md](references/langgraph-to-makers.md) — LangGraph (Node + Python) +- [references/deepagents-to-makers.md](references/deepagents-to-makers.md) — DeepAgents (Node + Python) +- [references/openai-agents-to-makers.md](references/openai-agents-to-makers.md) — OpenAI Agents SDK (Node + Python) +- [references/claude-agent-sdk-to-makers.md](references/claude-agent-sdk-to-makers.md) — Claude Agent SDK (Node + Python) diff --git a/skills/makers-migration/references/api-route-to-makers.md b/skills/makers-migration/references/api-route-to-makers.md new file mode 100644 index 0000000..5cd5b30 --- /dev/null +++ b/skills/makers-migration/references/api-route-to-makers.md @@ -0,0 +1,188 @@ +# Migration: Generic API Route → EdgeOne Makers (no-framework path) + +> This is the **framework-less fallback**. Use it when your project does **not** use any of the five +> agent frameworks (LangGraph, DeepAgents, OpenAI Agents SDK, Claude Agent SDK, CrewAI) — e.g. a custom +> agent loop wrapped in Express, a Next.js API route, or a plain HTTP endpoint. + +EdgeOne Makers supports running an agent **without any agent framework**: you just export +`onRequest(context)` (Node) or `async def handler(ctx):` (Python) and the runtime routes +`agents//` → `POST /`. This file covers converting a generic HTTP server (Express / +Next.js / Flask / FastAPI) into that form. + +For framework-specific migrations, see the sibling files: +`langgraph-to-makers.md`, `deepagents-to-makers.md`, `openai-agents-to-makers.md`, +`claude-agent-sdk-to-makers.md`, `crewai-to-makers.md`. + +--- + +## Example 1: Next.js API Route (app router) + +### ❌ Before + +```typescript +// app/api/chat/route.ts +import { NextRequest } from 'next/server'; +import OpenAI from 'openai'; + +export async function POST(req: NextRequest) { + const { message } = await req.json(); + const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + + const stream = await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: message }], + stream: true, + }); + + const encoder = new TextEncoder(); + const readable = new ReadableStream({ + async start(controller) { + for await (const chunk of stream) { + const text = chunk.choices[0]?.delta?.content || ''; + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`)); + } + controller.enqueue(encoder.encode('data: [DONE]\n\n')); + controller.close(); + }, + }); + + return new Response(readable, { + headers: { 'Content-Type': 'text/event-stream' }, + }); +} +``` + +### ✅ After + +```typescript +// agents/chat/index.ts +import { createSSEResponse } from '../_shared'; + +export async function onRequest(context: any) { + const { message } = context.request.body ?? {}; + if (!message) return new Response('Missing message', { status: 400 }); + + const signal = context.request.signal as AbortSignal; + const env = context.env; + + return createSSEResponse(async function* (sig) { + const response = await fetch(env.AI_GATEWAY_BASE_URL + '/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${env.AI_GATEWAY_API_KEY}`, + }, + body: JSON.stringify({ + model: env.AI_GATEWAY_MODEL || '@makers/deepseek-v4-flash', + messages: [{ role: 'user', content: message }], + stream: true, + }), + signal: sig, + }); + + const reader = (response.body as ReadableStream).getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + for (const line of lines) { + if (!line.startsWith('data: ')) continue; + const payload = line.slice(6).trim(); + if (payload === '[DONE]') continue; + try { + const parsed = JSON.parse(payload); + const text = parsed.choices?.[0]?.delta?.content || ''; + if (text) yield sseEvent({ type: 'ai_response', content: text }); + } catch { /* skip */ } + } + } + yield 'data: [DONE]\n\n'; + }, signal); +} +``` + +--- + +## Example 2: Express Router + +### ❌ Before + +```typescript +// server/routes/agent.ts +import express from 'express'; +const router = express.Router(); + +router.post('/summarize', async (req, res) => { + const { text } = req.body; + const apiKey = process.env.OPENAI_API_KEY; + + const completion = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: `Summarize: ${text}` }], + }), + }); + + const data = await completion.json(); + res.json({ summary: data.choices[0].message.content }); +}); + +export default router; +``` + +### ✅ After + +```typescript +// agents/summarize/index.ts +export async function onRequest(context: any) { + const { text } = context.request.body ?? {}; + if (!text) return new Response('Missing text', { status: 400 }); + + const env = context.env; + const completion = await fetch(env.AI_GATEWAY_BASE_URL + '/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${env.AI_GATEWAY_API_KEY}`, + }, + body: JSON.stringify({ + model: env.AI_GATEWAY_MODEL || '@makers/deepseek-v4-flash', + messages: [{ role: 'user', content: `Summarize: ${text}` }], + }), + }); + + const data = await completion.json(); + return new Response(JSON.stringify({ + summary: data.choices?.[0]?.message?.content ?? '', + }), { headers: { 'Content-Type': 'application/json' } }); +} +``` + +--- + +## Conversion Checklist + +| Express/Next.js | Makers | +|-----------------|--------| +| `export async function POST(req)` | `export async function onRequest(context)` | +| `await req.json()` | `context.request.body` | +| `req.headers.get('x')` | `context.request.headers['x']` | +| `res.json(data)` | `new Response(JSON.stringify(data), { headers: {...} })` | +| `process.env.X` | `context.env.X` | +| `app.post('/summarize', ...)` | `agents/summarize/index.ts` (auto-routed) | +| `router.get('/api/x')` | `agents/x/index.ts` (path = `/x`) | +| Streaming via `ReadableStream` | `createSSEResponse(gen, signal)` | + +> No `edgeone.json` `agents.framework` is required for the no-framework path — the runtime serves +> `onRequest` / `handler` directly. Set `agents.framework` only when you use one of the five agent +> frameworks (and `buildCommand`/`outputDirectory` for a frontend build as needed). diff --git a/skills/makers-migration/references/claude-agent-sdk-to-makers.md b/skills/makers-migration/references/claude-agent-sdk-to-makers.md new file mode 100644 index 0000000..989d4a7 --- /dev/null +++ b/skills/makers-migration/references/claude-agent-sdk-to-makers.md @@ -0,0 +1,180 @@ +# Migration: Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) → EdgeOne Makers + +Claude Agent SDK runs a Claude Code subprocess, so the biggest differences are: (1) route the model through AI Gateway by mapping `AI_GATEWAY_*` → `ANTHROPIC_*`, (2) provide a writable config/temp dir, (3) wire platform tools via `toClaudeMcpServer`, and (4) bind session memory through `claudeSessionStore`. + +--- + +## Node + +### ❌ Before — native Claude Agent SDK (Express + ANTHROPIC_API_KEY) + +```typescript +// server.ts +import express from 'express'; +import { query, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'; +import { z } from 'zod'; + +const app = express(); +app.use(express.json()); + +// Custom MCP server with hand-written tools +const customMcp = createSdkMcpServer({ + name: 'custom-tools', + tools: [{ + name: 'get_weather', + description: 'Get weather', + inputSchema: { city: z.string() }, + handler: async ({ city }) => ({ content: [{ type: 'text', text: `Weather in ${city}` }] }), + }], +}); + +app.post('/chat', async (req, res) => { + const { message } = req.body; + const stream = query({ + prompt: message, + options: { + env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY }, // ⚠️ direct Anthropic + maxTurns: 30, + mcpServers: { 'custom-tools': customMcp }, + }, + }); + res.setHeader('Content-Type', 'text/event-stream'); + for await (const msg of stream) { + res.write(`data: ${JSON.stringify(msg)}\n\n`); // raw SDK messages + } + res.write('data: [DONE]\n\n'); + res.end(); +}); + +app.listen(3000); +``` + +### ✅ After — Makers agent handler + +```typescript +// agents/chat/index.ts +import { query, createSdkMcpServer, getSessionInfo } from '@anthropic-ai/claude-agent-sdk'; +import { z } from 'zod'; +import { createSSEResponse, sseEvent } from '../_shared'; +import { resolveModelName, collectGatewayEnv } from '../_model'; // see platform/node-entry.md §3 + +const sseQueue: string[] = []; // side channel for custom-tool events + +export async function onRequest(context: any) { + const ctxEnv = context.env ?? {}; + const body = context.request.body ?? {}; + const message = typeof body.message === 'string' ? body.message.trim() : ''; + if (!message) return new Response(JSON.stringify({ error: "'message' is required" }), { + status: 400, headers: { 'Content-Type': 'application/json' }, + }); + + const signal = context.request.signal; + const conversationId = context.conversation_id; + const store = context.store ?? null; + + // ⭐ Platform tools via toClaudeMcpServer → createSdkMcpServer + const edgeoneBundle = context.tools.toClaudeMcpServer('edgeone', { alwaysLoad: true }); + const edgeoneMcpServer = createSdkMcpServer(edgeoneBundle); + + // Optional custom tools push events into sseQueue + const customMcpServer = createSdkMcpServer({ + name: 'custom-tools', + alwaysLoad: true, + tools: [{ + name: 'get_weather', + description: 'Get weather', + inputSchema: { city: z.string() }, + handler: async ({ city }) => { + sseQueue.push(sseEvent({ type: 'tool_result', name: 'get_weather', content: `Weather in ${city}` })); + return { content: [{ type: 'text', text: `Weather in ${city}` }] }; + }, + }], + }); + + async function* run(sig?: AbortSignal): AsyncGenerator { + // ⭐ Session binding (resume vs new) + let sessionBinding: any = {}; + if (store && conversationId) { + try { + const info = await getSessionInfo(conversationId, { dir: process.cwd(), sessionStore: store.claudeSessionStore() }); + if (info) sessionBinding = { resume: conversationId }; + } catch { sessionBinding = { sessionId: conversationId }; } + } + + const stream = query({ + prompt: message, + options: { + model: resolveModelName(ctxEnv), + env: { + ...collectGatewayEnv(ctxEnv), // ⭐ AI_GATEWAY_* → ANTHROPIC_* + CLAUDE_CONFIG_DIR: '/tmp/claude-agent-sdk', // ⭐ writable config dir (required) + CLAUDE_CODE_TMPDIR: '/tmp', // ⭐ writable temp dir (required) + }, + maxTurns: 30, + mcpServers: { edgeone: edgeoneMcpServer, 'custom-tools': customMcpServer }, + allowedTools: edgeoneBundle.allowedTools, + ...sessionBinding, + abortController: sig ? { signal: sig } as any : undefined, + }, + }); + + for await (const msg of stream) { + if (sig?.aborted) break; + while (sseQueue.length) yield sseQueue.shift()!; // drain custom-tool events + // dispatch msg.type → ai_response / tool_call / file_output ... + } + while (sseQueue.length) yield sseQueue.shift()!; + yield 'data: [DONE]\n\n'; + } + + return createSSEResponse(run, signal); +} +``` + +`edgeone.json`: +```json +{ "agents": { "framework": "claude-agent-sdk" } } +``` + +> `@anthropic-ai/claude-agent-sdk` is auto-externalized — no `externalNodeModules` needed. + +### Required differences vs native + +1. **EPIPE guard** — add once at module load: + ```typescript + process.stdout.on('error', (err: any) => { if (err.code === 'EPIPE') return; }); + ``` +2. **Writable config/temp dirs** — the SDK subprocess needs writable `~/.claude` and temp; set `CLAUDE_CONFIG_DIR='/tmp/claude-agent-sdk'` and `CLAUDE_CODE_TMPDIR='/tmp'` in `options.env`. +3. **No `process.env`** — agent endpoints disable `process.env`; always use `context.env`. +4. **Tools** — `context.tools.toClaudeMcpServer('edgeone', { alwaysLoad: true })` returns `{ name, tools, allowedTools }`; pass the created server as `edgeone` and set `allowedTools`. +5. **Sandbox** — if you use `context.sandbox`, its `/tmp` is per-request and easily lost; keep a process-level file cache and re-upload every request. See [makers-agents capabilities/sandbox.md](../../makers-agents/references/capabilities/sandbox.md). + +--- + +## Python equivalent + +Native `claude_agent_sdk` (Python) uses `query()` / `ClaudeAgentOptions`. On Makers: + +- Entry `async def handler(ctx):` +- `ctx.env` → `collect_gateway_env()` → inject into `ClaudeAgentOptions(env=...)` +- `ctx.tools.to_claude_mcp_server('edgeone', always_load=True)` → `create_sdk_mcp_server(bundle)` +- Set `CLAUDE_CONFIG_DIR` / `CLAUDE_CODE_TMPDIR` in env +- Session via `ctx.store.claude_session_store()` +- Stream via `ctx.utils.stream_sse(gen())` +- `buildCommand: ""`, `outputDirectory: ""` + +See [makers-agents python-frameworks/claude-sdk.md](../../makers-agents/references/python-frameworks/claude-sdk.md). + +--- + +## Conversion Checklist + +| Native Claude Agent SDK | Makers | +|-------------------------|--------| +| `env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY }` | `env: { ...collectGatewayEnv(context.env), CLAUDE_CONFIG_DIR, CLAUDE_CODE_TMPDIR }` | +| `mcpServers: { custom }` only | Add `edgeone: createSdkMcpServer(context.tools.toClaudeMcpServer(...))` | +| No EPIPE guard | `process.stdout.on('error', ...)` swallow EPIPE | +| No config dir handling | Set `CLAUDE_CONFIG_DIR='/tmp/claude-agent-sdk'`, `CLAUDE_CODE_TMPDIR='/tmp'` | +| Local filesystem session | `context.store.claudeSessionStore()` + `getSessionInfo` resume/new | +| `app.post('/chat', ...)` + raw SDK messages | `export async function onRequest(context)` + `createSSEResponse(gen, signal)` + `sseQueue` side channel | +| `process.env.X` | `context.env.X` | diff --git a/skills/makers-migration/references/crewai-to-makers.md b/skills/makers-migration/references/crewai-to-makers.md new file mode 100644 index 0000000..29441c4 --- /dev/null +++ b/skills/makers-migration/references/crewai-to-makers.md @@ -0,0 +1,135 @@ +# Migration: CrewAI (Python-only) → EdgeOne Makers + +CrewAI has no JS SDK, so this route is Python-only. The migration is about: routing the LLM through AI Gateway (`provider="openai"` to bypass LiteLLM), dropping `memory=True`/`verbose=True` for platform conventions, wrapping `crew.kickoff()` in a thread, and bridging the event_bus to Makers SSE. + +--- + +## ❌ Before — native CrewAI (Flask + direct OpenAI + LiteLLM) + +```python +# app.py +from flask import Flask, request, jsonify, Response +import json +from crewai import Agent, Crew, Process, Task, LLM + +app = Flask(__name__) + +# 1. LiteLLM dispatch by default (CrewAI 1.14+) — needs litellm installed +llm = LLM(model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"]) + +researcher = Agent(role="Researcher", goal="Answer questions", backstory="...", llm=llm, verbose=True) +task = Task(description="Answer: {q}", expected_output="answer", agent=researcher) + +crew = Crew(agents=[researcher], tasks=[task], process=Process.sequential, memory=True, verbose=True) + +@app.post("/chat") +def chat(): + q = request.json["q"] + result = crew.kickoff(inputs={"q": q}) # 2. blocking call, no SSE + return jsonify({"answer": str(result)}) +``` + +### Problems with the native version +- `LLM(model="gpt-4o", api_key=os.environ[...])` → LiteLLM dispatch (absent on Makers image → crash) +- `crew.kickoff()` is blocking → event loop stalls, no streaming +- `verbose=True` / `memory=True` → stdout noise + double memory +- Flask server → must become a Makers `handler(ctx)` + +--- + +## ✅ After — Makers agent handler + +```python +# agents/chat/index.py +import asyncio +import json +from typing import AsyncIterator + +DEFAULT_MODEL = "@makers/deepseek-v4-flash" + +def get_llm(env: dict): + from crewai import LLM + return LLM( + model=env.get("AI_GATEWAY_MODEL", DEFAULT_MODEL), + provider="openai", # ⭐ bypass LiteLLM (not bundled on platform) + api_key=env["AI_GATEWAY_API_KEY"], + base_url=env["AI_GATEWAY_BASE_URL"], + temperature=0.3, + timeout=300, + stream=True, + ) + +def build_crew(llm): + from crewai import Agent, Crew, Process, Task + researcher = Agent(role="Researcher", goal="Answer the user's question", + backstory="You are a helpful researcher.", llm=llm, verbose=False) + task = Task(description="Answer the user's question: {message}", + expected_output="A clear, concise answer", agent=researcher) + return Crew(agents=[researcher], tasks=[task], + process=Process.sequential, + memory=False, # ⭐ use ctx.store instead + verbose=False) # ⭐ events flow through event_bus + +async def event_stream(ctx, message) -> AsyncIterator[bytes]: + env = ctx.env # ⭐ context.env, never os.environ + crew = build_crew(get_llm(env)) + # ⭐ crew.kickoff is blocking → offload to a thread + task = asyncio.create_task(asyncio.to_thread(crew.kickoff, inputs={"message": message})) + while True: + if ctx.request.signal.is_set(): # ⭐ Python: .is_set(), not .aborted + break + done, _ = await asyncio.wait( + {task, asyncio.create_task(_queue_get())}, + return_when=asyncio.FIRST_COMPLETED, timeout=5, + ) + yield ctx.utils.sse({"type": "ping", "ts": int(asyncio.get_event_loop().time() * 1000)}) + for d in done: + if d is task: + final = d.result() + yield ctx.utils.sse({"type": "ai_response", "content": str(final)[:2000]}) + yield b"data: [DONE]\n\n" + return + +async def _queue_get(): + # placeholder for event_bus queue; extend with CrewProgressBridge if streaming tokens + await asyncio.sleep(3600) + +async def handler(ctx): + body = getattr(getattr(ctx, "request", None), "body", None) or {} + message = (body.get("message") or "").strip() + if not message: + return {"status_code": 400, "body": {"error": "Missing message"}} + return ctx.utils.stream_sse(event_stream(ctx, message)) +``` + +`edgeone.json`: +```json +{ + "buildCommand": "", + "outputDirectory": "", + "agents": { "framework": "crewai" } +} +``` + +`requirements.txt`: +```txt +crewai>=1.14.5 +openai>=1.50.0 +``` + +> For tool streaming, subscribe to `crewai_event_bus` `LLMStreamChunkEvent` → `ai_response` and `TaskCompletedEvent` → `tool_result` (see [makers-agents python-frameworks/crewai.md](../../makers-agents/references/python-frameworks/crewai.md) §6). For platform tools use `ctx.tools.to_crewai_tools(BaseTool)`. + +--- + +## Conversion Checklist + +| Native CrewAI | Makers | +|---------------|--------| +| `LLM(model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"])` | `LLM(model=env AI_GATEWAY_MODEL, provider="openai", api_key=ctx.env["AI_GATEWAY_API_KEY"], base_url=ctx.env["AI_GATEWAY_BASE_URL"])` | +| LiteLLM default dispatch | `provider="openai"` (mandatory — platform has no LiteLLM) | +| `crew.kickoff()` blocking | `asyncio.to_thread(crew.kickoff, ...)` | +| `verbose=True` | `verbose=False` | +| `memory=True` | `memory=False` + `ctx.store` | +| Flask `@app.post` + `jsonify` | `async def handler(ctx):` + `ctx.utils.stream_sse(gen())` | +| `os.environ` | `ctx.env` | +| No SSE | `ctx.utils.sse({type,...})` + `data: [DONE]\n\n` + `ping` heartbeat | diff --git a/skills/makers-migration/references/deepagents-to-makers.md b/skills/makers-migration/references/deepagents-to-makers.md new file mode 100644 index 0000000..c95ff40 --- /dev/null +++ b/skills/makers-migration/references/deepagents-to-makers.md @@ -0,0 +1,152 @@ +# Migration: DeepAgents (Node + Python) → EdgeOne Makers + +DeepAgents is a thin layer over LangGraph, so the migration is almost identical to LangGraph: swap the model endpoint, drop custom tools for `context.tools`, and replace the HTTP server with an `onRequest` handler. Memory reuses the LangGraph adapters. + +--- + +## Node + +### ❌ Before — native DeepAgents (Express + direct OpenAI) + +```typescript +// server.ts +import express from 'express'; +import { ChatOpenAI } from '@langchain/openai'; +import { createDeepAgent } from 'deepagents'; +import { tool } from '@langchain/core/tools'; +import { z } from 'zod'; + +const app = express(); +app.use(express.json()); + +const model = new ChatOpenAI({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o' }); + +// Custom tool implemented by hand +const internetSearch = tool( + async ({ query }: { query: string }) => `results for ${query}`, + { name: 'internet_search', schema: z.object({ query: z.string() }) }, +); + +const agent = createDeepAgent({ + model, + systemPrompt: 'You are a helpful research assistant.', + tools: [internetSearch], + maxTurns: 30, +}); + +app.post('/chat', async (req, res) => { + const { message } = req.body; + const stream = await agent.stream( + { messages: [{ role: 'user', content: message }] }, + { streamMode: 'messages' }, + ); + res.setHeader('Content-Type', 'text/event-stream'); + for await (const chunk of stream) { + res.write(`data: ${JSON.stringify(chunk)}\n\n`); + } + res.write('data: [DONE]\n\n'); + res.end(); +}); + +app.listen(3000); +``` + +### ✅ After — Makers agent handler + +```typescript +// agents/chat/index.ts +import { ChatOpenAI } from '@langchain/openai'; +import { createDeepAgent } from 'deepagents'; +import { tool } from '@langchain/core/tools'; +import { createSSEResponse, sseEvent } from '../_shared'; + +const MODEL_NAME = '@makers/deepseek-v4-flash'; + +function getModel(env: Record) { + return new ChatOpenAI({ + model: MODEL_NAME, + apiKey: env.AI_GATEWAY_API_KEY, // ⭐ AI Gateway + configuration: { baseURL: env.AI_GATEWAY_BASE_URL }, + temperature: 0, + timeout: 300_000, + }); +} + +let _agent: any = null; +function getAgent(model: any) { + if (_agent) return _agent; + _agent = createDeepAgent({ + model, + systemPrompt: 'You are a helpful research assistant.', + tools: [], // ⭐ platform tools injected automatically via context.tools + maxTurns: 30, + }); + return _agent; +} + +export async function onRequest(context: any) { + const { request, env, conversation_id: conversationId } = context; + const { message } = request?.body ?? {}; + if (!message) return new Response('Missing message', { status: 400 }); + + const signal = request?.signal as AbortSignal | undefined; + const agent = getAgent(getModel(env)); + + // ⭐ Platform tools auto-wired; no hand-written tool functions needed. + // If you need custom tools, wrap them with context.tools.toLangChainTools(tool). + + return createSSEResponse(async function* (sig) { + const stream = await agent.stream( + { messages: [{ role: 'user', content: message }] }, + { streamMode: 'messages', signal: sig, configurable: { thread_id: conversationId } }, + ); + for await (const chunk of stream) { + if (sig?.aborted) break; + const [msg] = chunk as any[]; + if (msg.tool_call_chunks?.length) { + for (const tc of msg.tool_call_chunks) if (tc.name) yield sseEvent({ type: 'tool_call', name: tc.name }); + } else if (msg.type === 'tool') { + yield sseEvent({ type: 'tool_result', name: msg.name, content: msg.text?.slice(0, 500) ?? '' }); + } else if (msg.text) { + yield sseEvent({ type: 'ai_response', content: msg.text }); + } + } + yield 'data: [DONE]\n\n'; + }, signal); +} +``` + +`edgeone.json`: +```json +{ "agents": { "framework": "deepagents" } } +``` + +> `deepagents` and all `@langchain/*` packages are auto-externalized — no `externalNodeModules` needed. + +--- + +## Python equivalent + +Same `createDeepAgent({ model, systemPrompt, tools, maxTurns })` API. On Makers: + +- Entry `async def handler(ctx):` +- `ctx.env["AI_GATEWAY_API_KEY"]` / `ctx.env["AI_GATEWAY_BASE_URL"]` +- Platform tools auto-injected; custom tools via `ctx.tools.toLangChainTools(tool)` +- Memory via `ctx.store.langgraphCheckpointer` / `ctx.store.langgraphStore` +- Stream via `ctx.utils.stream_sse(gen())` +- `buildCommand: ""`, `outputDirectory: ""` + +See [makers-agents python-frameworks/deepagents.md](../../makers-agents/references/python-frameworks/deepagents.md). + +--- + +## Conversion Checklist + +| Native DeepAgents | Makers | +|-------------------|--------| +| `new ChatOpenAI({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o' })` | `new ChatOpenAI({ apiKey: env.AI_GATEWAY_API_KEY, configuration: { baseURL: env.AI_GATEWAY_BASE_URL }, model: '@makers/...' })` | +| Hand-written `tool()` functions | `context.tools.toLangChainTools(tool)` (or leave `tools: []` for auto-injected platform tools) | +| `app.post('/chat', ...)` + manual SSE | `export async function onRequest(context)` + `createSSEResponse(gen, signal)` | +| No persistent memory | `context.store.langgraphCheckpointer` / `context.store.langgraphStore` (optional) | +| `process.env.X` | `context.env.X` | +| `streamMode: 'messages'` (no thread) | `configurable: { thread_id: context.conversation_id }` | diff --git a/skills/makers-migration/references/langgraph-to-makers.md b/skills/makers-migration/references/langgraph-to-makers.md new file mode 100644 index 0000000..9ec8b92 --- /dev/null +++ b/skills/makers-migration/references/langgraph-to-makers.md @@ -0,0 +1,179 @@ +# Migration: LangGraph (Node + Python) → EdgeOne Makers + +LangGraph's graph/state API is unchanged on Makers — you only swap three things: model endpoint, checkpointer/store backend, and the HTTP server. This file shows the native format and the exact changes. + +--- + +## Node + +### ❌ Before — native LangGraph (Express + in-memory state) + +```typescript +// server.ts +import express from 'express'; +import { ChatOpenAI } from '@langchain/openai'; +import { StateGraph, MessagesAnnotation, START, END } from '@langchain/langgraph'; +import { ToolNode } from '@langchain/langgraph/prebuilt'; +import { MemorySaver } from '@langchain/langgraph'; +import { tool } from '@langchain/core/tools'; +import { z } from 'zod'; + +const app = express(); +app.use(express.json()); + +// 1. Model points directly at OpenAI +const model = new ChatOpenAI({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o' }); + +// 2. Custom tool implemented by hand +const getWeather = tool( + async ({ city }: { city: string }) => `Weather in ${city} is sunny`, + { name: 'get_weather', schema: z.object({ city: z.string() }) }, +); + +// 3. In-memory checkpointer — lost on restart, not shared across instances +const checkpointer = new MemorySaver(); + +function buildGraph(model: any, tools: any[]) { + const modelWithTools = model.bindTools(tools); + const toolNode = new ToolNode(tools); + async function agentNode(state: typeof MessagesAnnotation.State) { + return { messages: [await modelWithTools.invoke(state.messages)] }; + } + function shouldContinue(state: typeof MessagesAnnotation.State): 'tools' | '__end__' { + const last = state.messages[state.messages.length - 1] as any; + return last.tool_calls?.length ? 'tools' : '__end__'; + } + return new StateGraph(MessagesAnnotation) + .addNode('agent', agentNode) + .addNode('tools', toolNode) + .addEdge(START, 'agent') + .addConditionalEdges('agent', shouldContinue) + .addEdge('tools', 'agent') + .compile({ checkpointer }); +} + +const graph = buildGraph(model, [getWeather]); + +// 4. Express server builds SSE by hand +app.post('/chat', async (req, res) => { + const { message, threadId } = req.body; + const stream = await graph.stream( + { messages: [{ role: 'user', content: message }] }, + { streamMode: 'messages', configurable: { thread_id: threadId } }, + ); + res.setHeader('Content-Type', 'text/event-stream'); + for await (const chunk of stream) { + // manual JSON.stringify of each delta... + res.write(`data: ${JSON.stringify(chunk)}\n\n`); + } + res.write('data: [DONE]\n\n'); + res.end(); +}); + +app.listen(3000); +``` + +### ✅ After — Makers agent handler + +```typescript +// agents/chat/index.ts +import { StateGraph, MessagesAnnotation, START, END } from '@langchain/langgraph'; +import { ToolNode } from '@langchain/langgraph/prebuilt'; +import { ChatOpenAI } from '@langchain/openai'; +import { tool } from '@langchain/core/tools'; +import { z } from 'zod'; +import { createSSEResponse, sseEvent } from '../_shared'; + +const MODEL_NAME = '@makers/deepseek-v4-flash'; + +function getModel(env: Record) { + return new ChatOpenAI({ + model: MODEL_NAME, + apiKey: env.AI_GATEWAY_API_KEY, // ⭐ AI Gateway, not OPENAI_API_KEY + configuration: { baseURL: env.AI_GATEWAY_BASE_URL }, + temperature: 0, + timeout: 300_000, + }); +} + +export async function onRequest(context: any) { + const { request, env, conversation_id: conversationId, store } = context; + const { message } = request?.body ?? {}; + if (!message) return new Response('Missing message', { status: 400 }); + + const signal = request?.signal as AbortSignal | undefined; + + const model = getModel(env); + + // ⭐ Platform tools become real StructuredTool instances + const tools = context.tools.toLangChainTools(tool); + + // ⭐ Persistent checkpointer/store from context.store (no MemorySaver) + const graph = new StateGraph(MessagesAnnotation) + .addNode('agent', async (state) => ({ messages: [await model.bindTools(tools).invoke(state.messages)] })) + .addNode('tools', new ToolNode(tools)) + .addEdge(START, 'agent') + .addConditionalEdges('agent', (s) => + ((s.messages[s.messages.length - 1] as any).tool_calls?.length) ? 'tools' : '__end__') + .addEdge('tools', 'agent') + .compile({ + checkpointer: store.langgraphCheckpointer, // ⭐ persistent + store: store.langgraphStore, // ⭐ long-term KV + }); + + return createSSEResponse(async function* (sig) { + const stream = await graph.stream( + { messages: [{ role: 'user', content: message }] }, + { streamMode: 'messages', signal: sig, configurable: { thread_id: conversationId } }, + ); + for await (const chunk of stream) { + if (sig?.aborted) break; + const [msg] = chunk as any[]; + if (msg.tool_call_chunks?.length) { + for (const tc of msg.tool_call_chunks) if (tc.name) yield sseEvent({ type: 'tool_call', name: tc.name }); + } else if (msg.type === 'tool') { + yield sseEvent({ type: 'tool_result', name: msg.name, content: msg.text?.slice(0, 500) ?? '' }); + } else if (msg.text) { + yield sseEvent({ type: 'ai_response', content: msg.text }); + } + } + yield 'data: [DONE]\n\n'; + }, signal); +} +``` + +`edgeone.json`: +```json +{ "agents": { "framework": "langgraph" } } +``` + +> All `@langchain/*` packages are auto-externalized — no `externalNodeModules` needed. + +--- + +## Python equivalent + +Native LangGraph-Python uses the same `StateGraph`/`MemorySaver`/`tool` API. On Makers: + +- Entry becomes `async def handler(ctx):` (file `agents/chat/index.py`) +- `ctx.env["AI_GATEWAY_API_KEY"]` / `ctx.env["AI_GATEWAY_BASE_URL"]` instead of `os.environ` +- `ctx.tools.toLangChainTools(tool)` for tools +- `ctx.store.langgraphCheckpointer` / `ctx.store.langgraphStore` instead of `MemorySaver` +- Stream via `ctx.utils.stream_sse(gen())` +- `buildCommand: ""`, `outputDirectory: ""` in `edgeone.json` + +See [makers-agents python-frameworks/langgraph.md](../../makers-agents/references/python-frameworks/langgraph.md). + +--- + +## Conversion Checklist + +| Native LangGraph | Makers | +|------------------|--------| +| `new ChatOpenAI({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o' })` | `new ChatOpenAI({ apiKey: env.AI_GATEWAY_API_KEY, configuration: { baseURL: env.AI_GATEWAY_BASE_URL }, model: '@makers/...' })` | +| `new MemorySaver()` | `store.langgraphCheckpointer` + `store.langgraphStore` | +| Custom `@tool()` functions | `context.tools.toLangChainTools(tool)` | +| `app.post('/chat', ...)` + manual SSE | `export async function onRequest(context)` + `createSSEResponse(gen, signal)` | +| `thread_id` from request body | `context.conversation_id` | +| `process.env.X` | `context.env.X` | +| `express.json()` body parsing | `context.request.body` (already parsed) | diff --git a/skills/makers-migration/references/openai-agents-to-makers.md b/skills/makers-migration/references/openai-agents-to-makers.md new file mode 100644 index 0000000..41da189 --- /dev/null +++ b/skills/makers-migration/references/openai-agents-to-makers.md @@ -0,0 +1,161 @@ +# Migration: OpenAI Agents SDK (`@openai/agents`) → EdgeOne Makers + +OpenAI Agents SDK has no special server format — it runs anywhere Node runs. The migration is about: routing the model through AI Gateway, replacing hand-built tools with `context.tools.all()`, swapping hand-managed history for `openaiSession`, and mapping SDK stream events to Makers SSE. + +--- + +## Node + +### ❌ Before — native OpenAI Agents SDK (Express + direct OpenAI) + +```typescript +// server.ts +import express from 'express'; +import OpenAI from 'openai'; +import { Agent, run } from '@openai/agents'; +import { z } from 'zod'; + +const app = express(); +app.use(express.json()); + +const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + +const agent = new Agent({ + name: 'Assistant', + instructions: 'You are a helpful assistant.', + model: 'gpt-4o', + tools: [ + { + name: 'get_weather', + description: 'Get weather', + parameters: z.object({ city: z.string() }), + execute: async ({ city }) => `Weather in ${city} is sunny`, + }, + ], +}); + +app.post('/chat', async (req, res) => { + const { message, history } = req.body; // ⚠️ caller must maintain history + const result = await run(agent, message, { stream: true }); + res.setHeader('Content-Type', 'text/event-stream'); + for await (const ev of result.toStream()) { + res.write(`data: ${JSON.stringify(ev)}\n\n`); // raw SDK events, not Makers SSE + } + res.write('data: [DONE]\n\n'); + res.end(); +}); + +app.listen(3000); +``` + +### ✅ After — Makers agent handler + +```typescript +// agents/chat/index.ts +import OpenAI from 'openai'; +import { Agent, run, OpenAIChatCompletionsModel, type Session } from '@openai/agents'; +import { createSSEResponse, sseEvent } from '../_shared'; + +const DEFAULT_MODEL = '@makers/hy3-preview'; + +export async function onRequest(context: any) { + const message = (context.request.body ?? {}).message as string | undefined; + if (!message) return new Response(JSON.stringify({ error: "'message' is required" }), { + status: 400, headers: { 'Content-Type': 'application/json' }, + }); + + const signal = context.request.signal as AbortSignal | undefined; + const env = (context.env ?? {}) as Record; + + // ⭐ OpenAI-compatible client → AI Gateway + const client = new OpenAI({ apiKey: env.AI_GATEWAY_API_KEY, baseURL: env.AI_GATEWAY_BASE_URL }); + const model = new OpenAIChatCompletionsModel(client, env.AI_GATEWAY_MODEL ?? DEFAULT_MODEL); + + // ⭐ context.tools.all() returns OpenAI function tools directly + const agent = new Agent({ + name: 'Assistant', + instructions: 'You are a helpful assistant.', + tools: context.tools.all(), + model, + }); + + // ⭐ Session auto-prepends history — no hand-managed messages array + const session: Session | undefined = + context.store && context.conversation_id + ? context.store.openaiSession(context.conversation_id) + : undefined; + + return createSSEResponse(async function* () { + try { + const result = await run(agent, message, { stream: true, signal, session }); + for await (const ev of result.toStream()) { + if (signal?.aborted) break; + const sse = toSseEvent(ev); + if (sse) yield sseEvent({ type: sse.event, ...sse.data }); + } + } catch (e) { + const err = e as Error; + if (err.name !== 'AbortError' && !signal?.aborted) { + yield sseEvent({ type: 'error_message', content: err.message }); + } + } + yield 'data: [DONE]\n\n'; + }, signal); +} + +// ⭐ Map SDK stream events to Makers SSE protocol +function toSseEvent(e: any) { + if (e.type === 'raw_model_stream_event' && e.data?.type === 'output_text_delta') { + return { event: 'ai_response', data: { content: e.data.delta as string } }; + } + if (e.type === 'run_item_stream_event' && e.name === 'tool_called') { + const n = e.item?.name ?? e.item?.rawItem?.name; + if (n) return { event: 'tool_call', data: { name: n } }; + } + if (e.type === 'run_item_stream_event' && e.name === 'tool_output') { + const n = e.item?.name ?? e.item?.rawItem?.name; + const out = e.item?.output ?? e.item?.rawItem?.output; + return { event: 'tool_result', data: { name: n, content: typeof out === 'string' ? out.slice(0, 500) : JSON.stringify(out).slice(0, 500) } }; + } + if (e.type === 'agent_updated_stream_event') { + return { event: 'tool_call', data: { name: `handoff:${e.agent?.name}` } }; + } + return null; +} +``` + +`edgeone.json`: +```json +{ "agents": { "framework": "openai-agents-sdk" } } +``` + +> Unlike `deepagents` / `@langchain/*` / `claude-agent-sdk`, `@openai/agents` and `openai` are **not** auto-externalized. If you hit `Dynamic require` / `Cannot find module` build errors, add `"externalNodeModules": ["openai", "@openai/agents"]` to the `agents` config. + +--- + +## Python equivalent + +Native `@openai/agents` (Python) uses `Agent` + `Runner.run()`. On Makers: + +- Entry `async def handler(ctx):` +- `ctx.env["AI_GATEWAY_API_KEY"]` / `ctx.env["AI_GATEWAY_BASE_URL"]` +- `ctx.tools.all()` for tools +- `ctx.store.openai_session(conversation_id)` — note Python method name +- Stream via `ctx.utils.stream_sse(gen())`, mapping `output_text_delta` → `ai_response`, `tool_called` → `tool_call` +- `buildCommand: ""`, `outputDirectory: ""` + +See [makers-agents python-frameworks/openai-agents.md](../../makers-agents/references/python-frameworks/openai-agents.md). + +--- + +## Conversion Checklist + +| Native OpenAI Agents SDK | Makers | +|--------------------------|--------| +| `new OpenAI({ apiKey: process.env.OPENAI_API_KEY })` | `new OpenAI({ apiKey: env.AI_GATEWAY_API_KEY, baseURL: env.AI_GATEWAY_BASE_URL })` | +| `model: 'gpt-4o'` | `new OpenAIChatCompletionsModel(client, env.AI_GATEWAY_MODEL ?? '@makers/...')` | +| Hand-written `tools: [{ name, parameters, execute }]` | `context.tools.all()` (OpenAI function format) | +| Caller maintains `history` array | `context.store.openaiSession(conversation_id)` auto-prepend | +| Raw SDK events over SSE | Map `output_text_delta` → `ai_response`, `tool_called` → `tool_call`, `tool_output` → `tool_result` | +| `app.post('/chat', ...)` | `export async function onRequest(context)` + `createSSEResponse(gen, signal)` | +| `process.env.X` | `context.env.X` |