You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_worker_runtime.py 20 kB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. import asyncio
  2. import logging
  3. import os
  4. from typing import Any, List
  5. import pytest
  6. from autogen_core import (
  7. PROTOBUF_DATA_CONTENT_TYPE,
  8. AgentId,
  9. AgentType,
  10. DefaultTopicId,
  11. MessageContext,
  12. RoutedAgent,
  13. Subscription,
  14. TopicId,
  15. TypeSubscription,
  16. default_subscription,
  17. event,
  18. try_get_known_serializers_for_type,
  19. type_subscription,
  20. )
  21. from autogen_ext.runtimes.grpc import GrpcWorkerAgentRuntime, GrpcWorkerAgentRuntimeHost
  22. from autogen_test_utils import (
  23. CascadingAgent,
  24. CascadingMessageType,
  25. ContentMessage,
  26. LoopbackAgent,
  27. LoopbackAgentWithDefaultSubscription,
  28. MessageType,
  29. NoopAgent,
  30. )
  31. from protos.serialization_test_pb2 import ProtoMessage
  32. @pytest.mark.asyncio
  33. async def test_agent_types_must_be_unique_single_worker() -> None:
  34. host_address = "localhost:50051"
  35. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  36. host.start()
  37. worker = GrpcWorkerAgentRuntime(host_address=host_address)
  38. worker.start()
  39. await worker.register_factory(type=AgentType("name1"), agent_factory=lambda: NoopAgent(), expected_class=NoopAgent)
  40. with pytest.raises(ValueError):
  41. await worker.register_factory(
  42. type=AgentType("name1"), agent_factory=lambda: NoopAgent(), expected_class=NoopAgent
  43. )
  44. await worker.register_factory(type=AgentType("name4"), agent_factory=lambda: NoopAgent(), expected_class=NoopAgent)
  45. await worker.stop()
  46. await host.stop()
  47. @pytest.mark.asyncio
  48. async def test_agent_types_must_be_unique_multiple_workers() -> None:
  49. host_address = "localhost:50052"
  50. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  51. host.start()
  52. worker1 = GrpcWorkerAgentRuntime(host_address=host_address)
  53. worker1.start()
  54. worker2 = GrpcWorkerAgentRuntime(host_address=host_address)
  55. worker2.start()
  56. await worker1.register_factory(type=AgentType("name1"), agent_factory=lambda: NoopAgent(), expected_class=NoopAgent)
  57. with pytest.raises(RuntimeError):
  58. await worker2.register_factory(
  59. type=AgentType("name1"), agent_factory=lambda: NoopAgent(), expected_class=NoopAgent
  60. )
  61. await worker2.register_factory(type=AgentType("name4"), agent_factory=lambda: NoopAgent(), expected_class=NoopAgent)
  62. await worker1.stop()
  63. await worker2.stop()
  64. await host.stop()
  65. @pytest.mark.asyncio
  66. async def test_register_receives_publish() -> None:
  67. host_address = "localhost:50053"
  68. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  69. host.start()
  70. worker1 = GrpcWorkerAgentRuntime(host_address=host_address)
  71. worker1.start()
  72. worker1.add_message_serializer(try_get_known_serializers_for_type(MessageType))
  73. await worker1.register_factory(
  74. type=AgentType("name1"), agent_factory=lambda: LoopbackAgent(), expected_class=LoopbackAgent
  75. )
  76. await worker1.add_subscription(TypeSubscription("default", "name1"))
  77. worker2 = GrpcWorkerAgentRuntime(host_address=host_address)
  78. worker2.start()
  79. worker2.add_message_serializer(try_get_known_serializers_for_type(MessageType))
  80. await worker2.register_factory(
  81. type=AgentType("name2"), agent_factory=lambda: LoopbackAgent(), expected_class=LoopbackAgent
  82. )
  83. await worker2.add_subscription(TypeSubscription("default", "name2"))
  84. # Publish message from worker1
  85. await worker1.publish_message(MessageType(), topic_id=TopicId("default", "default"))
  86. # Let the agent run for a bit.
  87. await asyncio.sleep(2)
  88. # Agents in default topic source should have received the message.
  89. worker1_agent = await worker1.try_get_underlying_agent_instance(AgentId("name1", "default"), LoopbackAgent)
  90. assert worker1_agent.num_calls == 1
  91. worker2_agent = await worker2.try_get_underlying_agent_instance(AgentId("name2", "default"), LoopbackAgent)
  92. assert worker2_agent.num_calls == 1
  93. # Agents in other topic source should not have received the message.
  94. worker1_agent = await worker1.try_get_underlying_agent_instance(AgentId("name1", "other"), LoopbackAgent)
  95. assert worker1_agent.num_calls == 0
  96. worker2_agent = await worker2.try_get_underlying_agent_instance(AgentId("name2", "other"), LoopbackAgent)
  97. assert worker2_agent.num_calls == 0
  98. await worker1.stop()
  99. await worker2.stop()
  100. await host.stop()
  101. @pytest.mark.asyncio
  102. async def test_register_receives_publish_cascade_single_worker() -> None:
  103. host_address = "localhost:50054"
  104. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  105. host.start()
  106. runtime = GrpcWorkerAgentRuntime(host_address=host_address)
  107. runtime.start()
  108. num_agents = 5
  109. num_initial_messages = 5
  110. max_rounds = 5
  111. total_num_calls_expected = 0
  112. for i in range(0, max_rounds):
  113. total_num_calls_expected += num_initial_messages * ((num_agents - 1) ** i)
  114. # Register agents
  115. for i in range(num_agents):
  116. await CascadingAgent.register(runtime, f"name{i}", lambda: CascadingAgent(max_rounds))
  117. # Publish messages
  118. for _ in range(num_initial_messages):
  119. await runtime.publish_message(CascadingMessageType(round=1), topic_id=DefaultTopicId())
  120. # Wait for all agents to finish.
  121. await asyncio.sleep(10)
  122. # Check that each agent received the correct number of messages.
  123. for i in range(num_agents):
  124. agent = await runtime.try_get_underlying_agent_instance(AgentId(f"name{i}", "default"), CascadingAgent)
  125. assert agent.num_calls == total_num_calls_expected
  126. await runtime.stop()
  127. await host.stop()
  128. @pytest.mark.skip(reason="Fix flakiness")
  129. @pytest.mark.asyncio
  130. async def test_register_receives_publish_cascade_multiple_workers() -> None:
  131. logging.basicConfig(level=logging.DEBUG)
  132. host_address = "localhost:50055"
  133. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  134. host.start()
  135. # TODO: Increasing num_initial_messages or max_round to 2 causes the test to fail.
  136. num_agents = 2
  137. num_initial_messages = 1
  138. max_rounds = 1
  139. total_num_calls_expected = 0
  140. for i in range(0, max_rounds):
  141. total_num_calls_expected += num_initial_messages * ((num_agents - 1) ** i)
  142. # Run multiple workers one for each agent.
  143. workers: List[GrpcWorkerAgentRuntime] = []
  144. # Register agents
  145. for i in range(num_agents):
  146. runtime = GrpcWorkerAgentRuntime(host_address=host_address)
  147. runtime.start()
  148. await CascadingAgent.register(runtime, f"name{i}", lambda: CascadingAgent(max_rounds))
  149. workers.append(runtime)
  150. # Publish messages
  151. publisher = GrpcWorkerAgentRuntime(host_address=host_address)
  152. publisher.add_message_serializer(try_get_known_serializers_for_type(CascadingMessageType))
  153. publisher.start()
  154. for _ in range(num_initial_messages):
  155. await publisher.publish_message(CascadingMessageType(round=1), topic_id=DefaultTopicId())
  156. await asyncio.sleep(20)
  157. # Check that each agent received the correct number of messages.
  158. for i in range(num_agents):
  159. agent = await workers[i].try_get_underlying_agent_instance(AgentId(f"name{i}", "default"), CascadingAgent)
  160. assert agent.num_calls == total_num_calls_expected
  161. for worker in workers:
  162. await worker.stop()
  163. await publisher.stop()
  164. await host.stop()
  165. @pytest.mark.asyncio
  166. async def test_default_subscription() -> None:
  167. host_address = "localhost:50056"
  168. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  169. host.start()
  170. worker = GrpcWorkerAgentRuntime(host_address=host_address)
  171. worker.start()
  172. publisher = GrpcWorkerAgentRuntime(host_address=host_address)
  173. publisher.add_message_serializer(try_get_known_serializers_for_type(MessageType))
  174. publisher.start()
  175. await LoopbackAgentWithDefaultSubscription.register(worker, "name", lambda: LoopbackAgentWithDefaultSubscription())
  176. await publisher.publish_message(MessageType(), topic_id=DefaultTopicId())
  177. await asyncio.sleep(2)
  178. # Agent in default topic source should have received the message.
  179. long_running_agent = await worker.try_get_underlying_agent_instance(
  180. AgentId("name", "default"), type=LoopbackAgentWithDefaultSubscription
  181. )
  182. assert long_running_agent.num_calls == 1
  183. # Agent in other topic source should not have received the message.
  184. other_long_running_agent = await worker.try_get_underlying_agent_instance(
  185. AgentId("name", key="other"), type=LoopbackAgentWithDefaultSubscription
  186. )
  187. assert other_long_running_agent.num_calls == 0
  188. await worker.stop()
  189. await publisher.stop()
  190. await host.stop()
  191. @pytest.mark.asyncio
  192. async def test_default_subscription_other_source() -> None:
  193. host_address = "localhost:50057"
  194. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  195. host.start()
  196. runtime = GrpcWorkerAgentRuntime(host_address=host_address)
  197. runtime.start()
  198. publisher = GrpcWorkerAgentRuntime(host_address=host_address)
  199. publisher.add_message_serializer(try_get_known_serializers_for_type(MessageType))
  200. publisher.start()
  201. await LoopbackAgentWithDefaultSubscription.register(runtime, "name", lambda: LoopbackAgentWithDefaultSubscription())
  202. await publisher.publish_message(MessageType(), topic_id=DefaultTopicId(source="other"))
  203. await asyncio.sleep(2)
  204. # Agent in default namespace should have received the message
  205. long_running_agent = await runtime.try_get_underlying_agent_instance(
  206. AgentId("name", "default"), type=LoopbackAgentWithDefaultSubscription
  207. )
  208. assert long_running_agent.num_calls == 0
  209. # Agent in other namespace should not have received the message
  210. other_long_running_agent = await runtime.try_get_underlying_agent_instance(
  211. AgentId("name", key="other"), type=LoopbackAgentWithDefaultSubscription
  212. )
  213. assert other_long_running_agent.num_calls == 1
  214. await runtime.stop()
  215. await publisher.stop()
  216. await host.stop()
  217. @pytest.mark.asyncio
  218. async def test_type_subscription() -> None:
  219. host_address = "localhost:50058"
  220. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  221. host.start()
  222. worker = GrpcWorkerAgentRuntime(host_address=host_address)
  223. worker.start()
  224. publisher = GrpcWorkerAgentRuntime(host_address=host_address)
  225. publisher.add_message_serializer(try_get_known_serializers_for_type(MessageType))
  226. publisher.start()
  227. @type_subscription("Other")
  228. class LoopbackAgentWithSubscription(LoopbackAgent): ...
  229. await LoopbackAgentWithSubscription.register(worker, "name", lambda: LoopbackAgentWithSubscription())
  230. await publisher.publish_message(MessageType(), topic_id=TopicId(type="Other", source="default"))
  231. await asyncio.sleep(2)
  232. # Agent in default topic source should have received the message.
  233. long_running_agent = await worker.try_get_underlying_agent_instance(
  234. AgentId("name", "default"), type=LoopbackAgentWithSubscription
  235. )
  236. assert long_running_agent.num_calls == 1
  237. # Agent in other topic source should not have received the message.
  238. other_long_running_agent = await worker.try_get_underlying_agent_instance(
  239. AgentId("name", key="other"), type=LoopbackAgentWithSubscription
  240. )
  241. assert other_long_running_agent.num_calls == 0
  242. await worker.stop()
  243. await publisher.stop()
  244. await host.stop()
  245. @pytest.mark.asyncio
  246. async def test_duplicate_subscription() -> None:
  247. host_address = "localhost:50059"
  248. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  249. worker1 = GrpcWorkerAgentRuntime(host_address=host_address)
  250. worker1_2 = GrpcWorkerAgentRuntime(host_address=host_address)
  251. host.start()
  252. try:
  253. worker1.start()
  254. await NoopAgent.register(worker1, "worker1", lambda: NoopAgent())
  255. worker1_2.start()
  256. # Note: This passes because worker1 is still running
  257. with pytest.raises(RuntimeError, match="Agent type worker1 already registered"):
  258. await NoopAgent.register(worker1_2, "worker1", lambda: NoopAgent())
  259. # This is somehow covered in test_disconnected_agent as well as a stop will also disconnect the agent.
  260. # Will keep them both for now as we might replace the way we simulate a disconnect
  261. await worker1.stop()
  262. with pytest.raises(ValueError):
  263. await NoopAgent.register(worker1_2, "worker1", lambda: NoopAgent())
  264. except Exception as ex:
  265. raise ex
  266. finally:
  267. await worker1_2.stop()
  268. await host.stop()
  269. @pytest.mark.asyncio
  270. async def test_disconnected_agent() -> None:
  271. host_address = "localhost:50060"
  272. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  273. host.start()
  274. worker1 = GrpcWorkerAgentRuntime(host_address=host_address)
  275. worker1_2 = GrpcWorkerAgentRuntime(host_address=host_address)
  276. # TODO: Implementing `get_current_subscriptions` and `get_subscribed_recipients` requires access
  277. # to some private properties. This needs to be updated once they are available publicly
  278. def get_current_subscriptions() -> List[Subscription]:
  279. return host._servicer._subscription_manager._subscriptions # type: ignore[reportPrivateUsage]
  280. async def get_subscribed_recipients() -> List[AgentId]:
  281. return await host._servicer._subscription_manager.get_subscribed_recipients(DefaultTopicId()) # type: ignore[reportPrivateUsage]
  282. try:
  283. worker1.start()
  284. await LoopbackAgentWithDefaultSubscription.register(
  285. worker1, "worker1", lambda: LoopbackAgentWithDefaultSubscription()
  286. )
  287. subscriptions1 = get_current_subscriptions()
  288. assert len(subscriptions1) == 2
  289. recipients1 = await get_subscribed_recipients()
  290. assert AgentId(type="worker1", key="default") in recipients1
  291. first_subscription_id = subscriptions1[0].id
  292. await worker1.publish_message(ContentMessage(content="Hello!"), DefaultTopicId())
  293. # This is a simple simulation of worker disconnct
  294. if worker1._host_connection is not None: # type: ignore[reportPrivateUsage]
  295. try:
  296. await worker1._host_connection.close() # type: ignore[reportPrivateUsage]
  297. except asyncio.CancelledError:
  298. pass
  299. await asyncio.sleep(1)
  300. subscriptions2 = get_current_subscriptions()
  301. assert len(subscriptions2) == 0
  302. recipients2 = await get_subscribed_recipients()
  303. assert len(recipients2) == 0
  304. await asyncio.sleep(1)
  305. worker1_2.start()
  306. await LoopbackAgentWithDefaultSubscription.register(
  307. worker1_2, "worker1", lambda: LoopbackAgentWithDefaultSubscription()
  308. )
  309. subscriptions3 = get_current_subscriptions()
  310. assert len(subscriptions3) == 2
  311. assert first_subscription_id not in [x.id for x in subscriptions3]
  312. recipients3 = await get_subscribed_recipients()
  313. assert len(set(recipients2)) == len(recipients2) # Make sure there are no duplicates
  314. assert AgentId(type="worker1", key="default") in recipients3
  315. except Exception as ex:
  316. raise ex
  317. finally:
  318. await worker1.stop()
  319. await worker1_2.stop()
  320. @default_subscription
  321. class ProtoReceivingAgent(RoutedAgent):
  322. def __init__(self) -> None:
  323. super().__init__("A loop back agent.")
  324. self.num_calls = 0
  325. self.received_messages: list[Any] = []
  326. @event
  327. async def on_new_message(self, message: ProtoMessage, ctx: MessageContext) -> None:
  328. self.num_calls += 1
  329. self.received_messages.append(message)
  330. @pytest.mark.asyncio
  331. async def test_proto_payloads() -> None:
  332. host_address = "localhost:50057"
  333. host = GrpcWorkerAgentRuntimeHost(address=host_address)
  334. host.start()
  335. receiver_runtime = GrpcWorkerAgentRuntime(
  336. host_address=host_address, payload_serialization_format=PROTOBUF_DATA_CONTENT_TYPE
  337. )
  338. receiver_runtime.start()
  339. publisher_runtime = GrpcWorkerAgentRuntime(
  340. host_address=host_address, payload_serialization_format=PROTOBUF_DATA_CONTENT_TYPE
  341. )
  342. publisher_runtime.add_message_serializer(try_get_known_serializers_for_type(ProtoMessage))
  343. publisher_runtime.start()
  344. await ProtoReceivingAgent.register(receiver_runtime, "name", ProtoReceivingAgent)
  345. await publisher_runtime.publish_message(ProtoMessage(message="Hello!"), topic_id=DefaultTopicId())
  346. await asyncio.sleep(2)
  347. # Agent in default namespace should have received the message
  348. long_running_agent = await receiver_runtime.try_get_underlying_agent_instance(
  349. AgentId("name", "default"), type=ProtoReceivingAgent
  350. )
  351. assert long_running_agent.num_calls == 1
  352. assert long_running_agent.received_messages[0].message == "Hello!"
  353. # Agent in other namespace should not have received the message
  354. other_long_running_agent = await receiver_runtime.try_get_underlying_agent_instance(
  355. AgentId("name", key="other"), type=ProtoReceivingAgent
  356. )
  357. assert other_long_running_agent.num_calls == 0
  358. assert len(other_long_running_agent.received_messages) == 0
  359. await receiver_runtime.stop()
  360. await publisher_runtime.stop()
  361. await host.stop()
  362. # TODO add tests for failure to deserialize
  363. @pytest.mark.asyncio
  364. async def test_grpc_max_message_size() -> None:
  365. default_max_size = 2**22
  366. new_max_size = default_max_size * 2
  367. small_message = ContentMessage(content="small message")
  368. big_message = ContentMessage(content="." * (default_max_size + 1))
  369. extra_grpc_config = [
  370. ("grpc.max_send_message_length", new_max_size),
  371. ("grpc.max_receive_message_length", new_max_size),
  372. ]
  373. host_address = "localhost:50061"
  374. host = GrpcWorkerAgentRuntimeHost(address=host_address, extra_grpc_config=extra_grpc_config)
  375. worker1 = GrpcWorkerAgentRuntime(host_address=host_address, extra_grpc_config=extra_grpc_config)
  376. worker2 = GrpcWorkerAgentRuntime(host_address=host_address)
  377. worker3 = GrpcWorkerAgentRuntime(host_address=host_address, extra_grpc_config=extra_grpc_config)
  378. try:
  379. host.start()
  380. worker1.start()
  381. worker2.start()
  382. worker3.start()
  383. await LoopbackAgentWithDefaultSubscription.register(
  384. worker1, "worker1", lambda: LoopbackAgentWithDefaultSubscription()
  385. )
  386. await LoopbackAgentWithDefaultSubscription.register(
  387. worker2, "worker2", lambda: LoopbackAgentWithDefaultSubscription()
  388. )
  389. await LoopbackAgentWithDefaultSubscription.register(
  390. worker3, "worker3", lambda: LoopbackAgentWithDefaultSubscription()
  391. )
  392. # with pytest.raises(Exception):
  393. await worker1.publish_message(small_message, DefaultTopicId())
  394. # This is a simple simulation of worker disconnct
  395. await asyncio.sleep(1)
  396. agent_instance_2 = await worker2.try_get_underlying_agent_instance(
  397. AgentId("worker2", key="default"), type=LoopbackAgent
  398. )
  399. agent_instance_3 = await worker3.try_get_underlying_agent_instance(
  400. AgentId("worker3", key="default"), type=LoopbackAgent
  401. )
  402. assert agent_instance_2.num_calls == 1
  403. assert agent_instance_3.num_calls == 1
  404. await worker1.publish_message(big_message, DefaultTopicId())
  405. await asyncio.sleep(2)
  406. assert agent_instance_2.num_calls == 1 # Worker 2 won't receive the big message
  407. assert agent_instance_3.num_calls == 2 # Worker 3 will receive the big message as has increased message length
  408. except Exception as e:
  409. raise e
  410. finally:
  411. await worker1.stop()
  412. # await worker2.stop() # Worker 2 somehow breaks can can not be stopped.
  413. await worker3.stop()
  414. await host.stop()
  415. if __name__ == "__main__":
  416. os.environ["GRPC_VERBOSITY"] = "DEBUG"
  417. os.environ["GRPC_TRACE"] = "all"
  418. asyncio.run(test_disconnected_agent())
  419. asyncio.run(test_grpc_max_message_size())