You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

979 lines
30 KiB

11 months ago
  1. const path = require("path");
  2. const fs = require("fs");
  3. const {
  4. reqBody,
  5. multiUserMode,
  6. userFromSession,
  7. safeJsonParse,
  8. } = require("../utils/http");
  9. const { normalizePath, isWithin } = require("../utils/files");
  10. const { Workspace } = require("../models/workspace");
  11. const { Document } = require("../models/documents");
  12. const { DocumentVectors } = require("../models/vectors");
  13. const { WorkspaceChats } = require("../models/workspaceChats");
  14. const { getVectorDbClass } = require("../utils/helpers");
  15. const { handleFileUpload, handlePfpUpload } = require("../utils/files/multer");
  16. const { validatedRequest } = require("../utils/middleware/validatedRequest");
  17. const { Telemetry } = require("../models/telemetry");
  18. const {
  19. flexUserRoleValid,
  20. ROLES,
  21. } = require("../utils/middleware/multiUserProtected");
  22. const { EventLogs } = require("../models/eventLogs");
  23. const {
  24. WorkspaceSuggestedMessages,
  25. } = require("../models/workspacesSuggestedMessages");
  26. const { validWorkspaceSlug } = require("../utils/middleware/validWorkspace");
  27. const { convertToChatHistory } = require("../utils/helpers/chat/responses");
  28. const { CollectorApi } = require("../utils/collectorApi");
  29. const {
  30. determineWorkspacePfpFilepath,
  31. fetchPfp,
  32. } = require("../utils/files/pfp");
  33. const { getTTSProvider } = require("../utils/TextToSpeech");
  34. const { WorkspaceThread } = require("../models/workspaceThread");
  35. const truncate = require("truncate");
  36. const { purgeDocument } = require("../utils/files/purgeDocument");
  37. function workspaceEndpoints(app) {
  38. if (!app) return;
  39. const responseCache = new Map();
  40. app.post(
  41. "/workspace/new",
  42. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  43. async (request, response) => {
  44. try {
  45. const user = await userFromSession(request, response);
  46. const { name = null, onboardingComplete = false } = reqBody(request);
  47. const { workspace, message } = await Workspace.new(name, user?.id);
  48. await Telemetry.sendTelemetry(
  49. "workspace_created",
  50. {
  51. multiUserMode: multiUserMode(response),
  52. LLMSelection: process.env.LLM_PROVIDER || "openai",
  53. Embedder: process.env.EMBEDDING_ENGINE || "inherit",
  54. VectorDbSelection: process.env.VECTOR_DB || "lancedb",
  55. TTSSelection: process.env.TTS_PROVIDER || "native",
  56. },
  57. user?.id
  58. );
  59. await EventLogs.logEvent(
  60. "workspace_created",
  61. {
  62. workspaceName: workspace?.name || "Unknown Workspace",
  63. },
  64. user?.id
  65. );
  66. if (onboardingComplete === true)
  67. await Telemetry.sendTelemetry("onboarding_complete");
  68. response.status(200).json({ workspace, message });
  69. } catch (e) {
  70. console.error(e.message, e);
  71. response.sendStatus(500).end();
  72. }
  73. }
  74. );
  75. app.post(
  76. "/workspace/:slug/update",
  77. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  78. async (request, response) => {
  79. try {
  80. const user = await userFromSession(request, response);
  81. const { slug = null } = request.params;
  82. const data = reqBody(request);
  83. const currWorkspace = multiUserMode(response)
  84. ? await Workspace.getWithUser(user, { slug })
  85. : await Workspace.get({ slug });
  86. if (!currWorkspace) {
  87. response.sendStatus(400).end();
  88. return;
  89. }
  90. await Workspace.trackChange(currWorkspace, data, user);
  91. const { workspace, message } = await Workspace.update(
  92. currWorkspace.id,
  93. data
  94. );
  95. response.status(200).json({ workspace, message });
  96. } catch (e) {
  97. console.error(e.message, e);
  98. response.sendStatus(500).end();
  99. }
  100. }
  101. );
  102. app.post(
  103. "/workspace/:slug/upload",
  104. [
  105. validatedRequest,
  106. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  107. handleFileUpload,
  108. ],
  109. async function (request, response) {
  110. try {
  111. const Collector = new CollectorApi();
  112. const { originalname } = request.file;
  113. const processingOnline = await Collector.online();
  114. if (!processingOnline) {
  115. response
  116. .status(500)
  117. .json({
  118. success: false,
  119. error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
  120. })
  121. .end();
  122. return;
  123. }
  124. const { success, reason } =
  125. await Collector.processDocument(originalname);
  126. if (!success) {
  127. response.status(500).json({ success: false, error: reason }).end();
  128. return;
  129. }
  130. Collector.log(
  131. `Document ${originalname} uploaded processed and successfully. It is now available in documents.`
  132. );
  133. await Telemetry.sendTelemetry("document_uploaded");
  134. await EventLogs.logEvent(
  135. "document_uploaded",
  136. {
  137. documentName: originalname,
  138. },
  139. response.locals?.user?.id
  140. );
  141. response.status(200).json({ success: true, error: null });
  142. } catch (e) {
  143. console.error(e.message, e);
  144. response.sendStatus(500).end();
  145. }
  146. }
  147. );
  148. app.post(
  149. "/workspace/:slug/upload-link",
  150. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  151. async (request, response) => {
  152. try {
  153. const Collector = new CollectorApi();
  154. const { link = "" } = reqBody(request);
  155. const processingOnline = await Collector.online();
  156. if (!processingOnline) {
  157. response
  158. .status(500)
  159. .json({
  160. success: false,
  161. error: `Document processing API is not online. Link ${link} will not be processed automatically.`,
  162. })
  163. .end();
  164. return;
  165. }
  166. const { success, reason } = await Collector.processLink(link);
  167. if (!success) {
  168. response.status(500).json({ success: false, error: reason }).end();
  169. return;
  170. }
  171. Collector.log(
  172. `Link ${link} uploaded processed and successfully. It is now available in documents.`
  173. );
  174. await Telemetry.sendTelemetry("link_uploaded");
  175. await EventLogs.logEvent(
  176. "link_uploaded",
  177. { link },
  178. response.locals?.user?.id
  179. );
  180. response.status(200).json({ success: true, error: null });
  181. } catch (e) {
  182. console.error(e.message, e);
  183. response.sendStatus(500).end();
  184. }
  185. }
  186. );
  187. app.post(
  188. "/workspace/:slug/update-embeddings",
  189. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  190. async (request, response) => {
  191. try {
  192. const user = await userFromSession(request, response);
  193. const { slug = null } = request.params;
  194. const { adds = [], deletes = [] } = reqBody(request);
  195. const currWorkspace = multiUserMode(response)
  196. ? await Workspace.getWithUser(user, { slug })
  197. : await Workspace.get({ slug });
  198. if (!currWorkspace) {
  199. response.sendStatus(400).end();
  200. return;
  201. }
  202. await Document.removeDocuments(
  203. currWorkspace,
  204. deletes,
  205. response.locals?.user?.id
  206. );
  207. const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
  208. currWorkspace,
  209. adds,
  210. response.locals?.user?.id
  211. );
  212. const updatedWorkspace = await Workspace.get({ id: currWorkspace.id });
  213. response.status(200).json({
  214. workspace: updatedWorkspace,
  215. message:
  216. failedToEmbed.length > 0
  217. ? `${failedToEmbed.length} documents failed to add.\n\n${errors
  218. .map((msg) => `${msg}`)
  219. .join("\n\n")}`
  220. : null,
  221. });
  222. } catch (e) {
  223. console.error(e.message, e);
  224. response.sendStatus(500).end();
  225. }
  226. }
  227. );
  228. app.delete(
  229. "/workspace/:slug",
  230. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  231. async (request, response) => {
  232. try {
  233. const { slug = "" } = request.params;
  234. const user = await userFromSession(request, response);
  235. const VectorDb = getVectorDbClass();
  236. const workspace = multiUserMode(response)
  237. ? await Workspace.getWithUser(user, { slug })
  238. : await Workspace.get({ slug });
  239. if (!workspace) {
  240. response.sendStatus(400).end();
  241. return;
  242. }
  243. await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
  244. await DocumentVectors.deleteForWorkspace(workspace.id);
  245. await Document.delete({ workspaceId: Number(workspace.id) });
  246. await Workspace.delete({ id: Number(workspace.id) });
  247. await EventLogs.logEvent(
  248. "workspace_deleted",
  249. {
  250. workspaceName: workspace?.name || "Unknown Workspace",
  251. },
  252. response.locals?.user?.id
  253. );
  254. try {
  255. await VectorDb["delete-namespace"]({ namespace: slug });
  256. } catch (e) {
  257. console.error(e.message);
  258. }
  259. response.sendStatus(200).end();
  260. } catch (e) {
  261. console.error(e.message, e);
  262. response.sendStatus(500).end();
  263. }
  264. }
  265. );
  266. app.delete(
  267. "/workspace/:slug/reset-vector-db",
  268. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  269. async (request, response) => {
  270. try {
  271. const { slug = "" } = request.params;
  272. const user = await userFromSession(request, response);
  273. const VectorDb = getVectorDbClass();
  274. const workspace = multiUserMode(response)
  275. ? await Workspace.getWithUser(user, { slug })
  276. : await Workspace.get({ slug });
  277. if (!workspace) {
  278. response.sendStatus(400).end();
  279. return;
  280. }
  281. await DocumentVectors.deleteForWorkspace(workspace.id);
  282. await Document.delete({ workspaceId: Number(workspace.id) });
  283. await EventLogs.logEvent(
  284. "workspace_vectors_reset",
  285. {
  286. workspaceName: workspace?.name || "Unknown Workspace",
  287. },
  288. response.locals?.user?.id
  289. );
  290. try {
  291. await VectorDb["delete-namespace"]({ namespace: slug });
  292. } catch (e) {
  293. console.error(e.message);
  294. }
  295. response.sendStatus(200).end();
  296. } catch (e) {
  297. console.error(e.message, e);
  298. response.sendStatus(500).end();
  299. }
  300. }
  301. );
  302. app.get(
  303. "/workspaces",
  304. [validatedRequest, flexUserRoleValid([ROLES.all])],
  305. async (request, response) => {
  306. try {
  307. const user = await userFromSession(request, response);
  308. const workspaces = multiUserMode(response)
  309. ? await Workspace.whereWithUser(user)
  310. : await Workspace.where();
  311. response.status(200).json({ workspaces });
  312. } catch (e) {
  313. console.error(e.message, e);
  314. response.sendStatus(500).end();
  315. }
  316. }
  317. );
  318. app.get(
  319. "/workspace/:slug",
  320. [validatedRequest, flexUserRoleValid([ROLES.all])],
  321. async (request, response) => {
  322. try {
  323. const { slug } = request.params;
  324. const user = await userFromSession(request, response);
  325. const workspace = multiUserMode(response)
  326. ? await Workspace.getWithUser(user, { slug })
  327. : await Workspace.get({ slug });
  328. response.status(200).json({ workspace });
  329. } catch (e) {
  330. console.error(e.message, e);
  331. response.sendStatus(500).end();
  332. }
  333. }
  334. );
  335. app.get(
  336. "/workspace/:slug/chats",
  337. [validatedRequest, flexUserRoleValid([ROLES.all])],
  338. async (request, response) => {
  339. try {
  340. const { slug } = request.params;
  341. const user = await userFromSession(request, response);
  342. const workspace = multiUserMode(response)
  343. ? await Workspace.getWithUser(user, { slug })
  344. : await Workspace.get({ slug });
  345. if (!workspace) {
  346. response.sendStatus(400).end();
  347. return;
  348. }
  349. const history = multiUserMode(response)
  350. ? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id)
  351. : await WorkspaceChats.forWorkspace(workspace.id);
  352. response.status(200).json({ history: convertToChatHistory(history) });
  353. } catch (e) {
  354. console.error(e.message, e);
  355. response.sendStatus(500).end();
  356. }
  357. }
  358. );
  359. app.delete(
  360. "/workspace/:slug/delete-chats",
  361. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  362. async (request, response) => {
  363. try {
  364. const { chatIds = [] } = reqBody(request);
  365. const user = await userFromSession(request, response);
  366. const workspace = response.locals.workspace;
  367. if (!workspace || !Array.isArray(chatIds)) {
  368. response.sendStatus(400).end();
  369. return;
  370. }
  371. // This works for both workspace and threads.
  372. // we simplify this by just looking at workspace<>user overlap
  373. // since they are all on the same table.
  374. await WorkspaceChats.delete({
  375. id: { in: chatIds.map((id) => Number(id)) },
  376. user_id: user?.id ?? null,
  377. workspaceId: workspace.id,
  378. });
  379. response.sendStatus(200).end();
  380. } catch (e) {
  381. console.error(e.message, e);
  382. response.sendStatus(500).end();
  383. }
  384. }
  385. );
  386. app.delete(
  387. "/workspace/:slug/delete-edited-chats",
  388. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  389. async (request, response) => {
  390. try {
  391. const { startingId } = reqBody(request);
  392. const user = await userFromSession(request, response);
  393. const workspace = response.locals.workspace;
  394. await WorkspaceChats.delete({
  395. workspaceId: workspace.id,
  396. thread_id: null,
  397. user_id: user?.id,
  398. id: { gte: Number(startingId) },
  399. });
  400. response.sendStatus(200).end();
  401. } catch (e) {
  402. console.error(e.message, e);
  403. response.sendStatus(500).end();
  404. }
  405. }
  406. );
  407. app.post(
  408. "/workspace/:slug/update-chat",
  409. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  410. async (request, response) => {
  411. try {
  412. const { chatId, newText = null } = reqBody(request);
  413. if (!newText || !String(newText).trim())
  414. throw new Error("Cannot save empty response");
  415. const user = await userFromSession(request, response);
  416. const workspace = response.locals.workspace;
  417. const existingChat = await WorkspaceChats.get({
  418. workspaceId: workspace.id,
  419. thread_id: null,
  420. user_id: user?.id,
  421. id: Number(chatId),
  422. });
  423. if (!existingChat) throw new Error("Invalid chat.");
  424. const chatResponse = safeJsonParse(existingChat.response, null);
  425. if (!chatResponse) throw new Error("Failed to parse chat response");
  426. await WorkspaceChats._update(existingChat.id, {
  427. response: JSON.stringify({
  428. ...chatResponse,
  429. text: String(newText),
  430. }),
  431. });
  432. response.sendStatus(200).end();
  433. } catch (e) {
  434. console.error(e.message, e);
  435. response.sendStatus(500).end();
  436. }
  437. }
  438. );
  439. app.post(
  440. "/workspace/:slug/chat-feedback/:chatId",
  441. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  442. async (request, response) => {
  443. try {
  444. const { chatId } = request.params;
  445. const { feedback = null } = reqBody(request);
  446. const existingChat = await WorkspaceChats.get({
  447. id: Number(chatId),
  448. workspaceId: response.locals.workspace.id,
  449. });
  450. if (!existingChat) {
  451. response.status(404).end();
  452. return;
  453. }
  454. const result = await WorkspaceChats.updateFeedbackScore(
  455. chatId,
  456. feedback
  457. );
  458. response.status(200).json({ success: result });
  459. } catch (error) {
  460. console.error("Error updating chat feedback:", error);
  461. response.status(500).end();
  462. }
  463. }
  464. );
  465. app.get(
  466. "/workspace/:slug/suggested-messages",
  467. [validatedRequest, flexUserRoleValid([ROLES.all])],
  468. async function (request, response) {
  469. try {
  470. const { slug } = request.params;
  471. const suggestedMessages =
  472. await WorkspaceSuggestedMessages.getMessages(slug);
  473. response.status(200).json({ success: true, suggestedMessages });
  474. } catch (error) {
  475. console.error("Error fetching suggested messages:", error);
  476. response
  477. .status(500)
  478. .json({ success: false, message: "Internal server error" });
  479. }
  480. }
  481. );
  482. app.post(
  483. "/workspace/:slug/suggested-messages",
  484. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  485. async (request, response) => {
  486. try {
  487. const { messages = [] } = reqBody(request);
  488. const { slug } = request.params;
  489. if (!Array.isArray(messages)) {
  490. return response.status(400).json({
  491. success: false,
  492. message: "Invalid message format. Expected an array of messages.",
  493. });
  494. }
  495. await WorkspaceSuggestedMessages.saveAll(messages, slug);
  496. return response.status(200).json({
  497. success: true,
  498. message: "Suggested messages saved successfully.",
  499. });
  500. } catch (error) {
  501. console.error("Error processing the suggested messages:", error);
  502. response.status(500).json({
  503. success: true,
  504. message: "Error saving the suggested messages.",
  505. });
  506. }
  507. }
  508. );
  509. app.post(
  510. "/workspace/:slug/update-pin",
  511. [
  512. validatedRequest,
  513. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  514. validWorkspaceSlug,
  515. ],
  516. async (request, response) => {
  517. try {
  518. const { docPath, pinStatus = false } = reqBody(request);
  519. const workspace = response.locals.workspace;
  520. const document = await Document.get({
  521. workspaceId: workspace.id,
  522. docpath: docPath,
  523. });
  524. if (!document) return response.sendStatus(404).end();
  525. await Document.update(document.id, { pinned: pinStatus });
  526. return response.status(200).end();
  527. } catch (error) {
  528. console.error("Error processing the pin status update:", error);
  529. return response.status(500).end();
  530. }
  531. }
  532. );
  533. app.get(
  534. "/workspace/:slug/tts/:chatId",
  535. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  536. async function (request, response) {
  537. try {
  538. const { chatId } = request.params;
  539. const workspace = response.locals.workspace;
  540. const cacheKey = `${workspace.slug}:${chatId}`;
  541. const wsChat = await WorkspaceChats.get({
  542. id: Number(chatId),
  543. workspaceId: workspace.id,
  544. });
  545. const cachedResponse = responseCache.get(cacheKey);
  546. if (cachedResponse) {
  547. response.writeHead(200, {
  548. "Content-Type": cachedResponse.mime || "audio/mpeg",
  549. });
  550. response.end(cachedResponse.buffer);
  551. return;
  552. }
  553. const text = safeJsonParse(wsChat.response, null)?.text;
  554. if (!text) return response.sendStatus(204).end();
  555. const TTSProvider = getTTSProvider();
  556. const buffer = await TTSProvider.ttsBuffer(text);
  557. if (buffer === null) return response.sendStatus(204).end();
  558. responseCache.set(cacheKey, { buffer, mime: "audio/mpeg" });
  559. response.writeHead(200, {
  560. "Content-Type": "audio/mpeg",
  561. });
  562. response.end(buffer);
  563. return;
  564. } catch (error) {
  565. console.error("Error processing the TTS request:", error);
  566. response.status(500).json({ message: "TTS could not be completed" });
  567. }
  568. }
  569. );
  570. app.get(
  571. "/workspace/:slug/pfp",
  572. [validatedRequest, flexUserRoleValid([ROLES.all])],
  573. async function (request, response) {
  574. try {
  575. const { slug } = request.params;
  576. const cachedResponse = responseCache.get(slug);
  577. if (cachedResponse) {
  578. response.writeHead(200, {
  579. "Content-Type": cachedResponse.mime || "image/png",
  580. });
  581. response.end(cachedResponse.buffer);
  582. return;
  583. }
  584. const pfpPath = await determineWorkspacePfpFilepath(slug);
  585. if (!pfpPath) {
  586. response.sendStatus(204).end();
  587. return;
  588. }
  589. const { found, buffer, mime } = fetchPfp(pfpPath);
  590. if (!found) {
  591. response.sendStatus(204).end();
  592. return;
  593. }
  594. responseCache.set(slug, { buffer, mime });
  595. response.writeHead(200, {
  596. "Content-Type": mime || "image/png",
  597. });
  598. response.end(buffer);
  599. return;
  600. } catch (error) {
  601. console.error("Error processing the logo request:", error);
  602. response.status(500).json({ message: "Internal server error" });
  603. }
  604. }
  605. );
  606. app.post(
  607. "/workspace/:slug/upload-pfp",
  608. [
  609. validatedRequest,
  610. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  611. handlePfpUpload,
  612. ],
  613. async function (request, response) {
  614. try {
  615. const { slug } = request.params;
  616. const uploadedFileName = request.randomFileName;
  617. if (!uploadedFileName) {
  618. return response.status(400).json({ message: "File upload failed." });
  619. }
  620. const workspaceRecord = await Workspace.get({
  621. slug,
  622. });
  623. const oldPfpFilename = workspaceRecord.pfpFilename;
  624. if (oldPfpFilename) {
  625. const storagePath = path.join(__dirname, "../storage/assets/pfp");
  626. const oldPfpPath = path.join(
  627. storagePath,
  628. normalizePath(workspaceRecord.pfpFilename)
  629. );
  630. if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
  631. throw new Error("Invalid path name");
  632. if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
  633. }
  634. const { workspace, message } = await Workspace._update(
  635. workspaceRecord.id,
  636. {
  637. pfpFilename: uploadedFileName,
  638. }
  639. );
  640. return response.status(workspace ? 200 : 500).json({
  641. message: workspace
  642. ? "Profile picture uploaded successfully."
  643. : message,
  644. });
  645. } catch (error) {
  646. console.error("Error processing the profile picture upload:", error);
  647. response.status(500).json({ message: "Internal server error" });
  648. }
  649. }
  650. );
  651. app.delete(
  652. "/workspace/:slug/remove-pfp",
  653. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  654. async function (request, response) {
  655. try {
  656. const { slug } = request.params;
  657. const workspaceRecord = await Workspace.get({
  658. slug,
  659. });
  660. const oldPfpFilename = workspaceRecord.pfpFilename;
  661. if (oldPfpFilename) {
  662. const storagePath = path.join(__dirname, "../storage/assets/pfp");
  663. const oldPfpPath = path.join(
  664. storagePath,
  665. normalizePath(oldPfpFilename)
  666. );
  667. if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
  668. throw new Error("Invalid path name");
  669. if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
  670. }
  671. const { workspace, message } = await Workspace._update(
  672. workspaceRecord.id,
  673. {
  674. pfpFilename: null,
  675. }
  676. );
  677. // Clear the cache
  678. responseCache.delete(slug);
  679. return response.status(workspace ? 200 : 500).json({
  680. message: workspace
  681. ? "Profile picture removed successfully."
  682. : message,
  683. });
  684. } catch (error) {
  685. console.error("Error processing the profile picture removal:", error);
  686. response.status(500).json({ message: "Internal server error" });
  687. }
  688. }
  689. );
  690. app.post(
  691. "/workspace/:slug/thread/fork",
  692. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  693. async (request, response) => {
  694. try {
  695. const user = await userFromSession(request, response);
  696. const workspace = response.locals.workspace;
  697. const { chatId, threadSlug } = reqBody(request);
  698. if (!chatId)
  699. return response.status(400).json({ message: "chatId is required" });
  700. // Get threadId we are branching from if that request body is sent
  701. // and is a valid thread slug.
  702. const threadId = !!threadSlug
  703. ? (
  704. await WorkspaceThread.get({
  705. slug: String(threadSlug),
  706. workspace_id: workspace.id,
  707. })
  708. )?.id ?? null
  709. : null;
  710. const chatsToFork = await WorkspaceChats.where(
  711. {
  712. workspaceId: workspace.id,
  713. user_id: user?.id,
  714. include: true, // only duplicate visible chats
  715. thread_id: threadId,
  716. api_session_id: null, // Do not include API session chats.
  717. id: { lte: Number(chatId) },
  718. },
  719. null,
  720. { id: "asc" }
  721. );
  722. const { thread: newThread, message: threadError } =
  723. await WorkspaceThread.new(workspace, user?.id);
  724. if (threadError)
  725. return response.status(500).json({ error: threadError });
  726. let lastMessageText = "";
  727. const chatsData = chatsToFork.map((chat) => {
  728. const chatResponse = safeJsonParse(chat.response, {});
  729. if (chatResponse?.text) lastMessageText = chatResponse.text;
  730. return {
  731. workspaceId: workspace.id,
  732. prompt: chat.prompt,
  733. response: JSON.stringify(chatResponse),
  734. user_id: user?.id,
  735. thread_id: newThread.id,
  736. };
  737. });
  738. await WorkspaceChats.bulkCreate(chatsData);
  739. await WorkspaceThread.update(newThread, {
  740. name: !!lastMessageText
  741. ? truncate(lastMessageText, 22)
  742. : "Forked Thread",
  743. });
  744. await Telemetry.sendTelemetry("thread_forked");
  745. await EventLogs.logEvent(
  746. "thread_forked",
  747. {
  748. workspaceName: workspace?.name || "Unknown Workspace",
  749. threadName: newThread.name,
  750. },
  751. user?.id
  752. );
  753. response.status(200).json({ newThreadSlug: newThread.slug });
  754. } catch (e) {
  755. console.error(e.message, e);
  756. response.status(500).json({ message: "Internal server error" });
  757. }
  758. }
  759. );
  760. app.put(
  761. "/workspace/workspace-chats/:id",
  762. [validatedRequest, flexUserRoleValid([ROLES.all])],
  763. async (request, response) => {
  764. try {
  765. const { id } = request.params;
  766. const user = await userFromSession(request, response);
  767. const validChat = await WorkspaceChats.get({
  768. id: Number(id),
  769. user_id: user?.id ?? null,
  770. });
  771. if (!validChat)
  772. return response
  773. .status(404)
  774. .json({ success: false, error: "Chat not found." });
  775. await WorkspaceChats._update(validChat.id, { include: false });
  776. response.json({ success: true, error: null });
  777. } catch (e) {
  778. console.error(e.message, e);
  779. response.status(500).json({ success: false, error: "Server error" });
  780. }
  781. }
  782. );
  783. /** Handles the uploading and embedding in one-call by uploading via drag-and-drop in chat container. */
  784. app.post(
  785. "/workspace/:slug/upload-and-embed",
  786. [
  787. validatedRequest,
  788. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  789. handleFileUpload,
  790. ],
  791. async function (request, response) {
  792. try {
  793. const { slug = null } = request.params;
  794. const user = await userFromSession(request, response);
  795. const currWorkspace = multiUserMode(response)
  796. ? await Workspace.getWithUser(user, { slug })
  797. : await Workspace.get({ slug });
  798. if (!currWorkspace) {
  799. response.sendStatus(400).end();
  800. return;
  801. }
  802. const Collector = new CollectorApi();
  803. const { originalname } = request.file;
  804. const processingOnline = await Collector.online();
  805. if (!processingOnline) {
  806. response
  807. .status(500)
  808. .json({
  809. success: false,
  810. error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
  811. })
  812. .end();
  813. return;
  814. }
  815. const { success, reason, documents } =
  816. await Collector.processDocument(originalname);
  817. if (!success || documents?.length === 0) {
  818. response.status(500).json({ success: false, error: reason }).end();
  819. return;
  820. }
  821. Collector.log(
  822. `Document ${originalname} uploaded processed and successfully. It is now available in documents.`
  823. );
  824. await Telemetry.sendTelemetry("document_uploaded");
  825. await EventLogs.logEvent(
  826. "document_uploaded",
  827. {
  828. documentName: originalname,
  829. },
  830. response.locals?.user?.id
  831. );
  832. const document = documents[0];
  833. const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
  834. currWorkspace,
  835. [document.location],
  836. response.locals?.user?.id
  837. );
  838. if (failedToEmbed.length > 0)
  839. return response
  840. .status(200)
  841. .json({ success: false, error: errors?.[0], document: null });
  842. response.status(200).json({
  843. success: true,
  844. error: null,
  845. document: { id: document.id, location: document.location },
  846. });
  847. } catch (e) {
  848. console.error(e.message, e);
  849. response.sendStatus(500).end();
  850. }
  851. }
  852. );
  853. app.delete(
  854. "/workspace/:slug/remove-and-unembed",
  855. [
  856. validatedRequest,
  857. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  858. handleFileUpload,
  859. ],
  860. async function (request, response) {
  861. try {
  862. const { slug = null } = request.params;
  863. const body = reqBody(request);
  864. const user = await userFromSession(request, response);
  865. const currWorkspace = multiUserMode(response)
  866. ? await Workspace.getWithUser(user, { slug })
  867. : await Workspace.get({ slug });
  868. if (!currWorkspace || !body.documentLocation)
  869. return response.sendStatus(400).end();
  870. // Will delete the document from the entire system + wil unembed it.
  871. await purgeDocument(body.documentLocation);
  872. response.status(200).end();
  873. } catch (e) {
  874. console.error(e.message, e);
  875. response.sendStatus(500).end();
  876. }
  877. }
  878. );
  879. }
  880. module.exports = { workspaceEndpoints };