push
Browse files- src/routes/chatRoutes.ts +4 -2
- src/services/chatProtocol.ts +83 -57
src/routes/chatRoutes.ts
CHANGED
|
@@ -42,9 +42,11 @@ const upload = multer({
|
|
| 42 |
* Send a text message and/or files to an agent. The system handles communication with any agent endpoint format.
|
| 43 |
*
|
| 44 |
* **Agent Communication Protocol:**
|
| 45 |
-
* - Files are
|
|
|
|
|
|
|
| 46 |
* - For text-only messages: Sends JSON payload to agent endpoint
|
| 47 |
-
* - For messages with files: Sends multipart/form-data with file
|
| 48 |
* - Supports custom headers via agent metadata (agent.metadata.headers)
|
| 49 |
* - Works with any agent endpoint format (REST, GraphQL, custom APIs)
|
| 50 |
*
|
|
|
|
| 42 |
* Send a text message and/or files to an agent. The system handles communication with any agent endpoint format.
|
| 43 |
*
|
| 44 |
* **Agent Communication Protocol:**
|
| 45 |
+
* - Files are sent directly to agent endpoints (not uploaded to IPFS)
|
| 46 |
+
* - Files are stored temporarily in memory during processing
|
| 47 |
+
* - File metadata is stored in chat history with 1-hour expiration
|
| 48 |
* - For text-only messages: Sends JSON payload to agent endpoint
|
| 49 |
+
* - For messages with files: Sends multipart/form-data with actual file buffers
|
| 50 |
* - Supports custom headers via agent metadata (agent.metadata.headers)
|
| 51 |
* - Works with any agent endpoint format (REST, GraphQL, custom APIs)
|
| 52 |
*
|
src/services/chatProtocol.ts
CHANGED
|
@@ -3,7 +3,6 @@ import FormData from 'form-data';
|
|
| 3 |
import { Agent } from '../entities/Agent';
|
| 4 |
import { ChatMessage, MessageRole } from '../entities/ChatMessage';
|
| 5 |
import { AppDataSource } from '../config/database';
|
| 6 |
-
import { IpfsService } from './ipfsService';
|
| 7 |
|
| 8 |
export interface ChatRequest {
|
| 9 |
message: string;
|
|
@@ -34,32 +33,22 @@ export interface ChatResponse {
|
|
| 34 |
*/
|
| 35 |
export class ChatProtocolService {
|
| 36 |
private timeout: number;
|
| 37 |
-
private ipfsService: IpfsService;
|
| 38 |
private maxFileSize: number;
|
| 39 |
|
| 40 |
constructor() {
|
| 41 |
this.timeout = parseInt(process.env.AGENT_CHAT_TIMEOUT || '30000');
|
| 42 |
-
this.ipfsService = new IpfsService();
|
| 43 |
this.maxFileSize = parseInt(process.env.MAX_FILE_SIZE || '10485760'); // 10MB default
|
| 44 |
}
|
| 45 |
|
| 46 |
/**
|
| 47 |
-
*
|
|
|
|
| 48 |
*/
|
| 49 |
-
private
|
| 50 |
-
fieldname: string;
|
| 51 |
-
originalname: string;
|
| 52 |
-
mimetype: string;
|
| 53 |
-
size: number;
|
| 54 |
-
ipfsHash: string;
|
| 55 |
-
url: string;
|
| 56 |
-
}>> {
|
| 57 |
if (!files || files.length === 0) {
|
| 58 |
-
return
|
| 59 |
}
|
| 60 |
|
| 61 |
-
const uploadedFiles = [];
|
| 62 |
-
|
| 63 |
for (const file of files) {
|
| 64 |
// Validate file size
|
| 65 |
if (file.size > this.maxFileSize) {
|
|
@@ -67,33 +56,7 @@ export class ChatProtocolService {
|
|
| 67 |
`File ${file.originalname} exceeds maximum size of ${this.maxFileSize} bytes`
|
| 68 |
);
|
| 69 |
}
|
| 70 |
-
|
| 71 |
-
// Upload to IPFS
|
| 72 |
-
try {
|
| 73 |
-
const fileObj = new File(
|
| 74 |
-
[file.buffer],
|
| 75 |
-
file.originalname,
|
| 76 |
-
{ type: file.mimetype }
|
| 77 |
-
);
|
| 78 |
-
|
| 79 |
-
const upload = await this.ipfsService.uploadFile(fileObj);
|
| 80 |
-
const gatewayUrl = this.ipfsService.getGatewayUrl(upload.cid);
|
| 81 |
-
|
| 82 |
-
uploadedFiles.push({
|
| 83 |
-
fieldname: file.fieldname,
|
| 84 |
-
originalname: file.originalname,
|
| 85 |
-
mimetype: file.mimetype,
|
| 86 |
-
size: file.size,
|
| 87 |
-
ipfsHash: upload.cid,
|
| 88 |
-
url: gatewayUrl,
|
| 89 |
-
});
|
| 90 |
-
} catch (error) {
|
| 91 |
-
console.error(`Error uploading file ${file.originalname}:`, error);
|
| 92 |
-
throw new Error(`Failed to upload file ${file.originalname} to IPFS`);
|
| 93 |
-
}
|
| 94 |
}
|
| 95 |
-
|
| 96 |
-
return uploadedFiles;
|
| 97 |
}
|
| 98 |
|
| 99 |
/**
|
|
@@ -110,23 +73,39 @@ export class ChatProtocolService {
|
|
| 110 |
throw new Error(`Message exceeds maximum length of ${maxLength} characters`);
|
| 111 |
}
|
| 112 |
|
| 113 |
-
//
|
|
|
|
| 114 |
let uploadedFiles: Array<{
|
| 115 |
fieldname: string;
|
| 116 |
originalname: string;
|
| 117 |
mimetype: string;
|
| 118 |
size: number;
|
| 119 |
-
|
| 120 |
-
|
|
|
|
| 121 |
}> = [];
|
| 122 |
|
| 123 |
if (request.files && request.files.length > 0) {
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
|
| 127 |
const conversationId = request.conversationId || this.generateConversationId();
|
| 128 |
|
| 129 |
-
|
| 130 |
const metadata = {
|
| 131 |
...request.metadata,
|
| 132 |
agentId: agent.id,
|
|
@@ -137,8 +116,8 @@ export class ChatProtocolService {
|
|
| 137 |
originalname: f.originalname,
|
| 138 |
mimetype: f.mimetype,
|
| 139 |
size: f.size,
|
| 140 |
-
|
| 141 |
-
|
| 142 |
})),
|
| 143 |
}),
|
| 144 |
};
|
|
@@ -164,13 +143,52 @@ export class ChatProtocolService {
|
|
| 164 |
formData.append('systemPrompt', agent.promptTemplate);
|
| 165 |
}
|
| 166 |
|
| 167 |
-
//
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
response = await axios.post(agent.endpoint, formData, {
|
| 176 |
timeout: this.timeout,
|
|
@@ -207,7 +225,7 @@ export class ChatProtocolService {
|
|
| 207 |
? `\n[Files: ${uploadedFiles.map(f => f.originalname).join(', ')}]`
|
| 208 |
: '';
|
| 209 |
|
| 210 |
-
// Save message to database
|
| 211 |
await this.saveMessage({
|
| 212 |
agentId: agent.id,
|
| 213 |
userId: request.userId,
|
|
@@ -215,7 +233,15 @@ export class ChatProtocolService {
|
|
| 215 |
content: userContent + filesInfo,
|
| 216 |
metadata: {
|
| 217 |
...metadata,
|
| 218 |
-
files: uploadedFiles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
},
|
| 220 |
});
|
| 221 |
|
|
|
|
| 3 |
import { Agent } from '../entities/Agent';
|
| 4 |
import { ChatMessage, MessageRole } from '../entities/ChatMessage';
|
| 5 |
import { AppDataSource } from '../config/database';
|
|
|
|
| 6 |
|
| 7 |
export interface ChatRequest {
|
| 8 |
message: string;
|
|
|
|
| 33 |
*/
|
| 34 |
export class ChatProtocolService {
|
| 35 |
private timeout: number;
|
|
|
|
| 36 |
private maxFileSize: number;
|
| 37 |
|
| 38 |
constructor() {
|
| 39 |
this.timeout = parseInt(process.env.AGENT_CHAT_TIMEOUT || '30000');
|
|
|
|
| 40 |
this.maxFileSize = parseInt(process.env.MAX_FILE_SIZE || '10485760'); // 10MB default
|
| 41 |
}
|
| 42 |
|
| 43 |
/**
|
| 44 |
+
* Validate files (no IPFS upload - files stored temporarily in memory)
|
| 45 |
+
* Files are kept in memory during request processing and sent directly to agent endpoints
|
| 46 |
*/
|
| 47 |
+
private validateFiles(files: ChatRequest['files']): void {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
if (!files || files.length === 0) {
|
| 49 |
+
return;
|
| 50 |
}
|
| 51 |
|
|
|
|
|
|
|
| 52 |
for (const file of files) {
|
| 53 |
// Validate file size
|
| 54 |
if (file.size > this.maxFileSize) {
|
|
|
|
| 56 |
`File ${file.originalname} exceeds maximum size of ${this.maxFileSize} bytes`
|
| 57 |
);
|
| 58 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
/**
|
|
|
|
| 73 |
throw new Error(`Message exceeds maximum length of ${maxLength} characters`);
|
| 74 |
}
|
| 75 |
|
| 76 |
+
// Store files temporarily (don't upload to IPFS)
|
| 77 |
+
// Files will be available for 1 hour before cleanup
|
| 78 |
let uploadedFiles: Array<{
|
| 79 |
fieldname: string;
|
| 80 |
originalname: string;
|
| 81 |
mimetype: string;
|
| 82 |
size: number;
|
| 83 |
+
buffer: Buffer;
|
| 84 |
+
tempUrl?: string; // Temporary URL for chat viewing
|
| 85 |
+
expiresAt: Date; // Cleanup after 1 hour
|
| 86 |
}> = [];
|
| 87 |
|
| 88 |
if (request.files && request.files.length > 0) {
|
| 89 |
+
// Validate files first
|
| 90 |
+
this.validateFiles(request.files);
|
| 91 |
+
|
| 92 |
+
// Store files temporarily without IPFS upload
|
| 93 |
+
// Files are kept in memory during request processing
|
| 94 |
+
// Metadata stored in DB with 1-hour expiration timestamp
|
| 95 |
+
const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000);
|
| 96 |
+
uploadedFiles = request.files.map(file => ({
|
| 97 |
+
fieldname: file.fieldname,
|
| 98 |
+
originalname: file.originalname,
|
| 99 |
+
mimetype: file.mimetype,
|
| 100 |
+
size: file.size,
|
| 101 |
+
buffer: file.buffer,
|
| 102 |
+
expiresAt: oneHourFromNow,
|
| 103 |
+
}));
|
| 104 |
}
|
| 105 |
|
| 106 |
const conversationId = request.conversationId || this.generateConversationId();
|
| 107 |
|
| 108 |
+
// Build metadata with file information (temporary storage, no IPFS)
|
| 109 |
const metadata = {
|
| 110 |
...request.metadata,
|
| 111 |
agentId: agent.id,
|
|
|
|
| 116 |
originalname: f.originalname,
|
| 117 |
mimetype: f.mimetype,
|
| 118 |
size: f.size,
|
| 119 |
+
expiresAt: f.expiresAt.toISOString(),
|
| 120 |
+
// Note: Files are stored temporarily, not on IPFS
|
| 121 |
})),
|
| 122 |
}),
|
| 123 |
};
|
|
|
|
| 143 |
formData.append('systemPrompt', agent.promptTemplate);
|
| 144 |
}
|
| 145 |
|
| 146 |
+
// Send actual files directly to agent endpoint (no IPFS upload)
|
| 147 |
+
// Use smart field naming based on file type and count
|
| 148 |
+
if (request.files && request.files.length > 0) {
|
| 149 |
+
const audioFiles = request.files.filter(f => f.mimetype.startsWith('audio/'));
|
| 150 |
+
|
| 151 |
+
// If single audio file, use 'audio_file' (common for transcription agents)
|
| 152 |
+
if (audioFiles.length === 1 && request.files.length === 1) {
|
| 153 |
+
const audioFile = request.files.find(f => f.mimetype.startsWith('audio/'));
|
| 154 |
+
if (audioFile) {
|
| 155 |
+
formData.append('audio_file', audioFile.buffer, {
|
| 156 |
+
filename: audioFile.originalname,
|
| 157 |
+
contentType: audioFile.mimetype,
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
// If single file of any type, use 'file'
|
| 162 |
+
else if (request.files.length === 1) {
|
| 163 |
+
const file = request.files[0];
|
| 164 |
+
formData.append('file', file.buffer, {
|
| 165 |
+
filename: file.originalname,
|
| 166 |
+
contentType: file.mimetype,
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
// Multiple files - use 'files' array or indexed fields
|
| 170 |
+
else {
|
| 171 |
+
request.files.forEach((file, index) => {
|
| 172 |
+
// Try common field names first
|
| 173 |
+
if (file.mimetype.startsWith('audio/')) {
|
| 174 |
+
formData.append(`audio_file${index > 0 ? `_${index}` : ''}`, file.buffer, {
|
| 175 |
+
filename: file.originalname,
|
| 176 |
+
contentType: file.mimetype,
|
| 177 |
+
});
|
| 178 |
+
} else if (file.mimetype.startsWith('image/')) {
|
| 179 |
+
formData.append(`image_file${index > 0 ? `_${index}` : ''}`, file.buffer, {
|
| 180 |
+
filename: file.originalname,
|
| 181 |
+
contentType: file.mimetype,
|
| 182 |
+
});
|
| 183 |
+
} else {
|
| 184 |
+
formData.append(`file${index > 0 ? `_${index}` : ''}`, file.buffer, {
|
| 185 |
+
filename: file.originalname,
|
| 186 |
+
contentType: file.mimetype,
|
| 187 |
+
});
|
| 188 |
+
}
|
| 189 |
+
});
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
|
| 193 |
response = await axios.post(agent.endpoint, formData, {
|
| 194 |
timeout: this.timeout,
|
|
|
|
| 225 |
? `\n[Files: ${uploadedFiles.map(f => f.originalname).join(', ')}]`
|
| 226 |
: '';
|
| 227 |
|
| 228 |
+
// Save message to database with file metadata (files stored temporarily, not on IPFS)
|
| 229 |
await this.saveMessage({
|
| 230 |
agentId: agent.id,
|
| 231 |
userId: request.userId,
|
|
|
|
| 233 |
content: userContent + filesInfo,
|
| 234 |
metadata: {
|
| 235 |
...metadata,
|
| 236 |
+
files: uploadedFiles.map(f => ({
|
| 237 |
+
fieldname: f.fieldname,
|
| 238 |
+
originalname: f.originalname,
|
| 239 |
+
mimetype: f.mimetype,
|
| 240 |
+
size: f.size,
|
| 241 |
+
expiresAt: f.expiresAt.toISOString(),
|
| 242 |
+
// Note: File buffers are not stored in DB, only metadata
|
| 243 |
+
// Files are available temporarily in memory for 1 hour
|
| 244 |
+
})),
|
| 245 |
},
|
| 246 |
});
|
| 247 |
|