Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Tests

on:
push:
branches: [main, "feature/**"]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: python -m pip install --upgrade pip
- run: python -m pip install -e ".[dev]"
- run: pytest
- run: python -m compileall opendecision
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
__pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/
.mypy_cache/
.venv/
dist/
build/
*.egg-info/
.env
.idea/
.vscode/
46 changes: 46 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Contributing to OpenDecision

Thank you for helping build a dependable decision layer for AI agents.

## Local setup

```bash
git clone https://github.com/Sunny-commit/OpenDecision.git
cd OpenDecision
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest
ruff check .
```

Install `.[laya]` only when testing the real provider. Unit tests should use a fake provider and must not download model weights.

## Pull requests

1. Open or reference an issue for substantial changes.
2. Create a focused branch.
3. Add tests and documentation.
4. Run `pytest` and `ruff check .`.
5. Explain behavior and safety implications in the pull request.

## Decision packs

A decision pack should:

- solve a repeated, clearly described decision problem;
- use a typed `choice`, `score`, or `noul` question;
- define labels in precise, non-overlapping language;
- include representative evaluation examples before being advertised as production-ready;
- document calibration assumptions and failure modes;
- default to review or blocking for ambiguous consequential actions.

Do not include secrets, personal production data, or unsupported accuracy claims.

## Design principles

- Keep the core framework-agnostic.
- Keep providers behind small adapters.
- Do not present generated explanations as model evidence.
- Preserve raw provider output for debugging while exposing a stable normalized result.
- Prefer explicit contracts over hidden thresholds.
132 changes: 132 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# OpenDecision

**A framework-agnostic decision layer for AI agents.**

OpenDecision places a typed, testable contract between an AI agent and the action it wants to take. It uses [Laya](https://github.com/NandhaKishorM/laya) for fast `choice`, `score`, and `noul` decisions without adding another text-generation step.

> Status: early MVP. Do not treat uncalibrated model confidence as a production safety guarantee.

## Why OpenDecision?

Agents repeatedly need to decide whether to continue, stop, call a tool, ask a person, retrieve more context, or escalate. OpenDecision makes those branches explicit and reusable:

- typed decision contracts instead of free-form generated text;
- normalized results across decision providers;
- community-maintained YAML decision packs;
- adapters that do not couple the core SDK to one agent framework;
- optional Laya loading, so importing OpenDecision never downloads a model.

## Install

```bash
pip install -e ".[laya]"
```

Laya and OpenDecision require Python 3.10 or newer. For the examples:

```bash
pip install -e ".[laya,langgraph,fastapi]"
```

## Quick start

```python
from opendecision import DecisionGuard

guard = DecisionGuard()
result = guard.decide(
state={"tool": "send_email", "recipient": "customer@example.com"},
question={
"type": "choice",
"instructions": "Should the agent execute this tool action?",
"options": {
"allow": "Proceed automatically",
"review": "Require human approval",
"block": "Stop the action",
},
},
)

print(result.decision)
print(result.probabilities)
print(result.confidence)
```

OpenDecision returns model decisions and evidence; it does **not** ask Laya to generate a reason. Applications may add an explanation separately without confusing generated prose with decision-model output.

## Decision packs

```python
from opendecision import DecisionGuard, load_policy

policy = load_policy("decision_packs/security/tool_risk.yaml")
result = DecisionGuard().decide(agent_state, policy.question)
```

The first pack includes a tool-risk contract with `allow`, `review`, and `block` outcomes.

## LangGraph

```python
from opendecision import DecisionGuard
from opendecision.integrations.langgraph import decision_node, route_by_decision

graph.add_node("tool_guard", decision_node(DecisionGuard(), question))
graph.add_conditional_edges(
"tool_guard",
route_by_decision(),
{"allow": "execute_tool", "review": "human_review", "block": "stop"},
)
```

## FastAPI

```bash
uvicorn app.main:app --reload
```

Then send `POST /v1/decide` with `state` and a typed `question`.

## Architecture

```text
Agent / application
|
DecisionGuard + contract
|
Provider adapter (Laya first)
|
Normalized DecisionResult
|
ALLOW | REVIEW | BLOCK
```

## Scope of v0.1

- Python SDK and Pydantic contracts
- lazy Laya provider
- choice, score, and noul normalization
- YAML decision packs
- LangGraph node and routing helpers
- FastAPI example
- unit tests that run without downloading model weights

A dashboard, TypeScript SDK, additional frameworks, model-generated explanations, and evaluation tooling are intentionally deferred.

## Calibration and safety

Laya's documentation notes meaningful limitations: base checkpoints can be weak zero-shot on typed workflows, confidence may require domain calibration, and high-cardinality choices need special handling. Benchmark decision packs on representative data before automating consequential actions. Prefer human review when evidence or authorization is insufficient.

## Development

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

See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.

## License

Apache License 2.0. Laya is a separate Apache-2.0 project and remains subject to its own license and notices.
Empty file added app/__init__.py
Empty file.
1 change: 1 addition & 0 deletions app/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Compatibility module; see examples/langgraph_guard.py for a complete graph."""
26 changes: 26 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Minimal FastAPI service for OpenDecision."""

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel

from opendecision import DecisionGuard, DecisionQuestion, DecisionResult

app = FastAPI(title="OpenDecision", version="0.1.0")
guard = DecisionGuard()


class DecisionRequest(BaseModel):
state: dict[str, Any] | str
question: DecisionQuestion


@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}


@app.post("/v1/decide", response_model=DecisionResult)
def decide(request: DecisionRequest) -> DecisionResult:
return guard.decide(request.state, request.question)
12 changes: 12 additions & 0 deletions decision_packs/security/tool_risk.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: tool-risk
description: Classify whether an agent tool action should proceed, be reviewed, or be blocked.
tags:
- security
- tool-use
question:
type: choice
instructions: Should the agent execute this tool action?
options:
allow: The action is low risk and can proceed automatically.
review: The action needs human approval before execution.
block: The action is destructive, unsafe, or not authorized.
38 changes: 38 additions & 0 deletions examples/langgraph_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Insert OpenDecision into a LangGraph workflow."""

from langgraph.graph import END, StateGraph
from typing_extensions import TypedDict

from opendecision import DecisionGuard
from opendecision.integrations.langgraph import decision_node, route_by_decision


class AgentState(TypedDict, total=False):
tool: str
arguments: dict
decision_result: dict


graph = StateGraph(AgentState)
graph.add_node(
"guard",
decision_node(
DecisionGuard(),
{
"type": "choice",
"instructions": "Should the agent execute this tool action?",
"options": {
"allow": "Proceed automatically",
"review": "Require human approval",
"block": "Stop the action",
},
},
),
)
graph.set_entry_point("guard")
graph.add_conditional_edges(
"guard",
route_by_decision(),
{"allow": END, "review": END, "block": END},
)
app = graph.compile()
17 changes: 17 additions & 0 deletions examples/tool_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Guard a tool call with a reusable decision contract."""

from opendecision import DecisionGuard, load_policy

policy = load_policy("decision_packs/security/tool_risk.yaml")
guard = DecisionGuard()

result = guard.decide(
state={
"tool": "delete_database",
"arguments": {"database": "production"},
"user_confirmed": False,
},
question=policy.question,
)

print(result.model_dump_json(indent=2))
15 changes: 15 additions & 0 deletions opendecision/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""OpenDecision: a typed decision layer for AI agents."""

from .guard import DecisionGuard
from .models import DecisionQuestion, DecisionResult
from .policies import DecisionPolicy, load_policy

__all__ = [
"DecisionGuard",
"DecisionQuestion",
"DecisionResult",
"DecisionPolicy",
"load_policy",
]

__version__ = "0.1.0"
Loading
Loading