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