Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
ollama.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "ollama.h"
6
7#include <QJsonObject>
8#include <QJsonArray>
9
10using namespace Qt::Literals::StringLiterals;
11
12namespace {
13static std::optional<QJsonArray> recursiveFind(const QJsonValue &jval, const QString &key)
14{
15 if (jval.isObject()) {
16 const QJsonObject obj = jval.toObject();
17 auto it = obj.find(key);
18 if (it != obj.end() && it->isArray())
19 return it->toArray();
20 for (it = obj.constBegin(); it != obj.constEnd(); ++it) {
21 if (it.key().trimmed() == key && it.value().isArray())
22 return it.value().toArray();
23 if (const auto r = recursiveFind(it.value(), key); r)
24 return r;
25 }
26 } else if (jval.isArray()) {
27 const QJsonArray arr = jval.toArray();
28 for (const QJsonValue &element : arr)
29 if (const auto r = recursiveFind(element, key); r)
30 return r;
31 } else if (jval.isString()) {
32 QString str = jval.toString();
33 const int startIdx = str.indexOf('{'_L1);
34 const int endIdx = str.lastIndexOf('}'_L1);
35 if (startIdx < 0 || endIdx < 0)
36 return {};
37 str.slice(startIdx, endIdx - startIdx + 1);
38 QJsonParseError err;
39 auto inner = QJsonDocument::fromJson(str.toUtf8(), &err);
40 if (err.error != QJsonParseError::NoError || !inner.isObject())
41 return {};
42 const auto obj = inner.object();
43 if (auto it = obj.find(key); it != obj.end()) {
44 if (it.value().isArray())
45 return it.value().toArray();
46 }
47 }
48 return {};
49}
50} // namespace
51
52QT_BEGIN_NAMESPACE
53
54Ollama::Ollama()
55 : m_payloadBase(std::make_unique<QJsonObject>()),
56 m_systemMessage(std::make_unique<QJsonObject>())
57{
58 m_payloadBase->insert("stream"_L1, false);
59 m_payloadBase->insert("think"_L1, false);
60
61 QJsonObject opts;
62 opts.insert("temperature"_L1, 0.05);
63 m_payloadBase->insert("options"_L1, opts);
64
65 m_systemMessage->insert("role"_L1, "system"_L1);
66 m_systemMessage->insert("content"_L1, makeSystemPrompt());
67}
68
69Ollama::~Ollama() = default;
70
71QList<Batch> Ollama::makeBatches(const Messages &messages, const QString &userContext) const
72{
73 QHash<QString, QList<const TranslatorMessage *>> groups;
74
75 for (const auto &item : messages.items)
76 groups[item->context() + item->label()].append(item);
77
78 QList<Batch> out;
79 out.reserve(groups.size());
80 for (auto it = groups.cbegin(); it != groups.cend(); ++it) {
81 auto msgIt = it.value().cbegin();
82 while (msgIt != it.value().cend()) {
83 Batch b;
84 b.srcLang = messages.srcLang;
85 b.tgtLang = messages.tgtLang;
86 b.context = it.key();
87 b.userContext = userContext;
88 b.items.reserve(it.value().size());
89 while (msgIt != it.value().cend() && b.items.size() < s_maxBatchSize) {
90 Item item;
91 item.msg = *msgIt;
92 item.translation = item.msg->translation();
93 b.items.append(std::move(item));
94 msgIt++;
95 }
96 out.append(std::move(b));
97 }
98 }
99 return out;
100}
101
102QHash<QString, QString> Ollama::extractTranslations(const QByteArray &response)
103{
104 QJsonParseError err;
105 QJsonDocument doc = QJsonDocument::fromJson(response, &err);
106 if (err.error != QJsonParseError::NoError) {
107 m_useJsonFormat--;
108 return {};
109 }
110
111 auto translations = recursiveFind(doc.object(), "Translations"_L1);
112 QHash<QString, QString> out;
113 if (!translations) {
114 m_useJsonFormat--;
115 return out;
116 }
117
118 // If we get a successful response by using json format, the model
119 // is a formatted model. So we want to prevent falling back to
120 // non formatted model (harmony) if there are occasional empty
121 // responses later.
122 if (m_useJsonFormat > 0)
123 m_useJsonFormat = std::numeric_limits<int>::max();
124
125 out.reserve(translations->size());
126 for (const QJsonValue &v : std::as_const(*translations)) {
127 if (v.isObject()) {
128 const QJsonObject obj = v.toObject();
129 const QString key = obj.keys().first();
130 if (QJsonValue val = obj.value(key); val.isString())
131 out[key] = val.toString();
132 }
133 }
134 return out;
135}
136
137QStringList Ollama::extractModels(const QByteArray &response) const
138{
139 QJsonParseError err;
140 QJsonDocument doc = QJsonDocument::fromJson(response, &err);
141 if (err.error != QJsonParseError::NoError)
142 return {};
143 const QJsonObject obj = doc.object();
144 const QJsonArray arr = obj.value("models"_L1).toArray();
145 QStringList models;
146 for (const QJsonValue &v : arr)
147 models.append(v.toObject().value("name"_L1).toString());
148 return models;
149}
150
152{
153 QJsonObject userMessage;
154 userMessage.insert("role"_L1, "user"_L1);
155 userMessage.insert("content"_L1, makePrompt(b));
156
157 QJsonArray messages;
158 messages.append(*m_systemMessage);
159 messages.append(userMessage);
160
161 QJsonObject req = *m_payloadBase;
162 req.insert("messages"_L1, messages);
163
164 if (m_useJsonFormat > 0)
165 req.insert("format"_L1, "json"_L1);
166
167 return QJsonDocument(req).toJson();
168}
169
170std::optional<QByteArray> Ollama::stageModel(const QString &modelName)
171{
172 if (auto m = m_payloadBase->constFind("model"_L1);
173 m == m_payloadBase->constEnd() || *m != modelName) {
174 m_useJsonFormat = s_maxJsonFormatTry;
175 m_payloadBase->insert("model"_L1, modelName);
176 }
177
178 std::optional<QByteArray> res;
179 if (!m_lastWakeupTimer.isValid() || m_lastWakeupTimer.hasExpired(s_wakeUpTimeOut)) {
180 m_lastWakeupTimer.start();
181 QJsonObject wakeup;
182 wakeup.insert("model"_L1, modelName);
183 res.emplace(QJsonDocument(wakeup).toJson());
184 }
185
186 return res;
187}
188
189void Ollama::setUrl(const QString &url)
190{
191 m_url = url;
192}
193
195{
196 return QUrl(m_url).resolved(QUrl("/api/chat"_L1));
197}
198
200{
201 return QUrl(m_url).resolved(QUrl("/api/tags"_L1));
202}
203
204QString Ollama::makePrompt(const Batch &b) const
205{
206 QStringList lines;
207 lines.reserve(b.items.size() + 32);
208
209 if (!b.userContext.isEmpty())
210 lines << "Application Context: "_L1 + b.userContext;
211
212 lines << "Context: "_L1 + b.context;
213 lines << "Target: "_L1 + b.tgtLang;
214 lines << "Items:"_L1;
215 for (const Item &it : b.items) {
216 QString line = "- source: '%1'"_L1.arg(it.msg->sourceText());
217 if (const QString comment = it.msg->comment(); !comment.isEmpty())
218 line += ", comment: '%1'"_L1.arg(comment);
219 lines << line;
220 }
221
222 return lines.join(QLatin1Char('\n'));
223}
224
225QString Ollama::makeSystemPrompt() const
226{
227 static QString systemPrompt = uR"(
228You are a professional software translator specialized in Qt UI strings.
229
230When given a list of items of the given 'Context', each may include:
231- source: the original text to translate
232- comment: an optional developer note for more context
233
234If "Application Context" is provided, use it to understand the domain and terminology
235appropriate for the application (e.g., medical, financial, gaming) to produce more
236accurate and contextually appropriate translations.
237
238Translate the items into the **target language** specified by the user,
239preserving keyboard accelerators (e.g. "&File"), placeholders (e.g. "%1"),
240and ending punctuation.
241
242RESULT FORMAT (MUST FOLLOW):
243A single JSON object with one key, "Translations",
244whose value is an array of objects.
245Each object maps the original source string to translated string:
246
247Two examples:
248
249Input:
250Context: MainWindow
251Target: German
252Items:
253 - source: "File"
254 - source: "Exit"
255 - source: "&Open", comment: "opens a document"
256
257Output:
258{"Translations":[{"File":"Datei"},{"Exit":"Beenden"},{"&Open":"&Öffnen"}]}
259
260Input:
261Context: MainWindow
262Target: French
263Items:
264– source: "File"
265– source: "Exit"
266Output:
267{"Translations":[{"File":"Fichier"},{"Exit":"Quitter"}]}
268
269Return **only** valid JSON, no code fences, no extra text.
270After generating and before returning, verify:
2711. Every string is in the target language; if any aren't, correct them before returning.
2722. Every JSON key exactly matches one of the input source strings.
2733. No key equals its value.
2744. Every string is translated
275)"_s;
276
277 return systemPrompt;
278}
279
280QT_END_NAMESPACE
Definition lalr.h:84
const TranslatorMessage * msg
QList< Batch > makeBatches(const Messages &messages, const QString &userContext) const override
Definition ollama.cpp:71
QUrl translationEndpoint() const override
Definition ollama.cpp:194
QUrl discoveryEndpoint() const override
Definition ollama.cpp:199
QStringList extractModels(const QByteArray &data) const override
Definition ollama.cpp:137
QHash< QString, QString > extractTranslations(const QByteArray &response) override
Definition ollama.cpp:102
QByteArray payload(const Batch &b) const override
Definition ollama.cpp:151
~Ollama() override
std::optional< QByteArray > stageModel(const QString &modelName) override
Definition ollama.cpp:170
void setUrl(const QString &url) override
Definition ollama.cpp:189