Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add OpenAI service #268

Merged
merged 2 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Then run local services and do any development you need.
PORT=8080 NETSBLOX_CLOUD=https://cloud.netsblox.org NETSBLOX_CLOUD_ID=LocalServices NETSBLOX_CLOUD_SECRET=SuperSecret npm start
```

If you get spurious login attempt errors in the console output (something like
"invalid token L in JSON"), you may need to temporarily disable the
`X-Authorization` header in `cloud-client.js`. BUT DO NOT COMMIT THIS CHANGE!

And finally, remove local services from your account. This is an optional step;
not doing this will only result in a warning message every time you load the
editor while local services are not running.
Expand Down
179 changes: 179 additions & 0 deletions src/procedures/open-ai/open-ai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* Access OpenAI's ChatGPT services for text and image generation!
* Note that you must be logged in to use this service, and you must provide this service with a valid OpenAI API key to use (for your account only!).
* Do not share your OpenAI API key with anyone else!
*
* @service
* @category ArtificialIntelligence
* @category Media
* @alpha
*/

const NBService = require("../utils/service");
const types = require("../../input-types");
const Storage = require("../../storage");
const axios = require("axios");

const OpenAI = new NBService("OpenAI");

types.defineType({
name: "Role",
description: "A role used by OpenAI's ChatGPT",
baseType: "Enum",
baseParams: ["system", "user", "assistant"],
});
types.defineType({
name: "Resolution",
description: "An image resolution for use by OpenAI's image generators",
baseType: "Enum",
baseParams: ["256x256", "512x512", "1024x1024"],
});

_usersDB = null;
function getUsersDB() {
if (!_usersDB) {
_usersDB = Storage.createCollection("open-ai-users");
}
return _usersDB;
}

class User {
constructor(username, key, textModel, imageModel) {
this.username = username;
this.key = key;
this.textModel = textModel;
this.imageModel = imageModel;
}
async save() {
await getUsersDB().updateOne({ username: this.username }, { $set: this }, {
upsert: true,
});
}
}

async function getUser(caller) {
const username = caller?.username;
if (!username) throw Error("You must be logged in to use this feature");

const info = await getUsersDB().findOne({ username });
const key = info?.key || null;
const textModel = info?.textModel || "gpt-3.5-turbo";
const imageModel = info?.imageModel || "dall-e-2";
return new User(username, key, textModel, imageModel);
}

function parsePrompt(prompt) {
if (typeof prompt === "string") {
return [{ role: "system", content: prompt }];
}
if (typeof (prompt[0]) === "string") {
return prompt.map((content) => ({ role: "user", content }));
}
return prompt.map((x) => ({ role: x[0], content: x[1] }));
}

function prettyError(e) {
if (e?.response?.statusText === "Unauthorized") {
return Error("Unauthorized request. Your API key may be invalid.");
}
return e;
}

/**
* Sets the OpenAI API key for the currently logged in account.
*
* Ideally, you should only run this command once per account
* from a throw-away project to avoid leaking your API key to other users.
*
* @param {String} key The new OpenAI API key to use for this account
*/
OpenAI.setKey = async function (key) {
const user = await getUser(this.caller);
user.key = key;
await user.save();
};

/**
* Generate text given a prompt.
* The prompt can take any of the following forms:
*
* - A single piece of text.
* - A list of multiple pieces of text representing a back-and-forth dialog.
* - A list of pairs representing a dialog. The second value of each pair is the dialog spoken,
* and the first value is the role of the speaker ("system", "user", or "assistant").
* User represents the human using the tool, assistant represents ChatGPT itself,
* and System is a special role you can use to give instructions for ChatGPT to follow.
*
* Note that this service does not maintain a chat history with ChatGPT.
* Because of this, if you would like to have a continued dialog rather than one-off completions,
* you must keep track of the dialog yourself in a list and provide the full dialog list to this service.
*
* @param {Union<String, Array<String>, Array<Tuple<Role, String>>>} prompt The prompt to provide to ChatGPT for completion.
* @returns {String} The generated text
*/
OpenAI.generateText = async function (prompt) {
const user = await getUser(this.caller);
if (!user.key) {
throw Error("an OpenAI API key has not been set for this account");
}

let resp;
try {
resp = await axios.post("https://api.openai.com/v1/chat/completions", {
model: user.textModel,
messages: parsePrompt(prompt),
}, {
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${user.key}`,
},
});
} catch (e) {
throw prettyError(e);
}

return resp.data.choices[0].message.content;
};

/**
* Generates an image given a prompt.
*
* @param {String} prompt The prompt to provide to ChatGPT for completion.
* @param {Resolution=} size The resolution of the generated image. Note that larger images are more expensive to generate.
* @returns {Image} The generated image
*/
OpenAI.generateImage = async function (prompt, size = "256x256") {
const user = await getUser(this.caller);
if (!user.key) {
throw Error("an OpeAI API key has not been set for this account");
}

let resp;
try {
resp = await axios.post("https://api.openai.com/v1/images/generations", {
model: user.imageModel,
prompt,
n: 1,
size,
}, {
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${user.key}`,
},
});
} catch (e) {
throw prettyError(e);
}

const img =
(await axios.get(resp.data.data[0].url, { responseType: "arraybuffer" }))
.data;

const rsp = this.response;
rsp.set("content-type", "image/png");
rsp.set("content-length", img.length);
rsp.set("connection", "close");
return rsp.status(200).send(img);
};

module.exports = OpenAI;
9 changes: 9 additions & 0 deletions test/procedures/open-ai.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const utils = require("../assets/utils");

describe(utils.suiteName(__filename), function () {
utils.verifyRPCInterfaces("OpenAI", [
["setKey", ["key"]],
["generateText", ["prompt"]],
["generateImage", ["prompt", "size"]],
]);
});