The Soul Engine is a powerful tool for creating, developing, and deploying AI souls. Unlike ChatBots, which are reactive systems, souls are dynamic, agentic, and stateful entities that are steerable by the developer, enabling the creation of engaging user and player experiences interacting with the souls.
To get started, clone a soul and its code to your local machine
npx soul-engine init <my-soul-name>
Finally, navigate to the root directory of your cloned soul and run
cd <my-soul-name>
npx soul-engine dev
which will connect your soul to the engine.
β When your soul is connected to the engine, any file changes are watched and pushed to the engine. The soul is then continually hosted live behind API
Integrating a soul with your application is simple via the soul-engine/souls
API. The API offers ReplicaCycle
, a client that manages connections with a soul running on the engine, including across cycles with different users.
import { ReplicaCycle, Actions, said, Events } from "soul-engine/soul";
// connect to the hosted soul running the subroutine "opensouls/samantha-provokes"
const samantha = new ReplicaCycle({
organization: "opensouls",
subroutine: "samantha-provokes",
})
// start a new cycle with the soul
const cycleId = await samantha.start()
// register listener to "SAYS" events from the soul
samantha.on(Actions.SAYS, ({ content, stream }) => {
// stream is a function that returns an AsyncIterable<string>
for await (const message of stream()) {
// do things
}
// content returns a Promise<string> that will
// resolve when all content is available
console.log(await content())
})
// give the soul a new perception of the user saying 'Hi!'
await samantha.newPerception(said("User", "Hi!"))
Any cycle can be resumed at a later point in time as well
const samantha = new ReplicaCycle({
organization: "opensouls",
subroutine: "samantha-provokes"
})
await samantha.resume(cycleId)
π§ Currently the dev and production environments are the same, so whatever files are deployed via
npx soulengine
for your soul are the ones being run by the engine
The Soul Engine API has two major components:
- Souls API - integrate hosted souls into your applications
- Engine API - create, debug, and host souls
The ReplicaCycle
API provides a convenient way to integrate hosted souls defined via the Engine API into your applications.
const samantha = new ReplicaCycle({
organization: "opensouls",
subroutine: "samantha-provokes",
})
Parameters:
organization
: the organization's unique identifiersubroutine
: unique url-safe name for the subroutine to pull from theorganization
organization
Methods:
start()
: initiates execution of the new soul's Subroutine, returns the promise of acycleId
upon connectionresume(cycleId: string)
: resumes execution of the new soul's Subroutine in the cycle associated with thecycleId
, returns a promise of the cycleId upon connectionnewPerception(perception: string)
: pushes a new memory on the soul's working memory and calls execution of theSubroutine
's currentMentalProcess
, possibly triggering aSAYS
event. We provide convenience methods for different perceptions such assays(userName: string, message: string)
stop()
: closes theReplicaCycle
π§ Currently the API is unauthenticated - will add authentication here on request
The Soul Engine provides a powerful and extensible API for directing the cognition of AI souls. At a high level, unlike existing tools like the Assistants API, Inworld, or Character.AI, the Soul Engine provides an API for the specification of digital souls as dynamical and responsive stateful systems.
Creating a soul powered by the Soul Engine begins with creation of a soul Blueprint
in the /src
directory of a soul's project, which defines the execution of a something we call a soul's Subroutine
.
β The nomenclature of subroutine is inspired by named by the subroutines of Data in Star Trek, which encapsulate an entity performing a particular skill or behavior in a defined scenario over some particular time.
Specifically, the Blueprint
is defined and exported in the src/soul.ts
file of a project. Let's consider the following example Blueprint
:
// src/soul.ts
import { Blueprint } from "@opensouls/engine";
import provokesSpeaker from "./provokesSpeaker.js";
import playsVictim from "./playsVictim.js";
import { html } from "common-tags";
const blueprint: Blueprint = {
subroutine: "samantha-shouts",
entity: "Samantha",
context: html`
You are modeling the mind of Samantha.
## Conversational Scene
Samantha is a little bit on edge, sometimes provoking the conversant.
## Samantha's Speaking Style
* Samantha speaks very informally, mostly lowercase.
* Lots of gen-z slang.
* Samantha texts MAX 1-2 sentences at a time
`,
initialProcess: provokesSpeaker,
mentalProcesses: [
provokesSpeaker,
playsVictim,
],
}
export blueprint;
Parameters:
The definition of a Blueprint
comprises:
subroutine
: url-safe name of the subroutineentity
: the name of the soul's self identificationcontext
: the high level context for the soul during the running of the subroutineinitialProcess
: a reference to aMentalProcess
. This process serves as the entrypoint to the soul's cognitionmentalProcesses
: a list of everyMentalProcess
that the Subroutine might visit in the main threadsubprocesses
(optional): a list of everyMentalProcess
that is run continuously as a subprocess of the main thread
The MentalProcess
API gives a powerful and functional way to specify stateful behavior of a soul, triggered by an external Perception
.
A soul's Subroutine
only ever has a single (main-threaded) active MentalProcess
, which defines the current behavior set. When a MentalProcess
executes, it operates on the current step
of the WorkingMemory
, returning a new step
of the WorkingMemory
.
β As the
WorkingMemory
grows with new memories, the oldest memories are compressed and stored to potentially be recalled
During operation on the WorkingMemory
's current step, a soul will often generate new memories like internalMonologue
or externalDialog
and take actions like speak
.
Every mental process needs to be defined and exported as its own file:
// src/exampleProcess.js
const exampleProcess: MentalProcess = async ({ step: initialStep, subroutine, params }) => {
let step = initialStep
// operations on the working memory step ...
return step
}
export default exampleProcess
Parameters:
step
: a instance of aCortexStep
representing the current state of theWorkingMemory
, containing the latestPerception
for operation onsubroutine
: theSubroutine
object containing hooks for adding stateful behavior to the functional representation of aMentalProcess
params
: static props passed into theMentalProcess
, e.g.{ wasProvoked: true }
MentalProcesses
can be launched to run continuously in the background following each run of the main-thread MentalProcess
by specifying them in the subprocesses
parameter of the soul's Blueprint
. The behavior of subprocesses
is the following:
- They operate on the
WorkingMemory
, identical to the main-threaded process - Each subprocess runs in order of the
subprocesses
list - Any new incoming
Perception
terminates execution of thesubprocesses
The engine's Subroutine
API uses hooks to manage side-effects and stateful behavior during MentalProcess
execution.
β The
use
paradigm is modelled after React hooks, which allow for stateful dynamics inside functional representations of behavior.
const { set, get, search } = useCycleMemory()
useProcessManager
is a Subroutine
hook that gives access to management of the active MentalProcess
const { invocationCount, setNextProcess } = useProcessManager()
Returns:
invocationCount
: a counter for runs of aMentalProcess
, 0 indexed, that resets on process changesetNextProcess(process: MentalProcess, params?: PropType)
: schedules the nextMentalProcess
that will run after the nextPerception
occurs by reference to anotherMentalProcess
. Optionally, parameters can be passed to the nextMentalProcess
via theparams
, e.g.{ wasProvoked: true }
useActions
is a subroutine hook that gives access to available actions a soul can take in its environment
const { speak, leaveConversation } = useActions()
Returns:
speak(message: string)
: tells the soul to send a message externallyleaveConversation()
: terminates execution of the soul'sSubroutine
useProcessMemory
is a Subroutine
hook that returns a local memory container as a way to persist information outside the WorkingMemory
across invocations of a MentalProcess
const wasProvoked = useProcessMemory(false)
console.log("current value of wasProvoked", wasProvoked.current)
// set the current value immediately
wasProvoked.current = true
β Process memories persist while a
MentalProcess
is continually invoked, but reset when the process changes
Here's a simple example MentalProcess
that uses many of the API features to define an interesting behavior, which provokes the user, and then plays victim after
// src/provokesSpeaker.js
import { ChatMessageRoleEnum, brainstorm, decision, externalDialog } from "socialagi";
import { MentalProcess, mentalQuery } from "soul-engine";
import playsVictim from "./playsVictim.js";
const provokesSpeaker: MentalProcess = async ({ step: initialStep, subroutine: { useProcessManager, useProcessMemory, useActions } }) => {
const { speak } = useActions()
const { invocationCount, setNextProcess } = useProcessManager()
let step = initialStep
step = await initialStep.next(externalDialog("Try to provoke the speaker"));
speak(step.value);
const provocationDecision = (await step.next(mentalQuery("Has Samantha successfully provoked the speaker?"))).value;
if (provocationDecision && invocationCount > 0) {
setNextProcess(playsVictim)
return step
}
return step
}
export default provokesSpeaker
Generally, creating interesting user interactions with souls requires many different mental processes to define the flow of an experience.
β This stateful behavior could alternatively be expressed via
useProcessMemory
to store the state of the provocation decision, however, complexity managing state in this way grows quickly.useProcessMemory
is often better for remembering stateful interactions grouped in a single process like the value of an object a user picked.
The engine supports two different GPT-based models for steps taken with SocialAGI CortexStep
:
"fast"
"quality"
Right now, these map onto particular freezes of OpenAI models, but over time will change. We're experimenting with using OSS models for "fast"
internally.
By default, "fast"
is chosen, but the choice can be explicit via:
step = await step.next(..., { model: "quality" })
Generally we recommend using "fast"
for many internal thought processes and "quality"
for the generation that a user or player interacts with.