Persistent chats update
save/load autosave/autoload
frdel committed
Oct 14, 2024 at 17:04 UTC
d9b0491d84b591b8add8f93c2844303727fb99f8
6 files changed
+205
-18
python/extensions/message_loop_end/_90_save_chat.py
+1
-1
@@ -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)
\ No newline at end of file
8
+ persist_chat.save_tmp_chat(self.agent.context)
\ No newline at end of file
python/helpers/persist_chat.py
+23
-10
@@ -12,21 +12,35 @@ CHATS_FOLDER = "tmp/chats"
12
LOG_SIZE = 1000
13
14
15
-def save_chat(context: AgentContext):
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
-
22
-def load_chats():
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))
@@ -91,20 +105,19 @@ def _deserialize_context(data):
105
id=data.get("id", None),
106
name=data.get("name", None),
107
log=log,
94
- paused=True,
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)
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
-
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
107
- context.streaming_agent = straming_agent
120
+ context.streaming_agent = streaming_agent
121
122
return context
123
run_ui.py
+68
-7
@@ -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
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
+# 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():
@@ -187,7 +247,7 @@ async def reset():
247
# context instance - get or create
248
context = get_context(ctxid)
249
context.reset()
190
- persist_chat.save_chat(context)
250
+ persist_chat.save_tmp_chat(context)
251
252
response = {
253
"ok": True,
@@ -287,15 +347,16 @@ async def poll():
347
return Response(response=response_json, status=200, mimetype="application/json")
348
# return jsonify(response)
349
350
+
351
def run():
291
- print("Initializing framework...")
352
+ print("Initializing framework...")
353
293
- #load env vars
354
+ # load env vars
355
load_dotenv()
356
357
# initialize contexts from persisted chats
297
- persist_chat.load_chats()
298
-
358
+ persist_chat.load_tmp_chats()
359
+
360
# Suppress only request logs but keep the startup messages
361
from werkzeug.serving import WSGIRequestHandler
362
@@ -310,4 +371,4 @@ def run():
371
372
# run the internal server
373
if __name__ == "__main__":
313
- run()
\ No newline at end of file
374
+ run()
webui/index.css
+2
@@ -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
+ 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
+2
@@ -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
+ <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
+109
@@ -373,6 +373,115 @@ function toggleCssProperty(selector, property, value) {
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