documentation & guidance updates, getting things done, fixing a possible NDE if you change env vars, changes to enable user picking "done", minor test changes, minor goal selection prompt improvements

This commit is contained in:
Joshua Smith
2025-04-03 15:54:44 -04:00
parent 40bd76e80f
commit 87b5699dc1
13 changed files with 189 additions and 67 deletions

View File

@@ -35,8 +35,8 @@ OPENAI_API_KEY=sk-proj-...
# Uncomment if using API key (not needed for local dev server) # Uncomment if using API key (not needed for local dev server)
# TEMPORAL_API_KEY=abcdef1234567890 # TEMPORAL_API_KEY=abcdef1234567890
# Set starting goal of agent # Set starting goal of agent - if unset default is all
AGENT_GOAL=goal_choose_agent_type # default, for multi-goal AGENT_GOAL=goal_choose_agent_type # default for multi-goal start
#AGENT_GOAL=goal_event_flight_invoice # for original goal #AGENT_GOAL=goal_event_flight_invoice # for original goal
#AGENT_GOAL=goal_match_train_invoice # for replay goal #AGENT_GOAL=goal_match_train_invoice # for replay goal
@@ -44,7 +44,7 @@ AGENT_GOAL=goal_choose_agent_type # default, for multi-goal
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin # default is all GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin # default is all
#GOAL_CATEGORIES=travel-flights #GOAL_CATEGORIES=travel-flights
# Set if the UI should force a user confirmation step or not # 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 SHOW_CONFIRM=True
# Money Scenarios: # Money Scenarios:

View File

@@ -12,6 +12,18 @@ It's really helpful to [watch the demo (5 minute YouTube video)](https://www.you
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. 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). For more, check out [architecture-decisions](./architecture-decisions.md).
## What is "Agentic AI"?
These are the key elements of an agentic framework:
1. Goals a human can get done, made up of tools that can execute individual steps
2. The "agent loop" - call LLM, either call tools or prompt human, repeat until goal(s) are done
3. Support for tool calls that require human 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 prompts)
7. Bonus: durable tool execution via Temporal Activities
For a deeper dive into this, check out the [architecture guide](./architecture.md).
## Setup and Configuration ## Setup and Configuration
See [the Setup guide](./setup.md). See [the Setup guide](./setup.md).

View File

@@ -11,7 +11,7 @@ import google.generativeai as genai
import anthropic import anthropic
import deepseek import deepseek
from dotenv import load_dotenv from dotenv import load_dotenv
from models.data_types import ValidationInput, ValidationResult, ToolPromptInput from models.data_types import ValidationInput, ValidationResult, ToolPromptInput, EnvLookupInput
load_dotenv(override=True) load_dotenv(override=True)
print( print(
@@ -471,6 +471,20 @@ class ToolActivities:
print(f"Full response: {response_content}") print(f"Full response: {response_content}")
raise raise
# get env vars for workflow
@activity.defn
async def get_env_bool(self, input: EnvLookupInput) -> bool:
""" gets boolean env vars for workflow as an activity result so it's deterministic
handles default/None
"""
value = os.getenv(input.env_var_name)
if value is None:
return input.default
if value is not None and value.lower() == "false":
return False
else:
return True
def get_current_date_human_readable(): def get_current_date_human_readable():
""" """
@@ -487,8 +501,6 @@ def get_current_date_human_readable():
async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict: async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
from tools import get_handler from tools import get_handler
# if current_tool == "move_money":
# workflow.logger.warning(f"trying for move_money direct")
tool_name = activity.info().activity_type # e.g. "FindEvents" tool_name = activity.info().activity_type # e.g. "FindEvents"
tool_args = activity.payload_converter().from_payload(args[0].payload, dict) tool_args = activity.payload_converter().from_payload(args[0].payload, dict)
activity.logger.info(f"Running dynamic tool '{tool_name}' with args: {tool_args}") activity.logger.info(f"Running dynamic tool '{tool_name}' with args: {tool_args}")
@@ -503,3 +515,5 @@ async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
# Optionally log or augment the result # Optionally log or augment the result
activity.logger.info(f"Tool '{tool_name}' result: {result}") activity.logger.info(f"Tool '{tool_name}' result: {result}")
return result return result

View File

@@ -1,16 +1,16 @@
## Customizing the Agent # Customizing the Agent
The agent is set up to allow for multiple goals and to switch back to choosing a new goal at the end of every successful goal. A goal is made up of a list of tools that the agent will guide the user through. The agent is set up to allow for multiple goals and to switch back to choosing a new goal at the end of every successful goal. A goal is made up of a list of tools that the agent will guide the user through.
It may be helpful to review the [architecture](./architecture.md) for a guide and definition of goals, tools, etc. It may be helpful to review the [architecture](./architecture.md) for a guide and definition of goals, tools, etc.
### Adding a New Goal Category ## Adding a New Goal Category
Goal Categories lets you pick which groups of goals to show. Set via an .env setting, GOAL_CATEGORIES. Goal Categories lets you pick which groups of goals to show. Set via an .env setting, GOAL_CATEGORIES.
1. Pick a unique one that has some business meaning 1. Pick a unique one that has some business meaning
2. Use it in your .env file 2. Use it in your .env file
3. Add to [.env.example](./.env.example) 3. Add to [.env.example](./.env.example)
4. Use it in your Goal definition, see below. 4. Use it in your Goal definition, see below.
### Adding a Goal ## 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 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") 2. Pick a name for your goal! (such as "goal_hr_schedule_pto")
3. Fill out the required elements: 3. Fill out the required elements:
@@ -34,9 +34,9 @@ tools=[
- `example_conversation_history`: LLM-facing sample conversation/interaction regarding the goal. See the existing goals for how to structure this. - `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)` 4. Add your new goal to the `goal_list` at the bottom using `goal_list.append(your_super_sweet_new_goal)`
### Adding Tools ## Adding Tools
#### Notes ### 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: 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:
``` ```
@@ -47,24 +47,42 @@ description="Help the user gather args for these tools in order: "
"4. BookPTO: Book PTO " "4. BookPTO: Book PTO "
``` ```
#### Add to Tool Registry 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) 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 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`. - `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 - `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=[]` - `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 ### 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 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 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 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` - 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 - 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() ### 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. - 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: Example:
``` ```
if tool_name == "CurrentPTO": if tool_name == "CurrentPTO":
return current_pto 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.

View File

@@ -42,3 +42,8 @@ class ValidationResult:
# Initialize empty dict if None # Initialize empty dict if None
if self.validationFailedReason is None: if self.validationFailedReason is None:
self.validationFailedReason = {} self.validationFailedReason = {}
@dataclass
class EnvLookupInput:
env_var_name: str
default: bool

View File

@@ -126,7 +126,9 @@ def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) ->
'{"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)>"}' '{"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. " "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 "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. '
'Next should only be "pick-new-goal" if all tools have been run (use the system prompt to figure that out).' 'Next should only be "pick-new-goal" if all tools have been run (use the system prompt to figure that out).'
#'If all tools have been run (use the system prompt to figure that out) then clear tool history.' todo maybe fix this
) )
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:

View File

@@ -62,6 +62,7 @@ async def main():
activities=[ activities=[
activities.agent_validatePrompt, activities.agent_validatePrompt,
activities.agent_toolPlanner, activities.agent_toolPlanner,
activities.get_env_bool,
dynamic_tool_activity, dynamic_tool_activity,
], ],
activity_executor=activity_executor, activity_executor=activity_executor,

View File

@@ -8,7 +8,8 @@ cp .env.example .env
``` ```
Then add API keys, configuration, as desired. Then add API keys, configuration, as desired.
If you want to show confirmations/enable the debugging UI, set
If you want to show confirmations/enable the debugging UI that shows tool args, set
```bash ```bash
SHOW_CONFIRM=True SHOW_CONFIRM=True
``` ```
@@ -188,13 +189,19 @@ 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`. 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: Money Movement #### Goals: FIN/Money Movement
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. - `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): 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 ```bash
FIN_START_REAL_WORKFLOW=FALSE #set this to true to start a real workflow 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).
## Customizing the Agent Further ## Customizing the Agent Further
- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them) - `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 - `goal_registry.py` contains descriptions of goals and the tools used to achieve them

View File

@@ -5,7 +5,7 @@ from temporalio.worker import Worker
from temporalio.testing import TestWorkflowEnvironment from temporalio.testing import TestWorkflowEnvironment
from api.main import get_initial_agent_goal from api.main import get_initial_agent_goal
from models.data_types import AgentGoalWorkflowParams, CombinedInput from models.data_types import AgentGoalWorkflowParams, CombinedInput
from workflows import AgentGoalWorkflow from workflows.agent_goal_workflow import AgentGoalWorkflow
from activities.tool_activities import ToolActivities, dynamic_tool_activity from activities.tool_activities import ToolActivities, dynamic_tool_activity
@@ -17,12 +17,7 @@ async def asyncTearDown(self):
# Clean up after tests # Clean up after tests
await self.env.shutdown() await self.env.shutdown()
async def test_workflow_success(client: Client): async def test_flight_booking(client: Client):
# Register the workflow and activity
# self.env.register_workflow(AgentGoalWorkflow)
# self.env.register_activity(ToolActivities.agent_validatePrompt)
# self.env.register_activity(ToolActivities.agent_toolPlanner)
# self.env.register_activity(dynamic_tool_activity)
task_queue_name = "agent-ai-workflow" task_queue_name = "agent-ai-workflow"
workflow_id = "agent-workflow" workflow_id = "agent-workflow"
@@ -37,16 +32,19 @@ async def test_workflow_success(client: Client):
workflow_id = "agent-workflow" workflow_id = "agent-workflow"
async with Worker(client, task_queue=task_queue_name, workflows=[AgentGoalWorkflow], activities=[ToolActivities.agent_validatePrompt, ToolActivities.agent_toolPlanner, dynamic_tool_activity]): 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( handle = await client.start_workflow(
AgentGoalWorkflow.run, id=workflow_id, task_queue=task_queue_name AgentGoalWorkflow.run, id=workflow_id, task_queue=task_queue_name
) )
# todo fix signals # todo send signals based on
await handle.signal(AgentGoalWorkflow.submit_greeting, "user1") await handle.signal(AgentGoalWorkflow.user_prompt, "book flights")
await handle.signal(AgentGoalWorkflow.submit_greeting, "user2") await handle.signal(AgentGoalWorkflow.user_prompt, "sydney in september")
assert WorkflowExecutionStatus.RUNNING == (await handle.describe()).status assert WorkflowExecutionStatus.RUNNING == (await handle.describe()).status
await handle.signal(AgentGoalWorkflow.exit)
assert ["Hello, user1", "Hello, user2"] == await handle.result() #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 assert WorkflowExecutionStatus.COMPLETED == (await handle.describe()).status

40
todo.md
View File

@@ -1,27 +1,45 @@
# todo list # todo list
[ ] mergey stuffs <br />
- [x] make confirm work how you want when force_confirm is on and off <br />
- [x] test with confirm on and off - single goal <br />
- [x] confirmation off-> it's unclear it's asking for confirmation unless we set `self.confirm = False` in the workflow - maybe we should take the args/confirm route? - test with confirm on box - test with book PTO<br />
- [x] test with confirm on and off - multi goal <br />
- [x] documenting confirm <br />
- [x] document how to do debugging confirm force confirm with toolchain in setup and adding-goals-and-tools <br />
- [x] document how to do optional confirm at goal/tool level <br />
- [ ] goal change management tweaks <br />
- [ ] maybe make the choose_Agent_goal tag not be system/not always included? <br />
- [ ] try taking out list-agents as a tool because agent_prompt_generators may do it for you <br />
- [ ] make goal selection not be a system tool but be an option in .env, see how that works, includes taking it out of the goal/toolset for all goals <br />
- [x] make the goal selection/capabilities work how you want <br />
- [x] make end-conversation work when force_confirm is on and off <br />
- [x] make tool selection work when force_confirm is on and off <br />
- [x] updates to PTO and money movement setup docs re data file <br />
- [x] fixing NDE about changing force_confirm <br />
- [x] rename self.confirm to self.confirmed to be clearer
- [x] rename show_confirm to show_confirm_and_tool_args
- [x] remove print debugging and todo comments
[ ] expand [tests](./tests/agent_goal_workflow_test.py)<br />
[ ] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py) <br /> [ ] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py) <br />
[x] make agent respond to name of goals and not just numbers <br /> [x] make agent respond to name of goals and not just numbers <br />
[x] josh to do fintech scenarios <br /> [x] josh to do fintech scenarios <br />
[ ] expand [tests](./tests/agent_goal_workflow_test.py)<br />
[ ] fintech goals <br /> [ ] 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 /> - 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 customers financial data (e.g., income, spending habits, savings, investments) and provides tailored advice, such as budgeting tips, investment options, or debt repayment strategies.<br /> - Personalized Financial Advice - An AI agent analyzes a customers 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 customers investment portfolio, rebalancing it automatically based on market trends, risk tolerance, and financial goals (e.g., shifting assets between stocks, bonds, or crypto).<br /> - Portfolio Management and Rebalancing - The AI monitors a customers investment portfolio, rebalancing it automatically based on market trends, risk tolerance, and financial goals (e.g., shifting assets between stocks, bonds, or crypto).<br />
[x] money movement - start money transfer <br />
[x] todo use env vars to do connect to local or non-local
[x] account balance - <br />
[ ] new loan/fraud check/update with start <br /> [ ] new loan/fraud check/update with start <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 /> [ ] 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 agents performance <br /> - Insight into the agents performance <br />
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br /> [ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br />
[ ] add visual feedback when workflow starting <br /> [ ] add visual feedback when workflow starting <br />
[ ] figure out how to allow user to list agents at any time - like end conversation <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).
[ ] change initial goal selection prompt to list capabilities and prompt more nicely - not a bulleted list - see how that works
[x] todo use env vars to do connect to local or non-local cloud for activities for money scenarios

View File

@@ -11,6 +11,16 @@
"email": "laine@awesome.com", "email": "laine@awesome.com",
"currentPTOHrs": 40, "currentPTOHrs": 40,
"hrsAddedPerMonth": 12 "hrsAddedPerMonth": 12
},
{
"email": "steve.this.is.for.you@gmail.com",
"currentPTOHrs": 4000,
"hrsAddedPerMonth": 20
},
{
"email": "your_email_here@yourcompany.com",
"currentPTOHrs": 150,
"hrsAddedPerMonth": 19
} }
] ]
} }

View File

@@ -39,8 +39,8 @@ goal_choose_agent_type = AgentGoal(
"agent: Here are the currently available agents.", "agent: Here are the currently available agents.",
"user_confirmed_tool_run: <user clicks confirm on ListAgents tool>", "user_confirmed_tool_run: <user clicks confirm on ListAgents tool>",
"tool_result: { 'agent_name': 'Event Flight Finder', 'goal_id': 'goal_event_flight_invoice', 'agent_description': 'Helps users find interesting events and arrange travel to them' }", "tool_result: { 'agent_name': 'Event Flight Finder', 'goal_id': 'goal_event_flight_invoice', 'agent_description': 'Helps users find interesting events and arrange travel to them' }",
"agent: The available agents are: 1. Event Flight Finder. \n Which agent would you like to speak to?", "agent: The available agents are: 1. Event Flight Finder. \n Which agent would you like to speak to (?",
"user: 1", "user: 1, Event Flight Finder",
"user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>", "user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>",
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }", "tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
] ]
@@ -153,7 +153,7 @@ goal_event_flight_invoice = AgentGoal(
tool_registry.find_events_tool, tool_registry.find_events_tool,
tool_registry.search_flights_tool, tool_registry.search_flights_tool,
tool_registry.create_invoice_tool, tool_registry.create_invoice_tool,
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end #tool_registry.list_agents_tool, #last tool must be list_agents to faciliate changing back to picking an agent again at the end
], ],
description="Help the user gather args for these tools in order: " description="Help the user gather args for these tools in order: "
"1. FindEvents: Find an event to travel to " "1. FindEvents: Find an event to travel to "

View File

@@ -6,7 +6,7 @@ from typing import Dict, Any, Union, List, Optional, Deque, TypedDict
from temporalio.common import RetryPolicy from temporalio.common import RetryPolicy
from temporalio import workflow from temporalio import workflow
from models.data_types import ConversationHistory, NextStep, ValidationInput from models.data_types import ConversationHistory, NextStep, ValidationInput, EnvLookupInput
from models.tool_definitions import AgentGoal from models.tool_definitions import AgentGoal
from workflows.workflow_helpers import LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT, \ from workflows.workflow_helpers import LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT, \
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT
@@ -26,12 +26,6 @@ with workflow.unsafe.imports_passed_through():
# Constants # Constants
MAX_TURNS_BEFORE_CONTINUE = 250 MAX_TURNS_BEFORE_CONTINUE = 250
show_confirm_env = os.getenv("SHOW_CONFIRM")
if show_confirm_env is not None and show_confirm_env.lower() == "false":
SHOW_CONFIRM = False
else:
SHOW_CONFIRM = True
#ToolData as part of the workflow is what's accessible to the UI - see LLMResponse.jsx for example #ToolData as part of the workflow is what's accessible to the UI - see LLMResponse.jsx for example
class ToolData(TypedDict, total=False): class ToolData(TypedDict, total=False):
next: NextStep next: NextStep
@@ -50,9 +44,10 @@ class AgentGoalWorkflow:
self.conversation_summary: Optional[str] = None self.conversation_summary: Optional[str] = None
self.chat_ended: bool = False self.chat_ended: bool = False
self.tool_data: Optional[ToolData] = None 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.tool_results: List[Dict[str, Any]] = []
self.goal: AgentGoal = {"tools": []} self.goal: AgentGoal = {"tools": []}
self.show_tool_args_confirmation: bool = True
# see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set # see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set
@workflow.run @workflow.run
@@ -63,6 +58,8 @@ class AgentGoalWorkflow:
params = combined_input.tool_params params = combined_input.tool_params
self.goal = combined_input.agent_goal 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 # add message from sample conversation provided in tools/goal_registry.py, if it exists
if params and params.conversation_summary: if params and params.conversation_summary:
self.add_message("conversation_summary", params.conversation_summary) self.add_message("conversation_summary", params.conversation_summary)
@@ -83,7 +80,7 @@ class AgentGoalWorkflow:
while True: while True:
# wait indefinitely for input from signals - user_prompt, end_chat, or confirm as defined below # wait indefinitely for input from signals - user_prompt, end_chat, or confirm as defined below
await workflow.wait_condition( await workflow.wait_condition(
lambda: bool(self.prompt_queue) or self.chat_ended or self.confirm lambda: bool(self.prompt_queue) or self.chat_ended or self.confirmed
) )
# handle chat should end. When chat ends, push conversation history to workflow results. # handle chat should end. When chat ends, push conversation history to workflow results.
@@ -141,7 +138,8 @@ class AgentGoalWorkflow:
initial_interval=timedelta(seconds=5), backoff_coefficient=1 initial_interval=timedelta(seconds=5), backoff_coefficient=1
), ),
) )
tool_data["force_confirm"] = SHOW_CONFIRM
tool_data["force_confirm"] = self.show_tool_args_confirmation
self.tool_data = tool_data self.tool_data = tool_data
# process the tool as dictated by the prompt response - what to do next, and with which tool # process the tool as dictated by the prompt response - what to do next, and with which tool
@@ -150,30 +148,35 @@ class AgentGoalWorkflow:
workflow.logger.info(f"next_step: {next_step}, current tool is {current_tool}") workflow.logger.info(f"next_step: {next_step}, current tool is {current_tool}")
#if the next step is to confirm... # make sure we're ready to run the tool & have everything we need
if next_step == "confirm" and current_tool: if next_step == "confirm" and current_tool:
args = tool_data.get("args", {}) args = tool_data.get("args", {})
#if we're missing arguments, go back to the top of the loop # if we're missing arguments, ask for them
if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue): if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue):
continue continue
#...otherwise, if we want to force the user to confirm, set that up
waiting_for_confirm = True waiting_for_confirm = True
if SHOW_CONFIRM:
self.confirm = False # 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...") 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: else:
#theory - set self.confirm to true bc that's the signal, so we can get around the signal?? self.confirmed = True
self.confirm = True
# else if the next step is to pick a new goal... # else if the next step is to pick a new goal...
elif next_step == "pick-new-goal": elif next_step == "pick-new-goal":
workflow.logger.info("All steps completed. Resetting goal.") workflow.logger.info("All steps completed. Resetting goal.")
self.change_goal("goal_choose_agent_type") self.change_goal("goal_choose_agent_type")
# else if the next step is to be done - this should only happen if the user requests it via "end conversation" # 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": elif next_step == "done":
self.add_message("agent", tool_data) self.add_message("agent", tool_data)
#todo send conversation to AI for analysis
# end the workflow # end the workflow
return str(self.conversation_history) return str(self.conversation_history)
@@ -198,10 +201,10 @@ class AgentGoalWorkflow:
#Signal that comes from api/main.py via a post to /confirm #Signal that comes from api/main.py via a post to /confirm
@workflow.signal @workflow.signal
async def confirm(self) -> None: async def confirmed(self) -> None:
"""Signal handler for user confirmation of tool execution.""" """Signal handler for user confirmation of tool execution."""
workflow.logger.info("Received user signal: confirmation") workflow.logger.info("Received user signal: confirmation")
self.confirm = True self.confirmed = True
#Signal that comes from api/main.py via a post to /end-chat #Signal that comes from api/main.py via a post to /end-chat
@workflow.signal @workflow.signal
@@ -210,6 +213,20 @@ class AgentGoalWorkflow:
workflow.logger.info("signal received: end_chat") workflow.logger.info("signal received: end_chat")
self.chat_ended = True 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 @workflow.query
def get_conversation_history(self) -> ConversationHistory: def get_conversation_history(self) -> ConversationHistory:
"""Query handler to retrieve the full conversation history.""" """Query handler to retrieve the full conversation history."""
@@ -274,7 +291,7 @@ class AgentGoalWorkflow:
# define if we're ready for tool execution # define if we're ready for tool execution
def ready_for_tool_execution(self, waiting_for_confirm: bool, current_tool: Any) -> bool: def ready_for_tool_execution(self, waiting_for_confirm: bool, current_tool: Any) -> bool:
if self.confirm and waiting_for_confirm and current_tool and self.tool_data: if self.confirmed and waiting_for_confirm and current_tool and self.tool_data:
return True return True
else: else:
return False return False
@@ -287,11 +304,23 @@ class AgentGoalWorkflow:
else: else:
return True return True
# look up env settings as needed in activities so they're part of history
async def lookup_wf_env_settings(self, combined_input: CombinedInput)->None:
env_lookup_input = EnvLookupInput(env_var_name = "SHOW_CONFIRM", default = True)
self.show_tool_args_confirmation = await workflow.execute_activity(
ToolActivities.get_env_bool,
env_lookup_input,
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=5), backoff_coefficient=1
),
)
# execute the tool - return False if we're not waiting for confirm anymore (always the case if it works successfully) # 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: async def execute_tool(self, current_tool: str)->bool:
workflow.logger.info(f"workflow step: user has confirmed, executing the tool {current_tool}") workflow.logger.info(f"workflow step: user has confirmed, executing the tool {current_tool}")
self.confirm = False self.confirmed = False
waiting_for_confirm = False waiting_for_confirm = False
confirmed_tool_data = self.tool_data.copy() confirmed_tool_data = self.tool_data.copy()
confirmed_tool_data["next"] = "user_confirmed_tool_run" confirmed_tool_data["next"] = "user_confirmed_tool_run"
@@ -317,5 +346,13 @@ class AgentGoalWorkflow:
self.change_goal("goal_choose_agent_type") self.change_goal("goal_choose_agent_type")
return waiting_for_confirm 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}:***")
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')}")
print(f"self.confirm: {self.confirmed}")
print(f"waiting_for_confirm (about to be set to true): {self.waiting_for_confirm}")