gateforge-sdk 0.2.4__tar.gz → 0.2.5__tar.gz
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.
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/PKG-INFO +2 -2
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/__init__.py +18 -1
- gateforge_sdk-0.2.5/gateforge/session_manager.py +344 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/pyproject.toml +2 -2
- gateforge_sdk-0.2.5/tests/test_phase2.py +528 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/.env.example +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/.gitignore +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/.pypirc.example +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/INSTALL.md +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/LICENSE +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/MANIFEST.in +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/PUBLISHING.md +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/PUBLISH_NOW.md +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/README.md +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/ab/__init__.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/ab/engine.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/client.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/config.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/context.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/features/__init__.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/guardrails/__init__.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/guardrails/engine.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/metrics.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/options.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/pii.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/pricing.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/prompt.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/providers/__init__.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/providers/anthropic.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/providers/gemini.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/providers/openai.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/response.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/tracing.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/wrappers/__init__.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/wrappers/anthropic.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/wrappers/gemini.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/gateforge/wrappers/openai.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/tests/__init__.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/tests/test_metrics.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/tests/test_phase1.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/tests/test_pii.py +0 -0
- {gateforge_sdk-0.2.4 → gateforge_sdk-0.2.5}/tests/test_providers.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gateforge-sdk
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session management, nested conversation support
|
|
5
5
|
Project-URL: Homepage, https://gateforge.dev
|
|
6
6
|
Project-URL: Documentation, https://gateforge.dev/docs
|
|
7
7
|
Project-URL: Repository, https://github.com/gateforge/gateforge-sdk
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.5"
|
|
4
4
|
|
|
5
5
|
# Auto-install spaCy model if missing (required for PII detection)
|
|
6
6
|
def _ensure_spacy_model():
|
|
@@ -26,6 +26,15 @@ from gateforge.response import GatforgeResponse
|
|
|
26
26
|
from gateforge.tracing import configure_otel
|
|
27
27
|
from gateforge.ab.engine import VariantAssignment
|
|
28
28
|
from gateforge.guardrails.engine import GuardrailViolation, GuardrailBlocked
|
|
29
|
+
from gateforge.session_manager import (
|
|
30
|
+
SessionManager,
|
|
31
|
+
SessionState,
|
|
32
|
+
get_session_manager,
|
|
33
|
+
create_session,
|
|
34
|
+
get_session,
|
|
35
|
+
get_current_conversation_id,
|
|
36
|
+
get_current_trace_info,
|
|
37
|
+
)
|
|
29
38
|
|
|
30
39
|
_client: GatforgeClient | None = None
|
|
31
40
|
|
|
@@ -426,6 +435,14 @@ __all__ = [
|
|
|
426
435
|
"agent",
|
|
427
436
|
"record_tool_call",
|
|
428
437
|
"record_tool_result",
|
|
438
|
+
# Session management (Phase 2)
|
|
439
|
+
"SessionManager",
|
|
440
|
+
"SessionState",
|
|
441
|
+
"get_session_manager",
|
|
442
|
+
"create_session",
|
|
443
|
+
"get_session",
|
|
444
|
+
"get_current_conversation_id",
|
|
445
|
+
"get_current_trace_info",
|
|
429
446
|
# OTel configuration
|
|
430
447
|
"configure_otel",
|
|
431
448
|
# Standalone PII helpers
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phase 2: Session Management & Enhanced Trace Context
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- SessionManager for persistent session state
|
|
6
|
+
- Enhanced metadata support (user_id, tags, custom fields)
|
|
7
|
+
- Session persistence (save/load from dict)
|
|
8
|
+
- Conversation helpers
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import uuid
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Optional, Dict, Any, List
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SessionState:
|
|
22
|
+
"""
|
|
23
|
+
Represents the state of a conversation session.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
conversation_id: Unique identifier for the conversation
|
|
27
|
+
user_id: Optional user identifier
|
|
28
|
+
created_at: Unix timestamp when session was created
|
|
29
|
+
updated_at: Unix timestamp of last activity
|
|
30
|
+
message_count: Number of messages/turns in this conversation
|
|
31
|
+
tags: Optional tags for categorization
|
|
32
|
+
metadata: Custom metadata dictionary
|
|
33
|
+
"""
|
|
34
|
+
conversation_id: str
|
|
35
|
+
user_id: Optional[str] = None
|
|
36
|
+
created_at: float = field(default_factory=time.time)
|
|
37
|
+
updated_at: float = field(default_factory=time.time)
|
|
38
|
+
message_count: int = 0
|
|
39
|
+
tags: List[str] = field(default_factory=list)
|
|
40
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
43
|
+
"""Serialize session state to dictionary."""
|
|
44
|
+
return {
|
|
45
|
+
"conversation_id": self.conversation_id,
|
|
46
|
+
"user_id": self.user_id,
|
|
47
|
+
"created_at": self.created_at,
|
|
48
|
+
"updated_at": self.updated_at,
|
|
49
|
+
"message_count": self.message_count,
|
|
50
|
+
"tags": self.tags,
|
|
51
|
+
"metadata": self.metadata,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, data: Dict[str, Any]) -> SessionState:
|
|
56
|
+
"""Deserialize session state from dictionary."""
|
|
57
|
+
return cls(
|
|
58
|
+
conversation_id=data["conversation_id"],
|
|
59
|
+
user_id=data.get("user_id"),
|
|
60
|
+
created_at=data.get("created_at", time.time()),
|
|
61
|
+
updated_at=data.get("updated_at", time.time()),
|
|
62
|
+
message_count=data.get("message_count", 0),
|
|
63
|
+
tags=data.get("tags", []),
|
|
64
|
+
metadata=data.get("metadata", {}),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def touch(self) -> None:
|
|
68
|
+
"""Update the updated_at timestamp and increment message count."""
|
|
69
|
+
self.updated_at = time.time()
|
|
70
|
+
self.message_count += 1
|
|
71
|
+
|
|
72
|
+
def add_tag(self, tag: str) -> None:
|
|
73
|
+
"""Add a tag to the session."""
|
|
74
|
+
if tag not in self.tags:
|
|
75
|
+
self.tags.append(tag)
|
|
76
|
+
|
|
77
|
+
def set_metadata(self, key: str, value: Any) -> None:
|
|
78
|
+
"""Set a metadata field."""
|
|
79
|
+
self.metadata[key] = value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SessionManager:
|
|
83
|
+
"""
|
|
84
|
+
Manages conversation sessions with persistence support.
|
|
85
|
+
|
|
86
|
+
Usage::
|
|
87
|
+
# Create manager
|
|
88
|
+
manager = SessionManager()
|
|
89
|
+
|
|
90
|
+
# Create new session
|
|
91
|
+
session = manager.create_session(user_id="user-123")
|
|
92
|
+
print(session.conversation_id)
|
|
93
|
+
|
|
94
|
+
# Get existing session
|
|
95
|
+
session = manager.get_session("conv-123")
|
|
96
|
+
|
|
97
|
+
# Update session activity
|
|
98
|
+
manager.touch("conv-123")
|
|
99
|
+
|
|
100
|
+
# Serialize all sessions
|
|
101
|
+
data = manager.to_dict()
|
|
102
|
+
|
|
103
|
+
# Load from serialized data
|
|
104
|
+
manager2 = SessionManager.from_dict(data)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self):
|
|
108
|
+
self._sessions: Dict[str, SessionState] = {}
|
|
109
|
+
|
|
110
|
+
def create_session(
|
|
111
|
+
self,
|
|
112
|
+
user_id: Optional[str] = None,
|
|
113
|
+
conversation_id: Optional[str] = None,
|
|
114
|
+
tags: Optional[List[str]] = None,
|
|
115
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
116
|
+
) -> SessionState:
|
|
117
|
+
"""
|
|
118
|
+
Create a new session.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
user_id: Optional user identifier
|
|
122
|
+
conversation_id: Optional explicit conversation_id (auto-generated if not provided)
|
|
123
|
+
tags: Optional list of tags
|
|
124
|
+
metadata: Optional custom metadata
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
New SessionState object
|
|
128
|
+
"""
|
|
129
|
+
cid = conversation_id or str(uuid.uuid4())
|
|
130
|
+
|
|
131
|
+
session = SessionState(
|
|
132
|
+
conversation_id=cid,
|
|
133
|
+
user_id=user_id,
|
|
134
|
+
tags=tags or [],
|
|
135
|
+
metadata=metadata or {},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self._sessions[cid] = session
|
|
139
|
+
return session
|
|
140
|
+
|
|
141
|
+
def get_session(self, conversation_id: str) -> Optional[SessionState]:
|
|
142
|
+
"""
|
|
143
|
+
Get an existing session by ID.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
conversation_id: The conversation ID to look up
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
SessionState if found, None otherwise
|
|
150
|
+
"""
|
|
151
|
+
return self._sessions.get(conversation_id)
|
|
152
|
+
|
|
153
|
+
def get_or_create_session(
|
|
154
|
+
self,
|
|
155
|
+
conversation_id: str,
|
|
156
|
+
user_id: Optional[str] = None,
|
|
157
|
+
) -> SessionState:
|
|
158
|
+
"""
|
|
159
|
+
Get existing session or create new one if not exists.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
conversation_id: The conversation ID
|
|
163
|
+
user_id: Optional user_id for new session
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Existing or new SessionState
|
|
167
|
+
"""
|
|
168
|
+
if conversation_id in self._sessions:
|
|
169
|
+
return self._sessions[conversation_id]
|
|
170
|
+
|
|
171
|
+
return self.create_session(
|
|
172
|
+
conversation_id=conversation_id,
|
|
173
|
+
user_id=user_id,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def touch(self, conversation_id: str) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Update session activity timestamp and message count.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
conversation_id: The conversation ID to touch
|
|
182
|
+
"""
|
|
183
|
+
session = self._sessions.get(conversation_id)
|
|
184
|
+
if session:
|
|
185
|
+
session.touch()
|
|
186
|
+
|
|
187
|
+
def delete_session(self, conversation_id: str) -> bool:
|
|
188
|
+
"""
|
|
189
|
+
Delete a session.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
conversation_id: The conversation ID to delete
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if deleted, False if not found
|
|
196
|
+
"""
|
|
197
|
+
if conversation_id in self._sessions:
|
|
198
|
+
del self._sessions[conversation_id]
|
|
199
|
+
return True
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def list_sessions(
|
|
203
|
+
self,
|
|
204
|
+
user_id: Optional[str] = None,
|
|
205
|
+
tags: Optional[List[str]] = None,
|
|
206
|
+
limit: Optional[int] = None,
|
|
207
|
+
) -> List[SessionState]:
|
|
208
|
+
"""
|
|
209
|
+
List sessions with optional filters.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
user_id: Filter by user_id
|
|
213
|
+
tags: Filter by tags (must have all specified tags)
|
|
214
|
+
limit: Maximum number of sessions to return
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of matching SessionState objects
|
|
218
|
+
"""
|
|
219
|
+
sessions = list(self._sessions.values())
|
|
220
|
+
|
|
221
|
+
if user_id:
|
|
222
|
+
sessions = [s for s in sessions if s.user_id == user_id]
|
|
223
|
+
|
|
224
|
+
if tags:
|
|
225
|
+
sessions = [s for s in sessions if all(tag in s.tags for tag in tags)]
|
|
226
|
+
|
|
227
|
+
# Sort by updated_at (most recent first)
|
|
228
|
+
sessions.sort(key=lambda s: s.updated_at, reverse=True)
|
|
229
|
+
|
|
230
|
+
if limit:
|
|
231
|
+
sessions = sessions[:limit]
|
|
232
|
+
|
|
233
|
+
return sessions
|
|
234
|
+
|
|
235
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
236
|
+
"""Serialize all sessions to dictionary."""
|
|
237
|
+
return {
|
|
238
|
+
"sessions": {
|
|
239
|
+
cid: session.to_dict()
|
|
240
|
+
for cid, session in self._sessions.items()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def from_dict(cls, data: Dict[str, Any]) -> SessionManager:
|
|
246
|
+
"""Deserialize SessionManager from dictionary."""
|
|
247
|
+
manager = cls()
|
|
248
|
+
|
|
249
|
+
sessions_data = data.get("sessions", {})
|
|
250
|
+
for cid, session_data in sessions_data.items():
|
|
251
|
+
manager._sessions[cid] = SessionState.from_dict(session_data)
|
|
252
|
+
|
|
253
|
+
return manager
|
|
254
|
+
|
|
255
|
+
def __len__(self) -> int:
|
|
256
|
+
"""Return number of active sessions."""
|
|
257
|
+
return len(self._sessions)
|
|
258
|
+
|
|
259
|
+
def __contains__(self, conversation_id: str) -> bool:
|
|
260
|
+
"""Check if conversation_id exists."""
|
|
261
|
+
return conversation_id in self._sessions
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# Global session manager (optional convenience)
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
_default_manager: Optional[SessionManager] = None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_session_manager() -> SessionManager:
|
|
272
|
+
"""Get or create the default session manager."""
|
|
273
|
+
global _default_manager
|
|
274
|
+
if _default_manager is None:
|
|
275
|
+
_default_manager = SessionManager()
|
|
276
|
+
return _default_manager
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def create_session(
|
|
280
|
+
user_id: Optional[str] = None,
|
|
281
|
+
conversation_id: Optional[str] = None,
|
|
282
|
+
tags: Optional[List[str]] = None,
|
|
283
|
+
) -> SessionState:
|
|
284
|
+
"""
|
|
285
|
+
Create a new session using the default manager.
|
|
286
|
+
|
|
287
|
+
Usage::
|
|
288
|
+
session = gateforge.create_session(user_id="user-123")
|
|
289
|
+
print(session.conversation_id)
|
|
290
|
+
"""
|
|
291
|
+
return get_session_manager().create_session(
|
|
292
|
+
user_id=user_id,
|
|
293
|
+
conversation_id=conversation_id,
|
|
294
|
+
tags=tags,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_session(conversation_id: str) -> Optional[SessionState]:
|
|
299
|
+
"""
|
|
300
|
+
Get an existing session using the default manager.
|
|
301
|
+
|
|
302
|
+
Usage::
|
|
303
|
+
session = gateforge.get_session("conv-123")
|
|
304
|
+
if session:
|
|
305
|
+
print(f"Messages: {session.message_count}")
|
|
306
|
+
"""
|
|
307
|
+
return get_session_manager().get_session(conversation_id)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def get_current_conversation_id() -> Optional[str]:
|
|
311
|
+
"""
|
|
312
|
+
Get the conversation_id from the active trace context.
|
|
313
|
+
|
|
314
|
+
Usage::
|
|
315
|
+
cid = gateforge.get_current_conversation_id()
|
|
316
|
+
if cid:
|
|
317
|
+
print(f"Current conversation: {cid}")
|
|
318
|
+
"""
|
|
319
|
+
from gateforge.context import get_active_trace
|
|
320
|
+
trace = get_active_trace()
|
|
321
|
+
return trace.conversation_id if trace else None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def get_current_trace_info() -> Optional[Dict[str, Any]]:
|
|
325
|
+
"""
|
|
326
|
+
Get information about the current trace context.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Dictionary with conversation_id, trace_id, step, or None if no active trace
|
|
330
|
+
|
|
331
|
+
Usage::
|
|
332
|
+
info = gateforge.get_current_trace_info()
|
|
333
|
+
if info:
|
|
334
|
+
print(f"Conversation: {info['conversation_id']}, Step: {info['step']}")
|
|
335
|
+
"""
|
|
336
|
+
from gateforge.context import get_active_trace
|
|
337
|
+
trace = get_active_trace()
|
|
338
|
+
if trace:
|
|
339
|
+
return {
|
|
340
|
+
"conversation_id": trace.conversation_id,
|
|
341
|
+
"trace_id": trace.trace_id,
|
|
342
|
+
"step": trace.step,
|
|
343
|
+
}
|
|
344
|
+
return None
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gateforge-sdk"
|
|
7
|
-
version = "0.2.
|
|
8
|
-
description = "Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session
|
|
7
|
+
version = "0.2.5"
|
|
8
|
+
description = "Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session management, nested conversation support"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Phase 2 features:
|
|
3
|
+
- SessionManager
|
|
4
|
+
- SessionState
|
|
5
|
+
- Enhanced trace context helpers
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import pytest
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
# Add gateforge to path
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestSessionState:
|
|
18
|
+
"""Tests for SessionState dataclass."""
|
|
19
|
+
|
|
20
|
+
def test_create_session_state(self):
|
|
21
|
+
"""Should create SessionState with required fields."""
|
|
22
|
+
from gateforge.session_manager import SessionState
|
|
23
|
+
|
|
24
|
+
session = SessionState(conversation_id="conv-123")
|
|
25
|
+
|
|
26
|
+
assert session.conversation_id == "conv-123"
|
|
27
|
+
assert session.user_id is None
|
|
28
|
+
assert session.message_count == 0
|
|
29
|
+
assert session.tags == []
|
|
30
|
+
assert session.metadata == {}
|
|
31
|
+
|
|
32
|
+
def test_create_session_state_with_user(self):
|
|
33
|
+
"""Should create SessionState with user_id."""
|
|
34
|
+
from gateforge.session_manager import SessionState
|
|
35
|
+
|
|
36
|
+
session = SessionState(
|
|
37
|
+
conversation_id="conv-123",
|
|
38
|
+
user_id="user-456",
|
|
39
|
+
tags=["test", "phase2"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert session.user_id == "user-456"
|
|
43
|
+
assert session.tags == ["test", "phase2"]
|
|
44
|
+
|
|
45
|
+
def test_session_to_dict(self):
|
|
46
|
+
"""Should serialize SessionState to dictionary."""
|
|
47
|
+
from gateforge.session_manager import SessionState
|
|
48
|
+
|
|
49
|
+
session = SessionState(
|
|
50
|
+
conversation_id="conv-123",
|
|
51
|
+
user_id="user-456",
|
|
52
|
+
message_count=5,
|
|
53
|
+
tags=["tag1"],
|
|
54
|
+
metadata={"key": "value"},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
data = session.to_dict()
|
|
58
|
+
|
|
59
|
+
assert data["conversation_id"] == "conv-123"
|
|
60
|
+
assert data["user_id"] == "user-456"
|
|
61
|
+
assert data["message_count"] == 5
|
|
62
|
+
assert data["tags"] == ["tag1"]
|
|
63
|
+
assert data["metadata"]["key"] == "value"
|
|
64
|
+
|
|
65
|
+
def test_session_from_dict(self):
|
|
66
|
+
"""Should deserialize SessionState from dictionary."""
|
|
67
|
+
from gateforge.session_manager import SessionState
|
|
68
|
+
|
|
69
|
+
data = {
|
|
70
|
+
"conversation_id": "conv-789",
|
|
71
|
+
"user_id": "user-999",
|
|
72
|
+
"message_count": 10,
|
|
73
|
+
"tags": ["a", "b"],
|
|
74
|
+
"metadata": {"x": 1},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
session = SessionState.from_dict(data)
|
|
78
|
+
|
|
79
|
+
assert session.conversation_id == "conv-789"
|
|
80
|
+
assert session.user_id == "user-999"
|
|
81
|
+
assert session.message_count == 10
|
|
82
|
+
assert session.tags == ["a", "b"]
|
|
83
|
+
|
|
84
|
+
def test_session_touch(self):
|
|
85
|
+
"""Should update timestamp and increment message count."""
|
|
86
|
+
from gateforge.session_manager import SessionState
|
|
87
|
+
|
|
88
|
+
session = SessionState(conversation_id="conv-123", message_count=0)
|
|
89
|
+
old_updated = session.updated_at
|
|
90
|
+
|
|
91
|
+
time.sleep(0.01) # Small delay to ensure timestamp changes
|
|
92
|
+
session.touch()
|
|
93
|
+
|
|
94
|
+
assert session.message_count == 1
|
|
95
|
+
assert session.updated_at > old_updated
|
|
96
|
+
|
|
97
|
+
def test_session_add_tag(self):
|
|
98
|
+
"""Should add tag to session."""
|
|
99
|
+
from gateforge.session_manager import SessionState
|
|
100
|
+
|
|
101
|
+
session = SessionState(conversation_id="conv-123")
|
|
102
|
+
session.add_tag("weather")
|
|
103
|
+
session.add_tag("agent")
|
|
104
|
+
|
|
105
|
+
assert "weather" in session.tags
|
|
106
|
+
assert "agent" in session.tags
|
|
107
|
+
assert len(session.tags) == 2
|
|
108
|
+
|
|
109
|
+
def test_session_add_duplicate_tag(self):
|
|
110
|
+
"""Should not add duplicate tags."""
|
|
111
|
+
from gateforge.session_manager import SessionState
|
|
112
|
+
|
|
113
|
+
session = SessionState(conversation_id="conv-123")
|
|
114
|
+
session.add_tag("test")
|
|
115
|
+
session.add_tag("test") # Duplicate
|
|
116
|
+
|
|
117
|
+
assert session.tags == ["test"]
|
|
118
|
+
assert len(session.tags) == 1
|
|
119
|
+
|
|
120
|
+
def test_session_set_metadata(self):
|
|
121
|
+
"""Should set metadata field."""
|
|
122
|
+
from gateforge.session_manager import SessionState
|
|
123
|
+
|
|
124
|
+
session = SessionState(conversation_id="conv-123")
|
|
125
|
+
session.set_metadata("model", "gemini-2.5-flash")
|
|
126
|
+
session.set_metadata("temperature", 0.7)
|
|
127
|
+
|
|
128
|
+
assert session.metadata["model"] == "gemini-2.5-flash"
|
|
129
|
+
assert session.metadata["temperature"] == 0.7
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestSessionManager:
|
|
133
|
+
"""Tests for SessionManager."""
|
|
134
|
+
|
|
135
|
+
def test_create_session(self):
|
|
136
|
+
"""Should create new session."""
|
|
137
|
+
from gateforge.session_manager import SessionManager
|
|
138
|
+
|
|
139
|
+
manager = SessionManager()
|
|
140
|
+
session = manager.create_session(user_id="user-123")
|
|
141
|
+
|
|
142
|
+
assert session.conversation_id is not None
|
|
143
|
+
assert session.user_id == "user-123"
|
|
144
|
+
assert len(manager) == 1
|
|
145
|
+
|
|
146
|
+
def test_create_session_with_explicit_id(self):
|
|
147
|
+
"""Should create session with explicit conversation_id."""
|
|
148
|
+
from gateforge.session_manager import SessionManager
|
|
149
|
+
|
|
150
|
+
manager = SessionManager()
|
|
151
|
+
session = manager.create_session(
|
|
152
|
+
conversation_id="my-conv-123",
|
|
153
|
+
user_id="user-456",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert session.conversation_id == "my-conv-123"
|
|
157
|
+
assert session.user_id == "user-456"
|
|
158
|
+
|
|
159
|
+
def test_get_session(self):
|
|
160
|
+
"""Should retrieve existing session."""
|
|
161
|
+
from gateforge.session_manager import SessionManager
|
|
162
|
+
|
|
163
|
+
manager = SessionManager()
|
|
164
|
+
session = manager.create_session(user_id="user-789")
|
|
165
|
+
|
|
166
|
+
retrieved = manager.get_session(session.conversation_id)
|
|
167
|
+
|
|
168
|
+
assert retrieved is not None
|
|
169
|
+
assert retrieved.user_id == "user-789"
|
|
170
|
+
|
|
171
|
+
def test_get_session_not_found(self):
|
|
172
|
+
"""Should return None for non-existent session."""
|
|
173
|
+
from gateforge.session_manager import SessionManager
|
|
174
|
+
|
|
175
|
+
manager = SessionManager()
|
|
176
|
+
retrieved = manager.get_session("non-existent")
|
|
177
|
+
|
|
178
|
+
assert retrieved is None
|
|
179
|
+
|
|
180
|
+
def test_get_or_create_session_existing(self):
|
|
181
|
+
"""Should return existing session."""
|
|
182
|
+
from gateforge.session_manager import SessionManager
|
|
183
|
+
|
|
184
|
+
manager = SessionManager()
|
|
185
|
+
session1 = manager.create_session(
|
|
186
|
+
conversation_id="conv-123",
|
|
187
|
+
user_id="user-original",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
session2 = manager.get_or_create_session(
|
|
191
|
+
conversation_id="conv-123",
|
|
192
|
+
user_id="user-new", # Should be ignored
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
assert session2.user_id == "user-original"
|
|
196
|
+
assert session1 is session2
|
|
197
|
+
|
|
198
|
+
def test_get_or_create_session_new(self):
|
|
199
|
+
"""Should create new session if not exists."""
|
|
200
|
+
from gateforge.session_manager import SessionManager
|
|
201
|
+
|
|
202
|
+
manager = SessionManager()
|
|
203
|
+
|
|
204
|
+
session = manager.get_or_create_session(
|
|
205
|
+
conversation_id="new-conv",
|
|
206
|
+
user_id="user-new",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
assert session.conversation_id == "new-conv"
|
|
210
|
+
assert session.user_id == "user-new"
|
|
211
|
+
|
|
212
|
+
def test_touch_session(self):
|
|
213
|
+
"""Should update session activity."""
|
|
214
|
+
from gateforge.session_manager import SessionManager
|
|
215
|
+
|
|
216
|
+
manager = SessionManager()
|
|
217
|
+
session = manager.create_session(conversation_id="conv-123")
|
|
218
|
+
|
|
219
|
+
manager.touch("conv-123")
|
|
220
|
+
|
|
221
|
+
assert session.message_count == 1
|
|
222
|
+
|
|
223
|
+
def test_delete_session(self):
|
|
224
|
+
"""Should delete session."""
|
|
225
|
+
from gateforge.session_manager import SessionManager
|
|
226
|
+
|
|
227
|
+
manager = SessionManager()
|
|
228
|
+
session = manager.create_session(conversation_id="conv-123")
|
|
229
|
+
|
|
230
|
+
result = manager.delete_session("conv-123")
|
|
231
|
+
|
|
232
|
+
assert result is True
|
|
233
|
+
assert len(manager) == 0
|
|
234
|
+
assert manager.get_session("conv-123") is None
|
|
235
|
+
|
|
236
|
+
def test_delete_nonexistent_session(self):
|
|
237
|
+
"""Should return False for non-existent session."""
|
|
238
|
+
from gateforge.session_manager import SessionManager
|
|
239
|
+
|
|
240
|
+
manager = SessionManager()
|
|
241
|
+
result = manager.delete_session("non-existent")
|
|
242
|
+
|
|
243
|
+
assert result is False
|
|
244
|
+
|
|
245
|
+
def test_list_sessions_all(self):
|
|
246
|
+
"""Should list all sessions."""
|
|
247
|
+
from gateforge.session_manager import SessionManager
|
|
248
|
+
|
|
249
|
+
manager = SessionManager()
|
|
250
|
+
manager.create_session(conversation_id="conv-1")
|
|
251
|
+
manager.create_session(conversation_id="conv-2")
|
|
252
|
+
manager.create_session(conversation_id="conv-3")
|
|
253
|
+
|
|
254
|
+
sessions = manager.list_sessions()
|
|
255
|
+
|
|
256
|
+
assert len(sessions) == 3
|
|
257
|
+
|
|
258
|
+
def test_list_sessions_by_user(self):
|
|
259
|
+
"""Should filter sessions by user_id."""
|
|
260
|
+
from gateforge.session_manager import SessionManager
|
|
261
|
+
|
|
262
|
+
manager = SessionManager()
|
|
263
|
+
manager.create_session(conversation_id="conv-1", user_id="user-a")
|
|
264
|
+
manager.create_session(conversation_id="conv-2", user_id="user-b")
|
|
265
|
+
manager.create_session(conversation_id="conv-3", user_id="user-a")
|
|
266
|
+
|
|
267
|
+
sessions = manager.list_sessions(user_id="user-a")
|
|
268
|
+
|
|
269
|
+
assert len(sessions) == 2
|
|
270
|
+
assert all(s.user_id == "user-a" for s in sessions)
|
|
271
|
+
|
|
272
|
+
def test_list_sessions_by_tags(self):
|
|
273
|
+
"""Should filter sessions by tags."""
|
|
274
|
+
from gateforge.session_manager import SessionManager
|
|
275
|
+
|
|
276
|
+
manager = SessionManager()
|
|
277
|
+
manager.create_session(conversation_id="conv-1", tags=["weather"])
|
|
278
|
+
manager.create_session(conversation_id="conv-2", tags=["search"])
|
|
279
|
+
manager.create_session(conversation_id="conv-3", tags=["weather", "agent"])
|
|
280
|
+
|
|
281
|
+
sessions = manager.list_sessions(tags=["weather"])
|
|
282
|
+
|
|
283
|
+
assert len(sessions) == 2
|
|
284
|
+
|
|
285
|
+
def test_list_sessions_limit(self):
|
|
286
|
+
"""Should limit number of sessions returned."""
|
|
287
|
+
from gateforge.session_manager import SessionManager
|
|
288
|
+
|
|
289
|
+
manager = SessionManager()
|
|
290
|
+
for i in range(10):
|
|
291
|
+
manager.create_session(conversation_id=f"conv-{i}")
|
|
292
|
+
|
|
293
|
+
sessions = manager.list_sessions(limit=3)
|
|
294
|
+
|
|
295
|
+
assert len(sessions) == 3
|
|
296
|
+
|
|
297
|
+
def test_list_sessions_sorted_by_updated(self):
|
|
298
|
+
"""Should return sessions sorted by updated_at (most recent first)."""
|
|
299
|
+
from gateforge.session_manager import SessionManager
|
|
300
|
+
import time
|
|
301
|
+
|
|
302
|
+
manager = SessionManager()
|
|
303
|
+
manager.create_session(conversation_id="conv-1")
|
|
304
|
+
time.sleep(0.01)
|
|
305
|
+
manager.create_session(conversation_id="conv-2")
|
|
306
|
+
time.sleep(0.01)
|
|
307
|
+
manager.create_session(conversation_id="conv-3")
|
|
308
|
+
|
|
309
|
+
sessions = manager.list_sessions()
|
|
310
|
+
|
|
311
|
+
assert sessions[0].conversation_id == "conv-3"
|
|
312
|
+
assert sessions[1].conversation_id == "conv-2"
|
|
313
|
+
assert sessions[2].conversation_id == "conv-1"
|
|
314
|
+
|
|
315
|
+
def test_serialize_to_dict(self):
|
|
316
|
+
"""Should serialize all sessions to dictionary."""
|
|
317
|
+
from gateforge.session_manager import SessionManager
|
|
318
|
+
|
|
319
|
+
manager = SessionManager()
|
|
320
|
+
manager.create_session(
|
|
321
|
+
conversation_id="conv-123",
|
|
322
|
+
user_id="user-456",
|
|
323
|
+
tags=["test"],
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
data = manager.to_dict()
|
|
327
|
+
|
|
328
|
+
assert "sessions" in data
|
|
329
|
+
assert "conv-123" in data["sessions"]
|
|
330
|
+
assert data["sessions"]["conv-123"]["user_id"] == "user-456"
|
|
331
|
+
|
|
332
|
+
def test_deserialize_from_dict(self):
|
|
333
|
+
"""Should deserialize SessionManager from dictionary."""
|
|
334
|
+
from gateforge.session_manager import SessionManager
|
|
335
|
+
|
|
336
|
+
data = {
|
|
337
|
+
"sessions": {
|
|
338
|
+
"conv-123": {
|
|
339
|
+
"conversation_id": "conv-123",
|
|
340
|
+
"user_id": "user-456",
|
|
341
|
+
"message_count": 5,
|
|
342
|
+
"tags": ["a", "b"],
|
|
343
|
+
"metadata": {},
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
manager = SessionManager.from_dict(data)
|
|
349
|
+
|
|
350
|
+
assert len(manager) == 1
|
|
351
|
+
session = manager.get_session("conv-123")
|
|
352
|
+
assert session.user_id == "user-456"
|
|
353
|
+
assert session.message_count == 5
|
|
354
|
+
|
|
355
|
+
def test_contains(self):
|
|
356
|
+
"""Should support 'in' operator."""
|
|
357
|
+
from gateforge.session_manager import SessionManager
|
|
358
|
+
|
|
359
|
+
manager = SessionManager()
|
|
360
|
+
manager.create_session(conversation_id="conv-123")
|
|
361
|
+
|
|
362
|
+
assert "conv-123" in manager
|
|
363
|
+
assert "conv-456" not in manager
|
|
364
|
+
|
|
365
|
+
def test_len(self):
|
|
366
|
+
"""Should support len()."""
|
|
367
|
+
from gateforge.session_manager import SessionManager
|
|
368
|
+
|
|
369
|
+
manager = SessionManager()
|
|
370
|
+
manager.create_session(conversation_id="conv-1")
|
|
371
|
+
manager.create_session(conversation_id="conv-2")
|
|
372
|
+
|
|
373
|
+
assert len(manager) == 2
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class TestGlobalHelpers:
|
|
377
|
+
"""Tests for global helper functions."""
|
|
378
|
+
|
|
379
|
+
def test_create_session_global(self):
|
|
380
|
+
"""Should create session using default manager."""
|
|
381
|
+
from gateforge.session_manager import create_session, get_session_manager
|
|
382
|
+
|
|
383
|
+
# Clear default manager
|
|
384
|
+
import gateforge.session_manager as sm
|
|
385
|
+
sm._default_manager = None
|
|
386
|
+
|
|
387
|
+
session = create_session(user_id="test-user")
|
|
388
|
+
|
|
389
|
+
assert session.user_id == "test-user"
|
|
390
|
+
assert len(get_session_manager()) == 1
|
|
391
|
+
|
|
392
|
+
def test_get_session_global(self):
|
|
393
|
+
"""Should get session using default manager."""
|
|
394
|
+
from gateforge.session_manager import create_session, get_session
|
|
395
|
+
|
|
396
|
+
# Clear default manager
|
|
397
|
+
import gateforge.session_manager as sm
|
|
398
|
+
sm._default_manager = None
|
|
399
|
+
|
|
400
|
+
session = create_session(conversation_id="test-conv")
|
|
401
|
+
retrieved = get_session("test-conv")
|
|
402
|
+
|
|
403
|
+
assert retrieved is session
|
|
404
|
+
|
|
405
|
+
def test_get_current_conversation_id_with_trace(self):
|
|
406
|
+
"""Should return conversation_id from active trace."""
|
|
407
|
+
from gateforge.session_manager import get_current_conversation_id
|
|
408
|
+
from gateforge.context import trace
|
|
409
|
+
|
|
410
|
+
with trace(conversation_id="test-conv-123"):
|
|
411
|
+
cid = get_current_conversation_id()
|
|
412
|
+
assert cid == "test-conv-123"
|
|
413
|
+
|
|
414
|
+
def test_get_current_conversation_id_no_trace(self):
|
|
415
|
+
"""Should return None when no active trace."""
|
|
416
|
+
from gateforge.session_manager import get_current_conversation_id
|
|
417
|
+
|
|
418
|
+
cid = get_current_conversation_id()
|
|
419
|
+
assert cid is None
|
|
420
|
+
|
|
421
|
+
def test_get_current_trace_info(self):
|
|
422
|
+
"""Should return trace info dictionary."""
|
|
423
|
+
from gateforge.session_manager import get_current_trace_info
|
|
424
|
+
from gateforge.context import trace
|
|
425
|
+
|
|
426
|
+
with trace(conversation_id="test-conv") as t:
|
|
427
|
+
# Advance step
|
|
428
|
+
t.next_step()
|
|
429
|
+
|
|
430
|
+
info = get_current_trace_info()
|
|
431
|
+
|
|
432
|
+
assert info is not None
|
|
433
|
+
assert info["conversation_id"] == "test-conv"
|
|
434
|
+
assert info["trace_id"] is not None
|
|
435
|
+
assert info["step"] == 1
|
|
436
|
+
|
|
437
|
+
def test_get_current_trace_info_no_trace(self):
|
|
438
|
+
"""Should return None when no active trace."""
|
|
439
|
+
from gateforge.session_manager import get_current_trace_info
|
|
440
|
+
|
|
441
|
+
info = get_current_trace_info()
|
|
442
|
+
assert info is None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class TestIntegration:
|
|
446
|
+
"""Integration tests for Phase 2 features."""
|
|
447
|
+
|
|
448
|
+
def test_session_manager_with_trace(self):
|
|
449
|
+
"""Should work with trace context manager."""
|
|
450
|
+
from gateforge.session_manager import SessionManager
|
|
451
|
+
from gateforge.context import trace
|
|
452
|
+
|
|
453
|
+
manager = SessionManager()
|
|
454
|
+
session = manager.create_session(conversation_id="conv-123")
|
|
455
|
+
|
|
456
|
+
with trace(conversation_id=session.conversation_id) as t:
|
|
457
|
+
# Simulate tool call
|
|
458
|
+
t.next_step()
|
|
459
|
+
t.next_step()
|
|
460
|
+
|
|
461
|
+
manager.touch(session.conversation_id)
|
|
462
|
+
|
|
463
|
+
assert session.message_count == 1
|
|
464
|
+
assert t.step == 2
|
|
465
|
+
|
|
466
|
+
def test_multiple_concurrent_sessions(self):
|
|
467
|
+
"""Should handle multiple concurrent sessions."""
|
|
468
|
+
from gateforge.session_manager import SessionManager
|
|
469
|
+
from gateforge.context import trace, get_active_trace
|
|
470
|
+
|
|
471
|
+
manager = SessionManager()
|
|
472
|
+
|
|
473
|
+
# Create two sessions
|
|
474
|
+
session1 = manager.create_session(conversation_id="conv-a")
|
|
475
|
+
session2 = manager.create_session(conversation_id="conv-b")
|
|
476
|
+
|
|
477
|
+
# Use first session
|
|
478
|
+
with trace(conversation_id=session1.conversation_id):
|
|
479
|
+
assert get_active_trace().conversation_id == "conv-a"
|
|
480
|
+
|
|
481
|
+
# Use second session
|
|
482
|
+
with trace(conversation_id=session2.conversation_id):
|
|
483
|
+
assert get_active_trace().conversation_id == "conv-b"
|
|
484
|
+
|
|
485
|
+
# Outside trace, no active session
|
|
486
|
+
assert get_active_trace() is None
|
|
487
|
+
|
|
488
|
+
def test_session_persistence_roundtrip(self):
|
|
489
|
+
"""Should serialize and deserialize correctly."""
|
|
490
|
+
from gateforge.session_manager import SessionManager
|
|
491
|
+
|
|
492
|
+
# Create manager with sessions
|
|
493
|
+
manager1 = SessionManager()
|
|
494
|
+
manager1.create_session(
|
|
495
|
+
conversation_id="conv-1",
|
|
496
|
+
user_id="user-a",
|
|
497
|
+
tags=["test"],
|
|
498
|
+
metadata={"key": "value1"},
|
|
499
|
+
)
|
|
500
|
+
manager1.create_session(
|
|
501
|
+
conversation_id="conv-2",
|
|
502
|
+
user_id="user-b",
|
|
503
|
+
tags=["prod"],
|
|
504
|
+
metadata={"key": "value2"},
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Serialize
|
|
508
|
+
data = manager1.to_dict()
|
|
509
|
+
|
|
510
|
+
# Deserialize
|
|
511
|
+
manager2 = SessionManager.from_dict(data)
|
|
512
|
+
|
|
513
|
+
# Verify
|
|
514
|
+
assert len(manager2) == 2
|
|
515
|
+
|
|
516
|
+
s1 = manager2.get_session("conv-1")
|
|
517
|
+
assert s1.user_id == "user-a"
|
|
518
|
+
assert s1.tags == ["test"]
|
|
519
|
+
assert s1.metadata["key"] == "value1"
|
|
520
|
+
|
|
521
|
+
s2 = manager2.get_session("conv-2")
|
|
522
|
+
assert s2.user_id == "user-b"
|
|
523
|
+
assert s2.tags == ["prod"]
|
|
524
|
+
assert s2.metadata["key"] == "value2"
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
if __name__ == '__main__':
|
|
528
|
+
pytest.main([__file__, '-v'])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|