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/feedback_consumer/feedback_consumer.py b/tap_lms/feedback_consumer/feedback_consumer.py
index e8deb2e2..c615e24a 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")
@@ -194,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
@@ -266,7 +272,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 +336,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..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():
@@ -216,7 +218,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 +241,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(
@@ -301,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,
@@ -309,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/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()
+
+
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/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,
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"