Compare commits
62 Commits
review-jos
...
0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1811e4cf59 | ||
|
|
157c337d23 | ||
|
|
e52ddd3e5e | ||
|
|
eb06cf5c8d | ||
|
|
e35181b5ad | ||
|
|
f7ef2b1c7e | ||
|
|
71e54b9ecd | ||
|
|
a7a2002217 | ||
|
|
5a3bfbd848 | ||
|
|
7bb6688797 | ||
|
|
847f4bbaef | ||
|
|
f8e0dd3b2a | ||
|
|
2f3afd6954 | ||
|
|
edb7df5b3c | ||
|
|
05041f9433 | ||
|
|
0767533ca1 | ||
|
|
611a6b6238 | ||
|
|
7850af6be2 | ||
|
|
a29b100017 | ||
|
|
a5fc6ad477 | ||
|
|
c9ae8b29bf | ||
|
|
7ef10e2481 | ||
|
|
183f834635 | ||
|
|
47c4b99f2c | ||
|
|
823208db3c | ||
|
|
b4aa929451 | ||
|
|
b4d57cfad6 | ||
|
|
2a1624f621 | ||
|
|
326a5a5beb | ||
|
|
163477c066 | ||
|
|
d48dafcaa5 | ||
|
|
32e856e494 | ||
|
|
a07af3e7bf | ||
|
|
fc07315358 | ||
|
|
ebb12feafa | ||
|
|
7b2c7cef8f | ||
|
|
4410f30642 | ||
|
|
77942b19a1 | ||
|
|
247bac0a28 | ||
|
|
cf55f0eaee | ||
|
|
0c678a120a | ||
|
|
6383614076 | ||
|
|
92bce11151 | ||
|
|
e5405907fa | ||
|
|
dddf7f4ccd | ||
|
|
6f245a1998 | ||
|
|
86a6dfe991 | ||
|
|
83c6a2454d | ||
|
|
463ae581ac | ||
|
|
e62b105872 | ||
|
|
7b52b8a817 | ||
|
|
6f9079ba12 | ||
|
|
ac44d35acb | ||
|
|
50463170d2 | ||
|
|
13d0085882 | ||
|
|
e92e3f43c9 | ||
|
|
812e295f3a | ||
|
|
dddd2977b9 | ||
|
|
4e0ff0e535 | ||
|
|
aba934e8b4 | ||
|
|
2539436a79 | ||
|
|
6517b624ee |
27
.env.example
@@ -1,26 +1,13 @@
|
|||||||
RAPIDAPI_KEY=9df2cb5...
|
RAPIDAPI_KEY=9df2cb5...
|
||||||
RAPIDAPI_HOST=sky-scrapper.p.rapidapi.com
|
RAPIDAPI_HOST_FLIGHTS=sky-scrapper.p.rapidapi.com #For travel flight information tool
|
||||||
FOOTBALL_DATA_API_KEY=....
|
RAPIDAPI_HOST_PACKAGE=trackingpackage.p.rapidapi.com #For eCommerce order status package tracking tool
|
||||||
|
FOOTBALL_DATA_API_KEY=
|
||||||
|
# Leave blank to use the built-in mock fixtures generator
|
||||||
|
|
||||||
STRIPE_API_KEY=sk_test_51J...
|
STRIPE_API_KEY=sk_test_51J...
|
||||||
|
|
||||||
LLM_PROVIDER=openai # default
|
LLM_MODEL=openai/gpt-4o # default
|
||||||
OPENAI_API_KEY=sk-proj-...
|
LLM_KEY=sk-proj-...
|
||||||
# or
|
|
||||||
#LLM_PROVIDER=grok
|
|
||||||
#GROK_API_KEY=xai-your-grok-api-key
|
|
||||||
# or
|
|
||||||
# LLM_PROVIDER=ollama
|
|
||||||
# OLLAMA_MODEL_NAME=qwen2.5:14b
|
|
||||||
# or
|
|
||||||
# LLM_PROVIDER=google
|
|
||||||
# GOOGLE_API_KEY=your-google-api-key
|
|
||||||
# or
|
|
||||||
# LLM_PROVIDER=anthropic
|
|
||||||
# ANTHROPIC_API_KEY=your-anthropic-api-key
|
|
||||||
# or
|
|
||||||
# LLM_PROVIDER=deepseek
|
|
||||||
# DEEPSEEK_API_KEY=your-deepseek-api-key
|
|
||||||
|
|
||||||
|
|
||||||
# uncomment and unset these environment variables to connect to the local dev server
|
# uncomment and unset these environment variables to connect to the local dev server
|
||||||
@@ -42,7 +29,7 @@ AGENT_GOAL=goal_choose_agent_type # for multi-goal start
|
|||||||
|
|
||||||
#Choose which category(ies) of goals you want to be listed by the Agent Goal picker if enabled above
|
#Choose which category(ies) of goals you want to be listed by the Agent Goal picker if enabled above
|
||||||
# - options are system (always included), hr, travel, or all.
|
# - options are system (always included), hr, travel, or all.
|
||||||
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin # default is all
|
GOAL_CATEGORIES=fin # default is all
|
||||||
#GOAL_CATEGORIES=travel-flights
|
#GOAL_CATEGORIES=travel-flights
|
||||||
|
|
||||||
# Set if the workflow should wait for the user to click a confirm button (and if the UI should show the confirm button and tool args)
|
# Set if the workflow should wait for the user to click a confirm button (and if the UI should show the confirm button and tool args)
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -32,4 +32,7 @@ coverage.xml
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
.env
|
.env
|
||||||
*.env
|
.env*
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
.cursor
|
||||||
175
AGENTS.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Temporal AI Agent Contribution Guide
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
- `workflows/` - Temporal workflows including the main AgentGoalWorkflow for multi-turn AI conversations
|
||||||
|
- `activities/` - Temporal activities for tool execution and LLM interactions
|
||||||
|
- `tools/` - AI agent tools organized by category (finance, HR, ecommerce, travel, etc.)
|
||||||
|
- `models/` - Data types and tool definitions used throughout the system
|
||||||
|
- `prompts/` - Agent prompt generators and templates
|
||||||
|
- `api/` - FastAPI server that exposes REST endpoints to interact with workflows
|
||||||
|
- `frontend/` - React-based web UI for chatting with the AI agent
|
||||||
|
- `tests/` - Comprehensive test suite for workflows and activities using Temporal's testing framework
|
||||||
|
- `enterprise/` - .NET worker implementation for enterprise activities (train booking)
|
||||||
|
- `scripts/` - Utility scripts for running workers and testing tools
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Quick Start with Docker
|
||||||
|
```bash
|
||||||
|
# Start all services with development hot-reload
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Quick rebuild without infrastructure
|
||||||
|
docker compose up -d --no-deps --build api worker frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Default URLs:
|
||||||
|
- Temporal UI: http://localhost:8080
|
||||||
|
- API: http://localhost:8000
|
||||||
|
- Frontend: http://localhost:5173
|
||||||
|
|
||||||
|
### Local Development Setup
|
||||||
|
|
||||||
|
1. **Prerequisites:**
|
||||||
|
```bash
|
||||||
|
# Install Poetry for Python dependency management
|
||||||
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
|
# Start Temporal server (Mac)
|
||||||
|
brew install temporal
|
||||||
|
temporal server start-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Backend (Python):**
|
||||||
|
```bash
|
||||||
|
# Quick setup using Makefile
|
||||||
|
make setup # Creates venv and installs dependencies
|
||||||
|
make run-worker # Starts the Temporal worker
|
||||||
|
make run-api # Starts the API server
|
||||||
|
|
||||||
|
# Or manually:
|
||||||
|
poetry install
|
||||||
|
poetry run python scripts/run_worker.py # In one terminal
|
||||||
|
poetry run uvicorn api.main:app --reload # In another terminal
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Frontend (React):**
|
||||||
|
```bash
|
||||||
|
make run-frontend # Using Makefile
|
||||||
|
|
||||||
|
# Or manually:
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npx vite
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Enterprise .NET Worker (optional):**
|
||||||
|
```bash
|
||||||
|
make run-enterprise # Using Makefile
|
||||||
|
|
||||||
|
# Or manually:
|
||||||
|
cd enterprise
|
||||||
|
dotnet build
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
Copy `.env.example` to `.env` and configure:
|
||||||
|
```bash
|
||||||
|
# Required: LLM Configuration
|
||||||
|
LLM_MODEL=openai/gpt-4o # or anthropic/claude-3-sonnet, etc.
|
||||||
|
LLM_KEY=your-api-key-here
|
||||||
|
|
||||||
|
# Optional: Agent Goals and Categories
|
||||||
|
AGENT_GOAL=goal_choose_agent_type
|
||||||
|
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin
|
||||||
|
|
||||||
|
# Optional: Tool-specific APIs
|
||||||
|
STRIPE_API_KEY=sk_test_... # For invoice creation
|
||||||
|
FOOTBALL_DATA_API_KEY=... # For real football fixtures
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The project includes comprehensive tests using Temporal's testing framework:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install test dependencies
|
||||||
|
poetry install --with dev
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
poetry run pytest
|
||||||
|
|
||||||
|
# Run with time-skipping for faster execution
|
||||||
|
poetry run pytest --workflow-environment=time-skipping
|
||||||
|
|
||||||
|
# Run specific test categories
|
||||||
|
poetry run pytest tests/test_tool_activities.py -v # Activity tests
|
||||||
|
poetry run pytest tests/test_agent_goal_workflow.py -v # Workflow tests
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
poetry run pytest --cov=workflows --cov=activities
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Coverage:**
|
||||||
|
- ✅ **Workflow Tests**: AgentGoalWorkflow signals, queries, state management
|
||||||
|
- ✅ **Activity Tests**: ToolActivities, LLM integration (mocked), environment configuration
|
||||||
|
- ✅ **Integration Tests**: End-to-end workflow and activity execution
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- **Quick Start**: [TESTING.md](TESTING.md) - Simple commands to run tests
|
||||||
|
- **Comprehensive Guide**: [tests/README.md](tests/README.md) - Detailed testing patterns and best practices
|
||||||
|
|
||||||
|
## Linting and Code Quality
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Poetry tasks
|
||||||
|
poetry run poe format # Format code with black and isort
|
||||||
|
poetry run poe lint # Check code style and types
|
||||||
|
poetry run poe test # Run test suite
|
||||||
|
|
||||||
|
# Manual commands
|
||||||
|
poetry run black .
|
||||||
|
poetry run isort .
|
||||||
|
poetry run mypy --check-untyped-defs --namespace-packages .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Customization
|
||||||
|
|
||||||
|
### Adding New Tools
|
||||||
|
1. Create tool implementation in `tools/` directory
|
||||||
|
2. Add tool function mapping in `tools/__init__.py`
|
||||||
|
3. Register tool definition in `tools/tool_registry.py`
|
||||||
|
4. Associate with goals in `tools/goal_registry.py`
|
||||||
|
|
||||||
|
### Configuring Goals
|
||||||
|
The agent supports multiple goal categories:
|
||||||
|
- **Financial**: Money transfers, loan applications (`fin/`)
|
||||||
|
- **HR**: PTO booking, payroll status (`hr/`)
|
||||||
|
- **Travel**: Flight/train booking, event finding
|
||||||
|
- **Ecommerce**: Order tracking, package management (`ecommerce/`)
|
||||||
|
|
||||||
|
See [adding-goals-and-tools.md](adding-goals-and-tools.md) for detailed customization guide.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This system implements "Agentic AI" with these key components:
|
||||||
|
1. **Goals** - High-level objectives accomplished through tool sequences
|
||||||
|
2. **Agent Loops** - LLM execution → tool calls → human input → repeat until goal completion
|
||||||
|
3. **Tool Approval** - Human confirmation for sensitive operations
|
||||||
|
4. **Conversation Management** - LLM-powered input validation and history summarization
|
||||||
|
5. **Durability** - Temporal workflows ensure reliable execution across failures
|
||||||
|
|
||||||
|
For detailed architecture information, see [architecture.md](architecture.md).
|
||||||
|
|
||||||
|
## Commit Messages and Pull Requests
|
||||||
|
- Use clear commit messages describing the change purpose
|
||||||
|
- Reference specific files and line numbers when relevant (e.g., `workflows/agent_goal_workflow.py:125`)
|
||||||
|
- Open PRs describing **what changed** and **why**
|
||||||
|
- Ensure tests pass before submitting: `poetry run pytest --workflow-environment=time-skipping`
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
- **Setup Guide**: [SETUP.md](SETUP.md) - Detailed configuration instructions
|
||||||
|
- **Architecture Decisions**: [architecture-decisions.md](architecture-decisions.md) - Why Temporal for AI agents
|
||||||
|
- **Demo Video**: [5-minute YouTube overview](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
||||||
|
- **Multi-Agent Demo**: [Advanced multi-agent execution](https://www.youtube.com/watch?v=8Dc_0dC14yY)
|
||||||
30
CHANGELOG.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.2.0] - 2025-04-24
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Multi‑goal agent architecture** with dynamic goal switching (`goal_choose_agent_type`, `ListAgents`, `ChangeGoal`).
|
||||||
|
- See [the architecture guide](./architecture.md) and [setup guide](./SETUP.md).
|
||||||
|
- **New goal categories & agents**: HR PTO scheduling/checking, paycheck integration, Financial (balances, money movement, loan application), E‑commerce order tracking.
|
||||||
|
- See [the guide for adding goals and tools](./adding-goals-and-tools.md).
|
||||||
|
- **Force Confirmation**: `SHOW_CONFIRM` will show a confirmation box before allowing the agent to run a tool.
|
||||||
|
- **Grok (`x.ai`) LLM provider** support via `GROK_API_KEY`.
|
||||||
|
- Extensive **docs**: `SETUP.md`, `architecture.md`, `architecture-decisions.md`, `adding-goals-and-tools.md`, plus new diagrams & assets.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **UI Confirmation Box** is less 'debug' looking and prettier.
|
||||||
|
- Package renamed to **`temporal_AI_agent`** and version bumped to **0.2.0** in `pyproject.toml`.
|
||||||
|
- Environment variables changed (see `.env_example`): (`RAPIDAPI_HOST_*`, `AGENT_GOAL` defaults, `GOAL_CATEGORIES`, `SHOW_CONFIRM`, `FIN_START_REAL_WORKFLOW`).
|
||||||
|
|
||||||
|
## [0.1.0] - 2025-01-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Initial release** of the Temporal AI Agent demo.
|
||||||
|
- **Single goal agent** architecture with a single goal and agent type.
|
||||||
|
- This is the agent demoed in the [YouTube video](https://www.youtube.com/watch?v=GEXllEH2XiQ).
|
||||||
|
|
||||||
|
[0.2.0]: https://github.com/temporal-community/temporal-ai-agent/pull/29
|
||||||
106
CONTRIBUTING.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Contributing to the Temporal AI Agent Project
|
||||||
|
|
||||||
|
This document provides guidelines for contributing to `temporal-ai-agent`. All setup and installation instructions can be found in [./SETUP.md](./SETUP.md).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Code Style & Formatting
|
||||||
|
We use `black` for code formatting and `isort` for import sorting to maintain a consistent codebase.
|
||||||
|
- **Format code:**
|
||||||
|
```bash
|
||||||
|
poetry run poe format
|
||||||
|
```
|
||||||
|
Or manually:
|
||||||
|
```bash
|
||||||
|
poetry run black .
|
||||||
|
poetry run isort .
|
||||||
|
```
|
||||||
|
Please format your code before committing.
|
||||||
|
|
||||||
|
### Linting & Type Checking
|
||||||
|
We use `mypy` for static type checking and other linters configured via `poe the poet`.
|
||||||
|
- **Run linters and type checks:**
|
||||||
|
```bash
|
||||||
|
poetry run poe lint
|
||||||
|
```
|
||||||
|
Or manually for type checking:
|
||||||
|
```bash
|
||||||
|
poetry run mypy --check-untyped-defs --namespace-packages .
|
||||||
|
```
|
||||||
|
Ensure all linting and type checks pass before submitting a pull request.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
Comprehensive testing is crucial for this project. We use `pytest` and Temporal's testing framework.
|
||||||
|
- **Install test dependencies** (if not already done with `poetry install --with dev`):
|
||||||
|
```bash
|
||||||
|
poetry install --with dev
|
||||||
|
```
|
||||||
|
- **Run all tests:**
|
||||||
|
```bash
|
||||||
|
poetry run pytest
|
||||||
|
```
|
||||||
|
- **Run tests with time-skipping (recommended for faster execution, especially in CI):**
|
||||||
|
```bash
|
||||||
|
poetry run pytest --workflow-environment=time-skipping
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed information on test categories, running specific tests, test environments, coverage, and troubleshooting, please refer to:
|
||||||
|
- [TESTING.md](./TESTING.md) (Quick Start and overview)
|
||||||
|
- [tests/README.md](./tests/README.md) (Comprehensive guide, patterns, and best practices)
|
||||||
|
|
||||||
|
**Ensure all tests pass before submitting a pull request.**
|
||||||
|
|
||||||
|
## Making Changes
|
||||||
|
|
||||||
|
### Adding New Tools or Goals
|
||||||
|
If you're looking to extend the agent's capabilities:
|
||||||
|
1. Create your tool implementation in the `tools/` directory.
|
||||||
|
2. Register your tool and associate it with relevant goals.
|
||||||
|
For detailed instructions, please see:
|
||||||
|
- [Agent Customization in agents.md](./agents.md#agent-customization)
|
||||||
|
- [Adding Goals and Tools Guide](./adding-goals-and-tools.md)
|
||||||
|
|
||||||
|
### General Code Changes
|
||||||
|
- Follow the existing code style and patterns.
|
||||||
|
- Ensure any new code is well-documented with comments.
|
||||||
|
- Write new tests for new functionality or bug fixes.
|
||||||
|
- Update existing tests if necessary.
|
||||||
|
|
||||||
|
## Submitting Contributions
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
When you're ready to submit your changes:
|
||||||
|
1. Push your branch to the remote repository.
|
||||||
|
2. Open a Pull Request (PR) against the `main` branch.
|
||||||
|
3. **Describe your changes:** Clearly explain what you changed and why. Reference any related issues.
|
||||||
|
4. **Ensure tests pass:** All CI checks, including tests and linters, must pass. The command `poetry run pytest --workflow-environment=time-skipping` is a good one to run locally.
|
||||||
|
5. **Request review:** Request a review from one or more maintainers.
|
||||||
|
|
||||||
|
## Reporting Bugs
|
||||||
|
If you encounter a bug, please:
|
||||||
|
1. **Search existing issues:** Check if the bug has already been reported.
|
||||||
|
2. **Open a new issue:** If not, create a new issue.
|
||||||
|
- Provide a clear and descriptive title.
|
||||||
|
- Include steps to reproduce the bug.
|
||||||
|
- Describe the expected behavior and what actually happened.
|
||||||
|
- Provide details about your environment (OS, Python version, Temporal server version, etc.).
|
||||||
|
- Include any relevant logs or screenshots.
|
||||||
|
|
||||||
|
## Suggesting Enhancements
|
||||||
|
We welcome suggestions for new features or improvements!
|
||||||
|
1. **Search existing issues/discussions:** See if your idea has already been discussed.
|
||||||
|
2. **Open a new issue:**
|
||||||
|
- Use a clear and descriptive title.
|
||||||
|
- Provide a detailed explanation of the enhancement and its benefits.
|
||||||
|
- Explain the use case or problem it solves.
|
||||||
|
- Include any potential implementation ideas if you have them.
|
||||||
|
|
||||||
|
## Key Resources
|
||||||
|
- **Project Overview**: [README.md](./README.md)
|
||||||
|
- **Detailed Contribution & Development Guide**: [agents.md](./agents.md)
|
||||||
|
- **Setup Instructions**: [SETUP.md](./SETUP.md)
|
||||||
|
- **Comprehensive Testing Guide**: [TESTING.md](./TESTING.md) and [tests/README.md](./tests/README.md)
|
||||||
|
- **System Architecture**: [architecture.md](./architecture.md)
|
||||||
|
- **Architecture Decisions**: [architecture-decisions.md](./architecture-decisions.md)
|
||||||
|
- **Customizing Agent Tools and Goals**: [adding-goals-and-tools.md](./adding-goals-and-tools.md)
|
||||||
|
- **To-Do List / Future Enhancements**: [todo.md](./todo.md)
|
||||||
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends gcc build-essential && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
RUN pip install --no-cache-dir poetry
|
||||||
|
|
||||||
|
# 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 application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Set Python to run in unbuffered mode (recommended for Docker)
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
|
||||||
|
# Expose the port the app will run on
|
||||||
|
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"]
|
||||||
63
Makefile
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
.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
|
||||||
|
cd frontend && npm install
|
||||||
|
|
||||||
|
# Run commands
|
||||||
|
run-worker:
|
||||||
|
poetry run python scripts/run_worker.py
|
||||||
|
|
||||||
|
run-api:
|
||||||
|
poetry run uvicorn api.main:app --reload
|
||||||
|
|
||||||
|
run-frontend:
|
||||||
|
cd frontend && npx vite
|
||||||
|
|
||||||
|
run-train-api:
|
||||||
|
poetry run python thirdparty/train_api.py
|
||||||
|
|
||||||
|
run-legacy-worker:
|
||||||
|
poetry run python scripts/run_legacy_worker.py
|
||||||
|
|
||||||
|
run-enterprise:
|
||||||
|
cd enterprise && dotnet build && dotnet run
|
||||||
|
|
||||||
|
# Development environment setup
|
||||||
|
setup-temporal-mac:
|
||||||
|
brew install temporal
|
||||||
|
temporal server start-dev
|
||||||
|
|
||||||
|
# Run all development services
|
||||||
|
run-dev:
|
||||||
|
@echo "Starting all development services..."
|
||||||
|
@make run-worker & \
|
||||||
|
make run-api & \
|
||||||
|
make run-frontend & \
|
||||||
|
wait
|
||||||
|
|
||||||
|
# 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 run-worker - Start the Temporal worker"
|
||||||
|
@echo " make run-api - Start the API server"
|
||||||
|
@echo " make run-frontend - Start the frontend development server"
|
||||||
|
@echo " make run-train-api - Start the train API server"
|
||||||
|
@echo " make run-legacy-worker - Start the legacy worker"
|
||||||
|
@echo " make run-enterprise - Build and run the enterprise .NET worker"
|
||||||
|
@echo " make setup-temporal-mac - Install and start Temporal server on Mac"
|
||||||
|
@echo " make run-dev - Start all development services (worker, API, frontend) in parallel"
|
||||||
80
README.md
@@ -2,30 +2,43 @@
|
|||||||
|
|
||||||
This demo shows a multi-turn conversation with an AI agent running inside a Temporal workflow. The purpose of the agent is to collect information towards a goal, running tools along the way. There's a simple DSL input for collecting information (currently set up to use mock functions to search for public events, search for flights around those events, then create a test Stripe invoice for the trip).
|
This demo shows a multi-turn conversation with an AI agent running inside a Temporal workflow. The purpose of the agent is to collect information towards a goal, running tools along the way. There's a simple DSL input for collecting information (currently set up to use mock functions to search for public events, search for flights around those events, then create a test Stripe invoice for the trip).
|
||||||
|
|
||||||
The AI will respond with clarifications and ask for any missing information to that goal. You can configure it to use [ChatGPT 4o](https://openai.com/index/hello-gpt-4o/), [Anthropic Claude](https://www.anthropic.com/claude), [Google Gemini](https://gemini.google.com), [Deepseek-V3](https://www.deepseek.com/), [Grok](https://docs.x.ai/docs/overview) or a local LLM of your choice using [Ollama](https://ollama.com).
|
The AI will respond with clarifications and ask for any missing information to that goal. You can configure it to use any LLM supported by [LiteLLM](https://docs.litellm.ai/docs/providers), including:
|
||||||
|
- OpenAI models (GPT-4, GPT-3.5)
|
||||||
|
- Anthropic Claude models
|
||||||
|
- Google Gemini models
|
||||||
|
- Deepseek models
|
||||||
|
- Ollama models (local)
|
||||||
|
- And many more!
|
||||||
|
|
||||||
It's really helpful to [watch the demo (5 minute YouTube video)](https://www.youtube.com/watch?v=GEXllEH2XiQ) to understand how interaction works.
|
It's really helpful to [watch the demo (5 minute YouTube video)](https://www.youtube.com/watch?v=GEXllEH2XiQ) to understand how interaction works.
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
[](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
||||||
|
|
||||||
|
### Multi-Agent Demo Video
|
||||||
|
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 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 reliablity, state management, a code-first approach that we really like, built-in observability and easy error handling.
|
||||||
For more, check out [architecture-decisions](./architecture-decisions.md).
|
For more, check out [architecture-decisions](./architecture-decisions.md).
|
||||||
|
|
||||||
## What is "Agentic AI"?
|
## What is "Agentic AI"?
|
||||||
These are the key elements of an agentic framework:
|
These are the key elements of an agentic framework:
|
||||||
1. Goals a human can get done, made up of tools that can execute individual steps
|
1. Goals that a system can accomplish, made up of tools that can execute individual steps
|
||||||
2. The "agent loop" - call LLM, either call tools or prompt human, repeat until goal(s) are done
|
2. Agent loops - executing an LLM, executing tools, and eliciting input from an external source such as a human: repeat until goal(s) are done
|
||||||
3. Support for tool calls that require human input and approval
|
3. Support for tool calls that require input and approval
|
||||||
4. Use of an LLM to check human input for relevance before calling the 'real' LLM
|
4. Use of an LLM to check human input for relevance before calling the 'real' LLM
|
||||||
5. use of an LLM to summarize and compact the conversation history
|
5. Use of an LLM to summarize and compact the conversation history
|
||||||
6. Prompt construction (made of system prompts, conversation history, and tool metadata - sent to the LLM to create user prompts)
|
6. Prompt construction made of system prompts, conversation history, and tool metadata - sent to the LLM to create user questions and confirmations
|
||||||
7. Bonus: durable tool execution via Temporal Activities
|
7. Ideally high durability (done in this system with Temporal Workflow and Activities)
|
||||||
|
|
||||||
For a deeper dive into this, check out the [architecture guide](./architecture.md).
|
For a deeper dive into this, check out the [architecture guide](./architecture.md).
|
||||||
|
|
||||||
## Setup and Configuration
|
## Setup and Configuration
|
||||||
See [the Setup guide](./setup.md).
|
See [the Setup guide](./SETUP.md) for detailed instructions. The basic configuration requires just two environment variables:
|
||||||
|
```bash
|
||||||
|
LLM_MODEL=openai/gpt-4o # or any other model supported by LiteLLM
|
||||||
|
LLM_KEY=your-api-key-here
|
||||||
|
```
|
||||||
|
|
||||||
## Customizing Interaction & Tools
|
## Customizing Interaction & Tools
|
||||||
See [the guide to adding goals and tools](./adding-goals-and-tools.md).
|
See [the guide to adding goals and tools](./adding-goals-and-tools.md).
|
||||||
@@ -33,28 +46,47 @@ See [the guide to adding goals and tools](./adding-goals-and-tools.md).
|
|||||||
## Architecture
|
## Architecture
|
||||||
See [the architecture guide](./architecture.md).
|
See [the architecture guide](./architecture.md).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The project includes comprehensive tests for workflows and activities using Temporal's testing framework:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies including test dependencies
|
||||||
|
poetry install --with dev
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
poetry run pytest
|
||||||
|
|
||||||
|
# Run with time-skipping for faster execution
|
||||||
|
poetry run pytest --workflow-environment=time-skipping
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Coverage:**
|
||||||
|
- ✅ **Workflow Tests**: AgentGoalWorkflow signals, queries, state management
|
||||||
|
- ✅ **Activity Tests**: ToolActivities, LLM integration (mocked), environment configuration
|
||||||
|
- ✅ **Integration Tests**: End-to-end workflow and activity execution
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- **Quick Start**: [TESTING.md](TESTING.md) - Simple commands to run tests
|
||||||
|
- **Comprehensive Guide**: [tests/README.md](tests/README.md) - Detailed testing documentation, patterns, and best practices
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
To contribute to this project, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
Start the Temporal Server and API server, see [setup](SETUP.md)
|
||||||
|
|
||||||
## Productionalization & Adding Features
|
## Productionalization & Adding Features
|
||||||
- In a prod setting, I would need to ensure that payload data is stored separately (e.g. in S3 or a noSQL db - the claim-check pattern), or otherwise 'garbage collected'. Without these techniques, long conversations will fill up the workflow's conversation history, and start to breach Temporal event history payload limits.
|
- In a prod setting, I would need to ensure that payload data is stored separately (e.g. in S3 or a noSQL db - the claim-check pattern), or otherwise 'garbage collected'. Without these techniques, long conversations will fill up the workflow's conversation history, and start to breach Temporal event history payload limits.
|
||||||
- A single worker can easily support many workflows - setting workflow ID differently would enable this.
|
- A single worker can easily support many agent workflows (chats) running at the same time. Currently the workflow ID is the same each time, so it will only run one agent at a time. To run multiple agents, you can use a different workflow ID each time (e.g. by using a UUID or timestamp).
|
||||||
- Continue-as-new shouldn't be a big consideration for this use case (as it would take many conversational turns to trigger). Regardless, we should verify that it's able to carry the agent state over to the new workflow execution.
|
|
||||||
- Perhaps the UI should show when the LLM response is being retried (i.e. activity retry attempt because the LLM provided bad output)
|
- Perhaps the UI should show when the LLM response is being retried (i.e. activity retry attempt because the LLM provided bad output)
|
||||||
- Tests would be nice! [See tests](./tests/).
|
- The project now includes comprehensive tests for workflows and activities! [See testing guide](TESTING.md).
|
||||||
|
|
||||||
|
See [the todo](./todo.md) for more details on things we want to do (or that you could contribute!).
|
||||||
See [the todo](./todo.md) for more details.
|
|
||||||
|
|
||||||
See [the guide to adding goals and tools](./adding-goals-and-tools.md) for more ways you can add features.
|
See [the guide to adding goals and tools](./adding-goals-and-tools.md) for more ways you can add features.
|
||||||
|
|
||||||
## For Temporal SAs
|
## Enablement Guide (internal resource for Temporal employees)
|
||||||
Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the enablement guide here (TODO).
|
Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the [enablement guide](https://docs.google.com/document/d/14E0cEOibUAgHPBqConbWXgPUBY0Oxrnt6_AImdiheW4/edit?tab=t.0#heading=h.ajnq2v3xqbu1).
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Running the tests requires `poe` and `pytest_asyncio` to be installed.
|
|
||||||
|
|
||||||
python -m pip install poethepoet
|
|
||||||
python -m pip install pytest_asyncio
|
|
||||||
|
|
||||||
Once you have `poe` and `pytest_asyncio` installed you can run:
|
|
||||||
|
|
||||||
poe test
|
|
||||||
|
|||||||
163
TESTING.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Testing the Temporal AI Agent
|
||||||
|
|
||||||
|
This guide provides instructions for running the comprehensive test suite for the Temporal AI Agent project.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Install dependencies**:
|
||||||
|
```bash
|
||||||
|
poetry install --with dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run all tests**:
|
||||||
|
```bash
|
||||||
|
poetry run pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run with time-skipping for faster execution**:
|
||||||
|
```bash
|
||||||
|
poetry run pytest --workflow-environment=time-skipping
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- **Activity Tests**: `tests/test_tool_activities.py`
|
||||||
|
- LLM integration (mocked)
|
||||||
|
- Environment configuration
|
||||||
|
- JSON processing
|
||||||
|
- Dynamic tool execution
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- **Workflow Tests**: `tests/test_agent_goal_workflow.py`
|
||||||
|
- Full workflow execution
|
||||||
|
- Signal and query handling
|
||||||
|
- State management
|
||||||
|
- Error scenarios
|
||||||
|
|
||||||
|
## Running Specific Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run only activity tests
|
||||||
|
poetry run pytest tests/test_tool_activities.py -v
|
||||||
|
|
||||||
|
# Run only workflow tests
|
||||||
|
poetry run pytest tests/test_agent_goal_workflow.py -v
|
||||||
|
|
||||||
|
# Run a specific test
|
||||||
|
poetry run pytest tests/test_tool_activities.py::TestToolActivities::test_sanitize_json_response -v
|
||||||
|
|
||||||
|
# Run tests matching a pattern
|
||||||
|
poetry run pytest -k "validation" -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Environment Options
|
||||||
|
|
||||||
|
### Local Environment (Default)
|
||||||
|
```bash
|
||||||
|
poetry run pytest --workflow-environment=local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Time-Skipping Environment (Recommended for CI)
|
||||||
|
```bash
|
||||||
|
poetry run pytest --workflow-environment=time-skipping
|
||||||
|
```
|
||||||
|
|
||||||
|
### External Temporal Server
|
||||||
|
```bash
|
||||||
|
poetry run pytest --workflow-environment=localhost:7233
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Tests can be configured with these environment variables:
|
||||||
|
|
||||||
|
- `LLM_MODEL`: Model for LLM testing (default: "openai/gpt-4")
|
||||||
|
- `LLM_KEY`: API key for LLM service (mocked in tests)
|
||||||
|
- `LLM_BASE_URL`: Custom LLM endpoint (optional)
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
The test suite covers:
|
||||||
|
|
||||||
|
✅ **Workflows**
|
||||||
|
- AgentGoalWorkflow initialization and execution
|
||||||
|
- Signal handling (user_prompt, confirm, end_chat)
|
||||||
|
- Query methods (conversation history, agent goal, tool data)
|
||||||
|
- State management and conversation flow
|
||||||
|
- Validation and error handling
|
||||||
|
|
||||||
|
✅ **Activities**
|
||||||
|
- ToolActivities class methods
|
||||||
|
- LLM integration (mocked)
|
||||||
|
- Environment variable handling
|
||||||
|
- JSON response processing
|
||||||
|
- Dynamic tool activity execution
|
||||||
|
|
||||||
|
✅ **Integration**
|
||||||
|
- End-to-end workflow execution
|
||||||
|
- Activity registration in workers
|
||||||
|
- Temporal client interactions
|
||||||
|
|
||||||
|
## Test Output
|
||||||
|
|
||||||
|
Successful test run example:
|
||||||
|
```
|
||||||
|
============================== test session starts ==============================
|
||||||
|
platform darwin -- Python 3.11.3, pytest-8.3.5, pluggy-1.5.0
|
||||||
|
rootdir: /Users/steveandroulakis/Documents/Code/agentic/temporal-demo/temporal-ai-agent
|
||||||
|
configfile: pyproject.toml
|
||||||
|
plugins: anyio-4.5.2, asyncio-0.26.0
|
||||||
|
collected 21 items
|
||||||
|
|
||||||
|
tests/test_tool_activities.py::TestToolActivities::test_sanitize_json_response PASSED
|
||||||
|
tests/test_tool_activities.py::TestToolActivities::test_parse_json_response_success PASSED
|
||||||
|
tests/test_tool_activities.py::TestToolActivities::test_get_wf_env_vars_default_values PASSED
|
||||||
|
...
|
||||||
|
|
||||||
|
============================== 21 passed in 12.5s ==============================
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Module not found errors**: Run `poetry install --with dev`
|
||||||
|
2. **Async warnings**: These are expected with pytest-asyncio and can be ignored
|
||||||
|
3. **Test timeouts**: Use `--workflow-environment=time-skipping` for faster execution
|
||||||
|
4. **Import errors**: Check that you're running tests from the project root directory
|
||||||
|
|
||||||
|
### Debugging Tests
|
||||||
|
|
||||||
|
Enable verbose logging:
|
||||||
|
```bash
|
||||||
|
poetry run pytest --log-cli-level=DEBUG -s
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with coverage:
|
||||||
|
```bash
|
||||||
|
poetry run pytest --cov=workflows --cov=activities
|
||||||
|
```
|
||||||
|
|
||||||
|
## Continuous Integration
|
||||||
|
|
||||||
|
For CI environments, use:
|
||||||
|
```bash
|
||||||
|
poetry run pytest --workflow-environment=time-skipping --tb=short
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- See `tests/README.md` for detailed testing documentation
|
||||||
|
- Review `tests/conftest.py` for available test fixtures
|
||||||
|
- Check individual test files for specific test scenarios
|
||||||
|
|
||||||
|
## Test Architecture
|
||||||
|
|
||||||
|
The tests use:
|
||||||
|
- **Temporal Testing Framework**: For workflow and activity testing
|
||||||
|
- **pytest-asyncio**: For async test support
|
||||||
|
- **unittest.mock**: For mocking external dependencies
|
||||||
|
- **Test Fixtures**: For consistent test data and setup
|
||||||
|
|
||||||
|
All external dependencies (LLM calls, file I/O) are mocked to ensure fast, reliable tests.
|
||||||
@@ -1,137 +1,34 @@
|
|||||||
import inspect
|
import inspect
|
||||||
from temporalio import activity
|
|
||||||
from ollama import chat, ChatResponse
|
|
||||||
from openai import OpenAI
|
|
||||||
import json
|
import json
|
||||||
from typing import Sequence, Optional
|
|
||||||
from temporalio.common import RawValue
|
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import google.generativeai as genai
|
from typing import Sequence
|
||||||
import anthropic
|
|
||||||
import deepseek
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from models.data_types import EnvLookupOutput, ValidationInput, ValidationResult, ToolPromptInput, EnvLookupInput
|
|
||||||
|
|
||||||
load_dotenv(override=True)
|
from dotenv import load_dotenv
|
||||||
print(
|
from litellm import completion
|
||||||
"Using LLM provider: "
|
from temporalio import activity
|
||||||
+ os.environ.get("LLM_PROVIDER", "openai")
|
from temporalio.common import RawValue
|
||||||
+ " (set LLM_PROVIDER in .env to change)"
|
|
||||||
|
from models.data_types import (
|
||||||
|
EnvLookupInput,
|
||||||
|
EnvLookupOutput,
|
||||||
|
ToolPromptInput,
|
||||||
|
ValidationInput,
|
||||||
|
ValidationResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.environ.get("LLM_PROVIDER") == "ollama":
|
load_dotenv(override=True)
|
||||||
print(
|
|
||||||
"Using Ollama (local) model: "
|
|
||||||
+ os.environ.get("OLLAMA_MODEL_NAME", "qwen2.5:14b")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ToolActivities:
|
class ToolActivities:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize LLM clients based on environment configuration."""
|
"""Initialize LLM client using LiteLLM."""
|
||||||
self.llm_provider = os.environ.get("LLM_PROVIDER", "openai").lower()
|
self.llm_model = os.environ.get("LLM_MODEL", "openai/gpt-4")
|
||||||
print(f"Initializing ToolActivities with LLM provider: {self.llm_provider}")
|
self.llm_key = os.environ.get("LLM_KEY")
|
||||||
|
self.llm_base_url = os.environ.get("LLM_BASE_URL")
|
||||||
# Initialize client variables (all set to None initially)
|
print(f"Initializing ToolActivities with LLM model: {self.llm_model}")
|
||||||
self.openai_client: Optional[OpenAI] = None
|
if self.llm_base_url:
|
||||||
self.grok_client: Optional[OpenAI] = None
|
print(f"Using custom base URL: {self.llm_base_url}")
|
||||||
self.anthropic_client: Optional[anthropic.Anthropic] = None
|
|
||||||
self.genai_configured: bool = False
|
|
||||||
self.deepseek_client: Optional[deepseek.DeepSeekAPI] = None
|
|
||||||
self.ollama_model_name: Optional[str] = None
|
|
||||||
self.ollama_initialized: bool = False
|
|
||||||
|
|
||||||
# Only initialize the client specified by LLM_PROVIDER
|
|
||||||
if self.llm_provider == "openai":
|
|
||||||
if os.environ.get("OPENAI_API_KEY"):
|
|
||||||
self.openai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
|
|
||||||
print("Initialized OpenAI client")
|
|
||||||
else:
|
|
||||||
print("Warning: OPENAI_API_KEY not set but LLM_PROVIDER is 'openai'")
|
|
||||||
|
|
||||||
elif self.llm_provider == "grok":
|
|
||||||
if os.environ.get("GROK_API_KEY"):
|
|
||||||
self.grok_client = OpenAI(api_key=os.environ.get("GROK_API_KEY"), base_url="https://api.x.ai/v1")
|
|
||||||
print("Initialized grok client")
|
|
||||||
else:
|
|
||||||
print("Warning: GROK_API_KEY not set but LLM_PROVIDER is 'grok'")
|
|
||||||
|
|
||||||
elif self.llm_provider == "anthropic":
|
|
||||||
if os.environ.get("ANTHROPIC_API_KEY"):
|
|
||||||
self.anthropic_client = anthropic.Anthropic(
|
|
||||||
api_key=os.environ.get("ANTHROPIC_API_KEY")
|
|
||||||
)
|
|
||||||
print("Initialized Anthropic client")
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"Warning: ANTHROPIC_API_KEY not set but LLM_PROVIDER is 'anthropic'"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.llm_provider == "google":
|
|
||||||
api_key = os.environ.get("GOOGLE_API_KEY")
|
|
||||||
if api_key:
|
|
||||||
genai.configure(api_key=api_key)
|
|
||||||
self.genai_configured = True
|
|
||||||
print("Configured Google Generative AI")
|
|
||||||
else:
|
|
||||||
print("Warning: GOOGLE_API_KEY not set but LLM_PROVIDER is 'google'")
|
|
||||||
|
|
||||||
elif self.llm_provider == "deepseek":
|
|
||||||
if os.environ.get("DEEPSEEK_API_KEY"):
|
|
||||||
self.deepseek_client = deepseek.DeepSeekAPI(
|
|
||||||
api_key=os.environ.get("DEEPSEEK_API_KEY")
|
|
||||||
)
|
|
||||||
print("Initialized DeepSeek client")
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"Warning: DEEPSEEK_API_KEY not set but LLM_PROVIDER is 'deepseek'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# For Ollama, we store the model name but actual initialization happens in warm_up_ollama
|
|
||||||
elif self.llm_provider == "ollama":
|
|
||||||
self.ollama_model_name = os.environ.get("OLLAMA_MODEL_NAME", "qwen2.5:14b")
|
|
||||||
print(
|
|
||||||
f"Using Ollama model: {self.ollama_model_name} (will be loaded on worker startup)"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"Warning: Unknown LLM_PROVIDER '{self.llm_provider}', defaulting to OpenAI"
|
|
||||||
)
|
|
||||||
|
|
||||||
def warm_up_ollama(self):
|
|
||||||
"""Pre-load the Ollama model to avoid cold start latency on first request"""
|
|
||||||
if self.llm_provider != "ollama" or self.ollama_initialized:
|
|
||||||
return False # No need to warm up if not using Ollama or already warmed up
|
|
||||||
|
|
||||||
try:
|
|
||||||
print(
|
|
||||||
f"Pre-loading Ollama model '{self.ollama_model_name}' - this may take 30+ seconds..."
|
|
||||||
)
|
|
||||||
start_time = datetime.now()
|
|
||||||
|
|
||||||
# Make a simple request to load the model into memory
|
|
||||||
chat(
|
|
||||||
model=self.ollama_model_name,
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": "You are an AI assistant"},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Hello! This is a warm-up message to load the model.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
elapsed_time = (datetime.now() - start_time).total_seconds()
|
|
||||||
print(f"✅ Ollama model loaded successfully in {elapsed_time:.2f} seconds")
|
|
||||||
self.ollama_initialized = True
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error pre-loading Ollama model: {str(e)}")
|
|
||||||
print(
|
|
||||||
"The worker will continue, but the first actual request may experience a delay."
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
@activity.defn
|
@activity.defn
|
||||||
async def agent_validatePrompt(
|
async def agent_validatePrompt(
|
||||||
@@ -187,7 +84,7 @@ class ToolActivities:
|
|||||||
prompt=validation_prompt, context_instructions=context_instructions
|
prompt=validation_prompt, context_instructions=context_instructions
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.agent_toolPlanner(prompt_input)
|
result = await self.agent_toolPlanner(prompt_input)
|
||||||
|
|
||||||
return ValidationResult(
|
return ValidationResult(
|
||||||
validationResult=result.get("validationResult", False),
|
validationResult=result.get("validationResult", False),
|
||||||
@@ -195,19 +92,43 @@ class ToolActivities:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@activity.defn
|
@activity.defn
|
||||||
def agent_toolPlanner(self, input: ToolPromptInput) -> dict:
|
async def agent_toolPlanner(self, input: ToolPromptInput) -> dict:
|
||||||
if self.llm_provider == "ollama":
|
messages = [
|
||||||
return self.prompt_llm_ollama(input)
|
{
|
||||||
elif self.llm_provider == "google":
|
"role": "system",
|
||||||
return self.prompt_llm_google(input)
|
"content": input.context_instructions
|
||||||
elif self.llm_provider == "anthropic":
|
+ ". The current date is "
|
||||||
return self.prompt_llm_anthropic(input)
|
+ datetime.now().strftime("%B %d, %Y"),
|
||||||
elif self.llm_provider == "deepseek":
|
},
|
||||||
return self.prompt_llm_deepseek(input)
|
{
|
||||||
elif self.llm_provider == "grok":
|
"role": "user",
|
||||||
return self.prompt_llm_grok(input)
|
"content": input.prompt,
|
||||||
else:
|
},
|
||||||
return self.prompt_llm_openai(input)
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
completion_kwargs = {
|
||||||
|
"model": self.llm_model,
|
||||||
|
"messages": messages,
|
||||||
|
"api_key": self.llm_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add base_url if configured
|
||||||
|
if self.llm_base_url:
|
||||||
|
completion_kwargs["base_url"] = self.llm_base_url
|
||||||
|
|
||||||
|
response = completion(**completion_kwargs)
|
||||||
|
|
||||||
|
response_content = response.choices[0].message.content
|
||||||
|
activity.logger.info(f"LLM response: {response_content}")
|
||||||
|
|
||||||
|
# Use the new sanitize function
|
||||||
|
response_content = self.sanitize_json_response(response_content)
|
||||||
|
|
||||||
|
return self.parse_json_response(response_content)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in LLM completion: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
def parse_json_response(self, response_content: str) -> dict:
|
def parse_json_response(self, response_content: str) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -220,266 +141,26 @@ class ToolActivities:
|
|||||||
print(f"Invalid JSON: {e}")
|
print(f"Invalid JSON: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def prompt_llm_openai(self, input: ToolPromptInput) -> dict:
|
|
||||||
if not self.openai_client:
|
|
||||||
api_key = os.environ.get("OPENAI_API_KEY")
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError(
|
|
||||||
"OPENAI_API_KEY is not set in the environment variables but LLM_PROVIDER is 'openai'"
|
|
||||||
)
|
|
||||||
self.openai_client = OpenAI(api_key=api_key)
|
|
||||||
print("Initialized OpenAI client on demand")
|
|
||||||
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": input.context_instructions
|
|
||||||
+ ". The current date is "
|
|
||||||
+ datetime.now().strftime("%B %d, %Y"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": input.prompt,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
chat_completion = self.openai_client.chat.completions.create(
|
|
||||||
model="gpt-4o", messages=messages # was gpt-4-0613
|
|
||||||
)
|
|
||||||
|
|
||||||
response_content = chat_completion.choices[0].message.content
|
|
||||||
activity.logger.info(f"ChatGPT response: {response_content}")
|
|
||||||
|
|
||||||
# Use the new sanitize function
|
|
||||||
response_content = self.sanitize_json_response(response_content)
|
|
||||||
|
|
||||||
return self.parse_json_response(response_content)
|
|
||||||
|
|
||||||
def prompt_llm_grok(self, input: ToolPromptInput) -> dict:
|
|
||||||
if not self.grok_client:
|
|
||||||
api_key = os.environ.get("GROK_API_KEY")
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError(
|
|
||||||
"GROK_API_KEY is not set in the environment variables but LLM_PROVIDER is 'grok'"
|
|
||||||
)
|
|
||||||
self.grok_client = OpenAI(api_key=api_key, base_url="https://api.x.ai/v1")
|
|
||||||
print("Initialized grok client on demand")
|
|
||||||
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": input.context_instructions
|
|
||||||
+ ". The current date is "
|
|
||||||
+ datetime.now().strftime("%B %d, %Y"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": input.prompt,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
chat_completion = self.grok_client.chat.completions.create(
|
|
||||||
model="grok-2-1212", messages=messages
|
|
||||||
)
|
|
||||||
|
|
||||||
response_content = chat_completion.choices[0].message.content
|
|
||||||
activity.logger.info(f"Grok response: {response_content}")
|
|
||||||
|
|
||||||
# Use the new sanitize function
|
|
||||||
response_content = self.sanitize_json_response(response_content)
|
|
||||||
|
|
||||||
return self.parse_json_response(response_content)
|
|
||||||
def prompt_llm_ollama(self, input: ToolPromptInput) -> dict:
|
|
||||||
# If not yet initialized, try to do so now (this is a backup if warm_up_ollama wasn't called or failed)
|
|
||||||
if not self.ollama_initialized:
|
|
||||||
print(
|
|
||||||
"Ollama model not pre-loaded. Loading now (this may take 30+ seconds)..."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
self.warm_up_ollama()
|
|
||||||
except Exception:
|
|
||||||
# We already logged the error in warm_up_ollama, continue with the actual request
|
|
||||||
pass
|
|
||||||
|
|
||||||
model_name = self.ollama_model_name or os.environ.get(
|
|
||||||
"OLLAMA_MODEL_NAME", "qwen2.5:14b"
|
|
||||||
)
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": input.context_instructions
|
|
||||||
+ ". The current date is "
|
|
||||||
+ get_current_date_human_readable(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": input.prompt,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
response: ChatResponse = chat(model=model_name, messages=messages)
|
|
||||||
print(f"Chat response: {response.message.content}")
|
|
||||||
|
|
||||||
# Use the new sanitize function
|
|
||||||
response_content = self.sanitize_json_response(response.message.content)
|
|
||||||
return self.parse_json_response(response_content)
|
|
||||||
except (json.JSONDecodeError, ValueError) as e:
|
|
||||||
# Re-raise JSON-related exceptions to let Temporal retry the activity
|
|
||||||
print(f"JSON parsing error with Ollama response: {str(e)}")
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
# Log and raise other exceptions that may need retrying
|
|
||||||
print(f"Error in Ollama chat: {str(e)}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def prompt_llm_google(self, input: ToolPromptInput) -> dict:
|
|
||||||
if not self.genai_configured:
|
|
||||||
api_key = os.environ.get("GOOGLE_API_KEY")
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError(
|
|
||||||
"GOOGLE_API_KEY is not set in the environment variables but LLM_PROVIDER is 'google'"
|
|
||||||
)
|
|
||||||
genai.configure(api_key=api_key)
|
|
||||||
self.genai_configured = True
|
|
||||||
print("Configured Google Generative AI on demand")
|
|
||||||
|
|
||||||
model = genai.GenerativeModel(
|
|
||||||
"models/gemini-1.5-flash",
|
|
||||||
system_instruction=input.context_instructions
|
|
||||||
+ ". The current date is "
|
|
||||||
+ datetime.now().strftime("%B %d, %Y"),
|
|
||||||
)
|
|
||||||
response = model.generate_content(input.prompt)
|
|
||||||
response_content = response.text
|
|
||||||
print(f"Google Gemini response: {response_content}")
|
|
||||||
|
|
||||||
# Use the new sanitize function
|
|
||||||
response_content = self.sanitize_json_response(response_content)
|
|
||||||
|
|
||||||
return self.parse_json_response(response_content)
|
|
||||||
|
|
||||||
def prompt_llm_anthropic(self, input: ToolPromptInput) -> dict:
|
|
||||||
if not self.anthropic_client:
|
|
||||||
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError(
|
|
||||||
"ANTHROPIC_API_KEY is not set in the environment variables but LLM_PROVIDER is 'anthropic'"
|
|
||||||
)
|
|
||||||
self.anthropic_client = anthropic.Anthropic(api_key=api_key)
|
|
||||||
print("Initialized Anthropic client on demand")
|
|
||||||
|
|
||||||
response = self.anthropic_client.messages.create(
|
|
||||||
#model="claude-3-5-sonnet-20241022", # todo try claude-3-7-sonnet-20250219
|
|
||||||
model="claude-3-7-sonnet-20250219", # todo try claude-3-7-sonnet-20250219
|
|
||||||
max_tokens=1024,
|
|
||||||
system=input.context_instructions
|
|
||||||
+ ". The current date is "
|
|
||||||
+ get_current_date_human_readable(),
|
|
||||||
messages=[
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": input.prompt,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
response_content = response.content[0].text
|
|
||||||
print(f"Anthropic response: {response_content}")
|
|
||||||
|
|
||||||
# Use the new sanitize function
|
|
||||||
response_content = self.sanitize_json_response(response_content)
|
|
||||||
|
|
||||||
return self.parse_json_response(response_content)
|
|
||||||
|
|
||||||
def prompt_llm_deepseek(self, input: ToolPromptInput) -> dict:
|
|
||||||
if not self.deepseek_client:
|
|
||||||
api_key = os.environ.get("DEEPSEEK_API_KEY")
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError(
|
|
||||||
"DEEPSEEK_API_KEY is not set in the environment variables but LLM_PROVIDER is 'deepseek'"
|
|
||||||
)
|
|
||||||
self.deepseek_client = deepseek.DeepSeekAPI(api_key=api_key)
|
|
||||||
print("Initialized DeepSeek client on demand")
|
|
||||||
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": input.context_instructions
|
|
||||||
+ ". The current date is "
|
|
||||||
+ datetime.now().strftime("%B %d, %Y"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": input.prompt,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
response = self.deepseek_client.chat_completion(prompt=messages)
|
|
||||||
response_content = response
|
|
||||||
print(f"DeepSeek response: {response_content}")
|
|
||||||
|
|
||||||
# Use the new sanitize function
|
|
||||||
response_content = self.sanitize_json_response(response_content)
|
|
||||||
|
|
||||||
return self.parse_json_response(response_content)
|
|
||||||
|
|
||||||
def sanitize_json_response(self, response_content: str) -> str:
|
def sanitize_json_response(self, response_content: str) -> str:
|
||||||
"""
|
"""
|
||||||
Extracts the JSON block from the response content as a string.
|
Sanitizes the response content to ensure it's valid JSON.
|
||||||
Supports:
|
|
||||||
- JSON surrounded by ```json and ```
|
|
||||||
- Raw JSON input
|
|
||||||
- JSON preceded or followed by extra text
|
|
||||||
Rejects invalid input that doesn't contain JSON.
|
|
||||||
"""
|
"""
|
||||||
try:
|
# Remove any markdown code block markers
|
||||||
start_marker = "```json"
|
response_content = response_content.replace("```json", "").replace("```", "")
|
||||||
end_marker = "```"
|
|
||||||
|
|
||||||
json_str = None
|
# Remove any leading/trailing whitespace
|
||||||
|
response_content = response_content.strip()
|
||||||
|
|
||||||
# Case 1: JSON surrounded by markers
|
return response_content
|
||||||
if start_marker in response_content and end_marker in response_content:
|
|
||||||
json_start = response_content.index(start_marker) + len(start_marker)
|
|
||||||
json_end = response_content.index(end_marker, json_start)
|
|
||||||
json_str = response_content[json_start:json_end].strip()
|
|
||||||
|
|
||||||
# Case 2: Text with valid JSON
|
|
||||||
else:
|
|
||||||
# Try to locate the JSON block by scanning for the first `{` and last `}`
|
|
||||||
json_start = response_content.find("{")
|
|
||||||
json_end = response_content.rfind("}")
|
|
||||||
|
|
||||||
if json_start != -1 and json_end != -1 and json_start < json_end:
|
|
||||||
json_str = response_content[json_start : json_end + 1].strip()
|
|
||||||
|
|
||||||
# Validate and ensure the extracted JSON is valid
|
|
||||||
if json_str:
|
|
||||||
json.loads(json_str) # This will raise an error if the JSON is invalid
|
|
||||||
return json_str
|
|
||||||
|
|
||||||
# If no valid JSON found, raise an error
|
|
||||||
raise ValueError("Response does not contain valid JSON.")
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# Invalid JSON
|
|
||||||
print(f"Invalid JSON detected in response: {response_content}")
|
|
||||||
raise ValueError("Response does not contain valid JSON.")
|
|
||||||
except Exception as e:
|
|
||||||
# Other errors
|
|
||||||
print(f"Error processing response: {str(e)}")
|
|
||||||
print(f"Full response: {response_content}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# get env vars for workflow
|
|
||||||
@activity.defn
|
@activity.defn
|
||||||
async def get_wf_env_vars(self, input: EnvLookupInput) -> EnvLookupOutput:
|
async def get_wf_env_vars(self, input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
""" gets env vars for workflow as an activity result so it's deterministic
|
"""gets env vars for workflow as an activity result so it's deterministic
|
||||||
handles default/None
|
handles default/None
|
||||||
"""
|
"""
|
||||||
output: EnvLookupOutput = EnvLookupOutput(show_confirm=input.show_confirm_default,
|
output: EnvLookupOutput = EnvLookupOutput(
|
||||||
multi_goal_mode=True)
|
show_confirm=input.show_confirm_default, multi_goal_mode=True
|
||||||
|
)
|
||||||
show_confirm_value = os.getenv(input.show_confirm_env_var_name)
|
show_confirm_value = os.getenv(input.show_confirm_env_var_name)
|
||||||
if show_confirm_value is None:
|
if show_confirm_value is None:
|
||||||
output.show_confirm = input.show_confirm_default
|
output.show_confirm = input.show_confirm_default
|
||||||
@@ -491,7 +172,10 @@ class ToolActivities:
|
|||||||
first_goal_value = os.getenv("AGENT_GOAL")
|
first_goal_value = os.getenv("AGENT_GOAL")
|
||||||
if first_goal_value is None:
|
if first_goal_value is None:
|
||||||
output.multi_goal_mode = True # default if unset
|
output.multi_goal_mode = True # default if unset
|
||||||
elif first_goal_value is not None and first_goal_value.lower() != "goal_choose_agent_type":
|
elif (
|
||||||
|
first_goal_value is not None
|
||||||
|
and first_goal_value.lower() != "goal_choose_agent_type"
|
||||||
|
):
|
||||||
output.multi_goal_mode = False
|
output.multi_goal_mode = False
|
||||||
else:
|
else:
|
||||||
output.multi_goal_mode = True
|
output.multi_goal_mode = True
|
||||||
@@ -499,17 +183,6 @@ class ToolActivities:
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def get_current_date_human_readable():
|
|
||||||
"""
|
|
||||||
Returns the current date in a human-readable format.
|
|
||||||
|
|
||||||
Example: Wednesday, January 1, 2025
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
return datetime.now().strftime("%A, %B %d, %Y")
|
|
||||||
|
|
||||||
|
|
||||||
@activity.defn(dynamic=True)
|
@activity.defn(dynamic=True)
|
||||||
async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||||
from tools import get_handler
|
from tools import get_handler
|
||||||
@@ -528,5 +201,3 @@ async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
|||||||
# Optionally log or augment the result
|
# Optionally log or augment the result
|
||||||
activity.logger.info(f"Tool '{tool_name}' result: {result}")
|
activity.logger.info(f"Tool '{tool_name}' result: {result}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Customizing the Agent
|
# Customizing the Agent
|
||||||
The agent is set up to allow for multiple goals and to switch back to choosing a new goal at the end of every successful goal. A goal is made up of a list of tools that the agent will guide the user through.
|
The agent is set up to have multiple agents, each with their own goal. It supports switching back to choosing a new goal at the end of every successful goal (or even mid-goal).
|
||||||
|
A goal is made up of a list of tools that the agent will guide the user through.
|
||||||
|
|
||||||
It may be helpful to review the [architecture](./architecture.md) for a guide and definition of goals, tools, etc.
|
It may be helpful to review the [architecture](./architecture.md) for a guide and definition of goals, tools, etc.
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ description="Help the user gather args for these tools in order: "
|
|||||||
```
|
```
|
||||||
|
|
||||||
Tools should generally return meaningful information and be generally ‘failsafe’ in returning a useful result based on input.
|
Tools should generally return meaningful information and be generally ‘failsafe’ in returning a useful result based on input.
|
||||||
(If you're doing a local data approach like those in [.tools/data/](./tools/data/)) it's good to document how they can be setup to get a good result in tool specific [setup](./setup.md).
|
(If you're doing a local data approach like those in [.tools/data/](./tools/data/)) it's good to document how they can be setup to get a good result in tool specific [setup](./SETUP.md).
|
||||||
|
|
||||||
### Add to Tool Registry
|
### Add to Tool Registry
|
||||||
1. Open [/tools/tool_registry.py](tools/tool_registry.py) - this file contains mapping of tool names to tool definitions (so the AI understands how to use them)
|
1. Open [/tools/tool_registry.py](tools/tool_registry.py) - this file contains mapping of tool names to tool definitions (so the AI understands how to use them)
|
||||||
|
|||||||
25
api/main.py
@@ -1,18 +1,18 @@
|
|||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from fastapi import FastAPI
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from temporalio.api.enums.v1 import WorkflowExecutionStatus
|
||||||
from temporalio.client import Client
|
from temporalio.client import Client
|
||||||
from temporalio.exceptions import TemporalError
|
from temporalio.exceptions import TemporalError
|
||||||
from temporalio.api.enums.v1 import WorkflowExecutionStatus
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
from models.data_types import AgentGoalWorkflowParams, CombinedInput
|
||||||
from models.data_types import CombinedInput, AgentGoalWorkflowParams
|
from shared.config import TEMPORAL_TASK_QUEUE, get_temporal_client
|
||||||
from tools.goal_registry import goal_list
|
from tools.goal_registry import goal_list
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||||
from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE
|
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
temporal_client: Optional[Client] = None
|
temporal_client: Optional[Client] = None
|
||||||
@@ -23,7 +23,9 @@ load_dotenv()
|
|||||||
|
|
||||||
def get_initial_agent_goal():
|
def get_initial_agent_goal():
|
||||||
"""Get the agent goal from environment variables."""
|
"""Get the agent goal from environment variables."""
|
||||||
env_goal = os.getenv("AGENT_GOAL", "goal_choose_agent_type") #if no goal is set in the env file, default to choosing an agent
|
env_goal = os.getenv(
|
||||||
|
"AGENT_GOAL", "goal_choose_agent_type"
|
||||||
|
) # if no goal is set in the env file, default to choosing an agent
|
||||||
for listed_goal in goal_list:
|
for listed_goal in goal_list:
|
||||||
if listed_goal.id == env_goal:
|
if listed_goal.id == env_goal:
|
||||||
return listed_goal
|
return listed_goal
|
||||||
@@ -120,6 +122,7 @@ async def get_conversation_history():
|
|||||||
status_code=500, detail="Internal server error while querying workflow."
|
status_code=500, detail="Internal server error while querying workflow."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/agent-goal")
|
@app.get("/agent-goal")
|
||||||
async def get_agent_goal():
|
async def get_agent_goal():
|
||||||
"""Calls the workflow's 'get_agent_goal' query."""
|
"""Calls the workflow's 'get_agent_goal' query."""
|
||||||
@@ -148,7 +151,7 @@ async def send_prompt(prompt: str):
|
|||||||
combined_input = CombinedInput(
|
combined_input = CombinedInput(
|
||||||
tool_params=AgentGoalWorkflowParams(None, None),
|
tool_params=AgentGoalWorkflowParams(None, None),
|
||||||
agent_goal=get_initial_agent_goal(),
|
agent_goal=get_initial_agent_goal(),
|
||||||
#change to get from workflow query
|
# change to get from workflow query
|
||||||
)
|
)
|
||||||
|
|
||||||
workflow_id = "agent-workflow"
|
workflow_id = "agent-workflow"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ This documents some of the "why" behind the [architecture](./architecture.md).
|
|||||||
|
|
||||||
## AI Models
|
## AI Models
|
||||||
We wanted to have flexibility to use different models, because this space is changing rapidly and models get better regularly.
|
We wanted to have flexibility to use different models, because this space is changing rapidly and models get better regularly.
|
||||||
Also, for you, we wanted to let you pick your model of choice. The system is designed to make changing models out simple. For how to do that, checkout the [setup guide](./setup.md).
|
Also, for you, we wanted to let you pick your model of choice. The system is designed to make changing models out simple. For how to do that, checkout the [setup guide](./SETUP.md).
|
||||||
|
|
||||||
## Temporal
|
## Temporal
|
||||||
We asked one of the AI models used in this demo to answer this question (edited minorly):
|
We asked one of the AI models used in this demo to answer this question (edited minorly):
|
||||||
|
|||||||
@@ -1,58 +1,65 @@
|
|||||||
# Elements
|
# Elements
|
||||||
These are the main elements of this system.
|
These are the main elements of this system. See [architecture decisions](./architecture-decisions.md) for information beind these choices.
|
||||||

|
In this document we will explain each element and their interactions, and then connect them all at the end.
|
||||||
|
<img src="./assets/Architecture_elements.png" width="50%" alt="Architecture Elements">
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
This is a [Temporal Workflow](https://docs.temporal.io/workflows) - a durable straightforward description of the process to be executed. For our example see [agent_goal_workflow.py](./workflows/agent_goal_workflow.py).
|
This is a [Temporal Workflow](https://docs.temporal.io/workflows) - a durable straightforward description of the process to be executed. See [agent_goal_workflow.py](./workflows/agent_goal_workflow.py).
|
||||||
Temporal is used to make the process scalable, durable, reliable, secure, and visible.
|
Temporal is used to make the process scalable, durable, reliable, secure, and visible.
|
||||||
|
|
||||||
### Workflow Responsibilities:
|
### Workflow Responsibilities:
|
||||||
- Orchestrates interactive loop
|
- Orchestrates interactive loops:
|
||||||
- Prompts LLM, Users
|
- LLM Loop: Prompts LLM, durably executes LLM, stores responses
|
||||||
|
- Interactive Loop: Elicits responses from input (in our case a human) and validates input responses
|
||||||
|
- Tool Execution Loop: Durably executes Tools
|
||||||
- Keeps record of all interactions ([Signals, Queries, Updates](https://docs.temporal.io/develop/python/message-passing))
|
- Keeps record of all interactions ([Signals, Queries, Updates](https://docs.temporal.io/develop/python/message-passing))
|
||||||
- Executes LLM durably
|
|
||||||
- Executes Tools durably
|
|
||||||
- Handles failures gracefully
|
- Handles failures gracefully
|
||||||
- Human, LLM and tool interaction history stored for debugging and analysis
|
- Input, LLM and Tool interaction history stored for debugging and analysis
|
||||||
|
|
||||||
## Activities
|
## Activities
|
||||||
These are [Temporal Activities](https://docs.temporal.io/activities). Defined as simple functions, they are auto-retried async/event driven behind the scenes. Activities durably execute Tools and the LLM. See [a sample activity](./activities/tool_activities.py).
|
These are [Temporal Activities](https://docs.temporal.io/activities). Defined as simple functions, they are auto-retried async/event driven behind the scenes. Activities durably execute Tools and the LLM. See [a sample activity](./activities/tool_activities.py).
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
Tools define the capabilities of the system. They are simple Python functions (could be in any language).
|
Tools define the capabilities of the system. They are simple Python functions (could be in any language as Temporal supports multiple languages).
|
||||||
They are executed by Temporal Activities. They are “just code” - can connect to any API or system. They also are where the "hard" business logic is: you can validate and retry actions using code you write.
|
They are executed by Temporal Activities. They are “just code” - can connect to any API or system. They also are where the deterministic business logic is: you can validate and retry actions using code you write.
|
||||||
Failures are handled gracefully by Temporal.
|
Failures are handled gracefully by Temporal.
|
||||||
|
|
||||||
Activities + Tools turn the probabalistic input from the user and LLM into deterministic action.
|
Activities + Tools turn the probabalistic input from the user and LLM into deterministic action.
|
||||||
|
|
||||||
## Prompts
|
## Prompts
|
||||||
Prompts are where the instructions to the LLM & users is. Prompts are made up of initial instructions, goal instructions, and tool instructions.
|
Prompts are where the instructions to the LLM are. Prompts are made up of initial instructions, goal instructions, and tool instructions.
|
||||||
See [agent prompts](./prompts/agent_prompt_generators.py) and [goal & tool prompts](./tools/goal_registry.py).
|
See [agent prompts](./prompts/agent_prompt_generators.py) and [goal & tool prompts](./tools/goal_registry.py).
|
||||||
|
|
||||||
This is where you can add probabalistic business logic, to control process flow, describe what to do, and give instruction and validation for the LLM.
|
This is where you can add probabalistic business logic to
|
||||||
|
- to control process flow
|
||||||
|
- describe what to do
|
||||||
|
- give examples of interactions
|
||||||
|
- give instruction and validation for the LLM
|
||||||
|
|
||||||
## LLM
|
## LLM
|
||||||
Probabalistic execution: it will _probably_ do what you tell it to do.
|
Probabalistic execution: it will _probably_ do what you tell it to do.
|
||||||
Turns the guidance from the prompts (see [agent prompts](./prompts/agent_prompt_generators.py) and [goal prompts](./tools/goal_registry.py)) into
|
Turns the guidance from the prompts (see [agent prompts](./prompts/agent_prompt_generators.py) and [goal prompts](./tools/goal_registry.py)) into
|
||||||
You have a choice of providers - see [setup](./setup.md).
|
You have a choice of providers - see [setup](./SETUP.md).
|
||||||
The LLM:
|
The LLM:
|
||||||
- Validates user input for tools
|
- Drives toward the initial Goal and any subsequent Goals selected by user
|
||||||
- Drives toward goal selected by user
|
- Decides what to do based on input, such as:
|
||||||
- Decides when to execute tools
|
- Validates user input for Tools
|
||||||
- Formats input and interprets output for tools
|
- Decides when to execute Tools
|
||||||
|
- Decides on next step for Goal
|
||||||
|
- Formats input and interprets output for Tools
|
||||||
- is executed by Temporal Activities
|
- is executed by Temporal Activities
|
||||||
- API failures and logical failures are handled transparently
|
- API failures and logical failures are handled transparently
|
||||||
|
|
||||||
## Interaction
|
## Interaction
|
||||||
Interaction is managed with Temporal Signals and Queries. These are durably stored in Workflow History.
|
Interaction is managed with Temporal Signals and Queries. These are durably stored in Workflow History.
|
||||||
Can be used for analysis and debugging. It's all “just code” so it's easy to add new Signals and Queries.
|
History can be used for analysis and debugging. It's all “just code” so it's easy to add new Signals and Queries.
|
||||||
Input can be very dynamic, just needs to be serializable.
|
Input can be very dynamic, just needs to be serializable.
|
||||||
|
|
||||||
The workflow executes in a loop: gathering input, validating input, executing tools, managing prompts, and then waiting for input.
|
The Workflow executes the Interaction Loop: gathering input, validating input, and providing a response:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Here's a more detailed example for gathering parameters for tools:
|
Here's a more detailed example for gathering inputs for Tools:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -64,4 +71,4 @@ Now that we have the pieces and what they do, here is a more complete diagram of
|
|||||||
|
|
||||||
|
|
||||||
# Adding features
|
# Adding features
|
||||||
Want to add more tools, See [adding goals and tools](./adding-goals-and-tools.md).
|
Want to add more Goals and Tools? See [adding goals and tools](./adding-goals-and-tools.md). Have fun!
|
||||||
BIN
assets/0.2.0_changes.jpeg
Normal file
|
After Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 129 KiB |
20
docker-compose.override.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
volumes:
|
||||||
|
- ./:/app:cached
|
||||||
|
command: uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
worker:
|
||||||
|
volumes:
|
||||||
|
- ./:/app:cached
|
||||||
|
command: python scripts/run_worker.py
|
||||||
|
|
||||||
|
train-api:
|
||||||
|
volumes:
|
||||||
|
- ./:/app:cached
|
||||||
|
command: python thirdparty/train_api.py
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app:cached
|
||||||
|
command: sh -c "apk update && apk add --no-cache xdg-utils && npm install && npx vite --host 0.0.0.0 --port 5173"
|
||||||
120
docker-compose.yml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
services:
|
||||||
|
# Database service
|
||||||
|
postgresql:
|
||||||
|
image: postgres:14
|
||||||
|
container_name: temporal-postgresql
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: temporal
|
||||||
|
POSTGRES_PASSWORD: temporal
|
||||||
|
POSTGRES_DB: temporal
|
||||||
|
volumes:
|
||||||
|
- postgresql:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- temporal-network
|
||||||
|
|
||||||
|
# Temporal services
|
||||||
|
temporal:
|
||||||
|
image: temporalio/auto-setup:1.27.2
|
||||||
|
container_name: temporal
|
||||||
|
ports:
|
||||||
|
- "7233:7233"
|
||||||
|
environment:
|
||||||
|
- DB=postgres12
|
||||||
|
- DB_PORT=5432
|
||||||
|
- POSTGRES_USER=temporal
|
||||||
|
- POSTGRES_PWD=temporal
|
||||||
|
- POSTGRES_SEEDS=postgresql
|
||||||
|
depends_on:
|
||||||
|
- postgresql
|
||||||
|
networks:
|
||||||
|
- temporal-network
|
||||||
|
|
||||||
|
temporal-admin-tools:
|
||||||
|
image: temporalio/admin-tools:1.27
|
||||||
|
container_name: temporal-admin-tools
|
||||||
|
depends_on:
|
||||||
|
- temporal
|
||||||
|
environment:
|
||||||
|
- TEMPORAL_CLI_ADDRESS=temporal:7233
|
||||||
|
networks:
|
||||||
|
- temporal-network
|
||||||
|
|
||||||
|
temporal-ui:
|
||||||
|
image: temporalio/ui:2.37.2
|
||||||
|
container_name: temporal-ui
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- TEMPORAL_ADDRESS=temporal:7233
|
||||||
|
- TEMPORAL_CORS_ORIGINS=http://localhost:8080
|
||||||
|
depends_on:
|
||||||
|
- temporal
|
||||||
|
networks:
|
||||||
|
- temporal-network
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: temporal-ai-agent-api
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
- temporal
|
||||||
|
networks:
|
||||||
|
- temporal-network
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- TEMPORAL_ADDRESS=temporal:7233
|
||||||
|
|
||||||
|
worker:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: temporal-ai-agent-worker
|
||||||
|
depends_on:
|
||||||
|
- temporal
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- TEMPORAL_ADDRESS=temporal:7233
|
||||||
|
command: python scripts/run_worker.py
|
||||||
|
networks:
|
||||||
|
- temporal-network
|
||||||
|
|
||||||
|
train-api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: temporal-ai-agent-train-api
|
||||||
|
depends_on:
|
||||||
|
- temporal
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- TEMPORAL_ADDRESS=temporal:7233
|
||||||
|
command: python thirdparty/train_api.py
|
||||||
|
networks:
|
||||||
|
- temporal-network
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: node:18-alpine
|
||||||
|
container_name: temporal-ai-agent-frontend
|
||||||
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
command: sh -c "apk update && apk add --no-cache xdg-utils && npm install && npx vite --host 0.0.0.0"
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
networks:
|
||||||
|
- temporal-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
temporal-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgresql:
|
||||||
@@ -1,56 +1,144 @@
|
|||||||
import React, { memo } from "react";
|
import React, { memo, useState } from "react";
|
||||||
|
|
||||||
|
/** Inline SVG icons so we don’t need an extra library */
|
||||||
|
const PlayIcon = ({ className }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M5 3.868v16.264c0 1.04 1.12 1.675 2.025 1.16l13.11-8.132a1.33 1.33 0 000-2.256L7.025 2.773C6.12 2.259 5 2.894 5 3.934z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SpinnerIcon = ({ className }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={`animate-spin ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
||||||
|
<path d="M22 12a10 10 0 00-10-10" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User‑friendly confirmation card that surfaces tool invocation details
|
||||||
|
* without developer jargon. Tweaks include:
|
||||||
|
* • Left green accent‑border + compact heading (visual hierarchy)
|
||||||
|
* • Collapsible arg list & array support (argument‑list UX)
|
||||||
|
* • Mobile‑first, pulsing confirm button (button affordance)
|
||||||
|
*/
|
||||||
const ConfirmInline = memo(({ data, confirmed, onConfirm }) => {
|
const ConfirmInline = memo(({ data, confirmed, onConfirm }) => {
|
||||||
const { args, tool } = data || {};
|
const { args = {}, tool } = data || {};
|
||||||
|
|
||||||
const renderArgs = () => {
|
// Collapsible argument list if we have more than 4 root keys
|
||||||
if (!args) return null;
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
const argEntries = Object.entries(args);
|
||||||
|
const shouldCollapse = argEntries.length > 4 && !showAll;
|
||||||
|
|
||||||
|
/** Recursively pretty‑print argument values (objects & arrays). */
|
||||||
|
const RenderValue = ({ value }) => {
|
||||||
|
if (value === null || value === undefined) return <span className="italic">‑</span>;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-1">
|
<ol className="pl-4 list-decimal space-y-0.5">
|
||||||
<strong>Args:</strong>
|
{value.map((v, i) => (
|
||||||
<pre className="bg-gray-100 dark:bg-gray-700 p-1 rounded text-sm whitespace-pre-wrap overflow-x-auto">
|
<li key={i} className="flex gap-1">
|
||||||
{JSON.stringify(args, null, 2)}
|
<RenderValue value={v} />
|
||||||
</pre>
|
</li>
|
||||||
</div>
|
))}
|
||||||
|
</ol>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "object") {
|
||||||
|
return (
|
||||||
|
<ul className="pl-4 space-y-0.5 list-disc marker:text-green-500 dark:marker:text-green-400">
|
||||||
|
{Object.entries(value).map(([k, v]) => (
|
||||||
|
<li key={k} className="flex gap-1">
|
||||||
|
<span className="capitalize text-gray-600 dark:text-gray-300">{k}: </span>
|
||||||
|
<RenderValue value={v} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span className="font-medium text-gray-800 dark:text-gray-100">{String(value)}</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cardBase =
|
||||||
|
"mt-2 p-3 rounded-lg border-l-4 border-green-500 bg-gray-100/60 dark:bg-gray-800/60 shadow-sm";
|
||||||
|
|
||||||
|
// ===== Running state =====
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 p-2 border border-gray-400 dark:border-gray-600 rounded
|
<div className={`${cardBase} flex items-center gap-3`} role="status">
|
||||||
bg-gray-50 dark:bg-gray-800 transition-colors duration-200">
|
<SpinnerIcon className="text-green-600 dark:text-green-400 w-4 h-4" />
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
<span className="text-sm text-gray-700 dark:text-gray-200">
|
||||||
<div>
|
Running <strong className="font-semibold">{tool ?? "Unknown"}</strong> …
|
||||||
<strong>Tool:</strong> {tool ?? "Unknown"}
|
</span>
|
||||||
</div>
|
|
||||||
{renderArgs()}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-green-600 dark:text-green-400 font-medium">
|
|
||||||
Running {tool}...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Confirmation state =====
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 p-2 border border-gray-400 dark:border-gray-600 rounded
|
<div className={`${cardBase} space-y-2`} role="group">
|
||||||
bg-gray-50 dark:bg-gray-800 transition-colors duration-200">
|
{/* Heading */}
|
||||||
<div className="text-gray-600 dark:text-gray-300">
|
<div className="flex items-center gap-2">
|
||||||
<div>
|
<PlayIcon className="text-green-600 dark:text-green-400 w-5 h-5 shrink-0" />
|
||||||
Agent is ready to run the tool: <strong>{tool ?? "Unknown"}</strong>
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
Ready to run <strong>{tool ?? "Unknown"}</strong>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{renderArgs()}
|
|
||||||
<div className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
{/* Dynamic argument list */}
|
||||||
Please confirm to proceed.
|
{argEntries.length > 0 && (
|
||||||
|
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{argEntries
|
||||||
|
.slice(0, shouldCollapse ? 4 : argEntries.length)
|
||||||
|
.map(([k, v]) => (
|
||||||
|
<div key={k} className="flex gap-1">
|
||||||
|
<span className="capitalize">{k}: </span>
|
||||||
|
<RenderValue value={v} />
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
{shouldCollapse && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAll(true)}
|
||||||
|
className="mt-1 text-green-600 dark:text-green-400 text-xs underline hover:no-underline"
|
||||||
|
>
|
||||||
|
…show all
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{showAll && argEntries.length > 4 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAll(false)}
|
||||||
|
className="mt-1 block text-green-600 dark:text-green-400 text-xs underline hover:no-underline"
|
||||||
|
>
|
||||||
|
show less
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right mt-2">
|
)}
|
||||||
|
|
||||||
|
{/* Confirm button */}
|
||||||
|
<div className="text-right">
|
||||||
<button
|
<button
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded
|
onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && onConfirm()}
|
||||||
transition-colors duration-200 focus:outline-none focus:ring-2
|
className="w-full sm:w-auto bg-green-600 hover:bg-green-700 text-white text-sm px-3 py-1.5 rounded-md shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-1 animate-pulse sm:animate-none"
|
||||||
focus:ring-green-500 focus:ring-opacity-50"
|
|
||||||
aria-label={`Confirm running ${tool}`}
|
aria-label={`Confirm running ${tool}`}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
@@ -60,6 +148,6 @@ const ConfirmInline = memo(({ data, confirmed, onConfirm }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ConfirmInline.displayName = 'ConfirmInline';
|
ConfirmInline.displayName = "ConfirmInline";
|
||||||
|
|
||||||
export default ConfirmInline;
|
export default ConfirmInline;
|
||||||
@@ -3,7 +3,7 @@ import NavBar from "../components/NavBar";
|
|||||||
import ChatWindow from "../components/ChatWindow";
|
import ChatWindow from "../components/ChatWindow";
|
||||||
import { apiService } from "../services/api";
|
import { apiService } from "../services/api";
|
||||||
|
|
||||||
const POLL_INTERVAL = 500; // 0.5 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, Deque, Dict, Any, List, Union, Literal
|
from typing import Any, Deque, Dict, List, Literal, Optional, Union
|
||||||
|
|
||||||
from models.tool_definitions import AgentGoal
|
from models.tool_definitions import AgentGoal
|
||||||
|
|
||||||
|
|
||||||
@@ -43,11 +44,13 @@ class ValidationResult:
|
|||||||
if self.validationFailedReason is None:
|
if self.validationFailedReason is None:
|
||||||
self.validationFailedReason = {}
|
self.validationFailedReason = {}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EnvLookupInput:
|
class EnvLookupInput:
|
||||||
show_confirm_env_var_name: str
|
show_confirm_env_var_name: str
|
||||||
show_confirm_default: bool
|
show_confirm_default: bool
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EnvLookupOutput:
|
class EnvLookupOutput:
|
||||||
show_confirm: bool
|
show_confirm: bool
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ToolDefinition:
|
|||||||
description: str
|
description: str
|
||||||
arguments: List[ToolArgument]
|
arguments: List[ToolArgument]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AgentGoal:
|
class AgentGoal:
|
||||||
id: str
|
id: str
|
||||||
@@ -24,6 +25,4 @@ class AgentGoal:
|
|||||||
tools: List[ToolDefinition]
|
tools: List[ToolDefinition]
|
||||||
description: str = "Description of the tools purpose and overall goal"
|
description: str = "Description of the tools purpose and overall goal"
|
||||||
starter_prompt: str = "Initial prompt to start the conversation"
|
starter_prompt: str = "Initial prompt to start the conversation"
|
||||||
example_conversation_history: str = (
|
example_conversation_history: str = "Example conversation history to help the AI agent understand the context of the conversation"
|
||||||
"Example conversation history to help the AI agent understand the context of the conversation"
|
|
||||||
)
|
|
||||||
|
|||||||
1605
poetry.lock
generated
@@ -1,11 +1,16 @@
|
|||||||
from models.tool_definitions import AgentGoal
|
|
||||||
from typing import Optional
|
|
||||||
import json
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
|
||||||
|
MULTI_GOAL_MODE: bool = None
|
||||||
|
|
||||||
MULTI_GOAL_MODE:bool = None
|
|
||||||
|
|
||||||
def generate_genai_prompt(
|
def generate_genai_prompt(
|
||||||
agent_goal: AgentGoal, conversation_history: str, multi_goal_mode:bool, raw_json: Optional[str] = None
|
agent_goal: AgentGoal,
|
||||||
|
conversation_history: str,
|
||||||
|
multi_goal_mode: bool,
|
||||||
|
raw_json: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a concise prompt for producing or validating JSON instructions
|
Generates a concise prompt for producing or validating JSON instructions
|
||||||
@@ -25,10 +30,12 @@ def generate_genai_prompt(
|
|||||||
prompt_lines.append(
|
prompt_lines.append(
|
||||||
"This is the ongoing history to determine which tool and arguments to gather:"
|
"This is the ongoing history to determine which tool and arguments to gather:"
|
||||||
)
|
)
|
||||||
prompt_lines.append("BEGIN CONVERSATION HISTORY")
|
prompt_lines.append("*BEGIN CONVERSATION HISTORY*")
|
||||||
prompt_lines.append(json.dumps(conversation_history, indent=2))
|
prompt_lines.append(json.dumps(conversation_history, indent=2))
|
||||||
prompt_lines.append("END CONVERSATION HISTORY")
|
prompt_lines.append("*END CONVERSATION HISTORY*")
|
||||||
prompt_lines.append("")
|
prompt_lines.append(
|
||||||
|
"REMINDER: You can use the conversation history to infer arguments for the tools."
|
||||||
|
)
|
||||||
|
|
||||||
# Example Conversation History (from agent_goal)
|
# Example Conversation History (from agent_goal)
|
||||||
if agent_goal.example_conversation_history:
|
if agent_goal.example_conversation_history:
|
||||||
@@ -84,7 +91,22 @@ def generate_genai_prompt(
|
|||||||
"2) If all required arguments are known, set next='confirm' and specify the tool.\n"
|
"2) If all required arguments are known, set next='confirm' and specify the tool.\n"
|
||||||
" The user will confirm before the tool is run.\n"
|
" The user will confirm before the tool is run.\n"
|
||||||
f"3) {generate_toolchain_complete_guidance()}\n"
|
f"3) {generate_toolchain_complete_guidance()}\n"
|
||||||
"4) response should be short and user-friendly.\n"
|
"4) response should be short and user-friendly.\n\n"
|
||||||
|
"Guardrails (always remember!)\n"
|
||||||
|
"1) If any required argument is missing, set next='question' and ask the user.\n"
|
||||||
|
"1) ALWAYS ask a question in your response if next='question'.\n"
|
||||||
|
"2) ALWAYS set next='confirm' if you have arguments\n "
|
||||||
|
'And respond with "let\'s proceed with <tool> (and any other useful info)" \n '
|
||||||
|
+ "DON'T set next='confirm' if you have a question to ask.\n"
|
||||||
|
"EXAMPLE: If you have a question to ask, set next='question' and ask the user.\n"
|
||||||
|
"3) You can carry over arguments from one tool to another.\n "
|
||||||
|
"EXAMPLE: If you asked for an account ID, then use the conversation history to infer that argument "
|
||||||
|
"going forward."
|
||||||
|
"4) If ListAgents in the conversation history is force_confirm='False', you MUST check "
|
||||||
|
+ "if the current tool contains userConfirmation. If it does, please ask the user to confirm details "
|
||||||
|
+ "with the user. userConfirmation overrides force_confirm='False'.\n"
|
||||||
|
+ "EXAMPLE: (force_confirm='False' AND userConfirmation exists on tool) Would you like me to <run tool> "
|
||||||
|
+ "with the following details: <details>?\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validation Task (If raw_json is provided)
|
# Validation Task (If raw_json is provided)
|
||||||
@@ -110,6 +132,7 @@ def generate_genai_prompt(
|
|||||||
|
|
||||||
return "\n".join(prompt_lines)
|
return "\n".join(prompt_lines)
|
||||||
|
|
||||||
|
|
||||||
def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) -> str:
|
def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a prompt for handling tool completion and determining next steps.
|
Generates a prompt for handling tool completion and determining next steps.
|
||||||
@@ -132,7 +155,10 @@ def generate_tool_completion_prompt(current_tool: str, dynamic_result: dict) ->
|
|||||||
f"{generate_pick_new_goal_guidance()}"
|
f"{generate_pick_new_goal_guidance()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_missing_args_prompt(current_tool: str, tool_data: dict, missing_args: list[str]) -> str:
|
|
||||||
|
def generate_missing_args_prompt(
|
||||||
|
current_tool: str, tool_data: dict, missing_args: list[str]
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a prompt for handling missing arguments for a tool.
|
Generates a prompt for handling missing arguments for a tool.
|
||||||
|
|
||||||
@@ -150,7 +176,8 @@ def generate_missing_args_prompt(current_tool: str, tool_data: dict, missing_arg
|
|||||||
"Only provide a valid JSON response without any comments or metadata."
|
"Only provide a valid JSON response without any comments or metadata."
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_multi_goal_mode_if_unset(mode:bool)->None:
|
|
||||||
|
def set_multi_goal_mode_if_unset(mode: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Set multi-mode (used to pass workflow)
|
Set multi-mode (used to pass workflow)
|
||||||
|
|
||||||
@@ -164,7 +191,8 @@ def set_multi_goal_mode_if_unset(mode:bool)->None:
|
|||||||
if MULTI_GOAL_MODE is None:
|
if MULTI_GOAL_MODE is None:
|
||||||
MULTI_GOAL_MODE = mode
|
MULTI_GOAL_MODE = mode
|
||||||
|
|
||||||
def is_multi_goal_mode()-> bool:
|
|
||||||
|
def is_multi_goal_mode() -> bool:
|
||||||
"""
|
"""
|
||||||
Centralized logic for if we're in multi-goal mode.
|
Centralized logic for if we're in multi-goal mode.
|
||||||
|
|
||||||
@@ -176,7 +204,8 @@ def is_multi_goal_mode()-> bool:
|
|||||||
"""
|
"""
|
||||||
return MULTI_GOAL_MODE
|
return MULTI_GOAL_MODE
|
||||||
|
|
||||||
def generate_pick_new_goal_guidance()-> str:
|
|
||||||
|
def generate_pick_new_goal_guidance() -> str:
|
||||||
"""
|
"""
|
||||||
Generates a prompt for guiding the LLM to pick a new goal or be done depending on multi-goal mode.
|
Generates a prompt for guiding the LLM to pick a new goal or be done depending on multi-goal mode.
|
||||||
|
|
||||||
@@ -187,10 +216,11 @@ def generate_pick_new_goal_guidance()-> str:
|
|||||||
str: A prompt string prompting the LLM to when to go to pick-new-goal
|
str: A prompt string prompting the LLM to when to go to pick-new-goal
|
||||||
"""
|
"""
|
||||||
if is_multi_goal_mode():
|
if is_multi_goal_mode():
|
||||||
return 'Next should only be "pick-new-goal" if all tools have been run (use the system prompt to figure that out) or the user explicitly requested to pick a new goal.'
|
return 'Next should only be "pick-new-goal" if all tools have been run for the current goal (use the system prompt to figure that out), or the user explicitly requested to pick a new goal.'
|
||||||
else:
|
else:
|
||||||
return 'Next should never be "pick-new-goal".'
|
return 'Next should never be "pick-new-goal".'
|
||||||
|
|
||||||
|
|
||||||
def generate_toolchain_complete_guidance() -> str:
|
def generate_toolchain_complete_guidance() -> str:
|
||||||
"""
|
"""
|
||||||
Generates a prompt for guiding the LLM to handle the end of the toolchain.
|
Generates a prompt for guiding the LLM to handle the end of the toolchain.
|
||||||
@@ -203,5 +233,5 @@ def generate_toolchain_complete_guidance() -> str:
|
|||||||
"""
|
"""
|
||||||
if is_multi_goal_mode():
|
if is_multi_goal_mode():
|
||||||
return "If no more tools are needed (user_confirmed_tool_run has been run for all), set next='confirm' and tool='ListAgents'."
|
return "If no more tools are needed (user_confirmed_tool_run has been run for all), set next='confirm' and tool='ListAgents'."
|
||||||
else :
|
else:
|
||||||
return "If no more tools are needed (user_confirmed_tool_run has been run for all), set next='done' and tool=''."
|
return "If no more tools are needed (user_confirmed_tool_run has been run for all), set next='done' and tool=''."
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "temporal-AI-agent"
|
name = "temporal_AI_agent"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Temporal AI Agent"
|
description = "Temporal AI Agent"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Steve Androulakis <steve.androulakis@temporal.io>"]
|
authors = [
|
||||||
|
"Steve Androulakis <steve.androulakis@temporal.io>",
|
||||||
|
"Laine Smith <lainecaseysmith@gmail.com>",
|
||||||
|
"Joshua Smith <josh.smith@temporal.io>"
|
||||||
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
# By default, Poetry will find packages automatically,
|
# By default, Poetry will find packages automatically,
|
||||||
@@ -13,7 +17,7 @@ packages = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.urls]
|
[tool.poetry.urls]
|
||||||
"Bug Tracker" = "https://github.com/temporalio/samples-python/issues"
|
"Bug Tracker" = "https://github.com/temporal-community/temporal-ai-agent/issues"
|
||||||
|
|
||||||
[tool.poe.tasks]
|
[tool.poe.tasks]
|
||||||
format = [{cmd = "black ."}, {cmd = "isort ."}]
|
format = [{cmd = "black ."}, {cmd = "isort ."}]
|
||||||
@@ -27,25 +31,42 @@ temporalio = "^1.8.0"
|
|||||||
|
|
||||||
# Standard library modules (e.g. asyncio, collections) don't need to be added
|
# Standard library modules (e.g. asyncio, collections) don't need to be added
|
||||||
# since they're built-in for Python 3.8+.
|
# since they're built-in for Python 3.8+.
|
||||||
ollama = "^0.4.5"
|
litellm = "^1.70.0"
|
||||||
pyyaml = "^6.0.2"
|
pyyaml = "^6.0.2"
|
||||||
fastapi = "^0.115.6"
|
fastapi = "^0.115.6"
|
||||||
uvicorn = "^0.34.0"
|
uvicorn = "^0.34.0"
|
||||||
python-dotenv = "^1.0.1"
|
python-dotenv = "^1.0.1"
|
||||||
openai = "^1.59.2"
|
|
||||||
stripe = "^11.4.1"
|
|
||||||
google-generativeai = "^0.8.4"
|
|
||||||
anthropic = "0.47.0"
|
|
||||||
deepseek = "^1.0.0"
|
|
||||||
requests = "^2.32.3"
|
requests = "^2.32.3"
|
||||||
pandas = "^2.2.3"
|
pandas = "^2.2.3"
|
||||||
|
stripe = "^11.4.1"
|
||||||
gtfs-kit = "^10.1.1"
|
gtfs-kit = "^10.1.1"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^7.3"
|
pytest = ">=8.2"
|
||||||
|
pytest-asyncio = "^0.26.0"
|
||||||
black = "^23.7"
|
black = "^23.7"
|
||||||
isort = "^5.12"
|
isort = "^5.12"
|
||||||
|
mypy = "^1.16.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.4.0"]
|
requires = ["poetry-core>=1.4.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
log_cli = true
|
||||||
|
log_cli_level = "INFO"
|
||||||
|
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
||||||
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
|
norecursedirs = ["vibe"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.10"
|
||||||
|
ignore_missing_imports = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
namespace_packages = true
|
||||||
|
explicit_package_bases = true
|
||||||
|
ignore_errors = true
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from shared.config import get_temporal_client
|
||||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
# Create client connected to server at the given address
|
# Create client connected to server at the given address
|
||||||
client = await Client.connect("localhost:7233")
|
client = await get_temporal_client()
|
||||||
|
|
||||||
workflow_id = "agent-workflow"
|
workflow_id = "agent-workflow"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from tools.search_flights import search_flights
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from tools.search_flights import search_flights
|
||||||
|
|
||||||
# Example usage
|
# Example usage
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
search_args = {"city": "Sydney", "month": "July"}
|
search_args = {"city": "Sydney", "month": "July"}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from tools.search_flights import search_flights
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from tools.search_flights import search_flights
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Suppose user typed "new" for New York, "lon" for London
|
# Suppose user typed "new" for New York, "lon" for London
|
||||||
flights = search_flights("London", "JFK", "2025-01-15", "2025-01-23")
|
flights = search_flights("London", "JFK", "2025-01-15", "2025-01-23")
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
|
||||||
from temporalio.worker import Worker
|
from temporalio.worker import Worker
|
||||||
|
|
||||||
from activities.tool_activities import dynamic_tool_activity
|
from activities.tool_activities import dynamic_tool_activity
|
||||||
|
from shared.config import TEMPORAL_LEGACY_TASK_QUEUE, get_temporal_client
|
||||||
from shared.config import get_temporal_client, TEMPORAL_LEGACY_TASK_QUEUE
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
@@ -24,7 +22,9 @@ async def main():
|
|||||||
activity_executor=activity_executor,
|
activity_executor=activity_executor,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Starting legacy worker, connecting to task queue: {TEMPORAL_LEGACY_TASK_QUEUE}")
|
print(
|
||||||
|
f"Starting legacy worker, connecting to task queue: {TEMPORAL_LEGACY_TASK_QUEUE}"
|
||||||
|
)
|
||||||
await worker.run()
|
await worker.run()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
from ollama import chat, ChatResponse
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
model_name = "mistral"
|
|
||||||
|
|
||||||
# The messages to pass to the model
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Why is the sky blue?",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Call ollama's chat function
|
|
||||||
response: ChatResponse = chat(model=model_name, messages=messages)
|
|
||||||
|
|
||||||
# Print the full message content
|
|
||||||
print(response.message.content)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,34 +1,33 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import os
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
from temporalio.worker import Worker
|
from temporalio.worker import Worker
|
||||||
|
|
||||||
from activities.tool_activities import ToolActivities, dynamic_tool_activity
|
from activities.tool_activities import ToolActivities, dynamic_tool_activity
|
||||||
|
from shared.config import TEMPORAL_TASK_QUEUE, get_temporal_client
|
||||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||||
|
|
||||||
from shared.config import get_temporal_client, TEMPORAL_TASK_QUEUE
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
# Print LLM configuration info
|
# Print LLM configuration info
|
||||||
llm_provider = os.environ.get("LLM_PROVIDER", "openai").lower()
|
llm_model = os.environ.get("LLM_MODEL", "openai/gpt-4")
|
||||||
print(f"Worker will use LLM provider: {llm_provider}")
|
print(f"Worker will use LLM model: {llm_model}")
|
||||||
|
|
||||||
# Create the client
|
# Create the client
|
||||||
client = await get_temporal_client()
|
client = await get_temporal_client()
|
||||||
|
|
||||||
# Initialize the activities class once with the specified LLM provider
|
# Initialize the activities class
|
||||||
activities = ToolActivities()
|
activities = ToolActivities()
|
||||||
print(f"ToolActivities initialized with LLM provider: {llm_provider}")
|
print(f"ToolActivities initialized with LLM model: {llm_model}")
|
||||||
|
|
||||||
# If using Ollama, pre-load the model to avoid cold start latency
|
# If using Ollama, pre-load the model to avoid cold start latency
|
||||||
if llm_provider == "ollama":
|
if llm_model.startswith("ollama"):
|
||||||
print("\n======== OLLAMA MODEL INITIALIZATION ========")
|
print("\n======== OLLAMA MODEL INITIALIZATION ========")
|
||||||
print("Ollama models need to be loaded into memory on first use.")
|
print("Ollama models need to be loaded into memory on first use.")
|
||||||
print("This may take 30+ seconds depending on your hardware and model size.")
|
print("This may take 30+ seconds depending on your hardware and model size.")
|
||||||
@@ -51,8 +50,6 @@ async def main():
|
|||||||
print("Worker ready to process tasks!")
|
print("Worker ready to process tasks!")
|
||||||
logging.basicConfig(level=logging.WARN)
|
logging.basicConfig(level=logging.WARN)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Run the worker
|
# Run the worker
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
||||||
worker = Worker(
|
worker = Worker(
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from shared.config import get_temporal_client
|
|||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
||||||
# Connect to Temporal and signal the workflow
|
# Connect to Temporal and signal the workflow
|
||||||
client = await get_temporal_client()
|
client = await get_temporal_client()
|
||||||
|
|
||||||
|
|||||||
149
setup.md
@@ -14,65 +14,83 @@ If you want to show confirmations/enable the debugging UI that shows tool args,
|
|||||||
SHOW_CONFIRM=True
|
SHOW_CONFIRM=True
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Quick Start with Makefile
|
||||||
|
|
||||||
|
We've provided a Makefile to simplify the setup and running of the application. Here are the main commands:
|
||||||
|
|
||||||
|
```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
|
||||||
|
make run-api # Starts the API server
|
||||||
|
make run-frontend # Starts the frontend development server
|
||||||
|
|
||||||
|
# Additional services
|
||||||
|
make run-train-api # Starts the train API server
|
||||||
|
make run-legacy-worker # Starts the legacy worker
|
||||||
|
make run-enterprise # Builds and runs the enterprise .NET worker
|
||||||
|
|
||||||
|
# Development environment setup
|
||||||
|
make setup-temporal-mac # Installs and starts Temporal server on Mac
|
||||||
|
|
||||||
|
# View all available commands
|
||||||
|
make help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Setup (Alternative to Makefile)
|
||||||
|
|
||||||
|
If you prefer to run commands manually, follow these steps:
|
||||||
|
|
||||||
### Agent Goal Configuration
|
### Agent Goal Configuration
|
||||||
|
|
||||||
The agent can be configured to pursue different goals using the `AGENT_GOAL` environment variable in your `.env` file. If unset, default is `goal_choose_agent_type`.
|
The agent can be configured to pursue different goals using the `AGENT_GOAL` environment variable in your `.env` file. If unset, default is `goal_choose_agent_type`.
|
||||||
|
|
||||||
If the first goal is `goal_choose_agent_type` the agent will support multiple goals using goal categories defined by `GOAL_CATEGORIES` in your .env file. If unset, default is all.
|
If the first goal is `goal_choose_agent_type` the agent will support multiple goals using goal categories defined by `GOAL_CATEGORIES` in your .env file. If unset, default is all. We recommend starting with `fin`.
|
||||||
```bash
|
```bash
|
||||||
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin
|
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin
|
||||||
```
|
```
|
||||||
|
|
||||||
See the section Goal-Specific Tool Configuration below for tool configuration for specific goals.
|
See the section Goal-Specific Tool Configuration below for tool configuration for specific goals.
|
||||||
|
|
||||||
### LLM Provider Configuration
|
### LLM Configuration
|
||||||
|
|
||||||
The agent can use OpenAI's GPT-4o, Google Gemini, Anthropic Claude, or a local LLM via Ollama. Set the `LLM_PROVIDER` environment variable in your `.env` file to choose the desired provider:
|
Note: We recommend using OpenAI's GPT-4o or Claude 3.5 Sonnet for the best results. There can be significant differences in performance and capabilities between models, especially for complex tasks.
|
||||||
|
|
||||||
- `LLM_PROVIDER=openai` for OpenAI's GPT-4o
|
The agent uses LiteLLM to interact with various LLM providers. Configure the following environment variables in your `.env` file:
|
||||||
- `LLM_PROVIDER=google` for Google Gemini
|
|
||||||
- `LLM_PROVIDER=anthropic` for Anthropic Claude
|
|
||||||
- `LLM_PROVIDER=deepseek` for DeepSeek-V3
|
|
||||||
- `LLM_PROVIDER=ollama` for running LLMs via [Ollama](https://ollama.ai) (not recommended for this use case)
|
|
||||||
|
|
||||||
### Option 1: OpenAI
|
- `LLM_MODEL`: The model to use (e.g., "openai/gpt-4o", "anthropic/claude-3-sonnet", "google/gemini-pro", etc.)
|
||||||
|
- `LLM_KEY`: Your API key for the selected provider
|
||||||
|
- `LLM_BASE_URL`: (Optional) Custom base URL for the LLM provider. Useful for:
|
||||||
|
- Using Ollama with a custom endpoint
|
||||||
|
- Using a proxy or custom API gateway
|
||||||
|
- Testing with different API versions
|
||||||
|
|
||||||
If using OpenAI, ensure you have an OpenAI key for the GPT-4o model. Set this in the `OPENAI_API_KEY` environment variable in `.env`.
|
LiteLLM will automatically detect the provider based on the model name. For example:
|
||||||
|
- For OpenAI models: `openai/gpt-4o` or `openai/gpt-3.5-turbo`
|
||||||
|
- For Anthropic models: `anthropic/claude-3-sonnet`
|
||||||
|
- For Google models: `google/gemini-pro`
|
||||||
|
- For Ollama models: `ollama/mistral` (requires `LLM_BASE_URL` set to your Ollama server)
|
||||||
|
|
||||||
### Option 2: Google Gemini
|
Example configurations:
|
||||||
|
```bash
|
||||||
|
# For OpenAI
|
||||||
|
LLM_MODEL=openai/gpt-4o
|
||||||
|
LLM_KEY=your-api-key-here
|
||||||
|
|
||||||
To use Google Gemini:
|
# For Anthropic
|
||||||
|
LLM_MODEL=anthropic/claude-3-sonnet
|
||||||
|
LLM_KEY=your-api-key-here
|
||||||
|
|
||||||
1. Obtain a Google API key and set it in the `GOOGLE_API_KEY` environment variable in `.env`.
|
# For Ollama with custom URL
|
||||||
2. Set `LLM_PROVIDER=google` in your `.env` file.
|
LLM_MODEL=ollama/mistral
|
||||||
|
LLM_BASE_URL=http://localhost:11434
|
||||||
|
```
|
||||||
|
|
||||||
### Option 3: Anthropic Claude (recommended)
|
For a complete list of supported models and providers, visit the [LiteLLM documentation](https://docs.litellm.ai/docs/providers).
|
||||||
|
|
||||||
I find that Claude Sonnet 3.5 performs better than the other hosted LLMs for this use case.
|
|
||||||
|
|
||||||
To use Anthropic:
|
|
||||||
|
|
||||||
1. Obtain an Anthropic API key and set it in the `ANTHROPIC_API_KEY` environment variable in `.env`.
|
|
||||||
2. Set `LLM_PROVIDER=anthropic` in your `.env` file.
|
|
||||||
|
|
||||||
### Option 4: Deepseek-V3
|
|
||||||
|
|
||||||
To use Deepseek-V3:
|
|
||||||
|
|
||||||
1. Obtain a Deepseek API key and set it in the `DEEPSEEK_API_KEY` environment variable in `.env`.
|
|
||||||
2. Set `LLM_PROVIDER=deepseek` in your `.env` file.
|
|
||||||
|
|
||||||
### Option 5: Local LLM via Ollama (not recommended)
|
|
||||||
|
|
||||||
To use a local LLM with Ollama:
|
|
||||||
|
|
||||||
1. Install [Ollama](https://ollama.com) and the [Qwen2.5 14B](https://ollama.com/library/qwen2.5) model.
|
|
||||||
- Run `ollama run <OLLAMA_MODEL_NAME>` to start the model. Note that this model is about 9GB to download.
|
|
||||||
- Example: `ollama run qwen2.5:14b`
|
|
||||||
|
|
||||||
2. Set `LLM_PROVIDER=ollama` in your `.env` file and `OLLAMA_MODEL_NAME` to the name of the model you installed.
|
|
||||||
|
|
||||||
Note: I found the other (hosted) LLMs to be MUCH more reliable for this use case. However, you can switch to Ollama if desired, and choose a suitably large model if your computer has the resources.
|
|
||||||
|
|
||||||
## Configuring Temporal Connection
|
## Configuring Temporal Connection
|
||||||
|
|
||||||
@@ -93,10 +111,32 @@ temporal server start-dev
|
|||||||
```
|
```
|
||||||
See the [Temporal documentation](https://learn.temporal.io/getting_started/python/dev_environment/) for other platforms.
|
See the [Temporal documentation](https://learn.temporal.io/getting_started/python/dev_environment/) for other platforms.
|
||||||
|
|
||||||
|
You can also run a local Temporal server using Docker Compose. See the `Development with Docker` section below.
|
||||||
|
|
||||||
## Running the Application
|
## Running the Application
|
||||||
|
|
||||||
### Python Backend
|
### Docker
|
||||||
|
- All services are defined in `docker-compose.yml` (includes a Temporal server).
|
||||||
|
- **Dev overrides** (mounted code, live‑reload commands) live in `docker-compose.override.yml` and are **auto‑merged** on `docker compose up`.
|
||||||
|
- To start **development** mode (with hot‑reload):
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
# quick rebuild without infra:
|
||||||
|
docker compose up -d --no-deps --build api train-api worker frontend
|
||||||
|
```
|
||||||
|
- To run **production** mode (ignore dev overrides):
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Default urls:
|
||||||
|
* Temporal UI: [http://localhost:8080](http://localhost:8080)
|
||||||
|
* API: [http://localhost:8000](http://localhost:8000)
|
||||||
|
* Frontend: [http://localhost:5173](http://localhost:5173)
|
||||||
|
|
||||||
|
### Local Machine (no docker)
|
||||||
|
|
||||||
|
**Python Backend**
|
||||||
|
|
||||||
Requires [Poetry](https://python-poetry.org/) to manage dependencies.
|
Requires [Poetry](https://python-poetry.org/) to manage dependencies.
|
||||||
|
|
||||||
@@ -119,7 +159,7 @@ 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.
|
||||||
|
|
||||||
### React UI
|
**React UI**
|
||||||
Start the frontend:
|
Start the frontend:
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
@@ -129,7 +169,6 @@ npx vite
|
|||||||
Access the UI at `http://localhost:5173`
|
Access the UI at `http://localhost:5173`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Goal-Specific Tool Configuration
|
## Goal-Specific Tool Configuration
|
||||||
Here is configuration guidance for specific goals. Travel and financial goals have configuration & setup as below.
|
Here is configuration guidance for specific goals. Travel and financial goals have configuration & setup as below.
|
||||||
### Goal: Find an event in Australia / New Zealand, book flights to it and invoice the user for the cost
|
### Goal: Find an event in Australia / New Zealand, book flights to it and invoice the user for the cost
|
||||||
@@ -145,7 +184,7 @@ Here is configuration guidance for specific goals. Travel and financial goals ha
|
|||||||
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env
|
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env
|
||||||
* It's free to sign up and get a key at [Stripe](https://stripe.com/)
|
* It's free to sign up and get a key at [Stripe](https://stripe.com/)
|
||||||
* Set permissions for read-write on: `Credit Notes, Invoices, Customers and Customer Sessions`
|
* Set permissions for read-write on: `Credit Notes, Invoices, Customers and Customer Sessions`
|
||||||
* If you're lazy go to `tools/create_invoice.py` and replace the `create_invoice` function with the mock `create_invoice_example` that exists in the same file.
|
* If you don't have a Stripe key, comment out the STRIPE_API_KEY in the .env file, and a dummy invoice will be created rather than a Stripe invoice. The function can be found in `tools/create_invoice.py`
|
||||||
|
|
||||||
### Goal: Find a Premier League match, book train tickets to it and invoice the user for the cost (Replay 2025 Keynote)
|
### Goal: Find a Premier League match, book train tickets to it and invoice the user for the cost (Replay 2025 Keynote)
|
||||||
- `AGENT_GOAL=goal_match_train_invoice` - Focuses on Premier League match attendance with train booking and invoice generation
|
- `AGENT_GOAL=goal_match_train_invoice` - Focuses on Premier League match attendance with train booking and invoice generation
|
||||||
@@ -153,8 +192,7 @@ Here is configuration guidance for specific goals. Travel and financial goals ha
|
|||||||
- Note, there is failure built in to this demo (the train booking step) to show how the agent can handle failures and retry. See Tool Configuration below for details.
|
- Note, there is failure built in to this demo (the train booking step) to show how the agent can handle failures and retry. See Tool Configuration below for details.
|
||||||
#### Configuring Agent Goal: goal_match_train_invoice
|
#### Configuring Agent Goal: goal_match_train_invoice
|
||||||
NOTE: This goal was developed for an on-stage demo and has failure (and its resolution) built in to show how the agent can handle failures and retry.
|
NOTE: This goal was developed for an on-stage demo and has failure (and its resolution) built in to show how the agent can handle failures and retry.
|
||||||
* Finding a match requires a key from [Football Data](https://www.football-data.org). Sign up for a free account, then see the 'My Account' page to get your API token. Set `FOOTBALL_DATA_API_KEY` to this value.
|
* Omit `FOOTBALL_DATA_API_KEY` from .env for the `SearchFixtures` tool to automatically return mock Premier League fixtures. Finding a real match requires a key from [Football Data](https://www.football-data.org). Sign up for a free account, then see the 'My Account' page to get your API token.
|
||||||
* If you're lazy go to `tools/search_fixtures.py` and replace the `search_fixtures` function with the mock `search_fixtures_example` that exists in the same file.
|
|
||||||
* We use a mock function to search for trains. Start the train API server to use the real API: `python thirdparty/train_api.py`
|
* We use a mock function to search for trains. Start the train API server to use the real API: `python thirdparty/train_api.py`
|
||||||
* * The train activity is 'enterprise' so it's written in C# and requires a .NET runtime. See the [.NET backend](#net-(enterprise)-backend) section for details on running it.
|
* * The train activity is 'enterprise' so it's written in C# and requires a .NET runtime. See the [.NET backend](#net-(enterprise)-backend) section for details on running it.
|
||||||
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env
|
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env
|
||||||
@@ -204,11 +242,14 @@ FIN_START_REAL_WORKFLOW=FALSE #set this to true to start a real workflow
|
|||||||
By default it will _not_ make a real workflow, it'll just fake it. If you get the worker running and want to start a workflow, in your [.env](./.env):
|
By default it will _not_ make a real workflow, it'll just fake it. If you get the worker running and want to start a workflow, in your [.env](./.env):
|
||||||
```bash
|
```bash
|
||||||
FIN_START_REAL_WORKFLOW=FALSE #set this to true to start a real workflow
|
FIN_START_REAL_WORKFLOW=FALSE #set this to true to start a real workflow
|
||||||
|
```
|
||||||
|
|
||||||
#### Goals: HR/PTO
|
#### Goals: HR/PTO
|
||||||
Make sure you have the mock users you want in (such as yourself) in [the PTO mock data file](./tools/data/employee_pto_data.json).
|
Make sure you have the mock users you want in (such as yourself) in [the PTO mock data file](./tools/data/employee_pto_data.json).
|
||||||
|
|
||||||
|
#### Goals: Ecommerce
|
||||||
|
Make sure you have the mock orders you want in (such as those with real tracking numbers) in [the mock orders file](./tools/data/customer_order_data.json).
|
||||||
|
|
||||||
|
|
||||||
## Customizing the Agent Further
|
## Customizing the Agent Further
|
||||||
- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them)
|
- `tool_registry.py` contains the mapping of tool names to tool definitions (so the AI understands how to use them)
|
||||||
@@ -217,3 +258,15 @@ Make sure you have the mock users you want in (such as yourself) in [the PTO moc
|
|||||||
- Note the mapping in `tools/__init__.py` to each tool
|
- Note the mapping in `tools/__init__.py` to each tool
|
||||||
|
|
||||||
For more details, check out [adding goals and tools guide](./adding-goals-and-tools.md).
|
For more details, check out [adding goals and tools guide](./adding-goals-and-tools.md).
|
||||||
|
|
||||||
|
## Setup Checklist
|
||||||
|
[ ] copy `.env.example` 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) configure your Temporal Cloud settings in `.env` <br />
|
||||||
|
[ ] `poetry run python scripts/run_worker.py` <br />
|
||||||
|
[ ] `poetry run uvicorn api.main:app --reload` <br />
|
||||||
|
[ ] `cd frontend`, `npm install`, `npx vite` <br />
|
||||||
|
[ ] Access the UI at `http://localhost:5173` <br />
|
||||||
|
|
||||||
|
And that's it! Happy AI Agent Exploring!
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from temporalio.client import Client
|
from temporalio.client import Client
|
||||||
from temporalio.service import TLSConfig
|
from temporalio.service import TLSConfig
|
||||||
@@ -9,13 +10,16 @@ load_dotenv(override=True)
|
|||||||
TEMPORAL_ADDRESS = os.getenv("TEMPORAL_ADDRESS", "localhost:7233")
|
TEMPORAL_ADDRESS = os.getenv("TEMPORAL_ADDRESS", "localhost:7233")
|
||||||
TEMPORAL_NAMESPACE = os.getenv("TEMPORAL_NAMESPACE", "default")
|
TEMPORAL_NAMESPACE = os.getenv("TEMPORAL_NAMESPACE", "default")
|
||||||
TEMPORAL_TASK_QUEUE = os.getenv("TEMPORAL_TASK_QUEUE", "agent-task-queue")
|
TEMPORAL_TASK_QUEUE = os.getenv("TEMPORAL_TASK_QUEUE", "agent-task-queue")
|
||||||
TEMPORAL_LEGACY_TASK_QUEUE = os.getenv("TEMPORAL_LEGACY_TASK_QUEUE", "agent-task-queue-legacy")
|
TEMPORAL_LEGACY_TASK_QUEUE = os.getenv(
|
||||||
|
"TEMPORAL_LEGACY_TASK_QUEUE", "agent-task-queue-legacy"
|
||||||
|
)
|
||||||
|
|
||||||
# Authentication settings
|
# Authentication settings
|
||||||
TEMPORAL_TLS_CERT = os.getenv("TEMPORAL_TLS_CERT", "")
|
TEMPORAL_TLS_CERT = os.getenv("TEMPORAL_TLS_CERT", "")
|
||||||
TEMPORAL_TLS_KEY = os.getenv("TEMPORAL_TLS_KEY", "")
|
TEMPORAL_TLS_KEY = os.getenv("TEMPORAL_TLS_KEY", "")
|
||||||
TEMPORAL_API_KEY = os.getenv("TEMPORAL_API_KEY", "")
|
TEMPORAL_API_KEY = os.getenv("TEMPORAL_API_KEY", "")
|
||||||
|
|
||||||
|
|
||||||
async def get_temporal_client() -> Client:
|
async def get_temporal_client() -> Client:
|
||||||
"""
|
"""
|
||||||
Creates a Temporal client based on environment configuration.
|
Creates a Temporal client based on environment configuration.
|
||||||
|
|||||||
350
tests/README.md
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# Temporal AI Agent - Testing Guide
|
||||||
|
|
||||||
|
This directory contains comprehensive tests for the Temporal AI Agent project. The tests cover workflows, activities, and integration scenarios using Temporal's testing framework.
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── README.md # This file - testing documentation
|
||||||
|
├── conftest.py # Test configuration and fixtures
|
||||||
|
├── test_agent_goal_workflow.py # Workflow tests
|
||||||
|
├── test_tool_activities.py # Activity tests
|
||||||
|
└── workflowtests/ # Legacy workflow tests
|
||||||
|
└── agent_goal_workflow_test.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Types
|
||||||
|
|
||||||
|
### 1. Workflow Tests (`test_agent_goal_workflow.py`)
|
||||||
|
|
||||||
|
Tests the main `AgentGoalWorkflow` class covering:
|
||||||
|
|
||||||
|
- **Workflow Initialization**: Basic workflow startup and state management
|
||||||
|
- **Signal Handling**: Testing user_prompt, confirm, end_chat signals
|
||||||
|
- **Query Methods**: Testing all workflow query endpoints
|
||||||
|
- **State Management**: Conversation history, goal changes, tool data
|
||||||
|
- **Validation Flow**: Prompt validation and error handling
|
||||||
|
- **Tool Execution Flow**: Confirmation and tool execution cycles
|
||||||
|
|
||||||
|
### 2. Activity Tests (`test_tool_activities.py`)
|
||||||
|
|
||||||
|
Tests the `ToolActivities` class and `dynamic_tool_activity` function:
|
||||||
|
|
||||||
|
- **LLM Integration**: Testing agent_toolPlanner with mocked LLM responses
|
||||||
|
- **Validation Logic**: Testing agent_validatePrompt with various scenarios
|
||||||
|
- **Environment Configuration**: Testing get_wf_env_vars with different env setups
|
||||||
|
- **JSON Processing**: Testing response parsing and sanitization
|
||||||
|
- **Dynamic Tool Execution**: Testing the dynamic activity dispatcher
|
||||||
|
- **Integration**: End-to-end activity execution in Temporal workers
|
||||||
|
|
||||||
|
### 3. Configuration Tests (`conftest.py`)
|
||||||
|
|
||||||
|
Provides shared test fixtures and configuration:
|
||||||
|
|
||||||
|
- **Temporal Environment**: Local and time-skipping test environments
|
||||||
|
- **Sample Data**: Pre-configured agent goals, conversation history, inputs
|
||||||
|
- **Test Client**: Configured Temporal client for testing
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Ensure you have the required dependencies installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry install --with dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Test Execution
|
||||||
|
|
||||||
|
Run all tests:
|
||||||
|
```bash
|
||||||
|
poetry run pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
Run specific test files:
|
||||||
|
```bash
|
||||||
|
# Workflow tests only
|
||||||
|
poetry run pytest tests/test_agent_goal_workflow.py
|
||||||
|
|
||||||
|
# Activity tests only
|
||||||
|
poetry run pytest tests/test_tool_activities.py
|
||||||
|
|
||||||
|
# Legacy tests
|
||||||
|
poetry run pytest tests/workflowtests/
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with verbose output:
|
||||||
|
```bash
|
||||||
|
poetry run pytest -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Environment Options
|
||||||
|
|
||||||
|
The tests support different Temporal environments via the `--workflow-environment` flag:
|
||||||
|
|
||||||
|
#### Local Environment (Default)
|
||||||
|
Uses a local Temporal test server:
|
||||||
|
```bash
|
||||||
|
poetry run pytest --workflow-environment=local
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Time-Skipping Environment
|
||||||
|
Uses Temporal's time-skipping test environment for faster execution:
|
||||||
|
```bash
|
||||||
|
poetry run pytest --workflow-environment=time-skipping
|
||||||
|
```
|
||||||
|
|
||||||
|
#### External Server
|
||||||
|
Connect to an existing Temporal server:
|
||||||
|
```bash
|
||||||
|
poetry run pytest --workflow-environment=localhost:7233
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Setup Script for AI Agent environments such as OpenAI Codex
|
||||||
|
```bash
|
||||||
|
export SHELL=/bin/bash
|
||||||
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
ls
|
||||||
|
poetry install --with dev
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Pre-download the temporal test server binary
|
||||||
|
poetry run python3 -c "
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from temporalio.testing import WorkflowEnvironment
|
||||||
|
|
||||||
|
async def predownload():
|
||||||
|
try:
|
||||||
|
print('Starting test server download...')
|
||||||
|
env = await WorkflowEnvironment.start_time_skipping()
|
||||||
|
print('Test server downloaded and started successfully')
|
||||||
|
await env.shutdown()
|
||||||
|
print('Test server shut down successfully')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error during download: {e}')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
asyncio.run(predownload())
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filtering Tests
|
||||||
|
|
||||||
|
Run tests by pattern:
|
||||||
|
```bash
|
||||||
|
# Run only validation tests
|
||||||
|
poetry run pytest -k "validation"
|
||||||
|
|
||||||
|
# Run only workflow tests
|
||||||
|
poetry run pytest -k "workflow"
|
||||||
|
|
||||||
|
# Run only activity tests
|
||||||
|
poetry run pytest -k "activity"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests by marker (if you add custom markers):
|
||||||
|
```bash
|
||||||
|
# Run only integration tests
|
||||||
|
poetry run pytest -m integration
|
||||||
|
|
||||||
|
# Skip slow tests
|
||||||
|
poetry run pytest -m "not slow"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Configuration
|
||||||
|
|
||||||
|
### Test Discovery
|
||||||
|
|
||||||
|
The `vibe/` directory is excluded from test collection to avoid conflicts with sample tests. This is configured in `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
norecursedirs = ["vibe"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Tests respect the following environment variables:
|
||||||
|
|
||||||
|
- `LLM_MODEL`: Model to use for LLM testing (defaults to "openai/gpt-4")
|
||||||
|
- `LLM_KEY`: API key for LLM service
|
||||||
|
- `LLM_BASE_URL`: Custom base URL for LLM service
|
||||||
|
- `SHOW_CONFIRM`: Whether to show confirmation dialogs
|
||||||
|
- `AGENT_GOAL`: Default agent goal setting
|
||||||
|
|
||||||
|
### Mocking Strategy
|
||||||
|
|
||||||
|
The tests use extensive mocking to avoid external dependencies:
|
||||||
|
|
||||||
|
- **LLM Calls**: Mocked using `unittest.mock` to avoid actual API calls
|
||||||
|
- **Tool Handlers**: Mocked to test workflow logic without tool execution
|
||||||
|
- **Environment Variables**: Patched for consistent test environments
|
||||||
|
|
||||||
|
## Writing New Tests
|
||||||
|
|
||||||
|
### Test Naming Convention
|
||||||
|
|
||||||
|
- Test files: `test_<module_name>.py`
|
||||||
|
- Test classes: `Test<ClassName>`
|
||||||
|
- Test methods: `test_<functionality>_<scenario>`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
class TestAgentGoalWorkflow:
|
||||||
|
async def test_user_prompt_signal_valid_input(self, client, sample_combined_input):
|
||||||
|
# Test implementation
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Fixtures
|
||||||
|
|
||||||
|
Leverage the provided fixtures for consistent test data:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_my_workflow(self, client, sample_agent_goal, sample_conversation_history):
|
||||||
|
# client: Temporal test client
|
||||||
|
# sample_agent_goal: Pre-configured AgentGoal
|
||||||
|
# sample_conversation_history: Sample conversation data
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mocking External Dependencies
|
||||||
|
|
||||||
|
Always mock external services:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@patch('activities.tool_activities.completion')
|
||||||
|
async def test_llm_integration(self, mock_completion):
|
||||||
|
mock_completion.return_value.choices[0].message.content = '{"test": "response"}'
|
||||||
|
# Test implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Workflow Signals and Queries
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_workflow_signal(self, client, sample_combined_input):
|
||||||
|
# Start workflow
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
sample_combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send signal
|
||||||
|
await handle.signal(AgentGoalWorkflow.user_prompt, "test message")
|
||||||
|
|
||||||
|
# Query state
|
||||||
|
conversation = await handle.query(AgentGoalWorkflow.get_conversation_history)
|
||||||
|
|
||||||
|
# End workflow
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
result = await handle.result()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Data and Fixtures
|
||||||
|
|
||||||
|
### Sample Agent Goal
|
||||||
|
|
||||||
|
The `sample_agent_goal` fixture provides a basic agent goal with:
|
||||||
|
- Goal ID: "test_goal"
|
||||||
|
- One test tool with a required string argument
|
||||||
|
- Suitable for most workflow testing scenarios
|
||||||
|
|
||||||
|
### Sample Conversation History
|
||||||
|
|
||||||
|
The `sample_conversation_history` fixture provides:
|
||||||
|
- Basic user and agent message exchange
|
||||||
|
- Proper message format for testing
|
||||||
|
|
||||||
|
### Sample Combined Input
|
||||||
|
|
||||||
|
The `sample_combined_input` fixture provides:
|
||||||
|
- Complete workflow input with agent goal and tool params
|
||||||
|
- Conversation summary and prompt queue
|
||||||
|
- Ready for workflow execution
|
||||||
|
|
||||||
|
## Debugging Tests
|
||||||
|
|
||||||
|
### Verbose Logging
|
||||||
|
|
||||||
|
Enable detailed logging:
|
||||||
|
```bash
|
||||||
|
poetry run pytest --log-cli-level=DEBUG -s
|
||||||
|
```
|
||||||
|
|
||||||
|
### Temporal Web UI
|
||||||
|
|
||||||
|
When using local environment, access Temporal Web UI at http://localhost:8233 to inspect workflow executions during tests.
|
||||||
|
|
||||||
|
### Test Isolation
|
||||||
|
|
||||||
|
Each test uses unique task queue names to prevent interference:
|
||||||
|
```python
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Continuous Integration
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Test
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- run: pip install poetry
|
||||||
|
- run: poetry install --with dev
|
||||||
|
- run: poetry run pytest --workflow-environment=time-skipping
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
|
||||||
|
Generate coverage reports:
|
||||||
|
```bash
|
||||||
|
poetry add --group dev pytest-cov
|
||||||
|
poetry run pytest --cov=workflows --cov=activities --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Mock External Dependencies**: Always mock LLM calls, file I/O, and network requests
|
||||||
|
2. **Use Time-Skipping**: For CI/CD, prefer time-skipping environment for speed
|
||||||
|
3. **Unique Identifiers**: Use UUIDs for workflow IDs and task queues
|
||||||
|
4. **Clean Shutdown**: Always end workflows properly in tests
|
||||||
|
5. **Descriptive Names**: Use clear, descriptive test names
|
||||||
|
6. **Test Edge Cases**: Include error scenarios and validation failures
|
||||||
|
7. **Keep Tests Fast**: Use mocks to avoid slow external calls
|
||||||
|
8. **Isolate Tests**: Ensure tests don't depend on each other
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Workflow Timeout**: Increase timeouts or use time-skipping environment
|
||||||
|
2. **Mock Not Working**: Check patch decorators and import paths
|
||||||
|
3. **Test Hanging**: Ensure workflows are properly ended with signals
|
||||||
|
4. **Environment Issues**: Check environment variable settings
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
- Check Temporal Python SDK documentation
|
||||||
|
- Review existing test patterns in the codebase
|
||||||
|
- Use `poetry run pytest --collect-only` to verify test discovery
|
||||||
|
- Run with `-v` flag for detailed output
|
||||||
|
|
||||||
|
## Legacy Tests
|
||||||
|
|
||||||
|
The `workflowtests/` directory contains legacy tests. New tests should be added to the main `tests/` directory following the patterns established in this guide.
|
||||||
@@ -41,7 +41,12 @@ def event_loop():
|
|||||||
async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]:
|
async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]:
|
||||||
env_type = request.config.getoption("--workflow-environment")
|
env_type = request.config.getoption("--workflow-environment")
|
||||||
if env_type == "local":
|
if env_type == "local":
|
||||||
env = await WorkflowEnvironment.start_local()
|
env = await WorkflowEnvironment.start_local(
|
||||||
|
dev_server_extra_args=[
|
||||||
|
"--dynamic-config-value",
|
||||||
|
"frontend.enableExecuteMultiOperation=true",
|
||||||
|
]
|
||||||
|
)
|
||||||
elif env_type == "time-skipping":
|
elif env_type == "time-skipping":
|
||||||
env = await WorkflowEnvironment.start_time_skipping()
|
env = await WorkflowEnvironment.start_time_skipping()
|
||||||
else:
|
else:
|
||||||
@@ -53,3 +58,54 @@ async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]:
|
|||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
async def client(env: WorkflowEnvironment) -> Client:
|
async def client(env: WorkflowEnvironment) -> Client:
|
||||||
return env.client
|
return env.client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_agent_goal():
|
||||||
|
"""Sample agent goal for testing."""
|
||||||
|
from models.tool_definitions import AgentGoal, ToolArgument, ToolDefinition
|
||||||
|
|
||||||
|
return AgentGoal(
|
||||||
|
id="test_goal",
|
||||||
|
category_tag="test",
|
||||||
|
agent_name="TestAgent",
|
||||||
|
agent_friendly_description="A test agent for testing purposes",
|
||||||
|
description="Test goal for agent testing",
|
||||||
|
tools=[
|
||||||
|
ToolDefinition(
|
||||||
|
name="TestTool",
|
||||||
|
description="A test tool for testing purposes",
|
||||||
|
arguments=[
|
||||||
|
ToolArgument(
|
||||||
|
name="test_arg", type="string", description="A test argument"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_conversation_history():
|
||||||
|
"""Sample conversation history for testing."""
|
||||||
|
return {
|
||||||
|
"messages": [
|
||||||
|
{"actor": "user", "response": "Hello, I need help with testing"},
|
||||||
|
{"actor": "agent", "response": "I can help you with that"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_combined_input(sample_agent_goal):
|
||||||
|
"""Sample combined input for workflow testing."""
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from models.data_types import AgentGoalWorkflowParams, CombinedInput
|
||||||
|
|
||||||
|
tool_params = AgentGoalWorkflowParams(
|
||||||
|
conversation_summary="Test conversation summary",
|
||||||
|
prompt_queue=deque(), # Start with empty queue for most tests
|
||||||
|
)
|
||||||
|
|
||||||
|
return CombinedInput(agent_goal=sample_agent_goal, tool_params=tool_params)
|
||||||
|
|||||||
543
tests/test_agent_goal_workflow.py
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from temporalio import activity
|
||||||
|
from temporalio.client import Client
|
||||||
|
from temporalio.worker import Worker
|
||||||
|
|
||||||
|
from models.data_types import (
|
||||||
|
AgentGoalWorkflowParams,
|
||||||
|
CombinedInput,
|
||||||
|
EnvLookupInput,
|
||||||
|
EnvLookupOutput,
|
||||||
|
ToolPromptInput,
|
||||||
|
ValidationInput,
|
||||||
|
ValidationResult,
|
||||||
|
)
|
||||||
|
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||||
|
|
||||||
|
|
||||||
|
class TestAgentGoalWorkflow:
|
||||||
|
"""Test cases for AgentGoalWorkflow."""
|
||||||
|
|
||||||
|
async def test_workflow_initialization(
|
||||||
|
self, client: Client, sample_combined_input: CombinedInput
|
||||||
|
):
|
||||||
|
"""Test workflow can be initialized and started."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[mock_get_wf_env_vars],
|
||||||
|
):
|
||||||
|
# Start workflow but don't wait for completion since it runs indefinitely
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
sample_combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify workflow is running
|
||||||
|
assert handle is not None
|
||||||
|
|
||||||
|
# Query the workflow to check initial state
|
||||||
|
conversation_history = await handle.query(
|
||||||
|
AgentGoalWorkflow.get_conversation_history
|
||||||
|
)
|
||||||
|
assert isinstance(conversation_history, dict)
|
||||||
|
assert "messages" in conversation_history
|
||||||
|
|
||||||
|
# Test goal query
|
||||||
|
agent_goal = await handle.query(AgentGoalWorkflow.get_agent_goal)
|
||||||
|
assert agent_goal == sample_combined_input.agent_goal
|
||||||
|
|
||||||
|
# End the workflow
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
result = await handle.result()
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
async def test_user_prompt_signal(
|
||||||
|
self, client: Client, sample_combined_input: CombinedInput
|
||||||
|
):
|
||||||
|
"""Test user_prompt signal handling."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
@activity.defn(name="agent_validatePrompt")
|
||||||
|
async def mock_agent_validatePrompt(
|
||||||
|
validation_input: ValidationInput,
|
||||||
|
) -> ValidationResult:
|
||||||
|
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||||
|
|
||||||
|
@activity.defn(name="agent_toolPlanner")
|
||||||
|
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
||||||
|
return {"next": "done", "response": "Test response from LLM"}
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[
|
||||||
|
mock_get_wf_env_vars,
|
||||||
|
mock_agent_validatePrompt,
|
||||||
|
mock_agent_toolPlanner,
|
||||||
|
],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
sample_combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send user prompt
|
||||||
|
await handle.signal(
|
||||||
|
AgentGoalWorkflow.user_prompt, "Hello, this is a test message"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for workflow to complete (it should end due to "done" next step)
|
||||||
|
result = await handle.result()
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
# Verify the conversation includes our message
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
conversation_history = json.loads(result.replace("'", '"'))
|
||||||
|
except Exception:
|
||||||
|
# Fallback to eval if json fails
|
||||||
|
conversation_history = eval(result)
|
||||||
|
messages = conversation_history["messages"]
|
||||||
|
|
||||||
|
# Should have our user message and agent response
|
||||||
|
user_messages = [msg for msg in messages if msg["actor"] == "user"]
|
||||||
|
assert len(user_messages) > 0
|
||||||
|
assert any(
|
||||||
|
"Hello, this is a test message" in str(msg["response"])
|
||||||
|
for msg in user_messages
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_confirm_signal(
|
||||||
|
self, client: Client, sample_combined_input: CombinedInput
|
||||||
|
):
|
||||||
|
"""Test confirm signal handling for tool execution."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
@activity.defn(name="agent_validatePrompt")
|
||||||
|
async def mock_agent_validatePrompt(
|
||||||
|
validation_input: ValidationInput,
|
||||||
|
) -> ValidationResult:
|
||||||
|
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||||
|
|
||||||
|
@activity.defn(name="agent_toolPlanner")
|
||||||
|
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
||||||
|
return {
|
||||||
|
"next": "confirm",
|
||||||
|
"tool": "TestTool",
|
||||||
|
"args": {"test_arg": "test_value"},
|
||||||
|
"response": "Ready to execute tool",
|
||||||
|
}
|
||||||
|
|
||||||
|
@activity.defn(name="TestTool")
|
||||||
|
async def mock_test_tool(args: dict) -> dict:
|
||||||
|
return {"result": "Test tool executed successfully"}
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[
|
||||||
|
mock_get_wf_env_vars,
|
||||||
|
mock_agent_validatePrompt,
|
||||||
|
mock_agent_toolPlanner,
|
||||||
|
mock_test_tool,
|
||||||
|
],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
sample_combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send user prompt that will require confirmation
|
||||||
|
await handle.signal(AgentGoalWorkflow.user_prompt, "Execute the test tool")
|
||||||
|
|
||||||
|
# Query to check tool data is set
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1) # Give workflow time to process
|
||||||
|
|
||||||
|
tool_data = await handle.query(AgentGoalWorkflow.get_latest_tool_data)
|
||||||
|
if tool_data:
|
||||||
|
assert tool_data.get("tool") == "TestTool"
|
||||||
|
assert tool_data.get("next") == "confirm"
|
||||||
|
|
||||||
|
# Send confirmation and end chat
|
||||||
|
await handle.signal(AgentGoalWorkflow.confirm)
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
|
||||||
|
result = await handle.result()
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
async def test_validation_failure(
|
||||||
|
self, client: Client, sample_combined_input: CombinedInput
|
||||||
|
):
|
||||||
|
"""Test workflow handles validation failures correctly."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
@activity.defn(name="agent_validatePrompt")
|
||||||
|
async def mock_agent_validatePrompt(
|
||||||
|
validation_input: ValidationInput,
|
||||||
|
) -> ValidationResult:
|
||||||
|
return ValidationResult(
|
||||||
|
validationResult=False,
|
||||||
|
validationFailedReason={
|
||||||
|
"next": "question",
|
||||||
|
"response": "Your request doesn't make sense in this context",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[mock_get_wf_env_vars, mock_agent_validatePrompt],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
sample_combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send invalid prompt
|
||||||
|
await handle.signal(
|
||||||
|
AgentGoalWorkflow.user_prompt, "Invalid nonsensical prompt"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Give workflow time to process the prompt
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
# End workflow to check conversation
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
result = await handle.result()
|
||||||
|
|
||||||
|
# Verify validation failure message was added
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
conversation_history = json.loads(result.replace("'", '"'))
|
||||||
|
except Exception:
|
||||||
|
# Fallback to eval if json fails
|
||||||
|
conversation_history = eval(result)
|
||||||
|
messages = conversation_history["messages"]
|
||||||
|
|
||||||
|
# Should have validation failure response
|
||||||
|
agent_messages = [msg for msg in messages if msg["actor"] == "agent"]
|
||||||
|
assert len(agent_messages) > 0
|
||||||
|
assert any(
|
||||||
|
"doesn't make sense" in str(msg["response"]) for msg in agent_messages
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_conversation_summary_initialization(
|
||||||
|
self, client: Client, sample_agent_goal
|
||||||
|
):
|
||||||
|
"""Test workflow initializes with conversation summary."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Create input with conversation summary
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
tool_params = AgentGoalWorkflowParams(
|
||||||
|
conversation_summary="Previous conversation summary", prompt_queue=deque()
|
||||||
|
)
|
||||||
|
combined_input = CombinedInput(
|
||||||
|
agent_goal=sample_agent_goal, tool_params=tool_params
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[mock_get_wf_env_vars],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Give workflow time to initialize
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
# Query conversation summary
|
||||||
|
summary = await handle.query(AgentGoalWorkflow.get_summary_from_history)
|
||||||
|
assert summary == "Previous conversation summary"
|
||||||
|
|
||||||
|
# Query conversation history - should include summary message
|
||||||
|
conversation_history = await handle.query(
|
||||||
|
AgentGoalWorkflow.get_conversation_history
|
||||||
|
)
|
||||||
|
messages = conversation_history["messages"]
|
||||||
|
|
||||||
|
# Should have conversation_summary message
|
||||||
|
summary_messages = [
|
||||||
|
msg for msg in messages if msg["actor"] == "conversation_summary"
|
||||||
|
]
|
||||||
|
assert len(summary_messages) == 1
|
||||||
|
assert summary_messages[0]["response"] == "Previous conversation summary"
|
||||||
|
|
||||||
|
# End workflow
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
await handle.result()
|
||||||
|
|
||||||
|
async def test_workflow_queries(
|
||||||
|
self, client: Client, sample_combined_input: CombinedInput
|
||||||
|
):
|
||||||
|
"""Test all workflow query methods."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[mock_get_wf_env_vars],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
sample_combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Give workflow time to initialize
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
# Test get_conversation_history query
|
||||||
|
conversation_history = await handle.query(
|
||||||
|
AgentGoalWorkflow.get_conversation_history
|
||||||
|
)
|
||||||
|
assert isinstance(conversation_history, dict)
|
||||||
|
assert "messages" in conversation_history
|
||||||
|
|
||||||
|
# Test get_agent_goal query
|
||||||
|
agent_goal = await handle.query(AgentGoalWorkflow.get_agent_goal)
|
||||||
|
assert agent_goal.id == sample_combined_input.agent_goal.id
|
||||||
|
|
||||||
|
# Test get_summary_from_history query
|
||||||
|
summary = await handle.query(AgentGoalWorkflow.get_summary_from_history)
|
||||||
|
# Summary might be None if not set, so check for that
|
||||||
|
if sample_combined_input.tool_params.conversation_summary:
|
||||||
|
assert summary == sample_combined_input.tool_params.conversation_summary
|
||||||
|
else:
|
||||||
|
assert summary is None
|
||||||
|
|
||||||
|
# Test get_latest_tool_data query (should be None initially)
|
||||||
|
tool_data = await handle.query(AgentGoalWorkflow.get_latest_tool_data)
|
||||||
|
assert tool_data is None
|
||||||
|
|
||||||
|
# End workflow
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
await handle.result()
|
||||||
|
|
||||||
|
async def test_enable_disable_debugging_confirm_signals(
|
||||||
|
self, client: Client, sample_combined_input: CombinedInput
|
||||||
|
):
|
||||||
|
"""Test debugging confirm enable/disable signals."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[mock_get_wf_env_vars],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
sample_combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test enable debugging confirm signal
|
||||||
|
await handle.signal(AgentGoalWorkflow.enable_debugging_confirm)
|
||||||
|
|
||||||
|
# Test disable debugging confirm signal
|
||||||
|
await handle.signal(AgentGoalWorkflow.disable_debugging_confirm)
|
||||||
|
|
||||||
|
# End workflow
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
result = await handle.result()
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
async def test_workflow_with_empty_prompt_queue(
|
||||||
|
self, client: Client, sample_agent_goal
|
||||||
|
):
|
||||||
|
"""Test workflow behavior with empty prompt queue."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Create input with empty prompt queue
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
tool_params = AgentGoalWorkflowParams(
|
||||||
|
conversation_summary=None, prompt_queue=deque()
|
||||||
|
)
|
||||||
|
combined_input = CombinedInput(
|
||||||
|
agent_goal=sample_agent_goal, tool_params=tool_params
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[mock_get_wf_env_vars],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Give workflow time to initialize
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
# Query initial state
|
||||||
|
conversation_history = await handle.query(
|
||||||
|
AgentGoalWorkflow.get_conversation_history
|
||||||
|
)
|
||||||
|
assert isinstance(conversation_history, dict)
|
||||||
|
assert "messages" in conversation_history
|
||||||
|
|
||||||
|
# Should have no messages initially (empty prompt queue, no summary)
|
||||||
|
messages = conversation_history["messages"]
|
||||||
|
assert len(messages) == 0
|
||||||
|
|
||||||
|
# End workflow
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
result = await handle.result()
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
async def test_multiple_user_prompts(
|
||||||
|
self, client: Client, sample_combined_input: CombinedInput
|
||||||
|
):
|
||||||
|
"""Test workflow handling multiple user prompts in sequence."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
@activity.defn(name="agent_validatePrompt")
|
||||||
|
async def mock_agent_validatePrompt(
|
||||||
|
validation_input: ValidationInput,
|
||||||
|
) -> ValidationResult:
|
||||||
|
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||||
|
|
||||||
|
@activity.defn(name="agent_toolPlanner")
|
||||||
|
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
||||||
|
# Keep workflow running for multiple prompts
|
||||||
|
return {"next": "question", "response": f"Processed: {input.prompt}"}
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[
|
||||||
|
mock_get_wf_env_vars,
|
||||||
|
mock_agent_validatePrompt,
|
||||||
|
mock_agent_toolPlanner,
|
||||||
|
],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
sample_combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send multiple prompts
|
||||||
|
await handle.signal(AgentGoalWorkflow.user_prompt, "First message")
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
await handle.signal(AgentGoalWorkflow.user_prompt, "Second message")
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
await handle.signal(AgentGoalWorkflow.user_prompt, "Third message")
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
# End workflow
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
result = await handle.result()
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
# Parse result and verify multiple messages
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
conversation_history = json.loads(result.replace("'", '"'))
|
||||||
|
except Exception:
|
||||||
|
conversation_history = eval(result)
|
||||||
|
messages = conversation_history["messages"]
|
||||||
|
|
||||||
|
# Should have at least one user message (timing dependent)
|
||||||
|
user_messages = [msg for msg in messages if msg["actor"] == "user"]
|
||||||
|
assert len(user_messages) >= 1
|
||||||
|
|
||||||
|
# Verify at least the first message was processed
|
||||||
|
message_texts = [str(msg["response"]) for msg in user_messages]
|
||||||
|
assert any("First message" in text for text in message_texts)
|
||||||
445
tests/test_tool_activities.py
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from temporalio.client import Client
|
||||||
|
from temporalio.testing import ActivityEnvironment
|
||||||
|
|
||||||
|
from activities.tool_activities import ToolActivities, dynamic_tool_activity
|
||||||
|
from models.data_types import (
|
||||||
|
EnvLookupInput,
|
||||||
|
EnvLookupOutput,
|
||||||
|
ToolPromptInput,
|
||||||
|
ValidationInput,
|
||||||
|
ValidationResult,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestToolActivities:
|
||||||
|
"""Test cases for ToolActivities."""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Set up test environment for each test."""
|
||||||
|
self.tool_activities = ToolActivities()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_validatePrompt_valid_prompt(
|
||||||
|
self, sample_agent_goal, sample_conversation_history
|
||||||
|
):
|
||||||
|
"""Test agent_validatePrompt with a valid prompt."""
|
||||||
|
validation_input = ValidationInput(
|
||||||
|
prompt="I need help with the test tool",
|
||||||
|
conversation_history=sample_conversation_history,
|
||||||
|
agent_goal=sample_agent_goal,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the agent_toolPlanner to return a valid response
|
||||||
|
mock_response = {"validationResult": True, "validationFailedReason": {}}
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
self.tool_activities, "agent_toolPlanner", new_callable=AsyncMock
|
||||||
|
) as mock_planner:
|
||||||
|
mock_planner.return_value = mock_response
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.agent_validatePrompt, validation_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, ValidationResult)
|
||||||
|
assert result.validationResult is True
|
||||||
|
assert result.validationFailedReason == {}
|
||||||
|
|
||||||
|
# Verify the mock was called with correct parameters
|
||||||
|
mock_planner.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_validatePrompt_invalid_prompt(
|
||||||
|
self, sample_agent_goal, sample_conversation_history
|
||||||
|
):
|
||||||
|
"""Test agent_validatePrompt with an invalid prompt."""
|
||||||
|
validation_input = ValidationInput(
|
||||||
|
prompt="asdfghjkl nonsense",
|
||||||
|
conversation_history=sample_conversation_history,
|
||||||
|
agent_goal=sample_agent_goal,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the agent_toolPlanner to return an invalid response
|
||||||
|
mock_response = {
|
||||||
|
"validationResult": False,
|
||||||
|
"validationFailedReason": {
|
||||||
|
"next": "question",
|
||||||
|
"response": "Your request doesn't make sense in this context",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
self.tool_activities, "agent_toolPlanner", new_callable=AsyncMock
|
||||||
|
) as mock_planner:
|
||||||
|
mock_planner.return_value = mock_response
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.agent_validatePrompt, validation_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, ValidationResult)
|
||||||
|
assert result.validationResult is False
|
||||||
|
assert "doesn't make sense" in str(result.validationFailedReason)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_toolPlanner_success(self):
|
||||||
|
"""Test agent_toolPlanner with successful LLM response."""
|
||||||
|
prompt_input = ToolPromptInput(
|
||||||
|
prompt="Test prompt", context_instructions="Test context instructions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the completion function
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.choices = [MagicMock()]
|
||||||
|
mock_response.choices[
|
||||||
|
0
|
||||||
|
].message.content = (
|
||||||
|
'{"next": "confirm", "tool": "TestTool", "response": "Test response"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("activities.tool_activities.completion") as mock_completion:
|
||||||
|
mock_completion.return_value = mock_response
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.agent_toolPlanner, prompt_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert result["next"] == "confirm"
|
||||||
|
assert result["tool"] == "TestTool"
|
||||||
|
assert result["response"] == "Test response"
|
||||||
|
|
||||||
|
# Verify completion was called with correct parameters
|
||||||
|
mock_completion.assert_called_once()
|
||||||
|
call_args = mock_completion.call_args[1]
|
||||||
|
assert call_args["model"] == self.tool_activities.llm_model
|
||||||
|
assert len(call_args["messages"]) == 2
|
||||||
|
assert call_args["messages"][0]["role"] == "system"
|
||||||
|
assert call_args["messages"][1]["role"] == "user"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_toolPlanner_with_custom_base_url(self):
|
||||||
|
"""Test agent_toolPlanner with custom base URL configuration."""
|
||||||
|
# Set up tool activities with custom base URL
|
||||||
|
with patch.dict(os.environ, {"LLM_BASE_URL": "https://custom.endpoint.com"}):
|
||||||
|
tool_activities = ToolActivities()
|
||||||
|
|
||||||
|
prompt_input = ToolPromptInput(
|
||||||
|
prompt="Test prompt", context_instructions="Test context instructions"
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.choices = [MagicMock()]
|
||||||
|
mock_response.choices[
|
||||||
|
0
|
||||||
|
].message.content = '{"next": "done", "response": "Test"}'
|
||||||
|
|
||||||
|
with patch("activities.tool_activities.completion") as mock_completion:
|
||||||
|
mock_completion.return_value = mock_response
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
await activity_env.run(tool_activities.agent_toolPlanner, prompt_input)
|
||||||
|
|
||||||
|
# Verify base_url was included in the call
|
||||||
|
call_args = mock_completion.call_args[1]
|
||||||
|
assert "base_url" in call_args
|
||||||
|
assert call_args["base_url"] == "https://custom.endpoint.com"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_toolPlanner_json_parsing_error(self):
|
||||||
|
"""Test agent_toolPlanner handles JSON parsing errors."""
|
||||||
|
prompt_input = ToolPromptInput(
|
||||||
|
prompt="Test prompt", context_instructions="Test context instructions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the completion function to return invalid JSON
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.choices = [MagicMock()]
|
||||||
|
mock_response.choices[0].message.content = "Invalid JSON response"
|
||||||
|
|
||||||
|
with patch("activities.tool_activities.completion") as mock_completion:
|
||||||
|
mock_completion.return_value = mock_response
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
with pytest.raises(Exception): # Should raise JSON parsing error
|
||||||
|
await activity_env.run(
|
||||||
|
self.tool_activities.agent_toolPlanner, prompt_input
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_wf_env_vars_default_values(self):
|
||||||
|
"""Test get_wf_env_vars with default values."""
|
||||||
|
env_input = EnvLookupInput(
|
||||||
|
show_confirm_env_var_name="SHOW_CONFIRM", show_confirm_default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clear environment variables
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.get_wf_env_vars, env_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, EnvLookupOutput)
|
||||||
|
assert result.show_confirm is True # default value
|
||||||
|
assert result.multi_goal_mode is True # default value
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_wf_env_vars_custom_values(self):
|
||||||
|
"""Test get_wf_env_vars with custom environment values."""
|
||||||
|
env_input = EnvLookupInput(
|
||||||
|
show_confirm_env_var_name="SHOW_CONFIRM", show_confirm_default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
with patch.dict(
|
||||||
|
os.environ, {"SHOW_CONFIRM": "false", "AGENT_GOAL": "specific_goal"}
|
||||||
|
):
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.get_wf_env_vars, env_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, EnvLookupOutput)
|
||||||
|
assert result.show_confirm is False # from env var
|
||||||
|
assert result.multi_goal_mode is False # from env var
|
||||||
|
|
||||||
|
def test_sanitize_json_response(self):
|
||||||
|
"""Test JSON response sanitization."""
|
||||||
|
# Test with markdown code blocks
|
||||||
|
response_with_markdown = '```json\n{"test": "value"}\n```'
|
||||||
|
sanitized = self.tool_activities.sanitize_json_response(response_with_markdown)
|
||||||
|
assert sanitized == '{"test": "value"}'
|
||||||
|
|
||||||
|
# Test with extra whitespace
|
||||||
|
response_with_whitespace = ' \n{"test": "value"} \n'
|
||||||
|
sanitized = self.tool_activities.sanitize_json_response(
|
||||||
|
response_with_whitespace
|
||||||
|
)
|
||||||
|
assert sanitized == '{"test": "value"}'
|
||||||
|
|
||||||
|
def test_parse_json_response_success(self):
|
||||||
|
"""Test successful JSON parsing."""
|
||||||
|
json_string = '{"next": "confirm", "tool": "TestTool"}'
|
||||||
|
result = self.tool_activities.parse_json_response(json_string)
|
||||||
|
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert result["next"] == "confirm"
|
||||||
|
assert result["tool"] == "TestTool"
|
||||||
|
|
||||||
|
def test_parse_json_response_failure(self):
|
||||||
|
"""Test JSON parsing with invalid JSON."""
|
||||||
|
invalid_json = "Not valid JSON"
|
||||||
|
|
||||||
|
with pytest.raises(Exception): # Should raise JSON parsing error
|
||||||
|
self.tool_activities.parse_json_response(invalid_json)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDynamicToolActivity:
|
||||||
|
"""Test cases for dynamic_tool_activity."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dynamic_tool_activity_sync_handler(self):
|
||||||
|
"""Test dynamic tool activity with synchronous handler."""
|
||||||
|
# Mock the activity info and payload converter
|
||||||
|
mock_info = MagicMock()
|
||||||
|
mock_info.activity_type = "TestTool"
|
||||||
|
|
||||||
|
mock_payload_converter = MagicMock()
|
||||||
|
mock_payload = MagicMock()
|
||||||
|
mock_payload.payload = b'{"test_arg": "test_value"}'
|
||||||
|
mock_payload_converter.from_payload.return_value = {"test_arg": "test_value"}
|
||||||
|
|
||||||
|
# Mock the handler function
|
||||||
|
def mock_handler(args):
|
||||||
|
return {"result": f"Handled {args['test_arg']}"}
|
||||||
|
|
||||||
|
with patch("temporalio.activity.info", return_value=mock_info), patch(
|
||||||
|
"temporalio.activity.payload_converter", return_value=mock_payload_converter
|
||||||
|
), patch("tools.get_handler", return_value=mock_handler):
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(dynamic_tool_activity, [mock_payload])
|
||||||
|
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert result["result"] == "Handled test_value"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dynamic_tool_activity_async_handler(self):
|
||||||
|
"""Test dynamic tool activity with asynchronous handler."""
|
||||||
|
# Mock the activity info and payload converter
|
||||||
|
mock_info = MagicMock()
|
||||||
|
mock_info.activity_type = "AsyncTestTool"
|
||||||
|
|
||||||
|
mock_payload_converter = MagicMock()
|
||||||
|
mock_payload = MagicMock()
|
||||||
|
mock_payload.payload = b'{"test_arg": "async_test"}'
|
||||||
|
mock_payload_converter.from_payload.return_value = {"test_arg": "async_test"}
|
||||||
|
|
||||||
|
# Mock the async handler function
|
||||||
|
async def mock_async_handler(args):
|
||||||
|
return {"async_result": f"Async handled {args['test_arg']}"}
|
||||||
|
|
||||||
|
with patch("temporalio.activity.info", return_value=mock_info), patch(
|
||||||
|
"temporalio.activity.payload_converter", return_value=mock_payload_converter
|
||||||
|
), patch("tools.get_handler", return_value=mock_async_handler):
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(dynamic_tool_activity, [mock_payload])
|
||||||
|
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert result["async_result"] == "Async handled async_test"
|
||||||
|
|
||||||
|
|
||||||
|
class TestToolActivitiesIntegration:
|
||||||
|
"""Integration tests for ToolActivities in a real Temporal environment."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_activities_in_worker(self, client: Client):
|
||||||
|
"""Test activities can be registered and executed in a worker."""
|
||||||
|
# task_queue_name = str(uuid.uuid4())
|
||||||
|
tool_activities = ToolActivities()
|
||||||
|
|
||||||
|
# Test get_wf_env_vars activity using ActivityEnvironment
|
||||||
|
env_input = EnvLookupInput(
|
||||||
|
show_confirm_env_var_name="TEST_CONFIRM", show_confirm_default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(tool_activities.get_wf_env_vars, env_input)
|
||||||
|
|
||||||
|
assert isinstance(result, EnvLookupOutput)
|
||||||
|
assert isinstance(result.show_confirm, bool)
|
||||||
|
assert isinstance(result.multi_goal_mode, bool)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEdgeCases:
|
||||||
|
"""Test edge cases and error handling."""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Set up test environment for each test."""
|
||||||
|
self.tool_activities = ToolActivities()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_validatePrompt_with_empty_conversation_history(
|
||||||
|
self, sample_agent_goal
|
||||||
|
):
|
||||||
|
"""Test validation with empty conversation history."""
|
||||||
|
validation_input = ValidationInput(
|
||||||
|
prompt="Test prompt",
|
||||||
|
conversation_history={"messages": []},
|
||||||
|
agent_goal=sample_agent_goal,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_response = {"validationResult": True, "validationFailedReason": {}}
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
self.tool_activities, "agent_toolPlanner", new_callable=AsyncMock
|
||||||
|
) as mock_planner:
|
||||||
|
mock_planner.return_value = mock_response
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.agent_validatePrompt, validation_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, ValidationResult)
|
||||||
|
assert result.validationResult
|
||||||
|
assert result.validationFailedReason == {}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_toolPlanner_with_long_prompt(self):
|
||||||
|
"""Test toolPlanner with very long prompt."""
|
||||||
|
long_prompt = "This is a very long prompt " * 100
|
||||||
|
tool_prompt_input = ToolPromptInput(
|
||||||
|
prompt=long_prompt, context_instructions="Test context instructions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the completion response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.choices = [MagicMock()]
|
||||||
|
mock_response.choices[
|
||||||
|
0
|
||||||
|
].message.content = '{"next": "done", "response": "Processed long prompt"}'
|
||||||
|
|
||||||
|
with patch("activities.tool_activities.completion", return_value=mock_response):
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.agent_toolPlanner, tool_prompt_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert result["next"] == "done"
|
||||||
|
assert "Processed long prompt" in result["response"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sanitize_json_with_various_formats(self):
|
||||||
|
"""Test JSON sanitization with various input formats."""
|
||||||
|
# Test markdown code blocks
|
||||||
|
markdown_json = '```json\n{"test": "value"}\n```'
|
||||||
|
result = self.tool_activities.sanitize_json_response(markdown_json)
|
||||||
|
assert result == '{"test": "value"}'
|
||||||
|
|
||||||
|
# Test with extra whitespace
|
||||||
|
whitespace_json = ' \n {"test": "value"} \n '
|
||||||
|
result = self.tool_activities.sanitize_json_response(whitespace_json)
|
||||||
|
assert result == '{"test": "value"}'
|
||||||
|
|
||||||
|
# Test already clean JSON
|
||||||
|
clean_json = '{"test": "value"}'
|
||||||
|
result = self.tool_activities.sanitize_json_response(clean_json)
|
||||||
|
assert result == '{"test": "value"}'
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_json_response_with_invalid_json(self):
|
||||||
|
"""Test JSON parsing with invalid JSON."""
|
||||||
|
with pytest.raises(json.JSONDecodeError):
|
||||||
|
self.tool_activities.parse_json_response("Invalid JSON {test: value")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_wf_env_vars_with_various_env_values(self):
|
||||||
|
"""Test environment variable parsing with different values."""
|
||||||
|
# Test with "true" string
|
||||||
|
with patch.dict(os.environ, {"TEST_CONFIRM": "true"}):
|
||||||
|
env_input = EnvLookupInput(
|
||||||
|
show_confirm_env_var_name="TEST_CONFIRM", show_confirm_default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.get_wf_env_vars, env_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.show_confirm
|
||||||
|
|
||||||
|
# Test with "false" string
|
||||||
|
with patch.dict(os.environ, {"TEST_CONFIRM": "false"}):
|
||||||
|
env_input = EnvLookupInput(
|
||||||
|
show_confirm_env_var_name="TEST_CONFIRM", show_confirm_default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.get_wf_env_vars, env_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not result.show_confirm
|
||||||
|
|
||||||
|
# Test with missing env var (should use default)
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
env_input = EnvLookupInput(
|
||||||
|
show_confirm_env_var_name="MISSING_VAR", show_confirm_default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(
|
||||||
|
self.tool_activities.get_wf_env_vars, env_input
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.show_confirm
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
|
import concurrent.futures
|
||||||
|
import uuid
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from temporalio import activity
|
||||||
from temporalio.client import Client, WorkflowExecutionStatus
|
from temporalio.client import Client, WorkflowExecutionStatus
|
||||||
from temporalio.worker import Worker
|
from temporalio.worker import Worker
|
||||||
import concurrent.futures
|
|
||||||
from temporalio.testing import WorkflowEnvironment
|
|
||||||
from api.main import get_initial_agent_goal
|
from api.main import get_initial_agent_goal
|
||||||
from models.data_types import AgentGoalWorkflowParams, CombinedInput
|
from models.data_types import (
|
||||||
|
AgentGoalWorkflowParams,
|
||||||
|
CombinedInput,
|
||||||
|
EnvLookupInput,
|
||||||
|
EnvLookupOutput,
|
||||||
|
ToolPromptInput,
|
||||||
|
ValidationInput,
|
||||||
|
ValidationResult,
|
||||||
|
)
|
||||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||||
from activities.tool_activities import ToolActivities, dynamic_tool_activity
|
|
||||||
from unittest.mock import patch
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
import os
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@@ -19,27 +26,45 @@ def my_context():
|
|||||||
print("Cleanup")
|
print("Cleanup")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def test_flight_booking(client: Client):
|
async def test_flight_booking(client: Client):
|
||||||
|
# load_dotenv("test_flights_single.env")
|
||||||
#load_dotenv("test_flights_single.env")
|
|
||||||
|
|
||||||
with my_context() as value:
|
with my_context() as value:
|
||||||
print(f"Working with {value}")
|
print(f"Working with {value}")
|
||||||
|
|
||||||
|
|
||||||
# Create the test environment
|
# Create the test environment
|
||||||
#env = await WorkflowEnvironment.start_local()
|
# env = await WorkflowEnvironment.start_local()
|
||||||
#client = env.client
|
# client = env.client
|
||||||
task_queue_name = "agent-ai-workflow"
|
task_queue_name = str(uuid.uuid4())
|
||||||
workflow_id = "agent-workflow"
|
workflow_id = str(uuid.uuid4())
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
|
# Create mock activity functions with proper signatures
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
@activity.defn(name="agent_validatePrompt")
|
||||||
|
async def mock_agent_validatePrompt(
|
||||||
|
validation_input: ValidationInput,
|
||||||
|
) -> ValidationResult:
|
||||||
|
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||||
|
|
||||||
|
@activity.defn(name="agent_toolPlanner")
|
||||||
|
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
||||||
|
return {"next": "done", "response": "Test response from LLM"}
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(
|
||||||
|
max_workers=100
|
||||||
|
) as activity_executor:
|
||||||
worker = Worker(
|
worker = Worker(
|
||||||
client,
|
client,
|
||||||
task_queue=task_queue_name,
|
task_queue=task_queue_name,
|
||||||
workflows=[AgentGoalWorkflow],
|
workflows=[AgentGoalWorkflow],
|
||||||
activities=[ToolActivities.agent_validatePrompt, ToolActivities.agent_toolPlanner, ToolActivities.get_wf_env_vars, dynamic_tool_activity],
|
activities=[
|
||||||
|
mock_get_wf_env_vars,
|
||||||
|
mock_agent_validatePrompt,
|
||||||
|
mock_agent_toolPlanner,
|
||||||
|
],
|
||||||
activity_executor=activity_executor,
|
activity_executor=activity_executor,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,9 +76,9 @@ async def test_flight_booking(client: Client):
|
|||||||
agent_goal=initial_agent_goal,
|
agent_goal=initial_agent_goal,
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt="Hello!"
|
prompt = "Hello!"
|
||||||
|
|
||||||
#async with Worker(client, task_queue=task_queue_name, workflows=[AgentGoalWorkflow], activities=[ToolActivities.agent_validatePrompt, ToolActivities.agent_toolPlanner, dynamic_tool_activity]):
|
# async with Worker(client, task_queue=task_queue_name, workflows=[AgentGoalWorkflow], activities=[ToolActivities.agent_validatePrompt, ToolActivities.agent_toolPlanner, dynamic_tool_activity]):
|
||||||
|
|
||||||
# todo set goal categories for scenarios
|
# todo set goal categories for scenarios
|
||||||
handle = await client.start_workflow(
|
handle = await client.start_workflow(
|
||||||
@@ -66,15 +91,21 @@ async def test_flight_booking(client: Client):
|
|||||||
)
|
)
|
||||||
# todo send signals to simulate user input
|
# todo send signals to simulate user input
|
||||||
# await handle.signal(AgentGoalWorkflow.user_prompt, "book flights") # for multi-goal
|
# await handle.signal(AgentGoalWorkflow.user_prompt, "book flights") # for multi-goal
|
||||||
await handle.signal(AgentGoalWorkflow.user_prompt, "sydney in september")
|
await handle.signal(
|
||||||
assert WorkflowExecutionStatus.RUNNING == (await handle.describe()).status
|
AgentGoalWorkflow.user_prompt, "sydney in september"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
WorkflowExecutionStatus.RUNNING == (await handle.describe()).status
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert ["Hello, user1", "Hello, user2"] == await handle.result()
|
||||||
|
await handle.signal(
|
||||||
|
AgentGoalWorkflow.user_prompt, "I'm all set, end conversation"
|
||||||
|
)
|
||||||
|
|
||||||
#assert ["Hello, user1", "Hello, user2"] == await handle.result()
|
# assert WorkflowExecutionStatus.COMPLETED == (await handle.describe()).status
|
||||||
await handle.signal(AgentGoalWorkflow.user_prompt, "I'm all set, end conversation")
|
|
||||||
|
|
||||||
#assert WorkflowExecutionStatus.COMPLETED == (await handle.describe()).status
|
|
||||||
|
|
||||||
result = await handle.result()
|
result = await handle.result()
|
||||||
#todo dump workflow history for analysis optional
|
print(f"Workflow result: {result}")
|
||||||
#todo assert result is good
|
# todo dump workflow history for analysis optional
|
||||||
|
# todo assert result is good
|
||||||
|
|||||||
6
thirdparty/train_api.py
vendored
@@ -1,9 +1,9 @@
|
|||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
||||||
from urllib.parse import parse_qs, urlparse
|
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
import time
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
|
||||||
def parse_datetime(datetime_str):
|
def parse_datetime(datetime_str):
|
||||||
|
|||||||
42
todo.md
@@ -1,7 +1,33 @@
|
|||||||
# todo list
|
# todo list
|
||||||
[ ] expand [tests](./tests/agent_goal_workflow_test.py)<br />
|
|
||||||
|
|
||||||
[ ] adding fintech goals <br />
|
## General Agent Enhancements
|
||||||
|
|
||||||
|
[ ] MCP: There is a plan to add MCP (Model Context Protocol) to the agent. This really really really needs to be done and is scheduled to be done by @steveandroulakis some time in June 2025.
|
||||||
|
|
||||||
|
[ ] Google's A2A is emerging as the standard way to hand off agents to other agents. We should examine implementing this soon.
|
||||||
|
|
||||||
|
[ ] Custom metrics/tracing is important for AI specific aspects such as number of LLM calls, number of bad LLM responses that require retrying, number of bad chat outcomes. We should add this.
|
||||||
|
|
||||||
|
[ ] Evals are very important in agents. We want to be able to 'judge' the agent's performance both in dev and production (AIOps). This will help us improve our agent's performance over time in a targeted fashion.
|
||||||
|
|
||||||
|
[ ] Dynamically switch LLMs on persistent failures: <br />
|
||||||
|
- detect failure in the activity using failurecount <br />
|
||||||
|
- activity switches to secondary LLM defined in .env
|
||||||
|
- activity reports switch to workflow
|
||||||
|
|
||||||
|
[ ] Collapse history/summarize chat after goal finished <br />
|
||||||
|
|
||||||
|
[ ] Write tests<br />
|
||||||
|
|
||||||
|
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br />
|
||||||
|
|
||||||
|
[ ] add visual feedback when workflow starting <br />
|
||||||
|
|
||||||
|
[ ] enable user to list agents at any time - like end conversation - probably with a next step<br />
|
||||||
|
|
||||||
|
## Ideas for more goals and tools
|
||||||
|
|
||||||
|
[ ] Add fintech goals <br />
|
||||||
- Fraud Detection and Prevention - The AI monitors transactions across accounts, flagging suspicious activities (e.g., unusual spending patterns or login attempts) and autonomously freezing accounts or notifying customers and compliance teams.<br />
|
- Fraud Detection and Prevention - The AI monitors transactions across accounts, flagging suspicious activities (e.g., unusual spending patterns or login attempts) and autonomously freezing accounts or notifying customers and compliance teams.<br />
|
||||||
- Personalized Financial Advice - An AI agent analyzes a customer’s financial data (e.g., income, spending habits, savings, investments) and provides tailored advice, such as budgeting tips, investment options, or debt repayment strategies.<br />
|
- Personalized Financial Advice - An AI agent analyzes a customer’s financial data (e.g., income, spending habits, savings, investments) and provides tailored advice, such as budgeting tips, investment options, or debt repayment strategies.<br />
|
||||||
- Portfolio Management and Rebalancing - The AI monitors a customer’s investment portfolio, rebalancing it automatically based on market trends, risk tolerance, and financial goals (e.g., shifting assets between stocks, bonds, or crypto).<br />
|
- Portfolio Management and Rebalancing - The AI monitors a customer’s investment portfolio, rebalancing it automatically based on market trends, risk tolerance, and financial goals (e.g., shifting assets between stocks, bonds, or crypto).<br />
|
||||||
@@ -10,14 +36,4 @@
|
|||||||
[ ] financial advise - args being freeform customer input about their financial situation, goals
|
[ ] financial advise - args being freeform customer input about their financial situation, goals
|
||||||
[ ] tool is maybe a new tool asking the LLM to advise
|
[ ] tool is maybe a new tool asking the LLM to advise
|
||||||
|
|
||||||
[ ] LLM failure->autoswitch: <br />
|
[ ] for demo simulate failure - add utilities/simulated failures from pipeline demo <br />
|
||||||
- detect failure in the activity using failurecount <br />
|
|
||||||
- activity switches to secondary LLM defined in .env
|
|
||||||
- activity reports switch to workflow
|
|
||||||
|
|
||||||
[ ] ask the ai agent how it did at the end of the conversation, was it efficient? successful? insert a search attribute to document that before return <br />
|
|
||||||
- Insight into the agent’s performance <br />
|
|
||||||
[ ] non-retry the api key error - "Invalid API Key provided: sk_test_**J..." and "AuthenticationError" <br />
|
|
||||||
[ ] add visual feedback when workflow starting <br />
|
|
||||||
[ ] enable user to list agents at any time - like end conversation - probably with a next step<br />
|
|
||||||
- with changing "'Next should only be "pick-new-goal" if all tools have been run (use the system prompt to figure that out).'" in [prompt_generators](./prompts/agent_prompt_generators.py).
|
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
from .search_fixtures import search_fixtures
|
|
||||||
from .search_flights import search_flights
|
|
||||||
from .search_trains import search_trains
|
|
||||||
from .search_trains import book_trains
|
|
||||||
from .create_invoice import create_invoice
|
|
||||||
from .find_events import find_events
|
|
||||||
from .list_agents import list_agents
|
|
||||||
from .change_goal import change_goal
|
from .change_goal import change_goal
|
||||||
from .transfer_control import transfer_control
|
from .create_invoice import create_invoice
|
||||||
|
from .ecommerce.get_order import get_order
|
||||||
from .hr.current_pto import current_pto
|
from .ecommerce.list_orders import list_orders
|
||||||
from .hr.book_pto import book_pto
|
from .ecommerce.track_package import track_package
|
||||||
from .hr.future_pto_calc import future_pto_calc
|
|
||||||
from .hr.checkpaybankstatus import checkpaybankstatus
|
|
||||||
|
|
||||||
from .fin.check_account_valid import check_account_valid
|
from .fin.check_account_valid import check_account_valid
|
||||||
from .fin.get_account_balances import get_account_balance
|
from .fin.get_account_balances import get_account_balance
|
||||||
from .fin.move_money import move_money
|
from .fin.move_money import move_money
|
||||||
from .fin.submit_loan_application import submit_loan_application
|
from .fin.submit_loan_application import submit_loan_application
|
||||||
|
from .find_events import find_events
|
||||||
from .give_hint import give_hint
|
from .give_hint import give_hint
|
||||||
from .guess_location import guess_location
|
from .guess_location import guess_location
|
||||||
|
from .hr.book_pto import book_pto
|
||||||
|
from .hr.checkpaybankstatus import checkpaybankstatus
|
||||||
|
from .hr.current_pto import current_pto
|
||||||
|
from .hr.future_pto_calc import future_pto_calc
|
||||||
|
from .list_agents import list_agents
|
||||||
|
from .search_fixtures import search_fixtures
|
||||||
|
from .search_flights import search_flights
|
||||||
|
from .search_trains import book_trains, search_trains
|
||||||
|
from .transfer_control import transfer_control
|
||||||
|
|
||||||
|
|
||||||
def get_handler(tool_name: str):
|
def get_handler(tool_name: str):
|
||||||
@@ -53,10 +52,16 @@ def get_handler(tool_name: str):
|
|||||||
return check_account_valid
|
return check_account_valid
|
||||||
if tool_name == "FinCheckAccountBalance":
|
if tool_name == "FinCheckAccountBalance":
|
||||||
return get_account_balance
|
return get_account_balance
|
||||||
if tool_name == "FinMoveMoneyOrder":
|
if tool_name == "FinMoveMoney":
|
||||||
return move_money
|
return move_money
|
||||||
if tool_name == "FinCheckAccountSubmitLoanApproval":
|
if tool_name == "FinCheckAccountSubmitLoanApproval":
|
||||||
return submit_loan_application
|
return submit_loan_application
|
||||||
|
if tool_name == "GetOrder":
|
||||||
|
return get_order
|
||||||
|
if tool_name == "TrackPackage":
|
||||||
|
return track_package
|
||||||
|
if tool_name == "ListOrders":
|
||||||
|
return list_orders
|
||||||
if tool_name == "GiveHint":
|
if tool_name == "GiveHint":
|
||||||
return give_hint
|
return give_hint
|
||||||
if tool_name == "GuessLocation":
|
if tool_name == "GuessLocation":
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
def change_goal(args: dict) -> dict:
|
def change_goal(args: dict) -> dict:
|
||||||
|
|
||||||
new_goal = args.get("goalID")
|
new_goal = args.get("goalID")
|
||||||
if new_goal is None:
|
if new_goal is None:
|
||||||
new_goal = "goal_choose_agent_type"
|
new_goal = "goal_choose_agent_type"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import stripe
|
import stripe
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ def ensure_customer_exists(
|
|||||||
def create_invoice(args: dict) -> dict:
|
def create_invoice(args: dict) -> dict:
|
||||||
"""Create and finalize a Stripe invoice."""
|
"""Create and finalize a Stripe invoice."""
|
||||||
# If an API key exists in the env file, find or create customer
|
# If an API key exists in the env file, find or create customer
|
||||||
if stripe.api_key is not None:
|
if stripe.api_key is not None and stripe.api_key != "":
|
||||||
customer_id = ensure_customer_exists(
|
customer_id = ensure_customer_exists(
|
||||||
args.get("customer_id"), args.get("email", "default@example.com")
|
args.get("customer_id"), args.get("email", "default@example.com")
|
||||||
)
|
)
|
||||||
@@ -69,15 +70,3 @@ def create_invoice(args: dict) -> dict:
|
|||||||
"invoiceURL": "https://pay.example.com/invoice/12345",
|
"invoiceURL": "https://pay.example.com/invoice/12345",
|
||||||
"reference": "INV-12345",
|
"reference": "INV-12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_invoice_example(args: dict) -> dict:
|
|
||||||
"""
|
|
||||||
This is an example implementation of the CreateInvoice tool
|
|
||||||
Doesn't call any external services, just returns a dummy response
|
|
||||||
"""
|
|
||||||
print("[CreateInvoice] Creating invoice with:", args)
|
|
||||||
return {
|
|
||||||
"invoiceStatus": "generated",
|
|
||||||
"invoiceURL": "https://pay.example.com/invoice/12345",
|
|
||||||
"reference": "INV-12345",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"name": "Matt Murdock",
|
"name": "Matt Murdock",
|
||||||
"email": "matt.murdock@nelsonmurdock.com",
|
"email": "matt.murdock@nelsonmurdock.com",
|
||||||
"account_id": "11235",
|
"account_id": "11235",
|
||||||
"checking_balance": 875.40,
|
"checking_balance": "1275.4",
|
||||||
"savings_balance": 3200.15,
|
"savings_balance": "2800.15",
|
||||||
"bitcoin_balance": 0.1378,
|
"bitcoin_balance": 0.1378,
|
||||||
"account_creation_date": "2014-03-10"
|
"account_creation_date": "2014-03-10"
|
||||||
},
|
},
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
"name": "Foggy Nelson",
|
"name": "Foggy Nelson",
|
||||||
"email": "foggy.nelson@nelsonmurdock.com",
|
"email": "foggy.nelson@nelsonmurdock.com",
|
||||||
"account_id": "112358",
|
"account_id": "112358",
|
||||||
"checking_balance": 1523.67,
|
"checking_balance": "1523.66",
|
||||||
"savings_balance": 4875.90,
|
"savings_balance": "4875.89",
|
||||||
"bitcoin_balance": 0.0923,
|
"bitcoin_balance": 0.0923,
|
||||||
"account_creation_date": "2014-03-10"
|
"account_creation_date": "2014-03-10"
|
||||||
},
|
},
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"email": "karen.page@nelsonmurdock.com",
|
"email": "karen.page@nelsonmurdock.com",
|
||||||
"account_id": "112",
|
"account_id": "112",
|
||||||
"checking_balance": 645.25,
|
"checking_balance": 645.25,
|
||||||
"savings_balance": 1830.50,
|
"savings_balance": "830.5",
|
||||||
"bitcoin_balance": 0.0456,
|
"bitcoin_balance": 0.0456,
|
||||||
"account_creation_date": "2015-01-15"
|
"account_creation_date": "2015-01-15"
|
||||||
},
|
},
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"name": "Wilson Fisk",
|
"name": "Wilson Fisk",
|
||||||
"email": "wilson.fisk@fiskcorp.com",
|
"email": "wilson.fisk@fiskcorp.com",
|
||||||
"account_id": "11",
|
"account_id": "11",
|
||||||
"checking_balance": 25000.00,
|
"checking_balance": 25000.0,
|
||||||
"savings_balance": 150000.75,
|
"savings_balance": 150000.75,
|
||||||
"bitcoin_balance": 5987.6721,
|
"bitcoin_balance": 5987.6721,
|
||||||
"account_creation_date": "2013-09-20"
|
"account_creation_date": "2013-09-20"
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
"name": "Frank Castle",
|
"name": "Frank Castle",
|
||||||
"email": "frank.castle@vigilante.net",
|
"email": "frank.castle@vigilante.net",
|
||||||
"account_id": "1",
|
"account_id": "1",
|
||||||
"checking_balance": 320.10,
|
"checking_balance": 320.1,
|
||||||
"savings_balance": 0.30,
|
"savings_balance": 0.3,
|
||||||
"bitcoin_balance": 15.2189,
|
"bitcoin_balance": 15.2189,
|
||||||
"account_creation_date": "2016-02-05"
|
"account_creation_date": "2016-02-05"
|
||||||
},
|
},
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
"name": "Joshua Smith",
|
"name": "Joshua Smith",
|
||||||
"email": "joshmsmith@gmail.com",
|
"email": "joshmsmith@gmail.com",
|
||||||
"account_id": "11235813",
|
"account_id": "11235813",
|
||||||
"checking_balance": 3021.90,
|
"checking_balance": 3021.9,
|
||||||
"savings_balance": 500.50,
|
"savings_balance": 500.5,
|
||||||
"bitcoin_balance": 0.001,
|
"bitcoin_balance": 0.001,
|
||||||
"account_creation_date": "2020-03-19"
|
"account_creation_date": "2020-03-19"
|
||||||
}
|
}
|
||||||
|
|||||||
81
tools/data/customer_order_data.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"orders": [
|
||||||
|
{
|
||||||
|
"id": "100",
|
||||||
|
"summary": "Lawyer Books",
|
||||||
|
"email": "matt.murdock@nelsonmurdock.com",
|
||||||
|
"status": "cancelled",
|
||||||
|
"order_date": "2025-03-30",
|
||||||
|
"last_update": "2025-04-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "101",
|
||||||
|
"summary": "Bonking Sticks",
|
||||||
|
"email": "matt.murdock@nelsonmurdock.com",
|
||||||
|
"status": "paid",
|
||||||
|
"order_date": "2025-04-01",
|
||||||
|
"last_order_update": "2025-04-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "102",
|
||||||
|
"summary": "Red Sunglasses",
|
||||||
|
"email": "matt.murdock@nelsonmurdock.com",
|
||||||
|
"status": "shipped",
|
||||||
|
"order_date": "2025-04-01",
|
||||||
|
"last_order_update": "2025-04-01",
|
||||||
|
"tracking_id": "UPS67890"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "200",
|
||||||
|
"summary": "Paper",
|
||||||
|
"email": "foggy.nelson@nelsonmurdock.com",
|
||||||
|
"status": "shipped",
|
||||||
|
"order_date": "2025-04-03",
|
||||||
|
"last_update": "2025-04-06",
|
||||||
|
"tracking_id": "USPS12345"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "300",
|
||||||
|
"summary": "Chemistry Books",
|
||||||
|
"email": "heisenberg@blue-meth.com",
|
||||||
|
"status": "shipped",
|
||||||
|
"order_date": "2025-03-30",
|
||||||
|
"last_update": "2025-04-06",
|
||||||
|
"tracking_id": "USPS12345"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "301",
|
||||||
|
"summary": "Book: Being a Cool Bro",
|
||||||
|
"email": "heisenberg@blue-meth.com",
|
||||||
|
"status": "cancelled",
|
||||||
|
"order_date": "2025-04-01",
|
||||||
|
"last_update": "2025-04-02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "302",
|
||||||
|
"summary": "Black Hat",
|
||||||
|
"email": "heisenberg@blue-meth.com",
|
||||||
|
"status": "delivered",
|
||||||
|
"order_date": "2025-04-01",
|
||||||
|
"last_update": "2025-04-06",
|
||||||
|
"tracking_id": "UPS67890"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "400",
|
||||||
|
"summary": "Giant Graphic Hoodie",
|
||||||
|
"email": "jessenotpinkman@blue-meth.com",
|
||||||
|
"status": "shipped",
|
||||||
|
"order_date": "2025-04-03",
|
||||||
|
"last_update": "2025-04-09",
|
||||||
|
"tracking_id": "UPS67890"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "401",
|
||||||
|
"summary": "Giant Pants",
|
||||||
|
"email": "jessenotpinkman@blue-meth.com",
|
||||||
|
"status": "processing",
|
||||||
|
"order_date": "2025-04-03",
|
||||||
|
"last_update": "2025-04-09"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
158
tools/data/dummy_tracking_data.json
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
{
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"TrackingNumber": "USPS12345",
|
||||||
|
"Delivered": false,
|
||||||
|
"Carrier": "USPS",
|
||||||
|
"ServiceType": "USPS Ground Advantage<SUP>™</SUP>",
|
||||||
|
"PickupDate": "",
|
||||||
|
"ScheduledDeliveryDate": "April 14, 2025",
|
||||||
|
"ScheduledDeliveryDateInDateTimeFromat": "2025-04-14T00:00:00",
|
||||||
|
"StatusCode": "In Transit from Origin Processing",
|
||||||
|
"Status": "Departed Post Office",
|
||||||
|
"StatusSummary": "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.",
|
||||||
|
"Message": "",
|
||||||
|
"DeliveredDateTime": "",
|
||||||
|
"DeliveredDateTimeInDateTimeFormat": null,
|
||||||
|
"SignatureName": "",
|
||||||
|
"DestinationCity": "CITY",
|
||||||
|
"DestinationState": "ST",
|
||||||
|
"DestinationZip": "12345",
|
||||||
|
"DestinationCountry": null,
|
||||||
|
"EventDate": "2025-04-10T07:06:00",
|
||||||
|
"TrackingDetails": [
|
||||||
|
{
|
||||||
|
"EventDateTime": "April 10, 2025 7:06 am",
|
||||||
|
"Event": "Departed Post Office",
|
||||||
|
"EventAddress": "IRON RIDGE WI 53035",
|
||||||
|
"State": "WI",
|
||||||
|
"City": "IRON RIDGE",
|
||||||
|
"Zip": "53035",
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-10T07:06:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "April 9, 2025 11:29 am",
|
||||||
|
"Event": "USPS picked up item",
|
||||||
|
"EventAddress": "IRON RIDGE WI 53035",
|
||||||
|
"State": "WI",
|
||||||
|
"City": "IRON RIDGE",
|
||||||
|
"Zip": "53035",
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-09T11:29:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "April 7, 2025 6:29 am",
|
||||||
|
"Event": "Shipping Label Created, USPS Awaiting Item",
|
||||||
|
"EventAddress": "IRON RIDGE WI 53035",
|
||||||
|
"State": "WI",
|
||||||
|
"City": "IRON RIDGE",
|
||||||
|
"Zip": "53035",
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-07T06:29:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TrackingNumber": "UPS67890",
|
||||||
|
"Delivered": true,
|
||||||
|
"Carrier": "UPS",
|
||||||
|
"ServiceType": "UPS Ground Saver®",
|
||||||
|
"PickupDate": "",
|
||||||
|
"ScheduledDeliveryDate": "",
|
||||||
|
"ScheduledDeliveryDateInDateTimeFromat": null,
|
||||||
|
"StatusCode": "D",
|
||||||
|
"Status": "DELIVERED",
|
||||||
|
"StatusSummary": "DELIVERED",
|
||||||
|
"Message": "",
|
||||||
|
"DeliveredDateTime": "20250415 154315",
|
||||||
|
"DeliveredDateTimeInDateTimeFormat": "2025-04-15T15:43:15",
|
||||||
|
"SignatureName": "",
|
||||||
|
"DestinationCity": "CHICAGO",
|
||||||
|
"DestinationState": "IL",
|
||||||
|
"DestinationZip": "",
|
||||||
|
"DestinationCountry": "US",
|
||||||
|
"EventDate": "2025-04-15T15:43:15",
|
||||||
|
"TrackingDetails": [
|
||||||
|
{
|
||||||
|
"EventDateTime": "20250415 154315",
|
||||||
|
"Event": "DELIVERED ",
|
||||||
|
"EventAddress": "CHICAGO IL US",
|
||||||
|
"State": "IL",
|
||||||
|
"City": "CHICAGO",
|
||||||
|
"Zip": null,
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-15T15:43:15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "20250415 090938",
|
||||||
|
"Event": "Out For Delivery Today",
|
||||||
|
"EventAddress": "Chicago IL US",
|
||||||
|
"State": "IL",
|
||||||
|
"City": "Chicago",
|
||||||
|
"Zip": null,
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-15T09:09:38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "20250415 074141",
|
||||||
|
"Event": "Loaded on Delivery Vehicle ",
|
||||||
|
"EventAddress": "Chicago IL US",
|
||||||
|
"State": "IL",
|
||||||
|
"City": "Chicago",
|
||||||
|
"Zip": null,
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-15T07:41:41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "20250415 032200",
|
||||||
|
"Event": "Arrived at Facility",
|
||||||
|
"EventAddress": "Chicago IL US",
|
||||||
|
"State": "IL",
|
||||||
|
"City": "Chicago",
|
||||||
|
"Zip": null,
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-15T03:22:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "20250414 223000",
|
||||||
|
"Event": "Departed from Facility",
|
||||||
|
"EventAddress": "Hodgkins IL US",
|
||||||
|
"State": "IL",
|
||||||
|
"City": "Hodgkins",
|
||||||
|
"Zip": null,
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-14T22:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "20250414 002700",
|
||||||
|
"Event": "Arrived at Facility",
|
||||||
|
"EventAddress": "Hodgkins IL US",
|
||||||
|
"State": "IL",
|
||||||
|
"City": "Hodgkins",
|
||||||
|
"Zip": null,
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-14T00:27:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "20250410 211700",
|
||||||
|
"Event": "Departed from Facility",
|
||||||
|
"EventAddress": "Las Vegas NV US",
|
||||||
|
"State": "NV",
|
||||||
|
"City": "Las Vegas",
|
||||||
|
"Zip": null,
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-10T21:17:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "20250410 132625",
|
||||||
|
"Event": "Arrived at Facility",
|
||||||
|
"EventAddress": "Las Vegas NV US",
|
||||||
|
"State": "NV",
|
||||||
|
"City": "Las Vegas",
|
||||||
|
"Zip": null,
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-10T13:26:25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "20250409 100659",
|
||||||
|
"Event": "Shipper created a label, UPS has not received the package yet. ",
|
||||||
|
"EventAddress": " US",
|
||||||
|
"State": null,
|
||||||
|
"City": null,
|
||||||
|
"Zip": null,
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-09T10:06:59"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tools/ecommerce/get_order.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||||
|
# called as part of a temporal activity with automatic retries
|
||||||
|
def get_order(args: dict) -> dict:
|
||||||
|
order_id = args.get("order_id")
|
||||||
|
|
||||||
|
file_path = (
|
||||||
|
Path(__file__).resolve().parent.parent / "data" / "customer_order_data.json"
|
||||||
|
)
|
||||||
|
if not file_path.exists():
|
||||||
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
|
with open(file_path, "r") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
order_list = data["orders"]
|
||||||
|
|
||||||
|
for order in order_list:
|
||||||
|
if order["id"] == order_id:
|
||||||
|
return order
|
||||||
|
|
||||||
|
return_msg = "Order " + order_id + " not found."
|
||||||
|
return {"error": return_msg}
|
||||||
32
tools/ecommerce/list_orders.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def sorting(e):
|
||||||
|
return e["order_date"]
|
||||||
|
|
||||||
|
|
||||||
|
def list_orders(args: dict) -> dict:
|
||||||
|
email_address = args.get("email_address")
|
||||||
|
|
||||||
|
file_path = (
|
||||||
|
Path(__file__).resolve().parent.parent / "data" / "customer_order_data.json"
|
||||||
|
)
|
||||||
|
if not file_path.exists():
|
||||||
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
|
with open(file_path, "r") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
order_list = data["orders"]
|
||||||
|
|
||||||
|
rtn_order_list = []
|
||||||
|
for order in order_list:
|
||||||
|
if order["email"] == email_address:
|
||||||
|
rtn_order_list.append(order)
|
||||||
|
|
||||||
|
if len(rtn_order_list) > 0:
|
||||||
|
rtn_order_list.sort(key=sorting)
|
||||||
|
return {"orders": rtn_order_list}
|
||||||
|
else:
|
||||||
|
return_msg = "No orders for customer " + email_address + " found."
|
||||||
|
return {"error": return_msg}
|
||||||
161
tools/ecommerce/track_package.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import http
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# Send back dummy data in the correct format - to use the real API, 1) change this to be track_package_fake and 2) change the below track_package_real to be track_package
|
||||||
|
def track_package(args: dict) -> dict:
|
||||||
|
tracking_id = args.get("tracking_id")
|
||||||
|
file_path = (
|
||||||
|
Path(__file__).resolve().parent.parent / "data" / "dummy_tracking_data.json"
|
||||||
|
)
|
||||||
|
if not file_path.exists():
|
||||||
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
|
with open(file_path, "r") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
package_list = data["packages"]
|
||||||
|
|
||||||
|
for package in package_list:
|
||||||
|
if package["TrackingNumber"] == tracking_id:
|
||||||
|
scheduled_delivery_date = package["ScheduledDeliveryDate"]
|
||||||
|
carrier = package["Carrier"]
|
||||||
|
status_summary = package["StatusSummary"]
|
||||||
|
tracking_details = package.get("TrackingDetails", [])
|
||||||
|
last_tracking_update = ""
|
||||||
|
if (
|
||||||
|
tracking_details
|
||||||
|
and tracking_details is not None
|
||||||
|
and tracking_details[0] is not None
|
||||||
|
):
|
||||||
|
last_tracking_update = tracking_details[0][
|
||||||
|
"EventDateTimeInDateTimeFormat"
|
||||||
|
]
|
||||||
|
|
||||||
|
tracking_link = ""
|
||||||
|
if carrier == "USPS":
|
||||||
|
tracking_link = f"https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1={tracking_id}"
|
||||||
|
elif carrier == "UPS":
|
||||||
|
tracking_link = (
|
||||||
|
f"https://www.ups.com/track?track=yes&trackNums={tracking_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"scheduled_delivery_date": scheduled_delivery_date,
|
||||||
|
"carrier": carrier,
|
||||||
|
"status_summary": status_summary,
|
||||||
|
"tracking_link": tracking_link,
|
||||||
|
"last_tracking_update": last_tracking_update,
|
||||||
|
}
|
||||||
|
|
||||||
|
return_msg = "Package not found with tracking info " + tracking_id
|
||||||
|
return {"error": return_msg}
|
||||||
|
|
||||||
|
|
||||||
|
"""Format of response:
|
||||||
|
{
|
||||||
|
"TrackingNumber": "",
|
||||||
|
"Delivered": false,
|
||||||
|
"Carrier": "USPS",
|
||||||
|
"ServiceType": "USPS Ground Advantage<SUP>™</SUP>",
|
||||||
|
"PickupDate": "",
|
||||||
|
"ScheduledDeliveryDate": "April 14, 2025",
|
||||||
|
"ScheduledDeliveryDateInDateTimeFromat": "2025-04-14T00:00:00",
|
||||||
|
"StatusCode": "In Transit from Origin Processing",
|
||||||
|
"Status": "Departed Post Office",
|
||||||
|
"StatusSummary": "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.",
|
||||||
|
"Message": "",
|
||||||
|
"DeliveredDateTime": "",
|
||||||
|
"DeliveredDateTimeInDateTimeFormat": null,
|
||||||
|
"SignatureName": "",
|
||||||
|
"DestinationCity": "CITY",
|
||||||
|
"DestinationState": "ST",
|
||||||
|
"DestinationZip": "12345",
|
||||||
|
"DestinationCountry": null,
|
||||||
|
"EventDate": "2025-04-10T07:06:00",
|
||||||
|
"TrackingDetails": [
|
||||||
|
{
|
||||||
|
"EventDateTime": "April 10, 2025 7:06 am",
|
||||||
|
"Event": "Departed Post Office",
|
||||||
|
"EventAddress": "IRON RIDGE WI 53035",
|
||||||
|
"State": "WI",
|
||||||
|
"City": "IRON RIDGE",
|
||||||
|
"Zip": "53035",
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-10T07:06:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "April 9, 2025 11:29 am",
|
||||||
|
"Event": "USPS picked up item",
|
||||||
|
"EventAddress": "IRON RIDGE WI 53035",
|
||||||
|
"State": "WI",
|
||||||
|
"City": "IRON RIDGE",
|
||||||
|
"Zip": "53035",
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-09T11:29:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventDateTime": "April 7, 2025 6:29 am",
|
||||||
|
"Event": "Shipping Label Created, USPS Awaiting Item",
|
||||||
|
"EventAddress": "IRON RIDGE WI 53035",
|
||||||
|
"State": "WI",
|
||||||
|
"City": "IRON RIDGE",
|
||||||
|
"Zip": "53035",
|
||||||
|
"EventDateTimeInDateTimeFormat": "2025-04-07T06:29:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def track_package_real(args: dict) -> dict:
|
||||||
|
tracking_id = args.get("tracking_id")
|
||||||
|
|
||||||
|
api_key = os.getenv("RAPIDAPI_KEY")
|
||||||
|
api_host = os.getenv("RAPIDAPI_HOST_PACKAGE", "trackingpackage.p.rapidapi.com")
|
||||||
|
|
||||||
|
conn = http.client.HTTPSConnection(api_host)
|
||||||
|
headers = {
|
||||||
|
"x-rapidapi-key": api_key,
|
||||||
|
"x-rapidapi-host": api_host,
|
||||||
|
"Authorization": "Basic Ym9sZGNoYXQ6TGZYfm0zY2d1QzkuKz9SLw==",
|
||||||
|
}
|
||||||
|
|
||||||
|
path = f"/TrackingPackage?trackingNumber={tracking_id}"
|
||||||
|
|
||||||
|
conn.request("GET", path, headers=headers)
|
||||||
|
res = conn.getresponse()
|
||||||
|
data = res.read()
|
||||||
|
data_decoded = data.decode("utf-8")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
json_data = json.loads(data_decoded)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {"error": "Invalid JSON response"}
|
||||||
|
|
||||||
|
scheduled_delivery_date = json_data["ScheduledDeliveryDate"]
|
||||||
|
carrier = json_data["Carrier"]
|
||||||
|
status_summary = json_data["StatusSummary"]
|
||||||
|
tracking_details = json_data.get("TrackingDetails", [])
|
||||||
|
last_tracking_update = ""
|
||||||
|
if (
|
||||||
|
tracking_details
|
||||||
|
and tracking_details is not None
|
||||||
|
and tracking_details[0] is not None
|
||||||
|
):
|
||||||
|
last_tracking_update = tracking_details[0]["EventDateTimeInDateTimeFormat"]
|
||||||
|
tracking_link = ""
|
||||||
|
if carrier == "USPS":
|
||||||
|
tracking_link = (
|
||||||
|
f"https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1={tracking_id}"
|
||||||
|
)
|
||||||
|
elif carrier == "UPS":
|
||||||
|
tracking_link = f"https://www.ups.com/track?track=yes&trackNums={tracking_id}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"scheduled_delivery_date": scheduled_delivery_date,
|
||||||
|
"carrier": carrier,
|
||||||
|
"status_summary": status_summary,
|
||||||
|
"tracking_link": tracking_link,
|
||||||
|
"last_tracking_update": last_tracking_update,
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
from pathlib import Path
|
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
# this is made to demonstrate functionality but it could just as durably be an API call
|
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||||
# called as part of a temporal activity with automatic retries
|
# called as part of a temporal activity with automatic retries
|
||||||
def check_account_valid(args: dict) -> dict:
|
def check_account_valid(args: dict) -> dict:
|
||||||
|
|
||||||
email = args.get("email")
|
email = args.get("email")
|
||||||
account_id = args.get("account_id")
|
account_id = args.get("account_id")
|
||||||
|
|
||||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
file_path = (
|
||||||
|
Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||||
|
)
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return {"error": "Data file not found."}
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
@@ -18,7 +20,12 @@ def check_account_valid(args: dict) -> dict:
|
|||||||
|
|
||||||
for account in account_list:
|
for account in account_list:
|
||||||
if account["email"] == email or account["account_id"] == account_id:
|
if account["email"] == email or account["account_id"] == account_id:
|
||||||
return{"status": "account valid"}
|
return {"status": "account valid"}
|
||||||
|
|
||||||
return_msg = "Account not found with email address " + email + " or account ID: " + account_id
|
return_msg = (
|
||||||
|
"Account not found with email address "
|
||||||
|
+ email
|
||||||
|
+ " or account ID: "
|
||||||
|
+ account_id
|
||||||
|
)
|
||||||
return {"error": return_msg}
|
return {"error": return_msg}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
from pathlib import Path
|
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
# this is made to demonstrate functionality but it could just as durably be an API call
|
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||||
# this assumes it's a valid account - use check_account_valid() to verify that first
|
# this assumes it's a valid account - use check_account_valid() to verify that first
|
||||||
def get_account_balance(args: dict) -> dict:
|
def get_account_balance(args: dict) -> dict:
|
||||||
|
account_key = args.get("email_address_or_account_ID")
|
||||||
|
|
||||||
account_key = args.get("accountkey")
|
file_path = (
|
||||||
|
Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
)
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return {"error": "Data file not found."}
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
@@ -17,7 +19,15 @@ def get_account_balance(args: dict) -> dict:
|
|||||||
|
|
||||||
for account in account_list:
|
for account in account_list:
|
||||||
if account["email"] == account_key or account["account_id"] == account_key:
|
if account["email"] == account_key or account["account_id"] == account_key:
|
||||||
return{ "name": account["name"], "email": account["email"], "account_id": account["account_id"], "checking_balance": account["checking_balance"], "savings_balance": account["savings_balance"], "bitcoin_balance": account["bitcoin_balance"], "account_creation_date": account["account_creation_date"] }
|
return {
|
||||||
|
"name": account["name"],
|
||||||
|
"email": account["email"],
|
||||||
|
"account_id": account["account_id"],
|
||||||
|
"checking_balance": account["checking_balance"],
|
||||||
|
"savings_balance": account["savings_balance"],
|
||||||
|
"bitcoin_balance": account["bitcoin_balance"],
|
||||||
|
"account_creation_date": account["account_creation_date"],
|
||||||
|
}
|
||||||
|
|
||||||
return_msg = "Account not found with for " + account_key
|
return_msg = "Account not found with for " + account_key
|
||||||
return {"error": return_msg}
|
return {"error": return_msg}
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import json
|
import json
|
||||||
from temporalio.client import Client
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from pathlib import Path
|
||||||
import asyncio
|
|
||||||
from temporalio.exceptions import WorkflowAlreadyStartedError
|
from temporalio.exceptions import WorkflowAlreadyStartedError
|
||||||
|
|
||||||
from shared.config import get_temporal_client
|
from shared.config import get_temporal_client
|
||||||
|
|
||||||
|
# enums for the java enum
|
||||||
from enum import Enum, auto
|
|
||||||
|
|
||||||
#enums for the java enum
|
|
||||||
# class ExecutionScenarios(Enum):
|
# class ExecutionScenarios(Enum):
|
||||||
# HAPPY_PATH = 0
|
# HAPPY_PATH = 0
|
||||||
# ADVANCED_VISIBILITY = auto() # 1
|
# ADVANCED_VISIBILITY = auto() # 1
|
||||||
@@ -20,6 +16,7 @@ from enum import Enum, auto
|
|||||||
# BUG_IN_WORKFLOW = auto() # 4
|
# BUG_IN_WORKFLOW = auto() # 4
|
||||||
# INVALID_ACCOUNT = auto() # 5
|
# INVALID_ACCOUNT = auto() # 5
|
||||||
|
|
||||||
|
|
||||||
# these dataclasses are for calling the Temporal Workflow
|
# these dataclasses are for calling the Temporal Workflow
|
||||||
# Python equivalent of the workflow we're calling's Java WorkflowParameterObj
|
# Python equivalent of the workflow we're calling's Java WorkflowParameterObj
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -27,58 +24,78 @@ class MoneyMovementWorkflowParameterObj:
|
|||||||
amount: int # Using snake_case as per Python conventions
|
amount: int # Using snake_case as per Python conventions
|
||||||
scenario: str
|
scenario: str
|
||||||
|
|
||||||
|
|
||||||
# this is made to demonstrate functionality but it could just as durably be an API call
|
# this is made to demonstrate functionality but it could just as durably be an API call
|
||||||
# this assumes it's a valid account - use check_account_valid() to verify that first
|
# this assumes it's a valid account - use check_account_valid() to verify that first
|
||||||
async def move_money(args: dict) -> dict:
|
async def move_money(args: dict) -> dict:
|
||||||
|
account_key = args.get("email_address_or_account_ID")
|
||||||
account_key = args.get("accountkey")
|
|
||||||
account_type: str = args.get("accounttype")
|
account_type: str = args.get("accounttype")
|
||||||
amount = args.get("amount")
|
amount = args.get("amount")
|
||||||
destinationaccount = args.get("destinationaccount")
|
destinationaccount = args.get("destinationaccount")
|
||||||
|
|
||||||
file_path = Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
file_path = (
|
||||||
|
Path(__file__).resolve().parent.parent / "data" / "customer_account_data.json"
|
||||||
|
)
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return {"error": "Data file not found."}
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
# todo validate there's enough money in the account
|
|
||||||
with open(file_path, "r") as file:
|
with open(file_path, "r") as file:
|
||||||
data = json.load(file)
|
data = json.load(file)
|
||||||
account_list = data["accounts"]
|
account_list = data["accounts"]
|
||||||
|
|
||||||
for account in account_list:
|
for account in account_list:
|
||||||
if account["email"] == account_key or account["account_id"] == account_key:
|
if account["email"] == account_key or account["account_id"] == account_key:
|
||||||
amount_str: str = str(amount) # LLM+python gets sassy about types but we need it to be str
|
amount_str: str = str(amount)
|
||||||
from_account_combo = account_key + account_type
|
from_account_combo = account_key + account_type
|
||||||
|
|
||||||
transfer_workflow_id = await start_workflow(amount_cents=str_dollars_to_cents(amount_str),from_account_name=from_account_combo, to_account_name=destinationaccount)
|
transfer_workflow_id = await start_workflow(
|
||||||
|
amount_cents=str_dollars_to_cents(amount_str),
|
||||||
|
from_account_name=from_account_combo,
|
||||||
|
to_account_name=destinationaccount,
|
||||||
|
)
|
||||||
|
|
||||||
account_type_key = 'checking_balance'
|
if account_type.casefold() == "checking":
|
||||||
if(account_type.casefold() == "checking" ):
|
from_key = "checking_balance"
|
||||||
account_type = "checking"
|
elif account_type.casefold() == "savings":
|
||||||
account_type_key = 'checking_balance'
|
from_key = "savings_balance"
|
||||||
|
|
||||||
elif(account_type.casefold() == "savings" ):
|
|
||||||
account_type = "savings"
|
|
||||||
account_type_key = 'savings_balance'
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("money order for account types other than checking or savings is not implemented.")
|
return_msg = "Money order for account types other than checking or savings is not implemented."
|
||||||
|
return {"error": return_msg}
|
||||||
|
|
||||||
new_balance: float = float(str_dollars_to_cents(str(account[account_type_key])))
|
to_key = (
|
||||||
new_balance = new_balance - float(str_dollars_to_cents(amount_str))
|
"savings_balance"
|
||||||
account[account_type_key] = str(new_balance / 100 ) #to dollars
|
if destinationaccount.casefold() == "savings"
|
||||||
with open(file_path, 'w') as file:
|
else "checking_balance"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update from-account balance
|
||||||
|
from_balance = float(str_dollars_to_cents(str(account[from_key])))
|
||||||
|
from_balance -= float(str_dollars_to_cents(amount_str))
|
||||||
|
account[from_key] = str(from_balance / 100)
|
||||||
|
|
||||||
|
# Update destination-account balance
|
||||||
|
to_balance = float(str_dollars_to_cents(str(account[to_key])))
|
||||||
|
to_balance += float(str_dollars_to_cents(amount_str))
|
||||||
|
account[to_key] = str(to_balance / 100)
|
||||||
|
|
||||||
|
with open(file_path, "w") as file:
|
||||||
json.dump(data, file, indent=4)
|
json.dump(data, file, indent=4)
|
||||||
|
|
||||||
return {'status': "money movement complete", 'confirmation id': transfer_workflow_id, 'new_balance': account[account_type_key]}
|
return {
|
||||||
|
"status": "money movement complete",
|
||||||
|
"confirmation id": transfer_workflow_id,
|
||||||
|
"new_balance": account[from_key],
|
||||||
|
"destination_balance": account[to_key],
|
||||||
|
}
|
||||||
|
|
||||||
return_msg = "Account not found with for " + account_key
|
return_msg = "Account not found with for " + account_key
|
||||||
return {"error": return_msg}
|
return {"error": return_msg}
|
||||||
|
|
||||||
# Async function to start workflow
|
|
||||||
async def start_workflow(amount_cents: int, from_account_name: str, to_account_name: str)-> str:
|
|
||||||
|
|
||||||
# Connect to Temporal
|
# Async function to start workflow
|
||||||
client = await get_temporal_client()
|
async def start_workflow(
|
||||||
|
amount_cents: int, from_account_name: str, to_account_name: str
|
||||||
|
) -> str:
|
||||||
start_real_workflow = os.getenv("FIN_START_REAL_WORKFLOW")
|
start_real_workflow = os.getenv("FIN_START_REAL_WORKFLOW")
|
||||||
if start_real_workflow is not None and start_real_workflow.lower() == "false":
|
if start_real_workflow is not None and start_real_workflow.lower() == "false":
|
||||||
START_REAL_WORKFLOW = False
|
START_REAL_WORKFLOW = False
|
||||||
@@ -86,34 +103,39 @@ async def start_workflow(amount_cents: int, from_account_name: str, to_account_n
|
|||||||
START_REAL_WORKFLOW = True
|
START_REAL_WORKFLOW = True
|
||||||
|
|
||||||
if START_REAL_WORKFLOW:
|
if START_REAL_WORKFLOW:
|
||||||
|
# Connect to Temporal
|
||||||
|
client = await get_temporal_client()
|
||||||
# Create the parameter object
|
# Create the parameter object
|
||||||
params = MoneyMovementWorkflowParameterObj(
|
params = MoneyMovementWorkflowParameterObj(
|
||||||
amount=amount_cents,
|
amount=amount_cents, scenario="HAPPY_PATH"
|
||||||
scenario="HAPPY_PATH"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
workflow_id="TRANSFER-ACCT-" + from_account_name + "-TO-" + to_account_name # business-relevant workflow ID
|
workflow_id = (
|
||||||
|
"TRANSFER-ACCT-" + from_account_name + "-TO-" + to_account_name
|
||||||
|
) # business-relevant workflow ID
|
||||||
|
|
||||||
try:
|
try:
|
||||||
handle = await client.start_workflow(
|
handle = await client.start_workflow(
|
||||||
"moneyTransferWorkflow", # Workflow name
|
"moneyTransferWorkflow", # Workflow name
|
||||||
params, # Workflow parameters
|
params, # Workflow parameters
|
||||||
id=workflow_id,
|
id=workflow_id,
|
||||||
task_queue="MoneyTransferJava" # Task queue name
|
task_queue="MoneyTransferJava", # Task queue name
|
||||||
)
|
)
|
||||||
return handle.id
|
return handle.id
|
||||||
except WorkflowAlreadyStartedError as e:
|
except WorkflowAlreadyStartedError:
|
||||||
existing_handle = client.get_workflow_handle(workflow_id=workflow_id)
|
existing_handle = client.get_workflow_handle(workflow_id=workflow_id)
|
||||||
return existing_handle.id
|
return existing_handle.id
|
||||||
else:
|
else:
|
||||||
return "TRANSFER-ACCT-" + from_account_name + "-TO-" + to_account_name + "not-real"
|
return (
|
||||||
|
"TRANSFER-ACCT-" + from_account_name + "-TO-" + to_account_name + "not-real"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#cleans a string dollar amount description to cents value
|
# cleans a string dollar amount description to cents value
|
||||||
def str_dollars_to_cents(dollar_str: str) -> int:
|
def str_dollars_to_cents(dollar_str: str) -> int:
|
||||||
try:
|
try:
|
||||||
# Remove '$' and any whitespace
|
# Remove '$' and any whitespace
|
||||||
cleaned_str = dollar_str.replace('$', '').strip()
|
cleaned_str = dollar_str.replace("$", "").strip()
|
||||||
|
|
||||||
# Handle empty string or invalid input
|
# Handle empty string or invalid input
|
||||||
if not cleaned_str:
|
if not cleaned_str:
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
from datetime import date, timedelta
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import json
|
|
||||||
from temporalio.client import (
|
|
||||||
Client,
|
|
||||||
WithStartWorkflowOperation,
|
|
||||||
WorkflowHandle,
|
|
||||||
WorkflowUpdateFailedError,
|
|
||||||
)
|
|
||||||
from temporalio import common
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from datetime import date
|
||||||
import asyncio
|
|
||||||
from temporalio.exceptions import WorkflowAlreadyStartedError
|
from temporalio import common
|
||||||
|
from temporalio.client import WithStartWorkflowOperation, WorkflowUpdateFailedError
|
||||||
|
|
||||||
from shared.config import get_temporal_client
|
from shared.config import get_temporal_client
|
||||||
|
|
||||||
|
|
||||||
@@ -24,39 +16,55 @@ class TransactionRequest:
|
|||||||
sourceAccount: str
|
sourceAccount: str
|
||||||
targetAccount: str
|
targetAccount: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TxResult:
|
class TxResult:
|
||||||
transactionId: str
|
transactionId: str
|
||||||
status: str
|
status: str
|
||||||
|
|
||||||
#demonstrate starting a workflow and early return pattern while the workflow continues
|
|
||||||
|
# demonstrate starting a workflow and early return pattern while the workflow continues
|
||||||
async def submit_loan_application(args: dict) -> dict:
|
async def submit_loan_application(args: dict) -> dict:
|
||||||
account_key = args.get("accountkey")
|
account_key = args.get("email_address_or_account_ID")
|
||||||
amount = args.get("amount")
|
amount = args.get("amount")
|
||||||
|
|
||||||
loan_status: dict = await start_workflow(amount=amount,account_name=account_key)
|
loan_status: dict = await start_workflow(amount=amount, account_name=account_key)
|
||||||
|
|
||||||
if loan_status.get("error") is None:
|
if loan_status.get("error") is None:
|
||||||
return {'status': loan_status.get("loan_application_status"), 'detailed_status': loan_status.get("application_details"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("transaction_id")}
|
return {
|
||||||
|
"status": loan_status.get("loan_application_status"),
|
||||||
|
"detailed_status": loan_status.get("application_details"),
|
||||||
|
"next_step": loan_status.get("advisement"),
|
||||||
|
"confirmation_id": loan_status.get("transaction_id"),
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
print(loan_status)
|
print(loan_status)
|
||||||
return loan_status
|
return loan_status
|
||||||
|
|
||||||
|
|
||||||
# Async function to start workflow
|
# Async function to start workflow
|
||||||
async def start_workflow(amount: str, account_name: str, )-> dict:
|
async def start_workflow(
|
||||||
|
amount: str,
|
||||||
# Connect to Temporal
|
account_name: str,
|
||||||
client = await get_temporal_client()
|
) -> dict:
|
||||||
start_real_workflow = os.getenv("FIN_START_REAL_WORKFLOW")
|
start_real_workflow = os.getenv("FIN_START_REAL_WORKFLOW")
|
||||||
if start_real_workflow is not None and start_real_workflow.lower() == "false":
|
if start_real_workflow is not None and start_real_workflow.lower() == "false":
|
||||||
START_REAL_WORKFLOW = False
|
# START_REAL_WORKFLOW = False
|
||||||
return {'loan_application_status': "applied", 'application_details': "loan application is submitted and initial validation is complete",'transaction_id': "APPLICATION"+account_name, 'advisement': "You'll receive a confirmation for final approval in three business days", }
|
return {
|
||||||
|
"loan_application_status": "applied",
|
||||||
|
"application_details": "loan application is submitted and initial validation is complete",
|
||||||
|
"transaction_id": "APPLICATION" + account_name,
|
||||||
|
"advisement": "You'll receive a confirmation for final approval in three business days",
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
START_REAL_WORKFLOW = True
|
# START_REAL_WORKFLOW = True
|
||||||
|
# Connect to Temporal
|
||||||
|
client = await get_temporal_client()
|
||||||
|
|
||||||
# Define the workflow ID and task queue
|
# Define the workflow ID and task queue
|
||||||
workflow_id = "LOAN_APPLICATION-"+account_name+"-"+date.today().strftime('%Y-%m-%d')
|
workflow_id = (
|
||||||
|
"LOAN_APPLICATION-" + account_name + "-" + date.today().strftime("%Y-%m-%d")
|
||||||
|
)
|
||||||
task_queue = "LatencyOptimizationTEST"
|
task_queue = "LatencyOptimizationTEST"
|
||||||
|
|
||||||
# Create a TransactionRequest (matching the Java workflow's expected input)
|
# Create a TransactionRequest (matching the Java workflow's expected input)
|
||||||
@@ -83,21 +91,27 @@ async def start_workflow(amount: str, account_name: str, )-> dict:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
except WorkflowUpdateFailedError:
|
except WorkflowUpdateFailedError:
|
||||||
print("aww man got exception WorkflowUpdateFailedError" )
|
print("aww man got exception WorkflowUpdateFailedError")
|
||||||
tx_result = None
|
tx_result = None
|
||||||
return_msg = "Loan could not be processed for " + account_name
|
return_msg = "Loan could not be processed for " + account_name
|
||||||
return {"error": return_msg}
|
return {"error": return_msg}
|
||||||
|
|
||||||
workflow_handle = await start_op.workflow_handle()
|
workflow_handle = await start_op.workflow_handle()
|
||||||
|
print(f"Workflow started with ID: {workflow_handle.id}")
|
||||||
print(tx_result)
|
print(tx_result)
|
||||||
|
|
||||||
print(f"Update result: Transaction ID = {tx_result.transactionId}, Message = {tx_result.status}")
|
print(
|
||||||
|
f"Update result: Transaction ID = {tx_result.transactionId}, Message = {tx_result.status}"
|
||||||
|
)
|
||||||
|
|
||||||
# Optionally, wait for the workflow to complete and get the final result
|
# Optionally, wait for the workflow to complete and get the final result
|
||||||
# final_result = await handle.result()
|
# final_result = await handle.result()
|
||||||
# print(f"Workflow completed with result: {final_result}")
|
# print(f"Workflow completed with result: {final_result}")
|
||||||
|
|
||||||
|
|
||||||
# return {'status': loan_status.get("loan_status"), 'detailed_status': loan_status.get("results"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("workflowID")}
|
# return {'status': loan_status.get("loan_status"), 'detailed_status': loan_status.get("results"), 'next_step': loan_status.get("advisement"), 'confirmation_id': loan_status.get("workflowID")}
|
||||||
return {'loan_application_status': "applied", 'application_details': "loan application is submitted and initial validation is complete",'transaction_id': tx_result.transactionId, 'advisement': "You'll receive a confirmation for final approval in three business days", }
|
return {
|
||||||
|
"loan_application_status": "applied",
|
||||||
|
"application_details": "loan application is submitted and initial validation is complete",
|
||||||
|
"transaction_id": tx_result.transactionId,
|
||||||
|
"advisement": "You'll receive a confirmation for final approval in three business days",
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def find_events(args: dict) -> dict:
|
def find_events(args: dict) -> dict:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ TREASURE_LOCATION = {
|
|||||||
"state_full": "Washington",
|
"state_full": "Washington",
|
||||||
"state_abbrev": "WA",
|
"state_abbrev": "WA",
|
||||||
"zip": "98121",
|
"zip": "98121",
|
||||||
"country": "USA"
|
"country": "USA",
|
||||||
}
|
}
|
||||||
|
|
||||||
HINTS = [
|
HINTS = [
|
||||||
@@ -12,8 +12,8 @@ HINTS = [
|
|||||||
"state of " + TREASURE_LOCATION["state_full"],
|
"state of " + TREASURE_LOCATION["state_full"],
|
||||||
"city of " + TREASURE_LOCATION["city"],
|
"city of " + TREASURE_LOCATION["city"],
|
||||||
"at a company HQ",
|
"at a company HQ",
|
||||||
"The company's tech traces its roots to a project called Cadence", #thanks, Grok
|
"The company's tech traces its roots to a project called Cadence", # thanks, Grok
|
||||||
"The company offers a tool that lets developers write code as if it's running forever, no matter what crashes", #thanks, Grok
|
"The company offers a tool that lets developers write code as if it's running forever, no matter what crashes", # thanks, Grok
|
||||||
]
|
]
|
||||||
''' Additional Grok provided hints about Temporal:
|
''' Additional Grok provided hints about Temporal:
|
||||||
"This company was founded by two engineers who previously worked on a system named after a South American river at Uber."
|
"This company was founded by two engineers who previously worked on a system named after a South American river at Uber."
|
||||||
@@ -26,6 +26,7 @@ HINTS = [
|
|||||||
"They’re backed by big venture capital names like Sequoia, betting on their vision for reliable software."
|
"They’re backed by big venture capital names like Sequoia, betting on their vision for reliable software."
|
||||||
"The company’s name might remind you of a word for something fleeting, yet their tech is built to last."'''
|
"The company’s name might remind you of a word for something fleeting, yet their tech is built to last."'''
|
||||||
|
|
||||||
|
|
||||||
def give_hint(args: dict) -> dict:
|
def give_hint(args: dict) -> dict:
|
||||||
hint_total = args.get("hint_total")
|
hint_total = args.get("hint_total")
|
||||||
if hint_total is None:
|
if hint_total is None:
|
||||||
@@ -35,7 +36,4 @@ def give_hint(args: dict) -> dict:
|
|||||||
hint_text = HINTS[index]
|
hint_text = HINTS[index]
|
||||||
|
|
||||||
hint_total = hint_total + 1
|
hint_total = hint_total + 1
|
||||||
return {
|
return {"hint_number": hint_total, "hint": hint_text}
|
||||||
"hint_number": hint_total,
|
|
||||||
"hint": hint_text
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,46 +1,52 @@
|
|||||||
|
import os
|
||||||
from typing import List
|
from typing import List
|
||||||
from models.tool_definitions import AgentGoal
|
|
||||||
import tools.tool_registry as tool_registry
|
import tools.tool_registry as tool_registry
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
|
||||||
# Turn on Silly Mode - this should be a description of the persona you'd like the bot to have and can be a single word or a phrase.
|
# 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:
|
# 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="Christopher Walken"
|
||||||
#SILLY_MODE="belligerent"
|
# SILLY_MODE="belligerent"
|
||||||
#
|
#
|
||||||
# Example if you want it to take on a persona (include 'a'):
|
# Example if you want it to take on a persona (include 'a'):
|
||||||
#SILLY_MODE="a pirate"
|
# SILLY_MODE="a pirate"
|
||||||
# Note - this only works with certain LLMs. Grok for sure will stay in character, while OpenAI will not.
|
# Note - this only works with certain LLMs. Grok for sure will stay in character, while OpenAI will not.
|
||||||
SILLY_MODE="off"
|
SILLY_MODE = "off"
|
||||||
if SILLY_MODE is not None and 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. "
|
silly_prompt = "You are " + SILLY_MODE + ", stay in character at all times. "
|
||||||
print("Silly mode is on: " + SILLY_MODE)
|
print("Silly mode is on: " + SILLY_MODE)
|
||||||
else:
|
else:
|
||||||
silly_prompt = ""
|
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."
|
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(
|
goal_choose_agent_type = AgentGoal(
|
||||||
id = "goal_choose_agent_type",
|
id="goal_choose_agent_type",
|
||||||
category_tag="agent_selection",
|
category_tag="agent_selection",
|
||||||
agent_name="Choose Agent",
|
agent_name="Choose Agent",
|
||||||
agent_friendly_description="Choose the type of agent to assist you today.",
|
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=[
|
tools=[
|
||||||
tool_registry.list_agents_tool,
|
tool_registry.list_agents_tool,
|
||||||
tool_registry.change_goal_tool,
|
tool_registry.change_goal_tool,
|
||||||
],
|
],
|
||||||
description="The user wants to choose which type of agent they will interact with. "
|
description="The user wants to choose which type of agent they will interact with. "
|
||||||
"Help the user gather args for these tools, in order: "
|
"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. "
|
"1. ListAgents: List agents available to interact with. Do not ask for user confirmation for this tool. "
|
||||||
"2. ChangeGoal: Change goal of agent "
|
"2. ChangeGoal: Change goal of agent "
|
||||||
"After these tools are complete, change your goal to the new goal as chosen by the user. ",
|
"After these tools are complete, change your goal to the new goal as chosen by the user. ",
|
||||||
starter_prompt=starter_prompt_generic + " Begin by listing all details of all agents as provided by the output of the first tool included in this goal. ",
|
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(
|
example_conversation_history="\n ".join(
|
||||||
[
|
[
|
||||||
"agent: Here are the currently available agents.",
|
"agent: Here are the currently available agents.",
|
||||||
"user_confirmed_tool_run: <user clicks confirm on ListAgents tool>",
|
"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',"
|
||||||
"tool_result: { '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: 1. Event Flight Finder. \n Which agent would you like to speak to? (You can respond with name or number.)",
|
"agent: The available agents are: Event Flight Finder and Schedule PTO. \n Which agent would you like to work with? ",
|
||||||
"user: 1, Event Flight Finder",
|
"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>",
|
"user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>",
|
||||||
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
|
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
|
||||||
]
|
]
|
||||||
@@ -54,7 +60,7 @@ pirate_category_tag = "pirate"
|
|||||||
if SILLY_MODE == "a pirate":
|
if SILLY_MODE == "a pirate":
|
||||||
pirate_category_tag = "system"
|
pirate_category_tag = "system"
|
||||||
goal_pirate_treasure = AgentGoal(
|
goal_pirate_treasure = AgentGoal(
|
||||||
id = "goal_pirate_treasure",
|
id="goal_pirate_treasure",
|
||||||
category_tag=pirate_category_tag,
|
category_tag=pirate_category_tag,
|
||||||
agent_name="Arrr, Find Me Treasure!",
|
agent_name="Arrr, Find Me Treasure!",
|
||||||
agent_friendly_description="Sail the high seas and find me pirate treasure, ye land lubber!",
|
agent_friendly_description="Sail the high seas and find me pirate treasure, ye land lubber!",
|
||||||
@@ -95,8 +101,9 @@ goal_pirate_treasure = AgentGoal(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ----- Travel Goals ---
|
||||||
goal_match_train_invoice = AgentGoal(
|
goal_match_train_invoice = AgentGoal(
|
||||||
id = "goal_match_train_invoice",
|
id="goal_match_train_invoice",
|
||||||
category_tag="travel-trains",
|
category_tag="travel-trains",
|
||||||
agent_name="UK Premier League Match Trip Booking",
|
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.",
|
agent_friendly_description="Book a trip to a city in the UK around the dates of a premier league match.",
|
||||||
@@ -108,10 +115,10 @@ goal_match_train_invoice = AgentGoal(
|
|||||||
],
|
],
|
||||||
description="The user wants to book a trip to a city in the UK around the dates of a premier league match. "
|
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. "
|
"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. "
|
"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: "
|
"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. "
|
"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 and list them for the customer to choose from "
|
"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 "
|
"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 ",
|
"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,
|
starter_prompt=starter_prompt_generic,
|
||||||
@@ -143,7 +150,7 @@ goal_match_train_invoice = AgentGoal(
|
|||||||
)
|
)
|
||||||
|
|
||||||
goal_event_flight_invoice = AgentGoal(
|
goal_event_flight_invoice = AgentGoal(
|
||||||
id = "goal_event_flight_invoice",
|
id="goal_event_flight_invoice",
|
||||||
category_tag="travel-flights",
|
category_tag="travel-flights",
|
||||||
agent_name="Australia and New Zealand Event Flight Booking",
|
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.",
|
agent_friendly_description="Book a trip to a city in Australia or New Zealand around the dates of events in that city.",
|
||||||
@@ -180,9 +187,10 @@ goal_event_flight_invoice = AgentGoal(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ----- HR Goals ---
|
||||||
# This goal uses the data/employee_pto_data.json file as dummy data.
|
# This goal uses the data/employee_pto_data.json file as dummy data.
|
||||||
goal_hr_schedule_pto = AgentGoal(
|
goal_hr_schedule_pto = AgentGoal(
|
||||||
id = "goal_hr_schedule_pto",
|
id="goal_hr_schedule_pto",
|
||||||
category_tag="hr",
|
category_tag="hr",
|
||||||
agent_name="Schedule PTO",
|
agent_name="Schedule PTO",
|
||||||
agent_friendly_description="Schedule PTO based on your available PTO.",
|
agent_friendly_description="Schedule PTO based on your available PTO.",
|
||||||
@@ -220,7 +228,7 @@ goal_hr_schedule_pto = AgentGoal(
|
|||||||
|
|
||||||
# This goal uses the data/employee_pto_data.json file as dummy data.
|
# This goal uses the data/employee_pto_data.json file as dummy data.
|
||||||
goal_hr_check_pto = AgentGoal(
|
goal_hr_check_pto = AgentGoal(
|
||||||
id = "goal_hr_check_pto",
|
id="goal_hr_check_pto",
|
||||||
category_tag="hr",
|
category_tag="hr",
|
||||||
agent_name="Check PTO Amount",
|
agent_name="Check PTO Amount",
|
||||||
agent_friendly_description="Check your available PTO.",
|
agent_friendly_description="Check your available PTO.",
|
||||||
@@ -245,7 +253,7 @@ goal_hr_check_pto = AgentGoal(
|
|||||||
|
|
||||||
# check integration with bank
|
# check integration with bank
|
||||||
goal_hr_check_paycheck_bank_integration_status = AgentGoal(
|
goal_hr_check_paycheck_bank_integration_status = AgentGoal(
|
||||||
id = "goal_hr_check_paycheck_bank_integration_status",
|
id="goal_hr_check_paycheck_bank_integration_status",
|
||||||
category_tag="hr",
|
category_tag="hr",
|
||||||
agent_name="Check paycheck deposit status",
|
agent_name="Check paycheck deposit status",
|
||||||
agent_friendly_description="Check your integration between your employer and your financial institution.",
|
agent_friendly_description="Check your integration between your employer and your financial institution.",
|
||||||
@@ -268,11 +276,12 @@ goal_hr_check_paycheck_bank_integration_status = AgentGoal(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ----- FinServ Goals ---
|
||||||
# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data
|
# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data
|
||||||
goal_fin_check_account_balances = AgentGoal(
|
goal_fin_check_account_balances = AgentGoal(
|
||||||
id = "goal_fin_check_account_balances",
|
id="goal_fin_check_account_balances",
|
||||||
category_tag="fin",
|
category_tag="fin",
|
||||||
agent_name="Check balances",
|
agent_name="Account Balances",
|
||||||
agent_friendly_description="Check your account balances in Checking, Savings, etc.",
|
agent_friendly_description="Check your account balances in Checking, Savings, etc.",
|
||||||
tools=[
|
tools=[
|
||||||
tool_registry.financial_check_account_is_valid,
|
tool_registry.financial_check_account_is_valid,
|
||||||
@@ -285,7 +294,7 @@ goal_fin_check_account_balances = AgentGoal(
|
|||||||
example_conversation_history="\n ".join(
|
example_conversation_history="\n ".join(
|
||||||
[
|
[
|
||||||
"user: I'd like to check my account balances",
|
"user: I'd like to check my account balances",
|
||||||
"agent: Sure! I can help you out with that. May I have your email address or account number?",
|
"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: email is bob.johnson@emailzzz.com ",
|
||||||
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||||
"tool_result: { 'status': account valid }",
|
"tool_result: { 'status': account valid }",
|
||||||
@@ -302,12 +311,12 @@ goal_fin_check_account_balances = AgentGoal(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# this tool checks account balances, and uses ./data/customer_account_data.json as dummy data
|
# 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
|
# it also uses a separate workflow/tool, see ./SETUP.md for details
|
||||||
goal_fin_move_money = AgentGoal(
|
goal_fin_move_money = AgentGoal(
|
||||||
id = "goal_fin_move_money",
|
id="goal_fin_move_money",
|
||||||
category_tag="fin",
|
category_tag="fin",
|
||||||
agent_name="Money Order",
|
agent_name="Money Movement",
|
||||||
agent_friendly_description="Initiate a money movement order.",
|
agent_friendly_description="Initiate money movement.",
|
||||||
tools=[
|
tools=[
|
||||||
tool_registry.financial_check_account_is_valid,
|
tool_registry.financial_check_account_is_valid,
|
||||||
tool_registry.financial_get_account_balances,
|
tool_registry.financial_get_account_balances,
|
||||||
@@ -316,13 +325,13 @@ goal_fin_move_money = AgentGoal(
|
|||||||
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: "
|
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"
|
"1. FinCheckAccountIsValid: validate the user's account is valid"
|
||||||
"2. FinCheckAccountBalance: Tell the user their account balance at the bank or financial institution"
|
"2. FinCheckAccountBalance: Tell the user their account balance at the bank or financial institution"
|
||||||
"3. FinMoveMoney: Initiate a money movement order",
|
"3. FinMoveMoney: Initiate money movement (transfer)",
|
||||||
starter_prompt=starter_prompt_generic,
|
starter_prompt=starter_prompt_generic,
|
||||||
example_conversation_history="\n ".join(
|
example_conversation_history="\n ".join(
|
||||||
[
|
[
|
||||||
"user: I'd like to transfer some money",
|
"user: I'd like to transfer some money",
|
||||||
"agent: Sure! I can help you out with that. May I have account number and email address?",
|
"agent: Sure! I can help you out with that. May I have account number and email address?",
|
||||||
"user: account number is 11235813",
|
"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>",
|
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||||
"tool_result: { 'status': account valid }",
|
"tool_result: { 'status': account valid }",
|
||||||
"agent: Great! Here are your account balances:",
|
"agent: Great! Here are your account balances:",
|
||||||
@@ -336,21 +345,21 @@ goal_fin_move_money = AgentGoal(
|
|||||||
"user: I'd like to move $500 from savings to account number #56789",
|
"user: I'd like to move $500 from savings to account number #56789",
|
||||||
"user_confirmed_tool_run: <user clicks confirm on FinMoveMoney tool>",
|
"user_confirmed_tool_run: <user clicks confirm on FinMoveMoney tool>",
|
||||||
"tool_result: { 'status': money movement complete, 'confirmation id': 333421, 'new_balance': $2700.15 }",
|
"tool_result: { 'status': money movement complete, 'confirmation id': 333421, 'new_balance': $2700.15 }",
|
||||||
"agent: Money movement order completed! New account balance: $2700.15. Your confirmation id is 333421. "
|
"agent: Money movement completed! New account balance: $2700.15. Your confirmation id is 333421. ",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# this starts a loan approval process
|
# this starts a loan approval process
|
||||||
# it also uses a separate workflow/tool, see ./setup.md for details #todo
|
# it also uses a separate workflow/tool, see ./SETUP.md for details
|
||||||
goal_fin_loan_application = AgentGoal(
|
goal_fin_loan_application = AgentGoal(
|
||||||
id = "goal_fin_loan_application",
|
id="goal_fin_loan_application",
|
||||||
category_tag="fin",
|
category_tag="fin",
|
||||||
agent_name="Easy Loan Apply",
|
agent_name="Easy Loan",
|
||||||
agent_friendly_description="Initiate loan application.",
|
agent_friendly_description="Initiate a simple loan application.",
|
||||||
tools=[
|
tools=[
|
||||||
tool_registry.financial_check_account_is_valid,
|
tool_registry.financial_check_account_is_valid,
|
||||||
tool_registry.financial_submit_loan_approval, #todo
|
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: "
|
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"
|
"1. FinCheckAccountIsValid: validate the user's account is valid"
|
||||||
@@ -359,7 +368,7 @@ goal_fin_loan_application = AgentGoal(
|
|||||||
example_conversation_history="\n ".join(
|
example_conversation_history="\n ".join(
|
||||||
[
|
[
|
||||||
"user: I'd like to apply for a loan",
|
"user: I'd like to apply for a loan",
|
||||||
"agent: Sure! I can help you out with that. May I have account number for confirmation?",
|
"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: account number is 11235813",
|
||||||
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||||
"tool_result: { 'status': account valid }",
|
"tool_result: { 'status': account valid }",
|
||||||
@@ -367,12 +376,86 @@ goal_fin_loan_application = AgentGoal(
|
|||||||
"user: I'd like a loan for $500",
|
"user: I'd like a loan for $500",
|
||||||
"user_confirmed_tool_run: <user clicks confirm on FinCheckAccountSubmitLoanApproval tool>",
|
"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 }",
|
"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. "
|
"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. ",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
#Add the goals to a list for more generic processing, like listing available agents
|
# ----- 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!",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the goals to a list for more generic processing, like listing available agents
|
||||||
goal_list: List[AgentGoal] = []
|
goal_list: List[AgentGoal] = []
|
||||||
goal_list.append(goal_choose_agent_type)
|
goal_list.append(goal_choose_agent_type)
|
||||||
goal_list.append(goal_pirate_treasure)
|
goal_list.append(goal_pirate_treasure)
|
||||||
@@ -384,6 +467,29 @@ goal_list.append(goal_hr_check_paycheck_bank_integration_status)
|
|||||||
goal_list.append(goal_fin_check_account_balances)
|
goal_list.append(goal_fin_check_account_balances)
|
||||||
goal_list.append(goal_fin_move_money)
|
goal_list.append(goal_fin_move_money)
|
||||||
goal_list.append(goal_fin_loan_application)
|
goal_list.append(goal_fin_loan_application)
|
||||||
|
goal_list.append(goal_ecomm_list_orders)
|
||||||
|
goal_list.append(goal_ecomm_order_status)
|
||||||
|
|
||||||
|
|
||||||
|
# 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 = True # default if unset
|
||||||
|
elif (
|
||||||
|
first_goal_value is not None
|
||||||
|
and first_goal_value.lower() != "goal_choose_agent_type"
|
||||||
|
):
|
||||||
|
multi_goal_mode = False
|
||||||
|
else:
|
||||||
|
multi_goal_mode = True
|
||||||
|
|
||||||
|
if multi_goal_mode:
|
||||||
|
for goal in goal_list:
|
||||||
|
list_agents_found: bool = False
|
||||||
|
for tool in goal.tools:
|
||||||
|
if tool.name == "ListAgents":
|
||||||
|
list_agents_found = True
|
||||||
|
continue
|
||||||
|
if list_agents_found is False:
|
||||||
|
goal.tools.append(tool_registry.list_agents_tool)
|
||||||
|
continue
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from .give_hint import TREASURE_LOCATION
|
from .give_hint import TREASURE_LOCATION
|
||||||
|
|
||||||
def guess_location(args: dict) -> dict:
|
|
||||||
|
|
||||||
|
def guess_location(args: dict) -> dict:
|
||||||
guess_address = args.get("address").lower()
|
guess_address = args.get("address").lower()
|
||||||
guess_city = args.get("city").lower()
|
guess_city = args.get("city").lower()
|
||||||
guess_state = args.get("state").lower()
|
guess_state = args.get("state").lower()
|
||||||
@@ -11,8 +11,12 @@ def guess_location(args: dict) -> dict:
|
|||||||
else:
|
else:
|
||||||
compare_state = TREASURE_LOCATION.get("state_full").lower()
|
compare_state = TREASURE_LOCATION.get("state_full").lower()
|
||||||
|
|
||||||
#Check for the street address to be included in the guess to account for "st" vs "street" or leaving Street off entirely
|
# Check for the street address to be included in the guess to account for "st" vs "street" or leaving Street off entirely
|
||||||
if TREASURE_LOCATION.get("address").lower() in guess_address and TREASURE_LOCATION.get("city").lower() == guess_city and compare_state == guess_state:
|
if (
|
||||||
|
TREASURE_LOCATION.get("address").lower() in guess_address
|
||||||
|
and TREASURE_LOCATION.get("city").lower() == guess_city
|
||||||
|
and compare_state == guess_state
|
||||||
|
):
|
||||||
return {"treasure_found": "True"}
|
return {"treasure_found": "True"}
|
||||||
else:
|
else:
|
||||||
return {"treasure_found": "False"}
|
return {"treasure_found": "False"}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
def book_pto(args: dict) -> dict:
|
def book_pto(args: dict) -> dict:
|
||||||
|
|
||||||
email = args.get("email")
|
email = args.get("email")
|
||||||
start_date = args.get("start_date")
|
start_date = args.get("start_date")
|
||||||
end_date = args.get("end_date")
|
end_date = args.get("end_date")
|
||||||
|
|
||||||
print(f"[BookPTO] Totally would send an email confirmation of PTO from {start_date} to {end_date} to {email} here!")
|
print(
|
||||||
|
f"[BookPTO] Totally would send an email confirmation of PTO from {start_date} to {end_date} to {email} here!"
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {"status": "success"}
|
||||||
"status": "success"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
from pathlib import Path
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def checkpaybankstatus(args: dict) -> dict:
|
def checkpaybankstatus(args: dict) -> dict:
|
||||||
|
|
||||||
email = args.get("email")
|
email = args.get("email")
|
||||||
|
|
||||||
if email == "grinch@grinch.com":
|
if email == "grinch@grinch.com":
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from pathlib import Path
|
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def current_pto(args: dict) -> dict:
|
def current_pto(args: dict) -> dict:
|
||||||
|
|
||||||
email = args.get("email")
|
email = args.get("email")
|
||||||
|
|
||||||
file_path = Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
file_path = (
|
||||||
|
Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
||||||
|
)
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return {"error": "Data file not found."}
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ def current_pto(args: dict) -> dict:
|
|||||||
for employee in employee_list:
|
for employee in employee_list:
|
||||||
if employee["email"] == email:
|
if employee["email"] == email:
|
||||||
num_hours = int(employee["currentPTOHrs"])
|
num_hours = int(employee["currentPTOHrs"])
|
||||||
num_days = float(num_hours/8)
|
num_days = float(num_hours / 8)
|
||||||
return {
|
return {
|
||||||
"num_hours": num_hours,
|
"num_hours": num_hours,
|
||||||
"num_days": num_days,
|
"num_days": num_days,
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import json
|
import json
|
||||||
import pandas
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
|
||||||
def future_pto_calc(args: dict) -> dict:
|
def future_pto_calc(args: dict) -> dict:
|
||||||
|
file_path = (
|
||||||
file_path = Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
|
||||||
|
)
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return {"error": "Data file not found."}
|
return {"error": "Data file not found."}
|
||||||
|
|
||||||
@@ -15,25 +17,39 @@ def future_pto_calc(args: dict) -> dict:
|
|||||||
end_date = datetime.strptime(args.get("end_date"), "%Y-%m-%d").date()
|
end_date = datetime.strptime(args.get("end_date"), "%Y-%m-%d").date()
|
||||||
email = args.get("email")
|
email = args.get("email")
|
||||||
|
|
||||||
#Next, set up the ability to calculate how much PTO will be added to the user's total by the start of the PTO request
|
# Next, set up the ability to calculate how much PTO will be added to the user's total by the start of the PTO request
|
||||||
today = date.today()
|
today = date.today()
|
||||||
|
|
||||||
if today > start_date:
|
if today > start_date:
|
||||||
return_msg = "PTO start date " + args.get("start_date") + "cannot be in the past"
|
return_msg = (
|
||||||
|
"PTO start date " + args.get("start_date") + "cannot be in the past"
|
||||||
|
)
|
||||||
return {"error": return_msg}
|
return {"error": return_msg}
|
||||||
|
|
||||||
if end_date < start_date:
|
if end_date < start_date:
|
||||||
return_msg = "PTO end date " + args.get("end_date") + " must be after PTO start date " + args.get("start_date")
|
return_msg = (
|
||||||
|
"PTO end date "
|
||||||
|
+ args.get("end_date")
|
||||||
|
+ " must be after PTO start date "
|
||||||
|
+ args.get("start_date")
|
||||||
|
)
|
||||||
return {"error": return_msg}
|
return {"error": return_msg}
|
||||||
|
|
||||||
#Get the number of business days, and then business hours (assume 8 hr biz day), included in the PTO request
|
# Get the number of business days, and then business hours (assume 8 hr biz day), included in the PTO request
|
||||||
biz_days_of_request = len(pandas.bdate_range(start=start_date, end=end_date, inclusive="both"))
|
biz_days_of_request = len(
|
||||||
|
pandas.bdate_range(start=start_date, end=end_date, inclusive="both")
|
||||||
|
)
|
||||||
if biz_days_of_request == 0:
|
if biz_days_of_request == 0:
|
||||||
return_msg = "There are no business days between " + args.get("start_date") + " and " + args.get("end_date")
|
return_msg = (
|
||||||
|
"There are no business days between "
|
||||||
|
+ args.get("start_date")
|
||||||
|
+ " and "
|
||||||
|
+ args.get("end_date")
|
||||||
|
)
|
||||||
return {"error": return_msg}
|
return {"error": return_msg}
|
||||||
biz_hours_of_request = biz_days_of_request * 8
|
biz_hours_of_request = biz_days_of_request * 8
|
||||||
|
|
||||||
#Assume PTO is added on the first of every month - month math compares rolling dates, so compare the PTO request with the first day of the current month.
|
# Assume PTO is added on the first of every month - month math compares rolling dates, so compare the PTO request with the first day of the current month.
|
||||||
today_first_of_month = date(today.year, today.month, 1)
|
today_first_of_month = date(today.year, today.month, 1)
|
||||||
time_difference = relativedelta(start_date, today_first_of_month)
|
time_difference = relativedelta(start_date, today_first_of_month)
|
||||||
months_to_accrue = time_difference.years * 12 + time_difference.months
|
months_to_accrue = time_difference.years * 12 + time_difference.months
|
||||||
@@ -47,7 +63,9 @@ def future_pto_calc(args: dict) -> dict:
|
|||||||
if employee["email"] == email:
|
if employee["email"] == email:
|
||||||
current_pto_hours = int(employee["currentPTOHrs"])
|
current_pto_hours = int(employee["currentPTOHrs"])
|
||||||
hrs_added_per_month = int(employee["hrsAddedPerMonth"])
|
hrs_added_per_month = int(employee["hrsAddedPerMonth"])
|
||||||
pto_available_at_start = current_pto_hours + (months_to_accrue * hrs_added_per_month)
|
pto_available_at_start = current_pto_hours + (
|
||||||
|
months_to_accrue * hrs_added_per_month
|
||||||
|
)
|
||||||
pto_hrs_remaining_after = pto_available_at_start - biz_hours_of_request
|
pto_hrs_remaining_after = pto_available_at_start - biz_hours_of_request
|
||||||
if pto_hrs_remaining_after >= 0:
|
if pto_hrs_remaining_after >= 0:
|
||||||
enough_pto = True
|
enough_pto = True
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import tools.goal_registry as goals
|
import tools.goal_registry as goals
|
||||||
|
|
||||||
def list_agents(args: dict) -> dict:
|
|
||||||
|
|
||||||
|
def list_agents(args: dict) -> dict:
|
||||||
goal_categories_start = os.getenv("GOAL_CATEGORIES")
|
goal_categories_start = os.getenv("GOAL_CATEGORIES")
|
||||||
if goal_categories_start is None:
|
if goal_categories_start is None:
|
||||||
goal_categories = ["all"] # default to 'all' categories
|
goal_categories = ["all"] # default to 'all' categories
|
||||||
@@ -11,9 +12,12 @@ def list_agents(args: dict) -> dict:
|
|||||||
goal_categories = goal_categories_start.split(",")
|
goal_categories = goal_categories_start.split(",")
|
||||||
|
|
||||||
# if multi-goal-mode, add agent_selection as a goal (defaults to True)
|
# if multi-goal-mode, add agent_selection as a goal (defaults to True)
|
||||||
if "agent_selection" not in goal_categories :
|
if "agent_selection" not in goal_categories:
|
||||||
first_goal_value = os.getenv("AGENT_GOAL")
|
first_goal_value = os.getenv("AGENT_GOAL")
|
||||||
if first_goal_value is None or first_goal_value.lower() == "goal_choose_agent_type":
|
if (
|
||||||
|
first_goal_value is None
|
||||||
|
or first_goal_value.lower() == "goal_choose_agent_type"
|
||||||
|
):
|
||||||
goal_categories.append("agent_selection")
|
goal_categories.append("agent_selection")
|
||||||
|
|
||||||
# always show goals labeled as "system," like the goal chooser
|
# always show goals labeled as "system," like the goal chooser
|
||||||
|
|||||||
@@ -1,64 +1,264 @@
|
|||||||
import os
|
import os
|
||||||
|
import random
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
PREMIER_LEAGUE_CLUBS_DATA = [
|
||||||
|
{"name": "Arsenal FC", "stadium": "Emirates Stadium"},
|
||||||
|
{"name": "Aston Villa FC", "stadium": "Villa Park"},
|
||||||
|
{"name": "AFC Bournemouth", "stadium": "Vitality Stadium"},
|
||||||
|
{"name": "Brentford FC", "stadium": "Gtech Community Stadium"},
|
||||||
|
{"name": "Brighton & Hove Albion FC", "stadium": "American Express Stadium"},
|
||||||
|
{"name": "Chelsea FC", "stadium": "Stamford Bridge"},
|
||||||
|
{"name": "Crystal Palace FC", "stadium": "Selhurst Park"},
|
||||||
|
{"name": "Everton FC", "stadium": "Goodison Park"},
|
||||||
|
{"name": "Fulham FC", "stadium": "Craven Cottage"},
|
||||||
|
{"name": "Ipswich Town FC", "stadium": "Portman Road"},
|
||||||
|
{"name": "Leicester City FC", "stadium": "King Power Stadium"},
|
||||||
|
{"name": "Liverpool FC", "stadium": "Anfield"},
|
||||||
|
{"name": "Manchester City FC", "stadium": "Etihad Stadium"},
|
||||||
|
{"name": "Manchester United FC", "stadium": "Old Trafford"},
|
||||||
|
{"name": "Newcastle United FC", "stadium": "St James' Park"},
|
||||||
|
{"name": "Nottingham Forest FC", "stadium": "City Ground"},
|
||||||
|
{"name": "Southampton FC", "stadium": "St Mary's Stadium"},
|
||||||
|
{"name": "Tottenham Hotspur FC", "stadium": "Tottenham Hotspur Stadium"},
|
||||||
|
{"name": "West Ham United FC", "stadium": "London Stadium"},
|
||||||
|
{"name": "Wolverhampton Wanderers FC", "stadium": "Molineux Stadium"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_future_matches(
|
||||||
|
team_name: str,
|
||||||
|
all_clubs_data: list,
|
||||||
|
num_matches: int = 12,
|
||||||
|
date_from: date = None,
|
||||||
|
date_to: date = None,
|
||||||
|
) -> list:
|
||||||
|
"""Generate a set of future Premier League matches for ``team_name``.
|
||||||
|
|
||||||
|
This is a purely mocked schedule. It returns up to ``num_matches``
|
||||||
|
fixtures, respecting the ``date_from`` and ``date_to`` constraints.
|
||||||
|
Matches are typically on Saturdays or Sundays.
|
||||||
|
"""
|
||||||
|
matches = []
|
||||||
|
|
||||||
|
team_details = next((c for c in all_clubs_data if c["name"] == team_name), None)
|
||||||
|
if not team_details:
|
||||||
|
return []
|
||||||
|
|
||||||
|
opponents_pool = [c for c in all_clubs_data if c["name"] != team_name]
|
||||||
|
if not opponents_pool:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Determine the maximum number of matches we can generate based on opponents
|
||||||
|
# and the requested num_matches
|
||||||
|
num_actual_matches_to_generate = min(num_matches, len(opponents_pool))
|
||||||
|
if num_actual_matches_to_generate == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Shuffle opponents once and pick them sequentially
|
||||||
|
random.shuffle(opponents_pool) # Shuffle in place
|
||||||
|
|
||||||
|
# Determine the initial Saturday for match week consideration
|
||||||
|
today_date = date.today()
|
||||||
|
# Default to next Saturday
|
||||||
|
current_match_week_saturday = today_date + timedelta(
|
||||||
|
days=(5 - today_date.weekday() + 7) % 7
|
||||||
|
)
|
||||||
|
|
||||||
|
# If today is Saturday and it's late evening, or if today is Sunday,
|
||||||
|
# advance to the following Saturday.
|
||||||
|
now_time = datetime.now().time()
|
||||||
|
if (
|
||||||
|
today_date.weekday() == 5
|
||||||
|
and now_time > datetime.strptime("20:00", "%H:%M").time()
|
||||||
|
) or (today_date.weekday() == 6):
|
||||||
|
current_match_week_saturday += timedelta(days=7)
|
||||||
|
|
||||||
|
# If date_from is specified, ensure our starting Saturday is not before it.
|
||||||
|
if date_from:
|
||||||
|
if current_match_week_saturday < date_from:
|
||||||
|
current_match_week_saturday = date_from
|
||||||
|
# Align current_match_week_saturday to be a Saturday on or after the potentially adjusted date
|
||||||
|
current_match_week_saturday += timedelta(
|
||||||
|
days=(5 - current_match_week_saturday.weekday() + 7) % 7
|
||||||
|
)
|
||||||
|
|
||||||
|
opponent_idx = 0
|
||||||
|
while len(matches) < num_actual_matches_to_generate and opponent_idx < len(
|
||||||
|
opponents_pool
|
||||||
|
):
|
||||||
|
# If the current week's Saturday is already past date_to, stop.
|
||||||
|
if date_to and current_match_week_saturday > date_to:
|
||||||
|
break
|
||||||
|
|
||||||
|
opponent_details = opponents_pool[opponent_idx]
|
||||||
|
is_saturday_game = random.choice([True, True, False])
|
||||||
|
actual_match_date = None
|
||||||
|
kick_off_time = ""
|
||||||
|
|
||||||
|
if is_saturday_game:
|
||||||
|
actual_match_date = current_match_week_saturday
|
||||||
|
kick_off_time = random.choice(["12:30", "15:00", "17:30"])
|
||||||
|
else: # Sunday game
|
||||||
|
actual_match_date = current_match_week_saturday + timedelta(days=1)
|
||||||
|
kick_off_time = random.choice(["14:00", "16:30"])
|
||||||
|
|
||||||
|
# Check if this specific match date is within the date_to constraint
|
||||||
|
if date_to and actual_match_date > date_to:
|
||||||
|
# If this game is too late, try the next week if possible.
|
||||||
|
# (This mainly affects Sunday games if Saturday was the last valid day)
|
||||||
|
current_match_week_saturday += timedelta(days=7)
|
||||||
|
continue # Skip adding this match, try next week.
|
||||||
|
|
||||||
|
match_datetime_gmt = (
|
||||||
|
f"{actual_match_date.strftime('%Y-%m-%d')} {kick_off_time} GMT"
|
||||||
|
)
|
||||||
|
is_home_match = random.choice([True, False])
|
||||||
|
|
||||||
|
if is_home_match:
|
||||||
|
team1_name = team_details["name"]
|
||||||
|
team2_name = opponent_details["name"]
|
||||||
|
stadium_name = team_details["stadium"]
|
||||||
|
else:
|
||||||
|
team1_name = opponent_details["name"]
|
||||||
|
team2_name = team_details["name"]
|
||||||
|
stadium_name = opponent_details["stadium"]
|
||||||
|
|
||||||
|
matches.append(
|
||||||
|
{
|
||||||
|
"team1": team1_name,
|
||||||
|
"team2": team2_name,
|
||||||
|
"stadium": stadium_name,
|
||||||
|
"datetime_gmt": match_datetime_gmt,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
opponent_idx += 1
|
||||||
|
current_match_week_saturday += timedelta(
|
||||||
|
days=7
|
||||||
|
) # Advance to next week's Saturday
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
BASE_URL = "https://api.football-data.org/v4"
|
BASE_URL = "https://api.football-data.org/v4"
|
||||||
|
|
||||||
|
|
||||||
def search_fixtures(args: dict) -> dict:
|
def search_fixtures(args: dict) -> dict:
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
api_key = os.getenv("FOOTBALL_DATA_API_KEY", "YOUR_DEFAULT_KEY")
|
api_key = os.getenv("FOOTBALL_DATA_API_KEY")
|
||||||
|
|
||||||
team_name = args.get("team")
|
team_name = args.get("team")
|
||||||
date_from_str = args.get("date_from")
|
date_from_str = args.get("date_from")
|
||||||
date_to_str = args.get("date_to")
|
date_to_str = args.get("date_to")
|
||||||
headers = {"X-Auth-Token": api_key}
|
|
||||||
team_name = team_name.lower()
|
|
||||||
|
|
||||||
|
if not team_name:
|
||||||
|
return {"error": "Team name is required."}
|
||||||
|
|
||||||
|
parsed_date_from = None
|
||||||
|
if date_from_str:
|
||||||
try:
|
try:
|
||||||
date_from = datetime.strptime(date_from_str, "%Y-%m-%d")
|
parsed_date_from = datetime.strptime(date_from_str, "%Y-%m-%d").date()
|
||||||
date_to = datetime.strptime(date_to_str, "%Y-%m-%d")
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return {
|
return {
|
||||||
"error": "Invalid date provided. Expected format YYYY-MM-DD for both date_from and date_to."
|
"error": f"Invalid date_from: '{date_from_str}'. Expected format YYYY-MM-DD."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsed_date_to = None
|
||||||
|
if date_to_str:
|
||||||
|
try:
|
||||||
|
parsed_date_to = datetime.strptime(date_to_str, "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
return {
|
||||||
|
"error": f"Invalid date_to: '{date_to_str}'. Expected format YYYY-MM-DD."
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed_date_from and parsed_date_to and parsed_date_from > parsed_date_to:
|
||||||
|
return {"error": "date_from cannot be after date_to."}
|
||||||
|
|
||||||
|
# If no API key, fall back to mocked data
|
||||||
|
if not api_key:
|
||||||
|
# Use the parsed date objects (which can be None)
|
||||||
|
fixtures = get_future_matches(
|
||||||
|
team_name,
|
||||||
|
PREMIER_LEAGUE_CLUBS_DATA,
|
||||||
|
date_from=parsed_date_from,
|
||||||
|
date_to=parsed_date_to,
|
||||||
|
# num_matches can be passed explicitly if needed, otherwise defaults to 12
|
||||||
|
)
|
||||||
|
if not fixtures:
|
||||||
|
# Check if the team name itself was invalid, as get_future_matches returns [] for that too
|
||||||
|
team_details_check = next(
|
||||||
|
(c for c in PREMIER_LEAGUE_CLUBS_DATA if c["name"] == team_name), None
|
||||||
|
)
|
||||||
|
if not team_details_check:
|
||||||
|
return {"error": f"Team '{team_name}' not found in mocked data."}
|
||||||
|
# If team is valid, an empty fixtures list means no matches fit the criteria (e.g., date range)
|
||||||
|
return {"fixtures": fixtures}
|
||||||
|
|
||||||
|
# API Key is present, proceed with API logic
|
||||||
|
# The API requires both date_from and date_to
|
||||||
|
if not parsed_date_from or not parsed_date_to:
|
||||||
|
return {
|
||||||
|
"error": "Both date_from and date_to (YYYY-MM-DD) are required for API search."
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {"X-Auth-Token": api_key}
|
||||||
|
# For API calls, team name matching might be case-insensitive or require specific handling
|
||||||
|
# The existing logic uses team_name.lower() for the API search path later.
|
||||||
|
|
||||||
# Fetch team ID
|
# Fetch team ID
|
||||||
teams_response = requests.get(f"{BASE_URL}/competitions/PL/teams", headers=headers)
|
teams_response = requests.get(f"{BASE_URL}/competitions/PL/teams", headers=headers)
|
||||||
if teams_response.status_code != 200:
|
if teams_response.status_code != 200:
|
||||||
return {"error": "Failed to fetch teams data."}
|
return {
|
||||||
|
"error": f"Failed to fetch teams data from API (status {teams_response.status_code})."
|
||||||
|
}
|
||||||
|
|
||||||
teams_data = teams_response.json()
|
teams_data = teams_response.json()
|
||||||
team_id = None
|
team_id = None
|
||||||
for team in teams_data["teams"]:
|
# Using lower() for comparison, assuming API team names might have varied casing
|
||||||
if team_name in team["name"].lower():
|
# or the input team_name might not be exact.
|
||||||
team_id = team["id"]
|
# The `ToolDefinition` lists exact names, so direct match might also be an option.
|
||||||
|
for team_api_data in teams_data.get("teams", []):
|
||||||
|
if team_name.lower() in team_api_data.get("name", "").lower():
|
||||||
|
team_id = team_api_data["id"]
|
||||||
break
|
break
|
||||||
|
|
||||||
if not team_id:
|
if not team_id:
|
||||||
return {"error": "Team not found."}
|
return {"error": f"Team '{team_name}' not found via API."}
|
||||||
|
|
||||||
date_from_formatted = date_from.strftime("%Y-%m-%d")
|
date_from_formatted = parsed_date_from.strftime("%Y-%m-%d")
|
||||||
date_to_formatted = date_to.strftime("%Y-%m-%d")
|
date_to_formatted = parsed_date_to.strftime("%Y-%m-%d")
|
||||||
fixtures_url = f"{BASE_URL}/teams/{team_id}/matches?dateFrom={date_from_formatted}&dateTo={date_to_formatted}"
|
fixtures_url = f"{BASE_URL}/teams/{team_id}/matches?dateFrom={date_from_formatted}&dateTo={date_to_formatted}"
|
||||||
print(fixtures_url)
|
# print(fixtures_url) # Keep for debugging if necessary
|
||||||
|
|
||||||
fixtures_response = requests.get(fixtures_url, headers=headers)
|
fixtures_response = requests.get(fixtures_url, headers=headers)
|
||||||
if fixtures_response.status_code != 200:
|
if fixtures_response.status_code != 200:
|
||||||
return {"error": "Failed to fetch fixtures data."}
|
return {
|
||||||
|
"error": f"Failed to fetch fixtures data from API (status {fixtures_response.status_code})."
|
||||||
|
}
|
||||||
|
|
||||||
fixtures_data = fixtures_response.json()
|
fixtures_data = fixtures_response.json()
|
||||||
matching_fixtures = []
|
matching_fixtures = []
|
||||||
|
|
||||||
for match in fixtures_data.get("matches", []):
|
for match in fixtures_data.get("matches", []):
|
||||||
match_datetime = datetime.strptime(match["utcDate"], "%Y-%m-%dT%H:%M:%SZ")
|
# Ensure match datetime parsing is robust
|
||||||
if match["competition"]["code"] == "PL":
|
try:
|
||||||
|
match_datetime_utc = datetime.strptime(
|
||||||
|
match["utcDate"], "%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# Skip malformed match entries or log an error
|
||||||
|
continue
|
||||||
|
|
||||||
|
if match.get("competition", {}).get("code") == "PL":
|
||||||
matching_fixtures.append(
|
matching_fixtures.append(
|
||||||
{
|
{
|
||||||
"date": match_datetime.strftime("%Y-%m-%d"),
|
"date": match_datetime_utc.strftime("%Y-%m-%d"),
|
||||||
"homeTeam": match["homeTeam"]["name"],
|
"homeTeam": match.get("homeTeam", {}).get("name", "N/A"),
|
||||||
"awayTeam": match["awayTeam"]["name"],
|
"awayTeam": match.get("awayTeam", {}).get("name", "N/A"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,34 +282,69 @@ def search_fixtures_example(args: dict) -> dict:
|
|||||||
|
|
||||||
# Validate dates
|
# Validate dates
|
||||||
try:
|
try:
|
||||||
date_from = datetime.strptime(date_from_str, "%Y-%m-%d")
|
# Ensure date strings are not None before parsing
|
||||||
date_to = datetime.strptime(date_to_str, "%Y-%m-%d")
|
if date_from_str is None or date_to_str is None:
|
||||||
|
raise ValueError("Date strings cannot be None")
|
||||||
|
date_from_obj = datetime.strptime(date_from_str, "%Y-%m-%d")
|
||||||
|
date_to_obj = datetime.strptime(date_to_str, "%Y-%m-%d")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return {
|
return {
|
||||||
"error": "Invalid date provided. Expected format YYYY-MM-DD for both date_from and date_to."
|
"error": "Invalid date provided. Expected format YYYY-MM-DD for both date_from and date_to."
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate 3 reasonable fixture dates within the given range
|
# Calculate 3 reasonable fixture dates within the given range
|
||||||
date_range = (date_to - date_from).days
|
date_range = (date_to_obj - date_from_obj).days
|
||||||
|
if date_range < 0: # date_from is after date_to
|
||||||
|
return {"fixtures": []} # No fixtures possible
|
||||||
|
|
||||||
|
fixture_dates_timestamps = []
|
||||||
if date_range < 21:
|
if date_range < 21:
|
||||||
# If range is less than 3 weeks, use evenly spaced fixtures
|
# If range is less than 3 weeks, use evenly spaced fixtures if possible
|
||||||
fixture_dates = [
|
if date_range >= 2: # Need at least some gap for 3 fixtures
|
||||||
date_from + timedelta(days=max(1, date_range // 3)),
|
fixture_dates_timestamps = [
|
||||||
date_from + timedelta(days=max(2, date_range * 2 // 3)),
|
date_from_obj
|
||||||
date_to - timedelta(days=min(2, date_range // 4)),
|
+ timedelta(days=max(0, date_range // 4)), # Closer to start
|
||||||
|
date_from_obj + timedelta(days=max(1, date_range // 2)), # Middle
|
||||||
|
date_to_obj - timedelta(days=max(0, date_range // 4)), # Closer to end
|
||||||
]
|
]
|
||||||
|
elif date_range == 1: # Only two days
|
||||||
|
fixture_dates_timestamps = [date_from_obj, date_to_obj]
|
||||||
|
elif date_range == 0: # Only one day
|
||||||
|
fixture_dates_timestamps = [date_from_obj]
|
||||||
|
else: # date_range is negative, handled above, or 0 (single day)
|
||||||
|
fixture_dates_timestamps = [date_from_obj] if date_range == 0 else []
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Otherwise space them out by weeks
|
# Otherwise space them out by weeks, ensuring they are within the bounds
|
||||||
fixture_dates = [
|
d1 = date_from_obj + timedelta(days=7)
|
||||||
date_from + timedelta(days=7),
|
d2 = date_from_obj + timedelta(days=14)
|
||||||
date_from + timedelta(days=14),
|
d3 = date_to_obj - timedelta(days=7) # Potential third game from the end
|
||||||
date_to - timedelta(days=7),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Ensure we only have 3 dates
|
fixture_dates_timestamps.append(d1)
|
||||||
fixture_dates = fixture_dates[:3]
|
if d2 <= date_to_obj and d2 > d1: # ensure d2 is valid and distinct
|
||||||
|
fixture_dates_timestamps.append(d2)
|
||||||
|
if (
|
||||||
|
d3 >= date_from_obj and d3 > d2 and d3 <= date_to_obj
|
||||||
|
): # ensure d3 is valid and distinct
|
||||||
|
fixture_dates_timestamps.append(d3)
|
||||||
|
elif (
|
||||||
|
d3 < date_from_obj and len(fixture_dates_timestamps) < 3
|
||||||
|
): # if d3 is too early, try using date_to_obj itself if distinct
|
||||||
|
if date_to_obj not in fixture_dates_timestamps:
|
||||||
|
fixture_dates_timestamps.append(date_to_obj)
|
||||||
|
|
||||||
|
# Ensure unique dates and sort, then take up to 3.
|
||||||
|
fixture_dates_timestamps = sorted(
|
||||||
|
list(
|
||||||
|
set(
|
||||||
|
f_date
|
||||||
|
for f_date in fixture_dates_timestamps
|
||||||
|
if date_from_obj <= f_date <= date_to_obj
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fixture_dates_final = fixture_dates_timestamps[:3]
|
||||||
|
|
||||||
# Expanded pool of opponent teams to avoid team playing against itself
|
|
||||||
all_opponents = [
|
all_opponents = [
|
||||||
"Manchester United FC",
|
"Manchester United FC",
|
||||||
"Leicester City FC",
|
"Leicester City FC",
|
||||||
@@ -120,35 +355,35 @@ def search_fixtures_example(args: dict) -> dict:
|
|||||||
"Tottenham Hotspur FC",
|
"Tottenham Hotspur FC",
|
||||||
"West Ham United FC",
|
"West Ham United FC",
|
||||||
"Everton FC",
|
"Everton FC",
|
||||||
|
"Generic Opponent A",
|
||||||
|
"Generic Opponent B",
|
||||||
|
"Generic Opponent C", # Fallbacks
|
||||||
]
|
]
|
||||||
|
|
||||||
# Select opponents that aren't the same as the requested team
|
|
||||||
available_opponents = [
|
available_opponents = [
|
||||||
team for team in all_opponents if team.lower() != team_name.lower()
|
team for team in all_opponents if team.lower() != team_name.lower()
|
||||||
]
|
]
|
||||||
|
|
||||||
# Ensure we have at least 3 opponents
|
# Ensure we have enough opponents for the number of fixtures we'll generate
|
||||||
if len(available_opponents) < 3:
|
if len(available_opponents) < len(fixture_dates_final):
|
||||||
# Add generic opponents if needed
|
needed = len(fixture_dates_final) - len(available_opponents)
|
||||||
additional_teams = [f"Opponent {i} FC" for i in range(1, 4)]
|
for i in range(needed):
|
||||||
available_opponents.extend(additional_teams)
|
available_opponents.append(f"Placeholder Opponent {i+1}")
|
||||||
|
|
||||||
# Take only the first 3 opponents
|
opponents = available_opponents[: len(fixture_dates_final)]
|
||||||
opponents = available_opponents[:3]
|
|
||||||
|
|
||||||
# Generate fixtures - always exactly 3
|
|
||||||
fixtures = []
|
fixtures = []
|
||||||
for i, fixture_date in enumerate(fixture_dates):
|
for i, fixture_date_obj in enumerate(fixture_dates_final):
|
||||||
date_str = fixture_date.strftime("%Y-%m-%d")
|
if i >= len(opponents): # Should not happen with the logic above
|
||||||
|
break
|
||||||
# Alternate between home and away games
|
date_str = fixture_date_obj.strftime("%Y-%m-%d")
|
||||||
if i % 2 == 0:
|
if i % 2 == 0: # Home game
|
||||||
fixtures.append(
|
|
||||||
{"date": date_str, "homeTeam": opponents[i], "awayTeam": team_name}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
fixtures.append(
|
fixtures.append(
|
||||||
{"date": date_str, "homeTeam": team_name, "awayTeam": opponents[i]}
|
{"date": date_str, "homeTeam": team_name, "awayTeam": opponents[i]}
|
||||||
)
|
)
|
||||||
|
else: # Away game
|
||||||
|
fixtures.append(
|
||||||
|
{"date": date_str, "homeTeam": opponents[i], "awayTeam": team_name}
|
||||||
|
)
|
||||||
|
|
||||||
return {"fixtures": fixtures}
|
return {"fixtures": fixtures}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
import http.client
|
import http.client
|
||||||
from dotenv import load_dotenv
|
import json
|
||||||
|
import os
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
def search_airport(query: str) -> list:
|
def search_airport(query: str) -> list:
|
||||||
"""
|
"""
|
||||||
@@ -11,7 +12,7 @@ def search_airport(query: str) -> list:
|
|||||||
"""
|
"""
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
api_key = os.getenv("RAPIDAPI_KEY", "YOUR_DEFAULT_KEY")
|
api_key = os.getenv("RAPIDAPI_KEY", "YOUR_DEFAULT_KEY")
|
||||||
api_host = os.getenv("RAPIDAPI_HOST", "sky-scrapper.p.rapidapi.com")
|
api_host = os.getenv("RAPIDAPI_HOST_FLIGHTS", "sky-scrapper.p.rapidapi.com")
|
||||||
|
|
||||||
conn = http.client.HTTPSConnection(api_host)
|
conn = http.client.HTTPSConnection(api_host)
|
||||||
headers = {
|
headers = {
|
||||||
@@ -73,7 +74,7 @@ def search_flights_real_api(
|
|||||||
# Step 2: Call flight search with resolved codes
|
# Step 2: Call flight search with resolved codes
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
api_key = os.getenv("RAPIDAPI_KEY", "YOUR_DEFAULT_KEY")
|
api_key = os.getenv("RAPIDAPI_KEY", "YOUR_DEFAULT_KEY")
|
||||||
api_host = os.getenv("RAPIDAPI_HOST", "sky-scrapper.p.rapidapi.com")
|
api_host = os.getenv("RAPIDAPI_HOST_FLIGHTS", "sky-scrapper.p.rapidapi.com")
|
||||||
|
|
||||||
conn = http.client.HTTPSConnection(api_host)
|
conn = http.client.HTTPSConnection(api_host)
|
||||||
headers = {
|
headers = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from models.tool_definitions import ToolDefinition, ToolArgument
|
from models.tool_definitions import ToolArgument, ToolDefinition
|
||||||
|
|
||||||
# ----- System tools -----
|
# ----- System tools -----
|
||||||
list_agents_tool = ToolDefinition(
|
list_agents_tool = ToolDefinition(
|
||||||
name="ListAgents",
|
name="ListAgents",
|
||||||
@@ -26,7 +27,8 @@ give_hint_tool = ToolDefinition(
|
|||||||
name="hint_total",
|
name="hint_total",
|
||||||
type="number",
|
type="number",
|
||||||
description="How many hints have been given",
|
description="How many hints have been given",
|
||||||
),],
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
guess_location_tool = ToolDefinition(
|
guess_location_tool = ToolDefinition(
|
||||||
@@ -54,7 +56,8 @@ guess_location_tool = ToolDefinition(
|
|||||||
# ----- Travel use cases tools -----
|
# ----- Travel use cases tools -----
|
||||||
search_flights_tool = ToolDefinition(
|
search_flights_tool = ToolDefinition(
|
||||||
name="SearchFlights",
|
name="SearchFlights",
|
||||||
description="Search for return flights from an origin to a destination within a date range (dateDepart, dateReturn).",
|
description="Search for return flights from an origin to a destination within a date range (dateDepart, dateReturn). "
|
||||||
|
"You are allowed to suggest dates from the conversation history, but ALWAYS ask the user if ok.",
|
||||||
arguments=[
|
arguments=[
|
||||||
ToolArgument(
|
ToolArgument(
|
||||||
name="origin",
|
name="origin",
|
||||||
@@ -76,12 +79,18 @@ search_flights_tool = ToolDefinition(
|
|||||||
type="ISO8601",
|
type="ISO8601",
|
||||||
description="End of date range in human readable format, when you want to return",
|
description="End of date range in human readable format, when you want to return",
|
||||||
),
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="userConfirmation",
|
||||||
|
type="string",
|
||||||
|
description="Indication of the user's desire to search flights, and to confirm the details "
|
||||||
|
+ "before moving on to the next step",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
search_trains_tool = ToolDefinition(
|
search_trains_tool = ToolDefinition(
|
||||||
name="SearchTrains",
|
name="SearchTrains",
|
||||||
description="Search for trains between two English cities. Returns a list of train information for the user to choose from.",
|
description="Search for trains between two English cities. Returns a list of train information for the user to choose from. Present the list to the user.",
|
||||||
arguments=[
|
arguments=[
|
||||||
ToolArgument(
|
ToolArgument(
|
||||||
name="origin",
|
name="origin",
|
||||||
@@ -115,6 +124,11 @@ book_trains_tool = ToolDefinition(
|
|||||||
type="string",
|
type="string",
|
||||||
description="The IDs of the trains to book, comma separated",
|
description="The IDs of the trains to book, comma separated",
|
||||||
),
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="userConfirmation",
|
||||||
|
type="string",
|
||||||
|
description="Indication of user's desire to book train tickets",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,12 +146,17 @@ create_invoice_tool = ToolDefinition(
|
|||||||
type="string",
|
type="string",
|
||||||
description="A description of the item details to be invoiced, inferred from the conversation history.",
|
description="A description of the item details to be invoiced, inferred from the conversation history.",
|
||||||
),
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="userConfirmation",
|
||||||
|
type="string",
|
||||||
|
description="Indication of user's desire to create an invoice",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
search_fixtures_tool = ToolDefinition(
|
search_fixtures_tool = ToolDefinition(
|
||||||
name="SearchFixtures",
|
name="SearchFixtures",
|
||||||
description="Search for upcoming fixtures for a given team within a date range inferred from the user's description. Valid teams this 24/25 season are Arsenal FC, Aston Villa FC, AFC Bournemouth, Brentford FC, Brighton & Hove Albion FC, Chelsea FC, Crystal Palace FC, Everton FC, Fulham FC, Ipswich Town FC, Leicester City FC, Liverpool FC, Manchester City FC, Manchester United FC, Newcastle United FC, Nottingham Forest FC, Southampton FC, Tottenham Hotspur FC, West Ham United FC, Wolverhampton Wanderers FC",
|
description="Search for upcoming fixtures for a given team within a date range inferred from the user's description. Ignore valid premier league dates. Valid teams this season are Arsenal FC, Aston Villa FC, AFC Bournemouth, Brentford FC, Brighton & Hove Albion FC, Chelsea FC, Crystal Palace FC, Everton FC, Fulham FC, Ipswich Town FC, Leicester City FC, Liverpool FC, Manchester City FC, Manchester United FC, Newcastle United FC, Nottingham Forest FC, Southampton FC, Tottenham Hotspur FC, West Ham United FC, Wolverhampton Wanderers FC",
|
||||||
arguments=[
|
arguments=[
|
||||||
ToolArgument(
|
ToolArgument(
|
||||||
name="team",
|
name="team",
|
||||||
@@ -278,10 +297,9 @@ financial_get_account_balances = ToolDefinition(
|
|||||||
name="FinCheckAccountBalance",
|
name="FinCheckAccountBalance",
|
||||||
description="Get account balance for your accounts. "
|
description="Get account balance for your accounts. "
|
||||||
"Returns the account balances of your accounts. ",
|
"Returns the account balances of your accounts. ",
|
||||||
|
|
||||||
arguments=[
|
arguments=[
|
||||||
ToolArgument(
|
ToolArgument(
|
||||||
name="accountkey",
|
name="email_address_or_account_ID",
|
||||||
type="string",
|
type="string",
|
||||||
description="email address or account ID of user",
|
description="email address or account ID of user",
|
||||||
),
|
),
|
||||||
@@ -289,15 +307,14 @@ financial_get_account_balances = ToolDefinition(
|
|||||||
)
|
)
|
||||||
|
|
||||||
financial_move_money = ToolDefinition(
|
financial_move_money = ToolDefinition(
|
||||||
name="FinMoveMoneyOrder",
|
name="FinMoveMoney",
|
||||||
description="Execute a money movement order. "
|
description="Send money from one account to another under the same acount ID (e.g. checking to savings). "
|
||||||
"Returns the status of the order and the account balance of the account money was moved from. ",
|
"Returns the status of the order and the new balances in each account. ",
|
||||||
|
|
||||||
arguments=[
|
arguments=[
|
||||||
ToolArgument(
|
ToolArgument(
|
||||||
name="accountkey",
|
name="email_address_or_account_ID",
|
||||||
type="string",
|
type="string",
|
||||||
description="email address or account ID of user",
|
description="email address or account ID of user (you will need both to find the account)",
|
||||||
),
|
),
|
||||||
ToolArgument(
|
ToolArgument(
|
||||||
name="accounttype",
|
name="accounttype",
|
||||||
@@ -307,25 +324,27 @@ financial_move_money = ToolDefinition(
|
|||||||
ToolArgument(
|
ToolArgument(
|
||||||
name="amount",
|
name="amount",
|
||||||
type="string",
|
type="string",
|
||||||
description="amount to move in the order",
|
description="amount to move in the order (e.g. checking or savings)",
|
||||||
),
|
),
|
||||||
|
|
||||||
ToolArgument(
|
ToolArgument(
|
||||||
name="destinationaccount",
|
name="destinationaccount",
|
||||||
type="string",
|
type="string",
|
||||||
description="account number to move the money to",
|
description="account to move the money to (e.g. checking or savings)",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="userConfirmation",
|
||||||
|
type="string",
|
||||||
|
description="Indication of user's desire to move money",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
financial_submit_loan_approval = ToolDefinition(
|
financial_submit_loan_approval = ToolDefinition(
|
||||||
name="FinCheckAccountSubmitLoanApproval",
|
name="FinCheckAccountSubmitLoanApproval",
|
||||||
description="Submit a loan application. "
|
description="Submit a loan application. " "Returns the loan status. ",
|
||||||
"Returns the loan status. ",
|
|
||||||
|
|
||||||
arguments=[
|
arguments=[
|
||||||
ToolArgument(
|
ToolArgument(
|
||||||
name="accountkey",
|
name="email_address_or_account_ID",
|
||||||
type="string",
|
type="string",
|
||||||
description="email address or account ID of user",
|
description="email address or account ID of user",
|
||||||
),
|
),
|
||||||
@@ -336,3 +355,45 @@ financial_submit_loan_approval = ToolDefinition(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ----- ECommerce Use Case Tools -----
|
||||||
|
ecomm_list_orders = ToolDefinition(
|
||||||
|
name="ListOrders",
|
||||||
|
description="Get all orders for a certain email address.",
|
||||||
|
arguments=[
|
||||||
|
ToolArgument(
|
||||||
|
name="email_address",
|
||||||
|
type="string",
|
||||||
|
description="Email address of user by which to find orders",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ecomm_get_order = ToolDefinition(
|
||||||
|
name="GetOrder",
|
||||||
|
description="Get infromation about an order by order ID.",
|
||||||
|
arguments=[
|
||||||
|
ToolArgument(
|
||||||
|
name="order_id",
|
||||||
|
type="string",
|
||||||
|
description="ID of order to determine status of",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ecomm_track_package = ToolDefinition(
|
||||||
|
name="TrackPackage",
|
||||||
|
description="Get tracking information for a package by shipping provider and tracking ID",
|
||||||
|
arguments=[
|
||||||
|
ToolArgument(
|
||||||
|
name="tracking_id",
|
||||||
|
type="string",
|
||||||
|
description="ID of package to track",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="userConfirmation",
|
||||||
|
type="string",
|
||||||
|
description="Indication of user's desire to get package tracking information",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import shared.config
|
import shared.config
|
||||||
|
|
||||||
def transfer_control(args: dict) -> dict:
|
|
||||||
|
|
||||||
|
def transfer_control(args: dict) -> dict:
|
||||||
return {
|
return {
|
||||||
"new_goal": shared.config.AGENT_GOAL,
|
"new_goal": shared.config.AGENT_GOAL,
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,35 @@
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Dict, Any, Union, List, Optional, Deque, TypedDict
|
from typing import Any, Deque, Dict, List, Optional, TypedDict, Union
|
||||||
|
|
||||||
from temporalio.common import RetryPolicy
|
|
||||||
from temporalio import workflow
|
from temporalio import workflow
|
||||||
|
from temporalio.common import RetryPolicy
|
||||||
|
|
||||||
from models.data_types import ConversationHistory, EnvLookupOutput, NextStep, ValidationInput, EnvLookupInput
|
from models.data_types import (
|
||||||
|
ConversationHistory,
|
||||||
|
EnvLookupInput,
|
||||||
|
EnvLookupOutput,
|
||||||
|
NextStep,
|
||||||
|
ValidationInput,
|
||||||
|
)
|
||||||
from models.tool_definitions import AgentGoal
|
from models.tool_definitions import AgentGoal
|
||||||
from workflows.workflow_helpers import LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT, \
|
|
||||||
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT
|
|
||||||
from workflows import workflow_helpers as helpers
|
from workflows import workflow_helpers as helpers
|
||||||
|
from workflows.workflow_helpers import (
|
||||||
|
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||||
|
LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
with workflow.unsafe.imports_passed_through():
|
with workflow.unsafe.imports_passed_through():
|
||||||
from activities.tool_activities import ToolActivities
|
from activities.tool_activities import ToolActivities
|
||||||
from prompts.agent_prompt_generators import (
|
from models.data_types import CombinedInput, ToolPromptInput
|
||||||
generate_genai_prompt
|
from prompts.agent_prompt_generators import generate_genai_prompt
|
||||||
)
|
|
||||||
from models.data_types import (
|
|
||||||
CombinedInput,
|
|
||||||
ToolPromptInput,
|
|
||||||
)
|
|
||||||
from tools.goal_registry import goal_list
|
from tools.goal_registry import goal_list
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
MAX_TURNS_BEFORE_CONTINUE = 250
|
MAX_TURNS_BEFORE_CONTINUE = 250
|
||||||
|
|
||||||
#ToolData as part of the workflow is what's accessible to the UI - see LLMResponse.jsx for example
|
|
||||||
|
# ToolData as part of the workflow is what's accessible to the UI - see LLMResponse.jsx for example
|
||||||
class ToolData(TypedDict, total=False):
|
class ToolData(TypedDict, total=False):
|
||||||
next: NextStep
|
next: NextStep
|
||||||
tool: str
|
tool: str
|
||||||
@@ -33,6 +37,7 @@ class ToolData(TypedDict, total=False):
|
|||||||
response: str
|
response: str
|
||||||
force_confirm: bool = True
|
force_confirm: bool = True
|
||||||
|
|
||||||
|
|
||||||
@workflow.defn
|
@workflow.defn
|
||||||
class AgentGoalWorkflow:
|
class AgentGoalWorkflow:
|
||||||
"""Workflow that manages tool execution with user confirmation and conversation history."""
|
"""Workflow that manages tool execution with user confirmation and conversation history."""
|
||||||
@@ -43,16 +48,21 @@ class AgentGoalWorkflow:
|
|||||||
self.conversation_summary: Optional[str] = None
|
self.conversation_summary: Optional[str] = None
|
||||||
self.chat_ended: bool = False
|
self.chat_ended: bool = False
|
||||||
self.tool_data: Optional[ToolData] = None
|
self.tool_data: Optional[ToolData] = None
|
||||||
self.confirmed: bool = False # indicates that we have confirmation to proceed to run tool
|
self.confirmed: bool = (
|
||||||
|
False # indicates that we have confirmation to proceed to run tool
|
||||||
|
)
|
||||||
self.tool_results: List[Dict[str, Any]] = []
|
self.tool_results: List[Dict[str, Any]] = []
|
||||||
self.goal: AgentGoal = {"tools": []}
|
self.goal: AgentGoal = {"tools": []}
|
||||||
self.show_tool_args_confirmation: bool = True # set from env file in activity lookup_wf_env_settings
|
self.show_tool_args_confirmation: bool = (
|
||||||
self.multi_goal_mode: bool = False # set from env file in activity lookup_wf_env_settings
|
True # set from env file in activity lookup_wf_env_settings
|
||||||
|
)
|
||||||
|
self.multi_goal_mode: bool = (
|
||||||
|
False # set from env file in activity lookup_wf_env_settings
|
||||||
|
)
|
||||||
|
|
||||||
# see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set
|
# see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set
|
||||||
@workflow.run
|
@workflow.run
|
||||||
async def run(self, combined_input: CombinedInput) -> str:
|
async def run(self, combined_input: CombinedInput) -> str:
|
||||||
|
|
||||||
"""Main workflow execution method."""
|
"""Main workflow execution method."""
|
||||||
# setup phase, starts with blank tool_params and agent_goal prompt as defined in tools/goal_registry.py
|
# setup phase, starts with blank tool_params and agent_goal prompt as defined in tools/goal_registry.py
|
||||||
params = combined_input.tool_params
|
params = combined_input.tool_params
|
||||||
@@ -96,7 +106,9 @@ class AgentGoalWorkflow:
|
|||||||
if self.prompt_queue:
|
if self.prompt_queue:
|
||||||
# get most recent prompt
|
# get most recent prompt
|
||||||
prompt = self.prompt_queue.popleft()
|
prompt = self.prompt_queue.popleft()
|
||||||
workflow.logger.info(f"workflow step: processing message on the prompt queue, message is {prompt}")
|
workflow.logger.info(
|
||||||
|
f"workflow step: processing message on the prompt queue, message is {prompt}"
|
||||||
|
)
|
||||||
|
|
||||||
# Validate user-provided prompts
|
# Validate user-provided prompts
|
||||||
if self.is_user_prompt(prompt):
|
if self.is_user_prompt(prompt):
|
||||||
@@ -108,7 +120,7 @@ class AgentGoalWorkflow:
|
|||||||
conversation_history=self.conversation_history,
|
conversation_history=self.conversation_history,
|
||||||
agent_goal=self.goal,
|
agent_goal=self.goal,
|
||||||
)
|
)
|
||||||
validation_result = await workflow.execute_activity(
|
validation_result = await workflow.execute_activity_method(
|
||||||
ToolActivities.agent_validatePrompt,
|
ToolActivities.agent_validatePrompt,
|
||||||
args=[validation_input],
|
args=[validation_input],
|
||||||
schedule_to_close_timeout=LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
schedule_to_close_timeout=LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||||
@@ -120,21 +132,28 @@ class AgentGoalWorkflow:
|
|||||||
|
|
||||||
# If validation fails, provide that feedback to the user - i.e., "your words make no sense, puny human" end this iteration of processing
|
# If validation fails, provide that feedback to the user - i.e., "your words make no sense, puny human" end this iteration of processing
|
||||||
if not validation_result.validationResult:
|
if not validation_result.validationResult:
|
||||||
workflow.logger.warning(f"Prompt validation failed: {validation_result.validationFailedReason}")
|
workflow.logger.warning(
|
||||||
self.add_message("agent", validation_result.validationFailedReason)
|
f"Prompt validation failed: {validation_result.validationFailedReason}"
|
||||||
|
)
|
||||||
|
self.add_message(
|
||||||
|
"agent", validation_result.validationFailedReason
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If valid, proceed with generating the context and prompt
|
# If valid, proceed with generating the context and prompt
|
||||||
context_instructions = generate_genai_prompt(
|
context_instructions = generate_genai_prompt(
|
||||||
agent_goal=self.goal,
|
agent_goal=self.goal,
|
||||||
conversation_history = self.conversation_history,
|
conversation_history=self.conversation_history,
|
||||||
multi_goal_mode=self.multi_goal_mode,
|
multi_goal_mode=self.multi_goal_mode,
|
||||||
raw_json=self.tool_data)
|
raw_json=self.tool_data,
|
||||||
|
)
|
||||||
|
|
||||||
prompt_input = ToolPromptInput(prompt=prompt, context_instructions=context_instructions)
|
prompt_input = ToolPromptInput(
|
||||||
|
prompt=prompt, context_instructions=context_instructions
|
||||||
|
)
|
||||||
|
|
||||||
# connect to LLM and execute to get next steps
|
# connect to LLM and execute to get next steps
|
||||||
tool_data = await workflow.execute_activity(
|
tool_data = await workflow.execute_activity_method(
|
||||||
ToolActivities.agent_toolPlanner,
|
ToolActivities.agent_toolPlanner,
|
||||||
prompt_input,
|
prompt_input,
|
||||||
schedule_to_close_timeout=LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
schedule_to_close_timeout=LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||||
@@ -151,13 +170,17 @@ class AgentGoalWorkflow:
|
|||||||
next_step = tool_data.get("next")
|
next_step = tool_data.get("next")
|
||||||
current_tool = tool_data.get("tool")
|
current_tool = tool_data.get("tool")
|
||||||
|
|
||||||
workflow.logger.info(f"next_step: {next_step}, current tool is {current_tool}")
|
workflow.logger.info(
|
||||||
|
f"next_step: {next_step}, current tool is {current_tool}"
|
||||||
|
)
|
||||||
|
|
||||||
# make sure we're ready to run the tool & have everything we need
|
# make sure we're ready to run the tool & have everything we need
|
||||||
if next_step == "confirm" and current_tool:
|
if next_step == "confirm" and current_tool:
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
# if we're missing arguments, ask for them
|
# if we're missing arguments, ask for them
|
||||||
if await helpers.handle_missing_args(current_tool, args, tool_data, self.prompt_queue):
|
if await helpers.handle_missing_args(
|
||||||
|
current_tool, args, tool_data, self.prompt_queue
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
waiting_for_confirm = True
|
waiting_for_confirm = True
|
||||||
@@ -169,22 +192,16 @@ class AgentGoalWorkflow:
|
|||||||
# if we have all needed arguments (handled above) and not holding for a debugging confirm, proceed:
|
# if we have all needed arguments (handled above) and not holding for a debugging confirm, proceed:
|
||||||
else:
|
else:
|
||||||
self.confirmed = True
|
self.confirmed = True
|
||||||
|
# else if the next step is to pick a new goal, set that to be the goal
|
||||||
# else if the next step is to pick a new goal, set the goal and tool to do it
|
|
||||||
elif next_step == "pick-new-goal":
|
elif next_step == "pick-new-goal":
|
||||||
workflow.logger.info("All steps completed. Resetting goal.")
|
workflow.logger.info("All steps completed. Resetting goal.")
|
||||||
self.change_goal("goal_choose_agent_type")
|
self.change_goal("goal_choose_agent_type")
|
||||||
next_step = tool_data["next"] = "confirm"
|
|
||||||
current_tool = tool_data["tool"] = "ListAgents"
|
|
||||||
waiting_for_confirm = True
|
|
||||||
self.confirmed = True
|
|
||||||
|
|
||||||
# else if the next step is to be done with the conversation such as if the user requests it via asking to "end conversation"
|
# else if the next step is to be done with the conversation such as if the user requests it via asking to "end conversation"
|
||||||
elif next_step == "done":
|
elif next_step == "done":
|
||||||
|
|
||||||
self.add_message("agent", tool_data)
|
self.add_message("agent", tool_data)
|
||||||
|
|
||||||
#here we could send conversation to AI for analysis
|
# here we could send conversation to AI for analysis
|
||||||
|
|
||||||
# end the workflow
|
# end the workflow
|
||||||
return str(self.conversation_history)
|
return str(self.conversation_history)
|
||||||
@@ -195,10 +212,10 @@ class AgentGoalWorkflow:
|
|||||||
self.prompt_queue,
|
self.prompt_queue,
|
||||||
self.goal,
|
self.goal,
|
||||||
MAX_TURNS_BEFORE_CONTINUE,
|
MAX_TURNS_BEFORE_CONTINUE,
|
||||||
self.add_message
|
self.add_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
#Signal that comes from api/main.py via a post to /send-prompt
|
# Signal that comes from api/main.py via a post to /send-prompt
|
||||||
@workflow.signal
|
@workflow.signal
|
||||||
async def user_prompt(self, prompt: str) -> None:
|
async def user_prompt(self, prompt: str) -> None:
|
||||||
"""Signal handler for receiving user prompts."""
|
"""Signal handler for receiving user prompts."""
|
||||||
@@ -208,28 +225,28 @@ class AgentGoalWorkflow:
|
|||||||
return
|
return
|
||||||
self.prompt_queue.append(prompt)
|
self.prompt_queue.append(prompt)
|
||||||
|
|
||||||
#Signal that comes from api/main.py via a post to /confirm
|
# Signal that comes from api/main.py via a post to /confirm
|
||||||
@workflow.signal
|
@workflow.signal
|
||||||
async def confirmed(self) -> None:
|
async def confirm(self) -> None:
|
||||||
"""Signal handler for user confirmation of tool execution."""
|
"""Signal handler for user confirmation of tool execution."""
|
||||||
workflow.logger.info("Received user signal: confirmation")
|
workflow.logger.info("Received user signal: confirmation")
|
||||||
self.confirmed = True
|
self.confirmed = True
|
||||||
|
|
||||||
#Signal that comes from api/main.py via a post to /end-chat
|
# Signal that comes from api/main.py via a post to /end-chat
|
||||||
@workflow.signal
|
@workflow.signal
|
||||||
async def end_chat(self) -> None:
|
async def end_chat(self) -> None:
|
||||||
"""Signal handler for ending the chat session."""
|
"""Signal handler for ending the chat session."""
|
||||||
workflow.logger.info("signal received: end_chat")
|
workflow.logger.info("signal received: end_chat")
|
||||||
self.chat_ended = True
|
self.chat_ended = True
|
||||||
|
|
||||||
#Signal that can be sent from Temporal Workflow UI to enable debugging confirm and override .env setting
|
# Signal that can be sent from Temporal Workflow UI to enable debugging confirm and override .env setting
|
||||||
@workflow.signal
|
@workflow.signal
|
||||||
async def enable_debugging_confirm(self) -> None:
|
async def enable_debugging_confirm(self) -> None:
|
||||||
"""Signal handler for enabling debugging confirm UI & associated logic."""
|
"""Signal handler for enabling debugging confirm UI & associated logic."""
|
||||||
workflow.logger.info("signal received: enable_debugging_confirm")
|
workflow.logger.info("signal received: enable_debugging_confirm")
|
||||||
self.enable_debugging_confirm = True
|
self.enable_debugging_confirm = True
|
||||||
|
|
||||||
#Signal that can be sent from Temporal Workflow UI to disable debugging confirm and override .env setting
|
# Signal that can be sent from Temporal Workflow UI to disable debugging confirm and override .env setting
|
||||||
@workflow.signal
|
@workflow.signal
|
||||||
async def disable_debugging_confirm(self) -> None:
|
async def disable_debugging_confirm(self) -> None:
|
||||||
"""Signal handler for disabling debugging confirm UI & associated logic."""
|
"""Signal handler for disabling debugging confirm UI & associated logic."""
|
||||||
@@ -275,7 +292,7 @@ class AgentGoalWorkflow:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def change_goal(self, goal: str) -> None:
|
def change_goal(self, goal: str) -> None:
|
||||||
""" Change the goal (usually on request of the user).
|
"""Change the goal (usually on request of the user).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
goal: goal to change to)
|
goal: goal to change to)
|
||||||
@@ -286,8 +303,9 @@ class AgentGoalWorkflow:
|
|||||||
self.goal = listed_goal
|
self.goal = listed_goal
|
||||||
workflow.logger.info("Changed goal to " + goal)
|
workflow.logger.info("Changed goal to " + goal)
|
||||||
if goal is None:
|
if goal is None:
|
||||||
workflow.logger.warning("Goal not set after goal reset, probably bad.") # if this happens, there's probably a problem with the goal list
|
workflow.logger.warning(
|
||||||
|
"Goal not set after goal reset, probably bad."
|
||||||
|
) # if this happens, there's probably a problem with the goal list
|
||||||
|
|
||||||
# workflow function that defines if chat should end
|
# workflow function that defines if chat should end
|
||||||
def chat_should_end(self) -> bool:
|
def chat_should_end(self) -> bool:
|
||||||
@@ -298,7 +316,9 @@ class AgentGoalWorkflow:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# define if we're ready for tool execution
|
# define if we're ready for tool execution
|
||||||
def ready_for_tool_execution(self, waiting_for_confirm: bool, current_tool: Any) -> bool:
|
def ready_for_tool_execution(
|
||||||
|
self, waiting_for_confirm: bool, current_tool: Any
|
||||||
|
) -> bool:
|
||||||
if self.confirmed and waiting_for_confirm and current_tool and self.tool_data:
|
if self.confirmed and waiting_for_confirm and current_tool and self.tool_data:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -313,11 +333,12 @@ class AgentGoalWorkflow:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# look up env settings in an activity so they're part of history
|
# look up env settings in an activity so they're part of history
|
||||||
async def lookup_wf_env_settings(self, combined_input: CombinedInput)->None:
|
async def lookup_wf_env_settings(self, combined_input: CombinedInput) -> None:
|
||||||
env_lookup_input = EnvLookupInput(
|
env_lookup_input = EnvLookupInput(
|
||||||
show_confirm_env_var_name = "SHOW_CONFIRM",
|
show_confirm_env_var_name="SHOW_CONFIRM",
|
||||||
show_confirm_default = True)
|
show_confirm_default=True,
|
||||||
env_output:EnvLookupOutput = await workflow.execute_activity(
|
)
|
||||||
|
env_output: EnvLookupOutput = await workflow.execute_activity_method(
|
||||||
ToolActivities.get_wf_env_vars,
|
ToolActivities.get_wf_env_vars,
|
||||||
env_lookup_input,
|
env_lookup_input,
|
||||||
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||||
@@ -330,8 +351,10 @@ class AgentGoalWorkflow:
|
|||||||
|
|
||||||
# execute the tool - return False if we're not waiting for confirm anymore (always the case if it works successfully)
|
# execute the tool - return False if we're not waiting for confirm anymore (always the case if it works successfully)
|
||||||
#
|
#
|
||||||
async def execute_tool(self, current_tool: str)->bool:
|
async def execute_tool(self, current_tool: str) -> bool:
|
||||||
workflow.logger.info(f"workflow step: user has confirmed, executing the tool {current_tool}")
|
workflow.logger.info(
|
||||||
|
f"workflow step: user has confirmed, executing the tool {current_tool}"
|
||||||
|
)
|
||||||
self.confirmed = False
|
self.confirmed = False
|
||||||
waiting_for_confirm = False
|
waiting_for_confirm = False
|
||||||
confirmed_tool_data = self.tool_data.copy()
|
confirmed_tool_data = self.tool_data.copy()
|
||||||
@@ -344,27 +367,34 @@ class AgentGoalWorkflow:
|
|||||||
self.tool_data,
|
self.tool_data,
|
||||||
self.tool_results,
|
self.tool_results,
|
||||||
self.add_message,
|
self.add_message,
|
||||||
self.prompt_queue
|
self.prompt_queue,
|
||||||
)
|
)
|
||||||
|
|
||||||
#set new goal if we should
|
# set new goal if we should
|
||||||
if len(self.tool_results) > 0:
|
if len(self.tool_results) > 0:
|
||||||
if "ChangeGoal" in self.tool_results[-1].values() and "new_goal" in self.tool_results[-1].keys():
|
if (
|
||||||
|
"ChangeGoal" in self.tool_results[-1].values()
|
||||||
|
and "new_goal" in self.tool_results[-1].keys()
|
||||||
|
):
|
||||||
new_goal = self.tool_results[-1].get("new_goal")
|
new_goal = self.tool_results[-1].get("new_goal")
|
||||||
workflow.logger.info(f"Booya new goal!: {new_goal}")
|
|
||||||
self.change_goal(new_goal)
|
self.change_goal(new_goal)
|
||||||
elif "ListAgents" in self.tool_results[-1].values() and self.goal.id != "goal_choose_agent_type":
|
elif (
|
||||||
workflow.logger.info("setting goal to goal_choose_agent_type")
|
"ListAgents" in self.tool_results[-1].values()
|
||||||
|
and self.goal.id != "goal_choose_agent_type"
|
||||||
|
):
|
||||||
self.change_goal("goal_choose_agent_type")
|
self.change_goal("goal_choose_agent_type")
|
||||||
return waiting_for_confirm
|
return waiting_for_confirm
|
||||||
|
|
||||||
# debugging helper - drop this in various places in the workflow to get status
|
# debugging helper - drop this in various places in the workflow to get status
|
||||||
# also don't forget you can look at the workflow itself and do queries if you want
|
# also don't forget you can look at the workflow itself and do queries if you want
|
||||||
def print_useful_workflow_vars(self, status_or_step:str) -> None:
|
def print_useful_workflow_vars(self, status_or_step: str) -> None:
|
||||||
print(f"***{status_or_step}:***")
|
print(f"***{status_or_step}:***")
|
||||||
|
if self.goal:
|
||||||
|
print(f"current goal: {self.goal.id}")
|
||||||
|
if self.tool_data:
|
||||||
print(f"force confirm? {self.tool_data['force_confirm']}")
|
print(f"force confirm? {self.tool_data['force_confirm']}")
|
||||||
print(f"next step: {self.tool_data.get('next')}")
|
print(f"next step: {self.tool_data.get('next')}")
|
||||||
print(f"current_tool: {self.tool_data.get('tool')}")
|
print(f"current_tool: {self.tool_data.get('tool')}")
|
||||||
print(f"self.confirm: {self.confirmed}")
|
else:
|
||||||
print(f"waiting_for_confirm (about to be set to true): {self.waiting_for_confirm}")
|
print("no tool data initialized yet")
|
||||||
|
print(f"self.confirmed: {self.confirmed}")
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Dict, Any, Deque
|
from typing import Any, Deque, Dict
|
||||||
|
|
||||||
from temporalio import workflow
|
from temporalio import workflow
|
||||||
from temporalio.exceptions import ActivityError
|
|
||||||
from temporalio.common import RetryPolicy
|
from temporalio.common import RetryPolicy
|
||||||
|
from temporalio.exceptions import ActivityError
|
||||||
|
|
||||||
from models.data_types import ConversationHistory, ToolPromptInput
|
from models.data_types import ConversationHistory, ToolPromptInput
|
||||||
from prompts.agent_prompt_generators import (
|
from prompts.agent_prompt_generators import (
|
||||||
|
|||||||