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")