Skip to content

Commit

Permalink
Feature/handle in sdk errors more gracefully (#55)
Browse files Browse the repository at this point in the history
* ✨ Allow SDKs to send system messages

* ✨ Send system messages when on chat message handler crashes
  • Loading branch information
aurelien-brabant committed Oct 31, 2023
1 parent 9d626cc commit e2eb1a5
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 51 deletions.
2 changes: 2 additions & 0 deletions sdks/node-sdk/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export class Agent {
format: localMessageFormatToRemote[format],
agentId: this.config.agentId,
attachments: uploadedAttachments,
source: 'AGENT',
type: 'TEXT',
});
}

Expand Down
33 changes: 31 additions & 2 deletions sdks/node-sdk/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,36 @@ import { OnChatMessageHandler, RawChatMessage } from "./types";
import { IncomingChatMessage } from "./incoming-chat-message";
import { Logger } from "./logger";
import { HttpApi } from "./http";
import { MessageFormat, SendMessageOptions, SendMessagePayload } from "./types"
import { localMessageFormatToRemote } from "./constants";

export interface ProjectConfig {
url: string;
projectId: string;
secret: string;
}


class System {
constructor(private readonly realtime: RealtimeClient) {}

send(
{ text, conversationId }: Omit<SendMessagePayload, 'attachments'>,
options: SendMessageOptions = {}
) {
const format: MessageFormat = options.format ?? 'PlainText';

this.realtime.emit('chat-message', {
text,
conversationId,
format: localMessageFormatToRemote[format],
attachments: [],
source: 'SYSTEM',
type: 'TEXT',
});
}
}

export class Project {
private readonly realtime: RealtimeClient;
private readonly isDebugEnabled: boolean = false;
Expand All @@ -21,6 +44,7 @@ export class Project {
name: 'Server',
})
private readonly http: HttpApi;
public readonly system: System;

constructor(
config: ProjectConfig,
Expand All @@ -42,6 +66,7 @@ export class Project {
});
})
this.http = new HttpApi(config);
this.system = new System(this.realtime);
}

async connect() {
Expand All @@ -52,12 +77,16 @@ export class Project {
onChatMessage(handler: OnChatMessageHandler) {
this.realtime.on('chat-message', async (data: any) => {
const rawChatMessage = data.data as RawChatMessage;
const message = new IncomingChatMessage(this.http, rawChatMessage);

try {
await handler(new IncomingChatMessage(this.http, rawChatMessage));
await handler(message);
} catch (err: any) {
this.clientLogger.error('onChatMessage: got uncaught exception while running handler. Consider handling errors in your handler directly.');
console.error(err);
this.system.send({
conversationId: message.conversationId,
text: 'An error occured while processing your request. Please try again.'
})
}
});
}
Expand Down
28 changes: 26 additions & 2 deletions sdks/python-sdk/agentlabs/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@
from typing import Any, Callable, Dict

from .agent import Agent
from .chat import IncomingChatMessage
from .chat import IncomingChatMessage, MessageFormat

from ._internals.http import HttpApi
from ._internals.logger import Logger
from ._internals.realtime import RealtimeClient

class System:
def __init__(self, realtime: RealtimeClient):
self._realtime = realtime

def send(self, text: str, conversation_id: str, format: MessageFormat = MessageFormat.PLAIN_TEXT):
"""Sends a system message to a conversation."""
self._realtime.emit('chat-message', {
"conversationId": conversation_id,
"text": text,
"source": "SYSTEM",
"format": format.value,
"attachments": []
})

class Project:
"""Represents a project on the AgentLabs server.
This class is used to instantiate a backend connection for
Expand Down Expand Up @@ -41,6 +55,7 @@ def __init__(self, agentlabs_url: str, project_id: str, secret: str) -> None:
secret=secret
)
self._realtime = RealtimeClient(project_id=project_id, secret=secret, url=agentlabs_url)
self.system = System(realtime=self._realtime)
self._realtime.on('message', self._log_message)
self._realtime.on('heartbeat', self._handle_hearbeat)

Expand All @@ -50,7 +65,16 @@ def on_chat_message(self, fn: Callable[[IncomingChatMessage], None]):
"""
def wrapper(payload: Any):
chat_message = IncomingChatMessage(self._http, message=payload['data'])
fn(chat_message)

try:
fn(chat_message)
except Exception as e:
self._client_logger.error("An uncaught exception occurred while executing the handler. In order to provide a better user experience, consider handling exceptions from inside your handler.")
self._client_logger.error(str(e))
self.system.send(
text="An error occured while processing your request. Please try again.",
conversation_id=chat_message.conversation_id
)

self._realtime.on('chat-message', wrapper)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MessageFormat } from '@prisma/client';
import { ChatMessageSource, MessageFormat } from '@prisma/client';
import { Type } from 'class-transformer';
import {
IsEnum,
Expand Down Expand Up @@ -30,7 +30,8 @@ class AgentMessageDataDto {
messageId: string;

@IsString()
agentId: string;
@IsOptional()
agentId?: string;

@IsEnum(MessageFormat)
format: MessageFormat;
Expand All @@ -39,6 +40,9 @@ class AgentMessageDataDto {
@Type(() => AttachmentDto)
attachments: AttachmentDto[];

@IsIn(['AGENT', 'SYSTEM'] satisfies ChatMessageSource[])
source: 'AGENT' | 'SYSTEM';

@IsIn(MessageTypes)
type: MessageType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ export class ProjectBackendConnectionGateway
@MessageBody()
payload: AgentMessageDto,
): Promise<BaseRealtimeMessageDto> {
if (!payload.data.agentId) {
return {
message: 'Agent id is missing',
timestamp: new Date().toISOString(),
data: {},
};
}

const conversation =
await this.conversationsService.findConversationByIdWithAgent(
payload.data.conversationId,
Expand Down Expand Up @@ -226,15 +234,9 @@ export class ProjectBackendConnectionGateway
@ConnectedSocket() client: Socket,
@MessageBody() payload: AgentMessageDto,
): Promise<BaseRealtimeMessageDto> {
const conversation =
await this.conversationsService.findConversationByIdWithAgent(
payload.data.conversationId,
);

if (!conversation) {
const message = `Conversation not found: ID=${payload.data.conversationId}`;

const error = (code: string, message: string): BaseRealtimeMessageDto => {
this.logger.error(message);

client.send({
message,
});
Expand All @@ -244,45 +246,49 @@ export class ProjectBackendConnectionGateway
timestamp: new Date().toISOString(),
data: {},
error: {
code: 'CONVERSATION_NOT_FOUND',
code,
message,
},
};
}

try {
await this.conversationMutexManager.acquire(conversation.id);
};

const isProjectAgent = await this.agentsService.isProjectAgent(
conversation.projectId,
payload.data.agentId,
const conversation =
await this.conversationsService.findConversationByIdWithAgent(
payload.data.conversationId,
);

if (!isProjectAgent) {
const message = `Message rejected: agent ${payload.data.agentId} is not an agent of project ${conversation.projectId}.`;
if (!conversation) {
return error(
'CONVERSATION_NOT_FOUND',
`Conversation not found: ID=${payload.data.conversationId}`,
);
}

client.send({
message,
});
try {
await this.conversationMutexManager.acquire(conversation.id);

return {
message,
timestamp: new Date().toISOString(),
data: {},
error: {
code: 'AGENT_NOT_FOUND',
message,
},
};
if (payload.data.agentId) {
const isProjectAgent = await this.agentsService.isProjectAgent(
conversation.projectId,
payload.data.agentId,
);

if (!isProjectAgent) {
return error(
'AGENT_NOT_FOUND',
`Message rejected: agent ${payload.data.agentId} is not an agent of project ${conversation.projectId}.`,
);
}
}

const message = await this.messagesService.createAgentMessage({
const message = await this.messagesService.createMessage({
conversationId: conversation.id,
text: payload.data.text,
format: payload.data.format,
agentId: payload.data.agentId,
type: payload.data.type,
metadata: payload.data.metadata,
source: payload.data.source,
});

const linkAttachmentPromises = payload.data.attachments.map(
Expand All @@ -299,19 +305,10 @@ export class ProjectBackendConnectionGateway
});

if (!frontendConnection) {
const message = `Frontend connection not found: MEMBER_ID=${conversation.memberId},PROJECT_ID=${conversation.projectId},AGENT_ID=${payload.data.agentId}`;

this.logger.error(message);

return {
message,
timestamp: new Date().toISOString(),
data: {},
error: {
code: 'FRONTEND_CONNECTION_NOT_FOUND',
message,
},
};
return error(
'FRONTEND_CONNECTION_NOT_FOUND',
`Frontend connection not found: MEMBER_ID=${conversation.memberId},PROJECT_ID=${conversation.projectId},AGENT_ID=${payload.data.agentId}`,
);
}

frontendConnection.socket.emit('chat-message', {
Expand All @@ -320,7 +317,7 @@ export class ProjectBackendConnectionGateway
conversationId: conversation.id,
text: payload.data.text,
format: payload.data.format,
source: 'AGENT',
source: payload.data.source,
messageId: message.id,
agentId: payload.data.agentId,
attachments: messageAttachments,
Expand Down

0 comments on commit e2eb1a5

Please sign in to comment.