Interaction System
Horus uses interactions for typed runtime prompting.
An interaction is made of three parts:
- an interaction model that describes the prompt and parses the result
- a transport that delivers the prompt to a frontend
- a renderer that connects a specific interaction type to a specific transport
Core Concept
The core interaction model is BaseInteraction[T]:
class BaseInteraction[T](AutoRegistry, entry_point="interaction"):
registry_key: ClassVar[str] = "kind"
kind: str
value_key: str
title: str | None = None
prompt: str | None = None
description: str | None = None
default: T | None = None
value: T | None = None
@abstractmethod
async def parse(self, value: object) -> T:
...
Each interaction:
- declares a
kind - describes what should be shown to the user
- parses raw renderer output into a typed value
Transports and Renderers
BaseInteractionTransport is responsible for asking an interaction:
result = await transport.ask(interaction, max_retries=3)
When ask() runs, Horus:
- looks up the renderer for the transport and interaction pair
- emits interaction lifecycle events
- renders the prompt
- parses the raw answer
- retries on parse errors until
max_retriesis exhausted
Renderers are registered per transport/interaction pair through a derived key:
<transport kind>:<interaction kind>
For example, the built-in CLI string renderer is registered as cli:string.
Built-in Interactions
Horus currently includes these built-in interaction types:
StringInteractionConfirmInteractionDropdownInteractionFileInteraction
It also includes:
CLIInteractionTransport- CLI renderers for the built-in interactions above
Task Integration
Interactions are task-oriented.
BaseTask has access to an interaction transport through the interaction field:
interaction: BaseInteractionTransport | None = None
This allows task code to ask runtime questions through its configured transport.
FunctionTask defaults this field to CLIInteractionTransport(), which makes
interactive code-first workflows straightforward to author.
FunctionTask Example
from horus_builtin.interaction.common.string import StringInteraction
from horus_builtin.task.function import FunctionTask
from horus_runtime.core.task.base import BaseTask
@FunctionTask.task(wf)
async def choose_dataset(task: BaseTask) -> None:
assert task.interaction is not None
dataset = await task.interaction.ask(
StringInteraction(
value_key="dataset-name",
title="Dataset",
prompt="Which dataset should be used?",
default="sample",
)
)
print(dataset)
Interaction Events
The transport emits events during the interaction lifecycle:
InteractionAskedEventInteractionAnsweredEventInteractionRetryEventInteractionFailedEvent
These events integrate with the normal Horus event bus.
Registering Custom Interaction Plugins
Custom interaction components are registered through Python entry points:
[project.entry-points."horus.interaction"]
common = "horus_builtin.interaction.common"
[project.entry-points."horus.interaction_transport"]
cli = "horus_builtin.interaction.cli"
[project.entry-points."horus.interaction_renderer"]
cli = "horus_builtin.interaction.cli"
Use:
horus.interactionfor interaction model typeshorus.interaction_transportfor transportshorus.interaction_rendererfor renderers