How to Build Assistants with Tools#
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
.
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",
)
from wayflowcore.models import VllmModel
llm = VllmModel(
model_id="model-id",
host_port="VLLM_HOST_PORT",
)
from wayflowcore.models import OllamaModel
llm = OllamaModel(
model_id="model-id",
)
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"
}
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:\n\
1. 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```\n\
Here 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)