diff --git a/src/apis/sms.ts b/src/apis/sms.ts new file mode 100644 index 00000000..cef21cd8 --- /dev/null +++ b/src/apis/sms.ts @@ -0,0 +1,68 @@ +import { context, SpanStatusCode, trace } from '@opentelemetry/api'; +import type { SmsReply } from '../io/sms'; +import { getTracer, recordException } from '../router/router'; +import { safeStringify } from '../server/util'; +import type { AgentContext, AgentRequest, SMSService } from '../types'; +import { POST } from './api'; + +export default class SmsApi implements SMSService { + async send( + _req: AgentRequest, + ctx: AgentContext, + to: string[], + message: SmsReply, + from?: string + ): Promise { + const timeout = 15_000; + const tracer = getTracer(); + const currentContext = context.active(); + + const span = tracer.startSpan('agentuity.sms.send', {}, currentContext); + + try { + const spanContext = trace.setSpan(currentContext, span); + + return await context.with(spanContext, async () => { + span.setAttribute('@agentuity/agentId', ctx.agent.id); + const resp = await POST( + '/sms/send', + safeStringify({ + agentId: ctx.agent.id, + from: from, + to: to, + message: message.text, + }), + { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + timeout + ); + if (resp.status === 200) { + span.setStatus({ code: SpanStatusCode.OK }); + return; + } + throw new Error( + `error sending sms: ${resp.response.statusText} (${resp.response.status}): ${resp.json}` + ); + }); + } catch (ex) { + recordException(span, ex); + throw ex; + } finally { + span.end(); + } + } + + /** + * @deprecated Use reply on data.email.sms() + */ + async sendReply( + _agentId: string, + _phoneNumber: string, + _authToken: string, + _messageId: string + ): Promise { + throw new Error('reply not supported in context'); + } +} diff --git a/src/io/sms.ts b/src/io/sms.ts index 2b2d999e..bc426d70 100644 --- a/src/io/sms.ts +++ b/src/io/sms.ts @@ -13,7 +13,7 @@ type TwilioResponse = { }; /** - * A reply to an email + * A reply to an SMS */ export interface SmsReply { /** @@ -54,7 +54,63 @@ export class Sms { return this._message.Body; } + async send( + req: AgentRequest, + ctx: AgentContext, + to: string[], + message: SmsReply, + from?: string + ) { + const timeout = 15_000; + const tracer = getTracer(); + const currentContext = context.active(); + + const authToken = req.metadata?.['twilio-auth-token'] as string; + if (!authToken) { + throw new Error( + 'twilio authorization token is required but not found in metadata' + ); + } + + const span = tracer.startSpan('agentuity.twilio.send', {}, currentContext); + + try { + const spanContext = trace.setSpan(currentContext, span); + + return await context.with(spanContext, async () => { + span.setAttribute('@agentuity/agentId', ctx.agent.id); + const resp = await POST( + '/sms/send', + safeStringify({ + agentId: ctx.agent.id, + from: from, + to: to, + message: message.text, + }), + { + 'Content-Type': 'application/json', + }, + timeout, + authToken + ); + if (resp.status === 200) { + span.setStatus({ code: SpanStatusCode.OK }); + return; + } + throw new Error( + `error sending sms: ${resp.response.statusText} (${resp.response.status})` + ); + }); + } catch (ex) { + recordException(span, ex); + throw ex; + } finally { + span.end(); + } + } + async sendReply(req: AgentRequest, ctx: AgentContext, reply: string) { + const timeout = 15_000; const tracer = getTracer(); const currentContext = context.active(); @@ -87,7 +143,7 @@ export class Sms { 'Content-Type': 'application/json', 'X-Agentuity-Message-Id': this.messageId, }, - undefined, + timeout, authToken ); if (resp.status === 200) { @@ -120,3 +176,4 @@ export async function parseSms(data: Buffer): Promise { ); } } + diff --git a/src/server/server.ts b/src/server/server.ts index ce204870..a9d26c1e 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -7,6 +7,7 @@ import KeyValueAPI from '../apis/keyvalue'; import ObjectStoreAPI from '../apis/objectstore'; import PatchPortal from '../apis/patchportal'; import PromptAPI from '../apis/prompt/index.js'; +import SmsAPI from '../apis/sms'; import StreamAPIImpl from '../apis/stream'; import VectorAPI from '../apis/vector'; import type { Logger } from '../logger'; @@ -182,6 +183,7 @@ const email = new EmailAPI(); const discord = new DiscordAPI(); const objectstore = new ObjectStoreAPI(); const prompts = new PromptAPI(); +const sms = new SmsAPI(); // PatchPortal will be initialized lazily since it's async let patchportal: PatchPortal | null = null; @@ -222,6 +224,7 @@ export async function createServerContext( discord, objectstore, patchportal, + sms, sdkVersion: req.sdkVersion, agents: req.agents, scope: 'local', diff --git a/src/types.ts b/src/types.ts index 1c27d09a..b4de46d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -724,6 +724,17 @@ export interface SMSService { /** * send an SMS to a phone number */ + send( + req: AgentRequest, + context: AgentContext, + to: string[], + message: import('./io/sms').SmsReply, + from?: string + ): Promise; + + /** + * send an SMS reply to an incoming SMS message + */ sendReply( agentId: string, phoneNumber: string, @@ -1059,6 +1070,11 @@ export interface AgentContext { */ slack: SlackService; + /** + * the sms service + */ + sms: SMSService; + /** * EXPERIMENTAL: prompts API for accessing and compiling prompts */ @@ -1351,4 +1367,4 @@ export interface DataPayload { * the metadata */ metadata?: JsonObject; -} +} \ No newline at end of file