Skip to content

Instantly share code, notes, and snippets.

@elithrar
Created February 25, 2025 14:27
Show Gist options
  • Save elithrar/8237f0b5802f638790ab93b7433589d8 to your computer and use it in GitHub Desktop.
Save elithrar/8237f0b5802f638790ab93b7433589d8 to your computer and use it in GitHub Desktop.
Claude 3.7 + building Cloudflare Agents using the agents-sdk
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Cloudflare Agent Chat</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
#chat-container {
border: 1px solid #ccc;
border-radius: 5px;
height: 400px;
overflow-y: auto;
padding: 10px;
margin-bottom: 10px;
}
.message {
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 5px;
}
.user {
background-color: #e6f7ff;
text-align: right;
margin-left: 20%;
}
.assistant {
background-color: #f0f0f0;
margin-right: 20%;
}
#message-form {
display: flex;
gap: 10px;
}
#message-input {
flex: 1;
padding: 8px;
}
.typing-indicator {
color: #888;
font-style: italic;
}
</style>
</head>
<body>
<h1>Simple Agent Chat</h1>
<div>
<label for="name-input">Your Name:</label>
<input type="text" id="name-input" value="Guest">
<button id="update-name">Update</button>
</div>
<div id="connection-status">Connecting...</div>
<div id="chat-container"></div>
<form id="message-form">
<input type="text" id="message-input" placeholder="Type a message..." autocomplete="off">
<button type="submit">Send</button>
</form>
<script>
// Get the unique agent ID from URL or generate one
const agentId = new URL(window.location.href).searchParams.get('id') ||
`user-${Math.random().toString(36).substring(2, 10)}`;
// Add ID to URL without page reload
if (!window.location.href.includes('id=')) {
const url = new URL(window.location.href);
url.searchParams.set('id', agentId);
window.history.pushState({}, '', url);
}
// Set up WebSocket connection to agent
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/agents/my-simple-agent/${agentId}`;
let socket;
let reconnectAttempts = 0;
function connectWebSocket() {
socket = new WebSocket(wsUrl);
socket.onopen = () => {
document.getElementById('connection-status').textContent = 'Connected';
document.getElementById('connection-status').style.color = 'green';
reconnectAttempts = 0;
};
socket.onclose = () => {
document.getElementById('connection-status').textContent = 'Disconnected';
document.getElementById('connection-status').style.color = 'red';
// Attempt to reconnect with exponential backoff
if (reconnectAttempts < 5) {
const timeout = Math.pow(2, reconnectAttempts) * 1000;
reconnectAttempts++;
setTimeout(connectWebSocket, timeout);
}
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
let typingIndicator = null;
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
switch (data.type) {
case 'state_update':
// Render existing chat history
const chatContainer = document.getElementById('chat-container');
chatContainer.innerHTML = '';
data.state.messages.forEach(message => {
addMessageToUI(message.role, message.content);
});
// Update name input
document.getElementById('name-input').value = data.state.userName;
break;
case 'chunk':
// Handle streaming message chunks
if (!typingIndicator) {
typingIndicator = document.createElement('div');
typingIndicator.className = 'message assistant typing-indicator';
typingIndicator.textContent = '';
document.getElementById('chat-container').appendChild(typingIndicator);
}
typingIndicator.textContent += data.content;
document.getElementById('chat-container').scrollTop = document.getElementById('chat-container').scrollHeight;
break;
case 'done':
// Finalize streaming message
if (typingIndicator) {
typingIndicator.className = 'message assistant';
typingIndicator = null;
}
break;
case 'error':
console.error('Agent returned error:', data.message);
addMessageToUI('assistant', `Error: ${data.message}`);
break;
case 'name_updated':
console.log(`Name updated to: ${data.name}`);
break;
}
} catch (error) {
console.error('Error processing message:', error);
}
};
}
function addMessageToUI(role, content) {
const chatContainer = document.getElementById('chat-container');
const messageElement = document.createElement('div');
messageElement.className = `message ${role}`;
messageElement.textContent = content;
chatContainer.appendChild(messageElement);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// Handle form submission
document.getElementById('message-form').addEventListener('submit', (event) => {
event.preventDefault();
const messageInput = document.getElementById('message-input');
const message = messageInput.value.trim();
if (message && socket.readyState === WebSocket.OPEN) {
// Add message to UI immediately
addMessageToUI('user', message);
// Send to agent
socket.send(JSON.stringify({
type: 'chat_message',
content: message
}));
// Clear input
messageInput.value = '';
}
});
// Handle name updates
document.getElementById('update-name').addEventListener('click', () => {
const nameInput = document.getElementById('name-input');
const name = nameInput.value.trim();
if (name && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: 'set_name',
name: name
}));
}
});
// Initial connection
connectWebSocket();
</script>
</body>
</html>
import { Agent, Connection, ConnectionContext, WSMessage, routeAgentRequest } from "agents-sdk";
import { OpenAI } from "openai";
// Define environment type with our needed bindings
interface Env {
OPENAI_API_KEY: string;
MySimpleAgent: any;
}
// Define chat message type for our state
interface ChatMessage {
role: "user" | "assistant";
content: string;
timestamp: number;
}
// Define our agent state structure
interface AgentState {
messages: ChatMessage[];
userName: string;
}
// Our Agent implementation
export class MySimpleAgent extends Agent<Env, AgentState> {
// Initialize state when the agent is first created
async onStart() {
if (!this.state) {
await this.setState({
messages: [],
userName: "Guest",
});
}
console.log('Agent started with ID:', this.id);
}
// Handle WebSocket connections
async onConnect(connection: Connection, ctx: ConnectionContext) {
console.log("New client connected:", connection.id);
// You could authenticate users here with tokens from request headers
// const token = ctx.request.headers.get('Authorization');
// Accept the connection
connection.accept();
// Send the current state to the newly connected client
connection.send(JSON.stringify({
type: "state_update",
state: this.state
}));
}
// Handle incoming WebSocket messages
async onMessage(connection: Connection, message: WSMessage) {
try {
const data = JSON.parse(message as string);
// Handle different message types
switch (data.type) {
case "chat_message":
await this.handleChatMessage(connection, data.content);
break;
case "set_name":
await this.updateUserName(connection, data.name);
break;
default:
connection.send(JSON.stringify({
type: "error",
message: "Unknown message type"
}));
}
} catch (error) {
console.error("Error processing message:", error);
connection.send(JSON.stringify({
type: "error",
message: "Failed to process message"
}));
}
}
// Handle WebSocket errors
async onError(connection: Connection, error: unknown): Promise<void> {
console.error(`WebSocket error for client ${connection.id}:`, error);
}
// Handle WebSocket close
async onClose(connection: Connection, code: number, reason: string, wasClean: boolean): Promise<void> {
console.log(`Client ${connection.id} disconnected. Code: ${code}, Reason: ${reason}, Clean: ${wasClean}`);
}
// Handle HTTP requests directly
async onRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
// Simple API endpoint to get chat history
if (url.pathname.endsWith('/history')) {
return Response.json({
messages: this.state.messages
});
}
// Default response
return new Response("Agent is running. Connect via WebSocket for chat functionality.", {
headers: { "Content-Type": "text/plain" }
});
}
// Handle state updates
async onStateUpdate(state: AgentState): Promise<void> {
console.log("State updated:", state);
}
// Custom method to handle chat messages
private async handleChatMessage(connection: Connection, content: string) {
// Add the user message to state
const userMessage: ChatMessage = {
role: "user",
content,
timestamp: Date.now()
};
// Update state with new user message
await this.setState({
...this.state,
messages: [...this.state.messages, userMessage]
});
// Generate a response using OpenAI
try {
const client = new OpenAI({
apiKey: this.env.OPENAI_API_KEY,
});
// Start a stream for real-time responses
const stream = await client.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{ role: "system", content: `You are a helpful assistant named SimpleAgent. You're chatting with ${this.state.userName}.` },
...this.state.messages.map(msg => ({ role: msg.role, content: msg.content }))
],
stream: true,
});
// Buffer to accumulate the complete response
let assistantResponse = '';
// Stream chunks back to the client
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
assistantResponse += content;
// Send streaming updates
connection.send(JSON.stringify({
type: "chunk",
content
}));
}
}
// Create the complete message
const assistantMessage: ChatMessage = {
role: "assistant",
content: assistantResponse,
timestamp: Date.now()
};
// Update state with assistant response
await this.setState({
...this.state,
messages: [...this.state.messages, assistantMessage]
});
// Send completion signal
connection.send(JSON.stringify({
type: "done"
}));
} catch (error) {
console.error("Error generating response:", error);
connection.send(JSON.stringify({
type: "error",
message: "Failed to generate response"
}));
}
}
// Custom method to update the user name
private async updateUserName(connection: Connection, name: string) {
if (typeof name === 'string' && name.trim()) {
await this.setState({
...this.state,
userName: name.trim()
});
connection.send(JSON.stringify({
type: "name_updated",
name: name.trim()
}));
} else {
connection.send(JSON.stringify({
type: "error",
message: "Invalid name provided"
}));
}
}
// Helper method to schedule a reminder
async scheduleReminder(minutes: number, message: string) {
await this.schedule(minutes * 60, "sendReminder", { message });
}
// Method called by the scheduler
async sendReminder(data: { message: string }) {
// Add reminder to chat history
const reminderMessage: ChatMessage = {
role: "assistant",
content: `REMINDER: ${data.message}`,
timestamp: Date.now()
};
await this.setState({
...this.state,
messages: [...this.state.messages, reminderMessage]
});
// Broadcast to all connected clients
// In a real implementation, you would track active connections
}
}
// Worker entry point
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Route requests to the appropriate agent instance
return (await routeAgentRequest(request, env)) ||
new Response("Not Found", { status: 404 });
}
};
{
"$schema": "https://json.schemastore.org/wrangler.json",
"name": "my-simple-agent",
"main": "src/index.ts",
"compatibility_date": "2024-03-01",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "MySimpleAgent",
"class_name": "MySimpleAgent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_classes": ["MySimpleAgent"],
"new_sqlite_classes": ["MySimpleAgent"]
}
],
"vars": {
"OPENAI_API_KEY": ""
},
"observability": {
"enabled": true
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment