555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
321 lines
10 KiB
Python
321 lines
10 KiB
Python
import hashlib
|
||
import logging
|
||
import re
|
||
from typing import Any, Dict, List
|
||
|
||
from mem0.configs.prompts import (
|
||
AGENT_MEMORY_EXTRACTION_PROMPT,
|
||
FACT_RETRIEVAL_PROMPT,
|
||
USER_MEMORY_EXTRACTION_PROMPT,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def get_fact_retrieval_messages(message, is_agent_memory=False):
|
||
"""Get fact retrieval messages based on the memory type.
|
||
|
||
Args:
|
||
message: The message content to extract facts from
|
||
is_agent_memory: If True, use agent memory extraction prompt, else use user memory extraction prompt
|
||
|
||
Returns:
|
||
tuple: (system_prompt, user_prompt)
|
||
"""
|
||
if is_agent_memory:
|
||
return AGENT_MEMORY_EXTRACTION_PROMPT, f"Input:\n{message}"
|
||
else:
|
||
return USER_MEMORY_EXTRACTION_PROMPT, f"Input:\n{message}"
|
||
|
||
|
||
def get_fact_retrieval_messages_legacy(message):
|
||
"""Legacy function for backward compatibility."""
|
||
return FACT_RETRIEVAL_PROMPT, f"Input:\n{message}"
|
||
|
||
|
||
def ensure_json_instruction(system_prompt, user_prompt):
|
||
"""Ensure the word 'json' appears in the prompts when using json_object response format.
|
||
|
||
OpenAI's API requires the word 'json' to appear in the messages when
|
||
response_format is set to {"type": "json_object"}. When users provide a
|
||
custom_instructions that doesn't include 'json', this causes a
|
||
400 error. This function appends a JSON format instruction to the system
|
||
prompt if 'json' is not already present in either prompt.
|
||
|
||
Args:
|
||
system_prompt: The system prompt string
|
||
user_prompt: The user prompt string
|
||
|
||
Returns:
|
||
tuple: (system_prompt, user_prompt) with JSON instruction added if needed
|
||
"""
|
||
combined = (system_prompt + user_prompt).lower()
|
||
if "json" not in combined:
|
||
system_prompt += (
|
||
"\n\nYou must return your response in valid JSON format "
|
||
"with a 'facts' key containing an array of strings."
|
||
)
|
||
return system_prompt, user_prompt
|
||
|
||
|
||
def parse_messages(messages):
|
||
response = ""
|
||
for msg in messages:
|
||
role = msg.get("role")
|
||
content = msg.get("content")
|
||
# Skip messages without textual content (e.g. assistant tool-call
|
||
# messages that carry `tool_calls` but no `content` key).
|
||
if content is None:
|
||
continue
|
||
if role == "system":
|
||
response += f"system: {content}\n"
|
||
elif role == "user":
|
||
response += f"user: {content}\n"
|
||
elif role == "assistant":
|
||
response += f"assistant: {content}\n"
|
||
return response
|
||
|
||
|
||
def format_entities(entities):
|
||
if not entities:
|
||
return ""
|
||
|
||
formatted_lines = []
|
||
for entity in entities:
|
||
simplified = f"{entity['source']} -- {entity['relationship']} -- {entity['destination']}"
|
||
formatted_lines.append(simplified)
|
||
|
||
return "\n".join(formatted_lines)
|
||
|
||
def normalize_facts(raw_facts):
|
||
"""Normalize LLM-extracted facts to a list of strings.
|
||
|
||
Smaller LLMs (e.g. llama3.1:8b) sometimes return facts as objects
|
||
like {"fact": "..."} or {"text": "..."} instead of plain strings.
|
||
This mirrors the TypeScript FactRetrievalSchema validation.
|
||
"""
|
||
if not raw_facts:
|
||
return []
|
||
normalized = []
|
||
for item in raw_facts:
|
||
if isinstance(item, str):
|
||
fact = item
|
||
elif isinstance(item, dict):
|
||
fact = item.get("fact") or item.get("text")
|
||
if fact is None:
|
||
logger.warning("Unexpected fact shape from LLM, skipping: %s", item)
|
||
continue
|
||
else:
|
||
fact = str(item)
|
||
if fact:
|
||
normalized.append(fact)
|
||
return normalized
|
||
|
||
|
||
def remove_code_blocks(content: str) -> str:
|
||
"""
|
||
Removes enclosing code block markers ```[language] and ``` from a given string.
|
||
|
||
Remarks:
|
||
- The function uses a regex pattern to match code blocks that may start with ``` followed by an optional language tag (letters or numbers) and end with ```.
|
||
- If a code block is detected, it returns only the inner content, stripping out the markers.
|
||
- If no code block markers are found, the original content is returned as-is.
|
||
"""
|
||
pattern = r"^```[a-zA-Z0-9]*\n([\s\S]*?)\n```$"
|
||
match = re.match(pattern, content.strip())
|
||
match_res=match.group(1).strip() if match else content.strip()
|
||
return re.sub(r"<think>.*?</think>", "", match_res, flags=re.DOTALL).strip()
|
||
|
||
|
||
|
||
def extract_json(text):
|
||
"""
|
||
Extracts JSON content from a string, removing enclosing triple backticks and optional 'json' tag if present.
|
||
If no code block is found, attempts to locate JSON by finding the first '{' and last '}'.
|
||
If that also fails, returns the text as-is.
|
||
"""
|
||
text = text.strip()
|
||
match = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL)
|
||
if match:
|
||
json_str = match.group(1)
|
||
else:
|
||
start_idx = text.find("{")
|
||
end_idx = text.rfind("}")
|
||
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
|
||
json_str = text[start_idx : end_idx + 1]
|
||
else:
|
||
json_str = text
|
||
return json_str
|
||
|
||
|
||
def get_image_description(image_obj, llm, vision_details):
|
||
"""
|
||
Get the description of the image
|
||
"""
|
||
|
||
if isinstance(image_obj, str):
|
||
messages = [
|
||
{
|
||
"role": "user",
|
||
"content": [
|
||
{
|
||
"type": "text",
|
||
"text": "A user is providing an image. Provide a high level description of the image and do not include any additional text.",
|
||
},
|
||
{"type": "image_url", "image_url": {"url": image_obj, "detail": vision_details}},
|
||
],
|
||
},
|
||
]
|
||
else:
|
||
messages = [image_obj]
|
||
|
||
response = llm.generate_response(messages=messages)
|
||
return response
|
||
|
||
|
||
def parse_vision_messages(messages, llm=None, vision_details="auto"):
|
||
"""
|
||
Parse the vision messages from the messages
|
||
"""
|
||
returned_messages = []
|
||
for msg in messages:
|
||
role = msg.get("role")
|
||
content = msg.get("content")
|
||
if role == "system":
|
||
returned_messages.append(msg)
|
||
continue
|
||
|
||
# Skip messages without content (e.g. assistant tool-call messages
|
||
# that carry `tool_calls` but no `content` key).
|
||
if content is None:
|
||
continue
|
||
|
||
# Handle message content
|
||
if isinstance(content, list):
|
||
if llm is None:
|
||
text_parts = [
|
||
part["text"] for part in msg["content"]
|
||
if isinstance(part, dict) and part.get("type") == "text"
|
||
]
|
||
if not text_parts:
|
||
continue
|
||
returned_messages.append({"role": role, "content": " ".join(text_parts)})
|
||
else:
|
||
description = get_image_description(msg, llm, vision_details)
|
||
returned_messages.append({"role": role, "content": description})
|
||
elif isinstance(content, dict) and content.get("type") == "image_url":
|
||
if llm is None:
|
||
continue
|
||
image_url_obj = content.get("image_url")
|
||
image_url = image_url_obj.get("url") if isinstance(image_url_obj, dict) else None
|
||
if not image_url:
|
||
raise ValueError("image_url content part is missing image_url.url")
|
||
try:
|
||
description = get_image_description(image_url, llm, vision_details)
|
||
returned_messages.append({"role": role, "content": description})
|
||
except Exception:
|
||
raise Exception(f"Error while downloading {image_url}.")
|
||
else:
|
||
# Regular text content
|
||
returned_messages.append(msg)
|
||
|
||
return returned_messages
|
||
|
||
|
||
def process_telemetry_filters(filters):
|
||
"""
|
||
Process the telemetry filters
|
||
"""
|
||
if filters is None:
|
||
return {}
|
||
|
||
encoded_ids = {}
|
||
if "user_id" in filters:
|
||
encoded_ids["user_id"] = hashlib.md5(filters["user_id"].encode()).hexdigest()
|
||
if "agent_id" in filters:
|
||
encoded_ids["agent_id"] = hashlib.md5(filters["agent_id"].encode()).hexdigest()
|
||
if "run_id" in filters:
|
||
encoded_ids["run_id"] = hashlib.md5(filters["run_id"].encode()).hexdigest()
|
||
|
||
return list(filters.keys()), encoded_ids
|
||
|
||
|
||
def sanitize_relationship_for_cypher(relationship) -> str:
|
||
"""Sanitize relationship text for Cypher queries by replacing problematic characters."""
|
||
char_map = {
|
||
"...": "_ellipsis_",
|
||
"…": "_ellipsis_",
|
||
"。": "_period_",
|
||
",": "_comma_",
|
||
";": "_semicolon_",
|
||
":": "_colon_",
|
||
"!": "_exclamation_",
|
||
"?": "_question_",
|
||
"(": "_lparen_",
|
||
")": "_rparen_",
|
||
"【": "_lbracket_",
|
||
"】": "_rbracket_",
|
||
"《": "_langle_",
|
||
"》": "_rangle_",
|
||
"'": "_apostrophe_",
|
||
'"': "_quote_",
|
||
"\\": "_backslash_",
|
||
"/": "_slash_",
|
||
"|": "_pipe_",
|
||
"&": "_ampersand_",
|
||
"=": "_equals_",
|
||
"+": "_plus_",
|
||
"*": "_asterisk_",
|
||
"^": "_caret_",
|
||
"%": "_percent_",
|
||
"$": "_dollar_",
|
||
"#": "_hash_",
|
||
"@": "_at_",
|
||
"!": "_bang_",
|
||
"?": "_question_",
|
||
"(": "_lparen_",
|
||
")": "_rparen_",
|
||
"[": "_lbracket_",
|
||
"]": "_rbracket_",
|
||
"{": "_lbrace_",
|
||
"}": "_rbrace_",
|
||
"<": "_langle_",
|
||
">": "_rangle_",
|
||
"-": "_",
|
||
}
|
||
|
||
# Apply replacements and clean up
|
||
sanitized = relationship
|
||
for old, new in char_map.items():
|
||
sanitized = sanitized.replace(old, new)
|
||
|
||
return re.sub(r"_+", "_", sanitized).strip("_")
|
||
|
||
|
||
def remove_spaces_from_entities(
|
||
entity_list: List[Any],
|
||
*,
|
||
sanitize_relationship: bool = True,
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Normalize entity relation dicts from LLM/tool output: lowercase, spaces to underscores.
|
||
|
||
Skips entries that are not non-empty dicts or that lack any of
|
||
``source``, ``relationship``, or ``destination`` (avoids KeyError on ``[{}]``
|
||
or partial dicts).
|
||
"""
|
||
required = ("source", "relationship", "destination")
|
||
cleaned: List[Dict[str, Any]] = []
|
||
for item in entity_list:
|
||
if not isinstance(item, dict) or not item:
|
||
continue
|
||
if not all(key in item for key in required):
|
||
continue
|
||
item["source"] = item["source"].lower().replace(" ", "_")
|
||
rel = item["relationship"].lower().replace(" ", "_")
|
||
item["relationship"] = sanitize_relationship_for_cypher(rel) if sanitize_relationship else rel
|
||
item["destination"] = item["destination"].lower().replace(" ", "_")
|
||
cleaned.append(item)
|
||
return cleaned
|
||
|