Source: lib/db/writes.js

  1. /** @module lib/db/writes */
  2. import { ObjectID } from "bson";
  3. import {
  4. getCompletionsCollection,
  5. getCosineSimilarityCollection,
  6. } from "./mongodb";
  7. import { getUserByEmail } from "./reads";
  8. import OpenAI from "openai";
  9. import { hitChiron } from "../chiron";
  10. import { createComment } from "./comments/writes";
  11. import { logError } from "../o11y/log";
  12. const openai = new OpenAI({
  13. apiKey: process.env.OPENAI_TOKEN,
  14. });
  15. // TODO: Replace all dream-specific scheme by post-specific scheme
  16. export * from "./posts/writes";
  17. export * from "./users/writes";
  18. export * from "./comments/writes";
  19. export * from "./inbox/writes";
  20. export * from "./stars/writes";
  21. export * from "./account/writes";
  22. // All methods below this line shouldn't be ported to any package.
  23. /**
  24. * Generates a comment from a completion, created by an AI.
  25. * It uses the createComment method to create the comment.
  26. *
  27. * @todo move AI logic from createComment to this method
  28. * @param {string} comment
  29. * @param {string} postId
  30. */
  31. export async function createAIComment(comment, postId) {
  32. const data = {
  33. comment,
  34. dreamId: postId,
  35. session: {
  36. user: {
  37. name: "Sonia",
  38. email: "no-reply@eutiveumsonho.com",
  39. image: "https://eutiveumsonho.com/android-chrome-192x192.png",
  40. },
  41. expires: new Date(8640000000000000), // Maximum timestamp,
  42. },
  43. };
  44. await createComment(data);
  45. }
  46. /**
  47. * Saves a completion to the database
  48. *
  49. * @param {*} completion
  50. * @param {*} postId
  51. * @param {*} userEmail
  52. * @param {*} userId
  53. */
  54. export async function saveCompletion(completion, postId, userEmail, userId) {
  55. const collection = await getCompletionsCollection();
  56. // This should never happen as the client route (triggered first time a completion
  57. // is generated) always provides the userEmail from the session.
  58. // In the meanwhile, backend routes (triggered from Chiron or from upateDream), always provides the userId.
  59. // Backend routes
  60. if (!userEmail && !userId) {
  61. throw new Error("No user data provided");
  62. }
  63. let user = {};
  64. // Client route; first completion
  65. if (userEmail && !userId) {
  66. user = await getUserByEmail(userEmail);
  67. }
  68. const data = {
  69. userId: ObjectID(userId ? userId : userEmail && user ? user._id : userId),
  70. dreamId: ObjectID(postId),
  71. completion,
  72. pendingReview: true,
  73. createdAt: new Date().toISOString(),
  74. updatedAt: new Date().toISOString(),
  75. };
  76. const result = await collection.insertOne(data);
  77. return { result, data };
  78. }
  79. /**
  80. * Starts the completion generation process, which is followed by a
  81. * human-in-the-loop review process until it gets back to this
  82. * service
  83. *
  84. * session and userId params are optional because this method
  85. * has two possible workflows, one using the session and the other
  86. * using the userIds. The session workflow starts on the frontend,
  87. * while the userId workflow starts on the backend.
  88. *
  89. * This is this way because saveCompletions method is, and this method
  90. * calls it.
  91. *
  92. * @param {string} postId The dream id
  93. * @param {string} text The dream data text
  94. * @param {object} session (Optional) The session object. If not provided, the `userId` must be provided.
  95. * @param {string} userId (Optional) The user id. If not provided, the `session` must be provided.
  96. */
  97. export async function generateCompletion(postId, text, session, userId) {
  98. const params = {
  99. messages: [
  100. { role: "system", content: systemInstruction },
  101. { role: "user", content: text },
  102. ],
  103. /**
  104. * @link https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo
  105. */
  106. model: "gpt-4o-mini",
  107. /**
  108. * What sampling temperature to use, between 0 and 2.
  109. * Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
  110. * We generally recommend altering this or top_p but not both.
  111. *
  112. * @link https://platform.openai.com/docs/api-reference/chat/create#chat-create-temperature
  113. * @default 1
  114. */
  115. temperature: 0.2,
  116. /**
  117. * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
  118. * @link https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids
  119. */
  120. user: userId,
  121. };
  122. const completion = await openai.chat.completions.create(params);
  123. const { result, data } = await saveCompletion(
  124. completion,
  125. postId,
  126. session?.user?.email,
  127. userId
  128. );
  129. if (result?.acknowledged || result?.insertedId) {
  130. await hitChiron(data);
  131. }
  132. }
  133. /**
  134. * Some prompt instructions for the AI to generate completions.
  135. */
  136. const systemInstruction = `Act as a psychotherapist specializing in dream interpretation with a deep knowledge of archetypes and mythology.
  137. When presented with a dream narrative, provide insightful analysis and open-ended questions to help the dreamer gain a deeper understanding of their dream.
  138. Do not provide personal opinions or assumptions about the dreamer.
  139. Provide only factual interpretations based on the information given.
  140. Keep your answer short and concise, with 5000 characters at most.
  141. If the dream looks incomplete, never complete it.
  142. Always respond in the language in which the dream narrative is presented, even if it differs from the initial instruction language (English).`;
  143. /**
  144. * Saves the cosine similarity score between two texts.
  145. */
  146. export async function saveCosineSimilarityScore(scoreData) {
  147. const csCollection = await getCosineSimilarityCollection();
  148. try {
  149. await csCollection.insertOne({
  150. scoreData,
  151. createdAt: new Date().toISOString(),
  152. });
  153. } catch (error) {
  154. logError(error, {
  155. service: "db",
  156. component: "saveCosineSimilarityScore",
  157. });
  158. }
  159. }