Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ WORKDIR /app
RUN pip install uv
COPY pyproject.toml uv.lock ./
RUN uv sync --no-cache
RUN uv pip install validators

COPY backend/ ./backend/
RUN mkdir -p /app/frontend
COPY --from=frontend-builder /app/public /app/frontend/public
Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from urllib.parse import urlparse, urlunparse
from uuid import uuid4

import agent_validators
import bleach
import httpx
import socketio
import validators

from a2a.client import A2ACardResolver
from a2a.client.client import Client, ClientConfig, ClientEvent
Expand Down Expand Up @@ -114,7 +114,7 @@ async def _process_a2a_response(
response_data = event.model_dump(exclude_none=True)
response_data['id'] = response_id

validation_errors = validators.validate_message(response_data)
validation_errors = agent_validators.validate_message(response_data)
response_data['validation_errors'] = validation_errors

await _emit_debug_log(sid, response_id, 'response', response_data)
Expand Down Expand Up @@ -200,7 +200,7 @@ async def get_agent_card(request: Request) -> JSONResponse:
card = await card_resolver.get_agent_card()

card_data = card.model_dump(exclude_none=True)
validation_errors = validators.validate_agent_card(card_data)
validation_errors = agent_validators.validate_agent_card(card_data)
response_data = {
'card': card_data,
'validation_errors': validation_errors,
Expand Down
48 changes: 24 additions & 24 deletions backend/tests/test_validators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from backend import validators
from backend import agent_validators


# ==============================================================================
Expand Down Expand Up @@ -31,7 +31,7 @@ def valid_card_data():
class TestValidateAgentCard:
def test_valid_card(self, valid_card_data):
"""A valid agent card should produce no validation errors."""
errors = validators.validate_agent_card(valid_card_data)
errors = agent_validators.validate_agent_card(valid_card_data)
assert not errors

@pytest.mark.parametrize(
Expand All @@ -51,7 +51,7 @@ def test_missing_required_field(self, valid_card_data, missing_field):
"""A missing required field should be detected."""
card_data = valid_card_data.copy()
del card_data[missing_field]
errors = validators.validate_agent_card(card_data)
errors = agent_validators.validate_agent_card(card_data)
assert f"Required field is missing: '{missing_field}'." in errors

@pytest.mark.parametrize(
Expand All @@ -62,7 +62,7 @@ def test_invalid_url(self, valid_card_data, invalid_url):
"""An invalid URL format should be detected."""
card_data = valid_card_data.copy()
card_data['url'] = invalid_url
errors = validators.validate_agent_card(card_data)
errors = agent_validators.validate_agent_card(card_data)
assert (
"Field 'url' must be an absolute URL starting with http:// or https://."
in errors
Expand All @@ -72,7 +72,7 @@ def test_invalid_capabilities_type(self, valid_card_data):
"""The 'capabilities' field must be an object."""
card_data = valid_card_data.copy()
card_data['capabilities'] = 'not-an-object'
errors = validators.validate_agent_card(card_data)
errors = agent_validators.validate_agent_card(card_data)
assert "Field 'capabilities' must be an object." in errors

@pytest.mark.parametrize(
Expand All @@ -82,7 +82,7 @@ def test_invalid_modes_type_not_array(self, valid_card_data, field):
"""Input/Output modes fields must be arrays."""
card_data = valid_card_data.copy()
card_data[field] = 'not-a-list'
errors = validators.validate_agent_card(card_data)
errors = agent_validators.validate_agent_card(card_data)
assert f"Field '{field}' must be an array of strings." in errors

@pytest.mark.parametrize(
Expand All @@ -92,14 +92,14 @@ def test_invalid_modes_type_item_not_string(self, valid_card_data, field):
"""Input/Output modes arrays must contain only strings."""
card_data = valid_card_data.copy()
card_data[field] = [123, 'string']
errors = validators.validate_agent_card(card_data)
errors = agent_validators.validate_agent_card(card_data)
assert f"All items in '{field}' must be strings." in errors

def test_invalid_skills_type(self, valid_card_data):
"""The 'skills' field must be an array."""
card_data = valid_card_data.copy()
card_data['skills'] = 'not-a-list'
errors = validators.validate_agent_card(card_data)
errors = agent_validators.validate_agent_card(card_data)
assert (
"Field 'skills' must be an array of AgentSkill objects." in errors
)
Expand All @@ -108,7 +108,7 @@ def test_empty_skills_array(self, valid_card_data):
"""An empty 'skills' array should produce a warning."""
card_data = valid_card_data.copy()
card_data['skills'] = []
errors = validators.validate_agent_card(card_data)
errors = agent_validators.validate_agent_card(card_data)
assert (
"Field 'skills' array is empty. Agent must have at least one skill if it performs actions."
in errors
Expand All @@ -123,50 +123,50 @@ def test_empty_skills_array(self, valid_card_data):
class TestValidateMessage:
def test_missing_kind(self):
"""A message missing the 'kind' field should be detected."""
errors = validators.validate_message({})
errors = agent_validators.validate_message({})
assert "Response from agent is missing required 'kind' field." in errors

def test_unknown_kind(self):
"""An unknown message kind should be detected."""
errors = validators.validate_message({'kind': 'unknown-kind'})
errors = agent_validators.validate_message({'kind': 'unknown-kind'})
assert "Unknown message kind received: 'unknown-kind'." in errors

# Tests for 'task' kind
def test_valid_task(self):
"""A valid task message should produce no errors."""
data = {'kind': 'task', 'id': '123', 'status': {'state': 'running'}}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert not errors

def test_task_missing_id(self):
"""A task message missing 'id' should produce an error."""
data = {'kind': 'task', 'status': {'state': 'running'}}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert "Task object missing required field: 'id'." in errors

def test_task_missing_status(self):
"""A task message missing 'status' should produce an error."""
data = {'kind': 'task', 'id': '123'}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert "Task object missing required field: 'status.state'." in errors

def test_task_missing_status_state(self):
"""A task message missing 'status.state' should produce an error."""
data = {'kind': 'task', 'id': '123', 'status': {}}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert "Task object missing required field: 'status.state'." in errors

# Tests for 'status-update' kind
def test_valid_status_update(self):
"""A valid status-update message should produce no errors."""
data = {'kind': 'status-update', 'status': {'state': 'thinking'}}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert not errors

def test_status_update_missing_status(self):
"""A status-update missing 'status' should produce an error."""
data = {'kind': 'status-update'}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert (
"StatusUpdate object missing required field: 'status.state'."
in errors
Expand All @@ -175,7 +175,7 @@ def test_status_update_missing_status(self):
def test_status_update_missing_state(self):
"""A status-update missing 'status.state' should produce an error."""
data = {'kind': 'status-update', 'status': {}}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert (
"StatusUpdate object missing required field: 'status.state'."
in errors
Expand All @@ -188,13 +188,13 @@ def test_valid_artifact_update(self):
'kind': 'artifact-update',
'artifact': {'parts': [{'text': 'result'}]},
}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert not errors

def test_artifact_update_missing_artifact(self):
"""An artifact-update missing 'artifact' should produce an error."""
data = {'kind': 'artifact-update'}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert (
"ArtifactUpdate object missing required field: 'artifact'."
in errors
Expand All @@ -210,7 +210,7 @@ def test_artifact_update_invalid_parts(self, parts_value):
data = {'kind': 'artifact-update', 'artifact': {}}
if parts_value is not None:
data['artifact']['parts'] = parts_value
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert "Artifact object must have a non-empty 'parts' array." in errors

# Tests for 'message' kind
Expand All @@ -221,7 +221,7 @@ def test_valid_message(self):
'parts': [{'text': 'hello'}],
'role': 'agent',
}
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert not errors

@pytest.mark.parametrize(
Expand All @@ -234,7 +234,7 @@ def test_message_invalid_parts(self, parts_value):
data = {'kind': 'message', 'role': 'agent'}
if parts_value is not None:
data['parts'] = parts_value
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert "Message object must have a non-empty 'parts' array." in errors

@pytest.mark.parametrize(
Expand All @@ -247,5 +247,5 @@ def test_message_invalid_role(self, role_value):
data = {'kind': 'message', 'parts': [{'text': 'hello'}]}
if role_value is not None:
data['role'] = role_value
errors = validators.validate_message(data)
errors = agent_validators.validate_message(data)
assert "Message from agent must have 'role' set to 'agent'." in errors
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies = [
"python-socketio",
"jinja2",
"bleach>=6.2.0",
"validators>=0.35.0",
]

[tool.setuptools]
Expand Down
19 changes: 18 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading