@reconcrap/people-network-memory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +476 -0
- package/docs/mcp_tools.md +138 -0
- package/harness_adapters/openclaw/mcp.managed.unix.template.json +25 -0
- package/harness_adapters/openclaw/mcp.managed.windows.template.json +26 -0
- package/harness_adapters/openclaw/mcp.template.json +14 -0
- package/harness_adapters/openclaw/ppl/SKILL.md +114 -0
- package/package.json +30 -0
- package/pyproject.toml +26 -0
- package/scripts/install_windows.ps1 +92 -0
- package/scripts/npm/people-memory.js +276 -0
- package/scripts/people_memory_bootstrap.py +247 -0
- package/scripts/run_graphiti_live_from_liepin.ps1 +87 -0
- package/scripts/run_tests_with_artifacts.ps1 +307 -0
- package/src/people_network_memory/__init__.py +6 -0
- package/src/people_network_memory/application/__init__.py +16 -0
- package/src/people_network_memory/application/normalization.py +1441 -0
- package/src/people_network_memory/application/services.py +921 -0
- package/src/people_network_memory/cli.py +1212 -0
- package/src/people_network_memory/config.py +268 -0
- package/src/people_network_memory/domain/__init__.py +55 -0
- package/src/people_network_memory/domain/identity.py +77 -0
- package/src/people_network_memory/domain/models.py +355 -0
- package/src/people_network_memory/fixtures/__init__.py +6 -0
- package/src/people_network_memory/fixtures/eval.py +398 -0
- package/src/people_network_memory/fixtures/extractor_eval.py +364 -0
- package/src/people_network_memory/fixtures/generator.py +290 -0
- package/src/people_network_memory/fixtures/report.py +252 -0
- package/src/people_network_memory/graphiti_adapter/__init__.py +9 -0
- package/src/people_network_memory/graphiti_adapter/episode_formatter.py +70 -0
- package/src/people_network_memory/graphiti_adapter/graphiti_store.py +655 -0
- package/src/people_network_memory/graphiti_adapter/indexer.py +194 -0
- package/src/people_network_memory/graphiti_adapter/ontology.py +68 -0
- package/src/people_network_memory/harness_adapters/__init__.py +2 -0
- package/src/people_network_memory/harness_adapters/openclaw/__init__.py +9 -0
- package/src/people_network_memory/harness_adapters/openclaw/installer.py +577 -0
- package/src/people_network_memory/harness_adapters/openclaw/integration_eval.py +508 -0
- package/src/people_network_memory/harness_adapters/openclaw/smoke.py +292 -0
- package/src/people_network_memory/infrastructure/__init__.py +2 -0
- package/src/people_network_memory/infrastructure/archive_backup.py +171 -0
- package/src/people_network_memory/infrastructure/diagnostics.py +171 -0
- package/src/people_network_memory/infrastructure/embeddings.py +155 -0
- package/src/people_network_memory/infrastructure/file_store.py +129 -0
- package/src/people_network_memory/infrastructure/graphiti_promotion.py +212 -0
- package/src/people_network_memory/infrastructure/id_generator.py +40 -0
- package/src/people_network_memory/infrastructure/in_memory_store.py +1008 -0
- package/src/people_network_memory/infrastructure/llm_extractor.py +476 -0
- package/src/people_network_memory/infrastructure/llm_identity_advisor.py +200 -0
- package/src/people_network_memory/infrastructure/llm_judge.py +162 -0
- package/src/people_network_memory/infrastructure/redaction.py +21 -0
- package/src/people_network_memory/infrastructure/release_check.py +186 -0
- package/src/people_network_memory/infrastructure/retrieval_intent.py +98 -0
- package/src/people_network_memory/infrastructure/semantic_index.py +262 -0
- package/src/people_network_memory/mcp_server/__init__.py +2 -0
- package/src/people_network_memory/mcp_server/contracts.py +85 -0
- package/src/people_network_memory/mcp_server/runtime.py +133 -0
- package/src/people_network_memory/mcp_server/tools.py +588 -0
- package/src/people_network_memory/ports/__init__.py +2 -0
- package/src/people_network_memory/ports/errors.py +25 -0
- package/src/people_network_memory/ports/interfaces.py +103 -0
- package/src/people_network_memory/projection/__init__.py +6 -0
- package/src/people_network_memory/projection/builders.py +46 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Embedding clients for local and OpenAI-compatible providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from people_network_memory.config import PeopleMemoryConfig
|
|
11
|
+
from people_network_memory.ports.errors import ConfigError, PeopleMemoryError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EmbeddingError(PeopleMemoryError):
|
|
15
|
+
"""Embedding provider request failed."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class EmbeddingCheck:
|
|
20
|
+
ok: bool
|
|
21
|
+
provider: str
|
|
22
|
+
model: str
|
|
23
|
+
base_url: str
|
|
24
|
+
dimension: int | None = None
|
|
25
|
+
error: str | None = None
|
|
26
|
+
|
|
27
|
+
def to_json(self) -> dict[str, object]:
|
|
28
|
+
payload: dict[str, object] = {
|
|
29
|
+
"ok": self.ok,
|
|
30
|
+
"provider": self.provider,
|
|
31
|
+
"model": self.model,
|
|
32
|
+
"base_url": self.base_url,
|
|
33
|
+
}
|
|
34
|
+
if self.dimension is not None:
|
|
35
|
+
payload["dimension"] = self.dimension
|
|
36
|
+
if self.error:
|
|
37
|
+
payload["error"] = self.error
|
|
38
|
+
return payload
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class EmbeddingSettings:
|
|
43
|
+
provider: str
|
|
44
|
+
base_url: str
|
|
45
|
+
model: str
|
|
46
|
+
api_key: str
|
|
47
|
+
dimension: int | None = None
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_config(cls, config: PeopleMemoryConfig) -> "EmbeddingSettings":
|
|
51
|
+
provider = (config.embedding_provider or "").strip().lower()
|
|
52
|
+
if provider in {"ollama", "local_ollama"}:
|
|
53
|
+
return cls(
|
|
54
|
+
provider="ollama",
|
|
55
|
+
base_url=config.embedding_base_url or "http://localhost:11434/v1",
|
|
56
|
+
model=config.embedding_model or "nomic-embed-text",
|
|
57
|
+
api_key=config.embedding_api_key or "ollama",
|
|
58
|
+
dimension=config.embedding_dim or 768,
|
|
59
|
+
)
|
|
60
|
+
if provider in {"openai_compatible", "ark", "doubao", "volcengine"}:
|
|
61
|
+
missing = []
|
|
62
|
+
if not config.embedding_base_url:
|
|
63
|
+
missing.append("PEOPLE_MEMORY_EMBEDDING_BASE_URL")
|
|
64
|
+
if not config.embedding_model:
|
|
65
|
+
missing.append("PEOPLE_MEMORY_EMBEDDING_MODEL")
|
|
66
|
+
if not config.embedding_api_key:
|
|
67
|
+
missing.append("PEOPLE_MEMORY_EMBEDDING_API_KEY")
|
|
68
|
+
if missing:
|
|
69
|
+
raise ConfigError(
|
|
70
|
+
"OpenAI-compatible embeddings require: " + ", ".join(missing)
|
|
71
|
+
)
|
|
72
|
+
return cls(
|
|
73
|
+
provider="openai_compatible" if provider in {"ark", "doubao", "volcengine"} else provider,
|
|
74
|
+
base_url=config.embedding_base_url,
|
|
75
|
+
model=config.embedding_model,
|
|
76
|
+
api_key=config.embedding_api_key,
|
|
77
|
+
dimension=config.embedding_dim,
|
|
78
|
+
)
|
|
79
|
+
raise ConfigError(
|
|
80
|
+
"Set PEOPLE_MEMORY_EMBEDDING_PROVIDER to `ollama`, `openai_compatible`, "
|
|
81
|
+
"or a supported OpenAI-compatible alias such as `volcengine`."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class OpenAICompatibleEmbeddingClient:
|
|
86
|
+
def __init__(self, settings: EmbeddingSettings, *, timeout_seconds: float = 60.0) -> None:
|
|
87
|
+
self._settings = settings
|
|
88
|
+
self._timeout = timeout_seconds
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def settings(self) -> EmbeddingSettings:
|
|
92
|
+
return self._settings
|
|
93
|
+
|
|
94
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
95
|
+
if not texts:
|
|
96
|
+
return []
|
|
97
|
+
url = self._settings.base_url.rstrip("/") + "/embeddings"
|
|
98
|
+
headers = {
|
|
99
|
+
"Authorization": f"Bearer {self._settings.api_key}",
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
}
|
|
102
|
+
payload = {"model": self._settings.model, "input": texts}
|
|
103
|
+
try:
|
|
104
|
+
response = httpx.post(url, headers=headers, json=payload, timeout=self._timeout)
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
except httpx.HTTPStatusError as exc:
|
|
107
|
+
detail = _safe_error_detail(exc.response)
|
|
108
|
+
raise EmbeddingError(f"embedding provider returned HTTP {exc.response.status_code}: {detail}") from exc
|
|
109
|
+
except httpx.HTTPError as exc:
|
|
110
|
+
raise EmbeddingError(f"embedding provider request failed: {exc}") from exc
|
|
111
|
+
data = response.json()
|
|
112
|
+
return _parse_embeddings(data)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_embedding_provider(config: PeopleMemoryConfig, sample: str) -> EmbeddingCheck:
|
|
116
|
+
settings = EmbeddingSettings.from_config(config)
|
|
117
|
+
client = OpenAICompatibleEmbeddingClient(settings)
|
|
118
|
+
try:
|
|
119
|
+
vectors = client.embed([sample])
|
|
120
|
+
dimension = len(vectors[0]) if vectors else 0
|
|
121
|
+
return EmbeddingCheck(
|
|
122
|
+
ok=dimension > 0,
|
|
123
|
+
provider=settings.provider,
|
|
124
|
+
model=settings.model,
|
|
125
|
+
base_url=settings.base_url,
|
|
126
|
+
dimension=dimension,
|
|
127
|
+
)
|
|
128
|
+
except PeopleMemoryError as exc:
|
|
129
|
+
return EmbeddingCheck(
|
|
130
|
+
ok=False,
|
|
131
|
+
provider=settings.provider,
|
|
132
|
+
model=settings.model,
|
|
133
|
+
base_url=settings.base_url,
|
|
134
|
+
error=str(exc),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_embeddings(data: dict[str, Any]) -> list[list[float]]:
|
|
139
|
+
raw_items = data.get("data")
|
|
140
|
+
if not isinstance(raw_items, list):
|
|
141
|
+
raise EmbeddingError("embedding provider response missing `data` list")
|
|
142
|
+
vectors: list[list[float]] = []
|
|
143
|
+
for item in raw_items:
|
|
144
|
+
embedding = item.get("embedding") if isinstance(item, dict) else None
|
|
145
|
+
if not isinstance(embedding, list):
|
|
146
|
+
raise EmbeddingError("embedding provider response item missing `embedding` list")
|
|
147
|
+
vectors.append([float(value) for value in embedding])
|
|
148
|
+
return vectors
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _safe_error_detail(response: httpx.Response) -> str:
|
|
152
|
+
text = response.text
|
|
153
|
+
if len(text) > 500:
|
|
154
|
+
text = text[:500] + "..."
|
|
155
|
+
return text
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Durable local JSON adapter.
|
|
2
|
+
|
|
3
|
+
This is a pragmatic local-first backend for development and early use while the
|
|
4
|
+
Graphiti spike remains gated. It preserves the same ports as the in-memory
|
|
5
|
+
adapter and keeps the Graphiti boundary untouched.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from people_network_memory.config import PeopleMemoryConfig
|
|
15
|
+
from people_network_memory.domain.models import (
|
|
16
|
+
PersonMemoryRecord,
|
|
17
|
+
ReviewItem,
|
|
18
|
+
SocialInteraction,
|
|
19
|
+
)
|
|
20
|
+
from people_network_memory.infrastructure.in_memory_store import InMemoryPeopleStore
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JsonPeopleStore(InMemoryPeopleStore):
|
|
24
|
+
def __init__(self, path: Path) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self._path = path
|
|
27
|
+
self._load()
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_config(cls, config: PeopleMemoryConfig) -> "JsonPeopleStore":
|
|
31
|
+
return cls(local_json_path(config))
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def path(self) -> Path:
|
|
35
|
+
return self._path
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def validate_export_payload(cls, payload: dict[str, Any]) -> dict[str, Any]:
|
|
39
|
+
people = [
|
|
40
|
+
PersonMemoryRecord.model_validate(item).model_dump(mode="json")
|
|
41
|
+
for item in payload.get("people", [])
|
|
42
|
+
]
|
|
43
|
+
review_items = [
|
|
44
|
+
ReviewItem.model_validate(item).model_dump(mode="json")
|
|
45
|
+
for item in payload.get("review_items", [])
|
|
46
|
+
]
|
|
47
|
+
interactions = [
|
|
48
|
+
SocialInteraction.model_validate(item).model_dump(mode="json")
|
|
49
|
+
for item in payload.get("interactions", [])
|
|
50
|
+
]
|
|
51
|
+
return {
|
|
52
|
+
"people": people,
|
|
53
|
+
"review_items": review_items,
|
|
54
|
+
"interactions": interactions,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def restore_to_path(cls, path: Path, payload: dict[str, Any]) -> None:
|
|
59
|
+
validated = cls.validate_export_payload(payload)
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
path.write_text(
|
|
62
|
+
json.dumps(validated, ensure_ascii=False, indent=2, sort_keys=True),
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def save_interaction(self, interaction: SocialInteraction, identity_map: dict[str, str | None]):
|
|
67
|
+
result = super().save_interaction(interaction, identity_map)
|
|
68
|
+
self._flush()
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
def add_review_item(self, item: ReviewItem) -> None:
|
|
72
|
+
super().add_review_item(item)
|
|
73
|
+
self._flush()
|
|
74
|
+
|
|
75
|
+
def update_review_item(self, item: ReviewItem) -> ReviewItem:
|
|
76
|
+
updated = super().update_review_item(item)
|
|
77
|
+
self._flush()
|
|
78
|
+
return updated
|
|
79
|
+
|
|
80
|
+
def merge_people(
|
|
81
|
+
self, *, source_person_id: str, target_person_id: str, note: str | None = None
|
|
82
|
+
) -> PersonMemoryRecord:
|
|
83
|
+
record = super().merge_people(
|
|
84
|
+
source_person_id=source_person_id,
|
|
85
|
+
target_person_id=target_person_id,
|
|
86
|
+
note=note,
|
|
87
|
+
)
|
|
88
|
+
self._flush()
|
|
89
|
+
return record
|
|
90
|
+
|
|
91
|
+
def _load(self) -> None:
|
|
92
|
+
if not self._path.exists():
|
|
93
|
+
return
|
|
94
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
95
|
+
self.people = {
|
|
96
|
+
item["person_id"]: PersonMemoryRecord.model_validate(item)
|
|
97
|
+
for item in data.get("people", [])
|
|
98
|
+
}
|
|
99
|
+
self.review_items = [
|
|
100
|
+
ReviewItem.model_validate(item) for item in data.get("review_items", [])
|
|
101
|
+
]
|
|
102
|
+
self.interactions = {
|
|
103
|
+
f"interaction_{index:04d}": SocialInteraction.model_validate(item)
|
|
104
|
+
for index, item in enumerate(data.get("interactions", []), start=1)
|
|
105
|
+
}
|
|
106
|
+
self.evidence = {
|
|
107
|
+
evidence.evidence_id: evidence
|
|
108
|
+
for person in self.people.values()
|
|
109
|
+
for evidence in person.evidence
|
|
110
|
+
}
|
|
111
|
+
self._reserve_loaded_ids()
|
|
112
|
+
|
|
113
|
+
def _reserve_loaded_ids(self) -> None:
|
|
114
|
+
self._ids.reserve_existing_ids("person", self.people)
|
|
115
|
+
self._ids.reserve_existing_ids("review", [item.review_id for item in self.review_items])
|
|
116
|
+
self._ids.reserve_existing_ids("evidence", self.evidence)
|
|
117
|
+
self._ids.reserve_existing_ids("interaction", self.interactions)
|
|
118
|
+
self._ids.reserve_next_value("interaction", len(self.interactions) + 1)
|
|
119
|
+
|
|
120
|
+
def _flush(self) -> None:
|
|
121
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
self._path.write_text(
|
|
123
|
+
json.dumps(self.export_data(), ensure_ascii=False, indent=2, sort_keys=True),
|
|
124
|
+
encoding="utf-8",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def local_json_path(config: PeopleMemoryConfig) -> Path:
|
|
129
|
+
return Path(config.data_path).expanduser() / "people-memory.json"
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Promotion gate for making Graphiti/Kuzu the recommended backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from people_network_memory.config import PeopleMemoryConfig
|
|
11
|
+
from people_network_memory.fixtures.eval import evaluate_services
|
|
12
|
+
from people_network_memory.fixtures.generator import generate_mock_dataset
|
|
13
|
+
from people_network_memory.infrastructure.diagnostics import graphiti_spike_checks
|
|
14
|
+
from people_network_memory.infrastructure.embeddings import check_embedding_provider
|
|
15
|
+
from people_network_memory.ports.errors import PeopleMemoryError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_graphiti_promotion_gate(
|
|
19
|
+
config: PeopleMemoryConfig,
|
|
20
|
+
*,
|
|
21
|
+
seed: int = 42,
|
|
22
|
+
max_interactions: int | None = None,
|
|
23
|
+
max_queries: int | None = None,
|
|
24
|
+
isolated: bool = True,
|
|
25
|
+
include_cases: bool = False,
|
|
26
|
+
failures_only: bool = True,
|
|
27
|
+
skip_live: bool = False,
|
|
28
|
+
skip_embedding_check: bool = False,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
"""Run the repeatable evidence gate for Graphiti/Kuzu promotion.
|
|
31
|
+
|
|
32
|
+
The gate is intentionally stricter than a smoke test: static checks must
|
|
33
|
+
pass, live embedding must work unless skipped, and the fixture eval must
|
|
34
|
+
meet V1 recall/evidence/sensitivity thresholds before promotion is green.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if config.backend != "graphiti":
|
|
38
|
+
config = replace(config, backend="graphiti")
|
|
39
|
+
|
|
40
|
+
static_checks = [check.to_json() for check in graphiti_spike_checks(config)]
|
|
41
|
+
static_ok = all(bool(check["ok"]) for check in static_checks)
|
|
42
|
+
embedding_payload = _embedding_gate(config, skip=skip_embedding_check)
|
|
43
|
+
eval_payload: dict[str, Any]
|
|
44
|
+
|
|
45
|
+
if skip_live:
|
|
46
|
+
eval_payload = {
|
|
47
|
+
"ok": False,
|
|
48
|
+
"skipped": True,
|
|
49
|
+
"reason": "live Graphiti/Kuzu fixture eval was skipped",
|
|
50
|
+
}
|
|
51
|
+
elif not static_ok:
|
|
52
|
+
eval_payload = {
|
|
53
|
+
"ok": False,
|
|
54
|
+
"skipped": True,
|
|
55
|
+
"reason": "static Graphiti/Kuzu checks did not pass",
|
|
56
|
+
}
|
|
57
|
+
elif not embedding_payload["ok"]:
|
|
58
|
+
eval_payload = {
|
|
59
|
+
"ok": False,
|
|
60
|
+
"skipped": True,
|
|
61
|
+
"reason": "embedding provider check did not pass",
|
|
62
|
+
}
|
|
63
|
+
else:
|
|
64
|
+
eval_payload = _run_live_eval(
|
|
65
|
+
config,
|
|
66
|
+
seed=seed,
|
|
67
|
+
max_interactions=max_interactions,
|
|
68
|
+
max_queries=max_queries,
|
|
69
|
+
isolated=isolated,
|
|
70
|
+
include_cases=include_cases,
|
|
71
|
+
failures_only=failures_only,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
checked = int(eval_payload.get("checked", 0) or 0)
|
|
75
|
+
ingested = int(eval_payload.get("ingested_interactions", 0) or 0)
|
|
76
|
+
fixture_summary = eval_payload.get("fixture_summary", {})
|
|
77
|
+
target_queries = (
|
|
78
|
+
int(fixture_summary.get("eval_queries", 40))
|
|
79
|
+
if isinstance(fixture_summary, dict)
|
|
80
|
+
else 40
|
|
81
|
+
)
|
|
82
|
+
target_interactions = (
|
|
83
|
+
int(fixture_summary.get("interactions", 90))
|
|
84
|
+
if isinstance(fixture_summary, dict)
|
|
85
|
+
else 90
|
|
86
|
+
)
|
|
87
|
+
bounded_run = max_queries is not None or max_interactions is not None
|
|
88
|
+
full_eval = not bounded_run and checked >= min(40, target_queries)
|
|
89
|
+
full_ingest = not bounded_run and ingested >= min(60, target_interactions)
|
|
90
|
+
promotion_ok = (
|
|
91
|
+
static_ok
|
|
92
|
+
and bool(embedding_payload["ok"])
|
|
93
|
+
and bool(eval_payload.get("passes_v1_thresholds"))
|
|
94
|
+
and full_eval
|
|
95
|
+
and full_ingest
|
|
96
|
+
)
|
|
97
|
+
return {
|
|
98
|
+
"ok": promotion_ok,
|
|
99
|
+
"gate": "graphiti_kuzu_promotion",
|
|
100
|
+
"current_default_backend": "local_json",
|
|
101
|
+
"candidate_backend": "graphiti",
|
|
102
|
+
"graph_backend_kind": config.graph_backend_kind,
|
|
103
|
+
"isolated": isolated,
|
|
104
|
+
"bounded_run": bounded_run,
|
|
105
|
+
"seed": seed,
|
|
106
|
+
"checked": checked,
|
|
107
|
+
"passes_v1_thresholds": bool(eval_payload.get("passes_v1_thresholds")),
|
|
108
|
+
"static_checks": static_checks,
|
|
109
|
+
"embedding_check": embedding_payload,
|
|
110
|
+
"fixture_eval": eval_payload,
|
|
111
|
+
"promotion_requirements": {
|
|
112
|
+
"static_checks": "all pass",
|
|
113
|
+
"embedding_check": "pass",
|
|
114
|
+
"recall_at_3": ">= 0.70",
|
|
115
|
+
"recall_at_5": ">= 0.85",
|
|
116
|
+
"returned_result_evidence_rate": "1.0",
|
|
117
|
+
"sensitive_leaks": "0",
|
|
118
|
+
"full_eval": ">= 40 checked queries; bounded diagnostic runs cannot promote",
|
|
119
|
+
"full_ingest": ">= 60 ingested interactions; bounded diagnostic runs cannot promote",
|
|
120
|
+
},
|
|
121
|
+
"promotion_status": _promotion_status(
|
|
122
|
+
promotion_ok=promotion_ok,
|
|
123
|
+
bounded_run=bounded_run,
|
|
124
|
+
eval_passed=bool(eval_payload.get("passes_v1_thresholds")),
|
|
125
|
+
),
|
|
126
|
+
"next_step": (
|
|
127
|
+
"Graphiti/Kuzu can be recommended for advanced recall while local_json remains the export fallback."
|
|
128
|
+
if promotion_ok
|
|
129
|
+
else "Bounded Graphiti/Kuzu diagnostic passed; run the unbounded gate before promotion."
|
|
130
|
+
if bounded_run and bool(eval_payload.get("passes_v1_thresholds"))
|
|
131
|
+
else "Keep local_json as the default and fix failed gate items before promoting Graphiti/Kuzu."
|
|
132
|
+
),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _promotion_status(
|
|
137
|
+
*, promotion_ok: bool, bounded_run: bool, eval_passed: bool
|
|
138
|
+
) -> str:
|
|
139
|
+
if promotion_ok:
|
|
140
|
+
return "ready_to_recommend"
|
|
141
|
+
if bounded_run and eval_passed:
|
|
142
|
+
return "bounded_pass_not_promotable"
|
|
143
|
+
return "keep_gated"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _embedding_gate(config: PeopleMemoryConfig, *, skip: bool) -> dict[str, Any]:
|
|
147
|
+
if skip:
|
|
148
|
+
return {
|
|
149
|
+
"ok": False,
|
|
150
|
+
"skipped": True,
|
|
151
|
+
"reason": "embedding provider check was skipped",
|
|
152
|
+
}
|
|
153
|
+
try:
|
|
154
|
+
return check_embedding_provider(config, "Alice likes robotics and coffee.").to_json()
|
|
155
|
+
except PeopleMemoryError as exc:
|
|
156
|
+
return {"ok": False, "error": str(exc)}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _run_live_eval(
|
|
160
|
+
config: PeopleMemoryConfig,
|
|
161
|
+
*,
|
|
162
|
+
seed: int,
|
|
163
|
+
max_interactions: int | None,
|
|
164
|
+
max_queries: int | None,
|
|
165
|
+
isolated: bool,
|
|
166
|
+
include_cases: bool,
|
|
167
|
+
failures_only: bool,
|
|
168
|
+
) -> dict[str, Any]:
|
|
169
|
+
from people_network_memory.mcp_server.runtime import build_runtime
|
|
170
|
+
|
|
171
|
+
temp_dir: tempfile.TemporaryDirectory[str] | None = None
|
|
172
|
+
if isolated:
|
|
173
|
+
temp_dir = tempfile.TemporaryDirectory()
|
|
174
|
+
config = replace(
|
|
175
|
+
config,
|
|
176
|
+
data_path=temp_dir.name,
|
|
177
|
+
graphiti_kuzu_path=str(Path(temp_dir.name) / "graphiti.kuzu"),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
runtime = None
|
|
181
|
+
try:
|
|
182
|
+
dataset = generate_mock_dataset(seed=seed)
|
|
183
|
+
runtime = build_runtime(config)
|
|
184
|
+
result = evaluate_services(
|
|
185
|
+
dataset,
|
|
186
|
+
record_service=runtime.record_service,
|
|
187
|
+
retrieve_service=runtime.retrieve_service,
|
|
188
|
+
max_interactions=max_interactions,
|
|
189
|
+
max_queries=max_queries,
|
|
190
|
+
only_answerable=True,
|
|
191
|
+
)
|
|
192
|
+
payload = result.to_json(
|
|
193
|
+
include_cases=include_cases or failures_only,
|
|
194
|
+
failures_only=failures_only,
|
|
195
|
+
)
|
|
196
|
+
payload.update(
|
|
197
|
+
{
|
|
198
|
+
"ok": bool(payload["passes_v1_thresholds"]),
|
|
199
|
+
"backend": "graphiti",
|
|
200
|
+
"fixture_summary": dataset.summary(),
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
return payload
|
|
204
|
+
except PeopleMemoryError as exc:
|
|
205
|
+
return {"ok": False, "error": str(exc)}
|
|
206
|
+
except Exception as exc: # pragma: no cover - live provider defensive path
|
|
207
|
+
return {"ok": False, "error": f"Unexpected Graphiti gate failure: {exc}"}
|
|
208
|
+
finally:
|
|
209
|
+
if runtime is not None:
|
|
210
|
+
runtime.close()
|
|
211
|
+
if temp_dir is not None:
|
|
212
|
+
temp_dir.cleanup()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Deterministic and random ID generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ID_PATTERN = re.compile(r"^(?P<prefix>.+)_(?P<number>\d+)$")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SequentialIdGenerator:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._next_values: dict[str, int] = {}
|
|
16
|
+
|
|
17
|
+
def new_id(self, prefix: str) -> str:
|
|
18
|
+
value = self._next_values.get(prefix, 1)
|
|
19
|
+
self._next_values[prefix] = value + 1
|
|
20
|
+
return f"{prefix}_{value:04d}"
|
|
21
|
+
|
|
22
|
+
def reserve_existing_ids(self, prefix: str, ids: Iterable[str]) -> None:
|
|
23
|
+
max_value = 0
|
|
24
|
+
for item_id in ids:
|
|
25
|
+
match = ID_PATTERN.match(item_id)
|
|
26
|
+
if not match or match.group("prefix") != prefix:
|
|
27
|
+
continue
|
|
28
|
+
max_value = max(max_value, int(match.group("number")))
|
|
29
|
+
if max_value:
|
|
30
|
+
self.reserve_next_value(prefix, max_value + 1)
|
|
31
|
+
|
|
32
|
+
def reserve_next_value(self, prefix: str, next_value: int) -> None:
|
|
33
|
+
if next_value < 1:
|
|
34
|
+
return
|
|
35
|
+
self._next_values[prefix] = max(self._next_values.get(prefix, 1), next_value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UuidGenerator:
|
|
39
|
+
def new_id(self, prefix: str) -> str:
|
|
40
|
+
return f"{prefix}_{uuid.uuid4().hex[:12]}"
|