add markdown parsing + latex + tree-sitter highlighting for codeblocks in llm responses
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "llmclient.hpp"
|
||||
#include "message.hpp"
|
||||
#include "segment.hpp"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
@@ -17,6 +18,38 @@
|
||||
|
||||
namespace ZShell::llm {
|
||||
|
||||
namespace {
|
||||
|
||||
QString segmentTypeName(LlmSegment::Type type) {
|
||||
switch (type) {
|
||||
case LlmSegment::Type::Reasoning:
|
||||
return QStringLiteral("reasoning");
|
||||
case LlmSegment::Type::ToolCall:
|
||||
return QStringLiteral("tool_call");
|
||||
case LlmSegment::Type::Content:
|
||||
return QStringLiteral("content");
|
||||
}
|
||||
return QStringLiteral("reasoning");
|
||||
}
|
||||
|
||||
LlmSegment::Type segmentTypeFromName(const QString& name) {
|
||||
if (name == QLatin1String("tool_call"))
|
||||
return LlmSegment::Type::ToolCall;
|
||||
if (name == QLatin1String("content"))
|
||||
return LlmSegment::Type::Content;
|
||||
return LlmSegment::Type::Reasoning;
|
||||
}
|
||||
|
||||
// A null QString binds as SQL NULL, which violates the NOT NULL columns;
|
||||
// DEFAULT only applies to omitted columns, not explicit NULLs.
|
||||
QString sqlText(const QString& value) {
|
||||
if (value.isNull())
|
||||
return QStringLiteral("");
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatStore::ChatStore(QObject* parent)
|
||||
: QObject(parent), m_connectionName(QUuid::createUuid().toString()) {
|
||||
openDb();
|
||||
@@ -86,14 +119,29 @@ void ChatStore::openDb() {
|
||||
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||
" message_id INTEGER NOT NULL REFERENCES messages "
|
||||
"(id) ON DELETE CASCADE,\n"
|
||||
" content TEXT,\n"
|
||||
" reasoning TEXT,\n"
|
||||
" timestamp INTEGER NOT NULL,\n"
|
||||
" reasoning_elapsed_ms INTEGER NOT NULL DEFAULT 0,\n"
|
||||
" content_elapsed_ms INTEGER NOT NULL DEFAULT 0,\n"
|
||||
" is_active INTEGER NOT NULL DEFAULT 1\n"
|
||||
")"));
|
||||
}
|
||||
{
|
||||
QSqlQuery query(db);
|
||||
query.exec(
|
||||
QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS segments (\n"
|
||||
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||
" generation_id INTEGER NOT NULL REFERENCES generations "
|
||||
"(id) ON DELETE CASCADE,\n"
|
||||
" type TEXT NOT NULL,\n"
|
||||
" text TEXT NOT NULL DEFAULT '',\n"
|
||||
" name TEXT NOT NULL DEFAULT '',\n"
|
||||
" tool_call_id TEXT NOT NULL DEFAULT '',\n"
|
||||
" arguments TEXT NOT NULL DEFAULT '',\n"
|
||||
" result TEXT NOT NULL DEFAULT '',\n"
|
||||
" status INTEGER NOT NULL DEFAULT 0,\n"
|
||||
" elapsed_ms INTEGER NOT NULL DEFAULT 0,\n"
|
||||
" timestamp INTEGER NOT NULL\n"
|
||||
")"));
|
||||
}
|
||||
{
|
||||
QSqlQuery query(db);
|
||||
query.exec(QStringLiteral(
|
||||
@@ -102,6 +150,9 @@ void ChatStore::openDb() {
|
||||
query.exec(QStringLiteral(
|
||||
"CREATE INDEX IF NOT EXISTS idx_generations_message "
|
||||
"ON generations (message_id)"));
|
||||
query.exec(QStringLiteral(
|
||||
"CREATE INDEX IF NOT EXISTS idx_segments_generation "
|
||||
"ON segments (generation_id)"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,8 +263,8 @@ void ChatStore::saveMeta(ChatSession* session) {
|
||||
QSqlQuery query(db());
|
||||
query.prepare("UPDATE sessions SET title = :title, icon = :icon "
|
||||
"WHERE id = :id");
|
||||
query.bindValue(":title", session->title());
|
||||
query.bindValue(":icon", session->icon());
|
||||
query.bindValue(":title", sqlText(session->title()));
|
||||
query.bindValue(":icon", sqlText(session->icon()));
|
||||
query.bindValue(":id", session->id());
|
||||
if (!query.exec())
|
||||
qWarning() << "ChatStore: failed to save meta for" << session->id()
|
||||
@@ -234,7 +285,7 @@ bool ChatStore::saveSession(ChatSession* session) {
|
||||
query.prepare(
|
||||
"UPDATE sessions SET title = :title, updated_at = :updated_at "
|
||||
"WHERE id = :id");
|
||||
query.bindValue(":title", session->title());
|
||||
query.bindValue(":title", sqlText(session->title()));
|
||||
query.bindValue(":updated_at", session->updatedAtMs());
|
||||
query.bindValue(":id", session->id());
|
||||
ok = query.exec();
|
||||
@@ -252,10 +303,15 @@ bool ChatStore::saveSession(ChatSession* session) {
|
||||
"VALUES (:id, :role, :timestamp)");
|
||||
QSqlQuery generationInsert(handle);
|
||||
ok = ok && generationInsert.prepare(
|
||||
"INSERT INTO generations (message_id, content, reasoning, "
|
||||
"timestamp, reasoning_elapsed_ms, content_elapsed_ms, "
|
||||
"is_active) VALUES (:mid, :content, :reasoning, :timestamp, "
|
||||
":reasoning_elapsed_ms, :content_elapsed_ms, :is_active)");
|
||||
"INSERT INTO generations (message_id, timestamp, is_active) "
|
||||
"VALUES (:mid, :timestamp, :is_active)");
|
||||
QSqlQuery segmentInsert(handle);
|
||||
ok = ok && segmentInsert.prepare(
|
||||
"INSERT INTO segments (generation_id, type, text, name, "
|
||||
"tool_call_id, arguments, result, status, elapsed_ms, "
|
||||
"timestamp) VALUES (:gid, :type, :text, :name, "
|
||||
":tool_call_id, :arguments, :result, :status, :elapsed_ms, "
|
||||
":timestamp)");
|
||||
// The model holds messages most recent first; the database keeps
|
||||
// natural rowid order, so iterate from the oldest row up.
|
||||
const auto* model = session->messagesModel();
|
||||
@@ -279,14 +335,8 @@ bool ChatStore::saveSession(ChatSession* session) {
|
||||
for (int i = 0; ok && i < message->generationCount(); ++i) {
|
||||
const auto* generation = message->generation(i);
|
||||
generationInsert.bindValue(":mid", messageId);
|
||||
generationInsert.bindValue(":content", generation->content());
|
||||
generationInsert.bindValue(
|
||||
":reasoning", generation->reasoning());
|
||||
generationInsert.bindValue(":timestamp", generation->timestamp());
|
||||
generationInsert.bindValue(
|
||||
":reasoning_elapsed_ms", generation->reasoningElapsedMs());
|
||||
generationInsert.bindValue(
|
||||
":content_elapsed_ms", generation->contentElapsedMs());
|
||||
":timestamp", generation->timestamp());
|
||||
generationInsert.bindValue(
|
||||
":is_active",
|
||||
i == message->activeGenerationIndex() ? 1 : 0);
|
||||
@@ -297,6 +347,33 @@ bool ChatStore::saveSession(ChatSession* session) {
|
||||
<< generationInsert.lastError().text();
|
||||
break;
|
||||
}
|
||||
const int generationId =
|
||||
generationInsert.lastInsertId().toInt();
|
||||
for (const auto* segment : generation->segments()) {
|
||||
segmentInsert.bindValue(":gid", generationId);
|
||||
segmentInsert.bindValue(
|
||||
":type", segmentTypeName(segment->type()));
|
||||
segmentInsert.bindValue(":text", sqlText(segment->text()));
|
||||
segmentInsert.bindValue(":name", sqlText(segment->name()));
|
||||
segmentInsert.bindValue(
|
||||
":tool_call_id", sqlText(segment->toolCallId()));
|
||||
segmentInsert.bindValue(
|
||||
":arguments", sqlText(segment->arguments()));
|
||||
segmentInsert.bindValue(":result", sqlText(segment->result()));
|
||||
segmentInsert.bindValue(
|
||||
":status", static_cast<int>(segment->status()));
|
||||
segmentInsert.bindValue(
|
||||
":elapsed_ms", segment->elapsedMs());
|
||||
segmentInsert.bindValue(
|
||||
":timestamp", segment->timestamp());
|
||||
if (!segmentInsert.exec()) {
|
||||
ok = false;
|
||||
qWarning() << "ChatStore: saveSession" << id
|
||||
<< "segment insert failed:"
|
||||
<< segmentInsert.lastError().text();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,21 +410,54 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
query.value(2).toLongLong());
|
||||
QSqlQuery generationQuery(db());
|
||||
generationQuery.prepare(
|
||||
"SELECT content, reasoning, timestamp, reasoning_elapsed_ms, "
|
||||
"content_elapsed_ms, is_active FROM generations "
|
||||
"SELECT id, timestamp, is_active FROM generations "
|
||||
"WHERE message_id = :mid ORDER BY rowid");
|
||||
generationQuery.bindValue(":mid", messageId);
|
||||
int activeIndex = 0;
|
||||
if (generationQuery.exec()) {
|
||||
int index = 0;
|
||||
while (generationQuery.next()) {
|
||||
message->addGeneration(
|
||||
generationQuery.value(2).toLongLong(),
|
||||
generationQuery.value(0).toString(),
|
||||
generationQuery.value(1).toString(),
|
||||
generationQuery.value(3).toLongLong(),
|
||||
generationQuery.value(4).toLongLong());
|
||||
if (generationQuery.value(5).toInt() != 0)
|
||||
auto* generation = message->addGeneration(
|
||||
generationQuery.value(1).toLongLong());
|
||||
QSqlQuery segmentQuery(db());
|
||||
segmentQuery.prepare(
|
||||
"SELECT type, text, name, tool_call_id, arguments, "
|
||||
"result, status, elapsed_ms, timestamp FROM segments "
|
||||
"WHERE generation_id = :gid ORDER BY rowid");
|
||||
segmentQuery.bindValue(
|
||||
":gid", generationQuery.value(0).toInt());
|
||||
if (segmentQuery.exec()) {
|
||||
while (segmentQuery.next()) {
|
||||
auto* segment = new LlmSegment(
|
||||
segmentTypeFromName(
|
||||
segmentQuery.value(0).toString()),
|
||||
segmentQuery.value(8).toLongLong(),
|
||||
generation);
|
||||
segment->setText(
|
||||
segmentQuery.value(1).toString());
|
||||
segment->setName(
|
||||
segmentQuery.value(2).toString());
|
||||
segment->setToolCallId(
|
||||
segmentQuery.value(3).toString());
|
||||
segment->appendArguments(
|
||||
segmentQuery.value(4).toString());
|
||||
segment->setResult(
|
||||
segmentQuery.value(5).toString());
|
||||
segment->setStatus(
|
||||
static_cast<LlmSegment::Status>(
|
||||
segmentQuery.value(6).toInt()));
|
||||
segment->restore(
|
||||
segmentQuery.value(7).toLongLong());
|
||||
generation->addSegment(segment);
|
||||
}
|
||||
} else {
|
||||
qWarning() << "ChatStore: failed to load segments for "
|
||||
<< "generation"
|
||||
<< generationQuery.value(0).toInt()
|
||||
<< ":"
|
||||
<< segmentQuery.lastError().text();
|
||||
}
|
||||
if (generationQuery.value(2).toInt() != 0)
|
||||
activeIndex = index;
|
||||
++index;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user