Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34c797d285 | ||
|
|
a0d8a02b94 |
@@ -7,6 +7,11 @@ mode: "wide"
|
|||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Python">
|
<Tab title="Python">
|
||||||
|
|
||||||
|
<Update label="2026-03-03" description="v1.0.5">
|
||||||
|
- **Telemetry Fix**
|
||||||
|
- Fixed an issue where the PostHog client was initialized even after telemetry was disabled. Although events were not captured, the client was unnecessarily initialized.
|
||||||
|
</Update>
|
||||||
|
|
||||||
<Update label="2026-02-17" description="v1.0.4">
|
<Update label="2026-02-17" description="v1.0.4">
|
||||||
|
|
||||||
**New Features & Updates:**
|
**New Features & Updates:**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mem0ai",
|
"name": "mem0ai",
|
||||||
"version": "2.2.3",
|
"version": "2.2.4",
|
||||||
"description": "The Memory Layer For Your AI Apps",
|
"description": "The Memory Layer For Your AI Apps",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"typescript": "5.5.4"
|
"typescript": "5.5.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "1.7.7",
|
"axios": "1.13.6",
|
||||||
"openai": "^4.93.0",
|
"openai": "^4.93.0",
|
||||||
"uuid": "9.0.1",
|
"uuid": "9.0.1",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
|
|||||||
Generated
+25
-10
@@ -44,8 +44,8 @@ importers:
|
|||||||
specifier: 3.1.11
|
specifier: 3.1.11
|
||||||
version: 3.1.11
|
version: 3.1.11
|
||||||
axios:
|
axios:
|
||||||
specifier: 1.7.7
|
specifier: 1.13.6
|
||||||
version: 1.7.7
|
version: 1.13.6
|
||||||
cloudflare:
|
cloudflare:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.3.0(encoding@0.1.13)
|
version: 4.3.0(encoding@0.1.13)
|
||||||
@@ -1621,10 +1621,10 @@ packages:
|
|||||||
integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==,
|
integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==,
|
||||||
}
|
}
|
||||||
|
|
||||||
axios@1.7.7:
|
axios@1.13.6:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==,
|
integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==,
|
||||||
}
|
}
|
||||||
|
|
||||||
babel-jest@29.7.0:
|
babel-jest@29.7.0:
|
||||||
@@ -2560,10 +2560,10 @@ packages:
|
|||||||
}
|
}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
follow-redirects@1.15.9:
|
follow-redirects@1.15.11:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==,
|
integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==,
|
||||||
}
|
}
|
||||||
engines: { node: ">=4.0" }
|
engines: { node: ">=4.0" }
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2592,6 +2592,13 @@ packages:
|
|||||||
}
|
}
|
||||||
engines: { node: ">= 6" }
|
engines: { node: ">= 6" }
|
||||||
|
|
||||||
|
form-data@4.0.5:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==,
|
||||||
|
}
|
||||||
|
engines: { node: ">= 6" }
|
||||||
|
|
||||||
formdata-node@4.4.1:
|
formdata-node@4.4.1:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -6566,10 +6573,10 @@ snapshots:
|
|||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
axios@1.7.7:
|
axios@1.13.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.9
|
follow-redirects: 1.15.11
|
||||||
form-data: 4.0.2
|
form-data: 4.0.5
|
||||||
proxy-from-env: 1.1.0
|
proxy-from-env: 1.1.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
@@ -7170,7 +7177,7 @@ snapshots:
|
|||||||
kolorist: 1.8.0
|
kolorist: 1.8.0
|
||||||
read-pkg: 8.1.0
|
read-pkg: 8.1.0
|
||||||
|
|
||||||
follow-redirects@1.15.9: {}
|
follow-redirects@1.15.11: {}
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7186,6 +7193,14 @@ snapshots:
|
|||||||
es-set-tostringtag: 2.1.0
|
es-set-tostringtag: 2.1.0
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
form-data@4.0.5:
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
es-set-tostringtag: 2.1.0
|
||||||
|
hasown: 2.0.2
|
||||||
|
mime-types: 2.1.35
|
||||||
|
|
||||||
formdata-node@4.4.1:
|
formdata-node@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
node-domexception: 1.0.0
|
node-domexception: 1.0.0
|
||||||
|
|||||||
+1
-27
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -9,7 +8,6 @@ except ImportError:
|
|||||||
from mem0.configs.llms.base import BaseLlmConfig
|
from mem0.configs.llms.base import BaseLlmConfig
|
||||||
from mem0.configs.llms.ollama import OllamaConfig
|
from mem0.configs.llms.ollama import OllamaConfig
|
||||||
from mem0.llms.base import LLMBase
|
from mem0.llms.base import LLMBase
|
||||||
from mem0.memory.utils import extract_json
|
|
||||||
|
|
||||||
|
|
||||||
class OllamaLLM(LLMBase):
|
class OllamaLLM(LLMBase):
|
||||||
@@ -63,28 +61,7 @@ class OllamaLLM(LLMBase):
|
|||||||
"tool_calls": [],
|
"tool_calls": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
if isinstance(response, dict):
|
# Ollama doesn't support tool calls in the same way, so we return the content
|
||||||
raw_calls = response.get("message", {}).get("tool_calls") or []
|
|
||||||
else:
|
|
||||||
raw_calls = getattr(response.message, "tool_calls", None) or []
|
|
||||||
|
|
||||||
for tool_call in raw_calls:
|
|
||||||
if isinstance(tool_call, dict):
|
|
||||||
fn = tool_call.get("function", {})
|
|
||||||
name = fn.get("name", "")
|
|
||||||
arguments = fn.get("arguments", {})
|
|
||||||
else:
|
|
||||||
fn = getattr(tool_call, "function", None)
|
|
||||||
name = getattr(fn, "name", "") if fn else ""
|
|
||||||
arguments = getattr(fn, "arguments", {}) if fn else {}
|
|
||||||
|
|
||||||
if isinstance(arguments, str):
|
|
||||||
arguments = json.loads(extract_json(arguments))
|
|
||||||
|
|
||||||
processed_response["tool_calls"].append(
|
|
||||||
{"name": name, "arguments": arguments}
|
|
||||||
)
|
|
||||||
|
|
||||||
return processed_response
|
return processed_response
|
||||||
else:
|
else:
|
||||||
return content
|
return content
|
||||||
@@ -136,8 +113,5 @@ class OllamaLLM(LLMBase):
|
|||||||
# Remove OpenAI-specific parameters that Ollama doesn't support
|
# Remove OpenAI-specific parameters that Ollama doesn't support
|
||||||
params.pop("max_tokens", None) # Ollama uses different parameter names
|
params.pop("max_tokens", None) # Ollama uses different parameter names
|
||||||
|
|
||||||
if tools:
|
|
||||||
params["tools"] = tools
|
|
||||||
|
|
||||||
response = self.client.chat(**params)
|
response = self.client.chat(**params)
|
||||||
return self._parse_response(response, tools)
|
return self._parse_response(response, tools)
|
||||||
|
|||||||
@@ -24,14 +24,18 @@ logging.getLogger("urllib3").setLevel(logging.CRITICAL + 1)
|
|||||||
|
|
||||||
class AnonymousTelemetry:
|
class AnonymousTelemetry:
|
||||||
def __init__(self, vector_store=None):
|
def __init__(self, vector_store=None):
|
||||||
self.posthog = Posthog(project_api_key=PROJECT_API_KEY, host=HOST)
|
if not MEM0_TELEMETRY:
|
||||||
|
self.posthog = None
|
||||||
|
self.user_id = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.posthog = Posthog(project_api_key=PROJECT_API_KEY, host=HOST)
|
||||||
self.user_id = get_or_create_user_id(vector_store)
|
self.user_id = get_or_create_user_id(vector_store)
|
||||||
|
|
||||||
if not MEM0_TELEMETRY:
|
|
||||||
self.posthog.disabled = True
|
|
||||||
|
|
||||||
def capture_event(self, event_name, properties=None, user_email=None):
|
def capture_event(self, event_name, properties=None, user_email=None):
|
||||||
|
if self.posthog is None:
|
||||||
|
return
|
||||||
|
|
||||||
if properties is None:
|
if properties is None:
|
||||||
properties = {}
|
properties = {}
|
||||||
properties = {
|
properties = {
|
||||||
@@ -49,13 +53,17 @@ class AnonymousTelemetry:
|
|||||||
self.posthog.capture(distinct_id=distinct_id, event=event_name, properties=properties)
|
self.posthog.capture(distinct_id=distinct_id, event=event_name, properties=properties)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.posthog.shutdown()
|
if self.posthog is not None:
|
||||||
|
self.posthog.shutdown()
|
||||||
|
|
||||||
|
|
||||||
client_telemetry = AnonymousTelemetry()
|
client_telemetry = AnonymousTelemetry()
|
||||||
|
|
||||||
|
|
||||||
def capture_event(event_name, memory_instance, additional_data=None):
|
def capture_event(event_name, memory_instance, additional_data=None):
|
||||||
|
if not MEM0_TELEMETRY:
|
||||||
|
return
|
||||||
|
|
||||||
oss_telemetry = AnonymousTelemetry(
|
oss_telemetry = AnonymousTelemetry(
|
||||||
vector_store=memory_instance._telemetry_vector_store
|
vector_store=memory_instance._telemetry_vector_store
|
||||||
if hasattr(memory_instance, "_telemetry_vector_store")
|
if hasattr(memory_instance, "_telemetry_vector_store")
|
||||||
@@ -81,6 +89,9 @@ def capture_event(event_name, memory_instance, additional_data=None):
|
|||||||
|
|
||||||
|
|
||||||
def capture_client_event(event_name, instance, additional_data=None):
|
def capture_client_event(event_name, instance, additional_data=None):
|
||||||
|
if not MEM0_TELEMETRY:
|
||||||
|
return
|
||||||
|
|
||||||
event_data = {
|
event_data = {
|
||||||
"function": f"{instance.__class__.__module__}.{instance.__class__.__name__}",
|
"function": f"{instance.__class__.__module__}.{instance.__class__.__name__}",
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mem0ai"
|
name = "mem0ai"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
description = "Long-term memory for AI Agents"
|
description = "Long-term memory for AI Agents"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Mem0", email = "founders@mem0.ai" }
|
{ name = "Mem0", email = "founders@mem0.ai" }
|
||||||
|
|||||||
@@ -32,111 +32,3 @@ def test_generate_response_without_tools(mock_ollama_client):
|
|||||||
model="llama3.1:70b", messages=messages, options={"temperature": 0.7, "num_predict": 100, "top_p": 1.0}
|
model="llama3.1:70b", messages=messages, options={"temperature": 0.7, "num_predict": 100, "top_p": 1.0}
|
||||||
)
|
)
|
||||||
assert response == "I'm doing well, thank you for asking!"
|
assert response == "I'm doing well, thank you for asking!"
|
||||||
|
|
||||||
|
|
||||||
def test_generate_response_with_tools_passes_tools_to_client(mock_ollama_client):
|
|
||||||
"""Tools should be forwarded to ollama client.chat()."""
|
|
||||||
config = OllamaConfig(model="llama3.1:70b", temperature=0.1, max_tokens=100, top_p=1.0)
|
|
||||||
llm = OllamaLLM(config)
|
|
||||||
messages = [{"role": "user", "content": "Extract entities from: Alice works at UCSD"}]
|
|
||||||
tools = [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "extract_entities",
|
|
||||||
"description": "Extract entities",
|
|
||||||
"parameters": {"type": "object", "properties": {"entities": {"type": "array"}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
mock_response = {
|
|
||||||
"message": {
|
|
||||||
"content": "",
|
|
||||||
"tool_calls": [
|
|
||||||
{
|
|
||||||
"function": {
|
|
||||||
"name": "extract_entities",
|
|
||||||
"arguments": {"entities": [{"name": "Alice"}, {"name": "UCSD"}]},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_ollama_client.chat.return_value = mock_response
|
|
||||||
|
|
||||||
response = llm.generate_response(messages, tools=tools)
|
|
||||||
|
|
||||||
# Verify tools were passed to client.chat
|
|
||||||
call_kwargs = mock_ollama_client.chat.call_args
|
|
||||||
assert "tools" in call_kwargs.kwargs or (len(call_kwargs.args) > 0 and "tools" in call_kwargs[1])
|
|
||||||
assert call_kwargs[1]["tools"] == tools
|
|
||||||
|
|
||||||
# Verify tool_calls were parsed correctly
|
|
||||||
assert response["tool_calls"] == [
|
|
||||||
{"name": "extract_entities", "arguments": {"entities": [{"name": "Alice"}, {"name": "UCSD"}]}}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_response_with_tools_no_tool_calls_in_response(mock_ollama_client):
|
|
||||||
"""When model returns content without tool_calls, tool_calls should be empty list."""
|
|
||||||
config = OllamaConfig(model="llama3.1:70b", temperature=0.1, max_tokens=100, top_p=1.0)
|
|
||||||
llm = OllamaLLM(config)
|
|
||||||
messages = [{"role": "user", "content": "Hello"}]
|
|
||||||
tools = [{"type": "function", "function": {"name": "noop", "parameters": {}}}]
|
|
||||||
|
|
||||||
mock_response = {"message": {"content": "I cannot use tools for this.", "tool_calls": []}}
|
|
||||||
mock_ollama_client.chat.return_value = mock_response
|
|
||||||
|
|
||||||
response = llm.generate_response(messages, tools=tools)
|
|
||||||
|
|
||||||
assert response["content"] == "I cannot use tools for this."
|
|
||||||
assert response["tool_calls"] == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_response_with_tools_string_arguments(mock_ollama_client):
|
|
||||||
"""When tool_call arguments come as JSON string, they should be parsed."""
|
|
||||||
config = OllamaConfig(model="llama3.1:70b", temperature=0.1, max_tokens=100, top_p=1.0)
|
|
||||||
llm = OllamaLLM(config)
|
|
||||||
messages = [{"role": "user", "content": "test"}]
|
|
||||||
tools = [{"type": "function", "function": {"name": "test_fn", "parameters": {}}}]
|
|
||||||
|
|
||||||
mock_response = {
|
|
||||||
"message": {
|
|
||||||
"content": "",
|
|
||||||
"tool_calls": [
|
|
||||||
{"function": {"name": "test_fn", "arguments": '{"key": "value"}'}}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_ollama_client.chat.return_value = mock_response
|
|
||||||
|
|
||||||
response = llm.generate_response(messages, tools=tools)
|
|
||||||
|
|
||||||
assert response["tool_calls"] == [{"name": "test_fn", "arguments": {"key": "value"}}]
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_response_with_tools_object_style(mock_ollama_client):
|
|
||||||
"""Test _parse_response with object-style response (non-dict)."""
|
|
||||||
config = OllamaConfig(model="llama3.1:70b")
|
|
||||||
llm = OllamaLLM(config)
|
|
||||||
|
|
||||||
# Simulate object-style response
|
|
||||||
mock_fn = Mock()
|
|
||||||
mock_fn.name = "extract"
|
|
||||||
mock_fn.arguments = {"entities": ["Alice"]}
|
|
||||||
|
|
||||||
mock_tool_call = Mock()
|
|
||||||
mock_tool_call.function = mock_fn
|
|
||||||
|
|
||||||
mock_message = Mock()
|
|
||||||
mock_message.content = ""
|
|
||||||
mock_message.tool_calls = [mock_tool_call]
|
|
||||||
|
|
||||||
mock_response = Mock()
|
|
||||||
mock_response.message = mock_message
|
|
||||||
|
|
||||||
tools = [{"type": "function", "function": {"name": "extract"}}]
|
|
||||||
result = llm._parse_response(mock_response, tools)
|
|
||||||
|
|
||||||
assert result["tool_calls"] == [{"name": "extract", "arguments": {"entities": ["Alice"]}}]
|
|
||||||
|
|||||||
+104
-25
@@ -1,35 +1,114 @@
|
|||||||
import os
|
import threading
|
||||||
from unittest.mock import patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
MEM0_TELEMETRY = os.environ.get("MEM0_TELEMETRY", "True")
|
import mem0.memory.telemetry as telemetry_module
|
||||||
|
|
||||||
if isinstance(MEM0_TELEMETRY, str):
|
|
||||||
MEM0_TELEMETRY = MEM0_TELEMETRY.lower() in ("true", "1", "yes")
|
|
||||||
|
|
||||||
|
|
||||||
def use_telemetry():
|
class TestTelemetryDisabled:
|
||||||
if os.getenv("MEM0_TELEMETRY", "true").lower() == "true":
|
"""Verify PostHog is never instantiated when telemetry is disabled."""
|
||||||
return True
|
|
||||||
return False
|
def test_posthog_not_created_when_disabled(self):
|
||||||
|
"""Posthog() constructor should never be called when MEM0_TELEMETRY=False."""
|
||||||
|
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||||
|
with patch("mem0.memory.telemetry.Posthog") as mock_posthog:
|
||||||
|
at = telemetry_module.AnonymousTelemetry()
|
||||||
|
mock_posthog.assert_not_called()
|
||||||
|
assert at.posthog is None
|
||||||
|
assert at.user_id is None
|
||||||
|
|
||||||
|
def test_capture_event_noop_when_disabled(self):
|
||||||
|
"""capture_event() should return immediately without creating AnonymousTelemetry."""
|
||||||
|
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||||
|
with patch("mem0.memory.telemetry.AnonymousTelemetry") as mock_cls:
|
||||||
|
telemetry_module.capture_event("test.event", MagicMock())
|
||||||
|
mock_cls.assert_not_called()
|
||||||
|
|
||||||
|
def test_capture_client_event_noop_when_disabled(self):
|
||||||
|
"""capture_client_event() should return immediately without calling posthog."""
|
||||||
|
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||||
|
mock_instance = MagicMock()
|
||||||
|
mock_client_telemetry = MagicMock()
|
||||||
|
with patch.object(telemetry_module, "client_telemetry", mock_client_telemetry):
|
||||||
|
telemetry_module.capture_client_event("test.event", mock_instance)
|
||||||
|
mock_client_telemetry.capture_event.assert_not_called()
|
||||||
|
|
||||||
|
def test_instance_capture_event_noop_when_posthog_is_none(self):
|
||||||
|
"""AnonymousTelemetry.capture_event() should be a no-op when posthog is None."""
|
||||||
|
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||||
|
at = telemetry_module.AnonymousTelemetry()
|
||||||
|
at.capture_event("test.event", {"key": "value"}) # should not raise
|
||||||
|
|
||||||
|
def test_close_noop_when_posthog_is_none(self):
|
||||||
|
"""close() should not raise when posthog is None."""
|
||||||
|
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||||
|
at = telemetry_module.AnonymousTelemetry()
|
||||||
|
at.close() # should not raise
|
||||||
|
|
||||||
|
def test_no_threads_spawned_when_disabled(self):
|
||||||
|
"""No consumer threads should be created when telemetry is disabled."""
|
||||||
|
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||||
|
threads_before = threading.active_count()
|
||||||
|
telemetry_module.AnonymousTelemetry()
|
||||||
|
threads_after = threading.active_count()
|
||||||
|
assert threads_after == threads_before
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
class TestTelemetryEnabled:
|
||||||
def reset_env():
|
"""Verify PostHog works normally when telemetry is enabled."""
|
||||||
with patch.dict(os.environ, {}, clear=True):
|
|
||||||
yield
|
def test_posthog_created_when_enabled(self):
|
||||||
|
"""Posthog() should be instantiated when MEM0_TELEMETRY=True."""
|
||||||
|
with patch.object(telemetry_module, "MEM0_TELEMETRY", True):
|
||||||
|
with patch("mem0.memory.telemetry.Posthog") as mock_posthog:
|
||||||
|
with patch("mem0.memory.telemetry.get_or_create_user_id", return_value="test-user"):
|
||||||
|
at = telemetry_module.AnonymousTelemetry()
|
||||||
|
mock_posthog.assert_called_once()
|
||||||
|
assert at.posthog is not None
|
||||||
|
assert at.user_id == "test-user"
|
||||||
|
|
||||||
|
def test_capture_event_sends_when_enabled(self):
|
||||||
|
"""capture_event() should create AnonymousTelemetry and call capture when enabled."""
|
||||||
|
with patch.object(telemetry_module, "MEM0_TELEMETRY", True):
|
||||||
|
with patch("mem0.memory.telemetry.AnonymousTelemetry") as mock_cls:
|
||||||
|
mock_at = MagicMock()
|
||||||
|
mock_cls.return_value = mock_at
|
||||||
|
mock_memory = MagicMock()
|
||||||
|
mock_memory.config.graph_store.config = None
|
||||||
|
mock_memory.api_version = "v1"
|
||||||
|
telemetry_module.capture_event("test.event", mock_memory)
|
||||||
|
mock_at.capture_event.assert_called_once()
|
||||||
|
|
||||||
|
def test_capture_client_event_sends_when_enabled(self):
|
||||||
|
"""capture_client_event() should call client_telemetry.capture_event when enabled."""
|
||||||
|
with patch.object(telemetry_module, "MEM0_TELEMETRY", True):
|
||||||
|
mock_client_telemetry = MagicMock()
|
||||||
|
with patch.object(telemetry_module, "client_telemetry", mock_client_telemetry):
|
||||||
|
mock_instance = MagicMock()
|
||||||
|
mock_instance.user_email = "test@example.com"
|
||||||
|
telemetry_module.capture_client_event("test.event", mock_instance)
|
||||||
|
mock_client_telemetry.capture_event.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_telemetry_enabled():
|
class TestTelemetryEnvVar:
|
||||||
with patch.dict(os.environ, {"MEM0_TELEMETRY": "true"}):
|
"""Verify the MEM0_TELEMETRY env var parsing logic."""
|
||||||
assert use_telemetry() is True
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
def test_telemetry_disabled():
|
"value,expected",
|
||||||
with patch.dict(os.environ, {"MEM0_TELEMETRY": "false"}):
|
[
|
||||||
assert use_telemetry() is False
|
("true", True),
|
||||||
|
("True", True),
|
||||||
|
("TRUE", True),
|
||||||
def test_telemetry_default_enabled():
|
("1", True),
|
||||||
assert use_telemetry() is True
|
("yes", True),
|
||||||
|
("false", False),
|
||||||
|
("False", False),
|
||||||
|
("0", False),
|
||||||
|
("no", False),
|
||||||
|
("anything_else", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_env_var_parsing(self, value, expected):
|
||||||
|
result = value.lower() in ("true", "1", "yes")
|
||||||
|
assert result == expected
|
||||||
|
|||||||
Reference in New Issue
Block a user