empathy-framework 4.6.6__py3-none-any.whl → 4.7.1__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.
- empathy_framework-4.7.1.dist-info/METADATA +690 -0
- empathy_framework-4.7.1.dist-info/RECORD +379 -0
- {empathy_framework-4.6.6.dist-info → empathy_framework-4.7.1.dist-info}/top_level.txt +1 -2
- empathy_healthcare_plugin/monitors/monitoring/__init__.py +9 -9
- empathy_llm_toolkit/agent_factory/__init__.py +6 -6
- empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +7 -10
- empathy_llm_toolkit/agents_md/__init__.py +22 -0
- empathy_llm_toolkit/agents_md/loader.py +218 -0
- empathy_llm_toolkit/agents_md/parser.py +271 -0
- empathy_llm_toolkit/agents_md/registry.py +307 -0
- empathy_llm_toolkit/commands/__init__.py +51 -0
- empathy_llm_toolkit/commands/context.py +375 -0
- empathy_llm_toolkit/commands/loader.py +301 -0
- empathy_llm_toolkit/commands/models.py +231 -0
- empathy_llm_toolkit/commands/parser.py +371 -0
- empathy_llm_toolkit/commands/registry.py +429 -0
- empathy_llm_toolkit/config/__init__.py +8 -8
- empathy_llm_toolkit/config/unified.py +3 -7
- empathy_llm_toolkit/context/__init__.py +22 -0
- empathy_llm_toolkit/context/compaction.py +455 -0
- empathy_llm_toolkit/context/manager.py +434 -0
- empathy_llm_toolkit/hooks/__init__.py +24 -0
- empathy_llm_toolkit/hooks/config.py +306 -0
- empathy_llm_toolkit/hooks/executor.py +289 -0
- empathy_llm_toolkit/hooks/registry.py +302 -0
- empathy_llm_toolkit/hooks/scripts/__init__.py +39 -0
- empathy_llm_toolkit/hooks/scripts/evaluate_session.py +201 -0
- empathy_llm_toolkit/hooks/scripts/first_time_init.py +285 -0
- empathy_llm_toolkit/hooks/scripts/pre_compact.py +207 -0
- empathy_llm_toolkit/hooks/scripts/session_end.py +183 -0
- empathy_llm_toolkit/hooks/scripts/session_start.py +163 -0
- empathy_llm_toolkit/hooks/scripts/suggest_compact.py +225 -0
- empathy_llm_toolkit/learning/__init__.py +30 -0
- empathy_llm_toolkit/learning/evaluator.py +438 -0
- empathy_llm_toolkit/learning/extractor.py +514 -0
- empathy_llm_toolkit/learning/storage.py +560 -0
- empathy_llm_toolkit/providers.py +4 -11
- empathy_llm_toolkit/security/__init__.py +17 -17
- empathy_llm_toolkit/utils/tokens.py +2 -5
- empathy_os/__init__.py +202 -70
- empathy_os/cache_monitor.py +5 -3
- empathy_os/cli/__init__.py +11 -55
- empathy_os/cli/__main__.py +29 -15
- empathy_os/cli/commands/inspection.py +21 -12
- empathy_os/cli/commands/memory.py +4 -12
- empathy_os/cli/commands/profiling.py +198 -0
- empathy_os/cli/commands/utilities.py +27 -7
- empathy_os/cli.py +28 -57
- empathy_os/cli_unified.py +525 -1164
- empathy_os/cost_tracker.py +9 -3
- empathy_os/dashboard/server.py +200 -2
- empathy_os/hot_reload/__init__.py +7 -7
- empathy_os/hot_reload/config.py +6 -7
- empathy_os/hot_reload/integration.py +35 -35
- empathy_os/hot_reload/reloader.py +57 -57
- empathy_os/hot_reload/watcher.py +28 -28
- empathy_os/hot_reload/websocket.py +2 -2
- empathy_os/memory/__init__.py +11 -4
- empathy_os/memory/claude_memory.py +1 -1
- empathy_os/memory/cross_session.py +8 -12
- empathy_os/memory/edges.py +6 -6
- empathy_os/memory/file_session.py +770 -0
- empathy_os/memory/graph.py +30 -30
- empathy_os/memory/nodes.py +6 -6
- empathy_os/memory/short_term.py +15 -9
- empathy_os/memory/unified.py +606 -140
- empathy_os/meta_workflows/agent_creator.py +3 -9
- empathy_os/meta_workflows/cli_meta_workflows.py +113 -53
- empathy_os/meta_workflows/form_engine.py +6 -18
- empathy_os/meta_workflows/intent_detector.py +64 -24
- empathy_os/meta_workflows/models.py +3 -1
- empathy_os/meta_workflows/pattern_learner.py +13 -31
- empathy_os/meta_workflows/plan_generator.py +55 -47
- empathy_os/meta_workflows/session_context.py +2 -3
- empathy_os/meta_workflows/workflow.py +20 -51
- empathy_os/models/cli.py +2 -2
- empathy_os/models/tasks.py +1 -2
- empathy_os/models/telemetry.py +4 -1
- empathy_os/models/token_estimator.py +3 -1
- empathy_os/monitoring/alerts.py +938 -9
- empathy_os/monitoring/alerts_cli.py +346 -183
- empathy_os/orchestration/execution_strategies.py +12 -29
- empathy_os/orchestration/pattern_learner.py +20 -26
- empathy_os/orchestration/real_tools.py +6 -15
- empathy_os/platform_utils.py +2 -1
- empathy_os/plugins/__init__.py +2 -2
- empathy_os/plugins/base.py +64 -64
- empathy_os/plugins/registry.py +32 -32
- empathy_os/project_index/index.py +49 -15
- empathy_os/project_index/models.py +1 -2
- empathy_os/project_index/reports.py +1 -1
- empathy_os/project_index/scanner.py +1 -0
- empathy_os/redis_memory.py +10 -7
- empathy_os/resilience/__init__.py +1 -1
- empathy_os/resilience/health.py +10 -10
- empathy_os/routing/__init__.py +7 -7
- empathy_os/routing/chain_executor.py +37 -37
- empathy_os/routing/classifier.py +36 -36
- empathy_os/routing/smart_router.py +40 -40
- empathy_os/routing/{wizard_registry.py → workflow_registry.py} +47 -47
- empathy_os/scaffolding/__init__.py +8 -8
- empathy_os/scaffolding/__main__.py +1 -1
- empathy_os/scaffolding/cli.py +28 -28
- empathy_os/socratic/__init__.py +3 -19
- empathy_os/socratic/ab_testing.py +25 -36
- empathy_os/socratic/blueprint.py +38 -38
- empathy_os/socratic/cli.py +34 -20
- empathy_os/socratic/collaboration.py +30 -28
- empathy_os/socratic/domain_templates.py +9 -1
- empathy_os/socratic/embeddings.py +17 -13
- empathy_os/socratic/engine.py +135 -70
- empathy_os/socratic/explainer.py +70 -60
- empathy_os/socratic/feedback.py +24 -19
- empathy_os/socratic/forms.py +15 -10
- empathy_os/socratic/generator.py +51 -35
- empathy_os/socratic/llm_analyzer.py +25 -23
- empathy_os/socratic/mcp_server.py +99 -159
- empathy_os/socratic/session.py +19 -13
- empathy_os/socratic/storage.py +98 -67
- empathy_os/socratic/success.py +38 -27
- empathy_os/socratic/visual_editor.py +51 -39
- empathy_os/socratic/web_ui.py +99 -66
- empathy_os/telemetry/cli.py +3 -1
- empathy_os/telemetry/usage_tracker.py +1 -3
- empathy_os/test_generator/__init__.py +3 -3
- empathy_os/test_generator/cli.py +28 -28
- empathy_os/test_generator/generator.py +64 -66
- empathy_os/test_generator/risk_analyzer.py +11 -11
- empathy_os/vscode_bridge 2.py +173 -0
- empathy_os/vscode_bridge.py +173 -0
- empathy_os/workflows/__init__.py +212 -120
- empathy_os/workflows/batch_processing.py +8 -24
- empathy_os/workflows/bug_predict.py +1 -1
- empathy_os/workflows/code_review.py +20 -5
- empathy_os/workflows/code_review_pipeline.py +13 -8
- empathy_os/workflows/keyboard_shortcuts/workflow.py +6 -2
- empathy_os/workflows/manage_documentation.py +1 -0
- empathy_os/workflows/orchestrated_health_check.py +6 -11
- empathy_os/workflows/orchestrated_release_prep.py +3 -3
- empathy_os/workflows/pr_review.py +18 -10
- empathy_os/workflows/progressive/README 2.md +454 -0
- empathy_os/workflows/progressive/__init__ 2.py +92 -0
- empathy_os/workflows/progressive/__init__.py +2 -12
- empathy_os/workflows/progressive/cli 2.py +242 -0
- empathy_os/workflows/progressive/cli.py +14 -37
- empathy_os/workflows/progressive/core 2.py +488 -0
- empathy_os/workflows/progressive/core.py +12 -12
- empathy_os/workflows/progressive/orchestrator 2.py +701 -0
- empathy_os/workflows/progressive/orchestrator.py +166 -144
- empathy_os/workflows/progressive/reports 2.py +528 -0
- empathy_os/workflows/progressive/reports.py +22 -31
- empathy_os/workflows/progressive/telemetry 2.py +280 -0
- empathy_os/workflows/progressive/telemetry.py +8 -14
- empathy_os/workflows/progressive/test_gen 2.py +514 -0
- empathy_os/workflows/progressive/test_gen.py +29 -48
- empathy_os/workflows/progressive/workflow 2.py +628 -0
- empathy_os/workflows/progressive/workflow.py +31 -70
- empathy_os/workflows/release_prep.py +21 -6
- empathy_os/workflows/release_prep_crew.py +1 -0
- empathy_os/workflows/secure_release.py +13 -6
- empathy_os/workflows/security_audit.py +8 -3
- empathy_os/workflows/test_coverage_boost_crew.py +3 -2
- empathy_os/workflows/test_maintenance_crew.py +1 -0
- empathy_os/workflows/test_runner.py +16 -12
- empathy_software_plugin/SOFTWARE_PLUGIN_README.md +25 -703
- empathy_software_plugin/cli.py +0 -122
- patterns/README.md +119 -0
- patterns/__init__.py +95 -0
- patterns/behavior.py +298 -0
- patterns/code_review_memory.json +441 -0
- patterns/core.py +97 -0
- patterns/debugging.json +3763 -0
- patterns/empathy.py +268 -0
- patterns/health_check_memory.json +505 -0
- patterns/input.py +161 -0
- patterns/memory_graph.json +8 -0
- patterns/refactoring_memory.json +1113 -0
- patterns/registry.py +663 -0
- patterns/security_memory.json +8 -0
- patterns/structural.py +415 -0
- patterns/validation.py +194 -0
- coach_wizards/__init__.py +0 -45
- coach_wizards/accessibility_wizard.py +0 -91
- coach_wizards/api_wizard.py +0 -91
- coach_wizards/base_wizard.py +0 -209
- coach_wizards/cicd_wizard.py +0 -91
- coach_wizards/code_reviewer_README.md +0 -60
- coach_wizards/code_reviewer_wizard.py +0 -180
- coach_wizards/compliance_wizard.py +0 -91
- coach_wizards/database_wizard.py +0 -91
- coach_wizards/debugging_wizard.py +0 -91
- coach_wizards/documentation_wizard.py +0 -91
- coach_wizards/generate_wizards.py +0 -347
- coach_wizards/localization_wizard.py +0 -173
- coach_wizards/migration_wizard.py +0 -91
- coach_wizards/monitoring_wizard.py +0 -91
- coach_wizards/observability_wizard.py +0 -91
- coach_wizards/performance_wizard.py +0 -91
- coach_wizards/prompt_engineering_wizard.py +0 -661
- coach_wizards/refactoring_wizard.py +0 -91
- coach_wizards/scaling_wizard.py +0 -90
- coach_wizards/security_wizard.py +0 -92
- coach_wizards/testing_wizard.py +0 -91
- empathy_framework-4.6.6.dist-info/METADATA +0 -1597
- empathy_framework-4.6.6.dist-info/RECORD +0 -410
- empathy_llm_toolkit/wizards/__init__.py +0 -43
- empathy_llm_toolkit/wizards/base_wizard.py +0 -364
- empathy_llm_toolkit/wizards/customer_support_wizard.py +0 -190
- empathy_llm_toolkit/wizards/healthcare_wizard.py +0 -378
- empathy_llm_toolkit/wizards/patient_assessment_README.md +0 -64
- empathy_llm_toolkit/wizards/patient_assessment_wizard.py +0 -193
- empathy_llm_toolkit/wizards/technology_wizard.py +0 -209
- empathy_os/wizard_factory_cli.py +0 -170
- empathy_software_plugin/wizards/__init__.py +0 -42
- empathy_software_plugin/wizards/advanced_debugging_wizard.py +0 -395
- empathy_software_plugin/wizards/agent_orchestration_wizard.py +0 -511
- empathy_software_plugin/wizards/ai_collaboration_wizard.py +0 -503
- empathy_software_plugin/wizards/ai_context_wizard.py +0 -441
- empathy_software_plugin/wizards/ai_documentation_wizard.py +0 -503
- empathy_software_plugin/wizards/base_wizard.py +0 -288
- empathy_software_plugin/wizards/book_chapter_wizard.py +0 -519
- empathy_software_plugin/wizards/code_review_wizard.py +0 -604
- empathy_software_plugin/wizards/debugging/__init__.py +0 -50
- empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +0 -414
- empathy_software_plugin/wizards/debugging/config_loaders.py +0 -446
- empathy_software_plugin/wizards/debugging/fix_applier.py +0 -469
- empathy_software_plugin/wizards/debugging/language_patterns.py +0 -385
- empathy_software_plugin/wizards/debugging/linter_parsers.py +0 -470
- empathy_software_plugin/wizards/debugging/verification.py +0 -369
- empathy_software_plugin/wizards/enhanced_testing_wizard.py +0 -537
- empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +0 -816
- empathy_software_plugin/wizards/multi_model_wizard.py +0 -501
- empathy_software_plugin/wizards/pattern_extraction_wizard.py +0 -422
- empathy_software_plugin/wizards/pattern_retriever_wizard.py +0 -400
- empathy_software_plugin/wizards/performance/__init__.py +0 -9
- empathy_software_plugin/wizards/performance/bottleneck_detector.py +0 -221
- empathy_software_plugin/wizards/performance/profiler_parsers.py +0 -278
- empathy_software_plugin/wizards/performance/trajectory_analyzer.py +0 -429
- empathy_software_plugin/wizards/performance_profiling_wizard.py +0 -305
- empathy_software_plugin/wizards/prompt_engineering_wizard.py +0 -425
- empathy_software_plugin/wizards/rag_pattern_wizard.py +0 -461
- empathy_software_plugin/wizards/security/__init__.py +0 -32
- empathy_software_plugin/wizards/security/exploit_analyzer.py +0 -290
- empathy_software_plugin/wizards/security/owasp_patterns.py +0 -241
- empathy_software_plugin/wizards/security/vulnerability_scanner.py +0 -604
- empathy_software_plugin/wizards/security_analysis_wizard.py +0 -322
- empathy_software_plugin/wizards/security_learning_wizard.py +0 -740
- empathy_software_plugin/wizards/tech_debt_wizard.py +0 -726
- empathy_software_plugin/wizards/testing/__init__.py +0 -27
- empathy_software_plugin/wizards/testing/coverage_analyzer.py +0 -459
- empathy_software_plugin/wizards/testing/quality_analyzer.py +0 -525
- empathy_software_plugin/wizards/testing/test_suggester.py +0 -533
- empathy_software_plugin/wizards/testing_wizard.py +0 -274
- wizards/__init__.py +0 -82
- wizards/admission_assessment_wizard.py +0 -644
- wizards/care_plan.py +0 -321
- wizards/clinical_assessment.py +0 -769
- wizards/discharge_planning.py +0 -77
- wizards/discharge_summary_wizard.py +0 -468
- wizards/dosage_calculation.py +0 -497
- wizards/incident_report_wizard.py +0 -454
- wizards/medication_reconciliation.py +0 -85
- wizards/nursing_assessment.py +0 -171
- wizards/patient_education.py +0 -654
- wizards/quality_improvement.py +0 -705
- wizards/sbar_report.py +0 -324
- wizards/sbar_wizard.py +0 -608
- wizards/shift_handoff_wizard.py +0 -535
- wizards/soap_note_wizard.py +0 -679
- wizards/treatment_plan.py +0 -15
- {empathy_framework-4.6.6.dist-info → empathy_framework-4.7.1.dist-info}/WHEEL +0 -0
- {empathy_framework-4.6.6.dist-info → empathy_framework-4.7.1.dist-info}/entry_points.txt +0 -0
- {empathy_framework-4.6.6.dist-info → empathy_framework-4.7.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,267 +1,430 @@
|
|
|
1
|
-
"""Alert CLI
|
|
1
|
+
"""Alert CLI Workflow
|
|
2
2
|
|
|
3
|
-
Interactive
|
|
3
|
+
Interactive workflow for setting up LLM telemetry alerts.
|
|
4
4
|
|
|
5
5
|
**Usage:**
|
|
6
|
-
empathy alerts init
|
|
7
|
-
empathy alerts list
|
|
8
|
-
empathy alerts delete
|
|
9
|
-
empathy alerts watch
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Copyright 2025 Smart-AI-Memory
|
|
6
|
+
empathy alerts init # Interactive setup workflow
|
|
7
|
+
empathy alerts list # List configured alerts
|
|
8
|
+
empathy alerts delete # Delete an alert
|
|
9
|
+
empathy alerts watch # Start monitoring
|
|
10
|
+
empathy alerts history # View alert history
|
|
11
|
+
empathy alerts metrics # View current metrics
|
|
12
|
+
|
|
13
|
+
Copyright 2025-2026 Smart-AI-Memory
|
|
14
14
|
Licensed under Fair Source License 0.9
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
import
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from typing import Any
|
|
20
|
-
|
|
21
|
-
import click
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class AlertEngine:
|
|
25
|
-
"""Alert engine with SQLite storage"""
|
|
26
|
-
|
|
27
|
-
def __init__(self, db_path: str = ".empathy/alerts.db"):
|
|
28
|
-
self.db_path = Path(db_path)
|
|
29
|
-
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
-
self._init_db()
|
|
31
|
-
|
|
32
|
-
def _init_db(self) -> None:
|
|
33
|
-
"""Initialize SQLite database"""
|
|
34
|
-
conn = sqlite3.connect(self.db_path)
|
|
35
|
-
cursor = conn.cursor()
|
|
36
|
-
|
|
37
|
-
cursor.execute(
|
|
38
|
-
"""
|
|
39
|
-
CREATE TABLE IF NOT EXISTS alerts (
|
|
40
|
-
id TEXT PRIMARY KEY,
|
|
41
|
-
name TEXT NOT NULL,
|
|
42
|
-
metric TEXT NOT NULL,
|
|
43
|
-
threshold REAL NOT NULL,
|
|
44
|
-
channel TEXT NOT NULL,
|
|
45
|
-
webhook_url TEXT,
|
|
46
|
-
email TEXT,
|
|
47
|
-
enabled INTEGER DEFAULT 1,
|
|
48
|
-
cooldown INTEGER DEFAULT 3600,
|
|
49
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
50
|
-
)
|
|
51
|
-
"""
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
conn.commit()
|
|
55
|
-
conn.close()
|
|
56
|
-
|
|
57
|
-
def add_alert(
|
|
58
|
-
self,
|
|
59
|
-
alert_id: str,
|
|
60
|
-
name: str,
|
|
61
|
-
metric: str,
|
|
62
|
-
threshold: float,
|
|
63
|
-
channel: str,
|
|
64
|
-
webhook_url: str | None = None,
|
|
65
|
-
email: str | None = None,
|
|
66
|
-
) -> None:
|
|
67
|
-
"""Add a new alert"""
|
|
68
|
-
conn = sqlite3.connect(self.db_path)
|
|
69
|
-
cursor = conn.cursor()
|
|
70
|
-
|
|
71
|
-
cursor.execute(
|
|
72
|
-
"""
|
|
73
|
-
INSERT INTO alerts (id, name, metric, threshold, channel, webhook_url, email)
|
|
74
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
75
|
-
""",
|
|
76
|
-
(alert_id, name, metric, threshold, channel, webhook_url, email),
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
conn.commit()
|
|
80
|
-
conn.close()
|
|
81
|
-
|
|
82
|
-
def list_alerts(self) -> list[dict[str, Any]]:
|
|
83
|
-
"""List all alerts"""
|
|
84
|
-
conn = sqlite3.connect(self.db_path)
|
|
85
|
-
cursor = conn.cursor()
|
|
86
|
-
|
|
87
|
-
cursor.execute("SELECT * FROM alerts")
|
|
88
|
-
rows = cursor.fetchall()
|
|
89
|
-
|
|
90
|
-
conn.close()
|
|
91
|
-
|
|
92
|
-
alerts = []
|
|
93
|
-
for row in rows:
|
|
94
|
-
alerts.append(
|
|
95
|
-
{
|
|
96
|
-
"id": row[0],
|
|
97
|
-
"name": row[1],
|
|
98
|
-
"metric": row[2],
|
|
99
|
-
"threshold": row[3],
|
|
100
|
-
"channel": row[4],
|
|
101
|
-
"webhook_url": row[5],
|
|
102
|
-
"email": row[6],
|
|
103
|
-
"enabled": bool(row[7]),
|
|
104
|
-
"cooldown": row[8],
|
|
105
|
-
"created_at": row[9],
|
|
106
|
-
}
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
return alerts
|
|
110
|
-
|
|
111
|
-
def delete_alert(self, alert_id: str) -> bool:
|
|
112
|
-
"""Delete an alert by ID"""
|
|
113
|
-
conn = sqlite3.connect(self.db_path)
|
|
114
|
-
cursor = conn.cursor()
|
|
17
|
+
from __future__ import annotations
|
|
115
18
|
|
|
116
|
-
|
|
117
|
-
|
|
19
|
+
import signal
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
118
22
|
|
|
119
|
-
|
|
120
|
-
conn.close()
|
|
23
|
+
import click
|
|
121
24
|
|
|
122
|
-
|
|
25
|
+
from .alerts import (
|
|
26
|
+
get_alert_engine,
|
|
27
|
+
)
|
|
123
28
|
|
|
124
29
|
|
|
125
30
|
@click.group()
|
|
126
31
|
def alerts():
|
|
127
|
-
"""Alert management commands"""
|
|
32
|
+
"""Alert management commands for LLM telemetry monitoring."""
|
|
128
33
|
pass
|
|
129
34
|
|
|
130
35
|
|
|
131
36
|
@alerts.command()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
37
|
+
@click.option("--non-interactive", is_flag=True, help="Skip interactive prompts")
|
|
38
|
+
@click.option("--metric", type=click.Choice(["daily_cost", "error_rate", "avg_latency", "token_usage"]))
|
|
39
|
+
@click.option("--threshold", type=float)
|
|
40
|
+
@click.option("--channel", type=click.Choice(["webhook", "email", "stdout"]))
|
|
41
|
+
@click.option("--webhook-url", help="Webhook URL (for webhook channel)")
|
|
42
|
+
@click.option("--email", help="Email address (for email channel)")
|
|
43
|
+
def init(
|
|
44
|
+
non_interactive: bool,
|
|
45
|
+
metric: str | None,
|
|
46
|
+
threshold: float | None,
|
|
47
|
+
channel: str | None,
|
|
48
|
+
webhook_url: str | None,
|
|
49
|
+
email: str | None,
|
|
50
|
+
):
|
|
51
|
+
"""Initialize an alert with interactive workflow or CLI flags."""
|
|
52
|
+
if non_interactive:
|
|
53
|
+
# Non-interactive mode - require all parameters
|
|
54
|
+
if not all([metric, threshold, channel]):
|
|
55
|
+
click.echo("Error: --metric, --threshold, and --channel required in non-interactive mode")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
if channel == "webhook" and not webhook_url:
|
|
59
|
+
click.echo("Error: --webhook-url required for webhook channel")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
if channel == "email" and not email:
|
|
63
|
+
click.echo("Error: --email required for email channel")
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
_create_alert(metric, threshold, channel, webhook_url, email)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# Interactive workflow
|
|
70
|
+
click.echo("🔔 Alert Setup Workflow\n")
|
|
135
71
|
|
|
136
72
|
# Question 1: What metric?
|
|
137
73
|
click.echo("1. What metric do you want to monitor?")
|
|
138
|
-
click.echo(" a) Daily cost")
|
|
139
|
-
click.echo(" b) Error rate")
|
|
74
|
+
click.echo(" a) Daily cost (total USD spent)")
|
|
75
|
+
click.echo(" b) Error rate (% of failed calls)")
|
|
140
76
|
click.echo(" c) Latency (avg response time)")
|
|
141
|
-
click.echo(" d) Token usage")
|
|
77
|
+
click.echo(" d) Token usage (total tokens)")
|
|
142
78
|
|
|
143
79
|
metric_choice = click.prompt("Choose (a/b/c/d)", type=click.Choice(["a", "b", "c", "d"]))
|
|
144
80
|
|
|
145
81
|
metric_map = {
|
|
146
|
-
"a": ("daily_cost", "Daily Cost"),
|
|
147
|
-
"b": ("error_rate", "Error Rate"),
|
|
148
|
-
"c": ("avg_latency", "Average Latency"),
|
|
149
|
-
"d": ("token_usage", "Token Usage"),
|
|
82
|
+
"a": ("daily_cost", "Daily Cost", "USD"),
|
|
83
|
+
"b": ("error_rate", "Error Rate", "%"),
|
|
84
|
+
"c": ("avg_latency", "Average Latency", "ms"),
|
|
85
|
+
"d": ("token_usage", "Token Usage", "tokens"),
|
|
150
86
|
}
|
|
151
87
|
|
|
152
|
-
metric, metric_name = metric_map[metric_choice]
|
|
88
|
+
metric, metric_name, unit = metric_map[metric_choice]
|
|
153
89
|
|
|
154
90
|
# Question 2: What threshold?
|
|
155
91
|
click.echo(f"\n2. What threshold for {metric_name}?")
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
threshold = click.prompt("Token usage threshold", type=int, default=100000)
|
|
92
|
+
defaults = {
|
|
93
|
+
"daily_cost": 10.0,
|
|
94
|
+
"error_rate": 10.0,
|
|
95
|
+
"avg_latency": 3000,
|
|
96
|
+
"token_usage": 100000,
|
|
97
|
+
}
|
|
98
|
+
threshold = click.prompt(f"Threshold ({unit})", type=float, default=defaults[metric])
|
|
164
99
|
|
|
165
100
|
# Question 3: Where to send?
|
|
166
101
|
click.echo("\n3. Where should alerts be sent?")
|
|
167
|
-
click.echo(" a) Webhook (Slack, Discord,
|
|
102
|
+
click.echo(" a) Webhook (Slack, Discord, Teams)")
|
|
168
103
|
click.echo(" b) Email")
|
|
169
|
-
click.echo(" c)
|
|
104
|
+
click.echo(" c) Console output")
|
|
170
105
|
|
|
171
106
|
channel_choice = click.prompt("Choose (a/b/c)", type=click.Choice(["a", "b", "c"]))
|
|
172
107
|
|
|
173
108
|
channel_map = {
|
|
174
109
|
"a": "webhook",
|
|
175
110
|
"b": "email",
|
|
176
|
-
"c": "
|
|
111
|
+
"c": "stdout",
|
|
177
112
|
}
|
|
178
113
|
|
|
179
114
|
channel = channel_map[channel_choice]
|
|
180
|
-
|
|
181
115
|
webhook_url = None
|
|
182
|
-
|
|
116
|
+
email_addr = None
|
|
183
117
|
|
|
184
118
|
if channel == "webhook":
|
|
185
119
|
webhook_url = click.prompt("Webhook URL")
|
|
186
120
|
elif channel == "email":
|
|
187
|
-
|
|
121
|
+
email_addr = click.prompt("Email address")
|
|
122
|
+
|
|
123
|
+
_create_alert(metric, threshold, channel, webhook_url, email_addr)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _create_alert(
|
|
127
|
+
metric: str,
|
|
128
|
+
threshold: float,
|
|
129
|
+
channel: str,
|
|
130
|
+
webhook_url: str | None,
|
|
131
|
+
email: str | None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Create an alert with the given configuration."""
|
|
134
|
+
engine = get_alert_engine()
|
|
135
|
+
alert_id = f"alert_{metric}_{int(time.time())}"
|
|
136
|
+
|
|
137
|
+
metric_names = {
|
|
138
|
+
"daily_cost": "Daily Cost",
|
|
139
|
+
"error_rate": "Error Rate",
|
|
140
|
+
"avg_latency": "Average Latency",
|
|
141
|
+
"token_usage": "Token Usage",
|
|
142
|
+
}
|
|
188
143
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
144
|
+
try:
|
|
145
|
+
engine.add_alert(
|
|
146
|
+
alert_id=alert_id,
|
|
147
|
+
name=f"{metric_names.get(metric, metric)} Alert",
|
|
148
|
+
metric=metric,
|
|
149
|
+
threshold=threshold,
|
|
150
|
+
channel=channel,
|
|
151
|
+
webhook_url=webhook_url,
|
|
152
|
+
email=email,
|
|
153
|
+
)
|
|
192
154
|
|
|
193
|
-
|
|
194
|
-
alert_id
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
channel=channel,
|
|
199
|
-
webhook_url=webhook_url,
|
|
200
|
-
email=email,
|
|
201
|
-
)
|
|
155
|
+
click.echo("\n✅ Alert created successfully!")
|
|
156
|
+
click.echo(f" ID: {alert_id}")
|
|
157
|
+
click.echo(f" Metric: {metric_names.get(metric, metric)}")
|
|
158
|
+
click.echo(f" Threshold: {threshold}")
|
|
159
|
+
click.echo(f" Channel: {channel}")
|
|
202
160
|
|
|
203
|
-
|
|
204
|
-
click.echo(f" ID: {alert_id}")
|
|
205
|
-
click.echo(f" Metric: {metric_name}")
|
|
206
|
-
click.echo(f" Threshold: {threshold}")
|
|
207
|
-
click.echo(f" Channel: {channel}")
|
|
161
|
+
click.echo("\n💡 Tip: Run 'empathy alerts watch' to start monitoring")
|
|
208
162
|
|
|
209
|
-
|
|
163
|
+
except ValueError as e:
|
|
164
|
+
click.echo(f"\n❌ Error: {e}")
|
|
165
|
+
sys.exit(1)
|
|
210
166
|
|
|
211
167
|
|
|
212
168
|
@alerts.command(name="list")
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
169
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
170
|
+
def list_cmd(as_json: bool):
|
|
171
|
+
"""List all configured alerts."""
|
|
172
|
+
engine = get_alert_engine()
|
|
216
173
|
alerts_list = engine.list_alerts()
|
|
217
174
|
|
|
218
175
|
if not alerts_list:
|
|
219
|
-
|
|
176
|
+
if as_json:
|
|
177
|
+
click.echo("[]")
|
|
178
|
+
else:
|
|
179
|
+
click.echo("No alerts configured. Run 'empathy alerts init' to create one.")
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
if as_json:
|
|
183
|
+
import json
|
|
184
|
+
click.echo(json.dumps([a.to_dict() for a in alerts_list], indent=2))
|
|
220
185
|
return
|
|
221
186
|
|
|
222
187
|
click.echo("📋 Configured Alerts:\n")
|
|
223
188
|
|
|
224
189
|
for alert in alerts_list:
|
|
225
|
-
status = "✓ Enabled" if alert
|
|
226
|
-
click.echo(f" [{status}] {alert
|
|
227
|
-
click.echo(f" ID: {alert
|
|
228
|
-
click.echo(f" Metric: {alert
|
|
229
|
-
click.echo(f" Channel: {alert
|
|
190
|
+
status = "✓ Enabled" if alert.enabled else "✗ Disabled"
|
|
191
|
+
click.echo(f" [{status}] {alert.name}")
|
|
192
|
+
click.echo(f" ID: {alert.alert_id}")
|
|
193
|
+
click.echo(f" Metric: {alert.metric.value} >= {alert.threshold}")
|
|
194
|
+
click.echo(f" Channel: {alert.channel.value}")
|
|
195
|
+
click.echo(f" Severity: {alert.severity.value}")
|
|
196
|
+
click.echo(f" Cooldown: {alert.cooldown_seconds}s")
|
|
230
197
|
click.echo()
|
|
231
198
|
|
|
232
199
|
|
|
233
200
|
@alerts.command()
|
|
234
201
|
@click.argument("alert_id")
|
|
235
202
|
def delete(alert_id: str):
|
|
236
|
-
"""Delete an alert by ID"""
|
|
237
|
-
engine =
|
|
203
|
+
"""Delete an alert by ID."""
|
|
204
|
+
engine = get_alert_engine()
|
|
238
205
|
deleted = engine.delete_alert(alert_id)
|
|
239
206
|
|
|
240
207
|
if deleted:
|
|
241
208
|
click.echo(f"✅ Alert '{alert_id}' deleted successfully")
|
|
242
209
|
else:
|
|
243
210
|
click.echo(f"❌ Alert '{alert_id}' not found")
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@alerts.command()
|
|
215
|
+
@click.argument("alert_id")
|
|
216
|
+
def enable(alert_id: str):
|
|
217
|
+
"""Enable an alert by ID."""
|
|
218
|
+
engine = get_alert_engine()
|
|
219
|
+
if engine.enable_alert(alert_id):
|
|
220
|
+
click.echo(f"✅ Alert '{alert_id}' enabled")
|
|
221
|
+
else:
|
|
222
|
+
click.echo(f"❌ Alert '{alert_id}' not found")
|
|
223
|
+
sys.exit(1)
|
|
244
224
|
|
|
245
225
|
|
|
246
226
|
@alerts.command()
|
|
247
|
-
@click.
|
|
248
|
-
def
|
|
249
|
-
"""
|
|
227
|
+
@click.argument("alert_id")
|
|
228
|
+
def disable(alert_id: str):
|
|
229
|
+
"""Disable an alert by ID."""
|
|
230
|
+
engine = get_alert_engine()
|
|
231
|
+
if engine.disable_alert(alert_id):
|
|
232
|
+
click.echo(f"✅ Alert '{alert_id}' disabled")
|
|
233
|
+
else:
|
|
234
|
+
click.echo(f"❌ Alert '{alert_id}' not found")
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@alerts.command()
|
|
239
|
+
@click.option("--interval", default=60, help="Check interval in seconds (default: 60)")
|
|
240
|
+
@click.option("--daemon", is_flag=True, help="Run as background daemon")
|
|
241
|
+
@click.option("--once", is_flag=True, help="Check once and exit")
|
|
242
|
+
def watch(interval: int, daemon: bool, once: bool):
|
|
243
|
+
"""Watch telemetry and trigger alerts when thresholds are exceeded."""
|
|
244
|
+
engine = get_alert_engine()
|
|
245
|
+
|
|
246
|
+
alerts_list = engine.list_alerts()
|
|
247
|
+
if not alerts_list:
|
|
248
|
+
click.echo("No alerts configured. Run 'empathy alerts init' first.")
|
|
249
|
+
sys.exit(1)
|
|
250
|
+
|
|
251
|
+
enabled_count = sum(1 for a in alerts_list if a.enabled)
|
|
252
|
+
click.echo(f"🔔 Monitoring {enabled_count} enabled alert(s)")
|
|
253
|
+
|
|
254
|
+
if once:
|
|
255
|
+
# Single check mode
|
|
256
|
+
events = engine.check_and_trigger()
|
|
257
|
+
if events:
|
|
258
|
+
click.echo(f"\n⚠️ {len(events)} alert(s) triggered!")
|
|
259
|
+
for event in events:
|
|
260
|
+
click.echo(f" - {event.alert_name}: {event.current_value:.2f} >= {event.threshold:.2f}")
|
|
261
|
+
else:
|
|
262
|
+
click.echo("✅ All metrics within thresholds")
|
|
263
|
+
return
|
|
264
|
+
|
|
250
265
|
if daemon:
|
|
251
266
|
click.echo("🔄 Starting alert watcher as daemon...")
|
|
252
|
-
click.echo("⚠️
|
|
253
|
-
|
|
254
|
-
|
|
267
|
+
click.echo("⚠️ Daemon mode runs in background. Use 'ps aux | grep empathy' to check status.")
|
|
268
|
+
# Daemonize
|
|
269
|
+
_daemonize()
|
|
270
|
+
|
|
271
|
+
click.echo(f"🔄 Starting alert watcher (checking every {interval}s)...")
|
|
272
|
+
click.echo(" Press Ctrl+C to stop\n")
|
|
273
|
+
|
|
274
|
+
# Set up signal handler for graceful shutdown
|
|
275
|
+
running = True
|
|
276
|
+
|
|
277
|
+
def signal_handler(sig, frame):
|
|
278
|
+
nonlocal running
|
|
279
|
+
running = False
|
|
280
|
+
click.echo("\n✓ Alert watcher stopped")
|
|
281
|
+
|
|
282
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
283
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
284
|
+
|
|
285
|
+
check_count = 0
|
|
286
|
+
triggered_count = 0
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
while running:
|
|
290
|
+
check_count += 1
|
|
291
|
+
events = engine.check_and_trigger()
|
|
292
|
+
|
|
293
|
+
if events:
|
|
294
|
+
triggered_count += len(events)
|
|
295
|
+
for event in events:
|
|
296
|
+
click.echo(f"⚠️ ALERT: {event.alert_name}")
|
|
297
|
+
click.echo(f" {event.metric.value}: {event.current_value:.2f} >= {event.threshold:.2f}")
|
|
298
|
+
|
|
299
|
+
# Status update every 5 checks
|
|
300
|
+
if check_count % 5 == 0:
|
|
301
|
+
click.echo(f" [Check #{check_count}] Monitoring... ({triggered_count} alerts triggered)")
|
|
302
|
+
|
|
303
|
+
time.sleep(interval)
|
|
304
|
+
except KeyboardInterrupt:
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
click.echo(f"\n📊 Summary: {check_count} checks, {triggered_count} alerts triggered")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _daemonize():
|
|
311
|
+
"""Daemonize the current process (Unix only)."""
|
|
312
|
+
import os
|
|
313
|
+
|
|
314
|
+
# Double fork to detach from terminal
|
|
315
|
+
try:
|
|
316
|
+
pid = os.fork()
|
|
317
|
+
if pid > 0:
|
|
318
|
+
# Parent exits
|
|
319
|
+
sys.exit(0)
|
|
320
|
+
except OSError as e:
|
|
321
|
+
click.echo(f"Fork #1 failed: {e}")
|
|
322
|
+
sys.exit(1)
|
|
323
|
+
|
|
324
|
+
# Decouple from parent environment
|
|
325
|
+
os.chdir("/")
|
|
326
|
+
os.setsid()
|
|
327
|
+
os.umask(0)
|
|
328
|
+
|
|
329
|
+
# Second fork
|
|
330
|
+
try:
|
|
331
|
+
pid = os.fork()
|
|
332
|
+
if pid > 0:
|
|
333
|
+
click.echo(f"Daemon started with PID: {pid}")
|
|
334
|
+
sys.exit(0)
|
|
335
|
+
except OSError as e:
|
|
336
|
+
click.echo(f"Fork #2 failed: {e}")
|
|
337
|
+
sys.exit(1)
|
|
338
|
+
|
|
339
|
+
# Redirect standard file descriptors
|
|
340
|
+
sys.stdout.flush()
|
|
341
|
+
sys.stderr.flush()
|
|
342
|
+
|
|
343
|
+
# Close file descriptors
|
|
344
|
+
with open("/dev/null", "rb", 0) as f:
|
|
345
|
+
os.dup2(f.fileno(), sys.stdin.fileno())
|
|
346
|
+
with open("/dev/null", "ab", 0) as f:
|
|
347
|
+
os.dup2(f.fileno(), sys.stdout.fileno())
|
|
348
|
+
with open("/dev/null", "ab", 0) as f:
|
|
349
|
+
os.dup2(f.fileno(), sys.stderr.fileno())
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@alerts.command()
|
|
353
|
+
@click.option("--alert-id", help="Filter by alert ID")
|
|
354
|
+
@click.option("--limit", default=20, help="Maximum records to show")
|
|
355
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
356
|
+
def history(alert_id: str | None, limit: int, as_json: bool):
|
|
357
|
+
"""View alert trigger history."""
|
|
358
|
+
engine = get_alert_engine()
|
|
359
|
+
records = engine.get_alert_history(alert_id=alert_id, limit=limit)
|
|
360
|
+
|
|
361
|
+
if not records:
|
|
362
|
+
if as_json:
|
|
363
|
+
click.echo("[]")
|
|
364
|
+
else:
|
|
365
|
+
click.echo("No alert history found.")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
if as_json:
|
|
369
|
+
import json
|
|
370
|
+
click.echo(json.dumps(records, indent=2))
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
click.echo("📜 Alert History:\n")
|
|
374
|
+
|
|
375
|
+
for record in records:
|
|
376
|
+
delivered = "✓" if record["delivered"] else "✗"
|
|
377
|
+
click.echo(f" [{delivered}] {record['alert_id']}")
|
|
378
|
+
click.echo(f" Metric: {record['metric']} = {record['current_value']:.2f} (threshold: {record['threshold']:.2f})")
|
|
379
|
+
click.echo(f" Severity: {record['severity']}")
|
|
380
|
+
click.echo(f" Triggered: {record['triggered_at']}")
|
|
381
|
+
if record.get("delivery_error"):
|
|
382
|
+
click.echo(f" Error: {record['delivery_error']}")
|
|
383
|
+
click.echo()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@alerts.command()
|
|
387
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
388
|
+
def metrics(as_json: bool):
|
|
389
|
+
"""View current telemetry metrics."""
|
|
390
|
+
engine = get_alert_engine()
|
|
391
|
+
current_metrics = engine.get_metrics()
|
|
392
|
+
|
|
393
|
+
if as_json:
|
|
394
|
+
import json
|
|
395
|
+
click.echo(json.dumps(current_metrics, indent=2))
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
click.echo("📊 Current Metrics (last 24 hours):\n")
|
|
399
|
+
|
|
400
|
+
metric_info = {
|
|
401
|
+
"daily_cost": ("Daily Cost", "USD"),
|
|
402
|
+
"error_rate": ("Error Rate", "%"),
|
|
403
|
+
"avg_latency": ("Avg Latency", "ms"),
|
|
404
|
+
"token_usage": ("Token Usage", "tokens"),
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for key, value in current_metrics.items():
|
|
408
|
+
name, unit = metric_info.get(key, (key, ""))
|
|
409
|
+
click.echo(f" {name}: {value:.2f} {unit}")
|
|
410
|
+
|
|
411
|
+
click.echo()
|
|
412
|
+
|
|
413
|
+
# Show alerts that would trigger
|
|
414
|
+
alerts_list = engine.list_alerts()
|
|
415
|
+
triggered = []
|
|
416
|
+
for alert in alerts_list:
|
|
417
|
+
if alert.enabled:
|
|
418
|
+
current = current_metrics.get(alert.metric.value, 0)
|
|
419
|
+
if current >= alert.threshold:
|
|
420
|
+
triggered.append((alert.name, current, alert.threshold))
|
|
421
|
+
|
|
422
|
+
if triggered:
|
|
423
|
+
click.echo("⚠️ Alerts that would trigger:")
|
|
424
|
+
for name, current, threshold in triggered:
|
|
425
|
+
click.echo(f" {name}: {current:.2f} >= {threshold:.2f}")
|
|
255
426
|
else:
|
|
256
|
-
click.echo("
|
|
257
|
-
click.echo("💡 Tip: Use VSCode extension for automatic monitoring")
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
while True:
|
|
261
|
-
# TODO: Check telemetry and trigger alerts
|
|
262
|
-
__import__("time").sleep(60)
|
|
263
|
-
except KeyboardInterrupt:
|
|
264
|
-
click.echo("\n✓ Alert watcher stopped")
|
|
427
|
+
click.echo("✅ All metrics within thresholds")
|
|
265
428
|
|
|
266
429
|
|
|
267
430
|
if __name__ == "__main__":
|