Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
++
++ #include "openaichatmanager.h"
++
++ #include "modelsapi.h"
++ #include "responsesapi.h"
++
++ #include <QMap>
++
++ using namespace QtOpenAPI;
++ using namespace QtOpenAPI::Examples;
++
++ static QMap<OpenAIChatManager::CharacterMode, QString> DefaultCharacters = {
++ {
++ OpenAIChatManager::CharacterMode::Wizard,
++ "You are an 80th-level wizard. You know everything about the magic and magical creatures. "
++ "You always can give the information about any magical topic. "
++ "You don't give information about topics unrelated to the magic. "
++ "If someone asks you about anything unrelated to magic topics, "
++ "you politely suggest to switch to the magic related stuff. "
++ "Your dream is to find a student to teach the magic. "
++ "Also, you like jokes and often use metaphors and humor to explain magical concepts."
++ "If a person continues to ask you about unrelated to the magic topics, "
++ "you cast the 'Obliviate' spell on the person trying make him to forget the unrelated "
++ "topic! You simply do it when the time is right. If person doesn't forget the topic, "
++ "you say him he has a strong magician skills and suggest to train magic spells together."
++ },
++ {
++ OpenAIChatManager::CharacterMode::Scientist,
++ "You are Albert Einstein. You speak, think, and behave exactly like Albert Einstein, "
++ "reflecting both his public persona and personal quirks. You possess complete knowledge "
++ "of his life, thoughts, and work, as well as a deep understanding of physics, including "
++ "the latest discoveries. Your are passionately devoted to physics, but you prefer "
++ "to keep your answers simple. "
++ "You are a follower of the string theory. But you start speaking about it only "
++ "if you were asked to speak about the string theory. You never suggest to speak "
++ "about the string theory first. If a person asks you about topics unrelated to physics, "
++ "you redirect the conversation to physics. "
++ "If the person insists to speak about unrelated to physics topics more then three times, "
++ "you say: 'Two things are infinite: the universe and human stupidity; "
++ "and I'm not sure about the universe'. "
++ "Note, you should use humor to explain complex concepts in true Einstein style."
++ },
++ {
++ OpenAIChatManager::CharacterMode::DefaultMode,
++ "" // Normal Chat mode, nothing to add
++ }
++ };
++
++ OpenAIChatManager::OpenAIChatManager(QObject *parent)
++ : QObject{parent}
++ {
++ m_responseApi = new QtOpenAPI::ResponsesApi(this);
++ m_modelsApi = new ModelsApi(this);
++
++ QByteArray envKey = qgetenv("CHAT_OPENAI_KEY");
++ if (!envKey.isEmpty()) {
++ m_apiKey = QString::fromLatin1(envKey);
++ m_responseApi->setBearerToken(m_apiKey);
++ m_modelsApi->setBearerToken(m_apiKey);
++
++ // request model list once at singleton creation
++ requestModelList();
++ } else {
++ qCritical() << "Please, set CHAT_OPENAI_KEY environment variable for using the example.";
++ }
++ }
++
++ OpenAIChatManager::~OpenAIChatManager()
++ {
++ }
++
++ QString OpenAIChatManager::modelName() const
++ {
++ return m_modelName;
++ }
++
++ void OpenAIChatManager::setModelName(const QString &newName)
++ {
++ if (m_modelName == newName)
++ return;
++ m_modelName = newName;
++ // Well, it's not necessary, but we can clean it. Just in case.
++ m_responseId.clear();
++ emit modelNameChanged();
++ }
++
++ QString OpenAIChatManager::userRequest() const
++ {
++ return m_userRequest;
++ }
++
++ void OpenAIChatManager::setUserRequest(const QString &request)
++ {
++ if (m_userRequest == request)
++ return;
++ m_userRequest = request;
++ emit userRequestChanged();
++ }
++
++ QStringList OpenAIChatManager::modelList()
++ {
++ return m_modelList;
++ }
++
++ void OpenAIChatManager::setModelList(const QStringList &list)
++ {
++ if (m_modelList == list)
++ return;
++ m_modelList = list;
++ emit modelListChanged();
++ }
++
++ OpenAIChatManager::CharacterMode OpenAIChatManager::characterId() const
++ {
++ return m_characterId;
++ }
++
++ void OpenAIChatManager::setCharacterId(CharacterMode character)
++ {
++ if (m_characterId == character)
++ return;
++ m_characterId = character;
++ emit characterIdChanged();
++ }
++
++ QString OpenAIChatManager::customUserCharacter() const
++ {
++ return m_customUserCharacter;
++ }
++
++ void OpenAIChatManager::setCustomUserCharacter(const QString &character)
++ {
++ if (m_customUserCharacter == character)
++ return;
++ m_customUserCharacter = character;
++ emit customUserCharacterChanged();
++ }
++
++ void OpenAIChatManager::sendUserRequest()
++ {
++ GenerateResponse response;
++ response.setModel(m_modelName);
++ response.setStore(true);
++ response.setInput(m_userRequest);
++ response.setInstructions((m_characterId == OpenAIChatManager::CharacterMode::UserCharacter)
++ ? m_customUserCharacter : DefaultCharacters.value(m_characterId));
++ if (!m_responseId.isEmpty())
++ response.setPreviousResponseId(m_responseId);
++
++ //! [0]
++ m_responseApi->createResponse(response, this, [&](const QRestReply &reply,
++ const Response &summary) {
++ if (!reply.isSuccess()) {
++ qWarning() << "createResponse:" << reply.errorString() << reply.error();
++ } else {
++ // To save the context of conversation with the openAI model,
++ // we need to track returning ResponseId of the last model's message.
++ // Context becomes irrelevant after switching to the other model. If the model
++ // doesn't know the context id, it will response like if the ResponseId is empty.
++ m_responseId = summary.getId();
++ const QList<OutputMessage> outputMessage = summary.getOutput();
++ for (qsizetype msgIndex = 0; msgIndex < outputMessage.size(); msgIndex++) {
++ const QList<OutputTextContent> messages
++ = outputMessage.at(msgIndex).getContent();
++ for (qsizetype contentIndex = 0; contentIndex < messages.size(); contentIndex++)
++ emit responseReady(messages.at(contentIndex).getText());
++ }
++ }
++ });
++ //! [0]
++ }
++
++ void OpenAIChatManager::requestModelList()
++ {
++ m_modelsApi->listModels(this, [&](const QRestReply &reply,
++ const ListModelsResponse &summary) {
++ if (!reply.isSuccess()) {
++ qWarning() << "listModels:" << reply.errorString() << reply.error();
++ m_modelList.clear();
++ } else {
++ for (qsizetype modelIndex = 0; modelIndex < summary.getData().size(); modelIndex++) {
++ const QString modelName = summary.getData().at(modelIndex).getId();
++ // Note, the list of available models contains unrelated stuff,
++ // like image generating models or search models. We canot use them
++ // for text mode interactions. So, removing them here.
++ if (!m_modelList.contains(modelName)
++ && modelName.contains("gpt", Qt::CaseInsensitive)
++ && !modelName.contains("audio", Qt::CaseInsensitive)
++ && !modelName.contains("image", Qt::CaseInsensitive)
++ && !modelName.contains("search", Qt::CaseInsensitive)
++ && !modelName.contains("realtime", Qt::CaseInsensitive)
++ && !modelName.contains("transcribe", Qt::CaseInsensitive)
++ && !modelName.contains("-pro-", Qt::CaseInsensitive)
++ && !modelName.contains("tts", Qt::CaseInsensitive)) {
++ m_modelList.append(modelName);
++ }
++ }
++ }
++ emit modelListChanged();
++ });
++ }