Source: pages/api/data/completions.js

/** @module pages/api/data/ai-comments */
import { getServerSession } from "../../../lib/auth";
import { cosineSimilarityScore } from "../../../lib/data-analysis/cosine-similarity";
import {
  getPostById,
  getUserByEmail,
  hasAiCommentedOnPost,
} from "../../../lib/db/reads";
import {
  generateCompletion,
  saveCosineSimilarityScore,
} from "../../../lib/db/writes";
import {
  BAD_REQUEST,
  METHOD_NOT_ALLOWED,
  SERVER_ERROR,
  FORBIDDEN,
} from "../../../lib/errors";

/**
 * This is the API route for creating a comment generated by AI.
 * Through this endpoint, this service hits OpenAI to generate a completion
 * and then saves it to the database, associating it with a comment.
 */
function completionsHandler(req, res) {
  switch (req.method) {
    case "POST":
      return completionsPost(req, res);
    default:
      res.setHeader("Allow", ["POST"]);
      res.status(405).end(METHOD_NOT_ALLOWED);
      return res;
  }
}

/**
 * This is the POST handler for the completions API route.
 * It is responsible for generating a completion and saving it to the database.
 * It starts the first (out of 3) completion workflow, which is triggered by a user
 * when submitting a dream.
 */
async function completionsPost(req, res) {
  const session = await getServerSession(req, res);

  if (!session) {
    res.status(403).end(FORBIDDEN);
    return res;
  }

  if (!req.body.dreamId || !req.body.text) {
    res.status(400).end(BAD_REQUEST);
    return res;
  }

  const hasCommented = await hasAiCommentedOnPost(req.body.dreamId);
  const dreamData = await getPostById(req.body.dreamId);
  const user = await getUserByEmail(session.user.email);

  if (
    dreamData?.visibility === "private" &&
    !user?.settings?.aiCommentsOnPrivatePosts
  ) {
    console.log(
      "Post visibility not public nor anonymous, and user settings for comments on private posts disabled. Not generating completion"
    );

    res.setHeader("Content-Type", "application/json");
    res.status(200).end("OK");
    return res;
  }

  if (dreamData?.text?.length < 30) {
    console.log(
      "Post length less than 30 characters. Not generating completion"
    );

    res.setHeader("Content-Type", "application/json");
    res.status(200).end("OK");
    return res;
  }

  if (hasCommented) {
    // TODO: Even though we're not generating a completion as of now, we should still
    // calculate the cosine similarity score to evaluate whether we should generate
    // another completion or not to override the previous one based on what has changed
    // in the dream text.
    // We are logging the score for now for further analysis.
    const csScore = cosineSimilarityScore(req.body.text, dreamData?.text);
    await saveCosineSimilarityScore({
      postId: req.body.dreamId,
      previousPostLength: dreamData?.text.length,
      currentPostLength: req.body.text.length,
      cosineSimilarityScore: csScore,
    });

    res.setHeader("Content-Type", "application/json");
    res.status(200).send("OK");
    return res;
  }

  try {
    console.log("Generating completion from workflow #1");
    await generateCompletion(req.body.dreamId, req.body.text, session);

    res.setHeader("Content-Type", "application/json");
    res.status(202).send("Accepted");

    return res;
  } catch (error) {
    console.error({
      error,
      service: "api",
      pathname: "/api/data/completions",
      method: "post",
    });
    res.status(500).end(SERVER_ERROR);

    return res;
  }
}

export default completionsHandler;