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