etchmem 0.1.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.
etchmem/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """
2
+ skillmem — a reconsolidating three-tier memory engine for LLM agents.
3
+
4
+ Public surface::
5
+
6
+ from etchmem import Engine
7
+
8
+ engine = Engine()
9
+ engine.remember("text")
10
+ results = engine.recall("query")
11
+ summary = engine.consolidate()
12
+ """
13
+ from etchmem.engine import Engine
14
+ from etchmem.config import Config
15
+ from etchmem.models import SearchResult, Record, InjectedArticle
16
+
17
+ __all__ = [
18
+ "Engine",
19
+ "Config",
20
+ "SearchResult",
21
+ "Record",
22
+ "InjectedArticle",
23
+ ]
24
+
25
+ __version__ = "0.1.0"
etchmem/config.py ADDED
@@ -0,0 +1,109 @@
1
+ """
2
+ skillmem configuration.
3
+
4
+ All settings can be overridden by passing a Config instance to Engine().
5
+ Environment variables are read at import time as defaults.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ @dataclass
14
+ class Config:
15
+ # ── Storage ───────────────────────────────────────────────────────────
16
+ data_dir: str = field(
17
+ default_factory=lambda: os.environ.get("SKILLMEM_DATA_DIR", ".skillmem")
18
+ )
19
+
20
+ # ── Collections ───────────────────────────────────────────────────────
21
+ collection_relational: str = "relational"
22
+ collection_buffer: str = "buffer"
23
+ collection_injected: str = "injected"
24
+
25
+ # ── TTL (seconds) for relational records ─────────────────────────────
26
+ # Default: 7 days
27
+ relational_ttl_seconds: int = field(
28
+ default_factory=lambda: int(
29
+ os.environ.get("SKILLMEM_RELATIONAL_TTL", str(7 * 24 * 3600))
30
+ )
31
+ )
32
+
33
+ # ── Recall ────────────────────────────────────────────────────────────
34
+ recall_top_k_injected: int = 5
35
+ recall_top_k_relational: int = 5
36
+
37
+ # Asymmetric scoring weights
38
+ recall_injected_weight: float = 1.0 # high-trust base
39
+ recall_relational_weight: float = 0.6 # freshness-boosted, confidence-discounted
40
+
41
+ # ── Consolidation thresholds ──────────────────────────────────────────
42
+ # branch_threshold: cosine distance above which a cluster splits into
43
+ # multiple injected articles (distinct experiences → distinct articles).
44
+ # Higher = harder to branch.
45
+ branch_threshold: float = field(
46
+ default_factory=lambda: float(
47
+ os.environ.get("SKILLMEM_BRANCH_THRESHOLD", "0.4")
48
+ )
49
+ )
50
+
51
+ # dedup_delete_threshold: cosine distance below which two injected
52
+ # articles are "the same" → older is superseded.
53
+ # MUST be strictly tighter (lower) than branch_threshold.
54
+ dedup_delete_threshold: float = field(
55
+ default_factory=lambda: float(
56
+ os.environ.get("SKILLMEM_DEDUP_THRESHOLD", "0.15")
57
+ )
58
+ )
59
+
60
+ # ── Affectability buckets ─────────────────────────────────────────────
61
+ affectability_drop_below: float = 0.3 # < this → drop (noise)
62
+ affectability_keep_below: float = 0.7 # 0.3–0.7 → keep in buffer
63
+ affectability_default: float = 0.5 # default when no hint given
64
+
65
+ # ── LLM connector ─────────────────────────────────────────────────────
66
+ llm_provider: str = field(
67
+ default_factory=lambda: os.environ.get("SKILLMEM_LLM_PROVIDER", "auto")
68
+ ) # "openai" | "anthropic" | "auto"
69
+
70
+ openai_api_key: str | None = field(
71
+ default_factory=lambda: os.environ.get("OPENAI_API_KEY")
72
+ )
73
+ openai_model: str = field(
74
+ default_factory=lambda: os.environ.get("SKILLMEM_OPENAI_MODEL", "gpt-4o-mini")
75
+ )
76
+
77
+ anthropic_api_key: str | None = field(
78
+ default_factory=lambda: os.environ.get("ANTHROPIC_API_KEY")
79
+ )
80
+ anthropic_model: str = field(
81
+ default_factory=lambda: os.environ.get(
82
+ "SKILLMEM_ANTHROPIC_MODEL", "claude-haiku-4-5-20251001"
83
+ )
84
+ )
85
+
86
+ # ── Consolidation ─────────────────────────────────────────────────────
87
+ consolidate_default_num_records: int | str = "all"
88
+ consolidate_default_method: str = "LIFO"
89
+
90
+ # ── Embedding function ─────────────────────────────────────────────────
91
+ # None (default) → Chroma's bundled ONNX model (downloads on first use).
92
+ # "hash" → offline deterministic hash embedding (tests/CI only).
93
+ # Any chromadb EmbeddingFunction subclass → custom.
94
+ embedding_function: object = None
95
+
96
+ # Minimum cluster size before promoting (orphaned records use
97
+ # affectability; clustered records bypass this gate).
98
+ min_cluster_size: int = 2
99
+
100
+ def __post_init__(self) -> None:
101
+ if self.dedup_delete_threshold >= self.branch_threshold:
102
+ raise ValueError(
103
+ f"dedup_delete_threshold ({self.dedup_delete_threshold}) must be "
104
+ f"strictly less than branch_threshold ({self.branch_threshold})"
105
+ )
106
+
107
+
108
+ # Module-level default — override by passing Config() to Engine.
109
+ DEFAULT_CONFIG = Config()
File without changes
@@ -0,0 +1,35 @@
1
+ """
2
+ LLMConnector abstract base class.
3
+
4
+ The connector's ONLY job is article synthesis — generative completion.
5
+ Embeddings are always Chroma-native and never go through this connector.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+
11
+
12
+ class LLMConnector(ABC):
13
+ """
14
+ Single-method interface for generative synthesis.
15
+
16
+ Used in exactly one place in the worker:
17
+ - Reconsolidation-with-new-signal: rewrite an existing injected article
18
+ blended with fresh context.
19
+
20
+ All other worker operations (clustering, affectability arithmetic,
21
+ the reconsolidation gate) are LLM-free.
22
+ """
23
+
24
+ @abstractmethod
25
+ def complete(self, prompt: str, **opts) -> str:
26
+ """
27
+ Run a completion against the configured LLM.
28
+
29
+ Args:
30
+ prompt: The full synthesis prompt (already assembled by the worker).
31
+ **opts: Optional provider-specific overrides (max_tokens, temperature…).
32
+
33
+ Returns:
34
+ The generated text string.
35
+ """
@@ -0,0 +1,121 @@
1
+ """
2
+ Remote LLM connector — default synthesis backend.
3
+
4
+ Supports OpenAI and Anthropic. Auto-detects available keys.
5
+ Provider resolution order when llm_provider == "auto":
6
+ 1. Use OpenAI if OPENAI_API_KEY is set.
7
+ 2. Fall back to Anthropic if ANTHROPIC_API_KEY is set.
8
+ 3. Raise RuntimeError if neither key is configured.
9
+
10
+ Models (from README):
11
+ OpenAI: gpt-4o-mini (default; gpt-5.4-nano when available)
12
+ Anthropic: claude-haiku-4-5-20251001
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from typing import Any
18
+
19
+ from etchmem.connector.base import LLMConnector
20
+
21
+
22
+ class RemoteConnector(LLMConnector):
23
+ """
24
+ API-backed connector for OpenAI and Anthropic.
25
+
26
+ The connector is intentionally thin — it wraps one method (complete)
27
+ and delegates everything else to the provider SDK.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ provider: str = "auto", # "openai" | "anthropic" | "auto"
33
+ openai_api_key: str | None = None,
34
+ openai_model: str = "gpt-4o-mini",
35
+ anthropic_api_key: str | None = None,
36
+ anthropic_model: str = "claude-haiku-4-5-20251001",
37
+ ) -> None:
38
+ self._openai_key = openai_api_key
39
+ self._openai_model = openai_model
40
+ self._anthropic_key = anthropic_api_key
41
+ self._anthropic_model = anthropic_model
42
+ self._provider = self._resolve_provider(provider)
43
+
44
+ def _resolve_provider(self, requested: str) -> str:
45
+ if requested == "auto":
46
+ if self._openai_key:
47
+ return "openai"
48
+ if self._anthropic_key:
49
+ return "anthropic"
50
+ raise RuntimeError(
51
+ "skillmem: no LLM API key configured. "
52
+ "Set OPENAI_API_KEY or ANTHROPIC_API_KEY (or pass them to Config)."
53
+ )
54
+ if requested == "openai" and not self._openai_key:
55
+ raise RuntimeError(
56
+ "skillmem: llm_provider='openai' but OPENAI_API_KEY is not set."
57
+ )
58
+ if requested == "anthropic" and not self._anthropic_key:
59
+ raise RuntimeError(
60
+ "skillmem: llm_provider='anthropic' but ANTHROPIC_API_KEY is not set."
61
+ )
62
+ return requested
63
+
64
+ def complete(self, prompt: str, **opts: Any) -> str:
65
+ """Run synthesis completion against the configured provider."""
66
+ if self._provider == "openai":
67
+ return self._complete_openai(prompt, **opts)
68
+ return self._complete_anthropic(prompt, **opts)
69
+
70
+ def _complete_openai(self, prompt: str, **opts: Any) -> str:
71
+ try:
72
+ from openai import OpenAI # type: ignore[import]
73
+ except ImportError as e:
74
+ raise ImportError("skillmem: 'openai' package is not installed.") from e
75
+
76
+ client = OpenAI(api_key=self._openai_key)
77
+ max_tokens = opts.pop("max_tokens", 2048)
78
+ temperature = opts.pop("temperature", 0.3)
79
+
80
+ response = client.chat.completions.create(
81
+ model=self._openai_model,
82
+ messages=[{"role": "user", "content": prompt}],
83
+ max_tokens=max_tokens,
84
+ temperature=temperature,
85
+ **opts,
86
+ )
87
+ return response.choices[0].message.content or ""
88
+
89
+ def _complete_anthropic(self, prompt: str, **opts: Any) -> str:
90
+ try:
91
+ import anthropic # type: ignore[import]
92
+ except ImportError as e:
93
+ raise ImportError("skillmem: 'anthropic' package is not installed.") from e
94
+
95
+ client = anthropic.Anthropic(api_key=self._anthropic_key)
96
+ max_tokens = opts.pop("max_tokens", 2048)
97
+
98
+ message = client.messages.create(
99
+ model=self._anthropic_model,
100
+ max_tokens=max_tokens,
101
+ messages=[{"role": "user", "content": prompt}],
102
+ **opts,
103
+ )
104
+ # Extract text from the first text block
105
+ for block in message.content:
106
+ if hasattr(block, "text"):
107
+ return block.text
108
+ return ""
109
+
110
+
111
+ # ── Convenience factory ───────────────────────────────────────────────────────
112
+
113
+ def build_connector(config: "Any") -> RemoteConnector:
114
+ """Build a RemoteConnector from a Config object."""
115
+ return RemoteConnector(
116
+ provider=config.llm_provider,
117
+ openai_api_key=config.openai_api_key,
118
+ openai_model=config.openai_model,
119
+ anthropic_api_key=config.anthropic_api_key,
120
+ anthropic_model=config.anthropic_model,
121
+ )
File without changes