From a3ec7b045a8e2677106fe50c83a793b5fcbf1889 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 26 Mar 2025 13:21:13 -0400 Subject: [PATCH] adding move money scenario - still a bit rough but it works --- activities/tool_activities.py | 10 ++- todo.md | 27 +++---- tools/__init__.py | 3 + tools/fin/get_account_balances.py | 3 +- tools/fin/move_money.py | 122 ++++++++++++++++++++++++++++++ tools/goal_registry.py | 42 ++++++++++ tools/tool_registry.py | 30 ++++++++ 7 files changed, 216 insertions(+), 21 deletions(-) create mode 100644 tools/fin/move_money.py diff --git a/activities/tool_activities.py b/activities/tool_activities.py index 39cc228..1250a42 100644 --- a/activities/tool_activities.py +++ b/activities/tool_activities.py @@ -1,3 +1,4 @@ +import inspect from temporalio import activity from ollama import chat, ChatResponse from openai import OpenAI @@ -483,16 +484,21 @@ def get_current_date_human_readable(): @activity.defn(dynamic=True) -def dynamic_tool_activity(args: Sequence[RawValue]) -> dict: +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}") # Delegate to the relevant function handler = get_handler(tool_name) - result = handler(tool_args) + if inspect.iscoroutinefunction(handler): + result = await handler(tool_args) + else: + result = handler(tool_args) # Optionally log or augment the result activity.logger.info(f"Tool '{tool_name}' result: {result}") diff --git a/todo.md b/todo.md index 44653c0..6319155 100644 --- a/todo.md +++ b/todo.md @@ -1,34 +1,27 @@ # todo list -[x] add confirmation env setting to setup guide
-
[ ] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py)
-[ ] make agent respond to name of goals and not just numbers
-[x] L look at slides
+[x] make agent respond to name of goals and not just numbers
[ ] josh to do fintech scenarios
[ ] expand [tests](./tests/agent_goal_workflow_test.py)
-[x] fix logging statements not to be all warn, maybe set logging level to info
- -[x] create people management scenarios
- -[ ] 2. Others HR goals:
-- book work travel
-- check insurance coverages
-- expense management
-- check in on the health of the team
[ ] fintech goals
- 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.
- 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.
- 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).
-- money movement - start money transfer
-- [x] account balance -
+[x] money movement - start money transfer
+[ ] todo use env vars to do connect to local or non-local +[x] account balance -
+[ ] new loan/fraud check/update with start
[ ] 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
- Insight into the agent’s performance
[ ] 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
-[ ] figure out how to allow user to list agents at any time - like end conversation
\ No newline at end of file +[ ] figure out how to allow user to list agents at any time - like end conversation
+ +[ ] change initial goal selection prompt to list capabilities and prompt more nicely - not a bulleted list - see how that works + +[ ] todo use env vars to do connect to local or non-local cloud for activities for money scenarios \ No newline at end of file diff --git a/tools/__init__.py b/tools/__init__.py index 0dd10c0..61d3545 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -15,6 +15,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 .give_hint import give_hint from .guess_location import guess_location @@ -51,6 +52,8 @@ def get_handler(tool_name: str): return check_account_valid if tool_name == "FinCheckAccountBalance": return get_account_balance + if tool_name == "FinMoveMoneyOrder": + return move_money if tool_name == "GiveHint": return give_hint if tool_name == "GuessLocation": diff --git a/tools/fin/get_account_balances.py b/tools/fin/get_account_balances.py index ca44fb6..be0854c 100644 --- a/tools/fin/get_account_balances.py +++ b/tools/fin/get_account_balances.py @@ -17,8 +17,7 @@ def get_account_balance(args: dict) -> dict: for account in account_list: if account["email"] == account_key or account["account_id"] == account_key: - #return{"status": "account valid"} return{ "name": account["name"], "email": account["email"], "account_id": account["account_id"], "checking_balance": account["checking_balance"], "savings_balance": account["savings_balance"], "bitcoin_balance": account["bitcoin_balance"], "account_creation_date": account["account_creation_date"] } - return_msg = "Account not found with email address " + email + " or account ID: " + account_id + return_msg = "Account not found with for " + account_key return {"error": return_msg} \ No newline at end of file diff --git a/tools/fin/move_money.py b/tools/fin/move_money.py new file mode 100644 index 0000000..d33119e --- /dev/null +++ b/tools/fin/move_money.py @@ -0,0 +1,122 @@ +from pathlib import Path +import json +from temporalio.client import Client +from dataclasses import dataclass +from typing import Optional +import asyncio +from temporalio.exceptions import WorkflowAlreadyStartedError + +from enum import Enum, auto + +#enums for the java enum +# class ExecutionScenarios(Enum): +# HAPPY_PATH = 0 +# ADVANCED_VISIBILITY = auto() # 1 +# HUMAN_IN_LOOP = auto() # 2 +# API_DOWNTIME = auto() # 3 +# BUG_IN_WORKFLOW = auto() # 4 +# INVALID_ACCOUNT = auto() # 5 + +# these dataclasses are for calling the Temporal Workflow +# Python equivalent of the workflow we're calling's Java WorkflowParameterObj +@dataclass +class MoneyMovementWorkflowParameterObj: + amount: int # Using snake_case as per Python conventions + scenario: str + +# this is made to demonstrate functionality but it could just as durably be an API call +# 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") + destinationaccount = args.get("destinationaccount") + + file_path = Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json" + if not file_path.exists(): + return {"error": "Data file not found."} + + # todo validate there's enough money in the account + with open(file_path, "r") as file: + data = json.load(file) + account_list = data["accounts"] + + for account in account_list: + if account["email"] == account_key or account["account_id"] == account_key: + amount_str: str = str(amount) + + transfer_workflow_id = await start_workflow(amount_cents=str_dollars_to_cents(amount_str),from_account_name=account_key, to_account_name=destinationaccount) + + account_type_key = 'checking_balance' + if(account_type.casefold() == "checking" ): + account_type = "checking" + account_type_key = 'checking_balance' + + elif(account_type.casefold() == "savings" ): + account_type = "savings" + account_type_key = 'savings_balance' + else: + raise NotImplementedError("money order for account types other than checking or savings is not implemented.") + + new_balance: float = float(str_dollars_to_cents(str(account[account_type_key]))) + new_balance = new_balance - float(str_dollars_to_cents(amount_str)) + account[account_type_key] = str(new_balance / 100 ) #to dollars + with open(file_path, 'w') as file: + json.dump(data, file, indent=4) + + return {'status': "money movement complete", 'confirmation id': transfer_workflow_id, 'new_balance': account["checking_balance"]} + + return_msg = "Account not found with for " + account_key + return {"error": return_msg} + +# Async function to start workflow +async def start_workflow(amount_cents: int, from_account_name: str, to_account_name: str)-> str: + + + # Connect to Temporal + # todo use env vars to do connect to local or non-local + client:Client = await Client.connect("localhost:7233") + + # Create the parameter object + params = MoneyMovementWorkflowParameterObj( + amount=amount_cents*100, + scenario="HAPPY_PATH" + ) + + workflow_id="TRANSFER-ACCT-" + from_account_name + "-TO-" + to_account_name # business-relevant workflow ID + + try: + handle = await client.start_workflow( + "moneyTransferWorkflow", # Workflow name + params, # Workflow parameters + id=workflow_id, + task_queue="MoneyTransferJava" # Task queue name + ) + return handle.id + except WorkflowAlreadyStartedError as e: + existing_handle = client.get_workflow_handle(workflow_id=workflow_id) + return existing_handle.id + + + + +#cleans a string dollar amount description to cents value +def str_dollars_to_cents(dollar_str: str) -> int: + try: + # Remove '$' and any whitespace + cleaned_str = dollar_str.replace('$', '').strip() + + # Handle empty string or invalid input + if not cleaned_str: + raise ValueError("Empty amount provided") + + # Convert to float and then to cents + amount = float(cleaned_str) + if amount < 0: + raise ValueError("Negative amounts not allowed") + + return int(amount * 100) + except ValueError as e: + raise ValueError(f"Invalid dollar amount format: {dollar_str}") from e \ No newline at end of file diff --git a/tools/goal_registry.py b/tools/goal_registry.py index 343bb32..d422749 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -308,6 +308,46 @@ goal_fin_check_account_balances = AgentGoal( ), ) +# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data +goal_fin_move_money = AgentGoal( + id = "goal_fin_move_money", + category_tag="fin", + agent_name="Money Order", + agent_friendly_description="Initiate a money movement order.", + tools=[ + tool_registry.financial_check_account_is_valid, + tool_registry.financial_get_account_balances, + tool_registry.financial_move_money, + tool_registry.list_agents_tool, #last tool must be list_agents to fasciliate changing back to picking an agent again at the end + ], + description="The user wants to transfer money in their account at the bank or 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. FinCheckAccountBalance: Tell the user their account balance at the bank or financial institution" + "3. FinMoveMoney: Initiate a money movement order", + starter_prompt=starter_prompt_generic, + example_conversation_history="\n ".join( + [ + "user: I'd like 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: ", + "tool_result: { 'status': account valid }", + "agent: Great! Here are your account balances:", + "user_confirmed_tool_run: ", #todo is this needed? + "tool_result: { 'name': Matt Murdock, 'email': matt.murdock@nelsonmurdock.com, 'account_id': 11235, 'checking_balance': 875.40, 'savings_balance': 3200.15, 'bitcoin_balance': 0.1378, 'account_creation_date': 2014-03-10 }", + "agent: Your account balances are as follows: \n " + "Checking: $875.40. \n " + "Savings: $3200.15. \n " + "Bitcoint: 0.1378 \n " + "agent: how much would you like to move, from which account type, and to which account number?", + "user: I'd like to move $500 from savings to account number #56789", + "user_confirmed_tool_run: ", + "tool_result: { 'status': money movement complete, 'confirmation id': 333421, 'new_balance': $2700.15 }", + "agent: Money movement order completed! New account balance: $2700.15. Your confirmation id is 333421. " + ] + ), +) + #todo add money movement, fraud check (update with start) #Add the goals to a list for more generic processing, like listing available agents goal_list: List[AgentGoal] = [] @@ -319,5 +359,7 @@ goal_list.append(goal_hr_schedule_pto) 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) + diff --git a/tools/tool_registry.py b/tools/tool_registry.py index 3930cc6..ffbafb4 100644 --- a/tools/tool_registry.py +++ b/tools/tool_registry.py @@ -285,4 +285,34 @@ financial_get_account_balances = ToolDefinition( description="email address or account ID of user", ), ], +) + +financial_move_money = ToolDefinition( + name="FinMoveMoneyOrder", + description="Execute a money movement order. " + "Returns the status of the order and the account balance of the account money was moved from. ", + + arguments=[ + ToolArgument( + name="accountkey", + type="string", + description="email address or account ID of user", + ), + ToolArgument( + name="accounttype", + type="string", + description="account type, such as checking or savings", + ), + ToolArgument( + name="amount", + type="string", + description="amount to move in the order", + ), + + ToolArgument( + name="destinationaccount", + type="string", + description="account number to move the money to", + ), + ], ) \ No newline at end of file