Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions .github/workflows/agentex-tutorials-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: Test Tutorial Agents

on:
workflow_dispatch:

jobs:
find-tutorials:
runs-on: ubuntu-latest
outputs:
tutorials: ${{ steps.get-tutorials.outputs.tutorials }}
steps:
- name: Checkout agentex-python repo
uses: actions/checkout@v4

- name: Find all tutorials
id: get-tutorials
run: |
cd examples/tutorials
# Find all tutorials and exclude specific temporal ones
all_tutorials=$(find . -name "manifest.yaml" -exec dirname {} \; | sort | sed 's|^\./||')

# Filter out the specified temporal tutorials that are being updated
filtered_tutorials=$(echo "$all_tutorials" | grep -v -E "(temporal)")

# Convert to JSON array
tutorials=$(echo "$filtered_tutorials" | jq -R -s -c 'split("\n") | map(select(length > 0))')

echo "tutorials=$tutorials" >> $GITHUB_OUTPUT
echo "All tutorials found: $(echo "$all_tutorials" | wc -l)"
echo "Filtered tutorials: $(echo "$filtered_tutorials" | wc -l)"
echo "Excluded tutorials:"
echo "$all_tutorials" | grep -E "(10_temporal/050_|10_temporal/070_|10_temporal/080_)" || echo " (none matched exclusion pattern)"
echo "Final tutorial list: $tutorials"

test-tutorial:
needs: find-tutorials
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
tutorial: ${{ fromJson(needs.find-tutorials.outputs.tutorials) }}
fail-fast: false
name: test-${{ matrix.tutorial }}

steps:
- name: Checkout agentex-python repo
uses: actions/checkout@v4

- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Checkout scale-agentex repo
uses: actions/checkout@v4
with:
repository: scaleapi/scale-agentex
path: scale-agentex

- name: Configure Docker Compose for host networking
run: |
cd scale-agentex/agentex
echo "🔧 Configuring AgentEx container for GitHub Actions networking..."

# Install yq for YAML manipulation
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq

# Add extra_hosts to agentex service to make host.docker.internal work
yq eval '.services.agentex.extra_hosts = ["host.docker.internal:host-gateway"]' -i docker-compose.yml

echo "✅ Added extra_hosts configuration to agentex service"

- name: Start AgentEx Server
run: |
cd scale-agentex/agentex
echo "🚀 Starting AgentEx server and dependencies..."

# Start all services
docker compose up -d

echo "⏳ Waiting for dependencies to be healthy..."

# Wait for services to be healthy
for i in {1..30}; do
if docker compose ps | grep -q "healthy"; then
echo "✅ Dependencies are healthy"
break
fi
echo " Attempt $i/30: Waiting for services..."
sleep 5
done

# Wait specifically for AgentEx server to be ready
echo "⏳ Waiting for AgentEx server to be ready..."
for i in {1..30}; do
if curl -s --max-time 5 http://localhost:5003/health >/dev/null 2>&1; then
echo "✅ AgentEx server is ready"
break
fi
echo " Attempt $i/30: Waiting for AgentEx server..."
sleep 5
done

- name: Build AgentEx SDK
run: |
echo "🔨 Building AgentEx SDK wheel..."
uv build
echo "✅ SDK built successfully"
ls -la dist/

- name: Test Tutorial
working-directory: ./examples/tutorials
env:
OPENAI_API_KEY: ${{ secrets.TUTORIAL_OPENAI_API_KEY }}
HEALTH_CHECK_PORT: 8080 # Use non-privileged port for temporal worker health checks
run: |
echo "Testing tutorial: ${{ matrix.tutorial }}"
AGENTEX_API_BASE_URL="http://localhost:5003" \
./run_agent_test.sh --build-cli "${{ matrix.tutorial }}"

- name: Record test result
id: test-result
if: always()
run: |
if [ "${{ steps.run-test.outcome }}" == "success" ]; then
echo "result=passed" >> $GITHUB_OUTPUT
else
echo "result=failed" >> $GITHUB_OUTPUT
fi

test-summary:
if: always()
needs: [find-tutorials, test-tutorial]
runs-on: ubuntu-latest
name: Test Summary
steps:
- name: Generate Test Summary
run: |
echo "# 🧪 Tutorial Tests Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

# Get tutorial list from needs context
tutorials='${{ needs.find-tutorials.outputs.tutorials }}'
88 changes: 88 additions & 0 deletions .github/workflows/build-and-push-tutorial-agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,91 @@ on:
required: true
type: string
default: "latest"

permissions:
contents: read
packages: write

jobs:
build-and-push-agent:
timeout-minutes: 10
name: Build Tutorial Agent
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Validate agent path exists
run: |
if [ ! -d "${{ inputs.agent_path }}" ]; then
echo "❌ Error: Agent path '${{ inputs.agent_path }}' does not exist"
exit 1
fi
echo "✅ Agent path verified: ${{ inputs.agent_path }}"

Comment on lines +43 to +49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.

You can view more details about this finding in the Semgrep AppSec Platform.

- name: Validate manifest.yaml exists
run: |
if [ ! -f "${{ inputs.agent_path }}/manifest.yaml" ]; then
echo "❌ Error: manifest.yaml not found in '${{ inputs.agent_path }}'"
exit 1
fi
echo "✅ manifest.yaml found"
echo "### Validation Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Agent Path**: ${{ inputs.agent_path }}" >> $GITHUB_STEP_SUMMARY
echo "- **Version Tag**: ${{ inputs.version_tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Status**: ✅ Validation passed" >> $GITHUB_STEP_SUMMARY

Comment on lines +51 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.

You can view more details about this finding in the Semgrep AppSec Platform.

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"

- name: Get latest agentex-sdk version from PyPI
id: get-version
run: |
LATEST_VERSION=$(curl -s https://pypi.org/pypi/agentex-sdk/json | jq -r '.info.version')
echo "Latest agentex-sdk version: $LATEST_VERSION"
echo "AGENTEX_SDK_VERSION=$LATEST_VERSION" >> $GITHUB_ENV
pip install agentex-sdk==$LATEST_VERSION
echo "Installed agentex-sdk version $LATEST_VERSION"

- name: Generate Image name
id: image-name
run: |
# Remove examples/tutorials/ prefix and replace / with -
AGENT_NAME=$(echo "${{ inputs.agent_path }}" | sed 's|^examples/tutorials/||' | sed 's|/|-|g')
echo "AGENT_NAME=$AGENT_NAME" >> $GITHUB_ENV
echo "agent_name=$AGENT_NAME" >> $GITHUB_OUTPUT
echo "Agent name set to $AGENT_NAME"

Comment on lines +81 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.

You can view more details about this finding in the Semgrep AppSec Platform.

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and Push Agent Image
env:
REGISTRY: ghcr.io
run: |
AGENT_NAME="${{ steps.image-name.outputs.agent_name }}"
VERSION_TAG="${{ inputs.version_tag }}"
REPOSITORY_NAME="${{ github.repository }}/tutorial-agents/${AGENT_NAME}"
FULL_IMAGE="${REGISTRY}/${REPOSITORY_NAME}:${VERSION_TAG}"

agentex agents build \
--manifest "${{ inputs.agent_path }}/manifest.yaml" \
--registry "${REGISTRY}" \
--tag "${VERSION_TAG}" \
--platforms "linux/amd64" \
--repository-name "${REPOSITORY_NAME}" \
--push

echo "Successfully built and pushed: ${FULL_IMAGE}"
echo "### Build Complete" >> $GITHUB_STEP_SUMMARY
echo "- **Image**: \`${FULL_IMAGE}\`" >> $GITHUB_STEP_SUMMARY
Comment on lines +98 to +114
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.

You can view more details about this finding in the Semgrep AppSec Platform.

Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
config=AgenticACPConfig(type="base"),
)


class StateModel(BaseModel):
input_list: List[dict]
turn_number: int
Expand All @@ -53,11 +54,7 @@ class StateModel(BaseModel):
args=["-y", "@modelcontextprotocol/server-sequential-thinking"],
),
StdioServerParameters(
command="uvx",
args=["openai-websearch-mcp"],
env={
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "")
}
command="uvx", args=["openai-websearch-mcp"], env={"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "")}
),
]

Expand All @@ -72,6 +69,7 @@ async def handle_task_create(params: CreateTaskParams):
)
await adk.state.create(task_id=params.task.id, agent_id=params.agent.id, state=state)


@acp.on_task_event_send
async def handle_event_send(params: SendEventParams):
# !!! Warning: Because "Agentic" ACPs are designed to be fully asynchronous, race conditions can occur if parallel events are sent. It is highly recommended to use the "temporal" type in the AgenticACPConfig instead to handle complex use cases. The "base" ACP is only designed to be used for simple use cases and for learning purposes.
Expand All @@ -85,7 +83,6 @@ async def handle_event_send(params: SendEventParams):
if params.event.content.author != "user":
raise ValueError(f"Expected user message, got {params.event.content.author}")


# 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:
Expand All @@ -94,12 +91,8 @@ async def handle_event_send(params: SendEventParams):
state.turn_number += 1
# Add the new user message to the message history
state.input_list.append({"role": "user", "content": params.event.content.content})

async with adk.tracing.span(
trace_id=params.task.id,
name=f"Turn {state.turn_number}",
input=state
) as span:

async with adk.tracing.span(trace_id=params.task.id, name=f"Turn {state.turn_number}", input=state) as span:
# Echo back the user's message so it shows up 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,
Expand Down Expand Up @@ -156,6 +149,7 @@ async def handle_event_send(params: SendEventParams):
if span:
span.output = state


@acp.on_task_cancel
async def handle_task_cancel(params: CancelTaskParams):
"""Default task cancel handler"""
Expand All @@ -173,8 +167,8 @@ async def mcp_server_context(mcp_server_params: list[StdioServerParameters]):
servers = []
for params in mcp_server_params:
server = MCPServerStdio(
name=f"Server: {params.command}",
params=params.model_dump(),
name=f"Server: {params.command}",
params=params.model_dump(),
cache_tools_list=True,
client_session_timeout_seconds=60,
)
Expand Down Expand Up @@ -253,7 +247,6 @@ async def run_openai_agent_with_custom_streaming(
try:
# Process streaming events with TaskMessage creation
async for event in result.stream_events():

if event.type == "run_item_stream_event":
if event.item.type == "tool_call_item":
tool_call_item = event.item.raw_item
Expand Down Expand Up @@ -374,9 +367,7 @@ async def run_openai_agent_with_custom_streaming(
if span:
span.output = {
"new_items": [
item.raw_item.model_dump()
if isinstance(item.raw_item, BaseModel)
else item.raw_item
item.raw_item.model_dump() if isinstance(item.raw_item, BaseModel) else item.raw_item
for item in result.new_items
],
"final_output": result.final_output,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async def test_send_event_and_poll_simple_query(self, client: AsyncAgentex, agen
break

# Verify state has been updated by polling the states for 10 seconds
for i in range(10):
for i in range(20):
if i == 9:
raise Exception("Timeout waiting for state updates")
states = await client.states.list(agent_id=agent_id, task_id=task.id)
Expand Down Expand Up @@ -187,7 +187,12 @@ async def test_multi_turn_conversation_with_state(self, client: AsyncAgentex, ag
sleep_interval=1.0,
):
assert isinstance(message, TaskMessage)
if message.content and message.content.type == "text" and message.content.author == "agent" and message.content.content:
if (
message.content
and message.content.type == "text"
and message.content.author == "agent"
and message.content.content
):
break

## keep polling the states for 10 seconds for the input_list and turn_number to be updated
Expand Down Expand Up @@ -216,7 +221,12 @@ async def test_multi_turn_conversation_with_state(self, client: AsyncAgentex, ag
timeout=30,
sleep_interval=1.0,
):
if message.content and message.content.type == "text" and message.content.author == "agent" and message.content.content:
if (
message.content
and message.content.type == "text"
and message.content.author == "agent"
and message.content.content
):
response_text = message.content.content.lower()
assert "blue" in response_text
found_response = True
Expand Down Expand Up @@ -273,7 +283,10 @@ async def stream_messages() -> None:
# For full messages, content is at the top level
# For delta messages, we need to check parent_task_message
if msg_type == "full":
if event.get("content", {}).get("type") == "text" and event.get("content", {}).get("author") == "user":
if (
event.get("content", {}).get("type") == "text"
and event.get("content", {}).get("author") == "user"
):
user_message_found = True
elif msg_type == "done":
break
Expand Down
Loading