Skip to content

Commit 92d42a3

Browse files
committed
Extract model‑specific thinking section parsing
* Added `ThinkingSectionParser` with token detection, extraction, and formatting. * Updated `ChatManager`, `ChatMessage`, and `ConversationsView` to use the new parser. * Removed in‑lining token constants and parsing logic from those classes. * Adjusted UI logic to toggle between raw and formatted thinking content.
1 parent 29785d1 commit 92d42a3

File tree

6 files changed

+105
-51
lines changed

6 files changed

+105
-51
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ add_qtc_plugin(LlamaCpp
6767
llamaprojectpanel.cpp llamaprojectpanel.h
6868
llamasettings.cpp llamasettings.h
6969
llamastorage.cpp llamastorage.h
70+
llamathinkingsectionparser.cpp llamathinkingsectionparser.h
7071
llamatheme.cpp llamatheme.h
7172
llamatr.h
7273
llamatypes.h

llamachatmanager.cpp

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "llamachatmanager.h"
77
#include "llamasettings.h"
88
#include "llamastorage.h"
9+
#include "llamathinkingsectionparser.h"
910

1011
Q_LOGGING_CATEGORY(llamaChatNetwork, "llama.cpp.chat.network", QtWarningMsg)
1112

@@ -96,6 +97,7 @@ void ChatManager::initServerProps()
9697
m_serverProps.modalities.audio = mod.value("audio").toBool();
9798
reply->deleteLater();
9899

100+
ThinkingSectionParser::setTokensFromServerProps(m_serverProps);
99101
emit serverPropsUpdated();
100102
});
101103
}
@@ -292,12 +294,8 @@ void ChatManager::generateMessage(const QString &convId,
292294
// (root + user + assistant) after the first reply.
293295
if (msgs.size() == 3) {
294296
summarizeConversationTitle(convId, pm.id, [this, convId](const QString &title) {
295-
QString shortTitle = title;
296-
const QString endToken = "<|end|>";
297-
auto endIdx = title.indexOf(endToken);
298-
if (endIdx != -1) {
299-
shortTitle = title.mid(endIdx + endToken.size());
300-
}
297+
auto [thinking, shortTitle] = ThinkingSectionParser::parseThinkingSection(
298+
title);
301299
renameConversation(convId, shortTitle);
302300
});
303301
}
@@ -369,25 +367,31 @@ void ChatManager::followUpQuestions(const QString &convId,
369367

370368
QJsonObject choice = choices[0].toObject();
371369
QJsonObject message = choice.value("message").toObject();
372-
QString content = message.value("content").toString().trimmed();
373370

374371
// Skip the thinking part
375-
const QString endToken = "<|end|>";
376-
auto endIdx = content.lastIndexOf(endToken);
377-
if (endIdx != -1) {
378-
content = content.mid(endIdx + endToken.size());
379-
}
372+
auto [thinking, content] = ThinkingSectionParser::parseThinkingSection(
373+
message.value("content").toString().trimmed());
380374

381375
if (content.isEmpty())
382376
return;
383377

384378
// Sometimes the model continues "thinking" also in the answer
385-
if (!content.startsWith("[\"")) {
386-
auto startOfArrayIdx = content.lastIndexOf("[\"");
379+
const QString startOfArray("[\"");
380+
const QString endOfArray("\"]");
381+
382+
if (!content.startsWith(startOfArray)) {
383+
auto startOfArrayIdx = content.lastIndexOf(startOfArray);
387384
if (startOfArrayIdx != -1)
388385
content = content.mid(startOfArrayIdx);
389386
}
390387

388+
// Sometimes we have \n``` at the end
389+
if (!content.endsWith(endOfArray)) {
390+
auto endOfArrayIdx = content.lastIndexOf(endOfArray);
391+
if (endOfArrayIdx != -1)
392+
content = content.left(endOfArrayIdx + endOfArray.size());
393+
}
394+
391395
// `content` should be a JSON array of strings (plain text).
392396
QJsonParseError err;
393397
QJsonDocument arrDoc = QJsonDocument::fromJson(content.toUtf8(), &err);

llamachatmessage.cpp

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "llamachatmessage.h"
3030
#include "llamamarkdownwidget.h"
3131
#include "llamatheme.h"
32+
#include "llamathinkingsectionparser.h"
3233
#include "llamatr.h"
3334

3435
using namespace Core;
@@ -37,10 +38,6 @@ using namespace Utils;
3738

3839
namespace LlamaCpp {
3940

40-
static const QString thinkingToken("<|channel|>analysis<|message|>");
41-
static const QString endToken("<|end|>");
42-
static qsizetype notfound = -1;
43-
4441
ChatMessage::ChatMessage(const Message &msg,
4542
const QVector<qint64> &siblingLeafIds,
4643
int siblingIdx,
@@ -68,7 +65,7 @@ void ChatMessage::buildUI()
6865
connect(m_markdownLabel, &MarkdownLabel::copyToClipboard, this, &ChatMessage::onCopyToClipboard);
6966
connect(m_markdownLabel, &MarkdownLabel::saveToFile, this, &ChatMessage::onSaveToDisk);
7067

71-
if (m_msg.content.indexOf(thinkingToken) != notfound) {
68+
if (ThinkingSectionParser::hasThinkingSection(m_msg.content)) {
7269
m_thoughtToggle = new QPushButton(this);
7370
m_thoughtToggle->setText(Tr::tr("Thought Process"));
7471
m_thoughtToggle->setToolTip(Tr::tr("Click to expand / hide the thought process"));
@@ -258,39 +255,21 @@ bool ChatMessage::eventFilter(QObject *obj, QEvent *event)
258255
void ChatMessage::renderMarkdown(const QString &text)
259256
{
260257
if (m_thoughtToggle) {
261-
bool isThinking = false;
258+
auto [thinking, message] = ThinkingSectionParser::parseThinkingSection(text);
262259
if (m_thoughtToggle->isChecked()) {
263-
QString message = text;
264-
message.replace(thinkingToken, ">");
265-
auto endIdx = message.indexOf(endToken);
266-
if (endIdx != notfound) {
267-
auto newLineIdx = message.indexOf("\n");
268-
while (newLineIdx < endIdx && newLineIdx != notfound) {
269-
message.insert(newLineIdx + 1, ">");
270-
newLineIdx = message.indexOf("\n", newLineIdx + 2);
271-
}
272-
} else {
273-
isThinking = true;
274-
message.replace("\n", "\n>");
275-
}
276-
message.replace(endToken, "\n\n");
277-
m_markdownLabel->setMarkdown(message);
260+
m_markdownLabel->setMarkdown(
261+
ThinkingSectionParser::formatThinkingContent(thinking) + "\n\n" + message);
278262
} else {
279-
auto endIdx = text.indexOf(endToken);
280-
if (endIdx != notfound) {
281-
m_markdownLabel->setMarkdown(text.mid(endIdx + endToken.size()));
282-
} else {
283-
isThinking = true;
284-
m_markdownLabel->setMarkdown("");
285-
}
263+
m_markdownLabel->setMarkdown(message);
286264
}
287265

288266
static QVector<QChar> chars{u'', u'', u'', u'', u'', u'', u'', u'', u'', u''};
289267
// Dividing with 33ms results in 30fps
290268
m_thoughtToggle->setText(
291-
isThinking ? Tr::tr("Thinking %1")
292-
.arg(chars[(QDateTime::currentMSecsSinceEpoch() / 33) % chars.size()])
293-
: Tr::tr("Thought Process"));
269+
message.isEmpty()
270+
? Tr::tr("Thinking %1")
271+
.arg(chars[(QDateTime::currentMSecsSinceEpoch() / 33) % chars.size()])
272+
: Tr::tr("Thought Process"));
294273

295274
} else {
296275
m_markdownLabel->setMarkdown(text);

llamaconversationsview.cpp

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "llamaconstants.h"
2727
#include "llamaconversationsmodel.h"
2828
#include "llamaconversationsview.h"
29+
#include "llamathinkingsectionparser.h"
2930
#include "llamatr.h"
3031

3132
using namespace Core;
@@ -299,12 +300,7 @@ bool ConversationsView::summarizeConversation()
299300

300301
ChatManager::instance()
301302
.summarizeConversationTitle(convId, chat.messages.last().id, [convId](const QString &title) {
302-
QString shortTitle = title;
303-
const QString endToken = "<|end|>";
304-
auto endIdx = title.indexOf(endToken);
305-
if (endIdx != -1) {
306-
shortTitle = title.mid(endIdx + endToken.size());
307-
}
303+
auto [thinking, shortTitle] = ThinkingSectionParser::parseThinkingSection(title);
308304
ChatManager::instance().renameConversation(convId, shortTitle);
309305
});
310306

llamathinkingsectionparser.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include "llamathinkingsectionparser.h"
2+
#include "llamatypes.h"
3+
4+
namespace LlamaCpp {
5+
QString ThinkingSectionParser::m_startToken;
6+
QString ThinkingSectionParser::m_endToken;
7+
8+
void ThinkingSectionParser::setTokensFromServerProps(const LlamaCppServerProps &serverProps)
9+
{
10+
if (serverProps.model_path.contains("gpt-oss", Qt::CaseInsensitive)) {
11+
m_startToken = "<|channel|>analysis<|message|>";
12+
m_endToken = "<|end|>";
13+
} else {
14+
// Tested with DeepSeek.
15+
m_startToken = "<think>";
16+
m_endToken = "</think>";
17+
}
18+
}
19+
20+
QPair<QString, QString> ThinkingSectionParser::parseThinkingSection(const QString &text)
21+
{
22+
int startIdx = text.indexOf(m_startToken);
23+
int endIdx = text.indexOf(m_endToken);
24+
25+
if (startIdx != -1 && endIdx != -1 && startIdx + m_startToken.length() < endIdx) {
26+
QString thinkingContent = text.mid(startIdx + m_startToken.length(),
27+
endIdx - startIdx - m_startToken.length());
28+
QString restContent = text.mid(endIdx + m_endToken.length());
29+
return {thinkingContent, restContent};
30+
} else if (startIdx != -1 && endIdx == -1) {
31+
QString thinkingContent = text.mid(startIdx + m_startToken.length());
32+
return {thinkingContent, {}};
33+
}
34+
35+
return {{}, text};
36+
}
37+
38+
bool ThinkingSectionParser::hasThinkingSection(const QString &text)
39+
{
40+
int startIdx = text.indexOf(m_startToken);
41+
int endIdx = text.indexOf(m_endToken);
42+
return (startIdx != -1 && endIdx != -1 && startIdx < endIdx)
43+
|| (startIdx != -1 && endIdx == -1);
44+
}
45+
46+
QString ThinkingSectionParser::formatThinkingContent(const QString &thinkingContent)
47+
{
48+
QString formatted = thinkingContent;
49+
formatted.replace("\n", "\n>");
50+
return ">" + formatted;
51+
}
52+
} // namespace LlamaCpp

llamathinkingsectionparser.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
#include <QString>
4+
5+
namespace LlamaCpp {
6+
7+
class LlamaCppServerProps;
8+
9+
class ThinkingSectionParser
10+
{
11+
private:
12+
static QString m_startToken;
13+
static QString m_endToken;
14+
15+
public:
16+
static void setTokensFromServerProps(const LlamaCppServerProps &serverProps);
17+
18+
static QPair<QString, QString> parseThinkingSection(const QString &text);
19+
static bool hasThinkingSection(const QString &text);
20+
static QString formatThinkingContent(const QString &thinkingContent);
21+
};
22+
} // namespace LlamaCpp

0 commit comments

Comments
 (0)