clawed 2.3.2__tar.gz → 2.3.4__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.2 → clawed-2.3.4}/PKG-INFO +1 -1
- {clawed-2.3.2 → clawed-2.3.4}/clawed/__init__.py +1 -1
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/prompt.py +5 -1
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/generate_lesson_bundle.py +69 -11
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/ingest_materials.py +57 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/update_soul.py +11 -2
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/generate.py +28 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/ingest.py +48 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/llm.py +26 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/memory_engine.py +29 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/models.py +36 -0
- clawed-2.3.4/clawed/persona_evolution.py +271 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/admin_lesson_plan.txt +5 -1
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/lesson_plan.txt +5 -1
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/persona_extract.txt +11 -1
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/student_packet.txt +10 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/reading_report.py +186 -2
- {clawed-2.3.2 → clawed-2.3.4}/clawed/slide_images.py +53 -15
- {clawed-2.3.2 → clawed-2.3.4}/clawed/sub_packet.py +7 -0
- clawed-2.3.4/clawed/voice_check.py +192 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/workspace.py +110 -0
- {clawed-2.3.2 → clawed-2.3.4}/pyproject.toml +1 -1
- {clawed-2.3.2 → clawed-2.3.4}/.gitignore +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/LICENSE +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/README.md +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/__main__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/_legacy_gateway.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/approvals.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/autonomy.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/context.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/core.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/custom_tools.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/drive/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/drive/auth.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/drive/client.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/fake_llm.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/loop.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/memory/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/memory/curriculum.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/memory/curriculum_kb.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/memory/embeddings.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/memory/episodes.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/memory/identity.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/memory/loader.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/memory/preferences.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/planner.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/scheduler.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/base.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/configure_profile.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/curriculum_map.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/drive_create_doc.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/drive_create_slides.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/drive_list.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/drive_organize.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/drive_read.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/drive_upload.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/export_document.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/gap_analysis.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/generate_assessment.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/generate_lesson.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/generate_materials.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/generate_unit.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/parent_comm.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/read_heartbeat.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/read_workspace.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/request_approval.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/schedule_task.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/search_lessons.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/search_my_materials.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/search_standards.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/student_insights.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/sub_packet.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/agent_core/tools/switch_model.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/analytics.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/deps.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/chat.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/export.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/feedback.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/gateway_chat.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/generate.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/ingest.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/lessons.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/school.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/settings.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/routes/tools.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/server.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/static/app.js +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/static/style.css +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/static/widget.js +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/analytics.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/base.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/dashboard.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/generate.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/index.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/lesson.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/profile.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/settings.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/stats.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/api/templates/students.html +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/assessment.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/asset_registry.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/auth/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/auth/google_auth.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/bot_state.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/chat.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/cli.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/cli_chat.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/_helpers.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/bot.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/config.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/config_llm.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/config_profile.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/export.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/generate_assessment.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/generate_unit.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/queue.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/schedule_cmd.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/sub.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/commands/workspace_cmd.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/config.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/corpus.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/curriculum_map.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/database.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/demo/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/demo/demo_assessment.json +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/demo/demo_lesson_science_g6.json +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/demo/demo_lesson_social_studies_g8.json +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/demo/demo_unit_plan.json +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/differentiation.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/doc_export.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/drive.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/evaluation.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/export_docx.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/export_handout.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/export_markdown.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/export_pdf.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/export_pptx.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/export_templates.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/export_theme.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/exporter.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/feedback.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/formats/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/formats/flipchart.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/formats/notebook.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/formats/xbk.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/gateway.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/gateway_response.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/generation.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/export.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/feedback.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/gaps.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/generate.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/misc.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/onboard.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/schedule.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/handlers/standards.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/improver.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/ingestor.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/io.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/lesson.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/materials.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/mcp_server.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/model_router.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/onboarding.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/openclaw_plugin.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/parent_comm.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/persona.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/planner.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/504_accommodations.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/assessment.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/curriculum_gaps.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/dbq_assessment.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/differentiation.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/formative_assessment.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/iep_modification.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/pacing_guide.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/parent_note.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/quiz.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/rubric.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/sub_packet.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/summative_assessment.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/tiered_assignments.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/unit_plan.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/worksheet.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/prompts/year_map.txt +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/quality.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/router.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/sanitize.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/scheduler.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/school.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/search.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/art.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/base.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/computer_science.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/ela.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/foreign_language.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/history.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/library.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/math.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/music.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/physical_education.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/science.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/social_studies.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/skills/special_education.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/standards.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/state.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/state_standards.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/student_bot.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/student_cli.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/student_telegram_bot.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/task_queue.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/templates_lib.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/tools.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/transports/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/transports/cli.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/transports/openclaw.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/transports/student_telegram.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/transports/telegram.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/transports/web.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/tui.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/tui_chat.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/clawed/voice.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/eduagent/__init__.py +0 -0
- {clawed-2.3.2 → clawed-2.3.4}/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.4
|
|
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
|
|
@@ -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.4"
|
|
21
21
|
__author__ = "Jon Maccarello & Claw-ED contributors"
|
|
22
22
|
__description__ = "Personal AI teaching agent. Learns your voice, works while you sleep."
|
|
23
23
|
|
|
@@ -91,7 +91,11 @@ def build_system_prompt(
|
|
|
91
91
|
" 'Building your lesson package now — plan, handout, and slides coming up.'\n"
|
|
92
92
|
"The teacher should always know you're working, not stuck.\n"
|
|
93
93
|
"1. Read SOUL.md to know your voice and values\n"
|
|
94
|
-
"2.
|
|
94
|
+
"2. **MANDATORY: Before calling generate_lesson_bundle, ALWAYS call "
|
|
95
|
+
"search_my_materials first** with the lesson topic. This is non-negotiable. "
|
|
96
|
+
"The teacher has uploaded materials — if you skip this step, you will "
|
|
97
|
+
"generate generic content instead of building on their prior work. "
|
|
98
|
+
"Tell the teacher what you found before generating.\n"
|
|
95
99
|
"3. Generate complete packages (lesson plan + student handout + slideshow) "
|
|
96
100
|
"using generate_lesson_bundle\n"
|
|
97
101
|
"4. Never ask 'want me to create materials?' -- just create them\n"
|
|
@@ -22,7 +22,10 @@ class GenerateLessonBundleTool:
|
|
|
22
22
|
"description": (
|
|
23
23
|
"Generate a COMPLETE teaching package for a topic: "
|
|
24
24
|
"a lesson plan (DOCX), a student handout (DOCX), and "
|
|
25
|
-
"a slideshow (PPTX). All three files are created at once."
|
|
25
|
+
"a slideshow (PPTX). All three files are created at once. "
|
|
26
|
+
"IMPORTANT: Always call search_my_materials FIRST to find "
|
|
27
|
+
"the teacher's existing materials on this topic before "
|
|
28
|
+
"calling this tool."
|
|
26
29
|
),
|
|
27
30
|
"parameters": {
|
|
28
31
|
"type": "object",
|
|
@@ -232,6 +235,23 @@ class GenerateLessonBundleTool:
|
|
|
232
235
|
except Exception:
|
|
233
236
|
pass # Review is best-effort, don't block on failure
|
|
234
237
|
|
|
238
|
+
# ── Voice validation ──────────────────────────────────────────
|
|
239
|
+
voice_notes: list[str] = []
|
|
240
|
+
try:
|
|
241
|
+
from clawed.voice_check import check_voice_match
|
|
242
|
+
|
|
243
|
+
voice_result = check_voice_match(
|
|
244
|
+
persona=persona,
|
|
245
|
+
do_now=lesson.do_now,
|
|
246
|
+
direct_instruction_opening=lesson.direct_instruction[:500] if lesson.direct_instruction else "",
|
|
247
|
+
)
|
|
248
|
+
if not voice_result.passed:
|
|
249
|
+
for issue in voice_result.issues:
|
|
250
|
+
voice_notes.append(issue)
|
|
251
|
+
logger.info("Voice check issue: %s", issue)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.debug("Voice check failed: %s", e)
|
|
254
|
+
|
|
235
255
|
# ── Generate student packet + admin plan in parallel ──────────
|
|
236
256
|
import asyncio
|
|
237
257
|
|
|
@@ -323,19 +343,57 @@ class GenerateLessonBundleTool:
|
|
|
323
343
|
logger.error("PPTX export failed: %s", e)
|
|
324
344
|
errors.append(f"Slideshow PPTX failed: {e}")
|
|
325
345
|
|
|
326
|
-
# ── Build response
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
lines.append("
|
|
334
|
-
|
|
335
|
-
|
|
346
|
+
# ── Build honest response ─────────────────────────────────────
|
|
347
|
+
used_fallback_packet = not student_packet and any("Student packet" in s for s in side_effects)
|
|
348
|
+
|
|
349
|
+
lines = []
|
|
350
|
+
|
|
351
|
+
if len(generated_files) == 3 and not errors:
|
|
352
|
+
lines.append(f"Complete teaching package for: {lesson.title}")
|
|
353
|
+
lines.append("All three files ready to print:")
|
|
354
|
+
for se in side_effects:
|
|
355
|
+
lines.append(f" - {se}")
|
|
356
|
+
elif generated_files:
|
|
357
|
+
lines.append(f"Generated {len(generated_files)} of 3 files for: {lesson.title}")
|
|
358
|
+
for se in side_effects:
|
|
359
|
+
lines.append(f" - {se}")
|
|
360
|
+
if errors:
|
|
361
|
+
lines.append("")
|
|
362
|
+
for err in errors:
|
|
363
|
+
clean_err = str(err).split("\n")[0][:200]
|
|
364
|
+
lines.append(f" Could not generate: {clean_err}")
|
|
365
|
+
lines.append("Want me to try the failed item(s) again?")
|
|
366
|
+
else:
|
|
367
|
+
lines.append(f"Failed to generate teaching package for: {lesson.title}")
|
|
336
368
|
for err in errors:
|
|
337
369
|
lines.append(f" - {err}")
|
|
338
370
|
|
|
371
|
+
if used_fallback_packet:
|
|
372
|
+
lines.append("")
|
|
373
|
+
lines.append(
|
|
374
|
+
"Note: The student packet was generated using a simpler method — "
|
|
375
|
+
"it may not have full graphic organizers. Let me know if you'd like me to regenerate it."
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if kb_context:
|
|
379
|
+
lines.append("\nReferenced your existing materials on this topic.")
|
|
380
|
+
|
|
381
|
+
# Self-review findings
|
|
382
|
+
try:
|
|
383
|
+
if review and not review.get("passed", True) and review.get("issues"):
|
|
384
|
+
lines.append("")
|
|
385
|
+
lines.append("Quality notes:")
|
|
386
|
+
for issue in review["issues"][:3]:
|
|
387
|
+
lines.append(f" - {issue}")
|
|
388
|
+
except NameError:
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
if voice_notes:
|
|
392
|
+
lines.append("\nVoice match notes:")
|
|
393
|
+
for note in voice_notes:
|
|
394
|
+
lines.append(f" - {note}")
|
|
395
|
+
lines.append("Want me to adjust the lesson to better match your voice?")
|
|
396
|
+
|
|
339
397
|
return ToolResult(
|
|
340
398
|
text="\n".join(lines),
|
|
341
399
|
files=generated_files,
|
|
@@ -65,7 +65,31 @@ class IngestMaterialsTool:
|
|
|
65
65
|
from clawed.persona import extract_persona, save_persona
|
|
66
66
|
|
|
67
67
|
persona = await extract_persona(docs, context.config)
|
|
68
|
+
# Override LLM-inferred name with configured teacher name
|
|
69
|
+
try:
|
|
70
|
+
if context.config and context.config.teacher_profile and context.config.teacher_profile.name:
|
|
71
|
+
persona.name = f"{context.config.teacher_profile.name} Teaching Persona"
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
try:
|
|
75
|
+
_id_path = Path.home() / ".eduagent" / "workspace" / "identity.md"
|
|
76
|
+
if _id_path.exists():
|
|
77
|
+
import re as _re
|
|
78
|
+
_id_content = _id_path.read_text(encoding="utf-8")
|
|
79
|
+
_name_match = _re.match(r"^#\s+(.+)", _id_content)
|
|
80
|
+
if _name_match:
|
|
81
|
+
_tname = _name_match.group(1).strip()
|
|
82
|
+
if _tname and _tname != "Teacher":
|
|
83
|
+
persona.name = f"{_tname} Teaching Persona"
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
68
86
|
save_persona(persona, Path.home() / ".eduagent")
|
|
87
|
+
# Track persona changes for evolution
|
|
88
|
+
try:
|
|
89
|
+
from clawed.persona_evolution import record_ingestion_changes
|
|
90
|
+
record_ingestion_changes(old_persona=None, new_persona=persona)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
69
93
|
except Exception:
|
|
70
94
|
pass
|
|
71
95
|
|
|
@@ -129,6 +153,39 @@ class IngestMaterialsTool:
|
|
|
129
153
|
except Exception as e:
|
|
130
154
|
logger.debug("KB indexing failed: %s", e)
|
|
131
155
|
|
|
156
|
+
# Register assets for file-level search
|
|
157
|
+
try:
|
|
158
|
+
from clawed.asset_registry import AssetRegistry
|
|
159
|
+
registry = AssetRegistry()
|
|
160
|
+
asset_count = 0
|
|
161
|
+
for doc in docs:
|
|
162
|
+
doc_type_val = (
|
|
163
|
+
doc.doc_type.value
|
|
164
|
+
if hasattr(doc.doc_type, "value")
|
|
165
|
+
else str(doc.doc_type)
|
|
166
|
+
)
|
|
167
|
+
extraction = None
|
|
168
|
+
if doc.source_path:
|
|
169
|
+
try:
|
|
170
|
+
from clawed.ingestor import extract_rich
|
|
171
|
+
extraction = extract_rich(Path(doc.source_path))
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
aid = registry.register_asset(
|
|
175
|
+
teacher_id=context.teacher_id,
|
|
176
|
+
source_path=doc.source_path or "",
|
|
177
|
+
title=doc.title,
|
|
178
|
+
doc_type=doc_type_val,
|
|
179
|
+
text=doc.content,
|
|
180
|
+
extraction=extraction,
|
|
181
|
+
)
|
|
182
|
+
if aid:
|
|
183
|
+
asset_count += 1
|
|
184
|
+
if asset_count:
|
|
185
|
+
summary += f" ({asset_count} files catalogued for search)"
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.debug("Asset registration failed: %s", e)
|
|
188
|
+
|
|
132
189
|
# Update SOUL.md with what we learned
|
|
133
190
|
try:
|
|
134
191
|
soul_path = Path.home() / ".eduagent" / "workspace" / "SOUL.md"
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
5
|
from datetime import date
|
|
6
|
-
from pathlib import Path
|
|
7
6
|
from typing import Any
|
|
8
7
|
|
|
9
8
|
from clawed.agent_core.context import AgentContext, ToolResult
|
|
@@ -92,7 +91,9 @@ class UpdateSoulTool:
|
|
|
92
91
|
text=f"Unknown section '{section_key}'. Valid sections: {valid}"
|
|
93
92
|
)
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
from clawed.workspace import SOUL_PATH
|
|
95
|
+
|
|
96
|
+
soul_path = SOUL_PATH
|
|
96
97
|
soul_path.parent.mkdir(parents=True, exist_ok=True)
|
|
97
98
|
|
|
98
99
|
# Read or create SOUL.md
|
|
@@ -104,6 +105,14 @@ class UpdateSoulTool:
|
|
|
104
105
|
# Build the datestamped entry
|
|
105
106
|
entry = f"\n\n*({date.today().isoformat()})* {content}\n"
|
|
106
107
|
|
|
108
|
+
# Check for duplicate before writing
|
|
109
|
+
from clawed.workspace import _deduplicate_entry
|
|
110
|
+
|
|
111
|
+
if _deduplicate_entry(current, content, header):
|
|
112
|
+
return ToolResult(
|
|
113
|
+
text=f"Observation already exists in SOUL.md section '{section_key}' — skipping duplicate.",
|
|
114
|
+
)
|
|
115
|
+
|
|
107
116
|
# Insert after the section header
|
|
108
117
|
if header in current:
|
|
109
118
|
current = current.replace(header, header + entry, 1)
|
|
@@ -130,8 +130,36 @@ def ingest(
|
|
|
130
130
|
persona = _run_async(extract_persona(documents))
|
|
131
131
|
progress.update(task, description="Persona extracted!")
|
|
132
132
|
|
|
133
|
+
# Override LLM-inferred name with configured teacher name
|
|
134
|
+
try:
|
|
135
|
+
cfg = AppConfig.load()
|
|
136
|
+
if cfg.teacher_profile and cfg.teacher_profile.name:
|
|
137
|
+
persona.name = f"{cfg.teacher_profile.name} Teaching Persona"
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
# Also check identity.md
|
|
141
|
+
try:
|
|
142
|
+
identity_path = Path.home() / ".eduagent" / "workspace" / "identity.md"
|
|
143
|
+
if identity_path.exists():
|
|
144
|
+
import re
|
|
145
|
+
content = identity_path.read_text(encoding="utf-8")
|
|
146
|
+
name_match = re.match(r"^#\s+(.+)", content)
|
|
147
|
+
if name_match:
|
|
148
|
+
teacher_name = name_match.group(1).strip()
|
|
149
|
+
if teacher_name and teacher_name != "Teacher":
|
|
150
|
+
persona.name = f"{teacher_name} Teaching Persona"
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
133
154
|
out = save_persona(persona, _output_dir())
|
|
134
155
|
|
|
156
|
+
# Track persona changes for evolution
|
|
157
|
+
try:
|
|
158
|
+
from clawed.persona_evolution import record_ingestion_changes
|
|
159
|
+
record_ingestion_changes(old_persona=None, new_persona=persona)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
135
163
|
# Index documents into curriculum knowledge base for KB search
|
|
136
164
|
kb_msg = ""
|
|
137
165
|
try:
|
|
@@ -60,6 +60,25 @@ class IngestHandler:
|
|
|
60
60
|
try:
|
|
61
61
|
from clawed.models import AppConfig
|
|
62
62
|
persona = await extract_persona(documents, AppConfig.load())
|
|
63
|
+
# Override LLM-inferred name with configured teacher name
|
|
64
|
+
try:
|
|
65
|
+
config = AppConfig.load()
|
|
66
|
+
if config.teacher_profile and config.teacher_profile.name:
|
|
67
|
+
persona.name = f"{config.teacher_profile.name} Teaching Persona"
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
try:
|
|
71
|
+
_id_path = Path.home() / ".eduagent" / "workspace" / "identity.md"
|
|
72
|
+
if _id_path.exists():
|
|
73
|
+
import re as _re
|
|
74
|
+
_id_content = _id_path.read_text(encoding="utf-8")
|
|
75
|
+
_name_match = _re.match(r"^#\s+(.+)", _id_content)
|
|
76
|
+
if _name_match:
|
|
77
|
+
_tname = _name_match.group(1).strip()
|
|
78
|
+
if _tname and _tname != "Teacher":
|
|
79
|
+
persona.name = f"{_tname} Teaching Persona"
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
63
82
|
from clawed.persona import save_persona
|
|
64
83
|
save_persona(persona, Path.home() / ".eduagent")
|
|
65
84
|
style_info = f"\nLearned teaching style: {persona.teaching_style}"
|
|
@@ -97,6 +116,35 @@ class IngestHandler:
|
|
|
97
116
|
except Exception as e:
|
|
98
117
|
logger.debug("KB indexing skipped: %s", e)
|
|
99
118
|
|
|
119
|
+
# Register assets (files, images, YouTube links) for search
|
|
120
|
+
try:
|
|
121
|
+
from clawed.asset_registry import AssetRegistry
|
|
122
|
+
registry = AssetRegistry()
|
|
123
|
+
asset_count = 0
|
|
124
|
+
for doc in documents:
|
|
125
|
+
doc_type_val = doc.doc_type.value if hasattr(doc.doc_type, "value") else str(doc.doc_type)
|
|
126
|
+
extraction = None
|
|
127
|
+
if doc.source_path:
|
|
128
|
+
try:
|
|
129
|
+
from clawed.ingestor import extract_rich
|
|
130
|
+
extraction = extract_rich(Path(doc.source_path))
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
aid = registry.register_asset(
|
|
134
|
+
teacher_id=teacher_id,
|
|
135
|
+
source_path=doc.source_path or "",
|
|
136
|
+
title=doc.title,
|
|
137
|
+
doc_type=doc_type_val,
|
|
138
|
+
text=doc.content,
|
|
139
|
+
extraction=extraction,
|
|
140
|
+
)
|
|
141
|
+
if aid:
|
|
142
|
+
asset_count += 1
|
|
143
|
+
if asset_count:
|
|
144
|
+
kb_info += f" ({asset_count} files catalogued with images and links)"
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.debug("Asset registration skipped: %s", e)
|
|
147
|
+
|
|
100
148
|
return GatewayResponse(
|
|
101
149
|
text=f"Ingested {len(documents)} document(s).{style_info}{kb_info}"
|
|
102
150
|
)
|
|
@@ -293,10 +293,36 @@ class LLMClient:
|
|
|
293
293
|
|
|
294
294
|
prompt_path = Path(__file__).parent / "prompts" / "student_packet.txt"
|
|
295
295
|
prompt_template = prompt_path.read_text(encoding="utf-8")
|
|
296
|
+
|
|
297
|
+
# Build handout style block from persona context
|
|
298
|
+
import re as _re
|
|
299
|
+
handout_style = ""
|
|
300
|
+
if persona_context:
|
|
301
|
+
hs_match = _re.search(
|
|
302
|
+
r"=== Handout Style ===\n(.+?)(?:\n===|\Z)",
|
|
303
|
+
persona_context, _re.DOTALL,
|
|
304
|
+
)
|
|
305
|
+
if hs_match:
|
|
306
|
+
handout_style = hs_match.group(1).strip()
|
|
307
|
+
|
|
308
|
+
if handout_style:
|
|
309
|
+
handout_style_block = (
|
|
310
|
+
f"This teacher's handout style: {handout_style}. "
|
|
311
|
+
"Match this format. If the teacher uses guided notes, include them. "
|
|
312
|
+
"If they use graphic organizers, lead with those. If they prefer "
|
|
313
|
+
"dense source packets, make the sources the centerpiece. "
|
|
314
|
+
"Don't impose a format the teacher wouldn't recognize as their own."
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
handout_style_block = (
|
|
318
|
+
"No specific handout style detected — use the default format below."
|
|
319
|
+
)
|
|
320
|
+
|
|
296
321
|
prompt = (
|
|
297
322
|
prompt_template
|
|
298
323
|
.replace("{lesson_json}", lesson_json[:6000])
|
|
299
324
|
.replace("{persona}", persona_context)
|
|
325
|
+
.replace("{handout_style_block}", handout_style_block)
|
|
300
326
|
)
|
|
301
327
|
|
|
302
328
|
system = (
|
|
@@ -397,6 +397,35 @@ def process_feedback(
|
|
|
397
397
|
except Exception as e:
|
|
398
398
|
logger.debug("Drift detection failed: %s", e)
|
|
399
399
|
|
|
400
|
+
# Check if persona evolution should trigger
|
|
401
|
+
try:
|
|
402
|
+
from clawed.persona_evolution import apply_confirmed_changes, get_confirmed_changes
|
|
403
|
+
confirmed = get_confirmed_changes()
|
|
404
|
+
if confirmed:
|
|
405
|
+
from clawed.commands._helpers import persona_path
|
|
406
|
+
from clawed.persona import load_persona
|
|
407
|
+
pp = persona_path()
|
|
408
|
+
if pp.exists():
|
|
409
|
+
current_persona = load_persona(pp)
|
|
410
|
+
updated, descriptions = apply_confirmed_changes(current_persona)
|
|
411
|
+
if descriptions:
|
|
412
|
+
pp.write_text(updated.model_dump_json(indent=2), encoding="utf-8")
|
|
413
|
+
# Log to SOUL.md
|
|
414
|
+
from clawed.workspace import SOUL_PATH
|
|
415
|
+
if SOUL_PATH.exists():
|
|
416
|
+
soul_content = SOUL_PATH.read_text(encoding="utf-8")
|
|
417
|
+
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
418
|
+
for desc in descriptions:
|
|
419
|
+
entry = f"\n\n*({stamp})* Fingerprint updated: {desc}\n"
|
|
420
|
+
marker = "## Agent Observations"
|
|
421
|
+
soul_content = soul_content.replace(
|
|
422
|
+
marker, marker + entry, 1
|
|
423
|
+
)
|
|
424
|
+
SOUL_PATH.write_text(soul_content, encoding="utf-8")
|
|
425
|
+
logger.info("Persona evolution applied: %s", "; ".join(descriptions))
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.debug("Persona evolution check failed: %s", e)
|
|
428
|
+
|
|
400
429
|
return applied
|
|
401
430
|
|
|
402
431
|
|
|
@@ -201,6 +201,11 @@ class TeacherPersona(BaseModel):
|
|
|
201
201
|
'Always ends DI with "here is where it gets interesting"',
|
|
202
202
|
'Uses physical movement — students walk to corners for opinion spectrums'."""
|
|
203
203
|
|
|
204
|
+
handout_style: str = ""
|
|
205
|
+
"""Description of the teacher's handout/worksheet style, e.g.
|
|
206
|
+
'Dense text packets with primary source excerpts and marginal annotations'
|
|
207
|
+
or 'Graphic organizer-heavy with minimal text, always includes an image hook'."""
|
|
208
|
+
|
|
204
209
|
def to_prompt_context(self) -> str:
|
|
205
210
|
"""Serialize persona into a string for LLM prompt injection."""
|
|
206
211
|
lines = [
|
|
@@ -250,6 +255,10 @@ class TeacherPersona(BaseModel):
|
|
|
250
255
|
for move in self.signature_moves:
|
|
251
256
|
lines.append(f"- {move}")
|
|
252
257
|
|
|
258
|
+
if self.handout_style:
|
|
259
|
+
lines.append(f"\n=== Handout Style ===\n{self.handout_style}")
|
|
260
|
+
lines.append("Student packets must match this format.")
|
|
261
|
+
|
|
253
262
|
if self.voice_examples:
|
|
254
263
|
lines.append("")
|
|
255
264
|
lines.append("=== Voice Examples (write like this) ===")
|
|
@@ -479,6 +488,15 @@ class WorksheetItem(BaseModel):
|
|
|
479
488
|
answer_key: str = ""
|
|
480
489
|
point_value: int = 1
|
|
481
490
|
|
|
491
|
+
@field_validator("point_value", mode="before")
|
|
492
|
+
@classmethod
|
|
493
|
+
def _coerce_point_value(cls, v):
|
|
494
|
+
if isinstance(v, str):
|
|
495
|
+
import re
|
|
496
|
+
match = re.match(r"(\d+)", v.strip())
|
|
497
|
+
return int(match.group(1)) if match else 1
|
|
498
|
+
return v
|
|
499
|
+
|
|
482
500
|
|
|
483
501
|
class AssessmentQuestion(BaseModel):
|
|
484
502
|
"""A single assessment question."""
|
|
@@ -490,6 +508,15 @@ class AssessmentQuestion(BaseModel):
|
|
|
490
508
|
correct_answer: str = ""
|
|
491
509
|
point_value: int = 1
|
|
492
510
|
|
|
511
|
+
@field_validator("point_value", mode="before")
|
|
512
|
+
@classmethod
|
|
513
|
+
def _coerce_point_value(cls, v):
|
|
514
|
+
if isinstance(v, str):
|
|
515
|
+
import re
|
|
516
|
+
match = re.match(r"(\d+)", v.strip())
|
|
517
|
+
return int(match.group(1)) if match else 1
|
|
518
|
+
return v
|
|
519
|
+
|
|
493
520
|
|
|
494
521
|
class RubricCriterion(BaseModel):
|
|
495
522
|
"""A single rubric criterion with levels."""
|
|
@@ -665,6 +692,15 @@ class SummativeQuestion(BaseModel):
|
|
|
665
692
|
point_value: int = 1
|
|
666
693
|
standard_aligned: str = ""
|
|
667
694
|
|
|
695
|
+
@field_validator("point_value", mode="before")
|
|
696
|
+
@classmethod
|
|
697
|
+
def _coerce_point_value(cls, v):
|
|
698
|
+
if isinstance(v, str):
|
|
699
|
+
import re
|
|
700
|
+
match = re.match(r"(\d+)", v.strip())
|
|
701
|
+
return int(match.group(1)) if match else 1
|
|
702
|
+
return v
|
|
703
|
+
|
|
668
704
|
|
|
669
705
|
class SummativeAssessment(BaseModel):
|
|
670
706
|
"""Unit test — mix of MC, short answer, DBQ/essay aligned to all unit objectives."""
|