├── .gitignore
├── README.md
├── api
└── index.js
├── examples
└── rug-merchant.html
├── package.json
├── pnpm-lock.yaml
├── vercel.json
└── wizard_orpheus.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .env*
2 | .vercel
3 | node_modules/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wizard Orpheus
2 |
3 | Wizard Orpheus is a JavaScript library that makes it easy peasy to build AI apps in JavaScript with minimal prior knowledge!
4 |
5 | ### How To Use It
6 |
7 | Create an HTML file:
8 |
9 | ```html
10 |
11 |
12 | My Game
13 |
14 |
15 |
16 |
17 |
18 |
19 |
53 |
54 |
55 | ```
56 |
57 | ## Documentation
58 |
59 | WizardOrpheus has 4 functions:
60 |
61 | - `var game = new WizardOrpheus(apiKey, gameEnginePrompt`)
62 | - The Wizard Orpheus function will intitialize your game. You're able to input a prompt that will be fed to the bot and set the rules for the game.
63 |
64 | ```js
65 | var game = new WizardOrpheus("OPENAI_KEY", `
66 | You're a merchant and you have a customer who is trying to get you to sell your
67 | rug for only $100. You need to sell it for $200
68 | `)
69 | ```
70 |
71 | - `game.variable(variableName, variablePrompt, startingValue)`
72 | - Create a game variable. Wizard Orpheus will automatically update this variable's value as the game is played, and include its current value in `botAction` functions
73 |
74 | ```js
75 | game.variable('playerHealth', `Current health of the player, from 0 to 100.
76 | Every time something happens where they get hurt (which happens often), this
77 | should decrease. They die at 0.`, 100)
78 |
79 | game.variable('merchantAngerLevel', `How angry the merchant is, on a scale from
80 | 0 to 50. He is very tempermental.`, 0)
81 | ```
82 |
83 | - `game.createUserAction({ name: 'functionName', parameters: ['sentence descriptions of each variable you will pass'], howBotShouldHandle: "description of what the bot should do" })`
84 | - This generates a function you can call later in your code to trigger actions that happen in the game. Example: "sendMessage", "attack", "explore".
85 |
86 | ```js
87 | game.createUserAction({
88 | name: 'message',
89 | parameters: ['The user's message to the merchant'],
90 | howBotShouldHandle: 'Reply to the user with your own message'
91 | })
92 |
93 | game.message('Hello merchant! I will offer 150 for this rug.')
94 |
95 | game.createUserAction({
96 | name: 'attackMerchant',
97 | parameters: [`The amount of damage the player inflicts on the merchant', 'What
98 | the user did to attack`],
99 | howBotShouldHandle: 'Reply to the user and update variables as needed.'
100 | })
101 |
102 | game.attackMerchant(10, 'knife')
103 | ```
104 |
105 | - `game.botAction(actionName, descriptionOfAction, actionParameters, functionToCall)`
106 | - This defines something that the bot can do. After each user action, the bot will decide to use one or more of these actions to respond. The `data` object includes all of the variables defined in actionParameters, and also has `data.currentVariables`, which has all of the previously declared `game.variable` vars in it. The way to access them is `data.currentVariables.playerHealth.value` (you need `.value`, you can't do just `data.currentVariables.playerHealth`)
107 |
108 | ```js
109 | game.botAction(
110 | 'reply',
111 | 'Send a message to the user',
112 | {
113 | message: 'The message to display on the screen'
114 | },
115 | data => {
116 | document.body.innerHTML += '' + data.message + '
'
117 | })
118 |
119 | game.botAction(
120 | 'merchantWifeReply',
121 | `The merchant's wife joins the conversation, replies to the user, and calls
122 | the merchant an idiot.`,
123 | {
124 | wifeReply: "The wife's reply to the user"
125 | }, data => {
126 | document.body.innerHTML += "Merchant's Wife: " + data.wifeReply + "
"
127 | })
128 |
129 | game.botAction('merchantAttack', "The merchant attacks the user, inflicting damage", {
130 | attackMethod: "A two sentence description of how the merchant attacks the user.",
131 | attackWeapon: "A single noun of what the merchant used to attack. Ex. 'candlestick'",
132 | damage: "A number of how much damage the merchant inflicted on the user"
133 | }, data => {
134 | document.body.innerHTML += 'Merchant attacks using ' +
135 | data.attackWeapon + ' inflicting ' + data.damage +
136 | ' - ' + data.attackMethod + '
'
137 | })
138 | ```
--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------
1 | // for future rate limiting
2 | import { kv } from '@vercel/kv';
3 |
4 | const defaultModel = 'gpt-4o-mini'
5 | const apiKey = process.env.OPENAI_API_KEY
6 |
7 | function enableCors(resp) {
8 | resp.headers.set('Access-Control-Allow-Origin', '*')
9 | resp.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
10 | resp.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, Wizard-Orpheus-URL')
11 |
12 | return resp
13 | }
14 |
15 | function upset(obj, key, value) {
16 | if (!obj[key]) {
17 | obj[key] = value
18 | }
19 |
20 | return obj
21 | }
22 |
23 | export function OPTIONS(req) {
24 | return enableCors(new Response(null, { status: 200 }))
25 | }
26 |
27 | // Proxy
28 | export async function POST(req) {
29 | const origin = req.headers.get('origin')
30 | const gameUrl = req.headers.get('wizard-orpheus-url')
31 |
32 | if (!gameUrl.startsWith(origin)) { // We are being scammed! And someone is trying to spoof their URL.
33 | return new Response('Invalid origin', { status: 403 })
34 | }
35 |
36 | const path = new URL(req.url).pathname.substring(4) // the .substring removes the leading /api/ from the path, which vercel adds
37 | let body = await req.json()
38 |
39 | body.model = defaultModel
40 |
41 | const resp = await fetch(`https://api.openai.com${path}`, {
42 | method: 'POST',
43 | headers: {
44 | 'Content-Type': 'application/json',
45 | 'Authorization': `Bearer ${apiKey} `,
46 | },
47 | body: JSON.stringify(body)
48 | })
49 |
50 | let respBody = await resp.json();
51 | let usage = respBody.usage
52 |
53 | // Store in the DB. Each game has its rate limits info at `game:URL_OF_GAME`.
54 | //
55 | // We have a list of all games stored in `games` ranked by how many requests
56 | // have been made to the game.
57 | try {
58 | let g = await kv.hgetall(`game:${gameUrl}`)
59 | if (!g) g = {}
60 | g = upset(g, 'reqCount', 0)
61 | g = upset(g, 'promptTokenCount', 0)
62 | g = upset(g, 'completionTokenCount', 0)
63 |
64 | g['lastReq'] = new Date().toISOString()
65 |
66 | g.reqCount = parseInt(g.reqCount) + 1
67 | g.promptTokenCount = parseInt(g.promptTokenCount) + usage.prompt_tokens
68 | g.completionTokenCount = parseInt(g.completionTokenCount) + usage.completion_tokens
69 |
70 | await kv.hset(`game:${gameUrl}`, g)
71 | await kv.zadd('games', { score: parseInt(g.reqCount), member: gameUrl })
72 | } catch (e) {
73 | console.error('Redis error:', e)
74 | }
75 |
76 | return enableCors(new Response(JSON.stringify(respBody), {
77 | headers: {
78 | 'Content-Type': 'application/json',
79 | },
80 | status: resp.status
81 | }))
82 | }
83 |
84 | // Gallery
85 | export async function GET(req) {
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/examples/rug-merchant.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Club Noise Generator
5 |
19 |
20 |
21 |
22 | Your job is to convince the rug merchant to sell his rug to you for $100. Start by saying 'Hi':
23 |
24 |
25 | (press enter to submit)
26 |
27 | Current asking price: $200
28 | Current merchant anger level (up to 100): 0
29 |
30 |
31 | Merchant health: 50/50
32 |
33 | Player health: 50/50
34 |
35 |
36 |
37 |
38 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@opentelemetry/api": "^1.7.0",
4 | "@vercel/kv": "^1.0.1"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '6.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | dependencies:
8 | '@opentelemetry/api':
9 | specifier: ^1.7.0
10 | version: 1.7.0
11 | '@vercel/kv':
12 | specifier: ^1.0.1
13 | version: 1.0.1
14 |
15 | packages:
16 |
17 | /@opentelemetry/api@1.7.0:
18 | resolution: {integrity: sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==}
19 | engines: {node: '>=8.0.0'}
20 | dev: false
21 |
22 | /@upstash/redis@1.25.1:
23 | resolution: {integrity: sha512-ACj0GhJ4qrQyBshwFgPod6XufVEfKX2wcaihsEvSdLYnY+m+pa13kGt1RXm/yTHKf4TQi/Dy2A8z/y6WUEOmlg==}
24 | dependencies:
25 | crypto-js: 4.2.0
26 | dev: false
27 |
28 | /@vercel/kv@1.0.1:
29 | resolution: {integrity: sha512-uTKddsqVYS2GRAM/QMNNXCTuw9N742mLoGRXoNDcyECaxEXvIHG0dEY+ZnYISV4Vz534VwJO+64fd9XeSggSKw==}
30 | engines: {node: '>=14.6'}
31 | dependencies:
32 | '@upstash/redis': 1.25.1
33 | dev: false
34 |
35 | /crypto-js@4.2.0:
36 | resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
37 | dev: false
38 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/api/:path*",
5 | "destination": "/api/"
6 | }
7 | ]
8 | }
--------------------------------------------------------------------------------
/wizard_orpheus.js:
--------------------------------------------------------------------------------
1 | console.log('🪄🦕 https://github.com/hackclub/wizard-orpheus')
2 |
3 | class WizardOrpheus {
4 | constructor(openAiApiKey, prompt) {
5 | if (openAiApiKey && !prompt) {
6 | prompt = openAiApiKey
7 | openAiApiKey = ''
8 | }
9 |
10 | this.apiHost = 'https://wizard-orpheus.hackclub.dev/api'
11 | this.apiKey = openAiApiKey
12 | this.prompt = prompt
13 | this.model = "gpt-4-turbo-preview"
14 | this.variables = {}
15 | this.messages = [
16 | {
17 | role: 'system',
18 | content: `${this.prompt}
19 |
20 | You MUST call a function. Do not reply with a message under any circumstance.`
21 | }
22 | ]
23 | this.tools = []
24 |
25 | this.inputFunctions = {}
26 | this.outputFunctions = {}
27 | }
28 |
29 | variable(name, description, defaultValue) {
30 | this.variables[name] = {
31 | value: defaultValue,
32 | description
33 | }
34 | }
35 |
36 | createUserAction({ name, parameters, howBotShouldHandle }) {
37 | this[name] = (...args) => {
38 | let inputObj = {}
39 |
40 | args.forEach((arg, i) => {
41 | inputObj[parameters[i]] = arg
42 | })
43 |
44 | this.messages.push({
45 | role: 'user',
46 | content: `The user used the '${name}' action with the following user-provided input: ${JSON.stringify(inputObj)}"
47 |
48 | Determine your next action and pick the most appropriate tool to call. You MUST call a tool, and not reply a message.
49 |
50 | Update the values of currentVariables with your latest state and include them in your call to the tool. These are the current values of currentVariables: ${JSON.stringify(this.variables)}
51 | `
52 | })
53 |
54 | fetch(`${this.apiHost}/v1/chat/completions`, {
55 | method: 'POST',
56 | headers: {
57 | 'Wizard-Orpheus-URL': window.location.href,
58 | 'Content-Type': 'application/json',
59 | 'Authorization': `Bearer ${this.apiKey} `,
60 | },
61 | body: JSON.stringify({
62 | model: this.model,
63 | messages: this.messages,
64 | tools: this.tools,
65 | tool_choice: 'auto'
66 | })
67 | })
68 | .then(resp => resp.json())
69 | .then(body => {
70 | let botReply = body.choices[0].message
71 | this.messages.push({
72 | "role": "assistant",
73 | "tool_calls": botReply.tool_calls
74 | })
75 |
76 | botReply.tool_calls.forEach(botAction => {
77 | this.messages.push({
78 | "role": "tool",
79 | "tool_call_id": botAction.id,
80 | "content": 'ok'
81 | })
82 |
83 | this.outputFunctions[botAction.function.name](JSON.parse(botAction.function.arguments))
84 | })
85 | })
86 | }
87 | }
88 |
89 | botAction(type, prompt, args, callback) {
90 | args['currentVariables'] = `A JSON list of all currentVariables, with their current values, modified as needed based on the action taken by ChatGPT. In this format: ${JSON.stringify(this.variables)}`
91 |
92 | let props = {}
93 |
94 | for (let key in args) {
95 | props[key] = {
96 | response: {
97 | type: 'string',
98 | description: args[key]
99 | }
100 | }
101 | }
102 |
103 | this.tools.push({
104 | type: 'function',
105 | function: {
106 | name: type,
107 | description: prompt,
108 | parameters: {
109 | type: 'object',
110 | properties: props,
111 | required: Object.keys(args)
112 | }
113 | }
114 | })
115 |
116 | this.outputFunctions[type] = callback
117 | }
118 | }
119 |
--------------------------------------------------------------------------------