Enhance Dev Experience and Code Quality (#41)

* Format codebase to satisfy linters

* fixing pylance and ruff-checked files

* contributing md, and type and formatting fixes

* setup file capitalization

* test fix
This commit is contained in:
Steve Androulakis
2025-06-01 08:54:59 -07:00
committed by GitHub
parent e35181b5ad
commit eb06cf5c8d
52 changed files with 1282 additions and 1105 deletions

View File

@@ -1,31 +1,35 @@
from collections import deque
from datetime import timedelta
from typing import Dict, Any, Union, List, Optional, Deque, TypedDict
from typing import Any, Deque, Dict, List, Optional, TypedDict, Union
from temporalio.common import RetryPolicy
from temporalio import workflow
from temporalio.common import RetryPolicy
from models.data_types import ConversationHistory, EnvLookupOutput, NextStep, ValidationInput, EnvLookupInput
from models.data_types import (
ConversationHistory,
EnvLookupInput,
EnvLookupOutput,
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
from workflows.workflow_helpers import (
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
)
with workflow.unsafe.imports_passed_through():
from activities.tool_activities import ToolActivities
from prompts.agent_prompt_generators import (
generate_genai_prompt
)
from models.data_types import (
CombinedInput,
ToolPromptInput,
)
from models.data_types import CombinedInput, ToolPromptInput
from prompts.agent_prompt_generators import generate_genai_prompt
from tools.goal_registry import goal_list
# Constants
MAX_TURNS_BEFORE_CONTINUE = 250
#ToolData as part of the workflow is what's accessible to the UI - see LLMResponse.jsx for example
# 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
@@ -33,6 +37,7 @@ class ToolData(TypedDict, total=False):
response: str
force_confirm: bool = True
@workflow.defn
class AgentGoalWorkflow:
"""Workflow that manages tool execution with user confirmation and conversation history."""
@@ -43,16 +48,21 @@ class AgentGoalWorkflow:
self.conversation_summary: Optional[str] = None
self.chat_ended: bool = False
self.tool_data: Optional[ToolData] = None
self.confirmed: bool = False # indicates that we have confirmation to proceed to run tool
self.confirmed: bool = (
False # indicates that we have confirmation to proceed to run tool
)
self.tool_results: List[Dict[str, Any]] = []
self.goal: AgentGoal = {"tools": []}
self.show_tool_args_confirmation: bool = True # set from env file in activity lookup_wf_env_settings
self.multi_goal_mode: bool = False # set from env file in activity lookup_wf_env_settings
self.show_tool_args_confirmation: bool = (
True # set from env file in activity lookup_wf_env_settings
)
self.multi_goal_mode: bool = (
False # set from env file in activity lookup_wf_env_settings
)
# see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set
@workflow.run
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
@@ -68,12 +78,12 @@ class AgentGoalWorkflow:
if params and params.prompt_queue:
self.prompt_queue.extend(params.prompt_queue)
waiting_for_confirm = False
waiting_for_confirm = False
current_tool = None
# This is the main interactive loop. Main responsibilities:
# - Selecting and changing goals as directed by the user
# - reacting to user input (from signals)
# - reacting to user input (from signals)
# - validating user input to make sure it makes sense with the current goal and tools
# - calling the LLM through activities to determine next steps and prompts
# - executing the selected tools via activities
@@ -87,7 +97,7 @@ class AgentGoalWorkflow:
if self.chat_should_end():
return f"{self.conversation_history}"
# Execute the tool
# Execute the tool
if self.ready_for_tool_execution(waiting_for_confirm, current_tool):
waiting_for_confirm = await self.execute_tool(current_tool)
continue
@@ -96,10 +106,12 @@ class AgentGoalWorkflow:
if self.prompt_queue:
# get most recent prompt
prompt = self.prompt_queue.popleft()
workflow.logger.info(f"workflow step: processing message on the prompt queue, message is {prompt}")
workflow.logger.info(
f"workflow step: processing message on the prompt queue, message is {prompt}"
)
# Validate user-provided prompts
if self.is_user_prompt(prompt):
if self.is_user_prompt(prompt):
self.add_message("user", prompt)
# Validate the prompt before proceeding
@@ -120,18 +132,25 @@ class AgentGoalWorkflow:
# 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
# If valid, proceed with generating the context and prompt
context_instructions = generate_genai_prompt(
agent_goal=self.goal,
conversation_history = self.conversation_history,
multi_goal_mode=self.multi_goal_mode,
raw_json=self.tool_data)
prompt_input = ToolPromptInput(prompt=prompt, context_instructions=context_instructions)
agent_goal=self.goal,
conversation_history=self.conversation_history,
multi_goal_mode=self.multi_goal_mode,
raw_json=self.tool_data,
)
prompt_input = ToolPromptInput(
prompt=prompt, context_instructions=context_instructions
)
# connect to LLM and execute to get next steps
tool_data = await workflow.execute_activity_method(
@@ -151,20 +170,24 @@ class AgentGoalWorkflow:
next_step = tool_data.get("next")
current_tool = tool_data.get("tool")
workflow.logger.info(f"next_step: {next_step}, current tool is {current_tool}")
workflow.logger.info(
f"next_step: {next_step}, current tool is {current_tool}"
)
# make sure we're ready to run the tool & have everything we need
if next_step == "confirm" and current_tool:
args = tool_data.get("args", {})
# if we're missing arguments, ask for them
if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue):
# if we're missing arguments, ask for them
if await helpers.handle_missing_args(
current_tool, args, tool_data, self.prompt_queue
):
continue
waiting_for_confirm = True
# We have needed arguments, if we want to force the user to confirm, set that up
# We have needed arguments, if we want to force the user to confirm, set that up
if self.show_tool_args_confirmation:
self.confirmed = False # set that we're not confirmed
self.confirmed = False # set that we're not confirmed
workflow.logger.info("Waiting for user confirm signal...")
# if we have all needed arguments (handled above) and not holding for a debugging confirm, proceed:
else:
@@ -174,14 +197,11 @@ class AgentGoalWorkflow:
workflow.logger.info("All steps completed. Resetting goal.")
self.change_goal("goal_choose_agent_type")
# else if the next step is to be done with the conversation such as if the user requests it via asking to "end conversation"
elif next_step == "done":
self.add_message("agent", tool_data)
#here we could send conversation to AI for analysis
# here we could send conversation to AI for analysis
# end the workflow
return str(self.conversation_history)
@@ -192,10 +212,10 @@ class AgentGoalWorkflow:
self.prompt_queue,
self.goal,
MAX_TURNS_BEFORE_CONTINUE,
self.add_message
self.add_message,
)
#Signal that comes from api/main.py via a post to /send-prompt
# 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."""
@@ -205,28 +225,28 @@ class AgentGoalWorkflow:
return
self.prompt_queue.append(prompt)
#Signal that comes from api/main.py via a post to /confirm
# 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 signal: confirmation")
self.confirmed = True
#Signal that comes from api/main.py via a post to /end-chat
# 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.info("signal received: end_chat")
self.chat_ended = True
#Signal that can be sent from Temporal Workflow UI to enable debugging confirm and override .env setting
# Signal that can be sent from Temporal Workflow UI to enable debugging confirm and override .env setting
@workflow.signal
async def enable_debugging_confirm(self) -> None:
"""Signal handler for enabling debugging confirm UI & associated logic."""
workflow.logger.info("signal received: enable_debugging_confirm")
self.enable_debugging_confirm = True
#Signal that can be sent from Temporal Workflow UI to disable debugging confirm and override .env setting
# Signal that can be sent from Temporal Workflow UI to disable debugging confirm and override .env setting
@workflow.signal
async def disable_debugging_confirm(self) -> None:
"""Signal handler for disabling debugging confirm UI & associated logic."""
@@ -237,7 +257,7 @@ 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."""
@@ -245,7 +265,7 @@ class AgentGoalWorkflow:
@workflow.query
def get_summary_from_history(self) -> Optional[str]:
"""Query handler to retrieve the conversation summary if available.
"""Query handler to retrieve the conversation summary if available.
Used only for continue as new of the workflow."""
return self.conversation_summary
@@ -272,9 +292,9 @@ class AgentGoalWorkflow:
)
def change_goal(self, goal: str) -> None:
""" Change the goal (usually on request of the user).
Args:
"""Change the goal (usually on request of the user).
Args:
goal: goal to change to)
"""
if goal is not None:
@@ -283,8 +303,9 @@ class AgentGoalWorkflow:
self.goal = listed_goal
workflow.logger.info("Changed goal to " + goal)
if goal is None:
workflow.logger.warning("Goal not set after goal reset, probably bad.") # if this happens, there's probably a problem with the goal list
workflow.logger.warning(
"Goal not set after goal reset, probably bad."
) # if this happens, there's probably a problem with the goal list
# workflow function that defines if chat should end
def chat_should_end(self) -> bool:
@@ -293,9 +314,11 @@ class AgentGoalWorkflow:
return True
else:
return False
# define if we're ready for tool execution
def ready_for_tool_execution(self, waiting_for_confirm: bool, current_tool: Any) -> bool:
def ready_for_tool_execution(
self, waiting_for_confirm: bool, current_tool: Any
) -> bool:
if self.confirmed and waiting_for_confirm and current_tool and self.tool_data:
return True
else:
@@ -304,19 +327,19 @@ class AgentGoalWorkflow:
# LLM-tagged prompts start with "###"
# all others are from the user
def is_user_prompt(self, prompt) -> bool:
if prompt.startswith("###"):
return False
else:
return True
if prompt.startswith("###"):
return False
else:
return True
# look up env settings in an activity so they're part of history
async def lookup_wf_env_settings(self, combined_input: CombinedInput)->None:
async def lookup_wf_env_settings(self, combined_input: CombinedInput) -> None:
env_lookup_input = EnvLookupInput(
show_confirm_env_var_name = "SHOW_CONFIRM",
show_confirm_default = True,
show_confirm_env_var_name="SHOW_CONFIRM",
show_confirm_default=True,
)
env_output:EnvLookupOutput = await workflow.execute_activity_method(
ToolActivities.get_wf_env_vars,
env_output: EnvLookupOutput = await workflow.execute_activity_method(
ToolActivities.get_wf_env_vars,
env_lookup_input,
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
retry_policy=RetryPolicy(
@@ -325,11 +348,13 @@ class AgentGoalWorkflow:
)
self.show_tool_args_confirmation = env_output.show_confirm
self.multi_goal_mode = env_output.multi_goal_mode
# execute the tool - return False if we're not waiting for confirm anymore (always the case if it works successfully)
#
async def execute_tool(self, current_tool: str)->bool:
workflow.logger.info(f"workflow step: user has confirmed, executing the tool {current_tool}")
#
async def execute_tool(self, current_tool: str) -> bool:
workflow.logger.info(
f"workflow step: user has confirmed, executing the tool {current_tool}"
)
self.confirmed = False
waiting_for_confirm = False
confirmed_tool_data = self.tool_data.copy()
@@ -342,21 +367,27 @@ class AgentGoalWorkflow:
self.tool_data,
self.tool_results,
self.add_message,
self.prompt_queue
self.prompt_queue,
)
# 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():
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")
self.change_goal(new_goal)
elif "ListAgents" in self.tool_results[-1].values() and self.goal.id != "goal_choose_agent_type":
elif (
"ListAgents" in self.tool_results[-1].values()
and self.goal.id != "goal_choose_agent_type"
):
self.change_goal("goal_choose_agent_type")
return waiting_for_confirm
# debugging helper - drop this in various places in the workflow to get status
# also don't forget you can look at the workflow itself and do queries if you want
def print_useful_workflow_vars(self, status_or_step:str) -> None:
def print_useful_workflow_vars(self, status_or_step: str) -> None:
print(f"***{status_or_step}:***")
if self.goal:
print(f"current goal: {self.goal.id}")
@@ -367,4 +398,3 @@ class AgentGoalWorkflow:
else:
print("no tool data initialized yet")
print(f"self.confirmed: {self.confirmed}")

View File

@@ -1,8 +1,9 @@
from datetime import timedelta
from typing import Dict, Any, Deque
from typing import Any, Deque, Dict
from temporalio import workflow
from temporalio.exceptions import ActivityError
from temporalio.common import RetryPolicy
from temporalio.exceptions import ActivityError
from models.data_types import ConversationHistory, ToolPromptInput
from prompts.agent_prompt_generators import (