Metadata-Version: 2.4
Name: chatsee-ai
Version: 0.13.0
Summary: A Python SDK for Chatsee AI.
Maintainer: ChatSee
Maintainer-email: contact@chatsee.ai
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31.0
Provides-Extra: closed-loop
Requires-Dist: mcp>=1.2.0; extra == "closed-loop"
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: maintainer
Dynamic: maintainer-email
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# Chatsee SDK (Python)

Instrument an LLM agent so every turn arrives at ChatSee as a trace: what the
user asked, what the agent answered, which tools ran, how long each one took,
what failed, and who it was for.

```bash
pip install chatsee-ai
```

## Quick start

```python
from chatsee import ChatseeTracker

tracker = ChatseeTracker(
    agent_id="agent_1",
    tenant_id="tenant_1",
    api_base_url="dev",     # environment alias, or a full URL
    user_id="u_123",        # optional: who the end user is
)

tracker.start_turn("Where is my order?")

with tracker.track_tool_call("lookup_order", {"id": "A-91"}) as call:
    call["result"] = lookup_order("A-91")

tracker.end_turn("It ships tomorrow.")
```

One `start_turn` … `end_turn` pair is one turn. Everything logged in between —
tool calls, exceptions, metadata — belongs to that turn and is sent in a single
request when it closes.

## What gets captured

| Captured | How |
|----------|-----|
| User and bot message | `start_turn` / `end_turn` |
| End user identity | `user_id` (see below) |
| Session grouping | `session_id`, or the API generates one |
| Tool calls: name, arguments, result, error | `track_tool_call` / `log_tool_call` |
| **Per-tool duration** | measured automatically by `track_tool_call` |
| **Turn duration** | measured between `start_turn` and `end_turn` |
| **Token usage and cost** | `log_model_call` (see below) |
| Errors | `log_exception`, or any exception raised inside `track_tool_call` |
| System prompt | `start_turn(system_prompt=...)` |
| Anything else | `start_turn(metadata={...})` |

Durations are milliseconds of wall clock. When a duration was not measured it is
sent as absent, never as `0` — downstream latency figures are built from real
measurements only, so an unmeasured call is excluded rather than counted as
instant.

## Timing tool calls

`track_tool_call` is the recommended form: it times the call, records the
result, and still logs the call if the tool raises before re-raising to you.

```python
with tracker.track_tool_call("search", {"q": q}) as call:
    call["result"] = search(q)          # exceptions here are logged, then re-raised
```

If you already have the timing, or the call happened elsewhere, log it directly:

```python
tracker.log_tool_call("search", {"q": q}, result=hits, duration_ms=412)
tracker.log_tool_call("refund", {"id": "A-91"}, error="gateway timeout")
```

Both feed the per-tool metrics the ChatSee front end reads — call counts, error
rate, and p50/p95 latency per tool.

## Recording token usage

The SDK never sees your model calls, so it cannot count tokens for you — hand it
the usage object the provider already returned, once per call:

```python
resp = client.chat.completions.create(model="gpt-4o", messages=msgs)
tracker.log_model_call(model="gpt-4o", provider="openai", usage=resp.usage)
```

The field names are mapped for you, so the object goes in as it comes out:
`resp.usage` for OpenAI and Anthropic, `resp.usage_metadata` for Gemini. Pass the
numbers directly instead if you already have them:

```python
tracker.log_model_call(model="gpt-4o", prompt_tokens=1180, completion_tokens=240)
```

Log every call the turn made — a retry, a router call, a summarizer — and ChatSee
reports the turn's total from the sum. It also reports how many of those calls
reported usage at all, which is what stops an unmeasured turn being read as a
cheap one: a turn where nothing reported is shown as *unavailable*, not as zero
tokens. `cost_usd` is accepted but never inferred; omit it unless the provider
priced the call.

## Identifying the end user

`user_id` is a first-class field, so traces can be filtered and grouped by
person rather than by conversation. It is optional — when nothing is supplied,
ingestion assigns a deterministic anonymous id per session — but a real id is
what lets you follow one user across sessions.

```python
tracker = ChatseeTracker(..., user_id="u_123")     # one tracker, one user
tracker.start_turn("Hello", user_id="u_456")       # one tracker, many users
```

Precedence: `start_turn(user_id=...)`, then the tracker-level `user_id`, then
`metadata["user_id"]` — the older metadata form keeps working as a fallback.

## Environments

Select an environment by passing an alias as `api_base_url`, or any full URL.

| Alias | URL |
|-------|-----|
| `dev` | `https://dev-react.chatsee.ai/api` |
| `dev-legacy` | `https://dev.chatsee.ai/api` |
| `qa` (default) | `https://qa.chatsee.ai/api` |
| `demo` | `https://gcp-demo.chatsee.ai/api` |
| `poc` | `https://app.chatsee.ai/api` |

Redaction classifiers are fetched from the same environment
(`…/v1/redaction/fetch-classifiers`); override with `redaction_classifiers_url`.

## Closing a conversation early

Conversations are normally flushed by an inactivity timer. If you know a turn is
the last one, say so and it flushes immediately:

```python
tracker.end_turn("Glad I could help.", is_final_turn=True)
```

## Batching

Send many turns as one API call — one Processor run instead of one per turn.
Useful for backfills and imports.

```python
tracker.send_batch([
    {"user_message": "hi", "bot_message": "hello", "session_id": "s1"},
    {"user_message": "thanks", "bot_message": "any time", "session_id": "s1"},
])
```

## Redaction

Redaction runs client-side, before anything leaves the process. Classifiers come
from your environment and are cached.

```python
tracker = ChatseeTracker(..., redaction_enabled=True)   # redacts each turn
```

Or redact a payload on its own, without a tracker:

```python
from chatsee import redact

redact({"message": "Card 4111 1111 1111 1111"}, api_base_url="qa", fields_to_redact="*")
```

Defaults to `user_message`, `bot_message` and `interactions`; set
`redaction_fields_to_redact` (or `fields_to_redact="*"`) to change that.

## Closed-loop remediations

Pull remediation skills for this agent and acknowledge the ones you have
injected into its system prompt. Requires `mcp_server_url` and `mcp_api_key`;
tenant and agent are resolved server-side from the key.

```python
tracker = ChatseeTracker(..., mcp_server_url="https://…", mcp_api_key="…")

pending = tracker.fetch_remediations()                 # or mode="all"
tracker.acknowledge_remediations([r["id"] for r in pending["remediations"]])
```

## Track Claude Code (adapter)

Claude Code is a closed CLI, so rather than instrumenting it the adapter reads
its session logs, reconstructs each turn and sends it through the SDK.

```bash
# preview (sends nothing)
python -m chatsee.adapters.claude_code --latest --dry-run

# one-time hook install -> every completed turn auto-streams, no terminal needed
python -m chatsee.adapters.claude_code --install-hook \
  --agent-id <AGENT_ID> --tenant-id <TENANT_ID> --env dev

# remove it
python -m chatsee.adapters.claude_code --uninstall-hook
```

Alternatives to the hook: `--watch` (foreground tail) or a one-shot run. A
per-session checkpoint (`~/.chatsee/claude_code_state.json`) prevents duplicates.

**Redaction is on by default here.** Claude Code turns — tool arguments and
results especially — often contain secrets and PII, so the adapter redacts
client-side before sending (`user_message`, `bot_message`,
`tool_calls_details`, `exception`, `system_prompt`). Change the set with
`--redact-fields a,b,c`, or disable with `--no-redact` (not recommended). The
setting is preserved in installed hooks.

## Notes

- `agent_id` and `tenant_id` are required; there is no API key on the tracking path.
- `verify_ssl` defaults to `False` for the internal environments; set `True` for public ones.
- Encryption support has been removed from this SDK.
