bantuguru-api / model /feedback_engine.py
arkhangelos's picture
Upload model/feedback_engine.py with huggingface_hub
4ce1ff1 verified
Raw
History Blame Contribute Delete
14.9 kB
"""
Rule-Based Feedback Engine
Analyzes 3 cognitive aspects: Argument Structure, Reasoning, Evidence Use.
Uses NLP heuristics + keyword detection + IndoSBERT coherence results.
"""
import re
from model.config import DISCOURSE_MARKERS, REASONING_KEYWORDS, EVIDENCE_KEYWORDS
class FeedbackEngine:
"""
Generates formative feedback for essays based on:
1. Argument Structure β€” sentence count, discourse markers, paragraph flow
2. Reasoning β€” causal keywords, semantic coherence, logical connectors
3. Evidence Use β€” evidence keywords, specific examples, data references
Each aspect produces a level (low/medium/high) and natural language feedback.
"""
def __init__(self):
"""Initialize keyword lists from config."""
self.discourse_markers = DISCOURSE_MARKERS
self.reasoning_keywords = REASONING_KEYWORDS
self.evidence_keywords = EVIDENCE_KEYWORDS
def _count_sentences(self, text):
"""Count sentences in text."""
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
return len([s for s in sentences if len(s.strip()) >= 10])
def _count_paragraphs(self, text):
"""Count paragraphs (separated by newlines)."""
paragraphs = [p.strip() for p in text.split("\n") if len(p.strip()) > 20]
return max(len(paragraphs), 1)
def _find_keywords(self, text, keyword_list):
"""Find and count keyword occurrences in text."""
text_lower = text.lower()
found = {}
for keyword in keyword_list:
count = text_lower.count(keyword.lower())
if count > 0:
found[keyword] = count
return found
# ────────────────────────────────────────────────────
# 1. Argument Structure Analysis
# ────────────────────────────────────────────────────
def analyze_argument_structure(self, text):
"""
Analyze the argument structure of the essay.
Checks:
- Number of sentences (essay length/development)
- Discourse markers (logical flow indicators)
- Paragraph structure (organization)
- Transition variety
"""
num_sentences = self._count_sentences(text)
num_paragraphs = self._count_paragraphs(text)
markers_found = self._find_keywords(text, self.discourse_markers)
num_unique_markers = len(markers_found)
total_markers = sum(markers_found.values())
# Scoring rubric
score = 0.0
# Sentence count contribution (0-30 points)
if num_sentences >= 10:
score += 30
elif num_sentences >= 6:
score += 20
elif num_sentences >= 3:
score += 10
# Discourse marker variety (0-40 points)
if num_unique_markers >= 5:
score += 40
elif num_unique_markers >= 3:
score += 25
elif num_unique_markers >= 1:
score += 10
# Paragraph structure (0-30 points)
if num_paragraphs >= 3:
score += 30
elif num_paragraphs >= 2:
score += 20
elif num_paragraphs >= 1:
score += 10
# Determine level
if score >= 70:
level = "high"
elif score >= 40:
level = "medium"
else:
level = "low"
return {
"level": level,
"score": score,
"num_sentences": num_sentences,
"num_paragraphs": num_paragraphs,
"discourse_markers_found": markers_found,
"num_unique_markers": num_unique_markers,
}
# ────────────────────────────────────────────────────
# 2. Reasoning Analysis
# ────────────────────────────────────────────────────
def analyze_reasoning(self, text, coherence_data=None):
"""
Analyze reasoning quality of the essay.
Checks:
- Causal/reasoning keywords
- Semantic coherence (from IndoSBERT)
- Logical flow indicators
"""
keywords_found = self._find_keywords(text, self.reasoning_keywords)
num_unique_keywords = len(keywords_found)
total_keywords = sum(keywords_found.values())
# Scoring rubric
score = 0.0
# Reasoning keywords (0-40 points)
if num_unique_keywords >= 4:
score += 40
elif num_unique_keywords >= 2:
score += 25
elif num_unique_keywords >= 1:
score += 10
# Semantic coherence from IndoSBERT (0-40 points)
if coherence_data:
coherence_level = coherence_data.get("coherence_level", "low")
if coherence_level == "high":
score += 40
elif coherence_level == "medium":
score += 25
else:
score += 10
# Keyword density (0-20 points) - reasoning should appear throughout
num_sentences = self._count_sentences(text)
if num_sentences > 0:
density = total_keywords / num_sentences
if density >= 0.5:
score += 20
elif density >= 0.25:
score += 10
# Determine level
if score >= 65:
level = "high"
elif score >= 35:
level = "medium"
else:
level = "low"
return {
"level": level,
"score": score,
"reasoning_keywords_found": keywords_found,
"num_unique_keywords": num_unique_keywords,
"coherence_level": coherence_data.get("coherence_level", "unknown") if coherence_data else "not_analyzed",
}
# ────────────────────────────────────────────────────
# 3. Evidence Use Analysis
# ────────────────────────────────────────────────────
def analyze_evidence_use(self, text):
"""
Analyze use of evidence and examples in the essay.
Checks:
- Evidence keywords (misalnya, contohnya, berdasarkan, etc.)
- Specific examples or data references
- Supporting detail variety
"""
keywords_found = self._find_keywords(text, self.evidence_keywords)
num_unique_keywords = len(keywords_found)
total_keywords = sum(keywords_found.values())
# Scoring rubric
score = 0.0
# Evidence keyword variety (0-50 points)
if num_unique_keywords >= 4:
score += 50
elif num_unique_keywords >= 2:
score += 30
elif num_unique_keywords >= 1:
score += 15
# Evidence frequency (0-30 points)
if total_keywords >= 5:
score += 30
elif total_keywords >= 3:
score += 20
elif total_keywords >= 1:
score += 10
# Check for specific numbers/data (0-20 points)
numbers = re.findall(r'\d+[%,.]?\d*', text)
if len(numbers) >= 3:
score += 20
elif len(numbers) >= 1:
score += 10
# Determine level
if score >= 60:
level = "high"
elif score >= 30:
level = "medium"
else:
level = "low"
return {
"level": level,
"score": score,
"evidence_keywords_found": keywords_found,
"num_unique_keywords": num_unique_keywords,
"total_evidence_mentions": total_keywords,
}
# ────────────────────────────────────────────────────
# Feedback Template Generator
# ────────────────────────────────────────────────────
def _generate_argument_feedback(self, analysis):
"""Generate natural language feedback for argument structure."""
level = analysis["level"]
markers = analysis["num_unique_markers"]
sentences = analysis["num_sentences"]
if level == "high":
feedback = "Struktur argumen sudah sangat baik dan runtut."
if markers >= 5:
feedback += " Penggunaan penanda wacana (discourse markers) sudah bervariasi dan efektif."
elif level == "medium":
feedback = "Struktur argumen cukup baik, namun masih bisa ditingkatkan."
suggestions = []
if markers < 3:
suggestions.append("gunakan lebih banyak penanda wacana seperti 'pertama', 'selain itu', 'oleh karena itu'")
if sentences < 6:
suggestions.append("kembangkan ide dengan lebih banyak kalimat penjelas")
if suggestions:
feedback += " Saran: " + "; ".join(suggestions) + "."
else:
feedback = "Struktur argumen masih perlu diperbaiki."
suggestions = []
if markers < 1:
suggestions.append("tambahkan penanda wacana (pertama, kedua, selain itu, oleh karena itu) untuk menunjukkan alur berpikir")
if sentences < 3:
suggestions.append("kembangkan jawaban dengan lebih banyak kalimat")
suggestions.append("susun jawaban dalam beberapa paragraf yang jelas (pembuka, isi, penutup)")
feedback += " Saran: " + "; ".join(suggestions) + "."
return feedback
def _generate_reasoning_feedback(self, analysis):
"""Generate natural language feedback for reasoning."""
level = analysis["level"]
keywords = analysis["num_unique_keywords"]
if level == "high":
feedback = "Penalaran (reasoning) sudah sangat baik. Hubungan sebab-akibat terlihat jelas dan logis."
elif level == "medium":
feedback = "Penalaran cukup baik, namun bisa diperkuat."
suggestions = []
if keywords < 2:
suggestions.append("gunakan lebih banyak kata penghubung sebab-akibat seperti 'karena', 'sehingga', 'akibatnya'")
coherence = analysis.get("coherence_level", "unknown")
if coherence in ("low", "unknown"):
suggestions.append("pastikan setiap kalimat memiliki hubungan logis dengan kalimat sebelumnya")
if suggestions:
feedback += " Saran: " + "; ".join(suggestions) + "."
else:
feedback = "Penalaran masih perlu ditingkatkan."
feedback += " Saran: jelaskan hubungan sebab-akibat secara eksplisit menggunakan kata seperti 'karena', 'sehingga', 'oleh karena itu'; pastikan setiap argumen memiliki alasan yang jelas."
return feedback
def _generate_evidence_feedback(self, analysis):
"""Generate natural language feedback for evidence use."""
level = analysis["level"]
keywords = analysis["num_unique_keywords"]
if level == "high":
feedback = "Penggunaan bukti/contoh sudah sangat baik dan mendukung argumen dengan kuat."
elif level == "medium":
feedback = "Sudah ada beberapa bukti/contoh, namun bisa ditambah."
suggestions = []
if keywords < 2:
suggestions.append("tambahkan lebih banyak contoh spesifik dengan kata seperti 'misalnya', 'contohnya'")
suggestions.append("sertakan data atau fakta pendukung jika memungkinkan")
feedback += " Saran: " + "; ".join(suggestions) + "."
else:
feedback = "Tambahkan contoh dan bukti pendukung untuk memperkuat jawaban."
feedback += " Saran: gunakan kata seperti 'misalnya', 'contohnya', 'berdasarkan'; sertakan fakta, data, atau contoh nyata yang relevan."
return feedback
# ────────────────────────────────────────────────────
# Main Generate Method
# ────────────────────────────────────────────────────
def generate(self, essay_text, overall_score, coherence_data=None):
"""
Generate complete formative feedback for an essay.
Args:
essay_text: The essay text
overall_score: Predicted score from IndoBERT (1-5)
coherence_data: Optional coherence analysis from IndoSBERT
Returns:
dict with feedback for all 3 aspects
"""
# Analyze each aspect
arg_analysis = self.analyze_argument_structure(essay_text)
reasoning_analysis = self.analyze_reasoning(essay_text, coherence_data)
evidence_analysis = self.analyze_evidence_use(essay_text)
# Generate feedback text
arg_feedback = self._generate_argument_feedback(arg_analysis)
reasoning_feedback = self._generate_reasoning_feedback(reasoning_analysis)
evidence_feedback = self._generate_evidence_feedback(evidence_analysis)
return {
"overall_score": overall_score,
"argument_structure": {
"level": arg_analysis["level"],
"feedback": arg_feedback,
"details": {
"num_sentences": arg_analysis["num_sentences"],
"num_paragraphs": arg_analysis["num_paragraphs"],
"discourse_markers_used": list(arg_analysis["discourse_markers_found"].keys()),
},
},
"reasoning": {
"level": reasoning_analysis["level"],
"feedback": reasoning_feedback,
"details": {
"reasoning_keywords_used": list(reasoning_analysis["reasoning_keywords_found"].keys()),
"coherence_level": reasoning_analysis["coherence_level"],
},
},
"evidence_use": {
"level": evidence_analysis["level"],
"feedback": evidence_feedback,
"details": {
"evidence_keywords_used": list(evidence_analysis["evidence_keywords_found"].keys()),
"total_evidence_mentions": evidence_analysis["total_evidence_mentions"],
},
},
}