clawed 2.3.5__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 (250) hide show
  1. {clawed-2.3.5 → clawed-2.3.8}/PKG-INFO +28 -14
  2. {clawed-2.3.5 → clawed-2.3.8}/README.md +23 -11
  3. {clawed-2.3.5 → clawed-2.3.8}/clawed/__init__.py +1 -1
  4. {clawed-2.3.5 → clawed-2.3.8}/clawed/_legacy_gateway.py +11 -9
  5. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/context.py +13 -1
  6. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/core.py +25 -25
  7. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/memory/curriculum_kb.py +45 -1
  8. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/memory/embeddings.py +13 -4
  9. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/prompt.py +3 -0
  10. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/generate_lesson_bundle.py +58 -8
  11. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/ingest_materials.py +10 -1
  12. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/read_workspace.py +3 -1
  13. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/search_my_materials.py +15 -2
  14. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/server.py +36 -18
  15. {clawed-2.3.5 → clawed-2.3.8}/clawed/asset_registry.py +57 -8
  16. clawed-2.3.8/clawed/async_utils.py +22 -0
  17. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/__init__.py +5 -4
  18. {clawed-2.3.5 → clawed-2.3.8}/clawed/export_docx.py +3 -21
  19. {clawed-2.3.5 → clawed-2.3.8}/clawed/export_pptx.py +3 -20
  20. clawed-2.3.8/clawed/failure_codes.py +22 -0
  21. {clawed-2.3.5 → clawed-2.3.8}/clawed/generation.py +2 -3
  22. clawed-2.3.8/clawed/handlers/ingest.py +283 -0
  23. {clawed-2.3.5 → clawed-2.3.8}/clawed/handlers/onboard.py +15 -5
  24. {clawed-2.3.5 → clawed-2.3.8}/clawed/image_pipeline.py +7 -5
  25. {clawed-2.3.5 → clawed-2.3.8}/clawed/ingestor.py +291 -1
  26. {clawed-2.3.5 → clawed-2.3.8}/clawed/lesson.py +5 -3
  27. {clawed-2.3.5 → clawed-2.3.8}/clawed/llm.py +38 -25
  28. {clawed-2.3.5 → clawed-2.3.8}/clawed/master_content.py +18 -0
  29. {clawed-2.3.5 → clawed-2.3.8}/clawed/model_router.py +4 -3
  30. {clawed-2.3.5 → clawed-2.3.8}/clawed/models.py +1 -0
  31. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/master_content.txt +43 -4
  32. {clawed-2.3.5 → clawed-2.3.8}/clawed/quality.py +51 -0
  33. {clawed-2.3.5 → clawed-2.3.8}/clawed/slide_images.py +239 -81
  34. {clawed-2.3.5 → clawed-2.3.8}/clawed/tools.py +3 -2
  35. {clawed-2.3.5 → clawed-2.3.8}/clawed/transports/telegram.py +14 -1
  36. {clawed-2.3.5 → clawed-2.3.8}/clawed/validation.py +24 -11
  37. {clawed-2.3.5 → clawed-2.3.8}/pyproject.toml +4 -3
  38. clawed-2.3.5/clawed/handlers/ingest.py +0 -153
  39. {clawed-2.3.5 → clawed-2.3.8}/.gitignore +0 -0
  40. {clawed-2.3.5 → clawed-2.3.8}/LICENSE +0 -0
  41. {clawed-2.3.5 → clawed-2.3.8}/clawed/__main__.py +0 -0
  42. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent.py +0 -0
  43. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/__init__.py +0 -0
  44. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/approvals.py +0 -0
  45. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/autonomy.py +0 -0
  46. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/custom_tools.py +0 -0
  47. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/drive/__init__.py +0 -0
  48. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/drive/auth.py +0 -0
  49. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/drive/client.py +0 -0
  50. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/fake_llm.py +0 -0
  51. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/loop.py +0 -0
  52. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/memory/__init__.py +0 -0
  53. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/memory/curriculum.py +0 -0
  54. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/memory/episodes.py +0 -0
  55. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/memory/identity.py +0 -0
  56. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/memory/loader.py +0 -0
  57. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/memory/preferences.py +0 -0
  58. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/planner.py +0 -0
  59. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/scheduler.py +0 -0
  60. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/__init__.py +0 -0
  61. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/base.py +0 -0
  62. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/configure_profile.py +0 -0
  63. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/curriculum_map.py +0 -0
  64. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/drive_create_doc.py +0 -0
  65. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/drive_create_slides.py +0 -0
  66. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/drive_list.py +0 -0
  67. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/drive_organize.py +0 -0
  68. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/drive_read.py +0 -0
  69. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/drive_upload.py +0 -0
  70. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/export_document.py +0 -0
  71. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/gap_analysis.py +0 -0
  72. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/generate_assessment.py +0 -0
  73. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/generate_lesson.py +0 -0
  74. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/generate_materials.py +0 -0
  75. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/generate_unit.py +0 -0
  76. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/parent_comm.py +0 -0
  77. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/read_heartbeat.py +0 -0
  78. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/request_approval.py +0 -0
  79. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/schedule_task.py +0 -0
  80. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/search_lessons.py +0 -0
  81. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/search_standards.py +0 -0
  82. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/student_insights.py +0 -0
  83. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/sub_packet.py +0 -0
  84. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/switch_model.py +0 -0
  85. {clawed-2.3.5 → clawed-2.3.8}/clawed/agent_core/tools/update_soul.py +0 -0
  86. {clawed-2.3.5 → clawed-2.3.8}/clawed/analytics.py +0 -0
  87. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/__init__.py +0 -0
  88. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/deps.py +0 -0
  89. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/__init__.py +0 -0
  90. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/chat.py +0 -0
  91. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/export.py +0 -0
  92. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/feedback.py +0 -0
  93. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/gateway_chat.py +0 -0
  94. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/generate.py +0 -0
  95. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/ingest.py +0 -0
  96. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/lessons.py +0 -0
  97. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/school.py +0 -0
  98. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/settings.py +0 -0
  99. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/routes/tools.py +0 -0
  100. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/static/app.js +0 -0
  101. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/static/style.css +0 -0
  102. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/static/widget.js +0 -0
  103. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/analytics.html +0 -0
  104. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/base.html +0 -0
  105. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/dashboard.html +0 -0
  106. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/generate.html +0 -0
  107. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/index.html +0 -0
  108. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/lesson.html +0 -0
  109. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/profile.html +0 -0
  110. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/settings.html +0 -0
  111. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/stats.html +0 -0
  112. {clawed-2.3.5 → clawed-2.3.8}/clawed/api/templates/students.html +0 -0
  113. {clawed-2.3.5 → clawed-2.3.8}/clawed/assessment.py +0 -0
  114. {clawed-2.3.5 → clawed-2.3.8}/clawed/auth/__init__.py +0 -0
  115. {clawed-2.3.5 → clawed-2.3.8}/clawed/auth/google_auth.py +0 -0
  116. {clawed-2.3.5 → clawed-2.3.8}/clawed/bot_state.py +0 -0
  117. {clawed-2.3.5 → clawed-2.3.8}/clawed/chat.py +0 -0
  118. {clawed-2.3.5 → clawed-2.3.8}/clawed/cli.py +0 -0
  119. {clawed-2.3.5 → clawed-2.3.8}/clawed/cli_chat.py +0 -0
  120. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/__init__.py +0 -0
  121. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/_helpers.py +0 -0
  122. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/bot.py +0 -0
  123. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/config.py +0 -0
  124. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/config_llm.py +0 -0
  125. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/config_profile.py +0 -0
  126. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/export.py +0 -0
  127. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/generate.py +0 -0
  128. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/generate_assessment.py +0 -0
  129. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/generate_unit.py +0 -0
  130. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/queue.py +0 -0
  131. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/schedule_cmd.py +0 -0
  132. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/sub.py +0 -0
  133. {clawed-2.3.5 → clawed-2.3.8}/clawed/commands/workspace_cmd.py +0 -0
  134. {clawed-2.3.5 → clawed-2.3.8}/clawed/compile_slides.py +0 -0
  135. {clawed-2.3.5 → clawed-2.3.8}/clawed/compile_student.py +0 -0
  136. {clawed-2.3.5 → clawed-2.3.8}/clawed/compile_teacher.py +0 -0
  137. {clawed-2.3.5 → clawed-2.3.8}/clawed/config.py +0 -0
  138. {clawed-2.3.5 → clawed-2.3.8}/clawed/corpus.py +0 -0
  139. {clawed-2.3.5 → clawed-2.3.8}/clawed/curriculum_map.py +0 -0
  140. {clawed-2.3.5 → clawed-2.3.8}/clawed/database.py +0 -0
  141. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_assessment.json +0 -0
  142. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_formative_assessment.json +0 -0
  143. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_lesson_materials.json +0 -0
  144. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_lesson_science_g6.json +0 -0
  145. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_lesson_social_studies_g8.json +0 -0
  146. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_master_content.json +0 -0
  147. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_pacing_guide.json +0 -0
  148. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_quiz.json +0 -0
  149. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_rubric.json +0 -0
  150. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_unit_plan.json +0 -0
  151. {clawed-2.3.5 → clawed-2.3.8}/clawed/demo/demo_year_map.json +0 -0
  152. {clawed-2.3.5 → clawed-2.3.8}/clawed/differentiation.py +0 -0
  153. {clawed-2.3.5 → clawed-2.3.8}/clawed/doc_export.py +0 -0
  154. {clawed-2.3.5 → clawed-2.3.8}/clawed/drive.py +0 -0
  155. {clawed-2.3.5 → clawed-2.3.8}/clawed/evaluation.py +0 -0
  156. {clawed-2.3.5 → clawed-2.3.8}/clawed/export_handout.py +0 -0
  157. {clawed-2.3.5 → clawed-2.3.8}/clawed/export_markdown.py +0 -0
  158. {clawed-2.3.5 → clawed-2.3.8}/clawed/export_pdf.py +0 -0
  159. {clawed-2.3.5 → clawed-2.3.8}/clawed/export_templates.py +0 -0
  160. {clawed-2.3.5 → clawed-2.3.8}/clawed/export_theme.py +0 -0
  161. {clawed-2.3.5 → clawed-2.3.8}/clawed/exporter.py +0 -0
  162. {clawed-2.3.5 → clawed-2.3.8}/clawed/feedback.py +0 -0
  163. {clawed-2.3.5 → clawed-2.3.8}/clawed/formats/__init__.py +0 -0
  164. {clawed-2.3.5 → clawed-2.3.8}/clawed/formats/flipchart.py +0 -0
  165. {clawed-2.3.5 → clawed-2.3.8}/clawed/formats/notebook.py +0 -0
  166. {clawed-2.3.5 → clawed-2.3.8}/clawed/formats/xbk.py +0 -0
  167. {clawed-2.3.5 → clawed-2.3.8}/clawed/gateway.py +0 -0
  168. {clawed-2.3.5 → clawed-2.3.8}/clawed/gateway_response.py +0 -0
  169. {clawed-2.3.5 → clawed-2.3.8}/clawed/generation_report.py +0 -0
  170. {clawed-2.3.5 → clawed-2.3.8}/clawed/handlers/__init__.py +0 -0
  171. {clawed-2.3.5 → clawed-2.3.8}/clawed/handlers/export.py +0 -0
  172. {clawed-2.3.5 → clawed-2.3.8}/clawed/handlers/feedback.py +0 -0
  173. {clawed-2.3.5 → clawed-2.3.8}/clawed/handlers/gaps.py +0 -0
  174. {clawed-2.3.5 → clawed-2.3.8}/clawed/handlers/generate.py +0 -0
  175. {clawed-2.3.5 → clawed-2.3.8}/clawed/handlers/misc.py +0 -0
  176. {clawed-2.3.5 → clawed-2.3.8}/clawed/handlers/schedule.py +0 -0
  177. {clawed-2.3.5 → clawed-2.3.8}/clawed/handlers/standards.py +0 -0
  178. {clawed-2.3.5 → clawed-2.3.8}/clawed/improver.py +0 -0
  179. {clawed-2.3.5 → clawed-2.3.8}/clawed/io.py +0 -0
  180. {clawed-2.3.5 → clawed-2.3.8}/clawed/materials.py +0 -0
  181. {clawed-2.3.5 → clawed-2.3.8}/clawed/mcp_server.py +0 -0
  182. {clawed-2.3.5 → clawed-2.3.8}/clawed/memory_engine.py +0 -0
  183. {clawed-2.3.5 → clawed-2.3.8}/clawed/onboarding.py +0 -0
  184. {clawed-2.3.5 → clawed-2.3.8}/clawed/openclaw_plugin.py +0 -0
  185. {clawed-2.3.5 → clawed-2.3.8}/clawed/parent_comm.py +0 -0
  186. {clawed-2.3.5 → clawed-2.3.8}/clawed/persona.py +0 -0
  187. {clawed-2.3.5 → clawed-2.3.8}/clawed/persona_evolution.py +0 -0
  188. {clawed-2.3.5 → clawed-2.3.8}/clawed/planner.py +0 -0
  189. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/504_accommodations.txt +0 -0
  190. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/admin_lesson_plan.txt +0 -0
  191. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/assessment.txt +0 -0
  192. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/curriculum_gaps.txt +0 -0
  193. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/dbq_assessment.txt +0 -0
  194. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/differentiation.txt +0 -0
  195. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/formative_assessment.txt +0 -0
  196. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/iep_modification.txt +0 -0
  197. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/lesson_plan.txt +0 -0
  198. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/pacing_guide.txt +0 -0
  199. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/parent_note.txt +0 -0
  200. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/persona_extract.txt +0 -0
  201. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/quiz.txt +0 -0
  202. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/rubric.txt +0 -0
  203. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/student_packet.txt +0 -0
  204. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/sub_packet.txt +0 -0
  205. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/summative_assessment.txt +0 -0
  206. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/tiered_assignments.txt +0 -0
  207. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/unit_plan.txt +0 -0
  208. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/worksheet.txt +0 -0
  209. {clawed-2.3.5 → clawed-2.3.8}/clawed/prompts/year_map.txt +0 -0
  210. {clawed-2.3.5 → clawed-2.3.8}/clawed/reading_report.py +0 -0
  211. {clawed-2.3.5 → clawed-2.3.8}/clawed/router.py +0 -0
  212. {clawed-2.3.5 → clawed-2.3.8}/clawed/sanitize.py +0 -0
  213. {clawed-2.3.5 → clawed-2.3.8}/clawed/scheduler.py +0 -0
  214. {clawed-2.3.5 → clawed-2.3.8}/clawed/school.py +0 -0
  215. {clawed-2.3.5 → clawed-2.3.8}/clawed/search.py +0 -0
  216. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/__init__.py +0 -0
  217. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/art.py +0 -0
  218. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/base.py +0 -0
  219. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/computer_science.py +0 -0
  220. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/ela.py +0 -0
  221. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/foreign_language.py +0 -0
  222. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/history.py +0 -0
  223. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/library.py +0 -0
  224. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/math.py +0 -0
  225. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/music.py +0 -0
  226. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/physical_education.py +0 -0
  227. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/science.py +0 -0
  228. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/social_studies.py +0 -0
  229. {clawed-2.3.5 → clawed-2.3.8}/clawed/skills/special_education.py +0 -0
  230. {clawed-2.3.5 → clawed-2.3.8}/clawed/standards.py +0 -0
  231. {clawed-2.3.5 → clawed-2.3.8}/clawed/state.py +0 -0
  232. {clawed-2.3.5 → clawed-2.3.8}/clawed/state_standards.py +0 -0
  233. {clawed-2.3.5 → clawed-2.3.8}/clawed/student_bot.py +0 -0
  234. {clawed-2.3.5 → clawed-2.3.8}/clawed/student_cli.py +0 -0
  235. {clawed-2.3.5 → clawed-2.3.8}/clawed/student_telegram_bot.py +0 -0
  236. {clawed-2.3.5 → clawed-2.3.8}/clawed/sub_packet.py +0 -0
  237. {clawed-2.3.5 → clawed-2.3.8}/clawed/task_queue.py +0 -0
  238. {clawed-2.3.5 → clawed-2.3.8}/clawed/templates_lib.py +0 -0
  239. {clawed-2.3.5 → clawed-2.3.8}/clawed/transports/__init__.py +0 -0
  240. {clawed-2.3.5 → clawed-2.3.8}/clawed/transports/cli.py +0 -0
  241. {clawed-2.3.5 → clawed-2.3.8}/clawed/transports/openclaw.py +0 -0
  242. {clawed-2.3.5 → clawed-2.3.8}/clawed/transports/student_telegram.py +0 -0
  243. {clawed-2.3.5 → clawed-2.3.8}/clawed/transports/web.py +0 -0
  244. {clawed-2.3.5 → clawed-2.3.8}/clawed/tui.py +0 -0
  245. {clawed-2.3.5 → clawed-2.3.8}/clawed/tui_chat.py +0 -0
  246. {clawed-2.3.5 → clawed-2.3.8}/clawed/voice.py +0 -0
  247. {clawed-2.3.5 → clawed-2.3.8}/clawed/voice_check.py +0 -0
  248. {clawed-2.3.5 → clawed-2.3.8}/clawed/workspace.py +0 -0
  249. {clawed-2.3.5 → clawed-2.3.8}/eduagent/__init__.py +0 -0
  250. {clawed-2.3.5 → 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.5
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
@@ -18,14 +18,13 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Topic :: Education
20
20
  Requires-Python: >=3.10
21
- Requires-Dist: anthropic<1.0,>=0.40.0
22
21
  Requires-Dist: apscheduler<4.0,>=3.10.0
23
22
  Requires-Dist: fastapi<1.0,>=0.110.0
24
23
  Requires-Dist: httpx<1.0,>=0.25.0
25
24
  Requires-Dist: jinja2>=3.1.0
26
25
  Requires-Dist: json-repair>=0.30.0
26
+ Requires-Dist: lxml>=4.9.0
27
27
  Requires-Dist: mcp>=1.0.0
28
- Requires-Dist: openai>=1.0.0
29
28
  Requires-Dist: pydantic<3.0,>=2.0.0
30
29
  Requires-Dist: pymupdf>=1.23.0
31
30
  Requires-Dist: python-docx>=1.0.0
@@ -41,6 +40,7 @@ Provides-Extra: all
41
40
  Requires-Dist: faster-whisper>=0.10.0; extra == 'all'
42
41
  Requires-Dist: keyring>=24.0.0; extra == 'all'
43
42
  Requires-Dist: onnxruntime>=1.16.0; extra == 'all'
43
+ Requires-Dist: qrcode[pil]>=7.0; extra == 'all'
44
44
  Requires-Dist: textual>=0.56.0; extra == 'all'
45
45
  Requires-Dist: uvicorn[standard]>=0.27.0; extra == 'all'
46
46
  Provides-Extra: dev
@@ -59,6 +59,8 @@ Provides-Extra: memory
59
59
  Requires-Dist: onnxruntime>=1.16.0; extra == 'memory'
60
60
  Provides-Extra: pdf
61
61
  Requires-Dist: weasyprint>=60.0; extra == 'pdf'
62
+ Provides-Extra: qr
63
+ Requires-Dist: qrcode[pil]>=7.0; extra == 'qr'
62
64
  Provides-Extra: tui
63
65
  Requires-Dist: textual>=0.56.0; extra == 'tui'
64
66
  Provides-Extra: voice
@@ -80,23 +82,35 @@ Built on the OpenClaw agent framework. Open source. MIT license.
80
82
 
81
83
  ---
82
84
 
83
- ## What's new in v2.3.5
85
+ ## What's new in v2.3.8
84
86
 
85
- **Master Content Track.** One LLM call generates a single `MasterContent` object. Three output documents are compiled mechanically from the same source of truth -- no content drift between what the teacher sees, what students hold, and what's on screen:
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.
86
88
 
87
- 1. **Teacher DOCX** Full answer keys, scripted teacher language, guided notes with answers filled in, station answer keys, differentiation notes. Observation-ready.
88
- 2. **Student DOCX** — Same structure, blanks instead of answers. Guided notes with fill-in lines, station directions without answer keys, exit ticket without expected responses. Print and hand out.
89
- 3. **Slideshow PPTX** — Widescreen academic slides: title, vocabulary cards, instruction sections with images, primary source analysis, station overview, stimulus-based exit ticket.
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
90
 
91
- **Zero silent failures.** All 11 generators use `safe_generate_json()` with automatic retry on validation errors. Post-generation validators catch empty outputs, topic drift, and delegation phrases. Quality review fails closed. CLI shows warnings, not raw tracebacks.
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
92
 
93
- **Stimulus-based assessment.** Every question -- Do Now, guided notes, stations, exit ticket -- must be anchored to a stimulus (source text, data, diagram, scenario). Bare recall questions are banned at the prompt level.
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
94
 
95
- **Identity protection.** Onboarding only triggers on explicit `/setup`. Profile fields are validated and truncated. SOUL.md writes are audit-logged and capped at 500 chars.
95
+ ---
96
+
97
+ ## What's new in v2.3.7
98
+
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.
100
+
101
+ **12 new file formats.** Your old `.doc`, `.ppt`, `.xls`, `.xlsx`, `.csv`, `.rtf`, `.html`, `.odt`, and `.odp` files are now parsed and indexed. Previously only 8 formats were supported -- teachers' archives spanning decades of file formats were 93% invisible to search. Now they're searchable.
102
+
103
+ **Search actually works.** Three fixes to the search pipeline: cross-transport teacher ID fallback (files ingested via CLI now appear in Telegram searches), asset search errors are logged instead of silently swallowed, and the agent is explicitly instructed to surface results to you. Topic tags are auto-extracted from filenames and content for better matching.
104
+
105
+ **Background file ingestion.** Send your files and keep chatting. The bot acknowledges immediately, processes everything in a background thread, sends progress updates ("Indexed 50/200 documents..."), and a summary when done. Max 3 concurrent ingestions, individual file failures don't abort the batch.
106
+
107
+ **DEEP-tier model for lesson generation.** MasterContent routes to the DEEP tier. With a capable model (Claude Sonnet 4.6, GPT-4o), lesson quality improves dramatically.
108
+
109
+ **Security hardened.** Path traversal protection on all file-reading tools. XSS escaping on the web dashboard. Thread-safe tool definitions. ZIP bomb protection. Debug info no longer leaked to users. Ingest paths restricted to home directory.
96
110
 
97
- **Your files are first-class.** Teacher materials are searched (AssetRegistry + CurriculumKB) before every generation -- not just in the bundle tool, but in standalone lesson and unit generation too.
111
+ **50 MB lighter.** Removed unused `anthropic` and `openai` SDK dependencies. API key resolution unified across all code paths (env var + keyring + secrets file).
98
112
 
99
- **Parallel image pipeline.** All image specs from the MasterContent are fetched in parallel with configurable timeout and local caching.
113
+ **Everything from v2.3.5 still applies:** Master Content Track, stimulus-based assessment, zero silent failures, parallel image pipeline, identity protection.
100
114
 
101
115
  ---
102
116
 
@@ -113,7 +127,7 @@ Everything runs on your own computer. Your files never leave your machine unless
113
127
  ## How it works
114
128
 
115
129
  ```
116
- Your files (PDFs, DOCX, PPTX, TXT)
130
+ Your files (PDF, DOCX, PPTX, DOC, PPT, XLS, XLSX, CSV, RTF, HTML, ODT, TXT, and more)
117
131
  |
118
132
  v
119
133
  Claw-ED learns your teaching style
@@ -13,23 +13,35 @@ Built on the OpenClaw agent framework. Open source. MIT license.
13
13
 
14
14
  ---
15
15
 
16
- ## What's new in v2.3.5
16
+ ## What's new in v2.3.8
17
17
 
18
- **Master Content Track.** One LLM call generates a single `MasterContent` object. Three output documents are compiled mechanically from the same source of truth -- no content drift between what the teacher sees, what students hold, and what's on screen:
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
19
 
20
- 1. **Teacher DOCX** Full answer keys, scripted teacher language, guided notes with answers filled in, station answer keys, differentiation notes. Observation-ready.
21
- 2. **Student DOCX** — Same structure, blanks instead of answers. Guided notes with fill-in lines, station directions without answer keys, exit ticket without expected responses. Print and hand out.
22
- 3. **Slideshow PPTX** — Widescreen academic slides: title, vocabulary cards, instruction sections with images, primary source analysis, station overview, stimulus-based exit ticket.
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.
23
21
 
24
- **Zero silent failures.** All 11 generators use `safe_generate_json()` with automatic retry on validation errors. Post-generation validators catch empty outputs, topic drift, and delegation phrases. Quality review fails closed. CLI shows warnings, not raw tracebacks.
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.
25
23
 
26
- **Stimulus-based assessment.** Every question -- Do Now, guided notes, stations, exit ticket -- must be anchored to a stimulus (source text, data, diagram, scenario). Bare recall questions are banned at the prompt level.
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.
27
25
 
28
- **Identity protection.** Onboarding only triggers on explicit `/setup`. Profile fields are validated and truncated. SOUL.md writes are audit-logged and capped at 500 chars.
26
+ ---
27
+
28
+ ## What's new in v2.3.7
29
+
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.
31
+
32
+ **12 new file formats.** Your old `.doc`, `.ppt`, `.xls`, `.xlsx`, `.csv`, `.rtf`, `.html`, `.odt`, and `.odp` files are now parsed and indexed. Previously only 8 formats were supported -- teachers' archives spanning decades of file formats were 93% invisible to search. Now they're searchable.
33
+
34
+ **Search actually works.** Three fixes to the search pipeline: cross-transport teacher ID fallback (files ingested via CLI now appear in Telegram searches), asset search errors are logged instead of silently swallowed, and the agent is explicitly instructed to surface results to you. Topic tags are auto-extracted from filenames and content for better matching.
35
+
36
+ **Background file ingestion.** Send your files and keep chatting. The bot acknowledges immediately, processes everything in a background thread, sends progress updates ("Indexed 50/200 documents..."), and a summary when done. Max 3 concurrent ingestions, individual file failures don't abort the batch.
37
+
38
+ **DEEP-tier model for lesson generation.** MasterContent routes to the DEEP tier. With a capable model (Claude Sonnet 4.6, GPT-4o), lesson quality improves dramatically.
39
+
40
+ **Security hardened.** Path traversal protection on all file-reading tools. XSS escaping on the web dashboard. Thread-safe tool definitions. ZIP bomb protection. Debug info no longer leaked to users. Ingest paths restricted to home directory.
29
41
 
30
- **Your files are first-class.** Teacher materials are searched (AssetRegistry + CurriculumKB) before every generation -- not just in the bundle tool, but in standalone lesson and unit generation too.
42
+ **50 MB lighter.** Removed unused `anthropic` and `openai` SDK dependencies. API key resolution unified across all code paths (env var + keyring + secrets file).
31
43
 
32
- **Parallel image pipeline.** All image specs from the MasterContent are fetched in parallel with configurable timeout and local caching.
44
+ **Everything from v2.3.5 still applies:** Master Content Track, stimulus-based assessment, zero silent failures, parallel image pipeline, identity protection.
33
45
 
34
46
  ---
35
47
 
@@ -46,7 +58,7 @@ Everything runs on your own computer. Your files never leave your machine unless
46
58
  ## How it works
47
59
 
48
60
  ```
49
- Your files (PDFs, DOCX, PPTX, TXT)
61
+ Your files (PDF, DOCX, PPTX, DOC, PPT, XLS, XLSX, CSV, RTF, HTML, ODT, TXT, and more)
50
62
  |
51
63
  v
52
64
  Claw-ED learns your teaching style
@@ -17,7 +17,7 @@ if hasattr(sys.stderr, "reconfigure"):
17
17
  except Exception:
18
18
  pass
19
19
 
20
- __version__ = "2.3.5"
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
 
@@ -94,7 +94,8 @@ class Gateway:
94
94
  self._model_switch = ModelSwitchHandler()
95
95
 
96
96
  async def handle(self, message: str, teacher_id: str,
97
- files: list[Path] | None = None) -> GatewayResponse:
97
+ files: list[Path] | None = None,
98
+ progress_callback=None) -> GatewayResponse:
98
99
  """Process any message from any transport."""
99
100
  self._stats.messages_today += 1
100
101
  self.active_sessions[teacher_id] = {
@@ -112,31 +113,29 @@ class Gateway:
112
113
  if not has_config():
113
114
  return await self._onboard.step(teacher_id, message)
114
115
 
115
- return await self._dispatch(message, teacher_id, files)
116
+ return await self._dispatch(message, teacher_id, files,
117
+ progress_callback=progress_callback)
116
118
 
117
119
  except Exception as e:
118
120
  logger.warning("Gateway error: %s", e)
121
+ logger.debug("Gateway error detail: %s: %s", type(e).__name__, e)
119
122
  self._stats.errors_today += 1
120
123
  await self.emit("error", {"teacher_id": teacher_id, "message": str(e)})
121
124
 
122
125
  err = str(e).lower()
123
- debug_hint = f"\n\n[Debug: {type(e).__name__}: {str(e)[:200]}]"
124
126
  if "401" in err or "unauthorized" in err or "api key" in err:
125
127
  return GatewayResponse(
126
128
  text="Your AI provider key doesn't seem to be working. "
127
129
  "Run `clawed setup --reset` to reconfigure it."
128
- + debug_hint
129
130
  )
130
131
  if "connection" in err or "connect" in err or "timeout" in err:
131
132
  return GatewayResponse(
132
133
  text="Can't connect to your AI provider right now. "
133
134
  "Check your internet connection and try again."
134
- + debug_hint
135
135
  )
136
136
  return GatewayResponse(
137
137
  text="Something went wrong. Try again, or run "
138
138
  "`clawed setup --reset` to reconfigure."
139
- + debug_hint
140
139
  )
141
140
 
142
141
  async def handle_callback(self, callback_data: str, teacher_id: str) -> GatewayResponse:
@@ -177,13 +176,16 @@ class Gateway:
177
176
  }
178
177
 
179
178
  async def _dispatch(self, message: str, teacher_id: str,
180
- files: list[Path] | None = None) -> GatewayResponse:
179
+ files: list[Path] | None = None,
180
+ progress_callback=None) -> GatewayResponse:
181
181
  """Route a message to the appropriate handler based on intent."""
182
182
  if files:
183
- return await self._ingest.handle(teacher_id, files)
183
+ return await self._ingest.handle(teacher_id, files,
184
+ progress_callback=progress_callback)
184
185
 
185
186
  if self._looks_like_path(message):
186
- return await self._ingest.handle(teacher_id, path=message.strip())
187
+ return await self._ingest.handle(teacher_id, path=message.strip(),
188
+ progress_callback=progress_callback)
187
189
 
188
190
  # NOTE: parse_intent() is keyword/regex-based (zero cost).
189
191
  # When upgraded to LLM-based detection, use:
@@ -1,12 +1,15 @@
1
1
  """Core data types for the agent system."""
2
2
  from __future__ import annotations
3
3
 
4
+ import logging
4
5
  from dataclasses import dataclass, field
5
6
  from pathlib import Path
6
- from typing import Any
7
+ from typing import Any, Callable, Optional
7
8
 
8
9
  from clawed.models import AppConfig
9
10
 
11
+ logger = logging.getLogger(__name__)
12
+
10
13
 
11
14
  @dataclass
12
15
  class AgentContext:
@@ -19,6 +22,15 @@ class AgentContext:
19
22
  session_history: list[dict[str, Any]]
20
23
  improvement_context: str
21
24
  agent_name: str = "Claw-ED"
25
+ progress_callback: Optional[Callable[[str], None]] = None
26
+
27
+ def notify_progress(self, message: str) -> None:
28
+ """Send a progress update to the user if a callback is registered."""
29
+ if self.progress_callback:
30
+ try:
31
+ self.progress_callback(message)
32
+ except Exception as e:
33
+ logger.debug("Progress notification failed: %s", e)
22
34
 
23
35
 
24
36
  @dataclass
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  import asyncio
11
11
  import json
12
12
  import logging
13
+ import threading
13
14
  import time
14
15
  from datetime import datetime
15
16
  from pathlib import Path
@@ -28,6 +29,9 @@ from clawed.models import AppConfig
28
29
  logger = logging.getLogger(__name__)
29
30
 
30
31
 
32
+ _tool_lock = threading.Lock()
33
+
34
+
31
35
  class _LLMClientAdapter:
32
36
  """Adapts the existing clawed.agent module's LLM calling to LLMInterface.
33
37
 
@@ -45,28 +49,24 @@ class _LLMClientAdapter:
45
49
  tools: list[dict[str, Any]] | None = None,
46
50
  system: str = "",
47
51
  ) -> dict[str, Any]:
48
- # WARNING: This monkey-patches a module-level variable, which is NOT
49
- # safe under concurrent requests. For v1.0 (hosted/multi-teacher),
50
- # refactor the legacy agent functions to accept tool definitions as
51
- # a parameter instead of reading from the module global.
52
- #
53
52
  # The legacy agent functions operate on the global TOOL_DEFINITIONS.
54
- # We temporarily monkey-patch them so the registry schemas are used
55
- # instead. Since these functions read TOOL_DEFINITIONS at call time,
56
- # we swap the module-level list.
53
+ # We temporarily swap them under a lock so concurrent requests don't
54
+ # clobber each other's tool definitions.
57
55
  import clawed.agent as _agent_mod
58
56
  from clawed.agent import _call_with_native_tools, _call_with_ollama_tools
59
57
  from clawed.models import LLMProvider
60
58
 
61
- original_defs = _agent_mod.TOOL_DEFINITIONS
62
- _agent_mod.TOOL_DEFINITIONS = tools or []
59
+ with _tool_lock:
60
+ original_defs = _agent_mod.TOOL_DEFINITIONS
61
+ _agent_mod.TOOL_DEFINITIONS = tools or []
63
62
  try:
64
63
  if self._config.provider in (LLMProvider.ANTHROPIC, LLMProvider.OPENAI):
65
64
  return await _call_with_native_tools(messages, system, self._config)
66
65
  else:
67
66
  return await _call_with_ollama_tools(messages, system, self._config)
68
67
  finally:
69
- _agent_mod.TOOL_DEFINITIONS = original_defs
68
+ with _tool_lock:
69
+ _agent_mod.TOOL_DEFINITIONS = original_defs
70
70
 
71
71
 
72
72
  class Gateway:
@@ -116,6 +116,7 @@ class Gateway:
116
116
  message: str,
117
117
  teacher_id: str,
118
118
  files: list[Path] | None = None,
119
+ progress_callback: Any = None,
119
120
  ) -> GatewayResponse:
120
121
  """Process any message from any transport."""
121
122
  self._stats.messages_today += 1
@@ -128,9 +129,11 @@ class Gateway:
128
129
  })
129
130
 
130
131
  try:
131
- # 1. Files → ingest (deterministic, no LLM)
132
+ # 1. Files → ingest (deterministic, no LLM, runs in background)
132
133
  if files:
133
- return await self._ingest.handle(teacher_id, files)
134
+ return await self._ingest.handle(
135
+ teacher_id, files, progress_callback=progress_callback
136
+ )
134
137
 
135
138
  # 2. Onboarding state machine (deterministic, no LLM)
136
139
  if self._onboard.is_onboarding(teacher_id):
@@ -147,38 +150,32 @@ class Gateway:
147
150
  )
148
151
 
149
152
  # 4. Natural-language → agent loop
150
- return await self._agent_loop(message, teacher_id)
153
+ return await self._agent_loop(message, teacher_id, progress_callback=progress_callback)
151
154
 
152
155
  except Exception as e:
153
- logger.debug("Gateway error: %s", e)
156
+ logger.error("Agent error for teacher %s: %s", teacher_id, e, exc_info=True)
154
157
  self._stats.errors_today += 1
155
158
  await self.emit("error", {"teacher_id": teacher_id, "message": str(e)})
156
159
 
157
- # Teacher-friendly error messages (include debug info for troubleshooting)
160
+ # Teacher-friendly error messages (no internal details exposed)
158
161
  err = str(e).lower()
159
- debug_hint = f"\n\n[Debug: {type(e).__name__}: {str(e)[:200]}]"
160
162
  if "401" in err or "unauthorized" in err or "api key" in err:
161
163
  return GatewayResponse(
162
164
  text="Your AI provider key doesn't seem to be working. "
163
165
  "Run `clawed setup --reset` to reconfigure it."
164
- + debug_hint
165
166
  )
166
167
  if "connection" in err or "connect" in err or "timeout" in err:
167
168
  return GatewayResponse(
168
169
  text="Can't connect to your AI provider right now. "
169
170
  "Check your internet connection and try again."
170
- + debug_hint
171
171
  )
172
172
  if "rate limit" in err or "429" in err:
173
173
  return GatewayResponse(
174
174
  text="Your AI provider is temporarily overloaded. "
175
175
  "Wait a minute and try again."
176
- + debug_hint
177
176
  )
178
177
  return GatewayResponse(
179
- text="Something went wrong. Try again, or run "
180
- "`clawed setup --reset` to reconfigure."
181
- + debug_hint
178
+ text="Something went wrong. Please try again."
182
179
  )
183
180
 
184
181
  async def handle_callback(self, callback_data: str, teacher_id: str) -> GatewayResponse:
@@ -299,7 +296,7 @@ class Gateway:
299
296
  # Agent loop — the core reasoning path
300
297
  # ------------------------------------------------------------------
301
298
 
302
- async def _agent_loop(self, message: str, teacher_id: str) -> GatewayResponse:
299
+ async def _agent_loop(self, message: str, teacher_id: str, progress_callback: Any = None) -> GatewayResponse:
303
300
  """Load context, build prompt, and run the agent tool-use loop."""
304
301
  # 1. Load teacher context from canonical sources
305
302
  teacher_profile = self._load_teacher_profile()
@@ -342,7 +339,9 @@ class Gateway:
342
339
  # 2a. Load SOUL.md if available
343
340
  soul_context = ""
344
341
  try:
345
- 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"
346
345
  if soul_path.exists():
347
346
  soul_context = soul_path.read_text(encoding="utf-8")[:2000]
348
347
  except Exception:
@@ -393,6 +392,7 @@ class Gateway:
393
392
  session_history=session_history,
394
393
  improvement_context=memory_ctx["improvement_context"],
395
394
  agent_name=agent_name,
395
+ progress_callback=progress_callback,
396
396
  )
397
397
 
398
398
  # 4. Get or create LLM adapter
@@ -138,10 +138,13 @@ class CurriculumKB:
138
138
 
139
139
  with sqlite3.connect(self._db_path) as conn:
140
140
  conn.row_factory = sqlite3.Row
141
+ # Fetch up to 5000 chunks for scoring. This trades higher memory
142
+ # for better recall — teachers with large file collections may have
143
+ # thousands of chunks, and a 2000 cap was silently dropping results.
141
144
  rows = conn.execute(
142
145
  "SELECT doc_title, source_path, chunk_text, embedding, metadata, created_at "
143
146
  "FROM chunks WHERE teacher_id = ? "
144
- "LIMIT 2000",
147
+ "LIMIT 5000",
145
148
  (teacher_id,),
146
149
  ).fetchall()
147
150
 
@@ -166,6 +169,47 @@ class CurriculumKB:
166
169
  scored.sort(key=lambda x: x["similarity"], reverse=True)
167
170
  return scored[:top_k]
168
171
 
172
+ def search_all_teachers(
173
+ self,
174
+ query: str,
175
+ top_k: int = 10,
176
+ ) -> list[dict[str, Any]]:
177
+ """Fallback search across ALL teachers when teacher_id doesn't match.
178
+
179
+ This handles cross-transport mismatches (e.g. files ingested via
180
+ Telegram numeric ID, searched via CLI 'local-teacher').
181
+ """
182
+ query_embedding = self._embedder.embed(query)
183
+
184
+ with sqlite3.connect(self._db_path) as conn:
185
+ conn.row_factory = sqlite3.Row
186
+ # Search all chunks regardless of teacher_id — capped for safety
187
+ rows = conn.execute(
188
+ "SELECT doc_title, source_path, chunk_text, embedding, metadata, created_at "
189
+ "FROM chunks LIMIT 5000",
190
+ ).fetchall()
191
+
192
+ if not rows:
193
+ return []
194
+
195
+ scored = []
196
+ for row in rows:
197
+ stored_embedding = json.loads(row["embedding"])
198
+ sim = self._embedder.cosine_similarity(query_embedding, stored_embedding)
199
+ scored.append({
200
+ "doc_title": row["doc_title"],
201
+ "source_path": row["source_path"],
202
+ "chunk_text": row["chunk_text"],
203
+ "metadata": json.loads(row["metadata"]),
204
+ "created_at": row["created_at"],
205
+ "similarity": sim,
206
+ })
207
+
208
+ scored = [s for s in scored if s["similarity"] > 0.05]
209
+ logger.debug("KB fallback search '%s': %d chunks scored, %d above threshold", query, len(rows), len(scored))
210
+ scored.sort(key=lambda x: x["similarity"], reverse=True)
211
+ return scored[:top_k]
212
+
169
213
  def stats(self, teacher_id: str) -> dict[str, Any]:
170
214
  """Return stats about the teacher's curriculum knowledge base."""
171
215
  with sqlite3.connect(self._db_path) as conn:
@@ -60,7 +60,13 @@ class OllamaEmbedder:
60
60
 
61
61
 
62
62
  class TFIDFEmbedder:
63
- """TF-IDF with bigrams — no dependencies, always available."""
63
+ """TF-IDF with bigrams — no dependencies, always available.
64
+
65
+ Vocabulary is capped at MAX_VOCAB tokens to bound vector dimensionality
66
+ and prevent unbounded memory growth during large ingestion runs.
67
+ """
68
+
69
+ MAX_VOCAB = 10_000
64
70
 
65
71
  def __init__(self) -> None:
66
72
  self._vocab: dict[str, int] = {}
@@ -78,12 +84,15 @@ class TFIDFEmbedder:
78
84
  def embed(self, text: str) -> list[float]:
79
85
  tokens = self._tokenize(text)
80
86
  for t in tokens:
81
- if t not in self._vocab:
87
+ if t not in self._vocab and self._next_idx < self.MAX_VOCAB:
82
88
  self._vocab[t] = self._next_idx
83
89
  self._next_idx += 1
84
- vec = [0.0] * len(self._vocab)
90
+ dim = min(len(self._vocab), self.MAX_VOCAB)
91
+ vec = [0.0] * dim
85
92
  for t in tokens:
86
- vec[self._vocab[t]] += 1.0
93
+ idx = self._vocab.get(t)
94
+ if idx is not None and idx < dim:
95
+ vec[idx] += 1.0
87
96
  norm = math.sqrt(sum(x * x for x in vec)) or 1.0
88
97
  return [x / norm for x in vec]
89
98
 
@@ -96,6 +96,9 @@ def build_system_prompt(
96
96
  "The teacher has uploaded materials — if you skip this step, you will "
97
97
  "generate generic content instead of building on their prior work. "
98
98
  "Tell the teacher what you found before generating.\n"
99
+ " IMPORTANT: If search_my_materials returns results, you MUST list them "
100
+ "for the teacher. NEVER say 'I didn't find anything' if the tool returned "
101
+ "materials. Always surface what was found, even if it's not an exact match.\n"
99
102
  "3. Generate complete packages (lesson plan + student handout + slideshow) "
100
103
  "using generate_lesson_bundle\n"
101
104
  "4. Never ask 'want me to create materials?' -- just create them\n"
@@ -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
 
@@ -83,14 +84,24 @@ class GenerateLessonBundleTool:
83
84
  activity_type = params.get("activity_type", "general")
84
85
  include_images = params.get("include_images", True)
85
86
 
87
+ # ── Notify user we're starting ────────────────────────────────
88
+ context.notify_progress(
89
+ f"Working on your lesson materials for \"{topic}\" now — "
90
+ f"this usually takes 2-4 minutes. I'll send everything when it's ready!"
91
+ )
92
+
93
+ from clawed.generation_report import GenerationReport
94
+ report = GenerationReport()
95
+
86
96
  # ── Load config & persona from context ───────────────────────
87
97
  config = context.config
88
98
  persona = TeacherPersona()
89
99
  if context.persona:
90
100
  try:
91
101
  persona = TeacherPersona(**context.persona)
92
- except Exception:
93
- 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}")
94
105
 
95
106
  # ── Load state standards if teacher profile has a state ───────
96
107
  state = ""
@@ -121,7 +132,8 @@ class GenerateLessonBundleTool:
121
132
  len(assets), len(yt_links), topic,
122
133
  )
123
134
  except Exception as e:
124
- 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}")
125
137
 
126
138
  # KB chunk-level search (text excerpts)
127
139
  try:
@@ -156,7 +168,8 @@ class GenerateLessonBundleTool:
156
168
  )
157
169
  logger.info("KB search found %d relevant chunks for '%s'", len(kb_parts), topic)
158
170
  except Exception as e:
159
- 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}")
160
173
 
161
174
  # ── Build a UnitPlan with standards ──────────────────────────
162
175
  description = f"Introduction to {topic}"
@@ -199,14 +212,12 @@ class GenerateLessonBundleTool:
199
212
  teacher_materials=kb_prompt_section,
200
213
  )
201
214
  except Exception as e:
202
- 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__}")
203
217
 
204
218
  # ── Validate ──────────────────────────────────────────────────
205
- from clawed.generation_report import GenerationReport
206
219
  from clawed.validation import check_self_contained, validate_alignment, validate_master_content
207
220
 
208
- report = GenerationReport()
209
-
210
221
  mc_errors = validate_master_content(master, topic)
211
222
  for err in mc_errors:
212
223
  report.warnings.append(err)
@@ -277,6 +288,45 @@ class GenerateLessonBundleTool:
277
288
  logger.error("Slides compile failed: %s", e)
278
289
  errors.append(f"Slideshow PPTX failed: {e}")
279
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
+
280
330
  # ── Build response ─────────────────────────────────────────────
281
331
  lines = []
282
332