diff --git a/frontend/src/lib/components/chat/chat-message/ChatMessage.svelte b/frontend/src/lib/components/chat/chat-message/ChatMessage.svelte index 9e45b38..a61aefd 100644 --- a/frontend/src/lib/components/chat/chat-message/ChatMessage.svelte +++ b/frontend/src/lib/components/chat/chat-message/ChatMessage.svelte @@ -4,10 +4,12 @@ import MarkdownRenderer from "$lib/components/markdown/markdown-renderer.svelte"; import AgentIcon from "$lib/assets/img/agent-icon.svg"; import { authStore } from "$lib/stores/auth"; + import type { ChatMessageFormat } from "$lib/stores/chat"; export let time: string; export let body: string; export let from: "USER" | "AGENT" | "SYSTEM"; + export let format: ChatMessageFormat; let bubbleClass: string; @@ -37,7 +39,7 @@ {time}
- {#if from !== "USER"} + {#if format === "MARKDOWN"} {:else} {body} diff --git a/frontend/src/lib/components/chat/chat.svelte b/frontend/src/lib/components/chat/chat.svelte index 5b275eb..d2ba74a 100644 --- a/frontend/src/lib/components/chat/chat.svelte +++ b/frontend/src/lib/components/chat/chat.svelte @@ -59,7 +59,8 @@ addMessage({ id: uuidv4(), // this is a fake id, the real id will be set by the server ...payload.data, - createdAt: timestamp + createdAt: timestamp, + format: 'PLAIN_TEXT' }); isWaitingForAnswer = true; @@ -108,12 +109,15 @@ shouldRedirectToConversation = false; } + console.log(payload); + isWaitingForAnswer = false; addMessage({ id: payload.data.messageId, text: payload.data.text, source: payload.data.source, - createdAt: payload.timestamp + createdAt: payload.timestamp, + format: payload.data.format }); } @@ -128,7 +132,8 @@ id: payload.data.messageId, text: payload.data.text, source: 'AGENT', - createdAt: payload.timestamp + createdAt: payload.timestamp, + format: payload.data.format }); } @@ -186,7 +191,9 @@ + body={message.text} + format={message.format} + />
{/each} diff --git a/frontend/src/lib/stores/chat.ts b/frontend/src/lib/stores/chat.ts index aea165b..ee210ac 100644 --- a/frontend/src/lib/stores/chat.ts +++ b/frontend/src/lib/stores/chat.ts @@ -1,10 +1,18 @@ import { writable } from "svelte/store"; +export const ChatMessageFormats = [ + 'PLAIN_TEXT', + 'MARKDOWN', +] as const; + +export type ChatMessageFormat = typeof ChatMessageFormats[number]; + export interface ChatMessage { id: string; text: string; source: 'USER' | 'AGENT' | 'SYSTEM'; createdAt: string; + format: ChatMessageFormat; } export interface ChatStore { diff --git a/sdks/node-sdk/src/agent.ts b/sdks/node-sdk/src/agent.ts index bf0a249..1c2efdf 100644 --- a/sdks/node-sdk/src/agent.ts +++ b/sdks/node-sdk/src/agent.ts @@ -12,13 +12,26 @@ export interface RawChatMessage { memberId: string; } +export interface ReplyMessageOptions { + format?: MessageFormat; +} + +const MessageFormats = [ + 'PLAIN_TEXT', + 'MARKDOWN', +] as const; + +export type MessageFormat = typeof MessageFormats[number]; + export type OnChatMessageHandler = (message: IncomingChatMessage) => void; class StreamedMessage { private readonly messageId = randomUUID() private isEnded = false; + private format: MessageFormat; - constructor(private readonly io: Socket, private readonly conversationId: string) { + constructor(private readonly io: Socket, private readonly conversationId: string, options: ReplyMessageOptions) { + this.format = options.format ?? 'PLAIN_TEXT'; } /** @@ -37,6 +50,7 @@ class StreamedMessage { attachments: [], conversationId: this.conversationId, messageId: this.messageId, + format: this.format } }) } @@ -77,21 +91,24 @@ class IncomingChatMessage { * Such reply is intended to be read by the user in realtime. * If you are looking to send the entire message at once, use `reply` instead. */ - streamedReply() { - return new StreamedMessage(this.io, this.conversationId); + streamedReply(options: ReplyMessageOptions = {}) { + return new StreamedMessage(this.io, this.conversationId, options); } /** * Reply to the message, sending the entire message at once. * If you are looking to stream the reply as multiple parts, use `streamedReply` instead. */ - reply(message: string) { + reply(message: string, options: ReplyMessageOptions = {}) { + const format = options.format ?? 'PLAIN_TEXT'; + this.io.emit('chat-message', { timestamp: new Date().toISOString(), data: { text: message, attachments: [], conversationId: this.conversationId, + format } }); } diff --git a/sdks/python-sdk/agentlabs/agent.py b/sdks/python-sdk/agentlabs/agent.py index 0e38311..9f0e294 100644 --- a/sdks/python-sdk/agentlabs/agent.py +++ b/sdks/python-sdk/agentlabs/agent.py @@ -1,3 +1,4 @@ +from enum import Enum import os from typing import Any, Callable, TypedDict from socketio.pubsub_manager import uuid @@ -6,14 +7,11 @@ from agentlabs.logger import AgentLogger from .server import emit, agent_namespace -from .attachment import Attachment import socketio -class AgentConfig(TypedDict): - agentlabs_url: str; - project_id: str; - agent_id: str; - secret: str; +class MessageFormat(Enum): + PLAIN_TEXT = "PLAIN_TEXT" + MARKDOWN = "MARKDOWN" class _DecodedUser(TypedDict): id: str @@ -38,10 +36,11 @@ def __init__(self, decoded_user: _DecodedUser): class StreamedChatMessage: is_ended: bool = False - def __init__(self, io: socketio.Client, conversation_id: str): + def __init__(self, io: socketio.Client, conversation_id: str, format: MessageFormat): self.io = io self.conversation_id = conversation_id self.message_id = str(uuid.uuid4()) + self.format = format """ Writes a token to the stream. This can be used to send a message in multiple parts. @@ -55,7 +54,8 @@ def write(self, token: str): "conversationId": self.conversation_id, "messageId": self.message_id, "text": token, - "attachments": [] + "attachments": [], + "format": self.format.value }) """ @@ -85,18 +85,19 @@ def __init__(self, http: HttpApi,io: socketio.Client, message: _ChatMessage): in multiple parts. Well suited to stream LLM outputs. """ - def streamed_reply(self): - return StreamedChatMessage(self.io, self.conversation_id) + def streamed_reply(self, format: MessageFormat = MessageFormat.PLAIN_TEXT): + return StreamedChatMessage(self.io, self.conversation_id, format) """ Replies to the message instantly. If you are looking to stream a reply in multiple parts, use streamed_reply() instead. """ - def reply(self, message: str): + def reply(self, message: str, format: MessageFormat = MessageFormat.PLAIN_TEXT): emit(self.io, 'chat-message', { "conversationId": self.conversation_id, "text": message, - "attachments": [] + "attachments": [], + "format": format.value }) # TODO: we'll come back to this when we officially support attachments diff --git a/server/prisma/migrations/20231010132616_init/migration.sql b/server/prisma/migrations/20231010132616_init/migration.sql new file mode 100644 index 0000000..045eff6 --- /dev/null +++ b/server/prisma/migrations/20231010132616_init/migration.sql @@ -0,0 +1,378 @@ +-- CreateEnum +CREATE TYPE "OrganizationUserRole" AS ENUM ('USER', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "AuthMethodType" AS ENUM ('OAUTH2', 'EMAIL', 'PHONE_NUMBER', 'ANONYMOUS'); + +-- CreateEnum +CREATE TYPE "AuthProvider" AS ENUM ('PASSWORDLESS_EMAIL', 'EMAIL_AND_PASSWORD', 'SMS', 'ANONYMOUS', 'GOOGLE', 'GITHUB', 'GITLAB', 'MICROSOFT'); + +-- CreateEnum +CREATE TYPE "AgentMessageSource" AS ENUM ('USER', 'AGENT', 'SYSTEM'); + +-- CreateEnum +CREATE TYPE "MessageFormat" AS ENUM ('PLAIN_TEXT', 'MARKDOWN', 'HTML'); + +-- CreateEnum +CREATE TYPE "AttachmentStorageDriver" AS ENUM ('AWS_S3', 'AZURE_BLOB_STORAGE', 'GOOGLE_CLOUD_STORAGE', 'LOCAL_FILE_SYSTEM'); + +-- CreateTable +CREATE TABLE "PasswordHashConfig" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "algorithm" TEXT NOT NULL, + "memCost" INTEGER NOT NULL, + "keyLength" INTEGER NOT NULL, + "salt" TEXT NOT NULL, + + CONSTRAINT "PasswordHashConfig_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Onboarding" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "hasAddedAuthMethod" BOOLEAN NOT NULL DEFAULT false, + "hasUsedTheApplication" BOOLEAN NOT NULL DEFAULT false, + "organizationId" TEXT NOT NULL, + "projectId" TEXT, + "userId" TEXT NOT NULL, + + CONSTRAINT "Onboarding_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "email" TEXT NOT NULL, + "fullName" TEXT NOT NULL, + "profilePictureUrl" TEXT, + "verifiedAt" TIMESTAMP(3), + "bannedAt" TIMESTAMP(3), + "passwordHash" TEXT, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Organization" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + + CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OrganizationUser" ( + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "organizationId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "role" "OrganizationUserRole" NOT NULL DEFAULT 'ADMIN', + + CONSTRAINT "OrganizationUser_pkey" PRIMARY KEY ("organizationId","userId") +); + +-- CreateTable +CREATE TABLE "Project" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "creatorId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SmtpConfiguration" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "host" TEXT NOT NULL, + "port" INTEGER NOT NULL, + "secure" BOOLEAN NOT NULL, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL, + "senderName" TEXT NOT NULL, + "senderEmail" TEXT NOT NULL, + "replyTo" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + + CONSTRAINT "SmtpConfiguration_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuthMethod" ( + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "provider" "AuthProvider" NOT NULL, + "type" "AuthMethodType" NOT NULL, + "isEnabled" BOOLEAN NOT NULL DEFAULT false, + "clientId" TEXT, + "clientSecret" TEXT, + "clientSecretIv" TEXT, + "scopes" TEXT[], + "projectId" TEXT NOT NULL, + + CONSTRAINT "AuthMethod_pkey" PRIMARY KEY ("projectId","provider") +); + +-- CreateTable +CREATE TABLE "SdkSecret" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "revokedAt" TIMESTAMP(3), + "hash" TEXT NOT NULL, + "preview" TEXT NOT NULL, + "description" TEXT, + "projectId" TEXT NOT NULL, + "creatorId" TEXT NOT NULL, + + CONSTRAINT "SdkSecret_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Agent" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deletedAt" TIMESTAMP(3), + "name" TEXT NOT NULL, + "creatorId" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "logoUrl" TEXT, + + CONSTRAINT "Agent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MemberAuthVerificationCode" ( + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "code" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "projectId" TEXT NOT NULL, + "memberId" TEXT NOT NULL, + + CONSTRAINT "MemberAuthVerificationCode_pkey" PRIMARY KEY ("projectId","memberId") +); + +-- CreateTable +CREATE TABLE "MemberAuth" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "passwordHash" TEXT, + "memberId" TEXT NOT NULL, + "algorithm" TEXT NOT NULL, + "memCost" INTEGER NOT NULL, + "keyLength" INTEGER NOT NULL, + "salt" TEXT NOT NULL, + + CONSTRAINT "MemberAuth_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Member" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "verifiedAt" TIMESTAMP(3), + "bannedAt" TIMESTAMP(3), + "firstName" TEXT, + "lastName" TEXT, + "fullName" TEXT, + "email" TEXT NOT NULL, + "profilePictureUrl" TEXT, + "projectId" TEXT NOT NULL, + + CONSTRAINT "Member_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MemberIdentity" ( + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "provider" "AuthProvider" NOT NULL, + "providerUserId" TEXT NOT NULL, + "memberId" TEXT NOT NULL, + "lastSignedInAt" TIMESTAMP(3), + "accessToken" TEXT, + "refreshToken" TEXT, + "accessTokenExpiresAt" TIMESTAMP(3), + "refreshTokenExpiresAt" TIMESTAMP(3), + + CONSTRAINT "MemberIdentity_pkey" PRIMARY KEY ("memberId","provider") +); + +-- CreateTable +CREATE TABLE "AgentConversation" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "agentId" TEXT NOT NULL, + "memberId" TEXT NOT NULL, + + CONSTRAINT "AgentConversation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AgentMessage" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "source" "AgentMessageSource" NOT NULL, + "text" TEXT NOT NULL, + "conversationId" TEXT NOT NULL, + "format" "MessageFormat" NOT NULL DEFAULT 'PLAIN_TEXT', + + CONSTRAINT "AgentMessage_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AgentMessageAttachment" ( + "id" TEXT NOT NULL, + "attachmentId" TEXT NOT NULL, + "messageId" TEXT NOT NULL, + + CONSTRAINT "AgentMessageAttachment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Attachment" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + "mimeType" TEXT NOT NULL, + "checksumSha256" TEXT NOT NULL, + "driver" "AttachmentStorageDriver" NOT NULL, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "Attachment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AgentConnectionEvent" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "agentId" TEXT NOT NULL, + + CONSTRAINT "AgentConnectionEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PasswordHashConfig_userId_key" ON "PasswordHashConfig"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Onboarding_organizationId_key" ON "Onboarding"("organizationId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Onboarding_userId_key" ON "Onboarding"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "SmtpConfiguration_projectId_key" ON "SmtpConfiguration"("projectId"); + +-- CreateIndex +CREATE UNIQUE INDEX "SdkSecret_hash_key" ON "SdkSecret"("hash"); + +-- CreateIndex +CREATE UNIQUE INDEX "MemberAuthVerificationCode_memberId_key" ON "MemberAuthVerificationCode"("memberId"); + +-- CreateIndex +CREATE UNIQUE INDEX "MemberAuth_memberId_key" ON "MemberAuth"("memberId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AgentMessageAttachment_attachmentId_key" ON "AgentMessageAttachment"("attachmentId"); + +-- AddForeignKey +ALTER TABLE "PasswordHashConfig" ADD CONSTRAINT "PasswordHashConfig_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Onboarding" ADD CONSTRAINT "Onboarding_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Onboarding" ADD CONSTRAINT "Onboarding_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Onboarding" ADD CONSTRAINT "Onboarding_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OrganizationUser" ADD CONSTRAINT "OrganizationUser_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OrganizationUser" ADD CONSTRAINT "OrganizationUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SmtpConfiguration" ADD CONSTRAINT "SmtpConfiguration_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuthMethod" ADD CONSTRAINT "AuthMethod_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SdkSecret" ADD CONSTRAINT "SdkSecret_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SdkSecret" ADD CONSTRAINT "SdkSecret_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Agent" ADD CONSTRAINT "Agent_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Agent" ADD CONSTRAINT "Agent_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberAuthVerificationCode" ADD CONSTRAINT "MemberAuthVerificationCode_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberAuthVerificationCode" ADD CONSTRAINT "MemberAuthVerificationCode_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberAuth" ADD CONSTRAINT "MemberAuth_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Member" ADD CONSTRAINT "Member_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberIdentity" ADD CONSTRAINT "MemberIdentity_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AgentConversation" ADD CONSTRAINT "AgentConversation_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AgentConversation" ADD CONSTRAINT "AgentConversation_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AgentMessage" ADD CONSTRAINT "AgentMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "AgentConversation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AgentMessageAttachment" ADD CONSTRAINT "AgentMessageAttachment_attachmentId_fkey" FOREIGN KEY ("attachmentId") REFERENCES "Attachment"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AgentMessageAttachment" ADD CONSTRAINT "AgentMessageAttachment_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "AgentMessage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AgentConnectionEvent" ADD CONSTRAINT "AgentConnectionEvent_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 5b3c651..9bbb61d 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -325,6 +325,12 @@ enum AgentMessageSource { SYSTEM // reserved for future use } +enum MessageFormat { + PLAIN_TEXT + MARKDOWN + HTML // reserved for future use +} + model AgentMessage { id String @id @default(uuid()) createdAt DateTime @default(now()) @@ -337,6 +343,7 @@ model AgentMessage { conversation AgentConversation @relation(fields: [conversationId], references: [id]) attachments AgentMessageAttachment[] + format MessageFormat @default(PLAIN_TEXT) } model AgentMessageAttachment { diff --git a/server/src/agent-chat/agent-chat-messages/agent-chat-messages.service.ts b/server/src/agent-chat/agent-chat-messages/agent-chat-messages.service.ts index 59e5ba9..b6d6684 100644 --- a/server/src/agent-chat/agent-chat-messages/agent-chat-messages.service.ts +++ b/server/src/agent-chat/agent-chat-messages/agent-chat-messages.service.ts @@ -28,6 +28,7 @@ export class AgentChatMessagesService { text: payload.text, source: payload.source, conversationId: payload.conversationId, + format: payload.format, }, }); diff --git a/server/src/agent-chat/agent-chat-messages/agent-chat-messages.types.ts b/server/src/agent-chat/agent-chat-messages/agent-chat-messages.types.ts index 09303b5..9d3fdc2 100644 --- a/server/src/agent-chat/agent-chat-messages/agent-chat-messages.types.ts +++ b/server/src/agent-chat/agent-chat-messages/agent-chat-messages.types.ts @@ -1,7 +1,8 @@ -import { AgentMessageSource } from 'prisma/prisma-client'; +import { AgentMessageSource, MessageFormat } from 'prisma/prisma-client'; export interface CreateAgentChatMessagePayload { text: string; source: AgentMessageSource; conversationId: string; + format: MessageFormat; } diff --git a/server/src/agent-connection/agent-connection.gateway.ts b/server/src/agent-connection/agent-connection.gateway.ts index 7b7070d..66cee8b 100644 --- a/server/src/agent-connection/agent-connection.gateway.ts +++ b/server/src/agent-connection/agent-connection.gateway.ts @@ -172,10 +172,11 @@ export class AgentConnectionGateway try { await this.conversationMutexManager.acquire(conversation.id); - await this.messagesService.createMessage({ + const message = await this.messagesService.createMessage({ conversationId: conversation.id, text: payload.data.text, source: 'AGENT', + format: payload.data.format, }); const frontendConnection = @@ -201,7 +202,16 @@ export class AgentConnectionGateway }; } - frontendConnection.socket.emit('chat-message', payload); + frontendConnection.socket.emit('chat-message', { + timestamp: new Date().toISOString(), + data: { + conversationId: conversation.id, + text: payload.data.text, + format: payload.data.format, + source: 'AGENT', + messageId: message.id, + }, + }); return { message: 'Message sent successfully', @@ -221,6 +231,7 @@ export class AgentConnectionGateway messageId: payload.data.messageId, conversationId: payload.data.conversationId, token: payload.data.text, + format: payload.data.format, }); } diff --git a/server/src/agent-connection/agent-stream-manager/agent-stream-manager.service.ts b/server/src/agent-connection/agent-stream-manager/agent-stream-manager.service.ts index b7fb591..679703d 100644 --- a/server/src/agent-connection/agent-stream-manager/agent-stream-manager.service.ts +++ b/server/src/agent-connection/agent-stream-manager/agent-stream-manager.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { MessageFormat } from '@prisma/client'; import { Socket } from 'socket.io'; import { seconds } from 'src/common/ms-time'; import { FrontendConnectionManagerService } from 'src/frontend-connection-manager/frontend-connection-manager.service'; @@ -11,12 +12,14 @@ export interface StreamData { conversationId: string; messageId: string; createdAtTs: number; + format: MessageFormat; } interface HandlePayload { messageId: string; conversationId: string; token: string; + format: MessageFormat; } @Injectable() @@ -86,6 +89,7 @@ export class AgentStreamManagerService { text: stream.buffer, source: 'AGENT', conversationId: stream.conversationId, + format: stream.format, }, }); } else { @@ -143,6 +147,7 @@ export class AgentStreamManagerService { buffer: '', messageId: data.messageId, createdAtTs: Date.now(), + format: data.format, }; this.activeStreams.set(data.messageId, stream); @@ -154,6 +159,7 @@ export class AgentStreamManagerService { text: data.token, conversationId: stream.conversationId, messageId: stream.messageId, + format: stream.format, }, }); } finally { diff --git a/server/src/agent-connection/dto/agent-message.dto.ts b/server/src/agent-connection/dto/agent-message.dto.ts index 82e4037..ea67b51 100644 --- a/server/src/agent-connection/dto/agent-message.dto.ts +++ b/server/src/agent-connection/dto/agent-message.dto.ts @@ -1,5 +1,6 @@ +import { MessageFormat } from '@prisma/client'; import { Type } from 'class-transformer'; -import { IsString, ValidateNested } from 'class-validator'; +import { IsEnum, IsString, ValidateNested } from 'class-validator'; import { BaseRealtimeMessageDto } from 'src/common/base-realtime-message.dto'; class AgentMessageDataDto { @@ -11,6 +12,9 @@ class AgentMessageDataDto { @IsString() messageId: string; + + @IsEnum(MessageFormat) + format: MessageFormat; } export class AgentMessageDto extends BaseRealtimeMessageDto { diff --git a/server/src/agent-connection/dto/stream-chat-message-token.dto.ts b/server/src/agent-connection/dto/stream-chat-message-token.dto.ts index 3f20be8..3e82dad 100644 --- a/server/src/agent-connection/dto/stream-chat-message-token.dto.ts +++ b/server/src/agent-connection/dto/stream-chat-message-token.dto.ts @@ -1,5 +1,6 @@ +import { MessageFormat } from '@prisma/client'; import { Type } from 'class-transformer'; -import { IsString, ValidateNested } from 'class-validator'; +import { IsEnum, IsString, ValidateNested } from 'class-validator'; import { BaseRealtimeMessageDto } from 'src/common/base-realtime-message.dto'; class StreamChatMessageTokenDtoData { @@ -11,6 +12,9 @@ class StreamChatMessageTokenDtoData { @IsString() messageId: string; + + @IsEnum(MessageFormat) + format: MessageFormat; } export class StreamChatMessageTokenDto extends BaseRealtimeMessageDto { diff --git a/server/src/frontend-connection/frontend-connection.gateway.ts b/server/src/frontend-connection/frontend-connection.gateway.ts index 91cceff..027e7d1 100644 --- a/server/src/frontend-connection/frontend-connection.gateway.ts +++ b/server/src/frontend-connection/frontend-connection.gateway.ts @@ -152,6 +152,7 @@ export class FrontendConnectionGateway source: 'USER', text: payload.data.text, conversationId: conversation.id, + format: 'PLAIN_TEXT', }); const clientPayload: BaseRealtimeMessageDto = { @@ -189,6 +190,7 @@ export class FrontendConnectionGateway conversationId: conversation.id, source: 'SYSTEM', text, + format: 'MARKDOWN', }); const payload: BaseRealtimeMessageDto = { @@ -198,6 +200,7 @@ export class FrontendConnectionGateway conversationId: conversation.id, source: 'SYSTEM', messageId: message.id, + format: 'MARKDOWN', }, message: 'Agent is offline', };