Rork لا يُركب مباشرة كـOpenAI Base URL داخل Chatwoot Captain، لأن:
/v1/chat/completions على Rork.{completion} وليست choices.tool-input-* events، لا choices[].delta.tool_calls.الحل الصحيح هو Internal Adapter/Gateway.
Chatwoot Captain
|
| OpenAI-compatible requests
v
Rork Captain Adapter
- Authentication
- Tenant isolation
- OpenAI <-> Rork mapping
- Stream translation
- Tool-call state
- Validation
- Timeouts/retries
- Logs/metrics
- Fallback provider
|
v
Rork /agent/chat
الحد الأدنى:
POST /v1/chat/completions
GET /v1/models
GET /health
اختياري حسب إعداد Captain:
POST /v1/embeddings
لا توجه /v1/embeddings إلى Rork؛ استخدم مزود Embeddings منفصلًا.
POST https://toolkit.rork.com/agent/chat
استخدم /llm/text فقط لمسارات Text بسيطة لا تحتاج Tools أو streaming حقيقي.
Input من Chatwoot:
{
"role": "user",
"content": "Where is order ORD-42?"
}
Rork UIMessage:
{
"id": "cw-msg-123",
"role": "user",
"parts": [
{
"type": "text",
"text": "Where is order ORD-42?"
}
]
}
{
"role": "assistant",
"content": "I will check the order."
}
Rork:
{
"id": "assistant-123",
"role": "assistant",
"parts": [
{
"type": "text",
"text": "I will check the order."
}
]
}
Rork Agent يقبل system role ضمن model messages في الاختبارات، لكن UI message transport يجب اختباره مع نسخة الـSDK المستخدمة. أبسط تصميم للـAdapter:
{
"type": "function",
"function": {
"name": "lookup_order",
"description": "Look up an order by ID",
"parameters": {
"type": "object",
"properties": {
"orderId": {"type": "string"}
},
"required": ["orderId"],
"additionalProperties": false
}
}
}
{
"lookup_order": {
"description": "Look up an order by ID",
"jsonSchema": {
"type": "object",
"properties": {
"orderId": {"type": "string"}
},
"required": ["orderId"],
"additionalProperties": false
}
}
}
Mapping:
function openAIToolsToRork(tools: OpenAITool[]) {
return Object.fromEntries(
tools
.filter(tool => tool.type === "function")
.map(tool => [
tool.function.name,
{
description: tool.function.description ?? "",
jsonSchema: tool.function.parameters
}
])
);
}
قبل الإرسال:
Rork event:
{
"type": "tool-input-available",
"toolCallId": "call_123",
"toolName": "lookup_order",
"input": {
"orderId": "ORD-42"
}
}
OpenAI-compatible non-stream response part:
{
"id": "chatcmpl-rork-123",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "lookup_order",
"arguments": "{\"orderId\":\"ORD-42\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}
OpenAI stream chunk:
{
"choices": [
{
"index": 0,
"delta": {
"tool_calls": [
{
"index": 0,
"id": "call_123",
"type": "function",
"function": {
"name": "lookup_order",
"arguments": "{\"orderId\":\"ORD-42\"}"
}
}
]
},
"finish_reason": null
}
]
}
لا ترسل OpenAI tool call قبل اكتمال tool-input-available إلا إذا نفذت parser مضبوطًا لتجميع tool-input-delta.
Rork tool event
|
Adapter maps to OpenAI tool_call
|
Chatwoot executes Captain custom tool
|
Chatwoot sends role=tool result
|
Adapter maps result back to Rork
هذه الطريقة مفضلة عندما تريد الاستفادة من:
Rork tool event
|
Adapter validates and executes tool
|
Adapter appends Rork output-available
|
Rork returns final text
|
Adapter sends only final answer to Chatwoot
هذه الطريقة مفيدة عندما تكون الأدوات داخلية عندك أو عندما لا يدعم مسار Captain المستخدم tool round-trip بالشكل المطلوب.
ابدأ بـA إن كانت نسخة Chatwoot الحالية عندك تدعم tool calls مع custom endpoint بشكل كامل. استخدم B للأدوات الخاصة بالـERP/OpenWA أو عندما تحتاج تحكمًا أدق في التنفيذ.
Chatwoot/OpenAI message:
{
"role": "tool",
"tool_call_id": "call_123",
"content": "{\"status\":\"shipped\",\"trackingNumber\":\"TRK-9001\"}"
}
الـAdapter يحتاج state محفوظًا من الطلب السابق:
{
"toolCallId": "call_123",
"toolName": "lookup_order",
"input": {
"orderId": "ORD-42"
}
}
ثم يبني Rork assistant tool part:
{
"id": "assistant-tool-result-123",
"role": "assistant",
"parts": [
{
"type": "tool-lookup_order",
"toolCallId": "call_123",
"state": "output-available",
"input": {
"orderId": "ORD-42"
},
"output": {
"status": "shipped",
"trackingNumber": "TRK-9001"
}
}
]
}
ثم يعيد إرسال history إلى /agent/chat للحصول على final answer.
Rork:
{
"type": "text-delta",
"id": "gen-1",
"delta": "Hello"
}
OpenAI SSE:
data: {"id":"chatcmpl-rork-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
Rork finish:
{
"type": "finish",
"finishReason": "stop"
}
OpenAI finish:
data: {"id":"chatcmpl-rork-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Key مقترح:
chatwoot:{account_id}:{conversation_id}
البيانات:
{
"rorkMessages": [],
"pendingToolCalls": {},
"lastRequestId": "...",
"updatedAt": "..."
}
استخدم Redis أو storage مشترك؛ لا تعتمد على memory داخل process واحد.
| المرحلة | اقتراح مبدئي |
|---|---|
| Rork TTFB | 5s |
| Text completion | 30s |
| Tool execution | 10–20s حسب الأداة |
| Image operations | 30–60s |
| Total Captain turn | حد مناسب لتجربة Chatwoot |
لم يُكتشف Rork embeddings endpoint. افصل المسار:
Captain chat completion -> Rork Adapter
Knowledge embeddings -> OpenAI/other embedding provider
لا تحاول تزييف /v1/embeddings باستخدام Text Generation.
X-Chatwoot-Account-Id
X-Chatwoot-Conversation-Id
X-Chatwoot-Inbox-Id
X-Request-Id
تحقق منها داخل شبكة موثوقة فقط، ولا تقبلها كهوية من الإنترنت دون توقيع أو auth.
toolCallId.