Skip to content

chatomon.orbitals.base

The orbital extension point: the base class and the @orbital decorator.

Classes:

Name Description
Orbital

A check on a user message before the nucleus, or on a reply before the user.

FunctionOrbital

An orbital backed by a function; create it with orbital.

Functions:

Name Description
orbital

Turn a function into an orbital.

Attributes:

Name Type Description
OrbitalFunction

A sync or async function that checks a message and returns a verdict.

OrbitalFunction module-attribute

OrbitalFunction = Callable[
    [OrbitalContext], Verdict | Awaitable[Verdict]
]

A sync or async function that checks a message and returns a verdict.

Orbital

Orbital(
    *,
    name: str | None = None,
    reads: Sequence[str] = (),
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block"
)

Bases: ABC

A check on a user message before the nucleus, or on a reply before the user.

Subclasses implement check. The engine runs orbitals in the order they appear in the atom's chains, applies timeout and turns failures into a verdict according to on_error.

Parameters:

Name Type Description Default
name str | None

Unique name inside an atom. Defaults to default_name: the class name in snake case without the Orbital suffix (ScopeGuardOrbital becomes scope_guard).

None
reads Sequence[str]

Names of earlier orbitals whose annotations this orbital needs.

()
timeout float

Seconds the engine waits for check before treating it as failed.

30.0
on_error ErrorPolicy

What the engine does when check fails or times out: "block" (default, fail-closed) or "allow".

'block'

Raises:

Type Description
ChatomonConfigError

If the name, reads, timeout or error policy is invalid, or the class does not set calls_model.

Examples:

from chatomon import Orbital, Verdict
from chatomon.schemas import OrbitalContext


class NoShouting(Orbital):
    calls_model = False

    async def check(self, ctx: OrbitalContext) -> Verdict:
        if ctx.message.isupper():
            return Verdict.allow(guidance="The user is shouting: stay calm.")
        return Verdict.allow()


assert NoShouting().name == "no_shouting"

Methods:

Name Description
default_name

Return the name an instance gets when none is given.

check

Check one message and return a verdict.

bind

Receive the atom's connector; called once by Atom.build().

config

Return the public configuration that identifies this orbital.

fingerprint

Return a stable hash of the configuration, recorded in audit reports.

Attributes:

Name Type Description
sides frozenset[Side]

Sides this orbital can run on. Subclasses that support only one side override it.

calls_model bool

Whether check calls a model or another remote service.

Source code in src/chatomon/orbitals/base.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def __init__(
    self,
    *,
    name: str | None = None,
    reads: Sequence[str] = (),
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block",
) -> None:
    if not isinstance(getattr(type(self), "calls_model", None), bool):
        raise ChatomonConfigError(undeclared(type(self)))
    resolved = name if name is not None else type(self).default_name()
    if not _NAME_PATTERN.fullmatch(resolved):
        raise ChatomonConfigError(
            f"invalid orbital name {resolved!r}: use letters, digits, '_', '-' or '.'"
        )
    if isinstance(reads, str):
        raise ChatomonConfigError("reads must be a list of orbital names, not a single string")
    if timeout <= 0:
        raise ChatomonConfigError(f"timeout must be positive, got {timeout!r}")
    if on_error not in ("block", "allow"):
        raise ChatomonConfigError(f"on_error must be 'block' or 'allow', got {on_error!r}")
    self.name: str = resolved
    self.reads: tuple[str, ...] = tuple(reads)
    self.timeout: float = float(timeout)
    self.on_error: ErrorPolicy = on_error

sides class-attribute

sides: frozenset[Side] = frozenset({'input', 'output'})

Sides this orbital can run on. Subclasses that support only one side override it.

calls_model instance-attribute

calls_model: bool

Whether check calls a model or another remote service.

Every orbital class sets it to True or False: there is no default, and an orbital whose class does not set it cannot be created. A subclass keeps the value of its parent. The engine does not read it; tools that need to know whether a turn can run without a network do.

default_name classmethod

default_name() -> str

Return the name an instance gets when none is given.

It is the class name in snake case without the Orbital suffix: ScopeGuardOrbital becomes scope_guard. A subclass may override it. FunctionOrbital does not use it: each instance is named after its function.

Returns:

Type Description
str

The default name.

Source code in src/chatomon/orbitals/base.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
@classmethod
def default_name(cls) -> str:
    """Return the name an instance gets when none is given.

    It is the class name in snake case without the `Orbital` suffix:
    `ScopeGuardOrbital` becomes `scope_guard`. A subclass may override it.
    [FunctionOrbital][chatomon.orbitals.base.FunctionOrbital] does not use
    it: each instance is named after its function.

    Returns:
        The default name.
    """
    base = cls.__name__
    if base.endswith("Orbital") and base != "Orbital":
        base = base[: -len("Orbital")]
    return re.sub(r"(?<!^)(?=[A-Z])", "_", base).lower()

check abstractmethod async

check(ctx: OrbitalContext) -> Verdict

Check one message and return a verdict.

Raising an exception is allowed: the engine applies on_error.

Parameters:

Name Type Description Default
ctx OrbitalContext

The text under check, the history and earlier annotations.

required

Returns:

Type Description
Verdict

The verdict.

Source code in src/chatomon/orbitals/base.py
137
138
139
140
141
142
143
144
145
146
147
148
@abstractmethod
async def check(self, ctx: OrbitalContext) -> Verdict:
    """Check one message and return a verdict.

    Raising an exception is allowed: the engine applies `on_error`.

    Args:
        ctx: The text under check, the history and earlier annotations.

    Returns:
        The verdict.
    """

bind

bind(connector: Connector[Any]) -> None

Receive the atom's connector; called once by Atom.build().

The default does nothing. Orbitals that call a model, such as the judge, use the connector to reach it.

Parameters:

Name Type Description Default
connector Connector[Any]

The connector of the atom being built.

required
Source code in src/chatomon/orbitals/base.py
150
151
152
153
154
155
156
157
158
159
def bind(self, connector: Connector[Any]) -> None:
    """Receive the atom's connector; called once by `Atom.build()`.

    The default does nothing. Orbitals that call a model, such as the judge,
    use the connector to reach it.

    Args:
        connector: The connector of the atom being built.
    """
    del connector

config

config() -> dict[str, Any]

Return the public configuration that identifies this orbital.

Subclasses extend the dictionary with their own settings. It feeds fingerprint.

Returns:

Type Description
dict[str, Any]

A JSON-serializable dictionary.

Source code in src/chatomon/orbitals/base.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def config(self) -> dict[str, Any]:
    """Return the public configuration that identifies this orbital.

    Subclasses extend the dictionary with their own settings. It feeds
    [fingerprint][chatomon.orbitals.base.Orbital.fingerprint].

    Returns:
        A JSON-serializable dictionary.
    """
    return {
        "kind": type(self).__qualname__,
        "name": self.name,
        "reads": list(self.reads),
        "timeout": self.timeout,
        "on_error": self.on_error,
    }

fingerprint

fingerprint() -> str

Return a stable hash of the configuration, recorded in audit reports.

Returns:

Type Description
str

"sha256:" followed by the hex digest of config() as canonical JSON.

Source code in src/chatomon/orbitals/base.py
178
179
180
181
182
183
184
def fingerprint(self) -> str:
    """Return a stable hash of the configuration, recorded in audit reports.

    Returns:
        `"sha256:"` followed by the hex digest of `config()` as canonical JSON.
    """
    return sha256_text(stable_json(self.config()))

FunctionOrbital

FunctionOrbital(
    func: OrbitalFunction,
    *,
    name: str | None = None,
    reads: Sequence[str] = (),
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block",
    calls_model: bool = False
)

Bases: Orbital

An orbital backed by a function; create it with orbital.

Sync functions run in a worker thread so they never block the event loop. The engine's timeout stops waiting for them, but a function that never returns keeps its thread busy, and the default thread pool is small: give blocking calls their own timeouts.

Parameters:

Name Type Description Default
func OrbitalFunction

Function taking an OrbitalContext and returning a Verdict.

required
name str | None

Defaults to the function name.

None
reads Sequence[str]

Names of earlier orbitals whose annotations this orbital needs.

()
timeout float

Seconds before the check is treated as failed.

30.0
on_error ErrorPolicy

"block" (default) or "allow".

'block'
calls_model bool

Whether the function calls a model or another remote service.

False

Methods:

Name Description
check

Call the wrapped function.

config

Return the configuration, including the wrapped function's path.

Attributes:

Name Type Description
calls_model

A function is taken as calling no model; pass calls_model=True for one that does.

Source code in src/chatomon/orbitals/base.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def __init__(
    self,
    func: OrbitalFunction,
    *,
    name: str | None = None,
    reads: Sequence[str] = (),
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block",
    calls_model: bool = False,
) -> None:
    super().__init__(
        name=name if name is not None else getattr(func, "__name__", "function"),
        reads=reads,
        timeout=timeout,
        on_error=on_error,
    )
    if not isinstance(calls_model, bool):
        raise ChatomonConfigError(f"calls_model must be True or False, got {calls_model!r}")
    self.func = func
    self.calls_model = calls_model

calls_model class-attribute instance-attribute

calls_model = calls_model

A function is taken as calling no model; pass calls_model=True for one that does.

check async

check(ctx: OrbitalContext) -> Verdict

Call the wrapped function.

Parameters:

Name Type Description Default
ctx OrbitalContext

The context passed to the function.

required

Returns:

Type Description
Verdict

The function's verdict.

Raises:

Type Description
TypeError

If the function does not return a Verdict.

Source code in src/chatomon/orbitals/base.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
async def check(self, ctx: OrbitalContext) -> Verdict:
    """Call the wrapped function.

    Args:
        ctx: The context passed to the function.

    Returns:
        The function's verdict.

    Raises:
        TypeError: If the function does not return a Verdict.
    """
    if inspect.iscoroutinefunction(self.func):
        result: object = self.func(ctx)
    else:
        result = await asyncio.to_thread(self.func, ctx)
    if inspect.isawaitable(result):
        result = await result
    if not isinstance(result, Verdict):
        raise TypeError(
            f"orbital {self.name!r} returned {type(result).__name__}, expected Verdict"
        )
    return result

config

config() -> dict[str, Any]

Return the configuration, including the wrapped function's path.

Returns:

Type Description
dict[str, Any]

A JSON-serializable dictionary.

Source code in src/chatomon/orbitals/base.py
256
257
258
259
260
261
262
263
264
def config(self) -> dict[str, Any]:
    """Return the configuration, including the wrapped function's path.

    Returns:
        A JSON-serializable dictionary.
    """
    module = getattr(self.func, "__module__", "?")
    qualname = getattr(self.func, "__qualname__", repr(self.func))
    return {**super().config(), "function": f"{module}.{qualname}"}

orbital

orbital(func: OrbitalFunction) -> FunctionOrbital
orbital(
    *,
    name: str | None = None,
    reads: Sequence[str] = (),
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block",
    calls_model: bool = False
) -> Callable[[OrbitalFunction], FunctionOrbital]
orbital(
    func: OrbitalFunction | None = None,
    /,
    *,
    name: str | None = None,
    reads: Sequence[str] = (),
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block",
    calls_model: bool = False,
) -> (
    FunctionOrbital
    | Callable[[OrbitalFunction], FunctionOrbital]
)

Turn a function into an orbital.

Use it bare (@orbital) or with the keyword arguments of FunctionOrbital.

Parameters:

Name Type Description Default
func OrbitalFunction | None

The function, when used bare.

None
name str | None

Defaults to the function name.

None
reads Sequence[str]

Names of earlier orbitals whose annotations this orbital needs.

()
timeout float

Seconds before the check is treated as failed.

30.0
on_error ErrorPolicy

"block" (default) or "allow".

'block'
calls_model bool

Whether the function calls a model or another remote service.

False

Returns:

Type Description
FunctionOrbital | Callable[[OrbitalFunction], FunctionOrbital]

A FunctionOrbital, or a decorator

FunctionOrbital | Callable[[OrbitalFunction], FunctionOrbital]

that creates one.

Examples:

from chatomon import Verdict, orbital
from chatomon.schemas import OrbitalContext


@orbital
async def no_competitors(ctx: OrbitalContext) -> Verdict:
    if "amazon" in ctx.message.lower():
        return Verdict.block(guidance="Do not mention competitors.")
    return Verdict.allow()


@orbital(name="length_limit", on_error="allow")
def length_limit(ctx: OrbitalContext) -> Verdict:
    return Verdict.block() if len(ctx.message) > 2000 else Verdict.allow()


assert no_competitors.name == "no_competitors"
assert length_limit.on_error == "allow"
Source code in src/chatomon/orbitals/base.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
def orbital(
    func: OrbitalFunction | None = None,
    /,
    *,
    name: str | None = None,
    reads: Sequence[str] = (),
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block",
    calls_model: bool = False,
) -> FunctionOrbital | Callable[[OrbitalFunction], FunctionOrbital]:
    """Turn a function into an orbital.

    Use it bare (`@orbital`) or with the keyword arguments of
    [FunctionOrbital][chatomon.orbitals.base.FunctionOrbital].

    Args:
        func: The function, when used bare.
        name: Defaults to the function name.
        reads: Names of earlier orbitals whose annotations this orbital needs.
        timeout: Seconds before the check is treated as failed.
        on_error: `"block"` (default) or `"allow"`.
        calls_model: Whether the function calls a model or another remote service.

    Returns:
        A [FunctionOrbital][chatomon.orbitals.base.FunctionOrbital], or a decorator
        that creates one.

    Examples:
        ```python
        from chatomon import Verdict, orbital
        from chatomon.schemas import OrbitalContext


        @orbital
        async def no_competitors(ctx: OrbitalContext) -> Verdict:
            if "amazon" in ctx.message.lower():
                return Verdict.block(guidance="Do not mention competitors.")
            return Verdict.allow()


        @orbital(name="length_limit", on_error="allow")
        def length_limit(ctx: OrbitalContext) -> Verdict:
            return Verdict.block() if len(ctx.message) > 2000 else Verdict.allow()


        assert no_competitors.name == "no_competitors"
        assert length_limit.on_error == "allow"
        ```
    """

    def wrap(f: OrbitalFunction) -> FunctionOrbital:
        return FunctionOrbital(
            f, name=name, reads=reads, timeout=timeout, on_error=on_error, calls_model=calls_model
        )

    return wrap(func) if func is not None else wrap