Skip to content

Commit eaea8fe

Browse files
authored
Merge pull request #150 from scaleapi/adding-tutorials
Adding tests for tutorials
2 parents 747b7d8 + 43f5613 commit eaea8fe

File tree

65 files changed

+4996
-216
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+4996
-216
lines changed

.github/scripts/sync_agents.py

Whitespace-only changes.

.github/workflows/main.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,5 @@ name: Test AgentEx Tutorials
22

33
on:
44
workflow_dispatch:
5-
6-
workflow_call:
7-
85

6+
workflow_call:

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ jobs:
2121
curl -sSf https://rye.astral.sh/get | bash
2222
echo "$HOME/.rye/shims" >> $GITHUB_PATH
2323
env:
24-
RYE_VERSION: '0.44.0'
25-
RYE_INSTALL_OPTION: '--yes'
24+
RYE_VERSION: "0.44.0"
25+
RYE_INSTALL_OPTION: "--yes"
2626

2727
- name: Publish to PyPI
2828
run: |

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ dist
1414
codegen.log
1515
Brewfile.lock.json
1616

17-
.DS_Store
17+
.DS_Store
18+
19+
examples/**/uv.lock

examples/tutorials/00_sync/000_hello_acp/Dockerfile

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ RUN uv pip install --system --upgrade pip setuptools wheel
2222

2323
ENV UV_HTTP_TIMEOUT=1000
2424

25-
# Copy just the requirements file to optimize caching
26-
COPY 000_hello_acp/requirements.txt /app/requirements.txt
25+
# Copy pyproject.toml and README.md to install dependencies
26+
COPY 000_hello_acp/pyproject.toml /app/000_hello_acp/pyproject.toml
27+
COPY 000_hello_acp/README.md /app/000_hello_acp/README.md
2728

28-
WORKDIR /app/
29-
30-
# Install the required Python packages
31-
RUN uv pip install --system -r requirements.txt
29+
WORKDIR /app/000_hello_acp
3230

3331
# Copy the project code
34-
COPY 000_hello_acp/project /app/project
32+
COPY 000_hello_acp/project /app/000_hello_acp/project
33+
34+
# Install the required Python packages from pyproject.toml
35+
RUN uv pip install --system .
3536

3637
# Set environment variables
3738
ENV PYTHONPATH=/app

examples/tutorials/00_sync/000_hello_acp/manifest.yaml

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
build:
1616
context:
1717
# Root directory for the build context
18-
root: ../ # Keep this as the default root
18+
root: ../ # Keep this as the default root
1919

2020
# Paths to include in the Docker build context
2121
# Must include:
@@ -34,14 +34,13 @@ build:
3434
# Helps keep build context small and builds fast
3535
dockerignore: 000_hello_acp/.dockerignore
3636

37-
3837
# Local Development Configuration
3938
# -----------------------------
4039
# Only used when running the agent locally
4140
local_development:
4241
agent:
43-
port: 8000 # Port where your local ACP server is running
44-
host_address: host.docker.internal # Host address for Docker networking (host.docker.internal for Docker, localhost for direct)
42+
port: 8000 # Port where your local ACP server is running
43+
host_address: host.docker.internal # Host address for Docker networking (host.docker.internal for Docker, localhost for direct)
4544

4645
# File paths for local development (relative to this manifest.yaml)
4746
paths:
@@ -53,7 +52,6 @@ local_development:
5352
# /absolute/path/acp.py (absolute path)
5453
acp: project/acp.py
5554

56-
5755
# Agent Configuration
5856
# -----------------
5957
agent:
@@ -83,39 +81,39 @@ agent:
8381
# secret_name: openai-api-key
8482
# secret_key: api-key
8583

86-
# Optional: Set Environment variables for running your agent locally as well
84+
# Optional: Set Environment variables for running your agent locally as well
8785
# as for deployment later on
8886
# env:
8987
# - name: OPENAI_BASE_URL
9088
# value: "https://api.openai.com/v1"
9189
# - name: ACCOUNT_ID
9290
# value: "your_account_id_here"
9391

94-
9592
# Deployment Configuration
9693
# -----------------------
9794
# Configuration for deploying your agent to Kubernetes clusters
9895
deployment:
9996
# Container image configuration
10097
image:
10198
repository: "" # Update with your container registry
102-
tag: "latest" # Default tag, should be versioned in production
99+
tag: "latest" # Default tag, should be versioned in production
103100

104101
# Global deployment settings that apply to all clusters
105102
# These can be overridden in cluster-specific files (deploy/*.yaml)
106103
global:
107104
agent:
108105
name: "s000-hello-acp"
109106
description: "An AgentEx agent that just says hello and acknowledges the user's message"
110-
107+
111108
# Default replica count
112109
replicaCount: 1
113-
110+
114111
# Default resource requirements
115112
resources:
116113
requests:
117114
cpu: "500m"
118115
memory: "1Gi"
119116
limits:
120117
cpu: "1000m"
121-
memory: "2Gi"
118+
memory: "2Gi"
119+

examples/tutorials/00_sync/000_hello_acp/pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ requires-python = ">=3.12"
1111
dependencies = [
1212
"agentex-sdk",
1313
"scale-gp",
14+
"pytest",
15+
"pytest-xdist"
1416
]
1517

1618
[project.optional-dependencies]
@@ -30,4 +32,4 @@ target-version = ['py312']
3032

3133
[tool.isort]
3234
profile = "black"
33-
line_length = 88
35+
line_length = 88
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
Sample tests for AgentEx ACP agent.
3+
4+
This test suite demonstrates how to test the main AgentEx API functions:
5+
- Non-streaming message sending
6+
- Streaming message sending
7+
- Task creation via RPC
8+
9+
To run these tests:
10+
1. Make sure the agent is running (via docker-compose or `agentex agents run`)
11+
2. Set the AGENTEX_API_BASE_URL environment variable if not using default
12+
3. Run: pytest test_agent.py -v
13+
14+
Configuration:
15+
- AGENTEX_API_BASE_URL: Base URL for the AgentEx server (default: http://localhost:5003)
16+
- AGENT_NAME: Name of the agent to test (default: hello-acp)
17+
"""
18+
19+
import os
20+
from agentex.types import TextContentParam, TextDelta, TextContent
21+
from agentex.types.agent_rpc_params import ParamsSendMessageRequest
22+
from agentex.types.task_message_update import StreamTaskMessageDelta, StreamTaskMessageFull
23+
import pytest
24+
from agentex import Agentex
25+
26+
27+
# Configuration from environment variables
28+
AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003")
29+
AGENT_NAME = os.environ.get("AGENT_NAME", "s000-hello-acp")
30+
31+
32+
@pytest.fixture
33+
def client():
34+
"""Create an AgentEx client instance for testing."""
35+
client = Agentex(base_url=AGENTEX_API_BASE_URL)
36+
yield client
37+
# Clean up: close the client connection
38+
client.close()
39+
40+
41+
@pytest.fixture
42+
def agent_name():
43+
"""Return the agent name for testing."""
44+
return AGENT_NAME
45+
46+
47+
class TestNonStreamingMessages:
48+
"""Test non-streaming message sending."""
49+
50+
def test_send_simple_message(self, client: Agentex, agent_name: str):
51+
"""Test sending a simple message and receiving a response."""
52+
53+
message_content = "Hello, Agent! How are you?"
54+
response = client.agents.send_message(
55+
agent_name=agent_name,
56+
params=ParamsSendMessageRequest(
57+
content=TextContentParam(
58+
author="user",
59+
content=message_content,
60+
type="text",
61+
)
62+
),
63+
)
64+
result = response.result
65+
assert result is not None
66+
assert len(result) == 1
67+
message = result[0]
68+
assert isinstance(message.content, TextContent)
69+
assert (
70+
message.content.content
71+
== f"Hello! I've received your message. Here's a generic response, but in future tutorials we'll see how you can get me to intelligently respond to your message. This is what I heard you say: {message_content}"
72+
)
73+
74+
75+
class TestStreamingMessages:
76+
"""Test streaming message sending."""
77+
78+
def test_stream_simple_message(self, client: Agentex, agent_name: str):
79+
"""Test streaming a simple message and aggregating deltas."""
80+
81+
message_content = "Hello, Agent! Can you stream your response?"
82+
aggregated_content = ""
83+
full_content = ""
84+
received_chunks = False
85+
86+
for chunk in client.agents.send_message_stream(
87+
agent_name=agent_name,
88+
params=ParamsSendMessageRequest(
89+
content=TextContentParam(
90+
author="user",
91+
content=message_content,
92+
type="text",
93+
)
94+
),
95+
):
96+
received_chunks = True
97+
task_message_update = chunk.result
98+
# Collect text deltas as they arrive or check full messages
99+
if isinstance(task_message_update, StreamTaskMessageDelta) and task_message_update.delta is not None:
100+
delta = task_message_update.delta
101+
if isinstance(delta, TextDelta) and delta.text_delta is not None:
102+
aggregated_content += delta.text_delta
103+
104+
elif isinstance(task_message_update, StreamTaskMessageFull):
105+
content = task_message_update.content
106+
if isinstance(content, TextContent):
107+
full_content = content.content
108+
109+
if not full_content and not aggregated_content:
110+
raise AssertionError("No content was received in the streaming response.")
111+
if not received_chunks:
112+
raise AssertionError("No streaming chunks were received, when at least 1 was expected.")
113+
114+
if full_content:
115+
assert (
116+
full_content
117+
== f"Hello! I've received your message. Here's a generic response, but in future tutorials we'll see how you can get me to intelligently respond to your message. This is what I heard you say: {message_content}"
118+
)
119+
120+
if aggregated_content:
121+
assert (
122+
aggregated_content
123+
== f"Hello! I've received your message. Here's a generic response, but in future tutorials we'll see how you can get me to intelligently respond to your message. This is what I heard you say: {message_content}"
124+
)
125+
126+
127+
if __name__ == "__main__":
128+
pytest.main([__file__, "-v"])

examples/tutorials/00_sync/010_multiturn/Dockerfile

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,21 @@ RUN uv pip install --system --upgrade pip setuptools wheel
2222

2323
ENV UV_HTTP_TIMEOUT=1000
2424

25-
# Copy just the requirements file to optimize caching
26-
COPY 010_multiturn/requirements.txt /app/requirements.txt
25+
# Copy pyproject.toml and README.md to install dependencies
26+
COPY 010_multiturn/pyproject.toml /app/010_multiturn/pyproject.toml
27+
COPY 010_multiturn/README.md /app/010_multiturn/README.md
2728

28-
WORKDIR /app/
29-
30-
# Install the required Python packages
31-
RUN uv pip install --system -r requirements.txt
29+
WORKDIR /app/010_multiturn
3230

3331
# Copy the project code
34-
COPY 010_multiturn/project /app/project
32+
COPY 010_multiturn/project /app/010_multiturn/project
33+
34+
# Install the required Python packages from pyproject.toml
35+
RUN uv pip install --system .
3536

37+
WORKDIR /app/010_multiturn
3638
# Set environment variables
3739
ENV PYTHONPATH=/app
3840

3941
# Run the agent using uvicorn
40-
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
42+
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]

examples/tutorials/00_sync/010_multiturn/project/acp.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33

44
from agentex.lib import adk
55
from agentex.lib.types.acp import SendMessageParams
6-
from agentex.types.task_message import TaskMessageContent
76
from agentex.lib.utils.model_utils import BaseModel
87
from agentex.lib.types.llm_messages import LLMConfig, UserMessage, SystemMessage, AssistantMessage
98
from agentex.lib.sdk.fastacp.fastacp import FastACP
109
from agentex.types.task_message_update import TaskMessageUpdate
11-
from agentex.types.task_message_content import TextContent
10+
from agentex.types.task_message_content import TaskMessageContent
11+
from agentex.types import TextContent
1212

1313
# Create an ACP server
1414
acp = FastACP.create(
@@ -24,7 +24,7 @@ class StateModel(BaseModel):
2424
# Note: The return of this handler is required to be persisted by the Agentex Server
2525
@acp.on_message_send
2626
async def handle_message_send(
27-
params: SendMessageParams
27+
params: SendMessageParams,
2828
) -> Union[TaskMessageContent, AsyncGenerator[TaskMessageUpdate, None]]:
2929
"""
3030
In this tutorial, we'll see how to handle a basic multi-turn conversation without streaming.
@@ -33,12 +33,12 @@ async def handle_message_send(
3333
# 0. Validate the message.
3434
#########################################################
3535

36-
if not hasattr(params.content, 'type') or params.content.type != "text":
36+
if not hasattr(params.content, "type") or params.content.type != "text":
3737
raise ValueError(f"Expected text message, got {getattr(params.content, 'type', 'unknown')}")
3838

39-
if not hasattr(params.content, 'author') or params.content.author != "user":
39+
if not hasattr(params.content, "author") or params.content.author != "user":
4040
raise ValueError(f"Expected user message, got {getattr(params.content, 'author', 'unknown')}")
41-
41+
4242
if not os.environ.get("OPENAI_API_KEY"):
4343
return TextContent(
4444
author="agent",
@@ -74,12 +74,14 @@ async def handle_message_send(
7474
llm_messages = [
7575
SystemMessage(content=state.system_prompt),
7676
*[
77-
UserMessage(content=getattr(message.content, 'content', '')) if getattr(message.content, 'author', None) == "user" else AssistantMessage(content=getattr(message.content, 'content', ''))
77+
UserMessage(content=getattr(message.content, "content", ""))
78+
if getattr(message.content, "author", None) == "user"
79+
else AssistantMessage(content=getattr(message.content, "content", ""))
7880
for message in task_messages
79-
if getattr(message.content, 'type', None) == "text"
80-
]
81+
if getattr(message.content, "type", None) == "text"
82+
],
8183
]
82-
84+
8385
# TaskMessages are messages that are sent between an Agent and a Client. They are fundamentally decoupled from messages sent to the LLM. This is because you may want to send additional metadata to allow the client to render the message on the UI differently.
8486

8587
# LLMMessages are OpenAI-compatible messages that are sent to the LLM, and are used to track the state of a conversation with a model.
@@ -90,7 +92,7 @@ async def handle_message_send(
9092
# - Taking a markdown document output by an LLM, postprocessing it into a JSON object to clearly denote title, content, and footers. This can be sent as a DataContent TaskMessage to the client and converted back to markdown here to send back to the LLM.
9193
# - If using multiple LLMs (like in an actor-critic framework), you may want to send DataContent that denotes which LLM generated which part of the output and write conversion logic to split the TaskMessagehistory into multiple LLM conversations.
9294
# - If using multiple LLMs, but one LLM's output should not be sent to the user (i.e. a critic model), you can leverage the State as an internal storage mechanism to store the critic model's conversation history. This i s a powerful and flexible way to handle complex scenarios.
93-
95+
9496
#########################################################
9597
# 4. Call an LLM to respond to the user's message.
9698
#########################################################
@@ -113,7 +115,4 @@ async def handle_message_send(
113115
else:
114116
content_str = ""
115117

116-
return TextContent(
117-
author="agent",
118-
content=content_str
119-
)
118+
return TextContent(author="agent", content=content_str)

0 commit comments

Comments
 (0)