mirror of
https://github.com/temporal-community/temporal-ai-agent.git
synced 2026-03-15 14:08:08 +01:00
Merge pull request #2 from joshmsmith/development
more changes to scenarios, workflow simplification, docs improvements...more stuff, logging It's cool
This commit is contained in:
@@ -35,8 +35,11 @@ OPENAI_API_KEY=sk-proj-...
|
|||||||
# Uncomment if using API key (not needed for local dev server)
|
# Uncomment if using API key (not needed for local dev server)
|
||||||
# TEMPORAL_API_KEY=abcdef1234567890
|
# TEMPORAL_API_KEY=abcdef1234567890
|
||||||
|
|
||||||
# Agent Goal Configuration
|
# Set starting goal of agent
|
||||||
# AGENT_GOAL=goal_event_flight_invoice # (default) or goal_match_train_invoice
|
AGENT_GOAL=goal_choose_agent_type # (default)
|
||||||
|
|
||||||
|
#Choose which category(ies) of goals you want to be listed by the Agent - options are system (always included), hr, travel, or all.
|
||||||
|
GOAL_CATEGORIES=hr,travel # default is all
|
||||||
|
|
||||||
# Set if the UI should force a user confirmation step or not
|
# Set if the UI should force a user confirmation step or not
|
||||||
SHOW_CONFIRM=True
|
SHOW_CONFIRM=True
|
||||||
32
README.md
32
README.md
@@ -8,23 +8,45 @@ It's really helpful to [watch the demo (5 minute YouTube video)](https://www.you
|
|||||||
|
|
||||||
[](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
[](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
||||||
|
|
||||||
|
## Why Temporal?
|
||||||
|
There are a lot of AI and Agentic AI tools out there, and more on the way. But why Temporal? I asked one of the AI models used in this demo to answer this question (edited minorly):
|
||||||
|
|
||||||
|
### Reliability and State Management:
|
||||||
|
Temporal ensures durability and fault tolerance, which are critical for agentic AI systems that involve long-running, complex workflows. For example, it preserves application state across failures, allowing AI agents to resume from where they left off without losing progress. Major AI companies use this for research experiments and agentic flows, where reliability is essential for continuous exploration.
|
||||||
|
### Handling Complex, Dynamic Workflows:
|
||||||
|
Agentic AI often involves unpredictable, multi-step processes like web crawling or data searching. Temporal’s workflow orchestration simplifies managing these tasks by abstracting complexity, providing features like retries, timeouts, and signals/queries. Temporal makes observability and resuming failed complex experiments and deep searches simple.
|
||||||
|
### Scalability and Speed:
|
||||||
|
Temporal enables rapid development and scaling, crucial for AI systems handling large-scale experiments or production workloads. AI model deployment and SRE teams use it to get code to production quickly with scale as a focus, while research teams can (and do!) run hundreds of experiments daily. Temporal customers report a significant reduction in development time (e.g., 20 weeks to 2 weeks for a feature).
|
||||||
|
### Observability and Debugging:
|
||||||
|
Agentic AI systems need insight into where processes succeed or fail. Temporal provides end-to-end visibility and durable workflow history, which Temporal customers are using to track agentic flows and understand failure points.
|
||||||
|
### Simplified Error Handling:
|
||||||
|
Temporal abstracts failure management (e.g., retries, rollbacks) so developers can focus on AI logic rather than "plumbing" code. This is vital for agentic AI, where external interactions (e.g., APIs, data sources) are prone to failure.
|
||||||
|
### Flexibility for Experimentation:
|
||||||
|
For research-heavy agentic AI, Temporal supports dynamic, code-first workflows and easy integration of new signals/queries, aligning with researchers needs to iterate quickly on experimental paths.
|
||||||
|
|
||||||
|
In essence, Temporal’s value lies in its ability to make agentic AI systems more reliable, scalable, and easier to develop by handling the underlying complexity of distributed workflows for both research and applied AI tasks.
|
||||||
|
|
||||||
|
Temporal was built to solve the problems of distributed computing, including scalability, reliability, security, visibility, and complexity. Agentic AI systems are complex distributed systems, so Temporal should fit well. Scaling, security, and productionalization are major pain points in March 2025 for building agentic systems.
|
||||||
|
|
||||||
## Setup and Configuration
|
## Setup and Configuration
|
||||||
See [the Setup guide](./setup.md).
|
See [the Setup guide](./setup.md).
|
||||||
|
|
||||||
## Interaction
|
## Customizing Interaction & Tools
|
||||||
TODO
|
See [the guide to adding goals and tools](./adding-goals-and-tools.md).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
See [the architecture guide](./architecture.md).
|
See [the architecture guide](./architecture.md).
|
||||||
|
|
||||||
## Productionalization & Adding Features
|
## Productionalization & Adding Features
|
||||||
- In a prod setting, I would need to ensure that payload data is stored separately (e.g. in S3 or a noSQL db - the claim-check pattern), or otherwise 'garbage collected'. Without these techniques, long conversations will fill up the workflow's conversation history, and start to breach Temporal event history payload limits.
|
- In a prod setting, I would need to ensure that payload data is stored separately (e.g. in S3 or a noSQL db - the claim-check pattern), or otherwise 'garbage collected'. Without these techniques, long conversations will fill up the workflow's conversation history, and start to breach Temporal event history payload limits.
|
||||||
- Continue-as-new shouldn't be a big consideration for this use case (as it would take many conversational turns to trigger). Regardless, I should ensure that it's able to carry the agent state over to the new workflow execution.
|
- A single worker can easily support many workflows - setting workflow ID differently would enable this.
|
||||||
|
- Continue-as-new shouldn't be a big consideration for this use case (as it would take many conversational turns to trigger). Regardless, we should verify that it's able to carry the agent state over to the new workflow execution.
|
||||||
- Perhaps the UI should show when the LLM response is being retried (i.e. activity retry attempt because the LLM provided bad output)
|
- Perhaps the UI should show when the LLM response is being retried (i.e. activity retry attempt because the LLM provided bad output)
|
||||||
- Tests would be nice!
|
- Tests would be nice! [See tests](./tests/).
|
||||||
|
|
||||||
See [the todo](./todo.md) for more details.
|
See [the todo](./todo.md) for more details.
|
||||||
|
|
||||||
See Customization for more details. <-- TODO
|
See [the guide to adding goals and tools](./adding-goals-and-tools.md) for more ways you can add features.
|
||||||
|
|
||||||
## For Temporal SAs
|
## For Temporal SAs
|
||||||
Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the enablement guide here (TODO).
|
Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the enablement guide here (TODO).
|
||||||
|
|||||||
@@ -493,7 +493,6 @@ def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
|||||||
# Delegate to the relevant function
|
# Delegate to the relevant function
|
||||||
handler = get_handler(tool_name)
|
handler = get_handler(tool_name)
|
||||||
result = handler(tool_args)
|
result = handler(tool_args)
|
||||||
print(f"in dynamic tool activity, result: {result}")
|
|
||||||
|
|
||||||
# Optionally log or augment the result
|
# Optionally log or augment the result
|
||||||
activity.logger.info(f"Tool '{tool_name}' result: {result}")
|
activity.logger.info(f"Tool '{tool_name}' result: {result}")
|
||||||
|
|||||||
85
adding-goals-and-tools.md
Normal file
85
adding-goals-and-tools.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
## Customizing the Agent
|
||||||
|
The agent is set up to allow for multiple goals and to switch back to choosing a new goal at the end of every successful goal. A goal is made up of a list of tools that the agent will guide the user through.
|
||||||
|
|
||||||
|
It may be helpful to review the [architecture](./architecture.md) for a guide and definition of goals, tools, etc.
|
||||||
|
|
||||||
|
### Adding a Goal
|
||||||
|
1. Open [/tools/goal_registry.py](tools/goal_registry.py) - this file contains descriptions of goals and the tools used to achieve them
|
||||||
|
2. Pick a name for your goal!
|
||||||
|
3. Fill out the required elements:
|
||||||
|
- `id`: needs to be the same as the name
|
||||||
|
- `agent_name`: user-facing name for the agent/chatbot
|
||||||
|
- `agent_friendly_description`: user-facing description of what the agent/chatbot does
|
||||||
|
- `tools`: the list of tools the goal will walk the user through.
|
||||||
|
- Important! The last tool listed must be `list_agents_tool`. This allows the AI to let the user go back to choosing from the list of available goals.
|
||||||
|
- `description`:
|
||||||
|
- `starter-prompt`:
|
||||||
|
- `example_conversation_history`:
|
||||||
|
4. Add your new goal to the `goal_list` at the bottom using `goal_list.append(your_super_sweet_new_goal)`
|
||||||
|
|
||||||
|
### Adding Tools
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
Tools can be optional - you can indicate this in the tool listing of goal description (see above section re: goal registry) by adding something like, "This step is optional and can be skipped by moving to the next tool." Here is an example from the CalendarConflict tool/step of the [goal_hr_schedule_pto](tools/goal_registry.py#L134) goal:
|
||||||
|
|
||||||
|
```
|
||||||
|
description="Help the user gather args for these tools in order: "
|
||||||
|
"1. CurrentPTO: Tell the user how much PTO they currently have "
|
||||||
|
"2. FuturePTO: Tell the user how much PTO they will have as of the prospective date "
|
||||||
|
"3. CalendarConflict: Tell the user what conflicts if any exist around the prospective date on a list of calendars. This step is optional and can be skipped by moving to the next tool. "
|
||||||
|
"4. BookPTO: Book PTO "
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add to Tool Registry
|
||||||
|
- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them)
|
||||||
|
- `name`:
|
||||||
|
- `description`:
|
||||||
|
- `arguments`: These are the _input_ arguments to the tool.
|
||||||
|
|
||||||
|
#### Create Each Tool
|
||||||
|
- The tools themselves are defined in their own files in `/tools` - you can add a subfolder to organize them
|
||||||
|
- The file name and function name will be the same as each other and should also be the same as the name of the tool, without "tool" - so future_pto_tool would be future_pto.py with a function named future_pto within it.
|
||||||
|
- The function should have `args: dict` as the input and also return a `dict`
|
||||||
|
- The return dict should match the output format you specified in the goal's `example_conversation_history`
|
||||||
|
- tools are where the user input+model output becomes deterministic. Add validation here to make sure what the system is doing is valid and acceptable
|
||||||
|
|
||||||
|
#### Add to `tools/__init__.py`
|
||||||
|
- In `tools/__init__.py`, add an import statement for each new tool as well as an applicable return statement in `get_handler`. The tool name here should match the tool name as described in the goal's `description` field.
|
||||||
|
|
||||||
|
### Configuring the Starting Goal
|
||||||
|
|
||||||
|
The agent can be configured to pursue different goals using the `AGENT_GOAL` environment variable in your `.env` file.
|
||||||
|
|
||||||
|
#### Goal: Find an event in Australia / New Zealand, book flights to it and invoice the user for the cost
|
||||||
|
- `AGENT_GOAL=goal_event_flight_invoice` (default) - Helps users find events, book flights, and arrange train travel with invoice generation
|
||||||
|
- This is the scenario in the video above
|
||||||
|
|
||||||
|
#### Goal: Find a Premier League match, book train tickets to it and invoice the user for the cost
|
||||||
|
- `AGENT_GOAL=goal_match_train_invoice` - Focuses on Premier League match attendance with train booking and invoice generation
|
||||||
|
- This is a new goal that is part of an upcoming conference talk
|
||||||
|
|
||||||
|
If not specified, the agent defaults to `goal_event_flight_invoice`. Each goal comes with its own set of tools and conversation flows designed for specific use cases. You can examine `tools/goal_registry.py` to see the detailed configuration of each goal.
|
||||||
|
|
||||||
|
See the next section for tool configuration for each goal.
|
||||||
|
|
||||||
|
### Configuring Existing Tools
|
||||||
|
|
||||||
|
#### Agent Goal: goal_event_flight_invoice (default)
|
||||||
|
* The agent uses a mock function to search for events. This has zero configuration.
|
||||||
|
* By default the agent uses a mock function to search for flights.
|
||||||
|
* If you want to use the real flights API, go to `tools/search_flights.py` and replace the `search_flights` function with `search_flights_real_api` that exists in the same file.
|
||||||
|
* It's free to sign up at [RapidAPI](https://rapidapi.com/apiheya/api/sky-scrapper)
|
||||||
|
* This api might be slow to respond, so you may want to increase the start to close timeout, `TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT` in `workflows/workflow_helpers.py`
|
||||||
|
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env
|
||||||
|
* It's free to sign up and get a key at [Stripe](https://stripe.com/)
|
||||||
|
* If you're lazy go to `tools/create_invoice.py` and replace the `create_invoice` function with the mock `create_invoice_example` that exists in the same file.
|
||||||
|
|
||||||
|
#### Agent Goal: goal_match_train_invoice
|
||||||
|
|
||||||
|
* Finding a match requires a key from [Football Data](https://www.football-data.org). Sign up for a free account, then see the 'My Account' page to get your API token. Set `FOOTBALL_DATA_API_KEY` to this value.
|
||||||
|
* If you're lazy go to `tools/search_fixtures.py` and replace the `search_fixtures` function with the mock `search_fixtures_example` that exists in the same file.
|
||||||
|
* We use a mock function to search for trains. Start the train API server to use the real API: `python thirdparty/train_api.py`
|
||||||
|
* * The train activity is 'enterprise' so it's written in C# and requires a .NET runtime. See the [.NET backend](#net-(enterprise)-backend) section for details on running it.
|
||||||
|
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env
|
||||||
|
* It's free to sign up and get a key at [Stripe](https://stripe.com/)
|
||||||
|
* If you're lazy go to `tools/create_invoice.py` and replace the `create_invoice` function with the mock `create_invoice_example` that exists in the same file.
|
||||||
@@ -1,12 +1,67 @@
|
|||||||
# Elements
|
# Elements
|
||||||
|
These are the main elements of this system.
|
||||||

|

|
||||||
|
|
||||||
talk through the pieces
|
## Workflow
|
||||||
|
This is a [Temporal Workflow](https://docs.temporal.io/workflows) - a durable straightforward description of the process to be executed. For our example see [agent_goal_workflow.py](./workflows/agent_goal_workflow.py).
|
||||||
|
Temporal is used to make the process scalable, durable, reliable, secure, and visible.
|
||||||
|
|
||||||
|
### Workflow Responsibilities:
|
||||||
|
- Orchestrates interactive loop
|
||||||
|
- Prompts LLM, Users
|
||||||
|
- Keeps record of all interactions ([Signals, Queries, Updates](https://docs.temporal.io/develop/python/message-passing))
|
||||||
|
- Executes LLM durably
|
||||||
|
- Executes Tools durably
|
||||||
|
- Handles failures gracefully
|
||||||
|
- Human, LLM and tool interaction history stored for debugging and analysis
|
||||||
|
|
||||||
|
## Activities
|
||||||
|
These are [Temporal Activities](https://docs.temporal.io/activities). Defined as simple functions, they are auto-retried async/event driven behind the scenes. Activities durably execute Tools and the LLM. See [a sample activity](./activities/tool_activities.py).
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
Tools define the capabilities of the system. They are simple Python functions (could be in any language).
|
||||||
|
They are executed by Temporal Activities. They are “just code” - can connect to any API or system. They also are where the "hard" business logic is: you can validate and retry actions using code you write.
|
||||||
|
Failures are handled gracefully by Temporal.
|
||||||
|
|
||||||
|
Activities + Tools turn the probabalistic input from the user and LLM into deterministic action.
|
||||||
|
|
||||||
|
## Prompts
|
||||||
|
Prompts are where the instructions to the LLM & users is. Prompts are made up of initial instructions, goal instructions, and tool instructions.
|
||||||
|
See [agent prompts](./prompts/agent_prompt_generators.py) and [goal & tool prompts](./tools/goal_registry.py).
|
||||||
|
|
||||||
|
This is where you can add probabalistic business logic, to control process flow, describe what to do, and give instruction and validation for the LLM.
|
||||||
|
|
||||||
|
## LLM
|
||||||
|
Probabalistic execution: it will _probably_ do what you tell it to do.
|
||||||
|
Turns the guidance from the prompts (see [agent prompts](./prompts/agent_prompt_generators.py) and [goal prompts](./tools/goal_registry.py)) into
|
||||||
|
You have a choice of providers - see [setup](./setup.md).
|
||||||
|
The LLM:
|
||||||
|
- Validates user input for tools
|
||||||
|
- Drives toward goal selected by user
|
||||||
|
- Decides when to execute tools
|
||||||
|
- Formats input and interprets output for tools
|
||||||
|
- is executed by Temporal Activities
|
||||||
|
- API failures and logical failures are handled transparently
|
||||||
|
|
||||||
|
## Interaction
|
||||||
|
Interaction is managed with Temporal Signals and Queries. These are durably stored in Workflow History.
|
||||||
|
Can be used for analysis and debugging. It's all “just code” so it's easy to add new Signals and Queries.
|
||||||
|
Input can be very dynamic, just needs to be serializable.
|
||||||
|
|
||||||
|
The workflow executes in a loop: gathering input, validating input, executing tools, managing prompts, and then waiting for input.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's a more detailed example for gathering parameters for tools:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
# Architecture Model
|
# Architecture Model
|
||||||
|
Now that we have the pieces and what they do, here is a more complete diagram of how the pieces work together:
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
explain elements
|
|
||||||
|
|
||||||
# Adding features
|
# Adding features
|
||||||
link to how to LLM interactions/how to change
|
Want to add more tools, See [adding goals and tools](./adding-goals-and-tools.md).
|
||||||
BIN
assets/argument_gathering_cycle.png
Normal file
BIN
assets/argument_gathering_cycle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
BIN
assets/interaction_loop.png
Normal file
BIN
assets/interaction_loop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
@@ -18,6 +18,7 @@ class ToolDefinition:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class AgentGoal:
|
class AgentGoal:
|
||||||
id: str
|
id: str
|
||||||
|
category_tag: str
|
||||||
agent_name: str
|
agent_name: str
|
||||||
agent_friendly_description: str
|
agent_friendly_description: str
|
||||||
tools: List[ToolDefinition]
|
tools: List[ToolDefinition]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
import logging
|
||||||
|
|
||||||
from temporalio.worker import Worker
|
from temporalio.worker import Worker
|
||||||
|
|
||||||
@@ -48,6 +49,9 @@ async def main():
|
|||||||
print("===========================================================\n")
|
print("===========================================================\n")
|
||||||
|
|
||||||
print("Worker ready to process tasks!")
|
print("Worker ready to process tasks!")
|
||||||
|
logging.basicConfig(level=logging.WARN)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Run the worker
|
# Run the worker
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
||||||
|
|||||||
43
todo.md
43
todo.md
@@ -1,31 +1,35 @@
|
|||||||
# todo list
|
# todo list
|
||||||
[ ] clean up workflow/make functions
|
[x] clean up workflow/make functions
|
||||||
|
|
||||||
[ ] make the debugging confirms optional <br />
|
[x] make the debugging confirms optional <br />
|
||||||
|
[ ] add confirmation env setting to setup guide <br />
|
||||||
<br />
|
<br />
|
||||||
[ ] document *why* temporal for ai agents - scalability, durability, visibility in the readme <br />
|
[x] document *why* temporal for ai agents - scalability, durability, visibility in the readme <br />
|
||||||
[ ] fix readme: move setup to its own page, demo to its own page, add the why /|\ section <br />
|
[x] fix readme: move setup to its own page, demo to its own page, add the why /|\ section <br />
|
||||||
[ ] add architecture to readme <br />
|
[x] add architecture to readme <br />
|
||||||
- elements of app <br />
|
- elements of app <br />
|
||||||
- dive into llm interaction <br />
|
- dive into llm interaction <br />
|
||||||
- workflow breakdown - interactive loop <br />
|
- workflow breakdown - interactive loop <br />
|
||||||
- why temporal <br />
|
- why temporal <br />
|
||||||
|
|
||||||
[ ] setup readme, why readme, architecture readme, what this is in main readme with temporal value props and pictures <br />
|
[x] setup readme, why readme, architecture readme, what this is in main readme with temporal value props and pictures <br />
|
||||||
[ ] how to add more scenarios, tools <br />
|
[ ] how to add more scenarios, tools <br />
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
[ ] create tests<br />
|
|
||||||
|
|
||||||
[ ] create people management scenario <br />
|
[ ] create tests<br />
|
||||||
- check pay status <br />
|
[ ] fix logging statements not to be all warn, maybe set logging level to info
|
||||||
- book work travel <br />
|
|
||||||
- check PTO levels <br />
|
[ ] create people management scenarios <br />
|
||||||
- check insurance coverages <br />
|
[x] 1. Schedule PTO goal
|
||||||
- book PTO around a date (https://developers.google.com/calendar/api/guides/overview)? <br />
|
-- [ ] check current PTO level <br />
|
||||||
- scenario should use multiple tools <br />
|
-- [ ] determine PTO available as of date <br />
|
||||||
- expense management <br />
|
-- [ ] check for personal, team, or both calendar conflicts <br />
|
||||||
- check in on the health of the team <br />
|
-- [ ] book PTO around a date (send calendar invite?) (https://developers.google.com/calendar/api/guides/overview)? <br />
|
||||||
|
[ ] 2. Others:
|
||||||
|
-- check pay status <br />
|
||||||
|
-- book work travel <br />
|
||||||
|
-- check insurance coverages <br />
|
||||||
|
-- expense management <br />
|
||||||
|
-- check in on the health of the team <br />
|
||||||
|
|
||||||
[ ] demo the reasons why: <br />
|
[ ] demo the reasons why: <br />
|
||||||
- Orchestrate interactions across distributed data stores and tools <br />
|
- Orchestrate interactions across distributed data stores and tools <br />
|
||||||
@@ -42,4 +46,5 @@
|
|||||||
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br />
|
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br />
|
||||||
[ ] make it so you can yeet yourself out of a goal and pick a new one <br />
|
[ ] make it so you can yeet yourself out of a goal and pick a new one <br />
|
||||||
|
|
||||||
[ ] add visual feedback when workflow starting
|
[ ] add visual feedback when workflow starting <br />
|
||||||
|
[ ] figure out how to allow user to list agents at any time - like end conversation <br />
|
||||||
@@ -8,6 +8,10 @@ from .list_agents import list_agents
|
|||||||
from .change_goal import change_goal
|
from .change_goal import change_goal
|
||||||
from .transfer_control import transfer_control
|
from .transfer_control import transfer_control
|
||||||
|
|
||||||
|
from .hr.current_pto import current_pto
|
||||||
|
from .hr.book_pto import book_pto
|
||||||
|
from .hr.future_pto_calc import future_pto_calc
|
||||||
|
|
||||||
|
|
||||||
def get_handler(tool_name: str):
|
def get_handler(tool_name: str):
|
||||||
if tool_name == "SearchFixtures":
|
if tool_name == "SearchFixtures":
|
||||||
@@ -28,5 +32,11 @@ def get_handler(tool_name: str):
|
|||||||
return change_goal
|
return change_goal
|
||||||
if tool_name == "TransferControl":
|
if tool_name == "TransferControl":
|
||||||
return transfer_control
|
return transfer_control
|
||||||
|
if tool_name == "CurrentPTO":
|
||||||
|
return current_pto
|
||||||
|
if tool_name == "BookPTO":
|
||||||
|
return book_pto
|
||||||
|
if tool_name == "FuturePTOCalc":
|
||||||
|
return future_pto_calc
|
||||||
|
|
||||||
raise ValueError(f"Unknown tool: {tool_name}")
|
raise ValueError(f"Unknown tool: {tool_name}")
|
||||||
|
|||||||
17
tools/data/employee_pto_data.json
Normal file
17
tools/data/employee_pto_data.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"theCompany": {
|
||||||
|
"weLove": "theCompany",
|
||||||
|
"employees": [
|
||||||
|
{
|
||||||
|
"email": "josh.smith@temporal.io",
|
||||||
|
"currentPTOHrs": 400,
|
||||||
|
"hrsAddedPerMonth": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "lainecaseysmith@gmail.com",
|
||||||
|
"currentPTOHrs": 40,
|
||||||
|
"hrsAddedPerMonth": 12
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,45 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from models.tool_definitions import AgentGoal
|
from models.tool_definitions import AgentGoal
|
||||||
from tools.tool_registry import (
|
import tools.tool_registry as tool_registry
|
||||||
search_fixtures_tool,
|
|
||||||
search_flights_tool,
|
|
||||||
search_trains_tool,
|
|
||||||
book_trains_tool,
|
|
||||||
create_invoice_tool,
|
|
||||||
find_events_tool,
|
|
||||||
change_goal_tool,
|
|
||||||
list_agents_tool
|
|
||||||
)
|
|
||||||
|
|
||||||
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job"
|
# Turn on Silly Mode - this should be a description of the persona you'd like the bot to have and can be a single word or a phrase.
|
||||||
|
# Example if you want the bot to be a specific person, like Mario or Christopher Walken, or to describe a specific tone:
|
||||||
|
#SILLY_MODE="Christopher Walken"
|
||||||
|
#SILLY_MODE="belligerent"
|
||||||
|
#
|
||||||
|
# Example if you want it to take on a persona (include 'a'):
|
||||||
|
#SILLY_MODE="a pirate"
|
||||||
|
# Note - this only works with certain LLMs. Grok for sure will stay in character, while OpenAI will not.
|
||||||
|
SILLY_MODE="off"
|
||||||
|
if SILLY_MODE is not None and SILLY_MODE != "off":
|
||||||
|
silly_prompt = "You are " + SILLY_MODE +", stay in character at all times. "
|
||||||
|
print("Silly mode is on: " + SILLY_MODE)
|
||||||
|
else:
|
||||||
|
silly_prompt = ""
|
||||||
|
|
||||||
|
starter_prompt_generic = silly_prompt + "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||||
|
|
||||||
goal_choose_agent_type = AgentGoal(
|
goal_choose_agent_type = AgentGoal(
|
||||||
id = "goal_choose_agent_type",
|
id = "goal_choose_agent_type",
|
||||||
|
category_tag="system",
|
||||||
agent_name="Choose Agent",
|
agent_name="Choose Agent",
|
||||||
agent_friendly_description="Choose the type of agent to assist you today.",
|
agent_friendly_description="Choose the type of agent to assist you today.",
|
||||||
tools=[
|
tools=[
|
||||||
list_agents_tool,
|
tool_registry.list_agents_tool,
|
||||||
change_goal_tool,
|
tool_registry.change_goal_tool,
|
||||||
],
|
],
|
||||||
description="The user wants to choose which type of agent they will interact with. "
|
description="The user wants to choose which type of agent they will interact with. "
|
||||||
"Help the user gather args for these tools, in order: "
|
"Help the user gather args for these tools, in order: "
|
||||||
"1. ListAgents: List agents available to interact with "
|
"1. ListAgents: List agents available to interact with. Do not ask for user confirmation for this tool. "
|
||||||
"2. ChangeGoal: Change goal of agent "
|
"2. ChangeGoal: Change goal of agent "
|
||||||
"After these tools are complete, change your goal to the new goal as chosen by the user. ",
|
"After these tools are complete, change your goal to the new goal as chosen by the user. ",
|
||||||
starter_prompt=starter_prompt_generic,
|
starter_prompt=starter_prompt_generic + "Begin by listing all details of all agents as provided by the output of the first tool included in this goal. ",
|
||||||
example_conversation_history="\n ".join(
|
example_conversation_history="\n ".join(
|
||||||
[
|
[
|
||||||
"user: I'd like to choose an agent",
|
"agent: Here are the currently available agents.",
|
||||||
"agent: Sure! Would you like me to list the available agents?",
|
|
||||||
"user_confirmed_tool_run: <user clicks confirm on ListAgents tool>",
|
"user_confirmed_tool_run: <user clicks confirm on ListAgents tool>",
|
||||||
"tool_result: { 'agent_name': 'Event Flight Finder', 'goal_id': 'goal_event_flight_invoice', 'agent_description': 'Helps users find interesting events and arrange travel to them' }",
|
"tool_result: { 'agent_name': 'Event Flight Finder', 'goal_id': 'goal_event_flight_invoice', 'agent_description': 'Helps users find interesting events and arrange travel to them' }",
|
||||||
"agent: The available agents are: 1. Event Flight Finder. Which agent would you like to speak to?",
|
"agent: The available agents are: 1. Event Flight Finder. \n Which agent would you like to speak to?",
|
||||||
"user: 1",
|
"user: 1",
|
||||||
"user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>",
|
"user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>",
|
||||||
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
|
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
|
||||||
@@ -43,14 +49,15 @@ goal_choose_agent_type = AgentGoal(
|
|||||||
|
|
||||||
goal_match_train_invoice = AgentGoal(
|
goal_match_train_invoice = AgentGoal(
|
||||||
id = "goal_match_train_invoice",
|
id = "goal_match_train_invoice",
|
||||||
|
category_tag="travel",
|
||||||
agent_name="UK Premier League Match Trip Booking",
|
agent_name="UK Premier League Match Trip Booking",
|
||||||
agent_friendly_description="Book a trip to a city in the UK around the dates of a premier league match.",
|
agent_friendly_description="Book a trip to a city in the UK around the dates of a premier league match.",
|
||||||
tools=[
|
tools=[
|
||||||
search_fixtures_tool,
|
tool_registry.search_fixtures_tool,
|
||||||
search_trains_tool,
|
tool_registry.search_trains_tool,
|
||||||
book_trains_tool,
|
tool_registry.book_trains_tool,
|
||||||
create_invoice_tool,
|
tool_registry.create_invoice_tool,
|
||||||
list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
|
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
|
||||||
],
|
],
|
||||||
description="The user wants to book a trip to a city in the UK around the dates of a premier league match. "
|
description="The user wants to book a trip to a city in the UK around the dates of a premier league match. "
|
||||||
"Help the user find a premier league match to attend, search and book trains for that match and offers to invoice them for the cost of train tickets. "
|
"Help the user find a premier league match to attend, search and book trains for that match and offers to invoice them for the cost of train tickets. "
|
||||||
@@ -90,13 +97,14 @@ goal_match_train_invoice = AgentGoal(
|
|||||||
|
|
||||||
goal_event_flight_invoice = AgentGoal(
|
goal_event_flight_invoice = AgentGoal(
|
||||||
id = "goal_event_flight_invoice",
|
id = "goal_event_flight_invoice",
|
||||||
|
category_tag="travel",
|
||||||
agent_name="Australia and New Zealand Event Flight Booking",
|
agent_name="Australia and New Zealand Event Flight Booking",
|
||||||
agent_friendly_description="Book a trip to a city in Australia or New Zealand around the dates of events in that city.",
|
agent_friendly_description="Book a trip to a city in Australia or New Zealand around the dates of events in that city.",
|
||||||
tools=[
|
tools=[
|
||||||
find_events_tool,
|
tool_registry.find_events_tool,
|
||||||
search_flights_tool,
|
tool_registry.search_flights_tool,
|
||||||
create_invoice_tool,
|
tool_registry.create_invoice_tool,
|
||||||
list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
|
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
|
||||||
],
|
],
|
||||||
description="Help the user gather args for these tools in order: "
|
description="Help the user gather args for these tools in order: "
|
||||||
"1. FindEvents: Find an event to travel to "
|
"1. FindEvents: Find an event to travel to "
|
||||||
@@ -126,8 +134,53 @@ goal_event_flight_invoice = AgentGoal(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# This goal uses the data/employee_pto_data.json file as dummy data.
|
||||||
|
goal_hr_schedule_pto = AgentGoal(
|
||||||
|
id = "goal_hr_schedule_pto",
|
||||||
|
category_tag="hr",
|
||||||
|
agent_name="Schedule PTO",
|
||||||
|
agent_friendly_description="Schedule PTO based on your available PTO.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.current_pto_tool,
|
||||||
|
tool_registry.future_pto_calc_tool,
|
||||||
|
tool_registry.book_pto_tool,
|
||||||
|
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
|
||||||
|
],
|
||||||
|
description="The user wants to schedule paid time off (PTO) after today's date. To assist with that goal, help the user gather args for these tools in order: "
|
||||||
|
"1. CurrentPTO: Tell the user how much PTO they currently have "
|
||||||
|
"2. FuturePTOCalc: Tell the user how much PTO they will have as of the prospective future date "
|
||||||
|
"3. BookPTO: Book PTO after user types 'yes'",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to schedule some time off",
|
||||||
|
"agent: Sure! Let's start by determining how much PTO you currently have. May I have your email address?",
|
||||||
|
"user: bob.johnson@emailzzz.com",
|
||||||
|
"agent: Great! I can tell you how much PTO you currently have accrued.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on CurrentPTO tool>",
|
||||||
|
"tool_result: { 'num_hours': 400, 'num_days': 50 }",
|
||||||
|
"agent: You have 400 hours, or 50 days, of PTO available. What dates would you like to take your time off? ",
|
||||||
|
"user: Dec 1 through Dec 5",
|
||||||
|
"agent: Let's check if you'll have enough PTO accrued by Dec 1 of this year to accomodate that.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FuturePTO tool>"
|
||||||
|
'tool_result: {"enough_pto": True, "pto_hrs_remaining_after": 410}',
|
||||||
|
"agent: You do in fact have enough PTO to accommodate that, and will have 410 hours remaining after you come back. Do you want to check calendars for conflicts? If so, please provide one of the following: self, team, or both "
|
||||||
|
"user: both ",
|
||||||
|
"agent: Okay, checking both calendars for conflicts ",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on CheckCalendarConflict tool>",
|
||||||
|
'tool_result: { "calendar": "self", "title": "Meeting with Karen", "date": "2025-12-02", "time": "10:00AM"}',
|
||||||
|
"agent: On your calendar, you have a conflict: Meeting with Karen at 10AM Dec 2, 2025. Do you want to book the PTO?"
|
||||||
|
"user: yes "
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on BookPTO tool>",
|
||||||
|
'tool_result: { "status": "success" }',
|
||||||
|
"agent: PTO successfully booked! ",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
#Add the goals to a list for more generic processing, like listing available agents
|
#Add the goals to a list for more generic processing, like listing available agents
|
||||||
goal_list: List[AgentGoal] = []
|
goal_list: List[AgentGoal] = []
|
||||||
goal_list.append(goal_choose_agent_type)
|
goal_list.append(goal_choose_agent_type)
|
||||||
goal_list.append(goal_event_flight_invoice)
|
goal_list.append(goal_event_flight_invoice)
|
||||||
goal_list.append(goal_match_train_invoice)
|
goal_list.append(goal_match_train_invoice)
|
||||||
|
goal_list.append(goal_hr_schedule_pto)
|
||||||
|
|||||||
11
tools/hr/book_pto.py
Normal file
11
tools/hr/book_pto.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
def book_pto(args: dict) -> dict:
|
||||||
|
|
||||||
|
email = args.get("email")
|
||||||
|
start_date = args.get("start_date")
|
||||||
|
end_date = args.get("end_date")
|
||||||
|
|
||||||
|
print(f"[BookPTO] Totally would send an email confirmation of PTO from {start_date} to {end_date} to {email} here!")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
26
tools/hr/current_pto.py
Normal file
26
tools/hr/current_pto.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def current_pto(args: dict) -> dict:
|
||||||
|
|
||||||
|
email = args.get("email")
|
||||||
|
|
||||||
|
file_path = Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
||||||
|
if not file_path.exists():
|
||||||
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
|
data = json.load(open(file_path))
|
||||||
|
employee_list = data["theCompany"]["employees"]
|
||||||
|
|
||||||
|
for employee in employee_list:
|
||||||
|
if employee["email"] == email:
|
||||||
|
num_hours = int(employee["currentPTOHrs"])
|
||||||
|
num_days = float(num_hours/8)
|
||||||
|
return {
|
||||||
|
"num_hours": num_hours,
|
||||||
|
"num_days": num_days,
|
||||||
|
}
|
||||||
|
|
||||||
|
return_msg = "Employee not found with email address " + email
|
||||||
|
return {"error": return_msg}
|
||||||
60
tools/hr/future_pto_calc.py
Normal file
60
tools/hr/future_pto_calc.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import json
|
||||||
|
import pandas
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import date, datetime
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
|
||||||
|
def future_pto_calc(args: dict) -> dict:
|
||||||
|
|
||||||
|
file_path = Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
||||||
|
if not file_path.exists():
|
||||||
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
|
start_date = datetime.strptime(args.get("start_date"), "%Y-%m-%d").date()
|
||||||
|
end_date = datetime.strptime(args.get("end_date"), "%Y-%m-%d").date()
|
||||||
|
email = args.get("email")
|
||||||
|
|
||||||
|
#Next, set up the ability to calculate how much PTO will be added to the user's total by the start of the PTO request
|
||||||
|
today = date.today()
|
||||||
|
|
||||||
|
if today > start_date:
|
||||||
|
return_msg = "PTO start date " + args.get("start_date") + "cannot be in the past"
|
||||||
|
return {"error": return_msg}
|
||||||
|
|
||||||
|
if end_date < start_date:
|
||||||
|
return_msg = "PTO end date " + args.get("end_date") + " must be after PTO start date " + args.get("start_date")
|
||||||
|
return {"error": return_msg}
|
||||||
|
|
||||||
|
#Get the number of business days, and then business hours (assume 8 hr biz day), included in the PTO request
|
||||||
|
biz_days_of_request = len(pandas.bdate_range(start=start_date, end=end_date, inclusive="both"))
|
||||||
|
if biz_days_of_request == 0:
|
||||||
|
return_msg = "There are no business days between " + args.get("start_date") + " and " + args.get("end_date")
|
||||||
|
return {"error": return_msg}
|
||||||
|
biz_hours_of_request = biz_days_of_request * 8
|
||||||
|
|
||||||
|
#Assume PTO is added on the first of every month - month math compares rolling dates, so compare the PTO request with the first day of the current month.
|
||||||
|
today_first_of_month = date(today.year, today.month, 1)
|
||||||
|
time_difference = relativedelta(start_date, today_first_of_month)
|
||||||
|
months_to_accrue = time_difference.years * 12 + time_difference.months
|
||||||
|
|
||||||
|
data = json.load(open(file_path))
|
||||||
|
employee_list = data["theCompany"]["employees"]
|
||||||
|
|
||||||
|
enough_pto = False
|
||||||
|
|
||||||
|
for employee in employee_list:
|
||||||
|
if employee["email"] == email:
|
||||||
|
current_pto_hours = int(employee["currentPTOHrs"])
|
||||||
|
hrs_added_per_month = int(employee["hrsAddedPerMonth"])
|
||||||
|
pto_available_at_start = current_pto_hours + (months_to_accrue * hrs_added_per_month)
|
||||||
|
pto_hrs_remaining_after = pto_available_at_start - biz_hours_of_request
|
||||||
|
if pto_hrs_remaining_after >= 0:
|
||||||
|
enough_pto = True
|
||||||
|
return {
|
||||||
|
"enough_pto": enough_pto,
|
||||||
|
"pto_hrs_remaining_after": str(pto_hrs_remaining_after),
|
||||||
|
}
|
||||||
|
|
||||||
|
return_msg = "Employee not found with email address " + email
|
||||||
|
return {"error": return_msg}
|
||||||
@@ -1,10 +1,26 @@
|
|||||||
|
import os
|
||||||
import tools.goal_registry as goals
|
import tools.goal_registry as goals
|
||||||
|
|
||||||
def list_agents(args: dict) -> dict:
|
def list_agents(args: dict) -> dict:
|
||||||
|
|
||||||
|
goal_categories_start = os.getenv("GOAL_CATEGORIES")
|
||||||
|
if goal_categories_start is None:
|
||||||
|
goal_categories = ["all"] # default to 'all' categories
|
||||||
|
else:
|
||||||
|
goal_categories_start.strip().lower() # handle extra spaces or non-lowercase
|
||||||
|
goal_categories = goal_categories_start.split(",")
|
||||||
|
|
||||||
|
# always show goals labeled as "system," like the goal chooser
|
||||||
|
if "system" not in goal_categories:
|
||||||
|
goal_categories.append("system")
|
||||||
|
|
||||||
agents = []
|
agents = []
|
||||||
if goals.goal_list is not None:
|
if goals.goal_list is not None:
|
||||||
for goal in goals.goal_list:
|
for goal in goals.goal_list:
|
||||||
|
# add to list if either
|
||||||
|
# - all
|
||||||
|
# - current goal's tag is in goal_categories
|
||||||
|
if "all" in goal_categories or goal.category_tag in goal_categories:
|
||||||
agents.append(
|
agents.append(
|
||||||
{
|
{
|
||||||
"agent_name": goal.agent_name,
|
"agent_name": goal.agent_name,
|
||||||
|
|||||||
@@ -142,3 +142,84 @@ find_events_tool = ToolDefinition(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_pto_tool = ToolDefinition(
|
||||||
|
name="CurrentPTO",
|
||||||
|
description="Find how much PTO a user currently has accrued. "
|
||||||
|
"Returns the number of hours and (calculated) number of days of PTO. ",
|
||||||
|
arguments=[
|
||||||
|
ToolArgument(
|
||||||
|
name="email",
|
||||||
|
type="string",
|
||||||
|
description="email address of user",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
future_pto_calc_tool = ToolDefinition(
|
||||||
|
name="FuturePTOCalc",
|
||||||
|
description="Calculate if the user will have enough PTO as of their proposed date to accommodate the request. The proposed start and end dates should be in the future. "
|
||||||
|
"Returns a boolean enough_pto and how many hours of PTO they will have remaining if they take the proposed dates. ",
|
||||||
|
arguments=[
|
||||||
|
ToolArgument(
|
||||||
|
name="start_date",
|
||||||
|
type="string",
|
||||||
|
description="Start date of proposed PTO",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="end_date",
|
||||||
|
type="string",
|
||||||
|
description="End date of proposed PTO",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="email",
|
||||||
|
type="string",
|
||||||
|
description="email address of user",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
calendar_conflict_tool = ToolDefinition(
|
||||||
|
name="CalendarConflict",
|
||||||
|
description="Determine if the proposed PTO date(s) have conflicts. Returns list of conflicts. ",
|
||||||
|
arguments=[
|
||||||
|
ToolArgument(
|
||||||
|
name="check_self_calendar",
|
||||||
|
type="boolean",
|
||||||
|
description="Check self calendar for conflicts?",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="check_team_calendar",
|
||||||
|
type="boolean",
|
||||||
|
description="Check team calendar for conflicts?",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
book_pto_tool = ToolDefinition(
|
||||||
|
name="BookPTO",
|
||||||
|
description="Book PTO start and end date. Either 1) makes calendar item, or 2) sends calendar invite to self and boss? "
|
||||||
|
"Returns a success indicator. ",
|
||||||
|
arguments=[
|
||||||
|
ToolArgument(
|
||||||
|
name="start_date",
|
||||||
|
type="string",
|
||||||
|
description="Start date of proposed PTO",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="end_date",
|
||||||
|
type="string",
|
||||||
|
description="End date of proposed PTO",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="email",
|
||||||
|
type="string",
|
||||||
|
description="Email address of user, used to look up current PTO",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="userConfirmation",
|
||||||
|
type="string",
|
||||||
|
description="Indication of user's desire to book PTO",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -77,55 +77,32 @@ class AgentGoalWorkflow:
|
|||||||
# This is the main interactive loop. Main responsibilities:
|
# This is the main interactive loop. Main responsibilities:
|
||||||
# - Selecting and changing goals as directed by the user
|
# - Selecting and changing goals as directed by the user
|
||||||
# - reacting to user input (from signals)
|
# - reacting to user input (from signals)
|
||||||
# - calling activities to determine next steps and prompts
|
# - validating user input to make sure it makes sense with the current goal and tools
|
||||||
# - executing the selected tools
|
# - calling the LLM through activities to determine next steps and prompts
|
||||||
|
# - executing the selected tools via activities
|
||||||
while True:
|
while True:
|
||||||
# wait indefinitely for input from signals - user_prompt, end_chat, or confirm as defined below
|
# wait indefinitely for input from signals - user_prompt, end_chat, or confirm as defined below
|
||||||
await workflow.wait_condition(
|
await workflow.wait_condition(
|
||||||
lambda: bool(self.prompt_queue) or self.chat_ended or self.confirm
|
lambda: bool(self.prompt_queue) or self.chat_ended or self.confirm
|
||||||
)
|
)
|
||||||
|
|
||||||
# handle chat-end signal
|
# handle chat should end. When chat ends, push conversation history to workflow results.
|
||||||
if self.chat_ended:
|
if self.chat_should_end():
|
||||||
workflow.logger.warning(f"workflow step: chat-end signal received, ending")
|
|
||||||
workflow.logger.info("Chat ended.")
|
|
||||||
return f"{self.conversation_history}"
|
return f"{self.conversation_history}"
|
||||||
|
|
||||||
# Execute the tool
|
# Execute the tool
|
||||||
if self.confirm and waiting_for_confirm and current_tool and self.tool_data:
|
if self.ready_for_tool_execution(waiting_for_confirm, current_tool):
|
||||||
workflow.logger.warning(f"workflow step: user has confirmed, executing the tool {current_tool}")
|
waiting_for_confirm = await self.execute_tool(current_tool)
|
||||||
self.confirm = False
|
|
||||||
waiting_for_confirm = False
|
|
||||||
|
|
||||||
confirmed_tool_data = self.tool_data.copy()
|
|
||||||
confirmed_tool_data["next"] = "user_confirmed_tool_run"
|
|
||||||
self.add_message("user_confirmed_tool_run", confirmed_tool_data)
|
|
||||||
|
|
||||||
# execute the tool by key as defined in tools/__init__.py
|
|
||||||
await helpers.handle_tool_execution(
|
|
||||||
current_tool,
|
|
||||||
self.tool_data,
|
|
||||||
self.tool_results,
|
|
||||||
self.add_message,
|
|
||||||
self.prompt_queue
|
|
||||||
)
|
|
||||||
|
|
||||||
#set new goal if we should
|
|
||||||
if len(self.tool_results) > 0:
|
|
||||||
if "ChangeGoal" in self.tool_results[-1].values() and "new_goal" in self.tool_results[-1].keys():
|
|
||||||
new_goal = self.tool_results[-1].get("new_goal")
|
|
||||||
workflow.logger.warning(f"Booya new goal!: {new_goal}")
|
|
||||||
self.change_goal(new_goal)
|
|
||||||
elif "ListAgents" in self.tool_results[-1].values() and self.goal.id != "goal_choose_agent_type":
|
|
||||||
workflow.logger.warning("setting goal to goal_choose_agent_type")
|
|
||||||
self.change_goal("goal_choose_agent_type")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# if we've received messages to be processed on the prompt queue...
|
# process forward on the prompt queue if any
|
||||||
if self.prompt_queue:
|
if self.prompt_queue:
|
||||||
|
# get most recent prompt
|
||||||
prompt = self.prompt_queue.popleft()
|
prompt = self.prompt_queue.popleft()
|
||||||
workflow.logger.warning(f"workflow step: processing message on the prompt queue, message is {prompt}")
|
workflow.logger.info(f"workflow step: processing message on the prompt queue, message is {prompt}")
|
||||||
if not prompt.startswith("###"): #if the message isn't from the LLM but is instead from the user
|
|
||||||
|
# Validate user-provided prompts
|
||||||
|
if self.is_user_prompt(prompt):
|
||||||
self.add_message("user", prompt)
|
self.add_message("user", prompt)
|
||||||
|
|
||||||
# Validate the prompt before proceeding
|
# Validate the prompt before proceeding
|
||||||
@@ -171,7 +148,8 @@ class AgentGoalWorkflow:
|
|||||||
next_step = tool_data.get("next")
|
next_step = tool_data.get("next")
|
||||||
current_tool = tool_data.get("tool")
|
current_tool = tool_data.get("tool")
|
||||||
|
|
||||||
workflow.logger.warning(f"next_step: {next_step}, current tool is {current_tool}")
|
workflow.logger.info(f"next_step: {next_step}, current tool is {current_tool}")
|
||||||
|
|
||||||
#if the next step is to confirm...
|
#if the next step is to confirm...
|
||||||
if next_step == "confirm" and current_tool:
|
if next_step == "confirm" and current_tool:
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
@@ -212,9 +190,9 @@ class AgentGoalWorkflow:
|
|||||||
@workflow.signal
|
@workflow.signal
|
||||||
async def user_prompt(self, prompt: str) -> None:
|
async def user_prompt(self, prompt: str) -> None:
|
||||||
"""Signal handler for receiving user prompts."""
|
"""Signal handler for receiving user prompts."""
|
||||||
workflow.logger.warning(f"signal received: user_prompt, prompt is {prompt}")
|
workflow.logger.info(f"signal received: user_prompt, prompt is {prompt}")
|
||||||
if self.chat_ended:
|
if self.chat_ended:
|
||||||
workflow.logger.warning(f"Message dropped due to chat closed: {prompt}")
|
workflow.logger.info(f"Message dropped due to chat closed: {prompt}")
|
||||||
return
|
return
|
||||||
self.prompt_queue.append(prompt)
|
self.prompt_queue.append(prompt)
|
||||||
|
|
||||||
@@ -222,15 +200,14 @@ class AgentGoalWorkflow:
|
|||||||
@workflow.signal
|
@workflow.signal
|
||||||
async def confirm(self) -> None:
|
async def confirm(self) -> None:
|
||||||
"""Signal handler for user confirmation of tool execution."""
|
"""Signal handler for user confirmation of tool execution."""
|
||||||
workflow.logger.info("Received user confirmation")
|
workflow.logger.info("Received user signal: confirmation")
|
||||||
workflow.logger.warning(f"signal recieved: confirm")
|
|
||||||
self.confirm = True
|
self.confirm = True
|
||||||
|
|
||||||
#Signal that comes from api/main.py via a post to /end-chat
|
#Signal that comes from api/main.py via a post to /end-chat
|
||||||
@workflow.signal
|
@workflow.signal
|
||||||
async def end_chat(self) -> None:
|
async def end_chat(self) -> None:
|
||||||
"""Signal handler for ending the chat session."""
|
"""Signal handler for ending the chat session."""
|
||||||
workflow.logger.warning("signal received: end_chat")
|
workflow.logger.info("signal received: end_chat")
|
||||||
self.chat_ended = True
|
self.chat_ended = True
|
||||||
|
|
||||||
@workflow.query
|
@workflow.query
|
||||||
@@ -283,5 +260,61 @@ class AgentGoalWorkflow:
|
|||||||
if listed_goal.id == goal:
|
if listed_goal.id == goal:
|
||||||
self.goal = listed_goal
|
self.goal = listed_goal
|
||||||
# self.goal = goals.get(goal)
|
# self.goal = goals.get(goal)
|
||||||
workflow.logger.warning("Changed goal to " + goal)
|
workflow.logger.info("Changed goal to " + goal)
|
||||||
#todo reset goal or tools if this doesn't work or whatever
|
#todo reset goal or tools if this doesn't work or whatever
|
||||||
|
|
||||||
|
# workflow function that defines if chat should end
|
||||||
|
def chat_should_end(self) -> bool:
|
||||||
|
if self.chat_ended:
|
||||||
|
workflow.logger.info("Chat-end signal received. Chat ending.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# define if we're ready for tool execution
|
||||||
|
def ready_for_tool_execution(self, waiting_for_confirm: bool, current_tool: Any) -> bool:
|
||||||
|
if self.confirm and waiting_for_confirm and current_tool and self.tool_data:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# LLM-tagged prompts start with "###"
|
||||||
|
# all others are from the user
|
||||||
|
def is_user_prompt(self, prompt) -> bool:
|
||||||
|
if prompt.startswith("###"):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# execute the tool - return False if we're not waiting for confirm anymore (always the case if it works successfully)
|
||||||
|
#
|
||||||
|
async def execute_tool(self, current_tool: str)->bool:
|
||||||
|
workflow.logger.info(f"workflow step: user has confirmed, executing the tool {current_tool}")
|
||||||
|
self.confirm = False
|
||||||
|
waiting_for_confirm = False
|
||||||
|
confirmed_tool_data = self.tool_data.copy()
|
||||||
|
confirmed_tool_data["next"] = "user_confirmed_tool_run"
|
||||||
|
self.add_message("user_confirmed_tool_run", confirmed_tool_data)
|
||||||
|
|
||||||
|
# execute the tool by key as defined in tools/__init__.py
|
||||||
|
await helpers.handle_tool_execution(
|
||||||
|
current_tool,
|
||||||
|
self.tool_data,
|
||||||
|
self.tool_results,
|
||||||
|
self.add_message,
|
||||||
|
self.prompt_queue
|
||||||
|
)
|
||||||
|
|
||||||
|
#set new goal if we should
|
||||||
|
if len(self.tool_results) > 0:
|
||||||
|
if "ChangeGoal" in self.tool_results[-1].values() and "new_goal" in self.tool_results[-1].keys():
|
||||||
|
new_goal = self.tool_results[-1].get("new_goal")
|
||||||
|
workflow.logger.info(f"Booya new goal!: {new_goal}")
|
||||||
|
self.change_goal(new_goal)
|
||||||
|
elif "ListAgents" in self.tool_results[-1].values() and self.goal.id != "goal_choose_agent_type":
|
||||||
|
workflow.logger.info("setting goal to goal_choose_agent_type")
|
||||||
|
self.change_goal("goal_choose_agent_type")
|
||||||
|
return waiting_for_confirm
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user