Skip to content
112 changes: 112 additions & 0 deletions docs/examples/agno.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Agno

Integrates Agent Assembly with [Agno](https://docs.agno.com/) (formerly Phidata) to enforce governance policy on tool calls before they execute.

## What this example demonstrates

- Initializing Agent Assembly with `init_assembly()` in offline `sdk-only` mode.
- Installing Agno tool-level governance via the **native Agno adapter** β€” `AgnoPatch` patches `agno.tools.function.FunctionCall.execute`, the single chokepoint every Agno function-tool call runs through.
- Running an **allowed** tool call (`get_weather`), another **allowed** tool call (`summarize_docs`), and a **denied** tool call (`execute_sql`, blocked by policy rule `deny_arbitrary_execution`).
- How a denied tool short-circuits entirely β€” its body never runs β€” and returns a failure `FunctionExecutionResult` instead of a result.
- Driving genuine Agno `@tool` functions exactly as an Agno `Agent` does β€” `FunctionCall(...).execute()` β€” with no gateway, API key, or live LLM.

## The framework / library

[Agno](https://docs.agno.com/) (formerly Phidata) is the agent framework governed in this example. Agno has a **native Agent Assembly adapter** (`agent_assembly.adapters.agno`), so governance is wired by patching Agno's own tool-execution chokepoint rather than wrapping individual tools.

Version pins (from `pyproject.toml`):

| Dependency | Version |
|---|---|
| `agno` | `>=2.0.0` |
| `agent-assembly` | `>=0.0.1b4` |
| Python | `>=3.12` |

## How it works

`init_assembly()` is opened as a context manager in offline `sdk-only` mode with the agent id `agno-demo-agent`:

```python
with init_assembly(
gateway_url=gateway_url,
api_key=api_key,
agent_id="agno-demo-agent",
mode="sdk-only",
) as ctx:
...
```

**Hook point.** The Agno adapter governs Agno by patching `agno.tools.function.FunctionCall.execute` (and its async counterpart `aexecute`) β€” the single chokepoint every Agno function-tool call runs through. Every tool an Agno agent invokes therefore passes through policy *before* its body executes.

**Offline note.** In production, `init_assembly()` auto-detects Agno and wires the live runtime as the interceptor automatically. In this offline `sdk-only` demo there is no live runtime, so `init_assembly()` installs a no-op hook; the example reverts it and re-applies the hook wired to a local `LocalPolicyEngine` so the demo shows real allow/deny decisions without a gateway (the patch is idempotent, so the no-op hook is reverted first):

```python
AgnoPatch(policy).revert()
patch = AgnoPatch(policy)
assert patch.apply()
```

The adapter calls `check_tool_start` on `LocalPolicyEngine` (`src/policy.py`), which returns a decision dict in the gateway wire format `{"status": "allow"}` or `{"status": "deny", "reason": ...}`. `execute_sql` and `run_shell_command` are denied (arbitrary execution); everything else is allowed. The engine runs fail-closed, so an unknown verdict denies rather than allows.

**Deny behavior.** When `execute_sql` is denied, `FunctionCall.execute()` returns a `FunctionExecutionResult` with `status == "failure"` whose error carries the `[BLOCKED by governance policy]` marker and the `deny_arbitrary_execution` rule name. The tool's body never runs β€” the example proves this with a side-effect sink (`SQL_EXECUTIONS`) that stays empty for a denied call, the negative control that would fail if governance were a no-op.

## Prerequisites & running it

See [Preparing the runtime environment](preparing-the-runtime-environment.md) for the shared prerequisites.

Then run the example (offline β€” no API key and no running gateway required):

```bash
cd python/agno-tool-policy
uv sync --extra dev
uv run python src/main.py
```

### Expected output

```
==============================================================
Agent Assembly β€” Agno Tool Policy Demo
==============================================================

Initializing Agent Assembly (gateway: http://localhost:8080, sdk-only mode)...
Agent: agno-demo-agent
Gateway: http://localhost:8080
Mode: sdk-only (offline demo)

Policy rules (local simulation of gateway policy):
DENY β€” execute_sql, run_shell_command (arbitrary execution)
ALLOW β€” everything else

Agno governance hook installed on FunctionCall.execute.
Tools governed: get_weather, summarize_docs, execute_sql

Running governed tool calls:
--------------------------------------------
β†’ get_weather({'city': 'London'})
βœ… ALLOWED β€” Weather in London: sunny, 22C (mock)

β†’ summarize_docs({'topic': 'policy enforcement'})
βœ… ALLOWED β€” Summary for 'policy enforcement': Agent Assembly provides governance... (mock)

β†’ execute_sql({'sql': 'DROP TABLE users; --'})
❌ BLOCKED β€” [BLOCKED by governance policy] Tool 'execute_sql' is blocked by policy rule 'deny_arbitrary_execution'.. Please choose a different approach to accomplish this task.

Assembly context shut down.
```

## Run the smoke tests

The example ships offline smoke tests that drive real Agno `@tool` functions through the real `AgnoPatch` and assert genuine governance β€” an allowed tool runs and returns its output, a denied tool's body is short-circuited before it runs (the negative control):

```bash
cd python/agno-tool-policy
uv sync --extra dev
uv run pytest tests/ -v
```

## Links

- [Example directory](https://github.com/ai-agent-assembly/agent-assembly-examples/tree/master/python/agno-tool-policy)
- [Example README](https://github.com/ai-agent-assembly/agent-assembly-examples/blob/master/python/agno-tool-policy/README.md)
- [Agno documentation](https://docs.agno.com/)
7 changes: 5 additions & 2 deletions docs/examples/framework-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ repository.
| OpenAI Agents | `agent_assembly.adapters.openai_agents` | βœ… Validated β€” see [OpenAI Agents SDK](openai-agents-sdk.md). |
| Pydantic AI | `agent_assembly.adapters.pydantic_ai` | βœ… Validated β€” see [Pydantic AI](pydantic-ai.md). |
| Google ADK | `agent_assembly.adapters.google_adk` | βœ… Validated β€” see [Google ADK](google-adk.md). |
| Microsoft Agent Framework | `agent_assembly.adapters.microsoft_agent_framework` | βœ… Validated β€” governs `agent_framework.FunctionTool.invoke`; see the [`microsoft-agent-framework-tool-policy`](https://github.com/ai-agent-assembly/agent-assembly-examples/tree/master/python/microsoft-agent-framework-tool-policy) example. |
| LlamaIndex | _no native adapter_ | βœ… Validated (manual wrapper) β€” see [LlamaIndex β€” manual tool policy](llamaindex-tool-policy.md); governs `FunctionTool` calls via `GovernedToolRunner`. |
| Haystack | `agent_assembly.adapters.haystack` | βœ… Validated β€” governs `haystack.tools.Tool.invoke` (Haystack 2.x); see [Haystack](haystack.md). |
| LlamaIndex | `agent_assembly.adapters.llamaindex` | βœ… Validated β€” governs the concrete `FunctionTool.call` / `acall`; see [LlamaIndex](llamaindex-tool-policy.md). |
| Smolagents | `agent_assembly.adapters.smolagents` | βœ… Validated β€” governs `smolagents.tools.Tool.__call__`; see [Smolagents](smolagents.md). |
| Agno | `agent_assembly.adapters.agno` | βœ… Validated β€” governs `agno.tools.function.FunctionCall.execute` / `aexecute`; see [Agno](agno.md). |
| Microsoft Agent Framework | `agent_assembly.adapters.microsoft_agent_framework` | βœ… Validated β€” governs `agent_framework.FunctionTool.invoke`; see [Microsoft Agent Framework](microsoft-agent-framework.md). |
| MCP servers | `agent_assembly.adapters.mcp` | ⏳ Planned β€” adapter ships; a curated example is not yet vendored. |

!!! note "Adapter present vs. example present"
Expand Down
117 changes: 117 additions & 0 deletions docs/examples/haystack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Haystack

Integrates Agent Assembly with [Haystack](https://haystack.deepset.ai/) (deepset) to enforce governance policy on a real Haystack agent's tool calls before they execute, using the SDK's native Haystack adapter.

## What this example demonstrates

- Initializing Agent Assembly with `init_assembly()` in offline `sdk-only` mode (which also auto-detects Haystack).
- Installing the native Haystack adapter β€” `HaystackPatch` patches `haystack.tools.Tool.invoke`, the single tool-execution chokepoint Haystack 2.x uses.
- Running real `haystack.tools.Tool` instances through a genuine `ToolInvoker` (the component a Haystack `Agent` uses to run a model-chosen tool call).
- An **allowed** tool call (`query_index`) and another **allowed** tool call (`summarize_docs`) β€” their tool bodies execute.
- A **denied** tool call (`execute_sql`, blocked by `deny_arbitrary_execution`) β€” the deny short-circuits `Tool.invoke` and returns a `[BLOCKED by governance policy]` result *before the tool body runs*.
- No LLM, API key, or running gateway is involved β€” the tools are driven offline through a hand-built `ToolCall`.

## The framework / library

[Haystack](https://haystack.deepset.ai/) (deepset; installed as `haystack-ai`, imported as `haystack`) is the agent framework governed in this example. Haystack 2.x is the line that exposes the agentic `Agent` / `ToolInvoker` tool-call path and the `haystack.tools.Tool.invoke` hook point the adapter patches; the 1.x line predates that API and is out of scope.

Version pins (from `pyproject.toml`):

| Dependency | Version |
|---|---|
| `haystack-ai` | `>=2.0.0,<3.0` |
| `agent-assembly` | `>=0.0.1b2` |
| Python | `>=3.12` |

## How it works

`init_assembly()` is opened as a context manager in offline `sdk-only` mode with the agent id `haystack-demo-agent`:

```python
with init_assembly(
gateway_url=gateway_url,
api_key=api_key,
agent_id="haystack-demo-agent",
mode="sdk-only",
) as ctx:
...
```

**The hook point.** The native adapter patches `haystack.tools.Tool.invoke`. That single method is the execution chokepoint Haystack 2.x routes *every* tool through: it governs both the bare `Tool.invoke()` path and the full agent loop, because `haystack.components.agents.Agent` dispatches its tool calls via `haystack.components.tools.ToolInvoker`, which itself calls `tool_to_invoke.invoke(**final_args)`. Patching `Tool.invoke` therefore intercepts every tool execution β€” which is why governance is wired there and not at the higher-level `Agent` / `ToolInvoker` layer. `HaystackAdapter.get_supported_versions()` returns `[">=2.0.0,<3.0"]`.

**Offline note.** No LLM is needed. The example builds three real `haystack.tools.Tool` instances (`src/tools.py`) and drives them through a genuine `ToolInvoker` fed a hand-built `ToolCall` β€” the exact shape a chat model would emit β€” so the governed `Tool.invoke` is exercised on the real agent tool-dispatch path with no model, credentials, or gateway in the loop.

**How deny short-circuits.** The patched `Tool.invoke` first calls the interceptor's `check_tool_start` hook. The offline `LocalPolicyEngine` (`src/policy.py`) returns a gateway-format decision dict β€” `{"status": "deny", "reason": ...}` for the arbitrary-execution tools, `{"status": "allow"}` otherwise. On a deny the adapter returns a `[BLOCKED by governance policy]` message *without calling the original `invoke`*, so the tool's underlying function never runs. The policy carries `_enforce = True`, putting the adapter in the fail-closed `enforce` posture (an unknown or malformed verdict denies rather than silently allowing).

In offline `sdk-only` mode `init_assembly()` wires a no-op interceptor (there is no live gateway to answer policy), so the example reverts that and re-installs the same native adapter against the `LocalPolicyEngine` to make a real allow/deny visible without a gateway:

```python
HaystackPatch(LocalPolicyEngine()).revert() # drop the auto-applied no-op patch
patch = HaystackPatch(LocalPolicyEngine())
installed = patch.apply()
```

In production you instead point `init_assembly()` at a gateway and let its auto-detected adapter enforce real policy β€” no manual re-install is needed.

## Prerequisites & running it

See [Preparing the runtime environment](preparing-the-runtime-environment.md) for the shared prerequisites.

Then run the example (offline β€” no API key and no running gateway required):

```bash
cd python/haystack-tool-policy
uv sync --extra dev
uv run python src/main.py
```

### Expected output

The two safe tools run and the destructive tool is blocked before its body executes:

```
==============================================================
Agent Assembly β€” Haystack Tool Policy Demo
==============================================================

Initializing Agent Assembly (gateway: http://localhost:8080, sdk-only mode)...
Agent: haystack-demo-agent
Gateway: http://localhost:8080
Mode: sdk-only (offline demo)

Policy rules (local simulation of gateway policy):
DENY β€” execute_sql, run_shell_command (arbitrary execution)
ALLOW β€” everything else

Installing the native Haystack adapter against the demo policy...
Adapter installed: True

Running real Haystack tools through a ToolInvoker:
--------------------------------------------
β†’ query_index({'query': 'what is Agent Assembly?'})
βœ… ALLOWED β€” Index results for 'what is Agent Assembly?': [chunk-12, chunk-44, chunk-07] (mock)

β†’ summarize_docs({'topic': 'policy enforcement'})
βœ… ALLOWED β€” Summary for 'policy enforcement': Agent Assembly provides governance... (mock)

β†’ execute_sql({'sql': 'DROP TABLE users; --'})
❌ BLOCKED β€” [BLOCKED by governance policy] Tool 'execute_sql' is blocked by policy rule 'deny_arbitrary_execution'...

Tool bodies that actually executed: ['query_index', 'summarize_docs']
```

`execute_sql` is absent from the executed list β€” the deny short-circuited it before the tool ran, proving real governance rather than a no-op.

## Run the smoke tests

The example ships offline smoke tests that assert an allowed tool runs and a denied tool's body never executes:

```bash
uv run pytest tests/ -v
```

## Links

- [Example directory](https://github.com/ai-agent-assembly/agent-assembly-examples/tree/master/python/haystack-tool-policy)
- [Example README](https://github.com/ai-agent-assembly/agent-assembly-examples/blob/master/python/haystack-tool-policy/README.md)
- [Haystack documentation](https://haystack.deepset.ai/)
20 changes: 12 additions & 8 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,20 @@ adapter flow wires the framework, an annotated code walkthrough, and the expecte
| [OpenAI Agents SDK](openai-agents-sdk.md) | OpenAI Agents SDK | Approval-gated and denied tool calls, intercepting `FunctionTool.__call__`. |
| [Pydantic AI](pydantic-ai.md) | Pydantic AI | Tool-call governance driven offline by the built-in `TestModel`. |
| [Google ADK](google-adk.md) | Google ADK | A scripted offline tool trajectory governing `BaseTool.run_async` β€” no cloud credentials. |
| [LlamaIndex β€” manual tool policy](llamaindex-tool-policy.md) | LlamaIndex | The manual wrapper pattern (`GovernedToolRunner`) for a framework with no native adapter. |
| [Haystack](haystack.md) | Haystack | Real `haystack.tools.Tool` instances run through a `ToolInvoker`, governing `Tool.invoke`. |
| [LlamaIndex](llamaindex-tool-policy.md) | LlamaIndex | Native adapter governing the concrete `FunctionTool.call` / `acall` β€” a denied tool returns a blocked `ToolOutput`. |
| [Smolagents](smolagents.md) | Smolagents | Governs `smolagents.tools.Tool.__call__`; a denied tool's `forward()` body never runs. |
| [Agno](agno.md) | Agno | Governs `agno.tools.function.FunctionCall.execute`; a denied tool returns a failure result. |
| [Microsoft Agent Framework](microsoft-agent-framework.md) | Microsoft Agent Framework | Governs the async `FunctionTool.invoke`; offline mock path + a live path. |
| [Custom tool policy (no framework)](custom-tool-policy.md) | β€” | Govern plain Python functions with the minimal `governed()` helper β€” no AI framework required. |

## How the examples fit together

- **No native adapter?** [Custom tool policy](custom-tool-policy.md) shows the minimal
`governed()` building block; [LlamaIndex β€” manual tool policy](llamaindex-tool-policy.md)
builds on it with `GovernedToolRunner`. Use these patterns for any framework Agent Assembly
does not hook automatically.
- **Native adapter?** The LangChain, LangGraph, CrewAI, OpenAI Agents, Pydantic AI, and Google
ADK examples each rely on `init_assembly()` detecting the framework and installing its
governance hooks for you β€” see [Framework support](framework-support.md) for the full
adapter list and priority order.
`governed()` building block. Use that pattern for any framework Agent Assembly does not
hook automatically.
- **Native adapter?** The LangChain, LangGraph, CrewAI, OpenAI Agents, Pydantic AI, Google
ADK, Haystack, LlamaIndex, Smolagents, Agno, and Microsoft Agent Framework examples each
rely on `init_assembly()` detecting the framework and installing its governance hooks for
you β€” see [Framework support](framework-support.md) for the full adapter list and priority
order.
Loading