clawed 2.3.7__tar.gz → 2.3.8__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 (249) hide show
  1. {clawed-2.3.7 → clawed-2.3.8}/PKG-INFO +13 -1
  2. {clawed-2.3.7 → clawed-2.3.8}/README.md +12 -0
  3. {clawed-2.3.7 → clawed-2.3.8}/clawed/__init__.py +1 -1
  4. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/core.py +3 -1
  5. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/generate_lesson_bundle.py +52 -8
  6. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/server.py +6 -2
  7. clawed-2.3.8/clawed/async_utils.py +22 -0
  8. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/__init__.py +5 -4
  9. {clawed-2.3.7 → clawed-2.3.8}/clawed/export_docx.py +3 -21
  10. {clawed-2.3.7 → clawed-2.3.8}/clawed/export_pptx.py +3 -20
  11. clawed-2.3.8/clawed/failure_codes.py +22 -0
  12. {clawed-2.3.7 → clawed-2.3.8}/clawed/generation.py +2 -3
  13. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/onboard.py +15 -5
  14. {clawed-2.3.7 → clawed-2.3.8}/clawed/ingestor.py +1 -1
  15. {clawed-2.3.7 → clawed-2.3.8}/clawed/lesson.py +3 -1
  16. {clawed-2.3.7 → clawed-2.3.8}/clawed/llm.py +28 -21
  17. {clawed-2.3.7 → clawed-2.3.8}/clawed/quality.py +51 -0
  18. {clawed-2.3.7 → clawed-2.3.8}/clawed/validation.py +24 -11
  19. {clawed-2.3.7 → clawed-2.3.8}/pyproject.toml +1 -1
  20. {clawed-2.3.7 → clawed-2.3.8}/.gitignore +0 -0
  21. {clawed-2.3.7 → clawed-2.3.8}/LICENSE +0 -0
  22. {clawed-2.3.7 → clawed-2.3.8}/clawed/__main__.py +0 -0
  23. {clawed-2.3.7 → clawed-2.3.8}/clawed/_legacy_gateway.py +0 -0
  24. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent.py +0 -0
  25. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/__init__.py +0 -0
  26. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/approvals.py +0 -0
  27. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/autonomy.py +0 -0
  28. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/context.py +0 -0
  29. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/custom_tools.py +0 -0
  30. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/drive/__init__.py +0 -0
  31. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/drive/auth.py +0 -0
  32. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/drive/client.py +0 -0
  33. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/fake_llm.py +0 -0
  34. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/loop.py +0 -0
  35. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/memory/__init__.py +0 -0
  36. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/memory/curriculum.py +0 -0
  37. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/memory/curriculum_kb.py +0 -0
  38. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/memory/embeddings.py +0 -0
  39. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/memory/episodes.py +0 -0
  40. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/memory/identity.py +0 -0
  41. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/memory/loader.py +0 -0
  42. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/memory/preferences.py +0 -0
  43. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/planner.py +0 -0
  44. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/prompt.py +0 -0
  45. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/scheduler.py +0 -0
  46. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/__init__.py +0 -0
  47. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/base.py +0 -0
  48. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/configure_profile.py +0 -0
  49. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/curriculum_map.py +0 -0
  50. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/drive_create_doc.py +0 -0
  51. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/drive_create_slides.py +0 -0
  52. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/drive_list.py +0 -0
  53. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/drive_organize.py +0 -0
  54. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/drive_read.py +0 -0
  55. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/drive_upload.py +0 -0
  56. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/export_document.py +0 -0
  57. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/gap_analysis.py +0 -0
  58. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/generate_assessment.py +0 -0
  59. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/generate_lesson.py +0 -0
  60. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/generate_materials.py +0 -0
  61. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/generate_unit.py +0 -0
  62. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/ingest_materials.py +0 -0
  63. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/parent_comm.py +0 -0
  64. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/read_heartbeat.py +0 -0
  65. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/read_workspace.py +0 -0
  66. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/request_approval.py +0 -0
  67. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/schedule_task.py +0 -0
  68. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/search_lessons.py +0 -0
  69. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/search_my_materials.py +0 -0
  70. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/search_standards.py +0 -0
  71. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/student_insights.py +0 -0
  72. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/sub_packet.py +0 -0
  73. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/switch_model.py +0 -0
  74. {clawed-2.3.7 → clawed-2.3.8}/clawed/agent_core/tools/update_soul.py +0 -0
  75. {clawed-2.3.7 → clawed-2.3.8}/clawed/analytics.py +0 -0
  76. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/__init__.py +0 -0
  77. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/deps.py +0 -0
  78. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/__init__.py +0 -0
  79. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/chat.py +0 -0
  80. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/export.py +0 -0
  81. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/feedback.py +0 -0
  82. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/gateway_chat.py +0 -0
  83. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/generate.py +0 -0
  84. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/ingest.py +0 -0
  85. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/lessons.py +0 -0
  86. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/school.py +0 -0
  87. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/settings.py +0 -0
  88. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/routes/tools.py +0 -0
  89. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/static/app.js +0 -0
  90. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/static/style.css +0 -0
  91. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/static/widget.js +0 -0
  92. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/analytics.html +0 -0
  93. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/base.html +0 -0
  94. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/dashboard.html +0 -0
  95. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/generate.html +0 -0
  96. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/index.html +0 -0
  97. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/lesson.html +0 -0
  98. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/profile.html +0 -0
  99. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/settings.html +0 -0
  100. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/stats.html +0 -0
  101. {clawed-2.3.7 → clawed-2.3.8}/clawed/api/templates/students.html +0 -0
  102. {clawed-2.3.7 → clawed-2.3.8}/clawed/assessment.py +0 -0
  103. {clawed-2.3.7 → clawed-2.3.8}/clawed/asset_registry.py +0 -0
  104. {clawed-2.3.7 → clawed-2.3.8}/clawed/auth/__init__.py +0 -0
  105. {clawed-2.3.7 → clawed-2.3.8}/clawed/auth/google_auth.py +0 -0
  106. {clawed-2.3.7 → clawed-2.3.8}/clawed/bot_state.py +0 -0
  107. {clawed-2.3.7 → clawed-2.3.8}/clawed/chat.py +0 -0
  108. {clawed-2.3.7 → clawed-2.3.8}/clawed/cli.py +0 -0
  109. {clawed-2.3.7 → clawed-2.3.8}/clawed/cli_chat.py +0 -0
  110. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/__init__.py +0 -0
  111. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/_helpers.py +0 -0
  112. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/bot.py +0 -0
  113. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/config.py +0 -0
  114. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/config_llm.py +0 -0
  115. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/config_profile.py +0 -0
  116. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/export.py +0 -0
  117. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/generate.py +0 -0
  118. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/generate_assessment.py +0 -0
  119. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/generate_unit.py +0 -0
  120. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/queue.py +0 -0
  121. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/schedule_cmd.py +0 -0
  122. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/sub.py +0 -0
  123. {clawed-2.3.7 → clawed-2.3.8}/clawed/commands/workspace_cmd.py +0 -0
  124. {clawed-2.3.7 → clawed-2.3.8}/clawed/compile_slides.py +0 -0
  125. {clawed-2.3.7 → clawed-2.3.8}/clawed/compile_student.py +0 -0
  126. {clawed-2.3.7 → clawed-2.3.8}/clawed/compile_teacher.py +0 -0
  127. {clawed-2.3.7 → clawed-2.3.8}/clawed/config.py +0 -0
  128. {clawed-2.3.7 → clawed-2.3.8}/clawed/corpus.py +0 -0
  129. {clawed-2.3.7 → clawed-2.3.8}/clawed/curriculum_map.py +0 -0
  130. {clawed-2.3.7 → clawed-2.3.8}/clawed/database.py +0 -0
  131. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_assessment.json +0 -0
  132. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_formative_assessment.json +0 -0
  133. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_lesson_materials.json +0 -0
  134. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_lesson_science_g6.json +0 -0
  135. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_lesson_social_studies_g8.json +0 -0
  136. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_master_content.json +0 -0
  137. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_pacing_guide.json +0 -0
  138. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_quiz.json +0 -0
  139. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_rubric.json +0 -0
  140. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_unit_plan.json +0 -0
  141. {clawed-2.3.7 → clawed-2.3.8}/clawed/demo/demo_year_map.json +0 -0
  142. {clawed-2.3.7 → clawed-2.3.8}/clawed/differentiation.py +0 -0
  143. {clawed-2.3.7 → clawed-2.3.8}/clawed/doc_export.py +0 -0
  144. {clawed-2.3.7 → clawed-2.3.8}/clawed/drive.py +0 -0
  145. {clawed-2.3.7 → clawed-2.3.8}/clawed/evaluation.py +0 -0
  146. {clawed-2.3.7 → clawed-2.3.8}/clawed/export_handout.py +0 -0
  147. {clawed-2.3.7 → clawed-2.3.8}/clawed/export_markdown.py +0 -0
  148. {clawed-2.3.7 → clawed-2.3.8}/clawed/export_pdf.py +0 -0
  149. {clawed-2.3.7 → clawed-2.3.8}/clawed/export_templates.py +0 -0
  150. {clawed-2.3.7 → clawed-2.3.8}/clawed/export_theme.py +0 -0
  151. {clawed-2.3.7 → clawed-2.3.8}/clawed/exporter.py +0 -0
  152. {clawed-2.3.7 → clawed-2.3.8}/clawed/feedback.py +0 -0
  153. {clawed-2.3.7 → clawed-2.3.8}/clawed/formats/__init__.py +0 -0
  154. {clawed-2.3.7 → clawed-2.3.8}/clawed/formats/flipchart.py +0 -0
  155. {clawed-2.3.7 → clawed-2.3.8}/clawed/formats/notebook.py +0 -0
  156. {clawed-2.3.7 → clawed-2.3.8}/clawed/formats/xbk.py +0 -0
  157. {clawed-2.3.7 → clawed-2.3.8}/clawed/gateway.py +0 -0
  158. {clawed-2.3.7 → clawed-2.3.8}/clawed/gateway_response.py +0 -0
  159. {clawed-2.3.7 → clawed-2.3.8}/clawed/generation_report.py +0 -0
  160. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/__init__.py +0 -0
  161. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/export.py +0 -0
  162. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/feedback.py +0 -0
  163. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/gaps.py +0 -0
  164. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/generate.py +0 -0
  165. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/ingest.py +0 -0
  166. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/misc.py +0 -0
  167. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/schedule.py +0 -0
  168. {clawed-2.3.7 → clawed-2.3.8}/clawed/handlers/standards.py +0 -0
  169. {clawed-2.3.7 → clawed-2.3.8}/clawed/image_pipeline.py +0 -0
  170. {clawed-2.3.7 → clawed-2.3.8}/clawed/improver.py +0 -0
  171. {clawed-2.3.7 → clawed-2.3.8}/clawed/io.py +0 -0
  172. {clawed-2.3.7 → clawed-2.3.8}/clawed/master_content.py +0 -0
  173. {clawed-2.3.7 → clawed-2.3.8}/clawed/materials.py +0 -0
  174. {clawed-2.3.7 → clawed-2.3.8}/clawed/mcp_server.py +0 -0
  175. {clawed-2.3.7 → clawed-2.3.8}/clawed/memory_engine.py +0 -0
  176. {clawed-2.3.7 → clawed-2.3.8}/clawed/model_router.py +0 -0
  177. {clawed-2.3.7 → clawed-2.3.8}/clawed/models.py +0 -0
  178. {clawed-2.3.7 → clawed-2.3.8}/clawed/onboarding.py +0 -0
  179. {clawed-2.3.7 → clawed-2.3.8}/clawed/openclaw_plugin.py +0 -0
  180. {clawed-2.3.7 → clawed-2.3.8}/clawed/parent_comm.py +0 -0
  181. {clawed-2.3.7 → clawed-2.3.8}/clawed/persona.py +0 -0
  182. {clawed-2.3.7 → clawed-2.3.8}/clawed/persona_evolution.py +0 -0
  183. {clawed-2.3.7 → clawed-2.3.8}/clawed/planner.py +0 -0
  184. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/504_accommodations.txt +0 -0
  185. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/admin_lesson_plan.txt +0 -0
  186. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/assessment.txt +0 -0
  187. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/curriculum_gaps.txt +0 -0
  188. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/dbq_assessment.txt +0 -0
  189. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/differentiation.txt +0 -0
  190. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/formative_assessment.txt +0 -0
  191. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/iep_modification.txt +0 -0
  192. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/lesson_plan.txt +0 -0
  193. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/master_content.txt +0 -0
  194. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/pacing_guide.txt +0 -0
  195. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/parent_note.txt +0 -0
  196. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/persona_extract.txt +0 -0
  197. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/quiz.txt +0 -0
  198. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/rubric.txt +0 -0
  199. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/student_packet.txt +0 -0
  200. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/sub_packet.txt +0 -0
  201. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/summative_assessment.txt +0 -0
  202. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/tiered_assignments.txt +0 -0
  203. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/unit_plan.txt +0 -0
  204. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/worksheet.txt +0 -0
  205. {clawed-2.3.7 → clawed-2.3.8}/clawed/prompts/year_map.txt +0 -0
  206. {clawed-2.3.7 → clawed-2.3.8}/clawed/reading_report.py +0 -0
  207. {clawed-2.3.7 → clawed-2.3.8}/clawed/router.py +0 -0
  208. {clawed-2.3.7 → clawed-2.3.8}/clawed/sanitize.py +0 -0
  209. {clawed-2.3.7 → clawed-2.3.8}/clawed/scheduler.py +0 -0
  210. {clawed-2.3.7 → clawed-2.3.8}/clawed/school.py +0 -0
  211. {clawed-2.3.7 → clawed-2.3.8}/clawed/search.py +0 -0
  212. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/__init__.py +0 -0
  213. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/art.py +0 -0
  214. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/base.py +0 -0
  215. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/computer_science.py +0 -0
  216. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/ela.py +0 -0
  217. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/foreign_language.py +0 -0
  218. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/history.py +0 -0
  219. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/library.py +0 -0
  220. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/math.py +0 -0
  221. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/music.py +0 -0
  222. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/physical_education.py +0 -0
  223. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/science.py +0 -0
  224. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/social_studies.py +0 -0
  225. {clawed-2.3.7 → clawed-2.3.8}/clawed/skills/special_education.py +0 -0
  226. {clawed-2.3.7 → clawed-2.3.8}/clawed/slide_images.py +0 -0
  227. {clawed-2.3.7 → clawed-2.3.8}/clawed/standards.py +0 -0
  228. {clawed-2.3.7 → clawed-2.3.8}/clawed/state.py +0 -0
  229. {clawed-2.3.7 → clawed-2.3.8}/clawed/state_standards.py +0 -0
  230. {clawed-2.3.7 → clawed-2.3.8}/clawed/student_bot.py +0 -0
  231. {clawed-2.3.7 → clawed-2.3.8}/clawed/student_cli.py +0 -0
  232. {clawed-2.3.7 → clawed-2.3.8}/clawed/student_telegram_bot.py +0 -0
  233. {clawed-2.3.7 → clawed-2.3.8}/clawed/sub_packet.py +0 -0
  234. {clawed-2.3.7 → clawed-2.3.8}/clawed/task_queue.py +0 -0
  235. {clawed-2.3.7 → clawed-2.3.8}/clawed/templates_lib.py +0 -0
  236. {clawed-2.3.7 → clawed-2.3.8}/clawed/tools.py +0 -0
  237. {clawed-2.3.7 → clawed-2.3.8}/clawed/transports/__init__.py +0 -0
  238. {clawed-2.3.7 → clawed-2.3.8}/clawed/transports/cli.py +0 -0
  239. {clawed-2.3.7 → clawed-2.3.8}/clawed/transports/openclaw.py +0 -0
  240. {clawed-2.3.7 → clawed-2.3.8}/clawed/transports/student_telegram.py +0 -0
  241. {clawed-2.3.7 → clawed-2.3.8}/clawed/transports/telegram.py +0 -0
  242. {clawed-2.3.7 → clawed-2.3.8}/clawed/transports/web.py +0 -0
  243. {clawed-2.3.7 → clawed-2.3.8}/clawed/tui.py +0 -0
  244. {clawed-2.3.7 → clawed-2.3.8}/clawed/tui_chat.py +0 -0
  245. {clawed-2.3.7 → clawed-2.3.8}/clawed/voice.py +0 -0
  246. {clawed-2.3.7 → clawed-2.3.8}/clawed/voice_check.py +0 -0
  247. {clawed-2.3.7 → clawed-2.3.8}/clawed/workspace.py +0 -0
  248. {clawed-2.3.7 → clawed-2.3.8}/eduagent/__init__.py +0 -0
  249. {clawed-2.3.7 → clawed-2.3.8}/eduagent/_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clawed
3
- Version: 2.3.7
3
+ Version: 2.3.8
4
4
  Summary: Claw-ED — personal AI teaching agent. Learns your voice, works while you sleep.
5
5
  Project-URL: Homepage, https://github.com/SirhanMacx/Claw-ED
6
6
  Project-URL: Documentation, https://github.com/SirhanMacx/Claw-ED#readme
@@ -82,6 +82,18 @@ Built on the OpenClaw agent framework. Open source. MIT license.
82
82
 
83
83
  ---
84
84
 
85
+ ## What's new in v2.3.8
86
+
87
+ **No more silent failures.** Every step of lesson generation now reports what happened — persona loading, material search, quality review, voice matching. If something fails, you'll know exactly what and why, with structured NLAH failure codes. Quality review runs automatically on every generated lesson and fails closed: if the review itself crashes, it reports failure instead of silently passing.
88
+
89
+ **Stricter quality gates.** Lessons now require at least 6 guided notes, 3 exit ticket questions, and 2 primary sources with actual text. Topic drift is caught automatically. Voice match scoring compares generated lessons against your teaching persona.
90
+
91
+ **Safer onboarding.** Teacher names and subjects are validated and truncated. Invalid grade levels get a re-prompt instead of being silently accepted. Demo mode can be forced with `CLAWED_DEMO=1` for presentations.
92
+
93
+ **Async cleanup.** Background ingestion no longer risks crashing on Python 3.12+ from nested event loops. The fix is shared across all async call sites.
94
+
95
+ ---
96
+
85
97
  ## What's new in v2.3.7
86
98
 
87
99
  **Real images in every lesson.** Image specs are now required for every primary source and instruction section across all subjects. The LLM generates specific search queries ("Thomas Nast Boss Tweed political cartoon 1871") instead of leaving the field blank. Teacher images are found first using a three-stage progressive search (full query, individual keywords, subject fallback) with filename-weighted scoring across up to 150 candidates. External sources (Library of Congress, Wikimedia Commons, Unsplash) fill in the rest with subject-aware routing.
@@ -13,6 +13,18 @@ Built on the OpenClaw agent framework. Open source. MIT license.
13
13
 
14
14
  ---
15
15
 
16
+ ## What's new in v2.3.8
17
+
18
+ **No more silent failures.** Every step of lesson generation now reports what happened — persona loading, material search, quality review, voice matching. If something fails, you'll know exactly what and why, with structured NLAH failure codes. Quality review runs automatically on every generated lesson and fails closed: if the review itself crashes, it reports failure instead of silently passing.
19
+
20
+ **Stricter quality gates.** Lessons now require at least 6 guided notes, 3 exit ticket questions, and 2 primary sources with actual text. Topic drift is caught automatically. Voice match scoring compares generated lessons against your teaching persona.
21
+
22
+ **Safer onboarding.** Teacher names and subjects are validated and truncated. Invalid grade levels get a re-prompt instead of being silently accepted. Demo mode can be forced with `CLAWED_DEMO=1` for presentations.
23
+
24
+ **Async cleanup.** Background ingestion no longer risks crashing on Python 3.12+ from nested event loops. The fix is shared across all async call sites.
25
+
26
+ ---
27
+
16
28
  ## What's new in v2.3.7
17
29
 
18
30
  **Real images in every lesson.** Image specs are now required for every primary source and instruction section across all subjects. The LLM generates specific search queries ("Thomas Nast Boss Tweed political cartoon 1871") instead of leaving the field blank. Teacher images are found first using a three-stage progressive search (full query, individual keywords, subject fallback) with filename-weighted scoring across up to 150 candidates. External sources (Library of Congress, Wikimedia Commons, Unsplash) fill in the rest with subject-aware routing.
@@ -17,7 +17,7 @@ if hasattr(sys.stderr, "reconfigure"):
17
17
  except Exception:
18
18
  pass
19
19
 
20
- __version__ = "2.3.7"
20
+ __version__ = "2.3.8"
21
21
  __author__ = "Jon Maccarello & Claw-ED contributors"
22
22
  __description__ = "Personal AI teaching agent. Learns your voice, works while you sleep."
23
23
 
@@ -339,7 +339,9 @@ class Gateway:
339
339
  # 2a. Load SOUL.md if available
340
340
  soul_context = ""
341
341
  try:
342
- soul_path = Path.home() / ".eduagent" / "workspace" / "SOUL.md"
342
+ import os
343
+ data_dir = os.environ.get("EDUAGENT_DATA_DIR", str(Path.home() / ".eduagent"))
344
+ soul_path = Path(data_dir) / "workspace" / "SOUL.md"
343
345
  if soul_path.exists():
344
346
  soul_context = soul_path.read_text(encoding="utf-8")[:2000]
345
347
  except Exception:
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
  from clawed.agent_core.context import AgentContext, ToolResult
9
+ from clawed.failure_codes import FailureCode
9
10
 
10
11
  logger = logging.getLogger(__name__)
11
12
 
@@ -89,14 +90,18 @@ class GenerateLessonBundleTool:
89
90
  f"this usually takes 2-4 minutes. I'll send everything when it's ready!"
90
91
  )
91
92
 
93
+ from clawed.generation_report import GenerationReport
94
+ report = GenerationReport()
95
+
92
96
  # ── Load config & persona from context ───────────────────────
93
97
  config = context.config
94
98
  persona = TeacherPersona()
95
99
  if context.persona:
96
100
  try:
97
101
  persona = TeacherPersona(**context.persona)
98
- except Exception:
99
- pass
102
+ except Exception as e:
103
+ logger.warning("NLAH_FAILURE=%s: %s", FailureCode.PERSONA_PARSE_ERROR, e)
104
+ report.warnings.append(f"[{FailureCode.PERSONA_PARSE_ERROR}] Could not parse persona: {e}")
100
105
 
101
106
  # ── Load state standards if teacher profile has a state ───────
102
107
  state = ""
@@ -127,7 +132,8 @@ class GenerateLessonBundleTool:
127
132
  len(assets), len(yt_links), topic,
128
133
  )
129
134
  except Exception as e:
130
- logger.debug("Asset search failed: %s", e)
135
+ logger.warning("NLAH_FAILURE=%s: %s", FailureCode.ASSET_SEARCH_FAILED, e)
136
+ report.warnings.append(f"[{FailureCode.ASSET_SEARCH_FAILED}] Asset search failed: {e}")
131
137
 
132
138
  # KB chunk-level search (text excerpts)
133
139
  try:
@@ -162,7 +168,8 @@ class GenerateLessonBundleTool:
162
168
  )
163
169
  logger.info("KB search found %d relevant chunks for '%s'", len(kb_parts), topic)
164
170
  except Exception as e:
165
- logger.debug("KB search failed: %s", e)
171
+ logger.warning("NLAH_FAILURE=%s: %s", FailureCode.KB_SEARCH_FAILED, e)
172
+ report.warnings.append(f"[{FailureCode.KB_SEARCH_FAILED}] KB search failed: {e}")
166
173
 
167
174
  # ── Build a UnitPlan with standards ──────────────────────────
168
175
  description = f"Introduction to {topic}"
@@ -205,14 +212,12 @@ class GenerateLessonBundleTool:
205
212
  teacher_materials=kb_prompt_section,
206
213
  )
207
214
  except Exception as e:
208
- return ToolResult(text=f"Failed to generate lesson: {e}")
215
+ logger.error("NLAH_FAILURE=%s: %s", FailureCode.API_FAILURE, e)
216
+ return ToolResult(text=f"[{FailureCode.API_FAILURE}] Failed to generate lesson: {type(e).__name__}")
209
217
 
210
218
  # ── Validate ──────────────────────────────────────────────────
211
- from clawed.generation_report import GenerationReport
212
219
  from clawed.validation import check_self_contained, validate_alignment, validate_master_content
213
220
 
214
- report = GenerationReport()
215
-
216
221
  mc_errors = validate_master_content(master, topic)
217
222
  for err in mc_errors:
218
223
  report.warnings.append(err)
@@ -283,6 +288,45 @@ class GenerateLessonBundleTool:
283
288
  logger.error("Slides compile failed: %s", e)
284
289
  errors.append(f"Slideshow PPTX failed: {e}")
285
290
 
291
+ # ── Quality review (NLAH Stage 4 — non-blocking) ────────────
292
+ try:
293
+ from clawed.llm import LLMClient
294
+ from clawed.quality import score_voice_match
295
+
296
+ llm = LLMClient(config=config)
297
+ master_json = master.model_dump_json(indent=2)[:3000]
298
+ review = await llm.review_lesson_package(
299
+ lesson_json=master_json,
300
+ standards_present=bool(standards_list),
301
+ has_handout=len(generated_files) >= 2,
302
+ has_slideshow=len(generated_files) >= 3,
303
+ )
304
+ report.quality_review_passed = review.get("passed", False)
305
+ report.quality_review_issues = review.get("issues", [])
306
+ if not report.quality_review_passed:
307
+ for issue in report.quality_review_issues:
308
+ report.warnings.append(f"[REVIEW] {issue}")
309
+ logger.info("Quality review: FAILED — %d issues", len(report.quality_review_issues))
310
+ else:
311
+ logger.info("Quality review: PASSED")
312
+
313
+ # Voice match scoring
314
+ persona_ctx = persona.to_prompt_context() if persona else ""
315
+ all_text = " ".join(s.content for s in master.direct_instruction)
316
+ voice_score = await score_voice_match(all_text, persona_ctx, llm)
317
+ report.voice_check_passed = voice_score >= 3.0
318
+ if voice_score < 3.0:
319
+ report.warnings.append(
320
+ f"[{FailureCode.VOICE_MISMATCH}] Voice match score: {voice_score:.1f}/5.0"
321
+ )
322
+ logger.warning("Voice match: %.1f/5.0 — below threshold", voice_score)
323
+ else:
324
+ logger.info("Voice match: %.1f/5.0", voice_score)
325
+ except Exception as e:
326
+ logger.warning("Quality review/voice check failed: %s", e)
327
+ report.quality_review_passed = False
328
+ report.quality_review_issues = [f"Review failed: {type(e).__name__}"]
329
+
286
330
  # ── Build response ─────────────────────────────────────────────
287
331
  lines = []
288
332
 
@@ -266,7 +266,9 @@ def create_app() -> FastAPI:
266
266
  filter_html = ""
267
267
  if all_subjects:
268
268
  opts = "".join(
269
- f"<option value='{html_mod.escape(s)}' {'selected' if s == subject_filter else ''}>{html_mod.escape(s)}</option>"
269
+ f"<option value='{html_mod.escape(s)}'"
270
+ f" {'selected' if s == subject_filter else ''}"
271
+ f">{html_mod.escape(s)}</option>"
270
272
  for s in all_subjects
271
273
  )
272
274
  filter_html += (
@@ -275,7 +277,9 @@ def create_app() -> FastAPI:
275
277
  )
276
278
  if all_grades:
277
279
  opts = "".join(
278
- f"<option value='{html_mod.escape(g)}' {'selected' if g == grade_filter else ''}>{html_mod.escape(g)}</option>"
280
+ f"<option value='{html_mod.escape(g)}'"
281
+ f" {'selected' if g == grade_filter else ''}"
282
+ f">{html_mod.escape(g)}</option>"
279
283
  for g in all_grades
280
284
  )
281
285
  filter_html += (
@@ -0,0 +1,22 @@
1
+ """Shared async utility for running coroutines from sync or async contexts."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+
6
+
7
+ def run_async_safe(coro):
8
+ """Run an async coroutine, handling both sync and async calling contexts.
9
+
10
+ When called from inside a running event loop (e.g., agent_core tools),
11
+ uses a thread to avoid nested asyncio.run() errors. When called from
12
+ plain sync code (CLI), uses asyncio.run() directly.
13
+ """
14
+ try:
15
+ asyncio.get_running_loop()
16
+ # We're inside an event loop — run in a worker thread
17
+ import concurrent.futures
18
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
19
+ return pool.submit(asyncio.run, coro).result(timeout=30)
20
+ except RuntimeError:
21
+ # No running loop — safe to use asyncio.run()
22
+ return asyncio.run(coro)
@@ -34,11 +34,12 @@ def load_all_demos() -> dict[str, dict[str, Any]]:
34
34
  def is_demo_mode(config: Any = None) -> bool:
35
35
  """Check whether the app should run in demo mode (no API key configured).
36
36
 
37
- Args:
38
- config: Optional AppConfig instance. When provided, the check uses
39
- this config instead of loading global state. This allows
40
- LLMClient to pass its own injected config.
37
+ Can be forced with CLAWED_DEMO=1 environment variable, which overrides
38
+ stored keys. This is useful for demo presentations and development.
41
39
  """
40
+ import os
41
+ if os.environ.get("CLAWED_DEMO", "").strip() in ("1", "true", "yes"):
42
+ return True
42
43
  from clawed.config import resolve_credentials
43
44
  provider, key = resolve_credentials(config)
44
45
  return provider is None
@@ -5,12 +5,12 @@ Generates full lesson plan documents and print-ready student worksheets.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- import asyncio
9
8
  import logging
10
9
  from datetime import date
11
10
  from pathlib import Path
12
11
  from typing import TYPE_CHECKING, Any
13
12
 
13
+ from clawed.async_utils import run_async_safe
14
14
  from clawed.export_theme import _resolve_output, get_color_theme
15
15
 
16
16
  if TYPE_CHECKING:
@@ -19,24 +19,6 @@ if TYPE_CHECKING:
19
19
  logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
- def _run_async_safe(coro):
23
- """Run an async coroutine, handling both sync and async calling contexts.
24
-
25
- When called from inside a running event loop (e.g., agent_core tools),
26
- uses a thread to avoid nested asyncio.run() errors. When called from
27
- plain sync code (CLI), uses asyncio.run() directly.
28
- """
29
- try:
30
- asyncio.get_running_loop()
31
- # We're inside an event loop — run in a worker thread
32
- import concurrent.futures
33
- with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
34
- return pool.submit(asyncio.run, coro).result(timeout=30)
35
- except RuntimeError:
36
- # No running loop — safe to use asyncio.run()
37
- return asyncio.run(coro)
38
-
39
-
40
22
  # ── Image helpers (DOCX-specific) ─────────────────────────────────────
41
23
 
42
24
 
@@ -57,7 +39,7 @@ def _docx_add_image(
57
39
 
58
40
  from clawed.slide_images import fetch_slide_image
59
41
 
60
- img_path = _run_async_safe(fetch_slide_image(topic, subject=subject))
42
+ img_path = run_async_safe(fetch_slide_image(topic, subject=subject))
61
43
  if img_path and img_path.exists():
62
44
  doc.add_picture(str(img_path), width=Inches(width_inches))
63
45
  # Center the image
@@ -97,7 +79,7 @@ def _docx_add_content_image(
97
79
 
98
80
  from clawed.slide_images import _extract_key_concepts, fetch_content_image
99
81
 
100
- img_path = _run_async_safe(
82
+ img_path = run_async_safe(
101
83
  fetch_content_image(
102
84
  content_text,
103
85
  subject=subject,
@@ -12,6 +12,7 @@ from datetime import date
12
12
  from pathlib import Path
13
13
  from typing import TYPE_CHECKING, Optional
14
14
 
15
+ from clawed.async_utils import run_async_safe
15
16
  from clawed.export_theme import _hex_to_rgb, _resolve_output, get_color_theme
16
17
 
17
18
  if TYPE_CHECKING:
@@ -109,24 +110,6 @@ def _section_divider(prs, slide_num, text, theme, slide_w, slide_h):
109
110
  # ── Image fetching ────────────────────────────────────────────────────
110
111
 
111
112
 
112
- def _run_async_safe(coro):
113
- """Run an async coroutine, handling both sync and async calling contexts.
114
-
115
- When called from inside a running event loop (e.g., agent_core tools),
116
- uses a thread to avoid nested asyncio.run() errors. When called from
117
- plain sync code (CLI), uses asyncio.run() directly.
118
- """
119
- try:
120
- asyncio.get_running_loop()
121
- # We're inside an event loop — run in a worker thread
122
- import concurrent.futures
123
- with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
124
- return pool.submit(asyncio.run, coro).result(timeout=30)
125
- except RuntimeError:
126
- # No running loop — safe to use asyncio.run()
127
- return asyncio.run(coro)
128
-
129
-
130
113
  def _try_fetch_images(topics: list[tuple[str, str]], subject: str) -> dict[str, Optional[Path]]:
131
114
  """Attempt to fetch images for multiple topics. Non-blocking, short timeout.
132
115
 
@@ -148,7 +131,7 @@ def _try_fetch_images(topics: list[tuple[str, str]], subject: str) -> dict[str,
148
131
  results[key] = None
149
132
 
150
133
  try:
151
- _run_async_safe(_fetch_all())
134
+ run_async_safe(_fetch_all())
152
135
  except Exception as e:
153
136
  logger.debug("Image fetching failed: %s", e)
154
137
 
@@ -200,7 +183,7 @@ def _try_fetch_content_images(
200
183
  results[key] = None
201
184
 
202
185
  try:
203
- _run_async_safe(_fetch_all())
186
+ run_async_safe(_fetch_all())
204
187
  except Exception as e:
205
188
  logger.debug("Content image fetching failed: %s", e)
206
189
 
@@ -0,0 +1,22 @@
1
+ """NLAH failure taxonomy — structured failure codes for generation pipeline."""
2
+ from __future__ import annotations
3
+
4
+ from enum import Enum
5
+
6
+
7
+ class FailureCode(str, Enum):
8
+ """Machine-parseable failure codes per NLAH Section 6."""
9
+
10
+ NO_PERSONA = "NO_PERSONA"
11
+ SCHEMA_ERROR = "SCHEMA_ERROR"
12
+ TOPIC_DRIFT = "TOPIC_DRIFT"
13
+ DEMO_FIXTURE = "DEMO_FIXTURE"
14
+ EXPORT_INCOMPLETE = "EXPORT_INCOMPLETE"
15
+ EXPORT_ERROR = "EXPORT_ERROR"
16
+ REVIEW_FAILED = "REVIEW_FAILED"
17
+ CONTEXT_EXCEEDED = "CONTEXT_EXCEEDED"
18
+ API_FAILURE = "API_FAILURE"
19
+ VOICE_MISMATCH = "VOICE_MISMATCH"
20
+ PERSONA_PARSE_ERROR = "PERSONA_PARSE_ERROR"
21
+ KB_SEARCH_FAILED = "KB_SEARCH_FAILED"
22
+ ASSET_SEARCH_FAILED = "ASSET_SEARCH_FAILED"
@@ -624,12 +624,11 @@ async def handle_connect_local(
624
624
  progress_callback=_progress,
625
625
  )
626
626
  if docs:
627
- import asyncio
628
-
627
+ from clawed.async_utils import run_async_safe
629
628
  from clawed.persona import extract_persona
630
629
 
631
630
  persona_cfg = AppConfig.load()
632
- persona = asyncio.run(extract_persona(docs, persona_cfg))
631
+ persona = run_async_safe(extract_persona(docs, persona_cfg))
633
632
  session.persona = persona
634
633
  session.save()
635
634
  notify_callback(
@@ -5,12 +5,15 @@ Extracted from tg.py lines 1156-1263.
5
5
  """
6
6
  from __future__ import annotations
7
7
 
8
+ import logging
8
9
  import re
9
10
  from enum import Enum
10
11
 
11
12
  from clawed.gateway_response import GatewayResponse
12
13
  from clawed.models import AppConfig, TeacherPersona, TeacherProfile
13
14
 
15
+ logger = logging.getLogger(__name__)
16
+
14
17
 
15
18
  class OnboardState(Enum):
16
19
  ASK_SUBJECT = "ask_subject"
@@ -84,7 +87,7 @@ class OnboardHandler:
84
87
 
85
88
  if current == OnboardState.ASK_SUBJECT:
86
89
  grade, subject = _parse_grade_and_subject(text)
87
- state["subject"] = subject if subject else text.strip().title()
90
+ state["subject"] = (subject if subject else text.strip().title())[:100]
88
91
  if grade:
89
92
  state["grade"] = grade
90
93
  state["step"] = OnboardState.ASK_NAME
@@ -103,14 +106,21 @@ class OnboardHandler:
103
106
  elif re.search(r"(?:kindergarten|kinder|pre-?k)", text, re.IGNORECASE):
104
107
  state["grade"] = "K"
105
108
  else:
106
- state["grade"] = text.strip()
109
+ # Invalid grade re-prompt
110
+ return GatewayResponse(
111
+ text="I didn't catch that — what grade level? (K, 1-12, or Pre-K)"
112
+ )
107
113
  state["step"] = OnboardState.ASK_NAME
108
114
  return GatewayResponse(
109
115
  text=f"Grade {state['grade']} — got it.\n\nWhat's your name?"
110
116
  )
111
117
 
112
118
  if current == OnboardState.ASK_NAME:
113
- state["name"] = text.strip()
119
+ name = text.strip()[:100]
120
+ name = re.sub(r'[^\w\s\'-]', '', name).strip()
121
+ if not name:
122
+ return GatewayResponse(text="I need a name to personalize your lessons. What should I call you?")
123
+ state["name"] = name
114
124
  return self._complete_onboarding(teacher_id)
115
125
 
116
126
  return GatewayResponse(text="Something went wrong with setup. Try /start again.")
@@ -132,8 +142,8 @@ class OnboardHandler:
132
142
  from clawed.workspace import init_workspace
133
143
  persona = TeacherPersona(name=state["name"], subject_area=state["subject"])
134
144
  init_workspace(persona, config)
135
- except Exception:
136
- pass
145
+ except Exception as e:
146
+ logger.warning("Workspace init failed during onboarding: %s", e)
137
147
 
138
148
  del self._state[teacher_id]
139
149
 
@@ -457,7 +457,7 @@ def _extract_xlsx(path: Path) -> str:
457
457
  rows.append(f"[Sheet: {sheet}]")
458
458
  for i, row in enumerate(ws.iter_rows(values_only=True)):
459
459
  if i >= 5000:
460
- rows.append(f"... (truncated at 5000 rows)")
460
+ rows.append("... (truncated at 5000 rows)")
461
461
  break
462
462
  cells = [str(c) if c is not None else "" for c in row]
463
463
  if any(cells):
@@ -22,7 +22,9 @@ def _build_system_prompt(
22
22
  persona_context = persona.to_prompt_context() if persona else ""
23
23
  soul_context = ""
24
24
  try:
25
- soul_path = Path.home() / ".eduagent" / "workspace" / "SOUL.md"
25
+ import os
26
+ data_dir = os.environ.get("EDUAGENT_DATA_DIR", str(Path.home() / ".eduagent"))
27
+ soul_path = Path(data_dir) / "workspace" / "SOUL.md"
26
28
  if soul_path.exists():
27
29
  soul_context = soul_path.read_text(encoding="utf-8")[:2000]
28
30
  except Exception:
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import logging
6
7
  import os
7
8
  from typing import Any, Optional, Type
8
9
 
@@ -11,6 +12,8 @@ from pydantic import BaseModel, ValidationError
11
12
 
12
13
  from clawed.models import AppConfig, LLMProvider
13
14
 
15
+ logger = logging.getLogger(__name__)
16
+
14
17
 
15
18
  class LLMClient:
16
19
  """Unified async LLM client for all supported backends."""
@@ -414,35 +417,39 @@ class LLMClient:
414
417
  """Self-review a lesson package against observation-ready standards.
415
418
 
416
419
  Returns a dict with 'passed' (bool) and 'issues' (list of strings).
417
- If issues are found, the caller should attempt to fix them.
420
+ Fails closed: any exception returns passed=False (NLAH Section 3, Stage 4).
418
421
  """
419
- prompt = (
420
- f"Review this lesson package against observation-ready quality standards.\n\n"
421
- f"Lesson:\n{lesson_json[:3000]}\n\n"
422
- f"Package status: standards={'yes' if standards_present else 'MISSING'}, "
423
- f"handout={'yes' if has_handout else 'MISSING'}, "
424
- f"slideshow={'yes' if has_slideshow else 'MISSING'}\n\n"
425
- "Check these standards:\n"
426
- "1. Do all section times add up to a full class period (42-45 min)?\n"
427
- "2. Are specific standards codes listed (not just 'aligned to standards')?\n"
428
- "3. Is vocabulary defined for all content-specific terms?\n"
429
- "4. Are there checks for understanding every 7-10 minutes?\n"
430
- "5. Are all referenced materials self-contained (no phantom handouts)?\n"
431
- "6. Is the Do Now completable in 5 minutes?\n"
432
- "7. Are transitions scripted between sections?\n\n"
433
- 'Return JSON: {"passed": true/false, "issues": ["issue 1", "issue 2"]}'
434
- )
435
- raw = await self.generate(prompt, temperature=0.2, max_tokens=1000)
436
422
  try:
423
+ prompt = (
424
+ f"Review this lesson package against observation-ready quality standards.\n\n"
425
+ f"Lesson:\n{lesson_json[:3000]}\n\n"
426
+ f"Package status: standards={'yes' if standards_present else 'MISSING'}, "
427
+ f"handout={'yes' if has_handout else 'MISSING'}, "
428
+ f"slideshow={'yes' if has_slideshow else 'MISSING'}\n\n"
429
+ "Check these standards:\n"
430
+ "1. Do all section times add up to a full class period (42-45 min)?\n"
431
+ "2. Are specific standards codes listed (not just 'aligned to standards')?\n"
432
+ "3. Is vocabulary defined for all content-specific terms?\n"
433
+ "4. Are there checks for understanding every 7-10 minutes?\n"
434
+ "5. Are all referenced materials self-contained (no phantom handouts)?\n"
435
+ "6. Is the Do Now completable in 5 minutes?\n"
436
+ "7. Are transitions scripted between sections?\n\n"
437
+ 'Return JSON: {"passed": true/false, "issues": ["issue 1", "issue 2"]}'
438
+ )
439
+ raw = await self.generate(prompt, temperature=0.2, max_tokens=1000)
437
440
  cleaned = raw.strip()
438
441
  if cleaned.startswith("```"):
439
442
  lines = cleaned.split("\n")[1:]
440
443
  if lines and lines[-1].strip() == "```":
441
444
  lines = lines[:-1]
442
445
  cleaned = "\n".join(lines)
443
- return json.loads(cleaned)
444
- except Exception:
445
- return {"passed": False, "issues": ["Quality review could not parse LLM response review skipped"]}
446
+ result = json.loads(cleaned)
447
+ if "passed" not in result:
448
+ return {"passed": False, "issues": ["LLM response missing 'passed' field"]}
449
+ return result
450
+ except Exception as e:
451
+ logger.warning("REVIEW_FAILED: %s", e)
452
+ return {"passed": False, "issues": [f"Quality review failed: {type(e).__name__}"]}
446
453
 
447
454
  # ── Anthropic ────────────────────────────────────────────────────────
448
455
 
@@ -131,3 +131,54 @@ class LessonQualityScore:
131
131
  if materials.assessment_questions:
132
132
  parts.append(f"Assessment: {len(materials.assessment_questions)} questions")
133
133
  return "\n".join(parts)
134
+
135
+
136
+ async def score_voice_match(
137
+ lesson_text: str,
138
+ persona_context: str,
139
+ llm_client: Any = None,
140
+ ) -> float:
141
+ """Score how well lesson text matches the teacher's voice (1.0-5.0).
142
+
143
+ Uses an LLM to compare the lesson language, tone, scaffolding style,
144
+ and pedagogical patterns against the teacher persona.
145
+
146
+ Returns 3.0 (neutral) if scoring fails — NLAH: do not block delivery.
147
+ """
148
+ if not persona_context or not lesson_text:
149
+ return 3.0
150
+
151
+ prompt = (
152
+ "Rate how well this lesson matches the teacher's established voice "
153
+ "and teaching style on a scale of 1.0 to 5.0.\n\n"
154
+ "Teacher persona:\n"
155
+ f"{persona_context[:1500]}\n\n"
156
+ "Lesson excerpt:\n"
157
+ f"{lesson_text[:2000]}\n\n"
158
+ "Score criteria:\n"
159
+ "5.0 = Sounds exactly like this teacher wrote it\n"
160
+ "4.0 = Captures most of their style and patterns\n"
161
+ "3.0 = Generic but acceptable\n"
162
+ "2.0 = Noticeably different from their voice\n"
163
+ "1.0 = Completely wrong tone/style\n\n"
164
+ 'Return ONLY a JSON object: {"score": 4.2, "reason": "brief explanation"}'
165
+ )
166
+
167
+ if llm_client is None:
168
+ return 3.0 # No LLM available, neutral score
169
+
170
+ try:
171
+ import json as _json
172
+
173
+ raw = await llm_client.generate(prompt, temperature=0.2, max_tokens=200)
174
+ cleaned = raw.strip()
175
+ if cleaned.startswith("```"):
176
+ lines = cleaned.split("\n")[1:]
177
+ if lines and lines[-1].strip() == "```":
178
+ lines = lines[:-1]
179
+ cleaned = "\n".join(lines)
180
+ result = _json.loads(cleaned)
181
+ score = float(result.get("score", 3.0))
182
+ return max(1.0, min(5.0, score)) # Clamp to valid range
183
+ except Exception:
184
+ return 3.0 # Fail neutral — NLAH: do not block delivery