From eb9bf5fbc79951f9866cb9e64d52d6e7dba9da71 Mon Sep 17 00:00:00 2001 From: Zhang TingAn Date: Thu, 26 Dec 2024 10:29:07 +0800 Subject: [PATCH] feat: [aiManager] add interface 1.event when LLM changed 2.modelname 3.LLM`s idle state to description LLM can send request or not Log: as title --- src/base/ai/abstractllm.h | 2 + src/common/util/eventdefinitions.h | 3 + src/plugins/aimanager/aimanager.cpp | 15 ++- .../openai/openaicompatibleconversation.cpp | 124 ++++++++++++++---- .../aimanager/openai/openaicompatiblellm.cpp | 13 ++ .../aimanager/openai/openaicompatiblellm.h | 2 + 6 files changed, 135 insertions(+), 24 deletions(-) diff --git a/src/base/ai/abstractllm.h b/src/base/ai/abstractllm.h index 2117146ad..7a762281e 100644 --- a/src/base/ai/abstractllm.h +++ b/src/base/ai/abstractllm.h @@ -25,6 +25,7 @@ class AbstractLLM : public QObject explicit AbstractLLM(QObject *parent = nullptr); virtual ~AbstractLLM() {} + virtual QString modelName() const = 0; virtual QString modelPath() const = 0; virtual bool checkValid(QString *errStr) = 0; virtual QJsonObject create(const Conversation &conversation) = 0; @@ -37,6 +38,7 @@ class AbstractLLM : public QObject virtual void cancel() = 0; virtual void setMaxTokens(int maxToken) = 0; virtual Conversation *getCurrentConversation() = 0; + virtual bool isIdle() = 0; signals: void dataReceived(const QString &data, ResponseState statu); diff --git a/src/common/util/eventdefinitions.h b/src/common/util/eventdefinitions.h index 0ddbef114..8cd6f5682 100644 --- a/src/common/util/eventdefinitions.h +++ b/src/common/util/eventdefinitions.h @@ -133,6 +133,9 @@ OPI_OBJECT(session, OPI_INTERFACE(sessionRenamed, "oldName", "newName") OPI_INTERFACE(sessionRemoved, "session") ) +OPI_OBJECT(ai, + OPI_INTERFACE(LLMChanged) + ) struct AnalysedData { diff --git a/src/plugins/aimanager/aimanager.cpp b/src/plugins/aimanager/aimanager.cpp index ea3800ebc..14b83fb91 100644 --- a/src/plugins/aimanager/aimanager.cpp +++ b/src/plugins/aimanager/aimanager.cpp @@ -7,6 +7,7 @@ #include "openai/openaicompatiblellm.h" #include "services/option/optionmanager.h" #include "option/detailwidget.h" +#include "common/util/eventdefinitions.h" #include @@ -83,10 +84,22 @@ void AiManager::removeModel(const LLMInfo &info) void AiManager::readLLMFromOption() { + auto currentModels = d->models; + bool changed = false; d->models.clear(); + QMap map = OptionManager::getInstance()->getValue(kCATEGORY_CUSTOMMODELS, kCATEGORY_OPTIONKEY).toMap(); auto LLMs = map.value(kCATEGORY_CUSTOMMODELS); + if (LLMs.toList().size() != currentModels.size()) + changed = true; + for (auto llmInfo : LLMs.toList()) { - appendModel(LLMInfo::fromVariantMap(llmInfo.toMap())); + LLMInfo info = LLMInfo::fromVariantMap(llmInfo.toMap()); + if (!currentModels.contains(info)) + changed = true; + appendModel(info); } + + if (changed) + ai.LLMChanged(); } diff --git a/src/plugins/aimanager/openai/openaicompatibleconversation.cpp b/src/plugins/aimanager/openai/openaicompatibleconversation.cpp index 147552a1a..cf218b106 100644 --- a/src/plugins/aimanager/openai/openaicompatibleconversation.cpp +++ b/src/plugins/aimanager/openai/openaicompatibleconversation.cpp @@ -7,6 +7,52 @@ #include #include +void mergeFunctionCall(QJsonObject &functionCall, const QJsonObject &delta) +{ + if (delta.contains("name")) { + functionCall["name"] = functionCall["name"].toString() + delta.value("name").toString(); + } + if (delta.contains("arguments")) { + functionCall["arguments"] = functionCall["arguments"].toString() + delta.value("arguments").toString(); + } +} + +void mergeToolCallMap(QMap &toolCallMaps, const QJsonArray &tool_calls) +{ + for (const QJsonValue &tool_call : tool_calls) { + const QJsonObject &toolCallObj = tool_call.toObject(); + int index = toolCallObj["index"].toInt(); + + if (!toolCallMaps[index].contains("function")) { + toolCallMaps[index]["function"] = QJsonObject(); + } + + toolCallMaps[index]["index"] = index; + + if (toolCallObj.contains("id")) { + toolCallMaps[index]["id"] = toolCallObj.value("id"); + } + if (toolCallObj.contains("type")) { + toolCallMaps[index]["type"] = toolCallObj.value("type"); + } + + QJsonObject toolFun = toolCallMaps[index]["function"].toObject(); + + if (const QJsonValue &tmpToolFunVal = toolCallObj.value("function"); !tmpToolFunVal.isUndefined()) { + const QJsonObject &tmpToolFun = tmpToolFunVal.toObject(); + if (tmpToolFun.contains("name")) { + toolFun["name"] = toolFun["name"].toString() + tmpToolFun.value("name").toString(); + } + if (tmpToolFun.contains("arguments")) { + toolFun["arguments"] = toolFun["arguments"].toString() + tmpToolFun.value("arguments").toString(); + } + } + + toolCallMaps[index]["function"] = toolFun; + } +} + + OpenAiCompatibleConversation::OpenAiCompatibleConversation() { } @@ -14,38 +60,51 @@ OpenAiCompatibleConversation::OpenAiCompatibleConversation() QJsonObject OpenAiCompatibleConversation::parseContentString(const QString &content) { QString deltacontent; - QRegularExpression regex(R"(data:\s*\{(.*)\})"); QRegularExpressionMatchIterator iter = regex.globalMatch(content); + QString finishReason; + QJsonObject functionCall; + QMap toolCallMaps; - QString finishReason = ""; while (iter.hasNext()) { QRegularExpressionMatch match = iter.next(); QString matchString = match.captured(0); - int startIndex = matchString.indexOf('{'); int endIndex = matchString.lastIndexOf('}'); + if (startIndex < 0 || endIndex <= startIndex) { + continue; + } + QString content = matchString.mid(startIndex, endIndex - startIndex + 1); + QJsonObject j = QJsonDocument::fromJson(content.toUtf8()).object(); + + if (!j.contains("choices")) { + continue; + } - if (startIndex >= 0 && endIndex > startIndex) { - QString content = matchString.mid(startIndex, endIndex - startIndex + 1); - - QJsonObject j = QJsonDocument::fromJson(content.toUtf8()).object(); - if (j.contains("choices")) { - const QJsonArray &choices = j["choices"].toArray(); - for (auto choice = choices.begin(); choice != choices.end(); choice++) { - const QJsonObject &cj = choice->toObject(); - if (cj.contains("finish_reason")) - finishReason = cj["finish_reason"].toString(); - if (cj.contains("delta")) { - const QJsonObject &delta = cj["delta"].toObject(); - if (delta.contains("content")) { - const QString &deltaData = delta["content"].toString(); - deltacontent += deltaData; - } - } else if (cj.contains("text")) { - deltacontent += cj["text"].toString(); - } + const QJsonArray &choices = j["choices"].toArray(); + for (const QJsonValue &choice : choices) { + const QJsonObject &cj = choice.toObject(); + + if (cj.contains("finish_reason")) { + finishReason = cj["finish_reason"].toString(); + } + + if (cj.contains("delta")) { + const QJsonObject &delta = cj["delta"].toObject(); + + if (delta.contains("content")) { + deltacontent += delta["content"].toString(); + } + + if (delta.contains("function_call")) { + mergeFunctionCall(functionCall, delta.value("function_call").toObject()); } + + if (delta.contains("tool_calls")) { + mergeToolCallMap(toolCallMaps, delta.value("tool_calls").toArray()); + } + } else if (cj.contains("text")) { + deltacontent += cj["text"].toString(); } } } @@ -55,8 +114,27 @@ QJsonObject OpenAiCompatibleConversation::parseContentString(const QString &cont response["content"] = deltacontent; } - if (!finishReason.isEmpty()) + if (!functionCall.isEmpty() || !toolCallMaps.isEmpty()) { + QJsonObject tools; + + if (!functionCall.isEmpty()) { + tools["function_call"] = functionCall; + } + + if (!toolCallMaps.isEmpty()) { + QJsonArray toolCalls; + for (const auto &toolCallObj : toolCallMaps) { + toolCalls << toolCallObj; + } + tools["tool_calls"] = toolCalls; + } + + response["tools"] = tools; + } + + if (!finishReason.isEmpty()) { response["finish_reason"] = finishReason; + } return response; } diff --git a/src/plugins/aimanager/openai/openaicompatiblellm.cpp b/src/plugins/aimanager/openai/openaicompatiblellm.cpp index fe0a8a85d..9cb510b9c 100644 --- a/src/plugins/aimanager/openai/openaicompatiblellm.cpp +++ b/src/plugins/aimanager/openai/openaicompatiblellm.cpp @@ -119,6 +119,11 @@ OpenAiCompatibleLLM::~OpenAiCompatibleLLM() delete d; } +QString OpenAiCompatibleLLM::modelName() const +{ + return d->modelName; +} + QString OpenAiCompatibleLLM::modelPath() const { return d->modelPath; @@ -196,6 +201,9 @@ QJsonObject OpenAiCompatibleLLM::create(const Conversation &conversation) void OpenAiCompatibleLLM::request(const QJsonObject &data) { + if (d->waitingResponse) + return; + QByteArray body = QJsonDocument(data).toJson(); d->httpResult.clear(); d->waitingResponse = true; @@ -339,3 +347,8 @@ void OpenAiCompatibleLLM::setMaxTokens(int maxTokens) { d->maxTokens = maxTokens; } + +bool OpenAiCompatibleLLM::isIdle() +{ + return !d->waitingResponse; +} diff --git a/src/plugins/aimanager/openai/openaicompatiblellm.h b/src/plugins/aimanager/openai/openaicompatiblellm.h index fd35b0685..b83becd38 100644 --- a/src/plugins/aimanager/openai/openaicompatiblellm.h +++ b/src/plugins/aimanager/openai/openaicompatiblellm.h @@ -20,6 +20,7 @@ class OpenAiCompatibleLLM : public AbstractLLM void setModelPath(const QString &path); void setApiKey(const QString &apiKey); + QString modelName() const override; QString modelPath() const override; bool checkValid(QString *errStr) override; QJsonObject create(const Conversation &conversation) override; @@ -31,6 +32,7 @@ class OpenAiCompatibleLLM : public AbstractLLM void processResponse(QNetworkReply *reply) override; void cancel() override; void setMaxTokens(int maxTokens) override; + bool isIdle() override; signals: void requstCancel();