1 Commits

Author SHA1 Message Date
Mason Egger
b0408d9ed6 Update API to use proper query
Looks like you changed the query handler in the Workflow but didn't update the API. Minor patch.
2025-06-16 10:53:40 -05:00
26 changed files with 3417 additions and 3976 deletions

View File

@@ -1,20 +0,0 @@
{
"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"
}
}

View File

@@ -1,37 +0,0 @@
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

View File

@@ -34,10 +34,11 @@ Default URLs:
1. **Prerequisites:** 1. **Prerequisites:**
```bash ```bash
# Install uv and Temporal server (MacOS) # Install Poetry for Python dependency management
brew install uv curl -sSL https://install.python-poetry.org | python3 -
brew install temporal
# Start Temporal server (Mac)
brew install temporal
temporal server start-dev temporal server start-dev
``` ```
@@ -49,9 +50,9 @@ Default URLs:
make run-api # Starts the API server make run-api # Starts the API server
# Or manually: # Or manually:
uv sync poetry install
uv run scripts/run_worker.py # In one terminal poetry run python scripts/run_worker.py # In one terminal
uv run uvicorn api.main:app --reload # In another terminal poetry run uvicorn api.main:app --reload # In another terminal
``` ```
3. **Frontend (React):** 3. **Frontend (React):**
@@ -101,20 +102,20 @@ The project includes comprehensive tests using Temporal's testing framework:
```bash ```bash
# Install test dependencies # Install test dependencies
uv sync poetry install --with dev
# Run all tests # Run all tests
uv run pytest poetry run pytest
# Run with time-skipping for faster execution # Run with time-skipping for faster execution
uv run pytest --workflow-environment=time-skipping poetry run pytest --workflow-environment=time-skipping
# Run specific test categories # Run specific test categories
uv run pytest tests/test_tool_activities.py -v # Activity tests poetry run pytest tests/test_tool_activities.py -v # Activity tests
uv run pytest tests/test_agent_goal_workflow.py -v # Workflow tests poetry run pytest tests/test_agent_goal_workflow.py -v # Workflow tests
# Run with coverage # Run with coverage
uv run pytest --cov=workflows --cov=activities poetry run pytest --cov=workflows --cov=activities
``` ```
**Test Coverage:** **Test Coverage:**
@@ -129,15 +130,15 @@ uv run pytest --cov=workflows --cov=activities
## Linting and Code Quality ## Linting and Code Quality
```bash ```bash
# Using poe tasks # Using Poetry tasks
uv run poe format # Format code with black and isort poetry run poe format # Format code with black and isort
uv run poe lint # Check code style and types poetry run poe lint # Check code style and types
uv run poe test # Run test suite poetry run poe test # Run test suite
# Manual commands # Manual commands
uv run black . poetry run black .
uv run isort . poetry run isort .
uv run mypy --check-untyped-defs --namespace-packages . poetry run mypy --check-untyped-defs --namespace-packages .
``` ```
## Agent Customization ## Agent Customization
@@ -191,7 +192,7 @@ For detailed architecture information, see [architecture.md](docs/architecture.m
- Use clear commit messages describing the change purpose - Use clear commit messages describing the change purpose
- Reference specific files and line numbers when relevant (e.g., `workflows/agent_goal_workflow.py:125`) - Reference specific files and line numbers when relevant (e.g., `workflows/agent_goal_workflow.py:125`)
- Open PRs describing **what changed** and **why** - Open PRs describing **what changed** and **why**
- Ensure tests pass before submitting: `uv run pytest --workflow-environment=time-skipping` - Ensure tests pass before submitting: `poetry run pytest --workflow-environment=time-skipping`
## Additional Resources ## Additional Resources
- **Setup Guide**: [setup.md](docs/setup.md) - Detailed configuration instructions - **Setup Guide**: [setup.md](docs/setup.md) - Detailed configuration instructions

View File

@@ -4,19 +4,17 @@ WORKDIR /app
# Install system dependencies # Install system dependencies
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends gcc build-essential curl && \ apt-get install -y --no-install-recommends gcc build-essential && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Install uv # Copy requirements first for better caching
RUN curl -LsSf https://astral.sh/uv/install.sh | sh RUN pip install --no-cache-dir poetry
ENV PATH="$PATH:/root/.local/bin"
# Copy dependency files and README (needed for package build) # Install Python dependencies without creating a virtualenv
COPY pyproject.toml uv.lock README.md ./ COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create false \
# Install dependencies and create virtual environment && poetry install --without dev --no-interaction --no-ansi --no-root
RUN uv sync --frozen
# Copy application code # Copy application code
COPY . . COPY . .
@@ -29,4 +27,4 @@ ENV PYTHONPATH=/app
EXPOSE 8000 EXPOSE 8000
# Default to running only the API server; worker and train-api are separate Compose services # Default to running only the API server; worker and train-api are separate Compose services
CMD ["uv", "run", "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,24 +1,35 @@
.PHONY: setup install run-worker run-api run-frontend run-train-api run-legacy-worker run-enterprise setup-venv check-python run-dev .PHONY: setup install run-worker run-api run-frontend run-train-api run-legacy-worker run-enterprise setup-venv check-python run-dev
setup: # Setup commands
uv sync 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
cd frontend && npm install cd frontend && npm install
# Run commands # Run commands
run-worker: run-worker:
uv run scripts/run_worker.py poetry run python scripts/run_worker.py
run-api: run-api:
uv run uvicorn api.main:app --reload poetry run uvicorn api.main:app --reload
run-frontend: run-frontend:
cd frontend && npx vite cd frontend && npx vite
run-train-api: run-train-api:
uv run thirdparty/train_api.py poetry run python thirdparty/train_api.py
run-legacy-worker: run-legacy-worker:
uv run scripts/run_legacy_worker.py poetry run python scripts/run_legacy_worker.py
run-enterprise: run-enterprise:
cd enterprise && dotnet build && dotnet run cd enterprise && dotnet build && dotnet run
@@ -39,7 +50,9 @@ run-dev:
# Help command # Help command
help: help:
@echo "Available commands:" @echo "Available commands:"
@echo " make setup - Install all dependencies" @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 run-worker - Start the Temporal worker" @echo " make run-worker - Start the Temporal worker"
@echo " make run-api - Start the API server" @echo " make run-api - Start the API server"
@echo " make run-frontend - Start the frontend development server" @echo " make run-frontend - Start the frontend development server"

View File

@@ -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). See multi-agent execution in action [here](https://www.youtube.com/watch?v=8Dc_0dC14yY).
## Why Temporal? ## 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 reliability, 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 reliablity, state management, a code-first approach that we really like, built-in observability and easy error handling.
For more, check out [architecture-decisions](docs/architecture-decisions.md). For more, check out [architecture-decisions](docs/architecture-decisions.md).
## What is "Agentic AI"? ## What is "Agentic AI"?
@@ -65,13 +65,13 @@ The project includes comprehensive tests for workflows and activities using Temp
```bash ```bash
# Install dependencies including test dependencies # Install dependencies including test dependencies
uv sync poetry install --with dev
# Run all tests # Run all tests
uv run pytest poetry run pytest
# Run with time-skipping for faster execution # Run with time-skipping for faster execution
uv run pytest --workflow-environment=time-skipping poetry run pytest --workflow-environment=time-skipping
``` ```
**Test Coverage:** **Test Coverage:**

View File

@@ -2,17 +2,17 @@ services:
api: api:
volumes: volumes:
- ./:/app:cached - ./:/app:cached
command: uv run uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload command: uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
worker: worker:
volumes: volumes:
- ./:/app:cached - ./:/app:cached
command: uv run scripts/run_worker.py command: python scripts/run_worker.py
train-api: train-api:
volumes: volumes:
- ./:/app:cached - ./:/app:cached
command: uv run thirdparty/train_api.py command: python thirdparty/train_api.py
frontend: frontend:
volumes: volumes:

View File

@@ -79,7 +79,7 @@ services:
- .env - .env
environment: environment:
- TEMPORAL_ADDRESS=temporal:7233 - TEMPORAL_ADDRESS=temporal:7233
command: uv run scripts/run_worker.py command: python scripts/run_worker.py
networks: networks:
- temporal-network - temporal-network
@@ -94,7 +94,7 @@ services:
- .env - .env
environment: environment:
- TEMPORAL_ADDRESS=temporal:7233 - TEMPORAL_ADDRESS=temporal:7233
command: uv run thirdparty/train_api.py command: python thirdparty/train_api.py
networks: networks:
- temporal-network - temporal-network

View File

@@ -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. We use `black` for code formatting and `isort` for import sorting to maintain a consistent codebase.
- **Format code:** - **Format code:**
```bash ```bash
uv run poe format poetry run poe format
``` ```
Or manually Or manually:
``` ```bash
uv run black . poetry run black .
uv run isort . poetry run isort .
``` ```
Please format your code before committing. Please format your code before committing.
### Linting & Type Checking ### Linting & Type Checking
We use `mypy` for static type checking and other linters configured via `poe`. We use `mypy` for static type checking and other linters configured via `poe the poet`.
- **Run linters and type checks:** - **Run linters and type checks:**
```bash ```bash
uv run poe lint poetry run poe lint
``` ```
Or manually for type checking: Or manually for type checking:
```bash ```bash
uv run mypy --check-untyped-defs --namespace-packages . poetry run mypy --check-untyped-defs --namespace-packages .
``` ```
Ensure all linting and type checks pass before submitting a pull request. Ensure all linting and type checks pass before submitting a pull request.
## Testing ## Testing
Comprehensive testing is crucial for this project. We use `pytest` and Temporal's testing framework. Comprehensive testing is crucial for this project. We use `pytest` and Temporal's testing framework.
- **Install test dependencies:** - **Install test dependencies** (if not already done with `poetry install --with dev`):
```bash ```bash
uv sync poetry install --with dev
``` ```
- **Run all tests:** - **Run all tests:**
```bash ```bash
uv run pytest poetry run pytest
``` ```
- **Run tests with time-skipping (recommended for faster execution, especially in CI):** - **Run tests with time-skipping (recommended for faster execution, especially in CI):**
```bash ```bash
uv run pytest --workflow-environment=time-skipping poetry run pytest --workflow-environment=time-skipping
``` ```
For detailed information on test categories, running specific tests, test environments, coverage, and troubleshooting, please refer to: 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. 1. Push your branch to the remote repository.
2. Open a Pull Request (PR) against the `main` branch. 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. 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 `uv 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 `poetry run pytest --workflow-environment=time-skipping` is a good one to run locally.
5. **Request review:** Request a review from one or more maintainers. 5. **Request review:** Request a review from one or more maintainers.
## Reporting Bugs ## Reporting Bugs

View File

@@ -22,6 +22,8 @@ We've provided a Makefile to simplify the setup and running of the application.
```bash ```bash
# Initial setup # Initial setup
make setup # Creates virtual environment and installs dependencies make setup # Creates virtual environment and installs dependencies
make setup-venv # Creates virtual environment only
make install # Installs all dependencies
# Running the application # Running the application
make run-worker # Starts the Temporal worker make run-worker # Starts the Temporal worker
@@ -157,22 +159,24 @@ Default urls:
**Python Backend** **Python Backend**
Requires [`uv`](https://docs.astral.sh/uv/) to manage dependencies. Requires [Poetry](https://python-poetry.org/) to manage dependencies.
1. Install uv: `curl -LsSf https://astral.sh/uv/install.sh | sh` 1. `python -m venv venv`
2. `uv sync` 2. `source venv/bin/activate`
3. `poetry install`
Run the following commands in separate terminal windows: Run the following commands in separate terminal windows:
1. Start the Temporal worker: 1. Start the Temporal worker:
```bash ```bash
uv run scripts/run_worker.py poetry run python scripts/run_worker.py
``` ```
2. Start the API server: 2. Start the API server:
```bash ```bash
uv run uvicorn api.main:app --reload poetry run uvicorn api.main:app --reload
``` ```
Access the API at `/docs` to see the available endpoints. Access the API at `/docs` to see the available endpoints.
@@ -257,7 +261,7 @@ NOTE: This goal was developed for an on-stage demo and has failure (and its reso
Required to search and book trains! Required to search and book trains!
```bash ```bash
uv run thirdparty/train_api.py poetry run python thirdparty/train_api.py
# example url # 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 # http://localhost:8080/api/search?from=london&to=liverpool&outbound_time=2025-04-18T09:00:00&inbound_time=2025-04-20T09:00:00
@@ -269,7 +273,7 @@ uv run 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. These are Python activities that fail (raise NotImplemented) to show how Temporal handles a failure. You can run these activities with.
```bash ```bash
uv run scripts/run_legacy_worker.py poetry run python scripts/run_legacy_worker.py
``` ```
The activity will fail and be retried infinitely. To rescue the activity (and its corresponding workflows), kill the worker and run the .NET one in the section below. 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.
@@ -324,8 +328,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 /> [ ] Select an LLM and add your API key to `.env` <br />
[ ] (Optional) set your starting goal and goal category in `.env` <br /> [ ] (Optional) set your starting goal and goal category in `.env` <br />
[ ] (Optional) configure your Temporal Cloud settings in `.env` <br /> [ ] (Optional) configure your Temporal Cloud settings in `.env` <br />
[ ] `uv run scripts/run_worker.py` <br /> [ ] `poetry run python scripts/run_worker.py` <br />
[ ] `uv run uvicorn api.main:app --reload` <br /> [ ] `poetry run uvicorn api.main:app --reload` <br />
[ ] `cd frontend`, `npm install`, `npx vite` <br /> [ ] `cd frontend`, `npm install`, `npx vite` <br />
[ ] Access the UI at `http://localhost:5173` <br /> [ ] Access the UI at `http://localhost:5173` <br />

View File

@@ -6,17 +6,17 @@ This guide provides instructions for running the comprehensive test suite for th
1. **Install dependencies**: 1. **Install dependencies**:
```bash ```bash
uv sync poetry install --with dev
``` ```
2. **Run all tests**: 2. **Run all tests**:
```bash ```bash
uv run pytest poetry run pytest
``` ```
3. **Run with time-skipping for faster execution**: 3. **Run with time-skipping for faster execution**:
```bash ```bash
uv run pytest --workflow-environment=time-skipping poetry run pytest --workflow-environment=time-skipping
``` ```
## Test Categories ## Test Categories
@@ -39,33 +39,33 @@ This guide provides instructions for running the comprehensive test suite for th
```bash ```bash
# Run only activity tests # Run only activity tests
uv run pytest tests/test_tool_activities.py -v poetry run pytest tests/test_tool_activities.py -v
# Run only workflow tests # Run only workflow tests
uv run pytest tests/test_agent_goal_workflow.py -v poetry run pytest tests/test_agent_goal_workflow.py -v
# Run a specific test # Run a specific test
uv run pytest tests/test_tool_activities.py::TestToolActivities::test_sanitize_json_response -v poetry run pytest tests/test_tool_activities.py::TestToolActivities::test_sanitize_json_response -v
# Run tests matching a pattern # Run tests matching a pattern
uv run pytest -k "validation" -v poetry run pytest -k "validation" -v
``` ```
## Test Environment Options ## Test Environment Options
### Local Environment (Default) ### Local Environment (Default)
```bash ```bash
uv run pytest --workflow-environment=local poetry run pytest --workflow-environment=local
``` ```
### Time-Skipping Environment (Recommended for CI) ### Time-Skipping Environment (Recommended for CI)
```bash ```bash
uv run pytest --workflow-environment=time-skipping poetry run pytest --workflow-environment=time-skipping
``` ```
### External Temporal Server ### External Temporal Server
```bash ```bash
uv run pytest --workflow-environment=localhost:7233 poetry run pytest --workflow-environment=localhost:7233
``` ```
## Environment Variables ## Environment Variables
@@ -122,7 +122,7 @@ tests/test_tool_activities.py::TestToolActivities::test_get_wf_env_vars_default_
### Common Issues ### Common Issues
1. **Module not found errors**: Run `uv sync` 1. **Module not found errors**: Run `poetry install --with dev`
2. **Async warnings**: These are expected with pytest-asyncio and can be ignored 2. **Async warnings**: These are expected with pytest-asyncio and can be ignored
3. **Test timeouts**: Use `--workflow-environment=time-skipping` for faster execution 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 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: Enable verbose logging:
```bash ```bash
uv run pytest --log-cli-level=DEBUG -s poetry run pytest --log-cli-level=DEBUG -s
``` ```
Run with coverage: Run with coverage:
```bash ```bash
uv run pytest --cov=workflows --cov=activities poetry run pytest --cov=workflows --cov=activities
``` ```
## Continuous Integration ## Continuous Integration
For CI environments, use: For CI environments, use:
```bash ```bash
uv run pytest --workflow-environment=time-skipping --tb=short poetry run pytest --workflow-environment=time-skipping --tb=short
``` ```
## Additional Resources ## Additional Resources

View File

@@ -23,8 +23,6 @@
[ ] enable user to list agents at any time - like end conversation - probably with a next step<br /> [ ] 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 ## Ideas for more goals and tools
[ ] Add fintech goals <br /> [ ] Add fintech goals <br />

View File

@@ -6,10 +6,6 @@ import { apiService } from "../services/api";
const POLL_INTERVAL = 600; // 0.6 seconds const POLL_INTERVAL = 600; // 0.6 seconds
const INITIAL_ERROR_STATE = { visible: false, message: '' }; const INITIAL_ERROR_STATE = { visible: false, message: '' };
const DEBOUNCE_DELAY = 300; // 300ms debounce for user input 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) { function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value); const [debouncedValue, setDebouncedValue] = useState(value);
@@ -43,34 +39,13 @@ export default function App() {
const debouncedUserInput = useDebounce(userInput, DEBOUNCE_DELAY); const debouncedUserInput = useDebounce(userInput, DEBOUNCE_DELAY);
const errorTimerRef = useRef(null); const errorTimerRef = useRef(null);
const conversationFetchErrorCountRef = useRef(0);
const handleError = useCallback((error, context) => { const handleError = useCallback((error, context) => {
console.error(`${context}:`, error); console.error(`${context}:`, error);
const isConversationFetchError = const isConversationFetchError = error.status === 404;
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 const errorMessage = isConversationFetchError
? "Error fetching conversation. Retrying..." ? "Error fetching conversation. Retrying..." // Updated message
: `Error ${context.toLowerCase()}. Please try again.`; : `Error ${context.toLowerCase()}. Please try again.`;
setError(prevError => { setError(prevError => {
@@ -97,7 +72,6 @@ export default function App() {
if (errorTimerRef.current) { if (errorTimerRef.current) {
clearTimeout(errorTimerRef.current); clearTimeout(errorTimerRef.current);
} }
conversationFetchErrorCountRef.current = 0;
setError(INITIAL_ERROR_STATE); setError(INITIAL_ERROR_STATE);
}, []); }, []);

View File

@@ -1,17 +1,5 @@
const API_BASE_URL = 'http://127.0.0.1:8000'; 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 { class ApiError extends Error {
constructor(message, status) { constructor(message, status) {
super(message); super(message);
@@ -31,31 +19,12 @@ async function handleResponse(response) {
return response.json(); 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 = { export const apiService = {
async getConversationHistory() { async getConversationHistory() {
try { try {
const res = await fetchWithTimeout(`${API_BASE_URL}/get-conversation-history`); const res = await fetch(`${API_BASE_URL}/get-conversation-history`);
return handleResponse(res); return handleResponse(res);
} catch (error) { } catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError( throw new ApiError(
'Failed to fetch conversation history', 'Failed to fetch conversation history',
error.status || 500 error.status || 500
@@ -69,7 +38,7 @@ export const apiService = {
} }
try { try {
const res = await fetchWithTimeout( const res = await fetch(
`${API_BASE_URL}/send-prompt?prompt=${encodeURIComponent(message)}`, `${API_BASE_URL}/send-prompt?prompt=${encodeURIComponent(message)}`,
{ {
method: 'POST', method: 'POST',
@@ -80,9 +49,6 @@ export const apiService = {
); );
return handleResponse(res); return handleResponse(res);
} catch (error) { } catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError( throw new ApiError(
'Failed to send message', 'Failed to send message',
error.status || 500 error.status || 500
@@ -92,7 +58,7 @@ export const apiService = {
async startWorkflow() { async startWorkflow() {
try { try {
const res = await fetchWithTimeout( const res = await fetch(
`${API_BASE_URL}/start-workflow`, `${API_BASE_URL}/start-workflow`,
{ {
method: 'POST', method: 'POST',
@@ -103,9 +69,6 @@ export const apiService = {
); );
return handleResponse(res); return handleResponse(res);
} catch (error) { } catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError( throw new ApiError(
'Failed to start workflow', 'Failed to start workflow',
error.status || 500 error.status || 500
@@ -115,7 +78,7 @@ export const apiService = {
async confirm() { async confirm() {
try { try {
const res = await fetchWithTimeout(`${API_BASE_URL}/confirm`, { const res = await fetch(`${API_BASE_URL}/confirm`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -123,9 +86,6 @@ export const apiService = {
}); });
return handleResponse(res); return handleResponse(res);
} catch (error) { } catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError( throw new ApiError(
'Failed to confirm action', 'Failed to confirm action',
error.status || 500 error.status || 500

View File

@@ -5,6 +5,5 @@ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
open: true, open: true,
host: process.env.VITE_HOST ?? 'localhost',
}, },
}); });

View File

@@ -27,7 +27,7 @@ goal_food_ordering = AgentGoal(
"When they express interest in items, get pricing using list_prices. " "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. " "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. " "After they're done selecting items, get their customer details and create a Stripe customer. "
"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), " "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.", "3) finalize_invoice. The finalized invoice will contain a hosted_invoice_url for payment.",
starter_prompt=starter_prompt_generic, starter_prompt=starter_prompt_generic,
example_conversation_history="\n ".join( example_conversation_history="\n ".join(
@@ -59,8 +59,8 @@ goal_food_ordering = AgentGoal(
"agent: Thank you Jane! Creating your Stripe customer profile with name and email.", "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'>", "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"}}', '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 with payment due in 7 days.", "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', days_until_due=7>", "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"}}', 'tool_result: {"invoice": {"id": "in_InvOicE54321", "status": "draft", "customer": "cus_ExAmPlE12345"}}',
"agent: Invoice created! Now adding your first Pepperoni Pizza to the invoice.", "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'>", "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 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +1,57 @@
[project] [tool.poetry]
name = "temporal_AI_agent" name = "temporal_AI_agent"
version = "0.2.0" version = "0.2.0"
description = "Temporal AI Agent" description = "Temporal AI Agent"
authors = [
{ 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"
license = "MIT" license = "MIT"
dependencies = [ authors = [
"temporalio>=1.8.0,<2", "Steve Androulakis <steve.androulakis@temporal.io>",
"litellm>=1.70.0,<2", "Laine Smith <lainecaseysmith@gmail.com>",
"pyyaml>=6.0.2,<7", "Joshua Smith <josh.smith@temporal.io>"
"fastapi>=0.115.6,<0.116", ]
"uvicorn>=0.34.0,<0.35", readme = "README.md"
"python-dotenv>=1.0.1,<2",
"requests>=2.32.3,<3", # By default, Poetry will find packages automatically,
"pandas>=2.2.3,<3", # but explicitly including them is fine:
"stripe>=11.4.1,<12", packages = [
"gtfs-kit>=10.1.1,<11", { include = "**/*.py", from = "." }
"fastmcp>=2.7.0,<3",
] ]
[project.urls] [tool.poetry.urls]
"Bug Tracker" = "https://github.com/temporal-community/temporal-ai-agent/issues" "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] [tool.poe.tasks]
format = [{cmd = "black ."}, {cmd = "isort ."}] format = [{cmd = "black ."}, {cmd = "isort ."}]
lint = [{cmd = "black --check ."}, {cmd = "isort --check-only ."}, {ref = "lint-types" }] lint = [{cmd = "black --check ."}, {cmd = "isort --check-only ."}, {ref = "lint-types" }]
lint-types = "mypy --check-untyped-defs --namespace-packages ." lint-types = "mypy --check-untyped-defs --namespace-packages ."
test = "pytest" test = "pytest"
[tool.hatch.metadata] [tool.poetry.dependencies]
allow-direct-references = true python = ">=3.10,<4.0"
temporalio = "^1.8.0"
[tool.hatch.build] # Standard library modules (e.g. asyncio, collections) don't need to be added
packages = ["activities", "api", "goals", "models", "prompts", "shared", "tools", "workflows"] # 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"
[build-system] [build-system]
requires = ["hatchling"] requires = ["poetry-core>=1.4.0"]
build-backend = "hatchling.build" build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto" asyncio_mode = "auto"

View File

@@ -53,31 +53,31 @@ Provides shared test fixtures and configuration:
Ensure you have the required dependencies installed: Ensure you have the required dependencies installed:
```bash ```bash
uv sync poetry install --with dev
``` ```
### Basic Test Execution ### Basic Test Execution
Run all tests: Run all tests:
```bash ```bash
uv run pytest poetry run pytest
``` ```
Run specific test files: Run specific test files:
```bash ```bash
# Workflow tests only # Workflow tests only
uv run pytest tests/test_agent_goal_workflow.py poetry run pytest tests/test_agent_goal_workflow.py
# Activity tests only # Activity tests only
uv run pytest tests/test_tool_activities.py poetry run pytest tests/test_tool_activities.py
# Legacy tests # Legacy tests
uv run pytest tests/workflowtests/ poetry run pytest tests/workflowtests/
``` ```
Run with verbose output: Run with verbose output:
```bash ```bash
uv run pytest -v poetry run pytest -v
``` ```
### Test Environment Options ### Test Environment Options
@@ -87,34 +87,34 @@ The tests support different Temporal environments via the `--workflow-environmen
#### Local Environment (Default) #### Local Environment (Default)
Uses a local Temporal test server: Uses a local Temporal test server:
```bash ```bash
uv run pytest --workflow-environment=local poetry run pytest --workflow-environment=local
``` ```
#### Time-Skipping Environment #### Time-Skipping Environment
Uses Temporal's time-skipping test environment for faster execution: Uses Temporal's time-skipping test environment for faster execution:
```bash ```bash
uv run pytest --workflow-environment=time-skipping poetry run pytest --workflow-environment=time-skipping
``` ```
#### External Server #### External Server
Connect to an existing Temporal server: Connect to an existing Temporal server:
```bash ```bash
uv run pytest --workflow-environment=localhost:7233 poetry run pytest --workflow-environment=localhost:7233
``` ```
#### Setup Script for AI Agent environments such as OpenAI Codex #### Setup Script for AI Agent environments such as OpenAI Codex
```bash ```bash
export SHELL=/bin/bash export SHELL=/bin/bash
curl -LsSf https://astral.sh/uv/install.sh | sh curl -sSL https://install.python-poetry.org | python3 -
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
ls ls
uv sync poetry install --with dev
cd frontend cd frontend
npm install npm install
cd .. cd ..
# Pre-download the temporal test server binary # Pre-download the temporal test server binary
uv run python -c " poetry run python3 -c "
import asyncio import asyncio
import sys import sys
from temporalio.testing import WorkflowEnvironment from temporalio.testing import WorkflowEnvironment
@@ -139,22 +139,22 @@ asyncio.run(predownload())
Run tests by pattern: Run tests by pattern:
```bash ```bash
# Run only validation tests # Run only validation tests
uv run pytest -k "validation" poetry run pytest -k "validation"
# Run only workflow tests # Run only workflow tests
uv run pytest -k "workflow" poetry run pytest -k "workflow"
# Run only activity tests # Run only activity tests
uv run pytest -k "activity" poetry run pytest -k "activity"
``` ```
Run tests by marker (if you add custom markers): Run tests by marker (if you add custom markers):
```bash ```bash
# Run only integration tests # Run only integration tests
uv run pytest -m integration poetry run pytest -m integration
# Skip slow tests # Skip slow tests
uv run pytest -m "not slow" poetry run pytest -m "not slow"
``` ```
## Test Configuration ## Test Configuration
@@ -276,7 +276,7 @@ The `sample_combined_input` fixture provides:
Enable detailed logging: Enable detailed logging:
```bash ```bash
uv run pytest --log-cli-level=DEBUG -s poetry run pytest --log-cli-level=DEBUG -s
``` ```
### Temporal Web UI ### Temporal Web UI
@@ -301,18 +301,21 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: astral-sh/setup-uv@v5 - uses: actions/setup-python@v4
- run: uv sync with:
- run: uv run pytest --workflow-environment=time-skipping python-version: '3.10'
- run: pip install poetry
- run: poetry install --with dev
- run: poetry run pytest --workflow-environment=time-skipping
``` ```
### Test Coverage ### Test Coverage
Generate coverage reports: Generate coverage reports:
```bash ```bash
uv add --group dev pytest-cov poetry add --group dev pytest-cov
uv run pytest --cov=workflows --cov=activities --cov-report=html poetry run pytest --cov=workflows --cov=activities --cov-report=html
``` ```
## Best Practices ## Best Practices
@@ -339,7 +342,7 @@ uv run pytest --cov=workflows --cov=activities --cov-report=html
- Check Temporal Python SDK documentation - Check Temporal Python SDK documentation
- Review existing test patterns in the codebase - Review existing test patterns in the codebase
- Use `uv run pytest --collect-only` to verify test discovery - Use `poetry run pytest --collect-only` to verify test discovery
- Run with `-v` flag for detailed output - Run with `-v` flag for detailed output
## Legacy Tests ## Legacy Tests

View File

@@ -312,109 +312,6 @@ async def test_mcp_tool_execution_flow(client: Client):
assert captured["dynamic_args"]["server_definition"]["name"] == server_def.name 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 @pytest.mark.asyncio
async def test_mcp_tool_failure_recorded(client: Client): async def test_mcp_tool_failure_recorded(client: Client):
"""Failure of an MCP tool should be recorded in conversation history.""" """Failure of an MCP tool should be recorded in conversation history."""

View File

@@ -47,7 +47,7 @@ def create_invoice(args: dict) -> dict:
stripe.InvoiceItem.create( stripe.InvoiceItem.create(
customer=customer_id, customer=customer_id,
amount=amount_cents, amount=amount_cents,
currency="usd", currency="gbp",
description=args.get("tripDetails", "Service Invoice"), description=args.get("tripDetails", "Service Invoice"),
) )

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,9 @@
import calendar
import json import json
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any
def find_events(args: dict[str, Any]) -> dict[str, Any]: def find_events(args: dict) -> dict:
"""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_city = args.get("city", "").lower()
search_month = args.get("month", "").capitalize() search_month = args.get("month", "").capitalize()
@@ -28,33 +16,36 @@ def find_events(args: dict[str, Any]) -> dict[str, Any]:
except ValueError: except ValueError:
return {"error": "Invalid month provided."} return {"error": "Invalid month provided."}
# Determine the target year: use next upcoming occurrence of the month # Helper to wrap months into [1..12]
today = datetime.now() def get_adjacent_months(m):
if month_number >= today.month: prev_m = 12 if m == 1 else (m - 1)
target_year = today.year next_m = 1 if m == 12 else (m + 1)
else: return [prev_m, m, next_m]
target_year = today.year + 1
# Build the search month date range valid_months = get_adjacent_months(month_number)
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 = [] matching_events = []
with open(file_path) as f: for city_name, events in json.load(open(file_path)).items():
data = json.load(f)
for city_name, events in data.items():
if search_city and search_city not in city_name.lower(): if search_city and search_city not in city_name.lower():
continue continue
for event in events: for event in events:
event_start = datetime.strptime(event["dateFrom"], "%Y-%m-%d") date_from = datetime.strptime(event["dateFrom"], "%Y-%m-%d")
event_end = datetime.strptime(event["dateTo"], "%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"
# 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( matching_events.append(
{ {
"city": city_name, "city": city_name,
@@ -62,10 +53,12 @@ def find_events(args: dict[str, Any]) -> dict[str, Any]:
"dateFrom": event["dateFrom"], "dateFrom": event["dateFrom"],
"dateTo": event["dateTo"], "dateTo": event["dateTo"],
"description": event["description"], "description": event["description"],
"month": month_context,
} }
) )
# Add top-level metadata if you wish
return { return {
"note": f"Returning events that overlap with {search_month} {target_year}.", "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)}).",
"events": matching_events, "events": matching_events,
} }

View File

@@ -180,9 +180,10 @@ search_fixtures_tool = ToolDefinition(
find_events_tool = ToolDefinition( find_events_tool = ToolDefinition(
name="FindEvents", name="FindEvents",
description="Find upcoming events to travel to a given city (e.g., 'Melbourne') and a month. " description="Find upcoming events to travel to a given city (e.g., 'Melbourne') and a date or month. "
"It knows about events in Oceania only (e.g. major Australian and New Zealand cities). " "It knows about events in Oceania only (e.g. major Australian and New Zealand cities). "
"Returns events that overlap with the specified month. ", "It will search 1 month either side of the month provided. "
"Returns a list of events. ",
arguments=[ arguments=[
ToolArgument( ToolArgument(
name="city", name="city",
@@ -192,7 +193,7 @@ find_events_tool = ToolDefinition(
ToolArgument( ToolArgument(
name="month", name="month",
type="string", type="string",
description="The month to search for events (e.g., 'April')", description="The month to search for events (will search 1 month either side of the month provided)",
), ),
], ],
) )

2401
uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ from temporalio.common import RetryPolicy
from temporalio.exceptions import ActivityError from temporalio.exceptions import ActivityError
from models.data_types import ConversationHistory, ToolPromptInput from models.data_types import ConversationHistory, ToolPromptInput
from models.tool_definitions import AgentGoal, ToolDefinition from models.tool_definitions import AgentGoal
from prompts.agent_prompt_generators import ( from prompts.agent_prompt_generators import (
generate_missing_args_prompt, generate_missing_args_prompt,
generate_tool_completion_prompt, generate_tool_completion_prompt,
@@ -21,19 +21,63 @@ LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT = timedelta(minutes=30)
def is_mcp_tool(tool_name: str, goal: AgentGoal) -> bool: def is_mcp_tool(tool_name: str, goal: AgentGoal) -> bool:
"""Check if a tool should be dispatched via MCP.""" """Check if a tool is an MCP tool based on the goal's MCP server definition"""
if not goal.mcp_server_definition: if not goal.mcp_server_definition:
return False return False
# Native tools are registered with tools.get_handler. If lookup succeeds, # Check if the tool name matches any MCP tools that were loaded
# the tool should execute locally; otherwise treat it as MCP-provided. # We can identify MCP tools by checking if they're not in the original static tools
from tools import get_handler 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,
)
try: static_tool_names = {
get_handler(tool_name) list_agents_tool.name,
return False change_goal_tool.name,
except ValueError: give_hint_tool.name,
return True guess_location_tool.name,
search_flights_tool.name,
search_trains_tool.name,
book_trains_tool.name,
create_invoice_tool.name,
search_fixtures_tool.name,
find_events_tool.name,
current_pto_tool.name,
future_pto_calc_tool.name,
book_pto_tool.name,
paycheck_bank_integration_status_check.name,
financial_check_account_is_valid.name,
financial_get_account_balances.name,
financial_move_money.name,
financial_submit_loan_approval.name,
ecomm_list_orders.name,
ecomm_get_order.name,
ecomm_track_package.name,
food_add_to_cart_tool.name,
}
return tool_name not in static_tool_names
async def handle_tool_execution( async def handle_tool_execution(
@@ -54,13 +98,6 @@ async def handle_tool_execution(
# Add server definition to args for MCP tools # Add server definition to args for MCP tools
mcp_args = tool_data["args"].copy() 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 mcp_args["server_definition"] = goal.mcp_server_definition
dynamic_result = await workflow.execute_activity( dynamic_result = await workflow.execute_activity(