mirror of
https://github.com/temporal-community/temporal-ai-agent.git
synced 2026-03-15 14:08:08 +01:00
documentation & guidance updates, getting things done, fixing a possible NDE if you change env vars, changes to enable user picking "done", minor test changes, minor goal selection prompt improvements
This commit is contained in:
@@ -35,8 +35,8 @@ OPENAI_API_KEY=sk-proj-...
|
||||
# Uncomment if using API key (not needed for local dev server)
|
||||
# TEMPORAL_API_KEY=abcdef1234567890
|
||||
|
||||
# Set starting goal of agent
|
||||
AGENT_GOAL=goal_choose_agent_type # default, for multi-goal
|
||||
# Set starting goal of agent - if unset default is all
|
||||
AGENT_GOAL=goal_choose_agent_type # default for multi-goal start
|
||||
#AGENT_GOAL=goal_event_flight_invoice # for original goal
|
||||
#AGENT_GOAL=goal_match_train_invoice # for replay goal
|
||||
|
||||
@@ -44,7 +44,7 @@ AGENT_GOAL=goal_choose_agent_type # default, for multi-goal
|
||||
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin # default is all
|
||||
#GOAL_CATEGORIES=travel-flights
|
||||
|
||||
# Set if the UI should force a user confirmation step or not
|
||||
# Set if the workflow should wait for the user to click a confirm button (and if the UI should show the confirm button and tool args)
|
||||
SHOW_CONFIRM=True
|
||||
|
||||
# Money Scenarios:
|
||||
|
||||
12
README.md
12
README.md
@@ -12,6 +12,18 @@ It's really helpful to [watch the demo (5 minute YouTube video)](https://www.you
|
||||
There are a lot of AI and Agentic AI tools out there, and more on the way. But why Temporal? Temporal gives this system reliablity, state management, a code-first approach that we really like, built-in observability and easy error handling.
|
||||
For more, check out [architecture-decisions](./architecture-decisions.md).
|
||||
|
||||
## What is "Agentic AI"?
|
||||
These are the key elements of an agentic framework:
|
||||
1. Goals a human can get done, made up of tools that can execute individual steps
|
||||
2. The "agent loop" - call LLM, either call tools or prompt human, repeat until goal(s) are done
|
||||
3. Support for tool calls that require human input and approval
|
||||
4. Use of an LLM to check human input for relevance before calling the 'real' LLM
|
||||
5. use of an LLM to summarize and compact the conversation history
|
||||
6. Prompt construction (made of system prompts, conversation history, and tool metadata - sent to the LLM to create user prompts)
|
||||
7. Bonus: durable tool execution via Temporal Activities
|
||||
|
||||
For a deeper dive into this, check out the [architecture guide](./architecture.md).
|
||||
|
||||
## Setup and Configuration
|
||||
See [the Setup guide](./setup.md).
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import google.generativeai as genai
|
||||
import anthropic
|
||||
import deepseek
|
||||
from dotenv import load_dotenv
|
||||
from models.data_types import ValidationInput, ValidationResult, ToolPromptInput
|
||||
from models.data_types import ValidationInput, ValidationResult, ToolPromptInput, EnvLookupInput
|
||||
|
||||
load_dotenv(override=True)
|
||||
print(
|
||||
@@ -471,6 +471,20 @@ class ToolActivities:
|
||||
print(f"Full response: {response_content}")
|
||||
raise
|
||||
|
||||
# get env vars for workflow
|
||||
@activity.defn
|
||||
async def get_env_bool(self, input: EnvLookupInput) -> bool:
|
||||
""" gets boolean env vars for workflow as an activity result so it's deterministic
|
||||
handles default/None
|
||||
"""
|
||||
value = os.getenv(input.env_var_name)
|
||||
if value is None:
|
||||
return input.default
|
||||
if value is not None and value.lower() == "false":
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def get_current_date_human_readable():
|
||||
"""
|
||||
@@ -487,8 +501,6 @@ def get_current_date_human_readable():
|
||||
async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||
from tools import get_handler
|
||||
|
||||
# if current_tool == "move_money":
|
||||
# workflow.logger.warning(f"trying for move_money direct")
|
||||
tool_name = activity.info().activity_type # e.g. "FindEvents"
|
||||
tool_args = activity.payload_converter().from_payload(args[0].payload, dict)
|
||||
activity.logger.info(f"Running dynamic tool '{tool_name}' with args: {tool_args}")
|
||||
@@ -503,3 +515,5 @@ async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||
# Optionally log or augment the result
|
||||
activity.logger.info(f"Tool '{tool_name}' result: {result}")
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
## Customizing the Agent
|
||||
# Customizing the Agent
|
||||
The agent is set up to allow for multiple goals and to switch back to choosing a new goal at the end of every successful goal. A goal is made up of a list of tools that the agent will guide the user through.
|
||||
|
||||
It may be helpful to review the [architecture](./architecture.md) for a guide and definition of goals, tools, etc.
|
||||
|
||||
### Adding a New Goal Category
|
||||
## Adding a New Goal Category
|
||||
Goal Categories lets you pick which groups of goals to show. Set via an .env setting, GOAL_CATEGORIES.
|
||||
1. Pick a unique one that has some business meaning
|
||||
2. Use it in your .env file
|
||||
3. Add to [.env.example](./.env.example)
|
||||
4. Use it in your Goal definition, see below.
|
||||
|
||||
### Adding a Goal
|
||||
## Adding a Goal
|
||||
1. Open [/tools/goal_registry.py](tools/goal_registry.py) - this file contains descriptions of goals and the tools used to achieve them
|
||||
2. Pick a name for your goal! (such as "goal_hr_schedule_pto")
|
||||
3. Fill out the required elements:
|
||||
@@ -34,9 +34,9 @@ tools=[
|
||||
- `example_conversation_history`: LLM-facing sample conversation/interaction regarding the goal. See the existing goals for how to structure this.
|
||||
4. Add your new goal to the `goal_list` at the bottom using `goal_list.append(your_super_sweet_new_goal)`
|
||||
|
||||
### Adding Tools
|
||||
## Adding Tools
|
||||
|
||||
#### Notes
|
||||
### Optional Tools
|
||||
Tools can be optional - you can indicate this in the tool listing of goal description (see above section re: goal registry) by adding something like, "This step is optional and can be skipped by moving to the next tool." Here is an example from an older iteration of the `goal_hr_schedule_pto` goal, when it was going to have an optional step to check for existing calendar conflicts:
|
||||
|
||||
```
|
||||
@@ -47,24 +47,42 @@ description="Help the user gather args for these tools in order: "
|
||||
"4. BookPTO: Book PTO "
|
||||
```
|
||||
|
||||
#### Add to Tool Registry
|
||||
Tools should generally return meaningful information and be generally ‘failsafe’ in returning a useful result based on input.
|
||||
(If you're doing a local data approach like those in [.tools/data/](./tools/data/)) it's good to document how they can be setup to get a good result in tool specific [setup](./setup.md).
|
||||
|
||||
### Add to Tool Registry
|
||||
1. Open [/tools/tool_registry.py](tools/tool_registry.py) - this file contains mapping of tool names to tool definitions (so the AI understands how to use them)
|
||||
2. Define the tool
|
||||
- `name`: name of the tool - this is the name as defined in the goal description list of tools. The name should be (sort of) the same as the tool name given in the goal description. So, if the description lists "CurrentPTO" as a tool, the name here should be `current_pto_tool`.
|
||||
- `description`: LLM-facing description of tool
|
||||
- `arguments`: These are the _input_ arguments to the tool. Each input argument should be defined as a [ToolArgument](./models/tool_definitions.py). Tools don't have to have arguments but the arguments list has to be declared. If the tool you're creating doesn't have inputs, define arguments as `arguments=[]`
|
||||
|
||||
#### Create Each Tool
|
||||
### Create Each Tool
|
||||
- The tools themselves are defined in their own files in `/tools` - you can add a subfolder to organize them, see the hr tools for an example.
|
||||
- The file name and function name will be the same as each other and should also be the same as the name of the tool, without "tool" - so `current_pto_tool` would be `current_pto.py` with a function named `current_pto` within it.
|
||||
- The function should have `args: dict` as the input and also return a `dict`
|
||||
- The return dict should match the output format you specified in the goal's `example_conversation_history`
|
||||
- tools are where the user input+model output becomes deterministic. Add validation here to make sure what the system is doing is valid and acceptable
|
||||
|
||||
#### Add to `tools/__init__.py` and the tool get_handler()
|
||||
### Add to `tools/__init__.py` and the tool get_handler()
|
||||
- In [tools/__init__.py](./tools/__init__.py), add an import statement for each new tool as well as an applicable return statement in `get_handler`. The tool name here should match the tool name as described in the goal's `description` field.
|
||||
Example:
|
||||
```
|
||||
if tool_name == "CurrentPTO":
|
||||
return current_pto
|
||||
```
|
||||
## Tool Confirmation
|
||||
There are three ways to manage confirmation of tool runs:
|
||||
1. Arguments confirmation box - confirm tool arguments and execution with a button click
|
||||
- Can be disabled by env setting: `SHOW_CONFIRM=FALSE`
|
||||
2. Soft prompt confirmation via asking the model to prompt for confirmation: “Are you ready to be invoiced for the total cost of the train tickets?” in the [goal_registry](./tools/goal_registry.py).
|
||||
3. Hard confirmation requirement as a tool argument. See for example the PTO Scheduling Tool:
|
||||
```Python
|
||||
ToolArgument(
|
||||
name="userConfirmation",
|
||||
type="string",
|
||||
description="Indication of user's desire to book PTO",
|
||||
),
|
||||
```
|
||||
If you really want to wait for user confirmation, record it on the workflow (as a Signal) and not rely on the LLM to probably get it, use option #3.
|
||||
I recommend exploring all three. For a demo, I would decide if you want the Arguments confirmation in the UI, and if not I'd generally go with option #2 but use #3 for tools that make business sense to confirm, e.g. those tools that take action/write data.
|
||||
@@ -42,3 +42,8 @@ class ValidationResult:
|
||||
# Initialize empty dict if None
|
||||
if self.validationFailedReason is None:
|
||||
self.validationFailedReason = {}
|
||||
|
||||
@dataclass
|
||||
class EnvLookupInput:
|
||||
env_var_name: str
|
||||
default: bool
|
||||
@@ -126,7 +126,9 @@ def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) ->
|
||||
'{"next": "<question|confirm|pick-new-goal|done>", "tool": "<tool_name or null>", "args": {"<arg1>": "<value1 or null>", "<arg2>": "<value2 or null>}, "response": "<plain text (can include \\n line breaks)>"}'
|
||||
"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 be "done" if the user is asking to be done with the chat. '
|
||||
'Next should only be "pick-new-goal" if all tools have been run (use the system prompt to figure that out).'
|
||||
#'If all tools have been run (use the system prompt to figure that out) then clear tool history.' todo maybe fix this
|
||||
)
|
||||
|
||||
def generate_missing_args_prompt(current_tool: str, tool_data: dict, missing_args: list[str]) -> str:
|
||||
|
||||
@@ -62,6 +62,7 @@ async def main():
|
||||
activities=[
|
||||
activities.agent_validatePrompt,
|
||||
activities.agent_toolPlanner,
|
||||
activities.get_env_bool,
|
||||
dynamic_tool_activity,
|
||||
],
|
||||
activity_executor=activity_executor,
|
||||
|
||||
11
setup.md
11
setup.md
@@ -8,7 +8,8 @@ cp .env.example .env
|
||||
```
|
||||
|
||||
Then add API keys, configuration, as desired.
|
||||
If you want to show confirmations/enable the debugging UI, set
|
||||
|
||||
If you want to show confirmations/enable the debugging UI that shows tool args, set
|
||||
```bash
|
||||
SHOW_CONFIRM=True
|
||||
```
|
||||
@@ -188,13 +189,19 @@ 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`.
|
||||
|
||||
#### Goals: Money Movement
|
||||
#### Goals: FIN/Money Movement
|
||||
Make sure you have the mock users you want (such as yourself) in [the account mock data file](./tools/data/customer_account_data.json).
|
||||
|
||||
- `AGENT_GOAL=goal_fin_move_money` - This scenario _can_ initiate a secondary workflow to move money. Check out [this repo](https://github.com/temporal-sa/temporal-money-transfer-java) - you'll need to get the worker running and connected to the same account as the agentic worker.
|
||||
By default it will _not_ make a real workflow, it'll just fake it. If you get the worker running and want to start a workflow, in your [.env](./.env):
|
||||
```bash
|
||||
FIN_START_REAL_WORKFLOW=FALSE #set this to true to start a real workflow
|
||||
```
|
||||
|
||||
#### Goals: HR/PTO
|
||||
Make sure you have the mock users you want in (such as yourself) in [the PTO mock data file](./tools/data/employee_pto_data.json).
|
||||
|
||||
|
||||
## Customizing the Agent Further
|
||||
- `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
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||
from activities.tool_activities import ToolActivities, dynamic_tool_activity
|
||||
|
||||
|
||||
@@ -17,12 +17,7 @@ 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)
|
||||
async def test_flight_booking(client: Client):
|
||||
|
||||
task_queue_name = "agent-ai-workflow"
|
||||
workflow_id = "agent-workflow"
|
||||
@@ -37,16 +32,19 @@ async def test_workflow_success(client: Client):
|
||||
|
||||
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]):
|
||||
|
||||
# todo set goal categories for scenarios
|
||||
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")
|
||||
# todo send signals based on
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "book flights")
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "sydney in september")
|
||||
assert WorkflowExecutionStatus.RUNNING == (await handle.describe()).status
|
||||
|
||||
await handle.signal(AgentGoalWorkflow.exit)
|
||||
assert ["Hello, user1", "Hello, user2"] == await handle.result()
|
||||
|
||||
#assert ["Hello, user1", "Hello, user2"] == await handle.result()
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "I'm all set, end conversation")
|
||||
assert WorkflowExecutionStatus.COMPLETED == (await handle.describe()).status
|
||||
|
||||
|
||||
|
||||
40
todo.md
40
todo.md
@@ -1,27 +1,45 @@
|
||||
# todo list
|
||||
[ ] mergey stuffs <br />
|
||||
- [x] make confirm work how you want when force_confirm is on and off <br />
|
||||
- [x] test with confirm on and off - single goal <br />
|
||||
- [x] confirmation off-> it's unclear it's asking for confirmation unless we set `self.confirm = False` in the workflow - maybe we should take the args/confirm route? - test with confirm on box - test with book PTO<br />
|
||||
- [x] test with confirm on and off - multi goal <br />
|
||||
- [x] documenting confirm <br />
|
||||
- [x] document how to do debugging confirm force confirm with toolchain in setup and adding-goals-and-tools <br />
|
||||
- [x] document how to do optional confirm at goal/tool level <br />
|
||||
- [ ] goal change management tweaks <br />
|
||||
- [ ] maybe make the choose_Agent_goal tag not be system/not always included? <br />
|
||||
- [ ] try taking out list-agents as a tool because agent_prompt_generators may do it for you <br />
|
||||
- [ ] make goal selection not be a system tool but be an option in .env, see how that works, includes taking it out of the goal/toolset for all goals <br />
|
||||
- [x] make the goal selection/capabilities work how you want <br />
|
||||
- [x] make end-conversation work when force_confirm is on and off <br />
|
||||
|
||||
|
||||
- [x] make tool selection work when force_confirm is on and off <br />
|
||||
|
||||
- [x] updates to PTO and money movement setup docs re data file <br />
|
||||
- [x] fixing NDE about changing force_confirm <br />
|
||||
- [x] rename self.confirm to self.confirmed to be clearer
|
||||
- [x] rename show_confirm to show_confirm_and_tool_args
|
||||
|
||||
- [x] remove print debugging and todo comments
|
||||
[ ] expand [tests](./tests/agent_goal_workflow_test.py)<br />
|
||||
[ ] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py) <br />
|
||||
[x] make agent respond to name of goals and not just numbers <br />
|
||||
[x] josh to do fintech scenarios <br />
|
||||
[ ] expand [tests](./tests/agent_goal_workflow_test.py)<br />
|
||||
|
||||
|
||||
[ ] fintech goals <br />
|
||||
- Fraud Detection and Prevention - The AI monitors transactions across accounts, flagging suspicious activities (e.g., unusual spending patterns or login attempts) and autonomously freezing accounts or notifying customers and compliance teams.<br />
|
||||
- Personalized Financial Advice - An AI agent analyzes a customer’s financial data (e.g., income, spending habits, savings, investments) and provides tailored advice, such as budgeting tips, investment options, or debt repayment strategies.<br />
|
||||
- Portfolio Management and Rebalancing - The AI monitors a customer’s investment portfolio, rebalancing it automatically based on market trends, risk tolerance, and financial goals (e.g., shifting assets between stocks, bonds, or crypto).<br />
|
||||
[x] money movement - start money transfer <br />
|
||||
[x] todo use env vars to do connect to local or non-local
|
||||
[x] account balance - <br />
|
||||
|
||||
[ ] new loan/fraud check/update with start <br />
|
||||
|
||||
|
||||
[ ] 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 <br />
|
||||
- Insight into the agent’s performance <br />
|
||||
|
||||
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br />
|
||||
|
||||
[ ] add visual feedback when workflow starting <br />
|
||||
[ ] figure out how to allow user to list agents at any time - like end conversation <br />
|
||||
|
||||
[ ] change initial goal selection prompt to list capabilities and prompt more nicely - not a bulleted list - see how that works
|
||||
|
||||
[x] todo use env vars to do connect to local or non-local cloud for activities for money scenarios
|
||||
[ ] enable user to list agents at any time - like end conversation - probably with a next step<br />
|
||||
- with changing "'Next should only be "pick-new-goal" if all tools have been run (use the system prompt to figure that out).'" in [prompt_generators](./prompts/agent_prompt_generators.py).
|
||||
@@ -11,6 +11,16 @@
|
||||
"email": "laine@awesome.com",
|
||||
"currentPTOHrs": 40,
|
||||
"hrsAddedPerMonth": 12
|
||||
},
|
||||
{
|
||||
"email": "steve.this.is.for.you@gmail.com",
|
||||
"currentPTOHrs": 4000,
|
||||
"hrsAddedPerMonth": 20
|
||||
},
|
||||
{
|
||||
"email": "your_email_here@yourcompany.com",
|
||||
"currentPTOHrs": 150,
|
||||
"hrsAddedPerMonth": 19
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ goal_choose_agent_type = AgentGoal(
|
||||
"agent: Here are the currently available agents.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on ListAgents tool>",
|
||||
"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. \n Which agent would you like to speak to?",
|
||||
"user: 1",
|
||||
"agent: The available agents are: 1. Event Flight Finder. \n Which agent would you like to speak to (?",
|
||||
"user: 1, Event Flight Finder",
|
||||
"user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>",
|
||||
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
|
||||
]
|
||||
@@ -153,7 +153,7 @@ goal_event_flight_invoice = AgentGoal(
|
||||
tool_registry.find_events_tool,
|
||||
tool_registry.search_flights_tool,
|
||||
tool_registry.create_invoice_tool,
|
||||
tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end
|
||||
#tool_registry.list_agents_tool, #last tool must be list_agents to faciliate 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 "
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Dict, Any, Union, List, Optional, Deque, TypedDict
|
||||
from temporalio.common import RetryPolicy
|
||||
from temporalio import workflow
|
||||
|
||||
from models.data_types import ConversationHistory, NextStep, ValidationInput
|
||||
from models.data_types import ConversationHistory, NextStep, ValidationInput, EnvLookupInput
|
||||
from models.tool_definitions import AgentGoal
|
||||
from workflows.workflow_helpers import LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT, \
|
||||
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT
|
||||
@@ -26,12 +26,6 @@ with workflow.unsafe.imports_passed_through():
|
||||
# Constants
|
||||
MAX_TURNS_BEFORE_CONTINUE = 250
|
||||
|
||||
show_confirm_env = os.getenv("SHOW_CONFIRM")
|
||||
if show_confirm_env is not None and show_confirm_env.lower() == "false":
|
||||
SHOW_CONFIRM = False
|
||||
else:
|
||||
SHOW_CONFIRM = True
|
||||
|
||||
#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
|
||||
@@ -50,9 +44,10 @@ class AgentGoalWorkflow:
|
||||
self.conversation_summary: Optional[str] = None
|
||||
self.chat_ended: bool = False
|
||||
self.tool_data: Optional[ToolData] = None
|
||||
self.confirm: bool = False
|
||||
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
|
||||
|
||||
# see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set
|
||||
@workflow.run
|
||||
@@ -63,6 +58,8 @@ class AgentGoalWorkflow:
|
||||
params = combined_input.tool_params
|
||||
self.goal = combined_input.agent_goal
|
||||
|
||||
await self.lookup_wf_env_settings(combined_input)
|
||||
|
||||
# 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)
|
||||
@@ -83,7 +80,7 @@ class AgentGoalWorkflow:
|
||||
while True:
|
||||
# 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
|
||||
lambda: bool(self.prompt_queue) or self.chat_ended or self.confirmed
|
||||
)
|
||||
|
||||
# handle chat should end. When chat ends, push conversation history to workflow results.
|
||||
@@ -141,7 +138,8 @@ class AgentGoalWorkflow:
|
||||
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||
),
|
||||
)
|
||||
tool_data["force_confirm"] = SHOW_CONFIRM
|
||||
|
||||
tool_data["force_confirm"] = self.show_tool_args_confirmation
|
||||
self.tool_data = tool_data
|
||||
|
||||
# process the tool as dictated by the prompt response - what to do next, and with which tool
|
||||
@@ -150,30 +148,35 @@ class AgentGoalWorkflow:
|
||||
|
||||
workflow.logger.info(f"next_step: {next_step}, current tool is {current_tool}")
|
||||
|
||||
#if the next step is to confirm...
|
||||
# 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, go back to the top of the loop
|
||||
# if we're missing arguments, ask for them
|
||||
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
|
||||
if SHOW_CONFIRM:
|
||||
self.confirm = False
|
||||
|
||||
# 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
|
||||
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:
|
||||
#theory - set self.confirm to true bc that's the signal, so we can get around the signal??
|
||||
self.confirm = True
|
||||
self.confirmed = 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.")
|
||||
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"
|
||||
# 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)
|
||||
|
||||
#todo send conversation to AI for analysis
|
||||
|
||||
# end the workflow
|
||||
return str(self.conversation_history)
|
||||
|
||||
@@ -198,10 +201,10 @@ class AgentGoalWorkflow:
|
||||
|
||||
#Signal that comes from api/main.py via a post to /confirm
|
||||
@workflow.signal
|
||||
async def confirm(self) -> None:
|
||||
async def confirmed(self) -> None:
|
||||
"""Signal handler for user confirmation of tool execution."""
|
||||
workflow.logger.info("Received user signal: confirmation")
|
||||
self.confirm = True
|
||||
self.confirmed = True
|
||||
|
||||
#Signal that comes from api/main.py via a post to /end-chat
|
||||
@workflow.signal
|
||||
@@ -210,6 +213,20 @@ class AgentGoalWorkflow:
|
||||
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
|
||||
@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
|
||||
@workflow.signal
|
||||
async def disable_debugging_confirm(self) -> None:
|
||||
"""Signal handler for disabling debugging confirm UI & associated logic."""
|
||||
workflow.logger.info("signal received: disable_debugging_confirm")
|
||||
self.enable_debugging_confirm = False
|
||||
|
||||
@workflow.query
|
||||
def get_conversation_history(self) -> ConversationHistory:
|
||||
"""Query handler to retrieve the full conversation history."""
|
||||
@@ -274,7 +291,7 @@ class AgentGoalWorkflow:
|
||||
|
||||
# define if we're ready for tool execution
|
||||
def ready_for_tool_execution(self, waiting_for_confirm: bool, current_tool: Any) -> bool:
|
||||
if self.confirm and waiting_for_confirm and current_tool and self.tool_data:
|
||||
if self.confirmed and waiting_for_confirm and current_tool and self.tool_data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -287,11 +304,23 @@ class AgentGoalWorkflow:
|
||||
else:
|
||||
return True
|
||||
|
||||
# look up env settings as needed in activities so they're part of history
|
||||
async def lookup_wf_env_settings(self, combined_input: CombinedInput)->None:
|
||||
env_lookup_input = EnvLookupInput(env_var_name = "SHOW_CONFIRM", default = True)
|
||||
self.show_tool_args_confirmation = await workflow.execute_activity(
|
||||
ToolActivities.get_env_bool,
|
||||
env_lookup_input,
|
||||
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||
),
|
||||
)
|
||||
|
||||
# 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}")
|
||||
self.confirm = False
|
||||
self.confirmed = False
|
||||
waiting_for_confirm = False
|
||||
confirmed_tool_data = self.tool_data.copy()
|
||||
confirmed_tool_data["next"] = "user_confirmed_tool_run"
|
||||
@@ -317,5 +346,13 @@ class AgentGoalWorkflow:
|
||||
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:
|
||||
print(f"***{status_or_step}:***")
|
||||
print(f"force confirm? {self.tool_data['force_confirm']}")
|
||||
print(f"next step: {self.tool_data.get('next')}")
|
||||
print(f"current_tool: {self.tool_data.get('tool')}")
|
||||
print(f"self.confirm: {self.confirmed}")
|
||||
print(f"waiting_for_confirm (about to be set to true): {self.waiting_for_confirm}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user