Metadata-Version: 2.4
Name: tencentcloud-agentobs-sdk-openai-agent
Version: 0.0.1
Summary: CLS observability SDK for OpenAI Agents SDK — per-turn trace model, direct upload to Tencent Cloud CLS
Author: Tencent Cloud CLS Team
License: Apache-2.0
Project-URL: Homepage, https://cloud.tencent.com/product/cls
Project-URL: Documentation, https://cloud.tencent.com/document/product/614
Project-URL: Repository, https://github.com/TencentCloud/cls-sdk-openai
Keywords: tencent,cls,openai,agents,opentelemetry,observability,llm
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: opentelemetry-api>=1.20.0
Requires-Dist: opentelemetry-sdk>=1.20.0
Requires-Dist: opentelemetry-instrumentation>=0.40b0
Requires-Dist: wrapt>=1.14.0
Requires-Dist: openai-agents>=0.2.0
Requires-Dist: tencentcloud-cls-sdk-python>=0.0.1
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Dynamic: license-file

# tencentcloud-agentobs-sdk-openai-agent

面向 **OpenAI Agents SDK** 的可观测性 SDK：自动拦截 Agents SDK 的 trace/span 生命周期回调，转换为符合腾讯云 CLS GenAI Trace 规范的 OTel span，并直接上传到 **腾讯云 CLS**。

- **零侵入**：一行 `instrument()` 完成埋点，无需改动业务代码
- **per-turn trace 模型**：每次 `Runner.run()` 生成一条完整链路，层级为 `entry → agent → step → chat/tool`
- **并发安全**：父子关系读 `span.parent_id`，天然支持并行工具调用与 `as_tool` 子 agent 嵌套
- **合规可控**：三档内容捕获策略（`full` / `truncate` / `off`），支持强合规场景下完全不记录对话内容
- **完整指标**：token 用量、TTFT（首 token 延迟）、finish_reason、工具错误分类

---

## 安装

```bash
pip install tencentcloud-agentobs-sdk-openai-agent
```

运行时依赖会一并安装，其中关键的两项：

- `openai-agents >= 0.2.0` —— 被埋点的目标 SDK
- `tencentcloud-cls-sdk-python >= 1.0.8` —— CLS 上传客户端

要求 **Python >= 3.9**。

---

## 快速开始

一行 `setup()` 完成全部初始化：

```python
import os
from tencentcloud_agentobs_sdk_openai_agent import setup, CLSConfig

# ── 1. 配置 OpenAI（由 Agents SDK 使用，不属于本 SDK 的配置）──
# 最简方式：设置环境变量，openai 包会自动读取
os.environ["OPENAI_API_KEY"] = "sk-xxxx"

# 如需自定义 base_url（网关/代理/兼容接口/Azure）、organization、超时等，
# 改用自定义客户端交给 Agents SDK：
#   from openai import AsyncOpenAI
#   from agents import set_default_openai_client
#   set_default_openai_client(AsyncOpenAI(
#       api_key="sk-xxxx",
#       base_url="https://your-gateway/v1",
#       organization="org-xxxx",   # 可选
#       timeout=30,                # 可选
#   ))

# ── 2. 启用 CLS 可观测性（必须在跑 agent 之前调用）──
# 方式一：全部走环境变量（先 export CLS_ENDPOINT / CLS_TOPIC_ID / CLS_SECRET_ID / CLS_SECRET_KEY）
setup()

# 方式二：用 CLSConfig 显式提供配置（未提供的字段仍回落到环境变量）
setup(CLSConfig(
    endpoint="ap-guangzhou.cls.tencentcloudapi.com",
    topic_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    secret_id="your_secret_id",
    secret_key="your_secret_key",
    service_name="my-agent-app",
))

# ── 3. 之后照常使用 OpenAI Agents SDK 即可 ──
from agents import Agent, Runner

agent = Agent(
    name="math_tutor",
    instructions="You are a helpful math tutor.",
    model="gpt-4o",   # 指定模型；不写则用 Agents SDK 默认模型
)
result = Runner.run_sync(agent, "What is 12 * 8?")
print(result.final_output)
```

`setup()` 封装了创建 `TracerProvider`、挂载 `CLSCloudExporter`、注册埋点等全部步骤，并返回所用的 `TracerProvider`（需要继续挂载其他 processor 时可用）。

> **OpenAI API Key 由 OpenAI Agents SDK 管理，不属于本 SDK 的配置。** 本 SDK 只负责可观测性埋点与 CLS 上传，从不读取 OpenAI 密钥；调用 LLM 所需的 key、base_url 等都交给 Agents SDK / `openai` 包处理，与 `setup()` 相互独立。具体可配置项与函数签名以 [OpenAI Agents SDK 官方文档](https://openai.github.io/openai-agents-python/config/) 为准。

> `CLSConfig.replace_existing_processors`（默认 `False`）控制埋点注册方式：
> - `False`：追加到 Agents SDK 已有的 trace processor 列表，与其他 processor 共存
> - `True`：替换所有已有 processor（含 OpenAI 默认的），只保留本 SDK

---

## 配置

配置来源共两种，**优先级从高到低**：

1. **`CLSConfig` 显式传值** —— `CLSConfig(topic_id="xxx", ...)`
2. **系统环境变量** —— `export CLS_ENDPOINT=...`

构造 `CLSConfig` 时，未显式提供（保持 `None`）的字段会自动回落到对应环境变量，再回落到内置默认值。因此 `CLSConfig()` 等价于「全部走环境变量」，而 `CLSConfig(topic_id="xxx")` 表示「`topic_id` 用显式值，其余走环境变量」。

### 环境变量示例

```bash
export CLS_ENDPOINT=ap-guangzhou.cls.tencentcloudapi.com
export CLS_TOPIC_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export CLS_SECRET_ID=your_secret_id
export CLS_SECRET_KEY=your_secret_key
export CLS_SERVICE_NAME=my-agent-app
```

---

## 配置项清单

### 必填（缺一即在初始化时抛错）

| 环境变量 | CLSConfig 字段 | 说明 |
| --- | --- | --- |
| `CLS_ENDPOINT` | `endpoint` | CLS 接入地址（无 `https://` 前缀时自动补全） |
| `CLS_TOPIC_ID` | `topic_id` | CLS 日志主题 ID |
| `CLS_SECRET_ID` | `secret_id` | 腾讯云访问密钥 ID |
| `CLS_SECRET_KEY` | `secret_key` | 腾讯云访问密钥 Key |

### 可选

| 环境变量 | CLSConfig 字段 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `CLS_SERVICE_NAME` | `service_name` | `openai-agents-app` | 服务名，写入 span 的 `service.name` / `gen_ai.agent.type`，用于在 CLS 按应用维度筛选 |
| `CLS_HOST_NAME` | `host_name` | 本机 hostname | 主机名，写入 resource 属性 |
| `CLS_SOURCE` | `source` | 本机 IP | 日志来源标识；取不到 IP 时回落到 hostname |
| `CLS_BATCH_SIZE` | `batch_size` | `32` | buffer 攒够多少条 span 触发一次上传 |
| `CLS_DEBUG` | `debug` | `false` | 开启后日志级别降为 DEBUG，打印详细上传过程 |
| `CLS_LOCAL_DUMP` | `local_dump` | `false` | 开启后每批 span 上传前先以 JSON Lines 追加落盘（旁路，失败不影响上传） |
| `CLS_LOCAL_DUMP_FILE` | `local_dump_file` | `cls_spans.jsonl` | 本地落盘文件路径 |
| — | `replace_existing_processors` | `False` | 埋点注册方式：`True` 替换所有已有 processor，`False` 追加 |

布尔类变量（`CLS_DEBUG` / `CLS_LOCAL_DUMP`）接受 `1` / `true` / `yes`（大小写不敏感）为真。

### 内容捕获相关

| 环境变量 | CLSConfig 字段 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `content_mode` | `truncate` | 内容捕获档位，见下节 |
| `CLS_CONTENT_MAX_LENGTH` | `content_max_length` | `8192` | 单个文本字段的截断阈值（字符数） |
| `CLS_CONTENT_TOTAL_MAX_LENGTH` | `content_total_max_length` | `1048576` | 单个 span 属性的总量兜底阈值（字符数） |

### 日志相关

SDK 会为自身 logger 配置文件输出，无需额外配置即可看到链路日志（使用 `RotatingFileHandler` 自动轮转，不会写满磁盘）。这几项属于 SDK 自身 logging 的引导配置，**只能通过环境变量设置**，不在 `CLSConfig` 中。

| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `CLS_SDK_LOG_FILE` | `cls_sdk.log` | SDK 日志文件路径 |
| `CLS_SDK_LOG_LEVEL` | `INFO` | 日志级别 |
| `CLS_SDK_LOG_MAX_BYTES` | `10485760`（10 MB） | 单个日志文件大小上限 |
| `CLS_SDK_LOG_BACKUP_COUNT` | `3` | 日志轮转保留的备份数 |

---

## 内容捕获档位

通过 `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` 控制**是否记录对话内容**（input/output messages、工具入参与结果），三档语义：

| 档位 | 行为 | 适用场景 |
| --- | --- | --- |
| `full` | 完整记录对话内容，不截断 | 本地开发调试 |
| `truncate`（默认） | 记录内容，但对超长字段截断并打标记 | **生产环境** |
| `off` | 完全不记录对话内容，仅保留 token 数、耗时、finish_reason 等指标 | 强合规场景 |

**取值兼容性**：沿用 OTel GenAI semconv 的标准变量名，并兼容其历史布尔取值，便于已配置过其他 GenAI 埋点的用户无缝切换：

- `full` 档也接受：`true` / `1` / `yes` / `all` / `span` / `span_only`
- `off` 档也接受：`false` / `0` / `no` / `none` / `no_content`

**截断机制（`truncate` 档）为两层设计**：

- **第一层（逐字段截断）**：对每个文本字段单独限长（默认 8192 字符），JSON 结构完整保留，超长部分剪短并追加 `...[truncated 原长->新长]` 标记，CLS 查询端仍可用 `JSON_EXTRACT` 提取字段。
- **第二层（总量兜底）**：消息条数极多时，逐字段截断后总量仍可能超标（默认 1 MB），此层为最后防线，正常不触发。

发生截断时，对应 span 会额外写入 `{属性名}.truncated=true` 与 `{属性名}.original_size` 两个属性，便于区分「原文本就短」与「内容被截断了」。

---

## Span 层级结构

每次 `Runner.run()` 生成一条 trace，层级如下：

```
entry (SpanKind.SERVER，每轮一个根)
└── agent (SpanKind.INTERNAL，当前执行的 agent)
    └── step (SpanKind.INTERNAL，合成的 ReAct 轮次)
        ├── chat (SpanKind.CLIENT，一次 LLM 调用)
        └── tool (SpanKind.CLIENT，一次工具调用)
            └── agent [subagent] (通过 as_tool 触发的子 agent)
```

各层级说明：

- **entry** —— 本轮对话的根节点，携带 `session_id` / `turn_id`
- **agent** —— 一个 Agent 的执行区间，汇总该 agent 的 token 用量、LLM 调用次数、工具调用次数
- **step** —— 一个 ReAct 轮次（LLM 推理 →（可选）工具调用），由本 SDK 合成（Agents SDK 本身不产生 step span）
- **chat** —— 一次 LLM API 调用，记录 model、input/output messages、token 用量、finish_reason、TTFT
- **tool** —— 一次工具/函数调用，记录工具名、入参、结果、错误分类

> **父子关系的唯一来源是 `span.parent_id`**，SDK 不维护「当前活跃 agent」这类全局状态，因此并行工具调用、`as_tool` 嵌套子 agent 都能正确归位，互不干扰。

---

## 采集的关键指标

| 属性 | 所在 span | 说明 |
| --- | --- | --- |
| `gen_ai.usage.input_tokens` / `output_tokens` | chat / agent | token 用量（chat 单次；agent 为累计） |
| `gen_ai.response.time_to_first_token_ms` | chat | TTFT，首 token 延迟（流式与非流式均采集） |
| `gen_ai.response.finish_reasons` | chat | 归一化后的 finish_reason（`stop` / `length` / `tool_calls` / `content_filter` / `error`） |
| `gen_ai.response.model` / `gen_ai.request.model` | chat | 模型名 |
| `gen_ai.agent.message_count` / `tool_call_count` | agent | 该 agent 的 LLM / 工具调用次数 |
| `gen_ai.tool.error.type` / `error.message` | tool | 工具错误分类（`timeout` / `rate_limit` / `auth_error` / `connection_error` / `not_found` / `validation_error` 等） |

错误会**自动冒泡**到父级 step 和 agent：chat/tool 失败时，上层 span 也会被置为 `ERROR` 状态，保证在 CLS 侧按 agent 维度检索时能命中失败链路。

---

## 本地调试

无需上传 CLS 即可核对采集内容，通过 `CLSConfig` 开启本地落盘：

```python
from tencentcloud_agentobs_sdk_openai_agent import setup, CLSConfig

setup(CLSConfig(local_dump=True, local_dump_file="cls_spans.jsonl", debug=True))
```

或通过环境变量：

```bash
export CLS_LOCAL_DUMP=true
export CLS_DEBUG=true
```

每批 span 在上传前会以 JSON Lines（一行一条）追加写入本地文件，内容与实际上传 CLS 的完全一致。落盘是旁路能力，写文件失败只记日志，不影响上传主流程。

---

## 错误处理与可靠性

- **重试策略**：`5xx` / 网络错误的 span 放回 buffer 等待下次 flush；`4xx`（400/401/403/404/413）直接丢弃（重试无意义）
- **背压保护**：buffer 超过 10,000 条时丢弃最旧的数据，防止 OOM
- **线程安全**：所有 buffer 操作在锁保护下进行
- **兜底关闭**：trace 结束或 processor shutdown 时，会兜底关闭所有未正常结束的 span（如 task 被 cancel 的情况），避免 span 泄漏

---

## License

Apache-2.0
