Skip to content
2 changes: 2 additions & 0 deletions a2a-t-sample/README-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@
| 目录 | 说明 |
| --- | --- |
| [fault-management/](fault-management/) | 事件订阅样例——流式推送 Incident 通知 |
| [negotiation/](negotiation/) | 协商闭环样例——离线 propose → accept 往返 |
| [observability-sample/](observability-sample/) | 可观测样例——所有场景的 trace / 指标 / 日志,可选 Langfuse 后端 |
| [ran-energy-saving/](ran-energy-saving/) | 无线网络节能样例——运行 RAN 节能任务,并包含三类 Negotiation-T 协商(信息 / 目标 / 可行性) |
2 changes: 2 additions & 0 deletions a2a-t-sample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ See the repository root [`README.md`](../README.md) for setup and how to run.
| Directory | Description |
| --- | --- |
| [fault-management/](fault-management/) | Event subscription sample — stream Incident notifications |
| [negotiation/](negotiation/) | Negotiation closed-loop sample — offline propose → accept round trips |
| [observability-sample/](observability-sample/) | Observability sample — traces, metrics and logs for every scenario, with an optional Langfuse backend |
| [ran-energy-saving/](ran-energy-saving/) | RAN energy saving sample — run a RAN energy-saving task, including Negotiation-T negotiations in three kinds (information / target / feasibility) |
133 changes: 133 additions & 0 deletions a2a-t-sample/observability-sample/README-zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# observability-sample — 看见你的 A2A-T 智能体在做什么

一个可直接运行的样例:展示 **A2A-T 交互的完整生命周期**——客户端发起请求、服务端处理、
结果流式返回——每一步都由 `a2a-t-sdk` 的可观测层自动记录(trace / 指标 / 日志)。

样例里**没有任何手写的 OpenTelemetry 代码**:服务端加一个包装器、客户端做一次注册,
其余全部由 SDK 自动完成。

---

## 可以体验什么

| 场景(`--scenario=`) | 故事 |
|------------------------|------|
| `native` | 一个任务完整走完:客户端提交、服务端处理后流式返回 artifact。观察请求如何把两端"点亮"。 |
| `a2at` | 用 LLM 生成任务 prompt(默认 mock LLM——无需 API Key),并在 SDK 调用外手动加一个"L4" Span。 |
| `negotiation` | 智能体**先谈价、后干活**:两轮报价通过纯消息完成(此时还没有任务),双方达成一致后,真正的任务才启动并流式返回结果。 |

## 快速开始

前置条件:已在仓库根目录,且安装了 [uv](https://docs.astral.sh/uv/)。

```bash
# 仓库根目录——终端 1:启动服务端(端口 8100)
cd a2a-t-sample
export PYTHONPATH="$PWD/observability-sample/src"
export A2AT_OBSERVABILITY_ENABLED=true
uv run python -m obs_sample.server_main

# 终端 2:运行客户端场景
cd a2a-t-sample
export PYTHONPATH="$PWD/observability-sample/src"
uv run python -m obs_sample.client_main --scenario=native
```

Windows PowerShell:把 `export` 换成 `$env:PYTHONPATH = "$PWD\observability-sample\src"`。

两个终端都会输出日志(`[server] ...` / `[client] ...`);默认配置下服务端还会把每个
span 以 JSON 打印到控制台。

## 场景说明

### native — 一个任务,两端可见

1. 终端 1 启动服务端;终端 2 以 `--scenario=native` 运行客户端。
2. 客户端提交任务;服务端经历 *submitted → working* 并流式返回 artifact。
3. 每一步在**两端**都以 span 呈现,并汇入同一条 trace(客户端自动把 W3C `traceparent`
传给服务端)。

### a2at — 用 LLM 生成 prompt

```bash
uv run python -m obs_sample.client_main --scenario=a2at
```

客户端通过 SDK 的 LLM 门面生成任务 prompt。`a2a-t-sample/.env` 里 `A2AT_LLM_API_KEY`
留空时会使用**mock LLM**(固定回复),完全离线可跑。LLM 的 token 用量会被自动计量并
挂到 trace 上。

### negotiation — 先谈成,再执行

```bash
# 终端 1:以协商模式重启服务端
uv run python -m obs_sample.server_main --scenario=negotiation
# 终端 2:
uv run python -m obs_sample.client_main --scenario=negotiation
```

这是 Negotiation-T 谈判的真实时序:

1. **第 1 轮** — 客户端出价;服务端还一个不同的价并断开连接。*此时还没有任务——
谈判通过纯消息进行。*
2. **第 2 轮** — 客户端重连并接受对方的还价;服务端确认达成一致,再次断开。
3. **达成一致后任务才启动** — 客户端以独立请求启动真正的任务生命周期并接收结果。

每一轮都是一次独立交互;样例把它们串起来,让你能在同一个地方看到整场谈判
(见下方 Langfuse)。

## 把所有数据发到 Langfuse(可选)

在启动**两个进程之前**设置三个变量,所有 span 就会进入你的 Langfuse 项目——
每次交互一条可浏览的 trace:

```bash
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="https://us.cloud.langfuse.com"
```

在 Langfuse 界面里,每一轮的客户端/服务端 span 和最终任务会呈现在同一条时间线上——
谁出了什么价、哪一轮被接受、每一步耗时多久,一目了然。

## 记录了什么(速查表)

| 你做的事 | SDK 记录的内容 |
|----------|----------------|
| 客户端发请求 | `SendStreamingMessage` span(CLIENT)+ 耗时指标 |
| 服务端收到请求 | `SendStreamingMessage` span(SERVER),是客户端 span 的子节点 |
| 服务端流式发出事件 | `SendStreamingMessage-event` span,LINK 关联入口 |
| 发出 / 收到协商消息 | `SendStreamingMessage-negotiation` span(含轮次与施为动词) |
| 服务端推送通知 | `SendMessage-pushNotification` span,LINK 关联服务端入口 |
| LLM 消耗 token | token 用量指标 + 下一跳 trace 上的 `gen_ai.usage.*` |
| 值得记录的事件 | logger `a2at.observability` 上的结构化日志 |

三类信号可独立开关——见下方环境变量。

## 环境变量

常用:

| 变量 | 默认值 | 含义 |
|------|--------|------|
| `A2AT_OBSERVABILITY_ENABLED` | `true` | 总开关——`false` 时 SDK 完全透明 |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | 未设置 | 把 trace/指标/日志发到 OTLP collector,而不是控制台 |
| `A2AT_CONSOLE_EXPORTER` | `true` | `false` 关闭 dev 模式控制台兜底(stdout 不再输出 JSON) |
| `OTEL_SERVICE_NAME`(或 `A2AT_SERVICE_NAME`) | `a2a-t-agent` | 本进程在后端的名称 |
| `A2AT_OBS_SAMPLE_PORT` | `8100` | 样例服务端端口 |
| `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` / `LANGFUSE_BASE_URL` | 未设置 | 启用 Langfuse 后端 |

进阶(较少用到):`A2AT_TRACE_ENABLED`、`A2AT_METRIC_ENABLED`、`A2AT_LOG_ENABLED`
(按信号独立开关——例如关 trace 后指标与日志仍工作)、`A2AT_EXPORTER_PROTOCOL`
(`grpc` / `http/protobuf`)、`OTEL_INSTRUMENTATION_A2AT_SDK_ENABLED=false`(硬关闭)。

## 说明

- 服务端使用**官方默认**的 `DefaultRequestHandler`(a2a-sdk 1.1.x 的别名,指向
`DefaultRequestHandlerV2`):可观测装饰器会自动检测 V2 形状,并用装饰后的
executor / push sender 重建其任务注册表——per-event 与 push span 在官方 handler
上同样生效。(`LegacyRequestHandler` 通过逐属性包装的方式继续受支持。)
- 推送通知会回环到服务端的 `/push-sink`,因此样例能端到端展示
`SendMessage-pushNotification` span。
- 离线验证辅助脚本与样例代码放在一起(`check_*.py`、`verify_langfuse.py`)——它们是
一次性调试工具,不属于样例主流程。
181 changes: 111 additions & 70 deletions a2a-t-sample/observability-sample/README.md
Original file line number Diff line number Diff line change
@@ -1,98 +1,139 @@
# observability-sample — A2A-T 可观测性演示(v3.0 装饰器架构)
# observability-sample — see what your A2A-T agents are actually doing

基于 `a2a-t-sdk[observability]` v3.0 的端到端可观测性样例:一行装饰器接线、零手动 OTel 代码。
A runnable sample that shows the **full lifecycle of an A2A-T interaction** — a client
asks, a server answers, artifacts stream back — with every step automatically traced,
measured and logged by the `a2a-t-sdk` observability layer.

## 接线(v3.0)
There is **no manual OpenTelemetry code** in the sample: the server adds one wrapper,
the client adds one registration, and everything else is emitted by the SDK.

- Server:`A2ATRequestHandlerDecorator(LegacyRequestHandler(...))` —— 自动创建 SERVER 入口 Span
(从 `ServerCallContext.state["headers"]` 提取 traceparent 作为父上下文,无需 ASGI 中间件)、
包装 executor 产生 per-event Span、包装 push sender 产生 push Span
- Client:`register_client_factory(factory)` —— 之后 `factory.create(card)` 产出的 client 自动携带
观测 transport(CLIENT Span + traceparent 注入,经 `ClientCallContext.service_parameters` 落到 HTTP 头)
- OTel 自动配置:SDK 在首次使用时自动装配 Tracer/Meter/LoggerProvider(未配置
`OTEL_EXPORTER_OTLP_ENDPOINT` 时回退 Console 导出),样例不含任何手动 setup 代码
---

## a2a-sdk 1.1.x handler caveat(必须 LegacyRequestHandler)
## What you can try

完整 per-event Span 富化需要 `LegacyRequestHandler`。a2a-sdk 1.1.2 默认的 `DefaultRequestHandler`
(V2)在构造期即捕获裸的 `agent_executor`/`_push_sender` 引用(ActiveTaskRegistry),装饰器构造期的
executor/push 包装会被绕过。本样例固定使用 `LegacyRequestHandler`(服务端行为与 V2 兼容)。
| Scenario (`--scenario=`) | Story |
|--------------------------|-------|
| `native` | A task flows end to end: the client submits, the server works and streams artifacts back. Watch the request light up on both sides. |
| `a2at` | Prompt generation with an LLM (a mock LLM by default — no API key needed) and a manual "L4" span around the SDK call. |
| `negotiation` | The agent **bargains before it commits**: two offer rounds happen over plain messages (no task yet), and only after both sides agree does the real task start and stream its result. |

## 运行(离线,默认 Console 输出)
## Quick start

```powershell
cd a2a-t-sample
$env:PYTHONPATH = "$pwd\observability-sample\src"
$env:A2AT_OBSERVABILITY_ENABLED = "true" # 观测总开关(默认即 true;置 false 时装饰器完全直通)
Prerequisites: you are in the repository root and [uv](https://docs.astral.sh/uv/) is installed.

# 终端 1:server(端口 8100)
```bash
# from the repo root — terminal 1: start the server (port 8100)
cd a2a-t-sample
export PYTHONPATH="$PWD/observability-sample/src"
export A2AT_OBSERVABILITY_ENABLED=true
uv run python -m obs_sample.server_main

# 终端 2:client(原生 a2a-python 流量)
# terminal 2: run a client scenario
cd a2a-t-sample
export PYTHONPATH="$PWD/observability-sample/src"
uv run python -m obs_sample.client_main --scenario=native

# 或 client(A2ATClient 模板生成 + trace_facade L4 Span,需仓库 a2a-t-sample/.env:
# 不存在时从 env.example 复制并将 A2AT_LLM_API_KEY 置空(A2AT_LLM_API_KEY=),置空后自动走 mock LLM)
uv run python -m obs_sample.client_main --scenario=a2at

# 协商场景:server 需以 negotiation 模式启动(返回 Negotiation-T 终态 Message)
uv run python -m obs_sample.server_main --scenario=negotiation
# 然后另一个终端:
uv run python -m obs_sample.client_main --scenario=negotiation
```

`--scenario=a2at` 分支会调用一次幂等的 `setup()`(SDK 公共自动配置入口):L4 门面 Span
产生在首个 A2A 请求之前,需先完成 OTel 自动装配才不会丢失;native 场景无需任何调用,
首个请求即触发自动配置。
Windows PowerShell: use `$env:PYTHONPATH = "$PWD\observability-sample\src"` instead of `export`.

端口可用 `A2AT_OBS_SAMPLE_PORT` 覆盖(默认 8100)。样例 client 会请求 push notification
(回环到 server 的 `/push-sink`),用于演示 `SendMessage-pushNotification` Span。
You will see log lines on both terminals (`[server] ...` / `[client] ...`), and — with the
default configuration — every span printed as JSON on the server console.

## 预期输出(v3.0 Span 命名,a2a-java 对齐)
## Scenario walkthroughs

Client 终端(CLIENT Span):
### native — one task, both sides

- `SendStreamingMessage`(入口 Span,含 `extension.name`、`gen_ai.operation.name`、
`gen_ai.client.operation.duration` 指标;traceparent 注入请求头)
- `SendStreamingMessage-event`(per-event,LINK 到入口 Span)
- `--scenario=a2at` 另有 `a2at.sdk.client.generate_task_prompt`(L4)
- `--scenario=negotiation` 另有 `SendStreamingMessage-negotiation`(PARENT 到入口 Span,
含 `negotiation.id/round/max_rounds/performative` 属性;服务端返回 Negotiation-T 终态 Message 时自动识别)
1. Terminal 1 starts the server. Terminal 2 runs the client with `--scenario=native`.
2. The client submits a task; the server moves it through *submitted → working* and
streams an artifact back.
3. Each step appears as a span on **both** sides, joined into one trace
(the client passes a W3C `traceparent` to the server automatically).

Server 终端(SERVER/INTERNAL Span,与 Client 同 trace_id —— W3C traceparent 端到端贯通):
### a2at — generate a prompt with an LLM

- `SendStreamingMessage`(SERVER 入口 Span,父 = Client 入口 Span)
- `SendStreamingMessage-event`(per-event INTERNAL Span,父 = SERVER 入口 Span)
- `SendMessage-pushNotification`(CLIENT kind Span,LINK 到 SERVER 入口 Span)
- `--scenario=negotiation` 时 server 返回终态协商 Message(不产生 per-event Span 流)
```bash
uv run python -m obs_sample.client_main --scenario=a2at
```

日志与指标:
The client generates a task prompt through the SDK's LLM facade. With an empty
`A2AT_LLM_API_KEY` in `a2a-t-sample/.env` a canned **mock LLM** answers, so this runs
fully offline. The LLM's token usage is measured automatically and attached to the trace.

- logger `a2at.observability` 自动经 OTLP/Console 导出(`task.status_changed` / `task.artifact` 等)
- 指标:`gen_ai.client.operation.duration`(client)、`a2at.task.request.duration`(side=client/server)
### negotiation — bargain first, execute later

```bash
# terminal 1: restart the server in negotiation mode
uv run python -m obs_sample.server_main --scenario=negotiation
# terminal 2:
uv run python -m obs_sample.client_main --scenario=negotiation
```

## 配置开关
This is the real-world timing of a Negotiation-T deal:

| 环境变量 | 默认 | 说明 |
| --- | --- | --- |
| `A2AT_OBSERVABILITY_ENABLED` | `true` | 观测总开关;`false` → 装饰器完全直通(无任何观测) |
| `A2AT_TRACE_ENABLED` | `true` | Trace 信号开关;`false` → 不产生 Span,指标/日志仍工作 |
| `A2AT_METRIC_ENABLED` | `true` | Metric 信号开关 |
| `A2AT_LOG_ENABLED` | `true` | Log 信号开关 |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | 未设置 | 未设置 → Console 导出(dev 模式) |
| `A2AT_EXPORTER_PROTOCOL` | `grpc` | OTLP 协议(`grpc` / `http/protobuf`) |
| `OTEL_SERVICE_NAME`(或 `A2AT_SERVICE_NAME`) | `a2a-t-agent` | Resource `service.name` |
| `OTEL_INSTRUMENTATION_A2AT_SDK_ENABLED` | `true` | OTel 主开关;`false` → 全部 NoOp 直通 |
1. **Round 1** — the client proposes; the server counters with a different offer and
disconnects. *No task exists yet — bargaining happens over plain messages.*
2. **Round 2** — the client reconnects, accepts the counter-offer; the server confirms
the agreement and disconnects again.
3. **The task starts only after agreement** — a separate request now runs the full
task lifecycle and streams the result.

## 切换 OTLP 后端
Each round is an independent exchange; the sample ties them together so you can follow
the whole deal in one place (see Langfuse below).

```powershell
$env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://collector:4317"
```
## Send everything to Langfuse (optional)

(需另装 `opentelemetry-exporter-otlp-proto-grpc`;默认 Console 输出无需任何额外依赖。)
Set three variables before starting **both** processes and every span lands in your
Langfuse project as one browsable trace per interaction:

## 说明
```bash
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="https://us.cloud.langfuse.com"
```

- 断言式端到端验证见 `tests/integration/test_e2e_observability.py`(`A2AT_TEST_A2A=1`)。
- a2at 场景的 mock LLM 资源复用 `../subscribe-incident/resources/mock_responses`。
In the Langfuse UI you will see the client and server spans of every round and the final
task on a single timeline — including who proposed what, which round was accepted, and
how long each step took.

## What gets recorded (cheat sheet)

| You do | The SDK records |
|--------|-----------------|
| Client sends a request | `SendStreamingMessage` span (CLIENT) + duration metric |
| Server receives it | `SendStreamingMessage` span (SERVER), child of the client's |
| Server streams an event | `SendStreamingMessage-event` span, linked to the entry |
| A negotiation message is sent / received | `SendStreamingMessage-negotiation` span with round & performative |
| Server pushes a notification | `SendMessage-pushNotification` span, linked to the server entry |
| The LLM generates tokens | token-usage metric + `gen_ai.usage.*` on the next trace |
| Anything noteworthy | a structured log line on logger `a2at.observability` |

Signals can be toggled independently — see the environment variables below.

## Environment variables

Commonly used:

| Variable | Default | Meaning |
|----------|---------|---------|
| `A2AT_OBSERVABILITY_ENABLED` | `true` | Master switch — `false` makes the SDK fully transparent |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | *(unset)* | Send spans/metrics/logs to an OTLP collector instead of the console |
| `A2AT_CONSOLE_EXPORTER` | `true` | `false` disables the dev-mode console fallback (no stdout JSON) |
| `OTEL_SERVICE_NAME` (or `A2AT_SERVICE_NAME`) | `a2a-t-agent` | How this process is named in the backend |
| `A2AT_OBS_SAMPLE_PORT` | `8100` | Sample server port |
| `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` / `LANGFUSE_BASE_URL` | *(unset)* | Enable the Langfuse backend |

Advanced (rarely needed): `A2AT_TRACE_ENABLED`, `A2AT_METRIC_ENABLED`, `A2AT_LOG_ENABLED`
(per-signal switches — e.g. trace off keeps metrics and logs alive), `A2AT_EXPORTER_PROTOCOL`
(`grpc` / `http/protobuf`), `OTEL_INSTRUMENTATION_A2AT_SDK_ENABLED=false` (hard disable).

## Notes

- The server uses the **official default** `DefaultRequestHandler` (a2a-sdk 1.1.x alias
for `DefaultRequestHandlerV2`): the observability decorator detects the V2 shape and
rebuilds its task registry with the decorated executor / push sender, so per-event and
push spans work on the official handler too. (`LegacyRequestHandler` remains supported
via per-attribute wrapping.)
- Push notifications are echoed back to the server's `/push-sink` endpoint, which is
why the sample can show the `SendMessage-pushNotification` span end to end.
- Offline verification helpers live next to the sample code (`check_*.py`,
`verify_langfuse.py`) — they are one-off debugging tools, not part of the sample flow.
Loading
Loading