Welcome to utilitai!
This package is a minimal(ish) implementation of Utility AI in Python. It is designed to compliment btreeny, my Behaviour Trees implementation!
There actually aren't many options for utility AI in Python, which surprises me! In general for serious systems you should be looking at C++ (or, maybe, Rust 🦀), but hopefully this library will help on the prototyping journey!
The idea
List the things your agent could do, score each of them against the current state of the world, and do whichever scores highest.
from dataclasses import dataclass
from utilitai import ToConsider, curves
# In order to make decisions, we need context - this can be anything but
# a dataclass is a logical choice
@dataclass
class Context:
hunger: int
money: int
food: int
# We create a registry object which is generic over the context
things: ToConsider[Context] = ToConsider()
# And then add options to consider
@things.add('go to the shops')
def go_to_the_shops(ctx: Context):
return curves.clamped(curves.exponential(ctx.hunger)) * curves.is_gt_zero(ctx.money)
# Names can be inferred from function name
@things.add
def eat_food(ctx: Context):
return curves.exponential(ctx.hunger) * curves.is_gt_zero(ctx.food)
# We can also add a constant baseline
things.add_constant('starve', 0.0)
current_context = Context(hunger=1, money=1, food=0)
action = things.consider(current_context)
assert action == 'go to the shops'
Scoring functions
A scoring function takes your context and returns a float. That's the whole interface - no subclassing, no DSL, and the decorated function is returned unchanged so you can still call, test, or reuse it elsewhere.
Scores are only ever compared against each other, but keeping them in [0, 1] makes them much easier to reason about. The utilitai.curves module has the usual suspects for shaping a raw value into that range:
| curve | shape |
|---|---|
linear / inverse_linear |
identity / 1 - val |
quadratic / inverse_quadratic |
accelerates / decelerates quickly |
logistic(val, midpoint, steepness) |
S-shaped, sharp transition around the midpoint |
exponential(val, base) |
strongly favours high inputs |
smoothstep |
ease-in / ease-out, clamped to [0, 1] |
is_gt_zero / is_le_zero |
step functions, handy as on/off multipliers |
Because scoring functions are plain Python, curves compose by calling them and combining the results - multiply to say "and", max to say "or", and multiply by a constant to weight an option against its rivals:
@things.add('order a takeaway')
def order_a_takeaway(ctx: Context):
# Weighted below the other options - it's a treat, not a necessity
return 0.5 * curves.smoothstep(ctx.hunger) * curves.is_gt_zero(ctx.money)
Choosing between options
things.consider(context)returns the name of the highest scoring option. If two options tie, whichever was added first wins - so add any baselines first if you want them to break ties in their favour.things.add_constant(name, value)adds an option with a fixed score. This is the usual way to express "if nothing else appeals, do this".things.score(context)returns every option's score as a dict, which is useful in tests and when tuning curves.considerraisesValueErrorif nothing has been added, andaddraisesValueErroron a duplicate name.
Each call to consider logs the full set of scores at DEBUG level under the utilitai logger, so you can see why a decision was made:
import logging
logging.basicConfig(level=logging.DEBUG)
A slightly bigger example
actions: ToConsider[RobotState] = ToConsider()
# If we have no good choices, do nothing
actions.add_constant('do nothing', 1e-6)
@actions.add('recharge')
def recharge(state: RobotState) -> float:
# As our battery gets lower, our desire to stop and recharge gets higher
battery_used = 1 - state.remaining_battery_percentage / 100
return curves.inverse_quadratic(battery_used)
@actions.add('move to goal')
def move_to_goal(state: RobotState) -> float:
if is_at_goal(state) or not has_valid_path(state):
return 0.0
return 0.9 if can_reach_goal_with_battery(state) else 0.1
@actions.add('do a dance')
def do_a_dance(state: RobotState) -> float:
# Dancing before we make it to the goal would be silly!
return 1.0 if is_at_goal(state) else 0.0
while True:
# Perception
state = update_state_from_sensors(state)
# Planning (ish)
best_action = actions.consider(state)
# Control - maybe using btreeny!
control_robot_from_action(best_action, state)
The full version lives in examples/readme_example.py, and examples/eat_sleep_forage.py is a runnable simulation of a hunter-gatherer trying not to starve.
Metadata
Release files for utilitai 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| utilitai-0.2.0.tar.gz | 38.5 kB | With provenance | Details for utilitai-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| utilitai-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for utilitai-0.2.0-py3-none-any.whl |
Total release size: 46.6 kB
Release files / utilitai-0.2.0.tar.gz
| Download URL | utilitai-0.2.0.tar.gz |
|---|---|
| Size | 38.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7bf63aa3995e144039b336c82acfb946490389d4a40b824a95d448e6cb60c7c4
|
|
BLAKE2b-256 checksum How to use checksums |
643504ec37bf06d253c77ad3c2e313e301e69b3fed094f4d4df9055649983939
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 2, 2026.
Transparency logRelease files / utilitai-0.2.0-py3-none-any.whl
| Download URL | utilitai-0.2.0-py3-none-any.whl |
|---|---|
| Size | 8.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2c8d21925960f456b97e77772d89e05e10c25a62df97cc91fb83cfc3a25162cb
|
|
BLAKE2b-256 checksum How to use checksums |
dc5bcb5cd6163b826a3de035d12b72cdbb1567272ef8f1fc7caa2ddcf31639c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 2, 2026.
Transparency logSecurity for utilitai 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for utilitai-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.