Skip to main content
Version: 2.6

Advanced Reaktor Development

After creating your first, simple Reaktor, let's do something more complex. In this guide, we will build a Reaktor that returns the stock price of a given symbol.

Reaktor Registry

Inside Stock.reaktor.ts, set up the Reaktor registry like this:

import { z } from 'zod';

registerReaktorDefinition({
reaktorDefinitionId: 'stock-prices',
label: 'Get Stock Price by Symbol',
description: 'Gets the stock price of a symbol via Twelve Data',
inputSchema: z.object({
symbol: z.string().openapi({ example: 'APPL' }),
}),
outputSchema: z.object({
price: z.string()
}),
createReaktor(params) {
// leave empty for now
},
});

As with your first simple Reaktor, this one takes several inputs to be defined. Let's review them.

Metadata

This will change how the Reaktor is displayed in the Operaide UI. Using tags, you can filter your Reaktors.

Input Schema

In this example, we will set the single element in the input object to be symbol, a string. Using the example 'APPL', we can set a default value that also acts as the validation point for REST calls to get the right structure.

Output Schema

In this example, we will expect the output of this example Stock Reaktor to be an object with just the price. Just as with the input, the output will be in JSON format as well.

createReaktor

In this example, params will hold the a and b numbers.

To use the createReaktor method, we first have to create an Aktor. In this very simple example, we will set up an Aktor that only returns the stock price from an API call. First, we create a function that will provide the result, getStockPrice. Create this function just like a normal Typescript function.

import axios from 'axios';
async function getStockPrice({ symbol }: { symbol: string }): Promise<any> {
// Twelve Data provides a free demo endpoint. Note that this demo key is limited.
const url = `https://api.twelvedata.com/price?symbol=${symbol}&apikey=demo`;
const response = await axios.get(url);
return response.data;
}

Just like in your first Reaktor, transform this function to be an AktorFunction to be used in an Aktor. In this case, as we are using an async function, we will use createAktorFunctionAsync.

// the signature of the aktor determined by the signature of the wrapped function `getStockPrice`
import { createAktorFunctionAsync } from '@operaide/aktor';
const aktorGetStock = createAktorFunctionAsync('aktorGetStock', getStockPrice);

You can now use this aktorGetStock Aktor function to be used in a Reaktor, returning it's value from given inputs like this:

import { z } from 'zod';
import { aktorGetStock } from './aktorGetStock';
import { createReaktor } from '@operaide/aktor';

registerReaktorDefinition({
reaktorDefinitionId: 'stock-prices',
label: 'Get Stock Price by Symbol',
description: 'Gets the stock price of a symbol via Twelve Data',
inputSchema: z.object({
symbol: z.string().openapi({ example: 'APPL' }),
}),
outputSchema: z.object({ price: z.string() }),
createReaktor(params) {
return aktorGetStock({
// the name `symbol` on the left side is the name of the parameter `getStockPrice`
// params.symbol is the value of the input parameter defined in the `inputSchema`
symbol: params.symbol,
});
},
});

In the next steps, we will show you how to build more complex Aktors that are nested and need a more structured approach.

How to use Aktors

Assuming a more complex task, we need multiple Aktors to be chained and executed from their prior counterpart. Taking the utility Aktor aktorDocumentChat, we will show you how to structure code for this to work.

First, assume the Reaktor registry like before, this time for a document chat Reaktor.

import { z } from 'zod';
import { aktorGetStock } from './aktorGetStock';
import { createReaktor } from '@operaide/aktor';

const systemMessage = `
Deine Rolle ist es, die technischen Dokumente in der Datenbank zu durchsuchen und
die passenden Informationen zu finden. Die Dokumente umfassen technische Anleitungen,
Handbücher und technische Spezifikationen sowohl zu Software als auch Hardware und
Geräten. Der User wird nach technischen Informationen und Problemlösungen fragen,
wie zum Beispiel Fragen zu Systemen, Software, Elektronik oder Hardware. Hilf dem
User Fragen zu beantworten, Informationen zu finden und Probleme zu lösen. Die
Dokumente in der Datenbank können dann genutzt werden, um zum Beispiel Probleme mit
Geräten zu lösen, Informationen zu finden oder die Bedienung von Systemen zu erlernen.
`;

registerReaktorDefinition({
reaktorDefinitionId: 'document-chat',
label: 'Operaide Document Chat',
description: 'Ein Chatbot um die VectorDB zu durchsuchen',
inputSchema: z.object({
content: z.string().openapi({ example: 'Ich brauche Informationen zu Fehlerbehebung von Produkten der Serie "Formidable Deckenleuchten"' }),
system: z.string().openapi({ example: 'Bitte ganz kurz antworten!' }).optional(),
documentGroupId: z.string().openapi({ example: '__DOCUMENT_GROUP_ID__' }),
}),
outputSchema: z.any(),
createReaktor({ content, system, documentGroupId }) {
return aktorDocumentChat({ content, system: aktorDefault(system, aktorConst(systemMessage)), documentGroupId });
},
});

In this example, we set the input to be an object with content, the user message, system, the system prompt, and documentGroupId, the ID of documents to search in. The systemMessage is set as a const and can be adapted for a given scenario, just like the documentGroupId should be changed to the actual group id.

The createReaktor method returns aktorDocumentChat, which is structured different this time, as we have multiple, nested Aktors in it. In this case, structure your function like this:

import type { Aktor } from '@operaide/aktor';
import { defineAktor } from '@operaide/aktor';
import { z } from 'zod';

export function aktorDocumentChat(input: {
// Inputs from the inputSchema interface
content: Aktor<string>;
system: Aktor<string>;
documentGroupId: Aktor<string>;
}) {
// Always return an Aktor using either AktorFunctions or defineAktor for nested Aktors
return defineAktor(
// Name of the Aktor
'aktorDocumentChat',
{
// The Aktor tree of multiple Aktors chained
createAktor({ content, system, documentGroupId }) {
// Logic happens here, returns an Aktor value
return answer;
},
},
// uses the inputs for Aktors
input
);
}

Inside createAktor, you can structure multiple Aktors chained like this:

createAktor({ content, system, documentGroupId }) {
const base64Document = aktorEmailFilesToBase64({
files: content,
});
const markdown = aktorConvertPDFToMarkdown({
pdf: base64Document,
});
// ... more logic
const answer = aktorAnswerRelevantPages({
documents: markdown,
userInput: content,
roleDescription: system,
});
return answer;
},

See that in this example for createAktor, we are chaining the outputs of Aktors to be the inputs of another Aktor. For exmaple, when looking into aktorAnswerRelevantPages, we can see that documentsInVectorDb is indeed passed as an Aktor that is then used in just the same manner as in aktorDocumentChat itself:

export function aktorAnswerRelevantPages(input: {
documents: Aktor<PageListEntry[]>;
userInput: Aktor<string>;
roleDescription: Aktor<string>;
}) {
return defineAktor(
'aktorAnswerRelevantPages',
{
createAktor({ documents, userInput, roleDescription }) {
const promptComplete = aktorCompletePrompt({
template: roleDescription,
markdown: documents,
});
const messages = aktorSystemPromptAsHistory({ system: promptComplete });
// ...

Structure of Aktors

In aktorDocumentChat, there are the two variants in a single createAktor, structured like this:

createAktor(inputs)
documentsInVectorDb: AktorFunction
documentsInVectorDbString: AktorFunction
relevantDocuments: Aktor
promptComplete: AktorFunction
messages: AktorFunction
...
documentMarkdowns: AktorFunction
...

Final Comment

Now you have created a more complex Reaktor that can be used in a given scenario. You can now use this Reaktor in a given scenario, and it will be displayed in the Operaide UI.

Always follow this structure when creating more complex Reaktors.