From bdbf1d1f00fb66610d56aa9e5efacb8b5c83f9a2 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Mon, 18 Aug 2025 14:32:51 -0400 Subject: [PATCH 01/22] Add assistant urls and views --- server/api/views/assistant/urls.py | 5 + server/api/views/assistant/views.py | 217 ++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 server/api/views/assistant/urls.py create mode 100644 server/api/views/assistant/views.py diff --git a/server/api/views/assistant/urls.py b/server/api/views/assistant/urls.py new file mode 100644 index 00000000..4c68f952 --- /dev/null +++ b/server/api/views/assistant/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +from .views import Assistant + +urlpatterns = [path("v1/api/assistant", Assistant.as_view(), name="assistant")] diff --git a/server/api/views/assistant/views.py b/server/api/views/assistant/views.py new file mode 100644 index 00000000..cb67a480 --- /dev/null +++ b/server/api/views/assistant/views.py @@ -0,0 +1,217 @@ +import os +import json +import logging +from typing import Callable + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt + +from openai import OpenAI + +from ...services.embedding_services import get_closest_embeddings +from ...services.conversions_services import convert_uuids + +# Configure logging +logger = logging.getLogger(__name__) + + +# Open AI Cookbook: Handling Function Calls with Reasoning Models +# https://cookbook.openai.com/examples/reasoning_function_calls +def invoke_functions_from_response( + response, tool_mapping: dict[str, Callable] +) -> list[dict]: + """Extract all function calls from the response, look up the corresponding tool function(s) and execute them. + (This would be a good place to handle asynchroneous tool calls, or ones that take a while to execute.) + This returns a list of messages to be added to the conversation history. + + Parameters + ---------- + response : OpenAI Response + The response object from OpenAI containing output items that may include function calls + tool_mapping : dict[str, Callable] + A dictionary mapping function names (as strings) to their corresponding Python functions. + Keys should match the function names defined in the tools schema. + + Returns + ------- + list[dict] + List of function call output messages formatted for the OpenAI conversation. + Each message contains: + - type: "function_call_output" + - call_id: The unique identifier for the function call + - output: The result returned by the executed function (string or error message) + """ + intermediate_messages = [] + for response_item in response.output: + if response_item.type == "function_call": + target_tool = tool_mapping.get(response_item.name) + if target_tool: + try: + arguments = json.loads(response_item.arguments) + logger.info( + f"Invoking tool: {response_item.name} with arguments: {arguments}" + ) + tool_output = target_tool(**arguments) + logger.debug(f"Tool {response_item.name} completed successfully") + except Exception as e: + msg = f"Error executing function call: {response_item.name}: {e}" + tool_output = msg + logger.error(msg, exc_info=True) + else: + msg = f"ERROR - No tool registered for function call: {response_item.name}" + tool_output = msg + logger.error(msg) + intermediate_messages.append( + { + "type": "function_call_output", + "call_id": response_item.call_id, + "output": tool_output, + } + ) + elif response_item.type == "reasoning": + logger.debug("Reasoning step") + return intermediate_messages + + +@method_decorator(csrf_exempt, name="dispatch") +class Assistant(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + + client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) + + TOOL_DESCRIPTION = """ + Search through the user's uploaded documents using semantic similarity matching. + This function finds the most relevant document chunks based on the input query and + returns contextual information including page numbers, chunk locations, and similarity scores. + Use this to answer the user's questions. + """ + + TOOL_PROPERTY_DESCRIPTION = """ + The search query to find semantically similar content in uploaded documents. + Should be a natural language question or keyword phrase. + """ + + tools = [ + { + "type": "function", + "name": "search_documents", + "description": TOOL_DESCRIPTION, + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": TOOL_PROPERTY_DESCRIPTION, + } + }, + "required": ["query"], + }, + } + ] + + def search_documents(query: str, user=user) -> str: + """ + Search through user's uploaded documents using semantic similarity. + + This function performs vector similarity search against the user's document corpus + and returns formatted results with context information for the LLM to use. + + Parameters + ---------- + query : str + The search query string + user : User + The authenticated user whose documents to search + + Returns + ------- + str + Formatted search results containing document excerpts with metadata + + Raises + ------ + Exception + If embedding search fails + """ + + try: + embeddings_results = get_closest_embeddings( + user=user, message_data=query.strip() + ) + embeddings_results = convert_uuids(embeddings_results) + + if not embeddings_results: + return "No relevant documents found for your query. Please try different search terms or upload documents first." + + # Format results with clear structure and metadata + prompt_texts = [ + f"[Document {i + 1} - File: {obj['file_id']}, Page: {obj['page_number']}, Chunk: {obj['chunk_number']}, Similarity: {1 - obj['distance']:.3f}]\n{obj['text']}\n[End Document {i + 1}]" + for i, obj in enumerate(embeddings_results) + ] + + return "\n\n".join(prompt_texts) + + except Exception as e: + return f"Error searching documents: {str(e)}. Please try again if the issue persists." + + MODEL_DEFAULTS = { + "model": "gpt-5-nano", # 400,000 token context window + "reasoning": {"effort": "medium"}, + "tools": tools, + } + + # We fetch a response and then kick off a loop to handle the response + + request_data = request.data.get("message", None) + if not request_data: + return Response( + {"error": "Message data is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + message = str(request_data) + + response = client.responses.create( + input=[{"type": "text", "text": message}], **MODEL_DEFAULTS + ) + + # Open AI Cookbook: Handling Function Calls with Reasoning Models + # https://cookbook.openai.com/examples/reasoning_function_calls + while True: + # Mapping of the tool names we tell the model about and the functions that implement them + function_responses = invoke_functions_from_response( + response, tool_mapping={"search_documents": search_documents} + ) + if len(function_responses) == 0: # We're done reasoning + logger.info(f"Reasoning completed for user {user.id}") + final_response = response.output_text + logger.debug( + f"Final response length: {len(final_response)} characters" + ) + break + else: + logger.debug("More reasoning required, continuing...") + response = client.responses.create( + input=function_responses, + previous_response_id=response.id, + **MODEL_DEFAULTS, + ) + + return Response({"response": final_response}, status=status.HTTP_200_OK) + + except Exception as e: + logger.error( + f"Unexpected error in Assistant view for user {request.user.id if hasattr(request, 'user') else 'unknown'}: {e}", + exc_info=True, + ) + return Response( + {"error": "An unexpected error occurred. Please try again later."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) From 674c8c33d7a0ff9e8f29561cf423b2bbe03ff42a Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Mon, 18 Aug 2025 14:36:03 -0400 Subject: [PATCH 02/22] Add docstrings and minor formatting improvements for code clarity --- server/api/services/conversions_services.py | 17 ++++ server/api/services/embedding_services.py | 56 +++++++++--- server/api/services/tools/tools.py | 43 ++++----- server/api/views/conversations/urls.py | 9 +- server/api/views/conversations/views.py | 97 ++++++++++++--------- server/api/views/text_extraction/urls.py | 17 ++-- 6 files changed, 158 insertions(+), 81 deletions(-) diff --git a/server/api/services/conversions_services.py b/server/api/services/conversions_services.py index d134ff49..71931f17 100644 --- a/server/api/services/conversions_services.py +++ b/server/api/services/conversions_services.py @@ -2,6 +2,23 @@ def convert_uuids(data): + """ + Recursively convert UUID objects to strings in nested data structures. + + Traverses dictionaries, lists, and other data structures to find UUID objects + and converts them to their string representation for serialization. + + Parameters + ---------- + data : any + The data structure to process (dict, list, UUID, or any other type) + + Returns + ------- + any + The data structure with all UUID objects converted to strings. + Structure and types are preserved except for UUID -> str conversion. + """ if isinstance(data, dict): return {key: convert_uuids(value) for key, value in data.items()} elif isinstance(data, list): diff --git a/server/api/services/embedding_services.py b/server/api/services/embedding_services.py index 5aacab38..6fd34d35 100644 --- a/server/api/services/embedding_services.py +++ b/server/api/services/embedding_services.py @@ -1,29 +1,63 @@ # services/embedding_services.py + +from pgvector.django import L2Distance + from .sentencetTransformer_model import TransformerModel + # Adjust import path as needed from ..models.model_embeddings import Embeddings -from pgvector.django import L2Distance -def get_closest_embeddings(user, message_data, document_name=None, guid=None, num_results=10): +def get_closest_embeddings( + user, message_data, document_name=None, guid=None, num_results=10 +): + """ + Find the closest embeddings to a given message for a specific user. + + Parameters + ---------- + user : User + The user whose uploaded documents will be searched + message_data : str + The input message to find similar embeddings for + document_name : str, optional + Filter results to a specific document name + guid : str, optional + Filter results to a specific document GUID (takes precedence over document_name) + num_results : int, default 10 + Maximum number of results to return + + Returns + ------- + list[dict] + List of dictionaries containing embedding results with keys: + - name: document name + - text: embedded text content + - page_number: page number in source document + - chunk_number: chunk number within the document + - distance: L2 distance from query embedding + - file_id: GUID of the source file + """ + # transformerModel = TransformerModel.get_instance().model embedding_message = transformerModel.encode(message_data) # Start building the query based on the message's embedding - closest_embeddings_query = Embeddings.objects.filter( - upload_file__uploaded_by=user - ).annotate( - distance=L2Distance( - 'embedding_sentence_transformers', embedding_message) - ).order_by('distance') + closest_embeddings_query = ( + Embeddings.objects.filter(upload_file__uploaded_by=user) + .annotate( + distance=L2Distance("embedding_sentence_transformers", embedding_message) + ) + .order_by("distance") + ) # Filter by GUID if provided, otherwise filter by document name if provided if guid: closest_embeddings_query = closest_embeddings_query.filter( - upload_file__guid=guid) + upload_file__guid=guid + ) elif document_name: - closest_embeddings_query = closest_embeddings_query.filter( - name=document_name) + closest_embeddings_query = closest_embeddings_query.filter(name=document_name) # Slice the results to limit to num_results closest_embeddings_query = closest_embeddings_query[:num_results] diff --git a/server/api/services/tools/tools.py b/server/api/services/tools/tools.py index f9fa14c8..03ef1fd3 100644 --- a/server/api/services/tools/tools.py +++ b/server/api/services/tools/tools.py @@ -1,6 +1,8 @@ -from django.db import connection from typing import Dict, Any, Callable, List from dataclasses import dataclass + +from django.db import connection + from .database import ask_database, get_database_info database_schema_dict = get_database_info(connection) @@ -11,6 +13,7 @@ ] ) + @dataclass class ToolFunction: name: str @@ -18,6 +21,7 @@ class ToolFunction: description: str parameters: Dict[str, Any] + def create_tool_dict(tool: ToolFunction) -> Dict[str, Any]: return { "type": "function", @@ -28,10 +32,11 @@ def create_tool_dict(tool: ToolFunction) -> Dict[str, Any]: "type": "object", "properties": tool.parameters, "required": list(tool.parameters.keys()), - } - } + }, + }, } + TOOL_FUNCTIONS = [ ToolFunction( name="ask_database", @@ -56,60 +61,58 @@ def create_tool_dict(tool: ToolFunction) -> Dict[str, Any]: SQL should be written using this database schema: {database_schema_string} The query should be returned in plain text, not in JSON. - """ + """, } - } + }, ), ] # Automatically generate the tool_functions dictionary and tools list -tool_functions: Dict[str, Callable] = { - tool.name: tool.func for tool in TOOL_FUNCTIONS -} +tool_functions: Dict[str, Callable] = {tool.name: tool.func for tool in TOOL_FUNCTIONS} + +tools: List[Dict[str, Any]] = [create_tool_dict(tool) for tool in TOOL_FUNCTIONS] -tools: List[Dict[str, Any]] = [ - create_tool_dict(tool) for tool in TOOL_FUNCTIONS -] def validate_tool_inputs(tool_function_name, tool_arguments): """Validate the inputs for the execute_tool function.""" if not isinstance(tool_function_name, str) or not tool_function_name: raise ValueError("Invalid tool function name") - + if not isinstance(tool_arguments, dict): raise ValueError("Tool arguments must be a dictionary") - + # Check if the tool_function_name exists in the tools tool = next((t for t in tools if t["function"]["name"] == tool_function_name), None) if not tool: raise ValueError(f"Tool function '{tool_function_name}' does not exist") - + # Validate the tool arguments based on the tool's parameters parameters = tool["function"].get("parameters", {}) required_params = parameters.get("required", []) for param in required_params: if param not in tool_arguments: raise ValueError(f"Missing required parameter: {param}") - + # Check if the parameter types match the expected types properties = parameters.get("properties", {}) for param, prop in properties.items(): - expected_type = prop.get('type') + expected_type = prop.get("type") if param in tool_arguments: - if expected_type == 'string' and not isinstance(tool_arguments[param], str): + if expected_type == "string" and not isinstance(tool_arguments[param], str): raise ValueError(f"Parameter '{param}' must be of type string") - + + def execute_tool(function_name: str, arguments: Dict[str, Any]) -> str: """ Execute the appropriate function based on the function name. - + :param function_name: The name of the function to execute :param arguments: A dictionary of arguments to pass to the function :return: The result of the function execution """ # Validate tool inputs validate_tool_inputs(function_name, arguments) - + try: return tool_functions[function_name](**arguments) except Exception as e: diff --git a/server/api/views/conversations/urls.py b/server/api/views/conversations/urls.py index f10d6814..8896d964 100644 --- a/server/api/views/conversations/urls.py +++ b/server/api/views/conversations/urls.py @@ -1,13 +1,12 @@ from django.urls import path, include -from api.views.conversations import views from rest_framework.routers import DefaultRouter -# from views import ConversationViewSet + +from api.views.conversations import views router = DefaultRouter() -router.register(r'conversations', views.ConversationViewSet, - basename='conversation') +router.register(r"conversations", views.ConversationViewSet, basename="conversation") urlpatterns = [ path("chatgpt/extract_text/", views.extract_text, name="post_web_text"), - path("chatgpt/", include(router.urls)) + path("chatgpt/", include(router.urls)), ] diff --git a/server/api/views/conversations/views.py b/server/api/views/conversations/views.py index d5921eaf..ac7f1ba2 100644 --- a/server/api/views/conversations/views.py +++ b/server/api/views/conversations/views.py @@ -1,3 +1,7 @@ +import os +import json +import logging + from rest_framework.response import Response from rest_framework import viewsets, status from rest_framework.decorators import action @@ -9,10 +13,8 @@ import requests from openai import OpenAI import tiktoken -import os -import json -import logging from django.views.decorators.csrf import csrf_exempt + from .models import Conversation, Message from .serializers import ConversationSerializer from ...services.tools.tools import tools, execute_tool @@ -67,6 +69,7 @@ def get_tokens(string: str, encoding_name: str) -> str: class OpenAIAPIException(APIException): """Custom exception for OpenAI API errors.""" + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR default_detail = "An error occurred while communicating with the OpenAI API." default_code = "openai_api_error" @@ -95,26 +98,29 @@ def destroy(self, request, *args, **kwargs): self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) - @action(detail=True, methods=['post']) + @action(detail=True, methods=["post"]) def continue_conversation(self, request, pk=None): conversation = self.get_object() - user_message = request.data.get('message') - page_context = request.data.get('page_context') + user_message = request.data.get("message") + page_context = request.data.get("page_context") if not user_message: return Response({"error": "Message is required"}, status=400) # Save user message - Message.objects.create(conversation=conversation, - content=user_message, is_user=True) + Message.objects.create( + conversation=conversation, content=user_message, is_user=True + ) # Get ChatGPT response chatgpt_response = self.get_chatgpt_response( - conversation, user_message, page_context) + conversation, user_message, page_context + ) # Save ChatGPT response - Message.objects.create(conversation=conversation, - content=chatgpt_response, is_user=False) + Message.objects.create( + conversation=conversation, content=chatgpt_response, is_user=False + ) # Generate or update title if it's the first message or empty if conversation.messages.count() <= 2 or not conversation.title: @@ -123,25 +129,31 @@ def continue_conversation(self, request, pk=None): return Response({"response": chatgpt_response, "title": conversation.title}) - @action(detail=True, methods=['patch']) + @action(detail=True, methods=["patch"]) def update_title(self, request, pk=None): conversation = self.get_object() - new_title = request.data.get('title') + new_title = request.data.get("title") if not new_title: - return Response({"error": "New title is required"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": "New title is required"}, status=status.HTTP_400_BAD_REQUEST + ) conversation.title = new_title conversation.save() - return Response({"status": "Title updated successfully", "title": conversation.title}) + return Response( + {"status": "Title updated successfully", "title": conversation.title} + ) def get_chatgpt_response(self, conversation, user_message, page_context=None): client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) - messages = [{ - "role": "system", - "content": "You are a knowledgeable assistant. Balancer is a powerful tool for selecting bipolar medication for patients. We are open-source and available for free use. Your primary role is to assist licensed clinical professionals with information related to Balancer and bipolar medication selection. If applicable, use the supplied tools to assist the professional." - }] + messages = [ + { + "role": "system", + "content": "You are a knowledgeable assistant. Balancer is a powerful tool for selecting bipolar medication for patients. We are open-source and available for free use. Your primary role is to assist licensed clinical professionals with information related to Balancer and bipolar medication selection. If applicable, use the supplied tools to assist the professional.", + } + ] if page_context: context_message = f"If applicable, please use the following content to ask questions. If not applicable, please answer to the best of your ability: {page_context}" @@ -157,7 +169,7 @@ def get_chatgpt_response(self, conversation, user_message, page_context=None): model="gpt-3.5-turbo", messages=messages, tools=tools, - tool_choice="auto" + tool_choice="auto", ) response_message = response.choices[0].message @@ -166,37 +178,41 @@ def get_chatgpt_response(self, conversation, user_message, page_context=None): tool_calls = response_message.model_dump().get("tool_calls", []) if not tool_calls: - return response_message['content'] + return response_message["content"] # Handle tool calls # Add the assistant's message with tool calls to the conversation - messages.append({ - "role": "assistant", - "content": response_message.content or "", - "tool_calls": tool_calls - }) + messages.append( + { + "role": "assistant", + "content": response_message.content or "", + "tool_calls": tool_calls, + } + ) # Process each tool call for tool_call in tool_calls: - tool_call_id = tool_call['id'] - tool_function_name = tool_call['function']['name'] + tool_call_id = tool_call["id"] + tool_function_name = tool_call["function"]["name"] tool_arguments = json.loads( - tool_call['function'].get('arguments', '{}')) + tool_call["function"].get("arguments", "{}") + ) # Execute the tool results = execute_tool(tool_function_name, tool_arguments) # Add the tool response message - messages.append({ - "role": "tool", - "content": str(results), # Convert results to string - "tool_call_id": tool_call_id - }) + messages.append( + { + "role": "tool", + "content": str(results), # Convert results to string + "tool_call_id": tool_call_id, + } + ) # Final API call with tool results final_response = client.chat.completions.create( - model="gpt-3.5-turbo", - messages=messages + model="gpt-3.5-turbo", messages=messages ) return final_response.choices[0].message.content except OpenAI.error.OpenAIError as e: @@ -215,9 +231,12 @@ def generate_title(self, conversation): response = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ - {"role": "system", "content": "You are a helpful assistant that generates short, descriptive titles."}, - {"role": "user", "content": prompt} - ] + { + "role": "system", + "content": "You are a helpful assistant that generates short, descriptive titles.", + }, + {"role": "user", "content": prompt}, + ], ) return response.choices[0].message.content.strip() diff --git a/server/api/views/text_extraction/urls.py b/server/api/views/text_extraction/urls.py index bdf6244f..d0af979e 100644 --- a/server/api/views/text_extraction/urls.py +++ b/server/api/views/text_extraction/urls.py @@ -1,11 +1,16 @@ from django.urls import path -from .views import RuleExtractionAPIView, RuleExtractionAPIOpenAIView +from .views import RuleExtractionAPIView, RuleExtractionAPIOpenAIView urlpatterns = [ - - path('v1/api/rule_extraction', RuleExtractionAPIView.as_view(), - name='rule_extraction'), - path('v1/api/rule_extraction_openai', RuleExtractionAPIOpenAIView.as_view(), - name='rule_extraction_openai') + path( + "v1/api/rule_extraction", + RuleExtractionAPIView.as_view(), + name="rule_extraction", + ), + path( + "v1/api/rule_extraction_openai", + RuleExtractionAPIOpenAIView.as_view(), + name="rule_extraction_openai", + ), ] From 1e71158f5c7974545496d9a29d2bef7d51ffa176 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Mon, 18 Aug 2025 14:50:11 -0400 Subject: [PATCH 03/22] Update balancer_backend/urls.py --- server/balancer_backend/urls.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/server/balancer_backend/urls.py b/server/balancer_backend/urls.py index 1c5bad8b..f0772721 100644 --- a/server/balancer_backend/urls.py +++ b/server/balancer_backend/urls.py @@ -1,6 +1,8 @@ from django.contrib import admin # Import Django's admin interface module + # Import functions for URL routing and including other URL configs from django.urls import path, include, re_path + # Import TemplateView for rendering templates from django.views.generic import TemplateView import importlib # Import the importlib module for dynamic module importing @@ -10,25 +12,37 @@ # Map 'admin/' URL to the Django admin interface path("admin/", admin.site.urls), # Include Djoser's URL patterns under 'auth/' for basic auth - path('auth/', include('djoser.urls')), + path("auth/", include("djoser.urls")), # Include Djoser's JWT auth URL patterns under 'auth/' - path('auth/', include('djoser.urls.jwt')), + path("auth/", include("djoser.urls.jwt")), # Include Djoser's social auth URL patterns under 'auth/' - path('auth/', include('djoser.social.urls')), + path("auth/", include("djoser.social.urls")), ] # List of application names for which URL patterns will be dynamically added -urls = ['conversations', 'feedback', 'listMeds', 'risk', - 'uploadFile', 'ai_promptStorage', 'ai_settings', 'embeddings', 'medRules', 'text_extraction'] +urls = [ + "conversations", + "feedback", + "listMeds", + "risk", + "uploadFile", + "ai_promptStorage", + "ai_settings", + "embeddings", + "medRules", + "text_extraction", + "assistants", +] # Loop through each application name and dynamically import and add its URL patterns for url in urls: # Dynamically import the URL module for each app - url_module = importlib.import_module(f'api.views.{url}.urls') + url_module = importlib.import_module(f"api.views.{url}.urls") # Append the URL patterns from each imported module - urlpatterns += getattr(url_module, 'urlpatterns', []) + urlpatterns += getattr(url_module, "urlpatterns", []) # Add a catch-all URL pattern for handling SPA (Single Page Application) routing # Serve 'index.html' for any unmatched URL urlpatterns += [ - re_path(r'^.*$', TemplateView.as_view(template_name='index.html')),] + re_path(r"^.*$", TemplateView.as_view(template_name="index.html")), +] From 3342224349f4f97a73348832194465bac66c4881 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Mon, 18 Aug 2025 15:17:18 -0400 Subject: [PATCH 04/22] Fix typo in balancer_backend urls --- server/balancer_backend/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/balancer_backend/urls.py b/server/balancer_backend/urls.py index f0772721..56f307e4 100644 --- a/server/balancer_backend/urls.py +++ b/server/balancer_backend/urls.py @@ -31,7 +31,7 @@ "embeddings", "medRules", "text_extraction", - "assistants", + "assistant", ] # Loop through each application name and dynamically import and add its URL patterns From fef2899fea1f338ad8349c2235902be45ca755e5 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Mon, 18 Aug 2025 18:27:37 -0400 Subject: [PATCH 05/22] Improve logging, documentation, and response handling in assistant views and settings configuration --- server/api/views/assistant/views.py | 87 +++++++++---- server/balancer_backend/settings.py | 182 ++++++++++++++++------------ 2 files changed, 167 insertions(+), 102 deletions(-) diff --git a/server/api/views/assistant/views.py b/server/api/views/assistant/views.py index cb67a480..f8db1632 100644 --- a/server/api/views/assistant/views.py +++ b/server/api/views/assistant/views.py @@ -73,7 +73,7 @@ def invoke_functions_from_response( } ) elif response_item.type == "reasoning": - logger.debug("Reasoning step") + logger.debug(f"Reasoning step: {response_item.summary}") return intermediate_messages @@ -88,15 +88,16 @@ def post(self, request): client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) TOOL_DESCRIPTION = """ - Search through the user's uploaded documents using semantic similarity matching. - This function finds the most relevant document chunks based on the input query and - returns contextual information including page numbers, chunk locations, and similarity scores. - Use this to answer the user's questions. + Search the user's uploaded documents for information relevant to answering their question. + Call this function when you need to find specific information from the user's documents + to provide an accurate, citation-backed response. Always search before answering questions + about document content. """ TOOL_PROPERTY_DESCRIPTION = """ - The search query to find semantically similar content in uploaded documents. - Should be a natural language question or keyword phrase. + A specific search query to find relevant information in the user's documents. + Use keywords, phrases, or questions related to what the user is asking about. + Be specific rather than generic - use terms that would appear in the relevant documents. """ tools = [ @@ -119,7 +120,7 @@ def post(self, request): def search_documents(query: str, user=user) -> str: """ - Search through user's uploaded documents using semantic similarity. + Search through user's uploaded documents using semantic similarity. This function performs vector similarity search against the user's document corpus and returns formatted results with context information for the LLM to use. @@ -162,25 +163,58 @@ def search_documents(query: str, user=user) -> str: except Exception as e: return f"Error searching documents: {str(e)}. Please try again if the issue persists." + INSTRUCTIONS = """ + You are an AI assistant that helps users find and understand information about bipolar disorder + from their uploaded bipolar disorder research documents using semantic search. + + SEMANTIC SEARCH STRATEGY: + - Always perform semantic search using the search_documents function when users ask questions + - Use conceptually related terms and synonyms, not just exact keyword matches + - Search for the meaning and context of the user's question, not just literal words + - Consider medical terminology, lay terms, and related conditions when searching + + FUNCTION USAGE: + - When a user asks about information that might be in their documents ALWAYS use the search_documents function first + - Perform semantic searches using concepts, symptoms, treatments, and related terms from the user's question + - Only provide answers based on information found through document searches + + RESPONSE FORMAT: + After gathering information through semantic searches, provide responses that: + 1. Answer the user's question directly using only the found information + 2. Structure responses with clear sections and paragraphs + 3. Include citations after EACH sentence using this exact format: ***[File ID {file_id}, Page {page_number}, Chunk {chunk_number}]*** + 4. Only cite information that directly supports your statements + + If no relevant information is found in the documents, clearly state that the information is not available in the uploaded documents. + """ + MODEL_DEFAULTS = { + "instructions": INSTRUCTIONS, "model": "gpt-5-nano", # 400,000 token context window - "reasoning": {"effort": "medium"}, + "reasoning": {"effort": "medium", "summary": "auto"}, "tools": tools, } # We fetch a response and then kick off a loop to handle the response - request_data = request.data.get("message", None) - if not request_data: - return Response( - {"error": "Message data is required."}, - status=status.HTTP_400_BAD_REQUEST, - ) - message = str(request_data) + message = request.data.get("message", None) + previous_response_id = request.data.get("previous_response_id", None) - response = client.responses.create( - input=[{"type": "text", "text": message}], **MODEL_DEFAULTS - ) + if not previous_response_id: + response = client.responses.create( + input=[ + {"type": "message", "role": "user", "content": str(message)} + ], + **MODEL_DEFAULTS, + ) + else: + response = client.responses.create( + input=[ + {"type": "message", "role": "user", "content": str(message)} + ], + previous_response_id=str(previous_response_id), + **MODEL_DEFAULTS, + ) # Open AI Cookbook: Handling Function Calls with Reasoning Models # https://cookbook.openai.com/examples/reasoning_function_calls @@ -190,10 +224,11 @@ def search_documents(query: str, user=user) -> str: response, tool_mapping={"search_documents": search_documents} ) if len(function_responses) == 0: # We're done reasoning - logger.info(f"Reasoning completed for user {user.id}") - final_response = response.output_text + logger.info("Reasoning completed") + final_response_output_text = response.output_text + final_response_id = response.id logger.debug( - f"Final response length: {len(final_response)} characters" + f"Final response length: {len(final_response_output_text)} characters" ) break else: @@ -204,7 +239,13 @@ def search_documents(query: str, user=user) -> str: **MODEL_DEFAULTS, ) - return Response({"response": final_response}, status=status.HTTP_200_OK) + return Response( + { + "response_output_text": final_response_output_text, + "final_response_id": final_response_id, + }, + status=status.HTTP_200_OK, + ) except Exception as e: logger.error( diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index 91a53a8b..0b4a204c 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -29,57 +29,56 @@ # Fetching the value from the environment and splitting to list if necessary. # Fallback to '*' if the environment variable is not set. -ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split() +ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split() # If the environment variable contains '*', the split method would create a list with an empty string. # So you need to check for this case and adjust accordingly. -if ALLOWED_HOSTS == ['*'] or ALLOWED_HOSTS == ['']: - ALLOWED_HOSTS = ['*'] +if ALLOWED_HOSTS == ["*"] or ALLOWED_HOSTS == [""]: + ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'balancer_backend', - 'api', - 'corsheaders', - 'rest_framework', - 'djoser', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "balancer_backend", + "api", + "corsheaders", + "rest_framework", + "djoser", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', - + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "corsheaders.middleware.CorsMiddleware", ] -ROOT_URLCONF = 'balancer_backend.urls' +ROOT_URLCONF = "balancer_backend.urls" CORS_ALLOW_ALL_ORIGINS = True TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'build')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "build")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, @@ -89,7 +88,7 @@ # Change this to your desired URL LOGIN_REDIRECT_URL = os.environ.get("LOGIN_REDIRECT_URL") -WSGI_APPLICATION = 'balancer_backend.wsgi.application' +WSGI_APPLICATION = "balancer_backend.wsgi.application" # Database @@ -106,8 +105,8 @@ } } -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'smtp.gmail.com' +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "smtp.gmail.com" EMAIL_PORT = 587 EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") @@ -119,25 +118,25 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -147,64 +146,89 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'build/static'), + os.path.join(BASE_DIR, "build/static"), ] -STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATIC_ROOT = os.path.join(BASE_DIR, "static") AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', + "django.contrib.auth.backends.ModelBackend", ] REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated' - ], - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", ), } SIMPLE_JWT = { - 'AUTH_HEADER_TYPES': ('JWT',), - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), - 'TOKEN_OBTAIN_SERIALIZER': 'api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer', - 'AUTH_TOKEN_CLASSES': ( - 'rest_framework_simplejwt.tokens.AccessToken', - ) + "AUTH_HEADER_TYPES": ("JWT",), + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "TOKEN_OBTAIN_SERIALIZER": "api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer", + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), } DJOSER = { - 'LOGIN_FIELD': 'email', - 'USER_CREATE_PASSWORD_RETYPE': True, - 'USERNAME_CHANGED_EMAIL_CONFIRMATION': True, - 'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True, - 'SEND_CONFIRMATION_EMAIL': True, - 'SET_USERNAME_RETYPE': True, - 'SET_PASSWORD_RETYPE': True, - 'PASSWORD_RESET_CONFIRM_URL': 'password/reset/confirm/{uid}/{token}', - 'USERNAME_RESET_CONFIRM_URL': 'email/reset/confirm/{uid}/{token}', - 'ACTIVATION_URL': 'activate/{uid}/{token}', - 'SEND_ACTIVATION_EMAIL': True, - 'SOCIAL_AUTH_TOKEN_STRATEGY': 'djoser.social.token.jwt.TokenStrategy', - 'SOCIAL_AUTH_ALLOWED_REDIRECT_URIS': ['http://localhost:8000/google', 'http://localhost:8000/facebook'], - 'SERIALIZERS': { - 'user_create': 'api.models.serializers.UserCreateSerializer', - 'user': 'api.models.serializers.UserCreateSerializer', - 'current_user': 'api.models.serializers.UserCreateSerializer', - 'user_delete': 'djoser.serializers.UserDeleteSerializer', - } + "LOGIN_FIELD": "email", + "USER_CREATE_PASSWORD_RETYPE": True, + "USERNAME_CHANGED_EMAIL_CONFIRMATION": True, + "PASSWORD_CHANGED_EMAIL_CONFIRMATION": True, + "SEND_CONFIRMATION_EMAIL": True, + "SET_USERNAME_RETYPE": True, + "SET_PASSWORD_RETYPE": True, + "PASSWORD_RESET_CONFIRM_URL": "password/reset/confirm/{uid}/{token}", + "USERNAME_RESET_CONFIRM_URL": "email/reset/confirm/{uid}/{token}", + "ACTIVATION_URL": "activate/{uid}/{token}", + "SEND_ACTIVATION_EMAIL": True, + "SOCIAL_AUTH_TOKEN_STRATEGY": "djoser.social.token.jwt.TokenStrategy", + "SOCIAL_AUTH_ALLOWED_REDIRECT_URIS": [ + "http://localhost:8000/google", + "http://localhost:8000/facebook", + ], + "SERIALIZERS": { + "user_create": "api.models.serializers.UserCreateSerializer", + "user": "api.models.serializers.UserCreateSerializer", + "current_user": "api.models.serializers.UserCreateSerializer", + "user_delete": "djoser.serializers.UserDeleteSerializer", + }, } # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +AUTH_USER_MODEL = "api.UserAccount" -AUTH_USER_MODEL = 'api.UserAccount' +# Logging configuration +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "root": { + "handlers": ["console"], + "level": "DEBUG", + }, +} From 4376600b1796b4a3c3168c226b432ca7450a191f Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Tue, 19 Aug 2025 17:19:19 -0400 Subject: [PATCH 06/22] change debug logs to info logs for better visibility in production --- server/api/views/assistant/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/api/views/assistant/views.py b/server/api/views/assistant/views.py index f8db1632..8c75a207 100644 --- a/server/api/views/assistant/views.py +++ b/server/api/views/assistant/views.py @@ -56,7 +56,7 @@ def invoke_functions_from_response( f"Invoking tool: {response_item.name} with arguments: {arguments}" ) tool_output = target_tool(**arguments) - logger.debug(f"Tool {response_item.name} completed successfully") + logger.info(f"Tool {response_item.name} completed successfully") except Exception as e: msg = f"Error executing function call: {response_item.name}: {e}" tool_output = msg @@ -73,7 +73,7 @@ def invoke_functions_from_response( } ) elif response_item.type == "reasoning": - logger.debug(f"Reasoning step: {response_item.summary}") + logger.info(f"Reasoning step: {response_item.summary}") return intermediate_messages @@ -227,12 +227,12 @@ def search_documents(query: str, user=user) -> str: logger.info("Reasoning completed") final_response_output_text = response.output_text final_response_id = response.id - logger.debug( + logger.info( f"Final response length: {len(final_response_output_text)} characters" ) break else: - logger.debug("More reasoning required, continuing...") + logger.info("More reasoning required, continuing...") response = client.responses.create( input=function_responses, previous_response_id=response.id, From ad099debf6a6d1937fb4c552dbb668acffefad87 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Tue, 19 Aug 2025 17:39:34 -0400 Subject: [PATCH 07/22] Add cost metrics and duration logging --- server/api/views/assistant/views.py | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/server/api/views/assistant/views.py b/server/api/views/assistant/views.py index 8c75a207..e80f2333 100644 --- a/server/api/views/assistant/views.py +++ b/server/api/views/assistant/views.py @@ -1,6 +1,7 @@ import os import json import logging +import time from typing import Callable from rest_framework.views import APIView @@ -18,6 +19,37 @@ # Configure logging logger = logging.getLogger(__name__) +GPT_5_NANO_PRICING_DOLLARS_PER_MILLION_TOKENS = {"input": 0.05, "output": 0.40} + + +def calculate_cost_metrics(token_usage: dict, pricing: dict) -> dict: + """ + Calculate cost metrics based on token usage and pricing + + Args: + token_usage: Dictionary containing input_tokens and output_tokens + pricing: Dictionary containing input and output pricing per million tokens + + Returns: + Dictionary containing input_cost, output_cost, and total_cost in USD + """ + TOKENS_PER_MILLION = 1_000_000 + + # Pricing is in dollars per million tokens + input_cost_dollars = (pricing["input"] / TOKENS_PER_MILLION) * token_usage.get( + "input_tokens", 0 + ) + output_cost_dollars = (pricing["output"] / TOKENS_PER_MILLION) * token_usage.get( + "output_tokens", 0 + ) + total_cost_dollars = input_cost_dollars + output_cost_dollars + + return { + "input_cost": input_cost_dollars, + "output_cost": output_cost_dollars, + "total_cost": total_cost_dollars, + } + # Open AI Cookbook: Handling Function Calls with Reasoning Models # https://cookbook.openai.com/examples/reasoning_function_calls @@ -200,6 +232,10 @@ def search_documents(query: str, user=user) -> str: message = request.data.get("message", None) previous_response_id = request.data.get("previous_response_id", None) + # Track total duration and cost metrics + start_time = time.time() + total_token_usage = {"input_tokens": 0, "output_tokens": 0} + if not previous_response_id: response = client.responses.create( input=[ @@ -216,6 +252,15 @@ def search_documents(query: str, user=user) -> str: **MODEL_DEFAULTS, ) + # Accumulate token usage from initial response + if hasattr(response, "usage"): + total_token_usage["input_tokens"] += getattr( + response.usage, "input_tokens", 0 + ) + total_token_usage["output_tokens"] += getattr( + response.usage, "output_tokens", 0 + ) + # Open AI Cookbook: Handling Function Calls with Reasoning Models # https://cookbook.openai.com/examples/reasoning_function_calls while True: @@ -238,6 +283,29 @@ def search_documents(query: str, user=user) -> str: previous_response_id=response.id, **MODEL_DEFAULTS, ) + # Accumulate token usage from reasoning iterations + if hasattr(response, "usage"): + total_token_usage["input_tokens"] += getattr( + response.usage, "input_tokens", 0 + ) + total_token_usage["output_tokens"] += getattr( + response.usage, "output_tokens", 0 + ) + + # Calculate total duration and cost metrics + total_duration = time.time() - start_time + cost_metrics = calculate_cost_metrics( + total_token_usage, GPT_5_NANO_PRICING_DOLLARS_PER_MILLION_TOKENS + ) + + # Log cost and duration metrics + logger.info( + f"Request completed: " + f"Duration: {total_duration:.2f}s, " + f"Input tokens: {total_token_usage['input_tokens']}, " + f"Output tokens: {total_token_usage['output_tokens']}, " + f"Total cost: ${cost_metrics['total_cost']:.6f}" + ) return Response( { From 6161f5f4df0cacbf26225c8a4cc5b783a06aea67 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Tue, 19 Aug 2025 19:39:38 -0400 Subject: [PATCH 08/22] Change root logging level from DEBUG to INFO --- server/balancer_backend/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index 0b4a204c..175ca6ab 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -229,6 +229,6 @@ }, "root": { "handlers": ["console"], - "level": "DEBUG", + "level": "INFO", }, } From c90e95e561e05898d12a85c9fd4380e5eab84574 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Wed, 20 Aug 2025 15:13:22 -0400 Subject: [PATCH 09/22] Update Docker Compose configuration for PostgreSQL and frontend services --- docker-compose.yml | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d8a8ca75..f36517ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,18 @@ services: db: - build: - context: ./db - dockerfile: Dockerfile - volumes: - - postgres_data:/var/lib/postgresql/data/ - environment: - - POSTGRES_USER=balancer - - POSTGRES_PASSWORD=balancer - - POSTGRES_DB=balancer_dev - ports: + image: pgvector/pgvector:pg15 + volumes: + - postgres_data:/var/lib/postgresql/data/ + - ./init-vector-extension.sql:/docker-entrypoint-initdb.d/init-vector-extension.sql + environment: + - POSTGRES_USER=balancer + - POSTGRES_PASSWORD=balancer + - POSTGRES_DB=balancer_dev + ports: - "5433:5432" - networks: - app_net: - ipv4_address: 192.168.0.2 + networks: + app_net: + ipv4_address: 192.168.0.2 pgadmin: container_name: pgadmin4 image: dpage/pgadmin4 @@ -52,13 +51,13 @@ services: args: - IMAGE_NAME=balancer-frontend ports: - - "3000:3000" + - "3000:3000" environment: - - CHOKIDAR_USEPOLLING=true - # - VITE_API_BASE_URL=https://balancertestsite.com/ + - CHOKIDAR_USEPOLLING=true + # - VITE_API_BASE_URL=https://balancertestsite.com/ volumes: - - "./frontend:/usr/src/app:delegated" - - "/usr/src/app/node_modules/" + - "./frontend:/usr/src/app:delegated" + - "/usr/src/app/node_modules/" depends_on: - backend networks: @@ -72,4 +71,4 @@ networks: driver: default config: - subnet: "192.168.0.0/24" - gateway: 192.168.0.1 \ No newline at end of file + gateway: 192.168.0.1 From ff13ad4d5fc5c97c6e77b1023a93e46e108f41a3 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Wed, 20 Aug 2025 15:28:37 -0400 Subject: [PATCH 10/22] Update reasoning effort level and improve final response logging --- server/api/views/assistant/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/api/views/assistant/views.py b/server/api/views/assistant/views.py index e80f2333..fd49e5e5 100644 --- a/server/api/views/assistant/views.py +++ b/server/api/views/assistant/views.py @@ -223,7 +223,7 @@ def search_documents(query: str, user=user) -> str: MODEL_DEFAULTS = { "instructions": INSTRUCTIONS, "model": "gpt-5-nano", # 400,000 token context window - "reasoning": {"effort": "medium", "summary": "auto"}, + "reasoning": {"effort": "low", "summary": "auto"}, "tools": tools, } @@ -272,9 +272,7 @@ def search_documents(query: str, user=user) -> str: logger.info("Reasoning completed") final_response_output_text = response.output_text final_response_id = response.id - logger.info( - f"Final response length: {len(final_response_output_text)} characters" - ) + logger.info(f"Final response: {final_response_output_text}") break else: logger.info("More reasoning required, continuing...") From e6d66a26141335eb36be7c5dbd541ebf7f7e8450 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Wed, 20 Aug 2025 19:09:46 -0400 Subject: [PATCH 11/22] Simpler stateless chat interface that focuses on document-based assistance - Replace conversation-based chat system with direct assistant API calls - Remove ConversationList component and related conversation state management - Switch from continueConversation to sendAssistantMessage API endpoint - Track current messages and response IDs instead of full conversations - Remove conversation creation, deletion, and title updating functionality - Simplify UI by removing conversation list toggle and management buttons - Update welcome message to focus on document-based Q&A instead of bipolar treatment - Maintain backward compatibility by keeping Conversation interface export - Remove DOM content extraction and page content tracking - Streamline message handling with direct assistant responses --- frontend/src/api/apiClient.ts | 17 +- frontend/src/components/Header/Chat.tsx | 382 +++++++++--------------- 2 files changed, 156 insertions(+), 243 deletions(-) diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 73b74caf..3e672f1e 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -267,6 +267,20 @@ const updateConversationTitle = async ( } }; +// Assistant API functions +const sendAssistantMessage = async (message: string, previousResponseId?: string) => { + try { + const response = await api.post(`/v1/api/assistant`, { + message, + previous_response_id: previousResponseId, + }); + return response.data; + } catch (error) { + console.error("Error(s) during sendAssistantMessage: ", error); + throw error; + } +}; + export { handleSubmitFeedback, handleSendDrugSummary, @@ -279,5 +293,6 @@ export { updateConversationTitle, handleSendDrugSummaryStream, handleSendDrugSummaryStreamLegacy, - fetchRiskDataWithSources + fetchRiskDataWithSources, + sendAssistantMessage }; \ No newline at end of file diff --git a/frontend/src/components/Header/Chat.tsx b/frontend/src/components/Header/Chat.tsx index df11f68b..674ff438 100644 --- a/frontend/src/components/Header/Chat.tsx +++ b/frontend/src/components/Header/Chat.tsx @@ -4,12 +4,10 @@ import "../../components/Header/chat.css"; import { useState, useEffect, useRef } from "react"; import TypingAnimation from "./components/TypingAnimation"; import ErrorMessage from "../ErrorMessage"; -import ConversationList from "./ConversationList"; -import { extractContentFromDOM } from "../../services/domExtraction"; +import ParseStringWithLinks from "../../services/parsing/ParseWithSource"; import axios from "axios"; import { FaPlus, - FaMinus, FaTimes, FaComment, FaComments, @@ -19,13 +17,7 @@ import { FaExpandAlt, FaExpandArrowsAlt, } from "react-icons/fa"; -import { - fetchConversations, - continueConversation, - newConversation, - updateConversationTitle, - deleteConversation, -} from "../../api/apiClient"; +import { sendAssistantMessage } from "../../api/apiClient"; interface ChatLogItem { is_user: boolean; @@ -33,12 +25,14 @@ interface ChatLogItem { timestamp: string; // EX: 2025-01-16T16:21:14.981090Z } +// Keep interface for backward compatibility with existing imports export interface Conversation { title: string; messages: ChatLogItem[]; id: string; } + interface ChatDropDownProps { showChat: boolean; setShowChat: React.Dispatch>; @@ -47,12 +41,9 @@ interface ChatDropDownProps { const Chat: React.FC = ({ showChat, setShowChat }) => { const CHATBOT_NAME = "JJ"; const [inputValue, setInputValue] = useState(""); - const [chatLog, setChatLog] = useState([]); // Specify the type as ChatLogItem[] + const [currentMessages, setCurrentMessages] = useState([]); + const [currentResponseId, setCurrentResponseId] = useState(undefined); const [isLoading, setIsLoading] = useState(false); - const [showConversationList, setShowConversationList] = useState(false); - const [conversations, setConversations] = useState([]); - const [activeConversation, setActiveConversation] = - useState(null); const [error, setError] = useState(null); const suggestionPrompts = [ @@ -63,25 +54,8 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { "Risks associated with Lithium.", "What medications could cause liver issues?", ]; - const [pageContent, setPageContent] = useState(""); const chatContainerRef = useRef(null); - useEffect(() => { - const observer = new MutationObserver(() => { - const content = extractContentFromDOM(); - setPageContent(content); - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - characterData: true, - }); - - const extractedContent = extractContentFromDOM(); - // console.log(extractedContent); - setPageContent(extractedContent); - }, []); const [bottom, setBottom] = useState(false); @@ -96,7 +70,7 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { const [expandChat, setExpandChat] = useState(false); useEffect(() => { - if (chatContainerRef.current && activeConversation) { + if (chatContainerRef.current) { const chatContainer = chatContainerRef.current; // Use setTimeout to ensure the new message has been rendered setTimeout(() => { @@ -107,17 +81,8 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { ); }, 0); } - }, [activeConversation?.messages]); + }, [currentMessages]); - const loadConversations = async () => { - try { - const data = await fetchConversations(); - setConversations(data); - // setLoading(false); - } catch (error) { - console.error("Error loading conversations: ", error); - } - }; const scrollToBottom = (element: HTMLElement) => element.scroll({ top: element.scrollHeight, behavior: "smooth" }); @@ -141,68 +106,39 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { ) => { event.preventDefault(); + const messageContent = (inputValue || suggestion) ?? ""; + if (!messageContent.trim()) return; + const newMessage = { - content: (inputValue || suggestion) ?? "", + content: messageContent, is_user: true, timestamp: new Date().toISOString(), }; - const newMessages = [...chatLog, newMessage]; + try { + setIsLoading(true); + setError(null); - setChatLog(newMessages); + // Add user message to current conversation + const updatedMessages = [...currentMessages, newMessage]; + setCurrentMessages(updatedMessages); - // sendMessage(newMessages); - try { - let conversation = activeConversation; - let conversationCreated = false; - - // Create a new conversation if none exists - if (!conversation) { - conversation = await newConversation(); - setActiveConversation(conversation); - setShowConversationList(false); - conversationCreated = true; - } + // Call assistant API with previous response ID for continuity + const data = await sendAssistantMessage(messageContent, currentResponseId); - // Update the conversation with the new user message - const updatedMessages = [...conversation.messages, newMessage]; - setActiveConversation({ - ...conversation, - title: "Asking JJ...", - messages: updatedMessages, - }); + // Create assistant response message + const assistantMessage = { + content: data.response_output_text, + is_user: false, + timestamp: new Date().toISOString(), + }; - setIsLoading(true); + // Update messages and store new response ID for next message + setCurrentMessages(prev => [...prev, assistantMessage]); + setCurrentResponseId(data.final_response_id); - // Continue the conversation and update with the bot's response - const data = await continueConversation( - conversation.id, - newMessage.content, - pageContent - ); - - // Update the ConversationList component after previous function creates a title - if (conversationCreated) loadConversations(); // Note: no 'await' so this can occur in the background - - setActiveConversation((prevConversation: any) => { - if (!prevConversation) return null; - - return { - ...prevConversation, - messages: [ - ...prevConversation.messages, - { - is_user: false, - content: data.response, - timestamp: new Date().toISOString(), - }, - ], - title: data.title, - }; - }); - setError(null); } catch (error) { - console.error("Error(s) handling conversation:", error); + console.error("Error handling message:", error); let errorMessage = "Error submitting message"; if (error instanceof Error) { errorMessage = error.message; @@ -222,25 +158,8 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { } }; - const handleSelectConversation = (id: Conversation["id"]) => { - const selectedConversation = conversations.find( - (conversation: any) => conversation.id === id - ); - - if (selectedConversation) { - setActiveConversation(selectedConversation); - setShowConversationList(false); - } - }; - - const handleNewConversation = () => { - setActiveConversation(null); - setShowConversationList(false); - }; - useEffect(() => { if (showChat) { - loadConversations(); const resizeObserver = new ResizeObserver((entries) => { if (!entries || entries.length === 0) return; @@ -278,53 +197,34 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { className=" mx-auto flex h-full flex-col overflow-auto rounded " >
- - -
- {activeConversation !== null && !showConversationList ? ( - activeConversation.title - ) : ( - <> - - Ask {CHATBOT_NAME} - - )} -
+
+ + Ask {CHATBOT_NAME}
+ + + - {showConversationList ? ( -
- -
- ) : ( -
- {activeConversation === null || - activeConversation.messages.length === 0 ? ( - <> -
-
Hi there, I'm {CHATBOT_NAME}!
-

- You can ask me all your bipolar disorder treatment - questions. -

- - Learn more about my sources. - +
+ {currentMessages.length === 0 ? ( + <> +
+
Hi there, I'm {CHATBOT_NAME}!
+

+ You can ask me questions about your uploaded documents. + I'll search through them to provide accurate, cited answers. +

+ + Learn more about my sources. + +
+
+
+ +
Explore a medication
-
-
- -
Explore a medication
-
-
    - {suggestionPrompts.map((suggestion, index) => ( -
  • - -
  • - ))} -
+
    + {suggestionPrompts.map((suggestion, index) => ( +
  • + +
  • + ))} +
+
+
+
+ +
Refresh your memory
-
-
- -
Refresh your memory
+
    + {refreshPrompts.map((suggestion, index) => ( +
  • + +
  • + ))} +
+
+ + ) : ( + currentMessages + .slice() + .sort( + (a, b) => + new Date(a.timestamp).getTime() - + new Date(b.timestamp).getTime() + ) + .map((message, index) => ( +
+
+
-
    - {refreshPrompts.map((suggestion, index) => ( -
  • - -
  • - ))} -
-
- - ) : ( - activeConversation.messages - .slice() - .sort( - (a, b) => - new Date(a.timestamp).getTime() - - new Date(b.timestamp).getTime() - ) - .map((message, index) => ( -
-
- {message.is_user ? ( - - ) : ( - - )} -
-
+
+ {message.is_user ? (
 = ({ showChat, setShowChat }) => {
                             >
                               {message.content}
                             
-
+ ) : ( +
+ +
+ )}
- )) - )} - {isLoading && ( -
-
-
+ )) + )} + {isLoading && ( +
+
+
- )} - {error && } -
- )} +
+ )} + {error && } +
Date: Thu, 21 Aug 2025 14:37:35 -0400 Subject: [PATCH 12/22] Simplify message rendering and update citation format in views --- frontend/src/components/Header/Chat.tsx | 34 +++++++------------------ server/api/views/assistant/views.py | 4 +-- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/Header/Chat.tsx b/frontend/src/components/Header/Chat.tsx index 674ff438..95189a10 100644 --- a/frontend/src/components/Header/Chat.tsx +++ b/frontend/src/components/Header/Chat.tsx @@ -4,7 +4,6 @@ import "../../components/Header/chat.css"; import { useState, useEffect, useRef } from "react"; import TypingAnimation from "./components/TypingAnimation"; import ErrorMessage from "../ErrorMessage"; -import ParseStringWithLinks from "../../services/parsing/ParseWithSource"; import axios from "axios"; import { FaPlus, @@ -321,30 +320,15 @@ const Chat: React.FC = ({ showChat, setShowChat }) => {
- {message.is_user ? ( -
-                              {message.content}
-                            
- ) : ( -
- -
- )} +
+                            {message.content}
+                          
)) diff --git a/server/api/views/assistant/views.py b/server/api/views/assistant/views.py index fd49e5e5..ca65f335 100644 --- a/server/api/views/assistant/views.py +++ b/server/api/views/assistant/views.py @@ -186,7 +186,7 @@ def search_documents(query: str, user=user) -> str: # Format results with clear structure and metadata prompt_texts = [ - f"[Document {i + 1} - File: {obj['file_id']}, Page: {obj['page_number']}, Chunk: {obj['chunk_number']}, Similarity: {1 - obj['distance']:.3f}]\n{obj['text']}\n[End Document {i + 1}]" + f"[Document {i + 1} - File: {obj['file_id']}, Name: {obj['name']}, Page: {obj['page_number']}, Chunk: {obj['chunk_number']}, Similarity: {1 - obj['distance']:.3f}]\n{obj['text']}\n[End Document {i + 1}]" for i, obj in enumerate(embeddings_results) ] @@ -214,7 +214,7 @@ def search_documents(query: str, user=user) -> str: After gathering information through semantic searches, provide responses that: 1. Answer the user's question directly using only the found information 2. Structure responses with clear sections and paragraphs - 3. Include citations after EACH sentence using this exact format: ***[File ID {file_id}, Page {page_number}, Chunk {chunk_number}]*** + 3. Include citations using this exact format: ***[Name {name}, Page {page_number}]*** 4. Only cite information that directly supports your statements If no relevant information is found in the documents, clearly state that the information is not available in the uploaded documents. From da4dacc3a5d8862e75744b928ec853fbaf3889f2 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Thu, 21 Aug 2025 15:03:00 -0400 Subject: [PATCH 13/22] Restore files that only have linting changes --- server/api/services/tools/tools.py | 43 +++++------ server/api/views/conversations/urls.py | 9 ++- server/api/views/conversations/views.py | 97 ++++++++++-------------- server/api/views/text_extraction/urls.py | 17 ++--- 4 files changed, 70 insertions(+), 96 deletions(-) diff --git a/server/api/services/tools/tools.py b/server/api/services/tools/tools.py index 03ef1fd3..f9fa14c8 100644 --- a/server/api/services/tools/tools.py +++ b/server/api/services/tools/tools.py @@ -1,8 +1,6 @@ +from django.db import connection from typing import Dict, Any, Callable, List from dataclasses import dataclass - -from django.db import connection - from .database import ask_database, get_database_info database_schema_dict = get_database_info(connection) @@ -13,7 +11,6 @@ ] ) - @dataclass class ToolFunction: name: str @@ -21,7 +18,6 @@ class ToolFunction: description: str parameters: Dict[str, Any] - def create_tool_dict(tool: ToolFunction) -> Dict[str, Any]: return { "type": "function", @@ -32,11 +28,10 @@ def create_tool_dict(tool: ToolFunction) -> Dict[str, Any]: "type": "object", "properties": tool.parameters, "required": list(tool.parameters.keys()), - }, - }, + } + } } - TOOL_FUNCTIONS = [ ToolFunction( name="ask_database", @@ -61,58 +56,60 @@ def create_tool_dict(tool: ToolFunction) -> Dict[str, Any]: SQL should be written using this database schema: {database_schema_string} The query should be returned in plain text, not in JSON. - """, + """ } - }, + } ), ] # Automatically generate the tool_functions dictionary and tools list -tool_functions: Dict[str, Callable] = {tool.name: tool.func for tool in TOOL_FUNCTIONS} - -tools: List[Dict[str, Any]] = [create_tool_dict(tool) for tool in TOOL_FUNCTIONS] +tool_functions: Dict[str, Callable] = { + tool.name: tool.func for tool in TOOL_FUNCTIONS +} +tools: List[Dict[str, Any]] = [ + create_tool_dict(tool) for tool in TOOL_FUNCTIONS +] def validate_tool_inputs(tool_function_name, tool_arguments): """Validate the inputs for the execute_tool function.""" if not isinstance(tool_function_name, str) or not tool_function_name: raise ValueError("Invalid tool function name") - + if not isinstance(tool_arguments, dict): raise ValueError("Tool arguments must be a dictionary") - + # Check if the tool_function_name exists in the tools tool = next((t for t in tools if t["function"]["name"] == tool_function_name), None) if not tool: raise ValueError(f"Tool function '{tool_function_name}' does not exist") - + # Validate the tool arguments based on the tool's parameters parameters = tool["function"].get("parameters", {}) required_params = parameters.get("required", []) for param in required_params: if param not in tool_arguments: raise ValueError(f"Missing required parameter: {param}") - + # Check if the parameter types match the expected types properties = parameters.get("properties", {}) for param, prop in properties.items(): - expected_type = prop.get("type") + expected_type = prop.get('type') if param in tool_arguments: - if expected_type == "string" and not isinstance(tool_arguments[param], str): + if expected_type == 'string' and not isinstance(tool_arguments[param], str): raise ValueError(f"Parameter '{param}' must be of type string") - - + def execute_tool(function_name: str, arguments: Dict[str, Any]) -> str: """ Execute the appropriate function based on the function name. - + :param function_name: The name of the function to execute :param arguments: A dictionary of arguments to pass to the function :return: The result of the function execution """ # Validate tool inputs validate_tool_inputs(function_name, arguments) - + try: return tool_functions[function_name](**arguments) except Exception as e: diff --git a/server/api/views/conversations/urls.py b/server/api/views/conversations/urls.py index 8896d964..f10d6814 100644 --- a/server/api/views/conversations/urls.py +++ b/server/api/views/conversations/urls.py @@ -1,12 +1,13 @@ from django.urls import path, include -from rest_framework.routers import DefaultRouter - from api.views.conversations import views +from rest_framework.routers import DefaultRouter +# from views import ConversationViewSet router = DefaultRouter() -router.register(r"conversations", views.ConversationViewSet, basename="conversation") +router.register(r'conversations', views.ConversationViewSet, + basename='conversation') urlpatterns = [ path("chatgpt/extract_text/", views.extract_text, name="post_web_text"), - path("chatgpt/", include(router.urls)), + path("chatgpt/", include(router.urls)) ] diff --git a/server/api/views/conversations/views.py b/server/api/views/conversations/views.py index ac7f1ba2..d5921eaf 100644 --- a/server/api/views/conversations/views.py +++ b/server/api/views/conversations/views.py @@ -1,7 +1,3 @@ -import os -import json -import logging - from rest_framework.response import Response from rest_framework import viewsets, status from rest_framework.decorators import action @@ -13,8 +9,10 @@ import requests from openai import OpenAI import tiktoken +import os +import json +import logging from django.views.decorators.csrf import csrf_exempt - from .models import Conversation, Message from .serializers import ConversationSerializer from ...services.tools.tools import tools, execute_tool @@ -69,7 +67,6 @@ def get_tokens(string: str, encoding_name: str) -> str: class OpenAIAPIException(APIException): """Custom exception for OpenAI API errors.""" - status_code = status.HTTP_500_INTERNAL_SERVER_ERROR default_detail = "An error occurred while communicating with the OpenAI API." default_code = "openai_api_error" @@ -98,29 +95,26 @@ def destroy(self, request, *args, **kwargs): self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) - @action(detail=True, methods=["post"]) + @action(detail=True, methods=['post']) def continue_conversation(self, request, pk=None): conversation = self.get_object() - user_message = request.data.get("message") - page_context = request.data.get("page_context") + user_message = request.data.get('message') + page_context = request.data.get('page_context') if not user_message: return Response({"error": "Message is required"}, status=400) # Save user message - Message.objects.create( - conversation=conversation, content=user_message, is_user=True - ) + Message.objects.create(conversation=conversation, + content=user_message, is_user=True) # Get ChatGPT response chatgpt_response = self.get_chatgpt_response( - conversation, user_message, page_context - ) + conversation, user_message, page_context) # Save ChatGPT response - Message.objects.create( - conversation=conversation, content=chatgpt_response, is_user=False - ) + Message.objects.create(conversation=conversation, + content=chatgpt_response, is_user=False) # Generate or update title if it's the first message or empty if conversation.messages.count() <= 2 or not conversation.title: @@ -129,31 +123,25 @@ def continue_conversation(self, request, pk=None): return Response({"response": chatgpt_response, "title": conversation.title}) - @action(detail=True, methods=["patch"]) + @action(detail=True, methods=['patch']) def update_title(self, request, pk=None): conversation = self.get_object() - new_title = request.data.get("title") + new_title = request.data.get('title') if not new_title: - return Response( - {"error": "New title is required"}, status=status.HTTP_400_BAD_REQUEST - ) + return Response({"error": "New title is required"}, status=status.HTTP_400_BAD_REQUEST) conversation.title = new_title conversation.save() - return Response( - {"status": "Title updated successfully", "title": conversation.title} - ) + return Response({"status": "Title updated successfully", "title": conversation.title}) def get_chatgpt_response(self, conversation, user_message, page_context=None): client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) - messages = [ - { - "role": "system", - "content": "You are a knowledgeable assistant. Balancer is a powerful tool for selecting bipolar medication for patients. We are open-source and available for free use. Your primary role is to assist licensed clinical professionals with information related to Balancer and bipolar medication selection. If applicable, use the supplied tools to assist the professional.", - } - ] + messages = [{ + "role": "system", + "content": "You are a knowledgeable assistant. Balancer is a powerful tool for selecting bipolar medication for patients. We are open-source and available for free use. Your primary role is to assist licensed clinical professionals with information related to Balancer and bipolar medication selection. If applicable, use the supplied tools to assist the professional." + }] if page_context: context_message = f"If applicable, please use the following content to ask questions. If not applicable, please answer to the best of your ability: {page_context}" @@ -169,7 +157,7 @@ def get_chatgpt_response(self, conversation, user_message, page_context=None): model="gpt-3.5-turbo", messages=messages, tools=tools, - tool_choice="auto", + tool_choice="auto" ) response_message = response.choices[0].message @@ -178,41 +166,37 @@ def get_chatgpt_response(self, conversation, user_message, page_context=None): tool_calls = response_message.model_dump().get("tool_calls", []) if not tool_calls: - return response_message["content"] + return response_message['content'] # Handle tool calls # Add the assistant's message with tool calls to the conversation - messages.append( - { - "role": "assistant", - "content": response_message.content or "", - "tool_calls": tool_calls, - } - ) + messages.append({ + "role": "assistant", + "content": response_message.content or "", + "tool_calls": tool_calls + }) # Process each tool call for tool_call in tool_calls: - tool_call_id = tool_call["id"] - tool_function_name = tool_call["function"]["name"] + tool_call_id = tool_call['id'] + tool_function_name = tool_call['function']['name'] tool_arguments = json.loads( - tool_call["function"].get("arguments", "{}") - ) + tool_call['function'].get('arguments', '{}')) # Execute the tool results = execute_tool(tool_function_name, tool_arguments) # Add the tool response message - messages.append( - { - "role": "tool", - "content": str(results), # Convert results to string - "tool_call_id": tool_call_id, - } - ) + messages.append({ + "role": "tool", + "content": str(results), # Convert results to string + "tool_call_id": tool_call_id + }) # Final API call with tool results final_response = client.chat.completions.create( - model="gpt-3.5-turbo", messages=messages + model="gpt-3.5-turbo", + messages=messages ) return final_response.choices[0].message.content except OpenAI.error.OpenAIError as e: @@ -231,12 +215,9 @@ def generate_title(self, conversation): response = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ - { - "role": "system", - "content": "You are a helpful assistant that generates short, descriptive titles.", - }, - {"role": "user", "content": prompt}, - ], + {"role": "system", "content": "You are a helpful assistant that generates short, descriptive titles."}, + {"role": "user", "content": prompt} + ] ) return response.choices[0].message.content.strip() diff --git a/server/api/views/text_extraction/urls.py b/server/api/views/text_extraction/urls.py index d0af979e..bdf6244f 100644 --- a/server/api/views/text_extraction/urls.py +++ b/server/api/views/text_extraction/urls.py @@ -1,16 +1,11 @@ from django.urls import path - from .views import RuleExtractionAPIView, RuleExtractionAPIOpenAIView + urlpatterns = [ - path( - "v1/api/rule_extraction", - RuleExtractionAPIView.as_view(), - name="rule_extraction", - ), - path( - "v1/api/rule_extraction_openai", - RuleExtractionAPIOpenAIView.as_view(), - name="rule_extraction_openai", - ), + + path('v1/api/rule_extraction', RuleExtractionAPIView.as_view(), + name='rule_extraction'), + path('v1/api/rule_extraction_openai', RuleExtractionAPIOpenAIView.as_view(), + name='rule_extraction_openai') ] From 261634ceb4b07fcdc2a89928985478a455138884 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Thu, 21 Aug 2025 15:17:55 -0400 Subject: [PATCH 14/22] Restore files that had changes for local dev --- docker-compose.yml | 37 +++--- server/balancer_backend/settings.py | 182 ++++++++++++---------------- 2 files changed, 98 insertions(+), 121 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f36517ef..d8a8ca75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,19 @@ services: db: - image: pgvector/pgvector:pg15 - volumes: - - postgres_data:/var/lib/postgresql/data/ - - ./init-vector-extension.sql:/docker-entrypoint-initdb.d/init-vector-extension.sql - environment: - - POSTGRES_USER=balancer - - POSTGRES_PASSWORD=balancer - - POSTGRES_DB=balancer_dev - ports: + build: + context: ./db + dockerfile: Dockerfile + volumes: + - postgres_data:/var/lib/postgresql/data/ + environment: + - POSTGRES_USER=balancer + - POSTGRES_PASSWORD=balancer + - POSTGRES_DB=balancer_dev + ports: - "5433:5432" - networks: - app_net: - ipv4_address: 192.168.0.2 + networks: + app_net: + ipv4_address: 192.168.0.2 pgadmin: container_name: pgadmin4 image: dpage/pgadmin4 @@ -51,13 +52,13 @@ services: args: - IMAGE_NAME=balancer-frontend ports: - - "3000:3000" + - "3000:3000" environment: - - CHOKIDAR_USEPOLLING=true - # - VITE_API_BASE_URL=https://balancertestsite.com/ + - CHOKIDAR_USEPOLLING=true + # - VITE_API_BASE_URL=https://balancertestsite.com/ volumes: - - "./frontend:/usr/src/app:delegated" - - "/usr/src/app/node_modules/" + - "./frontend:/usr/src/app:delegated" + - "/usr/src/app/node_modules/" depends_on: - backend networks: @@ -71,4 +72,4 @@ networks: driver: default config: - subnet: "192.168.0.0/24" - gateway: 192.168.0.1 + gateway: 192.168.0.1 \ No newline at end of file diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index 175ca6ab..91a53a8b 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -29,56 +29,57 @@ # Fetching the value from the environment and splitting to list if necessary. # Fallback to '*' if the environment variable is not set. -ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split() +ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split() # If the environment variable contains '*', the split method would create a list with an empty string. # So you need to check for this case and adjust accordingly. -if ALLOWED_HOSTS == ["*"] or ALLOWED_HOSTS == [""]: - ALLOWED_HOSTS = ["*"] +if ALLOWED_HOSTS == ['*'] or ALLOWED_HOSTS == ['']: + ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "balancer_backend", - "api", - "corsheaders", - "rest_framework", - "djoser", + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'balancer_backend', + 'api', + 'corsheaders', + 'rest_framework', + 'djoser', ] MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "corsheaders.middleware.CorsMiddleware", + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'corsheaders.middleware.CorsMiddleware', + ] -ROOT_URLCONF = "balancer_backend.urls" +ROOT_URLCONF = 'balancer_backend.urls' CORS_ALLOW_ALL_ORIGINS = True TEMPLATES = [ { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "build")], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'build')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', ], }, }, @@ -88,7 +89,7 @@ # Change this to your desired URL LOGIN_REDIRECT_URL = os.environ.get("LOGIN_REDIRECT_URL") -WSGI_APPLICATION = "balancer_backend.wsgi.application" +WSGI_APPLICATION = 'balancer_backend.wsgi.application' # Database @@ -105,8 +106,8 @@ } } -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -EMAIL_HOST = "smtp.gmail.com" +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' EMAIL_PORT = 587 EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") @@ -118,25 +119,25 @@ AUTH_PASSWORD_VALIDATORS = [ { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = "en-us" +LANGUAGE_CODE = 'en-us' -TIME_ZONE = "UTC" +TIME_ZONE = 'UTC' USE_I18N = True @@ -146,89 +147,64 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = "/static/" +STATIC_URL = '/static/' STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "build/static"), + os.path.join(BASE_DIR, 'build/static'), ] -STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATIC_ROOT = os.path.join(BASE_DIR, 'static') AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", + 'django.contrib.auth.backends.ModelBackend', ] REST_FRAMEWORK = { - "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], - "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework_simplejwt.authentication.JWTAuthentication", + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated' + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', ), } SIMPLE_JWT = { - "AUTH_HEADER_TYPES": ("JWT",), - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), - "REFRESH_TOKEN_LIFETIME": timedelta(days=1), - "TOKEN_OBTAIN_SERIALIZER": "api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer", - "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), + 'AUTH_HEADER_TYPES': ('JWT',), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'TOKEN_OBTAIN_SERIALIZER': 'api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer', + 'AUTH_TOKEN_CLASSES': ( + 'rest_framework_simplejwt.tokens.AccessToken', + ) } DJOSER = { - "LOGIN_FIELD": "email", - "USER_CREATE_PASSWORD_RETYPE": True, - "USERNAME_CHANGED_EMAIL_CONFIRMATION": True, - "PASSWORD_CHANGED_EMAIL_CONFIRMATION": True, - "SEND_CONFIRMATION_EMAIL": True, - "SET_USERNAME_RETYPE": True, - "SET_PASSWORD_RETYPE": True, - "PASSWORD_RESET_CONFIRM_URL": "password/reset/confirm/{uid}/{token}", - "USERNAME_RESET_CONFIRM_URL": "email/reset/confirm/{uid}/{token}", - "ACTIVATION_URL": "activate/{uid}/{token}", - "SEND_ACTIVATION_EMAIL": True, - "SOCIAL_AUTH_TOKEN_STRATEGY": "djoser.social.token.jwt.TokenStrategy", - "SOCIAL_AUTH_ALLOWED_REDIRECT_URIS": [ - "http://localhost:8000/google", - "http://localhost:8000/facebook", - ], - "SERIALIZERS": { - "user_create": "api.models.serializers.UserCreateSerializer", - "user": "api.models.serializers.UserCreateSerializer", - "current_user": "api.models.serializers.UserCreateSerializer", - "user_delete": "djoser.serializers.UserDeleteSerializer", - }, + 'LOGIN_FIELD': 'email', + 'USER_CREATE_PASSWORD_RETYPE': True, + 'USERNAME_CHANGED_EMAIL_CONFIRMATION': True, + 'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True, + 'SEND_CONFIRMATION_EMAIL': True, + 'SET_USERNAME_RETYPE': True, + 'SET_PASSWORD_RETYPE': True, + 'PASSWORD_RESET_CONFIRM_URL': 'password/reset/confirm/{uid}/{token}', + 'USERNAME_RESET_CONFIRM_URL': 'email/reset/confirm/{uid}/{token}', + 'ACTIVATION_URL': 'activate/{uid}/{token}', + 'SEND_ACTIVATION_EMAIL': True, + 'SOCIAL_AUTH_TOKEN_STRATEGY': 'djoser.social.token.jwt.TokenStrategy', + 'SOCIAL_AUTH_ALLOWED_REDIRECT_URIS': ['http://localhost:8000/google', 'http://localhost:8000/facebook'], + 'SERIALIZERS': { + 'user_create': 'api.models.serializers.UserCreateSerializer', + 'user': 'api.models.serializers.UserCreateSerializer', + 'current_user': 'api.models.serializers.UserCreateSerializer', + 'user_delete': 'djoser.serializers.UserDeleteSerializer', + } } # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -AUTH_USER_MODEL = "api.UserAccount" -# Logging configuration -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", - "style": "{", - }, - "simple": { - "format": "{levelname} {message}", - "style": "{", - }, - }, - "handlers": { - "console": { - "class": "logging.StreamHandler", - "formatter": "verbose", - }, - }, - "root": { - "handlers": ["console"], - "level": "INFO", - }, -} +AUTH_USER_MODEL = 'api.UserAccount' From 5c3185f7ef37b7ec232d838c403c92c0e06731f0 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Thu, 21 Aug 2025 15:26:10 -0400 Subject: [PATCH 15/22] Revert "Restore files that had changes for local dev" This reverts commit 261634ceb4b07fcdc2a89928985478a455138884. --- docker-compose.yml | 37 +++--- server/balancer_backend/settings.py | 182 ++++++++++++++++------------ 2 files changed, 121 insertions(+), 98 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d8a8ca75..f36517ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,18 @@ services: db: - build: - context: ./db - dockerfile: Dockerfile - volumes: - - postgres_data:/var/lib/postgresql/data/ - environment: - - POSTGRES_USER=balancer - - POSTGRES_PASSWORD=balancer - - POSTGRES_DB=balancer_dev - ports: + image: pgvector/pgvector:pg15 + volumes: + - postgres_data:/var/lib/postgresql/data/ + - ./init-vector-extension.sql:/docker-entrypoint-initdb.d/init-vector-extension.sql + environment: + - POSTGRES_USER=balancer + - POSTGRES_PASSWORD=balancer + - POSTGRES_DB=balancer_dev + ports: - "5433:5432" - networks: - app_net: - ipv4_address: 192.168.0.2 + networks: + app_net: + ipv4_address: 192.168.0.2 pgadmin: container_name: pgadmin4 image: dpage/pgadmin4 @@ -52,13 +51,13 @@ services: args: - IMAGE_NAME=balancer-frontend ports: - - "3000:3000" + - "3000:3000" environment: - - CHOKIDAR_USEPOLLING=true - # - VITE_API_BASE_URL=https://balancertestsite.com/ + - CHOKIDAR_USEPOLLING=true + # - VITE_API_BASE_URL=https://balancertestsite.com/ volumes: - - "./frontend:/usr/src/app:delegated" - - "/usr/src/app/node_modules/" + - "./frontend:/usr/src/app:delegated" + - "/usr/src/app/node_modules/" depends_on: - backend networks: @@ -72,4 +71,4 @@ networks: driver: default config: - subnet: "192.168.0.0/24" - gateway: 192.168.0.1 \ No newline at end of file + gateway: 192.168.0.1 diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index 91a53a8b..175ca6ab 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -29,57 +29,56 @@ # Fetching the value from the environment and splitting to list if necessary. # Fallback to '*' if the environment variable is not set. -ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split() +ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split() # If the environment variable contains '*', the split method would create a list with an empty string. # So you need to check for this case and adjust accordingly. -if ALLOWED_HOSTS == ['*'] or ALLOWED_HOSTS == ['']: - ALLOWED_HOSTS = ['*'] +if ALLOWED_HOSTS == ["*"] or ALLOWED_HOSTS == [""]: + ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'balancer_backend', - 'api', - 'corsheaders', - 'rest_framework', - 'djoser', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "balancer_backend", + "api", + "corsheaders", + "rest_framework", + "djoser", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', - + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "corsheaders.middleware.CorsMiddleware", ] -ROOT_URLCONF = 'balancer_backend.urls' +ROOT_URLCONF = "balancer_backend.urls" CORS_ALLOW_ALL_ORIGINS = True TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'build')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "build")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, @@ -89,7 +88,7 @@ # Change this to your desired URL LOGIN_REDIRECT_URL = os.environ.get("LOGIN_REDIRECT_URL") -WSGI_APPLICATION = 'balancer_backend.wsgi.application' +WSGI_APPLICATION = "balancer_backend.wsgi.application" # Database @@ -106,8 +105,8 @@ } } -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'smtp.gmail.com' +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "smtp.gmail.com" EMAIL_PORT = 587 EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") @@ -119,25 +118,25 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -147,64 +146,89 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'build/static'), + os.path.join(BASE_DIR, "build/static"), ] -STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATIC_ROOT = os.path.join(BASE_DIR, "static") AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', + "django.contrib.auth.backends.ModelBackend", ] REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated' - ], - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", ), } SIMPLE_JWT = { - 'AUTH_HEADER_TYPES': ('JWT',), - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), - 'TOKEN_OBTAIN_SERIALIZER': 'api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer', - 'AUTH_TOKEN_CLASSES': ( - 'rest_framework_simplejwt.tokens.AccessToken', - ) + "AUTH_HEADER_TYPES": ("JWT",), + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "TOKEN_OBTAIN_SERIALIZER": "api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer", + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), } DJOSER = { - 'LOGIN_FIELD': 'email', - 'USER_CREATE_PASSWORD_RETYPE': True, - 'USERNAME_CHANGED_EMAIL_CONFIRMATION': True, - 'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True, - 'SEND_CONFIRMATION_EMAIL': True, - 'SET_USERNAME_RETYPE': True, - 'SET_PASSWORD_RETYPE': True, - 'PASSWORD_RESET_CONFIRM_URL': 'password/reset/confirm/{uid}/{token}', - 'USERNAME_RESET_CONFIRM_URL': 'email/reset/confirm/{uid}/{token}', - 'ACTIVATION_URL': 'activate/{uid}/{token}', - 'SEND_ACTIVATION_EMAIL': True, - 'SOCIAL_AUTH_TOKEN_STRATEGY': 'djoser.social.token.jwt.TokenStrategy', - 'SOCIAL_AUTH_ALLOWED_REDIRECT_URIS': ['http://localhost:8000/google', 'http://localhost:8000/facebook'], - 'SERIALIZERS': { - 'user_create': 'api.models.serializers.UserCreateSerializer', - 'user': 'api.models.serializers.UserCreateSerializer', - 'current_user': 'api.models.serializers.UserCreateSerializer', - 'user_delete': 'djoser.serializers.UserDeleteSerializer', - } + "LOGIN_FIELD": "email", + "USER_CREATE_PASSWORD_RETYPE": True, + "USERNAME_CHANGED_EMAIL_CONFIRMATION": True, + "PASSWORD_CHANGED_EMAIL_CONFIRMATION": True, + "SEND_CONFIRMATION_EMAIL": True, + "SET_USERNAME_RETYPE": True, + "SET_PASSWORD_RETYPE": True, + "PASSWORD_RESET_CONFIRM_URL": "password/reset/confirm/{uid}/{token}", + "USERNAME_RESET_CONFIRM_URL": "email/reset/confirm/{uid}/{token}", + "ACTIVATION_URL": "activate/{uid}/{token}", + "SEND_ACTIVATION_EMAIL": True, + "SOCIAL_AUTH_TOKEN_STRATEGY": "djoser.social.token.jwt.TokenStrategy", + "SOCIAL_AUTH_ALLOWED_REDIRECT_URIS": [ + "http://localhost:8000/google", + "http://localhost:8000/facebook", + ], + "SERIALIZERS": { + "user_create": "api.models.serializers.UserCreateSerializer", + "user": "api.models.serializers.UserCreateSerializer", + "current_user": "api.models.serializers.UserCreateSerializer", + "user_delete": "djoser.serializers.UserDeleteSerializer", + }, } # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +AUTH_USER_MODEL = "api.UserAccount" -AUTH_USER_MODEL = 'api.UserAccount' +# Logging configuration +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "root": { + "handlers": ["console"], + "level": "INFO", + }, +} From 9e8040d3f122e0a34805b786345b64c306c1ddec Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Mon, 25 Aug 2025 11:40:02 -0400 Subject: [PATCH 16/22] Add HIPPA Compliance and PHI Disclaimer --- frontend/src/components/Header/Chat.tsx | 61 +++++++++++++++++-------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/Header/Chat.tsx b/frontend/src/components/Header/Chat.tsx index 95189a10..6bad2e16 100644 --- a/frontend/src/components/Header/Chat.tsx +++ b/frontend/src/components/Header/Chat.tsx @@ -31,7 +31,6 @@ export interface Conversation { id: string; } - interface ChatDropDownProps { showChat: boolean; setShowChat: React.Dispatch>; @@ -41,7 +40,9 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { const CHATBOT_NAME = "JJ"; const [inputValue, setInputValue] = useState(""); const [currentMessages, setCurrentMessages] = useState([]); - const [currentResponseId, setCurrentResponseId] = useState(undefined); + const [currentResponseId, setCurrentResponseId] = useState< + string | undefined + >(undefined); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -55,7 +56,6 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { ]; const chatContainerRef = useRef(null); - const [bottom, setBottom] = useState(false); const handleScroll = (event: React.UIEvent) => { @@ -76,18 +76,17 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { chatContainer.scrollTop = chatContainer.scrollHeight; setBottom( chatContainer.scrollHeight - chatContainer.scrollTop === - chatContainer.clientHeight + chatContainer.clientHeight, ); }, 0); } }, [currentMessages]); - const scrollToBottom = (element: HTMLElement) => element.scroll({ top: element.scrollHeight, behavior: "smooth" }); const handleScrollDown = ( - event: React.MouseEvent + event: React.MouseEvent, ) => { event.preventDefault(); const element = document.getElementById("inside_chat"); @@ -101,7 +100,7 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { event: | React.FormEvent | React.MouseEvent, - suggestion?: string + suggestion?: string, ) => { event.preventDefault(); @@ -123,7 +122,10 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { setCurrentMessages(updatedMessages); // Call assistant API with previous response ID for continuity - const data = await sendAssistantMessage(messageContent, currentResponseId); + const data = await sendAssistantMessage( + messageContent, + currentResponseId, + ); // Create assistant response message const assistantMessage = { @@ -133,9 +135,8 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { }; // Update messages and store new response ID for next message - setCurrentMessages(prev => [...prev, assistantMessage]); + setCurrentMessages((prev) => [...prev, assistantMessage]); setCurrentResponseId(data.final_response_id); - } catch (error) { console.error("Error handling message:", error); let errorMessage = "Error submitting message"; @@ -212,7 +213,7 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { > - + - +
+
+
+ ⚠️ IMPORTANT NOTICE +
+

+ Balancer is NOT configured for use with Protected Health Information (PHI) as defined under HIPAA. + You must NOT enter any patient-identifiable information including names, addresses, dates of birth, + medical record numbers, or any other identifying information. Your queries may be processed by + third-party AI services that retain data for up to 30 days for abuse monitoring. By using Balancer, + you certify that you understand these restrictions and will not enter any PHI. +

+
@@ -305,7 +328,7 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { .sort( (a, b) => new Date(a.timestamp).getTime() - - new Date(b.timestamp).getTime() + new Date(b.timestamp).getTime(), ) .map((message, index) => (
= ({ showChat, setShowChat }) => { )) )} {isLoading && ( -
+
@@ -355,10 +381,7 @@ const Chat: React.FC = ({ showChat, setShowChat }) => {
) : ( -
setShowChat(true)} - className="chat_button no-print" - > +
setShowChat(true)} className="chat_button no-print">
)} From 1d5a1f72ffb446b9bc130aa0433d5c515145e8b1 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Mon, 25 Aug 2025 14:42:06 -0400 Subject: [PATCH 17/22] Add conversation management to the Chat component using session storage --- frontend/src/components/Header/Chat.tsx | 52 ++++++++++++++++++++++++- frontend/src/services/actions/auth.tsx | 3 ++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Header/Chat.tsx b/frontend/src/components/Header/Chat.tsx index 6bad2e16..0edee74a 100644 --- a/frontend/src/components/Header/Chat.tsx +++ b/frontend/src/components/Header/Chat.tsx @@ -18,7 +18,7 @@ import { } from "react-icons/fa"; import { sendAssistantMessage } from "../../api/apiClient"; -interface ChatLogItem { +export interface ChatLogItem { is_user: boolean; content: string; timestamp: string; // EX: 2025-01-16T16:21:14.981090Z @@ -46,6 +46,28 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + // Session storage functions for conversation management + const saveConversationToStorage = (messages: ChatLogItem[], responseId?: string) => { + const conversationData = { + messages, + responseId, + timestamp: new Date().toISOString(), + }; + sessionStorage.setItem('currentConversation', JSON.stringify(conversationData)); + }; + + const loadConversationFromStorage = () => { + const stored = sessionStorage.getItem('currentConversation'); + if (stored) { + try { + return JSON.parse(stored); + } catch (error) { + console.error('Error parsing stored conversation:', error); + } + } + return null; + }; + const suggestionPrompts = [ "What are the side effects of Latuda?", "Why is cariprazine better than valproate for a pregnant patient?", @@ -58,6 +80,24 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { const [bottom, setBottom] = useState(false); + // Load conversation from sessionStorage on component mount + useEffect(() => { + const storedConversation = loadConversationFromStorage(); + if (storedConversation) { + setCurrentMessages(storedConversation.messages || []); + setCurrentResponseId(storedConversation.responseId); + } + }, []); + + // Save conversation to sessionStorage when component unmounts + useEffect(() => { + return () => { + if (currentMessages.length > 0) { + saveConversationToStorage(currentMessages, currentResponseId); + } + }; + }, [currentMessages, currentResponseId]); + const handleScroll = (event: React.UIEvent) => { const target = event.target as HTMLElement; const bottom = @@ -120,6 +160,9 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { // Add user message to current conversation const updatedMessages = [...currentMessages, newMessage]; setCurrentMessages(updatedMessages); + + // Save user message immediately to prevent loss + saveConversationToStorage(updatedMessages, currentResponseId); // Call assistant API with previous response ID for continuity const data = await sendAssistantMessage( @@ -135,8 +178,12 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { }; // Update messages and store new response ID for next message - setCurrentMessages((prev) => [...prev, assistantMessage]); + const finalMessages = [...updatedMessages, assistantMessage]; + setCurrentMessages(finalMessages); setCurrentResponseId(data.final_response_id); + + // Save conversation to sessionStorage + saveConversationToStorage(finalMessages, data.final_response_id); } catch (error) { console.error("Error handling message:", error); let errorMessage = "Error submitting message"; @@ -207,6 +254,7 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { onClick={() => { setCurrentMessages([]); setCurrentResponseId(undefined); + sessionStorage.removeItem('currentConversation'); }} className="flex items-center justify-center" title="New Conversation" diff --git a/frontend/src/services/actions/auth.tsx b/frontend/src/services/actions/auth.tsx index 3a29bc38..2573c223 100644 --- a/frontend/src/services/actions/auth.tsx +++ b/frontend/src/services/actions/auth.tsx @@ -169,6 +169,9 @@ export const login = }; export const logout = () => async (dispatch: AppDispatch) => { + // Clear chat conversation data on logout for security + sessionStorage.removeItem('currentConversation'); + dispatch({ type: LOGOUT, }); From 6f1161c688e3c8329e23e4868543d131c1eab64d Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Mon, 25 Aug 2025 16:35:41 -0400 Subject: [PATCH 18/22] =?UTF-8?q?Prevent=20the=20=E2=80=9Cre-save=20after?= =?UTF-8?q?=20logout=E2=80=9D=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Header/Chat.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Header/Chat.tsx b/frontend/src/components/Header/Chat.tsx index 0edee74a..2f4f92ab 100644 --- a/frontend/src/components/Header/Chat.tsx +++ b/frontend/src/components/Header/Chat.tsx @@ -89,10 +89,13 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { } }, []); + // Save conversation to sessionStorage when component unmounts useEffect(() => { return () => { - if (currentMessages.length > 0) { + // Only save if the user hasn't logged out + const isLoggingOut = !localStorage.getItem("access"); + if (!isLoggingOut && currentMessages.length > 0) { saveConversationToStorage(currentMessages, currentResponseId); } }; From fa35396ac93ced9104e049de528479d0a8adfcf4 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Tue, 26 Aug 2025 14:17:04 -0400 Subject: [PATCH 19/22] Remove line from the disclosure --- frontend/src/components/Header/Chat.tsx | 70 ++++++++++++++++--------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/Header/Chat.tsx b/frontend/src/components/Header/Chat.tsx index 2f4f92ab..c6315068 100644 --- a/frontend/src/components/Header/Chat.tsx +++ b/frontend/src/components/Header/Chat.tsx @@ -47,22 +47,28 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { const [error, setError] = useState(null); // Session storage functions for conversation management - const saveConversationToStorage = (messages: ChatLogItem[], responseId?: string) => { + const saveConversationToStorage = ( + messages: ChatLogItem[], + responseId?: string, + ) => { const conversationData = { messages, responseId, timestamp: new Date().toISOString(), }; - sessionStorage.setItem('currentConversation', JSON.stringify(conversationData)); + sessionStorage.setItem( + "currentConversation", + JSON.stringify(conversationData), + ); }; const loadConversationFromStorage = () => { - const stored = sessionStorage.getItem('currentConversation'); + const stored = sessionStorage.getItem("currentConversation"); if (stored) { try { return JSON.parse(stored); } catch (error) { - console.error('Error parsing stored conversation:', error); + console.error("Error parsing stored conversation:", error); } } return null; @@ -89,12 +95,11 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { } }, []); - // Save conversation to sessionStorage when component unmounts useEffect(() => { return () => { // Only save if the user hasn't logged out - const isLoggingOut = !localStorage.getItem("access"); + const isLoggingOut = !localStorage.getItem("access"); if (!isLoggingOut && currentMessages.length > 0) { saveConversationToStorage(currentMessages, currentResponseId); } @@ -163,7 +168,7 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { // Add user message to current conversation const updatedMessages = [...currentMessages, newMessage]; setCurrentMessages(updatedMessages); - + // Save user message immediately to prevent loss saveConversationToStorage(updatedMessages, currentResponseId); @@ -184,7 +189,7 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { const finalMessages = [...updatedMessages, assistantMessage]; setCurrentMessages(finalMessages); setCurrentResponseId(data.final_response_id); - + // Save conversation to sessionStorage saveConversationToStorage(finalMessages, data.final_response_id); } catch (error) { @@ -257,7 +262,7 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { onClick={() => { setCurrentMessages([]); setCurrentResponseId(undefined); - sessionStorage.removeItem('currentConversation'); + sessionStorage.removeItem("currentConversation"); }} className="flex items-center justify-center" title="New Conversation" @@ -313,25 +318,38 @@ const Chat: React.FC = ({ showChat, setShowChat }) => { Learn more about my sources.
-
-
+
+
⚠️ IMPORTANT NOTICE
-

- Balancer is NOT configured for use with Protected Health Information (PHI) as defined under HIPAA. - You must NOT enter any patient-identifiable information including names, addresses, dates of birth, - medical record numbers, or any other identifying information. Your queries may be processed by - third-party AI services that retain data for up to 30 days for abuse monitoring. By using Balancer, - you certify that you understand these restrictions and will not enter any PHI. +

+ Balancer is NOT configured for use with Protected Health + Information (PHI) as defined under HIPAA. You must NOT + enter any patient-identifiable information including + names, addresses, dates of birth, medical record + numbers, or any other identifying information. By using + Balancer, you certify that you understand these + restrictions and will not enter any PHI.

From c2da354b65fe93b6694ab568bf940a16a4033646 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Tue, 2 Sep 2025 19:39:10 -0400 Subject: [PATCH 20/22] Restore docker-compose.yml --- docker-compose.yml | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f36517ef..d8a8ca75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,19 @@ services: db: - image: pgvector/pgvector:pg15 - volumes: - - postgres_data:/var/lib/postgresql/data/ - - ./init-vector-extension.sql:/docker-entrypoint-initdb.d/init-vector-extension.sql - environment: - - POSTGRES_USER=balancer - - POSTGRES_PASSWORD=balancer - - POSTGRES_DB=balancer_dev - ports: + build: + context: ./db + dockerfile: Dockerfile + volumes: + - postgres_data:/var/lib/postgresql/data/ + environment: + - POSTGRES_USER=balancer + - POSTGRES_PASSWORD=balancer + - POSTGRES_DB=balancer_dev + ports: - "5433:5432" - networks: - app_net: - ipv4_address: 192.168.0.2 + networks: + app_net: + ipv4_address: 192.168.0.2 pgadmin: container_name: pgadmin4 image: dpage/pgadmin4 @@ -51,13 +52,13 @@ services: args: - IMAGE_NAME=balancer-frontend ports: - - "3000:3000" + - "3000:3000" environment: - - CHOKIDAR_USEPOLLING=true - # - VITE_API_BASE_URL=https://balancertestsite.com/ + - CHOKIDAR_USEPOLLING=true + # - VITE_API_BASE_URL=https://balancertestsite.com/ volumes: - - "./frontend:/usr/src/app:delegated" - - "/usr/src/app/node_modules/" + - "./frontend:/usr/src/app:delegated" + - "/usr/src/app/node_modules/" depends_on: - backend networks: @@ -71,4 +72,4 @@ networks: driver: default config: - subnet: "192.168.0.0/24" - gateway: 192.168.0.1 + gateway: 192.168.0.1 \ No newline at end of file From 839a9245009467d29ba3283e63282128692bb9ae Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Tue, 2 Sep 2025 19:44:47 -0400 Subject: [PATCH 21/22] Logging configuration --- server/balancer_backend/settings.py | 204 ++++++++++++++-------------- 1 file changed, 103 insertions(+), 101 deletions(-) diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index 175ca6ab..9c73577b 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -29,56 +29,57 @@ # Fetching the value from the environment and splitting to list if necessary. # Fallback to '*' if the environment variable is not set. -ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split() +ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split() # If the environment variable contains '*', the split method would create a list with an empty string. # So you need to check for this case and adjust accordingly. -if ALLOWED_HOSTS == ["*"] or ALLOWED_HOSTS == [""]: - ALLOWED_HOSTS = ["*"] +if ALLOWED_HOSTS == ['*'] or ALLOWED_HOSTS == ['']: + ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "balancer_backend", - "api", - "corsheaders", - "rest_framework", - "djoser", + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'balancer_backend', + 'api', + 'corsheaders', + 'rest_framework', + 'djoser', ] MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "corsheaders.middleware.CorsMiddleware", + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'corsheaders.middleware.CorsMiddleware', + ] -ROOT_URLCONF = "balancer_backend.urls" +ROOT_URLCONF = 'balancer_backend.urls' CORS_ALLOW_ALL_ORIGINS = True TEMPLATES = [ { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "build")], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'build')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', ], }, }, @@ -88,7 +89,7 @@ # Change this to your desired URL LOGIN_REDIRECT_URL = os.environ.get("LOGIN_REDIRECT_URL") -WSGI_APPLICATION = "balancer_backend.wsgi.application" +WSGI_APPLICATION = 'balancer_backend.wsgi.application' # Database @@ -105,8 +106,8 @@ } } -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -EMAIL_HOST = "smtp.gmail.com" +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' EMAIL_PORT = 587 EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") @@ -118,25 +119,25 @@ AUTH_PASSWORD_VALIDATORS = [ { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = "en-us" +LANGUAGE_CODE = 'en-us' -TIME_ZONE = "UTC" +TIME_ZONE = 'UTC' USE_I18N = True @@ -146,89 +147,90 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = "/static/" +STATIC_URL = '/static/' STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "build/static"), + os.path.join(BASE_DIR, 'build/static'), ] -STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATIC_ROOT = os.path.join(BASE_DIR, 'static') AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", + 'django.contrib.auth.backends.ModelBackend', ] REST_FRAMEWORK = { - "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], - "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework_simplejwt.authentication.JWTAuthentication", + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated' + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', ), } SIMPLE_JWT = { - "AUTH_HEADER_TYPES": ("JWT",), - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), - "REFRESH_TOKEN_LIFETIME": timedelta(days=1), - "TOKEN_OBTAIN_SERIALIZER": "api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer", - "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), + 'AUTH_HEADER_TYPES': ('JWT',), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'TOKEN_OBTAIN_SERIALIZER': 'api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer', + 'AUTH_TOKEN_CLASSES': ( + 'rest_framework_simplejwt.tokens.AccessToken', + ) } DJOSER = { - "LOGIN_FIELD": "email", - "USER_CREATE_PASSWORD_RETYPE": True, - "USERNAME_CHANGED_EMAIL_CONFIRMATION": True, - "PASSWORD_CHANGED_EMAIL_CONFIRMATION": True, - "SEND_CONFIRMATION_EMAIL": True, - "SET_USERNAME_RETYPE": True, - "SET_PASSWORD_RETYPE": True, - "PASSWORD_RESET_CONFIRM_URL": "password/reset/confirm/{uid}/{token}", - "USERNAME_RESET_CONFIRM_URL": "email/reset/confirm/{uid}/{token}", - "ACTIVATION_URL": "activate/{uid}/{token}", - "SEND_ACTIVATION_EMAIL": True, - "SOCIAL_AUTH_TOKEN_STRATEGY": "djoser.social.token.jwt.TokenStrategy", - "SOCIAL_AUTH_ALLOWED_REDIRECT_URIS": [ - "http://localhost:8000/google", - "http://localhost:8000/facebook", - ], - "SERIALIZERS": { - "user_create": "api.models.serializers.UserCreateSerializer", - "user": "api.models.serializers.UserCreateSerializer", - "current_user": "api.models.serializers.UserCreateSerializer", - "user_delete": "djoser.serializers.UserDeleteSerializer", - }, + 'LOGIN_FIELD': 'email', + 'USER_CREATE_PASSWORD_RETYPE': True, + 'USERNAME_CHANGED_EMAIL_CONFIRMATION': True, + 'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True, + 'SEND_CONFIRMATION_EMAIL': True, + 'SET_USERNAME_RETYPE': True, + 'SET_PASSWORD_RETYPE': True, + 'PASSWORD_RESET_CONFIRM_URL': 'password/reset/confirm/{uid}/{token}', + 'USERNAME_RESET_CONFIRM_URL': 'email/reset/confirm/{uid}/{token}', + 'ACTIVATION_URL': 'activate/{uid}/{token}', + 'SEND_ACTIVATION_EMAIL': True, + 'SOCIAL_AUTH_TOKEN_STRATEGY': 'djoser.social.token.jwt.TokenStrategy', + 'SOCIAL_AUTH_ALLOWED_REDIRECT_URIS': ['http://localhost:8000/google', 'http://localhost:8000/facebook'], + 'SERIALIZERS': { + 'user_create': 'api.models.serializers.UserCreateSerializer', + 'user': 'api.models.serializers.UserCreateSerializer', + 'current_user': 'api.models.serializers.UserCreateSerializer', + 'user_delete': 'djoser.serializers.UserDeleteSerializer', + } } # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -AUTH_USER_MODEL = "api.UserAccount" +AUTH_USER_MODEL = 'api.UserAccount' # Logging configuration -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", - "style": "{", - }, - "simple": { - "format": "{levelname} {message}", - "style": "{", - }, - }, - "handlers": { - "console": { - "class": "logging.StreamHandler", - "formatter": "verbose", - }, - }, - "root": { - "handlers": ["console"], - "level": "INFO", - }, -} +# LOGGING = { +# "version": 1, +# "disable_existing_loggers": False, +# "formatters": { +# "verbose": { +# "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", +# "style": "{", +# }, +# "simple": { +# "format": "{levelname} {message}", +# "style": "{", +# }, +# }, +# "handlers": { +# "console": { +# "class": "logging.StreamHandler", +# "formatter": "verbose", +# }, +# }, +# "root": { +# "handlers": ["console"], +# "level": "INFO", +# }, +# } From 871706fa7264ac454a029b93dc1a3f80feaa0d50 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Tue, 2 Sep 2025 19:47:03 -0400 Subject: [PATCH 22/22] Linting and formating --- server/balancer_backend/settings.py | 157 ++++++++++++++-------------- 1 file changed, 78 insertions(+), 79 deletions(-) diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index 9c73577b..df62d198 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -29,57 +29,56 @@ # Fetching the value from the environment and splitting to list if necessary. # Fallback to '*' if the environment variable is not set. -ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split() +ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split() # If the environment variable contains '*', the split method would create a list with an empty string. # So you need to check for this case and adjust accordingly. -if ALLOWED_HOSTS == ['*'] or ALLOWED_HOSTS == ['']: - ALLOWED_HOSTS = ['*'] +if ALLOWED_HOSTS == ["*"] or ALLOWED_HOSTS == [""]: + ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'balancer_backend', - 'api', - 'corsheaders', - 'rest_framework', - 'djoser', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "balancer_backend", + "api", + "corsheaders", + "rest_framework", + "djoser", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', - + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "corsheaders.middleware.CorsMiddleware", ] -ROOT_URLCONF = 'balancer_backend.urls' +ROOT_URLCONF = "balancer_backend.urls" CORS_ALLOW_ALL_ORIGINS = True TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'build')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "build")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, @@ -89,7 +88,7 @@ # Change this to your desired URL LOGIN_REDIRECT_URL = os.environ.get("LOGIN_REDIRECT_URL") -WSGI_APPLICATION = 'balancer_backend.wsgi.application' +WSGI_APPLICATION = "balancer_backend.wsgi.application" # Database @@ -106,8 +105,8 @@ } } -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'smtp.gmail.com' +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "smtp.gmail.com" EMAIL_PORT = 587 EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") @@ -119,25 +118,25 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -147,69 +146,69 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'build/static'), + os.path.join(BASE_DIR, "build/static"), ] -STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATIC_ROOT = os.path.join(BASE_DIR, "static") AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', + "django.contrib.auth.backends.ModelBackend", ] REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated' - ], - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", ), } SIMPLE_JWT = { - 'AUTH_HEADER_TYPES': ('JWT',), - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), - 'TOKEN_OBTAIN_SERIALIZER': 'api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer', - 'AUTH_TOKEN_CLASSES': ( - 'rest_framework_simplejwt.tokens.AccessToken', - ) + "AUTH_HEADER_TYPES": ("JWT",), + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "TOKEN_OBTAIN_SERIALIZER": "api.models.TokenObtainPairSerializer.MyTokenObtainPairSerializer", + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), } DJOSER = { - 'LOGIN_FIELD': 'email', - 'USER_CREATE_PASSWORD_RETYPE': True, - 'USERNAME_CHANGED_EMAIL_CONFIRMATION': True, - 'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True, - 'SEND_CONFIRMATION_EMAIL': True, - 'SET_USERNAME_RETYPE': True, - 'SET_PASSWORD_RETYPE': True, - 'PASSWORD_RESET_CONFIRM_URL': 'password/reset/confirm/{uid}/{token}', - 'USERNAME_RESET_CONFIRM_URL': 'email/reset/confirm/{uid}/{token}', - 'ACTIVATION_URL': 'activate/{uid}/{token}', - 'SEND_ACTIVATION_EMAIL': True, - 'SOCIAL_AUTH_TOKEN_STRATEGY': 'djoser.social.token.jwt.TokenStrategy', - 'SOCIAL_AUTH_ALLOWED_REDIRECT_URIS': ['http://localhost:8000/google', 'http://localhost:8000/facebook'], - 'SERIALIZERS': { - 'user_create': 'api.models.serializers.UserCreateSerializer', - 'user': 'api.models.serializers.UserCreateSerializer', - 'current_user': 'api.models.serializers.UserCreateSerializer', - 'user_delete': 'djoser.serializers.UserDeleteSerializer', - } + "LOGIN_FIELD": "email", + "USER_CREATE_PASSWORD_RETYPE": True, + "USERNAME_CHANGED_EMAIL_CONFIRMATION": True, + "PASSWORD_CHANGED_EMAIL_CONFIRMATION": True, + "SEND_CONFIRMATION_EMAIL": True, + "SET_USERNAME_RETYPE": True, + "SET_PASSWORD_RETYPE": True, + "PASSWORD_RESET_CONFIRM_URL": "password/reset/confirm/{uid}/{token}", + "USERNAME_RESET_CONFIRM_URL": "email/reset/confirm/{uid}/{token}", + "ACTIVATION_URL": "activate/{uid}/{token}", + "SEND_ACTIVATION_EMAIL": True, + "SOCIAL_AUTH_TOKEN_STRATEGY": "djoser.social.token.jwt.TokenStrategy", + "SOCIAL_AUTH_ALLOWED_REDIRECT_URIS": [ + "http://localhost:8000/google", + "http://localhost:8000/facebook", + ], + "SERIALIZERS": { + "user_create": "api.models.serializers.UserCreateSerializer", + "user": "api.models.serializers.UserCreateSerializer", + "current_user": "api.models.serializers.UserCreateSerializer", + "user_delete": "djoser.serializers.UserDeleteSerializer", + }, } # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -AUTH_USER_MODEL = 'api.UserAccount' +AUTH_USER_MODEL = "api.UserAccount" # Logging configuration + # LOGGING = { # "version": 1, # "disable_existing_loggers": False,