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 = false;
108 return {};
109 }
110
111 auto translations = recursiveFind(doc.object(), "Translations"_L1);
112 QHash<QString, QString> out;
113 if (!translations) {
114 m_useJsonFormat = false;
115 return out;
116 }
117
118 out.reserve(translations->size());
119 for (const QJsonValue &v : std::as_const(*translations)) {
120 if (v.isObject()) {
121 const QJsonObject obj = v.toObject();
122 const QString key = obj.keys().first();
123 if (QJsonValue val = obj.value(key); val.isString())
124 out[key] = val.toString();
125 }
126 }
127 return out;
128}
129
130QStringList Ollama::extractModels(const QByteArray &response) const
131{
132 QJsonParseError err;
133 QJsonDocument doc = QJsonDocument::fromJson(response, &err);
134 if (err.error != QJsonParseError::NoError)
135 return {};
136 const QJsonObject obj = doc.object();
137 const QJsonArray arr = obj.value("models"_L1).toArray();
138 QStringList models;
139 for (const QJsonValue &v : arr)
140 models.append(v.toObject().value("name"_L1).toString());
141 return models;
142}
143
145{
146 QJsonObject userMessage;
147 userMessage.insert("role"_L1, "user"_L1);
148 userMessage.insert("content"_L1, makePrompt(b));
149
150 QJsonArray messages;
151 messages.append(*m_systemMessage);
152 messages.append(userMessage);
153
154 QJsonObject req = *m_payloadBase;
155 req.insert("messages"_L1, messages);
156
157 if (m_useJsonFormat)
158 req.insert("format"_L1, "json"_L1);
159
160 return QJsonDocument(req).toJson();
161}
162
163std::optional<QByteArray> Ollama::stageModel(const QString &modelName)
164{
165 if (auto m = m_payloadBase->constFind("model"_L1);
166 m == m_payloadBase->constEnd() || *m != modelName) {
167 m_useJsonFormat = true;
168 m_payloadBase->insert("model"_L1, modelName);
169 }
170
171 std::optional<QByteArray> res;
172 if (!m_lastWakeupTimer.isValid() || m_lastWakeupTimer.hasExpired(s_wakeUpTimeOut)) {
173 m_lastWakeupTimer.start();
174 QJsonObject wakeup;
175 wakeup.insert("model"_L1, modelName);
176 res.emplace(QJsonDocument(wakeup).toJson());
177 }
178
179 return res;
180}
181
182void Ollama::setUrl(const QString &url)
183{
184 m_url = url;
185}
186
188{
189 return QUrl(m_url).resolved(QUrl("/api/chat"_L1));
190}
191
193{
194 return QUrl(m_url).resolved(QUrl("/api/tags"_L1));
195}
196
197QString Ollama::makePrompt(const Batch &b) const
198{
199 QStringList lines;
200 lines.reserve(b.items.size() + 32);
201
202 if (!b.userContext.isEmpty())
203 lines << "Application Context: "_L1 + b.userContext;
204
205 lines << "Context: "_L1 + b.context;
206 lines << "Target: "_L1 + b.tgtLang;
207 lines << "Items:"_L1;
208 for (const Item &it : b.items) {
209 QString line = "- source: '%1'"_L1.arg(it.msg->sourceText());
210 if (const QString comment = it.msg->comment(); !comment.isEmpty())
211 line += ", comment: '%1'"_L1.arg(comment);
212 lines << line;
213 }
214
215 return lines.join(QLatin1Char('\n'));
216}
217
218QString Ollama::makeSystemPrompt() const
219{
220 static QString systemPrompt = uR"(
221You are a professional software translator specialized in Qt UI strings.
222
223When given a list of items of the given 'Context', each may include:
224- source: the original text to translate
225- comment: an optional developer note for more context
226
227If "Application Context" is provided, use it to understand the domain and terminology
228appropriate for the application (e.g., medical, financial, gaming) to produce more
229accurate and contextually appropriate translations.
230
231Translate the items into the **target language** specified by the user,
232preserving keyboard accelerators (e.g. "&File") and placeholders (e.g. "%1").
233
234RESULT FORMAT (MUST FOLLOW):
235A single JSON object with one key, "Translations",
236whose value is an array of objects.
237Each object maps the original source string to translated string:
238
239Two examples:
240
241Input:
242Context: MainWindow
243Target: German
244Items:
245 - source: "File"
246 - source: "Exit"
247 - source: "&Open", comment: "opens a document"
248
249Output:
250{"Translations":[{"File":"Datei"},{"Exit":"Beenden"},{"&Open":"&Öffnen"}]}
251
252Input:
253Context: MainWindow
254Target: French
255Items:
256– source: "File"
257– source: "Exit"
258Output:
259{"Translations":[{"File":"Fichier"},{"Exit":"Quitter"}]}
260
261Return **only** valid JSON, no code fences, no extra text.
262After generating and before returning, verify:
2631. Every string is in the target language; if any aren’t, correct them before returning.
2642. Every JSON key exactly matches one of the input source strings.
2653. No key equals its value.
2664. Every string is translated
267)"_s;
268
269 return systemPrompt;
270}
271
272QT_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:187
QUrl discoveryEndpoint() const override
Definition ollama.cpp:192
QStringList extractModels(const QByteArray &data) const override
Definition ollama.cpp:130
QHash< QString, QString > extractTranslations(const QByteArray &response) override
Definition ollama.cpp:102
QByteArray payload(const Batch &b) const override
Definition ollama.cpp:144
~Ollama() override
std::optional< QByteArray > stageModel(const QString &modelName) override
Definition ollama.cpp:163
void setUrl(const QString &url) override
Definition ollama.cpp:182