chuk-ai-session-manager 0.1.1__py3-none-any.whl → 0.2__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.
- chuk_ai_session_manager/__init__.py +336 -34
- chuk_ai_session_manager/api/__init__.py +1 -0
- chuk_ai_session_manager/api/simple_api.py +376 -0
- chuk_ai_session_manager/infinite_conversation.py +7 -4
- chuk_ai_session_manager/models/session.py +27 -18
- chuk_ai_session_manager/session_aware_tool_processor.py +6 -4
- chuk_ai_session_manager/session_prompt_builder.py +6 -4
- chuk_ai_session_manager/session_storage.py +176 -0
- chuk_ai_session_manager/utils/status_display_utils.py +474 -0
- {chuk_ai_session_manager-0.1.1.dist-info → chuk_ai_session_manager-0.2.dist-info}/METADATA +9 -8
- chuk_ai_session_manager-0.2.dist-info/RECORD +23 -0
- chuk_ai_session_manager/storage/__init__.py +0 -44
- chuk_ai_session_manager/storage/base.py +0 -50
- chuk_ai_session_manager/storage/providers/file.py +0 -348
- chuk_ai_session_manager/storage/providers/memory.py +0 -96
- chuk_ai_session_manager/storage/providers/redis.py +0 -295
- chuk_ai_session_manager-0.1.1.dist-info/RECORD +0 -24
- /chuk_ai_session_manager/{storage/providers → utils}/__init__.py +0 -0
- {chuk_ai_session_manager-0.1.1.dist-info → chuk_ai_session_manager-0.2.dist-info}/WHEEL +0 -0
- {chuk_ai_session_manager-0.1.1.dist-info → chuk_ai_session_manager-0.2.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-ai-session-manager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2
|
|
4
4
|
Summary: Session manager for AI applications
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: aiofiles>=24.1.0
|
|
8
|
+
Requires-Dist: chuk-sessions>=0.3
|
|
8
9
|
Requires-Dist: chuk-tool-processor>=0.4.1
|
|
9
10
|
Requires-Dist: pydantic>=2.11.3
|
|
10
11
|
Provides-Extra: tiktoken
|
|
@@ -70,10 +71,10 @@ This isn't just a demo framework - it's designed for production AI applications
|
|
|
70
71
|
|
|
71
72
|
```python
|
|
72
73
|
import asyncio
|
|
73
|
-
from chuk_ai_session_manager.
|
|
74
|
+
from chuk_ai_session_manager.session import Session
|
|
74
75
|
from chuk_ai_session_manager.models.session_event import SessionEvent
|
|
75
76
|
from chuk_ai_session_manager.models.event_source import EventSource
|
|
76
|
-
from chuk_ai_session_manager.
|
|
77
|
+
from chuk_ai_session_manager.chuk_sessions_storage import get_backend, ChukSessionsStore, InMemorySessionStore
|
|
77
78
|
|
|
78
79
|
async def main():
|
|
79
80
|
# Set up storage
|
|
@@ -116,8 +117,8 @@ import asyncio
|
|
|
116
117
|
import json
|
|
117
118
|
from openai import AsyncOpenAI
|
|
118
119
|
from chuk_tool_processor.registry import initialize
|
|
119
|
-
from chuk_ai_session_manager.
|
|
120
|
-
from chuk_ai_session_manager.
|
|
120
|
+
from chuk_ai_session_manager.session import Session
|
|
121
|
+
from chuk_ai_session_manager.chuk_sessions_storage import get_backend, ChukSessionsStore, InMemorySessionStore
|
|
121
122
|
|
|
122
123
|
# Import tools - auto-registers via decorators
|
|
123
124
|
from your_tools import sample_tools
|
|
@@ -160,7 +161,7 @@ asyncio.run(openai_integration_demo())
|
|
|
160
161
|
|
|
161
162
|
### In-Memory (Default)
|
|
162
163
|
```python
|
|
163
|
-
from chuk_ai_session_manager.
|
|
164
|
+
from chuk_ai_session_manager.chuk_sessions_storage import InMemorySessionStore, SessionStoreProvider
|
|
164
165
|
|
|
165
166
|
# Great for testing or single-process applications
|
|
166
167
|
store = InMemorySessionStore()
|
|
@@ -169,7 +170,7 @@ SessionStoreProvider.set_store(store)
|
|
|
169
170
|
|
|
170
171
|
### File Storage
|
|
171
172
|
```python
|
|
172
|
-
from chuk_ai_session_manager.
|
|
173
|
+
from chuk_ai_session_manager.chuk_sessions_storage.providers.file import create_file_session_store
|
|
173
174
|
|
|
174
175
|
# Persistent JSON file storage with async I/O
|
|
175
176
|
store = await create_file_session_store(directory="./sessions")
|
|
@@ -178,7 +179,7 @@ SessionStoreProvider.set_store(store)
|
|
|
178
179
|
|
|
179
180
|
### Redis Storage
|
|
180
181
|
```python
|
|
181
|
-
from chuk_ai_session_manager.
|
|
182
|
+
from chuk_ai_session_manager.chuk_sessions_storage.providers.redis import create_redis_session_store
|
|
182
183
|
|
|
183
184
|
# Distributed storage for production with TTL
|
|
184
185
|
store = await create_redis_session_store(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
chuk_ai_session_manager/__init__.py,sha256=0a8NezasrQJe2jm-1pZpzqPI-JCnrPNGTRLKDPRtyuk,11009
|
|
2
|
+
chuk_ai_session_manager/exceptions.py,sha256=WqrrUZuOAiUmz7tKnSnk0y222U_nV9a8LyaXLayn2fg,4420
|
|
3
|
+
chuk_ai_session_manager/infinite_conversation.py,sha256=7j3caMnsX27M5rjj4oOkqiy_2AfcupWwsAWRflnKiSo,12092
|
|
4
|
+
chuk_ai_session_manager/sample_tools.py,sha256=yZDM-ast5lv0YVHcd3GTxBMcJd7zuNkUhZPVIb06G0c,8155
|
|
5
|
+
chuk_ai_session_manager/session_aware_tool_processor.py,sha256=iVe3d-qfp5QGkdNrgfZeRYoOjd8nLZ0g6K7HW1thFE8,7274
|
|
6
|
+
chuk_ai_session_manager/session_prompt_builder.py,sha256=-ZTUczYh5emToInp4TRCj9FvF4CECyn45YHYKoWzmxE,17328
|
|
7
|
+
chuk_ai_session_manager/session_storage.py,sha256=HqzYDtwx4zN5an1zJmSZc56BpyD3KjT3IWonIpmnVXQ,5790
|
|
8
|
+
chuk_ai_session_manager/api/__init__.py,sha256=Lo_BoDW2rSn0Zw-CbjahOxc6ykjjTpucxHZo5FA2Gnc,41
|
|
9
|
+
chuk_ai_session_manager/api/simple_api.py,sha256=N2Y0b7JkQsWLCvU1uGyAfYTGqyyCmG26X2PnKt3vux4,12040
|
|
10
|
+
chuk_ai_session_manager/models/__init__.py,sha256=H1rRuDQDRf821JPUWUn5Zgwvc5BAqcEGekkHEmX-IgE,1167
|
|
11
|
+
chuk_ai_session_manager/models/event_source.py,sha256=mn_D16sXMa6nAX-5BzssygJPz6VF24GRe-3IaH7bTnI,196
|
|
12
|
+
chuk_ai_session_manager/models/event_type.py,sha256=TPPvAz-PlXVtrwXDNVFVnhdt1yEfgDGmKDGt8ArYcTk,275
|
|
13
|
+
chuk_ai_session_manager/models/session.py,sha256=Txnmqd5SmiMz6acur_zL5MiFHJjKqU2se895p7_zUNQ,11781
|
|
14
|
+
chuk_ai_session_manager/models/session_event.py,sha256=YPDbymduF42LLHtAv_k_kqlWF68vnth5J_HM4q-bOyI,5896
|
|
15
|
+
chuk_ai_session_manager/models/session_metadata.py,sha256=KFG7lc_E0BQTP2OD9Y529elVGJXppDUMqz8vVONW0rw,1510
|
|
16
|
+
chuk_ai_session_manager/models/session_run.py,sha256=uhMM4-WSrqOUsiWQPnyakInd-foZhxI-YnSHSWiZZwE,4369
|
|
17
|
+
chuk_ai_session_manager/models/token_usage.py,sha256=pnsNDMew9ToUqkRCIz1TADnHC5aKnautdLD4trCA6Zg,11121
|
|
18
|
+
chuk_ai_session_manager/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
chuk_ai_session_manager/utils/status_display_utils.py,sha256=id4TIE0VSq3thvDd4wKIyk3kBr_bUMqrtXmOI9CD8r8,19231
|
|
20
|
+
chuk_ai_session_manager-0.2.dist-info/METADATA,sha256=jucZml2QT7GsqdM8I4U5I7uBm8pdWtrS2dNdvWYoxvc,16465
|
|
21
|
+
chuk_ai_session_manager-0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
+
chuk_ai_session_manager-0.2.dist-info/top_level.txt,sha256=5RinqD0v-niHuLYePUREX4gEWTlrpgtUg0RfexVRBMk,24
|
|
23
|
+
chuk_ai_session_manager-0.2.dist-info/RECORD,,
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# chuk_ai_session_manager/storage/__init__.py
|
|
2
|
-
"""
|
|
3
|
-
Storage module for the chuk session manager.
|
|
4
|
-
"""
|
|
5
|
-
# Import base components first to avoid circular imports
|
|
6
|
-
try:
|
|
7
|
-
from chuk_ai_session_manager.storage.base import SessionStoreInterface, SessionStoreProvider
|
|
8
|
-
except ImportError:
|
|
9
|
-
pass
|
|
10
|
-
|
|
11
|
-
# Try to import providers if available
|
|
12
|
-
try:
|
|
13
|
-
from chuk_ai_session_manager.storage.providers.memory import InMemorySessionStore
|
|
14
|
-
except ImportError:
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
try:
|
|
18
|
-
from chuk_ai_session_manager.storage.providers.file import FileSessionStore, create_file_session_store
|
|
19
|
-
except ImportError:
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
# Try to import Redis - this is optional
|
|
23
|
-
try:
|
|
24
|
-
from chuk_ai_session_manager.storage.providers.redis import RedisSessionStore, create_redis_session_store
|
|
25
|
-
_has_redis = True
|
|
26
|
-
except ImportError:
|
|
27
|
-
_has_redis = False
|
|
28
|
-
|
|
29
|
-
# Define __all__ based on what was successfully imported
|
|
30
|
-
__all__ = []
|
|
31
|
-
|
|
32
|
-
# Basic components
|
|
33
|
-
for name in ['SessionStoreInterface', 'SessionStoreProvider', 'InMemorySessionStore']:
|
|
34
|
-
if name in globals():
|
|
35
|
-
__all__.append(name)
|
|
36
|
-
|
|
37
|
-
# File store
|
|
38
|
-
for name in ['FileSessionStore', 'create_file_session_store']:
|
|
39
|
-
if name in globals():
|
|
40
|
-
__all__.append(name)
|
|
41
|
-
|
|
42
|
-
# Redis store (optional)
|
|
43
|
-
if _has_redis:
|
|
44
|
-
__all__.extend(['RedisSessionStore', 'create_redis_session_store'])
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# chuk_ai_session_manager/storage/base.py
|
|
2
|
-
"""
|
|
3
|
-
Base interfaces and providers for async session storage.
|
|
4
|
-
"""
|
|
5
|
-
from abc import ABC, abstractmethod
|
|
6
|
-
from typing import Any, Dict, List, Optional, TypeVar
|
|
7
|
-
|
|
8
|
-
T = TypeVar('T')
|
|
9
|
-
|
|
10
|
-
class SessionStoreInterface(ABC):
|
|
11
|
-
"""Interface for pluggable async session stores."""
|
|
12
|
-
|
|
13
|
-
@abstractmethod
|
|
14
|
-
async def get(self, session_id: str) -> Optional[Any]:
|
|
15
|
-
"""Retrieve a session by its ID, or None if not found."""
|
|
16
|
-
...
|
|
17
|
-
|
|
18
|
-
@abstractmethod
|
|
19
|
-
async def save(self, session: Any) -> None:
|
|
20
|
-
"""Save or update a session object in the store."""
|
|
21
|
-
...
|
|
22
|
-
|
|
23
|
-
@abstractmethod
|
|
24
|
-
async def delete(self, session_id: str) -> None:
|
|
25
|
-
"""Delete a session by its ID."""
|
|
26
|
-
...
|
|
27
|
-
|
|
28
|
-
@abstractmethod
|
|
29
|
-
async def list_sessions(self, prefix: str = "") -> List[str]:
|
|
30
|
-
"""List all session IDs, optionally filtered by prefix."""
|
|
31
|
-
...
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class SessionStoreProvider:
|
|
35
|
-
"""Provider for a globally-shared async session store."""
|
|
36
|
-
_store: Optional[SessionStoreInterface] = None
|
|
37
|
-
|
|
38
|
-
@classmethod
|
|
39
|
-
def get_store(cls) -> SessionStoreInterface:
|
|
40
|
-
"""Get the currently configured session store."""
|
|
41
|
-
if cls._store is None:
|
|
42
|
-
# Defer import to avoid circular imports
|
|
43
|
-
from chuk_ai_session_manager.storage.providers.memory import InMemorySessionStore
|
|
44
|
-
cls._store = InMemorySessionStore()
|
|
45
|
-
return cls._store
|
|
46
|
-
|
|
47
|
-
@classmethod
|
|
48
|
-
def set_store(cls, store: SessionStoreInterface) -> None:
|
|
49
|
-
"""Set a new session store implementation."""
|
|
50
|
-
cls._store = store
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
# chuk_ai_session_manager/storage/providers/file.py
|
|
2
|
-
|
|
3
|
-
"""
|
|
4
|
-
Async file-based session storage implementation with improved async semantics.
|
|
5
|
-
"""
|
|
6
|
-
import json
|
|
7
|
-
import logging
|
|
8
|
-
import asyncio
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, Generic
|
|
12
|
-
import os
|
|
13
|
-
|
|
14
|
-
# Check for aiofiles availability
|
|
15
|
-
try:
|
|
16
|
-
import aiofiles
|
|
17
|
-
AIOFILES_AVAILABLE = True
|
|
18
|
-
except ImportError:
|
|
19
|
-
AIOFILES_AVAILABLE = False
|
|
20
|
-
logging.warning("aiofiles package not installed; falling back to synchronous I/O in thread pool.")
|
|
21
|
-
|
|
22
|
-
# session manager imports
|
|
23
|
-
from chuk_ai_session_manager.models.session import Session
|
|
24
|
-
from chuk_ai_session_manager.storage.base import SessionStoreInterface
|
|
25
|
-
from chuk_ai_session_manager.exceptions import SessionManagerError
|
|
26
|
-
|
|
27
|
-
# Type variable for serializable models
|
|
28
|
-
T = TypeVar('T', bound='Session')
|
|
29
|
-
|
|
30
|
-
# Setup logging
|
|
31
|
-
logger = logging.getLogger(__name__)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class FileStorageError(SessionManagerError):
|
|
35
|
-
"""Raised when file storage operations fail."""
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class SessionSerializer(Generic[T]):
|
|
40
|
-
"""Handles serialization and deserialization of session objects."""
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def to_dict(cls, obj: T) -> Dict[str, Any]:
|
|
44
|
-
"""Convert a session object to a dictionary for serialization."""
|
|
45
|
-
# Use Pydantic's model_dump method for serialization
|
|
46
|
-
return obj.model_dump()
|
|
47
|
-
|
|
48
|
-
@classmethod
|
|
49
|
-
def from_dict(cls, data: Dict[str, Any], model_class: Type[T]) -> T:
|
|
50
|
-
"""Convert a dictionary to a session object."""
|
|
51
|
-
try:
|
|
52
|
-
# Use Pydantic's model validation for deserialization
|
|
53
|
-
return model_class.model_validate(data)
|
|
54
|
-
except Exception as e:
|
|
55
|
-
raise FileStorageError(f"Failed to deserialize {model_class.__name__}: {str(e)}")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class FileSessionStore(SessionStoreInterface, Generic[T]):
|
|
59
|
-
"""
|
|
60
|
-
An async file session store that persists sessions to JSON files.
|
|
61
|
-
|
|
62
|
-
This implementation stores each session as a separate JSON file in
|
|
63
|
-
the specified directory, using aiofiles for non-blocking I/O when available.
|
|
64
|
-
It uses file locks to prevent race conditions during reads and writes.
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
def __init__(self,
|
|
68
|
-
directory: Union[str, Path],
|
|
69
|
-
session_class: Type[T] = Session,
|
|
70
|
-
auto_save: bool = True):
|
|
71
|
-
"""
|
|
72
|
-
Initialize the async file session store.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
directory: Directory where session files will be stored
|
|
76
|
-
session_class: The Session class to use for deserialization
|
|
77
|
-
auto_save: Whether to automatically save on each update
|
|
78
|
-
"""
|
|
79
|
-
self.directory = Path(directory)
|
|
80
|
-
self.directory.mkdir(parents=True, exist_ok=True)
|
|
81
|
-
self.session_class = session_class
|
|
82
|
-
self.auto_save = auto_save
|
|
83
|
-
|
|
84
|
-
# In-memory cache for better performance
|
|
85
|
-
self._cache: Dict[str, T] = {}
|
|
86
|
-
|
|
87
|
-
# Locks for file operations (keyed by session ID)
|
|
88
|
-
self._locks: Dict[str, asyncio.Lock] = {}
|
|
89
|
-
|
|
90
|
-
def _get_path(self, session_id: str) -> Path:
|
|
91
|
-
"""Get the file path for a session ID."""
|
|
92
|
-
return self.directory / f"{session_id}.json"
|
|
93
|
-
|
|
94
|
-
def _json_default(self, obj: Any) -> Any:
|
|
95
|
-
"""Handle non-serializable objects in JSON serialization."""
|
|
96
|
-
if isinstance(obj, datetime):
|
|
97
|
-
return obj.isoformat()
|
|
98
|
-
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
|
99
|
-
|
|
100
|
-
async def _get_lock(self, session_id: str) -> asyncio.Lock:
|
|
101
|
-
"""Get a lock for a specific session ID."""
|
|
102
|
-
if session_id not in self._locks:
|
|
103
|
-
self._locks[session_id] = asyncio.Lock()
|
|
104
|
-
return self._locks[session_id]
|
|
105
|
-
|
|
106
|
-
async def get(self, session_id: str) -> Optional[T]:
|
|
107
|
-
"""Async: Retrieve a session by its ID."""
|
|
108
|
-
# Check cache first
|
|
109
|
-
if session_id in self._cache:
|
|
110
|
-
return self._cache[session_id]
|
|
111
|
-
|
|
112
|
-
# If not in cache, try to load from file
|
|
113
|
-
file_path = self._get_path(session_id)
|
|
114
|
-
if not file_path.exists():
|
|
115
|
-
return None
|
|
116
|
-
|
|
117
|
-
# Get lock for this session
|
|
118
|
-
lock = await self._get_lock(session_id)
|
|
119
|
-
|
|
120
|
-
# Use lock for file read to prevent race conditions
|
|
121
|
-
async with lock:
|
|
122
|
-
try:
|
|
123
|
-
if AIOFILES_AVAILABLE:
|
|
124
|
-
async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
|
|
125
|
-
data_str = await f.read()
|
|
126
|
-
data = json.loads(data_str)
|
|
127
|
-
else:
|
|
128
|
-
# If aiofiles not available, use executor to avoid blocking
|
|
129
|
-
loop = asyncio.get_running_loop()
|
|
130
|
-
data_str = await loop.run_in_executor(
|
|
131
|
-
None,
|
|
132
|
-
lambda: open(file_path, 'r', encoding='utf-8').read()
|
|
133
|
-
)
|
|
134
|
-
data = json.loads(data_str)
|
|
135
|
-
|
|
136
|
-
session = SessionSerializer.from_dict(data, self.session_class)
|
|
137
|
-
# Update cache
|
|
138
|
-
self._cache[session_id] = session
|
|
139
|
-
return session
|
|
140
|
-
except (FileStorageError, json.JSONDecodeError, IOError) as e:
|
|
141
|
-
logger.error(f"Failed to load session {session_id}: {e}")
|
|
142
|
-
return None
|
|
143
|
-
|
|
144
|
-
async def save(self, session: T) -> None:
|
|
145
|
-
"""Async: Save a session to the store."""
|
|
146
|
-
session_id = session.id
|
|
147
|
-
# Update cache
|
|
148
|
-
self._cache[session_id] = session
|
|
149
|
-
|
|
150
|
-
if self.auto_save:
|
|
151
|
-
await self._save_to_file(session)
|
|
152
|
-
|
|
153
|
-
async def _save_to_file(self, session: T) -> None:
|
|
154
|
-
"""Async: Save a session to its JSON file."""
|
|
155
|
-
session_id = session.id
|
|
156
|
-
file_path = self._get_path(session_id)
|
|
157
|
-
|
|
158
|
-
# Get lock for this session
|
|
159
|
-
lock = await self._get_lock(session_id)
|
|
160
|
-
|
|
161
|
-
# Use lock for file write to prevent race conditions
|
|
162
|
-
async with lock:
|
|
163
|
-
try:
|
|
164
|
-
# Create temp file first to avoid partial writes
|
|
165
|
-
temp_path = file_path.with_suffix('.tmp')
|
|
166
|
-
|
|
167
|
-
data = SessionSerializer.to_dict(session)
|
|
168
|
-
json_str = json.dumps(data, default=self._json_default, indent=2)
|
|
169
|
-
|
|
170
|
-
if AIOFILES_AVAILABLE:
|
|
171
|
-
async with aiofiles.open(temp_path, 'w', encoding='utf-8') as f:
|
|
172
|
-
await f.write(json_str)
|
|
173
|
-
else:
|
|
174
|
-
# If aiofiles not available, use executor to avoid blocking
|
|
175
|
-
loop = asyncio.get_running_loop()
|
|
176
|
-
await loop.run_in_executor(
|
|
177
|
-
None,
|
|
178
|
-
lambda: open(temp_path, 'w', encoding='utf-8').write(json_str)
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
# Rename temp file to actual file (atomic operation)
|
|
182
|
-
os.replace(temp_path, file_path)
|
|
183
|
-
|
|
184
|
-
except (FileStorageError, IOError, TypeError) as e:
|
|
185
|
-
logger.error(f"Failed to save session {session_id}: {e}")
|
|
186
|
-
if temp_path.exists():
|
|
187
|
-
temp_path.unlink() # Clean up temp file
|
|
188
|
-
raise FileStorageError(f"Failed to save session {session_id}: {str(e)}")
|
|
189
|
-
|
|
190
|
-
async def delete(self, session_id: str) -> None:
|
|
191
|
-
"""Async: Delete a session by its ID."""
|
|
192
|
-
# Remove from cache
|
|
193
|
-
if session_id in self._cache:
|
|
194
|
-
del self._cache[session_id]
|
|
195
|
-
|
|
196
|
-
# Get lock for this session
|
|
197
|
-
lock = await self._get_lock(session_id)
|
|
198
|
-
|
|
199
|
-
# Use lock for deletion to prevent race conditions
|
|
200
|
-
async with lock:
|
|
201
|
-
# Remove file if it exists
|
|
202
|
-
file_path = self._get_path(session_id)
|
|
203
|
-
if file_path.exists():
|
|
204
|
-
try:
|
|
205
|
-
# Run in executor to avoid blocking
|
|
206
|
-
loop = asyncio.get_running_loop()
|
|
207
|
-
await loop.run_in_executor(None, file_path.unlink)
|
|
208
|
-
except IOError as e:
|
|
209
|
-
logger.error(f"Failed to delete session file {session_id}: {e}")
|
|
210
|
-
raise FileStorageError(f"Failed to delete session {session_id}: {str(e)}")
|
|
211
|
-
|
|
212
|
-
# Remove lock for this session
|
|
213
|
-
if session_id in self._locks:
|
|
214
|
-
del self._locks[session_id]
|
|
215
|
-
|
|
216
|
-
async def list_sessions(self, prefix: str = "") -> List[str]:
|
|
217
|
-
"""Async: List all session IDs, optionally filtered by prefix."""
|
|
218
|
-
try:
|
|
219
|
-
# Run in executor to avoid blocking
|
|
220
|
-
loop = asyncio.get_running_loop()
|
|
221
|
-
files = await loop.run_in_executor(
|
|
222
|
-
None,
|
|
223
|
-
lambda: list(self.directory.glob("*.json"))
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
# Extract the session IDs (filenames without extension)
|
|
227
|
-
session_ids = [f.stem for f in files]
|
|
228
|
-
|
|
229
|
-
# Filter by prefix if provided
|
|
230
|
-
if prefix:
|
|
231
|
-
session_ids = [sid for sid in session_ids if sid.startswith(prefix)]
|
|
232
|
-
|
|
233
|
-
return session_ids
|
|
234
|
-
except IOError as e:
|
|
235
|
-
logger.error(f"Failed to list sessions: {e}")
|
|
236
|
-
raise FileStorageError(f"Failed to list sessions: {str(e)}")
|
|
237
|
-
|
|
238
|
-
async def flush(self) -> None:
|
|
239
|
-
"""Async: Force save all cached sessions to disk."""
|
|
240
|
-
save_tasks = []
|
|
241
|
-
for session in self._cache.values():
|
|
242
|
-
# Create tasks but don't await them yet
|
|
243
|
-
task = asyncio.create_task(self._save_to_file(session))
|
|
244
|
-
save_tasks.append(task)
|
|
245
|
-
|
|
246
|
-
# Wait for all save operations to complete
|
|
247
|
-
if save_tasks:
|
|
248
|
-
# Use gather with return_exceptions to prevent one error from stopping all saves
|
|
249
|
-
results = await asyncio.gather(*save_tasks, return_exceptions=True)
|
|
250
|
-
|
|
251
|
-
# Log any errors
|
|
252
|
-
for result in results:
|
|
253
|
-
if isinstance(result, Exception):
|
|
254
|
-
logger.error(f"Error during flush: {result}")
|
|
255
|
-
|
|
256
|
-
async def clear_cache(self) -> None:
|
|
257
|
-
"""Async: Clear the in-memory cache."""
|
|
258
|
-
self._cache.clear()
|
|
259
|
-
|
|
260
|
-
async def vacuum(self) -> int:
|
|
261
|
-
"""
|
|
262
|
-
Async: Remove orphaned temporary files and fix any corrupt files.
|
|
263
|
-
|
|
264
|
-
Returns:
|
|
265
|
-
Number of fixed or removed files
|
|
266
|
-
"""
|
|
267
|
-
count = 0
|
|
268
|
-
|
|
269
|
-
try:
|
|
270
|
-
# Find all temp files
|
|
271
|
-
loop = asyncio.get_running_loop()
|
|
272
|
-
temp_files = await loop.run_in_executor(
|
|
273
|
-
None,
|
|
274
|
-
lambda: list(self.directory.glob("*.tmp"))
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
# Delete temp files
|
|
278
|
-
for temp_file in temp_files:
|
|
279
|
-
try:
|
|
280
|
-
await loop.run_in_executor(None, temp_file.unlink)
|
|
281
|
-
count += 1
|
|
282
|
-
except IOError as e:
|
|
283
|
-
logger.error(f"Failed to delete temp file {temp_file}: {e}")
|
|
284
|
-
|
|
285
|
-
# Find all json files
|
|
286
|
-
json_files = await loop.run_in_executor(
|
|
287
|
-
None,
|
|
288
|
-
lambda: list(self.directory.glob("*.json"))
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
# Check each file for corruption
|
|
292
|
-
for json_file in json_files:
|
|
293
|
-
try:
|
|
294
|
-
# Try to read the file
|
|
295
|
-
if AIOFILES_AVAILABLE:
|
|
296
|
-
async with aiofiles.open(json_file, 'r', encoding='utf-8') as f:
|
|
297
|
-
data_str = await f.read()
|
|
298
|
-
# Just try to parse it to see if it's valid JSON
|
|
299
|
-
json.loads(data_str)
|
|
300
|
-
else:
|
|
301
|
-
data_str = await loop.run_in_executor(
|
|
302
|
-
None,
|
|
303
|
-
lambda: open(json_file, 'r', encoding='utf-8').read()
|
|
304
|
-
)
|
|
305
|
-
json.loads(data_str)
|
|
306
|
-
except (json.JSONDecodeError, IOError) as e:
|
|
307
|
-
# File is corrupt, rename it
|
|
308
|
-
logger.warning(f"Found corrupt file {json_file}: {e}")
|
|
309
|
-
corrupt_path = json_file.with_suffix('.corrupt')
|
|
310
|
-
await loop.run_in_executor(
|
|
311
|
-
None,
|
|
312
|
-
lambda: os.rename(json_file, corrupt_path)
|
|
313
|
-
)
|
|
314
|
-
count += 1
|
|
315
|
-
|
|
316
|
-
return count
|
|
317
|
-
except Exception as e:
|
|
318
|
-
logger.error(f"Error during vacuum: {e}")
|
|
319
|
-
raise FileStorageError(f"Failed to vacuum storage: {str(e)}")
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
async def create_file_session_store(
|
|
323
|
-
directory: Union[str, Path],
|
|
324
|
-
session_class: Type[T] = Session,
|
|
325
|
-
auto_save: bool = True
|
|
326
|
-
) -> FileSessionStore[T]:
|
|
327
|
-
"""
|
|
328
|
-
Create an async file-based session store.
|
|
329
|
-
|
|
330
|
-
Args:
|
|
331
|
-
directory: Directory where session files will be stored
|
|
332
|
-
session_class: The Session class to use
|
|
333
|
-
auto_save: Whether to automatically save on each update
|
|
334
|
-
|
|
335
|
-
Returns:
|
|
336
|
-
A configured FileSessionStore
|
|
337
|
-
"""
|
|
338
|
-
store = FileSessionStore(directory, session_class, auto_save)
|
|
339
|
-
|
|
340
|
-
# Optional: Run vacuum on startup to clean any leftover temp files
|
|
341
|
-
try:
|
|
342
|
-
fixed_count = await store.vacuum()
|
|
343
|
-
if fixed_count > 0:
|
|
344
|
-
logger.info(f"Cleaned up {fixed_count} temporary or corrupt files during store initialization")
|
|
345
|
-
except Exception as e:
|
|
346
|
-
logger.warning(f"Error during initial vacuum: {e}")
|
|
347
|
-
|
|
348
|
-
return store
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# chuk_ai_session_manager/storage/providers/memory.py
|
|
2
|
-
"""
|
|
3
|
-
Async in-memory session storage implementation with improved async semantics.
|
|
4
|
-
"""
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
|
6
|
-
import asyncio
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
|
|
9
|
-
from chuk_ai_session_manager.storage.base import SessionStoreInterface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class InMemorySessionStore(SessionStoreInterface):
|
|
13
|
-
"""A simple in-memory store for Session objects with proper async interface.
|
|
14
|
-
|
|
15
|
-
This implementation stores sessions in a dictionary and is not
|
|
16
|
-
persistent across application restarts. It uses asyncio locks to
|
|
17
|
-
ensure thread safety when multiple coroutines access the store.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
def __init__(self) -> None:
|
|
21
|
-
"""Initialize an empty in-memory store."""
|
|
22
|
-
self._data: Dict[str, Any] = {}
|
|
23
|
-
self._lock = asyncio.Lock() # For thread safety in async operations
|
|
24
|
-
|
|
25
|
-
async def get(self, session_id: str) -> Optional[Any]:
|
|
26
|
-
"""Async: Retrieve a session by its ID, or None if not found."""
|
|
27
|
-
# Read operations don't need locking
|
|
28
|
-
return self._data.get(session_id)
|
|
29
|
-
|
|
30
|
-
async def save(self, session: Any) -> None:
|
|
31
|
-
"""Async: Save or update a session object in the store."""
|
|
32
|
-
async with self._lock:
|
|
33
|
-
self._data[session.id] = session
|
|
34
|
-
|
|
35
|
-
# Update metadata timestamp if available
|
|
36
|
-
if hasattr(session, 'metadata') and hasattr(session.metadata, 'update_timestamp'):
|
|
37
|
-
await session.metadata.update_timestamp()
|
|
38
|
-
|
|
39
|
-
async def delete(self, session_id: str) -> None:
|
|
40
|
-
"""Async: Delete a session by its ID."""
|
|
41
|
-
async with self._lock:
|
|
42
|
-
if session_id in self._data:
|
|
43
|
-
del self._data[session_id]
|
|
44
|
-
|
|
45
|
-
async def list_sessions(self, prefix: str = "") -> List[str]:
|
|
46
|
-
"""Async: List all session IDs, optionally filtered by prefix."""
|
|
47
|
-
# Read operations don't need locking
|
|
48
|
-
if not prefix:
|
|
49
|
-
return list(self._data.keys())
|
|
50
|
-
return [sid for sid in self._data.keys() if sid.startswith(prefix)]
|
|
51
|
-
|
|
52
|
-
async def clear(self) -> None:
|
|
53
|
-
"""Async: Clear all sessions from the store."""
|
|
54
|
-
async with self._lock:
|
|
55
|
-
self._data.clear()
|
|
56
|
-
|
|
57
|
-
async def get_by_property(self, key: str, value: Any) -> List[Any]:
|
|
58
|
-
"""
|
|
59
|
-
Async: Find sessions by a specific metadata property value.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
key: The metadata property key to search for
|
|
63
|
-
value: The value to match
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
A list of matching sessions
|
|
67
|
-
"""
|
|
68
|
-
results = []
|
|
69
|
-
for session in self._data.values():
|
|
70
|
-
if (hasattr(session, 'metadata') and
|
|
71
|
-
hasattr(session.metadata, 'properties') and
|
|
72
|
-
session.metadata.properties.get(key) == value):
|
|
73
|
-
results.append(session)
|
|
74
|
-
return results
|
|
75
|
-
|
|
76
|
-
async def get_by_state(self, key: str, value: Any) -> List[Any]:
|
|
77
|
-
"""
|
|
78
|
-
Async: Find sessions by a specific state value.
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
key: The state key to search for
|
|
82
|
-
value: The value to match
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
A list of matching sessions
|
|
86
|
-
"""
|
|
87
|
-
results = []
|
|
88
|
-
for session in self._data.values():
|
|
89
|
-
if (hasattr(session, 'state') and
|
|
90
|
-
session.state.get(key) == value):
|
|
91
|
-
results.append(session)
|
|
92
|
-
return results
|
|
93
|
-
|
|
94
|
-
async def count(self) -> int:
|
|
95
|
-
"""Async: Count the number of sessions in the store."""
|
|
96
|
-
return len(self._data)
|