diff --git a/src/agentex/lib/cli/commands/init.py b/src/agentex/lib/cli/commands/init.py index d00149b5..1654f48c 100644 --- a/src/agentex/lib/cli/commands/init.py +++ b/src/agentex/lib/cli/commands/init.py @@ -26,6 +26,7 @@ class TemplateType(str, Enum): TEMPORAL_OPENAI_AGENTS = "temporal-openai-agents" DEFAULT = "default" SYNC = "sync" + SYNC_OPENAI_AGENTS = "sync-openai-agents" def render_template( @@ -58,6 +59,7 @@ def create_project_structure( TemplateType.TEMPORAL_OPENAI_AGENTS: ["acp.py", "workflow.py", "run_worker.py", "activities.py"], TemplateType.DEFAULT: ["acp.py"], TemplateType.SYNC: ["acp.py"], + TemplateType.SYNC_OPENAI_AGENTS: ["acp.py"], }[template_type] # Create project/code files @@ -155,7 +157,7 @@ def validate_agent_name(text: str) -> bool | str: choices=[ {"name": "Async - ACP Only", "value": TemplateType.DEFAULT}, {"name": "Async - Temporal", "value": "temporal_submenu"}, - {"name": "Sync ACP", "value": TemplateType.SYNC}, + {"name": "Sync ACP", "value": "sync_submenu"}, ], ).ask() if not template_type: @@ -163,7 +165,6 @@ def validate_agent_name(text: str) -> bool | str: # If Temporal was selected, show sub-menu for Temporal variants if template_type == "temporal_submenu": - console.print() template_type = questionary.select( "Which Temporal template would you like to use?", choices=[ @@ -173,6 +174,16 @@ def validate_agent_name(text: str) -> bool | str: ).ask() if not template_type: return + elif template_type == "sync_submenu": + template_type = questionary.select( + "Which Sync template would you like to use?", + choices=[ + {"name": "Basic Sync ACP", "value": TemplateType.SYNC}, + {"name": "Sync ACP + OpenAI Agents SDK (Recommended)", "value": TemplateType.SYNC_OPENAI_AGENTS}, + ], + ).ask() + if not template_type: + return project_path = questionary.path( "Where would you like to create your project?", default="." diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/.dockerignore.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/.dockerignore.j2 new file mode 100644 index 00000000..c2d7fca4 --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/.dockerignore.j2 @@ -0,0 +1,43 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Environments +.env** +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Git +.git +.gitignore + +# Misc +.DS_Store diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 new file mode 100644 index 00000000..2ac5be7d --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 @@ -0,0 +1,42 @@ +# 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/** + +RUN uv pip install --system --upgrade pip setuptools wheel + +ENV UV_HTTP_TIMEOUT=1000 + +# Copy just the pyproject.toml file to optimize caching +COPY {{ project_path_from_build_root }}/pyproject.toml /app/{{ project_path_from_build_root }}/pyproject.toml + +WORKDIR /app/{{ project_path_from_build_root }} + +# Install the required Python packages using uv +RUN uv pip install --system . + +# Copy the project code +COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project + +# Set environment variables +ENV PYTHONPATH=/app + +# Run the agent using uvicorn +CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile.j2 new file mode 100644 index 00000000..4d9f41d4 --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile.j2 @@ -0,0 +1,43 @@ +# 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 \ + node \ + npm \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN uv pip install --system --upgrade pip setuptools wheel + +ENV UV_HTTP_TIMEOUT=1000 + +# Copy just the requirements file to optimize caching +COPY {{ project_path_from_build_root }}/requirements.txt /app/{{ project_path_from_build_root }}/requirements.txt + +WORKDIR /app/{{ project_path_from_build_root }} + +# Install the required Python packages +RUN uv pip install --system -r requirements.txt + +# Copy the project code +COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project + + +# Set environment variables +ENV PYTHONPATH=/app + +# Run the agent using uvicorn +CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/README.md.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/README.md.j2 new file mode 100644 index 00000000..b2105705 --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/README.md.j2 @@ -0,0 +1,313 @@ +# {{ agent_name }} - AgentEx Sync ACP Template + +This is a starter template for building synchronous agents with the AgentEx framework. It provides a basic implementation of the Agent 2 Client Protocol (ACP) with immediate response capabilities 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. +- **Sync ACP**: Synchronous Agent Communication Protocol that requires immediate responses +- **Message Handling**: How to process and respond to messages in real-time + +## Running the Agent + +1. Run the agent locally: +```bash +agentex agents run --manifest manifest.yaml +``` + +The agent will start on port 8000 and respond immediately to any messages it receives. + +## What's Inside + +This template: +- Sets up a basic sync ACP server +- Handles incoming messages with immediate responses +- Provides a foundation for building real-time agents +- Can include streaming support for long responses + +## 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) + +This file is essential for both local development and deployment of your agent. + +## Project Structure + +``` +{{ project_name }}/ +├── project/ # Your agent's code +│ ├── __init__.py +│ └── acp.py # ACP server and event handlers +├── Dockerfile # Container definition +├── manifest.yaml # Deployment config +├── dev.ipynb # Development notebook for testing +{% if use_uv %} +└── pyproject.toml # Dependencies (uv) +{% else %} +└── requirements.txt # Dependencies (pip) +{% endif %} +``` + +## Development + +### 1. Customize Message Handlers +- Modify the handlers in `acp.py` to implement your agent's logic +- Add your own tools and capabilities +- Implement custom response generation + +### 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 +- **Non-streaming tests**: Send messages and get complete responses +- **Streaming tests**: Test real-time streaming responses +- **Task management**: Optional task creation and management + +The notebook automatically uses your agent name (`{{ agent_name }}`) and provides examples for both streaming and non-streaming message handling. + +### 3. Manage Dependencies + +{% if use_uv %} +You chose **uv** for package management. Here's how to work with dependencies: + +```bash +# Add new dependencies +agentex uv add requests openai anthropic + +# 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 + +{% else %} +You chose **pip** for package management. Here's how to work with dependencies: + +```bash +# Edit requirements.txt manually to add dependencies +echo "requests" >> requirements.txt +echo "openai" >> requirements.txt + +# Install dependencies +pip install -r requirements.txt +``` + +**Benefits of pip:** +- Familiar workflow for most Python developers +- Simple requirements.txt management +- Wide compatibility +{% endif %} + +### 4. Configure Credentials +Options: +1. Add any required credentials to your manifest.yaml via the `env` section +2. Export them in your shell: `export OPENAI_API_KEY=...` +3. For local development, create a `.env.local` file in the project directory + +## 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 +``` + +### 3. Run Your Agent +```bash +# From this directory +export ENVIRONMENT=development && agentex agents run --manifest manifest.yaml +``` + +### 4. Interact with Your Agent + +**Option 1: Web UI (Recommended)** +```bash +# Start the local web interface +cd agentex-web +make dev + +# Then open http://localhost:3000 in your browser to chat with your agent +``` + +**Option 2: CLI (Deprecated)** +```bash +# Submit a task via CLI +agentex tasks submit --agent {{ agent_name }} --task "Your task here" +``` + +## 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 + +### Sync ACP Considerations +- Responses must be immediate (no long-running operations) +- Use streaming for longer responses +- Keep processing lightweight and fast +- Consider caching for frequently accessed data + +### 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) +- Test response times and optimize for speed + +### To build the agent Docker image locally (normally not necessary): + +1. Build the agent image: +```bash +agentex agents build --manifest manifest.yaml +``` +{% if use_uv %} +```bash +# Build with uv +agentex agents build --manifest manifest.yaml --push +``` +{% else %} +```bash +# Build with pip +agentex agents build --manifest manifest.yaml --push +``` +{% endif %} + + +## Advanced Features + +### Streaming Responses +Handle long responses with streaming: + +```python +# In project/acp.py +@acp.on_message_send +async def handle_message_send(params: SendMessageParams): + # For streaming responses + async def stream_response(): + for chunk in generate_response_chunks(): + yield TaskMessageUpdate( + content=chunk, + is_complete=False + ) + yield TaskMessageUpdate( + content="", + is_complete=True + ) + + return stream_response() +``` + +### Custom Response Logic +Add sophisticated response generation: + +```python +# In project/acp.py +@acp.on_message_send +async def handle_message_send(params: SendMessageParams): + # Analyze input + user_message = params.content.content + + # Generate response + response = await generate_intelligent_response(user_message) + + return TextContent( + author=MessageAuthor.AGENT, + content=response + ) +``` + +### Integration with External Services +{% if use_uv %} +```bash +# Add service clients +agentex uv add httpx requests-oauthlib + +# Add AI/ML libraries +agentex uv add openai anthropic transformers + +# Add fast processing libraries +agentex uv add numpy pandas +``` +{% else %} +```bash +# Add to requirements.txt +echo "httpx" >> requirements.txt +echo "openai" >> requirements.txt +echo "numpy" >> requirements.txt +pip install -r requirements.txt +``` +{% endif %} + +## 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. **Slow response times** + - Profile your message handling code + - Consider caching expensive operations + - Optimize database queries and API calls + +3. **Dependency issues** +{% if use_uv %} + - Run `agentex uv sync` to ensure all dependencies are installed +{% else %} + - Run `pip install -r requirements.txt` + - Check if all dependencies are correctly listed in requirements.txt +{% endif %} + +4. **Port conflicts** + - Check if another service is using port 8000 + - Use `lsof -i :8000` to find conflicting processes + +Happy building with Sync ACP! 🚀⚡ \ No newline at end of file diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/dev.ipynb.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/dev.ipynb.j2 new file mode 100644 index 00000000..d8c10a65 --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/dev.ipynb.j2 @@ -0,0 +1,167 @@ +{ + "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 = \"{{ agent_name }}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f6e6ef0", + "metadata": {}, + "outputs": [], + "source": [ + "# # (Optional) Create a new task. If you don't create a new task, each message will be sent to a new task. The server will create the task for you.\n", + "\n", + "# import uuid\n", + "\n", + "# TASK_ID = str(uuid.uuid4())[:8]\n", + "\n", + "# rpc_response = client.agents.rpc_by_name(\n", + "# agent_name=AGENT_NAME,\n", + "# method=\"task/create\",\n", + "# params={\n", + "# \"name\": f\"{TASK_ID}-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": [ + "# Test non streaming response\n", + "from agentex.types import TextContent\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_message(\n", + " agent_name=AGENT_NAME,\n", + " params={\n", + " \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n", + " \"stream\": False\n", + " }\n", + ")\n", + "\n", + "if not rpc_response or not rpc_response.result:\n", + " raise ValueError(\"No result in response\")\n", + "\n", + "# Extract and print just the text content from the response\n", + "for task_message in rpc_response.result:\n", + " content = task_message.content\n", + " if isinstance(content, TextContent):\n", + " text = content.content\n", + " print(text)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79688331", + "metadata": {}, + "outputs": [], + "source": [ + "# Test streaming response\n", + "from agentex.types.task_message_update import StreamTaskMessageDelta, StreamTaskMessageFull\n", + "from agentex.types.text_delta import TextDelta\n", + "\n", + "\n", + "# The result object of message/send will be a TaskMessageUpdate which is a union of the following types:\n", + "# - StreamTaskMessageStart: \n", + "# - An indicator that a streaming message was started, doesn't contain any useful content\n", + "# - StreamTaskMessageDelta: \n", + "# - A delta of a streaming message, contains the text delta to aggregate\n", + "# - StreamTaskMessageDone: \n", + "# - An indicator that a streaming message was done, doesn't contain any useful content\n", + "# - StreamTaskMessageFull: \n", + "# - A non-streaming message, there is nothing to aggregate, since this contains the full message, not deltas\n", + "\n", + "# Whenn processing StreamTaskMessageDelta, if you are expecting more than TextDeltas, such as DataDelta, ToolRequestDelta, or ToolResponseDelta, you can process them as well\n", + "# Whenn processing StreamTaskMessageFull, if you are expecting more than TextContent, such as DataContent, ToolRequestContent, or ToolResponseContent, you can process them as well\n", + "\n", + "for agent_rpc_response_chunk in client.agents.send_message_stream(\n", + " agent_name=AGENT_NAME,\n", + " params={\n", + " \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n", + " \"stream\": True\n", + " }\n", + "):\n", + " # We know that the result of the message/send when stream is set to True will be a TaskMessageUpdate\n", + " task_message_update = agent_rpc_response_chunk.result\n", + " # Print oly the text deltas as they arrive or any full messages\n", + " if isinstance(task_message_update, StreamTaskMessageDelta):\n", + " delta = task_message_update.delta\n", + " if isinstance(delta, TextDelta):\n", + " print(delta.text_delta, end=\"\", flush=True)\n", + " else:\n", + " print(f\"Found non-text {type(task_message)} object in streaming message.\")\n", + " elif isinstance(task_message_update, StreamTaskMessageFull):\n", + " content = task_message_update.content\n", + " if isinstance(content, TextContent):\n", + " print(content.content)\n", + " else:\n", + " print(f\"Found non-text {type(task_message)} object in full message.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5e7e042", + "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 +} diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/environments.yaml.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/environments.yaml.j2 new file mode 100644 index 00000000..73924abd --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/environments.yaml.j2 @@ -0,0 +1,53 @@ +# 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-{{agent_name}}" +# ********** 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: + replicaCount: 2 + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" + diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 new file mode 100644 index 00000000..b006c617 --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 @@ -0,0 +1,115 @@ +# 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: + - {{ project_path_from_build_root }} + + # Path to your agent's Dockerfile + # This defines how your agent's image is built from the context + # Relative to the root directory + dockerfile: {{ project_path_from_build_root }}/Dockerfile + + # Path to your agent's .dockerignore + # Filters unnecessary files from the build context + # Helps keep build context small and builds fast + dockerignore: {{ project_path_from_build_root }}/.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 + + +# Agent Configuration +# ----------------- +agent: + acp_type: sync + # Unique name for your agent + # Used for task routing and monitoring + name: {{ agent_name }} + + # Description of what your agent does + # Helps with documentation and discovery + description: {{ description }} + + # Temporal workflow configuration + # Set enabled: true to use Temporal workflows for long-running tasks + temporal: + enabled: false + + # Optional: Credentials mapping + # Maps Kubernetes secrets to environment variables + # Common credentials include: + credentials: [] # Update with your credentials + # - 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: {} # Update with your environment variables + # 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: [] # Update with your image pull secret names + # - name: my-registry-secret + + # Global deployment settings that apply to all clusters + # These can be overridden in cluster-specific environments (environments.yaml) + global: + # 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/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 new file mode 100644 index 00000000..20b31caf --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 @@ -0,0 +1,137 @@ +import os +from typing import AsyncGenerator, List + +from agentex.lib import adk +from agentex.lib.adk.providers._modules.sync_provider import SyncStreamingProvider, convert_openai_to_agentex_events +from agentex.lib.sdk.fastacp.fastacp import FastACP +from agentex.lib.types.acp import SendMessageParams +from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config +from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.utils.model_utils import BaseModel + +from agentex.types.task_message_update import TaskMessageUpdate +from agentex.types.task_message_content import TaskMessageContent +from agentex.lib.utils.logging import make_logger +from agents import Agent, Runner, RunConfig, function_tool + + +logger = make_logger(__name__) + +SGP_API_KEY = os.environ.get("SGP_API_KEY", "") +SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") + +if SGP_API_KEY and SGP_ACCOUNT_ID: + add_tracing_processor_config( + SGPTracingProcessorConfig( + sgp_api_key=SGP_API_KEY, + sgp_account_id=SGP_ACCOUNT_ID, + ) + ) + + +MODEL = "gpt-4o-mini" + +SYSTEM_PROMPT = """ + +You are a helpful assistant. Use your tools to help the user. + + + +Communicate in a witty and friendly manner + +""" + +AGENT_NAME = "{{ agent_name }}" + + +@function_tool +async def get_weather() -> str: + """ + Get the current weather. + + This is a dummy activity that returns a hardcoded string for demo purposes. + Replace this with a real weather API call in your implementation. + + Returns: + A string describing the current weather conditions. + """ + logger.info("get_weather activity called") + return "Sunny, 72°F" + + + +# Create an ACP server +acp = FastACP.create( + acp_type="sync", +) + +class StateModel(BaseModel): + input_list: List[dict] + turn_number: int + + +@acp.on_message_send +async def handle_message_send( + params: SendMessageParams +) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]: + if not os.environ.get("OPENAI_API_KEY"): + yield StreamTaskMessageFull( + index=0, + type="full", + content=TextContent( + author="agent", + content="Hey, sorry I'm unable to respond to your message because you're running this example without an OpenAI API key. Please set the OPENAI_API_KEY environment variable to run this example. Do this by either by adding a .env file to the project/ directory or by setting the environment variable in your terminal.", + ), + ) + + user_prompt = params.content.content + + # Retrieve the task state. Each event is handled as a new turn, so we need to get the state for the current turn. + task_state = await adk.state.get_by_task_and_agent(task_id=params.task.id, agent_id=params.agent.id) + if not task_state: + # If the state doesn't exist, create it. + state = StateModel(input_list=[], turn_number=0) + task_state = await adk.state.create(task_id=params.task.id, agent_id=params.agent.id, state=state) + else: + state = StateModel.model_validate(task_state.state) + + state.turn_number += 1 + state.input_list.append({"role": "user", "content": user_prompt}) + + # Initialize the sync provider and run config to allow for tracing + provider = SyncStreamingProvider( + trace_id=params.task.id, + ) + + run_config = RunConfig( + model_provider=provider, + ) + + # Initialize the agent + agent = Agent( + name=AGENT_NAME, + instructions=SYSTEM_PROMPT, + model=MODEL, + tools=[get_weather], + ) + + # Run the agent with the conversation history from state + result = Runner.run_streamed( + agent, + state.input_list, + run_config=run_config + ) + + # Convert the OpenAI events to Agentex events and stream them back to the client + async for agentex_event in convert_openai_to_agentex_events(result.stream_events()): + yield agentex_event + + # After streaming is complete, update state with the full conversation history + state.input_list = result.to_input_list() + await adk.state.update( + state_id=task_state.id, + task_id=params.task.id, + agent_id=params.agent.id, + state=state, + trace_id=params.task.id, + ) \ No newline at end of file diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/pyproject.toml.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/pyproject.toml.j2 new file mode 100644 index 00000000..34e04e6a --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/pyproject.toml.j2 @@ -0,0 +1,32 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "{{ project_name }}" +version = "0.1.0" +description = "{{ description }}" +requires-python = ">=3.12" +dependencies = [ + "agentex-sdk", + "scale-gp", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "black", + "isort", + "flake8", +] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.black] +line-length = 88 +target-version = ['py312'] + +[tool.isort] +profile = "black" +line_length = 88 diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/requirements.txt.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/requirements.txt.j2 new file mode 100644 index 00000000..0b8ae19b --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/requirements.txt.j2 @@ -0,0 +1,5 @@ +# Install agentex-sdk from local path +agentex-sdk + +# Scale GenAI Platform Python SDK +scale-gp diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/test_agent.py.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/test_agent.py.j2 new file mode 100644 index 00000000..7de4684f --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/test_agent.py.j2 @@ -0,0 +1,70 @@ +""" +Sample tests for AgentEx ACP agent. + +This test suite demonstrates how to test the main AgentEx API functions: +- Non-streaming message sending +- Streaming message sending +- Task creation via RPC + +To run these tests: +1. Make sure the agent is running (via docker-compose or `agentex agents run`) +2. Set the AGENTEX_API_BASE_URL environment variable if not using default +3. Run: pytest test_agent.py -v + +Configuration: +- AGENTEX_API_BASE_URL: Base URL for the AgentEx server (default: http://localhost:5003) +- AGENT_NAME: Name of the agent to test (default: {{ agent_name }}) +""" + +import os +import pytest +from agentex import Agentex + + +# Configuration from environment variables +AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003") +AGENT_NAME = os.environ.get("AGENT_NAME", "{{ agent_name }}") + + +@pytest.fixture +def client(): + """Create an AgentEx client instance for testing.""" + return Agentex(base_url=AGENTEX_API_BASE_URL) + + +@pytest.fixture +def agent_name(): + """Return the agent name for testing.""" + return AGENT_NAME + + +@pytest.fixture +def agent_id(client, agent_name): + """Retrieve the agent ID based on the agent name.""" + agents = client.agents.list() + for agent in agents: + if agent.name == agent_name: + return agent.id + raise ValueError(f"Agent with name {agent_name} not found.") + + +class TestNonStreamingMessages: + """Test non-streaming message sending.""" + + def test_send_message(self, client: Agentex, _agent_name: str): + """Test sending a message and receiving a response.""" + # TODO: Fill in the test based on what data your agent is expected to handle + ... + + +class TestStreamingMessages: + """Test streaming message sending.""" + + def test_send_stream_message(self, client: Agentex, _agent_name: str): + """Test streaming a message and aggregating deltas.""" + # TODO: Fill in the test based on what data your agent is expected to handle + ... + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])