agent-api-server 2.1.7__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.
Files changed (52) hide show
  1. agent_api_server/__init__.py +0 -0
  2. agent_api_server/api/__init__.py +0 -0
  3. agent_api_server/api/v1/__init__.py +0 -0
  4. agent_api_server/api/v1/api.py +25 -0
  5. agent_api_server/api/v1/config.py +57 -0
  6. agent_api_server/api/v1/graph.py +59 -0
  7. agent_api_server/api/v1/schema.py +57 -0
  8. agent_api_server/api/v1/thread.py +563 -0
  9. agent_api_server/cache/__init__.py +0 -0
  10. agent_api_server/cache/redis_cache.py +385 -0
  11. agent_api_server/callback_handler.py +18 -0
  12. agent_api_server/client/css/styles.css +1202 -0
  13. agent_api_server/client/favicon.ico +0 -0
  14. agent_api_server/client/index.html +102 -0
  15. agent_api_server/client/js/app.js +1499 -0
  16. agent_api_server/client/js/index.umd.js +824 -0
  17. agent_api_server/config_center/config_center.py +239 -0
  18. agent_api_server/configs/__init__.py +3 -0
  19. agent_api_server/configs/config.py +163 -0
  20. agent_api_server/dynamic_llm/__init__.py +0 -0
  21. agent_api_server/dynamic_llm/dynamic_llm.py +331 -0
  22. agent_api_server/listener.py +530 -0
  23. agent_api_server/log/__init__.py +0 -0
  24. agent_api_server/log/formatters.py +122 -0
  25. agent_api_server/log/logging.json +50 -0
  26. agent_api_server/mcp_convert/__init__.py +0 -0
  27. agent_api_server/mcp_convert/mcp_convert.py +375 -0
  28. agent_api_server/memeory/__init__.py +0 -0
  29. agent_api_server/memeory/postgres.py +233 -0
  30. agent_api_server/register/__init__.py +0 -0
  31. agent_api_server/register/register.py +65 -0
  32. agent_api_server/service.py +354 -0
  33. agent_api_server/service_hub/service_hub.py +233 -0
  34. agent_api_server/service_hub/service_hub_test.py +700 -0
  35. agent_api_server/shared/__init__.py +0 -0
  36. agent_api_server/shared/ase.py +54 -0
  37. agent_api_server/shared/base_model.py +103 -0
  38. agent_api_server/shared/common.py +110 -0
  39. agent_api_server/shared/decode_token.py +107 -0
  40. agent_api_server/shared/detect_message.py +410 -0
  41. agent_api_server/shared/get_model_info.py +491 -0
  42. agent_api_server/shared/message.py +419 -0
  43. agent_api_server/shared/util_func.py +372 -0
  44. agent_api_server/sso_service/__init__.py +1 -0
  45. agent_api_server/sso_service/sdk/__init__.py +1 -0
  46. agent_api_server/sso_service/sdk/client.py +224 -0
  47. agent_api_server/sso_service/sdk/credential.py +11 -0
  48. agent_api_server/sso_service/sdk/encoding.py +22 -0
  49. agent_api_server/sso_service/sso_service.py +177 -0
  50. agent_api_server-2.1.7.dist-info/METADATA +130 -0
  51. agent_api_server-2.1.7.dist-info/RECORD +52 -0
  52. agent_api_server-2.1.7.dist-info/WHEEL +4 -0
@@ -0,0 +1,530 @@
1
+ import json
2
+ import threading
3
+ import time
4
+ import logging
5
+ import asyncio
6
+ from typing import Optional, Dict, Any, List
7
+ import redis
8
+ import nats
9
+ from nats.js.api import StreamConfig, RetentionPolicy, StorageType, ConsumerConfig
10
+ from model_manage_client import ModelManageClient
11
+ from agent_api_server.callback_handler import notify_callbacks
12
+ from agent_api_server.configs import global_config
13
+ from enum import Enum, auto
14
+ from dataclasses import dataclass
15
+ from agent_api_server.shared.util_func import set_model_config
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ListenerType(Enum):
21
+ REDIS = auto()
22
+ NATS = auto()
23
+
24
+
25
+ @dataclass
26
+ class ModelUpdateEvent:
27
+ event_type: str
28
+ tenant_id: str
29
+ tool_name: Optional[str] = None
30
+ provider: Optional[str] = None
31
+ model: Optional[str] = None
32
+ credentials: Optional[Dict[str, Any]] = None
33
+ model_type: Optional[str] = None
34
+
35
+
36
+ class MessageListener:
37
+ def __init__(self, agent_name: str, client_token: str):
38
+ self.agent_name = agent_name
39
+ self.client_token = client_token
40
+ self.m_client = ModelManageClient(global_config.MODEL_MANAGER_SERVICE_URL, client_token)
41
+ self.running = False
42
+
43
+ @staticmethod
44
+ def parse_nats_message(message_data: Dict[str, Any]) -> List[ModelUpdateEvent]:
45
+ events = []
46
+ try:
47
+ event_type = message_data.get('event_type', '')
48
+ message = message_data.get('message', {})
49
+
50
+ if isinstance(message, dict):
51
+ logger.debug("Message is a dictionary, processing...")
52
+ for tool_name, tool_data in message.items():
53
+ if isinstance(tool_data, dict):
54
+ for model_type, model_data in tool_data.items():
55
+ events.append(ModelUpdateEvent(
56
+ event_type=event_type,
57
+ tenant_id=message_data.get('tenant_id', ''),
58
+ tool_name=tool_name,
59
+ provider=model_data.get('provider'),
60
+ model=model_data.get('model'),
61
+ credentials=model_data.get('credentials', {}),
62
+ model_type=model_type
63
+ ))
64
+
65
+ except Exception as e:
66
+ logger.error(f"Failed to parse message: {e}")
67
+
68
+ return events
69
+
70
+ @staticmethod
71
+ def parse_redis_message(message_data: Dict[str, Any]) -> List[ModelUpdateEvent]:
72
+ events = []
73
+ try:
74
+ event_type = message_data.get('event', '')
75
+ message = message_data.get('message', {})
76
+
77
+ if isinstance(message, dict):
78
+ logger.debug("Message is a dictionary, processing...")
79
+ for tool_name, tool_data in message.items():
80
+ for model_type, model_data in tool_data.items():
81
+ events.append(ModelUpdateEvent(
82
+ event_type=event_type,
83
+ tenant_id='',
84
+ tool_name=tool_name,
85
+ provider=model_data.get('provider'),
86
+ model=model_data.get('model'),
87
+ credentials=model_data.get('credentials', {}),
88
+ model_type=model_type
89
+ ))
90
+
91
+ except Exception as e:
92
+ logger.error(f"Failed to parse message: {e}")
93
+
94
+ return events
95
+
96
+ async def handle_events(self, events: List[ModelUpdateEvent], ts_id: str):
97
+ try:
98
+ agent_info = self.m_client.get_agent(agent_name=self.agent_name)
99
+ agent_id = agent_info[0].get('agent_id')
100
+ logger.info(f"Processing events for agent: {self.agent_name}, agent_id: {agent_id}")
101
+
102
+ for event in events:
103
+ if event.event_type in ('delete model', 'delete'):
104
+ tool_name = event.tool_name or 'default'
105
+ logger.info(f"""
106
+ Processing {event.event_type} event:
107
+ - Tenant ID: {ts_id}
108
+ - Tool: {tool_name}
109
+ - Provider: {event.provider}
110
+ - Model: {event.model}
111
+ - Model Type: {event.model_type}
112
+ """)
113
+
114
+ set_model_config(
115
+ tool_name=tool_name,
116
+ model_type=event.model_type,
117
+ model_name=event.model,
118
+ model_provider=event.provider,
119
+ credentials="",
120
+ agent_id=agent_id,
121
+ ts_id=ts_id
122
+ )
123
+
124
+ notify_callbacks(
125
+ ts_id=ts_id,
126
+ client_token=self.client_token,
127
+ model_type=event.model_type
128
+ )
129
+ elif event.event_type in ('update model', 'update'):
130
+ tool_name = event.tool_name or 'default'
131
+ logger.info(f"""
132
+ Processing {event.event_type} event:
133
+ - Tenant ID: {ts_id}
134
+ - Tool: {tool_name}
135
+ - Provider: {event.provider}
136
+ - Model: {event.model}
137
+ - Model Type: {event.model_type}
138
+ """)
139
+
140
+ set_model_config(
141
+ tool_name=tool_name,
142
+ model_type=event.model_type,
143
+ model_name=event.model,
144
+ model_provider=event.provider,
145
+ credentials=json.dumps(event.credentials) if event.credentials else "",
146
+ agent_id=agent_id,
147
+ ts_id=ts_id
148
+ )
149
+
150
+ notify_callbacks(
151
+ ts_id=ts_id,
152
+ client_token=self.client_token,
153
+ model_type=event.model_type
154
+ )
155
+
156
+ except Exception as e:
157
+ logger.error(f"Error processing events: {e}", exc_info=True)
158
+
159
+
160
+ class RedisListener(MessageListener):
161
+ def __init__(self, agent_name: str, client_token: str):
162
+ super().__init__(agent_name, client_token)
163
+ self.redis_client: Optional[redis.Redis] = None
164
+ self.pubsub: Optional[redis.client.PubSub] = None
165
+ self.base_retry_delay = 5
166
+ self.max_retry_delay = 60
167
+
168
+ def run(self):
169
+ self.running = True
170
+ retry_delay = self.base_retry_delay
171
+
172
+ while self.running:
173
+ try:
174
+ self._connect()
175
+ retry_delay = self.base_retry_delay
176
+ self._listen()
177
+ except redis.ConnectionError as e:
178
+ logger.error(f"Redis connection error: {e}")
179
+ time.sleep(retry_delay)
180
+ retry_delay = min(retry_delay * 2, self.max_retry_delay)
181
+ except Exception as e:
182
+ logger.error(f"Unexpected error: {e}")
183
+ time.sleep(retry_delay)
184
+ finally:
185
+ self._disconnect()
186
+
187
+ def _connect(self):
188
+ try:
189
+ self.redis_client = redis.Redis.from_url(
190
+ url=global_config.MODEL_MANAGER_REDIS_URL,
191
+ socket_timeout=10,
192
+ socket_keepalive=True,
193
+ health_check_interval=15,
194
+ retry_on_timeout=True,
195
+ socket_connect_timeout=5
196
+ )
197
+
198
+ if not self.redis_client.ping():
199
+ raise ConnectionError("Redis ping failed")
200
+
201
+ self.pubsub = self.redis_client.pubsub()
202
+ self.pubsub.psubscribe(f'/model/update/{self.agent_name}/*')
203
+ logger.info(f"Redis listener connected for agent: {self.agent_name}")
204
+
205
+ except Exception as e:
206
+ self._disconnect()
207
+ raise ConnectionError(f"Redis connection failed: {e}")
208
+
209
+ def _listen(self):
210
+ while self.running:
211
+ try:
212
+ message = self.pubsub.get_message(
213
+ ignore_subscribe_messages=True,
214
+ timeout=30
215
+ )
216
+
217
+ if message is None:
218
+ continue
219
+
220
+ logger.debug(f"Received raw message from Redis: {message}")
221
+
222
+ if message['type'] == 'pmessage':
223
+ try:
224
+ channel = message['channel'].decode()
225
+ data = message['data'].decode()
226
+ ts_id = channel.split('/')[-1]
227
+
228
+ message_data = json.loads(data)
229
+ events = self.parse_redis_message(message_data)
230
+
231
+ asyncio.run(self.handle_events(events, ts_id))
232
+ except json.JSONDecodeError as e:
233
+ logger.error(f"Failed to decode message JSON: {e}")
234
+ except Exception as e:
235
+ logger.error(f"Error processing message: {e}")
236
+
237
+ except redis.ConnectionError:
238
+ logger.warning("Redis connection lost during listening")
239
+ raise
240
+ except Exception as e:
241
+ logger.error(f"Unexpected error in listen loop: {e}")
242
+ time.sleep(1)
243
+
244
+ def _disconnect(self):
245
+ try:
246
+ if self.pubsub:
247
+ self.pubsub.close()
248
+ self.pubsub = None
249
+ except Exception as e:
250
+ logger.warning(f"Error closing pubsub: {e}")
251
+
252
+ try:
253
+ if self.redis_client:
254
+ self.redis_client.close()
255
+ self.redis_client = None
256
+ except Exception as e:
257
+ logger.warning(f"Error closing redis client: {e}")
258
+
259
+ def stop(self):
260
+ self.running = False
261
+ self._disconnect()
262
+ logger.info("Redis listener stopped")
263
+
264
+
265
+ class NATSListener(MessageListener):
266
+ def __init__(self, agent_name: str, client_token: str):
267
+ super().__init__(agent_name, client_token)
268
+ self.nc = None
269
+ self.js = None
270
+ self.subscription = None
271
+ self.loop = asyncio.new_event_loop()
272
+ self.reconnect_attempts = 0
273
+ self.max_reconnect_attempts = 10
274
+ self.base_reconnect_delay = 5
275
+ self.thread = threading.Thread(target=self._start_loop, daemon=True)
276
+ self.thread.start()
277
+
278
+ def _start_loop(self):
279
+ asyncio.set_event_loop(self.loop)
280
+ self.loop.run_forever()
281
+
282
+ def run(self):
283
+ self.running = True
284
+ asyncio.run_coroutine_threadsafe(self._connect_and_listen(), self.loop)
285
+
286
+ async def _connect_and_listen(self):
287
+ while self.running and self.reconnect_attempts < self.max_reconnect_attempts:
288
+ try:
289
+ await self._connect()
290
+ await self._ensure_stream_exists()
291
+ await self._subscribe_to_existing_consumer()
292
+ self.reconnect_attempts = 0
293
+ await self._monitor_connection()
294
+ except Exception as e:
295
+ logger.error(f"NATS connection error: {e}")
296
+ self.reconnect_attempts += 1
297
+ delay = min(self.base_reconnect_delay * (2 ** (self.reconnect_attempts - 1)), 60)
298
+ logger.info(f"Attempting to reconnect in {delay} seconds... (attempt {self.reconnect_attempts})")
299
+ await asyncio.sleep(delay)
300
+ finally:
301
+ await self._close()
302
+
303
+ async def _connect(self):
304
+ try:
305
+ self.nc = await nats.connect(
306
+ servers=[global_config.MODEL_MANAGER_NATS_URL],
307
+ reconnect_time_wait=5,
308
+ max_reconnect_attempts=-1,
309
+ disconnected_cb=self._on_disconnected,
310
+ reconnected_cb=self._on_reconnected,
311
+ closed_cb=self._on_closed,
312
+ error_cb=self._on_error
313
+ )
314
+ self.js = self.nc.jetstream()
315
+ logger.info(f"NATS connected for agent: {self.agent_name}")
316
+ except Exception as e:
317
+ logger.error(f"NATS connection failed: {e}")
318
+ raise
319
+
320
+ async def _ensure_stream_exists(self):
321
+ stream_name = "model-management-service"
322
+ subject = f"model-management-service.model.>"
323
+
324
+ try:
325
+ await self.js.stream_info(stream_name)
326
+ logger.debug(f"Stream {stream_name} already exists")
327
+ except Exception as e:
328
+ if "stream not found" in str(e).lower():
329
+ stream_config = StreamConfig(
330
+ name=stream_name,
331
+ subjects=[subject],
332
+ retention=RetentionPolicy.LIMITS,
333
+ max_msgs=10000,
334
+ max_bytes=1024 * 1024 * 10,
335
+ max_age=3600 * 24,
336
+ storage=StorageType.FILE
337
+ )
338
+
339
+ try:
340
+ await self.js.add_stream(config=stream_config)
341
+ logger.info(f"Created stream {stream_name} with subject {subject}")
342
+ except Exception as create_error:
343
+ logger.error(f"Failed to create stream {stream_name}: {create_error}")
344
+ raise
345
+ else:
346
+ logger.error(f"Error checking stream existence: {e}")
347
+ raise
348
+
349
+ async def _subscribe_to_existing_consumer(self):
350
+ stream_name = "model-management-service"
351
+ subject = f"model-management-service.model.agent.{self.agent_name}.>"
352
+ durable_name = f"{self.agent_name}-consumer"
353
+
354
+ async def message_handler(msg):
355
+ try:
356
+ data = msg.data.decode()
357
+ message_data = json.loads(data)
358
+
359
+ prefix = f"model-management-service.model.agent.{self.agent_name}."
360
+ ts_id = msg.subject[len(prefix):]
361
+
362
+ logger.debug(f"Received raw message from NATS: {message_data}")
363
+
364
+ events = self.parse_nats_message(message_data)
365
+ await self.handle_events(events, ts_id)
366
+ await msg.ack()
367
+ except Exception as e:
368
+ logger.error(f"Message handling failed: {e}")
369
+
370
+ try:
371
+ if self.subscription:
372
+ try:
373
+ await self.subscription.unsubscribe()
374
+ logger.info(f"Unsubscribed from previous subscription")
375
+ await asyncio.sleep(1)
376
+ except Exception as e:
377
+ logger.warning(f"Error unsubscribing: {e}")
378
+ finally:
379
+ self.subscription = None
380
+
381
+ consumer_info = await self.js.consumer_info(stream_name, durable_name)
382
+ logger.info(f"Found existing consumer: {durable_name}")
383
+
384
+ desired_config = ConsumerConfig(
385
+ durable_name=durable_name,
386
+ deliver_subject=f"deliver.{durable_name}",
387
+ ack_wait=30 * 1000,
388
+ deliver_policy="all",
389
+ ack_policy="explicit",
390
+ max_deliver=5,
391
+ filter_subject=subject
392
+ )
393
+
394
+ current_config = consumer_info.config
395
+ need_recreate = (
396
+ current_config.deliver_subject != desired_config.deliver_subject or
397
+ current_config.ack_wait != desired_config.ack_wait or
398
+ current_config.deliver_policy != desired_config.deliver_policy or
399
+ current_config.ack_policy != desired_config.ack_policy or
400
+ current_config.max_deliver != desired_config.max_deliver or
401
+ current_config.filter_subject != desired_config.filter_subject
402
+ )
403
+
404
+ if need_recreate:
405
+ logger.info(f"Consumer config mismatch, recreating consumer: {durable_name}")
406
+ try:
407
+ await self.js.delete_consumer(stream_name, durable_name)
408
+ logger.info(f"Deleted old consumer: {durable_name}")
409
+
410
+ await self.js.add_consumer(stream_name, config=desired_config)
411
+ logger.info(f"Created new consumer with updated config: {durable_name}")
412
+ except Exception as delete_error:
413
+ logger.error(f"Failed to recreate consumer: {delete_error}")
414
+ raise
415
+
416
+ self.subscription = await self.js.subscribe(
417
+ subject=subject,
418
+ durable=durable_name,
419
+ cb=message_handler,
420
+ stream=stream_name
421
+ )
422
+ logger.info(f"Subscribed to NATS consumer: {durable_name}")
423
+
424
+ except Exception as e:
425
+ if "consumer not found" in str(e).lower():
426
+ logger.info(f"Creating new consumer: {durable_name}")
427
+ consumer_config = ConsumerConfig(
428
+ durable_name=durable_name,
429
+ deliver_subject=f"deliver.{durable_name}",
430
+ ack_wait=30 * 1000,
431
+ deliver_policy="all",
432
+ ack_policy="explicit",
433
+ max_deliver=5,
434
+ filter_subject=subject
435
+ )
436
+
437
+ await self.js.add_consumer(stream_name, config=consumer_config)
438
+
439
+ self.subscription = await self.js.subscribe(
440
+ subject=subject,
441
+ durable=durable_name,
442
+ cb=message_handler,
443
+ stream=stream_name
444
+ )
445
+ logger.info(f"Created and subscribed to new NATS consumer: {durable_name}")
446
+ else:
447
+ logger.error(f"Failed to subscribe: {e}")
448
+ raise
449
+
450
+ async def _monitor_connection(self):
451
+ while self.running:
452
+ try:
453
+ if not self.nc or self.nc.is_closed:
454
+ logger.warning("NATS connection not available, attempting to reconnect...")
455
+ await self._connect()
456
+ continue
457
+
458
+ if not await self._check_consumer_status():
459
+ logger.warning("Consumer not active, waiting before resubscribe...")
460
+ await asyncio.sleep(10)
461
+ logger.warning("Attempting to resubscribe...")
462
+ await self._subscribe_to_existing_consumer()
463
+
464
+ await asyncio.sleep(5)
465
+ except Exception as e:
466
+ logger.error(f"Connection monitoring error: {e}")
467
+ await asyncio.sleep(10)
468
+
469
+ async def _check_consumer_status(self):
470
+ if not self.js:
471
+ return False
472
+
473
+ try:
474
+ durable_name = f"{self.agent_name}-consumer"
475
+ await self.js.consumer_info("model-management-service", durable_name)
476
+ return True
477
+ except Exception as e:
478
+ logger.warning(f"Error checking consumer status: {e}")
479
+ return False
480
+
481
+ async def _close(self):
482
+ if self.subscription:
483
+ try:
484
+ await self.subscription.unsubscribe()
485
+ self.subscription = None
486
+ logger.info("NATS subscription unsubscribed")
487
+ except Exception as e:
488
+ logger.warning(f"Error unsubscribing: {e}")
489
+
490
+ if self.nc and not self.nc.is_closed:
491
+ try:
492
+ await self.nc.close()
493
+ logger.info("NATS connection closed")
494
+ except Exception as e:
495
+ logger.warning(f"Error closing NATS connection: {e}")
496
+
497
+ @staticmethod
498
+ async def _on_disconnected():
499
+ logger.warning("NATS disconnected")
500
+
501
+ async def _on_reconnected(self):
502
+ logger.info("NATS reconnected")
503
+ if self.running:
504
+ await asyncio.sleep(2)
505
+ try:
506
+ await self._subscribe_to_existing_consumer()
507
+ except Exception as e:
508
+ logger.error(f"Failed to resubscribe: {e}")
509
+
510
+ @staticmethod
511
+ async def _on_closed():
512
+ logger.info("NATS connection closed")
513
+
514
+ @staticmethod
515
+ async def _on_error(e):
516
+ logger.error(f"NATS error: {e}")
517
+
518
+ def stop(self):
519
+ self.running = False
520
+ asyncio.run_coroutine_threadsafe(self._close(), self.loop)
521
+ logger.info("NATS listener stopped")
522
+
523
+
524
+ def create_listener(agent_name: str, client_token: str, listener_type: ListenerType):
525
+ if listener_type == ListenerType.REDIS:
526
+ return RedisListener(agent_name, client_token)
527
+ elif listener_type == ListenerType.NATS:
528
+ return NATSListener(agent_name, client_token)
529
+ else:
530
+ raise ValueError(f"Unsupported listener type: {listener_type}")
File without changes
@@ -0,0 +1,122 @@
1
+ import logging
2
+ import logging.config
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Dict, Any
6
+ from datetime import datetime, timezone
7
+
8
+ _logging_config: Dict[str, Any] = {}
9
+ _current_log_level = logging.INFO
10
+
11
+
12
+ def setup_logging() -> Dict[str, Any]:
13
+ global _logging_config, _current_log_level
14
+
15
+ current_dir = Path(__file__).parent.absolute()
16
+ config_path = current_dir / "logging.json"
17
+
18
+ if not config_path.exists():
19
+ raise FileNotFoundError(f"Logging config not found at: {config_path}")
20
+
21
+ with open(config_path, 'r', encoding='utf-8') as f:
22
+ _logging_config = json.load(f)
23
+
24
+ _current_log_level = _logging_config.get('root', {}).get('level', logging.INFO)
25
+ if isinstance(_current_log_level, str):
26
+ _current_log_level = logging.getLevelName(_current_log_level.upper())
27
+
28
+ _apply_logging_config()
29
+ return _logging_config
30
+
31
+
32
+ def _apply_logging_config():
33
+ logging.config.dictConfig(_logging_config)
34
+
35
+ for logger_name in logging.root.manager.loggerDict:
36
+ logger = logging.getLogger(logger_name)
37
+ if not logger.handlers:
38
+ logger.setLevel(_current_log_level)
39
+
40
+
41
+ def set_log_level(level: str | int):
42
+ global _current_log_level, _logging_config
43
+
44
+ if isinstance(level, str):
45
+ level = level.upper()
46
+ try:
47
+ level = logging.getLevelName(level) if level.isdigit() else getattr(logging, level)
48
+ except AttributeError:
49
+ raise ValueError(f"Invalid log level: {level}")
50
+
51
+ if level not in (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL):
52
+ raise ValueError(f"Invalid log level value: {level}")
53
+
54
+ _current_log_level = level
55
+ if 'root' in _logging_config:
56
+ _logging_config['root']['level'] = logging.getLevelName(level)
57
+
58
+ _apply_logging_config()
59
+ logging.info(f"Log level changed to {logging.getLevelName(level)}")
60
+
61
+
62
+ def get_log_level() -> str:
63
+ return logging.getLevelName(_current_log_level)
64
+
65
+ class ColorFormatter(logging.Formatter):
66
+ COLORS = {
67
+ 'timestamp': '\x1b[38;5;244m', #浅灰色时间戳
68
+ 'debug': '\x1b[36m', # Cyan
69
+ 'info': '\x1b[32m', # Green
70
+ 'warning': '\x1b[33m', # Yellow
71
+ 'error': '\x1b[31m', # Red
72
+ 'critical': '\x1b[41;1m', # 红色背景
73
+ 'module': '\x1b[38;5;33m', # 亮蓝色
74
+ 'thread': '\x1b[38;5;33m', # 亮蓝色
75
+ 'reset': '\x1b[0m'
76
+ }
77
+
78
+ def format(self, record):
79
+ colors = {
80
+ 'timestamp': self.COLORS['timestamp'],
81
+ 'level': self.COLORS.get(record.levelname.lower(), ''),
82
+ 'module': self.COLORS['module'],
83
+ 'thread': self.COLORS['thread'],
84
+ 'reset': self.COLORS['reset']
85
+ }
86
+
87
+ timestamp = datetime.fromtimestamp(record.created, tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:23] + 'Z'
88
+
89
+ message = self._format_message(record)
90
+
91
+ log_template = (
92
+ "{timestamp_color}{timestamp}{reset} "
93
+ "[{level_color}{levelname:8}{reset}] "
94
+ "{message} "
95
+ "[{module_color}{module}{reset}] "
96
+ "thread_name={thread_color}{thread}{reset}"
97
+ )
98
+
99
+ return log_template.format(
100
+ timestamp_color=colors['timestamp'],
101
+ timestamp=timestamp,
102
+ reset=colors['reset'],
103
+ level_color=colors['level'],
104
+ levelname=record.levelname.lower(),
105
+ message=message,
106
+ module_color=colors['module'],
107
+ module=record.module,
108
+ thread_color=colors['thread'],
109
+ thread=record.threadName
110
+ )
111
+
112
+ @staticmethod
113
+ def _format_message(record):
114
+ try:
115
+ if record.args:
116
+ if isinstance(record.args, (tuple, dict)):
117
+ return record.msg % record.args
118
+ elif isinstance(record.args, dict):
119
+ return record.msg.format(**record.args)
120
+ except Exception:
121
+ pass
122
+ return record.msg
@@ -0,0 +1,50 @@
1
+ {
2
+ "version": 1,
3
+ "incremental": false,
4
+ "disable_existing_loggers": false,
5
+ "formatters": {
6
+ "standard": {
7
+ "()": "agent_api_server.log.formatters.ColorFormatter",
8
+ "format": "%(asctime)s.%(msecs)03dZ [%(levelname)-8s] %(message)s [%(name)s] thread=%(threadName)s",
9
+ "datefmt": "%Y-%m-%d %H:%M:%S"
10
+ }
11
+ },
12
+ "handlers": {
13
+ "console": {
14
+ "class": "logging.StreamHandler",
15
+ "formatter": "standard",
16
+ "stream": "ext://sys.stdout"
17
+ }
18
+ },
19
+ "root": {
20
+ "handlers": ["console"],
21
+ "level": "INFO"
22
+ },
23
+ "loggers": {
24
+ "fastmcp": {
25
+ "handlers": ["console"],
26
+ "level": "INFO",
27
+ "propagate": false
28
+ },
29
+ "FastMCP": {
30
+ "handlers": ["console"],
31
+ "level": "INFO",
32
+ "propagate": false
33
+ },
34
+ "FastMCP.server": {
35
+ "handlers": ["console"],
36
+ "level": "INFO",
37
+ "propagate": false
38
+ },
39
+ "uvicorn": {
40
+ "handlers": ["console"],
41
+ "level": "INFO",
42
+ "propagate": false
43
+ },
44
+ "uvicorn.error": {
45
+ "handlers": ["console"],
46
+ "level": "INFO",
47
+ "propagate": false
48
+ }
49
+ }
50
+ }