211 lines
5.3 KiB
TypeScript
211 lines
5.3 KiB
TypeScript
/**
|
|
* Cloudflare Workers + Hono + OpenAI Agents
|
|
*
|
|
* Demonstrates:
|
|
* - Integrating agents with Hono framework
|
|
* - Multiple agent endpoints
|
|
* - Streaming with Hono
|
|
* - Type-safe routing
|
|
*/
|
|
|
|
import { Hono } from 'hono';
|
|
import { cors } from 'hono/cors';
|
|
import { z } from 'zod';
|
|
import { Agent, run, tool } from '@openai/agents';
|
|
|
|
// ========================================
|
|
// Agents
|
|
// ========================================
|
|
|
|
const summarizerAgent = new Agent({
|
|
name: 'Summarizer',
|
|
instructions: 'Summarize text concisely in 2-3 sentences.',
|
|
model: 'gpt-4o-mini',
|
|
});
|
|
|
|
const translatorAgent = new Agent({
|
|
name: 'Translator',
|
|
instructions: 'Translate text accurately while preserving meaning and tone.',
|
|
model: 'gpt-4o-mini',
|
|
});
|
|
|
|
const analyzerTool = tool({
|
|
name: 'analyze_sentiment',
|
|
description: 'Analyze sentiment',
|
|
parameters: z.object({
|
|
text: z.string(),
|
|
}),
|
|
execute: async ({ text }) => {
|
|
// Simplified sentiment analysis
|
|
const positive = ['good', 'great', 'excellent', 'love', 'amazing'];
|
|
const negative = ['bad', 'terrible', 'hate', 'awful', 'poor'];
|
|
|
|
const lowerText = text.toLowerCase();
|
|
const posCount = positive.filter(w => lowerText.includes(w)).length;
|
|
const negCount = negative.filter(w => lowerText.includes(w)).length;
|
|
|
|
if (posCount > negCount) return 'Positive sentiment detected';
|
|
if (negCount > posCount) return 'Negative sentiment detected';
|
|
return 'Neutral sentiment detected';
|
|
},
|
|
});
|
|
|
|
const analyzerAgent = new Agent({
|
|
name: 'Analyzer',
|
|
instructions: 'Analyze text for sentiment, tone, and key themes.',
|
|
tools: [analyzerTool],
|
|
model: 'gpt-4o-mini',
|
|
});
|
|
|
|
// ========================================
|
|
// Hono App
|
|
// ========================================
|
|
|
|
type Bindings = {
|
|
OPENAI_API_KEY: string;
|
|
};
|
|
|
|
const app = new Hono<{ Bindings: Bindings }>();
|
|
|
|
// Enable CORS
|
|
app.use('/*', cors());
|
|
|
|
// Health check
|
|
app.get('/', (c) => {
|
|
return c.json({
|
|
service: 'OpenAI Agents API',
|
|
version: '1.0.0',
|
|
agents: ['summarizer', 'translator', 'analyzer'],
|
|
});
|
|
});
|
|
|
|
// ========================================
|
|
// Summarizer Endpoint
|
|
// ========================================
|
|
|
|
app.post('/api/summarize', async (c) => {
|
|
try {
|
|
const { text } = await c.req.json();
|
|
|
|
if (!text) {
|
|
return c.json({ error: 'Missing text parameter' }, 400);
|
|
}
|
|
|
|
// Set API key from environment
|
|
process.env.OPENAI_API_KEY = c.env.OPENAI_API_KEY;
|
|
|
|
const result = await run(summarizerAgent, text);
|
|
|
|
return c.json({
|
|
summary: result.finalOutput,
|
|
tokens: result.usage.totalTokens,
|
|
});
|
|
|
|
} catch (error: any) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
});
|
|
|
|
// ========================================
|
|
// Translator Endpoint
|
|
// ========================================
|
|
|
|
app.post('/api/translate', async (c) => {
|
|
try {
|
|
const { text, targetLanguage } = await c.req.json();
|
|
|
|
if (!text || !targetLanguage) {
|
|
return c.json({ error: 'Missing required parameters' }, 400);
|
|
}
|
|
|
|
process.env.OPENAI_API_KEY = c.env.OPENAI_API_KEY;
|
|
|
|
const result = await run(
|
|
translatorAgent,
|
|
`Translate the following to ${targetLanguage}: ${text}`
|
|
);
|
|
|
|
return c.json({
|
|
translation: result.finalOutput,
|
|
sourceLanguage: 'auto-detected',
|
|
targetLanguage,
|
|
tokens: result.usage.totalTokens,
|
|
});
|
|
|
|
} catch (error: any) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
});
|
|
|
|
// ========================================
|
|
// Analyzer Endpoint (with Streaming)
|
|
// ========================================
|
|
|
|
app.post('/api/analyze', async (c) => {
|
|
try {
|
|
const { text, stream = false } = await c.req.json();
|
|
|
|
if (!text) {
|
|
return c.json({ error: 'Missing text parameter' }, 400);
|
|
}
|
|
|
|
process.env.OPENAI_API_KEY = c.env.OPENAI_API_KEY;
|
|
|
|
// Non-streaming
|
|
if (!stream) {
|
|
const result = await run(analyzerAgent, `Analyze this text: ${text}`);
|
|
return c.json({
|
|
analysis: result.finalOutput,
|
|
tokens: result.usage.totalTokens,
|
|
});
|
|
}
|
|
|
|
// Streaming
|
|
const streamResult = await run(analyzerAgent, `Analyze: ${text}`, {
|
|
stream: true,
|
|
});
|
|
|
|
const { readable, writable } = new TransformStream();
|
|
const writer = writable.getWriter();
|
|
const encoder = new TextEncoder();
|
|
|
|
// Stream in background
|
|
(async () => {
|
|
try {
|
|
for await (const event of streamResult) {
|
|
if (event.type === 'raw_model_stream_event') {
|
|
const chunk = event.data?.choices?.[0]?.delta?.content || '';
|
|
if (chunk) {
|
|
await writer.write(encoder.encode(`data: ${JSON.stringify({ chunk })}\n\n`));
|
|
}
|
|
}
|
|
}
|
|
|
|
await streamResult.completed;
|
|
await writer.write(encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`));
|
|
} catch (error: any) {
|
|
await writer.write(encoder.encode(`data: ${JSON.stringify({ error: error.message })}\n\n`));
|
|
} finally {
|
|
await writer.close();
|
|
}
|
|
})();
|
|
|
|
return new Response(readable, {
|
|
headers: {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
},
|
|
});
|
|
|
|
} catch (error: any) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
});
|
|
|
|
// ========================================
|
|
// Export Worker
|
|
// ========================================
|
|
|
|
export default app;
|