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.
- splunk_ao_a2a-0.1.0/.gitignore +186 -0
- splunk_ao_a2a-0.1.0/CHANGELOG.md +14 -0
- splunk_ao_a2a-0.1.0/PKG-INFO +309 -0
- splunk_ao_a2a-0.1.0/README.md +290 -0
- splunk_ao_a2a-0.1.0/examples/.env.example +13 -0
- splunk_ao_a2a-0.1.0/examples/two_agent_demo.py +225 -0
- splunk_ao_a2a-0.1.0/pyproject.toml +135 -0
- splunk_ao_a2a-0.1.0/src/splunk_ao_a2a/__init__.py +4 -0
- splunk_ao_a2a-0.1.0/src/splunk_ao_a2a/_client_patches.py +152 -0
- splunk_ao_a2a-0.1.0/src/splunk_ao_a2a/_constants.py +45 -0
- splunk_ao_a2a-0.1.0/src/splunk_ao_a2a/_context.py +168 -0
- splunk_ao_a2a-0.1.0/src/splunk_ao_a2a/_server_patches.py +140 -0
- splunk_ao_a2a-0.1.0/src/splunk_ao_a2a/_spans.py +223 -0
- splunk_ao_a2a-0.1.0/src/splunk_ao_a2a/_version.py +1 -0
- splunk_ao_a2a-0.1.0/src/splunk_ao_a2a/instrumentor.py +65 -0
- splunk_ao_a2a-0.1.0/src/splunk_ao_a2a/py.typed +0 -0
- splunk_ao_a2a-0.1.0/tests/conftest.py +16 -0
- splunk_ao_a2a-0.1.0/tests/test_client_patches.py +222 -0
- splunk_ao_a2a-0.1.0/tests/test_context.py +267 -0
- splunk_ao_a2a-0.1.0/tests/test_instrumentor.py +61 -0
- splunk_ao_a2a-0.1.0/tests/test_server_patches.py +197 -0
- splunk_ao_a2a-0.1.0/tests/test_spans.py +292 -0
- splunk_ao_a2a-0.1.0/tests/test_splunk_ao_compatibility.py +88 -0
|
@@ -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
|
+
[](https://pypi.org/project/splunk-ao-a2a/)
|
|
23
|
+
[](https://pypi.org/project/splunk-ao-a2a/)
|
|
24
|
+
[](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
|