agent-magnet 0.1.0__tar.gz

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.
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-magnet
3
+ Version: 0.1.0
4
+ Summary: Self-learning memory infrastructure for AI products
5
+ License: MIT
6
+ Keywords: ai,memory,llm,agents,mcp
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: fastapi
10
+ Requires-Dist: redis
11
+ Requires-Dist: litellm
12
+ Requires-Dist: mcp
13
+ Requires-Dist: qdrant-client
14
+ Requires-Dist: neo4j
15
+ Requires-Dist: python-dotenv
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0; extra == "dev"
18
+ Requires-Dist: pytest-asyncio; extra == "dev"
19
+
20
+ # Agent Magnet
21
+
22
+ Self-learning memory infrastructure for AI products.
23
+
24
+ ## Install
25
+ ```
26
+ pip install agent-magnet
27
+ ```
28
+
29
+ ## MCP Setup (Claude Desktop / Cursor)
30
+ Add to your MCP config:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "agent-magnet": {
36
+ "command": "agent-magnet-mcp",
37
+ "env": {
38
+ "MAGNET_REDIS_URL": "your_redis_url",
39
+ "MAGNET_OPENAI_KEY": "your_openai_key"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ## Proxy Setup (any OpenAI-compatible client)
47
+ ```python
48
+ client = OpenAI(
49
+ base_url="https://magnet-gateway.onrender.com/v1",
50
+ api_key="mg_sk_...",
51
+ default_headers={"x-session-id": "user_123"}
52
+ )
53
+ ```
@@ -0,0 +1,34 @@
1
+ # Agent Magnet
2
+
3
+ Self-learning memory infrastructure for AI products.
4
+
5
+ ## Install
6
+ ```
7
+ pip install agent-magnet
8
+ ```
9
+
10
+ ## MCP Setup (Claude Desktop / Cursor)
11
+ Add to your MCP config:
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "agent-magnet": {
17
+ "command": "agent-magnet-mcp",
18
+ "env": {
19
+ "MAGNET_REDIS_URL": "your_redis_url",
20
+ "MAGNET_OPENAI_KEY": "your_openai_key"
21
+ }
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ ## Proxy Setup (any OpenAI-compatible client)
28
+ ```python
29
+ client = OpenAI(
30
+ base_url="https://magnet-gateway.onrender.com/v1",
31
+ api_key="mg_sk_...",
32
+ default_headers={"x-session-id": "user_123"}
33
+ )
34
+ ```
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-magnet
3
+ Version: 0.1.0
4
+ Summary: Self-learning memory infrastructure for AI products
5
+ License: MIT
6
+ Keywords: ai,memory,llm,agents,mcp
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: fastapi
10
+ Requires-Dist: redis
11
+ Requires-Dist: litellm
12
+ Requires-Dist: mcp
13
+ Requires-Dist: qdrant-client
14
+ Requires-Dist: neo4j
15
+ Requires-Dist: python-dotenv
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0; extra == "dev"
18
+ Requires-Dist: pytest-asyncio; extra == "dev"
19
+
20
+ # Agent Magnet
21
+
22
+ Self-learning memory infrastructure for AI products.
23
+
24
+ ## Install
25
+ ```
26
+ pip install agent-magnet
27
+ ```
28
+
29
+ ## MCP Setup (Claude Desktop / Cursor)
30
+ Add to your MCP config:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "agent-magnet": {
36
+ "command": "agent-magnet-mcp",
37
+ "env": {
38
+ "MAGNET_REDIS_URL": "your_redis_url",
39
+ "MAGNET_OPENAI_KEY": "your_openai_key"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ## Proxy Setup (any OpenAI-compatible client)
47
+ ```python
48
+ client = OpenAI(
49
+ base_url="https://magnet-gateway.onrender.com/v1",
50
+ api_key="mg_sk_...",
51
+ default_headers={"x-session-id": "user_123"}
52
+ )
53
+ ```
@@ -0,0 +1,27 @@
1
+ README.md
2
+ pyproject.toml
3
+ agent_magnet.egg-info/PKG-INFO
4
+ agent_magnet.egg-info/SOURCES.txt
5
+ agent_magnet.egg-info/dependency_links.txt
6
+ agent_magnet.egg-info/entry_points.txt
7
+ agent_magnet.egg-info/requires.txt
8
+ agent_magnet.egg-info/top_level.txt
9
+ magnet/__init__.py
10
+ magnet/aggregate_store.py
11
+ magnet/buffer.py
12
+ magnet/classifier.py
13
+ magnet/client.py
14
+ magnet/consolidation.py
15
+ magnet/episodic_store.py
16
+ magnet/knowledge_store.py
17
+ magnet/mcp_server.py
18
+ magnet/memory_orchestrator.py
19
+ magnet/reflector.py
20
+ magnet/router.py
21
+ magnet/signals.py
22
+ magnet/store.py
23
+ tests/test_buffer.py
24
+ tests/test_classifier.py
25
+ tests/test_episodic_store.py
26
+ tests/test_memory_orchestrator.py
27
+ tests/test_router.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-magnet-mcp = magnet.mcp_server:main
@@ -0,0 +1,11 @@
1
+ fastapi
2
+ redis
3
+ litellm
4
+ mcp
5
+ qdrant-client
6
+ neo4j
7
+ python-dotenv
8
+
9
+ [dev]
10
+ pytest>=8.0
11
+ pytest-asyncio
@@ -0,0 +1,22 @@
1
+ from .client import BehavioralMemory
2
+ from .signals import SignalDetector
3
+ from .reflector import Reflector
4
+ from .classifier import IntelligentClassifier, ClassificationResult
5
+ from .router import ModelRouter, RouterDecision
6
+ from .episodic_store import EpisodicStore
7
+ from .knowledge_store import KnowledgeStore
8
+ from .memory_orchestrator import MemoryOrchestrator
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = [
12
+ "BehavioralMemory",
13
+ "SignalDetector",
14
+ "Reflector",
15
+ "IntelligentClassifier",
16
+ "ClassificationResult",
17
+ "ModelRouter",
18
+ "RouterDecision",
19
+ "EpisodicStore",
20
+ "KnowledgeStore",
21
+ "MemoryOrchestrator",
22
+ ]
@@ -0,0 +1,164 @@
1
+ # ═══════════════════════════════════════════════════
2
+ # GDPR/CCPA COMPLIANCE DOCUMENTATION
3
+ # ═══════════════════════════════════════════════════
4
+ # This module processes anonymous data in compliance with GDPR
5
+ # and CCPA standards. It does not contain personal data (PII).
6
+ #
7
+ # Legal Basis: Anonymous Data Processing
8
+ # "Data rendered anonymous in such a way that the data subject
9
+ # is not or no longer identifiable is excluded from GDPR scope."
10
+ #
11
+ # Techniques Applied:
12
+ # 1. K-Anonymity (min_k=5): Patterns unique to a single user
13
+ # are not added to the aggregate pool.
14
+ # 2. Differential Privacy (Laplace, ε=1.0):
15
+ # Mathematical noise is added to query results.
16
+ # 3. Data Minimization: Only signal type, category,
17
+ # dimension, and value are stored.
18
+ # 4. TTL: 90 days — automatic destruction.
19
+ #
20
+ # NEVER enters the Aggregate store:
21
+ # - user_id, session_id, project_id
22
+ # - Message content
23
+ # - Exact timestamps
24
+ # - IP addresses
25
+ # ═══════════════════════════════════════════════════
26
+
27
+ import datetime
28
+ import json
29
+ import numpy as np
30
+ from typing import Any
31
+
32
+ class AggregateSignalStore:
33
+ def __init__(self, redis_client: Any, min_k: int = 5, epsilon: float = 1.0):
34
+ self._redis = redis_client
35
+ self._min_k = min_k # k-anonymity: min 5 distinct records
36
+ self._epsilon = epsilon # differential privacy noise
37
+
38
+ def record(self, signal_type: str, query_category: str, dimension: str, dimension_value: str) -> None:
39
+ """
40
+ Records an anonymous signal in a GDPR-compliant manner.
41
+ Contains no PII — statistical counter only.
42
+ """
43
+ if not self._redis:
44
+ return
45
+
46
+ allowed_signal_types = {
47
+ "correction", "rejection", "preference", "clarification", "positive"
48
+ }
49
+ if signal_type not in allowed_signal_types:
50
+ return
51
+
52
+ allowed_dimensions = {
53
+ "response_length", "detail_level", "tone", "format", "language", "unknown", "heuristic", "llm_extracted"
54
+ }
55
+ if dimension not in allowed_dimensions:
56
+ return
57
+
58
+ # Time bucket (rounded to the hour for privacy, no exact timestamps)
59
+ hour_bucket = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H")
60
+
61
+ # Redis key — NO PII included
62
+ key = f"magnet:agg:{signal_type}:{query_category}:{dimension}:{dimension_value}"
63
+ counter_key = f"magnet:agg:count:{signal_type}:{query_category}"
64
+
65
+ try:
66
+ pipe = self._redis.pipeline()
67
+ pipe.incr(key)
68
+ pipe.incr(counter_key)
69
+ pipe.expire(key, 60 * 60 * 24 * 90) # 90-day TTL
70
+ pipe.expire(counter_key, 60 * 60 * 24 * 90)
71
+ pipe.execute()
72
+ except Exception:
73
+ pass
74
+
75
+ def get_prior(self, query_category: str, dimension: str) -> Any:
76
+ """
77
+ Returns the aggregate prior probability for a new user.
78
+ K-anonymity: Returns None if total records < min_k.
79
+ Differential Privacy: Adds Laplace noise to the results.
80
+ """
81
+ if not self._redis:
82
+ return None
83
+
84
+ pattern = f"magnet:agg:*:{query_category}:{dimension}:*"
85
+ try:
86
+ keys = list(self._redis.scan_iter(pattern))
87
+ if not keys:
88
+ return None
89
+
90
+ counts = {}
91
+ total = 0
92
+ for key in keys:
93
+ value = key.split(":")[-1]
94
+ count = int(self._redis.get(key) or 0)
95
+ counts[value] = count
96
+ total += count
97
+
98
+ if total < self._min_k:
99
+ return None
100
+
101
+ noisy_counts = {}
102
+ for value, count in counts.items():
103
+ noise = np.random.laplace(0, 1.0 / self._epsilon)
104
+ noisy_counts[value] = max(0, count + noise)
105
+
106
+ noisy_total = sum(noisy_counts.values())
107
+ if noisy_total == 0:
108
+ return None
109
+
110
+ return {v: round(c / noisy_total, 3) for v, c in noisy_counts.items()}
111
+ except Exception:
112
+ return None
113
+
114
+ def get_cold_start_injection(self, query_category: str) -> str:
115
+ """Generates the cold start injection context for a new user."""
116
+ if not self._redis:
117
+ return ""
118
+
119
+ lines = []
120
+ for dimension in ["response_length", "detail_level", "tone", "language", "heuristic", "llm_extracted"]:
121
+ prior = self.get_prior(query_category, dimension)
122
+ if prior:
123
+ top_value = max(prior, key=prior.get)
124
+ top_pct = int(prior[top_value] * 100)
125
+ if top_pct >= 55: # Add if there is a strong aggregate signal
126
+ lines.append(f" - {dimension}: {top_value} ({top_pct}% of users)")
127
+
128
+ if not lines:
129
+ return ""
130
+
131
+ return (
132
+ "[Aggregate Prior]\n"
133
+ "Based on anonymized patterns from similar users:\n" +
134
+ "\n".join(lines) + "\n\n"
135
+ "Note: These are statistical suggestions. "
136
+ "User's own behavior takes priority."
137
+ )
138
+
139
+ def store_consolidated_pattern(self, pattern: dict) -> bool:
140
+ """
141
+ Stores a cross-user consolidated pattern from ConsolidationEngine.
142
+ Never overwrites an existing entry that has equal or higher confidence.
143
+ Returns True if written, False if skipped.
144
+ """
145
+ if not self._redis:
146
+ return False
147
+ context = pattern.get("context", "general")
148
+ relation = pattern.get("relation", "unknown")
149
+ subject = pattern.get("subject", "").lower().replace(" ", "_")
150
+ key = f"magnet:consolidated:{context}:{relation}:{subject}"
151
+ try:
152
+ existing_raw = self._redis.get(key)
153
+ if existing_raw:
154
+ existing = json.loads(existing_raw)
155
+ if existing.get("confidence", 0.0) >= pattern.get("confidence", 0.0):
156
+ return False
157
+ self._redis.setex(
158
+ key,
159
+ 60 * 60 * 24 * 90,
160
+ json.dumps(pattern, ensure_ascii=False),
161
+ )
162
+ return True
163
+ except Exception:
164
+ return False
@@ -0,0 +1,78 @@
1
+ """
2
+ Signal Buffer
3
+ -------------
4
+ Accumulates signals in Redis.
5
+ Triggers Reflector when threshold is met.
6
+
7
+ If no Redis, in-memory fallback works (for development).
8
+ """
9
+
10
+ from __future__ import annotations
11
+ import json
12
+ import time
13
+ import logging
14
+ from typing import Any
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _BUFFER_PREFIX = "vmm:signals:"
19
+ _DEFAULT_TTL = 60 * 60 * 24 * 7 # 7 days
20
+ _ATOMIC_FLUSH_SCRIPT = """
21
+ local vals = redis.call('LRANGE', KEYS[1], 0, -1)
22
+ redis.call('DEL', KEYS[1])
23
+ return vals
24
+ """
25
+
26
+
27
+ class SignalBuffer:
28
+ def __init__(
29
+ self,
30
+ redis_client: Any | None = None,
31
+ threshold: int = 5,
32
+ ttl: int = _DEFAULT_TTL,
33
+ ):
34
+ self._redis = redis_client
35
+ self.threshold = threshold
36
+ self.ttl = ttl
37
+ self._memory: dict[str, list[dict]] = {} # fallback
38
+
39
+ def push(self, user_id: str, signals: list[dict]) -> int:
40
+ if not signals:
41
+ return self._count(user_id)
42
+
43
+ key = _BUFFER_PREFIX + user_id
44
+ enriched = [{"ts": time.time(), **s} for s in signals]
45
+
46
+ if self._redis:
47
+ pipe = self._redis.pipeline()
48
+ for sig in enriched:
49
+ pipe.rpush(key, json.dumps(sig))
50
+ pipe.expire(key, self.ttl)
51
+ pipe.execute()
52
+ return self._redis.llen(key)
53
+
54
+ self._memory.setdefault(key, []).extend(enriched)
55
+ return len(self._memory[key])
56
+
57
+ def should_reflect(self, user_id: str) -> bool:
58
+ return self._count(user_id) >= self.threshold
59
+
60
+ def flush(self, user_id: str) -> list[dict]:
61
+ key = _BUFFER_PREFIX + user_id
62
+ if self._redis:
63
+ results = self._redis.eval(_ATOMIC_FLUSH_SCRIPT, 1, key)
64
+ return [json.loads(r) for r in results]
65
+
66
+ return self._memory.pop(key, [])
67
+
68
+ def peek(self, user_id: str) -> list[dict]:
69
+ key = _BUFFER_PREFIX + user_id
70
+ if self._redis:
71
+ return [json.loads(r) for r in self._redis.lrange(key, 0, -1)]
72
+ return list(self._memory.get(key, []))
73
+
74
+ def _count(self, user_id: str) -> int:
75
+ key = _BUFFER_PREFIX + user_id
76
+ if self._redis:
77
+ return self._redis.llen(key)
78
+ return len(self._memory.get(key, []))