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,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}")
|