From 4117d5d62d1e5cfa465551fd3554f39c23ea4bfb Mon Sep 17 00:00:00 2001 From: Laine Date: Fri, 7 Mar 2025 16:12:21 -0500 Subject: [PATCH] Add new goal to choose agent type - only kind of working --- api/main.py | 15 ++++-------- shared/config.py | 3 +++ tools/__init__.py | 9 +++++++ tools/change_goal.py | 13 +++++++++++ tools/choose_agent.py | 27 +++++++++++++++++++++ tools/goal_registry.py | 40 ++++++++++++++++++++++++++++++-- tools/tool_registry.py | 38 ++++++++++++++++++++++++++++++ tools/transfer_control.py | 7 ++++++ workflows/agent_goal_workflow.py | 34 +++++++++++++++++++++++---- 9 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 tools/change_goal.py create mode 100644 tools/choose_agent.py create mode 100644 tools/transfer_control.py diff --git a/api/main.py b/api/main.py index 0b453e2..e5c42da 100644 --- a/api/main.py +++ b/api/main.py @@ -6,13 +6,12 @@ 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_match_train_invoice, goal_event_flight_invoice, goal_choose_agent_type from fastapi.middleware.cors import CORSMiddleware -from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE +from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE, AGENT_GOAL app = FastAPI() temporal_client: Optional[Client] = None @@ -26,15 +25,11 @@ def get_agent_goal(): goals = { "goal_match_train_invoice": goal_match_train_invoice, "goal_event_flight_invoice": goal_event_flight_invoice, + "goal_choose_agent_type": goal_choose_agent_type, } -# Agent Goal Configuration -#AGENT_GOAL=goal_event_flight_invoice -#AGENT_GOAL=goal_match_train_invoice - #goal_name = os.getenv("AGENT_GOAL") - goal_name = "goal_event_flight_invoice" - if goal_name is not None: - return goals.get(goal_name) + if AGENT_GOAL is not None: + return goals.get(AGENT_GOAL) else: #if no goal is set in the env file, default to event/flight use case return goals.get("goal_event_flight_invoice", goal_event_flight_invoice) diff --git a/shared/config.py b/shared/config.py index 282e6d2..0775a39 100644 --- a/shared/config.py +++ b/shared/config.py @@ -16,6 +16,9 @@ TEMPORAL_TLS_CERT = os.getenv("TEMPORAL_TLS_CERT", "") TEMPORAL_TLS_KEY = os.getenv("TEMPORAL_TLS_KEY", "") TEMPORAL_API_KEY = os.getenv("TEMPORAL_API_KEY", "") +#Starting agent goal - 1st goal is always to help user pick a next goal +AGENT_GOAL = "goal_choose_agent_type" + async def get_temporal_client() -> Client: """ diff --git a/tools/__init__.py b/tools/__init__.py index 37672c7..f71f072 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -4,6 +4,9 @@ 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 .choose_agent import choose_agent +from .change_goal import change_goal +from .transfer_control import transfer_control def get_handler(tool_name: str): @@ -19,5 +22,11 @@ def get_handler(tool_name: str): return create_invoice if tool_name == "FindEvents": return find_events + if tool_name == "ChooseAgent": + return choose_agent + if tool_name == "ChangeGoal": + return change_goal + if tool_name == "TransferControl": + return transfer_control raise ValueError(f"Unknown tool: {tool_name}") diff --git a/tools/change_goal.py b/tools/change_goal.py new file mode 100644 index 0000000..2458796 --- /dev/null +++ b/tools/change_goal.py @@ -0,0 +1,13 @@ +# can this just call the API endpoint to set the goal, if that changes to allow a param? +# --- OR --- +# end this workflow and start a new one with the new goal +import shared.config + +def change_goal(args: dict) -> dict: + + new_goal = args.get("goalID") + shared.config.AGENT_GOAL = new_goal + + return { + "new_goal": shared.config.AGENT_GOAL, + } \ No newline at end of file diff --git a/tools/choose_agent.py b/tools/choose_agent.py new file mode 100644 index 0000000..d081f20 --- /dev/null +++ b/tools/choose_agent.py @@ -0,0 +1,27 @@ +from pathlib import Path +import json + +def choose_agent(args: dict) -> dict: + + # file_path = Path(__file__).resolve().parent / "goal_regsitry.py" + #if not file_path.exists(): + # return {"error": "Data file not found."} + + agents = [] + agents.append( + { + "agent_name": "Event Flight Helper", + "goal_id": "goal_event_flight_invoice", + "agent_description": "Helps users find interesting events and arrange travel to them", + } + ) + agents.append( + { + "agent_name": "Soccer Train Thing Guy", + "goal_id": "goal_match_train_invoice", + "agent_description": "Something about soccer and trains and stuff", + } + ) + return { + "agents": agents, + } \ No newline at end of file diff --git a/tools/goal_registry.py b/tools/goal_registry.py index db7d1fc..5e85af5 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -6,6 +6,42 @@ from tools.tool_registry import ( book_trains_tool, create_invoice_tool, find_events_tool, + change_goal_tool, + choose_agent_tool, + transfer_control_tool +) + +starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job" + +goal_choose_agent_type = AgentGoal( + tools=[ + choose_agent_tool, + change_goal_tool, + transfer_control_tool + ], + description="The user wants to choose which type of agent they will interact with. " + "Help the user gather args for these tools, in order: " + "1. ChooseAgent: Choose which agent to interact with " + "2. ChangeGoal: Change goal of agent " + "3. TransferControl: Transfer control to new agent " + "After these tools are complete, change your goal to the new goal as chosen by the user. ", + starter_prompt=starter_prompt_generic, + example_conversation_history="\n ".join( + [ + "user: I'd like to choose an agent", + "agent: Sure! Would you like me to list the 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. Which agent would you like to speak to?", + "user: 1", + "user_confirmed_tool_run: ", + # bot changes goal here and hopefully just...switches?? + # could also end 1 workflow and start another with new goal + "tool_result: { 'new_goal': 'goal_event_flight_invoice' }", + "agent: Would you like to transfer control to the new agent now?", + "user: yes", + ] + ), ) goal_match_train_invoice = AgentGoal( @@ -23,7 +59,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", @@ -61,7 +97,7 @@ goal_event_flight_invoice = AgentGoal( "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", diff --git a/tools/tool_registry.py b/tools/tool_registry.py index 7fdc243..cd3ca91 100644 --- a/tools/tool_registry.py +++ b/tools/tool_registry.py @@ -1,5 +1,43 @@ from models.tool_definitions import ToolDefinition, ToolArgument +#This also doesn't help... +transfer_control_tool = ToolDefinition( + name="TransferControl", + description="Do one extra input from user to apply the new goal to the workflow (Hacky, hopefully temp). ", + arguments=[ + ToolArgument( + name="userConfirmation", + type="string", + description="dummy variable to make thing work", + ), + ], +) + + +choose_agent_tool = ToolDefinition( + name="ChooseAgent", + description="List available agents to interact with, pulled from goal_registry. ", + arguments=[ + ToolArgument( + name="userConfirmation", + type="string", + description="dummy variable to make thing work", + ), + ], +) + +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", + ), + ], +) + search_flights_tool = ToolDefinition( name="SearchFlights", description="Search for return flights from an origin to a destination within a date range (dateDepart, dateReturn).", diff --git a/tools/transfer_control.py b/tools/transfer_control.py new file mode 100644 index 0000000..127cc38 --- /dev/null +++ b/tools/transfer_control.py @@ -0,0 +1,7 @@ +import shared.config + +def transfer_control(args: dict) -> dict: + + return { + "new_goal": shared.config.AGENT_GOAL, + } \ No newline at end of file diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 1085da4..0e47b8a 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -1,5 +1,6 @@ from collections import deque from datetime import timedelta +import importlib from typing import Dict, Any, Union, List, Optional, Deque, TypedDict from temporalio.common import RetryPolicy @@ -10,6 +11,9 @@ from workflows.workflow_helpers import LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT, \ LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT from workflows import workflow_helpers as helpers + +#importlib.reload(my_module) + with workflow.unsafe.imports_passed_through(): from activities.tool_activities import ToolActivities from prompts.agent_prompt_generators import ( @@ -19,6 +23,10 @@ with workflow.unsafe.imports_passed_through(): CombinedInput, ToolPromptInput, ) + import shared.config + importlib.reload(shared.config) + #from shared.config import AGENT_GOAL + from tools.goal_registry import goal_match_train_invoice, goal_event_flight_invoice, goal_choose_agent_type # Constants MAX_TURNS_BEFORE_CONTINUE = 250 @@ -46,11 +54,11 @@ class AgentGoalWorkflow: @workflow.run async def run(self, combined_input: CombinedInput) -> str: """Main workflow execution method.""" - # setup phase + # setup phase, starts with blank tool_params and agent_goal prompt as defined in tools/goal_registry.py params = combined_input.tool_params agent_goal = combined_input.agent_goal - # set sample conversation to start + # 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 @@ -63,12 +71,25 @@ class AgentGoalWorkflow: # interactive loop while True: - # wait for signals + # wait for 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 ) + #update the goal, in case it's changed - doesn't help + goals = { + "goal_match_train_invoice": goal_match_train_invoice, + "goal_event_flight_invoice": goal_event_flight_invoice, + "goal_choose_agent_type": goal_choose_agent_type, + } + + if shared.config.AGENT_GOAL is not None: + agent_goal = goals.get(shared.config.AGENT_GOAL) + workflow.logger.warning("AGENT_GOAL: " + shared.config.AGENT_GOAL) + # workflow.logger.warning("agent_goal", agent_goal) + #process signals of various kinds + #chat-end signal if self.chat_ended: workflow.logger.info("Chat ended.") @@ -83,6 +104,7 @@ class AgentGoalWorkflow: 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, @@ -113,6 +135,7 @@ class AgentGoalWorkflow: ), ) + #If validation fails, provide that feedback to the user - i.e., "your words make no sense, human" if not validation_result.validationResult: workflow.logger.warning( f"Prompt validation failed: {validation_result.validationFailedReason}" @@ -132,7 +155,7 @@ class AgentGoalWorkflow: context_instructions=context_instructions, ) - # connect to LLM and get which tool to run + # connect to LLM and get...its feedback? which tool to run? ?? tool_data = await workflow.execute_activity( ToolActivities.agent_toolPlanner, prompt_input, @@ -171,6 +194,7 @@ class AgentGoalWorkflow: 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.""" @@ -179,12 +203,14 @@ class AgentGoalWorkflow: 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 + #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."""