agent-starter-pack 0.18.2__py3-none-any.whl → 0.21.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_starter_pack/agents/{langgraph_base_react → adk_a2a_base}/.template/templateconfig.yaml +5 -12
- agent_starter_pack/agents/adk_a2a_base/README.md +37 -0
- agent_starter_pack/{frontends/streamlit/frontend/style/app_markdown.py → agents/adk_a2a_base/app/__init__.py} +3 -23
- agent_starter_pack/agents/adk_a2a_base/app/agent.py +70 -0
- agent_starter_pack/agents/adk_a2a_base/notebooks/adk_a2a_app_testing.ipynb +583 -0
- agent_starter_pack/agents/{crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb → adk_a2a_base/notebooks/evaluating_adk_agent.ipynb} +163 -199
- agent_starter_pack/agents/adk_a2a_base/tests/integration/test_agent.py +58 -0
- agent_starter_pack/agents/adk_base/app/__init__.py +2 -2
- agent_starter_pack/agents/adk_base/app/agent.py +3 -0
- agent_starter_pack/agents/adk_base/notebooks/adk_app_testing.ipynb +13 -28
- agent_starter_pack/agents/adk_live/app/__init__.py +17 -0
- agent_starter_pack/agents/adk_live/app/agent.py +3 -0
- agent_starter_pack/agents/agentic_rag/app/__init__.py +2 -2
- agent_starter_pack/agents/agentic_rag/app/agent.py +3 -0
- agent_starter_pack/agents/agentic_rag/notebooks/adk_app_testing.ipynb +13 -28
- agent_starter_pack/agents/{crewai_coding_crew → langgraph_base}/.template/templateconfig.yaml +12 -9
- agent_starter_pack/agents/langgraph_base/README.md +30 -0
- agent_starter_pack/agents/langgraph_base/app/__init__.py +17 -0
- agent_starter_pack/agents/{langgraph_base_react → langgraph_base}/app/agent.py +4 -4
- agent_starter_pack/agents/{langgraph_base_react → langgraph_base}/tests/integration/test_agent.py +1 -1
- agent_starter_pack/base_template/.gitignore +4 -2
- agent_starter_pack/base_template/Makefile +110 -16
- agent_starter_pack/base_template/README.md +97 -12
- agent_starter_pack/base_template/deployment/terraform/dev/apis.tf +4 -6
- agent_starter_pack/base_template/deployment/terraform/dev/providers.tf +5 -1
- agent_starter_pack/base_template/deployment/terraform/dev/variables.tf +5 -3
- agent_starter_pack/base_template/deployment/terraform/dev/{% if cookiecutter.is_adk %}telemetry.tf{% else %}unused_telemetry.tf{% endif %} +193 -0
- agent_starter_pack/base_template/deployment/terraform/github.tf +16 -9
- agent_starter_pack/base_template/deployment/terraform/locals.tf +7 -7
- agent_starter_pack/base_template/deployment/terraform/providers.tf +5 -1
- agent_starter_pack/base_template/deployment/terraform/sql/completions.sql +138 -0
- agent_starter_pack/base_template/deployment/terraform/storage.tf +0 -9
- agent_starter_pack/base_template/deployment/terraform/variables.tf +15 -19
- agent_starter_pack/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}build_triggers.tf{% else %}unused_build_triggers.tf{% endif %} +20 -22
- agent_starter_pack/base_template/deployment/terraform/{% if cookiecutter.is_adk %}telemetry.tf{% else %}unused_telemetry.tf{% endif %} +206 -0
- agent_starter_pack/base_template/pyproject.toml +5 -17
- agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +19 -4
- agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +36 -11
- agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +24 -5
- agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +44 -9
- agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/telemetry.py +96 -0
- agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/{utils → app_utils}/typing.py +4 -6
- agent_starter_pack/{agents/crewai_coding_crew/app/crew/config/agents.yaml → base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}converters{% else %}unused_converters{% endif %}/__init__.py } +9 -23
- agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}converters{% else %}unused_converters{% endif %}/part_converter.py +138 -0
- agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}executor{% else %}unused_executor{% endif %}/__init__.py +13 -0
- agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}executor{% else %}unused_executor{% endif %}/a2a_agent_executor.py +265 -0
- agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}executor{% else %}unused_executor{% endif %}/task_result_aggregator.py +152 -0
- agent_starter_pack/cli/commands/create.py +40 -4
- agent_starter_pack/cli/commands/enhance.py +1 -1
- agent_starter_pack/cli/commands/register_gemini_enterprise.py +1070 -0
- agent_starter_pack/cli/main.py +2 -0
- agent_starter_pack/cli/utils/cicd.py +20 -4
- agent_starter_pack/cli/utils/template.py +257 -25
- agent_starter_pack/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +113 -16
- agent_starter_pack/deployment_targets/agent_engine/tests/load_test/README.md +2 -2
- agent_starter_pack/deployment_targets/agent_engine/tests/load_test/load_test.py +178 -9
- agent_starter_pack/deployment_targets/agent_engine/tests/{% if cookiecutter.is_a2a %}helpers.py{% else %}unused_helpers.py{% endif %} +138 -0
- agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +193 -307
- agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/app_utils/deploy.py +414 -0
- agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/{utils → app_utils}/{% if cookiecutter.is_adk_live %}expose_app.py{% else %}unused_expose_app.py{% endif %} +13 -14
- agent_starter_pack/deployment_targets/cloud_run/Dockerfile +4 -1
- agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +85 -86
- agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/service.tf +139 -107
- agent_starter_pack/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +228 -12
- agent_starter_pack/deployment_targets/cloud_run/tests/load_test/README.md +4 -4
- agent_starter_pack/deployment_targets/cloud_run/tests/load_test/load_test.py +92 -12
- agent_starter_pack/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/{server.py → fast_api_app.py} +194 -121
- agent_starter_pack/frontends/adk_live_react/frontend/package-lock.json +18 -18
- agent_starter_pack/frontends/adk_live_react/frontend/src/multimodal-live-types.ts +5 -3
- agent_starter_pack/resources/docs/adk-cheatsheet.md +198 -41
- agent_starter_pack/resources/locks/uv-adk_a2a_base-agent_engine.lock +4966 -0
- agent_starter_pack/resources/locks/uv-adk_a2a_base-cloud_run.lock +5011 -0
- agent_starter_pack/resources/locks/uv-adk_base-agent_engine.lock +1443 -709
- agent_starter_pack/resources/locks/uv-adk_base-cloud_run.lock +1058 -874
- agent_starter_pack/resources/locks/uv-adk_live-agent_engine.lock +1443 -709
- agent_starter_pack/resources/locks/uv-adk_live-cloud_run.lock +1058 -874
- agent_starter_pack/resources/locks/uv-agentic_rag-agent_engine.lock +1568 -749
- agent_starter_pack/resources/locks/uv-agentic_rag-cloud_run.lock +1123 -929
- agent_starter_pack/resources/locks/{uv-langgraph_base_react-agent_engine.lock → uv-langgraph_base-agent_engine.lock} +1714 -1689
- agent_starter_pack/resources/locks/{uv-langgraph_base_react-cloud_run.lock → uv-langgraph_base-cloud_run.lock} +1285 -2374
- agent_starter_pack/utils/watch_and_rebuild.py +1 -1
- {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/METADATA +3 -6
- {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/RECORD +89 -93
- agent_starter_pack-0.21.0.dist-info/entry_points.txt +2 -0
- llm.txt +4 -5
- agent_starter_pack/agents/crewai_coding_crew/README.md +0 -34
- agent_starter_pack/agents/crewai_coding_crew/app/agent.py +0 -47
- agent_starter_pack/agents/crewai_coding_crew/app/crew/config/tasks.yaml +0 -37
- agent_starter_pack/agents/crewai_coding_crew/app/crew/crew.py +0 -71
- agent_starter_pack/agents/crewai_coding_crew/tests/integration/test_agent.py +0 -47
- agent_starter_pack/agents/langgraph_base_react/README.md +0 -9
- agent_starter_pack/agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +0 -1574
- agent_starter_pack/base_template/deployment/terraform/dev/log_sinks.tf +0 -69
- agent_starter_pack/base_template/deployment/terraform/log_sinks.tf +0 -79
- agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/utils/tracing.py +0 -155
- agent_starter_pack/cli/utils/register_gemini_enterprise.py +0 -406
- agent_starter_pack/deployment_targets/agent_engine/deployment/terraform/{% if not cookiecutter.is_adk_live %}service.tf{% else %}unused_service.tf{% endif %} +0 -82
- agent_starter_pack/deployment_targets/agent_engine/notebooks/intro_agent_engine.ipynb +0 -1025
- agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +0 -99
- agent_starter_pack/frontends/streamlit/frontend/side_bar.py +0 -214
- agent_starter_pack/frontends/streamlit/frontend/streamlit_app.py +0 -265
- agent_starter_pack/frontends/streamlit/frontend/utils/chat_utils.py +0 -67
- agent_starter_pack/frontends/streamlit/frontend/utils/local_chat_history.py +0 -127
- agent_starter_pack/frontends/streamlit/frontend/utils/message_editing.py +0 -59
- agent_starter_pack/frontends/streamlit/frontend/utils/multimodal_utils.py +0 -217
- agent_starter_pack/frontends/streamlit/frontend/utils/stream_handler.py +0 -310
- agent_starter_pack/frontends/streamlit/frontend/utils/title_summary.py +0 -94
- agent_starter_pack/resources/locks/uv-crewai_coding_crew-agent_engine.lock +0 -6650
- agent_starter_pack/resources/locks/uv-crewai_coding_crew-cloud_run.lock +0 -7825
- agent_starter_pack-0.18.2.dist-info/entry_points.txt +0 -3
- /agent_starter_pack/agents/{crewai_coding_crew → langgraph_base}/notebooks/evaluating_langgraph_agent.ipynb +0 -0
- /agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/{utils → app_utils}/gcs.py +0 -0
- {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -47,7 +47,7 @@ def start_server() -> subprocess.Popen[str]:
|
|
|
47
47
|
sys.executable,
|
|
48
48
|
"-m",
|
|
49
49
|
"uvicorn",
|
|
50
|
-
"{{cookiecutter.agent_directory}}.
|
|
50
|
+
"{{cookiecutter.agent_directory}}.fast_api_app:app",
|
|
51
51
|
"--host",
|
|
52
52
|
"0.0.0.0",
|
|
53
53
|
"--port",
|
|
@@ -202,8 +202,8 @@ def test_feedback_endpoint(server_fixture: subprocess.Popen[str]) -> None:
|
|
|
202
202
|
feedback_data = {
|
|
203
203
|
"score": 5,
|
|
204
204
|
"text": "Great response!",
|
|
205
|
-
"
|
|
206
|
-
"
|
|
205
|
+
"user_id": "test-user-123",
|
|
206
|
+
"session_id": "test-session-123",
|
|
207
207
|
"log_type": "feedback",
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -220,12 +220,28 @@ import subprocess
|
|
|
220
220
|
import sys
|
|
221
221
|
import threading
|
|
222
222
|
import time
|
|
223
|
+
{%- if cookiecutter.is_a2a %}
|
|
223
224
|
import uuid
|
|
225
|
+
{%- endif %}
|
|
224
226
|
from collections.abc import Iterator
|
|
225
227
|
from typing import Any
|
|
226
228
|
|
|
227
229
|
import pytest
|
|
228
230
|
import requests
|
|
231
|
+
{%- if cookiecutter.is_a2a %}
|
|
232
|
+
from a2a.types import (
|
|
233
|
+
JSONRPCErrorResponse,
|
|
234
|
+
Message,
|
|
235
|
+
MessageSendParams,
|
|
236
|
+
Part,
|
|
237
|
+
Role,
|
|
238
|
+
SendMessageRequest,
|
|
239
|
+
SendMessageResponse,
|
|
240
|
+
SendStreamingMessageRequest,
|
|
241
|
+
SendStreamingMessageResponse,
|
|
242
|
+
TextPart,
|
|
243
|
+
)
|
|
244
|
+
{%- endif %}
|
|
229
245
|
from requests.exceptions import RequestException
|
|
230
246
|
|
|
231
247
|
# Configure logging
|
|
@@ -233,7 +249,10 @@ logging.basicConfig(level=logging.INFO)
|
|
|
233
249
|
logger = logging.getLogger(__name__)
|
|
234
250
|
|
|
235
251
|
BASE_URL = "http://127.0.0.1:8000/"
|
|
236
|
-
{%- if cookiecutter.
|
|
252
|
+
{%- if cookiecutter.is_a2a %}
|
|
253
|
+
A2A_RPC_URL = BASE_URL + "a2a/{{cookiecutter.agent_directory}}/"
|
|
254
|
+
AGENT_CARD_URL = A2A_RPC_URL + ".well-known/agent-card.json"
|
|
255
|
+
{%- elif cookiecutter.is_adk %}
|
|
237
256
|
STREAM_URL = BASE_URL + "run_sse"
|
|
238
257
|
{%- else %}
|
|
239
258
|
STREAM_URL = BASE_URL + "stream_messages"
|
|
@@ -255,7 +274,7 @@ def start_server() -> subprocess.Popen[str]:
|
|
|
255
274
|
sys.executable,
|
|
256
275
|
"-m",
|
|
257
276
|
"uvicorn",
|
|
258
|
-
"{{cookiecutter.agent_directory}}.
|
|
277
|
+
"{{cookiecutter.agent_directory}}.fast_api_app:app",
|
|
259
278
|
"--host",
|
|
260
279
|
"0.0.0.0",
|
|
261
280
|
"--port",
|
|
@@ -264,8 +283,8 @@ def start_server() -> subprocess.Popen[str]:
|
|
|
264
283
|
env = os.environ.copy()
|
|
265
284
|
env["INTEGRATION_TEST"] = "TRUE"
|
|
266
285
|
{%- if cookiecutter.session_type == "agent_engine" %}
|
|
267
|
-
#
|
|
268
|
-
env["
|
|
286
|
+
# Use in-memory session for local E2E tests instead of creating Agent Engine
|
|
287
|
+
env["USE_IN_MEMORY_SESSION"] = "true"
|
|
269
288
|
{%- endif %}
|
|
270
289
|
process = subprocess.Popen(
|
|
271
290
|
command,
|
|
@@ -292,7 +311,11 @@ def wait_for_server(timeout: int = 90, interval: int = 1) -> bool:
|
|
|
292
311
|
start_time = time.time()
|
|
293
312
|
while time.time() - start_time < timeout:
|
|
294
313
|
try:
|
|
314
|
+
{%- if cookiecutter.is_a2a %}
|
|
315
|
+
response = requests.get(AGENT_CARD_URL, timeout=10)
|
|
316
|
+
{%- else %}
|
|
295
317
|
response = requests.get("http://127.0.0.1:8000/docs", timeout=10)
|
|
318
|
+
{%- endif %}
|
|
296
319
|
if response.status_code == 200:
|
|
297
320
|
logger.info("Server is ready")
|
|
298
321
|
return True
|
|
@@ -323,6 +346,82 @@ def server_fixture(request: Any) -> Iterator[subprocess.Popen[str]]:
|
|
|
323
346
|
|
|
324
347
|
|
|
325
348
|
def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
|
|
349
|
+
{%- if cookiecutter.is_a2a %}
|
|
350
|
+
"""Test the chat stream functionality using A2A JSON-RPC protocol."""
|
|
351
|
+
logger.info("Starting chat stream test")
|
|
352
|
+
|
|
353
|
+
message = Message(
|
|
354
|
+
message_id=f"msg-user-{uuid.uuid4()}",
|
|
355
|
+
role=Role.user,
|
|
356
|
+
parts=[Part(root=TextPart(text="What's the weather in San Francisco?"))],
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
request = SendStreamingMessageRequest(
|
|
360
|
+
id="test-req-001",
|
|
361
|
+
params=MessageSendParams(message=message),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Send the request
|
|
365
|
+
response = requests.post(
|
|
366
|
+
A2A_RPC_URL,
|
|
367
|
+
headers=HEADERS,
|
|
368
|
+
json=request.model_dump(mode="json", exclude_none=True),
|
|
369
|
+
stream=True,
|
|
370
|
+
timeout=60,
|
|
371
|
+
)
|
|
372
|
+
assert response.status_code == 200
|
|
373
|
+
|
|
374
|
+
# Parse streaming JSON-RPC responses
|
|
375
|
+
responses: list[SendStreamingMessageResponse] = []
|
|
376
|
+
|
|
377
|
+
for line in response.iter_lines():
|
|
378
|
+
if line:
|
|
379
|
+
line_str = line.decode("utf-8")
|
|
380
|
+
if line_str.startswith("data: "):
|
|
381
|
+
event_json = line_str[6:]
|
|
382
|
+
json_data = json.loads(event_json)
|
|
383
|
+
streaming_response = SendStreamingMessageResponse.model_validate(
|
|
384
|
+
json_data
|
|
385
|
+
)
|
|
386
|
+
responses.append(streaming_response)
|
|
387
|
+
|
|
388
|
+
assert responses, "No responses received from stream"
|
|
389
|
+
|
|
390
|
+
# Check for final status update
|
|
391
|
+
final_responses = [
|
|
392
|
+
r.root
|
|
393
|
+
for r in responses
|
|
394
|
+
if hasattr(r.root, "result")
|
|
395
|
+
and hasattr(r.root.result, "final")
|
|
396
|
+
and r.root.result.final is True
|
|
397
|
+
]
|
|
398
|
+
assert final_responses, "No final response received"
|
|
399
|
+
|
|
400
|
+
final_response = final_responses[-1]
|
|
401
|
+
assert final_response.result.kind == "status-update"
|
|
402
|
+
assert hasattr(final_response.result, "status")
|
|
403
|
+
assert final_response.result.status.state == "completed"
|
|
404
|
+
|
|
405
|
+
# Check for artifact content
|
|
406
|
+
artifact_responses = [
|
|
407
|
+
r.root
|
|
408
|
+
for r in responses
|
|
409
|
+
if hasattr(r.root, "result") and r.root.result.kind == "artifact-update"
|
|
410
|
+
]
|
|
411
|
+
assert artifact_responses, "No artifact content received in stream"
|
|
412
|
+
|
|
413
|
+
# Verify text content is in the artifact
|
|
414
|
+
artifact_response = artifact_responses[-1]
|
|
415
|
+
assert hasattr(artifact_response.result, "artifact")
|
|
416
|
+
artifact = artifact_response.result.artifact
|
|
417
|
+
assert artifact.parts, "Artifact has no parts"
|
|
418
|
+
|
|
419
|
+
has_text = any(
|
|
420
|
+
part.root.kind == "text" and hasattr(part.root, "text") and part.root.text
|
|
421
|
+
for part in artifact.parts
|
|
422
|
+
)
|
|
423
|
+
assert has_text, "No text content found in artifact"
|
|
424
|
+
{%- else %}
|
|
326
425
|
"""Test the chat stream functionality."""
|
|
327
426
|
logger.info("Starting chat stream test")
|
|
328
427
|
{% if cookiecutter.is_adk %}
|
|
@@ -417,6 +516,94 @@ def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
|
|
|
417
516
|
break
|
|
418
517
|
assert has_content, "At least one message should have content"
|
|
419
518
|
{%- endif %}
|
|
519
|
+
{%- endif %}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
{%- if cookiecutter.is_a2a %}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def test_chat_non_streaming(server_fixture: subprocess.Popen[str]) -> None:
|
|
526
|
+
"""Test the non-streaming chat functionality using A2A JSON-RPC protocol."""
|
|
527
|
+
logger.info("Starting non-streaming chat test")
|
|
528
|
+
|
|
529
|
+
message = Message(
|
|
530
|
+
message_id=f"msg-user-{uuid.uuid4()}",
|
|
531
|
+
role=Role.user,
|
|
532
|
+
parts=[Part(root=TextPart(text="What's the weather in San Francisco?"))],
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
request = SendMessageRequest(
|
|
536
|
+
id="test-req-002",
|
|
537
|
+
params=MessageSendParams(message=message),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
response = requests.post(
|
|
541
|
+
A2A_RPC_URL,
|
|
542
|
+
headers=HEADERS,
|
|
543
|
+
json=request.model_dump(mode="json", exclude_none=True),
|
|
544
|
+
timeout=60,
|
|
545
|
+
)
|
|
546
|
+
assert response.status_code == 200
|
|
547
|
+
|
|
548
|
+
# Parse the single JSON-RPC response
|
|
549
|
+
response_data = response.json()
|
|
550
|
+
message_response = SendMessageResponse.model_validate(response_data)
|
|
551
|
+
logger.info(f"Received response: {message_response}")
|
|
552
|
+
|
|
553
|
+
# For non-streaming, the result is a Task object
|
|
554
|
+
json_rpc_resp = message_response.root
|
|
555
|
+
assert hasattr(json_rpc_resp, "result")
|
|
556
|
+
task = json_rpc_resp.result
|
|
557
|
+
assert task.kind == "task"
|
|
558
|
+
assert hasattr(task, "status")
|
|
559
|
+
assert task.status.state == "completed"
|
|
560
|
+
|
|
561
|
+
# Check that we got artifacts (the final agent output)
|
|
562
|
+
assert hasattr(task, "artifacts")
|
|
563
|
+
assert task.artifacts, "No artifacts in task"
|
|
564
|
+
|
|
565
|
+
# Verify we got text content in the artifact
|
|
566
|
+
artifact = task.artifacts[0]
|
|
567
|
+
assert artifact.parts, "Artifact has no parts"
|
|
568
|
+
|
|
569
|
+
has_text = any(
|
|
570
|
+
part.root.kind == "text" and hasattr(part.root, "text") and part.root.text
|
|
571
|
+
for part in artifact.parts
|
|
572
|
+
)
|
|
573
|
+
assert has_text, "No text content found in artifact"
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def test_chat_stream_error_handling(server_fixture: subprocess.Popen[str]) -> None:
|
|
577
|
+
"""Test the chat stream error handling with invalid A2A request."""
|
|
578
|
+
logger.info("Starting chat stream error handling test")
|
|
579
|
+
|
|
580
|
+
invalid_data = {
|
|
581
|
+
"jsonrpc": "2.0",
|
|
582
|
+
"id": "test-error-001",
|
|
583
|
+
"method": "message/send",
|
|
584
|
+
"params": {
|
|
585
|
+
"message": {
|
|
586
|
+
"role": "user",
|
|
587
|
+
# Missing required 'parts' field
|
|
588
|
+
"messageId": f"msg-user-{uuid.uuid4()}",
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
response = requests.post(
|
|
594
|
+
A2A_RPC_URL, headers=HEADERS, json=invalid_data, timeout=10
|
|
595
|
+
)
|
|
596
|
+
assert response.status_code == 200
|
|
597
|
+
|
|
598
|
+
response_data = response.json()
|
|
599
|
+
error_response = JSONRPCErrorResponse.model_validate(response_data)
|
|
600
|
+
assert "error" in response_data, "Expected JSON-RPC error in response"
|
|
601
|
+
|
|
602
|
+
# Assert error for invalid parameters
|
|
603
|
+
assert error_response.error.code == -32602
|
|
604
|
+
|
|
605
|
+
logger.info("Error handling test completed successfully")
|
|
606
|
+
{%- else %}
|
|
420
607
|
|
|
421
608
|
|
|
422
609
|
def test_chat_stream_error_handling(server_fixture: subprocess.Popen[str]) -> None:
|
|
@@ -433,6 +620,7 @@ def test_chat_stream_error_handling(server_fixture: subprocess.Popen[str]) -> No
|
|
|
433
620
|
f"Expected status code 422, got {response.status_code}"
|
|
434
621
|
)
|
|
435
622
|
logger.info("Error handling test completed successfully")
|
|
623
|
+
{%- endif %}
|
|
436
624
|
|
|
437
625
|
|
|
438
626
|
def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
|
|
@@ -443,11 +631,8 @@ def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
|
|
|
443
631
|
# Create sample feedback data
|
|
444
632
|
feedback_data = {
|
|
445
633
|
"score": 4,
|
|
446
|
-
|
|
447
|
-
"
|
|
448
|
-
{%- else %}
|
|
449
|
-
"run_id": str(uuid.uuid4()),
|
|
450
|
-
{%- endif %}
|
|
634
|
+
"user_id": "test-user-456",
|
|
635
|
+
"session_id": "test-session-456",
|
|
451
636
|
"text": "Great response!",
|
|
452
637
|
}
|
|
453
638
|
|
|
@@ -455,6 +640,37 @@ def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
|
|
|
455
640
|
FEEDBACK_URL, json=feedback_data, headers=HEADERS, timeout=10
|
|
456
641
|
)
|
|
457
642
|
assert response.status_code == 200
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
{%- if cookiecutter.is_a2a %}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def test_a2a_agent_json_generation(server_fixture: subprocess.Popen[str]) -> None:
|
|
649
|
+
"""
|
|
650
|
+
Test that the agent.json file is automatically generated and served correctly
|
|
651
|
+
via the well-known URI.
|
|
652
|
+
"""
|
|
653
|
+
# Verify the A2A endpoint serves the agent card
|
|
654
|
+
response = requests.get(AGENT_CARD_URL, timeout=10)
|
|
655
|
+
assert response.status_code == 200, f"A2A endpoint returned {response.status_code}"
|
|
656
|
+
|
|
657
|
+
# Validate required fields in served agent card
|
|
658
|
+
served_agent_card = response.json()
|
|
659
|
+
required_fields = [
|
|
660
|
+
"name",
|
|
661
|
+
"description",
|
|
662
|
+
"skills",
|
|
663
|
+
"capabilities",
|
|
664
|
+
"url",
|
|
665
|
+
"version",
|
|
666
|
+
]
|
|
667
|
+
for field in required_fields:
|
|
668
|
+
assert field in served_agent_card, (
|
|
669
|
+
f"Missing required field in served agent card: {field}"
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
{%- endif %}
|
|
458
674
|
{%- if cookiecutter.session_type == "agent_engine" %}
|
|
459
675
|
|
|
460
676
|
|
|
@@ -12,7 +12,7 @@ Follow these steps to execute load tests on your local machine:
|
|
|
12
12
|
Launch the FastAPI server in a separate terminal:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
uv run uvicorn {{cookiecutter.agent_directory}}.
|
|
15
|
+
uv run uvicorn {{cookiecutter.agent_directory}}.fast_api_app:app --host 0.0.0.0 --port 8000 --reload
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
**2. (In another tab) Create virtual environment with Locust**
|
|
@@ -92,14 +92,14 @@ Follow these steps to execute load tests on your local machine:
|
|
|
92
92
|
Launch the FastAPI server in a separate terminal:
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
uv run uvicorn {{cookiecutter.agent_directory}}.
|
|
95
|
+
uv run uvicorn {{cookiecutter.agent_directory}}.fast_api_app:app --host 0.0.0.0 --port 8000 --reload
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
**2. (In another tab) Create virtual environment with Locust**
|
|
99
99
|
Using another terminal tab, This is suggested to avoid conflicts with the existing application python environment.
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
|
-
python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1
|
|
102
|
+
python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1{%- if cookiecutter.is_a2a %} a2a-sdk~=0.3.9{%- endif %}
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
**3. Execute the Load Test:**
|
|
@@ -150,7 +150,7 @@ export _ID_TOKEN=$(gcloud auth print-identity-token -q)
|
|
|
150
150
|
**3. Execute the Load Test:**
|
|
151
151
|
Create virtual environment with Locust:
|
|
152
152
|
```bash
|
|
153
|
-
python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1
|
|
153
|
+
python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1{%- if cookiecutter.is_a2a %} a2a-sdk~=0.3.9{%- endif %}
|
|
154
154
|
```
|
|
155
155
|
|
|
156
156
|
Execute load tests. The following command executes the same load test parameters as the local test but targets your remote Cloud Run instance.
|
|
@@ -138,9 +138,23 @@ class RemoteAgentUser(WebSocketUser):
|
|
|
138
138
|
host = "http://localhost:8000" # Default for local testing
|
|
139
139
|
{%- else %}
|
|
140
140
|
|
|
141
|
+
import json
|
|
142
|
+
import logging
|
|
141
143
|
import os
|
|
142
144
|
import time
|
|
143
|
-
{%- if cookiecutter.
|
|
145
|
+
{%- if cookiecutter.is_a2a %}
|
|
146
|
+
import uuid
|
|
147
|
+
|
|
148
|
+
from a2a.types import (
|
|
149
|
+
Message,
|
|
150
|
+
MessageSendParams,
|
|
151
|
+
Part,
|
|
152
|
+
Role,
|
|
153
|
+
SendStreamingMessageRequest,
|
|
154
|
+
TextPart,
|
|
155
|
+
)
|
|
156
|
+
from locust import HttpUser, between, task
|
|
157
|
+
{%- elif cookiecutter.is_adk %}
|
|
144
158
|
import uuid
|
|
145
159
|
|
|
146
160
|
import requests
|
|
@@ -149,11 +163,23 @@ from locust import HttpUser, between, task
|
|
|
149
163
|
|
|
150
164
|
from locust import HttpUser, between, task
|
|
151
165
|
{%- endif %}
|
|
152
|
-
{
|
|
166
|
+
{%- if cookiecutter.is_a2a %}
|
|
167
|
+
|
|
168
|
+
ENDPOINT = "/a2a/{{cookiecutter.agent_directory}}"
|
|
169
|
+
{%- elif cookiecutter.is_adk %}
|
|
170
|
+
|
|
153
171
|
ENDPOINT = "/run_sse"
|
|
154
|
-
{
|
|
172
|
+
{%- else %}
|
|
173
|
+
|
|
155
174
|
ENDPOINT = "/stream_messages"
|
|
156
|
-
{
|
|
175
|
+
{%- endif %}
|
|
176
|
+
|
|
177
|
+
# Configure logging
|
|
178
|
+
logging.basicConfig(
|
|
179
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
180
|
+
)
|
|
181
|
+
logger = logging.getLogger(__name__)
|
|
182
|
+
|
|
157
183
|
|
|
158
184
|
class ChatStreamUser(HttpUser):
|
|
159
185
|
"""Simulates a user interacting with the chat stream API."""
|
|
@@ -162,6 +188,34 @@ class ChatStreamUser(HttpUser):
|
|
|
162
188
|
|
|
163
189
|
@task
|
|
164
190
|
def chat_stream(self) -> None:
|
|
191
|
+
{%- if cookiecutter.is_a2a %}
|
|
192
|
+
"""Simulates a chat stream interaction using A2A protocol."""
|
|
193
|
+
headers = {"Content-Type": "application/json"}
|
|
194
|
+
if os.environ.get("_ID_TOKEN"):
|
|
195
|
+
headers["Authorization"] = f"Bearer {os.environ['_ID_TOKEN']}"
|
|
196
|
+
|
|
197
|
+
message = Message(
|
|
198
|
+
message_id=f"msg-user-{uuid.uuid4()}",
|
|
199
|
+
role=Role.user,
|
|
200
|
+
parts=[Part(root=TextPart(text="Hello! What's the weather in New York?"))],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
request = SendStreamingMessageRequest(
|
|
204
|
+
id=f"req-{uuid.uuid4()}",
|
|
205
|
+
params=MessageSendParams(message=message),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
start_time = time.time()
|
|
209
|
+
|
|
210
|
+
with self.client.post(
|
|
211
|
+
ENDPOINT,
|
|
212
|
+
name=f"{ENDPOINT} message",
|
|
213
|
+
headers=headers,
|
|
214
|
+
json=request.model_dump(mode="json", exclude_none=True),
|
|
215
|
+
catch_response=True,
|
|
216
|
+
stream=True,
|
|
217
|
+
) as response:
|
|
218
|
+
{%- else %}
|
|
165
219
|
"""Simulates a chat stream interaction."""
|
|
166
220
|
headers = {"Content-Type": "application/json"}
|
|
167
221
|
if os.environ.get("_ID_TOKEN"):
|
|
@@ -218,8 +272,10 @@ class ChatStreamUser(HttpUser):
|
|
|
218
272
|
stream=True,
|
|
219
273
|
params={"alt": "sse"},
|
|
220
274
|
) as response:
|
|
275
|
+
{%- endif %}
|
|
221
276
|
if response.status_code == 200:
|
|
222
277
|
events = []
|
|
278
|
+
has_error = False
|
|
223
279
|
for line in response.iter_lines():
|
|
224
280
|
if line:
|
|
225
281
|
line_str = line.decode("utf-8")
|
|
@@ -234,16 +290,40 @@ class ChatStreamUser(HttpUser):
|
|
|
234
290
|
response=response,
|
|
235
291
|
context={},
|
|
236
292
|
)
|
|
293
|
+
|
|
294
|
+
# Check for error responses in the JSON payload
|
|
295
|
+
try:
|
|
296
|
+
event_data = json.loads(line_str)
|
|
297
|
+
if isinstance(event_data, dict) and "code" in event_data:
|
|
298
|
+
# Flag any non-2xx codes as errors
|
|
299
|
+
if event_data["code"] >= 400:
|
|
300
|
+
has_error = True
|
|
301
|
+
error_msg = event_data.get(
|
|
302
|
+
"message", "Unknown error"
|
|
303
|
+
)
|
|
304
|
+
response.failure(f"Error in response: {error_msg}")
|
|
305
|
+
logger.error(
|
|
306
|
+
"Received error response: code=%s, message=%s",
|
|
307
|
+
event_data["code"],
|
|
308
|
+
error_msg,
|
|
309
|
+
)
|
|
310
|
+
except json.JSONDecodeError:
|
|
311
|
+
# If it's not valid JSON, continue processing
|
|
312
|
+
pass
|
|
313
|
+
|
|
237
314
|
end_time = time.time()
|
|
238
315
|
total_time = end_time - start_time
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
316
|
+
|
|
317
|
+
# Only fire success event if no errors were found
|
|
318
|
+
if not has_error:
|
|
319
|
+
self.environment.events.request.fire(
|
|
320
|
+
request_type="POST",
|
|
321
|
+
name=f"{ENDPOINT} end",
|
|
322
|
+
response_time=total_time * 1000, # Convert to milliseconds
|
|
323
|
+
response_length=len(events),
|
|
324
|
+
response=response,
|
|
325
|
+
context={},
|
|
326
|
+
)
|
|
247
327
|
else:
|
|
248
328
|
response.failure(f"Unexpected status code: {response.status_code}")
|
|
249
329
|
{%- endif %}
|