memuron 0.1.1__py3-none-any.whl
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.
- memuron/__init__.py +3 -0
- memuron/actions/__init__.py +12 -0
- memuron/actions/context.py +63 -0
- memuron/actions/helpers.py +88 -0
- memuron/actions/memory.py +340 -0
- memuron/actions/memory_write.py +290 -0
- memuron/actions/nodes.py +340 -0
- memuron/actions/registry.py +5 -0
- memuron/actions/runtime.py +37 -0
- memuron/actions/spaces_documents.py +720 -0
- memuron/actions/sync.py +155 -0
- memuron/application/__init__.py +1 -0
- memuron/application/api.py +206 -0
- memuron/application/app.py +103 -0
- memuron/application/capabilities.py +82 -0
- memuron/application/cli.py +35 -0
- memuron/application/config.py +176 -0
- memuron/application/mcp.py +44 -0
- memuron/application/mcp_oauth.py +290 -0
- memuron/application/registry.py +52 -0
- memuron/context.py +532 -0
- memuron/documents/__init__.py +1 -0
- memuron/documents/link_guardian.py +192 -0
- memuron/documents/linking.py +292 -0
- memuron/documents/parser.py +1152 -0
- memuron/documents/storage.py +151 -0
- memuron/documents/url_ingest.py +375 -0
- memuron/domain/__init__.py +1 -0
- memuron/domain/decoders.py +1 -0
- memuron/domain/encoders.py +185 -0
- memuron/domain/lifecycles.py +8 -0
- memuron/domain/limits.py +6 -0
- memuron/domain/representations.py +56 -0
- memuron/domain/schemas.py +581 -0
- memuron/domain/scope_filter.py +104 -0
- memuron/graphfs/__init__.py +1 -0
- memuron/graphfs/manual.py +635 -0
- memuron/graphfs/projection.py +578 -0
- memuron/graphfs/query.py +1782 -0
- memuron/graphfs/read_model.py +574 -0
- memuron/ingest/__init__.py +1 -0
- memuron/ingest/guardian.py +213 -0
- memuron/ingest/jobs.py +424 -0
- memuron/ingest/prompts.py +147 -0
- memuron/memory/__init__.py +1 -0
- memuron/memory/engine.py +35 -0
- memuron/memory/projections.py +452 -0
- memuron/memory/recipes.py +3247 -0
- memuron/persistence/__init__.py +1 -0
- memuron/persistence/db_pool.py +57 -0
- memuron/persistence/identity_store.py +918 -0
- memuron/persistence/store_helpers.py +16 -0
- memuron/search/__init__.py +1 -0
- memuron/search/fulltext.py +110 -0
- memuron/search/hybrid.py +284 -0
- memuron/search/pgvector.py +252 -0
- memuron/security/__init__.py +1 -0
- memuron/security/auth.py +143 -0
- memuron/security/auth_provider.py +119 -0
- memuron/security/authorization.py +53 -0
- memuron/security/clerk_scopes.py +94 -0
- memuron/security/clerk_webhooks.py +61 -0
- memuron/security/jwt_tokens.py +53 -0
- memuron/security/passwords.py +38 -0
- memuron/security/tenant.py +58 -0
- memuron/spaces/__init__.py +1 -0
- memuron/spaces/model.py +35 -0
- memuron/spaces/service.py +155 -0
- memuron/sync/__init__.py +25 -0
- memuron/sync/folder.py +828 -0
- memuron-0.1.1.dist-info/METADATA +242 -0
- memuron-0.1.1.dist-info/RECORD +74 -0
- memuron-0.1.1.dist-info/WHEEL +4 -0
- memuron-0.1.1.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Guardian prompts and structured output schemas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
PROMPT_VERSION = "3.1"
|
|
10
|
+
|
|
11
|
+
SPACE_GUARDIAN_SECTION = """
|
|
12
|
+
SPACE CONTEXT
|
|
13
|
+
Active space: {active_space_name} ({active_space_token})
|
|
14
|
+
Space contract (what belongs here):
|
|
15
|
+
{active_space_prompt}
|
|
16
|
+
|
|
17
|
+
Other registered spaces (one-line summaries):
|
|
18
|
+
{other_spaces_summary}
|
|
19
|
+
|
|
20
|
+
Registered space tokens (pick from this list only):
|
|
21
|
+
{registered_tokens}
|
|
22
|
+
|
|
23
|
+
All registered spaces (name, token, contract):
|
|
24
|
+
{all_spaces_detail}
|
|
25
|
+
|
|
26
|
+
SPACE RULES — mode: {space_mode}
|
|
27
|
+
- Each memory belongs to exactly ONE `space.*` token unless the user content explicitly requires two (rare). Default: pick the single best-matching space.
|
|
28
|
+
- STRICT (user explicitly picked a space): MUST use `{active_space_token}` as the only space token unless the user text clearly spans two spaces.
|
|
29
|
+
- ASSIST (session default / no explicit pick): Choose the single best enabled `space.*` token. The hint space is a default only — route elsewhere when the contract fits better.
|
|
30
|
+
- Never tag all enabled spaces on one memory. Never invent tokens outside the registered list.
|
|
31
|
+
- Apply the active space contract for linking style when the memory belongs there.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
MEMORY_WRITE_GUARDIAN_PROMPT = """You are the Guardian of the memory system.
|
|
35
|
+
|
|
36
|
+
Produce a COMPLETE write plan for one new memory input in a single pass.
|
|
37
|
+
Return ONLY valid JSON. No markdown. No extra commentary.
|
|
38
|
+
|
|
39
|
+
INPUT
|
|
40
|
+
New content:
|
|
41
|
+
{new_content}
|
|
42
|
+
|
|
43
|
+
Candidate memories (with semantic similarity scores):
|
|
44
|
+
{candidates_text}
|
|
45
|
+
|
|
46
|
+
If there are no candidates, treat this as an empty list.
|
|
47
|
+
|
|
48
|
+
DECISION RULES
|
|
49
|
+
- Choose `update` only if the new content is about the SAME specific memory and mainly expands, corrects, or refines it.
|
|
50
|
+
- Choose `create` if the new content is a distinct memory, even if it is related.
|
|
51
|
+
- NEVER choose `update` when the target candidate is an image, document, or collection node (`node_type` ≠ text). Link to those nodes instead; text ingest must not rewrite media/collection records.
|
|
52
|
+
- Prefer granular memories over monolithic ones.
|
|
53
|
+
- Similarity scores are hints only.
|
|
54
|
+
|
|
55
|
+
MEMORY SHAPING RULES
|
|
56
|
+
- `final_content` should be concise, coherent, natural, and information-dense.
|
|
57
|
+
- If `action` is `update`, `final_content` must already be the merged final memory text.
|
|
58
|
+
- If information conflicts and `update` is correct, prefer newer information while keeping the memory coherent.
|
|
59
|
+
- Extract unified `scope` (5-12 strings) from `final_content` for classification and filtering.
|
|
60
|
+
|
|
61
|
+
GRAPH ACTION RULES
|
|
62
|
+
- You may propose up to 2 meaningful graph links to candidate memories.
|
|
63
|
+
- Only link when there is clear causal, same-entity, dependency, continuation, or topic-deepening value.
|
|
64
|
+
- Do not link for shallow token overlap.
|
|
65
|
+
- Each link `description` must be 2-3 natural questions and should contain question marks.
|
|
66
|
+
- You may also propose stale links to remove using `links_to_remove`.
|
|
67
|
+
- Do not propose neighbor scope rewrites in this prompt.
|
|
68
|
+
|
|
69
|
+
JSON SCHEMA
|
|
70
|
+
{{
|
|
71
|
+
"action": "create" | "update",
|
|
72
|
+
"target_memory_id": "candidate id when action=update, otherwise null",
|
|
73
|
+
"reasoning": "brief explanation",
|
|
74
|
+
"final_content": "canonical memory text",
|
|
75
|
+
"scope": ["token1", "token2"],
|
|
76
|
+
"connections": [
|
|
77
|
+
{{"target_id": "candidate id", "description": "2-3 questions connecting the memories?"}}
|
|
78
|
+
],
|
|
79
|
+
"links_to_remove": [["memory_id_1", "memory_id_2"]]
|
|
80
|
+
}}
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ConnectionSpec(BaseModel):
|
|
85
|
+
target_id: str
|
|
86
|
+
description: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class GuardianWritePlan(BaseModel):
|
|
90
|
+
action: Literal["create", "update"]
|
|
91
|
+
target_memory_id: str | None = None
|
|
92
|
+
reasoning: str
|
|
93
|
+
final_content: str
|
|
94
|
+
scope: list[str] = Field(default_factory=list)
|
|
95
|
+
connections: list[ConnectionSpec] = Field(default_factory=list)
|
|
96
|
+
links_to_remove: list[list[str]] = Field(default_factory=list)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
DOCUMENT_LINK_GUARDIAN_PROMPT = """You are the Guardian linking a newly ingested document into an existing memory graph.
|
|
100
|
+
|
|
101
|
+
You receive:
|
|
102
|
+
1. EXTERNAL CANDIDATES — up to 30 existing memories (retrieved by embedding similarity across chunks).
|
|
103
|
+
2. IN-DOCUMENT MEMORIES — chunks and image nodes from this ingest (chunks may link to each other or to images).
|
|
104
|
+
3. Optional vision attachments for image nodes (when provided).
|
|
105
|
+
|
|
106
|
+
TASK
|
|
107
|
+
For EACH in-document chunk listed below, propose 0, 1, or 2 OUTGOING semantic links.
|
|
108
|
+
- Links may target EXTERNAL CANDIDATES or other IN-DOCUMENT memories (chunks or images).
|
|
109
|
+
- Do NOT link to the collection node or the document source node.
|
|
110
|
+
- Only link when there is clear topical, causal, dependency, continuation, or deepening value.
|
|
111
|
+
- Do not link for shallow overlap.
|
|
112
|
+
- Each link description must be 2-3 natural questions and must contain question marks.
|
|
113
|
+
|
|
114
|
+
Return ONLY valid JSON matching the schema. No markdown.
|
|
115
|
+
|
|
116
|
+
IN-DOCUMENT CHUNKS
|
|
117
|
+
{chunks_text}
|
|
118
|
+
|
|
119
|
+
IN-DOCUMENT IMAGES
|
|
120
|
+
{images_text}
|
|
121
|
+
|
|
122
|
+
EXTERNAL CANDIDATES
|
|
123
|
+
{external_text}
|
|
124
|
+
|
|
125
|
+
JSON SCHEMA
|
|
126
|
+
{{
|
|
127
|
+
"reasoning": "brief overall strategy",
|
|
128
|
+
"chunk_links": [
|
|
129
|
+
{{
|
|
130
|
+
"chunk_id": "chunk memory id",
|
|
131
|
+
"connections": [
|
|
132
|
+
{{"target_id": "allowed target id", "description": "2-3 questions?"}}
|
|
133
|
+
]
|
|
134
|
+
}}
|
|
135
|
+
]
|
|
136
|
+
}}
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ChunkLinkAssignment(BaseModel):
|
|
141
|
+
chunk_id: str
|
|
142
|
+
connections: list[ConnectionSpec] = Field(default_factory=list)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class DocumentLinkGuardianPlan(BaseModel):
|
|
146
|
+
reasoning: str = ""
|
|
147
|
+
chunk_links: list[ChunkLinkAssignment] = Field(default_factory=list)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Memory event recipes, projections, and Artha engine adapter."""
|
memuron/memory/engine.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Memuron-specific ArthaEngine extensions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from urllib.parse import urlparse, urlunparse
|
|
6
|
+
|
|
7
|
+
from artha_engine import ArthaEngine
|
|
8
|
+
from artha_engine.store import is_postgres_dsn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def mask_database_url(url: str) -> str:
|
|
12
|
+
if not url:
|
|
13
|
+
return url
|
|
14
|
+
if is_postgres_dsn(url):
|
|
15
|
+
parsed = urlparse(url)
|
|
16
|
+
host = parsed.hostname or ""
|
|
17
|
+
if parsed.port:
|
|
18
|
+
host = f"{host}:{parsed.port}"
|
|
19
|
+
if parsed.username:
|
|
20
|
+
netloc = f"{parsed.username}:***@{host}"
|
|
21
|
+
else:
|
|
22
|
+
netloc = f"***@{host}" if host else "***"
|
|
23
|
+
return urlunparse(
|
|
24
|
+
(parsed.scheme, netloc, parsed.path, parsed.params, parsed.query, parsed.fragment)
|
|
25
|
+
)
|
|
26
|
+
return url
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MemuronArthaEngine(ArthaEngine):
|
|
30
|
+
def runtime_summary(self, *, profiles: list[str] | None = None) -> dict[str, object]:
|
|
31
|
+
summary = super().runtime_summary(profiles=profiles)
|
|
32
|
+
db_path = summary.get("db_path")
|
|
33
|
+
if db_path is not None:
|
|
34
|
+
summary = {**summary, "db_path": mask_database_url(str(db_path))}
|
|
35
|
+
return summary
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Memuron read-model projections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from artha_engine.store.projection_sql import sql_store_execute, sql_store_fetchall, sql_store_has_tables
|
|
9
|
+
|
|
10
|
+
from memuron.application.config import settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _json_dumps(value: object) -> str:
|
|
14
|
+
return json.dumps(value, ensure_ascii=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _json_loads(value: object, default: object) -> object:
|
|
18
|
+
if value is None:
|
|
19
|
+
return default
|
|
20
|
+
if isinstance(value, (list, dict)):
|
|
21
|
+
return value
|
|
22
|
+
try:
|
|
23
|
+
return json.loads(str(value))
|
|
24
|
+
except json.JSONDecodeError:
|
|
25
|
+
return default
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _drop_legacy_category_column(store: Any) -> None:
|
|
29
|
+
if not sql_store_has_tables(store):
|
|
30
|
+
return
|
|
31
|
+
try:
|
|
32
|
+
sql_store_execute(store, "ALTER TABLE memuron_memories DROP COLUMN category")
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _add_column_if_missing(store: Any, table: str, column_sql: str) -> None:
|
|
38
|
+
if not sql_store_has_tables(store):
|
|
39
|
+
return
|
|
40
|
+
try:
|
|
41
|
+
sql_store_execute(store, f"ALTER TABLE {table} ADD COLUMN {column_sql}")
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MemoryProjection:
|
|
47
|
+
name = "memuron_memories"
|
|
48
|
+
|
|
49
|
+
def init(self, store: Any) -> None:
|
|
50
|
+
if not sql_store_has_tables(store):
|
|
51
|
+
store.memuron_memories = getattr(store, "memuron_memories", {})
|
|
52
|
+
return
|
|
53
|
+
sql_store_execute(
|
|
54
|
+
store,
|
|
55
|
+
"""
|
|
56
|
+
CREATE TABLE IF NOT EXISTS memuron_memories (
|
|
57
|
+
artha_id TEXT PRIMARY KEY,
|
|
58
|
+
content TEXT NOT NULL,
|
|
59
|
+
node_type TEXT NOT NULL DEFAULT 'text',
|
|
60
|
+
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
61
|
+
perception TEXT,
|
|
62
|
+
encoding TEXT NOT NULL DEFAULT 'memory',
|
|
63
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
64
|
+
scope_json TEXT NOT NULL DEFAULT '[]',
|
|
65
|
+
embedding_json TEXT NOT NULL DEFAULT '[]',
|
|
66
|
+
created_at TEXT,
|
|
67
|
+
updated_at TEXT,
|
|
68
|
+
sequence BIGINT NOT NULL DEFAULT 0
|
|
69
|
+
)
|
|
70
|
+
""",
|
|
71
|
+
)
|
|
72
|
+
_drop_legacy_category_column(store)
|
|
73
|
+
_add_column_if_missing(store, "memuron_memories", "node_type TEXT NOT NULL DEFAULT 'text'")
|
|
74
|
+
_add_column_if_missing(store, "memuron_memories", "payload_json TEXT NOT NULL DEFAULT '{}'")
|
|
75
|
+
_add_column_if_missing(store, "memuron_memories", "perception TEXT")
|
|
76
|
+
_add_column_if_missing(store, "memuron_memories", "encoding TEXT NOT NULL DEFAULT 'memory'")
|
|
77
|
+
_add_column_if_missing(store, "memuron_memories", "metadata_json TEXT NOT NULL DEFAULT '{}'")
|
|
78
|
+
from memuron.search.fulltext import ensure_fulltext_schema
|
|
79
|
+
from memuron.search.pgvector import ensure_pgvector_schema
|
|
80
|
+
|
|
81
|
+
ensure_pgvector_schema(store, settings.vector_dimensions)
|
|
82
|
+
ensure_fulltext_schema(store)
|
|
83
|
+
|
|
84
|
+
def apply(self, event: dict[str, object], store: Any) -> None:
|
|
85
|
+
event_type = str(event.get("event_type", ""))
|
|
86
|
+
if event_type in {"delete", "memory.deleted"}:
|
|
87
|
+
if event.get("subject_type") == "memory":
|
|
88
|
+
self._delete(str(event["subject_id"]), store)
|
|
89
|
+
return
|
|
90
|
+
if event_type not in {"memory.created", "memory.updated", "collection.created"}:
|
|
91
|
+
return
|
|
92
|
+
payload = event.get("payload")
|
|
93
|
+
if not isinstance(payload, dict):
|
|
94
|
+
return
|
|
95
|
+
arthaanu = payload.get("arthaanu")
|
|
96
|
+
if not isinstance(arthaanu, dict):
|
|
97
|
+
return
|
|
98
|
+
value = arthaanu.get("value")
|
|
99
|
+
if not isinstance(value, dict):
|
|
100
|
+
return
|
|
101
|
+
content = value.get("content")
|
|
102
|
+
if not isinstance(content, str):
|
|
103
|
+
return
|
|
104
|
+
scope = _json_loads(value.get("scope"), [])
|
|
105
|
+
embedding = _json_loads(value.get("embedding"), [])
|
|
106
|
+
node_type = str(value.get("node_type") or "text")
|
|
107
|
+
payload_json = _json_dumps(_json_loads(value.get("payload"), {}))
|
|
108
|
+
perception = value.get("perception")
|
|
109
|
+
encoding = str(value.get("encoding") or "memory")
|
|
110
|
+
metadata_json = _json_dumps(_json_loads(value.get("metadata"), {}))
|
|
111
|
+
artha_id = str(event["subject_id"])
|
|
112
|
+
sequence = int(event["sequence"])
|
|
113
|
+
metadata = event.get("metadata")
|
|
114
|
+
created_at = None
|
|
115
|
+
if isinstance(metadata, dict):
|
|
116
|
+
created_at = metadata.get("created_at")
|
|
117
|
+
if sql_store_has_tables(store):
|
|
118
|
+
existing = sql_store_fetchall(
|
|
119
|
+
store,
|
|
120
|
+
"SELECT created_at FROM memuron_memories WHERE artha_id = ?",
|
|
121
|
+
(artha_id,),
|
|
122
|
+
)
|
|
123
|
+
if existing and existing[0].get("created_at"):
|
|
124
|
+
created_at = existing[0]["created_at"]
|
|
125
|
+
sql_store_execute(
|
|
126
|
+
store,
|
|
127
|
+
"""
|
|
128
|
+
INSERT INTO memuron_memories (
|
|
129
|
+
artha_id, content, node_type, payload_json, perception, encoding, metadata_json,
|
|
130
|
+
scope_json, embedding_json,
|
|
131
|
+
created_at, updated_at, sequence
|
|
132
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
133
|
+
ON CONFLICT(artha_id) DO UPDATE SET
|
|
134
|
+
content = excluded.content,
|
|
135
|
+
node_type = excluded.node_type,
|
|
136
|
+
payload_json = excluded.payload_json,
|
|
137
|
+
perception = excluded.perception,
|
|
138
|
+
encoding = excluded.encoding,
|
|
139
|
+
metadata_json = excluded.metadata_json,
|
|
140
|
+
scope_json = excluded.scope_json,
|
|
141
|
+
embedding_json = excluded.embedding_json,
|
|
142
|
+
updated_at = excluded.updated_at,
|
|
143
|
+
sequence = excluded.sequence
|
|
144
|
+
""",
|
|
145
|
+
(
|
|
146
|
+
artha_id,
|
|
147
|
+
content,
|
|
148
|
+
node_type,
|
|
149
|
+
payload_json,
|
|
150
|
+
perception if isinstance(perception, str) else None,
|
|
151
|
+
encoding,
|
|
152
|
+
metadata_json,
|
|
153
|
+
_json_dumps(scope if isinstance(scope, list) else []),
|
|
154
|
+
_json_dumps(embedding if isinstance(embedding, list) else []),
|
|
155
|
+
created_at or str(event.get("created_at", "")),
|
|
156
|
+
str(event.get("created_at", "")),
|
|
157
|
+
sequence,
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
from memuron.search.fulltext import sync_memory_fulltext
|
|
161
|
+
from memuron.search.pgvector import sync_memory_embedding
|
|
162
|
+
|
|
163
|
+
sync_memory_embedding(
|
|
164
|
+
store,
|
|
165
|
+
artha_id,
|
|
166
|
+
[float(value) for value in embedding] if isinstance(embedding, list) else [],
|
|
167
|
+
)
|
|
168
|
+
sync_memory_fulltext(store, artha_id, content)
|
|
169
|
+
else:
|
|
170
|
+
bucket = store.memuron_memories
|
|
171
|
+
prior = bucket.get(artha_id, {})
|
|
172
|
+
bucket[artha_id] = {
|
|
173
|
+
"content": content,
|
|
174
|
+
"node_type": node_type,
|
|
175
|
+
"payload": _json_loads(value.get("payload"), {}),
|
|
176
|
+
"perception": perception if isinstance(perception, str) else None,
|
|
177
|
+
"encoding": encoding,
|
|
178
|
+
"metadata": _json_loads(value.get("metadata"), {}),
|
|
179
|
+
"scope": scope if isinstance(scope, list) else [],
|
|
180
|
+
"embedding": embedding if isinstance(embedding, list) else [],
|
|
181
|
+
"created_at": prior.get("created_at") or str(event.get("created_at", "")),
|
|
182
|
+
"updated_at": str(event.get("created_at", "")),
|
|
183
|
+
"sequence": sequence,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def reset(self, store: Any) -> None:
|
|
187
|
+
if sql_store_has_tables(store):
|
|
188
|
+
sql_store_execute(store, "DELETE FROM memuron_memories")
|
|
189
|
+
else:
|
|
190
|
+
store.memuron_memories = {}
|
|
191
|
+
|
|
192
|
+
def _delete(self, artha_id: str, store: Any) -> None:
|
|
193
|
+
if sql_store_has_tables(store):
|
|
194
|
+
sql_store_execute(
|
|
195
|
+
store,
|
|
196
|
+
"DELETE FROM memuron_memories WHERE artha_id = ?",
|
|
197
|
+
(artha_id,),
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
store.memuron_memories.pop(artha_id, None)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class MemoryLinkProjection:
|
|
204
|
+
name = "memuron_links"
|
|
205
|
+
|
|
206
|
+
def init(self, store: Any) -> None:
|
|
207
|
+
if not sql_store_has_tables(store):
|
|
208
|
+
store.memuron_links = getattr(store, "memuron_links", {})
|
|
209
|
+
return
|
|
210
|
+
sql_store_execute(
|
|
211
|
+
store,
|
|
212
|
+
"""
|
|
213
|
+
CREATE TABLE IF NOT EXISTS memuron_links (
|
|
214
|
+
link_id TEXT PRIMARY KEY,
|
|
215
|
+
source_id TEXT NOT NULL,
|
|
216
|
+
target_id TEXT NOT NULL,
|
|
217
|
+
description TEXT NOT NULL,
|
|
218
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
219
|
+
embedding_json TEXT NOT NULL DEFAULT '[]',
|
|
220
|
+
sequence BIGINT NOT NULL DEFAULT 0
|
|
221
|
+
)
|
|
222
|
+
""",
|
|
223
|
+
)
|
|
224
|
+
_add_column_if_missing(store, "memuron_links", "metadata_json TEXT NOT NULL DEFAULT '{}'")
|
|
225
|
+
sql_store_execute(
|
|
226
|
+
store,
|
|
227
|
+
"CREATE INDEX IF NOT EXISTS idx_memuron_links_source ON memuron_links(source_id)",
|
|
228
|
+
)
|
|
229
|
+
sql_store_execute(
|
|
230
|
+
store,
|
|
231
|
+
"CREATE INDEX IF NOT EXISTS idx_memuron_links_target ON memuron_links(target_id)",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def apply(self, event: dict[str, object], store: Any) -> None:
|
|
235
|
+
event_type = str(event.get("event_type", ""))
|
|
236
|
+
if event_type == "delete" and event.get("subject_type") == "memory_link":
|
|
237
|
+
# Legacy replay only — new writes use link.removed.
|
|
238
|
+
self._delete(str(event["subject_id"]), store)
|
|
239
|
+
return
|
|
240
|
+
if event_type not in {"link.created", "link.removed"}:
|
|
241
|
+
return
|
|
242
|
+
if event_type == "link.removed":
|
|
243
|
+
link_id = str(event.get("subject_id", ""))
|
|
244
|
+
payload = event.get("payload")
|
|
245
|
+
if isinstance(payload, dict) and payload.get("link_id"):
|
|
246
|
+
link_id = str(payload["link_id"])
|
|
247
|
+
if link_id:
|
|
248
|
+
self._delete(link_id, store)
|
|
249
|
+
return
|
|
250
|
+
payload = event.get("payload")
|
|
251
|
+
if not isinstance(payload, dict):
|
|
252
|
+
return
|
|
253
|
+
arthaanu = payload.get("arthaanu")
|
|
254
|
+
if not isinstance(arthaanu, dict):
|
|
255
|
+
return
|
|
256
|
+
value = arthaanu.get("value")
|
|
257
|
+
if not isinstance(value, dict):
|
|
258
|
+
return
|
|
259
|
+
source_id = value.get("source_id")
|
|
260
|
+
target_id = value.get("target_id")
|
|
261
|
+
description = value.get("description")
|
|
262
|
+
if not isinstance(source_id, str) or not isinstance(target_id, str):
|
|
263
|
+
return
|
|
264
|
+
if not isinstance(description, str):
|
|
265
|
+
return
|
|
266
|
+
metadata = _json_loads(value.get("metadata"), {})
|
|
267
|
+
embedding = _json_loads(value.get("embedding"), [])
|
|
268
|
+
link_id = str(event["subject_id"])
|
|
269
|
+
sequence = int(event["sequence"])
|
|
270
|
+
if sql_store_has_tables(store):
|
|
271
|
+
sql_store_execute(
|
|
272
|
+
store,
|
|
273
|
+
"""
|
|
274
|
+
INSERT INTO memuron_links (
|
|
275
|
+
link_id, source_id, target_id, description, metadata_json, embedding_json, sequence
|
|
276
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
277
|
+
ON CONFLICT(link_id) DO UPDATE SET
|
|
278
|
+
source_id = excluded.source_id,
|
|
279
|
+
target_id = excluded.target_id,
|
|
280
|
+
description = excluded.description,
|
|
281
|
+
metadata_json = excluded.metadata_json,
|
|
282
|
+
embedding_json = excluded.embedding_json,
|
|
283
|
+
sequence = excluded.sequence
|
|
284
|
+
""",
|
|
285
|
+
(
|
|
286
|
+
link_id,
|
|
287
|
+
source_id,
|
|
288
|
+
target_id,
|
|
289
|
+
description,
|
|
290
|
+
_json_dumps(metadata if isinstance(metadata, dict) else {}),
|
|
291
|
+
_json_dumps(embedding if isinstance(embedding, list) else []),
|
|
292
|
+
sequence,
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
from memuron.search.pgvector import sync_link_embedding
|
|
296
|
+
|
|
297
|
+
sync_link_embedding(
|
|
298
|
+
store,
|
|
299
|
+
link_id,
|
|
300
|
+
[float(value) for value in embedding] if isinstance(embedding, list) else [],
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
store.memuron_links[link_id] = {
|
|
304
|
+
"source_id": source_id,
|
|
305
|
+
"target_id": target_id,
|
|
306
|
+
"description": description,
|
|
307
|
+
"metadata": metadata if isinstance(metadata, dict) else {},
|
|
308
|
+
"embedding": embedding if isinstance(embedding, list) else [],
|
|
309
|
+
"sequence": sequence,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
def reset(self, store: Any) -> None:
|
|
313
|
+
if sql_store_has_tables(store):
|
|
314
|
+
sql_store_execute(store, "DELETE FROM memuron_links")
|
|
315
|
+
else:
|
|
316
|
+
store.memuron_links = {}
|
|
317
|
+
|
|
318
|
+
def _delete(self, link_id: str, store: Any) -> None:
|
|
319
|
+
if sql_store_has_tables(store):
|
|
320
|
+
sql_store_execute(
|
|
321
|
+
store,
|
|
322
|
+
"DELETE FROM memuron_links WHERE link_id = ?",
|
|
323
|
+
(link_id,),
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
store.memuron_links.pop(link_id, None)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class MemoryPlacementProjection:
|
|
330
|
+
name = "memuron_placements"
|
|
331
|
+
|
|
332
|
+
def init(self, store: Any) -> None:
|
|
333
|
+
if not sql_store_has_tables(store):
|
|
334
|
+
store.memuron_placements = getattr(store, "memuron_placements", {})
|
|
335
|
+
return
|
|
336
|
+
sql_store_execute(
|
|
337
|
+
store,
|
|
338
|
+
"""
|
|
339
|
+
CREATE TABLE IF NOT EXISTS memuron_placements (
|
|
340
|
+
placement_id TEXT PRIMARY KEY,
|
|
341
|
+
parent_id TEXT NOT NULL,
|
|
342
|
+
child_id TEXT NOT NULL,
|
|
343
|
+
name TEXT NOT NULL,
|
|
344
|
+
scope_json TEXT NOT NULL DEFAULT '[]',
|
|
345
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
346
|
+
inherit_parent_scope BOOLEAN NOT NULL DEFAULT TRUE,
|
|
347
|
+
sequence BIGINT NOT NULL DEFAULT 0,
|
|
348
|
+
UNIQUE(parent_id, name)
|
|
349
|
+
)
|
|
350
|
+
""",
|
|
351
|
+
)
|
|
352
|
+
_add_column_if_missing(
|
|
353
|
+
store,
|
|
354
|
+
"memuron_placements",
|
|
355
|
+
"inherit_parent_scope BOOLEAN NOT NULL DEFAULT TRUE",
|
|
356
|
+
)
|
|
357
|
+
sql_store_execute(
|
|
358
|
+
store,
|
|
359
|
+
"CREATE INDEX IF NOT EXISTS idx_memuron_placements_parent ON memuron_placements(parent_id)",
|
|
360
|
+
)
|
|
361
|
+
sql_store_execute(
|
|
362
|
+
store,
|
|
363
|
+
"CREATE INDEX IF NOT EXISTS idx_memuron_placements_child ON memuron_placements(child_id)",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def apply(self, event: dict[str, object], store: Any) -> None:
|
|
367
|
+
event_type = str(event.get("event_type", ""))
|
|
368
|
+
if event_type == "delete" and event.get("subject_type") == "memory_placement":
|
|
369
|
+
self._delete(str(event["subject_id"]), store)
|
|
370
|
+
return
|
|
371
|
+
if event_type not in {"placement.created", "placement.removed"}:
|
|
372
|
+
return
|
|
373
|
+
if event_type == "placement.removed":
|
|
374
|
+
placement_id = str(event.get("subject_id", ""))
|
|
375
|
+
payload = event.get("payload")
|
|
376
|
+
if isinstance(payload, dict) and payload.get("placement_id"):
|
|
377
|
+
placement_id = str(payload["placement_id"])
|
|
378
|
+
if placement_id:
|
|
379
|
+
self._delete(placement_id, store)
|
|
380
|
+
return
|
|
381
|
+
payload = event.get("payload")
|
|
382
|
+
if not isinstance(payload, dict):
|
|
383
|
+
return
|
|
384
|
+
arthaanu = payload.get("arthaanu")
|
|
385
|
+
if not isinstance(arthaanu, dict):
|
|
386
|
+
return
|
|
387
|
+
value = arthaanu.get("value")
|
|
388
|
+
if not isinstance(value, dict):
|
|
389
|
+
return
|
|
390
|
+
parent_id = value.get("parent_id")
|
|
391
|
+
child_id = value.get("child_id")
|
|
392
|
+
name = value.get("name")
|
|
393
|
+
if not isinstance(parent_id, str) or not isinstance(child_id, str) or not isinstance(name, str):
|
|
394
|
+
return
|
|
395
|
+
scope = _json_loads(value.get("scope"), [])
|
|
396
|
+
metadata = _json_loads(value.get("metadata"), {})
|
|
397
|
+
inherit_parent_scope = bool(value.get("inherit_parent_scope", True))
|
|
398
|
+
placement_id = str(event["subject_id"])
|
|
399
|
+
sequence = int(event["sequence"])
|
|
400
|
+
if sql_store_has_tables(store):
|
|
401
|
+
sql_store_execute(
|
|
402
|
+
store,
|
|
403
|
+
"""
|
|
404
|
+
INSERT INTO memuron_placements (
|
|
405
|
+
placement_id, parent_id, child_id, name, scope_json, metadata_json,
|
|
406
|
+
inherit_parent_scope, sequence
|
|
407
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
408
|
+
ON CONFLICT(parent_id, name) DO UPDATE SET
|
|
409
|
+
placement_id = excluded.placement_id,
|
|
410
|
+
child_id = excluded.child_id,
|
|
411
|
+
scope_json = excluded.scope_json,
|
|
412
|
+
metadata_json = excluded.metadata_json,
|
|
413
|
+
inherit_parent_scope = excluded.inherit_parent_scope,
|
|
414
|
+
sequence = excluded.sequence
|
|
415
|
+
""",
|
|
416
|
+
(
|
|
417
|
+
placement_id,
|
|
418
|
+
parent_id,
|
|
419
|
+
child_id,
|
|
420
|
+
name,
|
|
421
|
+
_json_dumps(scope if isinstance(scope, list) else []),
|
|
422
|
+
_json_dumps(metadata if isinstance(metadata, dict) else {}),
|
|
423
|
+
inherit_parent_scope,
|
|
424
|
+
sequence,
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
store.memuron_placements[placement_id] = {
|
|
429
|
+
"parent_id": parent_id,
|
|
430
|
+
"child_id": child_id,
|
|
431
|
+
"name": name,
|
|
432
|
+
"scope": scope if isinstance(scope, list) else [],
|
|
433
|
+
"metadata": metadata if isinstance(metadata, dict) else {},
|
|
434
|
+
"inherit_parent_scope": inherit_parent_scope,
|
|
435
|
+
"sequence": sequence,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
def reset(self, store: Any) -> None:
|
|
439
|
+
if sql_store_has_tables(store):
|
|
440
|
+
sql_store_execute(store, "DELETE FROM memuron_placements")
|
|
441
|
+
else:
|
|
442
|
+
store.memuron_placements = {}
|
|
443
|
+
|
|
444
|
+
def _delete(self, placement_id: str, store: Any) -> None:
|
|
445
|
+
if sql_store_has_tables(store):
|
|
446
|
+
sql_store_execute(
|
|
447
|
+
store,
|
|
448
|
+
"DELETE FROM memuron_placements WHERE placement_id = ?",
|
|
449
|
+
(placement_id,),
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
store.memuron_placements.pop(placement_id, None)
|