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.
Files changed (259) hide show
  1. {clawed-2.5.1 → clawed-2.5.3}/PKG-INFO +1 -1
  2. {clawed-2.5.1 → clawed-2.5.3}/clawed/__init__.py +1 -1
  3. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/generate.py +41 -2
  4. clawed-2.5.3/clawed/compile_game.py +460 -0
  5. {clawed-2.5.1 → clawed-2.5.3}/clawed/export_docx.py +17 -3
  6. {clawed-2.5.1 → clawed-2.5.3}/clawed/export_pptx.py +428 -143
  7. {clawed-2.5.1 → clawed-2.5.3}/clawed/model_router.py +12 -13
  8. {clawed-2.5.1 → clawed-2.5.3}/clawed/multi_agent.py +2 -2
  9. clawed-2.5.3/clawed/review_output.py +253 -0
  10. {clawed-2.5.1 → clawed-2.5.3}/clawed/slide_images.py +285 -52
  11. {clawed-2.5.1 → clawed-2.5.3}/pyproject.toml +1 -1
  12. clawed-2.5.1/clawed/compile_game.py +0 -275
  13. {clawed-2.5.1 → clawed-2.5.3}/.gitignore +0 -0
  14. {clawed-2.5.1 → clawed-2.5.3}/LICENSE +0 -0
  15. {clawed-2.5.1 → clawed-2.5.3}/README.md +0 -0
  16. {clawed-2.5.1 → clawed-2.5.3}/clawed/__main__.py +0 -0
  17. {clawed-2.5.1 → clawed-2.5.3}/clawed/_legacy_gateway.py +0 -0
  18. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent.py +0 -0
  19. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/__init__.py +0 -0
  20. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/approvals.py +0 -0
  21. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/autonomy.py +0 -0
  22. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/context.py +0 -0
  23. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/core.py +0 -0
  24. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/custom_tools.py +0 -0
  25. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/drive/__init__.py +0 -0
  26. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/drive/auth.py +0 -0
  27. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/drive/client.py +0 -0
  28. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/fake_llm.py +0 -0
  29. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/loop.py +0 -0
  30. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/__init__.py +0 -0
  31. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/curriculum.py +0 -0
  32. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/curriculum_kb.py +0 -0
  33. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/embeddings.py +0 -0
  34. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/episodes.py +0 -0
  35. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/identity.py +0 -0
  36. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/loader.py +0 -0
  37. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/memory/preferences.py +0 -0
  38. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/planner.py +0 -0
  39. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/prompt.py +0 -0
  40. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/scheduler.py +0 -0
  41. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/__init__.py +0 -0
  42. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/base.py +0 -0
  43. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/configure_profile.py +0 -0
  44. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/curriculum_map.py +0 -0
  45. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_create_doc.py +0 -0
  46. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_create_slides.py +0 -0
  47. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_list.py +0 -0
  48. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_organize.py +0 -0
  49. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_read.py +0 -0
  50. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/drive_upload.py +0 -0
  51. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/export_document.py +0 -0
  52. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/gap_analysis.py +0 -0
  53. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_assessment.py +0 -0
  54. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_lesson.py +0 -0
  55. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_lesson_bundle.py +0 -0
  56. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_materials.py +0 -0
  57. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/generate_unit.py +0 -0
  58. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/ingest_materials.py +0 -0
  59. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/parent_comm.py +0 -0
  60. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/read_heartbeat.py +0 -0
  61. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/read_workspace.py +0 -0
  62. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/request_approval.py +0 -0
  63. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/schedule_task.py +0 -0
  64. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/search_lessons.py +0 -0
  65. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/search_my_materials.py +0 -0
  66. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/search_standards.py +0 -0
  67. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/student_insights.py +0 -0
  68. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/sub_packet.py +0 -0
  69. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/switch_model.py +0 -0
  70. {clawed-2.5.1 → clawed-2.5.3}/clawed/agent_core/tools/update_soul.py +0 -0
  71. {clawed-2.5.1 → clawed-2.5.3}/clawed/analytics.py +0 -0
  72. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/__init__.py +0 -0
  73. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/deps.py +0 -0
  74. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/__init__.py +0 -0
  75. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/chat.py +0 -0
  76. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/export.py +0 -0
  77. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/feedback.py +0 -0
  78. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/gateway_chat.py +0 -0
  79. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/generate.py +0 -0
  80. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/ingest.py +0 -0
  81. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/lessons.py +0 -0
  82. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/school.py +0 -0
  83. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/settings.py +0 -0
  84. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/routes/tools.py +0 -0
  85. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/server.py +0 -0
  86. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/static/app.js +0 -0
  87. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/static/style.css +0 -0
  88. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/static/widget.js +0 -0
  89. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/analytics.html +0 -0
  90. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/base.html +0 -0
  91. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/dashboard.html +0 -0
  92. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/generate.html +0 -0
  93. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/index.html +0 -0
  94. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/lesson.html +0 -0
  95. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/profile.html +0 -0
  96. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/settings.html +0 -0
  97. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/stats.html +0 -0
  98. {clawed-2.5.1 → clawed-2.5.3}/clawed/api/templates/students.html +0 -0
  99. {clawed-2.5.1 → clawed-2.5.3}/clawed/assessment.py +0 -0
  100. {clawed-2.5.1 → clawed-2.5.3}/clawed/asset_registry.py +0 -0
  101. {clawed-2.5.1 → clawed-2.5.3}/clawed/async_utils.py +0 -0
  102. {clawed-2.5.1 → clawed-2.5.3}/clawed/auth/__init__.py +0 -0
  103. {clawed-2.5.1 → clawed-2.5.3}/clawed/auth/google_auth.py +0 -0
  104. {clawed-2.5.1 → clawed-2.5.3}/clawed/bot_state.py +0 -0
  105. {clawed-2.5.1 → clawed-2.5.3}/clawed/chat.py +0 -0
  106. {clawed-2.5.1 → clawed-2.5.3}/clawed/cli.py +0 -0
  107. {clawed-2.5.1 → clawed-2.5.3}/clawed/cli_chat.py +0 -0
  108. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/__init__.py +0 -0
  109. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/_helpers.py +0 -0
  110. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/bot.py +0 -0
  111. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/config.py +0 -0
  112. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/config_llm.py +0 -0
  113. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/config_profile.py +0 -0
  114. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/export.py +0 -0
  115. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/game.py +0 -0
  116. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/generate_assessment.py +0 -0
  117. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/generate_unit.py +0 -0
  118. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/queue.py +0 -0
  119. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/schedule_cmd.py +0 -0
  120. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/sub.py +0 -0
  121. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/train.py +0 -0
  122. {clawed-2.5.1 → clawed-2.5.3}/clawed/commands/workspace_cmd.py +0 -0
  123. {clawed-2.5.1 → clawed-2.5.3}/clawed/compile_slides.py +0 -0
  124. {clawed-2.5.1 → clawed-2.5.3}/clawed/compile_student.py +0 -0
  125. {clawed-2.5.1 → clawed-2.5.3}/clawed/compile_teacher.py +0 -0
  126. {clawed-2.5.1 → clawed-2.5.3}/clawed/config.py +0 -0
  127. {clawed-2.5.1 → clawed-2.5.3}/clawed/corpus.py +0 -0
  128. {clawed-2.5.1 → clawed-2.5.3}/clawed/curriculum_map.py +0 -0
  129. {clawed-2.5.1 → clawed-2.5.3}/clawed/database.py +0 -0
  130. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/__init__.py +0 -0
  131. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_assessment.json +0 -0
  132. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_formative_assessment.json +0 -0
  133. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_lesson_materials.json +0 -0
  134. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_lesson_science_g6.json +0 -0
  135. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_lesson_social_studies_g8.json +0 -0
  136. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_master_content.json +0 -0
  137. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_pacing_guide.json +0 -0
  138. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_quiz.json +0 -0
  139. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_rubric.json +0 -0
  140. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_unit_plan.json +0 -0
  141. {clawed-2.5.1 → clawed-2.5.3}/clawed/demo/demo_year_map.json +0 -0
  142. {clawed-2.5.1 → clawed-2.5.3}/clawed/differentiation.py +0 -0
  143. {clawed-2.5.1 → clawed-2.5.3}/clawed/doc_export.py +0 -0
  144. {clawed-2.5.1 → clawed-2.5.3}/clawed/drive.py +0 -0
  145. {clawed-2.5.1 → clawed-2.5.3}/clawed/evaluation.py +0 -0
  146. {clawed-2.5.1 → clawed-2.5.3}/clawed/export_handout.py +0 -0
  147. {clawed-2.5.1 → clawed-2.5.3}/clawed/export_markdown.py +0 -0
  148. {clawed-2.5.1 → clawed-2.5.3}/clawed/export_pdf.py +0 -0
  149. {clawed-2.5.1 → clawed-2.5.3}/clawed/export_templates.py +0 -0
  150. {clawed-2.5.1 → clawed-2.5.3}/clawed/export_theme.py +0 -0
  151. {clawed-2.5.1 → clawed-2.5.3}/clawed/exporter.py +0 -0
  152. {clawed-2.5.1 → clawed-2.5.3}/clawed/failure_codes.py +0 -0
  153. {clawed-2.5.1 → clawed-2.5.3}/clawed/feedback.py +0 -0
  154. {clawed-2.5.1 → clawed-2.5.3}/clawed/formats/__init__.py +0 -0
  155. {clawed-2.5.1 → clawed-2.5.3}/clawed/formats/flipchart.py +0 -0
  156. {clawed-2.5.1 → clawed-2.5.3}/clawed/formats/notebook.py +0 -0
  157. {clawed-2.5.1 → clawed-2.5.3}/clawed/formats/xbk.py +0 -0
  158. {clawed-2.5.1 → clawed-2.5.3}/clawed/gateway.py +0 -0
  159. {clawed-2.5.1 → clawed-2.5.3}/clawed/gateway_response.py +0 -0
  160. {clawed-2.5.1 → clawed-2.5.3}/clawed/generation.py +0 -0
  161. {clawed-2.5.1 → clawed-2.5.3}/clawed/generation_report.py +0 -0
  162. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/__init__.py +0 -0
  163. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/export.py +0 -0
  164. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/feedback.py +0 -0
  165. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/gaps.py +0 -0
  166. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/generate.py +0 -0
  167. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/ingest.py +0 -0
  168. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/misc.py +0 -0
  169. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/onboard.py +0 -0
  170. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/schedule.py +0 -0
  171. {clawed-2.5.1 → clawed-2.5.3}/clawed/handlers/standards.py +0 -0
  172. {clawed-2.5.1 → clawed-2.5.3}/clawed/hermes_plugin.py +0 -0
  173. {clawed-2.5.1 → clawed-2.5.3}/clawed/image_pipeline.py +0 -0
  174. {clawed-2.5.1 → clawed-2.5.3}/clawed/improver.py +0 -0
  175. {clawed-2.5.1 → clawed-2.5.3}/clawed/ingestor.py +0 -0
  176. {clawed-2.5.1 → clawed-2.5.3}/clawed/io.py +0 -0
  177. {clawed-2.5.1 → clawed-2.5.3}/clawed/lesson.py +0 -0
  178. {clawed-2.5.1 → clawed-2.5.3}/clawed/llm.py +0 -0
  179. {clawed-2.5.1 → clawed-2.5.3}/clawed/master_content.py +0 -0
  180. {clawed-2.5.1 → clawed-2.5.3}/clawed/materials.py +0 -0
  181. {clawed-2.5.1 → clawed-2.5.3}/clawed/mcp_server.py +0 -0
  182. {clawed-2.5.1 → clawed-2.5.3}/clawed/memory_engine.py +0 -0
  183. {clawed-2.5.1 → clawed-2.5.3}/clawed/models.py +0 -0
  184. {clawed-2.5.1 → clawed-2.5.3}/clawed/onboarding.py +0 -0
  185. {clawed-2.5.1 → clawed-2.5.3}/clawed/openclaw_plugin.py +0 -0
  186. {clawed-2.5.1 → clawed-2.5.3}/clawed/parent_comm.py +0 -0
  187. {clawed-2.5.1 → clawed-2.5.3}/clawed/persona.py +0 -0
  188. {clawed-2.5.1 → clawed-2.5.3}/clawed/persona_evolution.py +0 -0
  189. {clawed-2.5.1 → clawed-2.5.3}/clawed/planner.py +0 -0
  190. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/504_accommodations.txt +0 -0
  191. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/admin_lesson_plan.txt +0 -0
  192. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/assessment.txt +0 -0
  193. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/curriculum_gaps.txt +0 -0
  194. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/dbq_assessment.txt +0 -0
  195. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/differentiation.txt +0 -0
  196. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/formative_assessment.txt +0 -0
  197. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/iep_modification.txt +0 -0
  198. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/lesson_plan.txt +0 -0
  199. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/master_content.txt +0 -0
  200. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/multi_agent_researcher.txt +0 -0
  201. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/multi_agent_reviewer.txt +0 -0
  202. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/pacing_guide.txt +0 -0
  203. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/parent_note.txt +0 -0
  204. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/persona_extract.txt +0 -0
  205. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/quiz.txt +0 -0
  206. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/rubric.txt +0 -0
  207. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/student_packet.txt +0 -0
  208. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/sub_packet.txt +0 -0
  209. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/summative_assessment.txt +0 -0
  210. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/tiered_assignments.txt +0 -0
  211. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/unit_plan.txt +0 -0
  212. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/worksheet.txt +0 -0
  213. {clawed-2.5.1 → clawed-2.5.3}/clawed/prompts/year_map.txt +0 -0
  214. {clawed-2.5.1 → clawed-2.5.3}/clawed/quality.py +0 -0
  215. {clawed-2.5.1 → clawed-2.5.3}/clawed/reading_report.py +0 -0
  216. {clawed-2.5.1 → clawed-2.5.3}/clawed/router.py +0 -0
  217. {clawed-2.5.1 → clawed-2.5.3}/clawed/sanitize.py +0 -0
  218. {clawed-2.5.1 → clawed-2.5.3}/clawed/scheduler.py +0 -0
  219. {clawed-2.5.1 → clawed-2.5.3}/clawed/school.py +0 -0
  220. {clawed-2.5.1 → clawed-2.5.3}/clawed/search.py +0 -0
  221. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/__init__.py +0 -0
  222. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/art.py +0 -0
  223. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/base.py +0 -0
  224. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/computer_science.py +0 -0
  225. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/ela.py +0 -0
  226. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/foreign_language.py +0 -0
  227. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/history.py +0 -0
  228. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/library.py +0 -0
  229. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/math.py +0 -0
  230. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/music.py +0 -0
  231. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/physical_education.py +0 -0
  232. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/science.py +0 -0
  233. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/social_studies.py +0 -0
  234. {clawed-2.5.1 → clawed-2.5.3}/clawed/skills/special_education.py +0 -0
  235. {clawed-2.5.1 → clawed-2.5.3}/clawed/standards.py +0 -0
  236. {clawed-2.5.1 → clawed-2.5.3}/clawed/state.py +0 -0
  237. {clawed-2.5.1 → clawed-2.5.3}/clawed/state_standards.py +0 -0
  238. {clawed-2.5.1 → clawed-2.5.3}/clawed/student_bot.py +0 -0
  239. {clawed-2.5.1 → clawed-2.5.3}/clawed/student_cli.py +0 -0
  240. {clawed-2.5.1 → clawed-2.5.3}/clawed/student_telegram_bot.py +0 -0
  241. {clawed-2.5.1 → clawed-2.5.3}/clawed/sub_packet.py +0 -0
  242. {clawed-2.5.1 → clawed-2.5.3}/clawed/task_queue.py +0 -0
  243. {clawed-2.5.1 → clawed-2.5.3}/clawed/templates_lib.py +0 -0
  244. {clawed-2.5.1 → clawed-2.5.3}/clawed/tools.py +0 -0
  245. {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/__init__.py +0 -0
  246. {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/cli.py +0 -0
  247. {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/hermes.py +0 -0
  248. {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/openclaw.py +0 -0
  249. {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/student_telegram.py +0 -0
  250. {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/telegram.py +0 -0
  251. {clawed-2.5.1 → clawed-2.5.3}/clawed/transports/web.py +0 -0
  252. {clawed-2.5.1 → clawed-2.5.3}/clawed/tui.py +0 -0
  253. {clawed-2.5.1 → clawed-2.5.3}/clawed/tui_chat.py +0 -0
  254. {clawed-2.5.1 → clawed-2.5.3}/clawed/validation.py +0 -0
  255. {clawed-2.5.1 → clawed-2.5.3}/clawed/voice.py +0 -0
  256. {clawed-2.5.1 → clawed-2.5.3}/clawed/voice_check.py +0 -0
  257. {clawed-2.5.1 → clawed-2.5.3}/clawed/workspace.py +0 -0
  258. {clawed-2.5.1 → clawed-2.5.3}/eduagent/__init__.py +0 -0
  259. {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.1
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.1"
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("markdown", "--format", "-f", help="Export format: markdown, pptx, docx, pdf, handout"),
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=None,
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 all lesson text fields for the handout
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