defmodule Medicode.Feedback do @moduledoc """ Context for managing user feedback on transcribed text and codes. """ import Ecto.Query import Pgvector.Ecto.Query, only: [cosine_distance: 2] alias Ecto.Multi alias Medicode.Feedback.CodeFeedback alias Medicode.Repo def track_response(params) do # TODO: Move this changeset into Multi and read errors from the result within the case statement changeset = CodeFeedback.changeset(%CodeFeedback{}, %{ code_vector_id: params["code_vector_id"], response: params["response"], text: params["text"], text_vector: params["text_vector"], user_id: params["user_id"] }) multi = Multi.new() |> Multi.exists?(:existing_feedback, fn _changes -> code_feedback_for_user_and_code_vector_query( Ecto.Changeset.fetch_change!(changeset, :text), Ecto.Changeset.fetch_change!(changeset, :user_id), Ecto.Changeset.fetch_change!(changeset, :code_vector_id) ) |> where([f], f.response == ^Ecto.Changeset.fetch_change!(changeset, :response)) end) |> Multi.delete_all(:delete_previous_feedback, fn _changes -> code_feedback_for_user_and_code_vector_query( Ecto.Changeset.fetch_change!(changeset, :text), Ecto.Changeset.fetch_change!(changeset, :user_id), Ecto.Changeset.fetch_change!(changeset, :code_vector_id) ) end) |> Multi.merge(fn %{existing_feedback: existing_feedback} -> if existing_feedback do Multi.new() else Multi.new() |> Multi.insert(:new_feedback, changeset) end end) case Repo.transaction(multi) do {:ok, _code_feedback} -> {:ok, message_for_response(params)} {:error, _failed_operation, _failed_value, _changes_so_far} -> {:error, Repo.collect_errors(changeset)} end end def insert_and_return(params) do changeset = CodeFeedback.changeset(%CodeFeedback{}, params) case Repo.insert(changeset) do {:ok, code_feedback} -> code_feedback {:error, _changeset} -> nil end end def delete(id) do case Repo.delete(id) do {:ok, _code_feedback} -> {:ok, "Deleted code feedback."} {:error, changeset} -> {:error, Repo.collect_errors(changeset)} end end def code_feedback_for_user_and_code_vector_query(text, user_id, code_vector_id) do from(f in CodeFeedback, inner_join: u in assoc(f, :user), inner_join: v in assoc(f, :code_vector), where: f.user_id == ^user_id and f.code_vector_id == ^code_vector_id and f.text == ^text ) end def code_feedback_for_user_and_code_vector(text, user_id, code_vector_id) do text |> code_feedback_for_user_and_code_vector_query(user_id, code_vector_id) |> Repo.one() end defp message_for_response(params) do action = if params["response"] == "true", do: "upvoted", else: "downvoted" "Successfully #{action} code" end @doc """ Given a portion of transcribed text, finds the most-relevant previous feedback for that transcribed text. The response from this previous feedback can then be used to modify the weight for the associated code when classifying this `text`. """ def find_related_feedback(search_vector, opts \\ []) do k = Keyword.get(opts, :num_results, 5) similarity_threshold = Keyword.get(opts, :similarity_threshold, 0.80) query = from(f in CodeFeedback, order_by: cosine_distance(f.text_vector, ^search_vector), where: 1 - cosine_distance(f.text_vector, ^search_vector) > ^similarity_threshold, limit: ^k, select: %{ code_vector_id: f.code_vector_id, response: f.response, cosine_similarity: 1 - cosine_distance(f.text_vector, ^search_vector) } ) Repo.all(query) end end