empathy-framework 3.5.6__py3-none-any.whl → 3.7.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.
- agents/compliance_anticipation_agent.py +113 -118
- agents/compliance_db.py +339 -0
- agents/epic_integration_wizard.py +37 -48
- agents/notifications.py +291 -0
- agents/trust_building_behaviors.py +66 -85
- coach_wizards/__init__.py +11 -12
- coach_wizards/accessibility_wizard.py +12 -12
- coach_wizards/api_wizard.py +12 -12
- coach_wizards/base_wizard.py +26 -20
- coach_wizards/cicd_wizard.py +15 -13
- coach_wizards/compliance_wizard.py +12 -12
- coach_wizards/database_wizard.py +12 -12
- coach_wizards/debugging_wizard.py +12 -12
- coach_wizards/documentation_wizard.py +12 -12
- coach_wizards/generate_wizards.py +1 -2
- coach_wizards/localization_wizard.py +21 -14
- coach_wizards/migration_wizard.py +12 -12
- coach_wizards/monitoring_wizard.py +12 -12
- coach_wizards/observability_wizard.py +12 -12
- coach_wizards/performance_wizard.py +12 -12
- coach_wizards/prompt_engineering_wizard.py +22 -25
- coach_wizards/refactoring_wizard.py +12 -12
- coach_wizards/scaling_wizard.py +12 -12
- coach_wizards/security_wizard.py +12 -12
- coach_wizards/testing_wizard.py +12 -12
- {empathy_framework-3.5.6.dist-info → empathy_framework-3.7.0.dist-info}/METADATA +234 -30
- empathy_framework-3.7.0.dist-info/RECORD +105 -0
- empathy_healthcare_plugin/__init__.py +1 -2
- empathy_llm_toolkit/__init__.py +5 -6
- empathy_llm_toolkit/claude_memory.py +14 -15
- empathy_llm_toolkit/code_health.py +27 -19
- empathy_llm_toolkit/contextual_patterns.py +11 -12
- empathy_llm_toolkit/core.py +43 -49
- empathy_llm_toolkit/git_pattern_extractor.py +16 -12
- empathy_llm_toolkit/levels.py +6 -13
- empathy_llm_toolkit/pattern_confidence.py +14 -18
- empathy_llm_toolkit/pattern_resolver.py +10 -12
- empathy_llm_toolkit/pattern_summary.py +13 -11
- empathy_llm_toolkit/providers.py +27 -38
- empathy_llm_toolkit/session_status.py +18 -20
- empathy_llm_toolkit/state.py +20 -21
- empathy_os/__init__.py +72 -73
- empathy_os/cli.py +193 -98
- empathy_os/cli_unified.py +68 -41
- empathy_os/config.py +31 -31
- empathy_os/coordination.py +48 -54
- empathy_os/core.py +90 -99
- empathy_os/cost_tracker.py +20 -23
- empathy_os/discovery.py +9 -11
- empathy_os/emergence.py +20 -21
- empathy_os/exceptions.py +18 -30
- empathy_os/feedback_loops.py +27 -30
- empathy_os/levels.py +31 -34
- empathy_os/leverage_points.py +27 -28
- empathy_os/logging_config.py +11 -12
- empathy_os/monitoring.py +27 -27
- empathy_os/pattern_library.py +29 -28
- empathy_os/persistence.py +30 -34
- empathy_os/platform_utils.py +46 -47
- empathy_os/redis_config.py +14 -15
- empathy_os/redis_memory.py +53 -56
- empathy_os/templates.py +12 -11
- empathy_os/trust_building.py +44 -36
- empathy_os/workflow_commands.py +123 -31
- empathy_software_plugin/__init__.py +1 -2
- empathy_software_plugin/cli.py +32 -25
- empathy_software_plugin/plugin.py +4 -8
- empathy_framework-3.5.6.dist-info/RECORD +0 -103
- {empathy_framework-3.5.6.dist-info → empathy_framework-3.7.0.dist-info}/WHEEL +0 -0
- {empathy_framework-3.5.6.dist-info → empathy_framework-3.7.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-3.5.6.dist-info → empathy_framework-3.7.0.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-3.5.6.dist-info → empathy_framework-3.7.0.dist-info}/top_level.txt +0 -0
agents/compliance_db.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Compliance Database with Append-Only Architecture.
|
|
2
|
+
|
|
3
|
+
Provides immutable audit trail for healthcare compliance tracking.
|
|
4
|
+
Supports INSERT operations only (no UPDATE/DELETE) for regulatory compliance.
|
|
5
|
+
|
|
6
|
+
Copyright 2025 Smart-AI-Memory
|
|
7
|
+
Licensed under Fair Source License 0.9
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sqlite3
|
|
11
|
+
from collections.abc import Generator
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ComplianceDatabase:
|
|
19
|
+
"""SQLite database for compliance tracking with append-only operations.
|
|
20
|
+
|
|
21
|
+
Features:
|
|
22
|
+
- Immutable audit trail (INSERT only, no UPDATE/DELETE)
|
|
23
|
+
- Audit date tracking
|
|
24
|
+
- Compliance status monitoring
|
|
25
|
+
- Gap detection and recording
|
|
26
|
+
- Thread-safe operations
|
|
27
|
+
|
|
28
|
+
Regulatory Compliance:
|
|
29
|
+
- Append-only design satisfies HIPAA audit log requirements
|
|
30
|
+
- No modification of historical records
|
|
31
|
+
- Complete audit trail preservation
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, db_path: str | None = None):
|
|
35
|
+
"""Initialize compliance database.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
db_path: Path to SQLite database file.
|
|
39
|
+
Defaults to agents/data/compliance.db
|
|
40
|
+
"""
|
|
41
|
+
if db_path is None:
|
|
42
|
+
# Default to agents/data/compliance.db
|
|
43
|
+
agents_dir = Path(__file__).parent
|
|
44
|
+
data_dir = agents_dir / "data"
|
|
45
|
+
data_dir.mkdir(exist_ok=True)
|
|
46
|
+
db_path = str(data_dir / "compliance.db")
|
|
47
|
+
|
|
48
|
+
self.db_path = db_path
|
|
49
|
+
self._init_schema()
|
|
50
|
+
|
|
51
|
+
@contextmanager
|
|
52
|
+
def _get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
|
53
|
+
"""Get database connection with automatic cleanup."""
|
|
54
|
+
conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
55
|
+
conn.row_factory = sqlite3.Row # Enable dict-like access
|
|
56
|
+
try:
|
|
57
|
+
yield conn
|
|
58
|
+
conn.commit()
|
|
59
|
+
except Exception:
|
|
60
|
+
conn.rollback()
|
|
61
|
+
raise
|
|
62
|
+
finally:
|
|
63
|
+
conn.close()
|
|
64
|
+
|
|
65
|
+
def _init_schema(self) -> None:
|
|
66
|
+
"""Initialize database schema if not exists."""
|
|
67
|
+
with self._get_connection() as conn:
|
|
68
|
+
conn.executescript(
|
|
69
|
+
"""
|
|
70
|
+
CREATE TABLE IF NOT EXISTS compliance_audits (
|
|
71
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
+
audit_date TIMESTAMP NOT NULL,
|
|
73
|
+
audit_type TEXT NOT NULL, -- 'HIPAA', 'GDPR', 'SOC2', etc.
|
|
74
|
+
findings TEXT, -- JSON string of findings
|
|
75
|
+
risk_score INTEGER, -- 0-100
|
|
76
|
+
auditor TEXT, -- Who performed the audit
|
|
77
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
78
|
+
-- No updated_at field (immutable records)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS compliance_gaps (
|
|
82
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
gap_type TEXT NOT NULL, -- 'missing_policy', 'expired_cert', etc.
|
|
84
|
+
severity TEXT NOT NULL, -- 'critical', 'high', 'medium', 'low'
|
|
85
|
+
detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
86
|
+
description TEXT,
|
|
87
|
+
affected_systems TEXT, -- JSON string of affected systems
|
|
88
|
+
compliance_framework TEXT, -- 'HIPAA', 'GDPR', etc.
|
|
89
|
+
detection_source TEXT -- 'automated_scan', 'manual_review', etc.
|
|
90
|
+
-- No status field (can't mark as "fixed", only add new record showing fix)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
CREATE TABLE IF NOT EXISTS compliance_status (
|
|
94
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
95
|
+
compliance_framework TEXT NOT NULL, -- 'HIPAA', 'GDPR', 'SOC2', etc.
|
|
96
|
+
status TEXT NOT NULL, -- 'compliant', 'non_compliant', 'pending'
|
|
97
|
+
effective_date TIMESTAMP NOT NULL,
|
|
98
|
+
notes TEXT,
|
|
99
|
+
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_audits_date ON compliance_audits(audit_date DESC);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_gaps_severity ON compliance_gaps(severity, detected_at DESC);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_status_framework ON compliance_status(compliance_framework, effective_date DESC);
|
|
105
|
+
"""
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def record_audit(
|
|
109
|
+
self,
|
|
110
|
+
audit_date: datetime,
|
|
111
|
+
audit_type: str,
|
|
112
|
+
findings: str | None = None,
|
|
113
|
+
risk_score: int | None = None,
|
|
114
|
+
auditor: str | None = None,
|
|
115
|
+
) -> int:
|
|
116
|
+
"""Record a compliance audit (append-only).
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
audit_date: When the audit was performed
|
|
120
|
+
audit_type: Type of audit ('HIPAA', 'GDPR', 'SOC2', etc.)
|
|
121
|
+
findings: JSON string of audit findings
|
|
122
|
+
risk_score: Risk score 0-100
|
|
123
|
+
auditor: Who performed the audit
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Audit record ID
|
|
127
|
+
|
|
128
|
+
Note:
|
|
129
|
+
This is an append-only operation. Cannot modify existing audits.
|
|
130
|
+
"""
|
|
131
|
+
with self._get_connection() as conn:
|
|
132
|
+
cursor = conn.execute(
|
|
133
|
+
"""
|
|
134
|
+
INSERT INTO compliance_audits (audit_date, audit_type, findings, risk_score, auditor)
|
|
135
|
+
VALUES (?, ?, ?, ?, ?)
|
|
136
|
+
""",
|
|
137
|
+
(audit_date, audit_type, findings, risk_score, auditor),
|
|
138
|
+
)
|
|
139
|
+
return cursor.lastrowid
|
|
140
|
+
|
|
141
|
+
def get_last_audit(self, audit_type: str | None = None) -> dict[str, Any] | None:
|
|
142
|
+
"""Get most recent audit record (read-only).
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
audit_type: Optional filter by audit type
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Audit record dict or None if no audits found
|
|
149
|
+
"""
|
|
150
|
+
with self._get_connection() as conn:
|
|
151
|
+
if audit_type:
|
|
152
|
+
cursor = conn.execute(
|
|
153
|
+
"""
|
|
154
|
+
SELECT * FROM compliance_audits
|
|
155
|
+
WHERE audit_type = ?
|
|
156
|
+
ORDER BY audit_date DESC
|
|
157
|
+
LIMIT 1
|
|
158
|
+
""",
|
|
159
|
+
(audit_type,),
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
cursor = conn.execute(
|
|
163
|
+
"""
|
|
164
|
+
SELECT * FROM compliance_audits
|
|
165
|
+
ORDER BY audit_date DESC
|
|
166
|
+
LIMIT 1
|
|
167
|
+
"""
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
row = cursor.fetchone()
|
|
171
|
+
if row is None:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"id": row["id"],
|
|
176
|
+
"audit_date": row["audit_date"],
|
|
177
|
+
"audit_type": row["audit_type"],
|
|
178
|
+
"findings": row["findings"],
|
|
179
|
+
"risk_score": row["risk_score"],
|
|
180
|
+
"auditor": row["auditor"],
|
|
181
|
+
"created_at": row["created_at"],
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def record_gap(
|
|
185
|
+
self,
|
|
186
|
+
gap_type: str,
|
|
187
|
+
severity: str,
|
|
188
|
+
description: str | None = None,
|
|
189
|
+
affected_systems: str | None = None,
|
|
190
|
+
compliance_framework: str | None = None,
|
|
191
|
+
detection_source: str = "automated_scan",
|
|
192
|
+
) -> int:
|
|
193
|
+
"""Record a compliance gap (append-only).
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
gap_type: Type of gap ('missing_policy', 'expired_cert', etc.)
|
|
197
|
+
severity: Severity level ('critical', 'high', 'medium', 'low')
|
|
198
|
+
description: Human-readable description
|
|
199
|
+
affected_systems: JSON string of affected systems
|
|
200
|
+
compliance_framework: Related framework ('HIPAA', 'GDPR', etc.)
|
|
201
|
+
detection_source: How gap was detected
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Gap record ID
|
|
205
|
+
|
|
206
|
+
Note:
|
|
207
|
+
This is an append-only operation. To mark a gap as fixed,
|
|
208
|
+
add a new status record, don't modify this one.
|
|
209
|
+
"""
|
|
210
|
+
with self._get_connection() as conn:
|
|
211
|
+
cursor = conn.execute(
|
|
212
|
+
"""
|
|
213
|
+
INSERT INTO compliance_gaps (
|
|
214
|
+
gap_type, severity, description, affected_systems,
|
|
215
|
+
compliance_framework, detection_source
|
|
216
|
+
)
|
|
217
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
218
|
+
""",
|
|
219
|
+
(
|
|
220
|
+
gap_type,
|
|
221
|
+
severity,
|
|
222
|
+
description,
|
|
223
|
+
affected_systems,
|
|
224
|
+
compliance_framework,
|
|
225
|
+
detection_source,
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
return cursor.lastrowid
|
|
229
|
+
|
|
230
|
+
def get_active_gaps(
|
|
231
|
+
self, severity: str | None = None, framework: str | None = None
|
|
232
|
+
) -> list[dict[str, Any]]:
|
|
233
|
+
"""Get all recorded gaps (read-only).
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
severity: Optional filter by severity
|
|
237
|
+
framework: Optional filter by compliance framework
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
List of gap records
|
|
241
|
+
|
|
242
|
+
Note:
|
|
243
|
+
Returns all gaps. In append-only design, gaps are never deleted.
|
|
244
|
+
To track fixes, use separate status records.
|
|
245
|
+
"""
|
|
246
|
+
with self._get_connection() as conn:
|
|
247
|
+
query = "SELECT * FROM compliance_gaps WHERE 1=1"
|
|
248
|
+
params: list[Any] = []
|
|
249
|
+
|
|
250
|
+
if severity:
|
|
251
|
+
query += " AND severity = ?"
|
|
252
|
+
params.append(severity)
|
|
253
|
+
|
|
254
|
+
if framework:
|
|
255
|
+
query += " AND compliance_framework = ?"
|
|
256
|
+
params.append(framework)
|
|
257
|
+
|
|
258
|
+
query += " ORDER BY detected_at DESC"
|
|
259
|
+
|
|
260
|
+
cursor = conn.execute(query, params)
|
|
261
|
+
rows = cursor.fetchall()
|
|
262
|
+
|
|
263
|
+
return [
|
|
264
|
+
{
|
|
265
|
+
"id": row["id"],
|
|
266
|
+
"gap_type": row["gap_type"],
|
|
267
|
+
"severity": row["severity"],
|
|
268
|
+
"detected_at": row["detected_at"],
|
|
269
|
+
"description": row["description"],
|
|
270
|
+
"affected_systems": row["affected_systems"],
|
|
271
|
+
"compliance_framework": row["compliance_framework"],
|
|
272
|
+
"detection_source": row["detection_source"],
|
|
273
|
+
}
|
|
274
|
+
for row in rows
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
def record_compliance_status(
|
|
278
|
+
self,
|
|
279
|
+
compliance_framework: str,
|
|
280
|
+
status: str,
|
|
281
|
+
effective_date: datetime,
|
|
282
|
+
notes: str | None = None,
|
|
283
|
+
) -> int:
|
|
284
|
+
"""Record compliance status change (append-only).
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
compliance_framework: Framework name ('HIPAA', 'GDPR', 'SOC2', etc.)
|
|
288
|
+
status: Status ('compliant', 'non_compliant', 'pending')
|
|
289
|
+
effective_date: When this status became effective
|
|
290
|
+
notes: Additional notes
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Status record ID
|
|
294
|
+
|
|
295
|
+
Note:
|
|
296
|
+
This is an append-only operation. Status history is preserved.
|
|
297
|
+
"""
|
|
298
|
+
with self._get_connection() as conn:
|
|
299
|
+
cursor = conn.execute(
|
|
300
|
+
"""
|
|
301
|
+
INSERT INTO compliance_status (compliance_framework, status, effective_date, notes)
|
|
302
|
+
VALUES (?, ?, ?, ?)
|
|
303
|
+
""",
|
|
304
|
+
(compliance_framework, status, effective_date, notes),
|
|
305
|
+
)
|
|
306
|
+
return cursor.lastrowid
|
|
307
|
+
|
|
308
|
+
def get_current_compliance_status(self, compliance_framework: str) -> dict[str, Any] | None:
|
|
309
|
+
"""Get most recent compliance status (read-only).
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
compliance_framework: Framework name
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Status record or None
|
|
316
|
+
"""
|
|
317
|
+
with self._get_connection() as conn:
|
|
318
|
+
cursor = conn.execute(
|
|
319
|
+
"""
|
|
320
|
+
SELECT * FROM compliance_status
|
|
321
|
+
WHERE compliance_framework = ?
|
|
322
|
+
ORDER BY effective_date DESC, recorded_at DESC
|
|
323
|
+
LIMIT 1
|
|
324
|
+
""",
|
|
325
|
+
(compliance_framework,),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
row = cursor.fetchone()
|
|
329
|
+
if row is None:
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
"id": row["id"],
|
|
334
|
+
"compliance_framework": row["compliance_framework"],
|
|
335
|
+
"status": row["status"],
|
|
336
|
+
"effective_date": row["effective_date"],
|
|
337
|
+
"notes": row["notes"],
|
|
338
|
+
"recorded_at": row["recorded_at"],
|
|
339
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Epic Integration Wizard - LangChain Agent
|
|
1
|
+
"""Epic Integration Wizard - LangChain Agent
|
|
3
2
|
Multi-step wizard for configuring and testing Epic FHIR integration
|
|
4
3
|
|
|
5
4
|
Copyright 2025 Smart AI Memory, LLC
|
|
@@ -26,8 +25,7 @@ logger = logging.getLogger(__name__)
|
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
class WizardState(TypedDict):
|
|
29
|
-
"""
|
|
30
|
-
State for Epic Integration Wizard
|
|
28
|
+
"""State for Epic Integration Wizard
|
|
31
29
|
|
|
32
30
|
Tracks all wizard steps, user inputs, validation results, and progress.
|
|
33
31
|
Microsoft-style linear progression with validation at each step.
|
|
@@ -114,8 +112,7 @@ def create_initial_state() -> WizardState:
|
|
|
114
112
|
|
|
115
113
|
|
|
116
114
|
async def step_1_prerequisites(state: WizardState) -> WizardState:
|
|
117
|
-
"""
|
|
118
|
-
Step 1: Check Prerequisites
|
|
115
|
+
"""Step 1: Check Prerequisites
|
|
119
116
|
|
|
120
117
|
Validates system readiness:
|
|
121
118
|
- Python version
|
|
@@ -142,7 +139,7 @@ async def step_1_prerequisites(state: WizardState) -> WizardState:
|
|
|
142
139
|
if not settings.database_url:
|
|
143
140
|
missing.append("Database configuration")
|
|
144
141
|
except Exception as e:
|
|
145
|
-
missing.append(f"Configuration error: {
|
|
142
|
+
missing.append(f"Configuration error: {e!s}")
|
|
146
143
|
|
|
147
144
|
state["prerequisites_checked"] = True
|
|
148
145
|
state["prerequisites_passed"] = len(missing) == 0
|
|
@@ -153,7 +150,7 @@ async def step_1_prerequisites(state: WizardState) -> WizardState:
|
|
|
153
150
|
# Even if automated prerequisites fail, user can still configure Epic manually
|
|
154
151
|
if state["prerequisites_passed"]:
|
|
155
152
|
state["messages"].append(
|
|
156
|
-
AIMessage(content="✅ All prerequisites met. Ready to configure Epic integration.")
|
|
153
|
+
AIMessage(content="✅ All prerequisites met. Ready to configure Epic integration."),
|
|
157
154
|
)
|
|
158
155
|
else:
|
|
159
156
|
# Don't block progression - just warn user
|
|
@@ -164,8 +161,7 @@ async def step_1_prerequisites(state: WizardState) -> WizardState:
|
|
|
164
161
|
|
|
165
162
|
|
|
166
163
|
async def step_2_credentials(state: WizardState) -> WizardState:
|
|
167
|
-
"""
|
|
168
|
-
Step 2: Epic Credentials Input
|
|
164
|
+
"""Step 2: Epic Credentials Input
|
|
169
165
|
|
|
170
166
|
Collects and validates:
|
|
171
167
|
- Epic Client ID
|
|
@@ -192,15 +188,14 @@ async def step_2_credentials(state: WizardState) -> WizardState:
|
|
|
192
188
|
state["completed_steps"].append(2)
|
|
193
189
|
state["current_step"] = 3
|
|
194
190
|
state["messages"].append(
|
|
195
|
-
AIMessage(content="✅ Epic credentials collected. Ready to test connection.")
|
|
191
|
+
AIMessage(content="✅ Epic credentials collected. Ready to test connection."),
|
|
196
192
|
)
|
|
197
193
|
|
|
198
194
|
return state
|
|
199
195
|
|
|
200
196
|
|
|
201
197
|
async def step_3_connection_test(state: WizardState) -> WizardState:
|
|
202
|
-
"""
|
|
203
|
-
Step 3: Test Epic FHIR Connection
|
|
198
|
+
"""Step 3: Test Epic FHIR Connection
|
|
204
199
|
|
|
205
200
|
Validates:
|
|
206
201
|
- OAuth token can be obtained
|
|
@@ -226,13 +221,13 @@ async def step_3_connection_test(state: WizardState) -> WizardState:
|
|
|
226
221
|
state["oauth_token_obtained"] = True
|
|
227
222
|
state["connection_test_passed"] = True
|
|
228
223
|
state["messages"].append(
|
|
229
|
-
AIMessage(content="✅ Successfully connected to Epic FHIR API")
|
|
224
|
+
AIMessage(content="✅ Successfully connected to Epic FHIR API"),
|
|
230
225
|
)
|
|
231
226
|
except Exception as e:
|
|
232
227
|
state["connection_test_error"] = str(e)
|
|
233
228
|
state["connection_test_passed"] = False
|
|
234
|
-
state["errors"].append(f"OAuth token request failed: {
|
|
235
|
-
state["messages"].append(AIMessage(content=f"❌ Connection failed: {
|
|
229
|
+
state["errors"].append(f"OAuth token request failed: {e!s}")
|
|
230
|
+
state["messages"].append(AIMessage(content=f"❌ Connection failed: {e!s}"))
|
|
236
231
|
return state
|
|
237
232
|
|
|
238
233
|
state["completed_steps"].append(3)
|
|
@@ -242,14 +237,13 @@ async def step_3_connection_test(state: WizardState) -> WizardState:
|
|
|
242
237
|
logger.error(f"Connection test failed: {e}", exc_info=True)
|
|
243
238
|
state["connection_test_error"] = str(e)
|
|
244
239
|
state["connection_test_passed"] = False
|
|
245
|
-
state["errors"].append(f"Connection test error: {
|
|
240
|
+
state["errors"].append(f"Connection test error: {e!s}")
|
|
246
241
|
|
|
247
242
|
return state
|
|
248
243
|
|
|
249
244
|
|
|
250
245
|
async def step_4_resource_permissions(state: WizardState) -> WizardState:
|
|
251
|
-
"""
|
|
252
|
-
Step 4: Select FHIR Resources and Scopes
|
|
246
|
+
"""Step 4: Select FHIR Resources and Scopes
|
|
253
247
|
|
|
254
248
|
User selects which FHIR resources to enable:
|
|
255
249
|
- Patient (demographics)
|
|
@@ -284,15 +278,14 @@ async def step_4_resource_permissions(state: WizardState) -> WizardState:
|
|
|
284
278
|
state["completed_steps"].append(4)
|
|
285
279
|
state["current_step"] = 5
|
|
286
280
|
state["messages"].append(
|
|
287
|
-
AIMessage(content=f"✅ Configured {len(scopes)} resource permissions: {', '.join(scopes)}")
|
|
281
|
+
AIMessage(content=f"✅ Configured {len(scopes)} resource permissions: {', '.join(scopes)}"),
|
|
288
282
|
)
|
|
289
283
|
|
|
290
284
|
return state
|
|
291
285
|
|
|
292
286
|
|
|
293
287
|
async def step_5_test_patient_lookup(state: WizardState) -> WizardState:
|
|
294
|
-
"""
|
|
295
|
-
Step 5: Test Patient Lookup
|
|
288
|
+
"""Step 5: Test Patient Lookup
|
|
296
289
|
|
|
297
290
|
Validates end-to-end functionality:
|
|
298
291
|
- Search patient by MRN
|
|
@@ -316,7 +309,8 @@ async def step_5_test_patient_lookup(state: WizardState) -> WizardState:
|
|
|
316
309
|
)
|
|
317
310
|
|
|
318
311
|
epic_client = EpicFHIRClient(
|
|
319
|
-
base_url=state["epic_fhir_base_url"],
|
|
312
|
+
base_url=state["epic_fhir_base_url"],
|
|
313
|
+
oauth_manager=oauth_manager,
|
|
320
314
|
)
|
|
321
315
|
|
|
322
316
|
# Search for patient
|
|
@@ -332,13 +326,13 @@ async def step_5_test_patient_lookup(state: WizardState) -> WizardState:
|
|
|
332
326
|
state["patient_data_retrieved"] = True
|
|
333
327
|
state["retrieved_patient_name"] = patient_name
|
|
334
328
|
state["messages"].append(
|
|
335
|
-
AIMessage(content=f"✅ Successfully retrieved patient: {patient_name}")
|
|
329
|
+
AIMessage(content=f"✅ Successfully retrieved patient: {patient_name}"),
|
|
336
330
|
)
|
|
337
331
|
else:
|
|
338
332
|
state["patient_test_error"] = "Patient not found"
|
|
339
333
|
state["errors"].append(f"Patient with MRN {state['test_mrn']} not found")
|
|
340
334
|
state["messages"].append(
|
|
341
|
-
AIMessage(content=f"❌ Patient not found: MRN {state['test_mrn']}")
|
|
335
|
+
AIMessage(content=f"❌ Patient not found: MRN {state['test_mrn']}"),
|
|
342
336
|
)
|
|
343
337
|
return state
|
|
344
338
|
|
|
@@ -348,15 +342,14 @@ async def step_5_test_patient_lookup(state: WizardState) -> WizardState:
|
|
|
348
342
|
except Exception as e:
|
|
349
343
|
logger.error(f"Patient lookup failed: {e}", exc_info=True)
|
|
350
344
|
state["patient_test_error"] = str(e)
|
|
351
|
-
state["errors"].append(f"Patient lookup error: {
|
|
352
|
-
state["messages"].append(AIMessage(content=f"❌ Patient lookup failed: {
|
|
345
|
+
state["errors"].append(f"Patient lookup error: {e!s}")
|
|
346
|
+
state["messages"].append(AIMessage(content=f"❌ Patient lookup failed: {e!s}"))
|
|
353
347
|
|
|
354
348
|
return state
|
|
355
349
|
|
|
356
350
|
|
|
357
351
|
async def step_6_review_confirm(state: WizardState) -> WizardState:
|
|
358
|
-
"""
|
|
359
|
-
Step 6: Review and Confirm Configuration
|
|
352
|
+
"""Step 6: Review and Confirm Configuration
|
|
360
353
|
|
|
361
354
|
Shows summary of all settings for final review:
|
|
362
355
|
- Epic endpoint and credentials
|
|
@@ -390,15 +383,14 @@ Please confirm to activate integration.
|
|
|
390
383
|
state["completed_steps"].append(6)
|
|
391
384
|
state["current_step"] = 7
|
|
392
385
|
state["messages"].append(
|
|
393
|
-
AIMessage(content="✅ Configuration confirmed. Activating integration...")
|
|
386
|
+
AIMessage(content="✅ Configuration confirmed. Activating integration..."),
|
|
394
387
|
)
|
|
395
388
|
|
|
396
389
|
return state
|
|
397
390
|
|
|
398
391
|
|
|
399
392
|
async def step_7_complete(state: WizardState) -> WizardState:
|
|
400
|
-
"""
|
|
401
|
-
Step 7: Complete Integration Setup
|
|
393
|
+
"""Step 7: Complete Integration Setup
|
|
402
394
|
|
|
403
395
|
Finalizes configuration:
|
|
404
396
|
- Saves settings to database/config
|
|
@@ -442,14 +434,14 @@ Next steps:
|
|
|
442
434
|
1. Train your staff on the Epic integration workflow
|
|
443
435
|
2. Review HIPAA compliance documentation
|
|
444
436
|
3. Monitor integration logs for any issues
|
|
445
|
-
"""
|
|
446
|
-
)
|
|
437
|
+
""",
|
|
438
|
+
),
|
|
447
439
|
)
|
|
448
440
|
|
|
449
441
|
except Exception as e:
|
|
450
442
|
logger.error(f"Integration activation failed: {e}", exc_info=True)
|
|
451
|
-
state["errors"].append(f"Activation error: {
|
|
452
|
-
state["messages"].append(AIMessage(content=f"❌ Activation failed: {
|
|
443
|
+
state["errors"].append(f"Activation error: {e!s}")
|
|
444
|
+
state["messages"].append(AIMessage(content=f"❌ Activation failed: {e!s}"))
|
|
453
445
|
|
|
454
446
|
return state
|
|
455
447
|
|
|
@@ -460,8 +452,7 @@ Next steps:
|
|
|
460
452
|
|
|
461
453
|
|
|
462
454
|
def should_continue(state: WizardState) -> str:
|
|
463
|
-
"""
|
|
464
|
-
Determine next step based on current state
|
|
455
|
+
"""Determine next step based on current state
|
|
465
456
|
|
|
466
457
|
Microsoft wizard pattern: linear progression with error handling
|
|
467
458
|
"""
|
|
@@ -482,20 +473,19 @@ def should_continue(state: WizardState) -> str:
|
|
|
482
473
|
|
|
483
474
|
if current_step == 1:
|
|
484
475
|
return "prerequisites"
|
|
485
|
-
|
|
476
|
+
if current_step == 2:
|
|
486
477
|
return "credentials"
|
|
487
|
-
|
|
478
|
+
if current_step == 3:
|
|
488
479
|
return "connection_test"
|
|
489
|
-
|
|
480
|
+
if current_step == 4:
|
|
490
481
|
return "resource_permissions"
|
|
491
|
-
|
|
482
|
+
if current_step == 5:
|
|
492
483
|
return "test_patient_lookup"
|
|
493
|
-
|
|
484
|
+
if current_step == 6:
|
|
494
485
|
return "review_confirm"
|
|
495
|
-
|
|
486
|
+
if current_step == 7:
|
|
496
487
|
return "complete"
|
|
497
|
-
|
|
498
|
-
return END
|
|
488
|
+
return END
|
|
499
489
|
|
|
500
490
|
|
|
501
491
|
# =============================================================================
|
|
@@ -504,8 +494,7 @@ def should_continue(state: WizardState) -> str:
|
|
|
504
494
|
|
|
505
495
|
|
|
506
496
|
def create_epic_wizard_graph():
|
|
507
|
-
"""
|
|
508
|
-
Create LangGraph workflow for Epic Integration Wizard
|
|
497
|
+
"""Create LangGraph workflow for Epic Integration Wizard
|
|
509
498
|
|
|
510
499
|
Microsoft-style multi-step wizard:
|
|
511
500
|
Prerequisites → Credentials → Connection Test → Permissions → Patient Test → Review → Complete
|