Intermediate 25 min

UI Layout

The final UI has three main parts:

  1. Chat Panel (left) - User interacts here
  2. Trace Viewer (right top) - See agent steps
  3. Memory Inspector (right bottom) - View agent memory

Backend API

First, create an API endpoint:

from flask import Flask, request, jsonify
from agent import run_agent_conversation

app = Flask(__name__)

@app.route('/api/chat', methods=['POST'])
def chat():
    data = request.json
    user_message = data.get('message')
    user_id = data.get('user_id')
    
    try:
        result = run_agent_conversation(user_message, user_id)
        return jsonify({
            'success': True,
            'type': result['type'],
            'reply': result['reply'],
            'trace': result.get('trace', []),
            'memory': memory_store.get(user_id) if user_id else {}
        })
    except Exception as e:
        return jsonify({
            'success': False,
            'error': str(e)
        }), 500

Frontend: Chat Interface

Here’s a simple React component:

import { useState } from 'react';

function ChatInterface() {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [loading, setLoading] = useState(false);
  const [trace, setTrace] = useState([]);
  const [memory, setMemory] = useState({});
  
  const sendMessage = async () => {
    if (!input.trim()) return;
    
    const userMessage = {
      role: 'user',
      content: input
    };
    
    setMessages(prev => [...prev, userMessage]);
    setInput('');
    setLoading(true);
    
    try {
      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          message: input,
          user_id: 'user-123' // In real app, get from auth
        })
      });
      
      const data = await response.json();
      
      if (data.success) {
        setMessages(prev => [...prev, {
          role: 'assistant',
          content: data.reply
        }]);
        setTrace(data.trace);
        setMemory(data.memory);
      }
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div className="chat-container">
      <div className="chat-messages">
        {messages.map((msg, i) => (
          <div key={i} className={`message message-${msg.role}`}>
            {msg.content}
          </div>
        ))}
        {loading && <div className="loading">Thinking...</div>}
      </div>
      
      <div className="chat-input">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Type your message..."
        />
        <button onClick={sendMessage}>Send</button>
      </div>
    </div>
  );
}

Trace Viewer Component

Display the agent trace:

function TraceViewer({ trace }) {
  if (!trace || trace.length === 0) {
    return <div>No trace data yet</div>;
  }
  
  return (
    <div className="trace-viewer">
      <h3>Agent Trace</h3>
      {trace.map((step, i) => (
        <div key={i} className={`trace-step trace-${step.action}`}>
          <div className="step-header">
            <span>Step {step.step}</span>
            <span className="action-badge">{step.action}</span>
          </div>
          
          {step.tool_name && (
            <div className="tool-info">
              <strong>Tool:</strong> {step.tool_name}
              {step.tool_args && (
                <pre>{JSON.stringify(step.tool_args, null, 2)}</pre>
              )}
            </div>
          )}
          
          {step.final_reply && (
            <div className="reply">{step.final_reply}</div>
          )}
        </div>
      ))}
    </div>
  );
}

Memory Inspector Component

Show agent memory:

function MemoryInspector({ memory, userId }) {
  if (!memory || Object.keys(memory).length === 0) {
    return <div>No memory data</div>;
  }
  
  return (
    <div className="memory-inspector">
      <h3>Agent Memory</h3>
      <div className="user-id">User: {userId}</div>
      <table>
        <thead>
          <tr>
            <th>Key</th>
            <th>Value</th>
          </tr>
        </thead>
        <tbody>
          {Object.entries(memory).map(([key, value]) => (
            <tr key={key}>
              <td><code>{key}</code></td>
              <td>{JSON.stringify(value)}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Complete Layout

Put it all together:

function AgentPlayground() {
  const [messages, setMessages] = useState([]);
  const [trace, setTrace] = useState([]);
  const [memory, setMemory] = useState({});
  const userId = 'user-123';
  
  return (
    <div className="playground">
      <div className="left-panel">
        <ChatInterface
          onMessage={handleMessage}
          messages={messages}
        />
      </div>
      
      <div className="right-panel">
        <TraceViewer trace={trace} />
        <MemoryInspector memory={memory} userId={userId} />
      </div>
    </div>
  );
}

Error Handling

Handle errors gracefully:

@app.route('/api/chat', methods=['POST'])
def chat():
    try:
        # Validate input
        if not request.json or 'message' not in request.json:
            return jsonify({'error': 'Message required'}), 400
        
        user_message = request.json['message']
        if len(user_message) > 1000:
            return jsonify({'error': 'Message too long'}), 400
        
        # Rate limiting (simple version)
        # ... check rate limits ...
        
        # Run agent
        result = run_agent_conversation(
            user_message,
            request.json.get('user_id'),
            max_steps=5
        )
        
        return jsonify({
            'success': True,
            **result
        })
        
    except TimeoutError:
        return jsonify({
            'success': False,
            'error': 'Request timed out'
        }), 504
        
    except Exception as e:
        logger.error(f"Agent error: {e}")
        return jsonify({
            'success': False,
            'error': 'Internal server error'
        }), 500

Scenario Presets

Add preset buttons for testing:

const scenarios = [
  { label: 'FAQ', message: 'What is SimpleSaaS?' },
  { label: 'Subscription', message: 'What\'s my current plan?' },
  { label: 'Billing Issue', message: 'I want a refund for last month' }
];

function ScenarioPresets({ onSelect }) {
  return (
    <div className="scenario-presets">
      {scenarios.map((scenario) => (
        <button
          key={scenario.label}
          onClick={() => onSelect(scenario.message)}
        >
          {scenario.label}
        </button>
      ))}
    </div>
  );
}

Live Demo

Here’s what the final UI looks like:

SimpleSaaS Support

Online
Support agent is ready. Try the scenarios above or type your own message.
8:15:19 AM

Trace Example

When the agent runs, you’ll see a trace like this:

Agent Execution Trace

Step 1 thinking 10:23:15
Reasoning: User asked about subscription. Need to check their account.
Step 2 tool call 10:23:16
Tool: get_subscription_status
Arguments
{
  "user_id": "user-123"
}
Result
{
  "plan": "Pro",
  "status": "active",
  "expires": "2025-12-01"
}
Step 3 final answer 10:23:17
Reply: Your current plan is Pro, and it's active until December 1, 2025.

Memory Display

Memory updates as the agent learns:

Agent Memory

User ID: user-123
Key Value Updated
last_known_plan Pro 2025-11-24 10:23:16
last_interaction subscription_check 2025-11-24 10:23:17

Key Implementation Points

  1. Separate concerns - Chat, trace, and memory are separate components
  2. Real-time updates - Update UI as agent processes
  3. Error handling - Show errors clearly to users
  4. Loading states - Show when agent is thinking
  5. Responsive design - Works on mobile and desktop

What’s Next?

You’ve built a complete agent! The final page covers extensions, exercises, and reflection on what you’ve learned.