mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
refactor(core): migrate core modules and update imports
This commit is contained in:
parent
32ff65ca0d
commit
4560ff09ad
190 changed files with 906 additions and 906 deletions
|
|
@ -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}:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
0
reme/__init__.py
Normal file
0
reme/core/__init__.py
Normal file
0
reme/core/__init__.py
Normal file
11
reme/core/context/__init__.py
Normal file
11
reme/core/context/__init__.py
Normal 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",
|
||||
]
|
||||
363
reme/core/context/prompt_handler.py
Normal file
363
reme/core/context/prompt_handler.py
Normal 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)})"
|
||||
38
reme/core/enumeration/json_schema_enum.py
Normal file
38
reme/core/enumeration/json_schema_enum.py
Normal 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()
|
||||
33
reme/core/enumeration/memory_type.py
Normal file
33
reme/core/enumeration/memory_type.py
Normal 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.
|
||||
"""
|
||||
|
||||
# Long‑term, relatively stable attributes about the user (name, roles, etc.)
|
||||
IDENTITY = "identity"
|
||||
|
||||
# User-specific preferences, habits, and evolving personal context
|
||||
PERSONAL = "personal"
|
||||
|
||||
# How‑to knowledge, workflows, and step‑by‑step 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"
|
||||
|
|
@ -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.
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
7
reme/core/utils/__init__.py
Normal file
7
reme/core/utils/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""utils"""
|
||||
|
||||
from .singleton import singleton
|
||||
|
||||
__all__ = [
|
||||
"singleton",
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
||||
# Long‑term, relatively stable attributes about the user (name, roles, etc.)
|
||||
IDENTITY = "identity"
|
||||
|
||||
# User-specific preferences, habits, and evolving personal context
|
||||
PERSONAL = "personal"
|
||||
|
||||
# How‑to knowledge, workflows, and step‑by‑step 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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue