Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desk/public/voice-demo.css

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions apps/desk/public/voice-demo.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions apps/desk/scripts/visual-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,20 @@ await run(['d1', 'migrations', 'apply', 'DB', '--local', '--persist-to', persist
await run(['d1', 'execute', 'DB', '--local', '--persist-to', persistence, '--file', seed, '--config', config])

const developmentEmail = process.env.MORROW_DEV_EMAIL ?? 'owner@example.com'
const optionalBindings = [
'SHOPIFY_SHOP_DOMAIN',
'SHOPIFY_ADMIN_TOKEN',
'SHOPIFY_CLIENT_ID',
'SHOPIFY_CLIENT_SECRET',
].flatMap((name) => process.env[name] ? ['--var', `${name}:${process.env[name]}`] : [])
const server = spawn(wrangler, [
'dev',
'--ip', '127.0.0.1',
'--port', String(port),
'--persist-to', persistence,
'--var', `MORROW_DEV_EMAIL:${developmentEmail}`,
'--var', 'MORROW_VOICE_DEMO_ENABLED:1',
...optionalBindings,
'--config', config,
], {
cwd: root,
Expand Down
4 changes: 3 additions & 1 deletion apps/desk/src/voice/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export type VoiceContactContinuation =

export const PRODUCT_HELP_CONTACT_CONTINUATION =
'I have shared my name and email. Ask me for my order number before opening a ticket.'
export const ORDER_LOOKUP_CONTACT_CONTINUATION =
'I have shared my name and email. Continue the order lookup using the order number I already gave you, or ask me for it if it is missing.'

/**
* Resume the exact agent flow interrupted by the in-thread identity card. These
Expand All @@ -45,7 +47,7 @@ export function contactContinuationMessage(value: unknown): string | null {
case 'open_ticket':
return 'I have shared my name and email. Continue opening the support ticket I requested.'
case 'order_lookup':
return 'I have shared my name and email. Continue the order lookup using the order number I already gave you, or ask me for it if it is missing.'
return ORDER_LOOKUP_CONTACT_CONTINUATION
case 'product_help':
return PRODUCT_HELP_CONTACT_CONTINUATION
default:
Expand Down
10 changes: 8 additions & 2 deletions apps/desk/src/voice/conversation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { PRODUCT_HELP_CONTACT_CONTINUATION } from './contact'
import {
ORDER_LOOKUP_CONTACT_CONTINUATION,
PRODUCT_HELP_CONTACT_CONTINUATION,
} from './contact'

export const VOICE_AGENT_MODEL = '@cf/zai-org/glm-4.7-flash'

Expand Down Expand Up @@ -26,7 +29,10 @@ function isSupportContinuation(
}

export function directVoiceResponse(transcript: string, messages: VoiceModelMessage[] = []): string | null {
if (transcript.trim() === PRODUCT_HELP_CONTACT_CONTINUATION) {
if (
transcript.trim() === PRODUCT_HELP_CONTACT_CONTINUATION
|| transcript.trim() === ORDER_LOOKUP_CONTACT_CONTINUATION
) {
const orderNumberAlreadyShared = messages
.slice(0, -1)
.some((message) => message.role === 'user' && ORDER_NUMBER_MENTION.test(message.content))
Expand Down
33 changes: 29 additions & 4 deletions apps/desk/src/voice/demo-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { voiceDemoEnabled } from './demo-page'
import {
hasVoiceContact,
normalizeVoiceContact,
ORDER_LOOKUP_CONTACT_CONTINUATION,
PRODUCT_HELP_CONTACT_CONTINUATION,
type VoiceContact,
} from './contact'
import {
Expand All @@ -36,7 +38,12 @@ import {
voiceAgentSystemPrompt,
} from './conversation'
import { shopifyConfigured } from '../integrations/shopify'
import { orderStatusForSession } from './orders'
import {
findOrderNumber,
isOrderLookupRequest,
orderStatusForSession,
orderStatusReply,
} from './orders'
import {
createVoiceVerification,
verifyVoiceVerificationCode,
Expand Down Expand Up @@ -221,7 +228,7 @@ export class MorrowDeskAgent extends VoiceAgent<Env> {
requestId: turnRequestId,
},
} satisfies VoiceConnectionState)
context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply' }))
context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply', reason: 'open_ticket' }))
return 'This needs a person from the team to review it. Add your email in the card below and I will open a ticket for you right away.'
}
try {
Expand All @@ -238,10 +245,28 @@ export class MorrowDeskAgent extends VoiceAgent<Env> {
}
}

const ordersAvailable = shopifyConfigured(this.env)
if (!contact && ordersAvailable && isOrderLookupRequest(transcript)) {
context.connection.setState({
...state,
pendingContactReason: 'order_lookup',
} satisfies VoiceConnectionState)
context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply', reason: 'order_lookup' }))
return 'To look up your order, add your name and the email used at checkout in the card below.'
}

const isOrderContinuation = transcript.trim() === ORDER_LOOKUP_CONTACT_CONTINUATION
|| transcript.trim() === PRODUCT_HELP_CONTACT_CONTINUATION
if (contact && ordersAvailable && isOrderContinuation) {
const orderNumber = findOrderNumber(messages.filter((message) => message.content.trim() !== transcript.trim()))
if (!orderNumber) return 'What is the order number from your confirmation email?'
const result = await orderStatusForSession(this.env, { email: contact.email }, orderNumber)
return orderStatusReply(result)
}

const directResponse = directVoiceResponse(transcript, messages)
if (directResponse) return directResponse

const ordersAvailable = shopifyConfigured(this.env)
const ordersEnabled = ordersAvailable && contact !== null
let contactCardRequested = false
const settings = await loadWorkspaceSettings(this.env.DB)
Expand Down Expand Up @@ -342,7 +367,7 @@ export class MorrowDeskAgent extends VoiceAgent<Env> {
...connectionState(context.connection),
pendingContactReason: continuationReason,
} satisfies VoiceConnectionState)
context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply' }))
context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply', reason: continuationReason }))
return reason === 'product_help'
? {
status: 'requested',
Expand Down
Loading
Loading