nexus-a2a 0.1.0__tar.gz

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.
Files changed (37) hide show
  1. nexus_a2a-0.1.0/.gitignore +29 -0
  2. nexus_a2a-0.1.0/.python-version +1 -0
  3. nexus_a2a-0.1.0/PKG-INFO +56 -0
  4. nexus_a2a-0.1.0/README.md +23 -0
  5. nexus_a2a-0.1.0/main.py +6 -0
  6. nexus_a2a-0.1.0/nexus_a2a/__init__.py +60 -0
  7. nexus_a2a-0.1.0/nexus_a2a/adapters/__init__.py +0 -0
  8. nexus_a2a-0.1.0/nexus_a2a/adapters/base.py +0 -0
  9. nexus_a2a-0.1.0/nexus_a2a/adapters/crewai.py +0 -0
  10. nexus_a2a-0.1.0/nexus_a2a/adapters/langgraph.py +0 -0
  11. nexus_a2a-0.1.0/nexus_a2a/core/__init__.py +0 -0
  12. nexus_a2a-0.1.0/nexus_a2a/core/orchestrator.py +0 -0
  13. nexus_a2a-0.1.0/nexus_a2a/core/registry.py +0 -0
  14. nexus_a2a-0.1.0/nexus_a2a/core/task_manager.py +0 -0
  15. nexus_a2a-0.1.0/nexus_a2a/decorators.py +210 -0
  16. nexus_a2a-0.1.0/nexus_a2a/models/__init__.py +0 -0
  17. nexus_a2a-0.1.0/nexus_a2a/models/agent.py +192 -0
  18. nexus_a2a-0.1.0/nexus_a2a/models/task.py +292 -0
  19. nexus_a2a-0.1.0/nexus_a2a/security/__init__.py +0 -0
  20. nexus_a2a-0.1.0/nexus_a2a/security/auth.py +0 -0
  21. nexus_a2a-0.1.0/nexus_a2a/security/rate_limiter.py +0 -0
  22. nexus_a2a-0.1.0/nexus_a2a/security/trust.py +0 -0
  23. nexus_a2a-0.1.0/nexus_a2a/security/validator.py +0 -0
  24. nexus_a2a-0.1.0/nexus_a2a/storage/__init__.py +0 -0
  25. nexus_a2a-0.1.0/nexus_a2a/storage/audit_logger.py +0 -0
  26. nexus_a2a-0.1.0/nexus_a2a/storage/metrics.py +0 -0
  27. nexus_a2a-0.1.0/nexus_a2a/storage/redis_store.py +0 -0
  28. nexus_a2a-0.1.0/nexus_a2a/storage/task_store.py +0 -0
  29. nexus_a2a-0.1.0/nexus_a2a/transport/__init__.py +0 -0
  30. nexus_a2a-0.1.0/nexus_a2a/transport/http_client.py +0 -0
  31. nexus_a2a-0.1.0/nexus_a2a/transport/sse.py +0 -0
  32. nexus_a2a-0.1.0/nexus_a2a/transport/webhook.py +0 -0
  33. nexus_a2a-0.1.0/pyproject.toml +90 -0
  34. nexus_a2a-0.1.0/tests/__init__.py +0 -0
  35. nexus_a2a-0.1.0/tests/test_decorator.py +210 -0
  36. nexus_a2a-0.1.0/tests/test_models.py +226 -0
  37. nexus_a2a-0.1.0/uv.lock +427 -0
@@ -0,0 +1,29 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ .Python
6
+
7
+ # Virtual environment (uv creates this)
8
+ .venv/
9
+
10
+ # Build artifacts
11
+ dist/
12
+ build/
13
+ *.egg-info/
14
+
15
+ # Type checking cache
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+
19
+ # Test coverage
20
+ .coverage
21
+ htmlcov/
22
+
23
+ # IDE
24
+ .vscode/settings.json
25
+ .idea/
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: nexus-a2a
3
+ Version: 0.1.0
4
+ Summary: A developer-friendly Python package for building AI agent-to-agent (A2A) communication with ease.
5
+ Project-URL: Homepage, https://github.com/yourusername/nexus-a2a
6
+ Project-URL: Documentation, https://github.com/yourusername/nexus-a2a#readme
7
+ Project-URL: Issues, https://github.com/yourusername/nexus-a2a/issues
8
+ Author-email: Your Name <you@example.com>
9
+ License: MIT
10
+ Keywords: a2a,agent,ai,llm,multi-agent,protocol
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.11
19
+ Provides-Extra: all
20
+ Requires-Dist: asyncpg>=0.29; extra == 'all'
21
+ Requires-Dist: redis>=5.0; extra == 'all'
22
+ Provides-Extra: dev
23
+ Requires-Dist: httpx>=0.27; extra == 'dev'
24
+ Requires-Dist: mypy>=1.10; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.4; extra == 'dev'
28
+ Provides-Extra: postgres
29
+ Requires-Dist: asyncpg>=0.29; extra == 'postgres'
30
+ Provides-Extra: redis
31
+ Requires-Dist: redis>=5.0; extra == 'redis'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # nexus-a2a
35
+
36
+ A developer-friendly Python package for building AI agent-to-agent (A2A) communication with ease.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install nexus-a2a
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ from nexus_a2a import agent, AgentNetwork
48
+
49
+ @agent(name="researcher", skills=["search"])
50
+ class ResearchAgent:
51
+ async def run(self, task):
52
+ return f"Result for: {task}"
53
+ ```
54
+
55
+ ## Status
56
+ 🚧 Active development — v0.1.0 alpha
@@ -0,0 +1,23 @@
1
+ # nexus-a2a
2
+
3
+ A developer-friendly Python package for building AI agent-to-agent (A2A) communication with ease.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install nexus-a2a
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from nexus_a2a import agent, AgentNetwork
15
+
16
+ @agent(name="researcher", skills=["search"])
17
+ class ResearchAgent:
18
+ async def run(self, task):
19
+ return f"Result for: {task}"
20
+ ```
21
+
22
+ ## Status
23
+ 🚧 Active development — v0.1.0 alpha
@@ -0,0 +1,6 @@
1
+ def main():
2
+ print("Hello from nexus-a2a!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,60 @@
1
+ """
2
+ nexus_a2a — Developer-friendly A2A multi-agent communication for Python.
3
+
4
+ Public API for Phase 1. Import everything you need from here:
5
+
6
+ from nexus_a2a import agent, get_card
7
+ from nexus_a2a import AgentCard, AgentSkill, AgentCapabilities
8
+ from nexus_a2a import Task, TaskState, Message, Artifact, Part
9
+ """
10
+
11
+ # ── Version ───────────────────────────────────────────────────────────────────
12
+ __version__ = "0.1.0"
13
+
14
+ # ── Decorator — the primary developer entry point ─────────────────────────────
15
+ from nexus_a2a.decorators import agent, get_card
16
+
17
+ # ── Agent models ──────────────────────────────────────────────────────────────
18
+ from nexus_a2a.models.agent import (
19
+ AgentAuthentication,
20
+ AgentCapabilities,
21
+ AgentCard,
22
+ AgentSkill,
23
+ AuthScheme,
24
+ InputMode,
25
+ OutputMode,
26
+ )
27
+
28
+ # ── Task models ───────────────────────────────────────────────────────────────
29
+ from nexus_a2a.models.task import (
30
+ Artifact,
31
+ Message,
32
+ MessageRole,
33
+ Part,
34
+ PartType,
35
+ Task,
36
+ TaskState,
37
+ )
38
+
39
+ # ── What gets exported when someone does: from nexus_a2a import * ─────────────
40
+ __all__ = [
41
+ # Decorator
42
+ "agent",
43
+ "get_card",
44
+ # Agent models
45
+ "AgentCard",
46
+ "AgentSkill",
47
+ "AgentCapabilities",
48
+ "AgentAuthentication",
49
+ "AuthScheme",
50
+ "InputMode",
51
+ "OutputMode",
52
+ # Task models
53
+ "Task",
54
+ "TaskState",
55
+ "Message",
56
+ "MessageRole",
57
+ "Part",
58
+ "PartType",
59
+ "Artifact",
60
+ ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,210 @@
1
+ """
2
+ nexus_a2a/decorators.py
3
+
4
+ The @agent decorator — the primary entry point for developers.
5
+ Wraps a class and automatically generates an AgentCard from its metadata.
6
+
7
+ Usage:
8
+ @agent(
9
+ name="ResearchAgent",
10
+ description="Searches the web and summarises results.",
11
+ skills=[{"id": "search", "name": "Web search", "description": "..."}],
12
+ url="http://localhost:8001",
13
+ )
14
+ class ResearchAgent:
15
+ async def run(self, task: Task) -> str:
16
+ return "result"
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import functools
22
+ import inspect
23
+ from collections.abc import Callable
24
+ from typing import Any, Callable, TypeVar, overload, cast
25
+
26
+ from nexus_a2a.models.agent import (
27
+ AgentAuthentication,
28
+ AgentCapabilities,
29
+ AgentCard,
30
+ AgentSkill,
31
+ AuthScheme,
32
+ )
33
+
34
+ # The decorated class can be any type
35
+ C = TypeVar("C", bound=type)
36
+ F = TypeVar("F", bound=Callable[..., Any])
37
+
38
+ # Attribute name we attach to the class so other layers can read the card
39
+ _AGENT_CARD_ATTR = "__nexus_agent_card__"
40
+
41
+
42
+ # ── Internal helpers ──────────────────────────────────────────────────────────
43
+
44
+ def _build_skill(raw: dict[str, Any] | AgentSkill) -> AgentSkill:
45
+ """Accept either an AgentSkill instance or a plain dict and return AgentSkill."""
46
+ if isinstance(raw, AgentSkill):
47
+ return raw
48
+ return AgentSkill(**raw)
49
+
50
+
51
+ def _has_async_run(cls: type) -> bool:
52
+ """Return True if the class has an async method named 'run'."""
53
+ method = getattr(cls, "run", None)
54
+ return method is not None and inspect.iscoroutinefunction(method)
55
+
56
+
57
+ # ── Public decorator ──────────────────────────────────────────────────────────
58
+
59
+ @overload
60
+ def agent(cls: C) -> C: ... # called as @agent (no parentheses)
61
+
62
+ @overload
63
+ def agent(
64
+ *,
65
+ name: str | None = None,
66
+ description: str | None = None,
67
+ version: str = "0.1.0",
68
+ url: str = "http://localhost:8000",
69
+ skills: list[dict[str, Any] | AgentSkill] | None = None,
70
+ streaming: bool = False,
71
+ push_notifications: bool = False,
72
+ auth_scheme: AuthScheme = AuthScheme.NONE,
73
+ ) -> Callable[[C], C]: ... # called as @agent(...) with arguments
74
+
75
+
76
+ def agent(
77
+ cls: C | None = None,
78
+ *,
79
+ name: str | None = None,
80
+ description: str | None = None,
81
+ version: str = "0.1.0",
82
+ url: str = "http://localhost:8000",
83
+ skills: list[dict[str, Any] | AgentSkill] | None = None,
84
+ streaming: bool = False,
85
+ push_notifications: bool = False,
86
+ auth_scheme: AuthScheme = AuthScheme.NONE,
87
+ ) -> C | Callable[[C], C]:
88
+ """
89
+ Class decorator that registers a class as an A2A-compatible agent
90
+ and automatically generates its AgentCard.
91
+
92
+ Can be used in two ways:
93
+
94
+ 1. With arguments (recommended):
95
+ @agent(
96
+ name="Summariser",
97
+ description="Summarises long documents.",
98
+ skills=[{"id": "summarise", "name": "Summarise", "description": "..."}],
99
+ url="http://localhost:8001",
100
+ )
101
+ class SummariserAgent:
102
+ async def run(self, task): ...
103
+
104
+ 2. Without arguments (uses class name and docstring as defaults):
105
+ @agent
106
+ class SummariserAgent:
107
+ \"\"\"Summarises long documents.\"\"\"
108
+ async def run(self, task): ...
109
+
110
+ After decoration, the class gains:
111
+ - SummariserAgent.__nexus_agent_card__ → AgentCard instance
112
+ - SummariserAgent.get_agent_card() → returns the AgentCard
113
+
114
+ Raises:
115
+ TypeError: If the class does not have an async `run` method.
116
+ ValueError: If name or description cannot be resolved.
117
+ """
118
+
119
+ def decorator(klass: C) -> C:
120
+ # ── Resolve metadata ──────────────────────────────────────────────────
121
+ resolved_name = name or klass.__name__
122
+ resolved_desc = (
123
+ description
124
+ or (inspect.getdoc(klass)) # use docstring if available
125
+ or f"A2A agent: {resolved_name}" # fallback
126
+ )
127
+
128
+ if not resolved_desc.strip():
129
+ raise ValueError(
130
+ f"@agent on '{resolved_name}': provide a description= "
131
+ "or add a docstring to the class."
132
+ )
133
+
134
+ # ── Validate the class has async run() ───────────────────────────────
135
+ if not _has_async_run(klass):
136
+ raise TypeError(
137
+ f"@agent on '{resolved_name}': the class must define "
138
+ "'async def run(self, task)' method."
139
+ )
140
+
141
+ # ── Build skills list ─────────────────────────────────────────────────
142
+ built_skills: list[AgentSkill] = [
143
+ _build_skill(s) for s in (skills or [])
144
+ ]
145
+
146
+ # ── Build AgentCard ───────────────────────────────────────────────────
147
+ card = AgentCard(
148
+ name=resolved_name,
149
+ description=resolved_desc,
150
+ version=version,
151
+ url=url, # type: ignore[arg-type] # Pydantic coerces str → HttpUrl
152
+ skills=built_skills,
153
+ capabilities=AgentCapabilities(
154
+ streaming=streaming,
155
+ push_notifications=push_notifications,
156
+ ),
157
+ authentication=AgentAuthentication(scheme=auth_scheme),
158
+ )
159
+
160
+ # ── Attach card to the class ──────────────────────────────────────────
161
+ setattr(klass, _AGENT_CARD_ATTR, card)
162
+
163
+ # ── Add helper class method ───────────────────────────────────────────
164
+ @classmethod # type: ignore[misc]
165
+ def get_agent_card(klass_inner: type) -> AgentCard:
166
+ """Return the AgentCard generated by @agent."""
167
+ return cast(AgentCard, getattr(klass_inner, _AGENT_CARD_ATTR))
168
+
169
+ klass.get_agent_card = get_agent_card # type: ignore[attr-defined]
170
+
171
+ # ── Preserve original class metadata ──────────────────────────────────
172
+ functools.update_wrapper(klass, klass, updated=[])
173
+
174
+ return klass
175
+
176
+ # Handle both @agent and @agent(...) call styles
177
+ if cls is not None:
178
+ # Called as @agent with no parentheses
179
+ return decorator(cls)
180
+
181
+ # Called as @agent(...) with arguments
182
+ return decorator
183
+
184
+
185
+ # ── Public helper ─────────────────────────────────────────────────────────────
186
+
187
+ def get_card(agent_cls: type) -> AgentCard:
188
+ """
189
+ Retrieve the AgentCard from a decorated agent class.
190
+
191
+ Args:
192
+ agent_cls: A class decorated with @agent.
193
+
194
+ Returns:
195
+ The AgentCard attached to the class.
196
+
197
+ Raises:
198
+ TypeError: If the class was not decorated with @agent.
199
+
200
+ Example:
201
+ card = get_card(ResearchAgent)
202
+ print(card.name)
203
+ """
204
+ card = getattr(agent_cls, _AGENT_CARD_ATTR, None)
205
+ if card is None:
206
+ raise TypeError(
207
+ f"'{agent_cls.__name__}' is not decorated with @agent. "
208
+ "Apply @agent before calling get_card()."
209
+ )
210
+ return cast(AgentCard, card)
File without changes
@@ -0,0 +1,192 @@
1
+ """
2
+ nexus_a2a/models/agent.py
3
+
4
+ Pydantic models that describe an AI agent's identity, capabilities,
5
+ and skills — mirroring the A2A protocol's AgentCard specification.
6
+
7
+ These models are the single source of truth for agent metadata
8
+ across the entire package.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from enum import Enum
14
+ from typing import Annotated, Any
15
+
16
+ from pydantic import BaseModel, Field, HttpUrl, field_validator
17
+
18
+ # ── Enums ─────────────────────────────────────────────────────────────────────
19
+
20
+ class AuthScheme(str, Enum):
21
+ """Supported authentication schemes for agent-to-agent calls."""
22
+ NONE = "none"
23
+ API_KEY = "api_key"
24
+ JWT = "jwt"
25
+ OAUTH2 = "oauth2"
26
+
27
+
28
+ class InputMode(str, Enum):
29
+ """MIME types an agent can accept as input."""
30
+ TEXT = "text/plain"
31
+ JSON = "application/json"
32
+ MULTIPART = "multipart/form-data"
33
+
34
+
35
+ class OutputMode(str, Enum):
36
+ """MIME types an agent can produce as output."""
37
+ TEXT = "text/plain"
38
+ JSON = "application/json"
39
+ MARKDOWN = "text/markdown"
40
+
41
+
42
+ # ── Sub-models ────────────────────────────────────────────────────────────────
43
+
44
+ class AgentAuthentication(BaseModel):
45
+ """
46
+ Describes how a client must authenticate when calling this agent.
47
+
48
+ Example:
49
+ AgentAuthentication(scheme=AuthScheme.JWT, token_url="https://auth.example.com/token")
50
+ """
51
+ scheme: AuthScheme = AuthScheme.NONE
52
+ token_url: HttpUrl | None = Field(
53
+ default=None,
54
+ description="URL to obtain a token — required for JWT and OAuth2 schemes.",
55
+ )
56
+ header_name: str | None = Field(
57
+ default=None,
58
+ description="Custom header name for API key auth. Defaults to 'X-API-Key'.",
59
+ )
60
+
61
+ model_config = {"use_enum_values": True}
62
+
63
+
64
+ class AgentCapabilities(BaseModel):
65
+ """
66
+ Flags that describe what protocol features this agent supports.
67
+ The client uses these to decide how to interact with the agent.
68
+ """
69
+ streaming: bool = Field(
70
+ default=False,
71
+ description="Agent supports Server-Sent Events for real-time task updates.",
72
+ )
73
+ push_notifications: bool = Field(
74
+ default=False,
75
+ description="Agent can POST task updates to a client-provided webhook URL.",
76
+ )
77
+ multi_turn: bool = Field(
78
+ default=True,
79
+ description="Agent supports back-and-forth conversations within a single task.",
80
+ )
81
+
82
+
83
+ class AgentSkill(BaseModel):
84
+ """
85
+ A single capability that an agent advertises.
86
+ Think of this as one entry in the agent's 'menu of services'.
87
+
88
+ Example:
89
+ AgentSkill(
90
+ id="web_search",
91
+ name="Web search",
92
+ description="Searches the web and returns summarised results.",
93
+ tags=["search", "web", "research"],
94
+ examples=["Search for latest AI papers", "Find Python docs for asyncio"],
95
+ )
96
+ """
97
+ id: Annotated[str, Field(min_length=1, max_length=64)]
98
+ name: Annotated[str, Field(min_length=1, max_length=128)]
99
+ description: Annotated[str, Field(min_length=1, max_length=1024)]
100
+ tags: list[str] = Field(
101
+ default_factory=list,
102
+ description="Keywords that help agents discover this skill.",
103
+ )
104
+ examples: list[str] = Field(
105
+ default_factory=list,
106
+ description="Sample inputs that demonstrate this skill.",
107
+ )
108
+
109
+ @field_validator("tags", "examples", mode="before")
110
+ @classmethod
111
+ def _no_empty_strings(cls, v: list[str]) -> list[str]:
112
+ """Strip whitespace and drop empty strings from lists."""
113
+ return [item.strip() for item in v if item.strip()]
114
+
115
+
116
+ # ── Primary model ─────────────────────────────────────────────────────────────
117
+
118
+ class AgentCard(BaseModel):
119
+ """
120
+ The complete identity document for an agent — exposed at the
121
+ well-known endpoint: GET /.well-known/agent-card.json
122
+
123
+ This is what other agents read during discovery to learn:
124
+ - What the agent can do (skills)
125
+ - How to reach it (url)
126
+ - How to authenticate (authentication)
127
+ - What formats it speaks (input_modes, output_modes)
128
+
129
+ Example:
130
+ AgentCard(
131
+ name="ResearchAgent",
132
+ description="Searches the web and summarises findings.",
133
+ version="1.0.0",
134
+ url="https://research-agent.example.com",
135
+ skills=[AgentSkill(id="search", name="Search", description="Web search")],
136
+ )
137
+ """
138
+
139
+ # Identity
140
+ name: Annotated[str, Field(min_length=1, max_length=128)]
141
+ description: Annotated[str, Field(min_length=1, max_length=1024)]
142
+ version: str = Field(
143
+ default="0.1.0",
144
+ description="Semantic version of this agent. Bump when skills change.",
145
+ )
146
+
147
+ # Network
148
+ url: HttpUrl = Field(
149
+ description="Base URL where this agent's A2A server is reachable.",
150
+ )
151
+
152
+ # Protocol
153
+ capabilities: AgentCapabilities = Field(default_factory=AgentCapabilities)
154
+ authentication: AgentAuthentication = Field(default_factory=AgentAuthentication)
155
+
156
+ # Skills
157
+ skills: list[AgentSkill] = Field(
158
+ default_factory=list,
159
+ description="List of capabilities this agent offers.",
160
+ )
161
+
162
+ # Communication formats
163
+ input_modes: list[InputMode] = Field(
164
+ default_factory=lambda: [InputMode.TEXT, InputMode.JSON],
165
+ )
166
+ output_modes: list[OutputMode] = Field(
167
+ default_factory=lambda: [OutputMode.TEXT, OutputMode.JSON],
168
+ )
169
+
170
+ model_config = {
171
+ "use_enum_values": True,
172
+ # Allows: AgentCard(**a2a_sdk_agent_card_dict) without extra fields
173
+ # causing an error — useful when parsing cards from external agents.
174
+ "extra": "ignore",
175
+ }
176
+
177
+ # ── Helpers ───────────────────────────────────────────────────────────────
178
+
179
+ def has_skill(self, skill_id: str) -> bool:
180
+ """Return True if this agent advertises the given skill id."""
181
+ return any(s.id == skill_id for s in self.skills)
182
+
183
+ def skill_ids(self) -> list[str]:
184
+ """Return a flat list of all advertised skill IDs."""
185
+ return [s.id for s in self.skills]
186
+
187
+ def to_well_known_dict(self) -> dict[str, Any] :
188
+ """
189
+ Serialise to the dict served at /.well-known/agent-card.json.
190
+ URLs are converted to plain strings so JSON serialisation works.
191
+ """
192
+ return self.model_dump(mode="json")