How to Build Assistants with Tools#

python-icon Download Python Script

Python script/notebook for this guide.

Tool use how-to script

Prerequisites

This guide assumes familiarity with:

Equipping assistants with Tools enhances their capabilities. In WayFlow, tools can be used in both conversational assistants (also known as Agents) as well as in Flows by using the ToolExecutionStep class.

WayFlow supports server-side, client-side tools as well as remote-tools.

See also

This guide focuses on using server/client tools. Read the API Documentation to learn more about the RemoteTool.

../../_images/types_of_tools.svg

In this guide, you will build a PDF summarizer using the different types of supported tools, both within a flow and with an agent.

Imports and LLM configuration#

To get started, first import the PyPDF library. First, install the PyPDF library:

$ pip install pypdf

Building LLM-powered assistants with tools in WayFlow requires the following imports.

from typing import Annotated

from wayflowcore.agent import Agent
from wayflowcore.controlconnection import ControlFlowEdge
from wayflowcore.executors.executionstatus import (
    FinishedStatus,
    ToolRequestStatus,
    UserMessageRequestStatus,
)
from wayflowcore.flow import Flow
from wayflowcore.models.llmmodelfactory import LlmModel
from wayflowcore.property import BooleanProperty, StringProperty
from wayflowcore.steps import OutputMessageStep, PromptExecutionStep, ToolExecutionStep
from wayflowcore.tools import ClientTool, ServerTool, Tool, ToolRequest, ToolResult, tool

In this guide, you will use an LLM.

WayFlow supports several LLM API providers. Select an LLM from the options below:

from wayflowcore.models import OCIGenAIModel

if __name__ == "__main__":

    llm = OCIGenAIModel(
        model_id="provider.model-id",
        service_endpoint="https://url-to-service-endpoint.com",
        compartment_id="compartment-id",
        auth_type="API_KEY",
    )

Note

API keys should not be stored anywhere in the code. Use environment variables or tools such as python-dotenv.

Helper functions#

The underlying tool used in this example is a PDF parser tool. The tool:

  • loads a PDF file;

  • reads the content of the pages;

  • returns the extracted text.

The PyPDFLoader API from the langchain_community Python library is used for this purpose.

def _read_and_clean_pdf_file(file_path: str, clean_pdf: bool = False):
    from langchain_community.document_loaders import PyPDFLoader

    loader = PyPDFLoader(file_path=file_path)
    page_content_list = []
    for page in loader.lazy_load():
        page_content_list.append(page.page_content)
    if clean_pdf:
        # we remove the extras "\n"
        all_content = []
        for page_content in page_content_list:
            for row in page_content.split("\n"):
                if not row.strip().endswith("."):
                    all_content.append(row)
                else:
                    all_content.append(row + "\n")
    else:
        all_content = page_content_list
    return "\n".join(page_content_list)


# The path to the pdf file to be summarized
PDF_FILE_PATH = "path/to/example_document.pdf"
Click to see the PDF content used in this example.
# Oracle Corporation

Oracle Corporation is an American multinational computer technology company headquartered in Austin, Texas. Co-founded in 1977 in Santa Clara, California, by Larry Ellison, who remains executive chairman, Oracle Corporation is the fourth-largest software company in the world by market capitalization as of 2025. Its market value was approximately US$614 billion in June 2025. The company's 2023 ranking in the Forbes Global 2000 was 80. The company sells database software, (particularly the Oracle Database), and cloud computing software and hardware. Oracle's core application software is a suite of enterprise software products, including enterprise resource planning (ERP), human capital management (HCM), customer relationship management (CRM), enterprise performance management (EPM), Customer Experience Commerce (CX Commerce) and supply chain management (SCM) software.

Overview of the types of tools#

This section covers how to use the following tools:

  • @tool decorator - The simplest way to create server-side tools by decorating Python functions.

  • ServerTool - For tools to be executed on the server side. Use this for tools running within the WayFlow environment, including local execution.

  • ClientTool - For tools to be executed on the client application.

Using the @tool Decorator#

WayFlow provides a convenient @tool decorator that simplifies the creation of server-side tools. By decorating a Python function with @tool, you automatically convert it into a ServerTool object ready to be used in your Flows and Agents.

The decorator automatically extracts information from the function:

  • The function name becomes the tool name

  • The function docstring becomes the tool description

  • Type annotations and parameter docstrings define input parameters

  • Return type annotations define the output type

In the example below, we show a few options for how to create a server-side tool using the @tool decorator:

### Option 1 - Using typing.Annotated
@tool("read_pdf")
def read_pdf_server_tool(
    file_path: Annotated[str, "Path to the pdf file"],
    clean_pdf: Annotated[bool, "Cleans and reformat the pdf pages"] = False,
) -> str:
    """Reads a PDF file given a filepath."""
    return _read_and_clean_pdf_file(file_path, clean_pdf)

### Option 2 - Using only the docstring
@tool("read_pdf", description_mode="only_docstring")
def read_pdf_server_tool(file_path: str, clean_pdf: bool = False) -> str:
    """Reads a PDF file given a filepath."""
    return _read_and_clean_pdf_file(file_path, clean_pdf)

In the above example, the decorated function read_pdf_server_tool is transformed into a ServerTool object. The tool’s name is derived from the function name, the description from the docstring, and the input parameters and output type from the type annotations.

Tip

You can set the description_mode parameter of the @tool decorator to only_docstring to use the parameter signature information from the docstrings instead of having to manually define them using Annotated[Type, "description"].

Creating a ServerTool#

The ServerTool is defined by specifying:

  • A tool name

  • A tool description

  • Input parameters, including names, types, and optional default values.

  • A Python callable, the function executed by the tool.

  • The output type.

In the example below, the tool takes two input parameters, one of which is optional, and returns a string.

### Option 1 - Using Properties
read_pdf_server_tool = ServerTool(
    name="read_pdf",
    description="Reads a PDF file given a filepath",
    input_descriptors=[
        StringProperty("file_path", description="Path to the pdf file"),
        BooleanProperty(
            "clean_pdf", description="Cleans and reformat the pdf pages", default_value=False
        ),
    ],
    output_descriptors=[StringProperty()],
    func=_read_and_clean_pdf_file,
)

### Option 2 - Using JSON Schema
read_pdf_server_tool = ServerTool(
    name="read_pdf",
    description="Reads a PDF file given a filepath",
    parameters={
        "file_path": {
            "type": "string",
            "description": "Path to the pdf file",
        },
        "clean_pdf": {
            "type": "boolean",
            "default": False,
            "description": "Cleans and reformat the pdf pages",
        },
    },
    func=_read_and_clean_pdf_file,
    output={"type": "string", "title": "tool_output"},
)

Creating a ClientTool#

The ClientTool is defined similarly to a ServerTool, except that it does not include a Python callable in its definition. When executed, a ClientTool returns a ToolRequest, which must be executed on the client side. The client then sends the execution result back to the assistant.

In the following example, the tool execution function is also defined. This function should be implemented based on the specific requirements of the assistant developer.

def _execute_read_pdf_request(tool_request: ToolRequest) -> str:
    args = tool_request.args
    if "file_path" not in args or "clean_pdf" not in args:
        print(f"Missing arguments in tool request, args were {args}")
        return "INVALID_REQUEST"
    return _read_and_clean_pdf_file(args["file_path"], args["clean_pdf"])


def execute_tool_from_tool_request(tool_request: ToolRequest) -> str:
    if tool_request.name == "read_pdf":
        return _execute_read_pdf_request(tool_request)
    else:
        raise ValueError(f"Unknown tool in tool request: {tool_request.name}")


### Option 1 - Using Properties
read_pdf_client_tool = ClientTool(
    name="read_pdf",
    description="Reads a PDF file given a filepath",
    input_descriptors=[
        StringProperty("file_path", description="Path to the pdf file"),
        BooleanProperty(
            "clean_pdf", description="Cleans and reformat the pdf pages", default_value=False
        ),
    ],
    output_descriptors=[StringProperty()],
)

### Option 2 - Using JSON Schema
read_pdf_client_tool = ClientTool(
    name="read_pdf",
    description="Reads a PDF file given a filepath",
    parameters={
        "file_path": {
            "type": "string",
            "description": "Path to the pdf file",
        },
        "clean_pdf": {
            "type": "boolean",
            "default": False,
            "description": "Cleans and reformat the pdf pages",
        },
    },
    output={"type": "string"},
)

Building Flows with Tools using the ToolExecutionStep#

Executing tools in Flows can be done using the ToolExecutionStep. The step simply requires the user to specify the tool to execute when the step is invoked.

Once the tool execution step is defined, the Flow can be constructed as usual. For more information, refer to the tutorial on Flows.

def build_flow(llm: LlmModel, tool: Tool) -> Flow:
    pdf_read_step = ToolExecutionStep(
        name="pdf_read_step",
        tool=tool,
    )
    summarization_step = PromptExecutionStep(
        name="summarization_step",
        llm=llm,
        prompt_template="Please summarize the following PDF in 100 words or less. PDF:\n{{pdf_content}}",
        input_mapping={"pdf_content": ToolExecutionStep.TOOL_OUTPUT},
    )
    output_step = OutputMessageStep(
        name="output_step",
        message_template="Here is the summarized pdf:\n{{summarized_pdf}}",
        input_mapping={"summarized_pdf": PromptExecutionStep.OUTPUT},
    )
    return Flow(
        begin_step=pdf_read_step,
        control_flow_edges=[
            ControlFlowEdge(source_step=pdf_read_step, destination_step=summarization_step),
            ControlFlowEdge(source_step=summarization_step, destination_step=output_step),
            ControlFlowEdge(source_step=output_step, destination_step=None),
        ],
    )

Executing ServerTool with a Flow#

When using the ServerTool, the tool execution is performed on the server side. As a consequence, the flow can be executed end-to-end with a single execute instruction.

assistant = build_flow(llm, read_pdf_server_tool)

inputs = {"file_path": PDF_FILE_PATH, "clean_pdf": False}
conversation = assistant.start_conversation(inputs=inputs)

status = conversation.execute()
if isinstance(status, FinishedStatus):
    flow_outputs = status.output_values
    print(f"---\nFlow outputs >>> {flow_outputs}\n---")
else:
    print(f"Invalid execution status, expected FinishedStatus, received {type(status)}")

Click to see the summarized PDF content.

Here is the summarized PDF:

Oracle Corporation is an American multinational computer technology company headquartered in Austin, Texas. It sells database software, cloud computing software and hardware, and enterprise software products including ERP, HCM, CRM, and SCM software.

Executing ClientTool with a Flow#

When using a ClientTool, the tool execution is performed on the client side. Upon request, the assistant sends a ToolRequest to the client, which is responsible for executing the tool. Once completed, the client returns a ToolResult to the assistant, allowing it to continue execution until the task is complete.

assistant = build_flow(llm, read_pdf_client_tool)

inputs = {"file_path": PDF_FILE_PATH, "clean_pdf": False}
conversation = assistant.start_conversation(inputs=inputs)

status = conversation.execute()

failed = False
if isinstance(status, ToolRequestStatus):
    # Executing the request and sending it back to the assistant
    tool_request = status.tool_requests[0]
    tool_result = execute_tool_from_tool_request(tool_request)
    conversation.append_tool_result(
        ToolResult(content=tool_result, tool_request_id=tool_request.tool_request_id)
    )
else:
    failed = True
    print(f"Invalid execution status, expected ToolRequestStatus, received {type(status)}")

if not failed:
    # Continuing the conversation
    status = conversation.execute()

if not failed and isinstance(status, FinishedStatus):
    flow_outputs = status.output_values
    print(f"---\nFlow outputs >>> {flow_outputs}\n---")
elif not failed:
    print(f"Invalid execution status, expected FinishedStatus, received {type(status)}")
else:
    pass

Building Agents with Tools#

Agents can be equipped with tools by specifying the list of tools the agent can access.

You do not need to mention the tools in the agent’s custom_instruction, as tool descriptions are automatically added internally.

def build_agent(llm: LlmModel, tool: Tool) -> Agent:
    from textwrap import dedent

    custom_instruction = dedent(
        """
        You are helping to load and summarize a PDF file given a filepath.
        ## Context
        You will receive a filepath from the username which indicates the path to the
        PDF file we want to summarize
        ## Task
        You will follow the next instructions:
        1. Use the tool to load the PDF file (don't go to the next step unless the file content was received).
           If the user does not specify anything, do not clean the PDF prior to summarizing it.
        2. Summarize the given PDF content in 100 words or less.
        ## Output Format
        Return the summarized document as follows:
        ```
        Here is the summarized pdf:
        [summarized pdf]
        ```
        """
    ).strip()

    return Agent(
        llm=llm,
        tools=[tool],
        custom_instruction=custom_instruction,
        max_iterations=3,
    )

Note

Agents can also be equipped with other flows and agents. This topic will be covered in a dedicated tutorial.

Executing ServerTool with an Agent#

Similar to executing a Flow with a ServerTool, Agents can be executed end-to-end using a single execute instruction. The key difference is that the file path is provided as a conversation message rather than as a flow input.

assistant = build_agent(llm, read_pdf_server_tool)

conversation = assistant.start_conversation()

conversation.append_user_message(
    f"Please summarize my PDF document (can be found at {PDF_FILE_PATH})"
)
status = conversation.execute()
if isinstance(status, UserMessageRequestStatus):
    assistant_reply = conversation.get_last_message()
    print(f"---\nAssistant >>> {assistant_reply.content}\n---")
else:
    print(f"Invalid execution status, expected UserMessageRequestStatus, received {type(status)}")

Important

In this case, the LLM must correctly generate the tool call with the file path as an input parameter. Smaller LLMs may struggle to reproduce the path accurately. In general, assistant developers should try to avoid having LLMs to manipulate complex strings.

Executing ClientTool with an Agent#

Similar to executing a Flow with a ClientTool, the tool request must be handled on the client side. The only difference is that the file path is provided as a conversation message instead of as a flow input.

assistant = build_agent(llm, read_pdf_client_tool)

conversation = assistant.start_conversation()
conversation.append_user_message(
    f"Please summarize my PDF document (can be found at {PDF_FILE_PATH})"
)

status = conversation.execute()

# Executing the request and sending it back to the assistant
if isinstance(status, ToolRequestStatus):
    tool_request = status.tool_requests[0]
    tool_result = execute_tool_from_tool_request(tool_request)
    conversation.append_tool_result(
        ToolResult(content=tool_result, tool_request_id=tool_request.tool_request_id)
    )
else:
    failed = True
    print(f"Invalid execution status, expected ToolRequestStatus, received {type(status)}")

if not failed:
    # Continuing the conversation
    status = conversation.execute()

if not failed and isinstance(status, UserMessageRequestStatus):
    assistant_reply = conversation.get_last_message()
    print(f"---\nAssistant >>> {assistant_reply.content}\n---")
elif not failed:
    print(f"Invalid execution status, expected UserMessageRequestStatus, received {type(status)}")
else:
    pass

Agent Spec Exporting/Loading#

You can export the assistant configuration to its Agent Spec configuration using the AgentSpecExporter.

from wayflowcore.agentspec import AgentSpecExporter

serialized_assistant = AgentSpecExporter().to_json(assistant)

Here is what the Agent Spec representation will look like ↓

Click here to see the assistant configuration.
{
  "component_type": "ExtendedAgent",
  "id": "382a74eb-fd01-4725-abe1-4ad2da5805de",
  "name": "agent_74b004c2",
  "description": "",
  "metadata": {
    "__metadata_info__": {
      "name": "agent_74b004c2",
      "description": ""
    }
  },
  "inputs": [],
  "outputs": [],
  "llm_config": {
    "component_type": "VllmConfig",
    "id": "dbe50cdc-1a2d-4b4a-8c20-09cba28c21f6",
    "name": "LLAMA_MODEL_ID",
    "description": null,
    "metadata": {
      "__metadata_info__": {}
    },
    "default_generation_parameters": null,
    "url": "LLAMA_API_URL",
    "model_id": "LLAMA_MODEL_ID"
  },
  "system_prompt": "You are helping to load and summarize a PDF file given a filepath.\n## Context\nYou will receive a filepath from the username which indicates the path to the\nPDF file we want to summarize\n## Task\nYou will follow the next instructions:\n1. Use the tool to load the PDF file (don't go to the next step unless the file content was received).\n   If the user does not specify anything, do not clean the PDF prior to summarizing it.\n2. Summarize the given PDF content in 100 words or less.\n## Output Format\nReturn the summarized document as follows:\n```\nHere is the summarized pdf:\n[summarized pdf]\n```",
  "tools": [
    {
      "component_type": "ClientTool",
      "id": "4b1a808b-473f-4e00-b413-23fd7143baf6",
      "name": "read_pdf",
      "description": "Reads a PDF file given a filepath",
      "metadata": {
        "__metadata_info__": {}
      },
      "inputs": [
        {
          "description": "Path to the pdf file",
          "type": "string",
          "title": "file_path"
        },
        {
          "description": "Cleans and reformat the pdf pages",
          "type": "boolean",
          "title": "clean_pdf",
          "default": false
        }
      ],
      "outputs": [
        {
          "type": "string",
          "title": "tool_output"
        }
      ]
    }
  ],
  "toolboxes": [],
  "context_providers": null,
  "can_finish_conversation": false,
  "max_iterations": 3,
  "initial_message": "Hi! How can I help you?",
  "caller_input_mode": "always",
  "agents": [],
  "flows": [],
  "agent_template": {
    "component_type": "PluginPromptTemplate",
    "id": "95b539ce-0908-4a45-9ab0-cd6fa1b346d7",
    "name": "",
    "description": "",
    "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": "2025-09-02T15:52:22.014400+00:00",
        "time_updated": "2025-09-02T15:52:22.014401+00:00"
      },
      {
        "role": "user",
        "contents": [],
        "tool_requests": null,
        "tool_result": null,
        "display_only": false,
        "sender": null,
        "recipients": [],
        "time_created": "2025-09-02T15:52:22.008803+00:00",
        "time_updated": "2025-09-02T15:52:22.010218+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": "2025-09-02T15:52:22.014421+00:00",
        "time_updated": "2025-09-02T15:52:22.014421+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": "372d6f16-b945-4b10-b1c8-adc143ddab9d",
        "name": "removeemptynonusermessage_messagetransform",
        "description": null,
        "metadata": {
          "__metadata_info__": {}
        },
        "component_plugin_name": "MessageTransformPlugin",
        "component_plugin_version": "25.4.0.dev0"
      }
    ],
    "tools": null,
    "native_tool_calling": true,
    "response_format": null,
    "native_structured_generation": true,
    "generation_config": null,
    "component_plugin_name": "PromptTemplatePlugin",
    "component_plugin_version": "25.4.0.dev0"
  },
  "component_plugin_name": "AgentPlugin",
  "component_plugin_version": "25.4.0.dev0",
  "agentspec_version": "25.4.1"
}

You can then load the configuration back to an assistant using the AgentSpecLoader.

from wayflowcore.agentspec import AgentSpecLoader

TOOL_REGISTRY = {"read_pdf": read_pdf_server_tool}
assistant: Agent = AgentSpecLoader(
    tool_registry=TOOL_REGISTRY
).load_json(serialized_assistant)

Note

This guide uses the following extension/plugin Agent Spec components:

  • PluginPromptTemplate

  • PluginRemoveEmptyNonUserMessageTransform

  • ExtendedAgent

See the list of available Agent Spec extension/plugin components in the API Reference

Next steps#

Having learned how to use ServerTool and ClientTool with assistants in WayFlow, you may now proceed to:

Full code#

Click on the card at the top of this page to download the full code for this guide or copy the code below.

download the full code for this guide or copy the code below.

  1# Copyright © 2025 Oracle and/or its affiliates.
  2#
  3# This software is under the Universal Permissive License
  4# %%[markdown]
  5# Code Example - How to Build Assistants with Tools
  6# -------------------------------------------------
  7
  8# How to use:
  9# Create a new Python virtual environment and install the latest WayFlow version.
 10# ```bash
 11# python -m venv venv-wayflowcore
 12# source venv-wayflowcore/bin/activate
 13# pip install --upgrade pip
 14# pip install "wayflowcore==26.1" 
 15# ```
 16
 17# You can now run the script
 18# 1. As a Python file:
 19# ```bash
 20# python howto_tooluse.py
 21# ```
 22# 2. As a Notebook (in VSCode):
 23# When viewing the file,
 24#  - press the keys Ctrl + Enter to run the selected cell
 25#  - or Shift + Enter to run the selected cell and move to the cell below# (UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl) or Apache License
 26# 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0), at your option.
 27
 28
 29
 30
 31# %%[markdown]
 32## Imports for this guide
 33
 34# %%
 35from typing import Annotated
 36
 37from wayflowcore.agent import Agent
 38from wayflowcore.controlconnection import ControlFlowEdge
 39from wayflowcore.executors.executionstatus import (
 40    FinishedStatus,
 41    ToolRequestStatus,
 42    UserMessageRequestStatus,
 43)
 44from wayflowcore.flow import Flow
 45from wayflowcore.models.llmmodelfactory import LlmModel
 46from wayflowcore.property import BooleanProperty, StringProperty
 47from wayflowcore.steps import OutputMessageStep, PromptExecutionStep, ToolExecutionStep
 48from wayflowcore.tools import ClientTool, ServerTool, Tool, ToolRequest, ToolResult, tool
 49
 50# %%[markdown]
 51## Configure your LLM
 52
 53# %%
 54from wayflowcore.models.vllmmodel import VllmModel
 55
 56llm = VllmModel(
 57    model_id="LLAMA_MODEL_ID",
 58    host_port="LLAMA_API_URL",
 59)
 60
 61# %%[markdown]
 62## Defining some helper functions
 63
 64# %%
 65def _read_and_clean_pdf_file(file_path: str, clean_pdf: bool = False):
 66    from langchain_community.document_loaders import PyPDFLoader
 67
 68    loader = PyPDFLoader(file_path=file_path)
 69    page_content_list = []
 70    for page in loader.lazy_load():
 71        page_content_list.append(page.page_content)
 72    if clean_pdf:
 73        # we remove the extras "\n"
 74        all_content = []
 75        for page_content in page_content_list:
 76            for row in page_content.split("\n"):
 77                if not row.strip().endswith("."):
 78                    all_content.append(row)
 79                else:
 80                    all_content.append(row + "\n")
 81    else:
 82        all_content = page_content_list
 83    return "\n".join(page_content_list)
 84
 85
 86# The path to the pdf file to be summarized
 87PDF_FILE_PATH = "path/to/example_document.pdf"
 88
 89
 90
 91# %%[markdown]
 92## Defining a tool using the tool decorator
 93
 94# %%
 95### Option 1 - Using typing.Annotated
 96@tool("read_pdf")
 97def read_pdf_server_tool(
 98    file_path: Annotated[str, "Path to the pdf file"],
 99    clean_pdf: Annotated[bool, "Cleans and reformat the pdf pages"] = False,
100) -> str:
101    """Reads a PDF file given a filepath."""
102    return _read_and_clean_pdf_file(file_path, clean_pdf)
103
104### Option 2 - Using only the docstring
105@tool("read_pdf", description_mode="only_docstring")
106def read_pdf_server_tool(file_path: str, clean_pdf: bool = False) -> str:
107    """Reads a PDF file given a filepath."""
108    return _read_and_clean_pdf_file(file_path, clean_pdf)
109
110
111# %%[markdown]
112## Defining a tool using the ServerTool
113
114# %%
115### Option 1 - Using Properties
116read_pdf_server_tool = ServerTool(
117    name="read_pdf",
118    description="Reads a PDF file given a filepath",
119    input_descriptors=[
120        StringProperty("file_path", description="Path to the pdf file"),
121        BooleanProperty(
122            "clean_pdf", description="Cleans and reformat the pdf pages", default_value=False
123        ),
124    ],
125    output_descriptors=[StringProperty()],
126    func=_read_and_clean_pdf_file,
127)
128
129### Option 2 - Using JSON Schema
130read_pdf_server_tool = ServerTool(
131    name="read_pdf",
132    description="Reads a PDF file given a filepath",
133    parameters={
134        "file_path": {
135            "type": "string",
136            "description": "Path to the pdf file",
137        },
138        "clean_pdf": {
139            "type": "boolean",
140            "default": False,
141            "description": "Cleans and reformat the pdf pages",
142        },
143    },
144    func=_read_and_clean_pdf_file,
145    output={"type": "string", "title": "tool_output"},
146)
147
148# %%[markdown]
149## Defining a build flow helper function
150
151# %%
152def build_flow(llm: LlmModel, tool: Tool) -> Flow:
153    pdf_read_step = ToolExecutionStep(
154        name="pdf_read_step",
155        tool=tool,
156    )
157    summarization_step = PromptExecutionStep(
158        name="summarization_step",
159        llm=llm,
160        prompt_template="Please summarize the following PDF in 100 words or less. PDF:\n{{pdf_content}}",
161        input_mapping={"pdf_content": ToolExecutionStep.TOOL_OUTPUT},
162    )
163    output_step = OutputMessageStep(
164        name="output_step",
165        message_template="Here is the summarized pdf:\n{{summarized_pdf}}",
166        input_mapping={"summarized_pdf": PromptExecutionStep.OUTPUT},
167    )
168    return Flow(
169        begin_step=pdf_read_step,
170        control_flow_edges=[
171            ControlFlowEdge(source_step=pdf_read_step, destination_step=summarization_step),
172            ControlFlowEdge(source_step=summarization_step, destination_step=output_step),
173            ControlFlowEdge(source_step=output_step, destination_step=None),
174        ],
175    )
176
177# %%[markdown]
178## Creating and running a flow with a server tool
179
180# %%
181assistant = build_flow(llm, read_pdf_server_tool)
182
183inputs = {"file_path": PDF_FILE_PATH, "clean_pdf": False}
184conversation = assistant.start_conversation(inputs=inputs)
185
186status = conversation.execute()
187if isinstance(status, FinishedStatus):
188    flow_outputs = status.output_values
189    print(f"---\nFlow outputs >>> {flow_outputs}\n---")
190else:
191    print(f"Invalid execution status, expected FinishedStatus, received {type(status)}")
192
193
194# %%[markdown]
195## Defining a build agent helper function
196
197# %%
198def build_agent(llm: LlmModel, tool: Tool) -> Agent:
199    from textwrap import dedent
200
201    custom_instruction = dedent(
202        """
203        You are helping to load and summarize a PDF file given a filepath.
204        ## Context
205        You will receive a filepath from the username which indicates the path to the
206        PDF file we want to summarize
207        ## Task
208        You will follow the next instructions:
209        1. Use the tool to load the PDF file (don't go to the next step unless the file content was received).
210           If the user does not specify anything, do not clean the PDF prior to summarizing it.
211        2. Summarize the given PDF content in 100 words or less.
212        ## Output Format
213        Return the summarized document as follows:
214        ```
215        Here is the summarized pdf:
216        [summarized pdf]
217        ```
218        """
219    ).strip()
220
221    return Agent(
222        llm=llm,
223        tools=[tool],
224        custom_instruction=custom_instruction,
225        max_iterations=3,
226    )
227
228# %%[markdown]
229## Creating and running an agent with a server tool
230
231# %%
232assistant = build_agent(llm, read_pdf_server_tool)
233
234conversation = assistant.start_conversation()
235
236conversation.append_user_message(
237    f"Please summarize my PDF document (can be found at {PDF_FILE_PATH})"
238)
239status = conversation.execute()
240if isinstance(status, UserMessageRequestStatus):
241    assistant_reply = conversation.get_last_message()
242    print(f"---\nAssistant >>> {assistant_reply.content}\n---")
243else:
244    print(f"Invalid execution status, expected UserMessageRequestStatus, received {type(status)}")
245
246
247# %%[markdown]
248## Defining a tool using the ClientTool
249
250# %%
251def _execute_read_pdf_request(tool_request: ToolRequest) -> str:
252    args = tool_request.args
253    if "file_path" not in args or "clean_pdf" not in args:
254        print(f"Missing arguments in tool request, args were {args}")
255        return "INVALID_REQUEST"
256    return _read_and_clean_pdf_file(args["file_path"], args["clean_pdf"])
257
258
259def execute_tool_from_tool_request(tool_request: ToolRequest) -> str:
260    if tool_request.name == "read_pdf":
261        return _execute_read_pdf_request(tool_request)
262    else:
263        raise ValueError(f"Unknown tool in tool request: {tool_request.name}")
264
265
266### Option 1 - Using Properties
267read_pdf_client_tool = ClientTool(
268    name="read_pdf",
269    description="Reads a PDF file given a filepath",
270    input_descriptors=[
271        StringProperty("file_path", description="Path to the pdf file"),
272        BooleanProperty(
273            "clean_pdf", description="Cleans and reformat the pdf pages", default_value=False
274        ),
275    ],
276    output_descriptors=[StringProperty()],
277)
278
279### Option 2 - Using JSON Schema
280read_pdf_client_tool = ClientTool(
281    name="read_pdf",
282    description="Reads a PDF file given a filepath",
283    parameters={
284        "file_path": {
285            "type": "string",
286            "description": "Path to the pdf file",
287        },
288        "clean_pdf": {
289            "type": "boolean",
290            "default": False,
291            "description": "Cleans and reformat the pdf pages",
292        },
293    },
294    output={"type": "string"},
295)
296
297# %%[markdown]
298## Creating and running a flow with a client tool
299
300# %%
301assistant = build_flow(llm, read_pdf_client_tool)
302
303inputs = {"file_path": PDF_FILE_PATH, "clean_pdf": False}
304conversation = assistant.start_conversation(inputs=inputs)
305
306status = conversation.execute()
307
308failed = False
309if isinstance(status, ToolRequestStatus):
310    # Executing the request and sending it back to the assistant
311    tool_request = status.tool_requests[0]
312    tool_result = execute_tool_from_tool_request(tool_request)
313    conversation.append_tool_result(
314        ToolResult(content=tool_result, tool_request_id=tool_request.tool_request_id)
315    )
316else:
317    failed = True
318    print(f"Invalid execution status, expected ToolRequestStatus, received {type(status)}")
319
320if not failed:
321    # Continuing the conversation
322    status = conversation.execute()
323
324if not failed and isinstance(status, FinishedStatus):
325    flow_outputs = status.output_values
326    print(f"---\nFlow outputs >>> {flow_outputs}\n---")
327elif not failed:
328    print(f"Invalid execution status, expected FinishedStatus, received {type(status)}")
329else:
330    pass
331
332
333# %%[markdown]
334## Creating and running an agent with a client tool
335
336# %%
337assistant = build_agent(llm, read_pdf_client_tool)
338
339conversation = assistant.start_conversation()
340conversation.append_user_message(
341    f"Please summarize my PDF document (can be found at {PDF_FILE_PATH})"
342)
343
344status = conversation.execute()
345
346# Executing the request and sending it back to the assistant
347if isinstance(status, ToolRequestStatus):
348    tool_request = status.tool_requests[0]
349    tool_result = execute_tool_from_tool_request(tool_request)
350    conversation.append_tool_result(
351        ToolResult(content=tool_result, tool_request_id=tool_request.tool_request_id)
352    )
353else:
354    failed = True
355    print(f"Invalid execution status, expected ToolRequestStatus, received {type(status)}")
356
357if not failed:
358    # Continuing the conversation
359    status = conversation.execute()
360
361if not failed and isinstance(status, UserMessageRequestStatus):
362    assistant_reply = conversation.get_last_message()
363    print(f"---\nAssistant >>> {assistant_reply.content}\n---")
364elif not failed:
365    print(f"Invalid execution status, expected UserMessageRequestStatus, received {type(status)}")
366else:
367    pass
368
369
370# %%[markdown]
371## Export config to Agent Spec
372
373# %%
374from wayflowcore.agentspec import AgentSpecExporter
375
376serialized_assistant = AgentSpecExporter().to_json(assistant)
377
378# %%[markdown]
379## Load Agent Spec config
380
381# %%
382from wayflowcore.agentspec import AgentSpecLoader
383
384TOOL_REGISTRY = {"read_pdf": read_pdf_server_tool}
385assistant: Agent = AgentSpecLoader(
386    tool_registry=TOOL_REGISTRY
387).load_json(serialized_assistant)