clawed 2.1.1__tar.gz → 2.3.1__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 (231) hide show
  1. {clawed-2.1.1 → clawed-2.3.1}/PKG-INFO +8 -6
  2. {clawed-2.1.1 → clawed-2.3.1}/README.md +7 -5
  3. {clawed-2.1.1 → clawed-2.3.1}/clawed/__init__.py +1 -1
  4. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/core.py +20 -1
  5. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/memory/episodes.py +50 -0
  6. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/memory/loader.py +23 -0
  7. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/generate_lesson_bundle.py +59 -43
  8. {clawed-2.1.1 → clawed-2.3.1}/clawed/export_docx.py +209 -0
  9. clawed-2.3.1/clawed/export_handout.py +368 -0
  10. {clawed-2.1.1 → clawed-2.3.1}/clawed/export_pptx.py +100 -44
  11. {clawed-2.1.1 → clawed-2.3.1}/clawed/llm.py +78 -0
  12. {clawed-2.1.1 → clawed-2.3.1}/clawed/memory_engine.py +180 -0
  13. {clawed-2.1.1 → clawed-2.3.1}/clawed/models.py +117 -0
  14. {clawed-2.1.1 → clawed-2.3.1}/clawed/onboarding.py +22 -5
  15. clawed-2.3.1/clawed/prompts/admin_lesson_plan.txt +69 -0
  16. clawed-2.3.1/clawed/prompts/student_packet.txt +87 -0
  17. {clawed-2.1.1 → clawed-2.3.1}/clawed/workspace.py +1 -0
  18. {clawed-2.1.1 → clawed-2.3.1}/pyproject.toml +1 -1
  19. clawed-2.1.1/clawed/export_handout.py +0 -229
  20. {clawed-2.1.1 → clawed-2.3.1}/.gitignore +0 -0
  21. {clawed-2.1.1 → clawed-2.3.1}/LICENSE +0 -0
  22. {clawed-2.1.1 → clawed-2.3.1}/clawed/__main__.py +0 -0
  23. {clawed-2.1.1 → clawed-2.3.1}/clawed/_legacy_gateway.py +0 -0
  24. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent.py +0 -0
  25. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/__init__.py +0 -0
  26. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/approvals.py +0 -0
  27. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/autonomy.py +0 -0
  28. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/context.py +0 -0
  29. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/custom_tools.py +0 -0
  30. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/drive/__init__.py +0 -0
  31. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/drive/auth.py +0 -0
  32. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/drive/client.py +0 -0
  33. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/fake_llm.py +0 -0
  34. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/loop.py +0 -0
  35. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/memory/__init__.py +0 -0
  36. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/memory/curriculum.py +0 -0
  37. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/memory/curriculum_kb.py +0 -0
  38. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/memory/embeddings.py +0 -0
  39. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/memory/identity.py +0 -0
  40. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/memory/preferences.py +0 -0
  41. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/planner.py +0 -0
  42. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/prompt.py +0 -0
  43. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/scheduler.py +0 -0
  44. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/__init__.py +0 -0
  45. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/base.py +0 -0
  46. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/configure_profile.py +0 -0
  47. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/curriculum_map.py +0 -0
  48. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/drive_create_doc.py +0 -0
  49. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/drive_create_slides.py +0 -0
  50. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/drive_list.py +0 -0
  51. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/drive_organize.py +0 -0
  52. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/drive_read.py +0 -0
  53. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/drive_upload.py +0 -0
  54. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/export_document.py +0 -0
  55. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/gap_analysis.py +0 -0
  56. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/generate_assessment.py +0 -0
  57. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/generate_lesson.py +0 -0
  58. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/generate_materials.py +0 -0
  59. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/generate_unit.py +0 -0
  60. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/ingest_materials.py +0 -0
  61. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/parent_comm.py +0 -0
  62. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/read_heartbeat.py +0 -0
  63. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/read_workspace.py +0 -0
  64. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/request_approval.py +0 -0
  65. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/schedule_task.py +0 -0
  66. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/search_lessons.py +0 -0
  67. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/search_my_materials.py +0 -0
  68. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/search_standards.py +0 -0
  69. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/student_insights.py +0 -0
  70. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/sub_packet.py +0 -0
  71. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/switch_model.py +0 -0
  72. {clawed-2.1.1 → clawed-2.3.1}/clawed/agent_core/tools/update_soul.py +0 -0
  73. {clawed-2.1.1 → clawed-2.3.1}/clawed/analytics.py +0 -0
  74. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/__init__.py +0 -0
  75. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/deps.py +0 -0
  76. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/__init__.py +0 -0
  77. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/chat.py +0 -0
  78. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/export.py +0 -0
  79. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/feedback.py +0 -0
  80. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/gateway_chat.py +0 -0
  81. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/generate.py +0 -0
  82. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/ingest.py +0 -0
  83. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/lessons.py +0 -0
  84. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/school.py +0 -0
  85. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/settings.py +0 -0
  86. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/routes/tools.py +0 -0
  87. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/server.py +0 -0
  88. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/static/app.js +0 -0
  89. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/static/style.css +0 -0
  90. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/static/widget.js +0 -0
  91. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/analytics.html +0 -0
  92. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/base.html +0 -0
  93. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/dashboard.html +0 -0
  94. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/generate.html +0 -0
  95. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/index.html +0 -0
  96. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/lesson.html +0 -0
  97. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/profile.html +0 -0
  98. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/settings.html +0 -0
  99. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/stats.html +0 -0
  100. {clawed-2.1.1 → clawed-2.3.1}/clawed/api/templates/students.html +0 -0
  101. {clawed-2.1.1 → clawed-2.3.1}/clawed/assessment.py +0 -0
  102. {clawed-2.1.1 → clawed-2.3.1}/clawed/asset_registry.py +0 -0
  103. {clawed-2.1.1 → clawed-2.3.1}/clawed/auth/__init__.py +0 -0
  104. {clawed-2.1.1 → clawed-2.3.1}/clawed/auth/google_auth.py +0 -0
  105. {clawed-2.1.1 → clawed-2.3.1}/clawed/bot_state.py +0 -0
  106. {clawed-2.1.1 → clawed-2.3.1}/clawed/chat.py +0 -0
  107. {clawed-2.1.1 → clawed-2.3.1}/clawed/cli.py +0 -0
  108. {clawed-2.1.1 → clawed-2.3.1}/clawed/cli_chat.py +0 -0
  109. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/__init__.py +0 -0
  110. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/_helpers.py +0 -0
  111. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/bot.py +0 -0
  112. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/config.py +0 -0
  113. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/config_llm.py +0 -0
  114. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/config_profile.py +0 -0
  115. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/export.py +0 -0
  116. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/generate.py +0 -0
  117. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/generate_assessment.py +0 -0
  118. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/generate_unit.py +0 -0
  119. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/queue.py +0 -0
  120. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/schedule_cmd.py +0 -0
  121. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/sub.py +0 -0
  122. {clawed-2.1.1 → clawed-2.3.1}/clawed/commands/workspace_cmd.py +0 -0
  123. {clawed-2.1.1 → clawed-2.3.1}/clawed/config.py +0 -0
  124. {clawed-2.1.1 → clawed-2.3.1}/clawed/corpus.py +0 -0
  125. {clawed-2.1.1 → clawed-2.3.1}/clawed/curriculum_map.py +0 -0
  126. {clawed-2.1.1 → clawed-2.3.1}/clawed/database.py +0 -0
  127. {clawed-2.1.1 → clawed-2.3.1}/clawed/demo/__init__.py +0 -0
  128. {clawed-2.1.1 → clawed-2.3.1}/clawed/demo/demo_assessment.json +0 -0
  129. {clawed-2.1.1 → clawed-2.3.1}/clawed/demo/demo_lesson_science_g6.json +0 -0
  130. {clawed-2.1.1 → clawed-2.3.1}/clawed/demo/demo_lesson_social_studies_g8.json +0 -0
  131. {clawed-2.1.1 → clawed-2.3.1}/clawed/demo/demo_unit_plan.json +0 -0
  132. {clawed-2.1.1 → clawed-2.3.1}/clawed/differentiation.py +0 -0
  133. {clawed-2.1.1 → clawed-2.3.1}/clawed/doc_export.py +0 -0
  134. {clawed-2.1.1 → clawed-2.3.1}/clawed/drive.py +0 -0
  135. {clawed-2.1.1 → clawed-2.3.1}/clawed/evaluation.py +0 -0
  136. {clawed-2.1.1 → clawed-2.3.1}/clawed/export_markdown.py +0 -0
  137. {clawed-2.1.1 → clawed-2.3.1}/clawed/export_pdf.py +0 -0
  138. {clawed-2.1.1 → clawed-2.3.1}/clawed/export_templates.py +0 -0
  139. {clawed-2.1.1 → clawed-2.3.1}/clawed/export_theme.py +0 -0
  140. {clawed-2.1.1 → clawed-2.3.1}/clawed/exporter.py +0 -0
  141. {clawed-2.1.1 → clawed-2.3.1}/clawed/feedback.py +0 -0
  142. {clawed-2.1.1 → clawed-2.3.1}/clawed/formats/__init__.py +0 -0
  143. {clawed-2.1.1 → clawed-2.3.1}/clawed/formats/flipchart.py +0 -0
  144. {clawed-2.1.1 → clawed-2.3.1}/clawed/formats/notebook.py +0 -0
  145. {clawed-2.1.1 → clawed-2.3.1}/clawed/formats/xbk.py +0 -0
  146. {clawed-2.1.1 → clawed-2.3.1}/clawed/gateway.py +0 -0
  147. {clawed-2.1.1 → clawed-2.3.1}/clawed/gateway_response.py +0 -0
  148. {clawed-2.1.1 → clawed-2.3.1}/clawed/generation.py +0 -0
  149. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/__init__.py +0 -0
  150. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/export.py +0 -0
  151. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/feedback.py +0 -0
  152. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/gaps.py +0 -0
  153. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/generate.py +0 -0
  154. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/ingest.py +0 -0
  155. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/misc.py +0 -0
  156. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/onboard.py +0 -0
  157. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/schedule.py +0 -0
  158. {clawed-2.1.1 → clawed-2.3.1}/clawed/handlers/standards.py +0 -0
  159. {clawed-2.1.1 → clawed-2.3.1}/clawed/improver.py +0 -0
  160. {clawed-2.1.1 → clawed-2.3.1}/clawed/ingestor.py +0 -0
  161. {clawed-2.1.1 → clawed-2.3.1}/clawed/io.py +0 -0
  162. {clawed-2.1.1 → clawed-2.3.1}/clawed/lesson.py +0 -0
  163. {clawed-2.1.1 → clawed-2.3.1}/clawed/materials.py +0 -0
  164. {clawed-2.1.1 → clawed-2.3.1}/clawed/mcp_server.py +0 -0
  165. {clawed-2.1.1 → clawed-2.3.1}/clawed/model_router.py +0 -0
  166. {clawed-2.1.1 → clawed-2.3.1}/clawed/openclaw_plugin.py +0 -0
  167. {clawed-2.1.1 → clawed-2.3.1}/clawed/parent_comm.py +0 -0
  168. {clawed-2.1.1 → clawed-2.3.1}/clawed/persona.py +0 -0
  169. {clawed-2.1.1 → clawed-2.3.1}/clawed/planner.py +0 -0
  170. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/504_accommodations.txt +0 -0
  171. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/assessment.txt +0 -0
  172. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/curriculum_gaps.txt +0 -0
  173. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/dbq_assessment.txt +0 -0
  174. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/differentiation.txt +0 -0
  175. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/formative_assessment.txt +0 -0
  176. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/iep_modification.txt +0 -0
  177. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/lesson_plan.txt +0 -0
  178. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/pacing_guide.txt +0 -0
  179. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/parent_note.txt +0 -0
  180. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/persona_extract.txt +0 -0
  181. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/quiz.txt +0 -0
  182. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/rubric.txt +0 -0
  183. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/sub_packet.txt +0 -0
  184. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/summative_assessment.txt +0 -0
  185. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/tiered_assignments.txt +0 -0
  186. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/unit_plan.txt +0 -0
  187. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/worksheet.txt +0 -0
  188. {clawed-2.1.1 → clawed-2.3.1}/clawed/prompts/year_map.txt +0 -0
  189. {clawed-2.1.1 → clawed-2.3.1}/clawed/quality.py +0 -0
  190. {clawed-2.1.1 → clawed-2.3.1}/clawed/reading_report.py +0 -0
  191. {clawed-2.1.1 → clawed-2.3.1}/clawed/router.py +0 -0
  192. {clawed-2.1.1 → clawed-2.3.1}/clawed/sanitize.py +0 -0
  193. {clawed-2.1.1 → clawed-2.3.1}/clawed/scheduler.py +0 -0
  194. {clawed-2.1.1 → clawed-2.3.1}/clawed/school.py +0 -0
  195. {clawed-2.1.1 → clawed-2.3.1}/clawed/search.py +0 -0
  196. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/__init__.py +0 -0
  197. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/art.py +0 -0
  198. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/base.py +0 -0
  199. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/computer_science.py +0 -0
  200. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/ela.py +0 -0
  201. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/foreign_language.py +0 -0
  202. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/history.py +0 -0
  203. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/library.py +0 -0
  204. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/math.py +0 -0
  205. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/music.py +0 -0
  206. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/physical_education.py +0 -0
  207. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/science.py +0 -0
  208. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/social_studies.py +0 -0
  209. {clawed-2.1.1 → clawed-2.3.1}/clawed/skills/special_education.py +0 -0
  210. {clawed-2.1.1 → clawed-2.3.1}/clawed/slide_images.py +0 -0
  211. {clawed-2.1.1 → clawed-2.3.1}/clawed/standards.py +0 -0
  212. {clawed-2.1.1 → clawed-2.3.1}/clawed/state.py +0 -0
  213. {clawed-2.1.1 → clawed-2.3.1}/clawed/state_standards.py +0 -0
  214. {clawed-2.1.1 → clawed-2.3.1}/clawed/student_bot.py +0 -0
  215. {clawed-2.1.1 → clawed-2.3.1}/clawed/student_cli.py +0 -0
  216. {clawed-2.1.1 → clawed-2.3.1}/clawed/student_telegram_bot.py +0 -0
  217. {clawed-2.1.1 → clawed-2.3.1}/clawed/sub_packet.py +0 -0
  218. {clawed-2.1.1 → clawed-2.3.1}/clawed/task_queue.py +0 -0
  219. {clawed-2.1.1 → clawed-2.3.1}/clawed/templates_lib.py +0 -0
  220. {clawed-2.1.1 → clawed-2.3.1}/clawed/tools.py +0 -0
  221. {clawed-2.1.1 → clawed-2.3.1}/clawed/transports/__init__.py +0 -0
  222. {clawed-2.1.1 → clawed-2.3.1}/clawed/transports/cli.py +0 -0
  223. {clawed-2.1.1 → clawed-2.3.1}/clawed/transports/openclaw.py +0 -0
  224. {clawed-2.1.1 → clawed-2.3.1}/clawed/transports/student_telegram.py +0 -0
  225. {clawed-2.1.1 → clawed-2.3.1}/clawed/transports/telegram.py +0 -0
  226. {clawed-2.1.1 → clawed-2.3.1}/clawed/transports/web.py +0 -0
  227. {clawed-2.1.1 → clawed-2.3.1}/clawed/tui.py +0 -0
  228. {clawed-2.1.1 → clawed-2.3.1}/clawed/tui_chat.py +0 -0
  229. {clawed-2.1.1 → clawed-2.3.1}/clawed/voice.py +0 -0
  230. {clawed-2.1.1 → clawed-2.3.1}/eduagent/__init__.py +0 -0
  231. {clawed-2.1.1 → clawed-2.3.1}/eduagent/_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clawed
3
- Version: 2.1.1
3
+ Version: 2.3.1
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
@@ -79,15 +79,17 @@ Built on the OpenClaw agent framework. Open source. MIT license.
79
79
 
80
80
  ---
81
81
 
82
- ## What's new in v2.0
82
+ ## What's new in v2.3
83
83
 
84
- **Clean output.** Zero XML tags, zero markdown artifacts, zero `<teacher prompt>` gibberish in your printed documents. Every text field in every export (DOCX, PPTX, handout) passes through a rewritten sanitization pipeline that strips LLM formatting artifacts before they reach the page.
84
+ **Three documents, not one.** Every lesson now generates three professional files in parallel:
85
85
 
86
- **Your voice, for real.** The lesson generation LLM now receives your full persona, your SOUL.md, and up to 2000 characters of your voice sample in the system prompt. Lessons actually sound like you wrote them -- not a generic "expert lesson plan writer."
86
+ 1. **Student Packet** (4-6 page DOCX workbook) Fill-in-the-blank guided notes, station sections with full primary source text and analysis questions, graphic organizer tables, exit ticket with sentence starters. This is what students hold in their hands.
87
+ 2. **Admin Lesson Plan** (observation-ready DOCX) — Multi-column table with per-section teacher actions (scripted language), student actions, observer look-fors, and differentiation. Anticipated student responses and misconceptions with teacher corrections. Teacher content knowledge appendix.
88
+ 3. **Slideshow** (PPTX) — Subject-themed slides with academic images from your own files first, then Library of Congress and Wikimedia. Vocabulary, source quotes, and section dividers on dedicated slides.
87
89
 
88
- **Your files matter.** When you say "make me a lesson on absolutism," the agent searches your knowledge base, finds your existing materials, and injects them as structured context into the generation prompt. The LLM sees your prior work and builds on it instead of starting from scratch.
90
+ **Your files are first-class.** Ingestion extracts images from your PPTX/DOCX files, catalogues YouTube links, and classifies every file by type. The agent tells you what you already have before generating.
89
91
 
90
- **Images on by default.** Slideshows now include academic images (Library of Congress, Wikimedia) without needing `--images`. Logging added to diagnose fetch failures.
92
+ **Pedagogical fingerprint.** "Teacher voice" means how you teach, not just how you sound. The persona captures source types, activity patterns, scaffolding moves, Do Now style, exit ticket format, and signature moves.
91
93
 
92
94
  ---
93
95
 
@@ -13,15 +13,17 @@ Built on the OpenClaw agent framework. Open source. MIT license.
13
13
 
14
14
  ---
15
15
 
16
- ## What's new in v2.0
16
+ ## What's new in v2.3
17
17
 
18
- **Clean output.** Zero XML tags, zero markdown artifacts, zero `<teacher prompt>` gibberish in your printed documents. Every text field in every export (DOCX, PPTX, handout) passes through a rewritten sanitization pipeline that strips LLM formatting artifacts before they reach the page.
18
+ **Three documents, not one.** Every lesson now generates three professional files in parallel:
19
19
 
20
- **Your voice, for real.** The lesson generation LLM now receives your full persona, your SOUL.md, and up to 2000 characters of your voice sample in the system prompt. Lessons actually sound like you wrote them -- not a generic "expert lesson plan writer."
20
+ 1. **Student Packet** (4-6 page DOCX workbook) Fill-in-the-blank guided notes, station sections with full primary source text and analysis questions, graphic organizer tables, exit ticket with sentence starters. This is what students hold in their hands.
21
+ 2. **Admin Lesson Plan** (observation-ready DOCX) — Multi-column table with per-section teacher actions (scripted language), student actions, observer look-fors, and differentiation. Anticipated student responses and misconceptions with teacher corrections. Teacher content knowledge appendix.
22
+ 3. **Slideshow** (PPTX) — Subject-themed slides with academic images from your own files first, then Library of Congress and Wikimedia. Vocabulary, source quotes, and section dividers on dedicated slides.
21
23
 
22
- **Your files matter.** When you say "make me a lesson on absolutism," the agent searches your knowledge base, finds your existing materials, and injects them as structured context into the generation prompt. The LLM sees your prior work and builds on it instead of starting from scratch.
24
+ **Your files are first-class.** Ingestion extracts images from your PPTX/DOCX files, catalogues YouTube links, and classifies every file by type. The agent tells you what you already have before generating.
23
25
 
24
- **Images on by default.** Slideshows now include academic images (Library of Congress, Wikimedia) without needing `--images`. Logging added to diagnose fetch failures.
26
+ **Pedagogical fingerprint.** "Teacher voice" means how you teach, not just how you sound. The persona captures source types, activity patterns, scaffolding moves, Do Now style, exit ticket format, and signature moves.
25
27
 
26
28
  ---
27
29
 
@@ -17,7 +17,7 @@ if hasattr(sys.stderr, "reconfigure"):
17
17
  except Exception:
18
18
  pass
19
19
 
20
- __version__ = "2.1.1"
20
+ __version__ = "2.3.1"
21
21
  __author__ = "Jon Maccarello & Claw-ED contributors"
22
22
  __description__ = "Personal AI teaching agent. Learns your voice, works while you sleep."
23
23
 
@@ -361,7 +361,18 @@ class Gateway:
361
361
  soul_context=soul_context,
362
362
  )
363
363
 
364
- # 2c. Enhance prompt for multi-step planning requests
364
+ # 2c. Cross-session context threading greet with continuity
365
+ last_session = memory_ctx.get("last_session_summary", "")
366
+ if last_session and not session_history:
367
+ system += (
368
+ "\n\n=== Last Session Context ===\n"
369
+ f"The teacher's last interaction was about: {last_session}\n"
370
+ "If this is a new conversation, greet them with continuity — e.g. "
371
+ '"Last time we worked on [topic]. Want to continue or start something new?"\n'
372
+ "=== End Last Session Context ===\n"
373
+ )
374
+
375
+ # 2d. Enhance prompt for multi-step planning requests
365
376
  from clawed.agent_core.planner import build_planning_prompt, is_planning_request
366
377
 
367
378
  if is_planning_request(message):
@@ -415,6 +426,14 @@ class Gateway:
415
426
  except Exception:
416
427
  pass
417
428
 
429
+ # 8. Maybe compress old episodes (runs every COMPRESSION_THRESHOLD episodes)
430
+ try:
431
+ from clawed.memory_engine import maybe_compress_episodes
432
+
433
+ maybe_compress_episodes(teacher_id)
434
+ except Exception:
435
+ pass
436
+
418
437
  return result
419
438
 
420
439
  # ------------------------------------------------------------------
@@ -96,3 +96,53 @@ class EpisodicMemory:
96
96
 
97
97
  scored.sort(key=lambda x: x["similarity"], reverse=True)
98
98
  return scored[:top_k]
99
+
100
+ def get_latest_episode(self, teacher_id: str) -> dict[str, Any] | None:
101
+ """Return the most recent episode for a teacher, or None."""
102
+ with sqlite3.connect(self._db_path) as conn:
103
+ conn.row_factory = sqlite3.Row
104
+ row = conn.execute(
105
+ "SELECT text, metadata, created_at FROM episodes "
106
+ "WHERE teacher_id = ? ORDER BY created_at DESC LIMIT 1",
107
+ (teacher_id,),
108
+ ).fetchone()
109
+ if not row:
110
+ return None
111
+ return {
112
+ "text": row["text"],
113
+ "metadata": json.loads(row["metadata"]),
114
+ "created_at": row["created_at"],
115
+ }
116
+
117
+ def count_episodes(self, teacher_id: str) -> int:
118
+ """Return the total number of episodes stored for a teacher."""
119
+ with sqlite3.connect(self._db_path) as conn:
120
+ row = conn.execute(
121
+ "SELECT COUNT(*) FROM episodes WHERE teacher_id = ?",
122
+ (teacher_id,),
123
+ ).fetchone()
124
+ return row[0] if row else 0
125
+
126
+ def get_all_episodes(
127
+ self,
128
+ teacher_id: str,
129
+ limit: int = 500,
130
+ offset: int = 0,
131
+ ) -> list[dict[str, Any]]:
132
+ """Return episodes in chronological order (oldest first)."""
133
+ with sqlite3.connect(self._db_path) as conn:
134
+ conn.row_factory = sqlite3.Row
135
+ rows = conn.execute(
136
+ "SELECT text, metadata, created_at FROM episodes "
137
+ "WHERE teacher_id = ? ORDER BY created_at ASC "
138
+ "LIMIT ? OFFSET ?",
139
+ (teacher_id, limit, offset),
140
+ ).fetchall()
141
+ return [
142
+ {
143
+ "text": row["text"],
144
+ "metadata": json.loads(row["metadata"]),
145
+ "created_at": row["created_at"],
146
+ }
147
+ for row in rows
148
+ ]
@@ -96,6 +96,28 @@ def load_memory_context(teacher_id: str, current_message: str) -> dict[str, Any]
96
96
  except Exception as e:
97
97
  logger.debug("Curriculum KB search failed: %s", e)
98
98
 
99
+ # Layer 6: Last session summary (cross-session context threading)
100
+ last_session_summary = ""
101
+ try:
102
+ from clawed.agent_core.memory.episodes import EpisodicMemory
103
+
104
+ mem = EpisodicMemory()
105
+ latest = mem.get_latest_episode(teacher_id)
106
+ if latest:
107
+ text = latest["text"]
108
+ # Extract teacher's message (first line is usually "Teacher: <msg>")
109
+ first_line = text.split("\n")[0].strip()
110
+ if first_line.startswith("Teacher: "):
111
+ topic = first_line[len("Teacher: "):]
112
+ else:
113
+ topic = first_line
114
+ # Truncate to a concise summary
115
+ if len(topic) > 150:
116
+ topic = topic[:147] + "..."
117
+ last_session_summary = topic
118
+ except Exception as e:
119
+ logger.debug("Last session summary load failed: %s", e)
120
+
99
121
  return {
100
122
  "identity_summary": identity_summary,
101
123
  "curriculum_summary": curriculum_summary,
@@ -104,4 +126,5 @@ def load_memory_context(teacher_id: str, current_message: str) -> dict[str, Any]
104
126
  "preferences_summary": preferences_summary,
105
127
  "autonomy_summary": autonomy_summary,
106
128
  "curriculum_kb_context": curriculum_kb_context,
129
+ "last_session_summary": last_session_summary,
107
130
  }
@@ -232,6 +232,44 @@ class GenerateLessonBundleTool:
232
232
  except Exception:
233
233
  pass # Review is best-effort, don't block on failure
234
234
 
235
+ # ── Generate student packet + admin plan in parallel ──────────
236
+ import asyncio
237
+
238
+ from clawed.llm import LLMClient
239
+
240
+ llm_client = LLMClient(config)
241
+ persona_ctx = persona.to_prompt_context()
242
+ student_packet = None
243
+ admin_plan = None
244
+
245
+ async def _gen_packet():
246
+ return await llm_client.generate_student_packet(
247
+ lesson_json_str, persona_context=persona_ctx,
248
+ )
249
+
250
+ async def _gen_admin():
251
+ return await llm_client.generate_admin_plan(
252
+ lesson_json_str, persona_context=persona_ctx,
253
+ )
254
+
255
+ try:
256
+ results = await asyncio.gather(
257
+ _gen_packet(), _gen_admin(), return_exceptions=True,
258
+ )
259
+ if not isinstance(results[0], Exception):
260
+ student_packet = results[0]
261
+ logger.info("Student packet generated: %d stations, %d guided notes",
262
+ len(student_packet.stations), len(student_packet.guided_notes))
263
+ else:
264
+ logger.warning("Student packet generation failed: %s", results[0])
265
+ if not isinstance(results[1], Exception):
266
+ admin_plan = results[1]
267
+ logger.info("Admin lesson plan generated: %d sections", len(admin_plan.sections))
268
+ else:
269
+ logger.warning("Admin plan generation failed: %s", results[1])
270
+ except Exception as e:
271
+ logger.warning("Parallel generation failed: %s", e)
272
+
235
273
  # ── Export all three files ────────────────────────────────────
236
274
  output_dir = Path("clawed_output").resolve()
237
275
  output_dir.mkdir(parents=True, exist_ok=True)
@@ -240,61 +278,39 @@ class GenerateLessonBundleTool:
240
278
  side_effects: list[str] = []
241
279
  errors: list[str] = []
242
280
 
243
- # 1. Lesson plan DOCX
281
+ # 1. Admin lesson plan DOCX (or fallback to basic lesson plan)
244
282
  try:
245
283
  from clawed.export_docx import export_lesson_docx
246
284
 
247
- docx_path = export_lesson_docx(lesson, persona, output_dir)
285
+ docx_path = export_lesson_docx(
286
+ lesson, persona, output_dir, admin_plan=admin_plan,
287
+ )
248
288
  generated_files.append(docx_path)
249
- side_effects.append(f"Lesson plan DOCX: {docx_path.name}")
289
+ label = "Admin lesson plan" if admin_plan else "Lesson plan"
290
+ side_effects.append(f"{label} DOCX: {docx_path.name}")
250
291
  except Exception as e:
251
292
  logger.error("Lesson DOCX export failed: %s", e)
252
293
  errors.append(f"Lesson plan DOCX failed: {e}")
253
294
 
254
- # 2. Generate student handout via LLM (first-class output, not regex extraction)
295
+ # 2. Student packet DOCX (structured) or fallback to old handout
255
296
  try:
256
- import json_repair
297
+ from clawed.export_handout import export_student_packet_docx
257
298
 
258
- from clawed.llm import LLMClient
259
-
260
- llm_client = LLMClient(config)
261
- handout_raw = await llm_client.generate_student_handout(
262
- lesson_json_str,
263
- persona_context=persona.to_prompt_context(),
264
- subject=subject,
265
- grade=grade,
266
- )
267
- # Parse handout JSON
268
- handout_cleaned = handout_raw.strip()
269
- if handout_cleaned.startswith("```"):
270
- lines = handout_cleaned.split("\n")
271
- lines = lines[1:]
272
- if lines and lines[-1].strip() == "```":
273
- lines = lines[:-1]
274
- handout_cleaned = "\n".join(lines)
275
- try:
276
- handout_data = json.loads(handout_cleaned)
277
- except json.JSONDecodeError:
278
- handout_data = json_repair.loads(handout_cleaned)
279
-
280
- from clawed.export_handout import export_handout_docx
281
-
282
- handout_path = export_handout_docx(handout_data, subject=subject)
283
- if handout_path:
284
- generated_files.append(Path(handout_path))
285
- side_effects.append(f"Student handout DOCX: {Path(handout_path).name}")
286
- except Exception as handout_err:
287
- logger.warning("LLM handout generation failed, falling back: %s", handout_err)
288
- # Fallback to regex-based handout
289
- try:
299
+ if student_packet:
300
+ packet_path = export_student_packet_docx(
301
+ student_packet, subject=subject, output_dir=output_dir,
302
+ )
303
+ else:
304
+ # Fallback: build minimal packet from lesson data
290
305
  from clawed.export_docx import export_student_handout
306
+ packet_path = export_student_handout(lesson, persona, output_dir)
291
307
 
292
- handout_path = export_student_handout(lesson, persona, output_dir)
293
- if handout_path:
294
- generated_files.append(Path(handout_path))
295
- side_effects.append(f"Student handout DOCX: {Path(handout_path).name}")
296
- except Exception:
297
- pass
308
+ if packet_path:
309
+ generated_files.append(Path(packet_path))
310
+ side_effects.append(f"Student packet DOCX: {Path(packet_path).name}")
311
+ except Exception as e:
312
+ logger.warning("Student packet export failed: %s", e)
313
+ errors.append(f"Student packet failed: {e}")
298
314
 
299
315
  # 3. Slideshow PPTX
300
316
  try:
@@ -170,11 +170,20 @@ def export_lesson_docx(
170
170
  persona: "TeacherPersona",
171
171
  output_dir: Path | None = None,
172
172
  agent_name: str = "Claw-ED",
173
+ admin_plan: Any = None,
173
174
  ) -> Path:
174
175
  """Generate a Word document from a lesson plan with embedded academic images.
175
176
 
177
+ If an AdminLessonPlan is provided, generates an observation-ready document
178
+ with per-section teacher/student actions, observer look-fors, anticipated
179
+ responses, and teacher content knowledge.
180
+
176
181
  Returns the path to the saved .docx file.
177
182
  """
183
+ # If we have an admin plan, use the enriched export
184
+ if admin_plan is not None:
185
+ return _export_admin_lesson_docx(lesson, persona, admin_plan, output_dir, agent_name)
186
+
178
187
  from docx import Document
179
188
  from docx.enum.text import WD_ALIGN_PARAGRAPH
180
189
  from docx.shared import Pt, RGBColor
@@ -813,3 +822,203 @@ def _remove_table_borders(table: Any) -> None:
813
822
  )
814
823
  borders.append(element)
815
824
  tblPr.append(borders)
825
+
826
+
827
+ # ── Admin lesson plan export ──────────────────────────────────────────
828
+
829
+
830
+ def _export_admin_lesson_docx(
831
+ lesson: "DailyLesson",
832
+ persona: "TeacherPersona",
833
+ admin_plan: Any,
834
+ output_dir: Path | None = None,
835
+ agent_name: str = "Claw-ED",
836
+ ) -> Path:
837
+ """Generate an observation-ready lesson plan DOCX with multi-column tables.
838
+
839
+ This produces the format administrators expect: per-section teacher actions,
840
+ student actions, observer look-fors, and differentiation in a table layout,
841
+ plus anticipated responses and teacher content knowledge sections.
842
+ """
843
+ from docx import Document
844
+ from docx.enum.table import WD_TABLE_ALIGNMENT
845
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
846
+ from docx.oxml.ns import qn
847
+ from docx.shared import Inches, Pt, RGBColor
848
+
849
+ from clawed.sanitize import sanitize_text
850
+
851
+ doc = Document()
852
+
853
+ # Page setup
854
+ for section in doc.sections:
855
+ section.top_margin = Inches(0.6)
856
+ section.bottom_margin = Inches(0.5)
857
+ section.left_margin = Inches(0.75)
858
+ section.right_margin = Inches(0.75)
859
+
860
+ style = doc.styles["Normal"]
861
+ style.font.name = "Calibri"
862
+ style.font.size = Pt(10)
863
+
864
+ theme = get_color_theme(persona.subject_area or "")
865
+ primary_hex = theme["primary"]
866
+ primary_rgb = RGBColor(int(primary_hex[:2], 16), int(primary_hex[2:4], 16), int(primary_hex[4:6], 16))
867
+
868
+ def _shade(cell, hex_color):
869
+ tc = cell._tc
870
+ tcPr = tc.get_or_add_tcPr()
871
+ shading = tcPr.makeelement(
872
+ qn("w:shd"), {qn("w:val"): "clear", qn("w:color"): "auto", qn("w:fill"): hex_color},
873
+ )
874
+ tcPr.append(shading)
875
+
876
+ # ── I. Header ─────────────────────────────────────────────────────
877
+ title_para = doc.add_paragraph()
878
+ title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
879
+ r = title_para.add_run("LESSON PLAN")
880
+ r.bold = True
881
+ r.font.size = Pt(18)
882
+ r.font.color.rgb = primary_rgb
883
+
884
+ # Overview table
885
+ teacher_name = getattr(admin_plan, "teacher_name", "") or persona.name or "Teacher"
886
+ overview_data = [
887
+ ("Teacher", sanitize_text(teacher_name)),
888
+ ("Course", sanitize_text(getattr(admin_plan, "course", persona.subject_area or ""))),
889
+ ("Date", sanitize_text(getattr(admin_plan, "date", ""))),
890
+ ("Topic", sanitize_text(getattr(admin_plan, "topic", lesson.title))),
891
+ ("Grade Level", sanitize_text(getattr(admin_plan, "grade_level", ""))),
892
+ ("Duration", f"{getattr(admin_plan, 'duration_minutes', 40)} Minutes"),
893
+ ("Aim", sanitize_text(getattr(admin_plan, "aim", lesson.objective))),
894
+ ]
895
+ standards = getattr(admin_plan, "standards", []) or lesson.standards
896
+ if standards:
897
+ overview_data.append(("Standards", sanitize_text(", ".join(standards[:3]))))
898
+ materials = getattr(admin_plan, "materials", []) or lesson.materials_needed
899
+ if materials:
900
+ overview_data.append(("Materials", sanitize_text(", ".join(materials[:6]))))
901
+
902
+ overview_table = doc.add_table(rows=len(overview_data), cols=2)
903
+ overview_table.alignment = WD_TABLE_ALIGNMENT.CENTER
904
+ for i, (label, value) in enumerate(overview_data):
905
+ label_cell = overview_table.rows[i].cells[0]
906
+ value_cell = overview_table.rows[i].cells[1]
907
+ label_cell.text = ""
908
+ lp = label_cell.paragraphs[0]
909
+ lr = lp.add_run(label)
910
+ lr.bold = True
911
+ lr.font.size = Pt(10)
912
+ _shade(label_cell, theme.get("bg_light", "F5F5F5").lstrip("#"))
913
+ value_cell.text = value
914
+ for p in value_cell.paragraphs:
915
+ for run in p.runs:
916
+ run.font.size = Pt(10)
917
+ _set_table_borders(overview_table)
918
+
919
+ # ── II. Section-by-Section Breakdown ──────────────────────────────
920
+ sections = getattr(admin_plan, "sections", [])
921
+ if sections:
922
+ doc.add_paragraph("")
923
+ h = doc.add_heading("Lesson Plan & Pacing", level=2)
924
+ for run in h.runs:
925
+ run.font.color.rgb = primary_rgb
926
+
927
+ # 5-column table: Section | Teacher Actions | Student Actions | Look-Fors | Differentiation
928
+ col_headers = [
929
+ "Section & Timing", "Teacher Actions", "Student Actions",
930
+ "Observer Look-Fors", "Differentiation",
931
+ ]
932
+ section_table = doc.add_table(rows=1 + len(sections), cols=5)
933
+ section_table.alignment = WD_TABLE_ALIGNMENT.CENTER
934
+
935
+ # Header row
936
+ for ci, header in enumerate(col_headers):
937
+ cell = section_table.rows[0].cells[ci]
938
+ cell.text = ""
939
+ p = cell.paragraphs[0]
940
+ r = p.add_run(header)
941
+ r.bold = True
942
+ r.font.size = Pt(9)
943
+ r.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
944
+ _shade(cell, primary_hex)
945
+
946
+ # Data rows
947
+ for ri, sec in enumerate(sections):
948
+ row = section_table.rows[ri + 1]
949
+ if hasattr(sec, "section_name"):
950
+ fields = [
951
+ f"{sec.section_name}\n({sec.timing_minutes} min)",
952
+ sanitize_text(sec.teacher_actions),
953
+ sanitize_text(sec.student_actions),
954
+ sanitize_text(sec.observer_look_fors),
955
+ sanitize_text(sec.differentiation),
956
+ ]
957
+ else:
958
+ fields = [str(sec)] + [""] * 4
959
+ for ci, text in enumerate(fields):
960
+ cell = row.cells[ci]
961
+ cell.text = text
962
+ for p in cell.paragraphs:
963
+ for run in p.runs:
964
+ run.font.size = Pt(9)
965
+ # Alternate row shading
966
+ if ri % 2 == 0:
967
+ for cell in row.cells:
968
+ _shade(cell, "F9F9F9")
969
+
970
+ _set_table_borders(section_table)
971
+
972
+ # ── III. Anticipated Responses & Misconceptions ───────────────────
973
+ responses = getattr(admin_plan, "anticipated_responses", [])
974
+ if responses:
975
+ doc.add_paragraph("")
976
+ h = doc.add_heading("Anticipated Student Responses & Misconceptions", level=2)
977
+ for run in h.runs:
978
+ run.font.color.rgb = primary_rgb
979
+
980
+ for resp in responses:
981
+ is_mis = getattr(resp, "is_misconception", False) if hasattr(resp, "is_misconception") else False
982
+ text = sanitize_text(getattr(resp, "response_or_misconception", str(resp)))
983
+ correction = sanitize_text(getattr(resp, "teacher_correction", ""))
984
+
985
+ p = doc.add_paragraph()
986
+ prefix = "MISCONCEPTION: " if is_mis else "EXPECTED: "
987
+ pr = p.add_run(prefix)
988
+ pr.bold = True
989
+ pr.font.size = Pt(10)
990
+ pr.font.color.rgb = RGBColor(0xCC, 0x33, 0x33) if is_mis else RGBColor(0x33, 0x99, 0x33)
991
+ tr = p.add_run(f'"{text}"')
992
+ tr.italic = True
993
+ tr.font.size = Pt(10)
994
+
995
+ if correction:
996
+ cp = doc.add_paragraph()
997
+ cp.paragraph_format.left_indent = Inches(0.3)
998
+ cr = cp.add_run(f"Redirect: {correction}")
999
+ cr.font.size = Pt(9)
1000
+ cr.font.color.rgb = RGBColor(0x44, 0x44, 0x44)
1001
+
1002
+ # ── IV. Teacher Content Knowledge ─────────────────────────────────
1003
+ tck = sanitize_text(getattr(admin_plan, "teacher_content_knowledge", ""))
1004
+ if tck:
1005
+ doc.add_paragraph("")
1006
+ h = doc.add_heading("Teacher Content Knowledge", level=2)
1007
+ for run in h.runs:
1008
+ run.font.color.rgb = primary_rgb
1009
+ p = doc.add_paragraph(tck)
1010
+ p.paragraph_format.space_after = Pt(6)
1011
+ for run in p.runs:
1012
+ run.font.size = Pt(10)
1013
+
1014
+ # ── Footer ────────────────────────────────────────────────────────
1015
+ doc.add_paragraph("")
1016
+ gen_footer = doc.add_paragraph()
1017
+ gen_footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
1018
+ fr = gen_footer.add_run(f"Generated by {agent_name}")
1019
+ fr.font.size = Pt(8)
1020
+ fr.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
1021
+
1022
+ out = _resolve_output(output_dir, lesson, ".docx")
1023
+ doc.save(str(out))
1024
+ return out