mirror of
https://github.com/temporal-community/temporal-ai-agent.git
synced 2026-03-15 22:18:09 +01:00
Merge pull request #29 from temporal-community/josh-multiagent-steve-fixes
Josh multiagent capability, Steve fixes. Original PR: https://github.com/temporal-community/temporal-ai-agent/pull/25
This commit is contained in:
25
.env.example
25
.env.example
@@ -1,5 +1,6 @@
|
||||
RAPIDAPI_KEY=9df2cb5...
|
||||
RAPIDAPI_HOST=sky-scrapper.p.rapidapi.com
|
||||
RAPIDAPI_HOST_FLIGHTS=sky-scrapper.p.rapidapi.com #For travel flight information tool
|
||||
RAPIDAPI_HOST_PACKAGE=trackingpackage.p.rapidapi.com #For eCommerce order status package tracking tool
|
||||
FOOTBALL_DATA_API_KEY=....
|
||||
|
||||
STRIPE_API_KEY=sk_test_51J...
|
||||
@@ -7,6 +8,9 @@ STRIPE_API_KEY=sk_test_51J...
|
||||
LLM_PROVIDER=openai # default
|
||||
OPENAI_API_KEY=sk-proj-...
|
||||
# or
|
||||
#LLM_PROVIDER=grok
|
||||
#GROK_API_KEY=xai-your-grok-api-key
|
||||
# or
|
||||
# LLM_PROVIDER=ollama
|
||||
# OLLAMA_MODEL_NAME=qwen2.5:14b
|
||||
# or
|
||||
@@ -32,5 +36,20 @@ OPENAI_API_KEY=sk-proj-...
|
||||
# Uncomment if using API key (not needed for local dev server)
|
||||
# TEMPORAL_API_KEY=abcdef1234567890
|
||||
|
||||
# Agent Goal Configuration
|
||||
# AGENT_GOAL=goal_event_flight_invoice # (default) or goal_match_train_invoice
|
||||
# Set starting goal of agent - if unset default is goal_choose_agent_type
|
||||
AGENT_GOAL=goal_choose_agent_type # for multi-goal start
|
||||
#AGENT_GOAL=goal_event_flight_invoice # for original goal
|
||||
#AGENT_GOAL=goal_match_train_invoice # for replay goal
|
||||
|
||||
#Choose which category(ies) of goals you want to be listed by the Agent Goal picker if enabled above
|
||||
# - options are system (always included), hr, travel, or all.
|
||||
GOAL_CATEGORIES=fin # default is all
|
||||
#GOAL_CATEGORIES=travel-flights
|
||||
|
||||
# Set if the workflow should wait for the user to click a confirm button (and if the UI should show the confirm button and tool args)
|
||||
SHOW_CONFIRM=True
|
||||
|
||||
# Money Scenarios:
|
||||
# Set if you want it to really start workflows - otherwise it'll fake it
|
||||
# if you want it to be real you'll need moneytransfer and early return workers running
|
||||
FIN_START_REAL_WORKFLOW=FALSE
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,4 +31,5 @@ coverage.xml
|
||||
# PyCharm / IntelliJ settings
|
||||
.idea/
|
||||
|
||||
.env
|
||||
.env
|
||||
.env*
|
||||
|
||||
226
README.md
226
README.md
@@ -2,204 +2,52 @@
|
||||
|
||||
This demo shows a multi-turn conversation with an AI agent running inside a Temporal workflow. The purpose of the agent is to collect information towards a goal, running tools along the way. There's a simple DSL input for collecting information (currently set up to use mock functions to search for public events, search for flights around those events, then create a test Stripe invoice for the trip).
|
||||
|
||||
The AI will respond with clarifications and ask for any missing information to that goal. You can configure it to use [ChatGPT 4o](https://openai.com/index/hello-gpt-4o/), [Anthropic Claude](https://www.anthropic.com/claude), [Google Gemini](https://gemini.google.com), [Deepseek-V3](https://www.deepseek.com/) or a local LLM of your choice using [Ollama](https://ollama.com).
|
||||
The AI will respond with clarifications and ask for any missing information to that goal. You can configure it to use [ChatGPT 4o](https://openai.com/index/hello-gpt-4o/), [Anthropic Claude](https://www.anthropic.com/claude), [Google Gemini](https://gemini.google.com), [Deepseek-V3](https://www.deepseek.com/), [Grok](https://docs.x.ai/docs/overview) or a local LLM of your choice using [Ollama](https://ollama.com).
|
||||
|
||||
[Watch the demo (5 minute YouTube video)](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
||||
It's really helpful to [watch the demo (5 minute YouTube video)](https://www.youtube.com/watch?v=GEXllEH2XiQ) to understand how interaction works.
|
||||
|
||||
[](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
||||
[](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
||||
|
||||
## Configuration
|
||||
### Multi-Agent Demo Video
|
||||
See multi-agent execution in action [here](https://www.youtube.com/watch?v=8Dc_0dC14yY).
|
||||
|
||||
This application uses `.env` files for configuration. Copy the [.env.example](.env.example) file to `.env` and update the values:
|
||||
## Why Temporal?
|
||||
There are a lot of AI and Agentic AI tools out there, and more on the way. But why Temporal? Temporal gives this system reliablity, state management, a code-first approach that we really like, built-in observability and easy error handling.
|
||||
For more, check out [architecture-decisions](./architecture-decisions.md).
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
## What is "Agentic AI"?
|
||||
These are the key elements of an agentic framework:
|
||||
1. Goals that a system can accomplish, made up of tools that can execute individual steps
|
||||
2. Agent loops - executing an LLM, executing tools, and eliciting input from an external source such as a human: repeat until goal(s) are done
|
||||
3. Support for tool calls that require input and approval
|
||||
4. Use of an LLM to check human input for relevance before calling the 'real' LLM
|
||||
5. Use of an LLM to summarize and compact the conversation history
|
||||
6. Prompt construction made of system prompts, conversation history, and tool metadata - sent to the LLM to create user questions and confirmations
|
||||
7. Ideally high durability (done in this system with Temporal Workflow and Activities)
|
||||
|
||||
### Agent Goal Configuration
|
||||
For a deeper dive into this, check out the [architecture guide](./architecture.md).
|
||||
|
||||
The agent can be configured to pursue different goals using the `AGENT_GOAL` environment variable in your `.env` file.
|
||||
## Setup and Configuration
|
||||
See [the Setup guide](./setup.md).
|
||||
|
||||
#### 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
|
||||
## Customizing Interaction & Tools
|
||||
See [the guide to adding goals and tools](./adding-goals-and-tools.md).
|
||||
|
||||
#### 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 goal was part of [Temporal's Replay 2025 conference keynote demo](https://www.youtube.com/watch?v=YDxAWrIBQNE)
|
||||
- Note, there is failure built in to this demo (the train booking step) to show how the agent can handle failures and retry. See Tool Configuration below for details.
|
||||
## Architecture
|
||||
See [the architecture guide](./architecture.md).
|
||||
|
||||
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.
|
||||
|
||||
### Tool Configuration
|
||||
|
||||
#### 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/)
|
||||
* Set permissions for read-write on: `Credit Notes, Invoices, Customers and Customer Sessions`
|
||||
* 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
|
||||
NOTE: This goal was developed for an on-stage demo and has failure (and its resolution) built in to show how the agent can handle failures and retry.
|
||||
* 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.
|
||||
|
||||
### LLM Provider Configuration
|
||||
|
||||
The agent can use OpenAI's GPT-4o, Google Gemini, Anthropic Claude, or a local LLM via Ollama. Set the `LLM_PROVIDER` environment variable in your `.env` file to choose the desired provider:
|
||||
|
||||
- `LLM_PROVIDER=openai` for OpenAI's GPT-4o
|
||||
- `LLM_PROVIDER=google` for Google Gemini
|
||||
- `LLM_PROVIDER=anthropic` for Anthropic Claude
|
||||
- `LLM_PROVIDER=deepseek` for DeepSeek-V3
|
||||
- `LLM_PROVIDER=ollama` for running LLMs via [Ollama](https://ollama.ai) (not recommended for this use case)
|
||||
|
||||
### Option 1: OpenAI
|
||||
|
||||
If using OpenAI, ensure you have an OpenAI key for the GPT-4o model. Set this in the `OPENAI_API_KEY` environment variable in `.env`.
|
||||
|
||||
### Option 2: Google Gemini
|
||||
|
||||
To use Google Gemini:
|
||||
|
||||
1. Obtain a Google API key and set it in the `GOOGLE_API_KEY` environment variable in `.env`.
|
||||
2. Set `LLM_PROVIDER=google` in your `.env` file.
|
||||
|
||||
### Option 3: Anthropic Claude (recommended)
|
||||
|
||||
I find that Claude Sonnet 3.5 performs better than the other hosted LLMs for this use case.
|
||||
|
||||
To use Anthropic:
|
||||
|
||||
1. Obtain an Anthropic API key and set it in the `ANTHROPIC_API_KEY` environment variable in `.env`.
|
||||
2. Set `LLM_PROVIDER=anthropic` in your `.env` file.
|
||||
|
||||
### Option 4: Deepseek-V3
|
||||
|
||||
To use Deepseek-V3:
|
||||
|
||||
1. Obtain a Deepseek API key and set it in the `DEEPSEEK_API_KEY` environment variable in `.env`.
|
||||
2. Set `LLM_PROVIDER=deepseek` in your `.env` file.
|
||||
|
||||
### Option 5: Local LLM via Ollama (not recommended)
|
||||
|
||||
To use a local LLM with Ollama:
|
||||
|
||||
1. Install [Ollama](https://ollama.com) and the [Qwen2.5 14B](https://ollama.com/library/qwen2.5) model.
|
||||
- Run `ollama run <OLLAMA_MODEL_NAME>` to start the model. Note that this model is about 9GB to download.
|
||||
- Example: `ollama run qwen2.5:14b`
|
||||
|
||||
2. Set `LLM_PROVIDER=ollama` in your `.env` file and `OLLAMA_MODEL_NAME` to the name of the model you installed.
|
||||
|
||||
Note: I found the other (hosted) LLMs to be MUCH more reliable for this use case. However, you can switch to Ollama if desired, and choose a suitably large model if your computer has the resources.
|
||||
|
||||
## Configuring Temporal Connection
|
||||
|
||||
By default, this application will connect to a local Temporal server (`localhost:7233`) in the default namespace, using the `agent-task-queue` task queue. You can override these settings in your `.env` file.
|
||||
|
||||
### Use Temporal Cloud
|
||||
|
||||
See [.env.example](.env.example) for details on connecting to Temporal Cloud using mTLS or API key authentication.
|
||||
|
||||
[Sign up for Temporal Cloud](https://temporal.io/get-cloud)
|
||||
|
||||
### Use a local Temporal Dev Server
|
||||
|
||||
On a Mac
|
||||
```bash
|
||||
brew install temporal
|
||||
temporal server start-dev
|
||||
```
|
||||
See the [Temporal documentation](https://learn.temporal.io/getting_started/python/dev_environment/) for other platforms.
|
||||
|
||||
|
||||
## Running the Application
|
||||
|
||||
### Python Backend
|
||||
|
||||
Requires [Poetry](https://python-poetry.org/) to manage dependencies.
|
||||
|
||||
1. `python -m venv venv`
|
||||
|
||||
2. `source venv/bin/activate`
|
||||
|
||||
3. `poetry install`
|
||||
|
||||
Run the following commands in separate terminal windows:
|
||||
|
||||
1. Start the Temporal worker:
|
||||
```bash
|
||||
poetry run python scripts/run_worker.py
|
||||
```
|
||||
|
||||
2. Start the API server:
|
||||
```bash
|
||||
poetry run uvicorn api.main:app --reload
|
||||
```
|
||||
Access the API at `/docs` to see the available endpoints.
|
||||
|
||||
### React UI
|
||||
Start the frontend:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npx vite
|
||||
```
|
||||
Access the UI at `http://localhost:5173`
|
||||
|
||||
### Python Search Trains API
|
||||
> Agent Goal: goal_match_train_invoice only
|
||||
|
||||
Required to search and book trains!
|
||||
```bash
|
||||
poetry run python thirdparty/train_api.py
|
||||
|
||||
# example url
|
||||
# http://localhost:8080/api/search?from=london&to=liverpool&outbound_time=2025-04-18T09:00:00&inbound_time=2025-04-20T09:00:00
|
||||
```
|
||||
|
||||
### Python Train Legacy Worker
|
||||
> Agent Goal: goal_match_train_invoice only
|
||||
|
||||
These are Python activities that fail (raise NotImplemented) to show how Temporal handles a failure. You can run these activities with.
|
||||
|
||||
```bash
|
||||
poetry run python scripts/run_legacy_worker.py
|
||||
```
|
||||
|
||||
The activity will fail and be retried infinitely. To rescue the activity (and its corresponding workflows), kill the worker and run the .NET one in the section below.
|
||||
|
||||
### .NET (enterprise) Worker ;)
|
||||
> Agent Goal: goal_match_train_invoice only
|
||||
|
||||
We have activities written in C# to call the train APIs.
|
||||
```bash
|
||||
cd enterprise
|
||||
dotnet build # ensure you brew install dotnet@8 first!
|
||||
dotnet run
|
||||
```
|
||||
If you're running your train API above on a different host/port then change the API URL in `Program.cs`. Otherwise, be sure to run it using `python thirdparty/train_api.py`.
|
||||
|
||||
## Customizing the Agent
|
||||
- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them)
|
||||
- `goal_registry.py` contains descriptions of goals and the tools used to achieve them
|
||||
- The tools themselves are defined in their own files in `/tools`
|
||||
- Note the mapping in `tools/__init__.py` to each tool
|
||||
|
||||
## TODO
|
||||
## 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.
|
||||
- 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 agent workflows (chats) running at the same time. Currently the workflow ID is the same each time, so it will only run one agent at a time. To run multiple agents, you can use a different workflow ID each time (e.g. by using a UUID or timestamp).
|
||||
- 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 guide to adding goals and tools](./adding-goals-and-tools.md) for more ways you can add features.
|
||||
|
||||
## Enablement Guide (internal resource for Temporal employees)
|
||||
Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the [enablement guide](https://docs.google.com/document/d/14E0cEOibUAgHPBqConbWXgPUBY0Oxrnt6_AImdiheW4/edit?tab=t.0#heading=h.ajnq2v3xqbu1).
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import inspect
|
||||
from temporalio import activity
|
||||
from ollama import chat, ChatResponse
|
||||
from openai import OpenAI
|
||||
@@ -10,7 +11,7 @@ import google.generativeai as genai
|
||||
import anthropic
|
||||
import deepseek
|
||||
from dotenv import load_dotenv
|
||||
from models.data_types import ValidationInput, ValidationResult, ToolPromptInput
|
||||
from models.data_types import EnvLookupOutput, ValidationInput, ValidationResult, ToolPromptInput, EnvLookupInput
|
||||
|
||||
load_dotenv(override=True)
|
||||
print(
|
||||
@@ -34,6 +35,7 @@ class ToolActivities:
|
||||
|
||||
# Initialize client variables (all set to None initially)
|
||||
self.openai_client: Optional[OpenAI] = None
|
||||
self.grok_client: Optional[OpenAI] = None
|
||||
self.anthropic_client: Optional[anthropic.Anthropic] = None
|
||||
self.genai_configured: bool = False
|
||||
self.deepseek_client: Optional[deepseek.DeepSeekAPI] = None
|
||||
@@ -47,6 +49,13 @@ class ToolActivities:
|
||||
print("Initialized OpenAI client")
|
||||
else:
|
||||
print("Warning: OPENAI_API_KEY not set but LLM_PROVIDER is 'openai'")
|
||||
|
||||
elif self.llm_provider == "grok":
|
||||
if os.environ.get("GROK_API_KEY"):
|
||||
self.grok_client = OpenAI(api_key=os.environ.get("GROK_API_KEY"), base_url="https://api.x.ai/v1")
|
||||
print("Initialized grok client")
|
||||
else:
|
||||
print("Warning: GROK_API_KEY not set but LLM_PROVIDER is 'grok'")
|
||||
|
||||
elif self.llm_provider == "anthropic":
|
||||
if os.environ.get("ANTHROPIC_API_KEY"):
|
||||
@@ -195,6 +204,8 @@ class ToolActivities:
|
||||
return self.prompt_llm_anthropic(input)
|
||||
elif self.llm_provider == "deepseek":
|
||||
return self.prompt_llm_deepseek(input)
|
||||
elif self.llm_provider == "grok":
|
||||
return self.prompt_llm_grok(input)
|
||||
else:
|
||||
return self.prompt_llm_openai(input)
|
||||
|
||||
@@ -237,13 +248,47 @@ class ToolActivities:
|
||||
)
|
||||
|
||||
response_content = chat_completion.choices[0].message.content
|
||||
print(f"ChatGPT response: {response_content}")
|
||||
activity.logger.info(f"ChatGPT response: {response_content}")
|
||||
|
||||
# Use the new sanitize function
|
||||
response_content = self.sanitize_json_response(response_content)
|
||||
|
||||
return self.parse_json_response(response_content)
|
||||
|
||||
def prompt_llm_grok(self, input: ToolPromptInput) -> dict:
|
||||
if not self.grok_client:
|
||||
api_key = os.environ.get("GROK_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
"GROK_API_KEY is not set in the environment variables but LLM_PROVIDER is 'grok'"
|
||||
)
|
||||
self.grok_client = OpenAI(api_key=api_key, base_url="https://api.x.ai/v1")
|
||||
print("Initialized grok client on demand")
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": input.context_instructions
|
||||
+ ". The current date is "
|
||||
+ datetime.now().strftime("%B %d, %Y"),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": input.prompt,
|
||||
},
|
||||
]
|
||||
|
||||
chat_completion = self.grok_client.chat.completions.create(
|
||||
model="grok-2-1212", messages=messages
|
||||
)
|
||||
|
||||
response_content = chat_completion.choices[0].message.content
|
||||
activity.logger.info(f"Grok response: {response_content}")
|
||||
|
||||
# Use the new sanitize function
|
||||
response_content = self.sanitize_json_response(response_content)
|
||||
|
||||
return self.parse_json_response(response_content)
|
||||
def prompt_llm_ollama(self, input: ToolPromptInput) -> dict:
|
||||
# If not yet initialized, try to do so now (this is a backup if warm_up_ollama wasn't called or failed)
|
||||
if not self.ollama_initialized:
|
||||
@@ -325,7 +370,8 @@ class ToolActivities:
|
||||
print("Initialized Anthropic client on demand")
|
||||
|
||||
response = self.anthropic_client.messages.create(
|
||||
model="claude-3-5-sonnet-20241022", # todo try claude-3-7-sonnet-20250219
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
#model="claude-3-7-sonnet-20250219", # doesn't do as well
|
||||
max_tokens=1024,
|
||||
system=input.context_instructions
|
||||
+ ". The current date is "
|
||||
@@ -426,6 +472,32 @@ class ToolActivities:
|
||||
print(f"Full response: {response_content}")
|
||||
raise
|
||||
|
||||
# get env vars for workflow
|
||||
@activity.defn
|
||||
async def get_wf_env_vars(self, input: EnvLookupInput) -> EnvLookupOutput:
|
||||
""" gets env vars for workflow as an activity result so it's deterministic
|
||||
handles default/None
|
||||
"""
|
||||
output: EnvLookupOutput = EnvLookupOutput(show_confirm=input.show_confirm_default,
|
||||
multi_goal_mode=True)
|
||||
show_confirm_value = os.getenv(input.show_confirm_env_var_name)
|
||||
if show_confirm_value is None:
|
||||
output.show_confirm = input.show_confirm_default
|
||||
elif show_confirm_value is not None and show_confirm_value.lower() == "false":
|
||||
output.show_confirm = False
|
||||
else:
|
||||
output.show_confirm = True
|
||||
|
||||
first_goal_value = os.getenv("AGENT_GOAL")
|
||||
if first_goal_value is None:
|
||||
output.multi_goal_mode = True # default if unset
|
||||
elif first_goal_value is not None and first_goal_value.lower() != "goal_choose_agent_type":
|
||||
output.multi_goal_mode = False
|
||||
else:
|
||||
output.multi_goal_mode = True
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def get_current_date_human_readable():
|
||||
"""
|
||||
@@ -439,7 +511,7 @@ def get_current_date_human_readable():
|
||||
|
||||
|
||||
@activity.defn(dynamic=True)
|
||||
def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||
async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||
from tools import get_handler
|
||||
|
||||
tool_name = activity.info().activity_type # e.g. "FindEvents"
|
||||
@@ -448,8 +520,13 @@ def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||
|
||||
# Delegate to the relevant function
|
||||
handler = get_handler(tool_name)
|
||||
result = handler(tool_args)
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
result = await handler(tool_args)
|
||||
else:
|
||||
result = handler(tool_args)
|
||||
|
||||
# Optionally log or augment the result
|
||||
activity.logger.info(f"Tool '{tool_name}' result: {result}")
|
||||
return result
|
||||
|
||||
|
||||
|
||||
100
adding-goals-and-tools.md
Normal file
100
adding-goals-and-tools.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Customizing the Agent
|
||||
The agent is set up to have multiple agents, each with their own goal. It supports switching back to choosing a new goal at the end of every successful goal (or even mid-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 New Goal Category
|
||||
Goal Categories lets you pick which groups of goals to show. Set via an .env setting, `GOAL_CATEGORIES`.
|
||||
Even if you don't intend to use the goal in a multi-goal scenario, goal categories are useful for others.
|
||||
1. Pick a unique one that has some business meaning
|
||||
2. Use it in your [.env](./.env) file
|
||||
3. Add to [.env.example](./.env.example)
|
||||
4. Use it in your Goal definition, see below.
|
||||
|
||||
## 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! (such as "goal_hr_schedule_pto")
|
||||
3. Fill out the required elements:
|
||||
- `id`: needs to be the same as the name
|
||||
- `agent_name`: user-facing name for the agent/chatbot
|
||||
- `category_tag`: category for the goal
|
||||
- `agent_friendly_description`: user-facing description of what the agent/chatbot does
|
||||
- `tools`: the list of tools the goal will walk the user through. These will be defined in the [tools/tool_registry.py](tools/tool_registry.py) and should be defined in list form as tool_registry.[name of tool]
|
||||
|
||||
Example:
|
||||
```
|
||||
tools=[
|
||||
tool_registry.current_pto_tool,
|
||||
tool_registry.future_pto_calc_tool,
|
||||
tool_registry.book_pto_tool,
|
||||
]
|
||||
```
|
||||
- `description`: LLM-facing description of the goal that lists the tools by name and purpose.
|
||||
- `starter-prompt`: LLM-facing first prompt given to begin the scenario. This field can contain instructions that are different from other goals, like "begin by providing the output of the first tool" rather than waiting on user confirmation. (See [goal_choose_agent_type](tools/goal_registry.py) for an example.)
|
||||
- `example_conversation_history`: LLM-facing sample conversation/interaction regarding the goal. See the existing goals for how to structure this.
|
||||
4. Add your new goal to the `goal_list` at the bottom using `goal_list.append(your_super_sweet_new_goal)`
|
||||
|
||||
## Adding Tools
|
||||
|
||||
### Note on Optional Tools
|
||||
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 an older iteration of the `goal_hr_schedule_pto` goal, when it was going to have an optional step to check for existing calendar conflicts:
|
||||
|
||||
```
|
||||
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 "
|
||||
```
|
||||
|
||||
Tools should generally return meaningful information and be generally ‘failsafe’ in returning a useful result based on input.
|
||||
(If you're doing a local data approach like those in [.tools/data/](./tools/data/)) it's good to document how they can be setup to get a good result in tool specific [setup](./setup.md).
|
||||
|
||||
### Add to Tool Registry
|
||||
1. Open [/tools/tool_registry.py](tools/tool_registry.py) - this file contains mapping of tool names to tool definitions (so the AI understands how to use them)
|
||||
2. Define the tool
|
||||
- `name`: name of the tool - this is the name as defined in the goal description list of tools. The name should be (sort of) the same as the tool name given in the goal description. So, if the description lists "CurrentPTO" as a tool, the name here should be `current_pto_tool`.
|
||||
- `description`: LLM-facing description of tool
|
||||
- `arguments`: These are the _input_ arguments to the tool. Each input argument should be defined as a [ToolArgument](./models/tool_definitions.py). Tools don't have to have arguments but the arguments list has to be declared. If the tool you're creating doesn't have inputs, define arguments as `arguments=[]`
|
||||
|
||||
### Create Each Tool
|
||||
- The tools themselves are defined in their own files in `/tools` - you can add a subfolder to organize them, see the hr tools for an example.
|
||||
- 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 `current_pto_tool` would be `current_pto.py` with a function named `current_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` and the tool get_handler()
|
||||
- In [tools/__init__.py](./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.
|
||||
Example:
|
||||
```
|
||||
if tool_name == "CurrentPTO":
|
||||
return current_pto
|
||||
```
|
||||
## Tool Confirmation
|
||||
There are three ways to manage confirmation of tool runs:
|
||||
1. Arguments confirmation box - confirm tool arguments and execution with a button click
|
||||
- Can be disabled by env setting: `SHOW_CONFIRM=FALSE`
|
||||
2. Soft prompt confirmation via asking the model to prompt for confirmation: “Are you ready to be invoiced for the total cost of the train tickets?” in the [goal_registry](./tools/goal_registry.py).
|
||||
3. Hard confirmation requirement as a tool argument. See for example the PTO Scheduling Tool:
|
||||
```Python
|
||||
ToolArgument(
|
||||
name="userConfirmation",
|
||||
type="string",
|
||||
description="Indication of user's desire to book PTO",
|
||||
),
|
||||
```
|
||||
If you really want to wait for user confirmation, record it on the workflow (as a Signal) and not rely on the LLM to probably get it, use option #3.
|
||||
I recommend exploring all three. For a demo, I would decide if you want the Arguments confirmation in the UI, and if not I'd generally go with option #2 but use #3 for tools that make business sense to confirm, e.g. those tools that take action/write data.
|
||||
|
||||
## Add a Goal & Tools Checklist
|
||||
[ ] Add goal in [/tools/goal_registry.py](tools/goal_registry.py) <br />
|
||||
- [ ] If a new category, add Goal Category to [.env](./.env) and [.env.example](./.env.example) <br />
|
||||
- [ ] don't forget the goal list at the bottom of the [goal_registry.py](tools/goal_registry.py) <br />
|
||||
|
||||
[ ] Add Tools listed in the Goal Registry to the [tool_registry.py](tools/tool_registry.py) <br />
|
||||
[ ] Define your tools as Activities in `/tools` <br />
|
||||
[ ] Add your tools to [tool list](tools/__init__.py) in the tool get_handler() <br />
|
||||
|
||||
And that's it! Happy AI Agent building!
|
||||
61
api/main.py
61
api/main.py
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from fastapi import FastAPI
|
||||
from typing import Optional
|
||||
from temporalio.client import Client
|
||||
@@ -6,11 +7,10 @@ from temporalio.api.enums.v1 import WorkflowExecutionStatus
|
||||
from fastapi import HTTPException
|
||||
from dotenv import load_dotenv
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||
from models.data_types import CombinedInput, AgentGoalWorkflowParams
|
||||
from tools.goal_registry import goal_match_train_invoice, goal_event_flight_invoice
|
||||
from tools.goal_registry import goal_list
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE
|
||||
|
||||
@@ -21,14 +21,12 @@ temporal_client: Optional[Client] = None
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def get_agent_goal():
|
||||
def get_initial_agent_goal():
|
||||
"""Get the agent goal from environment variables."""
|
||||
goal_name = os.getenv("AGENT_GOAL", "goal_match_train_invoice")
|
||||
goals = {
|
||||
"goal_match_train_invoice": goal_match_train_invoice,
|
||||
"goal_event_flight_invoice": goal_event_flight_invoice,
|
||||
}
|
||||
return goals.get(goal_name, goal_event_flight_invoice)
|
||||
env_goal = os.getenv("AGENT_GOAL", "goal_choose_agent_type") #if no goal is set in the env file, default to choosing an agent
|
||||
for listed_goal in goal_list:
|
||||
if listed_goal.id == env_goal:
|
||||
return listed_goal
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
@@ -113,10 +111,35 @@ async def get_conversation_history():
|
||||
status_code=404, detail="Workflow worker unavailable or not found."
|
||||
)
|
||||
|
||||
# For other Temporal errors, return a 500
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Internal server error while querying workflow."
|
||||
)
|
||||
if "workflow not found" in error_message:
|
||||
await start_workflow()
|
||||
return []
|
||||
else:
|
||||
# For other Temporal errors, return a 500
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Internal server error while querying workflow."
|
||||
)
|
||||
|
||||
@app.get("/agent-goal")
|
||||
async def get_agent_goal():
|
||||
"""Calls the workflow's 'get_agent_goal' query."""
|
||||
try:
|
||||
# Get workflow handle
|
||||
handle = temporal_client.get_workflow_handle("agent-workflow")
|
||||
|
||||
# Check if the workflow is completed
|
||||
workflow_status = await handle.describe()
|
||||
if workflow_status.status == 2:
|
||||
# Workflow is completed; return an empty response
|
||||
return {}
|
||||
|
||||
# Query the workflow
|
||||
agent_goal = await handle.query("get_agent_goal")
|
||||
return agent_goal
|
||||
except TemporalError as e:
|
||||
# Workflow not found; return an empty response
|
||||
print(e)
|
||||
return {}
|
||||
|
||||
|
||||
@app.post("/send-prompt")
|
||||
@@ -124,7 +147,8 @@ async def send_prompt(prompt: str):
|
||||
# Create combined input with goal from environment
|
||||
combined_input = CombinedInput(
|
||||
tool_params=AgentGoalWorkflowParams(None, None),
|
||||
agent_goal=get_agent_goal(),
|
||||
agent_goal=get_initial_agent_goal(),
|
||||
#change to get from workflow query
|
||||
)
|
||||
|
||||
workflow_id = "agent-workflow"
|
||||
@@ -168,13 +192,12 @@ async def end_chat():
|
||||
|
||||
@app.post("/start-workflow")
|
||||
async def start_workflow():
|
||||
# Get the configured goal
|
||||
agent_goal = get_agent_goal()
|
||||
initial_agent_goal = get_initial_agent_goal()
|
||||
|
||||
# Create combined input
|
||||
combined_input = CombinedInput(
|
||||
tool_params=AgentGoalWorkflowParams(None, None),
|
||||
agent_goal=agent_goal,
|
||||
agent_goal=initial_agent_goal,
|
||||
)
|
||||
|
||||
workflow_id = "agent-workflow"
|
||||
@@ -186,9 +209,9 @@ async def start_workflow():
|
||||
id=workflow_id,
|
||||
task_queue=TEMPORAL_TASK_QUEUE,
|
||||
start_signal="user_prompt",
|
||||
start_signal_args=["### " + agent_goal.starter_prompt],
|
||||
start_signal_args=["### " + initial_agent_goal.starter_prompt],
|
||||
)
|
||||
|
||||
return {
|
||||
"message": f"Workflow started with goal's starter prompt: {agent_goal.starter_prompt}."
|
||||
"message": f"Workflow started with goal's starter prompt: {initial_agent_goal.starter_prompt}."
|
||||
}
|
||||
|
||||
33
architecture-decisions.md
Normal file
33
architecture-decisions.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Architecture Decisions
|
||||
This documents some of the "why" behind the [architecture](./architecture.md).
|
||||
|
||||
## AI Models
|
||||
We wanted to have flexibility to use different models, because this space is changing rapidly and models get better regularly.
|
||||
Also, for you, we wanted to let you pick your model of choice. The system is designed to make changing models out simple. For how to do that, checkout the [setup guide](./setup.md).
|
||||
|
||||
## Temporal
|
||||
We 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.
|
||||
|
||||
In this system Temporal lets you:
|
||||
- Orchestrate interactions across distributed data stores and tools <br />
|
||||
- Hold state, potentially over long periods of time <br />
|
||||
- Ability to ‘self-heal’ and retry until the (probabilistic) LLM returns valid data <br />
|
||||
- Support for human intervention such as approvals <br />
|
||||
- Parallel processing for efficiency of data retrieval and tool use <br />
|
||||
74
architecture.md
Normal file
74
architecture.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Elements
|
||||
These are the main elements of this system. See [architecture decisions](./architecture-decisions.md) for information beind these choices.
|
||||
In this document we will explain each element and their interactions, and then connect them all at the end.
|
||||
<img src="./assets/Architecture_elements.png" width="50%" alt="Architecture Elements">
|
||||
|
||||
## Workflow
|
||||
This is a [Temporal Workflow](https://docs.temporal.io/workflows) - a durable straightforward description of the process to be executed. 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 loops:
|
||||
- LLM Loop: Prompts LLM, durably executes LLM, stores responses
|
||||
- Interactive Loop: Elicits responses from input (in our case a human) and validates input responses
|
||||
- Tool Execution Loop: Durably executes Tools
|
||||
- Keeps record of all interactions ([Signals, Queries, Updates](https://docs.temporal.io/develop/python/message-passing))
|
||||
- Handles failures gracefully
|
||||
- Input, 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 as Temporal supports multiple languages).
|
||||
They are executed by Temporal Activities. They are “just code” - can connect to any API or system. They also are where the deterministic 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 are. 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
|
||||
- to control process flow
|
||||
- describe what to do
|
||||
- give examples of interactions
|
||||
- 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:
|
||||
- Drives toward the initial Goal and any subsequent Goals selected by user
|
||||
- Decides what to do based on input, such as:
|
||||
- Validates user input for Tools
|
||||
- Decides when to execute Tools
|
||||
- Decides on next step for Goal
|
||||
- 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.
|
||||
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 the Interaction Loop: gathering input, validating input, and providing a response:
|
||||
|
||||

|
||||
|
||||
Here's a more detailed example for gathering inputs for Tools:
|
||||
|
||||

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

|
||||
|
||||
|
||||
# Adding features
|
||||
Want to add more Goals and Tools? See [adding goals and tools](./adding-goals-and-tools.md). Have fun!
|
||||
BIN
assets/Architecture_elements.png
Normal file
BIN
assets/Architecture_elements.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 376 KiB |
BIN
assets/ai_agent_architecture_model.png
Normal file
BIN
assets/ai_agent_architecture_model.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/argument_gathering_cycle.png
Normal file
BIN
assets/argument_gathering_cycle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 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 |
BIN
assets/temporal-agentic-ai-slides.pdf
Normal file
BIN
assets/temporal-agentic-ai-slides.pdf
Normal file
Binary file not shown.
@@ -2,6 +2,7 @@ using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Temporalio.Activities;
|
||||
using TrainSearchWorker.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TrainSearchWorker.Activities;
|
||||
|
||||
@@ -23,6 +24,7 @@ public class TrainActivities
|
||||
[Activity]
|
||||
public async Task<JourneyResponse> SearchTrains(SearchTrainsRequest request)
|
||||
{
|
||||
ActivityExecutionContext.Current.Logger.LogInformation($"SearchTrains from {request.From} to {request.To}");
|
||||
var response = await _client.GetAsync(
|
||||
$"api/search?from={Uri.EscapeDataString(request.From)}" +
|
||||
$"&to={Uri.EscapeDataString(request.To)}" +
|
||||
@@ -30,17 +32,21 @@ public class TrainActivities
|
||||
$"&return_time={Uri.EscapeDataString(request.ReturnTime)}");
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
|
||||
// Deserialize into JourneyResponse rather than List<Journey>
|
||||
var journeyResponse = await response.Content.ReadFromJsonAsync<JourneyResponse>(_jsonOptions)
|
||||
?? throw new InvalidOperationException("Received null response from API");
|
||||
|
||||
ActivityExecutionContext.Current.Logger.LogInformation("SearchTrains completed");
|
||||
|
||||
return journeyResponse;
|
||||
}
|
||||
|
||||
[Activity]
|
||||
public async Task<BookTrainsResponse> BookTrains(BookTrainsRequest request)
|
||||
{
|
||||
ActivityExecutionContext.Current.Logger.LogInformation($"Booking trains with IDs: {request.TrainIds}");
|
||||
|
||||
// Build the URL using the train IDs from the request
|
||||
var url = $"api/book/{Uri.EscapeDataString(request.TrainIds)}";
|
||||
|
||||
@@ -52,6 +58,8 @@ public class TrainActivities
|
||||
var bookingResponse = await response.Content.ReadFromJsonAsync<BookTrainsResponse>(_jsonOptions)
|
||||
?? throw new InvalidOperationException("Received null response from API");
|
||||
|
||||
ActivityExecutionContext.Current.Logger.LogInformation("BookTrains completed");
|
||||
|
||||
return bookingResponse;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,19 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Temporalio.Client;
|
||||
using Temporalio.Worker;
|
||||
using TrainSearchWorker.Activities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
|
||||
// Set up dependency injection
|
||||
var services = new ServiceCollection();
|
||||
|
||||
var loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder
|
||||
.AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ")
|
||||
.SetMinimumLevel(LogLevel.Information);
|
||||
});
|
||||
|
||||
// Add HTTP client
|
||||
services.AddHttpClient("TrainApi", client =>
|
||||
{
|
||||
@@ -31,7 +40,10 @@ Console.WriteLine($"Connecting to Temporal at address: {address}");
|
||||
Console.WriteLine($"Using namespace: {ns}");
|
||||
|
||||
// Create worker options
|
||||
var options = new TemporalWorkerOptions("agent-task-queue-legacy");
|
||||
var options = new TemporalWorkerOptions("agent-task-queue-legacy")
|
||||
{
|
||||
LoggerFactory = loggerFactory
|
||||
};
|
||||
|
||||
// Register activities
|
||||
var activities = serviceProvider.GetRequiredService<TrainActivities>();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.4" />
|
||||
<PackageReference Include="Temporalio" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
201
frontend/package-lock.json
generated
201
frontend/package-lock.json
generated
@@ -825,247 +825,228 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz",
|
||||
"integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz",
|
||||
"integrity": "sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz",
|
||||
"integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz",
|
||||
"integrity": "sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz",
|
||||
"integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz",
|
||||
"integrity": "sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz",
|
||||
"integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz",
|
||||
"integrity": "sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz",
|
||||
"integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz",
|
||||
"integrity": "sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz",
|
||||
"integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz",
|
||||
"integrity": "sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz",
|
||||
"integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz",
|
||||
"integrity": "sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz",
|
||||
"integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz",
|
||||
"integrity": "sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz",
|
||||
"integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz",
|
||||
"integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz",
|
||||
"integrity": "sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz",
|
||||
"integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz",
|
||||
"integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz",
|
||||
"integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz",
|
||||
"integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz",
|
||||
"integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz",
|
||||
"integrity": "sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz",
|
||||
"integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz",
|
||||
"integrity": "sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz",
|
||||
"integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz",
|
||||
"integrity": "sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz",
|
||||
"integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz",
|
||||
"integrity": "sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz",
|
||||
"integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz",
|
||||
"integrity": "sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -1115,8 +1096,7 @@
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.3.4",
|
||||
@@ -2084,9 +2064,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.49",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
|
||||
"integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -2101,9 +2081,8 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -2339,10 +2318,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz",
|
||||
"integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==",
|
||||
"license": "MIT",
|
||||
"version": "4.34.7",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.7.tgz",
|
||||
"integrity": "sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.6"
|
||||
},
|
||||
@@ -2354,25 +2332,25 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.29.1",
|
||||
"@rollup/rollup-android-arm64": "4.29.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.29.1",
|
||||
"@rollup/rollup-darwin-x64": "4.29.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.29.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.29.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.29.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.29.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.29.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.29.1",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.29.1",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.29.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.29.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.29.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.29.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.29.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.29.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.29.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.29.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.34.7",
|
||||
"@rollup/rollup-android-arm64": "4.34.7",
|
||||
"@rollup/rollup-darwin-arm64": "4.34.7",
|
||||
"@rollup/rollup-darwin-x64": "4.34.7",
|
||||
"@rollup/rollup-freebsd-arm64": "4.34.7",
|
||||
"@rollup/rollup-freebsd-x64": "4.34.7",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.34.7",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.34.7",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.34.7",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.34.7",
|
||||
"@rollup/rollup-linux-x64-musl": "4.34.7",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.34.7",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.34.7",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.34.7",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -2719,14 +2697,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz",
|
||||
"integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==",
|
||||
"license": "MIT",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz",
|
||||
"integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.24.2",
|
||||
"postcss": "^8.4.49",
|
||||
"rollup": "^4.23.0"
|
||||
"postcss": "^8.5.1",
|
||||
"rollup": "^4.30.1"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
||||
@@ -150,4 +150,4 @@ const ConfirmInline = memo(({ data, confirmed, onConfirm }) => {
|
||||
|
||||
ConfirmInline.displayName = "ConfirmInline";
|
||||
|
||||
export default ConfirmInline;
|
||||
export default ConfirmInline;
|
||||
@@ -27,7 +27,7 @@ const LLMResponse = memo(({ data, onConfirm, isLastMessage, onHeightChange }) =>
|
||||
: data?.response;
|
||||
|
||||
const displayText = (response || '').trim();
|
||||
const requiresConfirm = data.next === "confirm" && isLastMessage;
|
||||
const requiresConfirm = data.force_confirm && data.next === "confirm" && isLastMessage;
|
||||
const defaultText = requiresConfirm
|
||||
? `Agent is ready to run "${data.tool}". Please confirm.`
|
||||
: '';
|
||||
|
||||
@@ -17,7 +17,7 @@ class CombinedInput:
|
||||
|
||||
Message = Dict[str, Union[str, Dict[str, Any]]]
|
||||
ConversationHistory = Dict[str, List[Message]]
|
||||
NextStep = Literal["confirm", "question", "done"]
|
||||
NextStep = Literal["confirm", "question", "pick-new-goal", "done"]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -42,3 +42,13 @@ class ValidationResult:
|
||||
# Initialize empty dict if None
|
||||
if self.validationFailedReason is None:
|
||||
self.validationFailedReason = {}
|
||||
|
||||
@dataclass
|
||||
class EnvLookupInput:
|
||||
show_confirm_env_var_name: str
|
||||
show_confirm_default: bool
|
||||
|
||||
@dataclass
|
||||
class EnvLookupOutput:
|
||||
show_confirm: bool
|
||||
multi_goal_mode: bool
|
||||
@@ -15,9 +15,12 @@ class ToolDefinition:
|
||||
description: str
|
||||
arguments: List[ToolArgument]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentGoal:
|
||||
id: str
|
||||
category_tag: str
|
||||
agent_name: str
|
||||
agent_friendly_description: str
|
||||
tools: List[ToolDefinition]
|
||||
description: str = "Description of the tools purpose and overall goal"
|
||||
starter_prompt: str = "Initial prompt to start the conversation"
|
||||
|
||||
@@ -2,15 +2,21 @@ from models.tool_definitions import AgentGoal
|
||||
from typing import Optional
|
||||
import json
|
||||
|
||||
MULTI_GOAL_MODE: bool = None
|
||||
|
||||
|
||||
def generate_genai_prompt(
|
||||
agent_goal: AgentGoal, conversation_history: str, raw_json: Optional[str] = None
|
||||
agent_goal: AgentGoal,
|
||||
conversation_history: str,
|
||||
multi_goal_mode: bool,
|
||||
raw_json: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generates a concise prompt for producing or validating JSON instructions
|
||||
with the provided tools and conversation history.
|
||||
"""
|
||||
prompt_lines = []
|
||||
set_multi_goal_mode_if_unset(multi_goal_mode)
|
||||
|
||||
# Intro / Role
|
||||
prompt_lines.append(
|
||||
@@ -23,10 +29,12 @@ def generate_genai_prompt(
|
||||
prompt_lines.append(
|
||||
"This is the ongoing history to determine which tool and arguments to gather:"
|
||||
)
|
||||
prompt_lines.append("BEGIN CONVERSATION HISTORY")
|
||||
prompt_lines.append("*BEGIN CONVERSATION HISTORY*")
|
||||
prompt_lines.append(json.dumps(conversation_history, indent=2))
|
||||
prompt_lines.append("END CONVERSATION HISTORY")
|
||||
prompt_lines.append("")
|
||||
prompt_lines.append("*END CONVERSATION HISTORY*")
|
||||
prompt_lines.append(
|
||||
"REMINDER: You can use the conversation history to infer arguments for the tools."
|
||||
)
|
||||
|
||||
# Example Conversation History (from agent_goal)
|
||||
if agent_goal.example_conversation_history:
|
||||
@@ -68,7 +76,7 @@ def generate_genai_prompt(
|
||||
"Your JSON format must be:\n"
|
||||
"{\n"
|
||||
' "response": "<plain text>",\n'
|
||||
' "next": "<question|confirm|done>",\n'
|
||||
' "next": "<question|confirm|pick-new-goal|done>",\n'
|
||||
' "tool": "<tool_name or null>",\n'
|
||||
' "args": {\n'
|
||||
' "<arg1>": "<value1 or null>",\n'
|
||||
@@ -81,9 +89,23 @@ def generate_genai_prompt(
|
||||
"1) If any required argument is missing, set next='question' and ask the user.\n"
|
||||
"2) If all required arguments are known, set next='confirm' and specify the tool.\n"
|
||||
" The user will confirm before the tool is run.\n"
|
||||
"3) If no more tools are needed (user_confirmed_tool_run has been run for all), set next='done' and tool=null.\n"
|
||||
"4) response should be short and user-friendly.\n"
|
||||
"5) Don't set next='done' until the final tool has returned user_confirmed_tool_run.\n"
|
||||
f"3) {generate_toolchain_complete_guidance()}\n"
|
||||
"4) response should be short and user-friendly.\n\n"
|
||||
"Guardrails (always remember!)\n"
|
||||
"1) If any required argument is missing, set next='question' and ask the user.\n"
|
||||
"1) ALWAYS ask a question in your response if next='question'.\n"
|
||||
"2) ALWAYS set next='confirm' if you have arguments\n "
|
||||
'And respond with "let\'s proceed with <tool> (and any other useful info)" \n '
|
||||
+ "DON'T set next='confirm' if you have a question to ask.\n"
|
||||
"EXAMPLE: If you have a question to ask, set next='question' and ask the user.\n"
|
||||
"3) You can carry over arguments from one tool to another.\n "
|
||||
"EXAMPLE: If you asked for an account ID, then use the conversation history to infer that argument "
|
||||
"going forward."
|
||||
"4) If ListAgents in the conversation history is force_confirm='False', you MUST check "
|
||||
+ "if the current tool contains userConfirmation. If it does, please ask the user to confirm details "
|
||||
+ "with the user. userConfirmation overrides force_confirm='False'.\n"
|
||||
+ "EXAMPLE: (force_confirm='False' AND userConfirmation exists on tool) Would you like me to <run tool> "
|
||||
+ "with the following details: <details>?\n"
|
||||
)
|
||||
|
||||
# Validation Task (If raw_json is provided)
|
||||
@@ -109,37 +131,41 @@ def generate_genai_prompt(
|
||||
|
||||
return "\n".join(prompt_lines)
|
||||
|
||||
|
||||
def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) -> str:
|
||||
"""
|
||||
Generates a prompt for handling tool completion and determining next steps.
|
||||
|
||||
|
||||
Args:
|
||||
current_tool: The name of the tool that just completed
|
||||
dynamic_result: The result data from the tool execution
|
||||
|
||||
|
||||
Returns:
|
||||
str: A formatted prompt string for the agent to process the tool completion
|
||||
"""
|
||||
return (
|
||||
f"### The '{current_tool}' tool completed successfully with {dynamic_result}. "
|
||||
"INSTRUCTIONS: Parse this tool result as plain text, and use the system prompt containing the list of tools in sequence and the conversation history (and previous tool_results) to figure out next steps, if any. "
|
||||
"You will need to use the tool_results to auto-fill arguments for subsequent tools and also to figure out if all tools have been run."
|
||||
'{"next": "<question|confirm|done>", "tool": "<tool_name or null>", "args": {"<arg1>": "<value1 or null>", "<arg2>": "<value2 or null>}, "response": "<plain text (can include \\n line breaks)>"}'
|
||||
"ONLY return those json keys (next, tool, args, response), nothing else."
|
||||
'Next should only be "done" if all tools have been run (use the system prompt to figure that out).'
|
||||
'Next should be "question" if the tool is not the last one in the sequence.'
|
||||
'Next should NOT be "confirm" at this point.'
|
||||
"You will need to use the tool_results to auto-fill arguments for subsequent tools and also to figure out if all tools have been run. "
|
||||
'{"next": "<question|confirm|pick-new-goal|done>", "tool": "<tool_name or null>", "args": {"<arg1>": "<value1 or null>", "<arg2>": "<value2 or null>}, "response": "<plain text (can include \\n line breaks)>"}'
|
||||
"ONLY return those json keys (next, tool, args, response), nothing else. "
|
||||
'Next should be "question" if the tool is not the last one in the sequence. '
|
||||
'Next should be "done" if the user is asking to be done with the chat. '
|
||||
f"{generate_pick_new_goal_guidance()}"
|
||||
)
|
||||
|
||||
def generate_missing_args_prompt(current_tool: str, tool_data: dict, missing_args: list[str]) -> str:
|
||||
|
||||
def generate_missing_args_prompt(
|
||||
current_tool: str, tool_data: dict, missing_args: list[str]
|
||||
) -> str:
|
||||
"""
|
||||
Generates a prompt for handling missing arguments for a tool.
|
||||
|
||||
|
||||
Args:
|
||||
current_tool: The name of the tool that needs arguments
|
||||
tool_data: The current tool data containing the response
|
||||
missing_args: List of argument names that are missing
|
||||
|
||||
|
||||
Returns:
|
||||
str: A formatted prompt string for requesting missing arguments
|
||||
"""
|
||||
@@ -148,3 +174,63 @@ def generate_missing_args_prompt(current_tool: str, tool_data: dict, missing_arg
|
||||
f"and following missing arguments for tool {current_tool}: {missing_args}. "
|
||||
"Only provide a valid JSON response without any comments or metadata."
|
||||
)
|
||||
|
||||
|
||||
def set_multi_goal_mode_if_unset(mode: bool) -> None:
|
||||
"""
|
||||
Set multi-mode (used to pass workflow)
|
||||
|
||||
Args:
|
||||
None
|
||||
|
||||
Returns:
|
||||
bool: True if in multi-goal mode, false if not
|
||||
"""
|
||||
global MULTI_GOAL_MODE
|
||||
if MULTI_GOAL_MODE is None:
|
||||
MULTI_GOAL_MODE = mode
|
||||
|
||||
|
||||
def is_multi_goal_mode() -> bool:
|
||||
"""
|
||||
Centralized logic for if we're in multi-goal mode.
|
||||
|
||||
Args:
|
||||
None
|
||||
|
||||
Returns:
|
||||
bool: True if in multi-goal mode, false if not
|
||||
"""
|
||||
return MULTI_GOAL_MODE
|
||||
|
||||
|
||||
def generate_pick_new_goal_guidance() -> str:
|
||||
"""
|
||||
Generates a prompt for guiding the LLM to pick a new goal or be done depending on multi-goal mode.
|
||||
|
||||
Args:
|
||||
None
|
||||
|
||||
Returns:
|
||||
str: A prompt string prompting the LLM to when to go to pick-new-goal
|
||||
"""
|
||||
if is_multi_goal_mode():
|
||||
return 'Next should only be "pick-new-goal" if all tools have been run for the current goal (use the system prompt to figure that out), or the user explicitly requested to pick a new goal.'
|
||||
else:
|
||||
return 'Next should never be "pick-new-goal".'
|
||||
|
||||
|
||||
def generate_toolchain_complete_guidance() -> str:
|
||||
"""
|
||||
Generates a prompt for guiding the LLM to handle the end of the toolchain.
|
||||
|
||||
Args:
|
||||
None
|
||||
|
||||
Returns:
|
||||
str: A prompt string prompting the LLM to prompt for a new goal, or be done
|
||||
"""
|
||||
if is_multi_goal_mode():
|
||||
return "If no more tools are needed (user_confirmed_tool_run has been run for all), set next='confirm' and tool='ListAgents'."
|
||||
else:
|
||||
return "If no more tools are needed (user_confirmed_tool_run has been run for all), set next='done' and tool=''."
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
[tool.poetry]
|
||||
name = "temporal-AI-agent"
|
||||
version = "0.1.0"
|
||||
name = "temporal_AI_agent"
|
||||
version = "0.2.0"
|
||||
description = "Temporal AI Agent"
|
||||
license = "MIT"
|
||||
authors = ["Steve Androulakis <steve.androulakis@temporal.io>"]
|
||||
authors = [
|
||||
"Steve Androulakis <steve.androulakis@temporal.io>",
|
||||
"Laine Smith <lainecaseysmith@gmail.com>",
|
||||
"Joshua Smith <josh.smith@temporal.io>"
|
||||
]
|
||||
readme = "README.md"
|
||||
|
||||
# By default, Poetry will find packages automatically,
|
||||
@@ -13,7 +17,13 @@ packages = [
|
||||
]
|
||||
|
||||
[tool.poetry.urls]
|
||||
"Bug Tracker" = "https://github.com/temporalio/samples-python/issues"
|
||||
"Bug Tracker" = "https://github.com/temporal-community/temporal-ai-agent/issues"
|
||||
|
||||
[tool.poe.tasks]
|
||||
format = [{cmd = "black ."}, {cmd = "isort ."}]
|
||||
lint = [{cmd = "black --check ."}, {cmd = "isort --check-only ."}, {ref = "lint-types" }]
|
||||
lint-types = "mypy --check-untyped-defs --namespace-packages ."
|
||||
test = "pytest"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<4.0"
|
||||
@@ -36,10 +46,18 @@ pandas = "^2.2.3"
|
||||
gtfs-kit = "^10.1.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.3"
|
||||
pytest = ">=8.2"
|
||||
pytest-asyncio = "^0.26.0"
|
||||
black = "^23.7"
|
||||
isort = "^5.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.4.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
log_cli = true
|
||||
log_cli_level = "INFO"
|
||||
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
@@ -1,8 +1,8 @@
|
||||
from tools.search_events import find_events
|
||||
from tools.search_flights import search_flights
|
||||
import json
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
search_args = {"city": "Sydney", "month": "July"}
|
||||
results = find_events(search_args)
|
||||
results = search_flights(search_args)
|
||||
print(json.dumps(results, indent=2))
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import concurrent.futures
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
|
||||
from temporalio.worker import Worker
|
||||
|
||||
@@ -48,6 +49,9 @@ async def main():
|
||||
print("===========================================================\n")
|
||||
|
||||
print("Worker ready to process tasks!")
|
||||
logging.basicConfig(level=logging.WARN)
|
||||
|
||||
|
||||
|
||||
# Run the worker
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
||||
@@ -58,6 +62,7 @@ async def main():
|
||||
activities=[
|
||||
activities.agent_validatePrompt,
|
||||
activities.agent_toolPlanner,
|
||||
activities.get_wf_env_vars,
|
||||
dynamic_tool_activity,
|
||||
],
|
||||
activity_executor=activity_executor,
|
||||
|
||||
234
setup.md
Normal file
234
setup.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# Setup Guide
|
||||
## Initial Configuration
|
||||
|
||||
This application uses `.env` files for configuration. Copy the [.env.example](.env.example) file to `.env` and update the values:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Then add API keys, configuration, as desired.
|
||||
|
||||
If you want to show confirmations/enable the debugging UI that shows tool args, set
|
||||
```bash
|
||||
SHOW_CONFIRM=True
|
||||
```
|
||||
|
||||
### Agent Goal Configuration
|
||||
|
||||
The agent can be configured to pursue different goals using the `AGENT_GOAL` environment variable in your `.env` file. If unset, default is `goal_choose_agent_type`.
|
||||
|
||||
If the first goal is `goal_choose_agent_type` the agent will support multiple goals using goal categories defined by `GOAL_CATEGORIES` in your .env file. If unset, default is all. We recommend starting with `fin`.
|
||||
```bash
|
||||
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin
|
||||
```
|
||||
|
||||
See the section Goal-Specific Tool Configuration below for tool configuration for specific goals.
|
||||
|
||||
### LLM Provider Configuration
|
||||
|
||||
The agent can use OpenAI's GPT-4o, Google Gemini, Anthropic Claude, or a local LLM via Ollama. Set the `LLM_PROVIDER` environment variable in your `.env` file to choose the desired provider:
|
||||
|
||||
- `LLM_PROVIDER=openai` for OpenAI's GPT-4o
|
||||
- `LLM_PROVIDER=google` for Google Gemini
|
||||
- `LLM_PROVIDER=anthropic` for Anthropic Claude
|
||||
- `LLM_PROVIDER=deepseek` for DeepSeek-V3
|
||||
- `LLM_PROVIDER=ollama` for running LLMs via [Ollama](https://ollama.ai) (not recommended for this use case)
|
||||
|
||||
### Option 1: OpenAI
|
||||
|
||||
If using OpenAI, ensure you have an OpenAI key for the GPT-4o model. Set this in the `OPENAI_API_KEY` environment variable in `.env`.
|
||||
|
||||
### Option 2: Google Gemini
|
||||
|
||||
To use Google Gemini:
|
||||
|
||||
1. Obtain a Google API key and set it in the `GOOGLE_API_KEY` environment variable in `.env`.
|
||||
2. Set `LLM_PROVIDER=google` in your `.env` file.
|
||||
|
||||
### Option 3: Anthropic Claude (recommended)
|
||||
|
||||
I find that Claude Sonnet 3.5 performs better than the other hosted LLMs for this use case.
|
||||
|
||||
To use Anthropic:
|
||||
|
||||
1. Obtain an Anthropic API key and set it in the `ANTHROPIC_API_KEY` environment variable in `.env`.
|
||||
2. Set `LLM_PROVIDER=anthropic` in your `.env` file.
|
||||
|
||||
### Option 4: Deepseek-V3
|
||||
|
||||
To use Deepseek-V3:
|
||||
|
||||
1. Obtain a Deepseek API key and set it in the `DEEPSEEK_API_KEY` environment variable in `.env`.
|
||||
2. Set `LLM_PROVIDER=deepseek` in your `.env` file.
|
||||
|
||||
### Option 5: Local LLM via Ollama (not recommended)
|
||||
|
||||
To use a local LLM with Ollama:
|
||||
|
||||
1. Install [Ollama](https://ollama.com) and the [Qwen2.5 14B](https://ollama.com/library/qwen2.5) model.
|
||||
- Run `ollama run <OLLAMA_MODEL_NAME>` to start the model. Note that this model is about 9GB to download.
|
||||
- Example: `ollama run qwen2.5:14b`
|
||||
|
||||
2. Set `LLM_PROVIDER=ollama` in your `.env` file and `OLLAMA_MODEL_NAME` to the name of the model you installed.
|
||||
|
||||
Note: I found the other (hosted) LLMs to be MUCH more reliable for this use case. However, you can switch to Ollama if desired, and choose a suitably large model if your computer has the resources.
|
||||
|
||||
## Configuring Temporal Connection
|
||||
|
||||
By default, this application will connect to a local Temporal server (`localhost:7233`) in the default namespace, using the `agent-task-queue` task queue. You can override these settings in your `.env` file.
|
||||
|
||||
### Use Temporal Cloud
|
||||
|
||||
See [.env.example](.env.example) for details on connecting to Temporal Cloud using mTLS or API key authentication.
|
||||
|
||||
[Sign up for Temporal Cloud](https://temporal.io/get-cloud)
|
||||
|
||||
### Use a local Temporal Dev Server
|
||||
|
||||
On a Mac
|
||||
```bash
|
||||
brew install temporal
|
||||
temporal server start-dev
|
||||
```
|
||||
See the [Temporal documentation](https://learn.temporal.io/getting_started/python/dev_environment/) for other platforms.
|
||||
|
||||
|
||||
## Running the Application
|
||||
|
||||
### Python Backend
|
||||
|
||||
Requires [Poetry](https://python-poetry.org/) to manage dependencies.
|
||||
|
||||
1. `python -m venv venv`
|
||||
|
||||
2. `source venv/bin/activate`
|
||||
|
||||
3. `poetry install`
|
||||
|
||||
Run the following commands in separate terminal windows:
|
||||
|
||||
1. Start the Temporal worker:
|
||||
```bash
|
||||
poetry run python scripts/run_worker.py
|
||||
```
|
||||
|
||||
2. Start the API server:
|
||||
```bash
|
||||
poetry run uvicorn api.main:app --reload
|
||||
```
|
||||
Access the API at `/docs` to see the available endpoints.
|
||||
|
||||
### React UI
|
||||
Start the frontend:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npx vite
|
||||
```
|
||||
Access the UI at `http://localhost:5173`
|
||||
|
||||
|
||||
|
||||
## Goal-Specific Tool Configuration
|
||||
Here is configuration guidance for specific goals. Travel and financial goals have configuration & setup as below.
|
||||
### 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` - Helps users find events, book flights, and arrange train travel with invoice generation
|
||||
- This is the scenario in the [original video](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
||||
|
||||
#### Configuring Agent Goal: goal_event_flight_invoice
|
||||
* 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/)
|
||||
* Set permissions for read-write on: `Credit Notes, Invoices, Customers and Customer Sessions`
|
||||
* 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.
|
||||
|
||||
### Goal: Find a Premier League match, book train tickets to it and invoice the user for the cost (Replay 2025 Keynote)
|
||||
- `AGENT_GOAL=goal_match_train_invoice` - Focuses on Premier League match attendance with train booking and invoice generation
|
||||
- This goal was part of [Temporal's Replay 2025 conference keynote demo](https://www.youtube.com/watch?v=YDxAWrIBQNE)
|
||||
- Note, there is failure built in to this demo (the train booking step) to show how the agent can handle failures and retry. See Tool Configuration below for details.
|
||||
#### Configuring Agent Goal: goal_match_train_invoice
|
||||
NOTE: This goal was developed for an on-stage demo and has failure (and its resolution) built in to show how the agent can handle failures and retry.
|
||||
* 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.
|
||||
|
||||
##### Python Search Trains API
|
||||
> Agent Goal: goal_match_train_invoice only
|
||||
|
||||
Required to search and book trains!
|
||||
```bash
|
||||
poetry run python thirdparty/train_api.py
|
||||
|
||||
# example url
|
||||
# http://localhost:8080/api/search?from=london&to=liverpool&outbound_time=2025-04-18T09:00:00&inbound_time=2025-04-20T09:00:00
|
||||
```
|
||||
|
||||
##### Python Train Legacy Worker
|
||||
> Agent Goal: goal_match_train_invoice only
|
||||
|
||||
These are Python activities that fail (raise NotImplemented) to show how Temporal handles a failure. You can run these activities with.
|
||||
|
||||
```bash
|
||||
poetry run python scripts/run_legacy_worker.py
|
||||
```
|
||||
|
||||
The activity will fail and be retried infinitely. To rescue the activity (and its corresponding workflows), kill the worker and run the .NET one in the section below.
|
||||
|
||||
##### .NET (enterprise) Worker ;)
|
||||
We have activities written in C# to call the train APIs.
|
||||
```bash
|
||||
cd enterprise
|
||||
dotnet build # ensure you brew install dotnet@8 first!
|
||||
dotnet run
|
||||
```
|
||||
If you're running your train API above on a different host/port then change the API URL in `Program.cs`. Otherwise, be sure to run it using `python thirdparty/train_api.py`.
|
||||
|
||||
#### Goals: FIN - Money Movement and Loan Application
|
||||
Make sure you have the mock users you want (such as yourself) in [the account mock data file](./tools/data/customer_account_data.json).
|
||||
|
||||
- `AGENT_GOAL=goal_fin_move_money` - This scenario _can_ initiate a secondary workflow to move money. Check out [this repo](https://github.com/temporal-sa/temporal-money-transfer-java) - you'll need to get the worker running and connected to the same account as the agentic worker.
|
||||
By default it will _not_ make a real workflow, it'll just fake it. If you get the worker running and want to start a workflow, in your [.env](./.env):
|
||||
```bash
|
||||
FIN_START_REAL_WORKFLOW=FALSE #set this to true to start a real workflow
|
||||
```
|
||||
- `AGENT_GOAL=goal_fin_loan_application` - This scenario _can_ initiate a secondary workflow to apply for a loan. Check out [this repo](https://github.com/temporal-sa/temporal-latency-optimization-scenarios) - you'll need to get the worker running and connected to the same account as the agentic worker.
|
||||
By default it will _not_ make a real workflow, it'll just fake it. If you get the worker running and want to start a workflow, in your [.env](./.env):
|
||||
```bash
|
||||
FIN_START_REAL_WORKFLOW=FALSE #set this to true to start a real workflow
|
||||
```
|
||||
|
||||
#### Goals: HR/PTO
|
||||
Make sure you have the mock users you want in (such as yourself) in [the PTO mock data file](./tools/data/employee_pto_data.json).
|
||||
|
||||
#### Goals: Ecommerce
|
||||
Make sure you have the mock orders you want in (such as those with real tracking numbers) in [the mock orders file](./tools/data/customer_order_data.json).
|
||||
|
||||
|
||||
## Customizing the Agent Further
|
||||
- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them)
|
||||
- `goal_registry.py` contains descriptions of goals and the tools used to achieve them
|
||||
- The tools themselves are defined in their own files in `/tools`
|
||||
- Note the mapping in `tools/__init__.py` to each tool
|
||||
|
||||
For more details, check out [adding goals and tools guide](./adding-goals-and-tools.md).
|
||||
|
||||
## Setup Checklist
|
||||
[ ] copy `.env.example` to `.env` <br />
|
||||
[ ] Select an LLM and add your API key to `.env` <br />
|
||||
[ ] (Optional) set your starting goal and goal category in `.env` <br />
|
||||
[ ] (Optional) configure your Temporal Cloud settings in `.env` <br />
|
||||
[ ] `poetry run python scripts/run_worker.py` <br />
|
||||
[ ] `poetry run uvicorn api.main:app --reload` <br />
|
||||
[ ] `cd frontend`, `npm install`, `npx vite` <br />
|
||||
[ ] Access the UI at `http://localhost:5173` <br />
|
||||
|
||||
And that's it! Happy AI Agent Exploring!
|
||||
@@ -16,7 +16,6 @@ TEMPORAL_TLS_CERT = os.getenv("TEMPORAL_TLS_CERT", "")
|
||||
TEMPORAL_TLS_KEY = os.getenv("TEMPORAL_TLS_KEY", "")
|
||||
TEMPORAL_API_KEY = os.getenv("TEMPORAL_API_KEY", "")
|
||||
|
||||
|
||||
async def get_temporal_client() -> Client:
|
||||
"""
|
||||
Creates a Temporal client based on environment configuration.
|
||||
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
55
tests/conftest.py
Normal file
55
tests/conftest.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import asyncio
|
||||
import multiprocessing
|
||||
import sys
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from temporalio.client import Client
|
||||
from temporalio.testing import WorkflowEnvironment
|
||||
|
||||
# Due to https://github.com/python/cpython/issues/77906, multiprocessing on
|
||||
# macOS starting with Python 3.8 has changed from "fork" to "spawn". For
|
||||
# pre-3.8, we are changing it for them.
|
||||
if sys.version_info < (3, 8) and sys.platform.startswith("darwin"):
|
||||
multiprocessing.set_start_method("spawn", True)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--workflow-environment",
|
||||
default="local",
|
||||
help="Which workflow environment to use ('local', 'time-skipping', or target to existing server)",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
# See https://github.com/pytest-dev/pytest-asyncio/issues/68
|
||||
# See https://github.com/pytest-dev/pytest-asyncio/issues/257
|
||||
# Also need ProactorEventLoop on older versions of Python with Windows so
|
||||
# that asyncio subprocess works properly
|
||||
if sys.version_info < (3, 8) and sys.platform == "win32":
|
||||
loop = asyncio.ProactorEventLoop()
|
||||
else:
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]:
|
||||
env_type = request.config.getoption("--workflow-environment")
|
||||
if env_type == "local":
|
||||
env = await WorkflowEnvironment.start_local()
|
||||
elif env_type == "time-skipping":
|
||||
env = await WorkflowEnvironment.start_time_skipping()
|
||||
else:
|
||||
env = WorkflowEnvironment.from_client(await Client.connect(env_type))
|
||||
yield env
|
||||
await env.shutdown()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client(env: WorkflowEnvironment) -> Client:
|
||||
return env.client
|
||||
80
tests/workflowtests/agent_goal_workflow_test.py
Normal file
80
tests/workflowtests/agent_goal_workflow_test.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from temporalio.client import Client, WorkflowExecutionStatus
|
||||
from temporalio.worker import Worker
|
||||
import concurrent.futures
|
||||
from temporalio.testing import WorkflowEnvironment
|
||||
from api.main import get_initial_agent_goal
|
||||
from models.data_types import AgentGoalWorkflowParams, CombinedInput
|
||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||
from activities.tool_activities import ToolActivities, dynamic_tool_activity
|
||||
from unittest.mock import patch
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
@contextmanager
|
||||
def my_context():
|
||||
print("Setup")
|
||||
yield "some_value" # Value assigned to 'as' variable
|
||||
print("Cleanup")
|
||||
|
||||
|
||||
|
||||
async def test_flight_booking(client: Client):
|
||||
|
||||
#load_dotenv("test_flights_single.env")
|
||||
|
||||
with my_context() as value:
|
||||
print(f"Working with {value}")
|
||||
|
||||
|
||||
# Create the test environment
|
||||
#env = await WorkflowEnvironment.start_local()
|
||||
#client = env.client
|
||||
task_queue_name = "agent-ai-workflow"
|
||||
workflow_id = "agent-workflow"
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
||||
worker = Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[ToolActivities.agent_validatePrompt, ToolActivities.agent_toolPlanner, ToolActivities.get_wf_env_vars, dynamic_tool_activity],
|
||||
activity_executor=activity_executor,
|
||||
)
|
||||
|
||||
async with worker:
|
||||
initial_agent_goal = get_initial_agent_goal()
|
||||
# Create combined input
|
||||
combined_input = CombinedInput(
|
||||
tool_params=AgentGoalWorkflowParams(None, None),
|
||||
agent_goal=initial_agent_goal,
|
||||
)
|
||||
|
||||
prompt="Hello!"
|
||||
|
||||
#async with Worker(client, task_queue=task_queue_name, workflows=[AgentGoalWorkflow], activities=[ToolActivities.agent_validatePrompt, ToolActivities.agent_toolPlanner, dynamic_tool_activity]):
|
||||
|
||||
# todo set goal categories for scenarios
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
combined_input,
|
||||
id=workflow_id,
|
||||
task_queue=task_queue_name,
|
||||
start_signal="user_prompt",
|
||||
start_signal_args=[prompt],
|
||||
)
|
||||
# todo send signals to simulate user input
|
||||
# await handle.signal(AgentGoalWorkflow.user_prompt, "book flights") # for multi-goal
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "sydney in september")
|
||||
assert WorkflowExecutionStatus.RUNNING == (await handle.describe()).status
|
||||
|
||||
|
||||
#assert ["Hello, user1", "Hello, user2"] == await handle.result()
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "I'm all set, end conversation")
|
||||
|
||||
#assert WorkflowExecutionStatus.COMPLETED == (await handle.describe()).status
|
||||
|
||||
result = await handle.result()
|
||||
#todo dump workflow history for analysis optional
|
||||
#todo assert result is good
|
||||
32
todo.md
Normal file
32
todo.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# todo list
|
||||
[x] take steve's confirm box changes https://temporaltechnologies.slack.com/archives/D062SV8KEEM/p1745251279164319 <br />
|
||||
[ ] consider adding goal categories to goal picker
|
||||
|
||||
[ ] adding fintech goals <br />
|
||||
- Fraud Detection and Prevention - The AI monitors transactions across accounts, flagging suspicious activities (e.g., unusual spending patterns or login attempts) and autonomously freezing accounts or notifying customers and compliance teams.<br />
|
||||
- Personalized Financial Advice - An AI agent analyzes a customer’s financial data (e.g., income, spending habits, savings, investments) and provides tailored advice, such as budgeting tips, investment options, or debt repayment strategies.<br />
|
||||
- Portfolio Management and Rebalancing - The AI monitors a customer’s investment portfolio, rebalancing it automatically based on market trends, risk tolerance, and financial goals (e.g., shifting assets between stocks, bonds, or crypto).<br />
|
||||
|
||||
[ ] new loan/fraud check/update with start <br />
|
||||
[ ] financial advise - args being freeform customer input about their financial situation, goals
|
||||
[ ] tool is maybe a new tool asking the LLM to advise
|
||||
|
||||
[ ] for demo simulate failure - add utilities/simulated failures from pipeline demo <br />
|
||||
|
||||
[ ] LLM failure->autoswitch: <br />
|
||||
- detect failure in the activity using failurecount <br />
|
||||
- activity switches to secondary LLM defined in .env
|
||||
- activity reports switch to workflow
|
||||
|
||||
[ ] for demo simulate failure - add utilities/simulated failures from pipeline demo <br />
|
||||
|
||||
[ ] expand [tests](./tests/agent_goal_workflow_test.py)<br />
|
||||
[ ] collapse history/summarize after goal finished <br />
|
||||
[ ] add aws bedrock <br />
|
||||
|
||||
[ ] ask the ai agent how it did at the end of the conversation, was it efficient? successful? insert a search attribute to document that before return <br />
|
||||
- Insight into the agent’s performance <br />
|
||||
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br />
|
||||
[ ] add visual feedback when workflow starting <br />
|
||||
[ ] enable user to list agents at any time - like end conversation - probably with a next step<br />
|
||||
- with changing "'Next should only be "pick-new-goal" if all tools have been run (use the system prompt to figure that out).'" in [prompt_generators](./prompts/agent_prompt_generators.py).
|
||||
@@ -4,6 +4,26 @@ from .search_trains import search_trains
|
||||
from .search_trains import book_trains
|
||||
from .create_invoice import create_invoice
|
||||
from .find_events import find_events
|
||||
from .list_agents import list_agents
|
||||
from .change_goal import change_goal
|
||||
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
|
||||
from .hr.checkpaybankstatus import checkpaybankstatus
|
||||
|
||||
from .fin.check_account_valid import check_account_valid
|
||||
from .fin.get_account_balances import get_account_balance
|
||||
from .fin.move_money import move_money
|
||||
from .fin.submit_loan_application import submit_loan_application
|
||||
|
||||
from .ecommerce.get_order import get_order
|
||||
from .ecommerce.track_package import track_package
|
||||
from .ecommerce.list_orders import list_orders
|
||||
|
||||
from .give_hint import give_hint
|
||||
from .guess_location import guess_location
|
||||
|
||||
|
||||
def get_handler(tool_name: str):
|
||||
@@ -19,5 +39,37 @@ def get_handler(tool_name: str):
|
||||
return create_invoice
|
||||
if tool_name == "FindEvents":
|
||||
return find_events
|
||||
if tool_name == "ListAgents":
|
||||
return list_agents
|
||||
if tool_name == "ChangeGoal":
|
||||
return change_goal
|
||||
if tool_name == "TransferControl":
|
||||
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
|
||||
if tool_name == "CheckPayBankStatus":
|
||||
return checkpaybankstatus
|
||||
if tool_name == "FinCheckAccountIsValid":
|
||||
return check_account_valid
|
||||
if tool_name == "FinCheckAccountBalance":
|
||||
return get_account_balance
|
||||
if tool_name == "FinMoveMoney":
|
||||
return move_money
|
||||
if tool_name == "FinCheckAccountSubmitLoanApproval":
|
||||
return submit_loan_application
|
||||
if tool_name == "GetOrder":
|
||||
return get_order
|
||||
if tool_name == "TrackPackage":
|
||||
return track_package
|
||||
if tool_name == "ListOrders":
|
||||
return list_orders
|
||||
if tool_name == "GiveHint":
|
||||
return give_hint
|
||||
if tool_name == "GuessLocation":
|
||||
return guess_location
|
||||
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
9
tools/change_goal.py
Normal file
9
tools/change_goal.py
Normal file
@@ -0,0 +1,9 @@
|
||||
def change_goal(args: dict) -> dict:
|
||||
|
||||
new_goal = args.get("goalID")
|
||||
if new_goal is None:
|
||||
new_goal = "goal_choose_agent_type"
|
||||
|
||||
return {
|
||||
"new_goal": new_goal,
|
||||
}
|
||||
@@ -4,7 +4,7 @@ from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=True) # Load environment variables from a .env file
|
||||
|
||||
stripe.api_key = os.getenv("STRIPE_API_KEY", "YOUR_DEFAULT_KEY")
|
||||
stripe.api_key = os.getenv("STRIPE_API_KEY")
|
||||
|
||||
|
||||
def ensure_customer_exists(
|
||||
@@ -26,41 +26,49 @@ def ensure_customer_exists(
|
||||
|
||||
def create_invoice(args: dict) -> dict:
|
||||
"""Create and finalize a Stripe invoice."""
|
||||
# Find or create customer
|
||||
customer_id = ensure_customer_exists(
|
||||
args.get("customer_id"), args.get("email", "default@example.com")
|
||||
)
|
||||
# If an API key exists in the env file, find or create customer
|
||||
if stripe.api_key is not None:
|
||||
customer_id = ensure_customer_exists(
|
||||
args.get("customer_id"), args.get("email", "default@example.com")
|
||||
)
|
||||
|
||||
# Get amount and convert to cents
|
||||
amount = args.get("amount", 200.00) # Default to $200.00
|
||||
try:
|
||||
amount_cents = int(float(amount) * 100)
|
||||
except (TypeError, ValueError):
|
||||
return {"error": "Invalid amount provided. Please confirm the amount."}
|
||||
# Get amount and convert to cents
|
||||
amount = args.get("amount", 200.00) # Default to $200.00
|
||||
try:
|
||||
amount_cents = int(float(amount) * 100)
|
||||
except (TypeError, ValueError):
|
||||
return {"error": "Invalid amount provided. Please confirm the amount."}
|
||||
|
||||
# Create an invoice item
|
||||
stripe.InvoiceItem.create(
|
||||
customer=customer_id,
|
||||
amount=amount_cents,
|
||||
currency="gbp",
|
||||
description=args.get("tripDetails", "Service Invoice"),
|
||||
)
|
||||
# Create an invoice item
|
||||
stripe.InvoiceItem.create(
|
||||
customer=customer_id,
|
||||
amount=amount_cents,
|
||||
currency="gbp",
|
||||
description=args.get("tripDetails", "Service Invoice"),
|
||||
)
|
||||
|
||||
# Create and finalize the invoice
|
||||
invoice = stripe.Invoice.create(
|
||||
customer=customer_id,
|
||||
collection_method="send_invoice", # Invoice is sent to the customer
|
||||
days_until_due=args.get("days_until_due", 7), # Default due date: 7 days
|
||||
pending_invoice_items_behavior="include", # No pending invoice items
|
||||
)
|
||||
finalized_invoice = stripe.Invoice.finalize_invoice(invoice.id)
|
||||
|
||||
return {
|
||||
"invoiceStatus": finalized_invoice.status,
|
||||
"invoiceURL": finalized_invoice.hosted_invoice_url,
|
||||
"reference": finalized_invoice.number,
|
||||
}
|
||||
# Create and finalize the invoice
|
||||
invoice = stripe.Invoice.create(
|
||||
customer=customer_id,
|
||||
collection_method="send_invoice", # Invoice is sent to the customer
|
||||
days_until_due=args.get("days_until_due", 7), # Default due date: 7 days
|
||||
pending_invoice_items_behavior="include", # No pending invoice items
|
||||
)
|
||||
finalized_invoice = stripe.Invoice.finalize_invoice(invoice.id)
|
||||
|
||||
return {
|
||||
"invoiceStatus": finalized_invoice.status,
|
||||
"invoiceURL": finalized_invoice.hosted_invoice_url,
|
||||
"reference": finalized_invoice.number,
|
||||
}
|
||||
# if no API key is in the env file, return dummy info
|
||||
else:
|
||||
print("[CreateInvoice] Creating invoice with:", args)
|
||||
return {
|
||||
"invoiceStatus": "generated",
|
||||
"invoiceURL": "https://pay.example.com/invoice/12345",
|
||||
"reference": "INV-12345",
|
||||
}
|
||||
|
||||
def create_invoice_example(args: dict) -> dict:
|
||||
"""
|
||||
|
||||
58
tools/data/customer_account_data.json
Normal file
58
tools/data/customer_account_data.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"name": "Matt Murdock",
|
||||
"email": "matt.murdock@nelsonmurdock.com",
|
||||
"account_id": "11235",
|
||||
"checking_balance": "1275.4",
|
||||
"savings_balance": "2800.15",
|
||||
"bitcoin_balance": 0.1378,
|
||||
"account_creation_date": "2014-03-10"
|
||||
},
|
||||
{
|
||||
"name": "Foggy Nelson",
|
||||
"email": "foggy.nelson@nelsonmurdock.com",
|
||||
"account_id": "112358",
|
||||
"checking_balance": "1523.66",
|
||||
"savings_balance": "4875.89",
|
||||
"bitcoin_balance": 0.0923,
|
||||
"account_creation_date": "2014-03-10"
|
||||
},
|
||||
{
|
||||
"name": "Karen Page",
|
||||
"email": "karen.page@nelsonmurdock.com",
|
||||
"account_id": "112",
|
||||
"checking_balance": 645.25,
|
||||
"savings_balance": "830.5",
|
||||
"bitcoin_balance": 0.0456,
|
||||
"account_creation_date": "2015-01-15"
|
||||
},
|
||||
{
|
||||
"name": "Wilson Fisk",
|
||||
"email": "wilson.fisk@fiskcorp.com",
|
||||
"account_id": "11",
|
||||
"checking_balance": 25000.0,
|
||||
"savings_balance": 150000.75,
|
||||
"bitcoin_balance": 5987.6721,
|
||||
"account_creation_date": "2013-09-20"
|
||||
},
|
||||
{
|
||||
"name": "Frank Castle",
|
||||
"email": "frank.castle@vigilante.net",
|
||||
"account_id": "1",
|
||||
"checking_balance": 320.1,
|
||||
"savings_balance": 0.3,
|
||||
"bitcoin_balance": 15.2189,
|
||||
"account_creation_date": "2016-02-05"
|
||||
},
|
||||
{
|
||||
"name": "Joshua Smith",
|
||||
"email": "joshmsmith@gmail.com",
|
||||
"account_id": "11235813",
|
||||
"checking_balance": 3021.9,
|
||||
"savings_balance": 500.5,
|
||||
"bitcoin_balance": 0.001,
|
||||
"account_creation_date": "2020-03-19"
|
||||
}
|
||||
]
|
||||
}
|
||||
81
tools/data/customer_order_data.json
Normal file
81
tools/data/customer_order_data.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"id": "100",
|
||||
"summary": "Lawyer Books",
|
||||
"email": "matt.murdock@nelsonmurdock.com",
|
||||
"status": "cancelled",
|
||||
"order_date": "2025-03-30",
|
||||
"last_update": "2025-04-01"
|
||||
},
|
||||
{
|
||||
"id": "101",
|
||||
"summary": "Bonking Sticks",
|
||||
"email": "matt.murdock@nelsonmurdock.com",
|
||||
"status": "paid",
|
||||
"order_date": "2025-04-01",
|
||||
"last_order_update": "2025-04-01"
|
||||
},
|
||||
{
|
||||
"id": "102",
|
||||
"summary": "Red Sunglasses",
|
||||
"email": "matt.murdock@nelsonmurdock.com",
|
||||
"status": "shipped",
|
||||
"order_date": "2025-04-01",
|
||||
"last_order_update": "2025-04-01",
|
||||
"tracking_id": "UPS67890"
|
||||
},
|
||||
{
|
||||
"id": "200",
|
||||
"summary": "Paper",
|
||||
"email": "foggy.nelson@nelsonmurdock.com",
|
||||
"status": "shipped",
|
||||
"order_date": "2025-04-03",
|
||||
"last_update": "2025-04-06",
|
||||
"tracking_id": "USPS12345"
|
||||
},
|
||||
{
|
||||
"id": "300",
|
||||
"summary": "Chemistry Books",
|
||||
"email": "heisenberg@blue-meth.com",
|
||||
"status": "shipped",
|
||||
"order_date": "2025-03-30",
|
||||
"last_update": "2025-04-06",
|
||||
"tracking_id": "USPS12345"
|
||||
},
|
||||
{
|
||||
"id": "301",
|
||||
"summary": "Book: Being a Cool Bro",
|
||||
"email": "heisenberg@blue-meth.com",
|
||||
"status": "cancelled",
|
||||
"order_date": "2025-04-01",
|
||||
"last_update": "2025-04-02"
|
||||
},
|
||||
{
|
||||
"id": "302",
|
||||
"summary": "Black Hat",
|
||||
"email": "heisenberg@blue-meth.com",
|
||||
"status": "delivered",
|
||||
"order_date": "2025-04-01",
|
||||
"last_update": "2025-04-06",
|
||||
"tracking_id": "UPS67890"
|
||||
},
|
||||
{
|
||||
"id": "400",
|
||||
"summary": "Giant Graphic Hoodie",
|
||||
"email": "jessenotpinkman@blue-meth.com",
|
||||
"status": "shipped",
|
||||
"order_date": "2025-04-03",
|
||||
"last_update": "2025-04-09",
|
||||
"tracking_id": "UPS67890"
|
||||
},
|
||||
{
|
||||
"id": "401",
|
||||
"summary": "Giant Pants",
|
||||
"email": "jessenotpinkman@blue-meth.com",
|
||||
"status": "processing",
|
||||
"order_date": "2025-04-03",
|
||||
"last_update": "2025-04-09"
|
||||
}
|
||||
]
|
||||
}
|
||||
158
tools/data/dummy_tracking_data.json
Normal file
158
tools/data/dummy_tracking_data.json
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"TrackingNumber": "USPS12345",
|
||||
"Delivered": false,
|
||||
"Carrier": "USPS",
|
||||
"ServiceType": "USPS Ground Advantage<SUP>™</SUP>",
|
||||
"PickupDate": "",
|
||||
"ScheduledDeliveryDate": "April 14, 2025",
|
||||
"ScheduledDeliveryDateInDateTimeFromat": "2025-04-14T00:00:00",
|
||||
"StatusCode": "In Transit from Origin Processing",
|
||||
"Status": "Departed Post Office",
|
||||
"StatusSummary": "Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035.",
|
||||
"Message": "",
|
||||
"DeliveredDateTime": "",
|
||||
"DeliveredDateTimeInDateTimeFormat": null,
|
||||
"SignatureName": "",
|
||||
"DestinationCity": "CITY",
|
||||
"DestinationState": "ST",
|
||||
"DestinationZip": "12345",
|
||||
"DestinationCountry": null,
|
||||
"EventDate": "2025-04-10T07:06:00",
|
||||
"TrackingDetails": [
|
||||
{
|
||||
"EventDateTime": "April 10, 2025 7:06 am",
|
||||
"Event": "Departed Post Office",
|
||||
"EventAddress": "IRON RIDGE WI 53035",
|
||||
"State": "WI",
|
||||
"City": "IRON RIDGE",
|
||||
"Zip": "53035",
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-10T07:06:00"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "April 9, 2025 11:29 am",
|
||||
"Event": "USPS picked up item",
|
||||
"EventAddress": "IRON RIDGE WI 53035",
|
||||
"State": "WI",
|
||||
"City": "IRON RIDGE",
|
||||
"Zip": "53035",
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-09T11:29:00"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "April 7, 2025 6:29 am",
|
||||
"Event": "Shipping Label Created, USPS Awaiting Item",
|
||||
"EventAddress": "IRON RIDGE WI 53035",
|
||||
"State": "WI",
|
||||
"City": "IRON RIDGE",
|
||||
"Zip": "53035",
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-07T06:29:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"TrackingNumber": "UPS67890",
|
||||
"Delivered": true,
|
||||
"Carrier": "UPS",
|
||||
"ServiceType": "UPS Ground Saver®",
|
||||
"PickupDate": "",
|
||||
"ScheduledDeliveryDate": "",
|
||||
"ScheduledDeliveryDateInDateTimeFromat": null,
|
||||
"StatusCode": "D",
|
||||
"Status": "DELIVERED",
|
||||
"StatusSummary": "DELIVERED",
|
||||
"Message": "",
|
||||
"DeliveredDateTime": "20250415 154315",
|
||||
"DeliveredDateTimeInDateTimeFormat": "2025-04-15T15:43:15",
|
||||
"SignatureName": "",
|
||||
"DestinationCity": "CHICAGO",
|
||||
"DestinationState": "IL",
|
||||
"DestinationZip": "",
|
||||
"DestinationCountry": "US",
|
||||
"EventDate": "2025-04-15T15:43:15",
|
||||
"TrackingDetails": [
|
||||
{
|
||||
"EventDateTime": "20250415 154315",
|
||||
"Event": "DELIVERED ",
|
||||
"EventAddress": "CHICAGO IL US",
|
||||
"State": "IL",
|
||||
"City": "CHICAGO",
|
||||
"Zip": null,
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-15T15:43:15"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "20250415 090938",
|
||||
"Event": "Out For Delivery Today",
|
||||
"EventAddress": "Chicago IL US",
|
||||
"State": "IL",
|
||||
"City": "Chicago",
|
||||
"Zip": null,
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-15T09:09:38"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "20250415 074141",
|
||||
"Event": "Loaded on Delivery Vehicle ",
|
||||
"EventAddress": "Chicago IL US",
|
||||
"State": "IL",
|
||||
"City": "Chicago",
|
||||
"Zip": null,
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-15T07:41:41"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "20250415 032200",
|
||||
"Event": "Arrived at Facility",
|
||||
"EventAddress": "Chicago IL US",
|
||||
"State": "IL",
|
||||
"City": "Chicago",
|
||||
"Zip": null,
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-15T03:22:00"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "20250414 223000",
|
||||
"Event": "Departed from Facility",
|
||||
"EventAddress": "Hodgkins IL US",
|
||||
"State": "IL",
|
||||
"City": "Hodgkins",
|
||||
"Zip": null,
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-14T22:30:00"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "20250414 002700",
|
||||
"Event": "Arrived at Facility",
|
||||
"EventAddress": "Hodgkins IL US",
|
||||
"State": "IL",
|
||||
"City": "Hodgkins",
|
||||
"Zip": null,
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-14T00:27:00"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "20250410 211700",
|
||||
"Event": "Departed from Facility",
|
||||
"EventAddress": "Las Vegas NV US",
|
||||
"State": "NV",
|
||||
"City": "Las Vegas",
|
||||
"Zip": null,
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-10T21:17:00"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "20250410 132625",
|
||||
"Event": "Arrived at Facility",
|
||||
"EventAddress": "Las Vegas NV US",
|
||||
"State": "NV",
|
||||
"City": "Las Vegas",
|
||||
"Zip": null,
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-10T13:26:25"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "20250409 100659",
|
||||
"Event": "Shipper created a label, UPS has not received the package yet. ",
|
||||
"EventAddress": " US",
|
||||
"State": null,
|
||||
"City": null,
|
||||
"Zip": null,
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-09T10:06:59"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
27
tools/data/employee_pto_data.json
Normal file
27
tools/data/employee_pto_data.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"theCompany": {
|
||||
"weLove": "theCompany",
|
||||
"employees": [
|
||||
{
|
||||
"email": "josh.smith@temporal.io",
|
||||
"currentPTOHrs": 400,
|
||||
"hrsAddedPerMonth": 8
|
||||
},
|
||||
{
|
||||
"email": "laine@awesome.com",
|
||||
"currentPTOHrs": 40,
|
||||
"hrsAddedPerMonth": 12
|
||||
},
|
||||
{
|
||||
"email": "steve.this.is.for.you@gmail.com",
|
||||
"currentPTOHrs": 4000,
|
||||
"hrsAddedPerMonth": 20
|
||||
},
|
||||
{
|
||||
"email": "your_email_here@yourcompany.com",
|
||||
"currentPTOHrs": 150,
|
||||
"hrsAddedPerMonth": 19
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
23
tools/ecommerce/get_order.py
Normal file
23
tools/ecommerce/get_order.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||
# called as part of a temporal activity with automatic retries
|
||||
def get_order(args: dict) -> dict:
|
||||
|
||||
order_id = args.get("order_id")
|
||||
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_order_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
order_list = data["orders"]
|
||||
|
||||
for order in order_list:
|
||||
if order["id"] == order_id:
|
||||
return order
|
||||
|
||||
return_msg = "Order " + order_id + " not found."
|
||||
return {"error": return_msg}
|
||||
30
tools/ecommerce/list_orders.py
Normal file
30
tools/ecommerce/list_orders.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
def sorting(e):
|
||||
return e['order_date']
|
||||
|
||||
def list_orders(args: dict) -> dict:
|
||||
|
||||
email_address = args.get("email_address")
|
||||
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_order_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
order_list = data["orders"]
|
||||
|
||||
rtn_order_list = []
|
||||
for order in order_list:
|
||||
if order["email"] == email_address:
|
||||
rtn_order_list.append(order)
|
||||
|
||||
if len(rtn_order_list) > 0:
|
||||
rtn_order_list.sort(key=sorting)
|
||||
return {"orders": rtn_order_list}
|
||||
else:
|
||||
return_msg = "No orders for customer " + email_address + " found."
|
||||
return {"error": return_msg}
|
||||
|
||||
144
tools/ecommerce/track_package.py
Normal file
144
tools/ecommerce/track_package.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import http
|
||||
import os
|
||||
import json
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
#Send back dummy data in the correct format - to use the real API, 1) change this to be track_package_fake and 2) change the below track_package_real to be track_package
|
||||
def track_package(args: dict) -> dict:
|
||||
|
||||
tracking_id = args.get("tracking_id")
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "dummy_tracking_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
package_list = data["packages"]
|
||||
|
||||
for package in package_list:
|
||||
if package["TrackingNumber"] == tracking_id:
|
||||
scheduled_delivery_date = package["ScheduledDeliveryDate"]
|
||||
carrier = package["Carrier"]
|
||||
status_summary = package["StatusSummary"]
|
||||
tracking_details = package.get("TrackingDetails", [])
|
||||
last_tracking_update = ""
|
||||
if tracking_details and tracking_details is not None and tracking_details[0] is not None:
|
||||
last_tracking_update = tracking_details[0]["EventDateTimeInDateTimeFormat"]
|
||||
|
||||
tracking_link = ""
|
||||
if carrier == "USPS":
|
||||
tracking_link = f"https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1={tracking_id}"
|
||||
elif carrier == "UPS":
|
||||
tracking_link = f"https://www.ups.com/track?track=yes&trackNums={tracking_id}"
|
||||
|
||||
return {
|
||||
"scheduled_delivery_date": scheduled_delivery_date,
|
||||
"carrier": carrier,
|
||||
"status_summary": status_summary,
|
||||
"tracking_link": tracking_link,
|
||||
"last_tracking_update": last_tracking_update
|
||||
}
|
||||
|
||||
return_msg = "Package not found with tracking info " + tracking_id
|
||||
return {"error": return_msg}
|
||||
|
||||
'''Format of response:
|
||||
{
|
||||
"TrackingNumber": "",
|
||||
"Delivered": false,
|
||||
"Carrier": "USPS",
|
||||
"ServiceType": "USPS Ground Advantage<SUP>™</SUP>",
|
||||
"PickupDate": "",
|
||||
"ScheduledDeliveryDate": "April 14, 2025",
|
||||
"ScheduledDeliveryDateInDateTimeFromat": "2025-04-14T00:00:00",
|
||||
"StatusCode": "In Transit from Origin Processing",
|
||||
"Status": "Departed Post Office",
|
||||
"StatusSummary": "Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035.",
|
||||
"Message": "",
|
||||
"DeliveredDateTime": "",
|
||||
"DeliveredDateTimeInDateTimeFormat": null,
|
||||
"SignatureName": "",
|
||||
"DestinationCity": "CITY",
|
||||
"DestinationState": "ST",
|
||||
"DestinationZip": "12345",
|
||||
"DestinationCountry": null,
|
||||
"EventDate": "2025-04-10T07:06:00",
|
||||
"TrackingDetails": [
|
||||
{
|
||||
"EventDateTime": "April 10, 2025 7:06 am",
|
||||
"Event": "Departed Post Office",
|
||||
"EventAddress": "IRON RIDGE WI 53035",
|
||||
"State": "WI",
|
||||
"City": "IRON RIDGE",
|
||||
"Zip": "53035",
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-10T07:06:00"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "April 9, 2025 11:29 am",
|
||||
"Event": "USPS picked up item",
|
||||
"EventAddress": "IRON RIDGE WI 53035",
|
||||
"State": "WI",
|
||||
"City": "IRON RIDGE",
|
||||
"Zip": "53035",
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-09T11:29:00"
|
||||
},
|
||||
{
|
||||
"EventDateTime": "April 7, 2025 6:29 am",
|
||||
"Event": "Shipping Label Created, USPS Awaiting Item",
|
||||
"EventAddress": "IRON RIDGE WI 53035",
|
||||
"State": "WI",
|
||||
"City": "IRON RIDGE",
|
||||
"Zip": "53035",
|
||||
"EventDateTimeInDateTimeFormat": "2025-04-07T06:29:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
def track_package_real(args: dict) -> dict:
|
||||
|
||||
tracking_id = args.get("tracking_id")
|
||||
|
||||
api_key = os.getenv("RAPIDAPI_KEY")
|
||||
api_host = os.getenv("RAPIDAPI_HOST_PACKAGE", "trackingpackage.p.rapidapi.com")
|
||||
|
||||
conn = http.client.HTTPSConnection(api_host)
|
||||
headers = {
|
||||
"x-rapidapi-key": api_key,
|
||||
"x-rapidapi-host": api_host,
|
||||
"Authorization": "Basic Ym9sZGNoYXQ6TGZYfm0zY2d1QzkuKz9SLw==",
|
||||
}
|
||||
|
||||
path = f"/TrackingPackage?trackingNumber={tracking_id}"
|
||||
|
||||
conn.request("GET", path, headers=headers)
|
||||
res = conn.getresponse()
|
||||
data = res.read()
|
||||
data_decoded = data.decode("utf-8")
|
||||
conn.close()
|
||||
|
||||
try:
|
||||
json_data = json.loads(data_decoded)
|
||||
except json.JSONDecodeError:
|
||||
return {"error": "Invalid JSON response"}
|
||||
|
||||
scheduled_delivery_date = json_data["ScheduledDeliveryDate"]
|
||||
carrier = json_data["Carrier"]
|
||||
status_summary = json_data["StatusSummary"]
|
||||
tracking_details = json_data.get("TrackingDetails", [])
|
||||
last_tracking_update = ""
|
||||
if tracking_details and tracking_details is not None and tracking_details[0] is not None:
|
||||
last_tracking_update = tracking_details[0]["EventDateTimeInDateTimeFormat"]
|
||||
tracking_link = ""
|
||||
if carrier == "USPS":
|
||||
tracking_link = f"https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1={tracking_id}"
|
||||
elif carrier == "UPS":
|
||||
tracking_link = f"https://www.ups.com/track?track=yes&trackNums={tracking_id}"
|
||||
|
||||
return {
|
||||
"scheduled_delivery_date": scheduled_delivery_date,
|
||||
"carrier": carrier,
|
||||
"status_summary": status_summary,
|
||||
"tracking_link": tracking_link,
|
||||
"last_tracking_update": last_tracking_update
|
||||
}
|
||||
24
tools/fin/check_account_valid.py
Normal file
24
tools/fin/check_account_valid.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||
# called as part of a temporal activity with automatic retries
|
||||
def check_account_valid(args: dict) -> dict:
|
||||
|
||||
email = args.get("email")
|
||||
account_id = args.get("account_id")
|
||||
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
account_list = data["accounts"]
|
||||
|
||||
for account in account_list:
|
||||
if account["email"] == email or account["account_id"] == account_id:
|
||||
return{"status": "account valid"}
|
||||
|
||||
return_msg = "Account not found with email address " + email + " or account ID: " + account_id
|
||||
return {"error": return_msg}
|
||||
23
tools/fin/get_account_balances.py
Normal file
23
tools/fin/get_account_balances.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||
# this assumes it's a valid account - use check_account_valid() to verify that first
|
||||
def get_account_balance(args: dict) -> dict:
|
||||
|
||||
account_key = args.get("email_address_or_account_ID")
|
||||
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
account_list = data["accounts"]
|
||||
|
||||
for account in account_list:
|
||||
if account["email"] == account_key or account["account_id"] == account_key:
|
||||
return{ "name": account["name"], "email": account["email"], "account_id": account["account_id"], "checking_balance": account["checking_balance"], "savings_balance": account["savings_balance"], "bitcoin_balance": account["bitcoin_balance"], "account_creation_date": account["account_creation_date"] }
|
||||
|
||||
return_msg = "Account not found with for " + account_key
|
||||
return {"error": return_msg}
|
||||
157
tools/fin/move_money.py
Normal file
157
tools/fin/move_money.py
Normal file
@@ -0,0 +1,157 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import json
|
||||
from temporalio.client import Client
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
from temporalio.exceptions import WorkflowAlreadyStartedError
|
||||
from shared.config import get_temporal_client
|
||||
|
||||
|
||||
from enum import Enum, auto
|
||||
|
||||
# enums for the java enum
|
||||
# class ExecutionScenarios(Enum):
|
||||
# HAPPY_PATH = 0
|
||||
# ADVANCED_VISIBILITY = auto() # 1
|
||||
# HUMAN_IN_LOOP = auto() # 2
|
||||
# API_DOWNTIME = auto() # 3
|
||||
# BUG_IN_WORKFLOW = auto() # 4
|
||||
# INVALID_ACCOUNT = auto() # 5
|
||||
|
||||
|
||||
# these dataclasses are for calling the Temporal Workflow
|
||||
# Python equivalent of the workflow we're calling's Java WorkflowParameterObj
|
||||
@dataclass
|
||||
class MoneyMovementWorkflowParameterObj:
|
||||
amount: int # Using snake_case as per Python conventions
|
||||
scenario: str
|
||||
|
||||
|
||||
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||
# this assumes it's a valid account - use check_account_valid() to verify that first
|
||||
async def move_money(args: dict) -> dict:
|
||||
|
||||
account_key = args.get("email_address_or_account_ID")
|
||||
account_type: str = args.get("accounttype")
|
||||
amount = args.get("amount")
|
||||
destinationaccount = args.get("destinationaccount")
|
||||
|
||||
file_path = (
|
||||
Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||
)
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
account_list = data["accounts"]
|
||||
|
||||
for account in account_list:
|
||||
if account["email"] == account_key or account["account_id"] == account_key:
|
||||
amount_str: str = str(amount)
|
||||
from_account_combo = account_key + account_type
|
||||
|
||||
transfer_workflow_id = await start_workflow(
|
||||
amount_cents=str_dollars_to_cents(amount_str),
|
||||
from_account_name=from_account_combo,
|
||||
to_account_name=destinationaccount,
|
||||
)
|
||||
|
||||
if account_type.casefold() == "checking":
|
||||
from_key = "checking_balance"
|
||||
elif account_type.casefold() == "savings":
|
||||
from_key = "savings_balance"
|
||||
else:
|
||||
return_msg = "Money order for account types other than checking or savings is not implemented."
|
||||
return {"error": return_msg}
|
||||
|
||||
to_key = (
|
||||
"savings_balance"
|
||||
if destinationaccount.casefold() == "savings"
|
||||
else "checking_balance"
|
||||
)
|
||||
|
||||
# Update from-account balance
|
||||
from_balance = float(str_dollars_to_cents(str(account[from_key])))
|
||||
from_balance -= float(str_dollars_to_cents(amount_str))
|
||||
account[from_key] = str(from_balance / 100)
|
||||
|
||||
# Update destination-account balance
|
||||
to_balance = float(str_dollars_to_cents(str(account[to_key])))
|
||||
to_balance += float(str_dollars_to_cents(amount_str))
|
||||
account[to_key] = str(to_balance / 100)
|
||||
|
||||
with open(file_path, "w") as file:
|
||||
json.dump(data, file, indent=4)
|
||||
|
||||
return {
|
||||
"status": "money movement complete",
|
||||
"confirmation id": transfer_workflow_id,
|
||||
"new_balance": account[from_key],
|
||||
"destination_balance": account[to_key],
|
||||
}
|
||||
|
||||
return_msg = "Account not found with for " + account_key
|
||||
return {"error": return_msg}
|
||||
|
||||
|
||||
# Async function to start workflow
|
||||
async def start_workflow(
|
||||
amount_cents: int, from_account_name: str, to_account_name: str
|
||||
) -> str:
|
||||
|
||||
start_real_workflow = os.getenv("FIN_START_REAL_WORKFLOW")
|
||||
if start_real_workflow is not None and start_real_workflow.lower() == "false":
|
||||
START_REAL_WORKFLOW = False
|
||||
else:
|
||||
START_REAL_WORKFLOW = True
|
||||
|
||||
if START_REAL_WORKFLOW:
|
||||
# Connect to Temporal
|
||||
client = await get_temporal_client()
|
||||
# Create the parameter object
|
||||
params = MoneyMovementWorkflowParameterObj(
|
||||
amount=amount_cents, scenario="HAPPY_PATH"
|
||||
)
|
||||
|
||||
workflow_id = (
|
||||
"TRANSFER-ACCT-" + from_account_name + "-TO-" + to_account_name
|
||||
) # business-relevant workflow ID
|
||||
|
||||
try:
|
||||
handle = await client.start_workflow(
|
||||
"moneyTransferWorkflow", # Workflow name
|
||||
params, # Workflow parameters
|
||||
id=workflow_id,
|
||||
task_queue="MoneyTransferJava", # Task queue name
|
||||
)
|
||||
return handle.id
|
||||
except WorkflowAlreadyStartedError as e:
|
||||
existing_handle = client.get_workflow_handle(workflow_id=workflow_id)
|
||||
return existing_handle.id
|
||||
else:
|
||||
return (
|
||||
"TRANSFER-ACCT-" + from_account_name + "-TO-" + to_account_name + "not-real"
|
||||
)
|
||||
|
||||
|
||||
# cleans a string dollar amount description to cents value
|
||||
def str_dollars_to_cents(dollar_str: str) -> int:
|
||||
try:
|
||||
# Remove '$' and any whitespace
|
||||
cleaned_str = dollar_str.replace("$", "").strip()
|
||||
|
||||
# Handle empty string or invalid input
|
||||
if not cleaned_str:
|
||||
raise ValueError("Empty amount provided")
|
||||
|
||||
# Convert to float and then to cents
|
||||
amount = float(cleaned_str)
|
||||
if amount < 0:
|
||||
raise ValueError("Negative amounts not allowed")
|
||||
|
||||
return int(amount * 100)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid dollar amount format: {dollar_str}") from e
|
||||
103
tools/fin/submit_loan_application.py
Normal file
103
tools/fin/submit_loan_application.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from datetime import date, timedelta
|
||||
import os
|
||||
from pathlib import Path
|
||||
import json
|
||||
from temporalio.client import (
|
||||
Client,
|
||||
WithStartWorkflowOperation,
|
||||
WorkflowHandle,
|
||||
WorkflowUpdateFailedError,
|
||||
)
|
||||
from temporalio import common
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
from temporalio.exceptions import WorkflowAlreadyStartedError
|
||||
from shared.config import get_temporal_client
|
||||
|
||||
|
||||
# Define data structures to match the Java workflow's expected input/output
|
||||
# see https://github.com/temporal-sa/temporal-latency-optimization-scenarios for more details
|
||||
@dataclass
|
||||
class TransactionRequest:
|
||||
amount: float
|
||||
sourceAccount: str
|
||||
targetAccount: str
|
||||
|
||||
@dataclass
|
||||
class TxResult:
|
||||
transactionId: str
|
||||
status: str
|
||||
|
||||
#demonstrate starting a workflow and early return pattern while the workflow continues
|
||||
async def submit_loan_application(args: dict) -> dict:
|
||||
account_key = args.get("email_address_or_account_ID")
|
||||
amount = args.get("amount")
|
||||
|
||||
loan_status: dict = await start_workflow(amount=amount,account_name=account_key)
|
||||
|
||||
if loan_status.get("error") is None:
|
||||
return {'status': loan_status.get("loan_application_status"), 'detailed_status': loan_status.get("application_details"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("transaction_id")}
|
||||
else:
|
||||
print(loan_status)
|
||||
return loan_status
|
||||
|
||||
|
||||
# Async function to start workflow
|
||||
async def start_workflow(amount: str, account_name: str, )-> dict:
|
||||
|
||||
start_real_workflow = os.getenv("FIN_START_REAL_WORKFLOW")
|
||||
if start_real_workflow is not None and start_real_workflow.lower() == "false":
|
||||
START_REAL_WORKFLOW = False
|
||||
return {'loan_application_status': "applied", 'application_details': "loan application is submitted and initial validation is complete",'transaction_id': "APPLICATION"+account_name, 'advisement': "You'll receive a confirmation for final approval in three business days", }
|
||||
else:
|
||||
START_REAL_WORKFLOW = True
|
||||
# Connect to Temporal
|
||||
client = await get_temporal_client()
|
||||
|
||||
# Define the workflow ID and task queue
|
||||
workflow_id = "LOAN_APPLICATION-"+account_name+"-"+date.today().strftime('%Y-%m-%d')
|
||||
task_queue = "LatencyOptimizationTEST"
|
||||
|
||||
# Create a TransactionRequest (matching the Java workflow's expected input)
|
||||
tx_request = TransactionRequest(
|
||||
amount=float(amount),
|
||||
targetAccount=account_name,
|
||||
sourceAccount=account_name,
|
||||
)
|
||||
|
||||
start_op = WithStartWorkflowOperation(
|
||||
"TransactionWorkflowLocalBeforeUpdate",
|
||||
tx_request,
|
||||
id=workflow_id,
|
||||
id_conflict_policy=common.WorkflowIDConflictPolicy.USE_EXISTING,
|
||||
task_queue=task_queue,
|
||||
)
|
||||
|
||||
try:
|
||||
print("trying update-with-start")
|
||||
tx_result = TxResult(
|
||||
await client.execute_update_with_start_workflow(
|
||||
"returnInitResult",
|
||||
start_workflow_operation=start_op,
|
||||
)
|
||||
)
|
||||
except WorkflowUpdateFailedError:
|
||||
print("aww man got exception WorkflowUpdateFailedError" )
|
||||
tx_result = None
|
||||
return_msg = "Loan could not be processed for " + account_name
|
||||
return {"error": return_msg}
|
||||
|
||||
workflow_handle = await start_op.workflow_handle()
|
||||
print(tx_result)
|
||||
|
||||
print(f"Update result: Transaction ID = {tx_result.transactionId}, Message = {tx_result.status}")
|
||||
|
||||
# Optionally, wait for the workflow to complete and get the final result
|
||||
# final_result = await handle.result()
|
||||
# print(f"Workflow completed with result: {final_result}")
|
||||
|
||||
|
||||
# return {'status': loan_status.get("loan_status"), 'detailed_status': loan_status.get("results"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("workflowID")}
|
||||
return {'loan_application_status': "applied", 'application_details': "loan application is submitted and initial validation is complete",'transaction_id': tx_result.transactionId, 'advisement': "You'll receive a confirmation for final approval in three business days", }
|
||||
|
||||
41
tools/give_hint.py
Normal file
41
tools/give_hint.py
Normal file
@@ -0,0 +1,41 @@
|
||||
TREASURE_LOCATION = {
|
||||
"address": "300 Lenora",
|
||||
"city": "Seattle",
|
||||
"state_full": "Washington",
|
||||
"state_abbrev": "WA",
|
||||
"zip": "98121",
|
||||
"country": "USA"
|
||||
}
|
||||
|
||||
HINTS = [
|
||||
"country of " + TREASURE_LOCATION["country"],
|
||||
"state of " + TREASURE_LOCATION["state_full"],
|
||||
"city of " + TREASURE_LOCATION["city"],
|
||||
"at a company HQ",
|
||||
"The company's tech traces its roots to a project called Cadence", #thanks, Grok
|
||||
"The company offers a tool that lets developers write code as if it's running forever, no matter what crashes", #thanks, Grok
|
||||
]
|
||||
''' Additional Grok provided hints about Temporal:
|
||||
"This company was founded by two engineers who previously worked on a system named after a South American river at Uber."
|
||||
"Their platform is all about orchestrating workflows that can survive failures—like a conductor keeping the music going."
|
||||
"They offer a tool that lets developers write code as if it’s running forever, no matter what crashes."
|
||||
"Their mission is tied to making distributed systems feel as simple as writing a single app."
|
||||
"They’ve got a knack for ‘durability’—both in their software and their growing reputation."
|
||||
"This outfit spun out of experiences at AWS and Uber, blending cloud and ride-sharing know-how."
|
||||
"Their open-source framework has a community that’s ticking along, fixing bugs and adding features daily."
|
||||
"They’re backed by big venture capital names like Sequoia, betting on their vision for reliable software."
|
||||
"The company’s name might remind you of a word for something fleeting, yet their tech is built to last."'''
|
||||
|
||||
def give_hint(args: dict) -> dict:
|
||||
hint_total = args.get("hint_total")
|
||||
if hint_total is None:
|
||||
hint_total = 0
|
||||
|
||||
index = hint_total % len(HINTS)
|
||||
hint_text = HINTS[index]
|
||||
|
||||
hint_total = hint_total + 1
|
||||
return {
|
||||
"hint_number": hint_total,
|
||||
"hint": hint_text
|
||||
}
|
||||
@@ -1,19 +1,112 @@
|
||||
import os
|
||||
from typing import List
|
||||
from models.tool_definitions import AgentGoal
|
||||
from tools.tool_registry import (
|
||||
search_fixtures_tool,
|
||||
search_flights_tool,
|
||||
search_trains_tool,
|
||||
book_trains_tool,
|
||||
create_invoice_tool,
|
||||
find_events_tool,
|
||||
import tools.tool_registry as tool_registry
|
||||
|
||||
# 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(
|
||||
id = "goal_choose_agent_type",
|
||||
category_tag="agent_selection",
|
||||
agent_name="Choose Agent",
|
||||
agent_friendly_description="Choose the type of agent to assist you today. You can always interrupt an existing agent to pick a new one.",
|
||||
tools=[
|
||||
tool_registry.list_agents_tool,
|
||||
tool_registry.change_goal_tool,
|
||||
],
|
||||
description="The user wants to choose which type of agent they will interact with. "
|
||||
"Help the user select an agent by gathering args for the Changegoal tool, in order: "
|
||||
"1. ListAgents: List agents available to interact with. Do not ask for user confirmation for this tool. "
|
||||
"2. ChangeGoal: Change goal of agent "
|
||||
"After these tools are complete, change your goal to the new goal as chosen by the user. ",
|
||||
starter_prompt= 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. List all details of all agents as provided by the output of the first tool included in this goal. ",
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"agent: Here are the currently available agents.",
|
||||
"tool_result: { agents: 'agent_name': 'Event Flight Finder', 'goal_id': 'goal_event_flight_invoice', 'agent_description': 'Helps users find interesting events and arrange travel to them',"
|
||||
"'agent_name': 'Schedule PTO', 'goal_id': 'goal_hr_schedule_pto', 'agent_description': 'Schedule PTO based on your available PTO.' }",
|
||||
"agent: The available agents are: Event Flight Finder and Schedule PTO. \n Which agent would you like to work with? ",
|
||||
"user: I'd like to find an event and book flights using the Event Flight Finder",
|
||||
"user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>",
|
||||
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
goal_match_train_invoice = AgentGoal(
|
||||
# Easter egg - if silly mode = a pirate, include goal_pirate_treasure as a "system" goal so it always shows up.
|
||||
# Can also turn make this goal available by setting the GOAL_CATEGORIES in the env file to include 'pirate', but if SILLY_MODE
|
||||
# is not 'a pirate', the interaction as a whole will be less pirate-y.
|
||||
pirate_category_tag = "pirate"
|
||||
if SILLY_MODE == "a pirate":
|
||||
pirate_category_tag = "system"
|
||||
goal_pirate_treasure = AgentGoal(
|
||||
id = "goal_pirate_treasure",
|
||||
category_tag=pirate_category_tag,
|
||||
agent_name="Arrr, Find Me Treasure!",
|
||||
agent_friendly_description="Sail the high seas and find me pirate treasure, ye land lubber!",
|
||||
tools=[
|
||||
search_fixtures_tool,
|
||||
search_trains_tool,
|
||||
book_trains_tool,
|
||||
create_invoice_tool,
|
||||
tool_registry.give_hint_tool,
|
||||
tool_registry.guess_location_tool,
|
||||
],
|
||||
description="The user wants to find a pirate treasure. "
|
||||
"Help the user gather args for these tools, in a loop, until treasure_found is True or the user requests to be done: "
|
||||
"1. GiveHint: If the user wants a hint regarding the location of the treasure, give them a hint. If they do not want a hint, this tool is optional."
|
||||
"2. GuessLocation: The user guesses where the treasure is, by giving an address. ",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to try to find the treasure",
|
||||
"agent: Sure! Do you want a hint?",
|
||||
"user: yes",
|
||||
"agent: Here is hint number 1!",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GiveHint tool>",
|
||||
"tool_result: { 'hint_number': 1, 'hint': 'The treasure is in the state of Arizona.' }",
|
||||
"agent: The treasure is in the state of Arizona. Would you like to guess the address of the treasure? ",
|
||||
"user: Yes, address is 123 Main St Phoenix, AZ",
|
||||
"agent: Let's see if you found the treasure...",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GuessLocation tool>"
|
||||
"tool_result: {'treasure_found':False}",
|
||||
"agent: Nope, that's not the right location! Do you want another hint?",
|
||||
"user: yes",
|
||||
"agent: Here is hint number 2.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GiveHint tool>",
|
||||
"tool_result: { 'hint_number': 2, 'hint': 'The treasure is in the city of Tucson, AZ.' }",
|
||||
"agent: The treasure is in the city of Tucson, AZ. Would you like to guess the address of the treasure? ",
|
||||
"user: Yes, address is 456 Main St Tucson, AZ",
|
||||
"agent: Let's see if you found the treasure...",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GuessLocation tool>",
|
||||
"tool_result: {'treasure_found':True}",
|
||||
"agent: Congratulations, Land Lubber, you've found the pirate treasure!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# ----- Travel Goals ---
|
||||
goal_match_train_invoice = AgentGoal(
|
||||
id = "goal_match_train_invoice",
|
||||
category_tag="travel-trains",
|
||||
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.",
|
||||
tools=[
|
||||
tool_registry.search_fixtures_tool,
|
||||
tool_registry.search_trains_tool,
|
||||
tool_registry.book_trains_tool,
|
||||
tool_registry.create_invoice_tool,
|
||||
],
|
||||
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. "
|
||||
@@ -23,7 +116,7 @@ goal_match_train_invoice = AgentGoal(
|
||||
"2. SearchTrains: Search for trains to the city of the match and list them for the customer to choose from "
|
||||
"3. BookTrains: Book the train tickets, used to invoice the user for the cost of the train tickets "
|
||||
"4. CreateInvoice: Invoices the user for the cost of train tickets, with total and details inferred from the conversation history ",
|
||||
starter_prompt="Welcome me, give me a description of what you can do, then ask me for the details you need to begin your job as an agent ",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to travel to a premier league match",
|
||||
@@ -51,18 +144,21 @@ goal_match_train_invoice = AgentGoal(
|
||||
),
|
||||
)
|
||||
|
||||
# unused
|
||||
goal_event_flight_invoice = AgentGoal(
|
||||
id = "goal_event_flight_invoice",
|
||||
category_tag="travel-flights",
|
||||
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.",
|
||||
tools=[
|
||||
find_events_tool,
|
||||
search_flights_tool,
|
||||
create_invoice_tool,
|
||||
tool_registry.find_events_tool,
|
||||
tool_registry.search_flights_tool,
|
||||
tool_registry.create_invoice_tool,
|
||||
],
|
||||
description="Help the user gather args for these tools in order: "
|
||||
"1. FindEvents: Find an event to travel to "
|
||||
"2. SearchFlights: search for a flight around the event dates "
|
||||
"3. CreateInvoice: Create a simple invoice for the cost of that flight ",
|
||||
starter_prompt="Welcome me, give me a description of what you can do, then ask me for the details you need to do your job",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to travel to an event",
|
||||
@@ -85,3 +181,308 @@ goal_event_flight_invoice = AgentGoal(
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# ----- HR Goals ---
|
||||
# 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,
|
||||
],
|
||||
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 book the PTO? ",
|
||||
"user: yes ",
|
||||
"user_confirmed_tool_run: <user clicks confirm on BookPTO tool>",
|
||||
'tool_result: { "status": "success" }',
|
||||
"agent: PTO successfully booked! ",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# This goal uses the data/employee_pto_data.json file as dummy data.
|
||||
goal_hr_check_pto = AgentGoal(
|
||||
id = "goal_hr_check_pto",
|
||||
category_tag="hr",
|
||||
agent_name="Check PTO Amount",
|
||||
agent_friendly_description="Check your available PTO.",
|
||||
tools=[
|
||||
tool_registry.current_pto_tool,
|
||||
],
|
||||
description="The user wants to check their 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 ",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to check my time off amounts at the current time",
|
||||
"agent: Sure! I can help you out with that. 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.",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# check integration with bank
|
||||
goal_hr_check_paycheck_bank_integration_status = AgentGoal(
|
||||
id = "goal_hr_check_paycheck_bank_integration_status",
|
||||
category_tag="hr",
|
||||
agent_name="Check paycheck deposit status",
|
||||
agent_friendly_description="Check your integration between your employer and your financial institution.",
|
||||
tools=[
|
||||
tool_registry.paycheck_bank_integration_status_check,
|
||||
],
|
||||
description="The user wants to check their bank integration used to deposit their paycheck. To assist with that goal, help the user gather args for these tools in order: "
|
||||
"1. CheckPayBankStatus: Tell the user the status of their paycheck bank integration ",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to check paycheck bank integration",
|
||||
"agent: Sure! I can help you out with that. May I have your email address?",
|
||||
"user: bob.johnson@emailzzz.com",
|
||||
"agent: Great! I can tell you what the status is for your paycheck bank integration.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on CheckPayBankStatus tool>",
|
||||
"tool_result: { 'status': connected }",
|
||||
"agent: Your paycheck bank deposit integration is properly connected.",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# ----- FinServ Goals ---
|
||||
# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data
|
||||
goal_fin_check_account_balances = AgentGoal(
|
||||
id = "goal_fin_check_account_balances",
|
||||
category_tag="fin",
|
||||
agent_name="Account Balances",
|
||||
agent_friendly_description="Check your account balances in Checking, Savings, etc.",
|
||||
tools=[
|
||||
tool_registry.financial_check_account_is_valid,
|
||||
tool_registry.financial_get_account_balances,
|
||||
],
|
||||
description="The user wants to check their account balances at the bank or financial institution. To assist with that goal, help the user gather args for these tools in order: "
|
||||
"1. FinCheckAccountIsValid: validate the user's account is valid"
|
||||
"2. FinCheckAccountBalance: Tell the user their account balance at the bank or financial institution",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to check my account balances",
|
||||
"agent: Sure! I can help you out with that. May I have your email address and account number?",
|
||||
"user: email is bob.johnson@emailzzz.com ",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||
"tool_result: { 'status': account valid }",
|
||||
"agent: Great! I can tell you what the your account balances are.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FinCheckAccountBalance tool>",
|
||||
"tool_result: { 'name': Matt Murdock, 'email': matt.murdock@nelsonmurdock.com, 'account_id': 11235, 'checking_balance': 875.40, 'savings_balance': 3200.15, 'bitcoin_balance': 0.1378, 'account_creation_date': 2014-03-10 }",
|
||||
"agent: Your account balances are as follows: \n "
|
||||
"Checking: $875.40. \n "
|
||||
"Savings: $3200.15. \n "
|
||||
"Bitcoint: 0.1378 \n "
|
||||
"Thanks for being a customer since 2014!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data
|
||||
# it also uses a separate workflow/tool, see ./setup.md for details
|
||||
goal_fin_move_money = AgentGoal(
|
||||
id = "goal_fin_move_money",
|
||||
category_tag="fin",
|
||||
agent_name="Money Order",
|
||||
agent_friendly_description="Initiate a money movement order.",
|
||||
tools=[
|
||||
tool_registry.financial_check_account_is_valid,
|
||||
tool_registry.financial_get_account_balances,
|
||||
tool_registry.financial_move_money,
|
||||
],
|
||||
description="The user wants to transfer money in their account at the bank or financial institution. To assist with that goal, help the user gather args for these tools in order: "
|
||||
"1. FinCheckAccountIsValid: validate the user's account is valid"
|
||||
"2. FinCheckAccountBalance: Tell the user their account balance at the bank or financial institution"
|
||||
"3. FinMoveMoney: Initiate a money movement order",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to transfer some money",
|
||||
"agent: Sure! I can help you out with that. May I have account number and email address?",
|
||||
"user: my account number is 11235 and my email address is matt.murdock@nelsonmurdock.com",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||
"tool_result: { 'status': account valid }",
|
||||
"agent: Great! Here are your account balances:",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FinCheckAccountBalance tool>",
|
||||
"tool_result: { 'name': Matt Murdock, 'email': matt.murdock@nelsonmurdock.com, 'account_id': 11235, 'checking_balance': 875.40, 'savings_balance': 3200.15, 'bitcoin_balance': 0.1378, 'account_creation_date': 2014-03-10 }",
|
||||
"agent: Your account balances are as follows: \n "
|
||||
"Checking: $875.40. \n "
|
||||
"Savings: $3200.15. \n "
|
||||
"Bitcoint: 0.1378 \n "
|
||||
"agent: how much would you like to move, from which account type, and to which account number?",
|
||||
"user: I'd like to move $500 from savings to account number #56789",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FinMoveMoney tool>",
|
||||
"tool_result: { 'status': money movement complete, 'confirmation id': 333421, 'new_balance': $2700.15 }",
|
||||
"agent: Money movement order completed! New account balance: $2700.15. Your confirmation id is 333421. "
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# this starts a loan approval process
|
||||
# it also uses a separate workflow/tool, see ./setup.md for details
|
||||
goal_fin_loan_application = AgentGoal(
|
||||
id = "goal_fin_loan_application",
|
||||
category_tag="fin",
|
||||
agent_name="Easy Loan",
|
||||
agent_friendly_description="Initiate a simple loan application.",
|
||||
tools=[
|
||||
tool_registry.financial_check_account_is_valid,
|
||||
tool_registry.financial_submit_loan_approval,
|
||||
],
|
||||
description="The user wants to apply for a loan at the financial institution. To assist with that goal, help the user gather args for these tools in order: "
|
||||
"1. FinCheckAccountIsValid: validate the user's account is valid"
|
||||
"2. FinCheckAccountSubmitLoanApproval: submit the loan for approval",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to apply for a loan",
|
||||
"agent: Sure! I can help you out with that. May I have account number and email address to validate your account?",
|
||||
"user: account number is 11235813",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||
"tool_result: { 'status': account valid }",
|
||||
"agent: Great! We've validated your account. What will the loan amount be?",
|
||||
"user: I'd like a loan for $500",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FinCheckAccountSubmitLoanApproval tool>",
|
||||
"tool_result: { 'status': submitted, 'detailed_status': loan application is submitted and initial validation is complete, 'confirmation id': 333421, 'next_step': You'll receive a confirmation for final approval in three business days }",
|
||||
"agent: I have submitted your loan application process and the initial validation is successful. Your application ID is 333421. You'll receive a notification for final approval from us in three business days. "
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# ----- E-Commerce Goals ---
|
||||
# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data
|
||||
goal_ecomm_order_status = AgentGoal(
|
||||
id = "goal_ecomm_order_status",
|
||||
category_tag="ecommerce",
|
||||
agent_name="Check Order Status",
|
||||
agent_friendly_description="Check the status of your order.",
|
||||
tools=[
|
||||
tool_registry.ecomm_get_order,
|
||||
tool_registry.ecomm_track_package,
|
||||
],
|
||||
description="The user wants to learn the status of a specific order. If the status is 'shipped' or 'delivered', they might want to get the package tracking information. To assist with that goal, help the user gather args for these tools in order: "
|
||||
"1. GetOrder: get information about an order"
|
||||
"2. TrackPackage: provide tracking information for the package. This tool is optional and should only be offered if the status is 'shipped' OR 'delivered' - otherwise, skip this tool and do not mention it to the user.",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to know the status of my order",
|
||||
"agent: Sure! I can help you out with that. May I have your order number?",
|
||||
"user: 102 ",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GetOrderStatus tool>",
|
||||
"tool_result: { 'id': '102', 'summary': 'Red Sunglasses', 'email': 'matt.murdock@nelsonmurdock.com', 'status': 'shipped', 'order_date': '2025-04-01', 'last_order_update': '2025-04-06', 'tracking_id': '039813852990618' }",
|
||||
"agent: Your order 'Red Sunglasses,' placed April 1, 2025, was shipped on April 6, 2025. Would you like to see the tracking inforation?",
|
||||
"user: Yes",
|
||||
"user_confirmed_tool_run: <user clicks confirm on TrackPackage tool>",
|
||||
"tool_result: { 'scheduled_delivery_date': 'April 30, 2025', 'carrier': 'USPS', 'status_summary': 'Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035.', 'tracking_link': 'https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=12345','last_tracking_update': '2025-03-22T16:14:48'}",
|
||||
"agent: Your package is scheduled to be delivered on April 30, 2025 via USPS. Here is the most recent status from them regarding your package, updated as of March 22: \n"
|
||||
"Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035. \n"
|
||||
"You can find the full tracking details here: tracking_link !",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
goal_ecomm_list_orders = AgentGoal(
|
||||
id = "goal_ecomm_list_orders",
|
||||
category_tag="ecommerce",
|
||||
agent_name="List All Orders",
|
||||
agent_friendly_description="List all orders for a user.",
|
||||
tools=[
|
||||
tool_registry.ecomm_list_orders,
|
||||
tool_registry.ecomm_get_order,
|
||||
tool_registry.ecomm_track_package,
|
||||
],
|
||||
description="The user wants to see all of their orders. They may want more details about specific orders, and if the status of an order is 'shipped' or 'delivered', they might want to get the package tracking information. To assist with that goal, help the user gather args for this tool: "
|
||||
"1. ListOrders: list orders for a user"
|
||||
" and then offer the following tools, in a loop, until the user indicates they are done:"
|
||||
"2. GetOrder: get information about an order. This tool is optional."
|
||||
"3. TrackPackage: provide tracking information for the package. This tool is optional and should only be offered if the status is 'shipped' OR 'delivered' - otherwise, skip this tool and do not mention it to the user.",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to see all of my orders.",
|
||||
"agent: Sure! I can help you out with that. May I have your email address?",
|
||||
"user: email is bob.johnson@emailzzz.com ",
|
||||
"user_confirmed_tool_run: <user clicks confirm on ListOrders tool>",
|
||||
"tool_result: a list of orders including [{'id': '102', 'summary': 'Red Sunglasses', 'email': 'matt.murdock@nelsonmurdock.com', 'status': 'shipped', 'order_date': '2025-04-01', 'last_order_update': '2025-04-06', 'tracking_id': '039813852990618' }, { 'id': '103', 'summary': 'Blue Sunglasses', 'email': 'matt.murdock@nelsonmurdock.com', 'status': 'paid', 'order_date': '2025-04-03', 'last_order_update': '2025-04-07' }]",
|
||||
"agent: Your orders are as follows: \n",
|
||||
"1. Red Sunglasses, ordered 4/1/2025 \n",
|
||||
"2. Blue Sunglasses, ordered 4/3/2025 \n",
|
||||
"Would you like more information about any of your orders?"
|
||||
"user: Yes, the Red Sunglasses",
|
||||
"agent: Your order 'Red Sunglasses,' placed April 1, 2025, was shipped on April 6, 2025. Would you like to see the tracking inforation?",
|
||||
"user: Yes",
|
||||
"user_confirmed_tool_run: <user clicks confirm on TrackPackage tool>",
|
||||
"tool_result: { 'scheduled_delivery_date': 'April 30, 2025', 'carrier': 'USPS', 'status_summary': 'Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035.', 'tracking_link': 'https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=12345','last_tracking_update': '2025-03-22T16:14:48'}",
|
||||
"agent: Your package is scheduled to be delivered on April 30, 2025 via USPS. Here is the most recent status from them regarding your package \n, updated as of March 22: \n"
|
||||
"Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035. \n"
|
||||
"You can find the full tracking details here: tracking_link ! \n"
|
||||
"Would you like more information about any of your other orders?",
|
||||
"user: No"
|
||||
"agent: Thanks, and have a great day!"
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
#Add the goals to a list for more generic processing, like listing available agents
|
||||
goal_list: List[AgentGoal] = []
|
||||
goal_list.append(goal_choose_agent_type)
|
||||
goal_list.append(goal_pirate_treasure)
|
||||
goal_list.append(goal_event_flight_invoice)
|
||||
goal_list.append(goal_match_train_invoice)
|
||||
goal_list.append(goal_hr_schedule_pto)
|
||||
goal_list.append(goal_hr_check_pto)
|
||||
goal_list.append(goal_hr_check_paycheck_bank_integration_status)
|
||||
goal_list.append(goal_fin_check_account_balances)
|
||||
goal_list.append(goal_fin_move_money)
|
||||
goal_list.append(goal_fin_loan_application)
|
||||
goal_list.append(goal_ecomm_list_orders)
|
||||
goal_list.append(goal_ecomm_order_status)
|
||||
|
||||
|
||||
# for multi-goal, just set list agents as the last tool
|
||||
first_goal_value = os.getenv("AGENT_GOAL")
|
||||
if first_goal_value is None:
|
||||
multi_goal_mode = True # default if unset
|
||||
elif first_goal_value is not None and first_goal_value.lower() != "goal_choose_agent_type":
|
||||
multi_goal_mode = False
|
||||
else:
|
||||
multi_goal_mode = True
|
||||
|
||||
if multi_goal_mode:
|
||||
for goal in goal_list:
|
||||
list_agents_found:bool = False
|
||||
for tool in goal.tools:
|
||||
if tool.name == "ListAgents":
|
||||
list_agents_found = True
|
||||
continue
|
||||
if list_agents_found == False:
|
||||
goal.tools.append(tool_registry.list_agents_tool)
|
||||
continue
|
||||
18
tools/guess_location.py
Normal file
18
tools/guess_location.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from .give_hint import TREASURE_LOCATION
|
||||
|
||||
def guess_location(args: dict) -> dict:
|
||||
|
||||
guess_address = args.get("address").lower()
|
||||
guess_city = args.get("city").lower()
|
||||
guess_state = args.get("state").lower()
|
||||
|
||||
if len(guess_state) == 2:
|
||||
compare_state = TREASURE_LOCATION.get("state_abbrev").lower()
|
||||
else:
|
||||
compare_state = TREASURE_LOCATION.get("state_full").lower()
|
||||
|
||||
#Check for the street address to be included in the guess to account for "st" vs "street" or leaving Street off entirely
|
||||
if TREASURE_LOCATION.get("address").lower() in guess_address and TREASURE_LOCATION.get("city").lower() == guess_city and compare_state == guess_state:
|
||||
return {"treasure_found": "True"}
|
||||
else:
|
||||
return {"treasure_found": "False"}
|
||||
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"
|
||||
}
|
||||
15
tools/hr/checkpaybankstatus.py
Normal file
15
tools/hr/checkpaybankstatus.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
|
||||
def checkpaybankstatus(args: dict) -> dict:
|
||||
|
||||
email = args.get("email")
|
||||
|
||||
if email == "grinch@grinch.com":
|
||||
print("THE GRINCH IS FOUND!")
|
||||
return {"status": "no money for the grinch"}
|
||||
|
||||
# could do logic here or look up data but for now everyone but the grinch is getting paid
|
||||
return_msg = "connected"
|
||||
return {"status": return_msg}
|
||||
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}
|
||||
39
tools/list_agents.py
Normal file
39
tools/list_agents.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
import tools.goal_registry as goals
|
||||
|
||||
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(",")
|
||||
|
||||
# if multi-goal-mode, add agent_selection as a goal (defaults to True)
|
||||
if "agent_selection" not in goal_categories :
|
||||
first_goal_value = os.getenv("AGENT_GOAL")
|
||||
if first_goal_value is None or first_goal_value.lower() == "goal_choose_agent_type":
|
||||
goal_categories.append("agent_selection")
|
||||
|
||||
# always show goals labeled as "system," like the goal chooser
|
||||
if "system" not in goal_categories:
|
||||
goal_categories.append("system")
|
||||
|
||||
agents = []
|
||||
if goals.goal_list is not None:
|
||||
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(
|
||||
{
|
||||
"agent_name": goal.agent_name,
|
||||
"goal_id": goal.id,
|
||||
"agent_description": goal.agent_friendly_description,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"agents": agents,
|
||||
}
|
||||
@@ -11,7 +11,7 @@ def search_airport(query: str) -> list:
|
||||
"""
|
||||
load_dotenv(override=True)
|
||||
api_key = os.getenv("RAPIDAPI_KEY", "YOUR_DEFAULT_KEY")
|
||||
api_host = os.getenv("RAPIDAPI_HOST", "sky-scrapper.p.rapidapi.com")
|
||||
api_host = os.getenv("RAPIDAPI_HOST_FLIGHTS", "sky-scrapper.p.rapidapi.com")
|
||||
|
||||
conn = http.client.HTTPSConnection(api_host)
|
||||
headers = {
|
||||
@@ -73,7 +73,7 @@ def search_flights_real_api(
|
||||
# Step 2: Call flight search with resolved codes
|
||||
load_dotenv(override=True)
|
||||
api_key = os.getenv("RAPIDAPI_KEY", "YOUR_DEFAULT_KEY")
|
||||
api_host = os.getenv("RAPIDAPI_HOST", "sky-scrapper.p.rapidapi.com")
|
||||
api_host = os.getenv("RAPIDAPI_HOST_FLIGHTS", "sky-scrapper.p.rapidapi.com")
|
||||
|
||||
conn = http.client.HTTPSConnection(api_host)
|
||||
headers = {
|
||||
|
||||
@@ -1,8 +1,63 @@
|
||||
from models.tool_definitions import ToolDefinition, ToolArgument
|
||||
|
||||
# ----- System tools -----
|
||||
list_agents_tool = ToolDefinition(
|
||||
name="ListAgents",
|
||||
description="List available agents to interact with, pulled from goal_registry. ",
|
||||
arguments=[],
|
||||
)
|
||||
|
||||
change_goal_tool = ToolDefinition(
|
||||
name="ChangeGoal",
|
||||
description="Change the goal of the active agent. ",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="goalID",
|
||||
type="string",
|
||||
description="Which goal to change to",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
give_hint_tool = ToolDefinition(
|
||||
name="GiveHint",
|
||||
description="Give a hint to the user regarding the location of the pirate treasure. Use previous conversation to determine the hint_total, it should initially be 0 ",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="hint_total",
|
||||
type="number",
|
||||
description="How many hints have been given",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
guess_location_tool = ToolDefinition(
|
||||
name="GuessLocation",
|
||||
description="Allow the user to guess the location (in the form of an address) of the pirate treasure. ",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="address",
|
||||
type="string",
|
||||
description="Address at which the user is guessing the treasure is located",
|
||||
),
|
||||
ToolArgument(
|
||||
name="city",
|
||||
type="string",
|
||||
description="City at which the user is guessing the treasure is located",
|
||||
),
|
||||
ToolArgument(
|
||||
name="state",
|
||||
type="string",
|
||||
description="State at which the user is guessing the treasure is located",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# ----- Travel use cases tools -----
|
||||
search_flights_tool = ToolDefinition(
|
||||
name="SearchFlights",
|
||||
description="Search for return flights from an origin to a destination within a date range (dateDepart, dateReturn).",
|
||||
description="Search for return flights from an origin to a destination within a date range (dateDepart, dateReturn). "
|
||||
"You are allowed to suggest dates from the conversation history, but ALWAYS ask the user if ok.",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="origin",
|
||||
@@ -24,6 +79,12 @@ search_flights_tool = ToolDefinition(
|
||||
type="ISO8601",
|
||||
description="End of date range in human readable format, when you want to return",
|
||||
),
|
||||
ToolArgument(
|
||||
name="userConfirmation",
|
||||
type="string",
|
||||
description="Indication of the user's desire to search flights, and to confirm the details "
|
||||
+ "before moving on to the next step",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -63,6 +124,11 @@ book_trains_tool = ToolDefinition(
|
||||
type="string",
|
||||
description="The IDs of the trains to book, comma separated",
|
||||
),
|
||||
ToolArgument(
|
||||
name="userConfirmation",
|
||||
type="string",
|
||||
description="Indication of user's desire to book train tickets",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -80,6 +146,11 @@ create_invoice_tool = ToolDefinition(
|
||||
type="string",
|
||||
description="A description of the item details to be invoiced, inferred from the conversation history.",
|
||||
),
|
||||
ToolArgument(
|
||||
name="userConfirmation",
|
||||
type="string",
|
||||
description="Indication of user's desire to create an invoice",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -124,3 +195,205 @@ find_events_tool = ToolDefinition(
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# ----- HR use cases tools -----
|
||||
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, sent in the form yyyy-mm-dd",
|
||||
),
|
||||
ToolArgument(
|
||||
name="end_date",
|
||||
type="string",
|
||||
description="End date of proposed PTO, sent in the form yyyy-mm-dd",
|
||||
),
|
||||
ToolArgument(
|
||||
name="email",
|
||||
type="string",
|
||||
description="email address of user",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
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, sent in the form yyyy-mm-dd",
|
||||
),
|
||||
ToolArgument(
|
||||
name="end_date",
|
||||
type="string",
|
||||
description="End date of proposed PTO, sent in the form yyyy-mm-dd",
|
||||
),
|
||||
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",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
paycheck_bank_integration_status_check = ToolDefinition(
|
||||
name="CheckPayBankStatus",
|
||||
description="Check status of Bank Integration for Paychecks. "
|
||||
"Returns the status of the bank integration, connected or disconnected. ",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="email",
|
||||
type="string",
|
||||
description="email address of user",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# ----- Financial use cases tools -----
|
||||
financial_check_account_is_valid = ToolDefinition(
|
||||
name="FinCheckAccountIsValid",
|
||||
description="Check if an account is valid by email address or account ID. "
|
||||
"Returns the account status, valid or invalid. ",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="email",
|
||||
type="string",
|
||||
description="email address of user",
|
||||
),
|
||||
ToolArgument(
|
||||
name="account_id",
|
||||
type="string",
|
||||
description="account ID of user",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
financial_get_account_balances = ToolDefinition(
|
||||
name="FinCheckAccountBalance",
|
||||
description="Get account balance for your accounts. "
|
||||
"Returns the account balances of your accounts. ",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="email_address_or_account_ID",
|
||||
type="string",
|
||||
description="email address or account ID of user",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
financial_move_money = ToolDefinition(
|
||||
name="FinMoveMoney",
|
||||
description="Send money from one account to another under the same acount ID (e.g. checking to savings). "
|
||||
"Returns the status of the order and the new balances in each account. ",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="email_address_or_account_ID",
|
||||
type="string",
|
||||
description="email address or account ID of user",
|
||||
),
|
||||
ToolArgument(
|
||||
name="accounttype",
|
||||
type="string",
|
||||
description="account type, such as checking or savings",
|
||||
),
|
||||
ToolArgument(
|
||||
name="amount",
|
||||
type="string",
|
||||
description="amount to move in the order (e.g. checking or savings)",
|
||||
),
|
||||
ToolArgument(
|
||||
name="destinationaccount",
|
||||
type="string",
|
||||
description="account to move the money to (e.g. checking or savings)",
|
||||
),
|
||||
ToolArgument(
|
||||
name="userConfirmation",
|
||||
type="string",
|
||||
description="Indication of user's desire to move money",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
financial_submit_loan_approval = ToolDefinition(
|
||||
name="FinCheckAccountSubmitLoanApproval",
|
||||
description="Submit a loan application. " "Returns the loan status. ",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="email_address_or_account_ID",
|
||||
type="string",
|
||||
description="email address or account ID of user",
|
||||
),
|
||||
ToolArgument(
|
||||
name="amount",
|
||||
type="string",
|
||||
description="amount requested for the loan",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# ----- ECommerce Use Case Tools -----
|
||||
ecomm_list_orders = ToolDefinition(
|
||||
name="ListOrders",
|
||||
description="Get all orders for a certain email address.",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="email_address",
|
||||
type="string",
|
||||
description="Email address of user by which to find orders",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
ecomm_get_order = ToolDefinition(
|
||||
name="GetOrder",
|
||||
description="Get infromation about an order by order ID.",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="order_id",
|
||||
type="string",
|
||||
description="ID of order to determine status of",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
ecomm_track_package = ToolDefinition(
|
||||
name="TrackPackage",
|
||||
description="Get tracking information for a package by shipping provider and tracking ID",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="tracking_id",
|
||||
type="string",
|
||||
description="ID of package to track",
|
||||
),
|
||||
ToolArgument(
|
||||
name="userConfirmation",
|
||||
type="string",
|
||||
description="Indication of user's desire to get package tracking information",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
7
tools/transfer_control.py
Normal file
7
tools/transfer_control.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import shared.config
|
||||
|
||||
def transfer_control(args: dict) -> dict:
|
||||
|
||||
return {
|
||||
"new_goal": shared.config.AGENT_GOAL,
|
||||
}
|
||||
@@ -5,7 +5,8 @@ from typing import Dict, Any, Union, List, Optional, Deque, TypedDict
|
||||
from temporalio.common import RetryPolicy
|
||||
from temporalio import workflow
|
||||
|
||||
from models.data_types import ConversationHistory, NextStep, ValidationInput
|
||||
from models.data_types import ConversationHistory, EnvLookupOutput, NextStep, ValidationInput, EnvLookupInput
|
||||
from models.tool_definitions import AgentGoal
|
||||
from workflows.workflow_helpers import LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT, \
|
||||
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT
|
||||
from workflows import workflow_helpers as helpers
|
||||
@@ -19,15 +20,18 @@ with workflow.unsafe.imports_passed_through():
|
||||
CombinedInput,
|
||||
ToolPromptInput,
|
||||
)
|
||||
from tools.goal_registry import goal_list
|
||||
|
||||
# Constants
|
||||
MAX_TURNS_BEFORE_CONTINUE = 250
|
||||
|
||||
#ToolData as part of the workflow is what's accessible to the UI - see LLMResponse.jsx for example
|
||||
class ToolData(TypedDict, total=False):
|
||||
next: NextStep
|
||||
tool: str
|
||||
args: Dict[str, Any]
|
||||
response: str
|
||||
force_confirm: bool = True
|
||||
|
||||
@workflow.defn
|
||||
class AgentGoalWorkflow:
|
||||
@@ -39,15 +43,24 @@ class AgentGoalWorkflow:
|
||||
self.conversation_summary: Optional[str] = None
|
||||
self.chat_ended: bool = False
|
||||
self.tool_data: Optional[ToolData] = None
|
||||
self.confirm: bool = False
|
||||
self.confirmed: bool = False # indicates that we have confirmation to proceed to run tool
|
||||
self.tool_results: List[Dict[str, Any]] = []
|
||||
self.goal: AgentGoal = {"tools": []}
|
||||
self.show_tool_args_confirmation: bool = True # set from env file in activity lookup_wf_env_settings
|
||||
self.multi_goal_mode: bool = False # set from env file in activity lookup_wf_env_settings
|
||||
|
||||
# see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set
|
||||
@workflow.run
|
||||
async def run(self, combined_input: CombinedInput) -> str:
|
||||
"""Main workflow execution method."""
|
||||
params = combined_input.tool_params
|
||||
agent_goal = combined_input.agent_goal
|
||||
|
||||
"""Main workflow execution method."""
|
||||
# setup phase, starts with blank tool_params and agent_goal prompt as defined in tools/goal_registry.py
|
||||
params = combined_input.tool_params
|
||||
self.goal = combined_input.agent_goal
|
||||
|
||||
await self.lookup_wf_env_settings(combined_input)
|
||||
|
||||
# add message from sample conversation provided in tools/goal_registry.py, if it exists
|
||||
if params and params.conversation_summary:
|
||||
self.add_message("conversation_summary", params.conversation_summary)
|
||||
self.conversation_summary = params.conversation_summary
|
||||
@@ -55,47 +68,47 @@ class AgentGoalWorkflow:
|
||||
if params and params.prompt_queue:
|
||||
self.prompt_queue.extend(params.prompt_queue)
|
||||
|
||||
waiting_for_confirm = False
|
||||
waiting_for_confirm = False
|
||||
current_tool = None
|
||||
|
||||
# This is the main interactive loop. Main responsibilities:
|
||||
# - Selecting and changing goals as directed by the user
|
||||
# - reacting to user input (from signals)
|
||||
# - validating user input to make sure it makes sense with the current goal and tools
|
||||
# - calling the LLM through activities to determine next steps and prompts
|
||||
# - executing the selected tools via activities
|
||||
while True:
|
||||
# wait indefinitely for input from signals - user_prompt, end_chat, or confirm as defined below
|
||||
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.confirmed
|
||||
)
|
||||
|
||||
if self.chat_ended:
|
||||
workflow.logger.info("Chat ended.")
|
||||
# handle chat should end. When chat ends, push conversation history to workflow results.
|
||||
if self.chat_should_end():
|
||||
return f"{self.conversation_history}"
|
||||
|
||||
if self.confirm and waiting_for_confirm and current_tool and self.tool_data:
|
||||
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)
|
||||
|
||||
await helpers.handle_tool_execution(
|
||||
current_tool,
|
||||
self.tool_data,
|
||||
self.tool_results,
|
||||
self.add_message,
|
||||
self.prompt_queue
|
||||
)
|
||||
# Execute the tool
|
||||
if self.ready_for_tool_execution(waiting_for_confirm, current_tool):
|
||||
waiting_for_confirm = await self.execute_tool(current_tool)
|
||||
continue
|
||||
|
||||
# process forward on the prompt queue if any
|
||||
if self.prompt_queue:
|
||||
# get most recent prompt
|
||||
prompt = self.prompt_queue.popleft()
|
||||
if not prompt.startswith("###"):
|
||||
workflow.logger.info(f"workflow step: processing message on the prompt queue, message is {prompt}")
|
||||
|
||||
# Validate user-provided prompts
|
||||
if self.is_user_prompt(prompt):
|
||||
self.add_message("user", prompt)
|
||||
|
||||
# Validate the prompt before proceeding
|
||||
validation_input = ValidationInput(
|
||||
prompt=prompt,
|
||||
conversation_history=self.conversation_history,
|
||||
agent_goal=agent_goal,
|
||||
agent_goal=self.goal,
|
||||
)
|
||||
validation_result = await workflow.execute_activity(
|
||||
validation_result = await workflow.execute_activity_method(
|
||||
ToolActivities.agent_validatePrompt,
|
||||
args=[validation_input],
|
||||
schedule_to_close_timeout=LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||
@@ -105,26 +118,23 @@ class AgentGoalWorkflow:
|
||||
),
|
||||
)
|
||||
|
||||
# If validation fails, provide that feedback to the user - i.e., "your words make no sense, puny human" end this iteration of processing
|
||||
if not validation_result.validationResult:
|
||||
workflow.logger.warning(
|
||||
f"Prompt validation failed: {validation_result.validationFailedReason}"
|
||||
)
|
||||
self.add_message(
|
||||
"agent", validation_result.validationFailedReason
|
||||
)
|
||||
workflow.logger.warning(f"Prompt validation failed: {validation_result.validationFailedReason}")
|
||||
self.add_message("agent", validation_result.validationFailedReason)
|
||||
continue
|
||||
|
||||
# Proceed with generating the context and prompt
|
||||
# If valid, proceed with generating the context and prompt
|
||||
context_instructions = generate_genai_prompt(
|
||||
agent_goal, self.conversation_history, self.tool_data
|
||||
)
|
||||
agent_goal=self.goal,
|
||||
conversation_history = self.conversation_history,
|
||||
multi_goal_mode=self.multi_goal_mode,
|
||||
raw_json=self.tool_data)
|
||||
|
||||
prompt_input = ToolPromptInput(prompt=prompt, context_instructions=context_instructions)
|
||||
|
||||
prompt_input = ToolPromptInput(
|
||||
prompt=prompt,
|
||||
context_instructions=context_instructions,
|
||||
)
|
||||
|
||||
tool_data = await workflow.execute_activity(
|
||||
# connect to LLM and execute to get next steps
|
||||
tool_data = await workflow.execute_activity_method(
|
||||
ToolActivities.agent_toolPlanner,
|
||||
prompt_input,
|
||||
schedule_to_close_timeout=LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||
@@ -133,57 +143,105 @@ class AgentGoalWorkflow:
|
||||
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||
),
|
||||
)
|
||||
|
||||
tool_data["force_confirm"] = self.show_tool_args_confirmation
|
||||
self.tool_data = tool_data
|
||||
|
||||
# process the tool as dictated by the prompt response - what to do next, and with which tool
|
||||
next_step = tool_data.get("next")
|
||||
current_tool = tool_data.get("tool")
|
||||
|
||||
workflow.logger.info(f"next_step: {next_step}, current tool is {current_tool}")
|
||||
|
||||
# make sure we're ready to run the tool & have everything we need
|
||||
if next_step == "confirm" and current_tool:
|
||||
args = tool_data.get("args", {})
|
||||
# if we're missing arguments, ask for them
|
||||
if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue):
|
||||
continue
|
||||
|
||||
waiting_for_confirm = True
|
||||
self.confirm = False
|
||||
workflow.logger.info("Waiting for user confirm signal...")
|
||||
|
||||
# We have needed arguments, if we want to force the user to confirm, set that up
|
||||
if self.show_tool_args_confirmation:
|
||||
self.confirmed = False # set that we're not confirmed
|
||||
workflow.logger.info("Waiting for user confirm signal...")
|
||||
# if we have all needed arguments (handled above) and not holding for a debugging confirm, proceed:
|
||||
else:
|
||||
self.confirmed = True
|
||||
# else if the next step is to pick a new goal, set that to be the goal
|
||||
elif next_step == "pick-new-goal":
|
||||
workflow.logger.info("All steps completed. Resetting goal.")
|
||||
self.change_goal("goal_choose_agent_type")
|
||||
|
||||
|
||||
|
||||
# else if the next step is to be done with the conversation such as if the user requests it via asking to "end conversation"
|
||||
elif next_step == "done":
|
||||
workflow.logger.info("All steps completed. Exiting workflow.")
|
||||
|
||||
self.add_message("agent", tool_data)
|
||||
|
||||
#here we could send conversation to AI for analysis
|
||||
|
||||
# end the workflow
|
||||
return str(self.conversation_history)
|
||||
|
||||
self.add_message("agent", tool_data)
|
||||
await helpers.continue_as_new_if_needed(
|
||||
self.conversation_history,
|
||||
self.prompt_queue,
|
||||
agent_goal,
|
||||
self.goal,
|
||||
MAX_TURNS_BEFORE_CONTINUE,
|
||||
self.add_message
|
||||
)
|
||||
|
||||
#Signal that comes from api/main.py via a post to /send-prompt
|
||||
@workflow.signal
|
||||
async def user_prompt(self, prompt: str) -> None:
|
||||
"""Signal handler for receiving user prompts."""
|
||||
workflow.logger.info(f"signal received: user_prompt, prompt is {prompt}")
|
||||
if self.chat_ended:
|
||||
workflow.logger.warn(f"Message dropped due to chat closed: {prompt}")
|
||||
workflow.logger.info(f"Message dropped due to chat closed: {prompt}")
|
||||
return
|
||||
self.prompt_queue.append(prompt)
|
||||
|
||||
#Signal that comes from api/main.py via a post to /confirm
|
||||
@workflow.signal
|
||||
async def confirm(self) -> None:
|
||||
"""Signal handler for user confirmation of tool execution."""
|
||||
workflow.logger.info("Received user confirmation")
|
||||
self.confirm = True
|
||||
workflow.logger.info("Received user signal: confirmation")
|
||||
self.confirmed = True
|
||||
|
||||
#Signal that comes from api/main.py via a post to /end-chat
|
||||
@workflow.signal
|
||||
async def end_chat(self) -> None:
|
||||
"""Signal handler for ending the chat session."""
|
||||
workflow.logger.info("signal received: end_chat")
|
||||
self.chat_ended = True
|
||||
|
||||
#Signal that can be sent from Temporal Workflow UI to enable debugging confirm and override .env setting
|
||||
@workflow.signal
|
||||
async def enable_debugging_confirm(self) -> None:
|
||||
"""Signal handler for enabling debugging confirm UI & associated logic."""
|
||||
workflow.logger.info("signal received: enable_debugging_confirm")
|
||||
self.enable_debugging_confirm = True
|
||||
|
||||
#Signal that can be sent from Temporal Workflow UI to disable debugging confirm and override .env setting
|
||||
@workflow.signal
|
||||
async def disable_debugging_confirm(self) -> None:
|
||||
"""Signal handler for disabling debugging confirm UI & associated logic."""
|
||||
workflow.logger.info("signal received: disable_debugging_confirm")
|
||||
self.enable_debugging_confirm = False
|
||||
|
||||
@workflow.query
|
||||
def get_conversation_history(self) -> ConversationHistory:
|
||||
"""Query handler to retrieve the full conversation history."""
|
||||
return self.conversation_history
|
||||
|
||||
@workflow.query
|
||||
def get_agent_goal(self) -> AgentGoal:
|
||||
"""Query handler to retrieve the current goal of the agent."""
|
||||
return self.goal
|
||||
|
||||
@workflow.query
|
||||
def get_summary_from_history(self) -> Optional[str]:
|
||||
@@ -212,3 +270,101 @@ class AgentGoalWorkflow:
|
||||
self.conversation_history["messages"].append(
|
||||
{"actor": actor, "response": response}
|
||||
)
|
||||
|
||||
def change_goal(self, goal: str) -> None:
|
||||
""" Change the goal (usually on request of the user).
|
||||
|
||||
Args:
|
||||
goal: goal to change to)
|
||||
"""
|
||||
if goal is not None:
|
||||
for listed_goal in goal_list:
|
||||
if listed_goal.id == goal:
|
||||
self.goal = listed_goal
|
||||
workflow.logger.info("Changed goal to " + goal)
|
||||
if goal is None:
|
||||
workflow.logger.warning("Goal not set after goal reset, probably bad.") # if this happens, there's probably a problem with the goal list
|
||||
|
||||
|
||||
# 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.confirmed 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
|
||||
|
||||
# look up env settings in an activity so they're part of history
|
||||
async def lookup_wf_env_settings(self, combined_input: CombinedInput)->None:
|
||||
env_lookup_input = EnvLookupInput(
|
||||
show_confirm_env_var_name = "SHOW_CONFIRM",
|
||||
show_confirm_default = True,
|
||||
)
|
||||
env_output:EnvLookupOutput = await workflow.execute_activity_method(
|
||||
ToolActivities.get_wf_env_vars,
|
||||
env_lookup_input,
|
||||
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||
),
|
||||
)
|
||||
self.show_tool_args_confirmation = env_output.show_confirm
|
||||
self.multi_goal_mode = env_output.multi_goal_mode
|
||||
|
||||
# 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.confirmed = 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")
|
||||
self.change_goal(new_goal)
|
||||
elif "ListAgents" in self.tool_results[-1].values() and self.goal.id != "goal_choose_agent_type":
|
||||
self.change_goal("goal_choose_agent_type")
|
||||
return waiting_for_confirm
|
||||
|
||||
# debugging helper - drop this in various places in the workflow to get status
|
||||
# also don't forget you can look at the workflow itself and do queries if you want
|
||||
def print_useful_workflow_vars(self, status_or_step:str) -> None:
|
||||
print(f"***{status_or_step}:***")
|
||||
if self.goal:
|
||||
print(f"current goal: {self.goal.id}")
|
||||
if self.tool_data:
|
||||
print(f"force confirm? {self.tool_data['force_confirm']}")
|
||||
print(f"next step: {self.tool_data.get('next')}")
|
||||
print(f"current_tool: {self.tool_data.get('tool')}")
|
||||
else:
|
||||
print("no tool data initialized yet")
|
||||
print(f"self.confirmed: {self.confirmed}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user