mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-21 14:14:38 +00:00
Compare commits
1 Commits
kit/effect
...
enterprise
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc3fe8f4f5 |
@@ -11,6 +11,7 @@
|
||||
"start": "vite start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "2.0.89",
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -26,6 +27,7 @@
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"@stripe/stripe-js": "8.6.1",
|
||||
"ai": "catalog:",
|
||||
"chart.js": "4.5.1",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
|
||||
309
packages/console/app/src/lib/enterprise.ts
Normal file
309
packages/console/app/src/lib/enterprise.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { AWS } from "@opencode-ai/console-core/aws.js"
|
||||
import { generateObject } from "ai"
|
||||
import { z } from "zod"
|
||||
import { createLead } from "./salesforce"
|
||||
|
||||
const links = [
|
||||
{ label: "Docs", url: "https://opencode.ai/docs" },
|
||||
{ label: "Discord Community", url: "https://discord.gg/scN9YX6Fdd" },
|
||||
{ label: "GitHub", url: "https://github.com/anomalyco/opencode" },
|
||||
]
|
||||
|
||||
const from = "Stefan <stefan@anoma.ly>"
|
||||
const sign = "Stefan"
|
||||
|
||||
const shape = z.object({
|
||||
company: z.string().nullable().describe("Company name. Use null when unknown."),
|
||||
size: z
|
||||
.enum(["1-50", "51-250", "251-1000", "1001+"])
|
||||
.nullable()
|
||||
.describe("Company size bucket. Use null when unknown."),
|
||||
first: z.string().nullable().describe("First name only. Use null when unknown."),
|
||||
title: z.string().nullable().describe("Job title or role. Use null when unknown."),
|
||||
seats: z.number().int().positive().nullable().describe("Approximate seat count. Use null when unknown."),
|
||||
procurement: z
|
||||
.boolean()
|
||||
.describe("True when the inquiry is blocked on procurement, legal, vendor, security, or compliance review."),
|
||||
effort: z
|
||||
.enum(["low", "medium", "high"])
|
||||
.describe("Lead quality based on how specific and commercially relevant the inquiry is."),
|
||||
summary: z.string().nullable().describe("One sentence summary for the sales team. Use null when unnecessary."),
|
||||
})
|
||||
|
||||
const system = [
|
||||
"You triage inbound enterprise inquiries for OpenCode.",
|
||||
"Extract the fields from the form data and message.",
|
||||
"Do not invent facts. Use null when a field is unknown.",
|
||||
"First name should only contain the given name.",
|
||||
"Seats should only be set when the inquiry mentions or strongly implies a team, user, developer, or seat count.",
|
||||
"Procurement should be true when the inquiry mentions approval, review, legal, vendor, security, or compliance processes.",
|
||||
"Effort is low for vague or generic inquiries, medium for some business context, and high for strong buying intent, rollout scope, or blockers.",
|
||||
].join("\n")
|
||||
|
||||
export interface Inquiry {
|
||||
name: string
|
||||
role: string
|
||||
company?: string
|
||||
email: string
|
||||
phone?: string
|
||||
alias?: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export type Score = z.infer<typeof shape>
|
||||
|
||||
type Kind = "generic" | "procurement"
|
||||
type Mail = {
|
||||
subject: string
|
||||
text: string
|
||||
html: string
|
||||
}
|
||||
|
||||
function field(text?: string | null) {
|
||||
const value = text?.trim()
|
||||
if (!value) return null
|
||||
return value
|
||||
}
|
||||
|
||||
function safe(text: string) {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
function html(text: string) {
|
||||
return safe(text).replace(/\n/g, "<br>")
|
||||
}
|
||||
|
||||
export function fallback(input: Inquiry): Score {
|
||||
const text = [input.role, input.company, input.message].filter(Boolean).join("\n").toLowerCase()
|
||||
const procurement = /procurement|security|vendor|legal|approval|questionnaire|compliance/.test(text)
|
||||
const words = input.message.trim().split(/\s+/).filter(Boolean).length
|
||||
return {
|
||||
company: field(input.company),
|
||||
size: null,
|
||||
first: input.name.split(/\s+/)[0] ?? null,
|
||||
title: field(input.role),
|
||||
seats: null,
|
||||
procurement,
|
||||
effort: procurement ? "high" : words < 18 ? "low" : "medium",
|
||||
summary: null,
|
||||
}
|
||||
}
|
||||
|
||||
async function grade(input: Inquiry): Promise<Score> {
|
||||
const zen = createOpenAI({
|
||||
apiKey: "public",
|
||||
baseURL: "https://opencode.ai/zen/v1",
|
||||
})
|
||||
|
||||
return generateObject({
|
||||
model: zen.responses("gpt-5"),
|
||||
schema: shape,
|
||||
system,
|
||||
prompt: JSON.stringify(
|
||||
{
|
||||
name: input.name,
|
||||
role: input.role,
|
||||
company: field(input.company),
|
||||
email: input.email,
|
||||
phone: field(input.phone),
|
||||
message: input.message,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
})
|
||||
.then((result) => result.object)
|
||||
.catch((err) => {
|
||||
console.error("Failed to grade enterprise inquiry:", err)
|
||||
return fallback(input)
|
||||
})
|
||||
}
|
||||
|
||||
export function kind(score: Score): Kind | null {
|
||||
if (score.procurement) return "procurement"
|
||||
if (score.effort === "low") return "generic"
|
||||
return null
|
||||
}
|
||||
|
||||
function refs(kind: Kind) {
|
||||
const text = links.map(
|
||||
(item) => `${item.label}: ${item.url}${kind === "procurement" && item.label === "GitHub" ? " (MIT licensed)" : ""}`,
|
||||
)
|
||||
const markup = links
|
||||
.map(
|
||||
(item) =>
|
||||
`<li><a href="${item.url}">${safe(item.label)}</a>${kind === "procurement" && item.label === "GitHub" ? " (MIT licensed)" : ""}</li>`,
|
||||
)
|
||||
.join("")
|
||||
return { text, markup }
|
||||
}
|
||||
|
||||
export function reply(kind: Kind, name: string | null): Mail {
|
||||
const who = name ?? "there"
|
||||
const list = refs(kind)
|
||||
|
||||
if (kind === "generic") {
|
||||
return {
|
||||
subject: "Thanks for reaching out to OpenCode",
|
||||
text: [
|
||||
`Hi ${who},`,
|
||||
"",
|
||||
"Thanks for reaching out, we're happy to hear from you! We've received your message and are working through it. We're a small team doing our best to get back to everyone, so thank you for bearing with us.",
|
||||
"",
|
||||
"To help while you wait, here are some great places to start:",
|
||||
...list.text,
|
||||
"",
|
||||
"Hope you find what you need in there! Don't hesitate to reply if you have something more specific in mind.",
|
||||
"",
|
||||
"Best,",
|
||||
sign,
|
||||
].join("\n"),
|
||||
html: [
|
||||
`<p>Hi ${safe(who)},</p>`,
|
||||
"<p>Thanks for reaching out, we're happy to hear from you! We've received your message and are working through it. We're a small team doing our best to get back to everyone, so thank you for bearing with us.</p>",
|
||||
"<p>To help while you wait, here are some great places to start:</p>",
|
||||
`<ul>${list.markup}</ul>`,
|
||||
"<p>Hope you find what you need in there! Don't hesitate to reply if you have something more specific in mind.</p>",
|
||||
`<p>Best,<br>${safe(sign)}</p>`,
|
||||
].join(""),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subject: "OpenCode security and procurement notes",
|
||||
text: [
|
||||
`Hi ${who},`,
|
||||
"",
|
||||
"Thanks for reaching out! We're a small team working through messages as fast as we can, so thanks for bearing with us.",
|
||||
"",
|
||||
"A few notes that may help while this moves through security or procurement:",
|
||||
"- OpenCode is open source and MIT licensed.",
|
||||
"- Our managed offering is SOC 1 compliant.",
|
||||
"- Our managed offering is currently in the observation period for SOC 2.",
|
||||
"",
|
||||
"If anything is held up on the procurement or legal side, just reply and I'll get you whatever you need to keep things moving.",
|
||||
"",
|
||||
"To help while you wait, here are some great places to start:",
|
||||
...list.text,
|
||||
"",
|
||||
"Best,",
|
||||
sign,
|
||||
].join("\n"),
|
||||
html: [
|
||||
`<p>Hi ${safe(who)},</p>`,
|
||||
"<p>Thanks for reaching out! We're a small team working through messages as fast as we can, so thanks for bearing with us.</p>",
|
||||
"<p>A few notes that may help while this moves through security or procurement:</p>",
|
||||
"<ul><li>OpenCode is open source and MIT licensed.</li><li>Our managed offering is SOC 1 compliant.</li><li>Our managed offering is currently in the observation period for SOC 2.</li></ul>",
|
||||
"<p>If anything is held up on the procurement or legal side, just reply and I'll get you whatever you need to keep things moving.</p>",
|
||||
"<p>To help while you wait, here are some great places to start:</p>",
|
||||
`<ul>${list.markup}</ul>`,
|
||||
`<p>Best,<br>${safe(sign)}</p>`,
|
||||
].join(""),
|
||||
}
|
||||
}
|
||||
|
||||
function rows(input: Inquiry, score: Score, kind: Kind | null) {
|
||||
return [
|
||||
{ label: "Name", value: input.name },
|
||||
{ label: "Email", value: input.email },
|
||||
{ label: "Phone", value: field(input.phone) ?? "Unknown" },
|
||||
{ label: "Auto Reply", value: kind ?? "manual" },
|
||||
{ label: "Company", value: score.company ?? "Unknown" },
|
||||
{ label: "Company Size", value: score.size ?? "Unknown" },
|
||||
{ label: "First Name", value: score.first ?? "Unknown" },
|
||||
{ label: "Title", value: score.title ?? "Unknown" },
|
||||
{ label: "Seats", value: score.seats ? String(score.seats) : "Unknown" },
|
||||
{ label: "Procurement", value: score.procurement ? "Yes" : "No" },
|
||||
{ label: "Effort", value: score.effort },
|
||||
{ label: "Summary", value: score.summary ?? "None" },
|
||||
]
|
||||
}
|
||||
|
||||
function report(input: Inquiry, score: Score, kind: Kind | null): Mail {
|
||||
const list = rows(input, score, kind)
|
||||
return {
|
||||
subject: `Enterprise Inquiry from ${input.name}${kind ? ` (${kind})` : ""}`,
|
||||
text: [
|
||||
"New enterprise inquiry",
|
||||
"",
|
||||
...list.map((item) => `${item.label}: ${item.value}`),
|
||||
"",
|
||||
"Message:",
|
||||
input.message,
|
||||
].join("\n"),
|
||||
html: [
|
||||
"<p><strong>New enterprise inquiry</strong></p>",
|
||||
...list.map((item) => `<p><strong>${safe(item.label)}:</strong> ${html(item.value)}</p>`),
|
||||
`<p><strong>Message:</strong><br>${html(input.message)}</p>`,
|
||||
].join(""),
|
||||
}
|
||||
}
|
||||
|
||||
function note(input: Inquiry, score: Score, kind: Kind | null) {
|
||||
return [input.message, "", "---", ...rows(input, score, kind).map((item) => `${item.label}: ${item.value}`)].join(
|
||||
"\n",
|
||||
)
|
||||
}
|
||||
|
||||
export async function deliver(input: Inquiry) {
|
||||
const score = await grade(input)
|
||||
const next = kind(score)
|
||||
const msg = report(input, score, next)
|
||||
const auto = next ? reply(next, score.first) : null
|
||||
const jobs = [
|
||||
{
|
||||
name: "salesforce",
|
||||
job: createLead({
|
||||
name: input.name,
|
||||
role: score.title ?? input.role,
|
||||
company: score.company ?? field(input.company) ?? undefined,
|
||||
email: input.email,
|
||||
phone: field(input.phone) ?? undefined,
|
||||
message: note(input, score, next),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "internal",
|
||||
job: AWS.sendEmail({
|
||||
from,
|
||||
to: "contact@anoma.ly",
|
||||
subject: msg.subject,
|
||||
body: msg.text,
|
||||
html: msg.html,
|
||||
replyTo: input.email,
|
||||
}),
|
||||
},
|
||||
...(auto
|
||||
? [
|
||||
{
|
||||
name: "reply",
|
||||
job: AWS.sendEmail({
|
||||
from,
|
||||
to: input.email,
|
||||
subject: auto.subject,
|
||||
body: auto.text,
|
||||
html: auto.html,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
const out = await Promise.allSettled(jobs.map((item) => item.job))
|
||||
out.forEach((item, index) => {
|
||||
const name = jobs[index]!.name
|
||||
if (item.status === "rejected") {
|
||||
console.error(`Enterprise ${name} failed:`, item.reason)
|
||||
return
|
||||
}
|
||||
if (name === "salesforce" && !item.value) {
|
||||
console.error("Enterprise salesforce lead failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,23 +1,13 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { AWS } from "@opencode-ai/console-core/aws.js"
|
||||
import { waitUntil } from "@opencode-ai/console-resource"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
import { createLead } from "~/lib/salesforce"
|
||||
|
||||
interface EnterpriseFormData {
|
||||
name: string
|
||||
role: string
|
||||
company?: string
|
||||
email: string
|
||||
phone?: string
|
||||
alias?: string
|
||||
message: string
|
||||
}
|
||||
import { deliver, type Inquiry } from "~/lib/enterprise"
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
const dict = i18n(localeFromRequest(event.request))
|
||||
try {
|
||||
const body = (await event.request.json()) as EnterpriseFormData
|
||||
const body = (await event.request.json()) as Inquiry
|
||||
const trap = typeof body.alias === "string" ? body.alias.trim() : ""
|
||||
|
||||
if (trap) {
|
||||
@@ -33,45 +23,14 @@ export async function POST(event: APIEvent) {
|
||||
return Response.json({ error: dict["enterprise.form.error.invalidEmailFormat"] }, { status: 400 })
|
||||
}
|
||||
|
||||
const emailContent = `
|
||||
${body.message}<br><br>
|
||||
--<br>
|
||||
${body.name}<br>
|
||||
${body.role}<br>
|
||||
${body.company ? `${body.company}<br>` : ""}${body.email}<br>
|
||||
${body.phone ? `${body.phone}<br>` : ""}`.trim()
|
||||
|
||||
const [lead, mail] = await Promise.all([
|
||||
createLead({
|
||||
name: body.name,
|
||||
role: body.role,
|
||||
company: body.company,
|
||||
email: body.email,
|
||||
phone: body.phone,
|
||||
message: body.message,
|
||||
}),
|
||||
AWS.sendEmail({
|
||||
to: "contact@anoma.ly",
|
||||
subject: `Enterprise Inquiry from ${body.name}`,
|
||||
body: emailContent,
|
||||
replyTo: body.email,
|
||||
}).then(
|
||||
() => true,
|
||||
(err) => {
|
||||
console.error("Failed to send enterprise email:", err)
|
||||
return false
|
||||
},
|
||||
),
|
||||
])
|
||||
|
||||
if (!lead && !mail) {
|
||||
console.error("Enterprise inquiry delivery failed", { email: body.email })
|
||||
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
|
||||
}
|
||||
const job = deliver(body).catch((error) => {
|
||||
console.error("Error processing enterprise form:", error)
|
||||
})
|
||||
void waitUntil(job)
|
||||
|
||||
return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Error processing enterprise form:", error)
|
||||
console.error("Error reading enterprise form:", error)
|
||||
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
53
packages/console/app/test/enterprise.test.ts
Normal file
53
packages/console/app/test/enterprise.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { fallback, kind, reply, type Score } from "../src/lib/enterprise"
|
||||
|
||||
describe("enterprise lead routing", () => {
|
||||
test("routes procurement blockers to procurement reply", () => {
|
||||
const score = fallback({
|
||||
name: "Jane Doe",
|
||||
role: "CTO",
|
||||
company: "Acme",
|
||||
email: "jane@acme.com",
|
||||
message: "We're stuck in procurement, security review, and vendor approval through Coupa.",
|
||||
})
|
||||
|
||||
expect(score.procurement).toBe(true)
|
||||
expect(kind(score)).toBe("procurement")
|
||||
})
|
||||
|
||||
test("routes vague inquiries to the generic reply", () => {
|
||||
const score = fallback({
|
||||
name: "Jane Doe",
|
||||
role: "Engineer",
|
||||
email: "jane@example.com",
|
||||
message: "Can you tell me more about enterprise pricing?",
|
||||
})
|
||||
|
||||
expect(score.effort).toBe("low")
|
||||
expect(kind(score)).toBe("generic")
|
||||
})
|
||||
|
||||
test("keeps high intent leads for manual follow-up", () => {
|
||||
const score: Score = {
|
||||
company: "Acme",
|
||||
size: "1001+",
|
||||
first: "Jane",
|
||||
title: "CTO",
|
||||
seats: 500,
|
||||
procurement: false,
|
||||
effort: "high",
|
||||
summary: "Large rollout with clear buying intent.",
|
||||
}
|
||||
|
||||
expect(kind(score)).toBeNull()
|
||||
})
|
||||
|
||||
test("renders the procurement reply with security notes", () => {
|
||||
const mail = reply("procurement", "Jane")
|
||||
|
||||
expect(mail.subject).toContain("security")
|
||||
expect(mail.text).toContain("SOC 1 compliant")
|
||||
expect(mail.text).toContain("MIT licensed")
|
||||
expect(mail.html).toContain("Stefan")
|
||||
})
|
||||
})
|
||||
@@ -19,12 +19,17 @@ export namespace AWS {
|
||||
|
||||
export const sendEmail = fn(
|
||||
z.object({
|
||||
from: z.string().optional(),
|
||||
to: z.string(),
|
||||
subject: z.string(),
|
||||
body: z.string(),
|
||||
text: z.string().optional(),
|
||||
html: z.string().optional(),
|
||||
replyTo: z.string().optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
const text = input.text ?? input.body
|
||||
const html = input.html ?? input.body
|
||||
const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -32,7 +37,7 @@ export namespace AWS {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
FromEmailAddress: `OpenCode Zen <contact@anoma.ly>`,
|
||||
FromEmailAddress: input.from ?? "OpenCode Zen <contact@anoma.ly>",
|
||||
Destination: {
|
||||
ToAddresses: [input.to],
|
||||
},
|
||||
@@ -46,11 +51,11 @@ export namespace AWS {
|
||||
Body: {
|
||||
Text: {
|
||||
Charset: "UTF-8",
|
||||
Data: input.body,
|
||||
Data: text,
|
||||
},
|
||||
Html: {
|
||||
Charset: "UTF-8",
|
||||
Data: input.body,
|
||||
Data: html,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user