Metadata-Version: 2.4
Name: integritty-sdk
Version: 0.1.0
Summary: LLM Observability SDK
License-Expression: Apache-2.0
License-File: LICENSE
Author: jaya
Author-email: jaya@integritty.com
Requires-Python: >=3.10,<4.0.0
Classifier: License :: OSI Approved :: Apache Software License
Provides-Extra: all
Provides-Extra: anthropic
Provides-Extra: bedrock
Provides-Extra: crewai
Provides-Extra: google-adk
Provides-Extra: langchain
Provides-Extra: mongodb
Provides-Extra: mssql
Provides-Extra: mysql
Provides-Extra: openai
Provides-Extra: requests
Provides-Extra: threading
Provides-Extra: urllib
Requires-Dist: anthropic (>=0.40.0,<2.0.0) ; extra == "anthropic"
Requires-Dist: boto3 (>=1.42.54,<2.0.0) ; extra == "bedrock"
Requires-Dist: crewai (>=0.86.0,<2.0.0) ; extra == "crewai"
Requires-Dist: google-adk (>=1.2.1,<3.0.0) ; extra == "google-adk"
Requires-Dist: integritty-sdk[anthropic,bedrock,crewai,google-adk,langchain,mongodb,mssql,mysql,openai,requests,threading,urllib] ; extra == "all"
Requires-Dist: langchain-core (>=0.3.0,<2.0.0) ; extra == "langchain"
Requires-Dist: langchain-openai (>=0.2.0,<2.0.0) ; extra == "langchain"
Requires-Dist: openai (>=1.0.0,<3.0.0) ; extra == "openai"
Requires-Dist: opentelemetry-api (>=1.39.1,<2.0.0)
Requires-Dist: opentelemetry-exporter-otlp (>=1.39.1,<2.0.0)
Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.39.1,<2.0.0)
Requires-Dist: opentelemetry-instrumentation (>=0.60b1,<0.62)
Requires-Dist: opentelemetry-instrumentation-mysql (>=0.60b1,<0.62) ; extra == "mysql"
Requires-Dist: opentelemetry-instrumentation-pymongo (>=0.61b0,<0.62) ; extra == "mongodb"
Requires-Dist: opentelemetry-instrumentation-pymysql (>=0.60b1,<0.62) ; extra == "mysql"
Requires-Dist: opentelemetry-instrumentation-requests (>=0.60b1,<0.62) ; extra == "requests"
Requires-Dist: opentelemetry-instrumentation-threading (>=0.60b1,<0.62) ; extra == "threading"
Requires-Dist: opentelemetry-instrumentation-urllib (>=0.60b1,<0.62) ; extra == "urllib"
Requires-Dist: opentelemetry-sdk (>=1.39.1,<2.0.0)
Requires-Dist: pymssql (>=2.2.0) ; extra == "mssql"
Requires-Dist: pyodbc (>=4.0.0) ; extra == "mssql"
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
Requires-Dist: wrapt (>=1.0.0,<3.0.0) ; extra == "bedrock"
Description-Content-Type: text/markdown

# Integritty SDK

LLM Observability SDK built on OpenTelemetry. Instruments LangChain, AWS Bedrock, OpenAI, Anthropic, Google ADK, CrewAI, and common databases/HTTP clients.

## License

Licensed under the [Apache License, Version 2.0](LICENSE).
---

## Requirements

- Python `>=3.10, <4.0`

## Tested Versions

| Package | Tested Version |
|---|---|
| `opentelemetry-api` / `opentelemetry-sdk` | `>=1.39.1,<2.0.0` |
| `opentelemetry-exporter-otlp` | `>=1.39.1,<2.0.0` |
| `opentelemetry-instrumentation` | `>=0.60b1,<0.62` |
| `python-dotenv` | `>=1.0.0,<2.0.0` |
| `langchain-core` | `>=0.3.0,<2.0.0` |
| `langchain-openai` | `>=0.2.0,<2.0.0` |
| `boto3` (Bedrock) | `>=1.42.54,<2.0.0` |
| `wrapt` (Bedrock) | `>=1.0.0,<3.0.0` |
| `openai` | `>=1.0.0,<3.0.0` |
| `anthropic` | `>=0.80.0,<2.0.0` |
| `google-adk` | `>=1.2.1,<3.0.0` |
| `crewai` | `>=0.86.0,<2.0.0` |
| `opentelemetry-instrumentation-mysql` | `>=0.60b1,<0.62` |
| `opentelemetry-instrumentation-pymysql` | `>=0.60b1,<0.62` |
| `opentelemetry-instrumentation-pymongo` | `>=0.61b0,<0.62` |
| `pymssql` | `>=2.2.0` |
| `pyodbc` | `>=4.0.0` |
| `opentelemetry-instrumentation-urllib` | `>=0.60b1,<0.62` |
| `opentelemetry-instrumentation-requests` | `>=0.60b1,<0.62` |
| `opentelemetry-instrumentation-threading` | `>=0.60b1,<0.62` |

Below the minimum version, that integration's `instrument()` becomes a no-op (a warning is logged, everything else keeps working). `anthropic`, `openai`, `google-adk`, and `crewai` versions are actively version-gated at runtime — psycopg2/psycopg3/asyncpg and pymssql/pyodbc are patched manually and are not version-gated the same way.

---

## Installation

### 1. Install the SDK

```bash
pip install integritty-sdk
```

Or with Poetry:

```bash
poetry add integritty-sdk
```

### 2. Set environment variables

Create a `.env` file or export variables in your shell:

```env
# OTLP-compatible backend
OTEL_EXPORTER_OTLP_ENDPOINT= 

# Headers
OTEL_EXPORTER_OTLP_HEADERS=x-integritty-key=itg_.......

OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
```

### 3. Initialize in your application

Call `Integritty.init()` once at application startup, before any LLM or database calls.

```python
from integritty import Integritty

Integritty.init(service_name="my-app")
```

---

## What Gets Auto-Instrumented

Once `Integritty.init()` is called, the following are traced automatically with no additional code:

- **LangChain** — chains, agents, tools, LLM calls, streaming (this also covers **LangGraph** — a compiled graph runs on LangChain's Runnable interface, so its nodes are traced automatically with no separate integration or install step; `disable_langchain=True` disables both, there is no separate `disable_langgraph` flag)
- **AWS Bedrock** — hosted and runtime-hosted agents
- **OpenAI** — chat completions, embeddings
- **Anthropic** — messages API, tool use, streaming
- **Google ADK** (`google-adk`) — agents and tool calls
- **CrewAI** — crews, agents, tasks
- **MySQL** (`mysql.connector`, `pymysql`)
- **PostgreSQL** — psycopg2, psycopg3, asyncpg
- **MongoDB** (`pymongo`)
- **MSSQL** (`pymssql`, `pyodbc`)
- **HTTP** — `urllib` and `requests`-based outbound calls

Each integration can be turned off individually via `Integritty.init()` flags, e.g. `Integritty.init(service_name="my-app", disable_crewai=True, disable_requests=True)`. Flags follow the pattern `disable_<name>`: `disable_langchain`, `disable_bedrock`, `disable_openai`, `disable_anthropic`, `disable_google_adk`, `disable_crewai`, `disable_mysql`, `disable_pymysql`, `disable_psycopg2`, `disable_psycopg3`, `disable_asyncpg`, `disable_pymongo`, `disable_pymssql`, `disable_pyodbc`, `disable_requests`.

---

## Decorators

### `@Integritty.traced` (Bedrock — not needed for LangChain)

Wraps a function in a root span. All child operations (LLM, DB, etc.) become children of that span. Flushes traces on exit. Not required for a single Bedrock hosted-agent call — `invoke_agent`/`converse` already get their own span automatically, and `Integritty.init()` flushes on process exit regardless. Use it when a script makes **multiple** Bedrock/LLM calls that should nest under one shared trace instead of showing up as separate root spans. For AgentCore Runtime (**runtime-hosted**) apps, use `@Integritty.entrypoint` below instead.

```python
from integritty import Integritty

Integritty.init(service_name="my-app")

@Integritty.traced(span_name="my_agent_run")
def main(): 
 ask_question("What is RAG?") 

```

### `@Integritty.workflow` (Bedrock / OpenAI / general — not for LangChain)

Alias for `@Integritty.traced` — marks the top-level entry point of a request or workflow. Useful when you have multiple Bedrock or LLM calls inside one request handler.

```python
@Integritty.workflow(name="handle_chat")
def handle_chat():
    client.converse(...)
    client.invoke_agent(...)
```

Note: `name=` here sets the span/workflow name shown in traces — it is not a place to pass a user identifier. To attach `user_id`/`user_name` (or `session_id`/`conversation_id`) to a request, use `Integritty.request_context(...)` — see "Passing User / Session / Conversation IDs" below.

### `@Integritty.entrypoint(app)` (advanced — async request-handler frameworks)

**Not** an alias of `@Integritty.traced`/`@Integritty.workflow` — it's a separate, async-only decorator for frameworks whose own app object exposes an `.entrypoint(fn)` decorator (e.g. an AWS Bedrock AgentCore Runtime `app`). It wraps your async handler in a root span, seeds LangChain's tracing context so any `llm.ainvoke`/`llm.astream` call inside nests correctly under that root span, flushes on exit, and then registers the wrapped function with `app.entrypoint(...)`.

```python
@Integritty.entrypoint(app, span_name="handle_request")
async def handle_request(request):
    return await client.converse(...)
```

**Required, not just convenient, when your entrypoint calls LangChain/LangGraph internally** (e.g. a supervisor routing to specialist agents): LangChain's instrumentation tracks trace continuity through its own context variables, which reset on every fresh request. Without this decorator seeding them first, the LangChain call opens a disconnected second trace instead of nesting under the request's root span.

If your entrypoint only makes plain Bedrock/OpenAI/Anthropic calls (no LangChain involved), it's optional — each call already gets its own span automatically the same way it does for hosted agents. In that case use it only to group multiple calls under one shared trace, the same tradeoff as `@Integritty.workflow`.

For most non-LangChain use cases (plain scripts/services, no framework with its own `.entrypoint`), prefer `@Integritty.workflow` above.

### `@Integritty.task` / `@Integritty.tool` — manual flow tracing (no framework)

When your application does **not** use LangChain, CrewAI, or another auto-instrumented framework to orchestrate its logic (e.g. a plain Python service calling an LLM SDK and your own functions directly), there is no framework hooking your function calls into spans. Use `@Integritty.workflow` to mark the entry point, then `@Integritty.task` / `@Integritty.tool` to mark the steps inside it, so the trace still shows a proper parent/child structure instead of one flat span.

- **`@Integritty.workflow`** — the root span for the whole request/run. Flushes on exit. Use once, at the top-level function.
- **`@Integritty.task`** — a child span for an internal step/business-logic function (e.g. "fetch context", "build prompt", "post-process"). No flush — it's meant to run inside a workflow.
- **`@Integritty.tool`** — a child span for a tool/function call (e.g. a function the LLM calls, or a call to an external API/service). Sets `gen_ai.operation.name=execute_tool` in addition to the same input/output capture as `task`.

Both `@Integritty.task` and `@Integritty.tool` automatically capture the function's input arguments, return value, and any raised exception as span attributes — you don't need to set them manually. They nest under whatever span is currently active, so call them from inside a `@Integritty.workflow`-decorated function (or an `Integritty.span()` block).

```python
from integritty import Integritty

@Integritty.workflow(name="handle_chat")
def handle_chat(message: str):
    context = fetch_context(message)   # task
    return call_agent(context)         # tool

@Integritty.task(name="fetch_context")
def fetch_context(message: str):
    return db.search(message)

@Integritty.tool(name="call_agent")
def call_agent(context):
    return client.invoke_agent(context=context)
```

`@Integritty.tool` also works as a bare decorator or a direct wrapper, without requiring the parentheses form:

```python
@Integritty.tool
def search_kb(query):
    ...

search = Integritty.tool(search_fn, name="search_kb")
```

### `@Integritty.with_agent_name` (all adapters — LangChain, Bedrock, OpenAI, Anthropic, Google ADK, CrewAI)

Sets a fallback agent name that every adapter's instrumentation reads and stamps onto its own spans as `gen_ai.agent.name` whenever the framework itself doesn't supply one (e.g. no `run_name` in a LangChain invoke config). It works the same way regardless of which adapter your app uses — you don't need a different decorator per provider.

- **LangChain** — used as the fallback chain/agent span name when `run_name`/`metadata.agent_name` isn't provided.
- **Bedrock** — sets `gen_ai.agent.name` on every Bedrock span created inside the decorated function (hosted or runtime-hosted agents).
- **Anthropic / OpenAI** — in addition to the fallback name, also opens a dedicated parent `AGENT` span (`gen_ai.operation.name=invoke_agent`) around the decorated function, so orchestrator-style code that calls out to sub-agents shows up correctly in the trace.
- **Google ADK / CrewAI** — sets `gen_ai.agent.name` as a fallback on spans created inside the decorated function, same as Bedrock.

**LangChain**

```python
from integritty import Integritty

# no need to use decorator if you are already passing in metadata or if you are creating agent with create_agent.
@Integritty.with_agent_name("MyAgent")
def handle_chat(message):
    return agent.invoke({"messages": [{"role": "user", "content": message}]})
```

**Bedrock (Hosted Agents)**

Overrides the default alphanumeric `agentId` with a readable name on every span the call produces — works the same way for streaming and non-streaming `invoke_agent` calls.

```python
from integritty import Integritty

@Integritty.with_agent_name("MyBedrockAgent")
def handle_chat(message):
    return client.converse(
        modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
        messages=[{"role": "user", "content": [{"text": message}]}],
    )
```

**Bedrock (Runtime-Hosted Agents)**

Use it when no meaningful agent name can be inferred automatically — e.g. there's no graph node to read a name from. If a node already has a string name (`graph.add_node("addition", fn)`), the SDK captures that automatically and the decorator isn't needed.

```python
from integritty import Integritty

@Integritty.with_agent_name("multiply")
def multiplication_agent(state):
    result = llm.invoke(state["messages"])
    return {**state, "answer": result.content}
```

For streaming nodes, apply it to a regular `async def` function that calls `llm.astream` internally — not directly on an async generator (one that `yield`s), since the decorator resets the name in a `finally` block that fires before the first chunk is produced.

**OpenAI / Anthropic / Google ADK / CrewAI**

```python
from integritty import Integritty

@Integritty.with_agent_name("MyAgent")
def handle_chat(message):
    return client.chat.completions.create(   # or anthropic client.messages.create(...),
        model="gpt-4o",                      # or a Google ADK / CrewAI call
        messages=[{"role": "user", "content": message}],
    )
```

## Prompt / Response Content Capture (Hiding Sensitive Data)

There are two independent controls — use whichever fits, or both:

### 1. `Integritty.init(capture_message_content=...)`

Passed once to `Integritty.init()`. Controls whether **LangChain, Bedrock, OpenAI, Anthropic, and CrewAI** build the actual prompt/response text into span attributes at all (`gen_ai.prompt.N.content`, `gen_ai.completion.N.content`, etc.). Defaults to `True`. Does **not** apply to Google ADK (its instrumentor always captures; use the env vars below to redact it).

```python
Integritty.init(service_name="my-app", capture_message_content=False)
```

There is no environment variable equivalent for `capture_message_content` itself — it is only settable in code, at `Integritty.init()` call time. If you need to control content capture purely via `.env` without a code change, use control #2 below instead.

### 2. `INTEGRITTY_CAPTURE_INPUT_PROMPTS` / `INTEGRITTY_CAPTURE_OUTPUT` env vars

A global redaction pass applied to **every span from every adapter** (LangChain, Bedrock, OpenAI, Anthropic, Google ADK, CrewAI, plus your own `@Integritty.task`/`@Integritty.tool` spans) right before export — regardless of the `capture_message_content` setting above. Set either to `false` (case-insensitive) in your environment/`.env` file:

```env
# Strip all input prompt content (system/user messages + role labels) from every trace
INTEGRITTY_CAPTURE_INPUT_PROMPTS=false

# Strip all output/response content (assistant replies, tool outputs, task outputs) from every trace
INTEGRITTY_CAPTURE_OUTPUT=false
```

Both default to `true` (capture everything) when unset. This is the recommended way to hide sensitive prompt/response content in production without touching adapter-specific code, since it's enforced centrally in the span pipeline rather than per-instrumentor.

---

## Span Context Managers

For inline span grouping without decorators:

```python
# Sync
with Integritty.span("rag.query"):
    ask_question("What is RAG?")

# Async
async with Integritty.aspan("stream_response"):
    async for chunk in llm.astream(messages):
        yield chunk
```

---

## Passing User / Session / Conversation IDs

### LangChain

Pass `user_id`, `user_name`, and `conversation_id` via the invoke metadata:

```python
# metadata is an optional thing but in case the application is dealing with user id/name/session/conversation id then needs to pass like this only
metadata = {
    "conversation_id": conversation_id,
    "user_id": user_id,       
    "user_name": user_name,    
}

response = agent.invoke(
    {"messages": [{"role": "user", "content": message}]},
    {
        "run_name": "AGENT NAME", # no need of passing here if you are already using @Integritty.with_agent_name or if you are creating agent with create_agent
        "metadata": metadata,
    },
)
```

You can also set these at the request boundary with `Integritty.request_context(...)` (see below) — LangChain's invoke automatically picks up the values from it, so you don't have to thread `metadata` through every call.

### LangGraph

No separate integration — a compiled `StateGraph` is itself a Runnable, so it's traced by the same LangChain instrumentor with no extra install/init step (`Integritty.init(service_name="my-app")` is enough; there is no `disable_langgraph` flag, only `disable_langchain`, which covers both).

Pass `user_id`, `user_name`, and `conversation_id` the exact same way as the LangChain section above — via the invoke config's `metadata`:

```python
response = graph.invoke(
    {"messages": [{"role": "user", "content": message}]},
    {
        "run_name": "AGENT NAME",  # no need if using @Integritty.with_agent_name
        "metadata": {
            "conversation_id": conversation_id,
            "user_id": user_id,
            "user_name": user_name,
        },
    },
)
```

You get one thing for free that plain LangChain doesn't have: LangGraph stamps `langgraph_node` and `graph_id` onto each node's own internal metadata as it runs, and the instrumentation picks these up automatically to name each span after the graph node that produced it (surfaced as `chain.langgraph_node`) — no extra code needed to tell which node in your graph a given LLM call or tool call came from.

### Bedrock (Hosted Agents)

Use `Integritty.request_context()` as a context manager at the request boundary, wrapping the `invoke_agent`/`converse` call:

```python
from integritty import Integritty

with Integritty.request_context(user_id="u123", session_id="s456"):
    response = client.invoke_agent(
        agentId="QNF5HISBHB",
        agentAliasId="V0QTNEJ1BL",
        sessionId="s456",
        inputText=question,
    )
```

### Bedrock (Runtime-Hosted Agents)

Place it inside your `@Integritty.entrypoint`-decorated function, wrapping the graph/chain invocation, so every span produced during that request — supervisor, routing, specialist agents — carries the same IDs:

```python
from integritty import Integritty

@Integritty.entrypoint(app, span_name="math_agent")
async def agent(payload, context):
    with Integritty.request_context(user_id="user-012", user_name="Alice"):
        route = await supervisor_agent(payload["question"])
        return await AGENTS[route](payload["question"])
```

### OpenAI, Anthropic, Google ADK, CrewAI (any other non-LangChain flow)

Use `Integritty.request_context()` as a context manager at the request boundary:

```python
from integritty import Integritty

with Integritty.request_context(user_id="u123", session_id="s456", conversation_id="c789", agent_name="MyAgent"):
    response = client.chat.completions.create(...)
```

All spans created inside the `with` block automatically carry the provided IDs — this is handled identically by the OpenAI, Anthropic, Google ADK, and CrewAI instrumentations (each sets `llm.metadata.<key>` for every key you pass, plus `gen_ai.conversation.id` for `conversation_id`), so one call to `request_context()` covers whichever of these your app uses. `agent_name` is used as a fallback agent name (same effect as `@Integritty.with_agent_name`) when the framework doesn't supply one. Bedrock supports the same `request_context()` mechanics — see the two subsections above for where to place it.

**Note — this `agent_name=` fallback is scoped to OpenAI, Anthropic, Google ADK, CrewAI, and Bedrock, as listed above. It is not documented for LangChain/LangGraph.** For a LangChain/LangGraph flow, set the agent name via `run_name` or `metadata.agent_name` in the invoke config, or the `@Integritty.with_agent_name` decorator (see the LangChain subsection of `@Integritty.with_agent_name` above) — do not assume `request_context(agent_name=...)` carries over to LangChain's chain/agent span name just because `request_context(user_id=...)`/`user_name=`/`conversation_id=` do (those three are separately confirmed to work for LangChain via its invoke `metadata`).

### Combining `@Integritty.workflow` with `request_context`

`request_context()` is always used as a `with` block — never call it as a bare statement, since its `__enter__` is what actually attaches the IDs to the active span context; calling it without `with` does nothing. Inside a `@Integritty.workflow`-decorated entry point, wrap your LLM/agent call in the `with` block exactly as shown below — do not assign the result or call `request_context(...)` on its own line:

```python
from integritty import Integritty

Integritty.init(service_name="my-app")

@Integritty.workflow(name="handle_chat")
def handle_chat(user_name: str, message: str):
    with Integritty.request_context(user_name=user_name):
        response = client.converse(...)
        return response
```
