|
| 1 | +import { Hono } from 'hono'; |
| 2 | +import { cors } from 'hono/cors'; |
| 3 | +import { applyPermissionlessAgentSessionRouter } from '@nullshot/agent'; |
| 4 | +import { AiSdkAgent, AIUISDKMessage } from '@nullshot/agent/aisdk'; |
| 5 | +import { Service } from '@nullshot/agent'; |
| 6 | +import { ToolboxService } from '@nullshot/agent/services'; |
| 7 | +import { createWorkersAI } from 'workers-ai-provider'; |
| 8 | + |
| 9 | +// Minimal agent that echoes a short response via Workers AI |
| 10 | +export class QueueAgent extends AiSdkAgent<Env> { |
| 11 | + constructor(state: DurableObjectState, env: Env) { |
| 12 | + // If USE_MOCK_AI is enabled, we don't require the Workers AI binding |
| 13 | + let model: any; |
| 14 | + if (env.USE_MOCK_AI === 'true') { |
| 15 | + // Provide a dummy model; processMessage will short-circuit in mock mode |
| 16 | + model = {} as any; |
| 17 | + } else { |
| 18 | + if (!env.AI) throw new Error('AI binding missing. Configure Workers AI in wrangler.jsonc'); |
| 19 | + const workersai = createWorkersAI({ binding: env.AI }); |
| 20 | + model = workersai('@cf/meta/llama-3.1-8b-instruct' as any); |
| 21 | + } |
| 22 | + const services: Service[] = [new ToolboxService(env)]; |
| 23 | + super(state, env, model, services); |
| 24 | + } |
| 25 | + |
| 26 | + async processMessage(sessionId: string, messages: AIUISDKMessage): Promise<Response> { |
| 27 | + // Mock mode: return deterministic response without calling Workers AI |
| 28 | + if (this.env.USE_MOCK_AI === 'true') { |
| 29 | + const last = messages.messages[messages.messages.length - 1]; |
| 30 | + const userText = typeof last?.content === 'string' ? last.content : 'Hello'; |
| 31 | + const reply = `Mock response: ${userText}`; |
| 32 | + return new Response(reply, { headers: { 'Content-Type': 'text/plain' } }); |
| 33 | + } |
| 34 | + |
| 35 | + const result = await this.streamTextWithMessages(sessionId, messages.messages, { |
| 36 | + system: 'You are a helpful assistant. Keep responses concise.', |
| 37 | + maxSteps: 5, |
| 38 | + }); |
| 39 | + return result.toTextStreamResponse(); |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +// Hono app for producer and agent gateway |
| 44 | +const app = new Hono<{ Bindings: Env }>(); |
| 45 | +app.use('*', cors()); |
| 46 | + |
| 47 | +// Simple enqueue endpoint: { sessionId, messages } |
| 48 | +app.post('/enqueue', async (c) => { |
| 49 | + const body = await c.req.json<any>(); |
| 50 | + const sessionId: string = body.sessionId || crypto.randomUUID(); |
| 51 | + const messages = body.messages || [{ role: 'user', content: 'Hello!' }]; |
| 52 | + |
| 53 | + await c.env.REQUEST_QUEUE.send({ sessionId, messages }); |
| 54 | + |
| 55 | + return c.json({ enqueued: true, sessionId }); |
| 56 | +}); |
| 57 | + |
| 58 | +// Fetch latest result for session |
| 59 | +app.get('/result/:sessionId', async (c) => { |
| 60 | + const sessionId = c.req.param('sessionId'); |
| 61 | + const value = await c.env.RESULTS_KV.get(`result:${sessionId}`); |
| 62 | + if (!value) return c.json({ result: null }, 200); |
| 63 | + return c.json({ result: value }, 200); |
| 64 | +}); |
| 65 | + |
| 66 | +// Route /agent/chat/:sessionId to the DO agent |
| 67 | +applyPermissionlessAgentSessionRouter(app); |
| 68 | + |
| 69 | +export default { |
| 70 | + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { |
| 71 | + return app.fetch(request, env, ctx); |
| 72 | + }, |
| 73 | + |
| 74 | + // Queue consumer: run messages through the Agent DO |
| 75 | + async queue(batch: MessageBatch<any>, env: Env, ctx: ExecutionContext) { |
| 76 | + for (const msg of batch.messages) { |
| 77 | + try { |
| 78 | + const { sessionId, messages } = msg.body || {}; |
| 79 | + if (!sessionId || !messages) { |
| 80 | + console.warn('Invalid queue message, skipping'); |
| 81 | + continue; |
| 82 | + } |
| 83 | + const id = env.AGENT.idFromName(sessionId); |
| 84 | + const req = new Request('https://internal/agent/chat/' + sessionId, { |
| 85 | + method: 'POST', |
| 86 | + headers: { 'Content-Type': 'application/json' }, |
| 87 | + body: JSON.stringify({ id: crypto.randomUUID(), messages }), |
| 88 | + }); |
| 89 | + // Synchronously fetch the agent and persist full text to KV for retrieval |
| 90 | + const resp = await env.AGENT.get(id).fetch(req); |
| 91 | + const text = await resp.text(); |
| 92 | + ctx.waitUntil( |
| 93 | + env.RESULTS_KV.put(`result:${sessionId}`, text, { |
| 94 | + expirationTtl: 60 * 60, |
| 95 | + }), |
| 96 | + ); |
| 97 | + } catch (e) { |
| 98 | + console.error('Queue processing error:', e); |
| 99 | + throw e; |
| 100 | + } |
| 101 | + } |
| 102 | + }, |
| 103 | +}; |
0 commit comments