diff --git a/apps/spotlight/migrations/0002_copyexportsettings.py b/apps/spotlight/migrations/0002_copyexportsettings.py new file mode 100644 index 0000000..d870c1f --- /dev/null +++ b/apps/spotlight/migrations/0002_copyexportsettings.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.14 on 2024-09-12 20:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spotlight', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='CopyExportSettings', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('workspace_id', models.IntegerField(help_text='Workspace id of the organization')), + ('reimbursable_export_setting', models.JSONField(null=True)), + ('ccc_export_setting', models.JSONField(null=True)), + ], + options={ + 'db_table': 'copy_export_settings', + }, + ), + ] diff --git a/apps/spotlight/models.py b/apps/spotlight/models.py index 6186d34..e5f3b8c 100644 --- a/apps/spotlight/models.py +++ b/apps/spotlight/models.py @@ -21,3 +21,12 @@ class Query(models.Model): class Meta: db_table = 'queries' + +class CopyExportSettings(models.Model): + + workspace_id = models.IntegerField(help_text="Workspace id of the organization") + reimbursable_export_setting = models.JSONField(null=True) + ccc_export_setting = models.JSONField(null=True) + + class Meta: + db_table = 'copy_export_settings' diff --git a/apps/spotlight/prompts/suggestion_context_page_prompt.py b/apps/spotlight/prompts/suggestion_context_page_prompt.py new file mode 100644 index 0000000..953638e --- /dev/null +++ b/apps/spotlight/prompts/suggestion_context_page_prompt.py @@ -0,0 +1,175 @@ +SUGGESTION_PROMPT = """ + +Objectives: +You are a AI agent that suggests what actions and features +we provide for a specific page. You will get the user input at the end. + +Instructions: +These are the pages and their corresponding actions we provide, you will get the +URL as input and you have to reply the actions list: +Output should be in JSON format. + + +1. For /dashboard + Output: + {{ + "suggestions": {{ + "actions": [ + {{ + "code": "trigger_export", + "title": "Export IIF file", + "description": "Export the current data to an IIF file." + }} + ] + }} + }} + + +2. For /export_settings + Output: + {{ + "suggestions": {{ + "actions": [ + {{ + "code": "enable_reimbursable_expenses_export", + "title": "Enable reimbursable export settings", + "description": "Enable the option to export reimbursable expenses in Export Configuration." + }}, + {{ + "code": "disable_reimbursable_expenses_export", + "title": "Disable reimbursable export settings", + "description": "Disable the option to export reimbursable expenses in Export Configuration." + }}, + {{ + "code": "set_reimbursable_expenses_export_module_bill", + "title": "Select reimbursable export module as bill", + "description": "Choose Bill as the type of transaction in QuickBooks Desktop to export your Fyle expenses." + }}, + {{ + "code": "set_reimbursable_expenses_export_module_journal_entry", + "title": "Select reimbursable export module as journal entry", + "description": "Choose Journal Entry as the type of transaction in QuickBooks Desktop to export your Fyle expenses." + }}, + {{ + "code": "set_reimbursable_expenses_export_grouping_expense" + "title": "Group reimbursable expenses export by expense", + "description": "Set grouping to expense, this grouping reflects how the expense entries are posted in QuickBooks Desktop." + }}, + {{ + "code": "set_reimbursable_expenses_export_grouping_report", + "title": "Group reimbursable expenses export by report", + "description": "Set grouping to expense_report, this grouping reflects how the expense entries are posted in QuickBooks Desktop." + }}, + {{ + "code": "set_reimbursable_expenses_export_state_processing", + "title": "Set reimbursable expenses export state as processing", + "description": "You could choose to export expenses when they have been approved and are awaiting payment clearance." + }}, + {{ + "code": "set_reimbursable_expenses_export_state_paid", + "title": "Set reimbursable expenses export state as paid", + "description": "You could choose to export expenses when they have been paid out." + }}, + {{ + "code": "enable_corporate_card_expenses_export", + "title": "Enable option corporate card expenses export", + "description": "Enable the option to export of credit card expenses from Fyle to QuickBooks Desktop." + }}, + {{ + "code": "disable_corporate_card_expenses_export", + "title": "Disable reimbursable export settings", + "description": "Disable the option to export of credit card expenses from Fyle to QuickBooks Desktop." + }}, + {{ + "code": "set_corporate_credit_card_expenses_export_credit_card_purchase", + "title": "Set Credit Card Purchase transaction type to export", + "description": "Credit Card Purchase type of transaction in QuickBooks Desktop to be export as Fyle expenses." + }}, + {{ + "code": "set_corporate_credit_card_expenses_export_journal_entry", + "title": "Set Journal Entry transaction type to export", + "description": "Journal Entry type of transaction in QuickBooks Desktop to be export as Fyle expenses." + }}, + {{ + "code": "set_corporate_credit_card_expenses_purchased_from_field_employee", + "title": "Set Purchased From field to Employee", + "description": "Employee field should be represented as Payee for the credit card purchase." + }}, + {{ + "code": "set_corporate_credit_card_expenses_purchased_from_field_vendor", + "title": "Set Purchased From field to Vendor", + "description": "Vendor field should be represented as Payee for the credit card purchase." + }}, + {{ + "code": "set_corporate_credit_card_expenses_export_grouping_report", + "title": "Group corporate credit expenses export to report", + "description": "Group reports as the expense entries posted in QuickBooks Desktop." + }}, + {{ + "code": "set_corporate_credit_card_expenses_export_grouping_expense", + "title": "Group corporate credit expenses export to expenses", + "description": "Group expense as the expense entries posted in QuickBooks Desktop." + }}, + {{ + "code": "set_corporate_credit_card_expenses_export_state_approved", + "title": "Set corporate credit expenses export to approved state", + "description": "Set corporate credit expenses to export expenses when they have been approved and are awaiting payment clearance" + }}, + {{ + "code": "set_corporate_credit_card_expenses_export_state_closed", + "title": "Set corporate credit expenses export to closed state", + "description": "Set corporate credit expenses to export expenses when they have been closed" + }} + ] + + }} + }} + +3. /field_mappings + Output: + {{ + "suggestions": {{ + "actions": [ + {{ + "code": "set_customer_field_mapping_to_project" + "title": "Map Customer field to Project", + "description": "Set Project field in Fyle mapped to 'Customers' field in QuickBooks Desktops." + }}, + {{ + "code": "set_customer_field_mapping_to_cost_center", + "title": "Map Customer field to Cost Center", + "description": "Set Cost Center field in Fyle mapped to 'Customers' field in QuickBooks Desktop." + }}, + {{ + "code" "set_class_field_mapping_to_project", + "title": "Map Class field to Project", + "description": "Set Project field in Fyle mapped to 'Class' field in QuickBooks Desktop." + }}, + {{ + "code": "set_class_field_mapping_to_cost_center", + "title": "Map Class field to Cost Center", + "description": "Set Cost Center field in Fyle mapped to 'Class' field in QuickBooks Desktop." + }} + ] + }} +}} + + +----------------------------- +Important things to take care: +1. Match the user query and only reply the actions, nothing less nothing more. +2. Dont match the exact URL, it can be a bit different containing things in the beginning +or the end. +3. If the user query doesn't match any of the above provided URL please reply with empty suggestion like this: + +{{ + "suggestions": {{ + "actions": [] + }} +}} + +--------------------------- +User Query: {user_query} +--------------------------- + +""" diff --git a/apps/spotlight/service.py b/apps/spotlight/service.py index 6cf0528..f32ab31 100644 --- a/apps/spotlight/service.py +++ b/apps/spotlight/service.py @@ -6,9 +6,11 @@ import requests from apps.fyle.helpers import get_access_token +from apps.spotlight.models import CopyExportSettings from apps.workspaces.models import ExportSettings, FyleCredential from .prompts.support_genie import PROMPT as SUPPORT_GENIE_PROMPT from .prompts.spotlight_prompt import PROMPT as SPOTLIGHT_PROMPT +from .prompts.suggestion_context_page_prompt import SUGGESTION_PROMPT from . import llm @@ -58,6 +60,14 @@ def get_suggestions(cls, *, user_query: str) -> str: ) return llm.get_openai_response(system_prompt=formatted_prompt) +class SuggestionService: + @classmethod + def get_suggestions(cls, *, user_query: str) -> str: + formatted_prompt = SUGGESTION_PROMPT.format( + user_query=user_query + ) + + return llm.get_openai_response(system_prompt=formatted_prompt) class ActionService: @@ -79,6 +89,10 @@ def _get_action_function_from_code(cls, *, code: str) -> Callable: "set_corporate_credit_card_expenses_export_journal_entry": cls.set_cc_export_to_journal_entry, "set_corporate_credit_card_expenses_export_grouping_report": cls.set_cc_grouping_to_report, "set_corporate_credit_card_expenses_export_grouping_expense": cls.set_cc_grouping_to_expense, + "disable_reimbursable_expenses_export": cls.disable_reimbursable_expenses_export, + "enable_reimbursable_expenses_export": cls.enable_reimbursable_expenses_export, + "disable_corporate_card_expenses_export": cls.disable_corporate_card_expenses_export, + "enable_corporate_card_expenses_export": cls.enable_corporate_card_expenses_export } return code_to_function_map[code] @@ -315,6 +329,104 @@ def set_class_field_mapping_to_cost_center(cls, *, workspace_id: int): return ActionResponse(message="Field mapping updated successfully", is_success=True) return ActionResponse(message="Field mapping already exists", is_success=False) + @classmethod + def enable_reimbursable_expenses_export(cls, *, workspace_id: int): + fields_for_reimbursable = ['reimbursable_expenses_export_type', 'reimbursable_expense_state', 'reimbursable_expense_date', + 'reimbursable_expense_grouped_by', 'bank_account_name'] + + with transaction.atomic(): + export_settings = ExportSettings.objects.filter(workspace_id=workspace_id).first() + if export_settings: + if export_settings.reimbursable_expenses_export_type is None: + copied_export_settings = CopyExportSettings.objects.filter(workspace_id=workspace_id).first() + if copied_export_settings: + for field in fields_for_reimbursable: + setattr(export_settings, field, copied_export_settings.reimbursable_export_setting[field]) + + export_settings.save() + return ActionResponse(message='Successfully enabled reimbursable expense', is_success=True) + else: + return ActionResponse(message='Reimbursable Expense is already enabled', is_success=True) + + return ActionResponse(message="Export settings doesn't exists", is_success=False) + + @classmethod + def disable_reimbursable_expenses_export(cls, *, workspace_id: int): + fields_for_reimbursable = ['reimbursable_expenses_export_type', 'reimbursable_expense_state', 'reimbursable_expense_date', + 'reimbursable_expense_grouped_by', 'bank_account_name'] + with transaction.atomic(): + export_settings = ExportSettings.objects.filter(workspace_id=workspace_id).first() + if export_settings: + if export_settings.reimbursable_expenses_export_type is not None: + copied_export_settings, _ = CopyExportSettings.objects.get_or_create(workspace_id=workspace_id, + defaults={'reimbursable_export_setting': {}, 'ccc_export_setting': {}}) + reimbursable_export_setting = copied_export_settings.reimbursable_export_setting or {} + + for field in fields_for_reimbursable: + reimbursable_export_setting[field] = getattr(export_settings, field, None) + setattr(export_settings, field, None) + + copied_export_settings.reimbursable_export_setting = reimbursable_export_setting + + export_settings.save() + copied_export_settings.save() + + return ActionResponse(message='Reimbursable Expense successfully disabled!', is_success=True) + + else: + return ActionResponse(message='Reimbursable Expense is already disabled', is_success=True) + + return ActionResponse(message="Export settings doesn't exists", is_success=False) + + @classmethod + def enable_corporate_card_expenses_export(cls, *, workspace_id: int): + fields_for_ccc = ['credit_card_expense_export_type', 'credit_card_expense_state', 'credit_card_entity_name_preference', + 'credit_card_account_name', 'credit_card_expense_grouped_by', 'credit_card_expense_date'] + + with transaction.atomic(): + export_settings = ExportSettings.objects.filter(workspace_id=workspace_id).first() + if export_settings: + if export_settings.credit_card_expense_export_type is None: + copied_export_settings = CopyExportSettings.objects.filter(workspace_id=workspace_id).first() + if copied_export_settings: + for field in fields_for_ccc: + setattr(export_settings, field, copied_export_settings.ccc_export_setting[field]) + + export_settings.save() + return ActionResponse(message='Successfully enabled Corporate expense', is_success=True) + else: + return ActionResponse(message='Corporate Expense is already enabled', is_success=True) + + return ActionResponse(message="Export settings doesn't exists", is_success=False) + + @classmethod + def disable_corporate_card_expenses_export(cls, *, workspace_id: int): + fields_for_ccc = ['credit_card_expense_export_type', 'credit_card_expense_state', 'credit_card_entity_name_preference', + 'credit_card_account_name', 'credit_card_expense_grouped_by', 'credit_card_expense_date'] + with transaction.atomic(): + export_settings = ExportSettings.objects.filter(workspace_id=workspace_id).first() + if export_settings: + if export_settings.credit_card_expense_export_type is not None: + copied_export_settings, _ = CopyExportSettings.objects.get_or_create(workspace_id=workspace_id, + defaults={'reimbursable_export_setting': {}, 'ccc_export_setting': {}}) + ccc_export_setting = copied_export_settings.ccc_export_setting or {} + + for field in fields_for_ccc: + ccc_export_setting[field] = getattr(export_settings, field, None) + setattr(export_settings, field, None) + + copied_export_settings.ccc_export_setting = ccc_export_setting + + export_settings.save() + copied_export_settings.save() + + return ActionResponse(message='Corporate Expense successfully disabled!', is_success=True) + + else: + return ActionResponse(message='Corporate Expense is already disabled', is_success=True) + + return ActionResponse(message="Export settings doesn't exists", is_success=False) + @classmethod def action(cls, *, code: str, workspace_id: str): action_function = cls._get_action_function_from_code(code=code) diff --git a/apps/spotlight/urls.py b/apps/spotlight/urls.py index ff11aa1..6a5d266 100644 --- a/apps/spotlight/urls.py +++ b/apps/spotlight/urls.py @@ -14,7 +14,8 @@ """ from django.urls import path -from apps.spotlight.views import HelpQueryView, QueryView, RecentQueryView, ActionQueryView +from apps.spotlight.views import HelpQueryView, \ + QueryView, RecentQueryView, ActionQueryView, SuggestionForPage @@ -22,5 +23,6 @@ path('recent_queries/', RecentQueryView.as_view(), name='recent-queries'), path('query/', QueryView.as_view(), name='query'), path('help/', HelpQueryView.as_view(), name='help'), - path('action/', ActionQueryView.as_view(), name='action') + path('action/', ActionQueryView.as_view(), name='action'), + path('suggest_actions/', SuggestionForPage.as_view(), name='suggestion') ] diff --git a/apps/spotlight/views.py b/apps/spotlight/views.py index 87f5355..94d19ef 100644 --- a/apps/spotlight/views.py +++ b/apps/spotlight/views.py @@ -8,9 +8,11 @@ from apps.spotlight.models import Query from apps.spotlight.serializers import QuerySerializer -from .service import ActionService, HelpService, QueryService +from .service import ActionService, HelpService, QueryService, SuggestionService from apps.workspaces.models import FyleCredential from apps.fyle.helpers import get_access_token +from rest_framework.response import Response + code_action_map = { "trigger_export": 'http://localhost:8000/api/workspaces/2/trigger_export/' @@ -101,3 +103,11 @@ def post(self, request, *args, **kwargs): except Exception as e: print(e) return JsonResponse(data={"message": "Action failed"}, status=500) + +class SuggestionForPage(generics.CreateAPIView): + + def post(self, request, *args, **kwargs): + user_query = request.data['user_query'] + + support_response = SuggestionService.get_suggestions(user_query=user_query) + return JsonResponse(data={"message": support_response})