Build APIs that understand intent, not just requests.
Project description
intentful
Build APIs that understand intent, not just requests.
intentful is a Python library that lets backend developers annotate FastAPI endpoints with semantic context, making each endpoint naturally actionable via natural language — without chatbots, external agents, or losing control.
Key Principles
- Backend-first — the developer defines the boundaries, the LLM operates within them
- Progressive enhancement — the same endpoint works with structured payloads or natural language, without breaking anything
Installation
pip install intentful
With LLM backends:
pip install intentful[anthropic] # Claude
pip install intentful[openai] # GPT
pip install intentful[all] # all backends
Quick Start
1. Annotate your endpoints
from fastapi import FastAPI
from pydantic import BaseModel, Field
from intentful import intent, IntentContext
from intentful.integrations.fastapi import IntentRouter, setup_intentful
app = FastAPI()
router = IntentRouter(ai_backend="anthropic", language="pt")
class GeraTurmasSchema(BaseModel):
ano_lectivo: str = Field(..., description="Academic year (e.g. 2025/26)")
curso_id: int = Field(..., description="Course ID")
@router.post("/turmas/gerar")
@intent(
description="Create classes for an academic year",
context=IntentContext(
rules=[
"Each course has curricular years defined in the study plan",
"Default max capacity is 40 students per class",
],
allowed_operations=["CREATE", "READ"],
requires_confirmation=True,
),
path="/turmas/gerar",
)
async def gerar_turmas(payload: GeraTurmasSchema):
# your normal logic here
...
setup_intentful(app, router)
2. Use it both ways
# Traditional structured payload
curl -X POST http://localhost:8000/turmas/gerar \
-H "Content-Type: application/json" \
-d '{"ano_lectivo": "2025/26", "curso_id": 5}'
# Natural language via /intent
curl -X POST http://localhost:8000/intent \
-H "Content-Type: application/json" \
-d '{"prompt": "Create classes for Engineering in 2025/26"}'
# Dry-run mode (simulate without executing)
curl -X POST http://localhost:8000/intent \
-H "Content-Type: application/json" \
-d '{"prompt": "Create classes for Engineering in 2025/26", "dry_run": true}'
Two-Step Lookup Resolution
The problem
REST endpoints use identifiers (IDs) in paths — DELETE /orders/abc-456. But natural language prompts use descriptions — "delete João's order from yesterday". The LLM doesn't have access to your database and can't know the real ID. If it tries, it hallucinates.
The solution
Instead of trying to resolve everything in one step, intentful splits the process:
- Step 1 — Identify intent: The LLM picks the right endpoint and extracts search hints from the prompt (e.g.
customer_name: "João",created_at: "2026-03-14") — but never invents an ID. - Step 2 — Resolve references: The system uses your actual models/database to look up candidates matching those hints, then either auto-resolves (1 match), asks the user to choose (N matches), or returns an error (0 matches).
User: "delete João's order from yesterday"
│
▼
┌─────────┐
│ Step 1 │ LLM → identifies endpoint + extracts search hints
└────┬────┘
│ endpoint: DELETE /orders/{order_id}
│ lookup_hints: {customer_name: "João", created_at: "2026-03-14"}
▼
┌─────────┐
│ Step 2 │ System → queries your DB/model with the hints
└────┬────┘
│ found: order_id = "abc-456"
▼
┌─────────┐
│ Confirm │ "Delete order abc-456 (João, €45)?"
└────┬────┘
│ user confirms
▼
DELETE /orders/abc-456
Usage
Define a resolver_fn that queries your data source and pass it via LookupConfig:
from intentful import intent, IntentContext, LookupConfig
# Your lookup function — queries the real database
async def search_orders(hints: dict) -> list[dict]:
query = db.query(Order)
if "customer_name" in hints:
query = query.filter(Order.customer_name.ilike(f"%{hints['customer_name']}%"))
if "created_at" in hints:
query = query.filter(Order.created_at == hints["created_at"])
return [{"id": o.id, "customer_name": o.customer_name, "total": o.total}
for o in await query.all()]
@router.delete("/orders/{order_id}")
@intent(
description="Delete an order",
context=IntentContext(
allowed_operations=["DELETE"],
requires_confirmation=True,
),
method="DELETE",
path="/orders/{order_id}",
lookups={
"order_id": LookupConfig(
search_fields=["customer_name", "created_at", "description"],
resolver_fn=search_orders,
id_field="id",
display_fields=["customer_name", "total"],
)
},
)
async def delete_order(order_id: str):
...
The LookupConfig parameters:
| Parameter | Description |
|---|---|
search_fields |
Fields the LLM can use as search hints (shown in the prompt context) |
resolver_fn |
Async function that receives hints and returns a list of dicts |
id_field |
Which field in the result contains the ID (default: "id") |
display_fields |
Fields to show the user when confirming or choosing between candidates |
Resolution outcomes
| Result | Behaviour |
|---|---|
| 1 match | Auto-resolves the parameter and continues to execution/confirmation |
| N matches | Returns candidates to the client with lookup_results for the user to choose |
| 0 matches | Returns a 404 error explaining the parameter couldn't be resolved |
How It Works
1. Request arrives with "prompt" field
↓
2. IntentMiddleware intercepts (or /intent endpoint receives)
↓
3. Resolver queries the IntentRegistry
(all endpoints annotated with @intent)
↓
4. LLM receives: prompt + available endpoints + business rules + schemas
↓
5. LLM returns: { endpoint, payload, confidence, lookup_hints }
↓
6. Lookup Resolver: resolves hints against real data (if needed)
↓
7. Validator checks: valid schema? allowed operations? needs confirmation?
↓
8. If confirmed → executes the endpoint normally
↓
9. Auditor logs: original prompt, generated payload, user, timestamp, result
Features
@intentdecorator — annotate any FastAPI endpoint with semantic contextIntentRouter— drop-in replacement forAPIRouterwith intent support- Two-step lookup resolution — resolve natural language references to real IDs via your models
- Dual-mode endpoints — structured payloads and natural language on the same route
- Confirmation flow — require user confirmation for high-impact operations
- Dry-run mode — simulate operations without executing
- Audit trail — log every intent-based operation
- Multi-backend — Anthropic (Claude), OpenAI (GPT), Ollama (local models)
- Multilingual — accepts prompts in any language
Example
See examples/demo_app.py for a complete working example.
pip install intentful[anthropic]
export ANTHROPIC_API_KEY="sk-..."
uvicorn examples.demo_app:app --reload
Development
git clone https://github.com/intentful-dev/intentful.git
cd intentful
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest tests/ -v
Contributing
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file intentful-0.1.0.tar.gz.
File metadata
- Download URL: intentful-0.1.0.tar.gz
- Upload date:
- Size: 43.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8559f9db2b1b7c94043e736717fbfafadbd8f91c0988e7cc243b227646573581
|
|
| MD5 |
b0e80537e055cb80b07c004d60852ed9
|
|
| BLAKE2b-256 |
7d46755d1485b8f625b9d885bcf3f7201996a44bd7dec3e9d8ed14cd2af4f2b0
|
File details
Details for the file intentful-0.1.0-py3-none-any.whl.
File metadata
- Download URL: intentful-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
399d3ae464c882b67857b3f29551e1777731576b59fec8bdbab05a5d7e942c3f
|
|
| MD5 |
600f0f3ffa3b0e39720a4c63f74e6882
|
|
| BLAKE2b-256 |
622c7740658164f4446f5a650c6aafc2f4030e6942868f23b5e886e7ffce7d10
|