From 0554be382a59273abdfd4770ad52f9eb5fcf79b9 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Mon, 11 Aug 2025 16:29:10 +1200 Subject: [PATCH] [Docs] Add AI Chat API documentation and endpoint metadata --- apps/portal/src/app/Header.tsx | 4 + .../src/app/ai/chat/EndpointMetadata.tsx | 91 +++++++++++ .../app/ai/chat/handling-responses/page.mdx | 145 +++++++++++++++++ apps/portal/src/app/ai/chat/page.mdx | 53 +++++++ apps/portal/src/app/ai/layout.tsx | 23 +-- apps/portal/src/app/ai/sidebar.tsx | 38 +++++ .../Document/APIEndpointMeta/ApiEndpoint.tsx | 147 ++++++++---------- .../Document/APIEndpointMeta/common.tsx | 9 ++ 8 files changed, 412 insertions(+), 98 deletions(-) create mode 100644 apps/portal/src/app/ai/chat/EndpointMetadata.tsx create mode 100644 apps/portal/src/app/ai/chat/handling-responses/page.mdx create mode 100644 apps/portal/src/app/ai/chat/page.mdx create mode 100644 apps/portal/src/app/ai/sidebar.tsx create mode 100644 apps/portal/src/components/Document/APIEndpointMeta/common.tsx diff --git a/apps/portal/src/app/Header.tsx b/apps/portal/src/app/Header.tsx index 6d751a2d46b..794d55a1682 100644 --- a/apps/portal/src/app/Header.tsx +++ b/apps/portal/src/app/Header.tsx @@ -142,6 +142,10 @@ const apisLinks = [ ]; const aiLinks = [ + { + href: "/ai/chat", + name: "Chat API", + }, { href: "/ai/mcp", name: "MCP", diff --git a/apps/portal/src/app/ai/chat/EndpointMetadata.tsx b/apps/portal/src/app/ai/chat/EndpointMetadata.tsx new file mode 100644 index 00000000000..8c1c9d0ca17 --- /dev/null +++ b/apps/portal/src/app/ai/chat/EndpointMetadata.tsx @@ -0,0 +1,91 @@ +import { + type APIParameter, + ApiEndpoint, +} from "@/components/Document/APIEndpointMeta/ApiEndpoint"; +import { secretKeyHeaderParameter } from "../../../components/Document/APIEndpointMeta/common"; + +const contextFilterType = `\ +{ + from: string | null; + chain_ids: string[] | null; + session_id: string | null; +}`; + +const contextParameter: APIParameter = { + description: "Provide additional context information along with the message", + name: "context", + required: false, + type: contextFilterType, + example: { + from: "0x2247d5d238d0f9d37184d8332aE0289d1aD9991b", + chain_ids: [8453], + }, +}; + +const response200Example = `\ +{ + "message": "I've prepared a native ETH transfer as requested. Would you like to proceed with executing this transfer?", + "session_id": "123", + "request_id": "456", + "actions": [ + { + "type": "sign_transaction", + "data": { + "chainId": 8453, + "to": "0x1234567890123456789012345678901234567890", + "value": "10000000000000000", + "data": "0x" + }, + "session_id": "123", + "request_id": "456", + "source": "model", + "tool_name": null, + "description": null, + "kwargs": null + } + ] +}`; + +export function EndpointMetadata() { + return ( + + ); +} diff --git a/apps/portal/src/app/ai/chat/handling-responses/page.mdx b/apps/portal/src/app/ai/chat/handling-responses/page.mdx new file mode 100644 index 00000000000..046bde4d2c4 --- /dev/null +++ b/apps/portal/src/app/ai/chat/handling-responses/page.mdx @@ -0,0 +1,145 @@ +# Response Handling + +## Streamed vs non-streamed responses + +- **Streamed Responses**: This method streams data in real-time, providing immediate feedback as the response is generated. Set the `stream` parameter to `true` in your request, the API delivers responses via Server-Sent Events (SSE). +- **Non-Streamed Responses**: When the `stream` parameter is set to `false`, the API returns the complete response in a single JSON payload after processing is complete. + +## Handling Streamed Responses + +For `stream:true`, you'll need to handle the following event types: + +- `init`: Initializes the stream and provides session information +- `presence`: Provides backend status updates about worker processing +- `delta`: Contains chunks of the response message text +- `action`: Contains blockchain transaction or action data +- `image`: Contains image data +- `context`: Contains context data +- `error`: Contains error information if something goes wrong + +**Example SSE Stream:** + +```http +event: init +data: { + "session_id": "f4b45429-9570-4ee8-8c8f-8b267429915a", + "request_id": "9efc7f6a-8576-4d9c-8603-f6c72aa72164", + "type": "init", + "source": "model", +} + +event: presence +data: { + "session_id": "c238c379-9875-4729-afb2-bf3c26081e24", + "request_id": "8b0e733a-89fd-4585-8fac-b4334208eada", + "source": "model", + "type": "presence", + "data": "Need to convert 0.00001 ETH to wei, then prepare native transfer to `0x2247d5d238d0f9d37184d8332aE0289d1aD9991b`." +} + +event: presence +data: { + "session_id": "f4b45429-9570-4ee8-8c8f-8b267429915a", + "request_id": "9efc7f6a-8576-4d9c-8603-f6c72aa72164", + "type": "presence", + "source": "model", + "data": "Now that I have the wei value (10,000,000,000,000), I will prepare a native transfer to the recipient." +} + +event: delta +data: {"v": "To send 0.0001 ETH on the Sepolia network"} + +event: delta +data: {"v": " to the address associated with"} + +event: action +data: { + "session_id": "f4b45429-9570-4ee8-8c8f-8b267429915a", + "request_id": "9efc7f6a-8576-4d9c-8603-f6c72aa72164", + "type": "sign_transaction", + "source": "executor", + "data": "{\"chainId\": 11155111, \"to\": \"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\", \"data\": \"0x\", \"value\": \"0x5af3107a4000\"}" +} +``` + +### TypeScript Example + +We recommend using the `fetch-event-stream` package to handle the event stream in TypeScript. This gives you an easy way to handle each event type. + +```jsx +import { stream } from "fetch-event-stream"; + +const events = await stream(`https://api.thirdweb.com/ai/chat`, { + body: JSON.stringify({ + messages: [ + { + role: "user", + content: "Transfer 10 USDC to vitalik.eth", + }, + ], + }), + headers: { + "Content-Type": "application/json", + "x-secret-key": "", + }, + method: "POST", +}); + +// process the event stream +for await (const event of events) { + if (!event.data) { + continue; + } + switch (event.event) { + case "init": { + console.log("Init event", event.data); + // handle init event (session id and request id) + break; + } + case "presence": { + console.log("Presence event", event.data); + // handle intermediate thinking steps + break; + } + case "delta": { + console.log("Delta event", event.data); + // handle delta event (streamed output text response) + break; + } + case "action": { + const data = JSON.parse(event.data); + + // handle transaction signing + if (data.type === "sign_transaction") { + const transactionData = JSON.parse(data.data); + console.log("Sign transaction event", transactionData); + } + + // handle swap signing + if (data.type === "sign_swap") { + const swapData = JSON.parse(data.data); + console.log("Sign swap event", swapData); + } + + break; + } + case "image": { + const data = JSON.parse(event.data); + // handle image rendering + console.log("Image data", data); + break; + } + case "context": { + console.log("Context event", event.data); + // handle context changes (chain ids, wallet address, etc) + break; + } + case "error": { + console.log("Error event", event.data); + // handle error event (error message) + break; + } + } +} +``` + diff --git a/apps/portal/src/app/ai/chat/page.mdx b/apps/portal/src/app/ai/chat/page.mdx new file mode 100644 index 00000000000..97f474c0c81 --- /dev/null +++ b/apps/portal/src/app/ai/chat/page.mdx @@ -0,0 +1,53 @@ +import { EndpointMetadata } from "./EndpointMetadata"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@doc"; + +# Chat API + +The thirdweb AI chat API is a standard OpenAI-compatible chat completion API that allows you to interact with the thirdweb AI model, specialized for blockchain interactions. + +The thirdweb proprietary model is optimized for blockchain interactions and can: + +- Query real-time data from the blockchain +- Analyze transactions +- Fetch token balances, prices and metadata +- Prepare any contract call or transaction for signing +- Prepare swaps from any token pair +- Deploy contracts +- Generate images +- Search the web for information +- And more! + +You can use the API with the API directly, or with any OpenAI-compatible client library. + + + + HTTP API + OpenAI Client + + + + + + +### Using the OpenAI Client + +You can use the standard OpenAI client library to interact with the thirdweb AI model. + +```python +from openai import OpenAI + +client = OpenAI( + base_url="https://api.thirdweb.com/ai", + api_key="", +) +chat_completion = client.chat.completions.create( + model="t0", + messages=[{"role": "user", "content": "Transfer 10 USDC to vitalik.eth"}], + stream=False, + extra_body={ "context": { "from": "0x...", "chain_ids": [8453] }} +) + +print(chat_completion) +``` + + \ No newline at end of file diff --git a/apps/portal/src/app/ai/layout.tsx b/apps/portal/src/app/ai/layout.tsx index 1b870666a47..f2862ba983c 100644 --- a/apps/portal/src/app/ai/layout.tsx +++ b/apps/portal/src/app/ai/layout.tsx @@ -1,33 +1,16 @@ import { createMetadata } from "@doc"; -import { BookIcon, ZapIcon } from "lucide-react"; import { DocLayout } from "@/components/Layouts/DocLayout"; +import { sidebar } from "./sidebar"; export default async function Layout(props: { children: React.ReactNode }) { return ( - , - href: "/ai/mcp", - }, - { - name: "llms.txt", - icon: , - href: "/ai/llm-txt", - }, - ], - name: "AI", - }} - > +
{props.children}
); } export const metadata = createMetadata({ - description: "AI tools for agents and LLM clients.", + description: "AI tools for apps, agents and LLM clients.", title: "thirdweb AI", }); diff --git a/apps/portal/src/app/ai/sidebar.tsx b/apps/portal/src/app/ai/sidebar.tsx new file mode 100644 index 00000000000..5ae9dbb8624 --- /dev/null +++ b/apps/portal/src/app/ai/sidebar.tsx @@ -0,0 +1,38 @@ +import { BookIcon, ZapIcon } from "lucide-react"; +import type { SideBar } from "@/components/Layouts/DocLayout"; + +export const sidebar: SideBar = { + links: [ + { + name: "Chat API", + isCollapsible: false, + links: [ + { + name: "Get Started", + href: "/ai/chat", + icon: , + }, + { + name: "Response Handling", + href: "/ai/chat/handling-responses", + }, + { + name: "API Reference", + href: "https://api.thirdweb-dev.com/reference#tag/ai/post/ai/chat", + }, + ], + }, + { separator: true }, + { + name: "MCP Server", + icon: , + href: "/ai/mcp", + }, + { + name: "llms.txt", + icon: , + href: "/ai/llm-txt", + }, + ], + name: "AI", +}; diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index c57556e1a85..eff115b8664 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -1,7 +1,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "../../../lib/utils"; import { CodeBlock } from "../Code"; -import { Details } from "../Details"; import { Heading } from "../Heading"; import { Paragraph } from "../Paragraph"; import { RequestExample } from "./RequestExample"; @@ -12,12 +11,22 @@ export type APIParameter = required: false; description: React.ReactNode; type?: string; - example?: string | boolean | number | object; + example?: + | string + | boolean + | number + | object + | Array; } | { name: string; required: true; - example: string; + example: + | string + | boolean + | number + | object + | Array; description: React.ReactNode; type?: string; }; @@ -37,7 +46,7 @@ type ApiEndpointMeta = { }; export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { - const { request, responseExamples } = props.metadata; + const { responseExamples } = props.metadata; const requestExamples: Array<{ lang: "javascript" | "bash"; @@ -65,30 +74,12 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { return (
-
- +
+ {props.metadata.title} {props.metadata.description}
- - {/* Headers */} - {request.headers.length > 0 && ( - - )} - - {/* Path parameters */} - {request.pathParameters.length > 0 && ( - - )} - - {/* Body */} - {request.bodyParameters.length > 0 && ( - - )}
@@ -158,61 +149,61 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { ); } -function ParameterSection(props: { - title: string; - parameters: APIParameter[]; -}) { - return ( -
- - {props.title} - -
- {props.parameters - .sort((a, b) => { - if (a.required === b.required) { - return 0; - } - return a.required ? -1 : 1; - }) - .map((param) => ( - - ))} -
-
- ); -} +// function ParameterSection(props: { +// title: string; +// parameters: APIParameter[]; +// }) { +// return ( +//
+// +// {props.title} +// +//
+// {props.parameters +// .sort((a, b) => { +// if (a.required === b.required) { +// return 0; +// } +// return a.required ? -1 : 1; +// }) +// .map((param) => ( +// +// ))} +//
+//
+// ); +// } -function ParameterItem({ param }: { param: APIParameter }) { - return ( -
-
- {param.description} - {param.type && ( -
-

Type

- -
- )} -
-
- ); -} +// function ParameterItem({ param }: { param: APIParameter }) { +// return ( +//
+//
+// {param.description} +// {param.type && ( +//
+//

Type

+// +//
+// )} +//
+//
+// ); +// } function createCurlCommand(params: { metadata: ApiEndpointMeta }) { const url = `${params.metadata.origin}${params.metadata.path}`; diff --git a/apps/portal/src/components/Document/APIEndpointMeta/common.tsx b/apps/portal/src/components/Document/APIEndpointMeta/common.tsx new file mode 100644 index 00000000000..799d67a0363 --- /dev/null +++ b/apps/portal/src/components/Document/APIEndpointMeta/common.tsx @@ -0,0 +1,9 @@ +import type { APIParameter } from "./ApiEndpoint"; + +export const secretKeyHeaderParameter: APIParameter = { + description: "Your project secret key for authentication.", + example: "", + name: "x-secret-key", + required: true, + type: "string", +};