├── .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 | 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 | --------------------------------------------------------------------------------