Skip to main content
This guide shows how to build an AI-powered research assistant that combines Tako data visualizations with web search using LangGraph for agent workflows and CopilotKit for the frontend. The agent searches Tako via MCP (Model Context Protocol) and embeds interactive charts directly in generated reports.
Try the live demo at copilotkit.tako.com to see the integration in action.

Architecture Overview

┌─────────────────┐
│  Next.js App    │
│  (CopilotKit)   │
└────────┬────────┘

         │ useCoAgent

┌─────────────────┐
│  LangGraph      │
│  Agent          │
└────────┬────────┘

         ├─→ OpenAI (LLM)
         ├─→ Tavily (Web Search)
         └─→ Tako MCP Server (Data Visualizations)

Prerequisites

  • Node.js 18+ and Python 3.11+
  • API keys:
NamePurpose
OPENAI_API_KEYLLM for agent reasoning
TAVILY_API_KEYWeb search
TAKO_API_TOKENTako data visualizations
TAKO_MCP_URLMCP server endpoint (default: https://mcp.tako.com)

1. Set up the project

# Clone the example repo
git clone https://github.com/TakoData/tako-copilotkit.git
cd tako-copilotkit

# Install frontend dependencies
npm install

# Install Python agent dependencies
cd agents/python && uv sync && cd ../..

# Create environment file
cp .env.example .env.local
Add your API keys to .env.local:
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
TAKO_API_TOKEN=your-tako-token
TAKO_MCP_URL=https://mcp.tako.com

2. Define the MCP client

Create a client to communicate with Tako’s MCP server for knowledge search:
mcp_integration.py
import asyncio
import json
import httpx
from typing import Any, Dict, List, Optional

MCP_SERVER_URL = os.getenv("TAKO_MCP_URL", "https://mcp.tako.com")
TAKO_API_TOKEN = os.getenv("TAKO_API_TOKEN", "")

class SimpleMCPClient:
    """Minimal MCP client for Tako integration."""

    def __init__(self, base_url: str):
        self.base_url = base_url.rstrip("/")
        self.session_id = None
        self.message_id = 0
        self._responses = {}
        self._client = httpx.AsyncClient(timeout=120.0)

    async def connect(self):
        """Connect via SSE to get session ID."""
        self._sse_task = asyncio.create_task(self._sse_reader())
        for _ in range(50):
            if self.session_id:
                return True
            await asyncio.sleep(0.1)
        return False

    async def call_tool(self, name: str, args: dict):
        """Call an MCP tool."""
        return await self._send("tools/call", {"name": name, "arguments": args})

3. Create the knowledge search function

Use the MCP client to search Tako’s knowledge base:
mcp_integration.py
async def search_knowledge_base(
    query: str,
    count: int = 5,
    search_effort: str = "fast"
) -> List[Dict[str, Any]]:
    """Search Tako knowledge base via MCP."""

    result = await _call_mcp_tool("knowledge_search", {
        "query": query,
        "api_token": TAKO_API_TOKEN,
        "count": count,
        "search_effort": search_effort,  # "fast", "auto", or "deep"
        "country_code": "US",
        "locale": "en-US"
    })

    if result and "results" in result:
        formatted_results = []
        for item in result["results"]:
            item_id = item.get("card_id") or item.get("id")
            formatted_results.append({
                "type": "data_visualization",
                "id": item_id,
                "embed_url": f"https://tako.com/embed/{item_id}/?theme=dark",
                "title": item.get("title", ""),
                "description": item.get("description", ""),
                "url": f"https://tako.com/card/{item_id}"
            })
        return formatted_results
    return []

4. Get iframe HTML for charts

Retrieve embeddable iframe HTML for Tako visualizations:
mcp_integration.py
async def get_visualization_iframe(
    item_id: str = None,
    embed_url: str = None
) -> Optional[str]:
    """Get iframe HTML for a Tako visualization."""

    if item_id:
        result = await _call_mcp_tool("open_chart_ui", {
            "pub_id": item_id,
            "dark_mode": True,
            "width": 900,
            "height": 600
        })
        if result:
            resource = result.get("resource", {})
            return resource.get("htmlString")

    # Fallback: generate iframe directly
    if embed_url:
        return f'''<iframe
  width="100%" height="600"
  src="{embed_url}"
  frameborder="0"
></iframe>'''

    return None

5. Define the LangGraph agent state

Define the state that flows through your agent workflow:
state.py
from typing import List, Optional
from langgraph.graph import MessagesState
from pydantic import BaseModel

class Resource(BaseModel):
    url: str
    title: str
    description: str
    content: Optional[str] = None
    resource_type: str  # "web" or "tako_chart"
    iframe_html: Optional[str] = None

class AgentState(MessagesState):
    model: str
    research_question: str
    report: str
    resources: List[Resource]
    logs: List[dict]

6. Create the search node

Build a LangGraph node that searches both Tako and web sources in parallel:
search.py
import asyncio
from tavily import TavilyClient
from src.lib.mcp_integration import search_knowledge_base, get_visualization_iframe

async def search_node(state: AgentState, config: RunnableConfig):
    """Search Tako and web sources in parallel."""

    queries = state.get("queries", [])

    # Run searches in parallel
    tavily_tasks = [async_tavily_search(q) for q in queries]
    tako_tasks = [search_knowledge_base(q) for q in queries]

    all_results = await asyncio.gather(
        *tavily_tasks, *tako_tasks,
        return_exceptions=True
    )

    # Process Tako results and generate iframe HTML
    for tako_result in tako_results:
        if tako_result.get("type") == "data_visualization":
            iframe_html = await get_visualization_iframe(
                item_id=tako_result.get("id"),
                embed_url=tako_result.get("embed_url")
            )
            state["resources"].append({
                "url": tako_result["url"],
                "title": tako_result["title"],
                "description": tako_result["description"],
                "resource_type": "tako_chart",
                "iframe_html": iframe_html,
            })

    return state

7. Build the LangGraph workflow

Compose nodes into a complete agent workflow:
agent.py
from langgraph.graph import StateGraph
from langgraph.checkpoint.memory import MemorySaver

from src.lib.chat import chat_node
from src.lib.search import search_node
from src.lib.state import AgentState

# Define the graph
workflow = StateGraph(AgentState)
workflow.add_node("chat_node", chat_node)
workflow.add_node("search_node", search_node)

# Set entry point and edges
workflow.set_entry_point("chat_node")
workflow.add_edge("search_node", "chat_node")

# Compile with memory for conversation persistence
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)

8. Expose the agent via FastAPI

Create an endpoint that CopilotKit can connect to:
main.py
import os
from dotenv import load_dotenv
from fastapi import FastAPI
from copilotkit import LangGraphAgent
from copilotkit.integrations.fastapi import add_fastapi_endpoint
from src.agent import graph

load_dotenv()

app = FastAPI()

# Add the CopilotKit endpoint
add_fastapi_endpoint(
    app,
    agents=[
        LangGraphAgent(
            graph=graph,
            name="research_agent",
        )
    ]
)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=2024)

9. Connect the React frontend

Use CopilotKit hooks to connect your UI to the LangGraph agent:
Main.tsx
import { useCoAgent } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";

interface AgentState {
  model: string;
  research_question: string;
  resources: Resource[];
  report: string;
}

export default function Main() {
  const { state, setState } = useCoAgent<AgentState>({
    name: "research_agent",
    initialState: {
      model: "openai",
      research_question: "",
      resources: [],
      report: "",
    },
  });

  return (
    <div className="flex h-screen">
      {/* Chat panel */}
      <div className="w-1/3">
        <CopilotChat />
      </div>

      {/* Results panel with Tako charts */}
      <div className="w-2/3 p-4">
        <h2>{state.research_question}</h2>

        {/* Render Tako charts */}
        {state.resources
          .filter(r => r.resource_type === "tako_chart")
          .map(resource => (
            <div
              key={resource.url}
              dangerouslySetInnerHTML={{ __html: resource.iframe_html }}
            />
          ))}

        {/* Render report */}
        <div className="prose">
          {state.report}
        </div>
      </div>
    </div>
  );
}

10. Wrap the app with CopilotKit

Configure the CopilotKit provider to connect to your agent backend:
page.tsx
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/styles.css";
import Main from "./Main";

export default function Page() {
  return (
    <CopilotKit runtimeUrl="/api/copilotkit">
      <Main />
    </CopilotKit>
  );
}
Create the API route to proxy requests to your agent:
app/api/copilotkit/route.ts
import { CopilotRuntime, LangGraphHttpAgent } from "@copilotkit/runtime";
import { NextRequest } from "next/server";

export async function POST(req: NextRequest) {
  const runtime = new CopilotRuntime({
    agents: [
      new LangGraphHttpAgent({
        name: "research_agent",
        url: "http://localhost:2024/copilotkit",
      }),
    ],
  });

  return runtime.streamHttpServerResponse(req, new Response());
}

Run the application

# Start both frontend and agent
npm run dev
The app will be available at: Try queries like:
  • “Compare NVIDIA and AMD revenue over the last 5 years”
  • “What are the trends in global military spending?”
  • “How has inflation affected rent prices in major US cities?”
The agent will search Tako for relevant data visualizations and embed interactive charts directly in the research report.

Key concepts

ConceptDescription
MCP IntegrationTako exposes data via Model Context Protocol for structured access
Parallel SearchWeb and Tako searches run concurrently for faster results
Iframe EmbeddingCharts are embedded as responsive iframes with resize handling
State SyncCopilotKit’s useCoAgent keeps frontend and agent state synchronized
LangGraph WorkflowNodes handle chat, search, and report generation in a directed graph

Resources