Skip to content

Commit 2a89c1a

Browse files
committed
update deps, make some progress on email
1 parent ebcbf52 commit 2a89c1a

File tree

18 files changed

+1192
-339
lines changed

18 files changed

+1192
-339
lines changed

.changeset/busy-bananas-boil.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"hono-agents": patch
3+
"agents-sdk": patch
4+
---
5+
6+
update deps

examples/playground/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"license": "ISC",
1313
"description": "",
1414
"dependencies": {
15-
"cronstrue": "^2.54.0"
15+
"cronstrue": "^2.56.0",
16+
"mimetext": "^3.0.27",
17+
"postal-mime": "^2.4.3"
1618
}
1719
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { AIChatAgent } from "agents-sdk/ai-chat-agent";
2+
import type { Env } from "../server";
3+
import PostalMime from "postal-mime";
4+
import { getAgentByName } from "agents-sdk";
5+
6+
import { createMimeMessage } from "mimetext";
7+
import { streamText, createDataStreamResponse } from "ai";
8+
import type { StreamTextOnFinishCallback } from "ai";
9+
import { createOpenAI } from "@ai-sdk/openai";
10+
import * as MockEmail from "../mock-cloudflare-email";
11+
export async function sendEmail(
12+
id: DurableObjectId,
13+
EMAIL: SendEmail,
14+
from: string,
15+
fromName: string,
16+
fromDomain: string,
17+
recipient: string,
18+
subject: string,
19+
contentType: string,
20+
body: string
21+
) {
22+
if (!EMAIL) {
23+
throw new Error("Email is not configured");
24+
}
25+
26+
const msg = createMimeMessage();
27+
msg.setSender({ name: fromName, addr: from });
28+
msg.setRecipient(recipient);
29+
msg.setSubject(subject);
30+
msg.addMessage({
31+
contentType: contentType,
32+
data: body,
33+
});
34+
msg.setHeader("Message-ID", `<${idToBase64(id)}@${fromDomain}>`);
35+
36+
// import this dynamically import { EmailMessage } from 'cloudflare:email'
37+
const { EmailMessage } = await import("cloudflare:email");
38+
console.log(`sending email from ${from} to ${recipient}`);
39+
await EMAIL.send(new EmailMessage(from, recipient, msg.asRaw()));
40+
41+
return "Email sent successfully!";
42+
}
43+
44+
export function idToBase64(id: DurableObjectId) {
45+
return Buffer.from(id.toString(), "hex").toString("base64");
46+
}
47+
48+
export function base64IDtoString(base64id: string) {
49+
return Buffer.from(base64id, "base64").toString("hex");
50+
}
51+
52+
async function createMockEmail(options: {
53+
id: string;
54+
from: string;
55+
name: string;
56+
to: string;
57+
subject: string;
58+
body: string;
59+
contentType: string;
60+
}) {
61+
const email = createMimeMessage();
62+
email.setSender({ name: options.name, addr: options.from });
63+
email.setRecipient(options.to);
64+
email.setSubject(options.subject);
65+
email.addMessage({
66+
contentType: options.contentType,
67+
data: options.body,
68+
});
69+
email.setHeader("Message-ID", `<${options.id}@${options.from}>`);
70+
const mockEmailMessage = new MockEmail.MockEmailMessage(
71+
options.from,
72+
options.to,
73+
email.asRaw()
74+
);
75+
return mockEmailMessage;
76+
}
77+
export class EmailAgent extends AIChatAgent<Env> {
78+
openai = createOpenAI({
79+
apiKey: this.env.OPENAI_API_KEY,
80+
});
81+
82+
async onRequest(request: Request) {
83+
const url = new URL(request.url);
84+
if (url.pathname.endsWith("/api/email") && request.method === "POST") {
85+
// this is dev mode, or we would have receievd it directly in onEmail
86+
// so let's redirect it there
87+
const body = await request.json<{
88+
from: string;
89+
to: string;
90+
message: string;
91+
}>();
92+
console.log("received email", body);
93+
const mockEmail = new MockEmail.MockForwardableEmailMessage(
94+
body.from,
95+
body.to,
96+
body.message
97+
);
98+
this.onEmail(mockEmail);
99+
return new Response("OK", { status: 200 });
100+
}
101+
102+
return super.onRequest(request);
103+
}
104+
105+
onEmail(email: ForwardableEmailMessage) {
106+
//
107+
}
108+
async onChatMessage(onFinish: StreamTextOnFinishCallback<any>) {
109+
const dataStreamResponse = createDataStreamResponse({
110+
execute: async (dataStream) => {
111+
const result = streamText({
112+
model: this.openai("gpt-4o"),
113+
messages: this.messages,
114+
onStepFinish: async (step) => {
115+
// if ([...this.getConnections()].length === 0) {
116+
if (true) {
117+
// send an email instead
118+
try {
119+
console.log("sending email", step.text);
120+
// we would replace this with a send_email call
121+
const mockEmail = await getAgentByName(
122+
this.env.MockEmailService,
123+
"default"
124+
);
125+
const emailToSend = await createMockEmail({
126+
id: this.ctx.id.toString(),
127+
128+
name: "emailAgent",
129+
130+
subject: "Email from emailAgent",
131+
body: step.text,
132+
contentType: "text/plain",
133+
});
134+
mockEmail
135+
.toInbox({
136+
from: emailToSend.from,
137+
to: emailToSend.to,
138+
message: emailToSend.message,
139+
})
140+
.catch((e) => {
141+
console.error("error sending email", e);
142+
});
143+
} catch (e) {
144+
console.error("error sending email", e);
145+
}
146+
}
147+
},
148+
onFinish,
149+
});
150+
151+
result.mergeIntoDataStream(dataStream);
152+
},
153+
});
154+
155+
return dataStreamResponse;
156+
}
157+
}
158+
159+
// export const emailHandler: EmailExportedHandler<Env> =
160+
// async function emailHandler(
161+
// email: ForwardableEmailMessage,
162+
// env: Env,
163+
// ctx: ExecutionContext
164+
// ) {
165+
// // @ts-ignore
166+
// console.log(Object.fromEntries(email.headers.entries()));
167+
// const parsed = await PostalMime.parse(email.raw);
168+
// console.log(parsed);
169+
170+
// const routingMatch = email.headers
171+
// .get("references")
172+
// ?.match(/<([A-Za-z0-9+\/]{43}=)@gmad.dev/);
173+
// console.log({
174+
// references: email.headers.get("references"),
175+
// do_match: routingMatch,
176+
// });
177+
178+
// if (routingMatch) {
179+
// const [_, base64id] = routingMatch;
180+
181+
// try {
182+
// const ns = env.Email;
183+
// const stub = ns.get(ns.idFromString(base64IDtoString(base64id)));
184+
// await stub.receiveEmail(
185+
// email.from,
186+
// email.to,
187+
// email.headers.get("subject")!,
188+
// parsed.text!
189+
// );
190+
// } catch (e) {
191+
// console.error(e);
192+
// }
193+
// }
194+
// };
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {
2+
Agent,
3+
type AgentContext,
4+
type Connection,
5+
type WSMessage,
6+
} from "agents-sdk";
7+
import * as MockEmail from "../mock-cloudflare-email";
8+
import PostalMime from "postal-mime";
9+
import { createMimeMessage } from "mimetext";
10+
import type { Email as PostalEmail } from "postal-mime";
11+
export class MockEmailService<Env> extends Agent<Env> {
12+
constructor(ctx: AgentContext, env: Env) {
13+
super(ctx, env);
14+
this.sql`
15+
CREATE TABLE IF NOT EXISTS emails (
16+
id TEXT PRIMARY KEY NOT NULL UNIQUE,
17+
_from TEXT,
18+
_to TEXT,
19+
_raw TEXT
20+
)
21+
`;
22+
}
23+
24+
async onConnect(connection: Connection) {
25+
const rawEmails = this.sql`
26+
SELECT * FROM emails
27+
`;
28+
const emails: PostalEmail[] = [];
29+
for (const email of rawEmails) {
30+
const parsed = await PostalMime.parse(email._raw);
31+
emails.push(parsed);
32+
}
33+
connection.send(
34+
JSON.stringify({
35+
type: "inbox:all",
36+
messages: emails,
37+
})
38+
);
39+
}
40+
41+
onMessage(connection: Connection, message: WSMessage) {
42+
console.log("onMessage", message);
43+
const parsed = JSON.parse(message as string);
44+
if (parsed.type === "send-email") {
45+
this.toOutbox(parsed.to, parsed.subject, parsed.text);
46+
} else if (parsed.type === "clear-emails") {
47+
this.sql`
48+
DELETE FROM emails
49+
`;
50+
this.broadcast(
51+
JSON.stringify({
52+
type: "inbox:all",
53+
messages: [],
54+
})
55+
);
56+
}
57+
}
58+
59+
async toInbox(email: {
60+
from: string;
61+
to: string;
62+
message: string;
63+
}): Promise<void> {
64+
console.log("toInbox", email);
65+
const [mail] = this.sql`
66+
INSERT INTO emails (id, _from, _to, _raw)
67+
VALUES (${crypto.randomUUID()}, ${email.from}, ${email.to}, ${
68+
email.message
69+
})
70+
RETURNING *
71+
`;
72+
const parsed = await PostalMime.parse(mail._raw);
73+
this.broadcast(
74+
JSON.stringify({
75+
type: "inbox:new-message",
76+
message: parsed,
77+
})
78+
);
79+
}
80+
81+
async toOutbox(to: string, subject: string, text: string): Promise<void> {
82+
console.log("toOutbox", to, subject, text);
83+
const email = createMimeMessage();
84+
email.setSender({ name: "The Man", addr: "[email protected]" });
85+
email.setRecipient(to);
86+
email.setSubject(subject);
87+
email.addMessage({
88+
contentType: "text/plain",
89+
data: text,
90+
});
91+
const raw = email.asRaw();
92+
93+
const [mail] = this.sql`
94+
INSERT INTO emails (_from, _to, _raw)
95+
VALUES (${"[email protected]"}, ${to}, ${raw})
96+
RETURNING *
97+
`;
98+
this.broadcast(
99+
JSON.stringify({
100+
type: "outbox:new-message",
101+
message: mail,
102+
})
103+
);
104+
105+
await fetch("http://localhost:5173/agents/email-agent/default/api/email", {
106+
method: "POST",
107+
headers: {
108+
"Content-Type": "application/json",
109+
},
110+
body: JSON.stringify({
111+
112+
to: to,
113+
message: raw,
114+
}),
115+
});
116+
}
117+
}

examples/playground/src/client.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
33
import { useState, useEffect } from "react";
44
import { Scheduler } from "./components/Scheduler";
55
import { Stateful } from "./components/Stateful";
6+
import Email from "./components/Email";
67

78
interface Toast {
89
id: string;
@@ -67,8 +68,13 @@ function App() {
6768
<Scheduler addToast={addToast} />
6869
</div>
6970
<div className="col-span-1">
71+
<h2 className="text-xl font-bold mb-4">State Sync Demo</h2>
7072
<Stateful addToast={addToast} />
7173
</div>
74+
<div className="col-span-1">
75+
<h2 className="text-xl font-bold mb-4">Email (wip)</h2>
76+
<Email addToast={addToast} />
77+
</div>
7278
</div>
7379
</div>
7480
);

0 commit comments

Comments
 (0)