231 lines
9.6 KiB
Python
231 lines
9.6 KiB
Python
"""A2A Protocol Types - Agent2Agent v1.0 data structures."""
|
|
|
|
from __future__ import annotations
|
|
import uuid
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from enum import Enum
|
|
from typing import Any, Dict, List, Optional, Union
|
|
|
|
class TaskState(str, Enum):
|
|
SUBMITTED = "TASK_STATE_SUBMITTED"
|
|
WORKING = "TASK_STATE_WORKING"
|
|
INPUT_REQUIRED = "TASK_STATE_INPUT_REQUIRED"
|
|
COMPLETED = "TASK_STATE_COMPLETED"
|
|
FAILED = "TASK_STATE_FAILED"
|
|
CANCELED = "TASK_STATE_CANCELED"
|
|
REJECTED = "TASK_STATE_REJECTED"
|
|
@property
|
|
def terminal(self) -> bool:
|
|
return self in {TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELED, TaskState.REJECTED}
|
|
|
|
@dataclass
|
|
class TextPart:
|
|
text: str
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
def to_dict(self) -> dict:
|
|
d = {"text": self.text}
|
|
if self.metadata: d["metadata"] = self.metadata
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d): return cls(text=d["text"], metadata=d.get("metadata"))
|
|
|
|
@dataclass
|
|
class FilePart:
|
|
media_type: str = "application/octet-stream"
|
|
raw: Optional[str] = None
|
|
url: Optional[str] = None
|
|
filename: Optional[str] = None
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
def to_dict(self) -> dict:
|
|
d = {"mediaType": self.media_type}
|
|
if self.raw is not None: d["raw"] = self.raw
|
|
if self.url is not None: d["url"] = self.url
|
|
if self.filename: d["filename"] = self.filename
|
|
if self.metadata: d["metadata"] = self.metadata
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(media_type=d.get("mediaType","application/octet-stream"), raw=d.get("raw"), url=d.get("url"), filename=d.get("filename"), metadata=d.get("metadata"))
|
|
|
|
@dataclass
|
|
class DataPart:
|
|
data: Dict[str, Any]
|
|
media_type: str = "application/json"
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
def to_dict(self) -> dict:
|
|
d = {"data": self.data, "mediaType": self.media_type}
|
|
if self.metadata: d["metadata"] = self.metadata
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d): return cls(data=d["data"], media_type=d.get("mediaType","application/json"), metadata=d.get("metadata"))
|
|
|
|
Part = Union[TextPart, FilePart, DataPart]
|
|
|
|
def part_from_dict(d):
|
|
if "text" in d: return TextPart.from_dict(d)
|
|
if "raw" in d or "url" in d: return FilePart.from_dict(d)
|
|
if "data" in d: return DataPart.from_dict(d)
|
|
raise ValueError(f"Cannot discriminate Part type from keys: {list(d.keys())}")
|
|
|
|
@dataclass
|
|
class Message:
|
|
role: str
|
|
parts: List[Part]
|
|
message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
context_id: Optional[str] = None
|
|
task_id: Optional[str] = None
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
def to_dict(self):
|
|
d = {"role": self.role, "messageId": self.message_id, "parts": [p.to_dict() for p in self.parts]}
|
|
if self.context_id: d["contextId"] = self.context_id
|
|
if self.task_id: d["taskId"] = self.task_id
|
|
if self.metadata: d["metadata"] = self.metadata
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(role=d["role"], parts=[part_from_dict(p) for p in d["parts"]], message_id=d.get("messageId",str(uuid.uuid4())), context_id=d.get("contextId"), task_id=d.get("taskId"), metadata=d.get("metadata"))
|
|
|
|
@dataclass
|
|
class Artifact:
|
|
artifact_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
parts: List[Part] = field(default_factory=list)
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
def to_dict(self):
|
|
d = {"artifactId": self.artifact_id, "parts": [p.to_dict() for p in self.parts]}
|
|
if self.name: d["name"] = self.name
|
|
if self.description: d["description"] = self.description
|
|
if self.metadata: d["metadata"] = self.metadata
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(artifact_id=d.get("artifactId",str(uuid.uuid4())), parts=[part_from_dict(p) for p in d.get("parts",[])], name=d.get("name"), description=d.get("description"), metadata=d.get("metadata"))
|
|
|
|
@dataclass
|
|
class TaskStatus:
|
|
state: TaskState
|
|
message: Optional[Message] = None
|
|
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
def to_dict(self):
|
|
d = {"state": self.state.value, "timestamp": self.timestamp}
|
|
if self.message: d["message"] = self.message.to_dict()
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
msg = d.get("message")
|
|
return cls(state=TaskState(d["state"]), message=Message.from_dict(msg) if msg else None, timestamp=d.get("timestamp",datetime.now(timezone.utc).isoformat()))
|
|
|
|
@dataclass
|
|
class Task:
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
context_id: Optional[str] = None
|
|
status: TaskStatus = field(default_factory=lambda: TaskStatus(state=TaskState.SUBMITTED))
|
|
artifacts: List[Artifact] = field(default_factory=list)
|
|
history: List[Message] = field(default_factory=list)
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
def to_dict(self):
|
|
d = {"id": self.id, "status": self.status.to_dict()}
|
|
if self.context_id: d["contextId"] = self.context_id
|
|
if self.artifacts: d["artifacts"] = [a.to_dict() for a in self.artifacts]
|
|
if self.history: d["history"] = [m.to_dict() for m in self.history]
|
|
if self.metadata: d["metadata"] = self.metadata
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(id=d.get("id",str(uuid.uuid4())), context_id=d.get("contextId"), status=TaskStatus.from_dict(d["status"]) if "status" in d else TaskStatus(TaskState.SUBMITTED), artifacts=[Artifact.from_dict(a) for a in d.get("artifacts",[])], history=[Message.from_dict(m) for m in d.get("history",[])], metadata=d.get("metadata"))
|
|
|
|
@dataclass
|
|
class AgentSkill:
|
|
id: str
|
|
name: str
|
|
description: str = ""
|
|
tags: List[str] = field(default_factory=list)
|
|
examples: List[str] = field(default_factory=list)
|
|
input_modes: List[str] = field(default_factory=lambda: ["text"])
|
|
output_modes: List[str] = field(default_factory=lambda: ["text"])
|
|
def to_dict(self):
|
|
d = {"id": self.id, "name": self.name, "description": self.description}
|
|
if self.tags: d["tags"] = self.tags
|
|
if self.examples: d["examples"] = self.examples
|
|
if self.input_modes != ["text"]: d["inputModes"] = self.input_modes
|
|
if self.output_modes != ["text"]: d["outputModes"] = self.output_modes
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(id=d["id"], name=d.get("name",d["id"]), description=d.get("description",""), tags=d.get("tags",[]), examples=d.get("examples",[]), input_modes=d.get("inputModes",["text"]), output_modes=d.get("outputModes",["text"]))
|
|
|
|
@dataclass
|
|
class AgentCard:
|
|
name: str
|
|
description: str = ""
|
|
version: str = "1.0.0"
|
|
url: str = ""
|
|
skills: List[AgentSkill] = field(default_factory=list)
|
|
capabilities: Dict[str, bool] = field(default_factory=dict)
|
|
provider: Optional[Dict[str, str]] = None
|
|
def to_dict(self):
|
|
d = {"name": self.name, "description": self.description, "version": self.version, "url": self.url, "skills": [s.to_dict() for s in self.skills]}
|
|
if self.capabilities: d["capabilities"] = self.capabilities
|
|
if self.provider: d["provider"] = self.provider
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(name=d["name"], description=d.get("description",""), version=d.get("version","1.0.0"), url=d.get("url",""), skills=[AgentSkill.from_dict(s) for s in d.get("skills",[])], capabilities=d.get("capabilities",{}), provider=d.get("provider"))
|
|
|
|
@dataclass
|
|
class JSONRPCError:
|
|
code: int
|
|
message: str
|
|
data: Optional[Any] = None
|
|
def to_dict(self):
|
|
d = {"code": self.code, "message": self.message}
|
|
if self.data is not None: d["data"] = self.data
|
|
return d
|
|
|
|
@dataclass
|
|
class JSONRPCRequest:
|
|
method: str
|
|
params: Optional[Dict[str, Any]] = None
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
jsonrpc: str = "2.0"
|
|
def to_dict(self):
|
|
d = {"jsonrpc": self.jsonrpc, "method": self.method, "id": self.id}
|
|
if self.params is not None: d["params"] = self.params
|
|
return d
|
|
|
|
@dataclass
|
|
class JSONRPCResponse:
|
|
id: str
|
|
result: Optional[Any] = None
|
|
error: Optional[JSONRPCError] = None
|
|
jsonrpc: str = "2.0"
|
|
def to_dict(self):
|
|
d = {"jsonrpc": self.jsonrpc, "id": self.id}
|
|
if self.error: d["error"] = self.error.to_dict()
|
|
else: d["result"] = self.result
|
|
return d
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
err = d.get("error")
|
|
return cls(id=d["id"], result=d.get("result"), error=JSONRPCError(err["code"],err["message"],err.get("data")) if err else None)
|
|
|
|
class A2AError:
|
|
@staticmethod
|
|
def parse_error(data=None): return JSONRPCError(-32700, "Parse error", data)
|
|
@staticmethod
|
|
def invalid_request(data=None): return JSONRPCError(-32600, "Invalid Request", data)
|
|
@staticmethod
|
|
def method_not_found(data=None): return JSONRPCError(-32601, "Method not found", data)
|
|
@staticmethod
|
|
def invalid_params(data=None): return JSONRPCError(-32602, "Invalid params", data)
|
|
@staticmethod
|
|
def internal_error(data=None): return JSONRPCError(-32603, "Internal error", data)
|
|
@staticmethod
|
|
def task_not_found(task_id): return JSONRPCError(-32001, f"Task not found: {task_id}")
|
|
@staticmethod
|
|
def task_not_cancelable(task_id): return JSONRPCError(-32002, f"Task not cancelable: {task_id}")
|
|
@staticmethod
|
|
def agent_not_found(name): return JSONRPCError(-32009, f"Agent not found: {name}")
|