Spaces:
Runtime error
Runtime error
Commit ·
bc4f6b8
1
Parent(s): 3f986e6
Delete utils
Browse files- utils/app_common.py +0 -205
- utils/database_helper.py +0 -92
- utils/eleven_voice_cache.py +0 -65
- utils/llm_helper.py +0 -243
- utils/study.py +0 -148
- utils/study_repository.py +0 -200
utils/app_common.py
DELETED
|
@@ -1,205 +0,0 @@
|
|
| 1 |
-
import re
|
| 2 |
-
from bson import ObjectId
|
| 3 |
-
from dotenv import load_dotenv
|
| 4 |
-
import os
|
| 5 |
-
from interviewer_utils.interviewer import Interviewer
|
| 6 |
-
from utils.study import Study
|
| 7 |
-
from utils.study_repository import StudyRepository
|
| 8 |
-
from typing import Union, List
|
| 9 |
-
from utils.database_helper import DatabaseIO
|
| 10 |
-
import gradio as gr
|
| 11 |
-
|
| 12 |
-
load_dotenv()
|
| 13 |
-
INTERVIEW_USERS_COLLECTION = os.environ['MONGO_COLLECTION_USERS']
|
| 14 |
-
RESEARCHER_USERS_COLLECTION = os.environ['MONGO_COLLECTION_USERS']
|
| 15 |
-
STUDIES_COLLECTION = os.environ['MONGO_COLLECTION_STUDIES']
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def is_valid_email(email):
|
| 19 |
-
# Simple regex pattern for email validation
|
| 20 |
-
pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
|
| 21 |
-
return bool(re.match(pattern, email))
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def set_active_study(this_study_repository,
|
| 25 |
-
study_id):
|
| 26 |
-
"""
|
| 27 |
-
Using a repository object creates a study object for the given id
|
| 28 |
-
returns the study object and a status message
|
| 29 |
-
"""
|
| 30 |
-
|
| 31 |
-
study_id = study_id.split(':')[0]
|
| 32 |
-
this_study = this_study_repository.get_studies(study_id=study_id)
|
| 33 |
-
|
| 34 |
-
if not this_study:
|
| 35 |
-
return {}, "study does not exist"
|
| 36 |
-
|
| 37 |
-
if type(this_study) == list and len(this_study):
|
| 38 |
-
this_study = this_study[0]
|
| 39 |
-
|
| 40 |
-
# initialize a new study with this study in memory!
|
| 41 |
-
try:
|
| 42 |
-
# study validation is here! if db schema is older, will be notified during dev
|
| 43 |
-
active_study_obj = Study(**this_study)
|
| 44 |
-
except Exception as e:
|
| 45 |
-
return None, f"Study not found due to {e}"
|
| 46 |
-
|
| 47 |
-
return active_study_obj, ""
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def configure_interviewer(this_interviewer: Interviewer,
|
| 51 |
-
this_study_obj: Study,
|
| 52 |
-
history: List[List[str]]):
|
| 53 |
-
"""
|
| 54 |
-
takes an existing interviewer object and initializes it with details from the provided study object
|
| 55 |
-
|
| 56 |
-
:param history:
|
| 57 |
-
:param this_interviewer:
|
| 58 |
-
:param this_study_obj:
|
| 59 |
-
:return: interviewer object with the right initializations
|
| 60 |
-
"""
|
| 61 |
-
this_status = "Successfully configured interviewer object"
|
| 62 |
-
|
| 63 |
-
if this_study_obj is None or type(this_study_obj) != Study:
|
| 64 |
-
this_status = "The research study you are participating in does not" \
|
| 65 |
-
" exist or is not ready, please contact the researcher"
|
| 66 |
-
|
| 67 |
-
# if this study has
|
| 68 |
-
|
| 69 |
-
return this_interviewer, this_status
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def lookup_userid_by_email(email):
|
| 73 |
-
"""
|
| 74 |
-
Looks up email address and finds the right user and returns the user_id
|
| 75 |
-
:param email:
|
| 76 |
-
:return:
|
| 77 |
-
"""
|
| 78 |
-
if email is None or not is_valid_email(email):
|
| 79 |
-
return None, "Try with a valid email please!"
|
| 80 |
-
|
| 81 |
-
with DatabaseIO(collection_name=INTERVIEW_USERS_COLLECTION) as db_io:
|
| 82 |
-
user = db_io.collection.find_one({"email": email}, {"_id": 1})
|
| 83 |
-
if user:
|
| 84 |
-
return str(user["_id"]), "Found correct user"
|
| 85 |
-
else:
|
| 86 |
-
return None, "Error finding user"
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def process_transcript_to_chatbot_history(this_transcript):
|
| 90 |
-
messages_in_chatbot_format = []
|
| 91 |
-
for i in range(0, len(this_transcript), 2):
|
| 92 |
-
human_turn = this_transcript[i]['text']
|
| 93 |
-
ai_turn = this_transcript[i + 1]['text']
|
| 94 |
-
messages_in_chatbot_format.append([human_turn, ai_turn])
|
| 95 |
-
return messages_in_chatbot_format
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def set_user_and_study(
|
| 99 |
-
this_study_repository,
|
| 100 |
-
this_study_code,
|
| 101 |
-
this_user_email,
|
| 102 |
-
|
| 103 |
-
):
|
| 104 |
-
this_user_id, user_exists_message = lookup_userid_by_email(this_user_email)
|
| 105 |
-
|
| 106 |
-
interaction_disabled = gr.update( interactive=False)
|
| 107 |
-
interaction_enabled = gr.update( interactive=True)
|
| 108 |
-
|
| 109 |
-
if not this_user_id or not len(this_user_id):
|
| 110 |
-
this_return_message = user_exists_message
|
| 111 |
-
return None, "", this_return_message, *([interaction_disabled] * 5)
|
| 112 |
-
|
| 113 |
-
this_active_study, study_exists_message = set_active_study(this_study_repository, study_id=str(this_study_code))
|
| 114 |
-
if not this_active_study:
|
| 115 |
-
this_return_message = study_exists_message
|
| 116 |
-
return {}, this_user_id, this_return_message, *([interaction_disabled] * 5)
|
| 117 |
-
this_return_message = user_exists_message + " " + study_exists_message
|
| 118 |
-
|
| 119 |
-
return this_active_study, this_user_id, this_return_message, *([interaction_enabled] * 5)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def setup_conversation_context(this_active_study, this_study_repository, this_user_id):
|
| 123 |
-
"""
|
| 124 |
-
Populates the interview object
|
| 125 |
-
:param this_study_repository:
|
| 126 |
-
:param this_active_study:
|
| 127 |
-
:param this_user_id:
|
| 128 |
-
:return:
|
| 129 |
-
"""
|
| 130 |
-
|
| 131 |
-
if not this_active_study or not type(this_active_study) == Study:
|
| 132 |
-
return [], None, "Study code or user id is invalid"
|
| 133 |
-
this_transcript = load_previous_transcript(this_active_study,
|
| 134 |
-
this_user_id,
|
| 135 |
-
this_study_repository)
|
| 136 |
-
this_user_id_str = str(this_user_id)
|
| 137 |
-
this_interviewer = Interviewer(
|
| 138 |
-
research_topic=this_active_study.study_topic,
|
| 139 |
-
research_objectives=this_active_study.study_objective,
|
| 140 |
-
research_questions=this_active_study.research_questions,
|
| 141 |
-
interview_guidelines=this_active_study.interview_guidelines,
|
| 142 |
-
existing_conversation_history=this_transcript
|
| 143 |
-
)
|
| 144 |
-
|
| 145 |
-
if not this_transcript or not len(this_transcript):
|
| 146 |
-
# no previous conversation
|
| 147 |
-
ai_response = this_interviewer.chat_handler.converse('Hello')
|
| 148 |
-
this_constructed_transcript = [{'turn': 'Human', 'text': 'Hello'}, {'turn': 'AI', 'text': ai_response}]
|
| 149 |
-
# transcript did not exist, so must create the user info in database
|
| 150 |
-
|
| 151 |
-
this_view_chat_history = process_transcript_to_chatbot_history(this_constructed_transcript)
|
| 152 |
-
#
|
| 153 |
-
if 'human_interviews' not in this_active_study.interviews or not this_active_study.interviews['human_interviews']:
|
| 154 |
-
this_active_study.interviews['human_interviews'] = {}
|
| 155 |
-
this_previous_interview = this_active_study.interviews['human_interviews'].get(this_user_id)
|
| 156 |
-
# If not found, initialize an empty one
|
| 157 |
-
if not this_previous_interview:
|
| 158 |
-
this_previous_interview = {
|
| 159 |
-
"AI_interviewer": "Sophia (AI Researcher)",
|
| 160 |
-
"Human_Respondent": {
|
| 161 |
-
"participant_id": this_user_id_str
|
| 162 |
-
},
|
| 163 |
-
"transcript": []
|
| 164 |
-
}
|
| 165 |
-
this_active_study.interviews['human_interviews'][this_user_id_str] = this_previous_interview
|
| 166 |
-
this_active_study.interviews['human_interviews'][str(this_user_id)]['transcript'] = this_constructed_transcript
|
| 167 |
-
this_study_repository.update_study(study_id=this_active_study._id, updated_data=this_active_study.to_dict())
|
| 168 |
-
this_message = "First time talking to you ... "
|
| 169 |
-
else:
|
| 170 |
-
this_view_chat_history = process_transcript_to_chatbot_history(this_transcript)
|
| 171 |
-
this_message = "Welcome back, lets start where we left off!"
|
| 172 |
-
|
| 173 |
-
return this_view_chat_history, this_interviewer, this_message
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
def load_previous_transcript(this_study: Study,
|
| 177 |
-
this_user_id: Union[ObjectId | str],
|
| 178 |
-
this_study_repository: StudyRepository):
|
| 179 |
-
"""
|
| 180 |
-
loads existing interview
|
| 181 |
-
:param this_study:
|
| 182 |
-
:param this_user_id:
|
| 183 |
-
:param this_study_repository
|
| 184 |
-
:return:
|
| 185 |
-
"""
|
| 186 |
-
this_user_id_str = str(this_user_id)
|
| 187 |
-
|
| 188 |
-
# Ensure 'human_interviews' exists and is initialized properly
|
| 189 |
-
|
| 190 |
-
try:
|
| 191 |
-
# Try to get the interview of the participant
|
| 192 |
-
human_interviews = this_study.interviews['human_interviews']
|
| 193 |
-
this_previous_interview = human_interviews.get(this_user_id_str, None)
|
| 194 |
-
return this_previous_interview['transcript']
|
| 195 |
-
|
| 196 |
-
except (AttributeError, KeyError, Exception) as ae:
|
| 197 |
-
return []
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
if __name__ == "__main__":
|
| 201 |
-
# unit tests
|
| 202 |
-
study_repository = StudyRepository(collection_name_studies=STUDIES_COLLECTION)
|
| 203 |
-
sample_study_id = "64dc833a12e518bb5d4ea2b4"
|
| 204 |
-
sample_email = "paudyalprajwal@qualz.net"
|
| 205 |
-
interviewer = Interviewer(existing_conversation_history=['a', 'b'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/database_helper.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
from datetime import datetime
|
| 2 |
-
|
| 3 |
-
import pymongo
|
| 4 |
-
from pymongo.errors import PyMongoError
|
| 5 |
-
import os
|
| 6 |
-
import dotenv
|
| 7 |
-
from pymongo.server_api import ServerApi
|
| 8 |
-
|
| 9 |
-
class DatabaseIO:
|
| 10 |
-
def __init__(self, db_name=None, collection_name=None):
|
| 11 |
-
dotenv.load_dotenv()
|
| 12 |
-
|
| 13 |
-
mongo_username = os.environ['MONGO_USERNAME']
|
| 14 |
-
mongo_password = os.environ['MONGO_PASSWORD']
|
| 15 |
-
client_url = os.environ['MONGO_CLIENT_URL_DEV']
|
| 16 |
-
uri = f"mongodb+srv://{mongo_username}:{mongo_password}@{client_url}/?retryWrites=true&w=majority"
|
| 17 |
-
|
| 18 |
-
if not db_name:
|
| 19 |
-
db_name = os.environ['MONGO_DATABASE_NAME']
|
| 20 |
-
if not collection_name:
|
| 21 |
-
collection_name = os.environ['MONGO_COLLECTION']
|
| 22 |
-
|
| 23 |
-
self.client = pymongo.MongoClient(uri, server_api=ServerApi('1'))
|
| 24 |
-
self.db = self.client[db_name]
|
| 25 |
-
self.collection = self.db[collection_name]
|
| 26 |
-
|
| 27 |
-
def insert_document(self, article, collection=None, unique_on='_id', upsert=False):
|
| 28 |
-
|
| 29 |
-
if not collection:
|
| 30 |
-
collection = self.collection
|
| 31 |
-
|
| 32 |
-
article['date_modified'] = datetime.now().utcnow()
|
| 33 |
-
existing_document = collection.find_one({unique_on: article[unique_on]})
|
| 34 |
-
if existing_document:
|
| 35 |
-
# there is something like with the reddit id already
|
| 36 |
-
|
| 37 |
-
if upsert:
|
| 38 |
-
collection.update_one({unique_on: article[unique_on]}, {"$set": article})
|
| 39 |
-
else:
|
| 40 |
-
article['date_created'] = datetime.now().utcnow()
|
| 41 |
-
collection.insert_one(article)
|
| 42 |
-
|
| 43 |
-
def __enter__(self):
|
| 44 |
-
return self
|
| 45 |
-
|
| 46 |
-
def __exit__(self, exc_type, exc_value, traceback):
|
| 47 |
-
try:
|
| 48 |
-
self.client.close()
|
| 49 |
-
except PyMongoError as e:
|
| 50 |
-
print(f"An error occurred while closing the database connection: {e}")
|
| 51 |
-
raise
|
| 52 |
-
|
| 53 |
-
def __del__(self):
|
| 54 |
-
try:
|
| 55 |
-
self.client.close()
|
| 56 |
-
except Exception as e:
|
| 57 |
-
print(e)
|
| 58 |
-
|
| 59 |
-
def read_documents(self, query=None, sort_by=None, sort_order=None):
|
| 60 |
-
if query is None:
|
| 61 |
-
query = {}
|
| 62 |
-
try:
|
| 63 |
-
if sort_by:
|
| 64 |
-
if not sort_order or sort_order not in [1, -1]:
|
| 65 |
-
sort_order= 1
|
| 66 |
-
for article in self.collection.find(query).sort(sort_by, sort_order):
|
| 67 |
-
yield article
|
| 68 |
-
else:
|
| 69 |
-
for article in self.collection.find(query):
|
| 70 |
-
yield article
|
| 71 |
-
except Exception as e:
|
| 72 |
-
print(e)
|
| 73 |
-
|
| 74 |
-
def count_documents(self, query=None):
|
| 75 |
-
if query is None:
|
| 76 |
-
query = {}
|
| 77 |
-
try:
|
| 78 |
-
return self.collection.count_documents(query)
|
| 79 |
-
except Exception as e:
|
| 80 |
-
print(e)
|
| 81 |
-
|
| 82 |
-
def update_documents(self, query, update, upsert=True):
|
| 83 |
-
try:
|
| 84 |
-
self.collection.update_one(query, update, upsert=upsert)
|
| 85 |
-
except Exception as e:
|
| 86 |
-
print(e)
|
| 87 |
-
|
| 88 |
-
def delete_document(self, query):
|
| 89 |
-
try:
|
| 90 |
-
self.collection.delete_one(query)
|
| 91 |
-
except Exception as e:
|
| 92 |
-
print(e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/eleven_voice_cache.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
# Save default voices to avoid querying the API for unathorized users
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
import re
|
| 5 |
-
from typing import Iterator, List, Optional, Union
|
| 6 |
-
|
| 7 |
-
from elevenlabs.api import TTS, Model, Voice, VoiceClone, Voices, VoiceSettings
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
VOICES_CACHE = [
|
| 11 |
-
Voice(
|
| 12 |
-
voice_id="21m00Tcm4TlvDq8ikWAM",
|
| 13 |
-
name="Rachel",
|
| 14 |
-
category="premade",
|
| 15 |
-
settings=VoiceSettings(stability=0.75, similarity_boost=0.75),
|
| 16 |
-
),
|
| 17 |
-
Voice(
|
| 18 |
-
voice_id="AZnzlk1XvdvUeBnXmlld",
|
| 19 |
-
name="Domi",
|
| 20 |
-
category="premade",
|
| 21 |
-
settings=VoiceSettings(stability=0.1, similarity_boost=0.75),
|
| 22 |
-
),
|
| 23 |
-
Voice(
|
| 24 |
-
voice_id="EXAVITQu4vr4xnSDxMaL",
|
| 25 |
-
name="Bella",
|
| 26 |
-
category="premade",
|
| 27 |
-
settings=VoiceSettings(stability=0.245, similarity_boost=0.75),
|
| 28 |
-
),
|
| 29 |
-
Voice(
|
| 30 |
-
voice_id="ErXwobaYiN019PkySvjV",
|
| 31 |
-
name="Antoni",
|
| 32 |
-
category="premade",
|
| 33 |
-
settings=VoiceSettings(stability=0.195, similarity_boost=0.75),
|
| 34 |
-
),
|
| 35 |
-
Voice(
|
| 36 |
-
voice_id="MF3mGyEYCl7XYWbV9V6O",
|
| 37 |
-
name="Elli",
|
| 38 |
-
category="premade",
|
| 39 |
-
settings=VoiceSettings(stability=0.755, similarity_boost=0.75),
|
| 40 |
-
),
|
| 41 |
-
Voice(
|
| 42 |
-
voice_id="TxGEqnHWrfWFTfGW9XjX",
|
| 43 |
-
name="Josh",
|
| 44 |
-
category="premade",
|
| 45 |
-
settings=VoiceSettings(stability=0.15, similarity_boost=0.51),
|
| 46 |
-
),
|
| 47 |
-
Voice(
|
| 48 |
-
voice_id="VR6AewLTigWG4xSOukaG",
|
| 49 |
-
name="Arnold",
|
| 50 |
-
category="premade",
|
| 51 |
-
settings=VoiceSettings(stability=0.15, similarity_boost=0.75),
|
| 52 |
-
),
|
| 53 |
-
Voice(
|
| 54 |
-
voice_id="pNInz6obpgDQGcFmaJgB",
|
| 55 |
-
name="Adam",
|
| 56 |
-
category="premade",
|
| 57 |
-
settings=VoiceSettings(stability=0.2, similarity_boost=0.75),
|
| 58 |
-
),
|
| 59 |
-
Voice(
|
| 60 |
-
voice_id="yoZ06aMxZJJ28mfd3POQ",
|
| 61 |
-
name="Sam",
|
| 62 |
-
category="premade",
|
| 63 |
-
settings=VoiceSettings(stability=0.25, similarity_boost=0.75),
|
| 64 |
-
),
|
| 65 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/llm_helper.py
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
from langchain.schema import SystemMessage, HumanMessage
|
| 2 |
-
from langchain.chat_models import ChatOpenAI
|
| 3 |
-
import ast
|
| 4 |
-
import warnings
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
import os, json
|
| 7 |
-
from ra_utils.synthetic_panel_datamodel import parse_synthetic_panel_output
|
| 8 |
-
from langchain.memory import ConversationBufferMemory
|
| 9 |
-
from langchain import LLMChain
|
| 10 |
-
import dotenv
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class LLMChatHandler:
|
| 14 |
-
def __init__(self,
|
| 15 |
-
llm_model_name: str = None,
|
| 16 |
-
llm_temperature: float = None,
|
| 17 |
-
expertise_message: str = ""):
|
| 18 |
-
|
| 19 |
-
dotenv.load_dotenv()
|
| 20 |
-
if not llm_model_name:
|
| 21 |
-
llm_model_name = os.environ['OPENAI_MODEL']
|
| 22 |
-
if not llm_temperature:
|
| 23 |
-
llm_temperature=os.environ['OPENAI_MODEL_TEMPERATURE']
|
| 24 |
-
|
| 25 |
-
self.llm = ChatOpenAI(temperature=llm_temperature, model_name=llm_model_name)
|
| 26 |
-
|
| 27 |
-
project_dir = Path(os.environ['PROJECT_DIR'])
|
| 28 |
-
assert project_dir.exists()
|
| 29 |
-
with open(project_dir / 'study_prompts.json') as sp:
|
| 30 |
-
self.definition_objective_maps = json.load(sp)
|
| 31 |
-
|
| 32 |
-
if not len(expertise_message):
|
| 33 |
-
expertise_message = f"Assistant is a world renowned expert in qualitative research." \
|
| 34 |
-
f" Assistant is well known in your field worldwide and has published several books " \
|
| 35 |
-
f"and white-papers. Assistant is also an expert qualitative research analyst " \
|
| 36 |
-
f"who is thorough and methodical."
|
| 37 |
-
self.expertise_message = expertise_message
|
| 38 |
-
|
| 39 |
-
def predict(self, messages):
|
| 40 |
-
return self.llm.predict_messages(messages)
|
| 41 |
-
|
| 42 |
-
def guess_research_questions(self,
|
| 43 |
-
research_name: str,
|
| 44 |
-
research_topic: str,
|
| 45 |
-
research_objective: str,
|
| 46 |
-
research_framework: str,
|
| 47 |
-
research_questions_draft: str):
|
| 48 |
-
guess_research_questions_message = f"""
|
| 49 |
-
Based on the study named {research_name} focusing on the topic {research_topic}, and objectives for the
|
| 50 |
-
research as {research_objective} and a theoretical framework as {research_framework}, please help
|
| 51 |
-
me craft some research questions. Research questions are overarching broad questions that help me understand
|
| 52 |
-
about the research objective. You shouldn't suggest more than 3 questions.
|
| 53 |
-
Also take note of the specific instructions or draft enclosed within
|
| 54 |
-
triple backticks.
|
| 55 |
-
```INSTRUCTIONS: {research_questions_draft}```
|
| 56 |
-
""" + """ Your output should be formatted as a dictionary with two keys such as:
|
| 57 |
-
{
|
| 58 |
-
"research_questions": A single string with all the research questions you recommend
|
| 59 |
-
separated by a semi-colon.
|
| 60 |
-
It is critical to include only questions here and not any explanation or discussion
|
| 61 |
-
"reasoning": A brief explanation on the reasoning for choosing the questions.
|
| 62 |
-
Here you can include any additional things you
|
| 63 |
-
want to add including commentary of any kind
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
If there are any concerns or difficulties in crafting the questions,
|
| 67 |
-
please mention them under "reasoning" """
|
| 68 |
-
messages = [SystemMessage(content=self.expertise_message),
|
| 69 |
-
HumanMessage(content=guess_research_questions_message)]
|
| 70 |
-
research_questions_output = self.predict(messages)
|
| 71 |
-
|
| 72 |
-
try:
|
| 73 |
-
|
| 74 |
-
research_questions_dict = ast.literal_eval(research_questions_output.content)
|
| 75 |
-
if not isinstance(research_questions_dict, dict):
|
| 76 |
-
raise ValueError("Parsing output as dictionary did not work")
|
| 77 |
-
|
| 78 |
-
return research_questions_dict.get("research_questions", ""), \
|
| 79 |
-
research_questions_dict.get("reasoning", "Please try again. Are the study details descriptive enough?")
|
| 80 |
-
except (SyntaxError, ValueError) as e:
|
| 81 |
-
print(f"Error parsing output: {e}")
|
| 82 |
-
return "", "Please try again. Are the study details descriptive enough?"
|
| 83 |
-
|
| 84 |
-
def create_synthetic_panel(self,
|
| 85 |
-
research_topic: str,
|
| 86 |
-
research_objective: str,
|
| 87 |
-
research_framework: str,
|
| 88 |
-
instructions: str = "",
|
| 89 |
-
num_panelists: int = 5):
|
| 90 |
-
|
| 91 |
-
panel_creation_prompt_template = self.definition_objective_maps.get('sample_selection').get('prompt_template')
|
| 92 |
-
|
| 93 |
-
if num_panelists > 15:
|
| 94 |
-
warnings.warn("Only returning 15 panelists for now")
|
| 95 |
-
num_panelists = 15
|
| 96 |
-
|
| 97 |
-
m_custom_message = f"The profiles should be created for a qualitative research study with these details. " \
|
| 98 |
-
f"Study Topic: {research_topic}" \
|
| 99 |
-
f"Study Objective: {research_objective}" \
|
| 100 |
-
f"Study theoretical framework: {research_framework}" \
|
| 101 |
-
f"Please create {num_panelists} of profiles and consider these " \
|
| 102 |
-
f"instructions while creating the included within triple backticks: " \
|
| 103 |
-
f"```INSTRUCTIONS FOR PROFILE CREATION: {instructions}```."
|
| 104 |
-
|
| 105 |
-
# existing_synthetic_panelist_message = ""
|
| 106 |
-
# existing_panelists = self.synthetic_panelists
|
| 107 |
-
#
|
| 108 |
-
# if existing_panelists and len(existing_panelists) > 0:
|
| 109 |
-
# existing_synthetic_panelist_message = " and Here is a list of existing panelists " \
|
| 110 |
-
# "that should be modified according to the criteria I will state"
|
| 111 |
-
|
| 112 |
-
# panel_creation_message = panel_creation_prompt_template + existing_synthetic_panelist_message + m_custom_message
|
| 113 |
-
|
| 114 |
-
panel_creation_message = panel_creation_prompt_template + m_custom_message
|
| 115 |
-
|
| 116 |
-
messages = [SystemMessage(content=self.expertise_message),
|
| 117 |
-
HumanMessage(content=panel_creation_message)]
|
| 118 |
-
synthetic_panel_output = self.predict(messages).content
|
| 119 |
-
|
| 120 |
-
user_panel, rationale, discussion = parse_synthetic_panel_output(synthetic_panel_output)
|
| 121 |
-
|
| 122 |
-
return user_panel, rationale, discussion
|
| 123 |
-
|
| 124 |
-
def guess_interview_guidelines(self,
|
| 125 |
-
research_topic: str,
|
| 126 |
-
research_objective: str,
|
| 127 |
-
research_framework: str,
|
| 128 |
-
research_questions: str,
|
| 129 |
-
interview_guidelines_draft_or_instructions: str, ):
|
| 130 |
-
# Chat logic here
|
| 131 |
-
guess_study_objective_message = f"""
|
| 132 |
-
For a qualitative study focusing on the topic {research_topic}, please help
|
| 133 |
-
come up with some interview guidelines and script.
|
| 134 |
-
The objective of the research is to {research_objective}
|
| 135 |
-
The theoretical framework for the study is {research_framework}
|
| 136 |
-
The main research questions we have identified are: {research_questions}.
|
| 137 |
-
I will also include some draft question or additional instructions below within
|
| 138 |
-
triple backticks. Pay attention to these and use them accordingly.
|
| 139 |
-
Your output should be a dictionary with two keys:
|
| 140 |
-
|
| 141 |
-
1. "interview_script": Provide a script to guide the interview process.
|
| 142 |
-
The purpose is for for a researcher to use this to guide an interview session. The aim of the script is to
|
| 143 |
-
ensure appropriate questions as asked to fulfill the study objective and research questions. The interview
|
| 144 |
-
script should be a plain string and NOT a dictionary or any other data structure. If any sub-sections
|
| 145 |
-
are needed, use call caps for the subsection name.
|
| 146 |
-
|
| 147 |
-
2. "reasoning": This should be an overview of the rationale behind the questions, and be no longer than two
|
| 148 |
-
sentences.
|
| 149 |
-
|
| 150 |
-
If there are any concerns or difficulties in crafting the interview script, please mention them under "reasoning"
|
| 151 |
-
|
| 152 |
-
```INSTRUCTIONS: {interview_guidelines_draft_or_instructions}```"""
|
| 153 |
-
|
| 154 |
-
messages = [SystemMessage(content=self.expertise_message),
|
| 155 |
-
HumanMessage(content=guess_study_objective_message)]
|
| 156 |
-
study_objective_output = self.predict(messages)
|
| 157 |
-
try:
|
| 158 |
-
|
| 159 |
-
study_objective_dict = ast.literal_eval(study_objective_output.content)
|
| 160 |
-
if not isinstance(study_objective_dict, dict):
|
| 161 |
-
raise ValueError("Parsing output as dictionary did not work")
|
| 162 |
-
|
| 163 |
-
return study_objective_dict.get("interview_script", ""), \
|
| 164 |
-
study_objective_dict.get("reasoning", "Please try again. Are the study details descriptive enough?")
|
| 165 |
-
except (SyntaxError, ValueError) as e:
|
| 166 |
-
print(f"Error parsing output: {e}")
|
| 167 |
-
return "", "Please try again. Are the study details descriptive enough?"
|
| 168 |
-
|
| 169 |
-
def guess_study_objective(self, research_name: str,
|
| 170 |
-
research_topic: str,
|
| 171 |
-
study_objective_draft_or_instructions: str):
|
| 172 |
-
# Chat logic here
|
| 173 |
-
guess_study_objective_message = f"""
|
| 174 |
-
Based on the study named {research_name} focusing on the topic {research_topic}, please help
|
| 175 |
-
me improve my study objective. Take note of the specific instructions and draft enclosed within
|
| 176 |
-
triple backticks. Your output should be a dictionary with two keys:
|
| 177 |
-
|
| 178 |
-
1. "study_objective": Provide a concise research objective statement that is limited to one sentence.
|
| 179 |
-
|
| 180 |
-
2. "reasoning": This should detail the rationale behind the chosen objective, and be no longer than two
|
| 181 |
-
sentences.
|
| 182 |
-
|
| 183 |
-
If there are any concerns or difficulties in crafting the objective, please mention them under "reasoning"
|
| 184 |
-
|
| 185 |
-
```INSTRUCTIONS: {study_objective_draft_or_instructions}```"""
|
| 186 |
-
|
| 187 |
-
messages = [SystemMessage(content=self.expertise_message),
|
| 188 |
-
HumanMessage(content=guess_study_objective_message)]
|
| 189 |
-
study_objective_output = self.predict(messages)
|
| 190 |
-
try:
|
| 191 |
-
|
| 192 |
-
study_objective_dict = ast.literal_eval(study_objective_output.content)
|
| 193 |
-
if not isinstance(study_objective_dict, dict):
|
| 194 |
-
raise ValueError("Parsing output as dictionary did not work")
|
| 195 |
-
|
| 196 |
-
return study_objective_dict.get("study_objective", ""), \
|
| 197 |
-
study_objective_dict.get("reasoning", "Please try again. Are the study details descriptive enough?")
|
| 198 |
-
except (SyntaxError, ValueError) as e:
|
| 199 |
-
print(f"Error parsing output: {e}")
|
| 200 |
-
return "", "Please try again. Are the study details descriptive enough?"
|
| 201 |
-
|
| 202 |
-
def guess_theoretical_framework(self, research_name: str,
|
| 203 |
-
research_topic: str,
|
| 204 |
-
research_objective: str):
|
| 205 |
-
# Chat logic here
|
| 206 |
-
guess_theoretical_framework_message = f"""
|
| 207 |
-
Based on the given research details:
|
| 208 |
-
- Research Name: {research_name}
|
| 209 |
-
- Research Topic: {research_topic}
|
| 210 |
-
- Study Objective: {research_objective}
|
| 211 |
-
|
| 212 |
-
Please recommend a suitable theoretical framework for the study. The framework should be
|
| 213 |
-
one among the following: Ethnographic, Narrative,
|
| 214 |
-
Grounded Theory, Case Study, Phenomenological, and Exploratory. """ + """You output should
|
| 215 |
-
be a python dictionary with two keys and nothing else. Here is the format:
|
| 216 |
-
{
|
| 217 |
-
"theoretical_framework": Recommended framework from the list above.
|
| 218 |
-
It is critical to include only the framework and not any explanation
|
| 219 |
-
"reasoning": A brief explanation on why you think the chosen
|
| 220 |
-
framework is the most suitable for the study. Here you can include any additional things you
|
| 221 |
-
want to add including commentary of any kind
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
If there are any concerns or difficulties in crafting the objective,
|
| 225 |
-
please mention them under "reasoning"
|
| 226 |
-
"""
|
| 227 |
-
|
| 228 |
-
messages = [SystemMessage(content=self.expertise_message),
|
| 229 |
-
HumanMessage(content=guess_theoretical_framework_message)]
|
| 230 |
-
study_guess_theoretical_framework_output = self.predict(messages)
|
| 231 |
-
try:
|
| 232 |
-
|
| 233 |
-
study_guess_theoretical_framework_dict = ast.literal_eval(study_guess_theoretical_framework_output.content)
|
| 234 |
-
if not isinstance(study_guess_theoretical_framework_dict, dict):
|
| 235 |
-
raise ValueError("Parsing output as dictionary did not work")
|
| 236 |
-
|
| 237 |
-
return study_guess_theoretical_framework_dict.get("theoretical_framework", ""), \
|
| 238 |
-
study_guess_theoretical_framework_dict.get("reasoning",
|
| 239 |
-
"Please try again. "
|
| 240 |
-
"Are the study details descriptive enough?")
|
| 241 |
-
except (SyntaxError, ValueError) as e:
|
| 242 |
-
print(f"Error parsing output: {e}")
|
| 243 |
-
return "", "Please try again. Are the study details descriptive enough?"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/study.py
DELETED
|
@@ -1,148 +0,0 @@
|
|
| 1 |
-
from typing import List, Dict, Optional, Union
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
import os
|
| 4 |
-
import json
|
| 5 |
-
import warnings
|
| 6 |
-
from langchain.chat_models import ChatOpenAI
|
| 7 |
-
from bson import ObjectId
|
| 8 |
-
from utils.llm_helper import LLMChatHandler
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class InterviewSyntheticPanel:
|
| 12 |
-
def __init__(self, api_config: dict):
|
| 13 |
-
# Initialize API
|
| 14 |
-
pass
|
| 15 |
-
|
| 16 |
-
def create_panel(self, study_details: dict):
|
| 17 |
-
# Call the API with study details and create synthetic panel
|
| 18 |
-
pass
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
class Study:
|
| 22 |
-
def __init__(self,
|
| 23 |
-
_id: str,
|
| 24 |
-
study_descriptive_name: str,
|
| 25 |
-
study_objective: str,
|
| 26 |
-
study_framework: str,
|
| 27 |
-
study_topic: str,
|
| 28 |
-
research_questions: str,
|
| 29 |
-
interview_guidelines: Optional[str] = None,
|
| 30 |
-
interviews: Optional[Dict['str', List[Dict]]] = None,
|
| 31 |
-
overall_codes: Optional[Dict[str, List[str]]] = None,
|
| 32 |
-
synthetic_panel_pending_interviews: Optional [List[Dict]] = None,
|
| 33 |
-
**kwargs,
|
| 34 |
-
):
|
| 35 |
-
self.study_descriptive_name = study_descriptive_name
|
| 36 |
-
self.study_topic = study_topic
|
| 37 |
-
self._id = _id
|
| 38 |
-
self.study_objective = study_objective
|
| 39 |
-
self.study_framework = study_framework
|
| 40 |
-
self.interview_guidelines = interview_guidelines if interview_guidelines else ""
|
| 41 |
-
self.research_questions = research_questions
|
| 42 |
-
self.interviews = interviews if interviews else {}
|
| 43 |
-
self.overall_codes = overall_codes if overall_codes else {}
|
| 44 |
-
|
| 45 |
-
self.additional_fields = kwargs
|
| 46 |
-
self.chat_handler = LLMChatHandler()
|
| 47 |
-
|
| 48 |
-
self.synthetic_panel_pending_interviews = synthetic_panel_pending_interviews if\
|
| 49 |
-
synthetic_panel_pending_interviews else []
|
| 50 |
-
# Check if synthetic_interviews exists and is not empty
|
| 51 |
-
# if 'synthetic_interviews' in self.interviews and len(self.interviews.get('synthetic_interviews', "")):
|
| 52 |
-
# # Extract AI_Respondent from each item in synthetic_interviews and append to self.synthetic_panelists
|
| 53 |
-
# self.synthetic_panelists = [item['AI_Respondent'] for item in interviews.get('synthetic_interviews') if
|
| 54 |
-
# 'AI_Respondent' in item]
|
| 55 |
-
|
| 56 |
-
project_dir = Path(os.environ['PROJECT_DIR'])
|
| 57 |
-
assert project_dir.exists()
|
| 58 |
-
with open(project_dir / 'study_prompts.json') as sp:
|
| 59 |
-
self.definition_objective_maps = json.load(sp)
|
| 60 |
-
|
| 61 |
-
def add_interview(self, interview: Dict):
|
| 62 |
-
"""
|
| 63 |
-
Add an interview to the study.
|
| 64 |
-
|
| 65 |
-
:param interview: The interview data to add.
|
| 66 |
-
"""
|
| 67 |
-
self.interviews.append(interview)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def save_synthetic_panelists(self):
|
| 71 |
-
"""
|
| 72 |
-
attemp
|
| 73 |
-
:return:
|
| 74 |
-
"""
|
| 75 |
-
def remove_interview(self, interview_index: int):
|
| 76 |
-
"""
|
| 77 |
-
Remove an interview from the study based on its index.
|
| 78 |
-
|
| 79 |
-
:param interview_index: Index of the interview to remove.
|
| 80 |
-
"""
|
| 81 |
-
if 0 <= interview_index < len(self.interviews):
|
| 82 |
-
del self.interviews[interview_index]
|
| 83 |
-
|
| 84 |
-
def update_overall_codes(self, code: str, quotes: List[str]):
|
| 85 |
-
"""
|
| 86 |
-
Update or add a code and its associated quotes to the overall_codes.
|
| 87 |
-
|
| 88 |
-
:param code: The code to update or add.
|
| 89 |
-
:param quotes: The associated quotes.
|
| 90 |
-
"""
|
| 91 |
-
if code in self.overall_codes:
|
| 92 |
-
self.overall_codes[code].extend(quotes)
|
| 93 |
-
else:
|
| 94 |
-
self.overall_codes[code] = quotes
|
| 95 |
-
|
| 96 |
-
def to_dict(self) -> dict:
|
| 97 |
-
"""
|
| 98 |
-
Convert the object to a dictionary representation.
|
| 99 |
-
|
| 100 |
-
Returns:
|
| 101 |
-
dict: A dictionary representation of the object.
|
| 102 |
-
"""
|
| 103 |
-
return {
|
| 104 |
-
'_id': self._id,
|
| 105 |
-
'study_descriptive_name': self.study_descriptive_name,
|
| 106 |
-
'study_topic': self.study_topic,
|
| 107 |
-
'study_objective': self.study_objective,
|
| 108 |
-
'study_framework': self.study_framework,
|
| 109 |
-
'interview_guidelines': self.interview_guidelines,
|
| 110 |
-
'research_questions': self.research_questions,
|
| 111 |
-
'interviews': self.interviews,
|
| 112 |
-
'overall_codes': self.overall_codes,
|
| 113 |
-
'synthetic_panel_pending_interviews': self.synthetic_panel_pending_interviews,
|
| 114 |
-
**self.additional_fields
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
@staticmethod
|
| 119 |
-
def from_dict(data: Dict):
|
| 120 |
-
"""
|
| 121 |
-
Create a Study object from a dictionary.
|
| 122 |
-
|
| 123 |
-
:param data: The dictionary containing study data.
|
| 124 |
-
:return: A Study object.
|
| 125 |
-
"""
|
| 126 |
-
return Study(
|
| 127 |
-
study_id=data["id"],
|
| 128 |
-
objective=data["objective"],
|
| 129 |
-
study_framework=data["study_framework"],
|
| 130 |
-
study_descriptive_name=data["study_descriptive_name"],
|
| 131 |
-
study_topic=data["study_topic"],
|
| 132 |
-
interviews=data.get("interviews", []),
|
| 133 |
-
overall_codes=data.get("overall_codes", {})
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
# Usage
|
| 138 |
-
# study_data = {
|
| 139 |
-
# "id": "12345",
|
| 140 |
-
# "objective": "Test Objective",
|
| 141 |
-
# "study_framework": "Framework1",
|
| 142 |
-
# "study_descriptive_name": "Study1",
|
| 143 |
-
# "study_topic": "Topic1",
|
| 144 |
-
# "interviews": [],
|
| 145 |
-
# "overall_codes": {}
|
| 146 |
-
# }
|
| 147 |
-
# study = Study.from_dict(study_data)
|
| 148 |
-
# study_dict = study.to_dict()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/study_repository.py
DELETED
|
@@ -1,200 +0,0 @@
|
|
| 1 |
-
import dotenv
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
-
from typing import Optional, List, Tuple
|
| 6 |
-
from typing import Dict, Any
|
| 7 |
-
from utils.study import Study
|
| 8 |
-
from utils.database_helper import DatabaseIO
|
| 9 |
-
from bson import ObjectId
|
| 10 |
-
from typing import Union
|
| 11 |
-
|
| 12 |
-
from utils.llm_helper import LLMChatHandler
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class StudyRepository:
|
| 16 |
-
def __init__(self,
|
| 17 |
-
db_name=None,
|
| 18 |
-
collection_name_studies=None,
|
| 19 |
-
collection_name_users=None
|
| 20 |
-
):
|
| 21 |
-
"""
|
| 22 |
-
Initialize the StudyRepository with a database connection or client.
|
| 23 |
-
|
| 24 |
-
:param db: A mock database connection or client.
|
| 25 |
-
"""
|
| 26 |
-
|
| 27 |
-
dotenv.load_dotenv()
|
| 28 |
-
|
| 29 |
-
if not db_name:
|
| 30 |
-
db_name = os.environ['MONGO_DATABASE_NAME']
|
| 31 |
-
if not collection_name_studies:
|
| 32 |
-
collection_name_studies = os.environ['MONGO_COLLECTION_STUDIES']
|
| 33 |
-
if not collection_name_users:
|
| 34 |
-
collection_name_users = os.environ['MONGO_COLLECTION_USERS']
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
self.db_name = db_name
|
| 39 |
-
self.collection_name_studies = collection_name_studies
|
| 40 |
-
self.collection_name_users = collection_name_users
|
| 41 |
-
self.chat_handler = LLMChatHandler()
|
| 42 |
-
# self.db = self.client[db_name]
|
| 43 |
-
# self.collection = self.db.get_collection(self.collection_name) # Assuming 'study_v03' is the collection name
|
| 44 |
-
|
| 45 |
-
def create_study(self, study_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 46 |
-
# Validate the study data using the Study's constructor
|
| 47 |
-
study_data_with_default_id = study_data.copy()
|
| 48 |
-
study_data_with_default_id["_id"] = None
|
| 49 |
-
|
| 50 |
-
try:
|
| 51 |
-
study_instance = Study(**study_data_with_default_id)
|
| 52 |
-
except TypeError as e:
|
| 53 |
-
# Here, we're assuming the TypeError arises from a mismatch in the fields.
|
| 54 |
-
# This might not cover all validation cases, but it's a start.
|
| 55 |
-
raise ValueError(f"Validation error: {str(e)}")
|
| 56 |
-
except Exception as e:
|
| 57 |
-
raise Exception(f"Other exception: {str(e)}")
|
| 58 |
-
|
| 59 |
-
# If validation passes, insert the study into the database
|
| 60 |
-
|
| 61 |
-
with DatabaseIO(self.db_name, self.collection_name_studies) as db_io:
|
| 62 |
-
study_id = db_io.collection.insert_one(study_data).inserted_id
|
| 63 |
-
return study_id
|
| 64 |
-
|
| 65 |
-
def append_to_transcript(self, study_id, user_id, human_text, ai_response):
|
| 66 |
-
"""
|
| 67 |
-
Append the recent conversation to the transcript in the database.
|
| 68 |
-
|
| 69 |
-
:param db_url: MongoDB database URL.
|
| 70 |
-
:param db_name: Name of the MongoDB database.
|
| 71 |
-
:param collection_name: Name of the collection storing the transcripts.
|
| 72 |
-
:param participant_id: ID of the participant for whom the transcript is being updated.
|
| 73 |
-
:param human_text: The text/message from the human.
|
| 74 |
-
:param ai_response: The response from the AI.
|
| 75 |
-
"""
|
| 76 |
-
|
| 77 |
-
# Initialize the MongoDB client and select the database and collection
|
| 78 |
-
user_id = str(user_id)
|
| 79 |
-
# Define the conversation entries to be appended
|
| 80 |
-
new_entries = [
|
| 81 |
-
{
|
| 82 |
-
"turn": "Human",
|
| 83 |
-
"text": human_text,
|
| 84 |
-
},
|
| 85 |
-
{
|
| 86 |
-
"turn": "AI",
|
| 87 |
-
"text": ai_response,
|
| 88 |
-
}
|
| 89 |
-
]
|
| 90 |
-
|
| 91 |
-
# Append new entries to the transcript
|
| 92 |
-
with DatabaseIO(collection_name=self.collection_name_studies) as db_io:
|
| 93 |
-
query = {"_id": ObjectId(study_id)}
|
| 94 |
-
|
| 95 |
-
# Append new entries to the transcript using a dynamic field update
|
| 96 |
-
update_field = f"interviews.human_interviews.{user_id}.transcript"
|
| 97 |
-
update_action = {"$push": {update_field: {"$each": new_entries}}}
|
| 98 |
-
|
| 99 |
-
# Execute the update
|
| 100 |
-
db_io.collection.update_one(query, update_action)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
def get_studies(self,
|
| 104 |
-
study_id: Optional[str] = None,
|
| 105 |
-
user_id: Optional[str] = None,
|
| 106 |
-
to_filter_by_user_id: bool = True) -> Union[
|
| 107 |
-
List[Dict], Dict, Tuple[List[Dict], str], Tuple[int, str]]:
|
| 108 |
-
"""Retrieve studies from the database.
|
| 109 |
-
|
| 110 |
-
:param study_id: (Optional) If provided, retrieve only the study with this ID.
|
| 111 |
-
:param user_id: (Optional) If provided, filter results to studies the user is authorized to view.
|
| 112 |
-
:param to_filter_by_user_id: (Optional) If False, ignores the user_id and
|
| 113 |
-
returns all studies (todo remove in prod)
|
| 114 |
-
:return: The studies or a specific study data, or a tuple with a message if an error occurs.
|
| 115 |
-
"""
|
| 116 |
-
|
| 117 |
-
if study_id:
|
| 118 |
-
study_id = ObjectId(study_id)
|
| 119 |
-
with DatabaseIO(db_name=self.db_name, collection_name=self.collection_name_studies) as db_io:
|
| 120 |
-
study = db_io.collection.find_one({"_id": study_id})
|
| 121 |
-
if not study:
|
| 122 |
-
return {}
|
| 123 |
-
return study
|
| 124 |
-
|
| 125 |
-
# If user_id is provided and filter is desired, return studies the user is authorized to view.
|
| 126 |
-
if user_id and to_filter_by_user_id:
|
| 127 |
-
user_id = ObjectId(user_id)
|
| 128 |
-
with DatabaseIO(db_name=self.db_name, collection_name=self.collection_name_users) as db_io:
|
| 129 |
-
this_user = db_io.collection.find_one({'_id': user_id})
|
| 130 |
-
if not this_user:
|
| 131 |
-
return [], "User not found, please try authentication again."
|
| 132 |
-
|
| 133 |
-
user_study_ids = this_user.get('study_id', [])
|
| 134 |
-
user_study_ids = [ObjectId(uid) for uid in user_study_ids]
|
| 135 |
-
with DatabaseIO(db_name=self.db_name, collection_name=self.collection_name_studies) as db_io:
|
| 136 |
-
matching_studies = list(db_io.collection.find({'_id': {'$in': user_study_ids}}))
|
| 137 |
-
return matching_studies
|
| 138 |
-
|
| 139 |
-
# If neither is provided, return all studies.
|
| 140 |
-
with DatabaseIO(db_name=self.db_name, collection_name=self.collection_name_studies) as db_io:
|
| 141 |
-
return list(db_io.collection.find({}))
|
| 142 |
-
|
| 143 |
-
def update_study(self, study_id: str, updated_data: Dict):
|
| 144 |
-
"""
|
| 145 |
-
Update an existing study by its ID.
|
| 146 |
-
|
| 147 |
-
:param study_id: The ID of the study to update.
|
| 148 |
-
:param updated_data: The new data for the study.
|
| 149 |
-
"""
|
| 150 |
-
|
| 151 |
-
study_id = ObjectId(study_id)
|
| 152 |
-
|
| 153 |
-
# validate all data is of the right type
|
| 154 |
-
updated_data['_id'] = study_id
|
| 155 |
-
try:
|
| 156 |
-
study_instance = Study(**updated_data)
|
| 157 |
-
except TypeError as e:
|
| 158 |
-
# Here, we're assuming the TypeError arises from a mismatch in the fields.
|
| 159 |
-
# This might not cover all validation cases, but it's a start.
|
| 160 |
-
raise ValueError(f"Validation error: {str(e)}")
|
| 161 |
-
except Exception as e:
|
| 162 |
-
raise Exception(f"Other exception: {str(e)}")
|
| 163 |
-
|
| 164 |
-
# Fetch the current study data
|
| 165 |
-
with DatabaseIO(self.db_name, self.collection_name_studies) as db_io:
|
| 166 |
-
current_data = db_io.collection.find_one({"_id": study_id})
|
| 167 |
-
|
| 168 |
-
if not current_data:
|
| 169 |
-
raise ValueError(f"No study found with id: {study_id}")
|
| 170 |
-
|
| 171 |
-
# Construct a dictionary with only the changed values
|
| 172 |
-
changes_to_update = {}
|
| 173 |
-
for key, value in updated_data.items():
|
| 174 |
-
if current_data.get(key, "") != value:
|
| 175 |
-
changes_to_update[key] = value
|
| 176 |
-
|
| 177 |
-
# If there are changes, update the study in the database
|
| 178 |
-
if changes_to_update:
|
| 179 |
-
with DatabaseIO(self.db_name, self.collection_name_studies) as db_io:
|
| 180 |
-
result = db_io.collection.update_one({"_id": study_id}, {"$set": changes_to_update})
|
| 181 |
-
|
| 182 |
-
if result.acknowledged and result.matched_count == 1 and result.modified_count == 1:
|
| 183 |
-
return_msg = f"Succesfully updated study with id {study_id}"
|
| 184 |
-
else:
|
| 185 |
-
return_msg = f"Couldn't confirm updates, please try again or refresh page to see latest"
|
| 186 |
-
else:
|
| 187 |
-
return_msg = f"No changes found, study info is up to date"
|
| 188 |
-
|
| 189 |
-
return return_msg
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def delete_study(self, study_id: str):
|
| 193 |
-
"""
|
| 194 |
-
Delete a study by its ID.
|
| 195 |
-
|
| 196 |
-
:param study_id: The ID of the study to delete.
|
| 197 |
-
"""
|
| 198 |
-
study_id = ObjectId(study_id)
|
| 199 |
-
with DatabaseIO(self.db_name, self.collection_name_studies) as db_io:
|
| 200 |
-
db_io.collection.delete_one({"_id": study_id})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|