memoryagent-lib 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.
- memoryagent/__init__.py +35 -0
- memoryagent/confidence.py +82 -0
- memoryagent/config.py +35 -0
- memoryagent/consolidation.py +5 -0
- memoryagent/examples/export_memory.py +110 -0
- memoryagent/examples/memory_api_server.py +223 -0
- memoryagent/examples/minimal.py +47 -0
- memoryagent/examples/openai_agent.py +137 -0
- memoryagent/indexers.py +61 -0
- memoryagent/models.py +156 -0
- memoryagent/policy.py +171 -0
- memoryagent/retrieval.py +154 -0
- memoryagent/storage/base.py +86 -0
- memoryagent/storage/in_memory.py +88 -0
- memoryagent/storage/local_disk.py +415 -0
- memoryagent/system.py +182 -0
- memoryagent/utils.py +35 -0
- memoryagent/workers.py +169 -0
- memoryagent_lib-0.1.1.dist-info/METADATA +186 -0
- memoryagent_lib-0.1.1.dist-info/RECORD +22 -0
- memoryagent_lib-0.1.1.dist-info/WHEEL +5 -0
- memoryagent_lib-0.1.1.dist-info/top_level.txt +1 -0
memoryagent/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from memoryagent.config import MemorySystemConfig
|
|
2
|
+
from memoryagent.models import (
|
|
3
|
+
ConfidenceReport,
|
|
4
|
+
MemoryBlock,
|
|
5
|
+
MemoryEvent,
|
|
6
|
+
MemoryItem,
|
|
7
|
+
MemoryQuery,
|
|
8
|
+
MemoryType,
|
|
9
|
+
RetrievalPlan,
|
|
10
|
+
)
|
|
11
|
+
from memoryagent.policy import (
|
|
12
|
+
ConversationMemoryPolicy,
|
|
13
|
+
HeuristicMemoryPolicy,
|
|
14
|
+
MemoryDecision,
|
|
15
|
+
MemoryRoutingPolicy,
|
|
16
|
+
RoutingDecision,
|
|
17
|
+
)
|
|
18
|
+
from memoryagent.system import MemorySystem
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"MemorySystem",
|
|
22
|
+
"MemorySystemConfig",
|
|
23
|
+
"MemoryEvent",
|
|
24
|
+
"MemoryItem",
|
|
25
|
+
"MemoryQuery",
|
|
26
|
+
"MemoryType",
|
|
27
|
+
"MemoryBlock",
|
|
28
|
+
"RetrievalPlan",
|
|
29
|
+
"ConfidenceReport",
|
|
30
|
+
"ConversationMemoryPolicy",
|
|
31
|
+
"HeuristicMemoryPolicy",
|
|
32
|
+
"MemoryDecision",
|
|
33
|
+
"MemoryRoutingPolicy",
|
|
34
|
+
"RoutingDecision",
|
|
35
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from memoryagent.models import ConfidenceReport, MemoryQuery, ScoredMemory
|
|
7
|
+
from memoryagent.utils import clamp, safe_div, unique_tokens
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _semantic_relevance(results: List[ScoredMemory]) -> float:
|
|
11
|
+
if not results:
|
|
12
|
+
return 0.0
|
|
13
|
+
top_scores = [r.score for r in results[:5]]
|
|
14
|
+
return sum(top_scores) / len(top_scores)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _coverage(query: MemoryQuery, results: List[ScoredMemory]) -> float:
|
|
18
|
+
query_tokens = unique_tokens(query.text)
|
|
19
|
+
if not query_tokens:
|
|
20
|
+
return 0.0
|
|
21
|
+
covered = set()
|
|
22
|
+
for item in results[:5]:
|
|
23
|
+
covered |= unique_tokens(item.item.text())
|
|
24
|
+
return safe_div(len(query_tokens & covered), len(query_tokens))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _temporal_fit(results: List[ScoredMemory]) -> float:
|
|
28
|
+
if not results:
|
|
29
|
+
return 0.0
|
|
30
|
+
now = datetime.now(timezone.utc)
|
|
31
|
+
scores = []
|
|
32
|
+
for item in results[:5]:
|
|
33
|
+
age_days = max(0.0, (now - item.item.created_at).total_seconds() / 86400)
|
|
34
|
+
scores.append(1.0 / (1.0 + age_days))
|
|
35
|
+
return sum(scores) / len(scores)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _authority(results: List[ScoredMemory]) -> float:
|
|
39
|
+
if not results:
|
|
40
|
+
return 0.0
|
|
41
|
+
scores = [0.5 * r.item.authority + 0.5 * r.item.stability for r in results[:5]]
|
|
42
|
+
return sum(scores) / len(scores)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _consistency(results: List[ScoredMemory]) -> float:
|
|
46
|
+
if len(results) < 2:
|
|
47
|
+
return 0.5
|
|
48
|
+
tag_sets = [set(r.item.tags) for r in results[:5] if r.item.tags]
|
|
49
|
+
if not tag_sets:
|
|
50
|
+
return 0.4
|
|
51
|
+
overlap = set.intersection(*tag_sets) if len(tag_sets) > 1 else tag_sets[0]
|
|
52
|
+
union = set.union(*tag_sets) if len(tag_sets) > 1 else tag_sets[0]
|
|
53
|
+
return safe_div(len(overlap), len(union))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def evaluate_confidence(query: MemoryQuery, results: List[ScoredMemory]) -> ConfidenceReport:
|
|
57
|
+
semantic = _semantic_relevance(results)
|
|
58
|
+
coverage = _coverage(query, results)
|
|
59
|
+
temporal = _temporal_fit(results)
|
|
60
|
+
authority = _authority(results)
|
|
61
|
+
consistency = _consistency(results)
|
|
62
|
+
|
|
63
|
+
total = clamp(0.35 * semantic + 0.2 * coverage + 0.2 * temporal + 0.15 * authority + 0.1 * consistency)
|
|
64
|
+
|
|
65
|
+
if total >= 0.75:
|
|
66
|
+
recommendation = "accept"
|
|
67
|
+
elif total >= 0.6:
|
|
68
|
+
recommendation = "escalate_archive"
|
|
69
|
+
elif total >= 0.45:
|
|
70
|
+
recommendation = "fetch_cold"
|
|
71
|
+
else:
|
|
72
|
+
recommendation = "uncertain"
|
|
73
|
+
|
|
74
|
+
return ConfidenceReport(
|
|
75
|
+
total=total,
|
|
76
|
+
semantic_relevance=semantic,
|
|
77
|
+
coverage=coverage,
|
|
78
|
+
temporal_fit=temporal,
|
|
79
|
+
authority=authority,
|
|
80
|
+
consistency=consistency,
|
|
81
|
+
recommendation=recommendation,
|
|
82
|
+
)
|
memoryagent/config.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from memoryagent.models import RetrievalPlan
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConsolidationConfig(BaseModel):
|
|
12
|
+
archive_on_flush: bool = True
|
|
13
|
+
semantic_min_count: int = 2
|
|
14
|
+
perceptual_summary_limit: int = 5
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MemorySystemConfig(BaseModel):
|
|
18
|
+
"""System-wide configuration with sane local defaults."""
|
|
19
|
+
|
|
20
|
+
working_ttl_seconds: int = 3600
|
|
21
|
+
retrieval_plan: RetrievalPlan = Field(default_factory=RetrievalPlan)
|
|
22
|
+
consolidation: ConsolidationConfig = Field(default_factory=ConsolidationConfig)
|
|
23
|
+
cold_store_path: Path = Field(default_factory=lambda: Path(".memoryagent_cold"))
|
|
24
|
+
metadata_db_path: Path = Field(default_factory=lambda: Path(".memoryagent_hot.sqlite"))
|
|
25
|
+
feature_db_path: Path = Field(default_factory=lambda: Path(".memoryagent_features.sqlite"))
|
|
26
|
+
vector_db_path: Path = Field(default_factory=lambda: Path(".memoryagent_vectors.sqlite"))
|
|
27
|
+
vector_dim: int = 384
|
|
28
|
+
use_sqlite_vec: bool = False
|
|
29
|
+
sqlite_vec_extension_path: Optional[Path] = None
|
|
30
|
+
archive_index_path: Optional[Path] = None
|
|
31
|
+
|
|
32
|
+
def resolved_archive_path(self) -> Path:
|
|
33
|
+
if self.archive_index_path is not None:
|
|
34
|
+
return self.archive_index_path
|
|
35
|
+
return self.cold_store_path / "archive_index.json"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
9
|
+
COLD_ROOT = ROOT / ".memoryagent_cold"
|
|
10
|
+
HOT_DB = ROOT / ".memoryagent_hot.sqlite"
|
|
11
|
+
FEATURE_DB = ROOT / ".memoryagent_features.sqlite"
|
|
12
|
+
ARCHIVE_INDEX = COLD_ROOT / "archive_index.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_hot() -> List[Dict[str, Any]]:
|
|
16
|
+
if not HOT_DB.exists():
|
|
17
|
+
return []
|
|
18
|
+
with sqlite3.connect(HOT_DB) as conn:
|
|
19
|
+
rows = conn.execute(
|
|
20
|
+
"SELECT id, type, owner, summary, content_json, tags_json, created_at, updated_at, last_accessed, tier, pointer_json, ttl_seconds, confidence, authority, stability FROM memory_items"
|
|
21
|
+
).fetchall()
|
|
22
|
+
items = []
|
|
23
|
+
for row in rows:
|
|
24
|
+
(
|
|
25
|
+
item_id,
|
|
26
|
+
item_type,
|
|
27
|
+
owner,
|
|
28
|
+
summary,
|
|
29
|
+
content_json,
|
|
30
|
+
tags_json,
|
|
31
|
+
created_at,
|
|
32
|
+
updated_at,
|
|
33
|
+
last_accessed,
|
|
34
|
+
tier,
|
|
35
|
+
pointer_json,
|
|
36
|
+
ttl_seconds,
|
|
37
|
+
confidence,
|
|
38
|
+
authority,
|
|
39
|
+
stability,
|
|
40
|
+
) = row
|
|
41
|
+
items.append(
|
|
42
|
+
{
|
|
43
|
+
"id": item_id,
|
|
44
|
+
"type": item_type,
|
|
45
|
+
"owner": owner,
|
|
46
|
+
"summary": summary,
|
|
47
|
+
"content": json.loads(content_json) if content_json else None,
|
|
48
|
+
"tags": json.loads(tags_json) if tags_json else [],
|
|
49
|
+
"created_at": created_at,
|
|
50
|
+
"updated_at": updated_at,
|
|
51
|
+
"last_accessed": last_accessed,
|
|
52
|
+
"tier": tier,
|
|
53
|
+
"pointer": json.loads(pointer_json) if pointer_json else {},
|
|
54
|
+
"ttl_seconds": ttl_seconds,
|
|
55
|
+
"confidence": confidence,
|
|
56
|
+
"authority": authority,
|
|
57
|
+
"stability": stability,
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
return items
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_features() -> List[Dict[str, Any]]:
|
|
64
|
+
if not FEATURE_DB.exists():
|
|
65
|
+
return []
|
|
66
|
+
with sqlite3.connect(FEATURE_DB) as conn:
|
|
67
|
+
rows = conn.execute("SELECT owner, created_at, payload_json FROM features").fetchall()
|
|
68
|
+
features = []
|
|
69
|
+
for owner, created_at, payload_json in rows:
|
|
70
|
+
features.append(
|
|
71
|
+
{
|
|
72
|
+
"owner": owner,
|
|
73
|
+
"created_at": created_at,
|
|
74
|
+
"payload": json.loads(payload_json),
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
return features
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_cold_records() -> List[Dict[str, Any]]:
|
|
81
|
+
if not COLD_ROOT.exists():
|
|
82
|
+
return []
|
|
83
|
+
records = []
|
|
84
|
+
records_root = COLD_ROOT / "records"
|
|
85
|
+
if not records_root.exists():
|
|
86
|
+
return []
|
|
87
|
+
for path in records_root.rglob("*.json"):
|
|
88
|
+
try:
|
|
89
|
+
records.append({"path": str(path.relative_to(ROOT)), "payload": json.loads(path.read_text())})
|
|
90
|
+
except Exception:
|
|
91
|
+
continue
|
|
92
|
+
return records
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def load_archive_index() -> Dict[str, Any]:
|
|
96
|
+
if not ARCHIVE_INDEX.exists():
|
|
97
|
+
return {}
|
|
98
|
+
return json.loads(ARCHIVE_INDEX.read_text())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_memory_payload() -> Dict[str, Any]:
|
|
102
|
+
return {
|
|
103
|
+
"hot_items": load_hot(),
|
|
104
|
+
"features": load_features(),
|
|
105
|
+
"cold_records": load_cold_records(),
|
|
106
|
+
"archive_index": load_archive_index(),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = ["get_memory_payload"]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from memoryagent.examples.export_memory import get_memory_payload
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from memoryagent import (
|
|
12
|
+
HeuristicMemoryPolicy,
|
|
13
|
+
MemoryItem,
|
|
14
|
+
MemoryRoutingPolicy,
|
|
15
|
+
MemorySystem,
|
|
16
|
+
MemorySystemConfig,
|
|
17
|
+
MemoryType,
|
|
18
|
+
)
|
|
19
|
+
from memoryagent.utils import hash_embed, tokenize
|
|
20
|
+
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
|
|
23
|
+
load_dotenv()
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from openai import OpenAI
|
|
27
|
+
except Exception:
|
|
28
|
+
OpenAI = None
|
|
29
|
+
|
|
30
|
+
_history = {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_openai_client():
|
|
34
|
+
if OpenAI is None:
|
|
35
|
+
raise RuntimeError("openai package is not installed")
|
|
36
|
+
return OpenAI()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _openai_embedder(client, model: str, dim: int):
|
|
40
|
+
def _embed(text: str):
|
|
41
|
+
try:
|
|
42
|
+
response = client.embeddings.create(model=model, input=text)
|
|
43
|
+
return response.data[0].embedding
|
|
44
|
+
except Exception:
|
|
45
|
+
return hash_embed(text, dim)
|
|
46
|
+
|
|
47
|
+
return _embed
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _history_entry_text(entry) -> str:
|
|
51
|
+
if isinstance(entry, str):
|
|
52
|
+
return entry
|
|
53
|
+
if isinstance(entry, dict):
|
|
54
|
+
if "user" in entry and "assistant" in entry:
|
|
55
|
+
return f"User: {entry['user']} Assistant: {entry['assistant']}"
|
|
56
|
+
if "role" in entry and "text" in entry:
|
|
57
|
+
return f"{entry['role']}: {entry['text']}"
|
|
58
|
+
return str(entry)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_memory_system():
|
|
62
|
+
model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
|
63
|
+
embedding_model = os.environ.get("OPENAI_EMBED_MODEL", "text-embedding-3-small")
|
|
64
|
+
vector_dim = int(os.environ.get("OPENAI_EMBED_DIM", "1536"))
|
|
65
|
+
|
|
66
|
+
config = MemorySystemConfig(
|
|
67
|
+
use_sqlite_vec=True,
|
|
68
|
+
vector_dim=vector_dim,
|
|
69
|
+
sqlite_vec_extension_path=os.environ.get("SQLITE_VEC_PATH"),
|
|
70
|
+
)
|
|
71
|
+
client = _get_openai_client()
|
|
72
|
+
memory = MemorySystem(
|
|
73
|
+
config=config,
|
|
74
|
+
embedding_fn=_openai_embedder(client, embedding_model, config.vector_dim),
|
|
75
|
+
)
|
|
76
|
+
return memory, client, model
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MemoryAPIHandler(BaseHTTPRequestHandler):
|
|
80
|
+
def _send_json(self, payload, status=200):
|
|
81
|
+
data = json.dumps(payload, ensure_ascii=True).encode("utf-8")
|
|
82
|
+
self.send_response(status)
|
|
83
|
+
self.send_header("Content-Type", "application/json")
|
|
84
|
+
self.send_header("Content-Length", str(len(data)))
|
|
85
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
86
|
+
self.end_headers()
|
|
87
|
+
self.wfile.write(data)
|
|
88
|
+
|
|
89
|
+
def do_GET(self):
|
|
90
|
+
if self.path.startswith("/api/memory"):
|
|
91
|
+
payload = get_memory_payload()
|
|
92
|
+
owner = None
|
|
93
|
+
if "?" in self.path:
|
|
94
|
+
_, query = self.path.split("?", 1)
|
|
95
|
+
for part in query.split("&"):
|
|
96
|
+
if part.startswith("owner="):
|
|
97
|
+
owner = part.split("=", 1)[1]
|
|
98
|
+
break
|
|
99
|
+
if owner:
|
|
100
|
+
payload["hot_items"] = [item for item in payload.get("hot_items", []) if item.get("owner") == owner]
|
|
101
|
+
payload["features"] = [
|
|
102
|
+
item for item in payload.get("features", []) if item.get("owner") == owner
|
|
103
|
+
]
|
|
104
|
+
payload["cold_records"] = [
|
|
105
|
+
item for item in payload.get("cold_records", [])
|
|
106
|
+
if f"/{owner}/" in item.get("path", "")
|
|
107
|
+
]
|
|
108
|
+
self._send_json(payload)
|
|
109
|
+
return
|
|
110
|
+
if self.path in {"/", "/memory_viz.html"}:
|
|
111
|
+
html_path = Path(__file__).resolve().parent / "memory_viz.html"
|
|
112
|
+
if not html_path.exists():
|
|
113
|
+
self.send_error(404, "memory_viz.html not found")
|
|
114
|
+
return
|
|
115
|
+
data = html_path.read_bytes()
|
|
116
|
+
self.send_response(200)
|
|
117
|
+
self.send_header("Content-Type", "text/html")
|
|
118
|
+
self.send_header("Content-Length", str(len(data)))
|
|
119
|
+
self.end_headers()
|
|
120
|
+
self.wfile.write(data)
|
|
121
|
+
return
|
|
122
|
+
self.send_error(404, "Not found")
|
|
123
|
+
|
|
124
|
+
def do_POST(self):
|
|
125
|
+
if self.path not in {"/api/chat", "/api/chat/"}:
|
|
126
|
+
self.send_error(404, "Not found")
|
|
127
|
+
return
|
|
128
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
129
|
+
body = self.rfile.read(length).decode("utf-8") if length else "{}"
|
|
130
|
+
try:
|
|
131
|
+
payload = json.loads(body)
|
|
132
|
+
except Exception:
|
|
133
|
+
self.send_error(400, "Invalid JSON")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
owner = payload.get("owner", "user-001")
|
|
137
|
+
message = payload.get("message", "").strip()
|
|
138
|
+
if not message:
|
|
139
|
+
self.send_error(400, "Missing message")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
memory, client, model = _get_memory_system()
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
self.send_error(500, str(exc))
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
session = _history.setdefault(owner, {"turns": [], "working_id": str(uuid4())})
|
|
149
|
+
history = session["turns"]
|
|
150
|
+
bundle = memory.retrieve(message, owner=owner)
|
|
151
|
+
context_blocks = []
|
|
152
|
+
token_budget = memory.config.retrieval_plan.max_context_tokens
|
|
153
|
+
used_tokens = 0
|
|
154
|
+
for block in bundle.blocks:
|
|
155
|
+
block_text = f"- [{block.memory_type}] {block.text}"
|
|
156
|
+
if not isinstance(block_text, str):
|
|
157
|
+
block_text = str(block_text)
|
|
158
|
+
block_tokens = len(tokenize(block_text))
|
|
159
|
+
if used_tokens + block_tokens > token_budget:
|
|
160
|
+
break
|
|
161
|
+
context_blocks.append(block_text)
|
|
162
|
+
used_tokens += block_tokens
|
|
163
|
+
memory_context = "\n".join(str(item) for item in context_blocks) if context_blocks else "None."
|
|
164
|
+
recent_turns = history[-6:]
|
|
165
|
+
history_text_entries = [_history_entry_text(entry) for entry in recent_turns]
|
|
166
|
+
history_text = "\n".join(history_text_entries) if history_text_entries else "None."
|
|
167
|
+
prompt = (
|
|
168
|
+
"You are a helpful assistant.\n"
|
|
169
|
+
"Use the following memory context and recent chat history if relevant.\n"
|
|
170
|
+
f"Memory context:\n{memory_context}\n\n"
|
|
171
|
+
f"Recent chat:\n{history_text}\n\n"
|
|
172
|
+
f"User: {message}\n"
|
|
173
|
+
"Assistant:"
|
|
174
|
+
)
|
|
175
|
+
response = client.responses.create(model=model, input=prompt)
|
|
176
|
+
assistant_message = response.output_text
|
|
177
|
+
|
|
178
|
+
history.append({"user": message, "assistant": assistant_message})
|
|
179
|
+
|
|
180
|
+
working_item = MemoryItem(
|
|
181
|
+
id=session["working_id"],
|
|
182
|
+
type=MemoryType.WORKING,
|
|
183
|
+
owner=owner,
|
|
184
|
+
summary=f"Session transcript ({len(history)} turns)",
|
|
185
|
+
content={"turns": history},
|
|
186
|
+
tags=["conversation", "session-log"],
|
|
187
|
+
ttl_seconds=memory.config.working_ttl_seconds,
|
|
188
|
+
confidence=0.6,
|
|
189
|
+
)
|
|
190
|
+
memory.write(working_item)
|
|
191
|
+
|
|
192
|
+
policy = HeuristicMemoryPolicy()
|
|
193
|
+
routing_policy = MemoryRoutingPolicy()
|
|
194
|
+
history_for_policy = [_history_entry_text(entry) for entry in history]
|
|
195
|
+
decision = policy.should_store(owner, history_for_policy, message, assistant_message)
|
|
196
|
+
event = policy.to_event(owner, decision)
|
|
197
|
+
if event:
|
|
198
|
+
routing = routing_policy.route(event.to_item())
|
|
199
|
+
if routing.write_hot or routing.write_vector or routing.write_features:
|
|
200
|
+
memory.write(event)
|
|
201
|
+
|
|
202
|
+
self._send_json(
|
|
203
|
+
{
|
|
204
|
+
"reply": assistant_message,
|
|
205
|
+
"trace": {
|
|
206
|
+
"steps": bundle.trace.steps,
|
|
207
|
+
"escalations": bundle.trace.escalations,
|
|
208
|
+
"sources": bundle.trace.sources,
|
|
209
|
+
"confidence": bundle.confidence.total,
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def main() -> None:
|
|
216
|
+
server = HTTPServer(("127.0.0.1", 8000), MemoryAPIHandler)
|
|
217
|
+
print("Serving memory API at http://127.0.0.1:8000/api/memory")
|
|
218
|
+
print("Open http://127.0.0.1:8000/memory_viz.html")
|
|
219
|
+
server.serve_forever()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from memoryagent import MemoryEvent, MemorySystem
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main() -> None:
|
|
5
|
+
memory = MemorySystem()
|
|
6
|
+
|
|
7
|
+
owner = "session-123"
|
|
8
|
+
memory.write(
|
|
9
|
+
MemoryEvent(
|
|
10
|
+
content="User prefers concise summaries about climate policy.",
|
|
11
|
+
type="semantic",
|
|
12
|
+
owner=owner,
|
|
13
|
+
tags=["preference", "summary"],
|
|
14
|
+
confidence=0.7,
|
|
15
|
+
stability=0.8,
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
memory.write(
|
|
20
|
+
MemoryEvent(
|
|
21
|
+
content="Discussed EU carbon border adjustment mechanism.",
|
|
22
|
+
type="episodic",
|
|
23
|
+
owner=owner,
|
|
24
|
+
tags=["eu", "policy"],
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
memory.write_perceptual(
|
|
29
|
+
{
|
|
30
|
+
"content": "Audio signal indicates frustration when asked about timelines.",
|
|
31
|
+
"owner": owner,
|
|
32
|
+
"tags": ["sentiment", "frustration"],
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
bundle = memory.retrieve("What policy topics did we cover?", owner=owner)
|
|
37
|
+
|
|
38
|
+
print("Confidence:", bundle.confidence.total)
|
|
39
|
+
print("Blocks:")
|
|
40
|
+
for block in bundle.blocks:
|
|
41
|
+
print(f"- [{block.memory_type}] {block.text}")
|
|
42
|
+
|
|
43
|
+
memory.flush(owner)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from openai import OpenAI
|
|
7
|
+
|
|
8
|
+
from memoryagent import (
|
|
9
|
+
HeuristicMemoryPolicy,
|
|
10
|
+
MemoryEvent,
|
|
11
|
+
MemoryItem,
|
|
12
|
+
MemoryRoutingPolicy,
|
|
13
|
+
MemorySystem,
|
|
14
|
+
MemorySystemConfig,
|
|
15
|
+
MemoryType,
|
|
16
|
+
)
|
|
17
|
+
from memoryagent.utils import hash_embed, tokenize
|
|
18
|
+
from dotenv import load_dotenv
|
|
19
|
+
|
|
20
|
+
load_dotenv()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def openai_embedder(client: OpenAI, model: str, dim: int):
|
|
24
|
+
def _embed(text: str):
|
|
25
|
+
try:
|
|
26
|
+
response = client.embeddings.create(model=model, input=text)
|
|
27
|
+
return response.data[0].embedding
|
|
28
|
+
except Exception:
|
|
29
|
+
return hash_embed(text, dim)
|
|
30
|
+
|
|
31
|
+
return _embed
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> None:
|
|
35
|
+
# Requires: pip install openai sqlite-vec (or set SQLITE_VEC_PATH)
|
|
36
|
+
client = OpenAI()
|
|
37
|
+
|
|
38
|
+
model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
|
39
|
+
embedding_model = os.environ.get("OPENAI_EMBED_MODEL", "text-embedding-3-small")
|
|
40
|
+
vector_dim = int(os.environ.get("OPENAI_EMBED_DIM", "1536"))
|
|
41
|
+
|
|
42
|
+
config = MemorySystemConfig(
|
|
43
|
+
use_sqlite_vec=True,
|
|
44
|
+
vector_dim=vector_dim,
|
|
45
|
+
sqlite_vec_extension_path=os.environ.get("SQLITE_VEC_PATH"),
|
|
46
|
+
)
|
|
47
|
+
memory = MemorySystem(
|
|
48
|
+
config=config,
|
|
49
|
+
embedding_fn=openai_embedder(client, embedding_model, config.vector_dim),
|
|
50
|
+
)
|
|
51
|
+
policy = HeuristicMemoryPolicy()
|
|
52
|
+
routing_policy = MemoryRoutingPolicy()
|
|
53
|
+
session_working_id = str(uuid4())
|
|
54
|
+
|
|
55
|
+
owner = "user-001"
|
|
56
|
+
history: List[str] = []
|
|
57
|
+
|
|
58
|
+
while True:
|
|
59
|
+
user_message = input("User: ").strip()
|
|
60
|
+
if user_message.lower() in {"exit", "quit"}:
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
turn_start = time.time()
|
|
64
|
+
bundle = memory.retrieve(user_message, owner=owner)
|
|
65
|
+
print(
|
|
66
|
+
f"[trace] retrieve count={len(bundle.blocks)} confidence={bundle.confidence.total:.2f} tiers={bundle.used_tiers}"
|
|
67
|
+
)
|
|
68
|
+
if bundle.trace.escalations:
|
|
69
|
+
print(f"[trace] escalations={bundle.trace.escalations}")
|
|
70
|
+
if bundle.trace.steps:
|
|
71
|
+
print(f"[trace] steps={bundle.trace.steps}")
|
|
72
|
+
if bundle.trace.sources:
|
|
73
|
+
print(f"[trace] sources={bundle.trace.sources}")
|
|
74
|
+
context_blocks = []
|
|
75
|
+
token_budget = memory.config.retrieval_plan.max_context_tokens
|
|
76
|
+
used_tokens = 0
|
|
77
|
+
for block in bundle.blocks:
|
|
78
|
+
block_text = f"- [{block.memory_type}] {block.text}"
|
|
79
|
+
block_tokens = len(tokenize(block_text))
|
|
80
|
+
if used_tokens + block_tokens > token_budget:
|
|
81
|
+
break
|
|
82
|
+
context_blocks.append(block_text)
|
|
83
|
+
used_tokens += block_tokens
|
|
84
|
+
memory_context = "\n".join(context_blocks) if context_blocks else "None."
|
|
85
|
+
recent_turns = history[-6:]
|
|
86
|
+
history_text = "\n".join(recent_turns) if recent_turns else "None."
|
|
87
|
+
prompt = (
|
|
88
|
+
"You are a helpful assistant.\n"
|
|
89
|
+
"Use the following memory context and recent chat history if relevant.\n"
|
|
90
|
+
f"Memory context:\n{memory_context}\n\n"
|
|
91
|
+
f"Recent chat:\n{history_text}\n\n"
|
|
92
|
+
f"User: {user_message}\n"
|
|
93
|
+
"Assistant:"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
response = client.responses.create(
|
|
97
|
+
model=model,
|
|
98
|
+
input=prompt,
|
|
99
|
+
)
|
|
100
|
+
assistant_message = response.output_text
|
|
101
|
+
print(f"[trace] llm_latency_ms={(time.time() - turn_start) * 1000:.0f}")
|
|
102
|
+
print(f"Assistant: {assistant_message}")
|
|
103
|
+
|
|
104
|
+
history.append(f"User: {user_message}")
|
|
105
|
+
history.append(f"Assistant: {assistant_message}")
|
|
106
|
+
|
|
107
|
+
turns = [
|
|
108
|
+
{"role": "user", "text": entry.replace("User: ", "")}
|
|
109
|
+
if entry.startswith("User: ")
|
|
110
|
+
else {"role": "assistant", "text": entry.replace("Assistant: ", "")}
|
|
111
|
+
for entry in history
|
|
112
|
+
]
|
|
113
|
+
working_item = MemoryItem(
|
|
114
|
+
id=session_working_id,
|
|
115
|
+
type=MemoryType.WORKING,
|
|
116
|
+
owner=owner,
|
|
117
|
+
summary=f"Session transcript ({len(turns)} turns)",
|
|
118
|
+
content={"turns": turns},
|
|
119
|
+
tags=["conversation", "session-log"],
|
|
120
|
+
ttl_seconds=memory.config.working_ttl_seconds,
|
|
121
|
+
confidence=0.6,
|
|
122
|
+
)
|
|
123
|
+
memory.write(working_item)
|
|
124
|
+
|
|
125
|
+
decision = policy.should_store(owner, history, user_message, assistant_message)
|
|
126
|
+
print(decision)
|
|
127
|
+
event = policy.to_event(owner, decision)
|
|
128
|
+
if event:
|
|
129
|
+
routing = routing_policy.route(event.to_item())
|
|
130
|
+
print(f"[memory] store={decision.store} routing={routing.reasons}")
|
|
131
|
+
memory.write(event)
|
|
132
|
+
|
|
133
|
+
memory.flush(owner)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
main()
|