a digital entity named phi that roams bsky
at main 204 lines 6.8 kB view raw
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()