frdel commited on
Commit
a7c827a
·
1 Parent(s): 5b177ea

Persistent chats update

Browse files

save/load
autosave/autoload

python/extensions/message_loop_end/_90_save_chat.py CHANGED
@@ -5,4 +5,4 @@ from python.helpers import persist_chat
5
 
6
  class SaveChat(Extension):
7
  async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
8
- persist_chat.save_chat(self.agent.context)
 
5
 
6
  class SaveChat(Extension):
7
  async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
8
+ persist_chat.save_tmp_chat(self.agent.context)
python/helpers/persist_chat.py CHANGED
@@ -12,21 +12,35 @@ CHATS_FOLDER = "tmp/chats"
12
  LOG_SIZE = 1000
13
 
14
 
15
- def save_chat(context: AgentContext):
16
  relative_path = _get_file_path(context.id)
17
  data = _serialize_context(context)
18
  js = _safe_json_serialize(data, ensure_ascii=False)
19
  files.write_file(relative_path, js)
20
 
21
-
22
- def load_chats():
23
  json_files = files.list_files("tmp/chats", "*.json")
 
24
  for file in json_files:
25
  path = files.get_abs_path(CHATS_FOLDER, file)
26
  js = files.read_file(path)
27
  data = json.loads(js)
28
  ctx = _deserialize_context(data)
 
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  def remove_chat(ctxid):
32
  files.delete_file(_get_file_path(ctxid))
@@ -91,20 +105,19 @@ def _deserialize_context(data):
91
  id=data.get("id", None),
92
  name=data.get("name", None),
93
  log=log,
94
- paused=True,
95
  # agent0=agent0,
96
  # streaming_agent=straming_agent,
97
  )
98
 
99
  agents = data.get("agents", [])
100
  agent0 = _deserialize_agents(agents, config, context)
101
- streaming_agent_no = data.get("streaming_agent", 0)
102
- straming_agent = (
103
- agents[streaming_agent_no] if streaming_agent_no < len(agents) else None
104
- )
105
-
106
  context.agent0 = agent0
107
- context.streaming_agent = straming_agent
108
 
109
  return context
110
 
 
12
  LOG_SIZE = 1000
13
 
14
 
15
+ def save_tmp_chat(context: AgentContext):
16
  relative_path = _get_file_path(context.id)
17
  data = _serialize_context(context)
18
  js = _safe_json_serialize(data, ensure_ascii=False)
19
  files.write_file(relative_path, js)
20
 
21
+ def load_tmp_chats():
 
22
  json_files = files.list_files("tmp/chats", "*.json")
23
+ ctxids = []
24
  for file in json_files:
25
  path = files.get_abs_path(CHATS_FOLDER, file)
26
  js = files.read_file(path)
27
  data = json.loads(js)
28
  ctx = _deserialize_context(data)
29
+ ctxids.append(ctx.id)
30
+ return ctxids
31
 
32
+ def load_json_chats(jsons: list[str]):
33
+ ctxids = []
34
+ for js in jsons:
35
+ data = json.loads(js)
36
+ ctx = _deserialize_context(data)
37
+ ctxids.append(ctx.id)
38
+ return ctxids
39
+
40
+ def export_json_chat(context: AgentContext):
41
+ data = _serialize_context(context)
42
+ js = _safe_json_serialize(data, ensure_ascii=False)
43
+ return js
44
 
45
  def remove_chat(ctxid):
46
  files.delete_file(_get_file_path(ctxid))
 
105
  id=data.get("id", None),
106
  name=data.get("name", None),
107
  log=log,
108
+ paused=False,
109
  # agent0=agent0,
110
  # streaming_agent=straming_agent,
111
  )
112
 
113
  agents = data.get("agents", [])
114
  agent0 = _deserialize_agents(agents, config, context)
115
+ streaming_agent = agent0
116
+ while streaming_agent.number != data.get("streaming_agent", 0):
117
+ streaming_agent = streaming_agent.data.get("subordinate", None)
118
+
 
119
  context.agent0 = agent0
120
+ context.streaming_agent = streaming_agent
121
 
122
  return context
123
 
run_ui.py CHANGED
@@ -17,7 +17,7 @@ from python.helpers import persist_chat
17
 
18
  # initialize the internal Flask server
19
  app = Flask("app", static_folder=get_abs_path("./webui"), static_url_path="/")
20
- app.config['JSON_SORT_KEYS'] = False # Disable key sorting in jsonify
21
 
22
  lock = threading.Lock()
23
 
@@ -175,6 +175,66 @@ async def pause():
175
  return jsonify(response)
176
 
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  # restarting with new agent0
179
  @app.route("/reset", methods=["POST"])
180
  async def reset():
@@ -187,7 +247,7 @@ async def reset():
187
  # context instance - get or create
188
  context = get_context(ctxid)
189
  context.reset()
190
- persist_chat.save_chat(context)
191
 
192
  response = {
193
  "ok": True,
@@ -287,15 +347,16 @@ async def poll():
287
  return Response(response=response_json, status=200, mimetype="application/json")
288
  # return jsonify(response)
289
 
 
290
  def run():
291
- print("Initializing framework...")
292
 
293
- #load env vars
294
  load_dotenv()
295
 
296
  # initialize contexts from persisted chats
297
- persist_chat.load_chats()
298
-
299
  # Suppress only request logs but keep the startup messages
300
  from werkzeug.serving import WSGIRequestHandler
301
 
@@ -310,4 +371,4 @@ def run():
310
 
311
  # run the internal server
312
  if __name__ == "__main__":
313
- run()
 
17
 
18
  # initialize the internal Flask server
19
  app = Flask("app", static_folder=get_abs_path("./webui"), static_url_path="/")
20
+ app.config["JSON_SORT_KEYS"] = False # Disable key sorting in jsonify
21
 
22
  lock = threading.Lock()
23
 
 
175
  return jsonify(response)
176
 
177
 
178
+ # load chats from json
179
+ @app.route("/loadChats", methods=["POST"])
180
+ async def load_chats():
181
+ try:
182
+ # data sent to the server
183
+ input = request.get_json()
184
+ chats = input.get("chats", [])
185
+ if not chats:
186
+ raise Exception("No chats provided")
187
+
188
+ ctxids = persist_chat.load_json_chats(chats)
189
+
190
+ response = {
191
+ "ok": True,
192
+ "message": "Chats loaded.",
193
+ "ctxids": ctxids,
194
+ }
195
+
196
+ except Exception as e:
197
+ response = {
198
+ "ok": False,
199
+ "message": str(e),
200
+ }
201
+ PrintStyle.error(str(e))
202
+
203
+ # respond with json
204
+ return jsonify(response)
205
+
206
+
207
+ # load chats from json
208
+ @app.route("/exportChat", methods=["POST"])
209
+ async def export_chat():
210
+ try:
211
+ # data sent to the server
212
+ input = request.get_json()
213
+ ctxid = input.get("ctxid", "")
214
+ if not ctxid:
215
+ raise Exception("No context id provided")
216
+
217
+ context = get_context(ctxid)
218
+ content = persist_chat.export_json_chat(context)
219
+
220
+ response = {
221
+ "ok": True,
222
+ "message": "Chats loaded.",
223
+ "ctxid": context.id,
224
+ "content": content,
225
+ }
226
+
227
+ except Exception as e:
228
+ response = {
229
+ "ok": False,
230
+ "message": str(e),
231
+ }
232
+ PrintStyle.error(str(e))
233
+
234
+ # respond with json
235
+ return jsonify(response)
236
+
237
+
238
  # restarting with new agent0
239
  @app.route("/reset", methods=["POST"])
240
  async def reset():
 
247
  # context instance - get or create
248
  context = get_context(ctxid)
249
  context.reset()
250
+ persist_chat.save_tmp_chat(context)
251
 
252
  response = {
253
  "ok": True,
 
347
  return Response(response=response_json, status=200, mimetype="application/json")
348
  # return jsonify(response)
349
 
350
+
351
  def run():
352
+ print("Initializing framework...")
353
 
354
+ # load env vars
355
  load_dotenv()
356
 
357
  # initialize contexts from persisted chats
358
+ persist_chat.load_tmp_chats()
359
+
360
  # Suppress only request logs but keep the startup messages
361
  from werkzeug.serving import WSGIRequestHandler
362
 
 
371
 
372
  # run the internal server
373
  if __name__ == "__main__":
374
+ run()
webui/index.css CHANGED
@@ -502,6 +502,8 @@ h4 {
502
  font-family: "Rubik", Arial, Helvetica, sans-serif;
503
  font-size: var(--font-size-small);
504
  margin-top: 0;
 
 
505
  padding: var(--spacing-sm) 0.75rem;
506
  text-wrap: nowrap;
507
  background-color: var(--color-secondary);
 
502
  font-family: "Rubik", Arial, Helvetica, sans-serif;
503
  font-size: var(--font-size-small);
504
  margin-top: 0;
505
+ margin-bottom: var(--spacing-xs);
506
+ /* margin-right: var(--spacing-xs); */
507
  padding: var(--spacing-sm) 0.75rem;
508
  text-wrap: nowrap;
509
  background-color: var(--color-secondary);
webui/index.html CHANGED
@@ -45,6 +45,8 @@
45
  <h3>Quick Actions</h3>
46
  <button class="config-button" id="resetChat" @click="resetChat()">Reset chat</button>
47
  <button class="config-button" id="newChat" @click="newChat()">New Chat</button>
 
 
48
  </div>
49
 
50
  <div class="config-section" id="chats-section" x-data="{ contexts: [], selected: '' }"
 
45
  <h3>Quick Actions</h3>
46
  <button class="config-button" id="resetChat" @click="resetChat()">Reset chat</button>
47
  <button class="config-button" id="newChat" @click="newChat()">New Chat</button>
48
+ <button class="config-button" id="loadChats" @click="loadChats()">Load Chat</button>
49
+ <button class="config-button" id="loadChat" @click="saveChat()">Save Chat</button>
50
  </div>
51
 
52
  <div class="config-section" id="chats-section" x-data="{ contexts: [], selected: '' }"
webui/index.js CHANGED
@@ -373,6 +373,115 @@ function toggleCssProperty(selector, property, value) {
373
  }
374
  }
375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  function toast(text, type = 'info') {
377
  const toast = document.getElementById('toast');
378
 
 
373
  }
374
  }
375
 
376
+ window.loadChats = async function () {
377
+ try {
378
+ const fileContents = await readJsonFiles();
379
+ const response = await sendJsonData("/loadChats", { chats: fileContents });
380
+
381
+ if (!response) {
382
+ toast("No response returned.", "error")
383
+ } else if (!response.ok) {
384
+ if (response.message) {
385
+ toast(response.message, "error")
386
+ } else {
387
+ toast("Undefined error.", "error")
388
+ }
389
+ } else {
390
+ setContext(response.ctxids[0])
391
+ toast("Chats loaded.", "success")
392
+ }
393
+
394
+ } catch (e) {
395
+ toast(e.message, "error")
396
+ }
397
+ }
398
+
399
+ window.saveChat = async function () {
400
+ try {
401
+ const response = await sendJsonData("/exportChat", { ctxid: context });
402
+
403
+ if (!response) {
404
+ toast("No response returned.", "error")
405
+ } else if (!response.ok) {
406
+ if (response.message) {
407
+ toast(response.message, "error")
408
+ } else {
409
+ toast("Undefined error.", "error")
410
+ }
411
+ } else {
412
+ downloadFile(response.ctxid + ".json", response.content)
413
+ toast("Chat file downloaded.", "success")
414
+ }
415
+
416
+ } catch (e) {
417
+ toast(e.message, "error")
418
+ }
419
+ }
420
+
421
+ function downloadFile(filename, content) {
422
+ // Create a Blob with the content to save
423
+ const blob = new Blob([content], { type: 'application/json' });
424
+
425
+ // Create a link element
426
+ const link = document.createElement('a');
427
+
428
+ // Create a URL for the Blob
429
+ const url = URL.createObjectURL(blob);
430
+ link.href = url;
431
+
432
+ // Set the file name for download
433
+ link.download = filename;
434
+
435
+ // Programmatically click the link to trigger the download
436
+ link.click();
437
+
438
+ // Clean up by revoking the object URL
439
+ setTimeout(() => {
440
+ URL.revokeObjectURL(url);
441
+ }, 0);
442
+ }
443
+
444
+
445
+ function readJsonFiles() {
446
+ return new Promise((resolve, reject) => {
447
+ // Create an input element of type 'file'
448
+ const input = document.createElement('input');
449
+ input.type = 'file';
450
+ input.accept = '.json'; // Only accept JSON files
451
+ input.multiple = true; // Allow multiple file selection
452
+
453
+ // Trigger the file dialog
454
+ input.click();
455
+
456
+ // When files are selected
457
+ input.onchange = async () => {
458
+ const files = input.files;
459
+ if (!files.length) {
460
+ resolve([]); // Return an empty array if no files are selected
461
+ return;
462
+ }
463
+
464
+ // Read each file as a string and store in an array
465
+ const filePromises = Array.from(files).map(file => {
466
+ return new Promise((fileResolve, fileReject) => {
467
+ const reader = new FileReader();
468
+ reader.onload = () => fileResolve(reader.result);
469
+ reader.onerror = fileReject;
470
+ reader.readAsText(file);
471
+ });
472
+ });
473
+
474
+ try {
475
+ const fileContents = await Promise.all(filePromises);
476
+ resolve(fileContents);
477
+ } catch (error) {
478
+ reject(error); // In case of any file reading error
479
+ }
480
+ };
481
+ });
482
+ }
483
+
484
+
485
  function toast(text, type = 'info') {
486
  const toast = document.getElementById('toast');
487