splunk-ao-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.
@@ -0,0 +1,186 @@
1
+ .DS_STORE
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ *.py,cover
52
+ .hypothesis/
53
+ .pytest_cache/
54
+ cover/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ #poetry.lock
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ #pdm.lock
115
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
116
+ # in version control.
117
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
118
+ .pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
123
+ __pypackages__/
124
+
125
+ # Celery stuff
126
+ celerybeat-schedule
127
+ celerybeat.pid
128
+
129
+ # SageMath parsed files
130
+ *.sage.py
131
+
132
+ # Environments
133
+ .env
134
+ .venv
135
+ env/
136
+ venv/
137
+ ENV/
138
+ env.bak/
139
+ venv.bak/
140
+
141
+ # Spyder project settings
142
+ .spyderproject
143
+ .spyproject
144
+
145
+ # Rope project settings
146
+ .ropeproject
147
+
148
+ # mkdocs documentation
149
+ /site
150
+
151
+ # mypy
152
+ .mypy_cache/
153
+ .dmypy.json
154
+ dmypy.json
155
+
156
+ # Pyre type checker
157
+ .pyre/
158
+
159
+ # pytype static type analyzer
160
+ .pytype/
161
+
162
+ # Cython debug symbols
163
+ cython_debug/
164
+
165
+ # PyCharm
166
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
167
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
168
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
169
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
170
+ .idea/
171
+
172
+ # PyPI configuration file
173
+ .pypirc
174
+
175
+ node_modules/
176
+
177
+ .ruff_cache/
178
+
179
+ .python-version
180
+ .vscode/
181
+
182
+ # PyDoc generated docs
183
+ .generated_docs/
184
+
185
+ # Local-only working notes / scratch dir (not for git)
186
+ .local/
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ### Added
6
+
7
+ - Added automatic OpenTelemetry instrumentation for A2A client and server
8
+ operations.
9
+ - Added W3C trace-context propagation through A2A message metadata so remote
10
+ agents participate in one distributed trace.
11
+ - Added A2A task and context correlation, session grouping, and optional
12
+ message-content capture.
13
+ - Added deployment-aware OTLP export for Splunk Observability Cloud and
14
+ standalone Agent Observability through the core `splunk-ao` SDK.
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.4
2
+ Name: splunk-ao-a2a
3
+ Version: 0.1.0
4
+ Summary: Splunk AO observability auto-instrumentation for A2A (Agent-to-Agent) protocol
5
+ Project-URL: Repository, https://github.com/splunk/splunk-ao-python
6
+ Author-email: "Splunk Inc." <team@splunk.com>
7
+ License: Apache-2.0
8
+ Classifier: License :: OSI Approved :: Apache Software License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Python: <3.15,>=3.11
14
+ Requires-Dist: a2a-sdk<1.0.0,>=0.3.0
15
+ Requires-Dist: opentelemetry-api>=1.38.0
16
+ Requires-Dist: opentelemetry-sdk>=1.38.0
17
+ Requires-Dist: splunk-ao<1.0.0,>=0.1.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # splunk-ao-a2a
21
+
22
+ [![PyPI version](https://img.shields.io/pypi/v/splunk-ao-a2a.svg)](https://pypi.org/project/splunk-ao-a2a/)
23
+ [![Python versions](https://img.shields.io/pypi/pyversions/splunk-ao-a2a.svg)](https://pypi.org/project/splunk-ao-a2a/)
24
+ [![License](https://img.shields.io/pypi/l/splunk-ao-a2a.svg)](https://github.com/splunk/splunk-ao-python/blob/main/LICENSE)
25
+
26
+ Splunk AO observability for [A2A (Agent-to-Agent)](https://github.com/google/A2A) protocol interactions. Automatic tracing of agent-to-agent calls, task lifecycle, and cross-agent distributed trace correlation.
27
+
28
+ ## How It Works
29
+
30
+ ```
31
+ ┌──────────────────────────────────────────────────────────────────────┐
32
+ │ Single Distributed Trace │
33
+ ├──────────────────────────────────────────────────────────────────────┤
34
+ │ │
35
+ │ ┌─────────────────────┐ A2A Protocol ┌────────────────────┐ │
36
+ │ │ Orchestrator Agent │ (JSON-RPC) │ Researcher Agent │ │
37
+ │ │ │ │ │ │
38
+ │ │ plan │ send_message ──> │ on_message_send │ │
39
+ │ │ delegate ──────────┼──────────────────> │ invoke LLM │ │
40
+ │ │ synthesize │ <── stream events │ call tools │ │
41
+ │ └──────────┬──────────┘ └─────────┬──────────┘ │
42
+ │ │ │ │
43
+ │ │ OpenTelemetry Spans │ │
44
+ │ └─────────────────┬────────────────────────┘ │
45
+ │ │ │
46
+ │ ┌──────────▼──────────┐ │
47
+ │ │ Splunk AO │ │
48
+ │ │ (Trace Explorer) │ │
49
+ │ └─────────────────────┘ │
50
+ └──────────────────────────────────────────────────────────────────────┘
51
+ ```
52
+
53
+ `splunk-ao-a2a` instruments both the **client** (outbound calls) and **server** (inbound requests) sides of the A2A protocol. Trace context is propagated through A2A message metadata so all agents appear in a single distributed trace in Splunk AO.
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install splunk-ao-a2a
59
+ ```
60
+
61
+ **Requirements:** Python 3.11+, Splunk AO standalone or Splunk Observability Cloud credentials, and [a2a-sdk](https://pypi.org/project/a2a-sdk/) 0.3+
62
+
63
+ ## Quick Start
64
+
65
+ ```python
66
+ from splunk_ao.otel import add_splunk_ao_span_processor
67
+ from splunk_ao_a2a import A2AInstrumentor
68
+ from opentelemetry.sdk.trace import TracerProvider
69
+
70
+ provider = TracerProvider()
71
+ add_splunk_ao_span_processor(provider)
72
+ A2AInstrumentor().instrument(tracer_provider=provider, agent_name="orchestrator")
73
+
74
+ try:
75
+ run_application()
76
+ finally:
77
+ provider.shutdown()
78
+ ```
79
+
80
+ Once instrumented, all `a2a-sdk` client and server interactions produce OTel spans automatically.
81
+
82
+ ## Configuration
83
+
84
+ | Parameter | Description |
85
+ |-----------|-------------|
86
+ | `tracer_provider` | OTel `TracerProvider` instance. Falls back to the global provider if not specified. |
87
+ | `agent_name` | Name of this agent, set on spans as `gen_ai.agent.name`. |
88
+ | `capture_content` | Set to `False` to disable capturing message content (e.g. for PII compliance). |
89
+
90
+ For standalone Splunk AO:
91
+
92
+ | Environment Variable | Description |
93
+ |---------------------|-------------|
94
+ | `SPLUNK_AO_API_KEY` | Splunk AO API key (required) |
95
+ | `SPLUNK_AO_CONSOLE_URL` | Splunk AO console URL (required for self-hosted deployments, e.g. `http://localhost:8088`) |
96
+ | `SPLUNK_AO_API_URL` | Explicit API URL (optional; otherwise derived from the console URL) |
97
+ | `SPLUNK_AO_PROJECT` | Project name |
98
+ | `SPLUNK_AO_AGENT_STREAM` | Agent Stream name |
99
+
100
+ For Splunk Observability Cloud:
101
+
102
+ | Environment Variable | Description |
103
+ |---------------------|-------------|
104
+ | `SPLUNK_AO_REALM` | Observability Cloud realm (required) |
105
+ | `SPLUNK_AO_O11Y_TOKEN` | O11y ingest token used for OTLP export (required) |
106
+ | `SPLUNK_AO_O11Y_API_TOKEN` | Dedicated O11y API token for core SDK CRUD operations (optional) |
107
+ | `SPLUNK_AO_PROJECT` | Project name |
108
+ | `SPLUNK_AO_AGENT_STREAM` | Agent Stream name |
109
+
110
+ Applications should configure Project and Agent Stream routing by name.
111
+ The same Python setup works for both deployments; only the environment changes.
112
+
113
+ ## Features
114
+
115
+ ### Client & Server Instrumentation
116
+
117
+ The instrumentor patches both sides of the A2A protocol:
118
+
119
+ **Client-side** (outbound calls): `send_message`, `get_task`, `cancel_task`, `get_card`
120
+
121
+ **Server-side** (inbound requests): `on_message_send`, `on_message_send_stream`
122
+
123
+ ### Cross-Agent Distributed Tracing
124
+
125
+ When Agent A calls Agent B, trace context is propagated through A2A message metadata. The receiving agent joins the caller's trace, so both agents appear in a single distributed trace in Splunk AO.
126
+
127
+ ### Session Tracking
128
+
129
+ A2A's `context_id` is mapped to `session.id`, grouping all interactions within the same conversation into a Splunk AO session.
130
+
131
+ ### Disabling Instrumentation
132
+
133
+ ```python
134
+ instrumentor = A2AInstrumentor()
135
+ instrumentor.instrument(tracer_provider=provider, agent_name="my-agent")
136
+
137
+ # Restore original a2a-sdk behavior
138
+ instrumentor.uninstrument()
139
+ ```
140
+
141
+ ## Multi-Agent Example
142
+
143
+ Add distributed tracing to your multi-agent A2A workflow with just 4 lines of code. The rest is your standard agent logic — no changes needed.
144
+
145
+ ```python
146
+ import asyncio
147
+ import uuid
148
+
149
+ import httpx
150
+ import uvicorn
151
+ from a2a.client import ClientConfig, ClientFactory
152
+ from a2a.server.agent_execution import AgentExecutor, RequestContext
153
+ from a2a.server.apps.jsonrpc.starlette_app import A2AStarletteApplication
154
+ from a2a.server.events import EventQueue, InMemoryQueueManager
155
+ from a2a.server.request_handlers.default_request_handler import DefaultRequestHandler
156
+ from a2a.server.tasks import InMemoryTaskStore
157
+ from a2a.types import (
158
+ AgentCapabilities, AgentCard, AgentSkill, Message, Role,
159
+ TaskState, TaskStatus, TaskStatusUpdateEvent, TextPart,
160
+ )
161
+ from splunk_ao.otel import add_splunk_ao_span_processor
162
+ from splunk_ao_a2a import A2AInstrumentor
163
+ from langchain.agents import create_agent
164
+ from langchain_core.tools import tool
165
+ from langchain_openai import ChatOpenAI
166
+ from langgraph.graph import END, START, StateGraph
167
+ from opentelemetry.instrumentation.langchain import LangchainInstrumentor
168
+ from opentelemetry.sdk.trace import TracerProvider
169
+ from starlette.applications import Starlette
170
+ from typing_extensions import TypedDict
171
+
172
+ # ---- Only 4 lines needed for full distributed tracing ----
173
+ provider = TracerProvider()
174
+ add_splunk_ao_span_processor(provider)
175
+ A2AInstrumentor().instrument(tracer_provider=provider, agent_name="orchestrator")
176
+ LangchainInstrumentor().instrument(tracer_provider=provider)
177
+
178
+
179
+ # ---- Everything below is your standard agent code ----
180
+
181
+ llm = ChatOpenAI(model="gpt-4o-mini")
182
+
183
+ # Researcher agent — standard LangChain agent served over A2A
184
+
185
+ @tool
186
+ def search_kb(query: str) -> str:
187
+ """Search the travel knowledge base."""
188
+ if "paris" in query.lower():
189
+ return "Eiffel Tower 330m, Louvre 9.6M visitors/yr, 20 arrondissements."
190
+ return f"No results for: {query}"
191
+
192
+ researcher = create_agent(
193
+ llm, [search_kb],
194
+ system_prompt="Use search_kb to find facts, then summarize for a traveler.",
195
+ )
196
+
197
+ CARD = AgentCard(
198
+ name="researcher", description="Travel researcher", url="http://localhost:9867",
199
+ version="1.0.0", capabilities=AgentCapabilities(streaming=True),
200
+ default_input_modes=["text/plain"], default_output_modes=["text/plain"],
201
+ skills=[AgentSkill(id="qa", name="Q&A", description="Answer questions", tags=[])],
202
+ )
203
+
204
+ class ResearcherExecutor(AgentExecutor):
205
+ async def execute(self, ctx: RequestContext, queue: EventQueue) -> None:
206
+ result = await researcher.ainvoke({"messages": [("user", ctx.get_user_input())]})
207
+ await queue.enqueue_event(TaskStatusUpdateEvent(
208
+ task_id=ctx.task_id, context_id=ctx.context_id, final=True,
209
+ status=TaskStatus(state=TaskState.completed, message=Message(
210
+ message_id=str(uuid.uuid4()), role=Role.agent,
211
+ parts=[TextPart(text=result["messages"][-1].content or "")],
212
+ )),
213
+ ))
214
+
215
+ async def cancel(self, ctx: RequestContext, queue: EventQueue) -> None:
216
+ await queue.enqueue_event(TaskStatusUpdateEvent(
217
+ task_id=ctx.task_id, context_id=ctx.context_id, final=True,
218
+ status=TaskStatus(state=TaskState.canceled),
219
+ ))
220
+
221
+ # Orchestrator — standard LangGraph StateGraph
222
+
223
+ class State(TypedDict):
224
+ user_query: str
225
+ skills: list[str]
226
+ research_query: str
227
+ response: str
228
+ plan: str
229
+
230
+ def build_orchestrator(client):
231
+ async def discover(state: State) -> dict:
232
+ card = await client.get_card()
233
+ return {"skills": [s.name for s in card.skills]}
234
+
235
+ async def plan(state: State) -> dict:
236
+ result = await create_agent(llm,
237
+ system_prompt="Formulate a travel research question. Reply with ONLY the question.",
238
+ ).ainvoke({"messages": [("user", state["user_query"])]})
239
+ return {"research_query": result["messages"][-1].content}
240
+
241
+ async def delegate(state: State) -> dict:
242
+ msg = Message(message_id=str(uuid.uuid4()), role=Role.user,
243
+ parts=[TextPart(text=state["research_query"])], context_id="session-1")
244
+ async for event in client.send_message(msg):
245
+ if isinstance(event, tuple):
246
+ task = event[0]
247
+ if task.status and task.status.state == TaskState.completed and task.status.message:
248
+ return {"response": getattr(task.status.message.parts[0].root, "text", "")}
249
+ return {"response": ""}
250
+
251
+ async def synthesize(state: State) -> dict:
252
+ result = await create_agent(llm,
253
+ system_prompt="Create a brief 3-day itinerary from the research.",
254
+ ).ainvoke({"messages": [("user", f"Research:\n{state['response']}\n\nCreate itinerary.")]})
255
+ return {"plan": result["messages"][-1].content}
256
+
257
+ graph = StateGraph(State)
258
+ graph.add_node("discover", discover)
259
+ graph.add_node("plan", plan)
260
+ graph.add_node("delegate", delegate)
261
+ graph.add_node("synthesize", synthesize)
262
+ graph.add_edge(START, "discover")
263
+ graph.add_edge("discover", "plan")
264
+ graph.add_edge("plan", "delegate")
265
+ graph.add_edge("delegate", "synthesize")
266
+ graph.add_edge("synthesize", END)
267
+ return graph.compile()
268
+
269
+ # Run both agents
270
+
271
+ async def main():
272
+ app = Starlette()
273
+ A2AStarletteApplication(
274
+ agent_card=CARD,
275
+ http_handler=DefaultRequestHandler(
276
+ agent_executor=ResearcherExecutor(),
277
+ task_store=InMemoryTaskStore(), queue_manager=InMemoryQueueManager(),
278
+ ),
279
+ ).add_routes_to_app(app)
280
+ server = uvicorn.Server(uvicorn.Config(app, port=9867, log_level="warning"))
281
+ server_task = asyncio.create_task(server.serve())
282
+ await asyncio.sleep(1)
283
+
284
+ client = ClientFactory(
285
+ config=ClientConfig(streaming=True, httpx_client=httpx.AsyncClient(timeout=httpx.Timeout(120))),
286
+ ).create(CARD)
287
+ result = await build_orchestrator(client).ainvoke(
288
+ {"user_query": "Plan a 3-day trip to Paris", "skills": [], "research_query": "", "response": "", "plan": ""},
289
+ )
290
+ print(result["plan"])
291
+
292
+ server.should_exit = True
293
+ await server_task
294
+ provider.shutdown()
295
+
296
+ if __name__ == "__main__":
297
+ # Set environment variables: SPLUNK_AO_API_KEY, OPENAI_API_KEY
298
+ asyncio.run(main())
299
+ ```
300
+
301
+ ## Resources
302
+
303
+ - [Splunk AO Documentation](https://agent-observability-docs.splunk.com)
304
+ - [A2A Protocol Specification](https://a2a-protocol.org)
305
+ - [a2a-sdk Documentation](https://pypi.org/project/a2a-sdk)
306
+
307
+ ## License
308
+
309
+ Apache-2.0