Spaces:
Building
Building
Delete session.py
Browse files- session.py +0 -311
session.py
DELETED
@@ -1,311 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
Optimized Session Management for Flare Platform
|
3 |
-
"""
|
4 |
-
from dataclasses import dataclass, field
|
5 |
-
from typing import Dict, List, Optional, Any
|
6 |
-
from datetime import datetime
|
7 |
-
import json
|
8 |
-
import secrets
|
9 |
-
import hashlib
|
10 |
-
import time
|
11 |
-
|
12 |
-
from config.config_models import VersionConfig, IntentConfig
|
13 |
-
from utils.logger import log_debug, log_info
|
14 |
-
|
15 |
-
@dataclass
|
16 |
-
class Session:
|
17 |
-
"""Optimized session for future Redis storage"""
|
18 |
-
|
19 |
-
MAX_CHAT_HISTORY: int = field(default=20, init=False, repr=False)
|
20 |
-
|
21 |
-
session_id: str
|
22 |
-
project_name: str
|
23 |
-
version_no: int
|
24 |
-
is_realtime: Optional[bool] = False
|
25 |
-
locale: Optional[str] = "tr"
|
26 |
-
|
27 |
-
# State management - string for better debugging
|
28 |
-
state: str = "idle" # idle | collect_params | call_api | humanize
|
29 |
-
|
30 |
-
# Minimal stored data
|
31 |
-
current_intent: Optional[str] = None
|
32 |
-
variables: Dict[str, str] = field(default_factory=dict)
|
33 |
-
project_id: Optional[int] = None
|
34 |
-
version_id: Optional[int] = None
|
35 |
-
|
36 |
-
# Chat history - limited to recent messages
|
37 |
-
chat_history: List[Dict[str, str]] = field(default_factory=list)
|
38 |
-
|
39 |
-
# Metadata
|
40 |
-
created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
41 |
-
last_activity: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
42 |
-
|
43 |
-
# Parameter collection state
|
44 |
-
awaiting_parameters: List[str] = field(default_factory=list)
|
45 |
-
asked_parameters: Dict[str, int] = field(default_factory=dict)
|
46 |
-
unanswered_parameters: List[str] = field(default_factory=list)
|
47 |
-
parameter_ask_rounds: int = 0
|
48 |
-
|
49 |
-
# Transient data (not serialized to Redis)
|
50 |
-
_version_config: Optional[VersionConfig] = field(default=None, init=False, repr=False)
|
51 |
-
_intent_config: Optional[IntentConfig] = field(default=None, init=False, repr=False)
|
52 |
-
_auth_tokens: Dict[str, Dict] = field(default_factory=dict, init=False, repr=False)
|
53 |
-
|
54 |
-
def add_message(self, role: str, content: str) -> None:
|
55 |
-
"""Add message to chat history with size limit"""
|
56 |
-
message = {
|
57 |
-
"role": role,
|
58 |
-
"content": content,
|
59 |
-
"timestamp": datetime.utcnow().isoformat()
|
60 |
-
}
|
61 |
-
|
62 |
-
self.chat_history.append(message)
|
63 |
-
|
64 |
-
# Keep only recent messages
|
65 |
-
if len(self.chat_history) > self.MAX_CHAT_HISTORY:
|
66 |
-
self.chat_history = self.chat_history[-self.MAX_CHAT_HISTORY:]
|
67 |
-
|
68 |
-
# Update activity
|
69 |
-
self.last_activity = datetime.utcnow().isoformat()
|
70 |
-
|
71 |
-
log_debug(
|
72 |
-
f"Message added to session",
|
73 |
-
session_id=self.session_id,
|
74 |
-
role=role,
|
75 |
-
history_size=len(self.chat_history)
|
76 |
-
)
|
77 |
-
|
78 |
-
def add_turn(self, role: str, content: str) -> None:
|
79 |
-
"""Alias for add_message for compatibility"""
|
80 |
-
self.add_message(role, content)
|
81 |
-
|
82 |
-
def set_version_config(self, config: VersionConfig) -> None:
|
83 |
-
"""Set transient version config"""
|
84 |
-
self._version_config = config
|
85 |
-
|
86 |
-
def get_version_config(self) -> Optional[VersionConfig]:
|
87 |
-
"""Get transient version config"""
|
88 |
-
return self._version_config
|
89 |
-
|
90 |
-
def set_intent_config(self, config: IntentConfig) -> None:
|
91 |
-
"""Set current intent config"""
|
92 |
-
self._intent_config = config
|
93 |
-
self.current_intent = config.name if config else None
|
94 |
-
|
95 |
-
def get_intent_config(self) -> Optional[IntentConfig]:
|
96 |
-
"""Get current intent config"""
|
97 |
-
return self._intent_config
|
98 |
-
|
99 |
-
def reset_flow(self) -> None:
|
100 |
-
"""Reset conversation flow to idle"""
|
101 |
-
self.state = "idle"
|
102 |
-
self.current_intent = None
|
103 |
-
self._intent_config = None
|
104 |
-
self.awaiting_parameters = []
|
105 |
-
self.asked_parameters = {}
|
106 |
-
self.unanswered_parameters = []
|
107 |
-
self.parameter_ask_rounds = 0
|
108 |
-
|
109 |
-
log_debug(
|
110 |
-
f"Session flow reset",
|
111 |
-
session_id=self.session_id
|
112 |
-
)
|
113 |
-
|
114 |
-
def to_redis(self) -> str:
|
115 |
-
"""Serialize for Redis storage"""
|
116 |
-
data = {
|
117 |
-
'session_id': self.session_id,
|
118 |
-
'project_name': self.project_name,
|
119 |
-
'version_no': self.version_no,
|
120 |
-
'state': self.state,
|
121 |
-
'current_intent': self.current_intent,
|
122 |
-
'variables': self.variables,
|
123 |
-
'project_id': self.project_id,
|
124 |
-
'version_id': self.version_id,
|
125 |
-
'chat_history': self.chat_history[-self.MAX_CHAT_HISTORY:],
|
126 |
-
'created_at': self.created_at,
|
127 |
-
'last_activity': self.last_activity,
|
128 |
-
'awaiting_parameters': self.awaiting_parameters,
|
129 |
-
'asked_parameters': self.asked_parameters,
|
130 |
-
'unanswered_parameters': self.unanswered_parameters,
|
131 |
-
'parameter_ask_rounds': self.parameter_ask_rounds,
|
132 |
-
'is_realtime': self.is_realtime
|
133 |
-
}
|
134 |
-
return json.dumps(data, ensure_ascii=False)
|
135 |
-
|
136 |
-
@classmethod
|
137 |
-
def from_redis(cls, data: str) -> 'Session':
|
138 |
-
"""Deserialize from Redis"""
|
139 |
-
obj = json.loads(data)
|
140 |
-
return cls(**obj)
|
141 |
-
|
142 |
-
def get_state_info(self) -> dict:
|
143 |
-
"""Get debug info about current state"""
|
144 |
-
return {
|
145 |
-
'state': self.state,
|
146 |
-
'intent': self.current_intent,
|
147 |
-
'variables': list(self.variables.keys()),
|
148 |
-
'history_length': len(self.chat_history),
|
149 |
-
'awaiting_params': self.awaiting_parameters,
|
150 |
-
'last_activity': self.last_activity
|
151 |
-
}
|
152 |
-
|
153 |
-
def get_auth_token(self, api_name: str) -> Optional[Dict]:
|
154 |
-
"""Get cached auth token for API"""
|
155 |
-
return self._auth_tokens.get(api_name)
|
156 |
-
|
157 |
-
def set_auth_token(self, api_name: str, token_data: Dict) -> None:
|
158 |
-
"""Cache auth token for API"""
|
159 |
-
self._auth_tokens[api_name] = token_data
|
160 |
-
|
161 |
-
def is_expired(self, timeout_minutes: int = 30) -> bool:
|
162 |
-
"""Check if session is expired"""
|
163 |
-
last_activity_time = datetime.fromisoformat(self.last_activity.replace('Z', '+00:00'))
|
164 |
-
current_time = datetime.utcnow()
|
165 |
-
elapsed_minutes = (current_time - last_activity_time).total_seconds() / 60
|
166 |
-
return elapsed_minutes > timeout_minutes
|
167 |
-
|
168 |
-
|
169 |
-
def generate_secure_session_id() -> str:
|
170 |
-
"""Generate cryptographically secure session ID"""
|
171 |
-
# Use secrets for secure random generation
|
172 |
-
random_bytes = secrets.token_bytes(32)
|
173 |
-
|
174 |
-
# Add timestamp for uniqueness
|
175 |
-
timestamp = str(int(time.time() * 1000000))
|
176 |
-
|
177 |
-
# Combine and hash
|
178 |
-
combined = random_bytes + timestamp.encode()
|
179 |
-
session_id = hashlib.sha256(combined).hexdigest()
|
180 |
-
|
181 |
-
return f"session_{session_id[:32]}"
|
182 |
-
|
183 |
-
class SessionStore:
|
184 |
-
"""In-memory session store (to be replaced with Redis)"""
|
185 |
-
|
186 |
-
def __init__(self):
|
187 |
-
self._sessions: Dict[str, Session] = {}
|
188 |
-
self._lock = threading.Lock()
|
189 |
-
|
190 |
-
def create_session(
|
191 |
-
self,
|
192 |
-
project_name: str,
|
193 |
-
version_no: int,
|
194 |
-
is_realtime: bool = False,
|
195 |
-
locale: str = "tr"
|
196 |
-
) -> Session:
|
197 |
-
"""Create new session"""
|
198 |
-
session_id = generate_secure_session_id()
|
199 |
-
|
200 |
-
session = Session(
|
201 |
-
session_id=session_id,
|
202 |
-
project_name=project_name,
|
203 |
-
version_no=version_no,
|
204 |
-
is_realtime=is_realtime,
|
205 |
-
locale=locale
|
206 |
-
)
|
207 |
-
|
208 |
-
with self._lock:
|
209 |
-
self._sessions[session_id] = session
|
210 |
-
|
211 |
-
log_info(
|
212 |
-
"Session created",
|
213 |
-
session_id=session_id,
|
214 |
-
project=project_name,
|
215 |
-
version=version_no,
|
216 |
-
is_realtime=is_realtime,
|
217 |
-
locale=locale
|
218 |
-
)
|
219 |
-
|
220 |
-
return session
|
221 |
-
|
222 |
-
def get_session(self, session_id: str) -> Optional[Session]:
|
223 |
-
"""Get session by ID"""
|
224 |
-
with self._lock:
|
225 |
-
session = self._sessions.get(session_id)
|
226 |
-
|
227 |
-
if session and session.is_expired():
|
228 |
-
log_info(f"Session expired", session_id=session_id)
|
229 |
-
self.delete_session(session_id)
|
230 |
-
return None
|
231 |
-
|
232 |
-
return session
|
233 |
-
|
234 |
-
def update_session(self, session: Session) -> None:
|
235 |
-
"""Update session in store"""
|
236 |
-
session.last_activity = datetime.utcnow().isoformat()
|
237 |
-
|
238 |
-
with self._lock:
|
239 |
-
self._sessions[session.session_id] = session
|
240 |
-
|
241 |
-
def delete_session(self, session_id: str) -> None:
|
242 |
-
"""Delete session"""
|
243 |
-
with self._lock:
|
244 |
-
if session_id in self._sessions:
|
245 |
-
del self._sessions[session_id]
|
246 |
-
log_info(f"Session deleted", session_id=session_id)
|
247 |
-
|
248 |
-
def cleanup_expired_sessions(self, timeout_minutes: int = 30) -> int:
|
249 |
-
"""Clean up expired sessions"""
|
250 |
-
expired_count = 0
|
251 |
-
|
252 |
-
with self._lock:
|
253 |
-
expired_ids = [
|
254 |
-
sid for sid, session in self._sessions.items()
|
255 |
-
if session.is_expired(timeout_minutes)
|
256 |
-
]
|
257 |
-
|
258 |
-
for session_id in expired_ids:
|
259 |
-
del self._sessions[session_id]
|
260 |
-
expired_count += 1
|
261 |
-
|
262 |
-
if expired_count > 0:
|
263 |
-
log_info(
|
264 |
-
f"Cleaned up expired sessions",
|
265 |
-
count=expired_count
|
266 |
-
)
|
267 |
-
|
268 |
-
return expired_count
|
269 |
-
|
270 |
-
def get_active_session_count(self) -> int:
|
271 |
-
"""Get count of active sessions"""
|
272 |
-
with self._lock:
|
273 |
-
return len(self._sessions)
|
274 |
-
|
275 |
-
def get_session_stats(self) -> Dict[str, Any]:
|
276 |
-
"""Get session statistics"""
|
277 |
-
with self._lock:
|
278 |
-
realtime_count = sum(
|
279 |
-
1 for s in self._sessions.values()
|
280 |
-
if s.is_realtime
|
281 |
-
)
|
282 |
-
|
283 |
-
return {
|
284 |
-
'total_sessions': len(self._sessions),
|
285 |
-
'realtime_sessions': realtime_count,
|
286 |
-
'regular_sessions': len(self._sessions) - realtime_count
|
287 |
-
}
|
288 |
-
|
289 |
-
|
290 |
-
# Global session store instance
|
291 |
-
import threading
|
292 |
-
session_store = SessionStore()
|
293 |
-
|
294 |
-
# Session cleanup task
|
295 |
-
def start_session_cleanup(interval_minutes: int = 5, timeout_minutes: int = 30):
|
296 |
-
"""Start background task to clean up expired sessions"""
|
297 |
-
import asyncio
|
298 |
-
|
299 |
-
async def cleanup_task():
|
300 |
-
while True:
|
301 |
-
try:
|
302 |
-
expired = session_store.cleanup_expired_sessions(timeout_minutes)
|
303 |
-
if expired > 0:
|
304 |
-
log_info(f"Session cleanup completed", expired=expired)
|
305 |
-
except Exception as e:
|
306 |
-
log_error(f"Session cleanup error", error=str(e))
|
307 |
-
|
308 |
-
await asyncio.sleep(interval_minutes * 60)
|
309 |
-
|
310 |
-
# Run in background
|
311 |
-
asyncio.create_task(cleanup_task())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|