From 13f6b0e1e05d815c9c09ca8e32ebe16a802ce9df Mon Sep 17 00:00:00 2001 From: frdel <38891707+frdel@users.noreply.github.com> Date: Sun, 2 Mar 2025 20:42:11 +0100 Subject: [PATCH 1/5] kali-based WIP hacking edition work in progress --- docker/run/DockerfileKali | 33 +++++++++++++++++++ docker/run/build.txt | 3 ++ docker/run/fs/ins/install_additional.sh | 3 ++ docker/run/fs/ins/install_playwright.sh | 10 +++++- docker/run/fs/ins/install_searxng.sh | 2 +- docker/run/fs/ins/post_install.sh | 3 -- docker/run/fs/ins/pre_install.sh | 22 ++++++++++--- docker/run/fs/ins/pre_install_kali.sh | 6 ++++ docker/run/fs/ins/setup_ssh.sh | 2 +- python/helpers/dirty_json.py | 42 +++++++++---------------- python/helpers/history.py | 2 +- 11 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 docker/run/DockerfileKali create mode 100644 docker/run/fs/ins/pre_install_kali.sh diff --git a/docker/run/DockerfileKali b/docker/run/DockerfileKali new file mode 100644 index 000000000..f5808cdce --- /dev/null +++ b/docker/run/DockerfileKali @@ -0,0 +1,33 @@ +# Use the latest slim version of Kali Linux +FROM kalilinux/kali-rolling + +# Check if the argument is provided, else throw an error +ARG BRANCH +RUN if [ -z "$BRANCH" ]; then echo "ERROR: BRANCH is not set!" >&2; exit 1; fi +ENV BRANCH=$BRANCH + +# Copy contents of the project to /a0 +COPY ./fs/ / + +# pre installation steps +RUN bash /ins/pre_install.sh $BRANCH +RUN bash /ins/pre_install_kali.sh $BRANCH + +# install additional software +RUN bash /ins/install_additional.sh $BRANCH + +# install A0 +RUN bash /ins/install_A0.sh $BRANCH + +# cleanup repo and install A0 without caching, this speeds up builds +ARG CACHE_DATE=none +RUN echo "cache buster $CACHE_DATE" && bash /ins/install_A02.sh $BRANCH + +# post installation steps +RUN bash /ins/post_install.sh $BRANCH + +# Expose ports +EXPOSE 22 80 + +# initialize runtime +CMD ["/bin/bash", "-c", "/bin/bash /exe/initialize.sh $BRANCH"] \ No newline at end of file diff --git a/docker/run/build.txt b/docker/run/build.txt index 5dfde71e7..94b6ed213 100644 --- a/docker/run/build.txt +++ b/docker/run/build.txt @@ -4,6 +4,9 @@ docker build -t agent-zero-run:local --build-arg BRANCH=development --build-arg # local image without cache docker build -t agent-zero-run:local --build-arg BRANCH=development --no-cache . +# local image from Kali +docker build -f ./DockerfileKali -t agent-zero-run:hacking --build-arg BRANCH=main --build-arg CACHE_DATE=$(date +%Y-%m-%d:%H:%M:%S) . + # dockerhub push: docker login diff --git a/docker/run/fs/ins/install_additional.sh b/docker/run/fs/ins/install_additional.sh index e857ef471..477ef6e85 100644 --- a/docker/run/fs/ins/install_additional.sh +++ b/docker/run/fs/ins/install_additional.sh @@ -1,4 +1,7 @@ #!/bin/bash +# install playwright +bash /ins/install_playwright.sh "$@" + # searxng bash /ins/install_searxng.sh "$@" \ No newline at end of file diff --git a/docker/run/fs/ins/install_playwright.sh b/docker/run/fs/ins/install_playwright.sh index cd5755424..eca6ab98a 100644 --- a/docker/run/fs/ins/install_playwright.sh +++ b/docker/run/fs/ins/install_playwright.sh @@ -7,4 +7,12 @@ pip install playwright # install chromium with dependencies -playwright install --with-deps chromium-headless-shell +# for kali-based +if [ "$@" = "hacking" ]; then + apt-get install -y fonts-unifont libnss3 libnspr4 + playwright install chromium-headless-shell +else + # for debian based + playwright install --with-deps chromium-headless-shell +fi + diff --git a/docker/run/fs/ins/install_searxng.sh b/docker/run/fs/ins/install_searxng.sh index fdc7775ad..ee9ad3d1c 100644 --- a/docker/run/fs/ins/install_searxng.sh +++ b/docker/run/fs/ins/install_searxng.sh @@ -2,7 +2,7 @@ # Install necessary packages apt-get install -y \ - python3-dev python3-babel python3-venv \ + python3.12-dev python3-babel python3.12-venv \ uwsgi uwsgi-plugin-python3 \ git build-essential libxslt-dev zlib1g-dev libffi-dev libssl-dev diff --git a/docker/run/fs/ins/post_install.sh b/docker/run/fs/ins/post_install.sh index c1a7e1d18..410ff19a7 100644 --- a/docker/run/fs/ins/post_install.sh +++ b/docker/run/fs/ins/post_install.sh @@ -1,8 +1,5 @@ #!/bin/bash -# install playwright -bash /ins/install_playwright.sh "$@" - # Cleanup package list rm -rf /var/lib/apt/lists/* apt-get clean \ No newline at end of file diff --git a/docker/run/fs/ins/pre_install.sh b/docker/run/fs/ins/pre_install.sh index 74a78844f..48d6ed5ae 100644 --- a/docker/run/fs/ins/pre_install.sh +++ b/docker/run/fs/ins/pre_install.sh @@ -2,9 +2,8 @@ # Update and install necessary packages apt-get update && apt-get install -y \ - python3 \ - python3-pip \ - python3-venv \ + python3.12 \ + python3.12-venv \ nodejs \ npm \ openssh-server \ @@ -14,5 +13,18 @@ apt-get update && apt-get install -y \ git \ ffmpeg -# prepare SSH daemon -bash /ins/setup_ssh.sh "$@" \ No newline at end of file +# Configure system alternatives so that /usr/bin/python3 points to Python 3.12 +sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 +sudo update-alternatives --set python3 /usr/bin/python3.12 + +# Update pip3 symlink: if pip3.12 exists, point pip3 to it; +# otherwise, install pip using Python 3.12's ensurepip. +if [ -f /usr/bin/pip3.12 ]; then + sudo ln -sf /usr/bin/pip3.12 /usr/bin/pip3 +else + python3 -m ensurepip --upgrade + python3 -m pip install --upgrade pip +fi + +# Prepare SSH daemon +bash /ins/setup_ssh.sh "$@" diff --git a/docker/run/fs/ins/pre_install_kali.sh b/docker/run/fs/ins/pre_install_kali.sh new file mode 100644 index 000000000..dffcbdff3 --- /dev/null +++ b/docker/run/fs/ins/pre_install_kali.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# ubuntu based dependencies for playwright +# moved to install_playwright.sh +# apt-get update && apt-get install -y fonts-unifont fonts-ubuntu + diff --git a/docker/run/fs/ins/setup_ssh.sh b/docker/run/fs/ins/setup_ssh.sh index 958323623..331e905fc 100644 --- a/docker/run/fs/ins/setup_ssh.sh +++ b/docker/run/fs/ins/setup_ssh.sh @@ -1,6 +1,6 @@ #!/bin/bash # Set up SSH -mkdir /var/run/sshd && \ +mkdir -p /var/run/sshd && \ # echo 'root:toor' | chpasswd && \ sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config \ No newline at end of file diff --git a/python/helpers/dirty_json.py b/python/helpers/dirty_json.py index 28054ff10..b65c120db 100644 --- a/python/helpers/dirty_json.py +++ b/python/helpers/dirty_json.py @@ -103,9 +103,14 @@ class DirtyJson: return None def _match(self, text: str) -> bool: - cnt = len(text) - if self._peek(cnt).lower() == text.lower(): - self._advance(cnt) + # first char should match current char + if not self.current_char or self.current_char.lower() != text[0].lower(): + return False + + # peek remaining chars + remaining = len(text) - 1 + if self._peek(remaining).lower() == text[1:].lower(): + self._advance(len(text)) return True return False @@ -187,6 +192,13 @@ class DirtyJson: self._skip_whitespace() if self.current_char == ',': self._advance() + # handle trailing commas, end of array + self._skip_whitespace() + if self.current_char is None or self.current_char == ']': + if self.current_char == ']': + self._advance() + self.stack.pop() + return elif self.current_char != ']': self.stack.pop() return @@ -245,30 +257,6 @@ class DirtyJson: except ValueError: return float(number_str) - def _parse_true(self): - self._advance() - for char in 'rue': - if self.current_char != char: - return None - self._advance() - return True - - def _parse_false(self): - self._advance() - for char in 'alse': - if self.current_char != char: - return None - self._advance() - return False - - def _parse_null(self): - self._advance() - for char in 'ull': - if self.current_char != char: - return None - self._advance() - return None - def _parse_unquoted_string(self): result = "" while self.current_char is not None and self.current_char not in [':', ',', '}', ']']: diff --git a/python/helpers/history.py b/python/helpers/history.py index 766e01849..3f9e7c506 100644 --- a/python/helpers/history.py +++ b/python/helpers/history.py @@ -126,7 +126,7 @@ class Topic(Record): msg_max_size = ( set["chat_model_ctx_length"] * set["chat_model_ctx_history"] - * HISTORY_TOPIC_RATIO + * CURRENT_TOPIC_RATIO * LARGE_MESSAGE_TO_TOPIC_RATIO ) large_msgs = [] From 22eef83cc6628e13938a851beece8e7e8c6af19e Mon Sep 17 00:00:00 2001 From: Rafael Uzarowski Date: Sun, 2 Mar 2025 18:00:40 +0100 Subject: [PATCH 2/5] feat: chat model VISION support for user and tool msg attachments --- agent.py | 21 ++++--- initialize.py | 1 + prompts/default/agent.system.tools_vision.md | 3 + prompts/default/fw.tool_result.md | 5 +- .../system_prompt/_10_system_prompt.py | 10 +++- python/helpers/history.py | 58 +++++++++++++++++-- python/helpers/settings.py | 13 +++++ python/helpers/tool.py | 16 ++--- 8 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 prompts/default/agent.system.tools_vision.md diff --git a/agent.py b/agent.py index c5775d131..415b4a20e 100644 --- a/agent.py +++ b/agent.py @@ -10,8 +10,9 @@ import models from langchain_core.prompt_values import ChatPromptValue from python.helpers import extract_tools, rate_limiter, files, errors, history, tokens from python.helpers.print_style import PrintStyle -from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder -from langchain_core.messages import HumanMessage, SystemMessage, AIMessage +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, StringPromptTemplate +from langchain_core.prompts.image import ImagePromptTemplate +from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.language_models.llms import BaseLLM from langchain_core.embeddings import Embeddings @@ -19,6 +20,7 @@ import python.helpers.log as Log from python.helpers.dirty_json import DirtyJson from python.helpers.defer import DeferredTask from typing import Callable +from python.helpers.history import OutputMessage class AgentContext: @@ -187,7 +189,7 @@ class AgentConfig: @dataclass class UserMessage: message: str - attachments: list[str] + attachments: list[str] = field(default_factory=list[str]) class LoopData: @@ -357,10 +359,14 @@ class Agent: loop_data.extras_temporary.clear() # combine history and extras - history_combined = history.group_outputs_abab(loop_data.history_output + extras) + history_combined: list[OutputMessage] = history.group_outputs_abab(loop_data.history_output + extras) # convert history to LLM format - history_langchain = history.output_langchain(history_combined) + history_langchain: list[BaseMessage] = history.output_langchain(history_combined) + + PrintStyle(font_color="grey", background_color="black", bold=True, padding=True).print( + f"History Langchain: {history_langchain}" + ) # build chain from system prompt, message history and model prompt = ChatPromptTemplate.from_messages( @@ -479,9 +485,10 @@ class Agent: content = self.parse_prompt("fw.warning.md", message=message) return self.hist_add_message(False, content=content) - async def hist_add_tool_result(self, tool_name: str, tool_result: str): + async def hist_add_tool_result(self, tool_name: str, tool_result: str, attachments: list[str] = []): + attachments_str = json.dumps(attachments).replace("\n", "") content = self.parse_prompt( - "fw.tool_result.md", tool_name=tool_name, tool_result=tool_result + "fw.tool_result.md", tool_name=tool_name, tool_result=tool_result, attachments=attachments_str ) return self.hist_add_message(False, content=content) diff --git a/initialize.py b/initialize.py index ead07d2f2..0003b0223 100644 --- a/initialize.py +++ b/initialize.py @@ -13,6 +13,7 @@ def initialize(): provider=models.ModelProvider[current_settings["chat_model_provider"]], name=current_settings["chat_model_name"], ctx_length=current_settings["chat_model_ctx_length"], + vision=current_settings["chat_model_vision"], limit_requests=current_settings["chat_model_rl_requests"], limit_input=current_settings["chat_model_rl_input"], limit_output=current_settings["chat_model_rl_output"], diff --git a/prompts/default/agent.system.tools_vision.md b/prompts/default/agent.system.tools_vision.md new file mode 100644 index 000000000..0af7e7527 --- /dev/null +++ b/prompts/default/agent.system.tools_vision.md @@ -0,0 +1,3 @@ +## "Multimodal (Vision) Agent Tools" available: + +None yet. In future, this section will contain vision-only tools diff --git a/prompts/default/fw.tool_result.md b/prompts/default/fw.tool_result.md index 1943cc022..b45d456e8 100644 --- a/prompts/default/fw.tool_result.md +++ b/prompts/default/fw.tool_result.md @@ -1,6 +1,7 @@ ~~~json { "tool_name": {{tool_name}}, - "tool_result": {{tool_result}} + "tool_result": {{tool_result}}, + "attachments": {{attachments}} } -~~~ \ No newline at end of file +~~~ diff --git a/python/extensions/system_prompt/_10_system_prompt.py b/python/extensions/system_prompt/_10_system_prompt.py index e7544b2ec..68f7d6dd8 100644 --- a/python/extensions/system_prompt/_10_system_prompt.py +++ b/python/extensions/system_prompt/_10_system_prompt.py @@ -12,11 +12,17 @@ class SystemPrompt(Extension): system_prompt.append(main) system_prompt.append(tools) + def get_main_prompt(agent: Agent): return get_prompt("agent.system.main.md", agent) + def get_tools_prompt(agent: Agent): - return get_prompt("agent.system.tools.md", agent) + prompt = get_prompt("agent.system.tools.md", agent) + if agent.config.chat_model.vision: + prompt += '\n' + get_prompt("agent.system.tools_vision.md", agent) + return prompt + def get_prompt(file: str, agent: Agent): # variables for system prompts @@ -26,4 +32,4 @@ def get_prompt(file: str, agent: Agent): "date_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "agent_name": agent.agent_name, } - return agent.read_prompt(file, **vars) \ No newline at end of file + return agent.read_prompt(file, **vars) diff --git a/python/helpers/history.py b/python/helpers/history.py index 3f9e7c506..3fcb02426 100644 --- a/python/helpers/history.py +++ b/python/helpers/history.py @@ -3,11 +3,14 @@ import asyncio from collections import OrderedDict import json import math +import os from typing import Coroutine, Literal, TypedDict, cast from python.helpers import messages, tokens, settings, call_llm from enum import Enum -from langchain_core.messages import HumanMessage, SystemMessage, AIMessage - +from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage +from python.helpers.print_style import PrintStyle +from langchain_core.prompts import HumanMessagePromptTemplate +from typing import Any BULK_MERGE_COUNT = 3 TOPICS_KEEP_COUNT = 3 CURRENT_TOPIC_RATIO = 0.5 @@ -425,13 +428,60 @@ def group_outputs_abab(outputs: list[OutputMessage]) -> list[OutputMessage]: return result -def output_langchain(messages: list[OutputMessage]): +def output_langchain(messages: list[OutputMessage]) -> list[BaseMessage]: result = [] for m in messages: if m["ai"]: result.append(AIMessage(content=serialize_content(m["content"]))) else: - result.append(HumanMessage(content=serialize_content(m["content"]))) + contents = m["content"] + + # sometimes content is a list sometimes not + if not isinstance(contents, list): + contents = [contents] + + PrintStyle(font_color="grey", background_color="black", bold=True, padding=True).print( + f"Contents: {json.dumps(contents, indent=2)}" + ) + + template: list[dict[str, str]] = [] # type: ignore + message = "" + images = {} + for _, content in enumerate(contents): + if message: + # the first message is the user message, then the memory and solutions + message += "\n\n--- Memory & Solutions Section: ---\n\n" + + message += serialize_content(content) + + if isinstance(content, dict) and "attachments" in content: + attachments: list[str] = cast(list[str], content["attachments"]) + for attachment in attachments: + if not os.path.exists(str(attachment)): + continue + if attachment not in images: + import base64 + from mimetypes import guess_type + mime_type, _ = guess_type(str(attachment)) + if mime_type.startswith("image/"): + # Read and encode the image file + with open(str(attachment), "rb") as image_file: + base64_encoded_data = base64.b64encode(image_file.read()).decode('utf-8') + # Construct the data URL + images[attachment] = f"data:{mime_type};base64,{base64_encoded_data}" + + if message: + template.append({"type": "text", "text": message}) + if images: + for _, image in images.items(): + template.append({"type": "image_url", "image_url": image}) + if template: + # only jinja2 is safe for json, both mustache({{...}}) and f-string({...}) are not + result.append(HumanMessagePromptTemplate.from_template(template=template, partial_variables={}, template_format="jinja2")) # type: ignore + + PrintStyle(font_color="grey", background_color="black", bold=True, padding=True).print( + f"Result: {result}" + ) return result diff --git a/python/helpers/settings.py b/python/helpers/settings.py index 228330753..fc7a41ce2 100644 --- a/python/helpers/settings.py +++ b/python/helpers/settings.py @@ -16,6 +16,7 @@ class Settings(TypedDict): chat_model_kwargs: dict[str, str] chat_model_ctx_length: int chat_model_ctx_history: float + chat_model_vision: bool chat_model_rl_requests: int chat_model_rl_input: int chat_model_rl_output: int @@ -149,6 +150,17 @@ def convert_out(settings: Settings) -> SettingsOutput: } ) + + chat_model_fields.append( + { + "id": "chat_model_vision", + "title": "Supports Vision", + "description": "Models capable of Vision can for example natively see the content of image attachments.", + "type": "switch", + "value": settings["chat_model_vision"], + } + ) + chat_model_fields.append( { "id": "chat_model_rl_requests", @@ -777,6 +789,7 @@ def get_default_settings() -> Settings: chat_model_kwargs={ "temperature": "0" }, chat_model_ctx_length=120000, chat_model_ctx_history=0.7, + chat_model_vision=False, chat_model_rl_requests=0, chat_model_rl_input=0, chat_model_rl_output=0, diff --git a/python/helpers/tool.py b/python/helpers/tool.py index a50ec8295..fcfb51070 100644 --- a/python/helpers/tool.py +++ b/python/helpers/tool.py @@ -1,14 +1,16 @@ from abc import abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, field from agent import Agent from python.helpers.print_style import PrintStyle -from python.helpers import messages + @dataclass class Response: message:str - break_loop:bool - + break_loop: bool + attachments: list[str] = field(default_factory=list[str]) + + class Tool: def __init__(self, agent: Agent, name: str, args: dict[str,str], message: str, **kwargs) -> None: @@ -29,10 +31,10 @@ class Tool: PrintStyle(font_color="#85C1E9", bold=True).stream(self.nice_key(key)+": ") PrintStyle(font_color="#85C1E9", padding=isinstance(value,str) and "\n" in value).stream(value) PrintStyle().print() - + async def after_execution(self, response: Response, **kwargs): text = response.message.strip() - await self.agent.hist_add_tool_result(self.name, text) + await self.agent.hist_add_tool_result(self.name, text, response.attachments) PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}'") PrintStyle(font_color="#85C1E9").print(response.message) self.log.update(content=response.message) @@ -44,4 +46,4 @@ class Tool: words = key.split('_') words = [words[0].capitalize()] + [word.lower() for word in words[1:]] result = ' '.join(words) - return result \ No newline at end of file + return result From f3e973328741f999718ba9e61a4ded5c4c355a60 Mon Sep 17 00:00:00 2001 From: frdel <38891707+frdel@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:08:10 +0200 Subject: [PATCH 3/5] Squashed commit of the following: commit 88f277855b26218e8d6403d41d884edf4b942f5e Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Mar 18 13:39:42 2025 +0100 warpcast added commit 04e1f483de83237a325957eb9da9e972b898800f Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Feb 25 01:20:26 2025 +0100 Update README.md commit 10264892d02be291f29cebeccc736f6a55e72abf Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Feb 25 01:11:41 2025 +0100 showcase video commit d57ae67d36e517b4810c75629af1c27e5b0315d9 Merge: 33d65c4 af7f169 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Wed Feb 19 22:34:06 2025 +0100 Merge branch 'testing' commit af7f169383e646c9d694f89e41d2cbe6c9b26b4b Merge: 8605a81 3e187e8 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Wed Feb 19 22:33:50 2025 +0100 Merge branch 'development' into testing commit 33d65c45a1be66b320ae522c779b4c95aef3f1fe Merge: 2a9b30e 8605a81 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Thu Feb 13 21:36:01 2025 +0100 Merge branch 'testing' commit 8605a8183b16f139487d809d52d1a27ee3f33a09 Merge: 9fb3daf faf896a Author: frdel <38891707+frdel@users.noreply.github.com> Date: Thu Feb 13 21:35:47 2025 +0100 Merge branch 'development' into testing commit 2a9b30eddf51430f98c70e078502e5d1efe9195e Merge: 9ecf5b1 9fb3daf Author: frdel <38891707+frdel@users.noreply.github.com> Date: Sat Feb 8 10:33:48 2025 +0100 Merge branch 'testing' commit 9fb3daf76891d0a04f09108761e81668a32f7a13 Merge: c87b7aa 2eef196 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Sat Feb 8 10:33:31 2025 +0100 Merge branch 'development' into testing commit 9ecf5b11982e33bd0099acd46976791cddf2e4f6 Merge: 433db33 c87b7aa Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Jan 21 15:28:17 2025 +0100 Merge branch 'testing' commit c87b7aa38825f483d85300bb3a96e25e24b1c536 Merge: eb9411e 7a7d2ee Author: frdel <38891707+frdel@users.noreply.github.com> Date: Mon Jan 20 22:12:35 2025 +0100 Merge branch 'development' into testing commit 433db3336add663b0f4a1509cc8b2105afb95d06 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Mon Jan 20 10:09:50 2025 +0100 readme update commit 79826b913953781317c210d68794556e53896895 Merge: cf244e2 eb9411e Author: frdel <38891707+frdel@users.noreply.github.com> Date: Sun Jan 19 21:12:06 2025 +0100 Merge branch 'testing' commit eb9411e9f3ed668b835f7d522ece06930b449bff Merge: 0ce3fc7 97b5e64 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Sun Jan 19 21:11:39 2025 +0100 Merge branch 'development' into testing commit 0ce3fc762eb92af19434640130dcb8d6beee8138 Merge: e16290b 665b6c5 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Sun Jan 19 20:51:52 2025 +0100 Merge branch 'development' into testing commit e16290b444b5be4782c8221a432ab733d36e896f Merge: 74b85b8 cd82c44 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Sun Jan 19 14:43:43 2025 +0100 Merge branch 'development' into testing commit 74b85b80f58b1d3e386a226bf733682b6001c362 Merge: 097ea67 04fdc67 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Sat Jan 18 23:05:59 2025 +0100 Merge branch 'development' into testing commit 097ea67478615356ded8eca2f65957aa60e44327 Merge: 2502044 7a3de51 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Sat Jan 18 00:22:47 2025 +0100 Merge branch 'development' into testing commit cf244e241fd3971c5bb58b7b923cd21030073c2b Merge: 13beedc 2502044 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Fri Jan 17 08:53:26 2025 +0100 Merge branch 'testing' commit 2502044d7ff444ccb9e1790392f39a2e580fe299 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Fri Jan 17 08:53:09 2025 +0100 Fix await in memory_save commit 13beedc98e1ef6f15852495b111754134594c701 Merge: 3cefa15 a812f84 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Sat Dec 21 17:31:28 2024 +0100 Merge branch 'testing' commit 3cefa150c36f6872bd9e78bb1e35aaeff38bc7be Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Nov 26 10:57:43 2024 +0100 Github sponsors --- README.md | 9 +++++++-- docs/res/showcase-thumb.png | Bin 0 -> 81356 bytes 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 docs/res/showcase-thumb.png diff --git a/README.md b/README.md index b0a6a5a3e..d38f9fa34 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ # `Agent Zero` -[![Thanks to Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-Thanks%20to%20Sponsors-FF69B4?style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/frdel) [![Join our Skool Community](https://img.shields.io/badge/Skool-Join%20our%20Community-4A90E2?style=for-the-badge&logo=skool&logoColor=white)](https://www.skool.com/agent-zero) [![Join our Discord](https://img.shields.io/badge/Discord-Join%20our%20server-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/B8KZKNsPpj) [![Subscribe on YouTube](https://img.shields.io/badge/YouTube-Subscribe-red?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/@AgentZeroFW) [![Connect on LinkedIn](https://img.shields.io/badge/LinkedIn-Connect-blue?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/jan-tomasek/) [![Follow on X.com](https://img.shields.io/badge/X.com-Follow-1DA1F2?style=for-the-badge&logo=x&logoColor=white)](https://x.com/JanTomasekDev) +[![Thanks to Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-Thanks%20to%20Sponsors-FF69B4?style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/frdel) [![Join our Skool Community](https://img.shields.io/badge/Skool-Join%20our%20Community-4A90E2?style=for-the-badge&logo=skool&logoColor=white)](https://www.skool.com/agent-zero) [![Join our Discord](https://img.shields.io/badge/Discord-Join%20our%20server-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/B8KZKNsPpj) [![Subscribe on YouTube](https://img.shields.io/badge/YouTube-Subscribe-red?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/@AgentZeroFW) [![Connect on LinkedIn](https://img.shields.io/badge/LinkedIn-Connect-blue?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/jan-tomasek/) [![Follow on Warpcast](https://img.shields.io/badge/Warpcast-Follow-5A32F3?style=for-the-badge)](https://warpcast.com/agent-zero) + +> **Note:** Agent Zero does not use Twitter/X. Any Twitter/X accounts claiming to represent this project are fake. [Installation](./docs/installation.md) • [How to update](./docs/installation.md#how-to-update-agent-zero) • @@ -14,12 +16,15 @@ +[![Showcase](/docs/res/showcase-thumb.png)](https://youtu.be/lazLNcEYsiQ) + + + See [www.agent-zero.ai](https://agent-zero.ai) for more info [![Browser Agent](/docs/res/web_screenshot.jpg)](https://agent-zero.ai) -[![Browser Agent](/docs/res/081_vid.png)](https://youtu.be/quv145buW74) > [!NOTE] > **🎉 v0.8.1 Release**: Now featuring a browser agent capable of using Chromium for web interactions! This enables Agent Zero to browse the web, gather information, and interact with web content autonomously. diff --git a/docs/res/showcase-thumb.png b/docs/res/showcase-thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..c5e6d9d15ac0afa47d9810a118989b24f9005a53 GIT binary patch literal 81356 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�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 Date: Mon, 31 Mar 2025 07:09:54 +0000 Subject: [PATCH 4/5] Google model in embeddings as well as browser model enabled. (#351) * gemini embeddings as well as gemini in browser models works now --- models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models.py b/models.py index 4b517f1e2..09d0705a6 100644 --- a/models.py +++ b/models.py @@ -20,7 +20,7 @@ from langchain_huggingface import ( HuggingFaceEndpoint, ) from langchain_google_genai import ( - GoogleGenerativeAI, + ChatGoogleGenerativeAI, HarmBlockThreshold, HarmCategory, embeddings as google_embeddings, @@ -267,7 +267,7 @@ def get_google_chat( ): if not api_key: api_key = get_api_key("google") - return GoogleGenerativeAI(model=model_name, google_api_key=api_key, safety_settings={HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE}, **kwargs) # type: ignore + return ChatGoogleGenerativeAI(model=model_name, google_api_key=api_key, safety_settings={HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE}, **kwargs) # type: ignore def get_google_embedding( @@ -277,7 +277,7 @@ def get_google_embedding( ): if not api_key: api_key = get_api_key("google") - return google_embeddings.GoogleGenerativeAIEmbeddings(model=model_name, api_key=api_key, **kwargs) # type: ignore + return google_embeddings.GoogleGenerativeAIEmbeddings(model=model_name, google_api_key=api_key, **kwargs) # type: ignore # Mistral models From 8a9de4a2ee334fc34d6a71673273c7272308480b Mon Sep 17 00:00:00 2001 From: frdel <38891707+frdel@users.noreply.github.com> Date: Tue, 1 Apr 2025 13:55:50 +0200 Subject: [PATCH 5/5] vision prototype, terminal sessions Vision functionality, vision_load tool History.py re-work to support and optimize attachments token usage Code execution tool support for multiple sessions in parallel --- agent.py | 75 +++-- prompts/default/agent.system.instruments.md | 5 +- prompts/default/agent.system.memories.md | 3 +- prompts/default/agent.system.solutions.md | 3 +- prompts/default/agent.system.tool.code_exe.md | 7 +- prompts/default/agent.system.tools_vision.md | 17 +- prompts/default/fw.tool_result.md | 7 +- python/api/ctx_window_get.py | 8 +- python/api/history_get.py | 4 +- python/helpers/files.py | 36 +++ python/helpers/history.py | 285 ++++++++++-------- python/helpers/images.py | 35 +++ python/helpers/runtime.py | 22 ++ python/helpers/tool.py | 7 +- python/helpers/whisper.py | 2 +- python/tools/call_subordinate.py | 2 +- python/tools/code_execution_tool.py | 97 ++++-- python/tools/input.py | 2 +- python/tools/vision_load.py | 79 +++++ webui/js/history.js | 5 +- 20 files changed, 492 insertions(+), 209 deletions(-) create mode 100644 python/helpers/images.py create mode 100644 python/tools/vision_load.py diff --git a/agent.py b/agent.py index 415b4a20e..d6bc73973 100644 --- a/agent.py +++ b/agent.py @@ -10,7 +10,12 @@ import models from langchain_core.prompt_values import ChatPromptValue from python.helpers import extract_tools, rate_limiter, files, errors, history, tokens from python.helpers.print_style import PrintStyle -from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, StringPromptTemplate +from langchain_core.prompts import ( + ChatPromptTemplate, + MessagesPlaceholder, + HumanMessagePromptTemplate, + StringPromptTemplate, +) from langchain_core.prompts.image import ImagePromptTemplate from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage from langchain_core.language_models.chat_models import BaseChatModel @@ -91,7 +96,7 @@ class AgentContext: else: current_agent = self.agent0 - self.task =self.run_task(current_agent.monologue) + self.task = self.run_task(current_agent.monologue) return self.task def communicate(self, msg: "UserMessage", broadcast_level: int = 1): @@ -130,9 +135,9 @@ class AgentContext: async def _process_chain(self, agent: "Agent", msg: "UserMessage|str", user=True): try: msg_template = ( - await agent.hist_add_user_message(msg) # type: ignore + agent.hist_add_user_message(msg) # type: ignore if user - else await agent.hist_add_tool_result( + else agent.hist_add_tool_result( tool_name="call_subordinate", tool_result=msg # type: ignore ) ) @@ -283,9 +288,6 @@ class Agent: printer.stream(chunk) self.log_from_stream(full, log) - # store as last context window content - self.set_data(Agent.DATA_NAME_CTX_WINDOW, prompt.format()) - agent_response = await self.call_chat_model( prompt, callback=stream_callback ) @@ -296,10 +298,10 @@ class Agent: self.loop_data.last_response == agent_response ): # if assistant_response is the same as last message in history, let him know # Append the assistant's response to the history - await self.hist_add_ai_response(agent_response) + self.hist_add_ai_response(agent_response) # Append warning message to the history warning_msg = self.read_prompt("fw.msg_repeat.md") - await self.hist_add_warning(message=warning_msg) + self.hist_add_warning(message=warning_msg) PrintStyle(font_color="orange", padding=True).print( warning_msg ) @@ -307,7 +309,7 @@ class Agent: else: # otherwise proceed with tool # Append the assistant's response to the history - await self.hist_add_ai_response(agent_response) + self.hist_add_ai_response(agent_response) # process tools requested in agent message tools_result = await self.process_tools(agent_response) if tools_result: # final response of message loop available @@ -319,7 +321,7 @@ class Agent: except RepairableException as e: # Forward repairable errors to the LLM, maybe it can fix them error_message = errors.format_error(e) - await self.hist_add_warning(error_message) + self.hist_add_warning(error_message) PrintStyle(font_color="red", padding=True).print(error_message) self.context.log.log(type="error", content=error_message) except Exception as e: @@ -358,23 +360,31 @@ class Agent: extras += history.Message(False, content=extra).output() loop_data.extras_temporary.clear() - # combine history and extras - history_combined: list[OutputMessage] = history.group_outputs_abab(loop_data.history_output + extras) - - # convert history to LLM format - history_langchain: list[BaseMessage] = history.output_langchain(history_combined) - - PrintStyle(font_color="grey", background_color="black", bold=True, padding=True).print( - f"History Langchain: {history_langchain}" + # convert history + extras to LLM format + history_langchain: list[BaseMessage] = history.output_langchain( + loop_data.history_output + extras ) # build chain from system prompt, message history and model + system_text = "\n\n".join(loop_data.system) prompt = ChatPromptTemplate.from_messages( [ - SystemMessage(content="\n\n".join(loop_data.system)), + SystemMessage(content=system_text), *history_langchain, ] ) + + # store as last context window content + self.set_data( + Agent.DATA_NAME_CTX_WINDOW, + { + "text": prompt.format(), + "tokens": self.history.get_tokens() + + tokens.approximate_tokens(system_text) + + tokens.approximate_tokens(history.output_text(extras)), + }, + ) + return prompt def handle_critical_exception(self, exception: Exception): @@ -441,12 +451,12 @@ class Agent: def set_data(self, field: str, value): self.data[field] = value - def hist_add_message(self, ai: bool, content: history.MessageContent): - return self.history.add_message(ai=ai, content=content) - - async def hist_add_user_message( - self, message: UserMessage, intervention: bool = False + def hist_add_message( + self, ai: bool, content: history.MessageContent, tokens: int = 0 ): + return self.history.add_message(ai=ai, content=content, tokens=tokens) + + def hist_add_user_message(self, message: UserMessage, intervention: bool = False): self.history.new_topic() # user message starts a new topic in history # load message template based on intervention @@ -476,19 +486,18 @@ class Agent: self.last_user_message = msg return msg - async def hist_add_ai_response(self, message: str): + def hist_add_ai_response(self, message: str): self.loop_data.last_response = message content = self.parse_prompt("fw.ai_response.md", message=message) return self.hist_add_message(True, content=content) - async def hist_add_warning(self, message: history.MessageContent): + def hist_add_warning(self, message: history.MessageContent): content = self.parse_prompt("fw.warning.md", message=message) return self.hist_add_message(False, content=content) - async def hist_add_tool_result(self, tool_name: str, tool_result: str, attachments: list[str] = []): - attachments_str = json.dumps(attachments).replace("\n", "") + def hist_add_tool_result(self, tool_name: str, tool_result: str): content = self.parse_prompt( - "fw.tool_result.md", tool_name=tool_name, tool_result=tool_result, attachments=attachments_str + "fw.tool_result.md", tool_name=tool_name, tool_result=tool_result ) return self.hist_add_message(False, content=content) @@ -620,9 +629,9 @@ class Agent: msg = self.intervention self.intervention = None # reset the intervention message if progress.strip(): - await self.hist_add_ai_response(progress) + self.hist_add_ai_response(progress) # append the intervention message - await self.hist_add_user_message(msg, intervention=True) + self.hist_add_user_message(msg, intervention=True) raise InterventionException(msg) async def wait_if_paused(self): @@ -649,7 +658,7 @@ class Agent: return response.message else: msg = self.read_prompt("fw.msg_misformat.md") - await self.hist_add_warning(msg) + self.hist_add_warning(msg) PrintStyle(font_color="red", padding=True).print(msg) self.context.log.log( type="error", content=f"{self.agent_name}: Message misformat" diff --git a/prompts/default/agent.system.instruments.md b/prompts/default/agent.system.instruments.md index 677907270..ece095724 100644 --- a/prompts/default/agent.system.instruments.md +++ b/prompts/default/agent.system.instruments.md @@ -1,4 +1,5 @@ # Instruments -- following are instruments at disposal: +- following are instruments at disposal +- do not overly rely on them they might not be relevant -{{instruments}} \ No newline at end of file +{{instruments}} diff --git a/prompts/default/agent.system.memories.md b/prompts/default/agent.system.memories.md index f7ab13fa5..ae32bf361 100644 --- a/prompts/default/agent.system.memories.md +++ b/prompts/default/agent.system.memories.md @@ -1,4 +1,5 @@ # Memories on the topic -- following are your memories about current topic: +- following are memories about current topic +- do not overly rely on them they might not be relevant {{memories}} \ No newline at end of file diff --git a/prompts/default/agent.system.solutions.md b/prompts/default/agent.system.solutions.md index 0d8743d15..926f35b51 100644 --- a/prompts/default/agent.system.solutions.md +++ b/prompts/default/agent.system.solutions.md @@ -1,4 +1,5 @@ # Solutions from the past -- following are your memories about successful solutions of related problems: +- following are memories about successful solutions of related problems +- do not overly rely on them they might not be relevant {{solutions}} \ No newline at end of file diff --git a/prompts/default/agent.system.tool.code_exe.md b/prompts/default/agent.system.tool.code_exe.md index 9be20508f..cd7f2a8e1 100644 --- a/prompts/default/agent.system.tool.code_exe.md +++ b/prompts/default/agent.system.tool.code_exe.md @@ -3,10 +3,9 @@ execute terminal commands python nodejs code for computation or software tasks place code in "code" arg; escape carefully and indent properly select "runtime" arg: "terminal" "python" "nodejs" "output" "reset" -for dialogues (Y/N etc.), use "terminal" runtime next step, send answer +select "session" number, 0 default, others for multitasking if code runs long, use "output" to wait, "reset" to kill process use "pip" "npm" "apt-get" in "terminal" to install packages -important: never use implicit print/output—it doesn't work! to output, use print() or console.log() if tool outputs error, adjust code before retrying; knowledge_tool can help important: check code for placeholders or demo data; replace with real variables; don't reuse snippets @@ -26,6 +25,7 @@ usage: "tool_name": "code_execution_tool", "tool_args": { "runtime": "python", + "session": 0, "code": "import os\nprint(os.getcwd())", } } @@ -41,6 +41,7 @@ usage: "tool_name": "code_execution_tool", "tool_args": { "runtime": "terminal", + "session": 0, "code": "apt-get install zip", } } @@ -55,6 +56,7 @@ usage: "tool_name": "code_execution_tool", "tool_args": { "runtime": "output", + "session": 0, } } ~~~ @@ -68,6 +70,7 @@ usage: "tool_name": "code_execution_tool", "tool_args": { "runtime": "reset", + "session": 0, } } ~~~ \ No newline at end of file diff --git a/prompts/default/agent.system.tools_vision.md b/prompts/default/agent.system.tools_vision.md index 0af7e7527..dd04e8b26 100644 --- a/prompts/default/agent.system.tools_vision.md +++ b/prompts/default/agent.system.tools_vision.md @@ -1,3 +1,18 @@ ## "Multimodal (Vision) Agent Tools" available: -None yet. In future, this section will contain vision-only tools +### vision_load: +load image data to LLM +use paths arg for attachments + +**Example usage**: +```json +{ + "thoughts": [ + "I need to see the image...", + ], + "tool_name": "vision_load", + "tool_args": { + "paths": ["/path/to/image.png"], + } +} +``` \ No newline at end of file diff --git a/prompts/default/fw.tool_result.md b/prompts/default/fw.tool_result.md index b45d456e8..ef41f23bf 100644 --- a/prompts/default/fw.tool_result.md +++ b/prompts/default/fw.tool_result.md @@ -1,7 +1,6 @@ -~~~json +```json { "tool_name": {{tool_name}}, - "tool_result": {{tool_result}}, - "attachments": {{attachments}} + "tool_result": {{tool_result}} } -~~~ +``` diff --git a/python/api/ctx_window_get.py b/python/api/ctx_window_get.py index a8e20db94..16a4438b7 100644 --- a/python/api/ctx_window_get.py +++ b/python/api/ctx_window_get.py @@ -9,6 +9,10 @@ class GetCtxWindow(ApiHandler): context = self.get_context(ctxid) agent = context.streaming_agent or context.agent0 window = agent.get_data(agent.DATA_NAME_CTX_WINDOW) - size = tokens.approximate_tokens(window) + if not window or not isinstance(window, dict): + return {"content": "", "tokens": 0} - return {"content": window, "tokens": size} + text = window["text"] + tokens = window["tokens"] + + return {"content": text, "tokens": tokens} diff --git a/python/api/history_get.py b/python/api/history_get.py index 579a32890..9b1359c49 100644 --- a/python/api/history_get.py +++ b/python/api/history_get.py @@ -8,8 +8,8 @@ class GetHistory(ApiHandler): ctxid = input.get("context", []) context = self.get_context(ctxid) agent = context.streaming_agent or context.agent0 - history = agent.history.output() - size = tokens.approximate_tokens(agent.history.output_text()) + history = agent.history.output_text() + size = agent.history.get_tokens() return { "history": history, diff --git a/python/helpers/files.py b/python/helpers/files.py index 91bdd0566..bc0e97eb9 100644 --- a/python/helpers/files.py +++ b/python/helpers/files.py @@ -1,6 +1,7 @@ from fnmatch import fnmatch import json import os, re +import base64 import re import shutil @@ -45,6 +46,32 @@ def read_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs): return content +def read_file_bin(_relative_path, _backup_dirs=None): + # init backup dirs + if _backup_dirs is None: + _backup_dirs = [] + + # get absolute path + absolute_path = find_file_in_dirs(_relative_path, _backup_dirs) + + # read binary content + with open(absolute_path, "rb") as f: + return f.read() + + +def read_file_base64(_relative_path, _backup_dirs=None): + # init backup dirs + if _backup_dirs is None: + _backup_dirs = [] + + # get absolute path + absolute_path = find_file_in_dirs(_relative_path, _backup_dirs) + + # read binary content and encode to base64 + with open(absolute_path, "rb") as f: + return base64.b64encode(f.read()).decode('utf-8') + + def replace_placeholders_text(_content: str, **kwargs): # Replace placeholders with values from kwargs for key, value in kwargs.items(): @@ -175,6 +202,15 @@ def write_file_bin(relative_path: str, content: bytes): f.write(content) +def write_file_base64(relative_path: str, content: str): + # decode base64 string to bytes + data = base64.b64decode(content) + abs_path = get_abs_path(relative_path) + os.makedirs(os.path.dirname(abs_path), exist_ok=True) + with open(abs_path, "wb") as f: + f.write(data) + + def delete_file(relative_path: str): abs_path = get_abs_path(relative_path) if os.path.exists(abs_path): diff --git a/python/helpers/history.py b/python/helpers/history.py index 3fcb02426..c09ef61a5 100644 --- a/python/helpers/history.py +++ b/python/helpers/history.py @@ -1,16 +1,14 @@ from abc import abstractmethod import asyncio from collections import OrderedDict +from collections.abc import Mapping import json import math -import os -from typing import Coroutine, Literal, TypedDict, cast +from typing import Coroutine, Literal, TypedDict, cast, Union, Dict, List, Any, override from python.helpers import messages, tokens, settings, call_llm from enum import Enum -from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage -from python.helpers.print_style import PrintStyle -from langchain_core.prompts import HumanMessagePromptTemplate -from typing import Any +from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, AIMessage + BULK_MERGE_COUNT = 3 TOPICS_KEEP_COUNT = 3 CURRENT_TOPIC_RATIO = 0.5 @@ -18,14 +16,22 @@ HISTORY_TOPIC_RATIO = 0.3 HISTORY_BULK_RATIO = 0.2 TOPIC_COMPRESS_RATIO = 0.65 LARGE_MESSAGE_TO_TOPIC_RATIO = 0.25 +RAW_MESSAGE_OUTPUT_TEXT_TRIM = 100 -MessageContent = ( - list["MessageContent"] - | OrderedDict[str, "MessageContent"] - | list[OrderedDict[str, "MessageContent"]] - | str - | list[str] -) + +class RawMessage(TypedDict): + raw_content: "MessageContent" + preview: str | None + + +MessageContent = Union[ + List["MessageContent"], + Dict[str, "MessageContent"], + List[Dict[str, "MessageContent"]], + str, + List[str], + RawMessage, +] class OutputMessage(TypedDict): @@ -37,9 +43,9 @@ class Record: def __init__(self): pass + @abstractmethod def get_tokens(self) -> int: - out = self.output_text() - return tokens.approximate_tokens(out) + pass @abstractmethod async def compress(self) -> bool: @@ -70,10 +76,25 @@ class Record: class Message(Record): - def __init__(self, ai: bool, content: MessageContent): + def __init__(self, ai: bool, content: MessageContent, tokens: int = 0): self.ai = ai self.content = content - self.summary: MessageContent = "" + self.summary: str = "" + self.tokens: int = tokens or self.calculate_tokens() + + @override + def get_tokens(self) -> int: + if not self.tokens: + self.tokens = self.calculate_tokens() + return self.tokens + + def calculate_tokens(self): + text = self.output_text() + return tokens.approximate_tokens(text) + + def set_summary(self, summary: str): + self.summary = summary + self.tokens = self.calculate_tokens() async def compress(self): return False @@ -93,12 +114,15 @@ class Message(Record): "ai": self.ai, "content": self.content, "summary": self.summary, + "tokens": self.tokens, } @staticmethod def from_dict(data: dict, history: "History"): - msg = Message(ai=data["ai"], content=data.get("content", "Content lost")) + content = data.get("content", "Content lost") + msg = Message(ai=data["ai"], content=content) msg.summary = data.get("summary", "") + msg.tokens = data.get("tokens", 0) return msg @@ -108,8 +132,16 @@ class Topic(Record): self.summary: str = "" self.messages: list[Message] = [] - def add_message(self, ai: bool, content: MessageContent): - msg = Message(ai=ai, content=content) + def get_tokens(self): + if self.summary: + return tokens.approximate_tokens(self.summary) + else: + return sum(msg.get_tokens() for msg in self.messages) + + def add_message( + self, ai: bool, content: MessageContent, tokens: int = 0 + ) -> Message: + msg = Message(ai=ai, content=content, tokens=tokens) self.messages.append(msg) return msg @@ -118,7 +150,7 @@ class Topic(Record): return [OutputMessage(ai=False, content=self.summary)] else: msgs = [m for r in self.messages for m in r.output()] - return group_outputs_abab(msgs) + return msgs async def summarize(self): self.summary = await self.summarize_messages(self.messages) @@ -134,22 +166,31 @@ class Topic(Record): ) large_msgs = [] for m in (m for m in self.messages if not m.summary): + # TODO refactor this out = m.output() text = output_text(out) - tok = tokens.approximate_tokens(text) + tok = m.get_tokens() leng = len(text) if tok > msg_max_size: large_msgs.append((m, tok, leng, out)) large_msgs.sort(key=lambda x: x[1], reverse=True) for msg, tok, leng, out in large_msgs: trim_to_chars = leng * (msg_max_size / tok) - trunc = messages.truncate_dict_by_ratio( - self.history.agent, - out[0]["content"], - trim_to_chars * 1.15, - trim_to_chars * 0.85, - ) - msg.summary = trunc + # raw messages will be replaced as a whole, they would become invalid when truncated + if _is_raw_message(out[0]["content"]): + msg.set_summary( + "Message content replaced to save space in context window" + ) + + # regular messages will be truncated + else: + trunc = messages.truncate_dict_by_ratio( + self.history.agent, + out[0]["content"], + trim_to_chars * 1.15, + trim_to_chars * 0.85, + ) + msg.set_summary(_json_dumps(trunc)) return True return False @@ -175,6 +216,7 @@ class Topic(Record): return False async def summarize_messages(self, messages: list[Message]): + # FIXME: vision bytes are sent to utility LLM, send summary instead msg_txt = [m.output_text() for m in messages] summary = await self.history.agent.call_utility_model( system=self.history.agent.read_prompt("fw.topic_summary.sys.md"), @@ -194,9 +236,9 @@ class Topic(Record): @staticmethod def from_dict(data: dict, history: "History"): topic = Topic(history=history) - topic.summary = data["summary"] + topic.summary = data.get("summary", "") topic.messages = [ - Message.from_dict(m, history=history) for m in data["messages"] + Message.from_dict(m, history=history) for m in data.get("messages", []) ] return topic @@ -214,7 +256,7 @@ class Bulk(Record): return [OutputMessage(ai=False, content=self.summary)] else: msgs = [m for r in self.records for m in r.output()] - return group_outputs_abab(msgs) + return msgs async def compress(self): return False @@ -253,8 +295,15 @@ class History(Record): self.current = Topic(history=self) self.agent: Agent = agent + def get_tokens(self) -> int: + return ( + self.get_bulks_tokens() + + self.get_topics_tokens() + + self.get_current_topic_tokens() + ) + def is_over_limit(self): - limit = get_ctx_size_for_history() + limit = _get_ctx_size_for_history() total = self.get_tokens() return total > limit @@ -267,15 +316,10 @@ class History(Record): def get_current_topic_tokens(self) -> int: return self.current.get_tokens() - def get_tokens(self) -> int: - return ( - self.get_bulks_tokens() - + self.get_topics_tokens() - + self.get_current_topic_tokens() - ) - - def add_message(self, ai: bool, content: MessageContent): - return self.current.add_message(ai, content=content) + def add_message( + self, ai: bool, content: MessageContent, tokens: int = 0 + ) -> Message: + return self.current.add_message(ai, content=content, tokens=tokens) def new_topic(self): if self.current.messages: @@ -287,7 +331,6 @@ class History(Record): result += [m for b in self.bulks for m in b.output()] result += [m for t in self.topics for m in t.output()] result += self.current.output() - result = group_outputs_abab(result) return result @staticmethod @@ -307,7 +350,7 @@ class History(Record): def serialize(self): data = self.to_dict() - return json.dumps(data) + return _json_dumps(data) async def compress(self): compressed = False @@ -317,7 +360,7 @@ class History(Record): self.get_topics_tokens(), self.get_bulks_tokens(), ) - total = get_ctx_size_for_history() + total = _get_ctx_size_for_history() ratios = [ (curr, CURRENT_TOPIC_RATIO, "current_topic"), (hist, HISTORY_TOPIC_RATIO, "history_topic"), @@ -392,25 +435,46 @@ class History(Record): def deserialize_history(json_data: str, agent) -> History: history = History(agent=agent) if json_data: - data = json.loads(json_data) + data = _json_loads(json_data) history = History.from_dict(data, history=history) return history -def get_ctx_size_for_history() -> int: +def _get_ctx_size_for_history() -> int: set = settings.get_settings() return int(set["chat_model_ctx_length"] * set["chat_model_ctx_history"]) -def serialize_output(output: OutputMessage, ai_label="ai", human_label="human"): - return f'{ai_label if output["ai"] else human_label}: {serialize_content(output["content"])}' +def _stringify_output(output: OutputMessage, ai_label="ai", human_label="human"): + return f'{ai_label if output["ai"] else human_label}: {_stringify_content(output["content"])}' -def serialize_content(content: MessageContent) -> str: +def _stringify_content(content: MessageContent) -> str: + # already a string if isinstance(content, str): return content + + # raw messages return preview or trimmed json + if _is_raw_message(content): + preview: str = content.get("preview", "") # type: ignore + if preview: + return preview + text = _json_dumps(content) + if len(text) > RAW_MESSAGE_OUTPUT_TEXT_TRIM: + return text[:RAW_MESSAGE_OUTPUT_TEXT_TRIM] + "... TRIMMED" + return text + + # regular messages of non-string are dumped as json + return _json_dumps(content) + + +def _output_content_langchain(content: MessageContent): + if isinstance(content, str): + return content + if _is_raw_message(content): + return content["raw_content"] # type: ignore try: - return json.dumps(content) + return _json_dumps(content) except Exception as e: raise e @@ -421,98 +485,73 @@ def group_outputs_abab(outputs: list[OutputMessage]) -> list[OutputMessage]: if result and result[-1]["ai"] == out["ai"]: result[-1] = OutputMessage( ai=result[-1]["ai"], - content=merge_outputs(result[-1]["content"], out["content"]), + content=_merge_outputs(result[-1]["content"], out["content"]), ) else: result.append(out) return result -def output_langchain(messages: list[OutputMessage]) -> list[BaseMessage]: +def group_messages_abab(messages: list[BaseMessage]) -> list[BaseMessage]: + result = [] + for msg in messages: + if result and isinstance(result[-1], type(msg)): + # create new instance of the same type with merged content + result[-1] = type(result[-1])(content=_merge_outputs(result[-1].content, msg.content)) # type: ignore + else: + result.append(msg) + return result + + +def output_langchain(messages: list[OutputMessage]): result = [] for m in messages: if m["ai"]: - result.append(AIMessage(content=serialize_content(m["content"]))) + # result.append(AIMessage(content=serialize_content(m["content"]))) + result.append(AIMessage(_output_content_langchain(content=m["content"]))) # type: ignore else: - contents = m["content"] - - # sometimes content is a list sometimes not - if not isinstance(contents, list): - contents = [contents] - - PrintStyle(font_color="grey", background_color="black", bold=True, padding=True).print( - f"Contents: {json.dumps(contents, indent=2)}" - ) - - template: list[dict[str, str]] = [] # type: ignore - message = "" - images = {} - for _, content in enumerate(contents): - if message: - # the first message is the user message, then the memory and solutions - message += "\n\n--- Memory & Solutions Section: ---\n\n" - - message += serialize_content(content) - - if isinstance(content, dict) and "attachments" in content: - attachments: list[str] = cast(list[str], content["attachments"]) - for attachment in attachments: - if not os.path.exists(str(attachment)): - continue - if attachment not in images: - import base64 - from mimetypes import guess_type - mime_type, _ = guess_type(str(attachment)) - if mime_type.startswith("image/"): - # Read and encode the image file - with open(str(attachment), "rb") as image_file: - base64_encoded_data = base64.b64encode(image_file.read()).decode('utf-8') - # Construct the data URL - images[attachment] = f"data:{mime_type};base64,{base64_encoded_data}" - - if message: - template.append({"type": "text", "text": message}) - if images: - for _, image in images.items(): - template.append({"type": "image_url", "image_url": image}) - if template: - # only jinja2 is safe for json, both mustache({{...}}) and f-string({...}) are not - result.append(HumanMessagePromptTemplate.from_template(template=template, partial_variables={}, template_format="jinja2")) # type: ignore - - PrintStyle(font_color="grey", background_color="black", bold=True, padding=True).print( - f"Result: {result}" - ) + # result.append(HumanMessage(content=serialize_content(m["content"]))) + result.append(HumanMessage(_output_content_langchain(content=m["content"]))) # type: ignore + # ensure message type alternation + result = group_messages_abab(result) return result def output_text(messages: list[OutputMessage], ai_label="ai", human_label="human"): - return "\n".join(serialize_output(o, ai_label, human_label) for o in messages) + return "\n".join(_stringify_output(o, ai_label, human_label) for o in messages) -def merge_outputs(a: MessageContent, b: MessageContent) -> MessageContent: +def _merge_outputs(a: MessageContent, b: MessageContent) -> MessageContent: + if isinstance(a, str) and isinstance(b, str): + return a + b + if not isinstance(a, list): a = [a] if not isinstance(b, list): b = [b] - return a + b # type: ignore - # return merge_properties(a, b) + + return cast(MessageContent, a + b) -def merge_properties(a: MessageContent, b: MessageContent) -> MessageContent: - if isinstance(a, list): - if isinstance(b, list): - return a + b # type: ignore +def _merge_properties( + a: Dict[str, MessageContent], b: Dict[str, MessageContent] +) -> Dict[str, MessageContent]: + result = a.copy() + for k, v in b.items(): + if k in result: + result[k] = _merge_outputs(result[k], v) else: - return a + [b] - elif isinstance(b, list): - return [a] + b # type: ignore - elif isinstance(a, dict) and isinstance(b, dict): - for key, value in b.items(): - if key in a: - a[key] = merge_properties(a[key], value) - else: - a[key] = value - return a - elif isinstance(a, str) and isinstance(b, str): - return a + b - raise ValueError(f"Cannot merge {a} and {b}") + result[k] = v + return result + + +def _is_raw_message(obj: object) -> bool: + return isinstance(obj, Mapping) and "raw_content" in obj + + +def _json_dumps(obj): + return json.dumps(obj, ensure_ascii=False) + + +def _json_loads(obj): + return json.loads(obj) diff --git a/python/helpers/images.py b/python/helpers/images.py new file mode 100644 index 000000000..9377f1e9c --- /dev/null +++ b/python/helpers/images.py @@ -0,0 +1,35 @@ +from PIL import Image +import io +import math + + +def compress_image(image_data: bytes, *, max_pixels: int = 256_000, quality: int = 50) -> bytes: + """Compress an image by scaling it down and converting to JPEG with quality settings. + + Args: + image_data: Raw image bytes + max_pixels: Maximum number of pixels in the output image (width * height) + quality: JPEG quality setting (1-100) + + Returns: + Compressed image as bytes + """ + # load image from bytes + img = Image.open(io.BytesIO(image_data)) + + # calculate scaling factor to get to max_pixels + current_pixels = img.width * img.height + if current_pixels > max_pixels: + scale = math.sqrt(max_pixels / current_pixels) + new_width = int(img.width * scale) + new_height = int(img.height * scale) + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # convert to RGB if needed (for JPEG) + if img.mode in ('RGBA', 'P'): + img = img.convert('RGB') + + # save as JPEG with compression + output = io.BytesIO() + img.save(output, format='JPEG', quality=quality, optimize=True) + return output.getvalue() diff --git a/python/helpers/runtime.py b/python/helpers/runtime.py index 6709c6fdf..189d45288 100644 --- a/python/helpers/runtime.py +++ b/python/helpers/runtime.py @@ -2,6 +2,9 @@ import argparse import inspect from typing import TypeVar, Callable, Awaitable, Union, overload, cast from python.helpers import dotenv, rfc, settings +import asyncio +import threading +import queue T = TypeVar('T') R = TypeVar('R') @@ -102,3 +105,22 @@ def _get_rfc_url() -> str: url = url+":"+str(set["rfc_port_http"]) url += "/rfc" return url + + +def call_development_function_sync(func: Union[Callable[..., T], Callable[..., Awaitable[T]]], *args, **kwargs) -> T: + # run async function in sync manner + result_queue = queue.Queue() + + def run_in_thread(): + result = asyncio.run(call_development_function(func, *args, **kwargs)) + result_queue.put(result) + + thread = threading.Thread(target=run_in_thread) + thread.start() + thread.join(timeout=30) # wait for thread with timeout + + if thread.is_alive(): + raise TimeoutError("Function call timed out after 30 seconds") + + result = result_queue.get_nowait() + return cast(T, result) diff --git a/python/helpers/tool.py b/python/helpers/tool.py index fcfb51070..fef3058ac 100644 --- a/python/helpers/tool.py +++ b/python/helpers/tool.py @@ -1,5 +1,6 @@ from abc import abstractmethod -from dataclasses import dataclass, field +from dataclasses import dataclass + from agent import Agent from python.helpers.print_style import PrintStyle @@ -8,8 +9,6 @@ from python.helpers.print_style import PrintStyle class Response: message:str break_loop: bool - attachments: list[str] = field(default_factory=list[str]) - class Tool: @@ -34,7 +33,7 @@ class Tool: async def after_execution(self, response: Response, **kwargs): text = response.message.strip() - await self.agent.hist_add_tool_result(self.name, text, response.attachments) + self.agent.hist_add_tool_result(self.name, text) PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}'") PrintStyle(font_color="#85C1E9").print(response.message) self.log.update(content=response.message) diff --git a/python/helpers/whisper.py b/python/helpers/whisper.py index f92e4c8b5..0f644487d 100644 --- a/python/helpers/whisper.py +++ b/python/helpers/whisper.py @@ -30,7 +30,7 @@ async def _preload(model_name:str): is_updating_model = True if not _model or _model_name != model_name: PrintStyle.standard(f"Loading Whisper model: {model_name}") - _model = whisper.load_model(model_name) + _model = whisper.load_model(name=model_name) # type: ignore _model_name = model_name finally: is_updating_model = False diff --git a/python/tools/call_subordinate.py b/python/tools/call_subordinate.py index 73eb9b574..3a50ffdc4 100644 --- a/python/tools/call_subordinate.py +++ b/python/tools/call_subordinate.py @@ -18,7 +18,7 @@ class Delegation(Tool): # add user message to subordinate agent subordinate: Agent = self.agent.get_data(Agent.DATA_NAME_SUBORDINATE) - await subordinate.hist_add_user_message(UserMessage(message=message, attachments=[])) + subordinate.hist_add_user_message(UserMessage(message=message, attachments=[])) # run subordinate monologue result = await subordinate.monologue() # result diff --git a/python/tools/code_execution_tool.py b/python/tools/code_execution_tool.py index 6a0686875..593e734fa 100644 --- a/python/tools/code_execution_tool.py +++ b/python/tools/code_execution_tool.py @@ -12,7 +12,7 @@ from python.helpers.docker import DockerContainerManager @dataclass class State: - shell: LocalInteractiveSession | SSHInteractiveSession + shells: dict[int, LocalInteractiveSession | SSHInteractiveSession] docker: DockerContainerManager | None @@ -27,19 +27,26 @@ class CodeExecution(Tool): # os.chdir(files.get_abs_path("./work_dir")) #change CWD to work_dir runtime = self.args.get("runtime", "").lower().strip() + session = int(self.args.get("session", 0)) if runtime == "python": - response = await self.execute_python_code(self.args["code"]) + response = await self.execute_python_code( + code=self.args["code"], session=session + ) elif runtime == "nodejs": - response = await self.execute_nodejs_code(self.args["code"]) + response = await self.execute_nodejs_code( + code=self.args["code"], session=session + ) elif runtime == "terminal": - response = await self.execute_terminal_command(self.args["code"]) + response = await self.execute_terminal_command( + command=self.args["code"], session=session + ) elif runtime == "output": response = await self.get_terminal_output( - wait_with_output=5, wait_without_output=60 + session=session, wait_with_output=5, wait_without_output=60 ) elif runtime == "reset": - response = await self.reset_terminal() + response = await self.reset_terminal(session=session) else: response = self.agent.read_prompt( "fw.code_runtime_wrong.md", runtime=runtime @@ -72,11 +79,15 @@ class CodeExecution(Tool): # PrintStyle().print() def get_log_object(self): - return self.agent.context.log.log(type="code_exe", heading=f"{self.agent.agent_name}: Using tool '{self.name}'", content="", kvps=self.args) - + return self.agent.context.log.log( + type="code_exe", + heading=f"{self.agent.agent_name}: Using tool '{self.name}'", + content="", + kvps=self.args, + ) async def after_execution(self, response, **kwargs): - await self.agent.hist_add_tool_result(self.name, response.message) + self.agent.hist_add_tool_result(self.name, response.message) async def prepare_state(self, reset=False): self.state = self.agent.get_data("_cot_state") @@ -97,7 +108,11 @@ class CodeExecution(Tool): # initialize local or remote interactive shell insterface if self.agent.config.code_exec_ssh_enabled: - pswd = self.agent.config.code_exec_ssh_pass if self.agent.config.code_exec_ssh_pass else await rfc_exchange.get_root_password() + pswd = ( + self.agent.config.code_exec_ssh_pass + if self.agent.config.code_exec_ssh_pass + else await rfc_exchange.get_root_password() + ) shell = SSHInteractiveSession( self.agent.context.log, self.agent.config.code_exec_ssh_addr, @@ -108,42 +123,63 @@ class CodeExecution(Tool): else: shell = LocalInteractiveSession() - self.state = State(shell=shell, docker=docker) + self.state = State(shells={0: shell}, docker=docker) await shell.connect() self.agent.set_data("_cot_state", self.state) - async def execute_python_code(self, code: str, reset: bool = False): + async def execute_python_code(self, session: int, code: str, reset: bool = False): escaped_code = shlex.quote(code) command = f"ipython -c {escaped_code}" - return await self.terminal_session(command, reset) + return await self.terminal_session(session, command, reset) - async def execute_nodejs_code(self, code: str, reset: bool = False): + async def execute_nodejs_code(self, session: int, code: str, reset: bool = False): escaped_code = shlex.quote(code) command = f"node /exe/node_eval.js {escaped_code}" - return await self.terminal_session(command, reset) + return await self.terminal_session(session, command, reset) - async def execute_terminal_command(self, command: str, reset: bool = False): - return await self.terminal_session(command, reset) + async def execute_terminal_command( + self, session: int, command: str, reset: bool = False + ): + return await self.terminal_session(session, command, reset) - async def terminal_session(self, command: str, reset: bool = False): + async def terminal_session(self, session: int, command: str, reset: bool = False): await self.agent.handle_intervention() # wait for intervention and handle it, if paused # try again on lost connection for i in range(2): try: - + if reset: await self.reset_terminal() - self.state.shell.send_command(command) + if session not in self.state.shells: + if self.agent.config.code_exec_ssh_enabled: + pswd = ( + self.agent.config.code_exec_ssh_pass + if self.agent.config.code_exec_ssh_pass + else await rfc_exchange.get_root_password() + ) + shell = SSHInteractiveSession( + self.agent.context.log, + self.agent.config.code_exec_ssh_addr, + self.agent.config.code_exec_ssh_port, + self.agent.config.code_exec_ssh_user, + pswd, + ) + else: + shell = LocalInteractiveSession() + self.state.shells[session] = shell + await shell.connect() - PrintStyle(background_color="white", font_color="#1B4F72", bold=True).print( - f"{self.agent.agent_name} code execution output" - ) - return await self.get_terminal_output() + self.state.shells[session].send_command(command) + + PrintStyle( + background_color="white", font_color="#1B4F72", bold=True + ).print(f"{self.agent.agent_name} code execution output") + return await self.get_terminal_output(session) except Exception as e: - if i==1: + if i == 1: # try again on lost connection PrintStyle.error(str(e)) await self.prepare_state(reset=True) @@ -153,6 +189,7 @@ class CodeExecution(Tool): async def get_terminal_output( self, + session=0, reset_full_output=True, wait_with_output=3, wait_without_output=10, @@ -165,10 +202,10 @@ class CodeExecution(Tool): while max_exec_time <= 0 or time.time() - start_time < max_exec_time: await asyncio.sleep(SLEEP_TIME) # Wait for some output to be generated - full_output, partial_output = await self.state.shell.read_output( + full_output, partial_output = await self.state.shells[session].read_output( timeout=max_exec_time, reset_full_output=reset_full_output ) - reset_full_output = False # only reset once + reset_full_output = False # only reset once await self.agent.handle_intervention() # wait for intervention and handle it, if paused @@ -184,8 +221,10 @@ class CodeExecution(Tool): break return full_output - async def reset_terminal(self): - self.state.shell.close() + async def reset_terminal(self, session=0): + if session in self.state.shells: + self.state.shells[session].close() + del self.state.shells[session] await self.prepare_state(reset=True) response = self.agent.read_prompt("fw.code_reset.md") self.log.update(content=response) diff --git a/python/tools/input.py b/python/tools/input.py index 6e5a812f8..b23c9adee 100644 --- a/python/tools/input.py +++ b/python/tools/input.py @@ -20,4 +20,4 @@ class Input(Tool): return self.agent.context.log.log(type="code_exe", heading=f"{self.agent.agent_name}: Using tool '{self.name}'", content="", kvps=self.args) async def after_execution(self, response, **kwargs): - await self.agent.hist_add_tool_result(self.name, response.message) \ No newline at end of file + self.agent.hist_add_tool_result(self.name, response.message) \ No newline at end of file diff --git a/python/tools/vision_load.py b/python/tools/vision_load.py new file mode 100644 index 000000000..e1c455fa5 --- /dev/null +++ b/python/tools/vision_load.py @@ -0,0 +1,79 @@ +import base64 +from python.helpers.print_style import PrintStyle +from python.helpers.tool import Tool, Response +from python.helpers import runtime, files, images +from mimetypes import guess_type +from python.helpers import history + +# image optimization and token estimation for context window +MAX_PIXELS = 768_000 +QUALITY = 75 +TOKENS_ESTIMATE = 1500 + + +class VisionLoad(Tool): + async def execute(self, paths: list[str] = [], **kwargs) -> Response: + + self.images_dict = {} + template: list[dict[str, str]] = [] # type: ignore + + for path in paths: + if not await runtime.call_development_function(files.exists, str(path)): + continue + + if path not in self.images_dict: + mime_type, _ = guess_type(str(path)) + if mime_type and mime_type.startswith("image/"): + # Read binary file + file_content = await runtime.call_development_function( + files.read_file_base64, str(path) + ) + file_content = base64.b64decode(file_content) + # Compress and convert to JPEG + compressed = images.compress_image( + file_content, max_pixels=MAX_PIXELS, quality=QUALITY + ) + # Encode as base64 + file_content_b64 = base64.b64encode(compressed).decode("utf-8") + + # DEBUG: Save compressed image + # await runtime.call_development_function( + # files.write_file_base64, str(path), file_content_b64 + # ) + + # Construct the data URL (always JPEG after compression) + self.images_dict[path] = file_content_b64 + + return Response(message="dummy", break_loop=False) + + async def after_execution(self, response: Response, **kwargs): + + # build image data messages for LLMs, or error message + content = [] + if self.images_dict: + for _, image in self.images_dict.items(): + content.append( + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{image}"}, + } + ) + # append as raw message content for LLMs with vision tokens estimate + msg = history.RawMessage(raw_content=content, preview="") + self.agent.hist_add_message( + False, content=msg, tokens=TOKENS_ESTIMATE * len(content) + ) + else: + self.agent.hist_add_tool_result(self.name, "No images processed") + + # print and log short version + message = ( + "No images processed" + if not self.images_dict + else f"{len(self.images_dict)} images processed" + ) + PrintStyle( + font_color="#1B4F72", background_color="white", padding=True, bold=True + ).print(f"{self.agent.agent_name}: Response from tool '{self.name}'") + PrintStyle(font_color="#85C1E9").print(message) + self.log.update(result=message) diff --git a/webui/js/history.js b/webui/js/history.js index e62fc615e..040de72fe 100644 --- a/webui/js/history.js +++ b/webui/js/history.js @@ -3,9 +3,10 @@ import { getContext } from "../index.js"; export async function openHistoryModal() { try { const hist = await window.sendJsonData("/history_get", { context: getContext() }); - const data = JSON.stringify(hist.history, null, 4); + // const data = JSON.stringify(hist.history, null, 4); + const data = hist.history const size = hist.tokens - await showEditorModal(data, "json", `History ~${size} tokens`, "Conversation history visible to the LLM. History is compressed to fit into the context window over time."); + await showEditorModal(data, "markdown", `History ~${size} tokens`, "Conversation history visible to the LLM. History is compressed to fit into the context window over time."); } catch (e) { window.toastFetchError("Error fetching history", e) return