attune-ai 2.1.4__py3-none-any.whl → 2.2.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.
Files changed (123) hide show
  1. attune/cli/__init__.py +3 -55
  2. attune/cli/commands/batch.py +4 -12
  3. attune/cli/commands/cache.py +7 -15
  4. attune/cli/commands/provider.py +17 -0
  5. attune/cli/commands/routing.py +3 -1
  6. attune/cli/commands/setup.py +122 -0
  7. attune/cli/commands/tier.py +1 -3
  8. attune/cli/commands/workflow.py +31 -0
  9. attune/cli/parsers/cache.py +1 -0
  10. attune/cli/parsers/help.py +1 -3
  11. attune/cli/parsers/provider.py +7 -0
  12. attune/cli/parsers/routing.py +1 -3
  13. attune/cli/parsers/setup.py +7 -0
  14. attune/cli/parsers/status.py +1 -3
  15. attune/cli/parsers/tier.py +1 -3
  16. attune/cli_minimal.py +34 -28
  17. attune/cli_router.py +9 -7
  18. attune/cli_unified.py +3 -0
  19. attune/core.py +190 -0
  20. attune/dashboard/app.py +4 -2
  21. attune/dashboard/simple_server.py +3 -1
  22. attune/dashboard/standalone_server.py +7 -3
  23. attune/mcp/server.py +54 -102
  24. attune/memory/long_term.py +0 -2
  25. attune/memory/short_term/__init__.py +84 -0
  26. attune/memory/short_term/base.py +467 -0
  27. attune/memory/short_term/batch.py +219 -0
  28. attune/memory/short_term/caching.py +227 -0
  29. attune/memory/short_term/conflicts.py +265 -0
  30. attune/memory/short_term/cross_session.py +122 -0
  31. attune/memory/short_term/facade.py +655 -0
  32. attune/memory/short_term/pagination.py +215 -0
  33. attune/memory/short_term/patterns.py +271 -0
  34. attune/memory/short_term/pubsub.py +286 -0
  35. attune/memory/short_term/queues.py +244 -0
  36. attune/memory/short_term/security.py +300 -0
  37. attune/memory/short_term/sessions.py +250 -0
  38. attune/memory/short_term/streams.py +249 -0
  39. attune/memory/short_term/timelines.py +234 -0
  40. attune/memory/short_term/transactions.py +186 -0
  41. attune/memory/short_term/working.py +252 -0
  42. attune/meta_workflows/cli_commands/__init__.py +3 -0
  43. attune/meta_workflows/cli_commands/agent_commands.py +0 -4
  44. attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
  45. attune/meta_workflows/cli_commands/config_commands.py +0 -5
  46. attune/meta_workflows/cli_commands/memory_commands.py +0 -5
  47. attune/meta_workflows/cli_commands/template_commands.py +0 -5
  48. attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
  49. attune/meta_workflows/workflow.py +1 -1
  50. attune/models/adaptive_routing.py +4 -8
  51. attune/models/auth_cli.py +3 -9
  52. attune/models/auth_strategy.py +2 -4
  53. attune/models/provider_config.py +20 -1
  54. attune/models/telemetry/analytics.py +0 -2
  55. attune/models/telemetry/backend.py +0 -3
  56. attune/models/telemetry/storage.py +0 -2
  57. attune/orchestration/_strategies/__init__.py +156 -0
  58. attune/orchestration/_strategies/base.py +231 -0
  59. attune/orchestration/_strategies/conditional_strategies.py +373 -0
  60. attune/orchestration/_strategies/conditions.py +369 -0
  61. attune/orchestration/_strategies/core_strategies.py +491 -0
  62. attune/orchestration/_strategies/data_classes.py +64 -0
  63. attune/orchestration/_strategies/nesting.py +233 -0
  64. attune/orchestration/execution_strategies.py +58 -1567
  65. attune/orchestration/meta_orchestrator.py +1 -3
  66. attune/project_index/scanner.py +1 -3
  67. attune/project_index/scanner_parallel.py +7 -5
  68. attune/socratic_router.py +1 -3
  69. attune/telemetry/agent_coordination.py +9 -3
  70. attune/telemetry/agent_tracking.py +16 -3
  71. attune/telemetry/approval_gates.py +22 -5
  72. attune/telemetry/cli.py +3 -3
  73. attune/telemetry/commands/dashboard_commands.py +24 -8
  74. attune/telemetry/event_streaming.py +8 -2
  75. attune/telemetry/feedback_loop.py +10 -2
  76. attune/tools.py +1 -0
  77. attune/workflow_commands.py +1 -3
  78. attune/workflows/__init__.py +53 -10
  79. attune/workflows/autonomous_test_gen.py +160 -104
  80. attune/workflows/base.py +48 -664
  81. attune/workflows/batch_processing.py +2 -4
  82. attune/workflows/compat.py +156 -0
  83. attune/workflows/cost_mixin.py +141 -0
  84. attune/workflows/data_classes.py +92 -0
  85. attune/workflows/document_gen/workflow.py +11 -14
  86. attune/workflows/history.py +62 -37
  87. attune/workflows/llm_base.py +2 -4
  88. attune/workflows/migration.py +422 -0
  89. attune/workflows/output.py +3 -9
  90. attune/workflows/parsing_mixin.py +427 -0
  91. attune/workflows/perf_audit.py +3 -1
  92. attune/workflows/progress.py +10 -13
  93. attune/workflows/release_prep.py +5 -1
  94. attune/workflows/routing.py +0 -2
  95. attune/workflows/secure_release.py +2 -1
  96. attune/workflows/security_audit.py +19 -14
  97. attune/workflows/security_audit_phase3.py +28 -22
  98. attune/workflows/seo_optimization.py +29 -29
  99. attune/workflows/test_gen/test_templates.py +1 -4
  100. attune/workflows/test_gen/workflow.py +0 -2
  101. attune/workflows/test_gen_behavioral.py +7 -20
  102. attune/workflows/test_gen_parallel.py +6 -4
  103. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/METADATA +4 -3
  104. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/RECORD +119 -94
  105. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/entry_points.txt +0 -2
  106. attune_healthcare/monitors/monitoring/__init__.py +9 -9
  107. attune_llm/agent_factory/__init__.py +6 -6
  108. attune_llm/commands/__init__.py +10 -10
  109. attune_llm/commands/models.py +3 -3
  110. attune_llm/config/__init__.py +8 -8
  111. attune_llm/learning/__init__.py +3 -3
  112. attune_llm/learning/extractor.py +5 -3
  113. attune_llm/learning/storage.py +5 -3
  114. attune_llm/security/__init__.py +17 -17
  115. attune_llm/utils/tokens.py +3 -1
  116. attune/cli_legacy.py +0 -3957
  117. attune/memory/short_term.py +0 -2192
  118. attune/workflows/manage_docs.py +0 -87
  119. attune/workflows/test5.py +0 -125
  120. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/WHEEL +0 -0
  121. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE +0 -0
  122. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  123. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,300 @@
1
+ """Data sanitization - PII scrubbing and secrets detection.
2
+
3
+ This module handles security-sensitive data processing:
4
+ - PII (Personally Identifiable Information) scrubbing
5
+ - Secrets detection (API keys, passwords, tokens)
6
+ - Data sanitization before storage
7
+
8
+ Classes:
9
+ DataSanitizer: Handles PII and secrets scrubbing
10
+
11
+ Dependencies:
12
+ PIIScrubber: External PII detection/removal
13
+ SecretsDetector: External secrets detection
14
+
15
+ Example:
16
+ >>> from attune.memory.short_term.security import DataSanitizer
17
+ >>> from attune.memory.types import RedisMetrics
18
+ >>> sanitizer = DataSanitizer(
19
+ ... pii_scrub_enabled=True,
20
+ ... secrets_detection_enabled=True,
21
+ ... metrics=RedisMetrics()
22
+ ... )
23
+ >>> clean_data, pii_count = sanitizer.sanitize({"email": "user@example.com"})
24
+ >>> print(f"Scrubbed {pii_count} PII items")
25
+
26
+ Copyright 2025 Smart-AI-Memory
27
+ Licensed under Fair Source License 0.9
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ from typing import TYPE_CHECKING, Any
34
+
35
+ import structlog
36
+
37
+ from attune.memory.security.pii_scrubber import PIIScrubber
38
+ from attune.memory.security.secrets_detector import SecretsDetector
39
+ from attune.memory.security.secrets_detector import Severity as SecretSeverity
40
+ from attune.memory.types import RedisMetrics, SecurityError
41
+
42
+ if TYPE_CHECKING:
43
+ pass
44
+
45
+ logger = structlog.get_logger(__name__)
46
+
47
+
48
+ class DataSanitizer:
49
+ """Handles data sanitization for short-term memory.
50
+
51
+ Provides PII scrubbing and secrets detection to protect
52
+ sensitive information from being stored in Redis.
53
+
54
+ PII Detection:
55
+ - Email addresses
56
+ - Social Security Numbers (SSN)
57
+ - Phone numbers
58
+ - Credit card numbers
59
+
60
+ Secrets Detection:
61
+ - API keys
62
+ - Passwords
63
+ - Tokens
64
+ - Private keys
65
+
66
+ Attributes:
67
+ pii_enabled: Whether PII scrubbing is active
68
+ secrets_enabled: Whether secrets detection is active
69
+ metrics: RedisMetrics instance for tracking
70
+
71
+ Example:
72
+ >>> sanitizer = DataSanitizer(
73
+ ... pii_scrub_enabled=True,
74
+ ... secrets_detection_enabled=True,
75
+ ... metrics=RedisMetrics()
76
+ ... )
77
+ >>> data = {"user": "john", "email": "john@example.com"}
78
+ >>> clean, count = sanitizer.sanitize(data)
79
+ >>> print(clean) # Email will be redacted
80
+ {'user': 'john', 'email': '[EMAIL]'}
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ pii_scrub_enabled: bool = False,
86
+ secrets_detection_enabled: bool = False,
87
+ metrics: RedisMetrics | None = None,
88
+ ) -> None:
89
+ """Initialize data sanitizer.
90
+
91
+ Args:
92
+ pii_scrub_enabled: Enable PII scrubbing (emails, SSN, etc.)
93
+ secrets_detection_enabled: Enable secrets detection (API keys, etc.)
94
+ metrics: Optional RedisMetrics for tracking scrub operations
95
+ """
96
+ self.pii_enabled = pii_scrub_enabled
97
+ self.secrets_enabled = secrets_detection_enabled
98
+ self._metrics = metrics or RedisMetrics()
99
+
100
+ # Initialize scrubbers
101
+ self._pii_scrubber: PIIScrubber | None = None
102
+ self._secrets_detector: SecretsDetector | None = None
103
+
104
+ if pii_scrub_enabled:
105
+ self._pii_scrubber = PIIScrubber(enable_name_detection=False)
106
+ logger.debug(
107
+ "pii_scrubber_enabled",
108
+ message="PII scrubbing active for data sanitizer",
109
+ )
110
+
111
+ if secrets_detection_enabled:
112
+ self._secrets_detector = SecretsDetector()
113
+ logger.debug(
114
+ "secrets_detector_enabled",
115
+ message="Secrets detection active for data sanitizer",
116
+ )
117
+
118
+ @property
119
+ def metrics(self) -> RedisMetrics:
120
+ """Get metrics instance."""
121
+ return self._metrics
122
+
123
+ def sanitize(self, data: Any) -> tuple[Any, int]:
124
+ """Sanitize data by scrubbing PII and checking for secrets.
125
+
126
+ Performs two-step sanitization:
127
+ 1. Checks for secrets (API keys, passwords) - blocks if found
128
+ 2. Scrubs PII (emails, SSN, phone numbers) - redacts in place
129
+
130
+ Args:
131
+ data: Data to sanitize (dict, list, or str)
132
+
133
+ Returns:
134
+ Tuple of (sanitized_data, pii_count)
135
+ - sanitized_data: Data with PII redacted
136
+ - pii_count: Number of PII items found and scrubbed
137
+
138
+ Raises:
139
+ SecurityError: If critical/high severity secrets are detected
140
+
141
+ Example:
142
+ >>> sanitizer = DataSanitizer(pii_scrub_enabled=True)
143
+ >>> data = {"contact": "user@example.com"}
144
+ >>> clean, count = sanitizer.sanitize(data)
145
+ >>> count
146
+ 1
147
+ """
148
+ pii_count = 0
149
+
150
+ if data is None:
151
+ return data, 0
152
+
153
+ # Convert data to string for scanning
154
+ if isinstance(data, dict):
155
+ data_str = json.dumps(data)
156
+ elif isinstance(data, list):
157
+ data_str = json.dumps(data)
158
+ elif isinstance(data, str):
159
+ data_str = data
160
+ else:
161
+ # For other types, convert to string
162
+ data_str = str(data)
163
+
164
+ # Check for secrets first (before modifying data)
165
+ if self._secrets_detector is not None:
166
+ detections = self._secrets_detector.detect(data_str)
167
+ # Block critical and high severity secrets
168
+ critical_secrets = [
169
+ d
170
+ for d in detections
171
+ if d.severity in (SecretSeverity.CRITICAL, SecretSeverity.HIGH)
172
+ ]
173
+ if critical_secrets:
174
+ self._metrics.secrets_blocked_total += len(critical_secrets)
175
+ secret_types = [d.secret_type.value for d in critical_secrets]
176
+ logger.warning(
177
+ "secrets_detected_blocked",
178
+ secret_types=secret_types,
179
+ count=len(critical_secrets),
180
+ )
181
+ raise SecurityError(
182
+ f"Cannot store data containing secrets: {secret_types}. "
183
+ "Remove sensitive credentials before storing."
184
+ )
185
+
186
+ # Scrub PII
187
+ if self._pii_scrubber is not None:
188
+ sanitized_str, pii_detections = self._pii_scrubber.scrub(data_str)
189
+ pii_count = len(pii_detections)
190
+
191
+ if pii_count > 0:
192
+ self._metrics.pii_scrubbed_total += pii_count
193
+ self._metrics.pii_scrub_operations += 1
194
+ logger.debug(
195
+ "pii_scrubbed",
196
+ pii_count=pii_count,
197
+ pii_types=[d.pii_type for d in pii_detections],
198
+ )
199
+
200
+ # Convert back to original type
201
+ if isinstance(data, dict):
202
+ try:
203
+ return json.loads(sanitized_str), pii_count
204
+ except json.JSONDecodeError:
205
+ # If PII scrubbing broke JSON structure, return original
206
+ # This can happen if regex matches part of JSON syntax
207
+ logger.warning("pii_scrubbing_broke_json_returning_original")
208
+ return data, 0
209
+ elif isinstance(data, list):
210
+ try:
211
+ return json.loads(sanitized_str), pii_count
212
+ except json.JSONDecodeError:
213
+ logger.warning("pii_scrubbing_broke_json_returning_original")
214
+ return data, 0
215
+ else:
216
+ return sanitized_str, pii_count
217
+
218
+ return data, pii_count
219
+
220
+ def check_secrets(self, data: Any) -> list[str]:
221
+ """Check data for secrets without blocking.
222
+
223
+ Args:
224
+ data: Data to check (dict, list, or str)
225
+
226
+ Returns:
227
+ List of detected secret types (empty if none found)
228
+
229
+ Example:
230
+ >>> sanitizer = DataSanitizer(secrets_detection_enabled=True)
231
+ >>> secrets = sanitizer.check_secrets({"key": "sk-abc123..."})
232
+ >>> if secrets:
233
+ ... print(f"Found secrets: {secrets}")
234
+ """
235
+ if self._secrets_detector is None or data is None:
236
+ return []
237
+
238
+ # Convert data to string for scanning
239
+ if isinstance(data, dict):
240
+ data_str = json.dumps(data)
241
+ elif isinstance(data, list):
242
+ data_str = json.dumps(data)
243
+ elif isinstance(data, str):
244
+ data_str = data
245
+ else:
246
+ data_str = str(data)
247
+
248
+ detections = self._secrets_detector.detect(data_str)
249
+ return [d.secret_type.value for d in detections]
250
+
251
+ def scrub_pii_only(self, data: Any) -> tuple[Any, int]:
252
+ """Scrub only PII from data (no secrets blocking).
253
+
254
+ Args:
255
+ data: Data to scrub (dict, list, or str)
256
+
257
+ Returns:
258
+ Tuple of (scrubbed_data, pii_count)
259
+
260
+ Example:
261
+ >>> sanitizer = DataSanitizer(pii_scrub_enabled=True)
262
+ >>> clean, count = sanitizer.scrub_pii_only("Email: user@example.com")
263
+ >>> print(clean) # "Email: [EMAIL]"
264
+ """
265
+ if self._pii_scrubber is None or data is None:
266
+ return data, 0
267
+
268
+ # Convert data to string
269
+ if isinstance(data, dict):
270
+ data_str = json.dumps(data)
271
+ elif isinstance(data, list):
272
+ data_str = json.dumps(data)
273
+ elif isinstance(data, str):
274
+ data_str = data
275
+ else:
276
+ data_str = str(data)
277
+
278
+ sanitized_str, pii_detections = self._pii_scrubber.scrub(data_str)
279
+ pii_count = len(pii_detections)
280
+
281
+ if pii_count == 0:
282
+ return data, 0
283
+
284
+ # Update metrics
285
+ self._metrics.pii_scrubbed_total += pii_count
286
+ self._metrics.pii_scrub_operations += 1
287
+
288
+ # Convert back to original type
289
+ if isinstance(data, dict):
290
+ try:
291
+ return json.loads(sanitized_str), pii_count
292
+ except json.JSONDecodeError:
293
+ return data, 0
294
+ elif isinstance(data, list):
295
+ try:
296
+ return json.loads(sanitized_str), pii_count
297
+ except json.JSONDecodeError:
298
+ return data, 0
299
+ else:
300
+ return sanitized_str, pii_count
@@ -0,0 +1,250 @@
1
+ """Collaboration session management.
2
+
3
+ This module manages multi-agent collaboration sessions:
4
+ - Create: Start a new collaboration session
5
+ - Join: Add agents to existing session
6
+ - Get: Retrieve session information
7
+
8
+ Key Prefix: PREFIX_SESSION = "empathy:session:"
9
+
10
+ Classes:
11
+ SessionManager: Collaboration session operations
12
+
13
+ Example:
14
+ >>> from attune.memory.short_term.sessions import SessionManager
15
+ >>> from attune.memory.types import AgentCredentials, AccessTier
16
+ >>> sessions = SessionManager(base_ops)
17
+ >>> creds = AgentCredentials("agent_1", AccessTier.CONTRIBUTOR)
18
+ >>> sessions.create_session("session_1", creds, {"topic": "refactoring"})
19
+ >>> sessions.join_session("session_1", other_agent_creds)
20
+
21
+ Copyright 2025 Smart-AI-Memory
22
+ Licensed under Fair Source License 0.9
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ from datetime import datetime
29
+ from typing import TYPE_CHECKING, Any
30
+
31
+ import structlog
32
+
33
+ from attune.memory.types import (
34
+ AgentCredentials,
35
+ TTLStrategy,
36
+ )
37
+
38
+ if TYPE_CHECKING:
39
+ from attune.memory.short_term.base import BaseOperations
40
+
41
+ logger = structlog.get_logger(__name__)
42
+
43
+
44
+ class SessionManager:
45
+ """Collaboration session operations.
46
+
47
+ Manages multi-agent collaboration sessions including creation,
48
+ joining, and retrieval. Sessions track participants and metadata.
49
+
50
+ The class is designed to be composed with BaseOperations
51
+ for dependency injection.
52
+
53
+ Attributes:
54
+ PREFIX_SESSION: Key prefix for session namespace
55
+
56
+ Example:
57
+ >>> sessions = SessionManager(base_ops)
58
+ >>> creds = AgentCredentials("agent_1", AccessTier.CONTRIBUTOR)
59
+ >>> sessions.create_session("session_1", creds)
60
+ True
61
+ >>> info = sessions.get_session("session_1", creds)
62
+ """
63
+
64
+ PREFIX_SESSION = "empathy:session:"
65
+
66
+ def __init__(self, base: BaseOperations) -> None:
67
+ """Initialize session manager.
68
+
69
+ Args:
70
+ base: BaseOperations instance for storage access
71
+ """
72
+ self._base = base
73
+
74
+ def create_session(
75
+ self,
76
+ session_id: str,
77
+ credentials: AgentCredentials,
78
+ metadata: dict | None = None,
79
+ ) -> bool:
80
+ """Create a collaboration session.
81
+
82
+ Args:
83
+ session_id: Unique session identifier
84
+ credentials: Session creator
85
+ metadata: Optional session metadata
86
+
87
+ Returns:
88
+ True if created
89
+
90
+ Raises:
91
+ ValueError: If session_id is empty
92
+ TypeError: If metadata is not dict
93
+
94
+ Example:
95
+ >>> sessions.create_session(
96
+ ... "session_1",
97
+ ... creds,
98
+ ... metadata={"topic": "code review"},
99
+ ... )
100
+ True
101
+ """
102
+ # Pattern 1: String ID validation
103
+ if not session_id or not session_id.strip():
104
+ raise ValueError(f"session_id cannot be empty. Got: {session_id!r}")
105
+
106
+ # Pattern 5: Type validation
107
+ if metadata is not None and not isinstance(metadata, dict):
108
+ raise TypeError(f"metadata must be dict, got {type(metadata).__name__}")
109
+
110
+ key = f"{self.PREFIX_SESSION}{session_id}"
111
+ payload = {
112
+ "session_id": session_id,
113
+ "created_by": credentials.agent_id,
114
+ "created_at": datetime.now().isoformat(),
115
+ "participants": [credentials.agent_id],
116
+ "metadata": metadata or {},
117
+ }
118
+
119
+ success = self._base._set(key, json.dumps(payload), TTLStrategy.SESSION.value)
120
+
121
+ if success:
122
+ logger.info(
123
+ "session_created",
124
+ session_id=session_id,
125
+ created_by=credentials.agent_id,
126
+ )
127
+
128
+ return success
129
+
130
+ def join_session(
131
+ self,
132
+ session_id: str,
133
+ credentials: AgentCredentials,
134
+ ) -> bool:
135
+ """Join an existing session.
136
+
137
+ Args:
138
+ session_id: Session to join
139
+ credentials: Joining agent
140
+
141
+ Returns:
142
+ True if joined
143
+
144
+ Raises:
145
+ ValueError: If session_id is empty
146
+
147
+ Example:
148
+ >>> sessions.join_session("session_1", agent2_creds)
149
+ True
150
+ """
151
+ # Pattern 1: String ID validation
152
+ if not session_id or not session_id.strip():
153
+ raise ValueError(f"session_id cannot be empty. Got: {session_id!r}")
154
+
155
+ key = f"{self.PREFIX_SESSION}{session_id}"
156
+ raw = self._base._get(key)
157
+
158
+ if raw is None:
159
+ return False
160
+
161
+ payload = json.loads(raw)
162
+ if credentials.agent_id not in payload["participants"]:
163
+ payload["participants"].append(credentials.agent_id)
164
+ logger.info(
165
+ "session_joined",
166
+ session_id=session_id,
167
+ agent_id=credentials.agent_id,
168
+ )
169
+
170
+ return self._base._set(key, json.dumps(payload), TTLStrategy.SESSION.value)
171
+
172
+ def get_session(
173
+ self,
174
+ session_id: str,
175
+ credentials: AgentCredentials,
176
+ ) -> dict[str, Any] | None:
177
+ """Get session information.
178
+
179
+ Args:
180
+ session_id: Session identifier
181
+ credentials: Any participant can read
182
+
183
+ Returns:
184
+ Session data or None if not found
185
+
186
+ Example:
187
+ >>> info = sessions.get_session("session_1", creds)
188
+ >>> if info:
189
+ ... print(f"Participants: {info['participants']}")
190
+ """
191
+ key = f"{self.PREFIX_SESSION}{session_id}"
192
+ raw = self._base._get(key)
193
+
194
+ if raw is None:
195
+ return None
196
+
197
+ result: dict[str, Any] = json.loads(raw)
198
+ return result
199
+
200
+ def leave_session(
201
+ self,
202
+ session_id: str,
203
+ credentials: AgentCredentials,
204
+ ) -> bool:
205
+ """Leave a session.
206
+
207
+ Args:
208
+ session_id: Session to leave
209
+ credentials: Leaving agent
210
+
211
+ Returns:
212
+ True if left successfully
213
+ """
214
+ if not session_id or not session_id.strip():
215
+ raise ValueError(f"session_id cannot be empty. Got: {session_id!r}")
216
+
217
+ key = f"{self.PREFIX_SESSION}{session_id}"
218
+ raw = self._base._get(key)
219
+
220
+ if raw is None:
221
+ return False
222
+
223
+ payload = json.loads(raw)
224
+ if credentials.agent_id in payload["participants"]:
225
+ payload["participants"].remove(credentials.agent_id)
226
+ logger.info(
227
+ "session_left",
228
+ session_id=session_id,
229
+ agent_id=credentials.agent_id,
230
+ )
231
+ return self._base._set(key, json.dumps(payload), TTLStrategy.SESSION.value)
232
+
233
+ return False
234
+
235
+ def list_sessions(self) -> list[dict[str, Any]]:
236
+ """List all active sessions.
237
+
238
+ Returns:
239
+ List of session data dicts
240
+ """
241
+ pattern = f"{self.PREFIX_SESSION}*"
242
+ keys = self._base._keys(pattern)
243
+ sessions = []
244
+
245
+ for key in keys:
246
+ raw = self._base._get(key)
247
+ if raw:
248
+ sessions.append(json.loads(raw))
249
+
250
+ return sessions