Before following this tutorial, make sure you've installed Docker (https://www.docker.com/)
Run this tutorial from a bash terminal.
In this tutorial we'll create an Opencog service and publish it in SingularityNET.
Setup and run a docker container.
$ docker image build -t opencog_services_basic https://raw.githubusercontent.com/singnet/opencog-services/master/basic-dockerfile
$ docker image build -t opencog_services https://raw.githubusercontent.com/singnet/opencog-services/master/Dockerfile
$ docker run --name OPENCOG_SERVICE_DEV -ti opencog_services /bin/bash
From this point we follow the turorial in the Docker container's prompt.
The code in this repo is used to start and publish an Opencog service in
SingularityNET. bin/server
is the executable that actually listen for requests.
bin/client
can be used to send commands to the server locally (without blockchain).
Note that there's only one Opencog service, which expects comands like this:
# ./bin/client sync Echo foo bar
foo bar
The service will issue the comand Echo
passing 2 arguments: foo
and bar
.
Thus, to implement a new Opencog service, we'll actually need to implement a class
which is linked to bin/server
to provide the new command. So from now on we
use the terminology "command" instead of "service" because we'll actually
implement a new "command" to the Opencog "service" which is already in place.
The sync
keyword in the above command line is not relevant here.
Take a look at this document for details.
Note: If you decided to run the server to actually test the client command above, make sure you kill it before moving on.
We'll implement 2 versions of a "Hello world" command, one in C++ and another in Scheme. We need different names for the two commands so we'll call them HelloWorld and HelloWorld2 respectively.
For the C++ command, we need to implement a C++ class (with separated .h and .cc) in src/cpp-services
.
The class is supposed to have the exact name of the command in CamelCase
notation.
So the header file src/cpp-services/HelloWorld.h
will look like this:
#ifndef _OPENCOGSERVICES_HELLOWORLD_H
#define _OPENCOGSERVICES_HELLOWORLD_H
#include "../OpencogSNETService.h"
namespace opencogservices
{
class HelloWorld : public OpencogSNETService
{
public:
bool execute(std::string &output, const std::vector<std::string> &args);
HelloWorld();
~HelloWorld();
private:
};
}
#endif // _OPENCOGSERVICES_HELLOWORLD_H
The command class need to inherit from OpencogSNETService
. The only required
method is execute()
but OpencogSNETService
has other implemented helper
methods explained here.
execute()
expects a reference to std::string where its output is supposed to
be written and a std::vector with the input arguments. In our case we expect no
arguments.
So we only need to implement execute()
in src/cpp-services/HelloWorld.cc
. In our case it may
look like this:
#include "HelloWorld.h"
using namespace opencogservices;
using namespace std;
HelloWorld::HelloWorld() {}
HelloWorld::~HelloWorld() {}
bool HelloWorld::execute(string &output, const vector<std::string> &args)
{
output.assign("Hello World");
return false;
}
execute()
is supposed to return true
if and only if an error occurred so
our HelloHorld
implementation will just return false
meaning that
everything is OK.
Once the class is ready, edit src/OpencogSNETServiceFactory.cc
and add an
#include
statement and another else if
clause in the method factory()
according to your new command name.
Although OpencogSNETServiceFactory
may compile if you use different names for
the command and C++ class, you are supposed to use the exact same name.
To compile our new command just cd to src/
and:
# make clean
# make
OK we are ready to test our new C++ command.
For the Scheme command, we need to implement a Scheme file in
src/scm-services
with the exact name of the command in CamelCase
notation
defining a funcion named execute
which expects one single argument (a list with
all the arguments to the function).
So our Scheme file src/scm-services/HelloWorld2.scm
will look like this:
(define (execute args)
"Hello World 2"
)
If the command expects arguments, they will be passed as a list to the function. You can make an arbritary number of valid Scheme calls in your file e.g. load new modules, define several global variables or helper functions etc. The only requisite is that you nned to define a function with the exact name of the command expecting a list of arguments as the only parameter.
Important notes:
- Anything the command send to
stdout
(e.g. by calling the function(display)
) will be considered as part of the output. - Any arguments starting with
http:
orhttps:
will not be passed to the command. The target file will be fetched instead (it's supposed to be in Atomese) and fed into the AtomSpace. If you need to pass an URL to the command, you can quote it or prefix it in some other way.
So we're done with the Scheme command.
We are ready to test our two new commands. cd
to the root directory and start
the server:
# ./bin/server &
Now call our new commands:
# ./bin/client sync HelloWorld
Hello World
# ./bin/client sync HelloWorld2
Hello World 2
Once your commands are working properly, it's time to make a PR to integrate then in the SingularityNET Opencog service. Make sure you read our contribution guidelines before going on.
Before issuing a PR, chek if your new commands implementation is compliant with some basic standards for tests and documentation. in the root directory run:
# ./scripts/compliance_check.sh
tests/HelloWorld is mising
tests/HelloWorld/testCases.txt is mising
tests/HelloWorld/baseline is mising
docs/HelloWorld.md is mising
Note that we're missing a lot of files. Basically we need to provide test cases
for a regression test and a .md
documentation for our new commands.
First, we'll create the test cases for both commands.
# cd tests
# mkdir -p HelloWorld/baseline
Create a new file HelloWorld/testCases.txt
like this:
{"test-cases": [
{"input": "", "output": "t.txt"}
]}
testCases.txt
lists all the test cases in a JSON array. Each entry is a hash
with "input" and "output". the first key is the input parameters of the test
case and second one is the name of the file (which is expected to be in
HelloWorld/baseline/
) with the respective expected output.
We have only one test case so create HelloWorld/baseline/t.txt
like this:
Hello World
Execute integration tests:
./bin/runTests
Runing tests for Echo...
Runing tests for EchoScheme...
Runing tests for PatternMiner...
Runing tests for HelloWorld...
Runing tests for HelloWorld2...
Could execute tests for HelloWorld2
Still missing the test cases for HelloWorld2. Follow the same steps and run the
regression tests again to make sure all the tests pass. NOTE: The Scheme
commands append an extra \n
at the end of the output. So your baseline output
file need to have this empty line at the end.
Now we need to create a proper documentation for our new commands. Create
docs/HelloWorld.md
and docs/HelloWorld2.md
. You are advised to use one of
the documents of other commands as template for your own.
Use a MarkDown previewer of your choice. When you are done with the documentation, call:
# ./scripts/buildDocs.sh
This script will use your newly created documents to update the HTML user's guide of SingularityNET Opencog Services. Those HTML files are in the repository as well so don't forget to commit them in your PR.
You also need to update the main README.md
of the repository. Edit it and
look for the secion "Opencog services to SingularityNET". Add your newly
created commands to the list with a simple description of their arguments
and what is their expected output.
You are ready to submit your PR. Read our contribution guidelines before submiting it.
The superclass inherited by your command has a couple of helper methods and variables.
opencog::AtomSpace atomSpace;
bool loadAtomeseFile(std::string &errorMessage, const std::string &url);
void evaluateScheme(const std::string &scmLine);
void evaluateScheme(std::string &output, const std::string &scmLine);
void setConfigurationParameters(const std::string jsonString);
- atomSpace: a variable containing the AtomSpace object used to execute any Opencog call.
- loadAtomeseFile(): Fetches the contents of the passed URL (which is supposed to be a .scm file with an Atomese knowledge base) and load it in atomSpace. Any errors are reported in
errorMessage
. - evaluateScheme(): Evaluates the passed Scheme command (any Opencog's scheme functions use the public atomSpace). The Scheme command output is discarded.
- evaluateScheme(): Evaluates the passed Scheme command (any Opencog's scheme functions use the public atomSpace). The Scheme command output is returned in
output
. - setConfigurationParameters(): Use the passed JSON hash to set Opencog's configuration parameters (e.g.
{"Max_thread_num": "8", "Pattern_Max_Gram": "3"}
)