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 diff --git a/api/main.py b/api/main.py index a22405f..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") @@ -113,10 +111,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/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/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. diff --git a/todo.md b/todo.md index 46b59cb..d477f4c 100644 --- a/todo.md +++ b/todo.md @@ -41,3 +41,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 diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index c433463..3f73866 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,11 +26,19 @@ with workflow.unsafe.imports_passed_through(): # Constants 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 == "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: @@ -48,6 +57,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 @@ -77,12 +87,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 + # 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 @@ -99,8 +110,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(): @@ -112,10 +121,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 +144,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, @@ -164,35 +164,39 @@ class AgentGoalWorkflow: initial_interval=timedelta(seconds=5), backoff_coefficient=1 ), ) + tool_data["force_confirm"] = SHOW_CONFIRM 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!") + 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("next_step: confirm, ran the toolplanner, trying to confirm") 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, 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 - # 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 +212,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 +223,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