memkernel 0.2.0__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.
memkernel/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """MemKernel — Code-aware memory server for AI coding copilots."""
4
+
5
+ __version__ = "0.2.0"
@@ -0,0 +1,2 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
memkernel/api/app.py ADDED
@@ -0,0 +1,64 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """FastAPI admin API factory and entry point.
4
+
5
+ Production:
6
+ uvicorn memkernel.api.app:app --reload
7
+
8
+ Tests inject a pre-built MemoryService via create_app(memory_service=svc).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from contextlib import asynccontextmanager
15
+ from typing import TYPE_CHECKING
16
+
17
+ from fastapi import FastAPI
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import AsyncIterator
21
+
22
+ from memkernel.core.services import MemoryService
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def create_app(memory_service: MemoryService | None = None) -> FastAPI:
28
+ """FastAPI app factory.
29
+
30
+ Tests pass a pre-built MemoryService (with EphemeralClient, MockEmbedding).
31
+ Production calls with None — MemoryService is built from env at startup.
32
+ """
33
+
34
+ @asynccontextmanager
35
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
36
+ if memory_service is not None:
37
+ app.state.memory_service = memory_service
38
+ else:
39
+ app.state.memory_service = _build_service()
40
+ logger.info("MemoryService initialised for project %s", app.state.memory_service.project_id)
41
+ yield
42
+
43
+ application: FastAPI = FastAPI(
44
+ title="MemKernel Admin API",
45
+ version="0.1.0",
46
+ lifespan=lifespan,
47
+ )
48
+
49
+ from memkernel.api.routes.admin import router
50
+
51
+ application.include_router(router)
52
+ return application
53
+
54
+
55
+ def _build_service() -> MemoryService:
56
+ """Construct MemoryService from environment settings (production only)."""
57
+ from memkernel.factory import build_service
58
+
59
+ return build_service()
60
+
61
+
62
+ # Module-level app for `uvicorn memkernel.api.app:app`
63
+ # MemoryService is built lazily in the lifespan — safe to import without real deps.
64
+ app = create_app()
@@ -0,0 +1,2 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,44 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Admin API route handlers: /health, /stats, /collections."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from fastapi import APIRouter, Request
10
+
11
+ if TYPE_CHECKING:
12
+ from memkernel.core.services import MemoryService
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ def _svc(request: Request) -> MemoryService:
18
+ return request.app.state.memory_service # type: ignore[no-any-return]
19
+
20
+
21
+ @router.get("/health")
22
+ async def health(request: Request) -> dict[str, Any]:
23
+ """Server liveness check: status, collection count, memory count."""
24
+ svc = _svc(request)
25
+ return {
26
+ "status": "ok",
27
+ "project_id": svc.project_id,
28
+ "collection_count": svc.collection_count(),
29
+ "memory_count": svc.count(),
30
+ }
31
+
32
+
33
+ @router.get("/stats")
34
+ async def stats(request: Request) -> dict[str, Any]:
35
+ """Memory metrics: per-type breakdown, growth by time window, last recall latency."""
36
+ svc = _svc(request)
37
+ return svc.stats()
38
+
39
+
40
+ @router.get("/collections")
41
+ async def collections(request: Request) -> list[dict[str, Any]]:
42
+ """List all project collections with record counts."""
43
+ svc = _svc(request)
44
+ return svc.list_collections()
memkernel/config.py ADDED
@@ -0,0 +1,56 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """MemKernel configuration via environment variables."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Literal
8
+
9
+ from pydantic_settings import BaseSettings, SettingsConfigDict
10
+
11
+
12
+ class MemKernelSettings(BaseSettings):
13
+ """All settings are configurable via MEMKERNEL_ prefixed env vars."""
14
+
15
+ model_config = SettingsConfigDict(env_prefix="MEMKERNEL_", env_file=".env")
16
+
17
+ project_id: str = "default"
18
+ default_session_id: str = "default"
19
+
20
+ # ChromaDB
21
+ chroma_persist_path: str = ".memkernel/chroma"
22
+ chroma_collection_prefix: str = "memkernel"
23
+
24
+ # Embedding
25
+ embedding_model: str = "all-MiniLM-L6-v2"
26
+ embedding_dimension: int = 384
27
+
28
+ # Retrieval
29
+ search_top_k: int = 5
30
+ rrf_k: int = 60
31
+ preview_length: int = 200
32
+
33
+ # Chunking
34
+ chunk_size: int = 1500 # max chars per chunk
35
+ chunk_overlap: int = 200 # char overlap between token chunks (fallback only)
36
+
37
+ # Lifecycle / decay (see docs/architecture/memory-tiers.md)
38
+ episode_ttl_days: int = 30 # base TTL for short-term `episode` memories
39
+ observation_ttl_days: int = 7 # base TTL for short-term `observation` memories
40
+ task_ttl_hours: int = 24 # base TTL for working-memory `task` records
41
+ access_boost: float = 0.5 # each recall extends TTL by this fraction of the base TTL
42
+ recency_half_life_days: float = 7.0 # recency decay half-life for short-term memories
43
+
44
+ # Recall scoring: final = w_relevance * fused + w_recency * recency + w_importance * importance
45
+ recall_weight_relevance: float = 1.0
46
+ recall_weight_recency: float = 0.3
47
+ recall_weight_importance: float = 0.3
48
+
49
+ # LLM wiki bridge: directory holding the markdown wiki (index.md, decisions/, ...).
50
+ # Empty = no wiki configured; wiki_export/wiki_import then require an explicit path.
51
+ wiki_dir: str = ""
52
+
53
+ # Server
54
+ transport: Literal["stdio", "streamable-http"] = "stdio"
55
+ host: str = "127.0.0.1"
56
+ port: int = 8765
@@ -0,0 +1,2 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,143 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Domain models for MemKernel.
4
+
5
+ Memory is organised in three tiers (see docs/architecture/memory-tiers.md):
6
+
7
+ * ``working`` — the agent's scratchpad for the current task. Session-scoped,
8
+ expires within hours. Cognitive analogue: working memory.
9
+ * ``short_term`` — episodic memory: what happened, what was observed. Subject
10
+ to decay; frequently recalled items survive longer.
11
+ * ``long_term`` — semantic and procedural memory: decisions, conventions,
12
+ facts, procedures, code understanding. Never expires; curated
13
+ through consolidation and (optionally) an LLM wiki.
14
+
15
+ Each :class:`MemoryType` belongs to exactly one tier (``TYPE_TIER``), so callers
16
+ only choose a type; the tier, default importance and decay policy follow.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from datetime import datetime
22
+ from enum import StrEnum
23
+
24
+ from pydantic import BaseModel, Field
25
+
26
+
27
+ class MemoryTier(StrEnum):
28
+ """Lifecycle tier of a memory. Drives decay policy and recall weighting."""
29
+
30
+ WORKING = "working"
31
+ SHORT_TERM = "short_term"
32
+ LONG_TERM = "long_term"
33
+
34
+
35
+ class MemoryType(StrEnum):
36
+ """Classification of stored memories. Determines tier, importance and TTL policy."""
37
+
38
+ # Long-term, semantic
39
+ DECISION = "decision"
40
+ CONVENTION = "convention"
41
+ FACT = "fact"
42
+ CODE = "code"
43
+ # Long-term, procedural
44
+ PROCEDURE = "procedure"
45
+ # Short-term, episodic
46
+ EPISODE = "episode"
47
+ OBSERVATION = "observation"
48
+ # Working memory
49
+ TASK = "task"
50
+
51
+
52
+ TYPE_TIER: dict[MemoryType, MemoryTier] = {
53
+ MemoryType.DECISION: MemoryTier.LONG_TERM,
54
+ MemoryType.CONVENTION: MemoryTier.LONG_TERM,
55
+ MemoryType.FACT: MemoryTier.LONG_TERM,
56
+ MemoryType.CODE: MemoryTier.LONG_TERM,
57
+ MemoryType.PROCEDURE: MemoryTier.LONG_TERM,
58
+ MemoryType.EPISODE: MemoryTier.SHORT_TERM,
59
+ MemoryType.OBSERVATION: MemoryTier.SHORT_TERM,
60
+ MemoryType.TASK: MemoryTier.WORKING,
61
+ }
62
+
63
+ # Default importance (0..1) when the caller does not supply one.
64
+ # Mirrors the "importance" axis of Generative Agents (Park et al., 2023):
65
+ # durable, high-consequence knowledge scores high; raw observations score low.
66
+ DEFAULT_IMPORTANCE: dict[MemoryType, float] = {
67
+ MemoryType.DECISION: 0.9,
68
+ MemoryType.CONVENTION: 0.9,
69
+ MemoryType.PROCEDURE: 0.8,
70
+ MemoryType.FACT: 0.7,
71
+ MemoryType.CODE: 0.6,
72
+ MemoryType.TASK: 0.5,
73
+ MemoryType.EPISODE: 0.4,
74
+ MemoryType.OBSERVATION: 0.3,
75
+ }
76
+
77
+
78
+ def tier_of(memory_type: MemoryType) -> MemoryTier:
79
+ """Return the tier a memory type belongs to."""
80
+ return TYPE_TIER[memory_type]
81
+
82
+
83
+ class Memory(BaseModel):
84
+ """A stored unit of knowledge with full content and metadata."""
85
+
86
+ ref_id: str
87
+ content: str
88
+ type: MemoryType
89
+ project_id: str
90
+ session_id: str
91
+ timestamp: datetime
92
+ source: str = ""
93
+ content_hash: str
94
+ token_count: int
95
+ checkpoint: bool = False
96
+ chunk_refs: list[str] = []
97
+ # Tiered-memory metadata (all optional for backward compatibility with
98
+ # records written by memkernel 0.1.x, which carry none of these).
99
+ tier: MemoryTier = MemoryTier.LONG_TERM
100
+ importance: float = Field(default=0.5, ge=0.0, le=1.0)
101
+ access_count: int = 0
102
+ last_accessed: datetime | None = None
103
+ tags: list[str] = []
104
+ supersedes: str | None = None
105
+ promoted_from: str | None = None
106
+
107
+
108
+ class ArtifactRef(BaseModel):
109
+ """Lightweight reference returned by recall — no full content.
110
+
111
+ Matches spec FR-003: ref_id, preview, score, token_count, type, timestamp.
112
+ No checkpoint field — checkpoint filtering is query-side via checkpoint_only param.
113
+ ``tier`` and ``importance`` are included so the caller can see why a result
114
+ ranked where it did without a second round trip.
115
+ """
116
+
117
+ ref_id: str
118
+ preview: str
119
+ score: float
120
+ token_count: int
121
+ type: MemoryType
122
+ timestamp: datetime
123
+ tier: MemoryTier = MemoryTier.LONG_TERM
124
+ importance: float = 0.5
125
+
126
+
127
+ class SearchResult(BaseModel):
128
+ """Internal pre-fusion search result from a single source."""
129
+
130
+ ref_id: str
131
+ score: float
132
+ rank: int
133
+
134
+
135
+ class SearchQuery(BaseModel):
136
+ """Internal query representation for HybridSearchService."""
137
+
138
+ query_text: str
139
+ type_filter: MemoryType | None = None
140
+ tier_filter: MemoryTier | None = None
141
+ session_id_filter: str | None = None
142
+ checkpoint_only: bool = False
143
+ top_k: int = 5
@@ -0,0 +1,95 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Protocol definitions for MemKernel adapters.
4
+
5
+ Domain logic in core/ depends ONLY on these protocols.
6
+ Concrete implementations live in storage/, retrieval/, etc.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
12
+
13
+ if TYPE_CHECKING:
14
+ from memkernel.core.models import Memory, SearchResult
15
+
16
+
17
+ @runtime_checkable
18
+ class VectorStorePort(Protocol):
19
+ """Dense vector store backend (ChromaDB for MVP)."""
20
+
21
+ def upsert(
22
+ self,
23
+ ref_id: str,
24
+ content: str,
25
+ embedding: list[float],
26
+ metadata: dict[str, Any],
27
+ ) -> None: ...
28
+
29
+ def search_dense(
30
+ self,
31
+ query_embedding: list[float],
32
+ top_k: int,
33
+ where: dict[str, Any] | None = None,
34
+ ) -> list[SearchResult]: ...
35
+
36
+ def get_by_id(self, ref_id: str) -> Memory | None: ...
37
+
38
+ def delete(self, ref_id: str) -> bool: ...
39
+
40
+ def list_all(self, where: dict[str, Any] | None = None) -> list[Memory]: ...
41
+
42
+ def find_by_hash(self, content_hash: str, memory_type: str) -> Memory | None:
43
+ """Find by content_hash + type (dedup key per ADR-5)."""
44
+ ...
45
+
46
+ def update_metadata(self, ref_id: str, metadata: dict[str, Any]) -> None:
47
+ """Fetch-and-merge: only overwrites caller-specified keys."""
48
+ ...
49
+
50
+ def count(self) -> int: ...
51
+
52
+ def list_collections(self) -> list[dict[str, Any]]:
53
+ """List all collections visible to this adapter with their record counts."""
54
+ ...
55
+
56
+
57
+ @runtime_checkable
58
+ class EmbeddingPort(Protocol):
59
+ """Text embedding model."""
60
+
61
+ def embed(self, texts: list[str]) -> list[list[float]]: ...
62
+
63
+ @property
64
+ def dimension(self) -> int: ...
65
+
66
+
67
+ @runtime_checkable
68
+ class SparseIndexPort(Protocol):
69
+ """BM25 sparse search index."""
70
+
71
+ def add(self, ref_id: str, content: str) -> None: ...
72
+
73
+ def remove(self, ref_id: str) -> None: ...
74
+
75
+ def search(self, query: str, top_k: int) -> list[SearchResult]: ...
76
+
77
+ def search_all(self, query: str) -> list[SearchResult]:
78
+ """Score entire corpus, return all results with score > 0."""
79
+ ...
80
+
81
+ def rebuild(self, documents: list[tuple[str, str]]) -> None: ...
82
+
83
+ @property
84
+ def needs_rebuild(self) -> bool: ...
85
+
86
+ def mark_needs_rebuild(self) -> None: ...
87
+
88
+ def __len__(self) -> int: ...
89
+
90
+
91
+ @runtime_checkable
92
+ class ChunkerPort(Protocol):
93
+ """Code/text chunking strategy (stub for future US3)."""
94
+
95
+ def chunk(self, content: str, metadata: dict[str, Any]) -> list[str]: ...