agent-api-server 2.2.1__tar.gz → 2.2.1a0__tar.gz
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-2.2.1 → agent_api_server-2.2.1a0}/PKG-INFO +2 -1
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/thread.py +8 -1
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/configs/config.py +13 -6
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/dynamic_llm/dynamic_llm.py +58 -7
- agent_api_server-2.2.1a0/agent_api_server/observability/__init__.py +3 -0
- agent_api_server-2.2.1a0/agent_api_server/observability/langfuse.py +84 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/service.py +78 -28
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/get_model_info.py +46 -4
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/message.py +29 -4
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sso_service.py +8 -1
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/pyproject.toml +2 -1
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/README.md +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/api.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/config.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/graph.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/schema.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/cache/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/cache/redis_cache.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/callback_handler.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/css/styles.css +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/favicon.ico +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/index.html +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/js/app.js +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/js/index.umd.js +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/config_center/config_center.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/configs/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/dynamic_llm/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/dynamic_llm/model.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/listener.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/log/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/log/formatters.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/log/logging.json +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_convert/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_convert/mcp_convert.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_interceptor/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_interceptor/mcp_intecerpter.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/memeory/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/memeory/postgres.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/middleware/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/middleware/model.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/middleware/schema.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/register/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/register/register.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/schema/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/schema/context.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/service_hub/service_hub.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/service_hub/service_hub_test.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/ase.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/base_model.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/common.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/detect_message.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/util_func.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/__init__.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/client.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/credential.py +0 -0
- {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/encoding.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: agent-api-server
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1a0
|
|
4
4
|
Summary: A Langgraph agent API server that implements Langgraph agent's web capabilities and can interact with chatbot
|
|
5
5
|
Keywords: fastapi,langgraph,agent,api-server
|
|
6
6
|
Author: Zijie Zhang
|
|
@@ -21,6 +21,7 @@ Requires-Dist: fastmcp (>=2.13.0,<3.0.0)
|
|
|
21
21
|
Requires-Dist: langchain (>=1.2.0,<2.0.0)
|
|
22
22
|
Requires-Dist: langchain-core (>=1.2.5,<2.0.0)
|
|
23
23
|
Requires-Dist: langchain-mcp-adapters (>=0.2.1,<0.3.0)
|
|
24
|
+
Requires-Dist: langfuse (>=4.3.1,<5.0.0)
|
|
24
25
|
Requires-Dist: langgraph (>=1.0.6,<2.0.0)
|
|
25
26
|
Requires-Dist: langgraph-checkpoint (>=4.0.0,<5.0.0)
|
|
26
27
|
Requires-Dist: langgraph-checkpoint-postgres (>=3.0.3,<4.0.0)
|
|
@@ -21,6 +21,7 @@ from agent_api_server.shared.message import (
|
|
|
21
21
|
message_generator,
|
|
22
22
|
langchain_to_chat_message
|
|
23
23
|
)
|
|
24
|
+
from agent_api_server.observability import with_langfuse_config
|
|
24
25
|
|
|
25
26
|
logger = logging.getLogger(__name__)
|
|
26
27
|
api_router = APIRouter()
|
|
@@ -99,7 +100,13 @@ def _get_run_config(thread_id: str, ts_tenant: str, ei_token: str, graph_name: s
|
|
|
99
100
|
config["configurable"]["TSTenant"] = ts_tenant
|
|
100
101
|
config["configurable"]["EIToken"] = ei_token
|
|
101
102
|
config["configurable"]["files"] = files or []
|
|
102
|
-
return
|
|
103
|
+
return with_langfuse_config(
|
|
104
|
+
config,
|
|
105
|
+
thread_id=thread_id,
|
|
106
|
+
graph_name=graph_name,
|
|
107
|
+
ts_tenant=ts_tenant,
|
|
108
|
+
files=files,
|
|
109
|
+
)
|
|
103
110
|
|
|
104
111
|
|
|
105
112
|
async def _check_stop_flag(thread_id: str, storage: AsyncRedisThreadStorage):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
-
from pydantic import Field
|
|
2
|
+
from pydantic import Field, PrivateAttr
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Optional
|
|
5
5
|
from dotenv import load_dotenv
|
|
@@ -7,11 +7,19 @@ from dotenv import load_dotenv
|
|
|
7
7
|
|
|
8
8
|
class SSOConfig(BaseSettings):
|
|
9
9
|
SSO_URL: str = Field(
|
|
10
|
-
default="
|
|
10
|
+
default="", description="The URL of SSO service. Leave empty to disable SSO integration."
|
|
11
11
|
)
|
|
12
12
|
CLIENT_ID: str = Field(default="", description="The client-id of server")
|
|
13
13
|
CLIENT_SECRET: str = Field(default="", description="The client secret of server")
|
|
14
|
-
|
|
14
|
+
_client_token: str = PrivateAttr(default="")
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def CLIENT_TOKEN(self) -> str:
|
|
18
|
+
return self._client_token
|
|
19
|
+
|
|
20
|
+
@CLIENT_TOKEN.setter
|
|
21
|
+
def CLIENT_TOKEN(self, value: str) -> None:
|
|
22
|
+
self._client_token = value or ""
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
class CacheConfig(BaseSettings):
|
|
@@ -63,7 +71,7 @@ class LoggingConfig(BaseSettings):
|
|
|
63
71
|
|
|
64
72
|
class ModelManagerServiceConfig(BaseSettings):
|
|
65
73
|
MODEL_MANAGER_SERVICE_URL: str = Field(
|
|
66
|
-
default="
|
|
74
|
+
default="", description="The URL of model manager service url. Leave empty to disable Model Management integration."
|
|
67
75
|
)
|
|
68
76
|
|
|
69
77
|
SERVICE_EXTERNAL_URL: str = Field(
|
|
@@ -71,7 +79,7 @@ class ModelManagerServiceConfig(BaseSettings):
|
|
|
71
79
|
)
|
|
72
80
|
|
|
73
81
|
MODEL_MANAGER_REDIS_URL: str = Field(
|
|
74
|
-
default="
|
|
82
|
+
default="", description="Redis server URL which used by model_manager, format is: redis://host:port/db"
|
|
75
83
|
)
|
|
76
84
|
|
|
77
85
|
MODEL_MANAGER_NATS_URL: str = Field(
|
|
@@ -160,4 +168,3 @@ class Config(PostgresConfig, CacheConfig, LoggingConfig, SSOConfig, WebServerCon
|
|
|
160
168
|
env_file_encoding="utf-8",
|
|
161
169
|
extra="ignore",
|
|
162
170
|
)
|
|
163
|
-
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/dynamic_llm/dynamic_llm.py
RENAMED
|
@@ -24,6 +24,20 @@ from agent_api_server.shared.util_func import set_model_config, get_env
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def _has_model_management_config() -> bool:
|
|
28
|
+
return bool(global_config.MODEL_MANAGER_SERVICE_URL and global_config.CLIENT_TOKEN)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _dedupe(values: List[str]) -> List[str]:
|
|
32
|
+
seen = set()
|
|
33
|
+
result = []
|
|
34
|
+
for value in values:
|
|
35
|
+
if value not in seen:
|
|
36
|
+
seen.add(value)
|
|
37
|
+
result.append(value)
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
27
41
|
class DynamicLLM(Runnable):
|
|
28
42
|
_instance_lock = threading.Lock()
|
|
29
43
|
_llm_cache: Dict[str, ModelInstance] = {}
|
|
@@ -169,12 +183,17 @@ class DynamicLLM(Runnable):
|
|
|
169
183
|
@contextmanager
|
|
170
184
|
def _get_model_instance(self, llm_config: Dict[str, Any], config: RunnableConfig):
|
|
171
185
|
if self.use_sys_llm:
|
|
172
|
-
logger.info(f"use_sys_llm is {self.use_sys_llm}, so call model with system llm configuration
|
|
186
|
+
logger.info(f"use_sys_llm is {self.use_sys_llm}, so call model with system llm configuration")
|
|
187
|
+
if not _has_model_management_config():
|
|
188
|
+
raise ValueError(
|
|
189
|
+
"System LLM mode requires Model Management configuration. "
|
|
190
|
+
"Configure SSO_URL and MODEL_MANAGER_SERVICE_URL, or disable use_sys_llm."
|
|
191
|
+
)
|
|
173
192
|
|
|
174
193
|
self._model = ModelInstanceFactory.get_model_instance(
|
|
175
|
-
model_managment_url=
|
|
194
|
+
model_managment_url=global_config.MODEL_MANAGER_SERVICE_URL,
|
|
176
195
|
config_type=self.config_type,
|
|
177
|
-
token=
|
|
196
|
+
token=global_config.CLIENT_TOKEN,
|
|
178
197
|
app_id=llm_config.get('agent_id'),
|
|
179
198
|
app_name=llm_config.get('agent_id'),
|
|
180
199
|
)
|
|
@@ -188,6 +207,12 @@ class DynamicLLM(Runnable):
|
|
|
188
207
|
f"Local cache does not contain LLM credentials for tenant '{ts_id}'. "
|
|
189
208
|
f"Retrieve the credentials from Model Management and configure them in the system.")
|
|
190
209
|
|
|
210
|
+
if not _has_model_management_config():
|
|
211
|
+
raise ValueError(
|
|
212
|
+
"LLM credentials are missing and Model Management is not configured. "
|
|
213
|
+
"Provide model credentials in the runtime config, or configure SSO_URL and MODEL_MANAGER_SERVICE_URL."
|
|
214
|
+
)
|
|
215
|
+
|
|
191
216
|
m_client = ModelManageClient(global_config.MODEL_MANAGER_SERVICE_URL, global_config.CLIENT_TOKEN)
|
|
192
217
|
model_info = m_client.get_agent_models(agent_id, ts_id)
|
|
193
218
|
|
|
@@ -284,15 +309,38 @@ class DynamicLLM(Runnable):
|
|
|
284
309
|
self.use_sys_llm = True if value == "true" else False
|
|
285
310
|
|
|
286
311
|
config = {
|
|
287
|
-
"provider":
|
|
288
|
-
"model":
|
|
289
|
-
"credentials":
|
|
312
|
+
"provider": self._get_config_value(configurable, "PROVIDER", ts_tenant),
|
|
313
|
+
"model": self._get_config_value(configurable, "MODEL", ts_tenant),
|
|
314
|
+
"credentials": self._get_config_value(configurable, "CREDENTIALS", ts_tenant),
|
|
290
315
|
"agent_id": graph_name,
|
|
291
316
|
"ts_tenant": ts_tenant
|
|
292
317
|
}
|
|
293
318
|
|
|
294
319
|
return config
|
|
295
320
|
|
|
321
|
+
def _get_config_value(self, configurable: Dict[str, Any], field: str, ts_tenant: Optional[str] = None) -> Any:
|
|
322
|
+
for key in self._get_config_key_candidates(field, ts_tenant):
|
|
323
|
+
value = configurable.get(key)
|
|
324
|
+
if value not in (None, ""):
|
|
325
|
+
return value
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
def _get_config_key_candidates(self, field: str, ts_tenant: Optional[str] = None) -> List[str]:
|
|
329
|
+
keys = []
|
|
330
|
+
|
|
331
|
+
if ts_tenant:
|
|
332
|
+
keys.append(self._get_config_key(field, ts_tenant))
|
|
333
|
+
|
|
334
|
+
keys.append(self._get_config_key(field))
|
|
335
|
+
|
|
336
|
+
if self.tool_name != "default":
|
|
337
|
+
default_key = "_".join([self.config_type.value, field])
|
|
338
|
+
if ts_tenant:
|
|
339
|
+
keys.append(f"{default_key}_{ts_tenant}")
|
|
340
|
+
keys.append(default_key)
|
|
341
|
+
|
|
342
|
+
return _dedupe(keys)
|
|
343
|
+
|
|
296
344
|
def _get_config_key(self, field: str, ts_tenant: Optional[str] = None) -> str:
|
|
297
345
|
parts = [self.tool_name.upper(), self.config_type.value, field] if self.tool_name != "default" else [
|
|
298
346
|
self.config_type.value, field]
|
|
@@ -311,6 +359,9 @@ class DynamicLLM(Runnable):
|
|
|
311
359
|
if config is None:
|
|
312
360
|
return True
|
|
313
361
|
|
|
362
|
+
if config.get('provider') == 'ollama':
|
|
363
|
+
return False
|
|
364
|
+
|
|
314
365
|
credentials = config.get('credentials')
|
|
315
366
|
if not credentials:
|
|
316
367
|
return True
|
|
@@ -328,4 +379,4 @@ class DynamicLLM(Runnable):
|
|
|
328
379
|
return len(cls._llm_cache)
|
|
329
380
|
|
|
330
381
|
def __repr__(self) -> str:
|
|
331
|
-
return f"DynamicLLM(tool_name={self.tool_name}, config_type={self.config_type})"
|
|
382
|
+
return f"DynamicLLM(tool_name={self.tool_name}, config_type={self.config_type})"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
FALSE_VALUES = {"0", "false", "no", "off"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_langfuse_enabled() -> bool:
|
|
11
|
+
tracing_enabled = os.getenv("LANGFUSE_TRACING_ENABLED", "true").strip().lower()
|
|
12
|
+
if tracing_enabled in FALSE_VALUES:
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
return bool(os.getenv("LANGFUSE_PUBLIC_KEY") and os.getenv("LANGFUSE_SECRET_KEY"))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _merge_unique(values: List[str]) -> List[str]:
|
|
19
|
+
seen = set()
|
|
20
|
+
merged = []
|
|
21
|
+
for value in values:
|
|
22
|
+
if value and value not in seen:
|
|
23
|
+
seen.add(value)
|
|
24
|
+
merged.append(value)
|
|
25
|
+
return merged
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _new_langfuse_callback_handler():
|
|
29
|
+
from langfuse.langchain import CallbackHandler
|
|
30
|
+
|
|
31
|
+
return CallbackHandler()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def with_langfuse_config(
|
|
35
|
+
config: Dict[str, Any],
|
|
36
|
+
*,
|
|
37
|
+
thread_id: str,
|
|
38
|
+
graph_name: str,
|
|
39
|
+
ts_tenant: Optional[str],
|
|
40
|
+
files: Optional[List[Dict[str, Any]]] = None,
|
|
41
|
+
) -> Dict[str, Any]:
|
|
42
|
+
if not is_langfuse_enabled():
|
|
43
|
+
return config
|
|
44
|
+
|
|
45
|
+
configured = dict(config)
|
|
46
|
+
configured["configurable"] = dict(config.get("configurable") or {})
|
|
47
|
+
|
|
48
|
+
tags = _merge_unique(list(configured.get("tags") or []) + ["agent-api-server", graph_name])
|
|
49
|
+
metadata = dict(configured.get("metadata") or {})
|
|
50
|
+
metadata.update({
|
|
51
|
+
"langfuse_user_id": ts_tenant or "anonymous",
|
|
52
|
+
"langfuse_session_id": thread_id,
|
|
53
|
+
"langfuse_trace_name": f"{graph_name}:{thread_id}",
|
|
54
|
+
"langfuse_tags": tags,
|
|
55
|
+
"thread_id": thread_id,
|
|
56
|
+
"graph_name": graph_name,
|
|
57
|
+
"tenant": ts_tenant,
|
|
58
|
+
"file_count": len(files or []),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
callbacks = list(configured.get("callbacks") or [])
|
|
62
|
+
try:
|
|
63
|
+
callbacks.append(_new_langfuse_callback_handler())
|
|
64
|
+
except Exception:
|
|
65
|
+
logger.exception("Failed to initialize Langfuse callback handler")
|
|
66
|
+
return config
|
|
67
|
+
|
|
68
|
+
configured["callbacks"] = callbacks
|
|
69
|
+
configured["metadata"] = metadata
|
|
70
|
+
configured["tags"] = tags
|
|
71
|
+
configured["run_name"] = configured.get("run_name") or graph_name
|
|
72
|
+
return configured
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def shutdown_langfuse() -> None:
|
|
76
|
+
if not is_langfuse_enabled():
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
from langfuse import get_client
|
|
81
|
+
|
|
82
|
+
get_client().shutdown()
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.exception("Failed to shutdown Langfuse client")
|
|
@@ -10,7 +10,7 @@ from fastapi import HTTPException
|
|
|
10
10
|
from fastapi import FastAPI
|
|
11
11
|
from agent_api_server.api.v1.api import api_router
|
|
12
12
|
from pydantic_settings import BaseSettings
|
|
13
|
-
from typing import List, Any
|
|
13
|
+
from typing import List, Any, Optional
|
|
14
14
|
from fastapi.staticfiles import StaticFiles
|
|
15
15
|
from fastapi.middleware.cors import CORSMiddleware
|
|
16
16
|
from pathlib import Path
|
|
@@ -27,6 +27,7 @@ from agent_api_server.cache.redis_cache import AsyncRedisThreadStorage
|
|
|
27
27
|
from agent_api_server.memeory.postgres import AsyncPostgresCheckpointer
|
|
28
28
|
from contextlib import asynccontextmanager
|
|
29
29
|
from agent_api_server.shared.util_func import parse_agent_config
|
|
30
|
+
from agent_api_server.observability import shutdown_langfuse
|
|
30
31
|
|
|
31
32
|
WELCOME_ART = """
|
|
32
33
|
╦ ┌─┐┌┐┌┌─┐╔═╗┬─┐┌─┐┌─┐┬ ┬
|
|
@@ -73,6 +74,9 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
|
73
74
|
|
|
74
75
|
await AsyncRedisThreadStorage.close_worker_instance()
|
|
75
76
|
logger.info(f"Redis connection pool closed in worker {os.getpid()}")
|
|
77
|
+
|
|
78
|
+
shutdown_langfuse()
|
|
79
|
+
logger.info(f"Langfuse client shutdown completed in worker {os.getpid()}")
|
|
76
80
|
except Exception as e:
|
|
77
81
|
logger.error(f"Error closing connection pool in worker {os.getpid()}, error message is {str(e)}")
|
|
78
82
|
|
|
@@ -127,6 +131,44 @@ def create_fastapi_app() -> FastAPI:
|
|
|
127
131
|
return app
|
|
128
132
|
|
|
129
133
|
|
|
134
|
+
def _configured(value: Any) -> bool:
|
|
135
|
+
return bool(str(value or "").strip())
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _ensure_client_token_from_sso() -> bool:
|
|
139
|
+
if _configured(global_config.CLIENT_TOKEN):
|
|
140
|
+
logger.info("Using existing runtime client token; skip SSO client token generation")
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
if not _configured(global_config.SSO_URL):
|
|
144
|
+
logger.warning("SSO_URL is not configured; skip SSO client token generation")
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
sso_service = SSOService(sso_config=SSOConfig(
|
|
148
|
+
sso_address=global_config.SSO_URL,
|
|
149
|
+
client_id=global_config.CLIENT_ID,
|
|
150
|
+
client_secret=global_config.CLIENT_SECRET,
|
|
151
|
+
))
|
|
152
|
+
client_id, client_secret = sso_service.get_client_id_and_secret()
|
|
153
|
+
logger.debug(f"Client ID: {client_id}, Client Secret: {client_secret}")
|
|
154
|
+
|
|
155
|
+
global_config.CLIENT_TOKEN = sso_service.generate_client_token(client_id, client_secret)
|
|
156
|
+
logger.info("Generated runtime client token from SSO")
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _has_model_management_service() -> bool:
|
|
161
|
+
return _configured(global_config.MODEL_MANAGER_SERVICE_URL)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _determine_listener_type_optional() -> Optional[ListenerType]:
|
|
165
|
+
if hasattr(global_config, 'MODEL_MANAGER_NATS_URL') and global_config.MODEL_MANAGER_NATS_URL:
|
|
166
|
+
return ListenerType.NATS
|
|
167
|
+
if hasattr(global_config, 'MODEL_MANAGER_REDIS_URL') and global_config.MODEL_MANAGER_REDIS_URL:
|
|
168
|
+
return ListenerType.REDIS
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
130
172
|
async def init_fastapi_server():
|
|
131
173
|
print_welcome(host=settings.host, port=settings.port)
|
|
132
174
|
|
|
@@ -242,19 +284,9 @@ async def init_fastapi_server():
|
|
|
242
284
|
await checkpointer.close_worker_instance()
|
|
243
285
|
|
|
244
286
|
try:
|
|
245
|
-
|
|
246
|
-
sso_address=global_config.SSO_URL,
|
|
247
|
-
client_id=global_config.CLIENT_ID,
|
|
248
|
-
client_secret=global_config.CLIENT_SECRET,
|
|
249
|
-
))
|
|
250
|
-
client_id, client_secret = sso_service.get_client_id_and_secret()
|
|
251
|
-
logger.debug(f"Client ID: {client_id}, Client Secret: {client_secret}")
|
|
252
|
-
|
|
253
|
-
global_config.CLIENT_TOKEN = sso_service.generate_client_token(client_id, client_secret)
|
|
254
|
-
os.environ['CLIENT_TOKEN'] = global_config.CLIENT_TOKEN
|
|
255
|
-
logger.info(f"Generated Client Token: {os.environ['CLIENT_TOKEN']}")
|
|
287
|
+
has_client_token = _ensure_client_token_from_sso()
|
|
256
288
|
except Exception as e:
|
|
257
|
-
logger.error(f"Failed
|
|
289
|
+
logger.error(f"Failed to prepare client token from SSO: {e}")
|
|
258
290
|
raise
|
|
259
291
|
|
|
260
292
|
if not global_config.AGENT_AUTO_REGISTRATION:
|
|
@@ -267,6 +299,22 @@ async def init_fastapi_server():
|
|
|
267
299
|
)
|
|
268
300
|
return
|
|
269
301
|
|
|
302
|
+
if not has_client_token:
|
|
303
|
+
logger.warning(
|
|
304
|
+
"Agent auto-registration is enabled but runtime client token is unavailable and SSO_URL is not configured.\n"
|
|
305
|
+
"Skip automatic registration and model configuration listener startup.\n"
|
|
306
|
+
"Configure SSO_URL to enable these platform integrations.\n"
|
|
307
|
+
)
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
if not _has_model_management_service():
|
|
311
|
+
logger.warning(
|
|
312
|
+
"Agent auto-registration is enabled but MODEL_MANAGER_SERVICE_URL is not configured.\n"
|
|
313
|
+
"Skip automatic registration and model configuration listener startup.\n"
|
|
314
|
+
"Set MODEL_MANAGER_SERVICE_URL to enable Model Management integration.\n"
|
|
315
|
+
)
|
|
316
|
+
return
|
|
317
|
+
|
|
270
318
|
try:
|
|
271
319
|
for agent in agents:
|
|
272
320
|
agent["agent_url"] = global_config.SERVICE_EXTERNAL_URL
|
|
@@ -277,12 +325,15 @@ async def init_fastapi_server():
|
|
|
277
325
|
)
|
|
278
326
|
await agent_registry.register_all(agents=agents)
|
|
279
327
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
logger.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
328
|
+
listener_type = _determine_listener_type_optional()
|
|
329
|
+
if listener_type is None:
|
|
330
|
+
logger.warning(
|
|
331
|
+
"MODEL_MANAGER_REDIS_URL and MODEL_MANAGER_NATS_URL are not configured. "
|
|
332
|
+
"Skip model configuration listener startup."
|
|
333
|
+
)
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
logger.info(f"Using {listener_type.name} listener based on configuration")
|
|
286
337
|
|
|
287
338
|
for agent in agents:
|
|
288
339
|
listener = create_listener(
|
|
@@ -312,15 +363,14 @@ async def init_fastapi_server():
|
|
|
312
363
|
|
|
313
364
|
|
|
314
365
|
def determine_listener_type() -> ListenerType:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
)
|
|
366
|
+
listener_type = _determine_listener_type_optional()
|
|
367
|
+
if listener_type is not None:
|
|
368
|
+
return listener_type
|
|
369
|
+
|
|
370
|
+
raise ValueError(
|
|
371
|
+
"No valid message broker configured. "
|
|
372
|
+
"Please set either MODEL_MANAGER_NATS_URL or MODEL_MANAGER_REDIS_URL"
|
|
373
|
+
)
|
|
324
374
|
|
|
325
375
|
def run_mcp_server(log_cfg):
|
|
326
376
|
try:
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/get_model_info.py
RENAMED
|
@@ -79,6 +79,29 @@ class ConfigKeyBuilder:
|
|
|
79
79
|
|
|
80
80
|
return "_".join(parts)
|
|
81
81
|
|
|
82
|
+
@staticmethod
|
|
83
|
+
def get_candidates(tool_name: str, field: str, ts_tenant: Optional[str] = None) -> list[str]:
|
|
84
|
+
keys = []
|
|
85
|
+
|
|
86
|
+
if ts_tenant:
|
|
87
|
+
keys.append(ConfigKeyBuilder.get_key(tool_name, field, ts_tenant))
|
|
88
|
+
|
|
89
|
+
keys.append(ConfigKeyBuilder.get_key(tool_name, field))
|
|
90
|
+
|
|
91
|
+
if tool_name.lower() != "default":
|
|
92
|
+
default_key = ConfigKeyBuilder.get_key("default", field)
|
|
93
|
+
if ts_tenant:
|
|
94
|
+
keys.append(f"{default_key}_{ts_tenant}")
|
|
95
|
+
keys.append(default_key)
|
|
96
|
+
|
|
97
|
+
seen = set()
|
|
98
|
+
result = []
|
|
99
|
+
for key in keys:
|
|
100
|
+
if key not in seen:
|
|
101
|
+
seen.add(key)
|
|
102
|
+
result.append(key)
|
|
103
|
+
return result
|
|
104
|
+
|
|
82
105
|
|
|
83
106
|
class CredentialsParser:
|
|
84
107
|
"""Parses credential data from various formats."""
|
|
@@ -178,13 +201,26 @@ class LLMConfigManager:
|
|
|
178
201
|
graph_name = configurable.get("graph_name")
|
|
179
202
|
|
|
180
203
|
return LLMConfig(
|
|
181
|
-
provider=
|
|
182
|
-
model=
|
|
183
|
-
credentials=
|
|
204
|
+
provider=self._get_config_value(configurable, tool_name, "PROVIDER", ts_tenant),
|
|
205
|
+
model=self._get_config_value(configurable, tool_name, "MODEL", ts_tenant),
|
|
206
|
+
credentials=self._get_config_value(configurable, tool_name, "CREDENTIALS", ts_tenant),
|
|
184
207
|
agent_id=graph_name,
|
|
185
208
|
ts_tenant=ts_tenant
|
|
186
209
|
)
|
|
187
210
|
|
|
211
|
+
def _get_config_value(
|
|
212
|
+
self,
|
|
213
|
+
configurable: Dict[str, Any],
|
|
214
|
+
tool_name: str,
|
|
215
|
+
field: str,
|
|
216
|
+
ts_tenant: Optional[str] = None
|
|
217
|
+
) -> Optional[Any]:
|
|
218
|
+
for key in self.key_builder.get_candidates(tool_name, field, ts_tenant):
|
|
219
|
+
value = configurable.get(key)
|
|
220
|
+
if value not in (None, ""):
|
|
221
|
+
return value
|
|
222
|
+
return None
|
|
223
|
+
|
|
188
224
|
def get_api_version(self, tool_name: str, config: Optional[RunnableConfig]) -> str:
|
|
189
225
|
if not config:
|
|
190
226
|
logger.debug(f"No config provided for tool: {tool_name}")
|
|
@@ -422,6 +458,12 @@ def get_chat_model_information(tool_name: str, config: Optional[RunnableConfig])
|
|
|
422
458
|
use_sys_llm = configurable.get('use_sys_llm')
|
|
423
459
|
|
|
424
460
|
if use_sys_llm == 'true':
|
|
461
|
+
if not global_config.MODEL_MANAGER_SERVICE_URL or not global_config.CLIENT_TOKEN:
|
|
462
|
+
raise ValueError(
|
|
463
|
+
"System LLM mode requires Model Management configuration. "
|
|
464
|
+
"Configure SSO_URL and MODEL_MANAGER_SERVICE_URL, or disable use_sys_llm."
|
|
465
|
+
)
|
|
466
|
+
|
|
425
467
|
from llm_sdk.llm import ModelInstanceFactory
|
|
426
468
|
_model = ModelInstanceFactory.get_model_instance(
|
|
427
469
|
model_managment_url=global_config.MODEL_MANAGER_SERVICE_URL,
|
|
@@ -488,4 +530,4 @@ def get_chat_llm_model_base_model_name(tool_name: str, config: Optional[Runnable
|
|
|
488
530
|
if provider == 'azure_openai':
|
|
489
531
|
return config_manager.get_base_model_name(tool_name, config)
|
|
490
532
|
else:
|
|
491
|
-
return ''
|
|
533
|
+
return ''
|
|
@@ -17,6 +17,7 @@ from agent_api_server.shared.base_model import ChatMessage
|
|
|
17
17
|
from fastapi import Body, status, HTTPException
|
|
18
18
|
from agent_api_server.cache.redis_cache import AsyncRedisThreadStorage
|
|
19
19
|
from ..schema.context import Context
|
|
20
|
+
from agent_api_server.observability import with_langfuse_config
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
@@ -344,13 +345,37 @@ async def message_generator(
|
|
|
344
345
|
interrupted_tasks = [
|
|
345
346
|
task for task in state_get.tasks if hasattr(task, "interrupts") and task.interrupts
|
|
346
347
|
]
|
|
348
|
+
run_config = with_langfuse_config(
|
|
349
|
+
{
|
|
350
|
+
"configurable": {
|
|
351
|
+
**dict(get_env(ts_tenant=ts_tenant)),
|
|
352
|
+
"graph_name": state.graph_name,
|
|
353
|
+
"thread_id": state.thread_id,
|
|
354
|
+
"TSTenant": ts_tenant,
|
|
355
|
+
"EIToken": ei_token,
|
|
356
|
+
"files": files or [],
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
thread_id=state.thread_id,
|
|
360
|
+
graph_name=state.graph_name,
|
|
361
|
+
ts_tenant=ts_tenant,
|
|
362
|
+
files=files,
|
|
363
|
+
)
|
|
364
|
+
run_context = Context(
|
|
365
|
+
ts_tenant=ts_tenant,
|
|
366
|
+
input=inputs,
|
|
367
|
+
ei_token=ei_token,
|
|
368
|
+
graph_name=state.graph_name,
|
|
369
|
+
files=files or [],
|
|
370
|
+
config=get_env(ts_tenant=ts_tenant),
|
|
371
|
+
)
|
|
347
372
|
|
|
348
373
|
if interrupted_tasks:
|
|
349
374
|
async for stream_event in graph_instance.astream(
|
|
350
375
|
Command(resume=inputs.get("resume", "")),
|
|
351
|
-
config=
|
|
376
|
+
config=run_config,
|
|
352
377
|
stream_mode=["updates","messages", "custom"],
|
|
353
|
-
context=
|
|
378
|
+
context=run_context,
|
|
354
379
|
subgraphs=True
|
|
355
380
|
):
|
|
356
381
|
async for chunk in handle_stream_event(stream_event):
|
|
@@ -360,9 +385,9 @@ async def message_generator(
|
|
|
360
385
|
|
|
361
386
|
async for stream_event in graph_instance.astream(
|
|
362
387
|
inputs or {},
|
|
363
|
-
config=
|
|
388
|
+
config=run_config,
|
|
364
389
|
stream_mode=["updates","messages", "custom"],
|
|
365
|
-
context=
|
|
390
|
+
context=run_context,
|
|
366
391
|
subgraphs=True
|
|
367
392
|
):
|
|
368
393
|
async for chunk in handle_stream_event(stream_event):
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sso_service.py
RENAMED
|
@@ -58,9 +58,16 @@ class SSOService:
|
|
|
58
58
|
def __init__(self, sso_config: SSOConfig):
|
|
59
59
|
self.sso_config = sso_config
|
|
60
60
|
self.client_token = None
|
|
61
|
+
self.sso_client = None
|
|
62
|
+
|
|
63
|
+
if not self.sso_config.sso_address:
|
|
64
|
+
logger.info("SSO address is not configured; skipping SSO client initialization")
|
|
65
|
+
return
|
|
61
66
|
|
|
62
|
-
# Initialize SSOClient
|
|
63
67
|
self.sso_client = SSOClient()
|
|
68
|
+
if self.sso_config.client_id and self.sso_config.client_secret:
|
|
69
|
+
return
|
|
70
|
+
|
|
64
71
|
self.sso_config.client_id, self.sso_config.client_secret = get_sso_client_id_and_secret(
|
|
65
72
|
sso_config.sso_address, ""
|
|
66
73
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-api-server"
|
|
3
|
-
version = "2.2.1"
|
|
3
|
+
version = "2.2.1-alpha"
|
|
4
4
|
description = "A Langgraph agent API server that implements Langgraph agent's web capabilities and can interact with chatbot"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Zijie Zhang", email = "zijie.zhang@advantech.com.cn"}
|
|
@@ -38,6 +38,7 @@ dependencies = [
|
|
|
38
38
|
"starlette (>=0.49.3, <0.50.0)",
|
|
39
39
|
"aiohttp (>=3.13.3,<4.0.0)",
|
|
40
40
|
"langchain-mcp-adapters (>=0.2.1,<0.3.0)",
|
|
41
|
+
"langfuse (>=4.3.1,<5.0.0)",
|
|
41
42
|
]
|
|
42
43
|
|
|
43
44
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/config_center/config_center.py
RENAMED
|
File without changes
|
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/dynamic_llm/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_convert/__init__.py
RENAMED
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_convert/mcp_convert.py
RENAMED
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_interceptor/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/service_hub/service_hub.py
RENAMED
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/service_hub/service_hub_test.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/detect_message.py
RENAMED
|
File without changes
|
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/__init__.py
RENAMED
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/__init__.py
RENAMED
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/client.py
RENAMED
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/credential.py
RENAMED
|
File without changes
|
{agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/encoding.py
RENAMED
|
File without changes
|