clawed 2.5.1__tar.gz → 2.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clawed-2.5.1 → clawed-2.5.3}/PKG-INFO +1 -1
- {clawed-2.5.1 → clawed-2.5.3}/clawed/__init__.py +1 -1
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/generate.py +41 -2
- clawed-2.5.3/clawed/compile_game.py +460 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/export_docx.py +17 -3
- {clawed-2.5.1 → clawed-2.5.3}/clawed/export_pptx.py +428 -143
- {clawed-2.5.1 → clawed-2.5.3}/clawed/model_router.py +12 -13
- {clawed-2.5.1 → clawed-2.5.3}/clawed/multi_agent.py +2 -2
- clawed-2.5.3/clawed/review_output.py +253 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/slide_images.py +285 -52
- {clawed-2.5.1 → clawed-2.5.3}/pyproject.toml +1 -1
- clawed-2.5.1/clawed/compile_game.py +0 -275
- {clawed-2.5.1 → clawed-2.5.3}/.gitignore +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/LICENSE +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/README.md +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/__main__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/_legacy_gateway.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/approvals.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/autonomy.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/context.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/core.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/custom_tools.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/drive/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/drive/auth.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/drive/client.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/fake_llm.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/loop.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/curriculum.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/curriculum_kb.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/embeddings.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/episodes.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/identity.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/loader.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/preferences.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/planner.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/prompt.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/scheduler.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/base.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/configure_profile.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/curriculum_map.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_create_doc.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_create_slides.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_list.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_organize.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_read.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_upload.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/export_document.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/gap_analysis.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_assessment.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_lesson.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_lesson_bundle.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_materials.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_unit.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/ingest_materials.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/parent_comm.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/read_heartbeat.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/read_workspace.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/request_approval.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/schedule_task.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/search_lessons.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/search_my_materials.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/search_standards.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/student_insights.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/sub_packet.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/switch_model.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/update_soul.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/analytics.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/deps.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/chat.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/export.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/feedback.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/gateway_chat.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/generate.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/ingest.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/lessons.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/school.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/settings.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/tools.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/server.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/static/app.js +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/static/style.css +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/static/widget.js +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/analytics.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/base.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/dashboard.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/generate.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/index.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/lesson.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/profile.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/settings.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/stats.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/students.html +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/assessment.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/asset_registry.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/async_utils.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/auth/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/auth/google_auth.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/bot_state.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/chat.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/cli.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/cli_chat.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/_helpers.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/bot.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/config.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/config_llm.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/config_profile.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/export.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/game.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/generate_assessment.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/generate_unit.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/queue.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/schedule_cmd.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/sub.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/train.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/workspace_cmd.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/compile_slides.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/compile_student.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/compile_teacher.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/config.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/corpus.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/curriculum_map.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/database.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_assessment.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_formative_assessment.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_lesson_materials.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_lesson_science_g6.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_lesson_social_studies_g8.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_master_content.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_pacing_guide.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_quiz.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_rubric.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_unit_plan.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_year_map.json +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/differentiation.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/doc_export.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/drive.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/evaluation.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/export_handout.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/export_markdown.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/export_pdf.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/export_templates.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/export_theme.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/exporter.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/failure_codes.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/feedback.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/formats/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/formats/flipchart.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/formats/notebook.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/formats/xbk.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/gateway.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/gateway_response.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/generation.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/generation_report.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/export.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/feedback.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/gaps.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/generate.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/ingest.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/misc.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/onboard.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/schedule.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/standards.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/hermes_plugin.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/image_pipeline.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/improver.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/ingestor.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/io.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/lesson.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/llm.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/master_content.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/materials.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/mcp_server.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/memory_engine.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/models.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/onboarding.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/openclaw_plugin.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/parent_comm.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/persona.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/persona_evolution.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/planner.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/504_accommodations.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/admin_lesson_plan.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/assessment.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/curriculum_gaps.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/dbq_assessment.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/differentiation.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/formative_assessment.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/iep_modification.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/lesson_plan.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/master_content.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/multi_agent_researcher.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/multi_agent_reviewer.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/pacing_guide.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/parent_note.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/persona_extract.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/quiz.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/rubric.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/student_packet.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/sub_packet.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/summative_assessment.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/tiered_assignments.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/unit_plan.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/worksheet.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/year_map.txt +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/quality.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/reading_report.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/router.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/sanitize.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/scheduler.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/school.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/search.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/art.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/base.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/computer_science.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/ela.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/foreign_language.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/history.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/library.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/math.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/music.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/physical_education.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/science.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/social_studies.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/special_education.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/standards.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/state.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/state_standards.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/student_bot.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/student_cli.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/student_telegram_bot.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/sub_packet.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/task_queue.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/templates_lib.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/tools.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/cli.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/hermes.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/openclaw.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/student_telegram.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/telegram.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/web.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/tui.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/tui_chat.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/validation.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/voice.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/voice_check.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/clawed/workspace.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/eduagent/__init__.py +0 -0
- {clawed-2.5.1 → clawed-2.5.3}/eduagent/_compat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clawed
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.3
|
|
4
4
|
Summary: Free AI lesson planner for teachers. Learns your teaching voice from your curriculum files and generates complete lesson packages. Open source, runs locally, no subscription.
|
|
5
5
|
Project-URL: Homepage, https://sirhanmacx.github.io/Claw-ED
|
|
6
6
|
Project-URL: Documentation, https://github.com/SirhanMacx/Claw-ED#readme
|
|
@@ -17,7 +17,7 @@ if hasattr(sys.stderr, "reconfigure"):
|
|
|
17
17
|
except Exception:
|
|
18
18
|
pass
|
|
19
19
|
|
|
20
|
-
__version__ = "2.5.
|
|
20
|
+
__version__ = "2.5.3"
|
|
21
21
|
__author__ = "Jon Maccarello & Claw-ED contributors"
|
|
22
22
|
__description__ = "Personal AI teaching agent. Learns your voice, works while you sleep."
|
|
23
23
|
|
|
@@ -306,7 +306,10 @@ def lesson(
|
|
|
306
306
|
False, "--game/--no-game",
|
|
307
307
|
help="Also generate an interactive HTML learning game",
|
|
308
308
|
),
|
|
309
|
-
fmt: str = typer.Option(
|
|
309
|
+
fmt: str = typer.Option(
|
|
310
|
+
"handout", "--format", "-f",
|
|
311
|
+
help="Export: handout, docx, pptx, pdf, markdown",
|
|
312
|
+
),
|
|
310
313
|
):
|
|
311
314
|
"""Generate a detailed daily lesson plan.
|
|
312
315
|
|
|
@@ -408,13 +411,14 @@ def lesson(
|
|
|
408
411
|
from clawed.compile_teacher import compile_teacher_view
|
|
409
412
|
from clawed.multi_agent import multi_agent_generate_master_content
|
|
410
413
|
console.print("[dim]Using multi-agent pipeline (researcher→writer→reviewer)...[/dim]")
|
|
414
|
+
_ma_config = AppConfig.load()
|
|
411
415
|
master = _run_async(
|
|
412
416
|
multi_agent_generate_master_content(
|
|
413
417
|
topic=topic,
|
|
414
418
|
grade=grade,
|
|
415
419
|
subject=subject,
|
|
416
420
|
persona=persona,
|
|
417
|
-
config=
|
|
421
|
+
config=_ma_config,
|
|
418
422
|
unit_context=kb_prompt_section,
|
|
419
423
|
)
|
|
420
424
|
)
|
|
@@ -497,6 +501,41 @@ def lesson(
|
|
|
497
501
|
console.print(f"\n[green]Lesson saved:[/green] {json_path}")
|
|
498
502
|
if export_path:
|
|
499
503
|
console.print(f"[green]Exported:[/green] {export_path}")
|
|
504
|
+
|
|
505
|
+
# ── Quality review ────────────────────────────────────────────
|
|
506
|
+
if export_path:
|
|
507
|
+
try:
|
|
508
|
+
from clawed.review_output import (
|
|
509
|
+
review_docx,
|
|
510
|
+
review_pptx,
|
|
511
|
+
)
|
|
512
|
+
export_p = Path(export_path) if not isinstance(export_path, Path) else export_path
|
|
513
|
+
if export_p.suffix == ".pptx":
|
|
514
|
+
review = review_pptx(export_p)
|
|
515
|
+
elif export_p.suffix == ".docx":
|
|
516
|
+
review = review_docx(export_p)
|
|
517
|
+
else:
|
|
518
|
+
review = None
|
|
519
|
+
if review:
|
|
520
|
+
if review.passed:
|
|
521
|
+
console.print(
|
|
522
|
+
f" Quality: [green]{review.score:.1f}/10[/green]"
|
|
523
|
+
)
|
|
524
|
+
else:
|
|
525
|
+
console.print(
|
|
526
|
+
f" Quality: [red]{review.score:.1f}/10 "
|
|
527
|
+
f"({len(review.issues)} issues)[/red]"
|
|
528
|
+
)
|
|
529
|
+
for issue in review.issues[:5]:
|
|
530
|
+
_sev = {"critical": "red", "major": "yellow", "minor": "dim"}
|
|
531
|
+
console.print(
|
|
532
|
+
f" [{_sev.get(issue['severity'], 'dim')}]"
|
|
533
|
+
f"{issue['location']}: "
|
|
534
|
+
f"{issue['description']}[/{_sev.get(issue['severity'], 'dim')}]"
|
|
535
|
+
)
|
|
536
|
+
except Exception:
|
|
537
|
+
pass
|
|
538
|
+
|
|
500
539
|
console.print(
|
|
501
540
|
Panel(
|
|
502
541
|
f"[bold]Objective:[/bold] {daily.objective}\n"
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""Compile interactive HTML learning games from lesson content.
|
|
2
|
+
|
|
3
|
+
Every game is unique — the LLM designs the mechanic, visuals, and
|
|
4
|
+
interaction based on the lesson content, teacher's style, and student
|
|
5
|
+
preferences. No templates. No repetition.
|
|
6
|
+
|
|
7
|
+
The compiler:
|
|
8
|
+
1. Extracts game-worthy content from MasterContent
|
|
9
|
+
2. Asks the LLM to design and code a complete single-file HTML game
|
|
10
|
+
3. Validates the output (loads, contains educational content)
|
|
11
|
+
4. Falls back to regeneration if the game is broken
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from clawed.compile_game import compile_game
|
|
15
|
+
path = await compile_game(master, persona, output_dir)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from clawed.io import safe_filename
|
|
26
|
+
from clawed.llm import LLMClient
|
|
27
|
+
from clawed.master_content import MasterContent
|
|
28
|
+
from clawed.model_router import route as route_model
|
|
29
|
+
from clawed.models import AppConfig, TeacherPersona
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
GAME_SYSTEM_PROMPT = """\
|
|
34
|
+
You are an expert educational game developer. You create single-file HTML \
|
|
35
|
+
games that teach through play. Every game you make is UNIQUE — different \
|
|
36
|
+
mechanic, different visual style, different interaction pattern.
|
|
37
|
+
|
|
38
|
+
RULES:
|
|
39
|
+
- Output ONLY the complete HTML file. No explanation, no markdown fencing.
|
|
40
|
+
- The file MUST start with <!DOCTYPE html> then <html>, then <head>, then <body>. \
|
|
41
|
+
Always use this exact structure — never put CSS or text directly inside <html> \
|
|
42
|
+
without a <head> or <body> wrapper.
|
|
43
|
+
- The <head> MUST contain: <meta charset="UTF-8">, a <title> tag, and a <style> block.
|
|
44
|
+
- The <body> contains all visible elements and <script> tags.
|
|
45
|
+
- The file must be self-contained: all CSS and JS inline.
|
|
46
|
+
- You may use a Three.js CDN link for 3D: \
|
|
47
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
48
|
+
- The game MUST work on phones, tablets, Chromebooks (responsive, touch).
|
|
49
|
+
- The game MUST be genuinely FUN — not a boring quiz with buttons.
|
|
50
|
+
- Include scoring, feedback, progression (levels or rounds).
|
|
51
|
+
- Include a start screen with the lesson title and instructions.
|
|
52
|
+
- Include an end screen with score and "play again" button.
|
|
53
|
+
- ALL educational content must be embedded as data in the JS.
|
|
54
|
+
- Add a small footer: "Made with Claw-ED — github.com/SirhanMacx/Claw-ED"
|
|
55
|
+
|
|
56
|
+
VISUALS — DO NOT USE IMAGE FILES. Create ALL visuals programmatically:
|
|
57
|
+
- Use Three.js for immersive 3D scenes themed to the lesson content. \
|
|
58
|
+
Age of Exploration = 3D ocean with ships. Civil War = battlefields. \
|
|
59
|
+
Renaissance = marble halls. Science = molecular structures.
|
|
60
|
+
- Use CSS gradients, animations, box-shadows, and transforms for 2D polish.
|
|
61
|
+
- Use HTML Canvas for custom diagrams, maps, or illustrations.
|
|
62
|
+
- Use CSS shapes and emoji for icons — never <img> tags.
|
|
63
|
+
- Use SVG inline for any detailed graphics (maps, diagrams, symbols).
|
|
64
|
+
- The game should feel IMMERSIVE — like the student is inside the topic, \
|
|
65
|
+
not reading a quiz. 3D environments, particle effects, ambient animation.
|
|
66
|
+
- Go ABOVE AND BEYOND visually. This should look like a real game, not a \
|
|
67
|
+
school worksheet with buttons.
|
|
68
|
+
|
|
69
|
+
WHAT MAKES A GREAT LEARNING GAME:
|
|
70
|
+
- The mechanic TEACHES, not just tests. Students learn through the gameplay.
|
|
71
|
+
- Wrong answers give feedback that explains WHY it's wrong.
|
|
72
|
+
- Difficulty progresses — start easy, get harder.
|
|
73
|
+
- Visual and audio feedback (CSS animations, color changes, shake effects).
|
|
74
|
+
- Time pressure is optional but adds excitement when appropriate.
|
|
75
|
+
- Multiplayer/competitive elements are great if they fit.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _extract_game_content(master: MasterContent) -> str:
|
|
80
|
+
"""Extract educational content from MasterContent for game generation.
|
|
81
|
+
|
|
82
|
+
Accepts MasterContent or DailyLesson (single-agent path lacks subject/grade_level).
|
|
83
|
+
"""
|
|
84
|
+
parts = [
|
|
85
|
+
f"LESSON: {master.title}",
|
|
86
|
+
f"SUBJECT: {getattr(master, 'subject', 'Social Studies')}",
|
|
87
|
+
f"GRADE: {getattr(master, 'grade_level', '')}",
|
|
88
|
+
f"TOPIC: {getattr(master, 'topic', master.title)}",
|
|
89
|
+
f"OBJECTIVE: {master.objective}",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
if master.vocabulary:
|
|
93
|
+
parts.append("\nVOCABULARY:")
|
|
94
|
+
for v in master.vocabulary:
|
|
95
|
+
parts.append(
|
|
96
|
+
f" - {v.term}: {v.definition}"
|
|
97
|
+
+ (f" (context: {v.context_sentence})" if v.context_sentence else "")
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
guided_notes = getattr(master, "guided_notes", None)
|
|
101
|
+
if guided_notes:
|
|
102
|
+
parts.append("\nKEY FACTS (fill-in-the-blank):")
|
|
103
|
+
for note in guided_notes:
|
|
104
|
+
parts.append(f" - Q: {note.prompt} → A: {note.answer}")
|
|
105
|
+
|
|
106
|
+
exit_ticket = getattr(master, "exit_ticket", None)
|
|
107
|
+
if exit_ticket:
|
|
108
|
+
parts.append("\nQUIZ QUESTIONS:")
|
|
109
|
+
# DailyLesson exit_ticket is a list of ExitTicketQuestion (has .question)
|
|
110
|
+
# MasterContent exit_ticket may differ — handle both
|
|
111
|
+
for i, q in enumerate(exit_ticket if isinstance(exit_ticket, list) else [], 1):
|
|
112
|
+
question_text = getattr(q, "question", str(q))
|
|
113
|
+
parts.append(f" {i}. {question_text}")
|
|
114
|
+
if hasattr(q, "expected_answer") and q.expected_answer:
|
|
115
|
+
parts.append(f" Answer: {q.expected_answer}")
|
|
116
|
+
|
|
117
|
+
primary_sources = getattr(master, "primary_sources", None)
|
|
118
|
+
if primary_sources:
|
|
119
|
+
parts.append("\nPRIMARY SOURCES:")
|
|
120
|
+
for src in primary_sources:
|
|
121
|
+
src_title = getattr(src, "title", str(src))
|
|
122
|
+
src_type = getattr(src, "source_type", "")
|
|
123
|
+
parts.append(f" - {src_title} ({src_type})")
|
|
124
|
+
if hasattr(src, "content_text") and src.content_text:
|
|
125
|
+
parts.append(f" Text: {src.content_text[:300]}...")
|
|
126
|
+
|
|
127
|
+
direct_instruction = getattr(master, "direct_instruction", None)
|
|
128
|
+
if direct_instruction and not isinstance(direct_instruction, str):
|
|
129
|
+
parts.append("\nKEY CONCEPTS:")
|
|
130
|
+
for section in direct_instruction:
|
|
131
|
+
parts.append(f" - {getattr(section, 'heading', str(section))}")
|
|
132
|
+
if hasattr(section, "key_points") and section.key_points:
|
|
133
|
+
for pt in section.key_points[:3]:
|
|
134
|
+
parts.append(f" • {pt}")
|
|
135
|
+
elif isinstance(direct_instruction, str) and direct_instruction:
|
|
136
|
+
parts.append(f"\nKEY CONCEPTS:\n{direct_instruction[:500]}")
|
|
137
|
+
|
|
138
|
+
return "\n".join(parts)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _repair_html_structure(html: str) -> str:
|
|
142
|
+
"""Repair common LLM HTML generation failures.
|
|
143
|
+
|
|
144
|
+
LLMs sometimes emit:
|
|
145
|
+
- CSS rules directly after <!DOCTYPE html> with no <head> or <body>
|
|
146
|
+
- <title> text as bare text instead of inside a <title> tag
|
|
147
|
+
- <style> content without the wrapping <style> tag
|
|
148
|
+
- Missing </html> closer
|
|
149
|
+
|
|
150
|
+
This function detects those patterns and wraps the content into a
|
|
151
|
+
valid HTML skeleton, preserving all the CSS and JS the LLM generated.
|
|
152
|
+
"""
|
|
153
|
+
html_lower = html.lower()
|
|
154
|
+
|
|
155
|
+
# Strip duplicate DOCTYPE/html tags (LLM sometimes nests two documents)
|
|
156
|
+
if html.count("<!DOCTYPE") > 1:
|
|
157
|
+
# Keep only the content between the LAST <!DOCTYPE and end
|
|
158
|
+
# Actually: strip all but the first DOCTYPE
|
|
159
|
+
parts = html.split("<!DOCTYPE")
|
|
160
|
+
html = "<!DOCTYPE" + parts[1] # keep first occurrence + content
|
|
161
|
+
for extra in parts[2:]:
|
|
162
|
+
# Append content after stripping the duplicate preamble
|
|
163
|
+
extra_content = re.sub(
|
|
164
|
+
r"^[^>]*>\s*<html[^>]*>\s*", "", extra, flags=re.IGNORECASE
|
|
165
|
+
)
|
|
166
|
+
html += extra_content
|
|
167
|
+
html_lower = html.lower()
|
|
168
|
+
|
|
169
|
+
# Check for bare JS not wrapped in <script> tags
|
|
170
|
+
has_script_tags = "<script" in html_lower
|
|
171
|
+
has_js_code = bool(re.search(
|
|
172
|
+
r"(?:function\s+\w+|const\s+\w+\s*=|let\s+\w+\s*=|document\.|addEventListener)",
|
|
173
|
+
html
|
|
174
|
+
))
|
|
175
|
+
if not has_script_tags and has_js_code:
|
|
176
|
+
# Find where JS starts (after the last </div> or after CSS)
|
|
177
|
+
# Look for first function/const/let/document line
|
|
178
|
+
js_start = re.search(
|
|
179
|
+
r"\n((?:function |const |let |var |document\.|//\s*[-=]|class\s+\w+\s*\{))",
|
|
180
|
+
html
|
|
181
|
+
)
|
|
182
|
+
if js_start:
|
|
183
|
+
before_js = html[:js_start.start()]
|
|
184
|
+
js_code = html[js_start.start():]
|
|
185
|
+
# Strip trailing </body></html> from JS if present
|
|
186
|
+
js_code = re.sub(r"\s*</body>\s*</html>\s*$", "", js_code, flags=re.IGNORECASE)
|
|
187
|
+
html = before_js + "\n<script>\n" + js_code + "\n</script>\n</body>\n</html>"
|
|
188
|
+
html_lower = html.lower()
|
|
189
|
+
|
|
190
|
+
# Already well-formed — has <head>, <body>, and <script>
|
|
191
|
+
if "<head>" in html_lower and "<body>" in html_lower and (has_script_tags or "<script" in html_lower):
|
|
192
|
+
return html
|
|
193
|
+
|
|
194
|
+
# Has <head> but no <body> — unusual, leave it
|
|
195
|
+
if "<head>" in html_lower:
|
|
196
|
+
return html
|
|
197
|
+
|
|
198
|
+
# Missing <head>/<body> — LLM dumped CSS/JS straight into <html>
|
|
199
|
+
# Strategy: find the first <script or first element tag to split on,
|
|
200
|
+
# put everything before the first block element into <head>,
|
|
201
|
+
# everything else into <body>.
|
|
202
|
+
|
|
203
|
+
# Collect lines after <!DOCTYPE html><html ...>
|
|
204
|
+
# Find where the html tag ends
|
|
205
|
+
html_tag_end = html_lower.find(">", html_lower.find("<html"))
|
|
206
|
+
if html_tag_end == -1:
|
|
207
|
+
# No <html> tag at all — wrap everything
|
|
208
|
+
title_match = re.search(r"^([^\n<@{]+)", html.strip())
|
|
209
|
+
title_text = title_match.group(1).strip() if title_match else "Learning Game"
|
|
210
|
+
return (
|
|
211
|
+
f"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"
|
|
212
|
+
f"<meta charset=\"UTF-8\">\n"
|
|
213
|
+
f"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
|
|
214
|
+
f"<title>{title_text}</title>\n"
|
|
215
|
+
f"<style>\n{html}\n</style>\n</head>\n<body></body>\n</html>"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
_preamble = html[:html_tag_end + 1] # noqa: F841 # everything up to and including <html ...>
|
|
219
|
+
rest = html[html_tag_end + 1:].strip()
|
|
220
|
+
|
|
221
|
+
# Extract bare title text (first non-tag, non-CSS line after <html>)
|
|
222
|
+
# This catches lines like "Age of Exploration: Chart Your Course" emitted before the CSS
|
|
223
|
+
title_text = "Learning Game"
|
|
224
|
+
title_match = re.match(r"^\s*([A-Za-z][^\n@{<]{3,120})\n", rest)
|
|
225
|
+
if title_match:
|
|
226
|
+
candidate = title_match.group(1).strip()
|
|
227
|
+
# Accept as title if it looks like a human-readable title (not CSS)
|
|
228
|
+
if not candidate.startswith(("@", "{", ".", "#", "*", ":", "/")):
|
|
229
|
+
title_text = candidate
|
|
230
|
+
rest = rest[title_match.end():]
|
|
231
|
+
|
|
232
|
+
# Split: CSS/meta goes in <head>, script/div/etc goes in <body>
|
|
233
|
+
head_parts: list[str] = []
|
|
234
|
+
body_parts: list[str] = [] # noqa: F841
|
|
235
|
+
|
|
236
|
+
# Collect <meta> tags floating outside <head>
|
|
237
|
+
meta_tags = re.findall(r"<meta[^>]*>", rest, re.IGNORECASE)
|
|
238
|
+
for m in meta_tags:
|
|
239
|
+
head_parts.append(m)
|
|
240
|
+
rest = rest.replace(m, "", 1)
|
|
241
|
+
|
|
242
|
+
# Wrap bare CSS (starts with @import, :root, or selector{) in <style>
|
|
243
|
+
# Find contiguous CSS blocks before the first <script or <div
|
|
244
|
+
first_script = re.search(r"<script|<div|<section|<main|<canvas", rest, re.IGNORECASE)
|
|
245
|
+
split_at = first_script.start() if first_script else len(rest)
|
|
246
|
+
|
|
247
|
+
css_block = rest[:split_at].strip()
|
|
248
|
+
body_block = rest[split_at:].strip()
|
|
249
|
+
|
|
250
|
+
if css_block:
|
|
251
|
+
# Check if it's raw CSS (no surrounding <style> tags)
|
|
252
|
+
if not re.search(r"<style", css_block, re.IGNORECASE):
|
|
253
|
+
head_parts.append(f"<style>\n{css_block}\n</style>")
|
|
254
|
+
else:
|
|
255
|
+
head_parts.append(css_block)
|
|
256
|
+
|
|
257
|
+
head_html = "\n".join(head_parts)
|
|
258
|
+
body_html = body_block
|
|
259
|
+
|
|
260
|
+
# Ensure </html> at end
|
|
261
|
+
if not body_html.rstrip().endswith("</html>"):
|
|
262
|
+
body_html = body_html.rstrip()
|
|
263
|
+
if not body_html.endswith("</body>"):
|
|
264
|
+
body_html += "\n</body>"
|
|
265
|
+
body_html += "\n</html>"
|
|
266
|
+
|
|
267
|
+
repaired = (
|
|
268
|
+
f"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"
|
|
269
|
+
f"<meta charset=\"UTF-8\">\n"
|
|
270
|
+
f"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n"
|
|
271
|
+
f"<title>{title_text}</title>\n"
|
|
272
|
+
f"{head_html}\n"
|
|
273
|
+
f"</head>\n<body>\n"
|
|
274
|
+
f"{body_html}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
logger.info("Repaired HTML structure (was missing <head>/<body>)")
|
|
278
|
+
return repaired
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _validate_game_html(html: str, master: MasterContent) -> list[str]:
|
|
282
|
+
"""Validate that the generated HTML is a working game."""
|
|
283
|
+
issues = []
|
|
284
|
+
|
|
285
|
+
if not html or len(html) < 500:
|
|
286
|
+
issues.append("HTML is too short to be a real game")
|
|
287
|
+
|
|
288
|
+
if "<html" not in html.lower():
|
|
289
|
+
issues.append("Missing <html> tag")
|
|
290
|
+
|
|
291
|
+
if "<head>" not in html.lower():
|
|
292
|
+
issues.append("Missing <head> tag — CSS may render broken")
|
|
293
|
+
|
|
294
|
+
if "<body>" not in html.lower():
|
|
295
|
+
issues.append("Missing <body> tag — content structure invalid")
|
|
296
|
+
|
|
297
|
+
if "<script" not in html.lower():
|
|
298
|
+
issues.append("Missing <script> tag — no JavaScript")
|
|
299
|
+
|
|
300
|
+
if "function" not in html and "=>" not in html:
|
|
301
|
+
issues.append("No JavaScript functions found")
|
|
302
|
+
|
|
303
|
+
# Check that educational content is embedded
|
|
304
|
+
topic_words = getattr(master, "topic", master.title).lower().split()[:3]
|
|
305
|
+
html_lower = html.lower()
|
|
306
|
+
found_topic = any(w in html_lower for w in topic_words if len(w) > 3)
|
|
307
|
+
if not found_topic:
|
|
308
|
+
issues.append(
|
|
309
|
+
f"Topic words ({', '.join(topic_words)}) not found in game"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if master.vocabulary:
|
|
313
|
+
first_term = master.vocabulary[0].term.lower()
|
|
314
|
+
if first_term not in html_lower:
|
|
315
|
+
issues.append(
|
|
316
|
+
f"First vocabulary term '{first_term}' not in game"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return issues
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def compile_game(
|
|
323
|
+
master: "MasterContent | Any",
|
|
324
|
+
persona: TeacherPersona | None = None,
|
|
325
|
+
output_dir: Path | None = None,
|
|
326
|
+
config: AppConfig | None = None,
|
|
327
|
+
student_preferences: str = "",
|
|
328
|
+
game_style: str = "",
|
|
329
|
+
) -> Path:
|
|
330
|
+
"""Generate a unique interactive HTML game from lesson content.
|
|
331
|
+
|
|
332
|
+
The LLM designs the game mechanic, visuals, and interaction from
|
|
333
|
+
scratch every time. No templates.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
master: The lesson's MasterContent (source of truth).
|
|
337
|
+
persona: Teacher persona for voice/style matching.
|
|
338
|
+
output_dir: Where to save the .html file.
|
|
339
|
+
config: App config for model routing.
|
|
340
|
+
student_preferences: What students are into ("they love Among Us",
|
|
341
|
+
"obsessed with Minecraft", "competitive, love team challenges").
|
|
342
|
+
game_style: Specific game style request ("jeopardy", "escape room",
|
|
343
|
+
"battle royale quiz", etc.). If empty, LLM decides.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Path to the generated .html game file.
|
|
347
|
+
"""
|
|
348
|
+
if output_dir is None:
|
|
349
|
+
output_dir = Path("./clawed_output")
|
|
350
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
|
|
352
|
+
config = config or AppConfig.load()
|
|
353
|
+
# Route to DEEP tier within the teacher's chosen provider.
|
|
354
|
+
# Teacher picked Ollama? Gets their best Ollama model.
|
|
355
|
+
# Teacher picked Anthropic? Gets Opus. Their choice.
|
|
356
|
+
config = route_model("game_generate", config)
|
|
357
|
+
client = LLMClient(config)
|
|
358
|
+
|
|
359
|
+
# Build the game generation prompt
|
|
360
|
+
content = _extract_game_content(master)
|
|
361
|
+
|
|
362
|
+
prompt_parts = [
|
|
363
|
+
"Design and code a COMPLETE, UNIQUE, single-file HTML learning "
|
|
364
|
+
"game for this lesson.\n",
|
|
365
|
+
content,
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
if student_preferences:
|
|
369
|
+
prompt_parts.append(
|
|
370
|
+
f"\nSTUDENT PREFERENCES: {student_preferences}\n"
|
|
371
|
+
"Design the game mechanic to match what these students enjoy. "
|
|
372
|
+
"If they like a specific game, use a similar mechanic but "
|
|
373
|
+
"with educational content."
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
if game_style:
|
|
377
|
+
prompt_parts.append(
|
|
378
|
+
f"\nREQUESTED STYLE: {game_style}\n"
|
|
379
|
+
"Use this style as inspiration but make it your own."
|
|
380
|
+
)
|
|
381
|
+
else:
|
|
382
|
+
prompt_parts.append(
|
|
383
|
+
"\nNo specific style requested — surprise me. Be creative. "
|
|
384
|
+
"Pick a game mechanic that FITS this specific lesson content. "
|
|
385
|
+
"A timeline lesson should have different gameplay than a "
|
|
386
|
+
"vocabulary lesson. Think about what mechanic actually helps "
|
|
387
|
+
"students LEARN this material."
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if persona:
|
|
391
|
+
prompt_parts.append(
|
|
392
|
+
f"\nTEACHER'S STYLE: {persona.tone or 'engaging'}\n"
|
|
393
|
+
f"Grade level: {getattr(master, 'grade_level', '')}\n"
|
|
394
|
+
"Match the difficulty and humor to this teacher and grade."
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
prompt = "\n".join(prompt_parts)
|
|
398
|
+
|
|
399
|
+
# Generate with retry
|
|
400
|
+
max_attempts = 2
|
|
401
|
+
for attempt in range(max_attempts):
|
|
402
|
+
try:
|
|
403
|
+
html = await client.generate(
|
|
404
|
+
prompt=prompt,
|
|
405
|
+
system=GAME_SYSTEM_PROMPT,
|
|
406
|
+
temperature=0.8, # Creative — we want variety
|
|
407
|
+
max_tokens=12000,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Clean: strip markdown fencing if present
|
|
411
|
+
html = html.strip()
|
|
412
|
+
if html.startswith("```"):
|
|
413
|
+
html = re.sub(r"^```\w*\n?", "", html)
|
|
414
|
+
html = re.sub(r"\n?```$", "", html)
|
|
415
|
+
html = html.strip()
|
|
416
|
+
|
|
417
|
+
# Structural repair: LLMs sometimes emit CSS/content outside <head>/<body>.
|
|
418
|
+
# Detect and wrap into a valid HTML skeleton if needed.
|
|
419
|
+
html = _repair_html_structure(html)
|
|
420
|
+
|
|
421
|
+
# Validate
|
|
422
|
+
issues = _validate_game_html(html, master)
|
|
423
|
+
if issues and attempt < max_attempts - 1:
|
|
424
|
+
logger.warning(
|
|
425
|
+
"Game validation issues (attempt %d): %s",
|
|
426
|
+
attempt + 1,
|
|
427
|
+
"; ".join(issues),
|
|
428
|
+
)
|
|
429
|
+
prompt += (
|
|
430
|
+
"\n\nPREVIOUS ATTEMPT HAD ISSUES: "
|
|
431
|
+
+ "; ".join(issues)
|
|
432
|
+
+ "\nFix these issues. Output ONLY the complete HTML."
|
|
433
|
+
)
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
if issues:
|
|
437
|
+
logger.warning(
|
|
438
|
+
"Game has issues but using anyway: %s",
|
|
439
|
+
"; ".join(issues),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Save
|
|
443
|
+
fname = f"game_{safe_filename(master.title)}.html"
|
|
444
|
+
game_path = output_dir / fname
|
|
445
|
+
game_path.write_text(html, encoding="utf-8")
|
|
446
|
+
|
|
447
|
+
logger.info(
|
|
448
|
+
"Game compiled: %s (%d bytes)",
|
|
449
|
+
game_path.name,
|
|
450
|
+
len(html),
|
|
451
|
+
)
|
|
452
|
+
return game_path
|
|
453
|
+
|
|
454
|
+
except Exception as e:
|
|
455
|
+
logger.error("Game generation failed (attempt %d): %s", attempt + 1, e)
|
|
456
|
+
if attempt == max_attempts - 1:
|
|
457
|
+
raise
|
|
458
|
+
|
|
459
|
+
# Should never reach here
|
|
460
|
+
raise RuntimeError("Game generation failed after all attempts")
|
|
@@ -457,6 +457,8 @@ def export_student_handout(
|
|
|
457
457
|
|
|
458
458
|
Returns the path to the saved .docx file.
|
|
459
459
|
"""
|
|
460
|
+
import re as _re
|
|
461
|
+
|
|
460
462
|
from docx import Document
|
|
461
463
|
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
462
464
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
@@ -464,15 +466,27 @@ def export_student_handout(
|
|
|
464
466
|
|
|
465
467
|
from clawed.sanitize import sanitize_text
|
|
466
468
|
|
|
469
|
+
def _strip_answers(text: str) -> str:
|
|
470
|
+
"""Remove answer keys from student-facing content."""
|
|
471
|
+
# Strip (Answer: ...) patterns
|
|
472
|
+
text = _re.sub(r"\s*\(Answer:\s*[^)]*\)", "", text)
|
|
473
|
+
# Strip (answer: ...) lowercase variant
|
|
474
|
+
text = _re.sub(r"\s*\(answer:\s*[^)]*\)", "", text)
|
|
475
|
+
# Strip "Answer: ..." at end of lines
|
|
476
|
+
text = _re.sub(r"\s*Answer:\s*.+$", "", text, flags=_re.MULTILINE)
|
|
477
|
+
# Strip "Expected: ..." patterns
|
|
478
|
+
text = _re.sub(r"\s*\*?Expected:?\*?\s*.+$", "", text, flags=_re.MULTILINE)
|
|
479
|
+
return text.strip()
|
|
480
|
+
|
|
467
481
|
doc = Document()
|
|
468
482
|
|
|
469
|
-
# Sanitize
|
|
483
|
+
# Sanitize AND strip answers from all student-facing text
|
|
470
484
|
lesson.title = sanitize_text(lesson.title)
|
|
471
485
|
lesson.objective = sanitize_text(lesson.objective)
|
|
472
486
|
lesson.do_now = sanitize_text(lesson.do_now) if lesson.do_now else ""
|
|
473
487
|
lesson.direct_instruction = sanitize_text(lesson.direct_instruction) if lesson.direct_instruction else ""
|
|
474
|
-
lesson.guided_practice = sanitize_text(lesson.guided_practice) if lesson.guided_practice else ""
|
|
475
|
-
lesson.independent_work = sanitize_text(lesson.independent_work) if lesson.independent_work else ""
|
|
488
|
+
lesson.guided_practice = _strip_answers(sanitize_text(lesson.guided_practice)) if lesson.guided_practice else ""
|
|
489
|
+
lesson.independent_work = _strip_answers(sanitize_text(lesson.independent_work)) if lesson.independent_work else ""
|
|
476
490
|
for q in lesson.exit_ticket:
|
|
477
491
|
q.question = sanitize_text(q.question)
|
|
478
492
|
|