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,306 @@
|
|
|
1
|
+
"""Sensor Data Parsers
|
|
2
|
+
|
|
3
|
+
Parses sensor data from various formats (HL7, FHIR, manual entry).
|
|
4
|
+
|
|
5
|
+
This is like parsing linter output - converting various formats to standard structure.
|
|
6
|
+
|
|
7
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
8
|
+
Licensed under Fair Source 0.9
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VitalSignType(Enum):
|
|
19
|
+
"""Types of vital signs"""
|
|
20
|
+
|
|
21
|
+
HEART_RATE = "heart_rate"
|
|
22
|
+
BLOOD_PRESSURE = "blood_pressure"
|
|
23
|
+
RESPIRATORY_RATE = "respiratory_rate"
|
|
24
|
+
TEMPERATURE = "temperature"
|
|
25
|
+
OXYGEN_SATURATION = "oxygen_saturation"
|
|
26
|
+
MENTAL_STATUS = "mental_status"
|
|
27
|
+
PAIN_SCORE = "pain_score"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class VitalSignReading:
|
|
32
|
+
"""Standardized vital sign reading.
|
|
33
|
+
|
|
34
|
+
This is the universal format - all parsers convert to this.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
vital_type: VitalSignType
|
|
38
|
+
value: Any
|
|
39
|
+
unit: str
|
|
40
|
+
timestamp: datetime
|
|
41
|
+
source: str # "bedside_monitor", "manual_entry", "wearable"
|
|
42
|
+
patient_id: str
|
|
43
|
+
quality: str | None = None # "good", "poor", "artifact"
|
|
44
|
+
metadata: dict[str, Any] | None = None
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict[str, Any]:
|
|
47
|
+
"""Convert to dictionary"""
|
|
48
|
+
return {
|
|
49
|
+
"vital_type": self.vital_type.value,
|
|
50
|
+
"value": self.value,
|
|
51
|
+
"unit": self.unit,
|
|
52
|
+
"timestamp": self.timestamp.isoformat(),
|
|
53
|
+
"source": self.source,
|
|
54
|
+
"patient_id": self.patient_id,
|
|
55
|
+
"quality": self.quality,
|
|
56
|
+
"metadata": self.metadata or {},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BaseSensorParser:
|
|
61
|
+
"""Base class for sensor data parsers"""
|
|
62
|
+
|
|
63
|
+
def parse(self, data: str) -> list[VitalSignReading]:
|
|
64
|
+
"""Parse sensor data into standardized readings"""
|
|
65
|
+
raise NotImplementedError(
|
|
66
|
+
f"{self.__class__.__name__}.parse() must be implemented. "
|
|
67
|
+
"Create a subclass of BaseSensorParser and implement the parse() method. "
|
|
68
|
+
f"See FHIRObservationParser or SimpleJSONParser for examples."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FHIRObservationParser(BaseSensorParser):
|
|
73
|
+
"""Parse FHIR Observation resources.
|
|
74
|
+
|
|
75
|
+
FHIR is standard for healthcare data exchange.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# LOINC codes for common vitals
|
|
79
|
+
LOINC_MAPPINGS = {
|
|
80
|
+
"8867-4": VitalSignType.HEART_RATE,
|
|
81
|
+
"8480-6": VitalSignType.BLOOD_PRESSURE, # Systolic
|
|
82
|
+
"8462-4": VitalSignType.BLOOD_PRESSURE, # Diastolic
|
|
83
|
+
"9279-1": VitalSignType.RESPIRATORY_RATE,
|
|
84
|
+
"8310-5": VitalSignType.TEMPERATURE,
|
|
85
|
+
"2708-6": VitalSignType.OXYGEN_SATURATION,
|
|
86
|
+
"38208-5": VitalSignType.PAIN_SCORE,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def parse(self, data: str) -> list[VitalSignReading]:
|
|
90
|
+
"""Parse FHIR Observation JSON"""
|
|
91
|
+
try:
|
|
92
|
+
observation = json.loads(data)
|
|
93
|
+
except json.JSONDecodeError:
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
if observation.get("resourceType") != "Observation":
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
readings = []
|
|
100
|
+
|
|
101
|
+
# Extract LOINC code
|
|
102
|
+
code = observation.get("code", {})
|
|
103
|
+
loinc_code = None
|
|
104
|
+
|
|
105
|
+
for coding in code.get("coding", []):
|
|
106
|
+
if coding.get("system") == "http://loinc.org":
|
|
107
|
+
loinc_code = coding.get("code")
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if not loinc_code or loinc_code not in self.LOINC_MAPPINGS:
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
vital_type = self.LOINC_MAPPINGS[loinc_code]
|
|
114
|
+
|
|
115
|
+
# Extract value
|
|
116
|
+
value_qty = observation.get("valueQuantity", {})
|
|
117
|
+
value = value_qty.get("value")
|
|
118
|
+
unit = value_qty.get("unit", "")
|
|
119
|
+
|
|
120
|
+
# Extract timestamp
|
|
121
|
+
timestamp_str = observation.get("effectiveDateTime")
|
|
122
|
+
timestamp = (
|
|
123
|
+
datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
124
|
+
if timestamp_str
|
|
125
|
+
else datetime.now()
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Extract patient ID
|
|
129
|
+
subject = observation.get("subject", {})
|
|
130
|
+
patient_id = subject.get("reference", "").split("/")[-1]
|
|
131
|
+
|
|
132
|
+
reading = VitalSignReading(
|
|
133
|
+
vital_type=vital_type,
|
|
134
|
+
value=value,
|
|
135
|
+
unit=unit,
|
|
136
|
+
timestamp=timestamp,
|
|
137
|
+
source="fhir_observation",
|
|
138
|
+
patient_id=patient_id,
|
|
139
|
+
metadata={"loinc_code": loinc_code},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
readings.append(reading)
|
|
143
|
+
|
|
144
|
+
return readings
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class SimpleJSONParser(BaseSensorParser):
|
|
148
|
+
"""Parse simple JSON format for manual entry or simulation.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
{
|
|
152
|
+
"patient_id": "12345",
|
|
153
|
+
"timestamp": "2024-01-20T14:30:00Z",
|
|
154
|
+
"vitals": {
|
|
155
|
+
"hr": 110,
|
|
156
|
+
"systolic_bp": 95,
|
|
157
|
+
"diastolic_bp": 60,
|
|
158
|
+
"respiratory_rate": 24,
|
|
159
|
+
"temp_f": 101.5,
|
|
160
|
+
"o2_sat": 94
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
VITAL_MAPPINGS = {
|
|
167
|
+
"hr": (VitalSignType.HEART_RATE, "bpm"),
|
|
168
|
+
"heart_rate": (VitalSignType.HEART_RATE, "bpm"),
|
|
169
|
+
"systolic_bp": (VitalSignType.BLOOD_PRESSURE, "mmHg"),
|
|
170
|
+
"diastolic_bp": (VitalSignType.BLOOD_PRESSURE, "mmHg"),
|
|
171
|
+
"bp": (VitalSignType.BLOOD_PRESSURE, "mmHg"),
|
|
172
|
+
"respiratory_rate": (VitalSignType.RESPIRATORY_RATE, "/min"),
|
|
173
|
+
"rr": (VitalSignType.RESPIRATORY_RATE, "/min"),
|
|
174
|
+
"temp_f": (VitalSignType.TEMPERATURE, "°F"),
|
|
175
|
+
"temp_c": (VitalSignType.TEMPERATURE, "°C"),
|
|
176
|
+
"temperature": (VitalSignType.TEMPERATURE, "°F"),
|
|
177
|
+
"o2_sat": (VitalSignType.OXYGEN_SATURATION, "%"),
|
|
178
|
+
"spo2": (VitalSignType.OXYGEN_SATURATION, "%"),
|
|
179
|
+
"mental_status": (VitalSignType.MENTAL_STATUS, "text"),
|
|
180
|
+
"pain": (VitalSignType.PAIN_SCORE, "0-10"),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def parse(self, data: str) -> list[VitalSignReading]:
|
|
184
|
+
"""Parse simple JSON format"""
|
|
185
|
+
try:
|
|
186
|
+
parsed = json.loads(data)
|
|
187
|
+
except json.JSONDecodeError:
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
patient_id = parsed.get("patient_id", "unknown")
|
|
191
|
+
timestamp_str = parsed.get("timestamp")
|
|
192
|
+
timestamp = (
|
|
193
|
+
datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
194
|
+
if timestamp_str
|
|
195
|
+
else datetime.now()
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
vitals = parsed.get("vitals", {})
|
|
199
|
+
|
|
200
|
+
readings = []
|
|
201
|
+
|
|
202
|
+
for key, value in vitals.items():
|
|
203
|
+
if key in self.VITAL_MAPPINGS:
|
|
204
|
+
vital_type, unit = self.VITAL_MAPPINGS[key]
|
|
205
|
+
|
|
206
|
+
reading = VitalSignReading(
|
|
207
|
+
vital_type=vital_type,
|
|
208
|
+
value=value,
|
|
209
|
+
unit=unit,
|
|
210
|
+
timestamp=timestamp,
|
|
211
|
+
source="manual_entry",
|
|
212
|
+
patient_id=patient_id,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
readings.append(reading)
|
|
216
|
+
|
|
217
|
+
return readings
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class SensorParserFactory:
|
|
221
|
+
"""Factory for creating appropriate sensor parser"""
|
|
222
|
+
|
|
223
|
+
_parsers = {"fhir": FHIRObservationParser, "simple_json": SimpleJSONParser}
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def create(cls, format_type: str) -> BaseSensorParser:
|
|
227
|
+
"""Create parser for specified format"""
|
|
228
|
+
parser_class = cls._parsers.get(format_type)
|
|
229
|
+
|
|
230
|
+
if not parser_class:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"Unsupported sensor format: {format_type}. "
|
|
233
|
+
f"Supported: {', '.join(cls._parsers.keys())}",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return parser_class()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def parse_sensor_data(data: str, format_type: str = "simple_json") -> list[VitalSignReading]:
|
|
240
|
+
"""Convenience function to parse sensor data.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
data: Raw sensor data (JSON string)
|
|
244
|
+
format_type: "fhir" or "simple_json"
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of VitalSignReading objects
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
>>> data = '{"patient_id": "12345", "vitals": {"hr": 110}}'
|
|
251
|
+
>>> readings = parse_sensor_data(data, "simple_json")
|
|
252
|
+
>>> print(f"HR: {readings[0].value} {readings[0].unit}")
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
parser = SensorParserFactory.create(format_type)
|
|
256
|
+
return parser.parse(data)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def normalize_vitals(readings: list[VitalSignReading]) -> dict[str, Any]:
|
|
260
|
+
"""Normalize vital sign readings into protocol-checkable format.
|
|
261
|
+
|
|
262
|
+
Takes list of VitalSignReading and converts to dict for protocol checker.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
readings: List of vital sign readings
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Dictionary with normalized values for protocol checking
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
>>> normalized = normalize_vitals(readings)
|
|
272
|
+
>>> # Returns: {"hr": 110, "systolic_bp": 95, "respiratory_rate": 24}
|
|
273
|
+
|
|
274
|
+
"""
|
|
275
|
+
normalized = {}
|
|
276
|
+
|
|
277
|
+
for reading in readings:
|
|
278
|
+
if reading.vital_type == VitalSignType.HEART_RATE:
|
|
279
|
+
normalized["hr"] = reading.value
|
|
280
|
+
|
|
281
|
+
elif reading.vital_type == VitalSignType.BLOOD_PRESSURE:
|
|
282
|
+
# Determine if systolic or diastolic based on value
|
|
283
|
+
if reading.value > 60: # Likely systolic
|
|
284
|
+
normalized["systolic_bp"] = reading.value
|
|
285
|
+
else: # Likely diastolic
|
|
286
|
+
normalized["diastolic_bp"] = reading.value
|
|
287
|
+
|
|
288
|
+
elif reading.vital_type == VitalSignType.RESPIRATORY_RATE:
|
|
289
|
+
normalized["respiratory_rate"] = reading.value
|
|
290
|
+
|
|
291
|
+
elif reading.vital_type == VitalSignType.TEMPERATURE:
|
|
292
|
+
if reading.unit == "°F":
|
|
293
|
+
normalized["temp_f"] = reading.value
|
|
294
|
+
elif reading.unit == "°C":
|
|
295
|
+
normalized["temp_c"] = reading.value
|
|
296
|
+
|
|
297
|
+
elif reading.vital_type == VitalSignType.OXYGEN_SATURATION:
|
|
298
|
+
normalized["o2_sat"] = reading.value
|
|
299
|
+
|
|
300
|
+
elif reading.vital_type == VitalSignType.MENTAL_STATUS:
|
|
301
|
+
normalized["mental_status"] = reading.value
|
|
302
|
+
|
|
303
|
+
elif reading.vital_type == VitalSignType.PAIN_SCORE:
|
|
304
|
+
normalized["pain_score"] = reading.value
|
|
305
|
+
|
|
306
|
+
return normalized
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Trajectory Analyzer (Level 4)
|
|
2
|
+
|
|
3
|
+
Analyzes vital sign trends to predict patient deterioration BEFORE critical.
|
|
4
|
+
|
|
5
|
+
This is Level 4 Anticipatory Empathy - alerting before the patient meets full crisis criteria.
|
|
6
|
+
|
|
7
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
8
|
+
Licensed under Fair Source 0.9
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class VitalTrend:
|
|
17
|
+
"""Trend analysis for a single vital sign"""
|
|
18
|
+
|
|
19
|
+
parameter: str
|
|
20
|
+
current_value: float
|
|
21
|
+
previous_value: float
|
|
22
|
+
change: float
|
|
23
|
+
change_percent: float
|
|
24
|
+
direction: str # "increasing", "decreasing", "stable"
|
|
25
|
+
rate_of_change: float # units per hour
|
|
26
|
+
concerning: bool
|
|
27
|
+
reasoning: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TrajectoryPrediction:
|
|
32
|
+
"""Prediction of patient trajectory.
|
|
33
|
+
|
|
34
|
+
This is Level 4 - predicting BEFORE criteria met.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
trajectory_state: str # "stable", "improving", "concerning", "critical"
|
|
38
|
+
estimated_time_to_critical: str | None
|
|
39
|
+
vital_trends: list[VitalTrend]
|
|
40
|
+
overall_assessment: str
|
|
41
|
+
confidence: float
|
|
42
|
+
recommendations: list[str]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TrajectoryAnalyzer:
|
|
46
|
+
"""Analyzes vital sign trajectory to predict deterioration.
|
|
47
|
+
|
|
48
|
+
This implements Level 4 Anticipatory Empathy.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
# Define normal ranges
|
|
53
|
+
self.normal_ranges = {
|
|
54
|
+
"hr": (60, 100),
|
|
55
|
+
"systolic_bp": (90, 140),
|
|
56
|
+
"diastolic_bp": (60, 90),
|
|
57
|
+
"respiratory_rate": (12, 20),
|
|
58
|
+
"temp_f": (97.0, 99.5),
|
|
59
|
+
"o2_sat": (95, 100),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Define concerning rates of change
|
|
63
|
+
self.concerning_rates = {
|
|
64
|
+
"hr": 15, # bpm increase over 2 hours
|
|
65
|
+
"systolic_bp": 20, # mmHg decrease
|
|
66
|
+
"respiratory_rate": 5, # breaths/min increase
|
|
67
|
+
"temp_f": 2.0, # degrees increase
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def analyze_trajectory(
|
|
71
|
+
self,
|
|
72
|
+
current_data: dict[str, float],
|
|
73
|
+
historical_data: list[dict[str, Any]],
|
|
74
|
+
) -> TrajectoryPrediction:
|
|
75
|
+
"""Analyze patient trajectory from historical vitals.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
current_data: Current vital signs
|
|
79
|
+
historical_data: List of previous readings (last 6-12 hours)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
TrajectoryPrediction with assessment
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
>>> history = [
|
|
86
|
+
... {"timestamp": "12:00", "hr": 95, "systolic_bp": 120},
|
|
87
|
+
... {"timestamp": "13:00", "hr": 105, "systolic_bp": 110},
|
|
88
|
+
... {"timestamp": "14:00", "hr": 112, "systolic_bp": 95}
|
|
89
|
+
... ]
|
|
90
|
+
>>> prediction = analyzer.analyze_trajectory(current_vitals, history)
|
|
91
|
+
>>> if prediction.trajectory_state == "concerning":
|
|
92
|
+
... print(f"ALERT: {prediction.overall_assessment}")
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
if not historical_data:
|
|
96
|
+
return TrajectoryPrediction(
|
|
97
|
+
trajectory_state="stable",
|
|
98
|
+
estimated_time_to_critical=None,
|
|
99
|
+
vital_trends=[],
|
|
100
|
+
overall_assessment="Insufficient historical data for trajectory analysis",
|
|
101
|
+
confidence=0.3,
|
|
102
|
+
recommendations=["Continue monitoring"],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Analyze trends for each vital sign
|
|
106
|
+
vital_trends = []
|
|
107
|
+
|
|
108
|
+
for parameter, current_value in current_data.items():
|
|
109
|
+
if parameter in ["mental_status"]: # Skip non-numeric
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
trend = self._analyze_parameter_trend(parameter, current_value, historical_data)
|
|
113
|
+
|
|
114
|
+
if trend:
|
|
115
|
+
vital_trends.append(trend)
|
|
116
|
+
|
|
117
|
+
# Determine overall trajectory state
|
|
118
|
+
trajectory_state = self._determine_trajectory_state(vital_trends)
|
|
119
|
+
|
|
120
|
+
# Estimate time to critical (if concerning)
|
|
121
|
+
time_to_critical = None
|
|
122
|
+
if trajectory_state in ["concerning", "critical"]:
|
|
123
|
+
time_to_critical = self._estimate_time_to_critical(vital_trends, current_data)
|
|
124
|
+
|
|
125
|
+
# Generate overall assessment
|
|
126
|
+
assessment = self._generate_assessment(trajectory_state, vital_trends, time_to_critical)
|
|
127
|
+
|
|
128
|
+
# Generate recommendations
|
|
129
|
+
recommendations = self._generate_recommendations(trajectory_state, vital_trends)
|
|
130
|
+
|
|
131
|
+
# Calculate confidence
|
|
132
|
+
confidence = self._calculate_confidence(historical_data, vital_trends)
|
|
133
|
+
|
|
134
|
+
return TrajectoryPrediction(
|
|
135
|
+
trajectory_state=trajectory_state,
|
|
136
|
+
estimated_time_to_critical=time_to_critical,
|
|
137
|
+
vital_trends=vital_trends,
|
|
138
|
+
overall_assessment=assessment,
|
|
139
|
+
confidence=confidence,
|
|
140
|
+
recommendations=recommendations,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def _analyze_parameter_trend(
|
|
144
|
+
self,
|
|
145
|
+
parameter: str,
|
|
146
|
+
current_value: float,
|
|
147
|
+
historical_data: list[dict[str, Any]],
|
|
148
|
+
) -> VitalTrend | None:
|
|
149
|
+
"""Analyze trend for single parameter"""
|
|
150
|
+
# Extract historical values
|
|
151
|
+
historical_values = []
|
|
152
|
+
for entry in historical_data:
|
|
153
|
+
if parameter in entry and entry[parameter] is not None:
|
|
154
|
+
historical_values.append(entry[parameter])
|
|
155
|
+
|
|
156
|
+
if not historical_values:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# Calculate change from most recent
|
|
160
|
+
previous_value = historical_values[-1]
|
|
161
|
+
change = current_value - previous_value
|
|
162
|
+
change_percent = (change / previous_value * 100) if previous_value != 0 else 0
|
|
163
|
+
|
|
164
|
+
# Determine direction
|
|
165
|
+
if abs(change_percent) < 5:
|
|
166
|
+
direction = "stable"
|
|
167
|
+
elif change > 0:
|
|
168
|
+
direction = "increasing"
|
|
169
|
+
else:
|
|
170
|
+
direction = "decreasing"
|
|
171
|
+
|
|
172
|
+
# Calculate rate of change (per hour)
|
|
173
|
+
# Assuming historical data spans 2-6 hours
|
|
174
|
+
hours_elapsed = len(historical_values) / 2 # Rough estimate
|
|
175
|
+
rate_of_change = abs(change) / hours_elapsed if hours_elapsed > 0 else 0
|
|
176
|
+
|
|
177
|
+
# Determine if concerning
|
|
178
|
+
concerning, reasoning = self._is_trend_concerning(
|
|
179
|
+
parameter,
|
|
180
|
+
current_value,
|
|
181
|
+
change,
|
|
182
|
+
rate_of_change,
|
|
183
|
+
direction,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return VitalTrend(
|
|
187
|
+
parameter=parameter,
|
|
188
|
+
current_value=current_value,
|
|
189
|
+
previous_value=previous_value,
|
|
190
|
+
change=change,
|
|
191
|
+
change_percent=change_percent,
|
|
192
|
+
direction=direction,
|
|
193
|
+
rate_of_change=rate_of_change,
|
|
194
|
+
concerning=concerning,
|
|
195
|
+
reasoning=reasoning,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def _is_trend_concerning(
|
|
199
|
+
self,
|
|
200
|
+
parameter: str,
|
|
201
|
+
current_value: float,
|
|
202
|
+
change: float,
|
|
203
|
+
rate_of_change: float,
|
|
204
|
+
direction: str,
|
|
205
|
+
) -> tuple[bool, str]:
|
|
206
|
+
"""Determine if trend is concerning"""
|
|
207
|
+
# Check if currently out of normal range
|
|
208
|
+
if parameter in self.normal_ranges:
|
|
209
|
+
min_val, max_val = self.normal_ranges[parameter]
|
|
210
|
+
|
|
211
|
+
if current_value < min_val:
|
|
212
|
+
return True, f"{parameter} below normal range ({min_val}-{max_val})"
|
|
213
|
+
if current_value > max_val:
|
|
214
|
+
return True, f"{parameter} above normal range ({min_val}-{max_val})"
|
|
215
|
+
|
|
216
|
+
# Check rate of change
|
|
217
|
+
if parameter in self.concerning_rates:
|
|
218
|
+
threshold = self.concerning_rates[parameter]
|
|
219
|
+
|
|
220
|
+
if parameter == "hr" and direction == "increasing" and rate_of_change > threshold:
|
|
221
|
+
return True, f"HR increasing rapidly (+{change:.0f} bpm)"
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
parameter == "systolic_bp"
|
|
225
|
+
and direction == "decreasing"
|
|
226
|
+
and rate_of_change > threshold
|
|
227
|
+
):
|
|
228
|
+
return True, f"BP decreasing rapidly (-{abs(change):.0f} mmHg)"
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
parameter == "respiratory_rate"
|
|
232
|
+
and direction == "increasing"
|
|
233
|
+
and rate_of_change > threshold
|
|
234
|
+
):
|
|
235
|
+
return True, f"RR increasing rapidly (+{change:.0f} /min)"
|
|
236
|
+
|
|
237
|
+
if parameter == "temp_f" and direction == "increasing" and rate_of_change > threshold:
|
|
238
|
+
return True, f"Temp increasing rapidly (+{change:.1f}°F)"
|
|
239
|
+
|
|
240
|
+
return False, "Within normal trajectory"
|
|
241
|
+
|
|
242
|
+
def _determine_trajectory_state(self, vital_trends: list[VitalTrend]) -> str:
|
|
243
|
+
"""Determine overall trajectory state"""
|
|
244
|
+
concerning_trends = [t for t in vital_trends if t.concerning]
|
|
245
|
+
|
|
246
|
+
if not concerning_trends:
|
|
247
|
+
return "stable"
|
|
248
|
+
|
|
249
|
+
# Count concerning trends by severity
|
|
250
|
+
critical_parameters = ["systolic_bp", "o2_sat"]
|
|
251
|
+
critical_concerning = sum(
|
|
252
|
+
1 for t in concerning_trends if t.parameter in critical_parameters
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if critical_concerning >= 1:
|
|
256
|
+
return "critical"
|
|
257
|
+
|
|
258
|
+
if len(concerning_trends) >= 2:
|
|
259
|
+
return "concerning"
|
|
260
|
+
|
|
261
|
+
if len(concerning_trends) == 1:
|
|
262
|
+
return "concerning"
|
|
263
|
+
|
|
264
|
+
return "stable"
|
|
265
|
+
|
|
266
|
+
def _estimate_time_to_critical(
|
|
267
|
+
self,
|
|
268
|
+
vital_trends: list[VitalTrend],
|
|
269
|
+
current_data: dict[str, float],
|
|
270
|
+
) -> str | None:
|
|
271
|
+
"""Estimate time until patient meets critical criteria.
|
|
272
|
+
|
|
273
|
+
This is core Level 4 - predicting the future.
|
|
274
|
+
"""
|
|
275
|
+
# Example: If BP dropping at 10 mmHg/hour, currently 95, critical is 85
|
|
276
|
+
# Time to critical = (95 - 85) / 10 = 1 hour
|
|
277
|
+
|
|
278
|
+
for trend in vital_trends:
|
|
279
|
+
if not trend.concerning:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
if trend.parameter == "systolic_bp" and trend.direction == "decreasing":
|
|
283
|
+
critical_threshold = 90
|
|
284
|
+
current = trend.current_value
|
|
285
|
+
rate = trend.rate_of_change
|
|
286
|
+
|
|
287
|
+
if rate > 0:
|
|
288
|
+
hours_to_critical = (current - critical_threshold) / rate
|
|
289
|
+
if 0 < hours_to_critical < 24:
|
|
290
|
+
return f"~{int(hours_to_critical)} hours"
|
|
291
|
+
|
|
292
|
+
if trend.parameter == "o2_sat" and trend.direction == "decreasing":
|
|
293
|
+
critical_threshold = 90
|
|
294
|
+
current = trend.current_value
|
|
295
|
+
rate = trend.rate_of_change
|
|
296
|
+
|
|
297
|
+
if rate > 0:
|
|
298
|
+
hours_to_critical = (current - critical_threshold) / rate
|
|
299
|
+
if 0 < hours_to_critical < 24:
|
|
300
|
+
return f"~{int(hours_to_critical)} hours"
|
|
301
|
+
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
def _generate_assessment(
|
|
305
|
+
self,
|
|
306
|
+
trajectory_state: str,
|
|
307
|
+
vital_trends: list[VitalTrend],
|
|
308
|
+
time_to_critical: str | None,
|
|
309
|
+
) -> str:
|
|
310
|
+
"""Generate overall assessment"""
|
|
311
|
+
if trajectory_state == "stable":
|
|
312
|
+
return "Patient vitals stable. Continue routine monitoring."
|
|
313
|
+
|
|
314
|
+
concerning = [t for t in vital_trends if t.concerning]
|
|
315
|
+
|
|
316
|
+
if trajectory_state == "critical":
|
|
317
|
+
trends_desc = ", ".join(f"{t.parameter} {t.direction}" for t in concerning[:3])
|
|
318
|
+
return (
|
|
319
|
+
f"CRITICAL trajectory detected: {trends_desc}. Immediate intervention recommended."
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if trajectory_state == "concerning":
|
|
323
|
+
trends_desc = ", ".join(f"{t.parameter} {t.direction}" for t in concerning[:3])
|
|
324
|
+
|
|
325
|
+
if time_to_critical:
|
|
326
|
+
return (
|
|
327
|
+
f"Concerning trajectory: {trends_desc}. "
|
|
328
|
+
f"In our experience, this pattern suggests deterioration. "
|
|
329
|
+
f"Estimated time to critical: {time_to_critical}. "
|
|
330
|
+
"Early intervention may prevent escalation."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
f"Concerning trajectory: {trends_desc}. "
|
|
335
|
+
"In our experience, this pattern warrants closer monitoring."
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return "Patient trajectory under assessment."
|
|
339
|
+
|
|
340
|
+
def _generate_recommendations(
|
|
341
|
+
self,
|
|
342
|
+
trajectory_state: str,
|
|
343
|
+
vital_trends: list[VitalTrend],
|
|
344
|
+
) -> list[str]:
|
|
345
|
+
"""Generate actionable recommendations"""
|
|
346
|
+
if trajectory_state == "stable":
|
|
347
|
+
return ["Continue routine monitoring"]
|
|
348
|
+
|
|
349
|
+
recommendations = []
|
|
350
|
+
|
|
351
|
+
if trajectory_state in ["concerning", "critical"]:
|
|
352
|
+
recommendations.append("Notify physician of trajectory")
|
|
353
|
+
recommendations.append("Increase monitoring frequency")
|
|
354
|
+
|
|
355
|
+
concerning = [t for t in vital_trends if t.concerning]
|
|
356
|
+
|
|
357
|
+
for trend in concerning:
|
|
358
|
+
if trend.parameter == "systolic_bp":
|
|
359
|
+
recommendations.append("Assess volume status and perfusion")
|
|
360
|
+
elif trend.parameter == "hr":
|
|
361
|
+
recommendations.append("Assess for infection or pain")
|
|
362
|
+
elif trend.parameter == "respiratory_rate":
|
|
363
|
+
recommendations.append("Assess respiratory status and oxygenation")
|
|
364
|
+
elif trend.parameter == "temp_f":
|
|
365
|
+
recommendations.append("Assess for infection")
|
|
366
|
+
|
|
367
|
+
if trajectory_state == "critical":
|
|
368
|
+
recommendations.append("Consider rapid response team activation")
|
|
369
|
+
|
|
370
|
+
return recommendations
|
|
371
|
+
|
|
372
|
+
def _calculate_confidence(
|
|
373
|
+
self,
|
|
374
|
+
historical_data: list[dict[str, Any]],
|
|
375
|
+
vital_trends: list[VitalTrend],
|
|
376
|
+
) -> float:
|
|
377
|
+
"""Calculate confidence in prediction"""
|
|
378
|
+
# More data = higher confidence
|
|
379
|
+
data_points = len(historical_data)
|
|
380
|
+
data_confidence = min(data_points / 10, 1.0)
|
|
381
|
+
|
|
382
|
+
# More consistent trends = higher confidence
|
|
383
|
+
if vital_trends:
|
|
384
|
+
concerning_count = sum(1 for t in vital_trends if t.concerning)
|
|
385
|
+
trend_confidence = concerning_count / len(vital_trends)
|
|
386
|
+
else:
|
|
387
|
+
trend_confidence = 0.5
|
|
388
|
+
|
|
389
|
+
return (data_confidence + trend_confidence) / 2
|