From 5b58f30e0da97c0aec96d928115dbe03baa74d5d Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Fri, 11 Apr 2025 17:43:34 -0400 Subject: [PATCH 1/4] wip checkin --- adding-goals-and-tools.md | 20 ++++++++++++++++---- setup.md | 7 ++++++- todo.md | 2 ++ tools/__init__.py | 5 ++++- tools/fin/move_money.py | 1 - tools/goal_registry.py | 35 ++++++++++++++++++++++++++++++++++- tools/tool_registry.py | 19 +++++++++++++++++++ 7 files changed, 81 insertions(+), 8 deletions(-) diff --git a/adding-goals-and-tools.md b/adding-goals-and-tools.md index 910b39d..6ec806a 100644 --- a/adding-goals-and-tools.md +++ b/adding-goals-and-tools.md @@ -4,9 +4,10 @@ The agent is set up to allow for multiple goals and to switch back to choosing a It may be helpful to review the [architecture](./architecture.md) for a guide and definition of goals, tools, etc. ## Adding a New Goal Category -Goal Categories lets you pick which groups of goals to show. Set via an .env setting, GOAL_CATEGORIES. +Goal Categories lets you pick which groups of goals to show. Set via an .env setting, `GOAL_CATEGORIES`. +Even if you don't intend to use the goal in a multi-goal scenario, goal categories are useful for others. 1. Pick a unique one that has some business meaning -2. Use it in your .env file +2. Use it in your [.env](./.env) file 3. Add to [.env.example](./.env.example) 4. Use it in your Goal definition, see below. @@ -35,7 +36,7 @@ tools=[ ## Adding Tools -### Optional Tools +### Note on 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: ``` @@ -84,4 +85,15 @@ There are three ways to manage confirmation of tool runs: ), ``` 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. \ No newline at end of file +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. + +## Add a Goal & Tools Checklist +[ ] Add goal in [/tools/goal_registry.py](tools/goal_registry.py)
+- [ ] If a new category, add Goal Category to [.env](./.env) and [.env.example](./.env.example)
+- [ ] don't forget the goal list at the bottom of the [goal_registry.py](tools/goal_registry.py)
+ +[ ] Add Tools listed in the Goal Registry to the [tool_registry.py](tools/tool_registry.py)
+[ ] Define your tools as Activities in `/tools`
+[ ] Add your tools to [tool list](tools/__init__.py) in the tool get_handler()
+ +And that's it! Happy AI Agent building! diff --git a/setup.md b/setup.md index a31df80..a94f989 100644 --- a/setup.md +++ b/setup.md @@ -192,7 +192,7 @@ 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: FIN/Money Movement +#### Goals: FIN - Money Movement and Loan Application 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. @@ -200,6 +200,11 @@ By default it will _not_ make a real workflow, it'll just fake it. If you get th ```bash FIN_START_REAL_WORKFLOW=FALSE #set this to true to start a real workflow ``` +- `AGENT_GOAL=goal_fin_loan_application` - This scenario _can_ initiate a secondary workflow to apply for a loan. Check out [this repo](https://github.com/temporal-sa/temporal-latency-optimization-scenarios) - 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). diff --git a/todo.md b/todo.md index b43dd41..1c2027a 100644 --- a/todo.md +++ b/todo.md @@ -7,6 +7,8 @@ - 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).
[ ] new loan/fraud check/update with start
+[ ] financial advise - args being freeform customer input about their financial situation, goals + [ ] tool is maybe a new tool asking the LLM to advise [ ] LLM failure->autoswitch:
- detect failure in the activity using failurecount
- activity switches to secondary LLM defined in .env diff --git a/tools/__init__.py b/tools/__init__.py index 61d3545..e7df76c 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -16,6 +16,7 @@ from .hr.checkpaybankstatus import checkpaybankstatus from .fin.check_account_valid import check_account_valid from .fin.get_account_balances import get_account_balance from .fin.move_money import move_money +from .fin.submit_loan_application import submit_loan_application from .give_hint import give_hint from .guess_location import guess_location @@ -53,7 +54,9 @@ def get_handler(tool_name: str): if tool_name == "FinCheckAccountBalance": return get_account_balance if tool_name == "FinMoveMoneyOrder": - return move_money + return move_money + if tool_name == "FinCheckAccountSubmitLoanApproval": + return submit_loan_application if tool_name == "GiveHint": return give_hint if tool_name == "GuessLocation": diff --git a/tools/fin/move_money.py b/tools/fin/move_money.py index 1f17f3e..f9569b7 100644 --- a/tools/fin/move_money.py +++ b/tools/fin/move_money.py @@ -31,7 +31,6 @@ class MoneyMovementWorkflowParameterObj: # this assumes it's a valid account - use check_account_valid() to verify that first async def move_money(args: dict) -> dict: - print("in move_money") account_key = args.get("accountkey") account_type: str = args.get("accounttype") amount = args.get("amount") diff --git a/tools/goal_registry.py b/tools/goal_registry.py index 19f62fb..837f412 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -302,6 +302,7 @@ goal_fin_check_account_balances = AgentGoal( ) # this tool checks account balances, and uses ./data/customer_account_data.json as dummy data +# it also uses a separate workflow/tool, see ./setup.md for details goal_fin_move_money = AgentGoal( id = "goal_fin_move_money", category_tag="fin", @@ -319,7 +320,7 @@ goal_fin_move_money = AgentGoal( starter_prompt=starter_prompt_generic, example_conversation_history="\n ".join( [ - "user: I'd like transfer some money", + "user: I'd like to transfer some money", "agent: Sure! I can help you out with that. May I have account number and email address?", "user: account number is 11235813", "user_confirmed_tool_run: ", @@ -340,6 +341,37 @@ goal_fin_move_money = AgentGoal( ), ) +# this starts a loan approval process +# it also uses a separate workflow/tool, see ./setup.md for details #todo +goal_fin_loan_application = AgentGoal( + id = "goal_fin_loan_application", + category_tag="fin", + agent_name="Loan Application", + agent_friendly_description="Initiate loan application.", + tools=[ + tool_registry.financial_check_account_is_valid, + tool_registry.financial_submit_loan_approval, #todo + ], + description="The user wants to apply for a loan at the financial institution. To assist with that goal, help the user gather args for these tools in order: " + "1. FinCheckAccountIsValid: validate the user's account is valid" + "2. FinCheckAccountSubmitLoanApproval: submit the loan for approval", + starter_prompt=starter_prompt_generic, + example_conversation_history="\n ".join( + [ + "user: I'd like to apply for a loan", + "agent: Sure! I can help you out with that. May I have account number for confirmation?", + "user: account number is 11235813", + "user_confirmed_tool_run: ", + "tool_result: { 'status': account valid }", + "agent: Great! We've validated your account. What will the loan amount be?", + "user: I'd like a loan for $500", + "user_confirmed_tool_run: ", + "tool_result: { 'status': submitted, 'detailed_status': loan application is submitted and initial validation is complete, 'confirmation id': 333421, 'next_step': You'll receive a confirmation for final approval in three business days }", + "agent: I have submitted your loan application process and the initial validation is successful. You'll receive a confirmation from us in three business days. " + ] + ), +) + #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) @@ -351,6 +383,7 @@ goal_list.append(goal_hr_check_pto) goal_list.append(goal_hr_check_paycheck_bank_integration_status) goal_list.append(goal_fin_check_account_balances) goal_list.append(goal_fin_move_money) +goal_list.append(goal_fin_loan_application) diff --git a/tools/tool_registry.py b/tools/tool_registry.py index 24fad84..c0805fd 100644 --- a/tools/tool_registry.py +++ b/tools/tool_registry.py @@ -316,4 +316,23 @@ financial_move_money = ToolDefinition( description="account number to move the money to", ), ], +) + +financial_move_money = ToolDefinition( + name="FinCheckAccountSubmitLoanApproval", + description="Submit a loan application. " + "Returns the loan status. ", + + arguments=[ + ToolArgument( + name="accountkey", + type="string", + description="email address or account ID of user", + ), + ToolArgument( + name="amount", + type="string", + description="amount requested for the loan", + ), + ], ) \ No newline at end of file From f0524f2b5fa427eafb5507430eaef76540030437 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Fri, 11 Apr 2025 17:45:46 -0400 Subject: [PATCH 2/4] yeah this won't work --- tools/fin/submit_loan_application.py | 94 ++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tools/fin/submit_loan_application.py diff --git a/tools/fin/submit_loan_application.py b/tools/fin/submit_loan_application.py new file mode 100644 index 0000000..4aa1e83 --- /dev/null +++ b/tools/fin/submit_loan_application.py @@ -0,0 +1,94 @@ +from datetime import timedelta +import os +from pathlib import Path +import json +from temporalio.client import Client, WorkflowHandle +from dataclasses import dataclass +from typing import Optional +import asyncio +from temporalio.exceptions import WorkflowAlreadyStartedError +from shared.config import get_temporal_client + + +# Define data structures to match the Java workflow's expected input/output +# see https://github.com/temporal-sa/temporal-latency-optimization-scenarios for more details +@dataclass +class TransactionRequest: + amount: float + account_id: str + +@dataclass +class TxResult: + transaction_id: str + message: str + +#demonstrate starting a workflow and early return pattern while the workflow continues +async def submit_loan_application(args: dict) -> dict: + account_key = args.get("accountkey") + amount = args.get("amount") + + loan_status: dict = await start_workflow(amount=amount,account_name=account_key) + + return {'status': loan_status.get("loan_status"), 'detailed_status': loan_status.get("results"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("workflowID")} + + +# Async function to start workflow +async def start_workflow(amount: str, account_name: str, )-> dict: + + # Connect to Temporal + client = await get_temporal_client() + start_real_workflow = os.getenv("FIN_START_REAL_WORKFLOW") + if start_real_workflow is not None and start_real_workflow.lower() == "false": + START_REAL_WORKFLOW = False + else: + START_REAL_WORKFLOW = True + + if START_REAL_WORKFLOW: + + # Define the workflow ID and task queue + workflow_id = "APPLICATION-"+account_name + task_queue = "LatencyOptimizationTEST" + + # Create a TransactionRequest (matching the Java workflow's expected input) + tx_request = TransactionRequest( + amount=float(amount), + account_id=account_name + ) + + #try: + # Use update-with-start to start the workflow and call the update method + handle: WorkflowHandle = await client.start_workflow( + "TransactionWorkflowLocalBeforeUpdate.processTransaction", # Workflow name + tx_request, # Input to the processTransaction method + id=workflow_id, + task_queue=task_queue, + execution_timeout=timedelta(minutes=5), + # Specify the update to call immediately after starting + update="returnInitResult", + update_args=[] # No arguments needed for returnInitResult + ) + + # Wait for the update result (returnInitResult) + update_result = await handle.result_of_update("returnInitResult") + + # Since the result is a RawValue, we need to deserialize it + # For simplicity, assuming the result is TxResult (adjust based on actual serialization) + #result_dict = update_result.payloads[0].decode() # Simplified; use proper deserialization + tx_result = TxResult( + transaction_id=result_dict.get("transaction_id", ""), + message=result_dict.get("message", "") + ) + + print(f"Update result: Transaction ID = {tx_result.transaction_id}, Message = {tx_result.message}") + + # Optionally, wait for the workflow to complete and get the final result + # final_result = await handle.result() + # print(f"Workflow completed with result: {final_result}") + + #except Exception as e: + # print(f"Error executing workflow: {e}") + + + # return {'status': loan_status.get("loan_status"), 'detailed_status': loan_status.get("results"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("workflowID")} + return {'status': "status", 'detailed_status': "loan application is submitted and initial validation is complete",'confirmation id': "11358", 'next_step': "You'll receive a confirmation for final approval in three business days", } + \ No newline at end of file From 79dcd40dded5150a0533b02077a2ab41c40afb88 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Sat, 12 Apr 2025 15:41:47 -0400 Subject: [PATCH 3/4] well it kinda works --- tools/fin/submit_loan_application.py | 79 ++++++++++++++++------------ tools/goal_registry.py | 4 +- tools/tool_registry.py | 2 +- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/tools/fin/submit_loan_application.py b/tools/fin/submit_loan_application.py index 4aa1e83..4093ec0 100644 --- a/tools/fin/submit_loan_application.py +++ b/tools/fin/submit_loan_application.py @@ -1,8 +1,14 @@ -from datetime import timedelta +from datetime import date, timedelta import os from pathlib import Path import json -from temporalio.client import Client, WorkflowHandle +from temporalio.client import ( + Client, + WithStartWorkflowOperation, + WorkflowHandle, + WorkflowUpdateFailedError, +) +from temporalio import common from dataclasses import dataclass from typing import Optional import asyncio @@ -15,12 +21,13 @@ from shared.config import get_temporal_client @dataclass class TransactionRequest: amount: float - account_id: str + sourceAccount: str + targetAccount: str @dataclass class TxResult: - transaction_id: str - message: str + transactionId: str + status: str #demonstrate starting a workflow and early return pattern while the workflow continues async def submit_loan_application(args: dict) -> dict: @@ -29,7 +36,11 @@ async def submit_loan_application(args: dict) -> dict: loan_status: dict = await start_workflow(amount=amount,account_name=account_key) - return {'status': loan_status.get("loan_status"), 'detailed_status': loan_status.get("results"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("workflowID")} + if loan_status.get("error") is None: + return {'status': loan_status.get("loan_application_status"), 'detailed_status': loan_status.get("application_details"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("transaction_id")} + else: + print(loan_status) + return loan_status # Async function to start workflow @@ -40,55 +51,53 @@ async def start_workflow(amount: str, account_name: str, )-> dict: start_real_workflow = os.getenv("FIN_START_REAL_WORKFLOW") if start_real_workflow is not None and start_real_workflow.lower() == "false": START_REAL_WORKFLOW = False + return {'loan_application_status': "applied", 'application_details': "loan application is submitted and initial validation is complete",'transaction_id': "APPLICATION"+account_name, 'advisement': "You'll receive a confirmation for final approval in three business days", } else: START_REAL_WORKFLOW = True - if START_REAL_WORKFLOW: - # Define the workflow ID and task queue - workflow_id = "APPLICATION-"+account_name + workflow_id = "LOAN_APPLICATION-"+account_name+"-"+date.today().strftime('%Y-%m-%d') task_queue = "LatencyOptimizationTEST" # Create a TransactionRequest (matching the Java workflow's expected input) tx_request = TransactionRequest( amount=float(amount), - account_id=account_name + targetAccount=account_name, + sourceAccount=account_name, ) - #try: - # Use update-with-start to start the workflow and call the update method - handle: WorkflowHandle = await client.start_workflow( - "TransactionWorkflowLocalBeforeUpdate.processTransaction", # Workflow name - tx_request, # Input to the processTransaction method + start_op = WithStartWorkflowOperation( + "TransactionWorkflowLocalBeforeUpdate", + tx_request, id=workflow_id, + id_conflict_policy=common.WorkflowIDConflictPolicy.USE_EXISTING, task_queue=task_queue, - execution_timeout=timedelta(minutes=5), - # Specify the update to call immediately after starting - update="returnInitResult", - update_args=[] # No arguments needed for returnInitResult ) - # Wait for the update result (returnInitResult) - update_result = await handle.result_of_update("returnInitResult") - - # Since the result is a RawValue, we need to deserialize it - # For simplicity, assuming the result is TxResult (adjust based on actual serialization) - #result_dict = update_result.payloads[0].decode() # Simplified; use proper deserialization - tx_result = TxResult( - transaction_id=result_dict.get("transaction_id", ""), - message=result_dict.get("message", "") - ) + try: + print("trying update-with-start") + tx_result = TxResult( + await client.execute_update_with_start_workflow( + "returnInitResult", + start_workflow_operation=start_op, + ) + ) + except WorkflowUpdateFailedError: + print("aww man got exception WorkflowUpdateFailedError" ) + tx_result = None + return_msg = "Loan could not be processed for " + account_name + return {"error": return_msg} - print(f"Update result: Transaction ID = {tx_result.transaction_id}, Message = {tx_result.message}") + workflow_handle = await start_op.workflow_handle() + print(tx_result) + + print(f"Update result: Transaction ID = {tx_result.transactionId}, Message = {tx_result.status}") # Optionally, wait for the workflow to complete and get the final result # final_result = await handle.result() # print(f"Workflow completed with result: {final_result}") - #except Exception as e: - # print(f"Error executing workflow: {e}") - - # return {'status': loan_status.get("loan_status"), 'detailed_status': loan_status.get("results"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("workflowID")} - return {'status': "status", 'detailed_status': "loan application is submitted and initial validation is complete",'confirmation id': "11358", 'next_step': "You'll receive a confirmation for final approval in three business days", } + # return {'status': loan_status.get("loan_status"), 'detailed_status': loan_status.get("results"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("workflowID")} + return {'loan_application_status': "applied", 'application_details': "loan application is submitted and initial validation is complete",'transaction_id': tx_result.transactionId, 'advisement': "You'll receive a confirmation for final approval in three business days", } \ No newline at end of file diff --git a/tools/goal_registry.py b/tools/goal_registry.py index 837f412..95c329f 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -346,7 +346,7 @@ goal_fin_move_money = AgentGoal( goal_fin_loan_application = AgentGoal( id = "goal_fin_loan_application", category_tag="fin", - agent_name="Loan Application", + agent_name="Easy Loan Apply", agent_friendly_description="Initiate loan application.", tools=[ tool_registry.financial_check_account_is_valid, @@ -367,7 +367,7 @@ goal_fin_loan_application = AgentGoal( "user: I'd like a loan for $500", "user_confirmed_tool_run: ", "tool_result: { 'status': submitted, 'detailed_status': loan application is submitted and initial validation is complete, 'confirmation id': 333421, 'next_step': You'll receive a confirmation for final approval in three business days }", - "agent: I have submitted your loan application process and the initial validation is successful. You'll receive a confirmation from us in three business days. " + "agent: I have submitted your loan application process and the initial validation is successful. Your application ID is 333421. You'll receive a notification for final approval from us in three business days. " ] ), ) diff --git a/tools/tool_registry.py b/tools/tool_registry.py index c0805fd..c9b601b 100644 --- a/tools/tool_registry.py +++ b/tools/tool_registry.py @@ -318,7 +318,7 @@ financial_move_money = ToolDefinition( ], ) -financial_move_money = ToolDefinition( +financial_submit_loan_approval = ToolDefinition( name="FinCheckAccountSubmitLoanApproval", description="Submit a loan application. " "Returns the loan status. ", From 4f953132e075c7ae34119726c1e76d5f54d6bc9e Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Mon, 14 Apr 2025 11:42:40 -0400 Subject: [PATCH 4/4] minor todo updates --- todo.md | 1 + 1 file changed, 1 insertion(+) diff --git a/todo.md b/todo.md index 1c2027a..f67239e 100644 --- a/todo.md +++ b/todo.md @@ -9,6 +9,7 @@ [ ] new loan/fraud check/update with start
[ ] financial advise - args being freeform customer input about their financial situation, goals [ ] tool is maybe a new tool asking the LLM to advise + [ ] LLM failure->autoswitch:
- detect failure in the activity using failurecount
- activity switches to secondary LLM defined in .env