add removal of chat sessions, auto title + icon, proper stick-to-bottom as response is streamed
This commit is contained in:
+165
-1
@@ -140,8 +140,15 @@ void Chat::send(const QString& chatId, const QString& content) {
|
||||
QDateTime::currentMSecsSinceEpoch());
|
||||
while (session->messageCount() > 200)
|
||||
session->removeMessage(session->messages().first());
|
||||
if (session->title().isEmpty())
|
||||
if (session->title().isEmpty()) {
|
||||
session->setTitle(titleFrom(text));
|
||||
qInfo() << "Chat: new session" << chatId
|
||||
<< "fallback title" << session->title()
|
||||
<< "- requesting generated title and icon, model" << m_model
|
||||
<< "endpoint" << m_endpoint;
|
||||
requestTitle(chatId, text);
|
||||
requestIcon(chatId, text);
|
||||
}
|
||||
if (m_contextSize > 0 && m_lastTokenCount > m_contextSize * 4 / 5)
|
||||
trimHistory(session, m_contextSize);
|
||||
|
||||
@@ -226,6 +233,162 @@ void Chat::beginAssistant() {
|
||||
});
|
||||
}
|
||||
|
||||
void Chat::shortRequest(
|
||||
const QString& tag,
|
||||
const QString& systemPrompt,
|
||||
const QString& userText,
|
||||
std::function<void(QString result)> onResult) {
|
||||
const QUrl url =
|
||||
QUrl::fromUserInput(completionsPath(m_endpoint, "/chat/completions"));
|
||||
if (!url.isValid() || url.host().isEmpty())
|
||||
return;
|
||||
|
||||
qInfo() << "Chat:" << tag << "request POST" << url.toString()
|
||||
<< "model=" << m_model;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
|
||||
QJsonArray messages;
|
||||
QJsonObject system;
|
||||
system[QStringLiteral("role")] = QStringLiteral("system");
|
||||
system[QStringLiteral("content")] = systemPrompt;
|
||||
messages.append(system);
|
||||
QJsonObject user;
|
||||
user[QStringLiteral("role")] = QStringLiteral("user");
|
||||
user[QStringLiteral("content")] = userText.simplified().mid(0, 512);
|
||||
messages.append(user);
|
||||
|
||||
QJsonObject body;
|
||||
if (!m_model.isEmpty())
|
||||
body[QStringLiteral("model")] = m_model;
|
||||
body[QStringLiteral("stream")] = false;
|
||||
body[QStringLiteral("temperature")] = 0.3;
|
||||
body[QStringLiteral("max_tokens")] = 128;
|
||||
QJsonObject templateKwargs;
|
||||
templateKwargs[QStringLiteral("enable_thinking")] = false;
|
||||
body[QStringLiteral("chat_template_kwargs")] = templateKwargs;
|
||||
body[QStringLiteral("messages")] = messages;
|
||||
|
||||
auto* reply = m_manager.post(request, QJsonDocument(body).toJson());
|
||||
connect(
|
||||
reply,
|
||||
&QNetworkReply::finished,
|
||||
this,
|
||||
[this, reply, tag, onResult = std::move(onResult)]() {
|
||||
const QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
qInfo() << "Chat:" << tag << "request finished"
|
||||
<< "error=" << reply->error() << reply->errorString()
|
||||
<< "http=" << reply->attribute(
|
||||
QNetworkRequest::HttpStatusCodeAttribute).toInt()
|
||||
<< "response="
|
||||
<< QString::fromUtf8(data.left(400)).simplified();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
return;
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
const QJsonArray choices =
|
||||
doc.object()[QStringLiteral("choices")].toArray();
|
||||
if (choices.isEmpty())
|
||||
return;
|
||||
const QString result =
|
||||
choices.at(0).toObject()[QStringLiteral("message")].toObject()
|
||||
[QStringLiteral("content")].toString()
|
||||
.trimmed();
|
||||
qInfo() << "Chat:" << tag << "raw result" << result;
|
||||
onResult(result);
|
||||
});
|
||||
}
|
||||
|
||||
void Chat::requestTitle(const QString& chatId, const QString& userText) {
|
||||
shortRequest(
|
||||
QStringLiteral("title"),
|
||||
QStringLiteral(
|
||||
"Write a short, concise title for a chat conversation starting "
|
||||
"with the user's message below. At most six words, no quotation "
|
||||
"marks, no trailing punctuation. Reply with the title only."),
|
||||
userText,
|
||||
[this, chatId](QString title) {
|
||||
ChatSession* session = m_store->sessionById(chatId);
|
||||
if (!session) {
|
||||
qWarning() << "Chat: title request: session gone" << chatId;
|
||||
return;
|
||||
}
|
||||
const auto isQuote = [](QChar c) {
|
||||
return c == QLatin1Char('"') || c == QLatin1Char('\'') ||
|
||||
c == QChar(u'\u201C') || c == QChar(u'\u201D') ||
|
||||
c == QChar(u'\u2018') || c == QChar(u'\u2019');
|
||||
};
|
||||
while (title.size() >= 2 && isQuote(title.at(0)) &&
|
||||
isQuote(title.at(title.size() - 1)))
|
||||
title = title.mid(1, title.size() - 2).simplified();
|
||||
while (!title.isEmpty() &&
|
||||
(title.endsWith(QLatin1Char('.')) ||
|
||||
title.endsWith(QLatin1Char('!')) ||
|
||||
title.endsWith(QLatin1Char('?'))))
|
||||
title.chop(1);
|
||||
if (title.size() < 2) {
|
||||
qWarning() << "Chat: title rejected (too short)" << chatId
|
||||
<< title;
|
||||
return;
|
||||
}
|
||||
if (title.size() > 48)
|
||||
title = title.left(47) + QStringLiteral("…");
|
||||
qInfo() << "Chat: applying generated title" << chatId << title
|
||||
<< "(was" << session->title() << ")";
|
||||
session->setTitle(title);
|
||||
m_store->saveMeta(session);
|
||||
});
|
||||
}
|
||||
|
||||
void Chat::requestIcon(const QString& chatId, const QString& userText) {
|
||||
const QStringList icons = {
|
||||
"chat", "lightbulb", "code",
|
||||
"description", "article", "school",
|
||||
"work", "build", "science",
|
||||
"palette", "music_note", "sports_esports",
|
||||
"takeout_dining", "flight", "photo_camera",
|
||||
"psychology_alt", "favorite", "savings",
|
||||
"gamepad", "auto_awesome",
|
||||
};
|
||||
const QString prompt =
|
||||
QStringLiteral(
|
||||
"Pick the single icon name from this list that best matches the "
|
||||
"topic of the user's message below: %1. Reply with only the icon "
|
||||
"name, exactly as written in the list, and nothing else.")
|
||||
.arg(icons.join(QStringLiteral(", ")));
|
||||
shortRequest(
|
||||
QStringLiteral("icon"),
|
||||
prompt,
|
||||
userText,
|
||||
[this, chatId, icons](QString name) {
|
||||
ChatSession* session = m_store->sessionById(chatId);
|
||||
if (!session) {
|
||||
qWarning() << "Chat: icon request: session gone" << chatId;
|
||||
return;
|
||||
}
|
||||
name = name.simplified().toLower();
|
||||
while (name.size() >= 2 &&
|
||||
(name.at(0) == QLatin1Char('"') ||
|
||||
name.at(0) == QLatin1Char('\'')))
|
||||
name = name.mid(1).left(name.size() - 2).simplified();
|
||||
name.replace(QLatin1Char(' '), QLatin1Char('_'));
|
||||
if (!icons.contains(name)) {
|
||||
qWarning() << "Chat: icon not in list, using default" << chatId
|
||||
<< name;
|
||||
name = QStringLiteral("chat");
|
||||
}
|
||||
qInfo() << "Chat: applying generated icon" << chatId << name
|
||||
<< "(was" << session->icon() << ")";
|
||||
session->setIcon(name);
|
||||
m_store->saveMeta(session);
|
||||
});
|
||||
}
|
||||
|
||||
void Chat::stop() {
|
||||
if (!m_busy)
|
||||
return;
|
||||
@@ -275,6 +438,7 @@ void Chat::endStream() {
|
||||
if (m_streaming->content().isEmpty() && m_streaming->reasoning().isEmpty() && m_active)
|
||||
m_active->removeMessage(m_streaming);
|
||||
m_streaming = nullptr;
|
||||
m_active = nullptr;
|
||||
setBusy(this, false);
|
||||
setStreamingChatId(this, QString());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user