Skip to content

Commit 9eb6d4c

Browse files
authored
Merge branch 'next' into msun/agenticToAsync
2 parents 2cdae9b + 714c719 commit 9eb6d4c

File tree

6 files changed

+229
-54
lines changed

6 files changed

+229
-54
lines changed

.github/workflows/build-and-push-tutorial-agent.yml

Lines changed: 189 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,192 @@ name: Build and Push Tutorial Agent
33
on:
44
workflow_dispatch:
55
inputs:
6-
agent_path:
7-
description: "Path to the agent directory (e.g., examples/tutorials/10_async/00_base/000_hello_acp)"
8-
required: true
9-
type: string
10-
version_tag:
11-
description: "Version tag for the agent build (e.g., v1.0.0, latest)"
12-
required: true
13-
type: string
14-
default: "latest"
15-
16-
workflow_call:
17-
inputs:
18-
agent_path:
19-
description: "Path to the agent directory"
20-
required: true
21-
type: string
22-
version_tag:
23-
description: "Version tag for the agent build"
24-
required: true
25-
type: string
26-
default: "latest"
6+
rebuild_all:
7+
description: "Rebuild all tutorial agents regardless of changes, this is reserved for maintainers only."
8+
required: false
9+
type: boolean
10+
default: false
11+
12+
pull_request:
13+
paths:
14+
- "examples/tutorials/**"
15+
16+
push:
17+
branches:
18+
- main
19+
paths:
20+
- "examples/tutorials/**"
21+
22+
permissions:
23+
contents: read
24+
packages: write
25+
26+
jobs:
27+
check-permissions:
28+
if: ${{ github.event_name == 'workflow_dispatch' }}
29+
runs-on: ubuntu-latest
30+
steps:
31+
- name: Check if user is maintainer
32+
uses: actions/github-script@v7
33+
with:
34+
script: |
35+
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
36+
owner: context.repo.owner,
37+
repo: context.repo.repo,
38+
username: context.actor
39+
});
40+
41+
const allowedRoles = ['admin', 'maintain'];
42+
if (!allowedRoles.includes(permission.permission)) {
43+
throw new Error(`❌ User ${context.actor} does not have sufficient permissions. Required: ${allowedRoles.join(', ')}. Current: ${permission.permission}`);
44+
}
45+
46+
find-agents:
47+
runs-on: ubuntu-latest
48+
needs: [check-permissions]
49+
if: ${{ !cancelled() && !failure() }}
50+
outputs:
51+
agents: ${{ steps.get-agents.outputs.agents }}
52+
has_agents: ${{ steps.get-agents.outputs.has_agents }}
53+
steps:
54+
- name: Checkout repository
55+
uses: actions/checkout@v4
56+
with:
57+
fetch-depth: 0 # Fetch full history for git diff
58+
59+
- name: Find tutorial agents to build
60+
id: get-agents
61+
env:
62+
REBUILD_ALL: ${{ inputs.rebuild_all }}
63+
run: |
64+
# Find all tutorial directories with manifest.yaml
65+
all_agents=$(find examples/tutorials -name "manifest.yaml" -exec dirname {} \; | sort)
66+
agents_to_build=()
67+
68+
if [ "$REBUILD_ALL" = "true" ]; then
69+
echo "Rebuild all agents requested"
70+
agents_to_build=($(echo "$all_agents"))
71+
72+
echo "### 🔄 Rebuilding All Tutorial Agents" >> $GITHUB_STEP_SUMMARY
73+
else
74+
# Determine the base branch for comparison
75+
if [ "${{ github.event_name }}" = "pull_request" ]; then
76+
BASE_BRANCH="origin/${{ github.base_ref }}"
77+
echo "Comparing against PR base branch: $BASE_BRANCH"
78+
else
79+
BASE_BRANCH="HEAD~1"
80+
echo "Comparing against previous commit: $BASE_BRANCH"
81+
fi
82+
83+
# Check each agent directory for changes
84+
for agent_dir in $all_agents; do
85+
echo "Checking $agent_dir for changes..."
86+
87+
# Check if any files in this agent directory have changed
88+
if git diff --name-only $BASE_BRANCH HEAD | grep -q "^$agent_dir/"; then
89+
echo " ✅ Changes detected in $agent_dir"
90+
agents_to_build+=("$agent_dir")
91+
else
92+
echo " ⏭️ No changes in $agent_dir - skipping build"
93+
fi
94+
done
95+
96+
echo "### 🔄 Changed Tutorial Agents" >> $GITHUB_STEP_SUMMARY
97+
fi
98+
99+
# Convert array to JSON format and output summary
100+
if [ ${#agents_to_build[@]} -eq 0 ]; then
101+
echo "No agents to build"
102+
echo "agents=[]" >> $GITHUB_OUTPUT
103+
echo "has_agents=false" >> $GITHUB_OUTPUT
104+
else
105+
echo "Agents to build: ${#agents_to_build[@]}"
106+
agents_json=$(printf '%s\n' "${agents_to_build[@]}" | jq -R -s -c 'split("\n") | map(select(length > 0))')
107+
echo "agents=$agents_json" >> $GITHUB_OUTPUT
108+
echo "has_agents=true" >> $GITHUB_OUTPUT
109+
110+
echo "" >> $GITHUB_STEP_SUMMARY
111+
for agent in "${agents_to_build[@]}"; do
112+
echo "- \`$agent\`" >> $GITHUB_STEP_SUMMARY
113+
done
114+
echo "" >> $GITHUB_STEP_SUMMARY
115+
fi
116+
117+
build-agents:
118+
needs: find-agents
119+
if: ${{ needs.find-agents.outputs.has_agents == 'true' }}
120+
runs-on: ubuntu-latest
121+
timeout-minutes: 15
122+
strategy:
123+
matrix:
124+
agent_path: ${{ fromJson(needs.find-agents.outputs.agents) }}
125+
fail-fast: false
126+
127+
name: build-${{ matrix.agent_path }}
128+
steps:
129+
- name: Checkout repository
130+
uses: actions/checkout@v4
131+
132+
- name: Set up Docker Buildx
133+
uses: docker/setup-buildx-action@v3
134+
135+
- name: Set up Python
136+
uses: actions/setup-python@v4
137+
with:
138+
python-version: "3.12"
139+
140+
- name: Get latest agentex-sdk version from PyPI
141+
id: get-version
142+
run: |
143+
LATEST_VERSION=$(curl -s https://pypi.org/pypi/agentex-sdk/json | jq -r '.info.version')
144+
echo "Latest agentex-sdk version: $LATEST_VERSION"
145+
echo "AGENTEX_SDK_VERSION=$LATEST_VERSION" >> $GITHUB_ENV
146+
pip install agentex-sdk==$LATEST_VERSION
147+
echo "Installed agentex-sdk version $LATEST_VERSION"
148+
149+
- name: Generate Image name
150+
id: image-name
151+
run: |
152+
# Remove examples/tutorials/ prefix and replace / with -
153+
AGENT_NAME=$(echo "${{ matrix.agent_path }}" | sed 's|^examples/tutorials/||' | sed 's|/|-|g')
154+
echo "AGENT_NAME=$AGENT_NAME" >> $GITHUB_ENV
155+
echo "agent_name=$AGENT_NAME" >> $GITHUB_OUTPUT
156+
echo "Agent name set to $AGENT_NAME"
157+
158+
- name: Login to GitHub Container Registry
159+
# Only login if we're going to push (main branch or rebuild_all)
160+
if: ${{ github.event_name == 'push' || inputs.rebuild_all }}
161+
uses: docker/login-action@v3
162+
with:
163+
registry: ghcr.io
164+
username: ${{ github.actor }}
165+
password: ${{ secrets.GITHUB_TOKEN }}
166+
167+
- name: Build and Conditionally Push Agent Image
168+
env:
169+
REGISTRY: ghcr.io
170+
run: |
171+
AGENT_NAME="${{ steps.image-name.outputs.agent_name }}"
172+
REPOSITORY_NAME="${{ github.repository }}/tutorial-agents/${AGENT_NAME}"
173+
174+
# Determine if we should push based on event type
175+
if [ "${{ github.event_name }}" = "push" ] || [ "${{ inputs.rebuild_all }}" = "true" ]; then
176+
SHOULD_PUSH=true
177+
VERSION_TAG="latest"
178+
echo "🚀 Building and pushing agent: ${{ matrix.agent_path }}"
179+
else
180+
SHOULD_PUSH=false
181+
VERSION_TAG="${{ github.commit.sha }}"
182+
echo "🔍 Validating build for agent: ${{ matrix.agent_path }}"
183+
fi
184+
185+
# Build command - add --push only if we should push
186+
BUILD_ARGS="--manifest ${{ matrix.agent_path }}/manifest.yaml --registry ${REGISTRY} --tag ${VERSION_TAG} --platforms linux/amd64 --repository-name ${REPOSITORY_NAME}"
187+
188+
if [ "$SHOULD_PUSH" = "true" ]; then
189+
agentex agents build $BUILD_ARGS --push
190+
echo "✅ Successfully built and pushed: ${REGISTRY}/${REPOSITORY_NAME}:${VERSION_TAG}"
191+
else
192+
agentex agents build $BUILD_ARGS
193+
echo "✅ Build validation successful for: ${{ matrix.agent_path }}"
194+
fi

.stats.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 34
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp%2Fagentex-sdk-0556db8b729c565a582332ce7e175b6b0d95e0d56addd543673a9e52ebd5d58b.yml
3-
openapi_spec_hash: 8c0f9039f66b0017b2dea4574efefed4
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp%2Fagentex-sdk-5ba3790d74703c432197dccf5f03b4f4e40ab6a72dedd7abaea76ec720339148.yml
3+
openapi_spec_hash: a8ffcb25135faa13b5d180f6ee1f920d
44
config_hash: 0197f86ba1a4b1b5ce813d0e62138588

examples/tutorials/00_sync/000_hello_acp/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ RUN uv pip install --system --upgrade pip setuptools wheel
2222

2323
ENV UV_HTTP_TIMEOUT=1000
2424

25+
2526
# Copy pyproject.toml and README.md to install dependencies
2627
COPY 000_hello_acp/pyproject.toml /app/000_hello_acp/pyproject.toml
2728
COPY 000_hello_acp/README.md /app/000_hello_acp/README.md
@@ -38,4 +39,4 @@ RUN uv pip install --system .
3839
ENV PYTHONPATH=/app
3940

4041
# Run the agent using uvicorn
41-
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/000_hello_acp/project/acp.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
logger = make_logger(__name__)
1111

12-
1312
# Create an ACP server
1413
acp = FastACP.create(
1514
acp_type="sync",
@@ -18,18 +17,17 @@
1817

1918
@acp.on_message_send
2019
async def handle_message_send(
21-
params: SendMessageParams
20+
params: SendMessageParams,
2221
) -> Union[TaskMessageContent, AsyncGenerator[TaskMessageUpdate, None]]:
2322
"""Default message handler with streaming support"""
2423
# Extract content safely from the message
2524
message_text = ""
26-
if hasattr(params.content, 'content'):
27-
content_val = getattr(params.content, 'content', '')
25+
if hasattr(params.content, "content"):
26+
content_val = getattr(params.content, "content", "")
2827
if isinstance(content_val, str):
2928
message_text = content_val
3029

3130
return TextContent(
3231
author="agent",
3332
content=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_text}",
3433
)
35-

src/agentex/lib/adk/providers/_modules/sync_provider.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -164,18 +164,22 @@ async def get_response(
164164
output_items = response_output if isinstance(response_output, list) else [response_output]
165165

166166
for item in output_items:
167-
item_dict = _serialize_item(item)
168-
if item_dict:
169-
new_items.append(item_dict)
170-
171-
# Extract final_output from message type if available
172-
if item_dict.get('type') == 'message' and not final_output:
173-
content = item_dict.get('content', [])
174-
if content and isinstance(content, list):
175-
for content_part in content:
176-
if isinstance(content_part, dict) and 'text' in content_part:
177-
final_output = content_part['text']
178-
break
167+
try:
168+
item_dict = _serialize_item(item)
169+
if item_dict:
170+
new_items.append(item_dict)
171+
172+
# Extract final_output from message type if available
173+
if item_dict.get('type') == 'message' and not final_output:
174+
content = item_dict.get('content', [])
175+
if content and isinstance(content, list):
176+
for content_part in content:
177+
if isinstance(content_part, dict) and 'text' in content_part:
178+
final_output = content_part['text']
179+
break
180+
except Exception as e:
181+
logger.warning(f"Failed to serialize item in get_response: {e}")
182+
continue
179183

180184
span.output = {
181185
"new_items": new_items,
@@ -275,18 +279,22 @@ async def stream_response(
275279
if event_type == 'response.output_item.done':
276280
item = getattr(event, 'item', None)
277281
if item is not None:
278-
item_dict = _serialize_item(item)
279-
if item_dict:
280-
new_items.append(item_dict)
281-
282-
# Update final_response_text from message type if available
283-
if item_dict.get('type') == 'message':
284-
content = item_dict.get('content', [])
285-
if content and isinstance(content, list):
286-
for content_part in content:
287-
if isinstance(content_part, dict) and 'text' in content_part:
288-
final_response_text = content_part['text']
289-
break
282+
try:
283+
item_dict = _serialize_item(item)
284+
if item_dict:
285+
new_items.append(item_dict)
286+
287+
# Update final_response_text from message type if available
288+
if item_dict.get('type') == 'message':
289+
content = item_dict.get('content', [])
290+
if content and isinstance(content, list):
291+
for content_part in content:
292+
if isinstance(content_part, dict) and 'text' in content_part:
293+
final_response_text = content_part['text']
294+
break
295+
except Exception as e:
296+
logger.warning(f"Failed to serialize item in stream_response: {e}")
297+
continue
290298

291299
yield event
292300

src/agentex/types/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Agent(BaseModel):
3535
registration_metadata: Optional[Dict[str, object]] = None
3636
"""The metadata for the agent's registration."""
3737

38-
status: Optional[Literal["Ready", "Failed", "Unknown", "Deleted"]] = None
38+
status: Optional[Literal["Ready", "Failed", "Unknown", "Deleted", "Unhealthy"]] = None
3939
"""The status of the action, indicating if it's building, ready, failed, etc."""
4040

4141
status_reason: Optional[str] = None

0 commit comments

Comments
 (0)