dev merge

frdel committed Dec 8, 2024 at 20:29 UTC 50173ced3887c5aa17d3eda76d77811a45c05353
13 files changed +275 -192
python/api/import_knowledge.py
+16 -3
@@ -2,7 +2,7 @@ from python.helpers.api import ApiHandler
2 from flask import Request, Response
3
4 from python.helpers.file_browser import FileBrowser
5 -from python.helpers import files
5 +from python.helpers import files, memory
6 import os
7 from werkzeug.utils import secure_filename
8
@@ -12,8 +12,14 @@ class ImportKnowledge(ApiHandler):
12 if "files[]" not in request.files:
13 raise Exception("No files part")
14
15 + ctxid = request.form.get("ctxid", "")
16 + if not ctxid:
17 + raise Exception("No context id provided")
18 +
19 + context = self.get_context(ctxid)
20 +
21 file_list = request.files.getlist("files[]")
16 - KNOWLEDGE_FOLDER = files.get_abs_path("knowledge/custom/main")
22 + KNOWLEDGE_FOLDER = files.get_abs_path(memory.get_custom_knowledge_subdir_abs(context.agent0),"main")
23
24 saved_filenames = []
25
@@ -23,4 +29,11 @@ class ImportKnowledge(ApiHandler):
29 file.save(os.path.join(KNOWLEDGE_FOLDER, filename))
30 saved_filenames.append(filename)
31
26 - return {"message": "Knowledge Imported", "filenames": saved_filenames}
32 + #reload memory to re-import knowledge
33 + await memory.Memory.reload(context.agent0)
34 + context.log.set_initial_progress()
35 +
36 + return {
37 + "message": "Knowledge Imported",
38 + "filenames": saved_filenames[:5]
39 + }
\ No newline at end of file
python/api/nudge.py
-2
@@ -1,8 +1,6 @@
1 from python.helpers.api import ApiHandler
2 from flask import Request, Response
3
4 -from python.helpers import persist_chat
5 -
4 class Nudge(ApiHandler):
5 async def process(self, input: dict, request: Request) -> dict | Response:
6 ctxid = input.get("ctxid", "")
python/api/poll.py
+1
@@ -35,5 +35,6 @@ class Poll(ApiHandler):
35 "log_guid": context.log.guid,
36 "log_version": len(context.log.updates),
37 "log_progress": context.log.progress,
38 + "log_progress_active": context.log.progress_active,
39 "paused": context.paused,
40 }
\ No newline at end of file
python/extensions/message_loop_prompts/_50_recall_memories.py
+18 -9
@@ -1,29 +1,36 @@
1 +import asyncio
2 from python.helpers.extension import Extension
3 from python.helpers.memory import Memory
4 from agent import LoopData
5
6 +DATA_NAME_TASK = "_recall_memories_task"
7
8 class RecallMemories(Extension):
9
10 INTERVAL = 3
9 - HISTORY = 5 # TODO cleanup
11 + HISTORY = 5 # TODO cleanup
12 RESULTS = 3
13 THRESHOLD = 0.6
14
15 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
16
15 - if (
16 - loop_data.iteration % RecallMemories.INTERVAL == 0
17 - ): # every 3 iterations (or the first one) recall memories
18 - await self.search_memories(loop_data=loop_data, **kwargs)
17 + # every 3 iterations (or the first one) recall memories
18 + if loop_data.iteration % RecallMemories.INTERVAL == 0:
19 + task = asyncio.create_task(self.search_memories(loop_data=loop_data, **kwargs))
20 + else:
21 + task = None
22 +
23 + # set to agent to be able to wait for it
24 + self.agent.set_data(DATA_NAME_TASK, task)
25 +
26
27 async def search_memories(self, loop_data: LoopData, **kwargs):
28
22 - #cleanup
29 + # cleanup
30 extras = loop_data.extras_temporary
31 if "memories" in extras:
32 del extras["memories"]
26 -
33 +
34 # try:
35 # show temp info message
36 self.agent.context.log.log(
@@ -51,7 +58,9 @@ class RecallMemories(Extension):
58
59 # call util llm to summarize conversation
60 query = await self.agent.call_utility_llm(
54 - system=system, msg=loop_data.user_message.output_text() if loop_data.user_message else "", callback=log_callback
61 + system=system,
62 + msg=loop_data.user_message.output_text() if loop_data.user_message else "",
63 + callback=log_callback,
64 )
65
66 # get solutions database
@@ -91,7 +100,7 @@ class RecallMemories(Extension):
100
101 # append to prompt
102 extras["memories"] = memories_prompt
94 -
103 +
104 # except Exception as e:
105 # err = errors.format_error(e)
106 # self.agent.context.log.log(
python/extensions/message_loop_prompts/_51_recall_solutions.py
+10 -4
@@ -1,7 +1,9 @@
1 +import asyncio
2 from python.helpers.extension import Extension
3 from python.helpers.memory import Memory
4 from agent import LoopData
5
6 +DATA_NAME_TASK = "_recall_solutions_task"
7
8 class RecallSolutions(Extension):
9
@@ -13,10 +15,14 @@ class RecallSolutions(Extension):
15
16 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
17
16 - if (
17 - loop_data.iteration % RecallSolutions.INTERVAL == 0
18 - ): # every 3 iterations (or the first one) recall solution memories
19 - await self.search_solutions(loop_data=loop_data, **kwargs)
18 + # every 3 iterations (or the first one) recall memories
19 + if loop_data.iteration % RecallSolutions.INTERVAL == 0:
20 + task = asyncio.create_task(self.search_solutions(loop_data=loop_data, **kwargs))
21 + else:
22 + task = None
23 +
24 + # set to agent to be able to wait for it
25 + self.agent.set_data(DATA_NAME_TASK, task)
26
27 async def search_solutions(self, loop_data: LoopData, **kwargs):
28
python/extensions/message_loop_prompts/_90_organize_history_wait.py
+2 -7
@@ -15,7 +15,7 @@ class OrganizeHistoryWait(Extension):
15 # Check if the task is already done
16 if task:
17 if not task.done():
18 - self.log()
18 + self.agent.context.log.set_progress("Compressing history...")
19
20 # Wait for the task to complete
21 await task
@@ -24,11 +24,6 @@ class OrganizeHistoryWait(Extension):
24 self.agent.set_data(DATA_NAME_TASK, None)
25 else:
26 # no task running, start and wait
27 - self.log()
27 + self.agent.context.log.set_progress("Compressing history...")
28 await self.agent.history.compress()
29
30 - def log(self):
31 - if not hasattr(self, 'log_item') or not self.log_item:
32 - self.log_item = self.agent.context.log.log(
33 - type="util", heading="Waiting for history to be compressed..."
34 - )
python/extensions/message_loop_prompts/_91_recall_wait.py new
+19
@@ -0,0 +1,19 @@
1 +from python.helpers.extension import Extension
2 +from agent import LoopData
3 +from python.extensions.message_loop_prompts._50_recall_memories import DATA_NAME_TASK as DATA_NAME_TASK_MEMORIES
4 +from python.extensions.message_loop_prompts._51_recall_solutions import DATA_NAME_TASK as DATA_NAME_TASK_SOLUTIONS
5 +
6 +
7 +class RecallWait(Extension):
8 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
9 +
10 + task = self.agent.get_data(DATA_NAME_TASK_MEMORIES)
11 + if task and not task.done():
12 + self.agent.context.log.set_progress("Recalling memories...")
13 + await task
14 +
15 + task = self.agent.get_data(DATA_NAME_TASK_SOLUTIONS)
16 + if task and not task.done():
17 + self.agent.context.log.set_progress("Recalling solutions...")
18 + await task
19 +
python/extensions/monologue_end/_90_waiting_for_input_msg.py
+1 -3
@@ -6,7 +6,5 @@ class WaitingForInputMsg(Extension):
6 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
7 # show temp info message
8 if self.agent.number == 0:
9 - self.agent.context.log.log(
10 - type="util", heading="Waiting for input", temp=True
11 - )
9 + self.agent.context.log.set_initial_progress()
10
python/helpers/log.py
+162 -155
@@ -5,165 +5,172 @@ import uuid
5 from collections import OrderedDict # Import OrderedDict
6
7 Type = Literal[
8 - "agent",
9 - "code_exe",
10 - "error",
11 - "hint",
12 - "info",
13 - "progress",
14 - "response",
15 - "tool",
16 - "user",
17 - "util",
18 - "warning",
8 + "agent",
9 + "code_exe",
10 + "error",
11 + "hint",
12 + "info",
13 + "progress",
14 + "response",
15 + "tool",
16 + "input",
17 + "user",
18 + "util",
19 + "warning",
20 ]
21
22 @dataclass
23 class LogItem:
23 - log: "Log"
24 - no: int
25 - type: str
26 - heading: str
27 - content: str
28 - temp: bool
29 - kvps: Optional[OrderedDict] = None # Use OrderedDict for kvps
30 - id: Optional[str] = None # Add id field
31 - guid: str = ""
32 -
33 - def __post_init__(self):
34 - self.guid = self.log.guid
35 -
36 - def update(
37 - self,
38 - type: Type | None = None,
39 - heading: str | None = None,
40 - content: str | None = None,
41 - kvps: dict | None = None,
42 - temp: bool | None = None,
43 - **kwargs,
44 - ):
45 - if self.guid == self.log.guid:
46 - self.log.update_item(
47 - self.no,
48 - type=type,
49 - heading=heading,
50 - content=content,
51 - kvps=kvps,
52 - temp=temp,
53 - **kwargs,
54 - )
55 -
56 - def stream(self, heading: str | None = None, content: str | None = None, **kwargs):
57 - if heading is not None:
58 - self.update(heading=self.heading + heading)
59 - if content is not None:
60 - self.update(content=self.content + content)
61 -
62 - for k, v in kwargs.items():
63 - prev = self.kvps.get(k, "") if self.kvps else ""
64 - self.update(**{k: prev + v})
65 -
66 - def output(self):
67 - return {
68 - "no": self.no,
69 - "id": self.id, # Include id in output
70 - "type": self.type,
71 - "heading": self.heading,
72 - "content": self.content,
73 - "temp": self.temp,
74 - "kvps": self.kvps,
75 - }
24 + log: "Log"
25 + no: int
26 + type: str
27 + heading: str
28 + content: str
29 + temp: bool
30 + kvps: Optional[OrderedDict] = None # Use OrderedDict for kvps
31 + id: Optional[str] = None # Add id field
32 + guid: str = ""
33 +
34 + def __post_init__(self):
35 + self.guid = self.log.guid
36 +
37 + def update(
38 + self,
39 + type: Type | None = None,
40 + heading: str | None = None,
41 + content: str | None = None,
42 + kvps: dict | None = None,
43 + temp: bool | None = None,
44 + **kwargs,
45 + ):
46 + if self.guid == self.log.guid:
47 + self.log.update_item(
48 + self.no,
49 + type=type,
50 + heading=heading,
51 + content=content,
52 + kvps=kvps,
53 + temp=temp,
54 + **kwargs,
55 + )
56 +
57 + def stream(self, heading: str | None = None, content: str | None = None, **kwargs):
58 + if heading is not None:
59 + self.update(heading=self.heading + heading)
60 + if content is not None:
61 + self.update(content=self.content + content)
62 +
63 + for k, v in kwargs.items():
64 + prev = self.kvps.get(k, "") if self.kvps else ""
65 + self.update(**{k: prev + v})
66 +
67 + def output(self):
68 + return {
69 + "no": self.no,
70 + "id": self.id, # Include id in output
71 + "type": self.type,
72 + "heading": self.heading,
73 + "content": self.content,
74 + "temp": self.temp,
75 + "kvps": self.kvps,
76 + }
77
78 class Log:
79
79 - def __init__(self):
80 - self.guid: str = str(uuid.uuid4())
81 - self.updates: list[int] = []
82 - self.logs: list[LogItem] = []
83 - self.progress = ""
84 - self.progress_no = 0
85 -
86 - def log(
87 - self,
88 - type: Type,
89 - heading: str | None = None,
90 - content: str | None = None,
91 - kvps: dict | None = None,
92 - temp: bool | None = None,
93 - id: Optional[str] = None, # Add id parameter
94 - ) -> LogItem:
95 - # Use OrderedDict if kvps is provided
96 - if kvps is not None:
97 - kvps = OrderedDict(kvps)
98 - item = LogItem(
99 - log=self,
100 - no=len(self.logs),
101 - type=type,
102 - heading=heading or "",
103 - content=content or "",
104 - kvps=kvps,
105 - temp=temp or False,
106 - id=id, # Pass id to LogItem
107 - )
108 - self.logs.append(item)
109 - self.updates += [item.no]
110 - if heading and item.no >= self.progress_no:
111 - self.progress = heading
112 - self.progress_no = item.no
113 - return item
114 -
115 - def update_item(
116 - self,
117 - no: int,
118 - type: str | None = None,
119 - heading: str | None = None,
120 - content: str | None = None,
121 - kvps: dict | None = None,
122 - temp: bool | None = None,
123 - **kwargs,
124 - ):
125 - item = self.logs[no]
126 - if type is not None:
127 - item.type = type
128 - if heading is not None:
129 - item.heading = heading
130 - if no >= self.progress_no:
131 - self.progress = heading
132 - self.progress_no = no
133 - if content is not None:
134 - item.content = content
135 - if kvps is not None:
136 - item.kvps = OrderedDict(kvps) # Use OrderedDict to keep the order
137 -
138 - if temp is not None:
139 - item.temp = temp
140 -
141 - if kwargs:
142 - if item.kvps is None:
143 - item.kvps = OrderedDict() # Ensure kvps is an OrderedDict
144 - for k, v in kwargs.items():
145 - item.kvps[k] = v
146 -
147 - self.updates += [item.no]
148 -
149 - def output(self, start=None, end=None):
150 - if start is None:
151 - start = 0
152 - if end is None:
153 - end = len(self.updates)
154 -
155 - out = []
156 - seen = set()
157 - for update in self.updates[start:end]:
158 - if update not in seen:
159 - out.append(self.logs[update].output())
160 - seen.add(update)
161 -
162 - return out
163 -
164 - def reset(self):
165 - self.guid = str(uuid.uuid4())
166 - self.updates = []
167 - self.logs = []
168 - self.progress = ""
169 - self.progress_no = 0
\ No newline at end of file
80 + def __init__(self):
81 + self.guid: str = str(uuid.uuid4())
82 + self.updates: list[int] = []
83 + self.logs: list[LogItem] = []
84 + self.set_initial_progress()
85 +
86 + def log(
87 + self,
88 + type: Type,
89 + heading: str | None = None,
90 + content: str | None = None,
91 + kvps: dict | None = None,
92 + temp: bool | None = None,
93 + id: Optional[str] = None, # Add id parameter
94 + ) -> LogItem:
95 + # Use OrderedDict if kvps is provided
96 + if kvps is not None:
97 + kvps = OrderedDict(kvps)
98 + item = LogItem(
99 + log=self,
100 + no=len(self.logs),
101 + type=type,
102 + heading=heading or "",
103 + content=content or "",
104 + kvps=kvps,
105 + temp=temp or False,
106 + id=id, # Pass id to LogItem
107 + )
108 + self.logs.append(item)
109 + self.updates += [item.no]
110 + if heading and item.no >= self.progress_no:
111 + self.set_progress(heading, item.no)
112 + return item
113 +
114 + def update_item(
115 + self,
116 + no: int,
117 + type: str | None = None,
118 + heading: str | None = None,
119 + content: str | None = None,
120 + kvps: dict | None = None,
121 + temp: bool | None = None,
122 + **kwargs,
123 + ):
124 + item = self.logs[no]
125 + if type is not None:
126 + item.type = type
127 + if heading is not None:
128 + item.heading = heading
129 + if no >= self.progress_no:
130 + self.set_progress(heading, no)
131 + if content is not None:
132 + item.content = content
133 + if kvps is not None:
134 + item.kvps = OrderedDict(kvps) # Use OrderedDict to keep the order
135 +
136 + if temp is not None:
137 + item.temp = temp
138 +
139 + if kwargs:
140 + if item.kvps is None:
141 + item.kvps = OrderedDict() # Ensure kvps is an OrderedDict
142 + for k, v in kwargs.items():
143 + item.kvps[k] = v
144 +
145 + self.updates += [item.no]
146 +
147 + def set_progress(self, progress: str, no: int = 0, active: bool = True):
148 + self.progress = progress
149 + if not no:
150 + no = len(self.logs)
151 + self.progress_no = no
152 + self.progress_active = active
153 +
154 + def set_initial_progress(self):
155 + self.set_progress("Waiting for input", 0, False)
156 +
157 + def output(self, start=None, end=None):
158 + if start is None:
159 + start = 0
160 + if end is None:
161 + end = len(self.updates)
162 +
163 + out = []
164 + seen = set()
165 + for update in self.updates[start:end]:
166 + if update not in seen:
167 + out.append(self.logs[update].output())
168 + seen.add(update)
169 +
170 + return out
171 +
172 + def reset(self):
173 + self.guid = str(uuid.uuid4())
174 + self.updates = []
175 + self.logs = []
176 + self.set_initial_progress()
python/helpers/memory.py
+18 -1
@@ -72,6 +72,13 @@ class Memory:
72 memory_subdir=memory_subdir,
73 )
74
75 + @staticmethod
76 + async def reload(agent: Agent):
77 + memory_subdir = agent.config.memory_subdir or "default"
78 + if Memory.index.get(memory_subdir):
79 + del Memory.index[memory_subdir]
80 + return await Memory.get(agent)
81 +
82 @staticmethod
83 def initialize(
84 log_item: LogItem | None,
@@ -151,6 +158,9 @@ class Memory:
158 async def preload_knowledge(
159 self, log_item: LogItem | None, kn_dirs: list[str], memory_subdir: str
160 ):
161 + if log_item:
162 + log_item.update(heading="Preloading knowledge...")
163 +
164 # db abs path
165 db_dir = Memory._abs_db_dir(memory_subdir)
166
@@ -351,5 +361,12 @@ class Memory:
361 def get_timestamp():
362 return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
363
364 +
365 def get_memory_subdir_abs(agent: Agent) -> str:
355 - return files.get_abs_path("memory", agent.config.memory_subdir or "default")
\ No newline at end of file
366 + return files.get_abs_path("memory", agent.config.memory_subdir or "default")
367 +
368 +def get_custom_knowledge_subdir_abs(agent: Agent) -> str:
369 + for dir in agent.config.knowledge_subdirs:
370 + if dir != "default":
371 + return files.get_abs_path("knowledge", dir)
372 + raise Exception("No custom knowledge subdir set")
python/helpers/persist_chat.py
+1 -2
@@ -166,8 +166,7 @@ def _deserialize_agents(
166 def _deserialize_log(data: dict[str, Any]) -> "Log":
167 log = Log()
168 log.guid = data.get("guid", str(uuid.uuid4()))
169 - log.progress = "" # data.get("progress", "")
170 - log.progress_no = data.get("progress_no", 0)
169 + log.set_initial_progress()
170
171 # Deserialize the list of LogItem objects
172 i = 0
python/helpers/settings.py
+1 -1
@@ -703,7 +703,7 @@ def get_default_settings() -> Settings:
703 chat_model_temperature=0,
704 chat_model_kwargs={},
705 chat_model_ctx_length=8192,
706 - chat_model_ctx_history=0.65,
706 + chat_model_ctx_history=0.7,
707 util_model_provider=ModelProvider.OPENAI.name,
708 util_model_name="gpt-4o-mini",
709 util_model_temperature=0,
webui/index.js
+26 -5
@@ -269,6 +269,8 @@ window.loadKnowledge = async function () {
269 formData.append('files[]', file);
270 }
271
272 + formData.append('ctxid', getContext());
273 +
274 const response = await fetch('/import_knowledge', {
275 method: 'POST',
276 body: formData,
@@ -357,7 +359,7 @@ async function poll() {
359 afterMessagesUpdate(response.logs)
360 }
361
360 - updateProgress(response.log_progress)
362 + updateProgress(response.log_progress, response.log_progress_active)
363
364 //set ui model vars from backend
365 const inputAD = Alpine.$data(inputSection);
@@ -400,11 +402,30 @@ function speakMessages(logs) {
402 }
403 }
404
403 -function updateProgress(progress) {
404 - const defaultText = "Waiting for input"
405 - if (!progress) progress = defaultText
405 +function afterMessagesUpdate(logs) {
406 + if (localStorage.getItem('speech') == 'true') {
407 + speakMessages(logs)
408 + }
409 +}
410 +
411 +function speakMessages(logs) {
412 + // log.no, log.type, log.heading, log.content
413 + for (let i = logs.length - 1; i >= 0; i--) {
414 + const log = logs[i]
415 + if (log.type == "response") {
416 + if (log.no > lastSpokenNo) {
417 + lastSpokenNo = log.no
418 + speech.speak(log.content)
419 + return
420 + }
421 + }
422 + }
423 +}
424 +
425 +function updateProgress(progress, active) {
426 + if (!progress) progress = ""
427
407 - if (progress == defaultText) {
428 + if (!active) {
429 removeClassFromElement(progressBar, "shiny-text")
430 } else {
431 addClassToElement(progressBar, "shiny-text")