refactor(core): migrate core modules and update imports

This commit is contained in:
jinli.yl 2026-01-21 17:10:05 +08:00
parent 32ff65ca0d
commit 4560ff09ad
190 changed files with 906 additions and 906 deletions

View file

@ -10,8 +10,8 @@ from datetime import datetime, timezone
from tqdm import tqdm
from reme_ai.core_old.enumeration import Role
from reme_ai.core_old.schema import Message, MemoryNode
from reme_ai.core.enumeration import Role
from reme_ai.core.schema import Message, MemoryNode
from reme_ai.reme import ReMe
TEMPLATE_REME = """Memories for user {user_id}:

View file

@ -35,8 +35,8 @@ from eval_tools import (
evaluation_for_update_memory,
)
from llms import llm_request
from reme_ai.core_old.enumeration import MemoryType
from reme_ai.core_old.schema import MemoryNode
from reme_ai.core.enumeration import MemoryType
from reme_ai.core.schema import MemoryNode
from reme_ai.reme import ReMe
# Template for formatting memories (from shared YAML config)

View file

@ -26,8 +26,8 @@ from typing import Any
from loguru import logger
from eval_tools import evaluation_for_question2
from reme_ai.core_old.enumeration import MemoryType
from reme_ai.core_old.schema import MemoryNode
from reme_ai.core.enumeration import MemoryType
from reme_ai.core.schema import MemoryNode
from reme_ai.reme import ReMe

View file

@ -27,8 +27,8 @@ from typing import Any
from loguru import logger
from eval_tools import evaluation_for_question2
from reme_ai.core_old.enumeration import MemoryType
from reme_ai.core_old.schema import MemoryNode
from reme_ai.core.enumeration import MemoryType
from reme_ai.core.schema import MemoryNode
from reme_ai.reme import ReMe

View file

@ -27,8 +27,8 @@ from typing import Any
from loguru import logger
from eval_tools import evaluation_for_question2, answer_question_with_memories
from reme_ai.core_old.enumeration import MemoryType
from reme_ai.core_old.schema import MemoryNode
from reme_ai.core.enumeration import MemoryType
from reme_ai.core.schema import MemoryNode
from reme_ai.reme import ReMe

View file

@ -5,8 +5,8 @@ import re
from tenacity import retry, stop_after_attempt, wait_random_exponential, before_sleep_log
from reme_ai.core_old.schema import Message
from reme_ai.core_old.utils import load_env
from reme_ai.core.schema import Message
from reme_ai.core.utils import load_env
from reme_ai.reme import ReMe
logger = logging.getLogger(__name__)

View file

@ -16,8 +16,8 @@ from collections import defaultdict
from pathlib import Path
from typing import Any
from reme_ai.core_old.schema import Message
from reme_ai.core_old.utils import load_env
from reme_ai.core.schema import Message
from reme_ai.core.utils import load_env
from reme_ai.reme import ReMe
from tenacity import retry, stop_after_attempt, wait_random_exponential

View file

@ -16,8 +16,8 @@ from collections import defaultdict
from pathlib import Path
from typing import Any
from reme_ai.core_old.schema import Message
from reme_ai.core_old.utils import load_env
from reme_ai.core.schema import Message
from reme_ai.core.utils import load_env
from reme_ai.reme import ReMe
from tenacity import retry, stop_after_attempt, wait_random_exponential

0
reme/__init__.py Normal file
View file

0
reme/core/__init__.py Normal file
View file

View file

@ -0,0 +1,11 @@
"""context"""
from .base_context import BaseContext
from .prompt_handler import PromptHandler
from .registry_factory import R
__all__ = [
"BaseContext",
"PromptHandler",
"R",
]

View file

@ -0,0 +1,363 @@
"""Module for managing and formatting prompt templates from files or dictionaries.
This module provides a PromptHandler class that:
- Loads prompts from YAML/JSON files or dictionaries
- Supports multi-language prompts with automatic suffix handling
- Provides conditional line filtering using boolean flags
- Formats prompts with template variable substitution
- Validates format strings and provides helpful error messages
"""
import json
from pathlib import Path
from string import Formatter
from typing import Any, Dict, Optional, Union
import yaml
from loguru import logger
from .base_context import BaseContext
class PromptNotFoundError(KeyError):
"""Exception raised when a requested prompt template is not found."""
def __init__(self, prompt_name: str, available_prompts: list[str]):
self.prompt_name = prompt_name
self.available_prompts = available_prompts
super().__init__(
f"Prompt '{prompt_name}' not found. "
f"Available prompts: {', '.join(available_prompts[:10])}"
f"{'...' if len(available_prompts) > 10 else ''}",
)
class PromptFormattingError(ValueError):
"""Exception raised when prompt formatting fails."""
class PromptHandler(BaseContext):
"""A context-aware handler for loading, retrieving, and formatting prompt templates.
This handler supports:
- Loading prompts from YAML/JSON files or dictionaries
- Multi-language prompt support with automatic language suffix
- Conditional line filtering using boolean flags (e.g., [debug], [verbose])
- Template variable substitution with validation
- Method chaining for fluent API
Examples:
>>> handler = PromptHandler(language="en")
>>> handler.load_prompt_dict({
... "greeting_en": "Hello, {name}!",
... "farewell_en": "[debug]Debug mode\\nGoodbye, {name}!"
... })
>>> handler.prompt_format("greeting", name="Alice")
'Hello, Alice!'
>>> handler.prompt_format("farewell", name="Bob", debug=False)
'Goodbye, Bob!'
"""
def __init__(self, language: str = "", **kwargs):
"""Initialize the PromptHandler with optional language configuration.
Args:
language: Language code to append as suffix (e.g., "en", "zh", "ja").
If provided, get_prompt will automatically try to find
prompts with this suffix (e.g., "greeting" -> "greeting_en").
**kwargs: Additional key-value pairs to initialize the context.
"""
super().__init__(**kwargs)
self.language: str = language.strip()
def load_prompt_by_file(
self,
prompt_file_path: Optional[Union[Path, str]] = None,
overwrite: bool = True,
) -> "PromptHandler":
"""Load prompt configurations from a YAML or JSON file into the context.
Supports both YAML (.yaml, .yml) and JSON (.json) file formats.
Non-existent files are silently skipped.
Args:
prompt_file_path: Path to the prompt configuration file.
If None, returns self without changes.
overwrite: If True, allows overwriting existing prompts with warnings.
If False, skips existing prompts without overwriting.
Returns:
Self for method chaining.
Raises:
ValueError: If file format is not supported.
yaml.YAMLError: If YAML parsing fails.
json.JSONDecodeError: If JSON parsing fails.
"""
if prompt_file_path is None:
return self
if isinstance(prompt_file_path, str):
prompt_file_path = Path(prompt_file_path)
if not prompt_file_path.exists():
logger.warning(f"Prompt file not found: {prompt_file_path}")
return self
suffix = prompt_file_path.suffix.lower()
try:
with prompt_file_path.open(encoding="utf-8") as f:
if suffix in [".yaml", ".yml"]:
prompt_dict = yaml.safe_load(f)
elif suffix == ".json":
prompt_dict = json.load(f)
else:
raise ValueError(
f"Unsupported file format: {suffix}. " f"Supported formats: .yaml, .yml, .json",
)
logger.info(f"Loaded {len(prompt_dict or {})} prompts from {prompt_file_path}")
self.load_prompt_dict(prompt_dict, overwrite=overwrite)
except (yaml.YAMLError, json.JSONDecodeError) as e:
logger.error(f"Failed to parse prompt file {prompt_file_path}: {e}")
raise
return self
def load_prompt_dict(
self,
prompt_dict: Optional[Dict[str, Any]] = None,
overwrite: bool = True,
) -> "PromptHandler":
"""Merge a dictionary of prompt strings into the current context.
Only string values are stored as prompts. Non-string values are skipped.
Args:
prompt_dict: Dictionary mapping prompt names to prompt template strings.
overwrite: If True, allows overwriting existing prompts with warnings.
If False, skips existing prompts without overwriting.
Returns:
Self for method chaining.
"""
if not prompt_dict:
return self
for key, value in prompt_dict.items():
if not isinstance(value, str):
logger.debug(f"Skipping non-string prompt: key={key}, type={type(value)}")
continue
if key in self:
if overwrite:
logger.warning(
f"Overwriting prompt '{key}': " f"old length={len(self[key])}, new length={len(value)}",
)
self[key] = value
else:
logger.debug(f"Skipping existing prompt: key={key}")
else:
logger.debug(f"Adding new prompt: key={key}, length={len(value)}")
self[key] = value
return self
def get_prompt(self, prompt_name: str, fallback_to_base: bool = True) -> str:
"""Retrieve a prompt by name with automatic language suffix handling.
If a language is configured, this method will:
1. First try to find the prompt with language suffix (e.g., "greeting_en")
2. If not found and fallback_to_base is True, try the base name (e.g., "greeting")
3. Otherwise, raise PromptNotFoundError
Args:
prompt_name: Name of the prompt to retrieve.
fallback_to_base: If True and language-specific prompt not found,
fallback to prompt without language suffix.
Returns:
The prompt template string, stripped of leading/trailing whitespace.
Raises:
PromptNotFoundError: If the prompt is not found.
"""
# Try with language suffix first
if self.language and not prompt_name.endswith(f"_{self.language}"):
key_with_lang = f"{prompt_name}_{self.language}"
if key_with_lang in self:
return self[key_with_lang].strip()
# Try base name
if prompt_name in self:
return self[prompt_name].strip()
# Try fallback if enabled
if fallback_to_base and self.language:
# Check if prompt_name already has language suffix, try without it
if prompt_name.endswith(f"_{self.language}"):
base_name = prompt_name[: -(len(self.language) + 1)]
if base_name in self:
return self[base_name].strip()
# Not found, raise error with helpful message
available = list(self.keys())
raise PromptNotFoundError(prompt_name, available)
def has_prompt(self, prompt_name: str) -> bool:
"""Check if a prompt exists (with or without language suffix).
Args:
prompt_name: Name of the prompt to check.
Returns:
True if the prompt exists, False otherwise.
"""
try:
self.get_prompt(prompt_name)
return True
except PromptNotFoundError:
return False
def list_prompts(self, language_filter: Optional[str] = None) -> list[str]:
"""List all available prompt names.
Args:
language_filter: If provided, only return prompts for this language.
If None, return all prompts.
Returns:
List of prompt names.
"""
if language_filter is None:
return list(self.keys())
suffix = f"_{language_filter.strip()}"
return [key for key in self.keys() if key.endswith(suffix)]
@staticmethod
def _extract_format_fields(template: str) -> set[str]:
"""Extract all format field names from a template string.
Args:
template: Template string with {variable} placeholders.
Returns:
Set of field names used in the template.
"""
return {field_name for _, field_name, _, _ in Formatter().parse(template) if field_name is not None}
@staticmethod
def _filter_conditional_lines(prompt: str, flags: Dict[str, bool]) -> str:
"""Filter lines based on boolean flags.
Lines starting with [flag_name] are conditionally included based on
the value of flags[flag_name]. If True, the line is included (without
the flag marker). If False, the line is excluded.
Args:
prompt: The prompt text with conditional markers.
flags: Dictionary of flag names to boolean values.
Returns:
Filtered prompt text.
"""
filtered_lines = []
for line in prompt.split("\n"):
# Check each flag
matched_flag = None
for flag_name in flags:
marker = f"[{flag_name}]"
if line.startswith(marker):
matched_flag = flag_name
break
if matched_flag is None:
# No flag marker, always include
filtered_lines.append(line)
elif flags[matched_flag]:
# Flag is True, include without marker
marker = f"[{matched_flag}]"
filtered_lines.append(line[len(marker) :])
# else: Flag is False, skip this line
return "\n".join(filtered_lines)
def prompt_format(
self,
prompt_name: str,
validate: bool = True,
**kwargs,
) -> str:
"""Format a prompt with conditional line filtering and variable substitution.
This method performs two-stage formatting:
1. Conditional line filtering: Lines marked with [flag] are included only
if the corresponding boolean kwarg is True.
2. Variable substitution: Template variables {var} are replaced with
provided values.
Args:
prompt_name: Name of the prompt to format.
validate: If True, check that all required template variables are provided.
**kwargs: Keyword arguments for formatting. Boolean values are treated as
conditional flags, other values are used for template substitution.
Returns:
Formatted prompt string.
Raises:
PromptNotFoundError: If the prompt is not found.
PromptFormattingError: If validation fails or formatting errors occur.
Examples:
>>> handler = PromptHandler()
>>> handler["test"] = "[debug]Debug: {info}\\nResult: {value}"
>>> handler.prompt_format("test", debug=False, info="test", value=42)
'Result: 42'
>>> handler.prompt_format("test", debug=True, info="test", value=42)
'Debug: test\\nResult: 42'
"""
# Get the prompt template
prompt = self.get_prompt(prompt_name)
# Separate boolean flags from format variables
flag_kwargs = {k: v for k, v in kwargs.items() if isinstance(v, bool)}
format_kwargs = {k: v for k, v in kwargs.items() if not isinstance(v, bool)}
# Step 1: Filter conditional lines
if flag_kwargs:
prompt = self._filter_conditional_lines(prompt, flag_kwargs)
# Step 2: Validate required fields if requested
if validate:
required_fields = self._extract_format_fields(prompt)
missing_fields = required_fields - set(format_kwargs.keys())
if missing_fields:
raise PromptFormattingError(
f"Missing required format variables for prompt '{prompt_name}': "
f"{', '.join(sorted(missing_fields))}",
)
# Step 3: Format with variables
try:
if format_kwargs:
prompt = prompt.format(**format_kwargs)
except KeyError as e:
raise PromptFormattingError(
f"Format error in prompt '{prompt_name}': missing variable {e}",
) from e
except (ValueError, IndexError) as e:
raise PromptFormattingError(
f"Format error in prompt '{prompt_name}': {e}",
) from e
return prompt.strip()
def __repr__(self) -> str:
"""Return a string representation of the PromptHandler."""
return f"PromptHandler(language='{self.language}', " f"num_prompts={len(self)})"

View file

@ -0,0 +1,38 @@
"""Defines the standard data types supported by JSON Schema.
This enum maps common JSON Schema primitive types to their corresponding
Python runtime types, and provides a convenient string representation
compatible with JSON Schema (`"string"`, `"number"`, etc.).
"""
from enum import Enum
class JsonSchemaEnum(Enum):
"""Enumeration of valid JSON Schema data types.
The enum value is the corresponding Python type, while the string
representation (`str(...)`) is the canonical JSON Schema type name.
"""
# Textual data
STRING = str
# Numeric values, including integers and floats
NUMBER = float
# Integer-only numeric values
INTEGER = int
# JSON objects (key-value mappings)
OBJECT = dict
# Ordered JSON lists/arrays
ARRAY = list
# Boolean values: true / false
BOOLEAN = bool
def __str__(self) -> str:
"""Return the lowercase JSON Schema type name for this enum member."""
return self.name.lower()

View file

@ -0,0 +1,33 @@
"""Defines the high-level categories of memory managed by ReMe.
This enumeration is used across the system to tag, route, and store different
kinds of memories (identity, personal context, procedures, tools, etc.).
"""
from enum import Enum
class MemoryType(str, Enum):
"""Enumeration of memory categories used by the memory subsystem.
These types describe *what* a piece of memory is about, which guides
storage, retrieval, and summarization strategies.
"""
# Longterm, relatively stable attributes about the user (name, roles, etc.)
IDENTITY = "identity"
# User-specific preferences, habits, and evolving personal context
PERSONAL = "personal"
# Howto knowledge, workflows, and stepbystep instructions
PROCEDURAL = "procedural"
# Information learned about tools, APIs, and their usage patterns
TOOL = "tool"
# Condensed representation of larger memory collections
SUMMARY = "summary"
# Raw chronological interaction history, typically before summarization
HISTORY = "history"

View file

@ -6,7 +6,6 @@ memories in the ReMe system.
import datetime
import hashlib
import json
from typing import Any
from pydantic import BaseModel, Field, model_validator
@ -146,30 +145,6 @@ class MemoryNode(BaseModel):
metadata=metadata,
)
def format_memory(self) -> str:
"""Format memory as human-readable string.
Returns:
str: Formatted string with when_to_use, content, and ref_memory_id.
"""
parts: list[str] = [
f"memory_id={self.memory_id}",
]
if self.when_to_use:
parts.append(self.when_to_use)
if self.content:
parts.append(self.content)
if self.metadata:
parts.append(f"metadata={json.dumps(self.metadata, ensure_ascii=False)}")
if self.ref_memory_id:
parts.append(f"ref_memory_id={self.ref_memory_id}")
return " ".join(parts)
@classmethod
def from_vector_node(cls, node: VectorNode) -> "MemoryNode":
"""Reconstruct MemoryNode from VectorNode.

View file

@ -132,7 +132,7 @@ class Message(BaseModel):
def strip_md_func(line):
if strip_markdown_headers:
line = re.sub(r'\n##+ +', '\n', line)
line = re.sub(r"\n##+ +", "\n", line)
return line
if add_reasoning and self.reasoning_content:
@ -143,8 +143,9 @@ class Message(BaseModel):
elif isinstance(self.content, list):
for block in self.content:
text = block.content if isinstance(block.content, str) else \
json.dumps(block.content, ensure_ascii=False)
text = (
block.content if isinstance(block.content, str) else json.dumps(block.content, ensure_ascii=False)
)
text = str(text)
lines.append(strip_md_func(text))

View file

@ -101,7 +101,7 @@ class ServiceConfig(BaseModel):
init_logger: bool = Field(default=True)
disabled_flows: List[str] = Field(default_factory=list)
enabled_flows: List[str] = Field(default_factory=list)
mcp_servers: Dict[str, dict] = Field(default_factory=dict, description="External MCP Server configuration")
mcp_servers: Dict[str, dict] = Field(default_factory=dict)
mcp: MCPConfig = Field(default_factory=MCPConfig)
http: HttpConfig = Field(default_factory=HttpConfig)

View file

@ -1,6 +1,4 @@
"""
MCP Tool Schema definitions for recursive JSON Schema representation.
"""
"""MCP Tool Schema definitions for recursive JSON Schema representation."""
import json
from typing import Any, Dict, List, Optional, Union
@ -147,23 +145,17 @@ class ToolCall(BaseModel):
},
}
@classmethod
def from_mcp_tool(cls, tool: Tool) -> "ToolCall":
"""Creates a ToolCall instance from an MCP Tool object."""
# MCP Tool inputSchema maps directly to our parameters ToolAttr
return cls(
name=tool.name,
description=tool.description or "",
parameters=ToolAttr(**tool.inputSchema),
)
def to_mcp_tool(self) -> Tool:
"""Converts the instance back into an MCP Tool object."""
return Tool(
name=self.name,
description=self.description,
inputSchema=self.parameters.simple_input_dump(),
)
def simple_output_dump(self) -> dict:
"""Convert ToolCall to output format dictionary for API responses."""
return {
"index": self.index,
"id": self.id,
self.type: {
"arguments": self.arguments,
"name": self.name,
},
"type": self.type,
}
@property
def argument_dict(self) -> dict:
@ -208,21 +200,27 @@ class ToolCall(BaseModel):
return True
except json.JSONDecodeError:
# Try removing last character
if sanitized[-1] in ']}':
if sanitized[-1] in "]}":
sanitized = sanitized[:-1].rstrip()
else:
break
return False
def simple_output_dump(self) -> dict:
"""Convert ToolCall to output format dictionary for API responses."""
return {
"index": self.index,
"id": self.id,
self.type: {
"arguments": self.arguments,
"name": self.name,
},
"type": self.type,
}
@classmethod
def from_mcp_tool(cls, tool: Tool) -> "ToolCall":
"""Creates a ToolCall instance from an MCP Tool object."""
# MCP Tool inputSchema maps directly to our parameters ToolAttr
return cls(
name=tool.name,
description=tool.description or "",
parameters=ToolAttr(**tool.inputSchema),
)
def to_mcp_tool(self) -> Tool:
"""Converts the instance back into an MCP Tool object."""
return Tool(
name=self.name,
description=self.description,
inputSchema=self.parameters.simple_input_dump(),
)

View file

@ -0,0 +1,7 @@
"""utils"""
from .singleton import singleton
__all__ = [
"singleton",
]

View file

@ -0,0 +1,17 @@
"""Core module for ReMe AI framework."""
# pylint: disable=wrong-import-position
# flake8: noqa: F401
from . import config
from . import context
from . import embedding
from . import enumeration
from . import flow
from . import llm
from . import op
from . import schema
from . import service
from . import token_counter
from . import utils
from . import vector_store

View file

@ -2,10 +2,15 @@
from .base_context import BaseContext
from .prompt_handler import PromptHandler
from .registry_factory import R
from .registry import Registry
from .runtime_context import RuntimeContext
from .service_context import ServiceContext, C
__all__ = [
"BaseContext",
"PromptHandler",
"R",
"Registry",
"RuntimeContext",
"ServiceContext",
"C",
]

View file

@ -1,99 +1,24 @@
"""Module for managing and formatting prompt templates from files or dictionaries.
"""Module for managing and formatting prompt templates from files or dictionaries."""
This module provides a PromptHandler class that:
- Loads prompts from YAML/JSON files or dictionaries
- Supports multi-language prompts with automatic suffix handling
- Provides conditional line filtering using boolean flags
- Formats prompts with template variable substitution
- Validates format strings and provides helpful error messages
"""
import json
from pathlib import Path
from string import Formatter
from typing import Any, Dict, Optional, Union
import yaml
from loguru import logger
from .base_context import BaseContext
class PromptNotFoundError(KeyError):
"""Exception raised when a requested prompt template is not found."""
def __init__(self, prompt_name: str, available_prompts: list[str]):
self.prompt_name = prompt_name
self.available_prompts = available_prompts
super().__init__(
f"Prompt '{prompt_name}' not found. "
f"Available prompts: {', '.join(available_prompts[:10])}"
f"{'...' if len(available_prompts) > 10 else ''}",
)
class PromptFormattingError(ValueError):
"""Exception raised when prompt formatting fails."""
from .service_context import C
class PromptHandler(BaseContext):
"""A context-aware handler for loading, retrieving, and formatting prompt templates.
This handler supports:
- Loading prompts from YAML/JSON files or dictionaries
- Multi-language prompt support with automatic language suffix
- Conditional line filtering using boolean flags (e.g., [debug], [verbose])
- Template variable substitution with validation
- Method chaining for fluent API
Examples:
>>> handler = PromptHandler(language="en")
>>> handler.load_prompt_dict({
... "greeting_en": "Hello, {name}!",
... "farewell_en": "[debug]Debug mode\\nGoodbye, {name}!"
... })
>>> handler.prompt_format("greeting", name="Alice")
'Hello, Alice!'
>>> handler.prompt_format("farewell", name="Bob", debug=False)
'Goodbye, Bob!'
"""
"""A context-aware handler for loading, retrieving, and formatting prompt templates."""
def __init__(self, language: str = "", **kwargs):
"""Initialize the PromptHandler with optional language configuration.
Args:
language: Language code to append as suffix (e.g., "en", "zh", "ja").
If provided, get_prompt will automatically try to find
prompts with this suffix (e.g., "greeting" -> "greeting_en").
**kwargs: Additional key-value pairs to initialize the context.
"""
"""Initialize the handler with a specific language and optional context data."""
super().__init__(**kwargs)
self.language: str = language.strip()
self.language: str = language or C.language
def load_prompt_by_file(
self,
prompt_file_path: Optional[Union[Path, str]] = None,
overwrite: bool = True,
) -> "PromptHandler":
"""Load prompt configurations from a YAML or JSON file into the context.
Supports both YAML (.yaml, .yml) and JSON (.json) file formats.
Non-existent files are silently skipped.
Args:
prompt_file_path: Path to the prompt configuration file.
If None, returns self without changes.
overwrite: If True, allows overwriting existing prompts with warnings.
If False, skips existing prompts without overwriting.
Returns:
Self for method chaining.
Raises:
ValueError: If file format is not supported.
yaml.YAMLError: If YAML parsing fails.
json.JSONDecodeError: If JSON parsing fails.
"""
def load_prompt_by_file(self, prompt_file_path: Path | str = None):
"""Load prompt configurations from a YAML file into the context."""
if prompt_file_path is None:
return self
@ -101,263 +26,70 @@ class PromptHandler(BaseContext):
prompt_file_path = Path(prompt_file_path)
if not prompt_file_path.exists():
logger.warning(f"Prompt file not found: {prompt_file_path}")
return self
suffix = prompt_file_path.suffix.lower()
try:
with prompt_file_path.open(encoding="utf-8") as f:
if suffix in [".yaml", ".yml"]:
prompt_dict = yaml.safe_load(f)
elif suffix == ".json":
prompt_dict = json.load(f)
else:
raise ValueError(
f"Unsupported file format: {suffix}. " f"Supported formats: .yaml, .yml, .json",
)
logger.info(f"Loaded {len(prompt_dict or {})} prompts from {prompt_file_path}")
self.load_prompt_dict(prompt_dict, overwrite=overwrite)
except (yaml.YAMLError, json.JSONDecodeError) as e:
logger.error(f"Failed to parse prompt file {prompt_file_path}: {e}")
raise
with prompt_file_path.open(encoding="utf-8") as f:
# Load YAML content using the full loader
prompt_dict = yaml.load(f, yaml.FullLoader)
self.load_prompt_dict(prompt_dict)
return self
def load_prompt_dict(
self,
prompt_dict: Optional[Dict[str, Any]] = None,
overwrite: bool = True,
) -> "PromptHandler":
"""Merge a dictionary of prompt strings into the current context.
Only string values are stored as prompts. Non-string values are skipped.
Args:
prompt_dict: Dictionary mapping prompt names to prompt template strings.
overwrite: If True, allows overwriting existing prompts with warnings.
If False, skips existing prompts without overwriting.
Returns:
Self for method chaining.
"""
def load_prompt_dict(self, prompt_dict: dict = None):
"""Merge a dictionary of prompt strings into the current context."""
if not prompt_dict:
return self
for key, value in prompt_dict.items():
if not isinstance(value, str):
logger.debug(f"Skipping non-string prompt: key={key}, type={type(value)}")
continue
if key in self:
if overwrite:
logger.warning(
f"Overwriting prompt '{key}': " f"old length={len(self[key])}, new length={len(value)}",
)
self[key] = value
if isinstance(value, str):
if key in self:
logger.warning(f"Overwriting prompt key={key}, old_value={self[key]}, new_value={value}")
else:
logger.debug(f"Skipping existing prompt: key={key}")
else:
logger.debug(f"Adding new prompt: key={key}, length={len(value)}")
logger.debug(f"Adding new prompt key={key}, value={value}")
self[key] = value
return self
def get_prompt(self, prompt_name: str, fallback_to_base: bool = True) -> str:
"""Retrieve a prompt by name with automatic language suffix handling.
def get_prompt(self, prompt_name: str):
"""Retrieve a prompt by name, automatically appending the language suffix if needed."""
key: str = prompt_name
if self.language and not key.endswith(self.language.strip()):
key += "_" + self.language.strip()
If a language is configured, this method will:
1. First try to find the prompt with language suffix (e.g., "greeting_en")
2. If not found and fallback_to_base is True, try the base name (e.g., "greeting")
3. Otherwise, raise PromptNotFoundError
assert key in self, f"prompt_name={key} not found."
return self[key].strip()
Args:
prompt_name: Name of the prompt to retrieve.
fallback_to_base: If True and language-specific prompt not found,
fallback to prompt without language suffix.
Returns:
The prompt template string, stripped of leading/trailing whitespace.
Raises:
PromptNotFoundError: If the prompt is not found.
"""
# Try with language suffix first
if self.language and not prompt_name.endswith(f"_{self.language}"):
key_with_lang = f"{prompt_name}_{self.language}"
if key_with_lang in self:
return self[key_with_lang].strip()
# Try base name
if prompt_name in self:
return self[prompt_name].strip()
# Try fallback if enabled
if fallback_to_base and self.language:
# Check if prompt_name already has language suffix, try without it
if prompt_name.endswith(f"_{self.language}"):
base_name = prompt_name[: -(len(self.language) + 1)]
if base_name in self:
return self[base_name].strip()
# Not found, raise error with helpful message
available = list(self.keys())
raise PromptNotFoundError(prompt_name, available)
def has_prompt(self, prompt_name: str) -> bool:
"""Check if a prompt exists (with or without language suffix).
Args:
prompt_name: Name of the prompt to check.
Returns:
True if the prompt exists, False otherwise.
"""
try:
self.get_prompt(prompt_name)
return True
except PromptNotFoundError:
return False
def list_prompts(self, language_filter: Optional[str] = None) -> list[str]:
"""List all available prompt names.
Args:
language_filter: If provided, only return prompts for this language.
If None, return all prompts.
Returns:
List of prompt names.
"""
if language_filter is None:
return list(self.keys())
suffix = f"_{language_filter.strip()}"
return [key for key in self.keys() if key.endswith(suffix)]
@staticmethod
def _extract_format_fields(template: str) -> set[str]:
"""Extract all format field names from a template string.
Args:
template: Template string with {variable} placeholders.
Returns:
Set of field names used in the template.
"""
return {field_name for _, field_name, _, _ in Formatter().parse(template) if field_name is not None}
@staticmethod
def _filter_conditional_lines(prompt: str, flags: Dict[str, bool]) -> str:
"""Filter lines based on boolean flags.
Lines starting with [flag_name] are conditionally included based on
the value of flags[flag_name]. If True, the line is included (without
the flag marker). If False, the line is excluded.
Args:
prompt: The prompt text with conditional markers.
flags: Dictionary of flag names to boolean values.
Returns:
Filtered prompt text.
"""
filtered_lines = []
for line in prompt.split("\n"):
# Check each flag
matched_flag = None
for flag_name in flags:
marker = f"[{flag_name}]"
if line.startswith(marker):
matched_flag = flag_name
break
if matched_flag is None:
# No flag marker, always include
filtered_lines.append(line)
elif flags[matched_flag]:
# Flag is True, include without marker
marker = f"[{matched_flag}]"
filtered_lines.append(line[len(marker) :])
# else: Flag is False, skip this line
return "\n".join(filtered_lines)
def prompt_format(
self,
prompt_name: str,
validate: bool = True,
**kwargs,
) -> str:
"""Format a prompt with conditional line filtering and variable substitution.
This method performs two-stage formatting:
1. Conditional line filtering: Lines marked with [flag] are included only
if the corresponding boolean kwarg is True.
2. Variable substitution: Template variables {var} are replaced with
provided values.
Args:
prompt_name: Name of the prompt to format.
validate: If True, check that all required template variables are provided.
**kwargs: Keyword arguments for formatting. Boolean values are treated as
conditional flags, other values are used for template substitution.
Returns:
Formatted prompt string.
Raises:
PromptNotFoundError: If the prompt is not found.
PromptFormattingError: If validation fails or formatting errors occur.
Examples:
>>> handler = PromptHandler()
>>> handler["test"] = "[debug]Debug: {info}\\nResult: {value}"
>>> handler.prompt_format("test", debug=False, info="test", value=42)
'Result: 42'
>>> handler.prompt_format("test", debug=True, info="test", value=42)
'Debug: test\\nResult: 42'
"""
# Get the prompt template
def prompt_format(self, prompt_name: str, **kwargs) -> str:
"""Format a prompt by filtering flagged lines and filling template variables."""
prompt = self.get_prompt(prompt_name)
# Separate boolean flags from format variables
# Separate boolean flags from string formatting arguments
flag_kwargs = {k: v for k, v in kwargs.items() if isinstance(v, bool)}
format_kwargs = {k: v for k, v in kwargs.items() if not isinstance(v, bool)}
other_kwargs = {k: v for k, v in kwargs.items() if not isinstance(v, bool)}
# Step 1: Filter conditional lines
if flag_kwargs:
prompt = self._filter_conditional_lines(prompt, flag_kwargs)
split_prompt = []
for line in prompt.strip().split("\n"):
hit = False
hit_flag = True
for key, flag in flag_kwargs.items():
if not line.startswith(f"[{key}]"):
continue
# Step 2: Validate required fields if requested
if validate:
required_fields = self._extract_format_fields(prompt)
missing_fields = required_fields - set(format_kwargs.keys())
hit = True
hit_flag = flag
# Remove the flag prefix from the line
line = line.strip(f"[{key}]")
break
if missing_fields:
raise PromptFormattingError(
f"Missing required format variables for prompt '{prompt_name}': "
f"{', '.join(sorted(missing_fields))}",
)
# Include line if no flag is present or if the flag evaluates to True
if not hit:
split_prompt.append(line)
elif hit_flag:
split_prompt.append(line)
# Step 3: Format with variables
try:
if format_kwargs:
prompt = prompt.format(**format_kwargs)
except KeyError as e:
raise PromptFormattingError(
f"Format error in prompt '{prompt_name}': missing variable {e}",
) from e
except (ValueError, IndexError) as e:
raise PromptFormattingError(
f"Format error in prompt '{prompt_name}': {e}",
) from e
prompt = "\n".join(split_prompt)
return prompt.strip()
if other_kwargs:
# Apply standard Python string formatting
prompt = prompt.format(**other_kwargs)
def __repr__(self) -> str:
"""Return a string representation of the PromptHandler."""
return f"PromptHandler(language='{self.language}', " f"num_prompts={len(self)})"
return prompt

View file

@ -1,38 +1,18 @@
"""Defines the standard data types supported by JSON Schema.
This enum maps common JSON Schema primitive types to their corresponding
Python runtime types, and provides a convenient string representation
compatible with JSON Schema (`"string"`, `"number"`, etc.).
"""
"""Defines the standard data types supported by JSON Schema."""
from enum import Enum
class JsonSchemaEnum(Enum):
"""Enumeration of valid JSON Schema data types.
"""Enumeration of valid JSON Schema data types."""
The enum value is the corresponding Python type, while the string
representation (`str(...)`) is the canonical JSON Schema type name.
"""
# Textual data
STRING = str
# Numeric values, including integers and floats
NUMBER = float
# Integer-only numeric values
INTEGER = int
# JSON objects (key-value mappings)
OBJECT = dict
# Ordered JSON lists/arrays
ARRAY = list
# Boolean values: true / false
BOOLEAN = bool
def __str__(self) -> str:
"""Return the lowercase JSON Schema type name for this enum member."""
"""Returns the string representation of the enum value."""
return self.name.lower()

View file

@ -1,33 +1,25 @@
"""Defines the high-level categories of memory managed by ReMe.
This enumeration is used across the system to tag, route, and store different
kinds of memories (identity, personal context, procedures, tools, etc.).
"""
"""Memory type enumeration for the three-layer memory architecture."""
from enum import Enum
class MemoryType(str, Enum):
"""Enumeration of memory categories used by the memory subsystem.
"""
Three-layer memory architecture for agent memory management.
These types describe *what* a piece of memory is about, which guides
storage, retrieval, and summarization strategies.
Layer 1 - High-level Abstraction Memory:
- IDENTITY: Self-cognition (identity, personality, current state)
- PERSONAL: Person-specific memory (preferences and context about specific individuals)
- PROCEDURAL: Procedural memory (how-to knowledge, e.g., 4 steps to write financial reports)
- TOOL: Tool memory (tool usage patterns, success rates, token consumption, latency)
Layer 2 - Summary Memory (Compressed): Summarized digest of raw message history
Layer 3 - History Memory (Raw): Raw message history
"""
# Longterm, relatively stable attributes about the user (name, roles, etc.)
IDENTITY = "identity"
# User-specific preferences, habits, and evolving personal context
PERSONAL = "personal"
# Howto knowledge, workflows, and stepbystep instructions
PROCEDURAL = "procedural"
# Information learned about tools, APIs, and their usage patterns
TOOL = "tool"
# Condensed representation of larger memory collections
SUMMARY = "summary"
# Raw chronological interaction history, typically before summarization
HISTORY = "history"

View file

@ -6,6 +6,7 @@ memories in the ReMe system.
import datetime
import hashlib
import json
from typing import Any
from pydantic import BaseModel, Field, model_validator
@ -145,6 +146,30 @@ class MemoryNode(BaseModel):
metadata=metadata,
)
def format_memory(self) -> str:
"""Format memory as human-readable string.
Returns:
str: Formatted string with when_to_use, content, and ref_memory_id.
"""
parts: list[str] = [
f"memory_id={self.memory_id}",
]
if self.when_to_use:
parts.append(self.when_to_use)
if self.content:
parts.append(self.content)
if self.metadata:
parts.append(f"metadata={json.dumps(self.metadata, ensure_ascii=False)}")
if self.ref_memory_id:
parts.append(f"ref_memory_id={self.ref_memory_id}")
return " ".join(parts)
@classmethod
def from_vector_node(cls, node: VectorNode) -> "MemoryNode":
"""Reconstruct MemoryNode from VectorNode.

View file

@ -132,7 +132,7 @@ class Message(BaseModel):
def strip_md_func(line):
if strip_markdown_headers:
line = re.sub(r"\n##+ +", "\n", line)
line = re.sub(r'\n##+ +', '\n', line)
return line
if add_reasoning and self.reasoning_content:
@ -143,9 +143,8 @@ class Message(BaseModel):
elif isinstance(self.content, list):
for block in self.content:
text = (
block.content if isinstance(block.content, str) else json.dumps(block.content, ensure_ascii=False)
)
text = block.content if isinstance(block.content, str) else \
json.dumps(block.content, ensure_ascii=False)
text = str(text)
lines.append(strip_md_func(text))

View file

@ -101,7 +101,7 @@ class ServiceConfig(BaseModel):
init_logger: bool = Field(default=True)
disabled_flows: List[str] = Field(default_factory=list)
enabled_flows: List[str] = Field(default_factory=list)
mcp_servers: Dict[str, dict] = Field(default_factory=dict)
mcp_servers: Dict[str, dict] = Field(default_factory=dict, description="External MCP Server configuration")
mcp: MCPConfig = Field(default_factory=MCPConfig)
http: HttpConfig = Field(default_factory=HttpConfig)

View file

@ -1,4 +1,6 @@
"""MCP Tool Schema definitions for recursive JSON Schema representation."""
"""
MCP Tool Schema definitions for recursive JSON Schema representation.
"""
import json
from typing import Any, Dict, List, Optional, Union
@ -145,17 +147,23 @@ class ToolCall(BaseModel):
},
}
def simple_output_dump(self) -> dict:
"""Convert ToolCall to output format dictionary for API responses."""
return {
"index": self.index,
"id": self.id,
self.type: {
"arguments": self.arguments,
"name": self.name,
},
"type": self.type,
}
@classmethod
def from_mcp_tool(cls, tool: Tool) -> "ToolCall":
"""Creates a ToolCall instance from an MCP Tool object."""
# MCP Tool inputSchema maps directly to our parameters ToolAttr
return cls(
name=tool.name,
description=tool.description or "",
parameters=ToolAttr(**tool.inputSchema),
)
def to_mcp_tool(self) -> Tool:
"""Converts the instance back into an MCP Tool object."""
return Tool(
name=self.name,
description=self.description,
inputSchema=self.parameters.simple_input_dump(),
)
@property
def argument_dict(self) -> dict:
@ -200,27 +208,21 @@ class ToolCall(BaseModel):
return True
except json.JSONDecodeError:
# Try removing last character
if sanitized[-1] in "]}":
if sanitized[-1] in ']}':
sanitized = sanitized[:-1].rstrip()
else:
break
return False
@classmethod
def from_mcp_tool(cls, tool: Tool) -> "ToolCall":
"""Creates a ToolCall instance from an MCP Tool object."""
# MCP Tool inputSchema maps directly to our parameters ToolAttr
return cls(
name=tool.name,
description=tool.description or "",
parameters=ToolAttr(**tool.inputSchema),
)
def to_mcp_tool(self) -> Tool:
"""Converts the instance back into an MCP Tool object."""
return Tool(
name=self.name,
description=self.description,
inputSchema=self.parameters.simple_input_dump(),
)
def simple_output_dump(self) -> dict:
"""Convert ToolCall to output format dictionary for API responses."""
return {
"index": self.index,
"id": self.id,
self.type: {
"arguments": self.arguments,
"name": self.name,
},
"type": self.type,
}

View file

@ -1,7 +1,47 @@
"""utils"""
from .cache_handler import CacheHandler
from .case_converter import snake_to_camel, camel_to_snake
from .common_utils import run_coro_safely, execute_stream_task
from .env_utils import load_env
from .execute_tuils import exec_code, run_shell_command
from .http_client import HttpClient
from .llm_utils import extract_content, format_messages, deduplicate_memories
from .logger_utils import init_logger
from .logo_utils import print_logo
# Make MCPClient import optional to avoid breaking if MCP dependencies are not available
try:
from .mcp_client import MCPClient
_HAS_MCP = True
except ImportError:
MCPClient = None
_HAS_MCP = False
from .pydantic_config_parser import PydanticConfigParser
from .pydantic_utils import create_pydantic_model
from .singleton import singleton
from .time import timer, get_now_time
__all__ = [
"CacheHandler",
"snake_to_camel",
"camel_to_snake",
"run_coro_safely",
"execute_stream_task",
"load_env",
"exec_code",
"run_shell_command",
"HttpClient",
"extract_content",
"format_messages",
"deduplicate_memories",
"init_logger",
"print_logo",
"MCPClient",
"PydanticConfigParser",
"create_pydantic_model",
"singleton",
"timer",
"get_now_time",
]

View file

@ -5,9 +5,9 @@ from abc import ABC, abstractmethod
from collections.abc import Callable
from functools import partial
from reme_ai.core_old.context import C
from reme_ai.core_old.embedding import BaseEmbeddingModel
from reme_ai.core_old.schema import VectorNode
from reme_ai.core.context import C
from reme_ai.core.embedding import BaseEmbeddingModel
from reme_ai.core.schema import VectorNode
class BaseVectorStore(ABC):

Some files were not shown because too many files have changed in this diff Show more