From fe46f045fef64ac83d9f0edc2b79115f34eb25d7 Mon Sep 17 00:00:00 2001 From: frdel <38891707+frdel@users.noreply.github.com> Date: Wed, 4 Feb 2026 08:47:06 +0100 Subject: [PATCH] message rendering polishing --- agent.py | 4 +- .../monologue_end/_50_memorize_fragments.py | 296 +++++++++-------- .../monologue_end/_51_memorize_solutions.py | 310 +++++++++--------- .../_90_waiting_for_input_msg.py | 3 + python/helpers/errors.py | 18 +- python/helpers/settings.py | 4 +- webui/css/messages.css | 18 + webui/index.css | 4 + webui/js/messages.js | 212 +++++++----- 9 files changed, 465 insertions(+), 404 deletions(-) diff --git a/agent.py b/agent.py index db56d01a6..6cb96e3a7 100644 --- a/agent.py +++ b/agent.py @@ -590,7 +590,7 @@ class Agent: error_message = errors.format_error(e) self.context.log.log( - type="warning", content="Critical error occurred, retrying..." + type="warning", heading="Critical error occurred, retrying...", content=error_message ) PrintStyle(font_color="orange", padding=True).print( "Critical error occurred, retrying..." @@ -626,9 +626,7 @@ class Agent: PrintStyle(font_color="red", padding=True).print(error_message) self.context.log.log( type="error", - heading="Error", content=error_message, - kvps={"text": error_text}, ) PrintStyle(font_color="red", padding=True).print( f"{self.agent_name}: {error_text}" diff --git a/python/extensions/monologue_end/_50_memorize_fragments.py b/python/extensions/monologue_end/_50_memorize_fragments.py index 9060aef2d..c15aaa90b 100644 --- a/python/extensions/monologue_end/_50_memorize_fragments.py +++ b/python/extensions/monologue_end/_50_memorize_fragments.py @@ -1,5 +1,5 @@ import asyncio -from python.helpers import settings +from python.helpers import settings, errors from python.helpers.extension import Extension from python.helpers.memory import Memory from python.helpers.dirty_json import DirtyJson @@ -33,168 +33,166 @@ class MemorizeMemories(Extension): async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs): - - await asyncio.sleep(15) - - set = settings.get_settings() - - db = await Memory.get(self.agent) - - # get system message and chat history for util llm - system = self.agent.read_prompt("memory.memories_sum.sys.md") - msgs_text = self.agent.concat_messages(self.agent.history) - - # # log query streamed by LLM - # async def log_callback(content): - # log_item.stream(content=content) - - # call util llm to find info in history - memories_json = await self.agent.call_utility_model( - system=system, - message=msgs_text, - # callback=log_callback, - background=True, - ) - - # log data < no need for streaming utility messages - log_item.update(content=memories_json) - - # Add validation and error handling for memories_json - if not memories_json or not isinstance(memories_json, str): - log_item.update(heading="No response from utility model.") - return - - # Strip any whitespace that might cause issues - memories_json = memories_json.strip() - - if not memories_json: - log_item.update(heading="Empty response from utility model.") - return - try: - memories = DirtyJson.parse_string(memories_json) - except Exception as e: - log_item.update(heading=f"Failed to parse memories response: {str(e)}") - return + set = settings.get_settings() - # Validate that memories is a list or convertible to one - if memories is None: - log_item.update(heading="No valid memories found in response.") - return + db = await Memory.get(self.agent) - # If memories is not a list, try to make it one - if not isinstance(memories, list): - if isinstance(memories, (str, dict)): - memories = [memories] - else: - log_item.update(heading="Invalid memories format received.") + # get system message and chat history for util llm + system = self.agent.read_prompt("memory.memories_sum.sys.md") + msgs_text = self.agent.concat_messages(self.agent.history) + + # # log query streamed by LLM + # async def log_callback(content): + # log_item.stream(content=content) + + # call util llm to find info in history + memories_json = await self.agent.call_utility_model( + system=system, + message=msgs_text, + # callback=log_callback, + background=True, + ) + + # log data < no need for streaming utility messages + log_item.update(content=memories_json) + + # Add validation and error handling for memories_json + if not memories_json or not isinstance(memories_json, str): + log_item.update(heading="No response from utility model.") return - if not isinstance(memories, list) or len(memories) == 0: - log_item.update(heading="No useful information to memorize.") - return - else: - memories_txt = "\n\n".join([str(memory) for memory in memories]).strip() - log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt) + # Strip any whitespace that might cause issues + memories_json = memories_json.strip() - # Process memories with intelligent consolidation - total_processed = 0 - total_consolidated = 0 - rem = [] + if not memories_json: + log_item.update(heading="Empty response from utility model.") + return - for memory in memories: - # Convert memory to plain text - txt = f"{memory}" + try: + memories = DirtyJson.parse_string(memories_json) + except Exception as e: + log_item.update(heading=f"Failed to parse memories response: {str(e)}") + return - if set["memory_memorize_consolidation"]: - - try: - # Use intelligent consolidation system - from python.helpers.memory_consolidation import create_memory_consolidator - consolidator = create_memory_consolidator( - self.agent, - similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery - max_similar_memories=8, - max_llm_context_memories=4 - ) + # Validate that memories is a list or convertible to one + if memories is None: + log_item.update(heading="No valid memories found in response.") + return - # Create memory item-specific log for detailed tracking - memory_log = None # too many utility messages, skip log for now - # memory_log = self.agent.context.log.log( - # type="util", - # heading=f"Processing memory fragment: {txt[:50]}...", - # update_progress="none" # Don't affect status bar - # ) - - # Process with intelligent consolidation - result_obj = await consolidator.process_new_memory( - new_memory=txt, - area=Memory.Area.FRAGMENTS.value, - metadata={"area": Memory.Area.FRAGMENTS.value}, - log_item=memory_log - ) - - # Update the individual log item with completion status but keep it temporary - if result_obj.get("success"): - total_consolidated += 1 - if memory_log: - memory_log.update( - result="Fragment processed successfully", - heading=f"Memory fragment completed: {txt[:50]}...", - update_progress="none" # Show briefly then disappear - ) - else: - if memory_log: - memory_log.update( - result="Fragment processing failed", - heading=f"Memory fragment failed: {txt[:50]}...", - update_progress="none" # Show briefly then disappear - ) - total_processed += 1 - - except Exception as e: - # Log error but continue processing - log_item.update(consolidation_error=str(e)) - total_processed += 1 - - # Update final results with structured logging - log_item.update( - heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated", - memories=memories_txt, - result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated", - memories_processed=total_processed, - memories_consolidated=total_consolidated, - update_progress="none" - ) + # If memories is not a list, try to make it one + if not isinstance(memories, list): + if isinstance(memories, (str, dict)): + memories = [memories] + else: + log_item.update(heading="Invalid memories format received.") + return + if not isinstance(memories, list) or len(memories) == 0: + log_item.update(heading="No useful information to memorize.") + return else: + memories_txt = "\n\n".join([str(memory) for memory in memories]).strip() + log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt) - # remove previous fragments too similiar to this one - if set["memory_memorize_replace_threshold"] > 0: - rem += await db.delete_documents_by_query( - query=txt, - threshold=set["memory_memorize_replace_threshold"], - filter=f"area=='{Memory.Area.FRAGMENTS.value}'", + # Process memories with intelligent consolidation + total_processed = 0 + total_consolidated = 0 + rem = [] + + for memory in memories: + # Convert memory to plain text + txt = f"{memory}" + + if set["memory_memorize_consolidation"]: + + try: + # Use intelligent consolidation system + from python.helpers.memory_consolidation import create_memory_consolidator + consolidator = create_memory_consolidator( + self.agent, + similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery + max_similar_memories=8, + max_llm_context_memories=4 + ) + + # Create memory item-specific log for detailed tracking + memory_log = None # too many utility messages, skip log for now + # memory_log = self.agent.context.log.log( + # type="util", + # heading=f"Processing memory fragment: {txt[:50]}...", + # update_progress="none" # Don't affect status bar + # ) + + # Process with intelligent consolidation + result_obj = await consolidator.process_new_memory( + new_memory=txt, + area=Memory.Area.FRAGMENTS.value, + metadata={"area": Memory.Area.FRAGMENTS.value}, + log_item=memory_log + ) + + # Update the individual log item with completion status but keep it temporary + if result_obj.get("success"): + total_consolidated += 1 + if memory_log: + memory_log.update( + result="Fragment processed successfully", + heading=f"Memory fragment completed: {txt[:50]}...", + update_progress="none" # Show briefly then disappear + ) + else: + if memory_log: + memory_log.update( + result="Fragment processing failed", + heading=f"Memory fragment failed: {txt[:50]}...", + update_progress="none" # Show briefly then disappear + ) + total_processed += 1 + + except Exception as e: + # Log error but continue processing + log_item.update(consolidation_error=str(e)) + total_processed += 1 + + # Update final results with structured logging + log_item.update( + heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated", + memories=memories_txt, + result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated", + memories_processed=total_processed, + memories_consolidated=total_consolidated, + update_progress="none" + ) + + else: + + # remove previous fragments too similiar to this one + if set["memory_memorize_replace_threshold"] > 0: + rem += await db.delete_documents_by_query( + query=txt, + threshold=set["memory_memorize_replace_threshold"], + filter=f"area=='{Memory.Area.FRAGMENTS.value}'", + ) + if rem: + rem_txt = "\n\n".join(Memory.format_docs_plain(rem)) + log_item.update(replaced=rem_txt) + + # insert new memory + await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value}) + + log_item.update( + result=f"{len(memories)} entries memorized.", + heading=f"{len(memories)} entries memorized.", ) if rem: - rem_txt = "\n\n".join(Memory.format_docs_plain(rem)) - log_item.update(replaced=rem_txt) - - # insert new memory - await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value}) - - log_item.update( - result=f"{len(memories)} entries memorized.", - heading=f"{len(memories)} entries memorized.", - ) - if rem: - log_item.stream(result=f"\nReplaced {len(rem)} previous memories.") - + log_item.stream(result=f"\nReplaced {len(rem)} previous memories.") + - # except Exception as e: - # err = errors.format_error(e) - # self.agent.context.log.log( - # type="error", heading="Memorize memories extension error:", content=err - # ) + except Exception as e: + err = errors.format_error(e) + self.agent.context.log.log( + type="warning", heading="Memorize memories extension error", content=err + ) diff --git a/python/extensions/monologue_end/_51_memorize_solutions.py b/python/extensions/monologue_end/_51_memorize_solutions.py index eec574fdb..cc30fc588 100644 --- a/python/extensions/monologue_end/_51_memorize_solutions.py +++ b/python/extensions/monologue_end/_51_memorize_solutions.py @@ -1,5 +1,5 @@ import asyncio -from python.helpers import settings +from python.helpers import settings, errors from python.helpers.extension import Extension from python.helpers.memory import Memory from python.helpers.dirty_json import DirtyJson @@ -31,171 +31,171 @@ class MemorizeSolutions(Extension): return task async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs): - - set = settings.get_settings() - - db = await Memory.get(self.agent) - - # get system message and chat history for util llm - system = self.agent.read_prompt("memory.solutions_sum.sys.md") - msgs_text = self.agent.concat_messages(self.agent.history) - - # log query streamed by LLM - # async def log_callback(content): - # log_item.stream(content=content) - - # call util llm to find solutions in history - solutions_json = await self.agent.call_utility_model( - system=system, - message=msgs_text, - # callback=log_callback, - background=True, - ) - - # log query < no need for streaming utility messages - log_item.update(content=solutions_json) - - - - # Add validation and error handling for solutions_json - if not solutions_json or not isinstance(solutions_json, str): - log_item.update(heading="No response from utility model.") - return - - # Strip any whitespace that might cause issues - solutions_json = solutions_json.strip() - - if not solutions_json: - log_item.update(heading="Empty response from utility model.") - return - try: - solutions = DirtyJson.parse_string(solutions_json) - except Exception as e: - log_item.update(heading=f"Failed to parse solutions response: {str(e)}") - return + set = settings.get_settings() - # Validate that solutions is a list or convertible to one - if solutions is None: - log_item.update(heading="No valid solutions found in response.") - return + db = await Memory.get(self.agent) - # If solutions is not a list, try to make it one - if not isinstance(solutions, list): - if isinstance(solutions, (str, dict)): - solutions = [solutions] - else: - log_item.update(heading="Invalid solutions format received.") - return + # get system message and chat history for util llm + system = self.agent.read_prompt("memory.solutions_sum.sys.md") + msgs_text = self.agent.concat_messages(self.agent.history) - if not isinstance(solutions, list) or len(solutions) == 0: - log_item.update(heading="No successful solutions to memorize.") - return - else: - solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip() - log_item.update( - heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt + # log query streamed by LLM + # async def log_callback(content): + # log_item.stream(content=content) + + # call util llm to find solutions in history + solutions_json = await self.agent.call_utility_model( + system=system, + message=msgs_text, + # callback=log_callback, + background=True, ) - # Process solutions with intelligent consolidation - total_processed = 0 - total_consolidated = 0 - rem = [] + # log query < no need for streaming utility messages + log_item.update(content=solutions_json) - for solution in solutions: - # Convert solution to structured text - if isinstance(solution, dict): - problem = solution.get('problem', 'Unknown problem') - solution_text = solution.get('solution', 'Unknown solution') - txt = f"# Problem\n {problem}\n# Solution\n {solution_text}" + + + # Add validation and error handling for solutions_json + if not solutions_json or not isinstance(solutions_json, str): + log_item.update(heading="No response from utility model.") + return + + # Strip any whitespace that might cause issues + solutions_json = solutions_json.strip() + + if not solutions_json: + log_item.update(heading="Empty response from utility model.") + return + + try: + solutions = DirtyJson.parse_string(solutions_json) + except Exception as e: + log_item.update(heading=f"Failed to parse solutions response: {str(e)}") + return + + # Validate that solutions is a list or convertible to one + if solutions is None: + log_item.update(heading="No valid solutions found in response.") + return + + # If solutions is not a list, try to make it one + if not isinstance(solutions, list): + if isinstance(solutions, (str, dict)): + solutions = [solutions] + else: + log_item.update(heading="Invalid solutions format received.") + return + + if not isinstance(solutions, list) or len(solutions) == 0: + log_item.update(heading="No successful solutions to memorize.") + return else: - # If solution is not a dict, convert it to string - txt = f"# Solution\n {str(solution)}" - - if set["memory_memorize_consolidation"]: - try: - # Use intelligent consolidation system - from python.helpers.memory_consolidation import create_memory_consolidator - consolidator = create_memory_consolidator( - self.agent, - similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery - max_similar_memories=6, # Fewer for solutions (more complex) - max_llm_context_memories=3 - ) - - # Create solution-specific log for detailed tracking - solution_log = None # too many utility messages, skip log for now - # solution_log = self.agent.context.log.log( - # type="util", - # heading=f"Processing solution: {txt[:50]}...", - # update_progress="none" # Don't affect status bar - # ) - - # Process with intelligent consolidation - result_obj = await consolidator.process_new_memory( - new_memory=txt, - area=Memory.Area.SOLUTIONS.value, - metadata={"area": Memory.Area.SOLUTIONS.value}, - log_item=solution_log - ) - - # Update the individual log item with completion status but keep it temporary - if result_obj.get("success"): - total_consolidated += 1 - if solution_log: - solution_log.update( - result="Solution processed successfully", - heading=f"Solution completed: {txt[:50]}...", - update_progress="none" # Show briefly then disappear - ) - else: - if solution_log: - solution_log.update( - result="Solution processing failed", - heading=f"Solution failed: {txt[:50]}...", - update_progress="none" # Show briefly then disappear - ) - total_processed += 1 - - except Exception as e: - # Log error but continue processing - log_item.update(consolidation_error=str(e)) - total_processed += 1 - - # Update final results with structured logging + solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip() log_item.update( - heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated", - solutions=solutions_txt, - result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated", - solutions_processed=total_processed, - solutions_consolidated=total_consolidated, - update_progress="none" + heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt ) - else: - # remove previous solutions too similiar to this one - if set["memory_memorize_replace_threshold"] > 0: - rem += await db.delete_documents_by_query( - query=txt, - threshold=set["memory_memorize_replace_threshold"], - filter=f"area=='{Memory.Area.SOLUTIONS.value}'", + + # Process solutions with intelligent consolidation + total_processed = 0 + total_consolidated = 0 + rem = [] + + for solution in solutions: + # Convert solution to structured text + if isinstance(solution, dict): + problem = solution.get('problem', 'Unknown problem') + solution_text = solution.get('solution', 'Unknown solution') + txt = f"# Problem\n {problem}\n# Solution\n {solution_text}" + else: + # If solution is not a dict, convert it to string + txt = f"# Solution\n {str(solution)}" + + if set["memory_memorize_consolidation"]: + try: + # Use intelligent consolidation system + from python.helpers.memory_consolidation import create_memory_consolidator + consolidator = create_memory_consolidator( + self.agent, + similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery + max_similar_memories=6, # Fewer for solutions (more complex) + max_llm_context_memories=3 + ) + + # Create solution-specific log for detailed tracking + solution_log = None # too many utility messages, skip log for now + # solution_log = self.agent.context.log.log( + # type="util", + # heading=f"Processing solution: {txt[:50]}...", + # update_progress="none" # Don't affect status bar + # ) + + # Process with intelligent consolidation + result_obj = await consolidator.process_new_memory( + new_memory=txt, + area=Memory.Area.SOLUTIONS.value, + metadata={"area": Memory.Area.SOLUTIONS.value}, + log_item=solution_log + ) + + # Update the individual log item with completion status but keep it temporary + if result_obj.get("success"): + total_consolidated += 1 + if solution_log: + solution_log.update( + result="Solution processed successfully", + heading=f"Solution completed: {txt[:50]}...", + update_progress="none" # Show briefly then disappear + ) + else: + if solution_log: + solution_log.update( + result="Solution processing failed", + heading=f"Solution failed: {txt[:50]}...", + update_progress="none" # Show briefly then disappear + ) + total_processed += 1 + + except Exception as e: + # Log error but continue processing + log_item.update(consolidation_error=str(e)) + total_processed += 1 + + # Update final results with structured logging + log_item.update( + heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated", + solutions=solutions_txt, + result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated", + solutions_processed=total_processed, + solutions_consolidated=total_consolidated, + update_progress="none" + ) + else: + # remove previous solutions too similiar to this one + if set["memory_memorize_replace_threshold"] > 0: + rem += await db.delete_documents_by_query( + query=txt, + threshold=set["memory_memorize_replace_threshold"], + filter=f"area=='{Memory.Area.SOLUTIONS.value}'", + ) + if rem: + rem_txt = "\n\n".join(Memory.format_docs_plain(rem)) + log_item.update(replaced=rem_txt) + + # insert new solution + await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value}) + + log_item.update( + result=f"{len(solutions)} solutions memorized.", + heading=f"{len(solutions)} solutions memorized.", ) if rem: - rem_txt = "\n\n".join(Memory.format_docs_plain(rem)) - log_item.update(replaced=rem_txt) - - # insert new solution - await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value}) - - log_item.update( - result=f"{len(solutions)} solutions memorized.", - heading=f"{len(solutions)} solutions memorized.", - ) - if rem: - log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.") + log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.") - # except Exception as e: - # err = errors.format_error(e) - # self.agent.context.log.log( - # type="error", heading="Memorize solutions extension error:", content=err - # ) + except Exception as e: + err = errors.format_error(e) + self.agent.context.log.log( + type="warning", heading="Memorize solutions extension error", content=err + ) diff --git a/python/extensions/monologue_end/_90_waiting_for_input_msg.py b/python/extensions/monologue_end/_90_waiting_for_input_msg.py index 987852949..6e50de53e 100644 --- a/python/extensions/monologue_end/_90_waiting_for_input_msg.py +++ b/python/extensions/monologue_end/_90_waiting_for_input_msg.py @@ -8,3 +8,6 @@ class WaitingForInputMsg(Extension): if self.agent.number == 0: self.agent.context.log.set_initial_progress() + self.agent.context.log.log( + type="hint", heading="Waiting for input...", content="test content", dumy_kvp1=3, dumy_kvp2="test test" + ) diff --git a/python/helpers/errors.py b/python/helpers/errors.py index 21320aa9d..4a2beb35d 100644 --- a/python/helpers/errors.py +++ b/python/helpers/errors.py @@ -1,6 +1,7 @@ import re import traceback import asyncio +from typing import Literal def handle_error(e: Exception): @@ -13,7 +14,7 @@ def error_text(e: Exception): return str(e) -def format_error(e: Exception, start_entries=20, end_entries=15): +def format_error(e: Exception, start_entries=20, end_entries=15, error_message_position:Literal["top", "bottom", "none"] = "top"): # format traceback from the provided exception instead of the most recent one traceback_text = ''.join(traceback.format_exception(type(e), e, e.__traceback__)) # Split the traceback into lines @@ -50,13 +51,22 @@ def format_error(e: Exception, start_entries=20, end_entries=15): error_message = line break + if error_message and error_message_position in ("top", "bottom", "none"): + for i in range(len(trimmed_lines) - 1, -1, -1): + if trimmed_lines[i].strip() == error_message.strip(): + trimmed_lines = trimmed_lines[:i] + trimmed_lines[i + 1 :] + break + # Combine the trimmed traceback with the error message if not trimmed_lines: - result = error_message + result = "" if error_message_position == "none" else error_message else: result = "Traceback (most recent call last):\n" + "\n".join(trimmed_lines) - if error_message: - result += f"\n\n{error_message}" + + if error_message and error_message_position == "top": + result = f"{error_message}\n\n{result}" if result else error_message + elif error_message and error_message_position == "bottom": + result = f"{result}\n\n{error_message}" if result else error_message # at least something if not result: diff --git a/python/helpers/settings.py b/python/helpers/settings.py index 02d0ff58d..f3ac7f837 100644 --- a/python/helpers/settings.py +++ b/python/helpers/settings.py @@ -482,7 +482,7 @@ def get_default_settings() -> Settings: return Settings( version=_get_version(), chat_model_provider=get_default_value("chat_model_provider", "openrouter"), - chat_model_name=get_default_value("chat_model_name", "openai/gpt-4.1"), + chat_model_name=get_default_value("chat_model_name", "openai/gpt-5.2-chat"), chat_model_api_base=get_default_value("chat_model_api_base", ""), chat_model_kwargs=get_default_value("chat_model_kwargs", {"temperature": "0"}), chat_model_ctx_length=get_default_value("chat_model_ctx_length", 100000), @@ -492,7 +492,7 @@ def get_default_settings() -> Settings: chat_model_rl_input=get_default_value("chat_model_rl_input", 0), chat_model_rl_output=get_default_value("chat_model_rl_output", 0), util_model_provider=get_default_value("util_model_provider", "openrouter"), - util_model_name=get_default_value("util_model_name", "openai/gpt-4.1-mini"), + util_model_name=get_default_value("util_model_name", "google/gemini-3-flash-preview"), util_model_api_base=get_default_value("util_model_api_base", ""), util_model_ctx_length=get_default_value("util_model_ctx_length", 100000), util_model_ctx_input=get_default_value("util_model_ctx_input", 0.7), diff --git a/webui/css/messages.css b/webui/css/messages.css index b8e3a3e5c..833936e9e 100644 --- a/webui/css/messages.css +++ b/webui/css/messages.css @@ -286,6 +286,24 @@ margin-bottom: var(--spacing-sm); } +/* Warning messages styled like process groups */ +.message-warning .msg-heading { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: var(--spacing-xs); +} + +.message-warning .msg-heading h4 { + display: flex; + align-items: center; + font-size: var(--font-size-medium); + font-weight: 500; + color: var(--color-warning-text); + opacity: 0.9; + margin-bottom: var(--spacing-sm); +} + /* Terminal styling moved to new terminal block above */ /* Agent and AI Info */ diff --git a/webui/index.css b/webui/index.css index a8b48a905..849f09bce 100644 --- a/webui/index.css +++ b/webui/index.css @@ -24,6 +24,7 @@ --color-input-focus-dark: #101010; --color-chat-background-dark: #212121; --color-error-text-dark:#e72323; + --color-warning-text-dark:#e79c23; --color-table-row-dark: #272727; /* Light mode */ @@ -42,6 +43,7 @@ --color-input-focus-light: #dadada; --color-chat-background-light: #fafafa; --color-error-text-light:#920000; + --color-warning-text-light:#936214; --color-table-row-light: #edededf3; @@ -62,6 +64,7 @@ --color-background-hover: color-mix(in srgb, var(--color-border) 50%, transparent); --color-chat-background: var(--color-chat-background-dark); --color-error-text:var(--color-error-text-dark); + --color-warning-text:var(--color-warning-text-dark); --color-table-row: var(--color-table-row-dark); /* Spacing variables */ @@ -108,6 +111,7 @@ --color-background-hover: color-mix(in srgb, var(--color-border) 50%, transparent); --color-chat-background: var(--color-chat-background-light); --color-error-text:var(--color-error-text-light); + --color-warning-text:var(--color-warning-text-light); --color-table-row: var(--color-table-row-light); } diff --git a/webui/js/messages.js b/webui/js/messages.js index 034bf42a1..a5d523ae8 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -15,9 +15,9 @@ import { Scroller } from "./scroller.js"; // Delay before collapsing previous steps when a new step is added const STEP_COLLAPSE_DELAY = { - "agent": 2000, - "other": 4000, // tools should stay longer as next gen step is placed quickly -} + agent: 2000, + other: 4000, // tools should stay longer as next gen step is placed quickly +}; // delay collapse when hovering const STEP_COLLAPSE_HOVER_DELAY_MS = 5000; @@ -75,10 +75,14 @@ export function setMessages(messages) { const cutoff = isLargeAppend ? Math.max(0, messages.length - 2) : 0; const massRender = historyEmpty || isLargeAppend; - const mainScroller = new Scroller(history, { smooth: !massRender, toleranceRem: 4, reapplyDelayMs: 1000 }); + const mainScroller = new Scroller(history, { + smooth: !massRender, + toleranceRem: 4, + reapplyDelayMs: 1000, + }); + + const results = []; - const results = [] - // process messages for (let i = 0; i < messages.length; i++) { _massRender = historyEmpty || (isLargeAppend && i < cutoff); @@ -90,7 +94,7 @@ export function setMessages(messages) { const shouldScroll = historyEmpty || !results[results.length - 1]?.dontScroll; - if(shouldScroll) mainScroller.reApplyScroll(); + if (shouldScroll) mainScroller.reApplyScroll(); } // entrypoint called from poll/WS communication, this is how all messages are rendered and updated @@ -246,7 +250,11 @@ function drawProcessStep({ const isGroupComplete = isProcessGroupComplete(group); // Set start timestamp on group when first step is created - if (isNewStep && !group.hasAttribute("data-start-timestamp") && log.timestamp) { + if ( + isNewStep && + !group.hasAttribute("data-start-timestamp") && + log.timestamp + ) { group.setAttribute("data-start-timestamp", String(log.timestamp)); } @@ -260,10 +268,13 @@ function drawProcessStep({ step.setAttribute("data-log-type", log.type); step.setAttribute("data-step-id", id); step.setAttribute("data-agent-number", log.agentno); - + // set timestamp attribute (convert to milliseconds for duration calculation) if (log.timestamp) { - step.setAttribute("data-timestamp", String(Math.round(log.timestamp * 1000))); + step.setAttribute( + "data-timestamp", + String(Math.round(log.timestamp * 1000)), + ); } // apply step classes @@ -302,8 +313,14 @@ function drawProcessStep({ stepsContainer .querySelectorAll(".process-step.expanded") .forEach((expandedStep) => { - const delay = STEP_COLLAPSE_DELAY[expandedStep.getAttribute("data-log-type")] || STEP_COLLAPSE_DELAY.other; - console.log("collapsing", expandedStep.getAttribute("data-log-type"), delay); + const delay = + STEP_COLLAPSE_DELAY[expandedStep.getAttribute("data-log-type")] || + STEP_COLLAPSE_DELAY.other; + console.log( + "collapsing", + expandedStep.getAttribute("data-log-type"), + delay, + ); scheduleStepCollapse(expandedStep, delay); }); step.classList.add("expanded"); @@ -340,7 +357,8 @@ function drawProcessStep({ stepDetail, ".process-step-detail-scroll", "div", - "process-step-detail-scroll" ); + "process-step-detail-scroll", + ); // set click handlers setupProcessStepHandlers(step, stepHeader); @@ -367,7 +385,8 @@ function drawProcessStep({ // auto-scroller of the step detail const detailScroller = new Scroller(stepDetailScroll, { - smooth: !isMassRender(), toleranceRem: 4 + smooth: !isMassRender(), + toleranceRem: 4, }); // scroller for step detail content // update KVPs of the step detail @@ -489,7 +508,8 @@ export function _drawMessage({ // Update message classes (preserve collapsible state) const preserve = ["message-collapsible", "expanded", "has-overflow"] - .filter((c) => messageDiv.classList.contains(c)).join(" "); + .filter((c) => messageDiv.classList.contains(c)) + .join(" "); messageDiv.className = `message ${mainClass} ${messageClasses.join(" ")} ${preserve}`; // Handle heading (important for error/rate_limit messages that show context) @@ -718,7 +738,7 @@ export function drawMessageResponse({ }) { // response of subordinate agent - render as process step if (agentno && agentno > 0) { - const title = getStepTitle(heading, kvps, type); + const title = getStepTitle(heading, content, type); const contentText = String(content ?? ""); const actionButtons = contentText.trim() ? [ @@ -773,7 +793,7 @@ export function drawMessageResponse({ markdown: true, latex: true, mainClass: "message-agent-response", - smoothStream: false ,// smooth render disabled, not reliable yet !isMassRender(), // stream smoothly if not in mass render mode + smoothStream: false, // smooth render disabled, not reliable yet !isMassRender(), // stream smoothly if not in mass render mode }); // Collapsible with action buttons @@ -784,7 +804,12 @@ export function drawMessageResponse({ createActionButton("copy", "", () => copyToClipboard(responseText)), ].filter(Boolean) : []; - setupCollapsible(messageDiv, ":scope > .step-action-buttons", !isMassRender(), responseActionButtons); + setupCollapsible( + messageDiv, + ":scope > .step-action-buttons", + !isMassRender(), + responseActionButtons, + ); if (group) updateProcessGroupHeader(group); @@ -798,7 +823,6 @@ export function drawMessageUser({ kvps = null, ...additional }) { - // end last process group on any user message completeLastProcessGroup(); @@ -940,11 +964,10 @@ export function drawMessageTool({ agentno = 0, ...additional }) { - const tool_name = kvps?._tool_name || ""; - if(!tool_name){ - return drawMessageToolSimple({ ...arguments[0] }); + if (!tool_name) { + return drawMessageToolSimple({ ...arguments[0] }); } else if (kvps._tool_name === "skills_tool") { return drawMessageToolSimple({ ...arguments[0], code: "SKL" }); } else if (kvps._tool_name === "vision_load") { @@ -958,7 +981,6 @@ export function drawMessageTool({ } else { return drawMessageToolSimple({ ...arguments[0] }); } - } export function drawMessageToolSimple({ @@ -1249,7 +1271,6 @@ export function drawMessageUtil({ allowCompletedGroup: true, }); - result.dontScroll = !preferencesStore.showUtils; return result; } @@ -1264,7 +1285,7 @@ export function drawMessageHint({ agentno = 0, ...additional }) { - const title = getStepTitle(heading, kvps, type); + const title = getStepTitle(heading, content, type); const contentText = String(content ?? ""); const actionButtons = contentText.trim() ? [ @@ -1316,12 +1337,13 @@ export function drawMessageProgress({ export function drawMessageWarning({ id, + type, heading, content, kvps = null, ...additional }) { - const title = cleanStepTitle(heading || content); + const title = getStepTitle(heading, content, type); let displayKvps = { ...kvps }; const contentText = String(content ?? ""); const actionButtons = contentText.trim() @@ -1331,39 +1353,46 @@ export function drawMessageWarning({ ].filter(Boolean) : []; - //TODO: if process group is running, append there instead - // return drawProcessStep({ - // id, - // title, - // code: "WRN", - // classes: null, - // kvps: displayKvps, - // content, - // // contentClasses: [], - // log: arguments[0], - // }); + //if process group is running, append there + const group = getLastProcessGroup(false); + if (group) { + return drawProcessStep({ + id, + title, + code: "WRN", + // classes: null, + kvps: displayKvps, + content, + // contentClasses: [], + actionButtons, + log: arguments[0], + }); + } + + // if no process group is running, draw as standalone return drawStandaloneMessage({ id, - heading, + title, content, position: "mid", containerClasses: ["ai-container", "center-container"], mainClass: "message-warning", - kvps, + kvps: displayKvps, actionButtons, }); } export function drawMessageError({ id, + type, heading, content, kvps = null, ...additional }) { const contentText = String(content ?? ""); - const errorText = kvps?.text || "Error"; - const errorHeading = errorText ? `Error - ${errorText}` : "Error"; + let title = getStepTitle(heading, content, type); + let displayKvps = { ...kvps }; const actionButtons = [ createActionButton("detail", "", () => stepDetailStore.showStepDetail( @@ -1377,11 +1406,12 @@ export function drawMessageError({ return drawStandaloneMessage({ id, - heading: errorHeading, - content, + heading: title, + content: contentText, position: "mid", containerClasses: ["ai-container", "center-container"], mainClass: "message-error", + kvps: displayKvps, actionButtons, }); } @@ -1542,7 +1572,6 @@ function convertFilePaths(str) { return str.replace(/file:\/\//g, "/download_work_dir_file?path="); } - function escapeHTML(str) { const escapeChars = { "&": "&", @@ -1743,7 +1772,10 @@ function getNestedContainer(parentStep) { * Schedule a step to collapse after a delay * Automatically handles cancellation on click and reset on hover */ -function scheduleStepCollapse(stepElement, delayMs=STEP_COLLAPSE_DELAY.other) { +function scheduleStepCollapse( + stepElement, + delayMs = STEP_COLLAPSE_DELAY.other, +) { // skip if any existing timeout for this step if (stepElement.hasAttribute("data-collapse-timeout-id")) return; // skip already collapsed steps @@ -1830,33 +1862,14 @@ function findParentDelegationStep(group, agentno) { /** * Get a concise title for a process step */ -function getStepTitle(heading, kvps, type) { +function getStepTitle(heading, content, type) { // Try to get a meaningful title from heading or kvps if (heading && heading.trim()) { - return cleanStepTitle(heading, 100); + return cleanStepTitle(heading, 60); } - // For warnings/errors without heading, use content preview as title - if (type === "warning" || type === "error") { - // We'll show full content in detail, so just use type as title - return type === "warning" ? "Warning" : "Error"; - } - - if (kvps) { - // Try common fields for title - if (kvps.tool_name) { - const headline = kvps.headline ? cleanStepTitle(kvps.headline, 60) : ""; - return `${kvps.tool_name}${headline ? ": " + headline : ""}`; - } - if (kvps.headline) { - return cleanStepTitle(kvps.headline, 100); - } - if (kvps.query) { - return truncateText(kvps.query, 100); - } - if (kvps.thoughts) { - return truncateText(String(kvps.thoughts), 100); - } + if (content && content.trim()) { + return cleanStepTitle(content, 60); } // Fallback: capitalize type (backend is source of truth) @@ -1898,14 +1911,10 @@ export function convertIcons(html, classes = "") { */ function cleanStepTitle(text, maxLength = 100) { if (!text) return ""; - let cleaned = String(text); - - // Remove icon:// patterns (e.g., "icon://network_intelligence" or "icon://network_intelligence[Tooltip]") - cleaned = cleaned.replace(/icon:\/\/[a-zA-Z0-9_]+(\[(?:\\.|[^\]])*\])?\s*/g, ""); - - // Trim whitespace - cleaned = cleaned.trim(); - + let cleaned = String(text) + .replace(/icon:\/\/[a-zA-Z0-9_]+(\[(?:\\.|[^\]])*\])?\s*/g, "") + .replace(/\s+/g, " ") + .trim(); return truncateText(cleaned, maxLength); } @@ -1959,12 +1968,16 @@ function updateProcessGroupHeader(group) { // Update step count in metrics - All GEN steps from all agents per process group const stepMetricContainerEl = metricsEl?.querySelector(".metric-steps"); - const stepsMetricValEl = stepMetricContainerEl?.querySelector(".metric-value"); + const stepsMetricValEl = + stepMetricContainerEl?.querySelector(".metric-value"); if (stepsMetricValEl) { - let genSteps = group.querySelectorAll('.process-step[data-log-type="agent"]').length; + let genSteps = group.querySelectorAll( + '.process-step[data-log-type="agent"]', + ).length; genSteps -= 1; // don't count response as step stepsMetricValEl.textContent = genSteps.toString(); - if (genSteps <= 0) stepMetricContainerEl.classList.add("display-none"); // hide when no steps + if (genSteps <= 0) + stepMetricContainerEl.classList.add("display-none"); // hide when no steps else stepMetricContainerEl.classList.remove("display-none"); } @@ -1982,8 +1995,8 @@ function updateProcessGroupHeader(group) { dateStyle: "medium", timeStyle: "short", }); - timeMetricContainerEl.title = timeMetricContainerEl.dataset.bsOriginalTitle = - fullDateTime; + timeMetricContainerEl.title = + timeMetricContainerEl.dataset.bsOriginalTitle = fullDateTime; } } @@ -2003,8 +2016,10 @@ function updateProcessGroupHeader(group) { lastTimestampMs > 0 && formatDuration(Math.max(0, lastTimestampMs - firstTimestampMs)); - const durationMetricContainerEl = metricsEl?.querySelector(".metric-duration"); - const durationMetricValEl = durationMetricContainerEl?.querySelector(".metric-value"); + const durationMetricContainerEl = + metricsEl?.querySelector(".metric-duration"); + const durationMetricValEl = + durationMetricContainerEl?.querySelector(".metric-value"); if (durationMetricContainerEl && durationMetricValEl && durationText) { durationMetricValEl.textContent = durationText; durationMetricContainerEl.classList.remove("display-none"); @@ -2084,11 +2099,21 @@ function ensureChild(parent, selector, tagName, ...classNames) { } // Setup collapsible message with expand button and action buttons -function setupCollapsible(messageDiv, containerSelector, initialExpanded, actionButtons = []) { +function setupCollapsible( + messageDiv, + containerSelector, + initialExpanded, + actionButtons = [], +) { messageDiv.classList.add("message-collapsible"); messageDiv.classList.toggle("expanded", initialExpanded); - const container = ensureChild(messageDiv, containerSelector, "div", "step-action-buttons"); + const container = ensureChild( + messageDiv, + containerSelector, + "div", + "step-action-buttons", + ); container.textContent = ""; const btn = ensureChild(container, ".expand-btn", "button", "expand-btn"); @@ -2102,8 +2127,8 @@ function setupCollapsible(messageDiv, containerSelector, initialExpanded, action btn.onclick = () => { messageDiv.classList.toggle("expanded"); syncBtn(); - messageDiv.classList.contains("expanded") - || (messageDiv.querySelector(".message-body").scrollTop = 0); + messageDiv.classList.contains("expanded") || + (messageDiv.querySelector(".message-body").scrollTop = 0); }; actionButtons.filter(Boolean).forEach((b) => container.appendChild(b)); @@ -2111,11 +2136,16 @@ function setupCollapsible(messageDiv, containerSelector, initialExpanded, action // Detect overflow after render requestAnimationFrame(() => { const body = messageDiv.querySelector(".message-body"); - const fontSize = parseFloat(getComputedStyle(body || document.documentElement).fontSize || "16"); + const fontSize = parseFloat( + getComputedStyle(body || document.documentElement).fontSize || "16", + ); const maxHeight = messageDiv.classList.contains("expanded") ? fontSize * 15 - : (body?.clientHeight || 0); - messageDiv.classList.toggle("has-overflow", (body?.scrollHeight || 0) > maxHeight); + : body?.clientHeight || 0; + messageDiv.classList.toggle( + "has-overflow", + (body?.scrollHeight || 0) > maxHeight, + ); }); } @@ -2129,7 +2159,7 @@ function isMassRender() { function smoothRender(element, newContent, delay = 350) { // skip on mass render if (isMassRender()) { - element.innerHTML = newContent; + element.innerHTML = newContent; return; }