From 7398d8dec6ea7d93bc03eb7d2725bd31ea9be97b Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Thu, 2 Jan 2025 10:21:34 -0800 Subject: [PATCH] prompt generator overhaul --- activities/tool_activities.py | 53 +------ prompts/agent_prompt_generators.py | 227 +++++++++++++---------------- scripts/run_worker.py | 2 - workflows/tool_workflow.py | 20 +-- 4 files changed, 115 insertions(+), 187 deletions(-) diff --git a/activities/tool_activities.py b/activities/tool_activities.py index cc3f753..298a8f6 100644 --- a/activities/tool_activities.py +++ b/activities/tool_activities.py @@ -3,7 +3,6 @@ from temporalio import activity from temporalio.exceptions import ApplicationError from ollama import chat, ChatResponse import json -from models.tool_definitions import ToolsData from typing import Sequence from temporalio.common import RawValue @@ -16,7 +15,7 @@ class ToolPromptInput: class ToolActivities: @activity.defn - def prompt_llm(self, input: ToolPromptInput) -> str: + def prompt_llm(self, input: ToolPromptInput) -> dict: model_name = "qwen2.5:14b" messages = [ { @@ -32,62 +31,14 @@ class ToolActivities: ] response: ChatResponse = chat(model=model_name, messages=messages) - return response.message.content - @activity.defn - def parse_tool_data(self, json_str: str) -> dict: - """ - Parses a JSON string into a dictionary. - Raises a ValueError if the JSON is invalid. - """ try: - data = json.loads(json_str) + data = json.loads(response.message.content) except json.JSONDecodeError as e: raise ApplicationError(f"Invalid JSON: {e}") return data - @activity.defn - def validate_and_parse_json( - self, - response_prechecked: str, - tools_data: ToolsData, - conversation_history: str, - ) -> dict: - """ - 1) Build JSON validation instructions - 2) Call LLM with those instructions - 3) Parse the result - 4) If parsing fails, raise exception -> triggers retry - """ - - # 1) Build validation instructions - # (Generate the validation prompt exactly as you do in your workflow.) - from prompts.agent_prompt_generators import ( - generate_json_validation_prompt_from_tools_data, - ) - - validation_prompt = generate_json_validation_prompt_from_tools_data( - tools_data, conversation_history, response_prechecked - ) - - # 2) Call LLM - prompt_input = ToolPromptInput( - prompt=response_prechecked, - context_instructions=validation_prompt, - ) - validated_response = self.prompt_llm(prompt_input) - - # 3) Parse - # If parse fails, we raise ApplicationError -> triggers retry - try: - parsed = self.parse_tool_data(validated_response) - except Exception as e: - raise ApplicationError(f"Failed to parse validated JSON: {e}") - - # 4) If we get here, parse succeeded - return parsed - def get_current_date_human_readable(): """ diff --git a/prompts/agent_prompt_generators.py b/prompts/agent_prompt_generators.py index 70c1a6a..bdf7c25 100644 --- a/prompts/agent_prompt_generators.py +++ b/prompts/agent_prompt_generators.py @@ -1,162 +1,145 @@ from models.tool_definitions import ToolsData +from typing import Optional +import json -def generate_genai_prompt_from_tools_data( - tools_data: ToolsData, conversation_history: str +def generate_genai_prompt( + tools_data: ToolsData, conversation_history: str, raw_json: Optional[str] = None ) -> str: """ - Generates a prompt describing the tools and the instructions for the AI - assistant, using the conversation history provided, allowing for multiple - tools and a 'done' state. + Generates json containing a unified prompt for an AI system to: + - Understand the conversation so far. + - Know which tools exist and their arguments. + - Produce or validate JSON instructions accordingly. + + :param tools_data: An object containing your tool definitions. + :param conversation_history: The user's conversation history. + :param raw_json: The existing JSON to validate/correct (if any). + :return: A json containing the merged instructions. """ + prompt_lines = [] + # Intro / Role prompt_lines.append( - "You are an AI assistant that must determine all required arguments " - "for the tools to achieve the user's goal. " + "You are an AI assistant that must produce or validate JSON instructions " + "for a set of tools in order to achieve the user's goals." ) prompt_lines.append("") + + # Conversation History + prompt_lines.append("=== Conversation History ===") prompt_lines.append( - "Conversation history so far. \nANALYZE THIS HISTORY TO DETERMINE WHICH ARGUMENTS TO PRE-FILL AS SPECIFIED FOR THE TOOL BELOW: " + "Analyze this history for context on tool usage, known arguments, and what's left to do." ) prompt_lines.append(conversation_history) prompt_lines.append("") - # List all tools and their arguments - prompt_lines.append("Available tools and their required arguments:") - for tool in tools_data.tools: - prompt_lines.append(f"- Tool name: {tool.name}") - prompt_lines.append(f" Description: {tool.description}") - prompt_lines.append(" Arguments needed:") - for arg in tool.arguments: - prompt_lines.append(f" - {arg.name} ({arg.type}): {arg.description}") - prompt_lines.append("") - - prompt_lines.append("Instructions:") - prompt_lines.append( - "1. You may call multiple tools in sequence if needed, each requiring certain arguments. " - "Ask the user for missing details when necessary. " - ) - prompt_lines.append( - "2. If you do not yet have a specific argument value, ask the user for it by setting 'next': 'question'." - ) - prompt_lines.append( - "3. Once you have enough information for a particular tool, respond with 'next': 'confirm' and include the tool name in 'tool'." - ) - prompt_lines.append( - "4. If you have completed all necessary tools (no more actions needed), use 'next': 'done' in your JSON response ." - ) - prompt_lines.append( - "5. Your response must be valid JSON in this format:\n" - " {\n" - ' "response": "",\n' - ' "next": "",\n' - ' "tool": "",\n' - ' "args": {\n' - ' "": "",\n' - ' "": "", ...\n' - " }\n" - " }\n" - " where 'args' are the arguments for the tool (or empty if not needed)." - ) - prompt_lines.append( - "6. If you still need information from the user, use 'next': 'question'. " - "If you have enough info for a specific tool, use 'next': 'confirm'. " - "Do NOT use 'next': 'confirm' until you have all necessary arguments (i.e. they're NOT 'null') ." - "If you are finished with all tools, use 'next': 'done'." - ) - prompt_lines.append( - "7. Keep responses in plain text. Return valid JSON without extra commentary." - ) - prompt_lines.append("") - prompt_lines.append( - "Begin by prompting or confirming the necessary details. If any are missing (null) ensure you ask for them." - ) - - return "\n".join(prompt_lines) - - -def generate_json_validation_prompt_from_tools_data( - tools_data: ToolsData, conversation_history: str, raw_json: str -) -> str: - """ - Generates a prompt instructing the AI to: - 1. Check that the given raw JSON is syntactically valid. - 2. Ensure the 'tool' matches one of the defined tools or is 'none' if no tool is needed. - 3. Confirm or correct that all required arguments are present or set to null if missing. - 4. Return a corrected JSON if possible. - 5. Accept 'next' as one of 'question', 'confirm', or 'done'. - """ - prompt_lines = [] - - prompt_lines.append( - "You are an AI assistant that must validate the following JSON." - ) - prompt_lines.append("It may be malformed or incomplete.") - prompt_lines.append("You also have a list of tools and their required arguments.") - prompt_lines.append( - "You must ensure the JSON is valid and matches these definitions." - ) - prompt_lines.append("") - - prompt_lines.append("== Tools Definitions ==") + # Tools Definitions + prompt_lines.append("=== Tools Definitions ===") for tool in tools_data.tools: prompt_lines.append(f"Tool name: {tool.name}") prompt_lines.append(f" Description: {tool.description}") - prompt_lines.append(" Arguments required:") + prompt_lines.append(" Required arguments:") for arg in tool.arguments: prompt_lines.append(f" - {arg.name} ({arg.type}): {arg.description}") prompt_lines.append("") - prompt_lines.append("== JSON to Validate ==") - prompt_lines.append(raw_json) - prompt_lines.append("") - - prompt_lines.append("Validation checks:") - prompt_lines.append("1. Is the JSON syntactically valid? If not, fix it.") + # Instructions for Generating JSON (Always Shown) + prompt_lines.append("=== Instructions for JSON Generation ===") prompt_lines.append( - "2. Does the 'tool' field match one of the tools above (or 'none')?" + "1. You may sequentially call multiple tools, each requiring specific arguments." ) prompt_lines.append( - "3. Do the 'args' correspond exactly to the required arguments for that tool? " - "If arguments are missing, set them to null or correct them if possible." + "2. If any required argument is missing, set 'next': 'question' and ask the user for it." ) prompt_lines.append( - "4. Check the 'response' field is present. The user-facing text can be corrected but not removed." + "3. Once all arguments for a tool are known, set 'next': 'confirm' with 'tool' set to that tool's name." ) + prompt_lines.append("4. If no further actions are required, set 'next': 'done'.") prompt_lines.append( - "5. 'next' should be one of 'question', 'confirm', or 'done' (if no more actions)." - "Do NOT use 'next': 'confirm' until you have all args. If there are any args that are null then next='question'). " - ) - prompt_lines.append( - "6. If any of args is 'null' then ensure next = 'question' and that your response asks for this information from the user. " - ) - prompt_lines.append( - "7. If all tools mentioned above have 'completed successfully' (check the history) then next should be 'done'. " - ) - prompt_lines.append( - "Use the conversation history to parse known data for filling 'args' if possible. " - ) - prompt_lines.append("") - prompt_lines.append( - "Return only valid JSON in the format:\n" + "5. Always respond with valid JSON in this format:\n" "{\n" - ' "response": "...",\n' - ' "next": "question|confirm|done",\n' - ' "tool": "",\n' - ' "args": { ... }\n' + ' "response": "",\n' + ' "next": "",\n' + ' "tool": "",\n' + ' "args": {\n' + ' "": "",\n' + ' "": "",\n' + " ...\n" + " }\n" "}" ) prompt_lines.append( - "No additional commentary or explanation. Just the corrected JSON. " + "6. Use 'next': 'question' if you lack any required arguments based on the history and prompt. " + "Use 'next': 'confirm' only if NO arguments are missing. " + "Use 'next': 'done' if no more tool calls are needed." ) - prompt_lines.append("") - prompt_lines.append("Conversation history so far:") - prompt_lines.append(conversation_history) prompt_lines.append( - "\nIMPORTANT: ANALYZE THIS HISTORY TO DETERMINE WHICH ARGUMENTS TO PRE-FILL IN THE JSON RESPONSE. " + "7. Keep 'response' user-friendly with no extra commentary. Stick to valid JSON syntax. " + "Your goal is to guide the user through the running of these tools and elicit missing information." ) prompt_lines.append("") - prompt_lines.append("Begin validating now. ") + + # Instructions for Validation (Only if raw_json is provided) + if raw_json is not None: + prompt_lines.append("=== Validation Task ===") + prompt_lines.append( + "We have an existing JSON that may be malformed or incomplete. Validate and correct if needed." + ) + prompt_lines.append("") + prompt_lines.append("=== JSON to Validate ===") + prompt_lines.append(json.dumps(raw_json, indent=2)) + prompt_lines.append("") + prompt_lines.append("Validation Checks:") + prompt_lines.append("1. Fix any JSON syntax errors.") + prompt_lines.append("2. Ensure 'tool' is one of the defined tools or 'none'.") + prompt_lines.append( + "3. Check 'args' matches the required arguments for that tool; fill in from context or set null if unknown." + ) + prompt_lines.append("4. Ensure 'response' is present (plain user-facing text).") + prompt_lines.append( + "5. Ensure 'next' is one of 'question', 'confirm', 'done'. " + "Use 'question' if required args are still null, 'confirm' if all args are set, " + "and 'done' if no more actions remain." + ) + prompt_lines.append( + "6. Use the conversation history to see if arguments can be inferred." + ) + prompt_lines.append( + "7. Return only the fixed JSON if changes are required, with no extra commentary." + ) + + # Final Guidance + if raw_json is not None: + prompt_lines.append("") + prompt_lines.append( + "Begin by validating (and correcting) the JSON above, if needed." + ) + else: + prompt_lines.append("") + prompt_lines.append( + "Begin by generating a valid JSON response for the next step." + ) + + prompt_lines.append( + "REMINDER: If any required argument is missing, set 'next': 'question' and ask the user for it." + ) + prompt_lines.append( + """ + Example JSON: + { + "args": { + "dateDepart": "2025-03-26", + "dateReturn": "2025-04-20", + "destination": "Melbourne", + "origin": null + }, + "next": "question", + "response": "I need to know where you're flying from. What's your departure city?", + "tool": "SearchFlights" + } + """ + ) return "\n".join(prompt_lines) diff --git a/scripts/run_worker.py b/scripts/run_worker.py index 7213f9c..48f4149 100644 --- a/scripts/run_worker.py +++ b/scripts/run_worker.py @@ -22,8 +22,6 @@ async def main(): workflows=[ToolWorkflow], activities=[ activities.prompt_llm, - activities.parse_tool_data, - activities.validate_and_parse_json, dynamic_tool_activity, ], activity_executor=activity_executor, diff --git a/workflows/tool_workflow.py b/workflows/tool_workflow.py index 6425f31..1bf4d6a 100644 --- a/workflows/tool_workflow.py +++ b/workflows/tool_workflow.py @@ -8,7 +8,7 @@ from temporalio import workflow with workflow.unsafe.imports_passed_through(): from activities.tool_activities import ToolActivities, ToolPromptInput from prompts.agent_prompt_generators import ( - generate_genai_prompt_from_tools_data, + generate_genai_prompt, ) from models.data_types import CombinedInput, ToolWorkflowParams @@ -27,6 +27,7 @@ class ToolWorkflow: async def run(self, combined_input: CombinedInput) -> str: params = combined_input.tool_params tools_data = combined_input.tools_data + tool_data = None if params and params.conversation_summary: self.conversation_history.append( @@ -68,25 +69,20 @@ class ToolWorkflow: self.conversation_history.append(("user", prompt)) # 3) Call the LLM with the entire conversation + Tools - context_instructions = generate_genai_prompt_from_tools_data( - tools_data, self.format_history() + context_instructions = generate_genai_prompt( + tools_data, self.format_history(), tool_data ) prompt_input = ToolPromptInput( prompt=prompt, context_instructions=context_instructions, ) - responsePrechecked = await workflow.execute_activity_method( + tool_data = await workflow.execute_activity_method( ToolActivities.prompt_llm, prompt_input, schedule_to_close_timeout=timedelta(seconds=20), - ) - - # 4) Validate + parse in one shot - tool_data = await workflow.execute_activity_method( - ToolActivities.validate_and_parse_json, - args=[responsePrechecked, tools_data, self.format_history()], - schedule_to_close_timeout=timedelta(seconds=40), - retry_policy=RetryPolicy(initial_interval=timedelta(seconds=10)), + retry_policy=RetryPolicy( + maximum_attempts=5, initial_interval=timedelta(seconds=15) + ), ) # 5) Store it and show the conversation