Model Context Protocol (MCP) support with new use case (#42)
* initial mcp * food ordering with mcp * prompt eng * splitting out goals and updating docs * a diff so I can get tests from codex * a diff so I can get tests from codex * oops, missing files * tests, file formatting * readme and setup updates * setup.md link fixes * readme change * readme change * readme change * stripe food setup script * single agent mode default * prompt engineering for better multi agent performance * performance should be greatly improved * Update goals/finance.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update activities/tool_activities.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * co-pilot PR suggested this change, and now fixed it * stronger wording around json format response * formatting * moved docs to dir * moved image assets under docs * cleanup env example, stripe guidance * cleanup --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
43
.env.example
@@ -1,38 +1,45 @@
|
|||||||
RAPIDAPI_KEY=9df2cb5...
|
# Example environment configuration
|
||||||
RAPIDAPI_HOST_FLIGHTS=sky-scrapper.p.rapidapi.com #For travel flight information tool
|
|
||||||
|
### LLM configuration
|
||||||
|
LLM_MODEL=openai/gpt-4o
|
||||||
|
LLM_KEY=sk-proj-...
|
||||||
|
# LLM_MODEL=anthropic/claude-3-5-sonnet-20240620
|
||||||
|
# LLM_KEY=${ANTHROPIC_API_KEY}
|
||||||
|
# LLM_MODEL=gemini/gemini-2.5-flash-preview-04-17
|
||||||
|
# LLM_KEY=${GOOGLE_API_KEY}
|
||||||
|
|
||||||
|
### Tool API keys
|
||||||
|
# RAPIDAPI_KEY=9df2cb5... # Optional - if unset flight search generates realistic mock data
|
||||||
|
# RAPIDAPI_HOST_FLIGHTS=sky-scrapper.p.rapidapi.com # For real travel flight information (optional)
|
||||||
RAPIDAPI_HOST_PACKAGE=trackingpackage.p.rapidapi.com # For eCommerce order status package tracking tool
|
RAPIDAPI_HOST_PACKAGE=trackingpackage.p.rapidapi.com # For eCommerce order status package tracking tool
|
||||||
FOOTBALL_DATA_API_KEY=
|
FOOTBALL_DATA_API_KEY=
|
||||||
# Leave blank to use the built-in mock fixtures generator
|
# Leave blank to use the built-in mock fixtures generator
|
||||||
|
|
||||||
STRIPE_API_KEY=sk_test_51J...
|
STRIPE_API_KEY=sk_test_51J...
|
||||||
|
# Optional for `goal_event_flight_invoice` – if unset a mock invoice is created.
|
||||||
|
# Sign up for a free Stripe account and get a test key at https://dashboard.stripe.com/test/apikeys
|
||||||
|
|
||||||
LLM_MODEL=openai/gpt-4o # default
|
### Temporal connection (optional)
|
||||||
LLM_KEY=sk-proj-...
|
# Uncomment and update these values to connect to a non-default Temporal server
|
||||||
|
|
||||||
|
|
||||||
# uncomment and unset these environment variables to connect to the local dev server
|
|
||||||
# TEMPORAL_ADDRESS=namespace.acct.tmprl.cloud:7233
|
# TEMPORAL_ADDRESS=namespace.acct.tmprl.cloud:7233
|
||||||
# TEMPORAL_NAMESPACE=default
|
# TEMPORAL_NAMESPACE=default
|
||||||
# TEMPORAL_TASK_QUEUE=agent-task-queue
|
# TEMPORAL_TASK_QUEUE=agent-task-queue
|
||||||
|
|
||||||
# Uncomment if using mTLS (not needed for local dev server)
|
|
||||||
# TEMPORAL_TLS_CERT='path/to/cert.pem'
|
# TEMPORAL_TLS_CERT='path/to/cert.pem'
|
||||||
# TEMPORAL_TLS_KEY='path/to/key.pem'
|
# TEMPORAL_TLS_KEY='path/to/key.pem'
|
||||||
|
|
||||||
# Uncomment if using API key (not needed for local dev server)
|
|
||||||
# TEMPORAL_API_KEY=abcdef1234567890
|
# TEMPORAL_API_KEY=abcdef1234567890
|
||||||
|
|
||||||
# Set starting goal of agent - if unset default is goal_choose_agent_type
|
### Agent goal configuration
|
||||||
AGENT_GOAL=goal_choose_agent_type # for multi-goal start
|
# Set starting goal of agent - if unset default is goal_event_flight_invoice (single agent mode)
|
||||||
#AGENT_GOAL=goal_event_flight_invoice # for original goal
|
#AGENT_GOAL=goal_choose_agent_type # for multi-goal mode (experimental)
|
||||||
|
AGENT_GOAL=goal_event_flight_invoice
|
||||||
#AGENT_GOAL=goal_match_train_invoice # for replay goal
|
#AGENT_GOAL=goal_match_train_invoice # for replay goal
|
||||||
|
|
||||||
#Choose which category(ies) of goals you want to be listed by the Agent Goal picker if enabled above
|
# Choose which goal categories are listed by the Agent Goal picker if enabled above
|
||||||
# - options are system (always included), hr, travel, or all.
|
# Options: system (always included), hr, travel-flights, travel-trains, fin, ecommerce, mcp-integrations, food, all
|
||||||
GOAL_CATEGORIES=fin # default is all
|
GOAL_CATEGORIES=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)
|
### Other settings
|
||||||
SHOW_CONFIRM=True
|
SHOW_CONFIRM=True
|
||||||
|
|
||||||
# Money Scenarios:
|
# Money Scenarios:
|
||||||
|
|||||||
68
AGENTS.md
@@ -3,7 +3,9 @@
|
|||||||
## Repository Layout
|
## Repository Layout
|
||||||
- `workflows/` - Temporal workflows including the main AgentGoalWorkflow for multi-turn AI conversations
|
- `workflows/` - Temporal workflows including the main AgentGoalWorkflow for multi-turn AI conversations
|
||||||
- `activities/` - Temporal activities for tool execution and LLM interactions
|
- `activities/` - Temporal activities for tool execution and LLM interactions
|
||||||
- `tools/` - AI agent tools organized by category (finance, HR, ecommerce, travel, etc.)
|
- `tools/` - Native AI agent tool implementations organized by category (finance, HR, ecommerce, travel, etc.)
|
||||||
|
- `goals/` - Agent goal definitions organized by category, supporting both native and MCP tools
|
||||||
|
- `shared/` - Shared configuration including MCP server definitions
|
||||||
- `models/` - Data types and tool definitions used throughout the system
|
- `models/` - Data types and tool definitions used throughout the system
|
||||||
- `prompts/` - Agent prompt generators and templates
|
- `prompts/` - Agent prompt generators and templates
|
||||||
- `api/` - FastAPI server that exposes REST endpoints to interact with workflows
|
- `api/` - FastAPI server that exposes REST endpoints to interact with workflows
|
||||||
@@ -77,15 +79,20 @@ Default URLs:
|
|||||||
Copy `.env.example` to `.env` and configure:
|
Copy `.env.example` to `.env` and configure:
|
||||||
```bash
|
```bash
|
||||||
# Required: LLM Configuration
|
# Required: LLM Configuration
|
||||||
LLM_MODEL=openai/gpt-4o # or anthropic/claude-3-sonnet, etc.
|
LLM_MODEL=openai/gpt-4o
|
||||||
LLM_KEY=your-api-key-here
|
LLM_KEY=your-api-key-here
|
||||||
|
# LLM_MODEL=anthropic/claude-3-5-sonnet-20240620
|
||||||
|
# LLM_KEY=${ANTHROPIC_API_KEY}
|
||||||
|
# LLM_MODEL=gemini/gemini-2.5-flash-preview-04-17
|
||||||
|
# LLM_KEY=${GOOGLE_API_KEY}
|
||||||
|
|
||||||
# Optional: Agent Goals and Categories
|
# Optional: Agent Goals and Categories
|
||||||
AGENT_GOAL=goal_choose_agent_type
|
AGENT_GOAL=goal_choose_agent_type
|
||||||
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin
|
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin,ecommerce,mcp-integrations,food
|
||||||
|
|
||||||
# Optional: Tool-specific APIs
|
# Optional: Tool-specific APIs
|
||||||
STRIPE_API_KEY=sk_test_... # For invoice creation
|
STRIPE_API_KEY=sk_test_... # For invoice creation
|
||||||
|
# `goal_event_flight_invoice` works without this key – it falls back to a mock invoice if unset
|
||||||
FOOTBALL_DATA_API_KEY=... # For real football fixtures
|
FOOTBALL_DATA_API_KEY=... # For real football fixtures
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -117,7 +124,7 @@ poetry run pytest --cov=workflows --cov=activities
|
|||||||
- ✅ **Integration Tests**: End-to-end workflow and activity execution
|
- ✅ **Integration Tests**: End-to-end workflow and activity execution
|
||||||
|
|
||||||
**Documentation:**
|
**Documentation:**
|
||||||
- **Quick Start**: [TESTING.md](TESTING.md) - Simple commands to run tests
|
- **Quick Start**: [testing.md](docs/testing.md) - Simple commands to run tests
|
||||||
- **Comprehensive Guide**: [tests/README.md](tests/README.md) - Detailed testing patterns and best practices
|
- **Comprehensive Guide**: [tests/README.md](tests/README.md) - Detailed testing patterns and best practices
|
||||||
|
|
||||||
## Linting and Code Quality
|
## Linting and Code Quality
|
||||||
@@ -136,31 +143,50 @@ poetry run mypy --check-untyped-defs --namespace-packages .
|
|||||||
|
|
||||||
## Agent Customization
|
## Agent Customization
|
||||||
|
|
||||||
### Adding New Tools
|
### Adding New Goals and Tools
|
||||||
|
|
||||||
|
#### For Native Tools:
|
||||||
1. Create tool implementation in `tools/` directory
|
1. Create tool implementation in `tools/` directory
|
||||||
2. Add tool function mapping in `tools/__init__.py`
|
2. Add tool function mapping in `tools/__init__.py`
|
||||||
3. Register tool definition in `tools/tool_registry.py`
|
3. Register tool definition in `tools/tool_registry.py`
|
||||||
4. Associate with goals in `tools/goal_registry.py`
|
4. Add tool names to static tools list in `workflows/workflow_helpers.py`
|
||||||
|
5. Create or update goal definition in appropriate file in `goals/` directory
|
||||||
|
|
||||||
|
#### For MCP Tools:
|
||||||
|
1. Configure MCP server definition in `shared/mcp_config.py` (for reusable servers)
|
||||||
|
2. Create or update goal definition in appropriate file in `goals/` directory with `mcp_server_definition`
|
||||||
|
3. Set required environment variables (API keys, etc.)
|
||||||
|
|
||||||
|
#### For Goals:
|
||||||
|
1. Create goal file in `goals/` directory (e.g., `goals/my_category.py`)
|
||||||
|
2. Import and extend the goal list in `goals/__init__.py`
|
||||||
|
|
||||||
### Configuring Goals
|
### Configuring Goals
|
||||||
The agent supports multiple goal categories:
|
The agent supports multiple goal categories organized in `goals/`:
|
||||||
- **Financial**: Money transfers, loan applications (`fin/`)
|
- **Financial**: Money transfers, loan applications (`goals/finance.py`)
|
||||||
- **HR**: PTO booking, payroll status (`hr/`)
|
- **HR**: PTO booking, payroll status (`goals/hr.py`)
|
||||||
- **Travel**: Flight/train booking, event finding
|
- **Travel**: Flight/train booking, event finding (`goals/travel.py`)
|
||||||
- **Ecommerce**: Order tracking, package management (`ecommerce/`)
|
- **Ecommerce**: Order tracking, package management (`goals/ecommerce.py`)
|
||||||
|
- **Food**: Restaurant ordering and cart management (`goals/food.py`)
|
||||||
|
- **MCP Integrations**: External service integrations like Stripe (`goals/stripe_mcp.py`)
|
||||||
|
|
||||||
See [adding-goals-and-tools.md](adding-goals-and-tools.md) for detailed customization guide.
|
Goals can use:
|
||||||
|
- **Native Tools**: Custom implementations in `/tools/` directory
|
||||||
|
- **MCP Tools**: External tools via Model Context Protocol servers (configured in `shared/mcp_config.py`)
|
||||||
|
|
||||||
|
See [adding-goals-and-tools.md](docs/adding-goals-and-tools.md) for detailed customization guide.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
This system implements "Agentic AI" with these key components:
|
This system implements agentic AI—autonomous systems that pursue goals through iterative tool use and human feedback—with these key components:
|
||||||
1. **Goals** - High-level objectives accomplished through tool sequences
|
1. **Goals** - High-level objectives accomplished through tool sequences (organized in `/goals/` by category)
|
||||||
2. **Agent Loops** - LLM execution → tool calls → human input → repeat until goal completion
|
2. **Native & MCP Tools** - Custom implementations and external service integrations
|
||||||
3. **Tool Approval** - Human confirmation for sensitive operations
|
3. **Agent Loops** - LLM execution → tool calls → human input → repeat until goal completion
|
||||||
4. **Conversation Management** - LLM-powered input validation and history summarization
|
4. **Tool Approval** - Human confirmation for sensitive operations
|
||||||
5. **Durability** - Temporal workflows ensure reliable execution across failures
|
5. **Conversation Management** - LLM-powered input validation and history summarization
|
||||||
|
6. **Durability** - Temporal workflows ensure reliable execution across failures
|
||||||
|
|
||||||
For detailed architecture information, see [architecture.md](architecture.md).
|
For detailed architecture information, see [architecture.md](docs/architecture.md).
|
||||||
|
|
||||||
## Commit Messages and Pull Requests
|
## Commit Messages and Pull Requests
|
||||||
- Use clear commit messages describing the change purpose
|
- Use clear commit messages describing the change purpose
|
||||||
@@ -169,7 +195,7 @@ For detailed architecture information, see [architecture.md](architecture.md).
|
|||||||
- Ensure tests pass before submitting: `poetry run pytest --workflow-environment=time-skipping`
|
- Ensure tests pass before submitting: `poetry run pytest --workflow-environment=time-skipping`
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
- **Setup Guide**: [SETUP.md](SETUP.md) - Detailed configuration instructions
|
- **Setup Guide**: [setup.md](docs/setup.md) - Detailed configuration instructions
|
||||||
- **Architecture Decisions**: [architecture-decisions.md](architecture-decisions.md) - Why Temporal for AI agents
|
- **Architecture Decisions**: [architecture-decisions.md](docs/architecture-decisions.md) - Why Temporal for AI agents
|
||||||
- **Demo Video**: [5-minute YouTube overview](https://www.youtube.com/watch?v=GEXllEH2XiQ)
|
- **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)
|
- **Multi-Agent Demo**: [Advanced multi-agent execution](https://www.youtube.com/watch?v=8Dc_0dC14yY)
|
||||||
38
README.md
@@ -1,6 +1,10 @@
|
|||||||
# Temporal AI Agent
|
# Temporal AI Agent
|
||||||
|
|
||||||
This demo shows a multi-turn conversation with an AI agent running inside a Temporal workflow. The purpose of the agent is to collect information towards a goal, running tools along the way. 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. The agent supports both native tools and Model Context Protocol (MCP) tools, allowing it to interact with external services.
|
||||||
|
|
||||||
|
The agent operates in single-agent mode by default, focusing on one specific goal. It also supports experimental multi-agent/multi-goal mode where users can choose between different agent types and switch between them during conversations.
|
||||||
|
|
||||||
|
Goals are organized in the `/goals/` directory by category (finance, HR, travel, ecommerce, etc.) and can leverage both native and MCP tools.
|
||||||
|
|
||||||
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:
|
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)
|
- OpenAI models (GPT-4, GPT-3.5)
|
||||||
@@ -19,7 +23,7 @@ See multi-agent execution in action [here](https://www.youtube.com/watch?v=8Dc_0
|
|||||||
|
|
||||||
## 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](docs/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:
|
||||||
@@ -31,20 +35,29 @@ These are the key elements of an agentic framework:
|
|||||||
6. Prompt construction made of system prompts, conversation history, and tool metadata - sent to the LLM to create user questions and confirmations
|
6. Prompt construction made of system prompts, conversation history, and tool metadata - sent to the LLM to create user questions and confirmations
|
||||||
7. Ideally high durability (done in this system with Temporal Workflow and Activities)
|
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](docs/architecture.md).
|
||||||
|
|
||||||
|
## 🔧 MCP Tool Calling Support
|
||||||
|
|
||||||
|
This agent acts as an **MCP (Model Context Protocol) client**, enabling seamless integration with external services and tools. The system supports two types of tools:
|
||||||
|
- **Native Tools**: Custom tools implemented directly in the codebase (in `/tools/`)
|
||||||
|
- **MCP Tools**: External tools accessed via Model Context Protocol (MCP) servers like Stripe, databases, or APIs. Configuration is covered in [the Setup guide](docs/setup.md)
|
||||||
|
- Set `AGENT_GOAL=goal_food_ordering` with `SHOW_CONFIRM=False` in `.env` for an example of a goal that calls MCP Tools (Stripe).
|
||||||
|
|
||||||
## Setup and Configuration
|
## Setup and Configuration
|
||||||
See [the Setup guide](./SETUP.md) for detailed instructions. The basic configuration requires just two environment variables:
|
See [the Setup guide](docs/setup.md) for detailed instructions. The basic configuration requires just two environment variables:
|
||||||
```bash
|
```bash
|
||||||
LLM_MODEL=openai/gpt-4o # or any other model supported by LiteLLM
|
LLM_MODEL=openai/gpt-4o # or any other model supported by LiteLLM
|
||||||
LLM_KEY=your-api-key-here
|
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](docs/adding-goals-and-tools.md).
|
||||||
|
|
||||||
|
The system supports MCP (Model Context Protocol) for easy integration with external services. MCP server configurations are managed in `shared/mcp_config.py`, and goals are organized by category in the `/goals/` directory.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
See [the architecture guide](./architecture.md).
|
See [the architecture guide](docs/architecture.md).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@@ -66,25 +79,24 @@ poetry run pytest --workflow-environment=time-skipping
|
|||||||
- ✅ **Activity Tests**: ToolActivities, LLM integration (mocked), environment configuration
|
- ✅ **Activity Tests**: ToolActivities, LLM integration (mocked), environment configuration
|
||||||
- ✅ **Integration Tests**: End-to-end workflow and activity execution
|
- ✅ **Integration Tests**: End-to-end workflow and activity execution
|
||||||
|
|
||||||
**Documentation:**
|
- **Quick Start**: [testing.md](docs/testing.md) - Simple commands to run tests
|
||||||
- **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
|
- **Comprehensive Guide**: [tests/README.md](tests/README.md) - Detailed testing documentation, patterns, and best practices
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
To contribute to this project, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
To contribute to this project, see [contributing.md](docs/contributing.md).
|
||||||
|
|
||||||
Start the Temporal Server and API server, see [setup](SETUP.md)
|
Start the Temporal Server and API server, see [setup](docs/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 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).
|
- A single worker can easily support many agent workflows (chats) running at the same time. Currently the workflow ID is the same each time, so it will only run one agent at a time. To run multiple agents, you can use a different workflow ID each time (e.g. by using a UUID or timestamp).
|
||||||
- Perhaps the UI should show when the LLM response is being retried (i.e. activity retry attempt because the LLM provided bad output)
|
- Perhaps the UI should show when the LLM response is being retried (i.e. activity retry attempt because the LLM provided bad output)
|
||||||
- The project now includes comprehensive tests for workflows and activities! [See testing guide](TESTING.md).
|
- The project now includes comprehensive tests for workflows and activities! [See testing guide](docs/testing.md).
|
||||||
|
|
||||||
See [the todo](./todo.md) for more details on things we want to do (or that you could contribute!).
|
See [the todo](docs/todo.md) for more details on things we want to do (or that you could contribute!).
|
||||||
|
|
||||||
See [the guide to adding goals and tools](./adding-goals-and-tools.md) for more ways you can add features.
|
See [the guide to adding goals and tools](docs/adding-goals-and-tools.md) for more ways you can add features.
|
||||||
|
|
||||||
## Enablement Guide (internal resource for Temporal employees)
|
## Enablement Guide (internal resource for Temporal employees)
|
||||||
Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the [enablement guide](https://docs.google.com/document/d/14E0cEOibUAgHPBqConbWXgPUBY0Oxrnt6_AImdiheW4/edit?tab=t.0#heading=h.ajnq2v3xqbu1).
|
Check out the [slides](https://docs.google.com/presentation/d/1wUFY4v17vrtv8llreKEBDPLRtZte3FixxBUn0uWy5NU/edit#slide=id.g3333e5deaa9_0_0) here and the [enablement guide](https://docs.google.com/document/d/14E0cEOibUAgHPBqConbWXgPUBY0Oxrnt6_AImdiheW4/edit?tab=t.0#heading=h.ajnq2v3xqbu1).
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Sequence
|
from typing import Any, Dict, List, Optional, Sequence
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from litellm import completion
|
from litellm import completion
|
||||||
from temporalio import activity
|
from temporalio import activity
|
||||||
from temporalio.common import RawValue
|
from temporalio.common import RawValue
|
||||||
|
from temporalio.exceptions import ApplicationError
|
||||||
|
|
||||||
from models.data_types import (
|
from models.data_types import (
|
||||||
EnvLookupInput,
|
EnvLookupInput,
|
||||||
@@ -16,6 +18,17 @@ from models.data_types import (
|
|||||||
ValidationInput,
|
ValidationInput,
|
||||||
ValidationResult,
|
ValidationResult,
|
||||||
)
|
)
|
||||||
|
from models.tool_definitions import MCPServerDefinition
|
||||||
|
|
||||||
|
# Import MCP client libraries
|
||||||
|
try:
|
||||||
|
from mcp import ClientSession, StdioServerParameters
|
||||||
|
from mcp.client.stdio import stdio_client
|
||||||
|
except ImportError:
|
||||||
|
# Fallback if MCP not installed
|
||||||
|
ClientSession = None
|
||||||
|
StdioServerParameters = None
|
||||||
|
stdio_client = None
|
||||||
|
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
@@ -120,10 +133,16 @@ class ToolActivities:
|
|||||||
response = completion(**completion_kwargs)
|
response = completion(**completion_kwargs)
|
||||||
|
|
||||||
response_content = response.choices[0].message.content
|
response_content = response.choices[0].message.content
|
||||||
activity.logger.info(f"LLM response: {response_content}")
|
activity.logger.info(f"Raw LLM response: {repr(response_content)}")
|
||||||
|
activity.logger.info(f"LLM response content: {response_content}")
|
||||||
|
activity.logger.info(f"LLM response type: {type(response_content)}")
|
||||||
|
activity.logger.info(
|
||||||
|
f"LLM response length: {len(response_content) if response_content else 'None'}"
|
||||||
|
)
|
||||||
|
|
||||||
# Use the new sanitize function
|
# Use the new sanitize function
|
||||||
response_content = self.sanitize_json_response(response_content)
|
response_content = self.sanitize_json_response(response_content)
|
||||||
|
activity.logger.info(f"Sanitized response: {repr(response_content)}")
|
||||||
|
|
||||||
return self.parse_json_response(response_content)
|
return self.parse_json_response(response_content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -159,7 +178,7 @@ class ToolActivities:
|
|||||||
handles default/None
|
handles default/None
|
||||||
"""
|
"""
|
||||||
output: EnvLookupOutput = EnvLookupOutput(
|
output: EnvLookupOutput = EnvLookupOutput(
|
||||||
show_confirm=input.show_confirm_default, multi_goal_mode=True
|
show_confirm=input.show_confirm_default, multi_goal_mode=False
|
||||||
)
|
)
|
||||||
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:
|
||||||
@@ -171,17 +190,29 @@ 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 = False # default to single agent mode if unset
|
||||||
elif (
|
elif (
|
||||||
first_goal_value is not None
|
first_goal_value is not None
|
||||||
and first_goal_value.lower() != "goal_choose_agent_type"
|
and first_goal_value.lower() == "goal_choose_agent_type"
|
||||||
):
|
):
|
||||||
output.multi_goal_mode = False
|
|
||||||
else:
|
|
||||||
output.multi_goal_mode = True
|
output.multi_goal_mode = True
|
||||||
|
else:
|
||||||
|
output.multi_goal_mode = False
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def mcp_tool_activity(
|
||||||
|
self, tool_name: str, tool_args: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""MCP Tool"""
|
||||||
|
activity.logger.info(f"Executing MCP tool: {tool_name} with args: {tool_args}")
|
||||||
|
|
||||||
|
# Extract server definition
|
||||||
|
server_definition = tool_args.pop("server_definition", None)
|
||||||
|
|
||||||
|
return await _execute_mcp_tool(tool_name, tool_args, server_definition)
|
||||||
|
|
||||||
|
|
||||||
@activity.defn(dynamic=True)
|
@activity.defn(dynamic=True)
|
||||||
async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||||
@@ -191,7 +222,15 @@ async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
|||||||
tool_args = activity.payload_converter().from_payload(args[0].payload, dict)
|
tool_args = activity.payload_converter().from_payload(args[0].payload, dict)
|
||||||
activity.logger.info(f"Running dynamic tool '{tool_name}' with args: {tool_args}")
|
activity.logger.info(f"Running dynamic tool '{tool_name}' with args: {tool_args}")
|
||||||
|
|
||||||
# Delegate to the relevant function
|
# Check if this is an MCP tool call by looking for server_definition in args
|
||||||
|
server_definition = tool_args.pop("server_definition", None)
|
||||||
|
|
||||||
|
if server_definition:
|
||||||
|
# This is an MCP tool call - handle it directly
|
||||||
|
activity.logger.info(f"Executing MCP tool: {tool_name}")
|
||||||
|
return await _execute_mcp_tool(tool_name, tool_args, server_definition)
|
||||||
|
else:
|
||||||
|
# This is a regular tool - delegate to the relevant function
|
||||||
handler = get_handler(tool_name)
|
handler = get_handler(tool_name)
|
||||||
if inspect.iscoroutinefunction(handler):
|
if inspect.iscoroutinefunction(handler):
|
||||||
result = await handler(tool_args)
|
result = await handler(tool_args)
|
||||||
@@ -201,3 +240,228 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
# MCP Client Activities
|
||||||
|
|
||||||
|
|
||||||
|
def _build_connection(
|
||||||
|
server_definition: MCPServerDefinition | Dict[str, Any] | None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Build connection parameters from MCPServerDefinition or dict"""
|
||||||
|
if server_definition is None:
|
||||||
|
# Default to stdio connection with the main server
|
||||||
|
return {"type": "stdio", "command": "python", "args": ["server.py"], "env": {}}
|
||||||
|
|
||||||
|
# Handle both MCPServerDefinition objects and dicts (from Temporal serialization)
|
||||||
|
if isinstance(server_definition, dict):
|
||||||
|
return {
|
||||||
|
"type": server_definition.get("connection_type", "stdio"),
|
||||||
|
"command": server_definition.get("command", "python"),
|
||||||
|
"args": server_definition.get("args", ["server.py"]),
|
||||||
|
"env": server_definition.get("env", {}) or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": server_definition.connection_type,
|
||||||
|
"command": server_definition.command,
|
||||||
|
"args": server_definition.args,
|
||||||
|
"env": server_definition.env or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_result(result: Any) -> Any:
|
||||||
|
"""Normalize MCP tool result for serialization"""
|
||||||
|
if hasattr(result, "content"):
|
||||||
|
# Handle MCP result objects
|
||||||
|
if hasattr(result.content, "__iter__") and not isinstance(result.content, str):
|
||||||
|
return [
|
||||||
|
item.text if hasattr(item, "text") else str(item)
|
||||||
|
for item in result.content
|
||||||
|
]
|
||||||
|
return str(result.content)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_args_types(tool_args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Convert string arguments to appropriate types for MCP tools"""
|
||||||
|
converted_args = {}
|
||||||
|
|
||||||
|
for key, value in tool_args.items():
|
||||||
|
if key == "server_definition":
|
||||||
|
# Skip server_definition - it's metadata
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
# Try to convert string values to appropriate types
|
||||||
|
if value.isdigit():
|
||||||
|
# Convert numeric strings to integers
|
||||||
|
converted_args[key] = int(value)
|
||||||
|
elif value.replace(".", "").isdigit() and value.count(".") == 1:
|
||||||
|
# Convert decimal strings to floats
|
||||||
|
converted_args[key] = float(value)
|
||||||
|
elif value.lower() in ("true", "false"):
|
||||||
|
# Convert boolean strings
|
||||||
|
converted_args[key] = value.lower() == "true"
|
||||||
|
else:
|
||||||
|
# Keep as string
|
||||||
|
converted_args[key] = value
|
||||||
|
else:
|
||||||
|
# Keep non-string values as-is
|
||||||
|
converted_args[key] = value
|
||||||
|
|
||||||
|
return converted_args
|
||||||
|
|
||||||
|
|
||||||
|
async def _execute_mcp_tool(
|
||||||
|
tool_name: str,
|
||||||
|
tool_args: Dict[str, Any],
|
||||||
|
server_definition: MCPServerDefinition | Dict[str, Any] | None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Execute an MCP tool with the given arguments and server definition"""
|
||||||
|
activity.logger.info(f"Executing MCP tool: {tool_name}")
|
||||||
|
|
||||||
|
# Convert argument types for MCP tools
|
||||||
|
converted_args = _convert_args_types(tool_args)
|
||||||
|
connection = _build_connection(server_definition)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if connection["type"] == "stdio":
|
||||||
|
# Handle stdio connection
|
||||||
|
async with _stdio_connection(
|
||||||
|
command=connection.get("command", "python"),
|
||||||
|
args=connection.get("args", ["server.py"]),
|
||||||
|
env=connection.get("env", {}),
|
||||||
|
) as (read, write):
|
||||||
|
async with ClientSession(read, write) as session:
|
||||||
|
# Initialize the session
|
||||||
|
activity.logger.info(f"Initializing MCP session for {tool_name}")
|
||||||
|
await session.initialize()
|
||||||
|
activity.logger.info(f"MCP session initialized for {tool_name}")
|
||||||
|
|
||||||
|
# Call the tool
|
||||||
|
activity.logger.info(
|
||||||
|
f"Calling MCP tool {tool_name} with args: {converted_args}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = await session.call_tool(
|
||||||
|
tool_name, arguments=converted_args
|
||||||
|
)
|
||||||
|
activity.logger.info(
|
||||||
|
f"MCP tool {tool_name} returned result: {result}"
|
||||||
|
)
|
||||||
|
except Exception as tool_exc:
|
||||||
|
activity.logger.error(
|
||||||
|
f"MCP tool {tool_name} call failed: {type(tool_exc).__name__}: {tool_exc}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
normalized_result = _normalize_result(result)
|
||||||
|
activity.logger.info(f"MCP tool {tool_name} completed successfully")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tool": tool_name,
|
||||||
|
"success": True,
|
||||||
|
"content": normalized_result,
|
||||||
|
}
|
||||||
|
|
||||||
|
elif connection["type"] == "tcp":
|
||||||
|
# Handle TCP connection (placeholder for future implementation)
|
||||||
|
raise ApplicationError("TCP connections not yet implemented")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ApplicationError(f"Unsupported connection type: {connection['type']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
activity.logger.error(f"MCP tool {tool_name} failed: {str(e)}")
|
||||||
|
|
||||||
|
# Return error information
|
||||||
|
return {
|
||||||
|
"tool": tool_name,
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"error_type": type(e).__name__,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def _stdio_connection(command: str, args: list, env: dict):
|
||||||
|
"""Create stdio connection to MCP server"""
|
||||||
|
if stdio_client is None:
|
||||||
|
raise ApplicationError("MCP client libraries not available")
|
||||||
|
|
||||||
|
# Create server parameters
|
||||||
|
server_params = StdioServerParameters(command=command, args=args, env=env)
|
||||||
|
|
||||||
|
async with stdio_client(server_params) as (read, write):
|
||||||
|
yield read, write
|
||||||
|
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def mcp_list_tools(
|
||||||
|
server_definition: MCPServerDefinition, include_tools: Optional[List[str]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""List available MCP tools from the specified server"""
|
||||||
|
|
||||||
|
activity.logger.info(f"Listing MCP tools for server: {server_definition.name}")
|
||||||
|
|
||||||
|
connection = _build_connection(server_definition)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if connection["type"] == "stdio":
|
||||||
|
async with _stdio_connection(
|
||||||
|
command=connection.get("command", "python"),
|
||||||
|
args=connection.get("args", ["server.py"]),
|
||||||
|
env=connection.get("env", {}),
|
||||||
|
) as (read, write):
|
||||||
|
async with ClientSession(read, write) as session:
|
||||||
|
# Initialize the session
|
||||||
|
await session.initialize()
|
||||||
|
|
||||||
|
# List available tools
|
||||||
|
tools_response = await session.list_tools()
|
||||||
|
|
||||||
|
# Process tools based on include_tools filter
|
||||||
|
tools_info = {}
|
||||||
|
for tool in tools_response.tools:
|
||||||
|
# If include_tools is specified, only include those tools
|
||||||
|
if include_tools is None or tool.name in include_tools:
|
||||||
|
tools_info[tool.name] = {
|
||||||
|
"name": tool.name,
|
||||||
|
"description": tool.description,
|
||||||
|
"inputSchema": (
|
||||||
|
tool.inputSchema.model_dump()
|
||||||
|
if hasattr(tool.inputSchema, "model_dump")
|
||||||
|
else str(tool.inputSchema)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.logger.info(
|
||||||
|
f"Found {len(tools_info)} tools for server {server_definition.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"server_name": server_definition.name,
|
||||||
|
"success": True,
|
||||||
|
"tools": tools_info,
|
||||||
|
"total_available": len(tools_response.tools),
|
||||||
|
"filtered_count": len(tools_info),
|
||||||
|
}
|
||||||
|
|
||||||
|
elif connection["type"] == "tcp":
|
||||||
|
raise ApplicationError("TCP connections not yet implemented")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ApplicationError(f"Unsupported connection type: {connection['type']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
activity.logger.error(
|
||||||
|
f"Failed to list tools for server {server_definition.name}: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"server_name": server_definition.name,
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"error_type": type(e).__name__,
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ 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 goals import goal_list
|
||||||
from models.data_types import AgentGoalWorkflowParams, CombinedInput
|
from models.data_types import AgentGoalWorkflowParams, CombinedInput
|
||||||
from shared.config import TEMPORAL_TASK_QUEUE, get_temporal_client
|
from shared.config import TEMPORAL_TASK_QUEUE, get_temporal_client
|
||||||
from tools.goal_registry import goal_list
|
|
||||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -24,8 +24,8 @@ 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(
|
env_goal = os.getenv(
|
||||||
"AGENT_GOAL", "goal_choose_agent_type"
|
"AGENT_GOAL", "goal_event_flight_invoice"
|
||||||
) # if no goal is set in the env file, default to choosing an agent
|
) # if no goal is set in the env file, default to single agent mode
|
||||||
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
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://cdn.statically.io/gh/nguyenngoclongdev/cdn/main/schema/v10/terminal-keeper.json",
|
|
||||||
"theme": "tribe",
|
|
||||||
"active": "default",
|
|
||||||
"activateOnStartup": false,
|
|
||||||
"keepExistingTerminals": false,
|
|
||||||
"sessions": {
|
|
||||||
"default": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "frontend",
|
|
||||||
"autoExecuteCommands": true,
|
|
||||||
"commands": [
|
|
||||||
"cd frontend && npx vite"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "uvicorn",
|
|
||||||
"autoExecuteCommands": true,
|
|
||||||
"commands": [
|
|
||||||
"poetry run uvicorn api.main:app --reload"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "agent worker",
|
|
||||||
"autoExecuteCommands": true,
|
|
||||||
"commands": [
|
|
||||||
"poetry run python scripts/run_worker.py"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "trains worker",
|
|
||||||
"autoExecuteCommands": true,
|
|
||||||
"commands": [
|
|
||||||
"poetry run python scripts/run_legacy_worker.py"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"name": "trains_api",
|
|
||||||
"autoExecuteCommands": true,
|
|
||||||
"commands": [
|
|
||||||
"poetry run python thirdparty/train_api.py"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
docs/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Documentation Index
|
||||||
|
|
||||||
|
- **architecture.md** - Overview of system components and how they interact.
|
||||||
|
- **architecture-decisions.md** - Rationale behind key design choices.
|
||||||
|
- **changelog.md** - Project history and notable changes.
|
||||||
|
- **contributing.md** - How to contribute and run tests.
|
||||||
|
- **setup.md** - Installation and configuration instructions.
|
||||||
|
- **testing.md** - Commands for running the test suite.
|
||||||
|
- **adding-goals-and-tools.md** - Guide to extending the agent with new goals and tools.
|
||||||
|
- **todo.md** - Planned enhancements and future work.
|
||||||
@@ -1,41 +1,50 @@
|
|||||||
# Customizing the Agent
|
# Customizing the Agent
|
||||||
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).
|
The agent operates in single-agent mode by default, focusing on one specific goal. It also supports an experimental multi-agent mode where users can have multiple agents, each with their own goal, and supports switching back to choosing a new goal at the end of every successful goal (or even mid-goal).
|
||||||
A goal is made up of a list of tools that the agent will guide the user through.
|
|
||||||
|
A goal can use two types of tools:
|
||||||
|
- **Native Tools**: Custom tools implemented directly in the codebase (in `/tools/`)
|
||||||
|
- **MCP Tools**: External tools accessed via Model Context Protocol (MCP) servers
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
## Adding a New Goal Category
|
## Adding a New Goal Category
|
||||||
Goal Categories lets you pick which groups of goals to show. Set via an .env setting, `GOAL_CATEGORIES`.
|
Goal Categories lets you pick which groups of goals to show in multi-agent mode. Set via an .env setting, `GOAL_CATEGORIES`.
|
||||||
Even if you don't intend to use the goal in a multi-goal scenario, goal categories are useful for others.
|
Even if you don't intend to use the goal in a multi-agent scenario, goal categories are useful for organization and discovery.
|
||||||
1. Pick a unique one that has some business meaning
|
1. Pick a unique one that has some business meaning
|
||||||
2. Use it in your [.env](./.env) file
|
2. Use it in your [.env](./.env) file
|
||||||
3. Add to [.env.example](./.env.example)
|
3. Add to [.env.example](./.env.example)
|
||||||
4. Use it in your Goal definition, see below.
|
4. Use it in your Goal definition, see below.
|
||||||
|
|
||||||
## Adding a Goal
|
## Adding a Goal
|
||||||
1. Open [/tools/goal_registry.py](tools/goal_registry.py) - this file contains descriptions of goals and the tools used to achieve them
|
1. Create a new Python file in the `/goals/` directory (e.g., `goals/my_category.py`) - these files contain descriptions of goals and the tools used to achieve them
|
||||||
2. Pick a name for your goal! (such as "goal_hr_schedule_pto")
|
2. Pick a name for your goal! (such as "goal_hr_schedule_pto")
|
||||||
3. Fill out the required elements:
|
3. Fill out the required elements:
|
||||||
- `id`: needs to be the same as the name
|
- `id`: needs to be the same as the name
|
||||||
- `agent_name`: user-facing name for the agent/chatbot
|
- `agent_name`: user-facing name for the agent/chatbot
|
||||||
- `category_tag`: category for the goal
|
- `category_tag`: category for the goal
|
||||||
- `agent_friendly_description`: user-facing description of what the agent/chatbot does
|
- `agent_friendly_description`: user-facing description of what the agent/chatbot does
|
||||||
- `tools`: the list of tools the goal will walk the user through. These will be defined in the [tools/tool_registry.py](tools/tool_registry.py) and should be defined in list form as tool_registry.[name of tool]
|
- `tools`: the list of **native tools** the goal uses. These are defined in [tools/tool_registry.py](tools/tool_registry.py) as `tool_registry.[name_of_tool]`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```python
|
||||||
tools=[
|
tools=[
|
||||||
tool_registry.current_pto_tool,
|
tool_registry.current_pto_tool,
|
||||||
tool_registry.future_pto_calc_tool,
|
tool_registry.future_pto_calc_tool,
|
||||||
tool_registry.book_pto_tool,
|
tool_registry.book_pto_tool,
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
- `description`: LLM-facing description of the goal that lists the tools by name and purpose.
|
- `mcp_server_definition`: (Optional) MCP server configuration for external tools. Can use predefined configurations from `shared/mcp_config.py` or define custom ones. See [MCP Tools section](#adding-mcp-tools) below.
|
||||||
- `starter-prompt`: LLM-facing first prompt given to begin the scenario. This field can contain instructions that are different from other goals, like "begin by providing the output of the first tool" rather than waiting on user confirmation. (See [goal_choose_agent_type](tools/goal_registry.py) for an example.)
|
- `description`: LLM-facing description of the goal that lists all tools (native and MCP) by name and purpose.
|
||||||
|
- `starter_prompt`: LLM-facing first prompt given to begin the scenario. This field can contain instructions that are different from other goals, like "begin by providing the output of the first tool" rather than waiting on user confirmation. (See [goal_choose_agent_type](tools/goal_registry.py) for an example.)
|
||||||
- `example_conversation_history`: LLM-facing sample conversation/interaction regarding the goal. See the existing goals for how to structure this.
|
- `example_conversation_history`: LLM-facing sample conversation/interaction regarding the goal. See the existing goals for how to structure this.
|
||||||
4. Add your new goal to the `goal_list` at the bottom using `goal_list.append(your_super_sweet_new_goal)`
|
4. Add your new goal to a list variable (e.g., `my_category_goals: List[AgentGoal] = [your_super_sweet_new_goal]`)
|
||||||
|
5. Import and extend the goal list in `goals/__init__.py` by adding:
|
||||||
|
- Import: `from goals.my_category import my_category_goals`
|
||||||
|
- Extend: `goal_list.extend(my_category_goals)`
|
||||||
|
|
||||||
## Adding Tools
|
## Adding Native Tools
|
||||||
|
|
||||||
|
Native tools are custom implementations that run directly in your codebase. Use these for business logic specific to your application.
|
||||||
|
|
||||||
### Note on Optional Tools
|
### Note on Optional Tools
|
||||||
Tools can be optional - you can indicate this in the tool listing of goal description (see above section re: goal registry) by adding something like, "This step is optional and can be skipped by moving to the next tool." Here is an example from an older iteration of the `goal_hr_schedule_pto` goal, when it was going to have an optional step to check for existing calendar conflicts:
|
Tools can be optional - you can indicate this in the tool listing of goal description (see above section re: goal registry) by adding something like, "This step is optional and can be skipped by moving to the next tool." Here is an example from an older iteration of the `goal_hr_schedule_pto` goal, when it was going to have an optional step to check for existing calendar conflicts:
|
||||||
@@ -49,7 +58,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)
|
||||||
@@ -58,7 +67,7 @@ Tools should generally return meaningful information and be generally ‘failsaf
|
|||||||
- `description`: LLM-facing description of tool
|
- `description`: LLM-facing description of tool
|
||||||
- `arguments`: These are the _input_ arguments to the tool. Each input argument should be defined as a [ToolArgument](./models/tool_definitions.py). Tools don't have to have arguments but the arguments list has to be declared. If the tool you're creating doesn't have inputs, define arguments as `arguments=[]`
|
- `arguments`: These are the _input_ arguments to the tool. Each input argument should be defined as a [ToolArgument](./models/tool_definitions.py). Tools don't have to have arguments but the arguments list has to be declared. If the tool you're creating doesn't have inputs, define arguments as `arguments=[]`
|
||||||
|
|
||||||
### Create Each Tool
|
### Create Each Native Tool Implementation
|
||||||
- The tools themselves are defined in their own files in `/tools` - you can add a subfolder to organize them, see the hr tools for an example.
|
- The tools themselves are defined in their own files in `/tools` - you can add a subfolder to organize them, see the hr tools for an example.
|
||||||
- The file name and function name will be the same as each other and should also be the same as the name of the tool, without "tool" - so `current_pto_tool` would be `current_pto.py` with a function named `current_pto` within it.
|
- The file name and function name will be the same as each other and should also be the same as the name of the tool, without "tool" - so `current_pto_tool` would be `current_pto.py` with a function named `current_pto` within it.
|
||||||
- The function should have `args: dict` as the input and also return a `dict`
|
- The function should have `args: dict` as the input and also return a `dict`
|
||||||
@@ -66,12 +75,68 @@ Tools should generally return meaningful information and be generally ‘failsaf
|
|||||||
- tools are where the user input+model output becomes deterministic. Add validation here to make sure what the system is doing is valid and acceptable
|
- tools are where the user input+model output becomes deterministic. Add validation here to make sure what the system is doing is valid and acceptable
|
||||||
|
|
||||||
### Add to `tools/__init__.py` and the tool get_handler()
|
### Add to `tools/__init__.py` and the tool get_handler()
|
||||||
- In [tools/__init__.py](./tools/__init__.py), add an import statement for each new tool as well as an applicable return statement in `get_handler`. The tool name here should match the tool name as described in the goal's `description` field.
|
- In [tools/__init__.py](./tools/__init__.py), add an import statement for each new native tool as well as an applicable return statement in `get_handler`. The tool name here should match the tool name as described in the goal's `description` field.
|
||||||
Example:
|
Example:
|
||||||
```
|
```python
|
||||||
if tool_name == "CurrentPTO":
|
if tool_name == "CurrentPTO":
|
||||||
return current_pto
|
return current_pto
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Update workflow_helpers.py
|
||||||
|
- Add your new native tool to the static tools list in [workflows/workflow_helpers.py](workflows/workflow_helpers.py) so it's correctly identified as a native tool rather than an MCP tool.
|
||||||
|
|
||||||
|
## Adding MCP Tools
|
||||||
|
|
||||||
|
MCP (Model Context Protocol) tools are external tools provided by MCP servers. They're useful for integrating with third-party services like Stripe, databases, or APIs without implementing custom code.
|
||||||
|
|
||||||
|
### Configure MCP Server Definition
|
||||||
|
You can either use predefined MCP server configurations from `shared/mcp_config.py` or define custom ones.
|
||||||
|
|
||||||
|
#### Using Predefined Configurations
|
||||||
|
```python
|
||||||
|
from shared.mcp_config import get_stripe_mcp_server_definition
|
||||||
|
|
||||||
|
# In your goal definition:
|
||||||
|
mcp_server_definition=get_stripe_mcp_server_definition(included_tools=["list_products", "create_customer"])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom MCP Server Definition
|
||||||
|
Add an `mcp_server_definition` to your goal:
|
||||||
|
|
||||||
|
```python
|
||||||
|
mcp_server_definition=MCPServerDefinition(
|
||||||
|
name="stripe-mcp",
|
||||||
|
command="npx",
|
||||||
|
args=[
|
||||||
|
"-y",
|
||||||
|
"@stripe/mcp",
|
||||||
|
"--tools=all",
|
||||||
|
f"--api-key={os.getenv('STRIPE_API_KEY')}",
|
||||||
|
],
|
||||||
|
env=None,
|
||||||
|
included_tools=[
|
||||||
|
"list_products",
|
||||||
|
"list_prices",
|
||||||
|
"create_customer",
|
||||||
|
"create_invoice",
|
||||||
|
"create_payment_link",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP Tool Configuration
|
||||||
|
- `name`: Identifier for the MCP server
|
||||||
|
- `command`: Command to start the MCP server (e.g., "npx", "python")
|
||||||
|
- `args`: Arguments to pass to the command
|
||||||
|
- `env`: Environment variables for the server (optional)
|
||||||
|
- `included_tools`: List of specific tools to use from the server (optional - if omitted, all tools are included)
|
||||||
|
|
||||||
|
### How MCP Tools Work
|
||||||
|
- MCP tools are automatically loaded when the workflow starts
|
||||||
|
- They're dynamically converted to `ToolDefinition` objects
|
||||||
|
- The system automatically routes MCP tool calls to the appropriate MCP server
|
||||||
|
- No additional code implementation needed - just configuration
|
||||||
|
|
||||||
## Tool Confirmation
|
## Tool Confirmation
|
||||||
There are three ways to manage confirmation of tool runs:
|
There are three ways to manage confirmation of tool runs:
|
||||||
1. Arguments confirmation box - confirm tool arguments and execution with a button click
|
1. Arguments confirmation box - confirm tool arguments and execution with a button click
|
||||||
@@ -89,12 +154,24 @@ If you really want to wait for user confirmation, record it on the workflow (as
|
|||||||
I recommend exploring all three. For a demo, I would decide if you want the Arguments confirmation in the UI, and if not I'd generally go with option #2 but use #3 for tools that make business sense to confirm, e.g. those tools that take action/write data.
|
I recommend exploring all three. For a demo, I would decide if you want the Arguments confirmation in the UI, and if not I'd generally go with option #2 but use #3 for tools that make business sense to confirm, e.g. those tools that take action/write data.
|
||||||
|
|
||||||
## Add a Goal & Tools Checklist
|
## Add a Goal & Tools Checklist
|
||||||
[ ] Add goal in [/tools/goal_registry.py](tools/goal_registry.py) <br />
|
|
||||||
- [ ] If a new category, add Goal Category to [.env](./.env) and [.env.example](./.env.example) <br />
|
|
||||||
- [ ] don't forget the goal list at the bottom of the [goal_registry.py](tools/goal_registry.py) <br />
|
|
||||||
|
|
||||||
[ ] Add Tools listed in the Goal Registry to the [tool_registry.py](tools/tool_registry.py) <br />
|
### For All Goals:
|
||||||
[ ] Define your tools as Activities in `/tools` <br />
|
- [ ] Create goal file in `/goals/` directory (e.g., `goals/my_category.py`)
|
||||||
[ ] Add your tools to [tool list](tools/__init__.py) in the tool get_handler() <br />
|
- [ ] Add goal to the category's goal list in the file
|
||||||
|
- [ ] Import and extend the goal list in `goals/__init__.py`
|
||||||
|
- [ ] If a new category, add Goal Category to [.env](./.env) and [.env.example](./.env.example)
|
||||||
|
|
||||||
|
### For Native Tools:
|
||||||
|
- [ ] Add native tools to [tool_registry.py](tools/tool_registry.py)
|
||||||
|
- [ ] Implement tool functions in `/tools/` directory
|
||||||
|
- [ ] Add tools to [tools/__init__.py](tools/__init__.py) in the `get_handler()` function
|
||||||
|
- [ ] Add tool names to static tools list in [workflows/workflow_helpers.py](workflows/workflow_helpers.py)
|
||||||
|
|
||||||
|
### For MCP Tools:
|
||||||
|
- [ ] Add `mcp_server_definition` to your goal configuration (use `shared/mcp_config.py` for common servers)
|
||||||
|
- [ ] Ensure MCP server is available and properly configured
|
||||||
|
- [ ] Set required environment variables (API keys, etc.)
|
||||||
|
- [ ] Test MCP server connectivity before running the agent
|
||||||
|
- [ ] If creating new MCP server configs, add them to `shared/mcp_config.py` for reuse
|
||||||
|
|
||||||
And that's it! Happy AI Agent building!
|
And that's it! Happy AI Agent building!
|
||||||
@@ -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):
|
||||||
@@ -39,7 +39,7 @@ This is where you can add probabalistic business logic to
|
|||||||
## 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:
|
||||||
- Drives toward the initial Goal and any subsequent Goals selected by user
|
- Drives toward the initial Goal and any subsequent Goals selected by user
|
||||||
- Decides what to do based on input, such as:
|
- Decides what to do based on input, such as:
|
||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
@@ -8,12 +8,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Multi‑goal agent architecture** with dynamic goal switching (`goal_choose_agent_type`, `ListAgents`, `ChangeGoal`).
|
- **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).
|
- 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.
|
- **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).
|
- 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.
|
- **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`.
|
- **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.
|
- Extensive **docs**: `setup.md`, `architecture.md`, `architecture-decisions.md`, `adding-goals-and-tools.md`, plus new diagrams & assets.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **UI Confirmation Box** is less 'debug' looking and prettier.
|
- **UI Confirmation Box** is less 'debug' looking and prettier.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Contributing to the Temporal AI Agent Project
|
# 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).
|
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
|
## Getting Started
|
||||||
|
|
||||||
@@ -45,8 +45,8 @@ Comprehensive testing is crucial for this project. We use `pytest` and Temporal'
|
|||||||
```
|
```
|
||||||
|
|
||||||
For detailed information on test categories, running specific tests, test environments, coverage, and troubleshooting, please refer to:
|
For detailed information on test categories, running specific tests, test environments, coverage, and troubleshooting, please refer to:
|
||||||
- [TESTING.md](./TESTING.md) (Quick Start and overview)
|
- [testing.md](./testing.md) (Quick Start and overview)
|
||||||
- [tests/README.md](./tests/README.md) (Comprehensive guide, patterns, and best practices)
|
- [tests/README.md](../tests/README.md) (Comprehensive guide, patterns, and best practices)
|
||||||
|
|
||||||
**Ensure all tests pass before submitting a pull request.**
|
**Ensure all tests pass before submitting a pull request.**
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ If you're looking to extend the agent's capabilities:
|
|||||||
1. Create your tool implementation in the `tools/` directory.
|
1. Create your tool implementation in the `tools/` directory.
|
||||||
2. Register your tool and associate it with relevant goals.
|
2. Register your tool and associate it with relevant goals.
|
||||||
For detailed instructions, please see:
|
For detailed instructions, please see:
|
||||||
- [Agent Customization in agents.md](./agents.md#agent-customization)
|
- [Agent Customization in AGENTS.md](../AGENTS.md#agent-customization)
|
||||||
- [Adding Goals and Tools Guide](./adding-goals-and-tools.md)
|
- [Adding Goals and Tools Guide](./adding-goals-and-tools.md)
|
||||||
|
|
||||||
### General Code Changes
|
### General Code Changes
|
||||||
@@ -96,10 +96,10 @@ We welcome suggestions for new features or improvements!
|
|||||||
- Include any potential implementation ideas if you have them.
|
- Include any potential implementation ideas if you have them.
|
||||||
|
|
||||||
## Key Resources
|
## Key Resources
|
||||||
- **Project Overview**: [README.md](./README.md)
|
- **Project Overview**: [README.md](../README.md)
|
||||||
- **Detailed Contribution & Development Guide**: [agents.md](./agents.md)
|
- **Detailed Contribution & Development Guide**: [AGENTS.md](../AGENTS.md)
|
||||||
- **Setup Instructions**: [SETUP.md](./SETUP.md)
|
- **Setup Instructions**: [setup.md](./setup.md)
|
||||||
- **Comprehensive Testing Guide**: [TESTING.md](./TESTING.md) and [tests/README.md](./tests/README.md)
|
- **Comprehensive Testing Guide**: [testing.md](./testing.md) and [tests/README.md](../tests/README.md)
|
||||||
- **System Architecture**: [architecture.md](./architecture.md)
|
- **System Architecture**: [architecture.md](./architecture.md)
|
||||||
- **Architecture Decisions**: [architecture-decisions.md](./architecture-decisions.md)
|
- **Architecture Decisions**: [architecture-decisions.md](./architecture-decisions.md)
|
||||||
- **Customizing Agent Tools and Goals**: [adding-goals-and-tools.md](./adding-goals-and-tools.md)
|
- **Customizing Agent Tools and Goals**: [adding-goals-and-tools.md](./adding-goals-and-tools.md)
|
||||||
@@ -13,6 +13,7 @@ If you want to show confirmations/enable the debugging UI that shows tool args,
|
|||||||
```bash
|
```bash
|
||||||
SHOW_CONFIRM=True
|
SHOW_CONFIRM=True
|
||||||
```
|
```
|
||||||
|
We recommend setting this to `False` in most cases, as it can clutter the conversation with confirmation messages.
|
||||||
|
|
||||||
### Quick Start with Makefile
|
### Quick Start with Makefile
|
||||||
|
|
||||||
@@ -43,17 +44,37 @@ make help
|
|||||||
|
|
||||||
### Manual Setup (Alternative to Makefile)
|
### Manual Setup (Alternative to Makefile)
|
||||||
|
|
||||||
If you prefer to run commands manually, follow these steps:
|
If you prefer to run commands manually, see the sections below for detailed instructions on setting up the backend, frontend, and other components.
|
||||||
|
|
||||||
### 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 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`.
|
**Single Agent Mode (Default)**
|
||||||
|
By default, the agent operates in single-agent mode using a specific goal. If unset, the default is `goal_event_flight_invoice`.
|
||||||
|
|
||||||
|
To set a specific single goal:
|
||||||
|
```bash
|
||||||
|
AGENT_GOAL=goal_event_flight_invoice
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-Agent Mode (Experimental)**
|
||||||
|
The agent also supports an experimental multi-agent mode where users can choose between different agent types during the conversation. To enable this mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AGENT_GOAL=goal_choose_agent_type
|
||||||
|
```
|
||||||
|
|
||||||
|
When using multi-agent mode, you can control which agent categories are available using `GOAL_CATEGORIES` in your `.env` file. If unset, all categories are shown. Available categories include `hr`, `travel-flights`, `travel-trains`, `fin`, `ecommerce`, `mcp-integrations`, and `food`.
|
||||||
|
We recommend starting with `fin`:
|
||||||
```bash
|
```bash
|
||||||
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin
|
GOAL_CATEGORIES=hr,travel-flights,travel-trains,fin
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** Multi-agent mode is experimental and allows switching between different agents mid-conversation, but single-agent mode provides a more focused experience.
|
||||||
|
|
||||||
|
MCP (Model Context Protocol) tools are available for enhanced integration with external services. See the [MCP Tools Configuration](#mcp-tools-configuration) section for setup details.
|
||||||
|
|
||||||
See the section Goal-Specific Tool Configuration below for tool configuration for specific goals.
|
See the section Goal-Specific Tool Configuration below for tool configuration for specific goals.
|
||||||
|
|
||||||
### LLM Configuration
|
### LLM Configuration
|
||||||
@@ -169,6 +190,39 @@ npx vite
|
|||||||
Access the UI at `http://localhost:5173`
|
Access the UI at `http://localhost:5173`
|
||||||
|
|
||||||
|
|
||||||
|
## MCP Tools Configuration
|
||||||
|
|
||||||
|
MCP (Model Context Protocol) tools enable integration with external services without custom implementation. The system automatically handles MCP server lifecycle and tool discovery.
|
||||||
|
|
||||||
|
### Adding MCP Tools to Goals
|
||||||
|
Configure MCP servers in your goal definitions using either:
|
||||||
|
1. Predefined configurations from `shared/mcp_config.py`
|
||||||
|
2. Custom `MCPServerDefinition` objects
|
||||||
|
|
||||||
|
Example using Stripe MCP Server:
|
||||||
|
```python
|
||||||
|
from shared.mcp_config import get_stripe_mcp_server_definition
|
||||||
|
|
||||||
|
mcp_server_definition=get_stripe_mcp_server_definition(
|
||||||
|
included_tools=["list_products", "create_customer", "create_invoice"]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
See the file `goals/stripe_mcp.py` for an example of how to use MCP tools in a an `AgentGoal`.
|
||||||
|
|
||||||
|
### MCP Environment Variables
|
||||||
|
Set required API keys and configuration in your `.env` file:
|
||||||
|
```bash
|
||||||
|
# For Stripe MCP Server
|
||||||
|
STRIPE_API_KEY=sk_test_your_stripe_key_here
|
||||||
|
```
|
||||||
|
`goal_event_flight_invoice` does not require a Stripe key. If `STRIPE_API_KEY` is unset, that scenario falls back to a mock invoice.
|
||||||
|
|
||||||
|
#### Accessing Your Test API Keys
|
||||||
|
It's free to sign up for a Stripe account and generate test keys (no real money is involved). Use the Developers Dashboard to create, reveal, delete, and rotate API keys. Navigate to the API Keys tab in your dashboard or visit [https://dashboard.stripe.com/test/apikeys](https://dashboard.stripe.com/test/apikeys) directly.
|
||||||
|
|
||||||
|
For detailed guidance on adding MCP tools, see [adding-goals-and-tools.md](./adding-goals-and-tools.md).
|
||||||
|
|
||||||
## Goal-Specific Tool Configuration
|
## 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
|
||||||
@@ -177,14 +231,16 @@ Here is configuration guidance for specific goals. Travel and financial goals ha
|
|||||||
|
|
||||||
#### Configuring Agent Goal: goal_event_flight_invoice
|
#### Configuring Agent Goal: goal_event_flight_invoice
|
||||||
* The agent uses a mock function to search for events. This has zero configuration.
|
* The agent uses a mock function to search for events. This has zero configuration.
|
||||||
* By default the agent uses a mock function to search for flights.
|
* **Flight Search**: The agent intelligently handles flight searches:
|
||||||
* If you want to use the real flights API, go to `tools/search_flights.py` and replace the `search_flights` function with `search_flights_real_api` that exists in the same file.
|
* **Default behavior**: If no `RAPIDAPI_KEY` is set, the agent generates realistic flight data with smart pricing based on route type (domestic, international, trans-Pacific)
|
||||||
|
* **Real API (optional)**: To use live flight data, set `RAPIDAPI_KEY` in your `.env` file
|
||||||
* It's free to sign up at [RapidAPI](https://rapidapi.com/apiheya/api/sky-scrapper)
|
* It's free to sign up at [RapidAPI](https://rapidapi.com/apiheya/api/sky-scrapper)
|
||||||
* This api might be slow to respond, so you may want to increase the start to close timeout, `TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT` in `workflows/workflow_helpers.py`
|
* This API might be slow to respond, so you may want to increase the start to close timeout, `TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT` in `workflows/workflow_helpers.py`
|
||||||
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in .env
|
* The smart generation creates realistic pricing (e.g., US-Australia routes $1200-1800, domestic flights $200-800) with appropriate airlines for each region
|
||||||
* It's free to sign up and get a key at [Stripe](https://stripe.com/)
|
* Requires a Stripe key for the `create_invoice` tool. Set this in the `STRIPE_API_KEY` environment variable in `.env`
|
||||||
|
* It's free to sign up and get a key at [Stripe](https://stripe.com/) (test mode only, no real money)
|
||||||
* 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 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`
|
* If you don't have a Stripe key, comment out the `STRIPE_API_KEY` in the `.env` file, and a dummy invoice will be created rather than a Stripe invoice. The function can be found in `tools/create_invoice.py` – this is the default behavior for `goal_event_flight_invoice`.
|
||||||
|
|
||||||
### 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
|
||||||
@@ -195,8 +251,9 @@ NOTE: This goal was developed for an on-stage demo and has failure (and its reso
|
|||||||
* 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.
|
* 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.
|
||||||
* 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`
|
||||||
* 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/) (test mode only)
|
||||||
|
* If the key is missing this goal won't generate a real invoice – only `goal_event_flight_invoice` falls back to a mock invoice
|
||||||
* 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're lazy go to `tools/create_invoice.py` and replace the `create_invoice` function with the mock `create_invoice_example` that exists in the same file.
|
||||||
|
|
||||||
##### Python Search Trains API
|
##### Python Search Trains API
|
||||||
@@ -250,12 +307,19 @@ Make sure you have the mock users you want in (such as yourself) in [the PTO moc
|
|||||||
#### Goals: Ecommerce
|
#### 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).
|
Make sure you have the mock orders you want in (such as those with real tracking numbers) in [the mock orders file](./tools/data/customer_order_data.json).
|
||||||
|
|
||||||
|
### Goal: Food Ordering with MCP Integration (Stripe Payment Processing)
|
||||||
|
- `AGENT_GOAL=goal_food_ordering` - Demonstrates food ordering with Stripe payment processing via MCP
|
||||||
|
- Uses Stripe's MCP Server ([Agent Toolkit](https://github.com/stripe/agent-toolkit/tree/main/modelcontextprotocol)) for payment operations
|
||||||
|
- Requires `STRIPE_API_KEY` in your `.env` file
|
||||||
|
- Requires products in Stripe with metadata key `use_case=food_ordering_demo`. Run `tools/food/setup/create_stripe_products.py` to set up pizza menu items
|
||||||
|
- Example of MCP tool integration without custom implementation
|
||||||
|
- This is an excellent demonstration of MCP (Model Context Protocol) capabilities
|
||||||
|
|
||||||
|
|
||||||
## Customizing the Agent Further
|
## 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)
|
||||||
- `goal_registry.py` contains descriptions of goals and the tools used to achieve them
|
- `goals/` contains descriptions of goals and the tools used to achieve them
|
||||||
- The tools themselves are defined in their own files in `/tools`
|
- The tools themselves are defined in their own files in `/tools`
|
||||||
- Note the mapping in `tools/__init__.py` to each tool
|
|
||||||
|
|
||||||
For more details, check out [adding goals and tools guide](./adding-goals-and-tools.md).
|
For more details, check out [adding goals and tools guide](./adding-goals-and-tools.md).
|
||||||
|
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
## General Agent Enhancements
|
## 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.
|
[ ] 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.
|
[ ] 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.
|
||||||
44
goals/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import os
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import tools.tool_registry as tool_registry
|
||||||
|
from goals.agent_selection import agent_selection_goals
|
||||||
|
from goals.ecommerce import ecommerce_goals
|
||||||
|
from goals.finance import finance_goals
|
||||||
|
from goals.food import food_goals
|
||||||
|
from goals.hr import hr_goals
|
||||||
|
from goals.stripe_mcp import mcp_goals
|
||||||
|
from goals.travel import travel_goals
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
|
||||||
|
goal_list: List[AgentGoal] = []
|
||||||
|
goal_list.extend(agent_selection_goals)
|
||||||
|
goal_list.extend(travel_goals)
|
||||||
|
goal_list.extend(hr_goals)
|
||||||
|
goal_list.extend(finance_goals)
|
||||||
|
goal_list.extend(ecommerce_goals)
|
||||||
|
goal_list.extend(mcp_goals)
|
||||||
|
goal_list.extend(food_goals)
|
||||||
|
|
||||||
|
# for multi-goal, just set list agents as the last tool
|
||||||
|
first_goal_value = os.getenv("AGENT_GOAL")
|
||||||
|
if first_goal_value is None:
|
||||||
|
multi_goal_mode = False # default to single agent mode if unset
|
||||||
|
elif (
|
||||||
|
first_goal_value is not None
|
||||||
|
and first_goal_value.lower() == "goal_choose_agent_type"
|
||||||
|
):
|
||||||
|
multi_goal_mode = True
|
||||||
|
else:
|
||||||
|
multi_goal_mode = False
|
||||||
|
|
||||||
|
if multi_goal_mode:
|
||||||
|
for goal in goal_list:
|
||||||
|
list_agents_found: bool = False
|
||||||
|
for tool in goal.tools:
|
||||||
|
if tool.name == "ListAgents":
|
||||||
|
list_agents_found = True
|
||||||
|
continue
|
||||||
|
if list_agents_found is False:
|
||||||
|
goal.tools.append(tool_registry.list_agents_tool)
|
||||||
|
continue
|
||||||
106
goals/agent_selection.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
import tools.tool_registry as tool_registry
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
|
||||||
|
# Turn on Silly Mode - this should be a description of the persona you'd like the bot to have and can be a single word or a phrase.
|
||||||
|
# Example if you want the bot to be a specific person, like Mario or Christopher Walken, or to describe a specific tone:
|
||||||
|
# SILLY_MODE="Christopher Walken"
|
||||||
|
# SILLY_MODE="belligerent"
|
||||||
|
#
|
||||||
|
# Example if you want it to take on a persona (include 'a'):
|
||||||
|
# SILLY_MODE="a pirate"
|
||||||
|
# Note - this only works with certain LLMs. Grok for sure will stay in character, while OpenAI will not.
|
||||||
|
SILLY_MODE = "off"
|
||||||
|
if SILLY_MODE is not None and SILLY_MODE != "off":
|
||||||
|
silly_prompt = "You are " + SILLY_MODE + ", stay in character at all times. "
|
||||||
|
print("Silly mode is on: " + SILLY_MODE)
|
||||||
|
else:
|
||||||
|
silly_prompt = ""
|
||||||
|
|
||||||
|
starter_prompt_generic = (
|
||||||
|
silly_prompt
|
||||||
|
+ "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||||
|
)
|
||||||
|
|
||||||
|
goal_choose_agent_type = AgentGoal(
|
||||||
|
id="goal_choose_agent_type",
|
||||||
|
category_tag="agent_selection",
|
||||||
|
agent_name="Choose Agent",
|
||||||
|
agent_friendly_description="Choose the type of agent to assist you today. You can always interrupt an existing agent to pick a new one.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.list_agents_tool,
|
||||||
|
tool_registry.change_goal_tool,
|
||||||
|
],
|
||||||
|
description="The user wants to choose which type of agent they will interact with. "
|
||||||
|
"Help the user select an agent by gathering args for the Changegoal tool, in order: "
|
||||||
|
"1. ListAgents: List agents available to interact with. Do not ask for user confirmation for this tool. "
|
||||||
|
"2. ChangeGoal: Change goal of agent "
|
||||||
|
"After these tools are complete, change your goal to the new goal as chosen by the user. ",
|
||||||
|
starter_prompt=silly_prompt
|
||||||
|
+ "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job. List all details of all agents as provided by the output of the first tool included in this goal. ",
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"agent: Here are the currently available agents.",
|
||||||
|
"tool_result: { agents: 'agent_name': 'Event Flight Finder', 'goal_id': 'goal_event_flight_invoice', 'agent_description': 'Helps users find interesting events and arrange travel to them',"
|
||||||
|
"'agent_name': 'Schedule PTO', 'goal_id': 'goal_hr_schedule_pto', 'agent_description': 'Schedule PTO based on your available PTO.' }",
|
||||||
|
"agent: The available agents are: Event Flight Finder and Schedule PTO. \n Which agent would you like to work with? ",
|
||||||
|
"user: I'd like to find an event and book flights using the Event Flight Finder",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on ChangeGoal tool>",
|
||||||
|
"tool_result: { 'new_goal': 'goal_event_flight_invoice' }",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Easter egg - if silly mode = a pirate, include goal_pirate_treasure as a "system" goal so it always shows up.
|
||||||
|
# Can also turn make this goal available by setting the GOAL_CATEGORIES in the env file to include 'pirate', but if SILLY_MODE
|
||||||
|
# is not 'a pirate', the interaction as a whole will be less pirate-y.
|
||||||
|
pirate_category_tag = "pirate"
|
||||||
|
if SILLY_MODE == "a pirate":
|
||||||
|
pirate_category_tag = "system"
|
||||||
|
goal_pirate_treasure = AgentGoal(
|
||||||
|
id="goal_pirate_treasure",
|
||||||
|
category_tag=pirate_category_tag,
|
||||||
|
agent_name="Arrr, Find Me Treasure!",
|
||||||
|
agent_friendly_description="Sail the high seas and find me pirate treasure, ye land lubber!",
|
||||||
|
tools=[
|
||||||
|
tool_registry.give_hint_tool,
|
||||||
|
tool_registry.guess_location_tool,
|
||||||
|
],
|
||||||
|
description="The user wants to find a pirate treasure. "
|
||||||
|
"Help the user gather args for these tools, in a loop, until treasure_found is True or the user requests to be done: "
|
||||||
|
"1. GiveHint: If the user wants a hint regarding the location of the treasure, give them a hint. If they do not want a hint, this tool is optional."
|
||||||
|
"2. GuessLocation: The user guesses where the treasure is, by giving an address. ",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to try to find the treasure",
|
||||||
|
"agent: Sure! Do you want a hint?",
|
||||||
|
"user: yes",
|
||||||
|
"agent: Here is hint number 1!",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on GiveHint tool>",
|
||||||
|
"tool_result: { 'hint_number': 1, 'hint': 'The treasure is in the state of Arizona.' }",
|
||||||
|
"agent: The treasure is in the state of Arizona. Would you like to guess the address of the treasure? ",
|
||||||
|
"user: Yes, address is 123 Main St Phoenix, AZ",
|
||||||
|
"agent: Let's see if you found the treasure...",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on GuessLocation tool>"
|
||||||
|
"tool_result: {'treasure_found':False}",
|
||||||
|
"agent: Nope, that's not the right location! Do you want another hint?",
|
||||||
|
"user: yes",
|
||||||
|
"agent: Here is hint number 2.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on GiveHint tool>",
|
||||||
|
"tool_result: { 'hint_number': 2, 'hint': 'The treasure is in the city of Tucson, AZ.' }",
|
||||||
|
"agent: The treasure is in the city of Tucson, AZ. Would you like to guess the address of the treasure? ",
|
||||||
|
"user: Yes, address is 456 Main St Tucson, AZ",
|
||||||
|
"agent: Let's see if you found the treasure...",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on GuessLocation tool>",
|
||||||
|
"tool_result: {'treasure_found':True}",
|
||||||
|
"agent: Congratulations, Land Lubber, you've found the pirate treasure!",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
agent_selection_goals: List[AgentGoal] = [
|
||||||
|
goal_choose_agent_type,
|
||||||
|
goal_pirate_treasure,
|
||||||
|
]
|
||||||
83
goals/ecommerce.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
import tools.tool_registry as tool_registry
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
|
||||||
|
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||||
|
|
||||||
|
goal_ecomm_order_status = AgentGoal(
|
||||||
|
id="goal_ecomm_order_status",
|
||||||
|
category_tag="ecommerce",
|
||||||
|
agent_name="Check Order Status",
|
||||||
|
agent_friendly_description="Check the status of your order.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.ecomm_get_order,
|
||||||
|
tool_registry.ecomm_track_package,
|
||||||
|
],
|
||||||
|
description="The user wants to learn the status of a specific order. If the status is 'shipped' or 'delivered', they might want to get the package tracking information. To assist with that goal, help the user gather args for these tools in order: "
|
||||||
|
"1. GetOrder: get information about an order"
|
||||||
|
"2. TrackPackage: provide tracking information for the package. This tool is optional and should only be offered if the status is 'shipped' OR 'delivered' - otherwise, skip this tool and do not mention it to the user.",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to know the status of my order",
|
||||||
|
"agent: Sure! I can help you out with that. May I have your order number?",
|
||||||
|
"user: 102 ",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on GetOrderStatus tool>",
|
||||||
|
"tool_result: { 'id': '102', 'summary': 'Red Sunglasses', 'email': 'matt.murdock@nelsonmurdock.com', 'status': 'shipped', 'order_date': '2025-04-01', 'last_order_update': '2025-04-06', 'tracking_id': '039813852990618' }",
|
||||||
|
"agent: Your order 'Red Sunglasses,' placed April 1, 2025, was shipped on April 6, 2025. Would you like to see the tracking inforation?",
|
||||||
|
"user: Yes",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on TrackPackage tool>",
|
||||||
|
"tool_result: { 'scheduled_delivery_date': 'April 30, 2025', 'carrier': 'USPS', 'status_summary': 'Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035.', 'tracking_link': 'https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=12345','last_tracking_update': '2025-03-22T16:14:48'}",
|
||||||
|
"agent: Your package is scheduled to be delivered on April 30, 2025 via USPS. Here is the most recent status from them regarding your package, updated as of March 22: \\n"
|
||||||
|
"Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035. \\n"
|
||||||
|
"You can find the full tracking details here: tracking_link !",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
goal_ecomm_list_orders = AgentGoal(
|
||||||
|
id="goal_ecomm_list_orders",
|
||||||
|
category_tag="ecommerce",
|
||||||
|
agent_name="List All Orders",
|
||||||
|
agent_friendly_description="List all orders for a user.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.ecomm_list_orders,
|
||||||
|
tool_registry.ecomm_get_order,
|
||||||
|
tool_registry.ecomm_track_package,
|
||||||
|
],
|
||||||
|
description="The user wants to see all of their orders. They may want more details about specific orders, and if the status of an order is 'shipped' or 'delivered', they might want to get the package tracking information. To assist with that goal, help the user gather args for this tool: "
|
||||||
|
"1. ListOrders: list orders for a user"
|
||||||
|
" and then offer the following tools, in a loop, until the user indicates they are done:"
|
||||||
|
"2. GetOrder: get information about an order. This tool is optional."
|
||||||
|
"3. TrackPackage: provide tracking information for the package. This tool is optional and should only be offered if the status is 'shipped' OR 'delivered' - otherwise, skip this tool and do not mention it to the user.",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to see all of my orders.",
|
||||||
|
"agent: Sure! I can help you out with that. May I have your email address?",
|
||||||
|
"user: email is bob.johnson@emailzzz.com ",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on ListOrders tool>",
|
||||||
|
"tool_result: a list of orders including [{'id': '102', 'summary': 'Red Sunglasses', 'email': 'matt.murdock@nelsonmurdock.com', 'status': 'shipped', 'order_date': '2025-04-01', 'last_order_update': '2025-04-06', 'tracking_id': '039813852990618' }, { 'id': '103', 'summary': 'Blue Sunglasses', 'email': 'matt.murdock@nelsonmurdock.com', 'status': 'paid', 'order_date': '2025-04-03', 'last_order_update': '2025-04-07' }]",
|
||||||
|
"agent: Your orders are as follows: \\n",
|
||||||
|
"1. Red Sunglasses, ordered 4/1/2025 \\n",
|
||||||
|
"2. Blue Sunglasses, ordered 4/3/2025 \\n",
|
||||||
|
"Would you like more information about any of your orders?"
|
||||||
|
"user: Yes, the Red Sunglasses",
|
||||||
|
"agent: Your order 'Red Sunglasses,' placed April 1, 2025, was shipped on April 6, 2025. Would you like to see the tracking inforation?",
|
||||||
|
"user: Yes",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on TrackPackage tool>",
|
||||||
|
"tool_result: { 'scheduled_delivery_date': 'April 30, 2025', 'carrier': 'USPS', 'status_summary': 'Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035.', 'tracking_link': 'https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=12345','last_tracking_update': '2025-03-22T16:14:48'}",
|
||||||
|
"agent: Your package is scheduled to be delivered on April 30, 2025 via USPS. Here is the most recent status from them regarding your package \\n, updated as of March 22: \\n"
|
||||||
|
"Your item has left our acceptance facility and is in transit to a sorting facility on April 10, 2025 at 7:06 am in IRON RIDGE, WI 53035. \\n"
|
||||||
|
"You can find the full tracking details here: tracking_link ! \\n"
|
||||||
|
"Would you like more information about any of your other orders?",
|
||||||
|
"user: No" "agent: Thanks, and have a great day!",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
ecommerce_goals: List[AgentGoal] = [
|
||||||
|
goal_ecomm_order_status,
|
||||||
|
goal_ecomm_list_orders,
|
||||||
|
]
|
||||||
111
goals/finance.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
import tools.tool_registry as tool_registry
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
|
||||||
|
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||||
|
|
||||||
|
goal_fin_check_account_balances = AgentGoal(
|
||||||
|
id="goal_fin_check_account_balances",
|
||||||
|
category_tag="fin",
|
||||||
|
agent_name="Account Balances",
|
||||||
|
agent_friendly_description="Check your account balances in Checking, Savings, etc.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.financial_check_account_is_valid,
|
||||||
|
tool_registry.financial_get_account_balances,
|
||||||
|
],
|
||||||
|
description="The user wants to check their account balances at the bank or financial institution. To assist with that goal, help the user gather args for these tools in order: "
|
||||||
|
"1. FinCheckAccountIsValid: validate the user's account is valid"
|
||||||
|
"2. FinCheckAccountBalance: Tell the user their account balance at the bank or financial institution",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to check my account balances",
|
||||||
|
"agent: Sure! I can help you out with that. May I have your email address and account number?",
|
||||||
|
"user: email is bob.johnson@emailzzz.com ",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||||
|
"tool_result: { 'status': account valid }",
|
||||||
|
"agent: Great! I can tell you what the your account balances are.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FinCheckAccountBalance tool>",
|
||||||
|
"tool_result: { 'name': Matt Murdock, 'email': matt.murdock@nelsonmurdock.com, 'account_id': 11235, 'checking_balance': 875.40, 'savings_balance': 3200.15, 'bitcoin_balance': 0.1378, 'account_creation_date': 2014-03-10 }",
|
||||||
|
"agent: Your account balances are as follows: \\n "
|
||||||
|
"Checking: $875.40. \\n "
|
||||||
|
"Savings: $3200.15. \\n "
|
||||||
|
"Bitcoin: 0.1378 \\n "
|
||||||
|
"Thanks for being a customer since 2014!",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
goal_fin_move_money = AgentGoal(
|
||||||
|
id="goal_fin_move_money",
|
||||||
|
category_tag="fin",
|
||||||
|
agent_name="Money Movement",
|
||||||
|
agent_friendly_description="Initiate money movement.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.financial_check_account_is_valid,
|
||||||
|
tool_registry.financial_get_account_balances,
|
||||||
|
tool_registry.financial_move_money,
|
||||||
|
],
|
||||||
|
description="The user wants to transfer money in their account at the bank or financial institution. To assist with that goal, help the user gather args for these tools in order: "
|
||||||
|
"1. FinCheckAccountIsValid: validate the user's account is valid"
|
||||||
|
"2. FinCheckAccountBalance: Tell the user their account balance at the bank or financial institution"
|
||||||
|
"3. FinMoveMoney: Initiate money movement (transfer)",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to transfer some money",
|
||||||
|
"agent: Sure! I can help you out with that. May I have account number and email address?",
|
||||||
|
"user: my account number is 11235 and my email address is matt.murdock@nelsonmurdock.com",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||||
|
"tool_result: { 'status': account valid }",
|
||||||
|
"agent: Great! Here are your account balances:",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FinCheckAccountBalance tool>",
|
||||||
|
"tool_result: { 'name': Matt Murdock, 'email': matt.murdock@nelsonmurdock.com, 'account_id': 11235, 'checking_balance': 875.40, 'savings_balance': 3200.15, 'bitcoin_balance': 0.1378, 'account_creation_date': 2014-03-10 }",
|
||||||
|
"agent: Your account balances are as follows: \\n "
|
||||||
|
"Checking: $875.40. \\n "
|
||||||
|
"Savings: $3200.15. \\n "
|
||||||
|
"Bitcoint: 0.1378 \\n "
|
||||||
|
"agent: how much would you like to move, from which account type, and to which account number?",
|
||||||
|
"user: I'd like to move $500 from savings to account number #56789",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FinMoveMoney tool>",
|
||||||
|
"tool_result: { 'status': money movement complete, 'confirmation id': 333421, 'new_balance': $2700.15 }",
|
||||||
|
"agent: Money movement completed! New account balance: $2700.15. Your confirmation id is 333421. ",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
goal_fin_loan_application = AgentGoal(
|
||||||
|
id="goal_fin_loan_application",
|
||||||
|
category_tag="fin",
|
||||||
|
agent_name="Easy Loan",
|
||||||
|
agent_friendly_description="Initiate a simple loan application.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.financial_check_account_is_valid,
|
||||||
|
tool_registry.financial_submit_loan_approval,
|
||||||
|
],
|
||||||
|
description="The user wants to apply for a loan at the financial institution. To assist with that goal, help the user gather args for these tools in order: "
|
||||||
|
"1. FinCheckAccountIsValid: validate the user's account is valid"
|
||||||
|
"2. FinCheckAccountSubmitLoanApproval: submit the loan for approval",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to apply for a loan",
|
||||||
|
"agent: Sure! I can help you out with that. May I have account number and email address to validate your account?",
|
||||||
|
"user: account number is 11235813",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FincheckAccountIsValid tool>",
|
||||||
|
"tool_result: { 'status': account valid }",
|
||||||
|
"agent: Great! We've validated your account. What will the loan amount be?",
|
||||||
|
"user: I'd like a loan for $500",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FinCheckAccountSubmitLoanApproval tool>",
|
||||||
|
"tool_result: { 'status': submitted, 'detailed_status': loan application is submitted and initial validation is complete, 'confirmation id': 333421, 'next_step': You'll receive a confirmation for final approval in three business days }",
|
||||||
|
"agent: I have submitted your loan application process and the initial validation is successful. Your application ID is 333421. You'll receive a notification for final approval from us in three business days. ",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
finance_goals: List[AgentGoal] = [
|
||||||
|
goal_fin_check_account_balances,
|
||||||
|
goal_fin_move_money,
|
||||||
|
goal_fin_loan_application,
|
||||||
|
]
|
||||||
84
goals/food.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
import tools.tool_registry as tool_registry
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
from shared.mcp_config import get_stripe_mcp_server_definition
|
||||||
|
|
||||||
|
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||||
|
|
||||||
|
goal_food_ordering = AgentGoal(
|
||||||
|
id="goal_food_ordering",
|
||||||
|
category_tag="food",
|
||||||
|
agent_name="Food Ordering Assistant",
|
||||||
|
agent_friendly_description="Order food from Tony's Pizza Palace using Stripe for payment processing. Browse menu, add items to your order, and check out securely. Please ensure context carries over between tool runs.",
|
||||||
|
tools=[tool_registry.food_add_to_cart_tool],
|
||||||
|
mcp_server_definition=get_stripe_mcp_server_definition(
|
||||||
|
included_tools=[
|
||||||
|
"list_products",
|
||||||
|
"list_prices",
|
||||||
|
"create_customer",
|
||||||
|
"create_invoice",
|
||||||
|
"create_invoice_item",
|
||||||
|
"finalize_invoice",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
description="The user wants to order food from Tony's Pizza Palace. "
|
||||||
|
"First, help the user browse the menu by calling list_products. "
|
||||||
|
"When they express interest in items, get pricing using list_prices. "
|
||||||
|
"Add items to their cart using AddToCart as they decide - the order doesn't matter, multiple items can be added. "
|
||||||
|
"After they're done selecting items, get their customer details and create a Stripe customer. "
|
||||||
|
"For checkout: 1) create_invoice, 2) create_invoice_item for each individual item (IMPORTANT: create_invoice_item does NOT accept quantity parameter - call it once per item, so if user wants 2 pizzas, call create_invoice_item twice with the same price), "
|
||||||
|
"3) finalize_invoice. The finalized invoice will contain a hosted_invoice_url for payment.",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I want to order food.",
|
||||||
|
"agent: Welcome to Tony's Pizza Palace! I can help you browse our menu, add items to your cart, and process payment. Let me show you what we have available.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on list_products tool with limit=100 and product.metadata.use_case == 'food_ordering_demo'>",
|
||||||
|
'tool_result: {"products": [{"id": "prod_pepperoni", "name": "Pepperoni Pizza", "description": "A timeless classic featuring pepperoni"}, {"id": "prod_margherita", "name": "Margherita Pizza", "description": "Fresh mozzarella and basil"}, {"id": "prod_garlic_bread", "name": "Garlic Bread", "description": "Warm crusty bread with garlic butter"}, {"id": "prod_soda", "name": "Soda", "description": "A large bottle of cola"}]}',
|
||||||
|
"agent: We have Pepperoni Pizza, Margherita Pizza, Garlic Bread, and Soda available. What would you like to order?",
|
||||||
|
"user: I'd like 2 pepperoni pizzas and a soda.",
|
||||||
|
"agent: Excellent! Let me get the pricing for the pepperoni pizzas first so you know the cost.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on list_prices tool with product='prod_pepperoni'>",
|
||||||
|
'tool_result: {"prices": [{"id": "price_pepperoni_large", "product": "prod_pepperoni", "unit_amount": 2299, "currency": "usd"}]}',
|
||||||
|
"agent: Pepperoni Pizza is $22.99 each. Now let me check the soda pricing:",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on list_prices tool with product='prod_soda'>",
|
||||||
|
'tool_result: {"prices": [{"id": "price_soda", "product": "prod_soda", "unit_amount": 250, "currency": "usd"}]}',
|
||||||
|
"agent: And Soda is $2.50. So 2 Pepperoni Pizzas ($22.99 each) plus 1 Soda ($2.50) = $48.48 total. What's your email address to add these items to your cart?",
|
||||||
|
"user: jane.smith@example.com",
|
||||||
|
"agent: Perfect! Adding 2 Pepperoni Pizzas to your cart for jane.smith@example.com. Note: I'm using quantity=2 since you want 2 pizzas.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on AddToCart tool with customer_email='jane.smith@example.com', item_name='Pepperoni Pizza', item_price=22.99, quantity=2, stripe_product_id='prod_pepperoni'>",
|
||||||
|
'tool_result: {"status": "success", "message": "Added 2 x Pepperoni Pizza ($22.99) to cart for jane.smith@example.com", "item_added": {"quantity": 2, "name": "Pepperoni Pizza"}}',
|
||||||
|
"agent: Great! Added 2 Pepperoni Pizzas to your cart. Now adding the Soda (quantity=1):",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on AddToCart tool with customer_email='jane.smith@example.com', item_name='Soda', item_price=2.50, quantity=1, stripe_product_id='prod_soda'>",
|
||||||
|
'tool_result: {"status": "success", "message": "Added 1 x Soda ($2.50) to cart for jane.smith@example.com", "item_added": {"quantity": 1, "name": "Soda"}}',
|
||||||
|
"agent: Perfect! Added 1 Soda to your cart. Your cart now contains: 2 Pepperoni Pizzas ($22.99 each) and 1 Soda ($2.50). What else would you like to order?",
|
||||||
|
"user: Nothing, that's all.",
|
||||||
|
"agent: Great! Your order is complete with 2 Pepperoni Pizzas and 1 Soda for a total of $48.48. Now let's proceed to checkout. I'll need your full name to create your customer profile.",
|
||||||
|
"user: My name is Jane Smith.",
|
||||||
|
"agent: Thank you Jane! Creating your Stripe customer profile with name and email.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_customer tool with name='Jane Smith' and email='jane.smith@example.com'>",
|
||||||
|
'tool_result: {"customer": {"id": "cus_ExAmPlE12345", "name": "Jane Smith", "email": "jane.smith@example.com"}}',
|
||||||
|
"agent: Customer profile created! Now I'll create a draft invoice for your order.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_invoice tool with customer='cus_ExAmPlE12345'>",
|
||||||
|
'tool_result: {"invoice": {"id": "in_InvOicE54321", "status": "draft", "customer": "cus_ExAmPlE12345"}}',
|
||||||
|
"agent: Invoice created! Now adding your first Pepperoni Pizza to the invoice.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_pepperoni_large'>",
|
||||||
|
'tool_result: {"status": "success", "invoice_item": {"id": "ii_pizza1"}}',
|
||||||
|
"agent: First pizza added! Now adding your second Pepperoni Pizza to the invoice.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_pepperoni_large'>",
|
||||||
|
'tool_result: {"status": "success", "invoice_item": {"id": "ii_pizza2"}}',
|
||||||
|
"agent: Both pizzas added! Now adding your Soda to the invoice.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_soda'>",
|
||||||
|
'tool_result: {"status": "success", "invoice_item": {"id": "ii_soda"}}',
|
||||||
|
"agent: All items added to invoice! Now finalizing to generate the payment link.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on finalize_invoice tool with invoice='in_InvOicE54321'>",
|
||||||
|
'tool_result: {"invoice": {"id": "in_InvOicE54321", "status": "open", "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RKeJRMlw?s=ap", "amount_due": 4848}}',
|
||||||
|
"agent: Perfect! Your order is complete with 2 Pepperoni Pizzas and 1 Soda. Total: $48.48. Pay securely here: https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RKeJRMlw?s=ap\\\\n\\\\nThank you for ordering from Tony's Pizza Palace!",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
food_goals: List[AgentGoal] = [
|
||||||
|
goal_food_ordering,
|
||||||
|
]
|
||||||
97
goals/hr.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
import tools.tool_registry as tool_registry
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
|
||||||
|
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||||
|
|
||||||
|
goal_hr_schedule_pto = AgentGoal(
|
||||||
|
id="goal_hr_schedule_pto",
|
||||||
|
category_tag="hr",
|
||||||
|
agent_name="Schedule PTO",
|
||||||
|
agent_friendly_description="Schedule PTO based on your available PTO.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.current_pto_tool,
|
||||||
|
tool_registry.future_pto_calc_tool,
|
||||||
|
tool_registry.book_pto_tool,
|
||||||
|
],
|
||||||
|
description="The user wants to schedule paid time off (PTO) after today's date. To assist with that goal, help the user gather args for these tools in order: "
|
||||||
|
"1. CurrentPTO: Tell the user how much PTO they currently have "
|
||||||
|
"2. FuturePTOCalc: Tell the user how much PTO they will have as of the prospective future date "
|
||||||
|
"3. BookPTO: Book PTO after user types 'yes'",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to schedule some time off",
|
||||||
|
"agent: Sure! Let's start by determining how much PTO you currently have. May I have your email address?",
|
||||||
|
"user: bob.johnson@emailzzz.com",
|
||||||
|
"agent: Great! I can tell you how much PTO you currently have accrued.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on CurrentPTO tool>",
|
||||||
|
"tool_result: { 'num_hours': 400, 'num_days': 50 }",
|
||||||
|
"agent: You have 400 hours, or 50 days, of PTO available. What dates would you like to take your time off? ",
|
||||||
|
"user: Dec 1 through Dec 5",
|
||||||
|
"agent: Let's check if you'll have enough PTO accrued by Dec 1 of this year to accomodate that.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FuturePTO tool>"
|
||||||
|
'tool_result: {"enough_pto": True, "pto_hrs_remaining_after": 410}',
|
||||||
|
"agent: You do in fact have enough PTO to accommodate that, and will have 410 hours remaining after you come back. Do you want to book the PTO? ",
|
||||||
|
"user: yes ",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on BookPTO tool>",
|
||||||
|
'tool_result: { "status": "success" }',
|
||||||
|
"agent: PTO successfully booked! ",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
goal_hr_check_pto = AgentGoal(
|
||||||
|
id="goal_hr_check_pto",
|
||||||
|
category_tag="hr",
|
||||||
|
agent_name="Check PTO Amount",
|
||||||
|
agent_friendly_description="Check your available PTO.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.current_pto_tool,
|
||||||
|
],
|
||||||
|
description="The user wants to check their paid time off (PTO) after today's date. To assist with that goal, help the user gather args for these tools in order: "
|
||||||
|
"1. CurrentPTO: Tell the user how much PTO they currently have ",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to check my time off amounts at the current time",
|
||||||
|
"agent: Sure! I can help you out with that. May I have your email address?",
|
||||||
|
"user: bob.johnson@emailzzz.com",
|
||||||
|
"agent: Great! I can tell you how much PTO you currently have accrued.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on CurrentPTO tool>",
|
||||||
|
"tool_result: { 'num_hours': 400, 'num_days': 50 }",
|
||||||
|
"agent: You have 400 hours, or 50 days, of PTO available.",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
goal_hr_check_paycheck_bank_integration_status = AgentGoal(
|
||||||
|
id="goal_hr_check_paycheck_bank_integration_status",
|
||||||
|
category_tag="hr",
|
||||||
|
agent_name="Check paycheck deposit status",
|
||||||
|
agent_friendly_description="Check your integration between your employer and your financial institution.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.paycheck_bank_integration_status_check,
|
||||||
|
],
|
||||||
|
description="The user wants to check their bank integration used to deposit their paycheck. To assist with that goal, help the user gather args for these tools in order: "
|
||||||
|
"1. CheckPayBankStatus: Tell the user the status of their paycheck bank integration ",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to check paycheck bank integration",
|
||||||
|
"agent: Sure! I can help you out with that. May I have your email address?",
|
||||||
|
"user: bob.johnson@emailzzz.com",
|
||||||
|
"agent: Great! I can tell you what the status is for your paycheck bank integration.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on CheckPayBankStatus tool>",
|
||||||
|
"tool_result: { 'status': connected }",
|
||||||
|
"agent: Your paycheck bank deposit integration is properly connected.",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
hr_goals: List[AgentGoal] = [
|
||||||
|
goal_hr_schedule_pto,
|
||||||
|
goal_hr_check_pto,
|
||||||
|
goal_hr_check_paycheck_bank_integration_status,
|
||||||
|
]
|
||||||
37
goals/stripe_mcp.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
from shared.mcp_config import get_stripe_mcp_server_definition
|
||||||
|
|
||||||
|
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||||
|
|
||||||
|
goal_mcp_stripe = AgentGoal(
|
||||||
|
id="goal_mcp_stripe",
|
||||||
|
category_tag="mcp-integrations",
|
||||||
|
agent_name="Stripe MCP Agent",
|
||||||
|
agent_friendly_description="Manage Stripe operations via MCP",
|
||||||
|
tools=[], # Will be populated dynamically
|
||||||
|
mcp_server_definition=get_stripe_mcp_server_definition(included_tools=[]),
|
||||||
|
description="Help manage Stripe operations for customer and product data by using the customers.read and products.read tools.",
|
||||||
|
starter_prompt="Welcome! I can help you read Stripe customer and product information.",
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"agent: Welcome! I can help you read Stripe customer and product information. What would you like to do first?",
|
||||||
|
"user: what customers are there?",
|
||||||
|
"agent: I'll check for customers now.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on customers.read tool>",
|
||||||
|
'tool_result: { "customers": [{"id": "cus_abc", "name": "Customer A"}, {"id": "cus_xyz", "name": "Customer B"}] }',
|
||||||
|
"agent: I found two customers: Customer A and Customer B. Can I help with anything else?",
|
||||||
|
"user: what products exist?",
|
||||||
|
"agent: Let me get the list of products for you.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on products.read tool>",
|
||||||
|
'tool_result: { "products": [{"id": "prod_123", "name": "Gold Plan"}, {"id": "prod_456", "name": "Silver Plan"}] }',
|
||||||
|
"agent: I found two products: Gold Plan and Silver Plan.",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
mcp_goals: List[AgentGoal] = [
|
||||||
|
goal_mcp_stripe,
|
||||||
|
]
|
||||||
96
goals/travel.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
import tools.tool_registry as tool_registry
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
|
|
||||||
|
starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."
|
||||||
|
|
||||||
|
goal_match_train_invoice = AgentGoal(
|
||||||
|
id="goal_match_train_invoice",
|
||||||
|
category_tag="travel-trains",
|
||||||
|
agent_name="UK Premier League Match Trip Booking",
|
||||||
|
agent_friendly_description="Book a trip to a city in the UK around the dates of a premier league match.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.search_fixtures_tool,
|
||||||
|
tool_registry.search_trains_tool,
|
||||||
|
tool_registry.book_trains_tool,
|
||||||
|
tool_registry.create_invoice_tool,
|
||||||
|
],
|
||||||
|
description="The user wants to book a trip to a city in the UK around the dates of a premier league match. "
|
||||||
|
"Help the user find a premier league match to attend, search and book trains for that match and offers to invoice them for the cost of train tickets. "
|
||||||
|
"The user lives in London. Premier league fixtures may be mocked data, so don't worry about valid season dates and teams. "
|
||||||
|
"Gather args for these tools in order, ensuring you move the user from one tool to the next: "
|
||||||
|
"1. SearchFixtures: Search for fixtures for a team within a specified date range. The user might ask questions about the matches dates and locations to decide on where to go. "
|
||||||
|
"2. SearchTrains: Search for trains to the city of the match. Ensure you list them for the customer to choose from "
|
||||||
|
"3. BookTrains: Book the train tickets, used to invoice the user for the cost of the train tickets "
|
||||||
|
"4. CreateInvoice: Invoices the user for the cost of train tickets, with total and details inferred from the conversation history ",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to travel to a premier league match",
|
||||||
|
"agent: Sure! Let's start by finding a match you'd like to attend. I know about Premier League fixtures in the UK. Could you tell me which team and what date range you're interested in?"
|
||||||
|
"user: Wolves between mid-April and the first week of May, please."
|
||||||
|
"agent: Great! Let's find fixtures for Wolverhampton Wanderers FC between 2025-04-15 and 2025-05-05."
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on SearchFixtures tool, passing the full team name as an input>",
|
||||||
|
'tool_result: SearchFixtures responds in this JSON format and the results are parsed by the agent below {"fixtures": [{"awayTeam": "West Ham United FC","date": "2025-04-01","homeTeam": "Wolverhampton Wanderers FC"},...,',
|
||||||
|
"agent: I found 4 Wolves matches in May 2025: 1. Man City vs Wolves (Away) - May 3 2. Wolves vs Brighton (Home) - May 10 3. Crystal Palace vs Wolves (Away) - May 18 4. Wolves vs Brentford (Home) - May 25 Which match would you like to attend? I'll help you find trains from London.",
|
||||||
|
"user: How about the Man City match",
|
||||||
|
"agent: Great! Let's find trains to Manchester and back for the match.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on SearchTrains tool>",
|
||||||
|
'tool_result: SearchTrains responds in this JSON format and the results are parsed by the agent below: {"journeys": [{"arrival": "Manchester","arrival_time": "2025-04-18T12:11","departure": "London","departure_time": "2025-04-18T11:00","id": "T8049","platform": "1","price": 73.5,"type": "outbound"},',
|
||||||
|
"agent: I found several train options for your journey to Manchester including outbound options on May 9 at 11am, 3:30pm and 4:30pm and return options on May 11 at 9am, 11am and 1pm. Which trains would you like to book?",
|
||||||
|
"user: I'd like the 11am outbound and 3:30pm return",
|
||||||
|
"agent: Sounds good, let's book train tickets for the 11am outbound and 3:30pm return trains on May 9 and May 11, one day before and after the match.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on BookTrains tool>",
|
||||||
|
'tool_result: BookTrains responds in this JSON format and the results are parsed by the agent belowg [{"booking_reference": "BR10879","journey_id": "T9532,T1027","status": "confirmed"}]',
|
||||||
|
"agent: Your train tickets have been booked with the reference ABCD1234. Are you ready to be invoiced for the total cost of the train tickets?",
|
||||||
|
"user: Yes, pelase invoice me.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on CreateInvoice tool which includes details of the train journey, the match, and the total cost>",
|
||||||
|
'tool_result: CreateInvoice responds in this JSON format and the results are parsed by the agent below (including a link to the invoice): {"invoiceStatus": "open","invoiceURL": "https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RLeJRMlw?s=ap","reference": "F9B01CD7-0001"}',
|
||||||
|
"agent: Great! I've generated your invoice for your trains to the Manchester City match on the 10th of May. You can view and pay your invoice at this link: https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RLeJRMlw?s=ap <this is the end of the chat>",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
goal_event_flight_invoice = AgentGoal(
|
||||||
|
id="goal_event_flight_invoice",
|
||||||
|
category_tag="travel-flights",
|
||||||
|
agent_name="Australia and New Zealand Event Flight Booking",
|
||||||
|
agent_friendly_description="Book a trip to a city in Australia or New Zealand around the dates of events in that city.",
|
||||||
|
tools=[
|
||||||
|
tool_registry.find_events_tool,
|
||||||
|
tool_registry.search_flights_tool,
|
||||||
|
tool_registry.create_invoice_tool,
|
||||||
|
],
|
||||||
|
description="Help the user gather args for these tools in order: "
|
||||||
|
"1. FindEvents: Find an event in Australia or New Zealand to travel to "
|
||||||
|
"2. SearchFlights: search for a flight around the event dates "
|
||||||
|
"3. CreateInvoice: Create a simple invoice for the cost of that flight ",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I'd like to travel to an event",
|
||||||
|
"agent: Sure! Let's start by finding an event you'd like to attend. I know about events in Australia and New Zealand cities. Could you tell me which city and month you're interested in?",
|
||||||
|
"user: sydney in may please",
|
||||||
|
"agent: Great! Let's find an events in Sydney in May.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on FindEvents tool>",
|
||||||
|
"tool_result: { 'event_name': 'Vivid Sydney', 'event_date': '2023-05-01' }",
|
||||||
|
"agent: Found an event! There's Vivid Sydney on May 1 2025, ending on May 14 2025. Would you like to search for flights around these dates?",
|
||||||
|
"user: Yes, please",
|
||||||
|
"agent: Let's search for flights around these dates. Could you provide your departure city?",
|
||||||
|
"user: San Francisco",
|
||||||
|
"agent: Thanks, searching for flights from San Francisco to Sydney around 2023-02-25 to 2023-02-28.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on SearchFlights tool>"
|
||||||
|
'tool_result: results including {"flight_number": "CX101", "return_flight_number": "CX102", "price": 850.0}',
|
||||||
|
"agent: Found some flights! The cheapest is CX101 for $850. Would you like to generate an invoice for this flight?",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on CreateInvoice tool>",
|
||||||
|
'tool_result: { "status": "success", "invoice": { "flight_number": "CX101", "amount": 850.0 }, invoiceURL: "https://example.com/invoice" }',
|
||||||
|
"agent: Invoice generated! Here's the link: https://example.com/invoice",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
travel_goals: List[AgentGoal] = [
|
||||||
|
goal_match_train_invoice,
|
||||||
|
goal_event_flight_invoice,
|
||||||
|
]
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MCPServerDefinition:
|
||||||
|
"""Definition for an MCP (Model Context Protocol) server connection"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
command: str
|
||||||
|
args: List[str]
|
||||||
|
env: Optional[Dict[str, str]] = None
|
||||||
|
connection_type: str = "stdio"
|
||||||
|
included_tools: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -26,3 +38,4 @@ class AgentGoal:
|
|||||||
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 to help the AI agent understand the context of the conversation"
|
example_conversation_history: str = "Example conversation history to help the AI agent understand the context of the conversation"
|
||||||
|
mcp_server_definition: Optional[MCPServerDefinition] = None
|
||||||
|
|||||||
419
poetry.lock
generated
@@ -191,6 +191,20 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
|
|||||||
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||||
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "authlib"
|
||||||
|
version = "1.6.0"
|
||||||
|
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d"},
|
||||||
|
{file = "authlib-1.6.0.tar.gz", hash = "sha256:4367d32031b7af175ad3a323d571dc7257b7099d55978087ceae4a0d88cd3210"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cryptography = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "23.12.1"
|
version = "23.12.1"
|
||||||
@@ -262,6 +276,85 @@ files = [
|
|||||||
{file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
|
{file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "1.17.1"
|
||||||
|
description = "Foreign Function Interface for Python calling C code."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
|
||||||
|
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pycparser = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "3.4.1"
|
version = "3.4.1"
|
||||||
@@ -388,6 +481,65 @@ files = [
|
|||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "45.0.3"
|
||||||
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19"},
|
||||||
|
{file = "cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
|
||||||
|
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||||
|
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"]
|
||||||
|
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
||||||
|
sdist = ["build (>=1.0.0)"]
|
||||||
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
|
test = ["certifi (>=2024)", "cryptography-vectors (==45.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||||
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "distro"
|
name = "distro"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -433,6 +585,30 @@ typing-extensions = ">=4.8.0"
|
|||||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
|
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastmcp"
|
||||||
|
version = "2.7.0"
|
||||||
|
description = "The fast, Pythonic way to build MCP servers."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "fastmcp-2.7.0-py3-none-any.whl", hash = "sha256:5e0827a37bc71656edebb5f217423ce6f838d8f0e42f79c9f803349c0366fc80"},
|
||||||
|
{file = "fastmcp-2.7.0.tar.gz", hash = "sha256:6a081400ed46e1b74fbda3f5b7f806180f4091b7bf36bd4c52d7074934767004"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
authlib = ">=1.5.2"
|
||||||
|
exceptiongroup = ">=1.2.2"
|
||||||
|
httpx = ">=0.28.1"
|
||||||
|
mcp = ">=1.9.2,<2.0.0"
|
||||||
|
openapi-pydantic = ">=0.5.1"
|
||||||
|
python-dotenv = ">=1.1.0"
|
||||||
|
rich = ">=13.9.4"
|
||||||
|
typer = ">=0.15.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
websockets = ["websockets (>=15.0.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.18.0"
|
version = "3.18.0"
|
||||||
@@ -699,13 +875,13 @@ trio = ["trio (>=0.22.0,<1.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx"
|
name = "httpx"
|
||||||
version = "0.27.2"
|
version = "0.28.1"
|
||||||
description = "The next generation HTTP client."
|
description = "The next generation HTTP client."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
|
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||||
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
|
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -713,7 +889,6 @@ anyio = "*"
|
|||||||
certifi = "*"
|
certifi = "*"
|
||||||
httpcore = "==1.*"
|
httpcore = "==1.*"
|
||||||
idna = "*"
|
idna = "*"
|
||||||
sniffio = "*"
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
brotli = ["brotli", "brotlicffi"]
|
brotli = ["brotli", "brotlicffi"]
|
||||||
@@ -722,6 +897,17 @@ http2 = ["h2 (>=3,<5)"]
|
|||||||
socks = ["socksio (==1.*)"]
|
socks = ["socksio (==1.*)"]
|
||||||
zstd = ["zstandard (>=0.18.0)"]
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx-sse"
|
||||||
|
version = "0.4.0"
|
||||||
|
description = "Consume Server-Sent Event (SSE) messages with HTTPX."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"},
|
||||||
|
{file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "huggingface-hub"
|
name = "huggingface-hub"
|
||||||
version = "0.31.4"
|
version = "0.31.4"
|
||||||
@@ -995,6 +1181,30 @@ extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.
|
|||||||
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.3)", "litellm-proxy-extras (==0.1.21)", "mcp (==1.5.0)", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"]
|
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.3)", "litellm-proxy-extras (==0.1.21)", "mcp (==1.5.0)", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"]
|
||||||
utils = ["numpydoc"]
|
utils = ["numpydoc"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
|
||||||
|
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mdurl = ">=0.1,<1.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
|
||||||
|
code-style = ["pre-commit (>=3.0,<4.0)"]
|
||||||
|
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
|
||||||
|
linkify = ["linkify-it-py (>=1,<3)"]
|
||||||
|
plugins = ["mdit-py-plugins"]
|
||||||
|
profiling = ["gprof2dot"]
|
||||||
|
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
|
||||||
|
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markupsafe"
|
name = "markupsafe"
|
||||||
version = "3.0.2"
|
version = "3.0.2"
|
||||||
@@ -1065,6 +1275,44 @@ files = [
|
|||||||
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mcp"
|
||||||
|
version = "1.9.3"
|
||||||
|
description = "Model Context Protocol SDK"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "mcp-1.9.3-py3-none-any.whl", hash = "sha256:69b0136d1ac9927402ed4cf221d4b8ff875e7132b0b06edd446448766f34f9b9"},
|
||||||
|
{file = "mcp-1.9.3.tar.gz", hash = "sha256:587ba38448e81885e5d1b84055cfcc0ca56d35cd0c58f50941cab01109405388"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
anyio = ">=4.5"
|
||||||
|
httpx = ">=0.27"
|
||||||
|
httpx-sse = ">=0.4"
|
||||||
|
pydantic = ">=2.7.2,<3.0.0"
|
||||||
|
pydantic-settings = ">=2.5.2"
|
||||||
|
python-multipart = ">=0.0.9"
|
||||||
|
sse-starlette = ">=1.6.1"
|
||||||
|
starlette = ">=0.27"
|
||||||
|
uvicorn = {version = ">=0.23.1", markers = "sys_platform != \"emscripten\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"]
|
||||||
|
rich = ["rich (>=13.9.4)"]
|
||||||
|
ws = ["websockets (>=15.0.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
description = "Markdown URL utilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||||
|
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.4.4"
|
version = "6.4.4"
|
||||||
@@ -1336,6 +1584,20 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
|||||||
realtime = ["websockets (>=13,<16)"]
|
realtime = ["websockets (>=13,<16)"]
|
||||||
voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
|
voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openapi-pydantic"
|
||||||
|
version = "0.5.1"
|
||||||
|
description = "Pydantic OpenAPI schema implementation"
|
||||||
|
optional = false
|
||||||
|
python-versions = "<4.0,>=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146"},
|
||||||
|
{file = "openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pydantic = ">=1.8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "24.2"
|
version = "24.2"
|
||||||
@@ -1602,6 +1864,17 @@ files = [
|
|||||||
{file = "protobuf-5.29.2.tar.gz", hash = "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e"},
|
{file = "protobuf-5.29.2.tar.gz", hash = "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "2.22"
|
||||||
|
description = "C parser in Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||||
|
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.10.4"
|
version = "2.10.4"
|
||||||
@@ -1734,6 +2007,43 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-settings"
|
||||||
|
version = "2.9.1"
|
||||||
|
description = "Settings management using Pydantic"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"},
|
||||||
|
{file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pydantic = ">=2.7.0"
|
||||||
|
python-dotenv = ">=0.21.0"
|
||||||
|
typing-inspection = ">=0.4.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"]
|
||||||
|
azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
|
||||||
|
gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
|
||||||
|
toml = ["tomli (>=2.0.1)"]
|
||||||
|
yaml = ["pyyaml (>=6.0.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.1"
|
||||||
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
|
||||||
|
{file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
windows-terminal = ["colorama (>=0.4.6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyogrio"
|
name = "pyogrio"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -1878,18 +2188,29 @@ six = ">=1.5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
|
{file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"},
|
||||||
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
|
{file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
cli = ["click (>=5.0)"]
|
cli = ["click (>=5.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-multipart"
|
||||||
|
version = "0.0.20"
|
||||||
|
description = "A streaming multipart parser for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"},
|
||||||
|
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2025.1"
|
version = "2025.1"
|
||||||
@@ -2103,6 +2424,25 @@ urllib3 = ">=1.21.1,<3"
|
|||||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "14.0.0"
|
||||||
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8.0"
|
||||||
|
files = [
|
||||||
|
{file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"},
|
||||||
|
{file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
markdown-it-py = ">=2.2.0"
|
||||||
|
pygments = ">=2.13.0,<3.0.0"
|
||||||
|
typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpds-py"
|
name = "rpds-py"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
@@ -2303,6 +2643,17 @@ numpy = ">=1.14,<3"
|
|||||||
docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
|
docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
|
||||||
test = ["pytest", "pytest-cov"]
|
test = ["pytest", "pytest-cov"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellingham"
|
||||||
|
version = "1.5.4"
|
||||||
|
description = "Tool to Detect Surrounding Shell"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
|
||||||
|
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -2325,6 +2676,25 @@ files = [
|
|||||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sse-starlette"
|
||||||
|
version = "2.1.3"
|
||||||
|
description = "SSE plugin for Starlette"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772"},
|
||||||
|
{file = "sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
anyio = "*"
|
||||||
|
starlette = "*"
|
||||||
|
uvicorn = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
examples = ["fastapi"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "0.41.3"
|
version = "0.41.3"
|
||||||
@@ -2523,6 +2893,23 @@ notebook = ["ipywidgets (>=6)"]
|
|||||||
slack = ["slack-sdk"]
|
slack = ["slack-sdk"]
|
||||||
telegram = ["requests"]
|
telegram = ["requests"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typer"
|
||||||
|
version = "0.16.0"
|
||||||
|
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"},
|
||||||
|
{file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.0.0"
|
||||||
|
rich = ">=10.11.0"
|
||||||
|
shellingham = ">=1.3.0"
|
||||||
|
typing-extensions = ">=3.7.4.3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-protobuf"
|
name = "types-protobuf"
|
||||||
version = "5.29.1.20241207"
|
version = "5.29.1.20241207"
|
||||||
@@ -2545,6 +2932,20 @@ files = [
|
|||||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.1"
|
||||||
|
description = "Runtime typing introspection tools"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"},
|
||||||
|
{file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = ">=4.12.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tzdata"
|
name = "tzdata"
|
||||||
version = "2025.1"
|
version = "2025.1"
|
||||||
@@ -2743,4 +3144,4 @@ type = ["pytest-mypy"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.10,<4.0"
|
python-versions = ">=3.10,<4.0"
|
||||||
content-hash = "b391df89fabb111e4dd5d65a52a9db3a0bf9d95d5473e77cd0946beb940cf26f"
|
content-hash = "41d877763533efcd55e029b5686965972f9fcfee2c5e2183db2cb1d0c5b197e2"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ def generate_genai_prompt(
|
|||||||
conversation_history: str,
|
conversation_history: str,
|
||||||
multi_goal_mode: bool,
|
multi_goal_mode: bool,
|
||||||
raw_json: Optional[str] = None,
|
raw_json: Optional[str] = None,
|
||||||
|
mcp_tools_info: Optional[dict] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a concise prompt for producing or validating JSON instructions
|
Generates a concise prompt for producing or validating JSON instructions
|
||||||
@@ -22,7 +23,8 @@ def generate_genai_prompt(
|
|||||||
# Intro / Role
|
# Intro / Role
|
||||||
prompt_lines.append(
|
prompt_lines.append(
|
||||||
"You are an AI agent that helps fill required arguments for the tools described below. "
|
"You are an AI agent that helps fill required arguments for the tools described below. "
|
||||||
"You must respond with valid JSON ONLY, using the schema provided in the instructions."
|
"CRITICAL: You must respond with ONLY valid JSON using the exact schema provided. "
|
||||||
|
"DO NOT include any text before or after the JSON. Your entire response must be parseable JSON."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Main Conversation History
|
# Main Conversation History
|
||||||
@@ -48,11 +50,35 @@ def generate_genai_prompt(
|
|||||||
prompt_lines.append("END EXAMPLE")
|
prompt_lines.append("END EXAMPLE")
|
||||||
prompt_lines.append("")
|
prompt_lines.append("")
|
||||||
|
|
||||||
|
# Add MCP server context if present
|
||||||
|
if agent_goal.mcp_server_definition:
|
||||||
|
prompt_lines.append("=== MCP Server Information ===")
|
||||||
|
prompt_lines.append(
|
||||||
|
f"Connected to MCP Server: {agent_goal.mcp_server_definition.name}"
|
||||||
|
)
|
||||||
|
if mcp_tools_info and mcp_tools_info.get("success", False):
|
||||||
|
tools = mcp_tools_info.get("tools", {})
|
||||||
|
server_name = mcp_tools_info.get("server_name", "Unknown")
|
||||||
|
prompt_lines.append(
|
||||||
|
f"MCP Tools loaded from {server_name} ({len(tools)} tools):"
|
||||||
|
)
|
||||||
|
for tool_name, tool_info in tools.items():
|
||||||
|
prompt_lines.append(
|
||||||
|
f" - {tool_name}: {tool_info.get('description', 'No description')}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prompt_lines.append("Additional tools available via MCP integration:")
|
||||||
|
prompt_lines.append("")
|
||||||
|
|
||||||
# Tools Definitions
|
# Tools Definitions
|
||||||
prompt_lines.append("=== Tools Definitions ===")
|
prompt_lines.append("=== Tools Definitions ===")
|
||||||
prompt_lines.append(f"There are {len(agent_goal.tools)} available tools:")
|
prompt_lines.append(f"There are {len(agent_goal.tools)} available tools:")
|
||||||
prompt_lines.append(", ".join([t.name for t in agent_goal.tools]))
|
prompt_lines.append(", ".join([t.name for t in agent_goal.tools]))
|
||||||
prompt_lines.append(f"Goal: {agent_goal.description}")
|
prompt_lines.append(f"Goal: {agent_goal.description}")
|
||||||
|
prompt_lines.append(
|
||||||
|
"CRITICAL: You MUST follow the complete sequence described in the Goal above. "
|
||||||
|
"Do NOT skip steps or assume the goal is complete until ALL steps are done."
|
||||||
|
)
|
||||||
prompt_lines.append(
|
prompt_lines.append(
|
||||||
"Gather the necessary information for each tool in the sequence described above."
|
"Gather the necessary information for each tool in the sequence described above."
|
||||||
)
|
)
|
||||||
@@ -72,9 +98,12 @@ def generate_genai_prompt(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# JSON Format Instructions
|
# JSON Format Instructions
|
||||||
prompt_lines.append("=== Instructions for JSON Generation ===")
|
prompt_lines.append("=== CRITICAL: JSON-ONLY RESPONSE FORMAT ===")
|
||||||
prompt_lines.append(
|
prompt_lines.append(
|
||||||
"Your JSON format must be:\n"
|
"MANDATORY: Your response must be ONLY valid JSON with NO additional text.\n"
|
||||||
|
"NO explanations, NO comments, NO text before or after the JSON.\n"
|
||||||
|
"Your entire response must start with '{' and end with '}'.\n\n"
|
||||||
|
"Required JSON format:\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
' "response": "<plain text>",\n'
|
' "response": "<plain text>",\n'
|
||||||
' "next": "<question|confirm|pick-new-goal|done>",\n'
|
' "next": "<question|confirm|pick-new-goal|done>",\n'
|
||||||
@@ -84,29 +113,43 @@ def generate_genai_prompt(
|
|||||||
' "<arg2>": "<value2 or null>",\n'
|
' "<arg2>": "<value2 or null>",\n'
|
||||||
" ...\n"
|
" ...\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
"}"
|
"}\n\n"
|
||||||
|
"INVALID EXAMPLE: 'Thank you for providing... {\"response\": ...}'\n"
|
||||||
|
'VALID EXAMPLE: \'{"response": "Thank you for providing...", "next": ...}\''
|
||||||
)
|
)
|
||||||
prompt_lines.append(
|
prompt_lines.append(
|
||||||
"1) If any required argument is missing, set next='question' and ask the user.\n"
|
"DECISION LOGIC (follow this exact order):\n"
|
||||||
"2) If all required arguments are known, set next='confirm' and specify the tool.\n"
|
"1) Do I need to run a tool next?\n"
|
||||||
" The user will confirm before the tool is run.\n"
|
" - If your response says 'let's get/proceed/check/add/create/finalize...' -> YES, you need a tool\n"
|
||||||
f"3) {generate_toolchain_complete_guidance()}\n"
|
" - If you're announcing what you're about to do -> YES, you need a tool\n"
|
||||||
"4) response should be short and user-friendly.\n\n"
|
" - If no more steps needed for current goal -> NO, go to step 3\n\n"
|
||||||
"Guardrails (always remember!)\n"
|
"2) If YES to step 1: Do I have all required arguments?\n"
|
||||||
"1) If any required argument is missing, set next='question' and ask the user.\n"
|
" - Check tool definition for required args\n"
|
||||||
"1) ALWAYS ask a question in your response if next='question'.\n"
|
" - Can I fill missing args from conversation history?\n"
|
||||||
"2) ALWAYS set next='confirm' if you have arguments\n "
|
" - Can I use sensible defaults (limit=100, etc.)?\n"
|
||||||
'And respond with "let\'s proceed with <tool> (and any other useful info)" \n '
|
" - If ALL args available/inferrable -> set next='confirm', specify tool and args\n"
|
||||||
+ "DON'T set next='confirm' if you have a question to ask.\n"
|
" - If missing required args -> set next='question', ask for missing args, tool=null\n\n"
|
||||||
"EXAMPLE: If you have a question to ask, set next='question' and ask the user.\n"
|
"3) If NO to step 1: Is the entire goal complete?\n"
|
||||||
"3) You can carry over arguments from one tool to another.\n "
|
" - Check Goal description in system prompt - are ALL steps done?\n"
|
||||||
"EXAMPLE: If you asked for an account ID, then use the conversation history to infer that argument "
|
" - Check recent conversation for completion indicators ('finalized', 'complete', etc.)\n"
|
||||||
"going forward."
|
f" - If complete -> {generate_toolchain_complete_guidance()}\n"
|
||||||
"4) If ListAgents in the conversation history is force_confirm='False', you MUST check "
|
" - If not complete -> identify next needed tool, go to step 2\n\n"
|
||||||
+ "if the current tool contains userConfirmation. If it does, please ask the user to confirm details "
|
"CRITICAL RULES:\n"
|
||||||
+ "with the user. userConfirmation overrides force_confirm='False'.\n"
|
"• RESPOND WITH JSON ONLY - NO TEXT BEFORE OR AFTER THE JSON OBJECT\n"
|
||||||
+ "EXAMPLE: (force_confirm='False' AND userConfirmation exists on tool) Would you like me to <run tool> "
|
"• Your response must start with '{' and end with '}' - nothing else\n"
|
||||||
+ "with the following details: <details>?\n"
|
"• NEVER set next='question' without asking an actual question in your response\n"
|
||||||
|
"• NEVER set tool=null when you're announcing you'll run a specific tool\n"
|
||||||
|
"• If response contains 'let's proceed to get pricing' -> next='confirm', tool='list_prices'\n"
|
||||||
|
"• If response contains 'Now adding X' -> next='confirm', tool='create_invoice_item'\n"
|
||||||
|
"• Use conversation history to infer arguments (customer IDs, product IDs, etc.)\n"
|
||||||
|
"• Use sensible defaults rather than asking users for technical parameters\n"
|
||||||
|
"• Carry forward arguments between tools (same customer, same invoice, etc.)\n"
|
||||||
|
"• If force_confirm='False' in history, be declarative, don't ask permission\n\n"
|
||||||
|
"EXAMPLES:\n"
|
||||||
|
"WRONG: response='let\\'s get pricing', next='question', tool=null\n"
|
||||||
|
"RIGHT: response='let\\'s get pricing', next='confirm', tool='list_prices'\n"
|
||||||
|
"WRONG: response='adding pizza', next='question', tool='create_invoice_item'\n"
|
||||||
|
"RIGHT: response='adding pizza', next='confirm', tool='create_invoice_item'\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validation Task (If raw_json is provided)
|
# Validation Task (If raw_json is provided)
|
||||||
@@ -123,11 +166,16 @@ def generate_genai_prompt(
|
|||||||
|
|
||||||
# Prompt Start
|
# Prompt Start
|
||||||
prompt_lines.append("")
|
prompt_lines.append("")
|
||||||
|
prompt_lines.append("=== FINAL REMINDER ===")
|
||||||
|
prompt_lines.append("RESPOND WITH VALID JSON ONLY. NO ADDITIONAL TEXT.")
|
||||||
|
prompt_lines.append("")
|
||||||
if raw_json is not None:
|
if raw_json is not None:
|
||||||
prompt_lines.append("Begin by validating the provided JSON if necessary.")
|
prompt_lines.append(
|
||||||
|
"Validate the provided JSON and return ONLY corrected JSON."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
prompt_lines.append(
|
prompt_lines.append(
|
||||||
"Begin by producing a valid JSON response for the next tool or question."
|
"Return ONLY a valid JSON response. Start with '{' and end with '}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
return "\n".join(prompt_lines)
|
return "\n".join(prompt_lines)
|
||||||
@@ -216,7 +264,7 @@ 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 for the current goal (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 EVERY SINGLE STEP in the Goal description has been completed (check the system prompt Goal section carefully), or the user explicitly requested to pick a new goal. If any step is missing (like customer creation, invoice creation, or payment processing), continue with the next required tool.'
|
||||||
else:
|
else:
|
||||||
return 'Next should never be "pick-new-goal".'
|
return 'Next should never be "pick-new-goal".'
|
||||||
|
|
||||||
@@ -232,6 +280,6 @@ def generate_toolchain_complete_guidance() -> str:
|
|||||||
str: A prompt string prompting the LLM to prompt for a new goal, or be done
|
str: A prompt string prompting the LLM to prompt for a new goal, or be done
|
||||||
"""
|
"""
|
||||||
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 for the current goal (EVERY step in the Goal description has been completed AND user_confirmed_tool_run has been run for all required tools), set next='pick-new-goal' and tool=null to allow the user to choose their next action."
|
||||||
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 (EVERY step in the Goal description has been completed AND user_confirmed_tool_run has been run for all), set next='done' and tool=null."
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ requests = "^2.32.3"
|
|||||||
pandas = "^2.2.3"
|
pandas = "^2.2.3"
|
||||||
stripe = "^11.4.1"
|
stripe = "^11.4.1"
|
||||||
gtfs-kit = "^10.1.1"
|
gtfs-kit = "^10.1.1"
|
||||||
|
fastmcp = "^2.7.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = ">=8.2"
|
pytest = ">=8.2"
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import os
|
|||||||
from dotenv import load_dotenv
|
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,
|
||||||
|
mcp_list_tools,
|
||||||
|
)
|
||||||
from shared.config import TEMPORAL_TASK_QUEUE, get_temporal_client
|
from shared.config import TEMPORAL_TASK_QUEUE, get_temporal_client
|
||||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||||
|
|
||||||
@@ -48,7 +52,7 @@ async def main():
|
|||||||
print("===========================================================\n")
|
print("===========================================================\n")
|
||||||
|
|
||||||
print("Worker ready to process tasks!")
|
print("Worker ready to process tasks!")
|
||||||
logging.basicConfig(level=logging.WARN)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -60,7 +64,9 @@ async def main():
|
|||||||
activities.agent_validatePrompt,
|
activities.agent_validatePrompt,
|
||||||
activities.agent_toolPlanner,
|
activities.agent_toolPlanner,
|
||||||
activities.get_wf_env_vars,
|
activities.get_wf_env_vars,
|
||||||
|
activities.mcp_tool_activity,
|
||||||
dynamic_tool_activity,
|
dynamic_tool_activity,
|
||||||
|
mcp_list_tools,
|
||||||
],
|
],
|
||||||
activity_executor=activity_executor,
|
activity_executor=activity_executor,
|
||||||
)
|
)
|
||||||
|
|||||||
27
shared/mcp_config.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from models.tool_definitions import MCPServerDefinition
|
||||||
|
|
||||||
|
|
||||||
|
def get_stripe_mcp_server_definition(included_tools: list[str]) -> MCPServerDefinition:
|
||||||
|
"""
|
||||||
|
Returns a Stripe MCP server definition with customizable included tools.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
included_tools: List of tool names to include from the Stripe MCP server
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MCPServerDefinition configured for Stripe
|
||||||
|
"""
|
||||||
|
return MCPServerDefinition(
|
||||||
|
name="stripe-mcp",
|
||||||
|
command="npx",
|
||||||
|
args=[
|
||||||
|
"-y",
|
||||||
|
"@stripe/mcp",
|
||||||
|
"--tools=all",
|
||||||
|
f"--api-key={os.getenv('STRIPE_API_KEY')}",
|
||||||
|
],
|
||||||
|
env=None,
|
||||||
|
included_tools=included_tools,
|
||||||
|
)
|
||||||
418
tests/test_mcp_integration.py
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
import asyncio
|
||||||
|
import uuid
|
||||||
|
from collections import deque
|
||||||
|
from typing import Sequence
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from temporalio import activity
|
||||||
|
from temporalio.client import Client
|
||||||
|
from temporalio.common import RawValue
|
||||||
|
from temporalio.testing import ActivityEnvironment
|
||||||
|
from temporalio.worker import Worker
|
||||||
|
|
||||||
|
from activities.tool_activities import _convert_args_types, mcp_list_tools
|
||||||
|
from models.data_types import (
|
||||||
|
AgentGoalWorkflowParams,
|
||||||
|
CombinedInput,
|
||||||
|
EnvLookupInput,
|
||||||
|
EnvLookupOutput,
|
||||||
|
ToolPromptInput,
|
||||||
|
ValidationInput,
|
||||||
|
ValidationResult,
|
||||||
|
)
|
||||||
|
from models.tool_definitions import AgentGoal, MCPServerDefinition, ToolDefinition
|
||||||
|
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||||
|
from workflows.workflow_helpers import is_mcp_tool
|
||||||
|
|
||||||
|
|
||||||
|
class DummySession:
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def list_tools(self):
|
||||||
|
class Tool:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.description = f"desc {name}"
|
||||||
|
self.inputSchema = {}
|
||||||
|
|
||||||
|
return type(
|
||||||
|
"Resp", (), {"tools": [Tool("list_products"), Tool("create_customer")]}
|
||||||
|
)()
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_args_types_basic():
|
||||||
|
args = {
|
||||||
|
"count": "5",
|
||||||
|
"price": "12.5",
|
||||||
|
"flag_true": "true",
|
||||||
|
"flag_false": "false",
|
||||||
|
"name": "pizza",
|
||||||
|
"already_int": 2,
|
||||||
|
}
|
||||||
|
result = _convert_args_types(args)
|
||||||
|
assert result["count"] == 5 and isinstance(result["count"], int)
|
||||||
|
assert result["price"] == 12.5 and isinstance(result["price"], float)
|
||||||
|
assert result["flag_true"] is True
|
||||||
|
assert result["flag_false"] is False
|
||||||
|
assert result["name"] == "pizza"
|
||||||
|
assert result["already_int"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_mcp_tool_identification():
|
||||||
|
server_def = MCPServerDefinition(name="test", command="python", args=["server.py"])
|
||||||
|
goal = AgentGoal(
|
||||||
|
id="g",
|
||||||
|
category_tag="food",
|
||||||
|
agent_name="agent",
|
||||||
|
agent_friendly_description="",
|
||||||
|
description="",
|
||||||
|
tools=[ToolDefinition(name="AddToCart", description="", arguments=[])],
|
||||||
|
starter_prompt="",
|
||||||
|
example_conversation_history="",
|
||||||
|
mcp_server_definition=server_def,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert is_mcp_tool("list_products", goal) is True
|
||||||
|
assert is_mcp_tool("AddToCart", goal) is False
|
||||||
|
no_mcp_goal = AgentGoal(
|
||||||
|
id="g2",
|
||||||
|
category_tag="food",
|
||||||
|
agent_name="agent",
|
||||||
|
agent_friendly_description="",
|
||||||
|
description="",
|
||||||
|
tools=[],
|
||||||
|
starter_prompt="",
|
||||||
|
example_conversation_history="",
|
||||||
|
mcp_server_definition=None,
|
||||||
|
)
|
||||||
|
assert is_mcp_tool("list_products", no_mcp_goal) is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mcp_list_tools_success():
|
||||||
|
server_def = MCPServerDefinition(name="test", command="python", args=["server.py"])
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def dummy_connection(command, args, env):
|
||||||
|
yield None, None
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"activities.tool_activities._build_connection", return_value={"type": "stdio"}
|
||||||
|
), patch("activities.tool_activities._stdio_connection", dummy_connection), patch(
|
||||||
|
"activities.tool_activities.ClientSession", lambda r, w: DummySession()
|
||||||
|
):
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(mcp_list_tools, server_def, ["list_products"])
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["filtered_count"] == 1
|
||||||
|
assert "list_products" in result["tools"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mcp_list_tools_failure():
|
||||||
|
server_def = MCPServerDefinition(name="test", command="python", args=["server.py"])
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def failing_connection(*args, **kwargs):
|
||||||
|
raise RuntimeError("conn fail")
|
||||||
|
yield None, None
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"activities.tool_activities._build_connection", return_value={"type": "stdio"}
|
||||||
|
), patch("activities.tool_activities._stdio_connection", failing_connection):
|
||||||
|
activity_env = ActivityEnvironment()
|
||||||
|
result = await activity_env.run(mcp_list_tools, server_def)
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "conn fail" in result["error"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_workflow_loads_mcp_tools_dynamically(client: Client):
|
||||||
|
"""Workflow should load MCP tools and add them to the goal."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
server_def = MCPServerDefinition(name="test", command="python", args=["srv.py"])
|
||||||
|
goal = AgentGoal(
|
||||||
|
id="g_mcp",
|
||||||
|
category_tag="food",
|
||||||
|
agent_name="agent",
|
||||||
|
agent_friendly_description="",
|
||||||
|
description="",
|
||||||
|
tools=[],
|
||||||
|
starter_prompt="",
|
||||||
|
example_conversation_history="",
|
||||||
|
mcp_server_definition=server_def,
|
||||||
|
)
|
||||||
|
combined_input = CombinedInput(
|
||||||
|
agent_goal=goal,
|
||||||
|
tool_params=AgentGoalWorkflowParams(
|
||||||
|
conversation_summary=None, prompt_queue=deque()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
@activity.defn(name="mcp_list_tools")
|
||||||
|
async def mock_mcp_list_tools(
|
||||||
|
server_definition: MCPServerDefinition, include_tools=None
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"server_name": server_definition.name,
|
||||||
|
"success": True,
|
||||||
|
"tools": {
|
||||||
|
"list_products": {
|
||||||
|
"name": "list_products",
|
||||||
|
"description": "",
|
||||||
|
"inputSchema": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"total_available": 1,
|
||||||
|
"filtered_count": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[mock_get_wf_env_vars, mock_mcp_list_tools],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait until the MCP tools have been added
|
||||||
|
for _ in range(10):
|
||||||
|
updated_goal = await handle.query(AgentGoalWorkflow.get_agent_goal)
|
||||||
|
if any(t.name == "list_products" for t in updated_goal.tools):
|
||||||
|
break
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
else:
|
||||||
|
updated_goal = await handle.query(AgentGoalWorkflow.get_agent_goal)
|
||||||
|
|
||||||
|
assert any(t.name == "list_products" for t in updated_goal.tools)
|
||||||
|
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
await handle.result()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mcp_tool_execution_flow(client: Client):
|
||||||
|
"""MCP tool execution should pass server_definition to activity."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
server_def = MCPServerDefinition(name="test", command="python", args=["srv.py"])
|
||||||
|
goal = AgentGoal(
|
||||||
|
id="g_mcp_exec",
|
||||||
|
category_tag="food",
|
||||||
|
agent_name="agent",
|
||||||
|
agent_friendly_description="",
|
||||||
|
description="",
|
||||||
|
tools=[],
|
||||||
|
starter_prompt="",
|
||||||
|
example_conversation_history="",
|
||||||
|
mcp_server_definition=server_def,
|
||||||
|
)
|
||||||
|
combined_input = CombinedInput(
|
||||||
|
agent_goal=goal,
|
||||||
|
tool_params=AgentGoalWorkflowParams(
|
||||||
|
conversation_summary=None, prompt_queue=deque()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
captured: dict = {}
|
||||||
|
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
@activity.defn(name="agent_validatePrompt")
|
||||||
|
async def mock_validate(prompt: ValidationInput) -> ValidationResult:
|
||||||
|
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||||
|
|
||||||
|
@activity.defn(name="agent_toolPlanner")
|
||||||
|
async def mock_planner(input: ToolPromptInput) -> dict:
|
||||||
|
if "planner_called" not in captured:
|
||||||
|
captured["planner_called"] = True
|
||||||
|
return {
|
||||||
|
"next": "confirm",
|
||||||
|
"tool": "list_products",
|
||||||
|
"args": {"limit": "5"},
|
||||||
|
"response": "Listing products",
|
||||||
|
}
|
||||||
|
return {"next": "done", "response": "done"}
|
||||||
|
|
||||||
|
@activity.defn(name="mcp_list_tools")
|
||||||
|
async def mock_mcp_list_tools(
|
||||||
|
server_definition: MCPServerDefinition, include_tools=None
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"server_name": server_definition.name,
|
||||||
|
"success": True,
|
||||||
|
"tools": {
|
||||||
|
"list_products": {
|
||||||
|
"name": "list_products",
|
||||||
|
"description": "",
|
||||||
|
"inputSchema": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"total_available": 1,
|
||||||
|
"filtered_count": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
@activity.defn(name="dynamic_tool_activity", dynamic=True)
|
||||||
|
async def mock_dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||||
|
payload = activity.payload_converter().from_payload(args[0].payload, dict)
|
||||||
|
captured["dynamic_args"] = payload
|
||||||
|
return {"tool": "list_products", "success": True, "content": {"ok": True}}
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[
|
||||||
|
mock_get_wf_env_vars,
|
||||||
|
mock_validate,
|
||||||
|
mock_planner,
|
||||||
|
mock_mcp_list_tools,
|
||||||
|
mock_dynamic_tool_activity,
|
||||||
|
],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
await handle.signal(AgentGoalWorkflow.user_prompt, "show menu")
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
await handle.signal(AgentGoalWorkflow.confirm)
|
||||||
|
# Give workflow time to execute the MCP tool and finish
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
result = await handle.result()
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
assert "dynamic_args" in captured
|
||||||
|
assert "server_definition" in captured["dynamic_args"]
|
||||||
|
assert captured["dynamic_args"]["server_definition"]["name"] == server_def.name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mcp_tool_failure_recorded(client: Client):
|
||||||
|
"""Failure of an MCP tool should be recorded in conversation history."""
|
||||||
|
task_queue_name = str(uuid.uuid4())
|
||||||
|
server_def = MCPServerDefinition(name="test", command="python", args=["srv.py"])
|
||||||
|
goal = AgentGoal(
|
||||||
|
id="g_mcp_fail",
|
||||||
|
category_tag="food",
|
||||||
|
agent_name="agent",
|
||||||
|
agent_friendly_description="",
|
||||||
|
description="",
|
||||||
|
tools=[],
|
||||||
|
starter_prompt="",
|
||||||
|
example_conversation_history="",
|
||||||
|
mcp_server_definition=server_def,
|
||||||
|
)
|
||||||
|
combined_input = CombinedInput(
|
||||||
|
agent_goal=goal,
|
||||||
|
tool_params=AgentGoalWorkflowParams(
|
||||||
|
conversation_summary=None, prompt_queue=deque()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@activity.defn(name="get_wf_env_vars")
|
||||||
|
async def mock_get_wf_env_vars(input: EnvLookupInput) -> EnvLookupOutput:
|
||||||
|
return EnvLookupOutput(show_confirm=True, multi_goal_mode=True)
|
||||||
|
|
||||||
|
@activity.defn(name="agent_validatePrompt")
|
||||||
|
async def mock_validate(prompt: ValidationInput) -> ValidationResult:
|
||||||
|
return ValidationResult(validationResult=True, validationFailedReason={})
|
||||||
|
|
||||||
|
@activity.defn(name="agent_toolPlanner")
|
||||||
|
async def mock_planner(input: ToolPromptInput) -> dict:
|
||||||
|
return {
|
||||||
|
"next": "confirm",
|
||||||
|
"tool": "list_products",
|
||||||
|
"args": {},
|
||||||
|
"response": "Listing products",
|
||||||
|
}
|
||||||
|
|
||||||
|
@activity.defn(name="mcp_list_tools")
|
||||||
|
async def mock_mcp_list_tools(
|
||||||
|
server_definition: MCPServerDefinition, include_tools=None
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"server_name": server_definition.name,
|
||||||
|
"success": True,
|
||||||
|
"tools": {
|
||||||
|
"list_products": {
|
||||||
|
"name": "list_products",
|
||||||
|
"description": "",
|
||||||
|
"inputSchema": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"total_available": 1,
|
||||||
|
"filtered_count": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
@activity.defn(name="dynamic_tool_activity", dynamic=True)
|
||||||
|
async def failing_dynamic_tool(args: Sequence[RawValue]) -> dict:
|
||||||
|
return {
|
||||||
|
"tool": "list_products",
|
||||||
|
"success": False,
|
||||||
|
"error": "Connection timed out",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with Worker(
|
||||||
|
client,
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
workflows=[AgentGoalWorkflow],
|
||||||
|
activities=[
|
||||||
|
mock_get_wf_env_vars,
|
||||||
|
mock_validate,
|
||||||
|
mock_planner,
|
||||||
|
mock_mcp_list_tools,
|
||||||
|
failing_dynamic_tool,
|
||||||
|
],
|
||||||
|
):
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
AgentGoalWorkflow.run,
|
||||||
|
combined_input,
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
task_queue=task_queue_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
await handle.signal(AgentGoalWorkflow.user_prompt, "show menu")
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
await handle.signal(AgentGoalWorkflow.confirm)
|
||||||
|
# Give workflow time to record the failure result
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
await handle.signal(AgentGoalWorkflow.end_chat)
|
||||||
|
result = await handle.result()
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
history = json.loads(result.replace("'", '"'))
|
||||||
|
except Exception:
|
||||||
|
history = eval(result)
|
||||||
|
|
||||||
|
assert any(
|
||||||
|
msg["actor"] == "tool_result" and not msg["response"].get("success", True)
|
||||||
|
for msg in history["messages"]
|
||||||
|
)
|
||||||
@@ -6,7 +6,11 @@ import pytest
|
|||||||
from temporalio.client import Client
|
from temporalio.client import Client
|
||||||
from temporalio.testing import ActivityEnvironment
|
from temporalio.testing import ActivityEnvironment
|
||||||
|
|
||||||
from activities.tool_activities import ToolActivities, dynamic_tool_activity
|
from activities.tool_activities import (
|
||||||
|
MCPServerDefinition,
|
||||||
|
ToolActivities,
|
||||||
|
dynamic_tool_activity,
|
||||||
|
)
|
||||||
from models.data_types import (
|
from models.data_types import (
|
||||||
EnvLookupInput,
|
EnvLookupInput,
|
||||||
EnvLookupOutput,
|
EnvLookupOutput,
|
||||||
@@ -190,7 +194,7 @@ class TestToolActivities:
|
|||||||
|
|
||||||
assert isinstance(result, EnvLookupOutput)
|
assert isinstance(result, EnvLookupOutput)
|
||||||
assert result.show_confirm is True # default value
|
assert result.show_confirm is True # default value
|
||||||
assert result.multi_goal_mode is True # default value
|
assert result.multi_goal_mode is False # default value (single agent mode)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_wf_env_vars_custom_values(self):
|
async def test_get_wf_env_vars_custom_values(self):
|
||||||
@@ -443,3 +447,132 @@ class TestEdgeCases:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result.show_confirm
|
assert result.show_confirm
|
||||||
|
|
||||||
|
|
||||||
|
class TestMCPIntegration:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_convert_args_types(self):
|
||||||
|
from activities.tool_activities import _convert_args_types
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"int_val": "123",
|
||||||
|
"float_val": "123.45",
|
||||||
|
"bool_true": "true",
|
||||||
|
"bool_false": "False",
|
||||||
|
"string": "text",
|
||||||
|
"other": 5,
|
||||||
|
}
|
||||||
|
converted = _convert_args_types(args)
|
||||||
|
assert converted["int_val"] == 123
|
||||||
|
assert converted["float_val"] == 123.45
|
||||||
|
assert converted["bool_true"] is True
|
||||||
|
assert converted["bool_false"] is False
|
||||||
|
assert converted["string"] == "text"
|
||||||
|
assert converted["other"] == 5
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dynamic_tool_activity_mcp_call(self):
|
||||||
|
mcp_def = MCPServerDefinition(
|
||||||
|
name="stripe", command="python", args=["server.py"]
|
||||||
|
)
|
||||||
|
payload = MagicMock()
|
||||||
|
payload.payload = b'{"server_definition": null, "amount": "10", "flag": "true"}'
|
||||||
|
mock_info = MagicMock()
|
||||||
|
mock_info.activity_type = "list_products"
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def dummy_conn(*args, **kwargs):
|
||||||
|
yield (None, None)
|
||||||
|
|
||||||
|
class DummySession:
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def call_tool(self, tool_name, arguments=None):
|
||||||
|
self.called_tool = tool_name
|
||||||
|
self.called_args = arguments
|
||||||
|
return MagicMock(content="ok")
|
||||||
|
|
||||||
|
mock_payload_converter = MagicMock()
|
||||||
|
mock_payload_converter.from_payload.return_value = {
|
||||||
|
"server_definition": mcp_def,
|
||||||
|
"amount": "10",
|
||||||
|
"flag": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("activities.tool_activities._stdio_connection", dummy_conn), patch(
|
||||||
|
"activities.tool_activities.ClientSession", return_value=DummySession()
|
||||||
|
), patch(
|
||||||
|
"activities.tool_activities._build_connection",
|
||||||
|
return_value={
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "python",
|
||||||
|
"args": ["server.py"],
|
||||||
|
"env": {},
|
||||||
|
},
|
||||||
|
), patch(
|
||||||
|
"temporalio.activity.info", return_value=mock_info
|
||||||
|
), patch(
|
||||||
|
"temporalio.activity.payload_converter", return_value=mock_payload_converter
|
||||||
|
):
|
||||||
|
result = await ActivityEnvironment().run(dynamic_tool_activity, [payload])
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["tool"] == "list_products"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mcp_tool_activity_failure(self):
|
||||||
|
tool_activities = ToolActivities()
|
||||||
|
mcp_def = MCPServerDefinition(
|
||||||
|
name="stripe", command="python", args=["server.py"]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def dummy_conn(*args, **kwargs):
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def cm():
|
||||||
|
yield (None, None)
|
||||||
|
|
||||||
|
return cm()
|
||||||
|
|
||||||
|
class DummySession:
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def call_tool(self, tool_name, arguments=None):
|
||||||
|
raise TypeError("boom")
|
||||||
|
|
||||||
|
with patch("activities.tool_activities._stdio_connection", dummy_conn), patch(
|
||||||
|
"activities.tool_activities.ClientSession", return_value=DummySession()
|
||||||
|
), patch(
|
||||||
|
"activities.tool_activities._build_connection",
|
||||||
|
return_value={
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "python",
|
||||||
|
"args": ["server.py"],
|
||||||
|
"env": {},
|
||||||
|
},
|
||||||
|
):
|
||||||
|
result = await ActivityEnvironment().run(
|
||||||
|
tool_activities.mcp_tool_activity,
|
||||||
|
"list_products",
|
||||||
|
{"server_definition": mcp_def, "amount": "10"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert result["error_type"] == "TypeError"
|
||||||
|
|||||||
36
tests/test_workflow_helpers.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from models.tool_definitions import (
|
||||||
|
AgentGoal,
|
||||||
|
MCPServerDefinition,
|
||||||
|
ToolArgument,
|
||||||
|
ToolDefinition,
|
||||||
|
)
|
||||||
|
from workflows.workflow_helpers import is_mcp_tool
|
||||||
|
|
||||||
|
|
||||||
|
def make_goal(with_mcp: bool) -> AgentGoal:
|
||||||
|
tools = [ToolDefinition(name="AddToCart", description="", arguments=[])]
|
||||||
|
mcp_def = None
|
||||||
|
if with_mcp:
|
||||||
|
mcp_def = MCPServerDefinition(
|
||||||
|
name="stripe", command="python", args=["server.py"]
|
||||||
|
)
|
||||||
|
return AgentGoal(
|
||||||
|
id="g",
|
||||||
|
category_tag="test",
|
||||||
|
agent_name="Test",
|
||||||
|
agent_friendly_description="",
|
||||||
|
tools=tools,
|
||||||
|
mcp_server_definition=mcp_def,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_mcp_tool_recognizes_native():
|
||||||
|
goal = make_goal(True)
|
||||||
|
assert not is_mcp_tool("AddToCart", goal)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_mcp_tool_recognizes_mcp():
|
||||||
|
goal = make_goal(True)
|
||||||
|
assert is_mcp_tool("list_products", goal)
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import uuid
|
import uuid
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import Any, Dict, List, Optional, Sequence
|
||||||
|
|
||||||
from temporalio import activity
|
from temporalio import activity
|
||||||
from temporalio.client import Client, WorkflowExecutionStatus
|
from temporalio.client import Client, WorkflowExecutionStatus
|
||||||
|
from temporalio.common import RawValue
|
||||||
from temporalio.worker import Worker
|
from temporalio.worker import Worker
|
||||||
|
|
||||||
from api.main import get_initial_agent_goal
|
from api.main import get_initial_agent_goal
|
||||||
@@ -16,6 +18,7 @@ from models.data_types import (
|
|||||||
ValidationInput,
|
ValidationInput,
|
||||||
ValidationResult,
|
ValidationResult,
|
||||||
)
|
)
|
||||||
|
from models.tool_definitions import MCPServerDefinition
|
||||||
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
from workflows.agent_goal_workflow import AgentGoalWorkflow
|
||||||
|
|
||||||
|
|
||||||
@@ -53,6 +56,23 @@ async def test_flight_booking(client: Client):
|
|||||||
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
async def mock_agent_toolPlanner(input: ToolPromptInput) -> dict:
|
||||||
return {"next": "done", "response": "Test response from LLM"}
|
return {"next": "done", "response": "Test response from LLM"}
|
||||||
|
|
||||||
|
@activity.defn(name="mcp_list_tools")
|
||||||
|
async def mock_mcp_list_tools(
|
||||||
|
server_definition: MCPServerDefinition,
|
||||||
|
include_tools: Optional[List[str]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
return {"success": True, "tools": {}, "server_name": "test"}
|
||||||
|
|
||||||
|
@activity.defn(name="mcp_tool_activity")
|
||||||
|
async def mock_mcp_tool_activity(
|
||||||
|
tool_name: str, tool_args: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
return {"success": True, "result": "Mock MCP tool result"}
|
||||||
|
|
||||||
|
@activity.defn(name="dynamic_tool_activity", dynamic=True)
|
||||||
|
async def mock_dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
|
||||||
|
return {"success": True, "result": "Mock dynamic tool result"}
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(
|
with concurrent.futures.ThreadPoolExecutor(
|
||||||
max_workers=100
|
max_workers=100
|
||||||
) as activity_executor:
|
) as activity_executor:
|
||||||
@@ -64,6 +84,9 @@ async def test_flight_booking(client: Client):
|
|||||||
mock_get_wf_env_vars,
|
mock_get_wf_env_vars,
|
||||||
mock_agent_validatePrompt,
|
mock_agent_validatePrompt,
|
||||||
mock_agent_toolPlanner,
|
mock_agent_toolPlanner,
|
||||||
|
mock_mcp_list_tools,
|
||||||
|
mock_mcp_tool_activity,
|
||||||
|
mock_dynamic_tool_activity,
|
||||||
],
|
],
|
||||||
activity_executor=activity_executor,
|
activity_executor=activity_executor,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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 .find_events import find_events
|
||||||
|
from .food.add_to_cart import add_to_cart
|
||||||
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.book_pto import book_pto
|
||||||
@@ -66,5 +67,7 @@ def get_handler(tool_name: str):
|
|||||||
return give_hint
|
return give_hint
|
||||||
if tool_name == "GuessLocation":
|
if tool_name == "GuessLocation":
|
||||||
return guess_location
|
return guess_location
|
||||||
|
if tool_name == "AddToCart":
|
||||||
|
return add_to_cart
|
||||||
|
|
||||||
raise ValueError(f"Unknown tool: {tool_name}")
|
raise ValueError(f"Unknown tool: {tool_name}")
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import stripe
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv(override=True) # Load environment variables from a .env file
|
|
||||||
|
|
||||||
stripe.api_key = os.getenv("STRIPE_API_KEY")
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_customer_exists(
|
def ensure_customer_exists(
|
||||||
customer_id: str = None, email: str = "default@example.com"
|
customer_id: str = None, email: str = "default@example.com"
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Ensure a Stripe customer exists; create one if not."""
|
"""Ensure a Stripe customer exists; create one if not."""
|
||||||
|
import stripe
|
||||||
|
|
||||||
if customer_id:
|
if customer_id:
|
||||||
try:
|
try:
|
||||||
stripe.Customer.retrieve(customer_id)
|
stripe.Customer.retrieve(customer_id)
|
||||||
@@ -27,6 +24,12 @@ 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."""
|
||||||
|
import stripe
|
||||||
|
|
||||||
|
# Load environment variables and configure stripe
|
||||||
|
load_dotenv(override=True)
|
||||||
|
stripe.api_key = os.getenv("STRIPE_API_KEY")
|
||||||
|
|
||||||
# If an API key exists in the env file, find or create customer
|
# If an API key exists in the env file, find or create customer
|
||||||
if stripe.api_key is not None and stripe.api_key != "":
|
if stripe.api_key is not None and stripe.api_key != "":
|
||||||
customer_id = ensure_customer_exists(
|
customer_id = ensure_customer_exists(
|
||||||
|
|||||||
33
tools/food/add_to_cart.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
def add_to_cart(args: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Simple stateless cart tool for demo purposes.
|
||||||
|
In production, this would use proper session storage or database.
|
||||||
|
"""
|
||||||
|
customer_email = args.get("customer_email")
|
||||||
|
item_name = args.get("item_name")
|
||||||
|
item_price = float(args.get("item_price", 0))
|
||||||
|
quantity = int(args.get("quantity", 1))
|
||||||
|
stripe_product_id = args.get("stripe_product_id")
|
||||||
|
|
||||||
|
# Basic validation
|
||||||
|
if not customer_email:
|
||||||
|
return {"error": "Customer email is required"}
|
||||||
|
if not item_name:
|
||||||
|
return {"error": "Item name is required"}
|
||||||
|
if item_price <= 0:
|
||||||
|
return {"error": "Item price must be greater than 0"}
|
||||||
|
if quantity <= 0:
|
||||||
|
return {"error": "Quantity must be greater than 0"}
|
||||||
|
|
||||||
|
# For demo purposes, just acknowledge the addition
|
||||||
|
# In a real system, this would store to session/database
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Added {quantity} x {item_name} (${item_price}) to cart for {customer_email}",
|
||||||
|
"item_added": {
|
||||||
|
"name": item_name,
|
||||||
|
"price": item_price,
|
||||||
|
"quantity": quantity,
|
||||||
|
"stripe_product_id": stripe_product_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
48
tools/food/setup/archive_food_products.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
def delete_food_ordering_products():
|
||||||
|
"""Archive all Stripe products with metadata use_case = food_ordering_demo (since products with prices cannot be deleted)."""
|
||||||
|
import stripe
|
||||||
|
|
||||||
|
# Load environment variables and configure stripe
|
||||||
|
load_dotenv(override=True)
|
||||||
|
stripe.api_key = os.getenv("STRIPE_API_KEY")
|
||||||
|
|
||||||
|
if not stripe.api_key:
|
||||||
|
print("Error: STRIPE_API_KEY not found in environment variables")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Search for products with food_ordering_demo use_case
|
||||||
|
products = stripe.Product.search(
|
||||||
|
query="metadata['use_case']:'food_ordering_demo'", limit=100
|
||||||
|
)
|
||||||
|
|
||||||
|
if not products.data:
|
||||||
|
print("No products found with use_case = food_ordering_demo")
|
||||||
|
return
|
||||||
|
|
||||||
|
archived_count = 0
|
||||||
|
|
||||||
|
for product in products.data:
|
||||||
|
try:
|
||||||
|
# Archive the product (set active=False)
|
||||||
|
stripe.Product.modify(product.id, active=False)
|
||||||
|
print(f"Archived product: {product.name} (ID: {product.id})")
|
||||||
|
archived_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Error archiving product {product.name} (ID: {product.id}): {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\nSuccessfully archived {archived_count} products")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error searching for products: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
delete_food_ordering_products()
|
||||||
92
tools/food/setup/create_stripe_products.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
def create_stripe_products():
|
||||||
|
"""Create Stripe products and prices from the stripe_pizza_products.json file."""
|
||||||
|
import stripe
|
||||||
|
|
||||||
|
# Load environment variables and configure stripe
|
||||||
|
load_dotenv(override=True)
|
||||||
|
stripe.api_key = os.getenv("STRIPE_API_KEY")
|
||||||
|
|
||||||
|
if not stripe.api_key:
|
||||||
|
print("Error: STRIPE_API_KEY not found in environment variables")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load the products data
|
||||||
|
current_dir = os.path.dirname(__file__)
|
||||||
|
products_file = os.path.join(current_dir, "stripe_pizza_products.json")
|
||||||
|
|
||||||
|
with open(products_file, "r") as f:
|
||||||
|
products_data = json.load(f)
|
||||||
|
|
||||||
|
# Filter for food ordering demo products only
|
||||||
|
food_products = [
|
||||||
|
p
|
||||||
|
for p in products_data
|
||||||
|
if p.get("metadata", {}).get("use_case") == "food_ordering_demo"
|
||||||
|
]
|
||||||
|
|
||||||
|
created_products = []
|
||||||
|
|
||||||
|
for product_data in food_products:
|
||||||
|
try:
|
||||||
|
# Create the product with relevant fields
|
||||||
|
product = stripe.Product.create(
|
||||||
|
name=product_data["name"],
|
||||||
|
description=product_data.get("description"),
|
||||||
|
images=product_data.get("images", []),
|
||||||
|
metadata=product_data.get("metadata", {}),
|
||||||
|
type=product_data.get("type", "service"),
|
||||||
|
active=product_data.get("active", True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create price for the product if price_info exists
|
||||||
|
price_info = product_data.get("price_info")
|
||||||
|
if price_info:
|
||||||
|
price_amount = price_info.get("amount")
|
||||||
|
currency = price_info.get("currency", "usd")
|
||||||
|
|
||||||
|
price = stripe.Price.create(
|
||||||
|
currency=currency, unit_amount=price_amount, product=product.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set this price as the default price for the product
|
||||||
|
stripe.Product.modify(product.id, default_price=price.id)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Created product: {product.name} (ID: {product.id}) with default price ${price_amount/100:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
created_products.append(
|
||||||
|
{
|
||||||
|
"name": product.name,
|
||||||
|
"id": product.id,
|
||||||
|
"price_id": price.id,
|
||||||
|
"price_amount": price_amount,
|
||||||
|
"original_id": product_data["id"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Created product: {product.name} (ID: {product.id}) - No price defined"
|
||||||
|
)
|
||||||
|
created_products.append(
|
||||||
|
{
|
||||||
|
"name": product.name,
|
||||||
|
"id": product.id,
|
||||||
|
"original_id": product_data["id"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating product {product_data['name']}: {str(e)}")
|
||||||
|
|
||||||
|
print(f"\nSuccessfully created {len(created_products)} products with prices")
|
||||||
|
return created_products
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_stripe_products()
|
||||||
188
tools/food/setup/stripe_pizza_products.json
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "prod_SSWirxxS5A8gcT",
|
||||||
|
"object": "product",
|
||||||
|
"active": true,
|
||||||
|
"attributes": [],
|
||||||
|
"created": 1749360061,
|
||||||
|
"default_price": "price_1RXbfGKVZbzw7QA57Mj1akGI",
|
||||||
|
"description": "A large size bottle of cola.",
|
||||||
|
"images": [
|
||||||
|
"https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfbDJxckJKMDRnT1dDc253OHlZNWNkZkY5006Xg07kHT"
|
||||||
|
],
|
||||||
|
"livemode": false,
|
||||||
|
"marketing_features": [],
|
||||||
|
"metadata": {
|
||||||
|
"use_case": "food_ordering_demo"
|
||||||
|
},
|
||||||
|
"name": "Soda",
|
||||||
|
"price_info": {
|
||||||
|
"amount": 349,
|
||||||
|
"currency": "usd"
|
||||||
|
},
|
||||||
|
"package_dimensions": null,
|
||||||
|
"shippable": null,
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"tax_code": null,
|
||||||
|
"type": "service",
|
||||||
|
"unit_label": null,
|
||||||
|
"updated": 1749360062,
|
||||||
|
"url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prod_SSWhxv3tUy1YOG",
|
||||||
|
"object": "product",
|
||||||
|
"active": true,
|
||||||
|
"attributes": [],
|
||||||
|
"created": 1749359978,
|
||||||
|
"default_price": "price_1RXbdvKVZbzw7QA5ARomQvaf",
|
||||||
|
"description": "Our warm, crusty bread is generously spread with a savory garlic butter and toasted to golden perfection. It's the ideal aromatic and flavorful side to accompany your main course.",
|
||||||
|
"images": [
|
||||||
|
"https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfWTdIZTBkUjNZNFQ1ZEhSVG9nRnduY1pS00XVgLRRZD"
|
||||||
|
],
|
||||||
|
"livemode": false,
|
||||||
|
"marketing_features": [],
|
||||||
|
"metadata": {
|
||||||
|
"use_case": "food_ordering_demo"
|
||||||
|
},
|
||||||
|
"name": "Garlic Bread",
|
||||||
|
"price_info": {
|
||||||
|
"amount": 799,
|
||||||
|
"currency": "usd"
|
||||||
|
},
|
||||||
|
"package_dimensions": null,
|
||||||
|
"shippable": null,
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"tax_code": null,
|
||||||
|
"type": "service",
|
||||||
|
"unit_label": null,
|
||||||
|
"updated": 1749360084,
|
||||||
|
"url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prod_SSWgXa5bwUFCJs",
|
||||||
|
"object": "product",
|
||||||
|
"active": true,
|
||||||
|
"attributes": [],
|
||||||
|
"created": 1749359922,
|
||||||
|
"default_price": "price_1RXbd0KVZbzw7QA5Nq36vdLW",
|
||||||
|
"description": "A tribute to Italian simplicity, this pizza is topped with fresh mozzarella, a vibrant tomato sauce, and fragrant basil leaves. Each bite delivers a clean and authentic taste of Italy's most famous flavors.",
|
||||||
|
"images": [
|
||||||
|
"https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfamdmTXBFbzY0TW9rS2N0c2g0Tml2SERL00Evl60Ttq"
|
||||||
|
],
|
||||||
|
"livemode": false,
|
||||||
|
"marketing_features": [],
|
||||||
|
"metadata": {
|
||||||
|
"use_case": "food_ordering_demo"
|
||||||
|
},
|
||||||
|
"name": "Margherita Pizza",
|
||||||
|
"price_info": {
|
||||||
|
"amount": 1699,
|
||||||
|
"currency": "usd"
|
||||||
|
},
|
||||||
|
"package_dimensions": null,
|
||||||
|
"shippable": null,
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"tax_code": null,
|
||||||
|
"type": "service",
|
||||||
|
"unit_label": null,
|
||||||
|
"updated": 1749359998,
|
||||||
|
"url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prod_SSWf738UqIJzzi",
|
||||||
|
"object": "product",
|
||||||
|
"active": true,
|
||||||
|
"attributes": [],
|
||||||
|
"created": 1749359845,
|
||||||
|
"default_price": "price_1RXbbmKVZbzw7QA53EkjV2nB",
|
||||||
|
"description": "A timeless classic featuring a generous layer of savory pepperoni over rich tomato sauce and melted mozzarella cheese. It's the perfect choice for those who love a bold, meaty flavor on a perfectly baked crust.",
|
||||||
|
"images": [
|
||||||
|
"https://files.stripe.com/links/MDB8YWNjdF8xTkJPTHVLVlpienc3UUE1fGZsX3Rlc3RfcGRHc0c4cEZYWmR2bm0zOHBOa0FWMk5t008QmCJoWr"
|
||||||
|
],
|
||||||
|
"livemode": false,
|
||||||
|
"marketing_features": [],
|
||||||
|
"metadata": {
|
||||||
|
"use_case": "food_ordering_demo"
|
||||||
|
},
|
||||||
|
"name": "Pepperoni Pizza",
|
||||||
|
"price_info": {
|
||||||
|
"amount": 2299,
|
||||||
|
"currency": "usd"
|
||||||
|
},
|
||||||
|
"package_dimensions": null,
|
||||||
|
"shippable": null,
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"tax_code": null,
|
||||||
|
"type": "service",
|
||||||
|
"unit_label": null,
|
||||||
|
"updated": 1749359846,
|
||||||
|
"url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prod_SGMXBnatLlkJ4d",
|
||||||
|
"object": "product",
|
||||||
|
"active": true,
|
||||||
|
"attributes": [],
|
||||||
|
"created": 1746554502,
|
||||||
|
"default_price": "price_1RLpoJKVZbzw7QA5ra76Fk6g",
|
||||||
|
"description": null,
|
||||||
|
"images": [],
|
||||||
|
"livemode": false,
|
||||||
|
"marketing_features": [],
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ACME Scooter Token",
|
||||||
|
"package_dimensions": null,
|
||||||
|
"shippable": null,
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"tax_code": null,
|
||||||
|
"type": "service",
|
||||||
|
"unit_label": null,
|
||||||
|
"updated": 1746554503,
|
||||||
|
"url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prod_NxJPcqTWzXk45K",
|
||||||
|
"object": "product",
|
||||||
|
"active": true,
|
||||||
|
"attributes": [],
|
||||||
|
"created": 1684961969,
|
||||||
|
"default_price": null,
|
||||||
|
"description": "$12/Month subscription",
|
||||||
|
"images": [],
|
||||||
|
"livemode": false,
|
||||||
|
"marketing_features": [],
|
||||||
|
"metadata": {},
|
||||||
|
"name": "Starter Subscription",
|
||||||
|
"package_dimensions": null,
|
||||||
|
"shippable": null,
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"tax_code": null,
|
||||||
|
"type": "service",
|
||||||
|
"unit_label": null,
|
||||||
|
"updated": 1684961969,
|
||||||
|
"url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prod_NxJ4KvyENd0uUu",
|
||||||
|
"object": "product",
|
||||||
|
"active": true,
|
||||||
|
"attributes": [],
|
||||||
|
"created": 1684960731,
|
||||||
|
"default_price": null,
|
||||||
|
"description": "Created with the Stripe CLI",
|
||||||
|
"images": [],
|
||||||
|
"livemode": false,
|
||||||
|
"marketing_features": [],
|
||||||
|
"metadata": {},
|
||||||
|
"name": "Temporal Money Transfer",
|
||||||
|
"package_dimensions": null,
|
||||||
|
"shippable": null,
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"tax_code": null,
|
||||||
|
"type": "service",
|
||||||
|
"unit_label": null,
|
||||||
|
"updated": 1684960731,
|
||||||
|
"url": null
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import tools.tool_registry as tool_registry
|
import tools.tool_registry as tool_registry
|
||||||
from models.tool_definitions import AgentGoal
|
from models.tool_definitions import AgentGoal, MCPServerDefinition
|
||||||
|
|
||||||
# Turn on Silly Mode - this should be a description of the persona you'd like the bot to have and can be a single word or a phrase.
|
# 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:
|
||||||
@@ -311,7 +311,7 @@ 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",
|
||||||
@@ -351,7 +351,7 @@ goal_fin_move_money = AgentGoal(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# this starts a loan approval process
|
# this starts a loan approval process
|
||||||
# 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_loan_application = AgentGoal(
|
goal_fin_loan_application = AgentGoal(
|
||||||
id="goal_fin_loan_application",
|
id="goal_fin_loan_application",
|
||||||
category_tag="fin",
|
category_tag="fin",
|
||||||
@@ -455,6 +455,126 @@ goal_ecomm_list_orders = AgentGoal(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ----- MCP Integrations -----
|
||||||
|
goal_mcp_stripe = AgentGoal(
|
||||||
|
id="goal_mcp_stripe",
|
||||||
|
category_tag="mcp-integrations",
|
||||||
|
agent_name="Stripe MCP Agent",
|
||||||
|
agent_friendly_description="Manage Stripe operations via MCP",
|
||||||
|
tools=[], # Will be populated dynamically
|
||||||
|
mcp_server_definition=MCPServerDefinition(
|
||||||
|
name="stripe-mcp",
|
||||||
|
command="npx",
|
||||||
|
args=[
|
||||||
|
"-y",
|
||||||
|
"@stripe/mcp",
|
||||||
|
"--tools=all",
|
||||||
|
f"--api-key={os.getenv('STRIPE_API_KEY')}",
|
||||||
|
],
|
||||||
|
env=None,
|
||||||
|
included_tools=["list_customers", "list_products"],
|
||||||
|
),
|
||||||
|
description="Help manage Stripe operations for customer and product data by using the customers.read and products.read tools.",
|
||||||
|
starter_prompt="Welcome! I can help you read Stripe customer and product information.",
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"agent: Welcome! I can help you read Stripe customer and product information. What would you like to do first?",
|
||||||
|
"user: what customers are there?",
|
||||||
|
"agent: I'll check for customers now.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on customers.read tool>",
|
||||||
|
'tool_result: { "customers": [{"id": "cus_abc", "name": "Customer A"}, {"id": "cus_xyz", "name": "Customer B"}] }',
|
||||||
|
"agent: I found two customers: Customer A and Customer B. Can I help with anything else?",
|
||||||
|
"user: what products exist?",
|
||||||
|
"agent: Let me get the list of products for you.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on products.read tool>",
|
||||||
|
'tool_result: { "products": [{"id": "prod_123", "name": "Gold Plan"}, {"id": "prod_456", "name": "Silver Plan"}] }',
|
||||||
|
"agent: I found two products: Gold Plan and Silver Plan.",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- Food Ordering Goal -----
|
||||||
|
goal_food_ordering = AgentGoal(
|
||||||
|
id="goal_food_ordering",
|
||||||
|
category_tag="food",
|
||||||
|
agent_name="Food Ordering Assistant",
|
||||||
|
agent_friendly_description="Order food from Tony's Pizza Palace using Stripe for payment processing. Browse menu, add items to your order, and check out securely. Please ensure context carries over between tool runs.",
|
||||||
|
tools=[tool_registry.food_add_to_cart_tool],
|
||||||
|
mcp_server_definition=MCPServerDefinition(
|
||||||
|
name="stripe-mcp",
|
||||||
|
command="npx",
|
||||||
|
args=[
|
||||||
|
"-y",
|
||||||
|
"@stripe/mcp",
|
||||||
|
"--tools=all",
|
||||||
|
f"--api-key={os.getenv('STRIPE_API_KEY')}",
|
||||||
|
],
|
||||||
|
env=None,
|
||||||
|
included_tools=[
|
||||||
|
"list_products",
|
||||||
|
"list_prices",
|
||||||
|
"create_customer",
|
||||||
|
"create_invoice",
|
||||||
|
"create_invoice_item",
|
||||||
|
"finalize_invoice",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
description="The user wants to order food from Tony's Pizza Palace. "
|
||||||
|
"First, help the user browse the menu by calling list_products. "
|
||||||
|
"When they express interest in items, get pricing using list_prices. "
|
||||||
|
"Add items to their cart using AddToCart as they decide - the order doesn't matter, multiple items can be added. "
|
||||||
|
"After they're done selecting items, get their customer details and create a Stripe customer. "
|
||||||
|
"For checkout: 1) create_invoice, 2) create_invoice_item for each individual item (IMPORTANT: create_invoice_item does NOT accept quantity parameter - call it once per item, so if user wants 2 pizzas, call create_invoice_item twice with the same price), "
|
||||||
|
"3) finalize_invoice. The finalized invoice will contain a hosted_invoice_url for payment.",
|
||||||
|
starter_prompt=starter_prompt_generic,
|
||||||
|
example_conversation_history="\n ".join(
|
||||||
|
[
|
||||||
|
"user: I want to order food.",
|
||||||
|
"agent: Welcome to Tony's Pizza Palace! I can help you browse our menu, add items to your cart, and process payment. Let me show you what we have available.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on list_products tool with limit=100 and product.metadata.use_case == 'food_ordering_demo'>",
|
||||||
|
'tool_result: {"products": [{"id": "prod_pepperoni", "name": "Pepperoni Pizza", "description": "A timeless classic featuring pepperoni"}, {"id": "prod_margherita", "name": "Margherita Pizza", "description": "Fresh mozzarella and basil"}, {"id": "prod_garlic_bread", "name": "Garlic Bread", "description": "Warm crusty bread with garlic butter"}, {"id": "prod_soda", "name": "Soda", "description": "A large bottle of cola"}]}',
|
||||||
|
"agent: We have Pepperoni Pizza, Margherita Pizza, Garlic Bread, and Soda available. What would you like to order?",
|
||||||
|
"user: I'd like 2 pepperoni pizzas and a soda.",
|
||||||
|
"agent: Excellent! Let me get the pricing for the pepperoni pizzas first so you know the cost.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on list_prices tool with product='prod_pepperoni'>",
|
||||||
|
'tool_result: {"prices": [{"id": "price_pepperoni_large", "product": "prod_pepperoni", "unit_amount": 2299, "currency": "usd"}]}',
|
||||||
|
"agent: Pepperoni Pizza is $22.99 each. Now let me check the soda pricing:",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on list_prices tool with product='prod_soda'>",
|
||||||
|
'tool_result: {"prices": [{"id": "price_soda", "product": "prod_soda", "unit_amount": 250, "currency": "usd"}]}',
|
||||||
|
"agent: And Soda is $2.50. So 2 Pepperoni Pizzas ($22.99 each) plus 1 Soda ($2.50) = $48.48 total. What's your email address to add these items to your cart?",
|
||||||
|
"user: jane.smith@example.com",
|
||||||
|
"agent: Perfect! Adding 2 Pepperoni Pizzas to your cart for jane.smith@example.com. Note: I'm using quantity=2 since you want 2 pizzas.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on AddToCart tool with customer_email='jane.smith@example.com', item_name='Pepperoni Pizza', item_price=22.99, quantity=2, stripe_product_id='prod_pepperoni'>",
|
||||||
|
'tool_result: {"status": "success", "message": "Added 2 x Pepperoni Pizza ($22.99) to cart for jane.smith@example.com", "item_added": {"quantity": 2, "name": "Pepperoni Pizza"}}',
|
||||||
|
"agent: Great! Added 2 Pepperoni Pizzas to your cart. Now adding the Soda (quantity=1):",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on AddToCart tool with customer_email='jane.smith@example.com', item_name='Soda', item_price=2.50, quantity=1, stripe_product_id='prod_soda'>",
|
||||||
|
'tool_result: {"status": "success", "message": "Added 1 x Soda ($2.50) to cart for jane.smith@example.com", "item_added": {"quantity": 1, "name": "Soda"}}',
|
||||||
|
"agent: Excellent! Your cart now contains: 2 Pepperoni Pizzas ($22.99 each) and 1 Soda ($2.50). Ready to checkout? I'll need your full name.",
|
||||||
|
"user: Yes, let's proceed with payment. My name is Jane Smith.",
|
||||||
|
"agent: Thank you Jane! Creating your Stripe customer profile with name and email.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_customer tool with name='Jane Smith' and email='jane.smith@example.com'>",
|
||||||
|
'tool_result: {"customer": {"id": "cus_ExAmPlE12345", "name": "Jane Smith", "email": "jane.smith@example.com"}}',
|
||||||
|
"agent: Customer profile created! Now I'll create a draft invoice for your order.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_invoice tool with customer='cus_ExAmPlE12345'>",
|
||||||
|
'tool_result: {"invoice": {"id": "in_InvOicE54321", "status": "draft", "customer": "cus_ExAmPlE12345"}}',
|
||||||
|
"agent: Invoice created! Now adding your first Pepperoni Pizza to the invoice.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_pepperoni_large'>",
|
||||||
|
'tool_result: {"status": "success", "invoice_item": {"id": "ii_pizza1"}}',
|
||||||
|
"agent: First pizza added! Now adding your second Pepperoni Pizza to the invoice.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_pepperoni_large'>",
|
||||||
|
'tool_result: {"status": "success", "invoice_item": {"id": "ii_pizza2"}}',
|
||||||
|
"agent: Both pizzas added! Now adding your Soda to the invoice.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on create_invoice_item tool with customer='cus_ExAmPlE12345', invoice='in_InvOicE54321', price='price_soda'>",
|
||||||
|
'tool_result: {"status": "success", "invoice_item": {"id": "ii_soda"}}',
|
||||||
|
"agent: All items added to invoice! Now finalizing to generate the payment link.",
|
||||||
|
"user_confirmed_tool_run: <user clicks confirm on finalize_invoice tool with invoice='in_InvOicE54321'>",
|
||||||
|
'tool_result: {"invoice": {"id": "in_InvOicE54321", "status": "open", "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RKeJRMlw?s=ap", "amount_due": 4848}}',
|
||||||
|
"agent: Perfect! Your order is complete with 2 Pepperoni Pizzas and 1 Soda. Total: $48.48. Pay securely here: https://invoice.stripe.com/i/acct_1NBOLuKVZbzw7QA5/test_YWNjdF8xTkJPTHVLVlpienc3UUE1LF9SbGJlZ2xLMjhlc3lxNzFkVVBXbkswOUlFa2Rna1RxLDEzMDAwMTY4Nw0200RKeJRMlw?s=ap\\n\\nThank you for ordering from Tony's Pizza Palace!",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Add the goals to a list for more generic processing, like listing available agents
|
# 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)
|
||||||
@@ -469,19 +589,21 @@ 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_list_orders)
|
||||||
goal_list.append(goal_ecomm_order_status)
|
goal_list.append(goal_ecomm_order_status)
|
||||||
|
goal_list.append(goal_mcp_stripe)
|
||||||
|
goal_list.append(goal_food_ordering)
|
||||||
|
|
||||||
|
|
||||||
# for multi-goal, just set list agents as the last tool
|
# for multi-goal, just set list agents as the last tool
|
||||||
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:
|
||||||
multi_goal_mode = True # default if unset
|
multi_goal_mode = False # default to single agent mode if unset
|
||||||
elif (
|
elif (
|
||||||
first_goal_value is not None
|
first_goal_value is not None
|
||||||
and first_goal_value.lower() != "goal_choose_agent_type"
|
and first_goal_value.lower() == "goal_choose_agent_type"
|
||||||
):
|
):
|
||||||
multi_goal_mode = False
|
|
||||||
else:
|
|
||||||
multi_goal_mode = True
|
multi_goal_mode = True
|
||||||
|
else:
|
||||||
|
multi_goal_mode = False
|
||||||
|
|
||||||
if multi_goal_mode:
|
if multi_goal_mode:
|
||||||
for goal in goal_list:
|
for goal in goal_list:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import tools.goal_registry as goals
|
import goals
|
||||||
|
|
||||||
|
|
||||||
def list_agents(args: dict) -> dict:
|
def list_agents(args: dict) -> dict:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import http.client
|
import http.client
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -174,45 +175,166 @@ def search_flights_real_api(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_smart_flights(origin: str, destination: str) -> list:
|
||||||
|
"""
|
||||||
|
Generate realistic flight options with smart pricing based on origin and destination.
|
||||||
|
"""
|
||||||
|
# Common airlines for different regions
|
||||||
|
airlines_by_region = {
|
||||||
|
"domestic_us": [
|
||||||
|
{"name": "American Airlines", "code": "AA"},
|
||||||
|
{"name": "United Airlines", "code": "UA"},
|
||||||
|
{"name": "Delta Airlines", "code": "DL"},
|
||||||
|
{"name": "Southwest Airlines", "code": "WN"},
|
||||||
|
],
|
||||||
|
"us_international": [
|
||||||
|
{"name": "American Airlines", "code": "AA"},
|
||||||
|
{"name": "United Airlines", "code": "UA"},
|
||||||
|
{"name": "Delta Airlines", "code": "DL"},
|
||||||
|
{"name": "Virgin Atlantic", "code": "VS"},
|
||||||
|
],
|
||||||
|
"australia_nz": [
|
||||||
|
{"name": "Qantas", "code": "QF"},
|
||||||
|
{"name": "Jetstar", "code": "JQ"},
|
||||||
|
{"name": "Virgin Australia", "code": "VA"},
|
||||||
|
{"name": "Air New Zealand", "code": "NZ"},
|
||||||
|
],
|
||||||
|
"international": [
|
||||||
|
{"name": "American Airlines", "code": "AA"},
|
||||||
|
{"name": "United Airlines", "code": "UA"},
|
||||||
|
{"name": "Delta Airlines", "code": "DL"},
|
||||||
|
{"name": "Air New Zealand", "code": "NZ"},
|
||||||
|
{"name": "Qantas", "code": "QF"},
|
||||||
|
{"name": "Singapore Airlines", "code": "SQ"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine route type and base pricing
|
||||||
|
origin_lower = origin.lower()
|
||||||
|
dest_lower = destination.lower()
|
||||||
|
|
||||||
|
# Australia/NZ cities
|
||||||
|
anz_cities = [
|
||||||
|
"sydney",
|
||||||
|
"melbourne",
|
||||||
|
"syd",
|
||||||
|
"mel",
|
||||||
|
"auckland",
|
||||||
|
"akl",
|
||||||
|
"wellington",
|
||||||
|
"wlg",
|
||||||
|
"brisbane",
|
||||||
|
"bne",
|
||||||
|
"perth",
|
||||||
|
"per",
|
||||||
|
]
|
||||||
|
# US cities
|
||||||
|
us_cities = [
|
||||||
|
"los angeles",
|
||||||
|
"lax",
|
||||||
|
"san francisco",
|
||||||
|
"sfo",
|
||||||
|
"new york",
|
||||||
|
"nyc",
|
||||||
|
"jfk",
|
||||||
|
"chicago",
|
||||||
|
"ord",
|
||||||
|
"miami",
|
||||||
|
"mia",
|
||||||
|
]
|
||||||
|
|
||||||
|
is_origin_anz = any(city in origin_lower for city in anz_cities)
|
||||||
|
is_dest_anz = any(city in dest_lower for city in anz_cities)
|
||||||
|
is_origin_us = any(city in origin_lower for city in us_cities)
|
||||||
|
is_dest_us = any(city in dest_lower for city in us_cities)
|
||||||
|
|
||||||
|
# Determine airline pool and base price
|
||||||
|
if (is_origin_us and is_dest_anz) or (is_origin_anz and is_dest_us):
|
||||||
|
# Trans-Pacific routes
|
||||||
|
airline_pool = airlines_by_region["international"]
|
||||||
|
base_price = random.randint(1200, 1800)
|
||||||
|
elif is_origin_anz and is_dest_anz:
|
||||||
|
# Australia/NZ domestic
|
||||||
|
airline_pool = airlines_by_region["australia_nz"]
|
||||||
|
base_price = random.randint(300, 600)
|
||||||
|
elif is_origin_us and is_dest_us:
|
||||||
|
# US domestic
|
||||||
|
airline_pool = airlines_by_region["domestic_us"]
|
||||||
|
base_price = random.randint(200, 800)
|
||||||
|
else:
|
||||||
|
# General international
|
||||||
|
airline_pool = airlines_by_region["international"]
|
||||||
|
base_price = random.randint(800, 1500)
|
||||||
|
|
||||||
|
# Generate 3-4 flight options
|
||||||
|
num_flights = random.randint(3, 4)
|
||||||
|
results = []
|
||||||
|
used_airlines = set()
|
||||||
|
|
||||||
|
for i in range(num_flights):
|
||||||
|
# Pick unique airline
|
||||||
|
available_airlines = [a for a in airline_pool if a["name"] not in used_airlines]
|
||||||
|
if not available_airlines:
|
||||||
|
available_airlines = airline_pool # Reset if we run out
|
||||||
|
|
||||||
|
airline = random.choice(available_airlines)
|
||||||
|
used_airlines.add(airline["name"])
|
||||||
|
|
||||||
|
# Generate flight numbers
|
||||||
|
outbound_num = random.randint(100, 999)
|
||||||
|
return_num = random.randint(100, 999)
|
||||||
|
|
||||||
|
# Price variation (cheaper airlines get lower prices)
|
||||||
|
price_multiplier = 1.0
|
||||||
|
if "Southwest" in airline["name"] or "Jetstar" in airline["name"]:
|
||||||
|
price_multiplier = 0.7
|
||||||
|
elif "Virgin" in airline["name"]:
|
||||||
|
price_multiplier = 0.85
|
||||||
|
elif "Singapore" in airline["name"]:
|
||||||
|
price_multiplier = 1.2
|
||||||
|
|
||||||
|
# Add some random variation
|
||||||
|
price_variation = random.uniform(0.9, 1.1)
|
||||||
|
final_price = round(base_price * price_multiplier * price_variation, 2)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"operating_carrier": airline["name"],
|
||||||
|
"outbound_flight_code": f"{airline['code']}{outbound_num}",
|
||||||
|
"price": final_price,
|
||||||
|
"return_flight_code": f"{airline['code']}{return_num}",
|
||||||
|
"return_operating_carrier": airline["name"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort by price
|
||||||
|
results.sort(key=lambda x: x["price"])
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def search_flights(args: dict) -> dict:
|
def search_flights(args: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
Returns example flight search results in the requested JSON format.
|
Search for flights. Uses real API if RAPIDAPI_KEY is available, otherwise generates smart mock data.
|
||||||
"""
|
"""
|
||||||
|
load_dotenv(override=True)
|
||||||
|
api_key = os.getenv("RAPIDAPI_KEY")
|
||||||
|
|
||||||
origin = args.get("origin")
|
origin = args.get("origin")
|
||||||
destination = args.get("destination")
|
destination = args.get("destination")
|
||||||
|
|
||||||
|
if not origin or not destination:
|
||||||
|
return {"error": "Both origin and destination are required"}
|
||||||
|
|
||||||
|
# If API key is available, use the real API
|
||||||
|
if api_key and api_key != "YOUR_DEFAULT_KEY":
|
||||||
|
return search_flights_real_api(args)
|
||||||
|
|
||||||
|
# Otherwise, generate smart mock data
|
||||||
|
results = generate_smart_flights(origin, destination)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"destination": f"{destination}",
|
"destination": destination,
|
||||||
"origin": f"{origin}",
|
"origin": origin,
|
||||||
"results": [
|
"results": results,
|
||||||
{
|
|
||||||
"operating_carrier": "American Airlines",
|
|
||||||
"outbound_flight_code": "AA203",
|
|
||||||
"price": 1262.51,
|
|
||||||
"return_flight_code": "AA202",
|
|
||||||
"return_operating_carrier": "American Airlines",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"operating_carrier": "Air New Zealand",
|
|
||||||
"outbound_flight_code": "NZ488",
|
|
||||||
"price": 1396.00,
|
|
||||||
"return_flight_code": "NZ527",
|
|
||||||
"return_operating_carrier": "Air New Zealand",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"operating_carrier": "United Airlines",
|
|
||||||
"outbound_flight_code": "UA100",
|
|
||||||
"price": 1500.00,
|
|
||||||
"return_flight_code": "UA101",
|
|
||||||
"return_operating_carrier": "United Airlines",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"operating_carrier": "Delta Airlines",
|
|
||||||
"outbound_flight_code": "DL200",
|
|
||||||
"price": 1600.00,
|
|
||||||
"return_flight_code": "DL201",
|
|
||||||
"return_operating_carrier": "Delta Airlines",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
from models.tool_definitions import ToolArgument, ToolDefinition
|
from models.tool_definitions import ToolArgument, ToolDefinition
|
||||||
|
|
||||||
# ----- System tools -----
|
# ----- System tools -----
|
||||||
@@ -397,3 +399,75 @@ ecomm_track_package = ToolDefinition(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ----- Food Ordering Use Case Tools -----
|
||||||
|
food_add_to_cart_tool = ToolDefinition(
|
||||||
|
name="AddToCart",
|
||||||
|
description="Add a menu item to the customer's cart using item details from Stripe.",
|
||||||
|
arguments=[
|
||||||
|
ToolArgument(
|
||||||
|
name="customer_email",
|
||||||
|
type="string",
|
||||||
|
description="Email address of the customer",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="item_name",
|
||||||
|
type="string",
|
||||||
|
description="Name of the menu item (e.g., 'Margherita Pizza', 'Caesar Salad')",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="item_price",
|
||||||
|
type="number",
|
||||||
|
description="Price of the item in dollars (e.g., 14.99)",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="quantity",
|
||||||
|
type="number",
|
||||||
|
description="Quantity of the item to add (defaults to 1)",
|
||||||
|
),
|
||||||
|
ToolArgument(
|
||||||
|
name="stripe_product_id",
|
||||||
|
type="string",
|
||||||
|
description="Stripe product ID for reference (optional)",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# MCP Integration Functions
|
||||||
|
|
||||||
|
|
||||||
|
def create_mcp_tool_definitions(
|
||||||
|
mcp_tools_info: Dict[str, Dict],
|
||||||
|
) -> List[ToolDefinition]:
|
||||||
|
"""Convert MCP tool info to ToolDefinition objects"""
|
||||||
|
tool_definitions = []
|
||||||
|
|
||||||
|
for tool_name, tool_info in mcp_tools_info.items():
|
||||||
|
# Extract input schema properties
|
||||||
|
input_schema = tool_info.get("inputSchema", {})
|
||||||
|
properties = (
|
||||||
|
input_schema.get("properties", {}) if isinstance(input_schema, dict) else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert properties to ToolArgument objects
|
||||||
|
arguments = []
|
||||||
|
for param_name, param_info in properties.items():
|
||||||
|
if isinstance(param_info, dict):
|
||||||
|
arguments.append(
|
||||||
|
ToolArgument(
|
||||||
|
name=param_name,
|
||||||
|
type=param_info.get("type", "string"),
|
||||||
|
description=param_info.get("description", ""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create ToolDefinition
|
||||||
|
tool_def = ToolDefinition(
|
||||||
|
name=tool_info["name"],
|
||||||
|
description=tool_info.get("description", ""),
|
||||||
|
arguments=arguments,
|
||||||
|
)
|
||||||
|
tool_definitions.append(tool_def)
|
||||||
|
|
||||||
|
return tool_definitions
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ from workflows.workflow_helpers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
with workflow.unsafe.imports_passed_through():
|
with workflow.unsafe.imports_passed_through():
|
||||||
from activities.tool_activities import ToolActivities
|
from activities.tool_activities import ToolActivities, mcp_list_tools
|
||||||
|
from goals import goal_list
|
||||||
from models.data_types import CombinedInput, ToolPromptInput
|
from models.data_types import CombinedInput, ToolPromptInput
|
||||||
from prompts.agent_prompt_generators import generate_genai_prompt
|
from prompts.agent_prompt_generators import generate_genai_prompt
|
||||||
from tools.goal_registry import goal_list
|
from tools.tool_registry import create_mcp_tool_definitions
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
MAX_TURNS_BEFORE_CONTINUE = 250
|
MAX_TURNS_BEFORE_CONTINUE = 250
|
||||||
@@ -59,6 +60,7 @@ class AgentGoalWorkflow:
|
|||||||
self.multi_goal_mode: bool = (
|
self.multi_goal_mode: bool = (
|
||||||
False # set from env file in activity lookup_wf_env_settings
|
False # set from env file in activity lookup_wf_env_settings
|
||||||
)
|
)
|
||||||
|
self.mcp_tools_info: Optional[dict] = None # stores complete MCP tools result
|
||||||
|
|
||||||
# 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
|
||||||
@@ -70,6 +72,10 @@ class AgentGoalWorkflow:
|
|||||||
|
|
||||||
await self.lookup_wf_env_settings(combined_input)
|
await self.lookup_wf_env_settings(combined_input)
|
||||||
|
|
||||||
|
# If the goal has an MCP server definition, dynamically load MCP tools
|
||||||
|
if self.goal.mcp_server_definition:
|
||||||
|
await self.load_mcp_tools()
|
||||||
|
|
||||||
# add message from sample conversation provided in tools/goal_registry.py, if it exists
|
# add message from sample conversation provided in tools/goal_registry.py, if it exists
|
||||||
if params and params.conversation_summary:
|
if params and params.conversation_summary:
|
||||||
self.add_message("conversation_summary", params.conversation_summary)
|
self.add_message("conversation_summary", params.conversation_summary)
|
||||||
@@ -146,6 +152,7 @@ class AgentGoalWorkflow:
|
|||||||
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,
|
||||||
|
mcp_tools_info=self.mcp_tools_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt_input = ToolPromptInput(
|
prompt_input = ToolPromptInput(
|
||||||
@@ -368,6 +375,7 @@ class AgentGoalWorkflow:
|
|||||||
self.tool_results,
|
self.tool_results,
|
||||||
self.add_message,
|
self.add_message,
|
||||||
self.prompt_queue,
|
self.prompt_queue,
|
||||||
|
self.goal,
|
||||||
)
|
)
|
||||||
|
|
||||||
# set new goal if we should
|
# set new goal if we should
|
||||||
@@ -398,3 +406,43 @@ class AgentGoalWorkflow:
|
|||||||
else:
|
else:
|
||||||
print("no tool data initialized yet")
|
print("no tool data initialized yet")
|
||||||
print(f"self.confirmed: {self.confirmed}")
|
print(f"self.confirmed: {self.confirmed}")
|
||||||
|
|
||||||
|
async def load_mcp_tools(self) -> None:
|
||||||
|
"""Load MCP tools dynamically from the server definition"""
|
||||||
|
if not self.goal.mcp_server_definition:
|
||||||
|
return
|
||||||
|
|
||||||
|
workflow.logger.info(
|
||||||
|
f"Loading MCP tools from server: {self.goal.mcp_server_definition.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the list of tools to include (if specified)
|
||||||
|
include_tools = self.goal.mcp_server_definition.included_tools
|
||||||
|
|
||||||
|
# Call the MCP list tools activity
|
||||||
|
mcp_tools_result = await workflow.execute_activity(
|
||||||
|
mcp_list_tools,
|
||||||
|
args=[self.goal.mcp_server_definition, include_tools],
|
||||||
|
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||||
|
retry_policy=RetryPolicy(
|
||||||
|
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||||
|
),
|
||||||
|
summary=f"{self.goal.mcp_server_definition.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if mcp_tools_result.get("success", False):
|
||||||
|
tools_info = mcp_tools_result.get("tools", {})
|
||||||
|
workflow.logger.info(f"Successfully loaded {len(tools_info)} MCP tools")
|
||||||
|
|
||||||
|
# Store complete MCP tools result for use in prompt generation
|
||||||
|
self.mcp_tools_info = mcp_tools_result
|
||||||
|
|
||||||
|
# Convert MCP tools to ToolDefinition objects and add to goal
|
||||||
|
mcp_tool_definitions = create_mcp_tool_definitions(tools_info)
|
||||||
|
self.goal.tools.extend(mcp_tool_definitions)
|
||||||
|
|
||||||
|
workflow.logger.info(f"Added {len(mcp_tool_definitions)} MCP tools to goal")
|
||||||
|
else:
|
||||||
|
error_msg = mcp_tools_result.get("error", "Unknown error")
|
||||||
|
workflow.logger.error(f"Failed to load MCP tools: {error_msg}")
|
||||||
|
# Continue execution without MCP tools
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from temporalio.common import RetryPolicy
|
|||||||
from temporalio.exceptions import ActivityError
|
from temporalio.exceptions import ActivityError
|
||||||
|
|
||||||
from models.data_types import ConversationHistory, ToolPromptInput
|
from models.data_types import ConversationHistory, ToolPromptInput
|
||||||
|
from models.tool_definitions import AgentGoal
|
||||||
from prompts.agent_prompt_generators import (
|
from prompts.agent_prompt_generators import (
|
||||||
generate_missing_args_prompt,
|
generate_missing_args_prompt,
|
||||||
generate_tool_completion_prompt,
|
generate_tool_completion_prompt,
|
||||||
@@ -19,23 +20,104 @@ LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT = timedelta(seconds=20)
|
|||||||
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT = timedelta(minutes=30)
|
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT = timedelta(minutes=30)
|
||||||
|
|
||||||
|
|
||||||
|
def is_mcp_tool(tool_name: str, goal: AgentGoal) -> bool:
|
||||||
|
"""Check if a tool is an MCP tool based on the goal's MCP server definition"""
|
||||||
|
if not goal.mcp_server_definition:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if the tool name matches any MCP tools that were loaded
|
||||||
|
# We can identify MCP tools by checking if they're not in the original static tools
|
||||||
|
from tools.tool_registry import (
|
||||||
|
book_pto_tool,
|
||||||
|
book_trains_tool,
|
||||||
|
change_goal_tool,
|
||||||
|
create_invoice_tool,
|
||||||
|
current_pto_tool,
|
||||||
|
ecomm_get_order,
|
||||||
|
ecomm_list_orders,
|
||||||
|
ecomm_track_package,
|
||||||
|
financial_check_account_is_valid,
|
||||||
|
financial_get_account_balances,
|
||||||
|
financial_move_money,
|
||||||
|
financial_submit_loan_approval,
|
||||||
|
find_events_tool,
|
||||||
|
food_add_to_cart_tool,
|
||||||
|
future_pto_calc_tool,
|
||||||
|
give_hint_tool,
|
||||||
|
guess_location_tool,
|
||||||
|
list_agents_tool,
|
||||||
|
paycheck_bank_integration_status_check,
|
||||||
|
search_fixtures_tool,
|
||||||
|
search_flights_tool,
|
||||||
|
search_trains_tool,
|
||||||
|
)
|
||||||
|
|
||||||
|
static_tool_names = {
|
||||||
|
list_agents_tool.name,
|
||||||
|
change_goal_tool.name,
|
||||||
|
give_hint_tool.name,
|
||||||
|
guess_location_tool.name,
|
||||||
|
search_flights_tool.name,
|
||||||
|
search_trains_tool.name,
|
||||||
|
book_trains_tool.name,
|
||||||
|
create_invoice_tool.name,
|
||||||
|
search_fixtures_tool.name,
|
||||||
|
find_events_tool.name,
|
||||||
|
current_pto_tool.name,
|
||||||
|
future_pto_calc_tool.name,
|
||||||
|
book_pto_tool.name,
|
||||||
|
paycheck_bank_integration_status_check.name,
|
||||||
|
financial_check_account_is_valid.name,
|
||||||
|
financial_get_account_balances.name,
|
||||||
|
financial_move_money.name,
|
||||||
|
financial_submit_loan_approval.name,
|
||||||
|
ecomm_list_orders.name,
|
||||||
|
ecomm_get_order.name,
|
||||||
|
ecomm_track_package.name,
|
||||||
|
food_add_to_cart_tool.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tool_name not in static_tool_names
|
||||||
|
|
||||||
|
|
||||||
async def handle_tool_execution(
|
async def handle_tool_execution(
|
||||||
current_tool: str,
|
current_tool: str,
|
||||||
tool_data: Dict[str, Any],
|
tool_data: Dict[str, Any],
|
||||||
tool_results: list,
|
tool_results: list,
|
||||||
add_message_callback: callable,
|
add_message_callback: callable,
|
||||||
prompt_queue: Deque[str],
|
prompt_queue: Deque[str],
|
||||||
|
goal: AgentGoal = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Execute a tool after confirmation and handle its result."""
|
"""Execute a tool after confirmation and handle its result."""
|
||||||
workflow.logger.info(f"Confirmed. Proceeding with tool: {current_tool}")
|
workflow.logger.info(f"Confirmed. Proceeding with tool: {current_tool}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if this is an MCP tool
|
||||||
|
if goal and is_mcp_tool(current_tool, goal):
|
||||||
|
workflow.logger.info(f"Executing MCP tool: {current_tool}")
|
||||||
|
|
||||||
|
# Add server definition to args for MCP tools
|
||||||
|
mcp_args = tool_data["args"].copy()
|
||||||
|
mcp_args["server_definition"] = goal.mcp_server_definition
|
||||||
|
|
||||||
|
dynamic_result = await workflow.execute_activity(
|
||||||
|
current_tool,
|
||||||
|
mcp_args,
|
||||||
|
schedule_to_close_timeout=TOOL_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
|
||||||
|
start_to_close_timeout=TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT,
|
||||||
|
retry_policy=RetryPolicy(
|
||||||
|
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||||
|
),
|
||||||
|
summary=f"{goal.mcp_server_definition.name} (MCP Tool)",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Handle regular tools
|
||||||
task_queue = (
|
task_queue = (
|
||||||
TEMPORAL_LEGACY_TASK_QUEUE
|
TEMPORAL_LEGACY_TASK_QUEUE
|
||||||
if current_tool in ["SearchTrains", "BookTrains"]
|
if current_tool in ["SearchTrains", "BookTrains"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
dynamic_result = await workflow.execute_activity(
|
dynamic_result = await workflow.execute_activity(
|
||||||
current_tool,
|
current_tool,
|
||||||
tool_data["args"],
|
tool_data["args"],
|
||||||
@@ -46,8 +128,10 @@ async def handle_tool_execution(
|
|||||||
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
initial_interval=timedelta(seconds=5), backoff_coefficient=1
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
dynamic_result["tool"] = current_tool
|
dynamic_result["tool"] = current_tool
|
||||||
tool_results.append(dynamic_result)
|
tool_results.append(dynamic_result)
|
||||||
|
|
||||||
except ActivityError as e:
|
except ActivityError as e:
|
||||||
workflow.logger.error(f"Tool execution failed: {str(e)}")
|
workflow.logger.error(f"Tool execution failed: {str(e)}")
|
||||||
dynamic_result = {"error": str(e), "tool": current_tool}
|
dynamic_result = {"error": str(e), "tool": current_tool}
|
||||||
|
|||||||