Appearance
Reference
First of all, a Pinokio API is nothing more than a mapping from JSON-RPC to a JavaScript function call.
Rules
To build your own API, you need to follow the convention expected by the Pinokio API framework.
All Pinokio functions must take the following form:
- Every API should be stored under its own uniquely named folder under
~/pinokio/api
. - The API file must be a JavaScript class with
index.js
as its filename. - The JavaScript class must have one or more methods. The method names are important since they will be used in JSON-RPC calls.
- The methods must follow a specific function signature convention expected by the Pinokio API framework
API Class
Let's say we want to create an API that takes a JSON-RPC request like this:
json
{
"uri": "./echo.js",
"method": "run",
"params": {
"text": "hello world"
}
}
and returns the same value params.text
("hello world"
).
To accomplish this, we just need to write a JavaScript class and a method that looks something like this:
javascript
class Echo {
async run (request, ondata, kernel) {
return request.params.text
}
}
module.exports = Echo
class name
The name of the class (Echo
in this example) here is not important and you can name it whatever you want.
file name
The file name however, is important. In this case, since the uri is set to ./echo.js
, we need to place the echo.js
file in the same folder as the Pinokio script JSON file.
API Method
Each method must follow the protocol (must have a specific function signature):
javascript
async (request, ondata, kernel) {
// 1. do something with the request (the JSON-RPC payload)
// 2. use ondata() to send realtime updates during execution
// 3. use kernel to access some kernel level attributes and methods
// 4. finally, return a value (in case this API has a return value)
}
Let's walk through each parameter one by one:
- request
- ondata
- kernel
request
The request
object is used to utilize the JSON-RPC request object as well as some additional information.
request
: The JSON-RPC request object, along with some additional metadata attached by the Pinokio processoruri
: The request destination URImethod
: The request methodparams
: The request parameter. This is the attribute that's used most frequently to implement APIsdirname
: The absolute path of the folder that contains the API file being called (For example,~/pinokio/api/utils
)cwd
: The absolute path of the folder from which the script is being called. The difference fromdirname
is that thecwd
is the folder path of the run script that's currently calling this API, whereas thedirname
is the folder path of the API file being called. Similar to the difference between process.cwd() and __dirname in node.jsparent
: The parent script file object. Every API call in a single script shares the sameparent
object.uri
: The uri of the parent scriptpath
: The absolute file path of the parent scriptbody
: The actual script body object.
current
: The current instruction index within therun
array.next
: The next instruction index within therun
array.null
if the current instruction is the last step in therun
array.
ondata
The ondata()
callback function is used to emit events while the API is running.
Often (especially when using AI engines), a single API call may take a while to finish, and it is useful to be able to notify the realtime progress update to the client.
You can call the ondata()
callback to trigger these events.
javascript
ondata(data)
data
: the raw data stream (string). may includes the following attributes:raw
: The raw string (can include ANSI characters and escape sequences, and translates 1:1 to a terminal display). Emit this attribute if you need to display escape sequences in the terminal.state
: The full state representation of the current state. Whileraw
represents individual events, thestate
is a single full string that represents the current state.
Here's an example where you can call "say" and it will emit a "hello" event every 1 second, and return "finished" at the end.
javascript
class Hello {
async say (request, ondata, kernel) {
for(let i=0; i<10; i++) {
// wait 1 second
await new Promise(resolve => setTimeout(resolve, 1000));
// emit "hello " + i event
ondata({ raw: "hello" })
}
return "finished"
}
}
module.exports = Hello
Try saving above file under ~/pinokio/api/hello/index.js
, and then create an example run script at ~/pinokio/api/hello/example.json
:
json
{
"run": [{
"uri": "hello/index.js",
"method": "say"
}]
}
And when you run the example.json
script, it will print "hello" to the terminal every 1 second, and after 10 seconds it will return "finished" and halt.
kernel
Often, the RPC request won't be enough to carry out a task. For example you may need to access some low level methods or attributes.
You can use the kernel
variable for this. Since the kernel object essentially gives you full access to the entire Pinokio operating system, I can't mention everything, but here are some notable modules worth looking at:
kernel
- shell: You can use the low level Shell API with kernel.shell
Example
API with git URI
Let's imagine a simple script that contains one instruction:
json
{
"run": [{
"uri": "https://github.com/cocktailpeanut/llama.git/index.js",
"method": "run",
"params": {
"message": {
"p": "### Instruction\n\nWrite a brief controversial opinion.\n\n### Response\n\n",
"m": "../models/stable-vicuna/13b_q4_0.bin",
"n": 256
}
}
}]
}
The first thing Pinokio does is figuring out which module and its method the uri
resolves to, according to the URI resolution convention.
A git uri is made up of two components:
<remote git URI>/<relative path>
In the example above,
- The
https://github.com/cocktailpeanut/llama.git
part is the<remote git URI>
- The
index.js
part is the<relative path>
The actual algorithm:
- Begin URI parsing: Pinokio sees the uri
https://github.com/cocktailpeanut/llama.git/index.js
- Git uri extraction: the git repository uri is extracted from the full uri:
https://github.com/cocktailpeanut/llama.git
- Git config match: Pinokio checks if there is any top level folder under
~/pinokio/api
whose.git/config
includes the matching remote URLhttps://github.com/cocktailpeanut/llama.git
(This would imply that the folder has been downloaded from a remote git repository available at the URL) - Endpoint resolution: If there's a match (let's say it finds one at
~/pinokio/api/llama
), the resolution is complete, and the request is routed to the module inside the matched local folder (~/pinokio/api/llama
). - Route resolution: Now that the endpoint has been resolved, Pinokio looks at the
<relative path>
part of the full URI. In this case it'sindex.js
. Pinokio takes the resolved endpoint path from the previous step (~/pinokio/api/llama
) and resolves the rest of the file pathindex.js
, and ends up with the full local path~/pinokio/api/llama/index.js
. - Method resolution: Pinokio then looks at the JavaScript class file
~/pinokio/api/llama/index.js
and finds the methodrun
- Method Execution: Now that Pinokio knows which method inside which file needs to be executed, the only thing left is to actually execute the method by passing the
params
attribute.
The ~/pinokio/api/llama/index.js
must follow the API framework convention, and may look something like this:
javascript
class Llama {
async run (request, ondata, kernel) {
// do stuff with the request.params
}
}
module.exports = Llama
The request
parameter will contain:
json
{
uri: 'https://github.com/malfunctionize/llama.git/index.js',
method: 'run',
params: {
message: {
p: "### Instruction\n\nWrite a brief controversial opinion.\n\n### Response\n\n",
m: '../models/stable-vicuna/13b_q4_0.bin',
n: 256
}
},
dirname: '/Users/x/pinokio/api/llama',
cwd: '/Users/x/pinokio/api/llama/example',
root: 'https://github.com/malfunctionize/llama.git/example/stable-vicuna-13b-q4_0.json',
current: 0,
next: null
}
uri
: the full endpoint URI from the begin URI parsing step.method
: the RPC method passed in.params
: the RPC params passed in.dirname
: the resolved local path from the endpoint resolution step. This is the path under which the resolved module existscwd
: the current execution path. This is the folder that contains the script that is running currently.root
: the full path for the script file that is currently running.current
: The current instruction index within therun
array. In this case it's 0 since it's the first instruction in therun
array.next
: The next instruction index to be executed after the current request ends. In this case it'snull
since there is only one item in therun
array (the current instruction), andnull
means the program will halt after this step.
API with relative path
Now let's imagie the same script, but with relative path as its URI, instead of the remote git URI.
json
{
"run": [{
"uri": "./index.js",
"method": "run",
"params": {
"message": {
"p": "### Instruction\n\nWrite a brief controversial opinion.\n\n### Response\n\n",
"m": "../models/stable-vicuna/13b_q4_0.bin",
"n": 256
}
}
}]
}
Note that the uri
is a relative path, so it will look for the index.js
file in the same folder as the script file itself.