Skip to content

chatomon.orbitals.principled

Orbitals backed by Principled Intelligence models (the orbitals library).

Install them with pip install 'chatomon[principled]'. Each orbital receives a client you configure yourself (local, self-hosted or API backend) and its own service description: it is never derived from the atom. For a service reached over HTTP, ApiBackend is a shorter way to name it than building the client.

Classes:

Name Description
ApiBackend

A Principled Intelligence service reached over HTTP: the api backend of orbitals.

ScopeGuardOrbital

Classifies the user message with ScopeGuard (V1 or V2) and maps the class to a verdict.

ClaimExtractorOrbital

Extracts claims and intents from the last message; never blocks.

Attributes:

Name Type Description
HeadersProvider

Sync or async function returning HTTP headers, called before every request.

ScopeRule

A fixed verdict, or a function building one from the ScopeGuard result.

CHIT_CHAT_GUIDANCE

Guidance used for the Chit Chat class.

OUT_OF_SCOPE_GUIDANCE

Guidance used for the Out of Scope class.

V2_OUTPUT_FIELDS

Output fields always requested from ScopeGuard V2.

DEFAULT_SCOPE_RULES Mapping[str, ScopeRule]

Default verdict for every ScopeGuard class (V2 classes include V1 ones).

HeadersProvider module-attribute

HeadersProvider = Callable[
    [], Mapping[str, str] | Awaitable[Mapping[str, str]]
]

Sync or async function returning HTTP headers, called before every request.

ScopeRule module-attribute

ScopeRule = Verdict | Callable[[ScopeGuardResult], Verdict]

A fixed verdict, or a function building one from the ScopeGuard result.

CHIT_CHAT_GUIDANCE module-attribute

CHIT_CHAT_GUIDANCE = "Reply briefly and steer the conversation back to the service."

Guidance used for the Chit Chat class.

OUT_OF_SCOPE_GUIDANCE module-attribute

OUT_OF_SCOPE_GUIDANCE = "The request is outside your scope. Politely explain what you can help with."

Guidance used for the Out of Scope class.

V2_OUTPUT_FIELDS module-attribute

V2_OUTPUT_FIELDS = (
    "scope_class",
    "evidences",
    "reasoning",
    "suggested_response",
)

Output fields always requested from ScopeGuard V2.

DEFAULT_SCOPE_RULES module-attribute

DEFAULT_SCOPE_RULES: Mapping[str, ScopeRule] = {
    "Directly Supported": Verdict.allow(),
    "Potentially Supported": Verdict.allow(),
    "Chit Chat": Verdict.allow(CHIT_CHAT_GUIDANCE),
    "Predefined Answer": _predefined_answer,
    "Human Oversight": _human_oversight,
    "Out of Scope": Verdict.block(
        guidance=OUT_OF_SCOPE_GUIDANCE
    ),
    "Restricted": Verdict.block(),
}

Default verdict for every ScopeGuard class (V2 classes include V1 ones).

ApiBackend dataclass

ApiBackend(
    *,
    api_url: str,
    api_key_env: str | None = None,
    model: str | None = None
)

A Principled Intelligence service reached over HTTP: the api backend of orbitals.

A shortcut for the common case. Give it to ScopeGuardOrbital as guard or to ClaimExtractorOrbital as extractor, and the orbital builds the asynchronous client itself. For anything else, such as a local backend or a client with its own settings, pass a client you configured.

It holds the name of the variable, never the key: the orbital reads the variable when it is created.

Parameters:

Name Type Description Default
api_url str

Where the service answers, starting with http:// or https://.

required
api_key_env str | None

Name of the environment variable that holds the API key. When None no key is given, and the client of the orbitals library uses PRINCIPLED_API_KEY if that is set.

None
model str | None

Model the service should use; the library's default when None.

None

Raises:

Type Description
ChatomonConfigError

If the address is not an HTTP address, holds a user, a password, a query or a fragment, or api_key_env is not the name of a variable. The message never quotes the value it refuses.

Examples:

from chatomon.orbitals.principled import ApiBackend

backend = ApiBackend(api_url="http://127.0.0.1:8080", api_key_env="SCOPE_GUARD_KEY")
assert backend.api_key_env == "SCOPE_GUARD_KEY"  # the name: the value is never stored

ScopeGuardOrbital

ScopeGuardOrbital(
    *,
    guard: ScopeGuardClient | ApiBackend,
    description: (
        str | AIServiceDescription | AIServiceDescriptionV2
    ),
    mapping: Mapping[str | Enum, ScopeRule] | None = None,
    headers: HeadersProvider | None = None,
    history_window: int = 6,
    name: str | None = None,
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block"
)

Bases: Orbital

Classifies the user message with ScopeGuard (V1 or V2) and maps the class to a verdict.

Input side only: ScopeGuard classifies the last user message. Default mapping:

Class Verdict
Directly / Potentially Supported allow
Chit Chat allow with a short "steer back" guidance
Predefined Answer (V2) respond with suggested_response
Human Oversight (V2) respond with suggested_response, escalate
Out of Scope block with "explain what you can help with" guidance
Restricted block (default refusal)

Parameters:

Name Type Description Default
guard ScopeGuardClient | ApiBackend

A ScopeGuard client you configured (AsyncScopeGuardV2, ScopeGuardV2, AsyncScopeGuard or ScopeGuard), or an ApiBackend, from which the orbital builds an AsyncScopeGuardV2 itself. Async clients are recommended: sync ones run in a worker thread, and a request that hangs keeps that thread busy after the timeout (the sync client sets no HTTP timeout of its own).

required
description str | AIServiceDescription | AIServiceDescriptionV2

The guard's own service description (a string, or AIServiceDescriptionV2 for V2 / AIServiceDescription for V1).

required
mapping Mapping[str | Enum, ScopeRule] | None

Overrides for some classes; keys are class enums or their string values, values are verdicts or functions of the result.

None
headers HeadersProvider | None

Called before every request to refresh HTTP headers (for example a Cloud Run identity token). Needs an api backend client.

None
history_window int

Number of history messages sent with the user message.

6
name str | None

Unique name inside the atom; defaults to "scope_guard".

None
timeout float

Seconds before the check is treated as failed.

30.0
on_error ErrorPolicy

"block" (default) or "allow".

'block'

Raises:

Type Description
ChatomonConfigError

If a setting is invalid, or the environment variable an ApiBackend names is not set.

Examples:

from orbitals.scope_guard_v2 import AsyncScopeGuardV2
from orbitals.types import AIServiceDescriptionV2

from chatomon.orbitals.principled import ScopeGuardOrbital

scope_guard = ScopeGuardOrbital(
    guard=AsyncScopeGuardV2(backend="api", api_url="https://scope-guard.example.run.app"),
    description=AIServiceDescriptionV2(
        identity_role="Parcel tracking assistant of SpeedyPack",
        context="Italian courier; customers ask about their shipments",
        constraints=["Never discuss refunds"],
    ),
)

The same service named by plain values, with its key in a variable:

from chatomon.orbitals.principled import ApiBackend, ScopeGuardOrbital

scope_guard = ScopeGuardOrbital(
    guard=ApiBackend(
        api_url="https://scope-guard.example.run.app",
        api_key_env="PRINCIPLED_API_KEY",
    ),
    description="Parcel tracking assistant of SpeedyPack, an Italian courier.",
)

Methods:

Name Description
check

Classify the user message and apply the rule of its class.

config

Return the configuration: client, description, rules and window.

Attributes:

Name Type Description
calls_model

Every check is a call to the service.

Source code in src/chatomon/orbitals/principled.py
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
def __init__(
    self,
    *,
    guard: ScopeGuardClient | ApiBackend,
    description: str | AIServiceDescription | AIServiceDescriptionV2,
    mapping: Mapping[str | Enum, ScopeRule] | None = None,
    headers: HeadersProvider | None = None,
    history_window: int = 6,
    name: str | None = None,
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block",
) -> None:
    super().__init__(name=name, timeout=timeout, on_error=on_error)
    if isinstance(guard, ApiBackend):
        client: ScopeGuardClient = AsyncScopeGuardV2(
            backend="api", api_url=guard.api_url, model=guard.model, api_key=_api_key(guard)
        )
    elif isinstance(guard, AsyncScopeGuardV2 | ScopeGuardV2 | AsyncScopeGuard | ScopeGuard):
        client = guard
    else:
        raise ChatomonConfigError(
            f"guard must be a ScopeGuard client from the orbitals library or an ApiBackend, "
            f"got {type(guard).__name__}"
        )
    self.is_v2 = isinstance(client, AsyncScopeGuardV2 | ScopeGuardV2)
    if self.is_v2 and isinstance(description, AIServiceDescription):
        raise ChatomonConfigError(
            "ScopeGuard V2 needs an AIServiceDescriptionV2 (or a string) as description"
        )
    if not self.is_v2 and isinstance(description, AIServiceDescriptionV2):
        raise ChatomonConfigError(
            "ScopeGuard V1 needs an AIServiceDescription (or a string) as description"
        )
    _check_headers(client, headers)
    if history_window < 0:
        raise ChatomonConfigError(f"history_window must be >= 0, got {history_window!r}")
    # What was given, a client or a backend; `_client` is what the orbital calls.
    self.guard = guard
    self._client = client
    self.description = description
    self.headers = headers
    self.history_window = history_window
    # The overrides as they were given; `rules` is what the orbital applies.
    self.mapping = mapping
    self.rules: dict[str, ScopeRule] = dict(DEFAULT_SCOPE_RULES)
    for key, rule in (mapping or {}).items():
        self.rules[_scope_key(key)] = rule

calls_model class-attribute instance-attribute

calls_model = True

Every check is a call to the service.

check async

check(ctx: OrbitalContext) -> Verdict

Classify the user message and apply the rule of its class.

Parameters:

Name Type Description Default
ctx OrbitalContext

The orbital context.

required

Returns:

Type Description
Verdict

The verdict, with the class as reason and ScopeGuard's output as annotations.

Source code in src/chatomon/orbitals/principled.py
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
async def check(self, ctx: OrbitalContext) -> Verdict:
    """Classify the user message and apply the rule of its class.

    Args:
        ctx: The orbital context.

    Returns:
        The verdict, with the class as reason and ScopeGuard's output as annotations.
    """
    await _refresh_headers(self._client, self.headers)
    messages = (
        *_window(ctx.history, self.history_window),
        Message(role="user", content=ctx.message),
    )
    kwargs: dict[str, Any] = {"ai_service_description": self.description}
    if self.is_v2:
        kwargs["output_fields"] = list(V2_OUTPUT_FIELDS)
    result = await _call(self._client.validate, _conversation(messages), **kwargs)
    scope = result.scope_class.value
    rule = self.rules[scope]
    verdict = rule(result) if callable(rule) else rule
    annotations = verdict.annotations
    if annotations is None:
        annotations = {
            "scope_class": scope,
            "evidences": result.evidences,
            "reasoning": getattr(result, "reasoning", None),
        }
    return verdict.model_copy(
        update={"reason": verdict.reason or scope, "annotations": annotations}
    )

config

config() -> dict[str, Any]

Return the configuration: client, description, rules and window.

Returns:

Type Description
dict[str, Any]

A JSON-serializable dictionary.

Source code in src/chatomon/orbitals/principled.py
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def config(self) -> dict[str, Any]:
    """Return the configuration: client, description, rules and window.

    Returns:
        A JSON-serializable dictionary.
    """
    description = (
        self.description.model_dump()
        if isinstance(self.description, BaseModel)
        else self.description
    )
    rules = {key: _describe_rule(rule) for key, rule in sorted(self.rules.items())}
    return {
        **super().config(),
        "guard": _describe_client(self._client),
        "description": description,
        "rules": rules,
        "history_window": self.history_window,
    }

ClaimExtractorOrbital

ClaimExtractorOrbital(
    *,
    extractor: (
        AsyncClaimExtractor | ClaimExtractor | ApiBackend
    ),
    description: str | AIServiceDescription | None = None,
    intents_only: bool = False,
    headers: HeadersProvider | None = None,
    history_window: int = 6,
    name: str | None = None,
    reads: Sequence[str] = (),
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block"
)

Bases: Orbital

Extracts claims and intents from the last message; never blocks.

Its annotations (an Extractions object with claims and intents) are meant for a later orbital that reads them, typically a JudgeOrbital with reads=[...]. Works on both sides: on the output side it analyses the nucleus reply.

Parameters:

Name Type Description Default
extractor AsyncClaimExtractor | ClaimExtractor | ApiBackend

A ClaimExtractor client you configured (AsyncClaimExtractor or ClaimExtractor), or an ApiBackend, from which the orbital builds an AsyncClaimExtractor itself.

required
description str | AIServiceDescription | None

Optional service description (string or AIServiceDescription).

None
intents_only bool

Extract only intents.

False
headers HeadersProvider | None

Called before every request to refresh HTTP headers.

None
history_window int

Number of history messages sent with the last message.

6
name str | None

Unique name inside the atom; defaults to "claim_extractor".

None
reads Sequence[str]

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'

Raises:

Type Description
ChatomonConfigError

If a setting is invalid, or the environment variable an ApiBackend names is not set.

Examples:

from orbitals.claim_extractor import AsyncClaimExtractor

from chatomon import Knowledge
from chatomon.orbitals import JudgeOrbital
from chatomon.orbitals.principled import ClaimExtractorOrbital

faq = Knowledge("Opening hours: Monday to Friday, 9 to 18.")
output_orbitals = [
    ClaimExtractorOrbital(
        name="claims",
        extractor=AsyncClaimExtractor(
            backend="api", api_url="https://claims.example.run.app"
        ),
    ),
    JudgeOrbital(
        name="fact_check",
        model="openai:gpt-5-mini",
        instructions="Block replies whose claims are not supported by the reference.",
        context=faq,
        reads=["claims"],
    ),
]

Methods:

Name Description
check

Extract claims and intents from the message under check.

config

Return the configuration: client, description and options.

Attributes:

Name Type Description
calls_model

Every check is a call to the service.

Source code in src/chatomon/orbitals/principled.py
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
def __init__(
    self,
    *,
    extractor: AsyncClaimExtractor | ClaimExtractor | ApiBackend,
    description: str | AIServiceDescription | None = None,
    intents_only: bool = False,
    headers: HeadersProvider | None = None,
    history_window: int = 6,
    name: str | None = None,
    reads: Sequence[str] = (),
    timeout: float = 30.0,
    on_error: ErrorPolicy = "block",
) -> None:
    super().__init__(name=name, reads=reads, timeout=timeout, on_error=on_error)
    client: AsyncClaimExtractor | ClaimExtractor
    if isinstance(extractor, ApiBackend):
        url, key = extractor.api_url, _api_key(extractor)
        if extractor.model is None:
            # No model is given, so the library keeps its default.
            client = AsyncClaimExtractor(backend="api", api_url=url, api_key=key)
        else:
            client = AsyncClaimExtractor(
                backend="api", api_url=url, api_key=key, model=extractor.model
            )
    elif isinstance(extractor, AsyncClaimExtractor | ClaimExtractor):
        client = extractor
    else:
        raise ChatomonConfigError(
            f"extractor must be a ClaimExtractor client from the orbitals library "
            f"or an ApiBackend, got {type(extractor).__name__}"
        )
    _check_headers(client, headers)
    if history_window < 0:
        raise ChatomonConfigError(f"history_window must be >= 0, got {history_window!r}")
    # What was given, a client or a backend; `_client` is what the orbital calls.
    self.extractor = extractor
    self._client = client
    self.description = description
    self.intents_only = intents_only
    self.headers = headers
    self.history_window = history_window

calls_model class-attribute instance-attribute

calls_model = True

Every check is a call to the service.

check async

check(ctx: OrbitalContext) -> Verdict

Extract claims and intents from the message under check.

Parameters:

Name Type Description Default
ctx OrbitalContext

The orbital context.

required

Returns:

Type Description
Verdict

An allow verdict whose annotations are the extractions.

Source code in src/chatomon/orbitals/principled.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
async def check(self, ctx: OrbitalContext) -> Verdict:
    """Extract claims and intents from the message under check.

    Args:
        ctx: The orbital context.

    Returns:
        An `allow` verdict whose annotations are the extractions.
    """
    await _refresh_headers(self._client, self.headers)
    role = "user" if ctx.side == "input" else "assistant"
    messages = (
        *_window(ctx.history, self.history_window),
        Message(role=role, content=ctx.message),
    )
    result = await _call(
        self._client.extract,
        _conversation(messages),
        ai_service_description=self.description,
        intents_only=self.intents_only,
    )
    return Verdict.allow(annotations=result.extractions)

config

config() -> dict[str, Any]

Return the configuration: client, description and options.

Returns:

Type Description
dict[str, Any]

A JSON-serializable dictionary.

Source code in src/chatomon/orbitals/principled.py
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
def config(self) -> dict[str, Any]:
    """Return the configuration: client, description and options.

    Returns:
        A JSON-serializable dictionary.
    """
    description = (
        self.description.model_dump()
        if isinstance(self.description, BaseModel)
        else self.description
    )
    return {
        **super().config(),
        "extractor": _describe_client(self._client),
        "description": description,
        "intents_only": self.intents_only,
        "history_window": self.history_window,
    }