Compare commits
1 Commits
0.4.0
...
mock-footb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68743c2606 |
49
.env.example
@@ -1,48 +1,41 @@
|
||||
# Example environment configuration
|
||||
|
||||
### LLM configuration
|
||||
LLM_MODEL=openai/gpt-4o
|
||||
LLM_KEY=sk-proj-...
|
||||
# LLM_MODEL=anthropic/claude-3-5-sonnet-20240620
|
||||
# LLM_KEY=${ANTHROPIC_API_KEY}
|
||||
# LLM_MODEL=gemini/gemini-2.5-flash-preview-04-17
|
||||
# LLM_KEY=${GOOGLE_API_KEY}
|
||||
|
||||
### Tool API keys
|
||||
# RAPIDAPI_KEY=9df2cb5... # Optional - if unset flight search generates realistic mock data
|
||||
# RAPIDAPI_HOST_FLIGHTS=sky-scrapper.p.rapidapi.com # For real travel flight information (optional)
|
||||
RAPIDAPI_HOST_PACKAGE=trackingpackage.p.rapidapi.com # For eCommerce order status package tracking tool
|
||||
RAPIDAPI_KEY=9df2cb5...
|
||||
RAPIDAPI_HOST_FLIGHTS=sky-scrapper.p.rapidapi.com #For travel flight information tool
|
||||
RAPIDAPI_HOST_PACKAGE=trackingpackage.p.rapidapi.com #For eCommerce order status package tracking tool
|
||||
FOOTBALL_DATA_API_KEY=
|
||||
# Leave blank to use the built-in mock fixtures generator
|
||||
|
||||
STRIPE_API_KEY=sk_test_51J...
|
||||
# Optional for `goal_event_flight_invoice` – if unset a mock invoice is created.
|
||||
# Sign up for a free Stripe account and get a test key at https://dashboard.stripe.com/test/apikeys
|
||||
|
||||
### Temporal connection (optional)
|
||||
# Uncomment and update these values to connect to a non-default Temporal server
|
||||
LLM_MODEL=openai/gpt-4o # default
|
||||
LLM_KEY=sk-proj-...
|
||||
|
||||
|
||||
# uncomment and unset these environment variables to connect to the local dev server
|
||||
# TEMPORAL_ADDRESS=namespace.acct.tmprl.cloud:7233
|
||||
# TEMPORAL_NAMESPACE=default
|
||||
# TEMPORAL_TASK_QUEUE=agent-task-queue
|
||||
|
||||
# Uncomment if using mTLS (not needed for local dev server)
|
||||
# TEMPORAL_TLS_CERT='path/to/cert.pem'
|
||||
# TEMPORAL_TLS_KEY='path/to/key.pem'
|
||||
|
||||
# Uncomment if using API key (not needed for local dev server)
|
||||
# TEMPORAL_API_KEY=abcdef1234567890
|
||||
|
||||
### Agent goal configuration
|
||||
# Set starting goal of agent - if unset default is goal_event_flight_invoice (single agent mode)
|
||||
#AGENT_GOAL=goal_choose_agent_type # for multi-goal mode (experimental)
|
||||
AGENT_GOAL=goal_event_flight_invoice
|
||||
# Set starting goal of agent - if unset default is goal_choose_agent_type
|
||||
AGENT_GOAL=goal_choose_agent_type # for multi-goal start
|
||||
#AGENT_GOAL=goal_event_flight_invoice # for original goal
|
||||
#AGENT_GOAL=goal_match_train_invoice # for replay goal
|
||||
|
||||
# Choose which goal categories are listed by the Agent Goal picker if enabled above
|
||||
# Options: system (always included), hr, travel-flights, travel-trains, fin, ecommerce, mcp-integrations, food, all
|
||||
GOAL_CATEGORIES=all
|
||||
#Choose which category(ies) of goals you want to be listed by the Agent Goal picker if enabled above
|
||||
# - options are system (always included), hr, travel, or all.
|
||||
GOAL_CATEGORIES=fin # default is all
|
||||
#GOAL_CATEGORIES=travel-flights
|
||||
|
||||
### Other settings
|
||||
# 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:
|
||||
# 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
|
||||
FIN_START_REAL_WORKFLOW=FALSE
|
||||
3
.gitignore
vendored
@@ -33,6 +33,3 @@ coverage.xml
|
||||
|
||||
.env
|
||||
.env*
|
||||
|
||||
# Cursor
|
||||
.cursor
|
||||
201
AGENTS.md
@@ -1,201 +0,0 @@
|
||||
# Temporal AI Agent Contribution Guide
|
||||
|
||||
## Repository Layout
|
||||
- `workflows/` - Temporal workflows including the main AgentGoalWorkflow for multi-turn AI conversations
|
||||
- `activities/` - Temporal activities for tool execution and LLM interactions
|
||||
- `tools/` - Native AI agent tool implementations organized by category (finance, HR, ecommerce, travel, etc.)
|
||||
- `goals/` - Agent goal definitions organized by category, supporting both native and MCP tools
|
||||
- `shared/` - Shared configuration including MCP server definitions
|
||||
- `models/` - Data types and tool definitions used throughout the system
|
||||
- `prompts/` - Agent prompt generators and templates
|
||||
- `api/` - FastAPI server that exposes REST endpoints to interact with workflows
|
||||
- `frontend/` - React-based web UI for chatting with the AI agent
|
||||
- `tests/` - Comprehensive test suite for workflows and activities using Temporal's testing framework
|
||||
- `enterprise/` - .NET worker implementation for enterprise activities (train booking)
|
||||
- `scripts/` - Utility scripts for running workers and testing tools
|
||||
|
||||
## Running the Application
|
||||
|
||||
### Quick Start with Docker
|
||||
```bash
|
||||
# Start all services with development hot-reload
|
||||
docker compose up -d
|
||||
|
||||
# Quick rebuild without infrastructure
|
||||
docker compose up -d --no-deps --build api worker frontend
|
||||
```
|
||||
|
||||
Default URLs:
|
||||
- Temporal UI: http://localhost:8080
|
||||
- API: http://localhost:8000
|
||||
- Frontend: http://localhost:5173
|
||||
|
||||
### Local Development Setup
|
||||
|
||||
1. **Prerequisites:**
|
||||
```bash
|
||||
# Install Poetry for Python dependency management
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
# Start Temporal server (Mac)
|
||||
brew install temporal
|
||||
temporal server start-dev
|
||||
```
|
||||
|
||||
2. **Backend (Python):**
|
||||
```bash
|
||||
# Quick setup using Makefile
|
||||
make setup # Creates venv and installs dependencies
|
||||
make run-worker # Starts the Temporal worker
|
||||
make run-api # Starts the API server
|
||||
|
||||
# Or manually:
|
||||
poetry install
|
||||
poetry run python scripts/run_worker.py # In one terminal
|
||||
poetry run uvicorn api.main:app --reload # In another terminal
|
||||
```
|
||||
|
||||
3. **Frontend (React):**
|
||||
```bash
|
||||
make run-frontend # Using Makefile
|
||||
|
||||
# Or manually:
|
||||
cd frontend
|
||||
npm install
|
||||
npx vite
|
||||
```
|
||||
|
||||
4. **Enterprise .NET Worker (optional):**
|
||||
```bash
|
||||
make run-enterprise # Using Makefile
|
||||
|
||||
# Or manually:
|
||||
cd enterprise
|
||||
dotnet build
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
Copy `.env.example` to `.env` and configure:
|
||||
```bash
|
||||
# Required: LLM Configuration
|
||||
LLM_MODEL=openai/gpt-4o
|
||||
LLM_KEY=your-api-key-here
|
||||
# LLM_MODEL=anthropic/claude-3-5-sonnet-20240620
|
||||
# LLM_KEY=${ANTHROPIC_API_KEY}
|
||||
# LLM_MODEL=gemini/gemini-2.5-flash-preview-04-17
|
||||
# LLM_KEY=${GOOGLE_API_KEY}
|
||||
|
||||
# Optional: Agent Goals and Categories
|
||||
AGENT_GOAL=goal_choose_agent_type
|
||||
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin,ecommerce,mcp-integrations,food
|
||||
|
||||
# Optional: Tool-specific APIs
|
||||
STRIPE_API_KEY=sk_test_... # For invoice creation
|
||||
# `goal_event_flight_invoice` works without this key – it falls back to a mock invoice if unset
|
||||
FOOTBALL_DATA_API_KEY=... # For real football fixtures
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The project includes comprehensive tests using Temporal's testing framework:
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
poetry install --with dev
|
||||
|
||||
# Run all tests
|
||||
poetry run pytest
|
||||
|
||||
# Run with time-skipping for faster execution
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
|
||||
# Run specific test categories
|
||||
poetry run pytest tests/test_tool_activities.py -v # Activity tests
|
||||
poetry run pytest tests/test_agent_goal_workflow.py -v # Workflow tests
|
||||
|
||||
# Run with coverage
|
||||
poetry run pytest --cov=workflows --cov=activities
|
||||
```
|
||||
|
||||
**Test Coverage:**
|
||||
- ✅ **Workflow Tests**: AgentGoalWorkflow signals, queries, state management
|
||||
- ✅ **Activity Tests**: ToolActivities, LLM integration (mocked), environment configuration
|
||||
- ✅ **Integration Tests**: End-to-end workflow and activity execution
|
||||
|
||||
**Documentation:**
|
||||
- **Quick Start**: [testing.md](docs/testing.md) - Simple commands to run tests
|
||||
- **Comprehensive Guide**: [tests/README.md](tests/README.md) - Detailed testing patterns and best practices
|
||||
|
||||
## Linting and Code Quality
|
||||
|
||||
```bash
|
||||
# Using Poetry tasks
|
||||
poetry run poe format # Format code with black and isort
|
||||
poetry run poe lint # Check code style and types
|
||||
poetry run poe test # Run test suite
|
||||
|
||||
# Manual commands
|
||||
poetry run black .
|
||||
poetry run isort .
|
||||
poetry run mypy --check-untyped-defs --namespace-packages .
|
||||
```
|
||||
|
||||
## Agent Customization
|
||||
|
||||
### Adding New Goals and Tools
|
||||
|
||||
#### For Native Tools:
|
||||
1. Create tool implementation in `tools/` directory
|
||||
2. Add tool function mapping in `tools/__init__.py`
|
||||
3. Register tool definition in `tools/tool_registry.py`
|
||||
4. Add tool names to static tools list in `workflows/workflow_helpers.py`
|
||||
5. Create or update goal definition in appropriate file in `goals/` directory
|
||||
|
||||
#### For MCP Tools:
|
||||
1. Configure MCP server definition in `shared/mcp_config.py` (for reusable servers)
|
||||
2. Create or update goal definition in appropriate file in `goals/` directory with `mcp_server_definition`
|
||||
3. Set required environment variables (API keys, etc.)
|
||||
|
||||
#### For Goals:
|
||||
1. Create goal file in `goals/` directory (e.g., `goals/my_category.py`)
|
||||
2. Import and extend the goal list in `goals/__init__.py`
|
||||
|
||||
### Configuring Goals
|
||||
The agent supports multiple goal categories organized in `goals/`:
|
||||
- **Financial**: Money transfers, loan applications (`goals/finance.py`)
|
||||
- **HR**: PTO booking, payroll status (`goals/hr.py`)
|
||||
- **Travel**: Flight/train booking, event finding (`goals/travel.py`)
|
||||
- **Ecommerce**: Order tracking, package management (`goals/ecommerce.py`)
|
||||
- **Food**: Restaurant ordering and cart management (`goals/food.py`)
|
||||
- **MCP Integrations**: External service integrations like Stripe (`goals/stripe_mcp.py`)
|
||||
|
||||
Goals can use:
|
||||
- **Native Tools**: Custom implementations in `/tools/` directory
|
||||
- **MCP Tools**: External tools via Model Context Protocol servers (configured in `shared/mcp_config.py`)
|
||||
|
||||
See [adding-goals-and-tools.md](docs/adding-goals-and-tools.md) for detailed customization guide.
|
||||
|
||||
## Architecture
|
||||
|
||||
This system implements agentic AI—autonomous systems that pursue goals through iterative tool use and human feedback—with these key components:
|
||||
1. **Goals** - High-level objectives accomplished through tool sequences (organized in `/goals/` by category)
|
||||
2. **Native & MCP Tools** - Custom implementations and external service integrations
|
||||
3. **Agent Loops** - LLM execution → tool calls → human input → repeat until goal completion
|
||||
4. **Tool Approval** - Human confirmation for sensitive operations
|
||||
5. **Conversation Management** - LLM-powered input validation and history summarization
|
||||
6. **Durability** - Temporal workflows ensure reliable execution across failures
|
||||
|
||||
For detailed architecture information, see [architecture.md](docs/architecture.md).
|
||||
|
||||
## Commit Messages and Pull Requests
|
||||
- Use clear commit messages describing the change purpose
|
||||
- Reference specific files and line numbers when relevant (e.g., `workflows/agent_goal_workflow.py:125`)
|
||||
- Open PRs describing **what changed** and **why**
|
||||
- Ensure tests pass before submitting: `poetry run pytest --workflow-environment=time-skipping`
|
||||
|
||||
## Additional Resources
|
||||
- **Setup Guide**: [setup.md](docs/setup.md) - Detailed configuration instructions
|
||||
- **Architecture Decisions**: [architecture-decisions.md](docs/architecture-decisions.md) - Why Temporal for AI agents
|
||||
- **Demo Video**: [5-minute YouTube overview](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
||||
- **Multi-Agent Demo**: [Advanced multi-agent execution](https://www.youtube.com/watch?v=8Dc_0dC14yY)
|
||||
3
Makefile
@@ -5,7 +5,6 @@ setup: check-python setup-venv install
|
||||
|
||||
check-python:
|
||||
@which python3 >/dev/null 2>&1 || (echo "Python 3 is required. Please install it first." && exit 1)
|
||||
@which poetry >/dev/null 2>&1 || (echo "Poetry is required. Please install it first." && exit 1)
|
||||
|
||||
setup-venv:
|
||||
python3 -m venv venv
|
||||
@@ -60,4 +59,4 @@ help:
|
||||
@echo " make run-legacy-worker - Start the legacy worker"
|
||||
@echo " make run-enterprise - Build and run the enterprise .NET worker"
|
||||
@echo " make setup-temporal-mac - Install and start Temporal server on Mac"
|
||||
@echo " make run-dev - Start all development services (worker, API, frontend) in parallel"
|
||||
@echo " make run-dev - Start all development services (worker, API, frontend) in parallel"
|
||||
61
README.md
@@ -1,10 +1,6 @@
|
||||
# Temporal AI Agent
|
||||
|
||||
This demo shows a multi-turn conversation with an AI agent running inside a Temporal workflow. The purpose of the agent is to collect information towards a goal, running tools along the way. The agent supports both native tools and Model Context Protocol (MCP) tools, allowing it to interact with external services.
|
||||
|
||||
The agent operates in single-agent mode by default, focusing on one specific goal. It also supports experimental multi-agent/multi-goal mode where users can choose between different agent types and switch between them during conversations.
|
||||
|
||||
Goals are organized in the `/goals/` directory by category (finance, HR, travel, ecommerce, etc.) and can leverage both native and MCP tools.
|
||||
This demo shows a multi-turn conversation with an AI agent running inside a Temporal workflow. The purpose of the agent is to collect information towards a goal, running tools along the way. There's a simple DSL input for collecting information (currently set up to use mock functions to search for public events, search for flights around those events, then create a test Stripe invoice for the trip).
|
||||
|
||||
The AI will respond with clarifications and ask for any missing information to that goal. You can configure it to use any LLM supported by [LiteLLM](https://docs.litellm.ai/docs/providers), including:
|
||||
- OpenAI models (GPT-4, GPT-3.5)
|
||||
@@ -23,7 +19,7 @@ See multi-agent execution in action [here](https://www.youtube.com/watch?v=8Dc_0
|
||||
|
||||
## Why Temporal?
|
||||
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](docs/architecture-decisions.md).
|
||||
For more, check out [architecture-decisions](./architecture-decisions.md).
|
||||
|
||||
## What is "Agentic AI"?
|
||||
These are the key elements of an agentic framework:
|
||||
@@ -35,68 +31,31 @@ These are the key elements of an agentic framework:
|
||||
6. Prompt construction made of system prompts, conversation history, and tool metadata - sent to the LLM to create user questions and confirmations
|
||||
7. Ideally high durability (done in this system with Temporal Workflow and Activities)
|
||||
|
||||
For a deeper dive into this, check out the [architecture guide](docs/architecture.md).
|
||||
|
||||
## 🔧 MCP Tool Calling Support
|
||||
|
||||
This agent acts as an **MCP (Model Context Protocol) client**, enabling seamless integration with external services and tools. The system supports two types of tools:
|
||||
- **Native Tools**: Custom tools implemented directly in the codebase (in `/tools/`)
|
||||
- **MCP Tools**: External tools accessed via Model Context Protocol (MCP) servers like Stripe, databases, or APIs. Configuration is covered in [the Setup guide](docs/setup.md)
|
||||
- Set `AGENT_GOAL=goal_food_ordering` with `SHOW_CONFIRM=False` in `.env` for an example of a goal that calls MCP Tools (Stripe).
|
||||
For a deeper dive into this, check out the [architecture guide](./architecture.md).
|
||||
|
||||
## Setup and Configuration
|
||||
See [the Setup guide](docs/setup.md) for detailed instructions. The basic configuration requires just two environment variables:
|
||||
See [the Setup guide](./setup.md) for detailed instructions. The basic configuration requires just two environment variables:
|
||||
```bash
|
||||
LLM_MODEL=openai/gpt-4o # or any other model supported by LiteLLM
|
||||
LLM_KEY=your-api-key-here
|
||||
```
|
||||
|
||||
## Customizing Interaction & Tools
|
||||
See [the guide to adding goals and tools](docs/adding-goals-and-tools.md).
|
||||
|
||||
The system supports MCP (Model Context Protocol) for easy integration with external services. MCP server configurations are managed in `shared/mcp_config.py`, and goals are organized by category in the `/goals/` directory.
|
||||
See [the guide to adding goals and tools](./adding-goals-and-tools.md).
|
||||
|
||||
## Architecture
|
||||
See [the architecture guide](docs/architecture.md).
|
||||
|
||||
## Testing
|
||||
|
||||
The project includes comprehensive tests for workflows and activities using Temporal's testing framework:
|
||||
|
||||
```bash
|
||||
# Install dependencies including test dependencies
|
||||
poetry install --with dev
|
||||
|
||||
# Run all tests
|
||||
poetry run pytest
|
||||
|
||||
# Run with time-skipping for faster execution
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
**Test Coverage:**
|
||||
- ✅ **Workflow Tests**: AgentGoalWorkflow signals, queries, state management
|
||||
- ✅ **Activity Tests**: ToolActivities, LLM integration (mocked), environment configuration
|
||||
- ✅ **Integration Tests**: End-to-end workflow and activity execution
|
||||
|
||||
- **Quick Start**: [testing.md](docs/testing.md) - Simple commands to run tests
|
||||
- **Comprehensive Guide**: [tests/README.md](tests/README.md) - Detailed testing documentation, patterns, and best practices
|
||||
|
||||
## Development
|
||||
|
||||
To contribute to this project, see [contributing.md](docs/contributing.md).
|
||||
|
||||
Start the Temporal Server and API server, see [setup](docs/setup.md)
|
||||
See [the architecture guide](./architecture.md).
|
||||
|
||||
## Productionalization & Adding Features
|
||||
- In a prod setting, I would need to ensure that payload data is stored separately (e.g. in S3 or a noSQL db - the claim-check pattern), or otherwise 'garbage collected'. Without these techniques, long conversations will fill up the workflow's conversation history, and start to breach Temporal event history payload limits.
|
||||
- A single worker can easily support many agent workflows (chats) running at the same time. Currently the workflow ID is the same each time, so it will only run one agent at a time. To run multiple agents, you can use a different workflow ID each time (e.g. by using a UUID or timestamp).
|
||||
- Perhaps the UI should show when the LLM response is being retried (i.e. activity retry attempt because the LLM provided bad output)
|
||||
- The project now includes comprehensive tests for workflows and activities! [See testing guide](docs/testing.md).
|
||||
- Tests would be nice! [See tests](./tests/).
|
||||
|
||||
See [the todo](docs/todo.md) for more details on things we want to do (or that you could contribute!).
|
||||
|
||||
See [the guide to adding goals and tools](docs/adding-goals-and-tools.md) for more ways you can add features.
|
||||
See [the todo](./todo.md) for more details.
|
||||
|
||||
See [the guide to adding goals and tools](./adding-goals-and-tools.md) for more ways you can add features.
|
||||
|
||||
## Enablement Guide (internal resource for Temporal employees)
|
||||
Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the [enablement guide](https://docs.google.com/document/d/14E0cEOibUAgHPBqConbWXgPUBY0Oxrnt6_AImdiheW4/edit?tab=t.0#heading=h.ajnq2v3xqbu1).
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Sequence
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from litellm import completion
|
||||
from temporalio import activity
|
||||
import json
|
||||
from typing import Optional, Sequence
|
||||
from temporalio.common import RawValue
|
||||
from temporalio.exceptions import ApplicationError
|
||||
|
||||
from models.data_types import (
|
||||
EnvLookupInput,
|
||||
EnvLookupOutput,
|
||||
ToolPromptInput,
|
||||
ValidationInput,
|
||||
ValidationResult,
|
||||
)
|
||||
from models.tool_definitions import MCPServerDefinition
|
||||
|
||||
# Import MCP client libraries
|
||||
try:
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
except ImportError:
|
||||
# Fallback if MCP not installed
|
||||
ClientSession = None
|
||||
StdioServerParameters = None
|
||||
stdio_client = None
|
||||
import os
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
from models.data_types import EnvLookupOutput, ValidationInput, ValidationResult, ToolPromptInput, EnvLookupInput
|
||||
from litellm import completion
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
|
||||
class ToolActivities:
|
||||
def __init__(self):
|
||||
"""Initialize LLM client using LiteLLM."""
|
||||
@@ -44,9 +22,7 @@ class ToolActivities:
|
||||
print(f"Using custom base URL: {self.llm_base_url}")
|
||||
|
||||
@activity.defn
|
||||
async def agent_validatePrompt(
|
||||
self, validation_input: ValidationInput
|
||||
) -> ValidationResult:
|
||||
async def agent_validatePrompt(self, validation_input: ValidationInput) -> ValidationResult:
|
||||
"""
|
||||
Validates the prompt in the context of the conversation history and agent goal.
|
||||
Returns a ValidationResult indicating if the prompt makes sense given the context.
|
||||
@@ -123,26 +99,20 @@ class ToolActivities:
|
||||
completion_kwargs = {
|
||||
"model": self.llm_model,
|
||||
"messages": messages,
|
||||
"api_key": self.llm_key,
|
||||
"api_key": self.llm_key
|
||||
}
|
||||
|
||||
|
||||
# Add base_url if configured
|
||||
if self.llm_base_url:
|
||||
completion_kwargs["base_url"] = self.llm_base_url
|
||||
|
||||
response = completion(**completion_kwargs)
|
||||
|
||||
|
||||
response_content = response.choices[0].message.content
|
||||
activity.logger.info(f"Raw LLM response: {repr(response_content)}")
|
||||
activity.logger.info(f"LLM response content: {response_content}")
|
||||
activity.logger.info(f"LLM response type: {type(response_content)}")
|
||||
activity.logger.info(
|
||||
f"LLM response length: {len(response_content) if response_content else 'None'}"
|
||||
)
|
||||
activity.logger.info(f"LLM response: {response_content}")
|
||||
|
||||
# Use the new sanitize function
|
||||
response_content = self.sanitize_json_response(response_content)
|
||||
activity.logger.info(f"Sanitized response: {repr(response_content)}")
|
||||
|
||||
return self.parse_json_response(response_content)
|
||||
except Exception as e:
|
||||
@@ -166,20 +136,19 @@ class ToolActivities:
|
||||
"""
|
||||
# Remove any markdown code block markers
|
||||
response_content = response_content.replace("```json", "").replace("```", "")
|
||||
|
||||
|
||||
# Remove any leading/trailing whitespace
|
||||
response_content = response_content.strip()
|
||||
|
||||
|
||||
return response_content
|
||||
|
||||
@activity.defn
|
||||
async def get_wf_env_vars(self, input: EnvLookupInput) -> EnvLookupOutput:
|
||||
"""gets env vars for workflow as an activity result so it's deterministic
|
||||
handles default/None
|
||||
""" gets env vars for workflow as an activity result so it's deterministic
|
||||
handles default/None
|
||||
"""
|
||||
output: EnvLookupOutput = EnvLookupOutput(
|
||||
show_confirm=input.show_confirm_default, multi_goal_mode=False
|
||||
)
|
||||
output: EnvLookupOutput = EnvLookupOutput(show_confirm=input.show_confirm_default,
|
||||
multi_goal_mode=True)
|
||||
show_confirm_value = os.getenv(input.show_confirm_env_var_name)
|
||||
if show_confirm_value is None:
|
||||
output.show_confirm = input.show_confirm_default
|
||||
@@ -187,33 +156,17 @@ class ToolActivities:
|
||||
output.show_confirm = False
|
||||
else:
|
||||
output.show_confirm = True
|
||||
|
||||
|
||||
first_goal_value = os.getenv("AGENT_GOAL")
|
||||
if first_goal_value is None:
|
||||
output.multi_goal_mode = False # default to single agent mode if unset
|
||||
elif (
|
||||
first_goal_value is not None
|
||||
and first_goal_value.lower() == "goal_choose_agent_type"
|
||||
):
|
||||
output.multi_goal_mode = True
|
||||
else:
|
||||
output.multi_goal_mode = True # default if unset
|
||||
elif first_goal_value is not None and first_goal_value.lower() != "goal_choose_agent_type":
|
||||
output.multi_goal_mode = False
|
||||
else:
|
||||
output.multi_goal_mode = True
|
||||
|
||||
return output
|
||||
|
||||
@activity.defn
|
||||
async def mcp_tool_activity(
|
||||
self, tool_name: str, tool_args: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""MCP Tool"""
|
||||
activity.logger.info(f"Executing MCP tool: {tool_name} with args: {tool_args}")
|
||||
|
||||
# Extract server definition
|
||||
server_definition = tool_args.pop("server_definition", None)
|
||||
|
||||
return await _execute_mcp_tool(tool_name, tool_args, server_definition)
|
||||
|
||||
|
||||
@activity.defn(dynamic=True)
|
||||
async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||
from tools import get_handler
|
||||
@@ -222,246 +175,15 @@ async def dynamic_tool_activity(args: Sequence[RawValue]) -> 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}")
|
||||
|
||||
# Check if this is an MCP tool call by looking for server_definition in args
|
||||
server_definition = tool_args.pop("server_definition", None)
|
||||
|
||||
if server_definition:
|
||||
# This is an MCP tool call - handle it directly
|
||||
activity.logger.info(f"Executing MCP tool: {tool_name}")
|
||||
return await _execute_mcp_tool(tool_name, tool_args, server_definition)
|
||||
# Delegate to the relevant function
|
||||
handler = get_handler(tool_name)
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
result = await handler(tool_args)
|
||||
else:
|
||||
# This is a regular tool - delegate to the relevant function
|
||||
handler = get_handler(tool_name)
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
result = await handler(tool_args)
|
||||
else:
|
||||
result = handler(tool_args)
|
||||
result = handler(tool_args)
|
||||
|
||||
# Optionally log or augment the result
|
||||
activity.logger.info(f"Tool '{tool_name}' result: {result}")
|
||||
return result
|
||||
|
||||
|
||||
# MCP Client Activities
|
||||
|
||||
|
||||
def _build_connection(
|
||||
server_definition: MCPServerDefinition | Dict[str, Any] | None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Build connection parameters from MCPServerDefinition or dict"""
|
||||
if server_definition is None:
|
||||
# Default to stdio connection with the main server
|
||||
return {"type": "stdio", "command": "python", "args": ["server.py"], "env": {}}
|
||||
|
||||
# Handle both MCPServerDefinition objects and dicts (from Temporal serialization)
|
||||
if isinstance(server_definition, dict):
|
||||
return {
|
||||
"type": server_definition.get("connection_type", "stdio"),
|
||||
"command": server_definition.get("command", "python"),
|
||||
"args": server_definition.get("args", ["server.py"]),
|
||||
"env": server_definition.get("env", {}) or {},
|
||||
}
|
||||
|
||||
return {
|
||||
"type": server_definition.connection_type,
|
||||
"command": server_definition.command,
|
||||
"args": server_definition.args,
|
||||
"env": server_definition.env or {},
|
||||
}
|
||||
|
||||
|
||||
def _normalize_result(result: Any) -> Any:
|
||||
"""Normalize MCP tool result for serialization"""
|
||||
if hasattr(result, "content"):
|
||||
# Handle MCP result objects
|
||||
if hasattr(result.content, "__iter__") and not isinstance(result.content, str):
|
||||
return [
|
||||
item.text if hasattr(item, "text") else str(item)
|
||||
for item in result.content
|
||||
]
|
||||
return str(result.content)
|
||||
# Optionally log or augment the result
|
||||
activity.logger.info(f"Tool '{tool_name}' result: {result}")
|
||||
return result
|
||||
|
||||
|
||||
def _convert_args_types(tool_args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Convert string arguments to appropriate types for MCP tools"""
|
||||
converted_args = {}
|
||||
|
||||
for key, value in tool_args.items():
|
||||
if key == "server_definition":
|
||||
# Skip server_definition - it's metadata
|
||||
continue
|
||||
|
||||
if isinstance(value, str):
|
||||
# Try to convert string values to appropriate types
|
||||
if value.isdigit():
|
||||
# Convert numeric strings to integers
|
||||
converted_args[key] = int(value)
|
||||
elif value.replace(".", "").isdigit() and value.count(".") == 1:
|
||||
# Convert decimal strings to floats
|
||||
converted_args[key] = float(value)
|
||||
elif value.lower() in ("true", "false"):
|
||||
# Convert boolean strings
|
||||
converted_args[key] = value.lower() == "true"
|
||||
else:
|
||||
# Keep as string
|
||||
converted_args[key] = value
|
||||
else:
|
||||
# Keep non-string values as-is
|
||||
converted_args[key] = value
|
||||
|
||||
return converted_args
|
||||
|
||||
|
||||
async def _execute_mcp_tool(
|
||||
tool_name: str,
|
||||
tool_args: Dict[str, Any],
|
||||
server_definition: MCPServerDefinition | Dict[str, Any] | None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute an MCP tool with the given arguments and server definition"""
|
||||
activity.logger.info(f"Executing MCP tool: {tool_name}")
|
||||
|
||||
# Convert argument types for MCP tools
|
||||
converted_args = _convert_args_types(tool_args)
|
||||
connection = _build_connection(server_definition)
|
||||
|
||||
try:
|
||||
if connection["type"] == "stdio":
|
||||
# Handle stdio connection
|
||||
async with _stdio_connection(
|
||||
command=connection.get("command", "python"),
|
||||
args=connection.get("args", ["server.py"]),
|
||||
env=connection.get("env", {}),
|
||||
) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
# Initialize the session
|
||||
activity.logger.info(f"Initializing MCP session for {tool_name}")
|
||||
await session.initialize()
|
||||
activity.logger.info(f"MCP session initialized for {tool_name}")
|
||||
|
||||
# Call the tool
|
||||
activity.logger.info(
|
||||
f"Calling MCP tool {tool_name} with args: {converted_args}"
|
||||
)
|
||||
try:
|
||||
result = await session.call_tool(
|
||||
tool_name, arguments=converted_args
|
||||
)
|
||||
activity.logger.info(
|
||||
f"MCP tool {tool_name} returned result: {result}"
|
||||
)
|
||||
except Exception as tool_exc:
|
||||
activity.logger.error(
|
||||
f"MCP tool {tool_name} call failed: {type(tool_exc).__name__}: {tool_exc}"
|
||||
)
|
||||
raise
|
||||
|
||||
normalized_result = _normalize_result(result)
|
||||
activity.logger.info(f"MCP tool {tool_name} completed successfully")
|
||||
|
||||
return {
|
||||
"tool": tool_name,
|
||||
"success": True,
|
||||
"content": normalized_result,
|
||||
}
|
||||
|
||||
elif connection["type"] == "tcp":
|
||||
# Handle TCP connection (placeholder for future implementation)
|
||||
raise ApplicationError("TCP connections not yet implemented")
|
||||
|
||||
else:
|
||||
raise ApplicationError(f"Unsupported connection type: {connection['type']}")
|
||||
|
||||
except Exception as e:
|
||||
activity.logger.error(f"MCP tool {tool_name} failed: {str(e)}")
|
||||
|
||||
# Return error information
|
||||
return {
|
||||
"tool": tool_name,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _stdio_connection(command: str, args: list, env: dict):
|
||||
"""Create stdio connection to MCP server"""
|
||||
if stdio_client is None:
|
||||
raise ApplicationError("MCP client libraries not available")
|
||||
|
||||
# Create server parameters
|
||||
server_params = StdioServerParameters(command=command, args=args, env=env)
|
||||
|
||||
async with stdio_client(server_params) as (read, write):
|
||||
yield read, write
|
||||
|
||||
|
||||
@activity.defn
|
||||
async def mcp_list_tools(
|
||||
server_definition: MCPServerDefinition, include_tools: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""List available MCP tools from the specified server"""
|
||||
|
||||
activity.logger.info(f"Listing MCP tools for server: {server_definition.name}")
|
||||
|
||||
connection = _build_connection(server_definition)
|
||||
|
||||
try:
|
||||
if connection["type"] == "stdio":
|
||||
async with _stdio_connection(
|
||||
command=connection.get("command", "python"),
|
||||
args=connection.get("args", ["server.py"]),
|
||||
env=connection.get("env", {}),
|
||||
) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
# Initialize the session
|
||||
await session.initialize()
|
||||
|
||||
# List available tools
|
||||
tools_response = await session.list_tools()
|
||||
|
||||
# Process tools based on include_tools filter
|
||||
tools_info = {}
|
||||
for tool in tools_response.tools:
|
||||
# If include_tools is specified, only include those tools
|
||||
if include_tools is None or tool.name in include_tools:
|
||||
tools_info[tool.name] = {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"inputSchema": (
|
||||
tool.inputSchema.model_dump()
|
||||
if hasattr(tool.inputSchema, "model_dump")
|
||||
else str(tool.inputSchema)
|
||||
),
|
||||
}
|
||||
|
||||
activity.logger.info(
|
||||
f"Found {len(tools_info)} tools for server {server_definition.name}"
|
||||
)
|
||||
|
||||
return {
|
||||
"server_name": server_definition.name,
|
||||
"success": True,
|
||||
"tools": tools_info,
|
||||
"total_available": len(tools_response.tools),
|
||||
"filtered_count": len(tools_info),
|
||||
}
|
||||
|
||||
elif connection["type"] == "tcp":
|
||||
raise ApplicationError("TCP connections not yet implemented")
|
||||
|
||||
else:
|
||||
raise ApplicationError(f"Unsupported connection type: {connection['type']}")
|
||||
|
||||
except Exception as e:
|
||||
activity.logger.error(
|
||||
f"Failed to list tools for server {server_definition.name}: {str(e)}"
|
||||
)
|
||||
|
||||
return {
|
||||
"server_name": server_definition.name,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
}
|
||||
|
||||
@@ -1,50 +1,41 @@
|
||||
# Customizing the Agent
|
||||
The agent operates in single-agent mode by default, focusing on one specific goal. It also supports an experimental multi-agent mode where users can have multiple agents, each with their own goal, and supports switching back to choosing a new goal at the end of every successful goal (or even mid-goal).
|
||||
|
||||
A goal can use two types of tools:
|
||||
- **Native Tools**: Custom tools implemented directly in the codebase (in `/tools/`)
|
||||
- **MCP Tools**: External tools accessed via Model Context Protocol (MCP) servers
|
||||
The agent is set up to have multiple agents, each with their own goal. It supports switching back to choosing a new goal at the end of every successful goal (or even mid-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
|
||||
Goal Categories lets you pick which groups of goals to show in multi-agent mode. Set via an .env setting, `GOAL_CATEGORIES`.
|
||||
Even if you don't intend to use the goal in a multi-agent scenario, goal categories are useful for organization and discovery.
|
||||
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](./.env) file
|
||||
3. Add to [.env.example](./.env.example)
|
||||
4. Use it in your Goal definition, see below.
|
||||
|
||||
## Adding a Goal
|
||||
1. Create a new Python file in the `/goals/` directory (e.g., `goals/my_category.py`) - these files contain descriptions of goals and the tools used to achieve them
|
||||
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:
|
||||
- `id`: needs to be the same as the name
|
||||
- `agent_name`: user-facing name for the agent/chatbot
|
||||
- `category_tag`: category for the goal
|
||||
- `agent_friendly_description`: user-facing description of what the agent/chatbot does
|
||||
- `tools`: the list of **native tools** the goal uses. These are defined in [tools/tool_registry.py](tools/tool_registry.py) as `tool_registry.[name_of_tool]`
|
||||
- `tools`: the list of tools the goal will walk the user through. These will be defined in the [tools/tool_registry.py](tools/tool_registry.py) and should be defined in list form as tool_registry.[name of tool]
|
||||
|
||||
Example:
|
||||
```python
|
||||
```
|
||||
tools=[
|
||||
tool_registry.current_pto_tool,
|
||||
tool_registry.future_pto_calc_tool,
|
||||
tool_registry.book_pto_tool,
|
||||
]
|
||||
```
|
||||
- `mcp_server_definition`: (Optional) MCP server configuration for external tools. Can use predefined configurations from `shared/mcp_config.py` or define custom ones. See [MCP Tools section](#adding-mcp-tools) below.
|
||||
- `description`: LLM-facing description of the goal that lists all tools (native and MCP) by name and purpose.
|
||||
- `starter_prompt`: LLM-facing first prompt given to begin the scenario. This field can contain instructions that are different from other goals, like "begin by providing the output of the first tool" rather than waiting on user confirmation. (See [goal_choose_agent_type](tools/goal_registry.py) for an example.)
|
||||
- `description`: LLM-facing description of the goal that lists the tools by name and purpose.
|
||||
- `starter-prompt`: LLM-facing first prompt given to begin the scenario. This field can contain instructions that are different from other goals, like "begin by providing the output of the first tool" rather than waiting on user confirmation. (See [goal_choose_agent_type](tools/goal_registry.py) for an example.)
|
||||
- `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 a list variable (e.g., `my_category_goals: List[AgentGoal] = [your_super_sweet_new_goal]`)
|
||||
5. Import and extend the goal list in `goals/__init__.py` by adding:
|
||||
- Import: `from goals.my_category import my_category_goals`
|
||||
- Extend: `goal_list.extend(my_category_goals)`
|
||||
4. Add your new goal to the `goal_list` at the bottom using `goal_list.append(your_super_sweet_new_goal)`
|
||||
|
||||
## Adding Native Tools
|
||||
|
||||
Native tools are custom implementations that run directly in your codebase. Use these for business logic specific to your application.
|
||||
## Adding 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:
|
||||
@@ -67,7 +58,7 @@ Tools should generally return meaningful information and be generally ‘failsaf
|
||||
- `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 Native Tool Implementation
|
||||
### 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`
|
||||
@@ -75,68 +66,12 @@ Tools should generally return meaningful information and be generally ‘failsaf
|
||||
- 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()
|
||||
- In [tools/__init__.py](./tools/__init__.py), add an import statement for each new native 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.
|
||||
- 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:
|
||||
```python
|
||||
```
|
||||
if tool_name == "CurrentPTO":
|
||||
return current_pto
|
||||
```
|
||||
|
||||
### Update workflow_helpers.py
|
||||
- Add your new native tool to the static tools list in [workflows/workflow_helpers.py](workflows/workflow_helpers.py) so it's correctly identified as a native tool rather than an MCP tool.
|
||||
|
||||
## Adding MCP Tools
|
||||
|
||||
MCP (Model Context Protocol) tools are external tools provided by MCP servers. They're useful for integrating with third-party services like Stripe, databases, or APIs without implementing custom code.
|
||||
|
||||
### Configure MCP Server Definition
|
||||
You can either use predefined MCP server configurations from `shared/mcp_config.py` or define custom ones.
|
||||
|
||||
#### Using Predefined Configurations
|
||||
```python
|
||||
from shared.mcp_config import get_stripe_mcp_server_definition
|
||||
|
||||
# In your goal definition:
|
||||
mcp_server_definition=get_stripe_mcp_server_definition(included_tools=["list_products", "create_customer"])
|
||||
```
|
||||
|
||||
#### Custom MCP Server Definition
|
||||
Add an `mcp_server_definition` to your goal:
|
||||
|
||||
```python
|
||||
mcp_server_definition=MCPServerDefinition(
|
||||
name="stripe-mcp",
|
||||
command="npx",
|
||||
args=[
|
||||
"-y",
|
||||
"@stripe/mcp",
|
||||
"--tools=all",
|
||||
f"--api-key={os.getenv('STRIPE_API_KEY')}",
|
||||
],
|
||||
env=None,
|
||||
included_tools=[
|
||||
"list_products",
|
||||
"list_prices",
|
||||
"create_customer",
|
||||
"create_invoice",
|
||||
"create_payment_link",
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### MCP Tool Configuration
|
||||
- `name`: Identifier for the MCP server
|
||||
- `command`: Command to start the MCP server (e.g., "npx", "python")
|
||||
- `args`: Arguments to pass to the command
|
||||
- `env`: Environment variables for the server (optional)
|
||||
- `included_tools`: List of specific tools to use from the server (optional - if omitted, all tools are included)
|
||||
|
||||
### How MCP Tools Work
|
||||
- MCP tools are automatically loaded when the workflow starts
|
||||
- They're dynamically converted to `ToolDefinition` objects
|
||||
- The system automatically routes MCP tool calls to the appropriate MCP server
|
||||
- No additional code implementation needed - just configuration
|
||||
|
||||
## 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
|
||||
@@ -154,24 +89,12 @@ If you really want to wait for user confirmation, record it on the workflow (as
|
||||
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) <br />
|
||||
- [ ] If a new category, add Goal Category to [.env](./.env) and [.env.example](./.env.example) <br />
|
||||
- [ ] don't forget the goal list at the bottom of the [goal_registry.py](tools/goal_registry.py) <br />
|
||||
|
||||
### For All Goals:
|
||||
- [ ] Create goal file in `/goals/` directory (e.g., `goals/my_category.py`)
|
||||
- [ ] Add goal to the category's goal list in the file
|
||||
- [ ] Import and extend the goal list in `goals/__init__.py`
|
||||
- [ ] If a new category, add Goal Category to [.env](./.env) and [.env.example](./.env.example)
|
||||
|
||||
### For Native Tools:
|
||||
- [ ] Add native tools to [tool_registry.py](tools/tool_registry.py)
|
||||
- [ ] Implement tool functions in `/tools/` directory
|
||||
- [ ] Add tools to [tools/__init__.py](tools/__init__.py) in the `get_handler()` function
|
||||
- [ ] Add tool names to static tools list in [workflows/workflow_helpers.py](workflows/workflow_helpers.py)
|
||||
|
||||
### For MCP Tools:
|
||||
- [ ] Add `mcp_server_definition` to your goal configuration (use `shared/mcp_config.py` for common servers)
|
||||
- [ ] Ensure MCP server is available and properly configured
|
||||
- [ ] Set required environment variables (API keys, etc.)
|
||||
- [ ] Test MCP server connectivity before running the agent
|
||||
- [ ] If creating new MCP server configs, add them to `shared/mcp_config.py` for reuse
|
||||
[ ] Add Tools listed in the Goal Registry to the [tool_registry.py](tools/tool_registry.py) <br />
|
||||
[ ] Define your tools as Activities in `/tools` <br />
|
||||
[ ] Add your tools to [tool list](tools/__init__.py) in the tool get_handler() <br />
|
||||
|
||||
And that's it! Happy AI Agent building!
|
||||
27
api/main.py
@@ -1,18 +1,18 @@
|
||||
import asyncio
|
||||
import os
|
||||
from fastapi import FastAPI
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from temporalio.api.enums.v1 import WorkflowExecutionStatus
|
||||
from temporalio.client import Client
|
||||
from temporalio.exceptions import TemporalError
|
||||
from temporalio.api.enums.v1 import WorkflowExecutionStatus
|
||||
from fastapi import HTTPException
|
||||
from dotenv import load_dotenv
|
||||
import asyncio
|
||||
|
||||
from goals import goal_list
|
||||
from models.data_types import AgentGoalWorkflowParams, CombinedInput
|
||||
from shared.config import TEMPORAL_TASK_QUEUE, get_temporal_client
|
||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||
from models.data_types import CombinedInput, AgentGoalWorkflowParams
|
||||
from tools.goal_registry import goal_list
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE
|
||||
|
||||
app = FastAPI()
|
||||
temporal_client: Optional[Client] = None
|
||||
@@ -23,9 +23,7 @@ load_dotenv()
|
||||
|
||||
def get_initial_agent_goal():
|
||||
"""Get the agent goal from environment variables."""
|
||||
env_goal = os.getenv(
|
||||
"AGENT_GOAL", "goal_event_flight_invoice"
|
||||
) # if no goal is set in the env file, default to single agent mode
|
||||
env_goal = os.getenv("AGENT_GOAL", "goal_choose_agent_type") #if no goal is set in the env file, default to choosing an agent
|
||||
for listed_goal in goal_list:
|
||||
if listed_goal.id == env_goal:
|
||||
return listed_goal
|
||||
@@ -121,8 +119,7 @@ async def get_conversation_history():
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Internal server error while querying workflow."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@app.get("/agent-goal")
|
||||
async def get_agent_goal():
|
||||
"""Calls the workflow's 'get_agent_goal' query."""
|
||||
@@ -151,7 +148,7 @@ async def send_prompt(prompt: str):
|
||||
combined_input = CombinedInput(
|
||||
tool_params=AgentGoalWorkflowParams(None, None),
|
||||
agent_goal=get_initial_agent_goal(),
|
||||
# change to get from workflow query
|
||||
#change to get from workflow query
|
||||
)
|
||||
|
||||
workflow_id = "agent-workflow"
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
50
backup_sessions.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "https://cdn.statically.io/gh/nguyenngoclongdev/cdn/main/schema/v10/terminal-keeper.json",
|
||||
"theme": "tribe",
|
||||
"active": "default",
|
||||
"activateOnStartup": false,
|
||||
"keepExistingTerminals": false,
|
||||
"sessions": {
|
||||
"default": [
|
||||
[
|
||||
{
|
||||
"name": "frontend",
|
||||
"autoExecuteCommands": true,
|
||||
"commands": [
|
||||
"cd frontend && npx vite"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "uvicorn",
|
||||
"autoExecuteCommands": true,
|
||||
"commands": [
|
||||
"poetry run uvicorn api.main:app --reload"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"name": "agent worker",
|
||||
"autoExecuteCommands": true,
|
||||
"commands": [
|
||||
"poetry run python scripts/run_worker.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "trains worker",
|
||||
"autoExecuteCommands": true,
|
||||
"commands": [
|
||||
"poetry run python scripts/run_legacy_worker.py"
|
||||
]
|
||||
}
|
||||
],
|
||||
{
|
||||
"name": "trains_api",
|
||||
"autoExecuteCommands": true,
|
||||
"commands": [
|
||||
"poetry run python thirdparty/train_api.py"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# Documentation Index
|
||||
|
||||
- **architecture.md** - Overview of system components and how they interact.
|
||||
- **architecture-decisions.md** - Rationale behind key design choices.
|
||||
- **changelog.md** - Project history and notable changes.
|
||||
- **contributing.md** - How to contribute and run tests.
|
||||
- **setup.md** - Installation and configuration instructions.
|
||||
- **testing.md** - Commands for running the test suite.
|
||||
- **adding-goals-and-tools.md** - Guide to extending the agent with new goals and tools.
|
||||
- **todo.md** - Planned enhancements and future work.
|
||||
@@ -1,106 +0,0 @@
|
||||
# Contributing to the Temporal AI Agent Project
|
||||
|
||||
This document provides guidelines for contributing to `temporal-ai-agent`. All setup and installation instructions can be found in [setup.md](./setup.md).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Code Style & Formatting
|
||||
We use `black` for code formatting and `isort` for import sorting to maintain a consistent codebase.
|
||||
- **Format code:**
|
||||
```bash
|
||||
poetry run poe format
|
||||
```
|
||||
Or manually:
|
||||
```bash
|
||||
poetry run black .
|
||||
poetry run isort .
|
||||
```
|
||||
Please format your code before committing.
|
||||
|
||||
### Linting & Type Checking
|
||||
We use `mypy` for static type checking and other linters configured via `poe the poet`.
|
||||
- **Run linters and type checks:**
|
||||
```bash
|
||||
poetry run poe lint
|
||||
```
|
||||
Or manually for type checking:
|
||||
```bash
|
||||
poetry run mypy --check-untyped-defs --namespace-packages .
|
||||
```
|
||||
Ensure all linting and type checks pass before submitting a pull request.
|
||||
|
||||
## Testing
|
||||
Comprehensive testing is crucial for this project. We use `pytest` and Temporal's testing framework.
|
||||
- **Install test dependencies** (if not already done with `poetry install --with dev`):
|
||||
```bash
|
||||
poetry install --with dev
|
||||
```
|
||||
- **Run all tests:**
|
||||
```bash
|
||||
poetry run pytest
|
||||
```
|
||||
- **Run tests with time-skipping (recommended for faster execution, especially in CI):**
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
For detailed information on test categories, running specific tests, test environments, coverage, and troubleshooting, please refer to:
|
||||
- [testing.md](./testing.md) (Quick Start and overview)
|
||||
- [tests/README.md](../tests/README.md) (Comprehensive guide, patterns, and best practices)
|
||||
|
||||
**Ensure all tests pass before submitting a pull request.**
|
||||
|
||||
## Making Changes
|
||||
|
||||
### Adding New Tools or Goals
|
||||
If you're looking to extend the agent's capabilities:
|
||||
1. Create your tool implementation in the `tools/` directory.
|
||||
2. Register your tool and associate it with relevant goals.
|
||||
For detailed instructions, please see:
|
||||
- [Agent Customization in AGENTS.md](../AGENTS.md#agent-customization)
|
||||
- [Adding Goals and Tools Guide](./adding-goals-and-tools.md)
|
||||
|
||||
### General Code Changes
|
||||
- Follow the existing code style and patterns.
|
||||
- Ensure any new code is well-documented with comments.
|
||||
- Write new tests for new functionality or bug fixes.
|
||||
- Update existing tests if necessary.
|
||||
|
||||
## Submitting Contributions
|
||||
|
||||
### Pull Requests
|
||||
When you're ready to submit your changes:
|
||||
1. Push your branch to the remote repository.
|
||||
2. Open a Pull Request (PR) against the `main` branch.
|
||||
3. **Describe your changes:** Clearly explain what you changed and why. Reference any related issues.
|
||||
4. **Ensure tests pass:** All CI checks, including tests and linters, must pass. The command `poetry run pytest --workflow-environment=time-skipping` is a good one to run locally.
|
||||
5. **Request review:** Request a review from one or more maintainers.
|
||||
|
||||
## Reporting Bugs
|
||||
If you encounter a bug, please:
|
||||
1. **Search existing issues:** Check if the bug has already been reported.
|
||||
2. **Open a new issue:** If not, create a new issue.
|
||||
- Provide a clear and descriptive title.
|
||||
- Include steps to reproduce the bug.
|
||||
- Describe the expected behavior and what actually happened.
|
||||
- Provide details about your environment (OS, Python version, Temporal server version, etc.).
|
||||
- Include any relevant logs or screenshots.
|
||||
|
||||
## Suggesting Enhancements
|
||||
We welcome suggestions for new features or improvements!
|
||||
1. **Search existing issues/discussions:** See if your idea has already been discussed.
|
||||
2. **Open a new issue:**
|
||||
- Use a clear and descriptive title.
|
||||
- Provide a detailed explanation of the enhancement and its benefits.
|
||||
- Explain the use case or problem it solves.
|
||||
- Include any potential implementation ideas if you have them.
|
||||
|
||||
## Key Resources
|
||||
- **Project Overview**: [README.md](../README.md)
|
||||
- **Detailed Contribution & Development Guide**: [AGENTS.md](../AGENTS.md)
|
||||
- **Setup Instructions**: [setup.md](./setup.md)
|
||||
- **Comprehensive Testing Guide**: [testing.md](./testing.md) and [tests/README.md](../tests/README.md)
|
||||
- **System Architecture**: [architecture.md](./architecture.md)
|
||||
- **Architecture Decisions**: [architecture-decisions.md](./architecture-decisions.md)
|
||||
- **Customizing Agent Tools and Goals**: [adding-goals-and-tools.md](./adding-goals-and-tools.md)
|
||||
- **To-Do List / Future Enhancements**: [todo.md](./todo.md)
|
||||
163
docs/testing.md
@@ -1,163 +0,0 @@
|
||||
# Testing the Temporal AI Agent
|
||||
|
||||
This guide provides instructions for running the comprehensive test suite for the Temporal AI Agent project.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Install dependencies**:
|
||||
```bash
|
||||
poetry install --with dev
|
||||
```
|
||||
|
||||
2. **Run all tests**:
|
||||
```bash
|
||||
poetry run pytest
|
||||
```
|
||||
|
||||
3. **Run with time-skipping for faster execution**:
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Unit Tests
|
||||
- **Activity Tests**: `tests/test_tool_activities.py`
|
||||
- LLM integration (mocked)
|
||||
- Environment configuration
|
||||
- JSON processing
|
||||
- Dynamic tool execution
|
||||
|
||||
### Integration Tests
|
||||
- **Workflow Tests**: `tests/test_agent_goal_workflow.py`
|
||||
- Full workflow execution
|
||||
- Signal and query handling
|
||||
- State management
|
||||
- Error scenarios
|
||||
|
||||
## Running Specific Tests
|
||||
|
||||
```bash
|
||||
# Run only activity tests
|
||||
poetry run pytest tests/test_tool_activities.py -v
|
||||
|
||||
# Run only workflow tests
|
||||
poetry run pytest tests/test_agent_goal_workflow.py -v
|
||||
|
||||
# Run a specific test
|
||||
poetry run pytest tests/test_tool_activities.py::TestToolActivities::test_sanitize_json_response -v
|
||||
|
||||
# Run tests matching a pattern
|
||||
poetry run pytest -k "validation" -v
|
||||
```
|
||||
|
||||
## Test Environment Options
|
||||
|
||||
### Local Environment (Default)
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=local
|
||||
```
|
||||
|
||||
### Time-Skipping Environment (Recommended for CI)
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
### External Temporal Server
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=localhost:7233
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Tests can be configured with these environment variables:
|
||||
|
||||
- `LLM_MODEL`: Model for LLM testing (default: "openai/gpt-4")
|
||||
- `LLM_KEY`: API key for LLM service (mocked in tests)
|
||||
- `LLM_BASE_URL`: Custom LLM endpoint (optional)
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The test suite covers:
|
||||
|
||||
✅ **Workflows**
|
||||
- AgentGoalWorkflow initialization and execution
|
||||
- Signal handling (user_prompt, confirm, end_chat)
|
||||
- Query methods (conversation history, agent goal, tool data)
|
||||
- State management and conversation flow
|
||||
- Validation and error handling
|
||||
|
||||
✅ **Activities**
|
||||
- ToolActivities class methods
|
||||
- LLM integration (mocked)
|
||||
- Environment variable handling
|
||||
- JSON response processing
|
||||
- Dynamic tool activity execution
|
||||
|
||||
✅ **Integration**
|
||||
- End-to-end workflow execution
|
||||
- Activity registration in workers
|
||||
- Temporal client interactions
|
||||
|
||||
## Test Output
|
||||
|
||||
Successful test run example:
|
||||
```
|
||||
============================== test session starts ==============================
|
||||
platform darwin -- Python 3.11.3, pytest-8.3.5, pluggy-1.5.0
|
||||
rootdir: /Users/steveandroulakis/Documents/Code/agentic/temporal-demo/temporal-ai-agent
|
||||
configfile: pyproject.toml
|
||||
plugins: anyio-4.5.2, asyncio-0.26.0
|
||||
collected 21 items
|
||||
|
||||
tests/test_tool_activities.py::TestToolActivities::test_sanitize_json_response PASSED
|
||||
tests/test_tool_activities.py::TestToolActivities::test_parse_json_response_success PASSED
|
||||
tests/test_tool_activities.py::TestToolActivities::test_get_wf_env_vars_default_values PASSED
|
||||
...
|
||||
|
||||
============================== 21 passed in 12.5s ==============================
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Module not found errors**: Run `poetry install --with dev`
|
||||
2. **Async warnings**: These are expected with pytest-asyncio and can be ignored
|
||||
3. **Test timeouts**: Use `--workflow-environment=time-skipping` for faster execution
|
||||
4. **Import errors**: Check that you're running tests from the project root directory
|
||||
|
||||
### Debugging Tests
|
||||
|
||||
Enable verbose logging:
|
||||
```bash
|
||||
poetry run pytest --log-cli-level=DEBUG -s
|
||||
```
|
||||
|
||||
Run with coverage:
|
||||
```bash
|
||||
poetry run pytest --cov=workflows --cov=activities
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
For CI environments, use:
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=time-skipping --tb=short
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- See `tests/README.md` for detailed testing documentation
|
||||
- Review `tests/conftest.py` for available test fixtures
|
||||
- Check individual test files for specific test scenarios
|
||||
|
||||
## Test Architecture
|
||||
|
||||
The tests use:
|
||||
- **Temporal Testing Framework**: For workflow and activity testing
|
||||
- **pytest-asyncio**: For async test support
|
||||
- **unittest.mock**: For mocking external dependencies
|
||||
- **Test Fixtures**: For consistent test data and setup
|
||||
|
||||
All external dependencies (LLM calls, file I/O) are mocked to ensure fast, reliable tests.
|
||||
@@ -1,44 +0,0 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
import tools.tool_registry as tool_registry
|
||||
from goals.agent_selection import agent_selection_goals
|
||||
from goals.ecommerce import ecommerce_goals
|
||||
from goals.finance import finance_goals
|
||||
from goals.food import food_goals
|
||||
from goals.hr import hr_goals
|
||||
from goals.stripe_mcp import mcp_goals
|
||||
from goals.travel import travel_goals
|
||||
from models.tool_definitions import AgentGoal
|
||||
|
||||
goal_list: List[AgentGoal] = []
|
||||
goal_list.extend(agent_selection_goals)
|
||||
goal_list.extend(travel_goals)
|
||||
goal_list.extend(hr_goals)
|
||||
goal_list.extend(finance_goals)
|
||||
goal_list.extend(ecommerce_goals)
|
||||
goal_list.extend(mcp_goals)
|
||||
goal_list.extend(food_goals)
|
||||
|
||||
# for multi-goal, just set list agents as the last tool
|
||||
first_goal_value = os.getenv("AGENT_GOAL")
|
||||
if first_goal_value is None:
|
||||
multi_goal_mode = False # default to single agent mode if unset
|
||||
elif (
|
||||
first_goal_value is not None
|
||||
and first_goal_value.lower() == "goal_choose_agent_type"
|
||||
):
|
||||
multi_goal_mode = True
|
||||
else:
|
||||
multi_goal_mode = False
|
||||
|
||||
if multi_goal_mode:
|
||||
for goal in goal_list:
|
||||
list_agents_found: bool = False
|
||||
for tool in goal.tools:
|
||||
if tool.name == "ListAgents":
|
||||
list_agents_found = True
|
||||
continue
|
||||
if list_agents_found is False:
|
||||
goal.tools.append(tool_registry.list_agents_tool)
|
||||
continue
|
||||
@@ -1,106 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
import tools.tool_registry as tool_registry
|
||||
from models.tool_definitions import AgentGoal
|
||||
|
||||
# Turn on Silly Mode - this should be a description of the persona you'd like the bot to have and can be a single word or a phrase.
|
||||
# Example if you want the bot to be a specific person, like Mario or Christopher Walken, or to describe a specific tone:
|
||||
# SILLY_MODE="Christopher Walken"
|
||||
# SILLY_MODE="belligerent"
|
||||
#
|
||||
# Example if you want it to take on a persona (include 'a'):
|
||||
# SILLY_MODE="a pirate"
|
||||
# Note - this only works with certain LLMs. Grok for sure will stay in character, while OpenAI will not.
|
||||
SILLY_MODE = "off"
|
||||
if SILLY_MODE is not None and SILLY_MODE != "off":
|
||||
silly_prompt = "You are " + SILLY_MODE + ", stay in character at all times. "
|
||||
print("Silly mode is on: " + SILLY_MODE)
|
||||
else:
|
||||
silly_prompt = ""
|
||||
|
||||
starter_prompt_generic = (
|
||||
silly_prompt
|
||||
+ "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||
)
|
||||
|
||||
goal_choose_agent_type = AgentGoal(
|
||||
id="goal_choose_agent_type",
|
||||
category_tag="agent_selection",
|
||||
agent_name="Choose Agent",
|
||||
agent_friendly_description="Choose the type of agent to assist you today. You can always interrupt an existing agent to pick a new one.",
|
||||
tools=[
|
||||
tool_registry.list_agents_tool,
|
||||
tool_registry.change_goal_tool,
|
||||
],
|
||||
description="The user wants to choose which type of agent they will interact with. "
|
||||
"Help the user select an agent by gathering args for the Changegoal tool, in order: "
|
||||
"1. ListAgents: List agents available to interact with. Do not ask for user confirmation for this tool. "
|
||||
"2. ChangeGoal: Change goal of agent "
|
||||
"After these tools are complete, change your goal to the new goal as chosen by the user. ",
|
||||
starter_prompt=silly_prompt
|
||||
+ "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job. List all details of all agents as provided by the output of the first tool included in this goal. ",
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"agent: Here are the currently available agents.",
|
||||
"tool_result: { agents: 'agent_name': 'Event Flight Finder', 'goal_id': 'goal_event_flight_invoice', 'agent_description': 'Helps users find interesting events and arrange travel to them',"
|
||||
"'agent_name': 'Schedule PTO', 'goal_id': 'goal_hr_schedule_pto', 'agent_description': 'Schedule PTO based on your available PTO.' }",
|
||||
"agent: The available agents are: Event Flight Finder and Schedule PTO. \n Which agent would you like to work with? ",
|
||||
"user: I'd like to find an event and book flights using the Event Flight Finder",
|
||||
"user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>",
|
||||
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# Easter egg - if silly mode = a pirate, include goal_pirate_treasure as a "system" goal so it always shows up.
|
||||
# Can also turn make this goal available by setting the GOAL_CATEGORIES in the env file to include 'pirate', but if SILLY_MODE
|
||||
# is not 'a pirate', the interaction as a whole will be less pirate-y.
|
||||
pirate_category_tag = "pirate"
|
||||
if SILLY_MODE == "a pirate":
|
||||
pirate_category_tag = "system"
|
||||
goal_pirate_treasure = AgentGoal(
|
||||
id="goal_pirate_treasure",
|
||||
category_tag=pirate_category_tag,
|
||||
agent_name="Arrr, Find Me Treasure!",
|
||||
agent_friendly_description="Sail the high seas and find me pirate treasure, ye land lubber!",
|
||||
tools=[
|
||||
tool_registry.give_hint_tool,
|
||||
tool_registry.guess_location_tool,
|
||||
],
|
||||
description="The user wants to find a pirate treasure. "
|
||||
"Help the user gather args for these tools, in a loop, until treasure_found is True or the user requests to be done: "
|
||||
"1. GiveHint: If the user wants a hint regarding the location of the treasure, give them a hint. If they do not want a hint, this tool is optional."
|
||||
"2. GuessLocation: The user guesses where the treasure is, by giving an address. ",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to try to find the treasure",
|
||||
"agent: Sure! Do you want a hint?",
|
||||
"user: yes",
|
||||
"agent: Here is hint number 1!",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GiveHint tool>",
|
||||
"tool_result: { 'hint_number': 1, 'hint': 'The treasure is in the state of Arizona.' }",
|
||||
"agent: The treasure is in the state of Arizona. Would you like to guess the address of the treasure? ",
|
||||
"user: Yes, address is 123 Main St Phoenix, AZ",
|
||||
"agent: Let's see if you found the treasure...",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GuessLocation tool>"
|
||||
"tool_result: {'treasure_found':False}",
|
||||
"agent: Nope, that's not the right location! Do you want another hint?",
|
||||
"user: yes",
|
||||
"agent: Here is hint number 2.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GiveHint tool>",
|
||||
"tool_result: { 'hint_number': 2, 'hint': 'The treasure is in the city of Tucson, AZ.' }",
|
||||
"agent: The treasure is in the city of Tucson, AZ. Would you like to guess the address of the treasure? ",
|
||||
"user: Yes, address is 456 Main St Tucson, AZ",
|
||||
"agent: Let's see if you found the treasure...",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GuessLocation tool>",
|
||||
"tool_result: {'treasure_found':True}",
|
||||
"agent: Congratulations, Land Lubber, you've found the pirate treasure!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
agent_selection_goals: List[AgentGoal] = [
|
||||
goal_choose_agent_type,
|
||||
goal_pirate_treasure,
|
||||
]
|
||||
@@ -1,83 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
import tools.tool_registry as tool_registry
|
||||
from models.tool_definitions import AgentGoal
|
||||
|
||||
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||
|
||||
goal_ecomm_order_status = AgentGoal(
|
||||
id="goal_ecomm_order_status",
|
||||
category_tag="ecommerce",
|
||||
agent_name="Check Order Status",
|
||||
agent_friendly_description="Check the status of your order.",
|
||||
tools=[
|
||||
tool_registry.ecomm_get_order,
|
||||
tool_registry.ecomm_track_package,
|
||||
],
|
||||
description="The user wants to learn the status of a specific order. If the status is 'shipped' or 'delivered', they might want to get the package tracking information. To assist with that goal, help the user gather args for these tools in order: "
|
||||
"1. GetOrder: get information about an order"
|
||||
"2. TrackPackage: provide tracking information for the package. This tool is optional and should only be offered if the status is 'shipped' OR 'delivered' - otherwise, skip this tool and do not mention it to the user.",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to know the status of my order",
|
||||
"agent: Sure! I can help you out with that. May I have your order number?",
|
||||
"user: 102 ",
|
||||
"user_confirmed_tool_run: <user clicks confirm on GetOrderStatus tool>",
|
||||
"tool_result: { 'id': '102', 'summary': 'Red Sunglasses', 'email': 'matt.murdock@nelsonmurdock.com', 'status': 'shipped', 'order_date': '2025-04-01', 'last_order_update': '2025-04-06', 'tracking_id': '039813852990618' }",
|
||||
"agent: Your order 'Red Sunglasses,' placed April 1, 2025, was shipped on April 6, 2025. Would you like to see the tracking inforation?",
|
||||
"user: Yes",
|
||||
"user_confirmed_tool_run: <user clicks confirm on TrackPackage tool>",
|
||||
"tool_result: { 'scheduled_delivery_date': 'April 30, 2025', 'carrier': 'USPS', 'status_summary': 'Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035.', 'tracking_link': 'https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=12345','last_tracking_update': '2025-03-22T16:14:48'}",
|
||||
"agent: Your package is scheduled to be delivered on April 30, 2025 via USPS. Here is the most recent status from them regarding your package, updated as of March 22: \\n"
|
||||
"Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035. \\n"
|
||||
"You can find the full tracking details here: tracking_link !",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
goal_ecomm_list_orders = AgentGoal(
|
||||
id="goal_ecomm_list_orders",
|
||||
category_tag="ecommerce",
|
||||
agent_name="List All Orders",
|
||||
agent_friendly_description="List all orders for a user.",
|
||||
tools=[
|
||||
tool_registry.ecomm_list_orders,
|
||||
tool_registry.ecomm_get_order,
|
||||
tool_registry.ecomm_track_package,
|
||||
],
|
||||
description="The user wants to see all of their orders. They may want more details about specific orders, and if the status of an order is 'shipped' or 'delivered', they might want to get the package tracking information. To assist with that goal, help the user gather args for this tool: "
|
||||
"1. ListOrders: list orders for a user"
|
||||
" and then offer the following tools, in a loop, until the user indicates they are done:"
|
||||
"2. GetOrder: get information about an order. This tool is optional."
|
||||
"3. TrackPackage: provide tracking information for the package. This tool is optional and should only be offered if the status is 'shipped' OR 'delivered' - otherwise, skip this tool and do not mention it to the user.",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to see all of my orders.",
|
||||
"agent: Sure! I can help you out with that. May I have your email address?",
|
||||
"user: email is bob.johnson@emailzzz.com ",
|
||||
"user_confirmed_tool_run: <user clicks confirm on ListOrders tool>",
|
||||
"tool_result: a list of orders including [{'id': '102', 'summary': 'Red Sunglasses', 'email': 'matt.murdock@nelsonmurdock.com', 'status': 'shipped', 'order_date': '2025-04-01', 'last_order_update': '2025-04-06', 'tracking_id': '039813852990618' }, { 'id': '103', 'summary': 'Blue Sunglasses', 'email': 'matt.murdock@nelsonmurdock.com', 'status': 'paid', 'order_date': '2025-04-03', 'last_order_update': '2025-04-07' }]",
|
||||
"agent: Your orders are as follows: \\n",
|
||||
"1. Red Sunglasses, ordered 4/1/2025 \\n",
|
||||
"2. Blue Sunglasses, ordered 4/3/2025 \\n",
|
||||
"Would you like more information about any of your orders?"
|
||||
"user: Yes, the Red Sunglasses",
|
||||
"agent: Your order 'Red Sunglasses,' placed April 1, 2025, was shipped on April 6, 2025. Would you like to see the tracking inforation?",
|
||||
"user: Yes",
|
||||
"user_confirmed_tool_run: <user clicks confirm on TrackPackage tool>",
|
||||
"tool_result: { 'scheduled_delivery_date': 'April 30, 2025', 'carrier': 'USPS', 'status_summary': 'Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035.', 'tracking_link': 'https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=12345','last_tracking_update': '2025-03-22T16:14:48'}",
|
||||
"agent: Your package is scheduled to be delivered on April 30, 2025 via USPS. Here is the most recent status from them regarding your package \\n, updated as of March 22: \\n"
|
||||
"Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035. \\n"
|
||||
"You can find the full tracking details here: tracking_link ! \\n"
|
||||
"Would you like more information about any of your other orders?",
|
||||
"user: No" "agent: Thanks, and have a great day!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
ecommerce_goals: List[AgentGoal] = [
|
||||
goal_ecomm_order_status,
|
||||
goal_ecomm_list_orders,
|
||||
]
|
||||
111
goals/finance.py
@@ -1,111 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
import tools.tool_registry as tool_registry
|
||||
from models.tool_definitions import AgentGoal
|
||||
|
||||
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||
|
||||
goal_fin_check_account_balances = AgentGoal(
|
||||
id="goal_fin_check_account_balances",
|
||||
category_tag="fin",
|
||||
agent_name="Account Balances",
|
||||
agent_friendly_description="Check your account balances in Checking, Savings, etc.",
|
||||
tools=[
|
||||
tool_registry.financial_check_account_is_valid,
|
||||
tool_registry.financial_get_account_balances,
|
||||
],
|
||||
description="The user wants to check their account balances 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",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to check my account balances",
|
||||
"agent: Sure! I can help you out with that. May I have your email address and account number?",
|
||||
"user: email is bob.johnson@emailzzz.com ",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||
"tool_result: { 'status': account valid }",
|
||||
"agent: Great! I can tell you what the your account balances are.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FinCheckAccountBalance tool>",
|
||||
"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 "
|
||||
"Bitcoin: 0.1378 \\n "
|
||||
"Thanks for being a customer since 2014!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
goal_fin_move_money = AgentGoal(
|
||||
id="goal_fin_move_money",
|
||||
category_tag="fin",
|
||||
agent_name="Money Movement",
|
||||
agent_friendly_description="Initiate money movement.",
|
||||
tools=[
|
||||
tool_registry.financial_check_account_is_valid,
|
||||
tool_registry.financial_get_account_balances,
|
||||
tool_registry.financial_move_money,
|
||||
],
|
||||
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 money movement (transfer)",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"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: my account number is 11235 and my email address is matt.murdock@nelsonmurdock.com",
|
||||
"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>",
|
||||
"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 completed! New account balance: $2700.15. Your confirmation id is 333421. ",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
goal_fin_loan_application = AgentGoal(
|
||||
id="goal_fin_loan_application",
|
||||
category_tag="fin",
|
||||
agent_name="Easy Loan",
|
||||
agent_friendly_description="Initiate a simple loan application.",
|
||||
tools=[
|
||||
tool_registry.financial_check_account_is_valid,
|
||||
tool_registry.financial_submit_loan_approval,
|
||||
],
|
||||
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 and email address to validate your account?",
|
||||
"user: account number is 11235813",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||
"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: <user clicks confirm on FinCheckAccountSubmitLoanApproval tool>",
|
||||
"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. ",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
finance_goals: List[AgentGoal] = [
|
||||
goal_fin_check_account_balances,
|
||||
goal_fin_move_money,
|
||||
goal_fin_loan_application,
|
||||
]
|
||||
@@ -1,84 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
import tools.tool_registry as tool_registry
|
||||
from models.tool_definitions import AgentGoal
|
||||
from shared.mcp_config import get_stripe_mcp_server_definition
|
||||
|
||||
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||
|
||||
goal_food_ordering = AgentGoal(
|
||||
id="goal_food_ordering",
|
||||
category_tag="food",
|
||||
agent_name="Food Ordering Assistant",
|
||||
agent_friendly_description="Order food from Tony's Pizza Palace using Stripe for payment processing. Browse menu, add items to your order, and check out securely. Please ensure context carries over between tool runs.",
|
||||
tools=[tool_registry.food_add_to_cart_tool],
|
||||
mcp_server_definition=get_stripe_mcp_server_definition(
|
||||
included_tools=[
|
||||
"list_products",
|
||||
"list_prices",
|
||||
"create_customer",
|
||||
"create_invoice",
|
||||
"create_invoice_item",
|
||||
"finalize_invoice",
|
||||
]
|
||||
),
|
||||
description="The user wants to order food from Tony's Pizza Palace. "
|
||||
"First, help the user browse the menu by calling list_products. "
|
||||
"When they express interest in items, get pricing using list_prices. "
|
||||
"Add items to their cart using AddToCart as they decide - the order doesn't matter, multiple items can be added. "
|
||||
"After they're done selecting items, get their customer details and create a Stripe customer. "
|
||||
"For checkout: 1) create_invoice, 2) create_invoice_item for each individual item (IMPORTANT: create_invoice_item does NOT accept quantity parameter - call it once per item, so if user wants 2 pizzas, call create_invoice_item twice with the same price), "
|
||||
"3) finalize_invoice. The finalized invoice will contain a hosted_invoice_url for payment.",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I want to order food.",
|
||||
"agent: Welcome to Tony's Pizza Palace! I can help you browse our menu, add items to your cart, and process payment. Let me show you what we have available.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on list_products tool with limit=100 and product.metadata.use_case == 'food_ordering_demo'>",
|
||||
'tool_result: {"products": [{"id": "prod_pepperoni", "name": "Pepperoni Pizza", "description": "A timeless classic featuring pepperoni"}, {"id": "prod_margherita", "name": "Margherita Pizza", "description": "Fresh mozzarella and basil"}, {"id": "prod_garlic_bread", "name": "Garlic Bread", "description": "Warm crusty bread with garlic butter"}, {"id": "prod_soda", "name": "Soda", "description": "A large bottle of cola"}]}',
|
||||
"agent: We have Pepperoni Pizza, Margherita Pizza, Garlic Bread, and Soda available. What would you like to order?",
|
||||
"user: I'd like 2 pepperoni pizzas and a soda.",
|
||||
"agent: Excellent! Let me get the pricing for the pepperoni pizzas first so you know the cost.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on list_prices tool with product='prod_pepperoni'>",
|
||||
'tool_result: {"prices": [{"id": "price_pepperoni_large", "product": "prod_pepperoni", "unit_amount": 2299, "currency": "usd"}]}',
|
||||
"agent: Pepperoni Pizza is $22.99 each. Now let me check the soda pricing:",
|
||||
"user_confirmed_tool_run: <user clicks confirm on list_prices tool with product='prod_soda'>",
|
||||
'tool_result: {"prices": [{"id": "price_soda", "product": "prod_soda", "unit_amount": 250, "currency": "usd"}]}',
|
||||
"agent: And Soda is $2.50. So 2 Pepperoni Pizzas ($22.99 each) plus 1 Soda ($2.50) = $48.48 total. What's your email address to add these items to your cart?",
|
||||
"user: jane.smith@example.com",
|
||||
"agent: Perfect! Adding 2 Pepperoni Pizzas to your cart for jane.smith@example.com. Note: I'm using quantity=2 since you want 2 pizzas.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on AddToCart tool with customer_email='jane.smith@example.com', item_name='Pepperoni Pizza', item_price=22.99, quantity=2, stripe_product_id='prod_pepperoni'>",
|
||||
'tool_result: {"status": "success", "message": "Added 2 x Pepperoni Pizza ($22.99) to cart for jane.smith@example.com", "item_added": {"quantity": 2, "name": "Pepperoni Pizza"}}',
|
||||
"agent: Great! Added 2 Pepperoni Pizzas to your cart. Now adding the Soda (quantity=1):",
|
||||
"user_confirmed_tool_run: <user clicks confirm on AddToCart tool with customer_email='jane.smith@example.com', item_name='Soda', item_price=2.50, quantity=1, stripe_product_id='prod_soda'>",
|
||||
'tool_result: {"status": "success", "message": "Added 1 x Soda ($2.50) to cart for jane.smith@example.com", "item_added": {"quantity": 1, "name": "Soda"}}',
|
||||
"agent: Perfect! Added 1 Soda to your cart. Your cart now contains: 2 Pepperoni Pizzas ($22.99 each) and 1 Soda ($2.50). What else would you like to order?",
|
||||
"user: Nothing, that's all.",
|
||||
"agent: Great! Your order is complete with 2 Pepperoni Pizzas and 1 Soda for a total of $48.48. Now let's proceed to checkout. I'll need your full name to create your customer profile.",
|
||||
"user: My name is Jane Smith.",
|
||||
"agent: Thank you Jane! Creating your Stripe customer profile with name and email.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_customer tool with name='Jane Smith' and email='jane.smith@example.com'>",
|
||||
'tool_result: {"customer": {"id": "cus_ExAmPlE12345", "name": "Jane Smith", "email": "jane.smith@example.com"}}',
|
||||
"agent: Customer profile created! Now I'll create a draft invoice for your order.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_invoice tool with customer='cus_ExAmPlE12345'>",
|
||||
'tool_result: {"invoice": {"id": "in_InvOicE54321", "status": "draft", "customer": "cus_ExAmPlE12345"}}',
|
||||
"agent: Invoice created! Now adding your first Pepperoni Pizza to the invoice.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_pepperoni_large'>",
|
||||
'tool_result: {"status": "success", "invoice_item": {"id": "ii_pizza1"}}',
|
||||
"agent: First pizza added! Now adding your second Pepperoni Pizza to the invoice.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_pepperoni_large'>",
|
||||
'tool_result: {"status": "success", "invoice_item": {"id": "ii_pizza2"}}',
|
||||
"agent: Both pizzas added! Now adding your Soda to the invoice.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_soda'>",
|
||||
'tool_result: {"status": "success", "invoice_item": {"id": "ii_soda"}}',
|
||||
"agent: All items added to invoice! Now finalizing to generate the payment link.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on finalize_invoice tool with invoice='in_InvOicE54321'>",
|
||||
'tool_result: {"invoice": {"id": "in_InvOicE54321", "status": "open", "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RKeJRMlw?s=ap", "amount_due": 4848}}',
|
||||
"agent: Perfect! Your order is complete with 2 Pepperoni Pizzas and 1 Soda. Total: $48.48. Pay securely here: https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RKeJRMlw?s=ap\\\\n\\\\nThank you for ordering from Tony's Pizza Palace!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
food_goals: List[AgentGoal] = [
|
||||
goal_food_ordering,
|
||||
]
|
||||
97
goals/hr.py
@@ -1,97 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
import tools.tool_registry as tool_registry
|
||||
from models.tool_definitions import AgentGoal
|
||||
|
||||
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||
|
||||
goal_hr_schedule_pto = AgentGoal(
|
||||
id="goal_hr_schedule_pto",
|
||||
category_tag="hr",
|
||||
agent_name="Schedule PTO",
|
||||
agent_friendly_description="Schedule PTO based on your available PTO.",
|
||||
tools=[
|
||||
tool_registry.current_pto_tool,
|
||||
tool_registry.future_pto_calc_tool,
|
||||
tool_registry.book_pto_tool,
|
||||
],
|
||||
description="The user wants to schedule paid time off (PTO) after today's date. To assist with that goal, help the user gather args for these tools in order: "
|
||||
"1. CurrentPTO: Tell the user how much PTO they currently have "
|
||||
"2. FuturePTOCalc: Tell the user how much PTO they will have as of the prospective future date "
|
||||
"3. BookPTO: Book PTO after user types 'yes'",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to schedule some time off",
|
||||
"agent: Sure! Let's start by determining how much PTO you currently have. May I have your email address?",
|
||||
"user: bob.johnson@emailzzz.com",
|
||||
"agent: Great! I can tell you how much PTO you currently have accrued.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on CurrentPTO tool>",
|
||||
"tool_result: { 'num_hours': 400, 'num_days': 50 }",
|
||||
"agent: You have 400 hours, or 50 days, of PTO available. What dates would you like to take your time off? ",
|
||||
"user: Dec 1 through Dec 5",
|
||||
"agent: Let's check if you'll have enough PTO accrued by Dec 1 of this year to accomodate that.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FuturePTO tool>"
|
||||
'tool_result: {"enough_pto": True, "pto_hrs_remaining_after": 410}',
|
||||
"agent: You do in fact have enough PTO to accommodate that, and will have 410 hours remaining after you come back. Do you want to book the PTO? ",
|
||||
"user: yes ",
|
||||
"user_confirmed_tool_run: <user clicks confirm on BookPTO tool>",
|
||||
'tool_result: { "status": "success" }',
|
||||
"agent: PTO successfully booked! ",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
goal_hr_check_pto = AgentGoal(
|
||||
id="goal_hr_check_pto",
|
||||
category_tag="hr",
|
||||
agent_name="Check PTO Amount",
|
||||
agent_friendly_description="Check your available PTO.",
|
||||
tools=[
|
||||
tool_registry.current_pto_tool,
|
||||
],
|
||||
description="The user wants to check their paid time off (PTO) after today's date. To assist with that goal, help the user gather args for these tools in order: "
|
||||
"1. CurrentPTO: Tell the user how much PTO they currently have ",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to check my time off amounts at the current time",
|
||||
"agent: Sure! I can help you out with that. May I have your email address?",
|
||||
"user: bob.johnson@emailzzz.com",
|
||||
"agent: Great! I can tell you how much PTO you currently have accrued.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on CurrentPTO tool>",
|
||||
"tool_result: { 'num_hours': 400, 'num_days': 50 }",
|
||||
"agent: You have 400 hours, or 50 days, of PTO available.",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
goal_hr_check_paycheck_bank_integration_status = AgentGoal(
|
||||
id="goal_hr_check_paycheck_bank_integration_status",
|
||||
category_tag="hr",
|
||||
agent_name="Check paycheck deposit status",
|
||||
agent_friendly_description="Check your integration between your employer and your financial institution.",
|
||||
tools=[
|
||||
tool_registry.paycheck_bank_integration_status_check,
|
||||
],
|
||||
description="The user wants to check their bank integration used to deposit their paycheck. To assist with that goal, help the user gather args for these tools in order: "
|
||||
"1. CheckPayBankStatus: Tell the user the status of their paycheck bank integration ",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to check paycheck bank integration",
|
||||
"agent: Sure! I can help you out with that. May I have your email address?",
|
||||
"user: bob.johnson@emailzzz.com",
|
||||
"agent: Great! I can tell you what the status is for your paycheck bank integration.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on CheckPayBankStatus tool>",
|
||||
"tool_result: { 'status': connected }",
|
||||
"agent: Your paycheck bank deposit integration is properly connected.",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
hr_goals: List[AgentGoal] = [
|
||||
goal_hr_schedule_pto,
|
||||
goal_hr_check_pto,
|
||||
goal_hr_check_paycheck_bank_integration_status,
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
from models.tool_definitions import AgentGoal
|
||||
from shared.mcp_config import get_stripe_mcp_server_definition
|
||||
|
||||
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||
|
||||
goal_mcp_stripe = AgentGoal(
|
||||
id="goal_mcp_stripe",
|
||||
category_tag="mcp-integrations",
|
||||
agent_name="Stripe MCP Agent",
|
||||
agent_friendly_description="Manage Stripe operations via MCP",
|
||||
tools=[], # Will be populated dynamically
|
||||
mcp_server_definition=get_stripe_mcp_server_definition(included_tools=[]),
|
||||
description="Help manage Stripe operations for customer and product data by using the customers.read and products.read tools.",
|
||||
starter_prompt="Welcome! I can help you read Stripe customer and product information.",
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"agent: Welcome! I can help you read Stripe customer and product information. What would you like to do first?",
|
||||
"user: what customers are there?",
|
||||
"agent: I'll check for customers now.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on customers.read tool>",
|
||||
'tool_result: { "customers": [{"id": "cus_abc", "name": "Customer A"}, {"id": "cus_xyz", "name": "Customer B"}] }',
|
||||
"agent: I found two customers: Customer A and Customer B. Can I help with anything else?",
|
||||
"user: what products exist?",
|
||||
"agent: Let me get the list of products for you.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on products.read tool>",
|
||||
'tool_result: { "products": [{"id": "prod_123", "name": "Gold Plan"}, {"id": "prod_456", "name": "Silver Plan"}] }',
|
||||
"agent: I found two products: Gold Plan and Silver Plan.",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
mcp_goals: List[AgentGoal] = [
|
||||
goal_mcp_stripe,
|
||||
]
|
||||
@@ -1,96 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
import tools.tool_registry as tool_registry
|
||||
from models.tool_definitions import AgentGoal
|
||||
|
||||
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||
|
||||
goal_match_train_invoice = AgentGoal(
|
||||
id="goal_match_train_invoice",
|
||||
category_tag="travel-trains",
|
||||
agent_name="UK Premier League Match Trip Booking",
|
||||
agent_friendly_description="Book a trip to a city in the UK around the dates of a premier league match.",
|
||||
tools=[
|
||||
tool_registry.search_fixtures_tool,
|
||||
tool_registry.search_trains_tool,
|
||||
tool_registry.book_trains_tool,
|
||||
tool_registry.create_invoice_tool,
|
||||
],
|
||||
description="The user wants to book a trip to a city in the UK around the dates of a premier league match. "
|
||||
"Help the user find a premier league match to attend, search and book trains for that match and offers to invoice them for the cost of train tickets. "
|
||||
"The user lives in London. Premier league fixtures may be mocked data, so don't worry about valid season dates and teams. "
|
||||
"Gather args for these tools in order, ensuring you move the user from one tool to the next: "
|
||||
"1. SearchFixtures: Search for fixtures for a team within a specified date range. The user might ask questions about the matches dates and locations to decide on where to go. "
|
||||
"2. SearchTrains: Search for trains to the city of the match. Ensure you list them for the customer to choose from "
|
||||
"3. BookTrains: Book the train tickets, used to invoice the user for the cost of the train tickets "
|
||||
"4. CreateInvoice: Invoices the user for the cost of train tickets, with total and details inferred from the conversation history ",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to travel to a premier league match",
|
||||
"agent: Sure! Let's start by finding a match you'd like to attend. I know about Premier League fixtures in the UK. Could you tell me which team and what date range you're interested in?"
|
||||
"user: Wolves between mid-April and the first week of May, please."
|
||||
"agent: Great! Let's find fixtures for Wolverhampton Wanderers FC between 2025-04-15 and 2025-05-05."
|
||||
"user_confirmed_tool_run: <user clicks confirm on SearchFixtures tool, passing the full team name as an input>",
|
||||
'tool_result: SearchFixtures responds in this JSON format and the results are parsed by the agent below {"fixtures": [{"awayTeam": "West Ham United FC","date": "2025-04-01","homeTeam": "Wolverhampton Wanderers FC"},...,',
|
||||
"agent: I found 4 Wolves matches in May 2025: 1. Man City vs Wolves (Away) - May 3 2. Wolves vs Brighton (Home) - May 10 3. Crystal Palace vs Wolves (Away) - May 18 4. Wolves vs Brentford (Home) - May 25 Which match would you like to attend? I'll help you find trains from London.",
|
||||
"user: How about the Man City match",
|
||||
"agent: Great! Let's find trains to Manchester and back for the match.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on SearchTrains tool>",
|
||||
'tool_result: SearchTrains responds in this JSON format and the results are parsed by the agent below: {"journeys": [{"arrival": "Manchester","arrival_time": "2025-04-18T12:11","departure": "London","departure_time": "2025-04-18T11:00","id": "T8049","platform": "1","price": 73.5,"type": "outbound"},',
|
||||
"agent: I found several train options for your journey to Manchester including outbound options on May 9 at 11am, 3:30pm and 4:30pm and return options on May 11 at 9am, 11am and 1pm. Which trains would you like to book?",
|
||||
"user: I'd like the 11am outbound and 3:30pm return",
|
||||
"agent: Sounds good, let's book train tickets for the 11am outbound and 3:30pm return trains on May 9 and May 11, one day before and after the match.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on BookTrains tool>",
|
||||
'tool_result: BookTrains responds in this JSON format and the results are parsed by the agent belowg [{"booking_reference": "BR10879","journey_id": "T9532,T1027","status": "confirmed"}]',
|
||||
"agent: Your train tickets have been booked with the reference ABCD1234. Are you ready to be invoiced for the total cost of the train tickets?",
|
||||
"user: Yes, pelase invoice me.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on CreateInvoice tool which includes details of the train journey, the match, and the total cost>",
|
||||
'tool_result: CreateInvoice responds in this JSON format and the results are parsed by the agent below (including a link to the invoice): {"invoiceStatus": "open","invoiceURL": "https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RLeJRMlw?s=ap","reference": "F9B01CD7-0001"}',
|
||||
"agent: Great! I've generated your invoice for your trains to the Manchester City match on the 10th of May. You can view and pay your invoice at this link: https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RLeJRMlw?s=ap <this is the end of the chat>",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
goal_event_flight_invoice = AgentGoal(
|
||||
id="goal_event_flight_invoice",
|
||||
category_tag="travel-flights",
|
||||
agent_name="Australia and New Zealand Event Flight Booking",
|
||||
agent_friendly_description="Book a trip to a city in Australia or New Zealand around the dates of events in that city.",
|
||||
tools=[
|
||||
tool_registry.find_events_tool,
|
||||
tool_registry.search_flights_tool,
|
||||
tool_registry.create_invoice_tool,
|
||||
],
|
||||
description="Help the user gather args for these tools in order: "
|
||||
"1. FindEvents: Find an event in Australia or New Zealand to travel to "
|
||||
"2. SearchFlights: search for a flight around the event dates "
|
||||
"3. CreateInvoice: Create a simple invoice for the cost of that flight ",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I'd like to travel to an event",
|
||||
"agent: Sure! Let's start by finding an event you'd like to attend. I know about events in Australia and New Zealand cities. Could you tell me which city and month you're interested in?",
|
||||
"user: sydney in may please",
|
||||
"agent: Great! Let's find an events in Sydney in May.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on FindEvents tool>",
|
||||
"tool_result: { 'event_name': 'Vivid Sydney', 'event_date': '2023-05-01' }",
|
||||
"agent: Found an event! There's Vivid Sydney on May 1 2025, ending on May 14 2025. Would you like to search for flights around these dates?",
|
||||
"user: Yes, please",
|
||||
"agent: Let's search for flights around these dates. Could you provide your departure city?",
|
||||
"user: San Francisco",
|
||||
"agent: Thanks, searching for flights from San Francisco to Sydney around 2023-02-25 to 2023-02-28.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on SearchFlights tool>"
|
||||
'tool_result: results including {"flight_number": "CX101", "return_flight_number": "CX102", "price": 850.0}',
|
||||
"agent: Found some flights! The cheapest is CX101 for $850. Would you like to generate an invoice for this flight?",
|
||||
"user_confirmed_tool_run: <user clicks confirm on CreateInvoice tool>",
|
||||
'tool_result: { "status": "success", "invoice": { "flight_number": "CX101", "amount": 850.0 }, invoiceURL: "https://example.com/invoice" }',
|
||||
"agent: Invoice generated! Here's the link: https://example.com/invoice",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
travel_goals: List[AgentGoal] = [
|
||||
goal_match_train_invoice,
|
||||
goal_event_flight_invoice,
|
||||
]
|
||||
@@ -1,6 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Deque, Dict, List, Literal, Optional, Union
|
||||
|
||||
from typing import Optional, Deque, Dict, Any, List, Union, Literal
|
||||
from models.tool_definitions import AgentGoal
|
||||
|
||||
|
||||
@@ -44,14 +43,12 @@ class ValidationResult:
|
||||
if self.validationFailedReason is None:
|
||||
self.validationFailedReason = {}
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvLookupInput:
|
||||
show_confirm_env_var_name: str
|
||||
show_confirm_default: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvLookupOutput:
|
||||
show_confirm: bool
|
||||
multi_goal_mode: bool
|
||||
multi_goal_mode: bool
|
||||
@@ -1,17 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPServerDefinition:
|
||||
"""Definition for an MCP (Model Context Protocol) server connection"""
|
||||
|
||||
name: str
|
||||
command: str
|
||||
args: List[str]
|
||||
env: Optional[Dict[str, str]] = None
|
||||
connection_type: str = "stdio"
|
||||
included_tools: Optional[List[str]] = None
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -27,7 +15,6 @@ class ToolDefinition:
|
||||
description: str
|
||||
arguments: List[ToolArgument]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentGoal:
|
||||
id: str
|
||||
@@ -37,5 +24,6 @@ class AgentGoal:
|
||||
tools: List[ToolDefinition]
|
||||
description: str = "Description of the tools purpose and overall goal"
|
||||
starter_prompt: str = "Initial prompt to start the conversation"
|
||||
example_conversation_history: str = "Example conversation history to help the AI agent understand the context of the conversation"
|
||||
mcp_server_definition: Optional[MCPServerDefinition] = None
|
||||
example_conversation_history: str = (
|
||||
"Example conversation history to help the AI agent understand the context of the conversation"
|
||||
)
|
||||
|
||||
615
poetry.lock
generated
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from models.tool_definitions import AgentGoal
|
||||
from typing import Optional
|
||||
import json
|
||||
|
||||
MULTI_GOAL_MODE: bool = None
|
||||
|
||||
@@ -11,7 +10,6 @@ def generate_genai_prompt(
|
||||
conversation_history: str,
|
||||
multi_goal_mode: bool,
|
||||
raw_json: Optional[str] = None,
|
||||
mcp_tools_info: Optional[dict] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generates a concise prompt for producing or validating JSON instructions
|
||||
@@ -23,8 +21,7 @@ def generate_genai_prompt(
|
||||
# Intro / Role
|
||||
prompt_lines.append(
|
||||
"You are an AI agent that helps fill required arguments for the tools described below. "
|
||||
"CRITICAL: You must respond with ONLY valid JSON using the exact schema provided. "
|
||||
"DO NOT include any text before or after the JSON. Your entire response must be parseable JSON."
|
||||
"You must respond with valid JSON ONLY, using the schema provided in the instructions."
|
||||
)
|
||||
|
||||
# Main Conversation History
|
||||
@@ -50,35 +47,11 @@ def generate_genai_prompt(
|
||||
prompt_lines.append("END EXAMPLE")
|
||||
prompt_lines.append("")
|
||||
|
||||
# Add MCP server context if present
|
||||
if agent_goal.mcp_server_definition:
|
||||
prompt_lines.append("=== MCP Server Information ===")
|
||||
prompt_lines.append(
|
||||
f"Connected to MCP Server: {agent_goal.mcp_server_definition.name}"
|
||||
)
|
||||
if mcp_tools_info and mcp_tools_info.get("success", False):
|
||||
tools = mcp_tools_info.get("tools", {})
|
||||
server_name = mcp_tools_info.get("server_name", "Unknown")
|
||||
prompt_lines.append(
|
||||
f"MCP Tools loaded from {server_name} ({len(tools)} tools):"
|
||||
)
|
||||
for tool_name, tool_info in tools.items():
|
||||
prompt_lines.append(
|
||||
f" - {tool_name}: {tool_info.get('description', 'No description')}"
|
||||
)
|
||||
else:
|
||||
prompt_lines.append("Additional tools available via MCP integration:")
|
||||
prompt_lines.append("")
|
||||
|
||||
# Tools Definitions
|
||||
prompt_lines.append("=== Tools Definitions ===")
|
||||
prompt_lines.append(f"There are {len(agent_goal.tools)} available tools:")
|
||||
prompt_lines.append(", ".join([t.name for t in agent_goal.tools]))
|
||||
prompt_lines.append(f"Goal: {agent_goal.description}")
|
||||
prompt_lines.append(
|
||||
"CRITICAL: You MUST follow the complete sequence described in the Goal above. "
|
||||
"Do NOT skip steps or assume the goal is complete until ALL steps are done."
|
||||
)
|
||||
prompt_lines.append(
|
||||
"Gather the necessary information for each tool in the sequence described above."
|
||||
)
|
||||
@@ -98,12 +71,9 @@ def generate_genai_prompt(
|
||||
)
|
||||
|
||||
# JSON Format Instructions
|
||||
prompt_lines.append("=== CRITICAL: JSON-ONLY RESPONSE FORMAT ===")
|
||||
prompt_lines.append("=== Instructions for JSON Generation ===")
|
||||
prompt_lines.append(
|
||||
"MANDATORY: Your response must be ONLY valid JSON with NO additional text.\n"
|
||||
"NO explanations, NO comments, NO text before or after the JSON.\n"
|
||||
"Your entire response must start with '{' and end with '}'.\n\n"
|
||||
"Required JSON format:\n"
|
||||
"Your JSON format must be:\n"
|
||||
"{\n"
|
||||
' "response": "<plain text>",\n'
|
||||
' "next": "<question|confirm|pick-new-goal|done>",\n'
|
||||
@@ -113,43 +83,29 @@ def generate_genai_prompt(
|
||||
' "<arg2>": "<value2 or null>",\n'
|
||||
" ...\n"
|
||||
" }\n"
|
||||
"}\n\n"
|
||||
"INVALID EXAMPLE: 'Thank you for providing... {\"response\": ...}'\n"
|
||||
'VALID EXAMPLE: \'{"response": "Thank you for providing...", "next": ...}\''
|
||||
"}"
|
||||
)
|
||||
prompt_lines.append(
|
||||
"DECISION LOGIC (follow this exact order):\n"
|
||||
"1) Do I need to run a tool next?\n"
|
||||
" - If your response says 'let's get/proceed/check/add/create/finalize...' -> YES, you need a tool\n"
|
||||
" - If you're announcing what you're about to do -> YES, you need a tool\n"
|
||||
" - If no more steps needed for current goal -> NO, go to step 3\n\n"
|
||||
"2) If YES to step 1: Do I have all required arguments?\n"
|
||||
" - Check tool definition for required args\n"
|
||||
" - Can I fill missing args from conversation history?\n"
|
||||
" - Can I use sensible defaults (limit=100, etc.)?\n"
|
||||
" - If ALL args available/inferrable -> set next='confirm', specify tool and args\n"
|
||||
" - If missing required args -> set next='question', ask for missing args, tool=null\n\n"
|
||||
"3) If NO to step 1: Is the entire goal complete?\n"
|
||||
" - Check Goal description in system prompt - are ALL steps done?\n"
|
||||
" - Check recent conversation for completion indicators ('finalized', 'complete', etc.)\n"
|
||||
f" - If complete -> {generate_toolchain_complete_guidance()}\n"
|
||||
" - If not complete -> identify next needed tool, go to step 2\n\n"
|
||||
"CRITICAL RULES:\n"
|
||||
"• RESPOND WITH JSON ONLY - NO TEXT BEFORE OR AFTER THE JSON OBJECT\n"
|
||||
"• Your response must start with '{' and end with '}' - nothing else\n"
|
||||
"• NEVER set next='question' without asking an actual question in your response\n"
|
||||
"• NEVER set tool=null when you're announcing you'll run a specific tool\n"
|
||||
"• If response contains 'let's proceed to get pricing' -> next='confirm', tool='list_prices'\n"
|
||||
"• If response contains 'Now adding X' -> next='confirm', tool='create_invoice_item'\n"
|
||||
"• Use conversation history to infer arguments (customer IDs, product IDs, etc.)\n"
|
||||
"• Use sensible defaults rather than asking users for technical parameters\n"
|
||||
"• Carry forward arguments between tools (same customer, same invoice, etc.)\n"
|
||||
"• If force_confirm='False' in history, be declarative, don't ask permission\n\n"
|
||||
"EXAMPLES:\n"
|
||||
"WRONG: response='let\\'s get pricing', next='question', tool=null\n"
|
||||
"RIGHT: response='let\\'s get pricing', next='confirm', tool='list_prices'\n"
|
||||
"WRONG: response='adding pizza', next='question', tool='create_invoice_item'\n"
|
||||
"RIGHT: response='adding pizza', next='confirm', tool='create_invoice_item'\n"
|
||||
"1) If any required argument is missing, set next='question' and ask the user.\n"
|
||||
"2) If all required arguments are known, set next='confirm' and specify the tool.\n"
|
||||
" The user will confirm before the tool is run.\n"
|
||||
f"3) {generate_toolchain_complete_guidance()}\n"
|
||||
"4) response should be short and user-friendly.\n\n"
|
||||
"Guardrails (always remember!)\n"
|
||||
"1) If any required argument is missing, set next='question' and ask the user.\n"
|
||||
"1) ALWAYS ask a question in your response if next='question'.\n"
|
||||
"2) ALWAYS set next='confirm' if you have arguments\n "
|
||||
'And respond with "let\'s proceed with <tool> (and any other useful info)" \n '
|
||||
+ "DON'T set next='confirm' if you have a question to ask.\n"
|
||||
"EXAMPLE: If you have a question to ask, set next='question' and ask the user.\n"
|
||||
"3) You can carry over arguments from one tool to another.\n "
|
||||
"EXAMPLE: If you asked for an account ID, then use the conversation history to infer that argument "
|
||||
"going forward."
|
||||
"4) If ListAgents in the conversation history is force_confirm='False', you MUST check "
|
||||
+ "if the current tool contains userConfirmation. If it does, please ask the user to confirm details "
|
||||
+ "with the user. userConfirmation overrides force_confirm='False'.\n"
|
||||
+ "EXAMPLE: (force_confirm='False' AND userConfirmation exists on tool) Would you like me to <run tool> "
|
||||
+ "with the following details: <details>?\n"
|
||||
)
|
||||
|
||||
# Validation Task (If raw_json is provided)
|
||||
@@ -166,16 +122,11 @@ def generate_genai_prompt(
|
||||
|
||||
# Prompt Start
|
||||
prompt_lines.append("")
|
||||
prompt_lines.append("=== FINAL REMINDER ===")
|
||||
prompt_lines.append("RESPOND WITH VALID JSON ONLY. NO ADDITIONAL TEXT.")
|
||||
prompt_lines.append("")
|
||||
if raw_json is not None:
|
||||
prompt_lines.append(
|
||||
"Validate the provided JSON and return ONLY corrected JSON."
|
||||
)
|
||||
prompt_lines.append("Begin by validating the provided JSON if necessary.")
|
||||
else:
|
||||
prompt_lines.append(
|
||||
"Return ONLY a valid JSON response. Start with '{' and end with '}'."
|
||||
"Begin by producing a valid JSON response for the next tool or question."
|
||||
)
|
||||
|
||||
return "\n".join(prompt_lines)
|
||||
@@ -264,7 +215,7 @@ def generate_pick_new_goal_guidance() -> str:
|
||||
str: A prompt string prompting the LLM to when to go to pick-new-goal
|
||||
"""
|
||||
if is_multi_goal_mode():
|
||||
return 'Next should only be "pick-new-goal" if EVERY SINGLE STEP in the Goal description has been completed (check the system prompt Goal section carefully), or the user explicitly requested to pick a new goal. If any step is missing (like customer creation, invoice creation, or payment processing), continue with the next required tool.'
|
||||
return 'Next should only be "pick-new-goal" if all tools have been run for the current goal (use the system prompt to figure that out), or the user explicitly requested to pick a new goal.'
|
||||
else:
|
||||
return 'Next should never be "pick-new-goal".'
|
||||
|
||||
@@ -280,6 +231,6 @@ def generate_toolchain_complete_guidance() -> str:
|
||||
str: A prompt string prompting the LLM to prompt for a new goal, or be done
|
||||
"""
|
||||
if is_multi_goal_mode():
|
||||
return "If no more tools are needed for the current goal (EVERY step in the Goal description has been completed AND user_confirmed_tool_run has been run for all required tools), set next='pick-new-goal' and tool=null to allow the user to choose their next action."
|
||||
return "If no more tools are needed (user_confirmed_tool_run has been run for all), set next='confirm' and tool='ListAgents'."
|
||||
else:
|
||||
return "If no more tools are needed (EVERY step in the Goal description has been completed AND user_confirmed_tool_run has been run for all), set next='done' and tool=null."
|
||||
return "If no more tools are needed (user_confirmed_tool_run has been run for all), set next='done' and tool=''."
|
||||
|
||||
@@ -10,7 +10,7 @@ authors = [
|
||||
]
|
||||
readme = "README.md"
|
||||
|
||||
# By default, Poetry will find packages automatically,
|
||||
# By default, Poetry will find packages automatically,
|
||||
# but explicitly including them is fine:
|
||||
packages = [
|
||||
{ include = "**/*.py", from = "." }
|
||||
@@ -38,16 +38,13 @@ uvicorn = "^0.34.0"
|
||||
python-dotenv = "^1.0.1"
|
||||
requests = "^2.32.3"
|
||||
pandas = "^2.2.3"
|
||||
stripe = "^11.4.1"
|
||||
gtfs-kit = "^10.1.1"
|
||||
fastmcp = "^2.7.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = ">=8.2"
|
||||
pytest-asyncio = "^0.26.0"
|
||||
black = "^23.7"
|
||||
isort = "^5.12"
|
||||
mypy = "^1.16.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.4.0"]
|
||||
@@ -58,16 +55,4 @@ asyncio_mode = "auto"
|
||||
log_cli = true
|
||||
log_cli_level = "INFO"
|
||||
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
norecursedirs = ["vibe"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
ignore_missing_imports = true
|
||||
check_untyped_defs = true
|
||||
namespace_packages = true
|
||||
explicit_package_bases = true
|
||||
ignore_errors = true
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
@@ -1,12 +1,12 @@
|
||||
import asyncio
|
||||
|
||||
from shared.config import get_temporal_client
|
||||
|
||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||
|
||||
|
||||
async def main():
|
||||
# Create client connected to server at the given address
|
||||
client = await get_temporal_client()
|
||||
client = await Client.connect("localhost:7233")
|
||||
|
||||
workflow_id = "agent-workflow"
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
|
||||
from tools.search_flights import search_flights
|
||||
import json
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
|
||||
from tools.search_flights import search_flights
|
||||
import json
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Suppose user typed "new" for New York, "lon" for London
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import asyncio
|
||||
|
||||
import concurrent.futures
|
||||
|
||||
from temporalio.worker import Worker
|
||||
|
||||
from activities.tool_activities import dynamic_tool_activity
|
||||
from shared.config import TEMPORAL_LEGACY_TASK_QUEUE, get_temporal_client
|
||||
|
||||
from shared.config import get_temporal_client, TEMPORAL_LEGACY_TASK_QUEUE
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -22,9 +24,7 @@ async def main():
|
||||
activity_executor=activity_executor,
|
||||
)
|
||||
|
||||
print(
|
||||
f"Starting legacy worker, connecting to task queue: {TEMPORAL_LEGACY_TASK_QUEUE}"
|
||||
)
|
||||
print(f"Starting legacy worker, connecting to task queue: {TEMPORAL_LEGACY_TASK_QUEUE}")
|
||||
await worker.run()
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
|
||||
from temporalio.worker import Worker
|
||||
|
||||
from activities.tool_activities import (
|
||||
ToolActivities,
|
||||
dynamic_tool_activity,
|
||||
mcp_list_tools,
|
||||
)
|
||||
from shared.config import TEMPORAL_TASK_QUEUE, get_temporal_client
|
||||
from activities.tool_activities import ToolActivities, dynamic_tool_activity
|
||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||
|
||||
from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE
|
||||
|
||||
|
||||
async def main():
|
||||
# Load environment variables
|
||||
@@ -52,7 +49,7 @@ async def main():
|
||||
print("===========================================================\n")
|
||||
|
||||
print("Worker ready to process tasks!")
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.basicConfig(level=logging.WARN)
|
||||
|
||||
# Run the worker
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
||||
@@ -64,9 +61,7 @@ async def main():
|
||||
activities.agent_validatePrompt,
|
||||
activities.agent_toolPlanner,
|
||||
activities.get_wf_env_vars,
|
||||
activities.mcp_tool_activity,
|
||||
dynamic_tool_activity,
|
||||
mcp_list_tools,
|
||||
],
|
||||
activity_executor=activity_executor,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ from shared.config import get_temporal_client
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
# Connect to Temporal and signal the workflow
|
||||
client = await get_temporal_client()
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ If you want to show confirmations/enable the debugging UI that shows tool args,
|
||||
```bash
|
||||
SHOW_CONFIRM=True
|
||||
```
|
||||
We recommend setting this to `False` in most cases, as it can clutter the conversation with confirmation messages.
|
||||
|
||||
### Quick Start with Makefile
|
||||
|
||||
@@ -44,44 +43,24 @@ make help
|
||||
|
||||
### Manual Setup (Alternative to Makefile)
|
||||
|
||||
If you prefer to run commands manually, see the sections below for detailed instructions on setting up the backend, frontend, and other components.
|
||||
If you prefer to run commands manually, follow these steps:
|
||||
|
||||
### Agent Goal Configuration
|
||||
|
||||
The agent can be configured to pursue different goals using the `AGENT_GOAL` environment variable in your `.env` file.
|
||||
The agent can be configured to pursue different goals using the `AGENT_GOAL` environment variable in your `.env` file. If unset, default is `goal_choose_agent_type`.
|
||||
|
||||
**Single Agent Mode (Default)**
|
||||
By default, the agent operates in single-agent mode using a specific goal. If unset, the default is `goal_event_flight_invoice`.
|
||||
|
||||
To set a specific single goal:
|
||||
```bash
|
||||
AGENT_GOAL=goal_event_flight_invoice
|
||||
```
|
||||
|
||||
**Multi-Agent Mode (Experimental)**
|
||||
The agent also supports an experimental multi-agent mode where users can choose between different agent types during the conversation. To enable this mode:
|
||||
|
||||
```bash
|
||||
AGENT_GOAL=goal_choose_agent_type
|
||||
```
|
||||
|
||||
When using multi-agent mode, you can control which agent categories are available using `GOAL_CATEGORIES` in your `.env` file. If unset, all categories are shown. Available categories include `hr`, `travel-flights`, `travel-trains`, `fin`, `ecommerce`, `mcp-integrations`, and `food`.
|
||||
We recommend starting with `fin`:
|
||||
If the first goal is `goal_choose_agent_type` the agent will support multiple goals using goal categories defined by `GOAL_CATEGORIES` in your .env file. If unset, default is all. We recommend starting with `fin`.
|
||||
```bash
|
||||
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin
|
||||
```
|
||||
|
||||
**Note:** Multi-agent mode is experimental and allows switching between different agents mid-conversation, but single-agent mode provides a more focused experience.
|
||||
|
||||
MCP (Model Context Protocol) tools are available for enhanced integration with external services. See the [MCP Tools Configuration](#mcp-tools-configuration) section for setup details.
|
||||
|
||||
See the section Goal-Specific Tool Configuration below for tool configuration for specific goals.
|
||||
|
||||
### LLM Configuration
|
||||
|
||||
Note: We recommend using OpenAI's GPT-4o or Claude 3.5 Sonnet for the best results. There can be significant differences in performance and capabilities between models, especially for complex tasks.
|
||||
|
||||
The agent uses LiteLLM to interact with various LLM providers. Configure the following environment variables in your `.env` file:
|
||||
The agent uses LiteLLM to interact with various LLM providers. Configure theqfollowing environment variables in your `.env` file:
|
||||
|
||||
- `LLM_MODEL`: The model to use (e.g., "openai/gpt-4o", "anthropic/claude-3-sonnet", "google/gemini-pro", etc.)
|
||||
- `LLM_KEY`: Your API key for the selected provider
|
||||
@@ -188,40 +167,7 @@ npm install
|
||||
npx vite
|
||||
```
|
||||
Access the UI at `http://localhost:5173`
|
||||
|
||||
|
||||
## MCP Tools Configuration
|
||||
|
||||
MCP (Model Context Protocol) tools enable integration with external services without custom implementation. The system automatically handles MCP server lifecycle and tool discovery.
|
||||
|
||||
### Adding MCP Tools to Goals
|
||||
Configure MCP servers in your goal definitions using either:
|
||||
1. Predefined configurations from `shared/mcp_config.py`
|
||||
2. Custom `MCPServerDefinition` objects
|
||||
|
||||
Example using Stripe MCP Server:
|
||||
```python
|
||||
from shared.mcp_config import get_stripe_mcp_server_definition
|
||||
|
||||
mcp_server_definition=get_stripe_mcp_server_definition(
|
||||
included_tools=["list_products", "create_customer", "create_invoice"]
|
||||
)
|
||||
```
|
||||
|
||||
See the file `goals/stripe_mcp.py` for an example of how to use MCP tools in a an `AgentGoal`.
|
||||
|
||||
### MCP Environment Variables
|
||||
Set required API keys and configuration in your `.env` file:
|
||||
```bash
|
||||
# For Stripe MCP Server
|
||||
STRIPE_API_KEY=sk_test_your_stripe_key_here
|
||||
```
|
||||
`goal_event_flight_invoice` does not require a Stripe key. If `STRIPE_API_KEY` is unset, that scenario falls back to a mock invoice.
|
||||
|
||||
#### Accessing Your Test API Keys
|
||||
It's free to sign up for a Stripe account and generate test keys (no real money is involved). Use the Developers Dashboard to create, reveal, delete, and rotate API keys. Navigate to the API Keys tab in your dashboard or visit [https://dashboard.stripe.com/test/apikeys](https://dashboard.stripe.com/test/apikeys) directly.
|
||||
|
||||
For detailed guidance on adding MCP tools, see [adding-goals-and-tools.md](./adding-goals-and-tools.md).
|
||||
|
||||
|
||||
## Goal-Specific Tool Configuration
|
||||
Here is configuration guidance for specific goals. Travel and financial goals have configuration & setup as below.
|
||||
@@ -229,31 +175,28 @@ Here is configuration guidance for specific goals. Travel and financial goals ha
|
||||
- `AGENT_GOAL=goal_event_flight_invoice` - Helps users find events, book flights, and arrange train travel with invoice generation
|
||||
- This is the scenario in the [original video](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
||||
|
||||
#### Configuring Agent Goal: goal_event_flight_invoice
|
||||
#### Configuring Agent Goal: goal_event_flight_invoice
|
||||
* The agent uses a mock function to search for events. This has zero configuration.
|
||||
* **Flight Search**: The agent intelligently handles flight searches:
|
||||
* **Default behavior**: If no `RAPIDAPI_KEY` is set, the agent generates realistic flight data with smart pricing based on route type (domestic, international, trans-Pacific)
|
||||
* **Real API (optional)**: To use live flight data, set `RAPIDAPI_KEY` in your `.env` file
|
||||
* It's free to sign up at [RapidAPI](https://rapidapi.com/apiheya/api/sky-scrapper)
|
||||
* This API might be slow to respond, so you may want to increase the start to close timeout, `TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT` in `workflows/workflow_helpers.py`
|
||||
* The smart generation creates realistic pricing (e.g., US-Australia routes $1200-1800, domestic flights $200-800) with appropriate airlines for each region
|
||||
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in `.env`
|
||||
* It's free to sign up and get a key at [Stripe](https://stripe.com/) (test mode only, no real money)
|
||||
* By default the agent uses a mock function to search for flights.
|
||||
* If you want to use the real flights API, go to `tools/search_flights.py` and replace the `search_flights` function with `search_flights_real_api` that exists in the same file.
|
||||
* It's free to sign up at [RapidAPI](https://rapidapi.com/apiheya/api/sky-scrapper)
|
||||
* This api might be slow to respond, so you may want to increase the start to close timeout, `TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT` in `workflows/workflow_helpers.py`
|
||||
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env
|
||||
* It's free to sign up and get a key at [Stripe](https://stripe.com/)
|
||||
* Set permissions for read-write on: `Credit Notes, Invoices, Customers and Customer Sessions`
|
||||
* If you don't have a Stripe key, comment out the `STRIPE_API_KEY` in the `.env` file, and a dummy invoice will be created rather than a Stripe invoice. The function can be found in `tools/create_invoice.py` – this is the default behavior for `goal_event_flight_invoice`.
|
||||
* If you don't have a Stripe key, comment out the STRIPE_API_KEY in the .env file, and a dummy invoice will be created rather than a Stripe invoice. The function can be found in `tools/create_invoice.py`
|
||||
|
||||
### Goal: Find a Premier League match, book train tickets to it and invoice the user for the cost (Replay 2025 Keynote)
|
||||
- `AGENT_GOAL=goal_match_train_invoice` - Focuses on Premier League match attendance with train booking and invoice generation
|
||||
- This goal was part of [Temporal's Replay 2025 conference keynote demo](https://www.youtube.com/watch?v=YDxAWrIBQNE)
|
||||
- Note, there is failure built in to this demo (the train booking step) to show how the agent can handle failures and retry. See Tool Configuration below for details.
|
||||
#### Configuring Agent Goal: goal_match_train_invoice
|
||||
#### Configuring Agent Goal: goal_match_train_invoice
|
||||
NOTE: This goal was developed for an on-stage demo and has failure (and its resolution) built in to show how the agent can handle failures and retry.
|
||||
* Omit `FOOTBALL_DATA_API_KEY` from .env for the `SearchFixtures` tool to automatically return mock Premier League fixtures. Finding a real match requires a key from [Football Data](https://www.football-data.org). Sign up for a free account, then see the 'My Account' page to get your API token.
|
||||
* Finding a match requires a key from [Football Data](https://www.football-data.org). Sign up for a free account, then see the 'My Account' page to get your API token. Set `FOOTBALL_DATA_API_KEY` to this value. If the key is omitted, the `SearchFixtures` tool automatically returns mock Premier League fixtures (3 months into the future only).
|
||||
* We use a mock function to search for trains. Start the train API server to use the real API: `python thirdparty/train_api.py`
|
||||
* * The train activity is 'enterprise' so it's written in C# and requires a .NET runtime. See the [.NET backend](#net-(enterprise)-backend) section for details on running it.
|
||||
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in `.env`
|
||||
* It's free to sign up and get a key at [Stripe](https://stripe.com/) (test mode only)
|
||||
* If the key is missing this goal won't generate a real invoice – only `goal_event_flight_invoice` falls back to a mock invoice
|
||||
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env
|
||||
* It's free to sign up and get a key at [Stripe](https://stripe.com/)
|
||||
* If you're lazy go to `tools/create_invoice.py` and replace the `create_invoice` function with the mock `create_invoice_example` that exists in the same file.
|
||||
|
||||
##### Python Search Trains API
|
||||
@@ -269,15 +212,15 @@ poetry run python thirdparty/train_api.py
|
||||
|
||||
##### Python Train Legacy Worker
|
||||
> Agent Goal: goal_match_train_invoice only
|
||||
|
||||
|
||||
These are Python activities that fail (raise NotImplemented) to show how Temporal handles a failure. You can run these activities with.
|
||||
|
||||
|
||||
```bash
|
||||
poetry run python scripts/run_legacy_worker.py
|
||||
poetry run python scripts/run_legacy_worker.py
|
||||
```
|
||||
|
||||
|
||||
The activity will fail and be retried infinitely. To rescue the activity (and its corresponding workflows), kill the worker and run the .NET one in the section below.
|
||||
|
||||
|
||||
##### .NET (enterprise) Worker ;)
|
||||
We have activities written in C# to call the train APIs.
|
||||
```bash
|
||||
@@ -290,12 +233,12 @@ If you're running your train API above on a different host/port then change the
|
||||
#### 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.
|
||||
- `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
|
||||
```
|
||||
- `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.
|
||||
- `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
|
||||
@@ -307,19 +250,12 @@ Make sure you have the mock users you want in (such as yourself) in [the PTO moc
|
||||
#### Goals: Ecommerce
|
||||
Make sure you have the mock orders you want in (such as those with real tracking numbers) in [the mock orders file](./tools/data/customer_order_data.json).
|
||||
|
||||
### Goal: Food Ordering with MCP Integration (Stripe Payment Processing)
|
||||
- `AGENT_GOAL=goal_food_ordering` - Demonstrates food ordering with Stripe payment processing via MCP
|
||||
- Uses Stripe's MCP Server ([Agent Toolkit](https://github.com/stripe/agent-toolkit/tree/main/modelcontextprotocol)) for payment operations
|
||||
- Requires `STRIPE_API_KEY` in your `.env` file
|
||||
- Requires products in Stripe with metadata key `use_case=food_ordering_demo`. Run `tools/food/setup/create_stripe_products.py` to set up pizza menu items
|
||||
- Example of MCP tool integration without custom implementation
|
||||
- This is an excellent demonstration of MCP (Model Context Protocol) capabilities
|
||||
|
||||
|
||||
## Customizing the Agent Further
|
||||
- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them)
|
||||
- `goals/` 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
|
||||
- The tools themselves are defined in their own files in `/tools`
|
||||
- Note the mapping in `tools/__init__.py` to each tool
|
||||
|
||||
For more details, check out [adding goals and tools guide](./adding-goals-and-tools.md).
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from temporalio.client import Client
|
||||
from temporalio.service import TLSConfig
|
||||
@@ -10,16 +9,13 @@ load_dotenv(override=True)
|
||||
TEMPORAL_ADDRESS = os.getenv("TEMPORAL_ADDRESS", "localhost:7233")
|
||||
TEMPORAL_NAMESPACE = os.getenv("TEMPORAL_NAMESPACE", "default")
|
||||
TEMPORAL_TASK_QUEUE = os.getenv("TEMPORAL_TASK_QUEUE", "agent-task-queue")
|
||||
TEMPORAL_LEGACY_TASK_QUEUE = os.getenv(
|
||||
"TEMPORAL_LEGACY_TASK_QUEUE", "agent-task-queue-legacy"
|
||||
)
|
||||
TEMPORAL_LEGACY_TASK_QUEUE = os.getenv("TEMPORAL_LEGACY_TASK_QUEUE", "agent-task-queue-legacy")
|
||||
|
||||
# Authentication settings
|
||||
TEMPORAL_TLS_CERT = os.getenv("TEMPORAL_TLS_CERT", "")
|
||||
TEMPORAL_TLS_KEY = os.getenv("TEMPORAL_TLS_KEY", "")
|
||||
TEMPORAL_API_KEY = os.getenv("TEMPORAL_API_KEY", "")
|
||||
|
||||
|
||||
async def get_temporal_client() -> Client:
|
||||
"""
|
||||
Creates a Temporal client based on environment configuration.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import os
|
||||
|
||||
from models.tool_definitions import MCPServerDefinition
|
||||
|
||||
|
||||
def get_stripe_mcp_server_definition(included_tools: list[str]) -> MCPServerDefinition:
|
||||
"""
|
||||
Returns a Stripe MCP server definition with customizable included tools.
|
||||
|
||||
Args:
|
||||
included_tools: List of tool names to include from the Stripe MCP server
|
||||
|
||||
Returns:
|
||||
MCPServerDefinition configured for Stripe
|
||||
"""
|
||||
return MCPServerDefinition(
|
||||
name="stripe-mcp",
|
||||
command="npx",
|
||||
args=[
|
||||
"-y",
|
||||
"@stripe/mcp",
|
||||
"--tools=all",
|
||||
f"--api-key={os.getenv('STRIPE_API_KEY')}",
|
||||
],
|
||||
env=None,
|
||||
included_tools=included_tools,
|
||||
)
|
||||
350
tests/README.md
@@ -1,350 +0,0 @@
|
||||
# Temporal AI Agent - Testing Guide
|
||||
|
||||
This directory contains comprehensive tests for the Temporal AI Agent project. The tests cover workflows, activities, and integration scenarios using Temporal's testing framework.
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── README.md # This file - testing documentation
|
||||
├── conftest.py # Test configuration and fixtures
|
||||
├── test_agent_goal_workflow.py # Workflow tests
|
||||
├── test_tool_activities.py # Activity tests
|
||||
└── workflowtests/ # Legacy workflow tests
|
||||
└── agent_goal_workflow_test.py
|
||||
```
|
||||
|
||||
## Test Types
|
||||
|
||||
### 1. Workflow Tests (`test_agent_goal_workflow.py`)
|
||||
|
||||
Tests the main `AgentGoalWorkflow` class covering:
|
||||
|
||||
- **Workflow Initialization**: Basic workflow startup and state management
|
||||
- **Signal Handling**: Testing user_prompt, confirm, end_chat signals
|
||||
- **Query Methods**: Testing all workflow query endpoints
|
||||
- **State Management**: Conversation history, goal changes, tool data
|
||||
- **Validation Flow**: Prompt validation and error handling
|
||||
- **Tool Execution Flow**: Confirmation and tool execution cycles
|
||||
|
||||
### 2. Activity Tests (`test_tool_activities.py`)
|
||||
|
||||
Tests the `ToolActivities` class and `dynamic_tool_activity` function:
|
||||
|
||||
- **LLM Integration**: Testing agent_toolPlanner with mocked LLM responses
|
||||
- **Validation Logic**: Testing agent_validatePrompt with various scenarios
|
||||
- **Environment Configuration**: Testing get_wf_env_vars with different env setups
|
||||
- **JSON Processing**: Testing response parsing and sanitization
|
||||
- **Dynamic Tool Execution**: Testing the dynamic activity dispatcher
|
||||
- **Integration**: End-to-end activity execution in Temporal workers
|
||||
|
||||
### 3. Configuration Tests (`conftest.py`)
|
||||
|
||||
Provides shared test fixtures and configuration:
|
||||
|
||||
- **Temporal Environment**: Local and time-skipping test environments
|
||||
- **Sample Data**: Pre-configured agent goals, conversation history, inputs
|
||||
- **Test Client**: Configured Temporal client for testing
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Ensure you have the required dependencies installed:
|
||||
|
||||
```bash
|
||||
poetry install --with dev
|
||||
```
|
||||
|
||||
### Basic Test Execution
|
||||
|
||||
Run all tests:
|
||||
```bash
|
||||
poetry run pytest
|
||||
```
|
||||
|
||||
Run specific test files:
|
||||
```bash
|
||||
# Workflow tests only
|
||||
poetry run pytest tests/test_agent_goal_workflow.py
|
||||
|
||||
# Activity tests only
|
||||
poetry run pytest tests/test_tool_activities.py
|
||||
|
||||
# Legacy tests
|
||||
poetry run pytest tests/workflowtests/
|
||||
```
|
||||
|
||||
Run with verbose output:
|
||||
```bash
|
||||
poetry run pytest -v
|
||||
```
|
||||
|
||||
### Test Environment Options
|
||||
|
||||
The tests support different Temporal environments via the `--workflow-environment` flag:
|
||||
|
||||
#### Local Environment (Default)
|
||||
Uses a local Temporal test server:
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=local
|
||||
```
|
||||
|
||||
#### Time-Skipping Environment
|
||||
Uses Temporal's time-skipping test environment for faster execution:
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
#### External Server
|
||||
Connect to an existing Temporal server:
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=localhost:7233
|
||||
```
|
||||
|
||||
#### Setup Script for AI Agent environments such as OpenAI Codex
|
||||
```bash
|
||||
export SHELL=/bin/bash
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
ls
|
||||
poetry install --with dev
|
||||
cd frontend
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
# Pre-download the temporal test server binary
|
||||
poetry run python3 -c "
|
||||
import asyncio
|
||||
import sys
|
||||
from temporalio.testing import WorkflowEnvironment
|
||||
|
||||
async def predownload():
|
||||
try:
|
||||
print('Starting test server download...')
|
||||
env = await WorkflowEnvironment.start_time_skipping()
|
||||
print('Test server downloaded and started successfully')
|
||||
await env.shutdown()
|
||||
print('Test server shut down successfully')
|
||||
except Exception as e:
|
||||
print(f'Error during download: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
asyncio.run(predownload())
|
||||
"
|
||||
```
|
||||
|
||||
### Filtering Tests
|
||||
|
||||
Run tests by pattern:
|
||||
```bash
|
||||
# Run only validation tests
|
||||
poetry run pytest -k "validation"
|
||||
|
||||
# Run only workflow tests
|
||||
poetry run pytest -k "workflow"
|
||||
|
||||
# Run only activity tests
|
||||
poetry run pytest -k "activity"
|
||||
```
|
||||
|
||||
Run tests by marker (if you add custom markers):
|
||||
```bash
|
||||
# Run only integration tests
|
||||
poetry run pytest -m integration
|
||||
|
||||
# Skip slow tests
|
||||
poetry run pytest -m "not slow"
|
||||
```
|
||||
|
||||
## Test Configuration
|
||||
|
||||
### Test Discovery
|
||||
|
||||
The `vibe/` directory is excluded from test collection to avoid conflicts with sample tests. This is configured in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
norecursedirs = ["vibe"]
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Tests respect the following environment variables:
|
||||
|
||||
- `LLM_MODEL`: Model to use for LLM testing (defaults to "openai/gpt-4")
|
||||
- `LLM_KEY`: API key for LLM service
|
||||
- `LLM_BASE_URL`: Custom base URL for LLM service
|
||||
- `SHOW_CONFIRM`: Whether to show confirmation dialogs
|
||||
- `AGENT_GOAL`: Default agent goal setting
|
||||
|
||||
### Mocking Strategy
|
||||
|
||||
The tests use extensive mocking to avoid external dependencies:
|
||||
|
||||
- **LLM Calls**: Mocked using `unittest.mock` to avoid actual API calls
|
||||
- **Tool Handlers**: Mocked to test workflow logic without tool execution
|
||||
- **Environment Variables**: Patched for consistent test environments
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### Test Naming Convention
|
||||
|
||||
- Test files: `test_<module_name>.py`
|
||||
- Test classes: `Test<ClassName>`
|
||||
- Test methods: `test_<functionality>_<scenario>`
|
||||
|
||||
Example:
|
||||
```python
|
||||
class TestAgentGoalWorkflow:
|
||||
async def test_user_prompt_signal_valid_input(self, client, sample_combined_input):
|
||||
# Test implementation
|
||||
pass
|
||||
```
|
||||
|
||||
### Using Fixtures
|
||||
|
||||
Leverage the provided fixtures for consistent test data:
|
||||
|
||||
```python
|
||||
async def test_my_workflow(self, client, sample_agent_goal, sample_conversation_history):
|
||||
# client: Temporal test client
|
||||
# sample_agent_goal: Pre-configured AgentGoal
|
||||
# sample_conversation_history: Sample conversation data
|
||||
pass
|
||||
```
|
||||
|
||||
### Mocking External Dependencies
|
||||
|
||||
Always mock external services:
|
||||
|
||||
```python
|
||||
@patch('activities.tool_activities.completion')
|
||||
async def test_llm_integration(self, mock_completion):
|
||||
mock_completion.return_value.choices[0].message.content = '{"test": "response"}'
|
||||
# Test implementation
|
||||
```
|
||||
|
||||
### Testing Workflow Signals and Queries
|
||||
|
||||
```python
|
||||
async def test_workflow_signal(self, client, sample_combined_input):
|
||||
# Start workflow
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
sample_combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Send signal
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "test message")
|
||||
|
||||
# Query state
|
||||
conversation = await handle.query(AgentGoalWorkflow.get_conversation_history)
|
||||
|
||||
# End workflow
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
result = await handle.result()
|
||||
```
|
||||
|
||||
## Test Data and Fixtures
|
||||
|
||||
### Sample Agent Goal
|
||||
|
||||
The `sample_agent_goal` fixture provides a basic agent goal with:
|
||||
- Goal ID: "test_goal"
|
||||
- One test tool with a required string argument
|
||||
- Suitable for most workflow testing scenarios
|
||||
|
||||
### Sample Conversation History
|
||||
|
||||
The `sample_conversation_history` fixture provides:
|
||||
- Basic user and agent message exchange
|
||||
- Proper message format for testing
|
||||
|
||||
### Sample Combined Input
|
||||
|
||||
The `sample_combined_input` fixture provides:
|
||||
- Complete workflow input with agent goal and tool params
|
||||
- Conversation summary and prompt queue
|
||||
- Ready for workflow execution
|
||||
|
||||
## Debugging Tests
|
||||
|
||||
### Verbose Logging
|
||||
|
||||
Enable detailed logging:
|
||||
```bash
|
||||
poetry run pytest --log-cli-level=DEBUG -s
|
||||
```
|
||||
|
||||
### Temporal Web UI
|
||||
|
||||
When using local environment, access Temporal Web UI at http://localhost:8233 to inspect workflow executions during tests.
|
||||
|
||||
### Test Isolation
|
||||
|
||||
Each test uses unique task queue names to prevent interference:
|
||||
```python
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
|
||||
```yaml
|
||||
name: Test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- run: pip install poetry
|
||||
- run: poetry install --with dev
|
||||
- run: poetry run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
Generate coverage reports:
|
||||
```bash
|
||||
poetry add --group dev pytest-cov
|
||||
poetry run pytest --cov=workflows --cov=activities --cov-report=html
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Mock External Dependencies**: Always mock LLM calls, file I/O, and network requests
|
||||
2. **Use Time-Skipping**: For CI/CD, prefer time-skipping environment for speed
|
||||
3. **Unique Identifiers**: Use UUIDs for workflow IDs and task queues
|
||||
4. **Clean Shutdown**: Always end workflows properly in tests
|
||||
5. **Descriptive Names**: Use clear, descriptive test names
|
||||
6. **Test Edge Cases**: Include error scenarios and validation failures
|
||||
7. **Keep Tests Fast**: Use mocks to avoid slow external calls
|
||||
8. **Isolate Tests**: Ensure tests don't depend on each other
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Workflow Timeout**: Increase timeouts or use time-skipping environment
|
||||
2. **Mock Not Working**: Check patch decorators and import paths
|
||||
3. **Test Hanging**: Ensure workflows are properly ended with signals
|
||||
4. **Environment Issues**: Check environment variable settings
|
||||
|
||||
### Getting Help
|
||||
|
||||
- Check Temporal Python SDK documentation
|
||||
- Review existing test patterns in the codebase
|
||||
- Use `poetry run pytest --collect-only` to verify test discovery
|
||||
- Run with `-v` flag for detailed output
|
||||
|
||||
## Legacy Tests
|
||||
|
||||
The `workflowtests/` directory contains legacy tests. New tests should be added to the main `tests/` directory following the patterns established in this guide.
|
||||
@@ -41,12 +41,7 @@ def event_loop():
|
||||
async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]:
|
||||
env_type = request.config.getoption("--workflow-environment")
|
||||
if env_type == "local":
|
||||
env = await WorkflowEnvironment.start_local(
|
||||
dev_server_extra_args=[
|
||||
"--dynamic-config-value",
|
||||
"frontend.enableExecuteMultiOperation=true",
|
||||
]
|
||||
)
|
||||
env = await WorkflowEnvironment.start_local()
|
||||
elif env_type == "time-skipping":
|
||||
env = await WorkflowEnvironment.start_time_skipping()
|
||||
else:
|
||||
@@ -58,54 +53,3 @@ async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]:
|
||||
@pytest_asyncio.fixture
|
||||
async def client(env: WorkflowEnvironment) -> Client:
|
||||
return env.client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_agent_goal():
|
||||
"""Sample agent goal for testing."""
|
||||
from models.tool_definitions import AgentGoal, ToolArgument, ToolDefinition
|
||||
|
||||
return AgentGoal(
|
||||
id="test_goal",
|
||||
category_tag="test",
|
||||
agent_name="TestAgent",
|
||||
agent_friendly_description="A test agent for testing purposes",
|
||||
description="Test goal for agent testing",
|
||||
tools=[
|
||||
ToolDefinition(
|
||||
name="TestTool",
|
||||
description="A test tool for testing purposes",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="test_arg", type="string", description="A test argument"
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_conversation_history():
|
||||
"""Sample conversation history for testing."""
|
||||
return {
|
||||
"messages": [
|
||||
{"actor": "user", "response": "Hello, I need help with testing"},
|
||||
{"actor": "agent", "response": "I can help you with that"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_combined_input(sample_agent_goal):
|
||||
"""Sample combined input for workflow testing."""
|
||||
from collections import deque
|
||||
|
||||
from models.data_types import AgentGoalWorkflowParams, CombinedInput
|
||||
|
||||
tool_params = AgentGoalWorkflowParams(
|
||||
conversation_summary="Test conversation summary",
|
||||
prompt_queue=deque(), # Start with empty queue for most tests
|
||||
)
|
||||
|
||||
return CombinedInput(agent_goal=sample_agent_goal, tool_params=tool_params)
|
||||
|
||||
@@ -1,543 +0,0 @@
|
||||
import uuid
|
||||
|
||||
from temporalio import activity
|
||||
from temporalio.client import Client
|
||||
from temporalio.worker import Worker
|
||||
|
||||
from models.data_types import (
|
||||
AgentGoalWorkflowParams,
|
||||
CombinedInput,
|
||||
EnvLookupInput,
|
||||
EnvLookupOutput,
|
||||
ToolPromptInput,
|
||||
ValidationInput,
|
||||
ValidationResult,
|
||||
)
|
||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||
|
||||
|
||||
class TestAgentGoalWorkflow:
|
||||
"""Test cases for AgentGoalWorkflow."""
|
||||
|
||||
async def test_workflow_initialization(
|
||||
self, client: Client, sample_combined_input: CombinedInput
|
||||
):
|
||||
"""Test workflow can be initialized and started."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[mock_get_wf_env_vars],
|
||||
):
|
||||
# Start workflow but don't wait for completion since it runs indefinitely
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
sample_combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Verify workflow is running
|
||||
assert handle is not None
|
||||
|
||||
# Query the workflow to check initial state
|
||||
conversation_history = await handle.query(
|
||||
AgentGoalWorkflow.get_conversation_history
|
||||
)
|
||||
assert isinstance(conversation_history, dict)
|
||||
assert "messages" in conversation_history
|
||||
|
||||
# Test goal query
|
||||
agent_goal = await handle.query(AgentGoalWorkflow.get_agent_goal)
|
||||
assert agent_goal == sample_combined_input.agent_goal
|
||||
|
||||
# End the workflow
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
result = await handle.result()
|
||||
assert isinstance(result, str)
|
||||
|
||||
async def test_user_prompt_signal(
|
||||
self, client: Client, sample_combined_input: CombinedInput
|
||||
):
|
||||
"""Test user_prompt signal handling."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
@activity.defn(name="agent_validatePrompt")
|
||||
async def mock_agent_validatePrompt(
|
||||
validation_input: ValidationInput,
|
||||
) -> ValidationResult:
|
||||
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||
|
||||
@activity.defn(name="agent_toolPlanner")
|
||||
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
||||
return {"next": "done", "response": "Test response from LLM"}
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[
|
||||
mock_get_wf_env_vars,
|
||||
mock_agent_validatePrompt,
|
||||
mock_agent_toolPlanner,
|
||||
],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
sample_combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Send user prompt
|
||||
await handle.signal(
|
||||
AgentGoalWorkflow.user_prompt, "Hello, this is a test message"
|
||||
)
|
||||
|
||||
# Wait for workflow to complete (it should end due to "done" next step)
|
||||
result = await handle.result()
|
||||
assert isinstance(result, str)
|
||||
|
||||
# Verify the conversation includes our message
|
||||
import json
|
||||
|
||||
try:
|
||||
conversation_history = json.loads(result.replace("'", '"'))
|
||||
except Exception:
|
||||
# Fallback to eval if json fails
|
||||
conversation_history = eval(result)
|
||||
messages = conversation_history["messages"]
|
||||
|
||||
# Should have our user message and agent response
|
||||
user_messages = [msg for msg in messages if msg["actor"] == "user"]
|
||||
assert len(user_messages) > 0
|
||||
assert any(
|
||||
"Hello, this is a test message" in str(msg["response"])
|
||||
for msg in user_messages
|
||||
)
|
||||
|
||||
async def test_confirm_signal(
|
||||
self, client: Client, sample_combined_input: CombinedInput
|
||||
):
|
||||
"""Test confirm signal handling for tool execution."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
@activity.defn(name="agent_validatePrompt")
|
||||
async def mock_agent_validatePrompt(
|
||||
validation_input: ValidationInput,
|
||||
) -> ValidationResult:
|
||||
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||
|
||||
@activity.defn(name="agent_toolPlanner")
|
||||
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
||||
return {
|
||||
"next": "confirm",
|
||||
"tool": "TestTool",
|
||||
"args": {"test_arg": "test_value"},
|
||||
"response": "Ready to execute tool",
|
||||
}
|
||||
|
||||
@activity.defn(name="TestTool")
|
||||
async def mock_test_tool(args: dict) -> dict:
|
||||
return {"result": "Test tool executed successfully"}
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[
|
||||
mock_get_wf_env_vars,
|
||||
mock_agent_validatePrompt,
|
||||
mock_agent_toolPlanner,
|
||||
mock_test_tool,
|
||||
],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
sample_combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Send user prompt that will require confirmation
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "Execute the test tool")
|
||||
|
||||
# Query to check tool data is set
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(0.1) # Give workflow time to process
|
||||
|
||||
tool_data = await handle.query(AgentGoalWorkflow.get_latest_tool_data)
|
||||
if tool_data:
|
||||
assert tool_data.get("tool") == "TestTool"
|
||||
assert tool_data.get("next") == "confirm"
|
||||
|
||||
# Send confirmation and end chat
|
||||
await handle.signal(AgentGoalWorkflow.confirm)
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
|
||||
result = await handle.result()
|
||||
assert isinstance(result, str)
|
||||
|
||||
async def test_validation_failure(
|
||||
self, client: Client, sample_combined_input: CombinedInput
|
||||
):
|
||||
"""Test workflow handles validation failures correctly."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
@activity.defn(name="agent_validatePrompt")
|
||||
async def mock_agent_validatePrompt(
|
||||
validation_input: ValidationInput,
|
||||
) -> ValidationResult:
|
||||
return ValidationResult(
|
||||
validationResult=False,
|
||||
validationFailedReason={
|
||||
"next": "question",
|
||||
"response": "Your request doesn't make sense in this context",
|
||||
},
|
||||
)
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[mock_get_wf_env_vars, mock_agent_validatePrompt],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
sample_combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Send invalid prompt
|
||||
await handle.signal(
|
||||
AgentGoalWorkflow.user_prompt, "Invalid nonsensical prompt"
|
||||
)
|
||||
|
||||
# Give workflow time to process the prompt
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
# End workflow to check conversation
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
result = await handle.result()
|
||||
|
||||
# Verify validation failure message was added
|
||||
import json
|
||||
|
||||
try:
|
||||
conversation_history = json.loads(result.replace("'", '"'))
|
||||
except Exception:
|
||||
# Fallback to eval if json fails
|
||||
conversation_history = eval(result)
|
||||
messages = conversation_history["messages"]
|
||||
|
||||
# Should have validation failure response
|
||||
agent_messages = [msg for msg in messages if msg["actor"] == "agent"]
|
||||
assert len(agent_messages) > 0
|
||||
assert any(
|
||||
"doesn't make sense" in str(msg["response"]) for msg in agent_messages
|
||||
)
|
||||
|
||||
async def test_conversation_summary_initialization(
|
||||
self, client: Client, sample_agent_goal
|
||||
):
|
||||
"""Test workflow initializes with conversation summary."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
|
||||
# Create input with conversation summary
|
||||
from collections import deque
|
||||
|
||||
tool_params = AgentGoalWorkflowParams(
|
||||
conversation_summary="Previous conversation summary", prompt_queue=deque()
|
||||
)
|
||||
combined_input = CombinedInput(
|
||||
agent_goal=sample_agent_goal, tool_params=tool_params
|
||||
)
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[mock_get_wf_env_vars],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Give workflow time to initialize
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Query conversation summary
|
||||
summary = await handle.query(AgentGoalWorkflow.get_summary_from_history)
|
||||
assert summary == "Previous conversation summary"
|
||||
|
||||
# Query conversation history - should include summary message
|
||||
conversation_history = await handle.query(
|
||||
AgentGoalWorkflow.get_conversation_history
|
||||
)
|
||||
messages = conversation_history["messages"]
|
||||
|
||||
# Should have conversation_summary message
|
||||
summary_messages = [
|
||||
msg for msg in messages if msg["actor"] == "conversation_summary"
|
||||
]
|
||||
assert len(summary_messages) == 1
|
||||
assert summary_messages[0]["response"] == "Previous conversation summary"
|
||||
|
||||
# End workflow
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
await handle.result()
|
||||
|
||||
async def test_workflow_queries(
|
||||
self, client: Client, sample_combined_input: CombinedInput
|
||||
):
|
||||
"""Test all workflow query methods."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[mock_get_wf_env_vars],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
sample_combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Give workflow time to initialize
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Test get_conversation_history query
|
||||
conversation_history = await handle.query(
|
||||
AgentGoalWorkflow.get_conversation_history
|
||||
)
|
||||
assert isinstance(conversation_history, dict)
|
||||
assert "messages" in conversation_history
|
||||
|
||||
# Test get_agent_goal query
|
||||
agent_goal = await handle.query(AgentGoalWorkflow.get_agent_goal)
|
||||
assert agent_goal.id == sample_combined_input.agent_goal.id
|
||||
|
||||
# Test get_summary_from_history query
|
||||
summary = await handle.query(AgentGoalWorkflow.get_summary_from_history)
|
||||
# Summary might be None if not set, so check for that
|
||||
if sample_combined_input.tool_params.conversation_summary:
|
||||
assert summary == sample_combined_input.tool_params.conversation_summary
|
||||
else:
|
||||
assert summary is None
|
||||
|
||||
# Test get_latest_tool_data query (should be None initially)
|
||||
tool_data = await handle.query(AgentGoalWorkflow.get_latest_tool_data)
|
||||
assert tool_data is None
|
||||
|
||||
# End workflow
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
await handle.result()
|
||||
|
||||
async def test_enable_disable_debugging_confirm_signals(
|
||||
self, client: Client, sample_combined_input: CombinedInput
|
||||
):
|
||||
"""Test debugging confirm enable/disable signals."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[mock_get_wf_env_vars],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
sample_combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Test enable debugging confirm signal
|
||||
await handle.signal(AgentGoalWorkflow.enable_debugging_confirm)
|
||||
|
||||
# Test disable debugging confirm signal
|
||||
await handle.signal(AgentGoalWorkflow.disable_debugging_confirm)
|
||||
|
||||
# End workflow
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
result = await handle.result()
|
||||
assert isinstance(result, str)
|
||||
|
||||
async def test_workflow_with_empty_prompt_queue(
|
||||
self, client: Client, sample_agent_goal
|
||||
):
|
||||
"""Test workflow behavior with empty prompt queue."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
|
||||
# Create input with empty prompt queue
|
||||
from collections import deque
|
||||
|
||||
tool_params = AgentGoalWorkflowParams(
|
||||
conversation_summary=None, prompt_queue=deque()
|
||||
)
|
||||
combined_input = CombinedInput(
|
||||
agent_goal=sample_agent_goal, tool_params=tool_params
|
||||
)
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[mock_get_wf_env_vars],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Give workflow time to initialize
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Query initial state
|
||||
conversation_history = await handle.query(
|
||||
AgentGoalWorkflow.get_conversation_history
|
||||
)
|
||||
assert isinstance(conversation_history, dict)
|
||||
assert "messages" in conversation_history
|
||||
|
||||
# Should have no messages initially (empty prompt queue, no summary)
|
||||
messages = conversation_history["messages"]
|
||||
assert len(messages) == 0
|
||||
|
||||
# End workflow
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
result = await handle.result()
|
||||
assert isinstance(result, str)
|
||||
|
||||
async def test_multiple_user_prompts(
|
||||
self, client: Client, sample_combined_input: CombinedInput
|
||||
):
|
||||
"""Test workflow handling multiple user prompts in sequence."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
@activity.defn(name="agent_validatePrompt")
|
||||
async def mock_agent_validatePrompt(
|
||||
validation_input: ValidationInput,
|
||||
) -> ValidationResult:
|
||||
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||
|
||||
@activity.defn(name="agent_toolPlanner")
|
||||
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
||||
# Keep workflow running for multiple prompts
|
||||
return {"next": "question", "response": f"Processed: {input.prompt}"}
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[
|
||||
mock_get_wf_env_vars,
|
||||
mock_agent_validatePrompt,
|
||||
mock_agent_toolPlanner,
|
||||
],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
sample_combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Send multiple prompts
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "First message")
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "Second message")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "Third message")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# End workflow
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
result = await handle.result()
|
||||
assert isinstance(result, str)
|
||||
|
||||
# Parse result and verify multiple messages
|
||||
import json
|
||||
|
||||
try:
|
||||
conversation_history = json.loads(result.replace("'", '"'))
|
||||
except Exception:
|
||||
conversation_history = eval(result)
|
||||
messages = conversation_history["messages"]
|
||||
|
||||
# Should have at least one user message (timing dependent)
|
||||
user_messages = [msg for msg in messages if msg["actor"] == "user"]
|
||||
assert len(user_messages) >= 1
|
||||
|
||||
# Verify at least the first message was processed
|
||||
message_texts = [str(msg["response"]) for msg in user_messages]
|
||||
assert any("First message" in text for text in message_texts)
|
||||
@@ -1,418 +0,0 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from collections import deque
|
||||
from typing import Sequence
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from temporalio import activity
|
||||
from temporalio.client import Client
|
||||
from temporalio.common import RawValue
|
||||
from temporalio.testing import ActivityEnvironment
|
||||
from temporalio.worker import Worker
|
||||
|
||||
from activities.tool_activities import _convert_args_types, mcp_list_tools
|
||||
from models.data_types import (
|
||||
AgentGoalWorkflowParams,
|
||||
CombinedInput,
|
||||
EnvLookupInput,
|
||||
EnvLookupOutput,
|
||||
ToolPromptInput,
|
||||
ValidationInput,
|
||||
ValidationResult,
|
||||
)
|
||||
from models.tool_definitions import AgentGoal, MCPServerDefinition, ToolDefinition
|
||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||
from workflows.workflow_helpers import is_mcp_tool
|
||||
|
||||
|
||||
class DummySession:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
async def list_tools(self):
|
||||
class Tool:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.description = f"desc {name}"
|
||||
self.inputSchema = {}
|
||||
|
||||
return type(
|
||||
"Resp", (), {"tools": [Tool("list_products"), Tool("create_customer")]}
|
||||
)()
|
||||
|
||||
|
||||
def test_convert_args_types_basic():
|
||||
args = {
|
||||
"count": "5",
|
||||
"price": "12.5",
|
||||
"flag_true": "true",
|
||||
"flag_false": "false",
|
||||
"name": "pizza",
|
||||
"already_int": 2,
|
||||
}
|
||||
result = _convert_args_types(args)
|
||||
assert result["count"] == 5 and isinstance(result["count"], int)
|
||||
assert result["price"] == 12.5 and isinstance(result["price"], float)
|
||||
assert result["flag_true"] is True
|
||||
assert result["flag_false"] is False
|
||||
assert result["name"] == "pizza"
|
||||
assert result["already_int"] == 2
|
||||
|
||||
|
||||
def test_is_mcp_tool_identification():
|
||||
server_def = MCPServerDefinition(name="test", command="python", args=["server.py"])
|
||||
goal = AgentGoal(
|
||||
id="g",
|
||||
category_tag="food",
|
||||
agent_name="agent",
|
||||
agent_friendly_description="",
|
||||
description="",
|
||||
tools=[ToolDefinition(name="AddToCart", description="", arguments=[])],
|
||||
starter_prompt="",
|
||||
example_conversation_history="",
|
||||
mcp_server_definition=server_def,
|
||||
)
|
||||
|
||||
assert is_mcp_tool("list_products", goal) is True
|
||||
assert is_mcp_tool("AddToCart", goal) is False
|
||||
no_mcp_goal = AgentGoal(
|
||||
id="g2",
|
||||
category_tag="food",
|
||||
agent_name="agent",
|
||||
agent_friendly_description="",
|
||||
description="",
|
||||
tools=[],
|
||||
starter_prompt="",
|
||||
example_conversation_history="",
|
||||
mcp_server_definition=None,
|
||||
)
|
||||
assert is_mcp_tool("list_products", no_mcp_goal) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_list_tools_success():
|
||||
server_def = MCPServerDefinition(name="test", command="python", args=["server.py"])
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def dummy_connection(command, args, env):
|
||||
yield None, None
|
||||
|
||||
with patch(
|
||||
"activities.tool_activities._build_connection", return_value={"type": "stdio"}
|
||||
), patch("activities.tool_activities._stdio_connection", dummy_connection), patch(
|
||||
"activities.tool_activities.ClientSession", lambda r, w: DummySession()
|
||||
):
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(mcp_list_tools, server_def, ["list_products"])
|
||||
assert result["success"] is True
|
||||
assert result["filtered_count"] == 1
|
||||
assert "list_products" in result["tools"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_list_tools_failure():
|
||||
server_def = MCPServerDefinition(name="test", command="python", args=["server.py"])
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def failing_connection(*args, **kwargs):
|
||||
raise RuntimeError("conn fail")
|
||||
yield None, None
|
||||
|
||||
with patch(
|
||||
"activities.tool_activities._build_connection", return_value={"type": "stdio"}
|
||||
), patch("activities.tool_activities._stdio_connection", failing_connection):
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(mcp_list_tools, server_def)
|
||||
assert result["success"] is False
|
||||
assert "conn fail" in result["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflow_loads_mcp_tools_dynamically(client: Client):
|
||||
"""Workflow should load MCP tools and add them to the goal."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
server_def = MCPServerDefinition(name="test", command="python", args=["srv.py"])
|
||||
goal = AgentGoal(
|
||||
id="g_mcp",
|
||||
category_tag="food",
|
||||
agent_name="agent",
|
||||
agent_friendly_description="",
|
||||
description="",
|
||||
tools=[],
|
||||
starter_prompt="",
|
||||
example_conversation_history="",
|
||||
mcp_server_definition=server_def,
|
||||
)
|
||||
combined_input = CombinedInput(
|
||||
agent_goal=goal,
|
||||
tool_params=AgentGoalWorkflowParams(
|
||||
conversation_summary=None, prompt_queue=deque()
|
||||
),
|
||||
)
|
||||
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
@activity.defn(name="mcp_list_tools")
|
||||
async def mock_mcp_list_tools(
|
||||
server_definition: MCPServerDefinition, include_tools=None
|
||||
):
|
||||
return {
|
||||
"server_name": server_definition.name,
|
||||
"success": True,
|
||||
"tools": {
|
||||
"list_products": {
|
||||
"name": "list_products",
|
||||
"description": "",
|
||||
"inputSchema": {},
|
||||
},
|
||||
},
|
||||
"total_available": 1,
|
||||
"filtered_count": 1,
|
||||
}
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[mock_get_wf_env_vars, mock_mcp_list_tools],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
# Wait until the MCP tools have been added
|
||||
for _ in range(10):
|
||||
updated_goal = await handle.query(AgentGoalWorkflow.get_agent_goal)
|
||||
if any(t.name == "list_products" for t in updated_goal.tools):
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
else:
|
||||
updated_goal = await handle.query(AgentGoalWorkflow.get_agent_goal)
|
||||
|
||||
assert any(t.name == "list_products" for t in updated_goal.tools)
|
||||
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
await handle.result()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_tool_execution_flow(client: Client):
|
||||
"""MCP tool execution should pass server_definition to activity."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
server_def = MCPServerDefinition(name="test", command="python", args=["srv.py"])
|
||||
goal = AgentGoal(
|
||||
id="g_mcp_exec",
|
||||
category_tag="food",
|
||||
agent_name="agent",
|
||||
agent_friendly_description="",
|
||||
description="",
|
||||
tools=[],
|
||||
starter_prompt="",
|
||||
example_conversation_history="",
|
||||
mcp_server_definition=server_def,
|
||||
)
|
||||
combined_input = CombinedInput(
|
||||
agent_goal=goal,
|
||||
tool_params=AgentGoalWorkflowParams(
|
||||
conversation_summary=None, prompt_queue=deque()
|
||||
),
|
||||
)
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
@activity.defn(name="agent_validatePrompt")
|
||||
async def mock_validate(prompt: ValidationInput) -> ValidationResult:
|
||||
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||
|
||||
@activity.defn(name="agent_toolPlanner")
|
||||
async def mock_planner(input: ToolPromptInput) -> dict:
|
||||
if "planner_called" not in captured:
|
||||
captured["planner_called"] = True
|
||||
return {
|
||||
"next": "confirm",
|
||||
"tool": "list_products",
|
||||
"args": {"limit": "5"},
|
||||
"response": "Listing products",
|
||||
}
|
||||
return {"next": "done", "response": "done"}
|
||||
|
||||
@activity.defn(name="mcp_list_tools")
|
||||
async def mock_mcp_list_tools(
|
||||
server_definition: MCPServerDefinition, include_tools=None
|
||||
):
|
||||
return {
|
||||
"server_name": server_definition.name,
|
||||
"success": True,
|
||||
"tools": {
|
||||
"list_products": {
|
||||
"name": "list_products",
|
||||
"description": "",
|
||||
"inputSchema": {},
|
||||
},
|
||||
},
|
||||
"total_available": 1,
|
||||
"filtered_count": 1,
|
||||
}
|
||||
|
||||
@activity.defn(name="dynamic_tool_activity", dynamic=True)
|
||||
async def mock_dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||
payload = activity.payload_converter().from_payload(args[0].payload, dict)
|
||||
captured["dynamic_args"] = payload
|
||||
return {"tool": "list_products", "success": True, "content": {"ok": True}}
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[
|
||||
mock_get_wf_env_vars,
|
||||
mock_validate,
|
||||
mock_planner,
|
||||
mock_mcp_list_tools,
|
||||
mock_dynamic_tool_activity,
|
||||
],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "show menu")
|
||||
await asyncio.sleep(0.5)
|
||||
await handle.signal(AgentGoalWorkflow.confirm)
|
||||
# Give workflow time to execute the MCP tool and finish
|
||||
await asyncio.sleep(0.5)
|
||||
result = await handle.result()
|
||||
print(result)
|
||||
|
||||
assert "dynamic_args" in captured
|
||||
assert "server_definition" in captured["dynamic_args"]
|
||||
assert captured["dynamic_args"]["server_definition"]["name"] == server_def.name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_tool_failure_recorded(client: Client):
|
||||
"""Failure of an MCP tool should be recorded in conversation history."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
server_def = MCPServerDefinition(name="test", command="python", args=["srv.py"])
|
||||
goal = AgentGoal(
|
||||
id="g_mcp_fail",
|
||||
category_tag="food",
|
||||
agent_name="agent",
|
||||
agent_friendly_description="",
|
||||
description="",
|
||||
tools=[],
|
||||
starter_prompt="",
|
||||
example_conversation_history="",
|
||||
mcp_server_definition=server_def,
|
||||
)
|
||||
combined_input = CombinedInput(
|
||||
agent_goal=goal,
|
||||
tool_params=AgentGoalWorkflowParams(
|
||||
conversation_summary=None, prompt_queue=deque()
|
||||
),
|
||||
)
|
||||
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
@activity.defn(name="agent_validatePrompt")
|
||||
async def mock_validate(prompt: ValidationInput) -> ValidationResult:
|
||||
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||
|
||||
@activity.defn(name="agent_toolPlanner")
|
||||
async def mock_planner(input: ToolPromptInput) -> dict:
|
||||
return {
|
||||
"next": "confirm",
|
||||
"tool": "list_products",
|
||||
"args": {},
|
||||
"response": "Listing products",
|
||||
}
|
||||
|
||||
@activity.defn(name="mcp_list_tools")
|
||||
async def mock_mcp_list_tools(
|
||||
server_definition: MCPServerDefinition, include_tools=None
|
||||
):
|
||||
return {
|
||||
"server_name": server_definition.name,
|
||||
"success": True,
|
||||
"tools": {
|
||||
"list_products": {
|
||||
"name": "list_products",
|
||||
"description": "",
|
||||
"inputSchema": {},
|
||||
},
|
||||
},
|
||||
"total_available": 1,
|
||||
"filtered_count": 1,
|
||||
}
|
||||
|
||||
@activity.defn(name="dynamic_tool_activity", dynamic=True)
|
||||
async def failing_dynamic_tool(args: Sequence[RawValue]) -> dict:
|
||||
return {
|
||||
"tool": "list_products",
|
||||
"success": False,
|
||||
"error": "Connection timed out",
|
||||
}
|
||||
|
||||
async with Worker(
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[
|
||||
mock_get_wf_env_vars,
|
||||
mock_validate,
|
||||
mock_planner,
|
||||
mock_mcp_list_tools,
|
||||
failing_dynamic_tool,
|
||||
],
|
||||
):
|
||||
handle = await client.start_workflow(
|
||||
AgentGoalWorkflow.run,
|
||||
combined_input,
|
||||
id=str(uuid.uuid4()),
|
||||
task_queue=task_queue_name,
|
||||
)
|
||||
|
||||
await handle.signal(AgentGoalWorkflow.user_prompt, "show menu")
|
||||
await asyncio.sleep(0.5)
|
||||
await handle.signal(AgentGoalWorkflow.confirm)
|
||||
# Give workflow time to record the failure result
|
||||
await asyncio.sleep(0.5)
|
||||
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||
result = await handle.result()
|
||||
|
||||
import json
|
||||
|
||||
try:
|
||||
history = json.loads(result.replace("'", '"'))
|
||||
except Exception:
|
||||
history = eval(result)
|
||||
|
||||
assert any(
|
||||
msg["actor"] == "tool_result" and not msg["response"].get("success", True)
|
||||
for msg in history["messages"]
|
||||
)
|
||||
@@ -1,578 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from temporalio.client import Client
|
||||
from temporalio.testing import ActivityEnvironment
|
||||
|
||||
from activities.tool_activities import (
|
||||
MCPServerDefinition,
|
||||
ToolActivities,
|
||||
dynamic_tool_activity,
|
||||
)
|
||||
from models.data_types import (
|
||||
EnvLookupInput,
|
||||
EnvLookupOutput,
|
||||
ToolPromptInput,
|
||||
ValidationInput,
|
||||
ValidationResult,
|
||||
)
|
||||
|
||||
|
||||
class TestToolActivities:
|
||||
"""Test cases for ToolActivities."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment for each test."""
|
||||
self.tool_activities = ToolActivities()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_validatePrompt_valid_prompt(
|
||||
self, sample_agent_goal, sample_conversation_history
|
||||
):
|
||||
"""Test agent_validatePrompt with a valid prompt."""
|
||||
validation_input = ValidationInput(
|
||||
prompt="I need help with the test tool",
|
||||
conversation_history=sample_conversation_history,
|
||||
agent_goal=sample_agent_goal,
|
||||
)
|
||||
|
||||
# Mock the agent_toolPlanner to return a valid response
|
||||
mock_response = {"validationResult": True, "validationFailedReason": {}}
|
||||
|
||||
with patch.object(
|
||||
self.tool_activities, "agent_toolPlanner", new_callable=AsyncMock
|
||||
) as mock_planner:
|
||||
mock_planner.return_value = mock_response
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.agent_validatePrompt, validation_input
|
||||
)
|
||||
|
||||
assert isinstance(result, ValidationResult)
|
||||
assert result.validationResult is True
|
||||
assert result.validationFailedReason == {}
|
||||
|
||||
# Verify the mock was called with correct parameters
|
||||
mock_planner.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_validatePrompt_invalid_prompt(
|
||||
self, sample_agent_goal, sample_conversation_history
|
||||
):
|
||||
"""Test agent_validatePrompt with an invalid prompt."""
|
||||
validation_input = ValidationInput(
|
||||
prompt="asdfghjkl nonsense",
|
||||
conversation_history=sample_conversation_history,
|
||||
agent_goal=sample_agent_goal,
|
||||
)
|
||||
|
||||
# Mock the agent_toolPlanner to return an invalid response
|
||||
mock_response = {
|
||||
"validationResult": False,
|
||||
"validationFailedReason": {
|
||||
"next": "question",
|
||||
"response": "Your request doesn't make sense in this context",
|
||||
},
|
||||
}
|
||||
|
||||
with patch.object(
|
||||
self.tool_activities, "agent_toolPlanner", new_callable=AsyncMock
|
||||
) as mock_planner:
|
||||
mock_planner.return_value = mock_response
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.agent_validatePrompt, validation_input
|
||||
)
|
||||
|
||||
assert isinstance(result, ValidationResult)
|
||||
assert result.validationResult is False
|
||||
assert "doesn't make sense" in str(result.validationFailedReason)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_toolPlanner_success(self):
|
||||
"""Test agent_toolPlanner with successful LLM response."""
|
||||
prompt_input = ToolPromptInput(
|
||||
prompt="Test prompt", context_instructions="Test context instructions"
|
||||
)
|
||||
|
||||
# Mock the completion function
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[
|
||||
0
|
||||
].message.content = (
|
||||
'{"next": "confirm", "tool": "TestTool", "response": "Test response"}'
|
||||
)
|
||||
|
||||
with patch("activities.tool_activities.completion") as mock_completion:
|
||||
mock_completion.return_value = mock_response
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.agent_toolPlanner, prompt_input
|
||||
)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["next"] == "confirm"
|
||||
assert result["tool"] == "TestTool"
|
||||
assert result["response"] == "Test response"
|
||||
|
||||
# Verify completion was called with correct parameters
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args[1]
|
||||
assert call_args["model"] == self.tool_activities.llm_model
|
||||
assert len(call_args["messages"]) == 2
|
||||
assert call_args["messages"][0]["role"] == "system"
|
||||
assert call_args["messages"][1]["role"] == "user"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_toolPlanner_with_custom_base_url(self):
|
||||
"""Test agent_toolPlanner with custom base URL configuration."""
|
||||
# Set up tool activities with custom base URL
|
||||
with patch.dict(os.environ, {"LLM_BASE_URL": "https://custom.endpoint.com"}):
|
||||
tool_activities = ToolActivities()
|
||||
|
||||
prompt_input = ToolPromptInput(
|
||||
prompt="Test prompt", context_instructions="Test context instructions"
|
||||
)
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[
|
||||
0
|
||||
].message.content = '{"next": "done", "response": "Test"}'
|
||||
|
||||
with patch("activities.tool_activities.completion") as mock_completion:
|
||||
mock_completion.return_value = mock_response
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
await activity_env.run(tool_activities.agent_toolPlanner, prompt_input)
|
||||
|
||||
# Verify base_url was included in the call
|
||||
call_args = mock_completion.call_args[1]
|
||||
assert "base_url" in call_args
|
||||
assert call_args["base_url"] == "https://custom.endpoint.com"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_toolPlanner_json_parsing_error(self):
|
||||
"""Test agent_toolPlanner handles JSON parsing errors."""
|
||||
prompt_input = ToolPromptInput(
|
||||
prompt="Test prompt", context_instructions="Test context instructions"
|
||||
)
|
||||
|
||||
# Mock the completion function to return invalid JSON
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "Invalid JSON response"
|
||||
|
||||
with patch("activities.tool_activities.completion") as mock_completion:
|
||||
mock_completion.return_value = mock_response
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
with pytest.raises(Exception): # Should raise JSON parsing error
|
||||
await activity_env.run(
|
||||
self.tool_activities.agent_toolPlanner, prompt_input
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_wf_env_vars_default_values(self):
|
||||
"""Test get_wf_env_vars with default values."""
|
||||
env_input = EnvLookupInput(
|
||||
show_confirm_env_var_name="SHOW_CONFIRM", show_confirm_default=True
|
||||
)
|
||||
|
||||
# Clear environment variables
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.get_wf_env_vars, env_input
|
||||
)
|
||||
|
||||
assert isinstance(result, EnvLookupOutput)
|
||||
assert result.show_confirm is True # default value
|
||||
assert result.multi_goal_mode is False # default value (single agent mode)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_wf_env_vars_custom_values(self):
|
||||
"""Test get_wf_env_vars with custom environment values."""
|
||||
env_input = EnvLookupInput(
|
||||
show_confirm_env_var_name="SHOW_CONFIRM", show_confirm_default=True
|
||||
)
|
||||
|
||||
# Set environment variables
|
||||
with patch.dict(
|
||||
os.environ, {"SHOW_CONFIRM": "false", "AGENT_GOAL": "specific_goal"}
|
||||
):
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.get_wf_env_vars, env_input
|
||||
)
|
||||
|
||||
assert isinstance(result, EnvLookupOutput)
|
||||
assert result.show_confirm is False # from env var
|
||||
assert result.multi_goal_mode is False # from env var
|
||||
|
||||
def test_sanitize_json_response(self):
|
||||
"""Test JSON response sanitization."""
|
||||
# Test with markdown code blocks
|
||||
response_with_markdown = '```json\n{"test": "value"}\n```'
|
||||
sanitized = self.tool_activities.sanitize_json_response(response_with_markdown)
|
||||
assert sanitized == '{"test": "value"}'
|
||||
|
||||
# Test with extra whitespace
|
||||
response_with_whitespace = ' \n{"test": "value"} \n'
|
||||
sanitized = self.tool_activities.sanitize_json_response(
|
||||
response_with_whitespace
|
||||
)
|
||||
assert sanitized == '{"test": "value"}'
|
||||
|
||||
def test_parse_json_response_success(self):
|
||||
"""Test successful JSON parsing."""
|
||||
json_string = '{"next": "confirm", "tool": "TestTool"}'
|
||||
result = self.tool_activities.parse_json_response(json_string)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["next"] == "confirm"
|
||||
assert result["tool"] == "TestTool"
|
||||
|
||||
def test_parse_json_response_failure(self):
|
||||
"""Test JSON parsing with invalid JSON."""
|
||||
invalid_json = "Not valid JSON"
|
||||
|
||||
with pytest.raises(Exception): # Should raise JSON parsing error
|
||||
self.tool_activities.parse_json_response(invalid_json)
|
||||
|
||||
|
||||
class TestDynamicToolActivity:
|
||||
"""Test cases for dynamic_tool_activity."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dynamic_tool_activity_sync_handler(self):
|
||||
"""Test dynamic tool activity with synchronous handler."""
|
||||
# Mock the activity info and payload converter
|
||||
mock_info = MagicMock()
|
||||
mock_info.activity_type = "TestTool"
|
||||
|
||||
mock_payload_converter = MagicMock()
|
||||
mock_payload = MagicMock()
|
||||
mock_payload.payload = b'{"test_arg": "test_value"}'
|
||||
mock_payload_converter.from_payload.return_value = {"test_arg": "test_value"}
|
||||
|
||||
# Mock the handler function
|
||||
def mock_handler(args):
|
||||
return {"result": f"Handled {args['test_arg']}"}
|
||||
|
||||
with patch("temporalio.activity.info", return_value=mock_info), patch(
|
||||
"temporalio.activity.payload_converter", return_value=mock_payload_converter
|
||||
), patch("tools.get_handler", return_value=mock_handler):
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(dynamic_tool_activity, [mock_payload])
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["result"] == "Handled test_value"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dynamic_tool_activity_async_handler(self):
|
||||
"""Test dynamic tool activity with asynchronous handler."""
|
||||
# Mock the activity info and payload converter
|
||||
mock_info = MagicMock()
|
||||
mock_info.activity_type = "AsyncTestTool"
|
||||
|
||||
mock_payload_converter = MagicMock()
|
||||
mock_payload = MagicMock()
|
||||
mock_payload.payload = b'{"test_arg": "async_test"}'
|
||||
mock_payload_converter.from_payload.return_value = {"test_arg": "async_test"}
|
||||
|
||||
# Mock the async handler function
|
||||
async def mock_async_handler(args):
|
||||
return {"async_result": f"Async handled {args['test_arg']}"}
|
||||
|
||||
with patch("temporalio.activity.info", return_value=mock_info), patch(
|
||||
"temporalio.activity.payload_converter", return_value=mock_payload_converter
|
||||
), patch("tools.get_handler", return_value=mock_async_handler):
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(dynamic_tool_activity, [mock_payload])
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["async_result"] == "Async handled async_test"
|
||||
|
||||
|
||||
class TestToolActivitiesIntegration:
|
||||
"""Integration tests for ToolActivities in a real Temporal environment."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_activities_in_worker(self, client: Client):
|
||||
"""Test activities can be registered and executed in a worker."""
|
||||
# task_queue_name = str(uuid.uuid4())
|
||||
tool_activities = ToolActivities()
|
||||
|
||||
# Test get_wf_env_vars activity using ActivityEnvironment
|
||||
env_input = EnvLookupInput(
|
||||
show_confirm_env_var_name="TEST_CONFIRM", show_confirm_default=False
|
||||
)
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(tool_activities.get_wf_env_vars, env_input)
|
||||
|
||||
assert isinstance(result, EnvLookupOutput)
|
||||
assert isinstance(result.show_confirm, bool)
|
||||
assert isinstance(result.multi_goal_mode, bool)
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Test edge cases and error handling."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment for each test."""
|
||||
self.tool_activities = ToolActivities()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_validatePrompt_with_empty_conversation_history(
|
||||
self, sample_agent_goal
|
||||
):
|
||||
"""Test validation with empty conversation history."""
|
||||
validation_input = ValidationInput(
|
||||
prompt="Test prompt",
|
||||
conversation_history={"messages": []},
|
||||
agent_goal=sample_agent_goal,
|
||||
)
|
||||
|
||||
mock_response = {"validationResult": True, "validationFailedReason": {}}
|
||||
|
||||
with patch.object(
|
||||
self.tool_activities, "agent_toolPlanner", new_callable=AsyncMock
|
||||
) as mock_planner:
|
||||
mock_planner.return_value = mock_response
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.agent_validatePrompt, validation_input
|
||||
)
|
||||
|
||||
assert isinstance(result, ValidationResult)
|
||||
assert result.validationResult
|
||||
assert result.validationFailedReason == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_toolPlanner_with_long_prompt(self):
|
||||
"""Test toolPlanner with very long prompt."""
|
||||
long_prompt = "This is a very long prompt " * 100
|
||||
tool_prompt_input = ToolPromptInput(
|
||||
prompt=long_prompt, context_instructions="Test context instructions"
|
||||
)
|
||||
|
||||
# Mock the completion response
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[
|
||||
0
|
||||
].message.content = '{"next": "done", "response": "Processed long prompt"}'
|
||||
|
||||
with patch("activities.tool_activities.completion", return_value=mock_response):
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.agent_toolPlanner, tool_prompt_input
|
||||
)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["next"] == "done"
|
||||
assert "Processed long prompt" in result["response"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sanitize_json_with_various_formats(self):
|
||||
"""Test JSON sanitization with various input formats."""
|
||||
# Test markdown code blocks
|
||||
markdown_json = '```json\n{"test": "value"}\n```'
|
||||
result = self.tool_activities.sanitize_json_response(markdown_json)
|
||||
assert result == '{"test": "value"}'
|
||||
|
||||
# Test with extra whitespace
|
||||
whitespace_json = ' \n {"test": "value"} \n '
|
||||
result = self.tool_activities.sanitize_json_response(whitespace_json)
|
||||
assert result == '{"test": "value"}'
|
||||
|
||||
# Test already clean JSON
|
||||
clean_json = '{"test": "value"}'
|
||||
result = self.tool_activities.sanitize_json_response(clean_json)
|
||||
assert result == '{"test": "value"}'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_json_response_with_invalid_json(self):
|
||||
"""Test JSON parsing with invalid JSON."""
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
self.tool_activities.parse_json_response("Invalid JSON {test: value")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_wf_env_vars_with_various_env_values(self):
|
||||
"""Test environment variable parsing with different values."""
|
||||
# Test with "true" string
|
||||
with patch.dict(os.environ, {"TEST_CONFIRM": "true"}):
|
||||
env_input = EnvLookupInput(
|
||||
show_confirm_env_var_name="TEST_CONFIRM", show_confirm_default=False
|
||||
)
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.get_wf_env_vars, env_input
|
||||
)
|
||||
|
||||
assert result.show_confirm
|
||||
|
||||
# Test with "false" string
|
||||
with patch.dict(os.environ, {"TEST_CONFIRM": "false"}):
|
||||
env_input = EnvLookupInput(
|
||||
show_confirm_env_var_name="TEST_CONFIRM", show_confirm_default=True
|
||||
)
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.get_wf_env_vars, env_input
|
||||
)
|
||||
|
||||
assert not result.show_confirm
|
||||
|
||||
# Test with missing env var (should use default)
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
env_input = EnvLookupInput(
|
||||
show_confirm_env_var_name="MISSING_VAR", show_confirm_default=True
|
||||
)
|
||||
|
||||
activity_env = ActivityEnvironment()
|
||||
result = await activity_env.run(
|
||||
self.tool_activities.get_wf_env_vars, env_input
|
||||
)
|
||||
|
||||
assert result.show_confirm
|
||||
|
||||
|
||||
class TestMCPIntegration:
|
||||
@pytest.mark.asyncio
|
||||
async def test_convert_args_types(self):
|
||||
from activities.tool_activities import _convert_args_types
|
||||
|
||||
args = {
|
||||
"int_val": "123",
|
||||
"float_val": "123.45",
|
||||
"bool_true": "true",
|
||||
"bool_false": "False",
|
||||
"string": "text",
|
||||
"other": 5,
|
||||
}
|
||||
converted = _convert_args_types(args)
|
||||
assert converted["int_val"] == 123
|
||||
assert converted["float_val"] == 123.45
|
||||
assert converted["bool_true"] is True
|
||||
assert converted["bool_false"] is False
|
||||
assert converted["string"] == "text"
|
||||
assert converted["other"] == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dynamic_tool_activity_mcp_call(self):
|
||||
mcp_def = MCPServerDefinition(
|
||||
name="stripe", command="python", args=["server.py"]
|
||||
)
|
||||
payload = MagicMock()
|
||||
payload.payload = b'{"server_definition": null, "amount": "10", "flag": "true"}'
|
||||
mock_info = MagicMock()
|
||||
mock_info.activity_type = "list_products"
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def dummy_conn(*args, **kwargs):
|
||||
yield (None, None)
|
||||
|
||||
class DummySession:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
async def call_tool(self, tool_name, arguments=None):
|
||||
self.called_tool = tool_name
|
||||
self.called_args = arguments
|
||||
return MagicMock(content="ok")
|
||||
|
||||
mock_payload_converter = MagicMock()
|
||||
mock_payload_converter.from_payload.return_value = {
|
||||
"server_definition": mcp_def,
|
||||
"amount": "10",
|
||||
"flag": "true",
|
||||
}
|
||||
|
||||
with patch("activities.tool_activities._stdio_connection", dummy_conn), patch(
|
||||
"activities.tool_activities.ClientSession", return_value=DummySession()
|
||||
), patch(
|
||||
"activities.tool_activities._build_connection",
|
||||
return_value={
|
||||
"type": "stdio",
|
||||
"command": "python",
|
||||
"args": ["server.py"],
|
||||
"env": {},
|
||||
},
|
||||
), patch(
|
||||
"temporalio.activity.info", return_value=mock_info
|
||||
), patch(
|
||||
"temporalio.activity.payload_converter", return_value=mock_payload_converter
|
||||
):
|
||||
result = await ActivityEnvironment().run(dynamic_tool_activity, [payload])
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["tool"] == "list_products"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_tool_activity_failure(self):
|
||||
tool_activities = ToolActivities()
|
||||
mcp_def = MCPServerDefinition(
|
||||
name="stripe", command="python", args=["server.py"]
|
||||
)
|
||||
|
||||
async def dummy_conn(*args, **kwargs):
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def cm():
|
||||
yield (None, None)
|
||||
|
||||
return cm()
|
||||
|
||||
class DummySession:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
async def call_tool(self, tool_name, arguments=None):
|
||||
raise TypeError("boom")
|
||||
|
||||
with patch("activities.tool_activities._stdio_connection", dummy_conn), patch(
|
||||
"activities.tool_activities.ClientSession", return_value=DummySession()
|
||||
), patch(
|
||||
"activities.tool_activities._build_connection",
|
||||
return_value={
|
||||
"type": "stdio",
|
||||
"command": "python",
|
||||
"args": ["server.py"],
|
||||
"env": {},
|
||||
},
|
||||
):
|
||||
result = await ActivityEnvironment().run(
|
||||
tool_activities.mcp_tool_activity,
|
||||
"list_products",
|
||||
{"server_definition": mcp_def, "amount": "10"},
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_type"] == "TypeError"
|
||||
@@ -1,36 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from models.tool_definitions import (
|
||||
AgentGoal,
|
||||
MCPServerDefinition,
|
||||
ToolArgument,
|
||||
ToolDefinition,
|
||||
)
|
||||
from workflows.workflow_helpers import is_mcp_tool
|
||||
|
||||
|
||||
def make_goal(with_mcp: bool) -> AgentGoal:
|
||||
tools = [ToolDefinition(name="AddToCart", description="", arguments=[])]
|
||||
mcp_def = None
|
||||
if with_mcp:
|
||||
mcp_def = MCPServerDefinition(
|
||||
name="stripe", command="python", args=["server.py"]
|
||||
)
|
||||
return AgentGoal(
|
||||
id="g",
|
||||
category_tag="test",
|
||||
agent_name="Test",
|
||||
agent_friendly_description="",
|
||||
tools=tools,
|
||||
mcp_server_definition=mcp_def,
|
||||
)
|
||||
|
||||
|
||||
def test_is_mcp_tool_recognizes_native():
|
||||
goal = make_goal(True)
|
||||
assert not is_mcp_tool("AddToCart", goal)
|
||||
|
||||
|
||||
def test_is_mcp_tool_recognizes_mcp():
|
||||
goal = make_goal(True)
|
||||
assert is_mcp_tool("list_products", goal)
|
||||
@@ -1,25 +1,15 @@
|
||||
import concurrent.futures
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Dict, List, Optional, Sequence
|
||||
|
||||
from temporalio import activity
|
||||
from temporalio.client import Client, WorkflowExecutionStatus
|
||||
from temporalio.common import RawValue
|
||||
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,
|
||||
EnvLookupInput,
|
||||
EnvLookupOutput,
|
||||
ToolPromptInput,
|
||||
ValidationInput,
|
||||
ValidationResult,
|
||||
)
|
||||
from models.tool_definitions import MCPServerDefinition
|
||||
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
|
||||
@@ -29,69 +19,31 @@ def my_context():
|
||||
print("Cleanup")
|
||||
|
||||
|
||||
async def test_flight_booking(client: Client):
|
||||
# load_dotenv("test_flights_single.env")
|
||||
|
||||
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 = str(uuid.uuid4())
|
||||
workflow_id = str(uuid.uuid4())
|
||||
#env = await WorkflowEnvironment.start_local()
|
||||
#client = env.client
|
||||
task_queue_name = "agent-ai-workflow"
|
||||
workflow_id = "agent-workflow"
|
||||
|
||||
# Create mock activity functions with proper signatures
|
||||
@activity.defn(name="get_wf_env_vars")
|
||||
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||
|
||||
@activity.defn(name="agent_validatePrompt")
|
||||
async def mock_agent_validatePrompt(
|
||||
validation_input: ValidationInput,
|
||||
) -> ValidationResult:
|
||||
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||
|
||||
@activity.defn(name="agent_toolPlanner")
|
||||
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
||||
return {"next": "done", "response": "Test response from LLM"}
|
||||
|
||||
@activity.defn(name="mcp_list_tools")
|
||||
async def mock_mcp_list_tools(
|
||||
server_definition: MCPServerDefinition,
|
||||
include_tools: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
return {"success": True, "tools": {}, "server_name": "test"}
|
||||
|
||||
@activity.defn(name="mcp_tool_activity")
|
||||
async def mock_mcp_tool_activity(
|
||||
tool_name: str, tool_args: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
return {"success": True, "result": "Mock MCP tool result"}
|
||||
|
||||
@activity.defn(name="dynamic_tool_activity", dynamic=True)
|
||||
async def mock_dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||
return {"success": True, "result": "Mock dynamic tool result"}
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=100
|
||||
) as activity_executor:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
||||
worker = Worker(
|
||||
client,
|
||||
client,
|
||||
task_queue=task_queue_name,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[
|
||||
mock_get_wf_env_vars,
|
||||
mock_agent_validatePrompt,
|
||||
mock_agent_toolPlanner,
|
||||
mock_mcp_list_tools,
|
||||
mock_mcp_tool_activity,
|
||||
mock_dynamic_tool_activity,
|
||||
],
|
||||
activities=[ToolActivities.agent_validatePrompt, ToolActivities.agent_toolPlanner, ToolActivities.get_wf_env_vars, dynamic_tool_activity],
|
||||
activity_executor=activity_executor,
|
||||
)
|
||||
|
||||
async with worker:
|
||||
async with worker:
|
||||
initial_agent_goal = get_initial_agent_goal()
|
||||
# Create combined input
|
||||
combined_input = CombinedInput(
|
||||
@@ -99,36 +51,30 @@ async def test_flight_booking(client: Client):
|
||||
agent_goal=initial_agent_goal,
|
||||
)
|
||||
|
||||
prompt = "Hello!"
|
||||
prompt="Hello!"
|
||||
|
||||
# async with Worker(client, task_queue=task_queue_name, workflows=[AgentGoalWorkflow], activities=[ToolActivities.agent_validatePrompt, ToolActivities.agent_toolPlanner, dynamic_tool_activity]):
|
||||
#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,
|
||||
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
|
||||
)
|
||||
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
|
||||
|
||||
#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()
|
||||
print(f"Workflow result: {result}")
|
||||
# todo dump workflow history for analysis optional
|
||||
# todo assert result is good
|
||||
#todo dump workflow history for analysis optional
|
||||
#todo assert result is good
|
||||
8
thirdparty/train_api.py
vendored
@@ -1,9 +1,9 @@
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
|
||||
def parse_datetime(datetime_str):
|
||||
@@ -213,4 +213,4 @@ def run_server():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_server()
|
||||
run_server()
|
||||
@@ -1,31 +1,8 @@
|
||||
# todo list
|
||||
[x] take steve's confirm box changes https://temporaltechnologies.slack.com/archives/D062SV8KEEM/p1745251279164319 <br />
|
||||
[ ] consider adding goal categories to goal picker
|
||||
|
||||
## General Agent Enhancements
|
||||
|
||||
[ ] Google's A2A is emerging as the standard way to hand off agents to other agents. We should examine implementing this soon.
|
||||
|
||||
[ ] Custom metrics/tracing is important for AI specific aspects such as number of LLM calls, number of bad LLM responses that require retrying, number of bad chat outcomes. We should add this.
|
||||
|
||||
[ ] Evals are very important in agents. We want to be able to 'judge' the agent's performance both in dev and production (AIOps). This will help us improve our agent's performance over time in a targeted fashion.
|
||||
|
||||
[ ] Dynamically switch LLMs on persistent failures: <br />
|
||||
- detect failure in the activity using failurecount <br />
|
||||
- activity switches to secondary LLM defined in .env
|
||||
- activity reports switch to workflow
|
||||
|
||||
[ ] Collapse history/summarize chat after goal finished <br />
|
||||
|
||||
[ ] Write tests<br />
|
||||
|
||||
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br />
|
||||
|
||||
[ ] add visual feedback when workflow starting <br />
|
||||
|
||||
[ ] enable user to list agents at any time - like end conversation - probably with a next step<br />
|
||||
|
||||
## Ideas for more goals and tools
|
||||
|
||||
[ ] Add fintech goals <br />
|
||||
[ ] adding 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 />
|
||||
@@ -35,3 +12,21 @@
|
||||
[ ] tool is maybe a new tool asking the LLM to advise
|
||||
|
||||
[ ] for demo simulate failure - add utilities/simulated failures from pipeline demo <br />
|
||||
|
||||
[ ] LLM failure->autoswitch: <br />
|
||||
- detect failure in the activity using failurecount <br />
|
||||
- activity switches to secondary LLM defined in .env
|
||||
- activity reports switch to workflow
|
||||
|
||||
[ ] for demo simulate failure - add utilities/simulated failures from pipeline demo <br />
|
||||
|
||||
[ ] expand [tests](./tests/agent_goal_workflow_test.py)<br />
|
||||
[ ] collapse history/summarize after goal finished <br />
|
||||
[ ] add aws bedrock <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 />
|
||||
[ ] 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).
|
||||
@@ -1,25 +1,29 @@
|
||||
from .change_goal import change_goal
|
||||
from .search_fixtures import search_fixtures
|
||||
from .search_flights import search_flights
|
||||
from .search_trains import search_trains
|
||||
from .search_trains import book_trains
|
||||
from .create_invoice import create_invoice
|
||||
from .ecommerce.get_order import get_order
|
||||
from .ecommerce.list_orders import list_orders
|
||||
from .ecommerce.track_package import track_package
|
||||
from .find_events import find_events
|
||||
from .list_agents import list_agents
|
||||
from .change_goal import change_goal
|
||||
from .transfer_control import transfer_control
|
||||
|
||||
from .hr.current_pto import current_pto
|
||||
from .hr.book_pto import book_pto
|
||||
from .hr.future_pto_calc import future_pto_calc
|
||||
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 .find_events import find_events
|
||||
from .food.add_to_cart import add_to_cart
|
||||
|
||||
from .ecommerce.get_order import get_order
|
||||
from .ecommerce.track_package import track_package
|
||||
from .ecommerce.list_orders import list_orders
|
||||
|
||||
from .give_hint import give_hint
|
||||
from .guess_location import guess_location
|
||||
from .hr.book_pto import book_pto
|
||||
from .hr.checkpaybankstatus import checkpaybankstatus
|
||||
from .hr.current_pto import current_pto
|
||||
from .hr.future_pto_calc import future_pto_calc
|
||||
from .list_agents import list_agents
|
||||
from .search_fixtures import search_fixtures
|
||||
from .search_flights import search_flights
|
||||
from .search_trains import book_trains, search_trains
|
||||
from .transfer_control import transfer_control
|
||||
|
||||
|
||||
def get_handler(tool_name: str):
|
||||
@@ -67,7 +71,5 @@ def get_handler(tool_name: str):
|
||||
return give_hint
|
||||
if tool_name == "GuessLocation":
|
||||
return guess_location
|
||||
if tool_name == "AddToCart":
|
||||
return add_to_cart
|
||||
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
def change_goal(args: dict) -> dict:
|
||||
|
||||
new_goal = args.get("goalID")
|
||||
if new_goal is None:
|
||||
new_goal = "goal_choose_agent_type"
|
||||
|
||||
return {
|
||||
"new_goal": new_goal,
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import os
|
||||
|
||||
import stripe
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=True) # Load environment variables from a .env file
|
||||
|
||||
stripe.api_key = os.getenv("STRIPE_API_KEY")
|
||||
|
||||
|
||||
def ensure_customer_exists(
|
||||
customer_id: str = None, email: str = "default@example.com"
|
||||
) -> str:
|
||||
"""Ensure a Stripe customer exists; create one if not."""
|
||||
import stripe
|
||||
|
||||
if customer_id:
|
||||
try:
|
||||
stripe.Customer.retrieve(customer_id)
|
||||
@@ -24,12 +26,6 @@ def ensure_customer_exists(
|
||||
|
||||
def create_invoice(args: dict) -> dict:
|
||||
"""Create and finalize a Stripe invoice."""
|
||||
import stripe
|
||||
|
||||
# Load environment variables and configure stripe
|
||||
load_dotenv(override=True)
|
||||
stripe.api_key = os.getenv("STRIPE_API_KEY")
|
||||
|
||||
# If an API key exists in the env file, find or create customer
|
||||
if stripe.api_key is not None and stripe.api_key != "":
|
||||
customer_id = ensure_customer_exists(
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import json
|
||||
|
||||
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||
# called as part of a temporal activity with automatic retries
|
||||
def get_order(args: dict) -> dict:
|
||||
|
||||
order_id = args.get("order_id")
|
||||
|
||||
file_path = (
|
||||
Path(__file__).resolve().parent.parent / "data" / "customer_order_data.json"
|
||||
)
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_order_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
order_list = data["orders"]
|
||||
@@ -20,6 +18,6 @@ def get_order(args: dict) -> dict:
|
||||
for order in order_list:
|
||||
if order["id"] == order_id:
|
||||
return order
|
||||
|
||||
|
||||
return_msg = "Order " + order_id + " not found."
|
||||
return {"error": return_msg}
|
||||
return {"error": return_msg}
|
||||
@@ -1,20 +1,17 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import json
|
||||
|
||||
def sorting(e):
|
||||
return e["order_date"]
|
||||
|
||||
return e['order_date']
|
||||
|
||||
def list_orders(args: dict) -> dict:
|
||||
|
||||
email_address = args.get("email_address")
|
||||
|
||||
file_path = (
|
||||
Path(__file__).resolve().parent.parent / "data" / "customer_order_data.json"
|
||||
)
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_order_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
order_list = data["orders"]
|
||||
@@ -27,6 +24,7 @@ def list_orders(args: dict) -> dict:
|
||||
if len(rtn_order_list) > 0:
|
||||
rtn_order_list.sort(key=sorting)
|
||||
return {"orders": rtn_order_list}
|
||||
else:
|
||||
else:
|
||||
return_msg = "No orders for customer " + email_address + " found."
|
||||
return {"error": return_msg}
|
||||
|
||||
|
||||
@@ -1,59 +1,49 @@
|
||||
import http
|
||||
import json
|
||||
import os
|
||||
import json
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Send back dummy data in the correct format - to use the real API, 1) change this to be track_package_fake and 2) change the below track_package_real to be track_package
|
||||
#Send back dummy data in the correct format - to use the real API, 1) change this to be track_package_fake and 2) change the below track_package_real to be track_package
|
||||
def track_package(args: dict) -> dict:
|
||||
|
||||
tracking_id = args.get("tracking_id")
|
||||
file_path = (
|
||||
Path(__file__).resolve().parent.parent / "data" / "dummy_tracking_data.json"
|
||||
)
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "dummy_tracking_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
package_list = data["packages"]
|
||||
|
||||
for package in package_list:
|
||||
if package["TrackingNumber"] == tracking_id:
|
||||
scheduled_delivery_date = package["ScheduledDeliveryDate"]
|
||||
carrier = package["Carrier"]
|
||||
status_summary = package["StatusSummary"]
|
||||
tracking_details = package.get("TrackingDetails", [])
|
||||
last_tracking_update = ""
|
||||
if (
|
||||
tracking_details
|
||||
and tracking_details is not None
|
||||
and tracking_details[0] is not None
|
||||
):
|
||||
last_tracking_update = tracking_details[0][
|
||||
"EventDateTimeInDateTimeFormat"
|
||||
]
|
||||
|
||||
tracking_link = ""
|
||||
if carrier == "USPS":
|
||||
tracking_link = f"https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1={tracking_id}"
|
||||
elif carrier == "UPS":
|
||||
tracking_link = (
|
||||
f"https://www.ups.com/track?track=yes&trackNums={tracking_id}"
|
||||
)
|
||||
|
||||
return {
|
||||
"scheduled_delivery_date": scheduled_delivery_date,
|
||||
"carrier": carrier,
|
||||
"status_summary": status_summary,
|
||||
"tracking_link": tracking_link,
|
||||
"last_tracking_update": last_tracking_update,
|
||||
}
|
||||
scheduled_delivery_date = package["ScheduledDeliveryDate"]
|
||||
carrier = package["Carrier"]
|
||||
status_summary = package["StatusSummary"]
|
||||
tracking_details = package.get("TrackingDetails", [])
|
||||
last_tracking_update = ""
|
||||
if tracking_details and tracking_details is not None and tracking_details[0] is not None:
|
||||
last_tracking_update = tracking_details[0]["EventDateTimeInDateTimeFormat"]
|
||||
|
||||
tracking_link = ""
|
||||
if carrier == "USPS":
|
||||
tracking_link = f"https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1={tracking_id}"
|
||||
elif carrier == "UPS":
|
||||
tracking_link = f"https://www.ups.com/track?track=yes&trackNums={tracking_id}"
|
||||
|
||||
return {
|
||||
"scheduled_delivery_date": scheduled_delivery_date,
|
||||
"carrier": carrier,
|
||||
"status_summary": status_summary,
|
||||
"tracking_link": tracking_link,
|
||||
"last_tracking_update": last_tracking_update
|
||||
}
|
||||
|
||||
return_msg = "Package not found with tracking info " + tracking_id
|
||||
return {"error": return_msg}
|
||||
|
||||
|
||||
"""Format of response:
|
||||
'''Format of response:
|
||||
{
|
||||
"TrackingNumber": "",
|
||||
"Delivered": false,
|
||||
@@ -104,10 +94,9 @@ def track_package(args: dict) -> dict:
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
'''
|
||||
def track_package_real(args: dict) -> dict:
|
||||
|
||||
tracking_id = args.get("tracking_id")
|
||||
|
||||
api_key = os.getenv("RAPIDAPI_KEY")
|
||||
@@ -138,17 +127,11 @@ def track_package_real(args: dict) -> dict:
|
||||
status_summary = json_data["StatusSummary"]
|
||||
tracking_details = json_data.get("TrackingDetails", [])
|
||||
last_tracking_update = ""
|
||||
if (
|
||||
tracking_details
|
||||
and tracking_details is not None
|
||||
and tracking_details[0] is not None
|
||||
):
|
||||
if tracking_details and tracking_details is not None and tracking_details[0] is not None:
|
||||
last_tracking_update = tracking_details[0]["EventDateTimeInDateTimeFormat"]
|
||||
tracking_link = ""
|
||||
if carrier == "USPS":
|
||||
tracking_link = (
|
||||
f"https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1={tracking_id}"
|
||||
)
|
||||
tracking_link = f"https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1={tracking_id}"
|
||||
elif carrier == "UPS":
|
||||
tracking_link = f"https://www.ups.com/track?track=yes&trackNums={tracking_id}"
|
||||
|
||||
@@ -157,5 +140,5 @@ def track_package_real(args: dict) -> dict:
|
||||
"carrier": carrier,
|
||||
"status_summary": status_summary,
|
||||
"tracking_link": tracking_link,
|
||||
"last_tracking_update": last_tracking_update,
|
||||
}
|
||||
"last_tracking_update": last_tracking_update
|
||||
}
|
||||
@@ -1,31 +1,24 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import json
|
||||
|
||||
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||
# called as part of a temporal activity with automatic retries
|
||||
def check_account_valid(args: dict) -> dict:
|
||||
|
||||
email = args.get("email")
|
||||
account_id = args.get("account_id")
|
||||
|
||||
file_path = (
|
||||
Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||
)
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
account_list = data["accounts"]
|
||||
|
||||
for account in account_list:
|
||||
if account["email"] == email or account["account_id"] == account_id:
|
||||
return {"status": "account valid"}
|
||||
|
||||
return_msg = (
|
||||
"Account not found with email address "
|
||||
+ email
|
||||
+ " or account ID: "
|
||||
+ account_id
|
||||
)
|
||||
return {"error": return_msg}
|
||||
return{"status": "account valid"}
|
||||
|
||||
return_msg = "Account not found with email address " + email + " or account ID: " + account_id
|
||||
return {"error": return_msg}
|
||||
@@ -1,33 +1,23 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import json
|
||||
|
||||
# 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
|
||||
def get_account_balance(args: dict) -> dict:
|
||||
|
||||
account_key = args.get("email_address_or_account_ID")
|
||||
|
||||
file_path = (
|
||||
Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||
)
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
|
||||
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:
|
||||
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 for " + account_key
|
||||
return {"error": return_msg}
|
||||
return {"error": return_msg}
|
||||
@@ -1,12 +1,16 @@
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
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
|
||||
@@ -28,6 +32,7 @@ class MoneyMovementWorkflowParameterObj:
|
||||
# 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:
|
||||
|
||||
account_key = args.get("email_address_or_account_ID")
|
||||
account_type: str = args.get("accounttype")
|
||||
amount = args.get("amount")
|
||||
@@ -96,6 +101,7 @@ async def move_money(args: dict) -> dict:
|
||||
async def start_workflow(
|
||||
amount_cents: int, from_account_name: str, to_account_name: str
|
||||
) -> str:
|
||||
|
||||
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
|
||||
@@ -122,7 +128,7 @@ async def start_workflow(
|
||||
task_queue="MoneyTransferJava", # Task queue name
|
||||
)
|
||||
return handle.id
|
||||
except WorkflowAlreadyStartedError:
|
||||
except WorkflowAlreadyStartedError as e:
|
||||
existing_handle = client.get_workflow_handle(workflow_id=workflow_id)
|
||||
return existing_handle.id
|
||||
else:
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
from datetime import date, timedelta
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
from temporalio.client import (
|
||||
Client,
|
||||
WithStartWorkflowOperation,
|
||||
WorkflowHandle,
|
||||
WorkflowUpdateFailedError,
|
||||
)
|
||||
from temporalio import common
|
||||
from temporalio.client import WithStartWorkflowOperation, WorkflowUpdateFailedError
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
from temporalio.exceptions import WorkflowAlreadyStartedError
|
||||
from shared.config import get_temporal_client
|
||||
|
||||
|
||||
@@ -16,55 +24,39 @@ class TransactionRequest:
|
||||
sourceAccount: str
|
||||
targetAccount: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TxResult:
|
||||
transactionId: str
|
||||
status: str
|
||||
|
||||
|
||||
# demonstrate starting a workflow and early return pattern while the workflow continues
|
||||
#demonstrate starting a workflow and early return pattern while the workflow continues
|
||||
async def submit_loan_application(args: dict) -> dict:
|
||||
account_key = args.get("email_address_or_account_ID")
|
||||
amount = args.get("amount")
|
||||
|
||||
loan_status: dict = await start_workflow(amount=amount, account_name=account_key)
|
||||
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"),
|
||||
}
|
||||
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:
|
||||
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",
|
||||
}
|
||||
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
|
||||
# Connect to Temporal
|
||||
START_REAL_WORKFLOW = True
|
||||
# Connect to Temporal
|
||||
client = await get_temporal_client()
|
||||
|
||||
|
||||
# Define the workflow ID and task queue
|
||||
workflow_id = (
|
||||
"LOAN_APPLICATION-" + account_name + "-" + date.today().strftime("%Y-%m-%d")
|
||||
)
|
||||
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)
|
||||
@@ -91,27 +83,21 @@ async def start_workflow(
|
||||
)
|
||||
)
|
||||
except WorkflowUpdateFailedError:
|
||||
print("aww man got exception 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(f"Workflow started with ID: {workflow_handle.id}")
|
||||
print(tx_result)
|
||||
|
||||
print(
|
||||
f"Update result: Transaction ID = {tx_result.transactionId}, Message = {tx_result.status}"
|
||||
)
|
||||
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",
|
||||
}
|
||||
|
||||
# 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", }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
|
||||
def find_events(args: dict) -> dict:
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
def add_to_cart(args: dict) -> dict:
|
||||
"""
|
||||
Simple stateless cart tool for demo purposes.
|
||||
In production, this would use proper session storage or database.
|
||||
"""
|
||||
customer_email = args.get("customer_email")
|
||||
item_name = args.get("item_name")
|
||||
item_price = float(args.get("item_price", 0))
|
||||
quantity = int(args.get("quantity", 1))
|
||||
stripe_product_id = args.get("stripe_product_id")
|
||||
|
||||
# Basic validation
|
||||
if not customer_email:
|
||||
return {"error": "Customer email is required"}
|
||||
if not item_name:
|
||||
return {"error": "Item name is required"}
|
||||
if item_price <= 0:
|
||||
return {"error": "Item price must be greater than 0"}
|
||||
if quantity <= 0:
|
||||
return {"error": "Quantity must be greater than 0"}
|
||||
|
||||
# For demo purposes, just acknowledge the addition
|
||||
# In a real system, this would store to session/database
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Added {quantity} x {item_name} (${item_price}) to cart for {customer_email}",
|
||||
"item_added": {
|
||||
"name": item_name,
|
||||
"price": item_price,
|
||||
"quantity": quantity,
|
||||
"stripe_product_id": stripe_product_id,
|
||||
},
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def delete_food_ordering_products():
|
||||
"""Archive all Stripe products with metadata use_case = food_ordering_demo (since products with prices cannot be deleted)."""
|
||||
import stripe
|
||||
|
||||
# Load environment variables and configure stripe
|
||||
load_dotenv(override=True)
|
||||
stripe.api_key = os.getenv("STRIPE_API_KEY")
|
||||
|
||||
if not stripe.api_key:
|
||||
print("Error: STRIPE_API_KEY not found in environment variables")
|
||||
return
|
||||
|
||||
try:
|
||||
# Search for products with food_ordering_demo use_case
|
||||
products = stripe.Product.search(
|
||||
query="metadata['use_case']:'food_ordering_demo'", limit=100
|
||||
)
|
||||
|
||||
if not products.data:
|
||||
print("No products found with use_case = food_ordering_demo")
|
||||
return
|
||||
|
||||
archived_count = 0
|
||||
|
||||
for product in products.data:
|
||||
try:
|
||||
# Archive the product (set active=False)
|
||||
stripe.Product.modify(product.id, active=False)
|
||||
print(f"Archived product: {product.name} (ID: {product.id})")
|
||||
archived_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error archiving product {product.name} (ID: {product.id}): {str(e)}"
|
||||
)
|
||||
|
||||
print(f"\nSuccessfully archived {archived_count} products")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error searching for products: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
delete_food_ordering_products()
|
||||
@@ -1,92 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def create_stripe_products():
|
||||
"""Create Stripe products and prices from the stripe_pizza_products.json file."""
|
||||
import stripe
|
||||
|
||||
# Load environment variables and configure stripe
|
||||
load_dotenv(override=True)
|
||||
stripe.api_key = os.getenv("STRIPE_API_KEY")
|
||||
|
||||
if not stripe.api_key:
|
||||
print("Error: STRIPE_API_KEY not found in environment variables")
|
||||
return
|
||||
|
||||
# Load the products data
|
||||
current_dir = os.path.dirname(__file__)
|
||||
products_file = os.path.join(current_dir, "stripe_pizza_products.json")
|
||||
|
||||
with open(products_file, "r") as f:
|
||||
products_data = json.load(f)
|
||||
|
||||
# Filter for food ordering demo products only
|
||||
food_products = [
|
||||
p
|
||||
for p in products_data
|
||||
if p.get("metadata", {}).get("use_case") == "food_ordering_demo"
|
||||
]
|
||||
|
||||
created_products = []
|
||||
|
||||
for product_data in food_products:
|
||||
try:
|
||||
# Create the product with relevant fields
|
||||
product = stripe.Product.create(
|
||||
name=product_data["name"],
|
||||
description=product_data.get("description"),
|
||||
images=product_data.get("images", []),
|
||||
metadata=product_data.get("metadata", {}),
|
||||
type=product_data.get("type", "service"),
|
||||
active=product_data.get("active", True),
|
||||
)
|
||||
|
||||
# Create price for the product if price_info exists
|
||||
price_info = product_data.get("price_info")
|
||||
if price_info:
|
||||
price_amount = price_info.get("amount")
|
||||
currency = price_info.get("currency", "usd")
|
||||
|
||||
price = stripe.Price.create(
|
||||
currency=currency, unit_amount=price_amount, product=product.id
|
||||
)
|
||||
|
||||
# Set this price as the default price for the product
|
||||
stripe.Product.modify(product.id, default_price=price.id)
|
||||
|
||||
print(
|
||||
f"Created product: {product.name} (ID: {product.id}) with default price ${price_amount/100:.2f}"
|
||||
)
|
||||
|
||||
created_products.append(
|
||||
{
|
||||
"name": product.name,
|
||||
"id": product.id,
|
||||
"price_id": price.id,
|
||||
"price_amount": price_amount,
|
||||
"original_id": product_data["id"],
|
||||
}
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Created product: {product.name} (ID: {product.id}) - No price defined"
|
||||
)
|
||||
created_products.append(
|
||||
{
|
||||
"name": product.name,
|
||||
"id": product.id,
|
||||
"original_id": product_data["id"],
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating product {product_data['name']}: {str(e)}")
|
||||
|
||||
print(f"\nSuccessfully created {len(created_products)} products with prices")
|
||||
return created_products
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_stripe_products()
|
||||
@@ -1,188 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "prod_SSWirxxS5A8gcT",
|
||||
"object": "product",
|
||||
"active": true,
|
||||
"attributes": [],
|
||||
"created": 1749360061,
|
||||
"default_price": "price_1RXbfGKVZbzw7QA57Mj1akGI",
|
||||
"description": "A large size bottle of cola.",
|
||||
"images": [
|
||||
"https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfbDJxckJKMDRnT1dDc253OHlZNWNkZkY5006Xg07kHT"
|
||||
],
|
||||
"livemode": false,
|
||||
"marketing_features": [],
|
||||
"metadata": {
|
||||
"use_case": "food_ordering_demo"
|
||||
},
|
||||
"name": "Soda",
|
||||
"price_info": {
|
||||
"amount": 349,
|
||||
"currency": "usd"
|
||||
},
|
||||
"package_dimensions": null,
|
||||
"shippable": null,
|
||||
"statement_descriptor": null,
|
||||
"tax_code": null,
|
||||
"type": "service",
|
||||
"unit_label": null,
|
||||
"updated": 1749360062,
|
||||
"url": null
|
||||
},
|
||||
{
|
||||
"id": "prod_SSWhxv3tUy1YOG",
|
||||
"object": "product",
|
||||
"active": true,
|
||||
"attributes": [],
|
||||
"created": 1749359978,
|
||||
"default_price": "price_1RXbdvKVZbzw7QA5ARomQvaf",
|
||||
"description": "Our warm, crusty bread is generously spread with a savory garlic butter and toasted to golden perfection. It's the ideal aromatic and flavorful side to accompany your main course.",
|
||||
"images": [
|
||||
"https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfWTdIZTBkUjNZNFQ1ZEhSVG9nRnduY1pS00XVgLRRZD"
|
||||
],
|
||||
"livemode": false,
|
||||
"marketing_features": [],
|
||||
"metadata": {
|
||||
"use_case": "food_ordering_demo"
|
||||
},
|
||||
"name": "Garlic Bread",
|
||||
"price_info": {
|
||||
"amount": 799,
|
||||
"currency": "usd"
|
||||
},
|
||||
"package_dimensions": null,
|
||||
"shippable": null,
|
||||
"statement_descriptor": null,
|
||||
"tax_code": null,
|
||||
"type": "service",
|
||||
"unit_label": null,
|
||||
"updated": 1749360084,
|
||||
"url": null
|
||||
},
|
||||
{
|
||||
"id": "prod_SSWgXa5bwUFCJs",
|
||||
"object": "product",
|
||||
"active": true,
|
||||
"attributes": [],
|
||||
"created": 1749359922,
|
||||
"default_price": "price_1RXbd0KVZbzw7QA5Nq36vdLW",
|
||||
"description": "A tribute to Italian simplicity, this pizza is topped with fresh mozzarella, a vibrant tomato sauce, and fragrant basil leaves. Each bite delivers a clean and authentic taste of Italy's most famous flavors.",
|
||||
"images": [
|
||||
"https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfamdmTXBFbzY0TW9rS2N0c2g0Tml2SERL00Evl60Ttq"
|
||||
],
|
||||
"livemode": false,
|
||||
"marketing_features": [],
|
||||
"metadata": {
|
||||
"use_case": "food_ordering_demo"
|
||||
},
|
||||
"name": "Margherita Pizza",
|
||||
"price_info": {
|
||||
"amount": 1699,
|
||||
"currency": "usd"
|
||||
},
|
||||
"package_dimensions": null,
|
||||
"shippable": null,
|
||||
"statement_descriptor": null,
|
||||
"tax_code": null,
|
||||
"type": "service",
|
||||
"unit_label": null,
|
||||
"updated": 1749359998,
|
||||
"url": null
|
||||
},
|
||||
{
|
||||
"id": "prod_SSWf738UqIJzzi",
|
||||
"object": "product",
|
||||
"active": true,
|
||||
"attributes": [],
|
||||
"created": 1749359845,
|
||||
"default_price": "price_1RXbbmKVZbzw7QA53EkjV2nB",
|
||||
"description": "A timeless classic featuring a generous layer of savory pepperoni over rich tomato sauce and melted mozzarella cheese. It's the perfect choice for those who love a bold, meaty flavor on a perfectly baked crust.",
|
||||
"images": [
|
||||
"https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfcGRHc0c4cEZYWmR2bm0zOHBOa0FWMk5t008QmCJoWr"
|
||||
],
|
||||
"livemode": false,
|
||||
"marketing_features": [],
|
||||
"metadata": {
|
||||
"use_case": "food_ordering_demo"
|
||||
},
|
||||
"name": "Pepperoni Pizza",
|
||||
"price_info": {
|
||||
"amount": 2299,
|
||||
"currency": "usd"
|
||||
},
|
||||
"package_dimensions": null,
|
||||
"shippable": null,
|
||||
"statement_descriptor": null,
|
||||
"tax_code": null,
|
||||
"type": "service",
|
||||
"unit_label": null,
|
||||
"updated": 1749359846,
|
||||
"url": null
|
||||
},
|
||||
{
|
||||
"id": "prod_SGMXBnatLlkJ4d",
|
||||
"object": "product",
|
||||
"active": true,
|
||||
"attributes": [],
|
||||
"created": 1746554502,
|
||||
"default_price": "price_1RLpoJKVZbzw7QA5ra76Fk6g",
|
||||
"description": null,
|
||||
"images": [],
|
||||
"livemode": false,
|
||||
"marketing_features": [],
|
||||
"metadata": {},
|
||||
"name": "ACME Scooter Token",
|
||||
"package_dimensions": null,
|
||||
"shippable": null,
|
||||
"statement_descriptor": null,
|
||||
"tax_code": null,
|
||||
"type": "service",
|
||||
"unit_label": null,
|
||||
"updated": 1746554503,
|
||||
"url": null
|
||||
},
|
||||
{
|
||||
"id": "prod_NxJPcqTWzXk45K",
|
||||
"object": "product",
|
||||
"active": true,
|
||||
"attributes": [],
|
||||
"created": 1684961969,
|
||||
"default_price": null,
|
||||
"description": "$12/Month subscription",
|
||||
"images": [],
|
||||
"livemode": false,
|
||||
"marketing_features": [],
|
||||
"metadata": {},
|
||||
"name": "Starter Subscription",
|
||||
"package_dimensions": null,
|
||||
"shippable": null,
|
||||
"statement_descriptor": null,
|
||||
"tax_code": null,
|
||||
"type": "service",
|
||||
"unit_label": null,
|
||||
"updated": 1684961969,
|
||||
"url": null
|
||||
},
|
||||
{
|
||||
"id": "prod_NxJ4KvyENd0uUu",
|
||||
"object": "product",
|
||||
"active": true,
|
||||
"attributes": [],
|
||||
"created": 1684960731,
|
||||
"default_price": null,
|
||||
"description": "Created with the Stripe CLI",
|
||||
"images": [],
|
||||
"livemode": false,
|
||||
"marketing_features": [],
|
||||
"metadata": {},
|
||||
"name": "Temporal Money Transfer",
|
||||
"package_dimensions": null,
|
||||
"shippable": null,
|
||||
"statement_descriptor": null,
|
||||
"tax_code": null,
|
||||
"type": "service",
|
||||
"unit_label": null,
|
||||
"updated": 1684960731,
|
||||
"url": null
|
||||
}
|
||||
]
|
||||
@@ -1,10 +1,10 @@
|
||||
TREASURE_LOCATION = {
|
||||
"address": "300 Lenora",
|
||||
"city": "Seattle",
|
||||
"state_full": "Washington",
|
||||
"state_abbrev": "WA",
|
||||
"zip": "98121",
|
||||
"country": "USA",
|
||||
"address": "300 Lenora",
|
||||
"city": "Seattle",
|
||||
"state_full": "Washington",
|
||||
"state_abbrev": "WA",
|
||||
"zip": "98121",
|
||||
"country": "USA"
|
||||
}
|
||||
|
||||
HINTS = [
|
||||
@@ -12,8 +12,8 @@ HINTS = [
|
||||
"state of " + TREASURE_LOCATION["state_full"],
|
||||
"city of " + TREASURE_LOCATION["city"],
|
||||
"at a company HQ",
|
||||
"The company's tech traces its roots to a project called Cadence", # thanks, Grok
|
||||
"The company offers a tool that lets developers write code as if it's running forever, no matter what crashes", # thanks, Grok
|
||||
"The company's tech traces its roots to a project called Cadence", #thanks, Grok
|
||||
"The company offers a tool that lets developers write code as if it's running forever, no matter what crashes", #thanks, Grok
|
||||
]
|
||||
''' Additional Grok provided hints about Temporal:
|
||||
"This company was founded by two engineers who previously worked on a system named after a South American river at Uber."
|
||||
@@ -26,14 +26,16 @@ HINTS = [
|
||||
"They’re backed by big venture capital names like Sequoia, betting on their vision for reliable software."
|
||||
"The company’s name might remind you of a word for something fleeting, yet their tech is built to last."'''
|
||||
|
||||
|
||||
def give_hint(args: dict) -> dict:
|
||||
hint_total = args.get("hint_total")
|
||||
if hint_total is None:
|
||||
hint_total = 0
|
||||
|
||||
|
||||
index = hint_total % len(HINTS)
|
||||
hint_text = HINTS[index]
|
||||
|
||||
hint_total = hint_total + 1
|
||||
return {"hint_number": hint_total, "hint": hint_text}
|
||||
return {
|
||||
"hint_number": hint_total,
|
||||
"hint": hint_text
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from models.tool_definitions import AgentGoal
|
||||
import tools.tool_registry as tool_registry
|
||||
from models.tool_definitions import AgentGoal, MCPServerDefinition
|
||||
|
||||
# Turn on Silly Mode - this should be a description of the persona you'd like the bot to have and can be a single word or a phrase.
|
||||
# Example if you want the bot to be a specific person, like Mario or Christopher Walken, or to describe a specific tone:
|
||||
@@ -455,126 +454,6 @@ goal_ecomm_list_orders = AgentGoal(
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ----- MCP Integrations -----
|
||||
goal_mcp_stripe = AgentGoal(
|
||||
id="goal_mcp_stripe",
|
||||
category_tag="mcp-integrations",
|
||||
agent_name="Stripe MCP Agent",
|
||||
agent_friendly_description="Manage Stripe operations via MCP",
|
||||
tools=[], # Will be populated dynamically
|
||||
mcp_server_definition=MCPServerDefinition(
|
||||
name="stripe-mcp",
|
||||
command="npx",
|
||||
args=[
|
||||
"-y",
|
||||
"@stripe/mcp",
|
||||
"--tools=all",
|
||||
f"--api-key={os.getenv('STRIPE_API_KEY')}",
|
||||
],
|
||||
env=None,
|
||||
included_tools=["list_customers", "list_products"],
|
||||
),
|
||||
description="Help manage Stripe operations for customer and product data by using the customers.read and products.read tools.",
|
||||
starter_prompt="Welcome! I can help you read Stripe customer and product information.",
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"agent: Welcome! I can help you read Stripe customer and product information. What would you like to do first?",
|
||||
"user: what customers are there?",
|
||||
"agent: I'll check for customers now.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on customers.read tool>",
|
||||
'tool_result: { "customers": [{"id": "cus_abc", "name": "Customer A"}, {"id": "cus_xyz", "name": "Customer B"}] }',
|
||||
"agent: I found two customers: Customer A and Customer B. Can I help with anything else?",
|
||||
"user: what products exist?",
|
||||
"agent: Let me get the list of products for you.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on products.read tool>",
|
||||
'tool_result: { "products": [{"id": "prod_123", "name": "Gold Plan"}, {"id": "prod_456", "name": "Silver Plan"}] }',
|
||||
"agent: I found two products: Gold Plan and Silver Plan.",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# ----- Food Ordering Goal -----
|
||||
goal_food_ordering = AgentGoal(
|
||||
id="goal_food_ordering",
|
||||
category_tag="food",
|
||||
agent_name="Food Ordering Assistant",
|
||||
agent_friendly_description="Order food from Tony's Pizza Palace using Stripe for payment processing. Browse menu, add items to your order, and check out securely. Please ensure context carries over between tool runs.",
|
||||
tools=[tool_registry.food_add_to_cart_tool],
|
||||
mcp_server_definition=MCPServerDefinition(
|
||||
name="stripe-mcp",
|
||||
command="npx",
|
||||
args=[
|
||||
"-y",
|
||||
"@stripe/mcp",
|
||||
"--tools=all",
|
||||
f"--api-key={os.getenv('STRIPE_API_KEY')}",
|
||||
],
|
||||
env=None,
|
||||
included_tools=[
|
||||
"list_products",
|
||||
"list_prices",
|
||||
"create_customer",
|
||||
"create_invoice",
|
||||
"create_invoice_item",
|
||||
"finalize_invoice",
|
||||
],
|
||||
),
|
||||
description="The user wants to order food from Tony's Pizza Palace. "
|
||||
"First, help the user browse the menu by calling list_products. "
|
||||
"When they express interest in items, get pricing using list_prices. "
|
||||
"Add items to their cart using AddToCart as they decide - the order doesn't matter, multiple items can be added. "
|
||||
"After they're done selecting items, get their customer details and create a Stripe customer. "
|
||||
"For checkout: 1) create_invoice, 2) create_invoice_item for each individual item (IMPORTANT: create_invoice_item does NOT accept quantity parameter - call it once per item, so if user wants 2 pizzas, call create_invoice_item twice with the same price), "
|
||||
"3) finalize_invoice. The finalized invoice will contain a hosted_invoice_url for payment.",
|
||||
starter_prompt=starter_prompt_generic,
|
||||
example_conversation_history="\n ".join(
|
||||
[
|
||||
"user: I want to order food.",
|
||||
"agent: Welcome to Tony's Pizza Palace! I can help you browse our menu, add items to your cart, and process payment. Let me show you what we have available.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on list_products tool with limit=100 and product.metadata.use_case == 'food_ordering_demo'>",
|
||||
'tool_result: {"products": [{"id": "prod_pepperoni", "name": "Pepperoni Pizza", "description": "A timeless classic featuring pepperoni"}, {"id": "prod_margherita", "name": "Margherita Pizza", "description": "Fresh mozzarella and basil"}, {"id": "prod_garlic_bread", "name": "Garlic Bread", "description": "Warm crusty bread with garlic butter"}, {"id": "prod_soda", "name": "Soda", "description": "A large bottle of cola"}]}',
|
||||
"agent: We have Pepperoni Pizza, Margherita Pizza, Garlic Bread, and Soda available. What would you like to order?",
|
||||
"user: I'd like 2 pepperoni pizzas and a soda.",
|
||||
"agent: Excellent! Let me get the pricing for the pepperoni pizzas first so you know the cost.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on list_prices tool with product='prod_pepperoni'>",
|
||||
'tool_result: {"prices": [{"id": "price_pepperoni_large", "product": "prod_pepperoni", "unit_amount": 2299, "currency": "usd"}]}',
|
||||
"agent: Pepperoni Pizza is $22.99 each. Now let me check the soda pricing:",
|
||||
"user_confirmed_tool_run: <user clicks confirm on list_prices tool with product='prod_soda'>",
|
||||
'tool_result: {"prices": [{"id": "price_soda", "product": "prod_soda", "unit_amount": 250, "currency": "usd"}]}',
|
||||
"agent: And Soda is $2.50. So 2 Pepperoni Pizzas ($22.99 each) plus 1 Soda ($2.50) = $48.48 total. What's your email address to add these items to your cart?",
|
||||
"user: jane.smith@example.com",
|
||||
"agent: Perfect! Adding 2 Pepperoni Pizzas to your cart for jane.smith@example.com. Note: I'm using quantity=2 since you want 2 pizzas.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on AddToCart tool with customer_email='jane.smith@example.com', item_name='Pepperoni Pizza', item_price=22.99, quantity=2, stripe_product_id='prod_pepperoni'>",
|
||||
'tool_result: {"status": "success", "message": "Added 2 x Pepperoni Pizza ($22.99) to cart for jane.smith@example.com", "item_added": {"quantity": 2, "name": "Pepperoni Pizza"}}',
|
||||
"agent: Great! Added 2 Pepperoni Pizzas to your cart. Now adding the Soda (quantity=1):",
|
||||
"user_confirmed_tool_run: <user clicks confirm on AddToCart tool with customer_email='jane.smith@example.com', item_name='Soda', item_price=2.50, quantity=1, stripe_product_id='prod_soda'>",
|
||||
'tool_result: {"status": "success", "message": "Added 1 x Soda ($2.50) to cart for jane.smith@example.com", "item_added": {"quantity": 1, "name": "Soda"}}',
|
||||
"agent: Excellent! Your cart now contains: 2 Pepperoni Pizzas ($22.99 each) and 1 Soda ($2.50). Ready to checkout? I'll need your full name.",
|
||||
"user: Yes, let's proceed with payment. My name is Jane Smith.",
|
||||
"agent: Thank you Jane! Creating your Stripe customer profile with name and email.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_customer tool with name='Jane Smith' and email='jane.smith@example.com'>",
|
||||
'tool_result: {"customer": {"id": "cus_ExAmPlE12345", "name": "Jane Smith", "email": "jane.smith@example.com"}}',
|
||||
"agent: Customer profile created! Now I'll create a draft invoice for your order.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_invoice tool with customer='cus_ExAmPlE12345'>",
|
||||
'tool_result: {"invoice": {"id": "in_InvOicE54321", "status": "draft", "customer": "cus_ExAmPlE12345"}}',
|
||||
"agent: Invoice created! Now adding your first Pepperoni Pizza to the invoice.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_pepperoni_large'>",
|
||||
'tool_result: {"status": "success", "invoice_item": {"id": "ii_pizza1"}}',
|
||||
"agent: First pizza added! Now adding your second Pepperoni Pizza to the invoice.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_pepperoni_large'>",
|
||||
'tool_result: {"status": "success", "invoice_item": {"id": "ii_pizza2"}}',
|
||||
"agent: Both pizzas added! Now adding your Soda to the invoice.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_soda'>",
|
||||
'tool_result: {"status": "success", "invoice_item": {"id": "ii_soda"}}',
|
||||
"agent: All items added to invoice! Now finalizing to generate the payment link.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on finalize_invoice tool with invoice='in_InvOicE54321'>",
|
||||
'tool_result: {"invoice": {"id": "in_InvOicE54321", "status": "open", "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RKeJRMlw?s=ap", "amount_due": 4848}}',
|
||||
"agent: Perfect! Your order is complete with 2 Pepperoni Pizzas and 1 Soda. Total: $48.48. Pay securely here: https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RKeJRMlw?s=ap\\n\\nThank you for ordering from Tony's Pizza Palace!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# 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)
|
||||
@@ -589,21 +468,19 @@ 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)
|
||||
goal_list.append(goal_mcp_stripe)
|
||||
goal_list.append(goal_food_ordering)
|
||||
|
||||
|
||||
# for multi-goal, just set list agents as the last tool
|
||||
first_goal_value = os.getenv("AGENT_GOAL")
|
||||
if first_goal_value is None:
|
||||
multi_goal_mode = False # default to single agent mode if unset
|
||||
multi_goal_mode = True # default if unset
|
||||
elif (
|
||||
first_goal_value is not None
|
||||
and first_goal_value.lower() == "goal_choose_agent_type"
|
||||
and first_goal_value.lower() != "goal_choose_agent_type"
|
||||
):
|
||||
multi_goal_mode = True
|
||||
else:
|
||||
multi_goal_mode = False
|
||||
else:
|
||||
multi_goal_mode = True
|
||||
|
||||
if multi_goal_mode:
|
||||
for goal in goal_list:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from .give_hint import TREASURE_LOCATION
|
||||
|
||||
|
||||
def guess_location(args: dict) -> dict:
|
||||
|
||||
guess_address = args.get("address").lower()
|
||||
guess_city = args.get("city").lower()
|
||||
guess_state = args.get("state").lower()
|
||||
@@ -11,12 +11,8 @@ def guess_location(args: dict) -> dict:
|
||||
else:
|
||||
compare_state = TREASURE_LOCATION.get("state_full").lower()
|
||||
|
||||
# Check for the street address to be included in the guess to account for "st" vs "street" or leaving Street off entirely
|
||||
if (
|
||||
TREASURE_LOCATION.get("address").lower() in guess_address
|
||||
and TREASURE_LOCATION.get("city").lower() == guess_city
|
||||
and compare_state == guess_state
|
||||
):
|
||||
#Check for the street address to be included in the guess to account for "st" vs "street" or leaving Street off entirely
|
||||
if TREASURE_LOCATION.get("address").lower() in guess_address and TREASURE_LOCATION.get("city").lower() == guess_city and compare_state == guess_state:
|
||||
return {"treasure_found": "True"}
|
||||
else:
|
||||
return {"treasure_found": "False"}
|
||||
return {"treasure_found": "False"}
|
||||
@@ -1,10 +1,11 @@
|
||||
def book_pto(args: dict) -> dict:
|
||||
|
||||
email = args.get("email")
|
||||
start_date = args.get("start_date")
|
||||
end_date = args.get("end_date")
|
||||
|
||||
print(
|
||||
f"[BookPTO] Totally would send an email confirmation of PTO from {start_date} to {end_date} to {email} here!"
|
||||
)
|
||||
print(f"[BookPTO] Totally would send an email confirmation of PTO from {start_date} to {end_date} to {email} here!")
|
||||
|
||||
return {"status": "success"}
|
||||
return {
|
||||
"status": "success"
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
|
||||
def checkpaybankstatus(args: dict) -> dict:
|
||||
|
||||
email = args.get("email")
|
||||
|
||||
if email == "grinch@grinch.com":
|
||||
@@ -7,4 +12,4 @@ def checkpaybankstatus(args: dict) -> dict:
|
||||
|
||||
# could do logic here or look up data but for now everyone but the grinch is getting paid
|
||||
return_msg = "connected"
|
||||
return {"status": return_msg}
|
||||
return {"status": return_msg}
|
||||
@@ -1,27 +1,26 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
|
||||
def current_pto(args: dict) -> dict:
|
||||
|
||||
email = args.get("email")
|
||||
|
||||
file_path = (
|
||||
Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
||||
)
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
|
||||
data = json.load(open(file_path))
|
||||
employee_list = data["theCompany"]["employees"]
|
||||
|
||||
for employee in employee_list:
|
||||
if employee["email"] == email:
|
||||
num_hours = int(employee["currentPTOHrs"])
|
||||
num_days = float(num_hours / 8)
|
||||
num_days = float(num_hours/8)
|
||||
return {
|
||||
"num_hours": num_hours,
|
||||
"num_days": num_days,
|
||||
}
|
||||
|
||||
|
||||
return_msg = "Employee not found with email address " + email
|
||||
return {"error": return_msg}
|
||||
return {"error": return_msg}
|
||||
@@ -1,59 +1,43 @@
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pandas
|
||||
from pathlib import Path
|
||||
from datetime import date, datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
def future_pto_calc(args: dict) -> dict:
|
||||
file_path = (
|
||||
Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
||||
)
|
||||
|
||||
file_path = Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
||||
if not file_path.exists():
|
||||
return {"error": "Data file not found."}
|
||||
|
||||
|
||||
start_date = datetime.strptime(args.get("start_date"), "%Y-%m-%d").date()
|
||||
end_date = datetime.strptime(args.get("end_date"), "%Y-%m-%d").date()
|
||||
email = args.get("email")
|
||||
|
||||
# Next, set up the ability to calculate how much PTO will be added to the user's total by the start of the PTO request
|
||||
#Next, set up the ability to calculate how much PTO will be added to the user's total by the start of the PTO request
|
||||
today = date.today()
|
||||
|
||||
if today > start_date:
|
||||
return_msg = (
|
||||
"PTO start date " + args.get("start_date") + "cannot be in the past"
|
||||
)
|
||||
return_msg = "PTO start date " + args.get("start_date") + "cannot be in the past"
|
||||
return {"error": return_msg}
|
||||
|
||||
|
||||
if end_date < start_date:
|
||||
return_msg = (
|
||||
"PTO end date "
|
||||
+ args.get("end_date")
|
||||
+ " must be after PTO start date "
|
||||
+ args.get("start_date")
|
||||
)
|
||||
return_msg = "PTO end date " + args.get("end_date") + " must be after PTO start date " + args.get("start_date")
|
||||
return {"error": return_msg}
|
||||
|
||||
# Get the number of business days, and then business hours (assume 8 hr biz day), included in the PTO request
|
||||
biz_days_of_request = len(
|
||||
pandas.bdate_range(start=start_date, end=end_date, inclusive="both")
|
||||
)
|
||||
|
||||
#Get the number of business days, and then business hours (assume 8 hr biz day), included in the PTO request
|
||||
biz_days_of_request = len(pandas.bdate_range(start=start_date, end=end_date, inclusive="both"))
|
||||
if biz_days_of_request == 0:
|
||||
return_msg = (
|
||||
"There are no business days between "
|
||||
+ args.get("start_date")
|
||||
+ " and "
|
||||
+ args.get("end_date")
|
||||
)
|
||||
return_msg = "There are no business days between " + args.get("start_date") + " and " + args.get("end_date")
|
||||
return {"error": return_msg}
|
||||
biz_hours_of_request = biz_days_of_request * 8
|
||||
|
||||
# Assume PTO is added on the first of every month - month math compares rolling dates, so compare the PTO request with the first day of the current month.
|
||||
|
||||
#Assume PTO is added on the first of every month - month math compares rolling dates, so compare the PTO request with the first day of the current month.
|
||||
today_first_of_month = date(today.year, today.month, 1)
|
||||
time_difference = relativedelta(start_date, today_first_of_month)
|
||||
months_to_accrue = time_difference.years * 12 + time_difference.months
|
||||
|
||||
|
||||
data = json.load(open(file_path))
|
||||
employee_list = data["theCompany"]["employees"]
|
||||
|
||||
@@ -63,14 +47,12 @@ def future_pto_calc(args: dict) -> dict:
|
||||
if employee["email"] == email:
|
||||
current_pto_hours = int(employee["currentPTOHrs"])
|
||||
hrs_added_per_month = int(employee["hrsAddedPerMonth"])
|
||||
pto_available_at_start = current_pto_hours + (
|
||||
months_to_accrue * hrs_added_per_month
|
||||
)
|
||||
pto_available_at_start = current_pto_hours + (months_to_accrue * hrs_added_per_month)
|
||||
pto_hrs_remaining_after = pto_available_at_start - biz_hours_of_request
|
||||
if pto_hrs_remaining_after >= 0:
|
||||
enough_pto = True
|
||||
return {
|
||||
"enough_pto": enough_pto,
|
||||
"enough_pto": enough_pto,
|
||||
"pto_hrs_remaining_after": str(pto_hrs_remaining_after),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import os
|
||||
|
||||
import goals
|
||||
|
||||
import tools.goal_registry as goals
|
||||
|
||||
def list_agents(args: dict) -> dict:
|
||||
|
||||
goal_categories_start = os.getenv("GOAL_CATEGORIES")
|
||||
if goal_categories_start is None:
|
||||
goal_categories = ["all"] # default to 'all' categories
|
||||
goal_categories = ["all"] # default to 'all' categories
|
||||
else:
|
||||
goal_categories_start.strip().lower() # handle extra spaces or non-lowercase
|
||||
goal_categories_start.strip().lower() # handle extra spaces or non-lowercase
|
||||
goal_categories = goal_categories_start.split(",")
|
||||
|
||||
# if multi-goal-mode, add agent_selection as a goal (defaults to True)
|
||||
if "agent_selection" not in goal_categories:
|
||||
first_goal_value = os.getenv("AGENT_GOAL")
|
||||
if (
|
||||
first_goal_value is None
|
||||
or first_goal_value.lower() == "goal_choose_agent_type"
|
||||
):
|
||||
if "agent_selection" not in goal_categories :
|
||||
first_goal_value = os.getenv("AGENT_GOAL")
|
||||
if first_goal_value is None or first_goal_value.lower() == "goal_choose_agent_type":
|
||||
goal_categories.append("agent_selection")
|
||||
|
||||
# always show goals labeled as "system," like the goal chooser
|
||||
@@ -37,7 +33,7 @@ def list_agents(args: dict) -> dict:
|
||||
"goal_id": goal.id,
|
||||
"agent_description": goal.agent_friendly_description,
|
||||
}
|
||||
)
|
||||
)
|
||||
return {
|
||||
"agents": agents,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import os
|
||||
import random
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
import requests
|
||||
import random
|
||||
from datetime import datetime, timedelta, date
|
||||
from dotenv import load_dotenv
|
||||
|
||||
PREMIER_LEAGUE_CLUBS_DATA = [
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import urllib.parse
|
||||
|
||||
import json
|
||||
import http.client
|
||||
from dotenv import load_dotenv
|
||||
import urllib.parse
|
||||
|
||||
|
||||
def search_airport(query: str) -> list:
|
||||
@@ -175,166 +173,45 @@ def search_flights_real_api(
|
||||
}
|
||||
|
||||
|
||||
def generate_smart_flights(origin: str, destination: str) -> list:
|
||||
"""
|
||||
Generate realistic flight options with smart pricing based on origin and destination.
|
||||
"""
|
||||
# Common airlines for different regions
|
||||
airlines_by_region = {
|
||||
"domestic_us": [
|
||||
{"name": "American Airlines", "code": "AA"},
|
||||
{"name": "United Airlines", "code": "UA"},
|
||||
{"name": "Delta Airlines", "code": "DL"},
|
||||
{"name": "Southwest Airlines", "code": "WN"},
|
||||
],
|
||||
"us_international": [
|
||||
{"name": "American Airlines", "code": "AA"},
|
||||
{"name": "United Airlines", "code": "UA"},
|
||||
{"name": "Delta Airlines", "code": "DL"},
|
||||
{"name": "Virgin Atlantic", "code": "VS"},
|
||||
],
|
||||
"australia_nz": [
|
||||
{"name": "Qantas", "code": "QF"},
|
||||
{"name": "Jetstar", "code": "JQ"},
|
||||
{"name": "Virgin Australia", "code": "VA"},
|
||||
{"name": "Air New Zealand", "code": "NZ"},
|
||||
],
|
||||
"international": [
|
||||
{"name": "American Airlines", "code": "AA"},
|
||||
{"name": "United Airlines", "code": "UA"},
|
||||
{"name": "Delta Airlines", "code": "DL"},
|
||||
{"name": "Air New Zealand", "code": "NZ"},
|
||||
{"name": "Qantas", "code": "QF"},
|
||||
{"name": "Singapore Airlines", "code": "SQ"},
|
||||
],
|
||||
}
|
||||
|
||||
# Determine route type and base pricing
|
||||
origin_lower = origin.lower()
|
||||
dest_lower = destination.lower()
|
||||
|
||||
# Australia/NZ cities
|
||||
anz_cities = [
|
||||
"sydney",
|
||||
"melbourne",
|
||||
"syd",
|
||||
"mel",
|
||||
"auckland",
|
||||
"akl",
|
||||
"wellington",
|
||||
"wlg",
|
||||
"brisbane",
|
||||
"bne",
|
||||
"perth",
|
||||
"per",
|
||||
]
|
||||
# US cities
|
||||
us_cities = [
|
||||
"los angeles",
|
||||
"lax",
|
||||
"san francisco",
|
||||
"sfo",
|
||||
"new york",
|
||||
"nyc",
|
||||
"jfk",
|
||||
"chicago",
|
||||
"ord",
|
||||
"miami",
|
||||
"mia",
|
||||
]
|
||||
|
||||
is_origin_anz = any(city in origin_lower for city in anz_cities)
|
||||
is_dest_anz = any(city in dest_lower for city in anz_cities)
|
||||
is_origin_us = any(city in origin_lower for city in us_cities)
|
||||
is_dest_us = any(city in dest_lower for city in us_cities)
|
||||
|
||||
# Determine airline pool and base price
|
||||
if (is_origin_us and is_dest_anz) or (is_origin_anz and is_dest_us):
|
||||
# Trans-Pacific routes
|
||||
airline_pool = airlines_by_region["international"]
|
||||
base_price = random.randint(1200, 1800)
|
||||
elif is_origin_anz and is_dest_anz:
|
||||
# Australia/NZ domestic
|
||||
airline_pool = airlines_by_region["australia_nz"]
|
||||
base_price = random.randint(300, 600)
|
||||
elif is_origin_us and is_dest_us:
|
||||
# US domestic
|
||||
airline_pool = airlines_by_region["domestic_us"]
|
||||
base_price = random.randint(200, 800)
|
||||
else:
|
||||
# General international
|
||||
airline_pool = airlines_by_region["international"]
|
||||
base_price = random.randint(800, 1500)
|
||||
|
||||
# Generate 3-4 flight options
|
||||
num_flights = random.randint(3, 4)
|
||||
results = []
|
||||
used_airlines = set()
|
||||
|
||||
for i in range(num_flights):
|
||||
# Pick unique airline
|
||||
available_airlines = [a for a in airline_pool if a["name"] not in used_airlines]
|
||||
if not available_airlines:
|
||||
available_airlines = airline_pool # Reset if we run out
|
||||
|
||||
airline = random.choice(available_airlines)
|
||||
used_airlines.add(airline["name"])
|
||||
|
||||
# Generate flight numbers
|
||||
outbound_num = random.randint(100, 999)
|
||||
return_num = random.randint(100, 999)
|
||||
|
||||
# Price variation (cheaper airlines get lower prices)
|
||||
price_multiplier = 1.0
|
||||
if "Southwest" in airline["name"] or "Jetstar" in airline["name"]:
|
||||
price_multiplier = 0.7
|
||||
elif "Virgin" in airline["name"]:
|
||||
price_multiplier = 0.85
|
||||
elif "Singapore" in airline["name"]:
|
||||
price_multiplier = 1.2
|
||||
|
||||
# Add some random variation
|
||||
price_variation = random.uniform(0.9, 1.1)
|
||||
final_price = round(base_price * price_multiplier * price_variation, 2)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"operating_carrier": airline["name"],
|
||||
"outbound_flight_code": f"{airline['code']}{outbound_num}",
|
||||
"price": final_price,
|
||||
"return_flight_code": f"{airline['code']}{return_num}",
|
||||
"return_operating_carrier": airline["name"],
|
||||
}
|
||||
)
|
||||
|
||||
# Sort by price
|
||||
results.sort(key=lambda x: x["price"])
|
||||
return results
|
||||
|
||||
|
||||
def search_flights(args: dict) -> dict:
|
||||
"""
|
||||
Search for flights. Uses real API if RAPIDAPI_KEY is available, otherwise generates smart mock data.
|
||||
Returns example flight search results in the requested JSON format.
|
||||
"""
|
||||
load_dotenv(override=True)
|
||||
api_key = os.getenv("RAPIDAPI_KEY")
|
||||
|
||||
origin = args.get("origin")
|
||||
destination = args.get("destination")
|
||||
|
||||
if not origin or not destination:
|
||||
return {"error": "Both origin and destination are required"}
|
||||
|
||||
# If API key is available, use the real API
|
||||
if api_key and api_key != "YOUR_DEFAULT_KEY":
|
||||
return search_flights_real_api(args)
|
||||
|
||||
# Otherwise, generate smart mock data
|
||||
results = generate_smart_flights(origin, destination)
|
||||
|
||||
return {
|
||||
"currency": "USD",
|
||||
"destination": destination,
|
||||
"origin": origin,
|
||||
"results": results,
|
||||
"destination": f"{destination}",
|
||||
"origin": f"{origin}",
|
||||
"results": [
|
||||
{
|
||||
"operating_carrier": "American Airlines",
|
||||
"outbound_flight_code": "AA203",
|
||||
"price": 1262.51,
|
||||
"return_flight_code": "AA202",
|
||||
"return_operating_carrier": "American Airlines",
|
||||
},
|
||||
{
|
||||
"operating_carrier": "Air New Zealand",
|
||||
"outbound_flight_code": "NZ488",
|
||||
"price": 1396.00,
|
||||
"return_flight_code": "NZ527",
|
||||
"return_operating_carrier": "Air New Zealand",
|
||||
},
|
||||
{
|
||||
"operating_carrier": "United Airlines",
|
||||
"outbound_flight_code": "UA100",
|
||||
"price": 1500.00,
|
||||
"return_flight_code": "UA101",
|
||||
"return_operating_carrier": "United Airlines",
|
||||
},
|
||||
{
|
||||
"operating_carrier": "Delta Airlines",
|
||||
"outbound_flight_code": "DL200",
|
||||
"price": 1600.00,
|
||||
"return_flight_code": "DL201",
|
||||
"return_operating_carrier": "Delta Airlines",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from models.tool_definitions import ToolArgument, ToolDefinition
|
||||
from models.tool_definitions import ToolDefinition, ToolArgument
|
||||
|
||||
# ----- System tools -----
|
||||
list_agents_tool = ToolDefinition(
|
||||
@@ -399,75 +397,3 @@ ecomm_track_package = ToolDefinition(
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# ----- Food Ordering Use Case Tools -----
|
||||
food_add_to_cart_tool = ToolDefinition(
|
||||
name="AddToCart",
|
||||
description="Add a menu item to the customer's cart using item details from Stripe.",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="customer_email",
|
||||
type="string",
|
||||
description="Email address of the customer",
|
||||
),
|
||||
ToolArgument(
|
||||
name="item_name",
|
||||
type="string",
|
||||
description="Name of the menu item (e.g., 'Margherita Pizza', 'Caesar Salad')",
|
||||
),
|
||||
ToolArgument(
|
||||
name="item_price",
|
||||
type="number",
|
||||
description="Price of the item in dollars (e.g., 14.99)",
|
||||
),
|
||||
ToolArgument(
|
||||
name="quantity",
|
||||
type="number",
|
||||
description="Quantity of the item to add (defaults to 1)",
|
||||
),
|
||||
ToolArgument(
|
||||
name="stripe_product_id",
|
||||
type="string",
|
||||
description="Stripe product ID for reference (optional)",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# MCP Integration Functions
|
||||
|
||||
|
||||
def create_mcp_tool_definitions(
|
||||
mcp_tools_info: Dict[str, Dict],
|
||||
) -> List[ToolDefinition]:
|
||||
"""Convert MCP tool info to ToolDefinition objects"""
|
||||
tool_definitions = []
|
||||
|
||||
for tool_name, tool_info in mcp_tools_info.items():
|
||||
# Extract input schema properties
|
||||
input_schema = tool_info.get("inputSchema", {})
|
||||
properties = (
|
||||
input_schema.get("properties", {}) if isinstance(input_schema, dict) else {}
|
||||
)
|
||||
|
||||
# Convert properties to ToolArgument objects
|
||||
arguments = []
|
||||
for param_name, param_info in properties.items():
|
||||
if isinstance(param_info, dict):
|
||||
arguments.append(
|
||||
ToolArgument(
|
||||
name=param_name,
|
||||
type=param_info.get("type", "string"),
|
||||
description=param_info.get("description", ""),
|
||||
)
|
||||
)
|
||||
|
||||
# Create ToolDefinition
|
||||
tool_def = ToolDefinition(
|
||||
name=tool_info["name"],
|
||||
description=tool_info.get("description", ""),
|
||||
arguments=arguments,
|
||||
)
|
||||
tool_definitions.append(tool_def)
|
||||
|
||||
return tool_definitions
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import shared.config
|
||||
|
||||
|
||||
def transfer_control(args: dict) -> dict:
|
||||
|
||||
return {
|
||||
"new_goal": shared.config.AGENT_GOAL,
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,31 @@
|
||||
from collections import deque
|
||||
from datetime import timedelta
|
||||
from typing import Any, Deque, Dict, List, Optional, TypedDict, Union
|
||||
from typing import Dict, Any, Union, List, Optional, Deque, TypedDict
|
||||
|
||||
from temporalio import workflow
|
||||
from temporalio.common import RetryPolicy
|
||||
from temporalio import workflow
|
||||
|
||||
from models.data_types import (
|
||||
ConversationHistory,
|
||||
EnvLookupInput,
|
||||
EnvLookupOutput,
|
||||
NextStep,
|
||||
ValidationInput,
|
||||
)
|
||||
from models.data_types import ConversationHistory, EnvLookupOutput, 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
|
||||
from workflows import workflow_helpers as helpers
|
||||
from workflows.workflow_helpers import (
|
||||
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||
LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||
)
|
||||
|
||||
with workflow.unsafe.imports_passed_through():
|
||||
from activities.tool_activities import ToolActivities, mcp_list_tools
|
||||
from goals import goal_list
|
||||
from models.data_types import CombinedInput, ToolPromptInput
|
||||
from prompts.agent_prompt_generators import generate_genai_prompt
|
||||
from tools.tool_registry import create_mcp_tool_definitions
|
||||
from activities.tool_activities import ToolActivities
|
||||
from prompts.agent_prompt_generators import (
|
||||
generate_genai_prompt
|
||||
)
|
||||
from models.data_types import (
|
||||
CombinedInput,
|
||||
ToolPromptInput,
|
||||
)
|
||||
from tools.goal_registry import goal_list
|
||||
|
||||
# Constants
|
||||
MAX_TURNS_BEFORE_CONTINUE = 250
|
||||
|
||||
|
||||
# ToolData as part of the workflow is what's accessible to the UI - see LLMResponse.jsx for example
|
||||
#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
|
||||
tool: str
|
||||
@@ -38,7 +33,6 @@ class ToolData(TypedDict, total=False):
|
||||
response: str
|
||||
force_confirm: bool = True
|
||||
|
||||
|
||||
@workflow.defn
|
||||
class AgentGoalWorkflow:
|
||||
"""Workflow that manages tool execution with user confirmation and conversation history."""
|
||||
@@ -49,22 +43,16 @@ class AgentGoalWorkflow:
|
||||
self.conversation_summary: Optional[str] = None
|
||||
self.chat_ended: bool = False
|
||||
self.tool_data: Optional[ToolData] = None
|
||||
self.confirmed: bool = (
|
||||
False # indicates that we have confirmation to proceed to run tool
|
||||
)
|
||||
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 # set from env file in activity lookup_wf_env_settings
|
||||
)
|
||||
self.multi_goal_mode: bool = (
|
||||
False # set from env file in activity lookup_wf_env_settings
|
||||
)
|
||||
self.mcp_tools_info: Optional[dict] = None # stores complete MCP tools result
|
||||
self.show_tool_args_confirmation: bool = True # set from env file in activity lookup_wf_env_settings
|
||||
self.multi_goal_mode: bool = False # set from env file in activity lookup_wf_env_settings
|
||||
|
||||
# see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set
|
||||
@workflow.run
|
||||
async def run(self, combined_input: CombinedInput) -> str:
|
||||
|
||||
"""Main workflow execution method."""
|
||||
# setup phase, starts with blank tool_params and agent_goal prompt as defined in tools/goal_registry.py
|
||||
params = combined_input.tool_params
|
||||
@@ -72,10 +60,6 @@ class AgentGoalWorkflow:
|
||||
|
||||
await self.lookup_wf_env_settings(combined_input)
|
||||
|
||||
# If the goal has an MCP server definition, dynamically load MCP tools
|
||||
if self.goal.mcp_server_definition:
|
||||
await self.load_mcp_tools()
|
||||
|
||||
# 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)
|
||||
@@ -84,12 +68,12 @@ class AgentGoalWorkflow:
|
||||
if params and params.prompt_queue:
|
||||
self.prompt_queue.extend(params.prompt_queue)
|
||||
|
||||
waiting_for_confirm = False
|
||||
waiting_for_confirm = False
|
||||
current_tool = None
|
||||
|
||||
# This is the main interactive loop. Main responsibilities:
|
||||
# - Selecting and changing goals as directed by the user
|
||||
# - reacting to user input (from signals)
|
||||
# - reacting to user input (from signals)
|
||||
# - validating user input to make sure it makes sense with the current goal and tools
|
||||
# - calling the LLM through activities to determine next steps and prompts
|
||||
# - executing the selected tools via activities
|
||||
@@ -103,7 +87,7 @@ class AgentGoalWorkflow:
|
||||
if self.chat_should_end():
|
||||
return f"{self.conversation_history}"
|
||||
|
||||
# Execute the tool
|
||||
# Execute the tool
|
||||
if self.ready_for_tool_execution(waiting_for_confirm, current_tool):
|
||||
waiting_for_confirm = await self.execute_tool(current_tool)
|
||||
continue
|
||||
@@ -112,12 +96,10 @@ class AgentGoalWorkflow:
|
||||
if self.prompt_queue:
|
||||
# get most recent prompt
|
||||
prompt = self.prompt_queue.popleft()
|
||||
workflow.logger.info(
|
||||
f"workflow step: processing message on the prompt queue, message is {prompt}"
|
||||
)
|
||||
|
||||
workflow.logger.info(f"workflow step: processing message on the prompt queue, message is {prompt}")
|
||||
|
||||
# Validate user-provided prompts
|
||||
if self.is_user_prompt(prompt):
|
||||
if self.is_user_prompt(prompt):
|
||||
self.add_message("user", prompt)
|
||||
|
||||
# Validate the prompt before proceeding
|
||||
@@ -138,26 +120,18 @@ class AgentGoalWorkflow:
|
||||
|
||||
# If validation fails, provide that feedback to the user - i.e., "your words make no sense, puny human" end this iteration of processing
|
||||
if not validation_result.validationResult:
|
||||
workflow.logger.warning(
|
||||
f"Prompt validation failed: {validation_result.validationFailedReason}"
|
||||
)
|
||||
self.add_message(
|
||||
"agent", validation_result.validationFailedReason
|
||||
)
|
||||
workflow.logger.warning(f"Prompt validation failed: {validation_result.validationFailedReason}")
|
||||
self.add_message("agent", validation_result.validationFailedReason)
|
||||
continue
|
||||
|
||||
# If valid, proceed with generating the context and prompt
|
||||
context_instructions = generate_genai_prompt(
|
||||
agent_goal=self.goal,
|
||||
conversation_history=self.conversation_history,
|
||||
multi_goal_mode=self.multi_goal_mode,
|
||||
raw_json=self.tool_data,
|
||||
mcp_tools_info=self.mcp_tools_info,
|
||||
)
|
||||
|
||||
prompt_input = ToolPromptInput(
|
||||
prompt=prompt, context_instructions=context_instructions
|
||||
)
|
||||
agent_goal=self.goal,
|
||||
conversation_history = self.conversation_history,
|
||||
multi_goal_mode=self.multi_goal_mode,
|
||||
raw_json=self.tool_data)
|
||||
|
||||
prompt_input = ToolPromptInput(prompt=prompt, context_instructions=context_instructions)
|
||||
|
||||
# connect to LLM and execute to get next steps
|
||||
tool_data = await workflow.execute_activity_method(
|
||||
@@ -177,24 +151,20 @@ class AgentGoalWorkflow:
|
||||
next_step = tool_data.get("next")
|
||||
current_tool = tool_data.get("tool")
|
||||
|
||||
workflow.logger.info(
|
||||
f"next_step: {next_step}, current tool is {current_tool}"
|
||||
)
|
||||
workflow.logger.info(f"next_step: {next_step}, current tool is {current_tool}")
|
||||
|
||||
# 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, ask for them
|
||||
if await helpers.handle_missing_args(
|
||||
current_tool, args, tool_data, self.prompt_queue
|
||||
):
|
||||
# if we're missing arguments, ask for them
|
||||
if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue):
|
||||
continue
|
||||
|
||||
waiting_for_confirm = True
|
||||
|
||||
# We have needed arguments, if we want to force the user to confirm, set that up
|
||||
# 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
|
||||
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:
|
||||
@@ -204,11 +174,14 @@ class AgentGoalWorkflow:
|
||||
workflow.logger.info("All steps completed. Resetting goal.")
|
||||
self.change_goal("goal_choose_agent_type")
|
||||
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
# here we could send conversation to AI for analysis
|
||||
#here we could send conversation to AI for analysis
|
||||
|
||||
# end the workflow
|
||||
return str(self.conversation_history)
|
||||
@@ -219,10 +192,10 @@ class AgentGoalWorkflow:
|
||||
self.prompt_queue,
|
||||
self.goal,
|
||||
MAX_TURNS_BEFORE_CONTINUE,
|
||||
self.add_message,
|
||||
self.add_message
|
||||
)
|
||||
|
||||
# Signal that comes from api/main.py via a post to /send-prompt
|
||||
#Signal that comes from api/main.py via a post to /send-prompt
|
||||
@workflow.signal
|
||||
async def user_prompt(self, prompt: str) -> None:
|
||||
"""Signal handler for receiving user prompts."""
|
||||
@@ -232,28 +205,28 @@ class AgentGoalWorkflow:
|
||||
return
|
||||
self.prompt_queue.append(prompt)
|
||||
|
||||
# Signal that comes from api/main.py via a post to /confirm
|
||||
#Signal that comes from api/main.py via a post to /confirm
|
||||
@workflow.signal
|
||||
async def confirm(self) -> None:
|
||||
"""Signal handler for user confirmation of tool execution."""
|
||||
workflow.logger.info("Received user signal: confirmation")
|
||||
self.confirmed = True
|
||||
|
||||
# Signal that comes from api/main.py via a post to /end-chat
|
||||
#Signal that comes from api/main.py via a post to /end-chat
|
||||
@workflow.signal
|
||||
async def end_chat(self) -> None:
|
||||
"""Signal handler for ending the chat session."""
|
||||
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
|
||||
#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
|
||||
#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."""
|
||||
@@ -264,7 +237,7 @@ class AgentGoalWorkflow:
|
||||
def get_conversation_history(self) -> ConversationHistory:
|
||||
"""Query handler to retrieve the full conversation history."""
|
||||
return self.conversation_history
|
||||
|
||||
|
||||
@workflow.query
|
||||
def get_agent_goal(self) -> AgentGoal:
|
||||
"""Query handler to retrieve the current goal of the agent."""
|
||||
@@ -272,7 +245,7 @@ class AgentGoalWorkflow:
|
||||
|
||||
@workflow.query
|
||||
def get_summary_from_history(self) -> Optional[str]:
|
||||
"""Query handler to retrieve the conversation summary if available.
|
||||
"""Query handler to retrieve the conversation summary if available.
|
||||
Used only for continue as new of the workflow."""
|
||||
return self.conversation_summary
|
||||
|
||||
@@ -299,9 +272,9 @@ class AgentGoalWorkflow:
|
||||
)
|
||||
|
||||
def change_goal(self, goal: str) -> None:
|
||||
"""Change the goal (usually on request of the user).
|
||||
|
||||
Args:
|
||||
""" Change the goal (usually on request of the user).
|
||||
|
||||
Args:
|
||||
goal: goal to change to)
|
||||
"""
|
||||
if goal is not None:
|
||||
@@ -310,9 +283,8 @@ class AgentGoalWorkflow:
|
||||
self.goal = listed_goal
|
||||
workflow.logger.info("Changed goal to " + goal)
|
||||
if goal is None:
|
||||
workflow.logger.warning(
|
||||
"Goal not set after goal reset, probably bad."
|
||||
) # if this happens, there's probably a problem with the goal list
|
||||
workflow.logger.warning("Goal not set after goal reset, probably bad.") # if this happens, there's probably a problem with the goal list
|
||||
|
||||
|
||||
# workflow function that defines if chat should end
|
||||
def chat_should_end(self) -> bool:
|
||||
@@ -321,11 +293,9 @@ class AgentGoalWorkflow:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
# define if we're ready for tool execution
|
||||
def ready_for_tool_execution(
|
||||
self, waiting_for_confirm: bool, current_tool: Any
|
||||
) -> bool:
|
||||
def ready_for_tool_execution(self, waiting_for_confirm: bool, current_tool: Any) -> bool:
|
||||
if self.confirmed and waiting_for_confirm and current_tool and self.tool_data:
|
||||
return True
|
||||
else:
|
||||
@@ -334,19 +304,19 @@ class AgentGoalWorkflow:
|
||||
# LLM-tagged prompts start with "###"
|
||||
# all others are from the user
|
||||
def is_user_prompt(self, prompt) -> bool:
|
||||
if prompt.startswith("###"):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
if prompt.startswith("###"):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
# look up env settings in an activity so they're part of history
|
||||
async def lookup_wf_env_settings(self, combined_input: CombinedInput) -> None:
|
||||
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,
|
||||
show_confirm_env_var_name = "SHOW_CONFIRM",
|
||||
show_confirm_default = True,
|
||||
)
|
||||
env_output: EnvLookupOutput = await workflow.execute_activity_method(
|
||||
ToolActivities.get_wf_env_vars,
|
||||
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,
|
||||
retry_policy=RetryPolicy(
|
||||
@@ -355,13 +325,11 @@ class AgentGoalWorkflow:
|
||||
)
|
||||
self.show_tool_args_confirmation = env_output.show_confirm
|
||||
self.multi_goal_mode = env_output.multi_goal_mode
|
||||
|
||||
|
||||
# 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}"
|
||||
)
|
||||
#
|
||||
async def execute_tool(self, current_tool: str)->bool:
|
||||
workflow.logger.info(f"workflow step: user has confirmed, executing the tool {current_tool}")
|
||||
self.confirmed = False
|
||||
waiting_for_confirm = False
|
||||
confirmed_tool_data = self.tool_data.copy()
|
||||
@@ -374,28 +342,21 @@ class AgentGoalWorkflow:
|
||||
self.tool_data,
|
||||
self.tool_results,
|
||||
self.add_message,
|
||||
self.prompt_queue,
|
||||
self.goal,
|
||||
self.prompt_queue
|
||||
)
|
||||
|
||||
# set new goal if we should
|
||||
if len(self.tool_results) > 0:
|
||||
if (
|
||||
"ChangeGoal" in self.tool_results[-1].values()
|
||||
and "new_goal" in self.tool_results[-1].keys()
|
||||
):
|
||||
if "ChangeGoal" in self.tool_results[-1].values() and "new_goal" in self.tool_results[-1].keys():
|
||||
new_goal = self.tool_results[-1].get("new_goal")
|
||||
self.change_goal(new_goal)
|
||||
elif (
|
||||
"ListAgents" in self.tool_results[-1].values()
|
||||
and self.goal.id != "goal_choose_agent_type"
|
||||
):
|
||||
elif "ListAgents" in self.tool_results[-1].values() and self.goal.id != "goal_choose_agent_type":
|
||||
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:
|
||||
def print_useful_workflow_vars(self, status_or_step:str) -> None:
|
||||
print(f"***{status_or_step}:***")
|
||||
if self.goal:
|
||||
print(f"current goal: {self.goal.id}")
|
||||
@@ -406,43 +367,4 @@ class AgentGoalWorkflow:
|
||||
else:
|
||||
print("no tool data initialized yet")
|
||||
print(f"self.confirmed: {self.confirmed}")
|
||||
|
||||
async def load_mcp_tools(self) -> None:
|
||||
"""Load MCP tools dynamically from the server definition"""
|
||||
if not self.goal.mcp_server_definition:
|
||||
return
|
||||
|
||||
workflow.logger.info(
|
||||
f"Loading MCP tools from server: {self.goal.mcp_server_definition.name}"
|
||||
)
|
||||
|
||||
# Get the list of tools to include (if specified)
|
||||
include_tools = self.goal.mcp_server_definition.included_tools
|
||||
|
||||
# Call the MCP list tools activity
|
||||
mcp_tools_result = await workflow.execute_activity(
|
||||
mcp_list_tools,
|
||||
args=[self.goal.mcp_server_definition, include_tools],
|
||||
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||
),
|
||||
summary=f"{self.goal.mcp_server_definition.name}",
|
||||
)
|
||||
|
||||
if mcp_tools_result.get("success", False):
|
||||
tools_info = mcp_tools_result.get("tools", {})
|
||||
workflow.logger.info(f"Successfully loaded {len(tools_info)} MCP tools")
|
||||
|
||||
# Store complete MCP tools result for use in prompt generation
|
||||
self.mcp_tools_info = mcp_tools_result
|
||||
|
||||
# Convert MCP tools to ToolDefinition objects and add to goal
|
||||
mcp_tool_definitions = create_mcp_tool_definitions(tools_info)
|
||||
self.goal.tools.extend(mcp_tool_definitions)
|
||||
|
||||
workflow.logger.info(f"Added {len(mcp_tool_definitions)} MCP tools to goal")
|
||||
else:
|
||||
error_msg = mcp_tools_result.get("error", "Unknown error")
|
||||
workflow.logger.error(f"Failed to load MCP tools: {error_msg}")
|
||||
# Continue execution without MCP tools
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from datetime import timedelta
|
||||
from typing import Any, Deque, Dict
|
||||
|
||||
from typing import Dict, Any, Deque
|
||||
from temporalio import workflow
|
||||
from temporalio.common import RetryPolicy
|
||||
from temporalio.exceptions import ActivityError
|
||||
from temporalio.common import RetryPolicy
|
||||
|
||||
from models.data_types import ConversationHistory, ToolPromptInput
|
||||
from models.tool_definitions import AgentGoal
|
||||
from prompts.agent_prompt_generators import (
|
||||
generate_missing_args_prompt,
|
||||
generate_tool_completion_prompt,
|
||||
@@ -20,118 +18,35 @@ LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT = timedelta(seconds=20)
|
||||
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT = timedelta(minutes=30)
|
||||
|
||||
|
||||
def is_mcp_tool(tool_name: str, goal: AgentGoal) -> bool:
|
||||
"""Check if a tool is an MCP tool based on the goal's MCP server definition"""
|
||||
if not goal.mcp_server_definition:
|
||||
return False
|
||||
|
||||
# Check if the tool name matches any MCP tools that were loaded
|
||||
# We can identify MCP tools by checking if they're not in the original static tools
|
||||
from tools.tool_registry import (
|
||||
book_pto_tool,
|
||||
book_trains_tool,
|
||||
change_goal_tool,
|
||||
create_invoice_tool,
|
||||
current_pto_tool,
|
||||
ecomm_get_order,
|
||||
ecomm_list_orders,
|
||||
ecomm_track_package,
|
||||
financial_check_account_is_valid,
|
||||
financial_get_account_balances,
|
||||
financial_move_money,
|
||||
financial_submit_loan_approval,
|
||||
find_events_tool,
|
||||
food_add_to_cart_tool,
|
||||
future_pto_calc_tool,
|
||||
give_hint_tool,
|
||||
guess_location_tool,
|
||||
list_agents_tool,
|
||||
paycheck_bank_integration_status_check,
|
||||
search_fixtures_tool,
|
||||
search_flights_tool,
|
||||
search_trains_tool,
|
||||
)
|
||||
|
||||
static_tool_names = {
|
||||
list_agents_tool.name,
|
||||
change_goal_tool.name,
|
||||
give_hint_tool.name,
|
||||
guess_location_tool.name,
|
||||
search_flights_tool.name,
|
||||
search_trains_tool.name,
|
||||
book_trains_tool.name,
|
||||
create_invoice_tool.name,
|
||||
search_fixtures_tool.name,
|
||||
find_events_tool.name,
|
||||
current_pto_tool.name,
|
||||
future_pto_calc_tool.name,
|
||||
book_pto_tool.name,
|
||||
paycheck_bank_integration_status_check.name,
|
||||
financial_check_account_is_valid.name,
|
||||
financial_get_account_balances.name,
|
||||
financial_move_money.name,
|
||||
financial_submit_loan_approval.name,
|
||||
ecomm_list_orders.name,
|
||||
ecomm_get_order.name,
|
||||
ecomm_track_package.name,
|
||||
food_add_to_cart_tool.name,
|
||||
}
|
||||
|
||||
return tool_name not in static_tool_names
|
||||
|
||||
|
||||
async def handle_tool_execution(
|
||||
current_tool: str,
|
||||
tool_data: Dict[str, Any],
|
||||
tool_results: list,
|
||||
add_message_callback: callable,
|
||||
prompt_queue: Deque[str],
|
||||
goal: AgentGoal = None,
|
||||
) -> None:
|
||||
"""Execute a tool after confirmation and handle its result."""
|
||||
workflow.logger.info(f"Confirmed. Proceeding with tool: {current_tool}")
|
||||
|
||||
task_queue = (
|
||||
TEMPORAL_LEGACY_TASK_QUEUE
|
||||
if current_tool in ["SearchTrains", "BookTrains"]
|
||||
else None
|
||||
)
|
||||
|
||||
try:
|
||||
# Check if this is an MCP tool
|
||||
if goal and is_mcp_tool(current_tool, goal):
|
||||
workflow.logger.info(f"Executing MCP tool: {current_tool}")
|
||||
|
||||
# Add server definition to args for MCP tools
|
||||
mcp_args = tool_data["args"].copy()
|
||||
mcp_args["server_definition"] = goal.mcp_server_definition
|
||||
|
||||
dynamic_result = await workflow.execute_activity(
|
||||
current_tool,
|
||||
mcp_args,
|
||||
schedule_to_close_timeout=TOOL_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||
start_to_close_timeout=TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||
),
|
||||
summary=f"{goal.mcp_server_definition.name} (MCP Tool)",
|
||||
)
|
||||
else:
|
||||
# Handle regular tools
|
||||
task_queue = (
|
||||
TEMPORAL_LEGACY_TASK_QUEUE
|
||||
if current_tool in ["SearchTrains", "BookTrains"]
|
||||
else None
|
||||
)
|
||||
|
||||
dynamic_result = await workflow.execute_activity(
|
||||
current_tool,
|
||||
tool_data["args"],
|
||||
task_queue=task_queue,
|
||||
schedule_to_close_timeout=TOOL_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||
start_to_close_timeout=TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||
),
|
||||
)
|
||||
|
||||
dynamic_result = await workflow.execute_activity(
|
||||
current_tool,
|
||||
tool_data["args"],
|
||||
task_queue=task_queue,
|
||||
schedule_to_close_timeout=TOOL_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||
start_to_close_timeout=TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||
),
|
||||
)
|
||||
dynamic_result["tool"] = current_tool
|
||||
tool_results.append(dynamic_result)
|
||||
|
||||
except ActivityError as e:
|
||||
workflow.logger.error(f"Tool execution failed: {str(e)}")
|
||||
dynamic_result = {"error": str(e), "tool": current_tool}
|
||||
|
||||