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,385 @@
1
+ import os
2
+ import asyncio
3
+ import logging
4
+ import weakref
5
+ import pickle
6
+ import time
7
+ from typing import Dict, Any, Optional, List
8
+ from uuid import uuid4
9
+ from dataclasses import dataclass
10
+ import redis.asyncio as redis
11
+ from agent_api_server.configs import global_config
12
+ from redis.exceptions import RedisError, WatchError
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ @dataclass
17
+ class ThreadState:
18
+ thread_id: str
19
+ graph_name: str
20
+ status: str
21
+ created_at: float = time.time()
22
+ last_accessed: float = time.time()
23
+ metadata: Dict[str, Any] = None
24
+
25
+ class AsyncRedisThreadStorage:
26
+ _instances = weakref.WeakSet()
27
+ _shutdown_lock = asyncio.Lock()
28
+ _worker_instances = weakref.WeakValueDictionary()
29
+
30
+ def __init__(self, max_retries: int = 3, retry_delay: float = 1.0):
31
+ self.worker_pid = os.getpid()
32
+ logger.info(
33
+ f"Initializing Redis storage for worker {self.worker_pid} "
34
+ f"(max_retries={max_retries}, retry_delay={retry_delay:.1f}s)"
35
+ )
36
+
37
+ self.redis_url = self._get_redis_url()
38
+ self.redis: Optional[redis.Redis] = None
39
+ self.max_retries = max(max_retries, 1)
40
+ self.retry_delay = max(retry_delay, 0.1)
41
+ self._is_initialized = False
42
+ self._lock = asyncio.Lock()
43
+
44
+ AsyncRedisThreadStorage._instances.add(self)
45
+ AsyncRedisThreadStorage._worker_instances[self.worker_pid] = self
46
+ logger.debug(f"New Redis storage instance registered for worker {self.worker_pid}")
47
+
48
+ @classmethod
49
+ def get_worker_instance(cls) -> 'AsyncRedisThreadStorage':
50
+ """Get or create instance for current worker process"""
51
+ worker_pid = os.getpid()
52
+ if worker_pid not in cls._worker_instances:
53
+ instance = cls()
54
+ cls._worker_instances[worker_pid] = instance
55
+ logger.debug(f"Created new Redis storage instance for worker {worker_pid}")
56
+ return cls._worker_instances[worker_pid]
57
+
58
+ @classmethod
59
+ async def close_worker_instance(cls):
60
+ """Close instance for current worker process"""
61
+ worker_pid = os.getpid()
62
+ if worker_pid in cls._worker_instances:
63
+ instance = cls._worker_instances.pop(worker_pid)
64
+ await instance.close()
65
+
66
+ @staticmethod
67
+ def _get_redis_url() -> str:
68
+ """获取安全的Redis连接字符串(隐藏密码)"""
69
+ redis_url = global_config.REDIS_URL
70
+ safe_redis_url = redis_url.split('@')[0] + '@[REDACTED]' if '@' in redis_url else redis_url
71
+ logger.debug("Using Redis URL: %s", safe_redis_url)
72
+ return redis_url
73
+
74
+ @classmethod
75
+ async def close_all(cls):
76
+ logger.info("Initiating shutdown of all Redis storage instances")
77
+
78
+ async with cls._shutdown_lock:
79
+ instances = list(cls._instances)
80
+ if not instances:
81
+ logger.debug("No active Redis instances to close")
82
+ return
83
+
84
+ logger.debug("Closing %d active Redis instance(s)", len(instances))
85
+ for instance in instances:
86
+ try:
87
+ await instance.close()
88
+ logger.debug("Redis instance closed successfully")
89
+ except Exception as e:
90
+ logger.warning(
91
+ "Error closing Redis instance: %s",
92
+ str(e),
93
+ exc_info=logger.isEnabledFor(logging.DEBUG)
94
+ )
95
+
96
+ async def initialize(self) -> None:
97
+ """Initialize Redis connection"""
98
+ async with self._lock:
99
+ if self._is_initialized:
100
+ logger.debug("Redis already initialized, skipping")
101
+ return
102
+
103
+ logger.info("Starting Redis connection initialization")
104
+
105
+ try:
106
+ self.redis = redis.Redis.from_url(
107
+ self.redis_url,
108
+ )
109
+
110
+ await self.redis.ping()
111
+ self._is_initialized = True
112
+ logger.info("Redis storage initialized successfully")
113
+
114
+ except Exception as e:
115
+ logger.error("Redis initialization failed", exc_info=True)
116
+ await self._safe_close()
117
+ raise RuntimeError("Redis storage initialization failed") from e
118
+
119
+ @staticmethod
120
+ def _redis_key(thread_id: str) -> str:
121
+ return f"langgraph:thread:{thread_id}"
122
+
123
+ async def _save_to_redis(self, state: ThreadState) -> bool:
124
+ try:
125
+ key = self._redis_key(state.thread_id)
126
+ data = {
127
+ "graph_name": state.graph_name,
128
+ "status": state.status,
129
+ "created_at": state.created_at,
130
+ "last_accessed": state.last_accessed,
131
+ "metadata": pickle.dumps(state.metadata) if state.metadata else None,
132
+ "worker_pid": str(self.worker_pid)
133
+ }
134
+
135
+ async with self.redis.pipeline(transaction=True) as pipe:
136
+ await pipe.hset(key, mapping={k: v for k, v in data.items() if v is not None})
137
+ await pipe.execute()
138
+ return True
139
+ except RedisError as e:
140
+ logger.error(f"Failed to save thread {state.thread_id} to Redis: {str(e)}")
141
+ return False
142
+
143
+ async def _load_from_redis(self, thread_id: str) -> Optional[ThreadState]:
144
+ try:
145
+ key = self._redis_key(thread_id)
146
+ data = await self.redis.hgetall(key)
147
+ if not data:
148
+ return None
149
+
150
+ metadata = pickle.loads(data[b'metadata']) if data.get(b'metadata') else None
151
+
152
+ return ThreadState(
153
+ thread_id=thread_id,
154
+ graph_name=data[b'graph_name'].decode(),
155
+ status=data[b'status'].decode(),
156
+ created_at=float(data[b'created_at']),
157
+ last_accessed=float(data[b'last_accessed']),
158
+ metadata=metadata
159
+ )
160
+ except RedisError as e:
161
+ logger.error(f"Failed to load thread {thread_id} from Redis: {str(e)}")
162
+ return None
163
+ except Exception as e:
164
+ logger.error(f"Failed to deserialize thread {thread_id}: {str(e)}")
165
+ return None
166
+
167
+ async def create_thread_with_id(self, thread_id, graph_name: str) -> ThreadState:
168
+ async with self._lock:
169
+ state = ThreadState(
170
+ thread_id=thread_id,
171
+ graph_name=graph_name,
172
+ status="created"
173
+ )
174
+
175
+ if not await self._save_to_redis(state):
176
+ raise RuntimeError("Failed to persist thread state")
177
+
178
+ logger.debug(f"Created thread {thread_id} for graph {graph_name}")
179
+ return state
180
+
181
+ async def create_thread(self, graph_name: str) -> ThreadState:
182
+ async with self._lock:
183
+ thread_id = str(uuid4())
184
+ state = ThreadState(
185
+ thread_id=thread_id,
186
+ graph_name=graph_name,
187
+ status="created"
188
+ )
189
+
190
+ if not await self._save_to_redis(state):
191
+ raise RuntimeError("Failed to persist thread state")
192
+
193
+ logger.debug(f"Created thread {thread_id} for graph {graph_name}")
194
+ return state
195
+
196
+ async def get_thread(self, thread_id: str) -> Optional[ThreadState]:
197
+ state = await self._load_from_redis(thread_id)
198
+ if state:
199
+ # Update last_accessed time
200
+ state.last_accessed = time.time()
201
+ await self._save_to_redis(state)
202
+ return state
203
+ return None
204
+
205
+ async def update_thread(self, thread_id: str, **updates) -> bool:
206
+ async with self._lock:
207
+ state = await self._load_from_redis(thread_id)
208
+ if not state:
209
+ return False
210
+
211
+ for k, v in updates.items():
212
+ setattr(state, k, v)
213
+ state.last_accessed = time.time()
214
+
215
+ return await self._save_to_redis(state)
216
+
217
+ async def delete_thread(self, thread_id: str) -> bool:
218
+ """异步删除线程"""
219
+ async with self._lock:
220
+ try:
221
+ await self.redis.delete(self._redis_key(thread_id))
222
+ logger.debug(f"Deleted thread {thread_id}")
223
+ return True
224
+ except RedisError as e:
225
+ logger.error(f"Failed to delete thread {thread_id} from Redis: {str(e)}")
226
+ return False
227
+
228
+ async def _scan_redis_keys(self, pattern: str = "langgraph:thread:*") -> List[str]:
229
+ """异步扫描Redis键"""
230
+ try:
231
+ cursor = '0'
232
+ keys = []
233
+ while cursor != 0:
234
+ cursor, partial_keys = await self.redis.scan(
235
+ cursor=cursor,
236
+ match=pattern,
237
+ count=1000
238
+ )
239
+ keys.extend(partial_keys)
240
+ return [key.decode() for key in keys]
241
+ except RedisError as e:
242
+ logger.error(f"Failed to scan Redis keys: {str(e)}")
243
+ return []
244
+
245
+ async def list_threads(self) -> Dict[str, ThreadState]:
246
+ """异步列出线程"""
247
+ result = {}
248
+ keys = await self._scan_redis_keys()
249
+
250
+ for key in keys:
251
+ thread_id = key.split(":")[-1]
252
+ state = await self._load_from_redis(thread_id)
253
+ if state:
254
+ result[thread_id] = state
255
+
256
+ return result
257
+
258
+ async def get_stats(self) -> Dict[str, Any]:
259
+ """异步获取统计信息"""
260
+ try:
261
+ redis_count = len(await self._scan_redis_keys())
262
+ except RedisError:
263
+ redis_count = -1
264
+
265
+ return {
266
+ "redis_thread_count": redis_count,
267
+ }
268
+
269
+ async def _safe_close(self) -> None:
270
+ logger.debug("Starting Redis safe cleanup")
271
+ if self.redis:
272
+ try:
273
+ await self.redis.close()
274
+ except Exception as e:
275
+ logger.warning(f"Error closing Redis connection: {str(e)}", exc_info=True)
276
+
277
+ self.redis = None
278
+ self._is_initialized = False
279
+ logger.info("Redis resources cleaned up")
280
+
281
+ async def close(self) -> None:
282
+ async with self._lock:
283
+ if not self._is_initialized:
284
+ logger.debug("Redis already closed, skipping")
285
+ return
286
+
287
+ logger.info("Starting Redis graceful shutdown")
288
+
289
+ try:
290
+ await self._safe_close()
291
+ except Exception as e:
292
+ logger.error(
293
+ "Error during Redis shutdown: %s",
294
+ str(e),
295
+ exc_info=logger.isEnabledFor(logging.DEBUG)
296
+ )
297
+ finally:
298
+ AsyncRedisThreadStorage._instances.discard(self)
299
+ logger.debug("Redis instance unregistered")
300
+
301
+ async def __aenter__(self) -> 'AsyncRedisThreadStorage':
302
+ logger.debug("Entering Redis context manager")
303
+ await self.initialize()
304
+ return self
305
+
306
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
307
+ logger.debug("Exiting Redis context manager")
308
+ await self.close()
309
+
310
+ @property
311
+ def is_initialized(self):
312
+ return self._is_initialized
313
+
314
+ async def clear_stop_flag(self, thread_id: str) -> bool:
315
+ """Clear the stop flag for a thread.
316
+
317
+ Args:
318
+ thread_id: The ID of the thread to clear the stop flag for
319
+
320
+ Returns:
321
+ bool: True if the flag was cleared successfully, False otherwise
322
+ """
323
+ key = self._redis_key(thread_id)
324
+ try:
325
+ async with self.redis.pipeline() as pipe:
326
+ while True:
327
+ try:
328
+ await pipe.watch(key)
329
+ existing = await pipe.exists(key)
330
+ if not existing:
331
+ await pipe.unwatch()
332
+ return False
333
+
334
+ pipe.multi()
335
+ await pipe.hdel(key, "stop_requested")
336
+ await pipe.hdel(key, "stop_requested_at")
337
+ await pipe.hset(key, "status", "complete")
338
+ await pipe.execute()
339
+ return True
340
+ except WatchError:
341
+ continue
342
+ finally:
343
+ await pipe.unwatch()
344
+ except RedisError as e:
345
+ logger.error(f"Failed to clear stop flag for thread {thread_id}: {str(e)}")
346
+ return False
347
+
348
+ async def set_stop_flag(self, thread_id: str) -> bool:
349
+ key = self._redis_key(thread_id)
350
+ try:
351
+ async with self.redis.pipeline() as pipe:
352
+ while True:
353
+ try:
354
+ await pipe.watch(key)
355
+ existing = await pipe.hget(key, "status")
356
+ if existing is None:
357
+ await pipe.unwatch()
358
+ return False
359
+
360
+ pipe.multi()
361
+ await pipe.hset(key, "status", "stopping")
362
+ await pipe.hset(key, "stop_requested", "1")
363
+ await pipe.hset(key, "stop_requested_at", str(time.time()))
364
+ await pipe.execute()
365
+ return True
366
+ except WatchError:
367
+ continue
368
+ finally:
369
+ await pipe.unwatch()
370
+ except RedisError as e:
371
+ logger.error(f"Failed to set stop flag: {str(e)}")
372
+ return False
373
+
374
+ async def should_stop(self, thread_id: str) -> bool:
375
+ """检查是否需要停止"""
376
+ key = self._redis_key(thread_id)
377
+ try:
378
+ async with self.redis.pipeline() as pipe:
379
+ await pipe.hexists(key, "stop_requested")
380
+ await pipe.hget(key, "status")
381
+ exists, status = await pipe.execute()
382
+ return bool(exists) and status != b"stopped"
383
+ except RedisError as e:
384
+ logger.error(f"Failed to check stop flag: {str(e)}")
385
+ return False
@@ -0,0 +1,18 @@
1
+
2
+ _callbacks = []
3
+
4
+ def register_callback(callback):
5
+ """
6
+ 注册一个回调函数
7
+ """
8
+ _callbacks.append(callback)
9
+
10
+ def notify_callbacks(**kwargs):
11
+ """
12
+ 当模型配置更新时,通知所有注册的回调
13
+ """
14
+ for callback in _callbacks:
15
+ try:
16
+ callback(**kwargs)
17
+ except Exception as e:
18
+ print(f"[ERROR] Callback execution failed: {e}")