Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions scripts/check_doc_contents.py
Original file line number Diff line number Diff line change
@@ -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)

21 changes: 21 additions & 0 deletions scripts/console_consumer.py
Original file line number Diff line number Diff line change
@@ -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()
151 changes: 149 additions & 2 deletions tap_lms/feedback_consumer/feedback_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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

Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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:
Expand Down
31 changes: 27 additions & 4 deletions tap_lms/imgana/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -301,15 +311,28 @@ 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,
"description": assignment.description,
"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": [
Expand Down
23 changes: 23 additions & 0 deletions tap_lms/scripts/console_consumer.py
Original file line number Diff line number Diff line change
@@ -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()


9 changes: 5 additions & 4 deletions tap_lms/tap_lms/doctype/assignment/assignment.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"max_file_size",
"max_score",
"reference_material_section",
"reference_image",
"reference_images",
"rag_settings_section",
"enable_auto_feedback",
"feedback_prompt",
Expand Down Expand Up @@ -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",
Expand Down
49 changes: 43 additions & 6 deletions tap_lms/tap_lms/doctype/imgsubmission/imgsubmission.js
Original file line number Diff line number Diff line change
@@ -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": '<span class="badge badge-warning">⏳ Pending</span>',
"Success - Original": '<span class="badge badge-success">✓ Original</span>',
"Success - Flagged": '<span class="badge badge-danger">⚠ Flagged</span>',
"Failed": '<span class="badge badge-secondary">✗ Failed</span>'
};
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 `<span class="badge badge-${color}">${value}</span>`;
}
}
};
Loading