Skip to content
Merged
108 changes: 108 additions & 0 deletions docs/middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Runtime Middleware Adapters

Gradata's hooks only fire inside Claude Code. For direct-SDK agents
(raw OpenAI SDK, raw Anthropic SDK, LangChain, CrewAI), the
`gradata.middleware` subpackage provides runtime wrappers that inject
learned rules into system prompts and optionally enforce RULE-tier regex
patterns on outputs.

## Common behavior

All adapters share one rule source: the same `lessons.md` + brain
database Claude Code hooks use. Selection, confidence floor, and the
`<brain-rules>` XML format match `gradata.hooks.inject_brain_rules`.

- **Cap**: 10 rules per call (configurable via `RuleSource(max_rules=N)`).
- **Priority**: RULE > PATTERN, ties broken by confidence descending.
- **Strict mode**: `strict=False` (default) logs violations; `strict=True`
raises `gradata.middleware.RuleViolation` so callers can retry.
- **Kill switch**: set `GRADATA_BYPASS=1` to disable all injection and
enforcement.
- **Optional deps**: importing `AnthropicMiddleware`, `OpenAIMiddleware`,
`LangChainCallback`, or `CrewAIGuard` without their respective third-party
package raises a clear `ImportError` with an install hint.

## Anthropic

```python
from anthropic import Anthropic
from gradata.middleware import wrap_anthropic

client = wrap_anthropic(Anthropic(), brain_path="./brain")
# ... all client.messages.create(...) calls now get rules injected
resp = client.messages.create(
model="claude-sonnet-4-5",
messages=[{"role": "user", "content": "Write a short greeting"}],
max_tokens=128,
)
```

The wrapper mutates only the `system` kwarg (string or content-block
list) and post-checks the response's text blocks.

## OpenAI

```python
from openai import OpenAI
from gradata.middleware import wrap_openai

client = wrap_openai(OpenAI(), brain_path="./brain")
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Write a short greeting"}],
)
```

Rules land in the leading system message — extending it if present,
prepending a new one otherwise.

## LangChain

```python
from langchain_openai import ChatOpenAI
from gradata.middleware import LangChainCallback

llm = ChatOpenAI(callbacks=[LangChainCallback(brain_path="./brain")])
llm.invoke("Write a short greeting")
```

Implements `BaseCallbackHandler` with hooks on
`on_llm_start` / `on_chat_model_start` for injection and `on_llm_end` for
enforcement.

## CrewAI

```python
from crewai import Agent
from gradata.middleware import CrewAIGuard

guard = CrewAIGuard(brain_path="./brain", strict=True)
agent = Agent(
role="Writer",
goal="Draft clean prose",
backstory="...",
guardrails=[guard],
)
```

The guard returns `(True, output)` when clean and
`(False, "Gradata rule violation(s): ...")` when strict and a RULE-tier
pattern matches — CrewAI then retries.

## Advanced: custom rule source

If your lessons live somewhere other than `<brain_path>/lessons.md`,
construct a `RuleSource` directly:

```python
from gradata.middleware import RuleSource, wrap_anthropic
from anthropic import Anthropic

source = RuleSource(
lessons=[
{"state": "RULE", "confidence": 0.95, "category": "TONE",
"description": "Never use em dashes in prose"},
],
)
client = wrap_anthropic(Anthropic(), source=source, strict=True)
```
83 changes: 83 additions & 0 deletions src/gradata/middleware/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Runtime middleware adapters for non-Claude-Code environments.

Gradata's hooks only fire inside Claude Code. For direct-SDK agents
(raw OpenAI SDK, raw Anthropic SDK, LangChain, CrewAI) this subpackage
provides runtime wrappers that inject learned rules into system prompts
and enforce RULE-tier patterns on outputs.

Quick start:

from anthropic import Anthropic
from gradata.middleware import wrap_anthropic

client = wrap_anthropic(Anthropic(), brain_path="./brain")
# All client.messages.create(...) calls now get rules injected.

The adapters share a common :class:`RuleSource` that reads from the same
``lessons.md`` + brain database that Claude Code hooks use, so behaviour
is consistent across environments.

Environment overrides:
GRADATA_BYPASS=1 — disables all injection and enforcement (emergency kill switch).

Optional deps:
- AnthropicMiddleware / wrap_anthropic -> ``anthropic``
- OpenAIMiddleware / wrap_openai -> ``openai``
- LangChainCallback -> ``langchain-core``
- CrewAIGuard -> works with plain CrewAI guardrails

Importing an adapter without its optional dep raises a clear ImportError
with the install hint.
"""

from __future__ import annotations

from gradata.middleware._core import (
RuleSource,
RuleViolation,
build_brain_rules_block,
check_output,
is_bypassed,
)

# Adapters are exposed via lazy __getattr__ so importing the package
# doesn't require anthropic / openai / langchain / crewai to be installed.

__all__ = [ # noqa: RUF022 — logical grouping (core -> adapters) over alphabetical
"RuleSource",
"RuleViolation",
"build_brain_rules_block",
"check_output",
"is_bypassed",
# Lazy exports — see __getattr__
"AnthropicMiddleware",
"OpenAIMiddleware",
"LangChainCallback",
"CrewAIGuard",
"wrap_anthropic",
"wrap_openai",
]


# name -> (submodule, attribute) for lazy adapter loading.
_LAZY_EXPORTS = {
"AnthropicMiddleware": ("anthropic_adapter", "AnthropicMiddleware"),
"wrap_anthropic": ("anthropic_adapter", "wrap_anthropic"),
"OpenAIMiddleware": ("openai_adapter", "OpenAIMiddleware"),
"wrap_openai": ("openai_adapter", "wrap_openai"),
"LangChainCallback": ("langchain_adapter", "LangChainCallback"),
"CrewAIGuard": ("crewai_adapter", "CrewAIGuard"),
}


def __getattr__(name: str): # pragma: no cover - trivial dispatch
try:
module_name, attr_name = _LAZY_EXPORTS[name]
except KeyError:
raise AttributeError(
f"module {__name__!r} has no attribute {name!r}",
) from None
import importlib

module = importlib.import_module(f"{__name__}.{module_name}")
return getattr(module, attr_name)
Loading
Loading