mbxai 2.1.3__py3-none-any.whl → 2.3.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.
@@ -0,0 +1,248 @@
1
+ """
2
+ Example of a Redis session handler for the AgentClient.
3
+
4
+ This demonstrates how to implement a custom session handler that uses Redis
5
+ for storing agent sessions, enabling distributed and persistent session storage.
6
+
7
+ Requirements:
8
+ pip install redis
9
+
10
+ Usage:
11
+ python redis_session_handler_example.py
12
+ """
13
+
14
+ import json
15
+ import logging
16
+ from typing import Dict, Any, Optional
17
+ import redis
18
+
19
+ from mbxai import AgentClient, OpenRouterClient
20
+ from mbxai.agent.models import SessionHandler
21
+
22
+ # Configure logging
23
+ logging.basicConfig(level=logging.INFO)
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class RedisSessionHandler:
28
+ """Redis-based session handler for AgentClient."""
29
+
30
+ def __init__(
31
+ self,
32
+ redis_client: redis.Redis = None,
33
+ key_prefix: str = "mbxai:agent:session:",
34
+ ttl_seconds: int = 86400 # 24 hours default TTL
35
+ ):
36
+ """
37
+ Initialize Redis session handler.
38
+
39
+ Args:
40
+ redis_client: Redis client instance (creates default if None)
41
+ key_prefix: Prefix for Redis keys
42
+ ttl_seconds: Session TTL in seconds (0 = no expiration)
43
+ """
44
+ self.redis_client = redis_client or redis.Redis(
45
+ host='localhost',
46
+ port=6379,
47
+ db=0,
48
+ decode_responses=True
49
+ )
50
+ self.key_prefix = key_prefix
51
+ self.ttl_seconds = ttl_seconds
52
+
53
+ def _get_key(self, agent_id: str) -> str:
54
+ """Get Redis key for agent session."""
55
+ return f"{self.key_prefix}{agent_id}"
56
+
57
+ def get_session(self, agent_id: str) -> Optional[Dict[str, Any]]:
58
+ """Retrieve a session by agent ID."""
59
+ try:
60
+ key = self._get_key(agent_id)
61
+ session_json = self.redis_client.get(key)
62
+
63
+ if session_json:
64
+ session_data = json.loads(session_json)
65
+ logger.debug(f"Retrieved session {agent_id} from Redis")
66
+ return session_data
67
+
68
+ logger.debug(f"Session {agent_id} not found in Redis")
69
+ return None
70
+
71
+ except (redis.RedisError, json.JSONDecodeError) as e:
72
+ logger.error(f"Error retrieving session {agent_id}: {e}")
73
+ return None
74
+
75
+ def set_session(self, agent_id: str, session_data: Dict[str, Any]) -> None:
76
+ """Store session data for an agent ID."""
77
+ try:
78
+ key = self._get_key(agent_id)
79
+ session_json = json.dumps(session_data, default=str)
80
+
81
+ if self.ttl_seconds > 0:
82
+ self.redis_client.setex(key, self.ttl_seconds, session_json)
83
+ else:
84
+ self.redis_client.set(key, session_json)
85
+
86
+ logger.debug(f"Stored session {agent_id} in Redis")
87
+
88
+ except (redis.RedisError, json.JSONEncodeError) as e:
89
+ logger.error(f"Error storing session {agent_id}: {e}")
90
+ raise
91
+
92
+ def delete_session(self, agent_id: str) -> bool:
93
+ """Delete a session by agent ID."""
94
+ try:
95
+ key = self._get_key(agent_id)
96
+ result = self.redis_client.delete(key)
97
+
98
+ if result > 0:
99
+ logger.debug(f"Deleted session {agent_id} from Redis")
100
+ return True
101
+ else:
102
+ logger.debug(f"Session {agent_id} not found for deletion")
103
+ return False
104
+
105
+ except redis.RedisError as e:
106
+ logger.error(f"Error deleting session {agent_id}: {e}")
107
+ return False
108
+
109
+ def list_sessions(self) -> list[str]:
110
+ """List all active session IDs."""
111
+ try:
112
+ pattern = f"{self.key_prefix}*"
113
+ keys = self.redis_client.keys(pattern)
114
+
115
+ # Extract agent IDs from keys
116
+ agent_ids = [
117
+ key.replace(self.key_prefix, "")
118
+ for key in keys
119
+ ]
120
+
121
+ logger.debug(f"Found {len(agent_ids)} sessions in Redis")
122
+ return agent_ids
123
+
124
+ except redis.RedisError as e:
125
+ logger.error(f"Error listing sessions: {e}")
126
+ return []
127
+
128
+ def session_exists(self, agent_id: str) -> bool:
129
+ """Check if a session exists."""
130
+ try:
131
+ key = self._get_key(agent_id)
132
+ return self.redis_client.exists(key) > 0
133
+ except redis.RedisError as e:
134
+ logger.error(f"Error checking session existence {agent_id}: {e}")
135
+ return False
136
+
137
+ def get_session_ttl(self, agent_id: str) -> int:
138
+ """Get remaining TTL for a session (Redis-specific method)."""
139
+ try:
140
+ key = self._get_key(agent_id)
141
+ return self.redis_client.ttl(key)
142
+ except redis.RedisError as e:
143
+ logger.error(f"Error getting TTL for session {agent_id}: {e}")
144
+ return -1
145
+
146
+ def extend_session_ttl(self, agent_id: str, additional_seconds: int = None) -> bool:
147
+ """Extend session TTL (Redis-specific method)."""
148
+ try:
149
+ key = self._get_key(agent_id)
150
+ ttl = additional_seconds or self.ttl_seconds
151
+ return self.redis_client.expire(key, ttl)
152
+ except redis.RedisError as e:
153
+ logger.error(f"Error extending TTL for session {agent_id}: {e}")
154
+ return False
155
+
156
+
157
+ class DistributedRedisSessionHandler(RedisSessionHandler):
158
+ """Redis session handler with clustering/sentinel support."""
159
+
160
+ def __init__(
161
+ self,
162
+ redis_sentinels: list[tuple[str, int]] = None,
163
+ service_name: str = "mymaster",
164
+ **kwargs
165
+ ):
166
+ """
167
+ Initialize with Redis Sentinel for high availability.
168
+
169
+ Args:
170
+ redis_sentinels: List of (host, port) tuples for sentinels
171
+ service_name: Name of the Redis service in Sentinel
172
+ """
173
+ if redis_sentinels:
174
+ sentinel = redis.Sentinel(redis_sentinels)
175
+ redis_client = sentinel.master_for(service_name, decode_responses=True)
176
+ else:
177
+ redis_client = None
178
+
179
+ super().__init__(redis_client=redis_client, **kwargs)
180
+
181
+
182
+ def main():
183
+ """Example usage of Redis session handler."""
184
+
185
+ try:
186
+ # Create Redis session handler
187
+ redis_handler = RedisSessionHandler(
188
+ ttl_seconds=3600 # 1 hour TTL
189
+ )
190
+
191
+ # Test Redis connection
192
+ redis_handler.redis_client.ping()
193
+ logger.info("✅ Connected to Redis")
194
+
195
+ # Create AgentClient with Redis session handler
196
+ openrouter_client = OpenRouterClient(
197
+ api_key="your-api-key-here" # Replace with actual key
198
+ )
199
+
200
+ agent = AgentClient(
201
+ ai_client=openrouter_client,
202
+ human_in_loop=True,
203
+ session_handler=redis_handler
204
+ )
205
+
206
+ logger.info("✅ AgentClient created with Redis session handler")
207
+
208
+ # Example: Create a session
209
+ from pydantic import BaseModel
210
+
211
+ class SimpleResponse(BaseModel):
212
+ message: str
213
+
214
+ # Start agent conversation
215
+ response = agent.agent(
216
+ prompt="Hello, this is a test with Redis session storage",
217
+ final_response_structure=SimpleResponse
218
+ )
219
+
220
+ agent_id = response.agent_id
221
+ logger.info(f"✅ Created agent session: {agent_id}")
222
+
223
+ # Test session persistence
224
+ session_info = agent.get_session_info(agent_id)
225
+ logger.info(f"✅ Session retrieved: {len(session_info)} fields")
226
+
227
+ # Test session TTL
228
+ ttl = redis_handler.get_session_ttl(agent_id)
229
+ logger.info(f"✅ Session TTL: {ttl} seconds")
230
+
231
+ # List all sessions
232
+ sessions = agent.list_sessions()
233
+ logger.info(f"✅ Active sessions: {sessions}")
234
+
235
+ # Clean up
236
+ agent.delete_session(agent_id)
237
+ logger.info(f"✅ Session {agent_id} deleted")
238
+
239
+ except redis.ConnectionError:
240
+ logger.error("❌ Could not connect to Redis. Make sure Redis is running on localhost:6379")
241
+ logger.info("Start Redis with: redis-server")
242
+
243
+ except Exception as e:
244
+ logger.error(f"❌ Error: {e}")
245
+
246
+
247
+ if __name__ == "__main__":
248
+ main()
mbxai/mcp/server.py CHANGED
@@ -31,7 +31,7 @@ class MCPServer:
31
31
  self.app = FastAPI(
32
32
  title=self.name,
33
33
  description=self.description,
34
- version="2.1.3",
34
+ version="2.3.0",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
@@ -4,7 +4,7 @@ OpenRouter client implementation.
4
4
 
5
5
  from typing import Any, Optional, Union, Type
6
6
  from openai import OpenAI, OpenAIError, RateLimitError, APITimeoutError, APIConnectionError, BadRequestError, AuthenticationError
7
- from openai.lib._parsing import type_to_response_format_param
7
+
8
8
  from .models import OpenRouterModel, OpenRouterModelRegistry
9
9
  from .config import OpenRouterConfig
10
10
  from .schema import format_response
@@ -350,88 +350,6 @@ class OpenRouterClient:
350
350
  logger.error("Could not read response content")
351
351
  self._handle_api_error("parse completion", e)
352
352
 
353
- @with_retry()
354
- def create_parsed(
355
- self,
356
- messages: list[dict[str, Any]],
357
- response_format: Type[BaseModel],
358
- *,
359
- model: str | None = None,
360
- stream: bool = False,
361
- **kwargs: Any,
362
- ) -> Any:
363
- """Get a chat completion from OpenRouter with structured output.
364
-
365
- Args:
366
- messages: The messages to send to the model
367
- response_format: A Pydantic model defining the expected response format
368
- model: Optional model override
369
- stream: Whether to stream the response
370
- **kwargs: Additional arguments to pass to the API
371
-
372
- Returns:
373
- The parsed response from the model
374
- """
375
- try:
376
- # Convert Pydantic model to OpenAI response format
377
- response_format_param = type_to_response_format_param(response_format)
378
-
379
- # Log the request details
380
- logger.debug(f"Sending parsed chat completion request to OpenRouter with model: {model or self.model}")
381
- logger.debug(f"Message count: {len(messages)}")
382
- logger.debug(f"Response format: {json.dumps(response_format_param, indent=2)}")
383
-
384
- # Calculate total message size for logging
385
- total_size = sum(len(str(msg)) for msg in messages)
386
- logger.debug(f"Total message size: {total_size} bytes")
387
-
388
- request = {
389
- "model": model or self.model,
390
- "messages": messages,
391
- "stream": stream,
392
- "response_format": response_format_param,
393
- **kwargs,
394
- }
395
-
396
- response = self._client.chat.completions.create(**request)
397
-
398
- if response is None:
399
- logger.error("Received None response from OpenRouter API")
400
- raise OpenRouterAPIError("Received None response from OpenRouter API")
401
-
402
- # Validate response structure
403
- if not hasattr(response, 'choices'):
404
- logger.error(f"Response missing 'choices' attribute. Available attributes: {dir(response)}")
405
- raise OpenRouterAPIError("Invalid response format: missing 'choices' attribute")
406
-
407
- if response.choices is None:
408
- logger.error("Response choices is None")
409
- raise OpenRouterAPIError("Invalid response format: choices is None")
410
-
411
- logger.debug(f"Response type: {type(response)}")
412
- logger.debug(f"Response attributes: {dir(response)}")
413
- logger.debug(f"Received response from OpenRouter: {len(response.choices)} choices")
414
-
415
- return response
416
-
417
- except Exception as e:
418
- stack_trace = traceback.format_exc()
419
- logger.error(f"Error in parsed chat completion: {str(e)}")
420
- logger.error(f"Stack trace:\n{stack_trace}")
421
- logger.error(f"Request details: model={model or self.model}, stream={stream}, kwargs={kwargs}")
422
- logger.error(f"Message structure: {[{'role': msg.get('role'), 'content_length': len(str(msg.get('content', '')))} for msg in messages]}")
423
-
424
- if hasattr(e, 'response') and e.response is not None:
425
- logger.error(f"Response status: {e.response.status_code}")
426
- logger.error(f"Response headers: {e.response.headers}")
427
- try:
428
- content = e.response.text
429
- logger.error(f"Response content length: {len(content)} bytes")
430
- logger.error(f"Response content preview: {content[:1000]}...")
431
- except:
432
- logger.error("Could not read response content")
433
- self._handle_api_error("parsed chat completion", e)
434
-
435
353
  @classmethod
436
354
  def register_model(cls, name: str, value: str) -> None:
437
355
  """Register a new model.