> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thesys.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Two-Step Visualize Pattern

> Integrate C1 Visualize API with your existing LLM infrastructure for beautiful UI generation

This guide demonstrates how to integrate C1 Visualize API with your existing agentic application using a two-step architecture. This pattern allows you to leverage any LLM provider (OpenAI, Anthropic, etc.) for tool-calling and business logic, while using C1 exclusively for generating beautiful, interactive UI components.

## Overview

The two-step Visualize pattern separates concerns between your primary LLM and the UI generation layer:

1. **Step 1 - Business Logic**: Your primary LLM handles user requests, executes tool calls, and generates a final text/markdown response
2. **Step 2 - UI Generation**: C1 Visualize API converts the text response into interactive, generative UI components

This approach is ideal when:

* You want to use your existing LLM infrastructure for tool-calling
* You need to maintain complete conversation history with your primary LLM
* You want to add beautiful UI generation without refactoring your agent logic

<Note>
  For most applications, we recommend using [C1 as the Gateway LLM](/guides/integration-patterns#c1-as-the-gateway-llm-preferred) instead, which provides lower latency and better context awareness.
</Note>

## Architecture

```mermaid theme={null}
sequenceDiagram
    participant User
    participant Application
    participant LLM (OpenAI)
    participant C1 Visualize API
    participant ThreadStore

    User->>Application: Sends message
    Application->>LLM (OpenAI): Sends request (with history)
    Note over LLM (OpenAI): Processes, potentially calls tools
    LLM (OpenAI)-->>Application: Returns final text response
    Application->>ThreadStore: Stores LLM interaction history (AIMessage)
    Application->>C1 Visualize API: Sends final text response
    C1 Visualize API-->>Application: Returns GenUI response
    Application->>ThreadStore: Stores User message & GenUI response (UIMessage)
    Application-->>User: Displays GenUI response
```

## Key Concepts

### Message Types

The pattern uses two distinct message storage strategies:

**AIMessage**: Complete conversation history with your primary LLM

* Includes user prompts, assistant responses, tool calls and their results
* Essential for maintaining context in subsequent LLM calls

**UIMessage**: Display-optimized messages for the frontend

* User prompts and final generative UI responses from C1
* Used for rendering the chat interface

### The Two-Step Flow

1. **Step 1**: Call your primary LLM with full conversation context, allowing it to use tools and generate a complete response
2. **Step 2**: Send the final LLM response to C1 Visualize API, which transforms it into rich UI components without needing tool access

## Setup

### 1. Install Dependencies

<CodeGroup dropdown>
  ```bash npm theme={null}
  npm install openai @thesysai/genui-sdk @crayonai/react-core @crayonai/react-ui @crayonai/stream
  ```

  ```bash pip theme={null}
  pip install openai fastapi uvicorn thesys-genui-sdk
  ```
</CodeGroup>

### 2. Environment Variables

Create a `.env` file with your API keys:

```bash theme={null}
OPENAI_API_KEY=your_openai_api_key
THESYS_API_KEY=your_thesys_api_key
```

<Note>You can create a new API key from [Developer Console](https://console.thesys.dev/keys)</Note>

### 3. Configure OpenAI Clients

Create two OpenAI client instances - one for your standard LLM calls and one for C1 Visualize:

<CodeGroup dropdown>
  ```typescript TypeScript/Next.js theme={null}
  import OpenAI from "openai";

  // Standard OpenAI client for business logic and tool-calling
  const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
  });

  // C1 Visualize client for UI generation
  const thesysClient = new OpenAI({
    baseURL: "https://api.thesys.dev/v1/visualize",
    apiKey: process.env.THESYS_API_KEY,
  });
  ```

  ```python Python/FastAPI theme={null}
  import os
  from openai import OpenAI

  # Standard OpenAI client for business logic and tool-calling
  openai = OpenAI(
      api_key=os.environ["OPENAI_API_KEY"]
  )

  # C1 Visualize client for UI generation
  thesys_client = OpenAI(
      base_url="https://api.thesys.dev/v1/visualize",
      api_key=os.environ["THESYS_API_KEY"],
  )
  ```
</CodeGroup>

## Implementation

### Backend API Route

Create an API route that orchestrates the two-step process:

<CodeGroup dropdown>
  ```typescript app/api/chat/route.ts [expandable] theme={null}
  import { NextRequest } from "next/server";
  import OpenAI from "openai";
  import { transformStream } from "@crayonai/stream";
  import { tools } from "./tools";

  const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
  });

  const thesysClient = new OpenAI({
    baseURL: "https://api.thesys.dev/v1/visualize",
    apiKey: process.env.THESYS_API_KEY,
  });

  const SYSTEM_PROMPT = `You are a helpful assistant.

  Your outputs will be processed by a downstream visualizer agent to generate a user interface.
  The visualizer agent is responsible for generating a user interface based on your output but it does not have access to the tool calls or the database.
  In your response, you should provide enough information with possible actions like buttons, forms, links, etc. that the user can take to interact with the UI.

  If user asks for output in a specific component (e.g., graph, table, chart), generate the output in the requested format so the visualizer can render it properly. Give hints in your output so that the downstream agent can use those hints to generate UI properly.`;

  export async function POST(req: NextRequest) {
    const { prompt, threadId, responseId } = await req.json();

    // Step 1: Call Standard OpenAI API
    const previousAiMessages = await getAIThreadMessages(threadId);

    // Use runTools helper to handle tool execution automatically
    const runner = openai.beta.chat.completions.runTools({
      model: "gpt-4",
      messages: [
        { role: "system", content: SYSTEM_PROMPT },
        ...previousAiMessages,
        { role: "user", content: prompt.content },
      ],
      temperature: 0.1,
      stream: false,
      tools: tools,
    });

    // Wait for OpenAI to complete (including tool calls)
    await runner.done();
    const finalMessages = runner.messages;

    // Step 2: Store AI Interaction History
    const newAiMessagesToStore = [];

    // Add the user prompt
    newAiMessagesToStore.push(prompt);

    // Track messages and extract final assistant message
    let finalAssistantMessageForUI;
    runner.on("message", (message) => {
      newAiMessagesToStore.push({ ...message, id: crypto.randomUUID() });
      if (typeof message.content === "string") {
        finalAssistantMessageForUI = message;
      }
    });

    // Step 3: Validate Final Assistant Content

    if (!finalAssistantMessageForUI ||
        typeof finalAssistantMessageForUI.content !== "string") {
      console.error("No final assistant message found");
      return new Response("", { status: 200 });
    }

    // Step 4: Call C1 Visualize API
    const thesysStreamRunner = thesysClient.beta.chat.completions.runTools({
      model: "c1/anthropic/claude-sonnet-4/v-20251230",
      messages: [
        ...previousAiMessages,
        { role: "user", content: prompt.content },
        { role: "assistant", content: finalAssistantMessageForUI.content },
      ],
      stream: true,
      tools: [], // No tools needed for Visualize API
    });

    // Visualize API returns only one assistant message
    thesysStreamRunner.on("message", async (message) => {
      // Step 5: Store messages after streaming completes
      await addMessages(
        threadId,
        newAiMessagesToStore,
        [prompt, { ...message, id: responseId }]
      );
    });

    const llmStream = await thesysStreamRunner;

    // Step 6: Stream Response to Client
    const responseStream = transformStream(llmStream, (chunk) => {
      return chunk.choices[0]?.delta?.content || "";
    });

    return new Response(responseStream, {
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache, no-transform",
        Connection: "keep-alive",
      },
    });
  }
  ```

  <Note>
    For tool definitions, see the [Tool Calling guide](/guides/integrate-data/tool-calling) for complete examples of how to define and implement tools for your application.
  </Note>

  ```python main.py [expandable] theme={null}
  import os
  import json
  from typing import List, Dict, Any, Optional
  from fastapi import FastAPI
  from pydantic import BaseModel
  from openai import OpenAI
  from thesys_genui_sdk.fast_api import with_c1_response
  from thesys_genui_sdk.context import write_content
  from tools import tools, tool_impls

  app = FastAPI()

  # Standard OpenAI client for business logic and tool-calling
  openai = OpenAI(
      api_key=os.environ["OPENAI_API_KEY"]
  )

  # C1 Visualize client for UI generation
  thesys_client = OpenAI(
      base_url="https://api.thesys.dev/v1/visualize",
      api_key=os.environ["THESYS_API_KEY"],
  )

  SYSTEM_PROMPT = """You are a helpful assistant.

  Your outputs will be processed by a downstream visualizer agent to generate a user interface.
  The visualizer agent is responsible for generating a user interface based on your output but it does not have access to the tool calls or the database.
  In your response, you should provide enough information with possible actions like buttons, forms, links, etc. that the user can take to interact with the UI.

  If user asks for output in a specific component (e.g., graph, table, chart), generate the output in the requested format so the visualizer can render it properly. Give hints in your output so that the downstream agent can use those hints to generate UI properly."""


  class ChatMessage(BaseModel):
      role: str
      content: str
      id: Optional[str] = None


  class ChatRequest(BaseModel):
      prompt: ChatMessage
      threadId: str
      responseId: str


  def run_chat_with_tools(
      messages: List[Dict[str, Any]]
  ) -> str:
      """Step 1: Call Standard OpenAI API with tool execution"""
      completion = openai.chat.completions.create(
          model="gpt-4",
          messages=messages,
          temperature=0.1,
          tools=tools,
      )

      while True:
          choice = completion.choices[0]
          message = choice.message
          tool_calls = message.tool_calls or []

          # If there are no tool calls, return the assistant's answer
          if not tool_calls:
              return message.content or ""

          # Record the assistant message that requested tools
          messages.append(
              {
                  "role": "assistant",
                  "content": message.content or "",
                  "tool_calls": [
                      {
                          "id": tc.id,
                          "type": "function",
                          "function": {
                              "name": tc.function.name,
                              "arguments": tc.function.arguments,
                          },
                      }
                      for tc in tool_calls
                  ],
              }
          )

          # Execute tools and append results
          for tool_call in tool_calls:
              name = tool_call.function.name
              args = json.loads(tool_call.function.arguments or "{}")
              result = tool_impls[name](**args)
              messages.append(
                  {
                      "role": "tool",
                      "tool_call_id": tool_call.id,
                      "content": result,
                  }
              )

          # Ask the model again with tool results
          completion = openai.chat.completions.create(
              model="gpt-4",
              messages=messages,
              temperature=0.1,
              tools=tools,
          )


  @app.post("/api/chat")
  @with_c1_response()
  async def chat(request: ChatRequest):
      # Step 1: Get AI thread messages
      previous_ai_messages = await get_ai_thread_messages(request.threadId)

      # Prepare messages for OpenAI
      messages = [
          {"role": "system", "content": SYSTEM_PROMPT},
          *previous_ai_messages,
          {"role": "user", "content": request.prompt.content},
      ]

      # Step 2: Call OpenAI with tools and get final response
      final_assistant_message = run_chat_with_tools(messages.copy())

      # Step 3: Store AI interaction history
      new_ai_messages = [
          {"role": "user", "content": request.prompt.content, "id": request.prompt.id},
          {"role": "assistant", "content": final_assistant_message, "id": request.responseId},
      ]
      await add_ai_messages(request.threadId, new_ai_messages)

      # Step 4: Call C1 Visualize API
      visualize_messages = [
          *previous_ai_messages,
          {"role": "user", "content": request.prompt.content},
          {"role": "assistant", "content": final_assistant_message},
      ]

      stream = thesys_client.chat.completions.create(
          model="c1/anthropic/claude-sonnet-4/v-20251230",
          messages=visualize_messages,
          stream=True,
      )

      # Step 5: Stream response to client
      for chunk in stream:
          content = chunk.choices[0].delta.content
          if content:
              await write_content(content)

      # Step 6: Store UI messages
      await add_ui_messages(
          request.threadId,
          [
              {"role": "user", "content": request.prompt.content, "id": request.prompt.id},
              {"role": "assistant", "content": final_assistant_message, "id": request.responseId},
          ]
      )


  # Helper functions for message storage (implement based on your database)
  async def get_ai_thread_messages(thread_id: str) -> List[Dict[str, Any]]:
      # Implement fetching AI messages from your database
      pass


  async def add_ai_messages(thread_id: str, messages: List[Dict[str, Any]]):
      # Implement storing AI messages to your database
      pass


  async def add_ui_messages(thread_id: str, messages: List[Dict[str, Any]]):
      # Implement storing UI messages to your database
      pass
  ```
</CodeGroup>

### Frontend Component

Use the GenUI SDK to render the chat interface:

```tsx app/page.tsx  [expandable] theme={null}
"use client";

import "@crayonai/react-ui/styles/index.css";
import {
  C1Chat,
  useThreadListManager,
  useThreadManager,
} from "@thesysai/genui-sdk";
import * as apiClient from "@/src/apiClient";

export default function Home() {
  const threadListManager = useThreadListManager({
    fetchThreadList: () => apiClient.getThreadList(),
    deleteThread: (threadId) => apiClient.deleteThread(threadId),
    updateThread: (t) => apiClient.updateThread(t),
    createThread: (message) => apiClient.createThread(message.message),
  });

  const threadManager = useThreadManager({
    threadListManager,
    loadThread: (threadId) => apiClient.getMessages(threadId),
    onUpdateMessage: ({ message }) => {
      apiClient.updateMessage(
        threadListManager.selectedThreadId,
        message
      );
    },
    apiUrl: "/api/chat",
  });

  return (
    <C1Chat
      threadManager={threadManager}
      threadListManager={threadListManager}
    />
  );
}
```

## System Prompt Best Practices

When crafting your system prompt for the primary LLM, include guidance for the downstream visualizer:

1. **Mention the Visualizer**: Inform the LLM that its output will be processed by a UI generator
2. **Provide Context**: Explain that the visualizer doesn't have access to tools or database
3. **Request Structure**: Ask for actionable elements like buttons, forms, and links
4. **Format Hints**: Suggest specific UI components when appropriate (tables, charts, etc.)

### Example System Prompt

```typescript theme={null}
const SYSTEM_PROMPT = `You are a [domain-specific] assistant.

**Your Workflow:**
1. [Describe your agent's workflow]
2. [List key actions and tools]
3. [Specify behavior patterns]

**Visualizer Agent:**
Your outputs will be processed by a downstream visualizer agent to generate a user interface.
The visualizer agent is responsible for generating a user interface based on your output but it does not have access to the tool calls or the database.
In your response, you should provide enough information with possible actions like buttons to select items, forms to submit data, links to navigate, etc. that the user can take to interact with the UI.

If user asks for output in a specific component (e.g., graph, table, chart), generate the output in the requested format so the visualizer can render it properly. Give hints in your output so that the downstream agent can use those hints to generate UI properly.`;
```

## Message Persistence

Store two types of messages with distinct purposes:

**AIMessage Table**

* Stores complete LLM conversation history including tool calls and results
* Used for context in subsequent LLM calls

**UIMessage Table**

* Stores user-facing messages with generative UI responses
* Used for rendering chat interface

### Example Prisma Schema

```prisma schema.prisma  [expandable] theme={null}
model Thread {
  id          String      @id @default(uuid())
  title       String?
  createdAt   DateTime    @default(now())
  updatedAt   DateTime    @updatedAt
  aiMessages  AIMessage[]
  uiMessages  UIMessage[]
}

model AIMessage {
  id        String   @id @default(uuid())
  threadId  String
  thread    Thread   @relation(fields: [threadId], references: [id], onDelete: Cascade)
  role      String
  content   String?
  toolCalls Json?
  toolCallId String?
  name      String?
  createdAt DateTime @default(now())
}

model UIMessage {
  id        String   @id @default(uuid())
  threadId  String
  thread    Thread   @relation(fields: [threadId], references: [id], onDelete: Cascade)
  role      String
  content   String?
  createdAt DateTime @default(now())
}
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="C1 generates generic UI instead of domain-specific components">
    Enhance your system prompt to provide more context about expected UI patterns and include relevant metadata in the assistant's final message.

    ```typescript theme={null}
    const SYSTEM_PROMPT = `You are a ${domainName} assistant.
    ...
    Include specific details about ${relevantContext} in your responses.`;
    ```
  </Accordion>

  <Accordion title="Message history growing too large">
    For development, you can truncate to recent messages. For production, use prompt caching with conversation compaction:

    ```typescript theme={null}
    // Development: Keep only the last N messages
    const recentMessages = conversationHistory.slice(-10);

    // Production: Use prompt caching and compaction
    // Compact older messages into summaries while keeping recent context
    ```
  </Accordion>
</AccordionGroup>

## Best Practices

1. **Separate Concerns**: Keep business logic in your primary LLM and UI generation in C1
2. **Rich Context**: Provide detailed information in the final assistant message for better UI generation
3. **Actionable Elements**: Always include next-step actions (buttons, forms) in your responses
4. **Message Storage**: Store both AI and UI message histories for optimal context management
5. **Error Recovery**: Implement graceful fallbacks if either API call fails
6. **Testing**: Test the full two-step flow with various query types and edge cases

<Warning>
  This pattern introduces additional latency since you must wait for the primary LLM to complete before C1 can start streaming. For most applications, consider using [C1 as the Gateway LLM](/guides/integration-patterns#c1-as-the-gateway-llm-preferred) instead.
</Warning>

<Card title="Full Example on GitHub" icon="github" href="https://github.com/thesysdev/examples/tree/main/c1-vizualise-ecommerce-agent">
  See a complete working implementation of this pattern in our e-commerce agent example with tools, database integration, and production-ready code.
</Card>
