diff --git a/src/memos/api/handlers/formatters_handler.py b/src/memos/api/handlers/formatters_handler.py index 94988295b..f9e2022a9 100644 --- a/src/memos/api/handlers/formatters_handler.py +++ b/src/memos/api/handlers/formatters_handler.py @@ -84,6 +84,7 @@ def post_process_pref_mem( { "cube_id": mem_cube_id, "memories": pref_formatted_mem, + "total_nodes": len(pref_formatted_mem), } ) pref_instruction, pref_note = instruct_completion(pref_formatted_mem) @@ -116,12 +117,14 @@ def post_process_textual_mem( { "cube_id": mem_cube_id, "memories": fact_mem, + "total_nodes": len(fact_mem), } ) memories_result["tool_mem"].append( { "cube_id": mem_cube_id, "memories": tool_mem, + "total_nodes": len(tool_mem), } ) return memories_result diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index f6f1402fc..d81d1aba2 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -6,7 +6,11 @@ from typing import TYPE_CHECKING, Any, Literal -from memos.api.handlers.formatters_handler import format_memory_item +from memos.api.handlers.formatters_handler import ( + format_memory_item, + post_process_pref_mem, + post_process_textual_mem, +) from memos.api.product_models import ( DeleteMemoryRequest, DeleteMemoryResponse, @@ -209,54 +213,67 @@ def handle_get_memory(memory_id: str, naive_mem_cube: NaiveMemCube) -> GetMemory def handle_get_memories( get_mem_req: GetMemoryRequest, naive_mem_cube: NaiveMemCube ) -> GetMemoryResponse: - # TODO: Implement get memory with filter + results: dict[str, Any] = {"text_mem": [], "pref_mem": [], "tool_mem": []} memories = naive_mem_cube.text_mem.get_all( user_name=get_mem_req.mem_cube_id, user_id=get_mem_req.user_id, page=get_mem_req.page, page_size=get_mem_req.page_size, - ) - total_nodes = memories["total_nodes"] - total_edges = memories["total_edges"] - del memories["total_nodes"] - del memories["total_edges"] + filter=get_mem_req.filter, + )["nodes"] + + results = post_process_textual_mem(results, memories, get_mem_req.mem_cube_id) + + if not get_mem_req.include_tool_memory: + results["tool_mem"] = [] preferences: list[TextualMemoryItem] = [] - total_pref = 0 + format_preferences = [] if get_mem_req.include_preference and naive_mem_cube.pref_mem is not None: filter_params: dict[str, Any] = {} if get_mem_req.user_id is not None: filter_params["user_id"] = get_mem_req.user_id if get_mem_req.mem_cube_id is not None: filter_params["mem_cube_id"] = get_mem_req.mem_cube_id - - preferences, total_pref = naive_mem_cube.pref_mem.get_memory_by_filter( + if get_mem_req.filter is not None: + # Check and remove user_id/mem_cube_id from filter if present + filter_copy = get_mem_req.filter.copy() + removed_fields = [] + + if "user_id" in filter_copy: + filter_copy.pop("user_id") + removed_fields.append("user_id") + if "mem_cube_id" in filter_copy: + filter_copy.pop("mem_cube_id") + removed_fields.append("mem_cube_id") + + if removed_fields: + logger.warning( + f"Fields {removed_fields} found in filter will be ignored. " + f"Use request-level user_id/mem_cube_id parameters instead." + ) + + filter_params.update(filter_copy) + + preferences, _ = naive_mem_cube.pref_mem.get_memory_by_filter( filter_params, page=get_mem_req.page, page_size=get_mem_req.page_size ) format_preferences = [format_memory_item(item) for item in preferences] - return GetMemoryResponse( - message="Memories retrieved successfully", - data={ - "text_mem": [ - { - "cube_id": get_mem_req.mem_cube_id, - "memories": memories, - "total_nodes": total_nodes, - "total_edges": total_edges, - } - ], - "pref_mem": [ - { - "cube_id": get_mem_req.mem_cube_id, - "memories": format_preferences, - "total_nodes": total_pref, - } - ], - }, + results = post_process_pref_mem( + results, format_preferences, get_mem_req.mem_cube_id, get_mem_req.include_preference ) + # Filter to only keep text_mem, pref_mem, tool_mem + filtered_results = { + "text_mem": results.get("text_mem", []), + "pref_mem": results.get("pref_mem", []), + "tool_mem": results.get("tool_mem", []), + } + + return GetMemoryResponse(message="Memories retrieved successfully", data=filtered_results) + def handle_delete_memories(delete_mem_req: DeleteMemoryRequest, naive_mem_cube: NaiveMemCube): logger.info( diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index d5f301c9d..b2f8a9fa3 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -771,7 +771,9 @@ class GetMemoryRequest(BaseRequest): mem_cube_id: str = Field(..., description="Cube ID") user_id: str | None = Field(None, description="User ID") - include_preference: bool = Field(True, description="Whether to handle preference memory") + include_preference: bool = Field(True, description="Whether to return preference memory") + include_tool_memory: bool = Field(False, description="Whether to return tool memory") + filter: dict[str, Any] | None = Field(None, description="Filter for the memory") page: int | None = Field( None, description="Page number (starts from 1). If None, exports all data without pagination.", diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 2ed1af53e..3bf6d4927 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -1,5 +1,6 @@ import concurrent.futures import json +import re import traceback from typing import Any @@ -547,7 +548,11 @@ def _process_tool_trajectory_fine( for fast_item in fast_memory_items: # Extract memory text (string content) mem_str = fast_item.memory or "" - if not mem_str.strip() or "tool:" not in mem_str: + if not mem_str.strip() or ( + "tool:" not in mem_str + and "[tool_calls]:" not in mem_str + and not re.search(r".*?", mem_str, re.DOTALL) + ): continue try: resp = self._get_llm_tool_trajectory_response(mem_str) @@ -563,6 +568,8 @@ def _process_tool_trajectory_fine( value=m.get("trajectory", ""), info=info, memory_type=memory_type, + correctness=m.get("correctness", ""), + experience=m.get("experience", ""), tool_used_status=m.get("tool_used_status", []), ) fine_memory_items.append(node) @@ -606,16 +613,22 @@ def _process_multi_modal_data( if mode == "fast": return fast_memory_items else: - # Part A: call llm + # Part A: call llm in parallel using thread pool fine_memory_items = [] - fine_memory_items_string_parser = self._process_string_fine( - fast_memory_items, info, custom_tags - ) - fine_memory_items.extend(fine_memory_items_string_parser) - fine_memory_items_tool_trajectory_parser = self._process_tool_trajectory_fine( - fast_memory_items, info - ) + with ContextThreadPoolExecutor(max_workers=2) as executor: + future_string = executor.submit( + self._process_string_fine, fast_memory_items, info, custom_tags + ) + future_tool = executor.submit( + self._process_tool_trajectory_fine, fast_memory_items, info + ) + + # Collect results + fine_memory_items_string_parser = future_string.result() + fine_memory_items_tool_trajectory_parser = future_tool.result() + + fine_memory_items.extend(fine_memory_items_string_parser) fine_memory_items.extend(fine_memory_items_tool_trajectory_parser) # Part B: get fine multimodal items @@ -658,13 +671,18 @@ def _process_transfer_multi_modal_data( } fine_memory_items = [] - # Part A: call llm - fine_memory_items_string_parser = self._process_string_fine([raw_node], info, custom_tags) - fine_memory_items.extend(fine_memory_items_string_parser) + # Part A: call llm in parallel using thread pool + with ContextThreadPoolExecutor(max_workers=2) as executor: + future_string = executor.submit( + self._process_string_fine, [raw_node], info, custom_tags + ) + future_tool = executor.submit(self._process_tool_trajectory_fine, [raw_node], info) - fine_memory_items_tool_trajectory_parser = self._process_tool_trajectory_fine( - [raw_node], info - ) + # Collect results + fine_memory_items_string_parser = future_string.result() + fine_memory_items_tool_trajectory_parser = future_tool.result() + + fine_memory_items.extend(fine_memory_items_string_parser) fine_memory_items.extend(fine_memory_items_tool_trajectory_parser) # Part B: get fine multimodal items diff --git a/src/memos/mem_reader/read_multi_modal/system_parser.py b/src/memos/mem_reader/read_multi_modal/system_parser.py index deb2a9832..03a49afd8 100644 --- a/src/memos/mem_reader/read_multi_modal/system_parser.py +++ b/src/memos/mem_reader/read_multi_modal/system_parser.py @@ -1,6 +1,7 @@ """Parser for system messages.""" import ast +import hashlib import json import re import uuid @@ -42,9 +43,10 @@ def create_source( info: dict[str, Any], ) -> SourceMessage: """Create SourceMessage from system message.""" - content = message["content"] + + content = message.get("content", "") if isinstance(content, dict): - content = content["text"] + content = content.get("text", "") content_wo_tool_schema = re.sub( r"(.*?)", @@ -84,17 +86,154 @@ def parse_fast( info: dict[str, Any], **kwargs, ) -> list[TextualMemoryItem]: - content = message["content"] + content = message.get("content", "") if isinstance(content, dict): - content = content["text"] + content = content.get("text", "") - # Replace tool_schema content with "omitted" in remaining content - content_wo_tool_schema = re.sub( - r"(.*?)", - r"omitted", - content, - flags=re.DOTALL, - ) + # Find first tool_schema block + tool_schema_pattern = r"(.*?)" + match = re.search(tool_schema_pattern, content, flags=re.DOTALL) + + if match: + original_text = match.group(0) # Complete ... block + schema_content = match.group(1) # Content between the tags + + # Parse tool schema + try: + tool_schema = json.loads(schema_content) + assert isinstance(tool_schema, list), "Tool schema must be a list[dict]" + except json.JSONDecodeError: + try: + tool_schema = ast.literal_eval(schema_content) + assert isinstance(tool_schema, list), "Tool schema must be a list[dict]" + except (ValueError, SyntaxError, AssertionError): + logger.warning( + f"[SystemParser] Failed to parse tool schema with both JSON and ast.literal_eval: {schema_content[:100]}..." + ) + tool_schema = None + except AssertionError: + logger.warning( + f"[SystemParser] Tool schema must be a list[dict]: {schema_content[:100]}..." + ) + tool_schema = None + + # Process and replace + if tool_schema is not None: + + def remove_descriptions(obj): + """Recursively remove all 'description' keys from a nested dict/list structure.""" + if isinstance(obj, dict): + return { + k: remove_descriptions(v) for k, v in obj.items() if k != "description" + } + elif isinstance(obj, list): + return [remove_descriptions(item) for item in obj] + else: + return obj + + def keep_first_layer_params(obj): + """Only keep first layer parameter information, remove nested parameters.""" + if isinstance(obj, list): + return [keep_first_layer_params(item) for item in obj] + elif isinstance(obj, dict): + result = {} + for k, v in obj.items(): + if k == "properties" and isinstance(v, dict): + # For properties, only keep first layer parameter names and types + first_layer_props = {} + for param_name, param_info in v.items(): + if isinstance(param_info, dict): + # Only keep type and basic info, remove nested properties + first_layer_props[param_name] = { + key: val + for key, val in param_info.items() + if key in ["type", "enum", "required"] + and key != "properties" + } + else: + first_layer_props[param_name] = param_info + result[k] = first_layer_props + elif k == "parameters" and isinstance(v, dict): + # Process parameters object but only keep first layer + result[k] = keep_first_layer_params(v) + elif isinstance(v, dict | list) and k != "properties": + result[k] = keep_first_layer_params(v) + else: + result[k] = v + return result + else: + return obj + + def format_tool_schema_readable(tool_schema): + """Convert tool schema to readable format: tool_name: [param1 (type1), ...](required: ...)""" + lines = [] + for tool in tool_schema: + if not tool: + continue + + # Handle both new format and old-style OpenAI function format + if tool.get("type") == "function" and "function" in tool: + tool_info = tool.get("function") + if not tool_info: + continue + else: + tool_info = tool + + tool_name = tool_info.get("name", "unknown") + params_obj = tool_info.get("parameters", {}) + properties = params_obj.get("properties", {}) + required = params_obj.get("required", []) + + # Format parameters + param_strs = [] + for param_name, param_info in properties.items(): + if isinstance(param_info, dict): + param_type = param_info.get("type", "any") + # Handle enum + if "enum" in param_info and param_info["enum"] is not None: + # Ensure all enum values are strings + enum_values = [str(v) for v in param_info["enum"]] + param_type = f"{param_type}[{', '.join(enum_values)}]" + param_strs.append(f"{param_name} ({param_type})") + else: + param_strs.append(f"{param_name} (any)") + + # Format required parameters + # Ensure all required parameter names are strings + required_strs = [str(r) for r in required] if required else [] + required_str = ( + f"(required: {', '.join(required_strs)})" if required_strs else "" + ) + + # Construct the line + params_part = f"[{', '.join(param_strs)}]" if param_strs else "[]" + line = f"{tool_name}: {params_part}{required_str}" + lines.append(line) + + return "\n".join(lines) + + # Compression mode literal: ["compress", "omit"]. compress is core-information-preserving, omit is full omission. + compression_mode = "compress" + if compression_mode == "omit": + processed_text = "omitted" + elif compression_mode == "compress": + # First keep only first layer params, then remove descriptions + simple_tool_schema = keep_first_layer_params(tool_schema) + simple_tool_schema = remove_descriptions(simple_tool_schema) + # change to readable format + readable_schema = format_tool_schema_readable(simple_tool_schema) + + processed_text = f"{readable_schema}" + else: + raise ValueError(f"Unknown compression mode: {compression_mode}") + + content = content.replace(original_text, processed_text, 1) + + parts = ["system: "] + if message.get("chat_time"): + parts.append(f"[{message.get('chat_time')}]: ") + prefix = "".join(parts) + msg_line = f"{prefix}{content}\n" source = self.create_source(message, info) @@ -104,7 +243,7 @@ def parse_fast( session_id = info_.pop("session_id", "") # Split parsed text into chunks - content_chunks = self._split_text(content_wo_tool_schema) + content_chunks = self._split_text(msg_line) memory_items = [] for _chunk_idx, chunk_text in enumerate(content_chunks): @@ -132,9 +271,9 @@ def parse_fine( info: dict[str, Any], **kwargs, ) -> list[TextualMemoryItem]: - content = message["content"] + content = message.get("content", "") if isinstance(content, dict): - content = content["text"] + content = content.get("text", "") try: tool_schema = json.loads(content) assert isinstance(tool_schema, list), "Tool schema must be a list[dict]" @@ -155,6 +294,22 @@ def parse_fine( user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Deduplicate tool schemas based on memory content + # Use hash as key for efficiency, but store original string to handle collisions + seen_memories = {} # hash -> memory_str mapping + unique_schemas = [] + for schema in tool_schema: + memory_str = json.dumps(schema, ensure_ascii=False, sort_keys=True) + # Use SHA-256 for better collision resistance + memory_hash = hashlib.sha256(memory_str.encode("utf-8")).hexdigest() + + # Check if hash exists and verify the actual content (handle potential collision) + if memory_hash not in seen_memories: + seen_memories[memory_hash] = memory_str + unique_schemas.append(schema) + elif seen_memories[memory_hash] != memory_str: + unique_schemas.append(schema) + return [ TextualMemoryItem( id=str(uuid.uuid4()), @@ -168,5 +323,5 @@ def parse_fine( info=info_, ), ) - for schema in tool_schema + for schema in unique_schemas ] diff --git a/src/memos/memories/textual/tree.py b/src/memos/memories/textual/tree.py index c486e6cf6..b963cfa9b 100644 --- a/src/memos/memories/textual/tree.py +++ b/src/memos/memories/textual/tree.py @@ -327,13 +327,14 @@ def get_all( user_id: str | None = None, page: int | None = None, page_size: int | None = None, + filter: dict | None = None, ) -> dict: """Get all memories. Returns: list[TextualMemoryItem]: List of all memories. """ graph_output = self.graph_store.export_graph( - user_name=user_name, user_id=user_id, page=page, page_size=page_size + user_name=user_name, user_id=user_id, page=page, page_size=page_size, filter=filter ) return graph_output diff --git a/src/memos/templates/tool_mem_prompts.py b/src/memos/templates/tool_mem_prompts.py index 7d5363956..2fe8840b7 100644 --- a/src/memos/templates/tool_mem_prompts.py +++ b/src/memos/templates/tool_mem_prompts.py @@ -1,26 +1,47 @@ TOOL_TRAJECTORY_PROMPT_ZH = """ -你是一个专业的工具调用轨迹提取专家。你的任务是从给定的对话消息中提取完整的工具调用轨迹经验。 +你是一个专业的工具经验提取专家。你的任务是从给定的对话消息中提取完整的工具调用轨迹经验。 -## 提取规则: -1. 只有当对话中存在有价值的工具调用过程时才进行提取 -2. 有价值的轨迹至少包含以下元素: - - 用户的问题(user message) - - 助手的工具调用尝试(assistant message with tool_calls) - - 工具的执行结果(tool message with tool_call_id and content,无论成功或失败) - - 助手的响应(assistant message,无论是否给出最终答案) +## 分析判断步骤: +**步骤1:判断任务完成度** +根据用户反馈,判定correctness:success(成功)或 failed(失败),用户反馈决定权大于执行结果,用户反馈有误,则判定为failed + +**步骤2:成功轨迹(success)- 经验提炼** +从成功模式中提炼通用原则或规则,采用"when...then..."结构: +- when: 明确描述触发该经验的场景特征(任务类型、工具环境、参数特征等) +- then: 总结有效的参数模式、调用策略、最佳实践 +注意:经验是解决整个轨迹问题级别的,不仅仅针对单个工具 + +**步骤3:失败轨迹(failed)- 错误分析与经验提炼** +3.1 工具需求判断 + - 任务是否需要工具?(需要/直接回答/误调用) +3.2 工具调用检查 + - 工具存在性:是否在system中提供 + - 工具选择:是否选对工具 + - 参数正确性:是否符合类型定义 + - 幻觉检测:是否调用不存在的工具 +3.3 错误根因定位 + 结合消息中的错误反馈信息和上述分析,精准输出根本原因 +3.4 经验提炼(核心) + 从失败模式中提炼通用原则或规则,采用"when...then..."结构: + - when: 明确描述触发该经验的场景特征(任务类型、工具环境、参数特征等) + - then: 给出避免错误的通用策略、正确调用方式或决策规则 + 注意:经验是解决整个轨迹问题级别的,不仅仅针对单个工具 ## 输出格式: 返回一个JSON数组,格式如下: + ```json [ { - "trajectory": "自然语言输出包含'任务、使用的工具、工具观察、最终回答'的完整精炼的总结,体现顺序", + "correctness": "success 或 failed", + "trajectory": "精炼完整的自然语言总结,包含:[任务(用户任务) -> 执行动作(调用的工具/直接回答) -> 执行结果] (可能多轮) -> 最终回答", + "experience": "采用when...then...格式,例如:'when 遇到XX的任务时,应该YY'", "tool_used_status": [ { - "used_tool": "工具名1", + "used_tool": "工具名称(如果调用了工具)", "success_rate": "0.0-1.0之间的数值,表示该工具在本次轨迹中的成功率", "error_type": "调用失败时的错误类型和描述,成功时为空字符串", - "experience": "该工具的使用经验,比如常见的参数模式、执行特点、结果解读方式等" + "tool_experience": "调用该工具的经验,包括可能的前置条件和可能的后置效果" } ] } @@ -28,42 +49,72 @@ ``` ## 注意事项: -- 如果对话中没有完整的工具调用轨迹,返回空数组 - 每个轨迹必须是独立的完整过程 - 一个轨迹中可能涉及多个工具的使用,每个工具在tool_used_status中独立记录 +- 如果没有调用工具,tool_used_status为空数组[] +- 如果多条轨迹存在顺序依赖关系,需要将它们视为一条轨迹 - 只提取事实内容,不要添加任何解释或额外信息 - 确保返回的是有效的JSON格式 +- 输出的trajectory需要按照messages的发展顺序排列 +- experience必须是通用的、可复用的经验规则,而不是针对具体案例的描述 +- 无论成功或失败,都要提炼经验并使用when...then...格式 -请分析以下对话消息并提取工具调用轨迹: - +请分析以下对话消息并提取工具调用轨迹,基于以下对话消息: + {messages} - + """ TOOL_TRAJECTORY_PROMPT_EN = """ -You are a professional tool call trajectory extraction expert. Your task is to extract valuable tool call trajectory experiences from given conversation messages. +You are a professional tool experience extraction expert. Your task is to extract complete tool call trajectory experiences from given conversation messages. + +## Analysis and Judgment Steps: + +**Step 1: Assess Task Completion** +Determine correctness based on user feedback: success or failed, user feedback has higher priority than execution results, if user feedback is incorrect, then determine as failed + +**Step 2: Successful Trajectory (success) - Experience Extraction** +Extract general principles or rules from success patterns, using "when...then..." structure: +- when: clearly describe the scenario characteristics that trigger this experience (task type, tool environment, parameter characteristics, etc.) +- then: summarize effective parameter patterns, calling strategies, and best practices +Note: Experience is at the trajectory-level problem-solving, not just for a single tool -## Extraction Rules: -1. Only extract when there are valuable tool calling processes in the conversation -2. Valuable trajectories must contain at least the following elements: - - User's question (user message) - - Assistant's tool call attempt (assistant message with tool_calls) - - Tool execution results (tool message with tool_call_id and content, regardless of success or failure) - - Assistant's response (assistant message, whether or not a final answer is given) +**Step 3: Failed Trajectory (failed) - Error Analysis and Experience Extraction** + +3.1 Tool Requirement Assessment + - Does the task require tools? (required/direct answer/unnecessary call) + +3.2 Tool Call Verification + - Tool availability: provided in system? + - Tool selection: correct tool chosen? + - Parameter correctness: conform to type definitions? + - Hallucination detection: calling non-existent tools? + +3.3 Root Cause Identification + Combine error feedback from messages with above analysis to precisely output root cause + +3.4 Experience Extraction (Core) + Extract general principles or rules from failure patterns, using "when...then..." structure: + - when: clearly describe the scenario characteristics that trigger this experience (task type, tool environment, parameter characteristics, etc.) + - then: provide general strategies to avoid errors, correct calling approaches, or decision rules + Note: Experience is at the trajectory-level problem-solving, not just for a single tool ## Output Format: Return a JSON array in the following format: + ```json [ { - "trajectory": "Natural language summary containing 'task, tools used, tool observations, final answer' in a complete and refined manner, reflecting the sequence", + "correctness": "success or failed", + "trajectory": "Concise and complete natural language summary including: [task (user task) -> execution action (tool called/direct answer) -> execution result] (possibly multiple rounds) -> final answer", + "experience": "Use when...then... format, e.g., 'when encountering XX tasks, should do YY'", "tool_used_status": [ { - "used_tool": "Tool Name 1", - "success_rate": "Numerical value between 0.0-1.0, indicating the success rate of this tool in the current trajectory", + "used_tool": "Tool name (if tool was called)", + "success_rate": "Numerical value between 0.0-1.0, indicating the success rate of this tool in current trajectory", "error_type": "Error type and description when call fails, empty string when successful", - "experience": "Usage experience of this tool, such as common parameter patterns, execution characteristics, result interpretation methods, etc." + "tool_experience": "Experience of using this tool, including possible preconditions and possible post-effects" } ] } @@ -71,14 +122,18 @@ ``` ## Notes: -- If there are no complete tool call trajectories in the conversation, return an empty array - Each trajectory must be an independent complete process -- Multiple tools may be used in one trajectory, each tool is recorded independently in tool_used_status -- Only extract factual content, do not add any additional explanations or information +- A trajectory may involve multiple tools, each recorded independently in tool_used_status +- If no tool was called, tool_used_status is an empty array [] +- If multiple trajectories have sequential dependencies, treat them as one trajectory +- Only extract factual content, do not add any explanations or extra information - Ensure the returned content is valid JSON format +- The trajectory should be arranged according to the development order of messages +- Experience must be general and reusable rules, not descriptions specific to concrete cases +- Whether success or failed, always extract experience using when...then... format -Please analyze the following conversation messages and extract tool call trajectories: - +Please analyze the following conversation messages and extract tool call trajectories based on: + {messages} - + """