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.
Files changed (61) hide show
  1. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/PKG-INFO +2 -1
  2. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/thread.py +8 -1
  3. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/configs/config.py +13 -6
  4. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/dynamic_llm/dynamic_llm.py +58 -7
  5. agent_api_server-2.2.1a0/agent_api_server/observability/__init__.py +3 -0
  6. agent_api_server-2.2.1a0/agent_api_server/observability/langfuse.py +84 -0
  7. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/service.py +78 -28
  8. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/get_model_info.py +46 -4
  9. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/message.py +29 -4
  10. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sso_service.py +8 -1
  11. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/pyproject.toml +2 -1
  12. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/README.md +0 -0
  13. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/__init__.py +0 -0
  14. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/__init__.py +0 -0
  15. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/__init__.py +0 -0
  16. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/api.py +0 -0
  17. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/config.py +0 -0
  18. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/graph.py +0 -0
  19. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/api/v1/schema.py +0 -0
  20. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/cache/__init__.py +0 -0
  21. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/cache/redis_cache.py +0 -0
  22. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/callback_handler.py +0 -0
  23. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/css/styles.css +0 -0
  24. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/favicon.ico +0 -0
  25. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/index.html +0 -0
  26. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/js/app.js +0 -0
  27. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/client/js/index.umd.js +0 -0
  28. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/config_center/config_center.py +0 -0
  29. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/configs/__init__.py +0 -0
  30. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/dynamic_llm/__init__.py +0 -0
  31. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/dynamic_llm/model.py +0 -0
  32. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/listener.py +0 -0
  33. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/log/__init__.py +0 -0
  34. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/log/formatters.py +0 -0
  35. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/log/logging.json +0 -0
  36. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_convert/__init__.py +0 -0
  37. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_convert/mcp_convert.py +0 -0
  38. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_interceptor/__init__.py +0 -0
  39. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/mcp_interceptor/mcp_intecerpter.py +0 -0
  40. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/memeory/__init__.py +0 -0
  41. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/memeory/postgres.py +0 -0
  42. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/middleware/__init__.py +0 -0
  43. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/middleware/model.py +0 -0
  44. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/middleware/schema.py +0 -0
  45. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/register/__init__.py +0 -0
  46. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/register/register.py +0 -0
  47. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/schema/__init__.py +0 -0
  48. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/schema/context.py +0 -0
  49. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/service_hub/service_hub.py +0 -0
  50. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/service_hub/service_hub_test.py +0 -0
  51. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/__init__.py +0 -0
  52. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/ase.py +0 -0
  53. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/base_model.py +0 -0
  54. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/common.py +0 -0
  55. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/detect_message.py +0 -0
  56. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/shared/util_func.py +0 -0
  57. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/__init__.py +0 -0
  58. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/__init__.py +0 -0
  59. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/client.py +0 -0
  60. {agent_api_server-2.2.1 → agent_api_server-2.2.1a0}/agent_api_server/sso_service/sdk/credential.py +0 -0
  61. {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.1
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 config
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="http://sso/v4.0", description="The URL of SSO service"
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
- CLIENT_TOKEN: str = Field(default="", description="The client token of server")
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="https://api-am-ensaas.axa.wise-paas.com.cn", description="The URL of model manager service url"
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="redis://localhost:6379/0", description="Redis server URL which used by model_manager, format is: redis://host:port/db"
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
-
@@ -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, CLIENT_TOKEN is {global_config.CLIENT_TOKEN}")
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=os.environ['MODEL_MANAGER_SERVICE_URL'],
194
+ model_managment_url=global_config.MODEL_MANAGER_SERVICE_URL,
176
195
  config_type=self.config_type,
177
- token=os.environ['CLIENT_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": configurable.get(self._get_config_key("PROVIDER", ts_tenant)),
288
- "model": configurable.get(self._get_config_key("MODEL", ts_tenant)),
289
- "credentials": configurable.get(self._get_config_key("CREDENTIALS", ts_tenant)),
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,3 @@
1
+ from .langfuse import is_langfuse_enabled, shutdown_langfuse, with_langfuse_config
2
+
3
+ __all__ = ["is_langfuse_enabled", "shutdown_langfuse", "with_langfuse_config"]
@@ -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
- sso_service = SSOService(sso_config=SSOConfig(
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 during agent registration or listener setup: {e}")
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
- try:
281
- listener_type = determine_listener_type()
282
- logger.info(f"Using {listener_type.name} listener based on configuration")
283
- except ValueError as e:
284
- logger.error(str(e))
285
- raise
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
- if hasattr(global_config, 'MODEL_MANAGER_NATS_URL') and global_config.MODEL_MANAGER_NATS_URL:
316
- return ListenerType.NATS
317
- elif hasattr(global_config, 'MODEL_MANAGER_REDIS_URL') and global_config.MODEL_MANAGER_REDIS_URL:
318
- return ListenerType.REDIS
319
- else:
320
- raise ValueError(
321
- "No valid message broker configured. "
322
- "Please set either MODEL_MANAGER_NATS_URL or MODEL_MANAGER_REDIS_URL"
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:
@@ -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=configurable.get(self.key_builder.get_key(tool_name, "PROVIDER", ts_tenant)),
182
- model=configurable.get(self.key_builder.get_key(tool_name, "MODEL", ts_tenant)),
183
- credentials=configurable.get(self.key_builder.get_key(tool_name, "CREDENTIALS", ts_tenant)),
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={"configurable": {**dict(get_env(ts_tenant=ts_tenant)), "graph_name": state.graph_name, "thread_id": state.thread_id, "TSTenant": ts_tenant, "EIToken": ei_token, "files": files or []}},
376
+ config=run_config,
352
377
  stream_mode=["updates","messages", "custom"],
353
- context=Context(ts_tenant=ts_tenant, input=inputs, ei_token=ei_token, graph_name=state.graph_name, files=files or [], config=get_env(ts_tenant=ts_tenant)),
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={"configurable": {**dict(get_env(ts_tenant=ts_tenant)), "graph_name": state.graph_name, "thread_id": state.thread_id, "TSTenant": ts_tenant, "EIToken": ei_token, "files": files or []}},
388
+ config=run_config,
364
389
  stream_mode=["updates","messages", "custom"],
365
- context=Context(ts_tenant=ts_tenant, input=inputs, ei_token=ei_token, graph_name=state.graph_name, files=files or [], config=get_env(ts_tenant=ts_tenant)),
390
+ context=run_context,
366
391
  subgraphs=True
367
392
  ):
368
393
  async for chunk in handle_stream_event(stream_event):
@@ -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]