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