diff --git a/apps/spotlight/__init__.py b/apps/spotlight/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/spotlight/admin.py b/apps/spotlight/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/spotlight/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/spotlight/apps.py b/apps/spotlight/apps.py new file mode 100644 index 0000000..b227fb3 --- /dev/null +++ b/apps/spotlight/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SpotlightConfig(AppConfig): + name = 'spotlight' diff --git a/apps/spotlight/llm.py b/apps/spotlight/llm.py new file mode 100644 index 0000000..7db23e5 --- /dev/null +++ b/apps/spotlight/llm.py @@ -0,0 +1,96 @@ +import os +import boto3 +import openai +import json +from typing import Dict + + +AWS_REGION = os.environ.get("AWS_REGION") +AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") +AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") + +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") +KNOWLEDGE_BASE_ID = os.environ.get("KNOWLEDGE_BASE_ID") + + +bedrock_session = boto3.Session( + region_name=AWS_REGION, + aws_access_key_id=AWS_ACCESS_KEY_ID, + aws_secret_access_key=AWS_SECRET_ACCESS_KEY +) + +openai_client = openai.OpenAI( + api_key=OPENAI_API_KEY, + max_retries=5, + timeout=10 +) + + +def get_openai_response(*, system_prompt: str) -> dict: + try: + chat_completion_resp = openai_client.chat.completions.create( + model="gpt-4o", + response_format={ + "type": "json_object" + }, + messages=[ + {"role": "system", "content": system_prompt} + ], + temperature=0, + max_tokens=1000, + top_p=0, + frequency_penalty=0, + presence_penalty=0 + ) + + return json.loads( + chat_completion_resp.choices[0].message.content + ) + + except (openai.OpenAIError, json.JSONDecodeError) as e: + raise Exception(message=str(e)) + + + +def get_support_response_from_bedrock(*, prompt_template: str, input_message: str) -> Dict: + try: + bedrock_agent_runtime_client = bedrock_session.client( + 'bedrock-agent-runtime' + ) + + response = bedrock_agent_runtime_client.retrieve_and_generate( + input={ + 'text': input_message + }, + retrieveAndGenerateConfiguration={ + 'type': 'KNOWLEDGE_BASE', + 'knowledgeBaseConfiguration': { + 'knowledgeBaseId': KNOWLEDGE_BASE_ID, + 'modelArn': 'arn:aws:bedrock:ap-south-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0', + 'generationConfiguration': { + 'inferenceConfig': { + 'textInferenceConfig': { + 'maxTokens': 2048, + 'stopSequences': [], + 'temperature': 0, + 'topP': 1 + } + }, + 'promptTemplate': { + 'textPromptTemplate': prompt_template + } + }, + 'retrievalConfiguration': { + 'vectorSearchConfiguration': { + 'numberOfResults': 5, + 'overrideSearchType': 'HYBRID', + } + } + } + } + ) + + return response + + except json.JSONDecodeError as e: + print(e) diff --git a/apps/spotlight/migrations/0001_initial.py b/apps/spotlight/migrations/0001_initial.py new file mode 100644 index 0000000..c8af03b --- /dev/null +++ b/apps/spotlight/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 3.1.14 on 2024-09-10 05:28 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Query', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('query', models.TextField()), + ('workspace_id', models.IntegerField(help_text='Workspace id of the organization')), + ('_llm_response', models.JSONField(default={})), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Created at datetime')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Updated at datetime')), + ('user', models.ForeignKey(help_text='Reference to users table', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'queries', + }, + ), + ] 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/migrations/__init__.py b/apps/spotlight/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/spotlight/models.py b/apps/spotlight/models.py new file mode 100644 index 0000000..e5f3b8c --- /dev/null +++ b/apps/spotlight/models.py @@ -0,0 +1,32 @@ +from django.db import models +from django.contrib.auth import get_user_model + + +User = get_user_model() + + +# Create your models here. +class Query(models.Model): + id = models.AutoField(primary_key=True) + query = models.TextField() + workspace_id = models.IntegerField(help_text="Workspace id of the organization") + _llm_response = models.JSONField(default={}) + user = models.ForeignKey(User, on_delete=models.CASCADE, help_text='Reference to users table') + created_at = models.DateTimeField( + auto_now_add=True, help_text="Created at datetime" + ) + updated_at = models.DateTimeField( + auto_now=True, help_text="Updated at datetime" + ) + + 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/__init__.py b/apps/spotlight/prompts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/spotlight/prompts/spotlight_prompt.py b/apps/spotlight/prompts/spotlight_prompt.py new file mode 100644 index 0000000..2b8716d --- /dev/null +++ b/apps/spotlight/prompts/spotlight_prompt.py @@ -0,0 +1,668 @@ +PROMPT = """ +You are an AI assistant for integrations usage in expense management application. Your role is to interpret user searches and provide relevant suggestions in a JSON format. Use the following guidelines: + +-------------------- + +General Intructions: + 1. Analyze the user's search query to determine the context and intent. + 2. Based on the search, provide up to four relevant suggestions that may include: + - Action: Suggest a task the user can perform + - Navigation: Navigate user to a page related to user's query + - Help: Offer guidance or explanations + 4. Ensure that the suggestions are relevant to the user's search query and provide actionable or informative options. + 5. If a query is ambiguous, prioritize the most likely interpretations. + 6. IMPORTANT: If the user's search query does not match any specific actions, navigations, or help suggestions, return an empty Array for each key. + 7. IMPORTANT: Be very specific regarding the actions you give, only choose actions from the examples given below. + 8. Format your response as a JSON object with the following structure: + {{ + "search": "user's search query", + "suggestions": {{ + "actions" : [ + {{ + "type": "action", + "code": "unique code for the action", + "title": "suggest title" + "description": "brief description of the suggestion", + "icon": "icon code" + }}, + // ... up to two actions + ], + "navigations": [ + {{ + "type": "navigation", + "code": "unique code for the navigation", + "title": "suggest title" + "description": "brief description of the suggestion", + "url": "URL to navigate to", + "icon": "icon code" + }}, + // ... up to two navigations + ], + "help": [ + {{, + "type": "help", + "title": "suggest title" + "description": "form a question based on user question", + "icon": "icon code" + }}, + // ... up to two help suggestions + ] + }} + }} + +-------------------- + +Actions Intructions: + 1. Provide specific actions that the user can take based on their search query. + 2. Each action should have a unique code, title, and a brief description. + 3. The action should be actionable and relevant to the user's search query. + 4. IMPORTANT: Only choose actions from the examples given below. + 5. Interpret the user's search query to suggest relevant actions. + 6. Ignore spelling errors and variations in the user's search query. + 7. Suggest the best action that matches the user's search query. + + Actions Map: + "actions" : [ + {{ + "type": "action", + "code": "trigger_export", + "title": "Export IIF file", + "description": "Export the current data to an IIF file.", + "icon": "pi-file-export" + }}, + {{ + "type": "action", + "code": "enable_reimbursable_expenses_export", + "title": "Enable reimbursable export settings", + "description": "Enable the option to export reimbursable expenses in Export Configuration.", + "icon": "pi-check" + }}, + {{ + "type": "action", + "code": "disable_reimbursable_expenses_export", + "title": "Disable reimbursable export settings", + "description": "Disable the option to export reimbursable expenses in Export Configuration.", + "icon": "pi-times" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "code": "set_corporate_credit_card_expenses_export_credit_card_purchase", + "title": "Select Corporate Credit Card export module as credit card purchase", + "description": "Credit Card Purchase type of transaction in QuickBooks Desktop to be export as Fyle expenses.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "code": "set_corporate_credit_card_expenses_export_journal_entry", + "title": "Select Corporate Credit Card export module as journal entry", + "description": "Journal Entry type of transaction in QuickBooks Desktop to be export as Fyle expenses.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "code": "set_corporate_credit_card_expenses_purchased_from_field_employee", + "title": "Set Corporate Credit Card Purchase field to Employee", + "description": "Employee field should be represented as Payee for the credit card purchase.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "code": "set_corporate_credit_card_expenses_purchased_from_field_vendor", + "title": "Set Corporate Credit Card Purchase field to Vendor", + "description": "Vendor field should be represented as Payee for the credit card purchase.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-sitemap" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-sitemap" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-sitemap" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-sitemap" + }} + ] + +-------------------- + +Navigations Intructions: + 1. Provide specific navigations that the user can take based on their search query. + 2. Each navigation should have a unique code, title, description, and a URL. + 3. The navigation should guide the user to a relevant page based on their search query. + 4. IMPORTANT: Only choose navigations from the examples given below. + 5. Interpret the user's search query to suggest relevant navigations. + 6. Ignore spelling errors and variations in the user's search query. + 7. Suggest the best navigation that matches the user's search query. + + Navigations Map: + "navigations": [ + {{ + "type": "navigation", + "code": "go_to_dashboard", + "title": "Go to Dashboard", + "description": "Navigate to the IIF file management section for import/export options.", + "url": "/dashboard", + "icon": "pi-external-link" + }}, + {{ + "type": "navigation", + "code": "go_to_settings", + "title": "Go to Export Settings", + "description": "Navigate to the export settings section to manage export configurations.", + "url": "/configuration/export_settings", + "icon": "pi-external-link" + }}, + {{ + "type": "navigation", + "code": "go_to_field_mappings", + "title": "Go to Field Mappings", + "description": "Navigate to the Field Mapping Settings Section to manage Field Mapping Settings.", + "url": "/configuration/field_mapping", + "icon": "pi-external-link" + }}, + {{ + "type": "navigation", + "code": "go_to_advanced_settings", + "title": "Go to Advanced Settings", + "description": "Navigate to the advanced settings section to manage automatic export settings.", + "url": "/configuration/advanced_settings", + "icon": "pi-external-link" + }}, + {{ + "type": "navigation", + "code": "go_to_mappings", + "title": "Go to Mappings Page", + "description": "Navigate to the field mapping section to configure mappings.", + "url": "/mapping/corporate_card", + "icon": "pi-external-link" + }} + ] + +-------------------- + +Help Instructions: + 1. Formulate a question based on the user's search query in description. + 2. The question should be formulated to QBD as a context of question. + 3. The help suggestion should offer guidance or explanations related to the user's search query. + 4. Ignore spelling errors and variations in the user's search query. + + Examples: + 1. User Query Inputs: ["How to export IIF files?", "export IIF files", "IIF export", "learn how to export IIF files", "export to IIF", "how to export data to IIF", "IIF file export", "guide to exporting IIF files", "learn export", "IIF file", "how to export", "export instructions", "IIF export process"] + + Output: + {{ + "type": "help", + "code": "learn_export", + "title": "Learn more about IIF export", + "description": "How to export IIF File in QBD?.", + "code": "pi-info-circle" + }} + 2. User Query Inputs: ["Disable reimbursable expenses", "disable reimbursable export", "turn off reimbursable export", "stop exporting reimbursable expenses", "disable reimbursable expenses export", "disable export settings for reimbursable expenses", "deactivate reimbursable export", "disable reimbursable export settings", "turn off export for reimbursable expenses", "stop reimbursable export"] + + Output: + {{ + "type": "help", + "code": "disable_reimbursable_expenses_export", + "title": "Disable reimbursable export settings", + "description": "Disable the option to export reimbursable expenses in Export Configuration in QBD.", + "code": "pi-info-circle" + }} + 3. User Query: ["How to manage IIF files?", "manage IIF files", "IIF file management", "how to filter IIF files", "filter IIF files by date", "manage files in QBD", "IIF file filters", "filter IIF data", "how to filter by date in QuickBooks Desktop", "date filters in IIF files", "QBD IIF file management", "file management in QuickBooks Desktop", "IIF file organization"] + + Output: + {{ + "type": "help", + "code": "date_filter_help", + "title": "How to filter IIF files by date", + "description": "How to filter by date in QBD?", + "code": "pi-info-circle" + }} + 4. User Query: "How to map fields?" + {{ + "type": "help", + "code": "configure_credit_card_mapping", + "title": "How to configure credit card mapping", + "description": "how to set up field mappings for credit card transactions in QBD.", + "code": "pi-info-circle" + }} + 5. User Query: "How to create field mappings?" + {{ + "type": "help", + "code": "field_mapping_help", + "title": "How to create field mappings", + "description": "how to create new field mappings for import/export in QBD.", + "code": "pi-info-circle" + }} + 6. User Query: "How to set up automatic export?" + {{ + "type": "help", + "code": "automatic_export_help", + "title": "How to set up automatic export", + "description": "how to configure automatic export settings for your data in QBD.", + "code": "pi-info-circle" + }} + 7. User Query: "How to use memo field in export?" + {{ + "type": "help", + "code": "memo_field_help", + "title": "How to use memo field in export", + "description": "how to properly set and use the memo field in data exports in QBD.", + "code": "pi-info-circle" + }} + 8. User Query: "How to map fields?" + {{ + "type": "help", + "code": "map_fields_help", + "title": "How to map fields", + "description": "how to map fields for accurate data handling and export in QBD.", + "code": "pi-info-circle" + }} + 9. User Query: "How to create new field mappings?" + {{ + "type": "help", + "code": "set_automatic_export", + "title": "Set/Update automatic export settings", + "description": "how to create new field mappings for import/export in QBD.", + "code": "pi-info-circle" + }} + +--------------------------- +User Query: {user_query} +--------------------------- + +Examples: +1. User Input Options: ["import and export IIF files", "export", "IIF export", "trigger export", "export current data", "export to IIF", "go to dashboard", "dashboard", "manage IIF files", "import IIF files", "learn about IIF export", "learn export", "IIF file management"] + + Output: + {{ + "suggestions": {{ + "actions": [ + {{ + "type": "action", + "code": "trigger_export", + "title": "Export IIF file", + "description": "Export the current data to an IIF file.", + "icon": "pi-file-export" + }} + ], + "navigations": [ + {{ + "type": "navigation", + "code": "go_to_dashboard", + "title": "Go to Dashboard", + "url": "/dashboard", + "description": "Navigate to the IIF file management section for import/export options.", + "icon": "pi-external-link" + }} + ], + "help": [ + {{ + "code": "learn_export", + "title": "Learn more about IIF export", + "description": "How to export IIF files in QBD?", + "code": "pi-info-circle" + }} + ] + }} + }} + +2. User Input Options: ["update export settings", "export settings", "Settings", "enable reimbursable export", "disable reimbursable export", "select export module", "group by expense", "group by expense report", "export state as processing", "export state as paid", "select module as bill", "select module as journal entry", "learn about export settings", "go to export settings", "enable", "disable", "export", "grouping", "processing state", "paid state", "reimbursable export", "configure export"] + + Output: + {{ + "suggestions": {{ + "actions": [ + {{ + "type": "action", + "code": "enable_reimbursable_expenses_export", + "title": "Enable reimbursable export settings", + "description": "Enable the option to export reimbursable expenses in Export Configuration.", + ""icon": "pi-list-check" + }}, + {{ + "type": "action", + "code": "disable_reimbursable_expenses_export", + "title": "Disable reimbursable export settings", + "description": "Disable the option to export reimbursable expenses in Export Configuration.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "code": "set_reimbursable_expenses_export_grouping_expense_report", + "title": "Group reimbursable expenses export by expense report", + "description": "Set grouping to expense_report, this grouping reflects how the expense entries are posted in QuickBooks Desktop.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "code": "set_reimbursable_expenses_export_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.", + "icon": "pi-list-check" + }}, + {{ + "type": "action", + "code": "set_reimbursable_expenses_export_export_state_paid", + "title": "Set reimbursable expenses export state as paid", + "description": "You could choose to export expenses when they have been paid out.", + "icon": "pi-list-check" + }} + ], + "help": [ + {{ + "type": "help", + "code": "learn_export_settings", + "title": "Learn more about reimbursable expenses export settings", + "description": "How to enable or disable reimbursable export settings in QBD?", + "code": "pi-info-circle" + }} + ], + "navigations": [ + {{ + "type": "navigation", + "code": "go_to_settings", + "title": "Go to Export Settings", + "description": "Navigate to the export settings section to manage export configurations.", + "url": "/configuration/export_settings", + "icon": "pi-external-link" + }} + ] + }} + }} + +3. User Input: ["create or update field mappings settings", "update field mappings", "create field mappings", "field mappings", "map customer field to project", "map customer field to cost center", "map class field to project", "map class field to cost center", "field mapping settings", "go to field mappings", "learn field mappings", "field mapping help", "create new field mapping", "update field mapping", "import field mappings", "export field mappings"] + + Output: + {{ + "suggestions": {{ + "actions": [ + {{ + "type": "action", + "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 Desktop.", + "icon": "pi-sitemap" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-sitemap" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-sitemap" + }}, + {{ + "type": "action", + "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.", + "icon": "pi-sitemap" + }} + ], + "navigations": [ + {{ + "type": "navigation", + "code": "go_to_field_mappings", + "title": "Go to Field Mappings", + "description": "Navigate to the Field Mapping Settings Section to manage Field Mapping Settings.", + "url": "/configuration/field_mapping", + "icon": "pi-external-link" + }} + ], + "help": [ + {{ + "type": "help", + "code": "field_mapping_help", + "title": "How to create field mappings", + "description": "How to create new field mappings for import/export in QBD?", + "code": "pi-info-circle" + }} + ] + }} + }} + + +4. User Input: "update automatic export" + Output: + {{ + "suggestions": {{ + "actions" : [ + {{ + "type": "action", + "code": "set_automatic_export_settings", + "title": "Set/Update automatic export settings", + "description": "Configure the automatic export settings for scheduled exports.", + "icon": "pi-list-check" + }} + ], + "navigations": [ + {{ + "type": "navigation", + "code": "go_to_advanced_settings", + "title": "Go to Advanced Settings", + "description": "Navigate to the advanced settings section to manage automatic export settings.", + "url": "/configuration/advanced_settings", + "icon": "pi-external-link" + }} + ], + "help": [ + {{ + "type": "help", + "code": "automatic_export_help", + "title": "How to set up automatic export", + "description": "how to configure automatic export settings for your data in QBD?", + "code": "pi-info-circle" + }} + ] + }} + }} + + +5. User Input: "set memo field in export" + Output: + {{ + "suggestions": {{ + "actions" : [ + {{ + "type": "action", + "code": "set_memo_field", + "title": "Set/Update memo field for exports", + "description": "Configure the memo field for exported data to include relevant details.", + "icon": "pi-list-check" + }} + ], + "help": [ + {{ + "type": "help", + "code": "memo_field_help", + "title": "How to use memo field in export", + "description": "how to properly set and use the memo field in data exports in QBD?", + "code": "pi-info-circle" + }} + ], + "navigations": [ + {{ + "type": "navigation", + "code": "go_to_advanced_settings", + "title": "Go to Advanced Settings", + "description": "Navigate to the advanced settings section to configure memo field settings.", + "url": "/configuration/advanced_settings", + "icon": "pi-external-link" + }} + ] + }} + }} + +6. User Input: "map Fyle field to QBD fields" + Output: + {{ + "suggestions": {{ + "actions" : [ + {{ + "type": "action", + "code": "map_fields", + "title": "Map Fyle fields to QBD fields", + "description": "Configure the mapping of one field to another for iif export.", + "icon": "pi-sitemap" + }} + ], + "help": [ + {{ + "type": "help", + "code": "map_fields_help", + "title": "How to map fields", + "description": "how to map fields for accurate data handling and export in QBD?", + "code": "pi-info-circle" + }} + ], + "navigations": [ + {{ + "type": "navigation", + "code": "go_to_corporate_cards_mappings", + "title": "Go to Corporate Card Mappings Page", + "description": "Navigate to the corporate card field mapping section to configure mappings.", + url: "/mapping/corporate_card", + "icon": "pi-external-link" + }}, + {{ + "type": "navigation", + "code": "go_to_items_mappings", + "title": "Go to Items Mappings Page", + "description": "Navigate to the items field mapping section to configure mappings.", + url: "/mapping/item", + "icon": "pi-external-link" + }} + ] + }} + }} +""" \ No newline at end of file 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/prompts/support_genie.py b/apps/spotlight/prompts/support_genie.py new file mode 100644 index 0000000..0abfcbd --- /dev/null +++ b/apps/spotlight/prompts/support_genie.py @@ -0,0 +1,12 @@ +PROMPT = """ +You are a question-answering agent. Your task is to answer the user's question using only the information available in the provided search results. +Instructions: +1. The user will ask a question, and you must respond based solely on the information contained in the search results. +2. If the search results do not contain the information needed to answer the question, clearly state that an exact answer could not be found. +3. Do not assume that any assertion made by the user is true. Always verify the user's claims against the search results before including them in your response. +4. Your response must be factual and should only include information directly supported by the search results. Avoid making any assumptions or providing information not present in the documents. +5. Always respond in the third person. +Here are the search results in numbered order: +$search_results$ +$output_format_instructions$ +""" diff --git a/apps/spotlight/serializers.py b/apps/spotlight/serializers.py new file mode 100644 index 0000000..59cb025 --- /dev/null +++ b/apps/spotlight/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from .models import Query + + +class QuerySerializer(serializers.ModelSerializer): + class Meta: + model = Query + fields = ['query'] diff --git a/apps/spotlight/service.py b/apps/spotlight/service.py new file mode 100644 index 0000000..f32ab31 --- /dev/null +++ b/apps/spotlight/service.py @@ -0,0 +1,433 @@ +from dataclasses import dataclass +from typing import Callable, Dict +from django.db import transaction +import json + +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 + + +@dataclass +class ActionResponse: + message: str = None + is_success: bool = None + + +class HelpService: + @classmethod + def extract_citations(cls, *, citations: list) -> list: + urls = set() + for citation in citations: + for reference in citation["retrievedReferences"]: + urls.add(reference['location']['webLocation']['url']) + return list(urls) + + @classmethod + def format_response(cls, *, response: Dict) -> str: + # Extract citations + citations = cls.extract_citations(citations=response["citations"]) + + # Format response + formatted_response = response["output"]["text"] + if citations: + formatted_response = formatted_response + "\n\n*Sources:*\n" + "\n".join(citations) + + return formatted_response + + @classmethod + def get_support_response(cls, *, user_query: str) -> str: + response = llm.get_support_response_from_bedrock( + prompt_template=SUPPORT_GENIE_PROMPT, + input_message=user_query + ) + + return cls.format_response(response=response) + + +class QueryService: + @classmethod + def get_suggestions(cls, *, user_query: str) -> str: + formatted_prompt = SPOTLIGHT_PROMPT.format( + user_query=user_query + ) + 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: + + @classmethod + def _get_action_function_from_code(cls, *, code: str) -> Callable: + code_to_function_map = { + "trigger_export": cls.trigger_export, + "set_reimbursable_expenses_export_module_bill": cls.set_reimbursable_expenses_export_module_bill, + "set_reimbursable_expenses_export_module_journal_entry": cls.set_reimbursable_expenses_export_module_journal_entry, + "set_reimbursable_expenses_export_grouping_expense": cls.set_reimbursable_expenses_export_grouping_expense, + "set_reimbursable_expenses_export_grouping_report": cls.set_reimbursable_expenses_export_grouping_report, + "set_reimbursable_expenses_export_state_processing": cls.set_reimbursable_expenses_export_state_processing, + "set_reimbursable_expenses_export_state_paid": cls.set_reimbursable_expenses_export_state_paid, + "set_customer_field_mapping_to_project": cls.set_customer_field_mapping_to_project, + "set_customer_field_mapping_to_cost_center": cls.set_customer_field_mapping_to_cost_center, + "set_class_field_mapping_to_project": cls.set_class_field_mapping_to_project, + "set_class_field_mapping_to_cost_center": cls.set_class_field_mapping_to_cost_center, + "set_corporate_credit_card_expenses_export_credit_card_purchase": cls.set_cc_export_to_corporate_card_purchase, + "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] + + @classmethod + def get_headers(cls, *, access_token: str) -> Dict: + return { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + + @classmethod + def get_access_token(cls, *, workspace_id: int) -> str: + creds = FyleCredential.objects.get(workspace_id=workspace_id) + return get_access_token(creds.refresh_token) + + @classmethod + def set_reimbursable_expenses_export_module_bill(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter( + workspace_id=workspace_id + ).first() + if export_settings is None: + return ActionResponse(message="Failed to set reimbursable expense export type set to Bill", is_success=False) + else: + export_settings.reimbursable_expenses_export_type = 'BILL' + export_settings.save() + return ActionResponse(message="Reimbursable expense export type set to Bill", is_success=True) + + @classmethod + def set_reimbursable_expenses_export_module_journal_entry(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter( + workspace_id=workspace_id + ).first() + if export_settings is None: + return ActionResponse(message="Failed to set reimbursable expense export type set to Journal Entry", is_success=False) + else: + export_settings.reimbursable_expenses_export_type = 'JOURNAL_ENTRY' + export_settings.save() + return ActionResponse(message="Reimbursable expense export type set to Journal Entry", is_success=True) + + @classmethod + def set_reimbursable_expenses_export_grouping_expense(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter( + workspace_id=workspace_id + ).first() + if export_settings is None: + return ActionResponse(message="Failed to set reimbursable expense export grouping to Expenses", is_success=False) + else: + export_settings.reimbursable_expense_grouped_by = 'EXPENSE' + export_settings.save() + return ActionResponse(message="Reimbursable expense export group set to Expenses", is_success=True) + + @classmethod + def set_reimbursable_expenses_export_grouping_report(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter( + workspace_id=workspace_id + ).first() + if export_settings is None: + return ActionResponse(message="Failed to set reimbursable expense export grouping to Report", is_success=False) + else: + export_settings.reimbursable_expense_grouped_by = 'REPORT' + export_settings.save() + return ActionResponse(message="Reimbursable expense export group set to Report", is_success=True) + + @classmethod + def set_reimbursable_expenses_export_state_processing(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter( + workspace_id=workspace_id + ).first() + if export_settings is None: + return ActionResponse(message="Failed to set reimbursable expense export state to Processing", is_success=False) + else: + export_settings.reimbursable_expense_state = 'PAYMENT_PROCESSING' + export_settings.save() + return ActionResponse(message="Reimbursable expense export state set to Processing", is_success=True) + + @classmethod + def set_reimbursable_expenses_export_state_paid(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter( + workspace_id=workspace_id + ).first() + if export_settings is None: + return ActionResponse(message="Failed to set reimbursable expense export state to Paid", is_success=False) + else: + export_settings.reimbursable_expense_state = 'PAID' + export_settings.save() + return ActionResponse(message="Reimbursable expense export state set to Paid", is_success=True) + + @classmethod + def trigger_export(cls, *, workspace_id: int): + access_token = cls.get_access_token(workspace_id=workspace_id) + headers = cls.get_headers(access_token=access_token) + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + url = f'http://localhost:8000/api/workspaces/{workspace_id}/trigger_export/' + action_response = requests.post(url, json={}, headers=headers) + if action_response.status_code == 200: + return ActionResponse(message="Export triggered successfully", is_success=True) + return ActionResponse(message="Export triggered failed", is_success=False) + + + @classmethod + def set_cc_export_to_corporate_card_purchase(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter(workspace_id=workspace_id).first() + if export_settings: + export_settings.credit_card_expense_export_type = 'CREDIT_CARD_PURCHASE' + export_settings.save() + return ActionResponse(message="Successfully set corporate card expense as Credit Card Purchase", is_success=True) + + return ActionResponse(message="Export settings doesn't exists!", is_success=False) + + + @classmethod + def set_cc_export_to_journal_entry(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter(workspace_id=workspace_id).first() + if export_settings: + export_settings.credit_card_expense_export_type = 'JOURNAL_ENTRY' + export_settings.save() + return ActionResponse(message="Successfully set corporate card expense as JOURNAL ENTRY", is_success=True) + + return ActionResponse(message="Export settings doesn't exists!", is_success=False) + + @classmethod + def set_cc_grouping_to_report(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter(workspace_id=workspace_id).first() + if export_settings: + if export_settings.credit_card_expense_export_type == 'CREDIT_CARD_PURCHASE': + return ActionResponse(message='For Corporate Credit Purchase Export type expenses cannot be grouped by report', is_success=False) + else: + export_settings.credit_card_expense_grouped_by = 'REPORT' + export_settings.save() + return ActionResponse(message='Succesfully set corporate card group by to Report', is_success=True) + + return ActionResponse(message="Export settings doesn't exists!", is_success=False) + + + @classmethod + def set_cc_grouping_to_expense(cls, *, workspace_id: int): + with transaction.atomic(): + export_settings = ExportSettings.objects.filter(workspace_id=workspace_id).first() + if export_settings: + if export_settings.credit_card_expense_export_type == 'CREDIT_CARD_PURCHASE': + return ActionResponse(message='Already set to expense', is_success=True) + else: + export_settings.credit_card_expense_grouped_by = 'EXPENSE' + export_settings.save() + return ActionResponse(message='Succesfully set corporate card group by to EXPENSE', is_success=True) + + return ActionResponse(message="Export settings doesn't exists!", is_success=False) + + + @classmethod + def set_customer_field_mapping_to_project(cls, *, workspace_id: int): + access_token = cls.get_access_token(workspace_id=workspace_id) + headers = cls.get_headers(access_token=access_token) + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + url = f'http://localhost:8000/api/workspaces/{workspace_id}/field_mappings/' + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + action_response = requests.get(url, headers=headers) + action_response= action_response.json() + if action_response.get('project_type') != 'PROJECT' and action_response.get('class_type') != 'COST_CENTER': + action_response['project_type'] = 'PROJECT' + post_response = requests.post(url, headers=headers, data=json.dumps(action_response)) + return ActionResponse(message="Field mapping updated successfully", is_success=True) + return ActionResponse(message="Field mapping already exists", is_success=False) + + @classmethod + def set_customer_field_mapping_to_cost_center(cls, *, workspace_id: int): + access_token = cls.get_access_token(workspace_id=workspace_id) + headers = cls.get_headers(access_token=access_token) + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + url = f'http://localhost:8000/api/workspaces/{workspace_id}/field_mappings/' + + action_response = requests.get(url, headers=headers) + action_response= action_response.json() + if action_response.get('project_type') != 'COST_CENTER' and action_response.get('class_type') != 'PROJECT': + action_response['project_type'] = 'COST_CENTER' + post_response = requests.post(url, headers=headers, data=json.dumps(action_response)) + return ActionResponse(message="Field mapping updated successfully", is_success=True) + return ActionResponse(message="Field mapping already exists", is_success=False) + + @classmethod + def set_class_field_mapping_to_project(cls, *, workspace_id: int): + access_token = cls.get_access_token(workspace_id=workspace_id) + headers = cls.get_headers(access_token=access_token) + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + url = f'http://localhost:8000/api/workspaces/{workspace_id}/field_mappings/' + + action_response = requests.get(url, headers=headers) + action_response= action_response.json() + if action_response.get('project_type') != 'PROJECT' and action_response.get('class_type') != 'COST_CENTER': + action_response['class_type'] = 'PROJECT' + post_response = requests.post(url, headers=headers, data=json.dumps(action_response)) + return ActionResponse(message="Field mapping updated successfully", is_success=True) + return ActionResponse(message="Field mapping already exists", is_success=False) + + @classmethod + def set_class_field_mapping_to_cost_center(cls, *, workspace_id: int): + access_token = cls.get_access_token(workspace_id=workspace_id) + headers = cls.get_headers(access_token=access_token) + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + url = f'http://localhost:8000/api/workspaces/{workspace_id}/field_mappings/' + + action_response = requests.get(url, headers=headers) + action_response= action_response.json() + if action_response.get('project_type') != 'COST_CENTER' and action_response.get('class_type') != 'PROJECT': + action_response['class_type'] = 'COST_CENTER' + post_response = requests.post(url, headers=headers, data=json.dumps(action_response)) + 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) + return action_function(workspace_id=workspace_id) diff --git a/apps/spotlight/tests.py b/apps/spotlight/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/spotlight/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/spotlight/urls.py b/apps/spotlight/urls.py new file mode 100644 index 0000000..6a5d266 --- /dev/null +++ b/apps/spotlight/urls.py @@ -0,0 +1,28 @@ +"""quickbooks_desktop_api URL Configuration +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path + +from apps.spotlight.views import HelpQueryView, \ + QueryView, RecentQueryView, ActionQueryView, SuggestionForPage + + + +urlpatterns = [ + 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('suggest_actions/', SuggestionForPage.as_view(), name='suggestion') +] diff --git a/apps/spotlight/views.py b/apps/spotlight/views.py new file mode 100644 index 0000000..94d19ef --- /dev/null +++ b/apps/spotlight/views.py @@ -0,0 +1,113 @@ +import json +from django.http import JsonResponse +from django.db import transaction +from rest_framework import generics +import requests +from django_q.tasks import async_task + +from apps.spotlight.models import Query +from apps.spotlight.serializers import QuerySerializer + +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/' +} + +# Create your views here. +# class RecentQueryView(generics.ListAPIView): +# serializer_class = QuerySerializer +# # lookup_field = 'workspace_id' +# # lookup_url_kwarg = 'workspace_id' + +# def get_queryset(self): +# filters = { +# # 'workspace_id': self.kwargs.get('workspace_id'), +# # 'user': self.request.user, +# 'workspace_id': 1, +# 'user_id': 1, +# } + +# return Query.objects.filter( +# **filters +# ).all().order_by("-created_at")[:5] + + +class RecentQueryView(generics.ListAPIView): + serializer_class = QuerySerializer + lookup_field = 'workspace_id' + lookup_url_kwarg = 'workspace_id' + + def get(self, request, *args, **kwargs): + filters = { + 'workspace_id': self.kwargs.get('workspace_id'), + 'user': self.request.user, + } + + _recent_queries = Query.objects.filter( + **filters + ).all().order_by("-created_at")[:5] + + # recent_queries = [] + # for query in _recent_queries: + # recent_queries.append({ + # "query": query.query, + # "suggestions": query._llm_response["suggestions"] + # }) + recent_queries = [query.query for query in _recent_queries] + return JsonResponse(data={"recent_queries": recent_queries}, safe=False) + + +class QueryView(generics.CreateAPIView): + def post(self, request, *args, **kwargs): + workspace_id = self.kwargs.get('workspace_id') + user = self.request.user + with transaction.atomic(): + payload = json.loads(request.body) + user_query = payload["query"] + suggestions = QueryService.get_suggestions(user_query=user_query) + + Query.objects.create( + query=user_query, + workspace_id=workspace_id, + _llm_response=suggestions, + user_id=user.id + ) + return JsonResponse(data=suggestions["suggestions"]) + + +class HelpQueryView(generics.CreateAPIView): + def post(self, request, *args, **kwargs): + payload = json.loads(request.body) + user_query = payload["query"] + support_response = HelpService.get_support_response(user_query=user_query) + return JsonResponse(data={"message": support_response}) + + +class ActionQueryView(generics.CreateAPIView): + def post(self, request, *args, **kwargs): + workspace_id = self.kwargs.get('workspace_id') + payload = json.loads(request.body) + code = payload["code"] + + try: + action_response = ActionService.action(code=code, workspace_id=workspace_id) + if action_response.is_success is True: + return JsonResponse(data={"message": action_response.message}, status=200) + else: + return JsonResponse(data={"message": action_response.message}, status=500) + 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}) diff --git a/apps/workspaces/tasks.py b/apps/workspaces/tasks.py index a337d13..c69761e 100644 --- a/apps/workspaces/tasks.py +++ b/apps/workspaces/tasks.py @@ -100,3 +100,11 @@ def async_create_admin_subcriptions(workspace_id: int) -> None: 'webhook_url': '{}/workspaces/{}/fyle/webhook_callback/'.format(settings.API_URL, workspace_id) } platform.subscriptions.post(payload) + + +def trigger_export(workspace_id): + run_import_export(workspace_id=workspace_id) + new_expenses_imported = Expense.objects.filter( + workspace_id=workspace_id, exported=False + ).exists() + return new_expenses_imported \ No newline at end of file diff --git a/apps/workspaces/urls.py b/apps/workspaces/urls.py index 7b88a3e..8c7e3d1 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('/spotlight/', include('apps.spotlight.urls')) ] diff --git a/apps/workspaces/views.py b/apps/workspaces/views.py index 1f17221..b883771 100644 --- a/apps/workspaces/views.py +++ b/apps/workspaces/views.py @@ -16,7 +16,7 @@ WorkspaceSerializer, ExportSettingsSerializer, FieldMappingSerializer, AdvancedSettingSerializer ) -from .tasks import run_import_export +from .tasks import trigger_export class WorkspaceView(generics.CreateAPIView, generics.RetrieveAPIView): @@ -98,10 +98,7 @@ def post(self, request, *args, **kwargs): """ workspace_id = self.kwargs.get('workspace_id') - run_import_export(workspace_id=workspace_id) - new_expenses_imported = Expense.objects.filter( - workspace_id=workspace_id, exported=False - ).exists() + new_expenses_imported = trigger_export(workspace_id) return Response( status=status.HTTP_200_OK, diff --git a/docker-compose.yml.template b/docker-compose.yml.template index 7ba0d76..66aa1cc 100644 --- a/docker-compose.yml.template +++ b/docker-compose.yml.template @@ -25,6 +25,11 @@ services: DB_PASSWORD: postgres DB_HOST: db DB_PORT: 5432 + AWS_REGION: '' + AWS_ACCESS_KEY_ID: '' + AWS_SECRET_ACCESS_KEY: '' + OPENAI_API_KEY: '' + KNOWLEDGE_BASE_ID: '' worker: entrypoint: python manage.py qcluster restart: unless-stopped diff --git a/quickbooks_desktop_api/settings.py b/quickbooks_desktop_api/settings.py index d7579ca..b6907c9 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.spotlight' ] MIDDLEWARE = [ diff --git a/requirements.txt b/requirements.txt index d6669a8..e2a8ca4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ +# LLM Clients +boto3==1.35.14 +openai==1.44.0 + # Croniter package for djangoq croniter==1.3.8