empathy-framework 3.7.0__py3-none-any.whl → 3.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.
Files changed (267) hide show
  1. coach_wizards/code_reviewer_README.md +60 -0
  2. coach_wizards/code_reviewer_wizard.py +180 -0
  3. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/METADATA +20 -2
  4. empathy_framework-3.7.1.dist-info/RECORD +327 -0
  5. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/top_level.txt +5 -1
  6. empathy_healthcare_plugin/monitors/__init__.py +9 -0
  7. empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py +315 -0
  8. empathy_healthcare_plugin/monitors/monitoring/__init__.py +44 -0
  9. empathy_healthcare_plugin/monitors/monitoring/protocol_checker.py +300 -0
  10. empathy_healthcare_plugin/monitors/monitoring/protocol_loader.py +214 -0
  11. empathy_healthcare_plugin/monitors/monitoring/sensor_parsers.py +306 -0
  12. empathy_healthcare_plugin/monitors/monitoring/trajectory_analyzer.py +389 -0
  13. empathy_llm_toolkit/agent_factory/__init__.py +53 -0
  14. empathy_llm_toolkit/agent_factory/adapters/__init__.py +85 -0
  15. empathy_llm_toolkit/agent_factory/adapters/autogen_adapter.py +312 -0
  16. empathy_llm_toolkit/agent_factory/adapters/crewai_adapter.py +454 -0
  17. empathy_llm_toolkit/agent_factory/adapters/haystack_adapter.py +298 -0
  18. empathy_llm_toolkit/agent_factory/adapters/langchain_adapter.py +362 -0
  19. empathy_llm_toolkit/agent_factory/adapters/langgraph_adapter.py +333 -0
  20. empathy_llm_toolkit/agent_factory/adapters/native.py +228 -0
  21. empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +426 -0
  22. empathy_llm_toolkit/agent_factory/base.py +305 -0
  23. empathy_llm_toolkit/agent_factory/crews/__init__.py +67 -0
  24. empathy_llm_toolkit/agent_factory/crews/code_review.py +1113 -0
  25. empathy_llm_toolkit/agent_factory/crews/health_check.py +1246 -0
  26. empathy_llm_toolkit/agent_factory/crews/refactoring.py +1128 -0
  27. empathy_llm_toolkit/agent_factory/crews/security_audit.py +1018 -0
  28. empathy_llm_toolkit/agent_factory/decorators.py +286 -0
  29. empathy_llm_toolkit/agent_factory/factory.py +558 -0
  30. empathy_llm_toolkit/agent_factory/framework.py +192 -0
  31. empathy_llm_toolkit/agent_factory/memory_integration.py +324 -0
  32. empathy_llm_toolkit/agent_factory/resilient.py +320 -0
  33. empathy_llm_toolkit/cli/__init__.py +8 -0
  34. empathy_llm_toolkit/cli/sync_claude.py +487 -0
  35. empathy_llm_toolkit/code_health.py +150 -3
  36. empathy_llm_toolkit/config/__init__.py +29 -0
  37. empathy_llm_toolkit/config/unified.py +295 -0
  38. empathy_llm_toolkit/routing/__init__.py +32 -0
  39. empathy_llm_toolkit/routing/model_router.py +362 -0
  40. empathy_llm_toolkit/security/IMPLEMENTATION_SUMMARY.md +413 -0
  41. empathy_llm_toolkit/security/PHASE2_COMPLETE.md +384 -0
  42. empathy_llm_toolkit/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  43. empathy_llm_toolkit/security/QUICK_REFERENCE.md +316 -0
  44. empathy_llm_toolkit/security/README.md +262 -0
  45. empathy_llm_toolkit/security/__init__.py +62 -0
  46. empathy_llm_toolkit/security/audit_logger.py +929 -0
  47. empathy_llm_toolkit/security/audit_logger_example.py +152 -0
  48. empathy_llm_toolkit/security/pii_scrubber.py +640 -0
  49. empathy_llm_toolkit/security/secrets_detector.py +678 -0
  50. empathy_llm_toolkit/security/secrets_detector_example.py +304 -0
  51. empathy_llm_toolkit/security/secure_memdocs.py +1192 -0
  52. empathy_llm_toolkit/security/secure_memdocs_example.py +278 -0
  53. empathy_llm_toolkit/wizards/__init__.py +38 -0
  54. empathy_llm_toolkit/wizards/base_wizard.py +364 -0
  55. empathy_llm_toolkit/wizards/customer_support_wizard.py +190 -0
  56. empathy_llm_toolkit/wizards/healthcare_wizard.py +362 -0
  57. empathy_llm_toolkit/wizards/patient_assessment_README.md +64 -0
  58. empathy_llm_toolkit/wizards/patient_assessment_wizard.py +193 -0
  59. empathy_llm_toolkit/wizards/technology_wizard.py +194 -0
  60. empathy_os/__init__.py +52 -52
  61. empathy_os/adaptive/__init__.py +13 -0
  62. empathy_os/adaptive/task_complexity.py +127 -0
  63. empathy_os/cli.py +118 -8
  64. empathy_os/cli_unified.py +121 -1
  65. empathy_os/config/__init__.py +63 -0
  66. empathy_os/config/xml_config.py +239 -0
  67. empathy_os/dashboard/__init__.py +15 -0
  68. empathy_os/dashboard/server.py +743 -0
  69. empathy_os/memory/__init__.py +195 -0
  70. empathy_os/memory/claude_memory.py +466 -0
  71. empathy_os/memory/config.py +224 -0
  72. empathy_os/memory/control_panel.py +1298 -0
  73. empathy_os/memory/edges.py +179 -0
  74. empathy_os/memory/graph.py +567 -0
  75. empathy_os/memory/long_term.py +1193 -0
  76. empathy_os/memory/nodes.py +179 -0
  77. empathy_os/memory/redis_bootstrap.py +540 -0
  78. empathy_os/memory/security/__init__.py +31 -0
  79. empathy_os/memory/security/audit_logger.py +930 -0
  80. empathy_os/memory/security/pii_scrubber.py +640 -0
  81. empathy_os/memory/security/secrets_detector.py +678 -0
  82. empathy_os/memory/short_term.py +2119 -0
  83. empathy_os/memory/storage/__init__.py +15 -0
  84. empathy_os/memory/summary_index.py +583 -0
  85. empathy_os/memory/unified.py +619 -0
  86. empathy_os/metrics/__init__.py +12 -0
  87. empathy_os/metrics/prompt_metrics.py +190 -0
  88. empathy_os/models/__init__.py +136 -0
  89. empathy_os/models/__main__.py +13 -0
  90. empathy_os/models/cli.py +655 -0
  91. empathy_os/models/empathy_executor.py +354 -0
  92. empathy_os/models/executor.py +252 -0
  93. empathy_os/models/fallback.py +671 -0
  94. empathy_os/models/provider_config.py +563 -0
  95. empathy_os/models/registry.py +382 -0
  96. empathy_os/models/tasks.py +302 -0
  97. empathy_os/models/telemetry.py +548 -0
  98. empathy_os/models/token_estimator.py +378 -0
  99. empathy_os/models/validation.py +274 -0
  100. empathy_os/monitoring/__init__.py +52 -0
  101. empathy_os/monitoring/alerts.py +23 -0
  102. empathy_os/monitoring/alerts_cli.py +268 -0
  103. empathy_os/monitoring/multi_backend.py +271 -0
  104. empathy_os/monitoring/otel_backend.py +363 -0
  105. empathy_os/optimization/__init__.py +19 -0
  106. empathy_os/optimization/context_optimizer.py +272 -0
  107. empathy_os/plugins/__init__.py +28 -0
  108. empathy_os/plugins/base.py +361 -0
  109. empathy_os/plugins/registry.py +268 -0
  110. empathy_os/project_index/__init__.py +30 -0
  111. empathy_os/project_index/cli.py +335 -0
  112. empathy_os/project_index/crew_integration.py +430 -0
  113. empathy_os/project_index/index.py +425 -0
  114. empathy_os/project_index/models.py +501 -0
  115. empathy_os/project_index/reports.py +473 -0
  116. empathy_os/project_index/scanner.py +538 -0
  117. empathy_os/prompts/__init__.py +61 -0
  118. empathy_os/prompts/config.py +77 -0
  119. empathy_os/prompts/context.py +177 -0
  120. empathy_os/prompts/parser.py +285 -0
  121. empathy_os/prompts/registry.py +313 -0
  122. empathy_os/prompts/templates.py +208 -0
  123. empathy_os/resilience/__init__.py +56 -0
  124. empathy_os/resilience/circuit_breaker.py +256 -0
  125. empathy_os/resilience/fallback.py +179 -0
  126. empathy_os/resilience/health.py +300 -0
  127. empathy_os/resilience/retry.py +209 -0
  128. empathy_os/resilience/timeout.py +135 -0
  129. empathy_os/routing/__init__.py +43 -0
  130. empathy_os/routing/chain_executor.py +433 -0
  131. empathy_os/routing/classifier.py +217 -0
  132. empathy_os/routing/smart_router.py +234 -0
  133. empathy_os/routing/wizard_registry.py +307 -0
  134. empathy_os/trust/__init__.py +28 -0
  135. empathy_os/trust/circuit_breaker.py +579 -0
  136. empathy_os/validation/__init__.py +19 -0
  137. empathy_os/validation/xml_validator.py +281 -0
  138. empathy_os/wizard_factory_cli.py +170 -0
  139. empathy_os/workflows/__init__.py +360 -0
  140. empathy_os/workflows/base.py +1530 -0
  141. empathy_os/workflows/bug_predict.py +962 -0
  142. empathy_os/workflows/code_review.py +960 -0
  143. empathy_os/workflows/code_review_adapters.py +310 -0
  144. empathy_os/workflows/code_review_pipeline.py +720 -0
  145. empathy_os/workflows/config.py +600 -0
  146. empathy_os/workflows/dependency_check.py +648 -0
  147. empathy_os/workflows/document_gen.py +1069 -0
  148. empathy_os/workflows/documentation_orchestrator.py +1205 -0
  149. empathy_os/workflows/health_check.py +679 -0
  150. empathy_os/workflows/keyboard_shortcuts/__init__.py +39 -0
  151. empathy_os/workflows/keyboard_shortcuts/generators.py +386 -0
  152. empathy_os/workflows/keyboard_shortcuts/parsers.py +414 -0
  153. empathy_os/workflows/keyboard_shortcuts/prompts.py +295 -0
  154. empathy_os/workflows/keyboard_shortcuts/schema.py +193 -0
  155. empathy_os/workflows/keyboard_shortcuts/workflow.py +505 -0
  156. empathy_os/workflows/manage_documentation.py +804 -0
  157. empathy_os/workflows/new_sample_workflow1.py +146 -0
  158. empathy_os/workflows/new_sample_workflow1_README.md +150 -0
  159. empathy_os/workflows/perf_audit.py +687 -0
  160. empathy_os/workflows/pr_review.py +748 -0
  161. empathy_os/workflows/progress.py +445 -0
  162. empathy_os/workflows/progress_server.py +322 -0
  163. empathy_os/workflows/refactor_plan.py +691 -0
  164. empathy_os/workflows/release_prep.py +808 -0
  165. empathy_os/workflows/research_synthesis.py +404 -0
  166. empathy_os/workflows/secure_release.py +585 -0
  167. empathy_os/workflows/security_adapters.py +297 -0
  168. empathy_os/workflows/security_audit.py +1050 -0
  169. empathy_os/workflows/step_config.py +234 -0
  170. empathy_os/workflows/test5.py +125 -0
  171. empathy_os/workflows/test5_README.md +158 -0
  172. empathy_os/workflows/test_gen.py +1855 -0
  173. empathy_os/workflows/test_lifecycle.py +526 -0
  174. empathy_os/workflows/test_maintenance.py +626 -0
  175. empathy_os/workflows/test_maintenance_cli.py +590 -0
  176. empathy_os/workflows/test_maintenance_crew.py +821 -0
  177. empathy_os/workflows/xml_enhanced_crew.py +285 -0
  178. empathy_software_plugin/cli/__init__.py +120 -0
  179. empathy_software_plugin/cli/inspect.py +362 -0
  180. empathy_software_plugin/cli.py +3 -1
  181. empathy_software_plugin/wizards/__init__.py +42 -0
  182. empathy_software_plugin/wizards/advanced_debugging_wizard.py +392 -0
  183. empathy_software_plugin/wizards/agent_orchestration_wizard.py +511 -0
  184. empathy_software_plugin/wizards/ai_collaboration_wizard.py +503 -0
  185. empathy_software_plugin/wizards/ai_context_wizard.py +441 -0
  186. empathy_software_plugin/wizards/ai_documentation_wizard.py +503 -0
  187. empathy_software_plugin/wizards/base_wizard.py +288 -0
  188. empathy_software_plugin/wizards/book_chapter_wizard.py +519 -0
  189. empathy_software_plugin/wizards/code_review_wizard.py +606 -0
  190. empathy_software_plugin/wizards/debugging/__init__.py +50 -0
  191. empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +414 -0
  192. empathy_software_plugin/wizards/debugging/config_loaders.py +442 -0
  193. empathy_software_plugin/wizards/debugging/fix_applier.py +469 -0
  194. empathy_software_plugin/wizards/debugging/language_patterns.py +383 -0
  195. empathy_software_plugin/wizards/debugging/linter_parsers.py +470 -0
  196. empathy_software_plugin/wizards/debugging/verification.py +369 -0
  197. empathy_software_plugin/wizards/enhanced_testing_wizard.py +537 -0
  198. empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +816 -0
  199. empathy_software_plugin/wizards/multi_model_wizard.py +501 -0
  200. empathy_software_plugin/wizards/pattern_extraction_wizard.py +422 -0
  201. empathy_software_plugin/wizards/pattern_retriever_wizard.py +400 -0
  202. empathy_software_plugin/wizards/performance/__init__.py +9 -0
  203. empathy_software_plugin/wizards/performance/bottleneck_detector.py +221 -0
  204. empathy_software_plugin/wizards/performance/profiler_parsers.py +278 -0
  205. empathy_software_plugin/wizards/performance/trajectory_analyzer.py +429 -0
  206. empathy_software_plugin/wizards/performance_profiling_wizard.py +305 -0
  207. empathy_software_plugin/wizards/prompt_engineering_wizard.py +425 -0
  208. empathy_software_plugin/wizards/rag_pattern_wizard.py +461 -0
  209. empathy_software_plugin/wizards/security/__init__.py +32 -0
  210. empathy_software_plugin/wizards/security/exploit_analyzer.py +290 -0
  211. empathy_software_plugin/wizards/security/owasp_patterns.py +241 -0
  212. empathy_software_plugin/wizards/security/vulnerability_scanner.py +604 -0
  213. empathy_software_plugin/wizards/security_analysis_wizard.py +322 -0
  214. empathy_software_plugin/wizards/security_learning_wizard.py +740 -0
  215. empathy_software_plugin/wizards/tech_debt_wizard.py +726 -0
  216. empathy_software_plugin/wizards/testing/__init__.py +27 -0
  217. empathy_software_plugin/wizards/testing/coverage_analyzer.py +459 -0
  218. empathy_software_plugin/wizards/testing/quality_analyzer.py +531 -0
  219. empathy_software_plugin/wizards/testing/test_suggester.py +533 -0
  220. empathy_software_plugin/wizards/testing_wizard.py +274 -0
  221. hot_reload/README.md +473 -0
  222. hot_reload/__init__.py +62 -0
  223. hot_reload/config.py +84 -0
  224. hot_reload/integration.py +228 -0
  225. hot_reload/reloader.py +298 -0
  226. hot_reload/watcher.py +179 -0
  227. hot_reload/websocket.py +176 -0
  228. scaffolding/README.md +589 -0
  229. scaffolding/__init__.py +35 -0
  230. scaffolding/__main__.py +14 -0
  231. scaffolding/cli.py +240 -0
  232. test_generator/__init__.py +38 -0
  233. test_generator/__main__.py +14 -0
  234. test_generator/cli.py +226 -0
  235. test_generator/generator.py +325 -0
  236. test_generator/risk_analyzer.py +216 -0
  237. workflow_patterns/__init__.py +33 -0
  238. workflow_patterns/behavior.py +249 -0
  239. workflow_patterns/core.py +76 -0
  240. workflow_patterns/output.py +99 -0
  241. workflow_patterns/registry.py +255 -0
  242. workflow_patterns/structural.py +288 -0
  243. workflow_scaffolding/__init__.py +11 -0
  244. workflow_scaffolding/__main__.py +12 -0
  245. workflow_scaffolding/cli.py +206 -0
  246. workflow_scaffolding/generator.py +265 -0
  247. agents/code_inspection/patterns/inspection/recurring_B112.json +0 -18
  248. agents/code_inspection/patterns/inspection/recurring_F541.json +0 -16
  249. agents/code_inspection/patterns/inspection/recurring_FORMAT.json +0 -25
  250. agents/code_inspection/patterns/inspection/recurring_bug_20250822_def456.json +0 -16
  251. agents/code_inspection/patterns/inspection/recurring_bug_20250915_abc123.json +0 -16
  252. agents/code_inspection/patterns/inspection/recurring_bug_20251212_3c5b9951.json +0 -16
  253. agents/code_inspection/patterns/inspection/recurring_bug_20251212_97c0f72f.json +0 -16
  254. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a0871d53.json +0 -16
  255. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a9b6ec41.json +0 -16
  256. agents/code_inspection/patterns/inspection/recurring_bug_null_001.json +0 -16
  257. agents/code_inspection/patterns/inspection/recurring_builtin.json +0 -16
  258. agents/compliance_anticipation_agent.py +0 -1422
  259. agents/compliance_db.py +0 -339
  260. agents/epic_integration_wizard.py +0 -530
  261. agents/notifications.py +0 -291
  262. agents/trust_building_behaviors.py +0 -872
  263. empathy_framework-3.7.0.dist-info/RECORD +0 -105
  264. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/WHEEL +0 -0
  265. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/entry_points.txt +0 -0
  266. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/licenses/LICENSE +0 -0
  267. /empathy_os/{monitoring.py → agent_monitoring.py} +0 -0
@@ -0,0 +1,1298 @@
1
+ """Memory Control Panel for Empathy Framework
2
+
3
+ Enterprise-grade control panel for managing AI memory systems.
4
+ Provides both programmatic API and CLI interface.
5
+
6
+ Features:
7
+ - Redis lifecycle management (start/stop/status)
8
+ - Memory statistics and health monitoring
9
+ - Pattern management (list, search, delete)
10
+ - Configuration management
11
+ - Export/import capabilities
12
+
13
+ Usage (Python API):
14
+ from empathy_os.memory import MemoryControlPanel
15
+
16
+ panel = MemoryControlPanel()
17
+ print(panel.status())
18
+ panel.start_redis()
19
+ panel.show_statistics()
20
+
21
+ Usage (CLI):
22
+ python -m empathy_os.memory.control_panel status
23
+ python -m empathy_os.memory.control_panel start
24
+ python -m empathy_os.memory.control_panel stats
25
+ python -m empathy_os.memory.control_panel patterns --list
26
+
27
+ Copyright 2025 Smart AI Memory, LLC
28
+ Licensed under Fair Source 0.9
29
+ """
30
+
31
+ import argparse
32
+ import hashlib
33
+ import json
34
+ import logging
35
+ import os
36
+ import re
37
+ import signal
38
+ import ssl
39
+ import sys
40
+ import time
41
+ import warnings
42
+ from collections import defaultdict
43
+ from dataclasses import asdict, dataclass
44
+ from datetime import datetime
45
+ from http.server import BaseHTTPRequestHandler, HTTPServer
46
+ from pathlib import Path
47
+ from typing import Any
48
+ from urllib.parse import parse_qs, urlparse
49
+
50
+ import structlog
51
+
52
+ from .long_term import Classification, SecureMemDocsIntegration
53
+ from .redis_bootstrap import (
54
+ RedisStartMethod,
55
+ RedisStatus,
56
+ _check_redis_running,
57
+ ensure_redis,
58
+ stop_redis,
59
+ )
60
+ from .short_term import AccessTier, AgentCredentials, RedisShortTermMemory
61
+
62
+ # Suppress noisy warnings in CLI mode
63
+ warnings.filterwarnings("ignore", category=RuntimeWarning, module="runpy")
64
+
65
+ # Version
66
+ __version__ = "2.2.0"
67
+
68
+ logger = structlog.get_logger(__name__)
69
+
70
+ # =============================================================================
71
+ # Security Configuration
72
+ # =============================================================================
73
+
74
+ # Pattern ID validation regex - matches format: pat_YYYYMMDDHHMMSS_hexstring
75
+ PATTERN_ID_REGEX = re.compile(r"^pat_\d{14}_[a-f0-9]{8,16}$")
76
+
77
+ # Alternative pattern formats that are also valid
78
+ PATTERN_ID_ALT_REGEX = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]{2,63}$")
79
+
80
+ # Rate limiting configuration
81
+ RATE_LIMIT_WINDOW_SECONDS = 60
82
+ RATE_LIMIT_MAX_REQUESTS = 100 # Per IP per window
83
+
84
+
85
+ def _validate_pattern_id(pattern_id: str) -> bool:
86
+ """Validate pattern ID to prevent path traversal and injection attacks.
87
+
88
+ Args:
89
+ pattern_id: The pattern ID to validate
90
+
91
+ Returns:
92
+ True if valid, False otherwise
93
+
94
+ """
95
+ if not pattern_id or not isinstance(pattern_id, str):
96
+ return False
97
+
98
+ # Check for path traversal attempts
99
+ if ".." in pattern_id or "/" in pattern_id or "\\" in pattern_id:
100
+ return False
101
+
102
+ # Check for null bytes
103
+ if "\x00" in pattern_id:
104
+ return False
105
+
106
+ # Check length bounds
107
+ if len(pattern_id) < 3 or len(pattern_id) > 64:
108
+ return False
109
+
110
+ # Must match one of the valid formats
111
+ return bool(PATTERN_ID_REGEX.match(pattern_id) or PATTERN_ID_ALT_REGEX.match(pattern_id))
112
+
113
+
114
+ def _validate_agent_id(agent_id: str) -> bool:
115
+ """Validate agent ID format.
116
+
117
+ Args:
118
+ agent_id: The agent ID to validate
119
+
120
+ Returns:
121
+ True if valid, False otherwise
122
+
123
+ """
124
+ if not agent_id or not isinstance(agent_id, str):
125
+ return False
126
+
127
+ # Check for dangerous characters
128
+ if any(c in agent_id for c in [".", "/", "\\", "\x00", ";", "|", "&"]):
129
+ return False
130
+
131
+ # Check length bounds
132
+ if len(agent_id) < 1 or len(agent_id) > 64:
133
+ return False
134
+
135
+ # Simple alphanumeric with some allowed chars
136
+ return bool(re.match(r"^[a-zA-Z0-9_@.-]+$", agent_id))
137
+
138
+
139
+ def _validate_classification(classification: str | None) -> bool:
140
+ """Validate classification parameter.
141
+
142
+ Args:
143
+ classification: The classification to validate
144
+
145
+ Returns:
146
+ True if valid, False otherwise
147
+
148
+ """
149
+ if classification is None:
150
+ return True
151
+ if not isinstance(classification, str):
152
+ return False
153
+ return classification.upper() in ("PUBLIC", "INTERNAL", "SENSITIVE")
154
+
155
+
156
+ class RateLimiter:
157
+ """Simple in-memory rate limiter by IP address."""
158
+
159
+ def __init__(self, window_seconds: int = 60, max_requests: int = 100):
160
+ self.window_seconds = window_seconds
161
+ self.max_requests = max_requests
162
+ self._requests: dict[str, list[float]] = defaultdict(list)
163
+
164
+ def is_allowed(self, client_ip: str) -> bool:
165
+ """Check if request is allowed for this IP.
166
+
167
+ Args:
168
+ client_ip: The client IP address
169
+
170
+ Returns:
171
+ True if allowed, False if rate limited
172
+
173
+ """
174
+ now = time.time()
175
+ window_start = now - self.window_seconds
176
+
177
+ # Clean old entries
178
+ self._requests[client_ip] = [ts for ts in self._requests[client_ip] if ts > window_start]
179
+
180
+ # Check if over limit
181
+ if len(self._requests[client_ip]) >= self.max_requests:
182
+ logger.warning("rate_limit_exceeded", client_ip=client_ip)
183
+ return False
184
+
185
+ # Record this request
186
+ self._requests[client_ip].append(now)
187
+ return True
188
+
189
+ def get_remaining(self, client_ip: str) -> int:
190
+ """Get remaining requests for this IP."""
191
+ now = time.time()
192
+ window_start = now - self.window_seconds
193
+ recent = [ts for ts in self._requests[client_ip] if ts > window_start]
194
+ return max(0, self.max_requests - len(recent))
195
+
196
+
197
+ class APIKeyAuth:
198
+ """Simple API key authentication."""
199
+
200
+ def __init__(self, api_key: str | None = None):
201
+ """Initialize API key auth.
202
+
203
+ Args:
204
+ api_key: The API key to require. If None, reads from
205
+ EMPATHY_MEMORY_API_KEY env var. If still None, auth is disabled.
206
+
207
+ """
208
+ self.api_key = api_key or os.environ.get("EMPATHY_MEMORY_API_KEY")
209
+ self.enabled = bool(self.api_key)
210
+ self._key_hash: str | None = None
211
+ if self.enabled and self.api_key:
212
+ # Store hash of API key for comparison
213
+ self._key_hash = hashlib.sha256(self.api_key.encode()).hexdigest()
214
+ logger.info("api_key_auth_enabled")
215
+ else:
216
+ logger.info("api_key_auth_disabled", reason="no_key_configured")
217
+
218
+ def is_valid(self, provided_key: str | None) -> bool:
219
+ """Check if provided API key is valid.
220
+
221
+ Args:
222
+ provided_key: The key provided in the request
223
+
224
+ Returns:
225
+ True if valid or auth disabled, False otherwise
226
+
227
+ """
228
+ if not self.enabled:
229
+ return True
230
+
231
+ if not provided_key:
232
+ return False
233
+
234
+ # Constant-time comparison via hash
235
+ provided_hash = hashlib.sha256(provided_key.encode()).hexdigest()
236
+ return provided_hash == self._key_hash
237
+
238
+
239
+ @dataclass
240
+ class MemoryStats:
241
+ """Statistics for memory system."""
242
+
243
+ # Redis stats
244
+ redis_available: bool = False
245
+ redis_method: str = "none"
246
+ redis_keys_total: int = 0
247
+ redis_keys_working: int = 0
248
+ redis_keys_staged: int = 0
249
+ redis_memory_used: str = "0"
250
+
251
+ # Long-term stats
252
+ long_term_available: bool = False
253
+ patterns_total: int = 0
254
+ patterns_public: int = 0
255
+ patterns_internal: int = 0
256
+ patterns_sensitive: int = 0
257
+ patterns_encrypted: int = 0
258
+
259
+ # Performance stats
260
+ redis_ping_ms: float = 0.0
261
+ storage_bytes: int = 0
262
+ collection_time_ms: float = 0.0
263
+
264
+ # Timestamps
265
+ collected_at: str = ""
266
+
267
+
268
+ @dataclass
269
+ class ControlPanelConfig:
270
+ """Configuration for control panel."""
271
+
272
+ redis_host: str = "localhost"
273
+ redis_port: int = 6379
274
+ storage_dir: str = "./memdocs_storage"
275
+ audit_dir: str = "./logs"
276
+ auto_start_redis: bool = True
277
+
278
+
279
+ class MemoryControlPanel:
280
+ """Enterprise control panel for Empathy memory management.
281
+
282
+ Provides unified management interface for:
283
+ - Short-term memory (Redis)
284
+ - Long-term memory (MemDocs/file storage)
285
+ - Security and compliance controls
286
+
287
+ Example:
288
+ >>> panel = MemoryControlPanel()
289
+ >>> status = panel.status()
290
+ >>> print(f"Redis: {status['redis']['status']}")
291
+ >>> print(f"Patterns: {status['long_term']['pattern_count']}")
292
+
293
+ """
294
+
295
+ def __init__(self, config: ControlPanelConfig | None = None):
296
+ """Initialize control panel.
297
+
298
+ Args:
299
+ config: Configuration options (uses defaults if None)
300
+
301
+ """
302
+ self.config = config or ControlPanelConfig()
303
+ self._redis_status: RedisStatus | None = None
304
+ self._short_term: RedisShortTermMemory | None = None
305
+ self._long_term: SecureMemDocsIntegration | None = None
306
+
307
+ def status(self) -> dict[str, Any]:
308
+ """Get comprehensive status of memory system.
309
+
310
+ Returns:
311
+ Dictionary with status of all memory components
312
+
313
+ """
314
+ redis_running = _check_redis_running(self.config.redis_host, self.config.redis_port)
315
+
316
+ result = {
317
+ "timestamp": datetime.utcnow().isoformat() + "Z",
318
+ "redis": {
319
+ "status": "running" if redis_running else "stopped",
320
+ "host": self.config.redis_host,
321
+ "port": self.config.redis_port,
322
+ "method": self._redis_status.method.value if self._redis_status else "unknown",
323
+ },
324
+ "long_term": {
325
+ "status": (
326
+ "available" if Path(self.config.storage_dir).exists() else "not_initialized"
327
+ ),
328
+ "storage_dir": self.config.storage_dir,
329
+ "pattern_count": self._count_patterns(),
330
+ },
331
+ "config": {
332
+ "auto_start_redis": self.config.auto_start_redis,
333
+ "audit_dir": self.config.audit_dir,
334
+ },
335
+ }
336
+
337
+ return result
338
+
339
+ def start_redis(self, verbose: bool = True) -> RedisStatus:
340
+ """Start Redis if not running.
341
+
342
+ Args:
343
+ verbose: Print status messages
344
+
345
+ Returns:
346
+ RedisStatus with result
347
+
348
+ """
349
+ self._redis_status = ensure_redis(
350
+ host=self.config.redis_host,
351
+ port=self.config.redis_port,
352
+ auto_start=True,
353
+ verbose=verbose,
354
+ )
355
+ return self._redis_status
356
+
357
+ def stop_redis(self) -> bool:
358
+ """Stop Redis if we started it.
359
+
360
+ Returns:
361
+ True if stopped successfully
362
+
363
+ """
364
+ if self._redis_status and self._redis_status.method != RedisStartMethod.ALREADY_RUNNING:
365
+ return stop_redis(self._redis_status.method)
366
+ return False
367
+
368
+ def get_statistics(self) -> MemoryStats:
369
+ """Collect comprehensive statistics.
370
+
371
+ Returns:
372
+ MemoryStats with all metrics
373
+
374
+ """
375
+ start_time = time.perf_counter()
376
+ stats = MemoryStats(collected_at=datetime.utcnow().isoformat() + "Z")
377
+
378
+ # Redis stats
379
+ redis_running = _check_redis_running(self.config.redis_host, self.config.redis_port)
380
+ stats.redis_available = redis_running
381
+
382
+ if redis_running:
383
+ try:
384
+ memory = self._get_short_term()
385
+
386
+ # Measure Redis ping latency
387
+ ping_start = time.perf_counter()
388
+ redis_stats = memory.get_stats()
389
+ stats.redis_ping_ms = (time.perf_counter() - ping_start) * 1000
390
+
391
+ stats.redis_method = redis_stats.get("mode", "redis")
392
+ stats.redis_keys_total = redis_stats.get("total_keys", 0)
393
+ stats.redis_keys_working = redis_stats.get("working_keys", 0)
394
+ stats.redis_keys_staged = redis_stats.get("staged_keys", 0)
395
+ stats.redis_memory_used = redis_stats.get("used_memory", "0")
396
+ except Exception as e:
397
+ logger.warning("redis_stats_failed", error=str(e))
398
+
399
+ # Long-term stats
400
+ storage_path = Path(self.config.storage_dir)
401
+ if storage_path.exists():
402
+ stats.long_term_available = True
403
+
404
+ # Calculate storage size
405
+ try:
406
+ stats.storage_bytes = sum(
407
+ f.stat().st_size for f in storage_path.glob("**/*") if f.is_file()
408
+ )
409
+ except Exception:
410
+ pass
411
+
412
+ try:
413
+ long_term = self._get_long_term()
414
+ lt_stats = long_term.get_statistics()
415
+ stats.patterns_total = lt_stats.get("total_patterns", 0)
416
+ stats.patterns_public = lt_stats.get("by_classification", {}).get("PUBLIC", 0)
417
+ stats.patterns_internal = lt_stats.get("by_classification", {}).get("INTERNAL", 0)
418
+ stats.patterns_sensitive = lt_stats.get("by_classification", {}).get("SENSITIVE", 0)
419
+ stats.patterns_encrypted = lt_stats.get("encrypted_count", 0)
420
+ except Exception as e:
421
+ logger.warning("long_term_stats_failed", error=str(e))
422
+
423
+ # Total collection time
424
+ stats.collection_time_ms = (time.perf_counter() - start_time) * 1000
425
+
426
+ return stats
427
+
428
+ def list_patterns(
429
+ self,
430
+ classification: str | None = None,
431
+ limit: int = 100,
432
+ ) -> list[dict[str, Any]]:
433
+ """List patterns in long-term storage.
434
+
435
+ Args:
436
+ classification: Filter by classification (PUBLIC/INTERNAL/SENSITIVE)
437
+ limit: Maximum patterns to return
438
+
439
+ Returns:
440
+ List of pattern summaries
441
+
442
+ """
443
+ long_term = self._get_long_term()
444
+
445
+ class_filter = None
446
+ if classification:
447
+ class_filter = Classification[classification.upper()]
448
+
449
+ # Use admin user for listing
450
+ patterns = long_term.list_patterns(
451
+ user_id="admin@system",
452
+ classification=class_filter,
453
+ )
454
+
455
+ return patterns[:limit]
456
+
457
+ def delete_pattern(self, pattern_id: str, user_id: str = "admin@system") -> bool:
458
+ """Delete a pattern from long-term storage.
459
+
460
+ Args:
461
+ pattern_id: Pattern to delete
462
+ user_id: User performing deletion (for audit)
463
+
464
+ Returns:
465
+ True if deleted
466
+
467
+ """
468
+ long_term = self._get_long_term()
469
+ try:
470
+ return long_term.delete_pattern(pattern_id, user_id)
471
+ except Exception as e:
472
+ logger.error("delete_pattern_failed", pattern_id=pattern_id, error=str(e))
473
+ return False
474
+
475
+ def clear_short_term(self, agent_id: str = "admin") -> int:
476
+ """Clear all short-term memory for an agent.
477
+
478
+ Args:
479
+ agent_id: Agent whose memory to clear
480
+
481
+ Returns:
482
+ Number of keys deleted
483
+
484
+ """
485
+ memory = self._get_short_term()
486
+ creds = AgentCredentials(agent_id=agent_id, tier=AccessTier.STEWARD)
487
+ return memory.clear_working_memory(creds)
488
+
489
+ def export_patterns(self, output_path: str, classification: str | None = None) -> int:
490
+ """Export patterns to JSON file.
491
+
492
+ Args:
493
+ output_path: Path to output file
494
+ classification: Filter by classification
495
+
496
+ Returns:
497
+ Number of patterns exported
498
+
499
+ """
500
+ patterns = self.list_patterns(classification=classification)
501
+
502
+ export_data = {
503
+ "exported_at": datetime.utcnow().isoformat() + "Z",
504
+ "classification_filter": classification,
505
+ "pattern_count": len(patterns),
506
+ "patterns": patterns,
507
+ }
508
+
509
+ with open(output_path, "w") as f:
510
+ json.dump(export_data, f, indent=2)
511
+
512
+ return len(patterns)
513
+
514
+ def health_check(self) -> dict[str, Any]:
515
+ """Perform comprehensive health check.
516
+
517
+ Returns:
518
+ Health status with recommendations
519
+
520
+ """
521
+ status = self.status()
522
+ stats = self.get_statistics()
523
+
524
+ checks: list[dict[str, str]] = []
525
+ recommendations: list[str] = []
526
+ health: dict[str, Any] = {
527
+ "overall": "healthy",
528
+ "checks": checks,
529
+ "recommendations": recommendations,
530
+ }
531
+
532
+ # Check Redis
533
+ if status["redis"]["status"] == "running":
534
+ checks.append({"name": "redis", "status": "pass", "message": "Redis is running"})
535
+ else:
536
+ checks.append({"name": "redis", "status": "warn", "message": "Redis not running"})
537
+ recommendations.append("Start Redis for multi-agent coordination")
538
+ health["overall"] = "degraded"
539
+
540
+ # Check long-term storage
541
+ if status["long_term"]["status"] == "available":
542
+ checks.append({"name": "long_term", "status": "pass", "message": "Storage available"})
543
+ else:
544
+ checks.append(
545
+ {"name": "long_term", "status": "warn", "message": "Storage not initialized"},
546
+ )
547
+ recommendations.append("Initialize long-term storage directory")
548
+ health["overall"] = "degraded"
549
+
550
+ # Check pattern count
551
+ if stats.patterns_total > 0:
552
+ checks.append(
553
+ {
554
+ "name": "patterns",
555
+ "status": "pass",
556
+ "message": f"{stats.patterns_total} patterns stored",
557
+ },
558
+ )
559
+ else:
560
+ checks.append(
561
+ {"name": "patterns", "status": "info", "message": "No patterns stored yet"},
562
+ )
563
+
564
+ # Check encryption
565
+ if stats.patterns_sensitive > 0 and stats.patterns_encrypted < stats.patterns_sensitive:
566
+ checks.append(
567
+ {
568
+ "name": "encryption",
569
+ "status": "fail",
570
+ "message": "Some sensitive patterns are not encrypted",
571
+ },
572
+ )
573
+ recommendations.append("Enable encryption for sensitive patterns")
574
+ health["overall"] = "unhealthy"
575
+ elif stats.patterns_sensitive > 0:
576
+ checks.append(
577
+ {
578
+ "name": "encryption",
579
+ "status": "pass",
580
+ "message": "All sensitive patterns encrypted",
581
+ },
582
+ )
583
+
584
+ return health
585
+
586
+ def _get_short_term(self) -> RedisShortTermMemory:
587
+ """Get or create short-term memory instance."""
588
+ if self._short_term is None:
589
+ redis_running = _check_redis_running(self.config.redis_host, self.config.redis_port)
590
+ self._short_term = RedisShortTermMemory(
591
+ host=self.config.redis_host,
592
+ port=self.config.redis_port,
593
+ use_mock=not redis_running,
594
+ )
595
+ return self._short_term
596
+
597
+ def _get_long_term(self) -> SecureMemDocsIntegration:
598
+ """Get or create long-term memory instance."""
599
+ if self._long_term is None:
600
+ self._long_term = SecureMemDocsIntegration(
601
+ storage_dir=self.config.storage_dir,
602
+ audit_log_dir=self.config.audit_dir,
603
+ enable_encryption=True,
604
+ )
605
+ return self._long_term
606
+
607
+ def _count_patterns(self) -> int:
608
+ """Count patterns in storage."""
609
+ storage_path = Path(self.config.storage_dir)
610
+ if not storage_path.exists():
611
+ return 0
612
+ return len(list(storage_path.glob("*.json")))
613
+
614
+
615
+ def print_status(panel: MemoryControlPanel):
616
+ """Print status in a formatted way."""
617
+ status = panel.status()
618
+
619
+ print("\n" + "=" * 50)
620
+ print("EMPATHY MEMORY STATUS")
621
+ print("=" * 50)
622
+
623
+ # Redis
624
+ redis = status["redis"]
625
+ redis_icon = "✓" if redis["status"] == "running" else "✗"
626
+ print(f"\n{redis_icon} Redis: {redis['status'].upper()}")
627
+ print(f" Host: {redis['host']}:{redis['port']}")
628
+ if redis["method"] != "unknown":
629
+ print(f" Method: {redis['method']}")
630
+
631
+ # Long-term
632
+ lt = status["long_term"]
633
+ lt_icon = "✓" if lt["status"] == "available" else "○"
634
+ print(f"\n{lt_icon} Long-term Storage: {lt['status'].upper()}")
635
+ print(f" Path: {lt['storage_dir']}")
636
+ print(f" Patterns: {lt['pattern_count']}")
637
+
638
+ print()
639
+
640
+
641
+ def print_stats(panel: MemoryControlPanel):
642
+ """Print statistics in a formatted way."""
643
+ stats = panel.get_statistics()
644
+
645
+ print("\n" + "=" * 50)
646
+ print("EMPATHY MEMORY STATISTICS")
647
+ print("=" * 50)
648
+
649
+ print("\nShort-term Memory (Redis):")
650
+ print(f" Available: {stats.redis_available}")
651
+ if stats.redis_available:
652
+ print(f" Total keys: {stats.redis_keys_total}")
653
+ print(f" Working keys: {stats.redis_keys_working}")
654
+ print(f" Staged patterns: {stats.redis_keys_staged}")
655
+ print(f" Memory used: {stats.redis_memory_used}")
656
+
657
+ print("\nLong-term Memory (Patterns):")
658
+ print(f" Available: {stats.long_term_available}")
659
+ print(f" Total patterns: {stats.patterns_total}")
660
+ print(f" └─ PUBLIC: {stats.patterns_public}")
661
+ print(f" └─ INTERNAL: {stats.patterns_internal}")
662
+ print(f" └─ SENSITIVE: {stats.patterns_sensitive}")
663
+ print(f" Encrypted: {stats.patterns_encrypted}")
664
+
665
+ # Performance stats
666
+ print("\nPerformance:")
667
+ if stats.redis_ping_ms > 0:
668
+ print(f" Redis latency: {stats.redis_ping_ms:.2f}ms")
669
+ if stats.storage_bytes > 0:
670
+ size_kb = stats.storage_bytes / 1024
671
+ print(f" Storage size: {size_kb:.1f} KB")
672
+ print(f" Stats collected in: {stats.collection_time_ms:.2f}ms")
673
+
674
+ print()
675
+
676
+
677
+ def print_health(panel: MemoryControlPanel):
678
+ """Print health check in a formatted way."""
679
+ health = panel.health_check()
680
+
681
+ print("\n" + "=" * 50)
682
+ print("EMPATHY MEMORY HEALTH CHECK")
683
+ print("=" * 50)
684
+
685
+ status_icons = {"pass": "✓", "warn": "⚠", "fail": "✗", "info": "ℹ"}
686
+ overall_icon = (
687
+ "✓" if health["overall"] == "healthy" else "⚠" if health["overall"] == "degraded" else "✗"
688
+ )
689
+
690
+ print(f"\n{overall_icon} Overall: {health['overall'].upper()}")
691
+
692
+ print("\nChecks:")
693
+ for check in health["checks"]:
694
+ icon = status_icons.get(check["status"], "?")
695
+ print(f" {icon} {check['name']}: {check['message']}")
696
+
697
+ if health["recommendations"]:
698
+ print("\nRecommendations:")
699
+ for rec in health["recommendations"]:
700
+ print(f" • {rec}")
701
+
702
+ print()
703
+
704
+
705
+ class MemoryAPIHandler(BaseHTTPRequestHandler):
706
+ """HTTP request handler for Memory Control Panel API."""
707
+
708
+ panel: MemoryControlPanel | None = None # Set by server
709
+ rate_limiter: RateLimiter | None = None # Set by server
710
+ api_auth: APIKeyAuth | None = None # Set by server
711
+ allowed_origins: list[str] | None = None # Set by server for CORS
712
+
713
+ def log_message(self, format, *args):
714
+ """Override to use structlog instead of stderr."""
715
+ logger.debug("api_request", message=format % args)
716
+
717
+ def _get_client_ip(self) -> str:
718
+ """Get client IP address, handling proxies."""
719
+ # Check for X-Forwarded-For header (behind proxy)
720
+ forwarded = self.headers.get("X-Forwarded-For")
721
+ if forwarded:
722
+ # Take the first IP in the chain
723
+ return forwarded.split(",")[0].strip()
724
+ # Fall back to direct connection
725
+ return self.client_address[0]
726
+
727
+ def _check_rate_limit(self) -> bool:
728
+ """Check if request should be rate limited."""
729
+ if self.rate_limiter is None:
730
+ return True
731
+ return self.rate_limiter.is_allowed(self._get_client_ip())
732
+
733
+ def _check_auth(self) -> bool:
734
+ """Check API key authentication."""
735
+ if self.api_auth is None or not self.api_auth.enabled:
736
+ return True
737
+
738
+ # Check Authorization header
739
+ auth_header = self.headers.get("Authorization")
740
+ if auth_header and auth_header.startswith("Bearer "):
741
+ token = auth_header[7:]
742
+ return self.api_auth.is_valid(token)
743
+
744
+ # Check X-API-Key header
745
+ api_key = self.headers.get("X-API-Key")
746
+ if api_key:
747
+ return self.api_auth.is_valid(api_key)
748
+
749
+ return False
750
+
751
+ def _get_cors_origin(self) -> str:
752
+ """Get appropriate CORS origin header value."""
753
+ if self.allowed_origins is None:
754
+ # Default: allow localhost only
755
+ origin = self.headers.get("Origin", "")
756
+ if origin.startswith("http://localhost") or origin.startswith("https://localhost"):
757
+ return origin
758
+ return "http://localhost:8765"
759
+
760
+ if "*" in self.allowed_origins:
761
+ return "*"
762
+
763
+ origin = self.headers.get("Origin", "")
764
+ if origin in self.allowed_origins:
765
+ return origin
766
+
767
+ return self.allowed_origins[0] if self.allowed_origins else ""
768
+
769
+ def _send_json(self, data: Any, status: int = 200):
770
+ """Send JSON response."""
771
+ self.send_response(status)
772
+ self.send_header("Content-Type", "application/json")
773
+ self.send_header("Access-Control-Allow-Origin", self._get_cors_origin())
774
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
775
+ self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
776
+
777
+ # Add rate limit headers if available
778
+ if self.rate_limiter:
779
+ remaining = self.rate_limiter.get_remaining(self._get_client_ip())
780
+ self.send_header("X-RateLimit-Remaining", str(remaining))
781
+ self.send_header("X-RateLimit-Limit", str(self.rate_limiter.max_requests))
782
+
783
+ self.end_headers()
784
+ self.wfile.write(json.dumps(data).encode())
785
+
786
+ def _send_error(self, message: str, status: int = 400):
787
+ """Send error response."""
788
+ self._send_json({"error": message, "status_code": status}, status)
789
+
790
+ def do_OPTIONS(self):
791
+ """Handle CORS preflight."""
792
+ self.send_response(200)
793
+ self.send_header("Access-Control-Allow-Origin", self._get_cors_origin())
794
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
795
+ self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
796
+ self.end_headers()
797
+
798
+ def do_GET(self):
799
+ """Handle GET requests."""
800
+ # Rate limiting check
801
+ if not self._check_rate_limit():
802
+ self._send_error("Rate limit exceeded. Try again later.", 429)
803
+ return
804
+
805
+ # Authentication check (skip for ping endpoint)
806
+ parsed = urlparse(self.path)
807
+ path = parsed.path
808
+
809
+ if path != "/api/ping" and not self._check_auth():
810
+ self._send_error("Unauthorized. Provide valid API key.", 401)
811
+ return
812
+
813
+ query = parse_qs(parsed.query)
814
+
815
+ if path == "/api/ping":
816
+ self._send_json({"status": "ok", "service": "empathy-memory"})
817
+
818
+ elif path == "/api/status":
819
+ self._send_json(self.panel.status())
820
+
821
+ elif path == "/api/stats":
822
+ stats = self.panel.get_statistics()
823
+ self._send_json(asdict(stats))
824
+
825
+ elif path == "/api/health":
826
+ self._send_json(self.panel.health_check())
827
+
828
+ elif path == "/api/patterns":
829
+ classification = query.get("classification", [None])[0]
830
+
831
+ # Validate classification
832
+ if not _validate_classification(classification):
833
+ self._send_error("Invalid classification. Use PUBLIC, INTERNAL, or SENSITIVE.", 400)
834
+ return
835
+
836
+ # Validate and sanitize limit
837
+ try:
838
+ limit = int(query.get("limit", [100])[0])
839
+ limit = max(1, min(limit, 1000)) # Clamp between 1 and 1000
840
+ except (ValueError, TypeError):
841
+ limit = 100
842
+
843
+ patterns = self.panel.list_patterns(classification=classification, limit=limit)
844
+ self._send_json(patterns)
845
+
846
+ elif path == "/api/patterns/export":
847
+ classification = query.get("classification", [None])[0]
848
+
849
+ # Validate classification
850
+ if not _validate_classification(classification):
851
+ self._send_error("Invalid classification. Use PUBLIC, INTERNAL, or SENSITIVE.", 400)
852
+ return
853
+
854
+ patterns = self.panel.list_patterns(classification=classification)
855
+ export_data = {
856
+ "exported_at": datetime.utcnow().isoformat() + "Z",
857
+ "classification_filter": classification,
858
+ "patterns": patterns,
859
+ }
860
+ self._send_json({"pattern_count": len(patterns), "export_data": export_data})
861
+
862
+ elif path.startswith("/api/patterns/"):
863
+ pattern_id = path.split("/")[-1]
864
+
865
+ # Validate pattern ID
866
+ if not _validate_pattern_id(pattern_id):
867
+ self._send_error("Invalid pattern ID format", 400)
868
+ return
869
+
870
+ patterns = self.panel.list_patterns()
871
+ pattern = next((p for p in patterns if p.get("pattern_id") == pattern_id), None)
872
+ if pattern:
873
+ self._send_json(pattern)
874
+ else:
875
+ self._send_error("Pattern not found", 404)
876
+
877
+ else:
878
+ self._send_error("Not found", 404)
879
+
880
+ def do_POST(self):
881
+ """Handle POST requests."""
882
+ # Rate limiting check
883
+ if not self._check_rate_limit():
884
+ self._send_error("Rate limit exceeded. Try again later.", 429)
885
+ return
886
+
887
+ # Authentication check
888
+ if not self._check_auth():
889
+ self._send_error("Unauthorized. Provide valid API key.", 401)
890
+ return
891
+
892
+ parsed = urlparse(self.path)
893
+ path = parsed.path
894
+
895
+ # Read body if present (with size limit to prevent DoS)
896
+ content_length = int(self.headers.get("Content-Length", 0))
897
+ max_body_size = 1024 * 1024 # 1MB limit
898
+ if content_length > max_body_size:
899
+ self._send_error("Request body too large", 413)
900
+ return
901
+
902
+ body = {}
903
+ if content_length > 0:
904
+ try:
905
+ body = json.loads(self.rfile.read(content_length).decode())
906
+ except (json.JSONDecodeError, UnicodeDecodeError):
907
+ self._send_error("Invalid JSON body", 400)
908
+ return
909
+
910
+ if path == "/api/redis/start":
911
+ status = self.panel.start_redis(verbose=False)
912
+ self._send_json(
913
+ {
914
+ "success": status.available,
915
+ "message": f"Redis {'OK' if status.available else 'failed'} via {status.method.value}",
916
+ },
917
+ )
918
+
919
+ elif path == "/api/redis/stop":
920
+ stopped = self.panel.stop_redis()
921
+ self._send_json(
922
+ {
923
+ "success": stopped,
924
+ "message": "Redis stopped" if stopped else "Could not stop Redis",
925
+ },
926
+ )
927
+
928
+ elif path == "/api/memory/clear":
929
+ agent_id = body.get("agent_id", "admin")
930
+
931
+ # Validate agent ID
932
+ if not _validate_agent_id(agent_id):
933
+ self._send_error("Invalid agent ID format", 400)
934
+ return
935
+
936
+ deleted = self.panel.clear_short_term(agent_id)
937
+ self._send_json({"keys_deleted": deleted})
938
+
939
+ else:
940
+ self._send_error("Not found", 404)
941
+
942
+ def do_DELETE(self):
943
+ """Handle DELETE requests."""
944
+ # Rate limiting check
945
+ if not self._check_rate_limit():
946
+ self._send_error("Rate limit exceeded. Try again later.", 429)
947
+ return
948
+
949
+ # Authentication check
950
+ if not self._check_auth():
951
+ self._send_error("Unauthorized. Provide valid API key.", 401)
952
+ return
953
+
954
+ parsed = urlparse(self.path)
955
+ path = parsed.path
956
+
957
+ if path.startswith("/api/patterns/"):
958
+ pattern_id = path.split("/")[-1]
959
+
960
+ # Validate pattern ID to prevent path traversal
961
+ if not _validate_pattern_id(pattern_id):
962
+ self._send_error("Invalid pattern ID format", 400)
963
+ return
964
+
965
+ deleted = self.panel.delete_pattern(pattern_id)
966
+ self._send_json({"success": deleted})
967
+ else:
968
+ self._send_error("Not found", 404)
969
+
970
+
971
+ def run_api_server(
972
+ panel: MemoryControlPanel,
973
+ host: str = "localhost",
974
+ port: int = 8765,
975
+ api_key: str | None = None,
976
+ enable_rate_limit: bool = True,
977
+ rate_limit_requests: int = 100,
978
+ rate_limit_window: int = 60,
979
+ ssl_certfile: str | None = None,
980
+ ssl_keyfile: str | None = None,
981
+ allowed_origins: list[str] | None = None,
982
+ ):
983
+ """Run the Memory API server with security features.
984
+
985
+ Args:
986
+ panel: MemoryControlPanel instance
987
+ host: Host to bind to
988
+ port: Port to bind to
989
+ api_key: API key for authentication (or set EMPATHY_MEMORY_API_KEY env var)
990
+ enable_rate_limit: Enable rate limiting
991
+ rate_limit_requests: Max requests per window per IP
992
+ rate_limit_window: Rate limit window in seconds
993
+ ssl_certfile: Path to SSL certificate file for HTTPS
994
+ ssl_keyfile: Path to SSL key file for HTTPS
995
+ allowed_origins: List of allowed CORS origins (None = localhost only)
996
+
997
+ """
998
+ # Set up handler class attributes
999
+ MemoryAPIHandler.panel = panel
1000
+ MemoryAPIHandler.allowed_origins = allowed_origins
1001
+
1002
+ # Set up rate limiting
1003
+ if enable_rate_limit:
1004
+ MemoryAPIHandler.rate_limiter = RateLimiter(
1005
+ window_seconds=rate_limit_window,
1006
+ max_requests=rate_limit_requests,
1007
+ )
1008
+ else:
1009
+ MemoryAPIHandler.rate_limiter = None
1010
+
1011
+ # Set up API key authentication
1012
+ MemoryAPIHandler.api_auth = APIKeyAuth(api_key)
1013
+
1014
+ server = HTTPServer((host, port), MemoryAPIHandler)
1015
+
1016
+ # Enable HTTPS if certificates provided
1017
+ use_https = False
1018
+ if ssl_certfile and ssl_keyfile:
1019
+ if Path(ssl_certfile).exists() and Path(ssl_keyfile).exists():
1020
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
1021
+ context.load_cert_chain(ssl_certfile, ssl_keyfile)
1022
+ server.socket = context.wrap_socket(server.socket, server_side=True)
1023
+ use_https = True
1024
+ else:
1025
+ logger.warning("ssl_cert_not_found", certfile=ssl_certfile, keyfile=ssl_keyfile)
1026
+
1027
+ protocol = "https" if use_https else "http"
1028
+
1029
+ # Graceful shutdown handler
1030
+ def shutdown_handler(signum, frame):
1031
+ print("\n\nReceived shutdown signal...")
1032
+ print("Stopping API server...")
1033
+ server.shutdown()
1034
+ # Stop Redis if we started it
1035
+ if panel.stop_redis():
1036
+ print("Stopped Redis")
1037
+ print("Shutdown complete.")
1038
+ sys.exit(0)
1039
+
1040
+ # Register signal handlers
1041
+ signal.signal(signal.SIGINT, shutdown_handler)
1042
+ signal.signal(signal.SIGTERM, shutdown_handler)
1043
+
1044
+ print(f"\n{'=' * 50}")
1045
+ print("EMPATHY MEMORY API SERVER")
1046
+ print(f"{'=' * 50}")
1047
+ print(f"\nServer running at {protocol}://{host}:{port}")
1048
+
1049
+ # Security status
1050
+ print("\nSecurity:")
1051
+ print(f" HTTPS: {'✓ Enabled' if use_https else '✗ Disabled'}")
1052
+ print(f" API Key Auth: {'✓ Enabled' if MemoryAPIHandler.api_auth.enabled else '✗ Disabled'}")
1053
+ print(
1054
+ f" Rate Limit: {'✓ Enabled (' + str(rate_limit_requests) + '/min)' if enable_rate_limit else '✗ Disabled'}",
1055
+ )
1056
+ print(f" CORS Origins: {allowed_origins or ['localhost']}")
1057
+
1058
+ print("\nEndpoints:")
1059
+ print(" GET /api/ping Health check (no auth)")
1060
+ print(" GET /api/status Memory system status")
1061
+ print(" GET /api/stats Detailed statistics")
1062
+ print(" GET /api/health Health check with recommendations")
1063
+ print(" GET /api/patterns List patterns")
1064
+ print(" GET /api/patterns/export Export patterns")
1065
+ print(" POST /api/redis/start Start Redis")
1066
+ print(" POST /api/redis/stop Stop Redis")
1067
+ print(" POST /api/memory/clear Clear short-term memory")
1068
+
1069
+ if MemoryAPIHandler.api_auth.enabled:
1070
+ print("\nAuthentication:")
1071
+ print(" Add header: Authorization: Bearer <your-api-key>")
1072
+ print(" Or header: X-API-Key: <your-api-key>")
1073
+
1074
+ print("\nPress Ctrl+C to stop\n")
1075
+
1076
+ server.serve_forever()
1077
+
1078
+
1079
+ def _configure_logging(verbose: bool = False):
1080
+ """Configure logging for CLI mode."""
1081
+ level = logging.DEBUG if verbose else logging.WARNING
1082
+ logging.basicConfig(level=level, format="%(message)s")
1083
+ structlog.configure(
1084
+ wrapper_class=structlog.make_filtering_bound_logger(level),
1085
+ )
1086
+
1087
+
1088
+ def main():
1089
+ """CLI entry point."""
1090
+ parser = argparse.ArgumentParser(
1091
+ description="Empathy Memory Control Panel - Manage Redis and pattern storage",
1092
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1093
+ epilog="""
1094
+ Examples:
1095
+ %(prog)s status Show memory system status
1096
+ %(prog)s start Start Redis if not running
1097
+ %(prog)s stop Stop Redis (if we started it)
1098
+ %(prog)s stats Show detailed statistics
1099
+ %(prog)s health Run health check
1100
+ %(prog)s patterns List stored patterns
1101
+ %(prog)s export patterns.json Export patterns to file
1102
+ %(prog)s api --api-port 8765 Start REST API server only
1103
+ %(prog)s serve Start Redis + API server (recommended)
1104
+
1105
+ Quick Start:
1106
+ 1. pip install empathy-framework
1107
+ 2. empathy-memory serve
1108
+ 3. Open http://localhost:8765/api/status in browser
1109
+ """,
1110
+ )
1111
+
1112
+ parser.add_argument(
1113
+ "command",
1114
+ choices=[
1115
+ "status",
1116
+ "start",
1117
+ "stop",
1118
+ "stats",
1119
+ "health",
1120
+ "patterns",
1121
+ "export",
1122
+ "api",
1123
+ "serve",
1124
+ ],
1125
+ help="Command to execute",
1126
+ nargs="?",
1127
+ )
1128
+ parser.add_argument(
1129
+ "-V",
1130
+ "--version",
1131
+ action="version",
1132
+ version=f"empathy-memory {__version__}",
1133
+ )
1134
+ parser.add_argument(
1135
+ "--host",
1136
+ default="localhost",
1137
+ help="Redis host (or API host for 'api' command)",
1138
+ )
1139
+ parser.add_argument("--port", type=int, default=6379, help="Redis port")
1140
+ parser.add_argument(
1141
+ "--api-port",
1142
+ type=int,
1143
+ default=8765,
1144
+ help="API server port (for 'api' command)",
1145
+ )
1146
+ parser.add_argument(
1147
+ "--storage",
1148
+ default="./memdocs_storage",
1149
+ help="Long-term storage directory",
1150
+ )
1151
+ parser.add_argument(
1152
+ "--classification",
1153
+ "-c",
1154
+ help="Filter by classification (PUBLIC/INTERNAL/SENSITIVE)",
1155
+ )
1156
+ parser.add_argument("--output", "-o", help="Output file for export")
1157
+ parser.add_argument("--json", action="store_true", help="Output in JSON format")
1158
+ parser.add_argument("-v", "--verbose", action="store_true", help="Show debug output")
1159
+
1160
+ # Security options (for api/serve commands)
1161
+ parser.add_argument(
1162
+ "--api-key",
1163
+ help="API key for authentication (or set EMPATHY_MEMORY_API_KEY env var)",
1164
+ )
1165
+ parser.add_argument("--no-rate-limit", action="store_true", help="Disable rate limiting")
1166
+ parser.add_argument(
1167
+ "--rate-limit",
1168
+ type=int,
1169
+ default=100,
1170
+ help="Max requests per minute per IP (default: 100)",
1171
+ )
1172
+ parser.add_argument("--ssl-cert", help="Path to SSL certificate file for HTTPS")
1173
+ parser.add_argument("--ssl-key", help="Path to SSL key file for HTTPS")
1174
+ parser.add_argument(
1175
+ "--cors-origins",
1176
+ help="Comma-separated list of allowed CORS origins (default: localhost)",
1177
+ )
1178
+
1179
+ args = parser.parse_args()
1180
+
1181
+ # Configure logging (quiet by default)
1182
+ _configure_logging(verbose=args.verbose)
1183
+
1184
+ # If no command specified, show help
1185
+ if args.command is None:
1186
+ parser.print_help()
1187
+ sys.exit(0)
1188
+
1189
+ config = ControlPanelConfig(
1190
+ redis_host=args.host,
1191
+ redis_port=args.port,
1192
+ storage_dir=args.storage,
1193
+ )
1194
+ panel = MemoryControlPanel(config)
1195
+
1196
+ if args.command == "status":
1197
+ if args.json:
1198
+ print(json.dumps(panel.status(), indent=2))
1199
+ else:
1200
+ print_status(panel)
1201
+
1202
+ elif args.command == "start":
1203
+ status = panel.start_redis(verbose=not args.json)
1204
+ if args.json:
1205
+ print(json.dumps({"available": status.available, "method": status.method.value}))
1206
+ elif status.available:
1207
+ print(f"\n✓ Redis started via {status.method.value}")
1208
+ else:
1209
+ print(f"\n✗ Failed to start Redis: {status.message}")
1210
+ sys.exit(1)
1211
+
1212
+ elif args.command == "stop":
1213
+ if panel.stop_redis():
1214
+ print("✓ Redis stopped")
1215
+ else:
1216
+ print("⚠ Could not stop Redis (may not have been started by us)")
1217
+
1218
+ elif args.command == "stats":
1219
+ if args.json:
1220
+ print(json.dumps(asdict(panel.get_statistics()), indent=2))
1221
+ else:
1222
+ print_stats(panel)
1223
+
1224
+ elif args.command == "health":
1225
+ if args.json:
1226
+ print(json.dumps(panel.health_check(), indent=2))
1227
+ else:
1228
+ print_health(panel)
1229
+
1230
+ elif args.command == "patterns":
1231
+ patterns = panel.list_patterns(classification=args.classification)
1232
+ if args.json:
1233
+ print(json.dumps(patterns, indent=2))
1234
+ else:
1235
+ print(f"\nPatterns ({len(patterns)} found):")
1236
+ for p in patterns:
1237
+ print(
1238
+ f" [{p.get('classification', '?')}] {p.get('pattern_id', '?')} ({p.get('pattern_type', '?')})",
1239
+ )
1240
+
1241
+ elif args.command == "export":
1242
+ output = args.output or "patterns_export.json"
1243
+ count = panel.export_patterns(output, classification=args.classification)
1244
+ print(f"✓ Exported {count} patterns to {output}")
1245
+
1246
+ elif args.command == "api":
1247
+ # Parse CORS origins
1248
+ cors_origins = None
1249
+ if args.cors_origins:
1250
+ cors_origins = [o.strip() for o in args.cors_origins.split(",")]
1251
+
1252
+ run_api_server(
1253
+ panel,
1254
+ host=args.host,
1255
+ port=args.api_port,
1256
+ api_key=args.api_key,
1257
+ enable_rate_limit=not args.no_rate_limit,
1258
+ rate_limit_requests=args.rate_limit,
1259
+ ssl_certfile=args.ssl_cert,
1260
+ ssl_keyfile=args.ssl_key,
1261
+ allowed_origins=cors_origins,
1262
+ )
1263
+
1264
+ elif args.command == "serve":
1265
+ # Start Redis first
1266
+ print("\n" + "=" * 50)
1267
+ print("EMPATHY MEMORY - STARTING SERVICES")
1268
+ print("=" * 50)
1269
+
1270
+ print("\n[1/2] Starting Redis...")
1271
+ redis_status = panel.start_redis(verbose=False)
1272
+ if redis_status.available:
1273
+ print(f" ✓ Redis running via {redis_status.method.value}")
1274
+ else:
1275
+ print(f" ⚠ Redis not available: {redis_status.message}")
1276
+ print(" (Continuing with mock memory)")
1277
+
1278
+ # Parse CORS origins
1279
+ cors_origins = None
1280
+ if args.cors_origins:
1281
+ cors_origins = [o.strip() for o in args.cors_origins.split(",")]
1282
+
1283
+ print("\n[2/2] Starting API server...")
1284
+ run_api_server(
1285
+ panel,
1286
+ host=args.host,
1287
+ port=args.api_port,
1288
+ api_key=args.api_key,
1289
+ enable_rate_limit=not args.no_rate_limit,
1290
+ rate_limit_requests=args.rate_limit,
1291
+ ssl_certfile=args.ssl_cert,
1292
+ ssl_keyfile=args.ssl_key,
1293
+ allowed_origins=cors_origins,
1294
+ )
1295
+
1296
+
1297
+ if __name__ == "__main__":
1298
+ main()