Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions API_endpoint_cert.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#ifndef API_endpoint_cert_h
#define API_endpoint_cert_h

/* OpenAI API endpoint root certificate used to ensure response is actually from OpenAPI

TROUBLESHOOTING: If you get the "Could not connect to server. Trying again."
message in the serial monitor, you may want to check that this root certificate below has not expired.
*/

#define ROOT_CA_CERT "-----BEGIN CERTIFICATE-----\n" \
"MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD\n"\
"VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG\n"\
"A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw\n"\
"WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz\n"\
"IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi\n"\
"AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi\n"\
"QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR\n"\
"HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\n"\
"BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D\n"\
"9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8\n"\
"p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD\n"\
"-----END CERTIFICATE-----\n"

#endif // API_endpoint_cert.h
32 changes: 23 additions & 9 deletions ChatGPTuino.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ChatGPTuino::ChatGPTuino(uint32_t maxTokens = MIN_TOKENS, const uint16_t maxMsgs
_msgCount{0},
_sysMsgMode{Default},
_MAX_MESSAGE_LENGTH{_maxTokens * CHARS_PER_TOKEN},
_DYNAMIC_JSON_DOC_SIZE{
_JSON_DOC_SIZE{
(JSON_DATA_STRUCTURE_MEMORY_BASE + (_maxMsgs * JSON_DATA_STRUCTURE_MEMORY_PER_MSG)) + (JSON_KEY_STRING_MEMORY_BASE + ((_MAX_MESSAGE_LENGTH + JSON_VALUE_STRING_MEMORY_PER_MSG) * _maxMsgs)) + JSON_MEMORY_SLACK} {};

// Destructor
Expand Down Expand Up @@ -106,14 +106,25 @@ bool ChatGPTuino::init(const char *key, const char *model)
// Set system message mode and optionally system message
void ChatGPTuino::systemMessageMode(SysMessageModes mode, char *sysMsg)
{

#ifdef VERBOSE_PRINTS
Serial.println(" | systemMessageMode | START");
#endif

_sysMsgMode = mode;

if (sysMsg)
{
safe_strncpy(_sysMessageContent, _MAX_MESSAGE_LENGTH, sysMsg);
Serial.print("_sysMessageContent ->");
#ifdef VERBOSE_PRINTS
Serial.println(" | systemMessageMode | _sysMessageContent ->");
Serial.println(_sysMessageContent);
#endif
}

#ifdef VERBOSE_PRINTS
Serial.println(" | systemMessageMode | END");
#endif
}

char *ChatGPTuino::getLastMessageContent() const
Expand Down Expand Up @@ -230,6 +241,7 @@ GetResponseCodes ChatGPTuino::getResponse()
{

// Create a secure wifi client
// WiFiClient client;
WiFiClientSecure client;
client.setCACert(ROOT_CA_CERT);

Expand Down Expand Up @@ -305,7 +317,7 @@ void ChatGPTuino::postRequest(JsonDocument *pJsonRequestBody, WiFiClientSecure *
// Make request
pClient->print("POST ");
pClient->print(OPEN_AI_END_POINT);
pClient->println(" HTTP/1.1");
pClient->println(" HTTP/1.0"); // Can not seem to configure this for 1.1
// Send headers
pClient->print("Host: ");
pClient->println(OPEN_AI_SERVER);
Expand Down Expand Up @@ -388,30 +400,31 @@ bool ChatGPTuino::putResponseInMsgArray(WiFiClientSecure *pClient)
https://arduinojson.org/news/2020/03/22/version-6-15-0/ */
// StaticJsonDocument<500> filter; // JSON 7 UPDATE
JsonDocument filter;
//JsonObject filter_choices_0_message = filter["choices"][0].createNestedObject("message"); // JSON 7 UPDATE
// JsonObject filter_choices_0_message = filter["choices"][0].createNestedObject("message"); // JSON 7 UPDATE
JsonObject filter_choices_0_message = filter["choices"][0]["message"].to<JsonObject>();

filter_choices_0_message["role"] = true;
filter_choices_0_message["content"] = true;

// Deserialize the JSON
#ifdef VERBOSE_PRINTS
Serial.print(" | putResponseInMsgArray | ESP.getMaxAllocHeap -> ");
Serial.println(ESP.getMaxAllocHeap());
#endif

//JsonDocument jsonResponse(ESP.getMaxAllocHeap() - 1024);
// JsonDocument jsonResponse(ESP.getMaxAllocHeap() - 1024);
JsonDocument jsonResponse;
DeserializationError error = deserializeJson(jsonResponse, *pClient, DeserializationOption::Filter(filter));
jsonResponse.shrinkToFit();
Serial.println("putResponseInMsgArray - deserialize success");
Serial.println(" | putResponseInMsgArray | deserialize success");

// If deserialization fails, exit immediately and try again.
if (error)
{

pClient->stop();

Serial.print(" | deserializeJson() failed->");
Serial.print(" | putResponseInMsgArray | deserializeJson() failed->");
Serial.println(error.c_str());

return 0;
Expand All @@ -420,14 +433,15 @@ bool ChatGPTuino::putResponseInMsgArray(WiFiClientSecure *pClient)
const char *newMsg = jsonResponse["choices"][0]["message"]["content"] | "...";

#ifdef VERBOSE_PRINTS
Serial.print(" | putResponseInMsgArray | newMsg -> ");
Serial.println(newMsg);
Serial.print("measureJSON ");
Serial.print(" | putResponseInMsgArray | measureJSON ");
Serial.print(measureJson(jsonResponse["choices"][0]["message"]["content"]));
Serial.print(" | strlen ");
Serial.println(strlen(jsonResponse["choices"][0]["message"]["content"]));
#endif

putMessage(newMsg, strlen(jsonResponse["choices"][0]["message"]["content"]), Assistant);
Serial.println("putResponseInMsgArray - putMessage success");
Serial.println(" | putResponseInMsgArray | putMessage success");
return 1;
}
33 changes: 6 additions & 27 deletions ChatGPTuino.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// #include <sys/_stdint.h>
#include <ArduinoJson.h> // Handle JSON formatting for API calls
#include <WiFiClientSecure.h> // ESP32
#include <WiFi.h> // ESP32
#include <API_endpoint_cert.h>

#define MIN_TOKENS 50
#define MAX_TOKENS 2000 // Used for sizing JSON response
Expand All @@ -25,33 +27,10 @@
#define SERVER_RESPONSE_WAIT_TIME (15 * 1000) // How long to wait for a server response (seconds * 1000)

// #define DEBUG_SERVER_RESPONSE_BREAKING
// #define VERBOSE_PRINTS
#define VERBOSE_PRINTS

#define OPEN_AI_END_POINT "https://api.openai.com/v1/chat/completions"
#define OPEN_AI_SERVER "api.openai.com"
// OpenAI API endpoint root certificate used to ensure response is actually from OpenAPI
// TODO - Verify that the certificate matters! Have a check that verifies the connection is secure.
#define ROOT_CA_CERT "-----BEGIN CERTIFICATE-----\n" \
"MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\n" \
"RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD\n" \
"VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX\n" \
"DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y\n" \
"ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy\n" \
"VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr\n" \
"mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr\n" \
"IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK\n" \
"mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu\n" \
"XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy\n" \
"dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye\n" \
"jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1\n" \
"BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\n" \
"DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92\n" \
"9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx\n" \
"jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0\n" \
"Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz\n" \
"ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS\n" \
"R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp\n" \
"-----END CERTIFICATE-----\n"

enum Roles
{
Expand Down Expand Up @@ -120,9 +99,9 @@ class ChatGPTuino
return _MAX_MESSAGE_LENGTH;
}

uint32_t DYNAMIC_JSON_DOC_SIZE() const
uint32_t JSON_DOC_SIZE() const
{
return _DYNAMIC_JSON_DOC_SIZE;
return _JSON_DOC_SIZE;
}

char *getLastMessageContent() const;
Expand Down Expand Up @@ -164,7 +143,7 @@ class ChatGPTuino
uint16_t _maxMsgs;
uint16_t _msgCount;
uint32_t _MAX_MESSAGE_LENGTH;
uint32_t _DYNAMIC_JSON_DOC_SIZE; // NOTE: I BELIEVE THIS WILL BE DEPRECATED
uint32_t _JSON_DOC_SIZE; // NOTE: I BELIEVE THIS WILL BE DEPRECATED
char *_secret_key;
char *_model;
char *_sysMessageContent;
Expand Down
59 changes: 26 additions & 33 deletions tests/tests.ino
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
#include <WiFi.h> // ESP32

#include <AUnit.h> // Testing
#include <ChatGPTuino.h>
// #include "credentials.h" // Network name, password, and private API key

#include "secrets.h" // Network name, password, and private API key

#define TESTING_ON 1

/* Assert( expected value (Known Value), actual value(value under test)) */

// const char *test_key = "sk-VT65uEtK8cUfB1KuEx0QT3BlbkFJHnIvsADF3rJw5-XXXXXX";
const char *model = "gpt-4o";

/* Assert( expected value (Known Value), actual value(value under test)) */

#if TESTING_ON
test(ChatBox_itializes_with_valid_values) {

ChatGPTuino chat{ 0, 0 };
chat.init(test_key, model);
long testDocSize = 3056; //Based on Arduino JSON 6 Assistant
assertEqual((const char *)model, (const char *)chat.model());
assertEqual((long)chat.maxTokens(), (long)MIN_TOKENS);
assertEqual(chat.numMessages(), MIN_MESSAGES);
assertEqual((long)(CHARS_PER_TOKEN * chat.maxTokens()), (long)chat.MAX_MESSAGE_LENGTH());
assertEqual(testDocSize, (long)chat.DYNAMIC_JSON_DOC_SIZE());
}
// test(ChatBox_itializes_with_valid_values) {

// ChatGPTuino chat{ 0, 0 };
// chat.init(test_key, model);
// long testDocSize = 3056; //Based on Arduino JSON 6 Assistant
// assertEqual((const char *)model, (const char *)chat.model());
// assertEqual((long)chat.maxTokens(), (long)MIN_TOKENS);
// assertEqual(chat.numMessages(), MIN_MESSAGES);
// assertEqual((long)(CHARS_PER_TOKEN * chat.maxTokens()), (long)chat.MAX_MESSAGE_LENGTH());
// assertEqual(testDocSize, (long)chat.JSON_DOC_SIZE());
// }

// test(init_allocates_space_for_message_contexts) {

Expand Down Expand Up @@ -119,18 +118,18 @@ test(ChatBox_itializes_with_valid_values) {
// assertEqual((long)4, (long)chat.getLastMessageLength());
// }

// test(init_with_sys_msg_para_inserts_sys_msg) {
// ChatGPTuino chat{ 50, 4 };
// char *sysMsgTest = "If this is a test, please respond with only the word PASS, otherwise please respond with only the word FAIL";
// chat.init(key, model);
// chat.systemMessageMode(Insert, sysMsgTest);

// char *testMessage = "This is NOT a test.";
// chat.putMessage(testMessage, strlen(testMessage), User);
// chat.getResponse();

// assertEqual("PASS", (const char *)chat.getLastMessageContent());
// }
test(init_with_sys_msg_para_inserts_sys_msg) {
ChatGPTuino chat{ 50, 4 };
char *sysMsgTest = "If this is a test, please respond with only the word PASS, otherwise please respond with only the word FAIL";
chat.init(key, model);
chat.systemMessageMode(Insert, sysMsgTest);
Serial.println("TEST systemMessageMode success");
char *testMessage = "This is NOT a test.";
chat.putMessage(testMessage, strlen(testMessage), User);
chat.getResponse();

assertEqual("PASS", (const char *)chat.getLastMessageContent());
}


#endif
Expand All @@ -154,11 +153,5 @@ void loop() {

#if TESTING_ON
aunit::TestRunner::run();
// #else
// static bool runOnce = true;
// if (runOnce) {
// helloChatGPT();
// runOnce = false;
// }
#endif
}