From c18a40b5024df787c46f97f2098eb65b65fd6c49 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Tue, 8 Apr 2025 15:01:11 -0400 Subject: [PATCH] - 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 --- .env.example | 7 ++-- activities/tool_activities.py | 33 ++++++++++----- adding-goals-and-tools.md | 3 +- models/data_types.py | 9 ++++- prompts/agent_prompt_generators.py | 65 ++++++++++++++++++++++++++++-- scripts/run_worker.py | 2 +- setup.md | 7 +++- todo.md | 12 ++++-- tools/goal_registry.py | 21 +++------- tools/list_agents.py | 6 +++ workflows/agent_goal_workflow.py | 44 ++++++++++++-------- 11 files changed, 150 insertions(+), 59 deletions(-) diff --git a/.env.example b/.env.example index 5140da8..ef6bf09 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/activities/tool_activities.py b/activities/tool_activities.py index e546414..9d466bc 100644 --- a/activities/tool_activities.py +++ b/activities/tool_activities.py @@ -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(): diff --git a/adding-goals-and-tools.md b/adding-goals-and-tools.md index 67a2171..910b39d 100644 --- a/adding-goals-and-tools.md +++ b/adding-goals-and-tools.md @@ -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.
+ 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. diff --git a/models/data_types.py b/models/data_types.py index b7ee350..9b7d67d 100644 --- a/models/data_types.py +++ b/models/data_types.py @@ -45,5 +45,10 @@ class ValidationResult: @dataclass class EnvLookupInput: - env_var_name: str - default: bool \ No newline at end of file + show_confirm_env_var_name: str + show_confirm_default: bool + +@dataclass +class EnvLookupOutput: + show_confirm: bool + multi_goal_mode: bool \ No newline at end of file diff --git a/prompts/agent_prompt_generators.py b/prompts/agent_prompt_generators.py index 6bae621..71c9d67 100644 --- a/prompts/agent_prompt_generators.py +++ b/prompts/agent_prompt_generators.py @@ -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=''." \ No newline at end of file diff --git a/scripts/run_worker.py b/scripts/run_worker.py index 60aafbf..c0d2a81 100644 --- a/scripts/run_worker.py +++ b/scripts/run_worker.py @@ -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, diff --git a/setup.md b/setup.md index 4f6029a..a31df80 100644 --- a/setup.md +++ b/setup.md @@ -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. diff --git a/todo.md b/todo.md index a1bf30a..f06d42b 100644 --- a/todo.md +++ b/todo.md @@ -1,11 +1,15 @@ # todo list [ ] goal change management tweaks
- - [ ] maybe make the choose_Agent_goal tag not be system/not always included?
- - [ ] try taking out list-agents as a tool because agent_prompt_generators may do it for you
- - [ ] 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
+ - [x] maybe make the choose_Agent_goal tag not be system/not always included?
+ - [x] try taking out list-agents as a tool because agent_prompt_generators may do it for you
+ - [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
+ - [x] test single-goal
+ - [x] test claude and grok
+ - [x] document in sample env and docs how to control
[ ] expand [tests](./tests/agent_goal_workflow_test.py)
-[ ] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py)
+[x] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py)
+[x] test Grok with changes [ ] adding fintech goals
- 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.
diff --git a/tools/goal_registry.py b/tools/goal_registry.py index 7042919..19f62fb 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -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=[ @@ -33,13 +33,13 @@ goal_choose_agent_type = AgentGoal( "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=starter_prompt_generic + "Begin by listing all details of all agents as provided by the output of the first tool included in this goal. ", + starter_prompt=starter_prompt_generic + " Begin by listing all details of all agents as provided by the output of the first tool included in this goal. ", example_conversation_history="\n ".join( [ "agent: Here are the currently available agents.", "user_confirmed_tool_run: ", "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: ", "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: ", "tool_result: { 'status': account valid }", "agent: Great! Here are your account balances:", - "user_confirmed_tool_run: ", #todo is this needed? + "user_confirmed_tool_run: ", "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) diff --git a/tools/list_agents.py b/tools/list_agents.py index ba9aa20..d64fb20 100644 --- a/tools/list_agents.py +++ b/tools/list_agents.py @@ -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") diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 660e3b5..083332b 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -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) #