|
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 |
|
|
|
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 |
|
|