feat: Implement AgentChatHistoryCodec for chat history persistence with JSON support
This commit is contained in:
336
src/app/editor/system/agent_chat_history_codec.cc
Normal file
336
src/app/editor/system/agent_chat_history_codec.cc
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
#include "app/editor/system/agent_chat_history_codec.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "absl/strings/str_format.h"
|
||||||
|
#include "absl/time/clock.h"
|
||||||
|
#include "absl/time/time.h"
|
||||||
|
|
||||||
|
#ifdef YAZE_WITH_GRPC
|
||||||
|
#include "nlohmann/json.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace yaze {
|
||||||
|
namespace editor {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#ifdef YAZE_WITH_GRPC
|
||||||
|
using Json = nlohmann::json;
|
||||||
|
|
||||||
|
absl::Time ParseTimestamp(const Json& value) {
|
||||||
|
if (!value.is_string()) {
|
||||||
|
return absl::Now();
|
||||||
|
}
|
||||||
|
absl::Time parsed;
|
||||||
|
if (absl::ParseTime(absl::RFC3339_full, value.get<std::string>(),
|
||||||
|
absl::UTCTimeZone(), &parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return absl::Now();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json SerializeTableData(const cli::agent::ChatMessage::TableData& table) {
|
||||||
|
Json json;
|
||||||
|
json["headers"] = table.headers;
|
||||||
|
json["rows"] = table.rows;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<cli::agent::ChatMessage::TableData> ParseTableData(
|
||||||
|
const Json& json) {
|
||||||
|
if (!json.is_object()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
cli::agent::ChatMessage::TableData table;
|
||||||
|
if (json.contains("headers") && json["headers"].is_array()) {
|
||||||
|
for (const auto& header : json["headers"]) {
|
||||||
|
if (header.is_string()) {
|
||||||
|
table.headers.push_back(header.get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.contains("rows") && json["rows"].is_array()) {
|
||||||
|
for (const auto& row : json["rows"]) {
|
||||||
|
if (!row.is_array()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::vector<std::string> row_values;
|
||||||
|
for (const auto& value : row) {
|
||||||
|
if (value.is_string()) {
|
||||||
|
row_values.push_back(value.get<std::string>());
|
||||||
|
} else {
|
||||||
|
row_values.push_back(value.dump());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table.rows.push_back(std::move(row_values));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table.headers.empty() && table.rows.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json SerializeProposal(const cli::agent::ChatMessage::ProposalSummary& proposal) {
|
||||||
|
Json json;
|
||||||
|
json["id"] = proposal.id;
|
||||||
|
json["change_count"] = proposal.change_count;
|
||||||
|
json["executed_commands"] = proposal.executed_commands;
|
||||||
|
json["sandbox_rom_path"] = proposal.sandbox_rom_path.string();
|
||||||
|
json["proposal_json_path"] = proposal.proposal_json_path.string();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<cli::agent::ChatMessage::ProposalSummary> ParseProposal(
|
||||||
|
const Json& json) {
|
||||||
|
if (!json.is_object()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
cli::agent::ChatMessage::ProposalSummary summary;
|
||||||
|
summary.id = json.value("id", "");
|
||||||
|
summary.change_count = json.value("change_count", 0);
|
||||||
|
summary.executed_commands = json.value("executed_commands", 0);
|
||||||
|
if (json.contains("sandbox_rom_path") && json["sandbox_rom_path"].is_string()) {
|
||||||
|
summary.sandbox_rom_path = json["sandbox_rom_path"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (json.contains("proposal_json_path") &&
|
||||||
|
json["proposal_json_path"].is_string()) {
|
||||||
|
summary.proposal_json_path = json["proposal_json_path"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (summary.id.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // YAZE_WITH_GRPC
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool AgentChatHistoryCodec::Available() {
|
||||||
|
#ifdef YAZE_WITH_GRPC
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::StatusOr<AgentChatHistoryCodec::Snapshot> AgentChatHistoryCodec::Load(
|
||||||
|
const std::filesystem::path& path) {
|
||||||
|
#ifdef YAZE_WITH_GRPC
|
||||||
|
Snapshot snapshot;
|
||||||
|
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (!file.good()) {
|
||||||
|
return snapshot; // Treat missing file as empty history.
|
||||||
|
}
|
||||||
|
|
||||||
|
Json json;
|
||||||
|
try {
|
||||||
|
file >> json;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
return absl::InternalError(
|
||||||
|
absl::StrFormat("Failed to parse chat history: %s", e.what()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json.contains("messages") || !json["messages"].is_array()) {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& item : json["messages"]) {
|
||||||
|
if (!item.is_object()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cli::agent::ChatMessage message;
|
||||||
|
std::string sender = item.value("sender", "agent");
|
||||||
|
message.sender = sender == "user"
|
||||||
|
? cli::agent::ChatMessage::Sender::kUser
|
||||||
|
: cli::agent::ChatMessage::Sender::kAgent;
|
||||||
|
message.message = item.value("message", "");
|
||||||
|
message.timestamp = ParseTimestamp(item["timestamp"]);
|
||||||
|
|
||||||
|
if (item.contains("json_pretty") && item["json_pretty"].is_string()) {
|
||||||
|
message.json_pretty = item["json_pretty"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (item.contains("table_data")) {
|
||||||
|
message.table_data = ParseTableData(item["table_data"]);
|
||||||
|
}
|
||||||
|
if (item.contains("metrics") && item["metrics"].is_object()) {
|
||||||
|
cli::agent::ChatMessage::SessionMetrics metrics;
|
||||||
|
const auto& metrics_json = item["metrics"];
|
||||||
|
metrics.turn_index = metrics_json.value("turn_index", 0);
|
||||||
|
metrics.total_user_messages =
|
||||||
|
metrics_json.value("total_user_messages", 0);
|
||||||
|
metrics.total_agent_messages =
|
||||||
|
metrics_json.value("total_agent_messages", 0);
|
||||||
|
metrics.total_tool_calls = metrics_json.value("total_tool_calls", 0);
|
||||||
|
metrics.total_commands = metrics_json.value("total_commands", 0);
|
||||||
|
metrics.total_proposals = metrics_json.value("total_proposals", 0);
|
||||||
|
metrics.total_elapsed_seconds =
|
||||||
|
metrics_json.value("total_elapsed_seconds", 0.0);
|
||||||
|
metrics.average_latency_seconds =
|
||||||
|
metrics_json.value("average_latency_seconds", 0.0);
|
||||||
|
message.metrics = metrics;
|
||||||
|
}
|
||||||
|
if (item.contains("proposal")) {
|
||||||
|
message.proposal = ParseProposal(item["proposal"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.history.push_back(std::move(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.contains("collaboration") && json["collaboration"].is_object()) {
|
||||||
|
const auto& collab_json = json["collaboration"];
|
||||||
|
snapshot.collaboration.active = collab_json.value("active", false);
|
||||||
|
snapshot.collaboration.session_id = collab_json.value("session_id", "");
|
||||||
|
snapshot.collaboration.participants.clear();
|
||||||
|
if (collab_json.contains("participants") &&
|
||||||
|
collab_json["participants"].is_array()) {
|
||||||
|
for (const auto& participant : collab_json["participants"]) {
|
||||||
|
if (participant.is_string()) {
|
||||||
|
snapshot.collaboration.participants.push_back(
|
||||||
|
participant.get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (collab_json.contains("last_synced")) {
|
||||||
|
snapshot.collaboration.last_synced =
|
||||||
|
ParseTimestamp(collab_json["last_synced"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.contains("multimodal") && json["multimodal"].is_object()) {
|
||||||
|
const auto& multimodal_json = json["multimodal"];
|
||||||
|
if (multimodal_json.contains("last_capture_path") &&
|
||||||
|
multimodal_json["last_capture_path"].is_string()) {
|
||||||
|
std::string path_value =
|
||||||
|
multimodal_json["last_capture_path"].get<std::string>();
|
||||||
|
if (!path_value.empty()) {
|
||||||
|
snapshot.multimodal.last_capture_path =
|
||||||
|
std::filesystem::path(path_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot.multimodal.status_message =
|
||||||
|
multimodal_json.value("status_message", "");
|
||||||
|
if (multimodal_json.contains("last_updated")) {
|
||||||
|
snapshot.multimodal.last_updated =
|
||||||
|
ParseTimestamp(multimodal_json["last_updated"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
#else
|
||||||
|
(void)path;
|
||||||
|
return absl::UnimplementedError(
|
||||||
|
"Chat history persistence requires YAZE_WITH_GRPC=ON");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status AgentChatHistoryCodec::Save(
|
||||||
|
const std::filesystem::path& path, const Snapshot& snapshot) {
|
||||||
|
#ifdef YAZE_WITH_GRPC
|
||||||
|
Json json;
|
||||||
|
json["version"] = 2;
|
||||||
|
json["messages"] = Json::array();
|
||||||
|
|
||||||
|
for (const auto& message : snapshot.history) {
|
||||||
|
Json entry;
|
||||||
|
entry["sender"] =
|
||||||
|
message.sender == cli::agent::ChatMessage::Sender::kUser ? "user"
|
||||||
|
: "agent";
|
||||||
|
entry["message"] = message.message;
|
||||||
|
entry["timestamp"] = absl::FormatTime(absl::RFC3339_full,
|
||||||
|
message.timestamp,
|
||||||
|
absl::UTCTimeZone());
|
||||||
|
|
||||||
|
if (message.json_pretty.has_value()) {
|
||||||
|
entry["json_pretty"] = *message.json_pretty;
|
||||||
|
}
|
||||||
|
if (message.table_data.has_value()) {
|
||||||
|
entry["table_data"] = SerializeTableData(*message.table_data);
|
||||||
|
}
|
||||||
|
if (message.metrics.has_value()) {
|
||||||
|
const auto& metrics = *message.metrics;
|
||||||
|
Json metrics_json;
|
||||||
|
metrics_json["turn_index"] = metrics.turn_index;
|
||||||
|
metrics_json["total_user_messages"] = metrics.total_user_messages;
|
||||||
|
metrics_json["total_agent_messages"] = metrics.total_agent_messages;
|
||||||
|
metrics_json["total_tool_calls"] = metrics.total_tool_calls;
|
||||||
|
metrics_json["total_commands"] = metrics.total_commands;
|
||||||
|
metrics_json["total_proposals"] = metrics.total_proposals;
|
||||||
|
metrics_json["total_elapsed_seconds"] = metrics.total_elapsed_seconds;
|
||||||
|
metrics_json["average_latency_seconds"] =
|
||||||
|
metrics.average_latency_seconds;
|
||||||
|
entry["metrics"] = metrics_json;
|
||||||
|
}
|
||||||
|
if (message.proposal.has_value()) {
|
||||||
|
entry["proposal"] = SerializeProposal(*message.proposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
json["messages"].push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
Json collab_json;
|
||||||
|
collab_json["active"] = snapshot.collaboration.active;
|
||||||
|
collab_json["session_id"] = snapshot.collaboration.session_id;
|
||||||
|
collab_json["participants"] = snapshot.collaboration.participants;
|
||||||
|
if (snapshot.collaboration.last_synced != absl::InfinitePast()) {
|
||||||
|
collab_json["last_synced"] = absl::FormatTime(
|
||||||
|
absl::RFC3339_full, snapshot.collaboration.last_synced,
|
||||||
|
absl::UTCTimeZone());
|
||||||
|
}
|
||||||
|
json["collaboration"] = std::move(collab_json);
|
||||||
|
|
||||||
|
Json multimodal_json;
|
||||||
|
if (snapshot.multimodal.last_capture_path.has_value()) {
|
||||||
|
multimodal_json["last_capture_path"] =
|
||||||
|
snapshot.multimodal.last_capture_path->string();
|
||||||
|
} else {
|
||||||
|
multimodal_json["last_capture_path"] = "";
|
||||||
|
}
|
||||||
|
multimodal_json["status_message"] = snapshot.multimodal.status_message;
|
||||||
|
if (snapshot.multimodal.last_updated != absl::InfinitePast()) {
|
||||||
|
multimodal_json["last_updated"] = absl::FormatTime(
|
||||||
|
absl::RFC3339_full, snapshot.multimodal.last_updated,
|
||||||
|
absl::UTCTimeZone());
|
||||||
|
}
|
||||||
|
json["multimodal"] = std::move(multimodal_json);
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
auto directory = path.parent_path();
|
||||||
|
if (!directory.empty()) {
|
||||||
|
std::filesystem::create_directories(directory, ec);
|
||||||
|
if (ec) {
|
||||||
|
return absl::InternalError(absl::StrFormat(
|
||||||
|
"Unable to create chat history directory: %s", ec.message()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream file(path);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
return absl::InternalError("Cannot write chat history file");
|
||||||
|
}
|
||||||
|
|
||||||
|
file << json.dump(2);
|
||||||
|
return absl::OkStatus();
|
||||||
|
#else
|
||||||
|
(void)path;
|
||||||
|
(void)snapshot;
|
||||||
|
return absl::UnimplementedError(
|
||||||
|
"Chat history persistence requires YAZE_WITH_GRPC=ON");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace editor
|
||||||
|
} // namespace yaze
|
||||||
53
src/app/editor/system/agent_chat_history_codec.h
Normal file
53
src/app/editor/system/agent_chat_history_codec.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#ifndef YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_CODEC_H_
|
||||||
|
#define YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_CODEC_H_
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "absl/status/status.h"
|
||||||
|
#include "absl/status/statusor.h"
|
||||||
|
#include "absl/time/time.h"
|
||||||
|
#include "cli/service/agent/conversational_agent_service.h"
|
||||||
|
|
||||||
|
namespace yaze {
|
||||||
|
namespace editor {
|
||||||
|
|
||||||
|
// Bridges chat history persistence to optional JSON support. When the
|
||||||
|
// application is built without gRPC/JSON support these helpers gracefully
|
||||||
|
// degrade and report an Unimplemented status so the UI can disable
|
||||||
|
// persistence instead of failing to compile.
|
||||||
|
class AgentChatHistoryCodec {
|
||||||
|
public:
|
||||||
|
struct CollaborationState {
|
||||||
|
bool active = false;
|
||||||
|
std::string session_id;
|
||||||
|
std::vector<std::string> participants;
|
||||||
|
absl::Time last_synced = absl::InfinitePast();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MultimodalState {
|
||||||
|
std::optional<std::filesystem::path> last_capture_path;
|
||||||
|
std::string status_message;
|
||||||
|
absl::Time last_updated = absl::InfinitePast();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Snapshot {
|
||||||
|
std::vector<cli::agent::ChatMessage> history;
|
||||||
|
CollaborationState collaboration;
|
||||||
|
MultimodalState multimodal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns true when the codec can actually serialize / deserialize history.
|
||||||
|
static bool Available();
|
||||||
|
|
||||||
|
static absl::StatusOr<Snapshot> Load(const std::filesystem::path& path);
|
||||||
|
static absl::Status Save(const std::filesystem::path& path,
|
||||||
|
const Snapshot& snapshot);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace editor
|
||||||
|
} // namespace yaze
|
||||||
|
|
||||||
|
#endif // YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_CODEC_H_
|
||||||
@@ -3,21 +3,21 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "absl/strings/str_format.h"
|
#include "absl/strings/str_format.h"
|
||||||
#include "absl/time/clock.h"
|
#include "absl/time/clock.h"
|
||||||
#include "absl/time/time.h"
|
#include "absl/time/time.h"
|
||||||
#include "app/core/platform/file_dialog.h"
|
#include "app/core/platform/file_dialog.h"
|
||||||
|
#include "app/editor/system/agent_chat_history_codec.h"
|
||||||
#include "app/editor/system/proposal_drawer.h"
|
#include "app/editor/system/proposal_drawer.h"
|
||||||
#include "app/editor/system/toast_manager.h"
|
#include "app/editor/system/toast_manager.h"
|
||||||
#include "app/gui/icons.h"
|
#include "app/gui/icons.h"
|
||||||
#include "imgui/imgui.h"
|
#include "imgui/imgui.h"
|
||||||
#include "imgui/misc/cpp/imgui_stdlib.h"
|
#include "imgui/misc/cpp/imgui_stdlib.h"
|
||||||
#include "nlohmann/json.hpp"
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -47,95 +47,6 @@ std::filesystem::path ResolveHistoryPath() {
|
|||||||
return directory / "chat_history.json";
|
return directory / "chat_history.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
absl::Time ParseTimestamp(const nlohmann::json& value) {
|
|
||||||
if (!value.is_string()) {
|
|
||||||
return absl::Now();
|
|
||||||
}
|
|
||||||
absl::Time parsed;
|
|
||||||
if (absl::ParseTime(absl::RFC3339_full, value.get<std::string>(),
|
|
||||||
absl::UTCTimeZone(), &parsed)) {
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
return absl::Now();
|
|
||||||
}
|
|
||||||
|
|
||||||
nlohmann::json SerializeTableData(const ChatMessage::TableData& table) {
|
|
||||||
nlohmann::json json;
|
|
||||||
json["headers"] = table.headers;
|
|
||||||
json["rows"] = table.rows;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<ChatMessage::TableData> ParseTableData(const nlohmann::json& json) {
|
|
||||||
if (!json.is_object()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMessage::TableData table;
|
|
||||||
if (json.contains("headers") && json["headers"].is_array()) {
|
|
||||||
for (const auto& header : json["headers"]) {
|
|
||||||
if (header.is_string()) {
|
|
||||||
table.headers.push_back(header.get<std::string>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.contains("rows") && json["rows"].is_array()) {
|
|
||||||
for (const auto& row : json["rows"]) {
|
|
||||||
if (!row.is_array()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
std::vector<std::string> row_values;
|
|
||||||
for (const auto& value : row) {
|
|
||||||
if (value.is_string()) {
|
|
||||||
row_values.push_back(value.get<std::string>());
|
|
||||||
} else {
|
|
||||||
row_values.push_back(value.dump());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table.rows.push_back(std::move(row_values));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (table.headers.empty() && table.rows.empty()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
nlohmann::json SerializeProposal(const ChatMessage::ProposalSummary& proposal) {
|
|
||||||
nlohmann::json json;
|
|
||||||
json["id"] = proposal.id;
|
|
||||||
json["change_count"] = proposal.change_count;
|
|
||||||
json["executed_commands"] = proposal.executed_commands;
|
|
||||||
json["sandbox_rom_path"] = proposal.sandbox_rom_path.string();
|
|
||||||
json["proposal_json_path"] = proposal.proposal_json_path.string();
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<ChatMessage::ProposalSummary> ParseProposal(
|
|
||||||
const nlohmann::json& json) {
|
|
||||||
if (!json.is_object()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMessage::ProposalSummary summary;
|
|
||||||
summary.id = json.value("id", "");
|
|
||||||
summary.change_count = json.value("change_count", 0);
|
|
||||||
summary.executed_commands = json.value("executed_commands", 0);
|
|
||||||
if (json.contains("sandbox_rom_path") && json["sandbox_rom_path"].is_string()) {
|
|
||||||
summary.sandbox_rom_path = json["sandbox_rom_path"].get<std::string>();
|
|
||||||
}
|
|
||||||
if (json.contains("proposal_json_path") && json["proposal_json_path"].is_string()) {
|
|
||||||
summary.proposal_json_path = json["proposal_json_path"].get<std::string>();
|
|
||||||
}
|
|
||||||
if (summary.id.empty()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderTable(const ChatMessage::TableData& table_data) {
|
void RenderTable(const ChatMessage::TableData& table_data) {
|
||||||
const int column_count = static_cast<int>(table_data.headers.size());
|
const int column_count = static_cast<int>(table_data.headers.size());
|
||||||
if (column_count <= 0) {
|
if (column_count <= 0) {
|
||||||
@@ -175,6 +86,7 @@ AgentChatWidget::AgentChatWidget() {
|
|||||||
title_ = "Agent Chat";
|
title_ = "Agent Chat";
|
||||||
memset(input_buffer_, 0, sizeof(input_buffer_));
|
memset(input_buffer_, 0, sizeof(input_buffer_));
|
||||||
history_path_ = ResolveHistoryPath();
|
history_path_ = ResolveHistoryPath();
|
||||||
|
history_supported_ = AgentChatHistoryCodec::Available();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AgentChatWidget::SetRomContext(Rom* rom) {
|
void AgentChatWidget::SetRomContext(Rom* rom) {
|
||||||
@@ -212,122 +124,62 @@ void AgentChatWidget::EnsureHistoryLoaded() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!history_supported_) {
|
||||||
std::ifstream file(history_path_);
|
if (!history_warning_displayed_ && toast_manager_) {
|
||||||
if (!file.good()) {
|
toast_manager_->Show(
|
||||||
|
"Chat history requires gRPC/JSON support and is disabled",
|
||||||
|
ToastType::kWarning, 5.0f);
|
||||||
|
history_warning_displayed_ = true;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
absl::StatusOr<AgentChatHistoryCodec::Snapshot> result =
|
||||||
nlohmann::json json;
|
AgentChatHistoryCodec::Load(history_path_);
|
||||||
file >> json;
|
if (!result.ok()) {
|
||||||
if (!json.contains("messages") || !json["messages"].is_array()) {
|
if (result.status().code() == absl::StatusCode::kUnimplemented) {
|
||||||
|
history_supported_ = false;
|
||||||
|
if (!history_warning_displayed_ && toast_manager_) {
|
||||||
|
toast_manager_->Show(
|
||||||
|
"Chat history requires gRPC/JSON support and is disabled",
|
||||||
|
ToastType::kWarning, 5.0f);
|
||||||
|
history_warning_displayed_ = true;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ChatMessage> history;
|
|
||||||
for (const auto& item : json["messages"]) {
|
|
||||||
if (!item.is_object()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMessage message;
|
|
||||||
std::string sender = item.value("sender", "agent");
|
|
||||||
message.sender =
|
|
||||||
sender == "user" ? ChatMessage::Sender::kUser
|
|
||||||
: ChatMessage::Sender::kAgent;
|
|
||||||
message.message = item.value("message", "");
|
|
||||||
message.timestamp = ParseTimestamp(item["timestamp"]);
|
|
||||||
|
|
||||||
if (item.contains("json_pretty") && item["json_pretty"].is_string()) {
|
|
||||||
message.json_pretty = item["json_pretty"].get<std::string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.contains("table_data")) {
|
|
||||||
message.table_data = ParseTableData(item["table_data"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.contains("metrics") && item["metrics"].is_object()) {
|
|
||||||
ChatMessage::SessionMetrics metrics;
|
|
||||||
const auto& metrics_json = item["metrics"];
|
|
||||||
metrics.turn_index = metrics_json.value("turn_index", 0);
|
|
||||||
metrics.total_user_messages =
|
|
||||||
metrics_json.value("total_user_messages", 0);
|
|
||||||
metrics.total_agent_messages =
|
|
||||||
metrics_json.value("total_agent_messages", 0);
|
|
||||||
metrics.total_tool_calls =
|
|
||||||
metrics_json.value("total_tool_calls", 0);
|
|
||||||
metrics.total_commands = metrics_json.value("total_commands", 0);
|
|
||||||
metrics.total_proposals = metrics_json.value("total_proposals", 0);
|
|
||||||
metrics.total_elapsed_seconds =
|
|
||||||
metrics_json.value("total_elapsed_seconds", 0.0);
|
|
||||||
metrics.average_latency_seconds =
|
|
||||||
metrics_json.value("average_latency_seconds", 0.0);
|
|
||||||
message.metrics = metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.contains("proposal")) {
|
|
||||||
message.proposal = ParseProposal(item["proposal"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
history.push_back(std::move(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!history.empty()) {
|
|
||||||
agent_service_.ReplaceHistory(std::move(history));
|
|
||||||
last_history_size_ = agent_service_.GetHistory().size();
|
|
||||||
last_proposal_count_ = CountKnownProposals();
|
|
||||||
history_dirty_ = false;
|
|
||||||
last_persist_time_ = absl::Now();
|
|
||||||
if (toast_manager_) {
|
|
||||||
toast_manager_->Show("Restored chat history",
|
|
||||||
ToastType::kInfo, 3.5f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.contains("collaboration") && json["collaboration"].is_object()) {
|
|
||||||
const auto& collab_json = json["collaboration"];
|
|
||||||
collaboration_state_.active = collab_json.value("active", false);
|
|
||||||
collaboration_state_.session_id = collab_json.value("session_id", "");
|
|
||||||
collaboration_state_.participants.clear();
|
|
||||||
if (collab_json.contains("participants") &&
|
|
||||||
collab_json["participants"].is_array()) {
|
|
||||||
for (const auto& participant : collab_json["participants"]) {
|
|
||||||
if (participant.is_string()) {
|
|
||||||
collaboration_state_.participants.push_back(
|
|
||||||
participant.get<std::string>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (collab_json.contains("last_synced")) {
|
|
||||||
collaboration_state_.last_synced =
|
|
||||||
ParseTimestamp(collab_json["last_synced"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.contains("multimodal") && json["multimodal"].is_object()) {
|
|
||||||
const auto& multimodal_json = json["multimodal"];
|
|
||||||
if (multimodal_json.contains("last_capture_path") &&
|
|
||||||
multimodal_json["last_capture_path"].is_string()) {
|
|
||||||
std::string path = multimodal_json["last_capture_path"].get<std::string>();
|
|
||||||
if (!path.empty()) {
|
|
||||||
multimodal_state_.last_capture_path = std::filesystem::path(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
multimodal_state_.status_message =
|
|
||||||
multimodal_json.value("status_message", "");
|
|
||||||
if (multimodal_json.contains("last_updated")) {
|
|
||||||
multimodal_state_.last_updated =
|
|
||||||
ParseTimestamp(multimodal_json["last_updated"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
if (toast_manager_) {
|
if (toast_manager_) {
|
||||||
toast_manager_->Show(
|
toast_manager_->Show(
|
||||||
absl::StrFormat("Failed to load chat history: %s", e.what()),
|
absl::StrFormat("Failed to load chat history: %s",
|
||||||
|
result.status().ToString()),
|
||||||
ToastType::kError, 6.0f);
|
ToastType::kError, 6.0f);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AgentChatHistoryCodec::Snapshot snapshot = std::move(result.value());
|
||||||
|
|
||||||
|
if (!snapshot.history.empty()) {
|
||||||
|
agent_service_.ReplaceHistory(std::move(snapshot.history));
|
||||||
|
last_history_size_ = agent_service_.GetHistory().size();
|
||||||
|
last_proposal_count_ = CountKnownProposals();
|
||||||
|
history_dirty_ = false;
|
||||||
|
last_persist_time_ = absl::Now();
|
||||||
|
if (toast_manager_) {
|
||||||
|
toast_manager_->Show("Restored chat history",
|
||||||
|
ToastType::kInfo, 3.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collaboration_state_.active = snapshot.collaboration.active;
|
||||||
|
collaboration_state_.session_id = snapshot.collaboration.session_id;
|
||||||
|
collaboration_state_.participants = snapshot.collaboration.participants;
|
||||||
|
collaboration_state_.last_synced = snapshot.collaboration.last_synced;
|
||||||
|
|
||||||
|
multimodal_state_.last_capture_path =
|
||||||
|
snapshot.multimodal.last_capture_path;
|
||||||
|
multimodal_state_.status_message = snapshot.multimodal.status_message;
|
||||||
|
multimodal_state_.last_updated = snapshot.multimodal.last_updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AgentChatWidget::PersistHistory() {
|
void AgentChatWidget::PersistHistory() {
|
||||||
@@ -335,98 +187,51 @@ void AgentChatWidget::PersistHistory() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& history = agent_service_.GetHistory();
|
if (!history_supported_) {
|
||||||
|
history_dirty_ = false;
|
||||||
nlohmann::json json;
|
if (!history_warning_displayed_ && toast_manager_) {
|
||||||
json["version"] = 2;
|
toast_manager_->Show(
|
||||||
json["messages"] = nlohmann::json::array();
|
"Chat history requires gRPC/JSON support and is disabled",
|
||||||
|
ToastType::kWarning, 5.0f);
|
||||||
for (const auto& message : history) {
|
history_warning_displayed_ = true;
|
||||||
nlohmann::json entry;
|
}
|
||||||
entry["sender"] =
|
return;
|
||||||
message.sender == ChatMessage::Sender::kUser ? "user" : "agent";
|
}
|
||||||
entry["message"] = message.message;
|
|
||||||
entry["timestamp"] = absl::FormatTime(absl::RFC3339_full,
|
AgentChatHistoryCodec::Snapshot snapshot;
|
||||||
message.timestamp,
|
snapshot.history = agent_service_.GetHistory();
|
||||||
absl::UTCTimeZone());
|
snapshot.collaboration.active = collaboration_state_.active;
|
||||||
|
snapshot.collaboration.session_id = collaboration_state_.session_id;
|
||||||
if (message.json_pretty.has_value()) {
|
snapshot.collaboration.participants = collaboration_state_.participants;
|
||||||
entry["json_pretty"] = *message.json_pretty;
|
snapshot.collaboration.last_synced = collaboration_state_.last_synced;
|
||||||
}
|
snapshot.multimodal.last_capture_path =
|
||||||
if (message.table_data.has_value()) {
|
multimodal_state_.last_capture_path;
|
||||||
entry["table_data"] = SerializeTableData(*message.table_data);
|
snapshot.multimodal.status_message = multimodal_state_.status_message;
|
||||||
}
|
snapshot.multimodal.last_updated = multimodal_state_.last_updated;
|
||||||
if (message.metrics.has_value()) {
|
|
||||||
const auto& metrics = *message.metrics;
|
absl::Status status = AgentChatHistoryCodec::Save(history_path_, snapshot);
|
||||||
nlohmann::json metrics_json;
|
if (!status.ok()) {
|
||||||
metrics_json["turn_index"] = metrics.turn_index;
|
if (status.code() == absl::StatusCode::kUnimplemented) {
|
||||||
metrics_json["total_user_messages"] = metrics.total_user_messages;
|
history_supported_ = false;
|
||||||
metrics_json["total_agent_messages"] = metrics.total_agent_messages;
|
if (!history_warning_displayed_ && toast_manager_) {
|
||||||
metrics_json["total_tool_calls"] = metrics.total_tool_calls;
|
toast_manager_->Show(
|
||||||
metrics_json["total_commands"] = metrics.total_commands;
|
"Chat history requires gRPC/JSON support and is disabled",
|
||||||
metrics_json["total_proposals"] = metrics.total_proposals;
|
ToastType::kWarning, 5.0f);
|
||||||
metrics_json["total_elapsed_seconds"] = metrics.total_elapsed_seconds;
|
history_warning_displayed_ = true;
|
||||||
metrics_json["average_latency_seconds"] =
|
}
|
||||||
metrics.average_latency_seconds;
|
history_dirty_ = false;
|
||||||
entry["metrics"] = metrics_json;
|
return;
|
||||||
}
|
}
|
||||||
if (message.proposal.has_value()) {
|
|
||||||
entry["proposal"] = SerializeProposal(*message.proposal);
|
if (toast_manager_) {
|
||||||
}
|
toast_manager_->Show(
|
||||||
|
absl::StrFormat("Failed to persist chat history: %s",
|
||||||
json["messages"].push_back(std::move(entry));
|
status.ToString()),
|
||||||
}
|
ToastType::kError, 6.0f);
|
||||||
|
|
||||||
nlohmann::json collab_json;
|
|
||||||
collab_json["active"] = collaboration_state_.active;
|
|
||||||
collab_json["session_id"] = collaboration_state_.session_id;
|
|
||||||
collab_json["participants"] = collaboration_state_.participants;
|
|
||||||
if (collaboration_state_.last_synced != absl::InfinitePast()) {
|
|
||||||
collab_json["last_synced"] = absl::FormatTime(
|
|
||||||
absl::RFC3339_full, collaboration_state_.last_synced,
|
|
||||||
absl::UTCTimeZone());
|
|
||||||
}
|
|
||||||
json["collaboration"] = std::move(collab_json);
|
|
||||||
|
|
||||||
nlohmann::json multimodal_json;
|
|
||||||
if (multimodal_state_.last_capture_path.has_value()) {
|
|
||||||
multimodal_json["last_capture_path"] =
|
|
||||||
multimodal_state_.last_capture_path->string();
|
|
||||||
} else {
|
|
||||||
multimodal_json["last_capture_path"] = "";
|
|
||||||
}
|
|
||||||
multimodal_json["status_message"] = multimodal_state_.status_message;
|
|
||||||
if (multimodal_state_.last_updated != absl::InfinitePast()) {
|
|
||||||
multimodal_json["last_updated"] = absl::FormatTime(
|
|
||||||
absl::RFC3339_full, multimodal_state_.last_updated,
|
|
||||||
absl::UTCTimeZone());
|
|
||||||
}
|
|
||||||
json["multimodal"] = std::move(multimodal_json);
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
auto directory = history_path_.parent_path();
|
|
||||||
if (!directory.empty()) {
|
|
||||||
std::filesystem::create_directories(directory, ec);
|
|
||||||
if (ec) {
|
|
||||||
if (toast_manager_) {
|
|
||||||
toast_manager_->Show(
|
|
||||||
"Unable to create chat history directory",
|
|
||||||
ToastType::kError, 5.0f);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ofstream file(history_path_);
|
|
||||||
if (!file.is_open()) {
|
|
||||||
if (toast_manager_) {
|
|
||||||
toast_manager_->Show("Cannot write chat history",
|
|
||||||
ToastType::kError, 5.0f);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
file << json.dump(2);
|
|
||||||
history_dirty_ = false;
|
history_dirty_ = false;
|
||||||
last_persist_time_ = absl::Now();
|
last_persist_time_ = absl::Now();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ class AgentChatWidget {
|
|||||||
size_t last_history_size_ = 0;
|
size_t last_history_size_ = 0;
|
||||||
bool history_loaded_ = false;
|
bool history_loaded_ = false;
|
||||||
bool history_dirty_ = false;
|
bool history_dirty_ = false;
|
||||||
|
bool history_supported_ = true;
|
||||||
|
bool history_warning_displayed_ = false;
|
||||||
std::filesystem::path history_path_;
|
std::filesystem::path history_path_;
|
||||||
int last_proposal_count_ = 0;
|
int last_proposal_count_ = 0;
|
||||||
ToastManager* toast_manager_ = nullptr;
|
ToastManager* toast_manager_ = nullptr;
|
||||||
|
|||||||
Reference in New Issue
Block a user