test_openclaw_agent.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import pytest
  4. from agents.models.openai_responses import OpenAIResponsesModel
  5. from agency_swarm import Agency, Agent, OpenClawAgent
  6. from agency_swarm.integrations.openclaw_model import (
  7. build_openclaw_responses_model,
  8. register_current_app_openclaw_defaults,
  9. )
  10. from agency_swarm.tools.send_message import Handoff
  11. from agency_swarm.utils.model_utils import (
  12. get_default_settings_model_name,
  13. get_model_name,
  14. get_usage_tracking_model_name,
  15. )
  16. from tests.integration.fastapi._openclaw_test_support import reset_openclaw_current_app_defaults
  17. def test_openclaw_agent_auto_builds_responses_model(monkeypatch: pytest.MonkeyPatch) -> None:
  18. monkeypatch.delenv("OPENCLAW_PROXY_BASE_URL", raising=False)
  19. monkeypatch.delenv("OPENCLAW_PROXY_PORT", raising=False)
  20. monkeypatch.delenv("PORT", raising=False)
  21. monkeypatch.setenv("OPENCLAW_PROVIDER_MODEL", "openai/gpt-5.4-mini")
  22. agent = OpenClawAgent(
  23. name="OpenClawWorker",
  24. description="Worker",
  25. instructions="Handle OpenClaw work.",
  26. )
  27. assert isinstance(agent.model, OpenAIResponsesModel)
  28. assert agent.model.model == "openclaw:main"
  29. assert get_model_name(agent.model) == "openclaw:main"
  30. assert get_usage_tracking_model_name(agent.model) == "openai/gpt-5.4-mini"
  31. assert str(agent.model._client.base_url) == "http://127.0.0.1:8000/openclaw/v1/"
  32. assert agent.supports_outbound_communication is False
  33. assert agent.model_settings.reasoning is not None
  34. assert agent.model_settings.reasoning.effort == "none"
  35. assert agent.model_settings.verbosity == "low"
  36. def test_openclaw_agent_supports_custom_host_port_and_path() -> None:
  37. with pytest.MonkeyPatch.context() as monkeypatch:
  38. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "http://env.example/openclaw/v1")
  39. agent = OpenClawAgent(
  40. name="OpenClawWorker",
  41. description="Worker",
  42. instructions="Handle OpenClaw work.",
  43. host="127.0.0.1",
  44. port=18080,
  45. api_path="/worker/v1",
  46. )
  47. assert str(agent.model._client.base_url) == "http://127.0.0.1:18080/worker/v1/"
  48. assert agent.model.model == "openclaw:main"
  49. @pytest.mark.parametrize(
  50. ("kwargs", "expected_url"),
  51. [
  52. ({"api_path": "/worker/v1"}, "https://proxy.example/worker/v1/"),
  53. ({"host": "worker.example"}, "https://worker.example/openclaw/v1/"),
  54. ({"port": 9443}, "https://proxy.example:9443/openclaw/v1/"),
  55. ],
  56. )
  57. def test_openclaw_agent_preserves_env_proxy_base_url_when_partially_overridden(
  58. monkeypatch: pytest.MonkeyPatch,
  59. kwargs: dict[str, str | int],
  60. expected_url: str,
  61. ) -> None:
  62. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "https://proxy.example/openclaw/v1")
  63. agent = OpenClawAgent(
  64. name="OpenClawWorker",
  65. description="Worker",
  66. instructions="Handle OpenClaw work.",
  67. api_key="external-token",
  68. **kwargs,
  69. )
  70. assert str(agent.model._client.base_url) == expected_url
  71. def test_openclaw_agent_defaults_external_v1_urls_to_provider_model(
  72. monkeypatch: pytest.MonkeyPatch,
  73. ) -> None:
  74. monkeypatch.setenv("OPENCLAW_PROVIDER_MODEL", "anthropic/claude-sonnet-4-5")
  75. agent = OpenClawAgent(
  76. name="OpenClawWorker",
  77. description="Worker",
  78. instructions="Handle OpenClaw work.",
  79. base_url="http://127.0.0.1:18789/v1",
  80. api_key="external-token",
  81. )
  82. assert str(agent.model._client.base_url) == "http://127.0.0.1:18789/v1/"
  83. assert agent.model.model == "anthropic/claude-sonnet-4-5"
  84. def test_build_openclaw_responses_model_defaults_raw_gateway_to_local_runtime_token(
  85. monkeypatch: pytest.MonkeyPatch,
  86. ) -> None:
  87. monkeypatch.delenv("OPENCLAW_GATEWAY_TOKEN", raising=False)
  88. monkeypatch.delenv("OPENCLAW_PROXY_API_KEY", raising=False)
  89. monkeypatch.delenv("APP_TOKEN", raising=False)
  90. model = build_openclaw_responses_model(base_url="http://127.0.0.1:18789/v1")
  91. assert model._client.api_key == "openclaw-local-token"
  92. @pytest.mark.parametrize(
  93. "base_url",
  94. [
  95. "https://example.com/openclaw/v1",
  96. "http://127.0.0.1:18789/v1",
  97. ],
  98. )
  99. def test_openclaw_agent_preserves_model_alias_override_for_external_servers(base_url: str) -> None:
  100. agent = OpenClawAgent(
  101. name="OpenClawWorker",
  102. description="Worker",
  103. instructions="Handle OpenClaw work.",
  104. base_url=base_url,
  105. api_key="external-token",
  106. model="openclaw:custom",
  107. )
  108. assert str(agent.model._client.base_url) == f"{base_url}/"
  109. assert agent.model.model == "openclaw:custom"
  110. def test_build_openclaw_responses_model_preserves_explicit_alias_for_direct_gateway_urls() -> None:
  111. model = build_openclaw_responses_model(
  112. model="openclaw:custom",
  113. base_url="http://127.0.0.1:18789/v1",
  114. api_key="external-token",
  115. )
  116. assert model.model == "openclaw:custom"
  117. assert get_usage_tracking_model_name(model) == "openclaw:custom"
  118. assert get_default_settings_model_name(model) == "openclaw:custom"
  119. def test_openclaw_agent_uses_gateway_token_before_app_token_for_direct_gateway_urls(
  120. monkeypatch: pytest.MonkeyPatch,
  121. ) -> None:
  122. monkeypatch.setenv("OPENCLAW_PROXY_API_KEY", "proxy-token")
  123. monkeypatch.setenv("APP_TOKEN", "app-token")
  124. monkeypatch.setenv("OPENCLAW_GATEWAY_TOKEN", "gateway-token")
  125. agent = OpenClawAgent(
  126. name="OpenClawWorker",
  127. description="Worker",
  128. instructions="Handle OpenClaw work.",
  129. base_url="http://127.0.0.1:18789/v1",
  130. )
  131. assert agent.model._client.api_key == "gateway-token"
  132. def test_openclaw_agent_keeps_app_token_first_for_local_proxy_urls(monkeypatch: pytest.MonkeyPatch) -> None:
  133. monkeypatch.delenv("OPENCLAW_PROXY_API_KEY", raising=False)
  134. monkeypatch.setenv("APP_TOKEN", "app-token")
  135. monkeypatch.setenv("OPENCLAW_GATEWAY_TOKEN", "gateway-token")
  136. agent = OpenClawAgent(
  137. name="OpenClawWorker",
  138. description="Worker",
  139. instructions="Handle OpenClaw work.",
  140. )
  141. assert agent.model._client.api_key == "app-token"
  142. def test_openclaw_agent_treats_localhost_proxy_aliases_as_current_app_proxy(
  143. monkeypatch: pytest.MonkeyPatch,
  144. ) -> None:
  145. monkeypatch.delenv("OPENCLAW_PROXY_API_KEY", raising=False)
  146. monkeypatch.delenv("OPENCLAW_PROVIDER_MODEL", raising=False)
  147. monkeypatch.setenv("APP_TOKEN", "app-token")
  148. monkeypatch.delenv("OPENCLAW_GATEWAY_TOKEN", raising=False)
  149. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "http://127.0.0.1:8000/openclaw/v1")
  150. reset_openclaw_current_app_defaults(monkeypatch)
  151. model = build_openclaw_responses_model(base_url="http://localhost:8000/openclaw/v1")
  152. assert model._client.api_key == "app-token"
  153. assert get_usage_tracking_model_name(model) == "openai/gpt-5.4"
  154. def test_openclaw_agent_uses_app_token_for_explicit_same_app_proxy_url_when_one_current_app_proxy_is_registered(
  155. monkeypatch: pytest.MonkeyPatch,
  156. ) -> None:
  157. monkeypatch.setenv("OPENCLAW_PROXY_API_KEY", "proxy-token")
  158. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "http://127.0.0.1:9000/openclaw/v1")
  159. monkeypatch.setenv("APP_TOKEN", "app-token")
  160. monkeypatch.setenv("OPENCLAW_GATEWAY_TOKEN", "gateway-token")
  161. reset_openclaw_current_app_defaults(monkeypatch)
  162. register_current_app_openclaw_defaults(
  163. default_model="openclaw:custom",
  164. provider_model="openai/gpt-5.4-mini",
  165. base_url="http://127.0.0.1:9000/openclaw/v1",
  166. )
  167. agent = OpenClawAgent(
  168. name="OpenClawWorker",
  169. description="Worker",
  170. instructions="Handle OpenClaw work.",
  171. host="127.0.0.1",
  172. port=9000,
  173. )
  174. assert agent.model.model == "openclaw:custom"
  175. assert agent.model._client.api_key == "app-token"
  176. def test_openclaw_agent_rejects_manual_handoffs() -> None:
  177. recipient = Agent(
  178. name="Recipient",
  179. description="Recipient",
  180. instructions="Return the result.",
  181. model="gpt-5.4-mini",
  182. )
  183. with pytest.raises(TypeError, match="does not accept manual handoffs"):
  184. OpenClawAgent(
  185. name="OpenClawWorker",
  186. description="Worker",
  187. instructions="Handle OpenClaw work.",
  188. handoffs=[Handoff().create_handoff(recipient)],
  189. )
  190. def test_openclaw_agent_rejects_framework_tool_wiring() -> None:
  191. with pytest.raises(TypeError, match="does not accept Agency Swarm tool wiring"):
  192. OpenClawAgent(
  193. name="OpenClawWorker",
  194. description="Worker",
  195. instructions="Handle OpenClaw work.",
  196. tools=[object()],
  197. )
  198. def test_openclaw_agent_rejects_files_folder_tool_wiring() -> None:
  199. with pytest.raises(TypeError, match="does not accept Agency Swarm tool wiring"):
  200. OpenClawAgent(
  201. name="OpenClawWorker",
  202. description="Worker",
  203. instructions="Handle OpenClaw work.",
  204. files_folder="./files",
  205. )
  206. def test_openclaw_agent_rejects_manual_communication_capability_overrides() -> None:
  207. with pytest.raises(TypeError, match="always receive-only"):
  208. OpenClawAgent(
  209. name="OpenClawWorker",
  210. description="Worker",
  211. instructions="Handle OpenClaw work.",
  212. supports_outbound_communication=True,
  213. )
  214. def test_openclaw_agent_skips_shared_tool_wiring() -> None:
  215. openclaw_worker = OpenClawAgent(
  216. name="OpenClawWorker",
  217. description="Worker",
  218. instructions="Handle OpenClaw work.",
  219. )
  220. coordinator = Agent(
  221. name="Coordinator",
  222. description="Coordinator",
  223. instructions="Coordinate the work.",
  224. model="gpt-5.4-mini",
  225. )
  226. agency = Agency(
  227. coordinator,
  228. communication_flows=[(coordinator, openclaw_worker)],
  229. shared_tools=[object()],
  230. )
  231. assert agency.agents["OpenClawWorker"].supports_framework_tool_wiring is False
  232. def test_openclaw_agent_skips_shared_file_preprocessing_when_no_agent_supports_it(
  233. monkeypatch: pytest.MonkeyPatch,
  234. tmp_path: Path,
  235. ) -> None:
  236. monkeypatch.delenv("OPENAI_API_KEY", raising=False)
  237. shared_files = tmp_path / "shared-files"
  238. shared_files.mkdir()
  239. (shared_files / "notes.txt").write_text("hello", encoding="utf-8")
  240. openclaw_worker = OpenClawAgent(
  241. name="OpenClawWorker",
  242. description="Worker",
  243. instructions="Handle OpenClaw work.",
  244. )
  245. agency = Agency(openclaw_worker, shared_files_folder=str(shared_files))
  246. assert agency.agents["OpenClawWorker"].supports_framework_tool_wiring is False
  247. def test_openclaw_agent_cannot_register_subagent() -> None:
  248. agent = OpenClawAgent(
  249. name="OpenClawWorker",
  250. description="Worker",
  251. instructions="Handle OpenClaw work.",
  252. )
  253. recipient = Agent(
  254. name="Recipient",
  255. description="Recipient",
  256. instructions="Return the result.",
  257. model="gpt-5.4-mini",
  258. )
  259. with pytest.raises(ValueError, match="cannot register subagents because it is configured as receive-only"):
  260. agent.register_subagent(recipient)
  261. def test_openclaw_agent_cannot_be_sender_in_communication_flows() -> None:
  262. openclaw_worker = OpenClawAgent(
  263. name="OpenClawWorker",
  264. description="Worker",
  265. instructions="Handle OpenClaw work.",
  266. )
  267. specialist = Agent(
  268. name="Specialist",
  269. description="Specialist",
  270. instructions="Return the result.",
  271. model="gpt-5.4-mini",
  272. )
  273. with pytest.raises(ValueError, match="cannot be the sender in communication_flows"):
  274. Agency(openclaw_worker, communication_flows=[(openclaw_worker, specialist)])