Spaces:
Running on Zero
Running on Zero
File size: 25,063 Bytes
51d43b8 666d78c 51d43b8 666d78c 51d43b8 666d78c 51d43b8 666d78c 51d43b8 666d78c 51d43b8 666d78c 51d43b8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 | """
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
|