attune-ai 2.0.0__py3-none-any.whl
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.
- attune/__init__.py +358 -0
- attune/adaptive/__init__.py +13 -0
- attune/adaptive/task_complexity.py +127 -0
- attune/agent_monitoring.py +414 -0
- attune/cache/__init__.py +117 -0
- attune/cache/base.py +166 -0
- attune/cache/dependency_manager.py +256 -0
- attune/cache/hash_only.py +251 -0
- attune/cache/hybrid.py +457 -0
- attune/cache/storage.py +285 -0
- attune/cache_monitor.py +356 -0
- attune/cache_stats.py +298 -0
- attune/cli/__init__.py +152 -0
- attune/cli/__main__.py +12 -0
- attune/cli/commands/__init__.py +1 -0
- attune/cli/commands/batch.py +264 -0
- attune/cli/commands/cache.py +248 -0
- attune/cli/commands/help.py +331 -0
- attune/cli/commands/info.py +140 -0
- attune/cli/commands/inspect.py +436 -0
- attune/cli/commands/inspection.py +57 -0
- attune/cli/commands/memory.py +48 -0
- attune/cli/commands/metrics.py +92 -0
- attune/cli/commands/orchestrate.py +184 -0
- attune/cli/commands/patterns.py +207 -0
- attune/cli/commands/profiling.py +202 -0
- attune/cli/commands/provider.py +98 -0
- attune/cli/commands/routing.py +285 -0
- attune/cli/commands/setup.py +96 -0
- attune/cli/commands/status.py +235 -0
- attune/cli/commands/sync.py +166 -0
- attune/cli/commands/tier.py +121 -0
- attune/cli/commands/utilities.py +114 -0
- attune/cli/commands/workflow.py +579 -0
- attune/cli/core.py +32 -0
- attune/cli/parsers/__init__.py +68 -0
- attune/cli/parsers/batch.py +118 -0
- attune/cli/parsers/cache.py +65 -0
- attune/cli/parsers/help.py +41 -0
- attune/cli/parsers/info.py +26 -0
- attune/cli/parsers/inspect.py +66 -0
- attune/cli/parsers/metrics.py +42 -0
- attune/cli/parsers/orchestrate.py +61 -0
- attune/cli/parsers/patterns.py +54 -0
- attune/cli/parsers/provider.py +40 -0
- attune/cli/parsers/routing.py +110 -0
- attune/cli/parsers/setup.py +42 -0
- attune/cli/parsers/status.py +47 -0
- attune/cli/parsers/sync.py +31 -0
- attune/cli/parsers/tier.py +33 -0
- attune/cli/parsers/workflow.py +77 -0
- attune/cli/utils/__init__.py +1 -0
- attune/cli/utils/data.py +242 -0
- attune/cli/utils/helpers.py +68 -0
- attune/cli_legacy.py +3957 -0
- attune/cli_minimal.py +1159 -0
- attune/cli_router.py +437 -0
- attune/cli_unified.py +814 -0
- attune/config/__init__.py +66 -0
- attune/config/xml_config.py +286 -0
- attune/config.py +545 -0
- attune/coordination.py +870 -0
- attune/core.py +1511 -0
- attune/core_modules/__init__.py +15 -0
- attune/cost_tracker.py +626 -0
- attune/dashboard/__init__.py +41 -0
- attune/dashboard/app.py +512 -0
- attune/dashboard/simple_server.py +435 -0
- attune/dashboard/standalone_server.py +547 -0
- attune/discovery.py +306 -0
- attune/emergence.py +306 -0
- attune/exceptions.py +123 -0
- attune/feedback_loops.py +373 -0
- attune/hot_reload/README.md +473 -0
- attune/hot_reload/__init__.py +62 -0
- attune/hot_reload/config.py +83 -0
- attune/hot_reload/integration.py +229 -0
- attune/hot_reload/reloader.py +298 -0
- attune/hot_reload/watcher.py +183 -0
- attune/hot_reload/websocket.py +177 -0
- attune/levels.py +577 -0
- attune/leverage_points.py +441 -0
- attune/logging_config.py +261 -0
- attune/mcp/__init__.py +10 -0
- attune/mcp/server.py +506 -0
- attune/memory/__init__.py +237 -0
- attune/memory/claude_memory.py +469 -0
- attune/memory/config.py +224 -0
- attune/memory/control_panel.py +1290 -0
- attune/memory/control_panel_support.py +145 -0
- attune/memory/cross_session.py +845 -0
- attune/memory/edges.py +179 -0
- attune/memory/encryption.py +159 -0
- attune/memory/file_session.py +770 -0
- attune/memory/graph.py +570 -0
- attune/memory/long_term.py +913 -0
- attune/memory/long_term_types.py +99 -0
- attune/memory/mixins/__init__.py +25 -0
- attune/memory/mixins/backend_init_mixin.py +249 -0
- attune/memory/mixins/capabilities_mixin.py +208 -0
- attune/memory/mixins/handoff_mixin.py +208 -0
- attune/memory/mixins/lifecycle_mixin.py +49 -0
- attune/memory/mixins/long_term_mixin.py +352 -0
- attune/memory/mixins/promotion_mixin.py +109 -0
- attune/memory/mixins/short_term_mixin.py +182 -0
- attune/memory/nodes.py +179 -0
- attune/memory/redis_bootstrap.py +540 -0
- attune/memory/security/__init__.py +31 -0
- attune/memory/security/audit_logger.py +932 -0
- attune/memory/security/pii_scrubber.py +640 -0
- attune/memory/security/secrets_detector.py +678 -0
- attune/memory/short_term.py +2192 -0
- attune/memory/simple_storage.py +302 -0
- attune/memory/storage/__init__.py +15 -0
- attune/memory/storage_backend.py +167 -0
- attune/memory/summary_index.py +583 -0
- attune/memory/types.py +446 -0
- attune/memory/unified.py +182 -0
- attune/meta_workflows/__init__.py +74 -0
- attune/meta_workflows/agent_creator.py +248 -0
- attune/meta_workflows/builtin_templates.py +567 -0
- attune/meta_workflows/cli_commands/__init__.py +56 -0
- attune/meta_workflows/cli_commands/agent_commands.py +321 -0
- attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
- attune/meta_workflows/cli_commands/config_commands.py +232 -0
- attune/meta_workflows/cli_commands/memory_commands.py +182 -0
- attune/meta_workflows/cli_commands/template_commands.py +354 -0
- attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
- attune/meta_workflows/cli_meta_workflows.py +59 -0
- attune/meta_workflows/form_engine.py +292 -0
- attune/meta_workflows/intent_detector.py +409 -0
- attune/meta_workflows/models.py +569 -0
- attune/meta_workflows/pattern_learner.py +738 -0
- attune/meta_workflows/plan_generator.py +384 -0
- attune/meta_workflows/session_context.py +397 -0
- attune/meta_workflows/template_registry.py +229 -0
- attune/meta_workflows/workflow.py +984 -0
- attune/metrics/__init__.py +12 -0
- attune/metrics/collector.py +31 -0
- attune/metrics/prompt_metrics.py +194 -0
- attune/models/__init__.py +172 -0
- attune/models/__main__.py +13 -0
- attune/models/adaptive_routing.py +437 -0
- attune/models/auth_cli.py +444 -0
- attune/models/auth_strategy.py +450 -0
- attune/models/cli.py +655 -0
- attune/models/empathy_executor.py +354 -0
- attune/models/executor.py +257 -0
- attune/models/fallback.py +762 -0
- attune/models/provider_config.py +282 -0
- attune/models/registry.py +472 -0
- attune/models/tasks.py +359 -0
- attune/models/telemetry/__init__.py +71 -0
- attune/models/telemetry/analytics.py +594 -0
- attune/models/telemetry/backend.py +196 -0
- attune/models/telemetry/data_models.py +431 -0
- attune/models/telemetry/storage.py +489 -0
- attune/models/token_estimator.py +420 -0
- attune/models/validation.py +280 -0
- attune/monitoring/__init__.py +52 -0
- attune/monitoring/alerts.py +946 -0
- attune/monitoring/alerts_cli.py +448 -0
- attune/monitoring/multi_backend.py +271 -0
- attune/monitoring/otel_backend.py +362 -0
- attune/optimization/__init__.py +19 -0
- attune/optimization/context_optimizer.py +272 -0
- attune/orchestration/__init__.py +67 -0
- attune/orchestration/agent_templates.py +707 -0
- attune/orchestration/config_store.py +499 -0
- attune/orchestration/execution_strategies.py +2111 -0
- attune/orchestration/meta_orchestrator.py +1168 -0
- attune/orchestration/pattern_learner.py +696 -0
- attune/orchestration/real_tools.py +931 -0
- attune/pattern_cache.py +187 -0
- attune/pattern_library.py +542 -0
- attune/patterns/debugging/all_patterns.json +81 -0
- attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
- attune/patterns/refactoring_memory.json +89 -0
- attune/persistence.py +564 -0
- attune/platform_utils.py +265 -0
- attune/plugins/__init__.py +28 -0
- attune/plugins/base.py +361 -0
- attune/plugins/registry.py +268 -0
- attune/project_index/__init__.py +32 -0
- attune/project_index/cli.py +335 -0
- attune/project_index/index.py +667 -0
- attune/project_index/models.py +504 -0
- attune/project_index/reports.py +474 -0
- attune/project_index/scanner.py +777 -0
- attune/project_index/scanner_parallel.py +291 -0
- attune/prompts/__init__.py +61 -0
- attune/prompts/config.py +77 -0
- attune/prompts/context.py +177 -0
- attune/prompts/parser.py +285 -0
- attune/prompts/registry.py +313 -0
- attune/prompts/templates.py +208 -0
- attune/redis_config.py +302 -0
- attune/redis_memory.py +799 -0
- attune/resilience/__init__.py +56 -0
- attune/resilience/circuit_breaker.py +256 -0
- attune/resilience/fallback.py +179 -0
- attune/resilience/health.py +300 -0
- attune/resilience/retry.py +209 -0
- attune/resilience/timeout.py +135 -0
- attune/routing/__init__.py +43 -0
- attune/routing/chain_executor.py +433 -0
- attune/routing/classifier.py +217 -0
- attune/routing/smart_router.py +234 -0
- attune/routing/workflow_registry.py +343 -0
- attune/scaffolding/README.md +589 -0
- attune/scaffolding/__init__.py +35 -0
- attune/scaffolding/__main__.py +14 -0
- attune/scaffolding/cli.py +240 -0
- attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
- attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
- attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
- attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
- attune/socratic/__init__.py +256 -0
- attune/socratic/ab_testing.py +958 -0
- attune/socratic/blueprint.py +533 -0
- attune/socratic/cli.py +703 -0
- attune/socratic/collaboration.py +1114 -0
- attune/socratic/domain_templates.py +924 -0
- attune/socratic/embeddings.py +738 -0
- attune/socratic/engine.py +794 -0
- attune/socratic/explainer.py +682 -0
- attune/socratic/feedback.py +772 -0
- attune/socratic/forms.py +629 -0
- attune/socratic/generator.py +732 -0
- attune/socratic/llm_analyzer.py +637 -0
- attune/socratic/mcp_server.py +702 -0
- attune/socratic/session.py +312 -0
- attune/socratic/storage.py +667 -0
- attune/socratic/success.py +730 -0
- attune/socratic/visual_editor.py +860 -0
- attune/socratic/web_ui.py +958 -0
- attune/telemetry/__init__.py +39 -0
- attune/telemetry/agent_coordination.py +475 -0
- attune/telemetry/agent_tracking.py +367 -0
- attune/telemetry/approval_gates.py +545 -0
- attune/telemetry/cli.py +1231 -0
- attune/telemetry/commands/__init__.py +14 -0
- attune/telemetry/commands/dashboard_commands.py +696 -0
- attune/telemetry/event_streaming.py +409 -0
- attune/telemetry/feedback_loop.py +567 -0
- attune/telemetry/usage_tracker.py +591 -0
- attune/templates.py +754 -0
- attune/test_generator/__init__.py +38 -0
- attune/test_generator/__main__.py +14 -0
- attune/test_generator/cli.py +234 -0
- attune/test_generator/generator.py +355 -0
- attune/test_generator/risk_analyzer.py +216 -0
- attune/test_generator/templates/unit_test.py.jinja2 +272 -0
- attune/tier_recommender.py +384 -0
- attune/tools.py +183 -0
- attune/trust/__init__.py +28 -0
- attune/trust/circuit_breaker.py +579 -0
- attune/trust_building.py +527 -0
- attune/validation/__init__.py +19 -0
- attune/validation/xml_validator.py +281 -0
- attune/vscode_bridge.py +173 -0
- attune/workflow_commands.py +780 -0
- attune/workflow_patterns/__init__.py +33 -0
- attune/workflow_patterns/behavior.py +249 -0
- attune/workflow_patterns/core.py +76 -0
- attune/workflow_patterns/output.py +99 -0
- attune/workflow_patterns/registry.py +255 -0
- attune/workflow_patterns/structural.py +288 -0
- attune/workflows/__init__.py +539 -0
- attune/workflows/autonomous_test_gen.py +1268 -0
- attune/workflows/base.py +2667 -0
- attune/workflows/batch_processing.py +342 -0
- attune/workflows/bug_predict.py +1084 -0
- attune/workflows/builder.py +273 -0
- attune/workflows/caching.py +253 -0
- attune/workflows/code_review.py +1048 -0
- attune/workflows/code_review_adapters.py +312 -0
- attune/workflows/code_review_pipeline.py +722 -0
- attune/workflows/config.py +645 -0
- attune/workflows/dependency_check.py +644 -0
- attune/workflows/document_gen/__init__.py +25 -0
- attune/workflows/document_gen/config.py +30 -0
- attune/workflows/document_gen/report_formatter.py +162 -0
- attune/workflows/document_gen/workflow.py +1426 -0
- attune/workflows/document_manager.py +216 -0
- attune/workflows/document_manager_README.md +134 -0
- attune/workflows/documentation_orchestrator.py +1205 -0
- attune/workflows/history.py +510 -0
- attune/workflows/keyboard_shortcuts/__init__.py +39 -0
- attune/workflows/keyboard_shortcuts/generators.py +391 -0
- attune/workflows/keyboard_shortcuts/parsers.py +416 -0
- attune/workflows/keyboard_shortcuts/prompts.py +295 -0
- attune/workflows/keyboard_shortcuts/schema.py +193 -0
- attune/workflows/keyboard_shortcuts/workflow.py +509 -0
- attune/workflows/llm_base.py +363 -0
- attune/workflows/manage_docs.py +87 -0
- attune/workflows/manage_docs_README.md +134 -0
- attune/workflows/manage_documentation.py +821 -0
- attune/workflows/new_sample_workflow1.py +149 -0
- attune/workflows/new_sample_workflow1_README.md +150 -0
- attune/workflows/orchestrated_health_check.py +849 -0
- attune/workflows/orchestrated_release_prep.py +600 -0
- attune/workflows/output.py +413 -0
- attune/workflows/perf_audit.py +863 -0
- attune/workflows/pr_review.py +762 -0
- attune/workflows/progress.py +785 -0
- attune/workflows/progress_server.py +322 -0
- attune/workflows/progressive/README 2.md +454 -0
- attune/workflows/progressive/README.md +454 -0
- attune/workflows/progressive/__init__.py +82 -0
- attune/workflows/progressive/cli.py +219 -0
- attune/workflows/progressive/core.py +488 -0
- attune/workflows/progressive/orchestrator.py +723 -0
- attune/workflows/progressive/reports.py +520 -0
- attune/workflows/progressive/telemetry.py +274 -0
- attune/workflows/progressive/test_gen.py +495 -0
- attune/workflows/progressive/workflow.py +589 -0
- attune/workflows/refactor_plan.py +694 -0
- attune/workflows/release_prep.py +895 -0
- attune/workflows/release_prep_crew.py +969 -0
- attune/workflows/research_synthesis.py +404 -0
- attune/workflows/routing.py +168 -0
- attune/workflows/secure_release.py +593 -0
- attune/workflows/security_adapters.py +297 -0
- attune/workflows/security_audit.py +1329 -0
- attune/workflows/security_audit_phase3.py +355 -0
- attune/workflows/seo_optimization.py +633 -0
- attune/workflows/step_config.py +234 -0
- attune/workflows/telemetry_mixin.py +269 -0
- attune/workflows/test5.py +125 -0
- attune/workflows/test5_README.md +158 -0
- attune/workflows/test_coverage_boost_crew.py +849 -0
- attune/workflows/test_gen/__init__.py +52 -0
- attune/workflows/test_gen/ast_analyzer.py +249 -0
- attune/workflows/test_gen/config.py +88 -0
- attune/workflows/test_gen/data_models.py +38 -0
- attune/workflows/test_gen/report_formatter.py +289 -0
- attune/workflows/test_gen/test_templates.py +381 -0
- attune/workflows/test_gen/workflow.py +655 -0
- attune/workflows/test_gen.py +54 -0
- attune/workflows/test_gen_behavioral.py +477 -0
- attune/workflows/test_gen_parallel.py +341 -0
- attune/workflows/test_lifecycle.py +526 -0
- attune/workflows/test_maintenance.py +627 -0
- attune/workflows/test_maintenance_cli.py +590 -0
- attune/workflows/test_maintenance_crew.py +840 -0
- attune/workflows/test_runner.py +622 -0
- attune/workflows/tier_tracking.py +531 -0
- attune/workflows/xml_enhanced_crew.py +285 -0
- attune_ai-2.0.0.dist-info/METADATA +1026 -0
- attune_ai-2.0.0.dist-info/RECORD +457 -0
- attune_ai-2.0.0.dist-info/WHEEL +5 -0
- attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
- attune_ai-2.0.0.dist-info/top_level.txt +5 -0
- attune_healthcare/__init__.py +13 -0
- attune_healthcare/monitors/__init__.py +9 -0
- attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
- attune_healthcare/monitors/monitoring/__init__.py +44 -0
- attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
- attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
- attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
- attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
- attune_llm/README.md +553 -0
- attune_llm/__init__.py +28 -0
- attune_llm/agent_factory/__init__.py +53 -0
- attune_llm/agent_factory/adapters/__init__.py +85 -0
- attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
- attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
- attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
- attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
- attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
- attune_llm/agent_factory/adapters/native.py +228 -0
- attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
- attune_llm/agent_factory/base.py +305 -0
- attune_llm/agent_factory/crews/__init__.py +67 -0
- attune_llm/agent_factory/crews/code_review.py +1113 -0
- attune_llm/agent_factory/crews/health_check.py +1262 -0
- attune_llm/agent_factory/crews/refactoring.py +1128 -0
- attune_llm/agent_factory/crews/security_audit.py +1018 -0
- attune_llm/agent_factory/decorators.py +287 -0
- attune_llm/agent_factory/factory.py +558 -0
- attune_llm/agent_factory/framework.py +193 -0
- attune_llm/agent_factory/memory_integration.py +328 -0
- attune_llm/agent_factory/resilient.py +320 -0
- attune_llm/agents_md/__init__.py +22 -0
- attune_llm/agents_md/loader.py +218 -0
- attune_llm/agents_md/parser.py +271 -0
- attune_llm/agents_md/registry.py +307 -0
- attune_llm/claude_memory.py +466 -0
- attune_llm/cli/__init__.py +8 -0
- attune_llm/cli/sync_claude.py +487 -0
- attune_llm/code_health.py +1313 -0
- attune_llm/commands/__init__.py +51 -0
- attune_llm/commands/context.py +375 -0
- attune_llm/commands/loader.py +301 -0
- attune_llm/commands/models.py +231 -0
- attune_llm/commands/parser.py +371 -0
- attune_llm/commands/registry.py +429 -0
- attune_llm/config/__init__.py +29 -0
- attune_llm/config/unified.py +291 -0
- attune_llm/context/__init__.py +22 -0
- attune_llm/context/compaction.py +455 -0
- attune_llm/context/manager.py +434 -0
- attune_llm/contextual_patterns.py +361 -0
- attune_llm/core.py +907 -0
- attune_llm/git_pattern_extractor.py +435 -0
- attune_llm/hooks/__init__.py +24 -0
- attune_llm/hooks/config.py +306 -0
- attune_llm/hooks/executor.py +289 -0
- attune_llm/hooks/registry.py +302 -0
- attune_llm/hooks/scripts/__init__.py +39 -0
- attune_llm/hooks/scripts/evaluate_session.py +201 -0
- attune_llm/hooks/scripts/first_time_init.py +285 -0
- attune_llm/hooks/scripts/pre_compact.py +207 -0
- attune_llm/hooks/scripts/session_end.py +183 -0
- attune_llm/hooks/scripts/session_start.py +163 -0
- attune_llm/hooks/scripts/suggest_compact.py +225 -0
- attune_llm/learning/__init__.py +30 -0
- attune_llm/learning/evaluator.py +438 -0
- attune_llm/learning/extractor.py +514 -0
- attune_llm/learning/storage.py +560 -0
- attune_llm/levels.py +227 -0
- attune_llm/pattern_confidence.py +414 -0
- attune_llm/pattern_resolver.py +272 -0
- attune_llm/pattern_summary.py +350 -0
- attune_llm/providers.py +967 -0
- attune_llm/routing/__init__.py +32 -0
- attune_llm/routing/model_router.py +362 -0
- attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
- attune_llm/security/PHASE2_COMPLETE.md +384 -0
- attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- attune_llm/security/QUICK_REFERENCE.md +316 -0
- attune_llm/security/README.md +262 -0
- attune_llm/security/__init__.py +62 -0
- attune_llm/security/audit_logger.py +929 -0
- attune_llm/security/audit_logger_example.py +152 -0
- attune_llm/security/pii_scrubber.py +640 -0
- attune_llm/security/secrets_detector.py +678 -0
- attune_llm/security/secrets_detector_example.py +304 -0
- attune_llm/security/secure_memdocs.py +1192 -0
- attune_llm/security/secure_memdocs_example.py +278 -0
- attune_llm/session_status.py +745 -0
- attune_llm/state.py +246 -0
- attune_llm/utils/__init__.py +5 -0
- attune_llm/utils/tokens.py +349 -0
- attune_software/SOFTWARE_PLUGIN_README.md +57 -0
- attune_software/__init__.py +13 -0
- attune_software/cli/__init__.py +120 -0
- attune_software/cli/inspect.py +362 -0
- attune_software/cli.py +574 -0
- attune_software/plugin.py +188 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"""Learned Skills Storage for Continuous Learning
|
|
2
|
+
|
|
3
|
+
Persists and retrieves learned patterns and skills.
|
|
4
|
+
Provides storage for patterns extracted from sessions.
|
|
5
|
+
|
|
6
|
+
Architectural patterns inspired by everything-claude-code by Affaan Mustafa.
|
|
7
|
+
See: https://github.com/affaan-m/everything-claude-code (MIT License)
|
|
8
|
+
See: ACKNOWLEDGMENTS.md for full attribution.
|
|
9
|
+
|
|
10
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
11
|
+
Licensed under Fair Source 0.9
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from attune_llm.learning.extractor import ExtractedPattern, PatternCategory
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LearnedSkill:
|
|
30
|
+
"""A skill learned from pattern aggregation.
|
|
31
|
+
|
|
32
|
+
Skills are higher-level learnings derived from
|
|
33
|
+
multiple related patterns.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
skill_id: str
|
|
37
|
+
name: str
|
|
38
|
+
description: str
|
|
39
|
+
category: PatternCategory
|
|
40
|
+
patterns: list[str] # Pattern IDs
|
|
41
|
+
confidence: float
|
|
42
|
+
usage_count: int = 0
|
|
43
|
+
last_used: datetime | None = None
|
|
44
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
45
|
+
tags: list[str] = field(default_factory=list)
|
|
46
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, Any]:
|
|
49
|
+
"""Convert to dictionary."""
|
|
50
|
+
return {
|
|
51
|
+
"skill_id": self.skill_id,
|
|
52
|
+
"name": self.name,
|
|
53
|
+
"description": self.description,
|
|
54
|
+
"category": self.category.value,
|
|
55
|
+
"patterns": self.patterns,
|
|
56
|
+
"confidence": self.confidence,
|
|
57
|
+
"usage_count": self.usage_count,
|
|
58
|
+
"last_used": self.last_used.isoformat() if self.last_used else None,
|
|
59
|
+
"created_at": self.created_at.isoformat(),
|
|
60
|
+
"tags": self.tags,
|
|
61
|
+
"metadata": self.metadata,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_dict(cls, data: dict[str, Any]) -> LearnedSkill:
|
|
66
|
+
"""Create from dictionary."""
|
|
67
|
+
return cls(
|
|
68
|
+
skill_id=data["skill_id"],
|
|
69
|
+
name=data["name"],
|
|
70
|
+
description=data["description"],
|
|
71
|
+
category=PatternCategory(data["category"]),
|
|
72
|
+
patterns=data.get("patterns", []),
|
|
73
|
+
confidence=data.get("confidence", 0.5),
|
|
74
|
+
usage_count=data.get("usage_count", 0),
|
|
75
|
+
last_used=datetime.fromisoformat(data["last_used"]) if data.get("last_used") else None,
|
|
76
|
+
created_at=datetime.fromisoformat(data["created_at"])
|
|
77
|
+
if "created_at" in data
|
|
78
|
+
else datetime.now(),
|
|
79
|
+
tags=data.get("tags", []),
|
|
80
|
+
metadata=data.get("metadata", {}),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class LearnedSkillsStorage:
|
|
85
|
+
"""Manages storage of learned patterns and skills.
|
|
86
|
+
|
|
87
|
+
Provides persistence and retrieval for patterns extracted
|
|
88
|
+
from collaboration sessions, with support for querying
|
|
89
|
+
and filtering.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
storage_dir: str | Path = ".attune/learned_skills",
|
|
95
|
+
max_patterns_per_user: int = 100,
|
|
96
|
+
max_skills_per_user: int = 50,
|
|
97
|
+
):
|
|
98
|
+
"""Initialize the storage.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
storage_dir: Directory for storage files
|
|
102
|
+
max_patterns_per_user: Maximum patterns to store per user
|
|
103
|
+
max_skills_per_user: Maximum skills to store per user
|
|
104
|
+
"""
|
|
105
|
+
self.storage_dir = Path(storage_dir)
|
|
106
|
+
self._max_patterns = max_patterns_per_user
|
|
107
|
+
self._max_skills = max_skills_per_user
|
|
108
|
+
|
|
109
|
+
def _ensure_storage(self) -> None:
|
|
110
|
+
"""Ensure storage directory exists."""
|
|
111
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
|
|
113
|
+
def _get_user_dir(self, user_id: str) -> Path:
|
|
114
|
+
"""Get storage directory for a user."""
|
|
115
|
+
safe_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in user_id)
|
|
116
|
+
return self.storage_dir / safe_id
|
|
117
|
+
|
|
118
|
+
def _get_patterns_file(self, user_id: str) -> Path:
|
|
119
|
+
"""Get patterns file path for a user."""
|
|
120
|
+
return self._get_user_dir(user_id) / "patterns.json"
|
|
121
|
+
|
|
122
|
+
def _get_skills_file(self, user_id: str) -> Path:
|
|
123
|
+
"""Get skills file path for a user."""
|
|
124
|
+
return self._get_user_dir(user_id) / "skills.json"
|
|
125
|
+
|
|
126
|
+
# Pattern operations
|
|
127
|
+
|
|
128
|
+
def save_pattern(
|
|
129
|
+
self,
|
|
130
|
+
user_id: str,
|
|
131
|
+
pattern: ExtractedPattern,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Save a pattern for a user.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
user_id: User identifier
|
|
137
|
+
pattern: Pattern to save
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Pattern ID
|
|
141
|
+
"""
|
|
142
|
+
self._ensure_storage()
|
|
143
|
+
user_dir = self._get_user_dir(user_id)
|
|
144
|
+
user_dir.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
|
|
146
|
+
patterns = self._load_patterns(user_id)
|
|
147
|
+
|
|
148
|
+
# Check for duplicate
|
|
149
|
+
existing_ids = {p["pattern_id"] for p in patterns}
|
|
150
|
+
if pattern.pattern_id in existing_ids:
|
|
151
|
+
logger.debug(f"Pattern {pattern.pattern_id} already exists, updating")
|
|
152
|
+
patterns = [p for p in patterns if p["pattern_id"] != pattern.pattern_id]
|
|
153
|
+
|
|
154
|
+
patterns.append(pattern.to_dict())
|
|
155
|
+
|
|
156
|
+
# Enforce limit (remove oldest)
|
|
157
|
+
if len(patterns) > self._max_patterns:
|
|
158
|
+
patterns = sorted(
|
|
159
|
+
patterns,
|
|
160
|
+
key=lambda p: p.get("extracted_at", ""),
|
|
161
|
+
reverse=True,
|
|
162
|
+
)[: self._max_patterns]
|
|
163
|
+
|
|
164
|
+
self._save_patterns(user_id, patterns)
|
|
165
|
+
logger.info(f"Saved pattern {pattern.pattern_id} for user {user_id}")
|
|
166
|
+
|
|
167
|
+
return pattern.pattern_id
|
|
168
|
+
|
|
169
|
+
def save_patterns(
|
|
170
|
+
self,
|
|
171
|
+
user_id: str,
|
|
172
|
+
patterns: list[ExtractedPattern],
|
|
173
|
+
) -> list[str]:
|
|
174
|
+
"""Save multiple patterns.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
user_id: User identifier
|
|
178
|
+
patterns: Patterns to save
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of saved pattern IDs
|
|
182
|
+
"""
|
|
183
|
+
return [self.save_pattern(user_id, p) for p in patterns]
|
|
184
|
+
|
|
185
|
+
def get_pattern(
|
|
186
|
+
self,
|
|
187
|
+
user_id: str,
|
|
188
|
+
pattern_id: str,
|
|
189
|
+
) -> ExtractedPattern | None:
|
|
190
|
+
"""Get a specific pattern.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
user_id: User identifier
|
|
194
|
+
pattern_id: Pattern identifier
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Pattern or None if not found
|
|
198
|
+
"""
|
|
199
|
+
patterns = self._load_patterns(user_id)
|
|
200
|
+
|
|
201
|
+
for p in patterns:
|
|
202
|
+
if p.get("pattern_id") == pattern_id:
|
|
203
|
+
return ExtractedPattern.from_dict(p)
|
|
204
|
+
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def get_all_patterns(self, user_id: str) -> list[ExtractedPattern]:
|
|
208
|
+
"""Get all patterns for a user.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
user_id: User identifier
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of patterns
|
|
215
|
+
"""
|
|
216
|
+
patterns = self._load_patterns(user_id)
|
|
217
|
+
return [ExtractedPattern.from_dict(p) for p in patterns]
|
|
218
|
+
|
|
219
|
+
def get_patterns_by_category(
|
|
220
|
+
self,
|
|
221
|
+
user_id: str,
|
|
222
|
+
category: PatternCategory,
|
|
223
|
+
) -> list[ExtractedPattern]:
|
|
224
|
+
"""Get patterns by category.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
user_id: User identifier
|
|
228
|
+
category: Pattern category
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List of matching patterns
|
|
232
|
+
"""
|
|
233
|
+
all_patterns = self.get_all_patterns(user_id)
|
|
234
|
+
return [p for p in all_patterns if p.category == category]
|
|
235
|
+
|
|
236
|
+
def get_patterns_by_tag(
|
|
237
|
+
self,
|
|
238
|
+
user_id: str,
|
|
239
|
+
tag: str,
|
|
240
|
+
) -> list[ExtractedPattern]:
|
|
241
|
+
"""Get patterns by tag.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
user_id: User identifier
|
|
245
|
+
tag: Tag to filter by
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of matching patterns
|
|
249
|
+
"""
|
|
250
|
+
all_patterns = self.get_all_patterns(user_id)
|
|
251
|
+
return [p for p in all_patterns if tag in p.tags]
|
|
252
|
+
|
|
253
|
+
def search_patterns(
|
|
254
|
+
self,
|
|
255
|
+
user_id: str,
|
|
256
|
+
query: str,
|
|
257
|
+
) -> list[ExtractedPattern]:
|
|
258
|
+
"""Search patterns by trigger or context.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
user_id: User identifier
|
|
262
|
+
query: Search query
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
List of matching patterns
|
|
266
|
+
"""
|
|
267
|
+
all_patterns = self.get_all_patterns(user_id)
|
|
268
|
+
query_lower = query.lower()
|
|
269
|
+
|
|
270
|
+
return [
|
|
271
|
+
p
|
|
272
|
+
for p in all_patterns
|
|
273
|
+
if query_lower in p.trigger.lower()
|
|
274
|
+
or query_lower in p.context.lower()
|
|
275
|
+
or query_lower in p.resolution.lower()
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
def delete_pattern(self, user_id: str, pattern_id: str) -> bool:
|
|
279
|
+
"""Delete a pattern.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
user_id: User identifier
|
|
283
|
+
pattern_id: Pattern to delete
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
True if deleted
|
|
287
|
+
"""
|
|
288
|
+
patterns = self._load_patterns(user_id)
|
|
289
|
+
original_count = len(patterns)
|
|
290
|
+
|
|
291
|
+
patterns = [p for p in patterns if p.get("pattern_id") != pattern_id]
|
|
292
|
+
|
|
293
|
+
if len(patterns) < original_count:
|
|
294
|
+
self._save_patterns(user_id, patterns)
|
|
295
|
+
return True
|
|
296
|
+
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
def _load_patterns(self, user_id: str) -> list[dict[str, Any]]:
|
|
300
|
+
"""Load patterns from storage."""
|
|
301
|
+
patterns_file = self._get_patterns_file(user_id)
|
|
302
|
+
|
|
303
|
+
if not patterns_file.exists():
|
|
304
|
+
return []
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
with open(patterns_file, encoding="utf-8") as f:
|
|
308
|
+
return json.load(f)
|
|
309
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
310
|
+
logger.error(f"Failed to load patterns: {e}")
|
|
311
|
+
return []
|
|
312
|
+
|
|
313
|
+
def _save_patterns(
|
|
314
|
+
self,
|
|
315
|
+
user_id: str,
|
|
316
|
+
patterns: list[dict[str, Any]],
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Save patterns to storage."""
|
|
319
|
+
patterns_file = self._get_patterns_file(user_id)
|
|
320
|
+
|
|
321
|
+
with open(patterns_file, "w", encoding="utf-8") as f:
|
|
322
|
+
json.dump(patterns, f, indent=2, default=str)
|
|
323
|
+
|
|
324
|
+
# Skill operations
|
|
325
|
+
|
|
326
|
+
def save_skill(
|
|
327
|
+
self,
|
|
328
|
+
user_id: str,
|
|
329
|
+
skill: LearnedSkill,
|
|
330
|
+
) -> str:
|
|
331
|
+
"""Save a learned skill.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
user_id: User identifier
|
|
335
|
+
skill: Skill to save
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Skill ID
|
|
339
|
+
"""
|
|
340
|
+
self._ensure_storage()
|
|
341
|
+
user_dir = self._get_user_dir(user_id)
|
|
342
|
+
user_dir.mkdir(parents=True, exist_ok=True)
|
|
343
|
+
|
|
344
|
+
skills = self._load_skills(user_id)
|
|
345
|
+
|
|
346
|
+
# Check for duplicate
|
|
347
|
+
existing_ids = {s["skill_id"] for s in skills}
|
|
348
|
+
if skill.skill_id in existing_ids:
|
|
349
|
+
skills = [s for s in skills if s["skill_id"] != skill.skill_id]
|
|
350
|
+
|
|
351
|
+
skills.append(skill.to_dict())
|
|
352
|
+
|
|
353
|
+
# Enforce limit
|
|
354
|
+
if len(skills) > self._max_skills:
|
|
355
|
+
skills = sorted(
|
|
356
|
+
skills,
|
|
357
|
+
key=lambda s: s.get("created_at", ""),
|
|
358
|
+
reverse=True,
|
|
359
|
+
)[: self._max_skills]
|
|
360
|
+
|
|
361
|
+
self._save_skills(user_id, skills)
|
|
362
|
+
return skill.skill_id
|
|
363
|
+
|
|
364
|
+
def get_skill(
|
|
365
|
+
self,
|
|
366
|
+
user_id: str,
|
|
367
|
+
skill_id: str,
|
|
368
|
+
) -> LearnedSkill | None:
|
|
369
|
+
"""Get a specific skill.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
user_id: User identifier
|
|
373
|
+
skill_id: Skill identifier
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Skill or None
|
|
377
|
+
"""
|
|
378
|
+
skills = self._load_skills(user_id)
|
|
379
|
+
|
|
380
|
+
for s in skills:
|
|
381
|
+
if s.get("skill_id") == skill_id:
|
|
382
|
+
return LearnedSkill.from_dict(s)
|
|
383
|
+
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
def get_all_skills(self, user_id: str) -> list[LearnedSkill]:
|
|
387
|
+
"""Get all skills for a user.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
user_id: User identifier
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
List of skills
|
|
394
|
+
"""
|
|
395
|
+
skills = self._load_skills(user_id)
|
|
396
|
+
return [LearnedSkill.from_dict(s) for s in skills]
|
|
397
|
+
|
|
398
|
+
def record_skill_usage(
|
|
399
|
+
self,
|
|
400
|
+
user_id: str,
|
|
401
|
+
skill_id: str,
|
|
402
|
+
) -> None:
|
|
403
|
+
"""Record that a skill was used.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
user_id: User identifier
|
|
407
|
+
skill_id: Skill that was used
|
|
408
|
+
"""
|
|
409
|
+
skills = self._load_skills(user_id)
|
|
410
|
+
|
|
411
|
+
for s in skills:
|
|
412
|
+
if s.get("skill_id") == skill_id:
|
|
413
|
+
s["usage_count"] = s.get("usage_count", 0) + 1
|
|
414
|
+
s["last_used"] = datetime.now().isoformat()
|
|
415
|
+
break
|
|
416
|
+
|
|
417
|
+
self._save_skills(user_id, skills)
|
|
418
|
+
|
|
419
|
+
def delete_skill(self, user_id: str, skill_id: str) -> bool:
|
|
420
|
+
"""Delete a skill.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
user_id: User identifier
|
|
424
|
+
skill_id: Skill to delete
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
True if deleted
|
|
428
|
+
"""
|
|
429
|
+
skills = self._load_skills(user_id)
|
|
430
|
+
original_count = len(skills)
|
|
431
|
+
|
|
432
|
+
skills = [s for s in skills if s.get("skill_id") != skill_id]
|
|
433
|
+
|
|
434
|
+
if len(skills) < original_count:
|
|
435
|
+
self._save_skills(user_id, skills)
|
|
436
|
+
return True
|
|
437
|
+
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
def _load_skills(self, user_id: str) -> list[dict[str, Any]]:
|
|
441
|
+
"""Load skills from storage."""
|
|
442
|
+
skills_file = self._get_skills_file(user_id)
|
|
443
|
+
|
|
444
|
+
if not skills_file.exists():
|
|
445
|
+
return []
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
with open(skills_file, encoding="utf-8") as f:
|
|
449
|
+
return json.load(f)
|
|
450
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
451
|
+
logger.error(f"Failed to load skills: {e}")
|
|
452
|
+
return []
|
|
453
|
+
|
|
454
|
+
def _save_skills(
|
|
455
|
+
self,
|
|
456
|
+
user_id: str,
|
|
457
|
+
skills: list[dict[str, Any]],
|
|
458
|
+
) -> None:
|
|
459
|
+
"""Save skills to storage."""
|
|
460
|
+
skills_file = self._get_skills_file(user_id)
|
|
461
|
+
|
|
462
|
+
with open(skills_file, "w", encoding="utf-8") as f:
|
|
463
|
+
json.dump(skills, f, indent=2, default=str)
|
|
464
|
+
|
|
465
|
+
# Summary operations
|
|
466
|
+
|
|
467
|
+
def get_summary(self, user_id: str) -> dict[str, Any]:
|
|
468
|
+
"""Get learning summary for a user.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
user_id: User identifier
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Summary dictionary
|
|
475
|
+
"""
|
|
476
|
+
patterns = self.get_all_patterns(user_id)
|
|
477
|
+
skills = self.get_all_skills(user_id)
|
|
478
|
+
|
|
479
|
+
# Count by category
|
|
480
|
+
category_counts: dict[str, int] = {}
|
|
481
|
+
for pattern in patterns:
|
|
482
|
+
cat = pattern.category.value
|
|
483
|
+
category_counts[cat] = category_counts.get(cat, 0) + 1
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
"user_id": user_id,
|
|
487
|
+
"total_patterns": len(patterns),
|
|
488
|
+
"total_skills": len(skills),
|
|
489
|
+
"patterns_by_category": category_counts,
|
|
490
|
+
"avg_confidence": (
|
|
491
|
+
sum(p.confidence for p in patterns) / len(patterns) if patterns else 0.0
|
|
492
|
+
),
|
|
493
|
+
"most_used_skill": (max(skills, key=lambda s: s.usage_count).name if skills else None),
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
def clear_user_data(self, user_id: str) -> int:
|
|
497
|
+
"""Clear all data for a user.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
user_id: User identifier
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
Number of items cleared
|
|
504
|
+
"""
|
|
505
|
+
user_dir = self._get_user_dir(user_id)
|
|
506
|
+
count = 0
|
|
507
|
+
|
|
508
|
+
if user_dir.exists():
|
|
509
|
+
for file in user_dir.glob("*.json"):
|
|
510
|
+
try:
|
|
511
|
+
# Count items before deleting
|
|
512
|
+
with open(file, encoding="utf-8") as f:
|
|
513
|
+
data = json.load(f)
|
|
514
|
+
count += len(data) if isinstance(data, list) else 1
|
|
515
|
+
file.unlink()
|
|
516
|
+
except (OSError, json.JSONDecodeError):
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
user_dir.rmdir()
|
|
521
|
+
except OSError:
|
|
522
|
+
pass
|
|
523
|
+
|
|
524
|
+
return count
|
|
525
|
+
|
|
526
|
+
def format_patterns_for_context(
|
|
527
|
+
self,
|
|
528
|
+
user_id: str,
|
|
529
|
+
max_patterns: int = 5,
|
|
530
|
+
categories: list[PatternCategory] | None = None,
|
|
531
|
+
) -> str:
|
|
532
|
+
"""Format patterns for injection into context.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
user_id: User identifier
|
|
536
|
+
max_patterns: Maximum patterns to include
|
|
537
|
+
categories: Optional category filter
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
Formatted markdown string
|
|
541
|
+
"""
|
|
542
|
+
patterns = self.get_all_patterns(user_id)
|
|
543
|
+
|
|
544
|
+
if categories:
|
|
545
|
+
patterns = [p for p in patterns if p.category in categories]
|
|
546
|
+
|
|
547
|
+
# Sort by confidence
|
|
548
|
+
patterns = sorted(patterns, key=lambda p: p.confidence, reverse=True)
|
|
549
|
+
patterns = patterns[:max_patterns]
|
|
550
|
+
|
|
551
|
+
if not patterns:
|
|
552
|
+
return ""
|
|
553
|
+
|
|
554
|
+
lines = ["## Learned Patterns", ""]
|
|
555
|
+
|
|
556
|
+
for pattern in patterns:
|
|
557
|
+
lines.append(pattern.format_readable())
|
|
558
|
+
lines.append("")
|
|
559
|
+
|
|
560
|
+
return "\n".join(lines)
|