agent-api-server 2.1.13__tar.gz → 2.1.14__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 (100) hide show
  1. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/PKG-INFO +82 -9
  2. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/README.md +79 -3
  3. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/a2a_bridge/call_a2a_client.py +28 -0
  4. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/a2a_bridge/langgraph_executor.py +8 -3
  5. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/acp_bridge/go/agent.go +20 -11
  6. agent_api_server-2.1.14/agent_api_server/acp_bridge/go/agent_test.go +46 -0
  7. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/acp_bridge/go/main.go +9 -6
  8. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/api/v1/thread.py +10 -8
  9. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/app.py +5 -0
  10. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/config_center/config_center.py +32 -4
  11. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/configs/config.py +6 -1
  12. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/dynamic_llm/dynamic_llm.py +59 -51
  13. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/listener/base.py +62 -58
  14. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/listener/nats_listener.py +88 -99
  15. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/mcp_convert/mcp_convert.py +24 -8
  16. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/orchestrator/agent_registry.py +6 -2
  17. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/orchestrator/modes/chain.py +4 -1
  18. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/orchestrator/modes/discussion.py +4 -1
  19. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/orchestrator/orchestrator.py +1 -1
  20. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/register/register.py +2 -2
  21. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/services/config_builder.py +6 -1
  22. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/services/stream_service.py +13 -9
  23. agent_api_server-2.1.14/agent_api_server/shared/context_manager.py +452 -0
  24. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/decode_token.py +4 -2
  25. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/get_model_info.py +17 -6
  26. agent_api_server-2.1.14/agent_api_server/shared/loop_agent.py +1162 -0
  27. agent_api_server-2.1.14/agent_api_server/shared/markdown_fixer.py +469 -0
  28. agent_api_server-2.1.14/agent_api_server/shared/mcp_auth.py +458 -0
  29. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/message.py +9 -2
  30. agent_api_server-2.1.14/agent_api_server/shared/model_config.py +262 -0
  31. agent_api_server-2.1.14/agent_api_server/shared/model_manager_client.py +47 -0
  32. agent_api_server-2.1.14/agent_api_server/shared/request_context.py +80 -0
  33. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/sso_service/sdk/client.py +5 -4
  34. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/sso_service/sso_service.py +8 -7
  35. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/startup.py +3 -0
  36. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/pyproject.toml +3 -12
  37. agent_api_server-2.1.13/agent_api_server/.idea/.gitignore +0 -3
  38. agent_api_server-2.1.13/agent_api_server/.idea/agent_api_server.iml +0 -12
  39. agent_api_server-2.1.13/agent_api_server/.idea/inspectionProfiles/Project_Default.xml +0 -488
  40. agent_api_server-2.1.13/agent_api_server/.idea/inspectionProfiles/profiles_settings.xml +0 -6
  41. agent_api_server-2.1.13/agent_api_server/.idea/misc.xml +0 -4
  42. agent_api_server-2.1.13/agent_api_server/.idea/modules.xml +0 -8
  43. agent_api_server-2.1.13/agent_api_server/.idea/vcs.xml +0 -6
  44. agent_api_server-2.1.13/agent_api_server/.idea/workspace.xml +0 -50
  45. agent_api_server-2.1.13/agent_api_server/acp_bridge/go/agenthub-acp +0 -0
  46. agent_api_server-2.1.13/agent_api_server/shared/model_config.py +0 -203
  47. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/__init__.py +0 -0
  48. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/a2a_bridge/__init__.py +0 -0
  49. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/a2a_bridge/agent_card_builder.py +0 -0
  50. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/a2a_bridge/redis_task_store.py +0 -0
  51. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/a2a_bridge/server.py +0 -0
  52. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/acp_bridge/go/Makefile +0 -0
  53. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/acp_bridge/go/go.mod +0 -0
  54. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/acp_bridge/go/go.sum +0 -0
  55. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/agent_registration.py +0 -0
  56. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/api/__init__.py +0 -0
  57. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/api/v1/__init__.py +0 -0
  58. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/api/v1/api.py +0 -0
  59. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/api/v1/config.py +0 -0
  60. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/api/v1/graph.py +0 -0
  61. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/api/v1/schema.py +0 -0
  62. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/cache/__init__.py +0 -0
  63. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/cache/redis_cache.py +0 -0
  64. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/callback_handler.py +0 -0
  65. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/client/css/styles.css +0 -0
  66. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/client/favicon.ico +0 -0
  67. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/client/index.html +0 -0
  68. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/client/js/app.js +0 -0
  69. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/client/js/index.umd.js +0 -0
  70. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/configs/__init__.py +0 -0
  71. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/dynamic_llm/__init__.py +0 -0
  72. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/listener/__init__.py +0 -0
  73. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/listener/redis_listener.py +0 -0
  74. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/log/__init__.py +0 -0
  75. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/log/formatters.py +0 -0
  76. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/log/logging.json +0 -0
  77. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/mcp_convert/__init__.py +0 -0
  78. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/mcp_server.py +0 -0
  79. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/memory/__init__.py +0 -0
  80. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/memory/postgres.py +0 -0
  81. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/orchestrator/__init__.py +0 -0
  82. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/orchestrator/modes/__init__.py +0 -0
  83. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/orchestrator/types.py +0 -0
  84. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/register/__init__.py +0 -0
  85. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/service.py +0 -0
  86. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/service_hub/service_hub.py +0 -0
  87. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/service_hub/service_hub_test.py +0 -0
  88. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/services/__init__.py +0 -0
  89. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/__init__.py +0 -0
  90. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/ase.py +0 -0
  91. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/base_model.py +0 -0
  92. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/common.py +0 -0
  93. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/detect_message.py +0 -0
  94. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/exceptions.py +0 -0
  95. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/graph_loader.py +0 -0
  96. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/shared/util_func.py +0 -0
  97. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/sso_service/__init__.py +0 -0
  98. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/sso_service/sdk/__init__.py +0 -0
  99. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/sso_service/sdk/credential.py +0 -0
  100. {agent_api_server-2.1.13 → agent_api_server-2.1.14}/agent_api_server/sso_service/sdk/encoding.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: agent-api-server
3
- Version: 2.1.13
3
+ Version: 2.1.14
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
@@ -8,18 +8,14 @@ Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3.10
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
- Provides-Extra: a2a
12
11
  Requires-Dist: a2a-sdk[http-server] (==1.1.2)
13
- Requires-Dist: a2a-sdk[http-server] (==1.1.2) ; extra == "a2a"
14
12
  Requires-Dist: agent-client-protocol (>=0.11.0,<1.0.0)
15
- Requires-Dist: agent-client-protocol (>=0.11.0,<1.0.0) ; extra == "a2a"
16
13
  Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
17
14
  Requires-Dist: authlib (>=1.6.5)
18
15
  Requires-Dist: cryptography (>=45.0.4,<46.0.0)
19
16
  Requires-Dist: fastapi (>=0.117.0,<0.121.2)
20
17
  Requires-Dist: fastmcp (>=2.13.0,<3.0.0)
21
18
  Requires-Dist: httpx (>=0.27.0,<1.0.0)
22
- Requires-Dist: httpx (>=0.27.0,<1.0.0) ; extra == "a2a"
23
19
  Requires-Dist: langchain-community (==0.3.29)
24
20
  Requires-Dist: langchain-core (==0.3.76)
25
21
  Requires-Dist: langchain-openai (==0.3.33)
@@ -36,6 +32,7 @@ Requires-Dist: nats-py (>=2.11.0,<3.0.0)
36
32
  Requires-Dist: psycopg-binary (>=3.2.9,<4.0.0)
37
33
  Requires-Dist: psycopg-pool (>=3.2.6,<4.0.0)
38
34
  Requires-Dist: pydantic-settings (>=2.9.1,<3.0.0)
35
+ Requires-Dist: pytest (>=9.1.1,<10.0.0)
39
36
  Requires-Dist: redis (>=6.2.0,<7.0.0)
40
37
  Requires-Dist: starlette (>=0.49.3,<0.50.0)
41
38
  Requires-Dist: tenacity (>=9.1.2,<10.0.0)
@@ -616,13 +613,22 @@ ACP 采用**代理模式(Proxy Mode)**,CLI 进程作为编辑器与 FastAP
616
613
 
617
614
  ```bash
618
615
  # 从 PyPI 安装
619
- pip install agent-api-server
616
+ pip install agent-api-server==2.1.14
620
617
 
621
618
  # 或从本地 wheel 安装
622
- pip install dist/agent_api_server-2.1.9-py3-none-any.whl
619
+ pip install dist/agent_api_server-2.1.14-py3-none-any.whl
623
620
  ```
624
621
 
625
- 安装后 `agenthub-acp` 命令自动注册到 PATH。
622
+ `2.1.14` 使用本仓库的 LangGraph 0.6 依赖,并包含 WisePortal 组织适配。PyPI 上另有使用 LangGraph 1.x 的 `2.2.x` 版本,安装本版本时请指定版本号。
623
+
624
+ ACP 命令使用仓库中的 Go 实现,需要在源码目录单独构建,并将生成的程序放入 `PATH`:
625
+
626
+ ```bash
627
+ make -C agent_api_server/acp_bridge/go build
628
+ mkdir -p ~/.local/bin
629
+ install -m 755 agent_api_server/acp_bridge/go/agenthub-acp ~/.local/bin/agenthub-acp
630
+ export PATH="$HOME/.local/bin:$PATH"
631
+ ```
626
632
 
627
633
  **2. 启动 AgentHub 服务**
628
634
 
@@ -965,6 +971,73 @@ AGENT_AUTO_REGISTRATION=false
965
971
  ENABLE_A2A=false
966
972
  ```
967
973
 
974
+ ### WisePortal 配置
975
+
976
+ 服务启动时会从 `${CONFIG_CENTER_ADDRESS}/v1/public/configs`
977
+ 读取 `enableSrpPortal`,保存为 `global_config.isWisePortal`。布尔值 `true` 和
978
+ 字符串 `"true"`(忽略大小写和首尾空格)均开启该模式,其余值关闭。
979
+ `CONFIG_CENTER_ADDRESS` 默认是 `http://172.21.92.244:10086`,可通过环境变量覆盖。
980
+ 配置读取失败时记录警告并保留当前开关值,初始默认值为 `false`。
981
+ REST 与独立 MCP 服务都会在启动时读取配置;修改配置后需重启服务。
982
+
983
+ - 关闭时,REST 继续读取 Cookie `TSTenant`,MCP 继续读取 Header `TSTenant`。
984
+ - 开启时,优先读取 Header `Org-Str-Id`,其次 Header `orgStrId`、Cookie `orgStrId`,
985
+ 将该值作为 `TSTenant`。缺少组织字段时不会回退到旧租户字段。
986
+ - A2A 接收同样的 HTTP 组织字段,也支持 `metadata.orgStrId`。
987
+ - SDK 的 MCP 认证头、A2A、Model Manager、SSO 和配置中心客户端会在当前组织上下文中
988
+ 添加 `Org-Str-Id`。组织值不存入共享客户端默认 Header。
989
+ - Go ACP 可通过 `--org-str-id` 或 `ORG_STR_ID` 显式传入组织,代理到后端时携带组织 Header/Cookie。
990
+
991
+ 业务 Agent 自行创建 MCP/HTTP 客户端时,使用共享函数构建每次调用的 Header:
992
+
993
+ ```python
994
+ from agent_api_server.shared.request_context import outbound_headers
995
+
996
+ headers = outbound_headers({"Authorization": "Bearer ..."}, config=config)
997
+ # 开启 WisePortal 时:{"Authorization": "Bearer ...", "Org-Str-Id": "组织ID"}
998
+ ```
999
+
1000
+ `MCPAuthManager.get_headers(config)` 和 `get_headers_safe(config)` 也支持显式传入
1001
+ LangGraph 配置;在 SDK 执行的请求中,省略参数会自动使用当前请求的组织。
1002
+ 业务代码应按请求构建 Header,避免跨组织复用已生成的 Header 字典。
1003
+
1004
+ #### WisePortal 模型通知和缓存
1005
+
1006
+ 配置 `MODEL_MANAGER_NATS_URL` 并开启 WisePortal 后,每个 Agent 接收两类通知:
1007
+
1008
+ | Subject | 处理方式 |
1009
+ | --- | --- |
1010
+ | `model-management-service.model.agent.{agent_name}.{org_str_id}` | 更新或删除当前 Agent 对应工具的模型配置 |
1011
+ | `model-management-service.model.tenant.{org_str_id}` | 重新查询当前 Agent 的模型绑定,替换本地快照,并清空已删除的绑定 |
1012
+
1013
+ `org_str_id` 支持放在消息顶层或 `message` 内;存在时必须与 subject 的末尾一致。
1014
+ 没有 `org_str_id` 的旧消息使用 subject 末尾作为标识。`org_id` 仅作为消息元数据保留,
1015
+ 不会替代 `org_str_id`。模型回调仍使用 `ts_id` 参数,在 WisePortal 模式下该值为 `org_str_id`。
1016
+ 租户级通知中的通用凭证不会直接写入 Agent 缓存:SDK 会携带 `Org-Str-Id` 查询该 Agent
1017
+ 自己的模型配置,保存对应 Agent 注册 ID,用于解密模型凭证。
1018
+
1019
+ WisePortal 缓存路径为 `data/wiseportal_models/{org_str_id}/{agent_name}.env`,
1020
+ 其中配置键仍沿用 `CHAT_MODEL_{org_str_id}`、`CHAT_CREDENTIALS_{org_str_id}` 等格式。
1021
+ 缓存按组织和 Agent 隔离,写入采用文件锁和原子替换;不会将组织模型凭证写入共享进程环境。
1022
+ `DynamicLLM` 和模型信息查询会读取最新缓存,因此同一个执行配置也能在下次调用时看到更新或删除。
1023
+ 模型实例缓存同时区分组织和 Agent 注册 ID。
1024
+
1025
+ 首次切换时保留旧的 `data/.llm_cfg_env_{tenant_id}` 文件,新的缓存由通知或首次模型查询填充,
1026
+ 无需手动把旧租户 ID 改成组织 ID。关闭 WisePortal 后继续使用原有缓存路径和 Agent subject。
1027
+ 自行调用缓存接口的业务代码,应在 WisePortal 模式传入图名称:
1028
+
1029
+ ```python
1030
+ get_env(ts_tenant=org_str_id, agent_name=graph_name)
1031
+ set_model_config(..., ts_id=org_str_id, agent_name=graph_name)
1032
+ ```
1033
+
1034
+ 通知成功持久化后才 ACK;查询或写入失败会 NAK 请求重投,身份不一致或格式错误的消息会记录错误并终止重投。
1035
+ 两个 subject 对同一组织的处理会串行执行,防止较慢的租户级刷新覆盖后续 Agent 更新。
1036
+ 消费者配置更新保留已确认的位置;`ack_wait` 为 30 秒,最多投递 5 次。
1037
+
1038
+ 回归测试:`python -m pytest tests/test_model_notifications.py tests/test_wiseportal.py tests/test_markdown_fixer.py`。
1039
+ 可设置 `NATS_TEST_URL` 指向独立的 JetStream 测试实例,启用真实订阅、重投和重连测试。
1040
+
968
1041
  ### 定义 Agent
969
1042
 
970
1043
  创建 `langgraph.json` 配置文件:
@@ -572,13 +572,22 @@ ACP 采用**代理模式(Proxy Mode)**,CLI 进程作为编辑器与 FastAP
572
572
 
573
573
  ```bash
574
574
  # 从 PyPI 安装
575
- pip install agent-api-server
575
+ pip install agent-api-server==2.1.14
576
576
 
577
577
  # 或从本地 wheel 安装
578
- pip install dist/agent_api_server-2.1.9-py3-none-any.whl
578
+ pip install dist/agent_api_server-2.1.14-py3-none-any.whl
579
579
  ```
580
580
 
581
- 安装后 `agenthub-acp` 命令自动注册到 PATH。
581
+ `2.1.14` 使用本仓库的 LangGraph 0.6 依赖,并包含 WisePortal 组织适配。PyPI 上另有使用 LangGraph 1.x 的 `2.2.x` 版本,安装本版本时请指定版本号。
582
+
583
+ ACP 命令使用仓库中的 Go 实现,需要在源码目录单独构建,并将生成的程序放入 `PATH`:
584
+
585
+ ```bash
586
+ make -C agent_api_server/acp_bridge/go build
587
+ mkdir -p ~/.local/bin
588
+ install -m 755 agent_api_server/acp_bridge/go/agenthub-acp ~/.local/bin/agenthub-acp
589
+ export PATH="$HOME/.local/bin:$PATH"
590
+ ```
582
591
 
583
592
  **2. 启动 AgentHub 服务**
584
593
 
@@ -921,6 +930,73 @@ AGENT_AUTO_REGISTRATION=false
921
930
  ENABLE_A2A=false
922
931
  ```
923
932
 
933
+ ### WisePortal 配置
934
+
935
+ 服务启动时会从 `${CONFIG_CENTER_ADDRESS}/v1/public/configs`
936
+ 读取 `enableSrpPortal`,保存为 `global_config.isWisePortal`。布尔值 `true` 和
937
+ 字符串 `"true"`(忽略大小写和首尾空格)均开启该模式,其余值关闭。
938
+ `CONFIG_CENTER_ADDRESS` 默认是 `http://172.21.92.244:10086`,可通过环境变量覆盖。
939
+ 配置读取失败时记录警告并保留当前开关值,初始默认值为 `false`。
940
+ REST 与独立 MCP 服务都会在启动时读取配置;修改配置后需重启服务。
941
+
942
+ - 关闭时,REST 继续读取 Cookie `TSTenant`,MCP 继续读取 Header `TSTenant`。
943
+ - 开启时,优先读取 Header `Org-Str-Id`,其次 Header `orgStrId`、Cookie `orgStrId`,
944
+ 将该值作为 `TSTenant`。缺少组织字段时不会回退到旧租户字段。
945
+ - A2A 接收同样的 HTTP 组织字段,也支持 `metadata.orgStrId`。
946
+ - SDK 的 MCP 认证头、A2A、Model Manager、SSO 和配置中心客户端会在当前组织上下文中
947
+ 添加 `Org-Str-Id`。组织值不存入共享客户端默认 Header。
948
+ - Go ACP 可通过 `--org-str-id` 或 `ORG_STR_ID` 显式传入组织,代理到后端时携带组织 Header/Cookie。
949
+
950
+ 业务 Agent 自行创建 MCP/HTTP 客户端时,使用共享函数构建每次调用的 Header:
951
+
952
+ ```python
953
+ from agent_api_server.shared.request_context import outbound_headers
954
+
955
+ headers = outbound_headers({"Authorization": "Bearer ..."}, config=config)
956
+ # 开启 WisePortal 时:{"Authorization": "Bearer ...", "Org-Str-Id": "组织ID"}
957
+ ```
958
+
959
+ `MCPAuthManager.get_headers(config)` 和 `get_headers_safe(config)` 也支持显式传入
960
+ LangGraph 配置;在 SDK 执行的请求中,省略参数会自动使用当前请求的组织。
961
+ 业务代码应按请求构建 Header,避免跨组织复用已生成的 Header 字典。
962
+
963
+ #### WisePortal 模型通知和缓存
964
+
965
+ 配置 `MODEL_MANAGER_NATS_URL` 并开启 WisePortal 后,每个 Agent 接收两类通知:
966
+
967
+ | Subject | 处理方式 |
968
+ | --- | --- |
969
+ | `model-management-service.model.agent.{agent_name}.{org_str_id}` | 更新或删除当前 Agent 对应工具的模型配置 |
970
+ | `model-management-service.model.tenant.{org_str_id}` | 重新查询当前 Agent 的模型绑定,替换本地快照,并清空已删除的绑定 |
971
+
972
+ `org_str_id` 支持放在消息顶层或 `message` 内;存在时必须与 subject 的末尾一致。
973
+ 没有 `org_str_id` 的旧消息使用 subject 末尾作为标识。`org_id` 仅作为消息元数据保留,
974
+ 不会替代 `org_str_id`。模型回调仍使用 `ts_id` 参数,在 WisePortal 模式下该值为 `org_str_id`。
975
+ 租户级通知中的通用凭证不会直接写入 Agent 缓存:SDK 会携带 `Org-Str-Id` 查询该 Agent
976
+ 自己的模型配置,保存对应 Agent 注册 ID,用于解密模型凭证。
977
+
978
+ WisePortal 缓存路径为 `data/wiseportal_models/{org_str_id}/{agent_name}.env`,
979
+ 其中配置键仍沿用 `CHAT_MODEL_{org_str_id}`、`CHAT_CREDENTIALS_{org_str_id}` 等格式。
980
+ 缓存按组织和 Agent 隔离,写入采用文件锁和原子替换;不会将组织模型凭证写入共享进程环境。
981
+ `DynamicLLM` 和模型信息查询会读取最新缓存,因此同一个执行配置也能在下次调用时看到更新或删除。
982
+ 模型实例缓存同时区分组织和 Agent 注册 ID。
983
+
984
+ 首次切换时保留旧的 `data/.llm_cfg_env_{tenant_id}` 文件,新的缓存由通知或首次模型查询填充,
985
+ 无需手动把旧租户 ID 改成组织 ID。关闭 WisePortal 后继续使用原有缓存路径和 Agent subject。
986
+ 自行调用缓存接口的业务代码,应在 WisePortal 模式传入图名称:
987
+
988
+ ```python
989
+ get_env(ts_tenant=org_str_id, agent_name=graph_name)
990
+ set_model_config(..., ts_id=org_str_id, agent_name=graph_name)
991
+ ```
992
+
993
+ 通知成功持久化后才 ACK;查询或写入失败会 NAK 请求重投,身份不一致或格式错误的消息会记录错误并终止重投。
994
+ 两个 subject 对同一组织的处理会串行执行,防止较慢的租户级刷新覆盖后续 Agent 更新。
995
+ 消费者配置更新保留已确认的位置;`ack_wait` 为 30 秒,最多投递 5 次。
996
+
997
+ 回归测试:`python -m pytest tests/test_model_notifications.py tests/test_wiseportal.py tests/test_markdown_fixer.py`。
998
+ 可设置 `NATS_TEST_URL` 指向独立的 JetStream 测试实例,启用真实订阅、重投和重连测试。
999
+
924
1000
  ### 定义 Agent
925
1001
 
926
1002
  创建 `langgraph.json` 配置文件:
@@ -6,6 +6,10 @@ from uuid import uuid4
6
6
 
7
7
  import httpx
8
8
  from agent_api_server.shared.decode_token import decode_jwt
9
+ from agent_api_server.configs import global_config
10
+ from agent_api_server.shared.request_context import (
11
+ add_organization_header, organization_context, organization_id,
12
+ )
9
13
  from a2a.client.client import ClientConfig
10
14
  from a2a.client.client_factory import create_client
11
15
  from a2a.types import (
@@ -124,6 +128,7 @@ class AgentHubClient:
124
128
  if self._client is None:
125
129
  httpx_client = httpx.AsyncClient(
126
130
  timeout=httpx.Timeout(self._timeout, connect=10.0),
131
+ event_hooks={"request": [add_organization_header]},
127
132
  )
128
133
  client_config = ClientConfig(httpx_client=httpx_client)
129
134
 
@@ -162,6 +167,29 @@ class AgentHubClient:
162
167
  context_id: str | None = None,
163
168
  on_stream_chunk: Callable[[StreamResponse], None] | None = None,
164
169
  **extra_metadata,
170
+ ) -> A2AResult:
171
+ """Send a request with an organization scoped to this call, not the client."""
172
+ if global_config.isWisePortal:
173
+ tenant_id = extra_metadata.get("orgStrId") or organization_id() or tenant_id or ""
174
+ extra_metadata["orgStrId"] = tenant_id
175
+ with organization_context(tenant_id):
176
+ return await self._call(
177
+ raw_question=raw_question, tenant_id=tenant_id, ei_token=ei_token,
178
+ files=files, task_id=task_id, context_id=context_id,
179
+ on_stream_chunk=on_stream_chunk, **extra_metadata,
180
+ )
181
+
182
+ async def _call(
183
+ self,
184
+ *,
185
+ raw_question: str,
186
+ tenant_id: str = "",
187
+ ei_token: str = "",
188
+ files: list | None = None,
189
+ task_id: str | None = None,
190
+ context_id: str | None = None,
191
+ on_stream_chunk: Callable[[StreamResponse], None] | None = None,
192
+ **extra_metadata,
165
193
  ) -> A2AResult:
166
194
  """Send a message to the remote agent and consume the full event stream.
167
195
 
@@ -10,6 +10,8 @@ from a2a.server.events import EventQueue
10
10
  from a2a.server.tasks import TaskUpdater
11
11
  from a2a.types import Part, Message, Role, Task, TaskState, TaskStatus
12
12
  from agent_api_server.services.config_builder import build_configurable, build_langfuse_callbacks
13
+ from agent_api_server.configs import global_config
14
+ from agent_api_server.shared.request_context import organization_context, organization_id
13
15
 
14
16
  try:
15
17
  from langfuse import propagate_attributes
@@ -194,9 +196,10 @@ class LangGraphAgentExecutor(AgentExecutor):
194
196
 
195
197
  await updater.start_work()
196
198
 
197
- graph_task = asyncio.create_task(
198
- self._run_graph(graph, user_message, metadata, config, updater, is_continuation)
199
- )
199
+ with organization_context(config["configurable"].get("TSTenant")):
200
+ graph_task = asyncio.create_task(
201
+ self._run_graph(graph, user_message, metadata, config, updater, is_continuation)
202
+ )
200
203
  self._running_tasks[task_id] = graph_task
201
204
 
202
205
  try:
@@ -341,6 +344,8 @@ class LangGraphAgentExecutor(AgentExecutor):
341
344
  ) -> Dict[str, Any]:
342
345
  metadata = metadata or {}
343
346
  ts_tenant = metadata.get("tenant_id", "")
347
+ if global_config.isWisePortal:
348
+ ts_tenant = organization_id() or metadata.get("orgStrId", "")
344
349
 
345
350
  # Namespace thread_id by graph_name to isolate checkpoints
346
351
  # across multiple agents sharing the same PostgreSQL checkpointer.
@@ -28,7 +28,6 @@ func (e *HTTPError) Error() string {
28
28
  return fmt.Sprintf("Service error: %d", e.StatusCode)
29
29
  }
30
30
 
31
-
32
31
  // SessionState tracks an active ACP session.
33
32
  type SessionState struct {
34
33
  SessionID string `json:"session_id"`
@@ -50,6 +49,7 @@ type AgentHubACPAgent struct {
50
49
  serviceURL string
51
50
  agentName string
52
51
  tsTenant string
52
+ orgStrID string
53
53
  eiToken string
54
54
  agentVersion string
55
55
  agentDesc string
@@ -61,7 +61,7 @@ type AgentHubACPAgent struct {
61
61
  httpClient *http.Client
62
62
  sessions map[string]*SessionState
63
63
  pendingToolCalls map[string]map[string]*pendingToolCall // sessionID -> tcID -> info
64
- streamedNodes map[string]map[string]bool // sessionID -> set of nodes
64
+ streamedNodes map[string]map[string]bool // sessionID -> set of nodes
65
65
  }
66
66
 
67
67
  // NewAgentHubACPAgent creates a new agent instance.
@@ -81,7 +81,7 @@ func NewAgentHubACPAgent(
81
81
  agentDesc: agentDesc,
82
82
  userInputField: userInputField,
83
83
  extraInputs: extraInputs,
84
- streamMode: streamMode,
84
+ streamMode: streamMode,
85
85
  sessions: make(map[string]*SessionState),
86
86
  pendingToolCalls: make(map[string]map[string]*pendingToolCall),
87
87
  streamedNodes: make(map[string]map[string]bool),
@@ -95,6 +95,15 @@ func (a *AgentHubACPAgent) getHTTPClient() *http.Client {
95
95
  return a.httpClient
96
96
  }
97
97
 
98
+ // addOrganizationHeaders forwards an explicitly configured WisePortal identity.
99
+ // The backend's isWisePortal setting determines whether it replaces TSTenant.
100
+ func (a *AgentHubACPAgent) addOrganizationHeaders(req *http.Request) {
101
+ if a.orgStrID != "" {
102
+ req.Header.Set("Org-Str-Id", a.orgStrID)
103
+ req.AddCookie(&http.Cookie{Name: "orgStrId", Value: a.orgStrID})
104
+ }
105
+ }
106
+
98
107
  func (a *AgentHubACPAgent) doRequest(method, path string, body io.Reader, params url.Values) (*http.Response, error) {
99
108
  u := a.serviceURL + path
100
109
  if params != nil {
@@ -112,6 +121,7 @@ func (a *AgentHubACPAgent) doRequest(method, path string, body io.Reader, params
112
121
  if a.eiToken != "" {
113
122
  req.AddCookie(&http.Cookie{Name: "EIToken", Value: a.eiToken})
114
123
  }
124
+ a.addOrganizationHeaders(req)
115
125
  return a.httpClient.Do(req)
116
126
  }
117
127
 
@@ -181,6 +191,7 @@ func (a *AgentHubACPAgent) streamPrompt(sessionID, userText string) error {
181
191
  req.AddCookie(&http.Cookie{Name: "EIToken", Value: a.eiToken})
182
192
  }
183
193
 
194
+ a.addOrganizationHeaders(req)
184
195
  resp, err := a.httpClient.Do(req)
185
196
  if err != nil {
186
197
  return err
@@ -217,7 +228,6 @@ func (a *AgentHubACPAgent) streamPrompt(sessionID, userText string) error {
217
228
  return scanner.Err()
218
229
  }
219
230
 
220
-
221
231
  // --- Non-streaming (blocking run) ---
222
232
 
223
233
  func (a *AgentHubACPAgent) nonStreamPrompt(sessionID, userText string) error {
@@ -256,6 +266,7 @@ func (a *AgentHubACPAgent) nonStreamPrompt(sessionID, userText string) error {
256
266
  req.AddCookie(&http.Cookie{Name: "EIToken", Value: a.eiToken})
257
267
  }
258
268
 
269
+ a.addOrganizationHeaders(req)
259
270
  resp, err := a.httpClient.Do(req)
260
271
  if err != nil {
261
272
  return err
@@ -268,9 +279,9 @@ func (a *AgentHubACPAgent) nonStreamPrompt(sessionID, userText string) error {
268
279
  }
269
280
 
270
281
  var result struct {
271
- Status string `json:"status"`
282
+ Status string `json:"status"`
272
283
  ThreadID string `json:"thread_id"`
273
- Result struct {
284
+ Result struct {
274
285
  Type string `json:"type"`
275
286
  ContentType string `json:"content_type"`
276
287
  Content string `json:"content"`
@@ -457,14 +468,14 @@ func (a *AgentHubACPAgent) processSSEEvent(sessionID, raw string) {
457
468
  }
458
469
  // Send as tool_call (for tool list)
459
470
  a.sendStartToolCall(sessionID, tcID, title, nil, text, "completed")
460
-
471
+
461
472
  // Only send as agent_message_chunk if this node hasn't streamed tokens yet
462
473
  // (to avoid duplicate content in chat area)
463
474
  a.mu.Lock()
464
475
  streamed := a.streamedNodes[sessionID]
465
476
  hasStreamed := streamed != nil && streamed[node]
466
477
  a.mu.Unlock()
467
-
478
+
468
479
  if !hasStreamed {
469
480
  a.sendAgentMessage(sessionID, text)
470
481
  }
@@ -538,7 +549,7 @@ func truncate(s string, maxLen int) string {
538
549
  return s
539
550
  }
540
551
  return s[:maxLen]
541
- }// sendStartToolCallWithContent sends a tool call with content field (for tools_message).
552
+ } // sendStartToolCallWithContent sends a tool call with content field (for tools_message).
542
553
  func (a *AgentHubACPAgent) sendStartToolCallWithContent(sessionID, tcID, title, contentText string) {
543
554
  a.sendNotification(sessionID, "session/update", map[string]interface{}{
544
555
  "sessionId": sessionID,
@@ -558,5 +569,3 @@ func (a *AgentHubACPAgent) sendStartToolCallWithContent(sessionID, tcID, title,
558
569
  },
559
570
  })
560
571
  }
561
-
562
-
@@ -0,0 +1,46 @@
1
+ package main
2
+
3
+ import (
4
+ "net/http"
5
+ "net/http/httptest"
6
+ "testing"
7
+ )
8
+
9
+ func TestOrganizationForwarding(t *testing.T) {
10
+ for _, org := range []string{"", "org-a"} {
11
+ t.Run("organization="+org, func(t *testing.T) {
12
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13
+ if got := r.Header.Get("Org-Str-Id"); got != org {
14
+ t.Errorf("Org-Str-Id = %q, want %q", got, org)
15
+ }
16
+ cookie, err := r.Cookie("TSTenant")
17
+ if err != nil || cookie.Value != "legacy" {
18
+ t.Error("legacy tenant cookie was not preserved")
19
+ }
20
+ cookie, err = r.Cookie("orgStrId")
21
+ if org == "" {
22
+ if err != http.ErrNoCookie {
23
+ t.Error("unexpected orgStrId cookie in legacy request")
24
+ }
25
+ } else if err != nil || cookie.Value != org {
26
+ t.Error("missing organization cookie")
27
+ }
28
+ w.WriteHeader(http.StatusOK)
29
+ }))
30
+ defer server.Close()
31
+ agent := NewAgentHubACPAgent(server.URL, "agent", "legacy", "", "1", "", "user_input", nil, true)
32
+ agent.orgStrID = org
33
+ agent.sessions["session"] = &SessionState{ThreadID: "thread"}
34
+ response, err := agent.doRequest(http.MethodGet, "/probe", nil, nil)
35
+ if err != nil {
36
+ t.Fatal(err)
37
+ }
38
+ if err := response.Body.Close(); err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ if err := agent.streamPrompt("session", "hello"); err != nil {
42
+ t.Fatal(err)
43
+ }
44
+ })
45
+ }
46
+ }
@@ -27,6 +27,7 @@ func main() {
27
27
  serviceURL := flag.String("service-url", envOr("AGENT_SERVICE_URL", "http://localhost:8080"), "Base URL of the AgentHub service")
28
28
  agentName := flag.String("agent-name", envOr("AGENT_NAME", ""), "Agent (graph_name) to proxy")
29
29
  tsTenant := flag.String("ts-tenant", envOr("TS_TENANT", ""), "TSTenant auth cookie")
30
+ orgStrID := flag.String("org-str-id", envOr("ORG_STR_ID", ""), "WisePortal organization ID")
30
31
  eiToken := flag.String("ei-token", envOr("EI_TOKEN", ""), "EIToken auth cookie")
31
32
  agentVersion := flag.String("agent-version", envOr("AGENT_VERSION", "0.1.0"), "Agent version string")
32
33
  userInputField := flag.String("user-input-field", envOr("USER_INPUT_FIELD", "user_input"), "Field name for user input")
@@ -88,6 +89,7 @@ func main() {
88
89
  *serviceURL, *agentName, *tsTenant, *eiToken,
89
90
  *agentVersion, "", *userInputField, extraInputs, *streamMode,
90
91
  )
92
+ agent.orgStrID = *orgStrID
91
93
 
92
94
  if err := runStdioServer(agent); err != nil {
93
95
  log.Fatalf("Server error: %v", err)
@@ -112,6 +114,8 @@ Flags:
112
114
  (env: AGENT_NAME)
113
115
  -ts-tenant string
114
116
  TSTenant auth cookie (env: TS_TENANT)
117
+ -org-str-id string
118
+ WisePortal organization ID (env: ORG_STR_ID)
115
119
  -ei-token string
116
120
  EIToken auth cookie (env: EI_TOKEN)
117
121
  -agent-version string
@@ -300,7 +304,7 @@ func (a *AgentHubACPAgent) handleLoadSession(req jsonrpcRequest) *jsonrpcRespons
300
304
 
301
305
  func (a *AgentHubACPAgent) loadSessionHistory(sessionID, cwd string) {
302
306
  params := url.Values{
303
- "user_input_field": {a.userInputField},
307
+ "user_input_field": {a.userInputField},
304
308
  }
305
309
  resp, err := a.doRequest("GET", "/api/v1/thread/"+sessionID+"/history", nil, params)
306
310
  if err != nil {
@@ -315,7 +319,7 @@ func (a *AgentHubACPAgent) loadSessionHistory(sessionID, cwd string) {
315
319
  }
316
320
 
317
321
  var data struct {
318
- Messages []map[string]interface{} `json:"messages"`
322
+ Messages []map[string]interface{} `json:"messages"`
319
323
  }
320
324
  if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
321
325
  log.Printf("Failed to decode session history: %v", err)
@@ -327,7 +331,6 @@ func (a *AgentHubACPAgent) loadSessionHistory(sessionID, cwd string) {
327
331
  log.Printf("Loaded session %s with %d messages", sessionID, len(data.Messages))
328
332
  }
329
333
 
330
-
331
334
  // replayMessages replays session history using simple messages list.
332
335
  func (a *AgentHubACPAgent) replayMessages(sessionID string, messages []map[string]interface{}) {
333
336
  // Pre-scan: collect tool results by name for including in tool_call events
@@ -714,9 +717,9 @@ func cmdLoadSession(serviceURL, agentName, sessionID string, limit int, jsonOutp
714
717
  }
715
718
 
716
719
  var data struct {
717
- Messages []map[string]interface{} `json:"messages"`
718
- GraphName string `json:"graph_name"`
719
- Total int `json:"total"`
720
+ Messages []map[string]interface{} `json:"messages"`
721
+ GraphName string `json:"graph_name"`
722
+ Total int `json:"total"`
720
723
  }
721
724
  json.Unmarshal(body, &data)
722
725
 
@@ -24,6 +24,7 @@ from agent_api_server.shared.message import (
24
24
  from langgraph.pregel import Pregel
25
25
  from agent_api_server.services.config_builder import build_configurable
26
26
  from agent_api_server.services.stream_service import extract_user_id, execute_graph_stream
27
+ from agent_api_server.shared.request_context import request_tenant, organization_context
27
28
 
28
29
  logger = logging.getLogger(__name__)
29
30
  api_router = APIRouter()
@@ -239,7 +240,7 @@ async def stream(
239
240
  cookie_header = request.headers.get("cookie", "")
240
241
  cookie = SimpleCookie()
241
242
  cookie.load(cookie_header)
242
- ts_tenant = cookie.get("TSTenant").value if "TSTenant" in cookie else None
243
+ ts_tenant = request_tenant(request)
243
244
  ei_token = cookie.get("EIToken").value if "EIToken" in cookie else None
244
245
 
245
246
  state = await _get_thread_or_404(thread_id, get_graph_name)
@@ -316,7 +317,7 @@ async def run_thread(
316
317
  cookie_header = request.headers.get("cookie", "")
317
318
  cookie = SimpleCookie()
318
319
  cookie.load(cookie_header)
319
- ts_tenant = cookie.get("TSTenant").value if "TSTenant" in cookie else None
320
+ ts_tenant = request_tenant(request)
320
321
  ei_token = cookie.get("EIToken").value if "EIToken" in cookie else None
321
322
 
322
323
  storage = AsyncRedisThreadStorage.get_worker_instance()
@@ -434,13 +435,14 @@ async def _execute_graph_with_redis_check(
434
435
  else inputs or {}
435
436
  )
436
437
 
437
- execute_task = asyncio.create_task(
438
- graph_instance.ainvoke(
439
- invoke_input,
440
- config=run_cfg,
441
- stream_mode=["values"]
438
+ with organization_context(run_cfg.get("configurable", {}).get("TSTenant")):
439
+ execute_task = asyncio.create_task(
440
+ graph_instance.ainvoke(
441
+ invoke_input,
442
+ config=run_cfg,
443
+ stream_mode=["values"]
444
+ )
442
445
  )
443
- )
444
446
 
445
447
  while not execute_task.done():
446
448
  if await storage.should_stop(thread_id):
@@ -15,6 +15,8 @@ from agent_api_server.cache.redis_cache import AsyncRedisThreadStorage
15
15
  from agent_api_server.configs import global_config
16
16
  from agent_api_server.memory.postgres import AsyncPostgresCheckpointer
17
17
  from agent_api_server.shared.exceptions import AgentHubException
18
+ from agent_api_server.shared.request_context import OrganizationContextMiddleware
19
+ from agent_api_server.config_center.config_center import initialize_wiseportal_config
18
20
 
19
21
  import logging
20
22
 
@@ -72,6 +74,8 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
72
74
  cache = AsyncRedisThreadStorage.get_worker_instance()
73
75
 
74
76
  try:
77
+ await initialize_wiseportal_config()
78
+
75
79
  # 1. Infrastructure (always run, these are per-worker)
76
80
  await checkpointer.initialize()
77
81
  await cache.initialize()
@@ -134,6 +138,7 @@ async def _cleanup_resources():
134
138
  def create_fastapi_app() -> FastAPI:
135
139
  """Create and configure FastAPI application."""
136
140
  app = FastAPI(lifespan=lifespan)
141
+ app.add_middleware(OrganizationContextMiddleware)
137
142
 
138
143
  # Mount static files
139
144
  client_dir = Path(__file__).parent / "client"