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