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.
- mbxai/__init__.py +23 -2
- mbxai/agent/__init__.py +13 -2
- mbxai/agent/client.py +840 -629
- mbxai/agent/client_legacy.py +804 -0
- mbxai/agent/models.py +264 -31
- mbxai/examples/enhanced_agent_example.py +344 -0
- mbxai/examples/redis_session_handler_example.py +248 -0
- mbxai/mcp/server.py +1 -1
- mbxai/openrouter/client.py +1 -83
- mbxai-2.3.0.dist-info/METADATA +1191 -0
- {mbxai-2.1.3.dist-info → mbxai-2.3.0.dist-info}/RECORD +13 -10
- mbxai-2.1.3.dist-info/METADATA +0 -346
- {mbxai-2.1.3.dist-info → mbxai-2.3.0.dist-info}/WHEEL +0 -0
- {mbxai-2.1.3.dist-info → mbxai-2.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
mbxai/openrouter/client.py
CHANGED
@@ -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
|
-
|
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.
|