Package: enact
enact - A Framework for Generative Software.Enact is a python framework for building generative software, specifically
software that integrates with machine learning models or APIs that generate
distributions of outputs.Key featuresEnact was created to help developers build generative software. Our focus is on
offering core framework functionality required for developing, inspecting and
improving software that samples from generative subsystems such as LLMs and
diffusion models. Seewhy-enactfor a more in-depth discussion. To
this end, enact boosts your existing python programs by giving you the ability
torecursively track generative executions,persist execution data and system parameters in versioned storage,explore the space of possible system executions via rewind and replay,turn your existing code into asynchronous flows that can halt and
sample input from users, andorchestrate higher-order generative flows in which AI systems write and run
software.Enact represents a first-principles approach to supporting development of
generative software close to the level of the programming language. It is
therefore different from and complementary to libraries that focus on unified
integrations (e.g., API wrappers) or on particular algorithm schemas for
orchestrating generative flows (e.g., chain/tree-of-thought based approaches).Installation and overviewEnact is available as apypi packageand can
be installed via:pipinstallenactInvocationsEnact recursively tracks inputs and outputs of functions registered with the
framework. A record of an execution is called anInvocation, and can be
persisted to a store. This is useful for tracking the behavior generative
software.importenactimportrandom@enact.registerdefroll_die(sides:int)->int:"""Roll a die."""returnrandom.randint(1,sides)@enact.registerdefroll_sum(num_rolls:int)->int:"""Roll dice."""returnsum(roll_die(6)for_inrange(num_rolls))withenact.InMemoryStore()asstore:invocation=enact.invoke(roll_sum,(2,))print(enact.invocation_summary(invocation))Output:->roll_sum(2) = 7
  ->roll_die(6) = 2
  ->roll_die(6) = 5Rewind/ReplayInvocations can be rewound and replayed:# Rewind the last roll and replay it.withstore:first_roll=invocation.rewind(1)# Rewind by one roll.print('Partial invocation: ')print(enact.invocation_summary(first_roll))reroll_second=first_roll.replay()# Replay the second die roll.print('\nReplayed invocation: ')print(enact.invocation_summary(reroll_second))Output:Partialinvocation:->roll_sum(2)incomplete->roll_die(6)=2Replayedinvocation:->roll_sum(2)=8->roll_die(6)=2->roll_die(6)=6Human-in-the-loopIn enact, humans are treated as just another generative distribution. Their
input can be sampled usingenact.request_input.@enact.registerdefhuman_rolls_die():"""Query the user for a die roll."""returnenact.request_input(requested_type=int,for_value='Please roll a die.')@enact.registerdefroll_dice_user_flow():"""Request the number of die to roll and optionally sample die rolls."""num_rolls=enact.request_input(requested_type=int,for_value='Total number of rolls?')returnsum(human_rolls_die()for_inrange(num_rolls))request_responses={'Total number of rolls?':3,# Roll 3 dice.'Please roll a die.':6,# Humans always roll 6.}withstore:invocation_gen=enact.InvocationGenerator.from_callable(roll_dice_user_flow)# Process all user input requests in order.forinput_requestininvocation_gen:invocation_gen.set_input(request_responses[input_request.for_value()])print(enact.invocation_summary(invocation_gen.invocation))Output:->roll_dice_user_flow() = 18
  ->RequestInput(requested_type=<class 'int'>, context=None)(Total number of rolls?) = 3
  ->human_rolls_die() = 6
    ->RequestInput(requested_type=<class 'int'>, context=None)(Please roll a die.) = 6
  ->human_rolls_die() = 6
    ->RequestInput(requested_type=<class 'int'>, context=None)(Please roll a die.) = 6
  ->human_rolls_die() = 6
    ->RequestInput(requested_type=<class 'int'>, context=None)(Please roll a die.) = 6Making your types enact compatibleEnact has built-in support for basic python datatypes and a few other common
types common to generative software, such as numpy arrays and pillow images.If you need support for additional types, you can either define them asResourcedataclasses in the framework or define a wrapper.Defining a resourceimportdataclasses@enact.register@dataclasses.dataclassclassMyResource(enact.Resource):x:inty:list=dataclasses.field(default_factory=list)withstore:# Commit your resource to the store, obtain a reference.ref=enact.commit(MyResource(x=1,y=[2,3]))# Check out your reference.print(ref.checkout())# Equivalent to "print(ref())".Output:MyResource(x=1,y=[2,3])Defining a wrapperFor existing types, it can be more convenient to wrap them rather than to
redefine them as enactResourceobjects.This can be accomplished by registering aTypeWrappersubclass.classDie:"""Non-enact python type."""def__init__(self,sides):self.sides=sides@enact.registerdefroll(self):returnrandom.randint(1,self.sides)@enact.register@dataclasses.dataclassclassDieWrapper(enact.TypeWrapper[Die]):"""Wrapper for Die."""sides:int@classmethoddefwrapped_type(cls):returnDie@classmethoddefwrap(cls,value:Die):"""Translate to enact wrapper."""returnDieWrapper(value.sides)defunwrap(self)->Die:"""Translate to basic python type."""returnDie(self.sides)withstore:die=Die(sides=6)invocation=enact.invoke(die.roll)print(enact.invocation_summary(invocation))Output:-><__main__.Die object at 0x7f3464398340>.roll() = 4DocumentationFull documentation is work in progress.  See thequickstartandenact
conceptsfor more information.
And take a look at otherexamples.Why enact - A manifestoGenerative AI models are transforming the software development process.Traditional software relies primarily on functional buildings blocks in which
inputs and system state directly determine outputs. In contrast, software
that samples one or more generative subsystems associates each input with
a range of possible outputs.This small change in emphasis - from functions to conditional distributions -
implies a shift across multiple dimensions of the engineering process,
summarized in the table below.GenerativeTraditionalBuilding blockConditional distributionsDeterministic functionsEngineeringImprove output distributionsAdd features, debug errorsSubsystemsUniqueInterchangeableCode sharingComponentsFrameworksExecutionsTraining dataLogged for metrics/debuggingInteractivityWithin system componentsAt system boundariesCode vs dataOverlappingDistinctBuilding generative software means fitting distributionsGenerative AI can be used to quickly sketch out impressive end-to-end
experiences. Prototypes of text-based agents, for instance, often involve little
more than calling an API with a well-chosen prompt. Evolving such prototypes
into production-ready systems can be non-trivial though, as they may show
undesirable behaviors a certain percentage of the time, may fail to respond
correctly to unusual user inputs or may simply fail to meet a softly defined
quality target.Where traditional engineering focuses on implementing features and fixing bugs,
generative software engineering tunes the distribution represented by the system
as a whole towards some implicitly specified target, e.g., a set of example
outputs, corrections of previous outputs or task rewards.In some cases, tuning generative systems can be achieved directly using machine
learning techniques and frameworks. For instance, systems that consist of a thin
software wrapper around a monolithic machine learning model may directly train
the underlying model to shape system behavior.In other cases, particularly when running complex generative flows that involve
multiple interacting subcomponents that cannot be easily optimized end-to-end,
gradient-free methods are necessary to improve system performance. These range
from using programmers (either human or synthetic) to elaborate the algorithmic
guard-rails of the generative process, running experiments between API identical
subsystems or finding ideal parameters (e.g., prompt templates) for individual
components.Recursive elaboration and end-to-end compressionIn generative software, individual components produce distributions that may be
more or less well-suited to the system's overall goal: An LLM that performs well
on tabular data may perform less well when used as a chat agent. One fine-tuned
diffusion model may produce images that are closer to the target style than
another. This is distinct from the the case of traditional software engineering,
where the choice between two correct, API-identical implementations of a module
is of little importance to the behavior of the system as a whole.In addition to a choice between different machine learning models, there is a
choice between multiple algorithmic elaborations of a given model. The behavior
of GPT, for example, can be improved by providing algorithmic guardrails that
structure and steer the generation process such as chain-of-thought or
tree-of-thought prompting.The profusion of base models and possible algorithmic elaborations of base
models suggests that gradient-free search methods will take a central place in
generative software development. This could be as simple as running an
experiment in which one component is compared against another or as complex as
searching the space of software, e.g., using LLM-boosted genetic algorithms.Once a component has been optimized using such methods, the resulting data can
be used in gradient-based optimization to 'compress' the gained knowledge into
end-to-end trained models. This suggests that generative systems will alternate
gradient-based and gradient-free methods, with the former supplying new training
signal for the latter. (This loop is the core insight behind algorithms in the
AlphaZero family, in which data from a search-based elaboration of a model is
used to train the next iteration of the model.)Generative software is collaborativeThere are many generative AI components that are mutually API-compatible (e.g.,
text-to-image generation, LLMs) but that cannot be directly ranked in terms of
quality since they represent a unique take on the problem domain. The
combination of API compatibility and variation lends itself to a more
evolutionary, collaborative style than traditional software, where shared effort
tends to centralize into fewer, lower-level frameworks.This new collaborative approach is already evidenced by both widespread sharing
of prompt templates and the existence of public databases of fine-tuned models.Generative software reflects on its executions.Many popular programming languages offer capacity for reflection, wherein the
structure of code is programmatically interpretable by code. Generative software
additionally requiresdeep reflection, that is, the ability for code to
reflect on code execution.Sampling a generative model is computationally intensive and provides valuable
information that may be used as system feedback or training data. Since
generative software may include complex, recursive algorithms over generative
components, this suggests a need for not simply tracking inputs and outputs
at system boundaries, but at every level of execution.This is particularly relevant to higher order generative flows, in which the
output of one generative step structures the execution of the next: Such a
system can use call traces as feedback to improve its output, in the same way
that a human may use a debugger to step through an execution to debug an issue.Humans are in the loopUnlike traditional systems, where user interactions happen at system boundaries,
AI and humans are often equal participants in generative computations. An
example is Reinforcement Learning from Human Feedback (RLHF), where humans
provide feedback on AI output, only to be replaced by an AI model that mimics
their feedback behavior during the training process. In other cases, critical
generative flows (e.g., the drafting of legal documents) may require a human
verification step, without which the system provides subpar results.The choice between sampling human input and sampling a generative model is
involves considerations such as cost and quality, and may be subject to change
during the development of a system. For example, an early deployment of a system
may heavily sample outputs or feedback from human participants, until there is
enough data to bootstrap the target distribution using automated generative
components.Therefore, a generative system should be designed to utilize human input in the
same way it would interact with an API or subsystem.Data is code, code is dataTraditional software systems tend to allow a clear distinction between code
(written by developers) and data (generated by the user and the system). In
generative systems this distinction breaks down: Approaches such asAutoGPTorVoyageruse generative AI to generate
programs (specified in code or plain text), which in turn may be interpreted by
generative AI systems; prompts for chat-based generative AI could equally be
considered code or data.Development and ContributingThe framework is open source and Apache licensed. We are actively looking for
contributors that are excited about the vision. If you're interested in the
project, feel free to reach out toleo@agentic.ai.You can download the source code, report issues and create pull requests athttps://github.com/agentic-ai/enact.