attachments, files, prompt extras, prompt caching, refactors, cleanups
frdel committed
Dec 1, 2024 at 20:50 UTC
19f50d6d9509acdaea2a5ccd846b5de2722b4a07
24 files changed
+235
-218
agent.py
+110
-124
@@ -1,4 +1,5 @@
1
import asyncio
2
+from collections import OrderedDict
3
from dataclasses import dataclass, field
4
import time, importlib, inspect, os, json
5
from typing import Any, Optional, Dict, TypedDict
@@ -73,7 +74,7 @@ class AgentContext:
74
self.streaming_agent = None
75
self.paused = False
76
76
- def communicate(self, msg: str, broadcast_level: int = 1):
77
+ def communicate(self, msg: "UserMessage", broadcast_level: int = 1):
78
self.paused = False # unpause if paused
79
80
if self.streaming_agent:
@@ -85,9 +86,11 @@ class AgentContext:
86
# set intervention messages to agent(s):
87
intervention_agent = current_agent
88
while intervention_agent and broadcast_level != 0:
88
- intervention_agent.intervention_message = msg
89
+ intervention_agent.intervention = msg
90
broadcast_level -= 1
90
- intervention_agent = intervention_agent.data.get(Agent.DATA_NAME_SUPERIOR, None)
91
+ intervention_agent = intervention_agent.data.get(
92
+ Agent.DATA_NAME_SUPERIOR, None
93
+ )
94
else:
95
96
# self.process = DeferredTask(current_agent.monologue, msg)
@@ -96,13 +99,13 @@ class AgentContext:
99
return self.process
100
101
# this wrapper ensures that superior agents are called back if the chat was loaded from file and original callstack is gone
99
- async def _process_chain(self, agent: "Agent", msg: str, user=True):
102
+ async def _process_chain(self, agent: "Agent", msg: "UserMessage|str", user=True):
103
try:
104
msg_template = (
102
- await agent.hist_add_user_message(msg)
105
+ await agent.hist_add_user_message(msg) # type: ignore
106
if user
107
else await agent.hist_add_tool_result(
105
- tool_name="call_subordinate", tool_result=msg
108
+ tool_name="call_subordinate", tool_result=msg # type: ignore
109
)
110
)
111
response = await agent.monologue()
@@ -132,33 +135,40 @@ class AgentConfig:
135
response_timeout_seconds: int = 60
136
max_tool_response_length: int = 3000
137
code_exec_docker_enabled: bool = True
135
- code_exec_docker_name: str = "agent-zero-exe"
136
- code_exec_docker_image: str = "frdel/agent-zero-exe:latest"
138
+ code_exec_docker_name: str = "agent-zero-dev"
139
+ code_exec_docker_image: str = "frdel/agent-zero-run:development"
140
code_exec_docker_ports: dict[str, int] = field(
138
- default_factory=lambda: {"22/tcp": 50022}
141
+ default_factory=lambda: {"22/tcp": 55022, "80/tcp": 55080}
142
)
143
code_exec_docker_volumes: dict[str, dict[str, str]] = field(
144
default_factory=lambda: {
145
+ files.get_base_dir(): {"bind": "/a0", "mode": "rw"},
146
files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"},
143
- files.get_abs_path("instruments"): {"bind": "/instruments", "mode": "rw"},
147
}
148
)
149
code_exec_ssh_enabled: bool = True
150
code_exec_ssh_addr: str = "localhost"
148
- code_exec_ssh_port: int = 50022
151
+ code_exec_ssh_port: int = 55022
152
code_exec_ssh_user: str = "root"
153
code_exec_ssh_pass: str = "toor"
154
additional: Dict[str, Any] = field(default_factory=dict)
155
156
157
+@dataclass
158
+class UserMessage:
159
+ message: str
160
+ attachments: list[str]
161
+
162
+
163
class LoopData:
164
def __init__(self, **kwargs):
165
self.iteration = -1
166
self.system = []
167
self.user_message: history.Message | None = None
168
self.history_output: list[history.OutputMessage] = []
169
+ self.extras_temporary: OrderedDict[str, history.MessageContent] = OrderedDict()
170
+ self.extras_persistent: OrderedDict[str, history.MessageContent] = OrderedDict()
171
self.last_response = ""
161
- self.attachments = [] # Add attachments field
172
173
# override values with kwargs
174
for key, value in kwargs.items():
@@ -201,7 +211,7 @@ class Agent:
211
212
self.history = history.History(self)
213
self.last_user_message: history.Message | None = None
204
- self.intervention_message = ""
214
+ self.intervention: UserMessage | None = None
215
self.rate_limiter = rate_limiter.RateLimiter(
216
self.context.log,
217
max_calls=self.config.rate_limit_requests,
@@ -221,18 +231,6 @@ class Agent:
231
232
printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False)
233
224
- # # Include attachments in user message if available
225
- # if loop_data.attachments:
226
- # user_message += "\n" + "\n".join(
227
- # loop_data.attachments
228
- # ) # Add attachments to message
229
- # loop_data.attachments = (
230
- # []
231
- # ) # Clear attachments after adding to message
232
- # TODO attachments to extension
233
-
234
- # await self.hist_add_user_message(message=self.loop_data.message)
235
-
234
# let the agent run message loop until he stops it with a response tool
235
while True:
236
@@ -241,34 +239,13 @@ class Agent:
239
self.loop_data.iteration += 1
240
241
try:
244
-
245
- # set system prompt and message history
246
- self.loop_data.system = []
247
- self.loop_data.history_output = self.history.output()
248
-
249
- # and allow extensions to edit them
250
- await self.call_extensions(
251
- "message_loop_prompts", loop_data=self.loop_data
252
- )
253
-
254
- # build chain from system prompt, message history and model
255
- prompt = ChatPromptTemplate.from_messages(
256
- [
257
- SystemMessage(
258
- content="\n\n".join(self.loop_data.system)
259
- ),
260
- MessagesPlaceholder(variable_name="messages"),
261
- ]
262
- )
263
- chain = prompt | self.config.chat_model
264
-
265
- # convert history to LLM format
266
- history_langchain = history.output_langchain(
267
- self.loop_data.history_output
242
+ # prepare LLM chain (model, system, history)
243
+ chain, prompt = await self.prepare_chain(
244
+ loop_data=self.loop_data
245
)
246
247
# rate limiter TODO - move to extension, make per-model
271
- formatted_inputs = prompt.format(messages=history_langchain)
248
+ formatted_inputs = prompt.format()
249
self.set_data(self.DATA_NAME_CTX_WINDOW, formatted_inputs)
250
token_count = tokens.approximate_tokens(formatted_inputs)
251
self.rate_limiter.limit_call_and_input(token_count)
@@ -284,9 +261,7 @@ class Agent:
261
type="agent", heading=f"{self.agent_name}: Generating"
262
)
263
287
- async for chunk in chain.astream(
288
- {"messages": history_langchain}
289
- ):
264
+ async for chunk in chain.astream({}):
265
# wait for intervention and handle it, if paused
266
await self.handle_intervention(agent_response)
267
@@ -360,6 +335,40 @@ class Agent:
335
# call monologue_end extensions
336
await self.call_extensions("monologue_end", loop_data=self.loop_data) # type: ignore
337
338
+ async def prepare_chain(self, loop_data: LoopData):
339
+ # set system prompt and message history
340
+ loop_data.system = await self.get_system_prompt(self.loop_data)
341
+ loop_data.history_output = self.history.output()
342
+
343
+ # and allow extensions to edit them
344
+ await self.call_extensions("message_loop_prompts", loop_data=loop_data)
345
+
346
+ # extras (memory etc.)
347
+ extras: list[history.OutputMessage] = []
348
+ for extra in loop_data.extras_persistent.values():
349
+ extras += history.Message(False, content=extra).output()
350
+ for extra in loop_data.extras_temporary.values():
351
+ extras += history.Message(False, content=extra).output()
352
+ loop_data.extras_temporary.clear()
353
+
354
+ # combine history and extras
355
+ history_combined = history.group_outputs_abab(loop_data.history_output + extras)
356
+
357
+ # convert history to LLM format
358
+ history_langchain = history.output_langchain(history_combined)
359
+
360
+ # build chain from system prompt, message history and model
361
+ prompt = ChatPromptTemplate.from_messages(
362
+ [
363
+ SystemMessage(content="\n\n".join(loop_data.system)),
364
+ *history_langchain,
365
+ ]
366
+ )
367
+
368
+ # return callable chain
369
+ chain = prompt | self.config.chat_model
370
+ return chain, prompt
371
+
372
def handle_critical_exception(self, exception: Exception):
373
if isinstance(exception, HandledException):
374
raise exception # Re-raise the exception to kill the loop
@@ -378,7 +387,14 @@ class Agent:
387
self.context.log.log(type="error", content=error_message)
388
raise HandledException(exception) # Re-raise the exception to kill the loop
389
381
- def parse_prompt(self, file: str, **kwargs) -> tuple[list, dict]:
390
+ async def get_system_prompt(self, loop_data: LoopData) -> list[str]:
391
+ system_prompt = []
392
+ await self.call_extensions(
393
+ "system_prompt", system_prompt=system_prompt, loop_data=loop_data
394
+ )
395
+ return system_prompt
396
+
397
+ def parse_prompt(self, file: str, **kwargs):
398
prompt_dir = files.get_abs_path("prompts/default")
399
backup_dir = []
400
if (
@@ -389,12 +405,7 @@ class Agent:
405
prompt = files.parse_file(
406
files.get_abs_path(prompt_dir, file), _backup_dirs=backup_dir, **kwargs
407
)
392
- if isinstance(prompt, dict):
393
- return [], prompt
394
- elif isinstance(prompt, list):
395
- return prompt, {}
396
- else:
397
- return [prompt], {}
408
+ return prompt
409
410
def read_prompt(self, file: str, **kwargs) -> str:
411
prompt_dir = files.get_abs_path("prompts/default")
@@ -416,31 +427,55 @@ class Agent:
427
def set_data(self, field: str, value):
428
self.data[field] = value
429
419
- async def hist_add_user_message(self, message: str, intervention: bool = False):
430
+ def hist_add_message(self, ai: bool, content: history.MessageContent):
431
+ return self.history.add_message(ai=ai, content=content)
432
+
433
+ async def hist_add_user_message(
434
+ self, message: UserMessage, intervention: bool = False
435
+ ):
436
self.history.new_topic() # user message starts a new topic in history
437
+
438
+ # load message template based on intervention
439
if intervention:
422
- args, kwargs = self.parse_prompt("fw.intervention.md", message=message)
423
- msg = self.history.add_message(False, *args, **kwargs)
440
+ content = self.parse_prompt(
441
+ "fw.intervention.md",
442
+ message=message.message,
443
+ attachments=message.attachments,
444
+ )
445
else:
425
- args, kwargs = self.parse_prompt("fw.user_message.md", message=message)
426
- msg = self.history.add_message(False, *args, **kwargs)
446
+ content = self.parse_prompt(
447
+ "fw.user_message.md",
448
+ message=message.message,
449
+ attachments=message.attachments,
450
+ )
451
+
452
+ # remove empty attachments from template
453
+ if (
454
+ isinstance(content, dict)
455
+ and "attachments" in content
456
+ and not content["attachments"]
457
+ ):
458
+ del content["attachments"]
459
+
460
+ # add to history
461
+ msg = self.hist_add_message(False, content=content) # type: ignore
462
self.last_user_message = msg
463
return msg
464
465
async def hist_add_ai_response(self, message: str):
466
self.loop_data.last_response = message
432
- args, kwargs = self.parse_prompt("fw.ai_response.md", message=message)
433
- return self.history.add_message(True, *args, **kwargs)
467
+ content = self.parse_prompt("fw.ai_response.md", message=message)
468
+ return self.hist_add_message(True, content=content)
469
435
- async def hist_add_warning(self, message: str):
436
- args, kwargs = self.parse_prompt("fw.warning.md", message=message)
437
- return self.history.add_message(False, *args, **kwargs)
470
+ async def hist_add_warning(self, message: history.MessageContent):
471
+ content = self.parse_prompt("fw.warning.md", message=message)
472
+ return self.hist_add_message(False, content=content)
473
474
async def hist_add_tool_result(self, tool_name: str, tool_result: str):
440
- args, kwargs = self.parse_prompt(
475
+ content = self.parse_prompt(
476
"fw.tool_result.md", tool_name=tool_name, tool_result=tool_result
477
)
443
- return self.history.add_message(False, *args, **kwargs)
478
+ return self.hist_add_message(False, content=content)
479
480
def concat_messages(
481
self, messages
@@ -480,63 +515,14 @@ class Agent:
515
516
return response
517
483
- async def replace_middle_messages(self, middle_messages):
484
- cleanup_prompt = self.read_prompt("fw.msg_cleanup.md")
485
- log_item = self.context.log.log(
486
- type="util", heading="Mid messages cleanup summary"
487
- )
488
-
489
- PrintStyle(
490
- bold=True, font_color="orange", padding=True, background_color="white"
491
- ).print(f"{self.agent_name}: Mid messages cleanup summary")
492
- printer = PrintStyle(italic=True, font_color="orange", padding=False)
493
-
494
- def log_callback(content):
495
- printer.stream(content)
496
- log_item.stream(content=content)
497
-
498
- summary = await self.call_utility_llm(
499
- system=cleanup_prompt,
500
- msg=self.concat_messages(middle_messages),
501
- callback=log_callback,
502
- )
503
- new_human_message = HumanMessage(content=summary)
504
- return [new_human_message]
505
-
506
- async def cleanup_history(self, max: int, keep_start: int, keep_end: int):
507
- # if len(self.history) <= max:
508
- # return self.history
509
-
510
- # first_x = self.history[:keep_start]
511
- # last_y = self.history[-keep_end:]
512
-
513
- # # Identify the middle part
514
- # middle_part = self.history[keep_start:-keep_end]
515
-
516
- # # Ensure the first message in the middle is "human", if not, move one message back
517
- # if middle_part and middle_part[0].type != "human":
518
- # if len(first_x) > 0:
519
- # middle_part.insert(0, first_x.pop())
520
-
521
- # # Ensure the middle part has an odd number of messages
522
- # if len(middle_part) % 2 == 0:
523
- # middle_part = middle_part[:-1]
524
-
525
- # # Replace the middle part using the replacement function
526
- # new_middle_part = await self.replace_middle_messages(middle_part)
527
-
528
- # self.history = first_x + new_middle_part + last_y
529
-
530
- return self.history
531
-
518
async def handle_intervention(self, progress: str = ""):
519
while self.context.paused:
520
await asyncio.sleep(0.1) # wait if paused
521
if (
536
- self.intervention_message
522
+ self.intervention
523
): # if there is an intervention message, but not yet processed
538
- msg = self.intervention_message
539
- self.intervention_message = "" # reset the intervention message
524
+ msg = self.intervention
525
+ self.intervention = None # reset the intervention message
526
if progress.strip():
527
await self.hist_add_ai_response(progress)
528
# append the intervention message
docker/run/Dockerfile
+2
-3
@@ -42,8 +42,7 @@ RUN rm -rf /var/lib/apt/lists/*
42
RUN apt-get clean
43
44
# Expose ports
45
-# EXPOSE 22
46
-EXPOSE 80
45
+EXPOSE 22 80
46
47
# initialize runtime
49
-CMD ["/bin/bash", "/exe/initialize.sh"]
\ No newline at end of file
48
+CMD ["/bin/bash", "/exe/initialize.sh", "$BRANCH"]
\ No newline at end of file
docker/run/fs/exe/initialize.sh
+12
-1
@@ -1,5 +1,12 @@
1
#!/bin/bash
2
3
+# branch from parameter
4
+if [ -z "$1" ]; then
5
+ echo "Error: Branch parameter is empty. Please provide a valid branch name."
6
+ exit 1
7
+fi
8
+BRANCH="$1"
9
+
10
# Copy all contents from persistent /per to root directory (/) without overwriting
11
cp -r --no-preserve=ownership,mode /per/* /
12
@@ -11,7 +18,11 @@ chmod 444 /root/.profile
18
apt-get update &
19
20
# Start SSH service in background
14
-/usr/sbin/sshd -D &
21
+if [ "$BRANCH" != "development" ]; then
22
+ /usr/sbin/sshd -D -o ListenAddress=127.0.0.1 &
23
+else
24
+ /usr/sbin/sshd -D &
25
+fi
26
27
# Start searxng server in background
28
sudo -H -u searxng -i bash /exe/run_searxng.sh &
initialize.py
+7
-7
@@ -55,16 +55,16 @@ def initialize():
55
max_tool_response_length=3000,
56
# response_timeout_seconds = 60,
57
# code_exec_docker_enabled = True,
58
- # code_exec_docker_name = "agent-zero-exe",
59
- # code_exec_docker_image = "frdel/agent-zero-exe:latest",
60
- # code_exec_docker_ports = { "22/tcp": 50022 }
58
+ # code_exec_docker_name = "agent-zero-dev",
59
+ # code_exec_docker_image = "frdel/agent-zero-run:development",
60
+ # code_exec_docker_ports = { "22/tcp": 55022, "80/tcp": 55080 }
61
# code_exec_docker_volumes = {
62
- # files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"},
63
- # files.get_abs_path("instruments"): {"bind": "/instruments", "mode": "rw"},
64
- # },
62
+ # files.get_base_dir(): {"bind": "/a0", "mode": "rw"},
63
+ # files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"},
64
+ # },
65
# code_exec_ssh_enabled = True,
66
# code_exec_ssh_addr = "localhost",
67
- # code_exec_ssh_port = 50022,
67
+ # code_exec_ssh_port = 55022,
68
# code_exec_ssh_user = "root",
69
# code_exec_ssh_pass = "toor",
70
# additional = {},
prompts/default/fw.intervention.md
+2
-1
@@ -1,5 +1,6 @@
1
```json
2
{
3
- "user_intervention": {{message}}
3
+ "user_intervention": {{message}},
4
+ "attachments": {{attachments}}
5
}
6
```
\ No newline at end of file
prompts/default/fw.user_message.md
+2
-1
@@ -1,5 +1,6 @@
1
```json
2
{
3
- "user_message": {{message}}
3
+ "user_message": {{message}},
4
+ "attachments": {{attachments}}
5
}
6
```
python/api/delete_work_dir_file.py
+1
-2
@@ -10,8 +10,7 @@ class DeleteWorkDirFile(ApiHandler):
10
file_path = input.get('path', '')
11
current_path = input.get('currentPath', '')
12
13
- work_dir = files.get_abs_path("work_dir")
14
- browser = FileBrowser(work_dir)
13
+ browser = FileBrowser()
14
15
if browser.delete_file(file_path):
16
# Get updated file list
python/api/download_work_dir_file.py
+1
-2
@@ -12,8 +12,7 @@ class DownloadWorkDirFile(ApiHandler):
12
if not file_path:
13
raise ValueError("No file path provided")
14
15
- work_dir = files.get_abs_path("work_dir")
16
- browser = FileBrowser(work_dir)
15
+ browser = FileBrowser()
16
17
full_path = browser.get_full_path(file_path, True)
18
if os.path.isdir(full_path):
python/api/get_work_dir_files.py
+7
-3
@@ -2,14 +2,18 @@ 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, runtime
6
7
8
class GetWorkDirFiles(ApiHandler):
9
async def process(self, input: dict, request: Request) -> dict | Response:
10
current_path = request.args.get("path", "")
11
- work_dir = files.get_abs_path("work_dir")
12
- browser = FileBrowser(work_dir)
11
+ if current_path == "$WORK_DIR":
12
+ if runtime.is_development():
13
+ current_path = "work_dir"
14
+ else:
15
+ current_path = "root"
16
+ browser = FileBrowser()
17
result = browser.get_files(current_path)
18
19
return {"data": result}
python/api/message.py
+8
-7
@@ -1,4 +1,4 @@
1
-from agent import AgentContext
1
+from agent import AgentContext, UserMessage
2
from python.helpers.api import ApiHandler
3
from flask import Request, Response
4
@@ -30,17 +30,18 @@ class Message(ApiHandler):
30
attachments = request.files.getlist("attachments")
31
attachment_paths = []
32
33
- upload_folder = files.get_abs_path("work_dir/uploads")
33
+ upload_folder_int = "/a0/tmp/uploads"
34
+ upload_folder_ext = files.get_abs_path("tmp/uploads")
35
36
if attachments:
36
- os.makedirs(upload_folder, exist_ok=True)
37
+ os.makedirs(upload_folder_ext, exist_ok=True)
38
for attachment in attachments:
39
if attachment.filename is None:
40
continue
41
filename = secure_filename(attachment.filename)
41
- save_path = files.get_abs_path(upload_folder, filename)
42
+ save_path = files.get_abs_path(upload_folder_ext, filename)
43
attachment.save(save_path)
43
- attachment_paths.append(save_path)
44
+ attachment_paths.append(os.path.join(upload_folder_int, filename))
45
else:
46
# Handle JSON request as before
47
input_data = request.get_json()
@@ -56,7 +57,7 @@ class Message(ApiHandler):
57
context = self.get_context(ctxid)
58
59
# Store attachments in agent data
59
- context.agent0.set_data("attachments", attachment_paths)
60
+ # context.agent0.set_data("attachments", attachment_paths)
61
62
# Prepare attachment filenames for logging
63
attachment_filenames = (
@@ -84,4 +85,4 @@ class Message(ApiHandler):
85
id=message_id,
86
)
87
87
- return context.communicate(message), context
\ No newline at end of file
88
+ return context.communicate(UserMessage(message, attachment_paths)), context
\ No newline at end of file
python/api/upload.py
+1
-2
@@ -1,7 +1,6 @@
1
from python.helpers.api import ApiHandler
2
from flask import Request, Response
3
4
-from python.helpers.file_browser import FileBrowser
4
from python.helpers import files
5
from werkzeug.utils import secure_filename
6
@@ -17,7 +16,7 @@ class UploadFile(ApiHandler):
16
for file in file_list:
17
if file and self.allowed_file(file.filename): # Check file type
18
filename = secure_filename(file.filename) # type: ignore
20
- file.save(files.get_abs_path("work_dir/upload", filename))
19
+ file.save(files.get_abs_path("tmp/upload", filename))
20
saved_filenames.append(filename)
21
22
return {"filenames": saved_filenames} # Return saved filenames
python/api/upload_work_dir_files.py
+1
-2
@@ -16,8 +16,7 @@ class UploadWorkDirFiles(ApiHandler):
16
current_path = request.form.get('path', '')
17
uploaded_files = request.files.getlist("files[]")
18
19
- work_dir = files.get_abs_path("work_dir")
20
- browser = FileBrowser(work_dir)
19
+ browser = FileBrowser()
20
21
successful, failed = browser.save_files(uploaded_files, current_path)
22
python/extensions/message_loop_prompts/_30_include_attachments._py
renamed
python/extensions/message_loop_prompts/_50_recall_memories.py
+10
-4
@@ -18,6 +18,12 @@ class RecallMemories(Extension):
18
await self.search_memories(loop_data=loop_data, **kwargs)
19
20
async def search_memories(self, loop_data: LoopData, **kwargs):
21
+
22
+ #cleanup
23
+ extras = loop_data.extras_temporary
24
+ if "memories" in extras:
25
+ del extras["memories"]
26
+
27
# try:
28
# show temp info message
29
self.agent.context.log.log(
@@ -79,13 +85,13 @@ class RecallMemories(Extension):
85
log_item.update(memories=memories_text)
86
87
# place to prompt
82
- memories_prompt = self.agent.read_prompt(
88
+ memories_prompt = self.agent.parse_prompt(
89
"agent.system.memories.md", memories=memories_text
90
)
91
86
- # append to system message
87
- loop_data.system.append(memories_prompt)
88
-
92
+ # append to prompt
93
+ extras["memories"] = memories_prompt
94
+
95
# except Exception as e:
96
# err = errors.format_error(e)
97
# self.agent.context.log.log(
python/extensions/message_loop_prompts/_51_recall_solutions.py
+10
-2
@@ -19,6 +19,12 @@ class RecallSolutions(Extension):
19
await self.search_solutions(loop_data=loop_data, **kwargs)
20
21
async def search_solutions(self, loop_data: LoopData, **kwargs):
22
+
23
+ #cleanup
24
+ extras = loop_data.extras_temporary
25
+ if "solutions" in extras:
26
+ del extras["solutions"]
27
+
28
# try:
29
# show temp info message
30
self.agent.context.log.log(
@@ -86,10 +92,12 @@ class RecallSolutions(Extension):
92
solutions_text += solution.page_content + "\n\n"
93
solutions_text = solutions_text.strip()
94
log_item.update(solutions=solutions_text)
89
- solutions_prompt = self.agent.read_prompt(
95
+ solutions_prompt = self.agent.parse_prompt(
96
"agent.system.solutions.md", solutions=solutions_text
97
)
92
- loop_data.system.append(solutions_prompt)
98
+
99
+ # append to prompt
100
+ extras["solutions"] = solutions_prompt
101
102
# except Exception as e:
103
# err = errors.format_error(e)
python/extensions/system_prompt/_10_system_prompt.py
renamed
+3
-3
@@ -5,12 +5,12 @@ from agent import Agent, LoopData
5
6
class SystemPrompt(Extension):
7
8
- async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
8
+ async def execute(self, system_prompt: list[str]=[], loop_data: LoopData = LoopData(), **kwargs):
9
# append main system prompt and tools
10
main = get_main_prompt(self.agent)
11
tools = get_tools_prompt(self.agent)
12
- loop_data.system.append(main)
13
- loop_data.system.append(tools)
12
+ system_prompt.append(main)
13
+ system_prompt.append(tools)
14
15
def get_main_prompt(agent: Agent):
16
return get_prompt("agent.system.main.md", agent)
python/extensions/system_prompt/_20_behaviour_prompt.py
renamed
+2
-2
@@ -6,9 +6,9 @@ from python.helpers import files, memory
6
7
class BehaviourPrompt(Extension):
8
9
- async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
9
+ async def execute(self, system_prompt: list[str]=[], loop_data: LoopData = LoopData(), **kwargs):
10
prompt = read_rules(self.agent)
11
- loop_data.system.insert(0, prompt) #.append(prompt)
11
+ system_prompt.insert(0, prompt) #.append(prompt)
12
13
def get_custom_rules_file(agent: Agent):
14
return memory.get_memory_subdir_abs(agent) + f"/behaviour.md"
python/helpers/errors.py
+9
-7
@@ -7,7 +7,7 @@ def handle_error(e: Exception):
7
if isinstance(e, asyncio.CancelledError):
8
raise e
9
10
-def format_error(e: Exception, max_entries=4):
10
+def format_error(e: Exception, start_entries=6, end_entries=4):
11
traceback_text = traceback.format_exc()
12
# Split the traceback into lines
13
lines = traceback_text.split('\n')
@@ -15,13 +15,15 @@ def format_error(e: Exception, max_entries=4):
15
# Find all "File" lines
16
file_indices = [i for i, line in enumerate(lines) if line.strip().startswith("File ")]
17
18
- # If we found at least one "File" line, keep up to max_entries
19
- if file_indices:
20
- start_index = max(0, len(file_indices) - max_entries)
21
- trimmed_lines = lines[file_indices[start_index]:]
18
+ # If we found at least one "File" line, trim the middle if there are more than start_entries+end_entries lines
19
+ if len(file_indices) > start_entries + end_entries:
20
+ start_index = max(0, len(file_indices) - start_entries - end_entries)
21
+ trimmed_lines = lines[:file_indices[start_index]] + [
22
+ f"\n>>> {len(file_indices) - start_entries - end_entries} stack lines skipped <<<\n"
23
+ ] + lines[file_indices[start_index + end_entries]:]
24
else:
23
- # If no "File" lines found, just return the original traceback
24
- return traceback_text
25
+ # If no "File" lines found, or not enough to trim, just return the original traceback
26
+ trimmed_lines = lines
27
28
# Find the error message at the end
29
error_message = ""
python/helpers/file_browser.py
+7
-3
@@ -7,7 +7,7 @@ import zipfile
7
from werkzeug.utils import secure_filename
8
from datetime import datetime
9
10
-from python.helpers import files
10
+from python.helpers import files, runtime
11
12
class FileBrowser:
13
ALLOWED_EXTENSIONS = {
@@ -18,8 +18,12 @@ class FileBrowser:
18
19
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
20
21
- def __init__(self, base_dir: str):
22
- self.base_dir = Path(base_dir).resolve()
21
+ def __init__(self):
22
+ if runtime.is_development():
23
+ base_dir = files.get_base_dir()
24
+ else:
25
+ base_dir = "/"
26
+ self.base_dir = Path(base_dir)
27
28
def _check_file_size(self, file) -> bool:
29
try:
python/helpers/history.py
+28
-29
@@ -16,10 +16,10 @@ HISTORY_BULK_RATIO = 0.2
16
TOPIC_COMPRESS_RATIO = 0.65
17
LARGE_MESSAGE_TO_TOPIC_RATIO = 0.25
18
19
-OutputType = (
20
- list["OutputType"]
21
- | OrderedDict[str, "OutputType"]
22
- | list[OrderedDict[str, "OutputType"]]
19
+MessageContent = (
20
+ list["MessageContent"]
21
+ | OrderedDict[str, "MessageContent"]
22
+ | list[OrderedDict[str, "MessageContent"]]
23
| str
24
| list[str]
25
)
@@ -27,7 +27,7 @@ OutputType = (
27
28
class OutputMessage(TypedDict):
29
ai: bool
30
- content: OutputType
30
+ content: MessageContent
31
32
33
class Record:
@@ -67,19 +67,16 @@ class Record:
67
68
69
class Message(Record):
70
- def __init__(self, ai: bool, text: str | None = None, **kwargs: OutputType):
70
+ def __init__(self, ai: bool, content: MessageContent):
71
self.ai = ai
72
- self.text = text
73
- self.kwargs: OrderedDict[str, OutputType] = OrderedDict(**kwargs)
74
- self.summary: OutputType = ""
72
+ self.content = content
73
+ self.summary: MessageContent = ""
74
75
async def compress(self):
76
return False
77
78
def output(self):
80
- return [
81
- OutputMessage(ai=self.ai, content=self.summary or self.text or self.kwargs)
82
- ]
79
+ return [OutputMessage(ai=self.ai, content=self.summary or self.content)]
80
81
def output_langchain(self):
82
return output_langchain(self.output())
@@ -91,15 +88,14 @@ class Message(Record):
88
return {
89
"_cls": "Message",
90
"ai": self.ai,
94
- "text": self.text,
95
- "kwargs": self.kwargs,
91
+ "content": self.content,
92
"summary": self.summary,
93
}
94
95
@staticmethod
96
def from_dict(data: dict, history: "History"):
101
- msg = Message(ai=data["ai"], text=data["text"], **data["kwargs"])
102
- msg.summary = data["summary"]
97
+ msg = Message(ai=data["ai"], content=data.get("content", "Content lost"))
98
+ msg.summary = data.get("summary", "")
99
return msg
100
101
@@ -109,8 +105,8 @@ class Topic(Record):
105
self.summary: str = ""
106
self.messages: list[Message] = []
107
112
- def add_message(self, ai: bool, text: str | None = None, **kwargs):
113
- msg = Message(ai=ai, text=text, **kwargs)
108
+ def add_message(self, ai: bool, content: MessageContent):
109
+ msg = Message(ai=ai, content=content)
110
self.messages.append(msg)
111
return msg
112
@@ -145,7 +141,10 @@ class Topic(Record):
141
for msg, tok, leng, out in large_msgs:
142
trim_to_chars = leng * (msg_max_size / tok)
143
trunc = messages.truncate_dict_by_ratio(
148
- self.history.agent, out[0]["content"], trim_to_chars * 1.15, trim_to_chars * 0.85
144
+ self.history.agent,
145
+ out[0]["content"],
146
+ trim_to_chars * 1.15,
147
+ trim_to_chars * 0.85,
148
)
149
msg.summary = trunc
150
@@ -164,10 +163,10 @@ class Topic(Record):
163
cnt_to_sum = math.ceil((len(self.messages) - 2) * TOPIC_COMPRESS_RATIO)
164
msg_to_sum = self.messages[1 : cnt_to_sum + 1]
165
summary = await self.summarize_messages(msg_to_sum)
167
- sum_msg_args, sum_msg_kwargs = self.history.agent.parse_prompt(
166
+ sum_msg_content = self.history.agent.parse_prompt(
167
"fw.msg_summary.md", summary=summary
168
)
170
- sum_msg = Message(False, *sum_msg_args, **sum_msg_kwargs)
169
+ sum_msg = Message(False, sum_msg_content)
170
self.messages[1 : cnt_to_sum + 1] = [sum_msg]
171
return True
172
return False
@@ -274,8 +273,8 @@ class History(Record):
273
+ self.get_current_topic_tokens()
274
)
275
277
- def add_message(self, ai: bool, text: str | None = None, **kwargs: OutputType):
278
- return self.current.add_message(ai, text=text, **kwargs)
276
+ def add_message(self, ai: bool, content: MessageContent):
277
+ return self.current.add_message(ai, content=content)
278
279
def new_topic(self):
280
if self.current.messages:
@@ -367,9 +366,9 @@ class History(Record):
366
return True
367
368
async def compress_bulks(self):
370
- #merge bulks if possible
369
+ # merge bulks if possible
370
compressed = await self.merge_bulks_by(BULK_MERGE_COUNT)
372
- #remove oldest bulk if necessary
371
+ # remove oldest bulk if necessary
372
if not compressed:
373
self.bulks.pop(0)
374
return compressed
@@ -395,7 +394,7 @@ class History(Record):
394
395
def deserialize_history(json_data: str, agent) -> History:
396
history = History(agent=agent)
398
- if json_data:
397
+ if json_data:
398
data = json.loads(json_data)
399
history = History.from_dict(data, history=history)
400
return history
@@ -410,7 +409,7 @@ def serialize_output(output: OutputMessage, ai_label="ai", human_label="human"):
409
return f'{ai_label if output["ai"] else human_label}: {serialize_content(output["content"])}'
410
411
413
-def serialize_content(content: OutputType) -> str:
412
+def serialize_content(content: MessageContent) -> str:
413
if isinstance(content, str):
414
return content
415
try:
@@ -446,7 +445,7 @@ def output_text(messages: list[OutputMessage], ai_label="ai", human_label="human
445
return "\n".join(serialize_output(o, ai_label, human_label) for o in messages)
446
447
449
-def merge_outputs(a: OutputType, b: OutputType) -> OutputType:
448
+def merge_outputs(a: MessageContent, b: MessageContent) -> MessageContent:
449
if not isinstance(a, list):
450
a = [a]
451
if not isinstance(b, list):
@@ -455,7 +454,7 @@ def merge_outputs(a: OutputType, b: OutputType) -> OutputType:
454
# return merge_properties(a, b)
455
456
458
-def merge_properties(a: OutputType, b: OutputType) -> OutputType:
457
+def merge_properties(a: MessageContent, b: MessageContent) -> MessageContent:
458
if isinstance(a, list):
459
if isinstance(b, list):
460
return a + b # type: ignore
python/helpers/settings.py
+1
-2
@@ -46,7 +46,6 @@ class Settings(TypedDict):
46
stt_silence_duration: int
47
stt_waiting_timeout: int
48
49
-
49
class PartialSettings(Settings, total=False):
50
pass
51
@@ -132,7 +131,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
131
chat_model_fields.append(
132
{
133
"id": "chat_model_ctx_history",
135
- "title": "Context window space for chat history.",
134
+ "title": "Context window space for chat history",
135
"description": "Portion of context window dedicated to chat history visible to the agent. Chat history will automatically be optimized to fit. Smaller size will result in shorter and more summarized history. The remaining space will be used for system prompt, RAG and response.",
136
"type": "range",
137
"min": 0.01,
python/tools/unknown.py
+1
-1
@@ -1,5 +1,5 @@
1
from python.helpers.tool import Tool, Response
2
-from python.extensions.message_loop_prompts._10_system_prompt import (
2
+from python.extensions.system_prompt._10_system_prompt import (
3
get_tools_prompt,
4
)
5
webui/index.html
+8
-8
@@ -216,17 +216,17 @@
216
217
handleFileUpload(event) {
218
const files = event.target.files;
219
- if (files.length + this.attachments.length > 4) {
220
- alert('Maximum 4 attachments allowed');
221
- return;
222
- }
219
+ //if (files.length + this.attachments.length > 4) {
220
+ // alert('Maximum 4 attachments allowed');
221
+ // return;
222
+ //}
223
224
Array.from(files).forEach(file => {
225
const ext = file.name.split('.').pop().toLowerCase();
226
const allowedExts = new Set(['jpg', 'jpeg', 'png', 'bmp', 'md', 'py', 'js', 'sh',
227
'html', 'css', 'pdf', 'txt', 'csv', 'json']);
228
229
- if (allowedExts.has(ext)) {
229
+ //if (allowedExts.has(ext)) {
230
const isImage = ['jpg', 'jpeg', 'png', 'bmp'].includes(ext);
231
232
if (isImage) {
@@ -253,7 +253,7 @@
253
});
254
this.hasAttachments = true;
255
}
256
- }
256
+ //}
257
});
258
}
259
}">
@@ -294,7 +294,7 @@
294
multiple style="display: none" @change="handleFileUpload($event)">
295
296
<div x-show="showTooltip" class="tooltip">
297
- Limit: 4 attachments per message
297
+ Add attachments to the message
298
</div>
299
</div>
300
@@ -361,7 +361,7 @@
361
d="m6.38,20.23H1.75l7.03,67.03c.11,1.07.55,2.02,1.2,2.69.55.55,1.28.89,2.11.89h97.1c.82,0,1.51-.33,2.05-.87.68-.68,1.13-1.67,1.28-2.79l9.1-66.94H6.38Z"
362
fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="8"></path>
363
</svg>
364
- <p>work_dir Browser</p>
364
+ <p>Files</p>
365
</button>
366
367
<button class="text-button" id="history_inspect" @click="window.openHistoryModal()">
webui/js/file_browser.js
+2
-2
@@ -3,7 +3,7 @@ const fileBrowserModalProxy = {
3
isLoading: false,
4
5
browser: {
6
- title: "Work Directory Browser",
6
+ title: "File Browser",
7
currentPath: "",
8
entries: [],
9
parentPath: "",
@@ -24,7 +24,7 @@ const fileBrowserModalProxy = {
24
25
// Initialize currentPath to root if it's empty
26
if (!modalAD.browser.currentPath) {
27
- modalAD.browser.currentPath = "";
27
+ modalAD.browser.currentPath = "$WORK_DIR";
28
}
29
30
await modalAD.fetchFiles(modalAD.browser.currentPath);