Author: Drew Shea
Date Created:
Overview
The purpose of this document is to provide common code recipes for interactions with Routine C# objects (XBRApi.Routines) including RoutineInstance, Run, Artifact, InMemoryJsonArtifact, ArtifactInfo, and more.
Object: Routine Instance
Use Case: Create a Routine Instance
var routineClient = XBRApi.Routines.GetRoutineClient(si);
var routineInstance = routineClient.CreateRoutineInstanceAsync(
"InMemExecutionTestRoutine",
null
).Result;
Explanation:
-
Line 1: Retrieve RoutineClient
- We are retrieving the
RoutineClientwhich provides the ability to manageRoutineInstanceobjects.
- We are retrieving the
-
Line 3 - 6: Create RoutineInstance object with the least amount of configurations
- Line 4: This is the Routine Name that we want to create an instance of. The names of Routines can be found in the SensibleAI Studio → Explore Page.
- Line 5: We can optionally provide a name for the
RoutineInstancethat we want to create or we can jsut providenulland a name will be automatically generated.
Use Case: Store Arbitrary Json Attributes on the Routine Instance
It is not uncommon to want to have a Routine Instance take in additional metadata about the solution that it is operating within while also not wanting this metadata exposed to the user of the Routine Instance (often from a UI perspective). Storing arbitrary metadata as JSON on the Routine Instance allows the ability to handle these situations.
var routineInstance = routineClient.CreateRoutineInstanceAsync(
"InMemExecutionTestRoutine", // Routine Type Name
null, // Routine Name (null means the name will be automatically generated)
labels: new List<string> { "MyCustomLabel", "AnotherCustomLabel" }, // User defined labels you can filter/categorize Routine Instances by
attributes: new JObject()
{
{"RoutineAttribute", "RoutineAttributeValue" },
{ "RoutineAttribute2", "RoutineAttributeValue2" }
} // Arbitrary json attributes that will live at the routine instance level. These are accessible on the python side as well
).Result;
Explanation:
-
Line 5 - 9: Arbitrary JSON Attributes
- Arbitrary json attributes can be attached and stored on the Routine Instance that can then be retrieved later on the C# side and python side (see below)
C#: Access via RoutineClient
var rehydratedRoutineInstance = routineClient.GetRoutineInstanceAsync("my-routin-j52801l").Result;
var attributes = rehydratedRoutineClient.Attributes;
BRApi.ErrorLog.LogObject(si, "Rehydrated Attributes", attributes);
// Log Output:
Rehydrated Attributes
{
"RoutineAttribute": "RoutineAttributeValue",
"RoutineAttribute2": "RoutineAttributeValue2"
}
Note: There is a BRApi.ErrorLog.LogObject extension method located at the namespace using Workspace.XBR.Xperiflow.Utilities.Logging.Extensions;
Python: Access via the RoutineApi
For Routine Developers, you may access this Routine Instance Attributes wherever you have access to the RoutineApi object as a python dict at the api.routine.attributes property. For example, you can see the ability to interact with the attributes.
@routine
class InMemExecutionTestRoutine(BaseRoutine):
definition = RoutineDefinitionContext(...)
@rmethod(memory_capacity=2, allow_in_memory_execution=True)
def concat(self, api: RoutineApi, parameters: SimplePbm) -> InMemExecutionArtifact:
# Access Routine Instance Json Attributes via 'api'
my_value = api.routine.attributes["RoutineAttribute"]
# my_value will equal "RoutineAttributeValue"
Use Case: Store Data in the Routine Instance File System Directory
There are situations where you want to store data at the Routine Instance level that doesn’t fit into the model of input parameters and arbitrary json attributes. For example, you may have a situation where you want to store tabular data on the C# side and you want this data to be contained within the Routine Instance itself, so that when the Routine Instance is deleted, this data is wiped as well.
There is a property on the RoutineInstance object called SharedDataStore which provides a key-value pair abstraction over the MetaFileSystemClient to allows the developer user to read and write data to the following Routine MetaFileSystem root directory location: instances_/<routine_instance_id>/shared_/store_
C#: Write and Read Text Data to the SharedDataStore
var routineInstance = routineClient.GetRoutineInstanceAsync("my-routine-3823t2").Result;
// Case 1a: Write key-value pair where the value is text (behind the scenes it gets serialized to a .txt file)
routineInstance.SharedDataStore.WriteText("my_string_key", "hello world");
// Case 1b: Read the text back into memory
var rehydratedText = routineInstance.SharedDataStore.ReadText("my_key");
C#: Write and Read DataTable to the SharedDataStore
var routineInstance = routineClient.GetRoutineInstanceAsync("my-routine-3823t2").Result;
// A DataTable I want to save
var dt = new DataTable("MyDataTable");
dt.Columns.Add("Column1", typeof(string));
dt.Columns.Add("Column2", typeof(string));
dt.Rows.Add("Row1Column1", "Row1Column2");
dt.Rows.Add("Row2Column1", "Row2Column2");
// Case 2a: Write key-value pair where the value is a DataTable (behind the scenes it gets serialized as partitioned parquet files)
routineInstance.SharedDataStore.WriteDataTable("my_datatable", dt);
// Case 2b: Read the DataTable back into Memory
var rehydratedDt = routineInstance.SharedDataStore.ReadDataTable("my_datatable");
C#: Write and Read Json Data to the SharedDataStore
var routineInstance = routineClient.GetRoutineInstanceAsync("my-routine-3823t2").Result;
// Some json I want to save
var json = new JObject(
new JProperty("key1", "value1"),
new JProperty("key2", "value2")
);
// Case 3a: Write key-value pair where the value is a JObject (behind the scenes it serialized to a .json file)
routineInstance.SharedDataStore.WriteJson("my_json_key", json);
// Case 3b: Read the JObject back into memory
var rehydratedJson = routineInstance.SharedDataStore.ReadJsonAsJObject("my_json_key");
C#: Write and Read Json Serializable Objects to the SharedDataStore
public class SimpleObject
{
public string key1 { get; set; }
public string key2 { get; set; }
}
public void main()
{
var routineInstance = routineClient.GetRoutineInstanceAsync("my-routine-3823t2").Result;
// Some json serializable object I want to save
var simpleObject = new SimpleObject()
{
key1 = "value1",
key2 = "value2"
};
// Case 4a: Write key-value paire where the value is a Json Serializable object
routineInstance.SharedDataStore.WriteJson<SimpleObject>("my_json_object_key", simpleObject);
// Case 4b: Read the Simple Object back into Memory
var rehydratedSimpleObject = routineInstance.SharedDataStore.ReadJsonAsObject<SimpleObject>("my_json_object_key");
}
This SharedDataStore object also exists on the Run object. However, the data saved will be scoped to the Run at this Routine MetaFileSystem root directory location: instances_/<routine_instance_id>/runs_/<run_id>/shared_/store_
Object: Run
Use Case: Store Arbitrary Json Attributes on the Run
It is not uncommon to want to have a Run take in additional metadata about the solution that it is operating within while also not wanting this metadata exposed to the user of the Routine Instance (often from a UI perspective). Storing arbitrary metadata as JSON on the Run allows the ability to handle these situations.
var run = routineInstance.CreateRunAsync(
"concat",
null,
false,
false,
InvocationMethodType.Direct,
new JObject(
new JProperty("first", "Hello"),
new JProperty("second", "World")
),
"My Description",
storeArtifacts: true,
executionType: "in-memory",
attributes: new JObject()
{
{"RunAttribute", "RunAttributeValue" },
{ "RunAttribute2", "RunAttributeValue2" }
}
).Result;
Explanation:
-
Line 5 - 9: Arbitrary JSON Attributes
- Arbitrary json attributes can be attached and stored on the Routine Instance that can then be retrieved later on the C# side and python side (see below)
C#: Access via RoutineClient
var rehydratedRun = routineInstance.GetRunByIdentifierAsync(run.RunIdentifier).Result;
var attributes = rehydratedRun.Attributes;
BRApi.ErrorLog.LogObject(si, "Rehydrated Attributes", attributes);
// Log Output:
Rehydrated Attributes
{
{"RunAttribute", "RunAttributeValue" },
{ "RunAttribute2", "RunAttributeValue2" }
}
Note: There is a BRApi.ErrorLog.LogObject extension method located at the namespace using Workspace.XBR.Xperiflow.Utilities.Logging.Extensions;
Python: Access via the RoutineApi
You may access this Run Attributes wherever you have access to the RoutineApi object as a python dict at the api.run.attributes property. For example, you can see the ability to interact with the attributes.
@routine
class InMemExecutionTestRoutine(BaseRoutine):
definition = RoutineDefinitionContext(...)
@rmethod(memory_capacity=2, allow_in_memory_execution=True)
def concat(self, api: RoutineApi, parameters: SimplePbm) -> InMemExecutionArtifact:
# Access Routine Instance Json Attributes via 'api'
my_value = api.run.attributes["RunAttribute"]
# my_value will equal "RunAttributeValue"
Use Case: Run a Routine Method at the REST API Layer (“in-memory”)
Now available with the Xperiflow v4.0.0 release, there is the ability to execute Routine Methods “in-memory”. When invoking a Routine Method “in-memory” from the C# side, it will run the Routine Method on the Xperiflow Caller Server (web server). This method of execution allows for some powerful benefits over the traditional way of execution (now dubbed “background”) including:
-
Directly Return Json Serializable Artifacts
- Given that the Routine Method runs within the process that received the invocation, certain Artifact Types can directly return a Json Serializable version of the Artifact
-
Faster Runtimes
-
There is no 10 - 15 second Python process spin-up penalty like there when doing “background” execution
- Note: We have seen a Routine Method Run go from 30 seconds to under a second as part of the AI Account Reconciliation project that occurred
-
These faster runtimes allow to even run a Routine Method behind a UI interaction (a button click and screen refresh)
-
Python: In-Memory Executable Routine Methods
It is important to note not all Routine Methods are “in-memory” executable. A Routine Method is only “in-memory” executable if it has the argument allow_in_memory_execution=True within the @rmethod decorator.
@pbm
class InMemExecutionArtifact(ArtifactBaseModel):
concat_str: ArtifactDef[str, StrTextArtifactIOFactory] = ArtifactMetadataDef(
title="Concatenated String", description="The concatenated string based on input strings."
)
dataframe: ArtifactDef[pl.DataFrame, PolarsParquetArtifactIOFactory] = ArtifactMetadataDef(
title="DataFrame", description="The dataframe based on the concatenated string."
)
@routine
class InMemExecutionTestRoutine(BaseRoutine):
definition = RoutineDefinitionContext(
title="In Memory Execution Testing Routine",
tags=[RoutineTags_.ml, RoutineTags_.supervised, RoutineTags_.data_integration],
short_description="Testing routine for testing in memory execution",
long_description="Testing routine for testing in memory execution" * 10,
use_cases=[],
creation_date=dt.datetime(25, 3, 25),
author="Luke Heberling",
organization=OrganizationNames_.ONESTREAM,
version="0.0.1",
)
@rmethod(memory_capacity=2, allow_in_memory_execution=True) # Marked to Allow In-Memory Execution
def concat(self, api: RoutineApi, parameters: SimplePbm) -> InMemExecutionArtifact:
...
Explanation:
-
Line 3 - 5: A Json Serializable Artifact
- On our
ArtifactBaseModelimplementation, theconcat_strArtifact is Json Serializable. This is because theStrTextArtifactIOFactoryimplements a special method calledwrite_json_artifactthat we will see in more detail in a moment.
- On our
-
Line 6 - 8: Not Json Serializable Artifact
- On our
ArtifactBaseModelimplementation, theconcat_strArtifact is not Json Serializable. This is because it does not implement the special methodwrite_json_artifact. This means that if we want access to this object on the C# side, we will have use the standard methodology for pulling artifacts data.
- On our
-
Line 25: rmethod decorator turns on in-memory execution
- We can see that we have
allow_in_memory_executionset toTrue
- We can see that we have
Python: Json Serializable Artifacts
For Routine Developers, as briefly mentioned above in the benefits section, only certain Artifact Types are JSON serializable and therefore can be directly return from the “in-memory” execution.
The technical explanation here is: IArtifactWriter (python side) concrete implementations that implement the IJsonArtifactWriterMixin (ex: StrTextArtifactWriter) will have their Json representation of their Artifacts accessible as the response to an invocation of an “in-memory” Routine Method Run.
Let’s take a look at the StrTextArtifactWriter implementation:
class StrTextArtifactWriter(IArtifactWriter[str], IJsonArtifactWriterMixin[str]):
@property
def file_annotations(self) -> list[FileAnnotationContext]:
return [FileAnnotationContext(file_annotation="string.txt", file_annotation_description="text file of string")]
def write(self, fs: AbstractFileSystem, dirpath: str, data: str):
full_path = pathutil.join_parent_and_name(dirpath, "string.txt")
with fs.open(path=full_path, mode="w") as f:
f.write(data)
def write_json_artifact(self, data: str) -> dict[str, str | int | float | list[str]]:
return {"string_data": data}
Explanation:
-
Line 1: Inherits a Mixin
- See how it inherits a mixin called
IJsonArtifactWriterMixin[str]This defines a method calleddef write_json_artifacton it
- See how it inherits a mixin called
-
Line 11 - 12: The
write_json_artifactmethod- This is the method that gets called when an “in-memory” execution is finishing if the Routine Method Run were to return an Artifact that implements this
IJsonArtifactWriterMixin
- This is the method that gets called when an “in-memory” execution is finishing if the Routine Method Run were to return an Artifact that implements this