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
memuron/actions/sync.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Folder sync actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from artha_engine import ActionContext, ArthaEngine
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from memuron.actions.helpers import event_metadata, require_user_org
|
|
12
|
+
from memuron.actions.registry import actions
|
|
13
|
+
from memuron.documents.parser import MAX_DOCUMENT_UPLOAD_BYTES
|
|
14
|
+
from memuron.persistence.identity_store import IdentityStore
|
|
15
|
+
from memuron.security.tenant import merge_org_scope
|
|
16
|
+
from memuron.spaces.service import resolve_space_reference
|
|
17
|
+
from memuron.sync.folder import (
|
|
18
|
+
init_manifest,
|
|
19
|
+
load_manifest,
|
|
20
|
+
resolve_manifest_path,
|
|
21
|
+
run_engine_folder_sync,
|
|
22
|
+
save_manifest,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SyncInitInput(BaseModel):
|
|
27
|
+
path: str = Field(..., description="Local folder root to sync from.")
|
|
28
|
+
space_ref: str = Field(..., description="Target space UUID, slug, token, or /spaces path.")
|
|
29
|
+
include: list[str] = Field(default_factory=list, description="Include pattern; repeatable.")
|
|
30
|
+
exclude: list[str] = Field(default_factory=list, description="Exclude pattern; repeatable.")
|
|
31
|
+
manifest_path: str | None = Field(None, description="Manifest path override.")
|
|
32
|
+
max_file_bytes: int = Field(
|
|
33
|
+
MAX_DOCUMENT_UPLOAD_BYTES,
|
|
34
|
+
ge=1,
|
|
35
|
+
description="Skip files larger than this many bytes.",
|
|
36
|
+
)
|
|
37
|
+
overwrite: bool = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SyncRunInput(BaseModel):
|
|
41
|
+
path: str = Field(..., description="Folder root or manifest JSON path.")
|
|
42
|
+
manifest_path: str | None = Field(None, description="Manifest path override.")
|
|
43
|
+
dry_run: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _space_payload(identity: IdentityStore, *, org_id: str, space_ref: str) -> dict[str, Any]:
|
|
47
|
+
space = resolve_space_reference(identity, org_id=org_id, space_ref=space_ref)
|
|
48
|
+
if space is None:
|
|
49
|
+
raise KeyError("Space not found")
|
|
50
|
+
return {
|
|
51
|
+
"id": str(space["id"]),
|
|
52
|
+
"slug": str(space["slug"]),
|
|
53
|
+
"name": str(space["name"]),
|
|
54
|
+
"token": str(space["token"]),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@actions.action(
|
|
59
|
+
name="sync.init",
|
|
60
|
+
description="Initialize a one-way folder sync manifest for a local directory.",
|
|
61
|
+
kind="write",
|
|
62
|
+
scopes=["memory:write"],
|
|
63
|
+
cli="sync init",
|
|
64
|
+
http=False,
|
|
65
|
+
mcp=False,
|
|
66
|
+
inject={"identity": "identity"},
|
|
67
|
+
tags=["sync"],
|
|
68
|
+
)
|
|
69
|
+
def sync_init(
|
|
70
|
+
input: SyncInitInput,
|
|
71
|
+
context: ActionContext,
|
|
72
|
+
identity: IdentityStore,
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
_user_id, org_id = require_user_org(context.auth)
|
|
75
|
+
space = _space_payload(identity, org_id=org_id, space_ref=input.space_ref)
|
|
76
|
+
manifest, path = init_manifest(
|
|
77
|
+
input.path,
|
|
78
|
+
space_ref=input.space_ref,
|
|
79
|
+
include=input.include,
|
|
80
|
+
exclude=input.exclude,
|
|
81
|
+
max_file_bytes=input.max_file_bytes,
|
|
82
|
+
manifest_path=input.manifest_path,
|
|
83
|
+
overwrite=input.overwrite,
|
|
84
|
+
)
|
|
85
|
+
manifest.space = space
|
|
86
|
+
save_manifest(manifest, path)
|
|
87
|
+
return {
|
|
88
|
+
"status": "success",
|
|
89
|
+
"manifest_path": str(path),
|
|
90
|
+
"manifest": manifest.to_dict(),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@actions.action(
|
|
95
|
+
name="sync.add",
|
|
96
|
+
description="Add or replace a one-way folder sync manifest for a local directory.",
|
|
97
|
+
kind="write",
|
|
98
|
+
scopes=["memory:write"],
|
|
99
|
+
cli="sync add",
|
|
100
|
+
http=False,
|
|
101
|
+
mcp=False,
|
|
102
|
+
inject={"identity": "identity"},
|
|
103
|
+
tags=["sync"],
|
|
104
|
+
)
|
|
105
|
+
def sync_add(
|
|
106
|
+
input: SyncInitInput,
|
|
107
|
+
context: ActionContext,
|
|
108
|
+
identity: IdentityStore,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
return sync_init(
|
|
111
|
+
SyncInitInput(**{**input.model_dump(), "overwrite": True}),
|
|
112
|
+
context,
|
|
113
|
+
identity,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@actions.action(
|
|
118
|
+
name="sync.run",
|
|
119
|
+
description="Run a one-way folder sync from a manifest, reporting local deletes without deleting remote graph nodes.",
|
|
120
|
+
kind="write",
|
|
121
|
+
scopes=["memory:write"],
|
|
122
|
+
cli="sync run",
|
|
123
|
+
http=False,
|
|
124
|
+
mcp=False,
|
|
125
|
+
inject={"identity": "identity"},
|
|
126
|
+
tags=["sync"],
|
|
127
|
+
)
|
|
128
|
+
def sync_run(
|
|
129
|
+
input: SyncRunInput,
|
|
130
|
+
engine: ArthaEngine,
|
|
131
|
+
context: ActionContext,
|
|
132
|
+
identity: IdentityStore,
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
_user_id, org_id = require_user_org(context.auth)
|
|
135
|
+
manifest_path = resolve_manifest_path(input.path, input.manifest_path)
|
|
136
|
+
if not manifest_path.exists():
|
|
137
|
+
raise KeyError(f"Folder sync manifest not found: {manifest_path}")
|
|
138
|
+
manifest = load_manifest(manifest_path)
|
|
139
|
+
space = _space_payload(
|
|
140
|
+
identity,
|
|
141
|
+
org_id=org_id,
|
|
142
|
+
space_ref=str(manifest.space.get("token") or manifest.space_ref),
|
|
143
|
+
)
|
|
144
|
+
manifest.space = space
|
|
145
|
+
scope = merge_org_scope([space["token"]], org_id)
|
|
146
|
+
result = run_engine_folder_sync(
|
|
147
|
+
engine,
|
|
148
|
+
manifest,
|
|
149
|
+
scope=scope,
|
|
150
|
+
event_metadata=event_metadata(context),
|
|
151
|
+
manifest_path=manifest_path,
|
|
152
|
+
dry_run=input.dry_run,
|
|
153
|
+
)
|
|
154
|
+
result["manifest_path"] = str(Path(manifest_path))
|
|
155
|
+
return result
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Application entrypoints, configuration, and runtime assembly."""
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Memuron HTTP application factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from artha_engine.store import is_postgres_dsn
|
|
12
|
+
from fastapi import HTTPException, Request
|
|
13
|
+
|
|
14
|
+
from memuron.application.app import configure_clerk_env, create_application
|
|
15
|
+
from memuron.security.clerk_webhooks import router as clerk_webhook_router
|
|
16
|
+
from memuron.application.config import resolve_database_url, settings
|
|
17
|
+
from memuron.persistence.db_pool import close_postgres_pool
|
|
18
|
+
from memuron.ingest.jobs import start_ingest_workers, stop_ingest_workers
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatGPTMcpHeaderCompat:
|
|
24
|
+
"""Normalize ChatGPT connector JSON-RPC preflight posts for MCP."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, app, auth_challenge: str):
|
|
27
|
+
self.app = app
|
|
28
|
+
self.auth_challenge = auth_challenge
|
|
29
|
+
|
|
30
|
+
async def __call__(self, scope, receive, send):
|
|
31
|
+
if scope.get("type") != "http":
|
|
32
|
+
await self.app(scope, receive, send)
|
|
33
|
+
return
|
|
34
|
+
if scope.get("method") == "POST" and scope.get("path") == "/mcp":
|
|
35
|
+
if self._has_empty_body(scope):
|
|
36
|
+
await self._send_auth_challenge(send)
|
|
37
|
+
return
|
|
38
|
+
headers = []
|
|
39
|
+
saw_accept = False
|
|
40
|
+
for name, value in scope.get("headers", []):
|
|
41
|
+
normalized_name = name.lower()
|
|
42
|
+
if (
|
|
43
|
+
normalized_name == b"content-type"
|
|
44
|
+
and value.split(b";", 1)[0].strip().lower()
|
|
45
|
+
== b"application/octet-stream"
|
|
46
|
+
):
|
|
47
|
+
headers.append((name, b"application/json"))
|
|
48
|
+
elif normalized_name == b"accept":
|
|
49
|
+
saw_accept = True
|
|
50
|
+
media_types = [
|
|
51
|
+
item.split(b";", 1)[0].strip().lower()
|
|
52
|
+
for item in value.split(b",")
|
|
53
|
+
]
|
|
54
|
+
if not {
|
|
55
|
+
b"application/json",
|
|
56
|
+
b"text/event-stream",
|
|
57
|
+
}.issubset(set(media_types)):
|
|
58
|
+
headers.append(
|
|
59
|
+
(name, b"application/json, text/event-stream")
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
headers.append((name, value))
|
|
63
|
+
else:
|
|
64
|
+
headers.append((name, value))
|
|
65
|
+
if not saw_accept:
|
|
66
|
+
headers.append((b"accept", b"application/json, text/event-stream"))
|
|
67
|
+
scope = {**scope, "headers": headers}
|
|
68
|
+
await self.app(scope, receive, send)
|
|
69
|
+
|
|
70
|
+
def _has_empty_body(self, scope) -> bool:
|
|
71
|
+
for name, value in scope.get("headers", []):
|
|
72
|
+
if name.lower() == b"content-length":
|
|
73
|
+
return value.strip() == b"0"
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
async def _send_auth_challenge(self, send) -> None:
|
|
77
|
+
content = b'{"error":"authorization_required"}'
|
|
78
|
+
await send(
|
|
79
|
+
{
|
|
80
|
+
"type": "http.response.start",
|
|
81
|
+
"status": 401,
|
|
82
|
+
"headers": [
|
|
83
|
+
(b"content-type", b"application/json"),
|
|
84
|
+
(b"content-length", str(len(content)).encode("ascii")),
|
|
85
|
+
(b"www-authenticate", self.auth_challenge.encode("utf-8")),
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
await send({"type": "http.response.body", "body": content})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_app(db: str | Path | None = None):
|
|
93
|
+
configure_clerk_env()
|
|
94
|
+
application = create_application(db)
|
|
95
|
+
target = resolve_database_url() if db is None else str(db)
|
|
96
|
+
engine = application.engine
|
|
97
|
+
guardian = application.services["guardian"]
|
|
98
|
+
|
|
99
|
+
http_app = application.create_http_app(expose_engine=False)
|
|
100
|
+
|
|
101
|
+
from artha_engine.runtime.api.app import create_app as create_engine_app
|
|
102
|
+
|
|
103
|
+
from memuron.application.capabilities import memuron_engine_profile_capability
|
|
104
|
+
|
|
105
|
+
async def engine_auth(request: Request):
|
|
106
|
+
from artha_engine.app.auth import AuthenticationError
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
auth = await application.auth_provider.authenticate_http(request)
|
|
110
|
+
except AuthenticationError as exc:
|
|
111
|
+
raise HTTPException(status_code=401, detail=str(exc)) from exc
|
|
112
|
+
required = {"artha:admin"}
|
|
113
|
+
if "*" not in auth.scopes and not required.issubset(set(auth.scopes)):
|
|
114
|
+
raise HTTPException(status_code=403, detail="Engine admin scope required")
|
|
115
|
+
return auth
|
|
116
|
+
|
|
117
|
+
profile = memuron_engine_profile_capability(application.manifest())
|
|
118
|
+
http_app.mount(
|
|
119
|
+
"/engine",
|
|
120
|
+
create_engine_app(
|
|
121
|
+
engine,
|
|
122
|
+
auth_dependency=engine_auth,
|
|
123
|
+
profile_capabilities=[profile],
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
remote_mcp = None
|
|
128
|
+
public_domain = os.environ.get("RAILWAY_PUBLIC_DOMAIN", "").strip()
|
|
129
|
+
public_mcp_url = os.environ.get("MEMURON_MCP_PUBLIC_URL", "").strip()
|
|
130
|
+
if not public_mcp_url and public_domain:
|
|
131
|
+
public_mcp_url = f"https://{public_domain}/mcp"
|
|
132
|
+
if public_mcp_url:
|
|
133
|
+
from memuron.application.mcp_oauth import (
|
|
134
|
+
create_remote_mcp_server,
|
|
135
|
+
mcp_auth_challenge,
|
|
136
|
+
protected_resource_metadata,
|
|
137
|
+
protected_resource_metadata_path,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
remote_mcp = create_remote_mcp_server(application, public_mcp_url)
|
|
141
|
+
|
|
142
|
+
@http_app.get(
|
|
143
|
+
protected_resource_metadata_path(public_mcp_url),
|
|
144
|
+
include_in_schema=False,
|
|
145
|
+
)
|
|
146
|
+
def mcp_protected_resource_metadata():
|
|
147
|
+
return protected_resource_metadata(public_mcp_url)
|
|
148
|
+
|
|
149
|
+
http_app.mount(
|
|
150
|
+
"/",
|
|
151
|
+
ChatGPTMcpHeaderCompat(
|
|
152
|
+
remote_mcp.streamable_http_app(),
|
|
153
|
+
mcp_auth_challenge(public_mcp_url),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _warmup_embedder() -> None:
|
|
158
|
+
try:
|
|
159
|
+
engine.embedder.embed_queries(["memuron warmup"])
|
|
160
|
+
logger.info("Embedder warmed up (%s)", settings.embed_model)
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
logger.warning("Embedder warmup failed: %s", exc)
|
|
163
|
+
|
|
164
|
+
def _init_projections() -> None:
|
|
165
|
+
from memuron.memory.recipes import ensure_memory_projections
|
|
166
|
+
|
|
167
|
+
ensure_memory_projections(engine)
|
|
168
|
+
engine.refresh_projection("memuron_fs")
|
|
169
|
+
|
|
170
|
+
@contextlib.asynccontextmanager
|
|
171
|
+
async def lifespan(_app):
|
|
172
|
+
async with contextlib.AsyncExitStack() as stack:
|
|
173
|
+
if remote_mcp is not None:
|
|
174
|
+
await stack.enter_async_context(remote_mcp.session_manager.run())
|
|
175
|
+
await asyncio.to_thread(_warmup_embedder)
|
|
176
|
+
await asyncio.to_thread(_init_projections)
|
|
177
|
+
http_app.state.identity_store = application.services["identity"]
|
|
178
|
+
await start_ingest_workers(http_app, engine, guardian)
|
|
179
|
+
try:
|
|
180
|
+
yield
|
|
181
|
+
finally:
|
|
182
|
+
await stop_ingest_workers(http_app)
|
|
183
|
+
identity_store = getattr(http_app.state, "identity_store", None)
|
|
184
|
+
if identity_store is not None:
|
|
185
|
+
identity_store.close()
|
|
186
|
+
store_target = getattr(engine.store, "dsn", None) or getattr(
|
|
187
|
+
engine.store, "database_target", target
|
|
188
|
+
)
|
|
189
|
+
if is_postgres_dsn(str(store_target)):
|
|
190
|
+
close_postgres_pool(engine.store)
|
|
191
|
+
|
|
192
|
+
http_app.router.lifespan_context = lifespan
|
|
193
|
+
|
|
194
|
+
http_app.state.artha_application = application
|
|
195
|
+
http_app.state.engine = engine
|
|
196
|
+
http_app.state.guardian = guardian
|
|
197
|
+
http_app.state.identity_store = application.services["identity"]
|
|
198
|
+
http_app.state.ingest_job_store = application.services.get("jobs")
|
|
199
|
+
http_app.include_router(clerk_webhook_router)
|
|
200
|
+
return http_app
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def create_engine(db: str | Path | None = None):
|
|
204
|
+
from memuron.application.app import create_engine as _create_engine
|
|
205
|
+
|
|
206
|
+
return _create_engine(db)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Memuron ArthaApplication factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from artha_engine import ArthaApplication, create_store, resolve_database_target
|
|
10
|
+
from artha_engine.app.managed_api_keys import register_api_key_actions
|
|
11
|
+
from artha_engine.store.api_key_store import ApiKeyStore
|
|
12
|
+
from artha_engine.store import is_postgres_dsn
|
|
13
|
+
|
|
14
|
+
from memuron.actions import actions
|
|
15
|
+
from memuron.security.auth_provider import create_memuron_auth_provider
|
|
16
|
+
from memuron.security.authorization import MemuronAuthorizationPolicy
|
|
17
|
+
from memuron.application.config import resolve_database_url, settings
|
|
18
|
+
from memuron.persistence.db_pool import install_postgres_pool
|
|
19
|
+
from memuron.persistence.db_pool import close_postgres_pool
|
|
20
|
+
from memuron.memory.engine import MemuronArthaEngine
|
|
21
|
+
from memuron.ingest.guardian import create_guardian
|
|
22
|
+
from memuron.persistence.identity_store import IdentityStore
|
|
23
|
+
from memuron.ingest.jobs import IngestJobStore
|
|
24
|
+
from memuron.application.registry import build_registry
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _build_embedder():
|
|
28
|
+
from artha_engine import DeterministicTextEmbedder, FastEmbedTextEmbedder
|
|
29
|
+
|
|
30
|
+
if settings.embedder == "deterministic":
|
|
31
|
+
return DeterministicTextEmbedder(dimensions=8)
|
|
32
|
+
try:
|
|
33
|
+
return FastEmbedTextEmbedder(model_name=settings.embed_model)
|
|
34
|
+
except Exception:
|
|
35
|
+
return DeterministicTextEmbedder(dimensions=8)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_engine(db: str | Path | None = None) -> MemuronArthaEngine:
|
|
39
|
+
registry = build_registry()
|
|
40
|
+
target = resolve_database_target(db or resolve_database_url())
|
|
41
|
+
store = create_store(target, type_registry=registry.arthaanu_types)
|
|
42
|
+
if is_postgres_dsn(target):
|
|
43
|
+
install_postgres_pool(store)
|
|
44
|
+
return MemuronArthaEngine(
|
|
45
|
+
store=store,
|
|
46
|
+
registry=registry,
|
|
47
|
+
embedder=_build_embedder(),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_application(db: str | Path | None = None) -> ArthaApplication:
|
|
52
|
+
target = resolve_database_target(db or resolve_database_url())
|
|
53
|
+
engine = create_engine(db)
|
|
54
|
+
services: dict[str, Any] = {
|
|
55
|
+
"guardian": create_guardian(),
|
|
56
|
+
"identity": IdentityStore.from_target(target),
|
|
57
|
+
"jobs": IngestJobStore(target) if is_postgres_dsn(target) else None,
|
|
58
|
+
}
|
|
59
|
+
application = ArthaApplication(
|
|
60
|
+
name="memuron",
|
|
61
|
+
version="0.1.1",
|
|
62
|
+
description="Product memory layer on Artha Engine",
|
|
63
|
+
engine=engine,
|
|
64
|
+
actions=actions,
|
|
65
|
+
auth_provider=create_memuron_auth_provider(),
|
|
66
|
+
authorization_policy=MemuronAuthorizationPolicy(),
|
|
67
|
+
services=services,
|
|
68
|
+
)
|
|
69
|
+
api_key_store = ApiKeyStore.from_target(str(target))
|
|
70
|
+
registered = {item.name for item in actions.list()}
|
|
71
|
+
if "api_key.create" not in registered:
|
|
72
|
+
register_api_key_actions(
|
|
73
|
+
actions,
|
|
74
|
+
api_key_store,
|
|
75
|
+
path_prefix="/memuron/api-keys",
|
|
76
|
+
)
|
|
77
|
+
application.services["api_keys"] = api_key_store
|
|
78
|
+
application.auth_provider = create_memuron_auth_provider(api_key_store=api_key_store)
|
|
79
|
+
return application
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def close_application(application: ArthaApplication) -> None:
|
|
83
|
+
"""Release resources owned by a short-lived CLI or MCP application."""
|
|
84
|
+
identity = application.services.get("identity")
|
|
85
|
+
close = getattr(identity, "close", None)
|
|
86
|
+
if callable(close):
|
|
87
|
+
close()
|
|
88
|
+
close_postgres_pool(application.engine.store)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def configure_clerk_env() -> None:
|
|
92
|
+
"""Apply sensible Clerk defaults from documented env vars."""
|
|
93
|
+
if not os.environ.get("CLERK_ISSUER") and os.environ.get("NEXT_PUBLIC_CLERK_FRONTEND_API"):
|
|
94
|
+
os.environ["CLERK_ISSUER"] = os.environ["NEXT_PUBLIC_CLERK_FRONTEND_API"].rstrip("/")
|
|
95
|
+
if not os.environ.get("CLERK_ISSUER"):
|
|
96
|
+
os.environ.setdefault(
|
|
97
|
+
"CLERK_ISSUER",
|
|
98
|
+
"https://credible-perch-55.clerk.accounts.dev",
|
|
99
|
+
)
|
|
100
|
+
issuer = os.environ["CLERK_ISSUER"].rstrip("/")
|
|
101
|
+
os.environ.setdefault("CLERK_JWKS_URL", f"{issuer}/.well-known/jwks.json")
|
|
102
|
+
os.environ.setdefault("CLERK_AUTHORIZED_PARTIES", "http://localhost:3000")
|
|
103
|
+
os.environ.setdefault("ARTHA_AUTH_PROVIDER", "clerk_api_key")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Memuron profile manifest built from registered actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
MEMURON_PROFILE_NAME = "memuron"
|
|
8
|
+
|
|
9
|
+
_BASE_MANIFEST: dict[str, Any] = {
|
|
10
|
+
"profile": MEMURON_PROFILE_NAME,
|
|
11
|
+
"description": "Product memory layer: Guardian ingest, rich nodes, collections, links, semantic search.",
|
|
12
|
+
"api_prefix": "/memuron",
|
|
13
|
+
"arthaanu_types": ["memory", "memory_link", "memory_placement"],
|
|
14
|
+
"encoders": ["memory", "memory_link", "memory_placement"],
|
|
15
|
+
"projections": ["memuron_memories", "memuron_links", "memuron_placements", "memuron_fs"],
|
|
16
|
+
"semantic_events": [
|
|
17
|
+
"memory.created",
|
|
18
|
+
"memory.updated",
|
|
19
|
+
"collection.created",
|
|
20
|
+
"link.created",
|
|
21
|
+
"placement.created",
|
|
22
|
+
"delete",
|
|
23
|
+
],
|
|
24
|
+
"event_metadata": [
|
|
25
|
+
"domain_event_type",
|
|
26
|
+
"actor_id",
|
|
27
|
+
"tenant_id",
|
|
28
|
+
"auth_scopes",
|
|
29
|
+
"request_id",
|
|
30
|
+
],
|
|
31
|
+
"agent_tools": [
|
|
32
|
+
{
|
|
33
|
+
"name": "list_spaces",
|
|
34
|
+
"description": "List registered spaces with name, token, description, and default flag.",
|
|
35
|
+
"route": "GET /memuron/spaces",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "read_graph_filesystem_manual",
|
|
39
|
+
"description": "Read this before issuing filesystem commands.",
|
|
40
|
+
"route": "GET /memuron/spaces/query/manual?topic=overview",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "query_graph_filesystem",
|
|
44
|
+
"description": "Execute one command or a | pipeline.",
|
|
45
|
+
"route": "POST /memuron/spaces/query",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def memuron_profile_manifest(application_manifest: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
52
|
+
manifest = dict(_BASE_MANIFEST)
|
|
53
|
+
if application_manifest is not None:
|
|
54
|
+
manifest["actions"] = application_manifest.get("actions", [])
|
|
55
|
+
return manifest
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def memuron_engine_profile_capability(
|
|
59
|
+
application_manifest: dict[str, Any] | None = None,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
manifest = memuron_profile_manifest(application_manifest)
|
|
62
|
+
return {
|
|
63
|
+
"name": manifest["profile"],
|
|
64
|
+
"description": manifest.get("description"),
|
|
65
|
+
"projections": manifest.get("projections", []),
|
|
66
|
+
"arthaanu_types": manifest.get("arthaanu_types", []),
|
|
67
|
+
"actions": manifest.get("actions", []),
|
|
68
|
+
"routes": manifest.get("agent_tools", []),
|
|
69
|
+
"metadata": {
|
|
70
|
+
key: value
|
|
71
|
+
for key, value in manifest.items()
|
|
72
|
+
if key
|
|
73
|
+
not in {
|
|
74
|
+
"profile",
|
|
75
|
+
"description",
|
|
76
|
+
"projections",
|
|
77
|
+
"arthaanu_types",
|
|
78
|
+
"actions",
|
|
79
|
+
"agent_tools",
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Memuron CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from memuron.application.app import close_application, configure_clerk_env, create_application
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
configure_clerk_env()
|
|
14
|
+
os.environ.setdefault("ARTHA_AUTH_PROVIDER", "api_key")
|
|
15
|
+
os.environ.setdefault(
|
|
16
|
+
"ARTHA_CONFIG_PATH",
|
|
17
|
+
str(Path.home() / ".config" / "memuron" / "config.json"),
|
|
18
|
+
)
|
|
19
|
+
if os.environ.get("MEMURON_API_KEY") and not os.environ.get("ARTHA_API_KEY"):
|
|
20
|
+
os.environ["ARTHA_API_KEY"] = os.environ["MEMURON_API_KEY"]
|
|
21
|
+
if os.environ.get("MEMURON_BASE_URL") and not os.environ.get("ARTHA_BASE_URL"):
|
|
22
|
+
os.environ["ARTHA_BASE_URL"] = os.environ["MEMURON_BASE_URL"]
|
|
23
|
+
effective_argv = list(sys.argv[1:] if argv is None else argv)
|
|
24
|
+
help_only = not effective_argv or any(
|
|
25
|
+
item in {"-h", "--help"} for item in effective_argv
|
|
26
|
+
)
|
|
27
|
+
application = create_application(":memory:" if help_only else None)
|
|
28
|
+
try:
|
|
29
|
+
return application.run_cli(argv, prog="memuron")
|
|
30
|
+
finally:
|
|
31
|
+
close_application(application)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
raise SystemExit(main())
|