diff --git a/.gitignore b/.gitignore index 2d708e1..2fe1958 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,6 @@ coverage.xml # PyCharm / IntelliJ settings .idea/ -.env \ No newline at end of file +.env +.env* + diff --git a/README.md b/README.md index 6a0d212..8a97fc7 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,5 @@ See [the guide to adding goals and tools](./adding-goals-and-tools.md) for more ## For Temporal SAs Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the enablement guide here (TODO). + + 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/enterprise/Activities/TrainActivities.cs b/enterprise/Activities/TrainActivities.cs index 4f71b33..bf1b2b5 100644 --- a/enterprise/Activities/TrainActivities.cs +++ b/enterprise/Activities/TrainActivities.cs @@ -2,6 +2,7 @@ using System.Net.Http.Json; using System.Text.Json; using Temporalio.Activities; using TrainSearchWorker.Models; +using Microsoft.Extensions.Logging; namespace TrainSearchWorker.Activities; @@ -23,6 +24,7 @@ public class TrainActivities [Activity] public async Task SearchTrains(SearchTrainsRequest request) { + ActivityExecutionContext.Current.Logger.LogInformation($"SearchTrains from {request.From} to {request.To}"); var response = await _client.GetAsync( $"api/search?from={Uri.EscapeDataString(request.From)}" + $"&to={Uri.EscapeDataString(request.To)}" + @@ -30,17 +32,21 @@ public class TrainActivities $"&return_time={Uri.EscapeDataString(request.ReturnTime)}"); response.EnsureSuccessStatusCode(); - + // Deserialize into JourneyResponse rather than List var journeyResponse = await response.Content.ReadFromJsonAsync(_jsonOptions) ?? throw new InvalidOperationException("Received null response from API"); + ActivityExecutionContext.Current.Logger.LogInformation("SearchTrains completed"); + return journeyResponse; } [Activity] public async Task BookTrains(BookTrainsRequest request) { + ActivityExecutionContext.Current.Logger.LogInformation($"Booking trains with IDs: {request.TrainIds}"); + // Build the URL using the train IDs from the request var url = $"api/book/{Uri.EscapeDataString(request.TrainIds)}"; @@ -52,6 +58,8 @@ public class TrainActivities var bookingResponse = await response.Content.ReadFromJsonAsync(_jsonOptions) ?? throw new InvalidOperationException("Received null response from API"); + ActivityExecutionContext.Current.Logger.LogInformation("BookTrains completed"); + return bookingResponse; } diff --git a/enterprise/Program.cs b/enterprise/Program.cs index c8fbaa5..098e3e0 100644 --- a/enterprise/Program.cs +++ b/enterprise/Program.cs @@ -2,10 +2,19 @@ using Microsoft.Extensions.DependencyInjection; using Temporalio.Client; using Temporalio.Worker; using TrainSearchWorker.Activities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; // Set up dependency injection var services = new ServiceCollection(); +var loggerFactory = LoggerFactory.Create(builder => +{ + builder + .AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ") + .SetMinimumLevel(LogLevel.Information); +}); + // Add HTTP client services.AddHttpClient("TrainApi", client => { @@ -31,7 +40,10 @@ Console.WriteLine($"Connecting to Temporal at address: {address}"); Console.WriteLine($"Using namespace: {ns}"); // Create worker options -var options = new TemporalWorkerOptions("agent-task-queue-legacy"); +var options = new TemporalWorkerOptions("agent-task-queue-legacy") +{ + LoggerFactory = loggerFactory +}; // Register activities var activities = serviceProvider.GetRequiredService(); diff --git a/enterprise/TrainSearchWorker.csproj b/enterprise/TrainSearchWorker.csproj index e18bff4..b4eec41 100644 --- a/enterprise/TrainSearchWorker.csproj +++ b/enterprise/TrainSearchWorker.csproj @@ -7,6 +7,7 @@ + diff --git a/pyproject.toml b/pyproject.toml index 52bbd43..6f3c4f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "temporal-AI-agent" +name = "temporal_AI_agent" version = "0.1.0" description = "Temporal AI Agent" license = "MIT" @@ -13,7 +13,13 @@ packages = [ ] [tool.poetry.urls] -"Bug Tracker" = "https://github.com/temporalio/samples-python/issues" +"Bug Tracker" = "https://github.com/temporal-community/temporal-ai-agent/issues" + +[tool.poe.tasks] +format = [{cmd = "black ."}, {cmd = "isort ."}] +lint = [{cmd = "black --check ."}, {cmd = "isort --check-only ."}, {ref = "lint-types" }] +lint-types = "mypy --check-untyped-defs --namespace-packages ." +test = "pytest" [tool.poetry.dependencies] python = ">=3.10,<4.0" @@ -37,9 +43,16 @@ gtfs-kit = "^10.1.1" [tool.poetry.group.dev.dependencies] pytest = "^7.3" +pytest-asyncio = "^0.18.3" black = "^23.7" isort = "^5.12" [build-system] requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" \ No newline at end of file +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +asyncio_mode = "auto" +log_cli = true +log_cli_level = "INFO" +log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" \ No newline at end of file diff --git a/scripts/find_events_test.py b/scripts/find_events_test.py index fa8f613..6e75c9d 100644 --- a/scripts/find_events_test.py +++ b/scripts/find_events_test.py @@ -1,8 +1,8 @@ -from tools.search_events import find_events +from tools.search_flights import search_flights import json # Example usage if __name__ == "__main__": search_args = {"city": "Sydney", "month": "July"} - results = find_events(search_args) + results = search_flights(search_args) print(json.dumps(results, indent=2)) diff --git a/setup.md b/setup.md index a31df80..6a41558 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,12 @@ 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/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/agent_goal_workflow_test.py b/tests/agent_goal_workflow_test.py deleted file mode 100644 index c94206b..0000000 --- a/tests/agent_goal_workflow_test.py +++ /dev/null @@ -1,53 +0,0 @@ -import asyncio - -from temporalio.client import Client, WorkflowExecutionStatus -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.agent_goal_workflow import AgentGoalWorkflow -from activities.tool_activities import ToolActivities, dynamic_tool_activity - - -async def asyncSetUp(self): - # Set up the test environment - self.env = await TestWorkflowEnvironment.create_local() - -async def asyncTearDown(self): - # Clean up after tests - await self.env.shutdown() - -async def test_flight_booking(client: Client): - - task_queue_name = "agent-ai-workflow" - workflow_id = "agent-workflow" - - initial_agent_goal = get_initial_agent_goal() - - # Create combined input - combined_input = CombinedInput( - tool_params=AgentGoalWorkflowParams(None, None), - agent_goal=initial_agent_goal, - ) - - 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 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 - - - #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 - - - - - \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..95294fb --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,55 @@ +import asyncio +import multiprocessing +import sys +from typing import AsyncGenerator + +import pytest +import pytest_asyncio +from temporalio.client import Client +from temporalio.testing import WorkflowEnvironment + +# Due to https://github.com/python/cpython/issues/77906, multiprocessing on +# macOS starting with Python 3.8 has changed from "fork" to "spawn". For +# pre-3.8, we are changing it for them. +if sys.version_info < (3, 8) and sys.platform.startswith("darwin"): + multiprocessing.set_start_method("spawn", True) + + +def pytest_addoption(parser): + parser.addoption( + "--workflow-environment", + default="local", + help="Which workflow environment to use ('local', 'time-skipping', or target to existing server)", + ) + + +@pytest.fixture(scope="session") +def event_loop(): + # See https://github.com/pytest-dev/pytest-asyncio/issues/68 + # See https://github.com/pytest-dev/pytest-asyncio/issues/257 + # Also need ProactorEventLoop on older versions of Python with Windows so + # that asyncio subprocess works properly + if sys.version_info < (3, 8) and sys.platform == "win32": + loop = asyncio.ProactorEventLoop() + else: + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + + +@pytest_asyncio.fixture(scope="session") +async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]: + env_type = request.config.getoption("--workflow-environment") + if env_type == "local": + env = await WorkflowEnvironment.start_local() + elif env_type == "time-skipping": + env = await WorkflowEnvironment.start_time_skipping() + else: + env = WorkflowEnvironment.from_client(await Client.connect(env_type)) + yield env + await env.shutdown() + + +@pytest_asyncio.fixture +async def client(env: WorkflowEnvironment) -> Client: + return env.client diff --git a/tests/workflowtests/agent_goal_workflow_test.py b/tests/workflowtests/agent_goal_workflow_test.py new file mode 100644 index 0000000..ab67478 --- /dev/null +++ b/tests/workflowtests/agent_goal_workflow_test.py @@ -0,0 +1,80 @@ +from temporalio.client import Client, WorkflowExecutionStatus +from temporalio.worker import Worker +import concurrent.futures +from temporalio.testing import WorkflowEnvironment +from api.main import get_initial_agent_goal +from models.data_types import AgentGoalWorkflowParams, CombinedInput +from workflows.agent_goal_workflow import AgentGoalWorkflow +from activities.tool_activities import ToolActivities, dynamic_tool_activity +from unittest.mock import patch +from dotenv import load_dotenv +import os +from contextlib import contextmanager + + +@contextmanager +def my_context(): + print("Setup") + yield "some_value" # Value assigned to 'as' variable + print("Cleanup") + + + +async def test_flight_booking(client: Client): + + #load_dotenv("test_flights_single.env") + + with my_context() as value: + print(f"Working with {value}") + + + # Create the test environment + #env = await WorkflowEnvironment.start_local() + #client = env.client + task_queue_name = "agent-ai-workflow" + workflow_id = "agent-workflow" + + with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor: + worker = Worker( + client, + task_queue=task_queue_name, + workflows=[AgentGoalWorkflow], + activities=[ToolActivities.agent_validatePrompt, ToolActivities.agent_toolPlanner, ToolActivities.get_wf_env_vars, dynamic_tool_activity], + activity_executor=activity_executor, + ) + + async with worker: + initial_agent_goal = get_initial_agent_goal() + # Create combined input + combined_input = CombinedInput( + tool_params=AgentGoalWorkflowParams(None, None), + agent_goal=initial_agent_goal, + ) + + prompt="Hello!" + + #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, + combined_input, + id=workflow_id, + task_queue=task_queue_name, + start_signal="user_prompt", + start_signal_args=[prompt], + ) + # todo send signals to simulate user input + # await handle.signal(AgentGoalWorkflow.user_prompt, "book flights") # for multi-goal + await handle.signal(AgentGoalWorkflow.user_prompt, "sydney in september") + assert WorkflowExecutionStatus.RUNNING == (await handle.describe()).status + + + #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 + + result = await handle.result() + #todo dump workflow history for analysis optional + #todo assert result is good \ No newline at end of file diff --git a/todo.md b/todo.md index f06d42b..00ad1d0 100644 --- a/todo.md +++ b/todo.md @@ -1,15 +1,5 @@ # todo list -[ ] goal change management tweaks
- - [x] maybe make the choose_Agent_goal tag not be system/not always included?
- - [x] try taking out list-agents as a tool because agent_prompt_generators may do it for you
- - [x] 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
- - [x] test single-goal
- - [x] test claude and grok
- - [x] document in sample env and docs how to control
- [ ] expand [tests](./tests/agent_goal_workflow_test.py)
-[x] try claude-3-7-sonnet-20250219, see [tool_activities.py](./activities/tool_activities.py)
-[x] test Grok with changes [ ] adding 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.
@@ -17,7 +7,21 @@ - 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 +[ ] for demo simulate failure - add utilities/simulated failures from pipeline demo
+ +[ ] ecommerce goals
+- [ ] add to docs
+- [ ] decide about api key names with Laine
+ +[ ] LLM failure->autoswitch:
+ - detect failure in the activity using failurecount
+ - activity switches to secondary LLM defined in .env + - activity reports switch to workflow + +[ ] for demo simulate failure - add utilities/simulated failures from pipeline demo
[ ] 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
diff --git a/tools/__init__.py b/tools/__init__.py index 4bb37b3..6b7ab44 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 .ecommerce.get_order import get_order from .ecommerce.track_package import track_package @@ -57,7 +58,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 == "GetOrder": return get_order if tool_name == "TrackPackage": 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/fin/submit_loan_application.py b/tools/fin/submit_loan_application.py new file mode 100644 index 0000000..4093ec0 --- /dev/null +++ b/tools/fin/submit_loan_application.py @@ -0,0 +1,103 @@ +from datetime import date, timedelta +import os +from pathlib import Path +import json +from temporalio.client import ( + Client, + WithStartWorkflowOperation, + WorkflowHandle, + WorkflowUpdateFailedError, +) +from temporalio import common +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 + sourceAccount: str + targetAccount: str + +@dataclass +class TxResult: + transactionId: str + status: 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) + + 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 +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 + 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 + + # Define the workflow ID and task queue + 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), + targetAccount=account_name, + sourceAccount=account_name, + ) + + start_op = WithStartWorkflowOperation( + "TransactionWorkflowLocalBeforeUpdate", + tx_request, + id=workflow_id, + id_conflict_policy=common.WorkflowIDConflictPolicy.USE_EXISTING, + task_queue=task_queue, + ) + + 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} + + 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}") + + + # 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 5dea149..a164498 100644 --- a/tools/goal_registry.py +++ b/tools/goal_registry.py @@ -305,6 +305,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", @@ -322,7 +323,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: ", @@ -343,6 +344,34 @@ 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="Easy Loan Apply", + 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. Your application ID is 333421. You'll receive a notification for final approval from us in three business days. " + # ----- E-Commerce Goals --- #todo: add goal to list all orders for last X amount of time? # this tool checks account balances, and uses ./data/customer_account_data.json as dummy data @@ -430,5 +459,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) goal_list.append(goal_ecomm_list_orders) -goal_list.append(goal_ecomm_order_status) \ No newline at end of file +goal_list.append(goal_ecomm_order_status) + diff --git a/tools/tool_registry.py b/tools/tool_registry.py index a8eaa2a..efc3cf3 100644 --- a/tools/tool_registry.py +++ b/tools/tool_registry.py @@ -318,6 +318,22 @@ financial_move_money = ToolDefinition( ], ) +financial_submit_loan_approval = 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", + # ----- ECommerce Use Case Tools ----- ecomm_list_orders = ToolDefinition( name="ListOrders", diff --git a/workflows/agent_goal_workflow.py b/workflows/agent_goal_workflow.py index 20d1b86..aaee3c5 100644 --- a/workflows/agent_goal_workflow.py +++ b/workflows/agent_goal_workflow.py @@ -108,7 +108,7 @@ class AgentGoalWorkflow: conversation_history=self.conversation_history, agent_goal=self.goal, ) - validation_result = await workflow.execute_activity( + validation_result = await workflow.execute_activity_method( ToolActivities.agent_validatePrompt, args=[validation_input], schedule_to_close_timeout=LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT, @@ -134,7 +134,7 @@ class AgentGoalWorkflow: prompt_input = ToolPromptInput(prompt=prompt, context_instructions=context_instructions) # connect to LLM and execute to get next steps - tool_data = await workflow.execute_activity( + tool_data = await workflow.execute_activity_method( ToolActivities.agent_toolPlanner, prompt_input, schedule_to_close_timeout=LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT, @@ -211,7 +211,7 @@ class AgentGoalWorkflow: #Signal that comes from api/main.py via a post to /confirm @workflow.signal - async def confirmed(self) -> None: + async def confirm(self) -> None: """Signal handler for user confirmation of tool execution.""" workflow.logger.info("Received user signal: confirmation") self.confirmed = True @@ -317,8 +317,9 @@ class AgentGoalWorkflow: async def lookup_wf_env_settings(self, combined_input: CombinedInput)->None: env_lookup_input = EnvLookupInput( show_confirm_env_var_name = "SHOW_CONFIRM", - show_confirm_default = True) - env_output:EnvLookupOutput = await workflow.execute_activity( + show_confirm_default = True, + ) + env_output:EnvLookupOutput = await workflow.execute_activity_method( ToolActivities.get_wf_env_vars, env_lookup_input, start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT, @@ -363,9 +364,11 @@ class AgentGoalWorkflow: # 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}") + if self.tool_data: + 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')}") + else: + print("no tool data initialized yet") + print(f"self.confirmed: {self.confirmed}")