Files
hermes-agent/a2a/types.py

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}")