Skip to content

Commit f95aa72

Browse files
author
Krzysztof Dziedzic
committed
test: set up itk nightly runs
1 parent 666b203 commit f95aa72

8 files changed

Lines changed: 295 additions & 108 deletions

File tree

.github/workflows/itk.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
run: bash run_itk.sh
3232
working-directory: itk
3333
env:
34-
A2A_SAMPLES_REVISION: itk-v.021-alpha
34+
A2A_ITK_REVISION: main

.github/workflows/nightly.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Nightly ITK
2+
3+
on:
4+
schedule:
5+
- cron: '0 2 * * *' # 2:00 AM UTC daily
6+
workflow_dispatch: # Allow manual execution
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
nightly:
13+
name: Nightly ITK Run
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v6
19+
20+
- name: Install uv
21+
uses: astral-sh/setup-uv@v7
22+
23+
- name: Run Nightly ITK Tests
24+
run: bash run_itk.sh
25+
working-directory: itk
26+
env:
27+
A2A_ITK_REVISION: main
28+
ITK_NIGHTLY_RUN: "True"
29+
30+
- name: Upload Results to Rolling Release
31+
uses: softprops/action-gh-release@v2
32+
with:
33+
tag_name: "nightly-metrics"
34+
prerelease: true
35+
files: |
36+
itk/itk_python.json
37+
env:
38+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ docker-compose.yaml
1515
docs/ai/ai_learnings.md
1616

1717
# ITK Integration Test Artifacts
18-
itk/a2a-samples/
18+
itk/a2a-itk/
1919
itk/pyproto/
2020
itk/instruction.proto
2121
itk/logs/

itk/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ podman system migrate
3232

3333
### 1. Set Environment Variable
3434

35-
You must set the `A2A_SAMPLES_REVISION` environment variable to specify which revision of the `a2a-samples` repository to use for testing. This can be a branch name, tag, or commit hash.
35+
You must set the `A2A_ITK_REVISION` environment variable to specify which revision of the `a2a-itk` repository to use for testing. This can be a branch name, tag, or commit hash.
3636

3737
Example:
3838
```
39-
export A2A_SAMPLES_REVISION=itk-v.021-alpha
39+
export A2A_ITK_REVISION=main
4040
```
4141

4242
### 2. Execute Tests
@@ -48,7 +48,7 @@ Run the test script from this directory:
4848
```
4949

5050
The script will:
51-
- Clone `a2a-samples` (if not already present).
51+
- Clone `a2a-itk` (if not already present).
5252
- Checkout the specified revision.
5353
- Build the ITK service Docker image.
5454
- Run the tests and output results.

itk/main.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from a2a.client.errors import A2AClientError
2020
from a2a.compat.v0_3 import a2a_v0_3_pb2_grpc
2121
from a2a.compat.v0_3.grpc_handler import CompatGrpcHandler
22+
from a2a.compat.v0_3.types import Role as LegacyRole
2223
from a2a.server.agent_execution import AgentExecutor, RequestContext
2324
from a2a.server.events import EventQueue
2425
from a2a.server.routes import (
@@ -34,7 +35,7 @@
3435
InMemoryPushNotificationConfigStore,
3536
)
3637
from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore
37-
from a2a.types import a2a_pb2_grpc
38+
from a2a.types import Role, a2a_pb2_grpc
3839
from a2a.types.a2a_pb2 import (
3940
AgentCapabilities,
4041
AgentCard,
@@ -100,9 +101,22 @@ def extract_instruction(
100101
continue
101102
else:
102103
return inst
104+
103105
return None
104106

105107

108+
def _get_text_from_part(part: Any) -> str | None:
109+
"""Safely extracts text string from a Part object supporting protobuf, pydantic, and raw dict."""
110+
if not part:
111+
return None
112+
if hasattr(part, 'HasField') and part.HasField('text'):
113+
return part.text
114+
root = getattr(part, 'root', part)
115+
if isinstance(root, dict):
116+
return root.get('text')
117+
return getattr(root, 'text', None)
118+
119+
106120
def _extract_text_from_event(event: Any) -> list[str]:
107121
"""Extracts text parts from an event's message."""
108122
if isinstance(event, tuple):
@@ -128,7 +142,7 @@ def _extract_text_from_event(event: Any) -> list[str]:
128142
return results
129143

130144

131-
async def _handle_call_agent_with_resubscribe(
145+
async def _handle_call_agent_with_resubscribe( # noqa: PLR0912, PLR0915
132146
client: Client, request: SendMessageRequest
133147
) -> list[str]:
134148
"""Handles the send-disconnect-resubscribe flow."""
@@ -154,9 +168,32 @@ async def _handle_call_agent_with_resubscribe(
154168
finished = False
155169
async for event in resub_agen:
156170
logger.info('Event after re-subscribe: %s', event)
157-
if hasattr(event, 'HasField') and event.HasField('task'):
171+
if isinstance(event, Task):
172+
task_obj = event
173+
elif hasattr(event, 'HasField') and event.HasField('task'):
158174
task_obj = event.task
159175

176+
if task_obj and hasattr(task_obj, 'history'):
177+
for msg in task_obj.history:
178+
if msg.role in (
179+
Role.ROLE_AGENT,
180+
LegacyRole.agent,
181+
'ROLE_AGENT',
182+
):
183+
for part in msg.parts:
184+
text = _get_text_from_part(part)
185+
if text and 'task-finished' in text:
186+
logger.info(
187+
'Found task-finished in history, breaking loop!'
188+
)
189+
results.append(text.replace('task-finished', ''))
190+
finished = True
191+
break
192+
if finished:
193+
break
194+
if finished:
195+
break
196+
160197
extracted_text = _extract_text_from_event(event)
161198
for text in extracted_text:
162199
processed_text = text.replace('task-finished', '')
@@ -171,14 +208,13 @@ async def _handle_call_agent_with_resubscribe(
171208
if not results and task_obj and hasattr(task_obj, 'history'):
172209
logger.info('Results empty after loop, reading from history.')
173210
for msg in task_obj.history:
174-
# Check stringified role to support protobuf enums (2 for ROLE_AGENT in v0.3 and v1.0)
175-
# as well as string descriptors from dict/JSON forms.
176-
if str(msg.role) in {'2', 'ROLE_AGENT', 'agent'}:
177-
results.extend(
178-
part.text.replace('task-finished', '')
179-
for part in msg.parts
180-
if part.text
181-
)
211+
# Check role using SDK schemas for v1.0 (protobuf enum Role.ROLE_AGENT)
212+
# and v0.3 (pydantic enum LegacyRole.agent), as well as string forms.
213+
if msg.role in (Role.ROLE_AGENT, LegacyRole.agent, 'ROLE_AGENT'):
214+
for part in msg.parts:
215+
text = _get_text_from_part(part)
216+
if text:
217+
results.append(text.replace('task-finished', ''))
182218

183219
if not finished:
184220
logger.info('Canceling task %s after retrieval.', task_id)

itk/run_itk.sh

Lines changed: 35 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ cleanup() {
1414
docker stop itk-service > /dev/null 2>&1 || true
1515
docker rm itk-service > /dev/null 2>&1 || true
1616
docker rmi itk_service > /dev/null 2>&1 || true
17-
rm -rf a2a-samples > /dev/null 2>&1 || true
17+
rm -rf a2a-itk > /dev/null 2>&1 || true
1818
rm -rf pyproto > /dev/null 2>&1 || true
1919
rm -f instruction.proto > /dev/null 2>&1 || true
2020
echo "Done. Final exit code: $RESULT"
@@ -23,24 +23,24 @@ cleanup() {
2323
# Register cleanup function to run on script exit
2424
trap cleanup EXIT
2525

26-
# 1. Pull a2a-samples and checkout revision
27-
: "${A2A_SAMPLES_REVISION:?A2A_SAMPLES_REVISION environment variable must be set}"
26+
# 1. Pull a2a-itk and checkout revision
27+
: "${A2A_ITK_REVISION:?A2A_ITK_REVISION environment variable must be set}"
2828

29-
if [ ! -d "a2a-samples" ]; then
30-
git clone https://github.com/a2aproject/a2a-samples.git a2a-samples
29+
if [ ! -d "a2a-itk" ]; then
30+
git clone https://github.com/a2aproject/a2a-itk.git a2a-itk
3131
fi
32-
cd a2a-samples
32+
cd a2a-itk
3333
git fetch origin
34-
git checkout "$A2A_SAMPLES_REVISION"
34+
git checkout "$A2A_ITK_REVISION"
3535

3636
# Only pull if it's a branch (not a detached HEAD)
3737
if git symbolic-ref -q HEAD > /dev/null; then
38-
git pull origin "$A2A_SAMPLES_REVISION"
38+
git pull origin "$A2A_ITK_REVISION"
3939
fi
4040
cd ..
4141

42-
# 2. Copy instruction.proto from a2a-samples
43-
cp a2a-samples/itk/protos/instruction.proto ./instruction.proto
42+
# 2. Copy instruction.proto from a2a-itk
43+
cp a2a-itk/protos/instruction.proto ./instruction.proto
4444

4545
# 3. Build pyproto library
4646
mkdir -p pyproto
@@ -54,9 +54,9 @@ uv run --with grpcio-tools python -m grpc_tools.protoc \
5454
# Fix imports in generated file
5555
sed -i 's/^import instruction_pb2 as instruction__pb2/from . import instruction_pb2 as instruction__pb2/' pyproto/instruction_pb2_grpc.py
5656

57-
# 4. Build jit itk_service docker image from root of a2a-samples/itk
58-
# We run docker build from the itk directory inside a2a-samples
59-
docker build -t itk_service a2a-samples/itk
57+
# 4. Build jit itk_service docker image from root of a2a-itk
58+
# We run docker build from the root directory of a2a-itk
59+
docker build -t itk_service a2a-itk
6060

6161
# 5. Start docker service
6262
# Mounting a2a-python as repo and itk as current agent
@@ -109,86 +109,28 @@ if ! curl -s http://127.0.0.1:8000/ > /dev/null; then
109109
exit 1
110110
fi
111111

112-
echo "ITK Service is up! Sending compatibility test request..."
112+
SCENARIO_FILE="scenarios.json"
113+
if [ "${ITK_NIGHTLY_RUN^^}" = "TRUE" ]; then
114+
SCENARIO_FILE="scenarios_full.json"
115+
fi
116+
117+
echo "ITK Service is up! Sending compatibility test request using $SCENARIO_FILE..."
113118
RESPONSE=$(curl -s -X POST http://127.0.0.1:8000/run \
114119
-H "Content-Type: application/json" \
115-
-d '{
116-
"tests": [
117-
{
118-
"name": "Star Topology (Full) - JSONRPC & GRPC",
119-
"sdks": ["current", "python_v10", "python_v03", "go_v10", "go_v03"],
120-
"traversal": "euler",
121-
"edges": ["0->1", "0->2", "0->3", "0->4", "1->0", "2->0", "3->0", "4->0"],
122-
"protocols": ["jsonrpc", "grpc"],
123-
"behavior": "send_message"
124-
},
125-
{
126-
"name": "Star Topology (No Go v03) - HTTP_JSON",
127-
"sdks": ["current", "python_v10", "python_v03", "go_v10"],
128-
"traversal": "euler",
129-
"edges": ["0->1", "0->2", "0->3", "1->0", "2->0", "3->0"],
130-
"protocols": ["http_json"],
131-
"behavior": "send_message"
132-
},
133-
{
134-
"name": "Star Topology (Full) - JSONRPC & GRPC (Streaming)",
135-
"sdks": ["current", "python_v10", "python_v03", "go_v10", "go_v03"],
136-
"traversal": "euler",
137-
"edges": ["0->1", "0->2", "0->3", "0->4", "1->0", "2->0", "3->0", "4->0"],
138-
"protocols": ["jsonrpc", "grpc"],
139-
"streaming": true,
140-
"behavior": "send_message"
141-
},
142-
{
143-
"name": "Star Topology (No Go v03) - HTTP_JSON (Streaming)",
144-
"sdks": ["current", "python_v10", "python_v03", "go_v10"],
145-
"traversal": "euler",
146-
"edges": ["0->1", "0->2", "0->3", "1->0", "2->0", "3->0"],
147-
"protocols": ["http_json"],
148-
"streaming": true,
149-
"behavior": "send_message"
150-
},
151-
{
152-
"name": "Push Notification Test - JSONRPC & GRPC",
153-
"sdks": ["current", "python_v10", "python_v03", "go_v03"],
154-
"traversal": "euler",
155-
"edges": ["0->1", "0->2", "0->3", "1->0", "2->0", "3->0"],
156-
"protocols": ["jsonrpc", "grpc"],
157-
"behavior": "push_notification"
158-
},
159-
{
160-
"name": "Push Notification Test - HTTP_JSON",
161-
"sdks": ["current", "python_v10", "python_v03"],
162-
"traversal": "euler",
163-
"edges": ["0->1", "0->2", "1->0", "2->0"],
164-
"protocols": ["http_json"],
165-
"behavior": "push_notification"
166-
},
167-
{
168-
"name": "Resubscribe Test - JSONRPC",
169-
"sdks": ["current", "python_v10", "python_v03", "go_v10", "go_v03"],
170-
"traversal": "euler",
171-
"edges": ["0->1", "0->2", "0->3", "0->4", "1->0", "2->0", "3->0", "4->0"],
172-
"protocols": ["jsonrpc"],
173-
"streaming": true,
174-
"behavior": "resubscribe"
175-
},
176-
{
177-
"name": "Resubscribe Test - Python & Go Non-JSONRPC Protocols",
178-
"sdks": ["current", "python_v10", "python_v03", "go_v10"],
179-
"traversal": "euler",
180-
"edges": ["0->1", "0->2", "0->3", "1->0", "2->0", "3->0"],
181-
"protocols": ["grpc", "http_json"],
182-
"streaming": true,
183-
"behavior": "resubscribe"
184-
}
185-
]
186-
}')
187-
188-
echo "--------------------------------------------------------"
189-
echo "ITK TEST RESULTS:"
190-
echo "--------------------------------------------------------"
191-
echo "$RESPONSE" | python3 -c "
120+
-d "@$SCENARIO_FILE")
121+
122+
if [ "${ITK_NIGHTLY_RUN^^}" = "TRUE" ]; then
123+
echo "Nightly run detected. Saving raw results and running process_results.py..."
124+
echo "$RESPONSE" > raw_results.json
125+
python3 a2a-itk/scripts/process_results.py \
126+
--history_output_file itk_python.json \
127+
--history_url https://github.com/a2aproject/a2a-python/releases/download/nightly-metrics/itk_python.json
128+
RESULT=$?
129+
else
130+
echo "--------------------------------------------------------"
131+
echo "ITK TEST RESULTS:"
132+
echo "--------------------------------------------------------"
133+
echo "$RESPONSE" | python3 -c "
192134
import sys, json
193135
try:
194136
data = json.load(sys.stdin)
@@ -206,7 +148,8 @@ except Exception as e:
206148
print(f'Raw response: {data if \"data\" in locals() else \"no data\"}')
207149
sys.exit(1)
208150
"
209-
RESULT=$?
151+
RESULT=$?
152+
fi
210153
set -e
211154

212155
if [ $RESULT -ne 0 ]; then

0 commit comments

Comments
 (0)