yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
agent_chat_history_codec.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <optional>
6#include <string>
7#include <vector>
8
9#include "absl/strings/str_format.h"
10#include "absl/time/clock.h"
11#include "absl/time/time.h"
12
13#if defined(YAZE_WITH_JSON)
14#include "nlohmann/json.hpp"
15#endif
16
17namespace yaze {
18namespace editor {
19
20namespace {
21
22#if defined(YAZE_WITH_JSON)
23using Json = nlohmann::json;
24
25absl::Time ParseTimestamp(const Json& value) {
26 if (!value.is_string()) {
27 return absl::Now();
28 }
29 absl::Time parsed;
30 if (absl::ParseTime(absl::RFC3339_full, value.get<std::string>(), &parsed,
31 nullptr)) {
32 return parsed;
33 }
34 return absl::Now();
35}
36
37Json SerializeTableData(const cli::agent::ChatMessage::TableData& table) {
38 Json json;
39 json["headers"] = table.headers;
40 json["rows"] = table.rows;
41 return json;
42}
43
44std::optional<cli::agent::ChatMessage::TableData> ParseTableData(
45 const Json& json) {
46 if (!json.is_object()) {
47 return std::nullopt;
48 }
49
51 if (json.contains("headers") && json["headers"].is_array()) {
52 for (const auto& header : json["headers"]) {
53 if (header.is_string()) {
54 table.headers.push_back(header.get<std::string>());
55 }
56 }
57 }
58
59 if (json.contains("rows") && json["rows"].is_array()) {
60 for (const auto& row : json["rows"]) {
61 if (!row.is_array()) {
62 continue;
63 }
64 std::vector<std::string> row_values;
65 for (const auto& value : row) {
66 if (value.is_string()) {
67 row_values.push_back(value.get<std::string>());
68 } else {
69 row_values.push_back(value.dump());
70 }
71 }
72 table.rows.push_back(std::move(row_values));
73 }
74 }
75
76 if (table.headers.empty() && table.rows.empty()) {
77 return std::nullopt;
78 }
79
80 return table;
81}
82
83Json SerializeProposal(const cli::agent::ChatMessage::ProposalSummary& proposal) {
84 Json json;
85 json["id"] = proposal.id;
86 json["change_count"] = proposal.change_count;
87 json["executed_commands"] = proposal.executed_commands;
88 json["sandbox_rom_path"] = proposal.sandbox_rom_path.string();
89 json["proposal_json_path"] = proposal.proposal_json_path.string();
90 return json;
91}
92
93std::optional<cli::agent::ChatMessage::ProposalSummary> ParseProposal(
94 const Json& json) {
95 if (!json.is_object()) {
96 return std::nullopt;
97 }
98
100 summary.id = json.value("id", "");
101 summary.change_count = json.value("change_count", 0);
102 summary.executed_commands = json.value("executed_commands", 0);
103 if (json.contains("sandbox_rom_path") && json["sandbox_rom_path"].is_string()) {
104 summary.sandbox_rom_path = json["sandbox_rom_path"].get<std::string>();
105 }
106 if (json.contains("proposal_json_path") &&
107 json["proposal_json_path"].is_string()) {
108 summary.proposal_json_path = json["proposal_json_path"].get<std::string>();
109 }
110 if (summary.id.empty()) {
111 return std::nullopt;
112 }
113 return summary;
114}
115
116#endif // YAZE_WITH_GRPC
117
118} // namespace
119
121#if defined(YAZE_WITH_JSON)
122 return true;
123#else
124 return false;
125#endif
126}
127
128absl::StatusOr<AgentChatHistoryCodec::Snapshot> AgentChatHistoryCodec::Load(
129 const std::filesystem::path& path) {
130#if defined(YAZE_WITH_JSON)
131 Snapshot snapshot;
132
133 std::ifstream file(path);
134 if (!file.good()) {
135 return snapshot; // Treat missing file as empty history.
136 }
137
138 Json json;
139 try {
140 file >> json;
141 } catch (const std::exception& e) {
142 return absl::InternalError(
143 absl::StrFormat("Failed to parse chat history: %s", e.what()));
144 }
145
146 if (!json.contains("messages") || !json["messages"].is_array()) {
147 return snapshot;
148 }
149
150 for (const auto& item : json["messages"]) {
151 if (!item.is_object()) {
152 continue;
153 }
154
156 std::string sender = item.value("sender", "agent");
157 message.sender = sender == "user"
160 message.message = item.value("message", "");
161 message.timestamp = ParseTimestamp(item["timestamp"]);
162 message.is_internal = item.value("is_internal", false);
163
164 if (item.contains("json_pretty") && item["json_pretty"].is_string()) {
165 message.json_pretty = item["json_pretty"].get<std::string>();
166 }
167 if (item.contains("table_data")) {
168 message.table_data = ParseTableData(item["table_data"]);
169 }
170 if (item.contains("metrics") && item["metrics"].is_object()) {
172 const auto& metrics_json = item["metrics"];
173 metrics.turn_index = metrics_json.value("turn_index", 0);
174 metrics.total_user_messages =
175 metrics_json.value("total_user_messages", 0);
176 metrics.total_agent_messages =
177 metrics_json.value("total_agent_messages", 0);
178 metrics.total_tool_calls = metrics_json.value("total_tool_calls", 0);
179 metrics.total_commands = metrics_json.value("total_commands", 0);
180 metrics.total_proposals = metrics_json.value("total_proposals", 0);
181 metrics.total_elapsed_seconds =
182 metrics_json.value("total_elapsed_seconds", 0.0);
184 metrics_json.value("average_latency_seconds", 0.0);
185 message.metrics = metrics;
186 }
187 if (item.contains("proposal")) {
188 message.proposal = ParseProposal(item["proposal"]);
189 }
190
191 snapshot.history.push_back(std::move(message));
192 }
193
194 if (json.contains("collaboration") && json["collaboration"].is_object()) {
195 const auto& collab_json = json["collaboration"];
196 snapshot.collaboration.active = collab_json.value("active", false);
197 snapshot.collaboration.session_id = collab_json.value("session_id", "");
198 snapshot.collaboration.session_name =
199 collab_json.value("session_name", "");
200 snapshot.collaboration.participants.clear();
201 if (collab_json.contains("participants") &&
202 collab_json["participants"].is_array()) {
203 for (const auto& participant : collab_json["participants"]) {
204 if (participant.is_string()) {
205 snapshot.collaboration.participants.push_back(
206 participant.get<std::string>());
207 }
208 }
209 }
210 if (collab_json.contains("last_synced")) {
211 snapshot.collaboration.last_synced =
212 ParseTimestamp(collab_json["last_synced"]);
213 }
214 if (snapshot.collaboration.session_name.empty() &&
215 !snapshot.collaboration.session_id.empty()) {
216 snapshot.collaboration.session_name =
217 snapshot.collaboration.session_id;
218 }
219 }
220
221 if (json.contains("multimodal") && json["multimodal"].is_object()) {
222 const auto& multimodal_json = json["multimodal"];
223 if (multimodal_json.contains("last_capture_path") &&
224 multimodal_json["last_capture_path"].is_string()) {
225 std::string path_value =
226 multimodal_json["last_capture_path"].get<std::string>();
227 if (!path_value.empty()) {
229 std::filesystem::path(path_value);
230 }
231 }
232 snapshot.multimodal.status_message =
233 multimodal_json.value("status_message", "");
234 if (multimodal_json.contains("last_updated")) {
235 snapshot.multimodal.last_updated =
236 ParseTimestamp(multimodal_json["last_updated"]);
237 }
238 }
239
240 return snapshot;
241#else
242 (void)path;
243 return absl::UnimplementedError(
244 "Chat history persistence requires YAZE_WITH_GRPC=ON");
245#endif
246}
247
249 const std::filesystem::path& path, const Snapshot& snapshot) {
250#if defined(YAZE_WITH_JSON)
251 Json json;
252 json["version"] = 3;
253 json["messages"] = Json::array();
254
255 for (const auto& message : snapshot.history) {
256 Json entry;
257 entry["sender"] =
258 message.sender == cli::agent::ChatMessage::Sender::kUser ? "user"
259 : "agent";
260 entry["message"] = message.message;
261 entry["timestamp"] = absl::FormatTime(absl::RFC3339_full,
262 message.timestamp,
263 absl::UTCTimeZone());
264 entry["is_internal"] = message.is_internal;
265
266 if (message.json_pretty.has_value()) {
267 entry["json_pretty"] = *message.json_pretty;
268 }
269 if (message.table_data.has_value()) {
270 entry["table_data"] = SerializeTableData(*message.table_data);
271 }
272 if (message.metrics.has_value()) {
273 const auto& metrics = *message.metrics;
274 Json metrics_json;
275 metrics_json["turn_index"] = metrics.turn_index;
276 metrics_json["total_user_messages"] = metrics.total_user_messages;
277 metrics_json["total_agent_messages"] = metrics.total_agent_messages;
278 metrics_json["total_tool_calls"] = metrics.total_tool_calls;
279 metrics_json["total_commands"] = metrics.total_commands;
280 metrics_json["total_proposals"] = metrics.total_proposals;
281 metrics_json["total_elapsed_seconds"] = metrics.total_elapsed_seconds;
282 metrics_json["average_latency_seconds"] =
283 metrics.average_latency_seconds;
284 entry["metrics"] = metrics_json;
285 }
286 if (message.proposal.has_value()) {
287 entry["proposal"] = SerializeProposal(*message.proposal);
288 }
289
290 json["messages"].push_back(std::move(entry));
291 }
292
293 Json collab_json;
294 collab_json["active"] = snapshot.collaboration.active;
295 collab_json["session_id"] = snapshot.collaboration.session_id;
296 collab_json["session_name"] = snapshot.collaboration.session_name;
297 collab_json["participants"] = snapshot.collaboration.participants;
298 if (snapshot.collaboration.last_synced != absl::InfinitePast()) {
299 collab_json["last_synced"] = absl::FormatTime(
300 absl::RFC3339_full, snapshot.collaboration.last_synced,
301 absl::UTCTimeZone());
302 }
303 json["collaboration"] = std::move(collab_json);
304
305 Json multimodal_json;
306 if (snapshot.multimodal.last_capture_path.has_value()) {
307 multimodal_json["last_capture_path"] =
308 snapshot.multimodal.last_capture_path->string();
309 } else {
310 multimodal_json["last_capture_path"] = "";
311 }
312 multimodal_json["status_message"] = snapshot.multimodal.status_message;
313 if (snapshot.multimodal.last_updated != absl::InfinitePast()) {
314 multimodal_json["last_updated"] = absl::FormatTime(
315 absl::RFC3339_full, snapshot.multimodal.last_updated,
316 absl::UTCTimeZone());
317 }
318 json["multimodal"] = std::move(multimodal_json);
319
320 std::error_code ec;
321 auto directory = path.parent_path();
322 if (!directory.empty()) {
323 std::filesystem::create_directories(directory, ec);
324 if (ec) {
325 return absl::InternalError(absl::StrFormat(
326 "Unable to create chat history directory: %s", ec.message()));
327 }
328 }
329
330 std::ofstream file(path);
331 if (!file.is_open()) {
332 return absl::InternalError("Cannot write chat history file");
333 }
334
335 file << json.dump(2);
336 return absl::OkStatus();
337#else
338 (void)path;
339 (void)snapshot;
340 return absl::UnimplementedError(
341 "Chat history persistence requires YAZE_WITH_GRPC=ON");
342#endif
343}
344
345} // namespace editor
346} // namespace yaze
static absl::Status Save(const std::filesystem::path &path, const Snapshot &snapshot)
static absl::StatusOr< Snapshot > Load(const std::filesystem::path &path)
Main namespace for the application.
std::vector< std::vector< std::string > > rows
std::optional< std::string > json_pretty
std::optional< ProposalSummary > proposal
std::optional< SessionMetrics > metrics
std::optional< std::filesystem::path > last_capture_path
std::vector< cli::agent::ChatMessage > history