Skip to content

Commit

Permalink
Initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
ImanuelBertrand committed Mar 6, 2023
1 parent d2e58d2 commit 87b2ef4
Show file tree
Hide file tree
Showing 4 changed files with 302 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
5 changes: 5 additions & 0 deletions CHANGELOG.md
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
194 changes: 194 additions & 0 deletions MMM-ChatGPT.js
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);
},
});
102 changes: 102 additions & 0 deletions README.md
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."
}],
]
}
},

0 comments on commit 87b2ef4

Please sign in to comment.