#Function manager

A technical description will be given now on the internal structure of the functional nodes. In particular, the guide will go through the following main items:

Function print()

Quite analogous to the native Python print function, the print command allows printing into the simulator for debugging purposes.

The output of the print function will be displayed in the Logs / Application window of the simulator.

Definition of returns

The box Returns allows the CPA user to define the outgoing edges coming out from the functional node.

Every return is defined by a unique id automatically assigned by the system, a value and a key (both optional).

Returns defined in the table will be available as function outputs in the Build module. For example, given the following list of returns:

The function outputs (which means the edges outgoing from the functional node) list will be like it shown below:

⚠️Important note: Every function must have at least one return.

Functions exit() and assign()

The returns defined in the Returns configuration table can now be used in the source code to define the output of the function.

The CPA user can write an arbitrary list of instruction flows and assign them to a particular output.

     void: assign(    
            result, <-- (mandatory) integer that identifies the return Id 
            value,  <-- (optional) string describing the result
            key     <-- (optional) key describing the result    
      )

      void: exit(
            result,  <-- (mandatory) integer that identifies the return Id
            value,  <-- (optional) string describing the result
            key  <-- (optional) key describing the result
       )

When value and key parameters are not assigned, the system will retrieve the value and key parameters associated with the specified id in the Returns table.

The exit() function terminates the execution of the function, while assign() does not.

For example:

print("Print message number one")  
assign(1)  
print("Print message number two")
exit(1)  
print("This and the following code lines will not be executed")

Let’s consider the function is_email(), which has two returns,

  1. Valid email address
  2. Not valid email address

and will try to parse a textual variable as an email address: if it fails, exit(2) will be called, otherwise exit(1).

A possible code of the is_email() function can be the following:

import re       # re is the Regular Expression Python library

try:

# The textual variable to validate
var = "my_email@email.com"
# The email regular expression
regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'

# Match regular expression with the variable
if re.fullmatch(regex, var):
    exit(1)  # Valid email address
else:  
    exit(2)  # Not valid email address

except Exception as e:
    print("Got the following unexpected error in the execution: " + str(e))
    exit(2)  # Not valid email address

⚠️Important note: Every function must end with an exit() or an assign() call.
If missing, the simulator will raise the following error:

[x] No reference to 'assign()' or 'exit()' procedures found. Be sure that the source code always ends with an assignment 

Spixii tip A best practice would be to create at least two or more returns, where one is to catch any possible error, and encapsulate the entire source code into a try ... except statement as described below:

try:

    ...  ← write here the body of the function
    exit(1)
except Exception as e:
    print("Got the following unexpected error in the execution: " + str(e))
    exit(2)

Function get()

Variables assigned to nodes can be retrieved into the source code with the function get.

Function get is defined as below:

dict: get(
        var_name,
        index = 0
)

where var_name is the name of the variable to retrieve.

A variable can be overridden multiple times: by default, the get function will return the last occurrence of the variable, unless a value of the index greater than 0 is specified.

The result of the get function is a dictionary described as follows:

{
    "key": "key of var one",
    "value": "value of var one",
    "tms": "2022-01-01 10:00:00.0000"
} 

where value and key are the information of the selected variable and tms is the timestamp with format YYYY-MM-DD HH24:MI:SS.FFFF of the most recent assignment.

For example, let’s consider the following conversation:

and the chat below:

Then, the following information can be retrieved into the source code of the function.

1.  print(get("var1"))
2.  print(get("var1")["value"])
3.  print(get("var1")["key"])
4.  print(get("var2"))
5.  print(get("var2")[0])
6.  print(get("var2")[2])
7.  print(get("var3")["key"])
8.  print(get("var4")["key"])
9.  print(get("not_exsisting_var"))

After clicking Simulate, the function log will display:

1.  {"value":"Text associated to variable #1", "key":"", "tms":"2022-01-01 10:00:00.0000"}
2.  Text associated to variable #1
3.  ""
4.  {"value":"12345", "key":"", "tms":"2022-01-01 10:00:00.0000"}
5.  {"value":"12345", "key":"", "tms":"2022-01-01 10:00:00.0000"}
6.  {"value":"", "key":"", "tms":""}
7.  ""
8.  {"value":"Opt 1", "key":"Key of Opt 1", "tms":"2022-01-01 10:00:00.0000"}
9.  {"value":"", "key":"", "tms":""}

Definition of arguments

If a function is required in multiple steps of the conversational process, then it may be useful to adopt a parameterized approach. This can be easily done with the Arguments configuration table.

Every argument can be assigned with a unique name and optional default value and key.

The function is_equal, for example, has the following arguments:

FirstVariable
    default value:  
    default key:

SecondVariable
    default value:  
    default key:  

case_sensitive
    default value: Y  
    default key:

validate_key
    default value: N  
    default key:

The CPA user is able to assign the parameters straight from the function configuration in the Build tab.

Arguments can be used into the source code with the function get().

    try:  
        # Get FirstVariable and SecondVariable
        var1, var2 = get("FirstVariable"), get("SecondVariable")  

        # Consider value or key depending on the validate_key parameter  
        if get("validate_key")["value"] == "N":  
            var1, var2 = str(var1["value"]), str(var2["value"])  
        else:
            var1, var2 = str(var1["key"]), str(var2["key"])

        # Set to lower if not case sensitive  
        if get("case_sensitive")["value"] == "N":
            var1, var2 = var1.lower(), var2.lower()  

        # Check if FirstVariable and SecondVariable are the same  
        if var1 == var2:
            exit(1)  # FirstVariable and SecondVariable are the same
            exit(2)  # FirstVariable and SecondVariable are different

except Exception as e:
    print("Got the following unexpected error in the execution: " + str(e))
    exit(2)  # FirstVariable and SecondVariable are different

Function save()

Result of elaborations, new variables or API responses can be saved and used by other pieces of the conversation.

The save() function allows storing variables that can be retrieved with the get() function into the source code or with the {{}} notation in the text of the conversation.

bool: save(
    var_name,  ← (mandatory) name of the variable
    value = "",  ← (optional) value to store
    Key = ""  ← (optional) key to store
)

The function returns a Boolean true if the save process was successful, false otherwise.

Calling the save() function over an already existing variable will overwrite its value and key.

Some examples are reported below:

1.  save("myvar1","value at time 1", "key at time 1")
2.  save("myvar1", "value at time 2", "key at time 2")
3.  save("myvar1", "value at time 3")
4.  print(get("myvar1"))
5.  print(get("myvar1")[1])
6.  print(get("myvar1")[2])
7.  print(get("myvar1")[100])

The console log will contain the following information:

1.  {"value":"value at time 3", "key":"", "tms":"2022-01-01 10:00:03.0000"}
2.  {"value":"value at time 2", "key":"key at time 2", "tms":"2022-01-01 10:00:02.0000"}
3.  {"value":"value at time 1", "key":"key at time 1", "tms":"2022-01-01 10:00:01.0000"}
4.  {"value":"", "key":"", "tms":""}

Function bind()

The function bind() is a compact tool to embed CPA variables into a template, which is represented by a string. It becomes particularly useful when it’s necessary to create an API payload starting from a template with many placeholders.

Below it’s reported the definition:

str: bind(
    template = "",  ← (mandatory) string containing the placeholders
    arr = {}  ← (optional) dictionary of variables to bind
)

Here is an example of the result of the application of the bind() function. Let’s assume the following assignment of variables:

the application of the bind() function will be:

template = "Hi {{name}}! You chose the option {{opt.key}}. Bind {{no_variable}}"
result = bind(template)  
print(result)  
# "Hi Giskard! You chose the option Opt 1. Bind "  

result = bind(template, {"name":"Gaia","no_variable":"this"})
print(result)  
# "Hi Gaia! You chose the option Opt 1. Bind this"

Usage of the library importlib

Python uses a number of mechanisms to import libraries and modules: import, importlib.import_module() and __import__() are just a few examples.

Those methods are enabled in the CPA, but, to keep a high-security level of the conversational process, not all the packages can be imported. Also, most of the Python built-in objects are available and just some of them are adjusted or filtered out.

Let’s take for example the module os, which is partially forbidden as it may allow a malicious user to execute a potentially dangerous code into the server.

The source code

import os  
os.system("ls -l")

will return the following message in the Log Error console:

[x] Security error in importing a Python module: package "os" is forbidden. Will be ignored

The list of all the importable modules can be extracted with the following command:

for el in dir():
    print(el)

Among this list, there are three Extension modules: Requests, ReadFile and SQLAlchemy. They can be imported if they were included in the licence subscription.

Spixii keeps this list updated and validates periodically new packages and modules that can be included and used by the CPA users in their custom function implementations.

Extension module Request

When it comes to integrating with a SOAP Service, a REST API or connecting with another service, the package requests is one of the most stable and widely used modules to perform such integrations.

To use the requests package, let’s tick the checkbox Requests in the Extension module configuration.

Below it’s reported an example of a function with 3 returns (1: Warm, 2: Cold, 3: Generic error) that:

  1. import the requests module
  2. get the value of the variable city from the conversation
  3. call the Open Weather Map API
  4. check if the API didn’t respond with a header code 200 success
  5. parse the JSON response and save the new variables weather, temperature, pressure and humidity, that can be used in the conversation or other functions
  6. trigger the return 1: Warm if the temperature is higher than 20 C degrees, otherwise trigger the return 2: Cold.
  7. in case of unexpected error or unsuccessful API call, trigger the return 3: Generic error.


# Import the requests module
import requests

city = 'London' # Target city
appid = 'abcdefg123456' # Subscription key

# Body of the integration
try:
    Make API call
    req = requests.get("https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=" + appid)

# If status_code is not 200 then log the error and exit
if req.status_code != 200:
    print("API responded with a " + str(req.status_code) + " error")
    print(str(req.json()))
    exit(3) # Return 3: Generic error

# Get the JSON result
result = req.json()
save("weather", result["weather"]["main"]) # Save weather description
save("temperature", result["main"]["temp"]) # Save temperature
save("pressure", result["main"]["pressure"]) # Save pressure
save("humidity", result["main"]["humidity"]) # Save humidity

if result["main"]["temp"] > 22:
    exit(1) # Return 1: Warm
else:
    exit(2) # Return 2: Cold

except Exception as e:
    print("An unexpected exception was raised:\n" + str(e))
    exit(3) # Return 3: Generic error

⚠️Important note: If the Requests box in the Extension modules configuration table is not ticked, the system will show the following message in the Log Error console:

[x] Security error in importing a Python module: package "requests" is forbidden. Will be ignored

Extension module ReadFile

Let’s consider for example a function that is decoding a country's 3-digits code into the verbose country name (for example, ESP → Spain).

It would be quite difficult to save the entire list of codes and countries into a textual variable; also, it would be harder in terms of maintenance.

The ReadFile package allows the CPA user to read files and import them into the source code.

The syntax is described below:

f = ReadFile(
    path  ← (string) path/name of the file to read
)

The ReadFile class has a number of methods implemented:

f.read()        ← return the the file into a string  
f.readlines()   ← return the the file into an array (split by new lines)  
f.close()       ← close the current file stream

Coming back to the country code example, let’s suppose to obtain all the associations codes-names into a CSV (Comma-Separated values) like below:

"GBR","United Kingdom"
"ITA","Italy"
"FRA","France"
...

The following source code will


# Import readfile package
import readfile

# Get the country code from the conversation
country_code = get('CountryCode')['value']

# Open the file handler
file_handler = readfile("My_Porject_123/ListCountries.csv")

# Read the file into a string
csv_countries = file_handler.read()

# Read string line by line
for el in csv_countries.splitlines():
    # Split line with the CSV separator ","
    code, country = el.split(',')

    if code == country_code.upper():
        print("Found country name " + country + " associated to code " + country_code)
        save("countryName", country)
        exit(1)

print("No country found for code " + country_code)
exit(2)

The same example above can be done with a different input format, for example, a JSON string like the following:

{
    "GBR": "United Kingdom",
    "ITA": "Italy",
    "FRA": "France",
    ...
}

The file is imported by the ReadFile procedure and then parsed with the json package.

# Import readfile package
import readfile
# Import library to manipulate JSON objects
import json

# Get the country code from the conversation
country_code = get('CountryCode')['value']

# Open the file handler
file_handler = readfile("My_Porject_123/ListCountries.json")

# Read the file into a string and parse it as a JSON string
countries = json.loads(file_handler.read())

# Read string line by line
for key in countries
    if key == country_code.upper():
        print("Found country name " + countries[key] + " associated to code " + country_code)
        save("countryName", country[key])
        exit(1)

print("No country found for code " + country_code)
exit(2)

⚠️Important note: ReadFile extension module is available only for Advanced and Premium subscriptions.

If the ReadFile box in the Extension modules configuration table is not ticked, the system will show the following message in the Log Error console:

[x] Security error in importing a Python module: package "readfile" is forbidden. Will be ignored

Extension module SQLAlchemy

The SQLAlchemy is a powerful ORM (Object-Relational Mapping) that allows the CPA user to:

This component is currently under development and will be announced soon.

Technical background

The Python source code is very flexible and allows the CPA user to create custom logic, API connections and sophisticated chat behaviour.

As mentioned earlier, the import statement has been limited to avoid potential dangerous packages and keep secure the overall system. Furthermore, the following constraints are applied: