From 1f9b6d25c5ee90f4d5a90308bf8efa8239624999 Mon Sep 17 00:00:00 2001 From: manikandanmohan10 Date: Sat, 8 Nov 2025 03:55:16 +0000 Subject: [PATCH 01/20] NextAI Parsing --- .../doctype/nextai_parsing/__init__.py | 0 .../doctype/nextai_parsing/nextai_parsing.js | 31 ++++++++++ .../nextai_parsing/nextai_parsing.json | 57 +++++++++++++++++++ .../doctype/nextai_parsing/nextai_parsing.py | 8 +++ .../nextai_parsing/test_nextai_parsing.py | 9 +++ .../nextai_parsing_details/__init__.py | 0 .../nextai_parsing_details.js | 8 +++ .../nextai_parsing_details.json | 54 ++++++++++++++++++ .../nextai_parsing_details.py | 8 +++ .../test_nextai_parsing_details.py | 9 +++ 10 files changed, 184 insertions(+) create mode 100644 next_ai/next_ai/doctype/nextai_parsing/__init__.py create mode 100644 next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.js create mode 100644 next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.json create mode 100644 next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.py create mode 100644 next_ai/next_ai/doctype/nextai_parsing/test_nextai_parsing.py create mode 100644 next_ai/next_ai/doctype/nextai_parsing_details/__init__.py create mode 100644 next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.js create mode 100644 next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.json create mode 100644 next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.py create mode 100644 next_ai/next_ai/doctype/nextai_parsing_details/test_nextai_parsing_details.py diff --git a/next_ai/next_ai/doctype/nextai_parsing/__init__.py b/next_ai/next_ai/doctype/nextai_parsing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.js b/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.js new file mode 100644 index 0000000..51264aa --- /dev/null +++ b/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.js @@ -0,0 +1,31 @@ +// Copyright (c) 2025, NextAI Team and contributors +// For license information, please see license.txt + +frappe.ui.form.on('NextAI Parsing', { + doc: function (frm) { + if (!frm.doc.doc) return; + + // Clear existing child rows before adding new ones + frm.clear_table('parsing_details'); + + // Fetch metadata of the selected Doctype + frappe.model.with_doctype(frm.doc.doc, function () { + const meta = frappe.get_meta(frm.doc.doc); + + // Loop through fields + meta.fields.forEach(field => { + // Skip Section/Column/Tab breaks + if (['Section Break', 'Column Break', 'Tab Break'].includes(field.fieldtype)) return; + + // Add child row + let child = frm.add_child('parsing_details'); + child.field_name = field.fieldname; + child.field_label = field.label; + child.field_type = field.fieldtype; + }); + + frm.refresh_field('parsing_details'); + frappe.msgprint(__('Fields added from Doctype: {0}', [frm.doc.doctype])); + }); + } +}); diff --git a/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.json b/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.json new file mode 100644 index 0000000..d9fb462 --- /dev/null +++ b/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.json @@ -0,0 +1,57 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-11-02 19:15:51.309467", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "doc", + "section_break_xxgza", + "parsing_details" + ], + "fields": [ + { + "fieldname": "doc", + "fieldtype": "Link", + "label": "Doctype", + "options": "DocType" + }, + { + "fieldname": "parsing_details", + "fieldtype": "Table", + "label": "Parsing Details", + "options": "NextAI Parsing Details" + }, + { + "fieldname": "section_break_xxgza", + "fieldtype": "Section Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-11-02 19:21:55.739921", + "modified_by": "Administrator", + "module": "Next AI", + "name": "NextAI Parsing", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.py b/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.py new file mode 100644 index 0000000..7b6e246 --- /dev/null +++ b/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.py @@ -0,0 +1,8 @@ +# Copyright (c) 2025, NextAI Team and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class NextAIParsing(Document): + pass diff --git a/next_ai/next_ai/doctype/nextai_parsing/test_nextai_parsing.py b/next_ai/next_ai/doctype/nextai_parsing/test_nextai_parsing.py new file mode 100644 index 0000000..eef7483 --- /dev/null +++ b/next_ai/next_ai/doctype/nextai_parsing/test_nextai_parsing.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, NextAI Team and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestNextAIParsing(FrappeTestCase): + pass diff --git a/next_ai/next_ai/doctype/nextai_parsing_details/__init__.py b/next_ai/next_ai/doctype/nextai_parsing_details/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.js b/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.js new file mode 100644 index 0000000..afd3f29 --- /dev/null +++ b/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, NextAI Team and contributors +// For license information, please see license.txt + +frappe.ui.form.on('NextAI Parsing Details', { + // refresh: function(frm) { + + // } +}); diff --git a/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.json b/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.json new file mode 100644 index 0000000..03e71c3 --- /dev/null +++ b/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.json @@ -0,0 +1,54 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-11-02 19:18:22.047341", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "field_name", + "field_label", + "field_type", + "description" + ], + "fields": [ + { + "fieldname": "field_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Field Name" + }, + { + "fieldname": "field_label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Field Label" + }, + { + "fieldname": "field_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Field Type" + }, + { + "fieldname": "description", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-11-02 19:22:58.615233", + "modified_by": "Administrator", + "module": "Next AI", + "name": "NextAI Parsing Details", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.py b/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.py new file mode 100644 index 0000000..09e2e6c --- /dev/null +++ b/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.py @@ -0,0 +1,8 @@ +# Copyright (c) 2025, NextAI Team and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class NextAIParsingDetails(Document): + pass diff --git a/next_ai/next_ai/doctype/nextai_parsing_details/test_nextai_parsing_details.py b/next_ai/next_ai/doctype/nextai_parsing_details/test_nextai_parsing_details.py new file mode 100644 index 0000000..5cd29b5 --- /dev/null +++ b/next_ai/next_ai/doctype/nextai_parsing_details/test_nextai_parsing_details.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, NextAI Team and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestNextAIParsingDetails(FrappeTestCase): + pass From d4b755c71a6f6390b426cd9a22b059c618d04f7c Mon Sep 17 00:00:00 2001 From: Samyuktha Mary Date: Tue, 11 Nov 2025 14:39:32 +0000 Subject: [PATCH 02/20] Parsing Demo API --- next_ai/ai/parsing.py | 18 ++++++++++++++++++ .../nextai_parsing_details.json | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 next_ai/ai/parsing.py diff --git a/next_ai/ai/parsing.py b/next_ai/ai/parsing.py new file mode 100644 index 0000000..b1abe93 --- /dev/null +++ b/next_ai/ai/parsing.py @@ -0,0 +1,18 @@ +import frappe + + +@frappe.whitelist(methods=["POST"]) +def get_ai_parser_response(**kwargs): + message = NextAIParsing().get_response() + return {"status_code":200, "status": "sucess", "message": message} + + +class NextAIParsing: + """ + This class is mainly used to get the user input and parse the data inside the doctype fields. + """ + def __init__(self): + pass + + def get_response(self): + return {'status': 'success', 'Description': 'Parsed Data'} \ No newline at end of file diff --git a/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.json b/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.json index 03e71c3..1f9636e 100644 --- a/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.json +++ b/next_ai/next_ai/doctype/nextai_parsing_details/nextai_parsing_details.json @@ -33,7 +33,7 @@ }, { "fieldname": "description", - "fieldtype": "Data", + "fieldtype": "Small Text", "in_list_view": 1, "label": "Description" } @@ -41,12 +41,13 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-11-02 19:22:58.615233", + "modified": "2025-11-10 21:47:56.285297", "modified_by": "Administrator", "module": "Next AI", "name": "NextAI Parsing Details", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], From 6eee41c82e4838e54f7648ce208f1d1945702f55 Mon Sep 17 00:00:00 2001 From: Samyuktha Mary Date: Tue, 11 Nov 2025 14:44:35 +0000 Subject: [PATCH 03/20] response for todo --- next_ai/ai/parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next_ai/ai/parsing.py b/next_ai/ai/parsing.py index b1abe93..5534af9 100644 --- a/next_ai/ai/parsing.py +++ b/next_ai/ai/parsing.py @@ -15,4 +15,4 @@ def __init__(self): pass def get_response(self): - return {'status': 'success', 'Description': 'Parsed Data'} \ No newline at end of file + return {'status': 'Open', 'description': 'Parsed Data', 'priority': 'High'} \ No newline at end of file From fc3ab1bc57593c197569e37976791abde6cc127f Mon Sep 17 00:00:00 2001 From: Samyuktha Mary Date: Thu, 20 Nov 2025 10:19:25 +0000 Subject: [PATCH 04/20] BUG: JSON Field Added --- next_ai/ai/prompt.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/next_ai/ai/prompt.py b/next_ai/ai/prompt.py index abbb365..dcc775b 100644 --- a/next_ai/ai/prompt.py +++ b/next_ai/ai/prompt.py @@ -93,6 +93,19 @@ {input} """ +JSON = """ +You are a software enginer. You generate data in the JSON Format Only. Follow the instructions below: + +# Instructions +1. Read the User Input carefully. +2. Generate a response that is relevant to the User Input. +3. Ensure that the response is formatted in the JSON Only Strictly. +4. Do not include any additional text or explanations outside of the code block. +5. Follow the best Indentation and coding practices. + +# User Input +{input} +""" PROMPTS = { "Markdown Editor": MARKDOWN, @@ -102,4 +115,5 @@ "HTML Editor": HTML_EDITOR, "Text Editor": TEXT_EDITOR, "Code": CODE, + "JSON": JSON } \ No newline at end of file From 6762f5bf75634335edf34ef40a8150a92ddc9ddb Mon Sep 17 00:00:00 2001 From: Samyuktha Mary Date: Thu, 20 Nov 2025 12:20:26 +0000 Subject: [PATCH 05/20] Parsing fields & Dynamic Parsing Method --- next_ai/ai/parsing.py | 48 ++++++++- next_ai/ai/utils.py | 224 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+), 1 deletion(-) diff --git a/next_ai/ai/parsing.py b/next_ai/ai/parsing.py index 5534af9..344761c 100644 --- a/next_ai/ai/parsing.py +++ b/next_ai/ai/parsing.py @@ -1,4 +1,6 @@ import frappe +from pydantic import BaseModel, Field, create_model +from next_ai.ai.utils import get_parser_type_details @frappe.whitelist(methods=["POST"]) @@ -6,6 +8,11 @@ def get_ai_parser_response(**kwargs): message = NextAIParsing().get_response() return {"status_code":200, "status": "sucess", "message": message} +@frappe.whitelist(methods=["GET"]) +def get_parser_fieldtype_details(): + parser_type_details = get_parser_type_details() + return {"status_code":200, "status": "sucess", "message": parser_type_details} + class NextAIParsing: """ @@ -15,4 +22,43 @@ def __init__(self): pass def get_response(self): - return {'status': 'Open', 'description': 'Parsed Data', 'priority': 'High'} \ No newline at end of file + return {'status': 'Open', 'description': 'Parsed Data', 'priority': 'High'} + + + def build_dynamic_parser(fields: dict): + model_fields = {} + + for name, meta in fields.items(): + field_type = meta["type"] + description = meta.get("description", "") + + # Add field with default None (or False for bool) + default_value = False if field_type is bool else None + + model_fields[name] = (field_type, Field(default_value, description=description)) + + # dynamically create a pydantic model + DynamicBaseParser = create_model("DynamicBaseParser", **model_fields) + return DynamicBaseParser + + def test_dynamic_parser(self): + fields = { + "status": {"type": str, "description": "Status of the task"}, + "description": {"type": str, "description": "Description of the task"}, + "priority": {"type": str, "description": "Priority level"}, + "is_completed": {"type": bool, "description": "Completion status"} + } + + DynamicParser = self.build_dynamic_parser(fields) + + # Example usage + example_data = { + "status": "Open", + "description": "This is a test task.", + "priority": "High", + "is_completed": False + } + + parsed_instance = DynamicParser(**example_data) + frappe.logger().info(f"Parsed Instance: {parsed_instance}") + return parsed_instance \ No newline at end of file diff --git a/next_ai/ai/utils.py b/next_ai/ai/utils.py index 42d5a8d..8d33e88 100644 --- a/next_ai/ai/utils.py +++ b/next_ai/ai/utils.py @@ -1,5 +1,7 @@ import frappe from frappe.utils import now_datetime +from datetime import date, datetime + def nextai_usage_log_create(**kwargs): @@ -14,3 +16,225 @@ def nextai_usage_log_create(**kwargs): doc.insert(ignore_permissions=True) except Exception as e: frappe.log_error(frappe.get_traceback(), "Nextai Usage Log Create Failed") + + +def get_parser_type_details() -> dict: + parser_type_details = { + "Autocomplete": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Attach": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Attach Image": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Barcode": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Button": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Check": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Code": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Color": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Column Break": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Currency": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Data": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": True + }, + "Date": { + "type": date, + "default_description": "A calendar date in YYYY-MM-DD format.", + "is_active": True + }, + "Datetime": { + "type": datetime, + "default_description": "A full timestamp in YYYY-MM-DD HH:MM:SS format.", + "is_active": True + }, + "Duration": { + "type": int, + "default_description": "Duration value represented in total seconds (e.g., 1 hour = 3600 seconds).", + "is_active": False + }, + "Dynamic Link": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Float": { + "type": float, + "default_description": "A floating-point number with decimal precision (e.g., 3.142).", + "is_active": True + }, + "Fold": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Geolocation": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Heading": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "HTML": { + "type": str, + "default_description": "HTML content supported with Bootstrap styling and components.", + "is_active": True + }, + "HTML Editor": { + "type": str, + "default_description": "HTML content supported with Bootstrap styling and components.", + "is_active": True + }, + "Icon": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Image": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Int": { + "type": int, + "default_description": "An integer value without decimals.", + "is_active": True + }, + "JSON": { + "type": str, + "default_description": "A valid JSON string.", + "is_active": False + }, + "Link": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Long Text": { + "type": str, + "default_description": "A long text value.", + "is_active": True + }, + "Markdown Editor": { + "type": str, + "default_description": "A markdown formatted text value.", + "is_active": True + }, + "Password": { + "type": str, + "default_description": "A secure password string.", + "is_active": True + }, + "Percent": { + "type": float, + "default_description": "A percentage value represented as a number.", + "is_active": False + }, + "Phone": { + "type": str, + "default_description": "A phone number including the country code. Default country code is +91. example format +91-18001234567", + "is_active": True + }, + "Read Only": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Rating": { + "type": str, + "default_description": "A rating value between 0.0 and 1.0 in increments of 0.1 (allowed: 0.0, 0.1, 0.2 ... 0.9, 1.0).", + "is_active": True + }, + "Section Break": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Select": { + "type": str, + "default_description": "Select one value from the predefined options (case-sensitive; must match exactly). The options are: Option 1, Option 2, Option 3.", + "is_active": True + }, + "Signature": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Small Text": { + "type": str, + "default_description": "Provide a short single-line text value.", + "is_active": True + }, + "Tab Break": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Table": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Table MultiSelect": { + "type": str, + "default_description": "Get appropriate data for the field", + "is_active": False + }, + "Text": { + "type": str, + "default_description": "Provide multi-line plain text content.", + "is_active": True + }, + "Text Editor": { + "type": str, + "default_description": "Provide rich-text HTML content produced by a Quill editor. Example HTML

Hi I'm an example content, This is BOLD

", + "is_active": True + }, + "Time": { + "type": str, + "default_description": "Provide a valid time value in HH:MM:SS (24-hour) format. Example: 23:59:59", + "is_active": True + } + } + return parser_type_details + From 21a20caace84bcbb420a0bba27fbff9459a02e65 Mon Sep 17 00:00:00 2001 From: Samyuktha Mary Date: Mon, 24 Nov 2025 15:51:28 +0000 Subject: [PATCH 06/20] parser update --- next_ai/ai/parsing.py | 22 +++++++++++++++++++--- next_ai/ai/utils.py | 4 ++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/next_ai/ai/parsing.py b/next_ai/ai/parsing.py index 344761c..c4c1041 100644 --- a/next_ai/ai/parsing.py +++ b/next_ai/ai/parsing.py @@ -5,7 +5,12 @@ @frappe.whitelist(methods=["POST"]) def get_ai_parser_response(**kwargs): - message = NextAIParsing().get_response() + """ + doctype: str - The name of the doctype to parse data for. + name: str - The name of the document instance. + message: str - The user input message to be parsed. + """ + message = NextAIParsing().get_response(kwargs) return {"status_code":200, "status": "sucess", "message": message} @frappe.whitelist(methods=["GET"]) @@ -21,7 +26,11 @@ class NextAIParsing: def __init__(self): pass - def get_response(self): + def get_response(self, data) -> dict: + doctype = data.get("doctype") + doctype_name = data.get("name") + message = data.get("message") + return {'status': 'Open', 'description': 'Parsed Data', 'priority': 'High'} @@ -61,4 +70,11 @@ def test_dynamic_parser(self): parsed_instance = DynamicParser(**example_data) frappe.logger().info(f"Parsed Instance: {parsed_instance}") - return parsed_instance \ No newline at end of file + return parsed_instance + + + def get_parsing_field_details(self, name) -> dict: + """ + Get the field details for a given parser field name. + """ + pass \ No newline at end of file diff --git a/next_ai/ai/utils.py b/next_ai/ai/utils.py index 8d33e88..47ce8a9 100644 --- a/next_ai/ai/utils.py +++ b/next_ai/ai/utils.py @@ -58,7 +58,7 @@ def get_parser_type_details() -> dict: "Color": { "type": str, "default_description": "Get appropriate data for the field", - "is_active": False + "is_active": True }, "Column Break": { "type": str, @@ -88,7 +88,7 @@ def get_parser_type_details() -> dict: "Duration": { "type": int, "default_description": "Duration value represented in total seconds (e.g., 1 hour = 3600 seconds).", - "is_active": False + "is_active": True }, "Dynamic Link": { "type": str, From 602b9dfae0911928c39dd5d7f932ded9b62c684c Mon Sep 17 00:00:00 2001 From: Samyuktha Mary Date: Sun, 30 Nov 2025 07:31:34 +0000 Subject: [PATCH 07/20] Parsing Backend MVP --- next_ai/ai/parsing.py | 114 +++++++++++++++++++++++++++++++++++------- next_ai/ai/prompt.py | 17 ++++++- next_ai/ai/utils.py | 2 +- 3 files changed, 113 insertions(+), 20 deletions(-) diff --git a/next_ai/ai/parsing.py b/next_ai/ai/parsing.py index c4c1041..b0608eb 100644 --- a/next_ai/ai/parsing.py +++ b/next_ai/ai/parsing.py @@ -1,6 +1,10 @@ import frappe +import os +from langchain_google_genai import ChatGoogleGenerativeAI +from frappe import _ from pydantic import BaseModel, Field, create_model from next_ai.ai.utils import get_parser_type_details +from next_ai.ai.prompt import PARSING_PROMPT @frappe.whitelist(methods=["POST"]) @@ -10,9 +14,19 @@ def get_ai_parser_response(**kwargs): name: str - The name of the document instance. message: str - The user input message to be parsed. """ - message = NextAIParsing().get_response(kwargs) + data = frappe._dict(kwargs) + + # Mandatory fields + required_fields = ["doctype", "name", "message"] + + missing = [f for f in required_fields if not data.get(f)] + if missing: + frappe.throw(f"Missing mandatory fields: {', '.join(missing)}") + + message = NextAIParsing(api_data=data).get_response() return {"status_code":200, "status": "sucess", "message": message} + @frappe.whitelist(methods=["GET"]) def get_parser_fieldtype_details(): parser_type_details = get_parser_type_details() @@ -23,29 +37,83 @@ class NextAIParsing: """ This class is mainly used to get the user input and parse the data inside the doctype fields. """ - def __init__(self): - pass + def __init__(self, api_data: dict): + self.api_data = api_data - def get_response(self, data) -> dict: - doctype = data.get("doctype") - doctype_name = data.get("name") - message = data.get("message") + self.message = self.api_data.get("message") - return {'status': 'Open', 'description': 'Parsed Data', 'priority': 'High'} - + self.parsing_fields = self.get_parsing_field_details() + self.field_meta = get_parser_type_details() + + self.nextai_settings = self.get_nextai_settings() + self.validate_settings() - def build_dynamic_parser(fields: dict): + self.current_model = self.nextai_settings.model_name + + def validate_token(self): + if len(self.api_data['message']) > 8000: + frappe.throw(_("Prompt length exceeds the maximum limit of 8000 characters. Please shorten your prompt.")) + + def validate_settings(self): + if not self.nextai_settings.model_name: + frappe.throw(_("Model name is not set in NextAI Settings. Please configure the model name.")) + if not self.nextai_settings.platform: + frappe.throw(_("Platform is not set in NextAI Settings. Please configure the platform.")) + if not self.nextai_settings.get_password("api_key"): + frappe.throw(_("API Key is not set in NextAI Settings. Please configure the API Key.")) + + def get_nextai_settings(self): + nextai_settings = frappe.get_doc('NextAI Settings') + return nextai_settings + + def get_llm(self, model_name: str = None): + model_name = model_name or self.nextai_settings.model_name + try: + if self.nextai_settings.platform == 'Gemini': + os.environ['GOOGLE_API_KEY'] = self.nextai_settings.get_password("api_key") + llm = ChatGoogleGenerativeAI(model=model_name) + return llm + except Exception as e: + frappe.log_error(frappe.get_traceback(), "Error in NextAIParsing.get_llm") + frappe.throw(_("Error in getting LLM: {0}").format(str(e))) + + def get_structured_output_llm(self, model_name: str = None, base_model: BaseModel = None): + llm = self.get_llm(model_name=model_name) + so_llm = llm.with_structured_output(base_model) + return so_llm + + def get_prompt_message(self) -> str: + prompt = PARSING_PROMPT.format(input=self.message) + return prompt + + def get_response(self) -> dict: + DynamicBaseParser = self.build_dynamic_parser() + so_llm = self.get_structured_output_llm(model_name=self.current_model, base_model=DynamicBaseParser) + message = self.get_prompt_message() + response = so_llm.invoke(message) + response = response.__dict__ + return response + + def build_dynamic_parser(self): model_fields = {} + + for field_details in self.parsing_fields: + fields_type_detail = self.field_meta.get(field_details['field_type']) + if not fields_type_detail['is_active']: + continue - for name, meta in fields.items(): - field_type = meta["type"] - description = meta.get("description", "") + model_fields[field_details['field_name']] = {} + _type, default_description = fields_type_detail['type'], fields_type_detail['default_description'] - # Add field with default None (or False for bool) - default_value = False if field_type is bool else None + model_fields[field_details['field_name']]['type'] = _type + model_fields[field_details['field_name']]['description'] = field_details.get('description') or default_description - model_fields[name] = (field_type, Field(default_value, description=description)) + default_value = None + model_fields[field_details['field_name']] = ( + _type, + Field(default_value, description=model_fields[field_details['field_name']]['description']) + ) # dynamically create a pydantic model DynamicBaseParser = create_model("DynamicBaseParser", **model_fields) return DynamicBaseParser @@ -73,8 +141,18 @@ def test_dynamic_parser(self): return parsed_instance - def get_parsing_field_details(self, name) -> dict: + def get_parsing_field_details(self) -> dict: """ Get the field details for a given parser field name. """ - pass \ No newline at end of file + data = frappe.db.get_all( + "NextAI Parsing Details", + filters={ + "parenttype": "NextAI Parsing", + "parentfield": "parsing_details", + "parent": self.api_data.get("name"), + }, + fields=['*'], + order_by='idx' + ) + return data \ No newline at end of file diff --git a/next_ai/ai/prompt.py b/next_ai/ai/prompt.py index dcc775b..3c7eeca 100644 --- a/next_ai/ai/prompt.py +++ b/next_ai/ai/prompt.py @@ -116,4 +116,19 @@ "Text Editor": TEXT_EDITOR, "Code": CODE, "JSON": JSON -} \ No newline at end of file +} + + +PARSING_PROMPT = """ +You are an expert data parser. Your task is to extract and structure information based on the provided field definitions. Follow the instructions below: + +# Instructions +1. Read the Field Definitions carefully. +2. Read the User Input carefully. +3. Extract relevant information from the User Input based on the Field Definitions. +4. Ensure the extracted information more grammatically correct and contextually relevant. +5. If any date related incomplete information contains, use current time infomration to fill the missing details. + +# User Input +{input} +""" \ No newline at end of file diff --git a/next_ai/ai/utils.py b/next_ai/ai/utils.py index 47ce8a9..0a2f264 100644 --- a/next_ai/ai/utils.py +++ b/next_ai/ai/utils.py @@ -57,7 +57,7 @@ def get_parser_type_details() -> dict: }, "Color": { "type": str, - "default_description": "Get appropriate data for the field", + "default_description": "Apply color codes dynamically so that higher priorities shift toward red/orange, mid priorities toward yellow, and lower priorities toward green (#29CD42).", "is_active": True }, "Column Break": { From b30692b9d2577ca38b675a64e97cc765a6e2f3d6 Mon Sep 17 00:00:00 2001 From: Samyuktha Mary Date: Sun, 30 Nov 2025 07:31:34 +0000 Subject: [PATCH 08/20] Parsing Backend MVP --- next_ai/ai/parsing.py | 114 +++++++++++++++++++++++++++++++++++------- next_ai/ai/prompt.py | 17 ++++++- next_ai/ai/utils.py | 2 +- 3 files changed, 113 insertions(+), 20 deletions(-) diff --git a/next_ai/ai/parsing.py b/next_ai/ai/parsing.py index c4c1041..b0608eb 100644 --- a/next_ai/ai/parsing.py +++ b/next_ai/ai/parsing.py @@ -1,6 +1,10 @@ import frappe +import os +from langchain_google_genai import ChatGoogleGenerativeAI +from frappe import _ from pydantic import BaseModel, Field, create_model from next_ai.ai.utils import get_parser_type_details +from next_ai.ai.prompt import PARSING_PROMPT @frappe.whitelist(methods=["POST"]) @@ -10,9 +14,19 @@ def get_ai_parser_response(**kwargs): name: str - The name of the document instance. message: str - The user input message to be parsed. """ - message = NextAIParsing().get_response(kwargs) + data = frappe._dict(kwargs) + + # Mandatory fields + required_fields = ["doctype", "name", "message"] + + missing = [f for f in required_fields if not data.get(f)] + if missing: + frappe.throw(f"Missing mandatory fields: {', '.join(missing)}") + + message = NextAIParsing(api_data=data).get_response() return {"status_code":200, "status": "sucess", "message": message} + @frappe.whitelist(methods=["GET"]) def get_parser_fieldtype_details(): parser_type_details = get_parser_type_details() @@ -23,29 +37,83 @@ class NextAIParsing: """ This class is mainly used to get the user input and parse the data inside the doctype fields. """ - def __init__(self): - pass + def __init__(self, api_data: dict): + self.api_data = api_data - def get_response(self, data) -> dict: - doctype = data.get("doctype") - doctype_name = data.get("name") - message = data.get("message") + self.message = self.api_data.get("message") - return {'status': 'Open', 'description': 'Parsed Data', 'priority': 'High'} - + self.parsing_fields = self.get_parsing_field_details() + self.field_meta = get_parser_type_details() + + self.nextai_settings = self.get_nextai_settings() + self.validate_settings() - def build_dynamic_parser(fields: dict): + self.current_model = self.nextai_settings.model_name + + def validate_token(self): + if len(self.api_data['message']) > 8000: + frappe.throw(_("Prompt length exceeds the maximum limit of 8000 characters. Please shorten your prompt.")) + + def validate_settings(self): + if not self.nextai_settings.model_name: + frappe.throw(_("Model name is not set in NextAI Settings. Please configure the model name.")) + if not self.nextai_settings.platform: + frappe.throw(_("Platform is not set in NextAI Settings. Please configure the platform.")) + if not self.nextai_settings.get_password("api_key"): + frappe.throw(_("API Key is not set in NextAI Settings. Please configure the API Key.")) + + def get_nextai_settings(self): + nextai_settings = frappe.get_doc('NextAI Settings') + return nextai_settings + + def get_llm(self, model_name: str = None): + model_name = model_name or self.nextai_settings.model_name + try: + if self.nextai_settings.platform == 'Gemini': + os.environ['GOOGLE_API_KEY'] = self.nextai_settings.get_password("api_key") + llm = ChatGoogleGenerativeAI(model=model_name) + return llm + except Exception as e: + frappe.log_error(frappe.get_traceback(), "Error in NextAIParsing.get_llm") + frappe.throw(_("Error in getting LLM: {0}").format(str(e))) + + def get_structured_output_llm(self, model_name: str = None, base_model: BaseModel = None): + llm = self.get_llm(model_name=model_name) + so_llm = llm.with_structured_output(base_model) + return so_llm + + def get_prompt_message(self) -> str: + prompt = PARSING_PROMPT.format(input=self.message) + return prompt + + def get_response(self) -> dict: + DynamicBaseParser = self.build_dynamic_parser() + so_llm = self.get_structured_output_llm(model_name=self.current_model, base_model=DynamicBaseParser) + message = self.get_prompt_message() + response = so_llm.invoke(message) + response = response.__dict__ + return response + + def build_dynamic_parser(self): model_fields = {} + + for field_details in self.parsing_fields: + fields_type_detail = self.field_meta.get(field_details['field_type']) + if not fields_type_detail['is_active']: + continue - for name, meta in fields.items(): - field_type = meta["type"] - description = meta.get("description", "") + model_fields[field_details['field_name']] = {} + _type, default_description = fields_type_detail['type'], fields_type_detail['default_description'] - # Add field with default None (or False for bool) - default_value = False if field_type is bool else None + model_fields[field_details['field_name']]['type'] = _type + model_fields[field_details['field_name']]['description'] = field_details.get('description') or default_description - model_fields[name] = (field_type, Field(default_value, description=description)) + default_value = None + model_fields[field_details['field_name']] = ( + _type, + Field(default_value, description=model_fields[field_details['field_name']]['description']) + ) # dynamically create a pydantic model DynamicBaseParser = create_model("DynamicBaseParser", **model_fields) return DynamicBaseParser @@ -73,8 +141,18 @@ def test_dynamic_parser(self): return parsed_instance - def get_parsing_field_details(self, name) -> dict: + def get_parsing_field_details(self) -> dict: """ Get the field details for a given parser field name. """ - pass \ No newline at end of file + data = frappe.db.get_all( + "NextAI Parsing Details", + filters={ + "parenttype": "NextAI Parsing", + "parentfield": "parsing_details", + "parent": self.api_data.get("name"), + }, + fields=['*'], + order_by='idx' + ) + return data \ No newline at end of file diff --git a/next_ai/ai/prompt.py b/next_ai/ai/prompt.py index dcc775b..3c7eeca 100644 --- a/next_ai/ai/prompt.py +++ b/next_ai/ai/prompt.py @@ -116,4 +116,19 @@ "Text Editor": TEXT_EDITOR, "Code": CODE, "JSON": JSON -} \ No newline at end of file +} + + +PARSING_PROMPT = """ +You are an expert data parser. Your task is to extract and structure information based on the provided field definitions. Follow the instructions below: + +# Instructions +1. Read the Field Definitions carefully. +2. Read the User Input carefully. +3. Extract relevant information from the User Input based on the Field Definitions. +4. Ensure the extracted information more grammatically correct and contextually relevant. +5. If any date related incomplete information contains, use current time infomration to fill the missing details. + +# User Input +{input} +""" \ No newline at end of file diff --git a/next_ai/ai/utils.py b/next_ai/ai/utils.py index 47ce8a9..0a2f264 100644 --- a/next_ai/ai/utils.py +++ b/next_ai/ai/utils.py @@ -57,7 +57,7 @@ def get_parser_type_details() -> dict: }, "Color": { "type": str, - "default_description": "Get appropriate data for the field", + "default_description": "Apply color codes dynamically so that higher priorities shift toward red/orange, mid priorities toward yellow, and lower priorities toward green (#29CD42).", "is_active": True }, "Column Break": { From 412c2e9711713832f15fd0a53782ecff15e50227 Mon Sep 17 00:00:00 2001 From: manikandanmohan10 Date: Mon, 1 Dec 2025 03:15:08 +0000 Subject: [PATCH 09/20] NextAI Parsing UI changes --- next_ai/ai/utils.py | 1 + .../doctype/nextai_parsing/nextai_parsing.js | 49 +++--- next_ai/public/js/next_ai.js | 149 ++++++++++++++++++ 3 files changed, 180 insertions(+), 19 deletions(-) diff --git a/next_ai/ai/utils.py b/next_ai/ai/utils.py index 0a2f264..a06db14 100644 --- a/next_ai/ai/utils.py +++ b/next_ai/ai/utils.py @@ -18,6 +18,7 @@ def nextai_usage_log_create(**kwargs): frappe.log_error(frappe.get_traceback(), "Nextai Usage Log Create Failed") +@frappe.whitelist() def get_parser_type_details() -> dict: parser_type_details = { "Autocomplete": { diff --git a/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.js b/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.js index 51264aa..6dacfc9 100644 --- a/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.js +++ b/next_ai/next_ai/doctype/nextai_parsing/nextai_parsing.js @@ -1,31 +1,42 @@ -// Copyright (c) 2025, NextAI Team and contributors -// For license information, please see license.txt - frappe.ui.form.on('NextAI Parsing', { doc: function (frm) { if (!frm.doc.doc) return; - // Clear existing child rows before adding new ones frm.clear_table('parsing_details'); - // Fetch metadata of the selected Doctype - frappe.model.with_doctype(frm.doc.doc, function () { - const meta = frappe.get_meta(frm.doc.doc); + // Fetch parser-type details first + frappe.call({ + method: "next_ai.ai.utils.get_parser_type_details", + callback: function(r) { + const parser_map = r.message || {}; + + // Fetch metadata of selected Doctype + frappe.model.with_doctype(frm.doc.doc, function () { + const meta = frappe.get_meta(frm.doc.doc); + + meta.fields.forEach(field => { + // skip breaks + if (['Section Break', 'Column Break', 'Tab Break'].includes(field.fieldtype)) + return; + + // get parser rules for this field.type + const rule = parser_map[field.fieldtype]; - // Loop through fields - meta.fields.forEach(field => { - // Skip Section/Column/Tab breaks - if (['Section Break', 'Column Break', 'Tab Break'].includes(field.fieldtype)) return; + // skip if rule not found OR inactive + if (!rule || !rule.is_active) return; - // Add child row - let child = frm.add_child('parsing_details'); - child.field_name = field.fieldname; - child.field_label = field.label; - child.field_type = field.fieldtype; - }); + // Add child row + let child = frm.add_child('parsing_details'); + child.field_name = field.fieldname; + child.field_label = field.label; + child.field_type = field.fieldtype; + child.description = rule.default_description; + }); - frm.refresh_field('parsing_details'); - frappe.msgprint(__('Fields added from Doctype: {0}', [frm.doc.doctype])); + frm.refresh_field('parsing_details'); + frappe.msgprint(__('Active fields added from Doctype: {0}', [frm.doc.doc])); + }); + } }); } }); diff --git a/next_ai/public/js/next_ai.js b/next_ai/public/js/next_ai.js index 18506dd..b1ceba2 100644 --- a/next_ai/public/js/next_ai.js +++ b/next_ai/public/js/next_ai.js @@ -245,3 +245,152 @@ function typeText($target, text, type, callback) { requestAnimationFrame(step); } + + +function injectGlobalNextAIButton() { + const headerSelector = '.page-head .page-title'; + + const observer = new MutationObserver(() => { + const header = document.querySelector(headerSelector); + if (!header) return; + + // Only add button in FORM VIEW + const route = frappe.get_route(); + if (!route || route[0] !== "Form") return; + + // Avoid duplicate + if (document.querySelector('#nextai-global-btn')) return; + + console.log("Injecting Fancy NextAI Button…"); + + // ------------ Your Fancy Button ------------- // + const $icon = $('