diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f916713ee973039803a9274da2979245f97913fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +__pycache__/ +*.pyc +*.pyo +modules\firebase_credentials.json +test* \ No newline at end of file diff --git a/.vscode/.ai-schema-ignore b/.vscode/.ai-schema-ignore new file mode 100644 index 0000000000000000000000000000000000000000..0226a84e2259a61c00526f28d7a2c9423c86d325 --- /dev/null +++ b/.vscode/.ai-schema-ignore @@ -0,0 +1,261 @@ +# ============================================================ +# .ai-schema-ignore template file +# Defines files and folders to exclude from schema generation +# ============================================================ + +# ----------------------------- +# Universal File Extensions +# ----------------------------- +*.diff +*.err +*.orig +*.log +*.rej +*.swo +*.swp +*.vi +*~ +*.tmp +*.temp +*.bak +*.backup +*.old +*.lock +*.pid +*.cache +*.zip +*.rar +*.7z +*.tar +*.tar.gz +*.tgz +*.gz +*.bz2 +*.xz +*.iso +*.img +*.vsix +*.code-workspace +*.ai-schema-ignore +*.bundle.js +*.bundle.js.map +*.gps-extension/ + +# ----------------------------- +# Image & Media Files +# ----------------------------- +*.png +*.jpg +*.jpeg +*.webp +*.ico +*.svg +*.gif +*.psd +*.tiff +*.bmp +*.mp3 +*.wav +*.ogg +*.flac +*.mp4 +*.avi +*.mov +*.wmv +*.mkv +*.webm + +# ----------------------------- +# Font Files +# ----------------------------- +*.ttf +*.otf +*.woff +*.woff2 +*.eot + +# ----------------------------- +# Specific Files +# ----------------------------- +LICENSE +LICENSE.txt +CHANGELOG.md +README.md +README.txt +project_scheme.ps1 +project_structure_and_code.txt +project_structure.txt +scheme_creator.py +project_analysis.txt +.gitignore +package-lock.json +sftp.json + +# ----------------------------- +# General Development Folders +# ----------------------------- +.DS_Store +Thumbs.db +.cache/ +.project +.settings/ +.tmproj +*.esproj +nbproject/ +*.sublime-project +*.sublime-workspace +.vscode-test/ +coverage/ +reports/ +logs/ +.ai-schema/ + +# Dreamweaver +_notes/ +dwsync.xml + +# Komodo +*.komodoproject +.komododotools/ + +# Source Control +.hg/ +.svn/ +.CVS/ +.git/ + +# Development Artefacts +intermediate/ +.idea/ +cache/ +.vcode/ +node_modules/ +libs/ +.tools/ +languages/ +dist/ +build/ +out/ + +# ----------------------------- +# WordPress Specific +# ----------------------------- +wp-config.php +wp-config-sample.php +debug.log +.htaccess +*.db +*.sql +*.sqlite +*.csv +*.env +wp-admin/ +wp-includes/ +uploads/ +cache/ +wpcache/ +upgrade/ +*.mo +*.po +*.pot +*.min.js +*.min.css +vendor/ + +# WooCommerce +woocommerce/logs/ +woocommerce/tmp/ +woocommerce/packages/ +woocommerce-plugin-backups/ +woocommerce-logs/ + +# Plugins +*.zip +build/ +assets/ +languages/ +phpunit.xml +phpunit.xml.dist +tests/ +tests/_output/ +tests/_support/ +tests/_data/ + +# ----------------------------- +# Language-Specific Ignores +# ----------------------------- +# Node.js / TypeScript +node_modules/ +dist/ +build/ +*.tsbuildinfo + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +*.egg +*.egg-info +*.manifest +*.spec +pip-wheel-metadata/ + +# Java +*.class +*.jar +*.war +*.ear +*.jmod +target/ +out/ + +# .NET +bin/ +obj/ +*.dll +*.pdb +*.exe + +# Ruby +*.gem +*.rbc +.bundle/ +vendor/ + +# PHP +vendor/ +*.phar + +# C / C++ +*.o +*.out +*.so +*.dylib +*.a + +# Go +*.exe +*.test +vendor/ +go.sum + +# Rust +target/ +Cargo.lock + +# Swift / iOS +*.xcworkspace/ +*.xcuserdata/ +DerivedData/ +Pods/ + +# Android +*.apk +*.ap_ +*.dex +*.gradle +.gradle/ +local.properties + +myenv/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6881b100d9447ec92944604d3b75b9241fc4f6e9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# استخدم صورة أساسية خفيفة من Python +# force rebuild +FROM python:3.11 + +# اضبط مجلد العمل داخل الحاوية +WORKDIR /app + +# انسخ ملف requirements لو عندك +COPY requirements.txt . + +# نزّل المتطلبات +RUN pip install --no-cache-dir -r requirements.txt + +# انسخ ملفات المشروع +COPY . /app + +# عرّف البورت اللي البرنامج هيشتغل عليه +EXPOSE 7860 + +# استخدم Gunicorn بدلاً من uvicorn مباشرة لدعم عدة مستخدمين (Workers) +# رقم 4 يعني أنه سيشغل 4 عمليات (Processes) لخدمة الطلبات في نفس الوقت +# # CMD ["gunicorn", "app:app", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:7860", "--timeout", "120"] +# CMD ["gunicorn", "app:app","--workers", "2","--worker-class", "uvicorn.workers.UvicornWorker","--bind", "0.0.0.0:7860","--timeout", "180"] +# لو بتستخدم FastAPI (مع uvicorn): +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file diff --git a/README.md b/README.md index 6eb4bfbd044027f3af0733404626d2de0f8b7f5e..b42366248eabfd0e6578d83e0b60e14ec029ff01 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ --- -title: ContiAI -emoji: 🌍 -colorFrom: yellow -colorTo: red +title: ContiAI-v4 +emoji: 🐨 +colorFrom: blue +colorTo: green sdk: docker pinned: false +license: apache-2.0 +app_port: 7860 --- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/agents/analysis_phase/Introduction.py b/agents/analysis_phase/Introduction.py new file mode 100644 index 0000000000000000000000000000000000000000..dec1a5497554718659ca2c383a84784edec8137c --- /dev/null +++ b/agents/analysis_phase/Introduction.py @@ -0,0 +1,114 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm_g +from schemas import IntroductionOutput + +def intro_agent(): + model=llm_g() + return Agent( + role="Human-Crafted Arabic Introduction Writer", + goal=( + "Create a deeply human, stylistically rich, emotionally resonant Arabic introduction " + "for a training program based on the provided topic ({topic}), outlines, units, and outcomes. " + "The introduction must feel alive, credible, realistic, and engaging—not artificial or generic." + ), + backstory=( + "You are a master Arabic writer and educational content designer. " + "You craft introductions that carry a human breath—mixing scene, reflection, emotion, " + "and intellectual depth without artificial phrases. " + "Your style avoids AI clichés and empty rhetoric, relying instead on concrete imagery, " + "varied rhythm, personal tone, and subtle storytelling. " + "You write for {audience} with professionalism, depth, and authenticity." + ), + system_template=( + "أنت كاتب عربي محترف تمتد خبرته إلى تصميم مقدمات تدريبية آسرة تعتمد على الصورة " + "الحسية، والتجربة الواقعية، والنبرة الشخصية الصادقة. كتابتك تشبه النصوص البشرية القوية " + "وليست مصطنعة أو آلية.\n\n" + "==============================\n" + "## قواعد الكتابة الإنسانية (ANTI-AI RULES)\n" + "==============================\n" + "يجب على المقدمة أن:\n" + "- تكون بين 250 و 320 كلمة.\n" + "- تتجنب تمامًا: هل تساءلت يومًا، ليس مجرد… بل…، في عالمنا المتسارع، رحلة متكاملة، إحداث فرق حقيقي.\n" + "- تتجنب الأسلوب الإنشائي الدعائي واللغة البراقة الفارغة.\n" + "- تحتوي على جُمل قصيرة وأخرى طويلة (تنويع الإيقاع).\n" + "- تتضمن لقطة أو مشهدًا واقعيًا مرتبطًا بالموضوع (مشهد صف، موقف واقعي، صورة حسيّة واضحة).\n" + "- تحمل لمسة شخصية: دهشة، ملاحظة، موقف شعوري، رأي بسيط.\n" + "- تستخدم انتقالات طبيعية نابعة من الفكرة نفسها، لا من الروابط الميكانيكية مثل: بعد ذلك، لن يكتمل، سننتقل.\n" + "- تستخدم لغة عربية قوية وواضحة، بلا مبالغة، بلا زخرفة، بلا تكرار.\n" + "- تعرض مشكلة حقيقية أو تحديًا واقعيًا يتعلق بالموضوع.\n" + "- تبتعد عن العموميات والعبارات المستهلكة.\n\n" + "==============================\n" + "## أساليب افتتاح مسموح بها\n" + "==============================\n" + "يمكنك البدء بـ:\n" + "- مشهد قصير واقعي.\n" + "- سطر من تجربة إنسانية.\n" + "- تساؤل تأملي صادق (غير إنشائي).\n" + "- حقيقة أو رقم لها علاقة مباشرة بالموضوع.\n" + "- حكمة أو قول مناسب لكن غير مستهلك.\n\n" + "==============================\n" + "## البنية المرنة للمقدمة\n" + "==============================\n" + "1. افتتاح مشهدي أو واقعي أو تأملي.\n" + "2. ربط المشهد بطبيعة موضوع التدريب.\n" + "3. عرض المشكلة أو التحدي الحقيقي الذي يعالجه البرنامج.\n" + "4. توضيح قيمة الموضوع والفائدة بأسلوب ملموس لا إنشائي.\n" + "5. وعد تعليمي مشتق من المخرجات لكن بلغة إنسانية طبيعية.\n\n" + "==============================\n" + "## تعليمات إضافية لضمان الأسلوب البشري\n" + "==============================\n" + "- استخدم صورًا حسية: صوت، ضوء، حركة، مشهد، شعور.\n" + "- لا تجعل الجمل كلها بنفس الإيقاع أو الطول.\n" + "- لا تبدأ بأسئلة رنانة بلا معنى.\n" + "- تجنب الترتيب النمطي للجمل الذي ينتجه الذكاء الاصطناعي.\n" + "- قدم فائدة أو ملاحظة شخصية حقيقية.\n" + "- اربط الكلمات بالموقف وليس بالقوالب.\n\n" + ), + llm=model, + memory=False, + allow_delegation=False, + verbose=True, + max_iter=50, + cache=False, +) + +def intro_task(intro_agent): + return Task( + description=( + "Write a human-like Arabic introduction (300–400 words) that reflects true human rhythm, " + "## INPUT\n" + "You have access to the program’s full context:\n" + "- Topic: {topic}\n" + "- Outline (units and subtopics): {outlines}\n" + "- Unit details: {units}\n" + "- Learning outcomes: {outcomes}\n\n" + "## OBJECTIVE\n" + "Write an Arabic introduction that connects the learner emotionally and intellectually to the training content. " + "The introduction must:\n" + "- Reflect the program’s educational goals and outcomes.\n" + "- Be motivational, concise, and linguistically natural for {audience}.\n" + "## MUST INCLUDE:\n" + "• نبرة شخصية.\n" + "• لغة خالية من الإنشاء المكرر والعبارات المستهلكة.\n" + "• تنويع طول الجمل.\n" + "• انتقالات طبيعية.\n\n" + "## MUST AVOID:\n" + "• الأسئلة الافتتاحية المعلبة.\n" + "• الإنشاء الفارغ.\n" + "• الإفراط في الجمل الطويلة.\n" + "• العبارات التسويقية.\n" + "• الكلمات اللامعة الخاوية.\n\n" + "### OUTPUT\n" + "Return the IntroductionOutput structure in valid JSON format.\n\n" + ), + expected_output="""JSON object with this structure: + { + "introduction": "engaging introduction to the training program content, max 300 words." + } + """, + output_json=IntroductionOutput, + agent=intro_agent, + human_input=False, +) diff --git a/agents/analysis_phase/__init__.py b/agents/analysis_phase/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fe00669c5f890f6e6c8e723cc9a93e2753da9955 --- /dev/null +++ b/agents/analysis_phase/__init__.py @@ -0,0 +1,10 @@ +from .analysis_report import report_agent, report_task +from .analysis import analysis_agent, analysis_task +from .objectives import ( + objectives_generator, + objectives_task, + get_bloom_verb, +) +from .outcomes import outcomes_generator, outcomes_task +from .outliner import outline_agent, outline_task +from .Introduction import intro_agent, intro_task diff --git a/agents/analysis_phase/analysis.py b/agents/analysis_phase/analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..d09d1bef2383f4871d179e16dcdbc40a22bb88a2 --- /dev/null +++ b/agents/analysis_phase/analysis.py @@ -0,0 +1,103 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm +from schemas import AnalysisOutput + +def analysis_agent(): + model=llm() + return Agent( + role="Training Analysis Agent", + goal="\n".join( + [ + "Generate the analysis phase for the training program in Arabic based on the topic, outline, and training outcomes.", + "Follow the DNA methodology defined in the Outline Agent.", + "and follow the DNA parameters ({domain}, {content_type}, {audience}, {material_type}).", + "The analysis must include the following elements (taken from the official analysis framework):", + "", + "1. Goal:", + "- Overall goal expected after the training program.", + "- Must start with: 'أن يتمكّن المتدرب من ...'.", + "- Should be a complete sentence covering all training topics.", + "- Must be comprehensive, not limited to one aspect.", + "", + "2. Target Audience:", + "- The group receiving the training.", + "- Include average age (e.g., شباب في العشرينات، موظفون في الثلاثينات).", + "", + "3. Previous Experiences:", + "- Short sentences written as noun-phrases (مصادر).", + "- Express prior practical or knowledge-based experiences.", + "- Examples: ممارسة أنشطة جماعية، مواجهة ضغوط العمل، استخدام أدوات تنظيم، الالتزام بجداول، إدارة مهام يومية.", + "", + "4. Learner Characteristics:", + "- Common traits considering age and developmental aspects:", + " * Linguistic development: improving oral expression and listening.", + " * Cognitive development: analyzing and linking concepts, applying analytical skills.", + " * Social development: tendency to interact and collaborate.", + " * Emotional development: reacting to emotional situations.", + " * Personality growth: building independent identity and attitudes.", + " * Learning styles: preference for practical, auditory, or visual learning.", + "", + "5. Training Needs:", + "- Represent what the learner needs in skills, knowledge, and behaviors.", + "- Must be extracted from the training topics.", + "- 6 sentences required, each a noun-phrase.", + "- Each starts with a مصدر (e.g., فهم، إدراك، توظيف، استيعاب).", + "- Must not be copied titles but rephrased needs.", + "", + "6. Constraints:", + "- Barriers that may negatively affect training.", + "- Two types:", + " * Trainer constraints (4 sentences):Examples: (e.g.,ضيق الوقت، محدودية الوسائل التعليمية، تباين مستويات المتدربين، قلة الدعم الإداري.).", + " * Trainee constraints (4 sentences): Examples:(e.g.,ضعف الالتزام، ضعف الحافز، انشغالات شخصية، قلة الخبرة التقنية.).", + "- All sentences must start with a مصدر (محدودية، ضعف، صعوبة، تباين).", + "- Written as complete noun-phrases only.", + "", + "7. Duration:", + "- Exact training duration (hours/days).", + "- Training day = 4 hours.", + "- Examples: دورة يوم واحد = 4 ساعات، دورتان = 8 ساعات.", + "", + "8. Sources of Analysis:", + "- Pre-training instructional design processes.", + "- Examples: البحث عن مصادر علمية، تحليل احتياجات المتدربين، دراسة محاور الدورة، تحديد الفجوات بين الواقع والمأمول، تجميع البيانات.", + "- Must be written as noun-phrases starting and ending with a مصدر (e.g., جمع البيانات وتحليلها).", + ] + ), + backstory="\n".join( + [ + "This agent acts as an instructional designer responsible for producing the full analysis phase.", + "It ensures outputs are consistent with the Arabic training framework (مرحلة التحليل).", + "It builds on the Outline and Training Outcomes, while following DNA methodology and ollow the DNA parameters (domain, content_type, audience, material_type).", + ] + ), + llm=model, + verbose=True, +) + + +# ====== Task ====== +def analysis_task(analysis_agent): + return Task( + description="\n".join( + [ + "Generate the complete analysis phase in Arabic for the training program.", + "Use the outline (main_headings + sub_headings + sub_sub_headings) and training outcomes as input.", + "Follow the analysis framework:", + "- Goal", + "- Target Audience", + "- Previous Experiences", + "- Learner Characteristics", + "- Training Needs", + "- Constraints", + "- Duration", + "- Sources of Analysis", + "Ensure all outputs are written in Arabic, concise, and aligned with DNA methodology.", + "Output must strictly match the Pydantic schema (AnalysisOutput) and be in JSON format.", + ] + ), + expected_output="A JSON object in Arabic with all required analysis fields.", + output_json=AnalysisOutput, + agent=analysis_agent, +) diff --git a/agents/analysis_phase/analysis_report.py b/agents/analysis_phase/analysis_report.py new file mode 100644 index 0000000000000000000000000000000000000000..0c7b32c788725d7e474eb5f84b40859a48c44202 --- /dev/null +++ b/agents/analysis_phase/analysis_report.py @@ -0,0 +1,66 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm + + +# ====== Agent ====== +def report_agent(): + model=llm() + return Agent( + role="Training Report Builder", + goal="\n".join( + [ + "Take the outputs from title_agent, outline_agent, training_agent, and analysis_agent.", + "Generate a well-structured HTML report in Arabic.", + "", + "Rules:", + "- At the very top of the page, display the title from title_agent output as a main heading (

).", + "- The training_agent introduction should appear under a section titled: 'المقدمة'.", + "- The outline_agent output should appear under a section titled: 'محتويات الحقيبة'.", + "- The training_agent learning outcomes should appear under a section titled: 'مخرجات البرنامج التدريبي'.", + "- The analysis_agent output should appear under a section titled: 'مرحلة التحليل'.", + "", + "For the 'مرحلة التحليل' section:", + "- Present it inside an HTML table.", + "- The table should have two columns: 'العنصر' and 'الوصف'.", + "- Each row corresponds to one analysis element (الهدف، الفئة المستهدفة، الخبرات السابقة، خصائص المتدربين، الاحتياجات التدريبية، القيود، المدة الزمنية، مصادر التحليل).", + "", + "The final HTML page should be clean and well-structured with Arabic titles and proper formatting.", + ] + ), + backstory="\n".join( + [ + "This agent acts as a report generator.", + "It combines all outputs (title, outline, training outcomes, analysis) into one structured HTML training portfolio.", + ] + ), + llm=model, + verbose=True, +) + + +# ====== Task ====== +def report_task(report_agent): + return Task( + description="\n".join( + [ + "Build an HTML page combining all outputs:", + "- At the top of the page, show the title (from title_agent output) as

.", + "- Section 1: 'المقدمة' → from training_agent output (introduction).", + "- Section 2: 'محتويات الحقيبة' → from outline_agent output.", + "- Section 3: 'مخرجات البرنامج التدريبي' → from training_agent output (learning outcomes as a bulleted list).", + "- Section 4: 'مرحلة التحليل' → from analysis_agent output.", + " - The analysis section must be presented inside an HTML table same as this template {html_template}.", + "use css style to enhance the design more and the table responsive.", + "Care about colors, headers, and fonts.", + "don't change the style of analysis table", + " - The table should have two columns: 'العنصر' and 'الوصف'.", + " - Each row corresponds to one analysis element (goal, target audience, previous experiences, learner characteristics, training needs, constraints, duration, sources of analysis).", + "", + "Ensure the HTML page is clean, well-structured, and properly formatted in Arabic.", + ] + ), + expected_output="A single HTML string with a title, three sections, and a table for the analysis section. and don't put the code in html``` ```", + agent=report_agent, +) diff --git a/agents/analysis_phase/objectives.py b/agents/analysis_phase/objectives.py new file mode 100644 index 0000000000000000000000000000000000000000..fa207c8fc820ab2dd26e7bc912a6d40b45f2ab70 --- /dev/null +++ b/agents/analysis_phase/objectives.py @@ -0,0 +1,290 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import bloom_taxonomy_2, objectives_dimensions, llm_g +from schemas import LearningObjectivesOutput + + +def get_bloom_verb(level: str) -> str: + """Returns verbs from the given Bloom's taxonomy level.""" + + if level not in bloom_taxonomy_2: + raise ValueError(f"Invalid Bloom level: {level}") + verbs_key = [k for k in bloom_taxonomy_2[level].keys() if "_verbs_examples" in k][0] + return bloom_taxonomy_2[level][verbs_key] + + +def get_verbs_by_domain_and_level(domain, level_name): + """ + استخراج الأفعال المناسبة من قاموس الأبعاد التعليمية + + Args: + domain: "Knowledge", "Ability", or "Skills" + level_name: اسم المستوى مثل "Remember", "Response", "Practice" + + Returns: + قائمة بالأفعال المتاحة + """ + if domain not in objectives_dimensions: + return [] + + for level in objectives_dimensions[domain]["levels"]: + if level_name.lower() in level["level_name"].lower(): + return level["verbs_examples"] + + return [] + + +def objectives_generator(): + model=llm_g() + return Agent( + role="Expert Learning Objectives Designer (Instructional Design Specialist)", + goal=( + "Design a comprehensive list of clear, measurable, and achievable learning objectives in Arabic " + "for each subtopic in the training curriculum. Your objectives must be based on {topic} and the " + "content provided in {outlines}. " + "You must carefully read and analyze every subtopic title, its purpose, and the unit description " + "before writing any objective. " + "Each objective must align with Bloom’s Taxonomy and the three learning domains " + "(cognitive, affective, psychomotor), accurately reflect the intended learning outcome, " + "and meet all 12 professional instructional design standards. " + "**Critical:** You must maximize verb diversity across all objectives — avoid repeating the same " + "verb whenever possible." + ), + # system_template="", + backstory=( + "You are an expert instructional designer with deep experience in creating professional training programs. " + "You analyze each unit and subtopic to determine the type of knowledge, skill, or attitude learners " + "should achieve. " + "Then, you write one short, measurable, learner-centered objective for each subtopic following Bloom’s " + "Taxonomy sequence (Remember → Understand → Apply → Analyze → Evaluate → Create). " + "**You are exceptionally skilled at selecting diverse and non-repetitive verbs from Bloom’s Taxonomy " + "and the three learning domains.** " + "You maintain a mental tracker of all verbs used and actively avoid repetition across the entire curriculum. " + "You ensure maximum verb diversity, linguistic precision, and that every objective is " + "clear, measurable, realistic, achievable, and free from ambiguity or linguistic complexity. " + "Your objectives must reflect the parameters of {domain}, {content_type}, {audience}, and {material_type}, " + "while covering the cognitive, psychomotor, and affective domains." + ), + llm=model, + verbose=True, + max_iter=100, +) + + +def objectives_task(objectives_agent): + return Task( + description=( + "You have a complete and well-structured training curriculum provided in outlines: {outlines} and units details: {units}. " + "Your task is to create one detailed, measurable learning objective in Arabic for each subtopic, " + "aligned with Bloom’s Taxonomy and the three learning domains, and consistent with the purpose of each subtopic.\n\n" + "### Step-by-Step Process\n" + "1. Read the curriculum overview to understand the full learning journey.\n" + "2. **Before you begin, review the complete list of Bloom’s action verbs and the three domains for each level to plan maximum diversity.**\n" + "3. **Create a mental or written record of verbs as you use them to avoid repetition.**\n" + "4. For each unit:\n" + " a. Analyze the unit description to understand the learning scope and intended outcomes.\n" + " b. For each subtopic within that unit:\n" + " - Carefully read the subtopic title and its purpose.\n" + " - Identify the main learning outcome (Knowledge, Skill, or Attitude/Behavior).\n" + " - **First, determine the appropriate domain: Cognitive (Knowledge), Affective (Attitude), or Psychomotor (Skills).**\n" + " - Choose the most suitable Bloom’s level or the appropriate level of that domain.\n" + " - **From the available verbs at that level, select one that has not been used before (or only minimally used).**\n" + " - Choose three relevant Bloom’s verbs that suit the abilities of {audience}.\n" + " - Write one measurable learning objective in Arabic that accurately reflects the subtopic’s purpose.\n" + " - **Track the chosen verb to prevent reusing it in later objectives.**\n\n" + "### Requirements for the Three Learning Domains\n" + "**Objectives must cover the three domains in a balanced way:**\n\n" + f"**1. Cognitive Domain (Knowledge):**\n" + f"{objectives_dimensions['Knowledge']['description']}\n" + "**Levels:**\n" + + "\n".join( + [ + f" - {level['level_name']}: {level['description']}\n Verbs: {', '.join(level['verbs_examples'])}" + for level in objectives_dimensions["Knowledge"]["levels"] + ] + ) + + "\n\n" + f"**2. Affective Domain (Attitude/Ability):**\n" + f"{objectives_dimensions['Ability']['description']}\n" + "**Levels:**\n" + + "\n".join( + [ + f" - {level['level_name']}: {level['description']}\n Verbs: {', '.join(level['verbs_examples'])}" + for level in objectives_dimensions["Ability"]["levels"] + ] + ) + + "\n\n" + f"**3. Psychomotor Domain (Skills):**\n" + f"{objectives_dimensions['Skills']['description']}\n" + "**Levels:**\n" + + "\n".join( + [ + f" - {level['level_name']}: {level['description']}\n Verbs: {', '.join(level['verbs_examples'])}" + for level in objectives_dimensions["Skills"]["levels"] + ] + ) + + "\n\n" + "### Critical Requirement: Verb Diversity \n" + "**This is a top-priority rule that must be followed precisely:**\n\n" + "**Verb Non-Repetition Rules:**\n" + "1. **Maximize verb diversity** — each objective should ideally use a different verb than all previous ones.\n" + "2. **Before selecting a verb, check:** Have I used this verb before? If yes, choose another.\n" + "3. **Repeat a verb only when:**\n" + " - All suitable alternative verbs in that Bloom level or domain are exhausted.\n" + " - The subtopic’s content explicitly requires that verb for accuracy.\n" + " - You can justify why no other verb fits.\n" + "4. **Prioritize diversity over perfect synonyms** — it’s better to use a slightly different but valid verb than to repeat.\n" + "5. **Use the full range of verbs from Bloom and the three domains** — avoid overusing common ones like 'define' or 'explain'.\n" + "6. **Track verb usage:** after writing each objective, note the verb and consciously avoid it in the next.\n" + "7. **If repetition is necessary:**\n" + " - Space it out by at least 3–5 objectives.\n" + " - Add a justification note in your internal reasoning.\n" + " - Try using it in a different context or content area.\n\n" + "### The 12 Mandatory Design Standards — Each Objective Must Meet All\n" + "**Based on the detailed objectives reference document:**\n\n" + "1. **Linked to the general goal:**\n" + " - Every objective must connect to the course’s overall goal.\n" + " - Example: If the general goal is 'Understand the concept of e-learning', " + "the specific objective could be 'The trainee accurately defines e-learning.'\n\n" + "2. **Measurable:**\n" + " - Objectives must be assessable using measurable performance indicators or outcomes.\n" + " - Correct: 'The trainee compares synchronous and asynchronous learning accurately.'\n" + " - Incorrect: 'The trainee understands e-learning.' (Not measurable)\n\n" + "3. **Achievable:**\n" + " - Realistic, suitable for learner ability and course duration.\n" + " - Correct: 'The trainee applies the steps to create a learning management account.'\n" + " - Incorrect: 'The trainee develops a full e-learning system in one hour.' (Unrealistic)\n\n" + "4. **Covers all three domains (Cognitive, Psychomotor, Affective):**\n" + " - Cognitive: 'The trainee defines e-learning accurately.'\n" + " - Psychomotor: 'The trainee practices using LMS assessment tools.'\n" + " - Affective: 'The trainee demonstrates interest in participating in e-learning activities.'\n\n" + "5. **Aligned with Bloom’s hierarchy:**\n" + " - Objectives must progress logically from lower to higher levels (Remember → Create).\n\n" + "6. **Clarity and precision:** Objective must be completely clear and specific.\n\n" + "7. **Free from ambiguity:** No vague or overly complex phrasing.\n\n" + "8. **Vocabulary clarity:** All terms must be understandable to {audience}.\n\n" + "9. **Single, focused goal:** Each objective expresses only one learning outcome.\n" + " - Correct: 'The trainee identifies the components of a virtual classroom.'\n" + " - Incorrect: 'The trainee identifies and applies virtual classroom components.'\n\n" + "10. **Realistic and feasible:** Achievable within program time and resources.\n\n" + "11. **Consistent with program outcomes:** Must align with the general and specific learning outcomes.\n\n" + "12. **Content-linked:** Directly related to the unit or module content.\n\n" + "### Objective Writing Rules\n" + "- One objective per subtopic — no more, no less.\n" + "- Each must directly and precisely reflect the subtopic’s purpose.\n" + "- Follow Bloom’s sequence within each unit (from lower to higher order thinking).\n" + "- If a level doesn’t logically fit, skip it but maintain general progression.\n" + "- You may combine two levels (e.g., Understand + Apply) if justified by subtopic purpose.\n" + "- Start each objective with '[verb in present]' and keep length between 6–10 words.\n" + "- Use verbs suitable for {audience} — e.g., children cannot 'evaluate' or 'analyze' like professionals.\n" + "- **Core rule: Avoid verb repetition — use the widest possible range from all three domains.**\n" + "- Ensure objectives are **measurable** and **achievable**.\n" + "- Maintain linguistic clarity — no vague or double meanings.\n" + "- Each objective must express only one precise learning outcome.\n\n" + "### Strategies for Verb Diversity\n" + "**Use these strategies to ensure maximum variety:**\n\n" + "- **Alternate synonyms:**\n" + " - Instead of always 'define', use: name, list, mention, identify, classify, select.\n" + "- **Use less common verbs:**\n" + " - Distinguish, infer, demonstrate, interpret, justify, summarize, illustrate.\n" + "- **Match verbs to content type:**\n" + " - Use verbs that capture the learning result precisely.\n" + "- **Consult the full domain verb lists** (above) with 5–7 verbs per level.\n" + "- **Distribute objectives across the three domains** — avoid focusing only on the cognitive domain.\n\n" + "### Examples of Correct and Incorrect Objectives\n\n" + "**Correct Examples (Good verb diversity and domain balance):**\n" + "1. (Cognitive - Remember): The trainee lists five types of e-learning platforms.\n" + "2. (Cognitive - Understand): The trainee distinguishes between various digital assessment tools.\n" + "3. (Cognitive - Apply): The trainee applies active learning strategies in the classroom.\n" + "4. (Cognitive - Understand): The trainee infers the main benefits of blended learning.\n" + "5. (Psychomotor - Practice): The trainee practices designing an interactive learning activity.\n" + "6. (Affective - Respond): The trainee actively participates in collaborative learning discussions.\n" + "7. (Cognitive - Analyze): The trainee compares different instructional design models.\n" + "8. (Psychomotor - Adapt): The trainee adjusts learning activities to meet learner needs.\n\n" + "**Incorrect Examples (Verb repetition — avoid this):**\n" + "1. The trainee defines the types of e-learning platforms.\n" + "2. The trainee defines digital assessment tools.\n" + "3. The trainee defines active learning strategies.\n" + "4. The trainee defines blended learning benefits.\n" + "5. The trainee defines steps of activity design.\n\n" + "**Unclear or Unmeasurable Objectives:**\n" + "- 'The trainee understands e-learning.' (Vague, not measurable)\n" + "- 'The trainee learns how to use tools.' (Unclear — which tools?)\n" + "- 'The trainee becomes an expert in instructional design.' (Unrealistic, not measurable)\n\n" + "**Improved Versions:**\n" + "- 'The trainee accurately defines e-learning.'\n" + "- 'The trainee effectively uses LMS tools.'\n" + "- 'The trainee applies instructional design principles to create a learning unit.'\n\n" + "### Critical Quality Checklist — Verify Before Finalizing Each Objective:\n" + "1. ✓ Is it clear and accurately expresses the intended outcome?\n" + "2. ✓ Does it align with a Bloom’s level or learning domain?\n" + "3. ✓ Is it free from ambiguity?\n" + "4. ✓ Is all vocabulary appropriate for {audience}?\n" + "5. ✓ Does it express only one learning outcome?\n" + "6. ✓ Is it measurable through observable performance?\n" + "7. ✓ Is it realistic and achievable?\n" + "8. ✓ Does it align with the overall outcomes?\n" + "9. ✓ Is it directly linked to the subtopic content?\n" + "10. ✓ Is it sequenced logically (simple → complex)?\n" + "11. ✓ Is it suitable for the learners’ level?\n" + "12. ✓ Do all objectives collectively cover knowledge, skills, and attitudes?\n" + "**13. Verb Check: Have you reused this verb? If yes, can you replace it?**\n" + "**14. Domain Check: Are the objectives balanced across all three domains?**\n\n" + "### Self-Reflection Requirement\n" + "After generating all objectives for a unit, pause and evaluate:\n" + "- Do objectives progress logically through Bloom’s levels?\n" + "- **How many unique verbs did you use? Any repetition?**\n" + "- **Can any repeated verbs be replaced with alternatives?**\n" + "- Are verbs diverse and contextually appropriate?\n" + "- **Do objectives cover all three domains evenly?**\n" + "- Can an external evaluator assess each objective clearly?\n" + "- Are all objectives realistic for {audience} and course duration?\n\n" + "### Final Verification: Verb Diversity & Domain Balance\n" + "**Before submitting your final output:**\n" + "1. Count how many times each verb appears.\n" + "2. If any verb appears more than 2–3 times, review and replace it where possible.\n" + "3. Aim for at least 70–80% unique verbs across the entire curriculum.\n" + "4. Keep a mental record of verb use for ongoing awareness.\n" + "5. **Check balanced distribution:**\n" + " - Cognitive (Knowledge): 50–60% of objectives\n" + " - Affective (Attitude): 20–30% of objectives\n" + " - Psychomotor (Skills): 20–30% of objectives\n" + "6. If imbalance occurs, revisit and adjust classifications.\n\n" + "### Domain-Specific Guidelines\n\n" + "**When writing Cognitive (Knowledge) objectives:**\n" + "- Use for topics focused on information, understanding, applying, analyzing, evaluating, or creating.\n" + "- Examples: 'The trainee defines...', 'The trainee compares...', 'The trainee designs...'.\n\n" + "**When writing Affective (Attitude) objectives:**\n" + "- Use for topics involving values, emotions, interests, or behaviors.\n" + "- Examples: 'The trainee demonstrates interest in...', 'The trainee participates actively...', 'The trainee commits to...'.\n\n" + "**When writing Psychomotor (Skills) objectives:**\n" + "- Use for topics requiring physical, technical, or procedural performance.\n" + "- Examples: 'The trainee practices...', 'The trainee performs...', 'The trainee innovates...'.\n\n" + "### Final Important Notes\n" + "- **Top Priority:** Verb diversity and balanced domain distribution.\n" + "- **Quality over quantity:** One well-crafted, diverse objective is better than several repetitive ones.\n" + "- **Flexibility with precision:** Be flexible in verb choice but never compromise on accuracy.\n" + "- **Continuous review:** Revisit objectives regularly for diversity and balance.\n" + "- **Context matters:** Always choose the verb that best reflects what the learner should achieve in that specific subtopic.\n" + "### OUTPUT\n" + "Return the LearningObjectivesOutput structure in valid JSON format.\n\n" + ), + expected_output="""JSON object with this structure: + { + "objectives": [ + { + "unit": "Unit title", + "unit_description": "Brief summary of the unit scope", + "subtopic": "subtopic title", + "module_description": "Brief summary of what this subtopic covers", + "level": "Bloom's level assigned based on subtopic analysis", + "verbs": ["verb1", "verb2", "verb3"], + "objective": "One measurable Arabic objective" + } + ] + }""", + output_json=LearningObjectivesOutput, + agent=objectives_agent, + human_input=False, + ) diff --git a/agents/analysis_phase/outcomes.py b/agents/analysis_phase/outcomes.py new file mode 100644 index 0000000000000000000000000000000000000000..4cb074c25cfd6f69fb453a2fd50e340b3d15f762 --- /dev/null +++ b/agents/analysis_phase/outcomes.py @@ -0,0 +1,308 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm_g +from schemas import LearningOutcomesOutput + +def outcomes_generator(): + model=llm_g() + return Agent( + role="Expert DNA-Based Learning Outcomes Designer", + goal="\n".join( + [ + "Design a comprehensive list of high-quality Arabic learning outcomes for a training program based on the learning objectives provided.", + "Each outcome must be measurable, aligned with Bloom's taxonomy, and crafted using the DNA methodology.", + "", + "### CRITICAL RULES FOR OUTCOMES:", + "", + "**1. LINGUISTIC STRUCTURE:**", + " - Outcomes MUST be phrased as infinitive nouns (مصادر) such as: فهم، إدراك، تطبيق، تحليل، تقييم، تصميم.", + " - Use the verbal noun form (المصدر) not the verb itself.", + " - Structure: [مصدر] + [المحتوى/الموضوع] + [السياق إن وُجد]", + " - Example: 'فهم مبادئ التعليم الإلكتروني' NOT 'أن يفهم المتدرب...'", + "", + "**2. BLOOM'S TAXONOMY ALIGNMENT:**", + " - Outcomes MUST be strictly ordered according to Bloom's taxonomy hierarchy:", + " • Remember (التذكر) → Understand (الفهم) → Apply (التطبيق) → Analyze (التحليل) → Evaluate (التقييم) → Create (الإبداع)", + " - Start from lower cognitive levels and progress to higher levels.", + " - Each outcome must accurately reflect its assigned Bloom's level.", + " - Use appropriate verbal nouns (مصادر) that match the cognitive level.", + "", + "**3. VERB/NOUN DIVERSITY:**", + " - CRITICAL: Maximize diversity in verbal nouns (مصادر) across all outcomes.", + " - Avoid repeating the same مصدر unless absolutely necessary.", + " - Use synonyms and varied expressions within the same Bloom's level.", + " - Track usage to ensure maximum variety throughout the outcomes list.", + "", + "**4. CLARITY AND CONCISENESS:**", + " - Each outcome must be concise: 10-15 words maximum.", + " - Use clear, unambiguous language appropriate for {audience}.", + " - Avoid complex grammatical structures or nested phrases.", + " - Ensure smooth, natural Arabic flow.", + "", + "**5. MEASURABILITY:**", + " - Every outcome must be observable and measurable.", + " - The outcome should clearly indicate what success looks like.", + " - Avoid vague or abstract outcomes that cannot be assessed.", + "", + "**6. DNA METHODOLOGY INTEGRATION:**", + "Apply DNA principles to ensure comprehensive coverage:", + " - **Declarative (Conceptual)**: Theories, principles, abstract concepts, definitions", + " Examples: فهم المفاهيم، إدراك المبادئ، معرفة النظريات", + " - **Procedural**: Step-by-step methods, processes, how-to knowledge", + " Examples: تطبيق الخطوات، تنفيذ الإجراءات، ممارسة الأساليب", + " - **Structural**: Frameworks, models, system-level understanding, relationships", + " Examples: تحليل النماذج، فحص الأطر، استكشاف العلاقات", + " - **Personal/Metacognitive**: Self-reflection, personal development, awareness", + " Examples: تقييم الذات، تطوير المهارات الشخصية، إدراك القدرات", + " - **Realistic/Contextual**: Real-world application, case studies, practical scenarios", + " Examples: تطبيق في سياقات واقعية، حل مشكلات حقيقية، توظيف في الميدان", + "", + "Ensure outcomes collectively cover multiple DNA dimensions for holistic learning.", + ] + ), + backstory="\n".join( + [ + "You are an elite instructional designer specializing in outcomes-based education and the DNA knowledge framework.", + "You have deep expertise in:", + " - Arabic linguistic structures, particularly verbal nouns (المصادر)", + " - Bloom's Taxonomy and its precise application to learning outcomes", + " - DNA methodology for comprehensive knowledge representation", + " - Measurable outcome design that can be clearly assessed", + "", + "You analyze learning objectives carefully to extract the core learning achievements,", + "then synthesize them into well-crafted outcomes that are:", + " • Linguistically precise and natural in Arabic", + " • Aligned with the appropriate Bloom's cognitive level", + " • Diverse in vocabulary to avoid monotony", + " • Measurable and realistic for the {audience}", + " • Comprehensive in covering declarative, procedural, structural, personal, and realistic dimensions", + "", + "You consider the ({topic}, {domain}, {content_type}, {audience}, {material_type}) context", + "to ensure outcomes are appropriate, achievable, and relevant.", + "", + "You maintain a mental tracker of all verbal nouns (مصادر) used to maximize diversity across outcomes.", + ] + ), + llm=model, + verbose=True, + max_iter=100, +) + +def outcomes_task(outcomes_agent): + return Task( + description="\n".join( + [ + "You are given a set of learning objectives in: {objectives}.", + "", + "Your task is to analyze these objectives and generate high-quality Arabic learning outcomes", + "that are measurable, aligned with Bloom's taxonomy, and designed using DNA methodology.", + "", + "### STEP-BY-STEP PROCESS:", + "", + "**STEP 1: ANALYZE THE OBJECTIVES**", + " - Read all objectives carefully to understand the complete learning journey.", + " - Identify the main knowledge domains, skills, and behaviors covered.", + " - Group objectives by Bloom's taxonomy levels.", + " - Note the DNA dimensions present (declarative, procedural, structural, personal, realistic).", + "", + "**STEP 2: DETERMINE OUTCOME STRUCTURE**", + " - Decide how many outcomes are needed (typically one per Bloom's level that has objectives).", + " - If multiple objectives exist at one Bloom's level, synthesize them into ONE comprehensive outcome.", + " - Ensure outcomes progress logically from Remember → Create.", + "", + "**STEP 3: SELECT APPROPRIATE VERBAL NOUNS (مصادر)**", + " - For each Bloom's level, review the available verbal nouns (provided below).", + " - Select a مصدر that:", + " • Accurately represents the cognitive level", + " • Has NOT been used in previous outcomes (maximize diversity)", + " • Is appropriate for the {audience}", + " • Matches the DNA dimension of the content", + " - Maintain a mental list of used مصادر to avoid repetition.", + "", + "**STEP 4: CRAFT EACH OUTCOME**", + " - Structure: [مصدر] + [core content/concept] + [context if needed]", + " - Keep it concise: 6-10 words maximum.", + " - Ensure it's measurable and observable.", + " - Use natural, flowing Arabic.", + " - Verify it synthesizes all related objectives at that level.", + "", + "**STEP 5: QUALITY VERIFICATION**", + " - Check each outcome against the 12 quality criteria (see below).", + " - Ensure verb diversity across all outcomes.", + " - Verify Bloom's sequence is maintained.", + " - Confirm DNA dimensions are covered.", + "", + "### COMPREHENSIVE VERBAL NOUNS (مصادر) BY BLOOM'S LEVEL:", + "", + "Use these extensive lists to maximize diversity:", + "", + "**Remember (التذكر) - Declarative Focus:**", + "تعريف، تحديد، تسمية، ذكر، كتابة، عرض، سرد، تعداد، حفظ، استرجاع، تكرار، معرفة، إدراج، إشارة، اختيار", + "", + "**Understand (الفهم) - Conceptual Focus:**", + "فهم، إدراك، توضيح، شرح، تفسير، تلخيص، وصف، مناقشة، استنتاج، تمثيل، ترجمة، تعبير، صياغة، إعادة صياغة، استيعاب", + "", + "**Apply (التطبيق) - Procedural Focus:**", + "تطبيق، استخدام، توظيف، ممارسة، تنفيذ، إجراء، حل، تشغيل، إنجاز، ربط، تجريب، اختبار، عرض، إعداد، إظهار", + "", + "**Analyze (التحليل) - Structural Focus:**", + "تحليل، فحص، تمييز، مقارنة، تصنيف، فصل، تفريق، استقصاء، استكشاف، اكتشاف، ربط، تنظيم، تجزئة، دراسة، بحث", + "", + "**Evaluate (التقييم) - Metacognitive/Critical Focus:**", + "تقييم، حكم، نقد، تبرير، دعم، توصية، اختيار، مراجعة، دفاع، تقدير، فحص نقدي، موازنة، مفاضلة، ترجيح، إصدار حكم", + "", + "**Create (الإبداع) - Generative/Innovative Focus:**", + "تصميم، إنشاء، ابتكار، تطوير، بناء، تأليف، تخطيط، تركيب، صياغة، اقتراح، إعداد، إنتاج، تكوين، توليد، ابتداع", + "", + "### DNA METHODOLOGY INTEGRATION GUIDE:", + "", + "Ensure your outcomes collectively address multiple DNA dimensions:", + "", + "**Declarative/Conceptual Outcomes (مصادر مناسبة):**", + " - Focus on: concepts, definitions, theories, principles", + " - Verbs: تعريف، فهم، إدراك، معرفة، استيعاب", + " - Example: فهم المبادئ الأساسية للتعلم الإلكتروني", + "", + "**Procedural Outcomes (مصادر مناسبة):**", + " - Focus on: methods, steps, processes, how-to", + " - Verbs: تطبيق، تنفيذ، ممارسة، استخدام، إجراء", + " - Example: تطبيق خطوات تصميم محتوى تعليمي رقمي", + "", + "**Structural Outcomes (مصادر مناسبة):**", + " - Focus on: frameworks, models, relationships, systems", + " - Verbs: تحليل، فحص، استكشاف، ربط، تنظيم", + " - Example: تحليل العلاقة بين استراتيجيات التعليم وأنواع المحتوى", + "", + "**Personal/Metacognitive Outcomes (مصادر مناسبة):**", + " - Focus on: self-awareness, reflection, personal development", + " - Verbs: تقييم الذات، تطوير، إدراك، تحسين، تنمية", + " - Example: تقييم المهارات الشخصية في إدارة الوقت", + "", + "**Realistic/Contextual Outcomes (مصادر مناسبة):**", + " - Focus on: real-world application, case studies, practical use", + " - Verbs: توظيف في سياق واقعي، حل مشكلات، تطبيق ميداني", + " - Example: حل مشكلات تعليمية واقعية باستخدام التقنيات الحديثة", + "", + "### QUALITY CRITERIA CHECKLIST (12+2 Criteria):", + "", + "Before finalizing each outcome, verify:", + "", + "✓ 1. **Correct linguistic form**: Uses مصدر (infinitive noun), not verb", + "✓ 2. **Bloom's alignment**: Accurately reflects the assigned cognitive level", + "✓ 3. **Clear and unambiguous**: No vague or confusing language", + "✓ 4. **Appropriate vocabulary**: Suitable for {audience} level", + "✓ 5. **Single focus**: Expresses ONE outcome, not multiple combined", + "✓ 6. **Measurable**: Can be observed or assessed", + "✓ 7. **Realistic**: Achievable within program scope and time", + "✓ 8. **Aligned with objectives**: Synthesizes related learning objectives", + "✓ 9. **Content-relevant**: Directly relates to {topic} and {domain}", + "✓ 10. **Logical sequence**: Follows Bloom's order (low to high)", + "✓ 11. **Audience-appropriate**: Matches {audience} capabilities", + "✓ 12. **Comprehensive**: Covers knowledge, skills, attitudes collectively", + "✓ 13. **DIVERSE VOCABULARY**: Avoids repetition of مصادر", + "✓ 14. **DNA-aligned**: Reflects appropriate DNA dimension(s)", + "", + "### EXAMPLES OF HIGH-QUALITY OUTCOMES:", + "", + "**✅ EXCELLENT EXAMPLES:**", + "", + "**Remember Level:**", + "تعريف المصطلحات الأساسية في التعلم الإلكتروني بدقة.", + "(Declarative, clear, measurable, concise, uses تعريف)", + "", + "**Understand Level:**", + "إدراك العلاقة بين أنماط التعلم واستراتيجيات التدريس.", + "(Structural understanding, uses إدراك, different from تعريف)", + "", + "**Apply Level:**", + "توظيف أدوات التقييم الرقمية في بيئة تعليمية فعلية.", + "(Procedural + Realistic, uses توظيف, context-specific)", + "", + "**Analyze Level:**", + "تمييز نقاط القوة والضعف في نماذج التصميم التعليمي.", + "(Structural analysis, uses تمييز, critical thinking)", + "", + "**Evaluate Level:**", + "مقارنة فعالية استراتيجيات التعليم المختلفة وفق معايير محددة.", + "(Evaluative, uses مقارنة, criteria-based)", + "", + "**Create Level:**", + "ابتكار حلول إبداعية لتحديات التعليم عن بُعد.", + "(Generative, uses ابتكار, innovative focus)", + "", + "**❌ POOR EXAMPLES (Avoid These):**", + "", + "❌ 'أن يفهم المتدرب مفهوم التعليم الإلكتروني'", + "(Wrong: uses verb form, not مصدر; includes 'أن يفهم المتدرب')", + "", + "❌ 'فهم التعليم الإلكتروني'", + "(Too vague, not measurable, lacks specificity)", + "", + "❌ 'فهم وتطبيق مبادئ التعليم الإلكتروني'", + "(Double outcome, combines two Bloom's levels)", + "", + "❌ 'تعريف أنواع التعليم الإلكتروني وتصنيف منصات التعلم'", + "(Multiple outcomes in one, too long)", + "", + "❌ Using 'تعريف' for all Remember-level outcomes", + "(Lacks diversity, repetitive)", + "", + "### CRITICAL VERB DIVERSITY REQUIREMENT:", + "", + "**🚨 PRIMARY INSTRUCTION: Maximize مصدر diversity across all outcomes**", + "", + "**Diversity Rules:**", + "1. Before selecting a مصدر, check: Have I used this before?", + "2. If yes, select an alternative unless no suitable synonym exists.", + "3. Use the full range of available مصادر at each Bloom's level.", + "4. Aim for 80-100% unique مصادر across all outcomes.", + "5. Track usage mentally as you craft each outcome.", + "", + "**Diversity Strategies:**", + "- Rotate through synonyms systematically", + "- Match مصدر to specific DNA dimension when possible", + "- Use more specific مصادر rather than generic ones", + "- Consider the nuance each مصدر brings to meaning", + "", + "### SELF-REFLECTION AFTER GENERATION:", + "", + "After creating all outcomes, review and ask:", + "- Do outcomes progress clearly from Remember to Create?", + "- Is there good diversity in مصادر (minimal repetition)?", + "- Do outcomes collectively cover DNA dimensions?", + "- Are all outcomes measurable and realistic for {audience}?", + "- Does each outcome synthesize its related objectives effectively?", + "- Is the Arabic natural, clear, and linguistically correct?", + "- Would an evaluator be able to assess achievement of each outcome?", + "", + "### FINAL QUALITY ASSURANCE:", + "", + "Before submitting, verify:", + "✓ All outcomes use مصدر form (not verbs)", + "✓ Bloom's sequence is maintained throughout", + "✓ Each outcome is 10-15 words maximum", + "✓ Vocabulary is appropriate for {audience}", + "✓ DNA methodology is applied", + "✓ Outcomes are measurable and realistic", + "✓ Minimal repetition of مصادر (high diversity)", + "✓ Each outcome synthesizes multiple related objectives where applicable", + "", + "### OUTPUT\n" + "Return the LearningObjectivesOutput structure in valid JSON format.\n\n", + ] + ), + expected_output="""JSON object with this structure: + { + "objectives": [ + { + "unit": "Unit title", + "unit_description": "Brief summary of the unit scope", + "unit_outcome": "One measurable Arabic objective derived from the unit description and its subtopics and their purposes", + } + ], + }""", + output_json=LearningOutcomesOutput, + agent=outcomes_agent, + human_input=False, +) diff --git a/agents/analysis_phase/outliner.py b/agents/analysis_phase/outliner.py new file mode 100644 index 0000000000000000000000000000000000000000..82dae0eeaef509fceccfa57cb6131076163e7487 --- /dev/null +++ b/agents/analysis_phase/outliner.py @@ -0,0 +1,100 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm +#from schemas import CurriculumOutline + +# ========= 4. Outline Agent ========= +def outline_agent(): + model=llm() + return Agent( + role="Scientific Outline Generator", + goal="\n".join( + [ + "Generate a complete and scientifically accurate training curriculum for the topic: {topic}.", + "The curriculum must be structured, logically sequenced, and pedagogically useful.", + "Avoid vague, redundant, or repetitive points—each topic must be unique and non-overlapping.", + "Ensure strong logical flow: units must progress according to Bloom’s taxonomy principles—starting from simple recall/understanding, moving to deeper analysis, application, evaluation, and finally creation/innovation.", + "Important: Do NOT explicitly use Bloom verbs (e.g., analyze, evaluate, create). Instead, embed the cognitive levels implicitly in the phrasing of topics.", + "When generating topics, always use terms and concepts that are common, searchable, and can be easily found in Arabic or international online sources (articles, courses, books).", + "Avoid overly creative or abstract titles that are difficult to map to real content.", + "Follow this strict format:", + "- Output must be in JSON.", + "- Curriculum must include exactly 4 units.", + "- Each unit must have exactly 5 sub-topics.", + "- Units must be output with keys: unit_title, topics.", + "Each unit must reflect and cover one of the core concepts of the topic, so that together they encompass all aspects of the subject.", + "The 4 units must be interconnected, sequentially linked, and collectively ensure full coverage of all concepts of the topic, leaving no gaps.", + "Ensure all text is written in **Arabic language**.", + "Units should be connected and form a comprehensive training program that gradually increases in cognitive depth according to Bloom’s taxonomy.", + "Always adapt to metadata: domain ({domain}), content_type ({content_type}), audience ({audience}), material_type ({material_type}).", + "Apply DNA methodology explicitly in topic design:", + "- material_type:", + "-- Conceptual: theories, principles, abstract knowledge.", + "-- Procedural: step-by-step instructions or methods.", + "-- Structural: frameworks, models, or system views.", + "-- Personal: learner reflection, skills, or self-development aspects.", + "-- Realistic: real-world examples, applications, or case studies.", + "- content_type:", + "-- Educational → for structured learning (e.g., lessons, modules)", + "-- Awareness → for informative/raising awareness purposes (e.g., campaigns, workshops)", + "-- Training → for skill development with step-by-step practice (e.g., hands-on, exercises)", + "Important: The curriculum title must be identical to the input topic, and unit titles must reflect or embed the meaning of this title.", + ] + ), + backstory="\n".join( + [ + "This agent acts as both a scientific researcher and curriculum designer.", + "It generates structured outlines in the form of 4 units × 5 topics, aligned with DNA methodology.", + "It ensures progression follows Bloom’s taxonomy: Unit 1 covers basic recall/understanding, Unit 2 focuses on application of methods/tools, Unit 3 emphasizes analysis and evaluation in real contexts, and Unit 4 explores integration, innovation, or advanced synthesis.", + "The topics must always be realistic, practical, and searchable in Arabic or global sources to ensure that real content can be gathered later.", + "The final output must always be in Arabic.", + "The curriculum should progress from simple to complex, reflecting Bloom’s cognitive hierarchy, but without explicitly naming Bloom’s verbs in the text.", + ] + ), + llm=model, + verbose=True, + reasoning=True, + max_reasoning_attempts=3, + max_iter=100, +) + + +# ========= 5. Task ========= +def outline_task(outline_agent): + return Task( + description="\n".join( + [ + "Generate a structured outline for the topic: {topic}.", + "The output must strictly follow this format:", + "- JSON object with keys: 'title' and 'units'.", + "- 'title': must be exactly the same as the input topic (in Arabic).", + "- 'units': exactly 4 items.", + "- Each unit must have:", + " - unit_title (string, Arabic, must reflect or embed the curriculum title meaning)", + " - topics (list of exactly 5 short topics, in Arabic).", + "Ensure the units flow logically, progressively, and cover the subject fully.", + "The units must align with Bloom’s taxonomy progression: Unit 1 = basic knowledge & understanding, Unit 2 = tools & applications, Unit 3 = analysis & evaluation, Unit 4 = synthesis & advanced innovation.", + "Do NOT explicitly use Bloom’s action verbs; express the cognitive levels implicitly in the phrasing of topics.", + "Each topic must embed one aspect of DNA methodology (Conceptual, Procedural, Structural, Personal, Realistic) and (content_type: Training, Awareness, Educational).", + "All topics must be formulated using widely recognized and searchable concepts (to allow collecting real content from online sources).", + "Avoid repetition—no two topics should overlap in meaning.", + "Do not add extra fields outside the schema.", + "The entire output must be in Arabic.", + ] + ), + expected_output=( + "A valid JSON object with this structure:\n" + "{\n" + ' "title": "string (Arabic curriculum title)",\n' + ' "units": [\n' + " {\n" + ' "unit_title": "string (Arabic, must reflect the title)",\n' + ' "topics": ["topic1", "topic2", "topic3", "topic4", "topic5"]\n' + " }, ... (4 units total)\n" + " ]\n" + "}" + ), + #output_json=CurriculumOutline, + agent=outline_agent, +) diff --git a/agents/books/__init__.py b/agents/books/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0ea4a40e52f7eb01c56e661511923ae8f5bc7121 --- /dev/null +++ b/agents/books/__init__.py @@ -0,0 +1,37 @@ +from .units_structure import ( + course_structuring_agent, + course_structuring_task, +) +from .book_structure import ( + book_structure_agent, + generate_book_outline_task, +) +from .book_planning import ( + agent_task_planning +) +from .book_content import ( + make_writer_crew, + make_quality_crew, +) +from .book_content_gpt import ( + make_writer_crew_gpt, + make_quality_crew_gpt, +) +from .queries_agent import ( + query_agent, + query_task, +) +from .units_intro import ( + intro_generator, + intro_generation_task, +) +from .intro_plan import( + intro_style_planner, + intro_style_planner_task +) +from .auto_units_structure import ( + book_units_langchain_agent, +) +from .book_content_new import ( + make_writer_editor_crew +) \ No newline at end of file diff --git a/agents/books/apa_agent.py b/agents/books/apa_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..4fddefdf4a2a69c9f591d7114049ef7dd32a9ed4 --- /dev/null +++ b/agents/books/apa_agent.py @@ -0,0 +1,160 @@ +import json +from crewai import Agent, Task, Crew, Process +from schemas.books.sources_schema import SourceMetadataAgentResult +from modules import llm_oss_120b + + +def run_metadata_agent(source: dict) -> dict: + """ + Receives SourceForAgent as dict. + Extracts academic metadata with semantic understanding (no hallucination). + """ + + model = llm_oss_120b() + + # ===================================================== + # Agent + # ===================================================== + metadata_agent = Agent( + name="SourceMetadataExtractor", + role="Academic bibliographic metadata analyst", + goal=( + "فهم النص الأكاديمي وتحليل بنيته " + "لاستخراج البيانات الببليوجرافية الصحيحة " + "من غير تخمين أو هلوسة." + ), + backstory=( + "أنت أخصائي فهرسة أكاديمية بخبرة طويلة. " + "تفهم شكل الكتب، الرسائل الجامعية، " + "الأبحاث، والتقارير المؤسسية حتى لو لم تُعنون صراحة. " + "تعتمد على الفهم البنيوي للنص " + "لكن ترفض أي معلومة غير مؤكدة." + ), + llm=model, + verbose=False, + memory=False, + allow_delegation=False, + ) + + # ===================================================== + # Task (Arabic – semantic but strict) + # ===================================================== + metadata_task = Task( + name="Semantic extraction of academic bibliographic metadata", + description=( + "سيتم تزويدك بمصدر أكاديمي واحد بصيغة JSON.\n\n" + "============================\n" + "شكل بيانات الإدخال\n" + "============================\n" + "بيانات الإدخال عبارة عن كائن JSON يحتوي على المفاتيح التالية:\n\n" + "1) source_url:\n" + "- رابط الملف (للتوثيق فقط)\n\n" + "2) source_type:\n" + "- نوع تقني تقريبي (pdf / html)\n\n" + "3) domain:\n" + "- نطاق الموقع (قرينة ضعيفة)\n\n" + "4) text_pages:\n" + "- قائمة نصوص مستخرجة من أول صفحات المصدر\n" + "- هذا هو المصدر الحقيقي الوحيد للمعلومات\n\n" + "⚠️ تجاهل أي حقول أخرى إن وُجدت.\n\n" + "============================\n" + "مهم: كيفية فهم النص\n" + "============================\n" + "قد لا تكون البيانات مكتوبة بصيغة صريحة مثل:\n" + "«العنوان:» أو «المؤلف:».\n\n" + "مطلوب منك تحليل النص بنيويًا:\n" + "- العنوان: النص الذي يمثل اسم العمل كاملًا\n" + " (غالبًا في البداية، مميز، مكرر، أو منفصل)\n\n" + "- المؤلف:\n" + " اسم الشخص أو الجهة المسؤولة عن العمل نفسه\n" + " (وليس المشرف، وليس دار النشر)\n\n" + "- السنة:\n" + " اختر سنة النشر الصحيحة فقط\n" + " إذا وُجد أكثر من تاريخ:\n" + " ▪ استبعد التواريخ الهجرية\n" + " ▪ استبعد فترات دراسية\n" + " ▪ اختر السنة المرتبطة مباشرة بالنشر أو الإجازة العلمية\n\n" + "- الناشر / الجهة:\n" + " الجهة التي أصدرت العمل (جامعة، مجلة، مؤسسة)\n\n" + "============================\n" + "قواعد صارمة (لا يمكن كسرها)\n" + "============================\n" + "1) لا تُنشئ بيانات غير موجودة\n" + "2) لا تُصحح أخطاء لغوية أو OCR\n" + "3) لا تُكمل أسماء ناقصة\n" + "4) لا تعتمد على الرابط أو اسم الموقع\n" + "5) إذا وُجد شك → اعتبر المعلومة غير موجودة\n\n" + "============================\n" + "تصنيف نوع المصدر\n" + "============================\n" + "- book: كتاب مطبوع أو إلكتروني\n" + "- thesis: رسالة جامعية (ماجستير / دكتوراه)\n" + "- journal_article: مقال في مجلة علمية\n" + "- institutional_report: تقرير رسمي أو جامعي\n" + "- website: صفحة ويب\n\n" + "============================\n" + "التحقق من الصلاحية\n" + "============================\n" + "is_valid = true فقط إذا:\n" + "- يوجد عنوان مفهوم بوضوح\n" + "- يوجد مؤلف واحد على الأقل\n\n" + "غير ذلك:\n" + "- is_valid = false\n" + "- reason = سبب مختصر\n\n" + "============================\n" + "الإخراج\n" + "============================\n" + "- JSON فقط\n" + "- بدون شرح\n" + "- مطابق تمامًا لـ SourceMetadataAgentResult\n\n" + "============================\n" + "بيانات الإدخال:\n" + "{source}\n" + ), + expected_output="JSON مطابق لـ SourceMetadataAgentResult فقط.", + output_json=SourceMetadataAgentResult, + agent=metadata_agent, + ) + + # ===================================================== + # Crew + # ===================================================== + crew = Crew( + agents=[metadata_agent], + tasks=[metadata_task], + process=Process.sequential, + verbose=False, + memory=False, + ) + + result = crew.kickoff(inputs={"source": source}) + + tokens={} + if result.token_usage: + tokens = result.token_usage.dict() + + # ===================================================== + # Normalize output (SAFE) + # ===================================================== + if hasattr(result, "json_dict"): + jd = result.json_dict + return (jd() if callable(jd) else jd), tokens + + if hasattr(result, "model_dump"): + md = result.model_dump + return (md() if callable(md) else md), tokens + + try: + parsed = json.loads(str(result)) + if isinstance(parsed, dict): + return parsed, tokens + except Exception: + pass + + return { + "is_valid": False, + "reason": "agent_output_unparseable", + "normalized_source_type": None, + "metadata": None, + "evidence": None, + } diff --git a/agents/books/auto_units_structure.py b/agents/books/auto_units_structure.py new file mode 100644 index 0000000000000000000000000000000000000000..6a2ccd36dc00cadc19559415cec9f4a688956f7b --- /dev/null +++ b/agents/books/auto_units_structure.py @@ -0,0 +1,123 @@ +from modules import model, llm_g +import json +import re +from langchain_core.messages import SystemMessage, HumanMessage + +def book_units_langchain_agent( + topic: str, + units_number: int, + notes: str | None = None, + course_description: str | None = None, + course_audience: str | None = None, +): + + # ---------- Step 1 : Generate Markdown Outline ---------- + llm = llm_g() + + notes_block = "" + if notes: + notes_block = f""" +Additional author notes (optional guidance): +{notes} + +Use these notes to guide the course structure but do NOT copy them literally. +""" + + description_block = "" + if course_description: + description_block = f""" +Book description (guidance): +{course_description} + +Use it to understand the theme and depth of the course. +Do NOT copy it literally in the output. +""" + + audience_block = "" + if course_audience: + audience_block = f""" +Target audience (guidance): +{course_audience} + +Use it to adapt the level and focus of the units. +""" + + result = llm.call( + f""" +Create a structured outline for a professional course about [{topic}]. + +The course should contain exactly {units_number} units. + +For each unit include: +- Unit title +- Learning outcome +- Main topics + +{description_block} + +{audience_block} + +{notes_block} + +Return the outline in Markdown. +Focus on logical learning progression. +""" + ) + + # ---------- Step 2 : System Prompt for JSON extraction ---------- + system_prompt = f""" +Extract structured course data from the Markdown text. + +Return JSON with EXACTLY this structure: + +{{ + "course_name": "{topic}", + "course_description": "string", + "course_audience": "string", + "learning_outcomes": [ + "string" + ], + "units": [ + {{ + "unit_name": "string", + "outcome": "string", + "topics": [ + "string" + ] + }} + ] +}} + +Rules: + +- If course_description is missing, generate one +- If course_audience is missing, generate one +- Generate 4-6 topics per unit +- Units must equal {units_number} +- unit_name should look like: "الفصل الأول: ..." +- topics must be short educational titles +- Output ONLY valid JSON +- No markdown +- No explanations +""" + + # ---------- Step 3 : Call Model ---------- + llm_model = model() + + response = llm_model.invoke( + [ + SystemMessage(content=system_prompt), + HumanMessage(content=result), + ] + ) + + raw_output = response.content.strip() + + # ---------- Step 4 : Clean JSON ---------- + try: + structured_data = json.loads(raw_output) + except json.JSONDecodeError: + cleaned = re.sub(r"```json|```", "", raw_output).strip() + structured_data = json.loads(cleaned) + + return structured_data diff --git a/agents/books/book_cohesion.py b/agents/books/book_cohesion.py new file mode 100644 index 0000000000000000000000000000000000000000..8658c58c4878a94d1925d1c5e42e8fda54df6476 --- /dev/null +++ b/agents/books/book_cohesion.py @@ -0,0 +1,113 @@ +from crewai import Agent, Task +from modules import llm_open_grok +from schemas.books.book_cohesion import CohesionOutput +COHESION_AGENT_SYSTEM = """ +[ROLE] +أنت محرر سردي لكتاب عربي غير روائي. +لا تُولّد معرفة جديدة، ولا تُعيد كتابة المحتوى من الصفر. +مهمتك تحسين التماسك السردي والصوت الواحد عبر الفصول. + +──────────────────────────────── +[SCOPE – STRICT] +──────────────────────────────── +- يُمنع إضافة أفكار جديدة أو أمثلة جديدة من عندك. +- يُمنع حذف الأفكار الأساسية. +- يُمنع تعديل الاستشهادات أو قائمة المصادر أو صيغها. +- يُسمح فقط بـ: + 1) تعديل أول فقرة في كل فصل. + 2) تعديل آخر فقرة في كل فصل. + 3) إضافة جملة ربط واحدة عند الحاجة بين الفصول. + 4) إصلاح لغوي/ترقيم خفيف داخل الفقرتين المعدلتين فقط. + + +──────────────────────────────── +[NARRATIVE GOALS] +──────────────────────────────── +- جعل الفصول تُقرأ كجسد واحد لا كمقالات منفصلة. +- تثبيت صوت المؤلف (نبرة واحدة) عبر الفصول. +- ضمان تراكم فكري: ما ينتهي في فصل يمهّد لما بعده دون تكرار. +- منع التكرار الافتتاحي المعتاد (التعريفات المتشابهة في كل فصل). + +──────────────────────────────── +[RESEARCH DIGESTION RULE – CRITICAL] +──────────────────────────────── +- إذا كان المحتوى يتضمن عرضًا بحثيًا خامًا (أرقام/اختبارات/جداول): + لا تُدرج التفاصيل الرقمية داخل الفقرتين المعدلتين. + ركّز على "الدلالة" لا "البيانات". +- لا تحذف المعلومة من جسم الفصل؛ فقط تجنب تضخيمها في المدخل/الخاتمة. + +──────────────────────────────── +[LANGUAGE & PUNCTUATION – STRICT] +──────────────────────────────── +- العربية الفصحى المعاصرة. +- استخدام الفاصلة (،) داخل الجملة. +- الفاصلة المنقوطة (؛) عند الانتقال لتعليل/تفصيل. +- النقطة (.) لإنهاء فكرة مكتملة. +- لا جمل طويلة متكدسة؛ اجعل الإيقاع مقروءًا. + +──────────────────────────────── +[OUTPUT CONTRACT – STRICT] +──────────────────────────────── +أخرج JSON فقط بالشكل التالي: +{ + "edited_chapters": [...], + "narrative_memory": { + "central_thread": "", + "current_voice": "", + "key_concepts_introduced": [], + "concepts_to_continue": [], + "do_not_repeat": [], + "tone_continuity_notes": "" + } +} +""" + +COHESION_TASK_INSTRUCTIONS = """ +[INPUT] +{chunk_payload} +ستستقبل: +- book_meta +- chapters_chunk (2-3 فصول عادة) +- previous_narrative_memory (قد تكون null) + + +[TASK] +1) لكل فصل في chapters_chunk: + - عدّل فقط أول فقرة من أول محور/قسم يظهر فعليًا في النص (أول فقرة ضمن أول subsection موجود). + - وعدّل فقط آخر فقرة من آخر محور/قسم (آخر فقرة ضمن آخر subsection). + - إن لم يكن تقسيم الفقرات واضحًا: اعتبر أول 3-5 أسطر كـ "فقرة أولى" وآخر 3-5 أسطر كـ "فقرة أخيرة". +2) لا تغيّر باقي النص. +3) أضف جملة ربط واحدة فقط (عند الحاجة) في آخر فقرة من الفصل، تمهّد للفصل التالي. +4) حدّث narrative_memory: + - central_thread: خيط واحد يصف مسار الكتاب حتى الآن بجملة. + - current_voice: وصف موجز لصوت الكاتب (مثل: "تفسيري مهني تطبيقي"). + - key_concepts_introduced: أهم المصطلحات/الأفكار التي ظهرت لأول مرة في هذه الدفعة. + - concepts_to_continue: أفكار يجب استمرارها لاحقًا (تمهيد). + - do_not_repeat: جمل/تعريفات/افتتاحيات لا يجب تكرارها لاحقًا. + - tone_continuity_notes: ملاحظات قصيرة لضبط النبرة في الدفعة القادمة. + +[IMPORTANT] +- لا تضف أي قسم جديد. +- لا تلخص الفصول. +- لا تكتب خارج JSON. +""" + +def build_cohesion_agent(): + model = llm_open_grok() + return Agent( + role="محرر سردي", + goal="تحسين التماسك السردي والصوت الواحد عبر الفصول دون إضافة محتوى جديد", + backstory=COHESION_AGENT_SYSTEM, + llm=model, + verbose=False, + allow_delegation=False, + ) + + +def build_cohesion_task(agent): + return Task( + description=COHESION_TASK_INSTRUCTIONS, + agent=agent, + expected_output="JSON فقط يحتوي edited_chapters و narrative_memory", + output_json=CohesionOutput, + ) diff --git a/agents/books/book_content.py b/agents/books/book_content.py new file mode 100644 index 0000000000000000000000000000000000000000..b6bf6633d29b88ec2454eb34c1dd5d4c64fc5bd3 --- /dev/null +++ b/agents/books/book_content.py @@ -0,0 +1,867 @@ +# -*- coding: utf-8 -*- +""" +Arabic Book Subsection Pipeline (CrewAI) +- Writer (RAG-Strict + APA7 + Style-Exact) +- Quality Gate (developmental edit + hard QA) +- Optional JSON Contract Repair +Compatible with Grok 4.1 fast style prompting (clear context, explicit goals, strict output contract). +""" + +import os +import re +import json +from typing import Any, Dict, List + +# --------------------------------------------------------- +# Runtime flags +# --------------------------------------------------------- +os.environ["CREWAI_TRACING_ENABLED"] = "false" +os.environ["CREWAI_DISABLE_TELEMETRY"] = "true" + +from crewai import Agent, Task, Crew, Process +from modules import llm_4m, llm_open_grok + +# --------------------------------------------------------- +# Helpers +# --------------------------------------------------------- + + +def _to_text(result: Any) -> str: + """Normalize Crew result object to raw text.""" + if result is None: + return "" + if hasattr(result, "raw") and isinstance(result.raw, str): + return result.raw.strip() + return str(result).strip() + + +def extract_strict_json(text: str) -> Dict[str, Any]: + """ + Extract the first JSON object from LLM output and validate contract. + Contract: {"generated_content": str, "sources": list[str]} + """ + if not text: + return {"generated_content": "", "sources": []} + + # fast path + try: + obj = json.loads(text) + return validate_contract(obj) + except Exception: + pass + + # extract first JSON object + match = re.search(r"\{[\s\S]*\}", text) + if not match: + return {"generated_content": "", "sources": []} + + blob = match.group(0) + + # remove trailing commas before } or ] + blob = re.sub(r",\s*([}\]])", r"\1", blob) + + try: + obj = json.loads(blob) + return validate_contract(obj) + except Exception: + return {"generated_content": "", "sources": []} + + +def validate_contract(obj: Dict[str, Any]) -> Dict[str, Any]: + """Force exact output schema and safe types.""" + if not isinstance(obj, dict): + return {"generated_content": "", "sources": []} + + content = obj.get("generated_content", "") + sources = obj.get("sources", []) + + if not isinstance(content, str): + content = str(content) + + if not isinstance(sources, list): + sources = [] + + clean_sources: List[str] = [] + for s in sources: + if isinstance(s, str): + s2 = s.strip() + if s2 and s2 not in clean_sources: + clean_sources.append(s2) + + return {"generated_content": content.strip(), "sources": clean_sources} + + +# --------------------------------------------------------- +# Prompt blocks +# --------------------------------------------------------- +WRITER_DESCRIPTION = """ +أنت مؤلف كتاب عربي غير روائي، وتكتب محورًا فرعيًا واحدًا فقط. +مهمتك إنتاج نص نهائي صالح للنشر، ملتزم حرفيًا بالخطة والنطاق، +وبأسلوب تحليلي إنساني غير نمطي، لا يشبه الأوراق البحثية. +تفضّل الكتابة التفسيرية التحفظية على ترك الناتج فارغًا. + +================================================= +📌 مراعاة نوع الكتاب +================================================= + +يجب أن يتكيّف أسلوب الشرح مع طبيعة موضوع الكتاب. + +أمثلة: + +- كتاب تدريبي (مثل تدريب كرة القدم): +التركيز على التطبيق، الخطوات العملية، السيناريوهات التدريبية. + +- كتاب تعليمي: +التركيز على الشرح المفاهيمي والأمثلة التوضيحية. + +- كتاب تحليلي أو فكري: +التركيز على التحليل والتفسير والربط بين الأفكار. + +لا تستخدم نفس أسلوب العرض لجميع أنواع الكتب. + +================================================= +📌 أولًا: وعي مستوى الكتاب (Book-Level Awareness) +================================================= +- النص جزء من كتاب متكامل، وليس فصلًا مستقلًا ولا ورقة بحثية. +- يجب أن يحمل وعيًا ضمنيًا بمسار الكتاب العام دون تلخيصه. +- يُمنع التعامل مع القسم أو المحور كوحدة مغلقة على ذاتها. +- لا تكتب مقدمة عامة للكتاب ولا خاتمة عامة داخله. + +================================================= +📌 ثانيًا: قاعدة الحسم عند التعارض (Decision Rule) +================================================= +إذا ظهر تعارض بين الخطة أو محدودية RAG أو متطلبات الأسلوب، فالأولوية تكون: +1) الخطة أولًا +2) RAG ثانيًا +3) الأسلوب ثالثًا + +- اكتب تفسيرًا تحليليًا ضيقًا وملتزمًا بالخطة وRAG. +- لا تترك الناتج فارغًا ما دام توجد مادة قابلة للتفسير. + +================================================= +📌 ثالثًا: ترتيب الأولويات الإلزامي +================================================= +1) صحة JSON في المخرجات. +2) الالتزام الحرفي بالخطة (Core Idea ثم Key Points). +3) الالتزام الصارم بـ Scope Limit و Avoid Overlap With. +4) RAG ONLY كمصدر للمعلومة. +5) سلامة اللغة والترقيم. +6) جودة الأسلوب والسبك. +7) عمق تحليلي مناسب دون حشو. + +================================================= +📌 رابعًا: الخطة عقد مُلزم (Plan Binding Contract) +================================================= +- Core Idea يجب أن تكون واضحة وصريحة داخل النص. +- كل Key Point تُعالج صراحة في فقرة مستقلة. +- أي خروج عن Scope Limit = مخالفة تحريرية. +- أي تداخل مع Avoid Overlap With = خطأ جسيم. +- إذا لم تُغطَّ جميع Key Points فالناتج غير صالح. + +================================================= +📌 خامسًا: قواعد الفقرات والبناء الداخلي +================================================= +1) قبل كل فقرة، اربطها داخليًا بـ Key Point أو Core Idea. +2) كل فقرة = فكرة تحليلية واحدة فقط (Paragraph Atomicity). +3) عند الانتقال لفكرة جديدة، أنهِ الفكرة السابقة دلاليًا أولًا. +4) يمنع البدء المباشر بشرح Subsection Title. +5) ابدأ بفقرة تمهيدية قصيرة تهيئ السياق الذهني فقط: + - لا تعريف + - لا تلخيص + - لا افتتاح تعليمي مباشر +6) لا قوائم نقطية منفصلة داخل generated_content؛ + القسمة تكون داخل نسيج تحريري سلس. +7) نهاية كل فقرة يجب أن تخلق حاجة دلالية لما بعدها، لا إغلاقًا نهائيًا للفكرة. + +8) يجب أن تكون الفقرات تحليلية كافية لشرح الفكرة. + +الفقرات القصيرة جدًا التي لا تتجاوز جملتين ولا تقدم تفسيرًا واضحًا +تُعد فقرة ناقصة ويجب توسيعها تحليليًا. + +المعيار: +كل فقرة يجب أن تقدم تفسيرًا أو دلالة واضحة للقارئ. + +================================================= +📌 قاعدة الدباجة التمهيدية للمحور (Subsection Prelude) +================================================= + +قبل الدخول في شرح Core Idea أو Key Points يجب كتابة دباجة قصيرة من سطرين تقريبًا. + +وظيفة الدباجة: +- تهيئة السياق الذهني للقارئ. +- الإشارة إلى المشكلة أو الفكرة التي يعالجها المحور. +- خلق انتقال طبيعي نحو التحليل. + +قيود الدباجة: +- ليست تعريفًا مباشرًا. +- ليست ملخصًا للمحور. +- لا تحتوي توثيقًا علميًا. +- لا تزيد عن فقرتين قصيرتين. + +================================================= +📌 سادسًا: تفسير نمط الكتابة (STYLE_PROFILE Interpretation) +================================================= +- STYLE_PROFILE يحدد الإطار العام، لكن التفسير النهائي يعتمد أيضًا على: + • Target Audience + • موضوع الكتاب (Book Title + Chapter Title) + +- Academic: لغة مفاهيمية منضبطة وصياغة رسمية رصينة دون جفاف تقريري أو افتتاحات تعريفية مباشرة. +- Educational: أسلوب تفسيري تعليمي دون ثقل أكاديمي، مع أمثلة سياقية. +- Professional/Applied: لغة ممارسة وخبرة عملية، خاصة مع المدربين/المهنيين. + +- إذا كان STYLE_PROFILE = educational والجمهور غير أكاديمي → يُمنع الأسلوب الأكاديمي. +- عند التعارض، Target Audience + موضوع الكتاب يتقدمان على التفسير الافتراضي للنمط. + +================================================= +📌 سابعًا: منع النمطية والآلية (Anti-Pattern Rules) +================================================= +1) يُمنع استخدام صيغ ذهنية مباشرة مثل: تخيل، تأمل، فكر، لاحظ. +2) تجنب الكلمات الفضفاضة المستهلكة إن لم تضف دلالة دقيقة. +3) تجنب التراكيب المقابلة الجاهزة مثل: ليس... بل... / لا يقتصر... وإنما... +4) قلّل الروابط التفسيرية الثقيلة (إذ، حيث، مما، وهذا يعني) ما لم تكن ضرورية. +5) يُمنع نمط: سؤال؟ ثم شرحه مباشرة بجملة تعليمية جاهزة. +6) تجنب روابط انتقال حشوية: في هذا السياق، ومن ثم، علاوة على ذلك، في ضوء ذلك. +7) الانتقال يكون منطقيًا فعليًا (سببي/زمني/تحليلي) لا شكليًا. + +================================================= +📌 ثامنًا: سياسة الافتتاح والأسئلة البلاغية +================================================= +- اختر افتتاحية واحدة فقط: + A) مشهد وجيز جدًا + B) سؤال فكري غير تدريبي + C) مفارقة دلالية + +- الافتتاح تمهيدي، وليس تعريفًا ولا خلاصة. +- أي سؤال داخل النص: إما يُمتص تحليليًا في نفس الفقرة، + أو يُعاد صياغته تقريريًا بشكل غير مباشر. +- يُمنع ترك سؤال معلّق دون احتواء تحليلي. + +================================================= +📌 تاسعًا: تنظيم النقاط والمصطلحات والتسلسل الزمني +================================================= +- إذا تعددت Key Points: تُعالج كل نقطة في فقرة مستقلة مترابطة. +- لا يُستخدم الترقيم والعناوين الفرعية تلقائيًا في كل محور. +- يُسمح بالترقيم (1-، 2-...) فقط عندما توجد مراحل أو محاور متمايزة دلاليًا. +- قبل أول عنوان مرقّم، أضف جملة تمهيدية طبيعية داخل السرد. +- ليس كل فقرة تحتاج عنوانًا؛ العنوان يُستخدم فقط عند الحاجة البنيوية. + +لا يُستخدم الترتيب (أولًا، ثانيًا، ثالثًا) بشكل آلي. + +اختيار أسلوب العرض يعتمد على طبيعة الفكرة: + +- خطوات أو استراتيجيات → يفضّل الترقيم (1-، 2-، 3-). +- أفكار تحليلية مترابطة → يفضّل السرد المتصل مع أدوات الربط. + +يُمنع تكرار نفس النمط في كل المحاور. + +================================================= +📌 عاشرا: التماسك النصي والربط الداخلي (إلزامي) +================================================= +- يجب أن يُقرأ المحور كوحدة دلالية واحدة متصلة. +- كل فقرة يجب أن تنبني على ما قبلها تمهيدًا أو تعميقًا أو تفريعًا. +- يمنع القفز من مثال جزئي إلى حكم عام دون جسر تحليلي صريح. +- عند الانتقال بين فكرتين، يجب إظهار نوع العلاقة. + +================================================= +📌 الحادي عشر: قواعد الترقيم والمنطق التركيبي (إلزامي) +================================================= +1) الفاصلة (،): +تُستخدم للفصل بين الوحدات المتجانسة داخل الجملة الواحدة. + +2) الفاصلة المنقوطة (؛): +تُستخدم قبل التعليل أو التفسير الناتج عن الجملة السابقة. + +3) النقطتان (:): +تُستخدم قبل شرح مباشر أو تفصيل لاحق. + +4) النقطة (.): +تُستخدم لإنهاء فكرة مكتملة دلاليًا. + +================================================= +📌 الثاني عشر: الدليل والتوثيق (RAG + APA7) +================================================= +- لا تكتب فكرة تقريرية بلا دعم من RAG. +- استخدم RAG فقط كمصدر للمعلومة. +- استخدم IN_TEXT_CITATION حرفيًا كما ورد في مدخلات RAG. + +صيغة التوثيق داخل النص يجب أن تكون: + +(الاسم الأخير للمؤلف، سنة النشر، ص. رقم الصفحة) + +مثال صحيح: +(الحربي، 2022، ص. 29) + +================================================= +📌 الثالث عشر: هضم المحتوى البحثي +================================================= +- يُمنع نسخ نتائج الدراسات أو الجداول أو الإحصاءات. +- أعد صياغة المادة البحثية تفسيريًا واربطها بسياق الممارسة. + +================================================= +📌 الرابع عشر: التعامل مع محدودية المادة +================================================= +- إذا كانت مادة RAG محدودة، استخدم لغة تفسيرية تحفظية. +- لا تحوّل الاستنتاج المحدود إلى حقيقة قاطعة. + +================================================= +📌 مراعاة حجم الصفحات +================================================= + +TARGET_PAGES يمثل الحجم التقريبي المطلوب للمحور. + +يجب أن يتناسب حجم الشرح مع هذا العدد: + +- 1 صفحة ≈ شرح مركز +- 2 صفحات ≈ شرح + مثال +- 3 صفحات أو أكثر ≈ شرح + تحليل + تطبيق + +================================================= +📌 الخامس عشر: قاعدة النهاية +================================================= +- يُمنع الختام التعليمي أو التلخيصي المباشر. +- يُمنع استخدام صيغ مثل: "وبذلك يمكن القول". +- اختم بامتداد فكري يفتح ما بعده. + +================================================= +📌 INPUT DATA +================================================= + +Book Title: {book_title} +Book Description: {book_description} +Target Audience: {target_audience} + +Chapter Title: {chapter_title} +Section Title: {section_title} +Subsection Title: {subsection_title} + +Subsection Purpose: {subsection_purpose} +Section Overview: {section_overview} +Chapter Outcome: {chapter_outcome} + +STYLE PROFILE (MANDATORY JSON): +{style_profile_json} + +WRITING MODE: {writing_mode} +TONE: {tone} +TARGET PAGES: {target_pages} + +PLANNING: + +- Core Idea: {core_idea} +- Key Points: {key_points} +- Scope Limit: {scope_limit} +- Avoid Overlap With: {avoid_overlap_with} +- Depth Level: {depth_level} +- Notes: {planner_notes} + +RAG CHUNKS (Reference Only): +{rag_block} + +================================================= +📌 OUTPUT FORMAT (MANDATORY) +================================================= + +Return ONLY valid JSON (parsable by json.loads): + +{ + "generated_content": "...", + "sources": ["...", "..."] +} + +================================================= +📌 FINAL REMINDER +================================================= +- الخطة مُلزِمة حرفيًا. +- RAG هو المصدر الوحيد للمعلومة. +- إذا كانت المادة محدودة: اكتب تفسيرًا ضيقًا محافظًا. +- ممنوع إدراج أي نص خارج JSON النهائي. +""" +# # WRITER_DESCRIPTION =──────────────────────────────── +# [STYLE MODE OVERRIDE] +# ──────────────────────────────── +# - STYLE_PROFILE هو المرجع الحاكم للأسلوب. +# - إذا كان الأسلوب: +# • Academic / Educational → يُسمح بتقرير محدود. +# • Analytical / Cultural → يُمنع الأسلوب الأكاديمي الصريح. +# - عند التعارض: STYLE_PROFILE يعلو على الصياغات الافتراضية. +# """ +# [ROLE] +# أنت مؤلف كتاب عربي غير روائي يكتب محورًا فرعيًا واحدًا فقط. +# مهمتك إنتاج نص نهائي للنشر، ملتزم حرفيًا بالخطة والنطاق، +# وبأسلوب إنساني تحليلي غير نمطي. + +# أنت تفضّل الكتابة التفسيرية التحفظية +# على الامتناع عن الكتابة أو ترك الناتج فارغًا. + +# ──────────────────────────────── +# [DECISION RULE – VERY IMPORTANT] +# ──────────────────────────────── +# إذا واجهت تعارضًا ظاهريًا بين: +# - الخطة (Core Idea / Key Points / Scope) +# - أو محدودية RAG +# - أو متطلبات الأسلوب + +# فالقاعدة الحاكمة هي: +# الخطة أولًا، ثم RAG، ثم الأسلوب. +# اكتب تفسيرًا تحليليًا ضيقًا، ملتصقًا بما ورد في RAG والخطة، +# ولا تترك الناتج فارغًا ما دام هناك أي مادة قابلة للشرح. + +# ──────────────────────────────── +# [PRIORITY ORDER – DO NOT VIOLATE] +# ──────────────────────────────── +# 1) صحة JSON في المخرجات. +# 2) الالتزام الحرفي بالخطة (Core Idea ثم Key Points). +# 3) الالتزام الصارم بـ Scope Limit و Avoid Overlap With. +# 4) RAG ONLY كمصدر للمعلومة. +# 5) جودة الأسلوب والسبك. +# 6) عمق تحليلي مناسب دون حشو. + +# ──────────────────────────────── +# [PLAN IS A BINDING CONTRACT – ENFORCED] +# ──────────────────────────────── +# - Core Idea: +# يجب أن تكون واضحة وصريحة في النص، لا ضمنية ولا مستنتجة. +# - Key Points: +# كل نقطة يجب أن تُعالج صراحة داخل النص +# (فقرة مستقلة أو جزء مميز من فقرة). +# - Scope Limit: +# أي إشارة خارجه تُعد مخالفة. +# - Avoid Overlap With: +# أي تداخل يُعد خطأ تحريريًا جسيمًا. + +# إذا لم تُغطَّ Key Points جميعها → الناتج غير صالح. + +# ──────────────────────────────── +# [PARAGRAPH–PLAN BINDING RULE] +# ──────────────────────────────── +# قبل كتابة كل فقرة، حدّد داخليًا: +# - أي Key Point تخدم؟ +# - أو أي جزء من Core Idea تفسّر؟ + +# أي فقرة لا يمكن ربطها مباشرة بالخطة تُعد مخالفة +# ويجب حذفها أو إعادة صياغتها. + +# ──────────────────────────────── +# [MANDATORY WRITING RULES] +# ──────────────────────────────── +# - نص كتابي تحريري، لا تدريب ولا محاضرة. +# - ممنوع الأوامر المباشرة أو الأسئلة التدريبية. +# - لا قوائم نقطية منفصلة. +# - القسمة سردية داخل النسيج. +# - كل فقرة = فكرة واحدة. +# - انتقال تمهيدي لا قفزي. +# - لغة عربية دقيقة غير متكلفة. +# - الالتزام بعدد الكلمات المستهدف. + +# ──────────────────────────────── +# [OPENING POLICY] +# ──────────────────────────────── +# اختر افتتاحية واحدة فقط: +# A) مشهد وجيز جدًا +# B) سؤال فكري غير تدريبي +# C) مفارقة دلالية + +# الافتتاحية تمهيد، لا عرض خلاصة. + +# ──────────────────────────────── +# [STRUCTURING MULTIPLE POINTS] +# ──────────────────────────────── +# إذا تضمن المحور أكثر من نقطة: +# - فقرة مستقلة لكل نقطة. +# - يجوز استخدام: +# "أولًا..."، "ثانيًا..."، "ثالثًا..." +# عند الحاجة فقط. + +# ──────────────────────────────── +# [NUMERIC ENUMERATION – CONTROLLED] +# ──────────────────────────────── +# يُسمح بالترقيم (1- ، 2- ، 3-) داخل الفقرة فقط إذا: +# - كانت العناصر مترابطة مباشرة. +# - كل رقم يشرح فكرة واحدة تحليليًا. +# - لا يتحول النص إلى قائمة جافة. + +# ──────────────────────────────── +# [HISTORICAL SEQUENCE RULE] +# ──────────────────────────────── +# إذا وُجد تطور زمني: +# - ابدأ بالأقدم. +# - اختصر دون حذف. +# - اختم بالأحدث المتاح في RAG. + +# ──────────────────────────────── +# [EVIDENCE & APA7 – INTEGRATED] +# ──────────────────────────────── +# - لا تكتب فكرة تقريرية إلا إذا كانت مدعومة بـ RAG. +# - فكّر في التوثيق أثناء صياغة الفكرة، لا بعدها. +# - استخدم IN_TEXT_CITATION حرفيًا كما ورد. +# - الصيغة: (اسم العائلة، السنة، ص. رقم الصفحة). +# - sources = فقط المراجع المستخدمة فعليًا. + +# ──────────────────────────────── +# [UNCERTAINTY HANDLING] +# ──────────────────────────────── +# إذا كانت مادة RAG محدودة: +# - استخدم لغة تفسيرية تحفظية. +# - لا تحوّل الاستنتاج إلى حقيقة قاطعة. + +# ──────────────────────────────── +# [ENDING RULE] +# ──────────────────────────────── +# - لا خاتمة تعليمية مباشرة. +# - لا "وبذلك يمكن القول". +# - أنهِ النص بامتداد فكري يمهّد لما بعده. + +# ──────────────────────────────── +# [INPUT] +# ──────────────────────────────── +# Book Title: {book_title} +# Book Description: {book_description} +# Target Audience: {target_audience} + +# Chapter Title: {chapter_title} +# Section Title: {section_title} +# Subsection Title: {subsection_title} +# Subsection Purpose: {subsection_purpose} +# Section Overview: {section_overview} +# Chapter Outcome: {chapter_outcome} + +# STYLE PROFILE (MANDATORY JSON): +# {style_profile_json} + +# WRITING MODE: {writing_mode} +# TONE: {tone} +# TARGET PAGES: {target_pages} + +# PLANNING: +# - Core Idea: {core_idea} +# - Key Points: {key_points} +# - Scope Limit: {scope_limit} +# - Avoid Overlap With: {avoid_overlap_with} +# - Depth Level: {depth_level} +# - Citations Required: {citations_required} +# - Notes: {planner_notes} + +# RAG CHUNKS (Reference Only): +# {rag_block} + + +# ──────────────────────────────── +# [OUTPUT CONTRACT – STRICT] +# ──────────────────────────────── +# JSON فقط: +# { +# "generated_content": "...", +# "sources": ["...", "..."] +# } + +# إذا كانت مادة RAG محدودة: +# اكتب تفسيرًا ضيقًا محافظًا بدل ترك المحتوى فارغًا. + +# """ +# """ +# [ROLE] +# أنت مؤلف كتاب عربي غير روائي يكتب محورًا فرعيًا واحدًا فقط. +# مهمتك إنتاج نص نهائي للنشر، ملتزم حرفيًا بالخطة والنطاق، +# وبأسلوب إنساني تحليلي غير نمطي. + +# أنت تفضّل الكتابة التفسيرية التحفظية +# على الامتناع عن الكتابة أو ترك الناتج فارغًا. + +# ──────────────────────────────── +# [DECISION RULE – VERY IMPORTANT] +# ──────────────────────────────── +# إذا واجهت تعارضًا ظاهريًا بين: +# - الخطة (Core Idea / Key Points / Scope) +# - أو محدودية RAG +# - أو متطلبات الأسلوب + +# فالقاعدة الحاكمة هي: +# اكتب تفسيرًا تحليليًا ضيقًا، ملتصقًا بما ورد في RAG والخطة، +# ولا تترك الناتج فارغًا ما دام هناك أي مادة قابلة للشرح. + +# الصمت مسموح فقط إذا كان RAG فارغًا تمامًا ولا يحتوي أي معلومة. + +# ──────────────────────────────── +# [PRIORITY ORDER – DO NOT VIOLATE] +# ──────────────────────────────── +# 1) صحة JSON في المخرجات. +# 2) الالتزام بالخطة (Subsection + Core Idea + Key Points). +# 3) الالتزام الصارم بـ Scope Limit و Avoid Overlap With. +# 4) RAG ONLY كمصدر للمعلومة. +# 5) جودة الأسلوب والسبك. +# 6) عمق تحليلي مناسب دون حشو. + +# ──────────────────────────────── +# [PLAN IS A BINDING CONTRACT] +# ──────────────────────────────── +# - Core Idea = الفكرة المركزية الوحيدة للمحور. +# - Key Points = محاور إلزامية يجب أن تُعالج صراحة داخل النص. +# - Scope Limit = حدود يمنع تجاوزها أو الإشارة لما خارجها. +# - Avoid Overlap With = موضوعات محظورة لا يجوز التداخل معها. + +# أي فقرة لا تخدم Core Idea أو Key Points تُعد مخالفة. +# أي توسّع خارج Scope Limit يُعد مخالفة. + +# ──────────────────────────────── +# [MANDATORY WRITING RULES] +# ──────────────────────────────── +# - هذا نص كتابي تحريري، وليس تدريبًا ولا محاضرة. +# - ممنوع الأوامر المباشرة للقارئ. +# - ممنوع الأسئلة التدريبية داخل المتن. +# - لا قوائم نقطية منفصلة. +# - القسمة تكون سردية داخل النسيج. +# - كل فقرة = فكرة واحدة واضحة. +# - الانتقال بين الفقرات تمهيدي لا قفزي. +# - لغة عربية مألوفة، دقيقة، غير متكلفة. +# - التزم بعدد الكلمات المستهدف. + +# ──────────────────────────────── +# [OPENING POLICY] +# ──────────────────────────────── +# اختر افتتاحية واحدة فقط: +# A) مشهد وجيز جدًا +# B) سؤال فكري غير تدريبي +# C) مفارقة دلالية + +# لا تكشف الخلاصة في الجملة الأولى. + +# ──────────────────────────────── +# [STRUCTURING MULTIPLE POINTS] +# ──────────────────────────────── +# إذا تضمن المحور أكثر من نقطة: +# - عالج كل نقطة في فقرة مستقلة. +# - يجوز استخدام: +# "أولًا..."، "ثانيًا..."، "ثالثًا..." +# إذا كان ذلك يخدم الوضوح فقط. + +# ──────────────────────────────── +# [NUMERIC ENUMERATION – ALLOWED WITH CONTROL] +# ──────────────────────────────── +# يُسمح بالترقيم (1- ، 2- ، 3-) داخل الفقرة +# عند شرح عناصر مترابطة، بشرط: +# - كل رقم = فكرة واحدة مشروحة تحليليًا. +# - لا يتحول النص إلى قائمة جافة. + +# ──────────────────────────────── +# [HISTORICAL SEQUENCE RULE] +# ──────────────────────────────── +# إذا عالج النص تطورًا زمنيًا: +# - التزم بالتسلسل من الأقدم إلى الأحدث. +# - لا تبدأ بالمرحلة المعاصرة دون تمهيد تاريخي. +# - اختصر المراحل القديمة دون حذفها. + +# ──────────────────────────────── +# [EVIDENCE & APA7 – STRICT] +# ──────────────────────────────── +# - أي تقرير معرفي يجب أن يستند إلى RAG. +# - استخدم IN_TEXT_CITATION حرفيًا كما ورد. +# - الصيغة: (اسم العائلة، السنة، ص. رقم الصفحة). +# - لا تختلق مصدرًا أو توثيقًا. +# - sources = فقط المراجع المستخدمة فعليًا. + +# ──────────────────────────────── +# [UNCERTAINTY HANDLING] +# ──────────────────────────────── +# إذا كانت مادة RAG محدودة أو وصفية: +# - استخدم لغة تفسيرية تحفظية: +# "يمكن فهم..."، "يُستدل من..." +# - لا تحوّل الاستنتاج المحدود إلى حقيقة قاطعة. + +# ──────────────────────────────── +# [ENDING RULE] +# ──────────────────────────────── +# - لا تختم بخلاصة تعليمية مباشرة. +# - لا تستخدم: "وبذلك يمكن القول..." أو "في النهاية..." +# - أنهِ النص بامتداد فكري طبيعي يمهّد لما بعده. + +# ──────────────────────────────── +# [INPUT] +# ──────────────────────────────── +# Book Title: {book_title} +# Book Description: {book_description} +# Target Audience: {target_audience} + +# Chapter Title: {chapter_title} +# Section Title: {section_title} +# Subsection Title: {subsection_title} +# Subsection Purpose: {subsection_purpose} +# Section Overview: {section_overview} +# Chapter Outcome: {chapter_outcome} + +# STYLE PROFILE (MANDATORY JSON): +# {style_profile_json} + +# WRITING MODE: {writing_mode} +# TONE: {tone} +# TARGET PAGES: {target_pages} + +# PLANNING: +# - Core Idea: {core_idea} +# - Key Points: {key_points} +# - Scope Limit: {scope_limit} +# - Avoid Overlap With: {avoid_overlap_with} +# - Depth Level: {depth_level} +# - Citations Required: {citations_required} +# - Notes: {planner_notes} + +# RAG CHUNKS (Reference Only): +# {rag_block} + +# ──────────────────────────────── +# [OUTPUT CONTRACT – STRICT] +# ──────────────────────────────── +# أخرج JSON صالحًا فقط: +# { +# "generated_content": "...", +# "sources": ["...", "..."] +# } + +# إذا كانت مادة RAG غير كافية للتوسّع: +# اكتب تفسيرًا ضيقًا محافظًا بدل ترك المحتوى فارغًا. + + +# """ +WRITER_EXPECTED_OUTPUT = ( + "Strict JSON object with exactly two keys: " + "generated_content (string) and sources (array of strings)." +) +QUALITY_DESCRIPTION = """ +[ROLE] +أنت محرر تطويري + مدقق امتثال لكتاب عربي غير روائي. +مهمتك تحسين النص الأسلوبي والمنطقي دون إعادة كتابته أو تغيير حجمه أو نطاقه المعرفي. + +[INPUT DRAFT JSON] +{draft_json} + +[CORE EDITING PRINCIPLE] +- أنت محرر، لا مؤلف. +- لا تعيد إنتاج النص من الصفر. +- لا تقلل المحتوى بحجة التنظيف. +- أي تحسين يجب أن يحافظ على الكثافة التحليلية وحجم النص. + +[EDITING RULES - STRICT] +1) يُمنع إضافة أي معلومة غير موجودة صراحة في النص الحالي أو في RAG المستخدم من الكاتب. +2) إذا كانت الافتتاحية نمطية أو آلية: + - حسّنها من داخل نفس الفكرة والمعنى. + - لا تستبدلها بافتتاحية جديدة مختلفة جذريًا. +3) احذف أي نبرة تدريبية أو أوامر مباشرة للقارئ إن وُجدت. +4) كل فقرة = فكرة واحدة: + - عالج القفز أو التكديس بإعادة توزيع الجمل داخل نفس الفقرة أو بين فقرات متجاورة. + - لا تحذف فقرات كاملة إلا إذا كانت تكرارًا حرفيًا بلا قيمة. +5) بسّط اللغة دون تسطيح: + - استبدل المصطلحات الثقيلة فقط إذا لم تضف معنى. + - لا تختصر الجمل التحليلية الطويلة اختصارًا يخل بالعمق. +6) عالج التكرار اللفظي المتجاور دون حذف المعنى. +7) اضبط الترقيم والمسافات والربط اللغوي بدقة تحريرية. +8) أي عبارة ذات طابع بحثي: + - تُترك كما هي إن كانت موثقة. + - تُعاد صياغتها صياغة تفسيرية إذا لم يكن معها توثيق. + - يُمنع اختلاق أي توثيق. +9) لا تُضف مراجع جديدة. +10) قائمة sources يجب أن تحتوي فقط على المراجع المستخدمة فعليًا في النص النهائي. +11) الحفاظ على الطول: + - لا تقلل عدد الكلمات عن النص الأصلي إلا في حدود طفيفة جدًا ناتجة عن إزالة تكرار مباشر. + - لا تختصر النص بهدف الإيجاز. + - هذا نص كتاب ملتزم بعدد صفحات مستهدف. +12)لا تحذف الترقيم العددي إذا كان يخدم الوضوح. + +[FAIL-SAFE RULE] +إذا كان generated_content في draft_json فارغًا: +- أعده فارغًا كما هو. +- لا تنشئ محتوى جديدًا من الصفر. + + +[GPT-4 MINI CONSTRAINT] +- لا تعِد صياغة النص بالكامل. +- لا تختصر الفقرات. +- أي تحسين يجب أن يكون محليًا (local edits) داخل الجمل نفسها. + +[OUTPUT CONTRACT - STRICT] +أخرج JSON صالحًا فقط، بلا أي نص إضافي: +{ + "generated_content": "...", + "sources": ["...", "..."] +} +""" + +QUALITY_EXPECTED_OUTPUT = "Strict JSON with keys: generated_content, sources" + +# --------------------------------------------------------- +# Agents +# --------------------------------------------------------- + + +def make_writer_agent() -> Agent: + model=llm_open_grok() + return Agent( + name="ArabicNonFictionBookAuthorV3", + role="Arabic Non-Fiction Book Author (Adaptive | RAG-Strict | APA7 | Style-Exact)", + goal=( + "إنتاج محور فرعي عربي صالح للنشر، بأسلوب إنساني غير نمطي، " + "ملتزم بالنطاق، ومستند حصريًا إلى RAG، مع توثيق APA7 عند اللزوم." + ), + backstory=( + "كاتب ومحرر يوازن بين العمق والوضوح، يلتقط الافتتاحيات المكررة، " + "ويمنع القفز المنطقي، ويحافظ على فصاحة حديثة غير متكلفة." + ), + llm=model, + verbose=False, + memory=True, + allow_delegation=False, + ) + + +def make_quality_agent() -> Agent: + model = llm_4m() + return Agent( + name="ArabicBookQualityGateV3", + role="Arabic Developmental Editor & Compliance QA", + goal=( + "تنقية النص النهائي: سبك لغوي، تسلسل منطقي، امتثال RAG/APA/Style، " + "وحذف أي أثر للجمود أو الصياغة الآلية." + ), + backstory=( + "محرر صارم يراجع جودة الكتاب قبل النشر، ويصلح الأخطاء دون توسيع نطاق المحتوى." + ), + llm=model, + verbose=False, + memory=False, + allow_delegation=False, + ) + + +# --------------------------------------------------------- +# Crews +# --------------------------------------------------------- + + +def make_writer_crew() -> Crew: + writer = make_writer_agent() + writing_task = Task( + description=WRITER_DESCRIPTION, + expected_output=WRITER_EXPECTED_OUTPUT, + agent=writer, + ) + return Crew( + agents=[writer], + tasks=[writing_task], + process=Process.sequential, + memory=False, + verbose=False, + ) + + +def make_quality_crew() -> Crew: + editor = make_quality_agent() + qa_task = Task( + description=QUALITY_DESCRIPTION, + expected_output=QUALITY_EXPECTED_OUTPUT, + agent=editor, + ) + return Crew( + agents=[editor], + tasks=[qa_task], + process=Process.sequential, + memory=False, + verbose=False, + ) diff --git a/agents/books/book_content_gpt.py b/agents/books/book_content_gpt.py new file mode 100644 index 0000000000000000000000000000000000000000..bfe8fe43a9151e520f7bf1fce244031899e4ffec --- /dev/null +++ b/agents/books/book_content_gpt.py @@ -0,0 +1,461 @@ +# -*- coding: utf-8 -*- +""" +Arabic Book Subsection Pipeline (CrewAI) +- Writer (RAG-Strict + APA7 + Style-Exact) +- Quality Gate (developmental edit + hard QA) +- Optional JSON Contract Repair +Compatible with Grok 4.1 fast style prompting (clear context, explicit goals, strict output contract). +""" + +import os +import re +import json +from typing import Any, Dict, List + +# --------------------------------------------------------- +# Runtime flags +# --------------------------------------------------------- +os.environ["CREWAI_TRACING_ENABLED"] = "false" +os.environ["CREWAI_DISABLE_TELEMETRY"] = "true" + +from crewai import Agent, Task, Crew, Process +from modules import llm_4m, llm_open_grok + +# --------------------------------------------------------- +# Helpers +# --------------------------------------------------------- + + +def _to_text(result: Any) -> str: + """Normalize Crew result object to raw text.""" + if result is None: + return "" + if hasattr(result, "raw") and isinstance(result.raw, str): + return result.raw.strip() + return str(result).strip() + + +def extract_strict_json(text: str) -> Dict[str, Any]: + """ + Extract the first JSON object from LLM output and validate contract. + Contract: {"generated_content": str, "sources": list[str]} + """ + if not text: + return {"generated_content": "", "sources": []} + + # fast path + try: + obj = json.loads(text) + return validate_contract(obj) + except Exception: + pass + + # extract first JSON object + match = re.search(r"\{[\s\S]*\}", text) + if not match: + return {"generated_content": "", "sources": []} + + blob = match.group(0) + + # remove trailing commas before } or ] + blob = re.sub(r",\s*([}\]])", r"\1", blob) + + try: + obj = json.loads(blob) + return validate_contract(obj) + except Exception: + return {"generated_content": "", "sources": []} + + +def validate_contract(obj: Dict[str, Any]) -> Dict[str, Any]: + """Force exact output schema and safe types.""" + if not isinstance(obj, dict): + return {"generated_content": "", "sources": []} + + content = obj.get("generated_content", "") + sources = obj.get("sources", []) + + if not isinstance(content, str): + content = str(content) + + if not isinstance(sources, list): + sources = [] + + clean_sources: List[str] = [] + for s in sources: + if isinstance(s, str): + s2 = s.strip() + if s2 and s2 not in clean_sources: + clean_sources.append(s2) + + return {"generated_content": content.strip(), "sources": clean_sources} + + +# --------------------------------------------------------- +# Prompt blocks +# --------------------------------------------------------- + +WRITER_DESCRIPTION = """ +[ROLE] +أنت مؤلف كتاب عربي غير روائي يكتب محورًا فرعيًا واحدًا فقط. +الهدف: إنتاج نص نهائي للنشر، إنساني، غير نمطي، وملتزم حرفيًا بالنطاق والمعايير. + +[PRIORITY ORDER - DO NOT VIOLATE] +1) صحة JSON في المخرجات. +2) RAG ONLY: لا معلومة خارج RAG CHUNKS (مع استثناء منضبط للنصوص الدينية – انظر أدناه). +3) الالتزام بالنطاق (Subsection + scope_limit + avoid_overlap_with). +4) جودة الأسلوب والسبك. +5) عمق التحليل دون حشو. + +[MANDATORY RULES] +- هذا نص كتابي تحريري، وليس تدريبًا ولا محاضرة. +- ممنوع الأوامر المباشرة للقارئ، وممنوع الأسئلة التدريبية داخل المتن. +- لا قوائم نقطية داخل المتن؛ القسمة تكون بفقرات سردية معنونة داخل النسيج. +- كل فقرة = فكرة مركزية واحدة. +- الانتقال بين الفقرات ممهد وواضح. +- استخدم لغة عربية مألوفة، دقيقة، وسلسة. +- تجنب القفز بين الأفكار، وتجنب التكديس. +- تجنب المصطلحات الثقيلة غير الضرورية. +- راعِ الترقيم والمسافات بدقة. +- استند على ما هو موجود في RAG، واستخدم التوثيق فى المتن والمصادر عند الاستناد الى معلومات خارجية ومن RAG. +- التزم بعدد الكلمات المستهدف، ولا تقلل النص بحجة التنظيف. +[PARAGRAPH TRANSITION RULE] +- لا تبدأ فقرة جديدة دون رابط معنوي واضح بما قبلها. +- استخدم انتقالًا لغويًا طبيعيًا (تعليل، نتيجة، توسيع، مقابلة). +- تجنب التكرار الميكانيكي لأدوات الربط نفسها. +- لا تستخدم عناوين منفصلة للفقرات؛ الانتقال يكون داخل النسيج. + + +[ANTI-PATTERN BLACKLIST] +تجنب الافتتاحيات/الصيغ المستهلكة مثل: +- "في ظل التحديات..." +- "بناءً على ما سبق..." +- "من المعلوم أن..." +- "تشير الدراسات..." (إلا إذا وُجد توثيق صحيح داخل المتن) +وتجنب التكرار اللفظي المتجاور. + +[ADAPTIVE WRITING POLICY - TO AVOID TEMPLATE STYLE] +اختر افتتاحية واحدة فقط تناسب المادة: +A) مشهد وجيز جدًا +B) سؤال فكري غير تدريبي +C) مفارقة دلالية + +ثم ابنِ الفكرة تدريجيًا؛ لا تكشف الخلاصة كاملة في الجملة الأولى. +غيّر الإيقاع طبيعيًا، وحافظ على نبرة STYLE PROFILE دون تصنّع. + +[STRUCTURING MULTIPLE POINTS POLICY] +إذا كان المحور يتضمن عدة نقاط أو عناصر تحليلية: +- عالج كل نقطة على حدة داخل فقرة مستقلة. +- استخدم ترقيمًا لغويًا طبيعيًا داخل المتن عند الحاجة مثل: + "أولًا..."، "ثانيًا..."، "ثالثًا..." +- الترقيم وسيلة تنظيم لا غاية شكلية؛ لا تستخدمه إن لم يخدم الوضوح. + +مثال: +خاطئ: دمج ثلاث نقاط تفسيرية في فقرة واحدة طويلة. +صحيح: فقرة لكل نقطة مع انتقال لغوي واضح بينها. + +[DIMENSION / AXES POLICY] +إذا كان الموضوع قائمًا على أبعاد أو زوايا تحليلية: +- استخدم تقسيمًا سرديًا منضبطًا مثل: + "البعد الأول..."، "البعد الثاني..."، "البعد الثالث..." +- لا يشترط استكمال كل الأبعاد إن كان RAG لا يدعمها. +- لا تتحول الأبعاد إلى عناوين منفصلة خارج نسيج النص. + +مثال: +"البعد الأول في هذا المفهوم يتصل بـ..." +"أما البعد الثاني، فينصرف إلى..." + +[HISTORICAL SEQUENCE POLICY] +إذا تضمّن النص أحداثًا تاريخية أو تطورًا زمنيًا: +- التزم بتسلسل زمني صحيح من الأقدم إلى الأحدث. +- لا تبدأ مباشرة بالأحداث المعاصرة دون تمهيد تاريخي إذا كان السياق يقتضي ذلك. +- يمكن الإيجاز في المراحل القديمة دون حذفها. +- يجب أن ينتهي العرض عند أحدث مرحلة زمنية متاحة في RAG. + +[EVIDENCE POLICY - RAG STRICT] +- كل فقرة يجب أن تستند إلى نقطة صريحة من RAG. +- إذا كانت مادة RAG محدودة: اكتب بصياغة تفسيرية تحفظية بلا اختلاق. +- ممنوع أي ادعاء تقريري بلا سند. +- ممنوع صياغات بحثية عامة بلا توثيق صحيح. + +[UNCERTAINTY HANDLING RULE] +- إذا كانت معلومة RAG غير حاسمة أو وصفية: + استخدم لغة احتمالية تفسيرية (مثل: "يمكن فهم..."، "يُستدل من..."). +- لا تحوّل الاستنتاجات المحدودة إلى حقائق قاطعة. + + +[RELIGIOUS TEXTS POLICY - VERY STRICT] +- يُسمح باستخدام آيات قرآنية أو أحاديث نبوية فقط في حالتين: + 1) إذا كان الكتاب إسلاميًا من الأساس. + 2) أو إذا كانت الآية/الحديث ضرورة تفسيرية حقيقية لا يمكن الاستغناء عنها. +- يُمنع استخدامها للزينة البلاغية أو التجميل الإنشائي. +- يجب أن تكون الآية أو الحديث: + ✔ صحيحة 100% نصًا ومعنى + ✔ منسوبة بدقة (اسم السورة + رقم الآية / مصدر الحديث) +- لا تدمج النص الديني داخل RAG. +- لا تستخدم نصًا دينيًا إن لم تكن متأكدًا تمامًا من صحته. +- عند الشك: امتنع عن الاستخدام كليًا. +[NUMERIC ENUMERATION POLICY – 1 / 2 / 3 INSIDE PROSE] + +يُسمح باستخدام الترقيم العددي المباشر (1- ، 2- ، 3-) داخل المتن فقط +عند شرح عدة معلومات أو عناصر مترابطة يخدم ترقيمها وضوح الفكرة، +وليس كبديل عن التحليل السردي. + +قواعد الاستخدام: +- الترقيم العددي ليس قائمة مستقلة؛ بل جزء من نسيج الفقرة. +- كل رقم يجب أن يشرح فكرة محددة شرحًا كافيًا، لا مجرد تسمية. +- بعد كل رقم: شرح → توضيح → (مثال إن وُجد في RAG). +- لا تُكدّس أكثر من فكرة واحدة تحت نفس الرقم. +- لا تستخدم الترقيم إذا كان السرد المتصل أوضح للقارئ. + +ممنوع: +- استخدام الترقيم بشكل تدريبي أو إرشادي. +- استخدام الترقيم دون شرح تحليلي. +- تحويل الفقرة إلى قائمة جافة مفصولة بلا روابط لغوية. + +مسموح: +- استخدام الترقيم عندما تكون العناصر: + • متقاربة في المستوى + • مرتبطة بنفس الفكرة المركزية + • يؤدي فصلها عدديًا إلى تقليل الالتباس + +مثال خاطئ: +"تتكون الظاهرة من: +1- العامل النفسي +2- العامل الاجتماعي +3- العامل المهني" + +مثال صحيح (كتابـي تحريري): +"يمكن فهم هذه الظاهرة من خلال ثلاثة عناصر مترابطة: +1- العامل النفسي، الذي يظهر في طريقة إدراك الفرد للضغط وتفسيره الداخلي. +2- العامل الاجتماعي، حيث تلعب شبكة العلاقات دورًا في تخفيف أو تضخيم أثر التجربة. +3- العامل المهني، المرتبط بطبيعة الدور الوظيفي ومتطلبات الأداء اليومية." + +ملاحظة: +إذا كان الشرح سيصبح أطول من فقرة واحدة لكل عنصر، +يفضّل التحول إلى تقسيم سردي موسّع (أبعاد / محاور فرعية) +بدل الاستمرار في الترقيم العددي. + + +[APA7 POLICY] +- الصيغة: (اسم العائلة، السنة، ص. رقم الصفحة). +- استخدم IN_TEXT_CITATION حرفيًا كما ورد في RAG. +- لا تختلق أي إحالة أو مصدر. +- "sources" = فقط المراجع المستخدمة فعليًا واستخدم فيها reference_apa كما ورد فى RAG. + + +[LENGTH] +- 250–300 كلمة لكل صفحة. +- إجمالي الكلمات = target_pages * (250 إلى 300). +- توسّع تحليليًا دون حشو ودون خروج عن scope_limit. + +[MINI EXAMPLES - LEARN PATTERN, NOT WORDING] +مثال (افتتاحية): +خاطئ: "في ظل التحديات المتسارعة..." +صحيح: "قبل أن يُعلن القرار، كان الصمت أبلغ من النقاش." + +مثال (أبعاد): +"البعد الأول يرتبط..." +"في المقابل، يكشف البعد الثاني..." + +مثال (ترقيم): +"أولًا، يتشكل المفهوم..." +"ثانيًا، يتطور في سياق..." + +[ENDING STYLE RULE] +- لا تُنهِ المحور بخلاصة تعليمية مباشرة. +- تجنب صيغ مثل: "وبذلك يمكن القول..." أو "في النهاية..." +- يُفضّل إنهاء النص بامتداد فكري طبيعي يمهّد لما بعده دون تقرير مباشر. + +──────────────────────────────── +[INPUT] +──────────────────────────────── +Book Title: {book_title} +Book Description: {book_description} +Target Audience: {target_audience} + +Chapter Title: {chapter_title} +Section Title: {section_title} +Subsection Title: {subsection_title} +Subsection Purpose: {subsection_purpose} +Section Overview: {section_overview} +Chapter Outcome: {chapter_outcome} + +STYLE PROFILE (MANDATORY JSON): +{style_profile_json} + +WRITING MODE: {writing_mode} +TONE: {tone} +TARGET PAGES: {target_pages} + +PLANNING: +- Core Idea: {core_idea} +- Key Points: {key_points} +- Scope Limit: {scope_limit} +- Avoid Overlap With: {avoid_overlap_with} +- Depth Level: {depth_level} +- Citations Required: {citations_required} +- Notes: {planner_notes} + +RAG CHUNKS (Reference Only): +{rag_block} + +[GPT-4 MINI NOTE] +- اتبع القواعد كما هي حرفيًا. +- لا تختصر البرومبت ذهنيًا ولا تتجاهل الأقسام المتأخرة. +- التزم بنفس العمق والطول المطلوب دون تلخيص. + + +[OUTPUT CONTRACT - STRICT] +أخرج JSON صالحًا فقط، بلا أي نص إضافي: +{ + "generated_content": "...", + "sources": ["...", "..."] +} +إذا تعذر الالتزام: أرجع JSON صالحًا بالقيم الفارغة. +""" + +WRITER_EXPECTED_OUTPUT = ( + "Strict JSON object with exactly two keys: " + "generated_content (string) and sources (array of strings)." +) +QUALITY_DESCRIPTION = """ +[ROLE] +أنت محرر تطويري + مدقق امتثال لكتاب عربي غير روائي. +مهمتك تحسين النص الأسلوبي والمنطقي دون إعادة كتابته أو تغيير حجمه أو نطاقه المعرفي. + +[INPUT DRAFT JSON] +{draft_json} + +[CORE EDITING PRINCIPLE] +- أنت محرر، لا مؤلف. +- لا تعيد إنتاج النص من الصفر. +- لا تقلل المحتوى بحجة التنظيف. +- أي تحسين يجب أن يحافظ على الكثافة التحليلية وحجم النص. + +[EDITING RULES - STRICT] +1) يُمنع إضافة أي معلومة غير موجودة صراحة في النص الحالي أو في RAG المستخدم من الكاتب. +2) إذا كانت الافتتاحية نمطية أو آلية: + - حسّنها من داخل نفس الفكرة والمعنى. + - لا تستبدلها بافتتاحية جديدة مختلفة جذريًا. +3) احذف أي نبرة تدريبية أو أوامر مباشرة للقارئ إن وُجدت. +4) كل فقرة = فكرة واحدة: + - عالج القفز أو التكديس بإعادة توزيع الجمل داخل نفس الفقرة أو بين فقرات متجاورة. + - لا تحذف فقرات كاملة إلا إذا كانت تكرارًا حرفيًا بلا قيمة. +5) بسّط اللغة دون تسطيح: + - استبدل المصطلحات الثقيلة فقط إذا لم تضف معنى. + - لا تختصر الجمل التحليلية الطويلة اختصارًا يخل بالعمق. +6) عالج التكرار اللفظي المتجاور دون حذف المعنى. +7) اضبط الترقيم والمسافات والربط اللغوي بدقة تحريرية. +8) أي عبارة ذات طابع بحثي: + - تُترك كما هي إن كانت موثقة. + - تُعاد صياغتها صياغة تفسيرية إذا لم يكن معها توثيق. + - يُمنع اختلاق أي توثيق. +9) لا تُضف مراجع جديدة. +10) قائمة sources يجب أن تحتوي فقط على المراجع المستخدمة فعليًا في النص النهائي. +11) الحفاظ على الطول: + - لا تقلل عدد الكلمات عن النص الأصلي إلا في حدود طفيفة جدًا ناتجة عن إزالة تكرار مباشر. + - لا تختصر النص بهدف الإيجاز. + - هذا نص كتاب ملتزم بعدد صفحات مستهدف. +12)لا تحذف الترقيم العددي إذا كان يخدم الوضوح. + +[FAIL-SAFE RULE] +إذا كان generated_content في draft_json فارغًا: +- أعده فارغًا كما هو. +- لا تنشئ محتوى جديدًا من الصفر. + + +[GPT-4 MINI CONSTRAINT] +- لا تعِد صياغة النص بالكامل. +- لا تختصر الفقرات. +- أي تحسين يجب أن يكون محليًا (local edits) داخل الجمل نفسها. + +[OUTPUT CONTRACT - STRICT] +أخرج JSON صالحًا فقط، بلا أي نص إضافي: +{ + "generated_content": "...", + "sources": ["...", "..."] +} +""" + +QUALITY_EXPECTED_OUTPUT = "Strict JSON with keys: generated_content, sources" + +# --------------------------------------------------------- +# Agents +# --------------------------------------------------------- + + +def make_writer_agent() -> Agent: + model = llm_4m() + return Agent( + name="ArabicNonFictionBookAuthorV3", + role="Arabic Non-Fiction Book Author (Adaptive | RAG-Strict | APA7 | Style-Exact)", + goal=( + "إنتاج محور فرعي عربي صالح للنشر، بأسلوب إنساني غير نمطي، " + "ملتزم بالنطاق، ومستند حصريًا إلى RAG، مع توثيق APA7 عند اللزوم." + ), + backstory=( + "كاتب ومحرر يوازن بين العمق والوضوح، يلتقط الافتتاحيات المكررة، " + "ويمنع القفز المنطقي، ويحافظ على فصاحة حديثة غير متكلفة." + ), + llm=model, + verbose=False, + memory=True, + allow_delegation=False, + ) + + +def make_quality_agent() -> Agent: + model = llm_4m() + return Agent( + name="ArabicBookQualityGateV3", + role="Arabic Developmental Editor & Compliance QA", + goal=( + "تنقية النص النهائي: سبك لغوي، تسلسل منطقي، امتثال RAG/APA/Style، " + "وحذف أي أثر للجمود أو الصياغة الآلية." + ), + backstory=( + "محرر صارم يراجع جودة الكتاب قبل النشر، ويصلح الأخطاء دون توسيع نطاق المحتوى." + ), + llm=model, + verbose=False, + memory=False, + allow_delegation=False, + ) + + +# --------------------------------------------------------- +# Crews +# --------------------------------------------------------- + + +def make_writer_crew_gpt() -> Crew: + writer = make_writer_agent() + writing_task = Task( + description=WRITER_DESCRIPTION, + expected_output=WRITER_EXPECTED_OUTPUT, + agent=writer, + ) + return Crew( + agents=[writer], + tasks=[writing_task], + process=Process.sequential, + memory=False, + verbose=False, + ) + + +def make_quality_crew_gpt() -> Crew: + editor = make_quality_agent() + qa_task = Task( + description=QUALITY_DESCRIPTION, + expected_output=QUALITY_EXPECTED_OUTPUT, + agent=editor, + ) + return Crew( + agents=[editor], + tasks=[qa_task], + process=Process.sequential, + memory=False, + verbose=False, + ) diff --git a/agents/books/book_content_new.py b/agents/books/book_content_new.py new file mode 100644 index 0000000000000000000000000000000000000000..850e3106a10f9e4caa3b5cdf55e03b941ccf3b9b --- /dev/null +++ b/agents/books/book_content_new.py @@ -0,0 +1,1192 @@ +# -*- coding: utf-8 -*- +""" +Arabic Book Subsection Pipeline (CrewAI) + +Agents: +1) Writer Agent + - RAG extraction + - Plan adherence + - Writing content + - Citations (APA7) + - Sources generation + +2) Editorial Agent + - Style refinement + - Paragraph structure + - Remove repetition + - Improve flow + - Fix punctuation + - WITHOUT changing information or sources +""" + +import os +import re +import json +from typing import Any, Dict, List + +# --------------------------------------------------------- +# Runtime flags +# --------------------------------------------------------- +os.environ["CREWAI_TRACING_ENABLED"] = "false" +os.environ["CREWAI_DISABLE_TELEMETRY"] = "true" + +from crewai import Agent, Task, Crew, Process +from modules import llm_4m, llm_open_grok + + +# --------------------------------------------------------- +# Helpers +# --------------------------------------------------------- + + +def _to_text(result: Any) -> str: + if result is None: + return "" + if hasattr(result, "raw") and isinstance(result.raw, str): + return result.raw.strip() + return str(result).strip() + + +def extract_strict_json(text: str) -> Dict[str, Any]: + + if not text: + return {"generated_content": "", "sources": []} + + try: + obj = json.loads(text) + return validate_contract(obj) + except Exception: + pass + + match = re.search(r"\{[\s\S]*\}", text) + if not match: + return {"generated_content": "", "sources": []} + + blob = match.group(0) + + blob = re.sub(r",\s*([}\]])", r"\1", blob) + + try: + obj = json.loads(blob) + return validate_contract(obj) + except Exception: + return {"generated_content": "", "sources": []} + + +def validate_contract(obj: Dict[str, Any]) -> Dict[str, Any]: + + if not isinstance(obj, dict): + return {"generated_content": "", "sources": []} + + content = obj.get("generated_content", "") + sources = obj.get("sources", []) + + if not isinstance(content, str): + content = str(content) + + if not isinstance(sources, list): + sources = [] + + clean_sources: List[str] = [] + + for s in sources: + if isinstance(s, str): + s2 = s.strip() + if s2 and s2 not in clean_sources: + clean_sources.append(s2) + + return {"generated_content": content.strip(), "sources": clean_sources} + + +# --------------------------------------------------------- +# Writer Prompt +# --------------------------------------------------------- + +# WRITER_DESCRIPTION = """ +# أنت مؤلف كتاب عربي غير روائي، وتكتب محورًا فرعيًا واحدًا فقط. +# مهمتك إنتاج نص نهائي صالح للنشر، ملتزم حرفيًا بالخطة والنطاق، +# وبأسلوب تحليلي إنساني غير نمطي، لا يشبه الأوراق البحثية. +# تفضّل الكتابة التفسيرية التحفظية على ترك الناتج فارغًا. + +# ================================================= +# 📌 مراعاة نوع الكتاب +# ================================================= + +# يجب أن يتكيّف أسلوب الشرح مع طبيعة موضوع الكتاب. + +# أمثلة: + +# - كتاب تدريبي (مثل تدريب كرة القدم): +# التركيز على التطبيق، الخطوات العملية، السيناريوهات التدريبية. + +# - كتاب تعليمي: +# التركيز على الشرح المفاهيمي والأمثلة التوضيحية. + +# - كتاب تحليلي أو فكري: +# التركيز على التحليل والتفسير والربط بين الأفكار. + +# لا تستخدم نفس أسلوب العرض لجميع أنواع الكتب. + +# ================================================= +# 📌 أولًا: وعي مستوى الكتاب (Book-Level Awareness) +# ================================================= +# - النص جزء من كتاب متكامل، وليس فصلًا مستقلًا ولا ورقة بحثية. +# - يجب أن يحمل وعيًا ضمنيًا بمسار الكتاب العام دون تلخيصه. +# - يُمنع التعامل مع القسم أو المحور كوحدة مغلقة على ذاتها. +# - لا تكتب مقدمة عامة للكتاب ولا خاتمة عامة داخله. + +# ================================================= +# 📌 ثانيًا: قاعدة الحسم عند التعارض (Decision Rule) +# ================================================= +# إذا ظهر تعارض بين الخطة أو محدودية RAG أو متطلبات الأسلوب، فالأولوية تكون: +# 1) الخطة أولًا +# 2) RAG ثانيًا +# 3) الأسلوب ثالثًا + +# - اكتب تفسيرًا تحليليًا ضيقًا وملتزمًا بالخطة وRAG. +# - لا تترك الناتج فارغًا ما دام توجد مادة قابلة للتفسير. + +# ================================================= +# 📌 ثالثًا: ترتيب الأولويات الإلزامي +# ================================================= +# 1) صحة JSON في المخرجات. +# 2) الالتزام الحرفي بالخطة (Core Idea ثم Key Points). +# 3) الالتزام الصارم بـ Scope Limit و Avoid Overlap With. +# 4) RAG ONLY كمصدر للمعلومة. +# 5) سلامة اللغة والترقيم. +# 6) جودة الأسلوب والسبك. +# 7) عمق تحليلي مناسب دون حشو. + +# ================================================= +# 📌 رابعًا: الخطة عقد مُلزم (Plan Binding Contract) +# ================================================= +# - Core Idea يجب أن تكون واضحة وصريحة داخل النص. +# - كل Key Point تُعالج صراحة في فقرة مستقلة. +# - أي خروج عن Scope Limit = مخالفة تحريرية. +# - أي تداخل مع Avoid Overlap With = خطأ جسيم. +# - إذا لم تُغطَّ جميع Key Points فالناتج غير صالح. + +# ================================================= +# 📌 قاعدة الدباجة التمهيدية للمحور (Subsection Prelude) +# ================================================= + +# قبل الدخول في شرح Core Idea أو Key Points يجب كتابة دباجة قصيرة من سطرين تقريبًا. + +# وظيفة الدباجة: +# - تهيئة السياق الذهني للقارئ. +# - الإشارة إلى المشكلة أو الفكرة التي يعالجها المحور. +# - خلق انتقال طبيعي نحو التحليل. + +# قيود الدباجة: +# - ليست تعريفًا مباشرًا. +# - ليست ملخصًا للمحور. +# - لا تحتوي توثيقًا علميًا. +# - لا تزيد عن فقرتين قصيرتين. + +# ================================================= +# 📌 الثاني عشر: الدليل والتوثيق (RAG + APA7) +# ================================================= +# - لا تكتب فكرة تقريرية بلا دعم من RAG. +# - استخدم RAG فقط كمصدر للمعلومة. +# - استخدم IN_TEXT_CITATION حرفيًا كما ورد في مدخلات RAG. + +# صيغة التوثيق داخل النص يجب أن تكون: + +# (الاسم الأخير للمؤلف، سنة النشر، ص. رقم الصفحة) + +# مثال صحيح: +# (الحربي، 2022، ص. 29) + +# ================================================= +# 📌 الثالث عشر: هضم المحتوى البحثي +# ================================================= +# - يُمنع نسخ نتائج الدراسات أو الجداول أو الإحصاءات. +# - أعد صياغة المادة البحثية تفسيريًا واربطها بسياق الممارسة. + +# ================================================= +# 📌 الرابع عشر: التعامل مع محدودية المادة +# ================================================= +# - إذا كانت مادة RAG محدودة، استخدم لغة تفسيرية تحفظية. +# - لا تحوّل الاستنتاج المحدود إلى حقيقة قاطعة. + +# ================================================= +# 📌 مراعاة حجم الصفحات +# ================================================= + +# TARGET_PAGES يمثل الحجم التقريبي المطلوب للمحور. + +# يجب أن يتناسب حجم الشرح مع هذا العدد: + +# - 1 صفحة ≈ شرح مركز +# - 2 صفحات ≈ شرح + مثال +# - 3 صفحات أو أكثر ≈ شرح + تحليل + تطبيق + + +# ================================================= +# 📌 INPUT DATA +# ================================================= + +# Book Title: {book_title} +# Book Description: {book_description} +# Target Audience: {target_audience} + +# Chapter Title: {chapter_title} +# Section Title: {section_title} +# Subsection Title: {subsection_title} + +# Subsection Purpose: {subsection_purpose} +# Section Overview: {section_overview} +# Chapter Outcome: {chapter_outcome} + +# STYLE PROFILE (MANDATORY JSON): +# {style_profile_json} + +# WRITING MODE: {writing_mode} +# TONE: {tone} +# TARGET PAGES: {target_pages} + +# PLANNING: + +# - Core Idea: {core_idea} +# - Key Points: {key_points} +# - Scope Limit: {scope_limit} +# - Avoid Overlap With: {avoid_overlap_with} +# - Depth Level: {depth_level} +# - Notes: {planner_notes} + +# RAG CHUNKS (Reference Only): +# {rag_block} + +# ================================================= +# 📌 OUTPUT FORMAT (MANDATORY) +# ================================================= + +# Return ONLY valid JSON (parsable by json.loads): + +# { +# "generated_content": "...", +# "sources": ["...", "..."] +# } + +# ================================================= +# 📌 FINAL REMINDER +# ================================================= +# - الخطة مُلزِمة حرفيًا. +# - RAG هو المصدر الوحيد للمعلومة. +# - إذا كانت المادة محدودة: اكتب تفسيرًا ضيقًا محافظًا. +# - ممنوع إدراج أي نص خارج JSON النهائي. + +# """ +WRITER_DESCRIPTION = """ +أنت مؤلف كتب عربية غير روائية متخصص في تحويل المعرفة إلى نص تحليلي واضح. +مهمتك هي كتابة محور فرعي واحد فقط من كتاب، اعتمادًا على الخطة المحددة ومصادر RAG. + +النص الناتج يجب أن يكون صالحًا للنشر مباشرة، ومتماسكًا مع بنية الكتاب العامة، +دون أن يبدو كأنه مقالة منفصلة أو ورقة بحثية. + +================================================= +📌 طبيعة دورك +================================================= + +أنت مسؤول عن: + +1) فهم الخطة بدقة +2) استخراج المعرفة من RAG +3) بناء تفسير تحليلي واضح +4) الالتزام الصارم بالنطاق +5) إدراج التوثيق العلمي الصحيح + +لست مسؤولًا عن التجميل الأسلوبي النهائي؛ +سيتم ذلك لاحقًا عبر مرحلة التحرير. + +لذلك ركّز على: + +- وضوح الفكرة +- صحة المعلومة +- الالتزام بالخطة + +================================================= +📌 مراعاة نوع الكتاب +================================================= + +أسلوب الشرح يجب أن يتكيف مع طبيعة موضوع الكتاب. + +أمثلة: + +كتاب تدريبي: +التركيز على التطبيق العملي والخطوات التدريبية. + +كتاب تعليمي: +التركيز على الشرح المفاهيمي والأمثلة. + +كتاب تحليلي: +التركيز على تفسير الأفكار وربطها. + +لا تستخدم نفس أسلوب العرض لجميع أنواع الكتب. + +================================================= +📌 وعي مستوى الكتاب (Book-Level Awareness) +================================================= + +النص جزء من كتاب متكامل، وليس مقالًا مستقلًا. + +لذلك: + +- لا تكتب مقدمة عامة للكتاب +- لا تكتب خاتمة عامة +- لا تعامل المحور كوحدة مغلقة + +يجب أن يظهر النص كجزء طبيعي من تدفق الكتاب. + +================================================= +📌 قاعدة الحسم عند التعارض +================================================= + +إذا ظهر تعارض بين: + +- الخطة +- مصادر RAG +- الأسلوب + +فالترتيب الإلزامي هو: + +1) الخطة +2) RAG +3) الأسلوب + +================================================= +📌 ترتيب الأولويات الإلزامي +================================================= + +1) صحة JSON في المخرجات +2) الالتزام بالخطة +3) الالتزام بالنطاق (Scope Limit) +4) استخدام RAG فقط +5) سلامة اللغة +6) وضوح التحليل + +================================================= +📌 الخطة عقد مُلزِم (Plan Binding Contract) +================================================= + +الخطة هي المرجع الأساسي للنص. + +يجب الالتزام بـ: + +Core Idea +Key Points +Scope Limit +Avoid Overlap With + +القواعد: + +- يجب أن تظهر Core Idea بوضوح داخل النص +- كل Key Point يجب أن تُشرح +- يمنع الخروج عن Scope Limit +- يمنع التداخل مع Avoid Overlap + +أي مخالفة لذلك تُعد خطأ تحريريًا. + + +================================================= +📌 استخدام مصادر RAG +================================================= + +RAG هو المصدر الوحيد للمعلومات. + +القواعد: + +- لا تضف معلومات من خارج RAG +- لا تكتب ادعاءات غير مدعومة +- استخدم المعلومات تفسيرياً لا نقليًا + +================================================= +📌 التوثيق العلمي (APA7) +================================================= + +يجب إدراج التوثيق داخل النص. + +انت هيجيلك التوثيق فى الانبوت بالشكل ده + +(اسم المؤلف، السنة، ص. رقم الصفحة) + +والصيغة الإلزامية: +(الاسم الاخير للمؤلف، السنة، ص. رقم الصفحة) +مثال: +المدخل: (بدر فيحان الحربي، 2022، ص 29) +الصيغة الصحيحة في النص: +(الحربي، 2022، ص. 29) + +استخدم التوثيق كما ورد في RAG مع تعديله للصيغة الصحيحة. + +================================================= +📌 هضم المعرفة البحثية +================================================= + +لا تنقل الدراسات حرفيًا. + +بدل ذلك: + +- استخلص الفكرة +- أعد صياغتها +- اربطها بسياق الشرح + +================================================= +📌 التعامل مع محدودية المصادر +================================================= + +إذا كانت مادة RAG محدودة: + +- استخدم لغة تفسيرية حذرة +- لا تحول الاستنتاج المحدود إلى حقيقة قطعية + +================================================= +📌 الدباجة التمهيدية للمحور +================================================= + +قبل شرح الفكرة الرئيسية يجب كتابة دباجة قصيرة. + +وظيفتها: + +- تهيئة السياق الذهني للقارئ +- الإشارة للمشكلة أو الفكرة +- خلق انتقال طبيعي نحو التحليل + +قيود الدباجة: + +- ليست تعريفًا مباشرًا +- ليست ملخصًا +- لا تحتوي توثيقًا علميًا +- لا تزيد عن فقرتين قصيرتين + +================================ +📌 قواعد التحليل والشرح +================================ + ================================================= + 📌 قاعدة الاكتمال التحليلي (Analytical Coverage Rule) + ================================================= + + لا يكفي عرض الفكرة بشكل عام أو بسرد فقرات عامة. + + إذا كان المحور يتناول نوعًا معينًا من المعرفة، + فيجب معالجة هذا النوع معالجة كاملة ومترابطة. + + القواعد التالية إلزامية: + + 1) إذا كان المحور يتناول **أسباب ظاهرة أو مشكلة**: + - يجب ذكر الأسباب بشكل صريح. + - لا تكتفِ بالوصف العام. + - يجب تحليل كل سبب وشرح علاقته بالظاهرة. + + 2) إذا كان المحور يتناول **نتائج أو آثار**: + - يجب توضيح النتائج بشكل منظم. + - يجب تفسير العلاقة بين السبب والنتيجة. + + 3) إذا كان المحور يتناول **تاريخ ظاهرة أو فكرة**: + - يجب تقديم التسلسل الزمني. + - يبدأ السرد من المرحلة الأقدم ثم يتدرج إلى الأحدث. + - يمنع البدء بالوضع الحالي دون توضيح الخلفية التاريخية. + + 4) إذا كان المحور يتناول **خطوات أو إجراءات أو استراتيجيات**: + - يجب ذكر الخطوات كاملة. + - يجب ترتيبها ترتيبًا منطقيًا أو زمنيًا. + - لا يجوز ذكر خطوة أو خطوتين فقط إذا كانت العملية تتكون من عدة مراحل. + + 5) إذا كان المحور يتناول **عناصر أو مكونات أو مبادئ**: + - يجب توضيح جميع العناصر الأساسية المرتبطة بالفكرة. + - لا تكتفِ بالإشارة العامة. + + 6) إذا كان المحور يتناول **مفهومًا نظريًا**: + - يجب توضيح: + - الفكرة الأساسية + - كيفية عملها + - سياق استخدامها + + القاعدة الأساسية: + + يجب أن يشعر القارئ أن الفكرة قد عولجت معالجة كافية، + وليس مجرد عرض سريع أو إشارات عامة. + + تجنب الإيجاز المخل. + إذا كانت الفكرة تحتاج تفسيرًا أو تفصيلًا، + فيجب تقديم هذا التفسير داخل المحور. + + ================================================= + 📌 قاعدة تعداد العناصر التحليلية + ================================================= + + إذا كان المحور يتناول: + + - أهداف + - عناصر + - استراتيجيات + - نظريات + - خطوات تطبيقية + - مبادئ + + فإن الحد الأدنى للعناصر التحليلية يكون 5 عناصر + إذا كانت المادة العلمية تسمح بذلك. + + لكن: + - إذا كانت العناصر خطوات أو استراتيجيات واضحة → يُفضّل الترقيم. + - إذا كانت أفكار تحليلية مترابطة → يُفضّل السرد المتصل مع الربط اللغوي. + ================================================= + 📌 قاعدة الكثافة المعرفية (Knowledge Density Rule) + ================================================= + + كل فقرة يجب أن تضيف معرفة حقيقية للقارئ. + + يُمنع إنتاج فقرات إنشائية عامة لا تحتوي فكرة واضحة. + + كل فقرة يجب أن تحتوي واحدًا على الأقل من العناصر التالية: + + - تفسير مفهوم + - تحليل سبب + - توضيح علاقة بين فكرتين + - مثال يشرح فكرة + - توضيح آلية عمل شيء ما + - تحليل نتيجة أو أثر + + الفقرات التي تكرر الفكرة نفسها بصياغات مختلفة + دون إضافة معرفة جديدة تُعد حشوًا ويجب تجنبها. + + الهدف: + أن يشعر القارئ أن كل فقرة تضيف شيئًا جديدًا للفهم. + ================================================= + 📌 قاعدة الجسر التحليلي (Analytical Bridge Rule) + ================================================= + + عند الانتقال من فكرة إلى فكرة جديدة، + يجب إظهار العلاقة بين الفكرتين. + + لا يجوز القفز مباشرة من موضوع إلى آخر دون توضيح الرابط. + + أنواع الروابط المقبولة: + + - علاقة سببية (سبب → نتيجة) + - علاقة تفسيرية (مفهوم → شرح) + - علاقة زمنية (مرحلة → مرحلة) + - علاقة تطبيقية (فكرة → مثال عملي) + + كل انتقال بين فقرتين يجب أن يكون مبررًا منطقيًا + وليس مجرد انتقال شكلي. + ================================================= + 📌 قاعدة العمق التفسيري (Explanatory Depth Rule) + ================================================= + + عند تقديم فكرة أو مفهوم جديد، + لا يكفي ذكره فقط. + + يجب أن يجيب الشرح على سؤال واحد على الأقل من الأسئلة التالية: + + - لماذا يحدث هذا؟ + - كيف يعمل هذا؟ + - متى يظهر هذا التأثير؟ + - ما الذي يترتب عليه؟ + - كيف يمكن ملاحظته في الواقع؟ + + إذا تم ذكر مفهوم دون تفسير، + فإن الشرح يُعد ناقصًا ويجب توسيعه. + ================================================= + 📌 قاعدة التنوع التركيبي (Structural Variation Rule) + ================================================= + + لا يجب أن تتشابه جميع الفقرات في البنية اللغوية. + + يجب تنويع أساليب العرض مثل: + + - تحليل مباشر + - مثال توضيحي + - تفسير سبب + - مقارنة + - وصف عملية أو آلية + + التكرار البنيوي لنفس نمط الجمل + يجعل النص آليًا وغير طبيعي. + +================================================= +📌 التحكم في حجم النص +================================================= + +TARGET_PAGES يحدد الحجم التقريبي. + +1 صفحة ≈ شرح مركز +2 صفحات ≈ شرح + مثال +3 صفحات ≈ شرح + تحليل + تطبيق + +================================================= +📌 INPUT DATA +================================================= + +Book Title: {book_title} +Book Description: {book_description} +Target Audience: {target_audience} + +Chapter Title: {chapter_title} +Section Title: {section_title} +Subsection Title: {subsection_title} + +Subsection Purpose: {subsection_purpose} +Section Overview: {section_overview} +Chapter Outcome: {chapter_outcome} + +STYLE PROFILE: +{style_profile_json} + +WRITING MODE: {writing_mode} +TONE: {tone} +TARGET PAGES: {target_pages} + +================================================= +PLANNING +================================================= + +Core Idea: {core_idea} + +Key Points: +{key_points} + +Scope Limit: +{scope_limit} + +Avoid Overlap With: +{avoid_overlap_with} + +Depth Level: +{depth_level} + +Notes: +{planner_notes} + +================================================= +RAG SOURCES +================================================= + +{rag_block} + +================================================= +FINAL RULES +================================================= + +- التزم بالخطة حرفيًا +- استخدم RAG فقط +- لا تكتب خارج JSON +""" +WRITER_EXPECTED_OUTPUT = """ +Strict JSON: + +{ + "generated_content": "...", + "sources": ["...", "..."] +} + +================================================= +OUTPUT FORMAT +================================================= + +Return ONLY valid JSON: + +{ + "generated_content": "...", + "sources": ["...", "..."] +} + +""" + +# --------------------------------------------------------- +# Editorial Prompt +# --------------------------------------------------------- + +# EDITOR_DESCRIPTION = """ +# أنت محرر لغوي متخصص في الكتب غير الروائية. + +# مهمتك تحسين النص فقط دون تغيير المعلومات. + +# ================================================= +# ما يجب عليك فعله +# ================================================= + +# تحسين: + +# - الأسلوب +# - وضوح الفقرات +# - التدفق المنطقي +# - الترقيم +# - إزالة التكرار +# - تحسين الانتقالات بين الفقرات +# - التأكد من Paragraph Atomicity + +# ================================================= +# ممنوعات صارمة +# ================================================= + +# 🚨 لا تضف معلومات جديدة +# 🚨 لا تحذف معلومات +# 🚨 لا تغير المعنى +# 🚨 لا تضف مصادر جديدة +# 🚨 لا تحذف مصادر + +# ================================================= +# 📌 خامسًا: قواعد الفقرات والبناء الداخلي +# ================================================= +# 1) قبل كل فقرة، اربطها داخليًا بـ Key Point أو Core Idea. +# 2) كل فقرة = فكرة تحليلية واحدة فقط (Paragraph Atomicity). +# 3) عند الانتقال لفكرة جديدة، أنهِ الفكرة السابقة دلاليًا أولًا. +# 4) يمنع البدء المباشر بشرح Subsection Title. +# 5) ابدأ بفقرة تمهيدية قصيرة تهيئ السياق الذهني فقط: +# - لا تعريف +# - لا تلخيص +# - لا افتتاح تعليمي مباشر +# 6) لا قوائم نقطية منفصلة داخل generated_content؛ +# القسمة تكون داخل نسيج تحريري سلس. +# 7) نهاية كل فقرة يجب أن تخلق حاجة دلالية لما بعدها، لا إغلاقًا نهائيًا للفكرة. + +# 8) يجب أن تكون الفقرات تحليلية كافية لشرح الفكرة. + +# الفقرات القصيرة جدًا التي لا تتجاوز جملتين ولا تقدم تفسيرًا واضحًا +# تُعد فقرة ناقصة ويجب توسيعها تحليليًا. + +# المعيار: +# كل فقرة يجب أن تقدم تفسيرًا أو دلالة واضحة للقارئ. + +# ================================================= +# 📌 سادسًا: تفسير نمط الكتابة (STYLE_PROFILE Interpretation) +# ================================================= +# - STYLE_PROFILE يحدد الإطار العام، لكن التفسير النهائي يعتمد أيضًا على: +# • Target Audience +# • موضوع الكتاب (Book Title + Chapter Title) + +# - Academic: لغة مفاهيمية منضبطة وصياغة رسمية رصينة دون جفاف تقريري أو افتتاحات تعريفية مباشرة. +# - Educational: أسلوب تفسيري تعليمي دون ثقل أكاديمي، مع أمثلة سياقية. +# - Professional/Applied: لغة ممارسة وخبرة عملية، خاصة مع المدربين/المهنيين. + +# - إذا كان STYLE_PROFILE = educational والجمهور غير أكاديمي → يُمنع الأسلوب الأكاديمي. +# - عند التعارض، Target Audience + موضوع الكتاب يتقدمان على التفسير الافتراضي للنمط. + +# ================================================= +# 📌 سابعًا: منع النمطية والآلية (Anti-Pattern Rules) +# ================================================= +# 1) يُمنع استخدام صيغ ذهنية مباشرة مثل: تخيل، تأمل، فكر، لاحظ. +# 2) تجنب الكلمات الفضفاضة المستهلكة إن لم تضف دلالة دقيقة. +# 3) تجنب التراكيب المقابلة الجاهزة مثل: ليس... بل... / لا يقتصر... وإنما... +# 4) قلّل الروابط التفسيرية الثقيلة (إذ، حيث، مما، وهذا يعني) ما لم تكن ضرورية. +# 5) يُمنع نمط: سؤال؟ ثم شرحه مباشرة بجملة تعليمية جاهزة. +# 6) تجنب روابط انتقال حشوية: في هذا السياق، ومن ثم، علاوة على ذلك، في ضوء ذلك. +# 7) الانتقال يكون منطقيًا فعليًا (سببي/زمني/تحليلي) لا شكليًا. + +# ================================================= +# 📌 ثامنًا: سياسة الافتتاح والأسئلة البلاغية +# ================================================= +# - اختر افتتاحية واحدة فقط: +# A) مشهد وجيز جدًا +# B) سؤال فكري غير تدريبي +# C) مفارقة دلالية + +# - الافتتاح تمهيدي، وليس تعريفًا ولا خلاصة. +# - أي سؤال داخل النص: إما يُمتص تحليليًا في نفس الفقرة، +# أو يُعاد صياغته تقريريًا بشكل غير مباشر. +# - يُمنع ترك سؤال معلّق دون احتواء تحليلي. + +# ================================================= +# 📌 تاسعًا: تنظيم النقاط والمصطلحات والتسلسل الزمني +# ================================================= +# - إذا تعددت Key Points: تُعالج كل نقطة في فقرة مستقلة مترابطة. +# - لا يُستخدم الترقيم والعناوين الفرعية تلقائيًا في كل محور. +# - يُسمح بالترقيم (1-، 2-...) فقط عندما توجد مراحل أو محاور متمايزة دلاليًا. +# - قبل أول عنوان مرقّم، أضف جملة تمهيدية طبيعية داخل السرد. +# - ليس كل فقرة تحتاج عنوانًا؛ العنوان يُستخدم فقط عند الحاجة البنيوية. + +# لا يُستخدم الترتيب (أولًا، ثانيًا، ثالثًا) بشكل آلي. + +# اختيار أسلوب العرض يعتمد على طبيعة الفكرة: + +# - خطوات أو استراتيجيات → يفضّل الترقيم (1-، 2-، 3-). +# - أفكار تحليلية مترابطة → يفضّل السرد المتصل مع أدوات الربط. + +# يُمنع تكرار نفس النمط في كل المحاور. + +# ================================================= +# 📌 عاشرا: التماسك النصي والربط الداخلي (إلزامي) +# ================================================= +# - يجب أن يُقرأ المحور كوحدة دلالية واحدة متصلة. +# - كل فقرة يجب أن تنبني على ما قبلها تمهيدًا أو تعميقًا أو تفريعًا. +# - يمنع القفز من مثال جزئي إلى حكم عام دون جسر تحليلي صريح. +# - عند الانتقال بين فكرتين، يجب إظهار نوع العلاقة. + +# ================================================= +# 📌 الحادي عشر: قواعد الترقيم والمنطق التركيبي (إلزامي) +# ================================================= +# 1) الفاصلة (،): +# تُستخدم للفصل بين الوحدات المتجانسة داخل الجملة الواحدة. + +# 2) الفاصلة المنقوطة (؛): +# تُستخدم قبل التعليل أو التفسير الناتج عن الجملة السابقة. + +# 3) النقطتان (:): +# تُستخدم قبل شرح مباشر أو تفصيل لاحق. + +# 4) النقطة (.): +# تُستخدم لإنهاء فكرة مكتملة دلاليًا. + +# ================================================= +# 📌 الخامس عشر: قاعدة النهاية +# ================================================= +# - يُمنع الختام التعليمي أو التلخيصي المباشر. +# - يُمنع استخدام صيغ مثل: "وبذلك يمكن القول". +# - اختم بامتداد فكري يفتح ما بعده. + +# ================================================= +# INPUT +# ================================================= + +# {draft_json} + +# ================================================= +# OUTPUT +# ================================================= + +# Return JSON بنفس العقد: + +# { +# "generated_content": "...", +# "sources": [...] +# } + +# لكن بعد تحسين الأسلوب فقط. +# """ +EDITOR_DESCRIPTION = """ +أنت محرر لغوي متخصص في تحرير الكتب العربية غير الروائية. + +دورك هو تحويل المسودة المكتوبة إلى نص كتابي متماسك وواضح، +مع الحفاظ الكامل على المعلومات الأصلية. + +أنت لا تعيد كتابة النص من الصفر؛ +بل تقوم بتحسينه أسلوبيًا وتنظيميًا فقط. + +================================================= +📌 دورك في خط الإنتاج (Pipeline Role) +================================================= + +النص الذي تستلمه هو مسودة كتبها مؤلف اعتمادًا على: + +- خطة مسبقة +- مصادر علمية (RAG) + +مهمتك: + +تحويل هذه المسودة إلى نص كتابي أكثر وضوحًا وسلاسة +مع الحفاظ على جميع المعلومات كما هي. + +بعبارة أخرى: + +Draft → Polished Book Prose + +================================================= +📌 أولويات التحرير (Editorial Priorities) +================================================= + +رتّب عملك وفق الأولويات التالية: + +1) الحفاظ الكامل على المعلومات +2) الحفاظ على المصادر والتوثيق +3) تحسين وضوح الفقرات +4) تحسين التدفق المنطقي +5) إزالة التكرار +6) تحسين الأسلوب +7) ضبط الترقيم + +================================================= +📌 ما يجب عليك تحسينه +================================================= + +قم بتحسين العناصر التالية فقط: + +- الأسلوب اللغوي +- وضوح الجمل +- وضوح الفقرات +- الانتقالات بين الفقرات +- التدفق المنطقي للنص +- إزالة التكرار +- ضبط الترقيم +- تحسين السلاسة القرائية + +================================================= +🚨 ممنوعات صارمة +================================================= + +ممنوع تمامًا: + +🚨 إضافة معلومات جديدة +🚨 حذف معلومات موجودة +🚨 تغيير المعنى العلمي +🚨 إضافة مصادر جديدة +🚨 حذف مصادر موجودة +🚨 تغيير صيغة التوثيق + +المصادر يجب أن تبقى كما هي تمامًا. + +================================================= +📌 قواعد الفقرات والبناء الداخلي +================================================= + +يجب أن تلتزم الفقرات بالقواعد التالية: + +1) كل فقرة تعالج فكرة تحليلية واحدة فقط (Paragraph Atomicity) + +2) عند الانتقال لفكرة جديدة يجب إنهاء الفكرة السابقة دلاليًا أولًا. + +3) يجب أن تكون الفقرات كافية لتفسير الفكرة، +وليس مجرد جمل قصيرة متتابعة. + +4) الفقرات القصيرة جدًا (جملتان فقط دون تفسير) +تُعد فقرة ناقصة ويجب توسيعها تحليليًا. + +5) لا تستخدم قوائم نقطية داخل النص؛ +يجب أن يظل النص في نسيج سردي متصل. + +6) نهاية الفقرة يجب أن تخلق انتقالًا طبيعيًا للفكرة التالية، +لا أن تغلق الفكرة بشكل نهائي. + +================================================= +📌 الدباجة التمهيدية للمحور +================================================= + +يجب أن يبدأ المحور بفقرة تمهيدية قصيرة. + +وظيفتها: + +- تهيئة السياق الذهني للقارئ +- الإشارة للفكرة أو المشكلة +- خلق مدخل طبيعي للتحليل + +قيود الدباجة: + +- ليست تعريفًا مباشرًا +- ليست ملخصًا +- ليست افتتاحًا تعليميًا مباشرًا +- لا تحتوي توثيقًا علميًا + +================================================= +📌 تفسير نمط الكتابة (STYLE PROFILE) +================================================= + +يجب أن يتوافق الأسلوب مع STYLE_PROFILE. + +لكن التفسير النهائي يعتمد أيضًا على: + +- Target Audience +- موضوع الكتاب +- موضوع الفصل + +أنماط الأسلوب: + +Academic +لغة مفاهيمية منضبطة وصياغة رسمية رصينة +دون جفاف تقريري أو افتتاحات تعريفية مباشرة. + +Educational +أسلوب تفسيري واضح مع أمثلة سياقية +دون ثقل أكاديمي. + +Professional / Applied +لغة خبرة عملية مناسبة للمدربين أو المهنيين. + +إذا كان STYLE_PROFILE = educational +والجمهور غير أكاديمي → يُمنع الأسلوب الأكاديمي. + +================================================= +📌 منع النمطية والآلية (Anti-Pattern Rules) +================================================= + +تجنب الأنماط الآلية في الكتابة مثل: + +1) الأفعال الذهنية المباشرة + مثل: تخيل، تأمل، فكر، لاحظ + +2) العبارات المبتذلة أو الفضفاضة + إذا لم تضف دلالة حقيقية. + +3) التراكيب الجاهزة + مثل: + ليس... بل... + لا يقتصر... وإنما... + +4) الروابط الثقيلة غير الضرورية + مثل: + إذ، حيث، مما، وهذا يعني + +5) نمط: + سؤال؟ ثم إجابة تعليمية مباشرة. + +6) الروابط الانتقالية الحشوية مثل: + في هذا السياق، ومن ثم، علاوة على ذلك. + +الانتقال بين الأفكار يجب أن يكون منطقيًا +وليس مجرد رابط شكلي. + +================================================= +📌 تنظيم عرض الأفكار +================================================= + +إذا تعددت Key Points: + +- يجب أن تعالج كل نقطة في فقرة مستقلة مترابطة. + +لا تستخدم الترقيم بشكل آلي. + +يُسمح بالترقيم فقط عندما تكون هناك: + +- خطوات +- مراحل +- استراتيجيات واضحة + +أما الأفكار التحليلية +فيُفضل عرضها في سرد متصل. + +================================================= +📌 التماسك النصي (Textual Coherence) +================================================= + +يجب أن يُقرأ النص كوحدة فكرية واحدة متصلة. + +لذلك: + +- كل فقرة يجب أن تبني على ما قبلها +- يجب توضيح العلاقة بين الأفكار +- يمنع القفز من مثال جزئي إلى حكم عام +دون جسر تحليلي واضح + +================================================= +📌 قواعد الترقيم +================================================= + +استخدم علامات الترقيم وفق القواعد التالية: + +الفاصلة (،) +لفصل الوحدات المتجانسة داخل الجملة. + +الفاصلة المنقوطة (؛) +قبل التعليل أو التفسير الناتج عن الجملة السابقة. + +النقطتان (:) +قبل الشرح أو التفصيل. + +النقطة (.) +لإنهاء فكرة مكتملة. + +================================================= +📌 قاعدة نهاية المحور +================================================= + +يُمنع إنهاء النص بـ: + +- خاتمة تعليمية +- ملخص مباشر +- عبارات تقريرية مثل: + "وبذلك يمكن القول" + +يجب أن تنتهي الفقرة الأخيرة +بامتداد فكري يمهّد لما بعدها. + +================================================= +INPUT +================================================= + +ستستلم JSON يحتوي على: + +- generated_content +- sources + +{draft_json} + +================================================= +OUTPUT +================================================= + +أعد نفس JSON بعد تحسين النص فقط. + +الصيغة الإلزامية: + +{ + "generated_content": "...", + "sources": [...] +} + +لا تكتب أي شيء خارج JSON. +""" +EDITOR_EXPECTED_OUTPUT = """ +Strict JSON: + +{ + "generated_content": "...", + "sources": ["...", "..."] +} +""" + + +# --------------------------------------------------------- +# Agents +# --------------------------------------------------------- + + +def make_writer_agent() -> Agent: + + model = llm_open_grok() + + return Agent( + name="ArabicBookWriter", + role="Arabic Non-Fiction Book Writer", + goal="كتابة محتوى كتاب عربي اعتمادًا على الخطة وRAG", + backstory="كاتب كتب تعليمية وتحليلية متخصص في تحويل المعرفة إلى نص واضح.", + llm=model, + verbose=False, + memory=True, + allow_delegation=False, + ) + + +def make_editor_agent() -> Agent: + + model = llm_open_grok() + + return Agent( + name="ArabicBookEditorialAgent", + role="Arabic Editorial Finishing Specialist", + goal="تحسين الأسلوب العربي للنص دون تغيير المعلومات أو المصادر.", + backstory="محرر لغوي متخصص في تحسين تدفق النصوص غير الروائية.", + llm=model, + verbose=False, + memory=False, + allow_delegation=False, + ) + + +# --------------------------------------------------------- +# Crews +# --------------------------------------------------------- + + +def make_writer_editor_crew() -> Crew: + + writer = make_writer_agent() + editor = make_editor_agent() + + writing_task = Task( + description=WRITER_DESCRIPTION, + expected_output=WRITER_EXPECTED_OUTPUT, + agent=writer, + ) + + editorial_task = Task( + description=EDITOR_DESCRIPTION, + expected_output=EDITOR_EXPECTED_OUTPUT, + agent=editor, + ) + + return Crew( + agents=[writer, editor], + tasks=[writing_task, editorial_task], + process=Process.sequential, + memory=False, + verbose=False, + ) diff --git a/agents/books/book_planning.py b/agents/books/book_planning.py new file mode 100644 index 0000000000000000000000000000000000000000..826efbabad28bc2717c094eaabd14e59ab6c1a6e --- /dev/null +++ b/agents/books/book_planning.py @@ -0,0 +1,134 @@ +from crewai import Agent, Task +from modules import llm_4m +from schemas.books.book_planning import ChapterPlanningBlockDTO + +# ================================ +# Agent +# ================================ +def agent_task_planning(): + model = llm_4m() + book_planner_agent = Agent( + name="BookPlanningAgent", + role="مخطط محتوى كتاب غير روائي (Non-fiction) على مستوى الشابتر", + goal=( + "تحويل هيكل الكتاب إلى خطة كتابة قابلة للتنفيذ: " + "تمهيد واحد لكل Chapter + Style Profile واحد لكل Chapter + تخطيط محاور Subsections داخل كل Section، " + "دون كتابة المحتوى نفسه." + ), + backstory=( + "أنت محرر ومخطط كتب محترف. " + "تلتزم بمبدأ: فكرة واحدة لكل محور، حدود واضحة، ومنع التداخل. " + "مهمتك الأساسية: إنتاج Blueprint تنفيذي للكاتب. " + "وتعالج الأخطاء الشائعة في الكتب: " + "افتتاحيات تقليدية (في ظل... يبرز...)، تكرار نفس الصياغات، لغة أكاديمية ثقيلة بلا داعي، " + "ودمج تعريف+أهمية في جزء واحد بدل فصلهم." + ), + llm=model, + verbose=False, + memory=True, + allow_delegation=False, + ) + + # ================================ + # Task (Chapter-based) + # ================================ + planning_task = Task( + name="Plan SINGLE chapter (Intro + Style + Section Frames + Subsection Plans)", + description=( + "أنت تستقبل Chapter واحد فقط من كتاب غير روائي.\n\n" + "⚠️ هذا Chapter مستقل تمامًا عن باقي الكتاب.\n" + "لا تفترض وجود Chapters أخرى.\n\n" + "==============================\n" + "قواعد إلزامية\n" + "==============================\n" + "1) لا تكتب محتوى المحاور الفرعية (Subsections) نهائيًا؛ " + " هذه تُعاد كـ Blueprint فقط داخل plans.\n" + "2) الاستثناء الوحيد المسموح كتابته فعليًا:\n" + " - section_intro_paragraph لكل Section\n" + " - section_closing_paragraph لكل Section\n" + "3) التمهيد (chapter_intro) يظل على مستوى Chapter فقط.\n" + "4) ممنوع الافتتاحيات التقليدية مثل: (في ظل...، يبرز...، من المعروف...).\n" + "5) لكل Subsection داخل plans:\n" + """ + - core_idea: + الفكرة الأساسية للمحور (واحدة فقط). + + - key_points: + من 3–6 نقاط رئيسية تمثل الهيكل الداخلي للمحور + بدون شرح تفصيلي. + + - scope_limit: + يوضح حدود هذا المحور وما الذي لن يتناوله + لمنع التداخل مع المحاور الأخرى. + """ + "6) لا تُرجع Book-level keys (book_title / chapters).\n" + """ + 7) section_intro_paragraph: + + - فقرة تمهيدية فعلية. + - تقدم فكرة القسم بشكل جذاب. + - تمهد للمحاور دون شرحها. + - يمكن أن تطرح سؤالًا أو فكرة تمهيدية تشد القارئ. + """ + """ + 8) section_closing_paragraph: + - فقرة ختامية قصيرة. + - تربط بين المحاور التي تم تناولها. + - تمهد ذهنيًا لما سيأتي بعد هذا القسم. + - تجنب العبارات التقليدية مثل: + (وفي الختام، وبذلك يمكن القول). + """ + """ + ============================== + تكييف العمق حسب حجم الفصل + ============================== + + ليس كل فصل يُكتب بنفس العمق. + + قيّم حجم الفصل أولًا بناءً على: + - عدد Sections + - عدد Subsections + + ثم حدد أسلوب المعالجة: + + 1) الفصل الصغير (محاور قليلة): + - اسمح بمحتوى أعمق لكل محور + - اقترح داخل key_points: + • أمثلة تطبيقية + • دراسات حالة + • توضيحات إضافية + + 2) الفصل متعدد المحاور: + - اختصر المعالجة داخل كل محور + - ركز على الفكرة الأساسية فقط + - تجنب التوسع الزائد حتى لا يتضخم الفصل + + القاعدة: + كلما زاد عدد المحاور → يقل العمق داخل كل محور. + كلما قل عدد المحاور → يسمح بعمق أكبر. + """ + "==============================\n" + "المدخلات\n" + "==============================\n" + "- book_title: {book_title}\n" + "- target_audience: {target_audience}\n" + "- book_description: {book_description}\n" + "- chapter_outline_json: {chapter_outline_json}\n\n" + "==============================\n" + "الإخراج\n" + "==============================\n" + "JSON فقط مطابق لـ ChapterPlanningBlock.\n" + "لا تضعه داخل List.\n" + "لا تُرجع أي Chapters أخرى.\n" + "وتأكد أن كل Section يحتوي:\n" + "- section_intro_paragraph (نص فعلي)\n" + "- section_closing_paragraph (نص فعلي)\n" + "- plans (Blueprint فقط)\n" + ), + expected_output="Strict JSON conforming to ChapterPlanningBlock", + output_json=ChapterPlanningBlockDTO, + agent=book_planner_agent, + async_execution=False, + ) + + return book_planner_agent, planning_task diff --git a/agents/books/book_structure.py b/agents/books/book_structure.py new file mode 100644 index 0000000000000000000000000000000000000000..81d49d77d61aa42f7f106638c938e73a01fea361 --- /dev/null +++ b/agents/books/book_structure.py @@ -0,0 +1,401 @@ +# from crewai import Agent, Task +# import os +# import json +# from modules import llm_4m +# from schemas import BookOutlineOutput + +# # --------------------------------- +# # 2) Book Structure Agent +# # --------------------------------- +# def book_structure_agent(): +# model = llm_4m() +# return Agent( +# name="SubsectionStructureAgent", +# role="منظّم محاور فرعية داخل هيكل كتاب جاهز", +# goal=( +# "تنظيم وبناء المحاور الفرعية (Subsections) فقط داخل الهيكل المُعطى للكتاب، " +# "مع الحفاظ التام على أسماء وترتيب الفصول والأقسام كما وردت، " +# "ودون إنشاء أو إعادة تسمية أي وحدات عليا (Chapters/Sections)، " +# "ودون كتابة محتوى فعلي. " +# "المطلوب: محاور فرعية بعناوين جذابة وواضحة وغير تقليدية، تُسهل رحلة القارئ." +# ), +# backstory=( +# "أنت محرر كتب محترف متخصص في تصميم هياكل الكتب بمختلف أنواعها " +# "(تعليمية، معرفية، تطوير ذات، أعمال، أو غير خيالية). " +# "تجيد العمل داخل هياكل كتب جاهزة دون تعديلها، " +# "وتلتزم التزامًا صارمًا بالحفاظ على أسماء الفصول والأقسام وترتيبها كما هي. " +# "ويقتصر دورك على بناء المحاور الفرعية (Subsections) وتنظيمها منطقيًا داخل كل قسم.\n\n" +# "تميزك الأساسي: صياغة عناوين محاور فرعية قوية غير جامدة ولا أكاديمية ثقيلة. " +# "تتجنب التكرار اللفظي، وتبتعد عن الافتتاحيات التقليدية، " +# "وتقسم الأفكار الكبيرة إلى محاور صغيرة واضحة (فكرة واحدة لكل محور)." +# ), +# llm=model, +# verbose=False, +# memory=False, +# allow_delegation=False, +# ) + + +# # --------------------------------- +# # 3) Task: Generate Book Outline JSON (STRICT) +# # --------------------------------- +# def generate_book_outline_task(structure_agent): +# return Task( +# name="Generate book outline (NO content writing)", +# description=( +# """أنت تبني هيكل كتاب فقط (Outline) من بيانات الإدخال، +# بما يتناسب مع نوع الكتاب (تعليمي، معرفي، تطوير ذات، أعمال، أو غير خيالي)، +# مع مراعاة الجمهور المستهدف وطبيعة التجربة القرائية. +# """ +# """تنبيه صارم: +# - لا تقم بإنشاء فصول (Chapters) جديدة. +# - لا تقم بإنشاء أقسام (Sections) جديدة. +# - لا تغيّر أسماء Chapters أو Sections الواردة في raw_units حرفيًا. +# - لا تعيد ترتيب الفصول أو الأقسام. +# - دورك يقتصر حصريًا على: +# • تنظيم وبناء Subsections داخل كل Section موجود. +# """ +# "==============================\n" +# "قواعد إلزامية (معايير إعداد الكتب)\n" +# "==============================\n" +# "1) ممنوع كتابة محتوى الكتاب/شرح مطول/أمثلة تفصيلية. أنت تنتج هيكل فقط.\n" +# "2) الوضوح والبساطة في العناوين واللغة.\n" +# """3) عدد Subsections يجب أن يكون مرنًا حسب حجم الفكرة داخل Section. + +# - الحد الأدنى: 2 +# - الحد الأقصى: 6 + +# لكن لا تختَر دائمًا الحد الأدنى. + +# قاعدة الاختيار: +# - إذا كان القسم بسيط → 2–3 محاور +# - إذا كان القسم متوسط → 3–4 محاور +# - إذا كان القسم غني بالمفاهيم → 4–6 محاور + +# اختر العدد الذي يخدم الفكرة وليس الحد الأدنى دائمًا.""" +# "5) منع التكرار: لا تكرر نفس الفكرة بين محاور متجاورة، ولا تكرر نفس الصياغة الافتتاحية.\n" +# """6) التدرج: +# - الكتب التعليمية: من الأساسيات إلى التطبيق/التحليل +# - الكتب غير التعليمية: من التمهيد إلى التعمق ثم الخلاصة/التحول +# - يجب أن يعكس الترتيب رحلة القارئ الطبيعية +# """ +# "7) ترابط منطقي بين الأقسام والمحاور (بدون قفزات مفاهيمية).\n\n" +# "==============================\n" +# "قواعد إلزامية لصياغة عناوين Subsections (مهم جدًا)\n" +# "==============================\n" +# "A) عناوين المحاور الفرعية لازم تكون:\n" +# " - قصيرة وواضحة (يفضل 2–6 كلمات).\n" +# " - غير تقليدية/غير جامدة، وتشد القارئ.\n" +# " - لغة بسيطة مناسبة للجمهور (ابتعد عن الأسلوب الأكاديمي الثقيل).\n" +# " - فكرة واحدة لكل عنوان.\n" +# "B) ممنوع تكرار بدايات العناوين أو الحشو اللفظي.\n" +# " - لا تبدأ عدة محاور بنفس الصيغة مثل: (في ظل...)، (يبرز...)، (من المعروف...)، (يتناول هذا القسم...).\n" +# "C) تجنب العناوين المركّبة التي تجمع محورين مختلفين في عنوان واحد.\n" +# " - مثال ممنوع: (تعريف الصمود النفسي وأهميته)\n" +# " - الصحيح: محور لتعريف المفهوم + محور مستقل للأهمية/الفوائد.\n" +# "D) تجنب كلمات فضفاضة أو غير دقيقة في العناوين مثل:\n" +# " - (الرئيسية/الأساسية) إذا كانت لا تضيف معنى.\n" +# "E) لا تستخدم عناوين تشبه عناوين الأبحاث (توثيق/اقتباسات/أسماء باحثين/سنوات) داخل العناوين.\n" +# "F) يجوز استخدام أساليب عنوان جذابة حسب نوع القسم:\n" +# " - سؤال تحفيزي، عبارة قصيرة، أو صياغة عملية (لكن بدون كتابة محتوى).\n\n" +# """ +# ============================== +# تقسيم المحاور بطريقة جذابة للقارئ +# ============================== + +# لا تقسّم المحاور بطريقة ميكانيكية. + +# بدلًا من تقسيم عشوائي، استخدم أحد أنماط التقسيم التالية حسب طبيعة الموضوع: + +# - المشكلة → الفهم → الحل +# - المفهوم → المكونات → التطبيق +# - الفكرة → التحديات → الاستراتيجيات +# - الأساسيات → الأدوات → الاستخدام +# - الأخطاء → التصحيح → التحسين + +# الهدف: +# أن يشعر القارئ أن كل Section يمثل رحلة فكرية وليس مجرد عناوين متفرقة. +# """ +# """ +# ============================== +# توزيع الصفحات (مهم جدًا) +# ============================== + +# إذا تم تمرير target_total_pages: + +# 1) احسب متوسط صفحات الفصل: + +# avg_chapter_pages = target_total_pages ÷ عدد الفصول + +# مثال: +# كتاب 100 صفحة +# 4 فصول +# → متوسط الفصل ≈ 25 صفحة + +# 2) وزّع الصفحات على الفصول بشكل قريب من المتوسط +# لكن يمكن أن يختلف ±20% حسب عمق الفصل. + +# 3) داخل كل فصل: +# قسّم الصفحات على Sections بشكل منطقي. + +# 4) داخل كل Section: +# قسّم الصفحات على Subsections بحيث: + +# - المحور القصير: 1–2 صفحات +# - المحور المتوسط: 2–3 صفحات +# - المحور المتعمق: 3–5 صفحات + +# 5) تحقق إلزامي: + +# مجموع est_pages لكل Subsections +# = target_total_pages بالضبط + +# إذا لم يساوِ: +# قم بتعديل التوزيع حتى يتطابق المجموع. +# """ +# """ +# ============================== +# مراعاة حجم الكتاب +# ============================== + +# لا تقرر عدد المحاور أولًا. + +# ابدأ بتقدير حجم الكتاب: + +# - كتاب 60–100 صفحة → محاور أقل وشرح مركز +# - كتاب 100–180 صفحة → توازن بين عدد المحاور وعمق الشرح +# - كتاب 180+ صفحة → محاور أكثر وتفصيل أكبر + +# القاعدة الأساسية: + +# عدد الصفحات هو الذي يحدد عدد المحاور +# وليس العكس. +# """ +# "==============================\n" +# "الحفاظ على الأسماء\n" +# "==============================\n" +# """- استخدم عناوين Chapters وSections كما هي حرفيًا من raw_units. +# - يُمنع إعادة الصياغة أو التحسين اللغوي لعناوين Chapters/Sections. +# - التعديل/التحسين مسموح فقط داخل Subsections التي تنشئها. +# """ +# "==============================\n" +# "المدخلات\n" +# "==============================\n" +# "- book_title: {book_title}\n" +# "- target_audience: {target_audience}\n" +# "- book_description: {book_description}\n" +# "- learning_outcomes: {learning_outcomes}\n" +# "- raw_units: {raw_units}\n" +# "- target_total_pages (optional): {target_total_pages}\n" +# "- example_reference (optional): {example_reference}\n\n" +# """ملاحظة مهمة: +# raw_units تمثل الهيكل النهائي للفصول والأقسام. +# يُمنع تعديل أو إعادة تسمية أي Unit أو Topic. +# Subsections فقط هي التي يتم إنشاؤها وتنظيمها. +# """ +# "==============================\n" +# "مطلوب الإخراج (JSON صارم)\n" +# "==============================\n" +# "أخرج JSON فقط مطابق لـ BookOutlineOutput وبنفس أسماء الحقول:\n" +# "{\n" +# ' "book_title": "...",\n' +# ' "target_audience": "...",\n' +# ' "book_description": "...",\n' +# ' "chapters": [\n' +# " {\n" +# ' "chapter_title": "...",\n' +# ' "outcome": "...",\n' +# ' "sections": [\n' +# " {\n" +# ' "title": "...",\n' +# ' "overview": "...",\n' +# ' "subsections": [\n' +# " {\n" +# ' "title": "...",\n' +# ' "purpose": "...",\n' +# ' "suggested_queries": ["..."],\n' +# ' "difficulty": "basic|intermediate|advanced",\n' +# ' "est_pages": 1\n' +# " }\n" +# " ]\n" +# " }\n" +# " ]\n" +# " }\n" +# " ],\n" +# ' "outline_notes": "..."\n' +# ' "total_pages": 50\n' +# "}\n" +# "==============================\n" +# "تحقق إلزامي قبل الإخراج\n" +# "==============================\n" +# """ +# قبل إخراج الـ JSON النهائي، راجع ما يلي بدقة: +# - كل Chapter يحتوي 2–6 Sections +# - كل Section يحتوي على 2–6 Subsections (لا أقل من 2 نهائيًا) +# - إذا وجدت Section تحتوي فكرة واحدة فقط: +# → قم بتقسيمها إلى محورين منطقيين مختلفين +# - راجع عناوين Subsections: +# → لا تكرار في بدايات العناوين +# → لا عناوين جامدة/تقليدية +# → لا دمج (تعريف + أهمية) في عنوان واحد + +# 1) اجمع جميع est_pages في Subsections. +# 2) تحقق أن المجموع = target_total_pages. + +# إذا لم يتطابق: + +# قم بإعادة توزيع est_pages حتى يصبح المجموع مطابقًا تمامًا. +# - لا تخرج JSON حتى تتأكد أن جميع القيود محققة +# - أي مخالفة ستُعد إخراجًا غير صالح +# """ +# ), +# expected_output="Strict JSON matching BookOutlineOutput schema (NO extra keys).", +# output_json=BookOutlineOutput, +# agent=structure_agent, +# async_execution=False, +# ) +from crewai import Agent, Task +from modules import llm_4m +from schemas import BookOutlineOutput + + +# --------------------------------------------------- +# Agent: Book Structure Designer +# --------------------------------------------------- +def book_structure_agent(): + model = llm_4m() + + return Agent( + name="BookStructureDesigner", + role="مصمم هياكل كتب احترافية", + goal=( + "تصميم محاور فرعية جذابة داخل الهيكل المعطى للكتاب " + "مع الحفاظ الكامل على أسماء الفصول والأقسام كما هي " + "ودون تعديلها أو إعادة ترتيبها." + ), + backstory=( + "أنت محرر كتب محترف تعمل مع دور نشر كبيرة. " + "مهمتك تصميم المحاور الفرعية (Subsections) التي تجعل القارئ " + "يتنقل داخل القسم بسهولة وبفضول معرفي.\n\n" + "لا تكتب محتوى الكتاب. دورك هو تصميم الهيكل فقط.\n\n" + "تميزك الأساسي:\n" + "- صياغة عناوين جذابة وليست أكاديمية ثقيلة\n" + "- تقسيم الفكرة الكبيرة إلى محاور واضحة\n" + "- كل عنوان يمثل فكرة واحدة فقط\n" + "- تجنب التكرار والجمود في العناوين\n" + ), + llm=model, + verbose=False, + memory=False, + allow_delegation=False, + ) + + +# --------------------------------------------------- +# Task: Generate Book Outline +# --------------------------------------------------- +def generate_book_outline_task(structure_agent): + + return Task( + name="Generate book outline", + description=( + "مهمتك بناء هيكل كتاب (Book Outline) فقط.\n" + "لا تكتب محتوى فعلي للكتاب.\n\n" + "=========================\n" + "القواعد الأساسية\n" + "=========================\n" + "1) لا تقم بإنشاء Chapters جديدة\n" + "2) لا تقم بإنشاء Sections جديدة\n" + "3) لا تغيّر أسماء Chapters أو Sections\n" + "4) لا تعيد ترتيب الهيكل\n" + "5) دورك فقط إنشاء Subsections داخل كل Section\n\n" + "=========================\n" + "قواعد إنشاء Subsections\n" + "=========================\n" + "- كل Section يجب أن يحتوي 2 إلى 6 Subsections\n" + "- كل Subsection يمثل فكرة واحدة واضحة\n" + "- رتب المحاور بشكل منطقي بحيث تشكل رحلة للقارئ\n" + "أنماط تقسيم مناسبة:\n" + "- المفهوم → الفهم → التطبيق\n" + "- المشكلة → التحليل → الحل\n" + "- الفكرة → التحديات → الاستراتيجيات\n" + "- الأساسيات → الأدوات → الاستخدام\n\n" + "=========================\n" + "قواعد صياغة العناوين\n" + "=========================\n" + "العناوين يجب أن تكون:\n" + "- قصيرة (2 إلى 6 كلمات)\n" + "- جذابة وسهلة القراءة\n" + "- غير أكاديمية ثقيلة\n" + "- فكرة واحدة لكل عنوان\n" + "تجنب:\n" + "- تعريف + أهمية في نفس العنوان\n" + "- تكرار نفس بداية العنوان\n" + "- العناوين الجامدة\n\n" + "أمثلة لعناوين جيدة:\n" + "- لماذا تفشل الخطط؟\n" + "- لحظة اكتشاف المشكلة\n" + "- الفكرة التي تغير كل شيء\n" + "- عندما يتحول الضغط إلى فرصة\n\n" + "=========================\n" + "المدخلات\n" + "=========================\n" + "book_title: {book_title}\n" + "target_audience: {target_audience}\n" + "book_description: {book_description}\n" + "learning_outcomes: {learning_outcomes}\n" + "raw_units: {raw_units}\n" + "target_total_pages: {target_total_pages}\n\n" + "example_reference (استلهم منه أسلوب العناوين فقط):\n" + "{example_reference}\n\n" + "raw_units تمثل الهيكل النهائي للفصول والأقسام.\n" + "يُمنع تعديلها.\n" + "قم فقط بإضافة Subsections داخل كل Section.\n\n" + "=========================\n" + "توزيع الصفحات\n" + "=========================\n" + "إذا تم تمرير target_total_pages:\n" + "- وزع الصفحات على Subsections\n" + "- المحور القصير: 1-2 صفحات\n" + "- المحور المتوسط: 2-3 صفحات\n" + "- المحور العميق: 3-5 صفحات\n" + "مجموع الصفحات يجب أن يساوي target_total_pages.\n\n" + "=========================\n" + "الإخراج المطلوب\n" + "=========================\n" + "أخرج JSON فقط مطابق لـ BookOutlineOutput.\n" + "{\n" + ' "book_title": "...",\n' + ' "target_audience": "...",\n' + ' "book_description": "...",\n' + ' "chapters": [\n' + " {\n" + ' "chapter_title": "...",\n' + ' "outcome": "...",\n' + ' "sections": [\n' + " {\n" + ' "title": "...",\n' + ' "overview": "...",\n' + ' "subsections": [\n' + " {\n" + ' "title": "...",\n' + ' "purpose": "...",\n' + ' "suggested_queries": ["..."],\n' + ' "difficulty": "basic|intermediate|advanced",\n' + ' "est_pages": 1\n' + " }\n" + " ]\n" + " }\n" + " ]\n" + " }\n" + " ],\n" + ' "outline_notes": "...",\n' + ' "total_pages": 50\n' + "}\n\n" + "لا تضف أي نص خارج JSON." + ), + expected_output="Strict JSON matching BookOutlineOutput schema", + output_json=BookOutlineOutput, + agent=structure_agent, + async_execution=False, + ) diff --git a/agents/books/enrichment.py b/agents/books/enrichment.py new file mode 100644 index 0000000000000000000000000000000000000000..cbd4101e0be5e7e539a1ef2bce9682ddb3a8f5a6 --- /dev/null +++ b/agents/books/enrichment.py @@ -0,0 +1,140 @@ +from crewai import Agent, Task, Crew, Process +import os +import json +from modules import llm_open_grok +from schemas.books.enrichment import EnrichedContentOutput +def create_enrichment_agent(): + model=llm_open_grok() + return Agent( + name="ArabicNarrativeEnrichmentAgent", + role="Arabic Cognitive Content Enricher", + goal=( + "إثراء المحتوى العربي الأكاديمي بفواصل معرفية إنسانية عميقة، " + "تُدمج داخل النص بسلاسة دون كسر أسلوب سرد المحتوى. " + "تعمل هذه الفواصل على تعميق الفهم، وتحفيز التفكير، وربط المفاهيم النظرية بالواقع الإنساني، " + "تشمل: اقتباسات ملهمة، قصص واقعية، دراسات حالة، إحصائيات معرفية، " + "ونصائح عملية - كلها مدمجة بشكل طبيعي وذكي داخل المحتوى " + "يجب أن تبدو الإضافات كأنها من بنية النص الأصلية، " + "لا كإضافات خارجية أو مولدة آليًا." + ), + backstory=( + "أنت محرر معرفي عربي متخصص في صناعة المحتوى العميق غير المباشر. " + "عملك الأساسي هو تحويل النصوص الأكاديمية الجافة إلى نصوص حيّة نابضة بالفهم الإنساني، " + "من خلال إدخال فواصل تحليلية ذكية داخل السياق، " + "تشبه أسلوب كبار المفكرين العرب في الربط بين النظرية والواقع. " + "تمتلك خبرة واسعة في علم النفس التربوي، والقيادة، والتنمية البشرية، " + "وتجيد توظيف الأمثلة الواقعية والتاريخية دون تهويل أو إنشائية. " + "أسلوبك يشبه الكاتب البشري الخبير الذي يكتب بهدوء وعمق، " + "لا يستخدم لغة دعائية أو تحفيزية سطحية، " + "تحرص على أن تكون كل فاصلة ذات قيمة حقيقية، " + "تتجنب الاقتباسات المكررة أو القصص المستهلكة، " + "ولا يخرج عن روح النص الأصلي. " + "كل إضافة تقوم بها يجب أن تخدم الفكرة المركزية للمحتوى، " + "وتُشعر القارئ أنه اكتسب بعدًا فكريًا جديدًا، لا مجرد قصة جانبية." + ), + constraints=( + "1) يمنع منعًا باتًا استخدام لغة تحفيزية مباشرة أو شعاراتية.\n" + "2) يمنع استخدام عبارات مثل: في الختام، لا شك أن، من المهم جدًا.\n" + "3) كل فاصل يجب أن يخدم فكرة محددة سبقتْه مباشرة.\n" + "4) لا يُسمح بتكرار نفس الفكرة بصيغة أخرى.\n" + "5) الفاصل يجب أن يضيف بعدًا تطبيقيًا أو إنسانيًا جديدًا.\n" + "6) الأسلوب لغوي بشري هادئ، تحليلي، غير متكلف.\n" + "7) يمنع استخدام أسلوب الوعظ أو الخطابة.\n" + "8) يجب أن يبدو الفاصل كامتداد طبيعي للنص، لا كفقرة دخيلة.\n" + ), + llm=model, + verbose=False, + memory=False, + allow_delegation=False, + ) + + +def create_enrichment_task(enrichment_agent): + return Task( + description=( + "يتم تزويدك بمحتوى عربي أكاديمي موثوق مستخرج من كتاب\n" + "ومطلوب منك إثراؤه بفواصل إبداعية مبتكرة تجعله أكثر جاذبية وتأثيراً." + "دون إعادة كتابة المحتوى الأصلي.\n\n" + "=================================================\n" + " قواعد التنفيذ\n" + "=================================================\n" + " - الفاصلة يجب أن تأتي في السياق المناسب تماماً\n" + " - لا تقطع تسلسل الأفكار\n" + " - تُضاف بعد شرح المفهوم، ليس قبله\n" + " - تكون كتأكيد أو إثراء لما سبق\n\n" + "2) **الظهور المميز**:\n" + " - الاقتباسات: بين علامات تنصيص وفي سطر منفصل\n" + " - القصص: لها عنوان فرعي مميز بخط عريض\n" + " - مسموح بوجود فاصلتين وراء بعض اذا كانت لهم اضافة مميزه وقويه" + " - مسافة بصرية قبل وبعد كل فاصلة\n\n" + "3) **التنوع**:\n" + " - لا تستخدم نفس النوع مرتين متتاليتين\n" + " - نوّع بين القصص والاقتباسات والإحصائيات وباقي انواع الفصلات مسموح ايضا بالابتكار\n" + "4) **الأصالة**:\n" + " - تجنب الاقتباسات المكررة والمستهلكة\n" + " - كن مبدعاً في الصياغة\n\n" + "5) **الارتباط القوي بالموضوع**:\n" + " - كل فاصلة يجب أن تخدم المحور مباشرة\n" + # " - لا مجال للحشو أو الفواصل العامة\n" + " - القصة أو الاقتباس يجب أن يعمق الفهم\n\n" + "=================================================\n" + " شكل الفواصل\n" + "=================================================\n" + "الفاصل يجب أن يبدو كاستطراد ذكي من الكاتب نفسه:\n" + "1️⃣ **اقتباسات ملهمة ومعرفية**\n" + " - يمكن أن تكون:\n" + " * حكمة عربية أصيلة\n" + " * اقتباس من خبير أو مفكر معروف\n" + " * إحصائية مدهشة\n" + " * حقيقة علمية مثيرة\n" + " مثال: {ref1}\n\n" + "2️⃣ **قصص واقعية ملهمة**\n" + " - قصة قصيرة (3-6 فقرات)\n" + " - لها عنوان فرعي واضح\n" + " - تحكي عن تجربة حقيقية لشخص أو مؤسسة معروفة\n" + " - مثال: {ref2}\n\n" + "3️⃣ **دراسات حالة قصيرة**\n" + " - مثال تطبيقي من واقع الحياة\n" + " - يمكن أن تكون من:\n" + " * شركات ناجحة\n" + " * تجارب تعليمية\n" + " * مواقف اجتماعية\n" + " * أبحاث علمية مبسطة\n" + " - مثال: {ref3}\n\n" + "4️⃣ **نصائح عملية مميزة**\n" + " - نقاط إرشادية سريعة\n" + " - يمكن وضعها في إطار مميز\n" + # " - مثال: {ref4}\n\n" + "5️⃣ **إضاءات معرفية**\n" + " - معلومة مثيرة أو غير متوقعة\n" + " - حقيقة علمية مدهشة\n" + " - إحصائية مفاجئة\n\n" + " - مثال: {ref5}\n\n" + "يجب ذكر المصادر أو المرجع لكل إضافة بشكل واضح في نهاية الاضاءة، مع تضمين اسم المصدر وتاريخ الوصول إذا كان من الإنترنت.\n\n" + "=================================================\n" + "INPUT\n" + "=================================================\n" + "{generated_content}\n\n" + "audiance: {target_audiance}" + "=================================================\n" + "OUTPUT\n" + "=================================================\n" + "أعد نفس النص بعد إدخال الفواصل الإثرائية داخله، " + "مع الحفاظ الكامل على بنية المحتوى الأصلية." + ), + agent=enrichment_agent, + expected_output="A valid JSON following EnrichedContentOutput schema", + output_json=EnrichedContentOutput, + # output_file=os.path.join(output_dir, "enriched_content.json"), + ) + +def make_enrichment_crew(): + enrichment_agent = create_enrichment_agent() + enrichment_task = create_enrichment_task(enrichment_agent) + + return Crew( + agents=[enrichment_agent], + tasks=[enrichment_task], + process=Process.sequential, + verbose=False + ) diff --git a/agents/books/intro_plan.py b/agents/books/intro_plan.py new file mode 100644 index 0000000000000000000000000000000000000000..9c18f19b5d440682fbf4f7dd14136dbe7d8c99db --- /dev/null +++ b/agents/books/intro_plan.py @@ -0,0 +1,51 @@ +from crewai import Agent, Task +import os +import json +from modules import llm_g +from schemas import IntroPlan + +def intro_style_planner(): + model = llm_g() + return Agent( + role="Book Introduction Style Planner", + goal=( + "Design the MOST SUITABLE and VARIED introduction style for each chapter in a book. " + "You must ensure stylistic diversity across chapters and avoid repetition of intro approaches." + ), + backstory=( + "You are a senior book editor and literary architect. " + "You design how each chapter should open so the reader experiences emotional rhythm and variety. " + "You avoid monotony by alternating narrative, reflective, conceptual, and atmospheric openings. " + "You NEVER assign the same intro_function or tone repeatedly across consecutive chapters." + ), + llm=model, + verbose=True, + memory=False, +) + +def intro_style_planner_task(intro_style_planner_agent): + return Task( + description=( + "You are given a FULL BOOK PLAN containing ALL chapters.\n\n" + "Your job is NOT to analyze text.\n" + "Your job is to DESIGN the most suitable introduction style for EACH chapter.\n\n" + "### Goals:\n" + "1) Each chapter must receive a StyleOutput profile.\n" + "2) Styles MUST fit the chapter topic and emotional intent.\n" + "3) Styles MUST NOT repeat across chapters.\n" + "4) Consecutive chapters MUST NOT share the same intro_function.\n" + "5) The book must feel rhythmically varied (narrative → reflective → conceptual → atmospheric → etc).\n\n" + "### Diversity Rules (HARD CONSTRAINTS):\n" + "- Do NOT repeat the same intro_function more than once.\n" + "- Do NOT reuse identical tone or sentence_structure in adjacent chapters.\n" + "- Vary opening strategies: story, scene, question, metaphor, reflection, contrast.\n\n" + "### Output:\n" + "Return ONE JSON object for the whole book using IntroStylePlan schema.\n\n" + "Input book chapters:\n" + "{book_plan}" + ), + agent=intro_style_planner_agent, + expected_output="A valid JSON following IntroPlan schema with inferred intro-writing rules", + output_json=IntroPlan, + # output_file=os.path.join(output_dir, "style_output_2.json"), + ) diff --git a/agents/books/queries_agent.py b/agents/books/queries_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..6afb1748c582757b884da85cf7f37211224b86f9 --- /dev/null +++ b/agents/books/queries_agent.py @@ -0,0 +1,232 @@ +# from crewai import Agent, Task +# import os +# import json +# from modules import llm_4m +# from schemas import BookOutlineWithQueries + +# def query_agent(): +# model = llm_4m() +# return Agent( +# name="BookSourceQueryAgent", +# role="Generate production-ready source search queries for each subsection", +# goal=( +# "For each subsection in the book outline JSON, generate 2–3 short, precise search queries " +# "that are likely to retrieve trustworthy sources suitable for APA 7 citations. " +# "Sources may include academic papers, theses, institutional PDFs, and high-quality professional books." +# ), +# backstory=( +# "You are a research strategist who designs search queries optimized for sourcing credible references " +# "usable for in-text citations and reference lists. You know how to bias search results toward PDFs, " +# "books, theses, and institutional publications and avoid blogs, social media, marketing pages, and courses." +# ), +# verbose=False, +# llm=model, +# ) + + +# def query_task(query_agent): +# return Task( +# name="Generate academic/professional source queries per subsection", +# description=( +# "You will receive a structured BOOK JSON (chapters → sections → subsections). " +# # "For EACH subsection, generate 2–3 concise and diverse search queries in Arabic and/or English.\n\n" +# "For EACH subsection, generate 2–3 concise and diverse search queries in Arabic.\n\n" +# "=================================================\n" +# "📌 MANDATORY QUERY RULES (DO NOT VIOLATE)\n" +# "=================================================\n" +# "1) Each query MUST clearly target credible sources such as:\n" +# " - Books (professional or academic)\n" +# " - PDF documents\n" +# " - Research papers / academic studies\n" +# " - Theses / dissertations\n" +# " - Institutional publications (e.g., APA, WHO, universities)\n\n" +# "2) Prefer adding intent keywords when helpful:\n" +# " - Arabic: (PDF، كتاب، دراسة، بحث علمي، رسالة ماجستير)\n" +# # " - English: (PDF, book, research paper, academic study, thesis)\n\n" +# "3) Avoid queries that could return:\n" +# " - Blogs\n" +# " - Online courses\n" +# " - Marketing articles\n" +# " - Social media content\n" +# " - Wikipedia-like summaries\n\n" +# "4) Queries must be:\n" +# " - Short\n" +# " - Specific\n" +# " - Directly related to the subsection topic\n\n" +# "5) Do NOT repeat the same phrasing across queries. Each query must use a different angle:\n" +# " Example angles: definition + theory, measurement scale + instrument, meta-analysis + review, book + author.\n\n" +# "6) At least ONE query per subsection MUST explicitly target BOOKS.\n\n" +# "=================================================\n" +# "📌 INPUT\n" +# "=================================================\n" +# "Book JSON:\n" +# "{book}\n\n" +# "=================================================\n" +# "📌 OUTPUT REQUIREMENTS\n" +# "=================================================\n" +# "Return ONLY a valid JSON that keeps the SAME input structure. \n" +# "=========================\n" +# "الإخراج المطلوب\n" +# "=========================\n" +# "أخرج JSON فقط مطابق لـ BookOutlineWithQueries.\n" +# "{\n" +# ' "book_title": "...",\n' +# ' "target_audience": "...",\n' +# ' "book_description": "...",\n' +# ' "chapters": [\n' +# " {\n" +# ' "chapter_title": "...",\n' +# ' "outcome": "...",\n' +# ' "sections": [\n' +# " {\n" +# ' "title": "...",\n' +# ' "overview": "...",\n' +# ' "subsections": [\n' +# " {\n" +# ' "title": "...",\n' +# ' "purpose": "...",\n' +# ' "suggested_queries": ["..."],\n' +# ' "academic_queries": ["..."],\n' +# ' "difficulty": "basic|intermediate|advanced",\n' +# ' "est_pages": 1\n' +# " }\n" +# " ]\n" +# " }\n" +# " ]\n" +# " }\n" +# " ],\n" +# ' "outline_notes": "...",\n' +# ' "total_pages": 50\n' +# "}\n\n" +# "For each subsection, ADD a new field exactly named: academic_queries (2–3 items).\n" +# "Do NOT remove or rename any existing fields.\n" +# "Do NOT add explanations, comments, or extra text." +# "Follow the same structure of BookOutlineWithQueries Schema" +# "Return ONLY ONE JSON object." +# "Do NOT return multiple JSON objects." +# "Do NOT use tool calls." +# "Do NOT wrap the output." +# ), +# expected_output="A valid JSON following BookOutlineWithQueries schema", +# output_json=BookOutlineWithQueries, +# # output_file="book_with_academic_queries.json", +# agent=query_agent, +# ) + +from crewai import Agent, Task +import os +import json +from schemas.books.queries_schema import QueriesOutput +from modules import llm_4m + + + +# ========================================== +# Agent +# ========================================== + + +def query_agent(): + model = llm_4m() + + return Agent( + name="BookSourceQueryAgent", + role="Academic Search Query Generator", + goal=( + "Generate three high-quality academic search queries for each subsection " + "in order to retrieve credible sources suitable for APA 7 citations." + ), + backstory=( + "You are a professional research strategist specialized in designing " + "search queries that retrieve high-quality academic sources such as:\n" + "- scholarly books\n" + "- research papers\n" + "- theses and dissertations\n" + "- institutional publications\n" + "- scientific PDFs\n\n" + "You avoid low-quality sources such as blogs, marketing pages, " + "courses, social media posts, and generic summaries." + ), + verbose=False, + llm=model, + ) + + +# ========================================== +# Task +# ========================================== + + +def query_task(query_agent): + return Task( + name="Generate academic queries per subsection", + description=( + "You will receive a BOOK JSON that contains a hierarchy:\n" + "chapters → sections → subsections.\n\n" + "Your job is to generate academic search queries for EACH subsection.\n\n" + "=================================================\n" + "MANDATORY RULES\n" + "=================================================\n" + "1. For EACH subsection generate EXACTLY 3 queries.\n\n" + "2. Queries must target credible sources such as:\n" + " - Academic books\n" + " - Research papers\n" + " - Theses / dissertations\n" + " - Institutional publications\n" + " - Scientific PDF documents\n\n" + "3. Queries should be written primarily in Arabic.\n\n" + "4. Queries must be:\n" + " - Short\n" + " - Specific\n" + " - Directly related to the subsection topic\n\n" + "5. Prefer adding academic intent keywords when useful:\n" + " (PDF، كتاب، دراسة، بحث علمي، رسالة ماجستير)\n\n" + "6. At least ONE query per subsection MUST target BOOKS.\n\n" + "7. Avoid queries likely to return:\n" + " - Blogs\n" + " - Online courses\n" + " - Marketing articles\n" + " - Social media\n" + " - Wikipedia-like summaries\n\n" + "=================================================\n" + "INPUT\n" + "=================================================\n" + "Book JSON:\n" + "{book}\n\n" + "=================================================\n" + "OUTPUT FORMAT (STRICT)\n" + "=================================================\n" + "Return ONLY a JSON object with the following structure:\n\n" + "{\n" + ' "queries": [\n' + " {\n" + ' "subsection": "Subsection Title",\n' + ' "queries": [\n' + ' "query 1",\n' + ' "query 2",\n' + ' "query 3"\n' + " ]\n" + " }\n" + " ]\n" + "}\n\n" + "=================================================\n" + "CRITICAL OUTPUT RULES\n" + "=================================================\n" + "• Include EVERY subsection from the input.\n" + "• The number of objects in 'queries' MUST equal the number of subsections.\n" + "• Each subsection MUST contain EXACTLY 3 queries.\n" + "• Preserve the original subsection titles exactly.\n" + "• Do NOT include chapters or sections in the output.\n" + "• Do NOT add explanations, comments, or text outside JSON.\n" + "• Output must be valid JSON only." + ), + expected_output=( + "{" + '"queries":[' + '{"subsection":"Example subsection","queries":["q1","q2","q3"]}' + "]" + "}" + ), + agent=query_agent, + output_json=QueriesOutput, + ) diff --git a/agents/books/reference_lang.py b/agents/books/reference_lang.py new file mode 100644 index 0000000000000000000000000000000000000000..5bf43432d57178338c41d39f5f476b6c17cab386 --- /dev/null +++ b/agents/books/reference_lang.py @@ -0,0 +1,252 @@ +from langchain.messages import SystemMessage, HumanMessage +from modules import model_open_router +import os +import re + +# ✅ System Prompt optimized (بديل الدليل الضخم) +SYSTEM_PROMPT = """ +You are an expert in APA 7th edition reference formatting. + +STRICT RULES: + +- Output ONLY the formatted reference (no explanations). +- Each reference must be on a new line. +- Do NOT invent missing data. +- Use (n.d.) if date is missing. +- Keep original language (Arabic stays Arabic, English stays English). + +GENERAL FORMAT RULES: + +1. Author: + - Format: Last name, Initial(s). + - Multiple authors: + - 2 authors: Author1, A. A., & Author2, B. B. + - 3+ authors: Author1, A. A., et al. + +2. Year: + - Must be in parentheses: (2023) + +3. Title: + - Sentence case (only first word capitalized in English) + - Italicize book/journal titles conceptually + +4. Books: + Author. (Year). Title (Edition). City: Publisher. + +5. Journal articles: + Author. (Year). Title. Journal Name, Volume(Issue), pages. + +6. Conference papers: + Author. (Year). Title. Conference Name, Location, Date. + +7. Theses: + Author. (Year). Title [Thesis type], Institution. + +8. Websites: + - With author: + Author. (Year). Title. URL + - Without author: + Title. (Year). URL + +9. Arabic references: + - Keep Arabic format + - Use same structure: + المؤلف. (السنة). العنوان. المدينة: الناشر. + +10. Page numbers: + - Use pp. for English + - Use ص for Arabic + +11. Missing data: + - Use (n.d.) + - Use [Missing Info] only if critical + +12. Fix: + - spelling + - punctuation + - ordering + +OUTPUT: +- Clean APA formatted references only +- No bullets +- No numbering +""" + + +# ✅ تحسين: batching أصغر + fallback +def normalize_references_batch(refs_list, model,batch_size=2): + batches = [] + + for i in range(0, len(refs_list), batch_size): + chunk = refs_list[i : i + batch_size] + batches.append("\n".join(chunk)) + + results = [] + + for batch in batches: + try: + messages = [ + SystemMessage(content=SYSTEM_PROMPT), + HumanMessage(content=f"Format in APA 7:\n{batch}"), + ] + + response = model.invoke(messages) + + formatted = [ + line.strip() for line in response.content.split("\n") if line.strip() + ] + + results.extend(formatted) + + except Exception as e: + print(f"Error in batch: {e}") + + # fallback: رجعهم زي ما هم + results.extend(batch.split("\n")) + + return results + + +# ✅ استخراج المراجع +def extract_references(book_json): + refs = [] + + for chapter in book_json.get("chapters", []): + for section in chapter.get("sections", []): + for plan in section.get("plans", {}).values(): + for chunk in plan.get("rag_context", {}).get("chunks", []): + ref = chunk.get("reference_apa") + if ref: + refs.append(ref) + + book_name = book_json.get("book_title", "") + + return refs, book_name + + +# ✅ إزالة التكرار +def remove_duplicates(refs): + return list(dict.fromkeys(refs)) + + +# ✅ HTML cleaner +def save_refs_to_html(refs_list, book_name): + """ + تحويل قائمة المراجع إلى ملف HTML منسق أكاديمياً مع اسم الكتاب + """ + html_content = f""" + + + + + + قائمة المراجع - APA 7th Edition + + + +
+
{book_name}
+

قائمة المراجع (APA 7th Edition)

+
    + """ + + for ref in refs_list: + import re + + url_pattern = r"(https?://\S+)" + formatted_ref = re.sub( + url_pattern, r'
    \1', ref + ) + + html_content += f'
  • {formatted_ref}
  • \n' + + html_content += """ +
+
+ + + """ + + return html_content + + +# ✅ Main pipeline +def run_references(book_json): + refs, book_name = extract_references(book_json) + print(f"Extracted: {len(refs)}") + + refs = remove_duplicates(refs) + print(f"Unique: {len(refs)}") + + # ✅ Model (يفضل موديل أسرع) + model = model_open_router() + fixed = normalize_references_batch(refs, model) + print(f"Formatted: {len(fixed)}") + + html = save_refs_to_html(fixed, book_name) + + return html diff --git a/agents/books/units_intro.py b/agents/books/units_intro.py new file mode 100644 index 0000000000000000000000000000000000000000..f8f44aaa2b7d13071bf729e7e26672d7027f0f91 --- /dev/null +++ b/agents/books/units_intro.py @@ -0,0 +1,457 @@ +from crewai import Agent, Task +import os +import json +from modules import llm_open_grok, intro_reference1 +from schemas import ChapterIntro + +def intro_generator(): + model = llm_open_grok() + return Agent( + role="Metadata-Driven Chapter Introduction Writer", + + goal=( + "Write a stylistically precise and pedagogically aligned introduction " + "for chapter using ONLY the provided metadata and intro style. " + "The introduction must feel native to the book and strictly follow " + "the chapter’s declared structure, scope, and writing profile." + ), + backstory=( + + "=============================\n" + "CORE IDENTITY\n" + "=============================\n" + "You are a professional Arabic book author.\n" + "You write natural, fluid, elegant prose.\n" + "You NEVER sound robotic, or template-based.\n" + "You NEVER describe what the chapter will do.\n" + + + "=============================\n" + "LAYER 1 — HARD CONSTRAINTS (CANNOT BE VIOLATED)\n" + "=============================\n" + + "❌ NEVER start with a definition.\n" + "❌ NEVER summarize the chapter structure.\n" + "❌ NEVER list components or sections.\n" + "❌ NEVER say: هذا الفصل يتناول / يستعرض / يقدّم / يناقش\n" + "❌ NEVER mention audience (طلاب، قارئ، متدرب، موظف...)\n" + "❌ NEVER use research claims without attribution.\n" + "❌ NEVER use research terminology.\n" + "❌ NEVER use direct imagination commands:\n" + "تخيّل، تخيّلي، تخيّلوا، لنتخيّل، دعنا نتخيّل\n" + "If needed:\n" + '→ Describe the scene as if already unfolding\n' + '→ Use implicit cinematic narration, not commands\n\n' + + "FORBIDDEN RESEARCH LANGUAGE:\n" + "آليات، أدوات قياس، نظريات، نماذج، استبيان، تشخيص، إجراءات، ممارسات، خصائص، أبعاد، إطار نظري\n\n" + + "FORBIDDEN PHRASES:\n" + "في ظل، من خلال، يُعدّ، يبرز، يتجلى، يُسلّط الضوء، تشير الدراسات، أظهرت الأبحاث\n" + "If any appear → REWRITE.\n\n" + + "You MUST apply Arabic logical punctuation rules:\n" + + "1) Use SEMICOLON (؛) before:\n" + " • Explanation after generalization\n" + " • Clarification of a previous idea\n" + " • Sentences starting with:\n" + " حيث / إذ / لأن / مما + فعل مضارع / بالتالي / وبالتالي\n" + + "2) NEVER join cause–effect or general–specific ideas with a comma only.\n" + " If logic exists → punctuation MUST signal it.\n" + + "If logical punctuation is missing → REWRITE THE PARAGRAPH.\n" + + + + "=============================\n" + "LAYER 2 — OPENING RULE (MANDATORY)\n" + "=============================\n" + + "The FIRST sentence MUST hook emotionally or intellectually use the intro style given to choose using ONE of:\n" + "• question\n" + "• short scene\n" + "• realistic moment\n" + "• problem\n" + "• striking observation\n" + ". others\n" + + + "=============================\n" + "LAYER 3 — STYLE RULES\n" + "=============================\n" + + "Write like a human author:\n" + "• warm\n" + "• vivid\n" + # "• narrative\n" + "• concept-driven\n" + "Show ideas instead of defining them.\n" + "Avoid textbook tone.\n" + "Avoid definitive explanatory closures:\n" + "- ليس X بل Y\n" + "- هو في جوهره…\n" + "- يتمثل في…\n" + "Prefer:\n" + "• tension\n" + "• contrast\n" + "• unfolding meaning\n" + "• leaving conceptual space open\n\n" + + '=============================\n' + 'LAYER 3.5 — ANTI-AI & ANTI-TEMPLATE RULES (CRITICAL)\n' + '=============================\n' + + 'You MUST actively break Arabic AI-style patterns.\n' + + '1) DIRECT MENTAL COMMANDS — FORBIDDEN\n' + '❌ Never use verbs that push the reader outside the text:\n' + 'تخيّل / تأمّل / فكّر / لاحظ / انتبه\n' + + 'Instead:\n' + '• Convert the idea into a visible action, scene, or unfolding event.\n' + '• Write as if the moment is already happening, not requested.\n\n' + + '2) VAGUE OVERUSED NOUNS — FORBIDDEN UNLESS NECESSARY\n' + '❌ Avoid hollow abstractions that produce no image:\n' + 'عالم / إطار / أفق / سياق / منظومة / مشهد عام\n' + + 'Rule:\n' + '• If removing the word does not change meaning → DELETE it.\n' + '• Otherwise replace it with:\n' + ' - a concrete action\n' + ' - a specific situation\n' + ' - a lived position or tension\n\n' + + '3) READY-MADE CONTRAST STRUCTURES — FORBIDDEN\n' + '❌ Never use:\n' + 'ليس … بل …\n' + 'لا يقتصر على … وإنما …\n' + + 'Instead:\n' + '• Split the idea into two sentences.\n' + '• Let meaning escalate progressively instead of rhetorical opposition.\n\n' + + '4) EXPLANATORY CONNECTORS — RESTRICTED\n' + '❌ Avoid turning prose into a report using:\n' + 'إذ / مما / حيث / وهذا يعني / مما يؤدي إلى\n' + + 'Allowed only if:\n' + '• Logical punctuation (؛ :) is used correctly\n' + '• OR the sentence is rewritten as an independent action\n' + + 'Prefer:\n' + '• Embedded causality through narrative flow\n' + '• Action revealing consequence without explanation\n\n' + + '5) QUESTION + EXPLANATION PATTERN — FORBIDDEN\n' + '❌ Never ask a question then neutralize it with:\n' + 'هذا السؤال يكشف / يوضح / يعكس\n' + + 'Instead:\n' + '• Let the answer unfold through example, tension, or movement.\n' + '• The question must open space, not close it.\n\n' + + '6) FILLER TRANSITIONS — FORBIDDEN BY DEFAULT\n' + '❌ Avoid empty connectors:\n' + 'في هذا السياق / ومن ثم / علاوة على ذلك / في ضوء ذلك\n' + + 'Rule:\n' + '• Delete by default.\n' + '• Replace only with real:\n' + ' - temporal movement\n' + ' - causal pressure\n' + ' - conceptual shift\n\n' + + '=============================\n' + 'LAYER 3.6 — CONTEXT CONTINUITY RULE (CRITICAL)\n' + '=============================\n' + + 'You MUST maintain semantic pressure across sentences.\n' + + 'RULE 1 — NO ABSTRACT JUMP\n' + '❌ Never jump from:\n' + '• concrete challenge → abstract wisdom\n' + '• example → slogan\n' + '• problem → generalized conclusion\n' + + 'Unless:\n' + '• the abstract idea is a DIRECT consequence of the previous sentence\n' + '• and the causality is FELT, not declared\n\n' + + 'RULE 2 — TRANSITION VALIDATION\n' + 'Before using any transition word (و، لكن، ومع ذلك، وأمام، هكذا):\n' + + 'Ask internally:\n' + '"Does the previous sentence FORCE this next sentence?"\n' + + 'If the answer is NO → REWRITE BOTH.\n\n' + + 'RULE 3 — FORBIDDEN SAFE LANDINGS\n' + '❌ Avoid sentences that sound like safe endings:\n' + '• تكمن القوة في…\n' + '• هكذا يصبح…\n' + '• من هنا تبدأ…\n' + '• الاستكشاف هذا يعد…\n' + + 'These phrases CLOSE meaning instead of advancing it.\n' + + 'Prefer:\n' + '• unresolved momentum\n' + '• forward pressure\n' + '• conceptual instability\n' + + + "=============================\n" + "LAYER 4 — STRUCTURE LOGIC\n" + "=============================\n" + + "Paragraph 1 → hook + lived situation\n" + "Middle → deepen the concept philosophically/psychologically\n" + "Ending → create forward momentum (not summary)\n" + "• Every paragraph MUST grow from the previous one.\n" + "• Any conceptual shift MUST include:\n" + " - a causal connector (فـ / إذ / لذلك / ومن ثم)\n" + " - OR a logical punctuation mark (: ؛)\n\n" + + "❌ Forbidden:\n" + "- Sudden conclusions without transition\n" + "- Isolated sentences that could stand alone\n" + + "If a sentence can be removed without breaking meaning → REWRITE.\n" + + + "=============================\n" + "LAYER 5 — LANGUAGE QUALITY\n" + "=============================\n" + + "Arabic must be:\n" + "• grammatically perfect\n" + "• spelling perfect\n" + "• fluid and natural\n" + "• no repetition\n\n" + + "=============================\n" + "WHY_THIS_CHAPTER_NOW — SCOPE RULE (CRITICAL)\n" + "=============================\n" + + 'The field "why_this_chapter_now" serves ONLY as:\n' + '• a temporal justification\n' + '• a motivational background\n' + '• NOT a narrative scope\n\n' + + 'Rules:\n' + '1) It MUST NOT dictate geographical focus.\n' + '2) Any local reference (country, institution, region) is:\n' + ' - CONTEXTUAL\n' + ' - NOT foundational\n' + '3) Default writing scope is ALWAYS:\n' + ' • general\n' + ' • conceptual\n' + ' • globally applicable\n\n' + + 'Local contexts mentioned in "why_this_chapter_now":\n' + '• MAY be abstracted\n' + '• MAY be generalized\n' + '• MUST NOT dominate the introduction\n\n' + + + "=============================\n" + "SELF-REVISION LOOP (MANDATORY)\n" + "=============================\n" + + "After writing:\n" + "1) Scan for banned words\n" + "2) Remove any list or enumeration\n" + "3) Remove academic tone\n" + "4) Fix grammar\n" + "5) Rewrite any sentence that sounds generic\n\n" + + '6) Scan specifically for:\n' + ' - mental verbs\n' + ' - vague abstractions\n' + ' - rhetorical contrasts\n' + ' - filler transitions\n' + 'If found → rewrite the entire paragraph, not just the sentence.\n\n' + + "QUESTION HANDLING RULE (CRITICAL)\n" + "If a paragraph ends with a question:\n" + "1) The following sentence MUST do ONE of:\n" + " • Reframe the question conceptually\n" + " • Signal that the answer unfolds ahead (implicitly, not structurally)\n" + " • Provide a philosophical hinge\n\n" + + "2) NEVER follow a question with:\n" + " • a broken nominal sentence\n" + " • vague commentary\n" + " • unclear subject reference\n" + + "3) Preferred patterns:\n" + ' • "الإجابة على هذا السؤال تكمن في..."\n' + ' • "هذا السؤال يفتح بابًا نحو..."\n' + ' • "من هنا تبدأ الإشكالية الحقيقية..."\n\n' + + 'If the post-question sentence is weak → REWRITE BOTH.\n\n' + + + "Output ONLY the final introduction text." + ), + examples=[intro_reference1], + llm=model, + memory=True, + allow_delegation=False, + verbose=True +) + +def intro_generation_task(intro_generator_agent): + return Task( + description=( + "You are given structured chapters metadata:\n({chapter_metadata})\n andfocus on intros style for each chapter:\n({intro_style})\n\n" + "CRITICAL WRITING RULE — Opening Diversity:\n" + "Even if the tone is academic or formal," + "MANDATORY HOOK:\n" + "The first sentence MUST be engaging (question, story, scenario, or problem).\n" + "Never start with a definition.\n\n" + "AI-STYLE BAN:\n" + "Never use cliché academic phrases or repeated textbook templates.\n" + "If the sentence feels predictable, rewrite it.\n\n" + "BANNED VERBS (in all conjugations):\n" + "❌ يبرز / تبرز / يُبرز / نبرز / برز / بارز\n" + "❌ يتجلى / تتجلى / تجلّى / متجلّي\n" + "❌ يُعدّ / تُعدّ / عُدّ\n" + "❌ يُسلّط الضوء / نسلّط\n" + "❌ يغوص / نغوص\n" + "❌ يمتدّ النقاش / يمتدّ\n\n" + "BANNED META-DESCRIPTIONS:\n" + "❌ هذا الفصل + any verb (يبرز، يقدّم، يستعرض، يتناول، يناقش)\n" + "❌ نرى فيه / نُلاحظ هنا / نُتَعَرَّضُ هنا\n" + "❌ يدعونا هذا + anything\n" + "❌ من هنا، يصبح...\n\n" + "BANNED TRANSITIONS:\n" + "❌ من خلال (find alternatives: بفضل، عبر، بواسطة، or restructure)\n" + "❌ في ظل\n" + "❌ عبر استكشاف\n\n" + "**WHY THESE ARE BANNED:**\n" + "These phrases describe what THE CHAPTER DOES.\n" + "The introduction should describe THE CONCEPT, not the chapter's actions.\n\n" + "NO DIRECT ADDRESS:\n" + "Do not explicitly mention the audience (طلاب، متعلمون، قرّاء).\n" + "Use implicit contextual framing instead.\n\n" + "ARABIC QUALITY:\n" + "Zero spelling, grammar, or phrasing mistakes are allowed.\n" + "DO NOT use full tashkeel (vocalization marks) except:\n" + "- To disambiguate rare cases\n" + "- On the last word of a sentence if needed\n\n" + "Academic writing does NOT mean identical openings.\n" + "Each chapter MUST begin with a naturally varied opening strategy.\n" + "Before writing, internally choose ONE opening style that best fits the chapter according to what style says:\n" + "- contextual background\n" + "- problem framing\n" + "- conceptual importance\n" + "- analytical contrast\n" + "- research observation\n" + "- structural preview\n\n" + "=============================\n" + "AUDIENCE RULE (REINFORCED)\n" + "=============================\n\n" + "NEVER use these terms:\n" + "❌ المتدرّب / المتدربون\n" + "❌ القارئ / القرّاء\n" + "❌ الطالب / الطلاب\n" + "❌ المتعلّم / المتعلمون\n" + "❌ الموظف / الموظفون\n\n" + "**This is a BOOK, not a training course.**\n\n" + "Rotate openings naturally across chapters.\n" + "Never reuse the same opening structure repeatedly.\n" + "Avoid formulaic or AI-sounding sentences.\n" + "The introduction must feel written by a human author, not generated from a template.\n" + "Write ONE introduction that:\n" + "1) Follows the style of the chapter intro\n" + "2) Reflects the learning outcome\n" + "3) Frames all sections conceptually without detailing them\n" + "4) Respects scope limits and avoids overlap\n" + "5) Matches the declared intro_function\n\n" + "CRITICAL — NO SECTION SUMMARY:\n" + "Do NOT sequentially describe chapter sections.\n" + "Do NOT narrate the structure of the chapter.\n" + "The introduction must frame ideas, not summarize contents.\n\n" + "=============================\n" + "NO UNSOURCED RESEARCH CLAIMS\n" + "=============================\n" + "NEVER say:\n" + "• تشير الدراسات\n" + "• أثبتت الأبحاث\n" + "• تؤكد الدراسات\n" + "• يرى الباحثون\n\n" + "Unless a specific study, author, or year is explicitly provided in metadata.\n" + "Otherwise, discuss ideas conceptually only.\n\n" + "=============================\n" + "REQUIRED APPROACH\n" + "=============================\n\n" + "1. **START**: Hook that engages with the CONCEPT\n" + " Example: 'ما الذي يجعل البعض قادرين على...؟'\n\n" + "2. **DEVELOP**: Discuss the concept's nature, significance, challenges\n" + " - Focus on WHAT THE CONCEPT IS\n" + " - Focus on WHY IT MATTERS\n" + " - Focus on HOW IT MANIFESTS\n" + " - DO NOT focus on what the chapter covers\n\n" + "3. **CLOSE**: Connect to reader's context or forward implication\n" + " • Set intellectual expectations\n" + " • Create forward momentum\n" + " • NO phrases like 'هنا نستكشف' or 'هذا الفصل يتناول'\n\n" + "=============================\n" + "STYLE: AVOID RIGID ACADEMIC TONE\n" + "=============================\n\n" + "The introduction should be:\n" + "• Intellectually engaging (not dry)\n" + "• Fluid and connected (not choppy)\n" + "• Conceptually rich (not procedurally descriptive)\n" + "• Human-sounding (not research-report style)\n\n" + "❌ RIGID ACADEMIC STYLE:\n" + "'يُعرّف الصمود بأنه قدرة الفرد على التعافي. تشير الدراسات إلى دوره في الأداء.'\n\n" + "✅ FLUID ENGAGING STYLE:\n" + "'الصمود ليس مجرد قدرة على التحمّل، بل هو فنّ إعادة البناء الداخلي بعد كل اهتزاز.'\n\n" + "=============================\n" + "TRANSITIONS BETWEEN IDEAS\n" + "=============================\n\n" + "ALWAYS connect paragraphs smoothly.\n\n" + "Use varied transition tools:\n" + "• وفي هذا السياق...\n" + "• ومن هذا المنطلق...\n" + "• وهنا تكمن...\n" + "• لكن...\n" + "• غير أن...\n" + "• بينما...\n" + "• ومع ذلك...\n\n" + "❌ BAD (choppy jumps):\n" + "'الصمود مهم. له أركان. الشخصية الصامدة لها سمات.'\n\n" + "✅ GOOD (smooth flow):\n" + "'الصمود النفسي يتجاوز القوة الظاهرة. وفي أعماقه، تكمن بنية معقّدة من الأركان النفسية التي تشكّل معًا درعاً داخلياً. وهذا التماسك الداخلي هو ما يميّز...'\n\n" + "STYLE REFINEMENT:\n" + "Prefer conceptual language over procedural language.\n" + "Avoid verbs like: يبدأ، ينتقل، يستعرض، يختتم.\n\n" + "FORBIDDEN ROLES:\n" + "Never use: متدرّب، مشارك، متعلم في دورة، برنامج تدريبي\n" + "This is a BOOK, not training material.\n\n" + "FIELD OVERRIDES:\n" + "- reader_interaction:\n" + " → Prefer conceptual engagement over direct address\n" + '- opening_style includes "سؤال":\n' + " → Question MAY be implicit, not explicit\n" + "- explicit_structure_markers:\n" + " → Use conceptual flow, not verbal markers\n" + "- voice_type = إرشادي:\n" + " → Guide through idea progression, not instructions\n\n" + "Length rules:\n" + "- conceptual_explanation → 250–350 words\n" + "- academic orientation → 450–750 words\n" + "- analytical → 350–550 words\n\n" + "Output:\n" + "- JSON with a single key: intro" + ), + agent=intro_generator_agent, + expected_output="ChapterIntro JSON", + output_json=ChapterIntro, + # output_file=os.path.join(output_dir, "intro_output.json") + ) diff --git a/agents/books/units_structure.py b/agents/books/units_structure.py new file mode 100644 index 0000000000000000000000000000000000000000..0564dddee67c9d5bd7436f68d6c15e44f62fe0d0 --- /dev/null +++ b/agents/books/units_structure.py @@ -0,0 +1,109 @@ +from crewai import Agent, Task +from modules import llm_4m +from schemas import CourseSchema + + +def course_structuring_agent(): + model = llm_4m() + return Agent( + name="CourseStructuringAgent", + role="Educational Content Structuring Expert", + goal=( + "تحليل تصور فني (أو أكثر) لكتاب/حقيبة تدريبية " + "وتحويله إلى هيكل تدريبي JSON واضح ومنضبط بدون كتابة محتوى." + ), + backstory=( + "أنت خبير تصميم مناهج وحقائب تدريبية. " + "تجيد التعامل مع ملفات غير منظمة، ومحتوى متكرر أو موزع على أكثر من مستند. " + "مهمتك استخراج الهيكل العلمي فقط: وحدات + عناوين فرعية + نواتج تعلم. " + "تلتزم التزامًا صارمًا بعدم تغيير أي عنوان ورد كما هو." + ), + llm=model, + verbose=False, + memory=False, + allow_delegation=False, +) + + +def course_structuring_task(course_structuring_agent): + return Task( + agent=course_structuring_agent, + output_json=CourseSchema, + description=""" +أنت خبير في تصميم المناهج وبناء الحقائب التدريبية الاحترافية. + +سيتم تزويدك بـ: +- نص واحد أو أكثر (قد يكونوا من ملفين مختلفين) يمثلون: + • تصور فني للكتاب + • وصف الكتاب + • نواتج التعلم + • الفئة المستهدفة + • تقسيم الفصول والعناوين + +⚠️ ملاحظات مهمة عن النص: +- النص قد يحتوي على: + - شعارات، أسماء جهات، ترويسات تصميمية + - تكرار أو اختلاف في الصياغة +- هذه العناصر غير التعليمية يجب تجاهلها تمامًا +- اعتبر كل النصوص كأنها تمثل كتابًا واحدًا + +======================== +النصوص المُدخلة: +======================== +{merged_book_text} + +======================== +البيانات الإضافية (قد تكون فارغة): +======================== +- course_name: {course_name} +- course_audience: {course_audience} +- course_description: {course_description} +- learning_outcomes (إن وُجدت): {learning_outcomes} + +======================== +🎯 المطلوب منك: +======================== + +1) هيكلة المحتوى: +- كل فصل = Unit واحد +- عنوان الفصل = unit_name +- العناوين أو النقاط داخل الفصل = topics +- تجاهل المقدمة والخاتمة ككيانات مستقلة +- ❗ يُمنع منعًا باتًا: + - إعادة صياغة العناوين + - تحسين لغوي + - دمج أو تقسيم العناوين + +2) Learning Outcomes: +- استخدم learning_outcomes المُدخلة كما هي إن وُجدت +- إذا لم تكن موجودة: + - أنشئ Learning Outcomes عامة للكورس + - بصياغة تعليمية احترافية (يكون المتدرب قادرًا على...) + - لا تكرر عناوين الوحدات حرفيًا + +3) course_description: +- إذا كانت موجودة → لا تغيّرها +- إذا كانت فارغة → أنشئ وصفًا احترافيًا بناءً على المحتوى + +4) course_audience: +- إذا كانت موجودة → لا تغيّرها +- إذا كانت فارغة → استنتج الفئة الأنسب بدقة + +5) وحدات التدريب: +- لكل Unit: + - unit_name: اسم الفصل كما ورد + - topics: قائمة بالعناوين الفرعية فقط + - outcome: ناتج تعلم خاص بهذه الوحدة (جملة واحدة) + +======================== +📦 الإخراج النهائي: +======================== +- JSON فقط +- مطابق تمامًا لـ CourseSchema +- بدون أي شرح أو نص خارج JSON + +⚠️ أي حقل غير موجود في الـ Schema = مرفوض +""", + expected_output="Strict JSON matching CourseSchema schema.", + async_execution=False, +) diff --git a/agents/content_phase/__init__.py b/agents/content_phase/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..922d89d899c16ffa0a38445e6a31838222a36de8 --- /dev/null +++ b/agents/content_phase/__init__.py @@ -0,0 +1,17 @@ +from .final_outlines_before_content import ( + curriculum_designer, + generate_course_structure_task, +) +from .old_outlines_transformer import ( + course_structurer_agent, + transform_plan_to_course_task, +) +from .keyword_agent import query_agent, query_task +from .content_generation import course_writer_agent, course_writer_task +from .quiz_agent import generate_quiz_agent, generate_quiz_task +from .details_before_content.unit_intro import unit_intro_agent, unit_intro_task +from .details_before_content.details import units_details_agent, units_details_task +from .details_before_content.questions_activities_before_content import ( + units_eval_details_agent, + units_eval_details_task, +) diff --git a/agents/content_phase/content_generation.py b/agents/content_phase/content_generation.py new file mode 100644 index 0000000000000000000000000000000000000000..95a3d488b70ed5451dccf24f73762065b6ae9b0e --- /dev/null +++ b/agents/content_phase/content_generation.py @@ -0,0 +1,305 @@ +from crewai import Agent, Task +import os +import json +from modules import llm_open_grok, Example1, Example2, Example3, Example4, Example5 + + +def course_writer_agent(): + model = llm_open_grok() + + return Agent( + name="ArabicCourseWriterAgent", + role="Arabic Academic Content Synthesizer", + goal=( + "إنتاج محتوى أكاديمي عربي منظم ومتسلسل منطقيًا، " + "ومُعد خصيصًا لمقرر تدريبي يقدّمه المدرب للمتدربين. " + "يعتمد المحتوى على scraped content كمصدر معرفة فقط دون نسخ نصي. " + "يجب أن يكون كل محور غنيًا بالمعلومات، قابلًا للشرح المباشر، " + "ويشرح المفاهيم بعمق وليس بشكل سطحي. " + "يجب أن يظهر دور المدرب كقائد تعليمي ومصمم برامج تدريبية، " + "مع استخدام أمثلة تطبيقية واقعية وتوثيق علمي وفق APA الإصدار السابع." + ), + backstory=( + "أنت خبير متخصص في تطوير المناهج العربية وإعداد الحقائب التدريبية الأكاديمية، " + "وخاصة المقررات المهنية والتطبيقية مثل مقرر: {course_title}. " + "تمتلك خبرة واسعة في تصميم البرامج التدريبية وبناء المحتوى التعليمي المنظم " + "القابل للتقديم داخل القاعات التدريبية أو عبر المنصات الإلكترونية. " + "تتعامل مع المدرب باعتباره قائدًا تعليميًا وليس مجرد ناقل للمعلومات. " + "القائد التعليمي هو من يوجه المتدربين نحو أهداف واضحة، " + "وينظم عملية التعلم داخل الجلسة التدريبية ويضبط إيقاعها، " + "ويحفّز المتدربين على المشاركة الفعالة والتفكير النقدي." + "كما تنظر إلى المدرب أيضًا باعتباره مصممًا للبرامج التدريبية، " + "حيث يبدأ العمل بتحليل الاحتياجات التدريبية للمتدربين، " + "ثم تصميم برنامج تدريبي متكامل يتدرج منطقيًا بين المعرفة والمهارات والتطبيق." + "تلتزم دائمًا بالأسلوب الأكاديمي الواضح والدقيق، " + "وتشرح كل مفهوم أو عنصر يتم ذكره شرحًا مفصلًا وليس مجرد سرد." + "تهتم بالتنظيم البصري للنص بشكل كبير، " + "حيث يجب أن تكون كل نقطة في سطر مستقل، " + "وأن يتم استخدام العناوين الفرعية والنقاط المرقمة بوضوح." + "كما تلتزم بالتوثيق العلمي وفق APA الإصدار السابع، " + "مع كتابة أسماء المؤلفين كاملة وذكر نوع المصدر " + "(كتاب – PDF – بحث علمي – مصدر أكاديمي)." + "المصادر العربية يجب أن تكون هي الأغلب، " + "مع السماح بمصدر أجنبي واحد فقط عند الحاجة." + ), + llm=model, + verbose=True, + memory=True, + examples=[Example1, Example2, Example3, Example4, Example5], + allow_delegation=False, + ) + + +def course_writer_task(course_writer_agent): + + return Task( + description=( + "أنت تستقبل scraped content كمصدر معرفة فقط (Reference Only — Not Rewriting)، " + "ومطلوب منك إنتاج محتوى أكاديمي عربي منظم مرتبط مباشرة بعنوان Subtopic، " + "ومناسب ليستخدمه المدرب في الشرح المباشر للمتدربين.\n\n" + "=================================================\n" + "📌 أولًا: قواعد عامة إلزامية\n" + "=================================================\n" + "1) ابدأ دائمًا بجملة واضحة تحدد الفكرة الأساسية للمحور.\n" + "2) أضف مقدمة قصيرة تربط المحور الحالي بالمحور السابق.\n" + "3) امنع أي انحراف عن العنوان؛ يجب أن يخدم كل سطر موضوع المحور مباشرة.\n" + "4) لا تكرر الأفكار؛ إذا تكررت الفكرة يجب دمجها في نقطة واحدة واضحة.\n" + "5) استخدم لغة علمية واضحة ومباشرة بدون عبارات إنشائية أو شعاراتية.\n" + "6) يفضل ألا تتجاوز الجملة 20 كلمة قدر الإمكان.\n" + "7) يمنع وضع روابط أو URLs داخل النص.\n" + "8) أي مفهوم أو عنصر يتم ذكره يجب شرحه بوضوح ولا يسمح بالاكتفاء بذكره فقط.\n" + "9) يجب أن يكون الشرح عميقًا وكافيًا بحيث يستطيع المدرب استخدامه مباشرة في التدريب.\n" + "=================================================\n" + "📌 ثانيًا: التنظيم البصري للمحتوى (مهم جدًا)\n" + "=================================================\n" + "1) يجب أن يكون المحتوى منظمًا بصريًا.\n" + "2) عند استخدام النقاط يجب أن تكون:\n" + " - كل نقطة في سطر مستقل.\n" + " - لا تضع عدة نقاط في نفس السطر.\n" + "3) استخدم النقاط المرقمة (1،2،3) عند شرح العناصر.\n" + "4) استخدم العناوين الفرعية لتنظيم الأفكار داخل المحور.\n" + "5) لا تدمج التعريف والأهمية والمبادئ في فقرة واحدة.\n" + "6) المحتوى يجب أن يكون منسقًا ومنظمًا بصريًا، " + "وليس مجرد سطور متتابعة غير مهيكلة.\n" + "7) عند ذكر الأهمية:\n" + " - يجب شرحها باستفاضة.\n" + " - يمكن أن تكون في صورة نقاط مرقمة (1، 2، 3...).\n" + " - تبدأ بجملة تمهيدية توضح لماذا هذا المحور مهم في السياق التدريبي.\n" + "8) عند وجود خصائص أو سمات:\n" + " - تُعرض بنظام نقاط مرقمة.\n" + " - يتم شرح كل خاصية شرحًا واضحًا ومفصلًا.\n" + "9) عند وجود أنواع أو تصنيفات:\n" + " - يجب شرح كل نوع، وليس مجرد ذكر الأسماء.\n" + "10) عند وجود محور تاريخي:\n" + " - يتم عرض المحتوى بتسلسل زمني واضح.\n" + " - يشمل الفترات القديمة والحديثة.\n" + " - يتم شرح كل مرحلة زمنية وسياقها.\n" + "11) يمكن استخدام (أولًا، ثانيًا، ثالثًا) " + "عند تناول أكثر من فترة زمنية أو أكثر من فرع رئيسي.\n" + "12) يجب التركيز على المحور تركيزا صارما وعدم الخوض فى مواضيع اخره داخله لان ذلك سيسبب فى تكرار وايضا " + "عند تناول محور عن الادوات او التقنيات يجب التركيز عليها مع شرح كل اداة وطرق استخدامها وعدم الاكتفاء باداة او تقنية واحدة واظهر التنوع\n وعندما يكون محور عن امثلة يجب ذكر امثلة واقعية حقيقية مع ذكر الامثلة مفصلة والتركيز عليها.\n\n" + "=================================================\n" + "📌 ثالثًا: شرح دور المدرب داخل المحتوى\n" + "=================================================\n" + "عند الحديث عن التدريب يجب توضيح دور المدرب باعتباره:\n\n" + "1) قائدًا تعليميًا:\n" + " - يوجه المتدربين نحو أهداف واضحة.\n" + " - ينظم عملية التعلم داخل الجلسة التدريبية.\n" + " - يدير النقاش ويحفز المشاركة الفعالة.\n" + " - يساعد المتدربين على التفكير والتحليل وليس الحفظ فقط.\n\n" + "2) مصمم برامج تدريبية:\n" + " - يبدأ بتحليل الاحتياجات التدريبية للمتدربين.\n" + " - يصمم برنامجًا تدريبيًا متدرجًا.\n" + " - يدمج بين المعرفة والمهارات والتطبيق العملي.\n" + " - يستخدم أدوات وتقنيات تدريب متنوعة.\n" + "=================================================\n" + "📌 رابعًا: تنظيم المحتوى داخل المحور\n" + "=================================================\n" + "يمكن استخدام العناصر التالية حسب طبيعة الموضوع:\n" + "- تعريف المفهوم.\n" + "- أهمية الموضوع في التدريب.\n" + "- الخصائص أو السمات.\n" + "- الأنواع أو التصنيفات.\n" + "- الأدوات أو التقنيات المستخدمة.\n" + "- أمثلة واقعية حقيقية.\n" + "- تطبيق عملي أو سيناريو تدريبي.\n" + "- خلاصة تربط بالمحور التالي.\n" + "=================================================\n" + "📌 خامسًا: التوثيق والمصادر\n" + "=================================================\n" + "1) يتم وضع المراجع في قسم sources فقط.\n" + "2) يمنع وضع المراجع داخل generated_content.\n" + "3) يجب استخدام APA الإصدار السابع.\n" + "4) يجب كتابة أسماء المؤلفين كاملة.\n" + "5) يجب ذكر نوع المصدر بجانب المرجع مثل:\n" + "(كتاب – PDF – بحث علمي – موقع أكاديمي).\n" + "6) يجب أن تكون المصادر مرتبطة مباشرة بموضوع المحور.\n" + "7) يجب أن تكون المصادر العربية هي الأغلب.\n" + "8) يسمح بمصدر أجنبي واحد فقط عند الحاجة.\n" + "=================================================\n" + "📌 INPUT DATA\n" + "=================================================\n" + "Course Title: {course_title}\n" + "Unit: {unit_title}\n" + "Topic: {topic_title}\n" + "Subtopic: {subtopic_title}\n" + "Subtopic Description: {subtopic_description}\n" + "Scraped content (reference only):\n" + "{combined_scraped_texts}\n\n" + "=================================================\n" + "📌 OUTPUT FORMAT (MANDATORY)\n" + "=================================================\n" + "Return ONLY a valid JSON object:\n" + "{\n" + ' "generated_content": "Final Arabic academic content",\n' + ' "sources": ["(Author Full Name, Year). Title. Publisher. [Type]"]\n' + "}\n\n" + "=================================================\n" + "📌 FINAL REMINDER\n" + "=================================================\n" + "- الهدف النهائي هو إنتاج محتوى يستطيع المدرب استخدامه مباشرة في الشرح.\n" + "- يجب شرح كل عنصر يتم ذكره.\n" + "- التنظيم البصري للنص أولوية قصوى.\n" + "- كل نقطة يجب أن تكون في سطر مستقل.\n" + "- يجب الالتزام الصارم بموضوع المحور وعدم الخروج عنه.\n" + "- المراجع يجب أن تكون منفصلة تمامًا في sources." + ), + expected_output="JSON with generated_content and sources", + agent=course_writer_agent, + ) + + +# from crewai import Agent, Task +# import os +# import json +# from modules import llm_open_grok,Example1,Example2,Example3,Example4,Example5 +# def course_writer_agent(): +# model=llm_open_grok() +# return Agent( +# name="ArabicCourseWriterAgent", +# role="Arabic Academic Content Synthesizer", +# goal=( +# "إنتاج محتوى أكاديمي عربي منظم، متسلسل منطقيًا، " +# "ومُعد خصيصًا لمقرر تدريبي يقدّمه المدرب للمتدربين. " +# "يعتمد المحتوى على scraped content كمصدر معرفة فقط دون نسخ نصي. " +# "يجب أن يكون كل محور قابلًا للشرح المباشر، غنيًا بالمعلومات، " +# "ومدعومًا بالأمثلة التطبيقية والتوثيق العلمي وفق APA الإصدار السابع. " +# "كل جزء من المحتوى يجب أن يخدم هدف المقرر ويساعد المدرب على شرحه بسهولة." +# ), +# backstory=( +# "أنت خبير متخصص في تطوير المناهج العربية، " +# "وإعداد الحقائب التدريبية الأكاديمية للمقررات المهنية والتطبيقية، " +# "وخاصة مقرر: {course_title}. " +# "تمتلك خبرة واسعة في بناء محتوى تدريبي متسلسل، " +# "قابل للعرض والشرح داخل القاعة أو عبر المنصات الإلكترونية. " +# "تلتزم بالأسلوب الأكاديمي المنضبط، " +# "والتدرج المنطقي في الأفكار، " +# "وتشرح كل مفهوم، خاصية، أو عنصر يتم ذكره دون الاكتفاء بسرده فقط. " +# "تحرص دائمًا على التنظيم البصري للنص، " +# "واستخدام العناوين الفرعية والنقاط المرقمة، " +# "مع الالتزام الصارم بالتوثيق العلمي (APA 7) " +# "من كتب ومراجع علمية عربية وأجنبية ولكن يجب ان يكون العربية الاغلب (PDFs، كتب، أبحاث)." +# ), +# llm=model, +# verbose=True, +# memory=True, +# examples=[Example1, Example2, Example3, Example4, Example5], +# allow_delegation=False, +# ) + +# def course_writer_task(course_writer_agent): +# return Task( +# description=( +# "أنت تستقبل scraped content كمصدر معرفة فقط (Reference Only — Not Rewriting)، " +# "ومطلوب منك إنتاج محتوى أكاديمي عربي منظم، " +# "مرتبط مباشرة بعنوان الـ Subtopic، " +# "ومناسب ليستخدمه المدرب في الشرح المباشر للمتدربين.\n\n" +# "=================================================\n" +# "📌 أولًا: قواعد عامة إلزامية (لا يُسمح بتجاهلها)\n" +# "=================================================\n" +# "1) ابدأ دائمًا بجملة واضحة تُحدد الفكرة الأساسية للمحور.\n" +# "2) أضف مقدمة تمهيدية قصيرة تربط المحور الحالي بالمحور السابق.\n" +# "3) امنع أي انحراف عن العنوان؛ أي محتوى جانبي يتم توظيفه فقط لخدمة السياق.\n" +# "4) لا تكرر الأفكار؛ الفقرات المتشابهة تُدمج في نقطة واحدة متماسكة.\n" +# "5) استخدم لغة واقعية مباشرة، بدون عبارات شعاراتية أو إنشائية.\n" +# "6) الجملة الواحدة لا تتجاوز 20 كلمة قدر الإمكان.\n" +# "7) لا تضع روابط أو URLs داخل النص نهائيًا.\n" +# "8) أي عنصر يتم ذكره (مفهوم، خاصية، نوع، مبدأ) يجب شرحه، " +# "ولا يُسمح بالاكتفاء بذكره فقط.\n\n" +# "=================================================\n" +# "📌 ثانيًا: تنظيم وبناء المحتوى داخل كل محور\n" +# "=================================================\n" +# "1) المحتوى يجب أن يكون منسقًا ومنظمًا بصريًا، " +# "وليس مجرد سطور متتابعة غير مهيكلة.\n" +# "2) لا تدمج التعريف، الأهمية، والمبادئ في فقرة واحدة.\n" +# "3) عند ذكر الأهمية:\n" +# " - يجب شرحها باستفاضة.\n" +# " - يمكن أن تكون في صورة نقاط مرقمة (1، 2، 3...).\n" +# " - تبدأ بجملة تمهيدية توضح لماذا هذا المحور مهم في السياق التدريبي.\n" +# "4) عند وجود خصائص أو سمات:\n" +# " - تُعرض بنظام نقاط مرقمة.\n" +# " - يتم شرح كل خاصية شرحًا واضحًا ومفصلًا.\n" +# "5) عند وجود أنواع أو تصنيفات:\n" +# " - يجب شرح كل نوع، وليس مجرد ذكر الأسماء.\n" +# "6) عند وجود محور تاريخي:\n" +# " - يتم عرض المحتوى بتسلسل زمني واضح.\n" +# " - يشمل الفترات القديمة والحديثة.\n" +# " - يتم شرح كل مرحلة زمنية وسياقها.\n" +# "7) يمكن استخدام (أولًا، ثانيًا، ثالثًا) " +# "عند تناول أكثر من فترة زمنية أو أكثر من فرع رئيسي.\n" +# "8) يجب التركيز على المحور تركيزا صارما وعدم الخوض فى مواضيع اخره داخله لان ذلك سيسبب فى تكرار وايضا " +# "عند تناول محور عن الادوات او التقنيات يجب التركيز عليها مع شرح كل اداة وطرق استخدامها وعدم الاكتفاء باداة او تقنية واحدة واظهر التنوع\n وعندما يكون محور عن امثلة يجب ذكر امثلة واقعية حقيقية مع ذكر الامثلة مفصلة والتركيز عليها.\n\n" +# "=================================================\n" +# "📌 ثالثًا: الأساليب الممكن استخدامها (حسب الحاجة)\n" +# "=================================================\n" +# "- تعريف المفهوم أو المهارة.\n" +# "- أهمية الموضوع في السياق التدريبي.\n" +# "- المبادئ أو الأسس النظرية المرتبطة بالمحور.\n" +# "- تطبيق عملي واحد على الأقل (سيناريو أو حالة واقعية قصيرة).\n" +# "- خلاصة تربط المحور بالمحور أو المهارة التالية.\n" +# "- يُستخدم هذا الترتيب فقط عندما يخدم طبيعة المحور.\n\n" +# "=================================================\n" +# "📌 رابعًا: التوثيق والمصادر\n" +# "=================================================\n" +# "1) توثيق جميع الأفكار يكون في نهاية المحور فقط.\n" +# "2) استخدام نمط APA الإصدار السابع حصريًا.\n" +# "3) المصادر يجب أن تكون:\n" +# " - كتب علمية أو مراجع أكاديمية.\n" +# " - ملفات PDF وأبحاث علمية.\n" +# " - مصادر عربية وأجنبية (مسموح بالاثنين).\n" +# " - مسموح بمصدر واحد انجليزيا على الاكثر والاغلب عربية.\n" +# "4) يُمنع استخدام مواقع عامة كمصدر وحيد دون أصل علمي.\n" +# "5) يمنع وضع المراجع فى generated_content.\n" +# "6) يجب أن تذكر المصدر هل هو كتاب او pdf او موقع اكاديمى لازم تكتب جنب كل مصدر نوعه.\n" +# "6) يجب أن تكون المصادر مرتبطة مباشرة بمحتوى المحور.\n\n" +# "=================================================\n" +# "📌 INPUT DATA\n" +# "=================================================\n" +# "Course Title: {course_title}\n" +# "Unit: {unit_title}\n" +# "Topic: {topic_title}\n" +# "Subtopic: {subtopic_title}\n" +# "Subtopic Description: {subtopic_description}\n" +# "Scraped content (reference only):\n" +# "{combined_scraped_texts}\n\n" +# "=================================================\n" +# "📌 OUTPUT FORMAT (MANDATORY)\n" +# "=================================================\n" +# "Return ONLY a valid JSON object (parsable by json.loads):\n" +# "{\n" +# ' "generated_content": "Final well-structured Arabic academic content",\n' +# ' "sources": ["(Author, Year). Title. Publisher.", "(Author, Year). Title. Source."]\n' +# "}\n\n" +# "=================================================\n" +# "📌 FINAL REMINDER\n" +# "=================================================\n" +# "- الهدف النهائي: محتوى يستطيع المدرب استخدامه مباشرة في الشرح.\n" +# "- أي عنصر مذكور يجب شرحه بوضوح.\n" +# "- التنظيم والتنسيق أولوية قصوى.\n" +# "- يجب التركيز على المحور وعدم الخروج عن موضوعه وتنظيم الكلام عنه وشرحه باستفاضة.\n" +# "- عدم وضع المصادر والمراجع مع الكونتنت يجب ان يكونوا منفصلين فى sources." +# # "اعتمد في تسلسل المحتوى على ما تم شرحه في المحور السابق: {previous_context} لتجنب التكرار وتحقيق الترابط المنطقي." +# ), +# expected_output="JSON with generated_content and sources", +# agent=course_writer_agent, +# ) diff --git a/agents/content_phase/details_before_content/details.py b/agents/content_phase/details_before_content/details.py new file mode 100644 index 0000000000000000000000000000000000000000..5b5789d0a2551047c0a75bd3791fc6e5046f6200 --- /dev/null +++ b/agents/content_phase/details_before_content/details.py @@ -0,0 +1,58 @@ +from crewai import Agent, Task +import os +import json +from modules import llm_4m +from schemas import Units_details_output + + +def units_details_agent(): + model = llm_4m() + return Agent( + role="مصمم مناهج تعليمية", + goal=( + "Transform the detailed course structure produced by the curriculum_designer into a higher-level pedagogical expansion for each unit.\n\n" + "inputs:\n" + "- course_name: {course_name}\n" + "- course_audience: {course_audience}\n" + "- units: {units}\n" + "مرجع: استخدم المثال التالي كنموذج لإنتاج محتوى مفصل:" + "{example_reference}\n\n " + "Your output MUST follow the Units_details_output and UnitExpansion Pydantic models exactly.\n" + "For EACH unit, generate:\n" + "1. صياغة نواتج تعلم محددة، واضحة، قابلة للقياس، ومرتبطة بأهداف المقرر.\n" + "2. توضيح المعارف والمهارات المتضمنة في بداية كل وحدة.\n" + "3. تقديم مقدمة وجيزة تصف الهدف العام للوحدة.\n\n" + "ركز على جعل المحتوى مناسبًا للفئة المستهدفة المحددة في course_audience, مع الالتزام بنمط المثال المرجعي." + ), + backstory=( + "أنت مطوّر وحدات تدريبية ذو خبرة واسعة في إعداد المحتوى التعليمي الاحترافي. " + "تتعلم من المثال المرجعي المقدم لك لضمان إنتاج محتوى متسق، عملي، " + "ومتوافق مع المعايير التعليمية الحديثة. " + "دورك هو الارتقاء بمخرجات المصمم السابق إلى مستوى أعلى من التنظيم البيداغوجي، " + "مع الحفاظ على وضوح الأهداف، قابلية القياس، والتدرج المنطقي في نواتج التعلم." + ), + verbose=True, + memory=True, + llm=model, + allow_delegation=False, + ) + + +def units_details_task(units_details_agent): + return Task( + description=( + "make a new JSON matching Units_details_output. For each unit:\n" + "- Use the unit topics/subtopics to infer its scope.\n" + "- Generate measurable unit_objectives.\n" + "- Write a clear, short unit_description showing knowledge, and skills.\n" + "- Produce measurable learning_outcomes linked to the objectives.\n\n" + "The output MUST strictly follow: Units_details_output → expanded_units → UnitExpansion." + ), + expected_output=( + "Units_details_output JSON: fields: course_name, course_audience, expanded_units" + ), + output_json=Units_details_output, + agent=units_details_agent, + async_execution=False, + # output_file=os.path.join(output_dir, 'unit_expantion.json'), + ) diff --git a/agents/content_phase/details_before_content/questions_activities_before_content.py b/agents/content_phase/details_before_content/questions_activities_before_content.py new file mode 100644 index 0000000000000000000000000000000000000000..683e3eaa7e5cc99f98d222ac567169f4426d3635 --- /dev/null +++ b/agents/content_phase/details_before_content/questions_activities_before_content.py @@ -0,0 +1,62 @@ +from crewai import Agent, Task +import os +import json +from schemas import Units_eval_details +from modules import llm_4m + +def units_eval_details_agent(): + model = llm_4m() + return Agent( + role="مصمم مناهج تعليمية احترافي", + goal=( + "Transform the structured units coming from curriculum_designer into a fully pedagogical expansion for EACH unit.\n\n" + "You MUST generate for every unit the following fields exactly as defined in the EvalDetails model:\n" + "- trainer_instructions\n" + "- eval_questions\n" + "- If this is the FIRST UNIT ONLY, also generate:\n" + " • opening_activity\n" + " • question_after_opening_activity\n\n" + "You MUST learn from the provided example reference:\n" + "{example_reference}\n\n" + "and then produce NEW, CREATIVE, pedagogy-aligned content consistent with the course context.\n\n" + "Inputs:\n" + "- course_name: {course_name}\n" + "- course_audience: {course_audience}\n" + "- units: {units}\n\n" + "The output MUST strictly follow the Units_eval_details and EvalDetails Pydantic models.\n" + "Ensure that ALL generated content matches the style, structure, tone, and depth shown in the example reference.\n" + "Focus on making the content appropriate and engaging for the course_audience and logically aligned with the topics/subtopics of each unit." + ), + backstory=( + "أنت مطوّر وحدات تدريبية ذو خبرة واسعة في إعداد المحتوى التعليمي الاحترافي. " + "تتعلم من المثال المرجعي المقدم لك لضمان إنتاج محتوى متسق، عملي، " + "ومتوافق مع المعايير التعليمية الحديثة. " + "دورك هو الارتقاء بمخرجات المصمم السابق إلى مستوى أعلى من التنظيم البيداغوجي، " + "مع الحفاظ على اضافة لمستك الإبداعية دون الخروج عن النمط التعليمي الاحترافي." + ), + verbose=True, + memory=True, + llm=model, + allow_delegation=False, +) + +def units_eval_details_task(units_eval_details_agent): + return Task( + description=( + "Generate a new JSON strictly matching the Units_eval_details model.\n\n" + "For EACH unit in the provided list:\n" + "- Analyze the unit_name, outcome, and topics/subtopics to understand scope and depth.\n" + "- Produce trainer_instructions and eval_questions.\n" + "- For the FIRST unit ONLY, also generate opening_activity and question_after_opening_activity.\n\n" + "The output MUST strictly follow the hierarchical structure:\n" + "Units_eval_details → EvalDetails.\n" + "Content must follow the style demonstrated in example_reference and remain consistent with the course audience." + ), + expected_output=( + "Units_eval_details JSON: fields: course_name, course_audience, expanded_units" + ), + output_json=Units_eval_details, + agent=units_eval_details_agent, + async_execution=False, + # output_file=os.path.join(output_dir, 'Units_eval_details.json'), +) diff --git a/agents/content_phase/details_before_content/unit_intro.py b/agents/content_phase/details_before_content/unit_intro.py new file mode 100644 index 0000000000000000000000000000000000000000..0713849656aa770145e9720a328fcbf63d28fea7 --- /dev/null +++ b/agents/content_phase/details_before_content/unit_intro.py @@ -0,0 +1,131 @@ +from crewai import Agent, Task +import os +import json +from schemas import Units_Introduction +from modules import llm_g +def unit_intro_agent(): + model = llm_g() + return Agent( + role="كاتب مقدمات تعليمية متخصص", + goal=( + "كتابة مقدمة (وصف موضوع) شاملة واحترافية لكل وحدة دراسية بناءً على تفاصيلها.\n\n" + "المدخلات المطلوبة:\n" + "- course_name: {course_name} - اسم المقرر\n" + "- course_audience: {course_audience} - الفئة المستهدفة\n" + "- units: {units} - قائمة الوحدات \n\n" + "المثال المرجعي:\n" + "{example_reference}\n\n" + "متطلبات كتابة المقدمة (unit_introduction):\n" + "1. ابدأ بجملة افتتاحية قوية توضح أهمية الموضوع ومكانته في المقرر\n" + "2. اشرح السياق العام والهدف الرئيسي من الوحدة\n" + "3. وضّح العلاقة بين الموضوع والممارسة المهنية أو التطبيق العملي\n" + "4. اذكر الجوانب النظرية والعملية المتضمنة\n" + "5. قسّم المحتوى إلى محاور رئيسية واضحة (2-4 محاور)\n" + "6. أظهر القيمة المضافة والفوائد للمتعلم\n" + "7. استخدم لغة واضحة، مهنية، ومحفزة\n" + "8. الطول المناسب: 500-700 كلمة\n\n" + "معايير الجودة:\n" + "- التوافق مع مستوى الفئة المستهدفة ({course_audience})\n" + "- الاتساق التام مع أسلوب ونمط المثال المرجعي\n" + "- الترابط المنطقي والتدفق السلس للأفكار\n" + "- تغطية شاملة دون إسهاب ممل أو اختصار مخل" + ), + backstory=( + "أنت كاتب محتوى تعليمي متخصص بخبرة 12 عامًا في كتابة المقدمات والأوصاف للمقررات الدراسية. " + "تتميز بقدرتك الفائقة على:\n" + "- تحليل محتوى الوحدة (المواضيع، الأهداف، النواتج) واستخلاص الجوهر\n" + "- صياغة مقدمات جذابة تثير اهتمام المتعلم وتوضح قيمة المحتوى\n" + "- كتابة نصوص متوازنة بين العمق الأكاديمي والوضوح والبساطة\n" + "- التعلم من الأمثلة المرجعية والحفاظ على الاتساق الأسلوبي\n" + "- مراعاة خصائص الفئة المستهدفة في اختيار المفردات وأسلوب الطرح\n\n" + "مهمتك الأساسية:\n" + "تحويل المعلومات الأولية عن الوحدة (الأهداف والمواضيع والنواتج) إلى مقدمة تعليمية احترافية " + "تحقق الأهداف التالية:\n" + "✓ جذب انتباه المتعلم وتحفيزه\n" + "✓ توضيح السياق والأهمية\n" + "✓ تقديم خارطة واضحة للمحتوى\n" + "✓ إظهار الصلة بالواقع العملي\n" + "✓ بناء توقعات واقعية عن الوحدة\n\n" + "أنت ملتزم بمعايير الجودة العالية والاتساق مع النماذج المرجعية المقدمة." + ), + verbose=True, + memory=False, + llm=model, + allow_delegation=False, + output_pydantic=Units_Introduction, +) + +# إنشاء Task +def unit_intro_task(unit_intro_agent): + return Task( + description=( + "مهمتك هي إنتاج ملف JSON يحتوي على مقدمة (unit introduction) احترافية لكل وحدة.\n\n" + "خطوات العمل لكل وحدة:\n\n" + "1. التحليل والفهم:\n" + " - اقرأ بعناية: unit_name, topics, unit_objectives, learning_outcomes\n" + " - حدد الموضوع الرئيسي والأفكار المحورية\n" + " - افهم المستوى المعرفي والمهاري المطلوب\n" + " - حدد العلاقة بين الوحدة والمقرر العام\n\n" + "2. الاستلهام من المثال المرجعي:\n" + " - ادرس بنية المقدمات في المثال المرجعي\n" + " - لاحظ: الافتتاحية، تنظيم الفقرات، أسلوب السرد، طول النص\n" + " - حافظ على نفس المستوى من العمق والوضوح\n\n" + "3. كتابة المقدمة (unit_description):\n" + " أ) الفقرة الافتتاحية (2-3 جمل):\n" + # new + " - لا تبدأ أكثر من مقدمتين بنفس الفعل'\n" + " - كل وحدة يجب أن تبدأ بأسلوب افتتاح مختلف من حيث:\n" + " • زاوية الطرح\n" + " • البنية اللغوية\n" + " • نقطة الدخول للفكرة\n" + # + " - وضّح أهمية الموضوع ومكانته في المجال\n" + " - اذكر الهدف الرئيسي من الوحدة\n" + " - Avoid overused phrases (e.g., هل تساءلت يومًا، في عالمنا المتسارع…)\n" + " - Avoid flashy, promotional, or vague language\n" + " - Mix short and long sentences to vary rhythm\n" + " - Include a personal touch: surprise, observation, or simple opinion\n" + " - Use natural transitions from the idea itself\n" + # new + "أنماط افتتاح إلزامية (اختر نمطًا مختلفًا لكل وحدة):\n" + " 1) افتتاح سياقي: - يبدأ بوصف سياق أو مشكلة حقيقية مرتبطة بالمجال\n" + " 2) افتتاح مفاهيمي: - يبدأ بتفكيك مفهوم أساسي أو فكرة محورية\n" + " 3) افتتاح تطبيقي:- يبدأ من موقف مهني أو ممارسة عملية\n" + " 4) افتتاح تفسيري: - يبدأ بشرح سبب أهمية الموضوع أو أثره" + " 5) افتتاح تحليلي: - يبدأ بمقارنة، تصنيف، أو تساؤل تحليلي غير مباشر\n" + # + " ب) فقرة السياق والتطبيق (2-4 جمل):\n" + " - اشرح العلاقة بين النظرية والتطبيق\n" + " - وضّح كيف يستفيد المتعلم من هذه الوحدة عمليًا\n" + " - أظهر الارتباط بالواقع المهني أو الحياتي\n" + " ج) فقرة المحاور (اختيارية):\n" + " - إذا كانت الوحدة تحتوي على محاور واضحة، اذكرها بالترقيم:\n" + " 'يُركّز المحتوى على ... محاور رئيسية:'\n" + " '1- ...: ...'\n" + " '2- ...: ...'\n" + " د) فقرة الجانب العملي (اختيارية - حسب طبيعة الوحدة):\n" + " - إذا كانت الوحدة تتضمن تدريبات أو تطبيقات، اذكرها:\n" + " 'وفي الجانب التطبيقي، يتدرب المتدربون على...'\n\n" + "4. المراجعة والتدقيق:\n" + " - تأكد من الاتساق مع أسلوب المثال المرجعي\n" + " - راجع السلاسة اللغوية والترابط المنطقي\n" + " - تحقق من الطول (500-700 كلمة)\n" + " - تأكد من مناسبة المحتوى للفئة المستهدفة\n\n" + "ملاحظات هامة:\n" + "- استخدم الضمير 'المتدرب' أو 'المتعلم' حسب السياق\n" + "- استخدم أفعالًا قوية: يُعدّ، تُعدّ، يهدف، يُركّز، تتناول، يُسهم، يُمكّن\n" + "- تجنب التكرار والحشو\n" + "- حافظ على الطابع المهني والأكاديمي دون جفاف\n" + "- اجعل المقدمة محفزة وجاذبة للقراءة\n" + "-لا تكرر الافعال في بداية المقدمات قم بالتنويع\n\n" + "الناتج النهائي:\n" + "يجب أن يكون JSON متوافق مع: Units_introductions → UnitIntro → Units_introductions" + ), + expected_output=( + "Units_Introduction JSON: fields: course_name, course_audience, expanded_units" + ), + output_json=Units_Introduction, + agent=unit_intro_agent, + async_execution=False, + # output_file=os.path.join(output_dir, 'units_introductions.json'), +) diff --git a/agents/content_phase/final_outlines_before_content.py b/agents/content_phase/final_outlines_before_content.py new file mode 100644 index 0000000000000000000000000000000000000000..65b6e4486ccc3fa0dfc33dbe05fbaf739a3c1193 --- /dev/null +++ b/agents/content_phase/final_outlines_before_content.py @@ -0,0 +1,72 @@ +from crewai import Agent, Task +import os +import json +from modules import llm_4m +from schemas import CourseContent + +# ------------------------- +# 1. تعريف Agent: Curriculum Designer +# ------------------------- +def curriculum_designer(): + model = llm_4m() + return Agent( + role="مصمم مناهج تعليمية", + goal=( + # "أنت مصمم مناهج تعليمية. مهمتك هي تحويل بيانات المقرر إلى خريطة تعلمية مفصلة." + "أنت مصمم مناهج ذو خبرة عالية. مهمتك تحويل بيانات المقرر إلى خريطة تعلمية مفصلة تشمل وحدات، موضوعات، ونقاط فرعية (subtopics)" + "يمكن شرحها مباشرة من قبل مدرب للمتدربين. رتب المحتوى من المفاهيم الأساسية إلى التطبيق العملي، مع مراعاة وضوح كل نقطة وغرضها التعليمي للفئة المستهدفة." + "المُدخلات:" + "- course_name: {course_name}" + "- course_audience: {course_audience}" + "- course_description: {course_description}" + "- learning_outcomes: {learning_outcomes}" + "- units: {units}" + "مرجع: استخدم المثال التالي كنموذج لإنتاج محتوى مفصل:" + "{example_reference}" + "لكل وحدة في units:" + " - لكل موضوع أصلي، أنشئ 2-5 نقاط فرعية (subtopics) مفصلة." + " - لكل subtopic:" + " - title: عبارة قصيرة." + " - description: 1-2 جمل تشرح الفكرة والغرض التعليمي." + " - رتب النقاط من الأساسيات للتطبيق العملي." + " - ركز على جعل المحتوى مناسبًا للفئة المستهدفة المحددة في course_audience." + "تحويل بيانات المقرر إلى بنية مفصلة تشمل وحدات وموضوعات ونقاط فرعية، " + "مناسبة لأي فئة مستهدفة، مع الالتزام بنمط المثال المرجعي." + ), + backstory=( + "أنت مصمم مناهج ذو خبرة عالية. مهمتك هي ان تتعلم من المثال المرجعي لإنشاء محتوى عملي و تحليل بيانات المقرر وإنتاج خريطة تعلمية تفصيلية تشمل وحدات،" + "موضوعات، ونقاط فرعية (subtopics) يمكن شرحها مباشرة من قِبل مدرب لمتدربين،" + "مع ترتيبها من الأساسيات إلى التطبيق العملي، مع مراعاة {course_audience}الفئة المستهدفة." + # "أنت مصمم مناهج ذو خبرة عالية، تتعلم من المثال المرجعي لإنشاء محتوى عملي، " + # "متدرج، وعملي لأي audience." + ), + verbose=True, + memory=True, + llm=model, + allow_delegation=False, +) + +# ------------------------- +# 2. Task: إنشاء نواتج JSON مفصلة +# ------------------------- +def generate_course_structure_task(curriculum_designer): + return Task( + description=( + "تحويل بيانات المقرر (course_name, course_audience, units) إلى JSON مفصل يشمل expanded_units. " + "لكل موضوع، أنشئ 2-5 subtopics، كل subtopic يحتوي على title و description قصيرة تشرح الفكرة والغرض التعليمي. " + # "مع مراعاة أن تكون قابلة للشرح من قبل مدرب للمتدربين. رتب النقاط من المفاهيم الأساسية إلى التطبيق العملي." + "تأكد أن كل subtopic يمكن شرحه مباشرة من قبل مدرب للمتدربين. " + "رتب النقاط من المفاهيم الأساسية إلى التطبيق العملي، وأضف أمثلة عملية إذا أمكن." + ), + # description=( + # "تحويل بيانات المقرر (course_name, course_audience, units) إلى JSON مفصل " + # "يشمل expanded_units. لكل موضوع، أنشئ 2-5 subtopics، كل subtopic يحتوي على title و description." + # ), + expected_output=( + "CourseContent JSON: fields: course_name, course_audience, expanded_units" + ), + agent=curriculum_designer, + output_json=CourseContent, + async_execution=False, + # output_file=os.path.join(output_dir, "تحليل البيانات.json"), +) diff --git a/agents/content_phase/keyword_agent.py b/agents/content_phase/keyword_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..b695d5bbd23a369ca3cf2ebeb12d599bbe1b3247 --- /dev/null +++ b/agents/content_phase/keyword_agent.py @@ -0,0 +1,35 @@ +from crewai import Agent, Task, Crew +from modules import llm_open_nova +from schemas import CourseWithQueries # فيه الـ Pydantic model بتاعك + +# ==== Define Agent ==== +def query_agent(): + model = llm_open_nova() + return Agent( + name="SearchQueryAgent", + role="Generate 3 short, relevant search queries for each subtopic in the course", + goal=( + "For each subtopic in the course JSON, generate 3 short, precise, SEO-friendly search queries " + "related to topic. Avoid vague, repetitive, or off-topic queries." + ), + backstory=( + "This agent is a research strategist for the course. It creates queries suitable for " + "web search to gather useful, relevant content for each subtopic." + ), + verbose=True, + llm=model, +) + +# ==== Task Definition ==== +def query_task(query_agent): + return Task( + name="Generate 3 search queries in arabic and english per subtopic", + description=( + "Read the course JSON structure and generate 3 diverse queries per subtopic arabic and english " + "Course json is: {course} Queries must be concise, specific, and relevant." + ), + expected_output="A valid JSON following CourseWithQueries schema", + output_json=CourseWithQueries, + # output_file=output_file, + agent=query_agent, +) diff --git a/agents/content_phase/old_outlines_transformer.py b/agents/content_phase/old_outlines_transformer.py new file mode 100644 index 0000000000000000000000000000000000000000..ca9887384d7d4b7b979ff721bbe481068310584c --- /dev/null +++ b/agents/content_phase/old_outlines_transformer.py @@ -0,0 +1,88 @@ +from crewai import Agent, Task, Crew +from modules import llm_4m +from schemas import CourseData # فيه الـ Pydantic model بتاعك + + +def course_structurer_agent(): + model = llm_4m() + return Agent( + role="Instructional Designer", + goal=( + "تحويل مخطط برنامج تدريبي (بصيغة JSON) إلى هيكل موحّد لمقرر " + "تدريبي باسم course_data." + ), + backstory=( + "خبير تصميم تعليمي يقوم ببناء توصيفات مقررات تدريبية، " + "ويحوّل المحاور والأهداف العامة إلى مخرجات تعلم ووحدات وموضوعات." + ), + llm=model, + verbose=True, +) + +def transform_plan_to_course_task(course_structurer_agent): + return Task( + description=( + "أنت خبير تصميم تعليمي.\n" + "سيتم تزويدك بالمدخلات التالية:\n" + "1) plan_json: وهو JSON يمثل مخطط برنامج تدريبي كما يلي:\n" + "{plan_json}\n\n" + "2) audience_text: نص أولي يصف الفئة المستهدفة:\n" + "{audience_text}\n\n" + "مهمتك هي إنتاج كائن JSON جديد باسم course_data " + "له البنية التالية بالضبط:\n\n" + "course_data = {\n" + ' "course_name": "اسم المقرر",\n' + ' "course_audience": "وصف موسَّع للفئة المستهدفة",\n' + ' "course_description": "وصف عام للمقرر",\n' + ' "learning_outcomes": [\n' + " // عبارات مخرجات تعلم\n" + " ],\n" + ' "units": [\n' + " {\n" + ' "outcome": "مخرج تعلم للوحدة",\n' + ' "unit_name": "اسم الوحدة",\n' + ' "topics": [\n' + ' "موضوع 1",\n' + ' "موضوع 2",\n' + ' "..."\n' + " ]\n" + " }\n" + " ]\n" + "}\n\n" + "قواعد البناء:\n" + "1) استخرج اسم المقرر من حقل main_concept داخل plan_json " + 'واستخدمه كـ "course_name" بعد تحسين صياغته إن لزم.\n\n' + '2) استخدم audience_text كأساس لبناء "course_audience" ولكن:\n' + " - أَعِد صياغته صياغة لغوية قوية وواضحة.\n" + " - وسِّع الوصف بإضافة تفاصيل مناسبة عن الفئة المستهدفة " + " (مثل نوع خبرتهم، احتياجاتهم التدريبية، بيئتهم التعليمية)،\n" + " - مع الحفاظ على المعنى الأصلي وعدم اختلاق فئة مختلفة.\n\n" + '3) ابنِ "course_description" استناداً إلى:\n' + " - main_concept من plan_json\n" + " - General_objectives_of_the_plan من plan_json\n" + " - وصف المحاور (focus) في validated_units داخل plan_json\n" + " بحيث يكون الوصف فقرة أو فقرتين متماسكتين تشرح هدف المقرر ومحتواه.\n\n" + '4) صِغ "learning_outcomes" كقائمة من مخرجات تعلم (حوالي 8 إلى 15 مخرجاً) ' + "مستندة إلى:\n" + " - الأهداف العامة General_objectives_of_the_plan\n" + " - المحاور الرئيسية validated_units ومحاورها الفرعية axes (title, purpose).\n" + " على أن تبدأ كل عبارة بفعل مضارع واضح مثل: توضح، تحدد، تطبق، تميز، تحلل...\n\n" + '5) تكوين الوحدات "units" كالتالي (انتبِه جيداً):\n' + ' - لكل عنصر من validated_units في plan_json يجب إنشاء وحدة واحدة فقط في قائمة "units".\n' + " - لا تحوِّل العناصر داخل axes إلى وحدات منفصلة.\n" + " - unit_name = قيمة الحقل title لكل عنصر داخل validated_units (عنوان المحور الرئيسي).\n" + " - outcome = إعادة صياغة الحقل focus لنفس العنصر في صورة جملة مخرج تعلم مختصرة " + " تعبّر عن الهدف العام لهذه الوحدة.\n" + " - topics = قائمة الموضوعات الفرعية المأخوذة كما هي من الحقول title داخل axes " + " التابعة لنفس الوحدة، وبنفس الترتيب.\n" + " أي أن topics = [axis_1.title, axis_2.title, ...] دون إضافة موضوعات جديدة " + " ودون حذف أي موضوعات، ودون تغيير معاني العناوين.\n\n" + "6) يجب أن يكون المخرج النهائي JSON صالح نحويًا، بدون أي شرح إضافي،\n" + " أي: أطبع كائن course_data فقط ولا تضف نصوصاً خارجه.\n\n" + "7) استخدم اللغة العربية الفصحى في جميع النصوص، " + "مع الحفاظ على أسلوب مهني يناسب الأدلة التدريبية." + ), + agent=course_structurer_agent, + expected_output="JSON واحد بصيغة course_data فقط.", + output_json=CourseData, +) diff --git a/agents/content_phase/quiz_agent.py b/agents/content_phase/quiz_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..47f1cebbca468dcae5fb9f85e0422cff12170b46 --- /dev/null +++ b/agents/content_phase/quiz_agent.py @@ -0,0 +1,67 @@ +from crewai import Agent, Task, Crew +from modules import llm_g +from schemas import QuizOutput # فيه الـ Pydantic model بتاعك + +def generate_quiz_agent(): + model = llm_g() + return Agent( + role="مصمم أسئلة تدريبية", + goal=( + "إنشاء 20 سؤالًا تدريبيًا عالي الجودة مبنية على المحتوى التعليمي المقدم. " + "يجب أن تشمل الأسئلة مزيجًا من أسئلة مباشرة تتحقق من قراءة المتدرب للمحتوى، " + "وأسئلة تحليلية تتطلب فهمًا عميقًا واستنتاجًا. " + "المطلوب إنتاج أسئلة اختيار من متعدد وصح/خطأ باللغة العربية." + ), + backstory=( + "أنت خبير تربوي متخصص في القياس والتقويم، تعمل منذ 15 عاماً في تصميم الاختبارات الأكاديمية. " + "تتميز بقدرتك على صياغة أسئلة دقيقة تكشف مستوى استيعاب المتعلم، " + "وتدمج بين الأسئلة المباشرة وأسئلة الفهم العميق." + ), + verbose=True, + memory=False, + llm=model, + allow_delegation=False, +) + +def generate_quiz_task(generate_quiz_agent): + return Task( + description=( + "أنشئ 20 سؤالًا تدريبيًا بناءً على المعلومات التالية:\n" + "- course_name: {course_name}\n" + "- course_audience: {course_audience}\n" + "- unit_name: {unit_name}\n" + "- unit_outcome: {unit_outcome}\n" + "- topics: {topics}\n" + + "هيكل الأسئلة المطلوب:\n" + " - 5 أسئلة مباشرة للتحقق من قراءة المتدرب للمحتوى (مسموح باستخدام معلومات ظاهرية من النص).\n" + " - 10 أسئلة اختيار من متعدد متدرجة من المتوسطة إلى الصعبة تعتمد على التحليل والفهم.\n" + " - 5 أسئلة صح/خطأ تعتمد على الاستنتاج وربط المفاهيم.\n\n" + + "## معايير الجودة الإلزامية:\n" + "✅ لا تستخدم أبداً صيغ مثل: 'وفقاً للنص'، 'ذُكر في المحتوى'، 'كما ورد او اي كلمات تدل على المحتوى في الاسئله'\n" + "✅ استخدم أمثلة وسياقات لم تُذكر مباشرة في المحتوى\n" + "✅ تجنب الخيارات الواضحة الخطأ مثل 'كل ما سبق' أو 'لا شيء مما سبق'\n" + "✅ اجعل المُشتتات (الخيارات الخاطئة) مبنية على أخطاء مفاهيمية شائعة\n" + "✅ وايضاح ال topic الذي اعمد عليه السؤال كل سؤال يجب أن يكون مصحوباً بشرح تفصيلي للإجابة الصحيحة\n" + "✅ تأكد من أن الإجابة على كل سؤال موجودة فعلاً في المحتوى المقدم\n\n" + + "قواعد عامة:\n" + "✔️ الأسئلة المباشرة يجب أن تعتمد على معلومة واضحة موجودة داخل المحتوى.\n" + "✔️ الأسئلة التحليلية يجب أن تعتمد على الفهم العميق وليس النسخ المباشر.\n" + "✔️ تجنّب الأسئلة البديهية أو التي لا تحتاج قراءة المحتوى.\n" + "✔️ يجب توزيع الأسئلة بالتساوي على جميع المواضيع.\n" + "❌ ممنوع العبارات الصحيحة/الخاطئة المنقولة حرفياً من المحتوى\n" + "❌ ممنوع الأسئلة التي يمكن حلها بالمنطق العام دون قراءة المحتوى\n" + "يجب اختيار درجة الصعوبة من متوسط وصعب وصعب جدا وكتابتها فى الdifficulty\n\n " + "⚠️ كل سؤال يجب أن يحتوي على الحقل التالي:" + "- topic_question_related_to: اسم الموضوع الفرعي المستخدم في السؤال\n" + "أخرج JSON مطابق لنموذج QuizOutput." + "Output must be as QuizOutput schema" + ), + expected_output="QuizOutput JSON", + output_json=QuizOutput, + agent=generate_quiz_agent, + async_execution=False, + # output_file=os.path.join(output_dir, "generated_quiz.json"), +) diff --git a/agents/design_phase/__init__.py b/agents/design_phase/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f528f010324d6f6dea7fb299ef8b1764c0b97aa1 --- /dev/null +++ b/agents/design_phase/__init__.py @@ -0,0 +1,5 @@ +from .keywoard_researcher import keyword_researcher_agent, keyword_researcher_task +from .source_finder import search_engine_agent, search_engine_task +from .scraper.extractor_bulit_in import scraping_built_in_agent, scraping_built_in_task +from .scraper.extractor_bs4 import scraping_bs4_agent, scraping_bs4_task +# from .scraper.extractor_crawlee import scraping_crawlee_agent, scraping_crawlee_task diff --git a/agents/design_phase/keywoard_researcher.py b/agents/design_phase/keywoard_researcher.py new file mode 100644 index 0000000000000000000000000000000000000000..478d5065d23ab6f2a9c023c5025687d2efbfaaf7 --- /dev/null +++ b/agents/design_phase/keywoard_researcher.py @@ -0,0 +1,50 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +from modules import llm +#from schemas import SuggestedSearchQueries + +no_keywords = 25 + +keyword_researcher_agent = Agent( + role="search queries recommendation agent", + goal="\n".join( + [ + "To generate a set of structured and comprehensive search queries based on the topic {topic} " + "and the outline headings (main and sub-headings) provided from the outline task.", + "Queries should be varied, precise,relevant , and research-oriented.", + "For each heading and sub-heading, create multiple queries to help collect rich, relevant content from the web.", + "Queries must ensure coverage of the most important aspects of the topic, avoiding irrelevant or empty dimensions.", + "Avoid vague or overly generic queries.", + "Ensure queries reflect the scope of the outline, avoiding irrelevant or generic phrasing.", + "Strictly avoid generating queries related to religions, politics, or any biased/off-topic perspectives.", + ] + ), + backstory=( + "This agent acts as a scientific researcher and content strategist and instructional designer. " + "It takes the topic and its structured outline (headings + sub-headings) and expands them into precise, " + "targeted search queries. These queries are crafted to maximize coverage of the outline sections " + "and ensure the user can gather high-quality resources for each part." + ), + llm=llm, + verbose=True, +) + +keyword_researcher_task = Task( + name="search queries recommendation task", + description="\n".join( + [ + "Using the outline headings and sub-headings {outlines} for the topic {topic},", + "generate up to {no_keywords_Researcher_For_Each_Heading} short and precise search queries for each heading.", + "Each query must be short, complete, and precise, directly reflecting the heading or sub-heading it belongs to.", + "The aim is to cover the full breadth of the outline so that no important section is left without queries.", + "Queries should be research-oriented, avoiding vague or overly generic terms.", + "Important: All queries for the same heading must be unique and non-repetitive.", + "Do NOT generate queries that only differ by synonyms like 'concept' vs. 'definition'.", + "Instead, vary the focus (e.g., concept, benefits, methods, challenges, applications).", + "Strictly exclude any queries related to religion, politics, or biased/off-topic perspectives.", + ] + ), + expected_output="A JSON object containing a list of suggested search queries.", +# output_json=SuggestedSearchQueries, + agent=keyword_researcher_agent, +) diff --git a/agents/design_phase/scraper/extractor_bs4.py b/agents/design_phase/scraper/extractor_bs4.py new file mode 100644 index 0000000000000000000000000000000000000000..11ebcdae2a655f22ebb5a161809566f3d54b0299 --- /dev/null +++ b/agents/design_phase/scraper/extractor_bs4.py @@ -0,0 +1,96 @@ +from crewai import Agent, Task +from modules import llm_g +from tools import pdf_tool, scraping_tool +from tools import WebScrapingToolBS4 +from schemas import UnitSubtopicOutputModel + + +web_scraper = WebScrapingToolBS4(name="web_scraping_tool") + +scraping_bs4_agent = Agent( + role="Educational Content Scraping & Knowledge Extraction Agent", + goal="\n".join( + [ + "Collect and extract complete, structured, and educationally valuable content " + "from Arabic and English websites and PDFs that i will give to you related to the course topic: {topic}.", + "Focus on content that match the course domain ({domain}), content type ({content_type}), " + "and audience ({audience}).", + "Prioritize materials that can serve as strong foundations for creating {material_type} " + "learning materials (conceptual, structural, procedural, and real-world).", + "Extract full text including all sections, examples, and details, ensuring high accuracy for Arabic text.", + "Assess each source’s credibility and educational relevance, ranking them by usefulness for course design.", + "Provide concise expert notes and recommendations that will assist curriculum developers and instructional designers " + "in selecting the best materials for building a complete learning unit.", + ] + ), + backstory="\n".join( + [ + "You are a specialized educational data researcher trained to explore, extract, and organize academic and professional content.", + "You excel at discovering high-quality Arabic and English resources that align with specific course development objectives.", + "Your mission is to help course designers collect trustworthy, pedagogically sound materials that will form the backbone of educational units.", + "You understand how to evaluate the quality, relevance, and credibility of both web pages and PDFs.", + "You are particularly skilled at preserving Arabic text integrity and extracting complete structured information for learning materials.", + ] + ), + llm=llm_g, + tools=[web_scraper], + verbose=True, +) + + +scraping_bs4_task = Task( + description="\n".join( + [ + "Your task is to extract and organize full educational content from the following source:", + "", + "URL: {url}", + "Unit Title: {unit_title}", + "Subtopic Title: {subtopic_title}", + "Query Used: {query}", + "", + "This link is part of the course topic '{topic}' under the domain '{domain}'.", + "The extracted content should help create educational materials for the audience '{audience}', focusing on '{content_type}' learning goals.", + "", + "For the given URL:", + " - Extract the full title, structured text, and any available media (images, videos, audios, PDFs).", + " - Maintain the Arabic text structure and readability.", + " - Evaluate its reliability and educational value in relation to {material_type} material categories.", + " - Assign an agent recommendation rank (0–5) based on credibility and relevance.", + " - Provide short expert notes justifying the ranking and explaining how the content can contribute to the course design.", + "", + "Ensure no important content, examples, or explanations are omitted from extraction.", + "Output will be a json format with no task output or raw data only the formatted json dictionary.", + ] + ), + expected_output=( + "Return ONLY a valid Python dictionary.\n" + "- Do not include explanations, markdown, or code fences.\n" + "- The dictionary must be UTF-8 safe and directly usable in Python with ast.literal_eval.\n" + "- Keys must be wrapped in double quotes.\n\n" + "Format example:\n" + "{\n" + ' "unit_title": "من الفكرة إلى نموذج العمل: بناء الأساس الريادي",\n' + ' "subtopic_title": "مفهوم ريادة الأعمال وأهميتها الاقتصادية والاجتماعية",\n' + ' "query": "دور ريادة الأعمال في التنمية الاجتماعية",\n' + ' "parts": [\n' + " {\n" + ' "page_url": "https://example.com/page1",\n' + ' "title": "Understanding Entrepreneurship in the Arab World",\n' + ' "content": "Full educational content extracted from the site.",\n' + ' "img_url": ["https://example.com/image1.jpg"],\n' + ' "video_url": ["https://example.com/video1.mp4"],\n' + ' "audio_url": ["https://example.com/audio1.mp3"],\n' + ' "pdf_url": ["https://example.com/file1.pdf"],\n' + ' "agent_recommendation_rank": 4.8,\n' + ' "agent_recommendation_notes": "Rich Arabic content, relevant to conceptual materials."\n' + " }\n" + " ]\n" + "}\n\n" + "Make the output compatible with Python's ast library (use r1 = result.dict()['raw']; f_result = ast.literal_eval(r1)).\n" + "Ensure valid JSON syntax with no unterminated strings or extra text.\n" + "Output ONLY the dictionary — no thoughts, explanations, or markdown formatting." + "D" + ), + agent=scraping_bs4_agent, + output_json=UnitSubtopicOutputModel, +) diff --git a/agents/design_phase/scraper/extractor_bulit_in.py b/agents/design_phase/scraper/extractor_bulit_in.py new file mode 100644 index 0000000000000000000000000000000000000000..42a78a13d20d55fc7667a7b4af82c6bfc24e7a8a --- /dev/null +++ b/agents/design_phase/scraper/extractor_bulit_in.py @@ -0,0 +1,92 @@ +from crewai import Agent, Task +from modules import llm_g +from tools import pdf_tool, scraping_tool +from schemas import UnitSubtopicOutputModel + + +scraping_built_in_agent = Agent( + role="Educational Content Scraping & Knowledge Extraction Agent", + goal="\n".join( + [ + "Collect and extract complete, structured, and educationally valuable content " + "from Arabic and English websites and PDFs that i will give to you related to the course topic: {topic}.", + "Focus on content that match the course domain ({domain}), content type ({content_type}), " + "and audience ({audience}).", + "Prioritize materials that can serve as strong foundations for creating {material_type} " + "learning materials (conceptual, structural, procedural, and real-world).", + "Extract full text including all sections, examples, and details, ensuring high accuracy for Arabic text.", + "Assess each source’s credibility and educational relevance, ranking them by usefulness for course design.", + "Provide concise expert notes and recommendations that will assist curriculum developers and instructional designers " + "in selecting the best materials for building a complete learning unit.", + ] + ), + backstory="\n".join( + [ + "You are a specialized educational data researcher trained to explore, extract, and organize academic and professional content.", + "You excel at discovering high-quality Arabic and English resources that align with specific course development objectives.", + "Your mission is to help course designers collect trustworthy, pedagogically sound materials that will form the backbone of educational units.", + "You understand how to evaluate the quality, relevance, and credibility of both web pages and PDFs.", + "You are particularly skilled at preserving Arabic text integrity and extracting complete structured information for learning materials.", + ] + ), + llm=llm_g, + tools=[pdf_tool, scraping_tool], + verbose=True, +) + + +scraping_built_in_task = Task( + description="\n".join( + [ + "Your task is to extract and organize full educational content from the following source:", + "", + "URL: {url}", + "Unit Title: {unit_title}", + "Subtopic Title: {subtopic_title}", + "Query Used: {query}", + "", + "This link is part of the course topic '{topic}' under the domain '{domain}'.", + "The extracted content should help create educational materials for the audience '{audience}', focusing on '{content_type}' learning goals.", + "", + "For the given URL:", + " - Extract the full title, structured text, and any available media (images, videos, audios, PDFs).", + " - Maintain the Arabic text structure and readability.", + " - Evaluate its reliability and educational value in relation to {material_type} material categories.", + " - Assign an agent recommendation rank (0–5) based on credibility and relevance.", + " - Provide short expert notes justifying the ranking and explaining how the content can contribute to the course design.", + "", + "Ensure no important content, examples, or explanations are omitted from extraction.", + "Output will be a json format with no task output or raw data only the formatted json dictionary.", + ] + ), + expected_output=( + "Return ONLY a valid Python dictionary.\n" + "- Do not include explanations, markdown, or code fences.\n" + "- The dictionary must be UTF-8 safe and directly usable in Python with ast.literal_eval.\n" + "- Keys must be wrapped in double quotes.\n\n" + "Format example:\n" + "{\n" + ' "unit_title": "من الفكرة إلى نموذج العمل: بناء الأساس الريادي",\n' + ' "subtopic_title": "مفهوم ريادة الأعمال وأهميتها الاقتصادية والاجتماعية",\n' + ' "query": "دور ريادة الأعمال في التنمية الاجتماعية",\n' + ' "parts": [\n' + " {\n" + ' "page_url": "https://example.com/page1",\n' + ' "title": "Understanding Entrepreneurship in the Arab World",\n' + ' "content": "Full educational content extracted from the site.",\n' + ' "img_url": ["https://example.com/image1.jpg"],\n' + ' "video_url": ["https://example.com/video1.mp4"],\n' + ' "audio_url": ["https://example.com/audio1.mp3"],\n' + ' "pdf_url": ["https://example.com/file1.pdf"],\n' + ' "agent_recommendation_rank": 4.8,\n' + ' "agent_recommendation_notes": "Rich Arabic content, relevant to conceptual materials."\n' + " }\n" + " ]\n" + "}\n\n" + "Make the output compatible with Python's ast library (use r1 = result.dict()['raw']; f_result = ast.literal_eval(r1)).\n" + "Ensure valid JSON syntax with no unterminated strings or extra text.\n" + "Output ONLY the dictionary — no thoughts, explanations, or markdown formatting." + ), + agent=scraping_built_in_agent, + output_json=UnitSubtopicOutputModel, +) diff --git a/agents/design_phase/scraper/extractor_crawlee.py b/agents/design_phase/scraper/extractor_crawlee.py new file mode 100644 index 0000000000000000000000000000000000000000..76b16e77ad48cb5c6f7830d5d7f7b455d8e4639e --- /dev/null +++ b/agents/design_phase/scraper/extractor_crawlee.py @@ -0,0 +1,97 @@ +# from crewai import Agent, Task +# from modules import llm_g +# from tools import pdf_tool, scraping_tool +# from tools import WebScrapingCrawleeTool +# from schemas import UnitSubtopicOutputModel + + +# web_scraper = WebScrapingCrawleeTool() + +# scraping_crawlee_agent = Agent( +# role="Educational Content Scraping & Knowledge Extraction Agent", +# goal="\n".join( +# [ +# "Collect and extract complete, structured, and educationally valuable content " +# "from Arabic and English websites and PDFs that i will give to you related to the course topic: {topic}.", + # "Focus on content that match the course domain ({domain}), content type ({content_type}), " + # "and audience ({audience}).", +# "Prioritize materials that can serve as strong foundations for creating {material_type} " +# "learning materials (conceptual, structural, procedural, and real-world).", +# "Extract full text including all sections, examples, and details, ensuring high accuracy for Arabic text.", +# "Assess each source’s credibility and educational relevance, ranking them by usefulness for course design.", +# "Provide concise expert notes and recommendations that will assist curriculum developers and instructional designers " +# "in selecting the best materials for building a complete learning unit.", +# ] +# ), +# backstory="\n".join( +# [ +# "You are a specialized educational data researcher trained to explore, extract, and organize academic and professional content.", +# "You excel at discovering high-quality Arabic and English resources that align with specific course development objectives.", +# "Your mission is to help course designers collect trustworthy, pedagogically sound materials that will form the backbone of educational units.", +# "You understand how to evaluate the quality, relevance, and credibility of both web pages and PDFs.", +# "You are particularly skilled at preserving Arabic text integrity and extracting complete structured information for learning materials.", +# ] +# ), +# llm=llm_g, +# tools=[web_scraper], +# verbose=True, +# ) + + +# scraping_crawlee_task = Task( +# description="\n".join( +# [ +# "Your task is to extract and organize full educational content from the following source:", +# "", +# "URL: {url}", +# "Unit Title: {unit_title}", +# "Subtopic Title: {subtopic_title}", +# "Query Used: {query}", +# "", +# "This link is part of the course topic '{topic}' under the domain '{domain}'.", +# "The extracted content should help create educational materials for the audience '{audience}', focusing on '{content_type}' learning goals.", +# "", +# "For the given URL:", +# " - Extract the full title, structured text, and any available media (images, videos, audios, PDFs).", +# " - Maintain the Arabic text structure and readability.", +# " - Evaluate its reliability and educational value in relation to {material_type} material categories.", +# " - Assign an agent recommendation rank (0–5) based on credibility and relevance.", +# " - Provide short expert notes justifying the ranking and explaining how the content can contribute to the course design.", +# "", +# "Ensure no important content, examples, or explanations are omitted from extraction.", + # "Output will be a json format with no task output or raw data only the formatted json dictionary.", + +# ] +# ), +# expected_output=( +# "Return ONLY a valid Python dictionary.\n" +# "- Do not include explanations, markdown, or code fences.\n" +# "- The dictionary must be UTF-8 safe and directly usable in Python with ast.literal_eval.\n" +# "- Keys must be wrapped in double quotes.\n\n" +# "Format example:\n" +# "{\n" +# ' "unit_title": "من الفكرة إلى نموذج العمل: بناء الأساس الريادي",\n' +# ' "subtopic_title": "مفهوم ريادة الأعمال وأهميتها الاقتصادية والاجتماعية",\n' +# ' "query": "دور ريادة الأعمال في التنمية الاجتماعية",\n' +# ' "parts": [\n' +# " {\n" +# ' "page_url": "https://example.com/page1",\n' +# ' "title": "Understanding Entrepreneurship in the Arab World",\n' +# ' "content": "Full educational content extracted from the site.",\n' +# ' "img_url": ["https://example.com/image1.jpg"],\n' +# ' "video_url": ["https://example.com/video1.mp4"],\n' +# ' "audio_url": ["https://example.com/audio1.mp3"],\n' +# ' "pdf_url": ["https://example.com/file1.pdf"],\n' +# ' "agent_recommendation_rank": 4.8,\n' +# ' "agent_recommendation_notes": "Rich Arabic content, relevant to conceptual materials."\n' +# " }\n" +# " ]\n" +# "}\n\n" +# "Make the output compatible with Python's ast library (use r1 = result.dict()['raw']; f_result = ast.literal_eval(r1)).\n" +# "Ensure valid JSON syntax with no unterminated strings or extra text.\n" +# "Output ONLY the dictionary — no thoughts, explanations, or markdown formatting." +# ), +# agent=scraping_crawlee_agent, +# output_json=UnitSubtopicOutputModel, +# ) +# # \ No newline at end of file diff --git a/agents/design_phase/source_finder.py b/agents/design_phase/source_finder.py new file mode 100644 index 0000000000000000000000000000000000000000..4a8d3a232a3cc896780a3ca2df932572400fc92e --- /dev/null +++ b/agents/design_phase/source_finder.py @@ -0,0 +1,77 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +from modules import llm_oss +from crewai_tools import SerperDevTool +from schemas import SearchEngineOutput +from tools import URLValidatorTool + +# --- Tool --- +search_tool = SerperDevTool() +url_validate_tool = URLValidatorTool(name="url_validator") + +# --- Agent Definition --- +search_engine_agent = Agent( + role="Search and Validation agent", + goal="\n".join( + [ + "Search for diverse, high-quality, and reliable information related to {topic} using the suggested search queries.", + "Prioritize **scholarly and educational resources**: academic journals, books, research articles, and trusted institutional websites and our trusted sites we know: {TRUSTED_SITES}.", + "Include Arabic sources when contextually valuable, but prioritize English academic sources for broader coverage.", + "Ensure all results are concise, relevant, and aligned with the outline headings to directly support course design and educational content.", + "For each query, return at most {no_links} results with structured metadata (title, url, content summary, score, search_query).", + "Validate URLs before retrieving them and if it is not valid dont retieve", + ] + ), + backstory=( + "This agent acts like a scientific researcher and educational content curator." + "It executes the generated queries and gathers high-value, trustworthy information." + "The agent is trained to filter out irrelevant, commercial, or low-quality content, ensuring that " + "only academic and pedagogically useful resources are kept." + "for building interactive e-learning content are retrieved." + "It balances global knowledge with local cultural enrichment by including both English and Arabic sources." + "Uses Serper search and validates each found URL." + ), + verbose=True, + llm=llm_oss, + tools=[search_tool, url_validate_tool], + allow_delegation=False, +) + +# --- Task Definition --- +search_engine_task = Task( + name="Academic Web Search", + description="\n".join( + [ + "The task is to search the web for relevant and reliable resources about {topic} using the suggested search queries.", + "You must collect results from multiple queries to ensure diversity of perspectives and sources.", + "Here are the queries you can use to search:", + "{queries}", + "use unit title: {unit_title} and subtopic title: {subtopic_title} to complete the output and search correctly for each query.", + "For each query, retrieve at least 1 link and a maximum of {no_links} links only.", + "Prioritize scholarly, academic, and educational resources (journals, books, institutional websites).", + "Filter out irrelevant, suspicious, or low-quality results (e.g., personal blogs, spam, commercial ads).", + "Apply score filtering: ignore results with a confidence score lower than {score_th}.", + "The final results will serve as the knowledge base for building trusted, high-quality content, ", + "with a focus on supporting the course’s educational objectives and cultural enrichment goals.", + "with a focus on supporting the course’s educational objectives and cultural enrichment goals.", + "Get sites that newer than 2021.", + "Validate URLs before retrieving them and if it is not valid dont retieve", + + ] + ), + expected_output=( + "Return ONLY a valid UTF-8 safe Python dictionary (no markdown, no code blocks). " + "It must be directly parsable with ast.literal_eval. Use this format:\n\n" + "{\n" + ' "unit_title": "...",\n' + ' "subtopic_title": "...",\n' + ' "query": "...",\n' + ' "results": [\n' + ' {"url": "...", "title": "...", "content": "...", "score": 0.91}\n' + " ]\n" + "}" + ), + output_json=SearchEngineOutput, + agent=search_engine_agent, + output_file="search_results.json", +) diff --git a/agents/final_outliners/__init__.py b/agents/final_outliners/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..20b25dc9ecf9ff2827506065d321e33910121152 --- /dev/null +++ b/agents/final_outliners/__init__.py @@ -0,0 +1,4 @@ +from .subtopics_generator import subtopics_generator_agent, subtopics_generation_task +from .validator import validator_agent, validation_task +# from .units_langchain import units_langchain_agent +from .units_langchain_1v import units_langchain1_agent \ No newline at end of file diff --git a/agents/final_outliners/subtopics_generator.py b/agents/final_outliners/subtopics_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..356fa5ef253755f48c82c5179041836dff500249 --- /dev/null +++ b/agents/final_outliners/subtopics_generator.py @@ -0,0 +1,132 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm_5m, llm_g +from schemas import FullCurriculumStructure + + +def subtopics_generator_agent(): + model = llm_g() + return Agent( + role="Arabic Educational Subtopic Generator (Subtopic Developer)", + goal=( + "Analyze the provided metadata ({topic}, {domain}, {audience}, {content_type}, {material_type}) " + "along with the CurriculumStructure (containing the main concept, General_objectives_of_the_plan, and list of units and their details). " + "For each unit — based on its title and details — generate list of coherent and progressive Arabic subtopics (axes). " + "These subtopics should represent a logical and educational flow from foundational understanding " + "to advanced application and creativity. " + "Each unit's subtopic should represent a specific stage of learning progression — starting from basic knowledge acquisition " + "and moving toward critical thinking and creative application. " + ## ADD + "Example:" + "- Unit: التفكير التصميمي وحل المشكلات" + "- Given Subtopics:" + " • ماذا يعنى حل المشكلة؟" + " • 4 خطوات لحل المشكلة" + " • الفرق بين التفكير التحليلي والتصميمي في حل المشكلات" + " • نهج التفكير التصميمي لحل المشكلات" + " • نماذج تطبيقية ناجحة في حل المشكلات بالتفكير التصميمي" + "- Observation: The first two are introductory — essential for understanding the later applied parts." + ## ADD + "Finally, ensure that all generated subtopics comply with the approved Arabic educational titling standards:\n" + "- Titles must be clear, direct, and precise, avoiding ambiguous or poetic phrasing.\n" + "- Avoid dual or compound titles; use a single, focused phrasing.\n" + "- Each title must reflect its actual educational content accurately.\n" + # "- Limited creativity is acceptable, but without exaggeration or metaphorical obscurity.\n" + "- Always adapt the title to the target audience’s cultural and cognitive level.\n" + "These standards ensure the linguistic clarity and educational value of all generated subtopics." + "Ensure that the subtopics are written as Arabic educational subheadings that align with the unit’s focus, " + "support conceptual growth, and maintain thematic coherence across the curriculum." + "Ensure all subtopics are contextually relevant to the unit's focus and contribute to the learner's comprehensive understanding." + ), + backstory=( + "You are a senior Arabic curriculum designer and learning progression specialist. " + # "You possess deep expertise in Arabic pedagogy and cognitive development frameworks such as Bloom’s Taxonomy. " + "You skillfully expand each educational unit into coherent Arabic subtopics that reflect a logical learning progression — " + "starting from basic understanding and moving toward higher-order thinking and creative application. " + "Each subtopic builds upon the previous one to form a cohesive and culturally relevant learning journey. " + "Your designs prioritize conceptual clarity, educational depth, and alignment with Arabic academic standards." + "You also ensure that each generated subtopic is research-backed and can be supported by credible educational materials available online, enabling later integration with content retrieval systems." + ## ADD + "You strictly follow the approved Arabic educational title formulation standards, ensuring clarity, precision, and appropriateness for the learner’s level." + ), + output_format=( + "Return valid JSON strictly following this structure:\n" + "{\n" + " 'main_concept': '...',\n" + " 'units': [\n" + " {\n" + " 'unit_number': 1,\n" + " 'title': 'الوحدة الأولى: ...',\n" + " 'focus': '...',\n" + " 'axes': [\n" + " {'axis_number': 1, 'title': '...', 'purpose': '...'},\n" + " {'axis_number': 2, 'title': '...', 'purpose': '...'},\n" + " {'axis_number': 3, 'title': '...', 'purpose': '...'},\n" + " {'axis_number': 4, 'title': '...', 'purpose': '...'},\n" + " {'axis_number': 5, 'title': '...', 'purpose': '...'}\n" + " ]\n" + " }, ...\n" + " ]\n" + "}" + ), + # llm=llm_open_c35, + llm=model, + allow_delegation=False, + verbose=True, + reasoning=True, + max_iter=100, +) + +# ============================================================ +# 🧩 3. Axis Generation Task +# ============================================================ + +def subtopics_generation_task(subtopics_generator_agent): + return Task( + description=( + "## GOAL\n" + "Expand each educational unit from the given file into number of structured Arabic subtopics: {units}. " + # "Each axis should develop the learner’s understanding progressively — starting with remembering, " + # "then understanding, applying, analyzing, and finally creating.\n" + "Each subtopic should represent a cognitive development stage — moving from foundational understanding to applied mastery.\n\n" + "## CONTEXT\n" + # "You are provided with a units_agent_conceptual JSON (containing the main concept, related concepts, and list of units). " + "You are provided with a CurriculumStructure JSON (containing the main concept, General_objectives_of_the_plan, and list of units and their details). " + # "Each unit includes a title and conceptual focus.\n\n" + "## STEPS\n" + "1. Read the CurriculumStructure carefully to understand the hierarchy and conceptual logic.\n" + "2. For each unit:\n" + " - Identify its title and details.\n" + " - Generate list of progressive subtopics that unfold logically.\n" + " - Each subtopic must include:\n" + " • 'subtopic_number': numerical order of the subtopic in the unit.\n" + " • 'title': Arabic title that feels like a subheading in a textbook.\n" + " • 'purpose': one-sentence explanation of the educational goal behind that subtopic.\n\n" + "3. For each generated subtopic\n" + " - Prefer subtopics that are well-established in educational or academic literature.\n" + " - Avoid overly niche or abstract titles that would lack sufficient learning resources.\n\n" + ## ADD + "4. Validate that each subtopic:\n" + " - Aligns with Arabic educational title standards (clarity, focus, precision, and appropriateness).\n" + " - Avoids dual titles, poetic phrasing, or abstract metaphors.\n\n" + "5. **Subtopics Title Quality & Language Validation**\n" + " - Each subtopic title is the learner’s first point of contact with the concept, so ensure the following:\n" + " Title length: concise (4–8 words preferred).\n" + " Clear and contextually linked to its unit or topic.\n" + " Written in tone appropriate for {audience}.\n" + " Avoids punctuation marks (– , ; : . ! ? , /) except for simple questions.\n" + " Represents one coherent idea — avoid compound or dual phrasing.\n" + " Avoids activity-based terms like 'ورشة عمل' or 'نشاط'.\n" + " Sounds realistic, teachable, and suitable for Arabic educational content.\n" + " can be in form of short sentence or a question\n\n" + "## NOTES\n" + # "- Make sure the subtopics flow naturally, covering introduction, comprehension, practice, and innovation.\n" + # "- The 5 axes should reflect a natural flow following Bloom’s taxonomy: Remember → Understand → Apply → Analyze → Create.\n" + "- Ensure the titles sound academic, culturally appropriate, and pedagogically progressive.\n" + "- Return the result as a valid FullCurriculumStructure JSON containing all units with their subtopics." + ), + expected_output="A FullCurriculumStructure JSON object containing all units and their respective list of subtopics each.", + agent=subtopics_generator_agent, + output_json=FullCurriculumStructure, +) diff --git a/agents/final_outliners/units_langchain.py b/agents/final_outliners/units_langchain.py new file mode 100644 index 0000000000000000000000000000000000000000..fd4aa9745cb74228ad07e2ad56ba2f9812f87640 --- /dev/null +++ b/agents/final_outliners/units_langchain.py @@ -0,0 +1,136 @@ +# # from langchain.agents import create_tool_calling_agent, AgentExecutor +# # from langchain.prompts import ChatPromptTemplate +# # from modules import model, llm_g +# # import json +# # import re +# # import pandas as pd + + +# # def units_langchain_agent(topic: str, units_number: int): +# # # الخطوة 1: نطلب من llm_g إنشاء Markdown plan +# # result = llm_g.call( +# # f"Create a structured {units_number}-unit study plan for the topic [{topic}]" +# # ) + +# # # الخطوة 2: الـ System Prompt الخاص بالاستخراج +# # system_prompt = """ +# # You will receive Markdown text representing a study plan. +# # Extract structured data and return it strictly in this JSON format: + +# # {{ +# # "topic": "", +# # "General_objectives_of_the_plan": ["string", "string", ...], +# # "total_units": , +# # "units": [ +# # {{ +# # "unit_number": 1, +# # "unit_title": "string", +# # "description": "string", +# # "unit_objectives": ["string", "string", ...], +# # "unit_main_topics": ["string", "string", ...] +# # }}, +# # ... +# # ] +# # }} + +# # Output **only valid JSON**, no explanations, markdown, or extra text. +# # """ + +# # # الخطوة 3: إنشاء prompt فيه متغير agent_scratchpad المطلوب +# # prompt = ChatPromptTemplate.from_messages( +# # [ +# # ("system", system_prompt), +# # ("user", "{input}"), +# # ("assistant", "{agent_scratchpad}"), # << الإضافة المطلوبة +# # ] +# # ) + +# # # الخطوة 4: إنشاء الـ Agent +# # tools = [] # مفيش Tools هنا +# # agent = create_tool_calling_agent(model, tools, prompt=prompt) + +# # # الخطوة 5: تنفيذ الـ Agent +# # agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) +# # response = agent_executor.invoke({"input": result, "agent_scratchpad": ""}) + +# # # الخطوة 6: استخراج النتيجة +# # raw_output = response.get("output", "").strip() + +# # # الخطوة 7: تنظيف وتحويل JSON +# # try: +# # structured_data = json.loads(raw_output) +# # except json.JSONDecodeError: +# # cleaned = re.sub(r"```json|```", "", raw_output).strip() +# # structured_data = json.loads(cleaned) + +# # return structured_data + + +# from modules import model, llm_g +# import json +# import re +# from langchain.agents import initialize_agent, AgentType + +# # from langchain.agents import create_agent + + +# def units_langchain1_agent(topic: str, units_number: int): +# llm = llm_g() +# # الخطوة 1: نطلب من llm_g إنشاء Markdown plan +# result = llm.call( +# f"Create a structured {units_number}-unit study plan for the topic [{topic}] focuses on logical sequencing and educational activities without delving into scientific or technical details, The plan should be similar to a training plan that can be presented in a workshop or short course." +# ) + +# # الخطوة 2: الـ System Prompt الخاص بالاستخراج +# system_prompt = """ +# extract structured data from Markdown text you will be given. + +# The JSON should have this exact structure: +# { +# "topic": "", +# "General_objectives_of_the_plan": ["string", "string", ...] +# "total_units": , +# "units": [ +# { +# "unit_number": 1, +# "unit_title": "string", +# "description": "string", +# "unit_objectives": ["string", "string", ...], +# "unit_main_topics": ["string", "string", ...] +# }, +# ... +# ] +# } + +# NOTE: +# if any descriptions are missing from the given file you can generate them after understanding the context. + +# Output **only valid JSON**, no explanations, markdown, or extra text. +# """ + +# # agent = create_agent( +# # model=model, +# # system_prompt=system_prompt, +# # ) +# model_open = model() +# agent = initialize_agent( +# system_prompt=system_prompt, +# llm=model_open, +# agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, +# verbose=True, +# ) + +# response = agent.invoke({"messages": [{"role": "user", "content": result}]}) + +# ####################### +# # الخطوة 6: استخراج النتيجة +# raw_output = response["messages"][1].content.strip() + +# # الخطوة 7: تنظيف وتحويل JSON +# try: +# structured_data = json.loads(raw_output) +# except json.JSONDecodeError: +# cleaned = re.sub(r"```json|```", "", raw_output).strip() +# structured_data = json.loads(cleaned) + +# return structured_data diff --git a/agents/final_outliners/units_langchain_1v.py b/agents/final_outliners/units_langchain_1v.py new file mode 100644 index 0000000000000000000000000000000000000000..9025dd0c169833fbee723ea0ac54de2af869a7de --- /dev/null +++ b/agents/final_outliners/units_langchain_1v.py @@ -0,0 +1,69 @@ +from modules import model, llm_g +import json +import re +from langchain_core.messages import SystemMessage, HumanMessage + +def units_langchain1_agent(topic: str, units_number: int): + + # ---------- Step 1 : Generate Markdown Plan ---------- + llm = llm_g() + + result = llm.call( + f""" +Create a structured {units_number}-unit study plan for the topic [{topic}]. + +Focus on logical sequencing and educational activities without deep technical details. +The plan should resemble a training plan suitable for a workshop or short course. + +Return the plan in clear Markdown. +""" + ) + + # ---------- Step 2 : System Prompt for JSON extraction ---------- + system_prompt = """ +Extract structured data from the Markdown text you will receive. + +Return JSON with this exact structure: + +{ + "topic": "", + "General_objectives_of_the_plan": ["string", "string"], + "total_units": , + "units": [ + { + "unit_number": 1, + "unit_title": "string", + "description": "string", + "unit_objectives": ["string"], + "unit_main_topics": ["string"] + } + ] +} + +Rules: +- If descriptions are missing you may generate them from context +- Output ONLY valid JSON +- No markdown +- No explanations +""" + + # ---------- Step 3 : Call Gemini via LangChain ---------- + llm_model = model() + + response = llm_model.invoke( + [ + SystemMessage(content=system_prompt), + HumanMessage(content=result), + ] + ) + + raw_output = response.content.strip() + + # ---------- Step 4 : Clean JSON ---------- + try: + structured_data = json.loads(raw_output) + except json.JSONDecodeError: + cleaned = re.sub(r"```json|```", "", raw_output).strip() + structured_data = json.loads(cleaned) + + return structured_data diff --git a/agents/final_outliners/validator.py b/agents/final_outliners/validator.py new file mode 100644 index 0000000000000000000000000000000000000000..273a21f6f5fc6717a44e5672b8d0e1723306f7fc --- /dev/null +++ b/agents/final_outliners/validator.py @@ -0,0 +1,142 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm_5m, llm_g +from schemas import ValidatedCurriculumOutput, validated_parser +from functools import cache + +def validator_agent(): + model = llm_g() + return Agent( + role="Arabic Educational Validator (Curriculum Quality & Integrity Specialist)", + goal=( + "Ensure that all educational units and subtopics are conceptually complete, logically sequenced, linguistically refined, " + "and pedagogically sound for Arabic learners. Validate and enhance each unit's structure to maintain educational coherence " + "and clarity while preserving Arabic authenticity. Ensure all titles are in Arabic only. and try to include the notes given in the structure." + ), + backstory=( + "You are a senior Arabic educational content validator and curriculum refinement expert. " + "You specialize in verifying conceptual completeness, depth, and sequencing of educational materials. " + "You understand Bloom’s taxonomy and Arabic pedagogical standards deeply. " + "You ensure all subtopics form a coherent, realistic, and progressive learning journey for the audience. " + "You maintain academic rigor and cultural authenticity while improving clarity and depth." + ), + output_format=( + "Return a valid JSON with the following structure:\n" + "{\n" + " 'main_concept': '...',\n" + " 'General_objectives_of_the_plan': '...',\n" + " 'validated_units': [\n" + " {\n" + " 'unit_number': 1,\n" + " 'title': '...',\n" + " 'focus': '...',\n" + " 'validated_axes': [\n" + " {\n" + " 'axis_number': 1,\n" + " 'original_title': '...',\n" + " 'validated_title': '...',\n" + " 'original_purpose': '...',\n" + " 'validated_purpose': '...',\n" + " 'status': 'kept' | 'rephrased' | 'added' | 'removed'\n" + " }\n" + " ]\n" + " }\n" + " ]\n" + "}" + ), + # llm=llm_open_c35, + llm=model, + allow_delegation=False, + verbose=True, + reasoning=True, + max_iter=100, + cache=False, +) + +def validation_task(validator_agent): + return Task( + description=( + "Validate, refine, and enhance all generated educational units and their subtopics (only those needing improvement). " + "Ensure conceptual completeness, logical sequencing, and pedagogical depth across all units.\n\n" + "===================================================\n" + "## INPUT\n" + "You will be given a FullCurriculumStructure JSON from axis_generator_agent and details: {units}. " + "NOTES may also be provided {note}. These notes contain additional instructions or required new content. " + "so think how to achieve this within the educational plan, should you add new subtopic or new unit with new subtopic or modify anything(sure without deleting), " + "The agent must interpret the notes and add the required new subtopics or axes to the appropriate unit. " + "Example: If the note says 'تناول الاستخدامات الحديثة لتحليل البيانات في محور واحد', " + "you must add a new subtopic titled 'الاستخدامات الحديثة لتحليل البيانات' or any academically suitable Arabic title, " + "place it in the correct unit, and mark its status as 'added'.\n\n" + "===================================================\n" + "PRIMARY OBJECTIVE\n" + "===================================================\n" + "Ensure each unit exhibits:\n" + "1- Full conceptual and pedagogical integration.\n" + "2- Logical sequencing of ideas.\n" + "3- Arabic educational clarity and realism.\n" + "4- Comprehensive coverage of all described relations, aspects, and intentions.\n" + "5- Ensure the unit name best expresses its content in professional Arabic.\n" + "6- Avoid punctuation marks (– , ; : . ! ? , /) except for simple questions.\n" + "7- If removing punctuation affects grammar, enhance the title for natural Arabic style.\n\n" + "===================================================\n" + "PHASE 1 — COMPREHENSIVENESS & DEPTH ANALYSIS\n" + "===================================================\n" + "1. Analyze all subtopics for each unit and unit details.\n" + "2. Identify missing conceptual foundations (definitions, context, prerequisites, motivation).\n" + "3. Add any necessary **introductory subtopics** with clear Arabic titles and purposes.\n" + "4. Detect and remove any redundant or overlapping axes.\n\n" + "===================================================\n" + "PHASE 2 — INTEGRATION & SEQUENCING VALIDATION\n" + "===================================================\n" + "1. Ensure smooth conceptual progression from basic to advanced.\n" + "2. Merge or rephrase redundant axes while preserving educational intent.\n" + "3. Add **bridging subtopics** if conceptual jumps exist.\n" + "4. Reorder axes if needed to restore logical flow.\n\n" + "===================================================\n" + "PHASE 3 — REALISM & COVERAGE VALIDATION\n" + "===================================================\n" + "1. Confirm every conceptual aspect in the unit’s focus and all details is represented.\n" + "2. Ensure subtopics are realistic and pedagogically valid for the target audience.\n" + "3. Add missing real-world or practical dimensions.\n" + "4. Maintain natural, professional Arabic phrasing.\n\n" + "===================================================\n" + "PHASE 4 — SUBTOPICS TITLE QUALITY & LANGUAGE VALIDATION\n" + "===================================================\n" + "4. Validate that each Subtopic:\n" + " - Aligns with Arabic educational title standards (clarity, focus, precision, and appropriateness).\n" + " - Avoids dual titles, poetic phrasing, or abstract metaphors.\n\n" + "5. **Subtopics Title Quality & Language Validation**\n" + " - Title length: concise (4–8 words preferred).\n" + " - Clear and contextually linked to its unit.\n" + " - Tone appropriate for {audience}.\n" + " - Avoids punctuation marks except for simple questions.\n" + " - Represents one coherent idea, not compound.\n" + " - Avoids activity-based terms like 'ورشة عمل' or 'نشاط'.\n" + " - Sounds realistic, teachable, and suitable for Arabic educational content.\n\n" + "===================================================\n" + "PHASE 4.1 — ARABIC LANGUAGE INTEGRITY ENFORCEMENT\n" + "===================================================\n" + "1. Ensure that all titles (unit and subtopic) are written entirely in Arabic script.\n" + "2. If a title contains any Latin (English) letters, numerals, or symbols, rephrase it fully into correct Modern Standard Arabic.\n" + "3. Maintain original meaning and clarity when translating or rephrasing into Arabic.\n" + "4. Titles must not contain any transliterations (e.g., 'Entrepreneurship', 'AI', 'Design Thinking'). Replace them with Arabic equivalents.\n" + "5. Reject any title that cannot be made purely Arabic, and provide a suitable Arabic substitute aligned with educational context.\n\n" + "===================================================\n" + "PHASE 5 — OUTPUT STRUCTURE\n" + "===================================================\n" + "Return validated curriculum in JSON format with units, subtopics, and validation status.\n\n" + "===================================================\n" + "## NOTES\n" + "- Keep phrasing in professional Modern Standard Arabic.\n" + "- Avoid dual, poetic, or abstract titles.\n" + "- Ensure all outputs are grammatically correct and stylistically natural Arabic.\n" + "- Titles must not include any English or foreign words." + "- don't let the added notes destroy any of the curriculum structur only add what is needed." + ), + # guardrail="", + expected_output="A JSON object showing validated units and subtopics with Arabic-only titles.", + output_json=ValidatedCurriculumOutput, + output_parser=validated_parser, + agent=validator_agent, +) diff --git a/agents/outliner_phase/Deep_Semantic_Topics_Generator_1.py b/agents/outliner_phase/Deep_Semantic_Topics_Generator_1.py new file mode 100644 index 0000000000000000000000000000000000000000..813791e87ee900fbed794c644dc84470da6c4481 --- /dev/null +++ b/agents/outliner_phase/Deep_Semantic_Topics_Generator_1.py @@ -0,0 +1,121 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm +from schemas import CurriculumOutline, DeepSemanticTopicsOutput + +# --------------------------- +# === Agent 1: Deep Semantic Topics Generator (Full Coverage + Metadata Adaptive) === +# --------------------------- + +def research_agent(): + model = llm() + return Agent( + role="Deep Semantic Topics Generator (Educational Research Agent)", + goal="\n".join( + [ + "Generate as many interconnected and realistic Arabic topics as possible for the curriculum title: {topic}.", + "Do NOT divide topics into units — just produce a comprehensive standalone list of high-quality topics.", + "Your mission is to achieve *maximum conceptual and practical coverage* of the topic: cover every visible and implicit meaning embedded in the title: {topic}.", + "Interpret the topic deeply and explore all related sub-concepts, tools, methods, applications, personal skills, frameworks, real-world examples, and future trends.", + "No aspect of the topic should remain uncovered — ensure total thematic coverage from theory to practice.", + # === Adaptation & Metadata awareness === + "Always adapt to metadata: domain ({domain}), content_type ({content_type}), audience ({audience}), material_type ({material_type}).", + "Adjust tone, complexity, and depth according to the audience and domain type.", + "Apply DNA methodology explicitly in topic design:", + "- material_type:", + "-- Conceptual: theories, principles, abstract knowledge.", + "-- Procedural: step-by-step instructions or methods.", + "-- Structural: frameworks, models, or system views.", + "-- Personal: learner reflection, skills, or self-development aspects.", + "-- Realistic: real-world examples, applications, or case studies.", + "- content_type:", + "-- Educational → for structured learning (e.g., lessons, modules).", + "-- Awareness → for informative or awareness-raising purposes (e.g., campaigns, workshops).", + "-- Training → for hands-on skill development (e.g., exercises, applied learning).", + # === Learning progression === + "Ensure logical educational progression throughout the topics:", + " 1️⃣ Start with conceptual understanding (definitions, foundations, importance).", + " 2️⃣ Then practical tools and applications (methods, workflows, tools).", + " 3️⃣ Then analysis and real-world case studies (evaluation, lessons learned).", + " 4️⃣ End with strategies, integration, and innovation (advanced synthesis, policies, or future directions).", + # === Title & Content validation === + "Each topic must be realistic and searchable in Arabic or bilingual educational sources.", + "Before finalizing each topic, mentally confirm that real Arabic educational content exists online for it.", + "Reject or replace any vague, repetitive, or non-educational topics.", + "Avoid vague or redundant points — every topic must be unique and non-overlapping.", + # === Arabic educational title standards === + "Follow Arabic educational title standards strictly:", + " - Clear, concise, and direct phrasing (no metaphors or poetic language).", + " - Titles should be between 4–7 words long.", + " - Represent a clear learning focus, suitable for the target audience ({audience}).", + " - Example good: مهارات التفكير النقدي واتخاذ القرار", + " - Example bad: رحلة العقل نحو التفكير النقدي وصناعة القرار", + # === Output schema === + "For each topic, provide the following JSON fields exactly:", + " - title: Arabic, concise and direct", + " - description: 1–2 lines Arabic summarizing the topic", + " - dna: one of [Conceptual, Procedural, Structural, Personal, Realistic] and [Educational, Awareness, Training]", + " - suggested_unit: integer (1–4) showing depth progression (1=basic → 4=advanced)", + " - search_terms: 3–6 Arabic keywords relevant to the topic", + " - source: optional Arabic or bilingual educational source name/title (if known)", + # === Output constraints === + "Output must be a valid JSON list (no commentary, no explanations).", + "List must be ordered by suggested_unit (1 → 4) to reflect gradual learning progression.", + ] + ), + backstory="\n".join( + [ + "You are a senior educational researcher and curriculum architect with deep expertise in Arabic educational design and semantic topic analysis.", + "You deeply interpret the input topic — uncovering both its explicit and implicit meanings to ensure total thematic coverage.", + "You create coherent, realistic, and pedagogically structured topic sets that form a logical educational progression.", + "Each topic you design is grounded in actual Arabic or bilingual educational resources, ensuring that every subject can be realistically taught or studied.", + "You think step by step before finalizing outputs — validating realism, logical sequence, and online content availability.", + "Your work directly feeds the downstream Outline Agent, so clarity, precision, and educational completeness are essential in every topic you produce.", + ] + ), + allow_delegation=False, + llm=model, + reasoning=True, + verbose=True, + max_reasoning_attempts=4, + max_iter=100, +) + +# --------------------------- +# === Task 1: Generate Deep & Comprehensive Topics === +# --------------------------- + +def research_task(research_agent): + return Task( + description="\n".join( + [ + "Generate a comprehensive, realistic, and logically structured list of Arabic topics for {topic}.", + "Ensure full semantic coverage — address both the explicit and implicit meanings of the topic title to cover all relevant dimensions and perspectives.", + "Include topics that collectively span all DNA dimensions and metadata parameters: content_type ({content_type}) and material_type ({material_type}).", + "Each topic must be unique, clear, and pedagogically meaningful — avoiding repetition, overlap, or vague expressions.", + "All topics must be realistic and supported by potential Arabic or bilingual educational resources available online.", + "Ensure educational progression: begin with conceptual foundations and definitions, then move through methods and tools, practical applications, case studies, and finally strategic or innovative insights.", + "Maintain alignment with Arabic educational title standards: concise, direct, single-part titles with clear learning focus.", + "Output must be a valid JSON list following the schema below — no explanations, introductions, or extra commentary.", + ] + ), + expected_output=( + "A valid JSON list ordered by suggested_unit ascending:\n" + "[\n" + " {\n" + ' "title": "string (Arabic)",\n' + ' "description": "string (1–2 lines in Arabic)",\n' + ' "dna": "Conceptual|Procedural|Structural|Personal|Realistic",\n' + ' "suggested_unit": 1|2|3|4,\n' + ' "search_terms": ["string (Arabic keyword)", ...],\n' + ' "source": "string (optional)"\n' + " }, ...\n" + "]\n" + "The output must be valid JSON, without markdown formatting or code blocks." + ), + params={"min_topics": 10, "max_topics": 30}, # expand to ensure deeper coverage + output_json=DeepSemanticTopicsOutput, + # output_file=os.path.join(output_dir, "deep_semantic_topics.json"), + agent=research_agent, +) diff --git a/agents/outliner_phase/__init__.py b/agents/outliner_phase/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f146290fa981fe93a7cb53afa125472a058b2877 --- /dev/null +++ b/agents/outliner_phase/__init__.py @@ -0,0 +1,4 @@ +from .Deep_Semantic_Topics_Generator_1 import research_agent, research_task +from .topic_expander_2 import structure_agent, structure_task +from .module_organizer_3 import designer_agent, design_task +from .review_agent_4 import review_agent, review_task diff --git a/agents/outliner_phase/module_organizer_3.py b/agents/outliner_phase/module_organizer_3.py new file mode 100644 index 0000000000000000000000000000000000000000..500ad38f485704654743133079292a3b5d7de9ed --- /dev/null +++ b/agents/outliner_phase/module_organizer_3.py @@ -0,0 +1,49 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm +from schemas import CurriculumOutline + + +# --------------------------- +# === Agent 3: Module Organizer === +# --------------------------- +def designer_agent(): + model = llm() + return Agent( + role="Module Organizer Agent (Group into 4 modules)", + goal=( + "استلم الـ20 محورًا من Agent2. نظّمهم إلى **4 وحدات**، كل وحدة تحتوي على **5 مواضيع** " + "مرتبة متسلسلة ومنطقية. أعطِ كل وحدة عنوانًا واضحًا ومعبّرًا (module_title بالعربية)، " + "وقصّر عناوين المواضيع بحيث تكون جاهزة للاستخدام في الـ Curriculum (كل موضوع عبارة " + "عن جملة قصيرة أو عبارة). أعد صياغة المواضيع إن لزم لتكون متجانسة معًا." + ), + backstory=( + "أنت كبير مصممي مناهج. مهمتك إخراج المخرجات بصيغة CurriculumOutline المتوافقة مع الـ schema." + ), + llm=model, + allow_delegation=False, + verbose=True, +) + + +def design_task(designer_agent): + return Task( + description="تنظيم المحاور العشرين إلى 4 وحدات × 5 مواضيع في كل وحدة.", + expected_output=( + "A valid JSON object with this structure:\n" + "{\n" + ' "title": "string (Arabic curriculum title)",\n' + ' "units": [\n' + " {\n" + ' "unit_title": "string (Arabic, must reflect the title)",\n' + ' "topics": ["topic1", "topic2", "topic3", "topic4", "topic5"]\n' + " }, ... (4 units total)\n" + " ]\n" + "}\n" + "The output must be valid JSON, without markdown formatting or code blocks." + ), + output_json=CurriculumOutline, + # output_file=os.path.join(output_dir, "curriculum_outline.json"), + agent=designer_agent, +) diff --git a/agents/outliner_phase/review_agent_4.py b/agents/outliner_phase/review_agent_4.py new file mode 100644 index 0000000000000000000000000000000000000000..35e65aac15c0b7105f5569bd169d1f50d022b310 --- /dev/null +++ b/agents/outliner_phase/review_agent_4.py @@ -0,0 +1,38 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm +from schemas import CurriculumOutline + +# --------------------------- +# === Agent 4: Final Reviewer (اختياري) === +# --------------------------- +def review_agent(): + model = llm() + return Agent( + role="Final Review Agent", + goal=( + "راجع الناتج النهائي (4 وحدات × 5 مواضيع). صحح اللغة، احذف التكرار، وحسّن العناوين لتكون واضحة " + "وتتماشى مع الأسلوب التدريبي. تأكد أن 'title' يطابق الـ topic المدخل (بالعربية)." + ), + backstory=( + "أنت محرر تعليمي يركز على وضوح الصياغة والاتساق. لا تغيّر المحتوى الجوهري، فقط حسن الصياغة." + ), + llm=model, + allow_delegation=False, + verbose=True, +) + + +def review_task(review_agent): + return Task( + description="مراجعة نهائية لغوية وتنسيقية للـ Curriculum النهائي.", + expected_output=( + "Final polished JSON identical in structure to CurriculumOutline, " + "with clear Arabic titles and consistent formatting. " + "The output must be valid JSON, without markdown formatting or code blocks." + ), + output_json=CurriculumOutline, + # output_file=os.path.join(output_dir, "curriculum_outline_final.json"), + agent=review_agent, +) diff --git a/agents/outliner_phase/topic_expander_2.py b/agents/outliner_phase/topic_expander_2.py new file mode 100644 index 0000000000000000000000000000000000000000..6c67613e03dfdbe1fb72c19ec9ce2f63c9e3ab7b --- /dev/null +++ b/agents/outliner_phase/topic_expander_2.py @@ -0,0 +1,46 @@ +from crewai import Agent, Task, Crew, Process, LLM +import os +import json +from modules import llm +from schemas import ExpandedTopicsList + +# --------------------------- +# === Agent 2: Expander to 20 Topics === +# --------------------------- +def structure_agent(): + model = llm() + return Agent( + role="Expander to 20 Topics Agent (Structure & Relations)", + goal=( + "استلم قائمة المحاور المبدئية من Agent1. راجعها لإزالة التكرار ودمج المتقارب منها، " + "ثم أكمل ووسع القائمة لتصل إلى **بالضبط 20 محورًا** مترابطًا. " + "لكل محور اكتب وصفًا تفصيليًا 3-5 سطور يشرح محتوى المحور، ولماذا هو مهم، " + "واذكر علاقات واضحة بين المحاور (مثال: 'مرتبط بـ X لأن...'، أو 'يأتي بعد Y لأنه...'). " + "لا تقسم الوحدات بعد — فقط اضمن اتساق وترابط المحاور الـ20." + ), + backstory=( + "أنت مصمم تعليمي بارع في بناء التسلسل المنطقي بين الموضوعات. مهمتك تحويل قائمة أولية " + "إلى خريطة معرفية من 20 محورًا." + ), + llm=model, + allow_delegation=False, + verbose=True, +) + + +def structure_task(structure_agent): + return Task( + description="تنظيف القائمة السابقة وتوسيعها لتصل إلى 20 محورًا مترابطًا مع وصف مفصل.", + expected_output=( + "A JSON list of exactly 20 items:\n" + "[\n" + ' { "title": "string (topic title)",\n' + ' "description": "string (3–5 lines)",\n' + ' "relation": "string (e.g., related to X because...)" }\n' + "]\n" + "The output must be valid JSON, without markdown formatting or code blocks." + ), + output_json=ExpandedTopicsList, + # output_file=os.path.join(output_dir, "expanded_20_topics.json"), + agent=structure_agent, +) diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..e8c8d0d2d9d2147bc46bf766e4545bf77a93e671 --- /dev/null +++ b/app.py @@ -0,0 +1,106 @@ +import os +import shutil + +# Force HOME to /tmp so CrewAI doesn't try writing to /.local +os.environ["HOME"] = "/tmp" +os.environ["XDG_DATA_HOME"] = "/tmp/.local/share" +os.environ["CREWAI_STORAGE_PATH"] = "/tmp/crewai_data" +os.environ["CREWAI_HOME"] = "/tmp/crewai_home" + +# Create safe directories +os.makedirs("/tmp/.local/share", exist_ok=True) +os.makedirs("/tmp/crewai_data", exist_ok=True) +os.makedirs("/tmp/crewai_home", exist_ok=True) +from crewai import Crew, Process +import json +from fastapi import FastAPI +from routers import ( + objective_route, + outliner_route, + outcome_route, + introduction_route, + source_route, + scraper_route, + final_outlines_before_content_route, + content_generation_route, + quiz_route, + details_before_content_route, +) +from routers.books import ( + units_structure_route, + book_structure_route, + book_planning_route, + rag_route, + rag_automation_route, + book_content, + rag_admin, + queries_route, + search_route, + rag_sources, + units_intro, + book_enrichment, + reference, +) + + +app = FastAPI(title="AI Agent Project", version="1.0.0") + +app.include_router(outliner_route.router) +app.include_router(objective_route.router) +app.include_router(outcome_route.router) +app.include_router(introduction_route.router) +# app.include_router(source_route.router) +# app.include_router(scraper_route.router) +app.include_router(final_outlines_before_content_route.router) +app.include_router(content_generation_route.router) +app.include_router(quiz_route.router) +app.include_router(details_before_content_route.router) +app.include_router(units_structure_route.router) +app.include_router(book_structure_route.router) +app.include_router(book_planning_route.router) +app.include_router(rag_route.router) +app.include_router(book_content.router) +app.include_router(rag_automation_route.router) +app.include_router(rag_admin.router) +app.include_router(queries_route.router) +app.include_router(search_route.router) +app.include_router(rag_sources.router) +app.include_router(units_intro.router) +app.include_router(book_enrichment.router) +app.include_router(reference.router) + +# Clear Cashe +@app.get("/reset-cache") +def reset_cache(): + + paths = [ + "/tmp/crewai_data", + "/tmp/crewai_home", + "/tmp/.local/share", + ] + + for path in paths: + if os.path.exists(path): + shutil.rmtree(path, ignore_errors=True) + + os.makedirs("/tmp/crewai_data", exist_ok=True) + os.makedirs("/tmp/crewai_home", exist_ok=True) + os.makedirs("/tmp/.local/share", exist_ok=True) + + return {"message": "✅ Cache cleared successfully."} + + +# Example root endpoint +@app.get("/") +def read_root(): + return {"message": "Welcome to AI Agent Project API 🚀"} + + +# ------------------------ +# ✅ نقطة تشغيل السيرفر +# ------------------------ + +# if __name__ == "__main__": +# import uvicorn + +# uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True) diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2a235a433662472b87c76071b9382f3848d769a2 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1,9 @@ +from .content_crew_config import safe_run_cached +from .content_services import generate_course_content +from .content_converter_service import convert_dict_to_html +from .quiz_converter_service import convert_questions_dict_to_html +from .details_before_content_converter import render_course_html +from .books.planning_services import run_planning_on_outline +from .books.content_services import process_book +from .books.search_service import run_search_pipeline_parallel +from .books.book_content_converter_service import convert_dict_to_html diff --git a/core/books/book_content_converter_service copy.py b/core/books/book_content_converter_service copy.py new file mode 100644 index 0000000000000000000000000000000000000000..afc609a5735f7705bd4dbb05eb003e280f08a73d --- /dev/null +++ b/core/books/book_content_converter_service copy.py @@ -0,0 +1,1031 @@ +# -*- coding: utf-8 -*- +""" +Convert structured Arabic book JSON to a readable HTML with: +1) المحتوى قبل المراجعة +2) المحتوى بعد المراجعة +3) الشكل الجديد (عرض نهائي منسّق) + +يدعم شكلين من البيانات: +- الشكل القديم: chapters -> sections -> subsections +- الشكل الحالي: chapters -> sections -> plans (dict) +""" + +import argparse +import html +import json +import os +import re +from collections import OrderedDict +from datetime import datetime + + +DEFAULT_INPUT = "/mnt/data/response_1770559830366.json" +DEFAULT_OUTPUT = "/mnt/data/dist/index_before_after.html" + + +def slugify(s: str) -> str: + s = (s or "").strip().lower() + s = re.sub(r"\s+", "-", s) + s = re.sub(r"[^\w\u0600-\u06FF\-]+", "", s) # Arabic + alnum + _ + s = re.sub(r"-{2,}", "-", s).strip("-") + return s or "section" + + +def safe_html(text: str) -> str: + return html.escape(text or "") + + +def parse_reference_link(ref: str): + """ + يرجّع: + - النص بدون رابط + - الرابط إن وُجد + """ + ref = (ref or "").strip() + if not ref: + return "", "" + + # الحالة الأساسية: "متاح على: " + m = re.search(r"متاح\s+على:\s*(https?://\S+)", ref) + if m: + url = m.group(1).rstrip(").،,؛") + text_before = ref[: m.start()].strip() + text = f"{text_before} (متاح على:)" if text_before else "متاح على:" + return text, url + + # fallback: أي URL داخل النص + m2 = re.search(r"(https?://\S+)", ref) + if m2: + url = m2.group(1).rstrip(").،,؛") + text = ref.replace(m2.group(1), "").strip() or "مرجع" + return text, url + + return ref, "" + + +def render_ref_li(ref: str, count: int | None = None) -> str: + text, url = parse_reference_link(ref) + link_html = ( + f' لينك' + if url + else "" + ) + count_html = ( + f" ×{count}" if count is not None else "" + ) + return f"
  • {safe_html(text)}{link_html}{count_html}
  • " + + +def render_markdownish(text: str) -> str: + """ + تحويل بسيط للنص: + - #, ##, ###... إلى عناوين + - **bold** إلى strong + - سطور فارغة تفصل الفقرات + - القوائم (- / *) إلى
    • + """ + if not text: + return "

      لا يوجد محتوى.

      " + + escaped = safe_html(text) + escaped = re.sub(r"\*\*(.+?)\*\*", r"\1", escaped) + + lines = escaped.splitlines() + out = [] + paragraph_buf = [] + list_buf = [] + + def flush_paragraph(): + nonlocal paragraph_buf + if paragraph_buf: + out.append("

      " + "
      ".join(paragraph_buf).strip() + "

      ") + paragraph_buf = [] + + def flush_list(): + nonlocal list_buf + if list_buf: + out.append("
        " + "".join(f"
      • {x}
      • " for x in list_buf) + "
      ") + list_buf = [] + + for ln in lines: + raw = ln.strip() + if not raw: + flush_list() + flush_paragraph() + continue + + # headings + if raw.startswith("###### "): + flush_list() + flush_paragraph() + out.append(f"
      {raw[7:].strip()}
      ") + continue + if raw.startswith("##### "): + flush_list() + flush_paragraph() + out.append(f"
      {raw[6:].strip()}
      ") + continue + if raw.startswith("#### "): + flush_list() + flush_paragraph() + out.append(f"

      {raw[5:].strip()}

      ") + continue + if raw.startswith("### "): + flush_list() + flush_paragraph() + out.append(f"

      {raw[4:].strip()}

      ") + continue + if raw.startswith("## "): + flush_list() + flush_paragraph() + out.append(f"

      {raw[3:].strip()}

      ") + continue + if raw.startswith("# "): + flush_list() + flush_paragraph() + out.append(f"

      {raw[2:].strip()}

      ") + continue + + # bullets + if raw.startswith("- ") or raw.startswith("* "): + flush_paragraph() + list_buf.append(raw[2:].strip()) + else: + flush_list() + paragraph_buf.append(raw) + + flush_list() + flush_paragraph() + return "\n".join(out) + + +def merge_sources(item: dict) -> list: + """دمج المصادر القديمة والجديدة بدون تكرار.""" + seen = OrderedDict() + for key in ("sources", "sources_reviewed"): + for src in item.get(key, []) or []: + s = str(src).strip() + if s: + seen[s] = 1 + return list(seen.keys()) + + +def normalize_book(book: dict) -> dict: + """ + توحيد شكل البيانات لشكل داخلي واحد: + chapters -> sections -> items + item has: + title, purpose, difficulty, est_pages, before, after, final, status, sources + """ + normalized = { + "book_title": book.get("book_title", "كتاب"), + "book_description": book.get("book_description", ""), + "target_audience": book.get("target_audience", ""), + "chapters": [], + } + + for ch in book.get("chapters", []) or []: + ch_title = ch.get("chapter_title", "فصل") + out_ch = { + "chapter_title": ch_title, + "chapter_intro": ch.get("chapter_intro", {}), + "sections": [], + } + + for sec in ch.get("sections", []) or []: + sec_title = sec.get("section_title") or sec.get("title") or "قسم" + out_sec = { + "section_title": sec_title, + "overview": sec.get("overview", ""), + "section_intro_paragraph": ( + sec.get("section_intro_paragraph") or sec.get("section_intro") or "" + ).strip(), + "section_closing_paragraph": ( + sec.get("section_closing_paragraph") + or sec.get("section_outro") + or sec.get("section_closing") + or "" + ).strip(), + "items": [], + } + + # الشكل الحالي: plans = dict + plans = sec.get("plans") + if isinstance(plans, dict) and plans: + for plan_title, plan in plans.items(): + before = plan.get("generated_content", "") + after = plan.get("generated_content_reviewed") or before + out_sec["items"].append( + { + "title": plan_title, + "purpose": plan.get("core_idea", ""), + "difficulty": plan.get("depth_level", ""), + "est_pages": plan.get("estimated_pages", ""), + "before": before, + "after": after, + "final": after, # الشكل الجديد يعتمد النسخة بعد المراجعة + "status": plan.get("content_status") + or plan.get("status", ""), + "sources": merge_sources(plan), + "chunks": (plan.get("rag_context") or {}).get("chunks", []), + } + ) + + # الشكل القديم: subsections = list + subs = sec.get("subsections") + if isinstance(subs, list) and subs: + for sub in subs: + before = sub.get("generated_content", "") + after = sub.get("generated_content_reviewed") or before + out_sec["items"].append( + { + "title": sub.get("title", "عنوان فرعي"), + "purpose": sub.get("purpose", ""), + "difficulty": sub.get("difficulty", ""), + "est_pages": sub.get("est_pages", ""), + "before": before, + "after": after, + "final": after, + "status": sub.get("content_status", ""), + "sources": merge_sources(sub), + "chunks": (sub.get("rag_context") or {}).get("chunks", []), + "enrichment": plan.get("enrichment_meta", []), + } + ) + + out_ch["sections"].append(out_sec) + + normalized["chapters"].append(out_ch) + + return normalized + + +def collect_references(book: dict) -> OrderedDict: + """ + تجميع المراجع من: + - sources / sources_reviewed + - rag_context.chunks[].reference_apa + (في plans أو subsections) + """ + refs = OrderedDict() + + def add_ref(ref: str): + ref = (ref or "").strip() + if not ref: + return + refs.setdefault(ref, 0) + refs[ref] += 1 + + for ch in book.get("chapters", []) or []: + for sec in ch.get("sections", []) or []: + # plans + plans = sec.get("plans", {}) + if isinstance(plans, dict): + for _, item in plans.items(): + for r in item.get("sources", []) or []: + add_ref(str(r)) + for r in item.get("sources_reviewed", []) or []: + add_ref(str(r)) + rag = item.get("rag_context") or {} + for chunk in rag.get("chunks", []) or []: + add_ref(chunk.get("reference_apa", "")) + + # subsections + for sub in sec.get("subsections", []) or []: + for r in sub.get("sources", []) or []: + add_ref(str(r)) + for r in sub.get("sources_reviewed", []) or []: + add_ref(str(r)) + rag = sub.get("rag_context") or {} + for chunk in rag.get("chunks", []) or []: + add_ref(chunk.get("reference_apa", "")) + + return refs + + +def convert_dict_to_html(raw_book: dict) -> str: + book = normalize_book(raw_book) + + title = book.get("book_title", "كتاب") + desc = book.get("book_description", "") + audience = book.get("target_audience", "") + generated_at = datetime.now().strftime("%Y-%m-%d %H:%M") + + toc_items = [] + blocks = [] + + def add_toc(level: int, text: str, anchor: str): + toc_items.append((level, text, anchor)) + + book_anchor = f"book-{slugify(title)}" + add_toc(1, title, book_anchor) + + blocks.append( + f""" +
      +
      Book JSON Viewer
      +

      {safe_html(title)}

      + {f'

      {safe_html(desc)}

      ' if desc else ''} + {f'

      الفئة المستهدفة: {safe_html(audience)}

      ' if audience else ''} +
      + + +
      +
      +""" + ) + + item_counter = 0 + + for ci, ch in enumerate(book.get("chapters", []) or [], 1): + ch_title = ch.get("chapter_title", f"فصل {ci}") + ch_anchor = f"ch-{ci}-{slugify(ch_title)}" + add_toc(2, f"{ci}. {ch_title}", ch_anchor) + + intro = ch.get("chapter_intro") or {} + intro_hook = intro.get("hook_idea", "") + intro_reason = intro.get("why_this_chapter_now", "") + + blocks.append( + f""" +
      +
      +

      {safe_html(ch_title)}

      + {f'

      تمهيد: {safe_html(intro_hook)}

      ' if intro_hook else ''} + {f'

      لماذا الآن: {safe_html(intro_reason)}

      ' if intro_reason else ''} +
      +""" + ) + + for si, sec in enumerate(ch.get("sections", []) or [], 1): + sec_title = sec.get("section_title", f"قسم {si}") + sec_anchor = f"{ch_anchor}-sec-{si}-{slugify(sec_title)}" + add_toc(3, f"{ci}.{si} {sec_title}", sec_anchor) + section_intro = sec.get("section_intro_paragraph", "") + section_outro = sec.get("section_closing_paragraph", "") + + section_intro_html = ( + f""" +
      +
      افتتاحية القسم
      +
      {render_markdownish(section_intro)}
      +
      + """ + if section_intro + else "" + ) + + section_outro_html = ( + f""" +
      +
      خاتمة القسم
      +
      {render_markdownish(section_outro)}
      +
      + """ + if section_outro + else "" + ) + + blocks.append( + f""" +
      +

      {safe_html(sec_title)}

      + {f'

      {safe_html(sec.get("overview",""))}

      ' if sec.get("overview") else ''} + {section_intro_html} +""" + ) + + for ti, item in enumerate(sec.get("items", []) or [], 1): + item_counter += 1 + it_title = item.get("title", f"عنصر {ti}") + item_anchor = f"{sec_anchor}-item-{ti}-{slugify(it_title)}" + add_toc(4, f"{ci}.{si}.{ti} {it_title}", item_anchor) + + before = item.get("before", "") + after = item.get("after", "") + final = item.get("final", after) + sources = item.get("sources", []) or [] + + meta = [] + if item.get("difficulty"): + meta.append(f"المستوى: {safe_html(str(item['difficulty']))}") + if item.get("est_pages"): + meta.append(f"تقدير الصفحات: {safe_html(str(item['est_pages']))}") + if item.get("status"): + meta.append(f"الحالة: {safe_html(str(item['status']))}") + + meta_line = " | ".join(meta) + + src_html = "" + if sources: + lis = "\n".join(render_ref_li(s) for s in sources) + + src_html = f""" +
      + المصادر ({len(sources)}) +
        {lis}
      +
      +""" + chunks = item.get("chunks", []) or [] + chunks_html = "" + + if chunks: + chunk_items = [] + for ch in chunks: + title = safe_html(str(ch.get("title", ""))) + author = safe_html(str(ch.get("author", ""))) + page = safe_html(str(ch.get("page_start", ""))) + url = safe_html(str(ch.get("source_url", ""))) + + link = f'لينك' if url else "" + + chunk_items.append( + f""" +
    • + {title}
      + المؤلف: {author}
      + الصفحة: {page} + {link} +
    • + """ + ) + + chunks_html = f""" +
      + مقاطع RAG المستخدمة ({len(chunks)}) +
        + {''.join(chunk_items)} +
      +
      + """ + + enrichment = item.get("enrichment", []) or [] + enrichment_html = "" + + if enrichment: + + enrich_items = [] + + for e in enrichment: + + text = str(e.get("enrichment_generated", "")).strip() + + if not text: + continue + + etype = e.get("enrichment_type", "") + + label = { + "knowledge_flash": "💡 ومضة معرفية", + "analytical_quote": "🧠 اقتباس تحليلي", + "case_study": "📊 دراسة حالة", + "case_story": "📖 قصة تطبيقية", + }.get(etype, etype) + + enrich_items.append(f""" +
    • + {safe_html(label)}
      + {render_markdownish(text)} +
    • + """) + + if enrich_items: + + enrichment_html = f""" +
      + إثراءات معرفية ({len(enrich_items)}) +
        + {''.join(enrich_items)} +
      +
      + """ + + tab_before = f"tab-before-{item_counter}" + tab_after = f"tab-after-{item_counter}" + tab_final = f"tab-final-{item_counter}" + + blocks.append( + f""" +
      +
      +

      {safe_html(it_title)}

      + {f'
      {safe_html(item.get("purpose",""))}
      ' if item.get("purpose") else ''} + {f'
      {meta_line}
      ' if meta_line else ''} +
      + +
      + + + +
      + +
      +
      المحتوى قبل المراجعة
      +
      {render_markdownish(before)}
      +
      + +
      +
      المحتوى بعد المراجعة
      +
      {render_markdownish(after)}
      +
      + +
      +
      الشكل الجديد (عرض نهائي)
      +
      +
      {safe_html(it_title)}
      +
      {render_markdownish(final)}
      +
      +
      + + {src_html} + {chunks_html} + {enrichment_html} + +
      +""" + ) + + if section_outro_html: + blocks.append(section_outro_html) + + blocks.append("
      ") # end section + + blocks.append("
      ") # end chapter + + # References + refs = collect_references(raw_book) + refs_anchor = "references" + add_toc(2, "المراجع", refs_anchor) + if refs: + sorted_refs = sorted(refs.items(), key=lambda x: (-x[1], x[0])) + refs_list = "\n".join(render_ref_li(ref, count) for ref, count in sorted_refs) + else: + refs_list = "
    • لا توجد مراجع داخل الملف.
    • " + + blocks.append( + f""" +
      +

      المراجع

      +

      تم جمعها آليًا من sources / sources_reviewed / reference_apa.

      +
        {refs_list}
      +
      +""" + ) + + toc_html = "\n".join( + f'
    • {safe_html(text)}
    • ' + for lvl, text, anchor in toc_items + ) + + html_out = f""" + + + + + {safe_html(title)} - قبل/بعد + + + +
      +
      +
      {safe_html(title)}
      +
      + + +
      +
      +
      + +
      + + +
      +
      +
      + {"".join(blocks)} +
      +
      +
      +
      + + + + +""" + return html_out diff --git a/core/books/book_content_converter_service.py b/core/books/book_content_converter_service.py new file mode 100644 index 0000000000000000000000000000000000000000..53fba6c3c72a029e63fcd152e113ef542336ca5a --- /dev/null +++ b/core/books/book_content_converter_service.py @@ -0,0 +1,1042 @@ +# -*- coding: utf-8 -*- +""" +Convert structured Arabic book JSON to a readable HTML with: +1) المحتوى قبل المراجعة +2) المحتوى بعد المراجعة +3) الشكل الجديد (عرض نهائي منسّق) + +يدعم شكلين من البيانات: +- الشكل القديم: chapters -> sections -> subsections +- الشكل الحالي: chapters -> sections -> plans (dict) +""" + +import argparse +import html +import json +import os +import re +from collections import OrderedDict +from datetime import datetime + + +DEFAULT_INPUT = "/mnt/data/response_1770559830366.json" +DEFAULT_OUTPUT = "/mnt/data/dist/index_before_after.html" + + +def slugify(s: str) -> str: + s = (s or "").strip().lower() + s = re.sub(r"\s+", "-", s) + s = re.sub(r"[^\w\u0600-\u06FF\-]+", "", s) # Arabic + alnum + _ + s = re.sub(r"-{2,}", "-", s).strip("-") + return s or "section" + + +def safe_html(text: str) -> str: + return html.escape(text or "") + + +def parse_reference_link(ref: str): + """ + يرجّع: + - النص بدون رابط + - الرابط إن وُجد + """ + ref = (ref or "").strip() + if not ref: + return "", "" + + # الحالة الأساسية: "متاح على: " + m = re.search(r"متاح\s+على:\s*(https?://\S+)", ref) + if m: + url = m.group(1).rstrip(").،,؛") + text_before = ref[: m.start()].strip() + text = f"{text_before} (متاح على:)" if text_before else "متاح على:" + return text, url + + # fallback: أي URL داخل النص + m2 = re.search(r"(https?://\S+)", ref) + if m2: + url = m2.group(1).rstrip(").،,؛") + text = ref.replace(m2.group(1), "").strip() or "مرجع" + return text, url + + return ref, "" + + +def render_ref_li(ref: str, count: int | None = None, ref_id: str | None = None) -> str: + text, url = parse_reference_link(ref) + + rid = slugify(ref_id or text) + + link_html = ( + f' لينك' + if url + else "" + ) + + count_html = ( + f" ×{count}" if count is not None else "" + ) + + return f""" +
    • + {safe_html(text)} + {link_html} + {count_html} +
    • +""" + + +def render_markdownish(text: str) -> str: + """ + تحويل بسيط للنص: + - #, ##, ###... إلى عناوين + - **bold** إلى strong + - سطور فارغة تفصل الفقرات + - القوائم (- / *) إلى
      • + """ + if not text: + return "

        لا يوجد محتوى.

        " + + escaped = safe_html(text) + escaped = re.sub(r"\*\*(.+?)\*\*", r"\1", escaped) + + lines = escaped.splitlines() + out = [] + paragraph_buf = [] + list_buf = [] + + def flush_paragraph(): + nonlocal paragraph_buf + if paragraph_buf: + out.append("

        " + "
        ".join(paragraph_buf).strip() + "

        ") + paragraph_buf = [] + + def flush_list(): + nonlocal list_buf + if list_buf: + out.append("
          " + "".join(f"
        • {x}
        • " for x in list_buf) + "
        ") + list_buf = [] + + for ln in lines: + raw = ln.strip() + if not raw: + flush_list() + flush_paragraph() + continue + + # headings + if raw.startswith("###### "): + flush_list() + flush_paragraph() + out.append(f"
        {raw[7:].strip()}
        ") + continue + if raw.startswith("##### "): + flush_list() + flush_paragraph() + out.append(f"
        {raw[6:].strip()}
        ") + continue + if raw.startswith("#### "): + flush_list() + flush_paragraph() + out.append(f"

        {raw[5:].strip()}

        ") + continue + if raw.startswith("### "): + flush_list() + flush_paragraph() + out.append(f"

        {raw[4:].strip()}

        ") + continue + if raw.startswith("## "): + flush_list() + flush_paragraph() + out.append(f"

        {raw[3:].strip()}

        ") + continue + if raw.startswith("# "): + flush_list() + flush_paragraph() + out.append(f"

        {raw[2:].strip()}

        ") + continue + + # bullets + if raw.startswith("- ") or raw.startswith("* "): + flush_paragraph() + list_buf.append(raw[2:].strip()) + else: + flush_list() + paragraph_buf.append(raw) + + flush_list() + flush_paragraph() + return "\n".join(out) + + +def merge_sources(item: dict) -> list: + """دمج المصادر القديمة والجديدة بدون تكرار.""" + seen = OrderedDict() + for key in ("sources", "sources_reviewed"): + for src in item.get(key, []) or []: + s = str(src).strip() + if s: + seen[s] = 1 + return list(seen.keys()) + + +def normalize_book(book: dict) -> dict: + """ + توحيد شكل البيانات لشكل داخلي واحد: + chapters -> sections -> items + item has: + title, purpose, difficulty, est_pages, before, after, final, status, sources + """ + normalized = { + "book_title": book.get("book_title", "كتاب"), + "book_description": book.get("book_description", ""), + "target_audience": book.get("target_audience", ""), + "chapters": [], + } + + for ch in book.get("chapters", []) or []: + ch_title = ch.get("chapter_title", "فصل") + out_ch = { + "chapter_title": ch_title, + "chapter_intro": ch.get("chapter_intro", {}), + "sections": [], + } + + for sec in ch.get("sections", []) or []: + sec_title = sec.get("section_title") or sec.get("title") or "قسم" + out_sec = { + "section_title": sec_title, + "overview": sec.get("overview", ""), + "section_intro_paragraph": ( + sec.get("section_intro_paragraph") or sec.get("section_intro") or "" + ).strip(), + "section_closing_paragraph": ( + sec.get("section_closing_paragraph") + or sec.get("section_outro") + or sec.get("section_closing") + or "" + ).strip(), + "items": [], + } + + # الشكل الحالي: plans = dict + plans = sec.get("plans") + if isinstance(plans, dict) and plans: + for plan_title, plan in plans.items(): + before = plan.get("generated_content", "") + after = plan.get("generated_content_reviewed") or before + out_sec["items"].append( + { + "title": plan_title, + "purpose": plan.get("core_idea", ""), + "difficulty": plan.get("depth_level", ""), + "est_pages": plan.get("estimated_pages", ""), + "before": before, + "after": after, + "final": after, # الشكل الجديد يعتمد النسخة بعد المراجعة + "status": plan.get("content_status") + or plan.get("status", ""), + "sources": merge_sources(plan), + "chunks": (plan.get("rag_context") or {}).get("chunks", []), + } + ) + + # الشكل القديم: subsections = list + subs = sec.get("subsections") + if isinstance(subs, list) and subs: + for sub in subs: + before = sub.get("generated_content", "") + after = sub.get("generated_content_reviewed") or before + out_sec["items"].append( + { + "title": sub.get("title", "عنوان فرعي"), + "purpose": sub.get("purpose", ""), + "difficulty": sub.get("difficulty", ""), + "est_pages": sub.get("est_pages", ""), + "before": before, + "after": after, + "final": after, + "status": sub.get("content_status", ""), + "sources": merge_sources(sub), + "chunks": (sub.get("rag_context") or {}).get("chunks", []), + } + ) + + out_ch["sections"].append(out_sec) + + normalized["chapters"].append(out_ch) + + return normalized + + +def collect_references(book: dict) -> OrderedDict: + """ + تجميع المراجع من: + - sources / sources_reviewed + - rag_context.chunks[].reference_apa + (في plans أو subsections) + """ + refs = OrderedDict() + + def add_ref(ref: str): + ref = (ref or "").strip() + if not ref: + return + refs.setdefault(ref, 0) + refs[ref] += 1 + + for ch in book.get("chapters", []) or []: + for sec in ch.get("sections", []) or []: + # plans + plans = sec.get("plans", {}) + if isinstance(plans, dict): + for _, item in plans.items(): + for r in item.get("sources", []) or []: + add_ref(str(r)) + for r in item.get("sources_reviewed", []) or []: + add_ref(str(r)) + rag = item.get("rag_context") or {} + for chunk in rag.get("chunks", []) or []: + add_ref(chunk.get("reference_apa", "")) + + # subsections + for sub in sec.get("subsections", []) or []: + for r in sub.get("sources", []) or []: + add_ref(str(r)) + for r in sub.get("sources_reviewed", []) or []: + add_ref(str(r)) + rag = sub.get("rag_context") or {} + for chunk in rag.get("chunks", []) or []: + add_ref(chunk.get("reference_apa", "")) + + return refs + + +def convert_dict_to_html(raw_book: dict) -> str: + book = normalize_book(raw_book) + + title = book.get("book_title", "كتاب") + desc = book.get("book_description", "") + audience = book.get("target_audience", "") + generated_at = datetime.now().strftime("%Y-%m-%d %H:%M") + + toc_items = [] + blocks = [] + + def add_toc(level: int, text: str, anchor: str): + toc_items.append((level, text, anchor)) + + book_anchor = f"book-{slugify(title)}" + add_toc(1, title, book_anchor) + + blocks.append( + f""" +
        +
        Book JSON Viewer
        +

        {safe_html(title)}

        + {f'

        {safe_html(desc)}

        ' if desc else ''} + {f'

        الفئة المستهدفة: {safe_html(audience)}

        ' if audience else ''} +
        + + +
        +
        +""" + ) + + item_counter = 0 + + for ci, ch in enumerate(book.get("chapters", []) or [], 1): + ch_title = ch.get("chapter_title", f"فصل {ci}") + ch_anchor = f"ch-{ci}-{slugify(ch_title)}" + add_toc(2, f"{ci}. {ch_title}", ch_anchor) + + intro = ch.get("chapter_intro") or {} + intro_hook = intro.get("hook_idea", "") + intro_reason = intro.get("why_this_chapter_now", "") + + blocks.append( + f""" +
        +
        +

        {safe_html(ch_title)}

        + {f'

        تمهيد: {safe_html(intro_hook)}

        ' if intro_hook else ''} + {f'

        لماذا الآن: {safe_html(intro_reason)}

        ' if intro_reason else ''} +
        +""" + ) + + for si, sec in enumerate(ch.get("sections", []) or [], 1): + sec_title = sec.get("section_title", f"قسم {si}") + sec_anchor = f"{ch_anchor}-sec-{si}-{slugify(sec_title)}" + add_toc(3, f"{ci}.{si} {sec_title}", sec_anchor) + section_intro = sec.get("section_intro_paragraph", "") + section_outro = sec.get("section_closing_paragraph", "") + + section_intro_html = ( + f""" +
        +
        افتتاحية القسم
        +
        {render_markdownish(section_intro)}
        +
        + """ + if section_intro + else "" + ) + + section_outro_html = ( + f""" +
        +
        خاتمة القسم
        +
        {render_markdownish(section_outro)}
        +
        + """ + if section_outro + else "" + ) + + blocks.append( + f""" +
        +

        {safe_html(sec_title)}

        + {f'

        {safe_html(sec.get("overview",""))}

        ' if sec.get("overview") else ''} + {section_intro_html} +""" + ) + + for ti, item in enumerate(sec.get("items", []) or [], 1): + item_counter += 1 + it_title = item.get("title", f"عنصر {ti}") + item_anchor = f"{sec_anchor}-item-{ti}-{slugify(it_title)}" + add_toc(4, f"{ci}.{si}.{ti} {it_title}", item_anchor) + + before = item.get("before", "") + after = item.get("after", "") + final = item.get("final", after) + sources = item.get("sources", []) or [] + + meta = [] + if item.get("difficulty"): + meta.append(f"المستوى: {safe_html(str(item['difficulty']))}") + if item.get("est_pages"): + meta.append(f"تقدير الصفحات: {safe_html(str(item['est_pages']))}") + if item.get("status"): + meta.append(f"الحالة: {safe_html(str(item['status']))}") + + meta_line = " | ".join(meta) + + src_html = "" + if sources: + lis = "\n".join(render_ref_li(s, ref_id=s) for s in sources) + + src_html = f""" +
        + المصادر ({len(sources)}) +
          {lis}
        +
        +""" + chunks = item.get("chunks", []) or [] + chunks_html = "" + + if chunks: + chunk_items = [] + for ch in chunks: + title = safe_html(str(ch.get("title", ""))) + author = safe_html(str(ch.get("author", ""))) + page = safe_html(str(ch.get("page_start", ""))) + url = safe_html(str(ch.get("source_url", ""))) + + link = f'لينك' if url else "" + + chunk_items.append( + f""" +
      • + {title}
        + المؤلف: {author}
        + الصفحة: {page} + {link} +
      • + """ + ) + + chunks_html = f""" +
        + مقاطع RAG المستخدمة ({len(chunks)}) +
          + {''.join(chunk_items)} +
        +
        + """ + tab_before = f"tab-before-{item_counter}" + tab_after = f"tab-after-{item_counter}" + tab_final = f"tab-final-{item_counter}" + + blocks.append( + f""" +
        +
        +

        {safe_html(it_title)}

        + {f'
        {safe_html(item.get("purpose",""))}
        ' if item.get("purpose") else ''} + {f'
        {meta_line}
        ' if meta_line else ''} +
        + +
        + + + +
        + +
        +
        المحتوى قبل المراجعة
        +
        {render_markdownish(before)}
        +
        + +
        +
        المحتوى بعد المراجعة
        +
        {render_markdownish(after)}
        +
        + +
        +
        الشكل الجديد (عرض نهائي)
        +
        +
        {safe_html(it_title)}
        +
        {render_markdownish(final)}
        +
        +
        + + {src_html} + {chunks_html} + +
        +""" + ) + + if section_outro_html: + blocks.append(section_outro_html) + + blocks.append("
        ") # end section + + blocks.append("
        ") # end chapter + + # References + refs = collect_references(raw_book) + refs_anchor = "references" + add_toc(2, "المراجع", refs_anchor) + if refs: + sorted_refs = sorted(refs.items(), key=lambda x: (-x[1], x[0])) + refs_list = "\n".join( + render_ref_li(ref, count, ref) for ref, count in sorted_refs + ) + else: + refs_list = "
      • لا توجد مراجع داخل الملف.
      • " + + blocks.append( + f""" +
        +

        المراجع

        +

        تم جمعها آليًا من sources / sources_reviewed / reference_apa.

        +
          {refs_list}
        +
        +""" + ) + + toc_html = "\n".join( + f'
      • {safe_html(text)}
      • ' + for lvl, text, anchor in toc_items + ) + + html_out = f""" + + + + + {safe_html(title)} - قبل/بعد + + + +
        +
        +
        {safe_html(title)}
        +
        + + +
        +
        +
        + +
        + + +
        +
        +
        + {"".join(blocks)} +
        +
        +
        +
        + + + + +""" + return html_out diff --git a/core/books/cohesion_service.py b/core/books/cohesion_service.py new file mode 100644 index 0000000000000000000000000000000000000000..874c316ea077b84d161ce78b048796986af7693c --- /dev/null +++ b/core/books/cohesion_service.py @@ -0,0 +1,173 @@ +from typing import Dict, Any, List, Optional +from schemas.books.book_cohesion import Chapter, Section, Subsection +import os +import json +import time + +from crewai import Crew + +from agents.books.book_cohesion import build_cohesion_agent, build_cohesion_task + + +def _safe_str(x: Any) -> str: + return "" if x is None else str(x) + + +def normalize_raw_book_json(raw: Dict[str, Any]) -> Dict[str, Any]: + """ + يحوّل بنية كتابك الخام (اللي فيها chapters->sections->plans...) لبنية: + chapters -> sections -> subsections + """ + out = { + "book_title": raw.get("book_title", ""), + "target_audience": raw.get("target_audience", ""), + "book_description": raw.get("book_description", ""), + "chapters": [], + } + + for ch in raw.get("chapters", []): + chapter_title = ch.get("chapter_title", "") + # بعض ملفاتك قد لا تحتوي outcome صريح على مستوى الشابتر + outcome = _safe_str(ch.get("chapter_outcome") or ch.get("outcome") or "") + + sections_out: List[Dict[str, Any]] = [] + for sec in ch.get("sections", []): + sec_title = sec.get("section_title") or sec.get("title") or "" + overview = _safe_str( + sec.get("overview") or sec.get("section_overview") or "" + ) + + # في JSON الخام: subsections تأتي غالبًا داخل plans dict + subsections_out: List[Dict[str, Any]] = [] + plans = sec.get("plans", {}) or {} + + for sub_title, plan in plans.items(): + # هنا قد لا يكون عندك content مولّد داخل plan، حسب خط الإنتاج + # نحاول التقاطه من حقول شائعة: + generated = ( + plan.get("generated_content") + or plan.get("content") + or plan.get("generated") + or "" + ) + sources = plan.get("sources") or [] + purpose = _safe_str(plan.get("core_idea") or plan.get("purpose") or "") + est_pages = int(plan.get("estimated_pages") or 3) + + subsections_out.append( + { + "title": _safe_str(sub_title), + "purpose": ( + purpose if purpose else "يشرح هذا المحور غرضه داخل القسم." + ), + "generated_content": _safe_str(generated), + "sources": ( + [str(s) for s in sources] + if isinstance(sources, list) + else [] + ), + "est_pages": est_pages, + } + ) + + sections_out.append( + { + "title": _safe_str(sec_title), + "overview": overview, + "subsections": subsections_out, + } + ) + + out["chapters"].append( + { + "chapter_title": _safe_str(chapter_title), + "outcome": outcome, + "sections": sections_out, + } + ) + + return out + + +def extract_chunk( + normalized: Dict[str, Any], + start: int, + chunk_size: int = 3, + previous_memory: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + chapters = normalized.get("chapters", []) + chunk = chapters[start : start + chunk_size] + + return { + "book_meta": { + "book_title": normalized.get("book_title", ""), + "target_audience": normalized.get("target_audience", ""), + "book_description": normalized.get("book_description", ""), + }, + "chapters_chunk": chunk, + "previous_narrative_memory": previous_memory, + } + + +def run_cohesion_pipeline( + raw_book_json: Dict[str, Any], + chunk_size: int = 3, + run_id: Optional[str] = None, +) -> Dict[str, Any]: + """ + يشغّل محرر التماسك السردي على دفعات (2-3 فصول). + يحفظ: + - normalized.json + - chunk_inputs/*.json + - chunk_outputs/*.json + - final_edited_book.json + - final_narrative_memory.json + """ + run_id = run_id or str(int(time.time())) + + normalized = normalize_raw_book_json(raw_book_json) + agent = build_cohesion_agent() + + chapters = normalized.get("chapters", []) + edited_chapters_all: List[Dict[str, Any]] = [] + memory: Optional[Dict[str, Any]] = None + + idx = 0 + chunk_index = 0 + while idx < len(chapters): + payload = extract_chunk( + normalized, idx, chunk_size=chunk_size, previous_memory=memory + ) + + task = build_cohesion_task(agent) + crew = Crew(agents=[agent], tasks=[task], verbose=False) + result = crew.kickoff(inputs={"chunk_payload": payload}) + + # result ممكن يكون dict أو str حسب الإصدار + if isinstance(result.json_dict, str): + edited = result.json_dict + else: + edited = result + + # تجميع الفصول المعدلة + edited_chunk = edited.get("edited_chapters", []) + edited_chapters_all.extend(edited_chunk) + + # تحديث الذاكرة السردية + memory = edited.get("narrative_memory", memory) + + idx += chunk_size + chunk_index += 1 + + final_book = { + "book_title": normalized.get("book_title", ""), + "target_audience": normalized.get("target_audience", ""), + "book_description": normalized.get("book_description", ""), + "chapters": edited_chapters_all, + } + + return { + "run_id": run_id, + "final_book": final_book, + "final_narrative_memory": memory or {}, + } diff --git a/core/books/content_crew_config.py b/core/books/content_crew_config.py new file mode 100644 index 0000000000000000000000000000000000000000..61e1b9bcbd793dd6fb3e6d00804eee7730d49cab --- /dev/null +++ b/core/books/content_crew_config.py @@ -0,0 +1,283 @@ +# app/core/books/content_crew_config.py +import os +import re +import json +import time +import gc +import shutil +from typing import Dict, Any, List, Tuple, Set + +import litellm + +# ========================= +# Runtime / Telemetry flags +# ========================= +os.environ["CREWAI_TRACING_ENABLED"] = "false" +os.environ["CREWAI_DISABLE_TELEMETRY"] = "true" +os.environ["OTEL_SDK_DISABLED"] = "true" + +# ========================= +# LiteLLM global config +# ========================= +litellm.num_retries = 4 +litellm.retry_policy = { + "TimeoutError": 4, + "APIError": 3, + "OpenRouterException": 3, + "Exception": 2, +} +litellm.request_timeout = 120 +litellm.drop_params = True + + +# ===================================================== +# Helpers +# ===================================================== +def normalize_ws(s: str) -> str: + return " ".join((s or "").split()) + + +def build_rag_block( + chunks: List[Dict[str, Any]], max_chars_per_chunk: int = 1200 +) -> str: + """ + Build compact RAG block: + - Excerpt + - IN_TEXT_CITATION (verbatim) + - REFERENCE_APA + """ + blocks = [] + for i, ch in enumerate(chunks or [], start=1): + text = (ch.get("text") or "")[:max_chars_per_chunk] + in_cite = ch.get("in_text_citation") or "" + ref = ch.get("reference_apa") or "" + blocks.append( + f"[Chunk {i}]\n" + f"Excerpt:\n{text}\n\n" + f"IN_TEXT_CITATION (must be used verbatim when citing this chunk): {in_cite}\n" + f"REFERENCE_APA (use in sources list): {ref}\n" + ) + return "\n\n".join(blocks).strip() + + +def collect_references_from_chunks(chunks: List[Dict[str, Any]]) -> List[str]: + """ + Unique references in stable order. + """ + seen: Set[str] = set() + out: List[str] = [] + for ch in chunks or []: + ref = normalize_ws(ch.get("reference_apa") or "") + if ref and ref not in seen: + seen.add(ref) + out.append(ref) + return out + + +def reset_crewai_environment(): + """ + Optional utility to hard-reset temp/cache between heavy runs. + Safe if torch is not installed. + """ + print("🧹 Resetting CrewAI environment...") + + for var in [ + "CREWAI_HOME", + "CREWAI_DATA_DIR", + "CREWAI_CACHE_DIR", + "CREWAI_LOG_LEVEL", + ]: + os.environ.pop(var, None) + + paths_to_delete = [ + "/tmp/crewai", + "/tmp/crewai_data", + "/tmp/crewai_home", + "/tmp/.cache", + "/root/.cache", + os.path.expanduser("~/.cache"), + ] + for path in paths_to_delete: + if os.path.exists(path): + shutil.rmtree(path, ignore_errors=True) + + os.makedirs("/tmp/crewai_data", exist_ok=True) + os.makedirs("/tmp/crewai_home", exist_ok=True) + + gc.collect() + try: + import torch # optional + + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + except Exception: + pass + + print("✅ CrewAI environment reset COMPLETE.") + + +# ===================================================== +# Robust JSON extraction / parsing +# ===================================================== +def _strip_code_fences(text: str) -> str: + t = (text or "").strip() + t = re.sub(r"^```(?:json)?\s*", "", t, flags=re.IGNORECASE) + t = re.sub(r"\s*```$", "", t) + return t.strip() + + +def _result_to_text(result: Any) -> str: + raw = getattr(result, "raw", None) + if isinstance(raw, str) and raw.strip(): + return raw.strip() + return str(result or "").strip() + + +def _parse_first_json_object(text: str) -> Dict[str, Any] | None: + """ + Parse first JSON object robustly using JSONDecoder.raw_decode scan. + """ + t = _strip_code_fences(text) + + # direct parse + try: + obj = json.loads(t) + if isinstance(obj, dict): + return obj + except Exception: + pass + + # scan for first object + dec = json.JSONDecoder() + for i, ch in enumerate(t): + if ch != "{": + continue + try: + obj, _ = dec.raw_decode(t[i:]) + if isinstance(obj, dict): + return obj + except Exception: + continue + + return None + + +def _normalize_output_contract(obj: Dict[str, Any]) -> Tuple[str, List[str]]: + """ + Required contract: + {"generated_content": str, "sources": list[str]} + """ + if not isinstance(obj, dict): + return "", [] + + content = obj.get("generated_content", "") + sources = obj.get("sources", []) + + if not isinstance(content, str): + content = str(content) + + if not isinstance(sources, list): + sources = [] + + clean_sources: List[str] = [] + seen: Set[str] = set() + for s in sources: + s = normalize_ws(str(s)) + if s and s not in seen: + seen.add(s) + clean_sources.append(s) + + return content.strip(), clean_sources + + +# ===================================================== +# Template safety for CrewAI interpolation +# ===================================================== +_TEMPLATE_VAR_RE = re.compile(r"(? Set[str]: + if not text: + return set() + return set(_TEMPLATE_VAR_RE.findall(text)) + + +def _extract_crew_required_vars(crew) -> Set[str]: + """ + Extract placeholders from all task descriptions/expected_output. + This prevents KeyError: Template variable ... not found. + """ + keys: Set[str] = set() + for task in getattr(crew, "tasks", []) or []: + keys |= _extract_template_vars(getattr(task, "description", "") or "") + keys |= _extract_template_vars(getattr(task, "expected_output", "") or "") + return keys + + +def prepare_inputs_for_crew(crew, inputs: Dict[str, Any] | None) -> Dict[str, Any]: + """ + Fill any missing template variables with empty string, + and normalize values to interpolation-safe scalars. + """ + data = dict(inputs or {}) + required = _extract_crew_required_vars(crew) + + for k in required: + data.setdefault(k, "") + + # normalize complex values + for k, v in list(data.items()): + if isinstance(v, (dict, list)): + try: + data[k] = json.dumps(v, ensure_ascii=False) + except Exception: + data[k] = str(v) + + return data + + +# ===================================================== +# Safe kickoff runner +# ===================================================== +def safe_run_once(crew, inputs, retries: int = 4, sleep_s: float = 15): + """ + Returns: (generated_content, sources) + """ + prepared_inputs = prepare_inputs_for_crew(crew, inputs) + + for attempt in range(1, retries + 1): + try: + result = crew.kickoff(inputs=prepared_inputs) + except Exception as e: + msg = str(e) + + # dynamic recover if any missing template slipped + m = re.search(r"Template variable '([^']+)' not found", msg) + if m: + missing_key = m.group(1).strip() + prepared_inputs[missing_key] = "" + print(f"⚠️ Missing template key auto-filled: {missing_key}") + time.sleep(sleep_s) + continue + + print(f"❌ kickoff error attempt {attempt}: {msg[:250]}") + time.sleep(sleep_s) + continue + + raw_text = _result_to_text(result) + + if "Maximum iterations reached" in raw_text: + print(f"⚠️ Iteration limit hit (attempt {attempt})") + time.sleep(sleep_s) + continue + + obj = _parse_first_json_object(raw_text) + if obj is not None: + return _normalize_output_contract(obj) + + print(f"❌ JSON parse failed attempt {attempt}") + print(raw_text[:300]) + time.sleep(sleep_s) + + return "", [] diff --git a/core/books/content_services.py b/core/books/content_services.py new file mode 100644 index 0000000000000000000000000000000000000000..5869ea541b78b0cd3850f9e9fb958d59c89f3ee8 --- /dev/null +++ b/core/books/content_services.py @@ -0,0 +1,309 @@ +# app/core/books/content_services.py +import json +from copy import deepcopy +from concurrent.futures import ThreadPoolExecutor +from typing import Dict, Any, List, Tuple + +from .content_crew_config import safe_run_once, build_rag_block +from agents.books.book_content import make_writer_crew, make_quality_crew +from agents.books.book_content_gpt import make_writer_crew_gpt, make_quality_crew_gpt +from agents.books import make_writer_editor_crew + +MAX_CHAPTER_WORKERS = 3 + +# لو True: quality يقدر يولد محتوى حتى لو writer رجّع فاضي +# لو False: quality يراجع فقط، ولو draft فاضي يفضل فاضي +ALLOW_QUALITY_FROM_EMPTY_DRAFT = False + +# مفاتيح لازمة للـ writer prompt +WRITER_REQUIRED_KEYS = [ + "book_title", + "book_description", + "target_audience", + "chapter_title", + "chapter_outcome", + "section_title", + "section_overview", + "subsection_title", + "subsection_purpose", + "style_profile_json", + "writing_mode", + "tone", + "target_pages", + "core_idea", + "key_points", + "scope_limit", + "avoid_overlap_with", + "depth_level", + # "citations_required", + "planner_notes", + "rag_block", +] + + +def _iter_section_plans(section: dict): + """ + يدعم الشكل الجديد: + section["plans"] = { "عنوان المحور": { ...plan... }, ... } + + + fallback للشكل القديم: + section["subsections"] = [ {...}, ... ] + """ + plans = section.get("plans") + if isinstance(plans, dict) and plans: + for subsection_title, plan in plans.items(): + yield subsection_title, plan if isinstance(plan, dict) else {} + return + + # fallback legacy + for sub in section.get("subsections", []): + subsection_title = sub.get("title") or sub.get("subsection_title") or "" + yield subsection_title, sub if isinstance(sub, dict) else {} + + +def _normalize_plan(raw_plan: dict) -> dict: + """ + يوحّد شكل plan سواء جاي بالشكل الجديد أو القديم. + """ + if not isinstance(raw_plan, dict): + return {} + + # الشكل القديم: subsection فيه planning + rag_context + if "planning" in raw_plan and isinstance(raw_plan.get("planning"), dict): + planning = deepcopy(raw_plan.get("planning", {})) + planning.setdefault("purpose", raw_plan.get("purpose", "")) + planning.setdefault("rag_context", raw_plan.get("rag_context", {})) + planning.setdefault("status", raw_plan.get("status", "ready")) + return planning + + # الشكل الجديد: plan جاهز مباشرة + return deepcopy(raw_plan) + + +def _as_json_string(value, fallback=None) -> str: + if value is None: + value = fallback if fallback is not None else {} + if isinstance(value, str): + return value + try: + return json.dumps(value, ensure_ascii=False) + except Exception: + return str(value) + + +def _as_text(value) -> str: + if value is None: + return "" + return str(value).strip() + + +def _as_text_list(value) -> str: + """ + تحويل list/string إلى نص ثابت للـ interpolation. + """ + if value is None: + return "" + if isinstance(value, list): + cleaned = [str(x).strip() for x in value if str(x).strip()] + return " | ".join(cleaned) + return str(value).strip() + + +def _ensure_required_inputs(inputs: dict, required_keys: List[str], ctx: str = ""): + missing = [k for k in required_keys if k not in inputs] + if missing: + prefix = f"[{ctx}] " if ctx else "" + raise ValueError(f"{prefix}Missing required input keys: {missing}") + + +def build_inputs( + book_meta: Dict[str, Any], + chapter: Dict[str, Any], + section: Dict[str, Any], + subsection_title: str, + plan: Dict[str, Any], +) -> Dict[str, Any]: + chapter_intro = chapter.get("chapter_intro", {}) + if not isinstance(chapter_intro, dict): + chapter_intro = {} + + rag_context = plan.get("rag_context", {}) + if not isinstance(rag_context, dict): + rag_context = {} + + rag_block = build_rag_block(rag_context.get("chunks", [])) + + # style_profile غالبًا على مستوى chapter + style_profile = chapter.get("style_profile") + if style_profile is None: + style_profile = section.get("style_profile") + + # chapter_outcome fallback من chapter_intro + chapter_outcome = chapter.get("outcome") or chapter_intro.get( + "why_this_chapter_now", "" + ) + + safe_subsection_title = ( + _as_text(subsection_title) + or _as_text(plan.get("subsection_title", "")) + or "محور فرعي" + ) + + return { + "book_title": _as_text(book_meta.get("book_title", "")), + "book_description": _as_text(book_meta.get("book_description", "")), + "target_audience": _as_text(book_meta.get("target_audience", "")), + "chapter_title": _as_text(chapter.get("chapter_title", "")), + "chapter_outcome": _as_text(chapter_outcome), + "section_title": _as_text( + section.get("section_title", "") or section.get("title", "") + ), + "section_overview": _as_text(section.get("overview", "")), + "subsection_title": safe_subsection_title, + "subsection_purpose": _as_text(plan.get("purpose", "")), + "style_profile_json": _as_json_string(style_profile, fallback={}), + "writing_mode": _as_text(plan.get("writing_mode", "")), + "tone": _as_text(plan.get("tone", "")), + "target_pages": plan.get("estimated_pages", 1) or 1, + "core_idea": _as_text(plan.get("core_idea", "")), + "key_points": _as_text_list(plan.get("key_points", [])), + "scope_limit": _as_text(plan.get("scope_limit", "")), + "avoid_overlap_with": _as_text_list(plan.get("avoid_overlap_with", [])), + "depth_level": _as_text(plan.get("depth_level", "")), + # "citations_required": bool(plan.get("citations_required", False)), + "planner_notes": _as_text(plan.get("notes", "")), + "rag_block": _as_text(rag_block), + # اختياري + "chapter_intro_json": _as_json_string(chapter_intro, fallback={}), + } + + +def process_chapter(chapter: dict, book_meta: dict, chapter_idx: int) -> dict: + """ + معالجة فصل كامل (sequential داخله) — تُستدعى داخل Thread. + """ + print(f"\n📘 Processing Chapter {chapter_idx}") + + chapter_out = { + "chapter_title": chapter.get("chapter_title", ""), + "chapter_intro": deepcopy(chapter.get("chapter_intro", {})), + "style_profile": deepcopy(chapter.get("style_profile", {})), + "sections": [], + } + + if chapter.get("outcome") is not None: + chapter_out["outcome"] = chapter.get("outcome", "") + + for section in chapter.get("sections", []): + section_title = section.get("section_title", "") or section.get("title", "") + section_out = { + "section_title": section_title, + "overview": section.get("overview", ""), + "section_intro_paragraph": section.get("section_intro_paragraph", ""), + "section_closing_paragraph": section.get("section_closing_paragraph", ""), + "plans": {}, + } + writer_crew = make_writer_crew() + quality_crew = make_quality_crew() + writer_editor_crew = make_writer_editor_crew() + + for subsection_title, raw_plan in _iter_section_plans(section): + + plan = _normalize_plan(raw_plan) + print(f"🟢 Writing: {subsection_title}") + + plan_out = deepcopy(plan) + try: + inputs = build_inputs( + book_meta, chapter, section, subsection_title, plan + ) + + _ensure_required_inputs( + inputs, + WRITER_REQUIRED_KEYS, + ctx=f"chapter={chapter_idx}, section={section_title}, subsection={subsection_title}", + ) + + # 1) Writer + retry (داخل safe_run_once) + generated, sources = safe_run_once(writer_editor_crew, inputs) + content_before_review = generated + sources_before_review = sources + + # 2) Quality gate + reviewed_generated, reviewed_sources = "", [] + # if generated.strip() or ALLOW_QUALITY_FROM_EMPTY_DRAFT: + if reviewed_generated.strip() or ALLOW_QUALITY_FROM_EMPTY_DRAFT: + draft_json = json.dumps( + {"generated_content": generated, "sources": sources}, + ensure_ascii=False, + ) + quality_inputs = dict(inputs) + quality_inputs["draft_json"] = draft_json + reviewed_generated, reviewed_sources = safe_run_once( + quality_crew, quality_inputs + ) + + if reviewed_generated.strip(): + generated = reviewed_generated + sources = reviewed_sources + + plan_out["generated_content"] = content_before_review + plan_out["sources"] = sources_before_review + plan_out["generated_content_reviewed"] = generated + plan_out["sources_reviewed"] = sources + plan_out["content_status"] = ( + "generated" if generated.strip() else "empty" + ) + + except Exception as e: + plan_out["generated_content"] = "" + plan_out["sources"] = [] + plan_out["generated_content_reviewed"] = "" + plan_out["sources_reviewed"] = [] + plan_out["content_status"] = "error" + plan_out["error"] = str(e) + + section_out["plans"][subsection_title] = plan_out + + chapter_out["sections"].append(section_out) + + return chapter_out + + +def process_book(data: dict) -> dict: + """ + - parallel على مستوى chapters + - الحفاظ على ترتيب chapters + """ + book_meta = { + "book_title": data.get("book_title", ""), + "book_description": data.get("book_description", ""), + "target_audience": data.get("target_audience", ""), + } + + chapters = data.get("chapters", []) + chapters_out = [None] * len(chapters) + + futures = [] + with ThreadPoolExecutor(max_workers=MAX_CHAPTER_WORKERS) as executor: + for idx, chapter in enumerate(chapters): + future = executor.submit(process_chapter, chapter, book_meta, idx + 1) + futures.append((idx, future)) + + for idx, future in futures: + try: + chapters_out[idx] = future.result() + except Exception as e: + ch = chapters[idx] if idx < len(chapters) else {} + chapters_out[idx] = { + "chapter_title": ch.get("chapter_title", f"Chapter {idx+1}"), + "sections": [], + "error": str(e), + } + + return { + "message": "Book Content Generated Successfully 🚀", + "content_phase": "generated", + **book_meta, + "chapters": chapters_out, + } diff --git a/core/books/convert_enrichment_to_html.py b/core/books/convert_enrichment_to_html.py new file mode 100644 index 0000000000000000000000000000000000000000..a56ea018aa571c2bbb7013f0fc13ad6c4805c306 --- /dev/null +++ b/core/books/convert_enrichment_to_html.py @@ -0,0 +1,391 @@ +import json +import markdown + + +def load_json(path): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def md(text): + if not text: + return "" + return markdown.markdown( + text, extensions=["fenced_code", "tables", "nl2br", "sane_lists"] + ) + + +def render_html(book): + + html_parts = [] + + html_parts.append( + """ + + + + + + +Book Enrichment Viewer + + + + + + + + + + + +
        + +
        + + + + + +
        +""" + ) + + chapter_index = 0 + + for chapter in book.get("chapters", []): + + cid = f"chapter_{chapter_index}" + + html_parts.append( + f'
        ' + f'
        {chapter.get("chapter_title","")}
        ' + f'
        ' + ) + + for section in chapter.get("sections", []): + + sid = f"{cid}_section" + + html_parts.append( + f'
        ' + f'
        {section.get("section_title","")}
        ' + f"
        " + ) + + for plan_title, plan in section.get("plans", {}).items(): + + enrichments = plan.get("enrichment_meta", []) + + if not enrichments: + continue + + html_parts.append( + f'
        {plan_title}
        ' + ) + + for e in enrichments: + + etype = e.get("enrichment_type", "") + text = e.get("enrichment_generated", "") + + label = { + "knowledge_flash": "💡 ومضة معرفية", + "analytical_quote": "🧠 اقتباس تحليلي", + "case_study": "📊 دراسة حالة", + "case_story": "📖 قصة تطبيقية", + }.get(etype, etype) + + html_parts.append( + f""" +
        +
        {label}
        +
        {md(text)}
        +
        +""" + ) + + html_parts.append("
        ") + + html_parts.append("
        ") + + html_parts.append("
        ") + + html_parts.append( + f"" + ) + + chapter_index += 1 + + html_parts.append( + """ + +
        + + + + + +""" + ) + + return "\n".join(html_parts) diff --git a/core/books/fetch_url.py b/core/books/fetch_url.py new file mode 100644 index 0000000000000000000000000000000000000000..81b3a67879efe89bf7635db29eabce49500e9c6b --- /dev/null +++ b/core/books/fetch_url.py @@ -0,0 +1,84 @@ +import requests +from bs4 import BeautifulSoup +from urllib.parse import urljoin +from requests.exceptions import RequestException + +PDF_HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/121.0.0.0 Safari/537.36" + ), + "Accept": "application/pdf,application/octet-stream;q=0.9,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.9,ar;q=0.8", + "Referer": "https://www.google.com/", +} + + +def fetch_url(url: str): + try: + r = requests.get( + url, + timeout=20, + allow_redirects=True, + headers=PDF_HEADERS, + ) + r.raise_for_status() + ct = r.headers.get("Content-Type", "").lower() + return ct, r.content, r.url + except RequestException: + return None, None, None + + +def looks_like_pdf(content: bytes) -> bool: + return bool(content and content.startswith(b"%PDF")) + + +def get_pdf_bytes(source_url: str) -> bytes | None: + ct, content, final_url = fetch_url(source_url) + + if not ct or not content: + return None + + # ------------------------- + # 1️⃣ PDF مباشر (صريح أو كذاب) + # ------------------------- + if ( + "application/pdf" in ct + or final_url.lower().endswith(".pdf") + or looks_like_pdf(content) + ): + return content + + # ------------------------- + # 2️⃣ HTML wrapper + # ------------------------- + if "text/html" in ct: + try: + soup = BeautifulSoup( + content.decode("utf-8", errors="ignore"), + "html.parser", + ) + + # a[href] + for a in soup.find_all("a", href=True): + href = a["href"].strip() + if ".pdf" in href.lower(): + pdf_url = urljoin(final_url, href) + ct2, content2, _ = fetch_url(pdf_url) + if ct2 and ("application/pdf" in ct2 or looks_like_pdf(content2)): + return content2 + + # iframe / embed + for tag in soup.find_all(["iframe", "embed"], src=True): + src = tag["src"] + if ".pdf" in src.lower(): + pdf_url = urljoin(final_url, src) + ct2, content2, _ = fetch_url(pdf_url) + if ct2 and ("application/pdf" in ct2 or looks_like_pdf(content2)): + return content2 + + except Exception: + return None + + return None diff --git a/core/books/planning_services.py b/core/books/planning_services.py new file mode 100644 index 0000000000000000000000000000000000000000..4a7c2d74f6df5023db4f542fdfc1c3b000170626 --- /dev/null +++ b/core/books/planning_services.py @@ -0,0 +1,104 @@ +# core/planning_services.py + +from agents.books import agent_task_planning +from crewai import Crew, Process +import litellm +import json +import time +from schemas.books.book_planning import ChapterPlanningBlock, ChapterPlanningBlockDTO, SectionPlansBlock + +# ================================ +# LiteLLM Global Config +# ================================ +litellm.num_retries = 4 +litellm.retry_policy = { + "TimeoutError": 4, + "APIError": 3, + "OpenRouterException": 3, + "Exception": 2, +} +litellm.request_timeout = 120 +litellm.drop_params = True + + +def convert_dto_to_original(dto: ChapterPlanningBlockDTO) -> ChapterPlanningBlock: + return ChapterPlanningBlock( + chapter_title=dto.chapter_title, + chapter_intro=dto.chapter_intro, + style_profile=dto.style_profile, + sections=[ + SectionPlansBlock( + section_title=sec.section_title, + section_intro_paragraph=sec.section_intro_paragraph, + section_closing_paragraph=sec.section_closing_paragraph, + plans={item.subsection_title: item.plan for item in sec.plans}, + ) + for sec in dto.sections + ], + ) + + +# ================================ +# Crew (single chapter) +# ================================ +def build_planner_crew(): + agent, task = agent_task_planning() + return Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + memory=False, + verbose=False, + ) + + +# ================================ +# Runner +# ================================ +# core/planning_services.py (FIXED) + + +def run_planning_on_outline(book_outline: dict) -> dict: + chapters_output = [] + tokens = [] + + for idx, chapter in enumerate(book_outline.get("chapters", []), start=1): + inputs = { + "book_title": book_outline.get("book_title", ""), + "target_audience": book_outline.get("target_audience", ""), + "book_description": book_outline.get("book_description", ""), + "chapter_outline_json": json.dumps(chapter, ensure_ascii=False), + } + + print(f"🟦 Planning Chapter {idx}: {chapter['chapter_title']}") + + planner_crew = build_planner_crew() + result = planner_crew.kickoff(inputs=inputs) + + if result.token_usage: + tokens.append(result.token_usage.dict()) + + dto_dict = json.loads(result.raw) + try: + dto_result = ChapterPlanningBlockDTO(**dto_dict) + except Exception as e: + print("❌ DTO Parsing Error:", e) + print("RAW OUTPUT:", result.raw) + raise + final_result=convert_dto_to_original(dto_result) + chapters_output.append(final_result.dict()) + # chapter_plan = json.loads(result.raw) + # chapters_output.append(chapter_plan) + + time.sleep(0.4) + + return { + "book": { + "book_title": book_outline.get("book_title", ""), + "book_description": book_outline.get("book_description", ""), + "target_audience": book_outline.get("target_audience", ""), + "chapters": chapters_output, + "planning_notes": "تم التخطيط Chapter-by-Chapter بنجاح.", + }, + "tokens": tokens, + } diff --git a/core/books/search_processing.py b/core/books/search_processing.py new file mode 100644 index 0000000000000000000000000000000000000000..59dd4c80d9266bb4466c9486d27beac91e3a4eb8 --- /dev/null +++ b/core/books/search_processing.py @@ -0,0 +1,52 @@ +from urllib.parse import urlparse +from schemas import SearchResult + +def detect_source_type(url: str) -> str: + u = url.lower() + + if u.endswith(".pdf"): + return "pdf" + if any(x in u for x in ["springer", "elsevier", "sciencedirect", "tandfonline"]): + return "journal" + if any(x in u for x in ["isbn", "books.google", "openlibrary"]): + return "book" + if any(x in u for x in [".edu", ".gov", "apa.org", "who.int"]): + return "institutional" + return "web" + +BAD_DOMAINS = [ + "facebook", + "youtube", + "tiktok", + "instagram", + "linkedin", + "twitter", + "blog", + "course", + "medium", + "quora", + "slideshare", + "wikipedia", +] + +def is_good_domain(url: str) -> bool: + u = url.lower() + return not any(bad in u for bad in BAD_DOMAINS) + + +def extract_domain(url: str) -> str: + return urlparse(url).netloc.replace("www.", "") + + +def deduplicate_results(results: list[SearchResult]): + seen = set() + unique = [] + + for r in results: + key = (r.domain, r.title.lower().strip()[:120]) + if key in seen: + continue + seen.add(key) + unique.append(r) + + return unique diff --git a/core/books/search_service.py b/core/books/search_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e1437a0f00eaa3f8362017e2a0b59800f2b373b0 --- /dev/null +++ b/core/books/search_service.py @@ -0,0 +1,432 @@ +# import json +# import os +# import http.client +# from .search_processing import ( +# is_good_domain, +# detect_source_type, +# extract_domain, +# deduplicate_results, +# ) +# from schemas import SearchResult +# import threading + +# SERPER_API_KEYS = [ +# os.getenv("SERPER_API_KEY"), +# os.getenv("SERPER_API_KEY_1"), +# os.getenv("SERPER_API_KEY_2"), +# os.getenv("SERPER_API_KEY_3"), +# os.getenv("SERPER_API_KEY_335"), +# os.getenv("SERPER_API_KEY_zoe"), +# os.getenv("SERPER_API_KEY_38732"), +# os.getenv("SERPER_API_KEY_849"), +# ] + +# SERPER_API_KEYS = [k for k in SERPER_API_KEYS if k] + +# CURRENT_SERPER_INDEX = 0 +# SERPER_LOCK = threading.Lock() + + +# class SerperExhaustedError(Exception): +# pass + + +# def rotate_serper_key(): +# global CURRENT_SERPER_INDEX +# with SERPER_LOCK: +# CURRENT_SERPER_INDEX += 1 +# if CURRENT_SERPER_INDEX >= len(SERPER_API_KEYS): +# raise SerperExhaustedError("❌ SERPER APIs exhausted.") +# print(f"🔁 Switching to SERPER API #{CURRENT_SERPER_INDEX}") + + +# # def search_serper(query: str, max_results: int = 5, year_from: int | None = None): +# # conn = http.client.HTTPSConnection("google.serper.dev") +# # if year_from: +# # query = f"{query} Saudi Arabia after:{year_from}" +# # payload = json.dumps({"q": query, "page": 1}) +# # headers = { +# # "X-API-KEY": os.getenv("SERPER_API_KEY"), +# # "Content-Type": "application/json", +# # } + +# # conn.request("POST", "/search", payload, headers) +# # res = conn.getresponse() +# # data = json.loads(res.read().decode("utf-8")) + +# # results = [] +# # for r in data.get("organic", []): +# # url = r.get("link") +# # if not url: +# # continue +# # if not is_good_domain(url): +# # continue + +# # source_type = detect_source_type(url) +# # if source_type == "web": # ⛔ نرفض الويب العام بدري +# # continue + +# # domain = extract_domain(url) +# # position = r.get("position", 50) +# # score = round(1 / position, 4) + +# # results.append( +# # SearchResult( +# # query=query, +# # url=url, +# # title=r.get("title", ""), +# # snippet=r.get("snippet", ""), +# # source_type=source_type, +# # domain=domain, +# # score=score, +# # ) +# # ) + +# # if len(results) >= max_results: +# # break + + +# # return results +# def search_serper(query: str, max_results: int = 5, year_from: int | None = None): +# global CURRENT_SERPER_INDEX + +# if year_from: +# query = f"{query} Saudi Arabia after:{year_from}" + +# attempts = 0 + +# while attempts < len(SERPER_API_KEYS): +# api_key = SERPER_API_KEYS[CURRENT_SERPER_INDEX] + +# try: +# conn = http.client.HTTPSConnection("google.serper.dev", timeout=8) + +# payload = json.dumps({"q": query, "page": 1}) +# headers = { +# "X-API-KEY": api_key, +# "Content-Type": "application/json", +# } + +# conn.request("POST", "/search", payload, headers) +# res = conn.getresponse() + +# status = res.status +# data = res.read() + +# # ❌ key خلصت +# if status in (401, 403): +# rotate_serper_key() +# attempts += 1 +# continue + +# # ❌ error تاني +# if status != 200: +# return [] + +# response_json = json.loads(data.decode("utf-8")) +# break + +# except SerperExhaustedError: +# raise + +# except Exception: +# rotate_serper_key() +# attempts += 1 + +# else: +# raise SerperExhaustedError("❌ All SERPER keys failed.") + +# print(f"[SERPER] Using key #{CURRENT_SERPER_INDEX}") +# # ✅ parsing (زي ما هو عندك) +# results = [] + +# for r in response_json.get("organic", []): +# url = r.get("link") +# if not url: +# continue +# if not is_good_domain(url): +# continue + +# source_type = detect_source_type(url) +# if source_type == "web": +# continue + +# domain = extract_domain(url) +# position = r.get("position", 50) +# score = round(1 / position, 4) + +# results.append( +# SearchResult( +# query=query, +# url=url, +# title=r.get("title", ""), +# snippet=r.get("snippet", ""), +# source_type=source_type, +# domain=domain, +# score=score, +# ) +# ) + +# if len(results) >= max_results: +# break + +# return results + + +# # ============================ SEARCH RUNNER ============================ +# from concurrent.futures import ThreadPoolExecutor, as_completed + +# MAX_WORKERS = 15 # عدلها حسب سرعة السيرفر والـ API limits + + +# def run_search_pipeline_parallel( +# book_data: dict, +# ): +# all_results = [] +# queries = [] + +# # # 1️⃣ Collect all queries +# # for chapter in book_data["chapters"]: +# # for section in chapter["sections"]: +# # for sub in section["subsections"]: +# # for q in sub["academic_queries"]: +# # queries.append(q) +# for item in book_data["queries"]: +# for q in item["queries"]: +# queries.append(q) + +# print(f"🧠 Total queries: {len(queries)}") + +# # 2️⃣ Parallel search +# def _search_one(query: str): +# print(f"🔍 SEARCH: {query}") +# try: +# return search_serper(query, max_results=5, year_from=2019) +# except Exception as e: +# print(f"❌ Error searching [{query}]: {e}") +# return [] + +# with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: +# futures = [executor.submit(_search_one, q) for q in queries] + +# for future in as_completed(futures): +# try: +# results = future.result() +# all_results.extend(results) +# except Exception as e: +# print("❌ Worker crashed:", e) + +# print(f"🔁 Raw results: {len(all_results)}") + +# # 3️⃣ Deduplication +# clean_results = deduplicate_results(all_results) + +# print(f"✅ After dedup: {len(clean_results)}") + +# return { +# "message": "Search Generated Well 🚀", +# "all_results": len(all_results), +# "clean_results": len(clean_results), +# "result": [r.model_dump() for r in clean_results], +# } +import json +import os +import http.client +import threading +from queue import Queue, Empty +from concurrent.futures import ThreadPoolExecutor, as_completed + +from .search_processing import ( + is_good_domain, + detect_source_type, + extract_domain, + deduplicate_results, +) +from schemas import SearchResult + + +# ============================ KEYS SETUP ============================ + +SERPER_API_KEYS = [ + os.getenv("SERPER_API_KEY"), + os.getenv("SERPER_API_KEY_1"), + os.getenv("SERPER_API_KEY_2"), + os.getenv("SERPER_API_KEY_3"), + os.getenv("SERPER_API_KEY_335"), + os.getenv("SERPER_API_KEY_zoe"), + os.getenv("SERPER_API_KEY_38732"), + os.getenv("SERPER_API_KEY_849"), +] + +SERPER_API_KEYS = [k for k in SERPER_API_KEYS if k] + +if not SERPER_API_KEYS: + raise ValueError("❌ No SERPER API keys found.") + +# Queue للـ keys +SERPER_KEYS_QUEUE = Queue() + +for key in SERPER_API_KEYS: + SERPER_KEYS_QUEUE.put(key) + + +class SerperExhaustedError(Exception): + pass + + +# ============================ KEY MANAGEMENT ============================ + + +def get_key(): + try: + return SERPER_KEYS_QUEUE.get(timeout=2) + except Empty: + raise SerperExhaustedError("❌ No available SERPER keys.") + + +def return_key(key): + # رجّع الكي علشان تتستخدم تاني + SERPER_KEYS_QUEUE.put(key) + + +# ============================ SEARCH ============================ + + +def search_serper(query: str, max_results: int = 5, year_from: int | None = None): + if year_from: + query = f"{query} Saudi Arabia after:{year_from}" + + attempts = 0 + max_attempts = len(SERPER_API_KEYS) + + while attempts < max_attempts: + api_key = get_key() + + try: + conn = http.client.HTTPSConnection("google.serper.dev", timeout=8) + + payload = json.dumps({"q": query, "page": 1}) + headers = { + "X-API-KEY": api_key, + "Content-Type": "application/json", + } + + conn.request("POST", "/search", payload, headers) + res = conn.getresponse() + + status = res.status + data = res.read() + + # ❌ key انتهت + if status in (401, 403): + print(f"❌ Key failed (quota انتهت): {api_key[:6]}...") + attempts += 1 + continue + + # ❌ error تاني + if status != 200: + print(f"⚠️ Unexpected status {status}") + return [] + + response_json = json.loads(data.decode("utf-8")) + + print(f"✅ Key used successfully: {api_key[:6]}...") + + # رجّع الكي علشان reuse + return_key(api_key) + break + + except Exception as e: + print(f"❌ Exception with key {api_key[:6]}...: {e}") + attempts += 1 + + else: + raise SerperExhaustedError("❌ All SERPER keys failed.") + + # ============================ PARSING ============================ + + results = [] + + for r in response_json.get("organic", []): + url = r.get("link") + if not url: + continue + + if not is_good_domain(url): + continue + + source_type = detect_source_type(url) + if source_type == "web": + continue + + domain = extract_domain(url) + position = r.get("position", 50) + score = round(1 / position, 4) + + results.append( + SearchResult( + query=query, + url=url, + title=r.get("title", ""), + snippet=r.get("snippet", ""), + source_type=source_type, + domain=domain, + score=score, + ) + ) + + if len(results) >= max_results: + break + + return results + + +# ============================ SEARCH RUNNER ============================ + +MAX_WORKERS = 15 + + +def run_search_pipeline_parallel(book_data: dict): + all_results = [] + queries = [] + + # Collect queries + for item in book_data["queries"]: + for q in item["queries"]: + queries.append(q) + + print(f"🧠 Total queries: {len(queries)}") + + # Worker + def _search_one(query: str): + print(f"🔍 SEARCH: {query}") + try: + return search_serper(query, max_results=5, year_from=2019) + except Exception as e: + print(f"❌ Error searching [{query}]: {e}") + return [] + + # Parallel execution + with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + futures = [executor.submit(_search_one, q) for q in queries] + + for future in as_completed(futures): + try: + results = future.result() + all_results.extend(results) + except Exception as e: + print("❌ Worker crashed:", e) + + print(f"🔁 Raw results: {len(all_results)}") + + # Deduplication + clean_results = deduplicate_results(all_results) + + print(f"✅ After dedup: {len(clean_results)}") + + return { + "message": "Search Generated Well 🚀", + "all_results": len(all_results), + "clean_results": len(clean_results), + "result": [r.model_dump() for r in clean_results], + } diff --git a/core/books/storage.py b/core/books/storage.py new file mode 100644 index 0000000000000000000000000000000000000000..d2db0369fd81687bf8d581fce2aef70264d357b2 --- /dev/null +++ b/core/books/storage.py @@ -0,0 +1,174 @@ +# core/books/storage.py +import os +from supabase import create_client, Client +from typing import List, Optional, Dict, Any + +from schemas.books.sources_schema import DocRaw, DocMetadata + +SUPABASE_URL = os.getenv("SUPABASE_URL") +SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") + +supabase: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY) + + +def init_db(): + return + + +def check_db_health() -> bool: + try: + supabase.table("documents_raw").select("doc_id").limit(1).execute() + return True + except Exception: + return False + + +# ------------------------- +# RAW (Phase 1) +# ------------------------- +def insert_raw_document(raw: DocRaw) -> None: + data = raw.model_dump() + resp = supabase.table("documents_raw").insert(data).execute() + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase insert documents_raw failed: {resp.error}") + + +def mark_raw_status(doc_id: str, status: str, error_reason: str = "") -> None: + resp = ( + supabase.table("documents_raw") + .update({"status": status, "error_reason": error_reason}) + .eq("doc_id", doc_id) + .execute() + ) + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase update documents_raw failed: {resp.error}") + + +def fetch_pending_raw_docs(limit: int = 25) -> List[Dict[str, Any]]: + resp = ( + supabase.table("documents_raw") + .select("*") + .eq("status", "pending") + .limit(limit) + .execute() + ) + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase select documents_raw failed: {resp.error}") + return resp.data or [] + + +def fetch_raw_doc(doc_id: str) -> Optional[Dict[str, Any]]: + resp = ( + supabase.table("documents_raw") + .select("*") + .eq("doc_id", doc_id) + .limit(1) + .execute() + ) + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase select documents_raw failed: {resp.error}") + data = resp.data or [] + return data[0] if data else None + + +def fetch_raw_docs_for_user( + user_id: str, book_id: str | None = None, status: str = "pending" +) -> List[Dict[str, Any]]: + q = ( + supabase.table("documents_raw") + .select("*") + .eq("user_id", user_id) + .eq("status", status) + ) + if book_id: + q = q.eq("book_id", book_id) + + resp = q.execute() + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase select documents_raw failed: {resp.error}") + + return resp.data or [] + + +# ------------------------- +# METADATA (Phase 2) +# ------------------------- +def upsert_document_metadata(md: DocMetadata) -> None: + data = { + "doc_id": md.doc_id, + "title": md.title, + "authors": ", ".join(md.authors), + "year": md.year, + "publisher_or_journal": md.publisher_or_journal or "", + "normalized_source_type": md.normalized_source_type or "pdf", + "apa7": md.apa7 or "", + "metadata": md.metadata or {}, + } + resp = ( + supabase.table("documents_metadata") + .upsert(data, on_conflict="doc_id") + .execute() + ) + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase upsert documents_metadata failed: {resp.error}") + + +# ------------------------- +# METADATA (Phase 2) - READ +# ------------------------- +def fetch_document_metadata(doc_id: str) -> Optional[Dict[str, Any]]: + """ + Fetch processed metadata for a document. + """ + resp = ( + supabase.table("documents_metadata") + .select("*") + .eq("doc_id", doc_id) + .limit(1) + .execute() + ) + + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase select documents_metadata failed: {resp.error}") + + data = resp.data or [] + return data[0] if data else None + + +def delete_book_docs(user_id: str, book_id: str): + # يمسح raw وبالتالي metadata تتشال cascade + resp = ( + supabase.table("documents_raw") + .delete() + .eq("user_id", user_id) + .eq("book_id", book_id) + .execute() + ) + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase delete documents_raw failed: {resp.error}") + + +def delete_raw_doc(doc_id: str) -> None: + resp = supabase.table("documents_raw").delete().eq("doc_id", doc_id).execute() + + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase delete documents_raw failed: {resp.error}") + + +def delete_metadata(doc_id: str) -> None: + resp = supabase.table("documents_metadata").delete().eq("doc_id", doc_id).execute() + + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase delete documents_metadata failed: {resp.error}") + + +def delete_chunks(doc_id: str) -> None: + resp = ( + supabase.table("chunks") # أو اسم التابل عندك + .delete() + .eq("doc_id", doc_id) + .execute() + ) + + if getattr(resp, "error", None): + raise RuntimeError(f"Supabase delete chunks failed: {resp.error}") diff --git a/core/content_converter_service.py b/core/content_converter_service.py new file mode 100644 index 0000000000000000000000000000000000000000..d8148de32f49410e806a530d056b7ab5fb344308 --- /dev/null +++ b/core/content_converter_service.py @@ -0,0 +1,931 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +FastAPI Service لتحويل بيانات المقررات الدراسية من Dictionary إلى HTML +FastAPI Service for Converting Course Data from Dictionary to HTML +""" + +import re +import json +from html import escape +from typing import Dict, Any + + +def escape_html(text): + """تحويل النص إلى HTML آمن""" + if not text: + return "" + text = escape(text) + return text + + +def markdown_to_html(text): + """تحويل Markdown إلى HTML""" + if not text: + return "" + + # تقسيم النص إلى فقرات + paragraphs = text.split('\n\n') + html_parts = [] + + for para in paragraphs: + para = para.strip() + if not para: + continue + + # معالجة القوائم المرقمة (1., 2., إلخ) أو **text** + # التحقق من أن السطر يبدأ برقم متبوع بنقطة ومسافة + if para and len(para) > 3: + # محاولة مطابقة نمط "رقم. مسافة" + match = re.match(r'^(\d+)\.\s+(.+)$', para) + if match: + # قائمة مرقمة + if not html_parts or not html_parts[-1].startswith('') + html_parts.append('
          ') + + content = match.group(2).strip() + content = process_inline_markdown(content) + html_parts.append(f'
        1. {content}
        2. ') + continue + + # معالجة القوائم بنقاط (- أو *) + if para.startswith('- ') or para.startswith('* '): + if not html_parts or not html_parts[-1].startswith('') + html_parts.append('
            ') + + content = para[2:].strip() + content = process_inline_markdown(content) + html_parts.append(f'
          • {content}
          • ') + continue + + # إغلاق القوائم المفتوحة قبل الفقرات العادية + # لكن فقط إذا لم يكن السطر جزءًا من قائمة + should_close_lists = True + if para.startswith('- ') or para.startswith('* ') or (para and len(para) > 3 and re.match(r'^(\d+)\.\s+', para)): + should_close_lists = False + + if should_close_lists and html_parts: + last_item = html_parts[-1] if html_parts else "" + if last_item.startswith('') + break + elif html_parts[i].startswith('') + break + + # معالجة العناوين (###, ##, #) + if para.startswith('### '): + content = process_inline_markdown(para[4:].strip()) + html_parts.append(f'
            {content}
            ') + elif para.startswith('## '): + content = process_inline_markdown(para[3:].strip()) + html_parts.append(f'

            {content}

            ') + elif para.startswith('# '): + content = process_inline_markdown(para[2:].strip()) + html_parts.append(f'

            {content}

            ') + elif not para.startswith('- ') and not para.startswith('* ') and not (para and len(para) > 3 and re.match(r'^(\d+)\.\s+', para)): + # فقرة عادية (ليست قائمة ولا عنوان) + # تقسيم إلى أسطر إذا كان يحتوي على \n + if '\n' in para: + lines = para.split('\n') + processed_lines = [] + for line in lines: + line = line.strip() + if line: + # التحقق إذا كانت السطر يبدو كعنوان (قصير ولا ينتهي بنقطة) + if len(line) < 80 and not line.endswith('.') and not line.endswith(':') and not line.startswith('-') and not line.startswith('*'): + processed_lines.append(f'{process_inline_markdown(line)}') + else: + processed_lines.append(process_inline_markdown(line)) + content = '
            '.join(processed_lines) + else: + content = process_inline_markdown(para) + + html_parts.append(f'

            {content}

            ') + + # إغلاق أي قوائم مفتوحة + if html_parts: + if html_parts[-1].startswith('') + elif html_parts[-1].startswith('') + + return '\n'.join(html_parts) + + +def process_inline_markdown(text): + """معالجة Markdown داخل السطر (bold, italic, links)""" + if not text: + return "" + + # النص الغامق **text** أو __text__ + text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) + text = re.sub(r'__([^_]+)__', r'\1', text) + + # النص المائل *text* أو _text_ + text = re.sub(r'\*([^*]+)\*', r'\1', text) + text = re.sub(r'_([^_]+)_', r'\1', text) + + # الروابط [text](url) + text = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', r'\1', text) + + # الكود `code` + text = re.sub(r'`([^`]+)`', r'\1', text) + + return text + + +def generate_table_of_contents(data): + """توليد جدول المحتويات كـ JavaScript""" + + toc_items = [] + + if 'results' in data and isinstance(data['results'], list): + for unit_idx, unit in enumerate(data['results'], 1): + unit_id = f"unit-{unit_idx}" + unit_name = unit.get('unit_name', f'الوحدة {unit_idx}') + toc_items.append({ + 'id': unit_id, + 'text': f'الوحدة {unit_idx}: {unit_name}', + 'children': [] + }) + + if 'topics' in unit and isinstance(unit['topics'], list): + for topic_idx, topic in enumerate(unit['topics'], 1): + topic_id = f"unit-{unit_idx}-topic-{topic_idx}" + topic_title = topic.get('topic_title', f'الموضوع {topic_idx}') + toc_items[-1]['children'].append({ + 'id': topic_id, + 'text': f'{unit_idx}.{topic_idx} {topic_title}' + }) + + return json.dumps(toc_items, ensure_ascii=False, indent=2) + + +def get_css_content(): + """إرجاع محتوى ملف CSS""" + return """/* تصميم المقرر الدراسي - Course Design */ + +:root { + --primary-color: #2c3e50; + --secondary-color: #3498db; + --accent-color: #e74c3c; + --text-color: #333; + --bg-color: #ffffff; + --light-bg: #f8f9fa; + --border-color: #dee2e6; + --shadow: 0 2px 8px rgba(0,0,0,0.1); + --shadow-hover: 0 4px 12px rgba(0,0,0,0.15); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.8; + color: var(--text-color); + background-color: var(--light-bg); + direction: rtl; +} + +.container { + max-width: 1200px; + margin: 0 auto; + background-color: var(--bg-color); + box-shadow: var(--shadow); +} + +/* Header */ +.course-header { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + color: white; + padding: 3rem 2rem; + text-align: center; +} + +.course-title { + font-size: 2.5rem; + margin-bottom: 1.5rem; + font-weight: 700; + text-shadow: 2px 2px 4px rgba(0,0,0,0.2); +} + +.course-audience { + background: rgba(255,255,255,0.1); + padding: 1.5rem; + border-radius: 8px; + margin-top: 1.5rem; + text-align: right; +} + +.section-title { + font-size: 1.3rem; + margin-bottom: 0.5rem; + font-weight: 600; +} + +.audience-text { + font-size: 1.1rem; + line-height: 1.8; + opacity: 0.95; +} + +/* Table of Contents */ +.table-of-contents { + background-color: var(--light-bg); + padding: 2rem; + border-bottom: 3px solid var(--secondary-color); + position: relative; + top: 0; + z-index: 100; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.table-of-contents h2 { + color: var(--primary-color); + margin-bottom: 1rem; + font-size: 1.5rem; +} + +#toc-list { + list-style: none; +} + +#toc-list > li { + margin-bottom: 0.5rem; +} + +#toc-list > li > a { + display: block; + padding: 0.75rem 1rem; + color: var(--primary-color); + text-decoration: none; + font-weight: 600; + border-radius: 5px; + transition: all 0.3s ease; + border-right: 3px solid transparent; +} + +#toc-list > li > a:hover { + background-color: rgba(52, 152, 219, 0.1); + border-right-color: var(--secondary-color); + transform: translateX(-5px); +} + +#toc-list ul { + list-style: none; + margin-top: 0.5rem; + margin-right: 1rem; +} + +#toc-list ul li a { + display: block; + padding: 0.5rem 1rem; + color: #555; + text-decoration: none; + font-size: 0.95rem; + border-radius: 5px; + transition: all 0.3s ease; +} + +#toc-list ul li a:hover { + background-color: rgba(52, 152, 219, 0.1); + color: var(--secondary-color); +} + +/* Main Content */ +.course-content { + padding: 2rem; +} + +.unit-section { + margin-bottom: 4rem; + padding: 2rem; + background-color: var(--bg-color); + border-radius: 10px; + box-shadow: var(--shadow); + scroll-margin-top: 150px; +} + +.unit-header { + border-bottom: 3px solid var(--secondary-color); + padding-bottom: 1.5rem; + margin-bottom: 2rem; +} + +.unit-title { + font-size: 2rem; + color: var(--primary-color); + margin-bottom: 1rem; + font-weight: 700; + line-height: 1.4; +} + +.unit-outcome { + background: linear-gradient(135deg, #e8f4f8 0%, #d1ecf1 100%); + padding: 1.5rem; + border-radius: 8px; + border-right: 4px solid var(--secondary-color); + margin-top: 1rem; +} + +.outcome-title { + font-size: 1.2rem; + color: var(--primary-color); + margin-bottom: 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.outcome-title .icon { + font-size: 1.5rem; +} + +.outcome-text { + font-size: 1.05rem; + line-height: 1.8; + color: #444; +} + +/* Topics */ +.topics-container { + margin-top: 2rem; +} + +.topic-container { + margin-bottom: 3rem; + padding: 1.5rem; + background-color: var(--light-bg); + border-radius: 8px; + scroll-margin-top: 150px; +} + +.topic-title { + font-size: 1.6rem; + color: var(--primary-color); + margin-bottom: 1.5rem; + padding-bottom: 0.75rem; + border-bottom: 2px solid var(--border-color); + display: flex; + align-items: center; + gap: 0.75rem; +} + +.topic-number { + background: var(--secondary-color); + color: white; + padding: 0.4rem 0.8rem; + border-radius: 5px; + font-size: 1rem; + font-weight: 700; + min-width: 50px; + text-align: center; +} + +/* Subtopics */ +.subtopic-container { + margin-bottom: 2.5rem; + padding: 1.5rem; + background-color: white; + border-radius: 8px; + border-right: 4px solid var(--secondary-color); + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + scroll-margin-top: 150px; +} + +.subtopic-title { + font-size: 1.3rem; + color: var(--primary-color); + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.subtopic-number { + background: var(--accent-color); + color: white; + padding: 0.3rem 0.7rem; + border-radius: 5px; + font-size: 0.9rem; + font-weight: 700; + min-width: 45px; + text-align: center; +} + +.subtopic-description { + background-color: #fff3cd; + padding: 1rem; + border-radius: 5px; + margin-bottom: 1rem; + border-right: 3px solid #ffc107; +} + +.description-label { + font-weight: 600; + color: #856404; + margin-left: 0.5rem; +} + +.description-text { + color: #856404; + font-style: italic; +} + +.subtopic-content { + margin-top: 1.5rem; + line-height: 2; + color: #444; +} + +.subtopic-content p { + margin-bottom: 1rem; + text-align: justify; + font-size: 1.05rem; +} + +.subtopic-content strong { + color: var(--primary-color); + font-weight: 600; +} + +.subtopic-sources { + margin-top: 2rem; + padding: 1.5rem; + background-color: var(--light-bg); + border-radius: 8px; + border-top: 2px solid var(--border-color); +} + +.sources-title { + font-size: 1.1rem; + color: #666; + margin-bottom: 1rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.sources-title::before { + content: "🔗"; + font-size: 1.2rem; +} + +.sources-list { + list-style: none; + padding-right: 1rem; +} + +.sources-list li { + margin-bottom: 0.75rem; + padding: 0.5rem; + background-color: white; + border-radius: 5px; + transition: all 0.3s ease; +} + +.sources-list li:hover { + background-color: #e9ecef; + transform: translateX(-3px); +} + +.sources-list a { + color: var(--secondary-color); + text-decoration: none; + font-size: 0.95rem; + word-break: break-all; + transition: color 0.3s ease; +} + +.sources-list a:hover { + color: var(--accent-color); + text-decoration: underline; +} + +/* Footer */ +.course-footer { + background-color: var(--primary-color); + color: white; + text-align: center; + padding: 2rem; + margin-top: 3rem; +} + +/* Scroll to Top Button */ +.scroll-top-btn { + position: fixed; + bottom: 30px; + left: 30px; + width: 50px; + height: 50px; + background: var(--secondary-color); + color: white; + border: none; + border-radius: 50%; + font-size: 1.5rem; + cursor: pointer; + box-shadow: var(--shadow); + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + z-index: 1000; +} + +.scroll-top-btn.visible { + opacity: 1; + visibility: visible; +} + +.scroll-top-btn:hover { + background: var(--primary-color); + transform: translateY(-3px); + box-shadow: var(--shadow-hover); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .course-title { + font-size: 1.8rem; + } + + .course-header { + padding: 2rem 1.5rem; + } + + .course-content { + padding: 1rem; + } + + .unit-section { + padding: 1.5rem; + } + + .unit-title { + font-size: 1.5rem; + } + + .topic-title { + font-size: 1.3rem; + flex-direction: column; + align-items: flex-start; + } + + .subtopic-title { + font-size: 1.1rem; + flex-direction: column; + align-items: flex-start; + } + + .table-of-contents { + padding: 1rem; + position: relative; + } + + .scroll-top-btn { + bottom: 20px; + left: 20px; + width: 45px; + height: 45px; + } +} + +/* Print Styles */ +@media print { + .table-of-contents { + position: relative; + page-break-after: always; + } + + .scroll-top-btn { + display: none; + } + + .unit-section { + page-break-inside: avoid; + } + + .subtopic-container { + page-break-inside: avoid; + } +} +""" + + +def get_js_content(toc_data): + """إرجاع محتوى ملف JavaScript""" + return f"""// JavaScript for Course HTML + +// جدول المحتويات +const tocData = {toc_data}; + +// إنشاء جدول المحتويات +function buildTableOfContents() {{ + const tocList = document.getElementById('toc-list'); + if (!tocList) return; + + tocData.forEach(unit => {{ + const unitLi = document.createElement('li'); + const unitLink = document.createElement('a'); + unitLink.href = `#${{unit.id}}`; + unitLink.textContent = unit.text; + unitLi.appendChild(unitLink); + + if (unit.children && unit.children.length > 0) {{ + const topicsUl = document.createElement('ul'); + unit.children.forEach(topic => {{ + const topicLi = document.createElement('li'); + const topicLink = document.createElement('a'); + topicLink.href = `#${{topic.id}}`; + topicLink.textContent = topic.text; + topicLi.appendChild(topicLink); + topicsUl.appendChild(topicLi); + }}); + unitLi.appendChild(topicsUl); + }} + + tocList.appendChild(unitLi); + }}); + + // إضافة تأثير Scroll السلس عند النقر + document.querySelectorAll('#toc-list a').forEach(link => {{ + link.addEventListener('click', function(e) {{ + e.preventDefault(); + const targetId = this.getAttribute('href').substring(1); + const targetElement = document.getElementById(targetId); + if (targetElement) {{ + const offsetTop = targetElement.offsetTop - 100; + window.scrollTo({{ + top: offsetTop, + behavior: 'smooth' + }}); + // تحديث URL بدون إعادة تحميل الصفحة + history.pushState(null, null, `#${{targetId}}`); + }} + }}); + }}); +}} + +// زر العودة للأعلى +function setupScrollToTop() {{ + const scrollBtn = document.getElementById('scroll-top'); + if (!scrollBtn) return; + + window.addEventListener('scroll', function() {{ + if (window.pageYOffset > 300) {{ + scrollBtn.classList.add('visible'); + }} else {{ + scrollBtn.classList.remove('visible'); + }} + }}); + + scrollBtn.addEventListener('click', function() {{ + window.scrollTo({{ + top: 0, + behavior: 'smooth' + }}); + }}); +}} + +// تمييز القسم الحالي في جدول المحتويات +function highlightCurrentSection() {{ + const sections = document.querySelectorAll('.unit-section, .topic-container'); + const tocLinks = document.querySelectorAll('#toc-list a'); + + window.addEventListener('scroll', function() {{ + let current = ''; + const scrollPosition = window.pageYOffset + 150; + + sections.forEach(section => {{ + const sectionTop = section.offsetTop; + const sectionHeight = section.clientHeight; + if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {{ + current = section.getAttribute('id'); + }} + }}); + + tocLinks.forEach(link => {{ + link.classList.remove('active'); + if (link.getAttribute('href') === `#${{current}}`) {{ + link.classList.add('active'); + }} + }}); + }}); +}} + +// تهيئة عند تحميل الصفحة +document.addEventListener('DOMContentLoaded', function() {{ + buildTableOfContents(); + setupScrollToTop(); + highlightCurrentSection(); + + // إذا كان هناك hash في URL، قم بالتمرير إليه + if (window.location.hash) {{ + setTimeout(() => {{ + const targetId = window.location.hash.substring(1); + const targetElement = document.getElementById(targetId); + if (targetElement) {{ + const offsetTop = targetElement.offsetTop - 100; + window.scrollTo({{ + top: offsetTop, + behavior: 'smooth' + }}); + }} + }}, 500); + }} +}}); + +// إضافة تأثيرات تفاعلية على الروابط +document.addEventListener('DOMContentLoaded', function() {{ + const sourcesLinks = document.querySelectorAll('.sources-list a'); + sourcesLinks.forEach(link => {{ + link.addEventListener('mouseenter', function() {{ + this.style.transform = 'translateX(-3px)'; + }}); + link.addEventListener('mouseleave', function() {{ + this.style.transform = 'translateX(0)'; + }}); + }}); +}}); +""" + + +def convert_dict_to_html(data: Dict[str, Any]) -> str: + """ + تحويل Dictionary إلى HTML string + + Args: + data: Dictionary يحتوي على بيانات المقرر الدراسي + + Returns: + str: HTML string كامل + """ + html_parts = [] + + # توليد CSS و JS + css_content = get_css_content() + toc_data = generate_table_of_contents(data) + js_content = get_js_content(toc_data) + + # بداية HTML + html_parts.append(""" + + + + + {} + + + +
            +
            +

            {}

            +""".format( + escape(data.get('course_name', 'مقرر دراسي')), + css_content, + escape(data.get('course_name', 'مقرر دراسي')) + )) + + # الجمهور المستهدف + if 'course_audience' in data and data['course_audience']: + html_parts.append("""
            +

            الجمهور المستهدف

            +

            {}

            +
            +""".format(escape_html(data['course_audience']))) + + html_parts.append("""
            + + + +
            +""") + + # معالجة الوحدات + if 'results' in data and isinstance(data['results'], list): + for unit_idx, unit in enumerate(data['results'], 1): + # عنوان الوحدة + if 'unit_name' in unit: + unit_id = f"unit-{unit_idx}" + html_parts.append(f"""
            +
            +

            الوحدة {unit_idx}: {escape(unit['unit_name'])}

            +""") + + # مخرجات الوحدة + if 'unit_outcome' in unit and unit['unit_outcome']: + html_parts.append(f"""
            +

            + 🎯 + مخرجات التعلم للوحدة +

            +

            {escape_html(unit['unit_outcome'])}

            +
            +""") + + html_parts.append("
            ") + + # معالجة الموضوعات + if 'topics' in unit and isinstance(unit['topics'], list): + html_parts.append("""
            +""") + + for topic_idx, topic in enumerate(unit['topics'], 1): + # عنوان الموضوع + if 'topic_title' in topic: + topic_id = f"unit-{unit_idx}-topic-{topic_idx}" + html_parts.append(f"""
            +

            + {unit_idx}.{topic_idx} + {escape(topic['topic_title'])} +

            +""") + + # معالجة المحاور الفرعية (Subtopics) + if 'subtopics' in topic and isinstance(topic['subtopics'], list): + for subtopic_idx, subtopic in enumerate(topic['subtopics'], 1): + subtopic_id = f"unit-{unit_idx}-topic-{topic_idx}-subtopic-{subtopic_idx}" + html_parts.append(f"""
            +

            + {unit_idx}.{topic_idx}.{subtopic_idx} + {escape(subtopic.get('title', ''))} +

            +""") + + # الوصف + if 'description' in subtopic and subtopic['description']: + html_parts.append(f"""
            + الوصف: + {escape_html(subtopic['description'])} +
            +""") + + # المحتوى المولد + if 'generated_content' in subtopic and subtopic['generated_content']: + content = subtopic['generated_content'].strip() + # تحويل Markdown إلى HTML + markdown_html = markdown_to_html(content) + + html_parts.append('
            ') + html_parts.append(markdown_html) + html_parts.append("
            ") + + # المصادر + if 'sources' in subtopic and subtopic['sources'] and len(subtopic['sources']) > 0: + html_parts.append('
            ') + html_parts.append('
            المصادر:
            ') + html_parts.append(' ") + html_parts.append("
            ") + + html_parts.append("
            ") + + html_parts.append("
            ") + + html_parts.append("
            ") + + html_parts.append("
            ") + + html_parts.append("""
            + +
            +

            تم إنشاء هذا المقرر بواسطة أداة تحويل JSON إلى HTML

            +
            +
            + + + + + +""".format(js_content)) + + return '\n'.join(html_parts) + diff --git a/core/content_crew_config.py b/core/content_crew_config.py new file mode 100644 index 0000000000000000000000000000000000000000..6a2d72f4cd63bca5c577520a18a82d3bac779a54 --- /dev/null +++ b/core/content_crew_config.py @@ -0,0 +1,58 @@ +# app/core/crew_config.py +import litellm +from crewai import Crew, Process + +# ====== LITELLM GLOBAL CONFIG ====== +litellm.num_retries = 4 +litellm.retry_policy = { + "TimeoutError": 4, + "APIError": 3, + "OpenRouterException": 3, + "Exception": 2, +} +litellm.request_timeout = 120 +litellm.drop_params = True + +# # ====== IMPORT AGENT + TASK ====== +# from agents.content_phase import ( +# course_writer_agent, +# course_writer_task, +# ) # انت عندك دول جاهزين + + +# # ====== CACHED CREW (يُستخدم في أي مكان) ====== +# agent = course_writer_agent() +# task = course_writer_task(agent) +# cached_crew = Crew( +# agents=[agent], +# tasks=[task], +# process=Process.sequential, +# memory=False, +# verbose=False, +# ) + +# ====== SAFE RUN WRAPPER ====== +import json +import time + + +def safe_run_cached(inputs, cached_crew, retries: int = 4): + """ + يلف حوالين cached_crew.kickoff ويحاول يعمل JSON parsing كذا مرة + ويرجع (generated_content, sources) + """ + + for attempt in range(1, retries + 1): + result = cached_crew.kickoff(inputs=inputs) + + try: + # لو result.raw موجود بنفس الشكل اللي كنت بتستخدمه + raw = json.loads(result.raw) + return raw.get("generated_content", ""), raw.get("sources", []) + except Exception: + print(f"❌ JSON Error attempt {attempt}") + # تطبع أول جزء من الرد عشان الديبج + print(str(result)[:300]) + time.sleep(0.7) + + return "", [] diff --git a/core/content_services.py b/core/content_services.py new file mode 100644 index 0000000000000000000000000000000000000000..0de0679d99e4f0dd9bd8655bdc1dfa313689872e --- /dev/null +++ b/core/content_services.py @@ -0,0 +1,98 @@ +# app/core/content_service.py +from concurrent.futures import ThreadPoolExecutor +from crewai import Crew, Process +from .content_crew_config import safe_run_cached +from agents.content_phase import ( + course_writer_agent, + course_writer_task, +) # انت عندك دول جاهزين + +MAX_UNIT_WORKERS = 3 + + +def process_unit(unit: dict, course_title: str) -> dict: + output_unit = { + "unit_name": unit["unit_name"], + "unit_outcome": unit["outcome"], + "topics": [], + } + + # ====== CACHED CREW (يُستخدم في أي مكان) ====== + agent = course_writer_agent() + task = course_writer_task(agent) + cached_crew = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + memory=False, + verbose=False, + ) + + for topic in unit["topics"]: + topic_entry = {"topic_title": topic["title"], "subtopics": []} + + # sequential subtopics for highest quality + for sub in topic["subtopics"]: + combined_scraped = "\n\n".join( + r.get("scraped_content", "")[:1500] + for r in sub.get("results", []) + if "scraped_content" in r + ) + + inputs = { + "course_title": course_title, + "unit_title": unit["unit_name"], + "topic_title": topic["title"], + "subtopic_title": sub["title"], + "subtopic_description": sub["description"], + "combined_scraped_texts": combined_scraped, + } + + print(f"🟢 Writing: {sub['title']}") + + + generated, used_sources = safe_run_cached(inputs, cached_crew=cached_crew) + + topic_entry["subtopics"].append( + { + "title": sub["title"], + "description": sub["description"], + "generated_content": generated, + "sources": used_sources, + } + ) + + output_unit["topics"].append(topic_entry) + + return output_unit + + +def generate_course_content(course_data: dict) -> dict: + """ + دي اللي هتناديها من الـ FastAPI + course_data شكلها زي اللي كان في الفايل: + { + "course_name": ..., + "course_audience": ..., + "units": [...] + } + """ + units = course_data["units"] + final_results_ordered = [None] * len(units) + + futures = [] + with ThreadPoolExecutor(max_workers=MAX_UNIT_WORKERS) as executor: + for idx, unit in enumerate(units): + future = executor.submit(process_unit, unit, course_data["course_name"]) + futures.append((idx, future)) + + for idx, future in futures: + final_results_ordered[idx] = future.result() + + output = { + "course_name": course_data["course_name"], + "course_audience": course_data["course_audience"], + "results": final_results_ordered, + } + + return output diff --git a/core/details_before_content_converter.py b/core/details_before_content_converter.py new file mode 100644 index 0000000000000000000000000000000000000000..221a1454193aa257af978127b49a61b935e8f10a --- /dev/null +++ b/core/details_before_content_converter.py @@ -0,0 +1,319 @@ +from typing import Dict + +html_template = """ + + + + + {{COURSE_TITLE}} + + + + + + + + + +
            + +

            {{COURSE_TITLE}}

            +

            {{TARGET_AUDIENCE}}

            +
            + +
            + {{UNITS}} +
            + +
            + LMS-Ready • Generated by AI Agent +
            + + + + + +""" + +def find_unit(units, name): + for u in units: + if u["unit_name"] == name: + return u + return {} + + +def render_course_html( + course: Dict, introductions: Dict, evaluations: Dict, template=html_template +) -> str: + """ + Builds full course HTML from crew outputs + """ + + units_html = "" + + for idx, unit in enumerate(course["expanded_units"]): + name = unit["unit_name"] + + expanded_unit = find_unit(introductions["expanded_units"], name) + trainer_unit = find_unit(evaluations["expanded_units"], name) + + unit_id = f"unit_{idx}" + + units_html += f""" +
            +
            +

            {name}

            ▼ +
            + +
            +
            + + + +
            + +
            +

            الأهداف

            +
              {''.join(f'
            • {o}
            • ' for o in unit["unit_objectives"])}
            + +

            وصف الوحدة

            +

            {unit["unit_description"]}

            + +

            مخرجات التعلم

            +
              {''.join(f'
            • {o}
            • ' for o in unit["learning_outcomes"])}
            +
            + +
            +

            التمهيد

            +

            {expanded_unit.get("unit_introudction", "")}

            +
            + +
            + + +
            +
            +
            + """ + + return ( + template.replace("{{COURSE_TITLE}}", course["course_name"]) + .replace("{{TARGET_AUDIENCE}}", course["course_audience"]) + .replace("{{UNITS}}", units_html) + ) diff --git a/core/quiz_converter_service.py b/core/quiz_converter_service.py new file mode 100644 index 0000000000000000000000000000000000000000..d7117bbd765ce3be88899ab7216154539aef1d95 --- /dev/null +++ b/core/quiz_converter_service.py @@ -0,0 +1,757 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +FastAPI Service لتحويل بيانات الأسئلة من Dictionary إلى HTML +FastAPI Service for Converting Questions Data from Dictionary to HTML +""" + +import re +from html import escape +from typing import Dict, Any + + +def escape_html(text): + """تهريب HTML""" + if not text: + return "" + return escape(str(text)) + + +def markdown_to_html(text): + """تحويل Markdown بسيط إلى HTML""" + if not text: + return "" + + text = str(text) + + # العناوين + text = re.sub(r'^### (.*?)$', r'

            \1

            ', text, flags=re.MULTILINE) + text = re.sub(r'^## (.*?)$', r'

            \1

            ', text, flags=re.MULTILINE) + text = re.sub(r'^# (.*?)$', r'

            \1

            ', text, flags=re.MULTILINE) + + # النص العريض + text = re.sub(r'\*\*(.*?)\*\*', r'\1', text) + text = re.sub(r'__(.*?)__', r'\1', text) + + # النص المائل + text = re.sub(r'\*(.*?)\*', r'\1', text) + text = re.sub(r'_(.*?)_', r'\1', text) + + # القوائم + text = re.sub(r'^\d+\.\s+(.*?)$', r'
          • \1
          • ', text, flags=re.MULTILINE) + text = re.sub(r'^-\s+(.*?)$', r'
          • \1
          • ', text, flags=re.MULTILINE) + + # الفقرات + paragraphs = text.split('\n\n') + html_paragraphs = [] + in_list = False + + for para in paragraphs: + para = para.strip() + if not para: + continue + + if para.startswith('
          • ') or para.startswith(''): + html_paragraphs.append('
          ') + in_list = False + if para.startswith('
        3. ') and not in_list: + html_paragraphs.append('
            ') + in_list = True + html_paragraphs.append(para) + else: + if in_list: + html_paragraphs.append('
          ') + in_list = False + if not para.startswith('<'): + html_paragraphs.append(f'

          {escape_html(para)}

          ') + else: + html_paragraphs.append(para) + + if in_list: + html_paragraphs.append('
      ') + + return '\n'.join(html_paragraphs) + + +def get_difficulty_color(difficulty): + """الحصول على لون حسب مستوى الصعوبة""" + difficulty_lower = str(difficulty).lower() if difficulty else "" + if "سهل" in difficulty_lower or "easy" in difficulty_lower: + return "#27ae60" # أخضر + elif "صعب" in difficulty_lower or "hard" in difficulty_lower or "difficult" in difficulty_lower: + return "#e74c3c" # أحمر + else: + return "#f39c12" # برتقالي + + +def convert_questions_dict_to_html(data: Dict[str, Any]) -> str: + """ + تحويل Dictionary للأسئلة إلى HTML string + + Args: + data: Dictionary يحتوي على بيانات الأسئلة + - course_name: اسم المقرر + - course_audience: الجمهور المستهدف + - units: قائمة الوحدات + - unit_name: اسم الوحدة + - multiple_choice: قائمة أسئلة الاختيار من متعدد + - true_false: قائمة أسئلة الصح والخطأ + + Returns: + str: HTML string كامل + """ + course_name = data.get('course_name', 'الأسئلة') + course_audience = data.get('course_audience', '') + units = data.get('units', []) + + html_parts = [] + + # Header + html_parts.append(f""" + + + + + {escape_html(course_name)} - الأسئلة + + + + +
      +

      {escape_html(course_name)}

      +

      {escape_html(course_audience)}

      +
      + +
      +
      +

      📝 قسم الأسئلة

      +

      هذا القسم يحتوي على أسئلة المقرر. يمكنك إظهار/إخفاء الإجابات والشرح لكل سؤال.

      +
      """) + + # Units + for unit_idx, unit in enumerate(units): + unit_name = unit.get('unit_name', f'الوحدة {unit_idx + 1}') + multiple_choice_questions = unit.get('multiple_choice', []) + true_false_questions = unit.get('true_false', []) + total_questions = len(multiple_choice_questions) + len(true_false_questions) + + # جميع الوحدات تبدأ مغلقة + html_parts.append(f"""
      +
      +

      {escape_html(unit_name)} ({total_questions} سؤال)

      + ▼ +
      +
      """) + + if total_questions == 0: + html_parts.append("

      لا توجد أسئلة في هذه الوحدة

      ") + else: + q_counter = 0 + + # أسئلة الاختيار من متعدد + if multiple_choice_questions: + for q_idx, question_data in enumerate(multiple_choice_questions): + q_counter += 1 + question_text = question_data.get('question', '') + options = question_data.get('options', []) + answer = question_data.get('answer', '') + explanation = question_data.get('explanation', '') + difficulty = question_data.get('difficulty', 'متوسط') + topic_related = question_data.get('topic_question_related_to', '') + + difficulty_color = get_difficulty_color(difficulty) + + html_parts.append(f"""
      +
      +
      {q_counter}
      +
      {escape_html(question_text)}
      +
      {escape_html(difficulty)}
      +
      + +
        """) + + for opt_idx, option in enumerate(options): + option_letters = ['أ', 'ب', 'ج', 'د', 'هـ', 'و'] + option_label = option_letters[opt_idx] if opt_idx < len(option_letters) else chr(65 + opt_idx) + + html_parts.append(f"""
      • + {option_label}) + {escape_html(option)} +
      • """) + + html_parts.append("
      ") + + # Answer section + html_parts.append(f""" + +
      +
      + الإجابة الصحيحة: +
      {escape_html(answer)}
      +
      """) + + if explanation: + explanation_html = markdown_to_html(explanation) + html_parts.append(f"""
      +
      📝 الشرح:
      +
      {explanation_html}
      +
      """) + + if topic_related: + html_parts.append(f""" """) + + html_parts.append("
      ") + html_parts.append("
      ") + + # أسئلة الصح والخطأ + if true_false_questions: + for q_idx, question_data in enumerate(true_false_questions): + q_counter += 1 + question_text = question_data.get('question', '') + answer = question_data.get('answer', '') + explanation = question_data.get('explanation', '') + difficulty = question_data.get('difficulty', 'متوسط') + topic_related = question_data.get('topic_question_related_to', '') + + difficulty_color = get_difficulty_color(difficulty) + + html_parts.append(f"""
      +
      +
      {q_counter}
      +
      {escape_html(question_text)}
      +
      {escape_html(difficulty)}
      +
      + +
      +
      + ✓ + صحيح +
      +
      + ✗ + خطأ +
      +
      + + + +
      +
      + الإجابة الصحيحة: +
      {escape_html(answer)}
      +
      """) + + if explanation: + explanation_html = markdown_to_html(explanation) + html_parts.append(f"""
      +
      📝 الشرح:
      +
      {explanation_html}
      +
      """) + + if topic_related: + html_parts.append(f""" """) + + html_parts.append("
      ") + html_parts.append("
      ") + + # إغلاق الوحدة + html_parts.append("
      ") + html_parts.append("
      ") + + html_parts.append("""
      + +
      +

      تم إنشاء هذه الصفحة بواسطة أداة تحويل الأسئلة إلى HTML

      +
      + + + + + +""") + + return '\n'.join(html_parts) + diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c849c563783c986e86196c2feca1933418c6de93 --- /dev/null +++ b/modules/__init__.py @@ -0,0 +1,49 @@ +from .env import ( + llm, + llm_g, + llm_5m, + llm_4m, + llm_5n, + llm_oss, + llm_oss_120b, + llm_open_c3, + llm_open_c35, + llm_open_g25pro, + llm_open_nova, + llm_open_hermes, + llm_open_grok, + llm_qwen, + model, + model_open_router, +) +from .inputs import ( + TRUSTED_SITES, + bloom_taxonomy_1, + bloom_taxonomy_2, + html_template, + objectives_dimensions, + example_reference, + Example1, + Example2, + Example3, + Example4, + Example5, + example_reference2, + example_reference3, + example_reference4, + intro_reference1, + html_template_intro, + enrichment_examples, + # outlines, + # objectives, +) + +from .serper_env import SERPER_API_KEYS, get_valid_serper_key,get_serper_credits + +# from .directory import ( +# output_dir, +# keywords_dir, +# outliner_dir, +# scraper_dir, +# urls_dir +# ) diff --git a/modules/directory.py b/modules/directory.py new file mode 100644 index 0000000000000000000000000000000000000000..900af862a1d6a5872a6274175666c8af148b21da --- /dev/null +++ b/modules/directory.py @@ -0,0 +1,12 @@ +# import os + +# current_dir = os.path.dirname(os.path.abspath(__file__)) +# base_dir = os.path.dirname(current_dir) + +# output_dir = os.path.join(base_dir, "assets", "ai_agent_output") +# keywords_dir = os.path.join(base_dir, "assets", "keywords") +# outliner_dir = os.path.join(base_dir, "assets", "outliner_sections") +# scraper_dir = os.path.join(base_dir, "assets", "scraped_data") +# urls_dir = os.path.join(base_dir, "assets", "urls") + + diff --git a/modules/env.py b/modules/env.py new file mode 100644 index 0000000000000000000000000000000000000000..c1287dc052948eade16338d58908eb61a2d88106 --- /dev/null +++ b/modules/env.py @@ -0,0 +1,142 @@ +from crewai import LLM +import os +from dotenv import load_dotenv +from pathlib import Path +from langchain_openai import ChatOpenAI +from langchain_openrouter import ChatOpenRouter + +env_path = Path(__file__).resolve().parents[1] / ".env" +# Load variables from .env file +load_dotenv(dotenv_path=env_path) + +# Now you can access them +gemini_key = os.getenv("GEMINI_API_KEY") +tavily_key = os.getenv("TAVILY_API_KEY") +openrouter_key = os.getenv("OPENROUTER_API_KEY") +os.environ["ANTHROPIC_API_KEY"] = openrouter_key +# llm = LLM(model="gemini/gemini-2.5-flash", temperature=0, api_key=gemini_key) +# llm = LLM( +# model="openrouter/openai/gpt-oss-20b:free", +# api_key=openrouter_key, +# base_url="https://openrouter.ai/api/v1", +# ) +def llm(): + return LLM( + model="openrouter/openai/gpt-oss-20b:free", + api_key=openrouter_key, + base_url="https://openrouter.ai/api/v1", +) +def llm_oss(): + return LLM( + model="openrouter/openai/gpt-oss-20b:free", + api_key=openrouter_key, + base_url="https://openrouter.ai/api/v1", +) +def llm_oss_120b(): + return LLM( + model="openai/gpt-oss-120b:nitro", + api_key=openrouter_key, + base_url="https://openrouter.ai/api/v1", +) + + +def llm_g(): + return LLM( + model="openrouter/google/gemini-2.5-flash", + temperature=0, + api_key=openrouter_key, + base_url="https://openrouter.ai/api/v1", +) + +def llm_5m(): + return LLM( + model="gpt-5-mini-2025-08-07", +) + +def llm_4m(): + return LLM( + model="gpt-4.1-mini-2025-04-14", +) + +def llm_5n(): + return LLM( + model="gpt-5-nano-2025-08-07", + ) + +def llm_open_c3(): + return LLM( + api_key=os.environ["OPENROUTER_API_KEY"], + base_url="https://openrouter.ai/api/v1", + model="openrouter/anthropic/claude-3-haiku", + # streaming=False, + ) + +def llm_open_c35(): + return LLM( + api_key=os.environ["OPENROUTER_API_KEY"], + base_url="https://openrouter.ai/api/v1", + model="openrouter/anthropic/claude-3.5-haiku", + # streaming=False, +) +def llm_open_nova(): + return LLM( + api_key=os.environ["OPENROUTER_API_KEY"], + base_url="https://openrouter.ai/api/v1", + model="openrouter/amazon/nova-2-lite-v1", + # streaming=False, + # tools=[], + ) + +def llm_open_hermes(): + return LLM( + api_key=os.environ["OPENROUTER_API_KEY"], + base_url="https://openrouter.ai/api/v1", + model="openrouter/nousresearch/hermes-3-llama-3.1-405b:free", + # streaming=False, +) + +def llm_open_grok(): + return LLM( + api_key=os.environ["OPENROUTER_API_KEY"], + base_url="https://openrouter.ai/api/v1", + model="openrouter/x-ai/grok-4.1-fast", + # streaming=False, +) + +def llm_open_g25pro(): + return LLM( + api_key=os.environ["OPENROUTER_API_KEY"], + base_url="https://openrouter.ai/api/v1", + model="openrouter/google/gemini-2.5-pro", + # streaming=False, + max_output_tokens=40000, +) + +# Initialize Gemini model +def model(): + return ChatOpenAI( + model="google/gemini-2.5-flash", + temperature=0, + api_key=openrouter_key, + base_url="https://openrouter.ai/api/v1", +) + +def llm_qwen(): + return LLM( + api_key=os.environ["OPENROUTER_API_KEY"], + base_url="https://openrouter.ai/api/v1", + model="openrouter/qwen/qwen3.6-plus:free", + # streaming=False, + ) + +def model_open_router(): + return ChatOpenRouter( + model="x-ai/grok-4.1-fast", + api_key=os.getenv("OPENROUTER_API_KEY"), + temperature=0.2, + max_retries=2, + ) + +# # check llm +# response=llm_g.call("Hello, how are you?") +# print(response) diff --git a/modules/inputs.py b/modules/inputs.py new file mode 100644 index 0000000000000000000000000000000000000000..70927d86f06ebc6d7cc5eef443a0bdea25f797b2 --- /dev/null +++ b/modules/inputs.py @@ -0,0 +1,2155 @@ +import os +import json +# from .directory import output_dir +###################--Inputs--####################### +""" +/*********************************************************************************************************************************************/ +title="The main topic to generate an outline for", +description="The specific subject or concept that the outline should focus on. Example: 'Time Management', 'Machine Learning Basics', 'Healthy Nutrition'") +/*********************************************************************************************************************************************/ +title="Knowledge domain", +description="The broader field of knowledge or industry where the topic belongs. Example: IT, Engineering, Education, Healthcare, Business" +/*********************************************************************************************************************************************/ +title="Type of content", +description=( + "Educational → for structured learning (e.g., lessons, modules)\n" + "Awareness → for informative/raising awareness purposes (e.g., campaigns, workshops)\n" + "Training → for skill development with step-by-step practice (e.g., hands-on, exercises)" + ) +/*********************************************************************************************************************************************/ +title="Target audience", +description=( + "The group of people intended to benefit from the material. Example:\n" + "- Kids (age 6–12)\n" + "- High School Students (teenagers)\n" + "- University Students (academic level)\n" + "- Professionals (working individuals)\n" + "- General Public (all ages, non-technical)" + ) +/*********************************************************************************************************************************************/ +title="Nature of the scientific material", +description=( + "Defines the learning approach:\n" + "- Conceptual → focuses on theories, principles, and abstract knowledge.\n" + "- Procedural → step-by-step methods, instructions, or processes.\n" + "- Structural → frameworks, models, and organized structures.\n" + "- Personal → reflections, personal skills, and self-development aspects.\n" + "- Realistic → practical, case-based, real-world examples and applications." + ) +/*********************************************************************************************************************************************/ + +""" + +# inputs={ +# "topic":"بدله الفضاء", +# "domain": "Aerospace Engineering", +# "content_type": "Educational", +# "audience": "University Students", +# "material_type": ["Procedural","Realistic","Personal"] +# } + + +TRUSTED_SITES = [ + "scholar.google.com", + "researchgate.net", + "doaj.org", + "scopus.com", + "academia.edu", + "almanhal.com", + "digitallibrary.un.org", + "virtuallrc.com", + "dlc.dlib.indiana.edu", + "pubmed.ncbi.nlm.nih.gov", + "arxiv.org", + "archiveshub.jisc.ac.uk", +] + + +###################--BLOOM 1--####################### +bloom_taxonomy_1 = { + "Remember": """The process of recalling facts, terms, concepts, or basic principles from memory +without necessarily understanding them. This level involves recognition and recall +of previously learned material, such as definitions, lists, dates, formulas, or key terms. +The focus is strictly on accurate retrieval, not comprehension or application. +It is the foundation for higher-order thinking because learners must first be able +to retrieve information before they can understand, apply, or analyze it.""", + "Understand": """The process of constructing meaning from instructional materials and messages. +It involves explaining ideas or concepts, interpreting information, summarizing content, +and demonstrating comprehension of facts, rules, or principles. Learners show that they +grasp the essence of material by restating it in their own words, providing basic +interpretations, or making connections. At this level, students demonstrate they +understand the 'what' and 'why' of knowledge rather than just memorizing it.""", + "Apply": """The process of using learned material in new and concrete situations. +It includes executing procedures, implementing methods, or applying principles +to solve problems. Learners demonstrate their ability to transfer knowledge to +practical tasks, exercises, or unfamiliar contexts. This involves using facts, +techniques, and rules in a different way than originally learned, adapting them +to meet the needs of new situations. Application shows the ability to move beyond +rote memory and demonstrate understanding through action.""", + "Analyze": """The process of examining material in detail and breaking it into smaller components +to understand its structure and relationships. This involves identifying motives, causes, +assumptions, or evidence and distinguishing between facts and opinions. Learners also +draw conclusions, detect patterns, and find evidence to support generalizations. +Analysis requires uncovering how parts work together and how they contribute +to the overall meaning or outcome. It is a critical step that prepares learners +for evaluation and creation.""", + "Evaluate": """The process of presenting opinions and defending them by making judgments +about information, the validity of ideas, or the quality of work based on a set +of criteria. It includes checking, critiquing, and reviewing by applying logical reasoning +or evidence. Learners assess the strengths and weaknesses of approaches, solutions, +or arguments, providing justification for their conclusions. Evaluation demands both +critical thinking and the ability to apply standards consistently, making it +a higher-order skill that guides decision-making.""", + "Create": """The process of putting elements together to form a novel, functional whole. +It involves reorganizing concepts into a new pattern or structure, such as through planning, +design, or production. Learners generate original ideas, propose innovative solutions, +or produce unique work by combining existing knowledge in new ways. This level requires +synthesis, imagination, and innovation. Creation is the highest level of Bloom’s taxonomy, +demonstrating mastery by building something entirely new from prior knowledge +and skills.""", +} + + +###################--BLOOM 2--####################### + +bloom_taxonomy_2 = { + "Remember": { + "description": """The process of recalling facts, terms, concepts, or basic principles from memory +without necessarily understanding them. This level involves recognition and recall +of previously learned material, such as definitions, lists, dates, formulas, or key terms. +The focus is strictly on accurate retrieval, not comprehension or application. +It is the foundation for higher-order thinking because learners must first be able +to retrieve information before they can understand, apply, or analyze it.""", + "Remember_verbs_examples": [ + "يختار", + "يراقب", + "يعرض", + "ينسخ", + "يتهجى", + "يُعرّف", + "يقتبس", + "يقرأ", + "يتعرف على", + "يحدد", + "يكرر", + "يكتب", + "يعيد سرد", + "يصف", + "يطابق", + "يسمي", + "يرقم", + "يتلو", + "يبحث", + ], + }, + "Understand": { + "description": """The process of constructing meaning from instructional materials and messages. +It involves explaining ideas or concepts, interpreting information, summarizing content, +and demonstrating comprehension of facts, rules, or principles. Learners show that they +grasp the essence of material by restating it in their own words, providing basic +interpretations, or making connections. At this level, students demonstrate they +understand the 'what' and 'why' of knowledge rather than just memorizing it.""", + "Understand_verbs_examples": [ + "يسأل", + "يستشهد", + "يعمم", + "يتنبأ", + "يصنف", + "يقارن", + "يوضح", + "يَعُد", + "يناقش", + "يستدل", + "يراجع", + "يفسر", + "يعرض", + "يشرح", + "يلخص", + "يعبر عن", + "يترجم", + "يربط", + "يصف", + "يحول", + "يكتشف", + "يميز", + "يجمع", + "يحدد", + "يعيد الصياغة", + "يلخص", + ], + }, + "Apply": { + "description": """The process of using learned material in new and concrete situations. +It includes executing procedures, implementing methods, or applying principles +to solve problems. Learners demonstrate their ability to transfer knowledge to +practical tasks, exercises, or unfamiliar contexts. This involves using facts, +techniques, and rules in a different way than originally learned, adapting them +to meet the needs of new situations. Application shows the ability to move beyond +rote memory and demonstrate understanding through action.""", + "Apply_verbs_examples": [ + "يمثل", + "يوظف", + "يمارس", + "يدير", + "يجرب", + "يبني", + "يحسب", + "يحاكي", + "يحل", + "يستخدم", + "ينظم", + "يطور", + "يؤدي", + "يخطط", + "ينفذ", + "يرسم بياني", + "يستكمل", + "يفحص", + "يشرح", + "يحرر", + "يحمل", + "يشغل", + "يلون", + "يلعب", + "يحضر", + "يشارك", + "يرسم", + "يستخدم", + ], + }, + "Analyze": { + "description": """The process of examining material in detail and breaking it into smaller components +to understand its structure and relationships. This involves identifying motives, causes, +assumptions, or evidence and distinguishing between facts and opinions. Learners also +draw conclusions, detect patterns, and find evidence to support generalizations. +Analysis requires uncovering how parts work together and how they contribute +to the overall meaning or outcome. It is a critical step that prepares learners +for evaluation and creation.""", + "Analyze_verbs_examples": [ + "يحلل", + "يفحص", + "يحدد الأولويات", + "يجد", + "يرتب", + "يفترض", + "يوظف", + "يسلط الضوء", + "يناقش", + "يعيد تنظيم", + "يبحث", + "يستدل", + "يحدد الاختلافات", + "يكتشف", + "يبسط", + "يشرح", + "يحذف", + "يقارن", + "يربط بين", + "يستنتج", + "يفرق", + "يميز", + "يفصل", + "يقدر", + "يرسم الخرائط الذهنية", + "يحدد الخطوط العريضة بخطط", + ], + }, + "Evaluate": { + "description": """The process of presenting opinions and defending them by making judgments +about information, the validity of ideas, or the quality of work based on a set +of criteria. It includes checking, critiquing, and reviewing by applying logical reasoning +or evidence. Learners assess the strengths and weaknesses of approaches, solutions, +or arguments, providing justification for their conclusions. Evaluation demands both +critical thinking and the ability to apply standards consistently, making it +a higher-order skill that guides decision-making.""", + "Evaluate_verbs_examples": [ + "يعطي رأي", + "يدحض", + "يقيس", + "يُقيم", + "يجادل", + "يقدر", + "يقنع", + "يثبت", + "يعطي الأسباب", + "يومي", + "يصدر حكم على", + "يدعم", + "يضع معايير", + "يتحقق من صحة", + "يقرر", + "يفسر", + "يحكم على", + "يبرر", + "يتجادل", + "ينتقد", + "يناقش", + "يدافع", + "يحرر", + "يفترض", + "يقيس", + "يراقب", + "يتنبأ", + "يعيد صياغة", + ], + }, + "Create": { + "description": """The process of putting elements together to form a novel, functional whole. +It involves reorganizing concepts into a new pattern or structure, such as through planning, +design, or production. Learners generate original ideas, propose innovative solutions, +or produce unique work by combining existing knowledge in new ways. This level requires +synthesis, imagination, and innovation. Creation is the highest level of Bloom’s taxonomy, +demonstrating mastery by building something entirely new from prior knowledge +and skills.""", + "Create_verbs_examples": [ + "يدون", + "يتعاون", + "يؤلف", + "يبني", + "يصمم", + "يبتكر", + "يوجه", + "يضع تصور", + "يعيد صياغة", + "يخترع", + "يخلط", + "يعيد مزج", + "يعدل", + "يتفاوض", + "يخطط", + "ينتج", + "يبرمج", + "ينشر", + "يلعب الأدوار", + "يحاكي", + "يقترح حل", + "يعيد هيكلة", + "يكتب", + ], + }, +} + + +objectives_dimensions = { + "Knowledge": { + "description": "المجال المعرفي (Cognitive Domain): يركز على العمليات العقلية والفكرية، ويشمل اكتساب المعلومات، الفهم، التطبيق، التحليل، التقييم، والإبداع. يتدرج من مستويات التفكير الدنيا إلى العليا حسب تصنيف بلوم المعدل.", + "levels": [ + { + "level_name": "Remember (التذكر)", + "description": "استرجاع المعلومات والمعارف من الذاكرة طويلة المدى", + "verbs_examples": [ + "يذكر", + "يعرف", + "يحدد", + "يصنف", + "يسمي", + "يعدد", + "يسترجع", + ], + }, + { + "level_name": "Understand (الفهم)", + "description": "بناء المعنى من المعلومات الشفهية والمكتوبة والرسومية", + "verbs_examples": [ + "يفسر", + "يستنتج", + "يصف", + "يناقش", + "يترجم", + "يميز", + "يشرح", + "يلخص", + ], + }, + { + "level_name": "Apply (التطبيق)", + "description": "تنفيذ أو استخدام إجراء معين في موقف محدد", + "verbs_examples": [ + "يستخدم", + "يطبق", + "يجرب", + "يشرح", + "يبرهن", + "ينفذ", + "يحل", + ], + }, + { + "level_name": "Analyze (التحليل)", + "description": "تقسيم المادة إلى أجزائها المكونة وتحديد العلاقات بينها", + "verbs_examples": [ + "يحلل", + "يربط", + "يقارن", + "يوازن", + "يفحص", + "يميز", + "يصنف", + ], + }, + { + "level_name": "Evaluate (التقويم)", + "description": "إصدار أحكام بناءً على معايير ومحكات محددة", + "verbs_examples": [ + "يقيم", + "ينقد", + "يبرر", + "يوازن", + "يحكم", + "يدافع عن", + "يختار", + ], + }, + { + "level_name": "Create (الإبداع)", + "description": "دمج العناصر لتكوين كل متماسك أو وظيفي جديد وإعادة تنظيم العناصر في نمط أو بنية جديدة", + "verbs_examples": [ + "يصمم", + "يبتكر", + "يطور", + "يؤلف", + "ينشئ", + "يخطط", + "يبني", + ], + }, + ], + }, + "Ability": { + "description": "المجال الوجداني/الانفعالي (Affective Domain): يتعلق بالمشاعر والاتجاهات والقيم والميول. يتدرج من مجرد الاستقبال السلبي إلى التبني الكامل للقيم وجعلها جزءاً من شخصية المتعلم.", + "levels": [ + { + "level_name": "Reception (الاستقبال)", + "description": "الوعي والانتباه والاستعداد لاستقبال الظواهر والمثيرات", + "verbs_examples": ["ينتبه", "يختار", "يشعر", "يصغي", "يستقبل", "يلاحظ"], + }, + { + "level_name": "Response (الاستجابة)", + "description": "المشاركة النشطة والتفاعل مع الظواهر والاستجابة لها", + "verbs_examples": [ + "يستجيب", + "يشارك", + "يعاون", + "يمتثل", + "يتطوع", + "يساعد", + "يناقش", + ], + }, + { + "level_name": "Valuing (التقييم)", + "description": "إعطاء قيمة للأشياء والظواهر والسلوكيات والالتزام بها", + "verbs_examples": [ + "يقدر", + "يحترم", + "يؤيد", + "يدعم", + "يظهر اقتناعاً", + "يعتز", + ], + }, + { + "level_name": "Organization (التنظيم)", + "description": "تنظيم القيم المختلفة وحل التناقضات بينها وبناء نظام قيمي متسق", + "verbs_examples": [ + "ينظم", + "يقارن", + "يلتزم", + "يوازن", + "يرتب", + "يدمج", + "يوحد", + ], + }, + { + "level_name": "Characterization (التمييز/التشخيص)", + "description": "امتلاك نظام قيمي يتحكم في السلوك بشكل ثابت ومتسق ويصبح جزءاً من شخصية الفرد", + "verbs_examples": [ + "يميز", + "يفضل", + "يتبنى", + "يلتزم", + "يمارس", + "يحل", + "يتصرف وفقاً لـ", + ], + }, + ], + }, + "Skills": { + "description": "المجال المهاري/النفسحركي (Psychomotor Domain): يركز على المهارات الحركية والجسدية التي تتطلب تنسيقاً بين العقل والعضلات. يتدرج من الملاحظة البسيطة إلى الإتقان والإبداع الحركي.", + "levels": [ + { + "level_name": "Readiness (الاستعداد)", + "description": "الاستعداد الذهني والجسدي والانفعالي للقيام بالمهارة", + "verbs_examples": [ + "يتهيأ", + "يستعد", + "يظهر رغبة", + "يبدي اهتماماً", + "يتخذ وضعية", + ], + }, + { + "level_name": "Guided Response/Imitation (الاستجابة الموجهة/التقليد)", + "description": "محاكاة وتقليد المهارة تحت الإشراف والتوجيه", + "verbs_examples": ["يقلد", "يحاكي", "يكرر", "يتبع", "يجرب", "يستجيب"], + }, + { + "level_name": "Mechanism/Practice (الآلية/الممارسة)", + "description": "أداء المهارة بثقة ودقة دون الحاجة للتوجيه المستمر", + "verbs_examples": ["يمارس", "يطبق", "يؤدي", "ينفذ", "يكمل", "يعمل"], + }, + { + "level_name": "Complex Overt Response (الاستجابة الظاهرة المعقدة)", + "description": "أداء المهارة بسلاسة وإتقان وبشكل تلقائي", + "verbs_examples": [ + "يؤدي بسلاسة", + "يستخدم بانتظام", + "يتقن", + "ينجز بمهارة", + "يعمل بكفاءة", + ], + }, + { + "level_name": "Adaptation (التكيف)", + "description": "تعديل وتكييف المهارات لتناسب المواقف المختلفة", + "verbs_examples": [ + "يعدل", + "يطور", + "يبدل", + "يكيف", + "يعيد تنظيم", + "يحسن", + ], + }, + { + "level_name": "Origination/Creativity (الإبداع)", + "description": "ابتكار حركات وأنماط مهارية جديدة وإبداعية", + "verbs_examples": [ + "يبتكر", + "يصمم", + "يبدع", + "يخترع", + "يطور أسلوباً جديداً", + "يؤلف", + ], + }, + ], + }, +} +############################################################### +html_template = """ + + + + + + مرحلة التحليل + + + + + +
      إدارة الوقت
      +
      مرحلة التحليل
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      المتوقع تحقيقه (النهائي) + أن يتمكّن المتدرب من اكتساب مهارة إدارة وقته بفعالية، من خلال فهم المفهوم والأسس، + وتطبيق المهارات والتقنيات العملية، بما ينعكس على إنجاز أهدافه الشخصية والعملية بكفاءة عالية. +
      الفئة المستهدفةأعمارهم18 إلى 35 عامًا.
      الخبرات السابقة +
        +
      • ممارسة أعمال أو مهام تتطلب التخطيط أو التنظيم.
      • +
      • حضور أنشطة أو دورات سابقة في التنمية الذاتية أو المهارات الحياتية.
      • +
      • إلمام جزئي بأساسيات إدارة العمل أو الأنشطة التربوية.
      • +
      +
      خصائص المتدربين +
        +
      • امتلاك حصيلة لغوية جيدة مع قدرة على التعبير.
      • +
      • القدرة على التفكير التحليلي والمنطقي وحل المشكلات.
      • +
      • الرغبة في التفاعل مع المجموعات وتقدير العلاقات القائمة على الاحترام.
      • +
      • ميل قوي للاستقلالية في اتخاذ القرارات وحمل المسؤوليات.
      • +
      • ظهور ميول متزايدة بالذات والحاجة إلى التحكم في الانفعالات.
      • +
      • السعي لتطوير الهوية الذاتية والاجتماعية وتعزيز الثقة بالنفس.
      • +
      • تفضيل التعلم التفاعلي القائم على الأنشطة التطبيقية.
      • +
      +
      احتياجات المتدربين +
        +
      • تمييز أسباب ضياع الوقت الشخصي والتنظيمي.
      • +
      • تحقيق التوازن بين المهام والأهداف.
      • +
      • امتلاك أدوات عملية لتحديد الأولويات.
      • +
      • استخدام تقنيات جديدة أكثر كفاءة.
      • +
      • ضبط التوتر الناتج عن ضغوط المهام.
      • +
      • أساليب مجرَّبة ناجحة في استثمار الوقت.
      • +
      +
      القيودبالنسبة للمدرب +
        +
      • صعوبة التعامل مع تفاوت الخلفيات المعرفية.
      • +
      • محدودية الوقت المخصص للأنشطة التدريبية.
      • +
      • حاجة إلى تكييف الأمثلة بما يناسب الجمهور.
      • +
      • صعوبة متابعة الأداء الفردي لجميع المتدربين.
      • +
      +
      بالنسبة للمتدرب +
        +
      • محدودية الخبرة السابقة في إدارة الوقت.
      • +
      • صعوبة الالتزام بجميع التمارين المقترحة.
      • +
      • التأثر بالضغوط الشخصية والاجتماعية.
      • +
      • مقاومة تغيير العادات السلبية القديمة.
      • +
      +
      المدة الزمنيةمدة الكورس محسوبة بعدد الأيام (اليوم = 4 ساعات).
      جمع البيانات وتحليلها +
        +
      • تحديد محاور الدورة التدريبية.
      • +
      • تحليل الاحتياجات التدريبية.
      • +
      • تحديد الفجوات المعرفية والمهارية.
      • +
      • تصميم أنشطة لمعالجة الفجوات.
      • +
      • تجريب الأنشطة وتقييمها.
      • +
      • تطوير الأنشطة وتحسينها.
      • +
      +
      + + + + + """ + + +########################################################## +# # This is an input For === Objectives Agent === +# with open(os.path.join(output_dir, "curriculum_outline_final.json"), "r", encoding="utf-8") as f: +# outlines = json.load(f) + +# # This is an input For === Outcomes Agent === +# with open( +# os.path.join(output_dir, "step_analysis_2_objectives.json"), +# "r", +# encoding="utf-8", +# ) as f: +# objectives = json.load(f) + + +####################--Final Outlines Before Content Example--#################### + +example_reference = """ +الوحدة الأولى: مفاهيم الإرشاد السياحي ومكوناته + +الموضوع الأول: تعريف الإرشاد السياحي وأهميته +النقاط: +- تعريف الإرشاد السياحي +- عناصر التميّز في الإرشاد السياحي الاحترافي +- المقومات الأساسية للإرشاد السياحي +- أهمية الإرشاد السياحي +- منظومة السياحة في السعودية +- مكونات منظومة السياحة + +الموضوع الثاني: أدوار ومسؤوليات المرشد السياحي +النقاط: +- من هو المرشد السياحي؟ +- الأدوار الأساسية للمرشد السياحي +- مسؤوليات المرشد السياحي +- المهارات الواجب توافرها في المرشد السياحي + +الموضوع الثالث: أهمية الإرشاد السياحي في تعزيز تجربة السياح +النقاط: +- أهمية الإرشاد السياحي في تعزيز تجربة السياح +- الإرشاد السياحي كأداة للتفسير الثقافي +- كيف يحوّل التفسير الثقافي الزيارة إلى تجربة معرفية وعاطفية؟ +- أساليب تفسيرية فعّالة وتطبيقاتها العملية +- أمثلة للتفسير الثقافي الناجح +- الأبعاد النفسية لتجربة السائح +- العوامل النفسية الأساسية وتأثيرها العملي +- نموذج التجربة المتكاملة وتطبيقه التفسيري +- دور المرشد في خلق جوّ نفسي آمن وممتع + + +الوحدة الثانية: التواصل الفعّال مع السياح + +الموضوع الأول: مهارات الاتصال اللفظي وغير اللفظي +النقاط: +- مهارات الاتصال اللفظي (Verbal Communication Skills) +- مهارات الاتصال غير اللفظي (Non-Verbal Communication Skills) +- تطوير وتنمية مهارات الاتصال في السياحة +- تكامل الاتصال اللفظي وغير اللفظي في الإرشاد السياحي + +الموضوع الثاني: بناء علاقات إيجابية مع المجموعات السياحية +النقاط: +- فهم ديناميكيات المجموعة السياحية +- الاستماع الفعّال (Active Listening) وأثره في بناء العلاقات السياحية +- تعزيز الثقة والولاء +- التكنولوجيا كجسر للتفاعل المستمر +- توثيق الأداء والمسؤولية المهنية +- أساليب تطوير مهارات بناء العلاقات + +الموضوع الثالث: إدارة المواقف الصعبة أثناء الجولات +النقاط: +- فهم التحديات الشائعة في الجولات السياحية ومواجهتها +- تقنيات التخطيط لإدارة المشاكل والتحديات +- استخدام الاتصال الفعّال في حل المشكلات والتعامل مع الضغوط +- إدارة الأزمات وتطبيق خطط الطوارئ +- بناء علاقات استثنائية وإدارة النزاعات بفعالية +- تطبيقات عملية ودراسات حالة في التعامل مع المشاكل في الجولات السياحية +- أدوات قياس الأداء والتعلّم بعد الأزمة +- نموذج خطة إدارة الطوارئ في الجولات السياحية + + +الوحدة الثالثة: التخطيط والإعداد للجولات السياحية + +الموضوع الأول: تقنيات تخطيط المسارات السياحيالنقاط: +النقاط: +- مفهوم تخطيط المسار السياحي وأهدافه +- خطوات تخطيط مسار الجولة السياحية +- معايير اختيار الأنشطة والمزارات داخل المسار +- أدوات مساعدة في تخطيط المسارات + +الموضوع الثاني: تحديد المواقع المناسبة للجولات السياحية +النقاط: +- معايير اختيار المواقع السياحية +- تنسيق الزيارة حسب أنواع السياح +- فهم قدرات الموقع وحدود طاقته الاستيعابية + +الموضوع الثالث: إعداد البرامج السياحية بما يتناسب مع اهتمامات السياح +النقاط: +- تحليل اهتمامات واحتياجات الزوّار +- خطوات تصميم برنامج سياحي متكامل +- مبادئ تصميم البرامج المرنة (Flexible Programs) +- نماذج برامج حسب نوع السائح + + +الوحدة الرابعة: إدارة المواقف الطارئة + +الموضوع الأول: تحليل المشكلات الطارئة أثناء الجولات +النقاط: +- أنواع المشكلات الطارئة في الجولات السياحية +- خطوات التحليل السريع للحوادث (Rapid Incident Analysis) +- نموذج التقييم الثلاثي للطوارئ +- التواصل الفعّال أثناء الطوارئ + +الموضوع الثاني: تقنيات التعامل مع الأزمات +النقاط: +- مبادئ الاستجابة الأولية للأزمات +- خطوات إدارة الأزمات في السياحة +- أدوات وتقنيات الحد من المخاطر +- دور القيادة في الأزمات + +الموضوع الثالث: وضع خطط احتياطية لضمان سلامة السياح +النقاط: +- مفهوم الخطة الاحتياطية (Plan B) +- مكوّنات الخطة الاحتياطية +- تصميم خطة احتياطية فعالة +- نماذج خطط احتياطية جاهزة + + +""" +####################--Content Examples--#################### + + +Example1 = """ +اسم المقرر: مبادئ الارشاد السياحي +اسم الوحدة: مفاهيم الارشاد السياحي ومكوناته +اسم الموضوع: تعريف الارشاد السياحي وأهميته + +اسم المحور: تعريف الإرشاد السياحي + +المحتوى: + +• الإرشاد السياحي (Tourist Guiding) هو نشاط مهني يختص بمرافقة الزائرين أو المجموعات السياحية في مواقع مختلفة – تاريخية، ثقافية، طبيعية – مع تقديم المعلومة، التفسير، والتوعية، وتسهيل التفاعل مع البيئة المحلية. +الإرشاد السياحي يتضمن عناصر تقنية (معرفة المواقع، التاريخ، الثقافة، الجغرافيا)، مهارات تواصل (لغوي، تفسيري، شخصي)، مهارات إدارية وتنظيمية، ومراعاة القوانين والأنظمة التنظيمية في البلد المعني. +وفق دراسات أكاديمية، الإرشاد السياحي يُعرف بأنه نشاط محترف يربط الزائر بالمكان الثقافي/التاريخي/الطبيعي، يقدم تفسيراً معرفياً، ويُسهّل التفاعل بين السائح والمجتمع المحلي أو الموقع المُزار. مثلاً دراسة في مصر أشارت إلى أن الأداء في التفسير (interpretation) للمرشد له تأثير كبير على تجربة الزائر وولائه للوجهة. +دراسات مثل "Competencies of the Tourism Management Students in Tour Guiding" تؤكد أن المرشد يجب أن يكون ذا كفاءة معرفية ولغوية وثقافية عالية، بالإضافة لقدرات تواصلية وتنظيمية. + +""" +Example2 = """ +اسم المقرر: مبادئ الارشاد السياحي +اسم الوحدة: مفاهيم الارشاد السياحي ومكوناته +اسم الموضوع: تعريف الارشاد السياحي وأهميته + +اسم المحور: عناصر التميّز في الإرشاد السياحي الاحترافي + +المحتوى: + +• المصادقة والترخيص (Certification & Licensing): وجود مؤهلات رسمية يعترف بها من وزارة السياحة أو جهة تنظيمية، مما يضمن جودة المعلومة والموثوقية. +• اللغة الأجنبية / تعدد اللغات: القدرة على التحدث بلغة السائح أو أكثر من لغة، تسهيل التواصل، فهم الخلفية الثقافية. +• التحديث المعرفي المستمر: متابعة الأبحاث، التغيرات في السياسات، الحفريات الجديدة، الاكتشافات، الأحداث الجارية، التغير في وجهات النظر الثقافية أو الحساسية المجتمعية. +• استخدام التكنولوجيا والدعم البصري: خرائط، صور، عروض، مقاطع صوتية أو مرئية، تطبيقات، أدوات تفسيرية؛ لتحسین فہم السائح وجذب انتباهه. + +""" +Example3 = """ +اسم المقرر: مبادئ الارشاد السياحي +اسم الوحدة: مفاهيم الارشاد السياحي ومكوناته +اسم الموضوع: تعريف الارشاد السياحي وأهميته + +اسم المحور: المقاومات الاساسية للارشاد السياحى + +المحتوى: + +• التوجيه (Guiding): مرافقة السائح أو المجموعة خلال جولتهم، التبيين المكاني، التنقّل، الانتقال بين المعالم، وضمان أن العميل لا يضيع له الاتجاهات. +• التفسير (Interpretation): شرح الجانب التاريخي، الثقافي، الطبيعي، الاجتماعي؛ تزويد السائح بالمعلومات التي تربط الماضي بالحاضر، وربط المعالم بالقصص، الأساطير، التغيرات، الرموز. +• التواصل (Communication): القدرة على استخدام لغة السائح أو لغة وسيطة يفهمها، مهارات الاستماع، التحدث بوضوح، استخدام الحواس، القصة، الأسلوب، التفاعل مع أسئلة السياح. +• المعرفة (Knowledge): معرفتك التاريخ، الجغرافيا، الثقافة، الفنون، الدين، العادات والتقاليد، البيولوجيا والنباتات إذا الموقع طبيعي، والتراث المادي وغير المادي. +• المهارات التنظيمية والإدارية: التخطيط للجولة (الزمن، الترتيب، التنقلات، المواصلات، دخول المواقع، التذاكر، استراحات، السلامة)، إدارة المجموعة، التعامل مع الطوارئ، التنسيق مع الجهات المعنية. +• السلوك المهني والأخلاقي: الالتزام بالقوانين المحلية، احترام العادات والتقاليد، الحفاظ على البيئة والتراث، الشفافية، الأمانة، المظهر اللائق، الانضباط بالمواعيد. + +""" +Example4 = """ +اسم المقرر: مبادئ الارشاد السياحي +اسم الوحدة: مفاهيم الارشاد السياحي ومكوناته +اسم الموضوع: تعريف الارشاد السياحي وأهميته + +اسم المحور: أهمية الإرشاد السياحي (Importance) + +المحتوى: + +الإرشاد السياحي ليس ترفًا أو خدمة إضافية فقط؛ بل يُعتبر من الركائز الأساسية التي تؤثر مباشرة على جودة التجربة السياحية، وعلى سمعة الوجهة، وعلى التنمية المستدامة. فيما يلي أهميته مفصّلة مع أمثلة: + +1- تحسين تجربة السائح +• المرشد الجيد يُحوّل مجرد زيارة لمعلم إلى تجربة معرفية مشوقة من خلال التفسير الجيد، وربط الأحداث التاريخية بالواقع الحالي، وسرد القصة، مما يُضفي قيمة مضافة ويجعل التجربة لا تُنسى. +• مثال: دراسة في مصر (الأقصر) أظهرت أنّ الأداء في التفسير (interpretation performance) من المرشد يؤثر تأثیرًا كبيرا على مدى تذكّر الزائر للتجربة وعلى رغبته في العودة أو التوصية بالوجهة. + +2- تعزيز الصورة الذهنية للوجهة (Destination Image) وزيادة الترويج +• المرشد السياحي يعمل كـ "سفير" أو "سفيرة ثقافية" للبلد؛ أول من يلتقي بالسائح وآخر من يودّعه، فيُشكّل الانطباع الأول والأخير. جودة المعلومات، السلوك، اللغة، التنظيم كلها تنعكس على كيف يری السائح البلد. +• دراسة في مصر وجدت علاقة إيجابية مباشرة بين دور المرشد في الترويج السياحي وبين صورة الوجهة ونيّة الزائر للعودة، مع وسيط "رضا الزائر عن الجولات السياحية". + +3- المساهمة في السياحة المستدامة والمحافظة على التراث +• من خلال التفسير الواعي، المرشد يستطيع توعية الزائرين بأهمية الحفاظ على المواقع التاريخية والثقافية والطبيعية، احترام البيئة، العادات المحلية، وتشجيع سلوكيات لا تضر بالتراث. +• كذلك في السياحة الميسّرة (Accessible Tourism)، حيث يُعتبر المرشد مهمَّا جدًا لدعم السياح ذوي الاحتياجات الخاصة، من حيث التفاعل، الاستجابة، التسهيل، مما يُعزز الشمولية والجانب الإنساني في السياحة. + +4- التأثير الاقتصادي والاجتماعي +• المرشد الجيد يزيد من رضى الزائر، ما يؤدي إلى توصيات ونشر خبر إيجابي – يعني زيادة في عدد الزوار في المستقبل، وبالتالي تأثير اقتصادي مباشر على مشغّلي الجولات، المعالم، المجتمع المحلي. +• كذلك فهو يُساهم في خلق فرص عمل: مرشدون، مساعدين، مترجمين، خدمات الدعم اللوجستي، والحرف المحلية التي تُعرض للمسافرين، مما يدعم المجتمع المحلي. + +5- إدارة المجموعات والمواقف الطارئة +• المرشد يتحمّل مسؤولية كبيرة في تنظيم الوقت، التنقل، التأكّد من سلامة المجموعة، التعامل مع مواقف غير متوقعة – عبور الطرق، إصابات بسيطة، تغيّر الأحوال الجوية، اختلاف الثقافات داخل المجموعة. وجود مرشد محترف مع تدريب مناسب يُقلّل المخاطر ويُحسّن حسن سير الجولة. + +كما يمكننا أن نوضح الأهمية من وجهات نظر مختلفة كالتالي: + +أولًا: من وجهة السائح / تجربة الزائر +• يجعل زيارة المعالم أكثر إشباعًا من مجرد مشاهدة، بل من تجربة معرفية وتجريبية غنية. التفسير الجيد يساعد السائح أن يفهم لماذا هذا المكان مهم، ما حدث فيه، كيف أثر على الآخرين، وكيف يرتبط بالزائر نفسه. +• زيادة رضا السائح تُترجم إلى ولاء الوجهة: من المرجّح أن يعود، يوصي بها للآخرين، ينشر تجارب إيجابية. دراسات في مصر (المواقع التراثية في صعيد مصر) أشارت إلى أن تجربة التفسير المتميّز تزيد من "الولاء" لدى الزوار. + +ثانيا: من وجهة نظر الوجهة السياحية / الدولة +• المرشد هو "وجه" البلد أمام الزائر: انطباع أول وآخر، سواء بالمظهر، المعلومات، السلوك، اللغة. هذا يؤثر على الصورة الذهنية للبلد (destination image). +• التسويق غير الرسمي: الزائر الذي لديه تجربة جيدة يصبح مصدر دعائي (Word of Mouth)، يكتب تقييمات، ينشر صورًا، يشارك تجربته على وسائل التواصل؛ كل هذا يساوي دعاية قيمة لا تُقدّر بثمن. + +ثالثًا: من الناحية الاقتصادية والاجتماعية والتنموية +• إتاحة فرص عمل: المرشدون، مترجمون، مساعدين، خدمات النقل، التجارة المحلية، الحرفيين، المطاعم، الفنادق. +• توزيع الفوائد الاقتصادية على المجتمع المحلي: المرشد يمكن أن يروّج للمشاريع الصغيرة، الحرف المحلية، الأسواق المحلية، مما يدعم المجتمعات المضيفة. +• الحفاظ على التراث والمواقع: المرشدون يساهمون في التوعية بأهمية المحافظة على المواقع، حماية المعالم من الإهمال أو الاستغلال، تجنب السلوكيات التي تضر بالتراث أو البيئة. + +رابعًا: السياحة المستدامة والشمولية +• خدمة ذوي الاحتياجات الخاصة: المرشد الذي يعرف احتياجاتهم، يستخدم لغة الإشارة، يسهل الحركة، یکون مدرب على التعامل مع هذه الفئة، هذا يُوسّع السوق السياحي ويُحقق مبدأ الشمولية. +• التوعية البيئية: المرشد يمكنه إرشاد الزائرين إلى التصرف بمسؤولية في المواقع الطبيعية، تقليل المخلفات، استخدام الموارد الطبيعية بطريقة محترمة، احترام القوانين المتعلقة بالحيوانات والنباتات، وكذلك توعية الضيوف من التصوير أو السلوك الذي يؤثر سلبياً على البيئة. + +""" +Example5 = """ +اسم المقرر: مبادئ الارشاد السياحي +اسم الوحدة: مفاهيم الارشاد السياحي ومكوناته +اسم الموضوع: تعريف الارشاد السياحي وأهميته + +اسم المحور: منظومة السياحة في السعودية + +المحتوى: + +تُعد منظومة السياحة السعودية أحد أبرز النماذج الحديثة في تنظيم وتطوير القطاع السياحي على المستويين المحلي والدولي. وقد جاءت هذه المنظومة كجزء من تحقيق رؤية المملكة 2030 التي تهدف إلى تنويع الاقتصاد الوطني وتقليل الاعتماد على النفط من خلال جعل السياحة قطاعًا حيويًا مساهمًا في الناتج المحلي، ومولّدًا للفرص الوظيفية والاستثمارية. تضم المنظومة عددًا من الجهات الحكومية والمبادرات الوطنية التي تتكامل أدوارها لدعم السياحة وتطويرها وفق أعلى المعايير العالمية. + +مكونات منظومة السياحة: + +1- وزارة السياحة +تمثّل الجهة التنظيمية العليا في القطاع، والمسؤولة عن رسم السياسات العامة للسياحة في المملكة. +مهامها الرئيسة: +• وضع التشريعات والأنظمة واللوائح اللازمة لتطوير القطاع. +• إصدار التراخيص والتصنيفات الخاصة بالأنشطة والمنشآت السياحية. +• جذب الاستثمارات النوعية في مجالات السياحة المتنوعة. +• توفير بيئة استثمارية جاذبة ومحفّزة للمستثمرين المحليين والدوليين. +• تطوير ودعم الكوادر البشرية من خلال التدريب والتأهيل. +• خلق فرص عمل جديدة في المجالات السياحية المختلفة. + +2- الهيئة السعودية للسياحة +تُعنى بتسويق المملكة كوجهة سياحية عالمية، وتعزيز مكانتها في الأسواق الإقليمية والدولية. +مهامها الأساسية: +• إبراز المملكة كوجهة سياحية وتسويقها محليًا ودولیًا. +• تطوير الباقات والمنتجات السياحية بالتعاون مع القطاع الخاص. +• الإشراف على الحملات الترويجية والإعلامية للوجهات السياحية. +• المشاركة في المعارض والمؤتمرات السياحية داخل المملكة وخارجها. +• قياس وتحسين تجربة السائح من خلال متابعة مؤشرات الرضا والاقتراحات. + +3- صندوق التنمية السياحي +يُعد الذراع التمويلية للقطاع السياحي، ويهدف إلى تمكين المستثمرين وتحفيزهم على إنشاء المشاريع السياحية النوعية. +مهامه: +• تمويل وتطوير المناطق السياحية والمشاريع الاستثمارية. +• دعم المنشآت السياحية والخدمات المساندة بما يعزز البنية التحتية. +• إبرام العقود والاتفاقيات مع الجهات التمويلية والمستثمرين. +• المساهمة في تنمية المناطق المستهدفة لجعلها مراكز جذب سياحي مستدام. + +4- الهيئة السعودية للبحر الأحمر +تُعنى بتنظيم وتطوير الأنشطة السياحية البحرية في النطاق الجغرافي للبحر الأحمر. +مهامها: +• تنظيم الأنشطة الملاحية والبحرية السياحية والتنسيق مع الجهات ذات العلاقة. +• إصدار التراخيص والتصاريح لممارسة الأنشطة البحرية السياحية. +• تحديد المواقع والخرائط الملاحية المناسبة للنشاط السياحي البحري. +• ضمان حماية البيئة البحرية واستدامة مواردها. +• جذب الاستثمارات وتشجيع الممارسات السياحية البحرية الآمنة والمستدامة. + +5- برنامج الربط الجوي +مبادرة وطنية تهدف إلى تعزيز الاتصال الجوي بين المملكة والعالم، باعتباره عنصرًا رئيسيًا في تسهيل السياحة الدولية. +مهامه: +• تطوير المسارات الجوية وربط المملكة بالوجهات المستهدفة. +• التعاون مع شركات الطيران والمطارات المحلية والدولية لتعزيز الحركة السياحية. +• تقديم الدراسات والرؤى الاستراتيجية لتحليل الأسواق ودعم النمو. +• ضمان استدامة المسارات الجوية ودعم مبادرات النقل الجوي السياحي. + +6- مجلس التنمية السياحي +يُعد الجهة التنسيقية العليا بين جميع مكونات المنظومة. +مهامه: +• مواءمة الأهداف الوطنية للقطاع السياحي بين الجهات ذات العلاقة. +• تنظيم الجهود ومتابعة التنفيذ للمبادرات والخطط الاستراتيجية. +• تقييم الأداء العام للقطاع السياحي وقياس الأثر التنموي للمبادرات. + +""" + +example_reference2 = """ +الوحدة الأولى: مفاهيم الإرشاد السياحي ومكوناته + +المواضيع: + • تعريف الإرشاد السياحي وأهميته. + • أدوار ومسؤوليات المرشد السياحي. + • أهمية الإرشاد السياحي في تعزيز تجربة السياح. + +أهداف الوحدة: + "يعرَف مفهوم الإرشاد السياحي تعريفًا دقيقًا وواضحًا.", + "يبيّن أهمية الإرشاد السياحي في تعزيز تجربة السائح وتحقيق التنمية السياحية.", + "يحدد أدوار ومسؤوليات المرشد السياحي وفق المعايير المهنية.", + "يفسر العلاقة بين الإرشاد السياحي وجودة الخدمات السياحية.", + "يشارك بفاعلية في أنشطة جماعية لفهم مكونات العملية الإرشادية." + +وصف الموضوع: +يُعدّ موضوع "مفاهيم الإرشاد السياحي ومكوناته" من الركائز الأساسية في إعداد الكوادر العاملة في القطاع السياحي، +حيث يهدف إلى تزويد المتدربين بفهم شامل لمفهوم الإرشاد السياحي، وأدوار المرشد، ومسؤولياته، وتأثيره المباشر في +تعزيز تجربة السائح وجودة الخدمات السياحية. +يتناول هذا الموضوع الجوانب النظرية والعملية التي تُمكّن المتدرب من إدراك أهمية المرشد بوصفه عنصرًا بشريًا +محوريًا في صناعة السياحة، فهو لا يُقدّم المعلومة فحسب، بل يصنع تجربة ثقافية وإنسانية متكاملة، تُسهم في بناء +الصورة الذهنية الإيجابية عن الوجهة السياحية، وتدعم التنمية السياحية المستدامة. +يُركّز المحتوى على ثلاثة محاور رئيسية: +1- تعريف الإرشاد السياحي وأهميته: التعرف على المفهوم، الخصائص، والمقومات الأساسية لمهنة الإرشاد، وأثرها + في نجاح المنظومة السياحية. +2- أدوار ومسؤوليات المرشد السياحي: فهم الأبعاد الثقافية، الاقتصادية، الاجتماعية، والبيئية لدور المرشد، + والالتزامات المهنية المطلوبة. +3- أهمية الإرشاد السياحي في تعزيز تجربة السياح: تحليل كيفية مساهمة المرشد في إثراء تجربة الزائر، وتحفيز + الولاء السياحي، وبناء الصورة الإيجابية عن الوجهة. + +نواتج التعلم: + "تعريف مفهوم الإرشاد السياحي وشرح عناصره ومكوناته الأساسية.", + "تمييز أدوار ومسؤوليات المرشد السياحي في السياقات الثقافية والاقتصادية والاجتماعية.", + "تحليل أهمية الإرشاد السياحي في تحسين تجربة السائح وبناء الصورة الذهنية للوجهة.", + "تطبيق مبادئ السلوك المهني والأخلاقي في ممارسات الإرشاد السياحي الواقعية.", + "تقييم تأثير الأداء الإرشادي الجيد على رضا الزوار واستدامة النشاط السياحي.", + "تصميم تجربة إرشادية متكاملة تراعي التفاعل الثقافي، التواصل الفعّال، والجوانب التنظيمية." + + +------------------------------------------------------------ +الوحدة الثانية: التواصل الفعّال مع السياح + +المواضيع: + • مهارات الاتصال اللفظي وغير اللفظي. + • بناء علاقات إيجابية مع المجموعات السياحية. + • إدارة المواقف الصعبة أثناء الجولات. + +أهداف الوحدة: + "يُعرَّف المشارك مفهوم التواصل الفعّال وأهميته في السياحة.", + "يميّز بين مهارات الاتصال اللفظي وغير اللفظي.", + "يطبق أساليب بناء علاقات إيجابية مع المجموعات السياحية.", + "يُدير المواقف الصعبة أثناء الجولات السياحية بلباقة ومهنية.", + "يُحلّل مواقف الاتصال الواقعية ويقترح سلوكيات احترافية بديلة." + +وصف الموضوع: +يُعدّ موضوع "التواصل الفعال مع السياح" أحد أهم مرتكزات التدريب في مجال الإرشاد السياحي، إذ يُسهم في تعزيز +جودة الخدمات وتحسين تجربة الزوار. +يركّز هذا الموضوع على كيفية بناء قنوات تواصل إنسانية وثقافية ناجحة بين المرشد والسائح، تقوم على الاحترام، +الوضوح، والقدرة على فهم احتياجات الزائر وتوقعها. +من خلال هذه الوحدة، يتعرّف المتدرب على أساليب التواصل الفعال، وأثر اللغة، ونبرة الصوت، وتعبيرات الجسد، +والمظهر العام في نقل الانطباع الإيجابي. كما يكتسب مهارات احتواء المواقف الصعبة والتعامل مع ضغوط العمل +السياحي بهدوء وثقة. + +نواتج التعلم: + "توضيح الفروق الجوهرية بين التواصل الفعّال وغير الفعّال في مواقف الإرشاد السياحي.", + "تحليل مواقف تواصل متنوعة لتحديد السلوكيات اللفظية وغير اللفظية.", + "توظيف مهارات الصوت، والنبرة، ولغة الجسد في إيصال الرسائل بوضوح للسائح.", + "تطبيق أساليب بناء الثقة والانطباع الإيجابي مع أفراد المجموعات السياحية.", + "التعامل مع المواقف الصعبة أثناء الجولات مستخدمًا مهارات التهدئة والاستماع الفعّال.", + "تقديم تغذية راجعة بنّاءة لزملائه حول أسلوب التواصل لديهم بطريقة مهنية.", + "وضع خطة شخصية لتطوير مهاراته الاتصالية بما يتناسب مع متطلبات بيئة العمل السياحية." + + +------------------------------------------------------------ +الوحدة الثالثة: التخطيط والإعداد للجولات السياحية + +المواضيع: + • تقنيات تخطيط المسارات السياحية. + • تحديد المواقع المناسبة للجولات السياحية. + • إعداد البرامج السياحية بما يتناسب مع اهتمامات السياح. + +أهداف الوحدة: + "يشرح المتدرب مفهوم تخطيط المسارات السياحية وأهدافها.", + "يحدّد خطوات تخطيط مسار فعّال ومتسق للجولة السياحية.", + "يطبّق معايير اختيار المواقع والأنشطة داخل المسار السياحي.", + "يستخدم الأدوات المساندة في تخطيط الجولات السياحية باحتراف.", + "يقيم جاهزية المواقع السياحية وفق معايير واضحة.", + "يصمّم برنامجًا سياحيًا متكاملًا يناسب اهتمامات فئات مختلفة من الزوار." + +وصف الموضوع: +تُعدّ مهارات التخطيط السياحي حجر الأساس في عمل المرشد السياحي، إذ تضمن انسيابية الجولة وتحقيق رضا السائح +وجودة التجربة. +تركّز هذه الوحدة على منهجية تصميم المسار، ابتداءً من تحليل الجمهور المستهدف، مرورًا بدراسة المواقع وتحديد +توقيتات الزيارة، وصولًا إلى بناء برنامج متوازن يتضمن عناصر ثقافية وترفيهية وتجارب محلية أصيلة. +كما تُغطي الوحدة آليات تقييم جاهزية المواقع، ومعايير اختيار الأنشطة المناسبة، وأهمية التوافق بين نوع السائح +وطبيعة الجولة. +وفي الجانب التطبيقي، يتدرب المتدربون على تصميم مسارات وبرامج حقيقية باستخدام أدوات وممارسات مهنية +واقعية تُستخدم في صناعة السياحة. + +نواتج التعلم: + "تمييز المتدرب بين المسار المخطط جيدًا والمسار العشوائي.", + "تحليل احتياجات الزوار وتحويلها إلى عناصر في برنامج الجولة.", + "استخدام بطاقات تقييم المواقع لتحديد مدى جاهزيتها.", + "تطبيق مهارات تنظيم الوقت وتوزيع الأنشطة داخل برنامج الجولة.", + "تصميم مسار مكوّن من عدة محطات وفق معايير الجاذبية والأمان والانسيابية.", + "بناء برامج تناسب فئات مختلفة من السياح (عائلات، شباب، مغامرين...).", + "تقييم جودة البرنامج عبر مقارنة القرارات التخطيطية بالمعايير المهنية." + + +------------------------------------------------------------ +الوحدة الرابعة: إدارة المواقف الطارئة + +المواضيع: + • تحليل المشكلات الطارئة أثناء الجولات. + • تقنيات التعامل مع الأزمات. + • وضع خطط احتياطية لضمان سلامة السياح. + +أهداف الوحدة: + "يحدّد أنواع المواقف الطارئة المحتملة أثناء الجولات السياحية.", + "يطبّق خطوات التحليل السريع للحوادث (Rapid Incident Analysis).", + "يستخدم نموذج التقييم الثلاثي لتقدير خطورة الموقف واتخاذ القرار الأنسب.", + "يمارس أساليب التواصل الفعال أثناء الأزمة (تهدئة - توجيه - توثيق).", + "يطبّق استراتيجيات إدارة الأزمات في الميدان (العزل - الإخلاء - التنسيق).", + "يصمّم خطة احتياطية (Plan B) لضمان سلامة واستمرارية الجولة." + +وصف الموضوع: +تتناول هذه الوحدة المهارات الأساسية للتعامل مع الأزمات الطارئة أثناء الجولات السياحية، وهي مواقف لا يمكن +تجنبها حتى مع التخطيط الجيد. +تركّز الوحدة على تحليل طبيعة الطوارئ وتقدير خطورتها، وإدارة اللحظات الأولى حيث يُعد القرار السريع عاملًا +حاسمًا في السيطرة على الموقف. +تتضمن الوحدة تدريبات عملية على التعامل مع مواقف حقيقية، تشخيص الأخطاء الشائعة، وممارسة التفكير السريع، +إضافة إلى تجهيز أدوات الطوارئ والتدرب على مهارات التواصل تحت الضغط. +كما يتعلم المتدربون آليات بناء خطة احتياطية (Plan B) تضمن استمرار الجولة رغم حدوث خلل، مثل تغيّر الطقس، +تعطل وسائل النقل، أو إغلاق المواقع. + +نواتج التعلم: + "التمييز بين الأزمة البسيطة والأزمة الخطرة وفق عوامل الخطر والزمن والموارد.", + "تحليل موقف طارئ باستخدام خطوات التقييم السريع.", + "اتخاذ قرار فوري يحافظ على سلامة أفراد المجموعة.", + "إدارة التواصل بفاعلية داخل المجموعة وخارجها.", + "استخدام أدوات الطوارئ بشكل صحيح حسب الحاجة.", + "تنفيذ إخلاء آمن ومنظم للموقع عند الضرورة.", + "تصميم خطة احتياطية تشمل بدائل للمواقع والأنشطة ووسائل النقل." +""" + +example_reference3 = """ +الوحدة الأولى: مفاهيم الإرشاد السياحي ومكوناته + +المواضيع: + • تعريف الإرشاد السياحي وأهميته. + • أدوار ومسؤوليات المرشد السياحي. + • أهمية الإرشاد السياحي في تعزيز تجربة السياح. + +تمهيد: +يشهد العالم اليوم تطورًا غير مسبوق في قطاع السياحة، حيث لم تعد السياحة مجرّد انتقالٍ بين الأماكن، بل أصبحت +رحلة معرفية وثقافية وإنسانية تهدف إلى اكتشاف الآخر والتفاعل مع التاريخ والحضارة والهوية. وفي قلب هذه المنظومة +يقف المرشد السياحي باعتباره العنصر البشري الأهم في صناعة التجربة السياحية، فهو الواجهة التي يرى من خلالها +الزائر الدولة، والمفتاح الذي يفتح أمامه أبواب الفهم والدهشة والانبهار. +إنّ الإرشاد السياحي ليس عملًا آليًا يقتصر على تقديم المعلومات أو سرد الحقائق، بل هو عملية اتصال متكاملة تتضمن +مهارات التفاعل، والتفسير الثقافي، ونقل المعرفة بطريقة مؤثرة وممتعة، مما يجعله أداةً استراتيجية لتعزيز جودة +الخدمات السياحية ورفع مستوى رضا الزوار. ومن هنا تتضح أهمية دراسة مفاهيم الإرشاد السياحي ومكوناته كمدخل +رئيسي لتأهيل المرشدين وإعدادهم مهنيًا وثقافیًا وسلوکیًا. +يتناول هذا الموضوع تعريف الإرشاد السياحي بوصفه مزيجًا من المعرفة والسلوك والخبرة الإنسانية التي تمكّن المرشد من +تقديم تجربة متكاملة للزائر. كما يسلّط الضوء على مكونات العملية الإرشادية من تخطيطٍ للجولة، وتفسيرٍ للمعالم، +وإدارةٍ للمواقف، وتواصلٍ فعّال مع الزوار من خلفياتٍ متعددة. فكل عنصر من هذه المكونات يشكّل لبنة أساسية في بناء +تجربةٍ سياحية ناجحة تنعكس إيجابًا على سمعة الوجهة وعلى التنمية السياحية المستدامة. +ويُظهر المحتوى أيضًا كيف أن المرشد السياحي هو حلقة الوصل بين السائح والمكان، فهو يترجم التاريخ إلى قصة، والثقافة +إلى تجربة، والمعلومة إلى مشاعر وانطباعات تُحفّظ في الذاكرة. ولذلك تتطلّب المهنة وعيًا ثقافيًا، ولباقة في التواصل، +وقدرة على الإقناع والتأثير، والتزامًا بالأخلاقيات المهنية التي تنظّم العلاقة بين المرشد والسائح والمجتمع المحلي. +كما أنّ دراسة مفاهيم الإرشاد ومكوناته تمكّن المتدربين من فهم الأدوار المتعددة للمرشد السياحي (التثقيفية، التنظيمية، +الترفيهية، الترويجية، والأمنية) وكيفية التكامل بينها لضمان تجربة زائر متوازنة وشاملة. فكل مرشد ناجح يُسهم في +تحقيق أحد أهم أهداف السياحة الحديثة وهو تحويل الزيارة إلى تجربة، والتجربة إلى انتماء، والانتماء إلى ولاء. +وفي السياق العربي والسعودي، تتجلّى أهمية هذا الموضوع في ظل التحوّل السياحي الكبير الذي تشهده المملكة ضمن رؤية +2030، حيث أصبح المرشد السياحي أحد أعمدة النجاح في المشروعات السياحية الوطنية، مثل مشاريع العلا والدرعية +وجدة التاريخية. ومن ثمّ فإنّ فهم المفاهيم والمكونات الأساسية للإرشاد يُعدّ خطوةً جوهرية نحو تأهیل مرشدین محترفین +قادرین علی تمثیل ثقافتہم ومجتمعہم بوعي واقتدار. + +------------------------------------------------------------ +الوحدة الثانية: التواصل الفعّال مع السياح + +المواضيع: + • مهارات الاتصال اللفظي وغير اللفظي. + • بناء علاقات إيجابية مع المجموعات السياحية. + • إدارة المواقف الصعبة أثناء الجولات. + +تمهيد: +يُعدّ التواصل الإنساني حجر الأساس في بناء العلاقات بين الأفراد والمجتمعات، وفي السياحة تحديدًا يمثل التواصل +الفعال البوابة الأولى التي يعبر منها السائح نحو تجربة ثرية وآمنة وممتعة. +فالمرشد السياحي ليس ناقلا للمعلومات فحسب، بل هو صانع تجربة، وناقل ثقافة، وسفير لوطنه أمام العالم. ومن خلال +كلماته، ونبرته، وسلوكه، ولغته الجسدية، تتشكّل الصورة الذهنية عن الوجهة السياحية وجودة خدماتها ومستوى أهلها +في الضيافة والاحترافية. +إن العلاقة بين المرشد والسائح تتجاوز حدود المعلومة لتصل إلى التأثير النفسي والعاطفي في الزائر. فالسائح يستجيب +أولا لأسلوب المرشد وطريقته في التفاعل قبل أن يلتفت إلى مضمون ما يُقال. ومن هنا تأتي أهمية امتلاك المرشد لمهارات +تواصل متقدمة، تجمع بين الذكاء الاجتماعي، والوعي الثقافي، والقدرة على قراءة المواقف والتصرف بمرونة واتزان. +تظهر الإحصاءات الحديثة في قطاع السياحة أن أكثر من 70% من تقييمات الزوار الإيجابية ترتبط بجودة التواصل مع +مقدّمي الخدمة وليس فقط بالخدمات ذاتها (2023 ,UNWTO). فالكلمة اللطيفة، والابتسامة الصادقة، والقدرة على +الإنصات الفعال قد تُحوّل موققًا بسيطًا إلى ذكرى لا تُنسى، بينما ضعف التواصل قد یبدّد جهدًا تنظيميًا كاملاً في لحظة +واحدة من سوء الفهم أو الرد غير المناسب. +ومن هذا المنطلق، تأتي هذه الوحدة التدريبية لتزويد المتدربين بالأدوات العملية والمعرفية التي تمكّنهم من بناء جسور +التواصل الإنساني مع السياح من مختلف الثقافات والخلفيات، وفهم أثر الكلمة والإشارة والسلوك في تعزيز جودة +التجربة السياحية. +كما تسعى الوحدة إلى تدريب المتدربين على التعامل مع المواقف الصعبة وضغوط العمل بأسلوب احترافي، يقوم على +الهدوء، والتحليل، والقدرة على حل المشكلات في بيئات متعددة اللغات والثقافات. +يستند محتوى هذه الوحدة إلى معايير منظمة السياحة العالمية (UNWTO) وأفضل الممارسات الدولية في مجال الاتصال +السياحي، ويركّز على الجوانب العملية من خلال التمارين التفاعلية، ولعب الأدوار، وتحليل الحالات الواقعية من بيئة +العمل السعودية والعالمية. +وهي خطوة أساسية نحو إعداد مرشدين قادرين على أن يكونوا سفراء للثقافة السعودية، ونماذج مهنية راقية في +التعامل مع الزوار من مختلف أنحاء العالم. + +------------------------------------------------------------ +الوحدة الثالثة: التخطيط والإعداد للجولات السياحية + +المواضيع: + • تقنيات تخطيط المسارات السياحية. + • تحديد المواقع المناسبة للجولات السياحية. + • إعداد البرامج السياحية بما يتناسب مع اهتمامات السياح. + +تمهيد: +يُعدّ التخطيط الاحترافي للجولات السياحية العمود الفقري لنجاح تجربة السائح، وهو العامل الأول الذي يحدد جودة +الرحلة ومستوى رضا الزوّار قبل حتى أن يبدأوا الجولة فعليًا. فالمرشد السياحي المحترف لا يكتفي بامتلاك معلومات عن +المواقع، بل يمتلك القدرة على نسج هذه المواقع في قصة منسّقة، ومسار منسجم، وتجربة مدروسة تراعي اهتمامات الزائر +وراحته وسلامته. +إن تصميم مسار جولة ناجحة ليس مهمة عشوائية، بل هو عملية تحليل مدروسة تتضمن فهم الجمهور المستهدف، +وتقييم جاهزية المواقع، وتحديد التوقيتات المناسبة، وإضافة لحظات وقيمة خاصة تُحوّل الجولة من مجرد زيارة ... إلى +تجربة إنسانية وثقافية غنية. +فالمسار الجيد هو الذي يوازن بين المعلومات، والأنشطة، والإيقاع، وراحة السائح، ويُظهر الوجهة السياحية بأفضل صورة +ممکنۀ. + +تشير التقارير الحديثة في قطاع السياحة العالمي (2024,UNWTO) إلى أن %87من الزوّاريقيّمون رحلتهم بناءً على جودة +التنظيم والتسلسل المنطقي للجولة، وليس فقط على جمال المواقع. بل إن 6 من كل 10 سياح يفضلون الجولات التي +تراعي اهتماماتهم الفردية وتقدم لهم برنامجًا مرنًا يشعِرهم بأن التجربة صُممت خصيصًا لهم. +ومن هذا المنطلق، تأتي هذه الوحدة التدريبية لتزويد المتدربين بالمهارات العملية والمعايير المهنية لتخطيط المسارات، +واختيار المواقع، وتصميم البرامج السياحية الملائمة لمختلف الفئات. كما تركّز على كيفية استخدام الأدوات الحديثة في +التخطيط، وتحليل الخيارات، وتوقع التحديات، ووضع بدائل ذكية تضمن انسيابية الجولة مهما كانت الظروف. +تعتمد هذه الوحدة على مزيج من التطبيقات العملية، والدروس المستفادة من التجارب العالمية والسعودية، والتمارين +التفاعلية التي تحاكي واقع عمل المرشد السياحي، بهدف إعداد مرشدين قادرين على تقديم جولات منظمة، آمنة، ممتعة، +وذات طابع مميز يليق بالضيافة السعودية ومعايير الجودة الدولية. + +------------------------------------------------------------ +الوحدة الرابعة: إدارة المواقف الطارئة + +المواضيع: + • تحليل المشكلات الطارئة أثناء الجولات. + • تقنيات التعامل مع الأزمات. + • وضع خطط احتياطية لضمان سلامة السياح. + +تمهيد: +تُمثَل القدرة على إدارة المواقف الطارئة أحد أهم أعمدة الاحترافية في عمل المرشد السياحي، فهي المهارة التي لا تظهر إلا +عند اللحظة الحرجة، ولا تُختبر إلا حين تواجه المجموعة موقفًا مفاجئا قد يُعطَّل الجولة أو یعرّض سلامة الزوّار للخطر. +وعلى الرغم من أن التخطيط الجيد يخفف الكثير من التحديات، فإن الواقع يؤكد أن الطوارئ جزء طبيعي من الجولات +السياحية، وأن المرشد الذي يتقن التعامل معها هو الأقدر على حماية الزوّار والمحافظة على جودة التجربة. +إن إدارة الطوارئ ليست مجرد رد فعل سريع، بل هي عملية مهنية متكاملة تشمل التقييم الهادئ للحادث، واتخاذ القرار +خلال ثوانٍ محدودة، وتنظيم أفراد المجموعة، والتنسيق مع الجهات المختصة، والقدرة على التواصل بوضوح رغم +الضغط والارتباك. وتشير تقارير (2024 ,UNWTO) إلى أن 92% من الزوّار يشعرون بثقة أكبر في الجولات التي يقودها +مرشد قادر على التعامل مع المواقف غير المتوقعة، وأن مستوى رضا السائح يرتفع بنسبة 65% عندما يشعر بأن المرشد +يملك خطة واضحة للطوارئ وخيارات بديلة جاهزة. +وتتجاوز أهمية هذه المهارات الجانب الأمني إلى بناء الانطباع العام عن الوجهة السياحية نفسها، إذ إن الطريقة التي تُدار +بها الأزمة تعكس جودة القطاع، ومستوى الاستعداد، ودرجة الاحتراف التي يتوقعها السائح من الخدمات السياحية، +خاصة في الوجهات التي تشهد كثافة زوار أو طبيعة جغرافية متنوعة مثل السعودية. +ومن هذا المنطلق، تأتي هذه الوحدة التدريبية لتزويد المتدربين بالمعرفة العملية والأدوات المهنية للتحليل السريع +للمشكلات، وإدارة الأزمات، ووضع خطط احتياطية مرنة تضمن استمرار الجولة بأمان مهما كانت الظروف. +كما تعتمد الوحدة على تمارين محاكاة، وسيناريوهات واقعية، وتطبيقات تفاعلية تهدف إلى بناء قدرة حقيقية لدى +المتدريين على اتخاذ القرار تحت الضغط، وقيادة المجموعة بثقة، والحفاظ على هدوء الزوار حتى في أصعب اللحظات. +تركّز هذه الوحدة على أحدث المنهجيات العالمية في إدارة الطوارئ السياحية، مستفيدة من التجارب المحلية والدولية، +وبما يتوافق مع معايير الجودة والسلامة المعتمدة في المملكة العربية السعودية، وبهدف إعداد مرشدين قادرين ليس فقط +على تقديم جولات ممتعة ... بل جولات آمنة، احترافیة، ومستعدة لکل الاحتمالات. +""" + +example_reference4 = """ +الوحدة الأولى: مفاهيم الإرشاد السياحي ومكوناته + +عزيزي المدرب +تهدف هذه الوحدة إلى تعريف المتدربين بمفهوم الإرشاد السياحي ومكوناته الأساسية، وإبراز دوره المحوري في دعم +صناعة السياحة وتحسین تجربة الزوار. +اعتمد في تنفيذها على العمل الجماعي والتعاون بين المتدربين من خلال أنشطة تفاعلية تشجع على تبادل الخبرات والآراء +حول مفهوم الإرشاد، وأدوار المرشد السياحي، وأخلاقيات المهنة. +تذكّر أن دورك هو تيسير عملية التعلم لا تلقينها؛ حفّز المتدربين على النقاش، ووجّههم نحو التفکير في تجارب واقعية +تعزز فهمهم العملي لمفهوم الإرشاد السياحي. + +المواضيع: + • تعريف الإرشاد السياحي وأهميته. + • أدوار ومسؤوليات المرشد السياحي. + • أهمية الإرشاد السياحي في تعزيز تجربة السياح. + +تقييم قبلي للوحدة : +-ما الذي يجعل التجربة السياحية ناجحة في رأيك؟ المرشد أم المكان أم التنظيم؟ ولماذا؟ +-هل تعتقد أن المرشد السياحي ناقل للمعلومة فقط أم صانع للتجربة؟ وضّح رأيك. +-تخيّل أنك سائح في بلدٍ جديد، ما الانطباع الأول الذي سيتركه فيك المرشد؟ +-كيف يمكن للمرشد أن يوازن بين تلبية احتياجات الزوار والحفاظ على القيم المحلية؟ +-كيف يمكن للإرشاد السياحي أن يُسهم في تعزيز مكانة المملكة كوجهة عالمية؟ +-في رأيك، ما أبرز مكونات الإرشاد السياحي التي لا يمكن أن ينجح بلونها؟ + +النشاط الافتتاحي: +تهيئة المشاركين للموضوع وربطهم بدور المرشد السياحي السعودي. + +50 دقيقة + +محاكاة واقعية(Role Play) + +صور لمعالم سعودية (العلا، الدرعية، جدة التاريخية، نيوم، أبها، الطائف ... ) + لاصقات تقييم + مؤقت. + +الہدف + +الزمن + +الأسلوب + +الأدوات + +خطوات التنفيذ: +1. يرحب المدرب بالمشاركين ويعرض صورًا لمعالم سياحية من مختلف مناطق المملكة. +2. يطلب من كل متدرب اختيار صورة تمثّل موقعًا یعرفه أو زاره. +3. يمنح كل متدرب دقيقتين لتقديم هذا الموقع وكأنه مرشد سياحي سعودي. +4. بقية المتدربين يقيّمون العرض من حيث (المعلومة - الأسلوب - التواصل). +5. يدوّن المدرب على السبورة السمات التي لاحظها في العروض (حب الوطن، التفاعل، الترتيب، الثقة، نقل الثقافة ... ). +6. يربط المدرب بين هذه السمات ومفهوم "المرشد السعودي كممثل للهوية الوطنية." + +السؤال النظري بعد النشاط : +"في رأيك، كيف يمكن للمرشد السياحي السعودي أن يعكس صورة المملكة كوجهة عالمية في ظل رؤية 2030؟" + + +------------------------------------------------------------ +الوحدة الثانية: التواصل الفعّال مع السياح + +عزيزي المدرب +تهدف هذه الوحدة إلى تمكين المتدربين من اكتساب مهارات التواصل الفعّال التي تُعدّ جوهر النجاح في مهنة الإرشاد +السياحي. +تركّز الوحدة على تطوير قدرة المتدربين على استخدام التواصل اللفظي وغير اللفظي بفعالية، وبناء علاقات إيجابية مع +السياح من مختلف الثقافات، والتعامل مع المواقف الصعبة باحترافية ولباقة. +اعتمد في تنفيذها على أسلوب العرض والنقاش الموجّه الذي يتيح للمتدربين تحليل مواقف واقعية من الميدان السياحي، +وتبادل وجهات النظر حول السلوكيات الاتصالية الإيجابية والسلبية. +شجّعهم على محاكاة مواقف عملية وتمثيل أدوار المرشد والسائح لتجسيد المفاهيم النظرية وتحويلها إلى سلوك تطبيقي. +تذكّر أن دورك هو تيسير الحوار وتوجيهه نحو استنتاج المفاهيم من خلال التجربة والملاحظة، لا عبر التلقين المباشر. + +المواضيع: + • مهارات الاتصال اللفظي وغير اللفظي. + • بناء علاقات إيجابية مع المجموعات السياحية. + • إدارة المواقف الصعبة أثناء الجولات. + +تقييم قبلي للوحدة : +-ما هي المشكلة التي تجعلك تشعر بالتوتر فورًا في الجولة؟ وكيف تتعامل معها؟ +-هل يكفي أن تكون مرشدًا خبيراً بالمعلومات لتكسب ولاء السائح؟ أم هناك شيء آخر؟ +-إذا كنت لا تعرف إجابة سؤال، فما هو أصدق وأفضل رد يجب أن تقوله؟ +-هل يجب أن تعطي السائح حلًا واحداً، أم عدة خيرات، عندوجود شكوى؟ +-ما هي أصغر تفصيلة إنسانية يمكن أن تتك انطباعاً لا يُنسى لدى السائح؟ +-ما هو الخطأ الأكبر الذي نرتكبه عندما نرد على الغضب بالغضب؟ + + +------------------------------------------------------------ +الوحدة الثالثة: التخطيط والإعداد للجولات السياحية + +عزيزي المدرب: +تهدف هذه الوحدة إلى تمكين المتدربين من اكتساب مهارات التخطيط السياحي الفعّال، وهي من أهم المهارات التي +يحتاجها المرشد لضمان نجاح الجولة السياحية وتقديم تجربة متكاملة ترضي توقعات الزوار. +تتناول الوحدة أسس تصميم المسارات، اختيار المواقع المناسبة، وإعداد البرامج السياحية التي تلائم اهتمامات وثقافات +وأنماط السياح المختلفة. +اعتمد في تنفيذها على أسلوب التحليل والممارسة العملية، حيث يقوم المتدربون بتصمیم مسارات حقیقیة، وتحليل +مواقع، وإعداد برامج متكاملة. شجّعهم على حل المشكلات، وتقييم البدائل، واستخدام الأدوات الرقمية في التخطيط، +وذلك لتحويل المعرفة النظرية إلى مهارة عملية قابلة للتطبيق في الميدان. +تذكّر أن دورك هو تيسير التفكير، وإدارة النقاش، وتوجيه المتدربين للاستنتاج بأنفسهم، ومساعدتهم على ربط +التخطيط بالجودة، والأمان، وتجربة السائح. + +المواضيع: + • تقنيات تخطيط المسارات السياحية. + • تحديد المواقع المناسبة للجولات السياحية. + • إعداد البرامج السياحية بما يتناسب مع اهتمامات السياح. + +تقييم قبلي للوحدة : +-ما أول خطوة تقوم بها عندما يُطلب منك تصميم مسار لجولة سياحية جديدة؟ ولماذا تعتبر هذه +الخطوة مهمة؟ +-هل تعتقد أن كثرة المواقع داخل الجولة تجعل التجربة أفضل دائمًا؟ أم أن هناك عوامل أخرى تحدد +جودة الرنامج؟ وضّح رأيك. +-ما هو أكبر خطأ شاهدته أو تتوقع حدوثه عند إعداد جولة غير مخططة جيدًا؟ وكيف يمكن تجنّبه؟ +-كيف يمكن للمرشد أن يختار الأنشطة والمواقع المناسبة إذا كانت المجموعة السياحية متنوعة في +اهتماماتها؟ +-اذكر مثالًا على جولة سياحية حضرتها (أو تتخيلها) كانت ممتازة في تنظيم المسار ... ما العنصر الذي +جعلها ناجحة؟ +-في رأيك، ما الذي يميز برنامجًا سياحيًا «مصمم خصيصًا» عن برنامج عادي؟ وما أهم عناصر +التخصیص؟ +-إذا طُلب منك تصميم برنامج لعائلة، وآخر لشباب مغامرين ... ما أبرز اختلافين يجب أن تراعيهما في كل +برنامج؟ + + +------------------------------------------------------------ +الوحدة الرابعة: إدارة المواقف الطارئة + +عزيزي المدرب +تهدف هذه الوحدة إلى تمكين المتدربين من اكتساب مهارات التعامل الاحترافي مع المواقف الطارئة التي قد تواجه المرشد +السياحي أثناء الجولات. +تُعدّ إدارة الطوارئ عنصرًا جوهريًا في عمل المرشد، فهي لیست مجرد استجابة لحادث، بل هي مبارة قيادیة تُظهر قدرته +على حماية المجموعة، والحفاظ على الهدوء، واتخاذ القرار الصحيح في الوقت المناسب. +تتناول الوحدة تحليلاً لأنواع الأزمات المحتملة، خطوات التقييم السريع للحوادث، أساليب الاستجابة الأولية، وتقنيات +الحد من المخاطر، إضافةً إلى إعداد الخطط الاحتياطية لضمان استمرار الجولة بأمان وبأقل خسائر ممكنة. +اعتمد في تنفيذ الوحدة على أسلوب المحاكاة العملية والألعاب التدريبية-فكلما اقتریت الأنشطة من الواقع، زادت +قدرة المتدرب على استيعاب التصرف الصحيح في اللحظة الحرجة. +شجّع المتدربين على التفكير تحت الضغط، وعلى مناقشة الحلول بصوتٍ عالٍ، وعلى اختبار قراراتهم باستخدام +السيناريوهات المتدرجة من بسيطة إلى معقدة. +تذكّر أن دورك ليس إعطاء الحلول مباشرة، بل مساعدتهم على التحليل، التوقع، واتخاذ قرار سریع مبني علی المنطق +والهدوء.ساعدهم على الربط بين إدارة الطوارئ وبين معايير الجودة، والأمان، وثقة السائح في المرشد. + +المواضيع: + • تحليل المشكلات الطارئة أثناء الجولات. + • تقنيات التعامل مع الأزمات. + • وضع خطط احتياطية لضمان سلامة السياح. + +تقييم قبلي للوحدة : +-ما أول تصرف تقوم به عندما تواجه موقفًا طارئًا أثناء جولة سياحية؟ ولماذا تعتقد أن هذا التصرف هو +الأكثر أهمية؟ +-من وجهة نظرك، ما أخطر نوع من الطوارئ قد يواجهه المرشد خلال الجولة؟ وما سبب خطورته؟ +-هل تعتقد أن سرعة الاستجابة أهم من دقتها أثناء الطوارئ؟ أم أن الدقة أولى؟ وضّح رأيك مع مثال. +-برأيك، ما العلامة التي تدل على أن المرشد يمتلك مهارة "إدارة الأزمة" حتى قبل وقوع أي طارئ؟ +-تخيّل أن أحد الزوار اختفى عن المجموعة لبضع دقائق ... ما الخطأ الأكبر الذي يمكن أن يقع فيه المرشد +في هذه الحالة؟ +-ما الفرق بين «المشكلة البسيطة» و«الأزمة» في سياق الجولات السياحية؟ وكيف يمكن للمرشد التمييز +بـينہما؟ +-هل تعتقد أن وجود خطة احتياطية (Plan B) ضروري في كل جولة؟ اذكر مثالًا لحالة يمكن أن تتحول +إلى أزمة إذا لم تتوفر خطة بديلة. +""" + +intro_reference1 = { + "contentandstyle": { + "chapter_title": "الفصل الأول: التنمر", + "outcome": "أن يكون المتدرب قادرًا على فهم وتعريف مفهوم التنمر وأنماطه المختلفة وأركان مثلث التنمر.", + "sections": [ + { + "title": "ماذا يعني أن تكون متنمرًا أو ضحية للتنمر؟", + "overview": "تعريف التنمر ومدى خطورته في مواجهة الأفراد والمجتمعات.", + "subsections": [ + { + "title": "تعريف التنمر وأصله اللغوي", + "purpose": "توضيح معنى التنمر وأصله اللغوي وأسباب انتشاره.", + "suggested_queries": [ + "ما هو التنمر؟", + "ما العلاقة بين التنمر والنمر؟", + "متى يصبح الإنسان متنمرًا؟" + ], + "difficulty": "basic", + "est_pages": 2, + "planning": { + "status": "completed", + "estimated_pages": 2, + "writing_mode": "educational", + "tone": "educational-formal", + "core_idea": "تعريف ماهية التنمر وأصله اللغوي والربط بين السلوك الإنساني وطباع النمر في العدوانية والهيمنة.", + "key_points": [ + "تقديم تعريف واضح ومبسّط للتنمر كسلوك عدواني متكرر يهدف لفرض الهيمنة.", + "شرح الأصل اللغوي للتنمر وعلاقته بالنمر وطباعه الشرسة.", + "توضيح أن التنمر يشمل الأذى الجسدي والنفسي والاجتماعي.", + "تمييز التنمر عن السلوكيات العادية من خلال التكرار والقصد.", + "توضيح الأثر النفسي العميق للتنمر على الضحايا." + ], + "scope_limit": "لا يشمل شرح استراتيجيات مواجهة التنمر أو الحلول التفصيلية.", + "avoid_overlap_with": [ + "أنماط وأشكال التنمر", + "مثلث التنمر" + ], + "depth_level": "conceptual", + "citations_required": True, + "notes": "يُراعى الاستناد إلى مصادر معرفية حديثة مع الربط بين المعنى اللغوي والسلوكي." + } + }, + { + "title": "أنماط وأشكال التنمر", + "purpose": "تصنيف أنواع التنمر المختلفة وشرح خصائص كل نوع.", + "suggested_queries": [ + "ما هي أنواع التنمر؟", + "ما الفرق بين التنمر الجسدي واللفظي؟", + "ما هو التنمر الإلكتروني؟" + ], + "difficulty": "intermediate", + "est_pages": 4, + "planning": { + "status": "completed", + "estimated_pages": 4, + "writing_mode": "educational", + "tone": "educational-formal", + "core_idea": "تفصيل الأنواع المختلفة للتنمر من الجسدي إلى الإلكتروني مع شرح خصائص وأساليب كل نوع.", + "key_points": [ + "شرح التنمر الجسدي كأكثر الأشكال وضوحًا ومباشرة.", + "توضيح التنمر اللفظي كسلاح من الكلمات يهدم التقدير الذاتي.", + "تناول التنمر الاجتماعي الخفي وأساليب الإقصاء والعزل.", + "شرح التنمر الجنسي كأخطر أشكال التنمر وانتهاك للكرامة.", + "توضيح التنمر العاطفي/النفسي وتأثيره العميق.", + "تناول التنمر العنصري المبني على الكراهية والتمييز.", + "شرح مفصل للتنمر الإلكتروني وأساليبه المتعددة من المكالمات إلى الألعاب الإلكترونية." + ], + "scope_limit": "لا يشمل طرق الوقاية أو العلاج من التنمر بشكل تفصيلي.", + "avoid_overlap_with": [ + "تعريف التنمر", + "كيف تحمي نفسك من التنمر الإلكتروني" + ], + "depth_level": "detailed", + "citations_required": True, + "notes": "يجب التركيز على التنوع في الأمثلة وتوضيح خطورة كل نوع." + } + }, + { + "title": "كيف تحمي نفسك من التنمر الإلكتروني؟", + "purpose": "تقديم خطوات عملية وإرشادات للوقاية من التنمر الإلكتروني.", + "suggested_queries": [ + "كيف أحمي نفسي من التنمر الإلكتروني؟", + "ما هي خطوات الأمان الرقمي؟", + "كيف أتعامل مع المتنمرين على الإنترنت؟" + ], + "difficulty": "practical", + "est_pages": 2, + "planning": { + "status": "completed", + "estimated_pages": 2, + "writing_mode": "instructional", + "tone": "educational-formal", + "core_idea": "تقديم إرشادات عملية ومباشرة لحماية النفس من التنمر الإلكتروني.", + "key_points": [ + "تعزيز الحماية الرقمية من خلال كلمات المرور وميزات الأمان.", + "أهمية اختيار الأصدقاء والعلاقات الصحية على الإنترنت.", + "التفكير قبل النشر والحفاظ على الخصوصية.", + "تجنب المواقع والروابط المشبوهة.", + "حظر المتنمرين فورًا وعدم الرد عليهم.", + "أهمية طلب الدعم العائلي والمساعدة عند التعرض للتنمر." + ], + "scope_limit": "يركز على الوقاية وليس على العلاج النفسي أو القانوني.", + "avoid_overlap_with": [ + "أنماط التنمر الإلكتروني" + ], + "depth_level": "practical", + "citations_required": False, + "notes": "يُفضل استخدام أسلوب مباشر وواضح مع نقاط عملية قابلة للتطبيق." + } + }, + { + "title": "متى أكون فريسة للمتنمرين؟", + "purpose": "توضيح الخصائص والسمات التي تجعل الشخص أكثر عرضة للتنمر.", + "suggested_queries": [ + "ما الذي يجعلني عرضة للتنمر؟", + "ما هي صفات ضحايا التنمر؟", + "كيف يختار المتنمر ضحيته؟" + ], + "difficulty": "intermediate", + "est_pages": 2, + "planning": { + "status": "completed", + "estimated_pages": 2, + "writing_mode": "educational", + "tone": "educational-formal", + "core_idea": "تحديد السمات النفسية والاجتماعية التي تجعل الفرد هدفًا سهلًا للمتنمرين.", + "key_points": [ + "الضعف عند المواجهة وعدم القدرة على الرد.", + "إظهار الخوف بسهولة كعلامة يقرأها المتنمرون.", + "الصمت وعدم طلب المساعدة كدليل على الضعف.", + "افتقاد الدعم الاجتماعي وشبكة الأصدقاء.", + "ضعف الثقة بالنفس والنظرة السلبية للذات.", + "الانسحاب الاجتماعي والعزلة.", + "الحساسية المفرطة تجاه النقد.", + "الخوف من المستقبل وانعدام الأمان." + ], + "scope_limit": "لا يشمل طرق تعزيز الثقة بالنفس أو بناء المهارات الاجتماعية.", + "avoid_overlap_with": [ + "مثلث التنمر - الضحية" + ], + "depth_level": "analytical", + "citations_required": False, + "notes": "يجب التركيز على الجانب التوعوي دون إلقاء اللوم على الضحية." + } + }, + { + "title": "ماذا تعرف عن مثلث التنمر؟", + "purpose": "شرح الأطراف الثلاثة المكونة لمنظومة التنمر وأدوارها.", + "suggested_queries": [ + "ما هو مثلث التنمر؟", + "من هم أطراف التنمر؟", + "ما دور المتفرج في التنمر؟" + ], + "difficulty": "intermediate", + "est_pages": 3, + "planning": { + "status": "completed", + "estimated_pages": 3, + "writing_mode": "educational", + "tone": "educational-formal", + "core_idea": "توضيح أن التنمر منظومة ثلاثية تتكون من المتنمر والضحية والمتفرج، ولكل دور محدد.", + "key_points": [ + "شرح مفهوم مثلث التنمر كمنظومة متكاملة.", + "توضيح دور المتنمر وأنواع التنمر (فردي، جماعي متجانس وغير متجانس).", + "تحليل شخصية الضحية (السلبي والاستفزازي) وخصائصها.", + "شرح دور المتفرجين (السلبيون والمشاركون) وتأثير صمتهم.", + "توضيح أن كل طرف يلعب دورًا في استمرار التنمر أو إيقافه.", + "التأكيد على أهمية التدخل الإيجابي من المتفرجين." + ], + "scope_limit": "لا يشمل استراتيجيات تدخل تفصيلية أو برامج وقائية.", + "avoid_overlap_with": [ + "تعريف التنمر", + "خصائص الضحايا" + ], + "depth_level": "analytical", + "citations_required": True, + "notes": "يُراعى تقديم رؤية شاملة للأدوار المختلفة دون تحميل طرف واحد المسؤولية الكاملة." + } + } + ] + } + ], + "style_profile": { + "style": { + "tone": "educational-formal", + "language_level": "standard_academic", + "sentence_structure": "متوسطة إلى طويلة مع بناء منطقي واضح", + "detail_level": "متوسط إلى عالٍ", + "reader_interaction": "غير مباشر دون مخاطبة شخصية في الأقسام النظرية، مع بعض التساؤلات التفاعلية", + "authority_level": "medium", + "authority_markers": [ + "يُلاحظ أن", + "يمكن القول", + "من المعروف أن", + "غالبًا ما", + "في العادة" + ], + "voice_type": "explanatory_authoritative", + "uses_religious_references": False, + "uses_scientific_or_textual_references": True, + "explicit_structure_markers": True, + "example_markers": [ + "من أمثلته", + "من أبرز هذه الأساليب", + "يشمل", + "مثل" + ], + "intro_function": "conceptual_explanation_with_engagement", + "allows_definition_in_intro": True, + "allows_general_explanation_in_intro": True, + "allows_direct_address_in_intro": True, + "allows_explicit_statements": True, + "allows_religious_or_textual_references_in_intro": False, + "recommended_intro_tone": "رصين تمهيدي مع لمسة تساؤلية لإثارة التفكير", + "stylistic_notes": [ + "استخدام أسلوب تساؤلي في بداية بعض الأقسام لإشراك القارئ ذهنيًا.", + "الحفاظ على تسلسل منطقي صارم بين الأفكار مع ترقيم واضح.", + "الالتزام بوضوح الفكرة المركزية في كل فقرة مع تنوع الأمثلة.", + "استخدام مصطلحات مفهومة ومناسبة لمستوى طلاب الجامعات.", + "دمج الإرشادات العملية بأسلوب مباشر عند الحاجة (كما في قسم الحماية).", + "استخدام الاستعارات اللغوية (مثل النمر) لتقريب المفاهيم.", + "التنويع بين الأسلوب التحليلي والأسلوب الإرشادي حسب طبيعة القسم." + ], + "opening_style": "تمهيد يجمع بين التساؤل والتعريف لإثارة الفضول وتقديم المفهوم بوضوح", + "middle_style": "شرح وتفصيل منظم مدعوم بتصنيفات واضحة وأمثلة متنوعة مع المحافظة على الترابط المنطقي", + "closing_style": "إغلاق يلخص الفكرة الأساسية مع رسالة توعوية أو تحفيزية تربط بالقسم التالي" + } + } +}, + "intro":""""رسائل لا تُقال" أحيانًا، لا نعرف أن ما نعيشه يُسمى "تنمّرّا"، لأننا تعوّدنا أن نبتلع الكلمات الجارحة ونضحك على حساب أنفسنا، وكأن الأمر طبيعي. لكننا ندرك فيما بعد أن التنقّر شعورٌ مزعج يرافقنا حتى بعد انتهاء الموقف. فقد تكون جالسًا في صفّك، في عملك، في بيتك ... وتأتيك نظرة، كلمة، تصرّف، يهزّ شيئًا بداخلك. قد تضحك معهم كي لا تبدو حساشا، لكنك لا تنسى. وقد تمرّ التجربة، لكن الأثر يبقی. وفي هذا الفصل، لن نستخدم كلمات معقّدة أو تعريفات جامدة. بل، سنتحدث بصدق عن التنمّر كما هو: كيف يبدأ؟ متى يتسلل؟ من هم أبطاله؟ وسنكتشف معًا أن التنمّر ليس مشكلة ضحية فقط، بل مشكلة يعيشها الجميع بطرق مختلفة. • هل سبق وكنت متنمرًا دون أن تدري؟ • هل سمعت أو شاهدت موقف تنمر والتزمت الصمت؟ هل تعرضت للتنمر ولم تدافع عن نفسك؟ دعنا نجيب على هذه الأسئلة، ولنبدأ من هنا ...""", +} + + +html_template_intro=""" + + + + + + عارض محتوى الفصول + + + +
      +
      +

      📚 محتوى الفصول الأكاديمية

      +

      عرض منظم وجميل لمحتوى الكتاب الأكاديمي

      +
      + +
      + +
      +
      +
      + +
      +
      + + + + + +""" +examples_refrence1 = """ +الانضباط أساس النجاح في الظروف الصعبة + +حين تشتدّ الظروف وتتزاحم التحديات، لا يبقى في الميدان إلا من اعتاد +النظام، واحترام الوقت، والالتزام بالمبادئ. فالانضباط ليس مجرد التزام +خارجي بالتعليمات، بل هو وعيٌّ داخليٌّ يُوجّه السلوك ويصون الجهد، ويجعل +الإنسان قادراً على الثبات مهما تبدّلت الظروف. + +فالانضباط يعني أن تؤدي عملك بدقةٍ وإتقان، وأن تلتزم بالأنظمة والقواعد +بإرادتك لا بخوفٍ من العقاب، فهو مزيج من الحزم والمسؤولية، ومن احترام +الذات قبل احترام التعليمات. فالشخص المنضبط لا ينتظر توجيهاً في كل +مرة، لأنه يدرك أن التزامه بالنظام هو انعكاس لاحترافه وثقته بنفسه. + +وهو أيضًا حجر الزاوية في أي منظومة ناجحة، لأنه يضبط إيقاع العمل +ويمنح المنظمة استقرارها، كما يرفع من مستوى الأداء والجودة. وهو لا + +يقوم على القسوة، بل على الإقناع والتوجيه وبناء الوعي بأهمية احترام +القواعد والوقت والمهام. + +ففي بيئات العمل الصعبة، يصبح الانضباط هو خط الدفاع الأول: فهو الذي +يحفظ الطاقة من الهدر، ويوجّه الجهود نحو الهدف المشترك. فحين يسود +النظام، تتراجع الفوضى، ويجد كل فرد مكانه ودوره بوضوح. + +ولهذا، لا يُقاس الانضباط فقط بمدى الالتزام بالمواعيد أو اللوائح، بل بقدرة +الإنسان على أن يكون ثابتاً ومسؤولاً ومخلصاً في كل موقف. فالعقول +المنضبطة هي التي تبني المجتمعات القادرة على النهوض رغم الضغوط، +وهي التي تصنع النجاح المستدام مهما اشتدت الصعاب. (نوفل، 2015. +ص. 11) + + +الانضباط ليس قيدًا يُكبّل الخطى، بل جناحًا خفيًّا يُعينها على التحليق +بثباتٍ في وجه الرياح. + +حين اجتمع الانضباط والإبداع + +عندما عاد ستيف جوبز إلى آبل عام 1997، كانت الشركة تواجه أوضاعًا صعبة +واحتياطيها المالي القابل للاستهلاك يكاد ينفد. + +فبدلًا من إدارة مركزية مطلقة، اختار جوبز أن يُفعّل نمط العمل الجماعي +والمشاركة الفقالة: خلق فرقًا صغيرة من أصحاب المواهب (A-players)، +منحهم حرية المبادرة داخل إطارٍ من الانضباط العالي، وجرّأهم على الأفكار +الجريئة. + +وكانت الاجتماعات الدورية التي يُراجع فيها الفريق كله الأداء كاملة، تعمل +على تعزيز روح الشراكة والمساءلة: الجميع مشارك في التفكير، كل فرد +يُحاسب نفسه، والنتيجة كانت إعادة إطلاق منتجاتٍ ثورية مثل ال iMac والـ +.iPhone وصولا إلى الـ iPod + +ولقد جمعت تلك التجربة بين الانضباط - صناعة نظام عمل لا يسمح +بالتراخي أو التفريط - وبين المشاركة والتعاون - اعتراف بأن الإبداع والقرار +ليسا حكراً على القائد وحده، بل على الفريق ككل. + +وهكذا، أثبتت آبل أن القيادة بالضمير والتحفيز والمشاركة يمكن أن تحوّل +أزمات اقتصادية أو تنظيمية إلى منصّة انتصار وإبداع. +""" + +examples_refrence2 = """ +دور الأسرة في علاج التنمر +The Family's Role in Addressing Bullying +تُعد الأسرة الدرع الأول لحماية الطفل من الأذى النفسي والاجتماعي. فالمناخ الأسري +المتوازن يصنع طفلًا متوازنًا، قادرًا على مواجهة التنمر دون أن يتحول إلى ضحية أو +متنمر. وذلك من خلال: + +1. بناء بيئة آمنة داخل المنزل +(Establishing A Safe Home Environment) +• عامل أبناءك بالحب، وأشعرهم بالتقبّل دون شروط. +• استبدل الأوامر بالعلاقات الحوارية، فالطفل الذي يُصغى له يتعلم كيف يُصغي +للآخرين ويحترمهم. +تجنّب العنف اللفظي أو الجسدي في التربية، لأن الطفل الذي يختبر العنف في البيت +غالبًا ما ينقله إلى محيطه. +2. المتابعة الواعية دون رقابة خانقة +(Conscious Monitoring Without Suffocating Supervision) +• كن حاضرًا في عالم طفلك دون أن تفرض السيطرة. +• راقب ما يشاهده من محتوى عبر الشاشات والأجهزة الذكية، وناقشه فيما يراه. +• وفّر بدائل إيجابية كالمحتوى التربوي، والبرامج العلمية، والنشاطات الهادفة التي +تصقل شخصيته. +3. تعزيز المهارات النفسية والاجتماعية +(Enhancing Psychosocial Skills) +• علّم أبناءك كيف يعبرون عن أنفسهم، ويدافعون عن آرائهم بطريقة محترمة. +• امدح سلوكهم الإيجابي، وشجعهم على بناء صداقات صحية. +• نمّ لديهم مهارات التعاطف، وضبط المشاعر، واتخاذ القرار. +4. افتح باب الثقة بلا شروط +(Opening The Door To Unconditional Trust) +• اجعل أبناءك يشعرون أن الحديث معك ليس خطرًا، بل أمانًا. +• إذا أخبرك طفلك أنه تعرض للتنمر، لا تغضب، بل استمع له باهتمام، وامدحه على +شجاعته في الحديث. + +• لا تطلب منه أن يتجاهل ما يحدث أو أن يتصرف وحده. وواجه الموقف معه، لا أن +تتركه وحيدًا فيه. (وزارة التعليم، د.ت، ص.11) +5. تدخل فعّال ومتّزن عند حدوث التنمر +(Effective And Balanced Intervention When Bullying Occurs) +• علّم طفلك استراتيجيات المواجهة غير العنيفة، مثل الانسحاب من الموقف، اللعب +مع أصدقاء موثوقين، أو إبلاغ المعلم. +• تواصل مع المدرسة فورًا إذا استمر التنمر. وإن لم يُعالج الموقف، تحدث مباشرة +مع الإدارة. +• لا تُشجعه على العراك، فالعنف لا يُنهي المشكلة بل يفاقمها. +6. الاحتواء النفسي عند ظهور مؤشرات الخطر +(Psychological Containment When Danger Signs Appear) +• راقب تغيّر سلوك طفلك: صمت مفاجئ، خوف من الذهاب إلى المدرسة، اضطرابات +في النوم أو المزاج. +• لا تتردد في استشارة متخصص نفسي أو اجتماعي إذا شعرت بأن الطفل متأثر +بعمق +شاركه أن مشاعره مفهومة ومقبولة، وأنك إلى جانبه مهما كانت المشكلة. +(مؤسسة دبي لرعاية النساء والأطفال، د.ت، ص.9) + +فالأسرة الواعية لا تنتظر وقوع الأذى كي تتحرك، بل تزرع الثقة في نفس الطفل منذ +الصغر، وتعلّمه كيف يُعبّر عن نفسه ويحمي حدوده ... بالكلمة لا بالضرب، وبالموقف لا +بالصراخ، وبالحب لا بالخوف. + +أثبتت الدراسات أن 25% من التلاميذ يشجّعون على سلوكيات التنمّر، إذا +لم تعط لهم تربية سليمة تشجّعهم على نهج سلوكيات مضادة لها. +""" + +examples_refrence3 = """ +اتخاذ القرار في الوقت المناسب + +هو القدرة على مواجهة موقف يتطلب اختيارًا محددًا من بين عدة بدائل، مع +تقييم النتائج المحتملة لكل خيار، واختيار الأنسب بما يحقق الهدف المرجو. +ويُشير القرار إلى الرأي الذي يتخذه الإنسان ويطبقه، فهو عملية عقلية +منظمة تجمع بين التفكير، التقدير، والموازنة بين البدائل للوصول إلى الخيار +الأمثل. + +ويتضمن اتخاذ القرار عناصر رئيسة: وجود مشكلة أو موقف يتطلب اختيارًا، +توافر بدائل محتملة، تحليل هذه البدائل، واختيار الأنسب وفق معايير الرشد +والعقلانية. كما أن القرار ليس مجرد اختيار عشوائي، بل هو عملية واعية +تتطلب المعلومات الكافية، الإدراك الواعي، وترتيب الأولويات بشكل +موضوعي، مع مراعاة التوقيت المناسب للتنفيذ. + +فمهارة اتخاذ القرار تتطلب عقلانية وتبصّر، حيث يتم تقييم البدائل وفق +معايير محددة تشمل وضوح الهدف، ثبات الأولويات، كفاية المعلومات +ومصداقيتها، والإلمام بالمزايا والمنافع لكل خيار. وبهذا، يصبح القرار أداة +فعالة لتحقيق الأهداف الشخصية أو المهنية، وتقليل المخاطر المحتملة، +وضمان استغلال الفرص المتاحة بشكل أمثل. + +فإن القدرة على اتخاذ القرار في الوقت المناسب تمثل الفرق بين من يترك +الفرص تمرّ دون جدوى، ومن يستثمرها لصالح مستقبله. فالتفكير العميق، +الاستماع للنصائح الصادقة، والوعي بتبعات كل خيار، يساعد الفرد على +تحويل اللحظات الحرجة إلى نقاط انطلاق نحو النجاح والتفوق. (النوبى، +2021، ص.90) + +القرارات المصيرية لا تقاس بسرعة اتخاذها، بل بعمق +بصيرتك حين تختار، فالتأنّي الواعي قد يفتح لك أبوابًا لم تكن +تراها وأنت تندفع نحو أول طريق يلوح أمامك. + +القرار الذي صنع إمبراطورية + +لم تكن رحلة صالح كامل إلى عالم المال صدفة، بل سلسلة من +القرارات الجريئة التي اتّخذت في لحظاتها الحاسمة. وُلدت بداياته +من شغفٍ فطري بالتجارة، فبينما كان أقرانه يلعبون، كان هو +يصنع من عظام الخراف أدوات للعب "الكبوش" ويبيعها لزملائه +في المدرسة. وفي مرحلة المراهقة، لم يكتفِ بدور الطالب، بل حرّر +مجلات مدرسية ووزّعها مقابل مكافآت رمزية، لتكون أولى خطواته +نحو بناء حسِّ تجاري ناضچ. +وفي الجامعة، قرر ألا يكتفي بالمقاعد الدراسية، فاستأجر محلًا +صغيرًا قرب الجامعة ليحوّله إلى مطبعة لنسخ المذكرات وبيعها +للطلاب. وبعد تخرّجه، التحق بالعمل الحكومي، لكنه لم يرَ في +الوظيفة نهاية الطريق، بل محطة للتعلّم واكتساب الخبرة. ومع +مرور الوقت، كان عليه أن يتخذ قرارًا مصيريًا: هل يظل موظفًا +حكوميًّا آمنًا، أم يخوض مغامرة العمل الحر بكل ما فيها من +مخاطرة؟ فاختار الثانية، في خطوة بدت حينها جريئة، لكنها كانت +الشرارة التي غيّرت مجرى حياته. + +وبدأ "كمُعقّب" ينجز المعاملات، ثم أسس مشروعه الأول في +"نقل البريد الداخلي"، برأس مال لا يتجاوز 300 ألف ريال جمعها +من مشروعاته الصغيرة السابقة. لم يعتمد على الحظ، بل جاب +مدن المملكة بنفسه ليقيس المسافات ويحسب الوقود ويضع +خطة تشغيل دقيقة. ونجح المشروع واستمر أكثر من خمسة +عشر عامًا، فاتّسع طموحه أكثر، ليؤسس بعدها مجموعة "دله +البركة"، إحدى أضخم المجموعات الاقتصادية في المنطقة. + +فالقرار المفصلي في مسيرته جاء عندما تقدّم بعرض لتنفيذ +مشروع مدرسة الدفاع الجوي بجدة بمبلغ 15 مليون دولار، في +وقت قدّمت فيه شركة أمريكية عرضًا يفوقه بعشرة أضعاف. لم +يتردد صالح كامل، بل اتخذ القرار في الوقت المناسب، ففاز بالعقد +وفتح أمامه أبواب النجاح الواسعة. ومن هناك انطلقت شركته +إلى العالمية، وأصبحت "دله" اسمًا مرادفًا للتجارة الذكية والرؤية +البعيدة. + +ولقد أثبت صالح كامل أن القرارات المصيرية لا تحتاج إلى تسرّع، +بل إلى بصيرة تدرك متى تتحرّك ومتى تتريّث. فحين تأتي اللحظة +المناسبة، يكون القرار الحاسم هو الفارق بين من يعيش الفرصة، +ومن يصنع منها تاريخًا. (معهد ريادة الأعمال، 2025) +""" + + +examples_refrence4 = """ +تحمل المسؤولية المبكرة + +لا يُولد الإنسان عارقًا بالمسؤولية، بل يتعلّمها كما يتعلّم الكلام والسلوك +والأخلاق: فالمسؤولية ليست صفة فطرية، بل مهارة حياتية تُزرع في +الطفل منذ لحظاته الأولى، وتنمو معه خطوةً بخطوة حتى تكتمل في مرحلة +المراهقة. + +ويبدأ الطفل في اكتساب هذا الشعور من بيئته الأولى: من والديه ومن كل +من يتعامل معهم. فطريقة العناية به، وحدود الحرية الممنوحة له، ونوع +التوجيه الذي يتلقاه، كلها تُسهم في بناء إحساسه بالمسؤولية. ولهذا، فإن +الأبوين والمعلمين في المراحل المبكرة - من الروضة وحتى نهاية المرحلة +الثانوية - يتحملون دورًا أساسيًا في ترسيخ هذا الشعور، لأنه لا يأتي صدفة +ولا يتكوّن فجأة. + +فالطفل يتعلّم المسؤولية من خلال الاعتماد المتدرّج على ذاته. ففي البداية +يعتمد على والديه لتلبية حاجاته، ثم يبدأ تدريجيًا بتحمل أدوار صغيرة تناسب +قدراته، كترتيب ألعابه أو المساعدة في أعمال بسيطة. وهكذا، يتعلم أن +العطاء والالتزام جزء من كيانه، لا مجرّد تكليف خارجي. + +فالأسرة المدرسة الأولى للمسؤولية؛ إذ يتعلّم فيها الأبناء أن المسؤولية +ليست عبئًا، بل مشاركة وبذل واحترام للآخرين. ومن يتعلّم في بيئة قائمة +على التعاون والتعاطف، يشبُّ إنسانًا ناضجًا قادرًا على العطاء في أي +موقع. (فوستر، 1994، ص.16) + +المسؤولية لا تُمنح، بل تُكتسب بالثقة والعمل؛ ومن يتعلّم +حملها صغيرًا، يحمل الحياة بثباتٍ حين يكبر. + +كانت المزرعة أكثر من بيتٍ وذكريات؛ كانت معملًا للحياة، علّمتني أن القيادة +تبدأ من حبة تراب، وأن من أتقن العمل، وقدّم الجهد قبل الراحة، وتحمّل +المسؤولية مبكرًا، هو من يملك أن يصنع أثرًا لا يزول. فالقيم لا تُدرّس +بالكلمات، بل تُزرع بالأفعال، وما غُرس في تلك الأرض ظلّ ينبت فيّ حتى +اليوم. +""" +enrichment_examples = { + "example1": { + "content_before_enrich": examples_refrence1.split( # ref3 + "الانضباط ليس قيدًا يُكبّل الخطى" + )[0].strip(), + "content_with_enrich": examples_refrence1.strip(), + }, + "example2": { + "content_before_enrich": examples_refrence2.split( # ref5 + "أثبتت الدراسات أن 25%" + )[0].strip(), + "content_with_enrich": examples_refrence2.strip(), + }, + "example3": { + "content_before_enrich": examples_refrence3.split( # ref1 + "القرارات المصيرية لا تقاس بسرعة اتخاذها" + )[0].strip(), + "content_with_enrich": examples_refrence3.strip(), + }, + "example4": { # ref 2 + "content_before_enrich": examples_refrence4.split("المسؤولية لا تُمنح")[ + 0 + ].strip(), + "content_with_enrich": examples_refrence4.strip(), + }, +} diff --git a/modules/serper_env.py b/modules/serper_env.py new file mode 100644 index 0000000000000000000000000000000000000000..5a99fce81b3fde0796f871dda9b7207cde923c02 --- /dev/null +++ b/modules/serper_env.py @@ -0,0 +1,40 @@ +import http.client +import json +import os +import requests +SERPER_API_KEYS = [ + os.getenv("SERPER_API_KEY_1", ""), + os.getenv("SERPER_API_KEY_2", ""), + os.getenv("SERPER_API_KEY_3", ""), +] + +CURRENT_SERPER_INDEX = 0 + + +def get_serper_credits(api_key: str) -> int: + try: + conn = http.client.HTTPSConnection("google.serper.dev") + headers = {"X-API-KEY": api_key} + conn.request("GET", "/credits", headers=headers) + res = conn.getresponse() + data = json.loads(res.read().decode("utf-8")) + return int(data.get("credits", 0)) + except: + return 0 + + +def get_valid_serper_key(min_credits=100): + global CURRENT_SERPER_INDEX + + for idx, key in enumerate(SERPER_API_KEYS): + if not key: + continue + credits = get_serper_credits(key) + if credits >= min_credits: + CURRENT_SERPER_INDEX = idx + return key, credits, CURRENT_SERPER_INDEX + + raise Exception( + "❌ SERPER credits exhausted on all APIs. Please contact the developer. " + "❌ يجب التواصل مع المطور لإضافة API Keys جديدة لSERPER" + ) diff --git a/rag/automation.py b/rag/automation.py new file mode 100644 index 0000000000000000000000000000000000000000..6ae7324cbc86979baeed863f19fc7db61152683a --- /dev/null +++ b/rag/automation.py @@ -0,0 +1,146 @@ +from typing import List +import time + + +# ========================= +# Query Bundle Builder +# ========================= +def build_query_bundle(sub: dict, chapter_title: str, section_title: str) -> List[str]: + planning = sub.get("planning", {}) + queries = [] + + if planning.get("core_idea"): + queries.append(planning["core_idea"]) + + for kp in planning.get("key_points", [])[:3]: + if isinstance(kp, str): + queries.append(kp) + + for q in sub.get("suggested_queries", [])[:2]: + queries.append(q) + + queries.append(sub.get("title", "")) + queries.append(f"{chapter_title} - {section_title}") + + seen = set() + final = [] + for q in queries: + q = q.strip() + if q and q not in seen: + seen.add(q) + final.append(q) + + return final + + +# ========================= +# Citations +# ========================= +def make_in_text_citation(author, year, page_start): + a = author or "مؤلف" + y = year if isinstance(year, int) else "د.ت" + p = page_start if isinstance(page_start, int) else "؟" + return f"({a}، {y}، ص {p})" + + +def make_reference_apa(author, year, title): + a = author or "مؤلف" + y = year if isinstance(year, int) else "د.ت" + t = title or "مصدر بدون عنوان" + return f"{a}. ({y}). {t}." + + +# ========================= +# RAG for Subsection (MULTI-BOOK) +# ========================= +def build_rag_context_for_subsection( + rag_engines: List, + sub: dict, + chapter_title: str, + section_title: str, + top_k: int = 5, +) -> dict: + + queries = build_query_bundle(sub, chapter_title, section_title) + all_hits = [] + + # 🔥 search in ALL source books + for rag in rag_engines: + hits = rag.retrieve(queries=queries) + all_hits.extend(hits) + + if not all_hits: + return { + "query_bundle": queries, + "selected_k": 0, + "chunks": [], + "coverage_note": "لم يتم العثور على مراجع مناسبة لهذا المحور.", + } + + # sort globally + all_hits = sorted(all_hits, key=lambda x: x.score, reverse=True)[:top_k] + + chunks = [] + for h in all_hits: + payload = h.payload or {} + chunks.append( + { + "chunk_id": h.id, + "score": h.score, + "doc_id": payload.get("doc_id"), + "page_start": payload.get("page_start"), + "page_end": payload.get("page_end"), + "title": payload.get("title"), + "author": payload.get("author"), + "year": payload.get("year"), + "text": payload.get("text"), + "in_text_citation": make_in_text_citation( + payload.get("author"), + payload.get("year"), + payload.get("page_start"), + ), + "reference_apa": make_reference_apa( + payload.get("author"), + payload.get("year"), + payload.get("title"), + ), + } + ) + + return { + "query_bundle": queries, + "selected_k": len(chunks), + "chunks": chunks, + "coverage_note": "تم اختيار أفضل المراجع المتوافقة مع فكرة المحور وحدوده.", + } + + +# ========================= +# Book-Level Automation +# ========================= +def run_rag_automation_on_book( + planned_book: dict, + rag_engines: List, + top_k: int = 5, + sleep_s: float = 0.1, +) -> dict: + + for chapter in planned_book.get("chapters", []): + ch_title = chapter.get("chapter_title", "") + + for section in chapter.get("sections", []): + sec_title = section.get("title", "") + + for sub in section.get("subsections", []): + rag_context = build_rag_context_for_subsection( + rag_engines=rag_engines, + sub=sub, + chapter_title=ch_title, + section_title=sec_title, + top_k=top_k, + ) + + sub["rag_context"] = rag_context + time.sleep(sleep_s) + + return planned_book diff --git a/rag/automation_metadata.py b/rag/automation_metadata.py new file mode 100644 index 0000000000000000000000000000000000000000..21ecc4dfce2bded97821b4a62fb68e8adcdc51f3 --- /dev/null +++ b/rag/automation_metadata.py @@ -0,0 +1,176 @@ +from __future__ import annotations +from typing import Dict, Any, List + +from core.books.storage import ( + fetch_raw_docs_for_user, + mark_raw_status, + upsert_document_metadata, +) +from schemas.books.sources_schema import SourceForAgent, DocMetadata +from agents.books.apa_agent import run_metadata_agent + + +# -------- limits (VERY IMPORTANT) -------- +MAX_PAGES = 5 +MAX_CHARS_PER_PAGE = 5000 + + +def _build_agent_input(raw: Dict[str, Any]) -> Dict[str, Any]: + pages = raw.get("pages_head") or [] + # pages = pages[:MAX_PAGES] + # pages = [p[:MAX_CHARS_PER_PAGE] for p in pages] + + return SourceForAgent( + source_url=raw["source_url"], + source_type=raw.get("source_type", "pdf"), + domain=raw.get("domain", ""), + search_title=raw.get("search_title", "") or "", + search_snippet=raw.get("search_snippet", "") or "", + text_pages=pages, + ).model_dump() + + +def process_user_metadata(user_id: str, book_id: str | None = None) -> Dict[str, Any]: + pending = fetch_raw_docs_for_user( + user_id=user_id, + book_id=book_id, + status="pending", + ) + + total = len(pending) + processed = 0 + failed = 0 + items: List[Dict[str, Any]] = [] + tokens = [] + + if total == 0: + return { + "user_id": user_id, + "book_id": book_id, + "total": 0, + "processed": 0, + "failed": 0, + "items": [], + "mode": "sequential", + } + + for raw in pending: + doc_id = raw["doc_id"] + url = raw["source_url"] + + pages_head = raw.get("pages_head") or [] + if not pages_head: + mark_raw_status(doc_id, "failed", "no_pages_head") + failed += 1 + items.append( + { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": "no_pages_head", + } + ) + continue + + mark_raw_status(doc_id, "processing", "") + + try: + agent_in = _build_agent_input(raw) + out, token_usage = run_metadata_agent(agent_in) + except Exception as e: + print(f"⚠️ LLM internal exception for doc_id {doc_id}: {e}") + out = None + + # ---- بعد الـ try/except ---- + if not isinstance(out, dict): + mark_raw_status(doc_id, "failed", "llm_exception") + failed += 1 + items.append( + { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": "llm_exception", + } + ) + continue + + if token_usage: + tokens.append(token_usage) + + print(f"Metadata agent output for doc_id {doc_id}: {type(out)} - {out}") + print(f"is_valid: {out.get('is_valid')}") + if not out.get("is_valid"): + reason = out.get("reason", "invalid") + mark_raw_status(doc_id, "failed", reason) + failed += 1 + items.append( + { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": reason, + } + ) + continue + + md = out.get("metadata") or {} + if "title" not in md or "authors" not in md: + mark_raw_status(doc_id, "failed", "missing_title_or_authors") + failed += 1 + items.append( + { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": "missing_title_or_authors", + } + ) + continue + + try: + doc_md = DocMetadata( + doc_id=doc_id, + title=md["title"], + authors=md["authors"], + year=md.get("year"), + publisher_or_journal=md.get("publisher_or_journal") or "", + normalized_source_type=out.get("normalized_source_type", "pdf"), + apa7=md.get("apa7") or "", + metadata=out, + ) + + upsert_document_metadata(doc_md) + mark_raw_status(doc_id, "processed", "") + processed += 1 + items.append( + { + "doc_id": doc_id, + "url": url, + "status": "processed", + "title": doc_md.title, + } + ) + + except Exception: + mark_raw_status(doc_id, "failed", "db_failed") + failed += 1 + items.append( + { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": "db_failed", + } + ) + + return { + "user_id": user_id, + "book_id": book_id, + "total": total, + "processed": processed, + "failed": failed, + "items": items, + "mode": "sequential", + "tokens": tokens, + } diff --git a/rag/automation_metadata_new.py b/rag/automation_metadata_new.py new file mode 100644 index 0000000000000000000000000000000000000000..f7fa3b3f215de9bf030e0cf85faf140c9f882faa --- /dev/null +++ b/rag/automation_metadata_new.py @@ -0,0 +1,244 @@ +from __future__ import annotations +from typing import Dict, Any, List, Tuple +from concurrent.futures import ThreadPoolExecutor, as_completed + +from core.books.storage import ( + fetch_raw_docs_for_user, + mark_raw_status, + upsert_document_metadata, + delete_raw_doc, +) + +from schemas.books.sources_schema import SourceForAgent, DocMetadata +from agents.books.apa_agent import run_metadata_agent + +from .rag_engine_sources import ArabicBookRAGWithSources + + +# ========================= +# CONFIG +# ========================= +MAX_WORKERS = 4 +DELETE_ON_FAIL = True + + +# ========================= +# DELETE EVERYWHERE +# ========================= +def delete_document_everywhere(user_id: str, book_id: str, doc_id: str, rag_engine) -> None: + + for i in range(3): + try: + rag_engine.delete_from_qdrant(doc_id) + delete_raw_doc(doc_id) + print(f"🗑️ Fully deleted doc: {doc_id}") + return + except Exception as e: + print(f"Retry {i+1} failed: {e}") + + print(f"❌ Failed to delete after retries: {doc_id}") + + +# ========================= +# BUILD INPUT +# ========================= +def _build_agent_input(raw: Dict[str, Any]) -> Dict[str, Any]: + pages = raw.get("pages_head") or [] + + return SourceForAgent( + source_url=raw["source_url"], + source_type=raw.get("source_type", "pdf"), + domain=raw.get("domain", ""), + search_title=raw.get("search_title", "") or "", + search_snippet=raw.get("search_snippet", "") or "", + text_pages=pages, + ).model_dump() + + +# ========================= +# SINGLE DOC PROCESSOR +# ========================= +def process_single_doc( + raw: Dict[str, Any], + collection: str, + user_id: str, + book_id: str, + rag_engine, +) -> Tuple[Dict, int]: + + doc_id = raw["doc_id"] + url = raw["source_url"] + + try: + pages_head = raw.get("pages_head") or [] + + if not pages_head: + mark_raw_status(doc_id, "failed", "no_pages_head") + + if DELETE_ON_FAIL: + delete_document_everywhere(user_id, book_id, doc_id, rag_engine) + + return { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": "no_pages_head", + }, 0 + + mark_raw_status(doc_id, "processing", "") + + agent_in = _build_agent_input(raw) + out, token_usage = run_metadata_agent(agent_in) + + if not isinstance(out, dict): + mark_raw_status(doc_id, "failed", "llm_exception") + + if DELETE_ON_FAIL: + delete_document_everywhere(user_id, book_id, doc_id, rag_engine) + + return { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": "llm_exception", + }, 0 + + if not out.get("is_valid"): + reason = out.get("reason", "invalid") + + mark_raw_status(doc_id, "failed", reason) + + if DELETE_ON_FAIL: + delete_document_everywhere(user_id, book_id, doc_id,rag_engine) + + return { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": reason, + }, token_usage or 0 + + md = out.get("metadata") or {} + + if "title" not in md or "authors" not in md: + mark_raw_status(doc_id, "failed", "missing_title_or_authors") + + if DELETE_ON_FAIL: + delete_document_everywhere(user_id, book_id, doc_id,rag_engine) + + return { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": "missing_title_or_authors", + }, token_usage or 0 + + # ✅ SUCCESS + doc_md = DocMetadata( + doc_id=doc_id, + title=md["title"], + authors=md["authors"], + year=md.get("year"), + publisher_or_journal=md.get("publisher_or_journal") or "", + normalized_source_type=out.get("normalized_source_type", "pdf"), + apa7=md.get("apa7") or "", + metadata=out, + ) + + upsert_document_metadata(doc_md) + mark_raw_status(doc_id, "processed", "") + + return { + "doc_id": doc_id, + "url": url, + "status": "processed", + "title": doc_md.title, + }, token_usage or 0 + + except Exception as e: + print(f"❌ Unexpected error {doc_id}: {e}") + + mark_raw_status(doc_id, "failed", "unexpected_error") + + if DELETE_ON_FAIL: + delete_document_everywhere(user_id, book_id, doc_id, rag_engine) + + return { + "doc_id": doc_id, + "url": url, + "status": "failed", + "reason": "unexpected_error", + }, 0 + + +def get_rag_engine(user_id, book_id): + return ArabicBookRAGWithSources( + user_id=user_id, + book_id=book_id, + embedding_model="sentence-transformers/all-MiniLM-L6-v2", + ) + + +# ========================= +# MAIN FUNCTION +# ========================= +def process_user_metadata( + user_id: str, + book_id: str | None = None, +) -> Dict[str, Any]: + + pending = fetch_raw_docs_for_user( + user_id=user_id, + book_id=book_id, + status="pending", + ) + + total = len(pending) + + if total == 0: + return { + "user_id": user_id, + "book_id": book_id, + "total": 0, + "processed": 0, + "failed": 0, + "items": [], + "mode": "parallel", + } + + processed = 0 + failed = 0 + items: List[Dict[str, Any]] = [] + tokens = [] + + # 🔥 أهم سطر (collection per user/book) + collection = f"user_{user_id}__book_{book_id}" + rag_engine = get_rag_engine(user_id, book_id) + with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + futures = [ + executor.submit(process_single_doc, raw, collection,user_id,book_id,rag_engine) for raw in pending + ] + + for future in as_completed(futures): + result, token_usage = future.result() + + items.append(result) + + if result["status"] == "processed": + processed += 1 + else: + failed += 1 + + if token_usage: + tokens.append(token_usage) + + return { + "user_id": user_id, + "book_id": book_id, + "total": total, + "processed": processed, + "failed": failed, + "items": items, + "mode": "parallel", + "tokens": tokens, + } diff --git a/rag/automation_sources.py b/rag/automation_sources.py new file mode 100644 index 0000000000000000000000000000000000000000..e4b2143303c8eb156ab956315b446839096e382c --- /dev/null +++ b/rag/automation_sources.py @@ -0,0 +1,479 @@ +from typing import Any, Dict, List, Tuple +import time + +from core.books.storage import fetch_document_metadata + +# ========================= +# Helpers +# ========================= +def _as_dict(x: Any) -> Dict[str, Any]: + return x if isinstance(x, dict) else {} + + +def _as_list(x: Any) -> List[Any]: + return x if isinstance(x, list) else [] + + +def _get_planning_obj(sub: Dict[str, Any]) -> Dict[str, Any]: + """ + OLD schema: sub["planning"] موجود + NEW schema: الخطة نفسها هي sub مباشرة (plan object) + """ + planning = sub.get("planning") + if isinstance(planning, dict): + return planning + return sub + + +def _normalize_author(author_val: Any) -> str: + """ + يحاول يطلع اسم مؤلف مناسب للاقتباس. + """ + if isinstance(author_val, list): + for a in author_val: + if isinstance(a, str) and a.strip(): + return a.strip() + return "مؤلف" + if isinstance(author_val, str) and author_val.strip(): + return author_val.strip() + return "مؤلف" + + +def _normalize_year(year_val: Any) -> Any: + """ + يرجّع int لو ينفع، وإلا None. + """ + if isinstance(year_val, int): + return year_val + if isinstance(year_val, str): + y = year_val.strip() + if y.isdigit(): + try: + return int(y) + except Exception: + return None + return None + + +# ========================= +# Query Bundle Builder +# ========================= +def build_query_bundle( + sub: dict, + chapter_title: str, + section_title: str, + subsection_title: str = "", +) -> List[str]: + planning = _get_planning_obj(sub) + queries: List[str] = [] + + # 1) core_idea + core_idea = planning.get("core_idea") + if isinstance(core_idea, str) and core_idea.strip(): + queries.append(core_idea.strip()) + + # 2) key_points (top 3) + for kp in _as_list(planning.get("key_points"))[:3]: + if isinstance(kp, str) and kp.strip(): + queries.append(kp.strip()) + + # 3) suggested_queries (لو موجودة في أي شكل) + for q in _as_list(sub.get("suggested_queries"))[:2]: + if isinstance(q, str) and q.strip(): + queries.append(q.strip()) + + # 4) subsection title + if isinstance(subsection_title, str) and subsection_title.strip(): + queries.append(subsection_title.strip()) + elif isinstance(sub.get("title"), str) and sub.get("title", "").strip(): + # fallback للقديم + queries.append(sub["title"].strip()) + + # 5) context query + context_q = f"{chapter_title} - {section_title}".strip(" -") + if context_q: + queries.append(context_q) + + # de-dup مع الحفاظ على الترتيب + seen = set() + final = [] + for q in queries: + qq = q.strip() + if qq and qq not in seen: + seen.add(qq) + final.append(qq) + + return final + + +# ========================= +# Citations +# ========================= +def make_in_text_citation(author, year, page_start, title=None): + a = _normalize_author(author) + y = year if isinstance(year, int) else "د.ت" + p = page_start if isinstance(page_start, int) else "؟" + t = title.strip() if isinstance(title, str) and title.strip() else "مصدر بدون عنوان" + + if a == "مؤلف": + return f"({t}، {y}، ص. {p})" + citation = f"({a}، {y}، ص. {p})" + + return citation + + +def make_reference_apa(author, year, title, source_url=None,publisher_or_journal="", normalized_source_type="pdf"): + a = _normalize_author(author) + y = year if isinstance(year, int) else "د.ت" + t = title.strip() if isinstance(title, str) and title.strip() else "مصدر بدون عنوان" + p = publisher_or_journal.strip() if isinstance(publisher_or_journal, str) and publisher_or_journal.strip() else "" + n= normalized_source_type.strip() if isinstance(normalized_source_type, str) and normalized_source_type.strip() else "" + + if source_url: + base = f"المؤلفون:{a}. التاريخ:({y}). العنوان:{t}. الجامعة/الناشر/المجلة:{p}. نوع المصدر:{n}." + return f"{base} متاح على لينك: {source_url}" + return base + + +# ========================= +# RAG for Subsection / Plan +# ========================= +# def build_rag_context_for_subsection( +# rag_engines: List, +# sub: dict, +# chapter_title: str, +# section_title: str, +# subsection_title: str = "", +# top_k: int = 5, +# ) -> dict: +# queries = build_query_bundle( +# sub=sub, +# chapter_title=chapter_title, +# section_title=section_title, +# subsection_title=subsection_title, +# ) + +# all_hits = [] + +# # 🔍 search in all collections +# for rag in rag_engines: +# try: +# print("🔍 Searching collection:", getattr(rag, "collection", "unknown")) +# print("🔍 Queries:", queries) + +# # count اختياري (لو متاح في engine) +# try: +# count = rag.qdrant.count(rag.collection, exact=True) +# print("📦 Collection count:", count) +# except Exception: +# pass + +# hits = rag.retrieve(queries=queries) or [] +# all_hits.extend(hits) +# except Exception as e: +# print( +# f"⚠️ RAG engine failed on collection {getattr(rag, 'collection', 'unknown')}: {e}" +# ) +# continue + +# if not all_hits: +# return { +# "query_bundle": queries, +# "selected_k": 0, +# "chunks": [], +# "coverage_note": "لم يتم العثور على مراجع مناسبة لهذا المحور.", +# } + +# # sort globally + dedupe (doc_id, chunk_id) +# all_hits = sorted(all_hits, key=lambda x: getattr(x, "score", 0.0), reverse=True) + +# unique: List[Any] = [] +# seen_keys = set() +# for h in all_hits: +# payload = _as_dict(getattr(h, "payload", {})) +# key = (payload.get("doc_id"), getattr(h, "id", None)) +# if key in seen_keys: +# continue +# seen_keys.add(key) +# unique.append(h) +# if len(unique) >= top_k: +# break + +# chunks = [] +# for h in unique: +# payload = _as_dict(getattr(h, "payload", {})) +# doc_id = payload.get("doc_id") +# source_url = payload.get("source_url") + +# md = fetch_document_metadata(doc_id) if doc_id else None +# md = _as_dict(md) + +# # metadata fallback names +# author = md.get("authors") or md.get("author") +# year = _normalize_year(md.get("year")) +# title = md.get("title") or payload.get("title") + +# page_start = payload.get("page_start") +# if isinstance(page_start, str) and page_start.isdigit(): +# page_start = int(page_start) + +# page_end = payload.get("page_end") +# if isinstance(page_end, str) and page_end.isdigit(): +# page_end = int(page_end) + +# chunks.append( +# { +# "chunk_id": getattr(h, "id", None), +# "score": float(getattr(h, "score", 0.0) or 0.0), +# "doc_id": doc_id, +# "source_url": source_url, +# "page_start": page_start, +# "page_end": page_end, +# "text": payload.get("text"), +# "title": title, +# "author": _normalize_author(author), +# "year": year, +# "in_text_citation": make_in_text_citation( +# author=author, +# year=year, +# page_start=page_start, +# source_url=source_url, +# ), +# "reference_apa": make_reference_apa( +# author=author, year=year, title=title, source_url=source_url +# ), +# } +# ) + +# return { +# "query_bundle": queries, +# "selected_k": len(chunks), +# "chunks": chunks, +# "coverage_note": "تم اختيار أفضل المراجع المتوافقة مع فكرة المحور وحدوده.", +# } + + +def build_rag_context_for_subsection( + rag_engines: List, + sub: dict, + chapter_title: str, + section_title: str, + subsection_title: str = "", + top_k: int = 5, +) -> dict: + + queries = build_query_bundle( + sub=sub, + chapter_title=chapter_title, + section_title=section_title, + subsection_title=subsection_title, + ) + + all_hits = [] + + # 🔍 search in all collections + for rag in rag_engines: + try: + print("🔍 Searching collection:", getattr(rag, "collection", "unknown")) + print("🔍 Queries:", queries) + + try: + count = rag.qdrant.count(rag.collection, exact=True) + print("📦 Collection count:", count) + except Exception: + pass + + hits = rag.retrieve(queries=queries) or [] + all_hits.extend(hits) + + except Exception as e: + print( + f"⚠️ RAG engine failed on collection {getattr(rag, 'collection', 'unknown')}: {e}" + ) + continue + + if not all_hits: + return { + "query_bundle": queries, + "selected_k": 0, + "chunks": [], + "coverage_note": "لم يتم العثور على مراجع مناسبة لهذا المحور.", + } + + # ============================== + # Diversity + Source-Aware Ranking + # ============================== + + # sort by base score + all_hits = sorted( + all_hits, + key=lambda x: float(getattr(x, "score", 0.0) or 0.0), + reverse=True, + ) + + max_per_doc = 2 + min_unique_docs = 3 + + doc_counter = {} + rescored_hits = [] + + for h in all_hits: + payload = _as_dict(getattr(h, "payload", {})) + doc_id = payload.get("doc_id") + + if not doc_id: + continue + + base_score = float(getattr(h, "score", 0.0) or 0.0) + current_count = doc_counter.get(doc_id, 0) + + if current_count >= max_per_doc: + continue + + diversity_penalty = 1 - (0.25 * current_count) + adjusted_score = base_score * diversity_penalty + + doc_counter[doc_id] = current_count + 1 + + rescored_hits.append((h, adjusted_score)) + + # نرتب بالـ adjusted_score + rescored_hits = sorted(rescored_hits, key=lambda x: x[1], reverse=True) + + # ناخد top_k + selected = rescored_hits[:top_k] + + unique_doc_ids = { + _as_dict(getattr(h, "payload", {})).get("doc_id") + for h, _ in selected + if _as_dict(getattr(h, "payload", {})).get("doc_id") + } + + if len(unique_doc_ids) < min_unique_docs: + print("⚠️ Warning: Low source diversity detected") + + # ============================== + # Build Final Chunks + # ============================== + + chunks = [] + + for h, adjusted_score in selected: + payload = _as_dict(getattr(h, "payload", {})) + doc_id = payload.get("doc_id") + source_url = payload.get("source_url") + + md = fetch_document_metadata(doc_id) if doc_id else None + md = _as_dict(md) + + author = md.get("authors") or md.get("author") + year = _normalize_year(md.get("year")) + title = md.get("title") or payload.get("title") + publisher_or_journal= md.get("publisher_or_journal") or "" + normalized_source_type= md.get("normalized_source_type", "pdf") + + page_start = payload.get("page_start") + if isinstance(page_start, str) and page_start.isdigit(): + page_start = int(page_start) + + page_end = payload.get("page_end") + if isinstance(page_end, str) and page_end.isdigit(): + page_end = int(page_end) + + chunks.append( + { + "chunk_id": getattr(h, "id", None), + "score": float(getattr(h, "score", 0.0) or 0.0), + "adjusted_score": float(adjusted_score), + "doc_id": doc_id, + "source_url": source_url, + "page_start": page_start, + "page_end": page_end, + "text": payload.get("text"), + "title": title, + "author": _normalize_author(author), + "year": year, + "in_text_citation": make_in_text_citation( + author=author, + year=year, + page_start=page_start, + title=title, + ), + "reference_apa": make_reference_apa( + author=author, + year=year, + title=title, + source_url=source_url, + publisher_or_journal=publisher_or_journal, + normalized_source_type=normalized_source_type, + ), + } + ) + + return { + "query_bundle": queries, + "selected_k": len(chunks), + "chunks": chunks, + "coverage_note": "تم اختيار أفضل المراجع المتوافقة مع فكرة المحور وحدوده.", + } + + +# ========================= +# Book-Level Automation +# Supports NEW + OLD schema +# ========================= +def run_rag_automation_on_book_sources( + planned_book: dict, + rag_engines: List, + top_k: int = 5, + sleep_s: float = 0.1, +) -> dict: + for chapter in _as_list(planned_book.get("chapters")): + ch_title = chapter.get("chapter_title", "") + + for section in _as_list(chapter.get("sections")): + # NEW: section_title | OLD: title + sec_title = section.get("section_title", section.get("title", "")) + + # ------------------------- + # NEW SCHEMA: plans (dict) + # ------------------------- + plans = section.get("plans") + if isinstance(plans, dict): + for sub_title, plan_obj in plans.items(): + if not isinstance(plan_obj, dict): + continue + + rag_context = build_rag_context_for_subsection( + rag_engines=rag_engines, + sub=plan_obj, # plan نفسه + chapter_title=ch_title, + section_title=sec_title, + subsection_title=str(sub_title), + top_k=top_k, + ) + + section["plans"][sub_title]["rag_context"] = rag_context + time.sleep(sleep_s) + continue + + # ------------------------- + # OLD SCHEMA fallback: subsections (list) + # ------------------------- + for sub in _as_list(section.get("subsections")): + if not isinstance(sub, dict): + continue + + rag_context = build_rag_context_for_subsection( + rag_engines=rag_engines, + sub=sub, + chapter_title=ch_title, + section_title=sec_title, + subsection_title=sub.get("title", ""), + top_k=top_k, + ) + + sub["rag_context"] = rag_context + time.sleep(sleep_s) + + return planned_book diff --git a/rag/chuncking.py b/rag/chuncking.py new file mode 100644 index 0000000000000000000000000000000000000000..70de18a1392b145441db6eedeacd5f7300cd3661 --- /dev/null +++ b/rag/chuncking.py @@ -0,0 +1,86 @@ +# chunking.py +import re +from typing import List, Tuple + + +def approx_token_count(text: str) -> int: + """ + Rough token estimate for chunk sizing (heuristic). + Keep this simple but consistent: ~4 chars per token. + """ + return max(1, len(text) // 4) + + +def chunk_pages( + pages: List[str], + target_tokens: int = 520, + overlap_tokens: int = 80, +) -> List[Tuple[str, int, int]]: + """ + Split pages (list[str]) into overlapping chunks. + Returns list of tuples: (chunk_text, page_start, page_end) + + Overlap is implemented at paragraph level (keeps page ranges correct). + """ + chunks: List[Tuple[str, int, int]] = [] + + buffer: List[str] = [] + buffer_pages: List[int] = [] + buffer_tokens = 0 + + def make_chunk(): + nonlocal buffer, buffer_pages, buffer_tokens + if not buffer: + return + chunk_text = "\n\n".join(buffer).strip() + page_start = min(buffer_pages) + page_end = max(buffer_pages) + chunks.append((chunk_text, page_start, page_end)) + + for page_idx, page in enumerate(pages, start=1): + paragraphs = [p.strip() for p in re.split(r"\n\s*\n", page) if p.strip()] + + for para in paragraphs: + t = approx_token_count(para) + + # If single paragraph exceeds target, create it as its own chunk + if buffer_tokens == 0 and t > target_tokens: + # make chunk with this large paragraph alone + buffer = [para] + buffer_pages = [page_idx] + buffer_tokens = t + make_chunk() + buffer, buffer_pages, buffer_tokens = [], [], 0 + continue + + # If adding this paragraph would exceed target, flush current chunk + if buffer_tokens + t > target_tokens: + make_chunk() + + # prepare overlap: keep tail paragraphs whose tokens sum >= overlap_tokens + tail_buffer: List[str] = [] + tail_pages: List[int] = [] + tail_tokens = 0 + # iterate buffer in reverse to pick tail paragraphs + for p, p_pg in zip(reversed(buffer), reversed(buffer_pages)): + pt = approx_token_count(p) + tail_buffer.insert(0, p) + tail_pages.insert(0, p_pg) + tail_tokens += pt + if tail_tokens >= overlap_tokens: + break + + buffer = tail_buffer + buffer_pages = tail_pages + buffer_tokens = tail_tokens + + # append current paragraph + buffer.append(para) + buffer_pages.append(page_idx) + buffer_tokens += t + + # final flush + if buffer: + make_chunk() + + return chunks diff --git a/rag/ingest_net.py b/rag/ingest_net.py new file mode 100644 index 0000000000000000000000000000000000000000..77dd0cfa936291044ef44c1e2c81a0a225276ae9 --- /dev/null +++ b/rag/ingest_net.py @@ -0,0 +1,183 @@ +# rag/ingest_net.py +import uuid +import os +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import List, Dict, Any, Tuple + +from core.books.fetch_url import get_pdf_bytes +from core.books.storage import ( + init_db, + check_db_health, + insert_raw_document, + mark_raw_status, +) +from schemas.books.sources_schema import DocRaw + +MAX_INGEST_WORKERS = int(os.getenv("MAX_INGEST_WORKERS", "6")) +MIN_PDF_SIZE_KB = int(os.getenv("MIN_PDF_SIZE_KB", "60")) + +EMBEDDING_MODEL = os.getenv( + "EMBEDDING_MODEL", + "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", +) + +HEAD_PAGES_N = int(os.getenv("HEAD_PAGES_N", "7")) + + +def ingest_from_net(user_id: str, book_id: str, sources: List[Dict[str, Any]]): + """ + Two-phase ingest: + Phase A (concurrent): fetch PDFs, extract text/OCR, preprocess, insert raw doc to DB. + Phase B (sequential): load embedder once and ingest pages (encode + upsert) to Qdrant. + This avoids loading the embedding model many times (huge slow-down). + """ + init_db() + if not check_db_health(): + raise RuntimeError("Supabase / DB is not reachable") + + collection_name = f"user_{user_id}__book_{book_id}" + + # Worker for phase A: fetch + extract + preprocess + insert_raw_document + def fetch_and_prepare(src: Dict[str, Any]) -> Dict[str, Any]: + url = src.get("url") + doc_id = str(uuid.uuid4()) + start = time.time() + result = {"url": url, "doc_id": doc_id, "status": "rejected", "reason": None} + + try: + pdf_bytes = get_pdf_bytes(url) + if not pdf_bytes: + result["reason"] = "no_pdf_or_blocked" + return result + + if len(pdf_bytes) < MIN_PDF_SIZE_KB * 1024: + result["reason"] = "pdf_too_small" + return result + + # delayed imports for optional OCR libs + from .pdf_text import extract_text_pypdf2, is_text_usable + from .ocr import mistral_ocr_pdf + + pages = extract_text_pypdf2(pdf_bytes) + joined = "\n".join(pages) + + if is_text_usable(joined): + extraction_method = "text" + else: + if len(pdf_bytes) > 10 * 1024 * 1024: + result["reason"] = "pdf_too_large_for_ocr" + return result + try: + pages = mistral_ocr_pdf(pdf_bytes) + extraction_method = "ocr" + except Exception as e: + result["reason"] = f"ocr_failed:{str(e)}" + return result + + if not pages: + result["reason"] = "no_text_extracted" + return result + + # preprocess + from .preprocess import normalize_arabic, drop_common_headers_footers + + language = src.get("language", "ar") + if language == "ar": + pages = [normalize_arabic(p) for p in pages] + pages = drop_common_headers_footers(pages) + + pages_head = pages[:HEAD_PAGES_N] + + raw_doc = DocRaw( + doc_id=doc_id, + user_id=user_id, + book_id=book_id, + source_url=url, + source_type=src.get("source_type", "pdf"), + domain=src.get("domain", ""), + language=language, + pages_head=pages_head, + extraction_method=extraction_method, + pdf_size_bytes=len(pdf_bytes), + status="pending", + error_reason="", + ) + + # insert raw doc (phase 1) + insert_raw_document(raw_doc) + + result.update( + { + "status": "prepared", + "doc_id": doc_id, + "raw_doc": raw_doc, + "pages": pages, + "extraction": extraction_method, + "duration": time.time() - start, + } + ) + return result + + except Exception as e: + result["reason"] = f"fetch_prepare_failed:{str(e)}" + return result + + # Phase A: concurrent fetch + prepare + prepared_items = [] + with ThreadPoolExecutor(max_workers=MAX_INGEST_WORKERS) as ex: + futures = {ex.submit(fetch_and_prepare, src): src for src in sources} + for fut in as_completed(futures): + res = fut.result() + prepared_items.append(res) + + # Phase B: sequential embedding & qdrant upsert with single embedder instance + from .rag_engine_sources import ArabicBookRAGWithSources + + rag = ArabicBookRAGWithSources( + user_id=user_id, book_id=book_id, embedding_model=EMBEDDING_MODEL + ) + + items_out = [] + ingested = 0 + rejected = 0 + + for item in prepared_items: + if item.get("status") != "prepared": + items_out.append(item) + rejected += 1 + continue + + doc_id = item["doc_id"] + pages = item["pages"] + raw_doc = item["raw_doc"] + try: + stats = rag.ingest_pages(pages=pages, raw_doc=raw_doc) + items_out.append( + { + "url": item["url"], + "status": "ingested", + "doc_id": doc_id, + "source_type": raw_doc.source_type, + "extraction": item["extraction"], + "stats": stats, + } + ) + ingested += 1 + except Exception as e: + # mark failed on db + try: + mark_raw_status(doc_id, "failed", f"qdrant_failed:{str(e)}") + except Exception: + pass + items_out.append( + { + "url": item["url"], + "status": "rejected", + "reason": f"qdrant_failed:{str(e)}", + "doc_id": doc_id, + } + ) + rejected += 1 + + return items_out, ingested, rejected, rag.collection diff --git a/rag/ocr.py b/rag/ocr.py new file mode 100644 index 0000000000000000000000000000000000000000..f1d3ae9fa235f58b7dba1b8313c65b87c48605b4 --- /dev/null +++ b/rag/ocr.py @@ -0,0 +1,40 @@ +import os +import base64 +from typing import List +from mistralai import Mistral + + +def _pdf_to_base64(pdf_path: str) -> str: + with open(pdf_path, "rb") as f: + return base64.b64encode(f.read()).decode("utf-8") + + +def mistral_ocr_pdf(pdf_bytes: bytes) -> List[str]: + """ + OCR PDF bytes directly (no filesystem). + """ + print("🚀 OCR START | Mistral (in-memory)") + + client = Mistral(api_key=os.environ["MISTRAL_API_KEY"]) + b64 = base64.b64encode(pdf_bytes).decode("utf-8") + + resp = client.ocr.process( + model="mistral-ocr-latest", + document={ + "type": "document_url", + "document_url": f"data:application/pdf;base64,{b64}", + }, + include_image_base64=False, + ) + + pages = [] + for page in resp.pages: + if getattr(page, "markdown", None): + pages.append(page.markdown) + elif getattr(page, "text", None): + pages.append(page.text) + else: + pages.append("") + + print(f"✅ OCR DONE | pages={len(pages)}") + return pages diff --git a/rag/pdf_text.py b/rag/pdf_text.py new file mode 100644 index 0000000000000000000000000000000000000000..617b8646326d8c14af8c70bf49e72dab76d6df6e --- /dev/null +++ b/rag/pdf_text.py @@ -0,0 +1,97 @@ +from typing import List +from io import BytesIO +import re + +from PyPDF2 import PdfReader +from PyPDF2.errors import DependencyError + + +def extract_text_pypdf2(pdf_bytes: bytes) -> List[str]: + pages: List[str] = [] + + try: + reader = PdfReader(BytesIO(pdf_bytes)) + except DependencyError: + # AES encryption without pycryptodome + return [] + + # لو PDF مشفر + if reader.is_encrypted: + try: + reader.decrypt("") # حاول password فاضي + except Exception: + return [] + + for page in reader.pages: + try: + text = page.extract_text() or "" + except Exception: + text = "" + + text = re.sub(r"\s+\n", "\n", text) + text = re.sub(r"\n\s+", "\n", text) + text = re.sub(r"[ \t]+", " ", text) + pages.append(text.strip()) + + return pages + + +ARABIC_RANGE = re.compile(r"[\u0600-\u06FF]") +LATIN_RANGE = re.compile(r"[A-Za-z]") + + +def is_text_usable(text: str, min_len: int = 300) -> bool: + if not text or len(text) < min_len: + return False + + total_len = len(text) + + arabic_chars = len(ARABIC_RANGE.findall(text)) + latin_chars = len(LATIN_RANGE.findall(text)) + spaces = text.count(" ") + + # نسبة حروف مفهومة + readable_ratio = (arabic_chars + latin_chars) / total_len + + # ------------------------- + # 1️⃣ Check المسافات + # ------------------------- + space_ratio = spaces / total_len + if space_ratio < 0.02: # أقل من 2% مسافات = حروف لازقة + return False + + # ------------------------- + # 2️⃣ تحليل الكلمات + # ------------------------- + words = [w for w in text.split() if len(w) > 1] + if len(words) < 20: + return False + + avg_word_len = sum(len(w) for w in words) / len(words) + + # متوسط كلمة مبالغ فيه + if avg_word_len > 12: + return False + + # ------------------------- + # 3️⃣ كلمات طويلة جدًا (OCR خداع) + # ------------------------- + long_words = [w for w in words if len(w) > 15] + long_ratio = len(long_words) / len(words) + + if long_ratio > 0.25: # ربع الكلمات طويلة جدًا + return False + + # ------------------------- + # 4️⃣ Checks اللغات (زي ما كانت) + # ------------------------- + if arabic_chars > 50: + return True + + if latin_chars > 100: + return True + + if readable_ratio < 0.2: + return False + + return True diff --git a/rag/preprocess.py b/rag/preprocess.py new file mode 100644 index 0000000000000000000000000000000000000000..518641252b9c8582d4d37d17f858a06f69ba5e14 --- /dev/null +++ b/rag/preprocess.py @@ -0,0 +1,63 @@ +import re +from typing import List +from rapidfuzz import fuzz + +# ========================= +# Arabic Utilities +# ========================= + +_AR_DIACRITICS = re.compile(r"[\u0617-\u061A\u064B-\u0652\u0670\u06D6-\u06ED]") +_AR_TATWEEL = "\u0640" + + +def normalize_arabic(text: str) -> str: + """ + Normalize Arabic text: + - remove tatweel + - remove diacritics + - normalize spaces + """ + if not text: + return "" + text = text.replace(_AR_TATWEEL, "") + text = _AR_DIACRITICS.sub("", text) + text = re.sub(r"\s+", " ", text).strip() + return text + + +def drop_common_headers_footers( + pages: List[str], min_similarity: int = 92 +) -> List[str]: + """ + Detect and remove repeated headers / footers across pages. + """ + if not pages: + return pages + + first_lines, last_lines = [], [] + + for p in pages: + lines = [l.strip() for l in p.splitlines() if l.strip()] + first_lines.append("\n".join(lines[:2]) if len(lines) >= 2 else "") + last_lines.append("\n".join(lines[-2:]) if len(lines) >= 2 else "") + + def detect(candidates: List[str]) -> str | None: + candidates = sorted([c for c in candidates if c], key=len, reverse=True) + if not candidates: + return None + base = candidates[0] + hits = sum(1 for c in candidates if fuzz.ratio(base, c) >= min_similarity) + return base if hits >= max(3, int(0.4 * len(candidates))) else None + + header = detect(first_lines) + footer = detect(last_lines) + + cleaned_pages = [] + for p in pages: + if header: + p = p.replace(header, "") + if footer: + p = p.replace(footer, "") + cleaned_pages.append(p.strip()) + + return cleaned_pages diff --git a/rag/rag_engine.py b/rag/rag_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..8e82a70f3614d1e94e8571aa61ac518ce6e996b6 --- /dev/null +++ b/rag/rag_engine.py @@ -0,0 +1,121 @@ +import os +import uuid +from dataclasses import asdict +from typing import List, Optional + +from sentence_transformers import SentenceTransformer +from qdrant_client import QdrantClient +from qdrant_client.http import models as qm + +from schemas import ChunkRecord +from .ocr import mistral_ocr_pdf +from .preprocess import normalize_arabic, drop_common_headers_footers +from .chuncking import chunk_pages + + +class ArabicBookRAG: + def __init__( + self, + collection_name: str, # 👈 مهم + embedding_model="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + ): + self.collection = collection_name + self.embedder = SentenceTransformer(embedding_model) + + self.qdrant = QdrantClient( + url=os.environ["QDRANT_URL"], + api_key=os.environ["QDRANT_API_KEY"], + ) + + self._ensure_collection() + + def _ensure_collection(self): + dim = self.embedder.get_sentence_embedding_dimension() + collections = [c.name for c in self.qdrant.get_collections().collections] + + if self.collection not in collections: + self.qdrant.create_collection( + collection_name=self.collection, + vectors_config=qm.VectorParams(size=dim, distance=qm.Distance.COSINE), + ) + + # ========================= + # Ingest PDF (In-Memory) + # ========================= + def ingest_pdf(self, pdf_bytes: bytes, meta): + pages = mistral_ocr_pdf(pdf_bytes) + + pages = [normalize_arabic(p) for p in pages] + pages = drop_common_headers_footers(pages) + + chunks = chunk_pages(pages) + + records = [ + ChunkRecord( + chunk_id=str(uuid.uuid4()), + doc_id=meta.doc_id, + page_start=ps, + page_end=pe, + text=txt, + title=meta.title, + author=meta.author, + year=meta.year, + ) + for txt, ps, pe in chunks + ] + + vectors = self.embedder.encode( + [r.text for r in records], normalize_embeddings=True + ) + + self.qdrant.upsert( + collection_name=self.collection, + points=[ + qm.PointStruct( + id=r.chunk_id, + vector=v.tolist(), + payload=asdict(r), + ) + for r, v in zip(records, vectors) + ], + ) + + return {"pages": len(pages), "chunks": len(records)} + + # ========================= + # Retrieve + # ========================= + def retrieve( + self, queries: List[str], doc_id: Optional[str] = None, top_k: int = 8 + ): + must = [] + + if doc_id: + must.append( + qm.FieldCondition(key="doc_id", match=qm.MatchValue(value=doc_id)) + ) + + flt = qm.Filter(must=must) if must else None + hits = [] + + for q in queries: + qn = normalize_arabic(q) + vec = self.embedder.encode([qn], normalize_embeddings=True)[0] + + res = self.qdrant.query_points( + collection_name=self.collection, + query=vec.tolist(), + limit=top_k, + with_payload=True, + query_filter=flt, + ).points + + hits.extend(res) + + return hits + + # ========================= + # Delete whole book + # ========================= + def delete_collection(self): + self.qdrant.delete_collection(self.collection) diff --git a/rag/rag_engine_sources.py b/rag/rag_engine_sources.py new file mode 100644 index 0000000000000000000000000000000000000000..96f022993dbfb35541e1d42577f84fc5dc664b21 --- /dev/null +++ b/rag/rag_engine_sources.py @@ -0,0 +1,182 @@ +# rag/rag_engine_sources.py +import os +import uuid +from typing import List, Optional +from dataclasses import asdict + +from sentence_transformers import SentenceTransformer +from qdrant_client import QdrantClient +from qdrant_client.http import models as qm + +from schemas.books.sources_schema import ChunkRecord, DocRaw +from .preprocess import normalize_arabic +from .chuncking import chunk_pages # صحّحت اسم الملف من chuncking -> chunking + + +class ArabicBookRAGWithSources: + def __init__( + self, user_id: str, book_id: str, embedding_model: str, batch_size: int = 128 + ): + self.user_id = user_id + self.book_id = book_id + self.collection = f"user_{user_id}__book_{book_id}" + self.batch_size = batch_size + + # load embedder once per instance (this is why ingest_from_net uses single instance) + self.embedder = SentenceTransformer(embedding_model) + self.qdrant = QdrantClient( + url=os.environ["QDRANT_URL"], + api_key=os.environ["QDRANT_API_KEY"], + ) + self._ensure_collection() + + def _ensure_collection(self): + dim = self.embedder.get_sentence_embedding_dimension() + existing = self.qdrant.get_collections() + collections = ( + [c.name for c in existing.collections] + if existing and getattr(existing, "collections", None) + else [] + ) + if self.collection not in collections: + self.qdrant.create_collection( + collection_name=self.collection, + vectors_config=qm.VectorParams(size=dim, distance=qm.Distance.COSINE), + ) + + self.qdrant.create_payload_index( + collection_name=self.collection, + field_name="doc_id", + field_schema=qm.PayloadSchemaType.KEYWORD, + ) + + def ingest_pages(self, pages: List[str], raw_doc: DocRaw): + """ + Create chunks (with page ranges), embed in batches, and upsert to Qdrant in batches. + Returns stats dict. + """ + chunks = chunk_pages(pages) + + records = [] + for txt, ps, pe in chunks: + records.append( + ChunkRecord( + chunk_id=str(uuid.uuid4()), + user_id=self.user_id, + book_id=self.book_id, + doc_id=raw_doc.doc_id, + source_url=raw_doc.source_url, + source_type=raw_doc.source_type, + domain=raw_doc.domain, + title="", + authors="", + year=None, + publisher_or_journal="", + language=raw_doc.language, + apa7="", + page_start=ps, + page_end=pe, + text=txt, + ) + ) + + # encode in batches to save memory/time + vectors = [] + texts = [r.text for r in records] + for i in range(0, len(texts), self.batch_size): + batch_texts = texts[i : i + self.batch_size] + batch_vecs = self.embedder.encode(batch_texts, normalize_embeddings=True) + vectors.extend(batch_vecs) + + # upsert to Qdrant in batches + points = [] + for r, v in zip(records, vectors): + points.append( + qm.PointStruct( + id=r.chunk_id, + vector=v.tolist(), + payload={ + "chunk_id": r.chunk_id, + "user_id": r.user_id, + "book_id": r.book_id, + "doc_id": r.doc_id, + "source_url": r.source_url, + "source_type": r.source_type, + "domain": r.domain, + "language": r.language, + "page_start": r.page_start, + "page_end": r.page_end, + "text": r.text, + }, + ) + ) + + for i in range(0, len(points), self.batch_size): + batch = points[i : i + self.batch_size] + self.qdrant.upsert(collection_name=self.collection, points=batch) + + return {"pages": len(pages), "chunks": len(records)} + + def retrieve( + self, + queries: List[str], + doc_id: Optional[str] = None, + top_k: int = 8, + ): + if not queries: + return [] + + must = [] + if doc_id: + must.append( + qm.FieldCondition( + key="doc_id", + match=qm.MatchValue(value=doc_id), + ) + ) + + query_filter = qm.Filter(must=must) if must else None + + hits = [] + + for q in queries: + q = q.strip() + if not q: + continue + + q_norm = normalize_arabic(q) + vec = self.embedder.encode([q_norm], normalize_embeddings=True)[0] + + # use query_points (or search depending on client version) + res = self.qdrant.query_points( + collection_name=self.collection, + query=vec.tolist(), + limit=top_k, + with_payload=True, + query_filter=query_filter, + ).points + + hits.extend(res) + + return hits + + def delete_book_collection(self): + self.qdrant.delete_collection(self.collection) + + def delete_from_qdrant(self, doc_id: str): + try: + self.qdrant.delete( + collection_name=self.collection, + points_selector=qm.Filter( + must=[ + qm.FieldCondition( + key="doc_id", + match=qm.MatchValue(value=doc_id), + ) + ] + ), + ) + print(f"🗑️ Deleted from Qdrant: {doc_id}") + + except Exception as e: + print(f"❌ Qdrant delete error {doc_id}: {e}") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..6ed20dc86221e561882ff2a8df5be2825472029c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,37 @@ +crewai[tools] +PyPDF2 +reportlab +langchain_openai +litellm +# litellm[proxy] +openai >= 1.13.3 +fastapi +crewai[anthropic] +crewai[google-genai] +langchain +langchain_core +langchain-community +langchain-openrouter +# crawlee==1.1.0 +fitz +apify-fingerprint-datapoints +# browserforge +frontend +parsel +mistralai +# base64 +sentence-transformers +qdrant-client +pymupdf +rapidfuzz +supabase +crawlee==0.3.6 +browserforge==1.1.2 +pycryptodome +apscheduler +pydantic +pydantic[email] +fastapi-sso +gunicorn +uvicorn[standard] +markdown \ No newline at end of file diff --git a/routers/books/book_content.py b/routers/books/book_content.py new file mode 100644 index 0000000000000000000000000000000000000000..e5b63c011f023b916ac680bd43332c14cb367b51 --- /dev/null +++ b/routers/books/book_content.py @@ -0,0 +1,63 @@ +import os +from crewai import Crew, Process +import json +from fastapi import APIRouter, HTTPException, Depends, Body +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any +from core.books.cohesion_service import run_cohesion_pipeline +from fastapi.responses import HTMLResponse +import app +from core import process_book, convert_dict_to_html + + +router = APIRouter(prefix="/content", tags=["Books"]) + +@router.post("/book_content") +def run_content_generation(book_data: Dict): + # نحول لـ dict ونبعتها للـ service + book_dict = book_data + result = process_book(data=book_dict) + + return result + + +class CohesionRunRequest(BaseModel): + raw_book_json: Dict[str, Any] = Field(..., description="JSON الكتاب الخام بالكامل") + chunk_size: int = Field(3, ge=1, le=5, description="عدد الفصول في كل دفعة") + run_id: Optional[str] = Field(None, description="معرّف تشغيل اختياري") + + +@router.post("/cohesion") +def run_cohesion(req: CohesionRunRequest): + try: + result = run_cohesion_pipeline( + raw_book_json=req.raw_book_json, + chunk_size=req.chunk_size, + run_id=req.run_id, + ) + return { + "ok": True, + "run_id": result["run_id"], + "final_narrative_memory": result["final_narrative_memory"], + "final_edited_book": result["final_edited_book"], + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/convert", response_class=HTMLResponse) +def convert_to_html(course_data: Dict[str, Any] = Body(...)): + """ + تحويل بيانات المقرر/الكتاب من Dictionary إلى HTML + """ + try: + if not course_data: + raise HTTPException(status_code=400, detail="لا توجد بيانات مرسلة") + + html_content = convert_dict_to_html(course_data) + return HTMLResponse(content=html_content, status_code=200) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"خطأ في التحويل: {str(e)}") diff --git a/routers/books/book_enrichment.py b/routers/books/book_enrichment.py new file mode 100644 index 0000000000000000000000000000000000000000..681f1e155e358fdc0fb8f7dc16e97bf19a7f1846 --- /dev/null +++ b/routers/books/book_enrichment.py @@ -0,0 +1,118 @@ +import json +from crewai import Crew, Process +from agents.books.enrichment import make_enrichment_crew +from modules import enrichment_examples +from fastapi import APIRouter, HTTPException, Body +from fastapi.responses import HTMLResponse +from typing import Optional, Dict, Any +from core.books.convert_enrichment_to_html import render_html + + +router = APIRouter(prefix="/content", tags=["Books"]) + + +@router.post("/book-enrichment") +def run_book_enrichment(book_data: dict): + + book = book_data + + for chapter in book.get("chapters", []): + + for section in chapter.get("sections", []): + crew = make_enrichment_crew() + plans = section.get("plans", {}) + + for plan_title, plan_data in plans.items(): + + content = plan_data.get("generated_content", "") + + if not content.strip(): + continue + + print("Processing:", plan_title) + + inputs = { + "generated_content": content, + "target_audiance": book.get("target_audience", ""), + "ref1": enrichment_examples["example3"], + "ref2": enrichment_examples["example4"], + "ref3": enrichment_examples["example1"], + "ref5": enrichment_examples["example2"], + } + + try: + + result = crew.kickoff(inputs=inputs) + + raw_results = None + + if hasattr(result, "json_dict") and result.json_dict: + raw_results = result.json_dict + else: + try: + raw_results = json.loads(result.raw) + except: + raw_results = None + + enriched_content = "" + enriched_meta = [] + + if isinstance(raw_results, dict): + + enriched_content = ( + raw_results.get("enriched_content") + or raw_results.get("content") + or raw_results.get("content_after_enrichment") + or "" + ) + enriched_meta = ( + raw_results.get("meta") or raw_results.get("results") or [] + ) + + elif isinstance(raw_results, list) and len(raw_results) >= 2: + + enriched_content = raw_results[0] + enriched_meta = raw_results[1] + + plan_data["generated_content_reviewed"] = ( + enriched_content.strip() if enriched_content else content + ) + + plan_data["enrichment_meta"] = ( + [ + m.model_dump() if hasattr(m, "model_dump") else m + for m in enriched_meta + ] + if isinstance(enriched_meta, list) + else [] + ) + + except Exception as e: + + plan_data["generated_content_reviewed"] = content + plan_data["enrichment_meta"] = [] + plan_data["enrichment_error"] = str(e) + print("✅ enrichment finished") + + return { + "message": "Book Enrichment Generated Successfully 🚀", + "result": book, + } + + +@router.post("/convert_enrichment", response_class=HTMLResponse) +def convert_to_html(course_data: Dict[str, Any] = Body(...)): + """ + تحويل بيانات المقرر/الكتاب من Dictionary إلى HTML + """ + try: + if not course_data: + raise HTTPException(status_code=400, detail="لا توجد بيانات مرسلة") + + html_content = render_html(course_data) + return HTMLResponse(content=html_content, status_code=200) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"خطأ في التحويل: {str(e)}") diff --git a/routers/books/book_planning_route.py b/routers/books/book_planning_route.py new file mode 100644 index 0000000000000000000000000000000000000000..05a3a5f257c493aab1c38b5338e42e340894965b --- /dev/null +++ b/routers/books/book_planning_route.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter +from schemas import BookOutlineOutput +from core.books.planning_services import run_planning_on_outline + +router = APIRouter(prefix="/content", tags=["Books"]) + + +# ------------------------------------ +# === Book Planning (Chapter-based) +# ------------------------------------ +@router.post("/book-planning") +def run_book_planning(data: BookOutlineOutput): + """ + Input : BookOutlineOutput + Output : BookPlanningOutput (Chapter Intro + Style + Plans) + """ + + book_data = data.dict() + + result = run_planning_on_outline(book_outline=book_data) + + result_dict = result.get("book", {}) + tokens = result.get("tokens", []) + + total_tokens_sum = 0 + prompt_tokens_sum = 0 + cached_prompt_tokens_sum = 0 + completion_tokens_sum = 0 + successful_requests_sum = 0 + + for item in tokens: + total_tokens_sum += item.get("total_tokens", 0) + prompt_tokens_sum += item.get("prompt_tokens", 0) + cached_prompt_tokens_sum += item.get("cached_prompt_tokens", 0) + completion_tokens_sum += item.get("completion_tokens", 0) + successful_requests_sum += item.get("successful_requests", 0) + + return { + "message": "Book Planning Generated Successfully 🚀", + "total_tokens_sum": total_tokens_sum, + "prompt_tokens_sum": prompt_tokens_sum, + "cached_prompt_tokens_sum": cached_prompt_tokens_sum, + "completion_tokens_sum": completion_tokens_sum, + "successful_requests_sum": successful_requests_sum, + "result": result_dict, + } diff --git a/routers/books/book_structure_route.py b/routers/books/book_structure_route.py new file mode 100644 index 0000000000000000000000000000000000000000..fe2f04170f957696eca14bdfc9da796740abfe04 --- /dev/null +++ b/routers/books/book_structure_route.py @@ -0,0 +1,68 @@ +import os +from crewai import Crew, Process +import json +from fastapi import APIRouter, HTTPException, Depends +from agents.books import ( + book_structure_agent, + generate_book_outline_task, +) +from modules import ( + llm, + inputs, +) +from schemas import ( + CourseSchema +) + + +router = APIRouter(prefix="/structure", tags=["Books"]) + +# ------------------------------------ +# === Agent 7:Training Generator === +# ------------------------------------ + + +@router.post("/book-outline") +def run_book_outline( + data: CourseSchema, # زي OutlineInput + total_pages: int | None = None, +): + agent = book_structure_agent() + task = generate_book_outline_task(agent) + # 1️⃣ Crew + structure_crew = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + memory=False, + verbose=True, + ) + + # 2️⃣ inputs من الـ request + book_data = data.dict() + + inputs = { + "book_title": book_data.get("course_name") or book_data.get("book_title") or "", + "target_audience": book_data.get("course_audience") + or book_data.get("target_audience") + or "", + "book_description": book_data.get("course_description") + or book_data.get("book_description") + or "", + "learning_outcomes": book_data.get("learning_outcomes") or [], + "raw_units": book_data.get("units") or book_data.get("raw_units") or [], + "target_total_pages": book_data.get("total_pages") or total_pages or 0, + "example_reference": book_data.get("example_reference") or "", + } + + # 4️⃣ Run Crew + result = structure_crew.kickoff(inputs=inputs) + + print(result.json_dict) + + # 5️⃣ Response + return { + "message": "Book Outline Generated Successfully 🚀", + "result": result.json_dict, + "tokens": result.token_usage, + } diff --git a/routers/books/queries_route.py b/routers/books/queries_route.py new file mode 100644 index 0000000000000000000000000000000000000000..28583ee92ef3ed62d13788a78467c213c0be7a4b --- /dev/null +++ b/routers/books/queries_route.py @@ -0,0 +1,39 @@ +import os +from crewai import Crew, Process +import json +from fastapi import APIRouter, HTTPException, Depends +from agents.books import ( + query_agent, + query_task, +) + +from schemas import ( + BookOutlineOutput, +) + + +router = APIRouter(prefix="/content", tags=["Books"]) + +# ------------------------------------ +# === Agent 7:Training Generator === +# ------------------------------------ + + +@router.post("/queries") +def run_queries(data: BookOutlineOutput): + agent=query_agent() + task=query_task(agent) + queries_crew = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + ) + + book_data = data.dict() + user_inputs = { + "book": book_data, + } + + result = queries_crew.kickoff(inputs=user_inputs) + + return {"message": "Queries Generated Well 🚀", "result": result} diff --git a/routers/books/rag_admin.py b/routers/books/rag_admin.py new file mode 100644 index 0000000000000000000000000000000000000000..87c2489826d678b1e284c3ba6fa198adcd8b6199 --- /dev/null +++ b/routers/books/rag_admin.py @@ -0,0 +1,71 @@ +from fastapi import APIRouter +from qdrant_client import QdrantClient +import os + +router = APIRouter(prefix="/rag", tags=["RAG Admin"]) + + +@router.get("/collections") +def list_collections(): + client = QdrantClient( + url=os.environ["QDRANT_URL"], + api_key=os.environ["QDRANT_API_KEY"], + ) + + collections = client.get_collections().collections + + return { + "count": len(collections), + "collections": [ + { + "name": c.name, + } + for c in collections + ], + } + + +@router.get("/collections/{book_id}") +def get_collection_info(book_id: str): + collection_name = f"book_{book_id}" + + client = QdrantClient( + url=os.environ["QDRANT_URL"], + api_key=os.environ["QDRANT_API_KEY"], + ) + + try: + info = client.get_collection(collection_name) + count = client.count(collection_name=collection_name, exact=True) + + return { + "collection": collection_name, + "status": info.status, + "vectors_count": count.count, + } + + except Exception: + return { + "collection": collection_name, + "exists": False, + "message": "Collection not found", + } + + +@router.get("/books") +def list_books(): + client = QdrantClient( + url=os.environ["QDRANT_URL"], + api_key=os.environ["QDRANT_API_KEY"], + ) + + collections = client.get_collections().collections + + books = [ + c.name.replace("book_", "") for c in collections if c.name.startswith("book_") + ] + + return { + "books_count": len(books), + "books": books, + } diff --git a/routers/books/rag_automation_route.py b/routers/books/rag_automation_route.py new file mode 100644 index 0000000000000000000000000000000000000000..91aedf82825601ebc3655e67f7c2df3a20c36df0 --- /dev/null +++ b/routers/books/rag_automation_route.py @@ -0,0 +1,87 @@ +from fastapi import APIRouter +from typing import List +from rag.rag_engine import ArabicBookRAG +from rag.rag_engine_sources import ArabicBookRAGWithSources +from rag.automation import run_rag_automation_on_book +from rag.automation_sources import run_rag_automation_on_book_sources +import os + +router = APIRouter(prefix="/rag", tags=["Books"]) + + +@router.post("/automation") +def run_book_rag( + planned_book: dict, + source_books: List[str], # 👈 book_ids + top_k: int = 5, +): + # create RAG engine per source book + rag_engines = [ + ArabicBookRAG(collection_name=f"book_{book_id}") + for book_id in source_books + ] + + enriched_book = run_rag_automation_on_book( + planned_book=planned_book, + rag_engines=rag_engines, + top_k=top_k, + ) + + return enriched_book + + +from typing import Optional +from rag.automation_metadata_new import process_user_metadata + + +@router.post("/metadata/run") +def run_metadata(user_id: str, book_id: Optional[str] = None): + result = process_user_metadata(user_id=user_id, book_id=book_id) + tokens=result.get("tokens", []) + total_tokens_sum = 0 + prompt_tokens_sum = 0 + cached_prompt_tokens_sum = 0 + completion_tokens_sum = 0 + successful_requests_sum = 0 + + for item in tokens: + total_tokens_sum += item.get("total_tokens", 0) + prompt_tokens_sum += item.get("prompt_tokens", 0) + cached_prompt_tokens_sum += item.get("cached_prompt_tokens", 0) + completion_tokens_sum += item.get("completion_tokens", 0) + successful_requests_sum += item.get("successful_requests", 0) + result["token_usage"] = { + "total_tokens": total_tokens_sum, + "prompt_tokens": prompt_tokens_sum, + "cached_prompt_tokens": cached_prompt_tokens_sum, + "completion_tokens": completion_tokens_sum, + "successful_requests": successful_requests_sum, + } + result.pop("tokens", None) # remove individual token entries if not needed + return result + + +@router.post("/automation_sources") +def run_book_rag( + planned_book: dict, + user_id: str, + source_books: List[str], # book_ids + embedding_model: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", + top_k: int = 5, +): + rag_engines = [ + ArabicBookRAGWithSources( + user_id=user_id, + book_id=book_id, + embedding_model=embedding_model, + ) + for book_id in source_books + ] + + enriched_book = run_rag_automation_on_book_sources( + planned_book=planned_book, + rag_engines=rag_engines, + top_k=top_k, + ) + + return enriched_book diff --git a/routers/books/rag_route.py b/routers/books/rag_route.py new file mode 100644 index 0000000000000000000000000000000000000000..203c1ef354278b543c3bc4c0a789b064ab4e398e --- /dev/null +++ b/routers/books/rag_route.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, UploadFile, File, Form +import uuid +from schemas import DocMeta +from rag.rag_engine import ArabicBookRAG + +router = APIRouter(prefix="/rag", tags=["Books"]) + + +# ========================= +# Ingest PDF into Book Collection +# ========================= +@router.post("/pdf") +async def ingest_pdf( + pdf: UploadFile = File(...), + book_id: str = Form(...), # 👈 هو ده المفتاح + title: str = Form(...), + author: str = Form(""), + year: int = Form(0), +): + doc_id = str(uuid.uuid4()) + collection_name = f"book_{book_id}" + + pdf_bytes = await pdf.read() + + meta = DocMeta(doc_id=doc_id, title=title, author=author, year=year) + + rag = ArabicBookRAG(collection_name=collection_name) + stats = rag.ingest_pdf(pdf_bytes=pdf_bytes, meta=meta) + + return { + "book_id": book_id, + "doc_id": doc_id, + "collection": collection_name, + "stats": stats, + } + + +# ========================= +# Retrieve from Book +# ========================= +@router.post("/retrieve") +def retrieve( + queries: list[str], + book_id: str, + doc_id: str | None = None, +): + collection_name = f"book_{book_id}" + rag = ArabicBookRAG(collection_name=collection_name) + + return rag.retrieve(queries=queries, doc_id=doc_id) + + +# ========================= +# Delete Book Collection +# ========================= +@router.delete("/book") +def delete_book(book_id: str): + collection_name = f"book_{book_id}" + rag = ArabicBookRAG(collection_name=collection_name) + rag.delete_collection() + + return {"status": "deleted", "book_id": book_id, "collection": collection_name} diff --git a/routers/books/rag_sources.py b/routers/books/rag_sources.py new file mode 100644 index 0000000000000000000000000000000000000000..11b191b8c7fa9b1b6f368b3c322df9a437ad3265 --- /dev/null +++ b/routers/books/rag_sources.py @@ -0,0 +1,51 @@ +from fastapi import FastAPI, APIRouter +import os +from schemas.books.sources_schema import IngestFromNetRequest, IngestFromNetResponse +from rag.ingest_net import ingest_from_net +from rag.rag_engine_sources import ArabicBookRAGWithSources +from core.books.storage import init_db, delete_book_docs +# from config import EMBEDDING_MODEL +EMBEDDING_MODEL = os.getenv( + "EMBEDDING_MODEL", "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" +) + +router = APIRouter(prefix="/rag", tags=["Books"]) + + +@router.on_event("startup") +def _startup(): + init_db() + + +@router.post("/net/ingest", response_model=IngestFromNetResponse) +def ingest_net(req: IngestFromNetRequest): + items, ingested, rejected, collection = ingest_from_net( + user_id=req.user_id, + book_id=req.book_id, + sources=[s.model_dump() for s in req.sources], + ) + + return IngestFromNetResponse( + user_id=req.user_id, + book_id=req.book_id, + collection=collection, + total_sources=len(req.sources), + ingested=ingested, + rejected=rejected, + items=items, + ) + + +@router.delete("/net/book") +def delete_book(user_id: str, book_id: str): + rag = ArabicBookRAGWithSources( + user_id=user_id, book_id=book_id, embedding_model=EMBEDDING_MODEL + ) + rag.delete_book_collection() + delete_book_docs(user_id=user_id, book_id=book_id) + return { + "status": "deleted", + "user_id": user_id, + "book_id": book_id, + "collection": rag.collection, + } diff --git a/routers/books/reference.py b/routers/books/reference.py new file mode 100644 index 0000000000000000000000000000000000000000..d6b37a55c66bbc0863a9127c02534a9a7c80e7a5 --- /dev/null +++ b/routers/books/reference.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, HTTPException +from fastapi.responses import HTMLResponse +from agents.books.reference_lang import ( + run_references, +) + +router = APIRouter(prefix="/book", tags=["Books"]) + + +@router.post("/reference") +async def process_book( + book_json: dict , +): + try: + if not book_json: + raise HTTPException(status_code=400, detail="لا توجد بيانات مرسلة") + + html_content = run_references(book_json) + return HTMLResponse(content=html_content, status_code=200) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"خطأ في التحويل: {str(e)}") diff --git a/routers/books/search_route.py b/routers/books/search_route.py new file mode 100644 index 0000000000000000000000000000000000000000..d7f4f169b0eb01397ede760868da22bfd1f437f2 --- /dev/null +++ b/routers/books/search_route.py @@ -0,0 +1,27 @@ +import os +import json +from fastapi import APIRouter +from core.books.search_service import run_search_pipeline_parallel +from schemas.books.queries_schema import ( + QueriesOutput, +) + + +router = APIRouter(prefix="/content", tags=["Books"]) + +# ------------------------------------ +# === Agent 7:Training Generator === +# ------------------------------------ + + +@router.post("/search") +def run_search( + data: QueriesOutput, +): + + book_data = data.dict() + result = run_search_pipeline_parallel( + book_data=book_data, + ) + + return result diff --git a/routers/books/units_intro.py b/routers/books/units_intro.py new file mode 100644 index 0000000000000000000000000000000000000000..5d2a98875898a0051cdfb544c13657ca46f48411 --- /dev/null +++ b/routers/books/units_intro.py @@ -0,0 +1,103 @@ +import os +from crewai import Crew, Process +import json +from fastapi import APIRouter, HTTPException, Depends, Body +from agents.books import ( + intro_generator, + intro_generation_task, + intro_style_planner, + intro_style_planner_task +) +from typing import Optional, Dict, Any + +from fastapi.responses import HTMLResponse +from schemas import ( + BookPlanningOutput, +) +from modules import html_template_intro + + +router = APIRouter(prefix="/units_intro", tags=["Books"]) + +# ------------------------------------ +# === Agent 7:Training Generator === +# ------------------------------------ + + +@router.post("") +def run_intro_generation(data: BookPlanningOutput): + style_planner_agent = intro_style_planner() + style_planner_task = intro_style_planner_task(style_planner_agent) + intro_agent = intro_generator() + intro_task = intro_generation_task(intro_agent) + style_crew = Crew( + agents=[style_planner_agent], + tasks=[style_planner_task], + process=Process.sequential, + ) + intro_crew = Crew( + agents=[intro_agent], + tasks=[intro_task], + process=Process.sequential, + ) + + book_data = data.dict() + user_inputs = { + "book_plan": book_data, + } + + result = style_crew.kickoff(inputs=user_inputs) + intro_style=result.json_dict + + book_metadata = book_data.get("chapters", []) + intro_styles = intro_style.get("chapters", []) + all_outputs = [] + + # ------------------------- + # LOOP PER CHAPTER + # ------------------------- + for chapter_meta, style_meta in zip(book_metadata, intro_styles): + + print(f"✍️ Generating intro for: {chapter_meta['chapter_title']}") + print(f"chapter_meta {chapter_meta}\n") + chapter_meta.pop("style_profile") + chapter_meta['chapter_intro'].pop("hook_type") + chapter_meta['chapter_intro'].pop("hook_idea") + single_input = { + "chapter_metadata": chapter_meta, + "intro_style": style_meta + } + + result = intro_crew.kickoff(inputs=single_input) + + all_outputs.append({ + "chapter_name": chapter_meta["chapter_title"], + "intro": result["intro"] + }) + + # ------------------------- + # Save combined output + # ------------------------- + final_output = {"chapters": all_outputs} + + return {"message": "Queries Generated Well 🚀", "result": final_output} + + +@router.post("/convert", response_class=HTMLResponse) +def convert_to_html(intros: Dict[str, Any] = Body(...)): + """ + تحويل بيانات المقرر/الكتاب من Dictionary إلى HTML + """ + try: + if not intros: + raise HTTPException(status_code=400, detail="لا توجد بيانات مرسلة") + intros_list = intros.get("chapters", []) + json_str=json.dumps(intros_list, ensure_ascii=False, indent=2) + html_content = html_template_intro.replace("DATA_PLACEHOLDER", json_str) + + return HTMLResponse(content=html_content, status_code=200) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"خطأ في التحويل: {str(e)}") diff --git a/routers/books/units_structure_route.py b/routers/books/units_structure_route.py new file mode 100644 index 0000000000000000000000000000000000000000..a856e58c643ee2fa3a7c199dddf93db753c0dd2a --- /dev/null +++ b/routers/books/units_structure_route.py @@ -0,0 +1,81 @@ +from fastapi import APIRouter, UploadFile, File, Form +from crewai import Crew, Process +from tools import extract_text_from_pdf +import tempfile, os +from agents.books import ( + course_structuring_agent, + course_structuring_task, + book_units_langchain_agent, +) + +router = APIRouter(prefix="/structure", tags=["Books"]) + + +@router.post("/units_structure") +async def process_book( + pdf_1: UploadFile = File(...), + pdf_2: UploadFile = File(None), # 👈 ملف إضافي اختياري + course_name: str = Form(""), + course_audience: str = Form(""), + course_description: str = Form(""), + learning_outcomes: str = Form(""), # 👈 جديد +): + texts = [] + + for pdf in [pdf_1, pdf_2]: + if pdf is None: + continue + + with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp: + tmp.write(await pdf.read()) + pdf_path = tmp.name + + text = extract_text_from_pdf(pdf_path) + texts.append(text) + + os.remove(pdf_path) + + merged_text = "\n\n".join(texts) + agent = course_structuring_agent() + task = course_structuring_task(agent) + crew = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + verbose=False, + ) + + result = crew.kickoff( + inputs={ + "merged_book_text": merged_text, + "course_name": course_name, + "course_audience": course_audience, + "course_description": course_description, + "learning_outcomes": learning_outcomes, + } + ) + + return { + "message": "Course Structured Successfully 🚀", + "result": result, + } + + +@router.post("/auto_units_structure") +def run_auto_units_structure( + topic: str = Form(...), + units_number: int = Form(...), + notes: str = Form(None), + course_description: str = Form(None), + course_audience: str = Form(None), +): + + units = book_units_langchain_agent( + topic=topic, + units_number=units_number, + notes=notes, + course_description=course_description, + course_audience=course_audience, + ) + + return {"message": "Units Structured Successfully 🚀", "units": units} diff --git a/routers/content_generation_route.py b/routers/content_generation_route.py new file mode 100644 index 0000000000000000000000000000000000000000..3a174c4a525206382a4575a85b662776a9a2392b --- /dev/null +++ b/routers/content_generation_route.py @@ -0,0 +1,123 @@ +import os +from crewai import Crew, Process +import json +from typing import Dict, Any +from fastapi import APIRouter, HTTPException, Depends +from fastapi.responses import HTMLResponse +from agents.content_phase import ( + query_agent, + query_task, + course_writer_agent, + course_writer_task, +) +import app +from core import generate_course_content, convert_dict_to_html +from modules import ( + llm, + inputs, +) +from tools import scrape_course, SerperExhaustedError,CURRENT_SERPER_INDEX +from schemas import ( + CourseWithQueries, + CourseContent, +) + + +router = APIRouter(prefix="/content", tags=["Content Generation"]) + +# ------------------------------------ +# === Agent 7:Training Generator === +# ------------------------------------ + + +@router.post("/keyword_generation") +def run_keywords(outlines: CourseContent): + agent = query_agent() + task = query_task(agent) + keywords_crew = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + ) + + result = keywords_crew.kickoff(inputs={"course": outlines.dict()}) + print(result.json_dict) + + return {"message": "Keywords Generated Well 🚀", "result": result} + + +# @router.post("/scrape") +# def scrape_endpoint(course:CourseWithQueries): +# try: +# # نحول Pydantic model لـ dict بنفس الشكل اللي الفانكشن مستنياها +# course_dict = course.dict() +# summary, raw = scrape_course(course_dict) +# return { +# "message": "Queries Searched and Scraped Well 🚀", +# "summary": summary, +# "raw": raw, +# } + +# return {"message": "Queries Searched and Scraped Well 🚀", "summary": summary, "raw": raw} +# except Exception as e: +# # في البروداكشن خليه logging و متطلعش الرسالة الخام للعميل +# raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/scrape") +def scrape_endpoint(course: CourseWithQueries): + try: + course_dict = course.dict() + summary, raw = scrape_course(course_dict) + + return { + "message": "Queries Searched and Scraped Well 🚀", + "serper_api_used_index": CURRENT_SERPER_INDEX, + "summary": summary, + "raw": raw, + } + + except SerperExhaustedError as e: + raise HTTPException(status_code=500, detail=f"{str(e)}") + + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}") + + + +@router.post("/content_generation") +def run_content_generation(course_data: Dict): + # نحول لـ dict ونبعتها للـ service + course_dict = course_data + result = generate_course_content(course_dict) + + return {"message": "Content Generated Well 🚀", "result": result} + + +@router.post("/convert", response_class=HTMLResponse) +def convert_to_html(course_data: Dict[str, Any]): + """ + تحويل بيانات المقرر الدراسي من Dictionary إلى HTML + + Args: + course_data: Dictionary يحتوي على بيانات المقرر الدراسي + - course_name: اسم المقرر + - course_audience: الجمهور المستهدف + - results: قائمة الوحدات + + Returns: + HTML string كامل جاهز للعرض + """ + try: + # التحقق من وجود البيانات الأساسية + if not course_data: + raise HTTPException(status_code=400, detail="لا توجد بيانات مرسلة") + + # تحويل Dictionary إلى HTML + html_content = convert_dict_to_html(course_data) + + return HTMLResponse(content=html_content) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"خطأ في التحويل: {str(e)}") diff --git a/routers/details_before_content_route.py b/routers/details_before_content_route.py new file mode 100644 index 0000000000000000000000000000000000000000..aa75afb6c1c4bab9dfcc07b5a7a986801243fba1 --- /dev/null +++ b/routers/details_before_content_route.py @@ -0,0 +1,131 @@ +from fastapi import APIRouter, HTTPException +from fastapi.responses import HTMLResponse +from core.details_before_content_converter import render_course_html +from crewai import Crew, Process +from agents.content_phase import ( + units_details_agent, + units_details_task, + unit_intro_agent, + unit_intro_task, + units_eval_details_agent, + units_eval_details_task, +) +from modules import ( + example_reference2, + example_reference3, + example_reference4, +) + +router = APIRouter(prefix="/analysis", tags=["Unit Details Before Content"]) + + +@router.post("/generate-course-details") +def generate_course_details(course_data: dict): + try: + # =============================== + # Crew 1 – Units Details + # =============================== + details_agent=units_details_agent() + details_task=units_details_task(details_agent) + units_details_crew = Crew( + agents=[details_agent], + tasks=[details_task], + process=Process.sequential, + verbose=True, + ) + + units_details_result = units_details_crew.kickoff( + inputs={ + "course_name": course_data["course_name"], + "course_audience": course_data["course_audience"], + "units": course_data["expanded_units"], + "example_reference": example_reference2, + } + ) + + units_details_output = units_details_result.json_dict + + # =============================== + # Crew 2 – Unit Introductions + # =============================== + intro_agent=unit_intro_agent() + intro_task=unit_intro_task(intro_agent) + unit_intro_crew = Crew( + agents=[intro_agent], + tasks=[intro_task], + process=Process.sequential, + verbose=True, + ) + + unit_intro_result = unit_intro_crew.kickoff( + inputs={ + "course_name": course_data["course_name"], + "course_audience": course_data["course_audience"], + "units": course_data["expanded_units"], + "example_reference": example_reference3, + } + ) + + unit_intro_output = unit_intro_result.json_dict + + # =============================== + # Crew 3 – Units Evaluation Details + # =============================== + eval_details_agent=units_eval_details_agent() + eval_details_task=units_eval_details_task(eval_details_agent) + units_eval_details_crew = Crew( + agents=[eval_details_agent], + tasks=[eval_details_task], + process=Process.sequential, + verbose=True, + ) + + units_eval_details_result = units_eval_details_crew.kickoff( + inputs={ + "course_name": course_data["course_name"], + "course_audience": course_data["course_audience"], + "units": course_data["expanded_units"], + "example_reference": example_reference4, + } + ) + + units_eval_details_output = units_eval_details_result.json_dict + + # =============================== + # Final Response + # =============================== + return { + "message": "Course units generated successfully 🚀", + "units_details": units_details_output, + "unit_introductions": unit_intro_output, + "units_evaluation": units_eval_details_output, + } + + except KeyError as e: + raise HTTPException(status_code=400, detail=f"Missing required field: {str(e)}") + + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Unexpected error occurred: {str(e)}" + ) + + +@router.post("/render-course-html", response_class=HTMLResponse) +def render_course_html_api(payload: dict): + + try: + + html = render_course_html( + course=payload["course"], + introductions=payload["introductions"], + evaluations=payload["evaluations"], + # template=template, + ) + + return HTMLResponse(content=html) + + except KeyError as e: + raise HTTPException(400, f"Missing field: {str(e)}") + + except Exception as e: + raise HTTPException(500, str(e)) diff --git a/routers/final_outlines_before_content_route.py b/routers/final_outlines_before_content_route.py new file mode 100644 index 0000000000000000000000000000000000000000..278c59cf85b6d2e08b5450b4e437ff596dddf228 --- /dev/null +++ b/routers/final_outlines_before_content_route.py @@ -0,0 +1,86 @@ +import os +from crewai import Crew, Process +import json +from typing import Optional, Dict, Any +from fastapi import APIRouter, HTTPException, Depends +from agents.content_phase import ( + curriculum_designer, + generate_course_structure_task, + course_structurer_agent, + transform_plan_to_course_task, +) +from modules import ( + llm, + inputs, + example_reference, +) +from schemas import ( + ValidatedCurriculumOutput, + LearningOutcomesOutput, + CourseData, + DNAMetadata, + OutlineInput, +) + + +router = APIRouter(prefix="/analysis", tags=["Outlines Transformer Before Content"]) + +@router.post("/outlines_transformer") +def run_outlines_transformer( + outlines: Dict, + data: Optional[OutlineInput] = None, +): + agent=course_structurer_agent() + task=transform_plan_to_course_task(agent) + outlines_transformer_crew = Crew( + agents=[agent], + tasks=[task], + verbose=True, + ) + + # تحديد الـ audience + if data: + inputs = data.dict() + audience_text = inputs.get("audience") + else: + audience_text = outlines.get("course_audience") + + result = outlines_transformer_crew.kickoff( + inputs={ + "plan_json": outlines, + "audience_text": audience_text, + } + ) + + final_result = result.json_dict + + return {"message": "Outlines Transformed Well 🚀", "result": final_result} + + +@router.post("/final_outlines_before_content") +def run_final_outlines(outlines: CourseData): + agent = curriculum_designer() + task = generate_course_structure_task(agent) + final_outlines_crew = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + ) + + course_data = outlines.dict() + + result = final_outlines_crew.kickoff( + inputs={ + "course_name": course_data["course_name"], + "course_audience": course_data["course_audience"], + "course_description": course_data["course_description"], + "learning_outcomes": course_data["learning_outcomes"], + "units": course_data["units"], + "example_reference": example_reference, + # "agent_prompt": agent_prompt, + } + ) + + final_result = result.json_dict + + return {"message": "Introduction Generated Well 🚀", "result": final_result} diff --git a/routers/introduction_route.py b/routers/introduction_route.py new file mode 100644 index 0000000000000000000000000000000000000000..d440ba629e95fc30a71a07a19ebaec9006a7cbc5 --- /dev/null +++ b/routers/introduction_route.py @@ -0,0 +1,61 @@ +import os +from crewai import Crew, Process +import json +from fastapi import APIRouter, HTTPException, Depends +from agents.analysis_phase import ( + intro_agent, + intro_task, +) +from modules import ( + llm, + inputs, +) +from schemas import ( + ValidatedCurriculumOutput, + LearningOutcomesOutput, + DNAMetadata, + OutlineInput, +) + + +router = APIRouter(prefix="/analysis", tags=["Introduction"]) + +# ------------------------------------ +# === Agent 7:Training Generator === +# ------------------------------------ + + +@router.post("/introduction") +def run_training( + data: OutlineInput, + outlines: ValidatedCurriculumOutput, + outcomes: LearningOutcomesOutput, + units:dict, +): + agent = intro_agent() + task = intro_task(agent) + training_crew = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + ) + + inputs = data.dict() + user_inputs = DNAMetadata( + topic=inputs["topic"], + domain=inputs["domain"], + content_type=inputs["content_type"], + audience=inputs["audience"], + material_type=inputs["material_type"], + ).dict() + + merged_inputs = { + **user_inputs, + "outlines": outlines.dict(), + "outcomes": outcomes.dict(), + "units": units, + } + result = training_crew.kickoff(inputs=merged_inputs) + print(result.json_dict) + + return {"message": "Introduction Generated Well 🚀", "result": result} diff --git a/routers/objective_route.py b/routers/objective_route.py new file mode 100644 index 0000000000000000000000000000000000000000..93f1555b419dd0b11571e6a52d666b68d7529ef2 --- /dev/null +++ b/routers/objective_route.py @@ -0,0 +1,53 @@ +import os +from crewai import Crew, Process +import json +from fastapi import APIRouter, HTTPException, Depends +from agents.analysis_phase import ( + objectives_generator, + objectives_task, +) +from modules import ( + llm_g, + inputs, +) +from schemas import ( + ValidatedCurriculumOutput, + DNAMetadata, + OutlineInput, +) + + +router = APIRouter(prefix="/analysis", tags=["Objectives"]) + + +# ------------------------------------ +# === Agent 5:Objectives Generator === +# ------------------------------------ + + +@router.post("/objectives") +def run_objectives( + data: OutlineInput, outlines: ValidatedCurriculumOutput, units: dict +): + agent = objectives_generator() + task = objectives_task(agent) + objectives_crew = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + ) + inputs = data.dict() + user_inputs = DNAMetadata( + topic=inputs["topic"], + domain=inputs["domain"], + content_type=inputs["content_type"], + audience=inputs["audience"], + material_type=inputs["material_type"], + ).dict() + merged_inputs = {**user_inputs, "units":units,"outlines": outlines.dict()} + + result = objectives_crew.kickoff(inputs=merged_inputs) + + print(result.json_dict) + + return {"message": "Objectives Generated Well 🚀", "result": result} diff --git a/routers/outcome_route.py b/routers/outcome_route.py new file mode 100644 index 0000000000000000000000000000000000000000..4e3dc4478af3107e0b7eb538a649ab71fc6b581a --- /dev/null +++ b/routers/outcome_route.py @@ -0,0 +1,52 @@ +import os +from crewai import Crew, Process +import json +from fastapi import APIRouter, HTTPException, Depends +from agents.analysis_phase import ( + outcomes_generator, + outcomes_task, +) +from modules import ( + llm_g, + inputs, +) +from schemas import ( + ObjectivesOutput, + LearningObjectivesOutput, + DNAMetadata, + OutlineInput, +) + + +router = APIRouter(prefix="/analysis", tags=["outcomes"]) + + +# ------------------------------------ +# === Agent 6:Outcomes Generator === +# ------------------------------------ + + +@router.post("/outcomes") +def run_outcomes(data: OutlineInput, objectives: LearningObjectivesOutput): + agent = outcomes_generator() + task = outcomes_task(agent) + outcomes_crew = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + ) + + inputs = data.dict() + user_inputs = DNAMetadata( + topic=inputs["topic"], + domain=inputs["domain"], + content_type=inputs["content_type"], + audience=inputs["audience"], + material_type=inputs["material_type"], + ).dict() + merged_inputs = {**user_inputs, "objectives": objectives.dict()} + + result = outcomes_crew.kickoff(inputs=merged_inputs) + print(result.json_dict) + + return {"message": "Outcomes Generated Well 🚀", "result": result} diff --git a/routers/outliner_route.py b/routers/outliner_route.py new file mode 100644 index 0000000000000000000000000000000000000000..165ad018132bf7b913e58e774e4d73a868e51f72 --- /dev/null +++ b/routers/outliner_route.py @@ -0,0 +1,71 @@ +import os +from crewai import Crew, Process +import json +from fastapi import APIRouter, HTTPException, Depends +from agents.final_outliners import ( + subtopics_generator_agent, + subtopics_generation_task, + validator_agent, + validation_task, + units_langchain1_agent, +) +from modules import ( + llm_5m, + inputs, +) +from schemas import ( + DNAMetadata, + OutlineInput, +) + + +router = APIRouter(prefix="/outliner", tags=["Outliner"]) + + +# === Agent 1: Unit Generator === +# === Agent 2: Axis Generator=== +# === Agent 3: Validator === +# === Agent 4: Final Reviewer (اختياري) === +# ----------------------------------------------- +@router.post("/run_outliner_agents") +def run_outliner_agents(data: OutlineInput): + subtopics_agent = subtopics_generator_agent() + subtopics_task = subtopics_generation_task(subtopics_agent) + validator = validator_agent() + validation_task_instance = validation_task(validator) + + outline_crew = Crew( + agents=[subtopics_agent, validator], + tasks=[subtopics_task, validation_task_instance], + process=Process.sequential, + ) + + inputs = data.dict() + user_inputs = DNAMetadata( + topic=inputs["topic"], + domain=inputs["domain"], + content_type=inputs["content_type"], + audience=inputs["audience"], + material_type=inputs["material_type"], + note=inputs["note"], + ).dict() + + units = units_langchain1_agent( + topic=inputs["topic"], units_number=inputs["units_number"] + ) + + merged_input = { + **user_inputs, + "units": units, + } + + result = outline_crew.kickoff(inputs=merged_input) + + print("units: ", units) + print("-" * 50) + print("final result: ", result.json_dict) + return { + "message": "Outliner Generators works well 🚀", + "result": result, + "units": units, + } diff --git a/routers/quiz_route.py b/routers/quiz_route.py new file mode 100644 index 0000000000000000000000000000000000000000..e1fb7ac2c6908e471c5e7aeeb3071411531e9713 --- /dev/null +++ b/routers/quiz_route.py @@ -0,0 +1,68 @@ +from tools import generate_quiz_for_course +import os +from crewai import Crew, Process +import json +from typing import Dict, Any +import logging +from fastapi import APIRouter, HTTPException, Depends +from fastapi.responses import HTMLResponse + +from core import ( + generate_course_content, + convert_dict_to_html, + convert_questions_dict_to_html, +) + +logger = logging.getLogger("quiz_service") +logging.basicConfig(level=logging.INFO) + +router = APIRouter(prefix="/quiz", tags=["Quiz Generation"]) + + +@router.post("/generate") +def generate_quiz_endpoint(course_data: Dict[str, Any]) -> Dict[str, Any]: + """ + expects: course_data نفس الستركشر اللي كنت بتحمله من الـ JSON file + """ + try: + logger.info( + f"📥 Received course for quiz generation: {course_data.get('course_name')}" + ) + result = generate_quiz_for_course(course_data, logger=logger.info) + + return {"status": "success", "data": result} + + except Exception as e: + logger.exception("Unhandled error while generating quiz") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/convert", response_class=HTMLResponse) +async def convert_to_html(questions_data: Dict[str, Any]): + """ + تحويل بيانات الأسئلة من Dictionary إلى HTML + + Args: + questions_data: Dictionary يحتوي على بيانات الأسئلة + - course_name: اسم المقرر + - course_audience: الجمهور المستهدف + - units: قائمة الوحدات + - unit_name: اسم الوحدة + - multiple_choice: قائمة أسئلة الاختيار من متعدد + - true_false: قائمة أسئلة الصح والخطأ + + Returns: + HTML string كامل جاهز للعرض + """ + try: + # التحقق من وجود البيانات الأساسية + if not questions_data: + raise HTTPException(status_code=400, detail="لا توجد بيانات مرسلة") + + # تحويل Dictionary إلى HTML + html_content = convert_questions_dict_to_html(questions_data) + + return HTMLResponse(content=html_content) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"خطأ في التحويل: {str(e)}") diff --git a/routers/scraper_route.py b/routers/scraper_route.py new file mode 100644 index 0000000000000000000000000000000000000000..b5f286fe8fa24916aef56e1be8f8058b4de6bb94 --- /dev/null +++ b/routers/scraper_route.py @@ -0,0 +1,431 @@ +# import os +# import json +# import shutil +# from fastapi import APIRouter, HTTPException, UploadFile, Form, Request +# from crewai import Crew, Process +# from agents.design_phase import scraping_built_in_agent, scraping_built_in_task,scraping_bs4_agent,scraping_bs4_task +# from schemas import DNAMetadata, OutlineInput +# from tools import scrape_with_bs4, crawl_parse_url,crawl_bs_url,extract_pdf_content + +# router = APIRouter(prefix="/design", tags=["Design"]) + +# ################################# +# # Built-in # +# ################################# +# @router.post("/scraper_built_in") +# # async def run_training(request: Request, file: UploadFile, data: str = Form(...)): +# """Uploads keywords JSON + metadata JSON, runs CrewAI search, returns download link.""" + +# # ✅ Parse metadata JSON +# try: +# parsed_data = json.loads(data) +# metadata = OutlineInput(**parsed_data) +# except Exception as e: +# raise HTTPException(status_code=400, detail=f"Invalid JSON in 'data': {e}") + +# # ✅ Save uploaded file temporarily +# save_path = f"/tmp/{file.filename}" +# with open(save_path, "wb") as buffer: +# shutil.copyfileobj(file.file, buffer) + +# # ✅ Validate file extension +# if not save_path.lower().endswith(".json"): +# raise HTTPException(status_code=400, detail="File must be a JSON file") + +# # ✅ Load file content +# try: +# with open(save_path, "r", encoding="utf-8") as f: +# urls_data = json.load(f) +# except json.JSONDecodeError: +# raise HTTPException(status_code=400, detail="Invalid JSON file content") + +# # ✅ Initialize Crew +# crew = Crew( +# agents=[scraping_built_in_agent], +# tasks=[scraping_built_in_task], +# process=Process.sequential, +# ) + +# # ✅ Build static user metadata +# user_inputs = DNAMetadata( +# topic=metadata.topic, +# domain=metadata.domain, +# content_type=metadata.content_type, +# audience=metadata.audience, +# material_type=metadata.material_type, +# ).dict() + +# all_results = [] + +# # ✅ Iterate through each topic unit and result link +# for unit in urls_data["results"]: +# unit_title = unit["unit_title"] +# subtopic_title = unit["subtopic_title"] +# query = unit["query"] + +# for result_item in unit["results"]: +# url = result_item["url"] +# print(f"🔍 Running scrape for [{subtopic_title}] | URL: {url}") + +# merged_input = { +# **user_inputs, +# "url": url, +# "unit_title": unit_title, +# "subtopic_title": subtopic_title, +# "query": query, +# } + +# try: +# result = crew.kickoff(inputs=merged_input) +# all_results.append(result.dict()) + +# # usage = result.token_usage # CrewAI بيحسبها جاهز + +# # total_prompt += usage["prompt_tokens"] +# # total_completion += usage["completion_tokens"] +# # total_tokens += usage["total_tokens"] +# except Exception as e: +# print(f"⚠️ Error while processing '{url}': {e}") + +# # ✅ Save aggregated results +# output_data = {"results": all_results} +# output_file = "/tmp/search_results.json" +# with open(output_file, "w", encoding="utf-8") as f: +# json.dump(output_data, f, ensure_ascii=False, indent=2) + +# # ✅ Build download URL +# base_url = str(request.base_url).rstrip("/") +# download_link = ( +# f"{base_url}/design/download?filename={os.path.basename(output_file)}" +# ) + +# return { +# "message": "Scraping process completed successfully 🚀", +# "total_queries": len(all_results), +# # "total_prompt": total_prompt, +# # "total_completion": total_completion, +# # "total_tokens": total_tokens, +# "download_link": download_link, +# "result": all_results, +# "json_dict": output_data, +# } + +# ################################# +# # BS4 # +# ################################# +# @router.post("/scraper_bs4_agent") +# async def run_training(request: Request, file: UploadFile, data: str = Form(...)): +# """Uploads keywords JSON + metadata JSON, runs CrewAI search, returns download link.""" + +# # ✅ Parse metadata JSON +# try: +# parsed_data = json.loads(data) +# metadata = OutlineInput(**parsed_data) +# except Exception as e: +# raise HTTPException(status_code=400, detail=f"Invalid JSON in 'data': {e}") + +# # ✅ Save uploaded file temporarily +# save_path = f"/tmp/{file.filename}" +# with open(save_path, "wb") as buffer: +# shutil.copyfileobj(file.file, buffer) + +# # ✅ Validate file extension +# if not save_path.lower().endswith(".json"): +# raise HTTPException(status_code=400, detail="File must be a JSON file") + +# # ✅ Load file content +# try: +# with open(save_path, "r", encoding="utf-8") as f: +# urls_data = json.load(f) +# except json.JSONDecodeError: +# raise HTTPException(status_code=400, detail="Invalid JSON file content") + +# # ✅ Initialize Crew +# crew = Crew( +# agents=[scraping_bs4_agent], +# tasks=[scraping_bs4_task], +# process=Process.sequential, +# ) + +# # ✅ Build static user metadata +# user_inputs = DNAMetadata( +# topic=metadata.topic, +# domain=metadata.domain, +# content_type=metadata.content_type, +# audience=metadata.audience, +# material_type=metadata.material_type, +# ).dict() + +# all_results = [] + +# # ✅ Iterate through each topic unit and result link +# for unit in urls_data["results"]: +# unit_title = unit["unit_title"] +# subtopic_title = unit["subtopic_title"] +# query = unit["query"] + +# for result_item in unit["results"]: +# url = result_item["url"] +# print(f"🔍 Running scrape for [{subtopic_title}] | URL: {url}") + +# merged_input = { +# **user_inputs, +# "url": url, +# "unit_title": unit_title, +# "subtopic_title": subtopic_title, +# "query": query, +# } + +# try: +# result = crew.kickoff(inputs=merged_input) +# all_results.append(result.dict()) + +# # usage = result.token_usage # CrewAI بيحسبها جاهز + +# # total_prompt += usage["prompt_tokens"] +# # total_completion += usage["completion_tokens"] +# # total_tokens += usage["total_tokens"] +# except Exception as e: +# print(f"⚠️ Error while processing '{url}': {e}") + +# # ✅ Save aggregated results +# output_data = {"results": all_results} +# output_file = "/tmp/search_results.json" +# with open(output_file, "w", encoding="utf-8") as f: +# json.dump(output_data, f, ensure_ascii=False, indent=2) + +# # ✅ Build download URL +# base_url = str(request.base_url).rstrip("/") +# download_link = ( +# f"{base_url}/design/download?filename={os.path.basename(output_file)}" +# ) + +# return { +# "message": "Scraping process completed successfully 🚀", +# "total_queries": len(all_results), +# # "total_prompt": total_prompt, +# # "total_completion": total_completion, +# # "total_tokens": total_tokens, +# "download_link": download_link, +# "result": all_results, +# "json_dict": output_data, +# } + +# # ################################# +# # # crawlee # +# # ################################# +# # @router.post("/scraper_crawlee_agent") +# # async def run_training(request: Request, file: UploadFile, data: str = Form(...)): +# # """Uploads keywords JSON + metadata JSON, runs CrewAI search, returns download link.""" + +# # # ✅ Parse metadata JSON +# # try: +# # parsed_data = json.loads(data) +# # metadata = OutlineInput(**parsed_data) +# # except Exception as e: +# # raise HTTPException(status_code=400, detail=f"Invalid JSON in 'data': {e}") + +# # # ✅ Save uploaded file temporarily +# # save_path = f"/tmp/{file.filename}" +# # with open(save_path, "wb") as buffer: +# # shutil.copyfileobj(file.file, buffer) + +# # # ✅ Validate file extension +# # if not save_path.lower().endswith(".json"): +# # raise HTTPException(status_code=400, detail="File must be a JSON file") + +# # # ✅ Load file content +# # try: +# # with open(save_path, "r", encoding="utf-8") as f: +# # urls_data = json.load(f) +# # except json.JSONDecodeError: +# # raise HTTPException(status_code=400, detail="Invalid JSON file content") + +# # # ✅ Initialize Crew +# # crew = Crew( +# # agents=[scraping_built_in_agent], +# # tasks=[scraping_built_in_task], +# # process=Process.sequential, +# # ) + +# # # ✅ Build static user metadata +# # user_inputs = DNAMetadata( +# # topic=metadata.topic, +# # domain=metadata.domain, +# # content_type=metadata.content_type, +# # audience=metadata.audience, +# # material_type=metadata.material_type, +# # ).dict() + +# # all_results = [] + +# # # ✅ Iterate through each topic unit and result link +# # for unit in urls_data["results"]: +# # unit_title = unit["unit_title"] +# # subtopic_title = unit["subtopic_title"] +# # query = unit["query"] + +# # for result_item in unit["results"]: +# # url = result_item["url"] +# # print(f"🔍 Running scrape for [{subtopic_title}] | URL: {url}") + +# # merged_input = { +# # **user_inputs, +# # "url": url, +# # "unit_title": unit_title, +# # "subtopic_title": subtopic_title, +# # "query": query, +# # } + +# # try: +# # result = crew.kickoff(inputs=merged_input) +# # all_results.append(result.dict()) +# # except Exception as e: +# # print(f"⚠️ Error while processing '{url}': {e}") + +# # # ✅ Save aggregated results +# # output_data = {"results": all_results} +# # output_file = "/tmp/search_results.json" +# # with open(output_file, "w", encoding="utf-8") as f: +# # json.dump(output_data, f, ensure_ascii=False, indent=2) + +# # # ✅ Build download URL +# # base_url = str(request.base_url).rstrip("/") +# # download_link = ( +# # f"{base_url}/design/download?filename={os.path.basename(output_file)}" +# # ) + +# # return { +# # "message": "Scraping process completed successfully 🚀", +# # "total_queries": len(all_results), +# # "download_link": download_link, +# # "result": all_results, +# # "json_dict": output_data, +# # } + + +# ############################## +# # =================================================================== +# # SHARED HELPER +# # =================================================================== +# async def process_json_scrape(request: Request, file: UploadFile, data: str, mode: str): +# """ +# mode = 'bs4' or 'crawlee' +# """ +# # ---- Parse metadata JSON ---- +# try: +# metadata = json.loads(data) +# except Exception as e: +# raise HTTPException(status_code=400, detail=f"Invalid metadata JSON: {e}") + +# # ---- Save JSON file ---- +# save_path = f"/tmp/{file.filename}" +# with open(save_path, "wb") as buffer: +# shutil.copyfileobj(file.file, buffer) + +# if not save_path.endswith(".json"): +# raise HTTPException(status_code=400, detail="Uploaded file must be .json") + +# # ---- Load JSON content ---- +# try: +# with open(save_path, "r", encoding="utf-8") as f: +# urls_data = json.load(f) +# except Exception: +# raise HTTPException(status_code=400, detail="Invalid JSON file content") + +# all_results = [] + +# # ---- Loop through your structure ---- +# for unit in urls_data["results"]: +# unit_title = unit["unit_title"] +# subtopic_title = unit["subtopic_title"] +# query = unit["query"] + +# for result_item in unit["results"]: +# url = result_item["url"] + +# print(f"🔍 Scraping | {subtopic_title} | {url}") + +# try: +# # ----- PDF case ----- +# if url.lower().endswith(".pdf"): +# scraped = { +# "page_url": url, +# "title": "", +# "content": extract_pdf_content(url), +# "img_url": [], +# "video_url": [], +# "audio_url": [], +# "pdf_url": [], +# "agent_recommendation_rank": 4.2, +# "agent_recommendation_notes": "Scraped successfully using Crawlee + ParselCrawler.", +# "header": "Web Scraping Test", +# "sub_header": "Crawlee Version", +# } +# elif mode == "bs4": +# scraped = scrape_with_bs4(url) +# elif mode == "crawlee bs": +# scraped = await crawl_bs_url(url) + +# elif mode == "crawlee parsel": +# scraped = await crawl_parse_url(url) + +# all_results.append( +# { +# "unit_title": unit_title, +# "subtopic_title": subtopic_title, +# "query": query, +# "parts": scraped, +# } +# ) + +# except Exception as e: +# all_results.append( +# { +# "unit_title": unit_title, +# "subtopic_title": subtopic_title, +# "query": query, +# "url": url, +# "error": str(e), +# } +# ) + +# # ---- Save Output ---- +# output_file = "/tmp/scrape_results.json" +# with open(output_file, "w", encoding="utf-8") as f: +# json.dump({"results": all_results}, f, ensure_ascii=False, indent=2) + +# # ---- Download link ---- +# base_url = str(request.base_url).rstrip("/") +# download_link = f"{base_url}/design/download?filename=scrape_results.json" + +# return { +# "message": f"Scraping completed using {mode.upper()} ✔", +# "total_links": len(all_results), +# "download_link": download_link, +# "results": all_results, +# } + + +# # =================================================================== +# # ROUTE 1 — BS4 SCRAPER +# # =================================================================== +# @router.post("/scraper_bs4") +# async def scraper_bs4(request: Request, file: UploadFile, data: str = Form(...)): +# return await process_json_scrape(request, file, data, mode="bs4") + + +# # =================================================================== +# # ROUTE 2 — CRAWLEE SCRAPER PARSEL +# # =================================================================== +# @router.post("/scraper_crawlee_parsel") +# async def scraper_crawlee(request: Request, file: UploadFile, data: str = Form(...)): +# return await process_json_scrape(request, file, data, mode="crawlee parsel") + + +# # =================================================================== +# # ROUTE 2 — CRAWLEE SCRAPER BESUTIFULSOUP +# # =================================================================== +# @router.post("/scraper_crawlee_bs") +# async def scraper_crawlee(request: Request, file: UploadFile, data: str = Form(...)): +# return await process_json_scrape(request, file, data, mode="crawlee bs") diff --git a/routers/source_route.py b/routers/source_route.py new file mode 100644 index 0000000000000000000000000000000000000000..6f3843a2650ce52480abce1d73d9003de855980d --- /dev/null +++ b/routers/source_route.py @@ -0,0 +1,129 @@ +# import os +# import json +# import shutil +# from fastapi import APIRouter, HTTPException, UploadFile, Form, Request +# from crewai import Crew, Process +# from agents.design_phase import search_engine_agent, search_engine_task +# from schemas import DNAMetadata, OutlineInput +# from modules import TRUSTED_SITES + +# router = APIRouter(prefix="/design", tags=["Design"]) + + +# @router.post("/source_finder") +# async def run_training(request: Request, file: UploadFile, data: str = Form(...)): +# """Uploads keywords JSON + metadata JSON, runs CrewAI search, returns download link""" + +# # ✅ Parse metadata JSON manually +# try: +# parsed_data = json.loads(data) +# metadata = OutlineInput(**parsed_data) +# except Exception as e: +# raise HTTPException( +# status_code=400, detail=f"Invalid JSON in 'data' field: {str(e)}" +# ) + +# # ✅ Save uploaded file +# save_path = f"/tmp/{file.filename}" +# with open(save_path, "wb") as buffer: +# shutil.copyfileobj(file.file, buffer) + +# # ✅ Validate extension +# ext = save_path.lower().split(".")[-1] +# if ext != "json": +# raise HTTPException(status_code=400, detail="File must be a JSON file") + +# # ✅ Load keywords file +# with open(save_path, "r", encoding="utf-8") as f: +# try: +# units_data = json.load(f) +# except json.JSONDecodeError: +# raise HTTPException(status_code=400, detail="Invalid JSON file content") + +# # ✅ Initialize Crew +# crew = Crew( +# agents=[search_engine_agent], +# tasks=[search_engine_task], +# process=Process.sequential, +# ) + +# # ✅ Prepare static user input +# user_inputs = DNAMetadata( +# topic=metadata.topic, +# domain=metadata.domain, +# content_type=metadata.content_type, +# audience=metadata.audience, +# material_type=metadata.material_type, +# ).dict() + +# all_results = [] +# # total_prompt = 0 +# # total_completion = 0 + +# # ✅ Loop through each subtopic and query +# for unit in units_data: +# unit_title = unit["unit_title"] +# subtopic_title = unit["subtopic_title"] +# queries = unit["queries"] + +# for query in queries: +# print(f"🔍 Running search for [{subtopic_title}] | Query: {query}") + +# merged_input = { +# **user_inputs, +# "score_th": 0.6, +# "no_links": 3, +# "queries": query, +# "unit_title": unit_title, +# "subtopic_title": subtopic_title, +# "TRUSTED_SITES": TRUSTED_SITES, +# } + +# try: +# result = crew.kickoff(inputs=merged_input) +# all_results.append(result.json_dict) + +# # usage = result.token_usage # CrewAI بيحسبها جاهز + +# # total_prompt += usage["prompt_tokens"] +# # total_completion += usage["completion_tokens"] +# # total_tokens += usage["total_tokens"] + +# except Exception as e: +# print(f"⚠️ Error while running query '{query}': {e}") +# output_data = {"results": all_results} +# # ✅ Save results to file +# output_file = f"/tmp/search_results" +# with open(output_file, "w", encoding="utf-8") as f: +# json.dump(output_data, f, ensure_ascii=False, indent=2) + +# # ✅ Create downloadable link +# base_url = str(request.base_url).rstrip("/") +# download_link = ( +# f"{base_url}/design/download?filename={os.path.basename(output_file)}" +# ) + +# return { +# "message": "Search process completed successfully 🚀", +# "total_queries": len(all_results), +# # "total_prompt": total_prompt, +# # "total_completion": total_completion, +# # "total_tokens": total_tokens, +# "download_link": download_link, +# "result": all_results, +# "json_dict": output_data, +# } + + +# # ✅ New endpoint for downloading results +# @router.get("/download") +# async def download_file(filename: str): +# file_path = f"/tmp/{filename}" + +# if not os.path.exists(file_path): +# raise HTTPException(status_code=404, detail="File not found") + +# with open(file_path, "r", encoding="utf-8") as f: +# data = json.load(f) + +# return data diff --git a/schemas/__init__.py b/schemas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..baad0e3da1e01f5a8705ac7f0c29431d6732c53e --- /dev/null +++ b/schemas/__init__.py @@ -0,0 +1,34 @@ +from .analysis_schema import AnalysisOutput +from .dna_schema import DNAMetadata +from .extractor_schema import UnitSubtopicOutputModel +from .keywoard_schema import SuggestedSearchQueries, CourseWithQueries +from .objectives_schema import ObjectivesOutput, LearningObjectivesOutput +from .outcomes_schema import LearningOutcomesOutput +from .outliner_schema import ( + Subtopic, + UnitWithAxes, + FullCurriculumStructure, + ValidatedAxis, + ValidatedUnit, + ValidatedCurriculumOutput, + validated_parser, +) + +from .source_schema import SearchEngineOutput +from .introduction_schema import IntroductionOutput +from .inputs_schema import OutlineInput +from .final_outlines_before_content_schema import CourseContent +from .outlines_transformer_schema import CourseData +from .quiz_schema import QuizOutput +from .unit_details_intro import ( + Units_details_output, + Units_Introduction, + Units_eval_details, +) +from .books.units_structure import CourseSchema +from .books.book_structure import BookOutlineOutput +from .books.book_planning import BookPlanningOutput +from .books.rag import ChunkRecord,DocMeta +from .books.queries_schema import BookOutlineWithQueries +from .books.search_schema import SearchBatch, SearchResult +from .books.units_intro import ChapterIntro,IntroPlan diff --git a/schemas/analysis_schema.py b/schemas/analysis_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..bca88bca9a24c256ffaa49f49900510b1bb48ee0 --- /dev/null +++ b/schemas/analysis_schema.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Literal, Optional + +# ====== Pydantic Model for Analysis Phase ====== +class AnalysisOutput(BaseModel): + goal: str = Field( + ..., + description="Overall training goal, must start with 'أن يتمكّن المتدرب من...'.", + ) + target_audience: str = Field( + ..., + description="The target audience group and their average age/professional profile.", + ) + previous_experiences: List[str] = Field( + ..., description="3–5 short noun-phrases reflecting prior experiences." + ) + learner_characteristics: List[str] = Field( + ..., description="Learner characteristics based on developmental dimensions." + ) + training_needs: List[str] = Field( + ..., + description="6 noun-phrases representing training needs extracted from course topics.", + ) + constraints: Dict[str, List[str]] = Field( + ..., + description="Constraints divided into {'trainer': [...], 'trainee': [...]}, 4 noun-phrases each.", + ) + duration: str = Field(..., description="The duration of the course (days/hours).") + sources_of_analysis: List[str] = Field( + ..., + description="Noun-phrases starting and ending with a مصدر, e.g., 'جمع البيانات وتحليلها'.", + ) diff --git a/schemas/books/book_cohesion.py b/schemas/books/book_cohesion.py new file mode 100644 index 0000000000000000000000000000000000000000..2d94fb5e410c92745f0d2d33881b77ae9268091c --- /dev/null +++ b/schemas/books/book_cohesion.py @@ -0,0 +1,33 @@ +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, Field, conint + + +class Subsection(BaseModel): + title: str = Field(..., min_length=2) + purpose: str = Field(..., min_length=3) + generated_content: str + sources: List[str] = Field(default_factory=list) + est_pages: conint(ge=1, le=20) = 3 + + +class Section(BaseModel): + title: str = Field(..., min_length=2) + overview: str = Field(default="") + subsections: List[Subsection] = Field(default_factory=list) + + +class Chapter(BaseModel): + chapter_title: str = Field(..., min_length=2) + outcome: str = Field(default="") + sections: List[Section] = Field(default_factory=list) + + +class BookChunkInput(BaseModel): + book_meta: Dict[str, Any] + chapters_chunk: List[Chapter] + previous_narrative_memory: Optional[Dict[str, Any]] = None + + +class CohesionOutput(BaseModel): + edited_chapters: List[Chapter] + narrative_memory: Dict[str, Any] diff --git a/schemas/books/book_planning.py b/schemas/books/book_planning.py new file mode 100644 index 0000000000000000000000000000000000000000..8ae75c4c1e9582300891bc63db535c5ad7957c2d --- /dev/null +++ b/schemas/books/book_planning.py @@ -0,0 +1,310 @@ +from typing import List, Dict, Optional +from pydantic import BaseModel, Field + + +# ================================ +# Core Plan Block (per Subsection) +# ================================ +class PlanningBlock(BaseModel): + status: str = Field( + ..., + description=( + "حالة كتابة المحور.\n" + "أمثلة مسموحة:\n" + "- ready: يمكن كتابته مباشرة\n" + "- conditional: يُكتب بحذر أو يحتاج تمهيد" + ), + examples=["ready", "conditional"], + ) + + estimated_pages: int = Field( + ..., ge=1, description="عدد الصفحات المقترحة لهذا المحور" + ) + + writing_mode: str = Field( + ..., + description=( + "نمط الكتابة الأساسي للمحور.\n" + "أمثلة:\n" + "- academic\n" + "- explanatory\n" + "- training\n" + "- analytical\n" + "- reflective\n" + "- practical\n" + "- conceptual\n" + "- historical" + ), + examples=[ + "academic", + "explanatory", + "training", + "analytical", + "reflective", + "practical", + "conceptual", + "historical", + ], + ) + + tone: str = Field( + ..., + description=( + "النبرة العامة للكتابة.\n" + "أمثلة:\n" + "- formal\n" + "- educational\n" + "- neutral\n" + "- motivational\n" + "- informational\n" + "- practical\n" + "- persuasive\n" + "- conversational\n" + "- storytelling" + ), + examples=[ + "formal", + "educational", + "neutral", + "motivational", + "informational", + "practical", + "persuasive", + "conversational", + "storytelling", + ], + ) + + core_idea: str = Field( + ..., + min_length=12, + description="الفكرة المركزية الوحيدة لهذا المحور (جملة واحدة واضحة)", + ) + + key_points: List[str] = Field( + ..., + min_items=3, + description="نقاط التخطيط الأساسية (عناوين فقط بدون شرح)", + ) + + scope_limit: str = Field( + ..., min_length=10, description="ما الذي لا يجب تغطيته في هذا المحور" + ) + + avoid_overlap_with: List[str] = Field( + default_factory=list, + description="عناوين محاور أخرى يجب تجنب التداخل معها", + ) + + depth_level: str = Field( + ..., + description=( + "مستوى عمق المعالجة.\n" + "أمثلة:\n" + "- introductory\n" + "- basic\n" + "- intermediate\n" + "- advanced\n" + "- conceptual\n" + "- applied\n" + "- analytical" + ), + examples=[ + "introductory", + "basic", + "intermediate", + "advanced", + "conceptual", + "applied", + "analytical", + ], + ) + + citations_required: bool = Field( + ..., description="هل يتطلب هذا المحور توثيقًا علميًا داخل المتن؟" + ) + + notes: str = Field(default="", description="ملاحظات تنفيذية قصيرة للكاتب (اختياري)") + + +# ================================ +# Chapter Intro Blueprint +# ================================ +class ChapterIntroPlan(BaseModel): + hook_type: str = Field( + ..., + description=( + "نوع الخطاف الافتتاحي للتمهيد.\n" + "أمثلة:\n" + "- question\n" + "- story\n" + "- scene\n" + "- problem\n" + "- contrast" + ), + examples=["question", "story", "scene", "problem", "contrast"], + ) + + hook_idea: str = Field( + ..., min_length=12, description="فكرة الخطاف بجملة أو جملتين بدون كتابة فعلية" + ) + + why_this_chapter_now: str = Field( + ..., min_length=12, description="لماذا هذا الشابتر مهم الآن للقارئ؟" + ) + + bridge_to_sections: List[str] = Field( + ..., min_items=2, description="جمل ربط مختصرة تقود لأقسام الشابتر" + ) + + do_not_do: List[str] = Field( + default_factory=list, + description=( + "أشياء ممنوعة في التمهيد.\n" + "أمثلة:\n" + "- في ظل...\n" + "- يبرز...\n" + "- تعريف أكاديمي طويل\n" + "- توثيق داخل المتن" + ), + ) + + estimated_pages: int = Field( + ..., ge=1, description="عدد صفحات التمهيد المقترحة (1–4)" + ) + + +# ================================ +# Style Profile (Chapter-level) +# ================================ +class StyleOutput(BaseModel): + tone: str = Field(..., description="النبرة العامة لأسلوب الشابتر") + + language_level: str = Field( + ..., description="مستوى اللغة (مثال: مبسطة، قياسية، أكاديمية خفيفة)" + ) + + sentence_structure: str = Field( + ..., description="شكل الجمل (قصيرة، متوسطة، طويلة، مزيج)" + ) + + detail_level: str = Field(..., description="مستوى التفصيل (منخفض، متوسط، مرتفع)") + + reader_interaction: str = Field( + ..., description="طريقة مخاطبة القارئ (مباشرة، غير مباشرة، تأملية)" + ) + + authority_level: Optional[str] = Field( + None, description="درجة السلطة المعرفية للصوت السردي" + ) + + authority_markers: List[str] = Field( + default_factory=list, + description="عبارات تدل على السلطة المعرفية (مثال: تشير الدراسات إلى...)", + ) + + voice_type: Optional[str] = Field( + None, description="نوع الصوت السردي (تفسيري، إرشادي، تحليلي)" + ) + + uses_religious_references: Optional[bool] = None + uses_scientific_or_textual_references: Optional[bool] = None + + explicit_structure_markers: Optional[bool] = None + + example_markers: List[str] = Field( + default_factory=list, + description="عبارات تقديم الأمثلة (مثال: على سبيل المثال...)", + ) + + intro_function: Optional[str] = Field( + None, description="الوظيفة الأساسية لتمهيد الشابتر" + ) + + allows_definition_in_intro: Optional[bool] = None + allows_general_explanation_in_intro: Optional[bool] = None + allows_direct_address_in_intro: Optional[bool] = None + allows_explicit_statements: Optional[bool] = None + allows_religious_or_textual_references_in_intro: Optional[bool] = None + + recommended_intro_tone: Optional[str] = None + + stylistic_notes: List[str] = Field( + default_factory=list, description="ملاحظات أسلوبية عامة للكاتب" + ) + + opening_style: Optional[str] = None + middle_style: Optional[str] = None + closing_style: Optional[str] = None + + +class StyleProfileBlock(BaseModel): + style: StyleOutput + + +# ================================ +# Output per Section (Plans only) +# ================================ +class SectionPlansBlock(BaseModel): + section_title: str = Field(..., description="عنوان السكشن") + + # جديد: نص فعلي مكتوب + section_intro_paragraph: str = Field( + ..., + min_length=80, + description=( + "فقرة افتتاحية مكتوبة فعليًا لهذا السكشن (وليست تخطيطًا)، " + "تهيّئ السياق قبل المحاور الفرعية." + ), + ) + + # جديد: نص فعلي مكتوب + section_closing_paragraph: str = Field( + ..., + min_length=70, + description=( + "فقرة ختامية مكتوبة فعليًا لهذا السكشن (وليست تخطيطًا)، " + "تغلق السكشن بامتداد منطقي يمهّد لما بعده دون تلخيص تعليمي مباشر." + ), + ) + + plans: Dict[str, PlanningBlock] + +################################### +class SubsectionPlanItem(BaseModel): + subsection_title: str + plan: PlanningBlock + + +class SectionPlansBlockDTO(BaseModel): + section_title: str + + section_intro_paragraph: str + section_closing_paragraph: str + + plans: List[SubsectionPlanItem] + + +class ChapterPlanningBlockDTO(BaseModel): + chapter_title: str + chapter_intro: ChapterIntroPlan + style_profile: StyleProfileBlock + sections: List[SectionPlansBlockDTO] + + +# ================================ +# Output per Chapter +# ================================ +class ChapterPlanningBlock(BaseModel): + chapter_title: str + chapter_intro: ChapterIntroPlan + style_profile: StyleProfileBlock + sections: List[SectionPlansBlock] + + +class BookPlanningOutput(BaseModel): + book_title: str + book_description: Optional[str] = None + target_audience: str + chapters: List[ChapterPlanningBlock] + planning_notes: str = "" diff --git a/schemas/books/book_structure.py b/schemas/books/book_structure.py new file mode 100644 index 0000000000000000000000000000000000000000..47f559fcad578096a31f78154f98efe60467c4c7 --- /dev/null +++ b/schemas/books/book_structure.py @@ -0,0 +1,76 @@ +from typing import List, Literal, Optional, Dict, Any +from pydantic import BaseModel, Field, conint + + +# --------------------------------- +# 1) Pydantic models (BOOK NAMING) +# --------------------------------- +class Subsection(BaseModel): + """ + Subsection = محور/جزئية داخل القسم. + """ + + title: str = Field(..., min_length=4, description="عنوان قصير واضح للمحور") + purpose: str = Field( + ..., + min_length=12, + description="1-2 جملة: لماذا هذا المحور موجود؟ ما هدفه للقارئ؟", + ) + # optional: signals for later RAG planning (NOT content) + suggested_queries: List[str] = Field( + default_factory=list, description="أسئلة بحث لاحقًا للـRAG (اختياري)" + ) + difficulty: Literal["basic", "intermediate", "advanced"] = Field( + "basic", description="مستوى تقديم المحور" + ) + est_pages: conint(ge=1, le=12) = Field( + 3, description="تقدير صفحات للمحور (للتوازن ومنع التكديس)" + ) + + +class Section(BaseModel): + """ + Section = قسم داخل الفصل. + """ + + title: str = Field(..., min_length=4, description="عنوان القسم") + overview: str = Field( + ..., min_length=12, description="جملة/جملتين: نطاق القسم وما الذي سيغطيه" + ) + subsections: List[Subsection] = Field( + ..., min_items=2, description="2-6 محاور مرتبة منطقيًا" + ) + + +class Chapter(BaseModel): + """ + Chapter = فصل/وحدة كتاب. + """ + + chapter_title: str = Field(..., min_length=4, description="عنوان الفصل") + outcome: str = Field( + ..., min_length=12, description="ماذا يجب أن يعرف/يفهم القارئ بعد الفصل؟" + ) + sections: List[Section] = Field( + ..., min_items=2, description="2-6 أقسام مرتبة من الأساسيات للتطبيق" + ) + + +class BookOutlineOutput(BaseModel): + """ + Final JSON output for the book outline. + """ + + book_title: str + target_audience: str + book_description: str = Field( + ..., min_length=20, description="وصف مختصر للكتاب (2-4 جمل) — بدون تفاصيل محتوى" + ) + chapters: List[Chapter] + outline_notes: str = Field( + default="", + description="ملاحظات تحريرية عامة: توازن الفصول، منع التكرار، ملاحظات تنظيمية", + ) + total_pages: int = Field( + description="تقدير صفحات الكتاب كامل (للتوازن ومنع التكديس)" + ) diff --git a/schemas/books/enrichment.py b/schemas/books/enrichment.py new file mode 100644 index 0000000000000000000000000000000000000000..d478665eb4e7a58e1bb3cb45a6c106d2eed83976 --- /dev/null +++ b/schemas/books/enrichment.py @@ -0,0 +1,13 @@ +from typing import List, Literal +from pydantic import BaseModel + +class EnrichmentMeta(BaseModel): + enrichment_type: Literal[ + "case_story", "case_study", "knowledge_flash", "analytical_quote" + ] + enrichment_generated: str = "" # short excerpt of sentence it followed + + +class EnrichedContentOutput(BaseModel): + results: List[EnrichmentMeta] + content_after_enrichment: str diff --git a/schemas/books/queries_schema.py b/schemas/books/queries_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..224fe0e3f74eb826acc9490f35fc08040b47b629 --- /dev/null +++ b/schemas/books/queries_schema.py @@ -0,0 +1,69 @@ +import json +from typing import List, Optional +from pydantic import BaseModel, Field + + +# ======================================= +# 1) Pydantic Models (NEW STRUCTURE) +# ======================================= + + +class Subsection(BaseModel): + title: str + purpose: str = "" + suggested_queries: List[str] = Field(default_factory=list) + difficulty: Optional[str] = "" + est_pages: Optional[int] = None + + # ✅ NEW FIELD WE ADD + academic_queries: List[str] = Field(..., min_items=2, max_items=3) + + +class Section(BaseModel): + title: str + overview: str = "" + subsections: List[Subsection] + + +class Chapter(BaseModel): + chapter_title: str + outcome: str = "" + sections: List[Section] + + +class BookOutlineWithQueries(BaseModel): + book_title: str + target_audience: str = "" + book_description: str = "" + chapters: List[Chapter] + outline_notes: Optional[str] = "" + total_pages: Optional[int] = None + + +from typing import List +from pydantic import BaseModel, Field + + +# ======================================= +# Query Object +# ======================================= + + +class SubsectionQueries(BaseModel): + subsection: str = Field(..., description="Subsection title") + + queries: List[str] = Field( + ..., + min_items=3, + max_items=3, + description="Exactly three academic search queries for the subsection", + ) + + +# ======================================= +# Full Output Schema +# ======================================= + + +class QueriesOutput(BaseModel): + queries: List[SubsectionQueries] diff --git a/schemas/books/rag.py b/schemas/books/rag.py new file mode 100644 index 0000000000000000000000000000000000000000..7dded1be166ae14cfe0102424710562367096c18 --- /dev/null +++ b/schemas/books/rag.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass + +@dataclass +class DocMeta: + doc_id: str + title: str + author: str + year: int + publisher: str = "" + language: str = "ar" + +@dataclass +class ChunkRecord: + chunk_id: str + doc_id: str + page_start: int + page_end: int + text: str + title: str + author: str + year: int + language: str = "ar" diff --git a/schemas/books/search_schema.py b/schemas/books/search_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..7ecfd0ecd94cb559cf629f4b7f921c00ba9f031a --- /dev/null +++ b/schemas/books/search_schema.py @@ -0,0 +1,17 @@ +from typing import List +from pydantic import BaseModel, HttpUrl, Field + + +class SearchResult(BaseModel): + query: str + url: HttpUrl + title: str = "" + snippet: str = "" + + source_type: str # pdf | book | journal | institutional | web + domain: str + score: float + + +class SearchBatch(BaseModel): + results: List[SearchResult] diff --git a/schemas/books/sources_schema.py b/schemas/books/sources_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..011c4ea8767b6c72a243e0e52db50e8bbb7df523 --- /dev/null +++ b/schemas/books/sources_schema.py @@ -0,0 +1,233 @@ +# schemas/books/sources_schema.py +from __future__ import annotations +from dataclasses import dataclass + +from pydantic import BaseModel, Field, AliasChoices +from typing import List, Optional, Any, Dict, Literal + +# from typing import List, Optional, Literal +# from pydantic import BaseModel, Field, AliasChoices + + +# ============ Stage2 output item ============ +class SearchResult(BaseModel): + url: str + title: str = "" + snippet: str = "" + source_type: str = Field(..., description="pdf|journal|institutional|book|web") + domain: str = "" + score: float = 0.0 + query: Optional[str] = "" + + +# ============ Agent input ============ +# class SourceForAgent(BaseModel): +# source_url: str +# source_type: str +# domain: str = "" +# search_title: str = "" +# search_snippet: str = "" + +# ✅ يقبل ocr_pages أو text_pages +# text_pages: List[str] = Field( +# ..., +# min_length=1, +# max_length=3, +# validation_alias=AliasChoices("text_pages", "ocr_pages"), +# serialization_alias="text_pages", +# ) + + +# ============ Agent output ============ +from typing import List, Optional, Literal, Dict, Any +from pydantic import BaseModel, Field + + +class SourceMetadata(BaseModel): + title: Optional[str] = Field( + default=None, + description="Exact document title as explicitly written in the source text.", + ) + + authors: List[str] = Field( + default_factory=list, + description=( + "List of author names exactly as written. " + "Can include institutional author as a single item." + ), + ) + + year: Optional[int] = Field( + default=None, + description="Publication year if explicitly stated.", + ge=1500, + le=2100, + ) + + publisher_or_journal: Optional[str] = Field( + default=None, + description="Publisher name or journal name if explicitly present.", + ) + + language: Optional[Literal["ar", "en"]] = Field( + default=None, + description="Language of the document.", + ) + + apa7: Optional[str] = Field( + default=None, + description="APA citation if explicitly provided in the source (usually empty).", + ) + + +class SourceMetadataAgentResult(BaseModel): + is_valid: bool = Field( + ..., + description="Whether the source contains sufficient explicit metadata.", + ) + + reason: Optional[str] = Field( + default=None, + description=( + "Short reason if is_valid=false " + "(e.g. 'missing authors', 'missing title')." + ), + ) + + normalized_source_type: Optional[ + Literal[ + "book", + "journal_article", + "thesis", + "institutional_report", + "website", + ] + ] = Field( + default=None, + description="Normalized academic source type.", + ) + + metadata: Optional[SourceMetadata] = Field( + default=None, + description="Extracted bibliographic metadata.", + ) + + # Optional: keep raw evidence for auditing (agent may ignore) + evidence: Optional[Dict[str, Any]] = Field( + default=None, + description=( + "Optional raw evidence snippets for extracted fields. " + "Used for auditing/debugging only." + ), + ) + + +# class ExtractedMetadata(BaseModel): +# title: str = Field(..., description="Exact title as appears in source") +# authors: List[str] = Field(..., min_items=1, description="Author names as-is") +# year: Optional[int] = Field( +# None, description="Publication year if explicitly found" +# ) +# publisher_or_journal: Optional[str] = Field( +# None, description="Journal, university, publisher, or institution" +# ) +# language: Literal["ar", "en"] + + +# class MetadataSignals(BaseModel): +# title_source: Literal["first_page", "cover", "header", "unknown"] +# author_source: Literal["first_page", "cover", "footer", "unknown"] +# year_source: Literal["explicit", "inferred", "missing"] +# text_quality: Literal["good", "mixed", "poor"] + + +# class SourceMetadataAgentResult(BaseModel): +# is_valid: bool +# reason: Optional[str] = "" +# normalized_source_type: Literal[ +# "book", +# "journal_article", +# "thesis", +# "institutional_report", +# "website", +# ] +# confidence: float = Field(..., ge=0.0, le=1.0) +# metadata: Optional[ExtractedMetadata] = None +# signals: Optional[MetadataSignals] = None + + +# ============ Doc meta stored ============ + + +class DocRaw(BaseModel): + doc_id: str + user_id: str + book_id: str + source_url: str + source_type: str = "pdf" + domain: str = "" + language: str = "ar" + pages_head: List[str] = Field(default_factory=list) + extraction_method: str = "text" # text | ocr + pdf_size_bytes: Optional[int] = None + status: str = "pending" # pending | processed | failed + error_reason: str = "" + + +class DocMetadata(BaseModel): + doc_id: str + title: str + authors: List[str] + year: Optional[int] = None + publisher_or_journal: str = "" + normalized_source_type: str = "pdf" + apa7: str = "" + metadata: Dict[str, Any] = Field(default_factory=dict) + + +# موجود عندك غالباً - سيبه زي ما هو +class SourceForAgent(BaseModel): + source_url: str + source_type: str + domain: str + text_pages: List[str] + + search_title: Optional[str] = None + search_snippet: Optional[str] = None + + +# موجود عندك غالباً - سيبه زي ما هو +class ChunkRecord(BaseModel): + chunk_id: str + user_id: str + book_id: str + doc_id: str + source_url: str + source_type: str + domain: str + title: str + authors: str + year: Optional[int] + publisher_or_journal: str + language: str + apa7: str + page_start: int + page_end: int + text: str + + +# ============ API ============ +class IngestFromNetRequest(BaseModel): + user_id: str + book_id: str + sources: List[SearchResult] + + +class IngestFromNetResponse(BaseModel): + user_id: str + book_id: str + collection: str + total_sources: int + ingested: int + rejected: int + items: List[dict] diff --git a/schemas/books/units_intro.py b/schemas/books/units_intro.py new file mode 100644 index 0000000000000000000000000000000000000000..eed8e77c88dc50132e94d28be7551d781130f9f6 --- /dev/null +++ b/schemas/books/units_intro.py @@ -0,0 +1,68 @@ +from __future__ import annotations +from dataclasses import dataclass + +from pydantic import BaseModel, Field, AliasChoices +from typing import List, Optional, Any, Dict, Literal + + +class ChapterIntro(BaseModel): + chapter_name: str = "" + intro: str = "" + + +class StyleOutput(BaseModel): + # 1. Core Writing Style + tone: str + language_level: str + sentence_structure: str + detail_level: str + reader_interaction: str + + # 2. Explanation Signals + # explanation_density: Optional[str] = None # low / medium / high + # uses_explanatory_phrases: Optional[bool] = None + # example_phrases_detected: Optional[List[str]] = [] + + # 3. Authorial Authority + authority_level: Optional[str] = None # low / medium / high + # authority_markers: Optional[List[str]] = [] + voice_type: Optional[str] = ( + None # explanatory_authoritative / reflective / instructional + ) + + # 4. Reference & Legitimacy Usage + uses_religious_references: Optional[bool] = None + # uses_scientific_or_textual_references: Optional[bool] = None + # comfort_with_citation: Optional[str] = None # low / medium / high + + # 5. Organizational Explicitness + # explicit_structure_markers: Optional[bool] = None + # example_markers: Optional[List[str]] = [] + + # 6. Inferred Intro Writing Rules + intro_function: Optional[str] = ( + None # narrative_teasing / conceptual_explanation / didactic_guidance / academic_orientation + ) + allows_definition_in_intro: Optional[bool] = None + allows_general_explanation_in_intro: Optional[bool] = None + allows_direct_address_in_intro: Optional[bool] = None + allows_explicit_statements: Optional[bool] = None + allows_religious_or_textual_references_in_intro: Optional[bool] = None + recommended_intro_tone: Optional[str] = None + + # 7. Stylistic Notes + stylistic_notes: Optional[List[str]] = [] + + # Optional: original opening/middle/closing style in chapter (for reference) + opening_style: Optional[str] = None + middle_style: Optional[str] = None + closing_style: Optional[str] = None + + +class IntrosStyles(BaseModel): + chapter_name: str = "" + intro_style: StyleOutput + + +class IntroPlan(BaseModel): + chapters: List[IntrosStyles] = [] diff --git a/schemas/books/units_structure.py b/schemas/books/units_structure.py new file mode 100644 index 0000000000000000000000000000000000000000..88fca239bac10b34d1ac3b4b215f003671db11d7 --- /dev/null +++ b/schemas/books/units_structure.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, Field +from typing import List + + +class Unit(BaseModel): + outcome: str + unit_name: str + topics: List[str] + + +class CourseSchema(BaseModel): + course_name: str + course_audience: str + course_description: str + learning_outcomes: List[str] + units: List[Unit] diff --git a/schemas/dna_schema.py b/schemas/dna_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..4a49909307dc26e44463b207ffed33d2dba93476 --- /dev/null +++ b/schemas/dna_schema.py @@ -0,0 +1,82 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Literal, Optional + + +# ========= 1. Parameters for DNA methodology + topic ========= +class DNAMetadata(BaseModel): + topic: str = Field( + ..., + title="The main topic to generate an outline for", + description="The specific subject or concept that the outline should focus on. Example: 'Time Management', 'Machine Learning Basics', 'Healthy Nutrition'", + ) + + domain: str = Field( + ..., + title="Knowledge domain", + description="The broader field of knowledge or industry where the topic belongs. Example: IT, Engineering, Education, Healthcare, Business", + ) + + # content_type: Literal["Educational", "Awareness", "Training"] = Field( + + content_type: str = Field( + ..., + title="Type of content", + description=( + "Educational → for structured learning (e.g., lessons, modules)\n" + "Awareness → for informative/raising awareness purposes (e.g., campaigns, workshops)\n" + "Training → for skill development with step-by-step practice (e.g., hands-on, exercises)" + ), + ) + + # audience: str = Field( + # ..., + # title="Target audience", + # description=( + # "The group of people intended to benefit from the material. Example:\n" + # "- Kids (age 6–12)\n" + # "- High School Students (teenagers)\n" + # "- University Students (academic level)\n" + # "- Professionals (working individuals)\n" + # "- General Public (all ages, non-technical)" + # ) + # ) + + audience: str = Field( + ..., + title="Target audience", + description=( + "The group of people intended to benefit from the material. Examples:\n" + "- Education (Teachers, Principals, Students, Faculty, Instructional Designers)\n" + "- Special Education (Special Needs Teachers, Speech Therapists, Psychologists, ABA Specialists, Children with Disabilities)\n" + "- Family & Child (Parents, Counselors, Social Workers, Children, Adolescents, Youth, Elderly)\n" + "- Women Empowerment (Mothers, Teenage Girls, Women Entrepreneurs, Researchers, Artists)\n" + "- Self-Development (Individuals seeking personal growth, Life Coaches, Students, Graduates, Employees)\n" + "- Training (Soft/Technical Skills Trainers, Training Designers, Training Institutes)\n" + "- Human Resources (HR Managers, Recruiters, Employee Relations, L&D Officers)\n" + "- Management & Leadership (CEOs, Directors, Team Leaders, Consultants, Entrepreneurs)\n" + "- Law (Lawyers, Judges, Prosecutors, Legal Advisors, Notaries)\n" + "- Public Relations & Media (PR Officers, Journalists, Broadcasters, Media Professionals)\n" + "- Marketing & Sales (Marketing Managers, Sales Representatives, Digital Marketing Specialists, Content Creators)\n" + "- Technology & IT (Software Developers, Data Scientists, AI Engineers, Cybersecurity Specialists)\n" + "- Languages & Translation (Translators, Interpreters, Language Teachers, Proofreaders)\n" + "- Medicine & Nutrition (Doctors, Nurses, Pharmacists, Nutritionists, Mental Health Specialists)\n" + "- Occupational Health & Safety (HSE Officers, Safety Inspectors, First Aid Trainers)\n" + "- Sports (Athletes, Coaches, Referees, Nutritionists, Physiotherapists)\n" + "- Arts & Fashion (Artists, Designers, Photographers, Art Directors)" + ), + ) + + material_type: List[str] = Field( + ..., + title="Nature of the scientific material", + description=( + "Defines the learning approach:\n" + "- Conceptual → focuses on theories, principles, and abstract knowledge.\n" + "- Procedural → step-by-step methods, instructions, or processes.\n" + "- Structural → frameworks, models, and organized structures.\n" + "- Personal → reflections, personal skills, and self-development aspects.\n" + "- Realistic → practical, case-based, real-world examples and applications." + ), + ) + + note: Optional[str] = None diff --git a/schemas/extractor_schema.py b/schemas/extractor_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..43963594e38d76a24307075a252cf5a0e2ffc691 --- /dev/null +++ b/schemas/extractor_schema.py @@ -0,0 +1,24 @@ +from typing import List, Optional +from pydantic import BaseModel + + +class PartModel(BaseModel): + page_url: Optional[str] = None + title: Optional[str] = None + content: Optional[str] = None + + img_url: Optional[List[str]] = None + video_url: Optional[List[str]] = None + audio_url: Optional[List[str]] = None + pdf_url: Optional[List[str]] = None + + agent_recommendation_rank: Optional[float] = None + agent_recommendation_notes: Optional[str] = None + + +class UnitSubtopicOutputModel(BaseModel): + unit_title: Optional[str] = None + subtopic_title: Optional[str] = None + query: Optional[str] = None + + parts: Optional[List[PartModel]] = None diff --git a/schemas/final_outlines_before_content_schema.py b/schemas/final_outlines_before_content_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..96ad3663aaed4c7fe42af4fa1a4c41fbcb6c7876 --- /dev/null +++ b/schemas/final_outlines_before_content_schema.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel, Field +from typing import List + + +# =============================== +# المستوى الرابع: SubTopic +# =============================== +class SubTopic(BaseModel): + title: str = Field(..., description="عنوان العنوان الفرعي") + description: str = Field(..., description="وصف العنوان الفرعي") + + +# =============================== +# المستوى الثالث: Topic +# =============================== +class Topic(BaseModel): + topic_name: str = Field(..., description="اسم الموضوع") + subtopics: List[SubTopic] = Field(..., description="العناوين الفرعية للموضوع") + + +# =============================== +# المستوى الثاني: Unit +# =============================== +class ExpandedUnit(BaseModel): + unit_name: str = Field(..., description="اسم الوحدة") + outcome: str = Field(..., description="ناتج التعلم للوحدة") + topics: List[Topic] = Field(..., description="موضوعات الوحدة") + + +# =============================== +# المستوى الأول: Course +# =============================== +class CourseContent(BaseModel): + course_name: str = Field(..., description="اسم المقرر") + course_audience: str = Field(..., description="الفئة المستهدفة من المقرر") + expanded_units: List[ExpandedUnit] = Field( + ..., description="الوحدات التفصيلية للمقرر" + ) diff --git a/schemas/inputs_schema.py b/schemas/inputs_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..9b3ec8bc297b320b43453e7a94116c1e961e6311 --- /dev/null +++ b/schemas/inputs_schema.py @@ -0,0 +1,50 @@ +from pydantic import BaseModel +from typing import Optional + +# Step 1️⃣: Create a Pydantic model for inputs +class OutlineInput(BaseModel): + topic: str + domain: str + content_type: str + audience: str + material_type: list + units_number: int + note: Optional[str] = None + + +# { +# "topic": "إدارة الوقت", +# "domain": "Management", +# "content_type": "Educational", +# "audience": "kids", +# "material_type": ["Procedural", "Realistic", "Personal"], +# } + +# { +# "topic": "(Design thinking) تقنيات التفكير التصميمي لحل المشكلات واتخاذ القرارات", +# "domain": "Management", +# "content_type": "تدريبي", +# "audience": "القادة والموظفون والإداريون", +# "material_type": ["مفاهمية", "هيكلية", "شخصية", "واقعية", "اجرائية"], +# } + + +# { +# "topic": "ريادة الأعمال", +# "domain": "Management, Business", +# "content_type": "Awareness, Training", +# "audience": "CEOs, Directors, Entrepreneurs", +# "material_type": ["مفاهمية", "هيكلية", "اجرائية", "واقعية"], +# "units_number": 2, +# } + +# TOPIC = "استراتيجية التدريس" + +# { +# "topic": "استراتيجية التدريس", +# "domain": "Education, Management", +# "content_type": "Educational, Training - Skill development with practice", +# "audience": "Teachers", +# "material_type": ["مفاهمية", "هيكلية", "اجرائية", "واقعية"], +# "units_number": 2, +# } diff --git a/schemas/introduction_schema.py b/schemas/introduction_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..16822e32d05727db722a5ce896ff4cdb1eaaafca --- /dev/null +++ b/schemas/introduction_schema.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Literal, Optional + + +class IntroductionOutput(BaseModel): + introduction: str = Field( + ..., + title="Introduction", + description="engaging introduction to the training program content, max 300 words.", + ) diff --git a/schemas/keywoard_schema.py b/schemas/keywoard_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..f95cd894ed08e921210476db8e820b89c53d551e --- /dev/null +++ b/schemas/keywoard_schema.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Literal, Optional + + +no_keywords = 25 + + +class SuggestedSearchQueries(BaseModel): + queries: List[str] = Field( + ..., + title="List of suggested search queries", + min_items=1, + max_items=no_keywords, + ) + + +# ==== Pydantic models للبنية النهائية ==== +class Subtopic(BaseModel): + title: str + description: str = "" + queries: List[str] = Field(..., min_items=1, max_items=3) + + +class Topic(BaseModel): + topic_name: str + subtopics: List[Subtopic] + + +class Unit(BaseModel): + unit_name: str + outcome: str + topics: List[Topic] + + +class CourseWithQueries(BaseModel): + course_name: str + course_audience: str + expanded_units: List[Unit] diff --git a/schemas/objectives_schema.py b/schemas/objectives_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..bc7f0931eee819ae4d58d9fd6b92c4ea251a3d63 --- /dev/null +++ b/schemas/objectives_schema.py @@ -0,0 +1,65 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Literal, Optional +from typing import List +from pydantic import BaseModel, Field + + +class Objective(BaseModel): + unit: str = Field(..., description="The title of the unit.") + unit_description: str = Field(..., description="A brief summary of the unit scope.") + subtopic: str = Field(..., description="The title of the subtopic (axis).") + module_description: str = Field( + ..., description="A brief summary of what this subtopic covers." + ) + level: str = Field( + ..., description="Bloom's level assigned based on subtopic analysis." + ) + verbs: List[str] = Field( + ..., description="A list of verbs used to define the learning objective." + ) + objective: str = Field( + ..., + description="A measurable Arabic objective derived from the subtopic purpose.", + ) + + +class ObjectivesOutput(BaseModel): + objectives: List[Objective] = Field( + ..., + description="List of objectives including unit, subtopic, and measurable goals.", + ) + +from pydantic import BaseModel, Field +from typing import List, Dict, Literal, Optional + + +class LearningObjective(BaseModel): + """Schema for a single learning objective""" + + unit: str = Field(description="The title of the unit this objective belongs to") + unit_description: str = Field( + description="Brief summary of the unit scope and learning journey" + ) + subtopic: str = Field(description="The title of the specific subtopic") + module_description: str = Field( + description="Brief summary of what this subtopic covers" + ) + level: str = Field( + description="Bloom's Taxonomy level or learning domain level assigned based on subtopic analysis" + ) + verbs: List[str] = Field( + description="List of three relevant Bloom's verbs suitable for the audience", + min_items=3, + max_items=3, + ) + objective: str = Field( + description="One measurable learning objective in Arabic derived from the subtopic purpose" + ) + + +class LearningObjectivesOutput(BaseModel): + """Schema for the complete learning objectives output""" + + objectives: List[LearningObjective] = Field( + description="Complete list of learning objectives for all subtopics in the curriculum" + ) diff --git a/schemas/outcomes_schema.py b/schemas/outcomes_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..996f34055716d7fb4ea4f11224c812b0d3d04d8b --- /dev/null +++ b/schemas/outcomes_schema.py @@ -0,0 +1,28 @@ +from typing import List +from pydantic import BaseModel, Field + + +class LearningOutcomeItem(BaseModel): + unit: str = Field( + ..., + description="The title of the training unit for which the outcome is designed.", + ) + unit_description: str = Field( + ..., + description="A brief summary describing the scope and purpose of this unit.", + ) + unit_outcome: str = Field( + ..., + description=( + "One concise, measurable Arabic learning outcome derived from the unit description " + "and its subtopics and their purposes. The outcome should be written in مصدر form (infinitive noun), " + "be measurable, realistic, and aligned with Bloom’s taxonomy and DNA methodology." + ), + ) + + +class LearningOutcomesOutput(BaseModel): + outcomes: List[LearningOutcomeItem] = Field( + ..., + description="A list of unit-level Arabic learning outcomes covering all units in the program.", + ) diff --git a/schemas/outliner_schema.py b/schemas/outliner_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..2fe6d272067a685d059491d967274b17dfe66ae4 --- /dev/null +++ b/schemas/outliner_schema.py @@ -0,0 +1,207 @@ +import os +import json +import requests +import time +import re +from pydantic import BaseModel, Field, ValidationError +from typing import List, Dict, Literal, Optional +from .dna_schema import DNAMetadata + + +################==NEW==################ +class Subtopic(BaseModel): + """ + Represents a single subtopic within an educational unit. + + Each subtopic embodies a progressive learning step that moves from + basic understanding toward higher-order thinking such as + application, analysis, or creative synthesis. + """ + + subtopic_number: int = Field( + ..., + title="Subtopic Number", + description="The sequential order of this subtopic within the unit.", + ) + + title: str = Field( + ..., + title="Subtopic Title", + description=( + "The Arabic title of the subtopic inside the unit. " + "Each subtopic defines a distinct conceptual or cognitive step " + "that contributes to the overall unit progression." + ), + ) + + purpose: str = Field( + ..., + title="Subtopic Purpose", + description=( + "A concise statements describing the educational intent of this subtopic — " + "what learners should achieve cognitively (e.g., understanding, applying, analyzing, creating)." + ), + ) + + +############################################################################################################ +class UnitWithAxes(BaseModel): + """ + Represents a curriculum unit that contains multiple learning subtopics. + + Each unit defines a coherent topic focus and consists of number of subtopics, + each representing a structured subtopic or cognitive level that guides + the learner progressively from comprehension to advanced application. + """ + + unit_number: int = Field( + ..., + title="The sequential number of this unit within the overall curriculum (e.g., 1, 2, 3).", + ) + + title: str = Field( + ..., + title="Unit Title", + description="The Arabic title or name of the educational unit, clearly representing its theme or purpose.", + ) + + focus: str = Field( + ..., + title="Unit Focus", + description=( + "A concise overview highlighting the main concepts, skills, and learning outcomes " + "covered in this unit. It describes what learners should understand or achieve after completing it." + "It sets the intellectual direction for the included subtopics." + ), + ) + + subtopics: List[Subtopic] = Field( + ..., + title="Unit Subtopics", + description=( + "A list of coherent Arabic subtopics that make up the unit. " + "Each subtopic represents a progressive cognitive step — from foundational understanding " + "to creative synthesis — ensuring a balanced learning journey." + ), + ) + + +############################################################################################################ +class FullCurriculumStructure(BaseModel): + """ + Represents the complete process of subtopic generation for an educational curriculum. + + This model integrates educational metadata (via DNAMetadata), the conceptual hierarchy + (main and related concepts), and the pedagogical structure (units and subtopics). + It serves as the core structure for AI-driven generation, evaluation, and organization + of educational subtopics aligned with learner needs and content goals. + """ + + Metadata: List["DNAMetadata"] = Field( + ..., + title="List of DNA Metadata Objects", + description=( + "A list of DNAMetadata items, where each item represents a distinct topic or concept " + "along with its domain, audience, content type, and material nature.\n" + "This allows the agent to process multiple curriculum components or educational segments " + "within a single structured input." + ), + ) + + main_concept: str = Field( + ..., + title="Main Concept", + description=( + "The central or core idea around which the curriculum is built. " + "It defines the overarching theme that connects all units and topics together." + ), + ) + + related_concepts: List[str] = Field( + ..., + title="Related Concepts", + description=( + "A set of secondary or supporting ideas that complement or expand upon the main concept. " + "These help the agent establish conceptual links and subtopics within the curriculum." + ), + ) + + units: List[UnitWithAxes] = Field( + ..., + title="Units with Subtopics", + description=( + "A structured list of educational units. Each unit includes its title, " + "conceptual focus, and number of well-defined subtopics that progressively develop the learner’s understanding." + ), + ) + + +############################################################################################################ +# ---- Custom Pydantic parser ---- +class PydanticOutputParser: + """Safely parses model output into a Pydantic schema.""" + + def __init__(self, pydantic_object): + self.model = pydantic_object + + def parse(self, raw_output: str): + # Extract JSON substring between first { and last } + match = re.search(r"\{.*\}", raw_output, re.S) + if not match: + raise ValueError("❌ No JSON object found in model output.") + cleaned = match.group() + try: + return self.model.model_validate_json(cleaned) + except ValidationError as e: + raise ValueError(f"❌ JSON validation failed: {e}") + + +class ValidatedAxis(BaseModel): + """Schema for a validated subtopics""" + + subtopic_number: int = Field( + description="Sequential number of the subtopic within the unit", ge=1 + ) + + title: str = Field( + description="The improved/validated subtopic title in Arabic (4-8 words, clear, focused)" + ) + + purpose: str = Field( + description="The subtopic purpose explaining its educational intent" + ) + status: Literal["kept", "rephrased", "added", "removed"] = Field( + description="Validation status: 'kept' (unchanged), 'rephrased' (edited), 'added' (new), 'removed' (deleted/merged)" + ) + + +class ValidatedUnit(BaseModel): + """Schema for a validated educational unit""" + + unit_number: int = Field( + description="Sequential number of the unit in the curriculum", ge=1 + ) + title: str = Field(description="The title of the unit in Arabic") + focus: str = Field( + description="Description of the unit's main educational focus and objectives" + ) + axes: List[ValidatedAxis] = Field( + description="List of validated subtopics for this unit", min_items=1 + ) + + +class ValidatedCurriculumOutput(BaseModel): + """Schema for the complete validated curriculum output""" + + main_concept: str = Field( + description="The main concept or topic of the entire curriculum" + ) + General_objectives_of_the_plan: List[str] = Field( + description="the main gaols of this educational plan" + ) + validated_units: List[ValidatedUnit] = Field( + description="List of all validated educational units with their subtopics" + ) + + +validated_parser = PydanticOutputParser(pydantic_object=ValidatedCurriculumOutput) diff --git a/schemas/outlines_transformer_schema.py b/schemas/outlines_transformer_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..afa155e4b02fae9edc64670c80d506f6994fa66b --- /dev/null +++ b/schemas/outlines_transformer_schema.py @@ -0,0 +1,16 @@ +from typing import List +from pydantic import BaseModel, Field + + +class Unit(BaseModel): + outcome: str = Field(..., description="مخرج التعلم الخاص بهذه الوحدة") + unit_name: str = Field(..., description="اسم الوحدة أو المحور") + topics: List[str] = Field(..., description="قائمة الموضوعات الفرعية داخل الوحدة") + + +class CourseData(BaseModel): + course_name: str = Field(..., description="اسم المقرر التدريبي") + course_audience: str = Field(..., description="وصف موسع للفئة المستهدفة") + course_description: str = Field(..., description="الوصف العام للمقرر وهدفه") + learning_outcomes: List[str] = Field(..., description="قائمة مخرجات التعلم للمقرر") + units: List[Unit] = Field(..., description="قائمة الوحدات التعليمية وموضوعاتها") diff --git a/schemas/quiz_schema.py b/schemas/quiz_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..14988418ab7d8ace833f48252190c5c5fd0943be --- /dev/null +++ b/schemas/quiz_schema.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Literal, Optional + +class MultipleChoiceQuestion(BaseModel): + question: str + options: List[str] = Field(min_items=4, max_items=4) + answer: str + difficulty: Literal["سهل", "متوسط", "صعب", "صعب جداً"] + explanation: str # شرح الإجابة الصحيحة + topic_question_related_to: str + + +class TrueFalseQuestion(BaseModel): + question: str + answer: str # "صح" أو "خطأ" + difficulty: Literal["سهل", "متوسط", "صعب", "صعب جداً"] + explanation: str # شرح الإجابة + topic_question_related_to: str + + +class QuizOutput(BaseModel): + unit_name: str + multiple_choice: List[MultipleChoiceQuestion] + true_false: List[TrueFalseQuestion] diff --git a/schemas/source_schema.py b/schemas/source_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..8690531eeb57220e5b0aab3dbf22cbb310c9a4f6 --- /dev/null +++ b/schemas/source_schema.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Literal, Optional + + +class SearchResult(BaseModel): + url: str = Field(..., description="Source URL") + title: str = Field(..., description="Title of the result") + content: str = Field(..., description="Short summary of the page") + score: float = Field(..., description="Relevance or confidence score") + + +class SearchEngineOutput(BaseModel): + unit_title: str = Field(..., description="Module or unit name") + subtopic_title: str = Field(..., description="Subtopic within the unit") + query: str = Field(..., description="Executed search query") + results: List[SearchResult] = Field(..., description="List of search results") diff --git a/schemas/unit_details_intro.py b/schemas/unit_details_intro.py new file mode 100644 index 0000000000000000000000000000000000000000..77e78e75c16db42022b6b4f02becce557508a122 --- /dev/null +++ b/schemas/unit_details_intro.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Literal, Optional + +class UnitExpansion(BaseModel): + unit_name: str + # expanded_topics: List[TopicExpansion] + unit_objectives: List[str] + unit_description: str + learning_outcomes: List[str] + + +class Units_details_output(BaseModel): + course_name: str + course_audience: str + expanded_units: List[UnitExpansion] + +#########Intro############# +class UnitIntro(BaseModel): + unit_name: str + expanded_topics: List[str] + unit_introudction: str + + +class Units_Introduction(BaseModel): + course_name: str + course_audience: str + expanded_units: List[UnitIntro] + + +class EvalDetails(BaseModel): + unit_name: str + trainer_instructions: str + eval_questions: List[str] + opening_activity: Optional[str] = Field( + "when it is the first unit put opening activity" + ) + question_after_opening_activity: Optional[str] = Field( + "when it is the first unit put question after opening activity" + ) + + +class Units_eval_details(BaseModel): + course_name: str + course_audience: str + expanded_units: List[EvalDetails] diff --git a/static/new.txt b/static/new.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8474fea525ea93f2996c55be29484b0ff22f7479 --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1,18 @@ +# from .chapter_division import split_json +# from .scraper import web_scraping_tool +# from .tavily import search_engine_tool, is_recent +# from .scraper.scraper_crawlee import WebScrapingCrawleeTool +# from .scraper.scraper_bs4 import WebScrapingToolBS4 +# from .scraper.scraper_built_in import pdf_tool, scraping_tool +# from .validate_url import URLValidatorTool +# from .scraper.no_agent.pdf_extractor import extract_pdf_content +# from .scraper.no_agent.bs4_scraper import scrape_with_bs4 +# from .scraper.no_agent.crawlee_parse_scraper import crawl_parse_url +# from .scraper.no_agent.crawlee_bs_scraper import crawl_bs_url +from .searching_scraping_queries import ( + scrape_course, + SerperExhaustedError, + CURRENT_SERPER_INDEX, +) +from .quiz_runner import generate_quiz_for_course +from .ocr__units_service import extract_text_from_pdf diff --git a/tools/chapter_division.py b/tools/chapter_division.py new file mode 100644 index 0000000000000000000000000000000000000000..4690d3115ae64ad64a3f58548978387ad4e33fba --- /dev/null +++ b/tools/chapter_division.py @@ -0,0 +1,46 @@ +# import os +# import json +# import re +# from modules import outliner_dir, output_dir + +# # ---------- Helper: sanitize file names ---------- +# def safe_filename(name: str) -> str: +# return re.sub(r'[\\/*?:"<>|]', "", name).strip() + + +# # ---------- Main function ---------- +# def split_json(input_file: str, output_dir: str): +# # Load JSON +# with open(input_file, "r", encoding="utf-8") as f: +# data = json.load(f) + +# main_headings = data.get("main_headings", []) +# sub_headings = data.get("sub_headings", []) +# sub_sub_headings = data.get("sub_sub_headings", None) + +# # Ensure output folder exists +# os.makedirs(output_dir, exist_ok=True) + +# for idx, main in enumerate(main_headings): +# headers = sub_headings[idx] if idx < len(sub_headings) else [] + +# # لو فيه sub_sub_headings نجيبها، لو مش فيه نخليها null +# if sub_sub_headings and idx < len(sub_sub_headings): +# subs = sub_sub_headings[idx] +# else: +# subs = None + +# section = {"headers": headers, "sub_sub_headings": subs} + +# file_name = safe_filename(main) + ".json" +# file_path = os.path.join(output_dir, file_name) + +# with open(file_path, "w", encoding="utf-8") as out_f: +# json.dump(section, out_f, ensure_ascii=False, indent=2) + +# print(f"✅ Saved {file_name}") + + +# # ---------- Example usage ---------- +# if __name__ == "__main__": +# split_json("/content/ai_agent_output/step_1_outlines.json", "output_sections") diff --git a/tools/ocr__units_service.py b/tools/ocr__units_service.py new file mode 100644 index 0000000000000000000000000000000000000000..43de97f38a00c66353b7c5978d3aee59911cb1ea --- /dev/null +++ b/tools/ocr__units_service.py @@ -0,0 +1,27 @@ +import base64 +from mistralai import Mistral +import os + +client = Mistral(api_key=os.environ["MISTRAL_API_KEY"]) + + +def extract_text_from_pdf(pdf_path: str) -> str: + with open(pdf_path, "rb") as f: + pdf_bytes = f.read() + + b64 = base64.b64encode(pdf_bytes).decode("utf-8") + + resp = client.ocr.process( + model="mistral-ocr-latest", + document={ + "type": "document_url", + "document_url": f"data:application/pdf;base64,{b64}", + }, + include_image_base64=False, + ) + + pages_text = [] + for page in resp.pages: + pages_text.append(page.markdown) + + return "\n\n".join(pages_text) diff --git a/tools/quiz_runner.py b/tools/quiz_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..ee7c20d932d5227e261b33d661abc114c04edccf --- /dev/null +++ b/tools/quiz_runner.py @@ -0,0 +1,112 @@ +import time +from typing import Dict, Any, List +from agents.content_phase import generate_quiz_agent, generate_quiz_task +from crewai import Crew, Process + +# هنا أنت أصلاً مجهّز quiz_agent و generate_quiz_task +# from your_agents_module import quiz_agent, generate_quiz_task + +MAX_RETRIES = 3 +RETRY_DELAY = 3 # ثواني بين كل محاولة + +# ====================================================== +# 1️⃣ Clean Topics Function (نفس اللي عندك تقريبًا) +# ====================================================== + + +def clean_topics(topics: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + cleaned_topics = [] + + for topic in topics: + cleaned_subtopics = [] + + for sub in topic.get("subtopics", []): + cleaned_subtopics.append( + {"title": sub.get("title"), "content": sub.get("generated_content")} + ) + + cleaned_topics.append( + {"topic_title": topic.get("topic_title"), "subtopics": cleaned_subtopics} + ) + + return cleaned_topics + + + +def generate_quiz_for_course( + course_data: Dict[str, Any], logger=print +) -> Dict[str, Any]: + """ + course_data شكلها زي: + { + "course_name": "...", + "course_audience": "...", + "results": [ + { + "unit_name": "...", + "unit_outcome": "...", + "topics": [...] + }, + ... + ] + } + """ + all_units_quizzes = [] + agent = generate_quiz_agent() + task = generate_quiz_task(agent) + crew_3 = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential + ) + + + for unit in course_data["results"]: + unit_name = unit["unit_name"] + cleaned_topics = clean_topics(unit["topics"]) + + success = False + + for attempt in range(1, MAX_RETRIES + 1): + try: + logger(f"🧠 Unit '{unit_name}' | Attempt {attempt} started") + + result = crew_3.kickoff( + inputs={ + "course_name": course_data["course_name"], + "course_audience": course_data["course_audience"], + "unit_name": unit_name, + "unit_outcome": unit["unit_outcome"], + "topics": cleaned_topics, + } + ) + + if hasattr(result, "dict"): + all_units_quizzes.append(result.json_dict) + else: + all_units_quizzes.append(result) + + logger(f"✅ Unit '{unit_name}' succeeded on attempt {attempt}") + success = True + break + + except Exception as e: + logger( + f"❌ Unit '{unit_name}' failed on attempt {attempt} | Error: {str(e)}" + ) + if attempt < MAX_RETRIES: + logger(f"🔁 Retrying unit '{unit_name}' after {RETRY_DELAY}s...") + time.sleep(RETRY_DELAY) + + if not success: + logger( + f"🚨 Unit '{unit_name}' FAILED after {MAX_RETRIES} attempts — skipped." + ) + + final_output = { + "course_name": course_data["course_name"], + "course_audience": course_data["course_audience"], + "units": all_units_quizzes, + } + + return final_output diff --git a/tools/scraper/no_agent/bs4_scraper.py b/tools/scraper/no_agent/bs4_scraper.py new file mode 100644 index 0000000000000000000000000000000000000000..cdeefdaf053a24d5a11ab4ee177da29dc53d0827 --- /dev/null +++ b/tools/scraper/no_agent/bs4_scraper.py @@ -0,0 +1,41 @@ +import requests +from bs4 import BeautifulSoup + + +def scrape_with_bs4(url: str) -> dict: + try: + + # HTML case + response = requests.get(url, timeout=10) + soup = BeautifulSoup(response.text, "html.parser") + + # text = soup.get_text(separator="\n").strip() + # imgs= soup.find_all + + title = soup.title.string.strip() if soup.title else "No title" + text = " ".join([p.get_text(strip=True) for p in soup.find_all("p")]) + img_urls = [img["src"] for img in soup.find_all("img", src=True)] + video_urls = [vid["src"] for vid in soup.find_all("video", src=True)] + audio_urls = [aud["src"] for aud in soup.find_all("audio", src=True)] + pdf_urls = [ + a["href"] + for a in soup.find_all("a", href=True) + if a["href"].endswith(".pdf") + ] + + return { + "page_url": url, + "title": title, + "content": text, + "img_url": img_urls, + "video_url": video_urls, + "audio_url": audio_urls, + "pdf_url": pdf_urls, + "agent_recommendation_rank": 4.2, + "agent_recommendation_notes": "Scraped successfully using Crawlee + BeautifulSoupCrawler.", + "header": "Web Scraping Test", + "sub_header": "Crawlee Version", + } + + except Exception as e: + return {"url": url, "error": str(e)} diff --git a/tools/scraper/no_agent/crawlee_bs_scraper.py b/tools/scraper/no_agent/crawlee_bs_scraper.py new file mode 100644 index 0000000000000000000000000000000000000000..e421a624417b7d65346bb6904b044ccc1ecb1f8b --- /dev/null +++ b/tools/scraper/no_agent/crawlee_bs_scraper.py @@ -0,0 +1,44 @@ +import asyncio +from crawlee.crawlers import BeautifulSoupCrawler, BeautifulSoupCrawlingContext + +async def crawl_bs_url(url: str): + """Scrape a single URL using Crawlee + BeautifulSoupCrawler and return one record""" + + crawler = BeautifulSoupCrawler(max_requests_per_crawl=1) + result_container = {"data": None} + + + + @crawler.router.default_handler + async def handle_page(context: BeautifulSoupCrawlingContext): + soup = context.soup + + title = soup.title.string.strip() if soup.title else "No title" + text = " ".join([p.get_text(strip=True) for p in soup.find_all("p")]) + + img_urls = [img["src"] for img in soup.find_all("img", src=True)] + video_urls = [v["src"] for v in soup.find_all("video", src=True)] + audio_urls = [a["src"] for a in soup.find_all("audio", src=True)] + pdf_urls = [ + a["href"] + for a in soup.find_all("a", href=True) + if a["href"].lower().endswith(".pdf") + ] + + record = { + "page_url": context.request.url, + "title": title, + "content": text, + "img_url": img_urls, + "video_url": video_urls, + "audio_url": audio_urls, + "pdf_url": pdf_urls, + "agent_recommendation_rank": 4.2, + "agent_recommendation_notes": "Scraped successfully using Crawlee + BeautifulSoupCrawler.", + } + + result_container["data"] = record + await context.push_data(record) + + await crawler.run([url]) + return result_container["data"] diff --git a/tools/scraper/no_agent/crawlee_parse_scraper.py b/tools/scraper/no_agent/crawlee_parse_scraper.py new file mode 100644 index 0000000000000000000000000000000000000000..03d91fad9f3e23f721fd90f1aa078947540afa3d --- /dev/null +++ b/tools/scraper/no_agent/crawlee_parse_scraper.py @@ -0,0 +1,82 @@ +import asyncio +from crawlee.crawlers import ParselCrawler, ParselCrawlingContext +from urllib.parse import urljoin + + +async def crawl_parse_url(url: str): + """Scrape a single URL using Crawlee + ParselCrawler and return one record""" + + result_container = {"data": None} + + crawler = ParselCrawler(max_requests_per_crawl=1) + + + + @crawler.router.default_handler + async def handle_page(ctx: ParselCrawlingContext): + selector = ctx.selector + req_url = ctx.request.url + + # ---------- Title ---------- + title = selector.xpath("//title/text()").get() or "No title" + + # ---------- Extract page text ---------- + text = selector.xpath("string(//body)").get() + text = text.strip() if text else "" + + # ---------- Multimedia ---------- + img_urls = [ + urljoin(req_url, src) for src in selector.xpath("//img/@src").getall() + ] + + video_urls = [ + urljoin(req_url, src) for src in selector.xpath("//video/@src").getall() + ] + [ + urljoin(req_url, src) + for src in selector.xpath("//source[@type='video/mp4']/@src").getall() + ] + + audio_urls = [ + urljoin(req_url, src) for src in selector.xpath("//audio/@src").getall() + ] + [ + urljoin(req_url, src) + for src in selector.xpath("//source[@type='audio/mpeg']/@src").getall() + ] + + pdf_urls = [ + urljoin(req_url, href) + for href in selector.xpath("//a[contains(@href, '.pdf')]/@href").getall() + ] + + # ---------- Final structured result ---------- + record = { + "page_url": req_url, + "title": title, + "content": text, + "img_url": img_urls, + "video_url": video_urls, + "audio_url": audio_urls, + "pdf_url": pdf_urls, + "agent_recommendation_rank": 4.2, + "agent_recommendation_notes": "Scraped successfully using Crawlee + ParselCrawler.", + "header": "Web Scraping Test", + "sub_header": "Crawlee Version", + } + + result_container["data"] = record + await ctx.push_data(record) + + await crawler.run([url]) + + # fallback + return result_container["data"] or { + "page_url": url, + "title": "No title", + "content": "", + "img_url": [], + "video_url": [], + "audio_url": [], + "pdf_url": [], + "agent_recommendation_rank": 4.2, + "agent_recommendation_notes": "Scraped successfully using Crawlee + ParselCrawler.", + } diff --git a/tools/scraper/no_agent/pdf_extractor.py b/tools/scraper/no_agent/pdf_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..bea25d261a3fee52b535dd2aa1cb6a5463325ae7 --- /dev/null +++ b/tools/scraper/no_agent/pdf_extractor.py @@ -0,0 +1,18 @@ +import requests +import io +from PyPDF2 import PdfReader + +def extract_pdf_content(url: str) -> str: + try: + pdf_bytes = requests.get(url, timeout=10).content + pdf_file = io.BytesIO(pdf_bytes) + reader = PdfReader(pdf_file) + + text = "" + for page in reader.pages: + text += page.extract_text() or "" + + return text.strip() + + except Exception as e: + return f"PDF_ERROR: {str(e)}" diff --git a/tools/scraper/scraper_bs4.py b/tools/scraper/scraper_bs4.py new file mode 100644 index 0000000000000000000000000000000000000000..5ce413180d9b30e757c55f289b7f986db93c1447 --- /dev/null +++ b/tools/scraper/scraper_bs4.py @@ -0,0 +1,88 @@ +from crewai.tools import BaseTool +from typing import Literal +import requests +from bs4 import BeautifulSoup +import fitz # PyMuPDF +from urllib.parse import urlparse +import os + + +class WebScrapingToolBS4(BaseTool): + name: Literal["web_scraping_tool"] + description: str = ( + "Scrapes text and media from a webpage or PDF. " + "Returns a dictionary containing: page_url, title, content, img_url, " + "video_url, audio_url, pdf_url." + ) + + def extract_pdf_text(self, pdf_url): + try: + response = requests.get(pdf_url, timeout=15) + temp_path = "temp_scraped.pdf" + + with open(temp_path, "wb") as f: + f.write(response.content) + + text = "" + with fitz.open(temp_path) as doc: + for page in doc: + text += page.get_text() + + os.remove(temp_path) + return text.strip() + + except Exception as e: + return f"Error extracting PDF text: {str(e)}" + + def _run(self, url: str) -> dict: + """Synchronous execution of the scraping tool.""" + try: + parsed = urlparse(url) + + # ========== PDF Mode ========== + if parsed.path.lower().endswith(".pdf"): + text = self.extract_pdf_text(url) + return { + "page_url": url, + "title": os.path.basename(parsed.path), + "content": text, + "img_url": [], + "video_url": [], + "audio_url": [], + "pdf_url": [url], + } + + # ========== HTML Mode ========== + response = requests.get(url, timeout=15) + soup = BeautifulSoup(response.text, "html.parser") + + title = soup.title.string if soup.title else "Untitled" + + paragraphs = [p.get_text(" ", strip=True) for p in soup.find_all("p")] + content = "\n".join(paragraphs) + + images = [img["src"] for img in soup.find_all("img", src=True)] + videos = [v["src"] for v in soup.find_all("video", src=True)] + audios = [a["src"] for a in soup.find_all("audio", src=True)] + pdfs = [ + a["href"] + for a in soup.find_all("a", href=True) + if a["href"].lower().endswith(".pdf") + ] + + return { + "page_url": url, + "title": title, + "content": content, + "img_url": images, + "video_url": videos, + "audio_url": audios, + "pdf_url": pdfs, + } + + except Exception as e: + return {"error": str(e), "page_url": url} + + async def _arun(self, url: str) -> dict: + """Async version (calls sync internally).""" + return self._run(url) diff --git a/tools/scraper/scraper_built_in.py b/tools/scraper/scraper_built_in.py new file mode 100644 index 0000000000000000000000000000000000000000..277a28bd11c06e70c22315ae8799ddd634aa6131 --- /dev/null +++ b/tools/scraper/scraper_built_in.py @@ -0,0 +1,4 @@ +from crewai_tools import PDFSearchTool, ScrapeWebsiteTool + +pdf_tool = PDFSearchTool() +scraping_tool = ScrapeWebsiteTool() \ No newline at end of file diff --git a/tools/scraper/scraper_crawlee.py b/tools/scraper/scraper_crawlee.py new file mode 100644 index 0000000000000000000000000000000000000000..ac9bf271187d0b3f6a175eb32b010c41c090e652 --- /dev/null +++ b/tools/scraper/scraper_crawlee.py @@ -0,0 +1,90 @@ +# from crewai.tools import BaseTool +# from typing import Literal + +# from crawlee.crawlers import PlaywrightCrawler, PlaywrightCrawlingContext +# from crawlee.http_clients import HttpxHttpClient +# import fitz +# import asyncio +# import tempfile +# import os +# from urllib.parse import urlparse + + +# class WebScrapingCrawleeTool(BaseTool): +# name: Literal["web_scraping_crawlee_tool"] +# description: str = ( +# "Scrapes Arabic/English content from webpages using Crawlee PlaywrightCrawler " +# "or extracts readable text from PDF files. Returns a dictionary: " +# "page_url, title, content, img_url, video_url, audio_url, pdf_url." +# ) + +# async def scrape_with_crawlee(self, target_url): +# crawler = PlaywrightCrawler() +# extracted_data = {} + +# @crawler.router.default_handler +# async def default_handler(context: PlaywrightCrawlingContext) -> None: +# page = context.page +# title = await page.title() +# content = await page.content() +# images = await page.eval_on_selector_all( +# "img", "els => els.map(e => e.src)" +# ) +# videos = await page.eval_on_selector_all( +# "video", "els => els.map(e => e.src)" +# ) +# audios = await page.eval_on_selector_all( +# "audio", "els => els.map(e => e.src)" +# ) +# pdfs = await page.eval_on_selector_all( +# "a[href$='.pdf']", "els => els.map(e => e.href)" +# ) + +# extracted_data.update( +# { +# "title": title, +# "content": content, +# "img_url": images, +# "video_url": videos, +# "audio_url": audios, +# "pdf_url": pdfs, +# } +# ) + +# await crawler.run([target_url]) +# return extracted_data + +# def extract_pdf_text(self, pdf_url): +# http = HttpxHttpClient() +# response = http.get(pdf_url) +# with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp: +# tmp.write(response.content) +# tmp_path = tmp.name +# text = "" +# with fitz.open(tmp_path) as doc: +# for page in doc: +# text += page.get_text("text") +# os.remove(tmp_path) +# return text.strip() + +# def _run(self, url: str) -> dict: +# try: +# parsed = urlparse(url) +# if parsed.path.lower().endswith(".pdf"): +# content = self.extract_pdf_text(url) +# return { +# "page_url": url, +# "title": os.path.basename(parsed.path), +# "content": content, +# "img_url": [], +# "video_url": [], +# "audio_url": [], +# "pdf_url": [url], +# } +# scraped_data = asyncio.run(self.scrape_with_crawlee(url)) +# return {"page_url": url, **scraped_data} +# except Exception as e: +# return {"error": str(e), "page_url": url} + +# async def _arun(self, url: str) -> dict: +# return self._run(url) diff --git a/tools/searching_scraping_queries.py b/tools/searching_scraping_queries.py new file mode 100644 index 0000000000000000000000000000000000000000..c27a1a2352eaee567177554b573b22471c96abc4 --- /dev/null +++ b/tools/searching_scraping_queries.py @@ -0,0 +1,363 @@ +# scraper.py +import json +import time +import io +import threading +import http.client +import requests +from bs4 import BeautifulSoup +from PyPDF2 import PdfReader +from concurrent.futures import ThreadPoolExecutor + +# -------- CONFIG ---------- +import os + +# SERPER_API_KEY = os.getenv("SERPER_API_KEY", "") + +SERPER_API_KEYS = [ + os.getenv("SERPER_API_KEY"), + os.getenv("SERPER_API_KEY_1"), + os.getenv("SERPER_API_KEY_2"), + os.getenv("SERPER_API_KEY_3"), + os.getenv("SERPER_API_KEY_335"), + os.getenv("SERPER_API_KEY_zoe"), + os.getenv("SERPER_API_KEY_38732"), + os.getenv("SERPER_API_KEY_849"), +] + +SERPER_API_KEYS = [k for k in SERPER_API_KEYS if k] + +CURRENT_SERPER_INDEX = 0 +SERPER_LOCK = threading.Lock() + + +class SerperExhaustedError(Exception): + pass + + +def rotate_serper_key(): + global CURRENT_SERPER_INDEX + with SERPER_LOCK: + CURRENT_SERPER_INDEX += 1 + if CURRENT_SERPER_INDEX >= len(SERPER_API_KEYS): + raise SerperExhaustedError( + "❌ SERPER APIs exhausted. Please contact the developer." + ) + print(f"🔁 Switching to SERPER API #{CURRENT_SERPER_INDEX}") + + +BAD_KEYWORDS = [ + "facebook", + "youtube", + "tiktok", + "instagram", + "linkedin", + "x.com", + "twitter", + "pinterest", + "snapchat", + "blog", + "course", +] + +FAST_MODE = True +MAX_WORKERS = 15 +SCRAPE_CHAR_LIMIT = 2000 +REQUEST_TIMEOUT = 8 +PDF_READ_LIMIT = 2000 +SERP_MAX_RESULTS = 3 +RETRIES = 1 + +# -------- shared resources ---------- +session = requests.Session() +lock = threading.Lock() +GLOBAL_LINK_COUNTER = 0 + + +# -------- helpers ---------- +def is_good_source(url: str) -> bool: + if not url: + return False + u = url.lower() + return not any(bad in u for bad in BAD_KEYWORDS) + + +def is_valid_url(url: str) -> bool: + if FAST_MODE: + return url.startswith("http") + try: + r = session.head(url, timeout=3, allow_redirects=True) + return r.status_code in (200, 301, 302) + except: + return False + + +def extract_pdf_content(url: str) -> str: + try: + r = session.get(url, timeout=REQUEST_TIMEOUT) + pdf_bytes = r.content + reader = PdfReader(io.BytesIO(pdf_bytes)) + text = "" + for page in reader.pages: + text += page.extract_text() or "" + if len(text) >= PDF_READ_LIMIT: + break + return text.strip()[:SCRAPE_CHAR_LIMIT] + except: + return "" + + +def scrape_page(url, retries=RETRIES, limit=SCRAPE_CHAR_LIMIT): + for _ in range(retries): + try: + if url.lower().endswith(".pdf"): + return extract_pdf_content(url)[:limit] + r = session.get(url, timeout=REQUEST_TIMEOUT) + soup = BeautifulSoup(r.text, "html.parser") + text = soup.get_text(separator="\n", strip=True) + return text[:limit] + except: + time.sleep(0.3) + return "" + + +# def search_serper(query, max_results=SERP_MAX_RESULTS): +# if not SERPER_API_KEY: +# # في البروداكشن خليه raise Exception أحسن +# return [] + +# try: +# conn = http.client.HTTPSConnection("google.serper.dev") +# payload = json.dumps({"q": query, "page": 1}) +# headers = {"X-API-KEY": SERPER_API_KEY, "Content-Type": "application/json"} +# conn.request("POST", "/search", payload, headers) +# res = conn.getresponse() +# data = res.read() +# response_json = json.loads(data.decode("utf-8")) +# except: +# return [] + +# results = [] +# seen = set() +# if "organic" in response_json: +# for r in response_json["organic"]: +# url = r.get("link") +# if not url or url in seen: +# continue +# if not is_good_source(url): +# continue +# if not is_valid_url(url): +# continue +# seen.add(url) +# position = r.get("position", 100) +# score = round(1 / max(position, 1), 4) +# results.append( +# { +# "url": url, +# "title": r.get("title"), +# "content": r.get("snippet"), +# "score": score, +# } +# ) +# if len(results) >= max_results: +# break +# return results + + +def search_serper(query, max_results=SERP_MAX_RESULTS): + global CURRENT_SERPER_INDEX + + attempts = 0 + + while attempts < len(SERPER_API_KEYS): + api_key = SERPER_API_KEYS[CURRENT_SERPER_INDEX] + + try: + conn = http.client.HTTPSConnection( + "google.serper.dev", timeout=REQUEST_TIMEOUT + ) + payload = json.dumps({"q": query, "page": 1}) + headers = { + "X-API-KEY": api_key, + "Content-Type": "application/json", + } + + conn.request("POST", "/search", payload, headers) + res = conn.getresponse() + status = res.status + data = res.read() + + # ❌ API Key خلصت أو مش صالحة + if status in (401, 403): + rotate_serper_key() + attempts += 1 + continue + + # ❌ أي خطأ تاني + if status != 200: + return [] + + response_json = json.loads(data.decode("utf-8")) + break + + except SerperExhaustedError: + raise + + except Exception: + rotate_serper_key() + attempts += 1 + + else: + raise SerperExhaustedError( + "❌ SERPER APIs exhausted. Please contact the developer." + ) + + # -------- normal parsing ---------- + results = [] + seen = set() + + for r in response_json.get("organic", []): + url = r.get("link") + if not url or url in seen: + continue + if not is_good_source(url): + continue + if not is_valid_url(url): + continue + + seen.add(url) + position = r.get("position", 100) + score = round(1 / max(position, 1), 4) + + results.append( + { + "url": url, + "title": r.get("title"), + "content": r.get("snippet"), + "score": score, + } + ) + + if len(results) >= max_results: + break + + return results + + +def process_link(link_data): + url = link_data["url"] + idx = link_data.get("counter", -1) + print(f"🕷️ [SCRAPE #{idx}] START {url}") + t0 = time.perf_counter() + page_text = scrape_page(url) + duration = round(time.perf_counter() - t0, 2) + print(f"🕷️ [SCRAPE #{idx}] DONE in {duration}s — chars: {len(page_text)}") + link_data["scraped_content"] = page_text if page_text else "ERROR: EMPTY" + # summary_result و raw_result في الكيس دي نفس الحاجة + return link_data, dict(link_data) + + +def safe_append(summary_result, raw_result, final_output_summary, final_output_raw): + u = summary_result["unit_idx"] + t = summary_result["topic_idx"] + s = summary_result["sub_idx"] + with lock: + final_output_summary["units"][u]["topics"][t]["subtopics"][s]["results"].append( + summary_result + ) + final_output_raw["units"][u]["topics"][t]["subtopics"][s]["results"].append( + raw_result + ) + + +def scrape_course(course: dict): + """ + course هنا هو نفس الستركشر اللي كان جاي من JSON file + ويرجع dicts: (final_output_summary, final_output_raw) + """ + global GLOBAL_LINK_COUNTER + GLOBAL_LINK_COUNTER = 0 + + final_output_summary = { + "course_name": course["course_name"], + "course_audience": course["course_audience"], + "units": [], + } + final_output_raw = { + "course_name": course["course_name"], + "course_audience": course["course_audience"], + "units": [], + } + + all_tasks = [] + + for unit_idx, unit in enumerate(course["expanded_units"]): + unit_obj_sum = { + "unit_name": unit["unit_name"], + "outcome": unit["outcome"], + "topics": [], + } + unit_obj_raw = { + "unit_name": unit["unit_name"], + "outcome": unit["outcome"], + "topics": [], + } + + for topic_idx, topic in enumerate(unit["topics"]): + topic_sum = {"title": topic["topic_name"], "subtopics": []} + topic_raw = {"title": topic["topic_name"], "subtopics": []} + + for sub_idx, sub in enumerate(topic["subtopics"]): + topic_sum["subtopics"].append( + { + "title": sub["title"], + "description": sub.get("description", ""), + "queries": sub["queries"], + "results": [], + } + ) + topic_raw["subtopics"].append( + { + "title": sub["title"], + "description": sub.get("description", ""), + "queries": sub["queries"], + "results": [], + } + ) + + # لكل query في الـ subtopic + for query_text in sub["queries"]: + print( + f"\n🔍 [SEARCH] U{unit_idx} T{topic_idx} S{sub_idx} — Query: {query_text}" + ) + results = search_serper(query_text, max_results=SERP_MAX_RESULTS) + + for r in results: + with lock: + GLOBAL_LINK_COUNTER += 1 + r["counter"] = GLOBAL_LINK_COUNTER + r["query"] = query_text + r["unit_idx"] = unit_idx + r["topic_idx"] = topic_idx + r["sub_idx"] = sub_idx + print(f" ➕ [ADD LINK #{r['counter']}] {r['url']}") + all_tasks.append(r) + + unit_obj_sum["topics"].append(topic_sum) + unit_obj_raw["topics"].append(topic_raw) + + final_output_summary["units"].append(unit_obj_sum) + final_output_raw["units"].append(unit_obj_raw) + + print( + f"\n🚀 START PARALLEL SCRAPING — Total Links: {len(all_tasks)} — Workers: {MAX_WORKERS}\n" + ) + + def worker(link_data): + summary_result, raw_result = process_link(link_data) + safe_append(summary_result, raw_result, final_output_summary, final_output_raw) + + with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + list(executor.map(worker, all_tasks)) + + return final_output_summary, final_output_raw diff --git a/tools/tavily.py b/tools/tavily.py new file mode 100644 index 0000000000000000000000000000000000000000..b4b76a0e6b63718295c63cb9686750e55c7dca29 --- /dev/null +++ b/tools/tavily.py @@ -0,0 +1,64 @@ +# import os +# from tavily import TavilyClient +# from crewai.tools import tool +# from modules import TRUSTED_SITES + +# search_tool = TavilyClient(api_key=os.environ["TAVILY_API_KEY"]) + + +# def is_recent(result, min_year=2021): +# """Validate result date is >= min_year if available.""" +# pub_date = result.get("published_date") +# if not pub_date: +# return False # skip if no date +# try: +# year = int(pub_date.split("-")[0]) +# return year >= min_year +# except Exception: +# return False + + +# @tool +# def search_engine_tool(query: str): +# """Execute a focused web search for the given query. + +# - Prioritize **scholarly resources** (academic papers, books, journals, trusted educational sites). +# - Filters results to only those published/updated >= 2021. +# - Include **Arabic sources** when contextually relevant, but prioritize **English academic sources**. +# - Retrieve only **diverse and high-quality** results (avoid spam, low-quality blogs, or purely commercial pages). +# - For each result, ensure useful metadata is returned: +# * url +# * title +# * short content/summary +# * relevance score +# - Use a maximum of 5 results per query to keep output precise and useful. +# """ +# # Step 1: Normal Tavily search +# # tavily_results = search_tool.search(query) + +# site_query = f"{query} after:2020" +# tavily_results = search_tool.search(query=site_query, max_results=10) +# # Step 2: Force search inside trusted domains +# trusted_results = [] +# for site in TRUSTED_SITES: +# site_query = f"site:{site} {query}" +# try: +# res = search_tool.search(site_query) +# if res and "results" in res: +# trusted_results.extend(res["results"]) +# except Exception as e: +# print(f"Skipping {site}: {e}") + +# # Step 3: Merge + deduplicate +# all_results = tavily_results.get("results", []) + trusted_results +# seen = set() +# filtered = [] +# for r in all_results: +# if r["url"] not in seen: +# seen.add(r["url"]) +# filtered.append(r) + +# # Step 4: Limit to 5 best results +# final_results = sorted(filtered, key=lambda x: x.get("score", 0), reverse=True)[:5] + +# return {"results": final_results} diff --git a/tools/validate_url.py b/tools/validate_url.py new file mode 100644 index 0000000000000000000000000000000000000000..d4b078207c6f5fd6d77b373d5ddba11a472a5477 --- /dev/null +++ b/tools/validate_url.py @@ -0,0 +1,29 @@ +from crewai.tools import BaseTool +from typing import Literal + +import requests + +class URLValidatorTool(BaseTool): + name: Literal["url_validator"] + description : str = "Validate whether a given URL is reachable and returns a valid HTTP status." + + def _run(self, url: str) -> dict: + """Synchronous tool execution""" + try: + response = requests.head(url, allow_redirects=True, timeout=15) + return { + "url": url, + "status_code": response.status_code, + "ok": response.status_code < 400 + } + except Exception as e: + return { + "url": url, + "status_code": None, + "ok": False, + "error": str(e) + } + + async def _arun(self, url: str) -> dict: + """Asynchronous tool execution (not used but required)""" + return self._run(url)