Metadata-Version: 2.5
Name: langgraph-jev
Version: 0.1.0
Summary: A typed probabilistic decision node for LangGraph, powered by TypeSafe's Jev.
Project-URL: Homepage, https://github.com/iroy2000/langgraph-jev
Project-URL: Issues, https://github.com/iroy2000/langgraph-jev/issues
Author: langgraph-jev contributors
License-Expression: MIT
License-File: LICENSE
Keywords: agents,decision,jev,langchain,langgraph,typesafe
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: langchain-core>=0.3
Requires-Dist: langgraph>=0.2
Requires-Dist: pydantic>=2.0
Requires-Dist: typesafe-sdk>=0.7
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Provides-Extra: examples
Requires-Dist: python-dotenv>=1.0; extra == 'examples'
Description-Content-Type: text/markdown

# langgraph-jev

`langgraph-jev` is an open-source LangGraph/LangChain integration for
[Jev](https://docs.typesafe.ai/introduction), TypeSafe's typed probabilistic
decision API. `JevNode` wraps it as a decision node you drop into an agent
graph -- it's not an LLM and not an LLM wrapper.

Jev evaluates typed *questions* against *state* and returns typed answers with
probabilities and confidence attached. There's no text generation and nothing
to parse -- your code branches on, sorts by, and routes with the result
directly.

For example, a `work_ticket` graph might run an "understand" step over a
customer request, hand the structured output to a `JevNode` for typed
decisions (work type, priority, whether it needs an engineer), and then route
to a coding, support, or docs worker based on those decisions. See
`examples/work_ticket.py` for the full version of this.

## Installation

```bash
pip install langgraph-jev
```

Set your API key (never commit it):

```bash
export TYPESAFE_API_KEY=...
```

The examples in `examples/` also support a project-local `.env` file (already
covered by `.gitignore`) via optional `python-dotenv` support:

```bash
pip install "langgraph-jev[examples]"
echo "TYPESAFE_API_KEY=..." > .env
python examples/basic.py
```

## Quick start

```python
from langgraph_jev import JevNode, choice

jev = JevNode(
    questions={
        "route": choice(["coding", "support", "research"]),
    }
)
```

Running `examples/basic.py` against a real state produces typed decisions
with confidence scores, not free text:

![Terminal output of examples/basic.py](docs/screenshots/basic-example.png)

## Resource cleanup

`JevClient` (and anything built on it, like `JevNode`/`JevRunnable`) opens
underlying HTTP clients on construction. Close it when you're done, or use
it as a context manager:

```python
from langgraph_jev import JevClient, choice

with JevClient() as client:
    result = client.decide(state={"title": "..."}, questions={"route": choice(["a", "b"])})

# or, in async code:
async with JevClient() as client:
    result = await client.adecide(state={"title": "..."}, questions={"route": choice(["a", "b"])})
```

If you construct a long-lived `JevNode`/`JevRunnable` (e.g. one per graph,
reused across requests), there's no need to close it per-call -- call
`client.close()`/`await client.aclose()` once when your application shuts
down.

## LangGraph

`JevNode` is directly callable by LangGraph -- it receives graph state and
returns a state update (not a replacement of the whole state):

```python
graph.add_node("jev", jev)
```

By default the entire graph state is passed to Jev as `state`. Use
`state_key`/`output_key` to scope input and output:

```python
jev = JevNode(
    state_key="structured_request",
    output_key="decision",
    questions={...},
)
```

## Confidence routing

Choice and Score answers include a `confidence` score derived from the
response's probability distribution (see
[Confidence](https://docs.typesafe.ai/confidence)). Configure thresholds and
a policy for what happens below them:

```python
jev = JevNode(
    questions={
        "work_type": choice(["bug", "support", "configuration", "documentation"]),
        "priority": choice(["critical", "high", "normal", "low"]),
    },
    thresholds={"work_type": 0.85, "priority": 0.90},
    low_confidence="human_review",  # "allow" (default) | "human_review" | "error"
)


def route_after_jev(state):
    if state["decision"].requires_review:
        return "human_review"
    return "continue"
```

Low-confidence decisions are never silently discarded -- the full decision
and its confidence are always available on the result.

## Routing helper

```python
from langgraph_jev import route_by_decision


def route_work(state):
    return route_by_decision(
        state["decision"],
        field="work_type",
        routes={
            "bug": "coding_agent",
            "support": "support_agent",
            "documentation": "documentation_agent",
        },
        fallback="human_review",
    )
```

Jev makes the probabilistic decision; your application code decides what to
do with it. Routing stays deterministic.

## WorkTicket example

Jev doesn't generate the `WorkTicket` itself -- it provides the decisions,
and your application code builds the typed contract from them:

```python
decision = state["decision"]

ticket = WorkTicket(
    id="WT-1234",
    type=decision["work_type"].value,
    priority=decision["priority"].value,
    needs_engineer=decision["needs_engineer"].value,
    title=request["title"],
    description=request["description"],
    context={"customerTier": request.get("customerTier")},
    acceptance_criteria=["Identify root cause", "Add regression coverage if appropriate"],
)
```

![Terminal output of examples/work_ticket.py](docs/screenshots/work-ticket-example.png)

See `examples/work_ticket.py` for the full example: an understand step feeds
Jev, Jev's decisions build a `WorkTicket`, and the ticket routes to a coding,
support, or docs worker.

## LangChain Runnable

```python
from langgraph_jev import JevRunnable, choice

jev = JevRunnable(questions={"route": choice(["coding", "support", "research"])})
result = jev.invoke({"title": "Customer cannot login"})
```

Supports both `invoke()` and `ainvoke()`.

## Examples

| File | Demonstrates |
| --- | --- |
| `examples/basic.py` | Minimal `JevClient` usage |
| `examples/langgraph_basic.py` | A minimal LangGraph graph with a `JevNode` |
| `examples/confidence_routing.py` | Thresholds and `low_confidence="human_review"` |
| `examples/work_ticket.py` | Building a `WorkTicket` from Jev's decisions |
| `examples/multi_agent.py` | Full graph: understand -> Jev -> ticket -> routed workers |

## Public API

```python
from langgraph_jev import (
    JevClient,
    JevNode,
    JevRunnable,
    choice,
    boolean,
    score,
    route_by_decision,
)
```

That's the full public API. Errors (`JevError` and its subclasses
`JevConfigurationError`, `JevAPIError`, `JevTimeoutError`,
`JevValidationError`) and the result types (`JevDecision`, `JevResult`) are
also exported for type annotations and `except` clauses.

## Development

```bash
pip install -e ".[dev]"
pytest
ruff check .
ruff format .
mypy src
```

Tests use a fake/mock Jev transport and never require a real
`TYPESAFE_API_KEY`. CI runs `ruff check`, `ruff format --check`, `mypy`, and
`pytest` on Python 3.11-3.13.

## License

MIT
