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,770 @@
|
|
|
1
|
+
"""File-Based Session Memory for Empathy Framework.
|
|
2
|
+
|
|
3
|
+
Provides persistent session storage without requiring Redis.
|
|
4
|
+
Uses JSON files with atomic writes for data safety.
|
|
5
|
+
|
|
6
|
+
This is the primary storage layer for users without Redis.
|
|
7
|
+
Redis becomes an optional enhancement for real-time features.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Atomic writes (write to temp, then rename)
|
|
11
|
+
- TTL support with lazy expiration
|
|
12
|
+
- Session history for context continuity
|
|
13
|
+
- Auto-compaction of old sessions
|
|
14
|
+
- Cross-session pattern promotion
|
|
15
|
+
|
|
16
|
+
Architecture:
|
|
17
|
+
.attune/
|
|
18
|
+
├── sessions/
|
|
19
|
+
│ ├── current.json <- Active session state
|
|
20
|
+
│ ├── archive/ <- Compressed old sessions
|
|
21
|
+
│ └── index.json <- Session metadata index
|
|
22
|
+
├── patterns/
|
|
23
|
+
│ ├── staged/ <- Patterns awaiting validation
|
|
24
|
+
│ └── promoted/ <- Validated patterns
|
|
25
|
+
└── config.json <- User preferences
|
|
26
|
+
|
|
27
|
+
Copyright 2025-2026 Smart AI Memory, LLC
|
|
28
|
+
Licensed under Fair Source 0.9
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import gzip
|
|
34
|
+
import json
|
|
35
|
+
import time
|
|
36
|
+
import uuid
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
import structlog
|
|
42
|
+
|
|
43
|
+
from attune.config import _validate_file_path
|
|
44
|
+
|
|
45
|
+
logger = structlog.get_logger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# Configuration
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class FileSessionConfig:
|
|
55
|
+
"""Configuration for file-based session memory."""
|
|
56
|
+
|
|
57
|
+
# Storage locations
|
|
58
|
+
base_dir: str = ".empathy"
|
|
59
|
+
sessions_subdir: str = "sessions"
|
|
60
|
+
patterns_subdir: str = "patterns"
|
|
61
|
+
archive_subdir: str = "archive"
|
|
62
|
+
|
|
63
|
+
# Session settings
|
|
64
|
+
session_ttl_hours: int = 24
|
|
65
|
+
working_ttl_seconds: int = 3600 # 1 hour default for working memory
|
|
66
|
+
pattern_ttl_seconds: int = 86400 # 24 hours for staged patterns
|
|
67
|
+
|
|
68
|
+
# Archive settings
|
|
69
|
+
max_sessions_before_archive: int = 10
|
|
70
|
+
archive_compression: bool = True
|
|
71
|
+
archive_retention_days: int = 30
|
|
72
|
+
|
|
73
|
+
# Auto-save settings
|
|
74
|
+
auto_save_interval_seconds: int = 60
|
|
75
|
+
auto_compact_on_close: bool = True
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def sessions_dir(self) -> Path:
|
|
79
|
+
return Path(self.base_dir) / self.sessions_subdir
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def patterns_dir(self) -> Path:
|
|
83
|
+
return Path(self.base_dir) / self.patterns_subdir
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def archive_dir(self) -> Path:
|
|
87
|
+
return self.sessions_dir / self.archive_subdir
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# =============================================================================
|
|
91
|
+
# Data Models
|
|
92
|
+
# =============================================================================
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class WorkingEntry:
|
|
97
|
+
"""Entry in working memory."""
|
|
98
|
+
|
|
99
|
+
key: str
|
|
100
|
+
value: Any
|
|
101
|
+
agent_id: str
|
|
102
|
+
stashed_at: float
|
|
103
|
+
expires_at: float | None = None
|
|
104
|
+
|
|
105
|
+
def is_expired(self) -> bool:
|
|
106
|
+
if self.expires_at is None:
|
|
107
|
+
return False
|
|
108
|
+
return time.time() > self.expires_at
|
|
109
|
+
|
|
110
|
+
def to_dict(self) -> dict:
|
|
111
|
+
return {
|
|
112
|
+
"key": self.key,
|
|
113
|
+
"value": self.value,
|
|
114
|
+
"agent_id": self.agent_id,
|
|
115
|
+
"stashed_at": self.stashed_at,
|
|
116
|
+
"expires_at": self.expires_at,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_dict(cls, data: dict) -> WorkingEntry:
|
|
121
|
+
return cls(
|
|
122
|
+
key=data["key"],
|
|
123
|
+
value=data["value"],
|
|
124
|
+
agent_id=data["agent_id"],
|
|
125
|
+
stashed_at=data["stashed_at"],
|
|
126
|
+
expires_at=data.get("expires_at"),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class StagedPatternFile:
|
|
132
|
+
"""Pattern staged for validation (file-based version)."""
|
|
133
|
+
|
|
134
|
+
pattern_id: str
|
|
135
|
+
agent_id: str
|
|
136
|
+
pattern_type: str
|
|
137
|
+
name: str
|
|
138
|
+
description: str
|
|
139
|
+
code: str | None = None
|
|
140
|
+
confidence: float = 0.5
|
|
141
|
+
staged_at: float = field(default_factory=time.time)
|
|
142
|
+
expires_at: float | None = None
|
|
143
|
+
metadata: dict = field(default_factory=dict)
|
|
144
|
+
|
|
145
|
+
def is_expired(self) -> bool:
|
|
146
|
+
if self.expires_at is None:
|
|
147
|
+
return False
|
|
148
|
+
return time.time() > self.expires_at
|
|
149
|
+
|
|
150
|
+
def to_dict(self) -> dict:
|
|
151
|
+
return {
|
|
152
|
+
"pattern_id": self.pattern_id,
|
|
153
|
+
"agent_id": self.agent_id,
|
|
154
|
+
"pattern_type": self.pattern_type,
|
|
155
|
+
"name": self.name,
|
|
156
|
+
"description": self.description,
|
|
157
|
+
"code": self.code,
|
|
158
|
+
"confidence": self.confidence,
|
|
159
|
+
"staged_at": self.staged_at,
|
|
160
|
+
"expires_at": self.expires_at,
|
|
161
|
+
"metadata": self.metadata,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def from_dict(cls, data: dict) -> StagedPatternFile:
|
|
166
|
+
return cls(
|
|
167
|
+
pattern_id=data["pattern_id"],
|
|
168
|
+
agent_id=data["agent_id"],
|
|
169
|
+
pattern_type=data["pattern_type"],
|
|
170
|
+
name=data["name"],
|
|
171
|
+
description=data["description"],
|
|
172
|
+
code=data.get("code"),
|
|
173
|
+
confidence=data.get("confidence", 0.5),
|
|
174
|
+
staged_at=data.get("staged_at", time.time()),
|
|
175
|
+
expires_at=data.get("expires_at"),
|
|
176
|
+
metadata=data.get("metadata", {}),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class SessionState:
|
|
182
|
+
"""Complete state of a session."""
|
|
183
|
+
|
|
184
|
+
session_id: str
|
|
185
|
+
user_id: str
|
|
186
|
+
started_at: float
|
|
187
|
+
last_updated: float
|
|
188
|
+
working_memory: dict[str, WorkingEntry] = field(default_factory=dict)
|
|
189
|
+
staged_patterns: dict[str, StagedPatternFile] = field(default_factory=dict)
|
|
190
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
191
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
192
|
+
|
|
193
|
+
def to_dict(self) -> dict:
|
|
194
|
+
return {
|
|
195
|
+
"session_id": self.session_id,
|
|
196
|
+
"user_id": self.user_id,
|
|
197
|
+
"started_at": self.started_at,
|
|
198
|
+
"last_updated": self.last_updated,
|
|
199
|
+
"working_memory": {k: v.to_dict() for k, v in self.working_memory.items()},
|
|
200
|
+
"staged_patterns": {k: v.to_dict() for k, v in self.staged_patterns.items()},
|
|
201
|
+
"context": self.context,
|
|
202
|
+
"metadata": self.metadata,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def from_dict(cls, data: dict) -> SessionState:
|
|
207
|
+
return cls(
|
|
208
|
+
session_id=data["session_id"],
|
|
209
|
+
user_id=data["user_id"],
|
|
210
|
+
started_at=data["started_at"],
|
|
211
|
+
last_updated=data["last_updated"],
|
|
212
|
+
working_memory={
|
|
213
|
+
k: WorkingEntry.from_dict(v) for k, v in data.get("working_memory", {}).items()
|
|
214
|
+
},
|
|
215
|
+
staged_patterns={
|
|
216
|
+
k: StagedPatternFile.from_dict(v)
|
|
217
|
+
for k, v in data.get("staged_patterns", {}).items()
|
|
218
|
+
},
|
|
219
|
+
context=data.get("context", {}),
|
|
220
|
+
metadata=data.get("metadata", {}),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def new(cls, user_id: str) -> SessionState:
|
|
225
|
+
"""Create a new session state."""
|
|
226
|
+
now = time.time()
|
|
227
|
+
return cls(
|
|
228
|
+
session_id=f"session_{int(now)}_{uuid.uuid4().hex[:8]}",
|
|
229
|
+
user_id=user_id,
|
|
230
|
+
started_at=now,
|
|
231
|
+
last_updated=now,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# =============================================================================
|
|
236
|
+
# File Session Memory
|
|
237
|
+
# =============================================================================
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class FileSessionMemory:
|
|
241
|
+
"""File-based session memory with persistence.
|
|
242
|
+
|
|
243
|
+
This class provides the same interface as RedisShortTermMemory
|
|
244
|
+
but uses local JSON files instead of Redis.
|
|
245
|
+
|
|
246
|
+
Usage:
|
|
247
|
+
memory = FileSessionMemory(user_id="developer")
|
|
248
|
+
|
|
249
|
+
# Store working data
|
|
250
|
+
memory.stash("analysis_results", {"issues": 3})
|
|
251
|
+
|
|
252
|
+
# Retrieve data
|
|
253
|
+
results = memory.retrieve("analysis_results")
|
|
254
|
+
|
|
255
|
+
# Stage a pattern
|
|
256
|
+
memory.stage_pattern(
|
|
257
|
+
pattern_id="sec_001",
|
|
258
|
+
pattern_type="security",
|
|
259
|
+
name="SQL Injection Prevention",
|
|
260
|
+
description="Always use parameterized queries",
|
|
261
|
+
confidence=0.9
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Persist on close
|
|
265
|
+
memory.close()
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def __init__(
|
|
269
|
+
self,
|
|
270
|
+
user_id: str,
|
|
271
|
+
config: FileSessionConfig | None = None,
|
|
272
|
+
session_id: str | None = None,
|
|
273
|
+
):
|
|
274
|
+
"""Initialize file-based session memory.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
user_id: User/agent identifier
|
|
278
|
+
config: Configuration (uses defaults if None)
|
|
279
|
+
session_id: Resume specific session (creates new if None)
|
|
280
|
+
"""
|
|
281
|
+
self.user_id = user_id
|
|
282
|
+
self.config = config or FileSessionConfig()
|
|
283
|
+
self._dirty = False # Track unsaved changes
|
|
284
|
+
|
|
285
|
+
# Create directories
|
|
286
|
+
self._ensure_directories()
|
|
287
|
+
|
|
288
|
+
# Load or create session
|
|
289
|
+
if session_id:
|
|
290
|
+
self._state = self._load_session(session_id)
|
|
291
|
+
else:
|
|
292
|
+
self._state = self._load_current_or_create()
|
|
293
|
+
|
|
294
|
+
logger.info(
|
|
295
|
+
"file_session_memory_initialized",
|
|
296
|
+
user_id=user_id,
|
|
297
|
+
session_id=self._state.session_id,
|
|
298
|
+
base_dir=str(self.config.base_dir),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# =========================================================================
|
|
302
|
+
# Directory Management
|
|
303
|
+
# =========================================================================
|
|
304
|
+
|
|
305
|
+
def _ensure_directories(self) -> None:
|
|
306
|
+
"""Create required directories."""
|
|
307
|
+
dirs = [
|
|
308
|
+
self.config.sessions_dir,
|
|
309
|
+
self.config.patterns_dir,
|
|
310
|
+
self.config.patterns_dir / "staged",
|
|
311
|
+
self.config.patterns_dir / "promoted",
|
|
312
|
+
self.config.archive_dir,
|
|
313
|
+
]
|
|
314
|
+
for dir_path in dirs:
|
|
315
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
|
|
317
|
+
# =========================================================================
|
|
318
|
+
# Session Lifecycle
|
|
319
|
+
# =========================================================================
|
|
320
|
+
|
|
321
|
+
def _load_current_or_create(self) -> SessionState:
|
|
322
|
+
"""Load current session or create a new one."""
|
|
323
|
+
current_file = self.config.sessions_dir / "current.json"
|
|
324
|
+
|
|
325
|
+
if current_file.exists():
|
|
326
|
+
try:
|
|
327
|
+
data = json.loads(current_file.read_text(encoding="utf-8"))
|
|
328
|
+
state = SessionState.from_dict(data)
|
|
329
|
+
|
|
330
|
+
# Check if session is stale
|
|
331
|
+
age_hours = (time.time() - state.last_updated) / 3600
|
|
332
|
+
if age_hours < self.config.session_ttl_hours:
|
|
333
|
+
logger.info("session_resumed", session_id=state.session_id, age_hours=age_hours)
|
|
334
|
+
return state
|
|
335
|
+
|
|
336
|
+
# Archive stale session
|
|
337
|
+
logger.info("session_stale", session_id=state.session_id, age_hours=age_hours)
|
|
338
|
+
self._archive_session(state)
|
|
339
|
+
|
|
340
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
341
|
+
logger.warning("session_load_failed", error=str(e))
|
|
342
|
+
|
|
343
|
+
# Create new session
|
|
344
|
+
state = SessionState.new(self.user_id)
|
|
345
|
+
self._save_current(state)
|
|
346
|
+
logger.info("session_created", session_id=state.session_id)
|
|
347
|
+
return state
|
|
348
|
+
|
|
349
|
+
def _load_session(self, session_id: str) -> SessionState:
|
|
350
|
+
"""Load a specific session by ID."""
|
|
351
|
+
# Try current
|
|
352
|
+
current_file = self.config.sessions_dir / "current.json"
|
|
353
|
+
if current_file.exists():
|
|
354
|
+
data = json.loads(current_file.read_text(encoding="utf-8"))
|
|
355
|
+
if data.get("session_id") == session_id:
|
|
356
|
+
return SessionState.from_dict(data)
|
|
357
|
+
|
|
358
|
+
# Try archive
|
|
359
|
+
archive_file = self.config.archive_dir / f"{session_id}.json.gz"
|
|
360
|
+
if archive_file.exists():
|
|
361
|
+
with gzip.open(archive_file, "rt", encoding="utf-8") as f:
|
|
362
|
+
data = json.load(f)
|
|
363
|
+
return SessionState.from_dict(data)
|
|
364
|
+
|
|
365
|
+
raise ValueError(f"Session not found: {session_id}")
|
|
366
|
+
|
|
367
|
+
def _save_current(self, state: SessionState | None = None) -> None:
|
|
368
|
+
"""Save current session state with atomic write."""
|
|
369
|
+
state = state or self._state
|
|
370
|
+
state.last_updated = time.time()
|
|
371
|
+
|
|
372
|
+
current_file = self.config.sessions_dir / "current.json"
|
|
373
|
+
self._atomic_write(current_file, state.to_dict())
|
|
374
|
+
self._dirty = False
|
|
375
|
+
|
|
376
|
+
def _atomic_write(self, path: Path, data: dict) -> None:
|
|
377
|
+
"""Write JSON with atomic rename to prevent corruption."""
|
|
378
|
+
# Validate path
|
|
379
|
+
validated_path = _validate_file_path(str(path))
|
|
380
|
+
|
|
381
|
+
# Write to temp file first
|
|
382
|
+
tmp_path = validated_path.with_suffix(".tmp")
|
|
383
|
+
tmp_path.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
|
|
384
|
+
|
|
385
|
+
# Atomic rename
|
|
386
|
+
tmp_path.rename(validated_path)
|
|
387
|
+
|
|
388
|
+
def _archive_session(self, state: SessionState) -> Path:
|
|
389
|
+
"""Archive a session to compressed storage."""
|
|
390
|
+
archive_file = self.config.archive_dir / f"{state.session_id}.json.gz"
|
|
391
|
+
|
|
392
|
+
if self.config.archive_compression:
|
|
393
|
+
with gzip.open(archive_file, "wt", encoding="utf-8") as f:
|
|
394
|
+
json.dump(state.to_dict(), f, indent=2, default=str)
|
|
395
|
+
else:
|
|
396
|
+
archive_file = archive_file.with_suffix("") # Remove .gz
|
|
397
|
+
archive_file.write_text(
|
|
398
|
+
json.dumps(state.to_dict(), indent=2, default=str), encoding="utf-8"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
logger.info("session_archived", session_id=state.session_id, path=str(archive_file))
|
|
402
|
+
return archive_file
|
|
403
|
+
|
|
404
|
+
# =========================================================================
|
|
405
|
+
# Working Memory (Redis-compatible interface)
|
|
406
|
+
# =========================================================================
|
|
407
|
+
|
|
408
|
+
def stash(
|
|
409
|
+
self,
|
|
410
|
+
key: str,
|
|
411
|
+
value: Any,
|
|
412
|
+
ttl: int | None = None,
|
|
413
|
+
agent_id: str | None = None,
|
|
414
|
+
) -> bool:
|
|
415
|
+
"""Store data in working memory.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
key: Storage key
|
|
419
|
+
value: Data to store (must be JSON-serializable)
|
|
420
|
+
ttl: Time-to-live in seconds (default from config)
|
|
421
|
+
agent_id: Agent identifier (defaults to user_id)
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
True if stored successfully
|
|
425
|
+
"""
|
|
426
|
+
ttl = ttl or self.config.working_ttl_seconds
|
|
427
|
+
agent_id = agent_id or self.user_id
|
|
428
|
+
|
|
429
|
+
entry = WorkingEntry(
|
|
430
|
+
key=key,
|
|
431
|
+
value=value,
|
|
432
|
+
agent_id=agent_id,
|
|
433
|
+
stashed_at=time.time(),
|
|
434
|
+
expires_at=time.time() + ttl if ttl else None,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
self._state.working_memory[key] = entry
|
|
438
|
+
self._dirty = True
|
|
439
|
+
|
|
440
|
+
logger.debug("working_stashed", key=key, ttl=ttl)
|
|
441
|
+
return True
|
|
442
|
+
|
|
443
|
+
def retrieve(
|
|
444
|
+
self,
|
|
445
|
+
key: str,
|
|
446
|
+
agent_id: str | None = None,
|
|
447
|
+
) -> Any | None:
|
|
448
|
+
"""Retrieve data from working memory.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
key: Storage key
|
|
452
|
+
agent_id: Agent identifier (for cross-agent retrieval)
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Stored value or None if not found/expired
|
|
456
|
+
"""
|
|
457
|
+
# Clean up expired entries
|
|
458
|
+
self._cleanup_expired()
|
|
459
|
+
|
|
460
|
+
entry = self._state.working_memory.get(key)
|
|
461
|
+
if entry is None:
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
if entry.is_expired():
|
|
465
|
+
del self._state.working_memory[key]
|
|
466
|
+
self._dirty = True
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
return entry.value
|
|
470
|
+
|
|
471
|
+
def delete(self, key: str) -> bool:
|
|
472
|
+
"""Delete a key from working memory."""
|
|
473
|
+
if key in self._state.working_memory:
|
|
474
|
+
del self._state.working_memory[key]
|
|
475
|
+
self._dirty = True
|
|
476
|
+
return True
|
|
477
|
+
return False
|
|
478
|
+
|
|
479
|
+
def keys(self, pattern: str = "*") -> list[str]:
|
|
480
|
+
"""Get keys matching pattern (supports * wildcard)."""
|
|
481
|
+
import fnmatch
|
|
482
|
+
|
|
483
|
+
self._cleanup_expired()
|
|
484
|
+
return [k for k in self._state.working_memory.keys() if fnmatch.fnmatch(k, pattern)]
|
|
485
|
+
|
|
486
|
+
def _cleanup_expired(self) -> None:
|
|
487
|
+
"""Remove expired entries from working memory."""
|
|
488
|
+
expired = [k for k, v in self._state.working_memory.items() if v.is_expired()]
|
|
489
|
+
for key in expired:
|
|
490
|
+
del self._state.working_memory[key]
|
|
491
|
+
if expired:
|
|
492
|
+
self._dirty = True
|
|
493
|
+
logger.debug("expired_entries_cleaned", count=len(expired))
|
|
494
|
+
|
|
495
|
+
# =========================================================================
|
|
496
|
+
# Pattern Staging (Redis-compatible interface)
|
|
497
|
+
# =========================================================================
|
|
498
|
+
|
|
499
|
+
def stage_pattern(
|
|
500
|
+
self,
|
|
501
|
+
pattern_id: str,
|
|
502
|
+
pattern_type: str,
|
|
503
|
+
name: str,
|
|
504
|
+
description: str,
|
|
505
|
+
code: str | None = None,
|
|
506
|
+
confidence: float = 0.5,
|
|
507
|
+
metadata: dict | None = None,
|
|
508
|
+
) -> bool:
|
|
509
|
+
"""Stage a pattern for validation.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
pattern_id: Unique pattern identifier
|
|
513
|
+
pattern_type: Type of pattern (security, performance, etc.)
|
|
514
|
+
name: Human-readable name
|
|
515
|
+
description: Pattern description
|
|
516
|
+
code: Optional code example
|
|
517
|
+
confidence: Confidence score (0.0 - 1.0)
|
|
518
|
+
metadata: Additional metadata
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
True if staged successfully
|
|
522
|
+
"""
|
|
523
|
+
pattern = StagedPatternFile(
|
|
524
|
+
pattern_id=pattern_id,
|
|
525
|
+
agent_id=self.user_id,
|
|
526
|
+
pattern_type=pattern_type,
|
|
527
|
+
name=name,
|
|
528
|
+
description=description,
|
|
529
|
+
code=code,
|
|
530
|
+
confidence=confidence,
|
|
531
|
+
staged_at=time.time(),
|
|
532
|
+
expires_at=time.time() + self.config.pattern_ttl_seconds,
|
|
533
|
+
metadata=metadata or {},
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
self._state.staged_patterns[pattern_id] = pattern
|
|
537
|
+
self._dirty = True
|
|
538
|
+
|
|
539
|
+
# Also write to patterns/staged/ for persistence
|
|
540
|
+
pattern_file = self.config.patterns_dir / "staged" / f"{pattern_id}.json"
|
|
541
|
+
self._atomic_write(pattern_file, pattern.to_dict())
|
|
542
|
+
|
|
543
|
+
logger.info("pattern_staged", pattern_id=pattern_id, confidence=confidence)
|
|
544
|
+
return True
|
|
545
|
+
|
|
546
|
+
def get_staged_patterns(self, pattern_type: str | None = None) -> list[StagedPatternFile]:
|
|
547
|
+
"""Get all staged patterns, optionally filtered by type."""
|
|
548
|
+
self._cleanup_expired_patterns()
|
|
549
|
+
|
|
550
|
+
patterns = list(self._state.staged_patterns.values())
|
|
551
|
+
if pattern_type:
|
|
552
|
+
patterns = [p for p in patterns if p.pattern_type == pattern_type]
|
|
553
|
+
|
|
554
|
+
return sorted(patterns, key=lambda p: p.confidence, reverse=True)
|
|
555
|
+
|
|
556
|
+
def promote_pattern(
|
|
557
|
+
self,
|
|
558
|
+
pattern_id: str,
|
|
559
|
+
min_confidence: float = 0.7,
|
|
560
|
+
) -> tuple[bool, StagedPatternFile | None, str]:
|
|
561
|
+
"""Promote a staged pattern to permanent storage.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
pattern_id: Pattern to promote
|
|
565
|
+
min_confidence: Minimum confidence threshold
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
Tuple of (success, pattern, message)
|
|
569
|
+
"""
|
|
570
|
+
pattern = self._state.staged_patterns.get(pattern_id)
|
|
571
|
+
if pattern is None:
|
|
572
|
+
return False, None, "Pattern not found"
|
|
573
|
+
|
|
574
|
+
if pattern.is_expired():
|
|
575
|
+
del self._state.staged_patterns[pattern_id]
|
|
576
|
+
self._dirty = True
|
|
577
|
+
return False, None, "Pattern expired"
|
|
578
|
+
|
|
579
|
+
if pattern.confidence < min_confidence:
|
|
580
|
+
return False, None, f"Confidence {pattern.confidence} below threshold {min_confidence}"
|
|
581
|
+
|
|
582
|
+
# Move to promoted directory
|
|
583
|
+
promoted_file = self.config.patterns_dir / "promoted" / f"{pattern_id}.json"
|
|
584
|
+
self._atomic_write(promoted_file, pattern.to_dict())
|
|
585
|
+
|
|
586
|
+
# Remove from staged
|
|
587
|
+
staged_file = self.config.patterns_dir / "staged" / f"{pattern_id}.json"
|
|
588
|
+
if staged_file.exists():
|
|
589
|
+
staged_file.unlink()
|
|
590
|
+
del self._state.staged_patterns[pattern_id]
|
|
591
|
+
self._dirty = True
|
|
592
|
+
|
|
593
|
+
logger.info("pattern_promoted", pattern_id=pattern_id, confidence=pattern.confidence)
|
|
594
|
+
return True, pattern, "Pattern promoted successfully"
|
|
595
|
+
|
|
596
|
+
def _cleanup_expired_patterns(self) -> None:
|
|
597
|
+
"""Remove expired patterns."""
|
|
598
|
+
expired = [k for k, v in self._state.staged_patterns.items() if v.is_expired()]
|
|
599
|
+
for pattern_id in expired:
|
|
600
|
+
del self._state.staged_patterns[pattern_id]
|
|
601
|
+
# Also remove file
|
|
602
|
+
staged_file = self.config.patterns_dir / "staged" / f"{pattern_id}.json"
|
|
603
|
+
if staged_file.exists():
|
|
604
|
+
staged_file.unlink()
|
|
605
|
+
if expired:
|
|
606
|
+
self._dirty = True
|
|
607
|
+
logger.debug("expired_patterns_cleaned", count=len(expired))
|
|
608
|
+
|
|
609
|
+
# =========================================================================
|
|
610
|
+
# Context Management
|
|
611
|
+
# =========================================================================
|
|
612
|
+
|
|
613
|
+
def set_context(self, key: str, value: Any) -> None:
|
|
614
|
+
"""Store context data (no TTL, persists for session)."""
|
|
615
|
+
self._state.context[key] = value
|
|
616
|
+
self._dirty = True
|
|
617
|
+
|
|
618
|
+
def get_context(self, key: str, default: Any = None) -> Any:
|
|
619
|
+
"""Retrieve context data."""
|
|
620
|
+
return self._state.context.get(key, default)
|
|
621
|
+
|
|
622
|
+
def get_all_context(self) -> dict[str, Any]:
|
|
623
|
+
"""Get all context data."""
|
|
624
|
+
return self._state.context.copy()
|
|
625
|
+
|
|
626
|
+
# =========================================================================
|
|
627
|
+
# Session History
|
|
628
|
+
# =========================================================================
|
|
629
|
+
|
|
630
|
+
def get_recent_sessions(self, limit: int = 5) -> list[dict]:
|
|
631
|
+
"""Load recent archived sessions for context continuity.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
limit: Maximum number of sessions to return
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
List of session summaries (most recent first)
|
|
638
|
+
"""
|
|
639
|
+
archive_dir = self.config.archive_dir
|
|
640
|
+
sessions = []
|
|
641
|
+
|
|
642
|
+
# Find archived sessions
|
|
643
|
+
archive_files = sorted(
|
|
644
|
+
archive_dir.glob("session_*.json*"),
|
|
645
|
+
key=lambda p: p.stat().st_mtime,
|
|
646
|
+
reverse=True,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
for archive_file in archive_files[:limit]:
|
|
650
|
+
try:
|
|
651
|
+
if archive_file.suffix == ".gz":
|
|
652
|
+
with gzip.open(archive_file, "rt", encoding="utf-8") as f:
|
|
653
|
+
data = json.load(f)
|
|
654
|
+
else:
|
|
655
|
+
data = json.loads(archive_file.read_text(encoding="utf-8"))
|
|
656
|
+
|
|
657
|
+
sessions.append(
|
|
658
|
+
{
|
|
659
|
+
"session_id": data.get("session_id"),
|
|
660
|
+
"user_id": data.get("user_id"),
|
|
661
|
+
"started_at": data.get("started_at"),
|
|
662
|
+
"last_updated": data.get("last_updated"),
|
|
663
|
+
"context_keys": list(data.get("context", {}).keys()),
|
|
664
|
+
"pattern_count": len(data.get("staged_patterns", {})),
|
|
665
|
+
}
|
|
666
|
+
)
|
|
667
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
668
|
+
logger.warning("session_load_error", file=str(archive_file), error=str(e))
|
|
669
|
+
|
|
670
|
+
return sessions
|
|
671
|
+
|
|
672
|
+
# =========================================================================
|
|
673
|
+
# Statistics and Diagnostics
|
|
674
|
+
# =========================================================================
|
|
675
|
+
|
|
676
|
+
def get_stats(self) -> dict:
|
|
677
|
+
"""Get memory statistics."""
|
|
678
|
+
self._cleanup_expired()
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
"mode": "file",
|
|
682
|
+
"session_id": self._state.session_id,
|
|
683
|
+
"user_id": self.user_id,
|
|
684
|
+
"working_keys": len(self._state.working_memory),
|
|
685
|
+
"staged_patterns": len(self._state.staged_patterns),
|
|
686
|
+
"context_keys": len(self._state.context),
|
|
687
|
+
"session_age_hours": (time.time() - self._state.started_at) / 3600,
|
|
688
|
+
"dirty": self._dirty,
|
|
689
|
+
"base_dir": str(self.config.base_dir),
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
def is_connected(self) -> bool:
|
|
693
|
+
"""Check if storage is available (always True for file-based)."""
|
|
694
|
+
return True
|
|
695
|
+
|
|
696
|
+
# =========================================================================
|
|
697
|
+
# Lifecycle
|
|
698
|
+
# =========================================================================
|
|
699
|
+
|
|
700
|
+
def save(self) -> None:
|
|
701
|
+
"""Explicitly save session state."""
|
|
702
|
+
if self._dirty:
|
|
703
|
+
self._save_current()
|
|
704
|
+
logger.debug("session_saved", session_id=self._state.session_id)
|
|
705
|
+
|
|
706
|
+
def close(self) -> None:
|
|
707
|
+
"""Close session and save state."""
|
|
708
|
+
if self.config.auto_compact_on_close:
|
|
709
|
+
self._cleanup_expired()
|
|
710
|
+
self._cleanup_expired_patterns()
|
|
711
|
+
|
|
712
|
+
self.save()
|
|
713
|
+
logger.info("session_closed", session_id=self._state.session_id)
|
|
714
|
+
|
|
715
|
+
def __enter__(self) -> FileSessionMemory:
|
|
716
|
+
return self
|
|
717
|
+
|
|
718
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
719
|
+
self.close()
|
|
720
|
+
|
|
721
|
+
# =========================================================================
|
|
722
|
+
# Redis Compatibility (No-op for features requiring Redis)
|
|
723
|
+
# =========================================================================
|
|
724
|
+
|
|
725
|
+
@property
|
|
726
|
+
def use_mock(self) -> bool:
|
|
727
|
+
"""File-based memory is not mock mode."""
|
|
728
|
+
return False
|
|
729
|
+
|
|
730
|
+
def publish(self, channel: str, message: dict) -> int:
|
|
731
|
+
"""Publish is not supported in file mode."""
|
|
732
|
+
logger.warning("publish_not_supported", channel=channel)
|
|
733
|
+
return 0
|
|
734
|
+
|
|
735
|
+
def subscribe(self, channel: str, handler: Any) -> bool:
|
|
736
|
+
"""Subscribe is not supported in file mode."""
|
|
737
|
+
logger.warning("subscribe_not_supported", channel=channel)
|
|
738
|
+
return False
|
|
739
|
+
|
|
740
|
+
def supports_realtime(self) -> bool:
|
|
741
|
+
"""Check if real-time features are available."""
|
|
742
|
+
return False
|
|
743
|
+
|
|
744
|
+
def supports_distributed(self) -> bool:
|
|
745
|
+
"""Check if distributed features are available."""
|
|
746
|
+
return False
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
# =============================================================================
|
|
750
|
+
# Factory Function
|
|
751
|
+
# =============================================================================
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def get_file_session_memory(
|
|
755
|
+
user_id: str,
|
|
756
|
+
base_dir: str = ".empathy",
|
|
757
|
+
**kwargs,
|
|
758
|
+
) -> FileSessionMemory:
|
|
759
|
+
"""Create a file-based session memory instance.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
user_id: User/agent identifier
|
|
763
|
+
base_dir: Base directory for storage
|
|
764
|
+
**kwargs: Additional config options
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
Configured FileSessionMemory instance
|
|
768
|
+
"""
|
|
769
|
+
config = FileSessionConfig(base_dir=base_dir, **kwargs)
|
|
770
|
+
return FileSessionMemory(user_id=user_id, config=config)
|