a digital entity named phi that roams bsky
1"""Inspect and prune stored memories.
2
3Usage:
4 uv run scripts/memory_inspect.py # list all user namespaces
5 uv run scripts/memory_inspect.py USER_HANDLE # dump observations + interactions for a user
6 uv run scripts/memory_inspect.py USER_HANDLE --delete ID # delete a specific row by ID
7 uv run scripts/memory_inspect.py USER_HANDLE --purge-observations # delete ALL observations for a user
8 uv run scripts/memory_inspect.py --episodic # dump phi's episodic memories
9"""
10
11import argparse
12import sys
13
14from turbopuffer import Turbopuffer
15
16from bot.config import settings
17
18
19def get_client() -> Turbopuffer:
20 return Turbopuffer(api_key=settings.turbopuffer_api_key, region=settings.turbopuffer_region)
21
22
23def list_namespaces(client: Turbopuffer):
24 """List all namespaces that look like user memory."""
25 prefix = "phi-users-"
26 namespaces = client.namespaces()
27 user_ns = [ns for ns in namespaces if ns.id.startswith(prefix)]
28 if not user_ns:
29 print("no user namespaces found")
30 return
31 print(f"found {len(user_ns)} user namespaces:\n")
32 for ns in sorted(user_ns, key=lambda n: n.id):
33 handle = ns.id.removeprefix(prefix).replace("_", ".")
34 print(f" {handle:<40} ({ns.id})")
35
36
37def dump_user(client: Turbopuffer, handle: str):
38 """Dump all memory for a user."""
39 clean = handle.replace(".", "_").replace("@", "").replace("-", "_")
40 ns_name = f"phi-users-{clean}"
41 ns = client.namespace(ns_name)
42
43 try:
44 response = ns.query(
45 rank_by=("vector", "ANN", [0.5] * 1536),
46 top_k=200,
47 include_attributes=["kind", "content", "tags", "created_at"],
48 )
49 except Exception as e:
50 if "was not found" in str(e):
51 print(f"no namespace found for @{handle} ({ns_name})")
52 return
53 if "attribute" in str(e) and "not found" in str(e):
54 # old namespace without kind/tags columns
55 response = ns.query(
56 rank_by=("vector", "ANN", [0.5] * 1536),
57 top_k=200,
58 include_attributes=True,
59 )
60 else:
61 raise
62
63 if not response.rows:
64 print(f"no rows found for @{handle}")
65 return
66
67 observations = []
68 interactions = []
69 for row in response.rows:
70 kind = getattr(row, "kind", "unknown")
71 entry = {
72 "id": row.id,
73 "content": row.content,
74 "tags": getattr(row, "tags", []),
75 "created_at": getattr(row, "created_at", ""),
76 }
77 if kind == "observation":
78 observations.append(entry)
79 else:
80 interactions.append(entry)
81
82 if observations:
83 print(f"=== observations ({len(observations)}) ===\n")
84 for obs in observations:
85 tags = f" [{', '.join(obs['tags'])}]" if obs["tags"] else ""
86 print(f" [{obs['id']}] {obs['content']}{tags}")
87 if obs["created_at"]:
88 print(f" created: {obs['created_at']}")
89 print()
90
91 if interactions:
92 print(f"=== interactions ({len(interactions)}) ===\n")
93 for ix in interactions:
94 content = ix["content"].replace("\n", "\n ")
95 print(f" [{ix['id']}]")
96 print(f" {content}")
97 if ix["created_at"]:
98 print(f" created: {ix['created_at']}")
99 print()
100
101 print(f"total: {len(observations)} observations, {len(interactions)} interactions")
102
103
104def delete_row(client: Turbopuffer, handle: str, row_id: str):
105 """Delete a specific row by ID."""
106 clean = handle.replace(".", "_").replace("@", "").replace("-", "_")
107 ns_name = f"phi-users-{clean}"
108 ns = client.namespace(ns_name)
109 ns.write(deletes=[row_id])
110 print(f"deleted row {row_id} from {ns_name}")
111
112
113def purge_observations(client: Turbopuffer, handle: str):
114 """Delete all observations for a user."""
115 clean = handle.replace(".", "_").replace("@", "").replace("-", "_")
116 ns_name = f"phi-users-{clean}"
117 ns = client.namespace(ns_name)
118
119 try:
120 response = ns.query(
121 rank_by=("vector", "ANN", [0.5] * 1536),
122 top_k=200,
123 filters={"kind": ["Eq", "observation"]},
124 include_attributes=["content"],
125 )
126 except Exception as e:
127 if "was not found" in str(e):
128 print(f"no namespace found for @{handle}")
129 return
130 raise
131
132 if not response.rows:
133 print(f"no observations to purge for @{handle}")
134 return
135
136 ids = [row.id for row in response.rows]
137 print(f"purging {len(ids)} observations for @{handle}:")
138 for row in response.rows:
139 print(f" - {row.content}")
140
141 ns.write(deletes=ids)
142 print(f"\ndeleted {len(ids)} observations")
143
144
145def dump_episodic(client: Turbopuffer):
146 """Dump phi's episodic memories."""
147 ns = client.namespace("phi-episodic")
148
149 try:
150 response = ns.query(
151 rank_by=("vector", "ANN", [0.5] * 1536),
152 top_k=200,
153 include_attributes=["content", "tags", "source", "created_at"],
154 )
155 except Exception as e:
156 if "was not found" in str(e):
157 print("no episodic memories found (namespace doesn't exist yet)")
158 return
159 raise
160
161 if not response.rows:
162 print("no episodic memories found")
163 return
164
165 print(f"=== episodic memories ({len(response.rows)}) ===\n")
166 for row in response.rows:
167 tags = getattr(row, "tags", [])
168 source = getattr(row, "source", "unknown")
169 tag_str = f" [{', '.join(tags)}]" if tags else ""
170 print(f" [{row.id}] {row.content}{tag_str}")
171 print(f" source: {source} created: {getattr(row, 'created_at', '')}")
172 print()
173
174 print(f"total: {len(response.rows)} episodic memories")
175
176
177def main():
178 parser = argparse.ArgumentParser(description="Inspect and prune phi memories")
179 parser.add_argument("handle", nargs="?", help="User handle to inspect")
180 parser.add_argument("--delete", metavar="ID", help="Delete a specific row by ID")
181 parser.add_argument("--purge-observations", action="store_true", help="Delete all observations for a user")
182 parser.add_argument("--episodic", action="store_true", help="Dump phi's episodic (world) memories")
183 args = parser.parse_args()
184
185 client = get_client()
186
187 if args.episodic:
188 dump_episodic(client)
189 return
190
191 if not args.handle:
192 list_namespaces(client)
193 return
194
195 if args.purge_observations:
196 purge_observations(client, args.handle)
197 elif args.delete:
198 delete_row(client, args.handle, args.delete)
199 else:
200 dump_user(client, args.handle)
201
202
203if __name__ == "__main__":
204 main()