-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d2e58d2
commit 87b2ef4
Showing
4 changed files
with
302 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.idea |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# MMM-ChatGPT Change Log | ||
|
||
## [0.0.1] - 2023-03-05 | ||
|
||
Initial Release |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
/* Magic Mirror | ||
* Module: MMM-ChatGPT | ||
* | ||
* MIT Licensed. | ||
*/ | ||
|
||
Module.register("MMM-ChatGPT", { | ||
|
||
// Default module config. | ||
defaults: { | ||
updateInterval: 900, // seconds | ||
animationSpeed: 1, // seconds | ||
initialDelay: 0, // seconds | ||
initialPrompt: [], | ||
retryDelay: 1, // seconds | ||
|
||
loadingPlaceholder: "Loading...", | ||
|
||
model: "gpt-3.5-turbo", | ||
endpoint: 'https://api.openai.com/v1/chat/completions', | ||
apiKey: '', | ||
timeOut: 5, // seconds | ||
|
||
fontURL: "", | ||
fontSize: "", | ||
fontStyle: "", | ||
color: "", | ||
className: "light small", | ||
|
||
}, | ||
|
||
requiresVersion: "2.1.0", // Not tested on earlier versions than 2.22.0 | ||
|
||
start: function () { | ||
Log.info("Starting module: " + this.name); | ||
this.updateTimer = null; | ||
this.message = false; | ||
this.scheduleNextCall(this.config.initialDelay); | ||
}, | ||
|
||
/* suspend() | ||
* Stop any scheduled update. | ||
*/ | ||
supend: function () { | ||
this.updateTimer = null; | ||
}, | ||
|
||
/* resume() | ||
* Immediately fetch new data, will automatically schedule next update. | ||
* TODO: Improve by checking when the last response was fetched and only fetch if | ||
* the updateInterval has passed. | ||
*/ | ||
resume: function () { | ||
this.getData(); | ||
}, | ||
|
||
/* getPrompt() | ||
* Assembles the prompt for the API call | ||
* If multiple prompts are defined, one is chosen at random | ||
* The initial prompt is prepended to the chosen prompt | ||
*/ | ||
getPrompt: function () { | ||
let prompts = this.config.prompts; | ||
let len = prompts.length; | ||
let prompt = JSON.parse(JSON.stringify(prompts[Math.floor(Math.random() * len)])); | ||
let initialPrompt = JSON.parse(JSON.stringify(this.config.initialPrompt)); | ||
prompt = initialPrompt.concat(prompt); | ||
|
||
for (let msg of prompt) { | ||
let content = msg.content; | ||
let replacements = content.matchAll(/\{\{([^{}]+)\}\}/g) | ||
for (let m of replacements) { | ||
content = content.replace(m[0], eval(m[1])); | ||
} | ||
msg.content = content; | ||
} | ||
return prompt; | ||
}, | ||
|
||
/* getDom() | ||
* Construct the DOM for the module. | ||
*/ | ||
getDom: function () { | ||
let wrapper = document.createElement("div"); | ||
|
||
for (let key of ["fontURL", "fontSize", "fontStyle", "color"]) { | ||
if (this.config[key] !== "") { | ||
wrapper.style[key] = this.config[key]; | ||
} | ||
} | ||
|
||
if (!this.config.apiKey) { | ||
wrapper.innerHTML = "Missing API key"; | ||
} else if (this.message) { | ||
wrapper.innerHTML = this.message; | ||
} else { | ||
wrapper.innerHTML = this.config.loadingPlaceholder; | ||
} | ||
|
||
return wrapper; | ||
}, | ||
|
||
/* getData() | ||
* Call endpoint, process response and schedule next update. | ||
*/ | ||
getData: function () { | ||
let request = new XMLHttpRequest(); | ||
request.open("POST", this.config.endpoint, true); | ||
request.setRequestHeader("Authorization", "Bearer " + this.config.apiKey); | ||
request.setRequestHeader("Content-Type", "application/json"); | ||
request.timeout = this.config.timeOut * 1000; | ||
|
||
let self = this; | ||
request.onerror = function () { | ||
Log.error("ChatGPT API request failed"); | ||
self.scheduleNextCall(self.config.retryDelay); | ||
} | ||
request.onabort = function () { | ||
Log.error("ChatGPT API request aborted"); | ||
self.scheduleNextCall(self.config.retryDelay); | ||
} | ||
request.ontimeout = function () { | ||
Log.error("ChatGPT API request timeout"); | ||
self.scheduleNextCall(self.config.retryDelay); | ||
} | ||
request.onload = function () { | ||
let success = this.status === 200 | ||
? self.processResponse(this.response) | ||
: false; | ||
|
||
if (success) { | ||
self.scheduleNextCall(self.config.updateInterval); | ||
} else { | ||
Log.error("ChatGPT API response: " + this.status + ": " + this.response); | ||
if (this.status === 401) { | ||
self.message = "[401 Unauthorized, check your API key]"; | ||
self.updateDom(self.config.animationSpeed * 1000); | ||
self.updateTimer = null; | ||
return; | ||
} | ||
self.scheduleNextCall(self.config.retryDelay); | ||
} | ||
}; | ||
|
||
let payload = { | ||
"model": self.config.model, | ||
"messages": this.getPrompt() | ||
}; | ||
|
||
request.send(JSON.stringify(payload)); | ||
}, | ||
|
||
/* processResponse(response) | ||
* Process the response from the API. | ||
* Returns true if the response is valid, false otherwise. | ||
*/ | ||
processResponse: function (response) { | ||
let data; | ||
try { | ||
data = JSON.parse(response); | ||
} catch (e) { | ||
Log.error("ChatGPT API response is not valid JSON: " + response); | ||
return false; | ||
} | ||
|
||
if (!("choices" in data) | ||
|| data.choices.length === 0 | ||
|| !("message" in data.choices[0]) | ||
|| !("content" in data.choices[0].message) | ||
) { | ||
return false; | ||
} | ||
|
||
let message = data.choices[0].message.content; | ||
|
||
// ChatGPT sometimes likes to return a quoted string | ||
if (message.startsWith('"') && message.endsWith('"')) { | ||
message = message.replaceAll(/^["\s]+|["\s]+$/g, ""); | ||
} | ||
|
||
this.message = message; | ||
this.updateDom(this.config.animationSpeed * 1000); | ||
return true; | ||
}, | ||
|
||
/* scheduleNextCall(seconds) | ||
* Schedule next update. | ||
*/ | ||
scheduleNextCall: function (seconds) { | ||
clearTimeout(this.updateTimer); | ||
let self = this; | ||
this.updateTimer = setTimeout(() => self.getData(), seconds * 1000); | ||
}, | ||
}); |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
# MMM-ChatGPT | ||
|
||
This is a module for [MagicMirror²](https://github.com/MichMich/MagicMirror/). | ||
|
||
It fetches a response from ChatGPT using a configurable prompt at a configurable interval. | ||
|
||
### Prerequisites | ||
|
||
You need a paid [OpenAI](https:://platform.openai.com) account to be able to use this module.<br> | ||
At the time of writing this, the pricing is 0.002 per 1000 tokens, ~750 words (including prompt and response). | ||
For occasional requests, this is practically free, but you still need to set a payment method. | ||
You can, however, set a monthly limit to prevent accidental charges.<br> | ||
Once you have set up your account, generating API keys is possible in your user settings.<br> | ||
|
||
|
||
### Installation | ||
|
||
Navigate into your MagicMirrors modules folder: | ||
|
||
```shell | ||
cd ~/MagicMirror/modules | ||
``` | ||
Clone this repository: | ||
```shell | ||
git clone https://github.com/ImanuelBertrand/MMM-ChatGPT.git | ||
``` | ||
|
||
Add the following minimum configuration block to the modules array in the `config/config.js` file and adjust it to your liking: | ||
```js | ||
{ | ||
module: 'MMM-ChatGPT', | ||
position: 'bottom_bar', | ||
config: { | ||
apiKey: '[Your API Key]', | ||
} | ||
}, | ||
``` | ||
|
||
## Configuration | ||
|
||
| Option | Description | | ||
|----------------------|------------------------------------------------------------------------------------------------------------------------------------| | ||
| `apiKey` | Your API key, see prerequisites | | ||
| `updateInterval ` | Refresh interval in seconds <br>**Type:** `int` <br>Default `900` = 15 minutes | | ||
| `animationSpeed ` | Speed of the update animation in seconds <br>**Type:** `int` <br>Default `1` | | ||
| `initialDelay` | Delay before first update in seconds. Useful if the prompt needs to fetch data from the screen.<br>**Type:** `int` <br>Default `0` | | ||
| `initialPrompt` | The initial setup prompt. See examples. <br>**Type:** `string` <br>Default `[]` (None) | | ||
| `prompts` | The prompts to use. See examples. <br>**Type:** `string` <br>Default `[]` (None) | | ||
| `loadingPlaceholder` | The placeholder text to show while the request is loading <br>**Type:** `string` <br>Default `Loading ...` | | ||
| `model` | The OpenAI model to use <br>**Type:** `string` <br>Default `gpt-3.5-turbo` | | ||
| `endpoint` | The endpoint to use <br>**Type:** `string` <br>Default `https://api.openai.com/v1/chat/completions` | | ||
| `timeout` | Timeout in seconds <br>**Type:** `int` <br>Default `5` | | ||
| `retryDelay ` | Retry interval in seconds, in case the request fails <br>**Type:** `int` <br>Default `1` | | ||
| `fontURL` | Font URL to use for the text <br>**Type:** `string` <br>Default `''` (None, use system default) | | ||
| `fontSize` | CSS font size <br>**Type:** `string` <br>Default `''` (None, use system default) | | ||
| `fontStyle` | CSS font style <br>**Type:** `string` <br>Default `''` (None, use system default) | | ||
| `color` | CSS font color <br>**Type:** `string` <br>Default `''` (None, use system default) | | ||
| `className` | Class names to assign to the output<br>**Type:** `string` <br>Default `light small` | | ||
|
||
### Advanced usage | ||
If you define more than one prompt, the module will select one at random.<br> | ||
The inital prompt will be prefixed to the regular prompt. | ||
All prompts (both initial and regular) can contain code using `{{code}}` syntax. The code will be evaluated and replaced with the result. This allows you to fetch data from the screen and use it in the prompt. The code is evaluated in the context of the MagicMirror window, so you can use all the DOM API and other browser APIs.<br> | ||
|
||
### Security warning | ||
This module will, if configured to do so, execute code defined in the configuration.<br> | ||
This is intentional and not a bad thing, but (as always) be careful when copying code from the internet. | ||
|
||
### Example | ||
This example relies on different modules to be present on the screen (1x time, 3x weather). <br>It fetches the current weather, the weather for later today and the weather for the coming days and uses it in the prompt. It also uses the `initialPrompt` to set up the prompt with the current weather data. | ||
|
||
{ | ||
module: 'MMM-ChatGPT', | ||
position: 'bottom_bar', | ||
config: { | ||
color: 'white', | ||
apiKey: '[API KEY]', | ||
updateInterval: 60 * 60, | ||
initialDelay: 1, | ||
initialPrompt: [{ | ||
role: "system", | ||
content: "This is the current time: '{{document.querySelectorAll('.clock')[0].innerText}}'." + | ||
"This is the current weather including the time of sunset/sunrise: '{{document.querySelectorAll('.weather')[0].innerText}}'. " + | ||
"This is the weather for later today: '{{document.querySelectorAll('.weather')[1].innerText}}'. " + | ||
"This is the weather for the coming days: '{{document.querySelectorAll('.weather')[2].innerText}}'. " + | ||
}], | ||
prompts: [ | ||
[{ | ||
role: "user", | ||
content: "Make a joke." | ||
}], | ||
[{ | ||
role: "user", | ||
content: "Describe the current weather data in a slightly amusing way. Don't use more than one sentence." | ||
}], | ||
[{ | ||
role: "user", | ||
content: "Describe the current weather in the style of the Wee Free Men from the novels of Terry Pratchett, but don't mention any specific names from the novels." | ||
}], | ||
] | ||
} | ||
}, |