""" 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"], }, }, }