Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Please include a summary of the change, the problem it solves, the implementation approach, and relevant context. List any dependencies required for this change.

Related Issue (Required): Fixes @issue_number
Related Issue (Required): Fixes #issue_number

## Type of change

Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ jobs:
if: ${{ !startsWith(matrix.os, 'macos-13') }}
run: |
poetry install --no-interaction --extras all
- name: PyTest unit tests
- name: PyTest unit tests with coverage
if: ${{ !startsWith(matrix.os, 'macos-13') }}
shell: bash
run: |
poetry run pytest tests -vv --durations=10
poetry run pytest tests -vv --durations=10 \
--cov=src/memos \
--cov-report=term-missing \
--cov-fail-under=28
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
report/
cov-report/
.tox/
.nox/
.coverage
Expand Down
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: test
.PHONY: test test-report test-cov

install:
poetry install --extras all --with dev --with test
Expand All @@ -9,10 +9,25 @@ clean:
rm -rf .pytest_cache
rm -rf .ruff_cache
rm -rf tmp
rm -rf report cov-report
rm -f .coverage .coverage.*

test:
poetry run pytest tests

test-report:
poetry run pytest tests -vv --durations=10 \
--html=report/index.html \
--cov=src/memos \
--cov-report=term-missing \
--cov-report=html:cov-report/src

test-cov:
poetry run pytest tests \
--cov=src/memos \
--cov-report=term-missing \
--cov-report=html:cov-report/src

format:
poetry run ruff check --fix
poetry run ruff format
Expand Down
192 changes: 187 additions & 5 deletions poetry.lock

Large diffs are not rendered by default.

21 changes: 20 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
##############################################################################

name = "MemoryOS"
version = "2.0.9"
version = "2.0.10"
description = "Intelligence Begins with Memory"
license = {text = "Apache-2.0"}
readme = "README.md"
Expand Down Expand Up @@ -170,6 +170,8 @@ optional = true
[tool.poetry.group.test.dependencies]
pytest = "^8.3.5"
pytest-asyncio = "^0.23.5"
pytest-cov = "^6.1"
pytest-html = "^4.2"
ruff = "^0.11.8"

[tool.poetry.group.eval]
Expand Down Expand Up @@ -208,6 +210,23 @@ filterwarnings = [
]


[tool.coverage.run]
source = ["src/memos"]
branch = true

[tool.coverage.report]
show_missing = true
skip_empty = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"if __name__ == .__main__.",
]

[tool.coverage.html]
directory = "cov-report"


[tool.ruff]
##############################################################################
# Ruff is a fast Python linter and formatter.
Expand Down
2 changes: 1 addition & 1 deletion src/memos/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "2.0.9"
__version__ = "2.0.10"

from memos.configs.mem_cube import GeneralMemCubeConfig
from memos.configs.mem_os import MOSConfig
Expand Down
51 changes: 35 additions & 16 deletions src/memos/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,23 +321,40 @@ def get_activation_config() -> dict[str, Any]:

@staticmethod
def get_memreader_config() -> dict[str, Any]:
"""Get MemReader configuration for chat/doc extraction (fine-tuned 0.6B model)."""
return {
"backend": "openai",
"config": {
"model_name_or_path": os.getenv("MEMRADER_MODEL", "gpt-4o-mini"),
"temperature": 0.6,
"max_tokens": int(os.getenv("MEMRADER_MAX_TOKENS", "8000")),
"top_p": 0.95,
"top_k": 20,
"api_key": os.getenv("MEMRADER_API_KEY", "EMPTY"),
# Default to OpenAI base URL when env var is not provided to satisfy pydantic
# validation requirements during tests/import.
"api_base": os.getenv("MEMRADER_API_BASE", "https://api.openai.com/v1"),
"remove_think_prefix": True,
},
"""Get MemReader configuration for chat/doc extraction (fine-tuned 0.6B model).

When MEMREADER_GENERAL_MODEL is configured (i.e. a separate stable LLM exists),
the backup client is automatically enabled so that primary failures (self-deployed
model) fall back to the general LLM.
"""
config = {
"model_name_or_path": os.getenv("MEMRADER_MODEL", "gpt-4o-mini"),
"temperature": 0.6,
"max_tokens": int(os.getenv("MEMRADER_MAX_TOKENS", "8000")),
"top_p": 0.95,
"top_k": 20,
"api_key": os.getenv("MEMRADER_API_KEY", "EMPTY"),
# Default to OpenAI base URL when env var is not provided to satisfy pydantic
# validation requirements during tests/import.
"api_base": os.getenv("MEMRADER_API_BASE", "https://api.openai.com/v1"),
"remove_think_prefix": True,
}

general_model = os.getenv("MEMREADER_GENERAL_MODEL")
enable_backup = os.getenv("MEMREADER_ENABLE_BACKUP", "false").lower() == "true"
if general_model and enable_backup:
config["backup_client"] = True
config["backup_model_name_or_path"] = general_model
config["backup_api_key"] = os.getenv(
"MEMREADER_GENERAL_API_KEY", os.getenv("OPENAI_API_KEY", "EMPTY")
)
config["backup_api_base"] = os.getenv(
"MEMREADER_GENERAL_API_BASE",
os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"),
)

return {"backend": "openai", "config": config}

@staticmethod
def get_memreader_general_llm_config() -> dict[str, Any]:
"""Get general LLM configuration for non-chat/doc tasks.
Expand Down Expand Up @@ -837,7 +854,7 @@ def get_scheduler_config() -> dict[str, Any]:
),
"context_window_size": int(os.getenv("MOS_SCHEDULER_CONTEXT_WINDOW_SIZE", "5")),
"thread_pool_max_workers": int(
os.getenv("MOS_SCHEDULER_THREAD_POOL_MAX_WORKERS", "10000")
os.getenv("MOS_SCHEDULER_THREAD_POOL_MAX_WORKERS", "200")
),
"consume_interval_seconds": float(
os.getenv("MOS_SCHEDULER_CONSUME_INTERVAL_SECONDS", "0.01")
Expand All @@ -850,6 +867,8 @@ def get_scheduler_config() -> dict[str, Any]:
"MOS_SCHEDULER_ENABLE_ACTIVATION_MEMORY", "false"
).lower()
== "true",
"use_redis_queue": os.getenv("MEMSCHEDULER_USE_REDIS_QUEUE", "False").lower()
== "true",
},
}

Expand Down
7 changes: 4 additions & 3 deletions src/memos/api/handlers/component_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def init_server() -> dict[str, Any]:
searcher: Searcher = tree_mem.get_searcher(
manual_close_internet=os.getenv("ENABLE_INTERNET", "true").lower() == "false",
moscube=False,
process_llm=mem_reader.llm,
process_llm=mem_reader.general_llm,
)
logger.debug("Searcher created")

Expand All @@ -255,12 +255,13 @@ def init_server() -> dict[str, Any]:
# Initialize Scheduler
scheduler_config_dict = APIConfig.get_scheduler_config()
scheduler_config = SchedulerConfigFactory(
backend="optimized_scheduler", config=scheduler_config_dict
backend=scheduler_config_dict["backend"],
config=scheduler_config_dict["config"],
)
mem_scheduler: OptimizedScheduler = SchedulerFactory.from_config(scheduler_config)
mem_scheduler.initialize_modules(
chat_llm=llm,
process_llm=mem_reader.llm,
process_llm=mem_reader.general_llm,
db_engine=BaseDBManager.create_default_sqlite_engine(),
mem_reader=mem_reader,
redis_client=redis_client,
Expand Down
24 changes: 18 additions & 6 deletions src/memos/configs/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ class OpenAILLMConfig(BaseLLMConfig):
default="https://api.openai.com/v1", description="Base URL for OpenAI API"
)
extra_body: Any = Field(default=None, description="extra body")
backup_client: bool = Field(
default=False,
description="Whether to enable backup client for fallback on primary failure",
)
backup_api_key: str | None = Field(
default=None, description="API key for backup OpenAI-compatible endpoint"
)
backup_api_base: str | None = Field(
default=None, description="Base URL for backup OpenAI-compatible endpoint"
)
backup_model_name_or_path: str | None = Field(
default=None, description="Model name for backup endpoint"
)
backup_headers: dict[str, Any] | None = Field(
default=None, description="Default headers for backup client requests"
)


class OpenAIResponsesLLMConfig(BaseLLMConfig):
Expand All @@ -42,22 +58,18 @@ class OpenAIResponsesLLMConfig(BaseLLMConfig):
)


class QwenLLMConfig(BaseLLMConfig):
api_key: str = Field(..., description="API key for DashScope (Qwen)")
class QwenLLMConfig(OpenAILLMConfig):
api_base: str = Field(
default="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
description="Base URL for Qwen OpenAI-compatible API",
)
extra_body: Any = Field(default=None, description="extra body")


class DeepSeekLLMConfig(BaseLLMConfig):
api_key: str = Field(..., description="API key for DeepSeek")
class DeepSeekLLMConfig(OpenAILLMConfig):
api_base: str = Field(
default="https://api.deepseek.com",
description="Base URL for DeepSeek OpenAI-compatible API",
)
extra_body: Any = Field(default=None, description="Extra options for API")


class AzureLLMConfig(BaseLLMConfig):
Expand Down
5 changes: 4 additions & 1 deletion src/memos/configs/mem_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,10 @@ def validate_backend(cls, backend: str) -> str:
@model_validator(mode="after")
def create_config(self) -> "SchedulerConfigFactory":
config_class = self.backend_to_class[self.backend]
self.config = config_class(**self.config)
raw = self.config
if isinstance(raw, dict) and "config" in raw and "use_redis_queue" not in raw:
raw = raw["config"]
self.config = config_class(**raw)
return self


Expand Down
11 changes: 9 additions & 2 deletions src/memos/graph_dbs/neo4j_community.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,8 @@ def get_by_metadata(
user_name: str | None = None,
filter: dict | None = None,
knowledgebase_ids: list[str] | None = None,
user_name_flag: bool = True,
status: str | None = None,
) -> list[str]:
"""
Retrieve node IDs that match given metadata filters.
Expand All @@ -745,15 +747,20 @@ def get_by_metadata(
- Can be used for faceted recall or prefiltering before embedding rerank.
"""
logger.info(
f"[get_by_metadata] filters: {filters},user_name: {user_name},filter: {filter},knowledgebase_ids: {knowledgebase_ids}"
f"[get_by_metadata] filters: {filters},user_name: {user_name},filter: {filter},knowledgebase_ids: {knowledgebase_ids},status: {status}"
)
print(
f"[get_by_metadata] filters: {filters},user_name: {user_name},filter: {filter},knowledgebase_ids: {knowledgebase_ids}"
f"[get_by_metadata] filters: {filters},user_name: {user_name},filter: {filter},knowledgebase_ids: {knowledgebase_ids},status: {status}"
)
user_name = user_name if user_name else self.config.user_name
where_clauses = []
params = {}

# Add status filter if provided
if status:
where_clauses.append("n.status = $status")
params["status"] = status

for i, f in enumerate(filters):
field = f["field"]
op = f.get("op", "=")
Expand Down
Loading
Loading