mirror of
https://github.com/temporal-community/temporal-ai-agent.git
synced 2026-03-15 14:08:08 +01:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3180084e7c | ||
|
|
1bd69db1d0 | ||
|
|
98a1b75dff | ||
|
|
e248a6778d | ||
|
|
d0dca40b93 | ||
|
|
457fa1fce8 | ||
|
|
3c71a062fd | ||
|
|
380009f292 | ||
|
|
68ac9c40eb | ||
|
|
4ed4efbe83 | ||
|
|
40714071d6 | ||
|
|
c2785b79f4 | ||
|
|
861e55a8d0 | ||
|
|
49dd00ec3b |
20
.devcontainer/devcontainer.json
Normal file
20
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"name": "Temporal AI Agentic Demo",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/va-h/devcontainers-features/uv:1": {},
|
||||
"ghcr.io/devcontainers/features/python:1": {},
|
||||
"ghcr.io/devcontainers-extra/features/temporal-cli:1": {},
|
||||
"ghcr.io/mrsimonemms/devcontainers/tcld:1": {}
|
||||
},
|
||||
"forwardPorts": [
|
||||
5173,
|
||||
7233,
|
||||
8000,
|
||||
8233
|
||||
],
|
||||
"containerEnv": {
|
||||
"VITE_HOST": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
37
.github/workflows/ci.yml
vendored
Normal file
37
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
lint-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.13']
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync
|
||||
|
||||
- name: Format, lint, type check
|
||||
run: |
|
||||
uv run poe format
|
||||
uv run poe lint
|
||||
uv run poe lint-types
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
uv run pytest
|
||||
41
AGENTS.md
41
AGENTS.md
@@ -34,11 +34,10 @@ Default URLs:
|
||||
|
||||
1. **Prerequisites:**
|
||||
```bash
|
||||
# Install Poetry for Python dependency management
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
# Start Temporal server (Mac)
|
||||
# Install uv and Temporal server (MacOS)
|
||||
brew install uv
|
||||
brew install temporal
|
||||
|
||||
temporal server start-dev
|
||||
```
|
||||
|
||||
@@ -50,9 +49,9 @@ Default URLs:
|
||||
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
|
||||
uv sync
|
||||
uv run scripts/run_worker.py # In one terminal
|
||||
uv run uvicorn api.main:app --reload # In another terminal
|
||||
```
|
||||
|
||||
3. **Frontend (React):**
|
||||
@@ -102,20 +101,20 @@ The project includes comprehensive tests using Temporal's testing framework:
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
poetry install --with dev
|
||||
uv sync
|
||||
|
||||
# Run all tests
|
||||
poetry run pytest
|
||||
uv run pytest
|
||||
|
||||
# Run with time-skipping for faster execution
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
uv 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
|
||||
uv run pytest tests/test_tool_activities.py -v # Activity tests
|
||||
uv run pytest tests/test_agent_goal_workflow.py -v # Workflow tests
|
||||
|
||||
# Run with coverage
|
||||
poetry run pytest --cov=workflows --cov=activities
|
||||
uv run pytest --cov=workflows --cov=activities
|
||||
```
|
||||
|
||||
**Test Coverage:**
|
||||
@@ -130,15 +129,15 @@ poetry run pytest --cov=workflows --cov=activities
|
||||
## 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
|
||||
# Using poe tasks
|
||||
uv run poe format # Format code with black and isort
|
||||
uv run poe lint # Check code style and types
|
||||
uv run poe test # Run test suite
|
||||
|
||||
# Manual commands
|
||||
poetry run black .
|
||||
poetry run isort .
|
||||
poetry run mypy --check-untyped-defs --namespace-packages .
|
||||
uv run black .
|
||||
uv run isort .
|
||||
uv run mypy --check-untyped-defs --namespace-packages .
|
||||
```
|
||||
|
||||
## Agent Customization
|
||||
@@ -192,7 +191,7 @@ For detailed architecture information, see [architecture.md](docs/architecture.m
|
||||
- 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`
|
||||
- Ensure tests pass before submitting: `uv run pytest --workflow-environment=time-skipping`
|
||||
|
||||
## Additional Resources
|
||||
- **Setup Guide**: [setup.md](docs/setup.md) - Detailed configuration instructions
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -4,17 +4,19 @@ WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc build-essential && \
|
||||
apt-get install -y --no-install-recommends gcc build-essential curl && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first for better caching
|
||||
RUN pip install --no-cache-dir poetry
|
||||
# Install uv
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="$PATH:/root/.local/bin"
|
||||
|
||||
# Install Python dependencies without creating a virtualenv
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry install --without dev --no-interaction --no-ansi --no-root
|
||||
# Copy dependency files and README (needed for package build)
|
||||
COPY pyproject.toml uv.lock README.md ./
|
||||
|
||||
# Install dependencies and create virtual environment
|
||||
RUN uv sync --frozen
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
@@ -27,4 +29,4 @@ ENV PYTHONPATH=/app
|
||||
EXPOSE 8000
|
||||
|
||||
# Default to running only the API server; worker and train-api are separate Compose services
|
||||
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
CMD ["uv", "run", "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
27
Makefile
27
Makefile
@@ -1,35 +1,24 @@
|
||||
.PHONY: setup install run-worker run-api run-frontend run-train-api run-legacy-worker run-enterprise setup-venv check-python run-dev
|
||||
|
||||
# Setup commands
|
||||
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
|
||||
@echo "Virtual environment created. Don't forget to activate it with 'source venv/bin/activate'"
|
||||
|
||||
install:
|
||||
poetry install
|
||||
setup:
|
||||
uv sync
|
||||
cd frontend && npm install
|
||||
|
||||
# Run commands
|
||||
run-worker:
|
||||
poetry run python scripts/run_worker.py
|
||||
uv run scripts/run_worker.py
|
||||
|
||||
run-api:
|
||||
poetry run uvicorn api.main:app --reload
|
||||
uv run uvicorn api.main:app --reload
|
||||
|
||||
run-frontend:
|
||||
cd frontend && npx vite
|
||||
|
||||
run-train-api:
|
||||
poetry run python thirdparty/train_api.py
|
||||
uv run thirdparty/train_api.py
|
||||
|
||||
run-legacy-worker:
|
||||
poetry run python scripts/run_legacy_worker.py
|
||||
uv run scripts/run_legacy_worker.py
|
||||
|
||||
run-enterprise:
|
||||
cd enterprise && dotnet build && dotnet run
|
||||
@@ -50,9 +39,7 @@ run-dev:
|
||||
# Help command
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " make setup - Create virtual environment and install dependencies"
|
||||
@echo " make setup-venv - Create virtual environment only"
|
||||
@echo " make install - Install all dependencies"
|
||||
@echo " make setup - Install all dependencies"
|
||||
@echo " make run-worker - Start the Temporal worker"
|
||||
@echo " make run-api - Start the API server"
|
||||
@echo " make run-frontend - Start the frontend development server"
|
||||
|
||||
@@ -22,7 +22,7 @@ It's really helpful to [watch the demo (5 minute YouTube video)](https://www.you
|
||||
See multi-agent execution in action [here](https://www.youtube.com/watch?v=8Dc_0dC14yY).
|
||||
|
||||
## 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.
|
||||
There are a lot of AI and Agentic AI tools out there, and more on the way. But why Temporal? Temporal gives this system reliability, 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).
|
||||
|
||||
## What is "Agentic AI"?
|
||||
@@ -65,13 +65,13 @@ The project includes comprehensive tests for workflows and activities using Temp
|
||||
|
||||
```bash
|
||||
# Install dependencies including test dependencies
|
||||
poetry install --with dev
|
||||
uv sync
|
||||
|
||||
# Run all tests
|
||||
poetry run pytest
|
||||
uv run pytest
|
||||
|
||||
# Run with time-skipping for faster execution
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
uv run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
**Test Coverage:**
|
||||
|
||||
@@ -19,6 +19,7 @@ from models.data_types import (
|
||||
ValidationResult,
|
||||
)
|
||||
from models.tool_definitions import MCPServerDefinition
|
||||
from shared.mcp_client_manager import MCPClientManager
|
||||
|
||||
# Import MCP client libraries
|
||||
try:
|
||||
@@ -34,14 +35,17 @@ load_dotenv(override=True)
|
||||
|
||||
|
||||
class ToolActivities:
|
||||
def __init__(self):
|
||||
"""Initialize LLM client using LiteLLM."""
|
||||
def __init__(self, mcp_client_manager: MCPClientManager = None):
|
||||
"""Initialize LLM client using LiteLLM and optional MCP client manager"""
|
||||
self.llm_model = os.environ.get("LLM_MODEL", "openai/gpt-4")
|
||||
self.llm_key = os.environ.get("LLM_KEY")
|
||||
self.llm_base_url = os.environ.get("LLM_BASE_URL")
|
||||
self.mcp_client_manager = mcp_client_manager
|
||||
print(f"Initializing ToolActivities with LLM model: {self.llm_model}")
|
||||
if self.llm_base_url:
|
||||
print(f"Using custom base URL: {self.llm_base_url}")
|
||||
if self.mcp_client_manager:
|
||||
print("MCP client manager enabled for connection pooling")
|
||||
|
||||
@activity.defn
|
||||
async def agent_validatePrompt(
|
||||
@@ -205,13 +209,54 @@ class ToolActivities:
|
||||
async def mcp_tool_activity(
|
||||
self, tool_name: str, tool_args: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""MCP Tool"""
|
||||
"""MCP Tool - now using pooled connections"""
|
||||
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)
|
||||
if self.mcp_client_manager:
|
||||
# Use pooled connection
|
||||
return await self._execute_mcp_tool_pooled(
|
||||
tool_name, tool_args, server_definition
|
||||
)
|
||||
else:
|
||||
# Fallback to original implementation
|
||||
return await _execute_mcp_tool(tool_name, tool_args, server_definition)
|
||||
|
||||
async def _execute_mcp_tool_pooled(
|
||||
self,
|
||||
tool_name: str,
|
||||
tool_args: Dict[str, Any],
|
||||
server_definition: MCPServerDefinition | Dict[str, Any] | None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute MCP tool using pooled client connection"""
|
||||
activity.logger.info(f"Executing MCP tool with pooled connection: {tool_name}")
|
||||
|
||||
# Convert argument types for MCP tools
|
||||
converted_args = _convert_args_types(tool_args)
|
||||
|
||||
try:
|
||||
# Get pooled client
|
||||
client = await self.mcp_client_manager.get_client(server_definition)
|
||||
|
||||
# Call the tool using existing client session
|
||||
result = await client.call_tool(tool_name, arguments=converted_args)
|
||||
normalized_result = _normalize_result(result)
|
||||
|
||||
return {
|
||||
"tool": tool_name,
|
||||
"success": True,
|
||||
"content": normalized_result,
|
||||
}
|
||||
except Exception as e:
|
||||
activity.logger.error(f"MCP tool {tool_name} failed: {str(e)}")
|
||||
return {
|
||||
"tool": tool_name,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
}
|
||||
|
||||
|
||||
@activity.defn(dynamic=True)
|
||||
|
||||
@@ -65,7 +65,7 @@ async def get_tool_data():
|
||||
return {}
|
||||
|
||||
# Query the workflow
|
||||
tool_data = await handle.query("get_tool_data")
|
||||
tool_data = await handle.query("get_latest_tool_data")
|
||||
return tool_data
|
||||
except TemporalError as e:
|
||||
# Workflow not found; return an empty response
|
||||
|
||||
@@ -2,17 +2,17 @@ services:
|
||||
api:
|
||||
volumes:
|
||||
- ./:/app:cached
|
||||
command: uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
command: uv run uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
worker:
|
||||
volumes:
|
||||
- ./:/app:cached
|
||||
command: python scripts/run_worker.py
|
||||
command: uv run scripts/run_worker.py
|
||||
|
||||
train-api:
|
||||
volumes:
|
||||
- ./:/app:cached
|
||||
command: python thirdparty/train_api.py
|
||||
command: uv run thirdparty/train_api.py
|
||||
|
||||
frontend:
|
||||
volumes:
|
||||
|
||||
@@ -79,7 +79,7 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- TEMPORAL_ADDRESS=temporal:7233
|
||||
command: python scripts/run_worker.py
|
||||
command: uv run scripts/run_worker.py
|
||||
networks:
|
||||
- temporal-network
|
||||
|
||||
@@ -94,7 +94,7 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- TEMPORAL_ADDRESS=temporal:7233
|
||||
command: python thirdparty/train_api.py
|
||||
command: uv run thirdparty/train_api.py
|
||||
networks:
|
||||
- temporal-network
|
||||
|
||||
|
||||
@@ -8,40 +8,40 @@ This document provides guidelines for contributing to `temporal-ai-agent`. All s
|
||||
We use `black` for code formatting and `isort` for import sorting to maintain a consistent codebase.
|
||||
- **Format code:**
|
||||
```bash
|
||||
poetry run poe format
|
||||
uv run poe format
|
||||
```
|
||||
Or manually:
|
||||
```bash
|
||||
poetry run black .
|
||||
poetry run isort .
|
||||
Or manually
|
||||
```
|
||||
uv run black .
|
||||
uv 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`.
|
||||
We use `mypy` for static type checking and other linters configured via `poe`.
|
||||
- **Run linters and type checks:**
|
||||
```bash
|
||||
poetry run poe lint
|
||||
uv run poe lint
|
||||
```
|
||||
Or manually for type checking:
|
||||
```bash
|
||||
poetry run mypy --check-untyped-defs --namespace-packages .
|
||||
uv 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`):
|
||||
- **Install test dependencies:**
|
||||
```bash
|
||||
poetry install --with dev
|
||||
uv sync
|
||||
```
|
||||
- **Run all tests:**
|
||||
```bash
|
||||
poetry run pytest
|
||||
uv run pytest
|
||||
```
|
||||
- **Run tests with time-skipping (recommended for faster execution, especially in CI):**
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
uv run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
For detailed information on test categories, running specific tests, test environments, coverage, and troubleshooting, please refer to:
|
||||
@@ -73,7 +73,7 @@ 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.
|
||||
4. **Ensure tests pass:** All CI checks, including tests and linters, must pass. The command `uv 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
|
||||
|
||||
@@ -22,8 +22,6 @@ We've provided a Makefile to simplify the setup and running of the application.
|
||||
```bash
|
||||
# Initial setup
|
||||
make setup # Creates virtual environment and installs dependencies
|
||||
make setup-venv # Creates virtual environment only
|
||||
make install # Installs all dependencies
|
||||
|
||||
# Running the application
|
||||
make run-worker # Starts the Temporal worker
|
||||
@@ -159,24 +157,22 @@ Default urls:
|
||||
|
||||
**Python Backend**
|
||||
|
||||
Requires [Poetry](https://python-poetry.org/) to manage dependencies.
|
||||
Requires [`uv`](https://docs.astral.sh/uv/) to manage dependencies.
|
||||
|
||||
1. `python -m venv venv`
|
||||
1. Install uv: `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
||||
|
||||
2. `source venv/bin/activate`
|
||||
|
||||
3. `poetry install`
|
||||
2. `uv sync`
|
||||
|
||||
Run the following commands in separate terminal windows:
|
||||
|
||||
1. Start the Temporal worker:
|
||||
```bash
|
||||
poetry run python scripts/run_worker.py
|
||||
uv run scripts/run_worker.py
|
||||
```
|
||||
|
||||
2. Start the API server:
|
||||
```bash
|
||||
poetry run uvicorn api.main:app --reload
|
||||
uv run uvicorn api.main:app --reload
|
||||
```
|
||||
Access the API at `/docs` to see the available endpoints.
|
||||
|
||||
@@ -261,7 +257,7 @@ NOTE: This goal was developed for an on-stage demo and has failure (and its reso
|
||||
|
||||
Required to search and book trains!
|
||||
```bash
|
||||
poetry run python thirdparty/train_api.py
|
||||
uv run thirdparty/train_api.py
|
||||
|
||||
# example url
|
||||
# http://localhost:8080/api/search?from=london&to=liverpool&outbound_time=2025-04-18T09:00:00&inbound_time=2025-04-20T09:00:00
|
||||
@@ -273,7 +269,7 @@ poetry run python thirdparty/train_api.py
|
||||
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
|
||||
uv run 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.
|
||||
@@ -328,8 +324,8 @@ For more details, check out [adding goals and tools guide](./adding-goals-and-to
|
||||
[ ] Select an LLM and add your API key to `.env` <br />
|
||||
[ ] (Optional) set your starting goal and goal category in `.env` <br />
|
||||
[ ] (Optional) configure your Temporal Cloud settings in `.env` <br />
|
||||
[ ] `poetry run python scripts/run_worker.py` <br />
|
||||
[ ] `poetry run uvicorn api.main:app --reload` <br />
|
||||
[ ] `uv run scripts/run_worker.py` <br />
|
||||
[ ] `uv run uvicorn api.main:app --reload` <br />
|
||||
[ ] `cd frontend`, `npm install`, `npx vite` <br />
|
||||
[ ] Access the UI at `http://localhost:5173` <br />
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@ This guide provides instructions for running the comprehensive test suite for th
|
||||
|
||||
1. **Install dependencies**:
|
||||
```bash
|
||||
poetry install --with dev
|
||||
uv sync
|
||||
```
|
||||
|
||||
2. **Run all tests**:
|
||||
```bash
|
||||
poetry run pytest
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
3. **Run with time-skipping for faster execution**:
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
uv run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
## Test Categories
|
||||
@@ -39,33 +39,33 @@ This guide provides instructions for running the comprehensive test suite for th
|
||||
|
||||
```bash
|
||||
# Run only activity tests
|
||||
poetry run pytest tests/test_tool_activities.py -v
|
||||
uv run pytest tests/test_tool_activities.py -v
|
||||
|
||||
# Run only workflow tests
|
||||
poetry run pytest tests/test_agent_goal_workflow.py -v
|
||||
uv 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
|
||||
uv run pytest tests/test_tool_activities.py::TestToolActivities::test_sanitize_json_response -v
|
||||
|
||||
# Run tests matching a pattern
|
||||
poetry run pytest -k "validation" -v
|
||||
uv run pytest -k "validation" -v
|
||||
```
|
||||
|
||||
## Test Environment Options
|
||||
|
||||
### Local Environment (Default)
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=local
|
||||
uv run pytest --workflow-environment=local
|
||||
```
|
||||
|
||||
### Time-Skipping Environment (Recommended for CI)
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=time-skipping
|
||||
uv run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
### External Temporal Server
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=localhost:7233
|
||||
uv run pytest --workflow-environment=localhost:7233
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
@@ -122,7 +122,7 @@ tests/test_tool_activities.py::TestToolActivities::test_get_wf_env_vars_default_
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Module not found errors**: Run `poetry install --with dev`
|
||||
1. **Module not found errors**: Run `uv sync`
|
||||
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
|
||||
@@ -131,19 +131,19 @@ tests/test_tool_activities.py::TestToolActivities::test_get_wf_env_vars_default_
|
||||
|
||||
Enable verbose logging:
|
||||
```bash
|
||||
poetry run pytest --log-cli-level=DEBUG -s
|
||||
uv run pytest --log-cli-level=DEBUG -s
|
||||
```
|
||||
|
||||
Run with coverage:
|
||||
```bash
|
||||
poetry run pytest --cov=workflows --cov=activities
|
||||
uv run pytest --cov=workflows --cov=activities
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
For CI environments, use:
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=time-skipping --tb=short
|
||||
uv run pytest --workflow-environment=time-skipping --tb=short
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
[ ] enable user to list agents at any time - like end conversation - probably with a next step<br />
|
||||
|
||||
[ ] get this on the Model Context Protocol site's list of MCP clients https://modelcontextprotocol.io/clients
|
||||
|
||||
## Ideas for more goals and tools
|
||||
|
||||
[ ] Add fintech goals <br />
|
||||
|
||||
@@ -8,26 +8,54 @@ const MessageBubble = memo(({ message, fallback = "", isUser = false }) => {
|
||||
}
|
||||
|
||||
const renderTextWithLinks = (text) => {
|
||||
// First handle image markdown: 
|
||||
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const parts = text.split(urlRegex);
|
||||
|
||||
return parts.map((part, index) => {
|
||||
if (urlRegex.test(part)) {
|
||||
// Split by image markdown first
|
||||
const imageParts = text.split(imageRegex);
|
||||
|
||||
return imageParts.map((part, index) => {
|
||||
// Every third element (starting from index 2) is an image URL
|
||||
if (index > 0 && (index - 2) % 3 === 0) {
|
||||
const altText = imageParts[index - 1];
|
||||
const imageUrl = part;
|
||||
return (
|
||||
<a
|
||||
<img
|
||||
key={index}
|
||||
href={part}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-600 underline"
|
||||
aria-label={`External link to ${part}`}
|
||||
>
|
||||
{part}
|
||||
</a>
|
||||
src={imageUrl}
|
||||
alt={altText}
|
||||
className="max-w-full h-auto rounded mt-2 mb-2 mx-auto block border border-gray-300 dark:border-gray-600"
|
||||
style={{ maxHeight: '200px' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return part;
|
||||
});
|
||||
// Skip alt text parts (every second element after first)
|
||||
if (index > 0 && (index - 1) % 3 === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle regular text and links
|
||||
const linkParts = part.split(urlRegex);
|
||||
return linkParts.map((linkPart, linkIndex) => {
|
||||
if (urlRegex.test(linkPart)) {
|
||||
return (
|
||||
<a
|
||||
key={`${index}-${linkIndex}`}
|
||||
href={linkPart}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-600 underline"
|
||||
aria-label={`External link to ${linkPart}`}
|
||||
>
|
||||
{linkPart}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return linkPart;
|
||||
});
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,10 @@ import { apiService } from "../services/api";
|
||||
const POLL_INTERVAL = 600; // 0.6 seconds
|
||||
const INITIAL_ERROR_STATE = { visible: false, message: '' };
|
||||
const DEBOUNCE_DELAY = 300; // 300ms debounce for user input
|
||||
const CONVERSATION_FETCH_ERROR_DELAY_MS = 10000; // wait 10s before showing fetch errors
|
||||
const CONVERSATION_FETCH_ERROR_THRESHOLD = Math.ceil(
|
||||
CONVERSATION_FETCH_ERROR_DELAY_MS / POLL_INTERVAL
|
||||
);
|
||||
|
||||
function useDebounce(value, delay) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
@@ -39,13 +43,34 @@ export default function App() {
|
||||
const debouncedUserInput = useDebounce(userInput, DEBOUNCE_DELAY);
|
||||
|
||||
const errorTimerRef = useRef(null);
|
||||
const conversationFetchErrorCountRef = useRef(0);
|
||||
|
||||
const handleError = useCallback((error, context) => {
|
||||
console.error(`${context}:`, error);
|
||||
|
||||
const isConversationFetchError = error.status === 404;
|
||||
const isConversationFetchError =
|
||||
context === "fetching conversation" && (error.status === 404 || error.status === 408);
|
||||
|
||||
if (isConversationFetchError) {
|
||||
if (error.status === 404) {
|
||||
conversationFetchErrorCountRef.current += 1;
|
||||
|
||||
const hasExceededThreshold =
|
||||
conversationFetchErrorCountRef.current >= CONVERSATION_FETCH_ERROR_THRESHOLD;
|
||||
|
||||
if (!hasExceededThreshold) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// For timeouts or other connectivity errors surface immediately
|
||||
conversationFetchErrorCountRef.current = CONVERSATION_FETCH_ERROR_THRESHOLD;
|
||||
}
|
||||
} else {
|
||||
conversationFetchErrorCountRef.current = 0;
|
||||
}
|
||||
|
||||
const errorMessage = isConversationFetchError
|
||||
? "Error fetching conversation. Retrying..." // Updated message
|
||||
? "Error fetching conversation. Retrying..."
|
||||
: `Error ${context.toLowerCase()}. Please try again.`;
|
||||
|
||||
setError(prevError => {
|
||||
@@ -72,6 +97,7 @@ export default function App() {
|
||||
if (errorTimerRef.current) {
|
||||
clearTimeout(errorTimerRef.current);
|
||||
}
|
||||
conversationFetchErrorCountRef.current = 0;
|
||||
setError(INITIAL_ERROR_STATE);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
const API_BASE_URL = 'http://127.0.0.1:8000';
|
||||
|
||||
const resolveRequestTimeout = () => {
|
||||
const env = typeof import.meta !== 'undefined' ? import.meta.env : undefined;
|
||||
const configured = env?.VITE_API_TIMEOUT_MS;
|
||||
const parsed = Number.parseInt(configured, 10);
|
||||
if (Number.isFinite(parsed) && parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
return 15000;
|
||||
};
|
||||
|
||||
const REQUEST_TIMEOUT_MS = resolveRequestTimeout(); // default to 15s, overridable via Vite env
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(message, status) {
|
||||
super(message);
|
||||
@@ -19,12 +31,31 @@ async function handleResponse(response) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options = {}, timeout = REQUEST_TIMEOUT_MS) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
return await fetch(url, { ...options, signal: controller.signal });
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new ApiError('Request timed out', 408);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
export const apiService = {
|
||||
async getConversationHistory() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/get-conversation-history`);
|
||||
const res = await fetchWithTimeout(`${API_BASE_URL}/get-conversation-history`);
|
||||
return handleResponse(res);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ApiError(
|
||||
'Failed to fetch conversation history',
|
||||
error.status || 500
|
||||
@@ -38,7 +69,7 @@ export const apiService = {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
const res = await fetchWithTimeout(
|
||||
`${API_BASE_URL}/send-prompt?prompt=${encodeURIComponent(message)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -49,6 +80,9 @@ export const apiService = {
|
||||
);
|
||||
return handleResponse(res);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ApiError(
|
||||
'Failed to send message',
|
||||
error.status || 500
|
||||
@@ -58,7 +92,7 @@ export const apiService = {
|
||||
|
||||
async startWorkflow() {
|
||||
try {
|
||||
const res = await fetch(
|
||||
const res = await fetchWithTimeout(
|
||||
`${API_BASE_URL}/start-workflow`,
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -69,6 +103,9 @@ export const apiService = {
|
||||
);
|
||||
return handleResponse(res);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ApiError(
|
||||
'Failed to start workflow',
|
||||
error.status || 500
|
||||
@@ -78,7 +115,7 @@ export const apiService = {
|
||||
|
||||
async confirm() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/confirm`, {
|
||||
const res = await fetchWithTimeout(`${API_BASE_URL}/confirm`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -86,6 +123,9 @@ export const apiService = {
|
||||
});
|
||||
return handleResponse(res);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ApiError(
|
||||
'Failed to confirm action',
|
||||
error.status || 500
|
||||
|
||||
@@ -5,5 +5,6 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
open: true,
|
||||
host: process.env.VITE_HOST ?? 'localhost',
|
||||
},
|
||||
});
|
||||
@@ -23,11 +23,11 @@ goal_food_ordering = AgentGoal(
|
||||
]
|
||||
),
|
||||
description="The user wants to order food from Tony's Pizza Palace. "
|
||||
"First, help the user browse the menu by calling list_products. "
|
||||
"First, help the user browse the menu by calling list_products. You can refer to this data throughout the conversation to answer questions (without re-calling this tool). If menu items contain links to images, you can use markdown to display them e.g. "
|
||||
"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), "
|
||||
"For checkout: 1) create_invoice (always include days_until_due so the invoice has a due date, e.g., days_until_due=7), 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(
|
||||
@@ -35,8 +35,8 @@ goal_food_ordering = AgentGoal(
|
||||
"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?",
|
||||
'tool_result: {"products": [{"id": "prod_pepperoni", "name": "Pepperoni Pizza", "description": "A timeless classic featuring pepperoni", "images": ["https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfcGRHc0c4cEZYWmR2bm0zOHBOa0FWMk5t008QmCJoWr"]}, {"id": "prod_margherita", "name": "Margherita Pizza", "description": "Fresh mozzarella and basil", "images": ["https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfamdmTXBFbzY0TW9rS2N0c2g0Tml2SERL00Evl60Ttq"]}, {"id": "prod_garlic_bread", "name": "Garlic Bread", "description": "Warm crusty bread with garlic butter", "images": ["https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfWTdIZTBkUjNZNFQ1ZEhSVG9nRnduY1pS00XVgLRRZD"]}, {"id": "prod_soda", "name": "Soda", "description": "A large bottle of cola", "images": ["https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfbDJxckJKMDRnT1dDc243OHlZNWNkZkY5006Xg07kHT"]}]}',
|
||||
"agent: Welcome to our menu! Here's what we have available:\\n\\n**Pepperoni Pizza** - A timeless classic featuring pepperoni\\n\\n\\n**Margherita Pizza** - Fresh mozzarella and basil\\n\\n\\n**Garlic Bread** - Warm crusty bread with garlic butter\\n\\n\\n**Soda** - A large bottle of cola\\n\\n\\nWhat 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'>",
|
||||
@@ -59,8 +59,8 @@ goal_food_ordering = AgentGoal(
|
||||
"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'>",
|
||||
"agent: Customer profile created! Now I'll create a draft invoice for your order with payment due in 7 days.",
|
||||
"user_confirmed_tool_run: <user clicks confirm on create_invoice tool with customer='cus_ExAmPlE12345', days_until_due=7>",
|
||||
'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'>",
|
||||
|
||||
3147
poetry.lock
generated
3147
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,57 +1,57 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "temporal_AI_agent"
|
||||
version = "0.2.0"
|
||||
description = "Temporal AI Agent"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
"Steve Androulakis <steve.androulakis@temporal.io>",
|
||||
"Laine Smith <lainecaseysmith@gmail.com>",
|
||||
"Joshua Smith <josh.smith@temporal.io>"
|
||||
{ name = "Steve Androulakis", email = "steve.androulakis@temporal.io" },
|
||||
{ name = "Laine Smith", email = "lainecaseysmith@gmail.com" },
|
||||
{ name = "Joshua Smith", email = "josh.smith@temporal.io" },
|
||||
]
|
||||
requires-python = ">=3.10,<4.0"
|
||||
readme = "README.md"
|
||||
|
||||
# By default, Poetry will find packages automatically,
|
||||
# but explicitly including them is fine:
|
||||
packages = [
|
||||
{ include = "**/*.py", from = "." }
|
||||
license = "MIT"
|
||||
dependencies = [
|
||||
"temporalio>=1.8.0,<2",
|
||||
"litellm>=1.70.0,<2",
|
||||
"pyyaml>=6.0.2,<7",
|
||||
"fastapi>=0.115.6,<0.116",
|
||||
"uvicorn>=0.34.0,<0.35",
|
||||
"python-dotenv>=1.0.1,<2",
|
||||
"requests>=2.32.3,<3",
|
||||
"pandas>=2.2.3,<3",
|
||||
"stripe>=11.4.1,<12",
|
||||
"gtfs-kit>=10.1.1,<11",
|
||||
"fastmcp>=2.7.0,<3",
|
||||
]
|
||||
|
||||
[tool.poetry.urls]
|
||||
[project.urls]
|
||||
"Bug Tracker" = "https://github.com/temporal-community/temporal-ai-agent/issues"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.2",
|
||||
"pytest-asyncio>=0.26.0,<0.27",
|
||||
"black~=23.7",
|
||||
"isort~=5.12",
|
||||
"mypy>=1.16.0,<2",
|
||||
"poethepoet>=0.37.0",
|
||||
]
|
||||
|
||||
[tool.poe.tasks]
|
||||
format = [{cmd = "black ."}, {cmd = "isort ."}]
|
||||
lint = [{cmd = "black --check ."}, {cmd = "isort --check-only ."}, {ref = "lint-types" }]
|
||||
lint-types = "mypy --check-untyped-defs --namespace-packages ."
|
||||
test = "pytest"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<4.0"
|
||||
temporalio = "^1.8.0"
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
# Standard library modules (e.g. asyncio, collections) don't need to be added
|
||||
# since they're built-in for Python 3.8+.
|
||||
litellm = "^1.70.0"
|
||||
pyyaml = "^6.0.2"
|
||||
fastapi = "^0.115.6"
|
||||
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"
|
||||
[tool.hatch.build]
|
||||
packages = ["activities", "api", "goals", "models", "prompts", "shared", "tools", "workflows"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.4.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
@@ -12,6 +12,7 @@ from activities.tool_activities import (
|
||||
mcp_list_tools,
|
||||
)
|
||||
from shared.config import TEMPORAL_TASK_QUEUE, get_temporal_client
|
||||
from shared.mcp_client_manager import MCPClientManager
|
||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||
|
||||
|
||||
@@ -23,11 +24,14 @@ async def main():
|
||||
llm_model = os.environ.get("LLM_MODEL", "openai/gpt-4")
|
||||
print(f"Worker will use LLM model: {llm_model}")
|
||||
|
||||
# Create shared MCP client manager
|
||||
mcp_client_manager = MCPClientManager()
|
||||
|
||||
# Create the client
|
||||
client = await get_temporal_client()
|
||||
|
||||
# Initialize the activities class
|
||||
activities = ToolActivities()
|
||||
# Initialize the activities class with injected manager
|
||||
activities = ToolActivities(mcp_client_manager)
|
||||
print(f"ToolActivities initialized with LLM model: {llm_model}")
|
||||
|
||||
# If using Ollama, pre-load the model to avoid cold start latency
|
||||
@@ -54,25 +58,31 @@ async def main():
|
||||
print("Worker ready to process tasks!")
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Run the worker
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
||||
worker = Worker(
|
||||
client,
|
||||
task_queue=TEMPORAL_TASK_QUEUE,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[
|
||||
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,
|
||||
)
|
||||
# Run the worker with proper cleanup
|
||||
try:
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=100
|
||||
) as activity_executor:
|
||||
worker = Worker(
|
||||
client,
|
||||
task_queue=TEMPORAL_TASK_QUEUE,
|
||||
workflows=[AgentGoalWorkflow],
|
||||
activities=[
|
||||
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,
|
||||
)
|
||||
|
||||
print(f"Starting worker, connecting to task queue: {TEMPORAL_TASK_QUEUE}")
|
||||
await worker.run()
|
||||
print(f"Starting worker, connecting to task queue: {TEMPORAL_TASK_QUEUE}")
|
||||
await worker.run()
|
||||
finally:
|
||||
# Cleanup MCP connections when worker shuts down
|
||||
await mcp_client_manager.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
167
shared/mcp_client_manager.py
Normal file
167
shared/mcp_client_manager.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Dict, Tuple
|
||||
|
||||
from temporalio import activity
|
||||
|
||||
from models.tool_definitions import MCPServerDefinition
|
||||
|
||||
# Import MCP client libraries
|
||||
if TYPE_CHECKING:
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
class MCPClientManager:
|
||||
"""Manages pooled MCP client connections for reuse across tool calls"""
|
||||
|
||||
def __init__(self):
|
||||
self._clients: Dict[str, Any] = {}
|
||||
self._connections: Dict[str, Tuple[Any, Any]] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def get_client(
|
||||
self, server_def: MCPServerDefinition | Dict[str, Any] | None
|
||||
) -> Any:
|
||||
"""Return existing client or create new one, keyed by server definition hash"""
|
||||
async with self._lock:
|
||||
key = self._get_server_key(server_def)
|
||||
if key not in self._clients:
|
||||
await self._create_client(server_def, key)
|
||||
activity.logger.info(
|
||||
f"Created new MCP client for {self._get_server_name(server_def)}"
|
||||
)
|
||||
else:
|
||||
activity.logger.info(
|
||||
f"Reusing existing MCP client for {self._get_server_name(server_def)}"
|
||||
)
|
||||
return self._clients[key]
|
||||
|
||||
def _get_server_key(
|
||||
self, server_def: MCPServerDefinition | Dict[str, Any] | None
|
||||
) -> str:
|
||||
"""Generate unique key for server definition"""
|
||||
if server_def is None:
|
||||
return "default:python:server.py"
|
||||
|
||||
# Handle both MCPServerDefinition objects and dicts (from Temporal serialization)
|
||||
if isinstance(server_def, dict):
|
||||
name = server_def.get("name", "default")
|
||||
command = server_def.get("command", "python")
|
||||
args = server_def.get("args", ["server.py"])
|
||||
else:
|
||||
name = server_def.name
|
||||
command = server_def.command
|
||||
args = server_def.args
|
||||
|
||||
return f"{name}:{command}:{':'.join(args)}"
|
||||
|
||||
def _get_server_name(
|
||||
self, server_def: MCPServerDefinition | Dict[str, Any] | None
|
||||
) -> str:
|
||||
"""Get server name for logging"""
|
||||
if server_def is None:
|
||||
return "default"
|
||||
|
||||
if isinstance(server_def, dict):
|
||||
return server_def.get("name", "default")
|
||||
else:
|
||||
return server_def.name
|
||||
|
||||
def _build_connection(
|
||||
self, server_def: MCPServerDefinition | Dict[str, Any] | None
|
||||
) -> Dict[str, Any]:
|
||||
"""Build connection parameters from MCPServerDefinition or dict"""
|
||||
if server_def 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_def, dict):
|
||||
return {
|
||||
"type": server_def.get("connection_type", "stdio"),
|
||||
"command": server_def.get("command", "python"),
|
||||
"args": server_def.get("args", ["server.py"]),
|
||||
"env": server_def.get("env", {}) or {},
|
||||
}
|
||||
|
||||
return {
|
||||
"type": server_def.connection_type,
|
||||
"command": server_def.command,
|
||||
"args": server_def.args,
|
||||
"env": server_def.env or {},
|
||||
}
|
||||
|
||||
@asynccontextmanager
|
||||
async def _stdio_connection(self, command: str, args: list, env: dict):
|
||||
"""Create stdio connection to MCP server"""
|
||||
if stdio_client is None:
|
||||
raise Exception("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
|
||||
|
||||
async def _create_client(
|
||||
self, server_def: MCPServerDefinition | Dict[str, Any] | None, key: str
|
||||
):
|
||||
"""Create and store new client connection"""
|
||||
connection = self._build_connection(server_def)
|
||||
|
||||
if connection["type"] == "stdio":
|
||||
# Create stdio connection
|
||||
connection_manager = self._stdio_connection(
|
||||
command=connection.get("command", "python"),
|
||||
args=connection.get("args", ["server.py"]),
|
||||
env=connection.get("env", {}),
|
||||
)
|
||||
|
||||
# Enter the connection context
|
||||
read, write = await connection_manager.__aenter__()
|
||||
|
||||
# Create and initialize client session
|
||||
session = ClientSession(read, write)
|
||||
await session.initialize()
|
||||
|
||||
# Store both the session and connection manager for cleanup
|
||||
self._clients[key] = session
|
||||
self._connections[key] = (connection_manager, read, write)
|
||||
else:
|
||||
raise Exception(f"Unsupported connection type: {connection['type']}")
|
||||
|
||||
async def cleanup(self):
|
||||
"""Close all connections gracefully"""
|
||||
async with self._lock:
|
||||
# Close all client sessions
|
||||
for session in self._clients.values():
|
||||
try:
|
||||
await session.close()
|
||||
except Exception as e:
|
||||
activity.logger.warning(f"Error closing MCP session: {e}")
|
||||
|
||||
# Exit all connection contexts
|
||||
for connection_manager, read, write in self._connections.values():
|
||||
try:
|
||||
await connection_manager.__aexit__(None, None, None)
|
||||
except Exception as e:
|
||||
activity.logger.warning(f"Error closing MCP connection: {e}")
|
||||
|
||||
self._clients.clear()
|
||||
self._connections.clear()
|
||||
activity.logger.info("All MCP connections closed")
|
||||
@@ -53,31 +53,31 @@ Provides shared test fixtures and configuration:
|
||||
Ensure you have the required dependencies installed:
|
||||
|
||||
```bash
|
||||
poetry install --with dev
|
||||
uv sync
|
||||
```
|
||||
|
||||
### Basic Test Execution
|
||||
|
||||
Run all tests:
|
||||
```bash
|
||||
poetry run pytest
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
Run specific test files:
|
||||
```bash
|
||||
# Workflow tests only
|
||||
poetry run pytest tests/test_agent_goal_workflow.py
|
||||
uv run pytest tests/test_agent_goal_workflow.py
|
||||
|
||||
# Activity tests only
|
||||
poetry run pytest tests/test_tool_activities.py
|
||||
uv run pytest tests/test_tool_activities.py
|
||||
|
||||
# Legacy tests
|
||||
poetry run pytest tests/workflowtests/
|
||||
uv run pytest tests/workflowtests/
|
||||
```
|
||||
|
||||
Run with verbose output:
|
||||
```bash
|
||||
poetry run pytest -v
|
||||
uv run pytest -v
|
||||
```
|
||||
|
||||
### Test Environment Options
|
||||
@@ -87,34 +87,34 @@ The tests support different Temporal environments via the `--workflow-environmen
|
||||
#### Local Environment (Default)
|
||||
Uses a local Temporal test server:
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=local
|
||||
uv 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
|
||||
uv run pytest --workflow-environment=time-skipping
|
||||
```
|
||||
|
||||
#### External Server
|
||||
Connect to an existing Temporal server:
|
||||
```bash
|
||||
poetry run pytest --workflow-environment=localhost:7233
|
||||
uv 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 -
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
ls
|
||||
poetry install --with dev
|
||||
uv sync
|
||||
cd frontend
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
# Pre-download the temporal test server binary
|
||||
poetry run python3 -c "
|
||||
uv run python -c "
|
||||
import asyncio
|
||||
import sys
|
||||
from temporalio.testing import WorkflowEnvironment
|
||||
@@ -139,22 +139,22 @@ asyncio.run(predownload())
|
||||
Run tests by pattern:
|
||||
```bash
|
||||
# Run only validation tests
|
||||
poetry run pytest -k "validation"
|
||||
uv run pytest -k "validation"
|
||||
|
||||
# Run only workflow tests
|
||||
poetry run pytest -k "workflow"
|
||||
uv run pytest -k "workflow"
|
||||
|
||||
# Run only activity tests
|
||||
poetry run pytest -k "activity"
|
||||
uv run pytest -k "activity"
|
||||
```
|
||||
|
||||
Run tests by marker (if you add custom markers):
|
||||
```bash
|
||||
# Run only integration tests
|
||||
poetry run pytest -m integration
|
||||
uv run pytest -m integration
|
||||
|
||||
# Skip slow tests
|
||||
poetry run pytest -m "not slow"
|
||||
uv run pytest -m "not slow"
|
||||
```
|
||||
|
||||
## Test Configuration
|
||||
@@ -276,7 +276,7 @@ The `sample_combined_input` fixture provides:
|
||||
|
||||
Enable detailed logging:
|
||||
```bash
|
||||
poetry run pytest --log-cli-level=DEBUG -s
|
||||
uv run pytest --log-cli-level=DEBUG -s
|
||||
```
|
||||
|
||||
### Temporal Web UI
|
||||
@@ -301,21 +301,18 @@ 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
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
- run: uv sync
|
||||
- run: uv 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
|
||||
uv add --group dev pytest-cov
|
||||
uv run pytest --cov=workflows --cov=activities --cov-report=html
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
@@ -342,7 +339,7 @@ poetry run pytest --cov=workflows --cov=activities --cov-report=html
|
||||
|
||||
- Check Temporal Python SDK documentation
|
||||
- Review existing test patterns in the codebase
|
||||
- Use `poetry run pytest --collect-only` to verify test discovery
|
||||
- Use `uv run pytest --collect-only` to verify test discovery
|
||||
- Run with `-v` flag for detailed output
|
||||
|
||||
## Legacy Tests
|
||||
|
||||
@@ -312,6 +312,109 @@ async def test_mcp_tool_execution_flow(client: Client):
|
||||
assert captured["dynamic_args"]["server_definition"]["name"] == server_def.name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_invoice_defaults_days_until_due(client: Client):
|
||||
"""create_invoice should include a default days_until_due when missing."""
|
||||
task_queue_name = str(uuid.uuid4())
|
||||
server_def = MCPServerDefinition(name="test", command="python", args=["srv.py"])
|
||||
goal = AgentGoal(
|
||||
id="g_invoice_default",
|
||||
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": "create_invoice",
|
||||
"args": {"customer": "cus_123"},
|
||||
"response": "Creating invoice",
|
||||
}
|
||||
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": {
|
||||
"create_invoice": {
|
||||
"name": "create_invoice",
|
||||
"description": "",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"customer": {"type": "string"},
|
||||
"days_until_due": {"type": "number"},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"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": "create_invoice", "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, "make invoice")
|
||||
await asyncio.sleep(0.5)
|
||||
await handle.signal(AgentGoalWorkflow.confirm)
|
||||
await asyncio.sleep(0.5)
|
||||
await handle.result()
|
||||
|
||||
assert "dynamic_args" in captured
|
||||
assert captured["dynamic_args"]["days_until_due"] == 7
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_tool_failure_recorded(client: Client):
|
||||
"""Failure of an MCP tool should be recorded in conversation history."""
|
||||
|
||||
@@ -47,7 +47,7 @@ def create_invoice(args: dict) -> dict:
|
||||
stripe.InvoiceItem.create(
|
||||
customer=customer_id,
|
||||
amount=amount_cents,
|
||||
currency="gbp",
|
||||
currency="usd",
|
||||
description=args.get("tripDetails", "Service Invoice"),
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,21 @@
|
||||
import calendar
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def find_events(args: dict) -> dict:
|
||||
def find_events(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Find events that overlap with a given month in a specified city.
|
||||
|
||||
Args:
|
||||
args: Dictionary containing:
|
||||
- city: City name to search for events (e.g., 'Melbourne')
|
||||
- month: Month name to search (e.g., 'April')
|
||||
|
||||
Returns:
|
||||
Dictionary with 'events' list and 'note' with search context.
|
||||
"""
|
||||
search_city = args.get("city", "").lower()
|
||||
search_month = args.get("month", "").capitalize()
|
||||
|
||||
@@ -16,36 +28,33 @@ def find_events(args: dict) -> dict:
|
||||
except ValueError:
|
||||
return {"error": "Invalid month provided."}
|
||||
|
||||
# Helper to wrap months into [1..12]
|
||||
def get_adjacent_months(m):
|
||||
prev_m = 12 if m == 1 else (m - 1)
|
||||
next_m = 1 if m == 12 else (m + 1)
|
||||
return [prev_m, m, next_m]
|
||||
# Determine the target year: use next upcoming occurrence of the month
|
||||
today = datetime.now()
|
||||
if month_number >= today.month:
|
||||
target_year = today.year
|
||||
else:
|
||||
target_year = today.year + 1
|
||||
|
||||
valid_months = get_adjacent_months(month_number)
|
||||
# Build the search month date range
|
||||
month_start = datetime(target_year, month_number, 1)
|
||||
last_day = calendar.monthrange(target_year, month_number)[1]
|
||||
month_end = datetime(target_year, month_number, last_day)
|
||||
|
||||
matching_events = []
|
||||
for city_name, events in json.load(open(file_path)).items():
|
||||
with open(file_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
for city_name, events in data.items():
|
||||
if search_city and search_city not in city_name.lower():
|
||||
continue
|
||||
|
||||
for event in events:
|
||||
date_from = datetime.strptime(event["dateFrom"], "%Y-%m-%d")
|
||||
date_to = datetime.strptime(event["dateTo"], "%Y-%m-%d")
|
||||
|
||||
# If the event's start or end month is in our valid months
|
||||
if date_from.month in valid_months or date_to.month in valid_months:
|
||||
# Add metadata explaining how it matches
|
||||
if date_from.month == month_number or date_to.month == month_number:
|
||||
month_context = "requested month"
|
||||
elif (
|
||||
date_from.month == valid_months[0]
|
||||
or date_to.month == valid_months[0]
|
||||
):
|
||||
month_context = "previous month"
|
||||
else:
|
||||
month_context = "next month"
|
||||
event_start = datetime.strptime(event["dateFrom"], "%Y-%m-%d")
|
||||
event_end = datetime.strptime(event["dateTo"], "%Y-%m-%d")
|
||||
|
||||
# Check if the event overlaps with the search month
|
||||
# Two date ranges overlap if: start1 <= end2 AND start2 <= end1
|
||||
if month_start <= event_end and event_start <= month_end:
|
||||
matching_events.append(
|
||||
{
|
||||
"city": city_name,
|
||||
@@ -53,12 +62,10 @@ def find_events(args: dict) -> dict:
|
||||
"dateFrom": event["dateFrom"],
|
||||
"dateTo": event["dateTo"],
|
||||
"description": event["description"],
|
||||
"month": month_context,
|
||||
}
|
||||
)
|
||||
|
||||
# Add top-level metadata if you wish
|
||||
return {
|
||||
"note": f"Returning events from {search_month} plus one month either side (i.e., {', '.join(datetime(2025, m, 1).strftime('%B') for m in valid_months)}).",
|
||||
"note": f"Returning events that overlap with {search_month} {target_year}.",
|
||||
"events": matching_events,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
|
||||
@@ -1,617 +0,0 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
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:
|
||||
# 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!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# ----- Travel Goals ---
|
||||
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 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",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# ----- HR Goals ---
|
||||
# This goal uses the data/employee_pto_data.json file as dummy data.
|
||||
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! ",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# This goal uses the data/employee_pto_data.json file as dummy data.
|
||||
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.",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# check integration with bank
|
||||
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.",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# ----- FinServ Goals ---
|
||||
# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data
|
||||
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 "
|
||||
"Bitcoint: 0.1378 \n "
|
||||
"Thanks for being a customer since 2014!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data
|
||||
# it also uses a separate workflow/tool, see ./setup.md for details
|
||||
goal_fin_move_money = AgentGoal(
|
||||
id="goal_fin_move_money",
|
||||
category_tag="fin",
|
||||
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. ",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# this starts a loan approval process
|
||||
# it also uses a separate workflow/tool, see ./setup.md for details
|
||||
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. ",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# ----- E-Commerce Goals ---
|
||||
# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data
|
||||
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!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ----- 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)
|
||||
goal_list.append(goal_pirate_treasure)
|
||||
goal_list.append(goal_event_flight_invoice)
|
||||
goal_list.append(goal_match_train_invoice)
|
||||
goal_list.append(goal_hr_schedule_pto)
|
||||
goal_list.append(goal_hr_check_pto)
|
||||
goal_list.append(goal_hr_check_paycheck_bank_integration_status)
|
||||
goal_list.append(goal_fin_check_account_balances)
|
||||
goal_list.append(goal_fin_move_money)
|
||||
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
|
||||
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
|
||||
@@ -180,10 +180,9 @@ search_fixtures_tool = ToolDefinition(
|
||||
|
||||
find_events_tool = ToolDefinition(
|
||||
name="FindEvents",
|
||||
description="Find upcoming events to travel to a given city (e.g., 'Melbourne') and a date or month. "
|
||||
description="Find upcoming events to travel to a given city (e.g., 'Melbourne') and a month. "
|
||||
"It knows about events in Oceania only (e.g. major Australian and New Zealand cities). "
|
||||
"It will search 1 month either side of the month provided. "
|
||||
"Returns a list of events. ",
|
||||
"Returns events that overlap with the specified month. ",
|
||||
arguments=[
|
||||
ToolArgument(
|
||||
name="city",
|
||||
@@ -193,7 +192,7 @@ find_events_tool = ToolDefinition(
|
||||
ToolArgument(
|
||||
name="month",
|
||||
type="string",
|
||||
description="The month to search for events (will search 1 month either side of the month provided)",
|
||||
description="The month to search for events (e.g., 'April')",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from temporalio.common import RetryPolicy
|
||||
from temporalio.exceptions import ActivityError
|
||||
|
||||
from models.data_types import ConversationHistory, ToolPromptInput
|
||||
from models.tool_definitions import AgentGoal
|
||||
from models.tool_definitions import AgentGoal, ToolDefinition
|
||||
from prompts.agent_prompt_generators import (
|
||||
generate_missing_args_prompt,
|
||||
generate_tool_completion_prompt,
|
||||
@@ -21,63 +21,19 @@ 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"""
|
||||
"""Check if a tool should be dispatched via MCP."""
|
||||
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,
|
||||
)
|
||||
# Native tools are registered with tools.get_handler. If lookup succeeds,
|
||||
# the tool should execute locally; otherwise treat it as MCP-provided.
|
||||
from tools import get_handler
|
||||
|
||||
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
|
||||
try:
|
||||
get_handler(tool_name)
|
||||
return False
|
||||
except ValueError:
|
||||
return True
|
||||
|
||||
|
||||
async def handle_tool_execution(
|
||||
@@ -98,6 +54,13 @@ async def handle_tool_execution(
|
||||
|
||||
# Add server definition to args for MCP tools
|
||||
mcp_args = tool_data["args"].copy()
|
||||
|
||||
# Stripe's MCP server enforces days_until_due when the collection
|
||||
# method defaults to send_invoice. Provide a reasonable default when
|
||||
# the planner omits it so invoice creation doesn't fail upstream.
|
||||
if current_tool == "create_invoice" and "days_until_due" not in mcp_args:
|
||||
mcp_args["days_until_due"] = 7
|
||||
|
||||
mcp_args["server_definition"] = goal.mcp_server_definition
|
||||
|
||||
dynamic_result = await workflow.execute_activity(
|
||||
|
||||
Reference in New Issue
Block a user