- dynamic agent prompt based on multi goal or not

- made choose_agent_goal be dynamically included
- made tool selection not be required in all toolchains
- changes to get env vars easier in workflow
- Updated docs/guides, todo based on aboe
This commit is contained in:
Joshua Smith
2025-04-08 15:01:11 -04:00
parent f567583b3a
commit c18a40b502
11 changed files with 150 additions and 59 deletions

View File

@@ -35,12 +35,13 @@ OPENAI_API_KEY=sk-proj-...
# Uncomment if using API key (not needed for local dev server)
# TEMPORAL_API_KEY=abcdef1234567890
# Set starting goal of agent - if unset default is all
AGENT_GOAL=goal_choose_agent_type # default for multi-goal start
# 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 - options are system (always included), hr, travel, or all.
#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=hr,travel-flights,travel-trains,fin # default is all
#GOAL_CATEGORIES=travel-flights

View File

@@ -11,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, EnvLookupInput
from models.data_types import EnvLookupOutput, ValidationInput, ValidationResult, ToolPromptInput, EnvLookupInput
load_dotenv(override=True)
print(
@@ -370,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", # todo try claude-3-7-sonnet-20250219
model="claude-3-7-sonnet-20250219", # todo try claude-3-7-sonnet-20250219
max_tokens=1024,
system=input.context_instructions
+ ". The current date is "
@@ -473,17 +474,29 @@ class ToolActivities:
# 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
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
"""
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
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:
return True
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():

View File

@@ -19,14 +19,13 @@ Goal Categories lets you pick which groups of goals to show. Set via an .env set
- `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]
- Important! If you want to prompt the user with options for another goal, the last tool listed must be `list_agents_tool`. This allows the chatbot to guide the user back to choosing from the list of available goals once a goal is complete. This is recommended for multi-goal behavior. The `goal_choose_agent_type` is the exception as it handles the changing of goals. <br />
Example:
```
tools=[
tool_registry.current_pto_tool,
tool_registry.future_pto_calc_tool,
tool_registry.book_pto_tool,
tool_registry.list_agents_tool,
]
```
- `description`: LLM-facing description of the goal that lists the tools by name and purpose.

View File

@@ -45,5 +45,10 @@ class ValidationResult:
@dataclass
class EnvLookupInput:
env_var_name: str
default: bool
show_confirm_env_var_name: str
show_confirm_default: bool
@dataclass
class EnvLookupOutput:
show_confirm: bool
multi_goal_mode: bool

View File

@@ -2,15 +2,17 @@ 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(
@@ -81,7 +83,7 @@ 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='confirm' and tool='ListAgents'.\n"
f"3) {generate_toolchain_complete_guidance()}\n"
"4) response should be short and user-friendly.\n"
)
@@ -127,8 +129,7 @@ def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) ->
"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. '
'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
f"{generate_pick_new_goal_guidance()}"
)
def generate_missing_args_prompt(current_tool: str, tool_data: dict, missing_args: list[str]) -> str:
@@ -148,3 +149,59 @@ 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 (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=''."

View File

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

View File

@@ -16,9 +16,12 @@ 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.
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`.
The agent can support multiple goals using goal categories using `GOAL_CATEGORIES` in your .env file. If unset, default is all.
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.
```bash
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin
```
See the section Goal-Specific Tool Configuration below for tool configuration for specific goals.

12
todo.md
View File

@@ -1,11 +1,15 @@
# todo list
[ ] 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] maybe make the choose_Agent_goal tag not be system/not always included? <br />
- [x] try taking out list-agents as a tool because agent_prompt_generators may do it for you <br />
- [x] 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] test single-goal <br />
- [x] test claude and grok<br />
- [x] document in sample env and docs how to control <br />
[ ] expand [tests](./tests/agent_goal_workflow_test.py)<br />
[ ] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py) <br />
[x] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py) <br />
[x] test Grok with changes
[ ] 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 />

View File

@@ -21,7 +21,7 @@ starter_prompt_generic = silly_prompt + "Welcome me, give me a description of wh
goal_choose_agent_type = AgentGoal(
id = "goal_choose_agent_type",
category_tag="system",
category_tag="agent_selection",
agent_name="Choose Agent",
agent_friendly_description="Choose the type of agent to assist you today.",
tools=[
@@ -39,7 +39,7 @@ goal_choose_agent_type = AgentGoal(
"agent: Here are the currently available agents.",
"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' }",
"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? (You can respond with name or number.)",
"user: 1, Event Flight Finder",
"user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>",
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
@@ -61,7 +61,6 @@ goal_pirate_treasure = AgentGoal(
tools=[
tool_registry.give_hint_tool,
tool_registry.guess_location_tool,
tool_registry.list_agents_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: "
@@ -106,7 +105,6 @@ goal_match_train_invoice = AgentGoal(
tool_registry.search_trains_tool,
tool_registry.book_trains_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
],
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. "
@@ -153,7 +151,6 @@ goal_event_flight_invoice = AgentGoal(
tool_registry.find_events_tool,
tool_registry.search_flights_tool,
tool_registry.create_invoice_tool,
#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: "
"1. FindEvents: Find an event to travel to "
@@ -193,7 +190,6 @@ goal_hr_schedule_pto = AgentGoal(
tool_registry.current_pto_tool,
tool_registry.future_pto_calc_tool,
tool_registry.book_pto_tool,
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
],
description="The user wants to schedule paid time off (PTO) after today's date. To assist with that goal, help the user gather args for these tools in order: "
"1. CurrentPTO: Tell the user how much PTO they currently have "
@@ -230,7 +226,6 @@ goal_hr_check_pto = AgentGoal(
agent_friendly_description="Check your available PTO.",
tools=[
tool_registry.current_pto_tool,
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
],
description="The user wants to 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 ",
@@ -252,11 +247,10 @@ goal_hr_check_pto = AgentGoal(
goal_hr_check_paycheck_bank_integration_status = AgentGoal(
id = "goal_hr_check_paycheck_bank_integration_status",
category_tag="hr",
agent_name="Check paycheck bank integration status",
agent_friendly_description="Check your integration between paycheck payer and your financial institution.",
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,
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
],
description="The user wants to 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 ",
@@ -283,7 +277,6 @@ goal_fin_check_account_balances = AgentGoal(
tools=[
tool_registry.financial_check_account_is_valid,
tool_registry.financial_get_account_balances,
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
],
description="The user wants to 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"
@@ -318,7 +311,6 @@ goal_fin_move_money = AgentGoal(
tool_registry.financial_check_account_is_valid,
tool_registry.financial_get_account_balances,
tool_registry.financial_move_money,
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
],
description="The user wants to 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"
@@ -333,7 +325,7 @@ goal_fin_move_money = AgentGoal(
"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>", #todo is this needed?
"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 "
@@ -348,7 +340,6 @@ goal_fin_move_money = AgentGoal(
),
)
#todo add money movement, fraud check (update with start)
#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)

View File

@@ -10,6 +10,12 @@ def list_agents(args: dict) -> dict:
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")

View File

@@ -1,12 +1,11 @@
from collections import deque
from datetime import timedelta
import os
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, EnvLookupInput
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
@@ -47,7 +46,8 @@ class AgentGoalWorkflow:
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
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
@@ -125,7 +125,12 @@ class AgentGoalWorkflow:
continue
# If valid, proceed with generating the context and prompt
context_instructions = generate_genai_prompt(self.goal, self.conversation_history, self.tool_data)
context_instructions = generate_genai_prompt(
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)
# connect to LLM and execute to get next steps
@@ -165,17 +170,21 @@ class AgentGoalWorkflow:
else:
self.confirmed = True
# else if the next step is to pick a new goal...
# else if the next step is to pick a new goal, set the goal and tool to do it
elif next_step == "pick-new-goal":
workflow.logger.info("All steps completed. Resetting goal.")
self.change_goal("goal_choose_agent_type")
next_step = tool_data["next"] = "confirm"
current_tool = tool_data["tool"] = "ListAgents"
waiting_for_confirm = True
self.confirmed = True
# 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":
self.add_message("agent", tool_data)
#todo send conversation to AI for analysis
#here we could send conversation to AI for analysis
# end the workflow
return str(self.conversation_history)
@@ -266,12 +275,11 @@ class AgentGoalWorkflow:
)
def change_goal(self, goal: str) -> None:
'''goalsLocal = {
"goal_match_train_invoice": goal_match_train_invoice,
"goal_event_flight_invoice": goal_event_flight_invoice,
"goal_choose_agent_type": goal_choose_agent_type,
}'''
""" 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:
@@ -304,17 +312,21 @@ class AgentGoalWorkflow:
else:
return True
# look up env settings as needed in activities so they're part of history
# 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(env_var_name = "SHOW_CONFIRM", default = True)
self.show_tool_args_confirmation = await workflow.execute_activity(
ToolActivities.get_env_bool,
env_lookup_input = EnvLookupInput(
show_confirm_env_var_name = "SHOW_CONFIRM",
show_confirm_default = True)
env_output:EnvLookupOutput = await workflow.execute_activity(
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)
#