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,667 @@
|
|
|
1
|
+
"""Persistent Storage for Socratic Sessions and Blueprints
|
|
2
|
+
|
|
3
|
+
Provides multiple storage backends for persisting:
|
|
4
|
+
- Socratic sessions (in-progress and completed)
|
|
5
|
+
- Workflow blueprints (generated workflows)
|
|
6
|
+
- Success metrics history (for feedback loop)
|
|
7
|
+
|
|
8
|
+
Backends:
|
|
9
|
+
- JSONFileStorage: Simple file-based storage (default)
|
|
10
|
+
- SQLiteStorage: SQLite database for better querying
|
|
11
|
+
- RedisStorage: Redis for distributed/production use
|
|
12
|
+
|
|
13
|
+
Copyright 2026 Smart-AI-Memory
|
|
14
|
+
Licensed under Fair Source License 0.9
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import heapq
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import sqlite3
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from .blueprint import WorkflowBlueprint
|
|
30
|
+
from .session import SessionState, SocraticSession
|
|
31
|
+
from .success import SuccessEvaluation
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# STORAGE INTERFACE
|
|
38
|
+
# =============================================================================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StorageBackend(ABC):
|
|
42
|
+
"""Abstract base for storage backends."""
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def save_session(self, session: SocraticSession) -> None:
|
|
46
|
+
"""Save a Socratic session."""
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def load_session(self, session_id: str) -> SocraticSession | None:
|
|
50
|
+
"""Load a session by ID."""
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def list_sessions(
|
|
54
|
+
self,
|
|
55
|
+
state: SessionState | None = None,
|
|
56
|
+
limit: int = 100,
|
|
57
|
+
) -> list[dict[str, Any]]:
|
|
58
|
+
"""List sessions with optional filtering."""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def delete_session(self, session_id: str) -> bool:
|
|
62
|
+
"""Delete a session."""
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def save_blueprint(self, blueprint: WorkflowBlueprint) -> None:
|
|
66
|
+
"""Save a workflow blueprint."""
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def load_blueprint(self, blueprint_id: str) -> WorkflowBlueprint | None:
|
|
70
|
+
"""Load a blueprint by ID."""
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def list_blueprints(
|
|
74
|
+
self,
|
|
75
|
+
domain: str | None = None,
|
|
76
|
+
limit: int = 100,
|
|
77
|
+
) -> list[dict[str, Any]]:
|
|
78
|
+
"""List blueprints with optional filtering."""
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def save_evaluation(
|
|
82
|
+
self,
|
|
83
|
+
blueprint_id: str,
|
|
84
|
+
evaluation: SuccessEvaluation,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Save a success evaluation for a blueprint."""
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def get_evaluations(
|
|
90
|
+
self,
|
|
91
|
+
blueprint_id: str,
|
|
92
|
+
limit: int = 10,
|
|
93
|
+
) -> list[dict[str, Any]]:
|
|
94
|
+
"""Get evaluation history for a blueprint."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# =============================================================================
|
|
98
|
+
# JSON FILE STORAGE
|
|
99
|
+
# =============================================================================
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class JSONFileStorage(StorageBackend):
|
|
103
|
+
"""File-based JSON storage.
|
|
104
|
+
|
|
105
|
+
Structure:
|
|
106
|
+
base_dir/
|
|
107
|
+
sessions/
|
|
108
|
+
{session_id}.json
|
|
109
|
+
blueprints/
|
|
110
|
+
{blueprint_id}.json
|
|
111
|
+
evaluations/
|
|
112
|
+
{blueprint_id}/
|
|
113
|
+
{timestamp}.json
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> storage = JSONFileStorage(".attune/socratic")
|
|
117
|
+
>>> storage.save_session(session)
|
|
118
|
+
>>> loaded = storage.load_session(session.session_id)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, base_dir: str = ".attune/socratic"):
|
|
122
|
+
"""Initialize storage.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
base_dir: Base directory for storage
|
|
126
|
+
"""
|
|
127
|
+
self.base_dir = Path(base_dir)
|
|
128
|
+
self.sessions_dir = self.base_dir / "sessions"
|
|
129
|
+
self.blueprints_dir = self.base_dir / "blueprints"
|
|
130
|
+
self.evaluations_dir = self.base_dir / "evaluations"
|
|
131
|
+
|
|
132
|
+
# Create directories
|
|
133
|
+
self.sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
self.blueprints_dir.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
self.evaluations_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
def save_session(self, session: SocraticSession) -> None:
|
|
138
|
+
"""Save a session to JSON file."""
|
|
139
|
+
path = self.sessions_dir / f"{session.session_id}.json"
|
|
140
|
+
data = session.to_dict()
|
|
141
|
+
|
|
142
|
+
with path.open("w") as f:
|
|
143
|
+
json.dump(data, f, indent=2, default=str)
|
|
144
|
+
|
|
145
|
+
logger.debug(f"Saved session {session.session_id}")
|
|
146
|
+
|
|
147
|
+
def load_session(self, session_id: str) -> SocraticSession | None:
|
|
148
|
+
"""Load a session from JSON file."""
|
|
149
|
+
path = self.sessions_dir / f"{session_id}.json"
|
|
150
|
+
|
|
151
|
+
if not path.exists():
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
with path.open() as f:
|
|
156
|
+
data = json.load(f)
|
|
157
|
+
return SocraticSession.from_dict(data)
|
|
158
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
159
|
+
logger.error(f"Failed to load session {session_id}: {e}")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def list_sessions(
|
|
163
|
+
self,
|
|
164
|
+
state: SessionState | None = None,
|
|
165
|
+
limit: int = 100,
|
|
166
|
+
) -> list[dict[str, Any]]:
|
|
167
|
+
"""List sessions with optional state filter."""
|
|
168
|
+
sessions = []
|
|
169
|
+
|
|
170
|
+
for path in sorted(self.sessions_dir.glob("*.json"), reverse=True):
|
|
171
|
+
if len(sessions) >= limit:
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
with path.open() as f:
|
|
176
|
+
data = json.load(f)
|
|
177
|
+
|
|
178
|
+
# Filter by state if specified
|
|
179
|
+
if state and data.get("state") != state.value:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
sessions.append(
|
|
183
|
+
{
|
|
184
|
+
"session_id": data.get("session_id"),
|
|
185
|
+
"state": data.get("state"),
|
|
186
|
+
"goal": data.get("goal", "")[:100],
|
|
187
|
+
"created_at": data.get("created_at"),
|
|
188
|
+
"updated_at": data.get("updated_at"),
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
except (json.JSONDecodeError, KeyError):
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
return sessions
|
|
195
|
+
|
|
196
|
+
def delete_session(self, session_id: str) -> bool:
|
|
197
|
+
"""Delete a session file."""
|
|
198
|
+
path = self.sessions_dir / f"{session_id}.json"
|
|
199
|
+
|
|
200
|
+
if path.exists():
|
|
201
|
+
path.unlink()
|
|
202
|
+
return True
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def save_blueprint(self, blueprint: WorkflowBlueprint) -> None:
|
|
206
|
+
"""Save a blueprint to JSON file."""
|
|
207
|
+
path = self.blueprints_dir / f"{blueprint.id}.json"
|
|
208
|
+
data = blueprint.to_dict()
|
|
209
|
+
|
|
210
|
+
with path.open("w") as f:
|
|
211
|
+
json.dump(data, f, indent=2, default=str)
|
|
212
|
+
|
|
213
|
+
logger.debug(f"Saved blueprint {blueprint.id}")
|
|
214
|
+
|
|
215
|
+
def load_blueprint(self, blueprint_id: str) -> WorkflowBlueprint | None:
|
|
216
|
+
"""Load a blueprint from JSON file."""
|
|
217
|
+
path = self.blueprints_dir / f"{blueprint_id}.json"
|
|
218
|
+
|
|
219
|
+
if not path.exists():
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
with path.open() as f:
|
|
224
|
+
data = json.load(f)
|
|
225
|
+
return WorkflowBlueprint.from_dict(data)
|
|
226
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
227
|
+
logger.error(f"Failed to load blueprint {blueprint_id}: {e}")
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
def list_blueprints(
|
|
231
|
+
self,
|
|
232
|
+
domain: str | None = None,
|
|
233
|
+
limit: int = 100,
|
|
234
|
+
) -> list[dict[str, Any]]:
|
|
235
|
+
"""List blueprints with optional domain filter."""
|
|
236
|
+
blueprints = []
|
|
237
|
+
|
|
238
|
+
for path in sorted(self.blueprints_dir.glob("*.json"), reverse=True):
|
|
239
|
+
if len(blueprints) >= limit:
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
with path.open() as f:
|
|
244
|
+
data = json.load(f)
|
|
245
|
+
|
|
246
|
+
# Filter by domain if specified
|
|
247
|
+
if domain and data.get("domain") != domain:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
blueprints.append(
|
|
251
|
+
{
|
|
252
|
+
"id": data.get("id"),
|
|
253
|
+
"name": data.get("name"),
|
|
254
|
+
"domain": data.get("domain"),
|
|
255
|
+
"agents_count": len(data.get("agents", [])),
|
|
256
|
+
"generated_at": data.get("generated_at"),
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
except (json.JSONDecodeError, KeyError):
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
return blueprints
|
|
263
|
+
|
|
264
|
+
def save_evaluation(
|
|
265
|
+
self,
|
|
266
|
+
blueprint_id: str,
|
|
267
|
+
evaluation: SuccessEvaluation,
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Save an evaluation to JSON file."""
|
|
270
|
+
eval_dir = self.evaluations_dir / blueprint_id
|
|
271
|
+
eval_dir.mkdir(exist_ok=True)
|
|
272
|
+
|
|
273
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
274
|
+
path = eval_dir / f"{timestamp}.json"
|
|
275
|
+
|
|
276
|
+
data = evaluation.to_dict()
|
|
277
|
+
with path.open("w") as f:
|
|
278
|
+
json.dump(data, f, indent=2, default=str)
|
|
279
|
+
|
|
280
|
+
def get_evaluations(
|
|
281
|
+
self,
|
|
282
|
+
blueprint_id: str,
|
|
283
|
+
limit: int = 10,
|
|
284
|
+
) -> list[dict[str, Any]]:
|
|
285
|
+
"""Get evaluation history for a blueprint."""
|
|
286
|
+
eval_dir = self.evaluations_dir / blueprint_id
|
|
287
|
+
|
|
288
|
+
if not eval_dir.exists():
|
|
289
|
+
return []
|
|
290
|
+
|
|
291
|
+
evaluations = []
|
|
292
|
+
for path in heapq.nlargest(limit, eval_dir.glob("*.json")):
|
|
293
|
+
try:
|
|
294
|
+
with path.open() as f:
|
|
295
|
+
evaluations.append(json.load(f))
|
|
296
|
+
except json.JSONDecodeError:
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
return evaluations
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# =============================================================================
|
|
303
|
+
# SQLITE STORAGE
|
|
304
|
+
# =============================================================================
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class SQLiteStorage(StorageBackend):
|
|
308
|
+
"""SQLite database storage for better querying.
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
>>> storage = SQLiteStorage(".attune/socratic.db")
|
|
312
|
+
>>> storage.save_session(session)
|
|
313
|
+
>>> sessions = storage.list_sessions(state=SessionState.COMPLETED)
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
def __init__(self, db_path: str = ".attune/socratic.db"):
|
|
317
|
+
"""Initialize SQLite storage.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
db_path: Path to SQLite database file
|
|
321
|
+
"""
|
|
322
|
+
self.db_path = Path(db_path)
|
|
323
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
|
|
325
|
+
self._init_database()
|
|
326
|
+
|
|
327
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
328
|
+
"""Get database connection."""
|
|
329
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
330
|
+
conn.row_factory = sqlite3.Row
|
|
331
|
+
return conn
|
|
332
|
+
|
|
333
|
+
def _init_database(self) -> None:
|
|
334
|
+
"""Initialize database schema."""
|
|
335
|
+
with self._get_connection() as conn:
|
|
336
|
+
conn.executescript(
|
|
337
|
+
"""
|
|
338
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
339
|
+
session_id TEXT PRIMARY KEY,
|
|
340
|
+
state TEXT NOT NULL,
|
|
341
|
+
goal TEXT,
|
|
342
|
+
domain TEXT,
|
|
343
|
+
confidence REAL,
|
|
344
|
+
created_at TEXT,
|
|
345
|
+
updated_at TEXT,
|
|
346
|
+
data JSON NOT NULL
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_state ON sessions(state);
|
|
350
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_domain ON sessions(domain);
|
|
351
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at);
|
|
352
|
+
|
|
353
|
+
CREATE TABLE IF NOT EXISTS blueprints (
|
|
354
|
+
blueprint_id TEXT PRIMARY KEY,
|
|
355
|
+
name TEXT,
|
|
356
|
+
domain TEXT,
|
|
357
|
+
agents_count INTEGER,
|
|
358
|
+
generated_at TEXT,
|
|
359
|
+
source_session_id TEXT,
|
|
360
|
+
data JSON NOT NULL,
|
|
361
|
+
FOREIGN KEY (source_session_id) REFERENCES sessions(session_id)
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
CREATE INDEX IF NOT EXISTS idx_blueprints_domain ON blueprints(domain);
|
|
365
|
+
CREATE INDEX IF NOT EXISTS idx_blueprints_session ON blueprints(source_session_id);
|
|
366
|
+
|
|
367
|
+
CREATE TABLE IF NOT EXISTS evaluations (
|
|
368
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
369
|
+
blueprint_id TEXT NOT NULL,
|
|
370
|
+
overall_success INTEGER,
|
|
371
|
+
overall_score REAL,
|
|
372
|
+
evaluated_at TEXT,
|
|
373
|
+
data JSON NOT NULL,
|
|
374
|
+
FOREIGN KEY (blueprint_id) REFERENCES blueprints(blueprint_id)
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
CREATE INDEX IF NOT EXISTS idx_evaluations_blueprint ON evaluations(blueprint_id);
|
|
378
|
+
"""
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def save_session(self, session: SocraticSession) -> None:
|
|
382
|
+
"""Save session to database."""
|
|
383
|
+
data = session.to_dict()
|
|
384
|
+
domain = session.goal_analysis.domain if session.goal_analysis else None
|
|
385
|
+
confidence = session.goal_analysis.confidence if session.goal_analysis else None
|
|
386
|
+
|
|
387
|
+
with self._get_connection() as conn:
|
|
388
|
+
conn.execute(
|
|
389
|
+
"""
|
|
390
|
+
INSERT OR REPLACE INTO sessions
|
|
391
|
+
(session_id, state, goal, domain, confidence, created_at, updated_at, data)
|
|
392
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
393
|
+
""",
|
|
394
|
+
(
|
|
395
|
+
session.session_id,
|
|
396
|
+
session.state.value,
|
|
397
|
+
session.goal,
|
|
398
|
+
domain,
|
|
399
|
+
confidence,
|
|
400
|
+
session.created_at.isoformat(),
|
|
401
|
+
session.updated_at.isoformat(),
|
|
402
|
+
json.dumps(data, default=str),
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def load_session(self, session_id: str) -> SocraticSession | None:
|
|
407
|
+
"""Load session from database."""
|
|
408
|
+
with self._get_connection() as conn:
|
|
409
|
+
row = conn.execute(
|
|
410
|
+
"SELECT data FROM sessions WHERE session_id = ?", (session_id,)
|
|
411
|
+
).fetchone()
|
|
412
|
+
|
|
413
|
+
if row:
|
|
414
|
+
data = json.loads(row["data"])
|
|
415
|
+
return SocraticSession.from_dict(data)
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
def list_sessions(
|
|
419
|
+
self,
|
|
420
|
+
state: SessionState | None = None,
|
|
421
|
+
limit: int = 100,
|
|
422
|
+
) -> list[dict[str, Any]]:
|
|
423
|
+
"""List sessions from database."""
|
|
424
|
+
with self._get_connection() as conn:
|
|
425
|
+
if state:
|
|
426
|
+
rows = conn.execute(
|
|
427
|
+
"""
|
|
428
|
+
SELECT session_id, state, goal, domain, confidence, created_at, updated_at
|
|
429
|
+
FROM sessions
|
|
430
|
+
WHERE state = ?
|
|
431
|
+
ORDER BY updated_at DESC
|
|
432
|
+
LIMIT ?
|
|
433
|
+
""",
|
|
434
|
+
(state.value, limit),
|
|
435
|
+
).fetchall()
|
|
436
|
+
else:
|
|
437
|
+
rows = conn.execute(
|
|
438
|
+
"""
|
|
439
|
+
SELECT session_id, state, goal, domain, confidence, created_at, updated_at
|
|
440
|
+
FROM sessions
|
|
441
|
+
ORDER BY updated_at DESC
|
|
442
|
+
LIMIT ?
|
|
443
|
+
""",
|
|
444
|
+
(limit,),
|
|
445
|
+
).fetchall()
|
|
446
|
+
|
|
447
|
+
return [dict(row) for row in rows]
|
|
448
|
+
|
|
449
|
+
def delete_session(self, session_id: str) -> bool:
|
|
450
|
+
"""Delete session from database."""
|
|
451
|
+
with self._get_connection() as conn:
|
|
452
|
+
cursor = conn.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
|
|
453
|
+
return cursor.rowcount > 0
|
|
454
|
+
|
|
455
|
+
def save_blueprint(self, blueprint: WorkflowBlueprint) -> None:
|
|
456
|
+
"""Save blueprint to database."""
|
|
457
|
+
data = blueprint.to_dict()
|
|
458
|
+
|
|
459
|
+
with self._get_connection() as conn:
|
|
460
|
+
conn.execute(
|
|
461
|
+
"""
|
|
462
|
+
INSERT OR REPLACE INTO blueprints
|
|
463
|
+
(blueprint_id, name, domain, agents_count, generated_at, source_session_id, data)
|
|
464
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
465
|
+
""",
|
|
466
|
+
(
|
|
467
|
+
blueprint.id,
|
|
468
|
+
blueprint.name,
|
|
469
|
+
blueprint.domain,
|
|
470
|
+
len(blueprint.agents),
|
|
471
|
+
blueprint.generated_at,
|
|
472
|
+
blueprint.source_session_id,
|
|
473
|
+
json.dumps(data, default=str),
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def load_blueprint(self, blueprint_id: str) -> WorkflowBlueprint | None:
|
|
478
|
+
"""Load blueprint from database."""
|
|
479
|
+
with self._get_connection() as conn:
|
|
480
|
+
row = conn.execute(
|
|
481
|
+
"SELECT data FROM blueprints WHERE blueprint_id = ?", (blueprint_id,)
|
|
482
|
+
).fetchone()
|
|
483
|
+
|
|
484
|
+
if row:
|
|
485
|
+
data = json.loads(row["data"])
|
|
486
|
+
return WorkflowBlueprint.from_dict(data)
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
def list_blueprints(
|
|
490
|
+
self,
|
|
491
|
+
domain: str | None = None,
|
|
492
|
+
limit: int = 100,
|
|
493
|
+
) -> list[dict[str, Any]]:
|
|
494
|
+
"""List blueprints from database."""
|
|
495
|
+
with self._get_connection() as conn:
|
|
496
|
+
if domain:
|
|
497
|
+
rows = conn.execute(
|
|
498
|
+
"""
|
|
499
|
+
SELECT blueprint_id as id, name, domain, agents_count, generated_at
|
|
500
|
+
FROM blueprints
|
|
501
|
+
WHERE domain = ?
|
|
502
|
+
ORDER BY generated_at DESC
|
|
503
|
+
LIMIT ?
|
|
504
|
+
""",
|
|
505
|
+
(domain, limit),
|
|
506
|
+
).fetchall()
|
|
507
|
+
else:
|
|
508
|
+
rows = conn.execute(
|
|
509
|
+
"""
|
|
510
|
+
SELECT blueprint_id as id, name, domain, agents_count, generated_at
|
|
511
|
+
FROM blueprints
|
|
512
|
+
ORDER BY generated_at DESC
|
|
513
|
+
LIMIT ?
|
|
514
|
+
""",
|
|
515
|
+
(limit,),
|
|
516
|
+
).fetchall()
|
|
517
|
+
|
|
518
|
+
return [dict(row) for row in rows]
|
|
519
|
+
|
|
520
|
+
def save_evaluation(
|
|
521
|
+
self,
|
|
522
|
+
blueprint_id: str,
|
|
523
|
+
evaluation: SuccessEvaluation,
|
|
524
|
+
) -> None:
|
|
525
|
+
"""Save evaluation to database."""
|
|
526
|
+
data = evaluation.to_dict()
|
|
527
|
+
|
|
528
|
+
with self._get_connection() as conn:
|
|
529
|
+
conn.execute(
|
|
530
|
+
"""
|
|
531
|
+
INSERT INTO evaluations
|
|
532
|
+
(blueprint_id, overall_success, overall_score, evaluated_at, data)
|
|
533
|
+
VALUES (?, ?, ?, ?, ?)
|
|
534
|
+
""",
|
|
535
|
+
(
|
|
536
|
+
blueprint_id,
|
|
537
|
+
1 if evaluation.overall_success else 0,
|
|
538
|
+
evaluation.overall_score,
|
|
539
|
+
evaluation.evaluated_at,
|
|
540
|
+
json.dumps(data, default=str),
|
|
541
|
+
),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def get_evaluations(
|
|
545
|
+
self,
|
|
546
|
+
blueprint_id: str,
|
|
547
|
+
limit: int = 10,
|
|
548
|
+
) -> list[dict[str, Any]]:
|
|
549
|
+
"""Get evaluations from database."""
|
|
550
|
+
with self._get_connection() as conn:
|
|
551
|
+
rows = conn.execute(
|
|
552
|
+
"""
|
|
553
|
+
SELECT data FROM evaluations
|
|
554
|
+
WHERE blueprint_id = ?
|
|
555
|
+
ORDER BY evaluated_at DESC
|
|
556
|
+
LIMIT ?
|
|
557
|
+
""",
|
|
558
|
+
(blueprint_id, limit),
|
|
559
|
+
).fetchall()
|
|
560
|
+
|
|
561
|
+
return [json.loads(row["data"]) for row in rows]
|
|
562
|
+
|
|
563
|
+
def get_success_rate(self, blueprint_id: str) -> float:
|
|
564
|
+
"""Get overall success rate for a blueprint."""
|
|
565
|
+
with self._get_connection() as conn:
|
|
566
|
+
row = conn.execute(
|
|
567
|
+
"""
|
|
568
|
+
SELECT
|
|
569
|
+
COUNT(*) as total,
|
|
570
|
+
SUM(overall_success) as successes
|
|
571
|
+
FROM evaluations
|
|
572
|
+
WHERE blueprint_id = ?
|
|
573
|
+
""",
|
|
574
|
+
(blueprint_id,),
|
|
575
|
+
).fetchone()
|
|
576
|
+
|
|
577
|
+
if row and row["total"] > 0:
|
|
578
|
+
return row["successes"] / row["total"]
|
|
579
|
+
return 0.0
|
|
580
|
+
|
|
581
|
+
def search_blueprints(
|
|
582
|
+
self,
|
|
583
|
+
query: str,
|
|
584
|
+
limit: int = 20,
|
|
585
|
+
) -> list[dict[str, Any]]:
|
|
586
|
+
"""Search blueprints by name or domain."""
|
|
587
|
+
with self._get_connection() as conn:
|
|
588
|
+
rows = conn.execute(
|
|
589
|
+
"""
|
|
590
|
+
SELECT blueprint_id as id, name, domain, agents_count, generated_at
|
|
591
|
+
FROM blueprints
|
|
592
|
+
WHERE name LIKE ? OR domain LIKE ?
|
|
593
|
+
ORDER BY generated_at DESC
|
|
594
|
+
LIMIT ?
|
|
595
|
+
""",
|
|
596
|
+
(f"%{query}%", f"%{query}%", limit),
|
|
597
|
+
).fetchall()
|
|
598
|
+
|
|
599
|
+
return [dict(row) for row in rows]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# =============================================================================
|
|
603
|
+
# STORAGE MANAGER
|
|
604
|
+
# =============================================================================
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
@dataclass
|
|
608
|
+
class StorageConfig:
|
|
609
|
+
"""Storage configuration."""
|
|
610
|
+
|
|
611
|
+
backend: str = "json" # json, sqlite, redis
|
|
612
|
+
path: str = ".attune/socratic"
|
|
613
|
+
redis_url: str | None = None
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
class StorageManager:
|
|
617
|
+
"""Manages storage backend lifecycle.
|
|
618
|
+
|
|
619
|
+
Example:
|
|
620
|
+
>>> manager = StorageManager(StorageConfig(backend="sqlite"))
|
|
621
|
+
>>> storage = manager.get_storage()
|
|
622
|
+
>>> storage.save_session(session)
|
|
623
|
+
"""
|
|
624
|
+
|
|
625
|
+
def __init__(self, config: StorageConfig | None = None):
|
|
626
|
+
"""Initialize manager.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
config: Storage configuration
|
|
630
|
+
"""
|
|
631
|
+
self.config = config or StorageConfig()
|
|
632
|
+
self._storage: StorageBackend | None = None
|
|
633
|
+
|
|
634
|
+
def get_storage(self) -> StorageBackend:
|
|
635
|
+
"""Get or create storage backend."""
|
|
636
|
+
if self._storage is None:
|
|
637
|
+
self._storage = self._create_storage()
|
|
638
|
+
return self._storage
|
|
639
|
+
|
|
640
|
+
def _create_storage(self) -> StorageBackend:
|
|
641
|
+
"""Create storage backend based on config."""
|
|
642
|
+
if self.config.backend == "sqlite":
|
|
643
|
+
return SQLiteStorage(f"{self.config.path}.db")
|
|
644
|
+
elif self.config.backend == "redis":
|
|
645
|
+
# Redis would be implemented separately
|
|
646
|
+
logger.warning("Redis storage not implemented, using JSON")
|
|
647
|
+
return JSONFileStorage(self.config.path)
|
|
648
|
+
else:
|
|
649
|
+
return JSONFileStorage(self.config.path)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
# Default storage instance
|
|
653
|
+
_default_storage: StorageBackend | None = None
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def get_default_storage() -> StorageBackend:
|
|
657
|
+
"""Get the default storage backend."""
|
|
658
|
+
global _default_storage
|
|
659
|
+
if _default_storage is None:
|
|
660
|
+
_default_storage = JSONFileStorage()
|
|
661
|
+
return _default_storage
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def set_default_storage(storage: StorageBackend) -> None:
|
|
665
|
+
"""Set the default storage backend."""
|
|
666
|
+
global _default_storage
|
|
667
|
+
_default_storage = storage
|