diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa76d8d..ced36f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,12 +76,28 @@ jobs: sudo apt update sudo apt-get install -y mariadb-client + # - name: Setup Bench & DB + # run: | + # pip install frappe-bench<=15 + # bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench + # mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" + # mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + - name: Setup Bench & DB run: | - pip install frappe-bench - bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench - mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" - mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + pip install "frappe-bench<6" + bench init ~/frappe-bench \ + --frappe-branch version-15 \ + --skip-redis-config-generation \ + --skip-assets \ + --python "$(which python)" + + mariadb --host 127.0.0.1 --port 3306 -u root -proot \ + -e "SET GLOBAL character_set_server = 'utf8mb4'" + + mariadb --host 127.0.0.1 --port 3306 -u root -proot \ + -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + - name: Install App working-directory: /home/runner/frappe-bench diff --git a/next_ai/ai/parsing.py b/next_ai/ai/parsing.py new file mode 100644 index 0000000..b0608eb --- /dev/null +++ b/next_ai/ai/parsing.py @@ -0,0 +1,158 @@ +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"]) +def get_ai_parser_response(**kwargs): + """ + 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. + """ + 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() + return {"status_code":200, "status": "sucess", "message": parser_type_details} + + +class NextAIParsing: + """ + This class is mainly used to get the user input and parse the data inside the doctype fields. + """ + def __init__(self, api_data: dict): + self.api_data = api_data + + self.message = self.api_data.get("message") + + 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() + + 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 + + model_fields[field_details['field_name']] = {} + _type, default_description = fields_type_detail['type'], fields_type_detail['default_description'] + + model_fields[field_details['field_name']]['type'] = _type + model_fields[field_details['field_name']]['description'] = field_details.get('description') or default_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 + + 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 + + + def get_parsing_field_details(self) -> dict: + """ + Get the field details for a given parser field name. + """ + 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 abbb365..3c7eeca 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,20 @@ "HTML Editor": HTML_EDITOR, "Text Editor": TEXT_EDITOR, "Code": CODE, -} \ No newline at end of file + "JSON": JSON +} + + +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 42d5a8d..a06db14 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,226 @@ 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") + + +@frappe.whitelist() +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": "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": { + "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": True + }, + "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