Metadata-Version: 2.4
Name: tencentcloud-agentobs-sdk-openai-agent
Version: 0.0.2
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

[中文文档](README.zh-CN.md)

Observability SDK for the **OpenAI Agents SDK**: automatically intercepts
Agents SDK trace/span lifecycle callbacks, converts them into OTel spans
conforming to the Tencent Cloud CLS GenAI Trace specification, and uploads
directly to **Tencent Cloud CLS**.

- **Zero intrusion**: one `setup()` call completes instrumentation — no changes to business code
- **Per-turn trace model**: each `Runner.run()` produces a complete trace with `entry → agent → step → chat/tool` hierarchy
- **Concurrency safe**: parent-child relationships are derived from `span.parent_id` — parallel tool calls and `as_tool` sub-agent nesting just work
- **Compliance ready**: three content capture modes (`full` / `truncate` / `off`) for strict data-residency requirements
- **Full metrics**: token usage, TTFT (time to first token), finish_reason, tool error classification

---

## Installation

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

Runtime dependencies are installed automatically. The key ones:

- `openai-agents >= 0.2.0` — the target SDK being instrumented
- `tencentcloud-cls-sdk-python >= 1.0.8` — CLS upload client

Requires **Python >= 3.9**.

---

## Quick Start

A single `setup()` handles all initialization:

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

# 1. Configure OpenAI (managed by Agents SDK, not this SDK)
os.environ["OPENAI_API_KEY"] = "sk-xxxx"

# 2. Enable CLS observability (must be called before running any agent)
# Option A: all from environment variables
setup()

# Option B: explicit CLSConfig (unset fields fall back to env vars)
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",
))

# 3. Use the OpenAI Agents SDK as usual
from agents import Agent, Runner

agent = Agent(
    name="math_tutor",
    instructions="You are a helpful math tutor.",
    model="gpt-4o",
)
result = Runner.run_sync(agent, "What is 12 * 8?")
print(result.final_output)
```

`setup()` creates a `TracerProvider`, attaches the `CLSCloudExporter`, registers
instrumentation, and returns the provider (useful if you need to attach
additional processors).

> **The OpenAI API Key is managed by the Agents SDK, not by this SDK.** This
> SDK only handles observability instrumentation and CLS upload — it never reads
> OpenAI credentials. Key, base_url, etc. are handled by the Agents SDK /
> `openai` package, independent of `setup()`.

> `CLSConfig.replace_existing_processors` (default `False`) controls
> registration behavior:
> - `False`: append to the Agents SDK's existing trace processor list, coexisting with others
> - `True`: replace all existing processors (including OpenAI's default), keeping only this SDK

---

## Configuration

Two sources, in **descending priority**:

1. **`CLSConfig` explicit values** — `CLSConfig(topic_id="xxx", ...)`
2. **Environment variables** — `export CLS_ENDPOINT=...`

Fields left as `None` in `CLSConfig` automatically fall back to the
corresponding environment variable, then to built-in defaults.

### Environment Variable Example

```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
```

---

## Configuration Reference

### Required (missing any one raises an error at init)

| Env Var | CLSConfig Field | Description |
| --- | --- | --- |
| `CLS_ENDPOINT` | `endpoint` | CLS endpoint (`https://` is auto-prepended if missing) |
| `CLS_TOPIC_ID` | `topic_id` | CLS log topic ID |
| `CLS_SECRET_ID` | `secret_id` | Tencent Cloud access key ID |
| `CLS_SECRET_KEY` | `secret_key` | Tencent Cloud access key secret |

### Optional

| Env Var | CLSConfig Field | Default | Description |
| --- | --- | --- | --- |
| `CLS_AGENT_TYPE` | `agent_type` | `openai-agent` | Agent type, written to `gen_ai.agent.type`, and used as the resource `service.name` per spec |
| `CLS_PROVIDER_NAME` | `provider_name` | inferred from model | LLM provider, written to `gen_ai.provider.name`. If unset, inferred from the model prefix (`gpt`→openai, etc.); set explicitly for Azure/compatible endpoints where inference is unreliable |
| `CLS_USER_ID` | `user_id` | empty | End-user ID, written to `gen_ai.user.id` on every span for per-user filtering in CLS. Provided by the business layer; left empty if unset |
| `CLS_HOST_NAME` | `host_name` | local hostname | Host name for resource attributes |
| `CLS_SOURCE` | `source` | local IP | Log source identifier; falls back to hostname |
| `CLS_BATCH_SIZE` | `batch_size` | `32` | Spans per upload batch |
| `CLS_DEBUG` | `debug` | `false` | Enable DEBUG-level logging with detailed upload info |
| `CLS_LOCAL_DUMP` | `local_dump` | `false` | Dump each batch as JSON Lines before upload (sidecar, failure doesn't affect upload) |
| `CLS_LOCAL_DUMP_FILE` | `local_dump_file` | `cls_spans.jsonl` | Local dump file path |
| — | `replace_existing_processors` | `False` | `True` replaces all existing processors; `False` appends |

Boolean variables accept `1` / `true` / `yes` (case-insensitive).

### Content Capture

| Env Var | CLSConfig Field | Default | Description |
| --- | --- | --- | --- |
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `content_mode` | `truncate` | Content capture mode — see below |
| `CLS_CONTENT_MAX_LENGTH` | `content_max_length` | `8192` | Per-field truncation threshold (characters) |
| `CLS_CONTENT_TOTAL_MAX_LENGTH` | `content_total_max_length` | `1048576` | Per-span total attribute size backstop (characters) |

### SDK Logging

The SDK configures its own logger with a `RotatingFileHandler` (auto-rotates,
won't fill the disk). These are bootstrap settings, **settable only via
environment variables**:

| Env Var | Default | Description |
| --- | --- | --- |
| `CLS_SDK_LOG_FILE` | `cls_sdk.log` | SDK log file path |
| `CLS_SDK_LOG_LEVEL` | `INFO` | Log level |
| `CLS_SDK_LOG_MAX_BYTES` | `10485760` (10 MB) | Max size per log file |
| `CLS_SDK_LOG_BACKUP_COUNT` | `3` | Number of rotated backup files |

---

## Content Capture Modes

Controlled by `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` — determines
**whether conversation content is recorded** (input/output messages, tool
arguments and results):

| Mode | Behavior | Use Case |
| --- | --- | --- |
| `full` | Record complete content, no truncation | Local debugging |
| `truncate` (default) | Record content, truncate oversized fields with markers | **Production** |
| `off` | No content at all — only token counts, latency, finish_reason | Strict compliance |

**Compatibility**: uses the standard OTel GenAI semconv variable name and
accepts its legacy boolean values:

- `full` also accepts: `true` / `1` / `yes` / `all` / `span` / `span_only`
- `off` also accepts: `false` / `0` / `no` / `none` / `no_content`

**Truncation (`truncate` mode) is two-layered**:

- **Layer 1 (per-field)**: each text field is capped individually (default 8192
  chars), JSON structure is preserved, overflow is clipped with a
  `...[truncated orig→new]` marker. CLS queries can still `JSON_EXTRACT` fields.
- **Layer 2 (total backstop)**: when message count is extreme, per-field
  truncation may not be enough (default 1 MB cap). This layer is a safety net
  and rarely triggers.

When truncation occurs, the span gets additional
`{attribute}.truncated=true` and `{attribute}.original_size` attributes,
so "originally short" and "was truncated" are distinguishable.

---

## Span Hierarchy

Each `Runner.run()` produces one trace:

```
entry (SpanKind.SERVER — one root per turn)
└── agent (SpanKind.INTERNAL — the executing agent)
    └── step (SpanKind.INTERNAL — synthesized ReAct round)
        ├── chat (SpanKind.CLIENT — one LLM call)
        └── tool (SpanKind.CLIENT — one tool call)
            └── agent [subagent] (triggered via as_tool)
```

- **entry** — root of this turn, carries `session_id` / `turn_id`
- **agent** — an Agent's execution span, aggregates token usage and call counts
- **step** — a ReAct round (LLM reasoning → optional tool call), synthesized by
  this SDK (the Agents SDK itself does not produce step spans)
- **chat** — one LLM API call: model, input/output messages, tokens, finish_reason, TTFT
- **tool** — one tool/function call: name, arguments, result, error classification

> **Parent-child relationships come solely from `span.parent_id`.** The SDK
> maintains no "currently active agent" global state, so parallel tool calls and
> `as_tool` nested sub-agents are correctly attributed without interference.

---

## Key Metrics

| Attribute | Span | Description |
| --- | --- | --- |
| `gen_ai.usage.input_tokens` / `output_tokens` | chat / agent | Token usage (per-call for chat; cumulative for agent) |
| `gen_ai.response.time_to_first_token_ms` | chat | TTFT — collected for both streaming and non-streaming |
| `gen_ai.response.finish_reasons` | chat | Normalized finish reason (`stop` / `length` / `tool_calls` / `content_filter` / `error`) |
| `gen_ai.response.model` / `gen_ai.request.model` | chat | Model name |
| `gen_ai.agent.message_count` / `tool_call_count` | agent | LLM / tool call counts for this agent |
| `gen_ai.tool.error.type` / `error.message` | tool | Error classification (`timeout` / `rate_limit` / `auth_error` / `connection_error` / `not_found` / `validation_error` …) |

Errors **bubble up automatically**: when a chat/tool span fails, parent step
and agent spans are also set to `ERROR`, ensuring agent-level searches in CLS
hit failed traces.

---

## Local Debugging

Verify collected data without uploading to CLS — enable local dump via `CLSConfig`:

```python
from tencentcloud_agentobs_sdk_openai_agent import setup, CLSConfig

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

Or via environment variables:

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

Each batch is appended as JSON Lines before upload. This is a sidecar
capability — file write failures are logged but do not affect uploads.

---

## Error Handling & Reliability

- **Retry strategy**: `5xx` / network errors return spans to the buffer for the
  next flush; `4xx` (400/401/403/404/413) are discarded (retrying won't help)
- **Backpressure protection**: buffer exceeding 10,000 entries drops the oldest,
  preventing OOM
- **Thread safety**: all buffer operations are lock-protected
- **Fallback close**: on trace end or processor shutdown, all unclosed spans
  (e.g. cancelled tasks) are force-closed to prevent span leaks

---

## License

Apache-2.0
