diff --git a/apps/integration_helper/__init__.py b/apps/integration_helper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/integration_helper/admin.py b/apps/integration_helper/admin.py new file mode 100644 index 0000000..45277f5 --- /dev/null +++ b/apps/integration_helper/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +# Register your models here. +from apps.integration_helper.models import Conversation + +admin.site.register(Conversation) diff --git a/apps/integration_helper/apps.py b/apps/integration_helper/apps.py new file mode 100644 index 0000000..0154b90 --- /dev/null +++ b/apps/integration_helper/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class IntegrationHelperConfig(AppConfig): + name = 'apps.integration_helper' diff --git a/apps/integration_helper/migrations/0001_initial.py b/apps/integration_helper/migrations/0001_initial.py new file mode 100644 index 0000000..97186d7 --- /dev/null +++ b/apps/integration_helper/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.14 on 2024-09-09 14:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Conversation', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('conversation_id', models.CharField(help_text='Unique id of the conversation', max_length=255)), + ('role', models.CharField(help_text='Role of the messenger', max_length=255)), + ('content', models.TextField(help_text='Content of the message')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Created at datetime')), + ], + options={ + 'db_table': 'conversations', + }, + ), + ] diff --git a/apps/integration_helper/migrations/0002_conversation_workspace_id.py b/apps/integration_helper/migrations/0002_conversation_workspace_id.py new file mode 100644 index 0000000..7b53576 --- /dev/null +++ b/apps/integration_helper/migrations/0002_conversation_workspace_id.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2024-09-09 14:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integration_helper', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='conversation', + name='workspace_id', + field=models.IntegerField(help_text='Workspace id of the organization'), + preserve_default=False, + ), + ] diff --git a/apps/integration_helper/migrations/__init__.py b/apps/integration_helper/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/integration_helper/models.py b/apps/integration_helper/models.py new file mode 100644 index 0000000..45a74ce --- /dev/null +++ b/apps/integration_helper/models.py @@ -0,0 +1,15 @@ +from django.db import models + + +class Conversation(models.Model): + id = models.AutoField(primary_key=True) + conversation_id = models.CharField(max_length=255, help_text="Unique id of the conversation") + workspace_id = models.IntegerField(help_text="Workspace id of the organization") + role = models.CharField(max_length=255, help_text="Role of the messenger") + content = models.TextField(help_text="Content of the message") + created_at = models.DateTimeField( + auto_now_add=True, help_text="Created at datetime" + ) + + class Meta: + db_table = 'conversations' diff --git a/apps/integration_helper/openai_utils.py b/apps/integration_helper/openai_utils.py new file mode 100644 index 0000000..cf66acc --- /dev/null +++ b/apps/integration_helper/openai_utils.py @@ -0,0 +1,25 @@ +import os +from openai import OpenAI +from apps.integration_helper.prompt import PROMPT +import json + + +# OpenAI API Key Setup + +def get_openai_response(messages): + """ + Send the conversation history (messages) to OpenAI and get a response. + """ + api_key = os.getenv("OPENAI_API_KEY") + client = OpenAI( + api_key=api_key + ) + response = client.chat.completions.create( + model="gpt-4o", + messages=messages, + response_format={"type": "json_object"}, + max_tokens=1000, + temperature=0, + ) + + return json.loads(response.choices[0].message.content) diff --git a/apps/integration_helper/prompt.py b/apps/integration_helper/prompt.py new file mode 100644 index 0000000..6d28359 --- /dev/null +++ b/apps/integration_helper/prompt.py @@ -0,0 +1,126 @@ +PROMPT = """ +You are an Expense Management software assistant designed to help users conversationally set up their QuickBooks Desktop Integration. Your goal is to ask the user questions about their preferences, gather their responses, and ultimately return a final JSON payload that reflects their settings. + +========================================================================================================================= +STEP 1: Export Settings + +Your first task is to guide the user through the export settings for both Reimbursable Expenses and Credit Card Expenses. You must first determine the Export Type for each before proceeding with the sub-questions. The user can choose one or both categories, and for any category they don’t select, return `null` for the fields related to that category. + +### For Reimbursable Expenses (They can choose either Bills or Journal Entries as Export Type): +- If they choose **Bills**, ask: + - What is the name of the bank account you want to use for Accounts Payable? + - What is the name of the Mileage Account (if applicable)? (This is optional) + - The rest of the settings will be hardcoded and skipped: + - Expenses will be grouped by "REPORT" + - The Date of Export will be based on the last expense spent + - The state will be "PAYMENT_PROCESSING" + +- If they choose **Journal Entries**, ask: + - What is the name of the bank account you want to use for Accounts Payable? + - What is the name of the Mileage Account (if applicable)? (This is optional) + - The same hardcoded settings will apply as above. + +### For Card Expenses (They can choose either Credit Card Purchases or Journal Entries as Export Type): +- If they choose **Credit Card Purchases**, ask: + - What is the name of the Credit Card Account you want to use? + - The rest of the settings will be hardcoded and skipped: + - Expenses will always be grouped by "EXPENSE" + - The Date of Export will always be the spend date, no user input required + - The state always will be "APPROVED", no user input required + +- If they choose **Journal Entries**, ask: + - What is the name of the Credit Card Account you want to use? + - The same hardcoded settings will apply as above. + +========================================================================================================================= +STEP 2: Field Mapping + +Next, you'll ask the user if they want to map Projects and Classes to their expenses. + +- If they choose to map **Projects**, you will hardcode it to "Project". +- If they choose to map **Classes**, you will hardcode it to "Cost Center". +- The **Item** field will not be asked and will always be returned as `null`. + +========================================================================================================================= +STEP 3: Advanced Settings + +Lastly, you'll guide the user through the advanced settings where they can choose to schedule the export. Ask if they want to enable the scheduling feature, and if so, prompt them to set the frequency. The options are Daily, Weekly, or Monthly: + +- **Daily**: Ask for the time of day. +- **Weekly**: Ask for the day of the week and time of day. +- **Monthly**: Ask for the day of the month and time of day. + +Other advanced settings will be hardcoded and should not be asked: +- Emails will default to an empty list. +- Top Memo Structure will be set to include "employee_email". +- Expense Memo Structure will be set to include "employee_email", "merchant", "purpose", "category", "spent_on", "report_number", and "expense_link". + +========================================================================================================================= +FINAL OUTPUT: + +Your responses can only be in the form of below JSONs: + +For CONVERSATION: +{ + "output_type": "CONVERSATION", // FINAL for the FINAL JSON PAYLOAD and CONVERSATION for questions + "output": { + "question": "What is the name of the bank account you want to use for Accounts Payable?", // this question is just an example + } +} + +For FINAL: +{ + "output_type": "FINAL", // FINAL for the FINAL JSON PAYLOAD and CONVERSATION for questions + "output_export_settings": { + "reimbursable_expenses_export_type": "BILL", + "bank_account_name": "Accounts Payable", + "mileage_account_name": "Mileage", + "reimbursable_expense_state": "PAYMENT_PROCESSING", + "reimbursable_expense_date": "last_spent_at", + "reimbursable_expense_grouped_by": "REPORT", + "credit_card_expense_export_type": "CREDIT_CARD_PURCHASE", + "credit_card_expense_state": "APPROVED", + "credit_card_entity_name_preference": "VENDOR", + "credit_card_account_name": "Capital One 2222", + "credit_card_expense_grouped_by": "EXPENSE", + "credit_card_expense_date": "spent_at" + }, + "output_field_mapping": { + "class_type": "COST_CENTER", + "project_type": "PROJECT", + "item_type": null + }, + "output_advanced_settings": { + "expense_memo_structure": [ + "employee_email", + "merchant", + "purpose", + "category", + "spent_on", + "report_number", + "expense_link" + ], + "top_memo_structure": [ + "employee_email" + ], + "schedule_is_enabled": true, + "emails_selected": [], + "day_of_month": null, + "day_of_week": null, + "frequency": "DAILY", + "time_of_day": "12:00:00" + } +} + +========================================================================================================================= + +Ensure the following guidelines: + +1. **Ask Questions Step-by-Step:** Return one question at a time in JSON format unless the user provides all information at once. +2. **Confusing Answers Clarification:** If the user answer is confusing and gibberish, please ask clarification questions. +3. **If User Provides Info:** Respond with the next appropriate question. +4. **Final Output:** Once all questions are answered and all steps are answered, output the final JSON payload as specified. +5. **Return JSON:** Return all the keys even if the value is `null`. +6. **Steps Ensurity:** Ensure every step has been answered, never give final JSON without completing all steps. + +""" diff --git a/apps/integration_helper/tests.py b/apps/integration_helper/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/integration_helper/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/integration_helper/urls.py b/apps/integration_helper/urls.py new file mode 100644 index 0000000..595f3b9 --- /dev/null +++ b/apps/integration_helper/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from .views import CoversationsView + + +urlpatterns = [ + path(route='', view=CoversationsView.as_view(), name='conversations') +] diff --git a/apps/integration_helper/views.py b/apps/integration_helper/views.py new file mode 100644 index 0000000..1cd1ce9 --- /dev/null +++ b/apps/integration_helper/views.py @@ -0,0 +1,92 @@ +import uuid +from rest_framework import status, generics +from rest_framework.response import Response +from apps.integration_helper.models import Conversation +from apps.integration_helper.openai_utils import get_openai_response +from apps.integration_helper.prompt import PROMPT + + +class CoversationsView(generics.CreateAPIView, generics.DestroyAPIView): + """ + View for creating and deleting conversations. + """ + + def create(self, request, *args, **kwargs): + """ + Create a new conversation and get the first OpenAI response. + """ + content = request.data.get('content') + conversation_id = request.data.get('conversation_id') + workspace_id = kwargs['workspace_id'] + + if not content: + return Response( + {'error': 'content are required'}, status=status.HTTP_400_BAD_REQUEST + ) + + if not conversation_id: + conversation_id = str(uuid.uuid4()) + + Conversation.objects.update_or_create( + defaults={'content': PROMPT}, + conversation_id=conversation_id, + workspace_id=workspace_id, + role='system' + ) + + conversation = Conversation.objects.create( + conversation_id=conversation_id, + workspace_id=workspace_id, + role='user', + content=content + ) + + messages = list( + Conversation.objects.filter( + conversation_id=conversation_id, + workspace_id=workspace_id + ).values('role', 'content').order_by('created_at')) + + assistant_response = get_openai_response(messages) + + Conversation.objects.create( + conversation_id=conversation_id, + workspace_id=workspace_id, + role='assistant', + content=assistant_response, + ) + + return Response( + { + 'conversation_id': conversation.conversation_id, + 'content': assistant_response, + }, + status=status.HTTP_201_CREATED, + ) + + def delete(self, request, *args, **kwargs): + """ + Clear the conversation history by deleting it using conversation_id. + """ + workspace_id = kwargs['workspace_id'] + conversation_id = request.data.get('conversation_id') + if not conversation_id: + return Response( + { + 'error': 'conversation_id is required' + }, status=status.HTTP_400_BAD_REQUEST + ) + + conversations = Conversation.objects.filter( + conversation_id=conversation_id, + workspace_id=workspace_id + ) + + if conversations.exists(): + conversations.delete() + + return Response( + { + 'message': 'Conversation cleared' + }, status=status.HTTP_200_OK + ) diff --git a/apps/workspaces/urls.py b/apps/workspaces/urls.py index 7b88a3e..a93734d 100644 --- a/apps/workspaces/urls.py +++ b/apps/workspaces/urls.py @@ -34,4 +34,5 @@ path('/accounting_exports/', include('apps.tasks.urls')), path('/qbd_mappings/', include('apps.mappings.urls')), path('/fyle/', include('apps.fyle.urls')), + path('/conversations/', include('apps.integration_helper.urls')) ] diff --git a/quickbooks_desktop_api/settings.py b/quickbooks_desktop_api/settings.py index d7579ca..27e095b 100644 --- a/quickbooks_desktop_api/settings.py +++ b/quickbooks_desktop_api/settings.py @@ -59,7 +59,8 @@ 'apps.fyle', 'apps.tasks', 'apps.qbd', - 'apps.mappings' + 'apps.mappings', + 'apps.integration_helper' ] MIDDLEWARE = [ diff --git a/requirements.txt b/requirements.txt index d6669a8..d81ed0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -49,3 +49,5 @@ pytest-mock==3.8.2 # Sendgrid for sending emails_selected sendgrid==6.9.7 sentry-sdk==1.19.1 + +openai