asap-protocol 0.1.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.
asap/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """ASAP: Async Simple Agent Protocol.
2
+
3
+ A streamlined, scalable, asynchronous protocol for agent-to-agent communication
4
+ and task coordination.
5
+ """
6
+
7
+ __version__ = "0.1.0"
asap/cli.py ADDED
@@ -0,0 +1,220 @@
1
+ """Command-line interface for ASAP Protocol utilities.
2
+
3
+ This module provides CLI commands for schema export, inspection, and validation.
4
+
5
+ Example:
6
+ >>> # From terminal:
7
+ >>> # asap --version
8
+ >>> # asap export-schemas --output-dir ./schemas
9
+ >>> # asap list-schemas
10
+ >>> # asap show-schema agent
11
+ >>> # asap validate-schema message.json --schema-type envelope
12
+ """
13
+
14
+ import json
15
+ from pathlib import Path
16
+ from typing import Annotated, Optional
17
+
18
+ import typer
19
+ from pydantic import ValidationError
20
+
21
+ from asap import __version__
22
+ from asap.schemas import SCHEMA_REGISTRY, export_all_schemas, get_schema_json, list_schema_entries
23
+
24
+ app = typer.Typer(help="ASAP Protocol CLI.")
25
+
26
+ # Default directory for schema operations
27
+ DEFAULT_SCHEMAS_DIR = Path("schemas")
28
+
29
+ # Global verbose flag
30
+ _verbose: bool = False
31
+
32
+
33
+ def _version_callback(value: bool) -> None:
34
+ """Print the version and exit when requested."""
35
+ if value:
36
+ typer.echo(__version__)
37
+ raise typer.Exit()
38
+
39
+
40
+ VERSION_OPTION = typer.Option(
41
+ False,
42
+ "--version",
43
+ help="Show ASAP Protocol version and exit.",
44
+ callback=_version_callback,
45
+ is_eager=True,
46
+ )
47
+
48
+ # Module-level singleton options to avoid B008 linting errors
49
+ OUTPUT_DIR_EXPORT_OPTION = typer.Option(
50
+ DEFAULT_SCHEMAS_DIR,
51
+ "--output-dir",
52
+ help="Directory where JSON schemas will be written.",
53
+ )
54
+ OUTPUT_DIR_LIST_OPTION = typer.Option(
55
+ DEFAULT_SCHEMAS_DIR,
56
+ "--output-dir",
57
+ help="Directory where JSON schemas are written.",
58
+ )
59
+
60
+
61
+ @app.callback()
62
+ def cli(
63
+ version: bool = VERSION_OPTION,
64
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output."),
65
+ ) -> None:
66
+ """ASAP Protocol CLI entrypoint."""
67
+ global _verbose
68
+ _verbose = verbose
69
+
70
+
71
+ @app.command("export-schemas")
72
+ def export_schemas(
73
+ output_dir: Path = OUTPUT_DIR_EXPORT_OPTION,
74
+ ) -> None:
75
+ """Export all ASAP JSON schemas to the output directory."""
76
+ try:
77
+ written_paths = export_all_schemas(output_dir)
78
+ typer.echo(f"Exported {len(written_paths)} schemas to {output_dir}")
79
+ if _verbose:
80
+ for path in sorted(written_paths):
81
+ typer.echo(f" - {path.relative_to(output_dir)}")
82
+ except PermissionError as exc:
83
+ raise typer.BadParameter(f"Cannot write to directory: {output_dir}") from exc
84
+ except OSError as exc:
85
+ raise typer.BadParameter(f"Failed to export schemas: {exc}") from exc
86
+
87
+
88
+ @app.command("list-schemas")
89
+ def list_schemas(
90
+ output_dir: Path = OUTPUT_DIR_LIST_OPTION,
91
+ ) -> None:
92
+ """List available schema names and output paths."""
93
+ entries = list_schema_entries(output_dir)
94
+ for name, path in entries:
95
+ typer.echo(f"{name}\t{path.relative_to(output_dir)}")
96
+
97
+
98
+ @app.command("show-schema")
99
+ def show_schema(schema_name: str) -> None:
100
+ """Print the JSON schema for a named model."""
101
+ try:
102
+ schema = get_schema_json(schema_name)
103
+ except ValueError as exc:
104
+ raise typer.BadParameter(str(exc)) from exc
105
+
106
+ typer.echo(json.dumps(schema, indent=2))
107
+
108
+
109
+ def _detect_schema_type(data: dict[str, object]) -> str | None:
110
+ """Attempt to auto-detect schema type from JSON data.
111
+
112
+ Auto-detects envelope type if payload_type field is present.
113
+
114
+ Args:
115
+ data: Parsed JSON data.
116
+
117
+ Returns:
118
+ Detected schema type name or None if not detectable.
119
+ """
120
+ # Envelope detection: has payload_type field
121
+ if "payload_type" in data and "asap_version" in data:
122
+ return "envelope"
123
+ return None
124
+
125
+
126
+ def _validate_against_schema(data: dict[str, object], schema_type: str) -> list[str]:
127
+ """Validate JSON data against a registered schema.
128
+
129
+ Args:
130
+ data: Parsed JSON data to validate.
131
+ schema_type: Schema type name from SCHEMA_REGISTRY.
132
+
133
+ Returns:
134
+ List of validation error messages (empty if valid).
135
+
136
+ Raises:
137
+ ValueError: If schema_type is not registered.
138
+ """
139
+ if schema_type not in SCHEMA_REGISTRY:
140
+ raise ValueError(f"Unknown schema type: {schema_type}")
141
+
142
+ model_class = SCHEMA_REGISTRY[schema_type]
143
+ try:
144
+ model_class.model_validate(data)
145
+ return []
146
+ except ValidationError as exc:
147
+ errors: list[str] = []
148
+ for error in exc.errors():
149
+ loc = ".".join(str(part) for part in error["loc"])
150
+ msg = error["msg"]
151
+ errors.append(f" - {loc}: {msg}")
152
+ return errors
153
+
154
+
155
+ @app.command("validate-schema")
156
+ def validate_schema(
157
+ file: Annotated[Path, typer.Argument(help="Path to JSON file to validate.")],
158
+ schema_type: Annotated[
159
+ Optional[str],
160
+ typer.Option(
161
+ "--schema-type",
162
+ help="Schema type to validate against (e.g., agent, envelope, task_request).",
163
+ ),
164
+ ] = None,
165
+ ) -> None:
166
+ """Validate a JSON file against an ASAP schema.
167
+
168
+ The schema type can be auto-detected for envelope files (those containing
169
+ payload_type and asap_version fields). For other schema types, use the
170
+ --schema-type option.
171
+
172
+ Available schema types: agent, manifest, conversation, task, message,
173
+ artifact, state_snapshot, text_part, data_part, file_part, resource_part,
174
+ template_part, task_request, task_response, task_update, task_cancel,
175
+ message_send, state_query, state_restore, artifact_notify, mcp_tool_call,
176
+ mcp_tool_result, mcp_resource_fetch, mcp_resource_data, envelope.
177
+ """
178
+ # Check file exists
179
+ if not file.exists():
180
+ raise typer.BadParameter(f"File not found: {file}")
181
+
182
+ # Parse JSON
183
+ try:
184
+ content = file.read_text(encoding="utf-8")
185
+ data = json.loads(content)
186
+ except json.JSONDecodeError as exc:
187
+ raise typer.BadParameter(f"Invalid JSON: {exc}") from exc
188
+
189
+ if not isinstance(data, dict):
190
+ raise typer.BadParameter("JSON root must be an object")
191
+
192
+ # Determine schema type
193
+ effective_schema_type = schema_type
194
+ if effective_schema_type is None:
195
+ effective_schema_type = _detect_schema_type(data)
196
+ if effective_schema_type is None:
197
+ raise typer.BadParameter(
198
+ "Cannot auto-detect schema type. Use --schema-type to specify."
199
+ )
200
+
201
+ # Validate
202
+ try:
203
+ errors = _validate_against_schema(data, effective_schema_type)
204
+ except ValueError as exc:
205
+ raise typer.BadParameter(str(exc)) from exc
206
+
207
+ if errors:
208
+ error_details = "\n".join(errors)
209
+ raise typer.BadParameter(f"Validation error:\n{error_details}")
210
+
211
+ typer.echo(f"Valid {effective_schema_type} schema: {file}")
212
+
213
+
214
+ def main() -> None:
215
+ """Run the ASAP Protocol CLI."""
216
+ app()
217
+
218
+
219
+ if __name__ == "__main__":
220
+ main()
asap/errors.py ADDED
@@ -0,0 +1,150 @@
1
+ """ASAP Protocol Error Taxonomy.
2
+
3
+ This module defines the error hierarchy for the ASAP protocol,
4
+ providing structured error handling with specific error codes
5
+ and context information.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+
11
+ class ASAPError(Exception):
12
+ """Base exception for all ASAP protocol errors.
13
+
14
+ This is the root exception class that all ASAP-specific errors
15
+ should inherit from. It provides a standardized way to handle
16
+ protocol-level errors with error codes and additional context.
17
+
18
+ Attributes:
19
+ code: Error code following the asap:error/... pattern
20
+ message: Human-readable error message
21
+ details: Optional additional error context
22
+ """
23
+
24
+ def __init__(self, code: str, message: str, details: dict[str, Any] | None = None) -> None:
25
+ """Initialize ASAP error.
26
+
27
+ Args:
28
+ code: Error code (e.g., 'asap:protocol/invalid_state')
29
+ message: Human-readable error description
30
+ details: Optional dictionary with additional error context
31
+ """
32
+ super().__init__(message)
33
+ self.code = code
34
+ self.message = message
35
+ self.details = details or {}
36
+
37
+ def to_dict(self) -> dict[str, Any]:
38
+ """Convert error to dictionary for JSON serialization.
39
+
40
+ Returns:
41
+ Dictionary containing code, message, and details
42
+ """
43
+ return {
44
+ "code": self.code,
45
+ "message": self.message,
46
+ "details": self.details,
47
+ }
48
+
49
+
50
+ class InvalidTransitionError(ASAPError):
51
+ """Raised when attempting an invalid task state transition.
52
+
53
+ This error occurs when trying to change a task from one status
54
+ to another status that is not allowed by the state machine rules.
55
+
56
+ Attributes:
57
+ from_state: The current task status
58
+ to_state: The attempted target status
59
+ """
60
+
61
+ def __init__(
62
+ self, from_state: str, to_state: str, details: dict[str, Any] | None = None
63
+ ) -> None:
64
+ """Initialize invalid transition error.
65
+
66
+ Args:
67
+ from_state: Current task status
68
+ to_state: Attempted target status
69
+ details: Optional additional context
70
+ """
71
+ message = f"Invalid transition from '{from_state}' to '{to_state}'"
72
+ super().__init__(
73
+ code="asap:protocol/invalid_state",
74
+ message=message,
75
+ details={"from_state": from_state, "to_state": to_state, **(details or {})},
76
+ )
77
+ self.from_state = from_state
78
+ self.to_state = to_state
79
+
80
+
81
+ class MalformedEnvelopeError(ASAPError):
82
+ """Raised when receiving a malformed or invalid envelope.
83
+
84
+ This error occurs when the envelope structure is invalid,
85
+ missing required fields, or contains malformed data that
86
+ cannot be processed by the protocol.
87
+ """
88
+
89
+ def __init__(self, reason: str, details: dict[str, Any] | None = None) -> None:
90
+ """Initialize malformed envelope error.
91
+
92
+ Args:
93
+ reason: Description of what's malformed
94
+ details: Optional additional context (e.g., validation errors)
95
+ """
96
+ message = f"Malformed envelope: {reason}"
97
+ super().__init__(
98
+ code="asap:protocol/malformed_envelope", message=message, details=details or {}
99
+ )
100
+ self.reason = reason
101
+
102
+
103
+ class TaskNotFoundError(ASAPError):
104
+ """Raised when a requested task cannot be found.
105
+
106
+ This error occurs when attempting to access or modify a task
107
+ that doesn't exist in the system.
108
+ """
109
+
110
+ def __init__(self, task_id: str, details: dict[str, Any] | None = None) -> None:
111
+ """Initialize task not found error.
112
+
113
+ Args:
114
+ task_id: The ID of the task that was not found
115
+ details: Optional additional context
116
+ """
117
+ message = f"Task not found: {task_id}"
118
+ super().__init__(
119
+ code="asap:task/not_found",
120
+ message=message,
121
+ details={"task_id": task_id, **(details or {})},
122
+ )
123
+ self.task_id = task_id
124
+
125
+
126
+ class TaskAlreadyCompletedError(ASAPError):
127
+ """Raised when attempting to modify a task that is already completed.
128
+
129
+ This error occurs when trying to perform operations on a task
130
+ that has reached a terminal state and cannot be modified further.
131
+ """
132
+
133
+ def __init__(
134
+ self, task_id: str, current_status: str, details: dict[str, Any] | None = None
135
+ ) -> None:
136
+ """Initialize task already completed error.
137
+
138
+ Args:
139
+ task_id: The ID of the completed task
140
+ current_status: The current terminal status
141
+ details: Optional additional context
142
+ """
143
+ message = f"Task already completed: {task_id} (status: {current_status})"
144
+ super().__init__(
145
+ code="asap:task/already_completed",
146
+ message=message,
147
+ details={"task_id": task_id, "current_status": current_status, **(details or {})},
148
+ )
149
+ self.task_id = task_id
150
+ self.current_status = current_status
@@ -0,0 +1,25 @@
1
+ ## Overview
2
+
3
+ The examples demonstrate a minimal end-to-end flow between two agents:
4
+ an echo agent and a coordinator agent.
5
+
6
+ ## Running the demo
7
+
8
+ Run the demo runner module from the repository root:
9
+
10
+ - `uv run python -m asap.examples.run_demo`
11
+
12
+ This starts the echo agent on port 8001 and the coordinator agent on port 8000.
13
+ The coordinator sends a TaskRequest to the echo agent and logs the response.
14
+
15
+ ## Running agents individually
16
+
17
+ You can run the agents separately if needed:
18
+
19
+ - `uv run python -m asap.examples.echo_agent --host 127.0.0.1 --port 8001`
20
+ - `uv run python -m asap.examples.coordinator`
21
+
22
+ ## Notes
23
+
24
+ - The echo agent exposes `/.well-known/asap/manifest.json` for readiness checks.
25
+ - Update ports in `asap.examples.run_demo` if you change the defaults.
@@ -0,0 +1 @@
1
+ """Example agents and demo runner for ASAP protocol."""
@@ -0,0 +1,184 @@
1
+ """Coordinator agent example for ASAP protocol.
2
+
3
+ This module defines a coordinator agent with a manifest and FastAPI app.
4
+ The coordinator will dispatch tasks to other agents in later steps.
5
+ """
6
+
7
+ import argparse
8
+ import asyncio
9
+ from typing import Any, Sequence
10
+
11
+ from fastapi import FastAPI
12
+
13
+ from asap.models.entities import Capability, Endpoint, Manifest, Skill
14
+ from asap.models.envelope import Envelope
15
+ from asap.models.ids import generate_id
16
+ from asap.models.payloads import TaskRequest
17
+ from asap.observability import get_logger
18
+ from asap.observability.logging import bind_context, clear_context
19
+ from asap.transport.client import ASAPClient
20
+ from asap.transport.server import create_app
21
+
22
+ DEFAULT_AGENT_ID = "urn:asap:agent:coordinator"
23
+ DEFAULT_AGENT_NAME = "Coordinator Agent"
24
+ DEFAULT_AGENT_VERSION = "0.1.0"
25
+ DEFAULT_AGENT_DESCRIPTION = "Coordinates tasks across agents"
26
+ DEFAULT_ASAP_ENDPOINT = "http://localhost:8000/asap"
27
+ DEFAULT_ECHO_AGENT_ID = "urn:asap:agent:echo-agent"
28
+ DEFAULT_ECHO_BASE_URL = "http://127.0.0.1:8001"
29
+
30
+ logger = get_logger(__name__)
31
+
32
+
33
+ def build_manifest(asap_endpoint: str = DEFAULT_ASAP_ENDPOINT) -> Manifest:
34
+ """Build the manifest for the coordinator agent.
35
+
36
+ Args:
37
+ asap_endpoint: URL where the agent receives ASAP messages.
38
+
39
+ Returns:
40
+ Manifest describing the coordinator agent's capabilities and endpoints.
41
+ """
42
+ return Manifest(
43
+ id=DEFAULT_AGENT_ID,
44
+ name=DEFAULT_AGENT_NAME,
45
+ version=DEFAULT_AGENT_VERSION,
46
+ description=DEFAULT_AGENT_DESCRIPTION,
47
+ capabilities=Capability(
48
+ asap_version="0.1",
49
+ skills=[Skill(id="coordinate", description="Dispatch tasks to agents")],
50
+ state_persistence=False,
51
+ ),
52
+ endpoints=Endpoint(asap=asap_endpoint),
53
+ )
54
+
55
+
56
+ def create_coordinator_app(asap_endpoint: str = DEFAULT_ASAP_ENDPOINT) -> FastAPI:
57
+ """Create the FastAPI app for the coordinator agent.
58
+
59
+ Args:
60
+ asap_endpoint: URL where the agent receives ASAP messages.
61
+
62
+ Returns:
63
+ Configured FastAPI app.
64
+ """
65
+ manifest = build_manifest(asap_endpoint)
66
+ return create_app(manifest)
67
+
68
+
69
+ app = create_coordinator_app()
70
+
71
+
72
+ def build_task_request(payload: dict[str, Any]) -> TaskRequest:
73
+ """Build a TaskRequest payload for the echo agent.
74
+
75
+ Args:
76
+ payload: Input data to echo.
77
+
78
+ Returns:
79
+ TaskRequest payload ready for dispatch.
80
+ """
81
+ return TaskRequest(
82
+ conversation_id=generate_id(),
83
+ skill_id="echo",
84
+ input=payload,
85
+ )
86
+
87
+
88
+ def build_task_envelope(payload: dict[str, Any]) -> Envelope:
89
+ """Build a TaskRequest envelope targeting the echo agent.
90
+
91
+ Args:
92
+ payload: Input data to echo.
93
+
94
+ Returns:
95
+ TaskRequest envelope for transmission.
96
+ """
97
+ task_request = build_task_request(payload)
98
+ return Envelope(
99
+ asap_version="0.1",
100
+ sender=DEFAULT_AGENT_ID,
101
+ recipient=DEFAULT_ECHO_AGENT_ID,
102
+ payload_type="task.request",
103
+ payload=task_request.model_dump(),
104
+ trace_id=generate_id(),
105
+ )
106
+
107
+
108
+ async def dispatch_task(
109
+ payload: dict[str, Any],
110
+ echo_base_url: str = DEFAULT_ECHO_BASE_URL,
111
+ ) -> Envelope:
112
+ """Dispatch a task to the echo agent using ASAPClient.
113
+
114
+ Args:
115
+ payload: Input data to echo.
116
+ echo_base_url: Base URL for the echo agent (no trailing /asap).
117
+
118
+ Returns:
119
+ Response envelope from the echo agent.
120
+ """
121
+ envelope = build_task_envelope(payload)
122
+ bind_context(trace_id=envelope.trace_id, correlation_id=envelope.id)
123
+ try:
124
+ logger.info(
125
+ "asap.coordinator.request_sent",
126
+ request_id=envelope.id,
127
+ payload_type=envelope.payload_type,
128
+ payload=envelope.payload,
129
+ )
130
+ finally:
131
+ clear_context()
132
+ async with ASAPClient(echo_base_url) as client:
133
+ response = await client.send(envelope)
134
+
135
+ bind_context(
136
+ trace_id=envelope.trace_id,
137
+ correlation_id=response.correlation_id,
138
+ )
139
+ try:
140
+ logger.info(
141
+ "asap.coordinator.response_received",
142
+ request_id=envelope.id,
143
+ response_id=response.id,
144
+ payload_type=response.payload_type,
145
+ result=response.payload,
146
+ )
147
+ finally:
148
+ clear_context()
149
+ return response
150
+
151
+
152
+ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
153
+ """Parse command-line arguments for the coordinator agent.
154
+
155
+ Args:
156
+ argv: Optional list of CLI arguments for testing.
157
+
158
+ Returns:
159
+ Parsed argparse namespace.
160
+ """
161
+ parser = argparse.ArgumentParser(description="Run the ASAP coordinator agent.")
162
+ parser.add_argument(
163
+ "--echo-url",
164
+ default=DEFAULT_ECHO_BASE_URL,
165
+ help="Base URL for the echo agent (no trailing /asap).",
166
+ )
167
+ parser.add_argument(
168
+ "--message",
169
+ default="hello from coordinator",
170
+ help="Message to send to the echo agent.",
171
+ )
172
+ return parser.parse_args(argv)
173
+
174
+
175
+ def main(argv: Sequence[str] | None = None) -> None:
176
+ """Run a sample dispatch to the echo agent."""
177
+ args = parse_args(argv)
178
+ payload: dict[str, Any] = {"message": args.message}
179
+ response = asyncio.run(dispatch_task(payload, echo_base_url=args.echo_url))
180
+ logger.info("asap.coordinator.demo_complete", response=response.payload)
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()
@@ -0,0 +1,100 @@
1
+ """Echo agent example for ASAP protocol.
2
+
3
+ This module defines a minimal echo agent with a manifest and FastAPI app.
4
+ It uses the default handler registry to echo task input as output.
5
+ """
6
+
7
+ import argparse
8
+ from typing import Sequence
9
+
10
+ from fastapi import FastAPI
11
+ import uvicorn
12
+
13
+ from asap.models.entities import Capability, Endpoint, Manifest, Skill
14
+ from asap.transport.handlers import HandlerRegistry, create_echo_handler
15
+ from asap.transport.server import create_app
16
+
17
+ DEFAULT_AGENT_ID = "urn:asap:agent:echo-agent"
18
+ DEFAULT_AGENT_NAME = "Echo Agent"
19
+ DEFAULT_AGENT_VERSION = "0.1.0"
20
+ DEFAULT_AGENT_DESCRIPTION = "Echoes task input as output"
21
+ DEFAULT_ASAP_HOST = "127.0.0.1"
22
+ DEFAULT_ASAP_PORT = 8001
23
+ DEFAULT_ASAP_ENDPOINT = f"http://{DEFAULT_ASAP_HOST}:{DEFAULT_ASAP_PORT}/asap"
24
+
25
+
26
+ def build_manifest(asap_endpoint: str = DEFAULT_ASAP_ENDPOINT) -> Manifest:
27
+ """Build the manifest for the echo agent.
28
+
29
+ Args:
30
+ asap_endpoint: URL where the agent receives ASAP messages.
31
+
32
+ Returns:
33
+ Manifest describing the echo agent's capabilities and endpoints.
34
+ """
35
+ return Manifest(
36
+ id=DEFAULT_AGENT_ID,
37
+ name=DEFAULT_AGENT_NAME,
38
+ version=DEFAULT_AGENT_VERSION,
39
+ description=DEFAULT_AGENT_DESCRIPTION,
40
+ capabilities=Capability(
41
+ asap_version="0.1",
42
+ skills=[Skill(id="echo", description="Echo back the input")],
43
+ state_persistence=False,
44
+ ),
45
+ endpoints=Endpoint(asap=asap_endpoint),
46
+ )
47
+
48
+
49
+ def create_echo_app(asap_endpoint: str = DEFAULT_ASAP_ENDPOINT) -> FastAPI:
50
+ """Create the FastAPI app for the echo agent.
51
+
52
+ Args:
53
+ asap_endpoint: URL where the agent receives ASAP messages.
54
+
55
+ Returns:
56
+ Configured FastAPI app.
57
+ """
58
+ manifest = build_manifest(asap_endpoint)
59
+ registry = HandlerRegistry()
60
+ registry.register("task.request", create_echo_handler())
61
+ return create_app(manifest, registry)
62
+
63
+
64
+ app = create_echo_app()
65
+
66
+
67
+ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
68
+ """Parse command-line arguments for the echo agent.
69
+
70
+ Args:
71
+ argv: Optional list of CLI arguments for testing.
72
+
73
+ Returns:
74
+ Parsed argparse namespace.
75
+ """
76
+ parser = argparse.ArgumentParser(description="Run the ASAP echo agent.")
77
+ parser.add_argument(
78
+ "--host",
79
+ default=DEFAULT_ASAP_HOST,
80
+ help="Host to bind the echo agent server.",
81
+ )
82
+ parser.add_argument(
83
+ "--port",
84
+ type=int,
85
+ default=DEFAULT_ASAP_PORT,
86
+ help="Port to bind the echo agent server.",
87
+ )
88
+ return parser.parse_args(argv)
89
+
90
+
91
+ def main(argv: Sequence[str] | None = None) -> None:
92
+ """Run the echo agent with configurable host and port."""
93
+ args = parse_args(argv)
94
+ endpoint = f"http://{args.host}:{args.port}/asap"
95
+ agent_app = create_echo_app(endpoint)
96
+ uvicorn.run(agent_app, host=args.host, port=args.port)
97
+
98
+
99
+ if __name__ == "__main__":
100
+ main()