agent-api-server 2.1.14__tar.gz → 2.1.16__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 (91) hide show
  1. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/PKG-INFO +23 -5
  2. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/README.md +21 -3
  3. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/agent_registration.py +9 -4
  4. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/app.py +8 -4
  5. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/cache/redis_cache.py +9 -4
  6. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/config_center/config_center.py +37 -4
  7. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/configs/config.py +3 -0
  8. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/listener/redis_listener.py +2 -1
  9. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/memory/postgres.py +5 -3
  10. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/service_hub/service_hub.py +16 -8
  11. agent_api_server-2.1.16/agent_api_server/shared/redis_url.py +22 -0
  12. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/sso_service/sdk/client.py +50 -25
  13. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/sso_service/sso_service.py +21 -9
  14. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/startup.py +10 -6
  15. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/pyproject.toml +1 -1
  16. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/__init__.py +0 -0
  17. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/a2a_bridge/__init__.py +0 -0
  18. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/a2a_bridge/agent_card_builder.py +0 -0
  19. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/a2a_bridge/call_a2a_client.py +0 -0
  20. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/a2a_bridge/langgraph_executor.py +0 -0
  21. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/a2a_bridge/redis_task_store.py +0 -0
  22. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/a2a_bridge/server.py +0 -0
  23. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/acp_bridge/go/Makefile +0 -0
  24. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/acp_bridge/go/agent.go +0 -0
  25. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/acp_bridge/go/agent_test.go +0 -0
  26. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/acp_bridge/go/go.mod +0 -0
  27. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/acp_bridge/go/go.sum +0 -0
  28. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/acp_bridge/go/main.go +0 -0
  29. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/api/__init__.py +0 -0
  30. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/api/v1/__init__.py +0 -0
  31. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/api/v1/api.py +0 -0
  32. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/api/v1/config.py +0 -0
  33. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/api/v1/graph.py +0 -0
  34. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/api/v1/schema.py +0 -0
  35. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/api/v1/thread.py +0 -0
  36. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/cache/__init__.py +0 -0
  37. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/callback_handler.py +0 -0
  38. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/client/css/styles.css +0 -0
  39. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/client/favicon.ico +0 -0
  40. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/client/index.html +0 -0
  41. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/client/js/app.js +0 -0
  42. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/client/js/index.umd.js +0 -0
  43. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/configs/__init__.py +0 -0
  44. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/dynamic_llm/__init__.py +0 -0
  45. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/dynamic_llm/dynamic_llm.py +0 -0
  46. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/listener/__init__.py +0 -0
  47. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/listener/base.py +0 -0
  48. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/listener/nats_listener.py +0 -0
  49. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/log/__init__.py +0 -0
  50. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/log/formatters.py +0 -0
  51. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/log/logging.json +0 -0
  52. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/mcp_convert/__init__.py +0 -0
  53. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/mcp_convert/mcp_convert.py +0 -0
  54. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/mcp_server.py +0 -0
  55. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/memory/__init__.py +0 -0
  56. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/orchestrator/__init__.py +0 -0
  57. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/orchestrator/agent_registry.py +0 -0
  58. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/orchestrator/modes/__init__.py +0 -0
  59. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/orchestrator/modes/chain.py +0 -0
  60. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/orchestrator/modes/discussion.py +0 -0
  61. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/orchestrator/orchestrator.py +0 -0
  62. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/orchestrator/types.py +0 -0
  63. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/register/__init__.py +0 -0
  64. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/register/register.py +0 -0
  65. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/service.py +0 -0
  66. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/service_hub/service_hub_test.py +0 -0
  67. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/services/__init__.py +0 -0
  68. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/services/config_builder.py +0 -0
  69. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/services/stream_service.py +0 -0
  70. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/__init__.py +0 -0
  71. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/ase.py +0 -0
  72. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/base_model.py +0 -0
  73. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/common.py +0 -0
  74. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/context_manager.py +0 -0
  75. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/decode_token.py +0 -0
  76. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/detect_message.py +0 -0
  77. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/exceptions.py +0 -0
  78. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/get_model_info.py +0 -0
  79. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/graph_loader.py +0 -0
  80. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/loop_agent.py +0 -0
  81. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/markdown_fixer.py +0 -0
  82. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/mcp_auth.py +0 -0
  83. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/message.py +0 -0
  84. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/model_config.py +0 -0
  85. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/model_manager_client.py +0 -0
  86. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/request_context.py +0 -0
  87. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/shared/util_func.py +0 -0
  88. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/sso_service/__init__.py +0 -0
  89. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/sso_service/sdk/__init__.py +0 -0
  90. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/sso_service/sdk/credential.py +0 -0
  91. {agent_api_server-2.1.14 → agent_api_server-2.1.16}/agent_api_server/sso_service/sdk/encoding.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.3
2
2
  Name: agent-api-server
3
- Version: 2.1.14
3
+ Version: 2.1.16
4
4
  Summary: A Langgraph agent API server that implements Langgraph agent's web capabilities and can interact with chatbot
5
5
  Author: zijie.zhang@advantech.com.cn
6
6
  Requires-Python: >=3.10,<3.13
@@ -613,13 +613,13 @@ ACP 采用**代理模式(Proxy Mode)**,CLI 进程作为编辑器与 FastAP
613
613
 
614
614
  ```bash
615
615
  # 从 PyPI 安装
616
- pip install agent-api-server==2.1.14
616
+ pip install agent-api-server==2.1.16
617
617
 
618
618
  # 或从本地 wheel 安装
619
- pip install dist/agent_api_server-2.1.14-py3-none-any.whl
619
+ pip install dist/agent_api_server-2.1.16-py3-none-any.whl
620
620
  ```
621
621
 
622
- `2.1.14` 使用本仓库的 LangGraph 0.6 依赖,并包含 WisePortal 组织适配。PyPI 上另有使用 LangGraph 1.x 的 `2.2.x` 版本,安装本版本时请指定版本号。
622
+ `2.1.16` 使用本仓库的 LangGraph 0.6 依赖,并包含 WisePortal 组织适配、Redis `mode=standalone` 地址兼容,以及配置中心、SSO 和 ServiceHub 启动修复。PyPI 上另有使用 LangGraph 1.x 的 `2.2.x` 版本,安装本版本时请指定版本号。
623
623
 
624
624
  ACP 命令使用仓库中的 Go 实现,需要在源码目录单独构建,并将生成的程序放入 `PATH`:
625
625
 
@@ -948,6 +948,8 @@ pip install agent-api-server[a2a]
948
948
  SSO_URL=http://sso/v4.0
949
949
  CLIENT_ID=your_client_id
950
950
  CLIENT_SECRET=your_client_secret
951
+ # 自动查询/注册 SSO 客户端时使用;不同应用应使用不同名称。
952
+ # SSO_SERVICE_NAME=your-agent-service
951
953
 
952
954
  # 数据库
953
955
  POSTGRES_URL=postgresql://user:password@host:port/dbname
@@ -971,6 +973,13 @@ AGENT_AUTO_REGISTRATION=false
971
973
  ENABLE_A2A=false
972
974
  ```
973
975
 
976
+ `SSO_URL` 支持服务根地址或带 `/v4.0` 的地址,也可带末尾斜杠。
977
+ `CLIENT_ID` 和 `CLIENT_SECRET` 同时配置时直接使用;两项均为空时自动查询或注册
978
+ SSO 客户端,只配置其中一项会报错。`SSO_SERVICE_NAME` 未设置时兼容旧名称
979
+ `KB-Insight`,其他应用应显式设置自己的名称。
980
+ 提供 `ENSAAS_SERVICES` 时,ServiceHub 的数据库、Redis 和可选存储凭证会在
981
+ 创建连接对象前加载,用户名及密码中的特殊字符会进行 URL 编码。
982
+
974
983
  ### WisePortal 配置
975
984
 
976
985
  服务启动时会从 `${CONFIG_CENTER_ADDRESS}/v1/public/configs`
@@ -980,6 +989,15 @@ ENABLE_A2A=false
980
989
  配置读取失败时记录警告并保留当前开关值,初始默认值为 `false`。
981
990
  REST 与独立 MCP 服务都会在启动时读取配置;修改配置后需重启服务。
982
991
 
992
+ 同一公共接口返回的 `datacenter`、`cluster`、`workspace`、`namespace` 会在初始化
993
+ SSO 前写入同名环境变量,供 SSO 客户端查询、注册和客户端 Token 范围校验使用,
994
+ 与 WisePortal 开关是否开启无关。接口返回的非空字符串优先;字段缺失或为空、
995
+ 或配置读取失败时保留已有环境变量。启动日志会输出实际使用的 SSO 范围,并提示仍缺失的字段。
996
+
997
+ 设置 `ENABLE_CONFIG_CENTER=true` 后,还会从 `/v1/services` 读取 SSO 和 Model Manager
998
+ 的 API 地址,同步到全局配置与同名环境变量。此接口读取失败或某个地址为空时,
999
+ 保留原有地址;关闭该开关仍会读取上述公共配置。
1000
+
983
1001
  - 关闭时,REST 继续读取 Cookie `TSTenant`,MCP 继续读取 Header `TSTenant`。
984
1002
  - 开启时,优先读取 Header `Org-Str-Id`,其次 Header `orgStrId`、Cookie `orgStrId`,
985
1003
  将该值作为 `TSTenant`。缺少组织字段时不会回退到旧租户字段。
@@ -572,13 +572,13 @@ ACP 采用**代理模式(Proxy Mode)**,CLI 进程作为编辑器与 FastAP
572
572
 
573
573
  ```bash
574
574
  # 从 PyPI 安装
575
- pip install agent-api-server==2.1.14
575
+ pip install agent-api-server==2.1.16
576
576
 
577
577
  # 或从本地 wheel 安装
578
- pip install dist/agent_api_server-2.1.14-py3-none-any.whl
578
+ pip install dist/agent_api_server-2.1.16-py3-none-any.whl
579
579
  ```
580
580
 
581
- `2.1.14` 使用本仓库的 LangGraph 0.6 依赖,并包含 WisePortal 组织适配。PyPI 上另有使用 LangGraph 1.x 的 `2.2.x` 版本,安装本版本时请指定版本号。
581
+ `2.1.16` 使用本仓库的 LangGraph 0.6 依赖,并包含 WisePortal 组织适配、Redis `mode=standalone` 地址兼容,以及配置中心、SSO 和 ServiceHub 启动修复。PyPI 上另有使用 LangGraph 1.x 的 `2.2.x` 版本,安装本版本时请指定版本号。
582
582
 
583
583
  ACP 命令使用仓库中的 Go 实现,需要在源码目录单独构建,并将生成的程序放入 `PATH`:
584
584
 
@@ -907,6 +907,8 @@ pip install agent-api-server[a2a]
907
907
  SSO_URL=http://sso/v4.0
908
908
  CLIENT_ID=your_client_id
909
909
  CLIENT_SECRET=your_client_secret
910
+ # 自动查询/注册 SSO 客户端时使用;不同应用应使用不同名称。
911
+ # SSO_SERVICE_NAME=your-agent-service
910
912
 
911
913
  # 数据库
912
914
  POSTGRES_URL=postgresql://user:password@host:port/dbname
@@ -930,6 +932,13 @@ AGENT_AUTO_REGISTRATION=false
930
932
  ENABLE_A2A=false
931
933
  ```
932
934
 
935
+ `SSO_URL` 支持服务根地址或带 `/v4.0` 的地址,也可带末尾斜杠。
936
+ `CLIENT_ID` 和 `CLIENT_SECRET` 同时配置时直接使用;两项均为空时自动查询或注册
937
+ SSO 客户端,只配置其中一项会报错。`SSO_SERVICE_NAME` 未设置时兼容旧名称
938
+ `KB-Insight`,其他应用应显式设置自己的名称。
939
+ 提供 `ENSAAS_SERVICES` 时,ServiceHub 的数据库、Redis 和可选存储凭证会在
940
+ 创建连接对象前加载,用户名及密码中的特殊字符会进行 URL 编码。
941
+
933
942
  ### WisePortal 配置
934
943
 
935
944
  服务启动时会从 `${CONFIG_CENTER_ADDRESS}/v1/public/configs`
@@ -939,6 +948,15 @@ ENABLE_A2A=false
939
948
  配置读取失败时记录警告并保留当前开关值,初始默认值为 `false`。
940
949
  REST 与独立 MCP 服务都会在启动时读取配置;修改配置后需重启服务。
941
950
 
951
+ 同一公共接口返回的 `datacenter`、`cluster`、`workspace`、`namespace` 会在初始化
952
+ SSO 前写入同名环境变量,供 SSO 客户端查询、注册和客户端 Token 范围校验使用,
953
+ 与 WisePortal 开关是否开启无关。接口返回的非空字符串优先;字段缺失或为空、
954
+ 或配置读取失败时保留已有环境变量。启动日志会输出实际使用的 SSO 范围,并提示仍缺失的字段。
955
+
956
+ 设置 `ENABLE_CONFIG_CENTER=true` 后,还会从 `/v1/services` 读取 SSO 和 Model Manager
957
+ 的 API 地址,同步到全局配置与同名环境变量。此接口读取失败或某个地址为空时,
958
+ 保留原有地址;关闭该开关仍会读取上述公共配置。
959
+
942
960
  - 关闭时,REST 继续读取 Cookie `TSTenant`,MCP 继续读取 Header `TSTenant`。
943
961
  - 开启时,优先读取 Header `Org-Str-Id`,其次 Header `orgStrId`、Cookie `orgStrId`,
944
962
  将该值作为 `TSTenant`。缺少组织字段时不会回退到旧租户字段。
@@ -1,5 +1,6 @@
1
1
  """Agent registration and listener management service."""
2
2
  import os
3
+ import asyncio
3
4
  import threading
4
5
  from typing import List, Dict, Any
5
6
 
@@ -29,25 +30,29 @@ class AgentRegistrationService:
29
30
  SSOInitializationError: If SSO initialization fails
30
31
  """
31
32
  try:
32
- sso_service = SSOService(
33
+ sso_service = await asyncio.to_thread(
34
+ SSOService,
33
35
  sso_config=SSOConfig(
34
36
  sso_address=global_config.SSO_URL,
35
37
  client_id=global_config.CLIENT_ID,
36
38
  client_secret=global_config.CLIENT_SECRET,
39
+ service_name=global_config.SSO_SERVICE_NAME,
37
40
  )
38
41
  )
39
42
 
40
43
  client_id, client_secret = sso_service.get_client_id_and_secret()
41
44
  logger.debug("SSO credentials obtained - Client ID: %s", client_id)
42
45
 
43
- self._client_token = sso_service.generate_client_token(
44
- client_id, client_secret
46
+ self._client_token = await asyncio.to_thread(
47
+ sso_service.generate_client_token, client_id, client_secret
45
48
  )
46
49
 
50
+ global_config.CLIENT_ID = client_id
51
+ global_config.CLIENT_SECRET = client_secret
47
52
  global_config.CLIENT_TOKEN = self._client_token
48
53
  os.environ['CLIENT_TOKEN'] = self._client_token
49
54
 
50
- logger.info(f"SSO initialization completed successfully, CLIENT_TOKEN is {self._client_token}")
55
+ logger.info("SSO initialization completed successfully")
51
56
  return self._client_token
52
57
 
53
58
  except Exception as e:
@@ -61,6 +61,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
61
61
  """Single source of truth for application lifecycle.
62
62
 
63
63
  Initialization order:
64
+ 0. Configuration and credentials
64
65
  1. Infrastructure (Postgres + Redis)
65
66
  2. Schema migration (advisory-lock protected, multi-worker safe)
66
67
  3. SSO + Agent registration + Listeners (if not already done by init_fastapi_server)
@@ -70,11 +71,15 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
70
71
  from agent_api_server.startup import ApplicationStartup
71
72
 
72
73
  startup = ApplicationStartup()
73
- checkpointer = AsyncPostgresCheckpointer.get_worker_instance()
74
- cache = AsyncRedisThreadStorage.get_worker_instance()
75
74
 
76
75
  try:
77
- await initialize_wiseportal_config()
76
+ if not ApplicationStartup._initialized:
77
+ await initialize_wiseportal_config()
78
+ await startup._initialize_credentials()
79
+
80
+ # Constructors snapshot their connection URLs, so configuration must be ready first.
81
+ checkpointer = AsyncPostgresCheckpointer.get_worker_instance()
82
+ cache = AsyncRedisThreadStorage.get_worker_instance()
78
83
 
79
84
  # 1. Infrastructure (always run, these are per-worker)
80
85
  await checkpointer.initialize()
@@ -90,7 +95,6 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
90
95
  # 3. Credentials, SSO, Agent registration, Listeners
91
96
  # Only run if not already initialized by init_fastapi_server()
92
97
  if not ApplicationStartup._initialized:
93
- await startup._initialize_credentials()
94
98
  await startup.registration_service.initialize_sso()
95
99
 
96
100
  agents = await startup._load_agent_config()
@@ -9,6 +9,7 @@ from uuid import uuid4
9
9
  from dataclasses import dataclass
10
10
  import redis.asyncio as redis
11
11
  from agent_api_server.configs import global_config
12
+ from agent_api_server.shared.redis_url import normalize_redis_url
12
13
  from redis.exceptions import RedisError, WatchError
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -65,9 +66,13 @@ class AsyncRedisThreadStorage:
65
66
 
66
67
  @staticmethod
67
68
  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
69
+ """Normalize the configured Redis URL and redact credentials in logs."""
70
+ redis_url = normalize_redis_url(global_config.REDIS_URL)
71
+ scheme = redis_url.partition("://")[0]
72
+ safe_redis_url = (
73
+ f"{scheme}://[REDACTED]@{redis_url.rsplit('@', 1)[1]}"
74
+ if '@' in redis_url else redis_url
75
+ )
71
76
  logger.debug("Using Redis URL: %s", safe_redis_url)
72
77
  return redis_url
73
78
 
@@ -382,4 +387,4 @@ class AsyncRedisThreadStorage:
382
387
  return bool(exists) and status != b"stopped"
383
388
  except RedisError as e:
384
389
  logger.error(f"Failed to check stop flag: {str(e)}")
385
- return False
390
+ return False
@@ -244,17 +244,50 @@ def parse_wiseportal_enabled(value) -> bool:
244
244
  return value is True or (isinstance(value, str) and value.strip().lower() == "true")
245
245
 
246
246
 
247
- async def initialize_wiseportal_config() -> None:
248
- """Load the portal mode during server startup."""
249
- client = AsyncConfigCenterClient()
247
+ async def _initialize_public_config(client: AsyncConfigCenterClient) -> None:
250
248
  try:
251
249
  config = await client.public_config()
252
250
  global_config.isWisePortal = config["isWisePortal"]
251
+ scope_fields = ("datacenter", "cluster", "workspace", "namespace")
252
+ for field in scope_fields:
253
+ value = config.get(field)
254
+ if isinstance(value, str) and value.strip():
255
+ # SSOClient and verify_client_token both read these environment keys.
256
+ os.environ[field] = value.strip()
257
+
253
258
  logger.info("WisePortal mode: %s", global_config.isWisePortal)
259
+ logger.info("SSO scope: %s", {field: os.getenv(field) for field in scope_fields})
260
+ missing = [field for field in scope_fields if not os.getenv(field, "").strip()]
261
+ if missing:
262
+ logger.warning(
263
+ "Missing SSO scope fields: %s; configure them in /v1/public/configs "
264
+ "or environment variables",
265
+ ", ".join(missing),
266
+ )
254
267
  except (httpx.HTTPError, ValueError, TypeError, AttributeError) as exc:
255
268
  logger.warning(
256
- "Unable to load WisePortal public config; retaining isWisePortal=%s: %s",
269
+ "Unable to load public config; retaining existing SSO scope and isWisePortal=%s: %s",
257
270
  global_config.isWisePortal, exc,
258
271
  )
272
+
273
+
274
+ async def initialize_wiseportal_config() -> None:
275
+ """Load public scope and optional service discovery before clients are created."""
276
+ client = AsyncConfigCenterClient()
277
+ try:
278
+ await _initialize_public_config(client)
279
+ if global_config.ENABLE_CONFIG_CENTER:
280
+ try:
281
+ services = await client.service_config()
282
+ for field in ("SSO_URL", "MODEL_MANAGER_SERVICE_URL"):
283
+ address = services.get(field)
284
+ if isinstance(address, str) and address.strip():
285
+ address = address.strip().rstrip("/")
286
+ setattr(global_config, field, address)
287
+ # Legacy model-manager paths still read the environment.
288
+ os.environ[field] = address
289
+ logger.info("ConfigCenter service addresses loaded")
290
+ except (httpx.HTTPError, ValueError, TypeError, AttributeError) as exc:
291
+ logger.warning("Unable to load service config; retaining existing addresses: %s", exc)
259
292
  finally:
260
293
  await client.close()
@@ -12,6 +12,9 @@ class SSOConfig(BaseSettings):
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 = Field(default="", description="The client token of server")
15
+ SSO_SERVICE_NAME: str = Field(
16
+ default="", description="SSO client service name; defaults to KB-Insight for compatibility"
17
+ )
15
18
 
16
19
 
17
20
  class CacheConfig(BaseSettings):
@@ -8,6 +8,7 @@ import redis
8
8
 
9
9
  from agent_api_server.configs import global_config
10
10
  from agent_api_server.listener.base import MessageListener
11
+ from agent_api_server.shared.redis_url import normalize_redis_url
11
12
 
12
13
  import logging
13
14
 
@@ -48,7 +49,7 @@ class RedisListener(MessageListener):
48
49
  """Establish Redis connection and subscribe to channels."""
49
50
  try:
50
51
  self.redis_client = redis.Redis.from_url(
51
- url=global_config.MODEL_MANAGER_REDIS_URL,
52
+ url=normalize_redis_url(global_config.MODEL_MANAGER_REDIS_URL),
52
53
  socket_timeout=10,
53
54
  socket_keepalive=True,
54
55
  health_check_interval=15,
@@ -5,6 +5,7 @@ from typing import Optional, cast, Dict, Any
5
5
  from psycopg_pool import AsyncConnectionPool
6
6
  from psycopg.rows import dict_row
7
7
  from psycopg import OperationalError, AsyncConnection
8
+ from psycopg.conninfo import conninfo_to_dict
8
9
  from agent_api_server.configs import global_config
9
10
  from tenacity import (
10
11
  retry,
@@ -75,8 +76,9 @@ class AsyncPostgresCheckpointer:
75
76
  def _get_conn_str() -> str:
76
77
  """获取安全的连接字符串(隐藏密码)"""
77
78
  conn_str = global_config.POSTGRES_URL
78
- safe_conn_str = conn_str.split('@')[0] + '@[REDACTED]' if '@' in conn_str else conn_str
79
- logger.debug("Using connection string: %s", safe_conn_str)
79
+ connection = conninfo_to_dict(conn_str)
80
+ logger.debug("Using PostgreSQL connection: host=%s port=%s dbname=%s",
81
+ connection.get("host"), connection.get("port"), connection.get("dbname"))
80
82
  return conn_str
81
83
 
82
84
  @classmethod
@@ -264,4 +266,4 @@ class AsyncPostgresCheckpointer:
264
266
 
265
267
  @property
266
268
  def is_initialized(self):
267
- return self._is_initialized
269
+ return self._is_initialized
@@ -3,6 +3,7 @@ import os
3
3
  import logging
4
4
  from typing import Dict, List, Union
5
5
  from dataclasses import dataclass, field
6
+ from urllib.parse import quote
6
7
 
7
8
  logger = logging.getLogger(__name__)
8
9
 
@@ -85,31 +86,35 @@ class ServiceHubCredentials:
85
86
  raise CredentialNotFoundError("PostgreSQL credentials not found")
86
87
 
87
88
  cred = self.postgres[0]
88
- return f"postgresql://{cred.username}:{cred.password}@{cred.internal_hosts}/{cred.database}"
89
+ return (
90
+ f"postgresql://{quote(cred.username, safe='')}:{quote(cred.password, safe='')}"
91
+ f"@{cred.internal_hosts}/{quote(cred.database, safe='')}"
92
+ )
89
93
 
90
94
  def to_redis_dsn(self) -> str:
91
95
  if not self.redis:
92
96
  raise CredentialNotFoundError("Redis credentials not found")
93
97
 
94
98
  cred = self.redis[0]
99
+ username = quote(cred.username, safe="")
100
+ password = quote(cred.password, safe="")
95
101
 
96
102
  if not cred.password and not cred.username:
97
103
  return f"redis://{cred.internal_hosts}:{cred.port}"
98
104
 
99
105
  if cred.password and not cred.username:
100
- return f"redis://:{cred.password}@{cred.internal_hosts}:{cred.port}"
106
+ return f"redis://:{password}@{cred.internal_hosts}:{cred.port}"
101
107
 
102
108
  if cred.username and not cred.password:
103
- return f"redis://{cred.username}@{cred.internal_hosts}:{cred.port}"
109
+ return f"redis://{username}@{cred.internal_hosts}:{cred.port}"
104
110
 
105
111
  if cred.username and cred.password:
106
- return f"redis://{cred.username}:{cred.password}@{cred.internal_hosts}:{cred.port}"
112
+ return f"redis://{username}:{password}@{cred.internal_hosts}:{cred.port}"
107
113
 
108
114
  def get_blobstore_s3_config(self) -> Dict[str, str]:
109
115
  """Get Blobstore credentials as S3-compatible configuration dictionary"""
110
116
  if not self.blobstore:
111
- logger.warning("Blobstore credentials not found")
112
- return {}
117
+ raise CredentialNotFoundError("Blobstore credentials not found")
113
118
 
114
119
  cred = self.blobstore[0]
115
120
  return {
@@ -182,7 +187,10 @@ class ServiceHubCredentialParser:
182
187
  endpoint=data.get("credentials", {}).get("endpoint", ""),
183
188
  external_hosts=data.get("credentials", {}).get("externalHosts", ""),
184
189
  internal_hosts=data.get("credentials", {}).get("internalHosts", ""),
185
- signature_version=data.get("credentials", {}).get("signature-version", ""),
190
+ signature_version=(
191
+ data.get("credentials", {}).get("signatureVersion")
192
+ or data.get("credentials", {}).get("signature-version", "")
193
+ ),
186
194
  type=data.get("credentials", {}).get("type", ""),
187
195
  instance_name=data.get("instance_name", ""),
188
196
  label=data.get("label", ""),
@@ -230,4 +238,4 @@ class ServiceHubCredentialParser:
230
238
  class ServiceHubCredentialLoader:
231
239
  @staticmethod
232
240
  def load_credentials() -> ServiceHubCredentials:
233
- return ServiceHubCredentialParser.parse(os.getenv("ENSAAS_SERVICES"))
241
+ return ServiceHubCredentialParser.parse(os.getenv("ENSAAS_SERVICES"))
@@ -0,0 +1,22 @@
1
+ """Adapt Redis URLs from configuration providers for redis-py clients."""
2
+
3
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
4
+
5
+
6
+ def normalize_redis_url(url: str) -> str:
7
+ """Remove standalone topology metadata while retaining connection options.
8
+
9
+ redis-py forwards URL query parameters to the connection constructor, which
10
+ does not accept the config center's ``mode`` parameter. Other topologies
11
+ require dedicated clients and must not silently use a standalone client.
12
+ """
13
+ parsed = urlsplit(url)
14
+ parameters = parse_qsl(parsed.query, keep_blank_values=True)
15
+ modes = [value for name, value in parameters if name == "mode"]
16
+ if not modes:
17
+ return url
18
+ if any(mode.lower() != "standalone" for mode in modes):
19
+ raise ValueError("Unsupported Redis mode; only mode=standalone is supported")
20
+
21
+ query = urlencode([(name, value) for name, value in parameters if name != "mode"])
22
+ return urlunsplit(parsed._replace(query=query))
@@ -11,6 +11,14 @@ logger = logging.getLogger(__name__)
11
11
 
12
12
  CLIENT_CREDENTIALS = "client_credentials"
13
13
  CURRENT_SERVICE_NAME = "KB-Insight"
14
+ SSO_REQUEST_TIMEOUT = 30.0
15
+
16
+
17
+ def normalize_sso_address(address: str) -> str:
18
+ if not isinstance(address, str) or not address.strip():
19
+ raise ValueError("SSO service address is required")
20
+ address = address.strip().rstrip("/")
21
+ return address if address.endswith("/v4.0") else f"{address}/v4.0"
14
22
 
15
23
 
16
24
  class ClientRequest:
@@ -117,32 +125,40 @@ class SsoToken:
117
125
 
118
126
  class SSOClient:
119
127
  def __init__(self, service_name: Optional[str] = None):
128
+ service_name = service_name or os.getenv("SSO_SERVICE_NAME") or CURRENT_SERVICE_NAME
120
129
  self.client_info = ClientInfo(
121
130
  datacenter=os.getenv("datacenter"),
122
131
  cluster=os.getenv("cluster"),
123
132
  workspace=os.getenv("workspace"),
124
133
  namespace=os.getenv("namespace"),
125
134
  app_id=os.getenv("appID") or os.getenv("appId"),
126
- app_name=service_name or CURRENT_SERVICE_NAME,
127
- service_name=service_name or CURRENT_SERVICE_NAME,
135
+ app_name=service_name,
136
+ service_name=service_name,
128
137
  )
129
138
 
130
139
  def query_sso_client(self, address: str, req: ClientRequest) -> ClientInfo:
131
- path = f"{address}/clients/{quote(req.app_name)}"
132
- path += f"?namespace={req.namespace}&workspace={req.workspace}&cluster={req.cluster}&datacenter={req.datacenter}&serviceName={quote(req.service_name)}&appId={req.app_id}"
140
+ path = f"{normalize_sso_address(address)}/clients/{quote(req.app_name, safe='')}"
141
+ params = {
142
+ "namespace": req.namespace,
143
+ "workspace": req.workspace,
144
+ "cluster": req.cluster,
145
+ "datacenter": req.datacenter,
146
+ "serviceName": req.service_name,
147
+ "appId": req.app_id,
148
+ }
133
149
 
134
150
  credential = self.new_srp_token_credential(req.service_name)
135
151
  headers = outbound_headers({credential["header_name"]: credential["header_value"]})
136
152
 
137
- response = requests.get(path, headers=headers)
153
+ response = requests.get(path, params=params, headers=headers, timeout=SSO_REQUEST_TIMEOUT)
138
154
  response.raise_for_status()
139
155
  return ClientInfo.from_dict(response.json())
140
156
 
141
157
  def register_client(self, address: str, req: ClientRequest) -> ClientInfo:
142
- path = f"{address}/clients"
158
+ path = f"{normalize_sso_address(address)}/clients"
143
159
  body = json.dumps(req.to_dict())
144
160
 
145
- logger.info({body})
161
+ logger.info("Registering SSO client for service %s", req.service_name)
146
162
 
147
163
  credential = self.new_srp_token_credential(req.service_name)
148
164
  headers = {
@@ -150,12 +166,14 @@ class SSOClient:
150
166
  "Content-Type": "application/json"
151
167
  }
152
168
 
153
- response = requests.post(path, headers=outbound_headers(headers), data=body)
169
+ response = requests.post(
170
+ path, headers=outbound_headers(headers), data=body, timeout=SSO_REQUEST_TIMEOUT
171
+ )
154
172
  response.raise_for_status()
155
173
  return ClientInfo.from_dict(response.json())
156
174
 
157
175
  def update_sso_client(self, address: str, client_id: str, req: ClientRequest) -> ClientInfo:
158
- path = f"{address}/clients/{client_id}"
176
+ path = f"{normalize_sso_address(address)}/clients/{quote(client_id, safe='')}"
159
177
  body = json.dumps(req.to_dict())
160
178
 
161
179
  credential = self.new_srp_token_credential(req.service_name)
@@ -164,39 +182,46 @@ class SSOClient:
164
182
  "Content-Type": "application/json"
165
183
  }
166
184
 
167
- response = requests.put(path, headers=outbound_headers(headers), data=body)
185
+ response = requests.put(
186
+ path, headers=outbound_headers(headers), data=body, timeout=SSO_REQUEST_TIMEOUT
187
+ )
168
188
  response.raise_for_status()
169
189
  return ClientInfo.from_dict(response.json())
170
190
 
171
191
  def query_sso_client_token(self, address: str) -> SsoToken:
172
- path = f"{address}/oauth/token"
173
- path += f"?grant_type={CLIENT_CREDENTIALS}&client_id={self.client_info.client_id}&client_secret={self.client_info.client_secret}&duration=eternal"
192
+ path = f"{normalize_sso_address(address)}/oauth/token"
193
+ params = {
194
+ "grant_type": CLIENT_CREDENTIALS,
195
+ "client_id": self.client_info.client_id,
196
+ "client_secret": self.client_info.client_secret,
197
+ "duration": "eternal",
198
+ }
174
199
 
175
- response = requests.post(path, headers=outbound_headers())
200
+ response = requests.post(
201
+ path, params=params, headers=outbound_headers(), timeout=SSO_REQUEST_TIMEOUT
202
+ )
176
203
  response.raise_for_status()
177
204
  return SsoToken.from_dict(response.json())
178
205
 
179
206
  def set_sso_client(self, address: str) -> None:
180
- if not address.endswith("/v4.0"):
181
- address += "/v4.0"
207
+ address = normalize_sso_address(address)
182
208
 
183
209
  try:
184
210
  rsp = self.query_sso_client(address, self.client_info)
185
211
  except requests.exceptions.HTTPError as e:
186
- if "Not Found" in str(e):
212
+ if e.response is not None and e.response.status_code == 404:
187
213
  rsp = self.register_client(address, self.client_info)
188
214
  else:
189
- raise e
215
+ raise
190
216
 
191
217
  if rsp is None:
192
218
  raise Exception("Failed to set SSO client")
193
219
 
194
220
  if rsp.app_id != self.client_info.app_id:
195
- try:
196
- new_rsp = self.update_sso_client(address, rsp.client_id, self.client_info)
197
- rsp = new_rsp
198
- except requests.exceptions.HTTPError as e:
199
- logger.error(f"Failed to update SSO client: {e}")
221
+ rsp = self.update_sso_client(address, rsp.client_id, self.client_info)
222
+
223
+ if not rsp.client_id or not rsp.client_secret:
224
+ raise ValueError("SSO response is missing clientId or clientSecret")
200
225
 
201
226
  self.client_info = rsp
202
227
  return None
@@ -217,9 +242,9 @@ def get_sso_client_id_and_secret(address: str, service: str) -> tuple:
217
242
  try:
218
243
  sso.set_sso_client(address)
219
244
  except Exception as e:
220
- raise ValueError(f"get sso clientId and ClientSecret err: {e}")
245
+ raise ValueError(f"get sso clientId and ClientSecret err: {e}") from e
221
246
 
222
- logger.debug(
223
- f"ServiceName: {sso.client_info.service_name}, ClientID: {sso.client_info.client_id}, ClientSecret: {sso.client_info.client_secret}")
247
+ logger.debug("SSO credentials obtained for service %s, client ID %s",
248
+ sso.client_info.service_name, sso.client_info.client_id)
224
249
 
225
250
  return sso.client_info.client_id, sso.client_info.client_secret
@@ -6,16 +6,18 @@ from typing import List, Optional, Dict, Any
6
6
 
7
7
  import httpx
8
8
  from agent_api_server.shared.request_context import outbound_headers
9
- from .sdk import SSOClient, get_sso_client_id_and_secret
9
+ from .sdk import SSOClient
10
+ from .sdk.client import normalize_sso_address
10
11
 
11
12
  logger = logging.getLogger(__name__)
12
13
 
13
14
 
14
15
  class SSOConfig:
15
- def __init__(self, sso_address: str, client_id: str, client_secret: str):
16
- self.sso_address = sso_address
16
+ def __init__(self, sso_address: str, client_id: str, client_secret: str, service_name: str = ""):
17
+ self.sso_address = normalize_sso_address(sso_address)
17
18
  self.client_id = client_id
18
19
  self.client_secret = client_secret
20
+ self.service_name = service_name
19
21
 
20
22
 
21
23
  def get_client_info_by_client_token(token: str) -> Dict[str, Any]:
@@ -60,11 +62,17 @@ class SSOService:
60
62
  self.sso_config = sso_config
61
63
  self.client_token = None
62
64
 
63
- # Initialize SSOClient
64
- self.sso_client = SSOClient()
65
- self.sso_config.client_id, self.sso_config.client_secret = get_sso_client_id_and_secret(
66
- sso_config.sso_address, ""
67
- )
65
+ if bool(sso_config.client_id) != bool(sso_config.client_secret):
66
+ raise ValueError("CLIENT_ID and CLIENT_SECRET must be configured together")
67
+
68
+ self.sso_client = SSOClient(sso_config.service_name)
69
+ if sso_config.client_id and sso_config.client_secret:
70
+ self.sso_client.client_info.client_id = sso_config.client_id
71
+ self.sso_client.client_info.client_secret = sso_config.client_secret
72
+ else:
73
+ self.sso_client.set_sso_client(sso_config.sso_address)
74
+ self.sso_config.client_id = self.sso_client.client_info.client_id
75
+ self.sso_config.client_secret = self.sso_client.client_info.client_secret
68
76
 
69
77
  def get_client_token(self) -> Optional[str]:
70
78
  return self.client_token
@@ -172,7 +180,11 @@ class SSOService:
172
180
 
173
181
  # 获取 JSON 数据
174
182
  token_data = response.json()
175
- return token_data.get("accessToken")
183
+ access_token = token_data.get("accessToken")
184
+ if not isinstance(access_token, str) or not access_token:
185
+ raise ValueError("SSO response is missing accessToken")
186
+ self.client_token = access_token
187
+ return access_token
176
188
 
177
189
  def get_client_id_and_secret(self) -> tuple[str, str]:
178
190
  return self.sso_config.client_id, self.sso_config.client_secret
@@ -93,13 +93,17 @@ class ApplicationStartup:
93
93
  credentials = ServiceHubCredentialLoader.load_credentials()
94
94
  logger.info("Credentials loaded from ServiceHub")
95
95
 
96
- global_config.POSTGRES_URL = credentials.get("POSTGRES_URL", "")
97
- global_config.REDIS_URL = credentials.get("REDIS_URL", "")
96
+ postgres_url = credentials.to_postgres_dsn()
97
+ redis_url = credentials.to_redis_dsn()
98
+ storage = credentials.get_blobstore_s3_config() if credentials.blobstore else {}
98
99
 
99
- if "BLOB_ENDPOINT" in credentials:
100
- global_config.STORAGE_ENDPOINT = credentials["BLOB_ENDPOINT"]
101
- global_config.ACCESS_KEY = credentials.get("ACCESS_KEY", "")
102
- global_config.SECRET_KEY = credentials.get("SECRET_KEY", "")
100
+ global_config.POSTGRES_URL = postgres_url
101
+ global_config.REDIS_URL = redis_url
102
+
103
+ if storage:
104
+ global_config.STORAGE_ENDPOINT = storage["endpoint"]
105
+ global_config.ACCESS_KEY = storage["access_key"]
106
+ global_config.SECRET_KEY = storage["secret_key"]
103
107
 
104
108
  logger.info("Credentials initialized successfully")
105
109
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-api-server"
3
- version = "2.1.14"
3
+ version = "2.1.16"
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@advantech.com.cn"}