diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/Dockerfile b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/Dockerfile new file mode 100644 index 00000000..531cf804 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/Dockerfile @@ -0,0 +1,48 @@ +# syntax=docker/dockerfile:1.3 +FROM python:3.12-slim +COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/ + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + htop \ + vim \ + curl \ + tar \ + python3-dev \ + postgresql-client \ + build-essential \ + libpq-dev \ + gcc \ + cmake \ + netcat-openbsd \ + nodejs \ + npm \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/** + +# Install tctl (Temporal CLI) +RUN curl -L https://github.com/temporalio/tctl/releases/download/v1.18.1/tctl_1.18.1_linux_arm64.tar.gz -o /tmp/tctl.tar.gz && \ + tar -xzf /tmp/tctl.tar.gz -C /usr/local/bin && \ + chmod +x /usr/local/bin/tctl && \ + rm /tmp/tctl.tar.gz + +RUN uv pip install --system --upgrade pip setuptools wheel + +ENV UV_HTTP_TIMEOUT=1000 + +# Copy just the pyproject.toml file to optimize caching +COPY example_tutorial/pyproject.toml /app/example_tutorial/pyproject.toml + +WORKDIR /app/example_tutorial + +# Install the required Python packages using uv +RUN uv pip install --system . + +# Copy the project code +COPY example_tutorial/project /app/example_tutorial/project + +# Run the ACP server using uvicorn +CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"] + +# When we deploy the worker, we will replace the CMD with the following +# CMD ["python", "-m", "run_worker"] \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/README.md b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/README.md new file mode 100644 index 00000000..c46722b8 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/README.md @@ -0,0 +1,317 @@ +# example-tutorial - AgentEx Temporal Agent Template + +This is a starter template for building asynchronous agents with the AgentEx framework and Temporal. It provides a basic implementation of the Agent 2 Client Protocol (ACP) with Temporal workflow support to help you get started quickly. + +## What You'll Learn + +- **Tasks**: A task is a grouping mechanism for related messages. Think of it as a conversation thread or a session. +- **Messages**: Messages are communication objects within a task. They can contain text, data, or instructions. +- **ACP Events**: The agent responds to four main events: + - `task_received`: When a new task is created + - `task_message_received`: When a message is sent within a task + - `task_approved`: When a task is approved + - `task_canceled`: When a task is canceled +- **Temporal Workflows**: Long-running processes that can handle complex state management and async operations + +## Running the Agent + +1. Run the agent locally: +```bash +agentex agents run --manifest manifest.yaml +``` + +The agent will start on port 8000 and print messages whenever it receives any of the ACP events. + +## What's Inside + +This template: +- Sets up a basic ACP server with Temporal integration +- Handles each of the required ACP events +- Provides a foundation for building complex async agents +- Includes Temporal workflow and activity definitions + +## Next Steps + +For more advanced agent development, check out the AgentEx tutorials: + +- **Tutorials 00-08**: Learn about building synchronous agents with ACP +- **Tutorials 09-10**: Learn how to use Temporal to power asynchronous agents + - Tutorial 09: Basic Temporal workflow setup + - Tutorial 10: Advanced Temporal patterns and best practices + +These tutorials will help you understand: +- How to handle long-running tasks +- Implementing state machines +- Managing complex workflows +- Best practices for async agent development + +## The Manifest File + +The `manifest.yaml` file is your agent's configuration file. It defines: +- How your agent should be built and packaged +- What files are included in your agent's Docker image +- Your agent's name and description +- Local development settings (like the port your agent runs on) +- Temporal worker configuration + +This file is essential for both local development and deployment of your agent. + +## Project Structure + +``` +example_tutorial/ +├── project/ # Your agent's code +│ ├── __init__.py +│ ├── acp.py # ACP server and event handlers +│ ├── workflow.py # Temporal workflow definitions +│ ├── activities.py # Temporal activity definitions +│ └── run_worker.py # Temporal worker setup +├── Dockerfile # Container definition +├── manifest.yaml # Deployment config +├── dev.ipynb # Development notebook for testing + +└── pyproject.toml # Dependencies (uv) + +``` + +## Development + +### 1. Customize Event Handlers +- Modify the handlers in `acp.py` to implement your agent's logic +- Add your own tools and capabilities +- Implement custom state management + +### 2. Test Your Agent with the Development Notebook +Use the included `dev.ipynb` Jupyter notebook to test your agent interactively: + +```bash +# Start Jupyter notebook (make sure you have jupyter installed) +jupyter notebook dev.ipynb + +# Or use VS Code to open the notebook directly +code dev.ipynb +``` + +The notebook includes: +- **Setup**: Connect to your local AgentEx backend +- **Task creation**: Create a new task for the conversation +- **Event sending**: Send events to the agent and get responses +- **Async message subscription**: Subscribe to server-side events to receive agent responses +- **Rich message display**: Beautiful formatting with timestamps and author information + +The notebook automatically uses your agent name (`example-tutorial`) and demonstrates the agentic ACP workflow: create task → send event → subscribe to responses. + +### 3. Develop Temporal Workflows +- Edit `workflow.py` to define your agent's async workflow logic +- Modify `activities.py` to add custom activities +- Use `run_worker.py` to configure the Temporal worker + +### 4. Manage Dependencies + + +You chose **uv** for package management. Here's how to work with dependencies: + +```bash +# Add new dependencies +agentex uv add requests openai anthropic + +# Add Temporal-specific dependencies (already included) +agentex uv add temporalio + +# Install/sync dependencies +agentex uv sync + +# Run commands with uv +uv run agentex agents run --manifest manifest.yaml +``` + +**Benefits of uv:** +- Faster dependency resolution and installation +- Better dependency isolation +- Modern Python packaging standards + + + +### 5. Configure Credentials +- Add any required credentials to your manifest.yaml +- For local development, create a `.env` file in the project directory +- Use `load_dotenv()` only in development mode: + +```python +import os +from dotenv import load_dotenv + +if os.environ.get("ENVIRONMENT") == "development": + load_dotenv() +``` + +## Local Development + +### 1. Start the Agentex Backend +```bash +# Navigate to the backend directory +cd agentex + +# Start all services using Docker Compose +make dev + +# Optional: In a separate terminal, use lazydocker for a better UI (everything should say "healthy") +lzd +``` + +### 2. Setup Your Agent's requirements/pyproject.toml +```bash +agentex uv sync [--group editable-apy] +source .venv/bin/activate + +# OR +conda create -n example_tutorial python=3.12 +conda activate example_tutorial +pip install -r requirements.txt +``` +### 3. Run Your Agent +```bash +# From this directory +export ENVIRONMENT=development && [uv run] agentex agents run --manifest manifest.yaml +``` +4. **Interact with your agent** + +Option 0: CLI (deprecated - to be replaced once a new CLI is implemented - please use the web UI for now!) +```bash +# Submit a task via CLI +agentex tasks submit --agent example-tutorial --task "Your task here" +``` + +Option 1: Web UI +```bash +# Start the local web interface +cd agentex-web +make dev + +# Then open http://localhost:3000 in your browser to chat with your agent +``` + +## Development Tips + +### Environment Variables +- Set environment variables in project/.env for any required credentials +- Or configure them in the manifest.yaml under the `env` section +- The `.env` file is automatically loaded in development mode + +### Local Testing +- Use `export ENVIRONMENT=development` before running your agent +- This enables local service discovery and debugging features +- Your agent will automatically connect to locally running services + +### Temporal-Specific Tips +- Monitor workflows in the Temporal Web UI at http://localhost:8080 +- Use the Temporal CLI for advanced workflow management +- Check workflow logs for debugging async operations + +### Debugging +- Check agent logs in the terminal where you ran the agent +- Use the web UI to inspect task history and responses +- Monitor backend services with `lzd` (LazyDocker) +- Use Temporal Web UI for workflow debugging + +### To build the agent Docker image locally (normally not necessary): + +1. Build the agent image: +```bash +agentex agents build --manifest manifest.yaml +``` + +## Advanced Features + +### Temporal Workflows +Extend your agent with sophisticated async workflows: + +```python +# In project/workflow.py +@workflow.defn +class MyWorkflow(BaseWorkflow): + async def complex_operation(self): + # Multi-step async operations + # Error handling and retries + # State management + pass +``` + +### Custom Activities +Add custom activities for external operations. **Important**: Always specify appropriate timeouts (recommended: 10 minutes): + +```python +# In project/activities.py +from datetime import timedelta +from temporalio import activity +from temporalio.common import RetryPolicy + +@activity.defn(name="call_external_api") +async def call_external_api(data): + # HTTP requests, database operations, etc. + pass + +# In your workflow, call it with a timeout: +result = await workflow.execute_activity( + "call_external_api", + data, + start_to_close_timeout=timedelta(minutes=10), # Recommended: 10 minute timeout + heartbeat_timeout=timedelta(minutes=1), # Optional: heartbeat monitoring + retry_policy=RetryPolicy(maximum_attempts=3) # Optional: retry policy +) + +# Don't forget to register your custom activities in run_worker.py: +# all_activities = get_all_activities() + [your_custom_activity_function] +``` + +### Integration with External Services + +```bash +# Add service clients +agentex uv add httpx requests-oauthlib + +# Add AI/ML libraries +agentex uv add openai anthropic transformers + +# Add database clients +agentex uv add asyncpg redis +``` + + +## Troubleshooting + +### Common Issues + +1. **Agent not appearing in web UI** + - Check if agent is running on port 8000 + - Verify `ENVIRONMENT=development` is set + - Check agent logs for errors + +2. **Temporal workflow issues** + - Check Temporal Web UI at http://localhost:8080 + - Verify Temporal server is running in backend services + - Check workflow logs for specific errors + +3. **Dependency issues** + + - Run `agentex uv sync` to ensure all dependencies are installed + - Verify temporalio is properly installed + + +4. **Port conflicts** + - Check if another service is using port 8000 + - Use `lsof -i :8000` to find conflicting processes + +### Temporal-Specific Troubleshooting + +1. **Workflow not starting** + - Check if Temporal server is running (`docker ps`) + - Verify task queue configuration in `run_worker.py` + - Check workflow registration in the worker + +2. **Activity failures** + - Check activity logs in the console + - Verify activity registration + - Check for timeout issues + +Happy building with Temporal! 🚀⚡ \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/dev.ipynb b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/dev.ipynb new file mode 100644 index 00000000..04aa5cb9 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/dev.ipynb @@ -0,0 +1,126 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "36834357", + "metadata": {}, + "outputs": [], + "source": [ + "from agentex import Agentex\n", + "\n", + "client = Agentex(base_url=\"http://localhost:5003\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1c309d6", + "metadata": {}, + "outputs": [], + "source": [ + "AGENT_NAME = \"example-tutorial\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f6e6ef0", + "metadata": {}, + "outputs": [], + "source": [ + "# (REQUIRED) Create a new task. For Agentic agents, you must create a task for messages to be associated with.\n", + "import uuid\n", + "\n", + "rpc_response = client.agents.create_task(\n", + " agent_name=AGENT_NAME,\n", + " params={\n", + " \"name\": f\"{str(uuid.uuid4())[:8]}-task\",\n", + " \"params\": {}\n", + " }\n", + ")\n", + "\n", + "task = rpc_response.result\n", + "print(task)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b03b0d37", + "metadata": {}, + "outputs": [], + "source": [ + "# Send an event to the agent\n", + "\n", + "# The response is expected to be a list of TaskMessage objects, which is a union of the following types:\n", + "# - TextContent: A message with just text content \n", + "# - DataContent: A message with JSON-serializable data content\n", + "# - ToolRequestContent: A message with a tool request, which contains a JSON-serializable request to call a tool\n", + "# - ToolResponseContent: A message with a tool response, which contains response object from a tool call in its content\n", + "\n", + "# When processing the message/send response, if you are expecting more than TextContent, such as DataContent, ToolRequestContent, or ToolResponseContent, you can process them as well\n", + "\n", + "rpc_response = client.agents.send_event(\n", + " agent_name=AGENT_NAME,\n", + " params={\n", + " \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n", + " \"task_id\": task.id,\n", + " }\n", + ")\n", + "\n", + "event = rpc_response.result\n", + "print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6927cc0", + "metadata": {}, + "outputs": [], + "source": [ + "# Subscribe to the async task messages produced by the agent\n", + "from agentex.lib.utils.dev_tools import subscribe_to_async_task_messages\n", + "\n", + "task_messages = subscribe_to_async_task_messages(\n", + " client=client,\n", + " task=task, \n", + " only_after_timestamp=event.created_at, \n", + " print_messages=True,\n", + " rich_print=True,\n", + " timeout=5,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4864e354", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/environments.yaml b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/environments.yaml new file mode 100644 index 00000000..f9051191 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/environments.yaml @@ -0,0 +1,64 @@ +# Agent Environment Configuration +# ------------------------------ +# This file defines environment-specific settings for your agent. +# This DIFFERS from the manifest.yaml file in that it is used to program things that are ONLY per environment. + +# ********** EXAMPLE ********** +# schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI +# environments: +# dev: +# auth: +# principal: +# user_id: "1234567890" +# user_name: "John Doe" +# user_email: "john.doe@example.com" +# user_role: "admin" +# user_permissions: "read, write, delete" +# helm_overrides: # This is used to override the global helm values.yaml file in the agentex-agent helm charts +# replicas: 3 +# resources: +# requests: +# cpu: "1000m" +# memory: "2Gi" +# limits: +# cpu: "2000m" +# memory: "4Gi" +# env: +# - name: LOG_LEVEL +# value: "DEBUG" +# - name: ENVIRONMENT +# value: "staging" +# +# kubernetes: +# # OPTIONAL - Otherwise it will be derived from separately. However, this can be used to override the derived +# # namespace and deploy it with in the same namespace that already exists for a separate agent. +# namespace: "team-example-tutorial" +# ********** END EXAMPLE ********** + +schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI +environments: + dev: + auth: + principal: + user_id: # TODO: Fill in + account_id: # TODO: Fill in + helm_overrides: + # This is used to override the global helm values.yaml file in the agentex-agent helm charts + replicaCount: 2 + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" + temporal-worker: + enabled: true + replicaCount: 2 + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/manifest.yaml b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/manifest.yaml new file mode 100644 index 00000000..a165dca9 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/manifest.yaml @@ -0,0 +1,140 @@ +# Agent Manifest Configuration +# --------------------------- +# This file defines how your agent should be built and deployed. + +# Build Configuration +# ------------------ +# The build config defines what gets packaged into your agent's Docker image. +# This same configuration is used whether building locally or remotely. +# +# When building: +# 1. All files from include_paths are collected into a build context +# 2. The context is filtered by dockerignore rules +# 3. The Dockerfile uses this context to build your agent's image +# 4. The image is pushed to a registry and used to run your agent +build: + context: + # Root directory for the build context + root: ../ # Keep this as the default root + + # Paths to include in the Docker build context + # Must include: + # - Your agent's directory (your custom agent code) + # These paths are collected and sent to the Docker daemon for building + include_paths: + - example_tutorial + + # Path to your agent's Dockerfile + # This defines how your agent's image is built from the context + # Relative to the root directory + dockerfile: example_tutorial/Dockerfile + + # Path to your agent's .dockerignore + # Filters unnecessary files from the build context + # Helps keep build context small and builds fast + dockerignore: example_tutorial/.dockerignore + + +# Local Development Configuration +# ----------------------------- +# Only used when running the agent locally +local_development: + agent: + port: 8000 # Port where your local ACP server is running + host_address: host.docker.internal # Host address for Docker networking (host.docker.internal for Docker, localhost for direct) + + # File paths for local development (relative to this manifest.yaml) + paths: + # Path to ACP server file + # Examples: + # project/acp.py (standard) + # src/server.py (custom structure) + # ../shared/acp.py (shared across projects) + # /absolute/path/acp.py (absolute path) + acp: project/acp.py + + # Path to temporal worker file + # Examples: + # project/run_worker.py (standard) + # workers/temporal.py (custom structure) + # ../shared/worker.py (shared across projects) + worker: project/run_worker.py + + +# Agent Configuration +# ----------------- +agent: + # Type of agent - either sync or agentic + acp_type: agentic + + # Unique name for your agent + # Used for task routing and monitoring + name: example-tutorial + + # Description of what your agent does + # Helps with documentation and discovery + description: An AgentEx agent + + # Temporal workflow configuration + # This enables your agent to run as a Temporal workflow for long-running tasks + temporal: + enabled: true + workflows: + # Name of the workflow class + # Must match the @workflow.defn name in your workflow.py + - name: example-tutorial + + # Queue name for task distribution + # Used by Temporal to route tasks to your agent + # Convention: _task_queue + queue_name: example_tutorial_queue + + # Optional: Credentials mapping + # Maps Kubernetes secrets to environment variables + # Common credentials include: + credentials: + - env_var_name: REDIS_URL + secret_name: redis-url-secret + secret_key: url + # - env_var_name: OPENAI_API_KEY + # secret_name: openai-api-key + # secret_key: api-key + + # Optional: Set Environment variables for running your agent locally as well + # as for deployment later on + env: + OPENAI_API_KEY: "" + # OPENAI_BASE_URL: "" + OPENAI_ORG_ID: "" + + +# Deployment Configuration +# ----------------------- +# Configuration for deploying your agent to Kubernetes clusters +deployment: + # Container image configuration + image: + repository: "" # Update with your container registry + tag: "latest" # Default tag, should be versioned in production + + imagePullSecrets: + - name: my-registry-secret # Update with your image pull secret name + + # Global deployment settings that apply to all clusters + # These can be overridden using --override-file with custom configuration files + global: + agent: + name: "example-tutorial" + description: "An AgentEx agent" + + # Default replica count + replicaCount: 1 + + # Default resource requirements + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/__init__.py b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/acp.py b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/acp.py new file mode 100644 index 00000000..e04aaaab --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/acp.py @@ -0,0 +1,64 @@ +import os +import sys + +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin + +# === DEBUG SETUP (AgentEx CLI Debug Support) === +if os.getenv("AGENTEX_DEBUG_ENABLED") == "true": + try: + import debugpy + debug_port = int(os.getenv("AGENTEX_DEBUG_PORT", "5679")) + debug_type = os.getenv("AGENTEX_DEBUG_TYPE", "acp") + wait_for_attach = os.getenv("AGENTEX_DEBUG_WAIT_FOR_ATTACH", "false").lower() == "true" + + # Configure debugpy + debugpy.configure(subProcess=False) + debugpy.listen(debug_port) + + print(f"🐛 [{debug_type.upper()}] Debug server listening on port {debug_port}") + + if wait_for_attach: + print(f"⏳ [{debug_type.upper()}] Waiting for debugger to attach...") + debugpy.wait_for_client() + print(f"✅ [{debug_type.upper()}] Debugger attached!") + else: + print(f"📡 [{debug_type.upper()}] Ready for debugger attachment") + + except ImportError: + print("❌ debugpy not available. Install with: pip install debugpy") + sys.exit(1) + except Exception as e: + print(f"❌ Debug setup failed: {e}") + sys.exit(1) +# === END DEBUG SETUP === + +from agentex.lib.types.fastacp import TemporalACPConfig +from agentex.lib.sdk.fastacp.fastacp import FastACP + +# Create the ACP server +acp = FastACP.create( + acp_type="agentic", + config=TemporalACPConfig( + # When deployed to the cluster, the Temporal address will automatically be set to the cluster address + # For local development, we set the address manually to talk to the local Temporal service set up via docker compose + # We are also adding the Open AI Agents SDK plugin to the ACP. + type="temporal", + temporal_address=os.getenv("TEMPORAL_ADDRESS", "localhost:7233"), + plugins=[OpenAIAgentsPlugin()] + ) +) + + +# Notice that we don't need to register any handlers when we use type="temporal" +# If you look at the code in agentex.sdk.fastacp.impl.temporal_acp +# You can see that these handlers are automatically registered when the ACP is created + +# @acp.on_task_create +# This will be handled by the method in your workflow that is decorated with @workflow.run + +# @acp.on_task_event_send +# This will be handled by the method in your workflow that is decorated with @workflow.signal(name=SignalName.RECEIVE_MESSAGE) + +# @acp.on_task_cancel +# This does not need to be handled by your workflow. +# It is automatically handled by the temporal client which cancels the workflow directly \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/run_worker.py b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/run_worker.py new file mode 100644 index 00000000..04a44c49 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/run_worker.py @@ -0,0 +1,41 @@ +import asyncio + +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin + +from project.workflow import ExampleTutorialWorkflow +from agentex.lib.utils.debug import setup_debug_if_enabled +from agentex.lib.utils.logging import make_logger +from agentex.lib.environment_variables import EnvironmentVariables +from agentex.lib.core.temporal.activities import get_all_activities +from agentex.lib.core.temporal.workers.worker import AgentexWorker + +environment_variables = EnvironmentVariables.refresh() + +logger = make_logger(__name__) + + +async def main(): + # Setup debug mode if enabled + setup_debug_if_enabled() + + task_queue_name = environment_variables.WORKFLOW_TASK_QUEUE + if task_queue_name is None: + raise ValueError("WORKFLOW_TASK_QUEUE is not set") + + # Add activities to the worker + all_activities = get_all_activities() + [] # add your own activities here + + # Create a worker with automatic tracing + # We are also adding the Open AI Agents SDK plugin to the worker. + worker = AgentexWorker( + task_queue=task_queue_name, + plugins=[OpenAIAgentsPlugin()] + ) + + await worker.run( + activities=all_activities, + workflow=ExampleTutorialWorkflow, + ) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/workflow.py b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/workflow.py new file mode 100644 index 00000000..1c529e05 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/project/workflow.py @@ -0,0 +1,227 @@ +""" +OpenAI Agents SDK + Temporal Integration: Hello World Tutorial + +This tutorial demonstrates the fundamental integration between OpenAI Agents SDK and Temporal workflows. +It shows how to: + +1. Set up a basic Temporal workflow with OpenAI Agents SDK +2. Create a simple agent that responds to user messages +3. See how agent conversations become durable through Temporal +4. Understand the automatic activity creation for model invocations + +KEY CONCEPTS DEMONSTRATED: +- Basic agent creation with OpenAI Agents SDK +- Temporal workflow durability for agent conversations +- Automatic activity creation for LLM calls (visible in Temporal UI) +- Long-running agent workflows that can survive restarts + +This is the foundation before moving to more advanced patterns with tools and activities. +""" + +import json + +from agents import Agent, Runner +from temporalio import workflow + +from agentex.lib import adk +from agentex.lib.types.acp import SendEventParams, CreateTaskParams +from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent +from agentex.lib.environment_variables import EnvironmentVariables +from agentex.lib.core.temporal.types.workflow import SignalName +from agentex.lib.core.temporal.workflows.workflow import BaseWorkflow + +environment_variables = EnvironmentVariables.refresh() + +if environment_variables.WORKFLOW_NAME is None: + raise ValueError("Environment variable WORKFLOW_NAME is not set") + +if environment_variables.AGENT_NAME is None: + raise ValueError("Environment variable AGENT_NAME is not set") + +logger = make_logger(__name__) + +@workflow.defn(name=environment_variables.WORKFLOW_NAME) +class ExampleTutorialWorkflow(BaseWorkflow): + """ + Hello World Temporal Workflow with OpenAI Agents SDK Integration + + This workflow demonstrates the basic pattern for integrating OpenAI Agents SDK + with Temporal workflows. It shows how agent conversations become durable and + observable through Temporal's workflow engine. + + KEY FEATURES: + - Durable agent conversations that survive process restarts + - Automatic activity creation for LLM calls (visible in Temporal UI) + - Long-running workflows that can handle multiple user interactions + - Full observability and monitoring through Temporal dashboard + """ + def __init__(self): + super().__init__(display_name=environment_variables.AGENT_NAME) + self._complete_task = False + + @workflow.signal(name=SignalName.RECEIVE_EVENT) + async def on_task_event_send(self, params: SendEventParams) -> None: + """ + Handle incoming user messages and respond using OpenAI Agents SDK + + This signal handler demonstrates the basic integration pattern: + 1. Receive user message through Temporal signal + 2. Echo message back to UI for visibility + 3. Create and run OpenAI agent (automatically becomes a Temporal activity) + 4. Return agent's response to user + + TEMPORAL INTEGRATION MAGIC: + - When Runner.run() executes, it automatically creates a "invoke_model_activity" + - This activity is visible in Temporal UI with full observability + - If the LLM call fails, Temporal automatically retries it + - The entire conversation is durable and survives process restarts + """ + logger.info(f"Received task message instruction: {params}") + + # ============================================================================ + # STEP 1: Echo User Message + # ============================================================================ + # Echo back the client's message to show it in the UI. This is not done by default + # so the agent developer has full control over what is shown to the user. + await adk.messages.create(task_id=params.task.id, content=params.event.content) + + # ============================================================================ + # STEP 2: Create OpenAI Agent + # ============================================================================ + # Create a simple agent using OpenAI Agents SDK. This agent will respond in haikus + # to demonstrate the basic functionality. No tools needed for this hello world example. + # + # IMPORTANT: The OpenAI Agents SDK plugin (configured in acp.py and run_worker.py) + # automatically converts agent interactions into Temporal activities for durability. + + + agent = Agent( + name="Haiku Assistant", + instructions="You are a friendly assistant who always responds in the form of a haiku. " + "Each response should be exactly 3 lines following the 5-7-5 syllable pattern.", + ) + + # ============================================================================ + # STEP 3: Run Agent with Temporal Durability + # ============================================================================ + # This is where the magic happens! When Runner.run() executes: + # 1. The OpenAI Agents SDK makes LLM calls to generate responses + # 2. The plugin automatically wraps these calls as Temporal activities + # 3. You'll see "invoke_model_activity" appear in the Temporal UI + # 4. If the LLM call fails, Temporal retries it automatically + # 5. The conversation state is preserved even if the worker restarts + + # IMPORTANT NOTE ABOUT AGENT RUN CALLS: + # ===================================== + # Notice that we don't need to wrap the Runner.run() call in an activity! + # This might feel weird for anyone who has used Temporal before, as typically + # non-deterministic operations like LLM calls would need to be wrapped in activities. + # However, the OpenAI Agents SDK plugin is handling all of this automatically + # behind the scenes. + # + # Another benefit of this approach is that we don't have to serialize the arguments, + # which would typically be the case with Temporal activities - the plugin handles + # all of this for us, making the developer experience much smoother. + + # Pass the text content directly to Runner.run (it accepts strings) + result = await Runner.run(agent, params.event.content.content) + + # ============================================================================ + # STEP 4: Send Response Back to User + # ============================================================================ + # Send the agent's response back to the user interface + # The agent's haiku response will be displayed in the chat + await adk.messages.create( + task_id=params.task.id, + content=TextContent( + author="agent", + content=result.final_output, + ), + ) + + # ============================================================================ + # WHAT YOU'LL SEE IN TEMPORAL UI: + # ============================================================================ + # After running this: + # 1. Go to localhost:8080 (Temporal UI) + # 2. Find your workflow execution + # 3. You'll see an "invoke_model_activity" that shows: + # - Execution time for the LLM call + # - Input parameters (user message) + # - Output (agent's haiku response) + # - Retry attempts (if any failures occurred) + # + # This gives you full observability into your agent's LLM interactions! + # ============================================================================ + + @workflow.run + async def on_task_create(self, params: CreateTaskParams) -> str: + """ + Temporal Workflow Entry Point - Long-Running Agent Conversation + + This method runs when the workflow starts and keeps the agent conversation alive. + It demonstrates Temporal's ability to run workflows for extended periods (minutes, + hours, days, or even years) while maintaining full durability. + + TEMPORAL WORKFLOW LIFECYCLE: + 1. Workflow starts when a task is created + 2. Sends initial acknowledgment message to user + 3. Waits indefinitely for user messages (handled by on_task_event_send signal) + 4. Each user message triggers the signal handler which runs the OpenAI agent + 5. Workflow continues running until explicitly completed or canceled + + DURABILITY BENEFITS: + - Workflow survives worker restarts, deployments, infrastructure failures + - All agent conversation history is preserved in Temporal's event store + - Can resume from exact point of failure without losing context + - Scales to handle millions of concurrent agent conversations + """ + logger.info(f"Received task create params: {params}") + + # ============================================================================ + # WORKFLOW INITIALIZATION: Send Welcome Message + # ============================================================================ + # Acknowledge that the task has been created and the agent is ready. + # This message appears once when the conversation starts. + await adk.messages.create( + task_id=params.task.id, + content=TextContent( + author="agent", + content=f"🌸 Hello! I'm your Haiku Assistant, powered by OpenAI Agents SDK + Temporal! 🌸\n\n" + f"I'll respond to all your messages in beautiful haiku form. " + f"This conversation is now durable - even if I restart, our chat continues!\n\n" + f"Task created with params:\n{json.dumps(params.params, indent=2)}\n\n" + f"Send me a message and I'll respond with a haiku! 🎋", + ), + ) + + # ============================================================================ + # WORKFLOW PERSISTENCE: Wait for Completion Signal + # ============================================================================ + # This is the key to Temporal's power: the workflow runs indefinitely, + # handling user messages through signals (on_task_event_send) until + # explicitly told to complete. + # + # IMPORTANT: This wait_condition keeps the workflow alive and durable: + # - No timeout = workflow can run forever (perfect for ongoing conversations) + # - Temporal can handle millions of such concurrent workflows + # - If worker crashes, workflow resumes exactly where it left off + # - All conversation state is preserved in Temporal's event log + await workflow.wait_condition( + lambda: self._complete_task, + timeout=None, # No timeout = truly long-running agent conversation + ) + return "Agent conversation completed" + + @workflow.signal + async def complete_task_signal(self) -> None: + """ + Signal to gracefully complete the agent conversation workflow + + This signal can be sent to end the workflow cleanly. In a real application, + you might trigger this when a user ends the conversation or after a period + of inactivity. + """ + logger.info("Received signal to complete the agent conversation") + self._complete_task = True \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/pyproject.toml b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/pyproject.toml new file mode 100644 index 00000000..a5216dd7 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "example_tutorial" +version = "0.1.0" +description = "An AgentEx agent" +requires-python = ">=3.12" +dependencies = [ + "agentex-sdk>=0.4.18", + "scale-gp", + "temporalio>=1.18.0,<2", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "black", + "isort", + "flake8", + "debugpy>=1.8.15", +] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.black] +line-length = 88 +target-version = ['py312'] + +[tool.isort] +profile = "black" +line_length = 88 \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/Dockerfile b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/Dockerfile new file mode 100644 index 00000000..531cf804 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/Dockerfile @@ -0,0 +1,48 @@ +# syntax=docker/dockerfile:1.3 +FROM python:3.12-slim +COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/ + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + htop \ + vim \ + curl \ + tar \ + python3-dev \ + postgresql-client \ + build-essential \ + libpq-dev \ + gcc \ + cmake \ + netcat-openbsd \ + nodejs \ + npm \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/** + +# Install tctl (Temporal CLI) +RUN curl -L https://github.com/temporalio/tctl/releases/download/v1.18.1/tctl_1.18.1_linux_arm64.tar.gz -o /tmp/tctl.tar.gz && \ + tar -xzf /tmp/tctl.tar.gz -C /usr/local/bin && \ + chmod +x /usr/local/bin/tctl && \ + rm /tmp/tctl.tar.gz + +RUN uv pip install --system --upgrade pip setuptools wheel + +ENV UV_HTTP_TIMEOUT=1000 + +# Copy just the pyproject.toml file to optimize caching +COPY example_tutorial/pyproject.toml /app/example_tutorial/pyproject.toml + +WORKDIR /app/example_tutorial + +# Install the required Python packages using uv +RUN uv pip install --system . + +# Copy the project code +COPY example_tutorial/project /app/example_tutorial/project + +# Run the ACP server using uvicorn +CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"] + +# When we deploy the worker, we will replace the CMD with the following +# CMD ["python", "-m", "run_worker"] \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/README.md b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/README.md new file mode 100644 index 00000000..c46722b8 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/README.md @@ -0,0 +1,317 @@ +# example-tutorial - AgentEx Temporal Agent Template + +This is a starter template for building asynchronous agents with the AgentEx framework and Temporal. It provides a basic implementation of the Agent 2 Client Protocol (ACP) with Temporal workflow support to help you get started quickly. + +## What You'll Learn + +- **Tasks**: A task is a grouping mechanism for related messages. Think of it as a conversation thread or a session. +- **Messages**: Messages are communication objects within a task. They can contain text, data, or instructions. +- **ACP Events**: The agent responds to four main events: + - `task_received`: When a new task is created + - `task_message_received`: When a message is sent within a task + - `task_approved`: When a task is approved + - `task_canceled`: When a task is canceled +- **Temporal Workflows**: Long-running processes that can handle complex state management and async operations + +## Running the Agent + +1. Run the agent locally: +```bash +agentex agents run --manifest manifest.yaml +``` + +The agent will start on port 8000 and print messages whenever it receives any of the ACP events. + +## What's Inside + +This template: +- Sets up a basic ACP server with Temporal integration +- Handles each of the required ACP events +- Provides a foundation for building complex async agents +- Includes Temporal workflow and activity definitions + +## Next Steps + +For more advanced agent development, check out the AgentEx tutorials: + +- **Tutorials 00-08**: Learn about building synchronous agents with ACP +- **Tutorials 09-10**: Learn how to use Temporal to power asynchronous agents + - Tutorial 09: Basic Temporal workflow setup + - Tutorial 10: Advanced Temporal patterns and best practices + +These tutorials will help you understand: +- How to handle long-running tasks +- Implementing state machines +- Managing complex workflows +- Best practices for async agent development + +## The Manifest File + +The `manifest.yaml` file is your agent's configuration file. It defines: +- How your agent should be built and packaged +- What files are included in your agent's Docker image +- Your agent's name and description +- Local development settings (like the port your agent runs on) +- Temporal worker configuration + +This file is essential for both local development and deployment of your agent. + +## Project Structure + +``` +example_tutorial/ +├── project/ # Your agent's code +│ ├── __init__.py +│ ├── acp.py # ACP server and event handlers +│ ├── workflow.py # Temporal workflow definitions +│ ├── activities.py # Temporal activity definitions +│ └── run_worker.py # Temporal worker setup +├── Dockerfile # Container definition +├── manifest.yaml # Deployment config +├── dev.ipynb # Development notebook for testing + +└── pyproject.toml # Dependencies (uv) + +``` + +## Development + +### 1. Customize Event Handlers +- Modify the handlers in `acp.py` to implement your agent's logic +- Add your own tools and capabilities +- Implement custom state management + +### 2. Test Your Agent with the Development Notebook +Use the included `dev.ipynb` Jupyter notebook to test your agent interactively: + +```bash +# Start Jupyter notebook (make sure you have jupyter installed) +jupyter notebook dev.ipynb + +# Or use VS Code to open the notebook directly +code dev.ipynb +``` + +The notebook includes: +- **Setup**: Connect to your local AgentEx backend +- **Task creation**: Create a new task for the conversation +- **Event sending**: Send events to the agent and get responses +- **Async message subscription**: Subscribe to server-side events to receive agent responses +- **Rich message display**: Beautiful formatting with timestamps and author information + +The notebook automatically uses your agent name (`example-tutorial`) and demonstrates the agentic ACP workflow: create task → send event → subscribe to responses. + +### 3. Develop Temporal Workflows +- Edit `workflow.py` to define your agent's async workflow logic +- Modify `activities.py` to add custom activities +- Use `run_worker.py` to configure the Temporal worker + +### 4. Manage Dependencies + + +You chose **uv** for package management. Here's how to work with dependencies: + +```bash +# Add new dependencies +agentex uv add requests openai anthropic + +# Add Temporal-specific dependencies (already included) +agentex uv add temporalio + +# Install/sync dependencies +agentex uv sync + +# Run commands with uv +uv run agentex agents run --manifest manifest.yaml +``` + +**Benefits of uv:** +- Faster dependency resolution and installation +- Better dependency isolation +- Modern Python packaging standards + + + +### 5. Configure Credentials +- Add any required credentials to your manifest.yaml +- For local development, create a `.env` file in the project directory +- Use `load_dotenv()` only in development mode: + +```python +import os +from dotenv import load_dotenv + +if os.environ.get("ENVIRONMENT") == "development": + load_dotenv() +``` + +## Local Development + +### 1. Start the Agentex Backend +```bash +# Navigate to the backend directory +cd agentex + +# Start all services using Docker Compose +make dev + +# Optional: In a separate terminal, use lazydocker for a better UI (everything should say "healthy") +lzd +``` + +### 2. Setup Your Agent's requirements/pyproject.toml +```bash +agentex uv sync [--group editable-apy] +source .venv/bin/activate + +# OR +conda create -n example_tutorial python=3.12 +conda activate example_tutorial +pip install -r requirements.txt +``` +### 3. Run Your Agent +```bash +# From this directory +export ENVIRONMENT=development && [uv run] agentex agents run --manifest manifest.yaml +``` +4. **Interact with your agent** + +Option 0: CLI (deprecated - to be replaced once a new CLI is implemented - please use the web UI for now!) +```bash +# Submit a task via CLI +agentex tasks submit --agent example-tutorial --task "Your task here" +``` + +Option 1: Web UI +```bash +# Start the local web interface +cd agentex-web +make dev + +# Then open http://localhost:3000 in your browser to chat with your agent +``` + +## Development Tips + +### Environment Variables +- Set environment variables in project/.env for any required credentials +- Or configure them in the manifest.yaml under the `env` section +- The `.env` file is automatically loaded in development mode + +### Local Testing +- Use `export ENVIRONMENT=development` before running your agent +- This enables local service discovery and debugging features +- Your agent will automatically connect to locally running services + +### Temporal-Specific Tips +- Monitor workflows in the Temporal Web UI at http://localhost:8080 +- Use the Temporal CLI for advanced workflow management +- Check workflow logs for debugging async operations + +### Debugging +- Check agent logs in the terminal where you ran the agent +- Use the web UI to inspect task history and responses +- Monitor backend services with `lzd` (LazyDocker) +- Use Temporal Web UI for workflow debugging + +### To build the agent Docker image locally (normally not necessary): + +1. Build the agent image: +```bash +agentex agents build --manifest manifest.yaml +``` + +## Advanced Features + +### Temporal Workflows +Extend your agent with sophisticated async workflows: + +```python +# In project/workflow.py +@workflow.defn +class MyWorkflow(BaseWorkflow): + async def complex_operation(self): + # Multi-step async operations + # Error handling and retries + # State management + pass +``` + +### Custom Activities +Add custom activities for external operations. **Important**: Always specify appropriate timeouts (recommended: 10 minutes): + +```python +# In project/activities.py +from datetime import timedelta +from temporalio import activity +from temporalio.common import RetryPolicy + +@activity.defn(name="call_external_api") +async def call_external_api(data): + # HTTP requests, database operations, etc. + pass + +# In your workflow, call it with a timeout: +result = await workflow.execute_activity( + "call_external_api", + data, + start_to_close_timeout=timedelta(minutes=10), # Recommended: 10 minute timeout + heartbeat_timeout=timedelta(minutes=1), # Optional: heartbeat monitoring + retry_policy=RetryPolicy(maximum_attempts=3) # Optional: retry policy +) + +# Don't forget to register your custom activities in run_worker.py: +# all_activities = get_all_activities() + [your_custom_activity_function] +``` + +### Integration with External Services + +```bash +# Add service clients +agentex uv add httpx requests-oauthlib + +# Add AI/ML libraries +agentex uv add openai anthropic transformers + +# Add database clients +agentex uv add asyncpg redis +``` + + +## Troubleshooting + +### Common Issues + +1. **Agent not appearing in web UI** + - Check if agent is running on port 8000 + - Verify `ENVIRONMENT=development` is set + - Check agent logs for errors + +2. **Temporal workflow issues** + - Check Temporal Web UI at http://localhost:8080 + - Verify Temporal server is running in backend services + - Check workflow logs for specific errors + +3. **Dependency issues** + + - Run `agentex uv sync` to ensure all dependencies are installed + - Verify temporalio is properly installed + + +4. **Port conflicts** + - Check if another service is using port 8000 + - Use `lsof -i :8000` to find conflicting processes + +### Temporal-Specific Troubleshooting + +1. **Workflow not starting** + - Check if Temporal server is running (`docker ps`) + - Verify task queue configuration in `run_worker.py` + - Check workflow registration in the worker + +2. **Activity failures** + - Check activity logs in the console + - Verify activity registration + - Check for timeout issues + +Happy building with Temporal! 🚀⚡ \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/dev.ipynb b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/dev.ipynb new file mode 100644 index 00000000..04aa5cb9 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/dev.ipynb @@ -0,0 +1,126 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "36834357", + "metadata": {}, + "outputs": [], + "source": [ + "from agentex import Agentex\n", + "\n", + "client = Agentex(base_url=\"http://localhost:5003\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1c309d6", + "metadata": {}, + "outputs": [], + "source": [ + "AGENT_NAME = \"example-tutorial\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f6e6ef0", + "metadata": {}, + "outputs": [], + "source": [ + "# (REQUIRED) Create a new task. For Agentic agents, you must create a task for messages to be associated with.\n", + "import uuid\n", + "\n", + "rpc_response = client.agents.create_task(\n", + " agent_name=AGENT_NAME,\n", + " params={\n", + " \"name\": f\"{str(uuid.uuid4())[:8]}-task\",\n", + " \"params\": {}\n", + " }\n", + ")\n", + "\n", + "task = rpc_response.result\n", + "print(task)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b03b0d37", + "metadata": {}, + "outputs": [], + "source": [ + "# Send an event to the agent\n", + "\n", + "# The response is expected to be a list of TaskMessage objects, which is a union of the following types:\n", + "# - TextContent: A message with just text content \n", + "# - DataContent: A message with JSON-serializable data content\n", + "# - ToolRequestContent: A message with a tool request, which contains a JSON-serializable request to call a tool\n", + "# - ToolResponseContent: A message with a tool response, which contains response object from a tool call in its content\n", + "\n", + "# When processing the message/send response, if you are expecting more than TextContent, such as DataContent, ToolRequestContent, or ToolResponseContent, you can process them as well\n", + "\n", + "rpc_response = client.agents.send_event(\n", + " agent_name=AGENT_NAME,\n", + " params={\n", + " \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n", + " \"task_id\": task.id,\n", + " }\n", + ")\n", + "\n", + "event = rpc_response.result\n", + "print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6927cc0", + "metadata": {}, + "outputs": [], + "source": [ + "# Subscribe to the async task messages produced by the agent\n", + "from agentex.lib.utils.dev_tools import subscribe_to_async_task_messages\n", + "\n", + "task_messages = subscribe_to_async_task_messages(\n", + " client=client,\n", + " task=task, \n", + " only_after_timestamp=event.created_at, \n", + " print_messages=True,\n", + " rich_print=True,\n", + " timeout=5,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4864e354", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/environments.yaml b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/environments.yaml new file mode 100644 index 00000000..f9051191 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/environments.yaml @@ -0,0 +1,64 @@ +# Agent Environment Configuration +# ------------------------------ +# This file defines environment-specific settings for your agent. +# This DIFFERS from the manifest.yaml file in that it is used to program things that are ONLY per environment. + +# ********** EXAMPLE ********** +# schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI +# environments: +# dev: +# auth: +# principal: +# user_id: "1234567890" +# user_name: "John Doe" +# user_email: "john.doe@example.com" +# user_role: "admin" +# user_permissions: "read, write, delete" +# helm_overrides: # This is used to override the global helm values.yaml file in the agentex-agent helm charts +# replicas: 3 +# resources: +# requests: +# cpu: "1000m" +# memory: "2Gi" +# limits: +# cpu: "2000m" +# memory: "4Gi" +# env: +# - name: LOG_LEVEL +# value: "DEBUG" +# - name: ENVIRONMENT +# value: "staging" +# +# kubernetes: +# # OPTIONAL - Otherwise it will be derived from separately. However, this can be used to override the derived +# # namespace and deploy it with in the same namespace that already exists for a separate agent. +# namespace: "team-example-tutorial" +# ********** END EXAMPLE ********** + +schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI +environments: + dev: + auth: + principal: + user_id: # TODO: Fill in + account_id: # TODO: Fill in + helm_overrides: + # This is used to override the global helm values.yaml file in the agentex-agent helm charts + replicaCount: 2 + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" + temporal-worker: + enabled: true + replicaCount: 2 + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/manifest.yaml b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/manifest.yaml new file mode 100644 index 00000000..a165dca9 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/manifest.yaml @@ -0,0 +1,140 @@ +# Agent Manifest Configuration +# --------------------------- +# This file defines how your agent should be built and deployed. + +# Build Configuration +# ------------------ +# The build config defines what gets packaged into your agent's Docker image. +# This same configuration is used whether building locally or remotely. +# +# When building: +# 1. All files from include_paths are collected into a build context +# 2. The context is filtered by dockerignore rules +# 3. The Dockerfile uses this context to build your agent's image +# 4. The image is pushed to a registry and used to run your agent +build: + context: + # Root directory for the build context + root: ../ # Keep this as the default root + + # Paths to include in the Docker build context + # Must include: + # - Your agent's directory (your custom agent code) + # These paths are collected and sent to the Docker daemon for building + include_paths: + - example_tutorial + + # Path to your agent's Dockerfile + # This defines how your agent's image is built from the context + # Relative to the root directory + dockerfile: example_tutorial/Dockerfile + + # Path to your agent's .dockerignore + # Filters unnecessary files from the build context + # Helps keep build context small and builds fast + dockerignore: example_tutorial/.dockerignore + + +# Local Development Configuration +# ----------------------------- +# Only used when running the agent locally +local_development: + agent: + port: 8000 # Port where your local ACP server is running + host_address: host.docker.internal # Host address for Docker networking (host.docker.internal for Docker, localhost for direct) + + # File paths for local development (relative to this manifest.yaml) + paths: + # Path to ACP server file + # Examples: + # project/acp.py (standard) + # src/server.py (custom structure) + # ../shared/acp.py (shared across projects) + # /absolute/path/acp.py (absolute path) + acp: project/acp.py + + # Path to temporal worker file + # Examples: + # project/run_worker.py (standard) + # workers/temporal.py (custom structure) + # ../shared/worker.py (shared across projects) + worker: project/run_worker.py + + +# Agent Configuration +# ----------------- +agent: + # Type of agent - either sync or agentic + acp_type: agentic + + # Unique name for your agent + # Used for task routing and monitoring + name: example-tutorial + + # Description of what your agent does + # Helps with documentation and discovery + description: An AgentEx agent + + # Temporal workflow configuration + # This enables your agent to run as a Temporal workflow for long-running tasks + temporal: + enabled: true + workflows: + # Name of the workflow class + # Must match the @workflow.defn name in your workflow.py + - name: example-tutorial + + # Queue name for task distribution + # Used by Temporal to route tasks to your agent + # Convention: _task_queue + queue_name: example_tutorial_queue + + # Optional: Credentials mapping + # Maps Kubernetes secrets to environment variables + # Common credentials include: + credentials: + - env_var_name: REDIS_URL + secret_name: redis-url-secret + secret_key: url + # - env_var_name: OPENAI_API_KEY + # secret_name: openai-api-key + # secret_key: api-key + + # Optional: Set Environment variables for running your agent locally as well + # as for deployment later on + env: + OPENAI_API_KEY: "" + # OPENAI_BASE_URL: "" + OPENAI_ORG_ID: "" + + +# Deployment Configuration +# ----------------------- +# Configuration for deploying your agent to Kubernetes clusters +deployment: + # Container image configuration + image: + repository: "" # Update with your container registry + tag: "latest" # Default tag, should be versioned in production + + imagePullSecrets: + - name: my-registry-secret # Update with your image pull secret name + + # Global deployment settings that apply to all clusters + # These can be overridden using --override-file with custom configuration files + global: + agent: + name: "example-tutorial" + description: "An AgentEx agent" + + # Default replica count + replicaCount: 1 + + # Default resource requirements + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/__init__.py b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/acp.py b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/acp.py new file mode 100644 index 00000000..6f6d625a --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/acp.py @@ -0,0 +1,69 @@ +import os +import sys +from datetime import timedelta + +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters + +# === DEBUG SETUP (AgentEx CLI Debug Support) === +if os.getenv("AGENTEX_DEBUG_ENABLED") == "true": + try: + import debugpy + debug_port = int(os.getenv("AGENTEX_DEBUG_PORT", "5679")) + debug_type = os.getenv("AGENTEX_DEBUG_TYPE", "acp") + wait_for_attach = os.getenv("AGENTEX_DEBUG_WAIT_FOR_ATTACH", "false").lower() == "true" + + # Configure debugpy + debugpy.configure(subProcess=False) + debugpy.listen(debug_port) + + print(f"🐛 [{debug_type.upper()}] Debug server listening on port {debug_port}") + + if wait_for_attach: + print(f"⏳ [{debug_type.upper()}] Waiting for debugger to attach...") + debugpy.wait_for_client() + print(f"✅ [{debug_type.upper()}] Debugger attached!") + else: + print(f"📡 [{debug_type.upper()}] Ready for debugger attachment") + + except ImportError: + print("❌ debugpy not available. Install with: pip install debugpy") + sys.exit(1) + except Exception as e: + print(f"❌ Debug setup failed: {e}") + sys.exit(1) +# === END DEBUG SETUP === + +from agentex.lib.types.fastacp import TemporalACPConfig +from agentex.lib.sdk.fastacp.fastacp import FastACP + +# Create the ACP server +acp = FastACP.create( + acp_type="agentic", + config=TemporalACPConfig( + # When deployed to the cluster, the Temporal address will automatically be set to the cluster address + # For local development, we set the address manually to talk to the local Temporal service set up via docker compose + # We are also adding the Open AI Agents SDK plugin to the ACP. + type="temporal", + temporal_address=os.getenv("TEMPORAL_ADDRESS", "localhost:7233"), + plugins=[OpenAIAgentsPlugin( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(days=1) + ) + )] + ) +) + + +# Notice that we don't need to register any handlers when we use type="temporal" +# If you look at the code in agentex.sdk.fastacp.impl.temporal_acp +# You can see that these handlers are automatically registered when the ACP is created + +# @acp.on_task_create +# This will be handled by the method in your workflow that is decorated with @workflow.run + +# @acp.on_task_event_send +# This will be handled by the method in your workflow that is decorated with @workflow.signal(name=SignalName.RECEIVE_MESSAGE) + +# @acp.on_task_cancel +# This does not need to be handled by your workflow. +# It is automatically handled by the temporal client which cancels the workflow directly \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/activities.py b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/activities.py new file mode 100644 index 00000000..35ab678d --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/activities.py @@ -0,0 +1,104 @@ +import random +import asyncio + +from temporalio import activity + +from agentex.lib.utils.logging import make_logger + +logger = make_logger(__name__) +# ============================================================================ +# Temporal Activities for OpenAI Agents SDK Integration +# ============================================================================ +# This file defines Temporal activities that can be used in two different patterns: +# +# PATTERN 1: Direct conversion to agent tools using activity_as_tool() +# PATTERN 2: Called internally by function_tools for multi-step operations +# +# Activities represent NON-DETERMINISTIC operations that need durability: +# - API calls, database queries, file I/O, network operations +# - Any operation that could fail and needs automatic retries +# - Operations with variable latency or external dependencies + +# ============================================================================ +# PATTERN 1 EXAMPLE: Simple External Tool as Activity +# ============================================================================ +# This activity demonstrates PATTERN 1 usage: +# - Single non-deterministic operation (simulated API call) +# - Converted directly to an agent tool using activity_as_tool() +# - Each tool call creates exactly ONE activity in the workflow + +@activity.defn +async def get_weather(city: str) -> str: + """Get the weather for a given city. + + PATTERN 1 USAGE: This activity gets converted to an agent tool using: + activity_as_tool(get_weather, start_to_close_timeout=timedelta(seconds=10)) + + When the agent calls the weather tool: + 1. This activity runs with Temporal durability guarantees + 2. If it fails, Temporal automatically retries it + 3. The result is returned directly to the agent + """ + # Simulate API call to weather service + if city == "New York City": + return "The weather in New York City is 22 degrees Celsius" + else: + return "The weather is unknown" + +# ============================================================================ +# PATTERN 2 EXAMPLES: Activities Used Within Function Tools +# ============================================================================ +# These activities demonstrate PATTERN 2 usage: +# - Called internally by the move_money function tool (see tools.py) +# - Multiple activities coordinated by a single tool +# - Guarantees execution sequence and atomicity + +@activity.defn +async def withdraw_money(from_account: str, amount: float) -> str: + """Withdraw money from an account. + + PATTERN 2 USAGE: This activity is called internally by the move_money tool. + It's NOT converted to an agent tool directly - instead, it's orchestrated + by code inside the function_tool to guarantee proper sequencing. + """ + # Simulate variable API call latency (realistic for banking operations) + random_delay = random.randint(1, 5) + await asyncio.sleep(random_delay) + + # In a real implementation, this would make an API call to a banking service + logger.info(f"Withdrew ${amount} from {from_account}") + return f"Successfully withdrew ${amount} from {from_account}" + +@activity.defn +async def deposit_money(to_account: str, amount: float) -> str: + """Deposit money into an account. + + PATTERN 2 USAGE: This activity is called internally by the move_money tool + AFTER the withdraw_money activity succeeds. This guarantees the proper + sequence: withdraw → deposit, making the operation atomic. + """ + # Simulate banking API latency + await asyncio.sleep(2) + + # In a real implementation, this would make an API call to a banking service + logger.info(f"Successfully deposited ${amount} into {to_account}") + return f"Successfully deposited ${amount} into {to_account}" + +# ============================================================================ +# KEY INSIGHTS: +# ============================================================================ +# +# 1. ACTIVITY DURABILITY: All activities are automatically retried by Temporal +# if they fail, providing resilience against network issues, service outages, etc. +# +# 2. PATTERN 1 vs PATTERN 2 CHOICE: +# - Use Pattern 1 for simple, independent operations +# - Use Pattern 2 when you need guaranteed sequencing of multiple operations +# +# 3. OBSERVABILITY: Each activity execution appears in the Temporal UI with: +# - Execution time, retry attempts, input parameters, return values +# - Full traceability from agent tool call to activity execution +# +# 4. PARAMETERS: Notice how Pattern 2 activities now accept proper parameters +# (from_account, to_account, amount) that get passed through from the tool +# ============================================================================ diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/run_worker.py b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/run_worker.py new file mode 100644 index 00000000..49395f15 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/run_worker.py @@ -0,0 +1,47 @@ +import asyncio +from datetime import timedelta + +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters + +from project.workflow import ExampleTutorialWorkflow +from project.activities import get_weather, deposit_money, withdraw_money +from agentex.lib.utils.debug import setup_debug_if_enabled +from agentex.lib.utils.logging import make_logger +from agentex.lib.environment_variables import EnvironmentVariables +from agentex.lib.core.temporal.activities import get_all_activities +from agentex.lib.core.temporal.workers.worker import AgentexWorker + +environment_variables = EnvironmentVariables.refresh() + +logger = make_logger(__name__) + + +async def main(): + # Setup debug mode if enabled + setup_debug_if_enabled() + + task_queue_name = environment_variables.WORKFLOW_TASK_QUEUE + if task_queue_name is None: + raise ValueError("WORKFLOW_TASK_QUEUE is not set") + + # Add activities to the worker + all_activities = get_all_activities() + [withdraw_money, deposit_money, get_weather] # add your own activities here + + # Create a worker with automatic tracing + # We are also adding the Open AI Agents SDK plugin to the worker. + worker = AgentexWorker( + task_queue=task_queue_name, + plugins=[OpenAIAgentsPlugin( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(days=1) + ) + )], + ) + + await worker.run( + activities=all_activities, + workflow=ExampleTutorialWorkflow, + ) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/tools.py b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/tools.py new file mode 100644 index 00000000..b96afabc --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/tools.py @@ -0,0 +1,54 @@ +from datetime import timedelta + +from agents import function_tool +from temporalio import workflow + +from project.activities import deposit_money, withdraw_money + +# ============================================================================ +# PATTERN 2 EXAMPLE: Multiple Activities Within Tools +# ============================================================================ +# This demonstrates how to create a single tool that orchestrates multiple +# Temporal activities internally. This pattern is ideal when you need to: +# 1. Guarantee the sequence of operations (withdraw THEN deposit) +# 2. Make the entire operation atomic from the agent's perspective +# 3. Avoid relying on the LLM to correctly sequence multiple tool calls + +@function_tool +async def move_money(from_account: str, to_account: str, amount: float) -> str: + """Move money from one account to another atomically. + + This tool demonstrates PATTERN 2: Instead of having the LLM make two separate + tool calls (withdraw + deposit), we create ONE tool that internally coordinates + multiple activities. This guarantees: + - withdraw_money activity runs first + - deposit_money activity only runs if withdrawal succeeds + - Both operations are durable and will retry on failure + - The entire operation appears atomic to the agent + """ + + # STEP 1: Start the withdrawal activity + # This creates a Temporal activity that will be retried if it fails + withdraw_handle = workflow.start_activity_method( + withdraw_money, + start_to_close_timeout=timedelta(days=1) # Long timeout for banking operations + ) + + # Wait for withdrawal to complete before proceeding + # If this fails, the entire tool call fails and can be retried + await withdraw_handle.result() + + # STEP 2: Only after successful withdrawal, start the deposit activity + # This guarantees the sequence: withdraw THEN deposit + deposit_handle = workflow.start_activity_method( + deposit_money, + start_to_close_timeout=timedelta(days=1) + ) + + # Wait for deposit to complete + await deposit_handle.result() + + # PATTERN 2 BENEFIT: From the agent's perspective, this was ONE tool call + # But in Temporal UI, you'll see TWO activities executed in sequence + # Each activity gets its own retry logic and durability guarantees + return f"Successfully moved ${amount} from {from_account} to {to_account}" diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/workflow.py b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/workflow.py new file mode 100644 index 00000000..49d7b9ea --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/project/workflow.py @@ -0,0 +1,244 @@ +""" +OpenAI Agents SDK + Temporal Integration Tutorial + +This tutorial demonstrates two key patterns for integrating OpenAI Agents SDK with Temporal workflows: + +PATTERN 1: Simple External Tools as Activities (activity_as_tool) +- Convert individual Temporal activities directly into agent tools +- 1:1 mapping between tool calls and activities +- Best for: single non-deterministic operations (API calls, DB queries) +- Example: get_weather activity → weather tool + +PATTERN 2: Multiple Activities Within Tools (function_tool with internal activities) +- Create function tools that coordinate multiple activities internally +- 1:many mapping between tool calls and activities +- Best for: complex multi-step operations that need guaranteed sequencing +- Example: move_money tool → withdraw_money + deposit_money activities + +Both patterns provide durability, automatic retries, and full observability through Temporal. + +WHY THIS APPROACH IS GAME-CHANGING: +=================================== +There's a crucial meta-point that should be coming through here: **why is this different?** +This approach is truly transactional because of how the `await` works in Temporal workflows. +Consider a "move money" example - if the operation fails between the withdraw and deposit, +Temporal will resume exactly where it left off - the agent gets real-world flexibility even +if systems die. + +**Why even use Temporal? Why are we adding complexity?** The gain is enormous when you +consider what happens without it: + +In a traditional approach without Temporal, if you withdraw money but then the system crashes +before depositing, you're stuck in a broken state. The money has been withdrawn, but never +deposited. In a banking scenario, you can't just "withdraw again" - the money is already gone +from the source account, and your agent has no way to recover or know what state it was in. + +This is why you can't build very complicated agents without this confidence in transactional +behavior. Temporal gives us: + +- **Guaranteed execution**: If the workflow starts, it will complete, even through failures +- **Exact resumption**: Pick up exactly where we left off, not start over +- **Transactional integrity**: Either both operations complete, or the workflow can be designed + to handle partial completion +- **Production reliability**: Build agents that can handle real-world complexity and failures + +Without this foundation, agents remain fragile toys. With Temporal, they become production-ready +systems that can handle the complexities of the real world. +""" + +import json +import asyncio +from datetime import timedelta + +from agents import Agent, Runner, activity_as_tool +from temporalio import workflow + +from agentex.lib import adk +from project.activities import get_weather +from agentex.lib.types.acp import SendEventParams, CreateTaskParams +from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent +from agentex.lib.environment_variables import EnvironmentVariables +from agentex.lib.core.temporal.types.workflow import SignalName +from agentex.lib.core.temporal.workflows.workflow import BaseWorkflow + +environment_variables = EnvironmentVariables.refresh() + +if environment_variables.WORKFLOW_NAME is None: + raise ValueError("Environment variable WORKFLOW_NAME is not set") + +if environment_variables.AGENT_NAME is None: + raise ValueError("Environment variable AGENT_NAME is not set") + +logger = make_logger(__name__) + +@workflow.defn(name=environment_variables.WORKFLOW_NAME) +class ExampleTutorialWorkflow(BaseWorkflow): + """ + Minimal async workflow template for AgentEx Temporal agents. + """ + def __init__(self): + super().__init__(display_name=environment_variables.AGENT_NAME) + self._complete_task = False + self._pending_confirmation: asyncio.Queue[str] = asyncio.Queue() + + @workflow.signal(name=SignalName.RECEIVE_EVENT) + async def on_task_event_send(self, params: SendEventParams) -> None: + logger.info(f"Received task message instruction: {params}") + + # Echo back the client's message to show it in the UI. This is not done by default + # so the agent developer has full control over what is shown to the user. + await adk.messages.create(task_id=params.task.id, content=params.event.content) + + # ============================================================================ + # OpenAI Agents SDK + Temporal Integration: Two Patterns for Tool Creation + # ============================================================================ + + # #### When to Use Activities for Tools + # + # You'll want to use the activity pattern for tools in the following scenarios: + # + # - **API calls within the tool**: Whenever your tool makes an API call (external + # service, database, etc.), you must wrap it as an activity since these are + # non-deterministic operations that could fail or return different results + # - **Idempotent single operations**: When the tool performs an already idempotent + # single call that you want to ensure gets executed reliably with Temporal's retry + # guarantees + # + # Let's start with the case where it is non-deterministic. If this is the case, we + # want this tool to be an activity to guarantee that it will be executed. The way to + # do this is to add some syntax to make the tool call an activity. Let's create a tool + # that gives us the weather and create a weather agent. For this example, we will just + # return a hard-coded string but we can easily imagine this being an API call to a + # weather service which would make it non-deterministic. First we will create a new + # file called `activities.py`. Here we will create a function to get the weather and + # simply add an activity annotation on top. + + # There are TWO key patterns for integrating tools with the OpenAI Agents SDK in Temporal: + # + # PATTERN 1: Simple External Tools as Activities + # PATTERN 2: Multiple Activities Within Tools + # + # Choose the right pattern based on your use case: + + # ============================================================================ + # PATTERN 1: Simple External Tools as Activities + # ============================================================================ + # Use this pattern when: + # - You have a single non-deterministic operation (API call, DB query, etc.) + # - You want each tool call to be a single Temporal activity + # - You want simple 1:1 mapping between tool calls and activities + # + # HOW IT WORKS: + # 1. Define your function as a Temporal activity with @activity.defn (see activities.py) + # 2. Convert the activity to a tool using activity_as_tool() + # 3. Each time the agent calls this tool, it creates ONE activity in the workflow + # + # BENEFITS: + # - Automatic retries and durability for each tool call + # - Clear observability - each tool call shows as an activity in Temporal UI + # - Temporal handles all the failure recovery automatically + + weather_agent = Agent( + name="Weather Assistant", + instructions="You are a helpful weather agent. Use the get_weather tool to get the weather for a given city.", + tools=[ + # activity_as_tool() converts a Temporal activity into an agent tool + # The get_weather activity will be executed with durability guarantees + activity_as_tool( + get_weather, # This is defined in activities.py as @activity.defn + start_to_close_timeout=timedelta(seconds=10) + ), + ], + ) + + # Run the agent - when it calls the weather tool, it will create a get_weather activity + result = await Runner.run(weather_agent, params.event.content.content) + + # ============================================================================ + # PATTERN 2: Multiple Activities Within Tools + # ============================================================================ + # Use this pattern when: + # - You need multiple sequential non-deterministic operations within one tool + # - You want to guarantee the sequence of operations (not rely on LLM sequencing) + # - You need atomic operations that involve multiple steps + # + # HOW IT WORKS: + # 1. Create individual activities for each non-deterministic step (see activities.py) + # 2. Create a function tool using @function_tool that calls multiple activities internally + # 3. Each activity call uses workflow.start_activity_method() for durability + # 4. The tool coordinates the sequence deterministically (not the LLM) + # + # BENEFITS: + # - Guaranteed execution order (withdraw THEN deposit) + # - Each step is durable and retryable individually + # - Atomic operations from the agent's perspective + # - Better than having LLM make multiple separate tool calls + + # UNCOMMENT THIS SECTION TO SEE PATTERN 2 IN ACTION: + # money_mover_agent = Agent( + # name="Money Mover", + # instructions="You are a helpful money mover agent. Use the move_money tool to move money from one account to another.", + # tools=[ + # # move_money is defined in tools.py as @function_tool + # # Internally, it calls withdraw_money activity THEN deposit_money activity + # # This guarantees the sequence and makes both operations durable + # move_money, + # ], + # ) + + # # Run the agent - when it calls move_money tool, it will create TWO activities: + # # 1. withdraw_money activity + # # 2. deposit_money activity (only after withdraw succeeds) + # result = await Runner.run(money_mover_agent, params.event.content.content) + + # ============================================================================ + # PATTERN COMPARISON SUMMARY: + # ============================================================================ + # + # Pattern 1 (activity_as_tool): | Pattern 2 (function_tool with activities): + # - Single activity per tool call | - Multiple activities per tool call + # - 1:1 tool to activity mapping | - 1:many tool to activity mapping + # - Simple non-deterministic ops | - Complex multi-step operations + # - Let LLM sequence multiple tools | - Code controls activity sequencing + # - Example: get_weather, db_lookup | - Example: money_transfer, multi_step_workflow + # + # BOTH patterns provide: + # - Automatic retries and failure recovery + # - Full observability in Temporal UI + # - Durable execution guarantees + # - Seamless integration with OpenAI Agents SDK + # ============================================================================ + + # Send the agent's response back to the user + await adk.messages.create( + task_id=params.task.id, + content=TextContent( + author="agent", + content=result.final_output, + ), + ) + + @workflow.run + async def on_task_create(self, params: CreateTaskParams) -> str: + logger.info(f"Received task create params: {params}") + + # 1. Acknowledge that the task has been created. + await adk.messages.create( + task_id=params.task.id, + content=TextContent( + author="agent", + content=f"Hello! I've received your task. Normally you can do some state initialization here, or just pass and do nothing until you get your first event. For now I'm just acknowledging that I've received a task with the following params:\n\n{json.dumps(params.params, indent=2)}.\n\nYou should only see this message once, when the task is created. All subsequent events will be handled by the `on_task_event_send` handler.", + ), + ) + + await workflow.wait_condition( + lambda: self._complete_task, + timeout=None, # Set a timeout if you want to prevent the task from running indefinitely. Generally this is not needed. Temporal can run hundreds of millions of workflows in parallel and more. Only do this if you have a specific reason to do so. + ) + return "Task completed" + + @workflow.signal + async def fulfill_order_signal(self, success: bool) -> None: + if success == True: + await self._pending_confirmation.put(True) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/pyproject.toml b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/pyproject.toml new file mode 100644 index 00000000..b95b19bf --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "example_tutorial" +version = "0.1.0" +description = "An AgentEx agent" +requires-python = ">=3.12" +dependencies = [ + "agentex-sdk", + "scale-gp", + "temporalio>=1.18.0,<2", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "black", + "isort", + "flake8", + "debugpy>=1.8.15", +] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.black] +line-length = 88 +target-version = ['py312'] + +[tool.isort] +profile = "black" +line_length = 88 \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/Dockerfile b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/Dockerfile new file mode 100644 index 00000000..531cf804 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/Dockerfile @@ -0,0 +1,48 @@ +# syntax=docker/dockerfile:1.3 +FROM python:3.12-slim +COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/ + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + htop \ + vim \ + curl \ + tar \ + python3-dev \ + postgresql-client \ + build-essential \ + libpq-dev \ + gcc \ + cmake \ + netcat-openbsd \ + nodejs \ + npm \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/** + +# Install tctl (Temporal CLI) +RUN curl -L https://github.com/temporalio/tctl/releases/download/v1.18.1/tctl_1.18.1_linux_arm64.tar.gz -o /tmp/tctl.tar.gz && \ + tar -xzf /tmp/tctl.tar.gz -C /usr/local/bin && \ + chmod +x /usr/local/bin/tctl && \ + rm /tmp/tctl.tar.gz + +RUN uv pip install --system --upgrade pip setuptools wheel + +ENV UV_HTTP_TIMEOUT=1000 + +# Copy just the pyproject.toml file to optimize caching +COPY example_tutorial/pyproject.toml /app/example_tutorial/pyproject.toml + +WORKDIR /app/example_tutorial + +# Install the required Python packages using uv +RUN uv pip install --system . + +# Copy the project code +COPY example_tutorial/project /app/example_tutorial/project + +# Run the ACP server using uvicorn +CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"] + +# When we deploy the worker, we will replace the CMD with the following +# CMD ["python", "-m", "run_worker"] \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/README.md b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/README.md new file mode 100644 index 00000000..c46722b8 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/README.md @@ -0,0 +1,317 @@ +# example-tutorial - AgentEx Temporal Agent Template + +This is a starter template for building asynchronous agents with the AgentEx framework and Temporal. It provides a basic implementation of the Agent 2 Client Protocol (ACP) with Temporal workflow support to help you get started quickly. + +## What You'll Learn + +- **Tasks**: A task is a grouping mechanism for related messages. Think of it as a conversation thread or a session. +- **Messages**: Messages are communication objects within a task. They can contain text, data, or instructions. +- **ACP Events**: The agent responds to four main events: + - `task_received`: When a new task is created + - `task_message_received`: When a message is sent within a task + - `task_approved`: When a task is approved + - `task_canceled`: When a task is canceled +- **Temporal Workflows**: Long-running processes that can handle complex state management and async operations + +## Running the Agent + +1. Run the agent locally: +```bash +agentex agents run --manifest manifest.yaml +``` + +The agent will start on port 8000 and print messages whenever it receives any of the ACP events. + +## What's Inside + +This template: +- Sets up a basic ACP server with Temporal integration +- Handles each of the required ACP events +- Provides a foundation for building complex async agents +- Includes Temporal workflow and activity definitions + +## Next Steps + +For more advanced agent development, check out the AgentEx tutorials: + +- **Tutorials 00-08**: Learn about building synchronous agents with ACP +- **Tutorials 09-10**: Learn how to use Temporal to power asynchronous agents + - Tutorial 09: Basic Temporal workflow setup + - Tutorial 10: Advanced Temporal patterns and best practices + +These tutorials will help you understand: +- How to handle long-running tasks +- Implementing state machines +- Managing complex workflows +- Best practices for async agent development + +## The Manifest File + +The `manifest.yaml` file is your agent's configuration file. It defines: +- How your agent should be built and packaged +- What files are included in your agent's Docker image +- Your agent's name and description +- Local development settings (like the port your agent runs on) +- Temporal worker configuration + +This file is essential for both local development and deployment of your agent. + +## Project Structure + +``` +example_tutorial/ +├── project/ # Your agent's code +│ ├── __init__.py +│ ├── acp.py # ACP server and event handlers +│ ├── workflow.py # Temporal workflow definitions +│ ├── activities.py # Temporal activity definitions +│ └── run_worker.py # Temporal worker setup +├── Dockerfile # Container definition +├── manifest.yaml # Deployment config +├── dev.ipynb # Development notebook for testing + +└── pyproject.toml # Dependencies (uv) + +``` + +## Development + +### 1. Customize Event Handlers +- Modify the handlers in `acp.py` to implement your agent's logic +- Add your own tools and capabilities +- Implement custom state management + +### 2. Test Your Agent with the Development Notebook +Use the included `dev.ipynb` Jupyter notebook to test your agent interactively: + +```bash +# Start Jupyter notebook (make sure you have jupyter installed) +jupyter notebook dev.ipynb + +# Or use VS Code to open the notebook directly +code dev.ipynb +``` + +The notebook includes: +- **Setup**: Connect to your local AgentEx backend +- **Task creation**: Create a new task for the conversation +- **Event sending**: Send events to the agent and get responses +- **Async message subscription**: Subscribe to server-side events to receive agent responses +- **Rich message display**: Beautiful formatting with timestamps and author information + +The notebook automatically uses your agent name (`example-tutorial`) and demonstrates the agentic ACP workflow: create task → send event → subscribe to responses. + +### 3. Develop Temporal Workflows +- Edit `workflow.py` to define your agent's async workflow logic +- Modify `activities.py` to add custom activities +- Use `run_worker.py` to configure the Temporal worker + +### 4. Manage Dependencies + + +You chose **uv** for package management. Here's how to work with dependencies: + +```bash +# Add new dependencies +agentex uv add requests openai anthropic + +# Add Temporal-specific dependencies (already included) +agentex uv add temporalio + +# Install/sync dependencies +agentex uv sync + +# Run commands with uv +uv run agentex agents run --manifest manifest.yaml +``` + +**Benefits of uv:** +- Faster dependency resolution and installation +- Better dependency isolation +- Modern Python packaging standards + + + +### 5. Configure Credentials +- Add any required credentials to your manifest.yaml +- For local development, create a `.env` file in the project directory +- Use `load_dotenv()` only in development mode: + +```python +import os +from dotenv import load_dotenv + +if os.environ.get("ENVIRONMENT") == "development": + load_dotenv() +``` + +## Local Development + +### 1. Start the Agentex Backend +```bash +# Navigate to the backend directory +cd agentex + +# Start all services using Docker Compose +make dev + +# Optional: In a separate terminal, use lazydocker for a better UI (everything should say "healthy") +lzd +``` + +### 2. Setup Your Agent's requirements/pyproject.toml +```bash +agentex uv sync [--group editable-apy] +source .venv/bin/activate + +# OR +conda create -n example_tutorial python=3.12 +conda activate example_tutorial +pip install -r requirements.txt +``` +### 3. Run Your Agent +```bash +# From this directory +export ENVIRONMENT=development && [uv run] agentex agents run --manifest manifest.yaml +``` +4. **Interact with your agent** + +Option 0: CLI (deprecated - to be replaced once a new CLI is implemented - please use the web UI for now!) +```bash +# Submit a task via CLI +agentex tasks submit --agent example-tutorial --task "Your task here" +``` + +Option 1: Web UI +```bash +# Start the local web interface +cd agentex-web +make dev + +# Then open http://localhost:3000 in your browser to chat with your agent +``` + +## Development Tips + +### Environment Variables +- Set environment variables in project/.env for any required credentials +- Or configure them in the manifest.yaml under the `env` section +- The `.env` file is automatically loaded in development mode + +### Local Testing +- Use `export ENVIRONMENT=development` before running your agent +- This enables local service discovery and debugging features +- Your agent will automatically connect to locally running services + +### Temporal-Specific Tips +- Monitor workflows in the Temporal Web UI at http://localhost:8080 +- Use the Temporal CLI for advanced workflow management +- Check workflow logs for debugging async operations + +### Debugging +- Check agent logs in the terminal where you ran the agent +- Use the web UI to inspect task history and responses +- Monitor backend services with `lzd` (LazyDocker) +- Use Temporal Web UI for workflow debugging + +### To build the agent Docker image locally (normally not necessary): + +1. Build the agent image: +```bash +agentex agents build --manifest manifest.yaml +``` + +## Advanced Features + +### Temporal Workflows +Extend your agent with sophisticated async workflows: + +```python +# In project/workflow.py +@workflow.defn +class MyWorkflow(BaseWorkflow): + async def complex_operation(self): + # Multi-step async operations + # Error handling and retries + # State management + pass +``` + +### Custom Activities +Add custom activities for external operations. **Important**: Always specify appropriate timeouts (recommended: 10 minutes): + +```python +# In project/activities.py +from datetime import timedelta +from temporalio import activity +from temporalio.common import RetryPolicy + +@activity.defn(name="call_external_api") +async def call_external_api(data): + # HTTP requests, database operations, etc. + pass + +# In your workflow, call it with a timeout: +result = await workflow.execute_activity( + "call_external_api", + data, + start_to_close_timeout=timedelta(minutes=10), # Recommended: 10 minute timeout + heartbeat_timeout=timedelta(minutes=1), # Optional: heartbeat monitoring + retry_policy=RetryPolicy(maximum_attempts=3) # Optional: retry policy +) + +# Don't forget to register your custom activities in run_worker.py: +# all_activities = get_all_activities() + [your_custom_activity_function] +``` + +### Integration with External Services + +```bash +# Add service clients +agentex uv add httpx requests-oauthlib + +# Add AI/ML libraries +agentex uv add openai anthropic transformers + +# Add database clients +agentex uv add asyncpg redis +``` + + +## Troubleshooting + +### Common Issues + +1. **Agent not appearing in web UI** + - Check if agent is running on port 8000 + - Verify `ENVIRONMENT=development` is set + - Check agent logs for errors + +2. **Temporal workflow issues** + - Check Temporal Web UI at http://localhost:8080 + - Verify Temporal server is running in backend services + - Check workflow logs for specific errors + +3. **Dependency issues** + + - Run `agentex uv sync` to ensure all dependencies are installed + - Verify temporalio is properly installed + + +4. **Port conflicts** + - Check if another service is using port 8000 + - Use `lsof -i :8000` to find conflicting processes + +### Temporal-Specific Troubleshooting + +1. **Workflow not starting** + - Check if Temporal server is running (`docker ps`) + - Verify task queue configuration in `run_worker.py` + - Check workflow registration in the worker + +2. **Activity failures** + - Check activity logs in the console + - Verify activity registration + - Check for timeout issues + +Happy building with Temporal! 🚀⚡ \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/dev.ipynb b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/dev.ipynb new file mode 100644 index 00000000..04aa5cb9 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/dev.ipynb @@ -0,0 +1,126 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "36834357", + "metadata": {}, + "outputs": [], + "source": [ + "from agentex import Agentex\n", + "\n", + "client = Agentex(base_url=\"http://localhost:5003\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1c309d6", + "metadata": {}, + "outputs": [], + "source": [ + "AGENT_NAME = \"example-tutorial\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f6e6ef0", + "metadata": {}, + "outputs": [], + "source": [ + "# (REQUIRED) Create a new task. For Agentic agents, you must create a task for messages to be associated with.\n", + "import uuid\n", + "\n", + "rpc_response = client.agents.create_task(\n", + " agent_name=AGENT_NAME,\n", + " params={\n", + " \"name\": f\"{str(uuid.uuid4())[:8]}-task\",\n", + " \"params\": {}\n", + " }\n", + ")\n", + "\n", + "task = rpc_response.result\n", + "print(task)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b03b0d37", + "metadata": {}, + "outputs": [], + "source": [ + "# Send an event to the agent\n", + "\n", + "# The response is expected to be a list of TaskMessage objects, which is a union of the following types:\n", + "# - TextContent: A message with just text content \n", + "# - DataContent: A message with JSON-serializable data content\n", + "# - ToolRequestContent: A message with a tool request, which contains a JSON-serializable request to call a tool\n", + "# - ToolResponseContent: A message with a tool response, which contains response object from a tool call in its content\n", + "\n", + "# When processing the message/send response, if you are expecting more than TextContent, such as DataContent, ToolRequestContent, or ToolResponseContent, you can process them as well\n", + "\n", + "rpc_response = client.agents.send_event(\n", + " agent_name=AGENT_NAME,\n", + " params={\n", + " \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n", + " \"task_id\": task.id,\n", + " }\n", + ")\n", + "\n", + "event = rpc_response.result\n", + "print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6927cc0", + "metadata": {}, + "outputs": [], + "source": [ + "# Subscribe to the async task messages produced by the agent\n", + "from agentex.lib.utils.dev_tools import subscribe_to_async_task_messages\n", + "\n", + "task_messages = subscribe_to_async_task_messages(\n", + " client=client,\n", + " task=task, \n", + " only_after_timestamp=event.created_at, \n", + " print_messages=True,\n", + " rich_print=True,\n", + " timeout=5,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4864e354", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/environments.yaml b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/environments.yaml new file mode 100644 index 00000000..f9051191 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/environments.yaml @@ -0,0 +1,64 @@ +# Agent Environment Configuration +# ------------------------------ +# This file defines environment-specific settings for your agent. +# This DIFFERS from the manifest.yaml file in that it is used to program things that are ONLY per environment. + +# ********** EXAMPLE ********** +# schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI +# environments: +# dev: +# auth: +# principal: +# user_id: "1234567890" +# user_name: "John Doe" +# user_email: "john.doe@example.com" +# user_role: "admin" +# user_permissions: "read, write, delete" +# helm_overrides: # This is used to override the global helm values.yaml file in the agentex-agent helm charts +# replicas: 3 +# resources: +# requests: +# cpu: "1000m" +# memory: "2Gi" +# limits: +# cpu: "2000m" +# memory: "4Gi" +# env: +# - name: LOG_LEVEL +# value: "DEBUG" +# - name: ENVIRONMENT +# value: "staging" +# +# kubernetes: +# # OPTIONAL - Otherwise it will be derived from separately. However, this can be used to override the derived +# # namespace and deploy it with in the same namespace that already exists for a separate agent. +# namespace: "team-example-tutorial" +# ********** END EXAMPLE ********** + +schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI +environments: + dev: + auth: + principal: + user_id: # TODO: Fill in + account_id: # TODO: Fill in + helm_overrides: + # This is used to override the global helm values.yaml file in the agentex-agent helm charts + replicaCount: 2 + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" + temporal-worker: + enabled: true + replicaCount: 2 + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/manifest.yaml b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/manifest.yaml new file mode 100644 index 00000000..a165dca9 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/manifest.yaml @@ -0,0 +1,140 @@ +# Agent Manifest Configuration +# --------------------------- +# This file defines how your agent should be built and deployed. + +# Build Configuration +# ------------------ +# The build config defines what gets packaged into your agent's Docker image. +# This same configuration is used whether building locally or remotely. +# +# When building: +# 1. All files from include_paths are collected into a build context +# 2. The context is filtered by dockerignore rules +# 3. The Dockerfile uses this context to build your agent's image +# 4. The image is pushed to a registry and used to run your agent +build: + context: + # Root directory for the build context + root: ../ # Keep this as the default root + + # Paths to include in the Docker build context + # Must include: + # - Your agent's directory (your custom agent code) + # These paths are collected and sent to the Docker daemon for building + include_paths: + - example_tutorial + + # Path to your agent's Dockerfile + # This defines how your agent's image is built from the context + # Relative to the root directory + dockerfile: example_tutorial/Dockerfile + + # Path to your agent's .dockerignore + # Filters unnecessary files from the build context + # Helps keep build context small and builds fast + dockerignore: example_tutorial/.dockerignore + + +# Local Development Configuration +# ----------------------------- +# Only used when running the agent locally +local_development: + agent: + port: 8000 # Port where your local ACP server is running + host_address: host.docker.internal # Host address for Docker networking (host.docker.internal for Docker, localhost for direct) + + # File paths for local development (relative to this manifest.yaml) + paths: + # Path to ACP server file + # Examples: + # project/acp.py (standard) + # src/server.py (custom structure) + # ../shared/acp.py (shared across projects) + # /absolute/path/acp.py (absolute path) + acp: project/acp.py + + # Path to temporal worker file + # Examples: + # project/run_worker.py (standard) + # workers/temporal.py (custom structure) + # ../shared/worker.py (shared across projects) + worker: project/run_worker.py + + +# Agent Configuration +# ----------------- +agent: + # Type of agent - either sync or agentic + acp_type: agentic + + # Unique name for your agent + # Used for task routing and monitoring + name: example-tutorial + + # Description of what your agent does + # Helps with documentation and discovery + description: An AgentEx agent + + # Temporal workflow configuration + # This enables your agent to run as a Temporal workflow for long-running tasks + temporal: + enabled: true + workflows: + # Name of the workflow class + # Must match the @workflow.defn name in your workflow.py + - name: example-tutorial + + # Queue name for task distribution + # Used by Temporal to route tasks to your agent + # Convention: _task_queue + queue_name: example_tutorial_queue + + # Optional: Credentials mapping + # Maps Kubernetes secrets to environment variables + # Common credentials include: + credentials: + - env_var_name: REDIS_URL + secret_name: redis-url-secret + secret_key: url + # - env_var_name: OPENAI_API_KEY + # secret_name: openai-api-key + # secret_key: api-key + + # Optional: Set Environment variables for running your agent locally as well + # as for deployment later on + env: + OPENAI_API_KEY: "" + # OPENAI_BASE_URL: "" + OPENAI_ORG_ID: "" + + +# Deployment Configuration +# ----------------------- +# Configuration for deploying your agent to Kubernetes clusters +deployment: + # Container image configuration + image: + repository: "" # Update with your container registry + tag: "latest" # Default tag, should be versioned in production + + imagePullSecrets: + - name: my-registry-secret # Update with your image pull secret name + + # Global deployment settings that apply to all clusters + # These can be overridden using --override-file with custom configuration files + global: + agent: + name: "example-tutorial" + description: "An AgentEx agent" + + # Default replica count + replicaCount: 1 + + # Default resource requirements + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/__init__.py b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/acp.py b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/acp.py new file mode 100644 index 00000000..6f6d625a --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/acp.py @@ -0,0 +1,69 @@ +import os +import sys +from datetime import timedelta + +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters + +# === DEBUG SETUP (AgentEx CLI Debug Support) === +if os.getenv("AGENTEX_DEBUG_ENABLED") == "true": + try: + import debugpy + debug_port = int(os.getenv("AGENTEX_DEBUG_PORT", "5679")) + debug_type = os.getenv("AGENTEX_DEBUG_TYPE", "acp") + wait_for_attach = os.getenv("AGENTEX_DEBUG_WAIT_FOR_ATTACH", "false").lower() == "true" + + # Configure debugpy + debugpy.configure(subProcess=False) + debugpy.listen(debug_port) + + print(f"🐛 [{debug_type.upper()}] Debug server listening on port {debug_port}") + + if wait_for_attach: + print(f"⏳ [{debug_type.upper()}] Waiting for debugger to attach...") + debugpy.wait_for_client() + print(f"✅ [{debug_type.upper()}] Debugger attached!") + else: + print(f"📡 [{debug_type.upper()}] Ready for debugger attachment") + + except ImportError: + print("❌ debugpy not available. Install with: pip install debugpy") + sys.exit(1) + except Exception as e: + print(f"❌ Debug setup failed: {e}") + sys.exit(1) +# === END DEBUG SETUP === + +from agentex.lib.types.fastacp import TemporalACPConfig +from agentex.lib.sdk.fastacp.fastacp import FastACP + +# Create the ACP server +acp = FastACP.create( + acp_type="agentic", + config=TemporalACPConfig( + # When deployed to the cluster, the Temporal address will automatically be set to the cluster address + # For local development, we set the address manually to talk to the local Temporal service set up via docker compose + # We are also adding the Open AI Agents SDK plugin to the ACP. + type="temporal", + temporal_address=os.getenv("TEMPORAL_ADDRESS", "localhost:7233"), + plugins=[OpenAIAgentsPlugin( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(days=1) + ) + )] + ) +) + + +# Notice that we don't need to register any handlers when we use type="temporal" +# If you look at the code in agentex.sdk.fastacp.impl.temporal_acp +# You can see that these handlers are automatically registered when the ACP is created + +# @acp.on_task_create +# This will be handled by the method in your workflow that is decorated with @workflow.run + +# @acp.on_task_event_send +# This will be handled by the method in your workflow that is decorated with @workflow.signal(name=SignalName.RECEIVE_MESSAGE) + +# @acp.on_task_cancel +# This does not need to be handled by your workflow. +# It is automatically handled by the temporal client which cancels the workflow directly \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/activities.py b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/activities.py new file mode 100644 index 00000000..4cb05654 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/activities.py @@ -0,0 +1,45 @@ +import random +import asyncio + +from temporalio import activity, workflow +from temporalio.workflow import ParentClosePolicy + +from project.child_workflow import ChildWorkflow +from agentex.lib.environment_variables import EnvironmentVariables + +environment_variables = EnvironmentVariables.refresh() + +@activity.defn +async def get_weather(city: str) -> str: + """Get the weather for a given city""" + if city == "New York City": + return "The weather in New York City is 22 degrees Celsius" + else: + return "The weather is unknown" + +@activity.defn +async def withdraw_money() -> None: + """Withdraw money from an account""" + random_number = random.randint(0, 100) + await asyncio.sleep(random_number) + print("Withdrew money from account") + +@activity.defn +async def deposit_money() -> None: + """Deposit money into an account""" + await asyncio.sleep(10) + print("Deposited money into account") + + +@activity.defn +async def confirm_order() -> bool: + """Confirm order""" + result = await workflow.execute_child_workflow( + ChildWorkflow.on_task_create, + environment_variables.WORKFLOW_NAME + "_child", + id="child-workflow-id", + parent_close_policy=ParentClosePolicy.TERMINATE, + ) + + print(result) + return True diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/child_workflow.py b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/child_workflow.py new file mode 100644 index 00000000..3dc8520a --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/child_workflow.py @@ -0,0 +1,68 @@ +""" +Child Workflow for Human-in-the-Loop Pattern + +Child workflow that waits indefinitely for external human input via Temporal signals. +Benefits: Durable waiting, survives system failures, can wait days/weeks without resource consumption. + +Usage: External systems send signals to trigger workflow completion. +Production: Replace CLI with web dashboards, mobile apps, or API integrations. +""" + +import asyncio + +from temporalio import workflow + +from agentex.lib.utils.logging import make_logger +from agentex.lib.environment_variables import EnvironmentVariables + +environment_variables = EnvironmentVariables.refresh() +logger = make_logger(__name__) + + +@workflow.defn(name=environment_variables.WORKFLOW_NAME + "_child") +class ChildWorkflow(): + """ + Child workflow that waits for human approval via external signals. + + Lifecycle: Spawned by parent → waits for signal → human approves → completes. + Signal: temporal workflow signal --workflow-id="child-workflow-id" --name="fulfill_order_signal" --input=true + """ + + def __init__(self): + # Queue to handle signals from external systems (human input) + self._pending_confirmation: asyncio.Queue[bool] = asyncio.Queue() + + @workflow.run + async def on_task_create(self, name: str) -> str: + """ + Wait indefinitely for human approval signal. + + Uses workflow.wait_condition() to pause until external signal received. + Survives system failures and resumes exactly where it left off. + """ + logger.info(f"Child workflow started: {name}") + + while True: + # Wait until human sends approval signal (queue becomes non-empty) + await workflow.wait_condition( + lambda: not self._pending_confirmation.empty() + ) + + # Process human input and complete workflow + while not self._pending_confirmation.empty(): + break + + return "Task completed" + + @workflow.signal + async def fulfill_order_signal(self, success: bool) -> None: + """ + Receive human approval decision and trigger workflow completion. + + External systems send this signal to provide human input. + CLI: temporal workflow signal --workflow-id="child-workflow-id" --name="fulfill_order_signal" --input=true + Production: Use Temporal SDK from web apps, mobile apps, APIs, etc. + """ + # Add human decision to queue, which triggers wait_condition to resolve + if success == True: + await self._pending_confirmation.put(True) diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/run_worker.py b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/run_worker.py new file mode 100644 index 00000000..67aed618 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/run_worker.py @@ -0,0 +1,47 @@ +import asyncio +from datetime import timedelta + +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters + +from project.workflow import ChildWorkflow, ExampleTutorialWorkflow +from project.activities import confirm_order, deposit_money, withdraw_money +from agentex.lib.utils.debug import setup_debug_if_enabled +from agentex.lib.utils.logging import make_logger +from agentex.lib.environment_variables import EnvironmentVariables +from agentex.lib.core.temporal.activities import get_all_activities +from agentex.lib.core.temporal.workers.worker import AgentexWorker + +environment_variables = EnvironmentVariables.refresh() + +logger = make_logger(__name__) + + +async def main(): + # Setup debug mode if enabled + setup_debug_if_enabled() + + task_queue_name = environment_variables.WORKFLOW_TASK_QUEUE + if task_queue_name is None: + raise ValueError("WORKFLOW_TASK_QUEUE is not set") + + # Add activities to the worker + all_activities = get_all_activities() + [withdraw_money, deposit_money, confirm_order] # add your own activities here + + # Create a worker with automatic tracing + # We are also adding the Open AI Agents SDK plugin to the worker. + worker = AgentexWorker( + task_queue=task_queue_name, + plugins=[OpenAIAgentsPlugin( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(days=1) + ) + )], + ) + + await worker.run( + activities=all_activities, + workflows=[ExampleTutorialWorkflow, ChildWorkflow] + ) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/tools.py b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/tools.py new file mode 100644 index 00000000..92208ac4 --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/tools.py @@ -0,0 +1,37 @@ +""" +Human-in-the-Loop Tools for OpenAI Agents SDK + Temporal Integration + +Tools that pause agent execution and wait for human input using child workflows and signals. +Pattern: Agent calls tool → spawns child workflow → waits for signal → human approves → continues. +""" + +from agents import function_tool +from temporalio import workflow +from temporalio.workflow import ParentClosePolicy + +from project.child_workflow import ChildWorkflow +from agentex.lib.environment_variables import EnvironmentVariables + +environment_variables = EnvironmentVariables.refresh() + +@function_tool +async def wait_for_confirmation() -> str: + """ + Pause agent execution and wait for human approval via child workflow. + + Spawns a child workflow that waits for external signal. Human approves via: + temporal workflow signal --workflow-id="child-workflow-id" --name="fulfill_order_signal" --input=true + + Benefits: Durable waiting, survives system failures, scalable to millions of workflows. + """ + + # Spawn child workflow that waits for human signal + # Child workflow has fixed ID "child-workflow-id" so external systems can signal it + result = await workflow.execute_child_workflow( + ChildWorkflow.on_task_create, + environment_variables.WORKFLOW_NAME + "_child", + id="child-workflow-id", + parent_close_policy=ParentClosePolicy.TERMINATE, + ) + + return result \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/workflow.py b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/workflow.py new file mode 100644 index 00000000..c19e097a --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/project/workflow.py @@ -0,0 +1,135 @@ +""" +OpenAI Agents SDK + Temporal Integration: Human-in-the-Loop Tutorial + +This tutorial demonstrates how to pause agent execution and wait for human approval +using Temporal's child workflows and signals. + +KEY CONCEPTS: +- Child workflows: Independent workflows spawned by parent for human interaction +- Signals: External systems can send messages to running workflows +- Durable waiting: Agents can wait indefinitely for human input without losing state + +WHY THIS MATTERS: +Without Temporal, if your system crashes while waiting for human approval, you lose +all context. With Temporal, the agent resumes exactly where it left off after +system failures, making human-in-the-loop workflows production-ready. + +PATTERN: +1. Agent calls wait_for_confirmation tool +2. Tool spawns child workflow that waits for signal +3. Human approves via CLI/web app +4. Child workflow completes, agent continues + +Usage: `temporal workflow signal --workflow-id="child-workflow-id" --name="fulfill_order_signal" --input=true` +""" + +import json +import asyncio + +from agents import Agent, Runner +from temporalio import workflow + +from agentex.lib import adk +from project.tools import wait_for_confirmation +from agentex.lib.types.acp import SendEventParams, CreateTaskParams +from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent +from agentex.lib.environment_variables import EnvironmentVariables +from agentex.lib.core.temporal.types.workflow import SignalName +from agentex.lib.core.temporal.workflows.workflow import BaseWorkflow + +environment_variables = EnvironmentVariables.refresh() + +if environment_variables.WORKFLOW_NAME is None: + raise ValueError("Environment variable WORKFLOW_NAME is not set") + +if environment_variables.AGENT_NAME is None: + raise ValueError("Environment variable AGENT_NAME is not set") + +logger = make_logger(__name__) + +@workflow.defn(name=environment_variables.WORKFLOW_NAME) +class ExampleTutorialWorkflow(BaseWorkflow): + """ + Human-in-the-Loop Temporal Workflow + + Demonstrates agents that can pause execution and wait for human approval. + When approval is needed, the agent spawns a child workflow that waits for + external signals (human input) before continuing. + + Benefits: Durable waiting, survives system failures, scalable to millions of workflows. + """ + def __init__(self): + super().__init__(display_name=environment_variables.AGENT_NAME) + self._complete_task = False + self._pending_confirmation: asyncio.Queue[str] = asyncio.Queue() + + @workflow.signal(name=SignalName.RECEIVE_EVENT) + async def on_task_event_send(self, params: SendEventParams) -> None: + """ + Handle user messages with human-in-the-loop approval capability. + + When the agent needs human approval, it calls wait_for_confirmation which spawns + a child workflow that waits for external signals before continuing. + """ + logger.info(f"Received task message instruction: {params}") + + # Echo user message back to UI + await adk.messages.create(task_id=params.task.id, content=params.event.content) + + # Create agent with human-in-the-loop capability + # The wait_for_confirmation tool spawns a child workflow that waits for external signals + confirm_order_agent = Agent( + name="Confirm Order", + instructions="You are a helpful confirm order agent. When a user asks you to confirm an order, use the wait_for_confirmation tool to wait for confirmation.", + tools=[ + wait_for_confirmation, + ], + ) + + # Run agent - when human approval is needed, it will spawn child workflow and wait + result = await Runner.run(confirm_order_agent, params.event.content.content) + + # Send response back to user (includes result of any human approval process) + await adk.messages.create( + task_id=params.task.id, + content=TextContent( + author="agent", + content=result.final_output, + ), + ) + + @workflow.run + async def on_task_create(self, params: CreateTaskParams) -> str: + """ + Workflow entry point - starts the long-running human-in-the-loop agent. + + Handles both automated decisions and human approval workflows durably. + To approve waiting actions: temporal workflow signal --workflow-id="child-workflow-id" --name="fulfill_order_signal" --input=true + """ + logger.info(f"Received task create params: {params}") + + # Send welcome message when task is created + await adk.messages.create( + task_id=params.task.id, + content=TextContent( + author="agent", + content=f"Hello! I've received your task. Normally you can do some state initialization here, or just pass and do nothing until you get your first event. For now I'm just acknowledging that I've received a task with the following params:\n\n{json.dumps(params.params, indent=2)}.\n\nYou should only see this message once, when the task is created. All subsequent events will be handled by the `on_task_event_send` handler.", + ), + ) + + # Keep workflow running indefinitely to handle user messages and human approvals + # This survives system failures and can resume exactly where it left off + await workflow.wait_condition( + lambda: self._complete_task, + timeout=None, # No timeout for long-running human-in-the-loop workflows + ) + return "Task completed" + + # TEMPORAL UI (localhost:8080): + # - Main workflow shows agent activities + ChildWorkflow activity when approval needed + # - Child workflow appears as separate "child-workflow-id" that waits for signal + # - Timeline: invoke_model_activity → ChildWorkflow (waiting) → invoke_model_activity (after approval) + # + # To approve: temporal workflow signal --workflow-id="child-workflow-id" --name="fulfill_order_signal" --input=true + # Production: Replace CLI with web dashboards/APIs that send signals programmatically diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/pyproject.toml b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/pyproject.toml new file mode 100644 index 00000000..b95b19bf --- /dev/null +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "example_tutorial" +version = "0.1.0" +description = "An AgentEx agent" +requires-python = ">=3.12" +dependencies = [ + "agentex-sdk", + "scale-gp", + "temporalio>=1.18.0,<2", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "black", + "isort", + "flake8", + "debugpy>=1.8.15", +] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.black] +line-length = 88 +target-version = ['py312'] + +[tool.isort] +profile = "black" +line_length = 88 \ No newline at end of file