Skip to content

Commit

Permalink
Merge pull request #98 from identity-research-lab/fix-bad-relation
Browse files Browse the repository at this point in the history
Fix an issue with categories creating invalid relations with codes
  • Loading branch information
CoralineAda authored Aug 26, 2024
2 parents f3ff242 + 433a8ea commit b03835d
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 71 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ To create your local `.env` file, make a copy of `.env.example`. (This filename

Now, when you open the `.env` file in your text editor, you'll see a list of key/value pairs that need filling in. These are configuration options including database credentials, customizations, and the API keys that TMI-Web needs to communicate with third-party services. Follow the instructions provided in the file to register for the appropriate API keys.

### Run the test suite

rspec --format documentation spec/

To always run rspec with the documentation flag:

echo "--format documentation" > .rspec

### Start the background job runner

From the root directory of tmi-web, launch Sidekiq by typing:
Expand Down Expand Up @@ -135,14 +143,6 @@ To update the Neo4j database, type:

## Developer tips

### Run the test suite

rspec --format documentation spec/

To always run rspec with the documentation flag:

echo "--format documentation" > .rspec

### Clear the Sidekiq (background job) queue

Launch the local interactive Rails console:
Expand Down
29 changes: 8 additions & 21 deletions app/models/category.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,23 @@ class Category

has_many :out, :codes, rel_class: :CategorizedAs, dependent: :delete_orphans

# This is the prompt sent to the selected AI agent to provide instructions on category derivision.
PROMPT = %{
You are a social researcher doing data analysis. Please generate a list of the 20 most relevant themes from the following list of codes. The themes should be all lowercase and contain no punctuation. Codes should be stripped of quotation marks. Return each code with an array of its categories in JSON format. Use this JSON as the format:
{
"themes" : [
{
"theme": "foo",
"codes": [ "bar", "bat", "baz"]
}
]
}
The codes are as follows:
}

# Regenerates Category objects based on codes within a given context.
# This method uses the Clients::OpenAi client passing the codes as an argument to the prompt.
# The agent returns an array of themes, which are then captured as Category objects.
def self.from(context)
codes = Code.where(context: context)
response = Clients::OpenAi.request("#{PROMPT} #{codes.map(&:name).join(',')}")
return unless response['themes']
return unless codes.any?

text = codes.map(&:name).join(',')
return unless text.present?
return unless themes = DeriveThemes.perform(text)

Category.where(context: context).destroy_all

response['themes'].each do |record|
category = Category.find_or_create_by(name: record['theme'], context: context)
themes.each do |theme|
category = Category.find_or_create_by(name: theme['theme'].strip.downcase, context: context)
codes.each do |code|
next unless record['codes'].include?(code.name)
next unless theme['codes'].include?(code.name)
CategorizedAs.create(from_node: category, to_node: code)
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/code.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Code

validates :name, presence: true
validates :context, presence: true
validates_uniqueness_of :name, :scope => :context
validates_uniqueness_of :name, scope: :context

has_many :out, :personas, rel_class: :Experiences
has_many :in, :categories, rel_class: :CategorizedAs
Expand Down
39 changes: 39 additions & 0 deletions app/services/derive_themes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class DeriveThemes

attr_accessor :text

# This is the prompt sent to the selected AI agent to provide instructions on category derivision.
PROMPT = %{
You are a social researcher doing data analysis. Please generate a list of the 20 most relevant themes from the following list of codes. The themes should be all lowercase and contain no punctuation. Codes should be stripped of quotation marks. Return each code with an array of its categories in JSON format. Use this JSON as the format:
{
"themes" : [
{
"theme": "foo",
"codes": [ "bar", "bat", "baz"]
}
]
}
The codes are as follows:
}

def self.perform(text)
new(text).perform
end

def initialize(text)
@text = text
end

# Uses the OpenAI client to pass the prompt and text through the API for sentiment analysis.
def perform
return false unless text.present?

response = Clients::OpenAi.request("#{PROMPT} #{self.text}")
return false unless response['themes'].present?
return response['themes']
end


end
18 changes: 11 additions & 7 deletions app/services/export_to_graph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def perform
# Hydrates the associated Persona with data from the SurveyResponse.
# Note that this operation is destructive to a Persona that already exists.
def persona
@persona ||= Persona.create(
@persona ||= Persona.find_or_create_by(
name: "Persona #{survey_response.identifier}",
survey_response_id: survey_response.id,
permalink: survey_response.permalink
Expand All @@ -46,9 +46,11 @@ def populate_experience_codes
"notes" => survey_response.notes_codes
}
contexts_and_codes.each do |context, codes|
codes.each do |name|
code = Code.find_or_create_by(name: name, context: context)
Experiences.create(from_node: persona, to_node: code)
codes.compact.uniq.each do |name|
if code = Code.find_or_create_by(name: name, context: context)
next unless code.valid?
Experiences.create(from_node: persona, to_node: code)
end
end
end

Expand All @@ -67,9 +69,11 @@ def populate_id_codes
"pronouns" => survey_response.pronouns_id_codes
}
contexts_and_codes.each do |context, codes|
codes.each do |name|
identity = Identity.find_or_create_by(name: name, context: context)
IdentifiesWith.create(from_node: persona, to_node: identity)
codes.compact.uniq.each do |name|
if identity = Identity.find_or_create_by(name: name.strip, context: context)
next unless identity.valid?
IdentifiesWith.create(from_node: persona, to_node: identity)
end
end
end
end
Expand Down
6 changes: 1 addition & 5 deletions lib/clients/open_ai.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# A wrapper class for the OpenAI API.
# A wrapper class for the OpenAI API client.
class Clients::OpenAi

# Sends a prompt to the configured model and returns the relevant portion of the response.
#
# @param prompt [String] the text of the prompt to send to the model.
# @return [String] the JSON response returned by the API.

def self.request(prompt)
client = OpenAI::Client.new
response = client.chat(
Expand Down
39 changes: 10 additions & 29 deletions spec/services/export_to_graph_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
allow(Persona).to receive(:find_or_initialize_by).and_return(persona)
allow(Persona).to receive(:create).and_return(persona)
allow(persona).to receive(:destroy)
allow(IdentifiesWith).to receive(:create)
end

let(:service) { ExportToGraph.new(1) }

let(:code) { Code.new(name: "not okay", context: "age") }
let(:identity) { Identity.new(name: "genx", context: "age") }

let(:survey_response) {
SurveyResponse.new(
id: 1,
response_id: 1,
age_exp_codes: ["not okay"],
age_id_codes: ["genx"]
)
Expand All @@ -23,35 +28,11 @@
survey_response_id: 1
)
}
let(:service) {
ExportToGraph.new(1)
}

it 'assigns its SurveyResponse' do
expect(service.survey_response.id).to eq(1)
end

context "populates its codes" do

it 'finds or creates an associated code' do
allow(Identity).to receive(:find_or_create_by)
allow(IdentifiesWith).to receive(:create)

expect(Code).to receive(:find_or_create_by).with(name: "not okay", context: "age").and_return(Code.new)
expect(Experiences).to receive(:create).and_return(Experiences.new)

service.perform
end

it 'finds or creates an associated identity' do
allow(Code).to receive(:find_or_create_by)
allow(Experiences).to receive(:create)

expect(Identity).to receive(:find_or_create_by).with(name: "genx", context: "age").and_return(Identity.new)
expect(IdentifiesWith).to receive(:create).and_return(IdentifiesWith.new)
service.perform
end

it 'populates experience and identity codes' do
expect(Code).to receive(:find_or_create_by).with(name: "not okay", context: "age")
expect(Identity).to receive(:find_or_create_by).with(name: "genx", context: "age")
service.perform
end

end

0 comments on commit b03835d

Please sign in to comment.