C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 2m18s
C++ / clang-tidy (pull_request) Failing after 2m7s
JS/TS / fmt (pull_request) Failing after 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m38s
Rust / fmt (pull_request) Successful in 26s
Rust / clippy (pull_request) Successful in 47s
264 lines
7.2 KiB
C++
264 lines
7.2 KiB
C++
#include "generation.hpp"
|
|
|
|
#include "llmclient.hpp"
|
|
#include "messagemodel.hpp"
|
|
#include "session.hpp"
|
|
|
|
#include <QDateTime>
|
|
|
|
namespace ZShell::llm {
|
|
|
|
ChatGeneration::ChatGeneration(qint64 timestamp, QObject* parent)
|
|
: QObject(parent), m_timestamp(timestamp) {
|
|
m_timer.setParent(this);
|
|
m_timer.setInterval(500);
|
|
m_timer.setTimerType(Qt::CoarseTimer);
|
|
connect(&m_timer, &QTimer::timeout, this, [this]() {
|
|
bool anyRunning = false;
|
|
for (auto* segment : m_segments) {
|
|
if (!segment->running()) continue;
|
|
anyRunning = true;
|
|
segment->elapsedMsChanged();
|
|
}
|
|
if (anyRunning) Q_EMIT elapsedMsChanged();
|
|
if (!anyRunning && !m_streaming) m_timer.stop();
|
|
});
|
|
}
|
|
|
|
QString ChatGeneration::content() const {
|
|
QStringList parts;
|
|
for (const auto* segment : m_segments) {
|
|
if (segment->type() != LlmSegment::Type::Content ||
|
|
segment->text().isEmpty())
|
|
continue;
|
|
parts.append(segment->text());
|
|
}
|
|
return parts.join(QStringLiteral("\n\n"));
|
|
}
|
|
|
|
QString ChatGeneration::reasoning() const {
|
|
QStringList parts;
|
|
for (const auto* segment : m_segments) {
|
|
if (segment->type() != LlmSegment::Type::Reasoning ||
|
|
segment->text().isEmpty())
|
|
continue;
|
|
parts.append(segment->text());
|
|
}
|
|
return parts.join(QStringLiteral("\n\n"));
|
|
}
|
|
|
|
bool ChatGeneration::reasoningActive() const {
|
|
if (!m_streaming) return false;
|
|
if (!content().isEmpty()) return false;
|
|
return !hasRunningTool() && !hasPendingTool();
|
|
}
|
|
|
|
qint64 ChatGeneration::reasoningElapsedMs() const {
|
|
qint64 total = 0;
|
|
for (const auto* segment : m_segments)
|
|
if (segment->type() == LlmSegment::Type::Reasoning)
|
|
total += segment->elapsedMs();
|
|
return total;
|
|
}
|
|
|
|
qint64 ChatGeneration::contentElapsedMs() const {
|
|
qint64 total = 0;
|
|
for (const auto* segment : m_segments)
|
|
if (segment->type() == LlmSegment::Type::Content)
|
|
total += segment->elapsedMs();
|
|
return total;
|
|
}
|
|
|
|
qint64 ChatGeneration::toolsElapsedMs() const {
|
|
qint64 total = 0;
|
|
for (const auto* segment : m_segments)
|
|
if (segment->type() == LlmSegment::Type::ToolCall)
|
|
total += segment->elapsedMs();
|
|
return total;
|
|
}
|
|
|
|
int ChatGeneration::toolCallCount() const {
|
|
int count = 0;
|
|
for (const auto* segment : m_segments)
|
|
if (segment->type() == LlmSegment::Type::ToolCall) ++count;
|
|
return count;
|
|
}
|
|
|
|
bool ChatGeneration::hasRunningTool() const {
|
|
for (const auto* segment : m_segments)
|
|
if (segment->type() == LlmSegment::Type::ToolCall && segment->running())
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool ChatGeneration::hasPendingTool() const {
|
|
for (const auto* segment : m_segments)
|
|
if (segment->type() == LlmSegment::Type::ToolCall &&
|
|
segment->status() == LlmSegment::Status::Pending)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void ChatGeneration::setApprovalPending(bool value) {
|
|
if (m_approvalPending == value) return;
|
|
m_approvalPending = value;
|
|
Q_EMIT toolApprovalPendingChanged();
|
|
}
|
|
|
|
QVariantList ChatGeneration::pendingToolCalls() const {
|
|
QVariantList out;
|
|
if (!m_approvalPending) return out;
|
|
for (const auto* segment : m_segments)
|
|
if (segment->type() == LlmSegment::Type::ToolCall &&
|
|
segment->status() == LlmSegment::Status::Pending)
|
|
out.append(QVariant::fromValue(segment));
|
|
return out;
|
|
}
|
|
|
|
void ChatGeneration::approveTools() {
|
|
if (auto* llmClient = client()) llmClient->approveTools();
|
|
}
|
|
|
|
void ChatGeneration::denyTools() {
|
|
if (auto* llmClient = client()) llmClient->denyTools();
|
|
}
|
|
|
|
LlmClient* ChatGeneration::client() const {
|
|
if (auto* message = qobject_cast<ChatMessage*>(parent()))
|
|
if (auto* model = qobject_cast<ChatMessageModel*>(message->parent()))
|
|
if (auto* session = model->session()) return session->client();
|
|
return nullptr;
|
|
}
|
|
|
|
void ChatGeneration::updateReasoningActive() {
|
|
const bool active = reasoningActive();
|
|
if (m_reasoningActive == active) return;
|
|
m_reasoningActive = active;
|
|
Q_EMIT reasoningActiveChanged();
|
|
}
|
|
|
|
void ChatGeneration::setContent(const QString& value) {
|
|
LlmSegment* first = nullptr;
|
|
for (auto* segment : m_segments) {
|
|
if (segment->type() != LlmSegment::Type::Content) continue;
|
|
if (!first)
|
|
first = segment;
|
|
else
|
|
segment->setText(QString());
|
|
}
|
|
if (!first) {
|
|
if (value.isEmpty()) return;
|
|
first = new LlmSegment(
|
|
LlmSegment::Type::Content,
|
|
QDateTime::currentMSecsSinceEpoch(),
|
|
this);
|
|
addSegment(first);
|
|
}
|
|
first->setText(value);
|
|
}
|
|
|
|
void ChatGeneration::appendContent(const QString& piece) {
|
|
if (piece.isEmpty()) return;
|
|
for (auto* segment : m_segments) {
|
|
if (segment->type() == LlmSegment::Type::Reasoning &&
|
|
segment->running())
|
|
segment->close();
|
|
}
|
|
openContentSegment()->appendText(piece);
|
|
}
|
|
|
|
void ChatGeneration::appendReasoning(const QString& piece) {
|
|
if (piece.isEmpty()) return;
|
|
for (auto* segment : m_segments) {
|
|
if (segment->type() == LlmSegment::Type::Content && segment->running())
|
|
segment->close();
|
|
}
|
|
openReasoningSegment()->appendText(piece);
|
|
}
|
|
|
|
void ChatGeneration::setStreaming(bool value) {
|
|
if (m_streaming == value) return;
|
|
m_streaming = value;
|
|
Q_EMIT streamingChanged();
|
|
if (value) {
|
|
if (!m_timer.isActive()) m_timer.start();
|
|
} else {
|
|
closeOpenSegments();
|
|
m_timer.stop();
|
|
}
|
|
Q_EMIT elapsedMsChanged();
|
|
updateReasoningActive();
|
|
}
|
|
|
|
LlmSegment* ChatGeneration::openContentSegment() {
|
|
for (auto* segment : m_segments)
|
|
if (segment->type() == LlmSegment::Type::Content && segment->running())
|
|
return segment;
|
|
auto* segment = new LlmSegment(
|
|
LlmSegment::Type::Content, QDateTime::currentMSecsSinceEpoch(), this);
|
|
segment->begin();
|
|
addSegment(segment);
|
|
return segment;
|
|
}
|
|
|
|
LlmSegment* ChatGeneration::openReasoningSegment() {
|
|
for (auto* segment : m_segments)
|
|
if (segment->type() == LlmSegment::Type::Reasoning &&
|
|
segment->running())
|
|
return segment;
|
|
auto* segment = new LlmSegment(
|
|
LlmSegment::Type::Reasoning, QDateTime::currentMSecsSinceEpoch(), this);
|
|
segment->begin();
|
|
addSegment(segment);
|
|
return segment;
|
|
}
|
|
|
|
LlmSegment* ChatGeneration::beginToolCall(
|
|
const QString& name, const QString& toolCallId) {
|
|
closeOpenSegments();
|
|
auto* segment = new LlmSegment(
|
|
LlmSegment::Type::ToolCall, QDateTime::currentMSecsSinceEpoch(), this);
|
|
segment->setName(name);
|
|
segment->setToolCallId(toolCallId);
|
|
// Awaiting user approval until LlmClient::approveTools() promotes it
|
|
// to Running; denyTools()/endStream() finalize it as an error.
|
|
segment->setStatus(LlmSegment::Status::Pending);
|
|
segment->begin();
|
|
addSegment(segment);
|
|
Q_EMIT toolStateChanged();
|
|
return segment;
|
|
}
|
|
|
|
void ChatGeneration::addSegment(LlmSegment* segment) {
|
|
if (!segment || m_segments.contains(segment)) return;
|
|
segment->setParent(this);
|
|
connect(segment, &LlmSegment::textChanged, this, [this, segment]() {
|
|
if (segment->type() == LlmSegment::Type::Reasoning)
|
|
Q_EMIT reasoningChanged();
|
|
else if (segment->type() == LlmSegment::Type::Content)
|
|
Q_EMIT contentChanged();
|
|
updateReasoningActive();
|
|
});
|
|
connect(segment, &LlmSegment::statusChanged, this, [this]() {
|
|
Q_EMIT toolStateChanged();
|
|
});
|
|
connect(segment, &LlmSegment::resultChanged, this, [this]() {
|
|
Q_EMIT toolStateChanged();
|
|
});
|
|
connect(segment, &LlmSegment::runningChanged, this, [this]() {
|
|
Q_EMIT elapsedMsChanged();
|
|
Q_EMIT toolStateChanged();
|
|
updateReasoningActive();
|
|
});
|
|
m_segments.append(segment);
|
|
Q_EMIT segmentsChanged();
|
|
}
|
|
|
|
void ChatGeneration::closeOpenSegments() {
|
|
for (auto* segment : m_segments)
|
|
if (segment->running()) segment->close();
|
|
updateReasoningActive();
|
|
}
|
|
|
|
} // namespace ZShell::llm
|