Metadata-Version: 2.4
Name: badgerflow
Version: 0.3.0
Summary: BadgerFlow Python SDK: governed pro-code agents, the platform REST client, and the bf / agiel CLIs
License-Expression: Apache-2.0
Keywords: langgraph,agents,llm,governance,badgerflow,mlops
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Requires-Dist: pyjwt[crypto]>=2.8
Requires-Dist: openai>=1
Requires-Dist: typer>=0.12
Requires-Dist: pyyaml>=6
Requires-Dist: rich>=13
Requires-Dist: jsonschema>=4.23
Provides-Extra: langgraph
Requires-Dist: langgraph<2,>=1.2; extra == "langgraph"
Requires-Dist: langchain-core<2,>=1.6; extra == "langgraph"
Requires-Dist: langchain-openai<2,>=1.6; extra == "langgraph"
Provides-Extra: otel
Requires-Dist: opentelemetry-sdk>=1.27; extra == "otel"
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27; extra == "otel"
Requires-Dist: opentelemetry-instrumentation-httpx>=0.48b0; extra == "otel"
Requires-Dist: opentelemetry-instrumentation-logging>=0.48b0; extra == "otel"
Provides-Extra: server
Requires-Dist: fastapi>=0.115; extra == "server"
Requires-Dist: uvicorn>=0.30; extra == "server"
Requires-Dist: prometheus_client>=0.20; extra == "server"
Provides-Extra: plugin
Requires-Dist: mcp<3,>=2.2; extra == "plugin"
Requires-Dist: starlette>=0.48; extra == "plugin"
Requires-Dist: uvicorn>=0.31; extra == "plugin"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Provides-Extra: langchain
Requires-Dist: langchain>=0.2; extra == "langchain"
Requires-Dist: langchain-core>=0.2; extra == "langchain"
Provides-Extra: crewai
Requires-Dist: crewai>=0.30; extra == "crewai"
Provides-Extra: autogen
Requires-Dist: pyautogen>=0.2; extra == "autogen"
Dynamic: license-file

# badgerflow

The Python SDK for building **pro-code agents** that run as your own service and
are governed by [BadgerFlow](https://github.com/facileai/agiel) anyway.

You deploy the container. BadgerFlow holds the release, the approval, the
guardrail profile, the spend and the audit trail. Your agent never handles a
credential and is never called by anyone but the platform.

## Install

```bash
pip install 'badgerflow[server,langgraph]'
```

## A LangGraph agent in one command

```bash
bf init claims-triage --langgraph
cd claims-triage
bf dev                      # serve it locally, no platform needed
```

`bf init` writes `agent.py`, `badgerflow.yaml`, a `Dockerfile` and a
`.dockerignore`. The agent is an ordinary `StateGraph`; the only BadgerFlow
addition is a `@node(uses=...)` decorator declaring what each node may touch:

```python
@node(uses=Uses(knowledge=["claims-manual"]), determinism="recorded_effect")
async def retrieve(state: State) -> State:
    chunks = await ctx.knowledge.retrieve(state["input"]["description"], top_k=4)
    return {"excerpts": [c.text for c in chunks]}

app = Agent.from_langgraph(build_graph(), name="claims-triage", version="0.1.0",
                           input=Claim, output=Triage)
```

Those declarations are the contract. The platform compiles them into a graph it
can render — every node, every conditional edge as a router with its branch
labels, and the END your graph declares — checks them against what exists in
your namespace at release time, and denies a node at runtime that reaches for
something it never declared. `bf compile` shows that graph before you register.

`settings` is the half a reviewer reads rather than the platform enforces: what
the node is configured with, shown read-only on its card and inspector.

```python
@node(
    uses=Uses(model="gpt-4o"),
    determinism="recorded_inference",
    settings={"model": "gpt-4o", "temperature": 0, "system_prompt": TRIAGE_SYSTEM,
              "output": "_Triage"},
)
async def triage(state: State) -> State: ...
```

Any JSON object (or a pydantic model) up to 64 KB. It is pinned by the release
hash like the rest of the node's config, so what the approver read is what
runs — and it is declared, because the compiler cannot see a prompt that is
built inside the function.

## Work that outlives a request

An invocation is an HTTP request, and some agents run longer than one should be
held open. Call `ctx.accept()` and the platform stops waiting on the reply:

```python
@app.run
async def triage(ctx: Context, claim: Claim) -> Triage:
    ctx.accept()                 # answered 202; the platform now waits on events
    await slow_work(claim)       # minutes, not seconds
    return Triage(...)           # becomes the run's `complete` event
```

Nothing else changes. `ctx.step`, the governed clients and cancellation behave
identically, and the return value still becomes the run's result — it reaches
the platform as the terminal event rather than as the response body.

Two things to know. The platform's clock keeps running, so silence past the
`timeout_seconds` you registered fails the run; raise it at registration if the
work takes longer. And once you accept, you have accepted: the reply is 202 even
if the handler happens to finish first, so the same code cannot take one
governance path on a fast machine and another on a slow one.

## Human review in a LangGraph agent

Use LangGraph's own `interrupt()` — nothing BadgerFlow-specific:

```python
def gate(state):
    decision = interrupt({"question": f"Approve {state['amount']}?"})
    return {"decision": decision}
```

The run pauses, appears in the platform's Paused Inbox, and resumes when a
reviewer answers; `interrupt()` returns their answer. `interrupt_before=[...]`
works the same way, with the prompt naming the node it stopped before.

LangGraph needs a checkpointer to resume, and your pods are stateless: the pod
that resumes is usually not the pod that paused, often days later. So the SDK
binds `BadgerFlowCheckpointSaver`, which exports the graph's checkpoint into
the pause and restores it on the resume. You never configure it, and your
compiled graph is never modified.

Keep large values out of the graph state. The checkpoint travels with the pause
and is capped; a state that is too big fails the run with a clear reason rather
than producing a pause the platform cannot file. Hold a reference instead.

## Answering a chat

A chat-triggered agent receives each turn as text. The platform puts that text
in `input` and, when your input model has one place for it, under that field's
name in `structured` too: a field named `input`; else the one required string
field; else one named `message`, `text`, `query`, `question`, `prompt` or
`content`; else the one string field. So `Input(problem: str, attempt: str =
"")` receives the turn as `problem`, and `Input(message: str = "", problem: str
= "", attempt: str = "")` as `message`. Several optional string fields with
none of those names is left to your model, which then sees them all empty.

Going back, the bubble shows one field of your output model: the one you mark
with `Field(json_schema_extra={"x-badgerflow-reply": True})`, else one named
`reply`, `answer`, `response`, `message`, `text` or `content`, else the single
string field. An output that names none of those shows as its JSON, which is
the cue to mark the field. A streamed agent's deltas are always the reply.

## Streaming a run

Ask for `text/event-stream` and the invocation answers with the run's event
frames as they happen, the last one terminal:

```
data: {"seq":1,"type":"step_started","payload":{"node_id":"retrieve",...}}
data: {"seq":2,"type":"step_completed",...}
data: {"seq":3,"type":"complete","payload":{"output":{...}}}
```

Nothing in the handler changes: the same `ctx.step` blocks and the same return
value. What changes is where the evidence goes. On a streamed invocation the
frames ride the response instead of being posted to the platform's event
ingest, because both would record the run twice.

## Register and release

Deploy the container, then point the platform at it:

```bash
bf sync --wait-for-endpoint 120 --image "$IMAGE@$DIGEST"
```

The platform fetches `/.well-known/badgerflow-agent.json` from your endpoint and
records what **it** saw, so a registration is evidence rather than a claim.
`bf sync` exits non-zero when what the platform fetched is not what your
checkout compiles to, which is what makes it safe in CI.

Registering does not change what runs. A governed release does:

```bash
bf release submit --wait     # then a second human approves it in the UI
```

Each environment runs its own BadgerFlow with its own approvers, so run this
once per environment. Pass the image **digest**, never a tag: it is what a
rollback names and what makes "the same build" promoted from UAT to production
provable.

## Models from any Python code

A LangGraph app does not have to be a registered agent to use the models your
BadgerFlow workspace pays for. Create a model key in **Workspace settings →
Model keys**, then:

```bash
export BADGERFLOW_GATEWAY_URL=https://<platform>/api/gateway/ws/<workspace>
export BADGERFLOW_MODEL_KEY=sk-...
bf models                      # what this key may call, and the price per million tokens
```

```python
from badgerflow.langgraph import ChatBadgerFlow

llm = ChatBadgerFlow(model="claims-chat")   # outside a run: the model key
```

`badgerflow.llm.openai_client()` gives the same access as an `AsyncOpenAI`.
Every call is billed to the key's workspace and held to the key's model list and
budget. Inside a registered agent's run the run token is used instead, even when
a model key is set, so a release's attribution never depends on what a pod's
environment happens to contain. The Helm chart refuses to put a model key in a
governed agent at all.

## Tool plugins

A plugin gives BadgerFlow agents and workflows a new tool without changing the
platform. It is a standard MCP server plus a signed manifest, and both are built
from typed Python:

```bash
pip install 'badgerflow[plugin]' pytest
bf plugin init acme-crm && cd acme-crm
bf plugin test && bf plugin validate
```

```python
from pydantic import BaseModel, SecretStr
from badgerflow.plugin import Plugin, Permissions, ToolContext, credential

plugin = Plugin(
    name="acme-crm",
    version="0.1.0",
    description="Read customers in Acme CRM.",
    permissions=Permissions(egress=["api.acme.com"], models=["any-allowed"], storage_bytes=1_000_000),
)

class AcmeCredentials(BaseModel):
    api_key: SecretStr = credential(label="API key", kind="secret")

class Customer(BaseModel):
    id: str
    name: str
    tier: str

@plugin.tool(credentials=AcmeCredentials, effect="read")
async def find_customer(ctx: ToolContext[AcmeCredentials], email: str) -> Customer:
    """Look up a customer by email."""
    resp = await ctx.http.get("https://api.acme.com/customers", params={"email": email},
                              headers={"Authorization": f"Bearer {ctx.credentials.api_key.get_secret_value()}"})
    return Customer.model_validate(resp.json())

app = plugin.asgi()     # MCP streamable HTTP + /.well-known/badgerflow-plugin.json + /healthz
```

The tool's schemas come from its signature and return type and its description
from the docstring; `badgerflow-plugin.yaml` is generated from them by
`bf plugin manifest` and never edited. A plugin the platform would refuse fails
when the module imports.

Each workspace's credential reaches the tool only inside the platform's call.
`ctx.http` refuses a host outside `egress` before sending, and `ctx.llm` refuses a
model outside `models`. A raised exception becomes a tool error the model can
read. Test tools in-process with `badgerflow.plugin.testing.PluginTestClient`.

Before shipping, check the built image and call a tool the way the platform would:

```bash
bf plugin build
bf plugin validate --image acme-crm:0.1.0     # run non-root, read-only, no capabilities, as the cluster does
bf plugin call find_customer --image acme-crm:0.1.0 \
    --args '{"email": "ada@example.com"}' --credentials @creds.json
```

`call` sends the platform's call envelope, credentials included, and with
`--image` puts the plugin behind a local proxy that allows only `egress` and
prints each connection; `--url` calls a plugin already serving. `ctx.llm` and
`ctx.storage` have no local platform there.

Plugins ship only through your own registry. `bf plugin publish --registry <yours>
--key <pem> --keyid <id>` pushes the image by digest over the registry API (no
cosign, no Docker `insecure-registries`; `--insecure` for plain HTTP) and pushes
the signed attestation beside it; then `bf plugin submit --image <ref>`. The
platform imports a package only if a key in the operator's trust set signed it.
`bf plugin init` also writes a CI workflow that runs these on a version tag. `ctx.storage` is a JSON key-value store per calling
workspace under `storage_bytes`, in memory under `PluginTestClient` and on the
platform (`BADGERFLOW_PLATFORM_URL`) when served.

## Commands

| | |
|---|---|
| `bf init [--langgraph]` | scaffold a project |
| `bf dev` | serve locally, run-token verification off |
| `bf validate` | build the manifest offline, print its hash |
| `bf compile [--check]` | the graph the platform will see, and its hash |
| `bf register` / `bf sync` | register a build; `sync` is the CI form |
| `bf status` | registration, release, and hash agreement |
| `bf release submit \| status` | the governed release and its scorecard |
| `bf models` | the models your model key may call, with prices |
| `bf plugin init \| manifest \| test \| validate [--image]` | author and check a tool plugin, or its image |
| `bf plugin call <tool> --image \| --url` | one call with the platform's envelope and credentials |
| `bf plugin build \| publish \| sign \| submit \| dev` | package, sign and submit it |
| `bf skills install \| check` | teach a coding tool this SDK: `.agents/skills`, `.claude/skills`, an `AGENTS.md` block; `check` fails after an upgrade until you re-install |

## Coding tools

`bf init` and `bf plugin init` also install the SDK's agent skills — for Claude Code, Codex,
Cursor and any tool that reads `.agents/skills` or `AGENTS.md` — so the tool knows the commands,
the declarations and the rules without reading this SDK's source. Commit them. After upgrading
`badgerflow`, run `bf skills install`; `bf skills check` in CI fails while they describe another
version. `--no-skills` opts out. Claude Code users with access to the BadgerFlow repository can
also add it as a plugin marketplace: `/plugin marketplace add facileai/agiel`, then
`/plugin install badgerflow@badgerflow`.

## Versioning

The SDK follows semantic versioning. Its version is not decoration: it lands in
every manifest's `sdk` block and the platform stores it with the release, so it
is part of what an auditor sees. The wire contract is versioned **separately**
as `contract_version` — an SDK major bump does not imply a contract bump, and a
contract bump is announced on its own.

## Requires

Python 3.11+. The `server` extra pulls FastAPI and uvicorn, `langgraph` pulls
LangGraph and langchain-core, `otel` pulls the OpenTelemetry SDK, `plugin` pulls the
official MCP SDK. Without an
extra the corresponding surface is inert rather than broken.

## Licence

Apache-2.0, and it covers **this SDK only**. The BadgerFlow platform the SDK
talks to is not open source and is licensed separately. See `LICENSE` and
`NOTICE`.
