Created
March 20, 2023 10:36
-
-
Save christianscott/2e7b3cb3572b7b05a24708533559d358 to your computer and use it in GitHub Desktop.
NodeJS implementation of Simon Willinson's "A simple Python implementation of the ReAct pattern for LLMs"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// See https://til.simonwillison.net/llms/python-react-pattern | |
import * as proc from "node:child_process"; | |
import * as https from "node:https"; | |
import * as readline from "node:readline"; | |
import * as url from "node:url"; | |
import { Configuration, OpenAIApi } from "openai"; | |
async function main() { | |
const bot = | |
await ChatBot.create(`You run in a loop of Thought, Action, PAUSE, Observation. | |
At the end of the loop you output an Answer. If you do not have enough information to answer, try different Actions. | |
Use Thought to describe your thoughts about the question you have been asked. | |
Use Action to run one of the actions available to you - then return PAUSE. | |
Observation will be the result of running those actions. | |
Your available actions are: | |
terminal: | |
e.g. find . -name '*.js' | |
Returns the stdout from running the command with bash. You may have to redirect stderr to stdout | |
wikipedia: | |
e.g. wikipedia: Django | |
Returns a summary from searching Wikipedia | |
http: | |
e.g. http: https://christianfscott.com/ | |
Returns the contents of a GET request to the url | |
Example session: | |
Question: What is today's date? | |
Thought: I should use \`date\` to print today's date | |
Action: terminal: date | |
PAUSE | |
You will be called again with this: | |
Observation: Mon Mar 20 18:50:20 AEDT 2023 | |
You then output: | |
Answer: Today's date is Monday the 20th of March, 2023`); | |
const question = | |
process.argv[2] ?? (await prompt("What would you like to know? ")); | |
let nextPrompt = `Question: ${question}`; | |
let i = 0; | |
while (i < 5) { | |
i++; | |
const result = await bot.ask(nextPrompt); | |
console.log(result); | |
const actionRe = /^Action: (\w+): (.*)$/; | |
const actions = []; | |
for (const line of result.split("\n")) { | |
const match = actionRe.exec(line); | |
if (match == null) continue; | |
const [_, action, actionInput] = match; | |
actions.push({ action, actionInput }); | |
} | |
if (actions.length === 0) { | |
return; | |
} | |
for (const { action, actionInput } of actions) { | |
const actionImpl = ActionImpls[action]; | |
if (actionImpl == null) { | |
throw new Error(`unrecognized action ${action}`); | |
} | |
nextPrompt = `Observation: ${await actionImpl(actionInput)}`; | |
console.log(nextPrompt); | |
} | |
} | |
} | |
class ChatBot { | |
constructor(openai, system) { | |
this.openai = openai; | |
this.messages = [{ role: "system", content: system }]; | |
} | |
static async create(system) { | |
const configuration = new Configuration({ | |
organization: process.env.OPENAI_ORG, | |
apiKey: process.env.OPENAI_API_KEY, | |
}); | |
const openai = new OpenAIApi(configuration); | |
return new ChatBot(openai, system); | |
} | |
async ask(message) { | |
this.messages.push({ role: "user", content: message }); | |
const result = await this.exec(); | |
this.messages.push({ role: "assistant", content: result }); | |
return result; | |
} | |
async exec() { | |
const completion = await this.openai.createChatCompletion({ | |
model: "gpt-3.5-turbo", | |
messages: this.messages, | |
}); | |
if (completion.data.choices[0].message == null) { | |
throw new Error(`missing message`); | |
} | |
return completion.data.choices[0].message.content; | |
} | |
} | |
const ActionImpls = { | |
async terminal(input) { | |
const confirmed = await confirm( | |
`Allow the bot to run \`$ bash -c '${input}'\`? ` | |
); | |
if (!confirmed) { | |
console.log("😬 exiting"); | |
process.exit(1); | |
} | |
return proc.execSync(input).toString("utf-8").trim(); | |
}, | |
async wikipedia(input) { | |
const [{ title }] = await WikipediaApi.query(input); | |
const { extract } = await WikipediaApi.extractText(title); | |
return extract; | |
}, | |
async http(input) { | |
return Request.text(new url.URL(input)); | |
}, | |
}; | |
const WikipediaApi = { | |
async query(q) { | |
const p = new url.URLSearchParams(); | |
p.append("action", "query"); | |
p.append("list", "search"); | |
p.append("format", "json"); | |
p.append("srsearch", q); | |
const u = new url.URL("https://en.wikipedia.org/w/api.php"); | |
u.search = p.toString(); | |
const obj = await Request.JSON(u); | |
return obj.query.search; | |
}, | |
async extractText(title) { | |
const p = new url.URLSearchParams(); | |
p.append("action", "query"); | |
p.append("exchars", "1000"); | |
p.append("explaintext", "true"); | |
p.append("prop", "extracts"); | |
p.append("format", "json"); | |
p.append("titles", title); | |
const u = new url.URL("https://en.wikipedia.org/w/api.php"); | |
u.search = p.toString(); | |
const obj = await Request.JSON(u); | |
for (const [_, page] of Object.entries(obj.query.pages)) { | |
return page; | |
} | |
}, | |
}; | |
const Request = { | |
text(url) { | |
return new Promise((resolve, reject) => { | |
https | |
.get(url, (res) => { | |
const chunks = []; | |
res.on("data", (chunk) => chunks.push(...chunk)); | |
res.on("close", () => { | |
const b = Buffer.from(chunks).toString("utf-8"); | |
if ( | |
res.statusCode && | |
(res.statusCode >= 300 || res.statusCode < 200) | |
) { | |
reject(`request failed with status ${res.statusCode}`); | |
} else { | |
resolve(b); | |
} | |
}); | |
}) | |
.on("error", (err) => reject(err)); | |
}); | |
}, | |
async JSON(url) { | |
return JSON.parse(await this.text(url)); | |
}, | |
}; | |
async function confirm(question = "Confirm? ") { | |
const answer = await prompt(question); | |
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"; | |
} | |
function prompt(question) { | |
const rl = readline.createInterface({ | |
input: process.stdin, | |
output: process.stdout, | |
}); | |
return new Promise((resolve) => { | |
rl.question(question, (answer) => { | |
rl.close(); | |
resolve(answer); | |
}); | |
}); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment