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 +7 -0
- asap/cli.py +220 -0
- asap/errors.py +150 -0
- asap/examples/README.md +25 -0
- asap/examples/__init__.py +1 -0
- asap/examples/coordinator.py +184 -0
- asap/examples/echo_agent.py +100 -0
- asap/examples/run_demo.py +120 -0
- asap/models/__init__.py +146 -0
- asap/models/base.py +55 -0
- asap/models/constants.py +14 -0
- asap/models/entities.py +410 -0
- asap/models/enums.py +71 -0
- asap/models/envelope.py +94 -0
- asap/models/ids.py +55 -0
- asap/models/parts.py +207 -0
- asap/models/payloads.py +423 -0
- asap/models/types.py +39 -0
- asap/observability/__init__.py +43 -0
- asap/observability/logging.py +216 -0
- asap/observability/metrics.py +399 -0
- asap/schemas.py +203 -0
- asap/state/__init__.py +22 -0
- asap/state/machine.py +86 -0
- asap/state/snapshot.py +265 -0
- asap/transport/__init__.py +84 -0
- asap/transport/client.py +399 -0
- asap/transport/handlers.py +444 -0
- asap/transport/jsonrpc.py +190 -0
- asap/transport/middleware.py +359 -0
- asap/transport/server.py +739 -0
- asap_protocol-0.1.0.dist-info/METADATA +251 -0
- asap_protocol-0.1.0.dist-info/RECORD +36 -0
- asap_protocol-0.1.0.dist-info/WHEEL +4 -0
- asap_protocol-0.1.0.dist-info/entry_points.txt +2 -0
- asap_protocol-0.1.0.dist-info/licenses/LICENSE +190 -0
asap/__init__.py
ADDED
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
|
asap/examples/README.md
ADDED
|
@@ -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()
|