From d09db9f11f079cb8494af4795bcf9e45fd0ea6b4 Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 5 Mar 2025 17:24:18 -0500 Subject: [PATCH 01/29] Move where goal is set, make dummy data default for create_invoice --- api/main.py | 13 ++++++-- tools/create_invoice.py | 72 +++++++++++++++++++++++------------------ tools/goal_registry.py | 1 - 3 files changed, 51 insertions(+), 35 deletions(-) diff --git a/api/main.py b/api/main.py index b381bb6..0b453e2 100644 --- a/api/main.py +++ b/api/main.py @@ -23,12 +23,21 @@ load_dotenv() def get_agent_goal(): """Get the agent goal from environment variables.""" - goal_name = os.getenv("AGENT_GOAL", "goal_match_train_invoice") goals = { "goal_match_train_invoice": goal_match_train_invoice, "goal_event_flight_invoice": goal_event_flight_invoice, } - return goals.get(goal_name, goal_event_flight_invoice) +# 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) + 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) @app.on_event("startup") diff --git a/tools/create_invoice.py b/tools/create_invoice.py index 4771a53..9fcd1aa 100644 --- a/tools/create_invoice.py +++ b/tools/create_invoice.py @@ -4,7 +4,7 @@ from dotenv import load_dotenv load_dotenv(override=True) # Load environment variables from a .env file -stripe.api_key = os.getenv("STRIPE_API_KEY", "YOUR_DEFAULT_KEY") +stripe.api_key = os.getenv("STRIPE_API_KEY") def ensure_customer_exists( @@ -26,41 +26,49 @@ def ensure_customer_exists( def create_invoice(args: dict) -> dict: """Create and finalize a Stripe invoice.""" - # Find or create customer - customer_id = ensure_customer_exists( - args.get("customer_id"), args.get("email", "default@example.com") - ) + # If an API key exists in the env file, find or create customer + if stripe.api_key is not None: + customer_id = ensure_customer_exists( + args.get("customer_id"), args.get("email", "default@example.com") + ) - # Get amount and convert to cents - amount = args.get("amount", 200.00) # Default to $200.00 - try: - amount_cents = int(float(amount) * 100) - except (TypeError, ValueError): - return {"error": "Invalid amount provided. Please confirm the amount."} + # Get amount and convert to cents + amount = args.get("amount", 200.00) # Default to $200.00 + try: + amount_cents = int(float(amount) * 100) + except (TypeError, ValueError): + return {"error": "Invalid amount provided. Please confirm the amount."} - # Create an invoice item - stripe.InvoiceItem.create( - customer=customer_id, - amount=amount_cents, - currency="gbp", - description=args.get("tripDetails", "Service Invoice"), - ) + # Create an invoice item + stripe.InvoiceItem.create( + customer=customer_id, + amount=amount_cents, + currency="gbp", + description=args.get("tripDetails", "Service Invoice"), + ) - # Create and finalize the invoice - invoice = stripe.Invoice.create( - customer=customer_id, - collection_method="send_invoice", # Invoice is sent to the customer - days_until_due=args.get("days_until_due", 7), # Default due date: 7 days - pending_invoice_items_behavior="include", # No pending invoice items - ) - finalized_invoice = stripe.Invoice.finalize_invoice(invoice.id) - - return { - "invoiceStatus": finalized_invoice.status, - "invoiceURL": finalized_invoice.hosted_invoice_url, - "reference": finalized_invoice.number, - } + # Create and finalize the invoice + invoice = stripe.Invoice.create( + customer=customer_id, + collection_method="send_invoice", # Invoice is sent to the customer + days_until_due=args.get("days_until_due", 7), # Default due date: 7 days + pending_invoice_items_behavior="include", # No pending invoice items + ) + finalized_invoice = stripe.Invoice.finalize_invoice(invoice.id) + return { + "invoiceStatus": finalized_invoice.status, + "invoiceURL": finalized_invoice.hosted_invoice_url, + "reference": finalized_invoice.number, + } + # if no API key is in the env file, return dummy info + else: + print("[CreateInvoice] Creating invoice with:", args) + return { + "invoiceStatus": "generated", + "invoiceURL": "https://pay.example.com/invoice/12345", + "reference": "INV-12345", + } def create_invoice_example(args: dict) -> dict: """ diff --git a/tools/goal_registry.py b/tools/goal_registry.py index a903d68..db7d1fc 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -51,7 +51,6 @@ goal_match_train_invoice = AgentGoal( ), ) -# unused goal_event_flight_invoice = AgentGoal( tools=[ find_events_tool, From 4c933b505283e57798aab25fb37c0b64ab774322 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Fri, 7 Mar 2025 09:46:22 -0500 Subject: [PATCH 02/29] making plans --- todo.md | 22 ++++++++++++++++++++++ workflows/agent_goal_workflow.py | 10 +++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 todo.md diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..2d79665 --- /dev/null +++ b/todo.md @@ -0,0 +1,22 @@ +# todo list +[ ] create people management scenario
+ -- check pay status + -- book work travel + -- check PTO levels + -- check insurance coverages + -- book PTO around a date (https://developers.google.com/calendar/api/guides/overview)? + -- scenario should use multiple tools + -- expense management + -- check in on the health of the team +[ ] demo the reasons why: + -- Orchestrate interactions across distributed data stores and tools + -- Hold state, potentially over long periods of time + -- Ability to ‘self-heal’ and retry until the (probabilistic) LLM returns valid data + -- Support for human intervention such as approvals + -- Parallel processing for efficiency of data retrieval and tool use + -- Insight into the agent’s performance + +[ ] customize prompts in [workflow to manage scenario](./workflows/tool_workflow.py)
+[ ] add in new tools?
+[ ] create tests
+[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" \ No newline at end of file diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index fdc8fc5..9e17537 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -42,9 +42,11 @@ class AgentGoalWorkflow: self.confirm: bool = False self.tool_results: List[Dict[str, Any]] = [] + # see ../api/main.py#temporal_client.start_workflow() for how these parameters are set @workflow.run async def run(self, combined_input: CombinedInput) -> str: """Main workflow execution method.""" + # setup phase params = combined_input.tool_params agent_goal = combined_input.agent_goal @@ -55,18 +57,23 @@ class AgentGoalWorkflow: if params and params.prompt_queue: self.prompt_queue.extend(params.prompt_queue) - waiting_for_confirm = False + waiting_for_confirm = False # controls if we confirm with the user current_tool = None + # interactive loop while True: + # wait for signals await workflow.wait_condition( lambda: bool(self.prompt_queue) or self.chat_ended or self.confirm ) + #process signals of various kinds + #chat-end signal if self.chat_ended: workflow.logger.info("Chat ended.") return f"{self.conversation_history}" + # tool execution if selected and confirmed if self.confirm and waiting_for_confirm and current_tool and self.tool_data: self.confirm = False waiting_for_confirm = False @@ -135,6 +142,7 @@ class AgentGoalWorkflow: ) self.tool_data = tool_data + # move forward in the tool chain next_step = tool_data.get("next") current_tool = tool_data.get("tool") From 64d2a92630118dcd5c411906c5ac565dabf11d2f Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Fri, 7 Mar 2025 09:58:25 -0500 Subject: [PATCH 03/29] more understanding --- workflows/agent_goal_workflow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 9e17537..1085da4 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -50,6 +50,7 @@ class AgentGoalWorkflow: params = combined_input.tool_params agent_goal = combined_input.agent_goal + # set sample conversation to start if params and params.conversation_summary: self.add_message("conversation_summary", params.conversation_summary) self.conversation_summary = params.conversation_summary @@ -131,6 +132,7 @@ class AgentGoalWorkflow: context_instructions=context_instructions, ) + # connect to LLM and get which tool to run tool_data = await workflow.execute_activity( ToolActivities.agent_toolPlanner, prompt_input, From 4117d5d62d1e5cfa465551fd3554f39c23ea4bfb Mon Sep 17 00:00:00 2001 From: Laine Date: Fri, 7 Mar 2025 16:12:21 -0500 Subject: [PATCH 04/29] 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.""" From 8fafe4b09036a18155f5b73ae5fe1cba80b96f4f Mon Sep 17 00:00:00 2001 From: Laine Date: Tue, 11 Mar 2025 09:07:25 -0400 Subject: [PATCH 05/29] Change agent goal to be an element of the workflow, including query --- api/main.py | 36 +++++++++++++++++++++++++------- shared/config.py | 1 + tools/change_goal.py | 9 +++++++- workflows/agent_goal_workflow.py | 35 ++++++++++++++++++++----------- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/api/main.py b/api/main.py index e5c42da..d82826d 100644 --- a/api/main.py +++ b/api/main.py @@ -20,7 +20,7 @@ temporal_client: Optional[Client] = None load_dotenv() -def get_agent_goal(): +def get_initial_agent_goal(): """Get the agent goal from environment variables.""" goals = { "goal_match_train_invoice": goal_match_train_invoice, @@ -121,6 +121,27 @@ async def get_conversation_history(): raise HTTPException( status_code=500, detail="Internal server error while querying workflow." ) + +@app.get("/agent-goal") +async def get_agent_goal(): + """Calls the workflow's 'get_agent_goal' query.""" + try: + # Get workflow handle + handle = temporal_client.get_workflow_handle("agent-workflow") + + # Check if the workflow is completed + workflow_status = await handle.describe() + if workflow_status.status == 2: + # Workflow is completed; return an empty response + return {} + + # Query the workflow + agent_goal = await handle.query("get_agent_goal") + return agent_goal + except TemporalError as e: + # Workflow not found; return an empty response + print(e) + return {} @app.post("/send-prompt") @@ -128,7 +149,8 @@ async def send_prompt(prompt: str): # Create combined input with goal from environment combined_input = CombinedInput( tool_params=AgentGoalWorkflowParams(None, None), - agent_goal=get_agent_goal(), + agent_goal=get_initial_agent_goal(), + #change to get from workflow query ) workflow_id = "agent-workflow" @@ -172,13 +194,13 @@ async def end_chat(): @app.post("/start-workflow") async def start_workflow(): - # Get the configured goal - agent_goal = get_agent_goal() + # Get the initial goal as set in shared/config or env or just...always should be "pick a goal?" + initial_agent_goal = get_initial_agent_goal() # Create combined input combined_input = CombinedInput( tool_params=AgentGoalWorkflowParams(None, None), - agent_goal=agent_goal, + agent_goal=initial_agent_goal, ) workflow_id = "agent-workflow" @@ -190,9 +212,9 @@ async def start_workflow(): id=workflow_id, task_queue=TEMPORAL_TASK_QUEUE, start_signal="user_prompt", - start_signal_args=["### " + agent_goal.starter_prompt], + start_signal_args=["### " + initial_agent_goal.starter_prompt], ) return { - "message": f"Workflow started with goal's starter prompt: {agent_goal.starter_prompt}." + "message": f"Workflow started with goal's starter prompt: {initial_agent_goal.starter_prompt}." } diff --git a/shared/config.py b/shared/config.py index 0775a39..cb4b9da 100644 --- a/shared/config.py +++ b/shared/config.py @@ -18,6 +18,7 @@ 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" +#AGENT_GOAL = "goal_event_flight_invoice" async def get_temporal_client() -> Client: diff --git a/tools/change_goal.py b/tools/change_goal.py index 2458796..983e9c7 100644 --- a/tools/change_goal.py +++ b/tools/change_goal.py @@ -1,6 +1,13 @@ -# can this just call the API endpoint to set the goal, if that changes to allow a param? +# can this just call the API endpoint to set the goal, if that changes to allow a param? +# if this functions, it could work to both send a signal and also circumvent the UI -> API thing. Maybe? + # --- OR --- + # end this workflow and start a new one with the new goal + +# --- OR --- + +# send a signal to the workflow from here? import shared.config def change_goal(args: dict) -> dict: diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 0e47b8a..61479a3 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -7,6 +7,7 @@ from temporalio.common import RetryPolicy from temporalio import workflow from models.data_types import ConversationHistory, NextStep, ValidationInput +from models.tool_definitions import AgentGoal from workflows.workflow_helpers import LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT, \ LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT from workflows import workflow_helpers as helpers @@ -49,6 +50,8 @@ class AgentGoalWorkflow: self.tool_data: Optional[ToolData] = None self.confirm: bool = False self.tool_results: List[Dict[str, Any]] = [] + #set initial goal of "pick an agent" here?? + self.goal: AgentGoal = {"tools": []} # see ../api/main.py#temporal_client.start_workflow() for how these parameters are set @workflow.run @@ -56,7 +59,7 @@ class AgentGoalWorkflow: """Main workflow execution method.""" # setup phase, starts with blank tool_params and agent_goal prompt as defined in tools/goal_registry.py params = combined_input.tool_params - agent_goal = combined_input.agent_goal + self.goal = combined_input.agent_goal # add message from sample conversation provided in tools/goal_registry.py, if it exists if params and params.conversation_summary: @@ -77,15 +80,15 @@ class AgentGoalWorkflow: ) #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, - } + #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) + #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 @@ -112,6 +115,9 @@ class AgentGoalWorkflow: self.add_message, self.prompt_queue ) + # workflow.logger.warning("last tool_data tool: ", self.tool_data[-1].tool) + #workflow.logger.warning("last tool_data args: ", self.tool_data[-1].args) + # workflow.logger.warning("last tool_results [args]: ", self.tool_results[-1]["args"]) continue if self.prompt_queue: @@ -123,7 +129,7 @@ class AgentGoalWorkflow: validation_input = ValidationInput( prompt=prompt, conversation_history=self.conversation_history, - agent_goal=agent_goal, + agent_goal=self.goal, ) validation_result = await workflow.execute_activity( ToolActivities.agent_validatePrompt, @@ -147,7 +153,7 @@ class AgentGoalWorkflow: # Proceed with generating the context and prompt context_instructions = generate_genai_prompt( - agent_goal, self.conversation_history, self.tool_data + self.goal, self.conversation_history, self.tool_data ) prompt_input = ToolPromptInput( @@ -189,7 +195,7 @@ class AgentGoalWorkflow: await helpers.continue_as_new_if_needed( self.conversation_history, self.prompt_queue, - agent_goal, + self.goal, MAX_TURNS_BEFORE_CONTINUE, self.add_message ) @@ -220,6 +226,11 @@ class AgentGoalWorkflow: def get_conversation_history(self) -> ConversationHistory: """Query handler to retrieve the full conversation history.""" return self.conversation_history + + @workflow.query + def get_agent_goal(self) -> AgentGoal: + """Query handler to retrieve the current goal of the agent.""" + return self.goal @workflow.query def get_summary_from_history(self) -> Optional[str]: From 6939e3f94223b8f365db704cfa4414438b876f7b Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Tue, 11 Mar 2025 10:03:45 -0400 Subject: [PATCH 06/29] log less chatgpt stuff and actually change the goal --- activities/tool_activities.py | 3 ++- workflows/agent_goal_workflow.py | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/activities/tool_activities.py b/activities/tool_activities.py index e833b44..75e14e0 100644 --- a/activities/tool_activities.py +++ b/activities/tool_activities.py @@ -237,7 +237,7 @@ class ToolActivities: ) response_content = chat_completion.choices[0].message.content - print(f"ChatGPT response: {response_content}") + activity.logger.info(f"ChatGPT response: {response_content}") # Use the new sanitize function response_content = self.sanitize_json_response(response_content) @@ -449,6 +449,7 @@ def dynamic_tool_activity(args: Sequence[RawValue]) -> dict: # Delegate to the relevant function handler = get_handler(tool_name) result = handler(tool_args) + print(f"in dynamic tool activity, result: {result}") # Optionally log or augment the result activity.logger.info(f"Tool '{tool_name}' result: {result}") diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 61479a3..f66551a 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -115,9 +115,24 @@ class AgentGoalWorkflow: self.add_message, self.prompt_queue ) - # workflow.logger.warning("last tool_data tool: ", self.tool_data[-1].tool) - #workflow.logger.warning("last tool_data args: ", self.tool_data[-1].args) - # workflow.logger.warning("last tool_results [args]: ", self.tool_results[-1]["args"]) + if len(self.tool_results) > 0: + #workflow.logger.warning("last tool_results keys: ", self.tool_results[-1].keys()) + workflow.logger.warning(f"last tool_results keys: {self.tool_results[-1].keys()}") + workflow.logger.warning(f"last tool_results values:{ self.tool_results[-1].values()}") + if "new_goal" in self.tool_results[-1].keys() and "ChangeGoal" in self.tool_results[-1].values(): + new_goal = self.tool_results[-1].get("new_goal") + workflow.logger.warning(f"Booya new goal!: {new_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, + } + + if new_goal is not None: + self.goal = goals.get(new_goal) + #todo reset goal or tools if this doesn't work or whatever + else: + workflow.logger.warning("no tool results yet") continue if self.prompt_queue: From 64ffe7f635324fdda0768eb13505a1f665561588 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Tue, 11 Mar 2025 10:32:28 -0400 Subject: [PATCH 07/29] clean up logging and comments --- workflows/agent_goal_workflow.py | 43 +++++++++++--------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index f66551a..40890dc 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -79,18 +79,6 @@ class AgentGoalWorkflow: 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 @@ -115,24 +103,21 @@ class AgentGoalWorkflow: self.add_message, self.prompt_queue ) - if len(self.tool_results) > 0: - #workflow.logger.warning("last tool_results keys: ", self.tool_results[-1].keys()) - workflow.logger.warning(f"last tool_results keys: {self.tool_results[-1].keys()}") - workflow.logger.warning(f"last tool_results values:{ self.tool_results[-1].values()}") - if "new_goal" in self.tool_results[-1].keys() and "ChangeGoal" in self.tool_results[-1].values(): - new_goal = self.tool_results[-1].get("new_goal") - workflow.logger.warning(f"Booya new goal!: {new_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, - } - if new_goal is not None: - self.goal = goals.get(new_goal) - #todo reset goal or tools if this doesn't work or whatever - else: - workflow.logger.warning("no tool results yet") + #set new goal if we should + if len(self.tool_results) > 0 and "new_goal" in self.tool_results[-1].keys() and "ChangeGoal" in self.tool_results[-1].values(): + + new_goal = self.tool_results[-1].get("new_goal") + workflow.logger.info(f"Booya new goal!: {new_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, + } + + if new_goal is not None: + self.goal = goals.get(new_goal) + #todo reset goal or tools if this doesn't work or whatever continue if self.prompt_queue: From 804568e3669622e788b33b7fea23c53381d2892d Mon Sep 17 00:00:00 2001 From: Laine Date: Tue, 11 Mar 2025 10:41:22 -0400 Subject: [PATCH 08/29] Rename ChooseAgent tool to ListAgents --- tools/__init__.py | 6 +++--- tools/change_goal.py | 3 +-- tools/goal_registry.py | 17 +++++------------ tools/{choose_agent.py => list_agents.py} | 2 +- tools/tool_registry.py | 18 ++---------------- 5 files changed, 12 insertions(+), 34 deletions(-) rename tools/{choose_agent.py => list_agents.py} (95%) diff --git a/tools/__init__.py b/tools/__init__.py index f71f072..1ca7da2 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -4,7 +4,7 @@ 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 .list_agents import list_agents from .change_goal import change_goal from .transfer_control import transfer_control @@ -22,8 +22,8 @@ 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 == "ListAgents": + return list_agents if tool_name == "ChangeGoal": return change_goal if tool_name == "TransferControl": diff --git a/tools/change_goal.py b/tools/change_goal.py index 983e9c7..7588ae2 100644 --- a/tools/change_goal.py +++ b/tools/change_goal.py @@ -13,8 +13,7 @@ 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, + "new_goal": new_goal, } \ No newline at end of file diff --git a/tools/goal_registry.py b/tools/goal_registry.py index 5e85af5..01d1ea9 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -7,39 +7,32 @@ from tools.tool_registry import ( create_invoice_tool, find_events_tool, change_goal_tool, - choose_agent_tool, - transfer_control_tool + list_agents_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 + list_agents_tool, + change_goal_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 " + "1. ListAgents: List agents available 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: ", + "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", ] ), ) diff --git a/tools/choose_agent.py b/tools/list_agents.py similarity index 95% rename from tools/choose_agent.py rename to tools/list_agents.py index d081f20..62f3010 100644 --- a/tools/choose_agent.py +++ b/tools/list_agents.py @@ -1,7 +1,7 @@ from pathlib import Path import json -def choose_agent(args: dict) -> dict: +def list_agents(args: dict) -> dict: # file_path = Path(__file__).resolve().parent / "goal_regsitry.py" #if not file_path.exists(): diff --git a/tools/tool_registry.py b/tools/tool_registry.py index cd3ca91..7a08b3b 100644 --- a/tools/tool_registry.py +++ b/tools/tool_registry.py @@ -1,21 +1,7 @@ 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", +list_agents_tool = ToolDefinition( + name="ListAgents", description="List available agents to interact with, pulled from goal_registry. ", arguments=[ ToolArgument( From f13ed70bfe2fdaf66336430f42f23ee4aa0bae3b Mon Sep 17 00:00:00 2001 From: Laine Date: Tue, 11 Mar 2025 12:02:26 -0400 Subject: [PATCH 09/29] Change instructions to AI to handle switching back to ListAgents when done with tool chain --- api/main.py | 1 - prompts/agent_prompt_generators.py | 7 ++----- workflows/agent_goal_workflow.py | 32 +++++++++++++----------------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/api/main.py b/api/main.py index d82826d..1760458 100644 --- a/api/main.py +++ b/api/main.py @@ -194,7 +194,6 @@ async def end_chat(): @app.post("/start-workflow") async def start_workflow(): - # Get the initial goal as set in shared/config or env or just...always should be "pick a goal?" initial_agent_goal = get_initial_agent_goal() # Create combined input diff --git a/prompts/agent_prompt_generators.py b/prompts/agent_prompt_generators.py index 205205a..d5e301f 100644 --- a/prompts/agent_prompt_generators.py +++ b/prompts/agent_prompt_generators.py @@ -81,9 +81,8 @@ def generate_genai_prompt( "1) If any required argument is missing, set next='question' and ask the user.\n" "2) If all required arguments are known, set next='confirm' and specify the tool.\n" " The user will confirm before the tool is run.\n" - "3) If no more tools are needed (user_confirmed_tool_run has been run for all), set next='done' and tool=null.\n" + "3) If no more tools are needed (user_confirmed_tool_run has been run for all), set next='confirm' and tool='ListAgents'.\n" "4) response should be short and user-friendly.\n" - "5) Don't set next='done' until the final tool has returned user_confirmed_tool_run.\n" ) # Validation Task (If raw_json is provided) @@ -126,9 +125,7 @@ def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) -> "You will need to use the tool_results to auto-fill arguments for subsequent tools and also to figure out if all tools have been run." '{"next": "", "tool": "", "args": {"": "", "": "}, "response": ""}' "ONLY return those json keys (next, tool, args, response), nothing else." - 'Next should only be "done" if all tools have been run (use the system prompt to figure that out).' - 'Next should be "question" if the tool is not the last one in the sequence.' - 'Next should NOT be "confirm" at this point.' + 'Next should be "question".' ) def generate_missing_args_prompt(current_tool: str, tool_data: dict, missing_args: list[str]) -> str: diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 40890dc..c6c05ae 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -12,9 +12,6 @@ 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 ( @@ -24,9 +21,6 @@ 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 @@ -50,7 +44,6 @@ class AgentGoalWorkflow: self.tool_data: Optional[ToolData] = None self.confirm: bool = False self.tool_results: List[Dict[str, Any]] = [] - #set initial goal of "pick an agent" here?? self.goal: AgentGoal = {"tools": []} # see ../api/main.py#temporal_client.start_workflow() for how these parameters are set @@ -108,16 +101,8 @@ class AgentGoalWorkflow: if len(self.tool_results) > 0 and "new_goal" in self.tool_results[-1].keys() and "ChangeGoal" in self.tool_results[-1].values(): new_goal = self.tool_results[-1].get("new_goal") - workflow.logger.info(f"Booya new goal!: {new_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, - } - - if new_goal is not None: - self.goal = goals.get(new_goal) - #todo reset goal or tools if this doesn't work or whatever + workflow.logger.warning(f"Booya new goal!: {new_goal}") + self.change_goal(new_goal) continue if self.prompt_queue: @@ -161,7 +146,6 @@ class AgentGoalWorkflow: context_instructions=context_instructions, ) - # connect to LLM and get...its feedback? which tool to run? ?? tool_data = await workflow.execute_activity( ToolActivities.agent_toolPlanner, prompt_input, @@ -259,3 +243,15 @@ class AgentGoalWorkflow: self.conversation_history["messages"].append( {"actor": actor, "response": response} ) + + def change_goal(self, goal: str) -> None: + 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 goal is not None: + self.goal = goals.get(goal) + workflow.logger.warning("Changed goal to " + goal) + #todo reset goal or tools if this doesn't work or whatever From b2e4999562e787590fb69750046d5cd73e6bd692 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Tue, 11 Mar 2025 13:02:08 -0400 Subject: [PATCH 10/29] adding and clarifying comments --- workflows/agent_goal_workflow.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 40890dc..193e6c9 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -53,7 +53,7 @@ class AgentGoalWorkflow: #set initial goal of "pick an agent" here?? self.goal: AgentGoal = {"tools": []} - # see ../api/main.py#temporal_client.start_workflow() for how these parameters are set + # see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set @workflow.run async def run(self, combined_input: CombinedInput) -> str: """Main workflow execution method.""" @@ -69,24 +69,27 @@ class AgentGoalWorkflow: if params and params.prompt_queue: self.prompt_queue.extend(params.prompt_queue) - waiting_for_confirm = False # controls if we confirm with the user + waiting_for_confirm = False current_tool = None - # interactive loop + # This is the main interactive loop. Main responsibilities: + # - Selecting and changing goals as directed by the user + # - reacting to user input (from signals) + # - calling activities to determine next steps and prompts + # - executing the selected tools while True: - # wait for 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( lambda: bool(self.prompt_queue) or self.chat_ended or self.confirm ) - #process signals of various kinds - - #chat-end signal + # handle chat-end signal if self.chat_ended: workflow.logger.info("Chat ended.") + return f"{self.conversation_history}" - # tool execution if selected and confirmed + # execute tool if self.confirm and waiting_for_confirm and current_tool and self.tool_data: self.confirm = False waiting_for_confirm = False @@ -120,6 +123,7 @@ class AgentGoalWorkflow: #todo reset goal or tools if this doesn't work or whatever continue + # push messages to UI if there are any if self.prompt_queue: prompt = self.prompt_queue.popleft() if not prompt.startswith("###"): @@ -161,7 +165,7 @@ class AgentGoalWorkflow: context_instructions=context_instructions, ) - # connect to LLM and get...its feedback? which tool to run? ?? + # connect to LLM and get it to create a prompt for the user about the tool tool_data = await workflow.execute_activity( ToolActivities.agent_toolPlanner, prompt_input, @@ -186,6 +190,7 @@ class AgentGoalWorkflow: self.confirm = False workflow.logger.info("Waiting for user confirm signal...") + # todo probably here we can set the next step to be change-goal elif next_step == "done": workflow.logger.info("All steps completed. Exiting workflow.") self.add_message("agent", tool_data) From ae334a2caec2c244957022db1d56e8aee54d7e11 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Tue, 11 Mar 2025 14:01:23 -0400 Subject: [PATCH 11/29] adding grok to .env.example and updating todo --- .env.example | 3 + frontend/package-lock.json | 201 ++++++++++++++++--------------------- todo.md | 4 +- 3 files changed, 95 insertions(+), 113 deletions(-) diff --git a/.env.example b/.env.example index 9bf05b9..a1a1944 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,9 @@ STRIPE_API_KEY=sk_test_51J... LLM_PROVIDER=openai # default OPENAI_API_KEY=sk-proj-... # or +#LLM_PROVIDER=grok +#GROK_API_KEY=xai-your-grok-api-key +# or # LLM_PROVIDER=ollama # OLLAMA_MODEL_NAME=qwen2.5:14b # or diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 56a695a..5bc88a3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -825,247 +825,228 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", - "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz", + "integrity": "sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", - "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz", + "integrity": "sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", - "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz", + "integrity": "sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", - "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz", + "integrity": "sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", - "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz", + "integrity": "sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", - "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz", + "integrity": "sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", - "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz", + "integrity": "sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", - "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz", + "integrity": "sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", - "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz", + "integrity": "sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", - "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz", + "integrity": "sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", - "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz", + "integrity": "sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==", "cpu": [ "loong64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", - "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz", + "integrity": "sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==", "cpu": [ "ppc64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", - "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz", + "integrity": "sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==", "cpu": [ "riscv64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", - "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz", + "integrity": "sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==", "cpu": [ "s390x" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", - "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz", + "integrity": "sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", - "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz", + "integrity": "sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", - "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz", + "integrity": "sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", - "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz", + "integrity": "sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==", "cpu": [ "ia32" ], - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", - "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz", + "integrity": "sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -1115,8 +1096,7 @@ "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "license": "MIT" + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@vitejs/plugin-react": { "version": "4.3.4", @@ -2084,9 +2064,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", "funding": [ { "type": "opencollective", @@ -2101,9 +2081,8 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2339,10 +2318,9 @@ } }, "node_modules/rollup": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", - "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", - "license": "MIT", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.7.tgz", + "integrity": "sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==", "dependencies": { "@types/estree": "1.0.6" }, @@ -2354,25 +2332,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.29.1", - "@rollup/rollup-android-arm64": "4.29.1", - "@rollup/rollup-darwin-arm64": "4.29.1", - "@rollup/rollup-darwin-x64": "4.29.1", - "@rollup/rollup-freebsd-arm64": "4.29.1", - "@rollup/rollup-freebsd-x64": "4.29.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", - "@rollup/rollup-linux-arm-musleabihf": "4.29.1", - "@rollup/rollup-linux-arm64-gnu": "4.29.1", - "@rollup/rollup-linux-arm64-musl": "4.29.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", - "@rollup/rollup-linux-riscv64-gnu": "4.29.1", - "@rollup/rollup-linux-s390x-gnu": "4.29.1", - "@rollup/rollup-linux-x64-gnu": "4.29.1", - "@rollup/rollup-linux-x64-musl": "4.29.1", - "@rollup/rollup-win32-arm64-msvc": "4.29.1", - "@rollup/rollup-win32-ia32-msvc": "4.29.1", - "@rollup/rollup-win32-x64-msvc": "4.29.1", + "@rollup/rollup-android-arm-eabi": "4.34.7", + "@rollup/rollup-android-arm64": "4.34.7", + "@rollup/rollup-darwin-arm64": "4.34.7", + "@rollup/rollup-darwin-x64": "4.34.7", + "@rollup/rollup-freebsd-arm64": "4.34.7", + "@rollup/rollup-freebsd-x64": "4.34.7", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.7", + "@rollup/rollup-linux-arm-musleabihf": "4.34.7", + "@rollup/rollup-linux-arm64-gnu": "4.34.7", + "@rollup/rollup-linux-arm64-musl": "4.34.7", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.7", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.7", + "@rollup/rollup-linux-riscv64-gnu": "4.34.7", + "@rollup/rollup-linux-s390x-gnu": "4.34.7", + "@rollup/rollup-linux-x64-gnu": "4.34.7", + "@rollup/rollup-linux-x64-musl": "4.34.7", + "@rollup/rollup-win32-arm64-msvc": "4.34.7", + "@rollup/rollup-win32-ia32-msvc": "4.34.7", + "@rollup/rollup-win32-x64-msvc": "4.34.7", "fsevents": "~2.3.2" } }, @@ -2719,14 +2697,13 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", - "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", - "license": "MIT", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", + "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", "dependencies": { "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "postcss": "^8.5.1", + "rollup": "^4.30.1" }, "bin": { "vite": "bin/vite.js" diff --git a/todo.md b/todo.md index 2d79665..f77bc10 100644 --- a/todo.md +++ b/todo.md @@ -19,4 +19,6 @@ [ ] customize prompts in [workflow to manage scenario](./workflows/tool_workflow.py)
[ ] add in new tools?
[ ] create tests
-[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" \ No newline at end of file +[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" + +[ ] document *why* temporal for ai agents - scalability, durability in the readme \ No newline at end of file From 8db1dcd4a7f1e4b529969db7de66eeb159a4b643 Mon Sep 17 00:00:00 2001 From: Laine Date: Tue, 11 Mar 2025 14:48:39 -0400 Subject: [PATCH 12/29] Dynamically generate list of agents, try to fix goal changing flow --- models/tool_definitions.py | 4 +++- prompts/agent_prompt_generators.py | 3 ++- tools/change_goal.py | 14 ++----------- tools/goal_registry.py | 19 +++++++++++++++++- tools/list_agents.py | 32 ++++++++++-------------------- workflows/agent_goal_workflow.py | 19 ++++++++++++------ 6 files changed, 49 insertions(+), 42 deletions(-) diff --git a/models/tool_definitions.py b/models/tool_definitions.py index d4b085f..76d9ab3 100644 --- a/models/tool_definitions.py +++ b/models/tool_definitions.py @@ -15,9 +15,11 @@ class ToolDefinition: description: str arguments: List[ToolArgument] - @dataclass class AgentGoal: + id: str + agent_name: str + agent_friendly_description: str tools: List[ToolDefinition] description: str = "Description of the tools purpose and overall goal" starter_prompt: str = "Initial prompt to start the conversation" diff --git a/prompts/agent_prompt_generators.py b/prompts/agent_prompt_generators.py index d5e301f..cbd0d97 100644 --- a/prompts/agent_prompt_generators.py +++ b/prompts/agent_prompt_generators.py @@ -125,7 +125,8 @@ def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) -> "You will need to use the tool_results to auto-fill arguments for subsequent tools and also to figure out if all tools have been run." '{"next": "", "tool": "", "args": {"": "", "": "}, "response": ""}' "ONLY return those json keys (next, tool, args, response), nothing else." - 'Next should be "question".' + 'Next should be "question" if the tool is not the last one in the sequence.' + 'Next should only be "confirm" if all tools have been run (use the system prompt to figure that out).' ) def generate_missing_args_prompt(current_tool: str, tool_data: dict, missing_args: list[str]) -> str: diff --git a/tools/change_goal.py b/tools/change_goal.py index 7588ae2..df897ef 100644 --- a/tools/change_goal.py +++ b/tools/change_goal.py @@ -1,18 +1,8 @@ -# can this just call the API endpoint to set the goal, if that changes to allow a param? -# if this functions, it could work to both send a signal and also circumvent the UI -> API thing. Maybe? - -# --- OR --- - -# end this workflow and start a new one with the new goal - -# --- OR --- - -# send a signal to the workflow from here? -import shared.config - def change_goal(args: dict) -> dict: new_goal = args.get("goalID") + if new_goal is None: + new_goal = "goal_choose_agent_type" return { "new_goal": new_goal, diff --git a/tools/goal_registry.py b/tools/goal_registry.py index 01d1ea9..00be732 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -1,3 +1,4 @@ +from typing import List from models.tool_definitions import AgentGoal from tools.tool_registry import ( search_fixtures_tool, @@ -13,9 +14,12 @@ from tools.tool_registry import ( 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( + id = "goal_choose_agent_type", + agent_name="Choose Agent", + agent_friendly_description="Choose the type of agent to assist you today.", tools=[ list_agents_tool, - change_goal_tool + change_goal_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: " @@ -38,6 +42,9 @@ goal_choose_agent_type = AgentGoal( ) goal_match_train_invoice = AgentGoal( + id = "goal_match_train_invoice", + agent_name="UK Premier League Match Trip Booking", + agent_friendly_description="Book a trip to a city in the UK around the dates of a premier league match.", tools=[ search_fixtures_tool, search_trains_tool, @@ -81,10 +88,14 @@ goal_match_train_invoice = AgentGoal( ) goal_event_flight_invoice = AgentGoal( + id = "goal_event_flight_invoice", + agent_name="Australia and New Zealand Event Flight Booking", + agent_friendly_description="Book a trip to a city in Australia or New Zealand around the dates of events in that city.", tools=[ find_events_tool, search_flights_tool, create_invoice_tool, + list_agents_tool, ], description="Help the user gather args for these tools in order: " "1. FindEvents: Find an event to travel to " @@ -113,3 +124,9 @@ goal_event_flight_invoice = AgentGoal( ] ), ) + +#Add the goals to a list for more generic processing, like listing available agents +goal_list: List[AgentGoal] = [] +goal_list.append(goal_choose_agent_type) +goal_list.append(goal_event_flight_invoice) +goal_list.append(goal_match_train_invoice) diff --git a/tools/list_agents.py b/tools/list_agents.py index 62f3010..1fc56f3 100644 --- a/tools/list_agents.py +++ b/tools/list_agents.py @@ -1,27 +1,17 @@ -from pathlib import Path -import json +import tools.goal_registry as goals def list_agents(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", - } - ) + if goals.goal_list is not None: + for goal in goals.goal_list: + agents.append( + { + "agent_name": goal.agent_name, + "goal_id": goal.id, + "agent_description": goal.agent_friendly_description, + } + ) return { "agents": agents, - } \ No newline at end of file + } diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index c6c05ae..2569041 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -1,6 +1,5 @@ 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 @@ -97,12 +96,17 @@ class AgentGoalWorkflow: self.prompt_queue ) + workflow.logger.warning(f"tool_results keys: {self.tool_results[-1].keys()}") + workflow.logger.warning(f"tool_results values: {self.tool_results[-1].values()}") #set new goal if we should - if len(self.tool_results) > 0 and "new_goal" in self.tool_results[-1].keys() and "ChangeGoal" in self.tool_results[-1].values(): - - new_goal = self.tool_results[-1].get("new_goal") - workflow.logger.warning(f"Booya new goal!: {new_goal}") - self.change_goal(new_goal) + if len(self.tool_results) > 0: + if "ChangeGoal" in self.tool_results[-1].values() and "new_goal" in self.tool_results[-1].keys(): + new_goal = self.tool_results[-1].get("new_goal") + workflow.logger.warning(f"Booya new goal!: {new_goal}") + self.change_goal(new_goal) + elif "ListAgents" in self.tool_results[-1].values() and self.goal.id != "goal_choose_agent_type": + workflow.logger.warning("setting goal to goal_choose_agent_type") + self.change_goal("goal_choose_agent_type") continue if self.prompt_queue: @@ -252,6 +256,9 @@ class AgentGoalWorkflow: } if goal is not None: + # for listed_goal in goals.goal_list: + # if listed_goal.id == goal: + # self.goal = listed_goal self.goal = goals.get(goal) workflow.logger.warning("Changed goal to " + goal) #todo reset goal or tools if this doesn't work or whatever From bb733bc966603dfb095f538aa21614f013007a68 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Tue, 11 Mar 2025 15:05:03 -0400 Subject: [PATCH 13/29] updated todo --- todo.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/todo.md b/todo.md index f77bc10..685ff89 100644 --- a/todo.md +++ b/todo.md @@ -1,4 +1,12 @@ # todo list +[ ] multi-goal
+ [ ] set goal to list agents when done
+ +[ ] document *why* temporal for ai agents - scalability, durability in the readme
+[ ] fix readme: move setup to its own page, demo to its own page, add the why /|\ section
+[ ] add architecture to readme
+[ ] create tests
+ [ ] create people management scenario
-- check pay status -- book work travel @@ -18,7 +26,6 @@ [ ] customize prompts in [workflow to manage scenario](./workflows/tool_workflow.py)
[ ] add in new tools?
-[ ] create tests
+ [ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" -[ ] document *why* temporal for ai agents - scalability, durability in the readme \ No newline at end of file From c0a874b90e3d4eb3930bd282559e617b15667f0f Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Tue, 11 Mar 2025 15:52:47 -0400 Subject: [PATCH 14/29] added some workflow debugging, converted from "done" to pick-new-goal and updated prompts --- prompts/agent_prompt_generators.py | 12 ++++++------ workflows/agent_goal_workflow.py | 15 +++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/prompts/agent_prompt_generators.py b/prompts/agent_prompt_generators.py index cbd0d97..8a001f3 100644 --- a/prompts/agent_prompt_generators.py +++ b/prompts/agent_prompt_generators.py @@ -68,7 +68,7 @@ def generate_genai_prompt( "Your JSON format must be:\n" "{\n" ' "response": "",\n' - ' "next": "",\n' + ' "next": "",\n' ' "tool": "",\n' ' "args": {\n' ' "": "",\n' @@ -122,11 +122,11 @@ def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) -> return ( f"### The '{current_tool}' tool completed successfully with {dynamic_result}. " "INSTRUCTIONS: Parse this tool result as plain text, and use the system prompt containing the list of tools in sequence and the conversation history (and previous tool_results) to figure out next steps, if any. " - "You will need to use the tool_results to auto-fill arguments for subsequent tools and also to figure out if all tools have been run." - '{"next": "", "tool": "", "args": {"": "", "": "}, "response": ""}' - "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 only be "confirm" if all tools have been run (use the system prompt to figure that out).' + "You will need to use the tool_results to auto-fill arguments for subsequent tools and also to figure out if all tools have been run. " + '{"next": "", "tool": "", "args": {"": "", "": "}, "response": ""}' + "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 only be "pick-new-goal" if all tools have been run (use the system prompt to figure that out).' ) def generate_missing_args_prompt(current_tool: str, tool_data: dict, missing_args: list[str]) -> str: diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index ba5652b..4a6f3eb 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -169,8 +169,13 @@ class AgentGoalWorkflow: # move forward in the tool chain next_step = tool_data.get("next") current_tool = tool_data.get("tool") + if "next" in self.tool_data.keys(): + workflow.logger.warning(f"ran the toolplanner, next step: {next_step}") + else: + workflow.logger.warning("ran the toolplanner, next step not set!") if next_step == "confirm" and current_tool: + workflow.logger.warning("ran the toolplanner, trying to confirm") args = tool_data.get("args", {}) if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue): continue @@ -180,10 +185,12 @@ class AgentGoalWorkflow: workflow.logger.info("Waiting for user confirm signal...") # todo probably here we can set the next step to be change-goal - elif next_step == "done": - workflow.logger.info("All steps completed. Exiting workflow.") - self.add_message("agent", tool_data) - return str(self.conversation_history) + elif next_step == "pick-new-goal": + workflow.logger.info("All steps completed. Resetting goal.") + #self.add_message("agent", tool_data) + workflow.logger.warning("pick-new-goal time, setting goal to goal_choose_agent_type") + self.change_goal("goal_choose_agent_type") + #return str(self.conversation_history) self.add_message("agent", tool_data) await helpers.continue_as_new_if_needed( From 56cccd660dfd678d63da33c37520eb811b0ee7fa Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Tue, 11 Mar 2025 15:53:46 -0400 Subject: [PATCH 15/29] todo updates --- todo.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/todo.md b/todo.md index 685ff89..ed6429d 100644 --- a/todo.md +++ b/todo.md @@ -1,7 +1,10 @@ # todo list [ ] multi-goal
- [ ] set goal to list agents when done
+ [x] set goal to list agents when done
+ [ ] make this better/smoother
+[ ] make the debugging confirms optional
+[ ] grok integration
[ ] document *why* temporal for ai agents - scalability, durability in the readme
[ ] fix readme: move setup to its own page, demo to its own page, add the why /|\ section
[ ] add architecture to readme
From fdf5550ea3173cafaff1ff6f6557e602327a4be4 Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 09:01:31 -0400 Subject: [PATCH 16/29] Add "done" back in for prompts, remove argument from ListAgents tool def --- models/data_types.py | 2 +- prompts/agent_prompt_generators.py | 4 ++-- tools/tool_registry.py | 8 +------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/models/data_types.py b/models/data_types.py index 6f331a1..39b9357 100644 --- a/models/data_types.py +++ b/models/data_types.py @@ -17,7 +17,7 @@ class CombinedInput: Message = Dict[str, Union[str, Dict[str, Any]]] ConversationHistory = Dict[str, List[Message]] -NextStep = Literal["confirm", "question", "done"] +NextStep = Literal["confirm", "question", "pick-new-goal", "done"] @dataclass diff --git a/prompts/agent_prompt_generators.py b/prompts/agent_prompt_generators.py index 8a001f3..4ce9194 100644 --- a/prompts/agent_prompt_generators.py +++ b/prompts/agent_prompt_generators.py @@ -68,7 +68,7 @@ def generate_genai_prompt( "Your JSON format must be:\n" "{\n" ' "response": "",\n' - ' "next": "",\n' + ' "next": "",\n' ' "tool": "",\n' ' "args": {\n' ' "": "",\n' @@ -123,7 +123,7 @@ def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) -> f"### The '{current_tool}' tool completed successfully with {dynamic_result}. " "INSTRUCTIONS: Parse this tool result as plain text, and use the system prompt containing the list of tools in sequence and the conversation history (and previous tool_results) to figure out next steps, if any. " "You will need to use the tool_results to auto-fill arguments for subsequent tools and also to figure out if all tools have been run. " - '{"next": "", "tool": "", "args": {"": "", "": "}, "response": ""}' + '{"next": "", "tool": "", "args": {"": "", "": "}, "response": ""}' "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 only be "pick-new-goal" if all tools have been run (use the system prompt to figure that out).' diff --git a/tools/tool_registry.py b/tools/tool_registry.py index 7a08b3b..f80aa8b 100644 --- a/tools/tool_registry.py +++ b/tools/tool_registry.py @@ -3,13 +3,7 @@ from models.tool_definitions import ToolDefinition, ToolArgument list_agents_tool = ToolDefinition( name="ListAgents", description="List available agents to interact with, pulled from goal_registry. ", - arguments=[ - ToolArgument( - name="userConfirmation", - type="string", - description="dummy variable to make thing work", - ), - ], + arguments=[], ) change_goal_tool = ToolDefinition( From c418c185dbc590ebc6b70fd9724d7a648ed1d7a0 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 12 Mar 2025 09:13:47 -0400 Subject: [PATCH 17/29] added test --- tests/agent_goal_workflow_test.py | 55 +++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/agent_goal_workflow_test.py diff --git a/tests/agent_goal_workflow_test.py b/tests/agent_goal_workflow_test.py new file mode 100644 index 0000000..b280def --- /dev/null +++ b/tests/agent_goal_workflow_test.py @@ -0,0 +1,55 @@ +import asyncio + +from temporalio.client import Client, WorkflowExecutionStatus +from temporalio.worker import Worker +from temporalio.testing import TestWorkflowEnvironment +from api.main import get_initial_agent_goal +from models.data_types import AgentGoalWorkflowParams, CombinedInput +from workflows import AgentGoalWorkflow +from activities.tool_activities import ToolActivities, dynamic_tool_activity + + +async def asyncSetUp(self): + # Set up the test environment + self.env = await TestWorkflowEnvironment.create_local() + +async def asyncTearDown(self): + # Clean up after tests + await self.env.shutdown() + +async def test_workflow_success(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" + workflow_id = "agent-workflow" + + initial_agent_goal = get_initial_agent_goal() + + # Create combined input + combined_input = CombinedInput( + tool_params=AgentGoalWorkflowParams(None, None), + agent_goal=initial_agent_goal, + ) + + 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]): + handle = await client.start_workflow( + AgentGoalWorkflow.run, id=workflow_id, task_queue=task_queue_name + ) + # todo fix signals + await handle.signal(AgentGoalWorkflow.submit_greeting, "user1") + await handle.signal(AgentGoalWorkflow.submit_greeting, "user2") + assert WorkflowExecutionStatus.RUNNING == (await handle.describe()).status + + await handle.signal(AgentGoalWorkflow.exit) + assert ["Hello, user1", "Hello, user2"] == await handle.result() + assert WorkflowExecutionStatus.COMPLETED == (await handle.describe()).status + + + + + \ No newline at end of file From 947c5cd0f7f70485190b33d9d7e816240dba87d2 Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 09:20:09 -0400 Subject: [PATCH 18/29] Take out specific goals, add back in elif done so the workflow ends --- workflows/agent_goal_workflow.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 4a6f3eb..56f34a0 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -20,7 +20,7 @@ with workflow.unsafe.imports_passed_through(): CombinedInput, ToolPromptInput, ) - from tools.goal_registry import goal_match_train_invoice, goal_event_flight_invoice, goal_choose_agent_type + from tools.goal_registry import goal_list # Constants MAX_TURNS_BEFORE_CONTINUE = 250 @@ -175,11 +175,12 @@ class AgentGoalWorkflow: workflow.logger.warning("ran the toolplanner, next step not set!") if next_step == "confirm" and current_tool: - workflow.logger.warning("ran the toolplanner, trying to confirm") + workflow.logger.warning("next_step: confirm, ran the toolplanner, trying to confirm") args = tool_data.get("args", {}) if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue): continue +# Would swapping these two get rid of the confirm button step? waiting_for_confirm = True self.confirm = False workflow.logger.info("Waiting for user confirm signal...") @@ -187,10 +188,13 @@ class AgentGoalWorkflow: # todo probably here we can set the next step to be change-goal elif next_step == "pick-new-goal": workflow.logger.info("All steps completed. Resetting goal.") - #self.add_message("agent", tool_data) - workflow.logger.warning("pick-new-goal time, setting goal to goal_choose_agent_type") + workflow.logger.warning("next_step = pick-new-goal, setting goal to goal_choose_agent_type") self.change_goal("goal_choose_agent_type") - #return str(self.conversation_history) + + elif next_step == "done": + workflow.logger.warning("next_step = done") + self.add_message("agent", tool_data) + return str(self.conversation_history) self.add_message("agent", tool_data) await helpers.continue_as_new_if_needed( @@ -262,16 +266,16 @@ class AgentGoalWorkflow: ) def change_goal(self, goal: str) -> None: - goals = { + '''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, - } + }''' if goal is not None: - # for listed_goal in goals.goal_list: - # if listed_goal.id == goal: - # self.goal = listed_goal - self.goal = goals.get(goal) - workflow.logger.warning("Changed goal to " + goal) + for listed_goal in goal_list: + if listed_goal.id == goal: + self.goal = listed_goal + # self.goal = goals.get(goal) + workflow.logger.warning("Changed goal to " + goal) #todo reset goal or tools if this doesn't work or whatever From b2d6f789d979c0d8a57080d3044785133318ab66 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 12 Mar 2025 09:37:20 -0400 Subject: [PATCH 19/29] updated todo list --- todo.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/todo.md b/todo.md index ed6429d..a63530a 100644 --- a/todo.md +++ b/todo.md @@ -1,7 +1,9 @@ # todo list -[ ] multi-goal
+[x] multi-goal
[x] set goal to list agents when done
- [ ] make this better/smoother
+ [x] make this better/smoother
+ +[ ] clean up workflow/make functions [ ] make the debugging confirms optional
[ ] grok integration
@@ -31,4 +33,4 @@ [ ] add in new tools?
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" - +[ ] make it so you can yeet yourself out of a goal and pick a new one From df58eee9d4f2ae84e4e5c86885e14771cffcde44 Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 09:40:56 -0400 Subject: [PATCH 20/29] Change to use goal_list in the api code, add list_agents to the other goal as the last tool --- api/main.py | 16 ++++++---------- tools/goal_registry.py | 3 ++- workflows/agent_goal_workflow.py | 1 - 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/api/main.py b/api/main.py index 1760458..a22405f 100644 --- a/api/main.py +++ b/api/main.py @@ -9,7 +9,7 @@ import asyncio 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, goal_choose_agent_type +from tools.goal_registry import goal_list from fastapi.middleware.cors import CORSMiddleware from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE, AGENT_GOAL @@ -22,17 +22,13 @@ load_dotenv() def get_initial_agent_goal(): """Get the agent goal from environment variables.""" - 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 AGENT_GOAL is not None: - return goals.get(AGENT_GOAL) + for listed_goal in goal_list: + if listed_goal.id == AGENT_GOAL: + return listed_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) + #if no goal is set in the config file, default to choosing an agent + return goal_list.get("goal_choose_agent_type") @app.on_event("startup") diff --git a/tools/goal_registry.py b/tools/goal_registry.py index 00be732..39e2187 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -50,6 +50,7 @@ goal_match_train_invoice = AgentGoal( search_trains_tool, book_trains_tool, create_invoice_tool, + 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. " @@ -95,7 +96,7 @@ goal_event_flight_invoice = AgentGoal( find_events_tool, search_flights_tool, create_invoice_tool, - list_agents_tool, + list_agents_tool, #last tool must be list_agents to fasciliate 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 " diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 56f34a0..c433463 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -180,7 +180,6 @@ class AgentGoalWorkflow: if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue): continue -# Would swapping these two get rid of the confirm button step? waiting_for_confirm = True self.confirm = False workflow.logger.info("Waiting for user confirm signal...") From f969098dc89c1ae454c91e129ca7d2e642766823 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 12 Mar 2025 09:55:25 -0400 Subject: [PATCH 21/29] finishing grok support --- activities/tool_activities.py | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/activities/tool_activities.py b/activities/tool_activities.py index 75e14e0..5d5ffc3 100644 --- a/activities/tool_activities.py +++ b/activities/tool_activities.py @@ -34,6 +34,7 @@ class ToolActivities: # Initialize client variables (all set to None initially) self.openai_client: Optional[OpenAI] = None + self.grok_client: Optional[OpenAI] = None self.anthropic_client: Optional[anthropic.Anthropic] = None self.genai_configured: bool = False self.deepseek_client: Optional[deepseek.DeepSeekAPI] = None @@ -47,6 +48,13 @@ class ToolActivities: print("Initialized OpenAI client") else: print("Warning: OPENAI_API_KEY not set but LLM_PROVIDER is 'openai'") + + if self.llm_provider == "grok": + if os.environ.get("GROK_API_KEY"): + self.grok_client = OpenAI(api_key=os.environ.get("GROK_API_KEY"), base_url="https://api.x.ai/v1") + print("Initialized grok client") + else: + print("Warning: GROK_API_KEY not set but LLM_PROVIDER is 'grok'") elif self.llm_provider == "anthropic": if os.environ.get("ANTHROPIC_API_KEY"): @@ -195,6 +203,8 @@ class ToolActivities: return self.prompt_llm_anthropic(input) elif self.llm_provider == "deepseek": return self.prompt_llm_deepseek(input) + elif self.llm_provider == "grok": + return self.prompt_llm_grok(input) else: return self.prompt_llm_openai(input) @@ -244,6 +254,40 @@ class ToolActivities: return self.parse_json_response(response_content) + def prompt_llm_grok(self, input: ToolPromptInput) -> dict: + if not self.grok_client: + api_key = os.environ.get("GROK_API_KEY") + if not api_key: + raise ValueError( + "GROK_API_KEY is not set in the environment variables but LLM_PROVIDER is 'grok'" + ) + self.grok_client = OpenAI(api_key=api_key, base_url="https://api.x.ai/v1") + print("Initialized grok client on demand") + + messages = [ + { + "role": "system", + "content": input.context_instructions + + ". The current date is " + + datetime.now().strftime("%B %d, %Y"), + }, + { + "role": "user", + "content": input.prompt, + }, + ] + + chat_completion = self.grok_client.chat.completions.create( + model="grok-2-1212", messages=messages + ) + + response_content = chat_completion.choices[0].message.content + activity.logger.info(f"Grok response: {response_content}") + + # Use the new sanitize function + response_content = self.sanitize_json_response(response_content) + + return self.parse_json_response(response_content) def prompt_llm_ollama(self, input: ToolPromptInput) -> dict: # If not yet initialized, try to do so now (this is a backup if warm_up_ollama wasn't called or failed) if not self.ollama_initialized: From 0306a5d726ac74450a32501affb92c71afb3e19f Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 10:20:27 -0400 Subject: [PATCH 22/29] Auto-start workflow if one isn't found to get rid of startup error --- api/main.py | 12 ++++++++---- todo.md | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/api/main.py b/api/main.py index a22405f..f518325 100644 --- a/api/main.py +++ b/api/main.py @@ -113,10 +113,14 @@ async def get_conversation_history(): status_code=404, detail="Workflow worker unavailable or not found." ) - # For other Temporal errors, return a 500 - raise HTTPException( - status_code=500, detail="Internal server error while querying workflow." - ) + if "workflow not found" in error_message: + await start_workflow() + return [] + else: + # For other Temporal errors, return a 500 + raise HTTPException( + status_code=500, detail="Internal server error while querying workflow." + ) @app.get("/agent-goal") async def get_agent_goal(): diff --git a/todo.md b/todo.md index a63530a..a1ee04c 100644 --- a/todo.md +++ b/todo.md @@ -34,3 +34,5 @@ [ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" [ ] make it so you can yeet yourself out of a goal and pick a new one + +[ ] add visual feedback when workflow starting \ No newline at end of file From 697244e9708db5d94a53d93fce3e65186859b754 Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 10:30:42 -0400 Subject: [PATCH 23/29] Move AGENT_GOAL back to env file --- api/main.py | 14 ++++++-------- shared/config.py | 5 ----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/api/main.py b/api/main.py index f518325..7a05ba9 100644 --- a/api/main.py +++ b/api/main.py @@ -1,3 +1,4 @@ +import os from fastapi import FastAPI from typing import Optional from temporalio.client import Client @@ -11,7 +12,7 @@ from workflows.agent_goal_workflow import AgentGoalWorkflow from models.data_types import CombinedInput, AgentGoalWorkflowParams from tools.goal_registry import goal_list from fastapi.middleware.cors import CORSMiddleware -from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE, AGENT_GOAL +from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE app = FastAPI() temporal_client: Optional[Client] = None @@ -22,13 +23,10 @@ load_dotenv() def get_initial_agent_goal(): """Get the agent goal from environment variables.""" - if AGENT_GOAL is not None: - for listed_goal in goal_list: - if listed_goal.id == AGENT_GOAL: - return listed_goal - else: - #if no goal is set in the config file, default to choosing an agent - return goal_list.get("goal_choose_agent_type") + env_goal = os.getenv("AGENT_GOAL", "goal_choose_agent_type") #if no goal is set in the env file, default to choosing an agent + for listed_goal in goal_list: + if listed_goal.id == env_goal: + return listed_goal @app.on_event("startup") diff --git a/shared/config.py b/shared/config.py index cb4b9da..9590634 100644 --- a/shared/config.py +++ b/shared/config.py @@ -16,11 +16,6 @@ TEMPORAL_TLS_CERT = os.getenv("TEMPORAL_TLS_CERT", "") TEMPORAL_TLS_KEY = os.getenv("TEMPORAL_TLS_KEY", "") TEMPORAL_API_KEY = os.getenv("TEMPORAL_API_KEY", "") -#Starting agent goal - 1st goal is always to help user pick a next goal -AGENT_GOAL = "goal_choose_agent_type" -#AGENT_GOAL = "goal_event_flight_invoice" - - async def get_temporal_client() -> Client: """ Creates a Temporal client based on environment configuration. From 504361a5a7d94b8e2ba154430a82621585066e7b Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 11:25:57 -0400 Subject: [PATCH 24/29] Add a bunch of logging and comments re: what's happenin' --- workflows/agent_goal_workflow.py | 59 ++++++++++++++++---------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index c433463..accc1ae 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 os from typing import Dict, Any, Union, List, Optional, Deque, TypedDict from temporalio.common import RetryPolicy @@ -25,6 +26,8 @@ with workflow.unsafe.imports_passed_through(): # Constants MAX_TURNS_BEFORE_CONTINUE = 250 +SHOW_CONFIRM = os.getenv("SHOW_CONFIRM", True) + class ToolData(TypedDict, total=False): next: NextStep tool: str @@ -77,12 +80,13 @@ class AgentGoalWorkflow: # handle chat-end signal if self.chat_ended: + workflow.logger.warning(f"workflow step: chat-end signal received, ending") workflow.logger.info("Chat ended.") - return f"{self.conversation_history}" - # execute tool + # user has confirmed, now actually execute the tool if self.confirm and waiting_for_confirm and current_tool and self.tool_data: + workflow.logger.warning(f"workflow step: user has confirmed, executing the tool {current_tool}") self.confirm = False waiting_for_confirm = False @@ -112,10 +116,11 @@ class AgentGoalWorkflow: self.change_goal("goal_choose_agent_type") continue - # push messages to UI if there are any + # if we've received messages to be processed on the prompt queue... if self.prompt_queue: prompt = self.prompt_queue.popleft() - if not prompt.startswith("###"): + workflow.logger.warning(f"workflow step: processing message on the prompt queue, message is {prompt}") + if not prompt.startswith("###"): #if the message isn't from the LLM but is instead from the user self.add_message("user", prompt) # Validate the prompt before proceeding @@ -134,27 +139,17 @@ class AgentGoalWorkflow: ), ) - #If validation fails, provide that feedback to the user - i.e., "your words make no sense, human" + #If validation fails, provide that feedback to the user - i.e., "your words make no sense, puny human" end this iteration of processing if not validation_result.validationResult: - workflow.logger.warning( - f"Prompt validation failed: {validation_result.validationFailedReason}" - ) - self.add_message( - "agent", validation_result.validationFailedReason - ) + workflow.logger.warning(f"Prompt validation failed: {validation_result.validationFailedReason}") + self.add_message("agent", validation_result.validationFailedReason) continue - # Proceed with generating the context and prompt - context_instructions = generate_genai_prompt( - self.goal, self.conversation_history, self.tool_data - ) + # If valid, proceed with generating the context and prompt + context_instructions = generate_genai_prompt(self.goal, self.conversation_history, self.tool_data) + prompt_input = ToolPromptInput(prompt=prompt, context_instructions=context_instructions) - prompt_input = ToolPromptInput( - prompt=prompt, - context_instructions=context_instructions, - ) - - # connect to LLM and get it to create a prompt for the user about the tool + # connect to LLM and execute to get next steps tool_data = await workflow.execute_activity( ToolActivities.agent_toolPlanner, prompt_input, @@ -166,33 +161,34 @@ class AgentGoalWorkflow: ) self.tool_data = tool_data - # move forward in the tool chain + # process the tool as dictated by the prompt response - what to do next, and with which tool next_step = tool_data.get("next") current_tool = tool_data.get("tool") - if "next" in self.tool_data.keys(): - workflow.logger.warning(f"ran the toolplanner, next step: {next_step}") - else: - workflow.logger.warning("ran the toolplanner, next step not set!") + #if the next step is to confirm... if next_step == "confirm" and current_tool: - workflow.logger.warning("next_step: confirm, ran the toolplanner, trying to confirm") + workflow.logger.warning(f"next_step: confirm, current tool is {current_tool}") args = tool_data.get("args", {}) + #if we're missing arguments, go back to the top of the loop if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue): continue + #...otherwise, set up the request for user confirmation waiting_for_confirm = True self.confirm = False workflow.logger.info("Waiting for user confirm signal...") - # todo probably here we can set the next step to be change-goal + # else if the next step is to pick a new goal... elif next_step == "pick-new-goal": workflow.logger.info("All steps completed. Resetting goal.") workflow.logger.warning("next_step = pick-new-goal, setting goal to 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" elif next_step == "done": workflow.logger.warning("next_step = done") self.add_message("agent", tool_data) + # end the workflow return str(self.conversation_history) self.add_message("agent", tool_data) @@ -208,8 +204,9 @@ class AgentGoalWorkflow: @workflow.signal async def user_prompt(self, prompt: str) -> None: """Signal handler for receiving user prompts.""" + workflow.logger.warning(f"signal received: user_prompt, prompt is {prompt}") if self.chat_ended: - workflow.logger.warn(f"Message dropped due to chat closed: {prompt}") + workflow.logger.warning(f"Message dropped due to chat closed: {prompt}") return self.prompt_queue.append(prompt) @@ -218,12 +215,14 @@ class AgentGoalWorkflow: async def confirm(self) -> None: """Signal handler for user confirmation of tool execution.""" workflow.logger.info("Received user confirmation") + workflow.logger.warning(f"signal recieved: confirm") 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.""" + workflow.logger.warning("signal received: end_chat") self.chat_ended = True @workflow.query From 02a63917b2926a7201774d01b87b22d2974a4714 Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 12:49:00 -0400 Subject: [PATCH 25/29] Part of one of making confirmation optional - auto-confirm but still show everything --- workflows/agent_goal_workflow.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index accc1ae..bb79adc 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -26,7 +26,11 @@ with workflow.unsafe.imports_passed_through(): # Constants MAX_TURNS_BEFORE_CONTINUE = 250 -SHOW_CONFIRM = os.getenv("SHOW_CONFIRM", True) +SHOW_CONFIRM = True +show_confirm_env = os.getenv("SHOW_CONFIRM") +if show_confirm_env is not None: + if show_confirm_env == "Off": + SHOW_CONFIRM = False class ToolData(TypedDict, total=False): next: NextStep @@ -51,6 +55,7 @@ class AgentGoalWorkflow: # see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set @workflow.run async def run(self, combined_input: CombinedInput) -> str: + """Main workflow execution method.""" # setup phase, starts with blank tool_params and agent_goal prompt as defined in tools/goal_registry.py params = combined_input.tool_params @@ -84,7 +89,7 @@ class AgentGoalWorkflow: workflow.logger.info("Chat ended.") return f"{self.conversation_history}" - # user has confirmed, now actually execute the tool + # Execute the tool if self.confirm and waiting_for_confirm and current_tool and self.tool_data: workflow.logger.warning(f"workflow step: user has confirmed, executing the tool {current_tool}") self.confirm = False @@ -103,8 +108,6 @@ class AgentGoalWorkflow: self.prompt_queue ) - workflow.logger.warning(f"tool_results keys: {self.tool_results[-1].keys()}") - workflow.logger.warning(f"tool_results values: {self.tool_results[-1].values()}") #set new goal if we should if len(self.tool_results) > 0: if "ChangeGoal" in self.tool_results[-1].values() and "new_goal" in self.tool_results[-1].keys(): @@ -165,28 +168,30 @@ class AgentGoalWorkflow: next_step = tool_data.get("next") current_tool = tool_data.get("tool") + workflow.logger.warning(f"next_step: {next_step}, current tool is {current_tool}") #if the next step is to confirm... if next_step == "confirm" and current_tool: - workflow.logger.warning(f"next_step: confirm, current tool is {current_tool}") args = tool_data.get("args", {}) #if we're missing arguments, go back to the top of the loop if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue): continue - #...otherwise, set up the request for user confirmation + #...otherwise, if we want to force the user to confirm, set that up waiting_for_confirm = True - self.confirm = False - workflow.logger.info("Waiting for user confirm signal...") + if SHOW_CONFIRM: + self.confirm = False + workflow.logger.info("Waiting for user confirm signal...") + else: + #theory - set self.confirm to true bc that's the signal, so we can get around the signal?? + self.confirm = True # else if the next step is to pick a new goal... elif next_step == "pick-new-goal": workflow.logger.info("All steps completed. Resetting goal.") - workflow.logger.warning("next_step = pick-new-goal, setting goal to 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" elif next_step == "done": - workflow.logger.warning("next_step = done") self.add_message("agent", tool_data) # end the workflow return str(self.conversation_history) From 1a270fa917c2ccc771825d7ab8cee1881a426c70 Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 12:50:02 -0400 Subject: [PATCH 26/29] Forgot the env.example... --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index a1a1944..4701495 100644 --- a/.env.example +++ b/.env.example @@ -37,3 +37,6 @@ OPENAI_API_KEY=sk-proj-... # Agent Goal Configuration # AGENT_GOAL=goal_event_flight_invoice # (default) or goal_match_train_invoice + +# Set if the UI should force a user confirmation step or not +SHOW_CONFIRM=True \ No newline at end of file From a488bbac23b4123d960f59640080ea1f1d2ec662 Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 12:50:44 -0400 Subject: [PATCH 27/29] Use False, not Off --- workflows/agent_goal_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index bb79adc..cf780b4 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -29,7 +29,7 @@ MAX_TURNS_BEFORE_CONTINUE = 250 SHOW_CONFIRM = True show_confirm_env = os.getenv("SHOW_CONFIRM") if show_confirm_env is not None: - if show_confirm_env == "Off": + if show_confirm_env == "False": SHOW_CONFIRM = False class ToolData(TypedDict, total=False): From 380581b0d9cf9b193c6e236dc9a8b1f9fe914ff3 Mon Sep 17 00:00:00 2001 From: Laine Date: Wed, 12 Mar 2025 13:22:04 -0400 Subject: [PATCH 28/29] Part two of making confirmation optional - add flag to ToolData so the button won't show in the UI --- frontend/src/components/LLMResponse.jsx | 2 +- workflows/agent_goal_workflow.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/LLMResponse.jsx b/frontend/src/components/LLMResponse.jsx index feedbf7..33de334 100644 --- a/frontend/src/components/LLMResponse.jsx +++ b/frontend/src/components/LLMResponse.jsx @@ -27,7 +27,7 @@ const LLMResponse = memo(({ data, onConfirm, isLastMessage, onHeightChange }) => : data?.response; const displayText = (response || '').trim(); - const requiresConfirm = data.next === "confirm" && isLastMessage; + const requiresConfirm = data.force_confirm && data.next === "confirm" && isLastMessage; const defaultText = requiresConfirm ? `Agent is ready to run "${data.tool}". Please confirm.` : ''; diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index cf780b4..3f73866 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -32,11 +32,13 @@ if show_confirm_env is not None: if show_confirm_env == "False": SHOW_CONFIRM = False +#ToolData as part of the workflow is what's accessible to the UI - see LLMResponse.jsx for example class ToolData(TypedDict, total=False): next: NextStep tool: str args: Dict[str, Any] response: str + force_confirm: bool = True @workflow.defn class AgentGoalWorkflow: @@ -162,6 +164,7 @@ class AgentGoalWorkflow: initial_interval=timedelta(seconds=5), backoff_coefficient=1 ), ) + tool_data["force_confirm"] = SHOW_CONFIRM self.tool_data = tool_data # process the tool as dictated by the prompt response - what to do next, and with which tool From d807e9893d03ed230a8de1306560219c07e359b4 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 12 Mar 2025 13:37:04 -0400 Subject: [PATCH 29/29] updates to readme, docs, guides --- README.md | 196 ++---------------- architecture.md | 12 ++ assets/Architecture_elements.png | Bin 0 -> 40166 bytes .../agent-youtube-screenshot.jpeg | Bin assets/ai_agent_architecture_model.png | Bin 0 -> 147592 bytes setup.md | 176 ++++++++++++++++ todo.md | 53 +++-- 7 files changed, 235 insertions(+), 202 deletions(-) create mode 100644 architecture.md create mode 100644 assets/Architecture_elements.png rename agent-youtube-screenshot.jpeg => assets/agent-youtube-screenshot.jpeg (100%) create mode 100644 assets/ai_agent_architecture_model.png create mode 100644 setup.md diff --git a/README.md b/README.md index fd070f2..236f468 100644 --- a/README.md +++ b/README.md @@ -2,191 +2,29 @@ This demo shows a multi-turn conversation with an AI agent running inside a Temporal workflow. The purpose of the agent is to collect information towards a goal, running tools along the way. There's a simple DSL input for collecting information (currently set up to use mock functions to search for public events, search for flights around those events, then create a test Stripe invoice for the trip). -The AI will respond with clarifications and ask for any missing information to that goal. You can configure it to use [ChatGPT 4o](https://openai.com/index/hello-gpt-4o/), [Anthropic Claude](https://www.anthropic.com/claude), [Google Gemini](https://gemini.google.com), [Deepseek-V3](https://www.deepseek.com/) or a local LLM of your choice using [Ollama](https://ollama.com). +The AI will respond with clarifications and ask for any missing information to that goal. You can configure it to use [ChatGPT 4o](https://openai.com/index/hello-gpt-4o/), [Anthropic Claude](https://www.anthropic.com/claude), [Google Gemini](https://gemini.google.com), [Deepseek-V3](https://www.deepseek.com/), [Grok](https://docs.x.ai/docs/overview) or a local LLM of your choice using [Ollama](https://ollama.com). -[Watch the demo (5 minute YouTube video)](https://www.youtube.com/watch?v=GEXllEH2XiQ) +It's really helpful to [watch the demo (5 minute YouTube video)](https://www.youtube.com/watch?v=GEXllEH2XiQ) to understand how interaction works. -[![Watch the demo](./agent-youtube-screenshot.jpeg)](https://www.youtube.com/watch?v=GEXllEH2XiQ) +[![Watch the demo](./assets/agent-youtube-screenshot.jpeg)](https://www.youtube.com/watch?v=GEXllEH2XiQ) -## Configuration +## Setup and Configuration +See [the Setup guide](./setup.md). -This application uses `.env` files for configuration. Copy the [.env.example](.env.example) file to `.env` and update the values: +## Interaction +TODO -```bash -cp .env.example .env -``` +## Architecture +See [the architecture guide](./architecture.md). -### Agent Goal Configuration - -The agent can be configured to pursue different goals using the `AGENT_GOAL` environment variable in your `.env` file. - -#### Goal: Find an event in Australia / New Zealand, book flights to it and invoice the user for the cost -- `AGENT_GOAL=goal_event_flight_invoice` (default) - Helps users find events, book flights, and arrange train travel with invoice generation - - This is the scenario in the video above - -#### Goal: Find a Premier League match, book train tickets to it and invoice the user for the cost -- `AGENT_GOAL=goal_match_train_invoice` - Focuses on Premier League match attendance with train booking and invoice generation - - This is a new goal that is part of an upcoming conference talk - -If not specified, the agent defaults to `goal_event_flight_invoice`. Each goal comes with its own set of tools and conversation flows designed for specific use cases. You can examine `tools/goal_registry.py` to see the detailed configuration of each goal. - -See the next section for tool configuration for each goal. - -### Tool Configuration - -#### Agent Goal: goal_event_flight_invoice (default) -* The agent uses a mock function to search for events. This has zero configuration. -* By default the agent uses a mock function to search for flights. - * If you want to use the real flights API, go to `tools/search_flights.py` and replace the `search_flights` function with `search_flights_real_api` that exists in the same file. - * It's free to sign up at [RapidAPI](https://rapidapi.com/apiheya/api/sky-scrapper) - * This api might be slow to respond, so you may want to increase the start to close timeout, `TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT` in `workflows/workflow_helpers.py` -* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env - * It's free to sign up and get a key at [Stripe](https://stripe.com/) - * If you're lazy go to `tools/create_invoice.py` and replace the `create_invoice` function with the mock `create_invoice_example` that exists in the same file. - -#### Agent Goal: goal_match_train_invoice - -* Finding a match requires a key from [Football Data](https://www.football-data.org). Sign up for a free account, then see the 'My Account' page to get your API token. Set `FOOTBALL_DATA_API_KEY` to this value. - * If you're lazy go to `tools/search_fixtures.py` and replace the `search_fixtures` function with the mock `search_fixtures_example` that exists in the same file. -* We use a mock function to search for trains. Start the train API server to use the real API: `python thirdparty/train_api.py` -* * The train activity is 'enterprise' so it's written in C# and requires a .NET runtime. See the [.NET backend](#net-(enterprise)-backend) section for details on running it. -* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env - * It's free to sign up and get a key at [Stripe](https://stripe.com/) - * If you're lazy go to `tools/create_invoice.py` and replace the `create_invoice` function with the mock `create_invoice_example` that exists in the same file. - -### LLM Provider Configuration - -The agent can use OpenAI's GPT-4o, Google Gemini, Anthropic Claude, or a local LLM via Ollama. Set the `LLM_PROVIDER` environment variable in your `.env` file to choose the desired provider: - -- `LLM_PROVIDER=openai` for OpenAI's GPT-4o -- `LLM_PROVIDER=google` for Google Gemini -- `LLM_PROVIDER=anthropic` for Anthropic Claude -- `LLM_PROVIDER=deepseek` for DeepSeek-V3 -- `LLM_PROVIDER=ollama` for running LLMs via [Ollama](https://ollama.ai) (not recommended for this use case) - -### Option 1: OpenAI - -If using OpenAI, ensure you have an OpenAI key for the GPT-4o model. Set this in the `OPENAI_API_KEY` environment variable in `.env`. - -### Option 2: Google Gemini - -To use Google Gemini: - -1. Obtain a Google API key and set it in the `GOOGLE_API_KEY` environment variable in `.env`. -2. Set `LLM_PROVIDER=google` in your `.env` file. - -### Option 3: Anthropic Claude (recommended) - -I find that Claude Sonnet 3.5 performs better than the other hosted LLMs for this use case. - -To use Anthropic: - -1. Obtain an Anthropic API key and set it in the `ANTHROPIC_API_KEY` environment variable in `.env`. -2. Set `LLM_PROVIDER=anthropic` in your `.env` file. - -### Option 4: Deepseek-V3 - -To use Deepseek-V3: - -1. Obtain a Deepseek API key and set it in the `DEEPSEEK_API_KEY` environment variable in `.env`. -2. Set `LLM_PROVIDER=deepseek` in your `.env` file. - -### Option 5: Local LLM via Ollama (not recommended) - -To use a local LLM with Ollama: - -1. Install [Ollama](https://ollama.com) and the [Qwen2.5 14B](https://ollama.com/library/qwen2.5) model. - - Run `ollama run ` to start the model. Note that this model is about 9GB to download. - - Example: `ollama run qwen2.5:14b` - -2. Set `LLM_PROVIDER=ollama` in your `.env` file and `OLLAMA_MODEL_NAME` to the name of the model you installed. - -Note: I found the other (hosted) LLMs to be MUCH more reliable for this use case. However, you can switch to Ollama if desired, and choose a suitably large model if your computer has the resources. - -## Configuring Temporal Connection - -By default, this application will connect to a local Temporal server (`localhost:7233`) in the default namespace, using the `agent-task-queue` task queue. You can override these settings in your `.env` file. - -### Use Temporal Cloud - -See [.env.example](.env.example) for details on connecting to Temporal Cloud using mTLS or API key authentication. - -[Sign up for Temporal Cloud](https://temporal.io/get-cloud) - -### Use a local Temporal Dev Server - -On a Mac -```bash -brew install temporal -temporal server start-dev -``` -See the [Temporal documentation](https://learn.temporal.io/getting_started/python/dev_environment/) for other platforms. - - -## Running the Application - -### Python Backend - -Requires [Poetry](https://python-poetry.org/) to manage dependencies. - -1. `python -m venv venv` - -2. `source venv/bin/activate` - -3. `poetry install` - -Run the following commands in separate terminal windows: - -1. Start the Temporal worker: -```bash -poetry run python scripts/run_worker.py -``` - -2. Start the API server: -```bash -poetry run uvicorn api.main:app --reload -``` -Access the API at `/docs` to see the available endpoints. - -### React UI -Start the frontend: -```bash -cd frontend -npm install -npx vite -``` -Access the UI at `http://localhost:5173` - -### Python Search Trains API -> Agent Goal: goal_match_train_invoice only - -Required to search and book trains! -```bash -poetry run python thirdparty/train_api.py - -# example url -# http://localhost:8080/api/search?from=london&to=liverpool&outbound_time=2025-04-18T09:00:00&inbound_time=2025-04-20T09:00:00 -``` - -### .NET (enterprise) Backend ;) -> Agent Goal: goal_match_train_invoice only - -We have activities written in C# to call the train APIs. -```bash -cd enterprise -dotnet build # ensure you brew install dotnet@8 first! -dotnet run -``` -If you're running your train API above on a different host/port then change the API URL in `Program.cs`. Otherwise, be sure to run it using `python thirdparty/train_api.py`. - -## Customizing the Agent -- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them) -- `goal_registry.py` contains descriptions of goals and the tools used to achieve them -- The tools themselves are defined in their own files in `/tools` -- Note the mapping in `tools/__init__.py` to each tool - -## TODO +## Productionalization & Adding Features - In a prod setting, I would need to ensure that payload data is stored separately (e.g. in S3 or a noSQL db - the claim-check pattern), or otherwise 'garbage collected'. Without these techniques, long conversations will fill up the workflow's conversation history, and start to breach Temporal event history payload limits. - Continue-as-new shouldn't be a big consideration for this use case (as it would take many conversational turns to trigger). Regardless, I should ensure that it's able to carry the agent state over to the new workflow execution. - Perhaps the UI should show when the LLM response is being retried (i.e. activity retry attempt because the LLM provided bad output) -- Tests would be nice! \ No newline at end of file +- Tests would be nice! +See [the todo](./todo.md) for more details. + +See Customization for more details. <-- TODO + +## For Temporal SAs +Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the enablement guide here (TODO). diff --git a/architecture.md b/architecture.md new file mode 100644 index 0000000..f1a5d7b --- /dev/null +++ b/architecture.md @@ -0,0 +1,12 @@ +# Elements +![Architecture Elements](./assets/Architecture_elements.png "Architecture Elements") + +talk through the pieces + +# Architecture Model +![Architecture](./assets/ai_agent_architecture_model.png "Architecture Model") + +explain elements + +# Adding features +link to how to LLM interactions/how to change \ No newline at end of file diff --git a/assets/Architecture_elements.png b/assets/Architecture_elements.png new file mode 100644 index 0000000000000000000000000000000000000000..a1f7b61ad77eedada476470a395c8e8c8f14ba62 GIT binary patch literal 40166 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 literal 0 HcmV?d00001 diff --git a/agent-youtube-screenshot.jpeg b/assets/agent-youtube-screenshot.jpeg similarity index 100% rename from agent-youtube-screenshot.jpeg rename to assets/agent-youtube-screenshot.jpeg diff --git a/assets/ai_agent_architecture_model.png b/assets/ai_agent_architecture_model.png new file mode 100644 index 0000000000000000000000000000000000000000..e38f19b0dda868db88236af04cdd5cb3f494bb14 GIT binary patch literal 147592 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 CvD6{} literal 0 HcmV?d00001 diff --git a/setup.md b/setup.md new file mode 100644 index 0000000..5f20618 --- /dev/null +++ b/setup.md @@ -0,0 +1,176 @@ +## Configuration + +This application uses `.env` files for configuration. Copy the [.env.example](.env.example) file to `.env` and update the values: + +```bash +cp .env.example .env +``` + +### Agent Goal Configuration + +The agent can be configured to pursue different goals using the `AGENT_GOAL` environment variable in your `.env` file. + +#### Goal: Find an event in Australia / New Zealand, book flights to it and invoice the user for the cost +- `AGENT_GOAL=goal_event_flight_invoice` (default) - Helps users find events, book flights, and arrange train travel with invoice generation + - This is the scenario in the video above + +#### Goal: Find a Premier League match, book train tickets to it and invoice the user for the cost +- `AGENT_GOAL=goal_match_train_invoice` - Focuses on Premier League match attendance with train booking and invoice generation + - This is a new goal that is part of an upcoming conference talk + +If not specified, the agent defaults to `goal_event_flight_invoice`. Each goal comes with its own set of tools and conversation flows designed for specific use cases. You can examine `tools/goal_registry.py` to see the detailed configuration of each goal. + +See the next section for tool configuration for each goal. + +### Tool Configuration + +#### Agent Goal: goal_event_flight_invoice (default) +* The agent uses a mock function to search for events. This has zero configuration. +* By default the agent uses a mock function to search for flights. + * If you want to use the real flights API, go to `tools/search_flights.py` and replace the `search_flights` function with `search_flights_real_api` that exists in the same file. + * It's free to sign up at [RapidAPI](https://rapidapi.com/apiheya/api/sky-scrapper) + * This api might be slow to respond, so you may want to increase the start to close timeout, `TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT` in `workflows/workflow_helpers.py` +* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env + * It's free to sign up and get a key at [Stripe](https://stripe.com/) + * If you're lazy go to `tools/create_invoice.py` and replace the `create_invoice` function with the mock `create_invoice_example` that exists in the same file. + +#### Agent Goal: goal_match_train_invoice + +* Finding a match requires a key from [Football Data](https://www.football-data.org). Sign up for a free account, then see the 'My Account' page to get your API token. Set `FOOTBALL_DATA_API_KEY` to this value. + * If you're lazy go to `tools/search_fixtures.py` and replace the `search_fixtures` function with the mock `search_fixtures_example` that exists in the same file. +* We use a mock function to search for trains. Start the train API server to use the real API: `python thirdparty/train_api.py` +* * The train activity is 'enterprise' so it's written in C# and requires a .NET runtime. See the [.NET backend](#net-(enterprise)-backend) section for details on running it. +* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env + * It's free to sign up and get a key at [Stripe](https://stripe.com/) + * If you're lazy go to `tools/create_invoice.py` and replace the `create_invoice` function with the mock `create_invoice_example` that exists in the same file. + +### LLM Provider Configuration + +The agent can use OpenAI's GPT-4o, Google Gemini, Anthropic Claude, or a local LLM via Ollama. Set the `LLM_PROVIDER` environment variable in your `.env` file to choose the desired provider: + +- `LLM_PROVIDER=openai` for OpenAI's GPT-4o +- `LLM_PROVIDER=google` for Google Gemini +- `LLM_PROVIDER=anthropic` for Anthropic Claude +- `LLM_PROVIDER=deepseek` for DeepSeek-V3 +- `LLM_PROVIDER=ollama` for running LLMs via [Ollama](https://ollama.ai) (not recommended for this use case) + +### Option 1: OpenAI + +If using OpenAI, ensure you have an OpenAI key for the GPT-4o model. Set this in the `OPENAI_API_KEY` environment variable in `.env`. + +### Option 2: Google Gemini + +To use Google Gemini: + +1. Obtain a Google API key and set it in the `GOOGLE_API_KEY` environment variable in `.env`. +2. Set `LLM_PROVIDER=google` in your `.env` file. + +### Option 3: Anthropic Claude (recommended) + +I find that Claude Sonnet 3.5 performs better than the other hosted LLMs for this use case. + +To use Anthropic: + +1. Obtain an Anthropic API key and set it in the `ANTHROPIC_API_KEY` environment variable in `.env`. +2. Set `LLM_PROVIDER=anthropic` in your `.env` file. + +### Option 4: Deepseek-V3 + +To use Deepseek-V3: + +1. Obtain a Deepseek API key and set it in the `DEEPSEEK_API_KEY` environment variable in `.env`. +2. Set `LLM_PROVIDER=deepseek` in your `.env` file. + +### Option 5: Local LLM via Ollama (not recommended) + +To use a local LLM with Ollama: + +1. Install [Ollama](https://ollama.com) and the [Qwen2.5 14B](https://ollama.com/library/qwen2.5) model. + - Run `ollama run ` to start the model. Note that this model is about 9GB to download. + - Example: `ollama run qwen2.5:14b` + +2. Set `LLM_PROVIDER=ollama` in your `.env` file and `OLLAMA_MODEL_NAME` to the name of the model you installed. + +Note: I found the other (hosted) LLMs to be MUCH more reliable for this use case. However, you can switch to Ollama if desired, and choose a suitably large model if your computer has the resources. + +## Configuring Temporal Connection + +By default, this application will connect to a local Temporal server (`localhost:7233`) in the default namespace, using the `agent-task-queue` task queue. You can override these settings in your `.env` file. + +### Use Temporal Cloud + +See [.env.example](.env.example) for details on connecting to Temporal Cloud using mTLS or API key authentication. + +[Sign up for Temporal Cloud](https://temporal.io/get-cloud) + +### Use a local Temporal Dev Server + +On a Mac +```bash +brew install temporal +temporal server start-dev +``` +See the [Temporal documentation](https://learn.temporal.io/getting_started/python/dev_environment/) for other platforms. + + +## Running the Application + +### Python Backend + +Requires [Poetry](https://python-poetry.org/) to manage dependencies. + +1. `python -m venv venv` + +2. `source venv/bin/activate` + +3. `poetry install` + +Run the following commands in separate terminal windows: + +1. Start the Temporal worker: +```bash +poetry run python scripts/run_worker.py +``` + +2. Start the API server: +```bash +poetry run uvicorn api.main:app --reload +``` +Access the API at `/docs` to see the available endpoints. + +### React UI +Start the frontend: +```bash +cd frontend +npm install +npx vite +``` +Access the UI at `http://localhost:5173` + +### Python Search Trains API +> Agent Goal: goal_match_train_invoice only + +Required to search and book trains! +```bash +poetry run python thirdparty/train_api.py + +# example url +# http://localhost:8080/api/search?from=london&to=liverpool&outbound_time=2025-04-18T09:00:00&inbound_time=2025-04-20T09:00:00 +``` + +### .NET (enterprise) Backend ;) +> Agent Goal: goal_match_train_invoice only + +We have activities written in C# to call the train APIs. +```bash +cd enterprise +dotnet build # ensure you brew install dotnet@8 first! +dotnet run +``` +If you're running your train API above on a different host/port then change the API URL in `Program.cs`. Otherwise, be sure to run it using `python thirdparty/train_api.py`. + +## Customizing the Agent +- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them) +- `goal_registry.py` contains descriptions of goals and the tools used to achieve them +- The tools themselves are defined in their own files in `/tools` +- Note the mapping in `tools/__init__.py` to each tool \ No newline at end of file diff --git a/todo.md b/todo.md index a63530a..46b59cb 100644 --- a/todo.md +++ b/todo.md @@ -1,36 +1,43 @@ # todo list -[x] multi-goal
- [x] set goal to list agents when done
- [x] make this better/smoother
- [ ] clean up workflow/make functions [ ] make the debugging confirms optional
-[ ] grok integration
-[ ] document *why* temporal for ai agents - scalability, durability in the readme
+
+[ ] document *why* temporal for ai agents - scalability, durability, visibility in the readme
[ ] fix readme: move setup to its own page, demo to its own page, add the why /|\ section
[ ] add architecture to readme
+- elements of app
+- dive into llm interaction
+- workflow breakdown - interactive loop
+- why temporal
+ +[ ] setup readme, why readme, architecture readme, what this is in main readme with temporal value props and pictures
+[ ] how to add more scenarios, tools
+
+
[ ] create tests
[ ] create people management scenario
- -- check pay status - -- book work travel - -- check PTO levels - -- check insurance coverages - -- book PTO around a date (https://developers.google.com/calendar/api/guides/overview)? - -- scenario should use multiple tools - -- expense management - -- check in on the health of the team -[ ] demo the reasons why: - -- Orchestrate interactions across distributed data stores and tools - -- Hold state, potentially over long periods of time - -- Ability to ‘self-heal’ and retry until the (probabilistic) LLM returns valid data - -- Support for human intervention such as approvals - -- Parallel processing for efficiency of data retrieval and tool use - -- Insight into the agent’s performance +- check pay status
+- book work travel
+- check PTO levels
+- check insurance coverages
+- book PTO around a date (https://developers.google.com/calendar/api/guides/overview)?
+- scenario should use multiple tools
+- expense management
+- check in on the health of the team
+ +[ ] demo the reasons why:
+- Orchestrate interactions across distributed data stores and tools
+- Hold state, potentially over long periods of time
+- Ability to ‘self-heal’ and retry until the (probabilistic) LLM returns valid data
+- Support for human intervention such as approvals
+- Parallel processing for efficiency of data retrieval and tool use
+- Insight into the agent’s performance
+ - 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 [ ] customize prompts in [workflow to manage scenario](./workflows/tool_workflow.py)
[ ] add in new tools?
-[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" -[ ] make it so you can yeet yourself out of a goal and pick a new one +[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError"
+[ ] make it so you can yeet yourself out of a goal and pick a new one