Skip to content

Commit

Permalink
Working Node.js example and normalizing vs python and rust sdks (#3)
Browse files Browse the repository at this point in the history
* working nodejs example
* add nodejs example to readme
* additional readme content
* drop node 17 support :/

This normalizes the interface for the Node.js SDK versus the Python and Rust SDKs.
One significant difference is that Node.js custom node handlers accept a callback and returns their results via submission to that callback.
  • Loading branch information
kvey authored Jul 27, 2023
1 parent 879903b commit ac93ceb
Show file tree
Hide file tree
Showing 13 changed files with 824 additions and 880 deletions.
1 change: 0 additions & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ jobs:
matrix:
node_version:
- 16
- 17
- 18
- 19
- 20
Expand Down
118 changes: 117 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,132 @@ require them.
OPENAI_API_KEY=...
```

### Example
### Examples

In the table below are examples for Node.js, Python and Rust. You'll need to scroll horizontally to view each.

The following examples show how to build a simple agent that fetches the top stories from Hacker News and call
the OpenAI API to filter to AI related launches and then format that data into markdown. Results from the example
are pushed into the Chidori database and can be visualized using the prompt-graph-ui project. We'll update this example
with a pattern that makes those results more accessible soon.

<table>
<tr>
<th width="450px"><b>Node.js</b></th>
<th width="450px"><b>Python</b></th>
<th width="450px"><b>Rust</b></th>
</tr>
<tr>
<td>

```javascript
const axios = require('axios');
const {Chidori, GraphBuilder} = require("@1kbirds/chidori");

class Story {
constructor(title, url, score) {
this.title = title;
this.url = url;
this.score = score;
}
}

const HN_URL_TOP_STORIES = "https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty";

function fetchStory(id) {
return axios.get(`https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`)
.then(response => response.data);
}

function fetchHN() {
return axios.get(HN_URL_TOP_STORIES)
.then(response => {
const storyIds = response.data;
const tasks = storyIds.slice(0, 30).map(id => fetchStory(id)); // Limit to 30 stories
return Promise.all(tasks)
.then(stories => {
return stories.map(story => {
const { title, url, score } = story;
return new Story(title, url, score);
});
});
});
}

class ChidoriWorker {
constructor() {
this.c = new Chidori("0", "http://localhost:9800"); // Assuming this is a connection object, replaced with an empty object for now
}

async buildGraph() {
const g = new GraphBuilder();

const h = g.customNode({
name: "FetchTopHN",
nodeTypeName: "FetchTopHN",
output: "type FetchTopHN { output: String }"
});

const hInterpret = g.promptNode({
name: "InterpretTheGroup",
template: `
Based on the following list of HackerNews threads,
filter this list to only launches of new AI projects: {{FetchTopHN.output}}
`
});
hInterpret.runWhen(g, h);

const hFormatAndRank = g.promptNode({
name: "FormatAndRank",
template: `
Format this list of new AI projects in markdown, ranking the most
interesting projects from most interesting to least.
{{InterpretTheGroup.promptResult}}
`
});
hFormatAndRank.runWhen(g, hInterpret);

await g.commit(this.c, 0)
}

async run() {
// Construct the agent graph
await this.buildGraph();

// Start graph execution from the root
// Implement the functionality of the play function
await this.c.play(0, 0);

// Run the node execution loop
// Implement the functionality of the run_custom_node_loop function
await this.c.runCustomNodeLoop()
}
}


async function handleFetchHN(nodeWillExec, cb) {
const stories = await fetchHN();
// return JSON.stringify(stories);
return cb({ "output": JSON.stringify(stories) });
// return ;
}

async function main() {
let w = new ChidoriWorker();
await w.c.startServer(":memory:")
await w.c.registerCustomNodeHandle("FetchTopHN", handleFetchHN);
await w.run()
}


main();

```

</td>
<td>

```python
import aiohttp
import asyncio
Expand Down
80 changes: 57 additions & 23 deletions toolchain/chidori/package_node/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
"use strict";

const { promisify } = require("util");

const {
simpleFun,
nodehandleDebugExample,
nodehandleRunWhen,
nodehandleQuery,
chidoriNew,
Expand All @@ -15,21 +12,37 @@ const {
chidoriBranch,
chidoriQuery,
chidoriGraphStructure,
chidoriCustomNode,
chidoriDenoCodeNode,
chidoriVectorMemoryNode
chidoriRegisterCustomNodeHandle,
chidoriRunCustomNodeLoop,
graphbuilderNew,
graphbuilderCustomNode,
graphbuilderPromptNode,
graphbuilderDenoCodeNode,
graphbuilderVectorMemoryNode,
graphbuilderCommit
} = require("./native/chidori.node");

const toSnakeCase = str => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);

const transformKeys = (obj) => {
if (Array.isArray(obj)) {
return obj.map(val => transformKeys(val));
} else if (obj !== null && obj.constructor === Object) {
return Object.keys(obj).reduce((accumulator, key) => {
accumulator[toSnakeCase(key)] = transformKeys(obj[key]);
return accumulator;
}, {});
}
return obj;
};

// Wrapper class for the boxed `Database` for idiomatic JavaScript usage
class NodeHandle {
constructor() {
this.nh = nodehandleDebugExample();
constructor(nh) {
this.nh = nh;
}

runWhen(otherNodeHandle) {
return nodehandleRunWhen.call(this.nh, otherNodeHandle);
runWhen(graphBuilder, otherNodeHandle) {
return nodehandleRunWhen.call(this.nh, graphBuilder.g, otherNodeHandle.nh);
}

query(branch, frame) {
Expand All @@ -43,8 +56,8 @@ class Chidori {
this.chi = chidoriNew(fileId, url);
}

startServer() {
return chidoriStartServer.call(this.chi);
startServer(filePath) {
return chidoriStartServer.call(this.chi, filePath);
}

objectInterface(executionStatus) {
Expand All @@ -55,42 +68,63 @@ class Chidori {
return chidoriPlay.call(this.chi, branch, frame);
}

pause() {
pause(branch, frame) {
return chidoriPause.call(this.chi, branch, frame);
}

query(query, branch, frame) {
return chidoriQuery.call(this.chi, query, branch, frame)
}

branch() {
branch(branch, frame) {
return chidoriBranch.call(this.chi, branch, frame);
}

graphStructure() {
return chidoriGraphStructure.call(this.chi);
graphStructure(branch) {
return chidoriGraphStructure.call(this.chi, branch);
}

registerCustomNodeHandle(nodeTypeName, handle) {
// TODO: we actually pass a callback to the function provided by the user, which they invoke with their result
return chidoriRegisterCustomNodeHandle.call(this.chi, nodeTypeName, handle);
}

runCustomNodeLoop() {
return chidoriRunCustomNodeLoop.call(this.chi);
}

objInterface() {
return chidoriObjInterface.call(this.chi, branch, frame);
}

class GraphBuilder {
constructor() {
this.g = graphbuilderNew();
}

customNode(customNodeCreateOpts) {
return chidoriCustomNode.call(this.chi, createCustomNodeOpts);
customNode(createCustomNodeOpts) {
return new NodeHandle(graphbuilderCustomNode.call(this.g, transformKeys(createCustomNodeOpts)));
}

promptNode(promptNodeCreateOpts) {
return new NodeHandle(graphbuilderPromptNode.call(this.g, transformKeys(promptNodeCreateOpts)));
}

denoCodeNode(denoCodeNodeCreateOpts) {
return chidoriDenoCodeNode.call(this.chi, denoCodeNodeCreateOpts);
return new NodeHandle(graphbuilderDenoCodeNode.call(this.g, transformKeys(denoCodeNodeCreateOpts)));
}

vectorMemoryNode(vectorMemoryNodeCreateOpts) {
return chidoriVectorMemoryNode.call(this.chi, vectorMemoryNodeCreateOpts);
return new NodeHandle(graphbuilderVectorMemoryNode.call(this.g, transformKeys(vectorMemoryNodeCreateOpts)));
}

commit(chidori) {
return graphbuilderCommit.call(this.g, chidori.chi, 0);
}
}


module.exports = {
Chidori: Chidori,
GraphBuilder: GraphBuilder,
NodeHandle: NodeHandle,
simpleFun: simpleFun
};
Loading

0 comments on commit ac93ceb

Please sign in to comment.