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.
- agent_api_server/__init__.py +0 -0
- agent_api_server/api/__init__.py +0 -0
- agent_api_server/api/v1/__init__.py +0 -0
- agent_api_server/api/v1/api.py +25 -0
- agent_api_server/api/v1/config.py +57 -0
- agent_api_server/api/v1/graph.py +59 -0
- agent_api_server/api/v1/schema.py +57 -0
- agent_api_server/api/v1/thread.py +563 -0
- agent_api_server/cache/__init__.py +0 -0
- agent_api_server/cache/redis_cache.py +385 -0
- agent_api_server/callback_handler.py +18 -0
- agent_api_server/client/css/styles.css +1202 -0
- agent_api_server/client/favicon.ico +0 -0
- agent_api_server/client/index.html +102 -0
- agent_api_server/client/js/app.js +1499 -0
- agent_api_server/client/js/index.umd.js +824 -0
- agent_api_server/config_center/config_center.py +239 -0
- agent_api_server/configs/__init__.py +3 -0
- agent_api_server/configs/config.py +163 -0
- agent_api_server/dynamic_llm/__init__.py +0 -0
- agent_api_server/dynamic_llm/dynamic_llm.py +331 -0
- agent_api_server/listener.py +530 -0
- agent_api_server/log/__init__.py +0 -0
- agent_api_server/log/formatters.py +122 -0
- agent_api_server/log/logging.json +50 -0
- agent_api_server/mcp_convert/__init__.py +0 -0
- agent_api_server/mcp_convert/mcp_convert.py +375 -0
- agent_api_server/memeory/__init__.py +0 -0
- agent_api_server/memeory/postgres.py +233 -0
- agent_api_server/register/__init__.py +0 -0
- agent_api_server/register/register.py +65 -0
- agent_api_server/service.py +354 -0
- agent_api_server/service_hub/service_hub.py +233 -0
- agent_api_server/service_hub/service_hub_test.py +700 -0
- agent_api_server/shared/__init__.py +0 -0
- agent_api_server/shared/ase.py +54 -0
- agent_api_server/shared/base_model.py +103 -0
- agent_api_server/shared/common.py +110 -0
- agent_api_server/shared/decode_token.py +107 -0
- agent_api_server/shared/detect_message.py +410 -0
- agent_api_server/shared/get_model_info.py +491 -0
- agent_api_server/shared/message.py +419 -0
- agent_api_server/shared/util_func.py +372 -0
- agent_api_server/sso_service/__init__.py +1 -0
- agent_api_server/sso_service/sdk/__init__.py +1 -0
- agent_api_server/sso_service/sdk/client.py +224 -0
- agent_api_server/sso_service/sdk/credential.py +11 -0
- agent_api_server/sso_service/sdk/encoding.py +22 -0
- agent_api_server/sso_service/sso_service.py +177 -0
- agent_api_server-2.1.7.dist-info/METADATA +130 -0
- agent_api_server-2.1.7.dist-info/RECORD +52 -0
- 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
|
+
}
|