""" draft_builder.py — منشئ المسودات وملف المادة الخام + الملخص التحليلي النسخة v3.0 التغييرات: - Analytical Brief: ملخص تحليلي هيكلي قبل المادة الخام مباشرة - Object-level truncation: الاقتطاع على مستوى كائن الراوي كاملاً - تعطيك الأولوية للنتائج التي فيها qattan_evidence - إضافة volume في العرض (بيانات التوثيق الكاملة) - _render_narrator_block: دالة مساعدة قابلة للاختبار """ class PromptBuilder: def __init__(self, term: str, category: str, draft: dict): self.term = term self.category = category defaults = { "term": term, "category": category, "total_results": 0, "by_section": {}, } self.draft = {**defaults, **draft} def _group_by_narrator(self, results: list[dict]) -> dict[str, list[dict]]: grouped = {} for r in results: narrator = r.get("narrator", "").strip() or "أقوال عامة ونصوص متفرقة" if narrator not in grouped: grouped[narrator] = [] grouped[narrator].append(r) return grouped def _sort_items_by_quality(self, items: list[dict]) -> list[dict]: """ ترتيب النتائج: أولاً ما فيه دليل نسبة القطان، ثم الباقي. """ with_evidence = [r for r in items if r.get("qattan_evidence")] without_evidence = [r for r in items if not r.get("qattan_evidence")] return with_evidence + without_evidence def _render_narrator_block( self, narrator: str, items: list[dict], max_items: int = 50, max_chars_per_item: int = 800, ) -> str: """ تصيير كتلة راوٍ واحد — دالة مساعدة معزولة قابلة للاختبار. تشمل: اسم الكتاب، المؤلف، الجزء، الصفحة، الرابط، السياق، النص. """ lines = [] if narrator == "أقوال عامة ونصوص متفرقة": lines.append(f"#### 📌 نصوص متفرقة وعامة ({len(items)} نتيجة)") else: lines.append(f"#### 👤 الراوي: {narrator} ({len(items)} قول)") lines.append("") # ترتيب بالجودة: دليل القطان أولاً sorted_items = self._sort_items_by_quality(items) for item in sorted_items[:max_items]: book = item.get("book", "") author = item.get("author", "") volume = item.get("volume", "") page = item.get("page", "") text = item.get("text", "") link = item.get("link", "") context = item.get("context", "") qattan_evidence = item.get("qattan_evidence", "") is_negated = item.get("is_negated", False) # رأس المصدر مع بيانات الجزء والصفحة vol_page = "" if volume: vol_page += f" ج:{volume}" if page: vol_page += f" ص:{page}" lines.append(f"##### 📖 {book} — {author}{vol_page}") if link: lines.append(f"- **المصدر**: 🔗 {link}") meta = [] if is_negated: meta.append("⚠️ صيغة نفي") if qattan_evidence: meta.append(f"✅ دليل النسبة: «{qattan_evidence}»") if meta: lines.append(f"- **ملاحظات**: " + " | ".join(meta)) if context: lines.append(f"- **السياق**: ...{context.strip()[:400]}...") if text: lines.append("") truncated = text[:max_chars_per_item] lines.append(" > " + truncated.replace("\n", "\n > ")) lines.append("") return "\n".join(lines) def build_section_draft( self, section_key: str, max_chars: int | None = None ) -> str: """ بناء مسودة مبحث واحد مع Object-level truncation. الجديد في v2.0: - الاقتطاع يحترم حدود كائن الراوي (لا قطع في المنتصف) - النتائج ذات دليل القطان تُضاف أولاً قبل الحد - تحذير عند تجاوز الحد مع إحصاء ما تُرك """ if max_chars is None: max_chars = { "tahdid": 5_000, "lugha": 8_000, "tarikh": 8_000, "madloul": 15_000, # زيادة لاستيعاب الرواة الكثيرين "muqarana": 12_000, "khatima": 4_000, "masadir": 8_000, }.get(section_key, 8_000) key_to_sec = { "lugha": 1, "tarikh": 2, "madloul": 3, "muqarana": 4, "khatima": 5, } lines = [] if section_key == "tahdid": lines.append(f"### ملخص إحصائي لبيانات اللفظ «{self.term}»") lines.append(f"- **إجمالي النتائج**: {self.draft.get('total_results', 0)}") lines.append(f"- **المصادر والمصنفات**: {self.draft.get('books_count', 0)}") lines.append(f"- **الأئمة والنقاد**: {self.draft.get('authors_count', 0)}") narrators = self.draft.get("narrators", []) if narrators: lines.append(f"- **الرواة المترجمون** ({len(narrators)} راوٍ):") for n in narrators[:30]: lines.append(f" * {n['name']} — المصدر: {n['book']}") return "\n".join(lines) elif section_key == "masadir": lines.append(f"### المصادر المعتمدة في دراسة «{self.term}»") seen_books = {} by_section = self.draft.get("by_section", {}) for sec_data in by_section.values(): for item in sec_data.get("results", []): book = item.get("book", "") author = item.get("author", "") link = item.get("link", "") volume = item.get("volume", "") if book: key = (book, author) if key not in seen_books: seen_books[key] = link for (book, author), link in sorted(seen_books.items()): link_str = f" 🔗 {link}" if link else "" lines.append(f"- *{book}*، {author}.{link_str}") if len(seen_books) > 100: lines.append( f"\n> ⚠️ **تنبيه**: عدد المصادر كبير ({len(seen_books)} كتاباً). " f"اكتفِ بأهمها في القائمة مع ملاحظة أن الحصر الكامل في draft.md." ) return "\n".join(lines) elif section_key in key_to_sec: sec_num = key_to_sec[section_key] by_section = self.draft.get("by_section", {}) sec_data = by_section.get(sec_num) or by_section.get(str(sec_num)) if not sec_data: return "لا توجد نتائج خاصة بهذا المبحث في المادة الخام." lines.append( f"### نتائج المبحث: {sec_data.get('name', '')} ({sec_data.get('count', 0)} نتيجة)" ) if sec_data.get("description"): lines.append(f"*{sec_data['description']}*") lines.append("") results = sec_data.get("results", []) grouped_results = self._group_by_narrator(results) sorted_narrators = sorted( grouped_results.keys(), key=lambda x: (x == "أقوال عامة ونصوص متفرقة", x), ) # ── Object-level truncation ────────────────────────────────── current_size = 0 skipped_narrators = 0 for narrator in sorted_narrators: items = grouped_results[narrator] # تصيير كتلة الراوي كاملاً أولاً block = self._render_narrator_block(narrator, items) block_size = len(block) if current_size + block_size > max_chars: if current_size == 0: # الراوي الأول أكبر من الحد → خذ ما يسع lines.append(block[:max_chars]) current_size = max_chars else: skipped_narrators += 1 continue lines.append(block) lines.append("---") lines.append("") current_size += block_size if skipped_narrators > 0: lines.append( f"\n> ⚠️ **تنبيه**: تم حذف {skipped_narrators} راوٍ إضافي " f"لتجاوز حد المساحة ({max_chars:,} حرف). " f"راجع ملف draft.md الكامل للاطلاع على جميع النتائج." ) return "\n".join(lines) return "" def build_analytical_brief(self) -> str: """ بناء ملخص تحليلي هيكلي — يُقدَّم للنموذج قبل المادة الخام ليعرف بالضبط ماذا يكتب وما الدليل المتاح لكل قسم. """ by_section = self.draft.get("by_section", {}) total = self.draft.get("total_results", 0) narrators = self.draft.get("narrators", []) lines = [] lines.append(f"# الملخص التحليلي — لفظ «{self.term}»") lines.append(f"- التصنيف: {'تعديل' if self.category == 'tadeel' else 'جرح'}") lines.append(f"- إجمالي النتائج: {total}") lines.append(f"- عدد الرواة: {len(narrators)}") lines.append("") # Per-section breakdown lines.append("## توزيع المادة الخام على الأقسام:") lines.append("| القسم | عدد النتائج | النتائج مع دليل القطان | ملاحظات |") lines.append("|-------|-----------|----------------------|---------|") section_key_map = { 1: ("lugha", "المبحث الأول: الدراسة اللغوية"), 2: ("tarikh", "المبحث الثاني: الدراسة الاصطلاحية"), 3: ("madloul", "المبحث الثالث: استعمال القطان"), 4: ("muqarana", "المبحث الرابع: الدراسة التطبيقية المقارنة"), 5: ("khatima", "المبحث الخامس: الخاتمة"), } for sec_num, sec_data in by_section.items(): key_int = int(sec_num) if isinstance(sec_num, str) else sec_num results = sec_data.get("results", []) with_evidence = sum(1 for r in results if r.get("qattan_evidence")) sec_name = sec_data.get("name", f"قسم {key_int}") # Determine quality flag if with_evidence > 0: flag = f"✓ {with_evidence} نتيجة مع دليل القطان" elif len(results) == 0: flag = "✗ لا توجد نتائج — يحتاج بحثاً إضافياً" else: flag = "⚠ نتائج عامة بدون دليل نسبة صريح" lines.append(f"| {sec_name} | {len(results)} | {with_evidence} | {flag} |") lines.append("") # Narrator summary if narrators: lines.append("## أبرز الرواة المذكورين:") for n in narrators[:15]: name = n.get("name", "") book = n.get("book", "") lines.append(f"- {name} (المصدر: {book})") if len(narrators) > 15: lines.append(f"- ... و{len(narrators) - 15} راوٍ إضافياً") lines.append("") # Key instructions lines.append("## تعليمات للنموذج:") lines.append( "- استخدم كل التوثيقات الموجودة في المادة الخام — لا تترك أي نتيجة دون استخدام" ) lines.append("- كل فقرة يجب أن تحتوي توثيقاً مباشراً واحداً على الأقل") lines.append( "- إذا كان القسم ضعيفاً في المادة الخام، اكتب ما تراه ثم اختم بـ (يحتاج توثيقاً)" ) lines.append("- لا تكرر نفس التوثيق أكثر من مرتين متتاليتين") lines.append("- استخدم الصيغة: (المؤلف (ت وفاة)، الكتاب، ج، ص، 🔗 الرابط)") return "\n".join(lines) def build_markdown_draft(self) -> str: """بناء ملف المسودة الكاملة — للحفظ كمرجع، لا للـ AI مباشرة""" lines = [] lines.append(f"# {self.term} — ملف المادة الخام (Research Dossier)") lines.append("") lines.append(f"**التصنيف**: {'تعديل' if self.category == 'tadeel' else 'جرح'}") lines.append(f"**إجمالي النتائج**: {self.draft.get('total_results', 0)}") lines.append(f"**المصادر**: {self.draft.get('books_count', 0)}") lines.append(f"**المؤلفون**: {self.draft.get('authors_count', 0)}") lines.append("") lines.append("---") lines.append("") by_section = self.draft.get("by_section", {}) for sec_num in sorted(by_section.keys()): sec_data = by_section[sec_num] lines.append( f"## {sec_data.get('name', '')} ({sec_data.get('count', 0)} نتيجة)" ) if sec_data.get("description"): lines.append(f"*{sec_data['description']}*") lines.append("") results = sec_data.get("results", []) grouped_results = self._group_by_narrator(results) sorted_narrators = sorted( grouped_results.keys(), key=lambda x: (x == "أقوال عامة ونصوص متفرقة", x), ) for narrator in sorted_narrators: items = grouped_results[narrator] block = self._render_narrator_block( narrator, items, max_items=150, max_chars_per_item=1500 ) lines.append(block) lines.append("---") lines.append("") return "\n".join(lines) def build_ai_prompt(self) -> str: md_draft = self.build_markdown_draft() prompt = f"""# مهمة: كتابة دراسة أكاديمية شاملة للفظ «{self.term}» أنت باحث رسالة ماجستير تكتب في «ألفاظ الجرح والتعديل عند يحيى بن سعيد القطان (ت 198 هـ)». عليك كتابة دراسة شاملة ومفصلة تشمل كل الأقوال والروايات المتعلقة باللفظ. ## طريقة التعامل مع المادة - لا تجعل كثرة النتائج دليلاً على ثبوت النسبة. - لا تدخل في مبحث استعمال القطان إلا النتائج التي فيها «دليل النسبة إلى القطان». - النتائج الموسومة بـ «يحتاج مراجعة يدوية» تذكر في ملاحظات البحث فقط، ولا تبني عليها حكماً. - إن لم تثبت نسبة اللفظ إلى القطان من المادة الخام فصرح بذلك ولا تؤلف دراسة موهمة. ## المواد الخام من البحث — استخدمها كلها ولا تترك أي نتيجة {{md_draft[:25000]}} ## التعليمات الإلزامية — التزم بها حرفياً ### القواعد الذهبية — مقدسة غير قابلة للانتهاك 1. **لا تخترع روابط** — استخدم فقط الروابط الموجودة في المواد الخام 2. **لا تكتب فقرة بغير مصدر** — كل فقرة = توثيق واحد على الأقل 3. **لا تخترع أحاديث أو أقوالاً** — استخدم فقط ما وجدته في المواد الخام 4. **إذا لم تجد رابطاً** اكتب: (يحتاج توثيقاً) 5. **استخدم كل البيانات** — لا تترك أي نتيجة دون استخدام ### صيغة التوثيق — إلزامية داخل المتن استخدم التوثيق المباشر بين قوسين داخل الفقرة، لا تستخدم الحواشي المرقمة: `(المؤلف الكامل (ت وفاة هـ)، *اسم الكتاب كاملاً*، ج:_، ص:_، تحقيق: المحقق، الناشر، الطبعة، السنة، 🔗 https://shamela.ws/book/XXXX/YYYY)` إذا كانت المادة الخام لا تحتوي بيانات التحقيق أو الناشر فاكتب المتاح ثم أضف: `(يحتاج استكمال بيانات النشر)`. ### هيكل الدراسة — أقسام مفصلة ``` 1. تمهيد قصير 2. المبحث الأول: الدراسة اللغوية 3. المبحث الثاني: الدراسة الاصطلاحية 4. المبحث الثالث: استعمال القطان 5. المبحث الرابع: الدراسة التطبيقية المقارنة 6. المبحث الخامس: الخلاصة والنتائج 7. قائمة المصادر ``` ### أسلوب الكتابة — مباشر وشامل ومفصل - اكتب فقرات طويلة ومفصلة — لا تكتب فقرات قصيرة - استخدم كل الأقوال والروايات الموجودة في المواد الخام - اذكر كل راوٍ وصف باللفظ مع القول الكامل - استخدم عبارة «يبدو أن» و«لا يزال» عند الغموض - انقل النصوص بنصها بالعربية الأصلية - لا تكرر نفس المعنى بكلمات مختلفة - استخدم عبارة «إذا تقرر هذا» عند الانتقال بين الأفكار - اختم بـ«والله أعلم» ### القواعد النحوية والأسلوبية - لا تستخدم علامات تعجب أو أسئلة كثيرة - اكتب بلغة أكاديمية محايدة - لا تستخدم «نحن» — استخدم «يُلاحَظ» أو «يتبين» ### ممنوعات - ممنوع استخدام الجداول نهائياً في المستند (يُعرض الحصر في صورة فقرات وقوائم مرقمة منسابة) - اذكر حصر وتفصيل كافة الرواة في البداية فوراً بعد التمهيد وقبل المبحث الأول - ممنوع أي كلمة إنجليزية - ممنوع ذكر «الذكاء الاصطناعي» أو «AI» - التمييز بين: يحيى بن سعيد القطان (ت 198 هـ) ≠ ابن القطان الفاسي (ت 628 هـ) اكتب الدراسة كاملة الآن — من التمهيد إلى قائمة المصادر — دون توقف. ابدأ بـ: **تمهيد** عند تتبع أقوال يحيى بن سعيد القطان في الرواة...""" return prompt def build_external_super_prompt(self) -> str: md_draft = self.build_markdown_draft() # Load global instructions from config import SYSTEM_PROMPTS_DIR global_path = SYSTEM_PROMPTS_DIR / "00_global_rules.md" global_rules = "" if global_path.exists(): global_rules = global_path.read_text(encoding="utf-8") # Load section rules from modules.study_writer import SYSTEM_PROMPTS, SECTIONS sections_rules = [] for key, name in SECTIONS: filename = SYSTEM_PROMPTS.get(key, "") if filename: sec_path = SYSTEM_PROMPTS_DIR / filename if sec_path.exists(): try: raw_rules = sec_path.read_text(encoding="utf-8") sec_rules = raw_rules.replace("{term}", self.term) sections_rules.append( f"### قواعد كتابة قسم: {name} ({key})\n{sec_rules}\n" ) except Exception: sec_rules = sec_path.read_text(encoding="utf-8") sections_rules.append( f"### قواعد كتابة قسم: {name} ({key})\n{sec_rules}\n" ) sections_rules_str = "\n\n".join(sections_rules) # Build prompt using safe replacement/concatenation to avoid f-string brace parsing errors term_category_label = "تعديل" if self.category == "tadeel" else "جرح" header = ( "# البرومبت الخارق للكتابة اليدوية عبر AI خارجي (مثل Claude أو ChatGPT)\n" "هذا الملف مخصص لمساعدتك في كتابة الدراسة يدوياً عبر أي نموذج ذكاء اصطناعي تفضله في حال حدوث ضغط على خوادم OpenRouter أو رغبتك في استخدام نموذج محدد مثل Claude 3.5 Sonnet أو GPT-4o.\n\n" "## تعليمات الاستخدام:\n" "1. انسخ محتوى هذا الملف بالكامل.\n" "2. ألصقه في نافذة محادثة جديدة مع الذكاء الاصطناعي.\n" "3. انتظر حتى يقوم الذكاء الاصطناعي بتوليد الدراسة الأكاديمية كاملة (من التمهيد إلى قائمة المصادر).\n" '4. انسخ النص الناتج من الذكاء الاصطناعي، ثم ألصقه في قسم "الاستيراد اليدوي" في الواجهة الرسومية لتوليد ملف الورد بالتنسيق الاحترافي والهوامش الفنية والتدقيق المنهجي.\n\n' "---\n\n" "# [البرومبت المقترح للنسخ]\n\n" "أنت باحث أكاديمي متميز تكتب في رسالة الماجستير للباحث (أحمد مرزوق) تحت إشراف الأستاذ الدكتور (أحمد معبد عبد الكريم)، وموضوعك هو: «ألفاظ الجرح والتعديل عند يحيى بن سعيد القطان (ت 198 هـ)».\n\n" f"مطلوب منك كتابة دراسة أكاديمية شاملة ومفصلة للفظ: «{self.term}» (تصنيفه: {term_category_label})، وذلك بناءً على المادة الخام المرفقة وقواعد الصياغة والتوثيق الأكاديمية الصارمة المدرجة أدناه.\n\n" "---\n\n" "## أولاً: القواعد الأكاديمية والصياغة المنهجية (هام جداً)\n" ) middle = "\n\n---\n\n## ثانياً: قواعد كتابة مباحث الدراسة بالتفصيل\n" draft_section = ( "\n\n---\n\n" "## ثالثاً: المادة الخام الكاملة للبحث (استخرج منها كافة التوثيقات والأسماء والروابط)\n" ) footer = ( "\n\n---\n\n" "## رابعاً: أمر البدء بالكتابة\n" "اكتب الدراسة الآن كاملة (من التمهيد إلى قائمة المصادر) كملف واحد متصل متناسق، مستخدماً كل النقول والأدلة والروابط الواردة في المادة الخام أعلاه دون اختصار أو إهمال لأي راوٍ أو نتيجة.\n" "تنبيه حاسم: اكتب المحتوى النهائي مباشرة باللغة العربية الفصحى. ممنوع منعاً باتاً التفكير بصوت عالٍ أو كتابة أي مسودات أو تعليقات باللغة الإنجليزية. ابدأ الآن بـ:\n\n" "**التمهيد**\n" ) full_prompt = ( header + global_rules + middle + sections_rules_str + draft_section + md_draft + footer ) return full_prompt