From b78c4b71a5900becc2fa9a9ca7234187ce6d1c8d Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 19 Dec 2025 12:07:14 +0530 Subject: [PATCH 1/3] handling plg feedback --- .../feedback_consumer/feedback_consumer.py | 146 +++++++++++++++++- tap_lms/imgana/submission.py | 12 +- .../doctype/imgsubmission/imgsubmission.js | 49 +++++- .../doctype/imgsubmission/imgsubmission.json | 117 ++++++++++++-- 4 files changed, 302 insertions(+), 22 deletions(-) diff --git a/tap_lms/feedback_consumer/feedback_consumer.py b/tap_lms/feedback_consumer/feedback_consumer.py index e8deb2e2..8888f8ca 100644 --- a/tap_lms/feedback_consumer/feedback_consumer.py +++ b/tap_lms/feedback_consumer/feedback_consumer.py @@ -181,7 +181,8 @@ def process_message(self, ch, method, properties, body): try: message_data = json.loads(body) submission_id = message_data.get("submission_id") - + print(f"Processing feedback for : {submission_id}") + if not submission_id: raise ValueError("Missing submission_id in message") @@ -266,7 +267,7 @@ def is_retryable_error(self, error): # All other errors are considered retryable (database locks, network issues, etc.) return True - def update_submission(self, message_data: Dict): + def update_submission_old(self, message_data: Dict): """Update ImgSubmission with feedback data - FIXED to handle correct grade path""" try: submission_id = message_data["submission_id"] @@ -330,6 +331,147 @@ def update_submission(self, message_data: Dict): frappe.logger().error(f"Error updating ImgSubmission {submission_id}: {str(e)}") raise + + def update_submission(self, message_data: Dict): + """Update ImgSubmission with comprehensive plagiarism data""" + try: + + submission_id = message_data["submission_id"] + feedback_data = message_data.get("feedback", {}) + + # Get submission document + submission = frappe.get_doc("ImgSubmission", submission_id) + print(f"Updating submission : {submission_id}") + + # Extract plagiarism data + is_plagiarized = message_data.get("is_plagiarized", False) + is_ai_generated = message_data.get("is_ai_generated", False) + match_type = message_data.get("match_type", "original") + plagiarism_source = message_data.get("plagiarism_source", "none") + similarity_score = message_data.get("similarity_score", 0.0) + ai_detection_source = message_data.get("ai_detection_source") + ai_confidence = message_data.get("ai_confidence", 0.0) + similar_sources = message_data.get("similar_sources", []) + + # Determine plagiarism_status + plagiarism_status = self._determine_plagiarism_status( + is_plagiarized, is_ai_generated, match_type, plagiarism_source + ) + + # Determine result_status + result_status = self._determine_result_status(is_plagiarized, is_ai_generated) + + # Extract grade + grade = self._extract_grade(feedback_data, submission_id) + + # Prepare update data + update_data = { + "status": "Completed", + "result_status": result_status, + "completed_at": datetime.now(), + + # Plagiarism fields + "plagiarism_status": plagiarism_status, + "is_plagiarized": is_plagiarized, + "match_type": match_type, + "plagiarism_source": plagiarism_source, + "similarity_score": similarity_score * 100, + "similar_sources": json.dumps(similar_sources), + + # AI detection fields + "is_ai_generated": is_ai_generated, + "ai_detection_source": ai_detection_source or "", + "ai_confidence": ai_confidence * 100, + + # Feedback fields + "grade": grade, + "overall_feedback": feedback_data.get("overall_feedback", ""), + "generated_feedback": json.dumps(feedback_data), + "feedback_summary": message_data.get("summary", ""), + "plagiarism_result": message_data.get("plagiarism_score", 0), + + } + + submission.update(update_data) + submission.save(ignore_permissions=True) + frappe.db.commit() + + except Exception as e: + # Update result_status to Failed on error + self._mark_submission_failed(submission_id, str(e)) + frappe.logger().error(f"Error updating ImgSubmission: {str(e)}") + raise + + def _determine_result_status(self, is_plagiarized: bool, is_ai_generated: bool) -> str: + """Determine overall result status""" + if is_plagiarized or is_ai_generated: + return "Success - Flagged" + return "Success - Original" + + def _mark_submission_failed(self, submission_id: str, error_message: str): + """Mark submission as failed""" + try: + submission = frappe.get_doc("ImgSubmission", submission_id) + submission.status = "Failed" + + # Add error message if field exists + if hasattr(submission, 'error_message'): + submission.error_message = error_message[:500] # Limit length to prevent field overflow + + submission.save(ignore_permissions=True) + + frappe.logger().error(f"Marked submission {submission_id} as failed: {error_message}") + + except Exception as e: + frappe.logger().error(f"Error marking submission {submission_id} as failed: {str(e)}") + + def _determine_plagiarism_status( + self, is_plagiarized, is_ai_generated, match_type, plagiarism_source + ) -> str: + """Determine human-readable plagiarism status""" + + if is_ai_generated: + return "Flagged - AI Generated" + + if not is_plagiarized: + if match_type == "resubmission_allowed": + return "Resubmission Allowed" + return "Original" + + status_map = { + "exact_duplicate": "Flagged - Exact Match", + "near_duplicate": "Flagged - Near Duplicate", + "semantic_match": "Flagged - Semantic Match", + } + + if match_type in status_map: + return status_map[match_type] + + if plagiarism_source in ["peer", "peer_collusion"]: + return "Flagged - Peer Plagiarism" + elif plagiarism_source in ["self_cross_assignment", "self_late_resubmission"]: + return "Flagged - Self Plagiarism" + + return "Flagged - Exact Match" + + def _extract_grade(self, feedback_data, submission_id): + grade_recommendation = feedback_data.get("grade_recommendation", "0") + + try: + if isinstance(grade_recommendation, str): + # Remove any non-numeric characters except decimal point + grade_clean = ''.join(c for c in grade_recommendation if c.isdigit() or c == '.') + grade = float(grade_clean) if grade_clean else 0.0 + else: + grade = float(grade_recommendation) + except (ValueError, TypeError): + grade = 0.0 + frappe.logger().warning(f"Could not parse grade '{grade_recommendation}' for submission {submission_id}, using 0.0") + + return grade + + + def send_glific_notification(self, message_data: Dict): """Send feedback notification via Glific with proper error handling""" try: diff --git a/tap_lms/imgana/submission.py b/tap_lms/imgana/submission.py index 7deeb395..09cb54f1 100644 --- a/tap_lms/imgana/submission.py +++ b/tap_lms/imgana/submission.py @@ -216,7 +216,9 @@ def enqueue_submission(submission_id): "submission_id": submission.name, "assign_id": submission.assign_id, "student_id": submission.student_id, - "img_url": submission.img_url # This is now the GCS public URL + "img_url": submission.img_url, # This is now the GCS public URL + # Optional: Add metadata for better detection + "created_at": str(submission.created_at) } # Get RabbitMQ settings from DocType @@ -237,7 +239,13 @@ def enqueue_submission(submission_id): channel = connection.channel() # Declare the queue - channel.queue_declare(queue=rabbitmq_config['queue']) + try: + # First try passive declaration to check if queue exists + channel.queue_declare(queue=rabbitmq_config['queue'],durable=True,passive=True) + except Exception: + # If it doesn't exist, declare it + channel.queue_declare(queue=rabbitmq_config['queue'], durable=True) + # Publish the message to the queue channel.basic_publish( diff --git a/tap_lms/tap_lms/doctype/imgsubmission/imgsubmission.js b/tap_lms/tap_lms/doctype/imgsubmission/imgsubmission.js index 897a290c..2a10ab0d 100644 --- a/tap_lms/tap_lms/doctype/imgsubmission/imgsubmission.js +++ b/tap_lms/tap_lms/doctype/imgsubmission/imgsubmission.js @@ -1,8 +1,45 @@ -// Copyright (c) 2024, Techt4dev and contributors -// For license information, please see license.txt +frappe.listview_settings['ImgSubmission'] = { + add_fields: ["result_status", "plagiarism_status", "is_plagiarized", "is_ai_generated", "grade"], -frappe.ui.form.on('ImgSubmission', { - // refresh: function(frm) { + get_indicator: function(doc) { + // Primary indicator based on result_status + const result_status_map = { + "Pending": ["orange", "Pending"], + "Success - Original": ["green", "✓ Original"], + "Success - Flagged": ["red", "⚠ Flagged"], + "Failed": ["darkgrey", "✗ Failed"] + }; - // } -}); + const [color, label] = result_status_map[doc.result_status] || ["grey", "Unknown"]; + return [__(label), color, `result_status,=,${doc.result_status}`]; + }, + + formatters: { + result_status: function(value) { + const badges = { + "Pending": '⏳ Pending', + "Success - Original": '✓ Original', + "Success - Flagged": '⚠ Flagged', + "Failed": '✗ Failed' + }; + return badges[value] || value; + }, + + plagiarism_status: function(value) { + const colors = { + "Not Checked": "secondary", + "Original": "success", + "Flagged - Exact Match": "danger", + "Flagged - Near Duplicate": "warning", + "Flagged - Semantic Match": "info", + "Flagged - AI Generated": "purple", + "Flagged - Peer Plagiarism": "danger", + "Flagged - Self Plagiarism": "warning", + "Resubmission Allowed": "primary", + "Error": "dark" + }; + const color = colors[value] || "secondary"; + return `${value}`; + } + } +}; diff --git a/tap_lms/tap_lms/doctype/imgsubmission/imgsubmission.json b/tap_lms/tap_lms/doctype/imgsubmission/imgsubmission.json index 67bad236..72aabd25 100644 --- a/tap_lms/tap_lms/doctype/imgsubmission/imgsubmission.json +++ b/tap_lms/tap_lms/doctype/imgsubmission/imgsubmission.json @@ -12,14 +12,26 @@ "student_id", "img_url", "status", + "result_status", "created_at", "grade", + "plagiarism_section", + "plagiarism_status", + "is_plagiarized", + "match_type", + "plagiarism_source", + "similarity_score", + "similar_sources", + "ai_detection_section", + "is_ai_generated", + "ai_detection_source", + "ai_confidence", + "feedback_section", "plagiarism_result", "generated_feedback", "feedback_summary", "overall_feedback", - "completed_at", - "similar_sources" + "completed_at" ], "fields": [ { @@ -32,28 +44,114 @@ "fieldtype": "Data", "label": "Student ID" }, + { + "fieldname": "img_url", + "fieldtype": "Data", + "label": "Image URL" + }, { "fieldname": "status", "fieldtype": "Select", "label": "Status", "options": "Pending\nProcessing\nCompleted\nFailed" }, + { + "fieldname": "result_status", + "fieldtype": "Select", + "label": "Result Status", + "options": "Pending\nSuccess - Original\nSuccess - Flagged\nFailed", + "default": "Pending", + "in_list_view": 1, + "in_standard_filter": 1, + "description": "Overall feedback analysis result status" + }, { "default": "now", "fieldname": "created_at", "fieldtype": "Datetime", "label": "Created At" }, - { - "fieldname": "img_url", - "fieldtype": "Data", - "label": "Image URL" - }, { "fieldname": "grade", "fieldtype": "Float", "label": "Grade" }, + { + "fieldname": "plagiarism_section", + "fieldtype": "Section Break", + "label": "Plagiarism Detection Results" + }, + { + "fieldname": "plagiarism_status", + "fieldtype": "Select", + "label": "Plagiarism Status", + "options": "Not Checked\nOriginal\nFlagged - Exact Match\nFlagged - Near Duplicate\nFlagged - Semantic Match\nFlagged - AI Generated\nFlagged - Peer Plagiarism\nFlagged - Self Plagiarism\nResubmission Allowed\nError", + "default": "Not Checked", + "in_list_view": 1, + "in_standard_filter": 1 + }, + { + "fieldname": "is_plagiarized", + "fieldtype": "Check", + "label": "Is Plagiarized", + "default": 0, + "read_only": 1 + }, + { + "fieldname": "match_type", + "fieldtype": "Select", + "label": "Match Type", + "options": "\noriginal\nexact_duplicate\nnear_duplicate\nsemantic_match\nai_generated\nresubmission_allowed", + "read_only": 1 + }, + { + "fieldname": "plagiarism_source", + "fieldtype": "Select", + "label": "Plagiarism Source", + "options": "\nnone\npeer\npeer_collusion\nself_cross_assignment\nself_late_resubmission\nreference\nai_generated\nstock_image", + "read_only": 1 + }, + { + "fieldname": "similarity_score", + "fieldtype": "Percent", + "label": "Similarity Score", + "read_only": 1 + }, + { + "fieldname": "similar_sources", + "fieldtype": "Data", + "label": "Similar Sources" + }, + { + "fieldname": "ai_detection_section", + "fieldtype": "Section Break", + "label": "AI Detection Results" + }, + { + "fieldname": "is_ai_generated", + "fieldtype": "Check", + "label": "Is AI Generated", + "default": 0, + "read_only": 1 + }, + { + "fieldname": "ai_detection_source", + "fieldtype": "Data", + "label": "AI Detection Source", + "description": "e.g., DALL-E, Midjourney, Stable Diffusion", + "read_only": 1 + }, + { + "fieldname": "ai_confidence", + "fieldtype": "Percent", + "label": "AI Detection Confidence", + "read_only": 1 + }, + { + "fieldname": "feedback_section", + "fieldtype": "Section Break", + "label": "Feedback" + }, { "fieldname": "plagiarism_result", "fieldtype": "Data", @@ -78,11 +176,6 @@ "fieldname": "completed_at", "fieldtype": "Datetime", "label": "Completed_at" - }, - { - "fieldname": "similar_sources", - "fieldtype": "Data", - "label": "Similar Sources" } ], "index_web_pages_for_search": 1, From 1dffe01eb840e76136038aa81193674ec7b9fd7f Mon Sep 17 00:00:00 2001 From: Manu Agarwal Date: Fri, 2 Jan 2026 04:35:36 +0000 Subject: [PATCH 2/3] get_assignment_context- reference image added --- scripts/check_doc_contents.py | 21 ++++++++ scripts/console_consumer.py | 21 ++++++++ tap_lms/imgana/submission.py | 19 ++++++- .../doctype/assignment/assignment.json | 9 ++-- .../doctype/reference_image_item/__init__.py | 0 .../reference_image_item.js | 8 +++ .../reference_image_item.json | 54 +++++++++++++++++++ .../reference_image_item.py | 10 ++++ 8 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 scripts/check_doc_contents.py create mode 100644 scripts/console_consumer.py create mode 100644 tap_lms/tap_lms/doctype/reference_image_item/__init__.py create mode 100644 tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.js create mode 100644 tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.json create mode 100644 tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.py diff --git a/scripts/check_doc_contents.py b/scripts/check_doc_contents.py new file mode 100644 index 00000000..f753c746 --- /dev/null +++ b/scripts/check_doc_contents.py @@ -0,0 +1,21 @@ +import frappe +from frappe.utils.file_manager import get_file_path +import base64 +assignment_id = "fun-faces-1313" +parent_doc = frappe.get_doc("Assignment", assignment_id) +images = [] +for row in parent_doc.reference_images: + file_url = row.image + file_doc = frappe.get_doc("File", {"file_url": file_url}) + + file_path = file_doc.get_full_path() + with open(file_path, 'rb') as f: + content = base64.b64encode(f.read()).decode('utf-8') + images.append({ + 'name': file_doc.file_name, + 'content_type': 'image/jpeg', + 'content': content[:10] # base64 encoded + }) +context = { "reference_images": images} +print(context) + diff --git a/scripts/console_consumer.py b/scripts/console_consumer.py new file mode 100644 index 00000000..9a4f5fac --- /dev/null +++ b/scripts/console_consumer.py @@ -0,0 +1,21 @@ +# consumer code for testing in bench console + +from tap_lms.feedback_consumer.feedback_consumer import FeedbackConsumer +import frappe + +frappe.connect() + +print("\n=== Starting Feedback Consumer ===\n") +consumer = FeedbackConsumer() +consumer.setup_rabbitmq() + +# Check queue state (just for info) +queue_state = consumer.channel.queue_declare( + queue=consumer.settings.feedback_results_queue, + passive=True +) +print(f"Found {queue_state.method.message_count} messages in queue '{consumer.settings.feedback_results_queue}'\n") + +# Always start consuming - it will wait for new messages +print("Starting consumer... (waiting for messages, press CTRL+C to exit)") +consumer.start_consuming() \ No newline at end of file diff --git a/tap_lms/imgana/submission.py b/tap_lms/imgana/submission.py index 09cb54f1..cb5d5f6f 100644 --- a/tap_lms/imgana/submission.py +++ b/tap_lms/imgana/submission.py @@ -5,6 +5,8 @@ from urllib.parse import urlparse from google.cloud import storage import os +from frappe.utils.file_manager import get_file_path +import base64 def get_rabbitmq_settings(): @@ -309,7 +311,20 @@ def get_assignment_context(assignment_id, student_id=None): """Get complete assignment context for RAG service""" try: assignment = frappe.get_doc("Assignment", assignment_id) - + images = [] + for row in assignment.reference_images: + file_url = row.image + file_doc = frappe.get_doc("File", {"file_url": file_url}) + + file_path = file_doc.get_full_path() + with open(file_path, 'rb') as f: + content = base64.b64encode(f.read()).decode('utf-8') + images.append({ + 'name': file_doc.file_name, + 'content_type': 'image/jpeg', + 'content': content # base64 encoded + }) + context = { "assignment": { "name": assignment.assignment_name, @@ -317,7 +332,7 @@ def get_assignment_context(assignment_id, student_id=None): "type": assignment.assignment_type, "subject": assignment.subject, "submission_guidelines": assignment.submission_guidelines, - "reference_image": assignment.reference_image, + "reference_images": images, "max_score": assignment.max_score }, "learning_objectives": [ diff --git a/tap_lms/tap_lms/doctype/assignment/assignment.json b/tap_lms/tap_lms/doctype/assignment/assignment.json index 2958fd9c..4c31c127 100644 --- a/tap_lms/tap_lms/doctype/assignment/assignment.json +++ b/tap_lms/tap_lms/doctype/assignment/assignment.json @@ -19,7 +19,7 @@ "max_file_size", "max_score", "reference_material_section", - "reference_image", + "reference_images", "rag_settings_section", "enable_auto_feedback", "feedback_prompt", @@ -85,9 +85,10 @@ }, { "description": "Upload reference image for students to follow", - "fieldname": "reference_image", - "fieldtype": "Attach", - "label": "Reference Image" + "fieldname": "reference_images", + "fieldtype": "Table", + "label": "Reference Images", + "options": "Reference_Image_Item" }, { "fieldname": "reference_material_section", diff --git a/tap_lms/tap_lms/doctype/reference_image_item/__init__.py b/tap_lms/tap_lms/doctype/reference_image_item/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.js b/tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.js new file mode 100644 index 00000000..5975c016 --- /dev/null +++ b/tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Techt4dev and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Reference_Image', { + // refresh: function(frm) { + + // } +}); diff --git a/tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.json b/tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.json new file mode 100644 index 00000000..003faafd --- /dev/null +++ b/tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.json @@ -0,0 +1,54 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:{######}", + "creation": "2025-12-29 16:47:42.975464", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "istable": 1, + "field_order": [ + "image_name", + "image" + ], +"fields": [ + { + "fieldname": "image_name", + "fieldtype": "Data", + "label": "Image Name", + "read_only": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach", + "label": "Image", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-09-09 23:32:09.565540", + "modified_by": "Administrator", + "module": "TAP LMS", + "name": "Reference_Image_Item", + "naming_rule": "Expression", + "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": [] +} \ No newline at end of file diff --git a/tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.py b/tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.py new file mode 100644 index 00000000..92f43f38 --- /dev/null +++ b/tap_lms/tap_lms/doctype/reference_image_item/reference_image_item.py @@ -0,0 +1,10 @@ +# Copyright (c) 2024, Techt4dev and contributors +# For license information, please see license.txt + +import os +from frappe.model.document import Document + +class Reference_Image_Item(Document): + def before_insert(self): + if self.image and not self.image_name: + self.image_name = "abc" From 3a3b17f0282e8f5d27b608e72009f5fb744a28ca Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 9 Jan 2026 20:29:22 +0530 Subject: [PATCH 3/3] Db commit before submission update --- .../feedback_consumer/feedback_consumer.py | 5 ++++ tap_lms/scripts/console_consumer.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tap_lms/scripts/console_consumer.py diff --git a/tap_lms/feedback_consumer/feedback_consumer.py b/tap_lms/feedback_consumer/feedback_consumer.py index 8888f8ca..c615e24a 100644 --- a/tap_lms/feedback_consumer/feedback_consumer.py +++ b/tap_lms/feedback_consumer/feedback_consumer.py @@ -195,10 +195,15 @@ def process_message(self, ch, method, properties, body): return frappe.logger().info(f"Processing feedback for submission: {submission_id}") + frappe.db.commit() # Check if submission exists if not frappe.db.exists("ImgSubmission", submission_id): frappe.logger().error(f"ImgSubmission {submission_id} not found") + # get a list of existing submission ids for logging + # existing_ids = frappe.db.get_all("ImgSubmission", fields=["name"], limit=5) + # existing_ids_list = [doc.name for doc in existing_ids] + # print(f"Existing ImgSubmission IDs (sample): {existing_ids_list}") ch.basic_reject(delivery_tag=method.delivery_tag, requeue=False) return diff --git a/tap_lms/scripts/console_consumer.py b/tap_lms/scripts/console_consumer.py new file mode 100644 index 00000000..0dfbb818 --- /dev/null +++ b/tap_lms/scripts/console_consumer.py @@ -0,0 +1,23 @@ +# consumer code for testing in bench console + +from tap_lms.feedback_consumer.feedback_consumer import FeedbackConsumer +import frappe + +frappe.connect() + +print("\n=== Starting Feedback Consumer ===\n") +consumer = FeedbackConsumer() +consumer.setup_rabbitmq() + +# Check queue state (just for info) +queue_state = consumer.channel.queue_declare( + queue=consumer.settings.feedback_results_queue, + passive=True +) +print(f"Found {queue_state.method.message_count} messages in queue '{consumer.settings.feedback_results_queue}'\n") + +# Always start consuming - it will wait for new messages +print("Starting consumer... (waiting for messages, press CTRL+C to exit)") +consumer.start_consuming() + +