nexusbert commited on
Commit
b3094b0
·
1 Parent(s): f27f110

push new routes

Browse files
Files changed (2) hide show
  1. src/index.ts +7 -2
  2. src/routes/chat.ts +216 -0
src/index.ts CHANGED
@@ -11,6 +11,7 @@ import suggestRoute from "./routes/suggest";
11
  import wardrobeRoute from "./routes/wardrobe";
12
  import profileRoute from "./routes/profile";
13
  import avatarRoute from "./routes/avatar";
 
14
 
15
  dotenv.config();
16
 
@@ -41,7 +42,9 @@ app.get("/", (req, res) => {
41
  profile: "/api/profile",
42
  upload: "/api/upload",
43
  suggest: "/api/suggest",
44
- wardrobe: "/api/wardrobe"
 
 
45
  }
46
  },
47
  technologies: [
@@ -164,7 +167,8 @@ const swaggerOptions: swaggerJsdoc.Options = {
164
  { name: "Authentication", description: "User registration and login" },
165
  { name: "Profile", description: "User profile management" },
166
  { name: "Upload", description: "Wardrobe item upload and classification" },
167
- { name: "Suggest", description: "AI-powered outfit suggestions" },
 
168
  { name: "Avatar", description: "Ready Player Me avatar management" },
169
  { name: "Health", description: "Health check endpoints" }
170
  ]
@@ -218,6 +222,7 @@ app.use("/api/upload", uploadRoute);
218
  app.use("/api/suggest", suggestRoute);
219
  app.use("/api/wardrobe", wardrobeRoute);
220
  app.use("/api/avatar", avatarRoute);
 
221
 
222
  const PORT = process.env.PORT || 7860;
223
 
 
11
  import wardrobeRoute from "./routes/wardrobe";
12
  import profileRoute from "./routes/profile";
13
  import avatarRoute from "./routes/avatar";
14
+ import chatRoute from "./routes/chat";
15
 
16
  dotenv.config();
17
 
 
42
  profile: "/api/profile",
43
  upload: "/api/upload",
44
  suggest: "/api/suggest",
45
+ chat: "/api/chat",
46
+ wardrobe: "/api/wardrobe",
47
+ avatar: "/api/avatar"
48
  }
49
  },
50
  technologies: [
 
167
  { name: "Authentication", description: "User registration and login" },
168
  { name: "Profile", description: "User profile management" },
169
  { name: "Upload", description: "Wardrobe item upload and classification" },
170
+ { name: "Suggest", description: "AI-powered outfit suggestions with wardrobe" },
171
+ { name: "Chat", description: "Fashion chat without wardrobe" },
172
  { name: "Avatar", description: "Ready Player Me avatar management" },
173
  { name: "Health", description: "Health check endpoints" }
174
  ]
 
222
  app.use("/api/suggest", suggestRoute);
223
  app.use("/api/wardrobe", wardrobeRoute);
224
  app.use("/api/avatar", avatarRoute);
225
+ app.use("/api/chat", chatRoute);
226
 
227
  const PORT = process.env.PORT || 7860;
228
 
src/routes/chat.ts ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import axios from "axios";
3
+ import dotenv from "dotenv";
4
+ dotenv.config();
5
+
6
+ const router = express.Router();
7
+
8
+ const CHAT_ENDPOINT = process.env.CHAT_ENDPOINT || "";
9
+ const STREAM_ENDPOINT = process.env.STREAM_ENDPOINT || process.env.CHAT_ENDPOINT?.replace('/chat', '/chat/upload/stream') || '';
10
+ const AI_TIMEOUT = 10 * 60 * 1000;
11
+
12
+ /**
13
+ * @openapi
14
+ * /api/chat:
15
+ * post:
16
+ * summary: Fashion chat without wardrobe (proxied to AI)
17
+ * tags: [Chat]
18
+ * requestBody:
19
+ * required: true
20
+ * content:
21
+ * application/json:
22
+ * schema:
23
+ * type: object
24
+ * required:
25
+ * - message
26
+ * properties:
27
+ * message:
28
+ * type: string
29
+ * description: User's fashion question
30
+ * session_id:
31
+ * type: string
32
+ * description: Session ID for conversation context
33
+ * images:
34
+ * type: array
35
+ * items:
36
+ * type: string
37
+ * description: Optional array of image URLs or base64 images
38
+ * responses:
39
+ * 200:
40
+ * description: Chat response from AI
41
+ * 400:
42
+ * description: Bad request
43
+ * 500:
44
+ * description: Server error
45
+ */
46
+ router.post("/", async (req, res) => {
47
+ try {
48
+ const { message, session_id, images } = req.body;
49
+
50
+ if (!message) {
51
+ return res.status(400).json({ success: false, error: "message is required" });
52
+ }
53
+
54
+ const requestBody: any = {
55
+ message: message,
56
+ session_id: session_id || "default",
57
+ };
58
+
59
+ if (images && Array.isArray(images) && images.length > 0) {
60
+ requestBody.images = images;
61
+ }
62
+
63
+ console.log(`[Chat] Sending request to AI (timeout: ${AI_TIMEOUT / 1000}s)`);
64
+
65
+ const response = await axios.post(
66
+ CHAT_ENDPOINT,
67
+ requestBody,
68
+ {
69
+ headers: { "Content-Type": "application/json" },
70
+ timeout: AI_TIMEOUT,
71
+ }
72
+ );
73
+
74
+ let aiResponse = "";
75
+ if (typeof response.data === 'string') {
76
+ aiResponse = response.data;
77
+ } else if (response.data.response) {
78
+ aiResponse = response.data.response;
79
+ } else if (response.data.message) {
80
+ aiResponse = response.data.message;
81
+ } else if (response.data.text) {
82
+ aiResponse = response.data.text;
83
+ } else {
84
+ aiResponse = JSON.stringify(response.data);
85
+ }
86
+
87
+ res.json({
88
+ success: true,
89
+ response: aiResponse,
90
+ session_id: session_id || "default"
91
+ });
92
+ } catch (error: any) {
93
+ console.error("Chat error:", error.message);
94
+ res.status(500).json({ success: false, error: error.message || "Chat failed" });
95
+ }
96
+ });
97
+
98
+ /**
99
+ * @openapi
100
+ * /api/chat/stream:
101
+ * post:
102
+ * summary: Fashion chat with streaming response (proxied to AI)
103
+ * tags: [Chat]
104
+ * requestBody:
105
+ * required: true
106
+ * content:
107
+ * application/json:
108
+ * schema:
109
+ * type: object
110
+ * required:
111
+ * - message
112
+ * properties:
113
+ * message:
114
+ * type: string
115
+ * description: User's fashion question
116
+ * session_id:
117
+ * type: string
118
+ * description: Session ID for conversation context
119
+ * images:
120
+ * type: array
121
+ * items:
122
+ * type: string
123
+ * description: Optional array of image URLs or base64 images
124
+ * responses:
125
+ * 200:
126
+ * description: Server-Sent Events stream
127
+ * content:
128
+ * text/event-stream:
129
+ * schema:
130
+ * type: string
131
+ * 400:
132
+ * description: Bad request
133
+ * 500:
134
+ * description: Server error
135
+ */
136
+ router.post("/stream", async (req, res) => {
137
+ try {
138
+ const { message, session_id, images } = req.body;
139
+
140
+ if (!message) {
141
+ return res.status(400).json({ success: false, error: "message is required" });
142
+ }
143
+
144
+ res.setHeader('Content-Type', 'text/event-stream');
145
+ res.setHeader('Cache-Control', 'no-cache');
146
+ res.setHeader('Connection', 'keep-alive');
147
+ res.setHeader('X-Accel-Buffering', 'no');
148
+
149
+ const FormData = (await import('form-data')).default;
150
+ const formData = new FormData();
151
+ formData.append('message', message);
152
+ formData.append('session_id', session_id || 'default');
153
+
154
+ if (images && Array.isArray(images) && images.length > 0) {
155
+ images.forEach((image: string) => {
156
+ formData.append('images', image);
157
+ });
158
+ }
159
+
160
+ console.log(`[Chat Stream] Calling streaming endpoint: ${STREAM_ENDPOINT} (timeout: ${AI_TIMEOUT / 1000}s)`);
161
+
162
+ const response = await axios({
163
+ method: 'post',
164
+ url: STREAM_ENDPOINT,
165
+ data: formData,
166
+ headers: {
167
+ ...formData.getHeaders(),
168
+ },
169
+ responseType: 'stream',
170
+ timeout: AI_TIMEOUT,
171
+ });
172
+
173
+ response.data.on('data', (chunk: Buffer) => {
174
+ const lines = chunk.toString().split('\n');
175
+ for (const line of lines) {
176
+ if (line.startsWith('data:')) {
177
+ try {
178
+ const jsonStr = line.substring(5).trim();
179
+ if (jsonStr) {
180
+ const data = JSON.parse(jsonStr);
181
+ if (data.type === 'chunk' && data.content) {
182
+ res.write(`data: ${JSON.stringify({ type: "chunk", content: data.content })}\n\n`);
183
+ }
184
+ }
185
+ } catch (e) {
186
+ }
187
+ }
188
+ }
189
+ });
190
+
191
+ response.data.on('end', () => {
192
+ console.log(`[Chat Stream] Stream complete`);
193
+ res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
194
+ res.end();
195
+ });
196
+
197
+ response.data.on('error', (error: Error) => {
198
+ console.error('[Chat Stream] Stream error:', error);
199
+ res.write(`data: ${JSON.stringify({ type: "error", message: error.message })}\n\n`);
200
+ res.end();
201
+ });
202
+
203
+ req.on('close', () => {
204
+ console.log('[Chat Stream] Client disconnected');
205
+ response.data.destroy();
206
+ });
207
+
208
+ } catch (error: any) {
209
+ console.error("Chat stream error:", error.message);
210
+ res.write(`data: ${JSON.stringify({ type: "error", message: error.message || "Streaming failed" })}\n\n`);
211
+ res.end();
212
+ }
213
+ });
214
+
215
+ export default router;
216
+