Merge pull request #5 from joshmsmith/development

adding money movement scenario
This commit is contained in:
Joshua Smith
2025-03-27 11:24:24 -04:00
committed by GitHub
9 changed files with 238 additions and 23 deletions

View File

@@ -42,4 +42,9 @@ AGENT_GOAL=goal_choose_agent_type # (default)
GOAL_CATEGORIES=hr,travel,fin # default is all GOAL_CATEGORIES=hr,travel,fin # default is all
# Set if the UI should force a user confirmation step or not # Set if the UI should force a user confirmation step or not
SHOW_CONFIRM=True SHOW_CONFIRM=True
# Money Scenarios:
# Set if you want it to really start workflows - otherwise it'll fake it
# if you want it to be real you'll need moneytransfer and early return workers running
FIN_START_REAL_WORKFLOW=FALSE

View File

@@ -1,3 +1,4 @@
import inspect
from temporalio import activity from temporalio import activity
from ollama import chat, ChatResponse from ollama import chat, ChatResponse
from openai import OpenAI from openai import OpenAI
@@ -483,16 +484,21 @@ def get_current_date_human_readable():
@activity.defn(dynamic=True) @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 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_name = activity.info().activity_type # e.g. "FindEvents"
tool_args = activity.payload_converter().from_payload(args[0].payload, dict) tool_args = activity.payload_converter().from_payload(args[0].payload, dict)
activity.logger.info(f"Running dynamic tool '{tool_name}' with args: {tool_args}") activity.logger.info(f"Running dynamic tool '{tool_name}' with args: {tool_args}")
# Delegate to the relevant function # Delegate to the relevant function
handler = get_handler(tool_name) 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 # Optionally log or augment the result
activity.logger.info(f"Tool '{tool_name}' result: {result}") activity.logger.info(f"Tool '{tool_name}' result: {result}")

View File

@@ -176,6 +176,13 @@ 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`. 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`.
### Money Movement Scenario
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
```
## Customizing the Agent ## Customizing the Agent
- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them) - `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 - `goal_registry.py` contains descriptions of goals and the tools used to achieve them

29
todo.md
View File

@@ -1,34 +1,27 @@
# todo list # todo list
[x] add confirmation env setting to setup guide <br />
<br />
[ ] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py) <br /> [ ] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py) <br />
[ ] make agent respond to name of goals and not just numbers <br /> [x] make agent respond to name of goals and not just numbers <br />
[x] L look at slides <br /> [x] josh to do fintech scenarios <br />
[ ] josh to do fintech scenarios <br />
[ ] expand [tests](./tests/agent_goal_workflow_test.py)<br /> [ ] expand [tests](./tests/agent_goal_workflow_test.py)<br />
[x] fix logging statements not to be all warn, maybe set logging level to info <br />
[x] create people management scenarios <br />
[ ] 2. Others HR goals: <br />
- book work travel <br />
- check insurance coverages <br />
- expense management <br />
- check in on the health of the team <br />
[ ] fintech goals <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 /> - 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 customers financial data (e.g., income, spending habits, savings, investments) and provides tailored advice, such as budgeting tips, investment options, or debt repayment strategies.<br /> - Personalized Financial Advice - An AI agent analyzes a customers 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 customers investment portfolio, rebalancing it automatically based on market trends, risk tolerance, and financial goals (e.g., shifting assets between stocks, bonds, or crypto).<br /> - Portfolio Management and Rebalancing - The AI monitors a customers investment portfolio, rebalancing it automatically based on market trends, risk tolerance, and financial goals (e.g., shifting assets between stocks, bonds, or crypto).<br />
- money movement - start money transfer <br /> [x] money movement - start money transfer <br />
- [x] account balance - <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 /> [ ] 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 agents performance <br /> - Insight into the agents performance <br />
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br /> [ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br />
[ ] make it so you can yeet yourself out of a goal and pick a new one <br />
[ ] add visual feedback when workflow starting <br /> [ ] add visual feedback when workflow starting <br />
[ ] figure out how to allow user to list agents at any time - like end conversation <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

View File

@@ -15,6 +15,7 @@ from .hr.checkpaybankstatus import checkpaybankstatus
from .fin.check_account_valid import check_account_valid from .fin.check_account_valid import check_account_valid
from .fin.get_account_balances import get_account_balance from .fin.get_account_balances import get_account_balance
from .fin.move_money import move_money
from .give_hint import give_hint from .give_hint import give_hint
from .guess_location import guess_location from .guess_location import guess_location
@@ -51,6 +52,8 @@ def get_handler(tool_name: str):
return check_account_valid return check_account_valid
if tool_name == "FinCheckAccountBalance": if tool_name == "FinCheckAccountBalance":
return get_account_balance return get_account_balance
if tool_name == "FinMoveMoneyOrder":
return move_money
if tool_name == "GiveHint": if tool_name == "GiveHint":
return give_hint return give_hint
if tool_name == "GuessLocation": if tool_name == "GuessLocation":

View File

@@ -17,8 +17,7 @@ def get_account_balance(args: dict) -> dict:
for account in account_list: for account in account_list:
if account["email"] == account_key or account["account_id"] == account_key: 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{ "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} return {"error": return_msg}

130
tools/fin/move_money.py Normal file
View File

@@ -0,0 +1,130 @@
import os
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 shared.config import get_temporal_client
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) # LLM+python gets sassy about types but we need it to be str
from_account_combo = account_key + account_type
transfer_workflow_id = await start_workflow(amount_cents=str_dollars_to_cents(amount_str),from_account_name=from_account_combo, 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[account_type_key]}
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
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:
# Create the parameter object
params = MoneyMovementWorkflowParameterObj(
amount=amount_cents,
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
else:
return "TRANSFER-ACCT-" + from_account_name + "-TO-" + to_account_name + "not-real"
#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

View File

@@ -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: <user clicks confirm on FincheckAccountIsValid tool>",
"tool_result: { 'status': account valid }",
"agent: Great! Here are your account balances:",
"user_confirmed_tool_run: <user clicks confirm on FinCheckAccountBalance tool>", #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: <user clicks confirm on FinMoveMoney tool>",
"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) #todo add money movement, fraud check (update with start)
#Add the goals to a list for more generic processing, like listing available agents #Add the goals to a list for more generic processing, like listing available agents
goal_list: List[AgentGoal] = [] 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_pto)
goal_list.append(goal_hr_check_paycheck_bank_integration_status) goal_list.append(goal_hr_check_paycheck_bank_integration_status)
goal_list.append(goal_fin_check_account_balances) goal_list.append(goal_fin_check_account_balances)
goal_list.append(goal_fin_move_money)

View File

@@ -285,4 +285,34 @@ financial_get_account_balances = ToolDefinition(
description="email address or account ID of user", 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",
),
],
) )