Source code

Revision control

Copy as Markdown

// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "ollama.h"
#include "translationsettings.h"
#include "translationutils.h"
#include "translatormessage.h"
#include <QtCore/qjsonarray.h>
#include <QtCore/qjsonobject.h>
using namespace Qt::Literals::StringLiterals;
QT_BEGIN_NAMESPACE
Ollama::Ollama()
: m_payloadBase(std::make_unique<QJsonObject>()),
m_useJsonFormat(TranslationSettings::maxJsonFormatTries())
{
m_payloadBase->insert("stream"_L1, false);
m_payloadBase->insert("think"_L1, false);
QJsonObject opts;
opts.insert("temperature"_L1, TranslationSettings::temperature());
m_payloadBase->insert("options"_L1, opts);
}
Ollama::~Ollama() = default;
QList<Batch> Ollama::makeBatches(const Messages &messages, const QString &userContext) const
{
QHash<QString, QList<const TranslatorMessage *>> nonPluralGroups;
QHash<QString, QList<const TranslatorMessage *>> pluralGroups;
for (const auto &item : messages.items) {
const QString key = item->context() + item->label();
if (item->isPlural())
pluralGroups[key].append(item);
else
nonPluralGroups[key].append(item);
}
const int maxBatchSize = TranslationSettings::maxBatchSize();
QList<Batch> out;
out.reserve(nonPluralGroups.size() + pluralGroups.size());
auto createBatches = [&](const QHash<QString, QList<const TranslatorMessage *>> &groups,
int pluralFormsCount) {
for (auto it = groups.cbegin(); it != groups.cend(); ++it) {
auto msgIt = it.value().cbegin();
while (msgIt != it.value().cend()) {
Batch b;
b.srcLang = messages.srcLang;
b.tgtLang = messages.tgtLang;
b.context = it.key();
b.userContext = userContext;
b.pluralFormsCount = pluralFormsCount;
b.items.reserve(it.value().size());
while (msgIt != it.value().cend() && b.items.size() < maxBatchSize) {
Item item;
item.msg = *msgIt;
item.translation = item.msg->translation();
b.items.append(std::move(item));
msgIt++;
}
out.append(std::move(b));
}
}
};
createBatches(nonPluralGroups, 1);
createBatches(pluralGroups, messages.pluralFormsCount);
return out;
}
QHash<QString, QStringList> Ollama::extractTranslations(const QByteArray &response, bool plural)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(response, &err);
if (err.error != QJsonParseError::NoError) {
m_useJsonFormat--;
return {};
}
QHash<QString, QStringList> translations;
if (plural) {
translations = extractPluralTranslations(doc.object(), "Plurals"_L1);
} else {
auto singleTranslations = extractKeyValuePairs(doc.object(), "Translations"_L1);
for (auto it = singleTranslations.cbegin(); it != singleTranslations.cend(); ++it)
translations[it.key()] << it.value();
}
if (translations.isEmpty()) {
m_useJsonFormat--;
return translations;
}
// If we get a successful response by using json format, the model
// is a formatted model. So we want to prevent falling back to
// non formatted model (harmony) if there are occasional empty
// responses later.
if (m_useJsonFormat > 0)
m_useJsonFormat = std::numeric_limits<int>::max();
return translations;
}
QStringList Ollama::extractModels(const QByteArray &response) const
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(response, &err);
if (err.error != QJsonParseError::NoError)
return {};
const QJsonObject obj = doc.object();
const QJsonArray arr = obj.value("models"_L1).toArray();
QStringList models;
for (const QJsonValue &v : arr)
models.append(v.toObject().value("name"_L1).toString());
return models;
}
QByteArray Ollama::payload(const Batch &b) const
{
QJsonObject systemMessage;
systemMessage.insert("role"_L1, "system"_L1);
const bool plural = b.pluralFormsCount > 1;
systemMessage.insert("content"_L1,
plural ? pluralTranslationSystemPrompt() : translationSystemPrompt());
QJsonObject userMessage;
userMessage.insert("role"_L1, "user"_L1);
userMessage.insert("content"_L1, makePrompt(b));
QJsonArray messages;
messages.append(systemMessage);
messages.append(userMessage);
QJsonObject req = *m_payloadBase;
req.insert("messages"_L1, messages);
if (m_useJsonFormat > 0)
req.insert("format"_L1, "json"_L1);
return QJsonDocument(req).toJson();
}
std::optional<QByteArray> Ollama::stageModel(const QString &modelName)
{
if (auto m = m_payloadBase->constFind("model"_L1);
m == m_payloadBase->constEnd() || *m != modelName) {
m_useJsonFormat = TranslationSettings::maxJsonFormatTries();
m_payloadBase->insert("model"_L1, modelName);
}
std::optional<QByteArray> res;
if (!m_lastWakeupTimer.isValid()
|| m_lastWakeupTimer.hasExpired(TranslationSettings::ollamaWakeUpTimeoutMs())) {
m_lastWakeupTimer.start();
QJsonObject wakeup;
wakeup.insert("model"_L1, modelName);
res.emplace(QJsonDocument(wakeup).toJson());
}
return res;
}
void Ollama::setUrl(const QString &url)
{
m_url = url;
}
QUrl Ollama::translationEndpoint() const
{
return QUrl(m_url).resolved(QUrl("/api/chat"_L1));
}
QUrl Ollama::discoveryEndpoint() const
{
return QUrl(m_url).resolved(QUrl("/api/tags"_L1));
}
void Ollama::onRequestRejected()
{
m_useJsonFormat--;
}
QString Ollama::makePrompt(const Batch &b) const
{
QStringList lines;
lines.reserve(b.items.size() + 32);
if (!b.userContext.isEmpty())
lines << "Application Context: "_L1 + b.userContext;
lines << "Context: "_L1 + b.context;
lines << "Target: "_L1 + b.tgtLang;
if (b.pluralFormsCount > 1)
lines << "Plural forms: "_L1 + QString::number(b.pluralFormsCount);
lines << "Items:"_L1;
for (const Item &it : b.items) {
QString line = "- source: '%1'"_L1.arg(it.msg->sourceText());
if (const QString comment = it.msg->comment(); !comment.isEmpty())
line += ", comment: '%1'"_L1.arg(comment);
lines << line;
}
return lines.join(QLatin1Char('\n'));
}
QT_END_NAMESPACE