DEV Community

Cover image for How to Add a Cosmic AI Agent to Your Codebase: Website Chat with Full Content Context
Tony Spiro
Tony Spiro

Posted on • Originally published at cosmicjs.com

How to Add a Cosmic AI Agent to Your Codebase: Website Chat with Full Content Context

Most chat widgets on the web are disconnected from the product they sit on. They answer from a generic LLM with no knowledge of your actual content, pricing, or documentation. The result: hallucinated answers, frustrated users, and support tickets that never needed to exist.

Cosmic AI agents solve this at the infrastructure level. Because your agent lives inside your Cosmic project, it has direct read access to every object in your bucket: blog posts, docs, FAQs, product pages, and any other content type you define. No external vector database. No separate embedding pipeline. No syncing.

This tutorial walks through the anchor use case: building a website chat agent that answers visitor questions using your real CMS content, exposed via the Cosmic REST API and JavaScript/TypeScript SDK.


What We're Building

A Team Agent configured with:

  • CMS Read access to your content bucket
  • A REST API channel so your frontend can send messages and stream responses
  • A persona prompt that keeps answers grounded in your actual content
  • A streaming chat widget built in Next.js using the @cosmicjs/sdk

By the end, visitors on your site can ask "What is your refund policy?" or "How do I set up webhooks?" and get accurate, sourced answers pulled from your real CMS objects.


Step 1: Create the Team Agent

In your Cosmic project dashboard:

  1. Navigate to Team Agents in the project sidebar
  2. Click Create Team Agent
  3. Give it a name (e.g. "Site Assistant")
  4. Set the persona prompt (see below)
  5. Under Channels, enable REST API
  6. Under Capabilities, enable CMS Read
  7. Save the agent

Persona prompt example:

You are a helpful assistant for [Your Company]. You answer questions using the content in our CMS bucket: documentation, blog posts, FAQs, and product pages.

Rules:
- Only answer based on content you can find in the CMS. If you cannot find a relevant answer, say so honestly and suggest the user contact support.
- Keep answers concise and accurate. Quote directly from CMS content when helpful.
- Never fabricate pricing, feature availability, or technical details.
- If asked about something outside your knowledge base, direct the user to the relevant docs page or contact form.
Enter fullscreen mode Exit fullscreen mode

After saving, copy the Agent ID from the agent detail page. You will need it for the API calls.


Step 2: Get a Personal Access Token

All REST API calls to your agent require a Personal Access Token.

  1. Go to Account Settings > API Tokens
  2. Click Create Token
  3. Give it a descriptive name (e.g. "Site Chat Widget")
  4. Copy the token. You will only see it once.

Store it as an environment variable in your project:

COSMIC_AGENT_TOKEN=cos_your_token_here
COSMIC_AGENT_ID=your_agent_id_here
Enter fullscreen mode Exit fullscreen mode

Step 3: Create a Next.js API Route

Never expose your Personal Access Token to the browser. Instead, create a server-side API route that proxies messages to the Cosmic agent.

// app/api/chat/route.ts
import { NextRequest } from 'next/server';

const AGENT_ID = process.env.COSMIC_AGENT_ID!;
const AGENT_TOKEN = process.env.COSMIC_AGENT_TOKEN!;

export async function POST(req: NextRequest) {
  const { message, conversation_id } = await req.json();

  const response = await fetch(
    `https://dapi.cosmicjs.com/v3/ai/agents/${AGENT_ID}/messages`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${AGENT_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        message,
        conversation_id,
        stream: true,
      }),
    }
  );

  return new Response(response.body, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Build the Chat Widget Component

// components/ChatWidget.tsx
'use client';

import { useState, useRef, useEffect } from 'react';

type Message = {
  role: 'user' | 'assistant';
  content: string;
};

export function ChatWidget() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [conversationId, setConversationId] = useState<string | null>(null);
  const bottomRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  async function sendMessage() {
    if (!input.trim() || isLoading) return;

    const userMessage = input.trim();
    setInput('');
    setIsLoading(true);

    setMessages((prev) => [...prev, { role: 'user', content: userMessage }]);
    setMessages((prev) => [...prev, { role: 'assistant', content: '' }]);

    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: userMessage, conversation_id: conversationId }),
      });

      const reader = res.body!.getReader();
      const decoder = new TextDecoder();
      let fullText = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value);
        const lines = chunk.split('\n');

        for (const line of lines) {
          if (!line.startsWith('data: ')) continue;
          const data = JSON.parse(line.slice(6));

          if (data.type === 'info' && data.conversation_id) {
            setConversationId(data.conversation_id);
          }

          if (data.type === 'text_delta') {
            fullText = data.fullText;
            setMessages((prev) => [
              ...prev.slice(0, -1),
              { role: 'assistant', content: fullText },
            ]);
          }
        }
      }
    } catch (err) {
      console.error('Chat error:', err);
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <div className="chat-widget">
      <div className="messages">
        {messages.map((msg, i) => (
          <div key={i} className={`message ${msg.role}`}>
            {msg.content}
          </div>
        ))}
        <div ref={bottomRef} />
      </div>
      <div className="input-row">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Ask anything..."
          disabled={isLoading}
        />
        <button onClick={sendMessage} disabled={isLoading}>
          {isLoading ? 'Thinking...' : 'Send'}
        </button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Read Your CMS Content with the SDK

import { createBucketClient } from '@cosmicjs/sdk';

const cosmic = createBucketClient({
  bucketSlug: process.env.COSMIC_BUCKET_SLUG!,
  readKey: process.env.COSMIC_READ_KEY!,
});

export async function getFAQs() {
  const { objects } = await cosmic.objects
    .find({ type: 'faqs' })
    .props(['title', 'metadata.answer'])
    .limit(5);

  return objects;
}
Enter fullscreen mode Exit fullscreen mode

Get Started

Originally published at cosmicjs.com

Top comments (0)