Metadata-Version: 2.4
Name: ennew-script-observability
Version: 0.3.0
Summary: ENNEW Script Observability SDK: structured reporting for Codex-executed Python scripts (Langfuse v4 backend)
License: Proprietary
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: langfuse<5,>=4.7

# ENNEW Script Observability SDK

面向 Codex Python skill 的结构化观测 SDK。业务代码用接近普通日志的方式写入，
SDK 自动完成 Trace/Session 关联、Langfuse 初始化、脱敏、异常栈与进程退出 flush。

## 最简用法

```python
import logging

from ennew_script_observability import log, skill_entry
from ennew_script_observability import observe


logger = logging.getLogger(__name__)


@observe("provider.unpaywall")
def fetch_paper(doi: str):
    logger.info("开始获取论文", extra={"provider": "unpaywall"})
    return fetch_from_unpaywall(doi)


@skill_entry
def main() -> None:
    log.input("脚本输入", doi="10.1000/x", provider="unpaywall")
    result = fetch_paper("10.1000/x")
    log.output("脚本结果", success=True)
```

推荐优先使用三个入口：

- `@skill_entry`：装饰唯一入口，自动创建和结束根 Trace。
- `@observe("稳定名称")`：自动记录函数参数、返回值、耗时和异常。
- Python 原生 `logging`：照常使用 `logger.info/warning/error/exception`，SDK 在
  `@skill_entry` 运行期间自动桥接到当前 Langfuse 节点，并保留控制台日志。

复杂流程再使用 `log.input()`、`log.output()` 和 `log.step()` 手动控制节点。

`log.input()` 和 `log.output()` 会自动判断当前上下文：在 `log.step()` 外调用时
写入根 `script.run`，在步骤内调用时写入当前步骤。每次写入还会生成一个轻量的
`script.input/output` 或 `step.input/output` 事件。事件在 Log View 中直接显示为
`step.input · 请求参数`、`script.log · 开始获取论文` 这类日志行；稳定事件键仍保存
在 metadata 的 `event.key` 字段中，便于过滤和查询。

结构化字段、纯文本和任意 JSON 值都支持：

```python
log.input("查询参数", query="机理算法", top_k=3)
log.input("一段纯文本")
log.output("文档列表", [{"id": 1}, {"id": 2}])
log.output("模型回答", "共找到 2 篇文档")
```

错误也保持日志式写法：

```python
# 业务错误：自动关联当前 step，并更新根 Run 状态
log.error("资源不存在", code="PAPER-404", recoverable=False, doi=doi)

# 已捕获代码异常：在 except 块中调用，SDK 自动读取当前异常和 stacktrace
try:
    fetch()
except TimeoutError:
    log.exception("首次请求超时，准备重试", provider="arxiv", attempt=1)
```

## 固定 API

| API | 语义 | Langfuse 映射 |
|---|---|---|
| `@skill_entry` | 脚本生命周期 | 根 `tool` observation / 独立 Trace |
| `@skill_entry(capture_args=True, capture_result=True)` | 自动记录入口参数和返回值 | root input/output |
| `@observe(name, kind=..., capture_args=..., capture_result=...)` | 自动观测同步或异步函数 | child observation |
| `logger.info/warning/error/exception(...)` | Python 原生日志 | 当前节点 event；消息直接写入 event input |
| `log.input(message?, value?, **fields)` | 当前上下文输入，每个节点最多一次 | root/child input + 轻量 event |
| `log.output(message?, value?, **fields)` | 当前上下文输出，每个节点最多一次 | root/child output + 轻量 event |
| `log.step(name, kind="tool", **fields)` | 有耗时的业务步骤 | child observation |
| `log.result(**fields)` | 兼容旧 API，始终写根结果 | root output |
| `step.output(**fields)` | 兼容旧 API，写当前步骤结果 | child output |
| `log.debug/info/warning(message, event=..., **fields)` | 结构化业务日志 | event |
| `log.error(message, code=..., recoverable=..., **fields)` | 业务失败 | WARNING/ERROR event + 状态 |
| `log.exception(message, **fields)` | `except` 中的代码异常 | ERROR event + stacktrace |

`event` 可省略，SDK 会按日志级别使用稳定默认名。只有需要按业务事件单独筛选时才传。
`kind` 只在步骤确实属于 `retriever` 或 `generation` 时设置，默认 `tool`。
`value` 与 `**fields` 二选一，不能在同一次调用中混用。

原生日志的消息和 `extra` 字段会合并写入 Event Input：

```python
logger.info("检索完成", extra={"count": 3, "provider": "crossref"})

try:
    fetch()
except TimeoutError:
    logger.exception("请求超时")
```

在 Langfuse 中分别显示为 `INFO · 检索完成` 和 `ERROR · 请求超时`。第一条日志的
Input 是 `{"message":"检索完成","count":3,"provider":"crossref"}`；Metadata
只保留 `event.key`、logger 名称和级别。异常类型、异常消息与 stacktrace 也保存在
Metadata。

## Langfuse 自动初始化

SDK 与 Codex Langfuse 插件读取同一配置，优先级由低到高：

1. SDK 默认值；
2. `~/.codex/langfuse.json`；
3. `<cwd>/.codex/langfuse.json`；
4. 环境变量。

示例配置：

```json
{
  "enabled": true,
  "public_key": "pk-lf-...",
  "secret_key": "sk-lf-...",
  "base_url": "https://langfuse.example.com",
  "environment": "cloud-codex",
  "user_id": "codex-cloud",
  "tags": ["codex", "skill"],
  "metadata": {"deployment": "cloud"},
  "debug": false,
  "fail_on_error": false
}
```

环境变量同时支持插件专用名和 Langfuse 标准名；前者优先：

| 插件专用名 | 标准名 |
|---|---|
| `LANGFUSE_CODEX_PUBLIC_KEY` | `LANGFUSE_PUBLIC_KEY` |
| `LANGFUSE_CODEX_SECRET_KEY` | `LANGFUSE_SECRET_KEY` |
| `LANGFUSE_CODEX_BASE_URL` | `LANGFUSE_BASE_URL` |
| `LANGFUSE_CODEX_ENVIRONMENT` | `LANGFUSE_TRACING_ENVIRONMENT` |
| `LANGFUSE_CODEX_TAGS` | `LANGFUSE_TAGS` |
| `LANGFUSE_CODEX_METADATA` | `LANGFUSE_METADATA` |

业务脚本不需要读取配置文件、传凭据或创建 Langfuse 客户端。

## 执行上下文

执行层只需给子进程注入：

- `CODEX_THREAD_ID`：Langfuse `session_id`，云端使用时必需；
- `CODEX_TURN_ID` / `CODEX_CALL_ID`：推荐，写入 metadata；
- `ENNEW_SCRIPT_TRIGGER_SOURCE`：`skill` / `agents_md` / `manual`；
- `SKILL_NAME` / `SKILL_VERSION`：skill 标识。

`ENNEW_SCRIPT_OBSERVABILITY_STRICT=true` 或配置中的 `fail_on_error=true` 会在缺少
上下文、凭据或初始化失败时直接报错；默认模式则 fail-open，不影响业务脚本。

## 开发与验证

```bash
python3.12 -m venv .venv
.venv/bin/pip install -e . pytest
.venv/bin/python -m pytest tests/unit
.venv/bin/ennew-script-observability lint ../local-e2e/skills/ennew-observability-mock/scripts
```

本地构建和发布到 PyPI 的完整流程见 [PUBLISHING.md](PUBLISHING.md)。
