clawed 2.0.4__tar.gz → 2.1.0__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.0.4 → clawed-2.1.0}/PKG-INFO +1 -1
- {clawed-2.0.4 → clawed-2.1.0}/clawed/__init__.py +1 -1
- clawed-2.1.0/clawed/asset_registry.py +409 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/generate.py +77 -24
- {clawed-2.0.4 → clawed-2.1.0}/pyproject.toml +1 -1
- {clawed-2.0.4 → clawed-2.1.0}/.gitignore +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/LICENSE +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/README.md +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/__main__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/_legacy_gateway.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/approvals.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/autonomy.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/context.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/core.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/custom_tools.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/drive/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/drive/auth.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/drive/client.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/fake_llm.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/loop.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/memory/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/memory/curriculum.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/memory/curriculum_kb.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/memory/embeddings.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/memory/episodes.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/memory/identity.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/memory/loader.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/memory/preferences.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/planner.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/prompt.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/scheduler.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/base.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/configure_profile.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/curriculum_map.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/drive_create_doc.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/drive_create_slides.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/drive_list.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/drive_organize.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/drive_read.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/drive_upload.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/export_document.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/gap_analysis.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/generate_assessment.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/generate_lesson.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/generate_lesson_bundle.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/generate_materials.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/generate_unit.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/ingest_materials.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/parent_comm.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/read_heartbeat.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/read_workspace.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/request_approval.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/schedule_task.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/search_lessons.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/search_my_materials.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/search_standards.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/student_insights.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/sub_packet.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/switch_model.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/agent_core/tools/update_soul.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/analytics.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/deps.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/chat.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/export.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/feedback.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/gateway_chat.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/generate.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/ingest.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/lessons.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/school.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/settings.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/routes/tools.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/server.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/static/app.js +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/static/style.css +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/static/widget.js +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/analytics.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/base.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/dashboard.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/generate.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/index.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/lesson.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/profile.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/settings.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/stats.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/api/templates/students.html +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/assessment.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/auth/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/auth/google_auth.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/bot_state.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/chat.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/cli.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/cli_chat.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/_helpers.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/bot.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/config.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/config_llm.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/config_profile.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/export.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/generate_assessment.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/generate_unit.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/queue.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/schedule_cmd.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/sub.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/commands/workspace_cmd.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/config.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/corpus.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/curriculum_map.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/database.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/demo/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/demo/demo_assessment.json +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/demo/demo_lesson_science_g6.json +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/demo/demo_lesson_social_studies_g8.json +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/demo/demo_unit_plan.json +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/differentiation.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/doc_export.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/drive.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/evaluation.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/export_docx.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/export_handout.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/export_markdown.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/export_pdf.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/export_pptx.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/export_templates.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/export_theme.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/exporter.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/feedback.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/formats/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/formats/flipchart.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/formats/notebook.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/formats/xbk.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/gateway.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/gateway_response.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/generation.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/export.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/feedback.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/gaps.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/generate.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/ingest.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/misc.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/onboard.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/schedule.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/handlers/standards.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/improver.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/ingestor.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/io.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/lesson.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/llm.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/materials.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/mcp_server.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/memory_engine.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/model_router.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/models.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/onboarding.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/openclaw_plugin.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/parent_comm.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/persona.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/planner.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/504_accommodations.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/assessment.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/curriculum_gaps.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/dbq_assessment.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/differentiation.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/formative_assessment.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/iep_modification.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/lesson_plan.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/pacing_guide.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/parent_note.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/persona_extract.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/quiz.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/rubric.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/sub_packet.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/summative_assessment.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/tiered_assignments.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/unit_plan.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/worksheet.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/prompts/year_map.txt +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/quality.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/reading_report.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/router.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/sanitize.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/scheduler.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/school.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/search.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/art.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/base.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/computer_science.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/ela.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/foreign_language.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/history.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/library.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/math.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/music.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/physical_education.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/science.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/social_studies.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/skills/special_education.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/slide_images.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/standards.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/state.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/state_standards.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/student_bot.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/student_cli.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/student_telegram_bot.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/sub_packet.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/task_queue.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/templates_lib.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/tools.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/transports/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/transports/cli.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/transports/openclaw.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/transports/student_telegram.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/transports/telegram.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/transports/web.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/tui.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/tui_chat.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/voice.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/clawed/workspace.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/eduagent/__init__.py +0 -0
- {clawed-2.0.4 → clawed-2.1.0}/eduagent/_compat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clawed
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
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.0
|
|
20
|
+
__version__ = "2.1.0"
|
|
21
21
|
__author__ = "Jon Maccarello & Claw-ED contributors"
|
|
22
22
|
__description__ = "Personal AI teaching agent. Learns your voice, works while you sleep."
|
|
23
23
|
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Asset registry — file-level awareness of teacher's materials.
|
|
2
|
+
|
|
3
|
+
Sits alongside the curriculum KB (same SQLite database). While the KB stores
|
|
4
|
+
text chunks for semantic search, the asset registry stores one row per *file*
|
|
5
|
+
with rich metadata: material type, embedded images, YouTube links, slide counts.
|
|
6
|
+
|
|
7
|
+
This powers the "I found your Reconstruction PPT from 2020" experience.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
import sqlite3
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_DEFAULT_DB = Path.home() / ".eduagent" / "memory" / "curriculum_kb.db"
|
|
24
|
+
|
|
25
|
+
# ── YouTube URL normalization ────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
_YT_PATTERNS = [
|
|
28
|
+
re.compile(r'(?:https?://)?(?:www\.)?youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})'),
|
|
29
|
+
re.compile(r'(?:https?://)?youtu\.be/([a-zA-Z0-9_-]{11})'),
|
|
30
|
+
re.compile(r'(?:https?://)?(?:www\.)?youtube\.com/embed/([a-zA-Z0-9_-]{11})'),
|
|
31
|
+
re.compile(r'(?:https?://)?m\.youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})'),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
_URL_PATTERN = re.compile(r'https?://[^\s<>"\')\]]+')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_youtube_ids(text: str) -> list[str]:
|
|
38
|
+
"""Extract unique YouTube video IDs from text."""
|
|
39
|
+
ids: list[str] = []
|
|
40
|
+
for pat in _YT_PATTERNS:
|
|
41
|
+
for m in pat.finditer(text):
|
|
42
|
+
vid = m.group(1)
|
|
43
|
+
if vid not in ids:
|
|
44
|
+
ids.append(vid)
|
|
45
|
+
return ids
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def extract_urls(text: str) -> list[str]:
|
|
49
|
+
"""Extract all URLs from text."""
|
|
50
|
+
return _URL_PATTERN.findall(text)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def classify_url(url: str) -> str:
|
|
54
|
+
"""Classify a URL type."""
|
|
55
|
+
lower = url.lower()
|
|
56
|
+
if 'youtube.com' in lower or 'youtu.be' in lower:
|
|
57
|
+
return 'youtube'
|
|
58
|
+
if 'docs.google.com' in lower or 'drive.google.com' in lower:
|
|
59
|
+
return 'google_doc'
|
|
60
|
+
return 'website'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── Material type classification ─────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
_ASSESSMENT_KEYWORDS = {'test', 'quiz', 'exam', 'assessment', 'midterm', 'final', 'regents'}
|
|
66
|
+
_HANDOUT_KEYWORDS = {'handout', 'worksheet', 'graphic organizer', 'organizer', 'guided notes'}
|
|
67
|
+
_UNIT_PLAN_KEYWORDS = {'unit plan', 'essential questions', 'enduring understandings', 'pacing'}
|
|
68
|
+
_LESSON_PLAN_KEYWORDS = {'lesson plan', 'objective', 'do now', 'exit ticket', 'swbat'}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def classify_material_type(
|
|
72
|
+
doc_type: str, text: str, filename: str, slide_count: int | None = None,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Classify a document into a material type using heuristics."""
|
|
75
|
+
lower_fn = filename.lower()
|
|
76
|
+
lower_text = text[:2000].lower()
|
|
77
|
+
|
|
78
|
+
if doc_type == 'pptx':
|
|
79
|
+
if slide_count and slide_count >= 8:
|
|
80
|
+
return 'slideshow'
|
|
81
|
+
if slide_count and slide_count <= 3:
|
|
82
|
+
return 'fragment'
|
|
83
|
+
return 'slideshow'
|
|
84
|
+
|
|
85
|
+
combined = lower_fn + " " + lower_text
|
|
86
|
+
|
|
87
|
+
if any(kw in combined for kw in _ASSESSMENT_KEYWORDS):
|
|
88
|
+
return 'assessment'
|
|
89
|
+
if any(kw in combined for kw in _HANDOUT_KEYWORDS):
|
|
90
|
+
return 'handout'
|
|
91
|
+
if any(kw in combined for kw in _UNIT_PLAN_KEYWORDS):
|
|
92
|
+
return 'unit_plan'
|
|
93
|
+
if any(kw in combined for kw in _LESSON_PLAN_KEYWORDS):
|
|
94
|
+
return 'lesson_plan'
|
|
95
|
+
if doc_type == 'docx':
|
|
96
|
+
return 'notes'
|
|
97
|
+
return 'unknown'
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── Extracted metadata dataclasses ───────────────────────────────────
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class ExtractedImage:
|
|
104
|
+
"""An image extracted from a teaching document."""
|
|
105
|
+
image_bytes: bytes
|
|
106
|
+
format: str # 'png', 'jpeg', 'gif'
|
|
107
|
+
width: int | None = None
|
|
108
|
+
height: int | None = None
|
|
109
|
+
alt_text: str = ''
|
|
110
|
+
context_text: str = ''
|
|
111
|
+
slide_number: int | None = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class ExtractedURL:
|
|
116
|
+
"""A URL found in a teaching document."""
|
|
117
|
+
url: str
|
|
118
|
+
link_type: str # 'youtube', 'website', 'google_doc'
|
|
119
|
+
context_text: str = ''
|
|
120
|
+
title_hint: str = ''
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class ExtractionResult:
|
|
125
|
+
"""Rich extraction result from a document."""
|
|
126
|
+
text: str
|
|
127
|
+
page_count: int | None = None
|
|
128
|
+
slide_count: int | None = None
|
|
129
|
+
images: list[ExtractedImage] = field(default_factory=list)
|
|
130
|
+
urls: list[ExtractedURL] = field(default_factory=list)
|
|
131
|
+
word_count: int = 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── Asset Registry ───────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
class AssetRegistry:
|
|
137
|
+
"""File-level asset registry for teacher's materials.
|
|
138
|
+
|
|
139
|
+
Tracks complete files (slideshows, handouts, assessments) with metadata
|
|
140
|
+
about embedded images, YouTube links, material type, and completeness.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self, db_path: Path | None = None):
|
|
144
|
+
self._db_path = db_path or _DEFAULT_DB
|
|
145
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
self._init_db()
|
|
147
|
+
|
|
148
|
+
def _init_db(self) -> None:
|
|
149
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
150
|
+
conn.executescript("""
|
|
151
|
+
CREATE TABLE IF NOT EXISTS assets (
|
|
152
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
153
|
+
teacher_id TEXT NOT NULL,
|
|
154
|
+
source_path TEXT NOT NULL,
|
|
155
|
+
filename TEXT NOT NULL,
|
|
156
|
+
doc_type TEXT NOT NULL,
|
|
157
|
+
material_type TEXT NOT NULL,
|
|
158
|
+
title TEXT NOT NULL,
|
|
159
|
+
topic_tags TEXT DEFAULT '[]',
|
|
160
|
+
grade_hint TEXT DEFAULT '',
|
|
161
|
+
slide_count INTEGER,
|
|
162
|
+
page_count INTEGER,
|
|
163
|
+
word_count INTEGER DEFAULT 0,
|
|
164
|
+
has_images INTEGER DEFAULT 0,
|
|
165
|
+
image_count INTEGER DEFAULT 0,
|
|
166
|
+
youtube_urls TEXT DEFAULT '[]',
|
|
167
|
+
external_urls TEXT DEFAULT '[]',
|
|
168
|
+
completeness TEXT DEFAULT 'unknown',
|
|
169
|
+
file_size_bytes INTEGER,
|
|
170
|
+
content_hash TEXT NOT NULL,
|
|
171
|
+
indexed_at TEXT NOT NULL,
|
|
172
|
+
UNIQUE(teacher_id, content_hash)
|
|
173
|
+
);
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_assets_teacher
|
|
175
|
+
ON assets(teacher_id);
|
|
176
|
+
CREATE INDEX IF NOT EXISTS idx_assets_material_type
|
|
177
|
+
ON assets(teacher_id, material_type);
|
|
178
|
+
|
|
179
|
+
CREATE TABLE IF NOT EXISTS asset_images (
|
|
180
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
181
|
+
asset_id INTEGER NOT NULL REFERENCES assets(id),
|
|
182
|
+
image_index INTEGER NOT NULL,
|
|
183
|
+
image_path TEXT NOT NULL,
|
|
184
|
+
image_format TEXT DEFAULT '',
|
|
185
|
+
width_px INTEGER,
|
|
186
|
+
height_px INTEGER,
|
|
187
|
+
alt_text TEXT DEFAULT '',
|
|
188
|
+
context_text TEXT DEFAULT '',
|
|
189
|
+
slide_number INTEGER,
|
|
190
|
+
UNIQUE(asset_id, image_index)
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
CREATE TABLE IF NOT EXISTS asset_links (
|
|
194
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
195
|
+
asset_id INTEGER NOT NULL REFERENCES assets(id),
|
|
196
|
+
url TEXT NOT NULL,
|
|
197
|
+
link_type TEXT NOT NULL,
|
|
198
|
+
context_text TEXT DEFAULT '',
|
|
199
|
+
title_hint TEXT DEFAULT '',
|
|
200
|
+
UNIQUE(asset_id, url)
|
|
201
|
+
);
|
|
202
|
+
""")
|
|
203
|
+
|
|
204
|
+
def register_asset(
|
|
205
|
+
self,
|
|
206
|
+
teacher_id: str,
|
|
207
|
+
source_path: str,
|
|
208
|
+
title: str,
|
|
209
|
+
doc_type: str,
|
|
210
|
+
text: str,
|
|
211
|
+
extraction: ExtractionResult | None = None,
|
|
212
|
+
) -> int | None:
|
|
213
|
+
"""Register a file as an asset. Returns asset ID or None if duplicate."""
|
|
214
|
+
content_hash = hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()[:32]
|
|
215
|
+
filename = Path(source_path).name
|
|
216
|
+
|
|
217
|
+
material_type = classify_material_type(
|
|
218
|
+
doc_type, text, filename,
|
|
219
|
+
slide_count=extraction.slide_count if extraction else None,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
word_count = len(text.split()) if text else 0
|
|
223
|
+
slide_count = extraction.slide_count if extraction else None
|
|
224
|
+
page_count = extraction.page_count if extraction else None
|
|
225
|
+
|
|
226
|
+
# Extract URLs from text
|
|
227
|
+
yt_ids = extract_youtube_ids(text)
|
|
228
|
+
youtube_urls = [f"https://youtube.com/watch?v={vid}" for vid in yt_ids]
|
|
229
|
+
all_urls = extract_urls(text)
|
|
230
|
+
external_urls = [u for u in all_urls if 'youtube' not in u.lower() and 'youtu.be' not in u.lower()]
|
|
231
|
+
|
|
232
|
+
# Add URLs from extraction result
|
|
233
|
+
if extraction:
|
|
234
|
+
for eu in extraction.urls:
|
|
235
|
+
if eu.link_type == 'youtube' and eu.url not in youtube_urls:
|
|
236
|
+
youtube_urls.append(eu.url)
|
|
237
|
+
elif eu.url not in external_urls:
|
|
238
|
+
external_urls.append(eu.url)
|
|
239
|
+
|
|
240
|
+
image_count = len(extraction.images) if extraction else 0
|
|
241
|
+
has_images = 1 if image_count > 0 else 0
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
file_size = Path(source_path).stat().st_size if Path(source_path).exists() else 0
|
|
245
|
+
except Exception:
|
|
246
|
+
file_size = 0
|
|
247
|
+
|
|
248
|
+
completeness = 'complete' if material_type in ('slideshow', 'assessment', 'handout') else 'unknown'
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
252
|
+
cursor = conn.execute(
|
|
253
|
+
"INSERT OR IGNORE INTO assets "
|
|
254
|
+
"(teacher_id, source_path, filename, doc_type, material_type, title, "
|
|
255
|
+
"word_count, slide_count, page_count, has_images, image_count, "
|
|
256
|
+
"youtube_urls, external_urls, completeness, file_size_bytes, "
|
|
257
|
+
"content_hash, indexed_at) "
|
|
258
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
259
|
+
(
|
|
260
|
+
teacher_id, source_path, filename, doc_type, material_type, title,
|
|
261
|
+
word_count, slide_count, page_count, has_images, image_count,
|
|
262
|
+
json.dumps(youtube_urls), json.dumps(external_urls),
|
|
263
|
+
completeness, file_size, content_hash,
|
|
264
|
+
datetime.now().isoformat(),
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
if cursor.rowcount == 0:
|
|
268
|
+
return None # duplicate
|
|
269
|
+
asset_id = cursor.lastrowid
|
|
270
|
+
|
|
271
|
+
# Store image references
|
|
272
|
+
if extraction and extraction.images:
|
|
273
|
+
cache_dir = self._db_path.parent.parent / "cache" / "extracted" / content_hash
|
|
274
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
for idx, img in enumerate(extraction.images):
|
|
276
|
+
ext = img.format.lower().replace('jpeg', 'jpg')
|
|
277
|
+
img_path = cache_dir / f"{idx}.{ext}"
|
|
278
|
+
try:
|
|
279
|
+
img_path.write_bytes(img.image_bytes)
|
|
280
|
+
conn.execute(
|
|
281
|
+
"INSERT OR IGNORE INTO asset_images "
|
|
282
|
+
"(asset_id, image_index, image_path, image_format, "
|
|
283
|
+
"width_px, height_px, alt_text, context_text, slide_number) "
|
|
284
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
285
|
+
(
|
|
286
|
+
asset_id, idx, str(img_path), img.format,
|
|
287
|
+
img.width, img.height, img.alt_text,
|
|
288
|
+
img.context_text, img.slide_number,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.debug("Failed to save image %d: %s", idx, e)
|
|
293
|
+
|
|
294
|
+
# Store link references
|
|
295
|
+
for yt_url in youtube_urls:
|
|
296
|
+
conn.execute(
|
|
297
|
+
"INSERT OR IGNORE INTO asset_links "
|
|
298
|
+
"(asset_id, url, link_type, context_text) VALUES (?, ?, ?, ?)",
|
|
299
|
+
(asset_id, yt_url, 'youtube', ''),
|
|
300
|
+
)
|
|
301
|
+
for ext_url in external_urls[:20]: # cap at 20 external URLs
|
|
302
|
+
conn.execute(
|
|
303
|
+
"INSERT OR IGNORE INTO asset_links "
|
|
304
|
+
"(asset_id, url, link_type, context_text) VALUES (?, ?, ?, ?)",
|
|
305
|
+
(asset_id, ext_url, classify_url(ext_url), ''),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return asset_id
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.debug("Asset registration failed: %s", e)
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
def search_assets(
|
|
314
|
+
self, teacher_id: str, query: str, top_k: int = 10,
|
|
315
|
+
) -> list[dict[str, Any]]:
|
|
316
|
+
"""Search assets by keyword matching on title, filename, and material type."""
|
|
317
|
+
keywords = [w.lower() for w in query.split() if len(w) > 2]
|
|
318
|
+
if not keywords:
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
322
|
+
conn.row_factory = sqlite3.Row
|
|
323
|
+
rows = conn.execute(
|
|
324
|
+
"SELECT * FROM assets WHERE teacher_id = ?", (teacher_id,),
|
|
325
|
+
).fetchall()
|
|
326
|
+
|
|
327
|
+
scored: list[tuple[float, dict]] = []
|
|
328
|
+
for row in rows:
|
|
329
|
+
title_lower = row["title"].lower()
|
|
330
|
+
fn_lower = row["filename"].lower()
|
|
331
|
+
combined = title_lower + " " + fn_lower
|
|
332
|
+
score = sum(1 for kw in keywords if kw in combined)
|
|
333
|
+
if score > 0:
|
|
334
|
+
asset = dict(row)
|
|
335
|
+
asset["youtube_urls"] = json.loads(asset["youtube_urls"])
|
|
336
|
+
asset["external_urls"] = json.loads(asset["external_urls"])
|
|
337
|
+
scored.append((score, asset))
|
|
338
|
+
|
|
339
|
+
scored.sort(key=lambda x: (-x[0], x[1].get("material_type", "")))
|
|
340
|
+
return [item[1] for item in scored[:top_k]]
|
|
341
|
+
|
|
342
|
+
def get_youtube_links(self, teacher_id: str, query: str, top_k: int = 5) -> list[dict]:
|
|
343
|
+
"""Search for YouTube links related to a topic."""
|
|
344
|
+
assets = self.search_assets(teacher_id, query, top_k=50)
|
|
345
|
+
links: list[dict] = []
|
|
346
|
+
seen: set[str] = set()
|
|
347
|
+
for asset in assets:
|
|
348
|
+
for yt_url in asset.get("youtube_urls", []):
|
|
349
|
+
if yt_url not in seen:
|
|
350
|
+
seen.add(yt_url)
|
|
351
|
+
links.append({
|
|
352
|
+
"url": yt_url,
|
|
353
|
+
"from_file": asset["title"],
|
|
354
|
+
"material_type": asset["material_type"],
|
|
355
|
+
})
|
|
356
|
+
if len(links) >= top_k:
|
|
357
|
+
return links
|
|
358
|
+
return links
|
|
359
|
+
|
|
360
|
+
def format_asset_summary(self, assets: list[dict], youtube_links: list[dict] | None = None) -> str:
|
|
361
|
+
"""Format a human-readable summary of found assets."""
|
|
362
|
+
if not assets and not youtube_links:
|
|
363
|
+
return ""
|
|
364
|
+
|
|
365
|
+
lines: list[str] = []
|
|
366
|
+
if assets:
|
|
367
|
+
lines.append("Teacher's Existing Materials on This Topic:")
|
|
368
|
+
for a in assets:
|
|
369
|
+
type_label = a["material_type"].replace("_", " ").title()
|
|
370
|
+
extras: list[str] = []
|
|
371
|
+
if a.get("slide_count"):
|
|
372
|
+
extras.append(f"{a['slide_count']} slides")
|
|
373
|
+
if a.get("image_count"):
|
|
374
|
+
extras.append(f"{a['image_count']} images")
|
|
375
|
+
yt_count = len(a.get("youtube_urls", []))
|
|
376
|
+
if yt_count:
|
|
377
|
+
extras.append(f"{yt_count} YouTube links")
|
|
378
|
+
extra_str = f" ({', '.join(extras)})" if extras else ""
|
|
379
|
+
lines.append(f" - [{type_label}] \"{a['title']}\"{extra_str}")
|
|
380
|
+
lines.append(f" File: {a['filename']}")
|
|
381
|
+
|
|
382
|
+
if youtube_links:
|
|
383
|
+
lines.append("\nYouTube Links Found in Your Files:")
|
|
384
|
+
for link in youtube_links:
|
|
385
|
+
lines.append(f" - {link['url']} (from \"{link['from_file']}\")")
|
|
386
|
+
|
|
387
|
+
lines.append(
|
|
388
|
+
"\nReference and build on these existing materials. "
|
|
389
|
+
"If the teacher has taught this topic before, extend their work."
|
|
390
|
+
)
|
|
391
|
+
return "\n".join(lines)
|
|
392
|
+
|
|
393
|
+
def stats(self, teacher_id: str) -> dict[str, int]:
|
|
394
|
+
"""Return asset counts."""
|
|
395
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
396
|
+
total = conn.execute(
|
|
397
|
+
"SELECT COUNT(*) FROM assets WHERE teacher_id = ?", (teacher_id,),
|
|
398
|
+
).fetchone()[0]
|
|
399
|
+
images = conn.execute(
|
|
400
|
+
"SELECT COUNT(*) FROM asset_images ai "
|
|
401
|
+
"JOIN assets a ON ai.asset_id = a.id WHERE a.teacher_id = ?",
|
|
402
|
+
(teacher_id,),
|
|
403
|
+
).fetchone()[0]
|
|
404
|
+
links = conn.execute(
|
|
405
|
+
"SELECT COUNT(*) FROM asset_links al "
|
|
406
|
+
"JOIN assets a ON al.asset_id = a.id WHERE a.teacher_id = ?",
|
|
407
|
+
(teacher_id,),
|
|
408
|
+
).fetchone()[0]
|
|
409
|
+
return {"asset_count": total, "image_count": images, "link_count": links}
|
|
@@ -133,6 +133,7 @@ def ingest(
|
|
|
133
133
|
out = save_persona(persona, _output_dir())
|
|
134
134
|
|
|
135
135
|
# Index documents into curriculum knowledge base for KB search
|
|
136
|
+
kb_msg = ""
|
|
136
137
|
try:
|
|
137
138
|
from clawed.agent_core.memory.curriculum_kb import CurriculumKB
|
|
138
139
|
kb = CurriculumKB()
|
|
@@ -152,19 +153,48 @@ def ingest(
|
|
|
152
153
|
f"{stats['chunk_count']} searchable sections"
|
|
153
154
|
)
|
|
154
155
|
except Exception:
|
|
155
|
-
|
|
156
|
+
pass
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
158
|
+
# Register assets (file-level metadata, images, YouTube links)
|
|
159
|
+
asset_msg = ""
|
|
160
|
+
try:
|
|
161
|
+
from clawed.asset_registry import AssetRegistry
|
|
162
|
+
registry = AssetRegistry()
|
|
163
|
+
asset_count = 0
|
|
164
|
+
for doc in documents:
|
|
165
|
+
doc_type_val = doc.doc_type.value if hasattr(doc.doc_type, "value") else str(doc.doc_type)
|
|
166
|
+
asset_id = registry.register_asset(
|
|
167
|
+
teacher_id="default",
|
|
168
|
+
source_path=doc.source_path or "",
|
|
169
|
+
title=doc.title,
|
|
170
|
+
doc_type=doc_type_val,
|
|
171
|
+
text=doc.content,
|
|
172
|
+
)
|
|
173
|
+
if asset_id:
|
|
174
|
+
asset_count += 1
|
|
175
|
+
stats = registry.stats("default")
|
|
176
|
+
parts = [f"{stats['asset_count']} files indexed"]
|
|
177
|
+
if stats['link_count']:
|
|
178
|
+
parts.append(f"{stats['link_count']} links catalogued")
|
|
179
|
+
if stats['image_count']:
|
|
180
|
+
parts.append(f"{stats['image_count']} images extracted")
|
|
181
|
+
asset_msg = f"[bold]Asset registry:[/bold] {', '.join(parts)}"
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
info_parts = [
|
|
186
|
+
f"[green]Persona saved to {out}[/green]\n",
|
|
187
|
+
f"[bold]Style:[/bold] {persona.teaching_style.value.replace('_', ' ').title()}",
|
|
188
|
+
f"[bold]Tone:[/bold] {persona.tone}",
|
|
189
|
+
f"[bold]Subject:[/bold] {persona.subject_area}",
|
|
190
|
+
f"[bold]Format:[/bold] {persona.preferred_lesson_format}",
|
|
191
|
+
]
|
|
192
|
+
if kb_msg:
|
|
193
|
+
info_parts.append(kb_msg)
|
|
194
|
+
if asset_msg:
|
|
195
|
+
info_parts.append(asset_msg)
|
|
196
|
+
|
|
197
|
+
console.print(Panel("\n".join(info_parts), title="Teacher Persona"))
|
|
168
198
|
|
|
169
199
|
|
|
170
200
|
# ── Transcribe command ───────────────────────────────────────────────────
|
|
@@ -270,8 +300,28 @@ def lesson(
|
|
|
270
300
|
)
|
|
271
301
|
lesson_num = 1
|
|
272
302
|
|
|
273
|
-
# ── Search
|
|
303
|
+
# ── Search for teacher's existing materials (assets + KB) ──────
|
|
274
304
|
kb_prompt_section = ""
|
|
305
|
+
|
|
306
|
+
# Asset-level search (files, YouTube links, images)
|
|
307
|
+
try:
|
|
308
|
+
from clawed.asset_registry import AssetRegistry
|
|
309
|
+
registry = AssetRegistry()
|
|
310
|
+
assets = registry.search_assets("default", topic, top_k=5)
|
|
311
|
+
yt_links = registry.get_youtube_links("default", topic, top_k=3)
|
|
312
|
+
if assets or yt_links:
|
|
313
|
+
asset_summary = registry.format_asset_summary(assets, yt_links)
|
|
314
|
+
kb_prompt_section = asset_summary
|
|
315
|
+
# Show teacher what was found
|
|
316
|
+
for a in assets:
|
|
317
|
+
type_label = a["material_type"].replace("_", " ").title()
|
|
318
|
+
console.print(f"[dim]Found [{type_label}] \"{a['title']}\"[/dim]")
|
|
319
|
+
for link in yt_links:
|
|
320
|
+
console.print(f"[dim]Found YouTube: {link['url']}[/dim]")
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
# KB chunk-level search (supplements asset search)
|
|
275
325
|
try:
|
|
276
326
|
from clawed.agent_core.memory.curriculum_kb import CurriculumKB
|
|
277
327
|
kb = CurriculumKB()
|
|
@@ -279,18 +329,21 @@ def lesson(
|
|
|
279
329
|
if kb_results:
|
|
280
330
|
kb_parts = [r for r in kb_results if r.get("similarity", 0) > 0.1]
|
|
281
331
|
if kb_parts:
|
|
282
|
-
|
|
283
|
-
"
|
|
284
|
-
|
|
285
|
-
"Reference and build on their existing work:\n\n"
|
|
286
|
-
+ "\n\n".join(
|
|
287
|
-
f"From \"{r['doc_title']}\":\n{r['chunk_text'][:500]}"
|
|
288
|
-
for r in kb_parts
|
|
289
|
-
)
|
|
290
|
-
+ "\n\nUse these materials as a foundation. Reference the teacher's existing "
|
|
291
|
-
"lessons, reuse their graphic organizer formats, build on their approach."
|
|
332
|
+
chunk_section = "\n\n".join(
|
|
333
|
+
f"From \"{r['doc_title']}\":\n{r['chunk_text'][:500]}"
|
|
334
|
+
for r in kb_parts
|
|
292
335
|
)
|
|
293
|
-
|
|
336
|
+
if kb_prompt_section:
|
|
337
|
+
kb_prompt_section += "\n\n" + chunk_section
|
|
338
|
+
else:
|
|
339
|
+
kb_prompt_section = (
|
|
340
|
+
"Teacher's Existing Materials on This Topic\n"
|
|
341
|
+
"The teacher has created content on this topic before. "
|
|
342
|
+
"Reference and build on their existing work:\n\n"
|
|
343
|
+
+ chunk_section
|
|
344
|
+
)
|
|
345
|
+
if not assets:
|
|
346
|
+
console.print(f"[dim]Found {len(kb_parts)} related materials in knowledge base[/dim]")
|
|
294
347
|
except Exception:
|
|
295
348
|
pass
|
|
296
349
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|