diff --git a/docs/wayflowcore/source/core/code_examples/howto_a2aagent.py b/docs/wayflowcore/source/core/code_examples/howto_a2aagent.py index 21d9f54b4..956737b60 100644 --- a/docs/wayflowcore/source/core/code_examples/howto_a2aagent.py +++ b/docs/wayflowcore/source/core/code_examples/howto_a2aagent.py @@ -9,6 +9,8 @@ # mypy: ignore-errors # docs-title: Code Example - How to Use A2A Agents +### Basic Usage + # .. start-##_Creating_the_agent from wayflowcore.a2a.a2aagent import A2AAgent, A2AConnectionConfig @@ -35,13 +37,159 @@ print(f"Invalid execution status, expected UserMessageRequestStatus, received {type(status)}") # .. end-##_Running_the_agent -# .. start-##_Export_config_to_Agent_Spec +# .. start-##_Export_config_to_Agent_Spec1 from wayflowcore.agentspec import AgentSpecExporter serialized_assistant = AgentSpecExporter().to_json(agent) -# .. end-##_Export_config_to_Agent_Spec -# .. start-##_Load_Agent_Spec_config +# .. end-##_Export_config_to_Agent_Spec1 +# .. start-##_Load_Agent_Spec_config1 +from wayflowcore.agentspec import AgentSpecLoader + +agent: A2AAgent = AgentSpecLoader().load_json(serialized_assistant) +# .. end-##_Load_Agent_Spec_config1 + +### Manager Workers Usage + +# Server part +import random +from typing import Annotated + +from wayflowcore.agentserver.server import A2AServer +from wayflowcore.agent import Agent +from wayflowcore.models import VllmModel +from wayflowcore.tools import tool + +# .. start-##_llm +llm = VllmModel( + model_id="LLAMA_MODEL_ID", + host_port="LLAMA_API_URL", +) +# .. end-##_llm + +(llm,) = _update_globals(["vision_llm"]) # docs-skiprow # type: ignore + + +# .. start-##_Server_Setup_Prime_Agent +def get_prime_agent(): + @tool + def check_prime(a: Annotated[int, "first required integer"]) -> bool: + "Check if the first number (a) is a prime number." + if a < 2: + return False + for i in range(2, int(a**0.5) + 1): + if a % i == 0: + return False + return True + agent = Agent( + llm=llm, + name="prime_agent", + custom_instruction="You are a math agent that can check whether a number is prime or not using the equipped tool.", + tools=[check_prime], + can_finish_conversation=True, + ) + return agent +# .. end-##_Server_Setup_Prime_Agent + + +# .. start-##_Server_Setup_Sample_Agent +def get_sample_agent(): + @tool + def sample_number(a: Annotated[int, "first required integer"]) -> int: + "Simulate sampling from a range, return a random number between 1 and the specified value." + result = random.randint(1, a) # nosec + return result + agent = Agent( + llm=llm, + name="sample_agent", + custom_instruction="You are an agent that can generate a random number between 1 and a specified value.", + tools=[sample_number], + can_finish_conversation=True, + ) + return agent +# .. end-##_Server_Setup_Sample_Agent + +# .. start-##_Server_Startup_Logic +# Start both sample and prime servers +sample_agent = get_sample_agent() +sample_server = A2AServer() +sample_server.serve_agent(sample_agent, "http://") +# Note: Uncomment the line below to start the server +# sample_server.serve() + +prime_agent = get_prime_agent() +prime_server = A2AServer() +prime_server.serve_agent(prime_agent, "http://") +# Note: Uncomment the line below to start the server +# prime_server.serve(port=8001) +# .. end-##_Server_Startup_Logic + +(sample_server,) = _update_globals(["sample_a2a_server"]) # docs-skiprow # type: ignore +(prime_server,) = _update_globals(["prime_a2a_server"]) # docs-skiprow # type: ignore + +# Client part +from wayflowcore.a2a.a2aagent import A2AAgent, A2AConnectionConfig +from wayflowcore.agent import Agent +from wayflowcore.models import VllmModel + +# .. start-##_Client_Setup +sample_agent = A2AAgent( + name="sample_agent", + agent_url="http://", + description="Agent that can generate random numbers", + connection_config=A2AConnectionConfig(verify=False), +) +prime_agent = A2AAgent( + name="prime_agent", + agent_url="http://", + description="Agent that handles checking if numbers are prime.", + connection_config=A2AConnectionConfig(verify=False), +) +# .. end-##_Client_Setup + +(sample_agent, prime_agent) = _update_globals( + ["sample_a2a_agent", "prime_a2a_agent"] +) # docs-skiprow # type: ignore + +# .. start-##_Manager_Setup +MANAGER_SYSTEM_PROMPT = """ +You are a helpful assistant that can sample numbers and check if the sampled numbers are prime. +You delegate sampling tasks to the `sample_agent` and prime checking tasks to the `prime_agent`. +Follow these steps: +1. If the user asks to sample a number, delegate to the `sample_agent`. +2. If the user asks to check primes, delegate to the `prime_agent`. +3. If the user asks to sample a number and then check if the result is prime, call `sample_agent` first, then pass the result to `prime_agent`. +Always clarify the results before proceeding. +""".strip() + +manager = Agent( + name="PrimeChecker", + description="You are a PrimeChecker who can sample integers and check if they are prime.", + llm=llm, + custom_instruction=MANAGER_SYSTEM_PROMPT, +) +# .. end-##_Manager_Setup + +# .. start-##_ManagerWorkers_Execution +from wayflowcore.managerworkers import ManagerWorkers + +group = ManagerWorkers( + group_manager=manager, + workers=[sample_agent, prime_agent], +) + +main_conversation = group.start_conversation() +main_conversation.append_user_message("Sample a number from 1 to 20 and check if it's prime") +main_conversation.execute() +print(main_conversation.get_messages()) +# .. end-##_ManagerWorkers_Execution + +# .. start-##_Export_config_to_Agent_Spec2 +from wayflowcore.agentspec import AgentSpecExporter + +serialized_assistant = AgentSpecExporter().to_json(group) +# .. end-##_Export_config_to_Agent_Spec2 +# .. start-##_Load_Agent_Spec_config2 from wayflowcore.agentspec import AgentSpecLoader agent: A2AAgent = AgentSpecLoader().load_json(serialized_assistant) -# .. end-##_Load_Agent_Spec_config +# .. end-##_Load_Agent_Spec_config2 diff --git a/docs/wayflowcore/source/core/config_examples/howto_a2aagent.json b/docs/wayflowcore/source/core/config_examples/howto_a2aagent_1.json similarity index 77% rename from docs/wayflowcore/source/core/config_examples/howto_a2aagent.json rename to docs/wayflowcore/source/core/config_examples/howto_a2aagent_1.json index 41c11f5c5..d96ebc927 100644 --- a/docs/wayflowcore/source/core/config_examples/howto_a2aagent.json +++ b/docs/wayflowcore/source/core/config_examples/howto_a2aagent_1.json @@ -1,7 +1,7 @@ { "component_type": "A2AAgent", - "id": "5ba508ef-7cae-417a-9197-8f2aeee870b6", - "name": "a2a_agent_4311ebd5__auto", + "id": "47b982ac-dbbe-4674-8c97-f6d82a21554c", + "name": "a2a_agent_a5ea926f__auto", "description": "", "metadata": { "__metadata_info__": {} @@ -11,8 +11,8 @@ "agent_url": "http://", "connection_config": { "component_type": "A2AConnectionConfig", - "id": "c01ebe14-f467-4c01-a25b-0f3f22bfa550", - "name": "connection_config", + "id": "1912133f-0bc6-4846-a140-2a5bfb5f9c42", + "name": "", "description": null, "metadata": {}, "timeout": 600.0, diff --git a/docs/wayflowcore/source/core/config_examples/howto_a2aagent.yaml b/docs/wayflowcore/source/core/config_examples/howto_a2aagent_1.yaml similarity index 74% rename from docs/wayflowcore/source/core/config_examples/howto_a2aagent.yaml rename to docs/wayflowcore/source/core/config_examples/howto_a2aagent_1.yaml index 58ea22406..9181af24a 100644 --- a/docs/wayflowcore/source/core/config_examples/howto_a2aagent.yaml +++ b/docs/wayflowcore/source/core/config_examples/howto_a2aagent_1.yaml @@ -1,6 +1,6 @@ component_type: A2AAgent -id: 5ba508ef-7cae-417a-9197-8f2aeee870b6 -name: a2a_agent_4311ebd5__auto +id: 47b982ac-dbbe-4674-8c97-f6d82a21554c +name: a2a_agent_a5ea926f__auto description: '' metadata: __metadata_info__: {} @@ -9,8 +9,8 @@ outputs: [] agent_url: http:// connection_config: component_type: A2AConnectionConfig - id: 7d7a7c03-616b-4740-bd8e-ebcfed45092c - name: connection_config + id: 1912133f-0bc6-4846-a140-2a5bfb5f9c42 + name: '' description: null metadata: {} timeout: 600.0 diff --git a/docs/wayflowcore/source/core/config_examples/howto_a2aagent_2.json b/docs/wayflowcore/source/core/config_examples/howto_a2aagent_2.json new file mode 100644 index 000000000..30b61391e --- /dev/null +++ b/docs/wayflowcore/source/core/config_examples/howto_a2aagent_2.json @@ -0,0 +1,213 @@ +{ + "component_type": "ManagerWorkers", + "id": "21f138e2-c8c7-4373-af60-508cf0c1fd2e", + "name": "managerworkers_44603868__auto", + "description": "", + "metadata": { + "__metadata_info__": {} + }, + "inputs": [], + "outputs": [], + "group_manager": { + "component_type": "ExtendedAgent", + "id": "02709a13-1916-4720-89e1-56b0c0ea6922", + "name": "PrimeChecker", + "description": "You are a PrimeChecker who can sample integers and check if they are prime.", + "metadata": { + "__metadata_info__": {} + }, + "inputs": [], + "outputs": [], + "llm_config": { + "component_type": "VllmConfig", + "id": "91a8480a-3d6b-4160-b4a1-e1cd57b7c9a4", + "name": "llm_647ebd5e__auto", + "description": null, + "metadata": { + "__metadata_info__": {} + }, + "default_generation_parameters": null, + "url": "LLAMA_API_URL", + "model_id": "LLAMA_MODEL_ID", + "api_type": "chat_completions", + "api_key": null + }, + "system_prompt": "You are a helpful assistant that can sample numbers and check if the sampled numbers are prime.\nYou delegate sampling tasks to the `sample_agent` and prime checking tasks to the `prime_agent`.\nFollow these steps:\n1. If the user asks to sample a number, delegate to the `sample_agent`.\n2. If the user asks to check primes, delegate to the `prime_agent`.\n3. If the user asks to sample a number and then check if the result is prime, call `sample_agent` first, then pass the result to `prime_agent`.\nAlways clarify the results before proceeding.", + "tools": [], + "toolboxes": [], + "human_in_the_loop": true, + "context_providers": null, + "can_finish_conversation": false, + "raise_exceptions": false, + "max_iterations": 10, + "initial_message": "Hi! How can I help you?", + "caller_input_mode": "always", + "agents": [], + "flows": [], + "agent_template": { + "component_type": "PluginPromptTemplate", + "id": "23e82423-7df6-461e-a45b-31a6633348fe", + "name": "", + "description": null, + "metadata": { + "__metadata_info__": {} + }, + "messages": [ + { + "role": "system", + "contents": [ + { + "type": "text", + "content": "{% if custom_instruction %}{{custom_instruction}}{% endif %}" + } + ], + "tool_requests": null, + "tool_result": null, + "display_only": false, + "sender": null, + "recipients": [], + "time_created": "2026-01-14T14:27:15.835239+00:00", + "time_updated": "2026-01-14T14:27:15.835239+00:00" + }, + { + "role": "system", + "contents": [ + { + "type": "text", + "content": "$$__CHAT_HISTORY_PLACEHOLDER__$$" + } + ], + "tool_requests": null, + "tool_result": null, + "display_only": false, + "sender": null, + "recipients": [], + "time_created": "2026-01-14T14:27:15.831100+00:00", + "time_updated": "2026-01-14T14:27:15.831101+00:00" + }, + { + "role": "system", + "contents": [ + { + "type": "text", + "content": "{% if __PLAN__ %}The current plan you should follow is the following: \n{{__PLAN__}}{% endif %}" + } + ], + "tool_requests": null, + "tool_result": null, + "display_only": false, + "sender": null, + "recipients": [], + "time_created": "2026-01-14T14:27:15.835267+00:00", + "time_updated": "2026-01-14T14:27:15.835267+00:00" + } + ], + "output_parser": null, + "inputs": [ + { + "description": "\"custom_instruction\" input variable for the template", + "type": "string", + "title": "custom_instruction", + "default": "" + }, + { + "description": "\"__PLAN__\" input variable for the template", + "type": "string", + "title": "__PLAN__", + "default": "" + }, + { + "type": "array", + "items": {}, + "title": "__CHAT_HISTORY__" + } + ], + "pre_rendering_transforms": null, + "post_rendering_transforms": [ + { + "component_type": "PluginRemoveEmptyNonUserMessageTransform", + "id": "ffeb49dd-9a2e-485b-9ee3-c764e6340098", + "name": "removeemptynonusermessage_messagetransform", + "description": null, + "metadata": { + "__metadata_info__": {} + }, + "component_plugin_name": "MessageTransformPlugin", + "component_plugin_version": "26.1.0.dev5" + } + ], + "tools": null, + "native_tool_calling": true, + "response_format": null, + "native_structured_generation": true, + "generation_config": null, + "component_plugin_name": "PromptTemplatePlugin", + "component_plugin_version": "26.1.0.dev5" + }, + "component_plugin_name": "AgentPlugin", + "component_plugin_version": "26.1.0.dev5" + }, + "workers": [ + { + "component_type": "A2AAgent", + "id": "b554446e-9c21-4aaf-b393-1e91cc9aeab8", + "name": "sample_agent", + "description": "Agent that can generate random numbers", + "metadata": { + "__metadata_info__": {} + }, + "inputs": [], + "outputs": [], + "agent_url": "http://", + "connection_config": { + "component_type": "A2AConnectionConfig", + "id": "061d0a87-cfec-49e4-a427-fec4b4326724", + "name": "", + "description": null, + "metadata": {}, + "timeout": 600.0, + "headers": null, + "verify": false, + "key_file": null, + "cert_file": null, + "ssl_ca_cert": null + }, + "session_parameters": { + "timeout": 60.0, + "poll_interval": 2.0, + "max_retries": 5 + } + }, + { + "component_type": "A2AAgent", + "id": "d6e6a95b-c144-469b-91ad-972121406b83", + "name": "prime_agent", + "description": "Agent that handles checking if numbers are prime.", + "metadata": { + "__metadata_info__": {} + }, + "inputs": [], + "outputs": [], + "agent_url": "http://", + "connection_config": { + "component_type": "A2AConnectionConfig", + "id": "54c3ebde-5101-4c4c-9718-bced0142754c", + "name": "", + "description": null, + "metadata": {}, + "timeout": 600.0, + "headers": null, + "verify": false, + "key_file": null, + "cert_file": null, + "ssl_ca_cert": null + }, + "session_parameters": { + "timeout": 60.0, + "poll_interval": 2.0, + "max_retries": 5 + } + } + ], + "agentspec_version": "25.4.2" +} diff --git a/docs/wayflowcore/source/core/config_examples/howto_a2aagent_2.yaml b/docs/wayflowcore/source/core/config_examples/howto_a2aagent_2.yaml new file mode 100644 index 000000000..122ac4d01 --- /dev/null +++ b/docs/wayflowcore/source/core/config_examples/howto_a2aagent_2.yaml @@ -0,0 +1,183 @@ +component_type: ManagerWorkers +id: 21f138e2-c8c7-4373-af60-508cf0c1fd2e +name: managerworkers_44603868__auto +description: '' +metadata: + __metadata_info__: {} +inputs: [] +outputs: [] +group_manager: + component_type: ExtendedAgent + id: 02709a13-1916-4720-89e1-56b0c0ea6922 + name: PrimeChecker + description: You are a PrimeChecker who can sample integers and check if they are + prime. + metadata: + __metadata_info__: {} + inputs: [] + outputs: [] + llm_config: + component_type: VllmConfig + id: 91a8480a-3d6b-4160-b4a1-e1cd57b7c9a4 + name: llm_647ebd5e__auto + description: null + metadata: + __metadata_info__: {} + default_generation_parameters: null + url: LLAMA_API_URL + model_id: LLAMA_MODEL_ID + api_type: chat_completions + api_key: null + system_prompt: 'You are a helpful assistant that can sample numbers and check if + the sampled numbers are prime. + + You delegate sampling tasks to the `sample_agent` and prime checking tasks to + the `prime_agent`. + + Follow these steps: + + 1. If the user asks to sample a number, delegate to the `sample_agent`. + + 2. If the user asks to check primes, delegate to the `prime_agent`. + + 3. If the user asks to sample a number and then check if the result is prime, + call `sample_agent` first, then pass the result to `prime_agent`. + + Always clarify the results before proceeding.' + tools: [] + toolboxes: [] + human_in_the_loop: true + context_providers: null + can_finish_conversation: false + raise_exceptions: false + max_iterations: 10 + initial_message: Hi! How can I help you? + caller_input_mode: always + agents: [] + flows: [] + agent_template: + component_type: PluginPromptTemplate + id: 23e82423-7df6-461e-a45b-31a6633348fe + name: '' + description: null + metadata: + __metadata_info__: {} + messages: + - role: system + contents: + - type: text + content: '{% if custom_instruction %}{{custom_instruction}}{% endif %}' + tool_requests: null + tool_result: null + display_only: false + sender: null + recipients: [] + time_created: '2026-01-14T14:27:15.835239+00:00' + time_updated: '2026-01-14T14:27:15.835239+00:00' + - role: system + contents: + - type: text + content: $$__CHAT_HISTORY_PLACEHOLDER__$$ + tool_requests: null + tool_result: null + display_only: false + sender: null + recipients: [] + time_created: '2026-01-14T14:27:15.831100+00:00' + time_updated: '2026-01-14T14:27:15.831101+00:00' + - role: system + contents: + - type: text + content: "{% if __PLAN__ %}The current plan you should follow is the following:\ + \ \n{{__PLAN__}}{% endif %}" + tool_requests: null + tool_result: null + display_only: false + sender: null + recipients: [] + time_created: '2026-01-14T14:27:15.835267+00:00' + time_updated: '2026-01-14T14:27:15.835267+00:00' + output_parser: null + inputs: + - description: '"custom_instruction" input variable for the template' + type: string + title: custom_instruction + default: '' + - description: '"__PLAN__" input variable for the template' + type: string + title: __PLAN__ + default: '' + - type: array + items: {} + title: __CHAT_HISTORY__ + pre_rendering_transforms: null + post_rendering_transforms: + - component_type: PluginRemoveEmptyNonUserMessageTransform + id: 34f49abb-027f-4df6-96ab-a90763aeabbc + name: removeemptynonusermessage_messagetransform + description: null + metadata: + __metadata_info__: {} + component_plugin_name: MessageTransformPlugin + component_plugin_version: 26.1.0.dev5 + tools: null + native_tool_calling: true + response_format: null + native_structured_generation: true + generation_config: null + component_plugin_name: PromptTemplatePlugin + component_plugin_version: 26.1.0.dev5 + component_plugin_name: AgentPlugin + component_plugin_version: 26.1.0.dev5 +workers: +- component_type: A2AAgent + id: b554446e-9c21-4aaf-b393-1e91cc9aeab8 + name: sample_agent + description: Agent that can generate random numbers + metadata: + __metadata_info__: {} + inputs: [] + outputs: [] + agent_url: http:// + connection_config: + component_type: A2AConnectionConfig + id: 061d0a87-cfec-49e4-a427-fec4b4326724 + name: '' + description: null + metadata: {} + timeout: 600.0 + headers: null + verify: false + key_file: null + cert_file: null + ssl_ca_cert: null + session_parameters: + timeout: 60.0 + poll_interval: 2.0 + max_retries: 5 +- component_type: A2AAgent + id: d6e6a95b-c144-469b-91ad-972121406b83 + name: prime_agent + description: Agent that handles checking if numbers are prime. + metadata: + __metadata_info__: {} + inputs: [] + outputs: [] + agent_url: http:// + connection_config: + component_type: A2AConnectionConfig + id: 54c3ebde-5101-4c4c-9718-bced0142754c + name: '' + description: null + metadata: {} + timeout: 600.0 + headers: null + verify: false + key_file: null + cert_file: null + ssl_ca_cert: null + session_parameters: + timeout: 60.0 + poll_interval: 2.0 + max_retries: 5 +agentspec_version: 25.4.2 diff --git a/docs/wayflowcore/source/core/howtoguides/howto_a2a_serving.rst b/docs/wayflowcore/source/core/howtoguides/howto_a2a_serving.rst index 45e95edc2..b8e6c595a 100644 --- a/docs/wayflowcore/source/core/howtoguides/howto_a2a_serving.rst +++ b/docs/wayflowcore/source/core/howtoguides/howto_a2a_serving.rst @@ -129,7 +129,7 @@ Below is example Flow that is valid for serving, along with how it can be served Next steps ========== -Now that you have learned how to serve WayFlow assistants using A2A protocol, you may proceed to :doc:`How to Build A2A Agents ` to learn how consume an A2A-served agent. +Now that you have learned how to serve WayFlow assistants using A2A protocol, you may proceed to :doc:`How to Use A2A Agents ` to learn how consume an A2A-served agent. Full code ========= diff --git a/docs/wayflowcore/source/core/howtoguides/howto_a2aagent.rst b/docs/wayflowcore/source/core/howtoguides/howto_a2aagent.rst index 066fbf340..216c436bd 100644 --- a/docs/wayflowcore/source/core/howtoguides/howto_a2aagent.rst +++ b/docs/wayflowcore/source/core/howtoguides/howto_a2aagent.rst @@ -1,8 +1,8 @@ .. _top-howtoa2aagent: -============================ -How to Connect to A2A Agents -============================ +===================== +How to Use A2A Agents +===================== .. |python-icon| image:: ../../_static/icons/python-icon.svg :width: 40px @@ -23,19 +23,19 @@ How to Connect to A2A Agents - :doc:`LLM configuration <../howtoguides/llm_from_different_providers>` - :doc:`Using agents ` +`A2A Protocol `_ is an open standard that defines how two agents can communicate with each other. It covers both the serving and consumption aspects of agent interaction. +This step-by-step guide demonstrates how to use the protocol in WayFlow in different ways, focusing on consuming hosted agents. +For serving using A2A, refer :doc:`A2A Serving `. -`A2A Protocol `_ is an open standard that defines how two agents can communicate -with each other. It covers both the serving and consumption aspects of agent interaction. - -In this guide, you will learn how to connect to a remote agent using this protocol with the :ref:`A2AAgent ` -class from the ``wayflowcore`` package. +A2A Agents +========== +In this section, you will learn how to connect to a remote agent using this protocol with the :ref:`A2AAgent `. -Basic usage -=========== +Basic Usage +----------- -To get started with an A2A agent, you need the URL of the remote server agent you wish to connect to. -Once you have this information, creating your A2A agent is straightforward and can be done in just a few lines of code: +To get started with an A2A agent, you need the URL of the remote server agent you wish to connect to. Once you have this information, creating your A2A agent is straightforward and can be done in just a few lines of code: .. literalinclude:: ../code_examples/howto_a2aagent.py :language: python @@ -50,15 +50,14 @@ Then, use the agent as shown below: :end-before: .. end-##_Running_the_agent Agent Spec Exporting/Loading -============================ +---------------------------- You can export the assistant configuration to its Agent Spec configuration using the ``AgentSpecExporter``. .. literalinclude:: ../code_examples/howto_a2aagent.py :language: python - :start-after: .. start-##_Export_config_to_Agent_Spec - :end-before: .. end-##_Export_config_to_Agent_Spec - + :start-after: .. start-##_Export_config_to_Agent_Spec1 + :end-before: .. end-##_Export_config_to_Agent_Spec1 Here is what the **Agent Spec representation will look like ↓** @@ -68,30 +67,117 @@ Here is what the **Agent Spec representation will look like ↓** .. tab:: JSON - .. literalinclude:: ../config_examples/howto_a2aagent.json + .. literalinclude:: ../config_examples/howto_a2aagent_1.json :language: json .. tab:: YAML - .. literalinclude:: ../config_examples/howto_a2aagent.yaml + .. literalinclude:: ../config_examples/howto_a2aagent_1.yaml :language: yaml You can then load the configuration back to an assistant using the ``AgentSpecLoader``. +.. literalinclude:: ../code_examples/howto_a2aagent.py + :language: python + :start-after: .. start-##_Load_Agent_Spec_config1 + :end-before: .. end-##_Load_Agent_Spec_config1 + +Manager Workers with A2A Agents +=============================== + +While each agent has limited standalone capabilities, combining them unlocks powerful workflows. +Using :ref:`ManagerWorkers `, you can implement a manager agent that efficiently coordinates tasks between different specialized agents. +This modular, scalable architecture allows each agent to focus on specific tasks, increasing overall system capability and flexibility. + +In this example, you'll create two A2A agents: one for checking if numbers are prime, and another for generating random numbers. +A manager agent will coordinate their interactions, demonstrating the plug-and-play flexibility offered by the protocol for integrating specialized agents, regardless of their implementation details. + +Setting up the Agents +--------------------- + +In this section, you'll set up servers for two agents: ``prime_agent`` checks if numbers are prime, and ``sample_agent`` generates random numbers. + +.. literalinclude:: ../code_examples/howto_a2aagent.py + :language: python + :start-after: .. start-##_Server_Setup_Prime_Agent + :end-before: .. end-##_Server_Setup_Prime_Agent + +.. literalinclude:: ../code_examples/howto_a2aagent.py + :language: python + :start-after: .. start-##_Server_Setup_Sample_Agent + :end-before: .. end-##_Server_Setup_Sample_Agent + +.. literalinclude:: ../code_examples/howto_a2aagent.py + :language: python + :start-after: .. start-##_Server_Startup_Logic + :end-before: .. end-##_Server_Startup_Logic + +For further details, see :ref:`A2AServer `. + +On the client side, create ``A2AAgent`` instances to connect to the servers started above. + +.. literalinclude:: ../code_examples/howto_a2aagent.py + :language: python + :start-after: .. start-##_Client_Setup + :end-before: .. end-##_Client_Setup + +Now you can use these agents in :ref:`ManagerWorkers ` setup. + +.. literalinclude:: ../code_examples/howto_a2aagent.py + :language: python + :start-after: .. start-##_Manager_Setup + :end-before: .. end-##_Manager_Setup + +Executing Tasks +--------------- + +Now, you can execute a conversation in which the manager agent delegates tasks to the most appropriate agent based on their capabilities. +This demonstrates how ManagerWorkers can be used to orchestrate complex interactions seamlessly. + +.. literalinclude:: ../code_examples/howto_a2aagent.py + :language: python + :start-after: .. start-##_ManagerWorkers_Execution + :end-before: .. end-##_ManagerWorkers_Execution + +Agent Spec Exporting/Loading +---------------------------- + +You can export the assistant configuration to its Agent Spec configuration using the ``AgentSpecExporter``. .. literalinclude:: ../code_examples/howto_a2aagent.py :language: python - :start-after: .. start-##_Load_Agent_Spec_config - :end-before: .. end-##_Load_Agent_Spec_config + :start-after: .. start-##_Export_config_to_Agent_Spec2 + :end-before: .. end-##_Export_config_to_Agent_Spec2 +Here is what the **Agent Spec representation will look like ↓** -Next steps -========== +.. collapse:: Click here to see the assistant configuration. + + .. tabs:: + + .. tab:: JSON -Now that you have learned how to use A2A Agents in WayFlow, you may proceed to :doc:`How to Use Agents in Flows `. + .. literalinclude:: ../config_examples/howto_a2aagent_2.json + :language: json + + .. tab:: YAML + + .. literalinclude:: ../config_examples/howto_a2aagent_2.yaml + :language: yaml + +You can then load the configuration back to an assistant using the ``AgentSpecLoader``. + +.. literalinclude:: ../code_examples/howto_a2aagent.py + :language: python + :start-after: .. start-##_Load_Agent_Spec_config2 + :end-before: .. end-##_Load_Agent_Spec_config2 + +Next Steps +========== +This guide covered the basics of using A2A agents and coordinating interactions through manager workers in WayFlow. -Full code +Full Code ========= Click on the card at the :ref:`top of this page ` to download the full code for this guide or copy the code below. diff --git a/docs/wayflowcore/source/core/howtoguides/index.rst b/docs/wayflowcore/source/core/howtoguides/index.rst index a45cf6a4d..afee13353 100644 --- a/docs/wayflowcore/source/core/howtoguides/index.rst +++ b/docs/wayflowcore/source/core/howtoguides/index.rst @@ -42,7 +42,7 @@ These how-to guides demonstrate how to use the main features to create and custo Create a ReAct Agent How to Send Images to LLMs and Agents Use OCI Generative AI Agents - How to Connect to A2A Agents + How to Use A2A Agents Use Templates for Advanced Prompting Techniques .. toctree:: @@ -67,7 +67,6 @@ These how-to guides demonstrate how to use the main features to create and custo Build a Swarm of Agents Build a ManagerWorkers of Agents - .. toctree:: :maxdepth: 1 :caption: Deployment diff --git a/wayflowcore/src/wayflowcore/executors/_managerworkersconversation.py b/wayflowcore/src/wayflowcore/executors/_managerworkersconversation.py index 980cebdff..dda7c56e3 100644 --- a/wayflowcore/src/wayflowcore/executors/_managerworkersconversation.py +++ b/wayflowcore/src/wayflowcore/executors/_managerworkersconversation.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Dict, List, Optional, Union +from wayflowcore.a2a.a2aagent import A2AAgent from wayflowcore.agent import Agent from wayflowcore.conversation import Conversation from wayflowcore.executors._executionstate import ConversationExecutionState @@ -17,6 +18,7 @@ if TYPE_CHECKING: from wayflowcore.contextproviders import ContextProvider + from wayflowcore.executors._a2aagentconversation import A2AAgentConversation from wayflowcore.executors._agentconversation import AgentConversation logger = logging.getLogger(__name__) @@ -25,11 +27,13 @@ @dataclass class ManagerWorkersConversationExecutionState(ConversationExecutionState): current_agent_name: str - subconversations: Dict[str, Union["AgentConversation", "ManagerWorkersConversation"]] + subconversations: Dict[ + str, Union["AgentConversation", "ManagerWorkersConversation", "A2AAgentConversation"] + ] def _create_subconversation_for_agent( - self, agent: Union[Agent, ManagerWorkers] - ) -> Union["AgentConversation", "ManagerWorkersConversation"]: + self, agent: Union[Agent, ManagerWorkers, A2AAgent] + ) -> Union["AgentConversation", "ManagerWorkersConversation", "A2AAgentConversation"]: subconv = agent.start_conversation() self.subconversations[agent.name] = subconv @@ -48,7 +52,9 @@ def managerworkers(self) -> "ManagerWorkers": @property def subconversations( self, - ) -> Dict[str, Union["AgentConversation", "ManagerWorkersConversation"]]: + ) -> Dict[ + str, Union["AgentConversation", "ManagerWorkersConversation", "A2AAgentConversation"] + ]: return self.state.subconversations @property @@ -71,7 +77,7 @@ def __str__(self) -> str: def _get_agent_subconversation( self, agent_name: str - ) -> Optional[Union["AgentConversation", "ManagerWorkersConversation"]]: + ) -> Optional[Union["AgentConversation", "ManagerWorkersConversation", "A2AAgentConversation"]]: return self.state.subconversations.get(agent_name) def _get_main_subconversation( diff --git a/wayflowcore/src/wayflowcore/executors/_managerworkersexecutor.py b/wayflowcore/src/wayflowcore/executors/_managerworkersexecutor.py index c3d59cf55..6e0686cd9 100644 --- a/wayflowcore/src/wayflowcore/executors/_managerworkersexecutor.py +++ b/wayflowcore/src/wayflowcore/executors/_managerworkersexecutor.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Union from wayflowcore import Conversation +from wayflowcore.a2a.a2aagent import A2AAgent from wayflowcore.agent import Agent, CallerInputMode from wayflowcore.executors._agenticpattern_helpers import ( _SEND_MESSAGE_TOOL_NAME, @@ -30,6 +31,7 @@ if TYPE_CHECKING: from wayflowcore.agentconversation import AgentConversation + from wayflowcore.executors._a2aagentconversation import A2AAgentConversation from wayflowcore.executors._managerworkersconversation import ManagerWorkersConversation logger = logging.getLogger(__name__) @@ -54,15 +56,21 @@ def _create_manager_agent(group_manager: Union[Agent, LlmModel]) -> Agent: def _validate_agent_unicity( - agents: List[Union[Agent, ManagerWorkers]], -) -> Dict[str, Union[Agent, ManagerWorkers]]: - agent_by_name: Dict[str, Union["Agent", "ManagerWorkers"]] = {} - - for agent in agents: - if not isinstance(agent, (Agent, ManagerWorkers)): - raise TypeError( - f"Only Agent and ManagerWorker type are supported in ManagerWorkers, got component of type '{agent.__class__.__name__}'" - ) + worker_agents: List[Union[Agent, ManagerWorkers, A2AAgent]], manager_agent: Agent +) -> Dict[str, Union[Agent, ManagerWorkers, A2AAgent]]: + agent_by_name: Dict[str, Union["Agent", "ManagerWorkers", "A2AAgent"]] = {} + all_agents = worker_agents + [manager_agent] + for agent in all_agents: + if agent.name != manager_agent.name: + if not isinstance(agent, (Agent, ManagerWorkers, A2AAgent)): + raise TypeError( + f"Only Agent, ManagerWorker and A2AAgent types are supported as workers for ManagerWorkers, got component of type '{agent.__class__.__name__}'" + ) + else: + if not isinstance(agent, Agent): + raise TypeError( + f"Only Agents are supported as managers for ManagerWorkers, got component of type '{agent.__class__.__name__}'" + ) # Checking for missing name if not agent.name: @@ -146,7 +154,9 @@ async def _run_manager_round( @staticmethod async def _run_worker_round( - current_conversation: Union["AgentConversation", "ManagerWorkersConversation"], + current_conversation: Union[ + "AgentConversation", "ManagerWorkersConversation", "A2AAgentConversation" + ], execution_interrupts: Optional[Sequence[ExecutionInterrupt]] = None, ) -> ExecutionStatus: return await current_conversation.execute_async( @@ -158,6 +168,7 @@ async def execute_async( conversation: Conversation, execution_interrupts: Optional[Sequence[ExecutionInterrupt]] = None, ) -> ExecutionStatus: + from wayflowcore.executors._a2aagentconversation import A2AAgentConversation from wayflowcore.executors._managerworkersconversation import ManagerWorkersConversation if not isinstance(conversation, ManagerWorkersConversation): @@ -183,9 +194,11 @@ async def execute_async( if current_agent_name == managerworkers_config.manager_agent.name: current_agent = managerworkers_config.manager_agent - if isinstance(current_conversation, ManagerWorkersConversation): + if isinstance( + current_conversation, (ManagerWorkersConversation, A2AAgentConversation) + ): raise ValueError( - "Manager Conversation cannot be of type `ManagerWorkersConversation`." + "Manager Conversation cannot be of type `ManagerWorkersConversation` or `A2AAgentConversation`." ) # Handle pending tool requests of manager diff --git a/wayflowcore/src/wayflowcore/managerworkers.py b/wayflowcore/src/wayflowcore/managerworkers.py index 84923541c..411d9c003 100644 --- a/wayflowcore/src/wayflowcore/managerworkers.py +++ b/wayflowcore/src/wayflowcore/managerworkers.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union from wayflowcore._metadata import MetadataType +from wayflowcore.a2a.a2aagent import A2AAgent from wayflowcore.agent import Agent, CallerInputMode from wayflowcore.conversationalcomponent import ConversationalComponent from wayflowcore.idgeneration import IdGenerator @@ -30,7 +31,7 @@ @dataclass(init=False) class ManagerWorkers(ConversationalComponent, SerializableDataclassMixin, SerializableObject): group_manager: Union[LlmModel, Agent] - workers: List[Union[Agent, "ManagerWorkers"]] + workers: List[Union[Agent, "ManagerWorkers", A2AAgent]] caller_input_mode: CallerInputMode managerworkers_template: "PromptTemplate" input_descriptors: List["Property"] @@ -43,7 +44,7 @@ class ManagerWorkers(ConversationalComponent, SerializableDataclassMixin, Serial def __init__( self, group_manager: Union[LlmModel, Agent], - workers: List[Union[Agent, "ManagerWorkers"]], + workers: List[Union[Agent, "ManagerWorkers", A2AAgent]], caller_input_mode: CallerInputMode = CallerInputMode.ALWAYS, managerworkers_template: Optional["PromptTemplate"] = None, input_descriptors: Optional[List["Property"]] = None, @@ -125,8 +126,8 @@ def __init__( self.workers = workers - self._agent_by_name: Dict[str, Union["Agent", "ManagerWorkers"]] = _validate_agent_unicity( - self.workers + [self.manager_agent] + self._agent_by_name: Dict[str, Union["Agent", "ManagerWorkers", "A2AAgent"]] = ( + _validate_agent_unicity(self.workers, self.manager_agent) ) # Create send message tools for the group manager @@ -177,6 +178,7 @@ def start_conversation( from wayflowcore.agentconversation import AgentConversation from wayflowcore.events.event import ConversationCreatedEvent from wayflowcore.events.eventlistener import record_event + from wayflowcore.executors._a2aagentconversation import A2AAgentConversation from wayflowcore.executors._managerworkersconversation import ( ManagerWorkersConversation, ManagerWorkersConversationExecutionState, @@ -198,7 +200,9 @@ def start_conversation( ) ) - subconversations: Dict[str, Union[AgentConversation, ManagerWorkersConversation]] = {} + subconversations: Dict[ + str, Union[AgentConversation, ManagerWorkersConversation, A2AAgentConversation] + ] = {} subconversations[self.manager_agent.name] = self.manager_agent.start_conversation( inputs=inputs, messages=messages, diff --git a/wayflowcore/src/wayflowcore/serialization/_builtins_deserialization_plugin.py b/wayflowcore/src/wayflowcore/serialization/_builtins_deserialization_plugin.py index f6c82f79e..ba629d772 100644 --- a/wayflowcore/src/wayflowcore/serialization/_builtins_deserialization_plugin.py +++ b/wayflowcore/src/wayflowcore/serialization/_builtins_deserialization_plugin.py @@ -802,10 +802,17 @@ def convert_to_wayflow( ) raise ValueError(f"Unexpected tool type provided in the tool_registry: {type(tool)}") elif isinstance(agentspec_component, AgentSpecManagerWorkers): - for agent in [agentspec_component.group_manager] + agentspec_component.workers: # type: ignore[assignment] - if not isinstance(agent, AgentSpecAgent): + if not isinstance(agentspec_component.group_manager, AgentSpecAgent): + raise ValueError( + f"WayFlow ManagerWorkers' manager only supports agents of type `Agent`, " + f"but received `{type(agentspec_component.group_manager).__name__}` instead." + ) + for agent in agentspec_component.workers: # type: ignore[assignment] + if not isinstance( + agent, (AgentSpecAgent, AgentSpecManagerWorkers, AgentSpecA2AAgent) + ): raise ValueError( - f"WayFlow ManagerWorkers only supports agents of type `Agent`, " + f"WayFlow ManagerWorkers' workers only supports agents of type `Agent`, `ManagerWorker`, `A2AAgent`, " f"but received `{type(agent).__name__}` instead." ) diff --git a/wayflowcore/tests/a2a/conftest.py b/wayflowcore/tests/a2a/conftest.py index 69ff0a617..c4f093707 100644 --- a/wayflowcore/tests/a2a/conftest.py +++ b/wayflowcore/tests/a2a/conftest.py @@ -12,6 +12,8 @@ import pytest +from wayflowcore.a2a.a2aagent import A2AAgent, A2AConnectionConfig + from ..utils import LogTee, _check_server_is_up, _terminate_process_tree, get_available_port from .start_a2a_server import AgentType @@ -110,3 +112,49 @@ def adk_a2a_server_fixture(session_tmp_path): yield url finally: _terminate_process_tree(process, timeout=5.0) + + +@pytest.fixture(scope="session", name="sample_a2a_server") +def sample_a2a_server_fixture(request, session_tmp_path): + agent_type = getattr(request, "param", AgentType.SAMPLE_AGENT) + + host = "localhost" + port = get_available_port(session_tmp_path) + process, url = _start_a2a_server(host=host, port=port, agent_type=agent_type) + try: + yield url + time.sleep(0.5) + finally: + _terminate_process_tree(process, timeout=5.0) + + +@pytest.fixture(scope="session", name="prime_a2a_server") +def prime_a2a_server_fixture(request, session_tmp_path): + agent_type = getattr(request, "param", AgentType.PRIME_AGENT) + host = "localhost" + port = get_available_port(session_tmp_path) + process, url = _start_a2a_server(host=host, port=port, agent_type=agent_type) + try: + yield url + finally: + _terminate_process_tree(process, timeout=5.0) + + +@pytest.fixture(scope="session", name="sample_a2a_agent") +def sample_a2a_agent_fixture(sample_a2a_server): + return A2AAgent( + name="sample_agent", + agent_url=sample_a2a_server, + description="Agent that can generate random numbers", + connection_config=A2AConnectionConfig(verify=False), + ) + + +@pytest.fixture(scope="session", name="prime_a2a_agent") +def prime_a2a_agent_fixture(prime_a2a_server): + return A2AAgent( + name="prime_agent", + agent_url=prime_a2a_server, + description="Agent that handles checking if numbers are prime.", + connection_config=A2AConnectionConfig(verify=False), + ) diff --git a/wayflowcore/tests/a2a/start_a2a_server.py b/wayflowcore/tests/a2a/start_a2a_server.py index 58b132163..5b6fb7ddb 100644 --- a/wayflowcore/tests/a2a/start_a2a_server.py +++ b/wayflowcore/tests/a2a/start_a2a_server.py @@ -60,6 +60,8 @@ class AgentType(str, Enum): MANAGER_WORKERS = "manager_workers" SWARM = "swarm" ADK_AGENT = "adk_agent" + SAMPLE_AGENT = "sample_agent" + PRIME_AGENT = "prime_agent" # ============== Agent functions ============== @@ -247,6 +249,48 @@ def get_swarm() -> Swarm: return Swarm(first_agent=first_agent, relationships=[(first_agent, second_agent)]) +@register_agent(AgentType.SAMPLE_AGENT) +def get_sample_agent() -> Agent: + @tool + def sample_number(a: Annotated[int, "upper bound for random selection"]) -> int: + "Simulate sampling a random number between 1 and the specified upper bound." + import random + + result = random.randint(1, a) + return result + + agent = Agent( + llm=llm, + name="sample_agent", + custom_instruction="You are an agent that can generate a random number between 1 and a specified value.", + tools=[sample_number], + can_finish_conversation=True, + ) + return agent + + +@register_agent(AgentType.PRIME_AGENT) +def get_prime_agent() -> Agent: + @tool + def check_prime(a: Annotated[int, "first required integer"]) -> bool: + "Check if the input number is a prime number." + if a < 2: + return False + for i in range(2, int(a**0.5) + 1): + if a % i == 0: + return False + return True + + agent = Agent( + llm=llm, + name="prime_agent", + custom_instruction="You are a math agent that can check whether a number is prime or not using the equipped tool.", + tools=[check_prime], + can_finish_conversation=True, + ) + return agent + + # ============== Server ============== def create_server(agent_type: AgentType, host: str, port: int): """Create and configure the A2A server""" diff --git a/wayflowcore/tests/test_docs_examples.py b/wayflowcore/tests/test_docs_examples.py index e359cef54..a8d578421 100644 --- a/wayflowcore/tests/test_docs_examples.py +++ b/wayflowcore/tests/test_docs_examples.py @@ -17,7 +17,13 @@ from wayflowcore.datastore.inmemory import _INMEMORY_USER_WARNING from wayflowcore.transforms.summarization import _SUMMARIZATION_WARNING_MESSAGE -from .a2a.conftest import a2a_server_fixture # noqa +from .a2a.conftest import ( # noqa + a2a_server_fixture, + prime_a2a_agent_fixture, + prime_a2a_server_fixture, + sample_a2a_agent_fixture, + sample_a2a_server_fixture, +) from .a2a.test_a2aagent import a2a_agent, connection_config_no_verify # noqa from .mcptools.conftest import sse_mcp_server_http # noqa from .mcptools.test_mcp_tools import MCP_USER_QUERY @@ -128,8 +134,17 @@ def _update_globals(varnames_to_update: List[str]) -> Tuple[Any, ...]: replacements["oci_agent"] = pytest_request.getfixturevalue("oci_agent") if "a2a_agent" in varnames_to_update: replacements["a2a_agent"] = pytest_request.getfixturevalue("a2a_agent") - elif "sse_mcp_server" in varnames_to_update: + if "sse_mcp_server" in varnames_to_update: replacements["sse_mcp_server"] = pytest_request.getfixturevalue("sse_mcp_server_http") + # Support for A2A examples like howto_a2a.py + if "sample_a2a_agent" in varnames_to_update: + replacements["sample_a2a_agent"] = pytest_request.getfixturevalue("sample_a2a_agent") + if "prime_a2a_agent" in varnames_to_update: + replacements["prime_a2a_agent"] = pytest_request.getfixturevalue("prime_a2a_agent") + if "sample_a2a_server" in varnames_to_update: + replacements["sample_a2a_server"] = pytest_request.getfixturevalue("sample_a2a_server") + if "prime_a2a_server" in varnames_to_update: + replacements["prime_a2a_server"] = pytest_request.getfixturevalue("prime_a2a_server") return tuple(replacements[name] for name in varnames_to_update)