a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

concurrent notifications, activity feed polish, PCA memory layout

- notification_poller: dispatch notifications as background tasks with
semaphore(3), mark as read immediately, non-blocking daily post
- agent: make save_url title required so activity feed always has labels
- main: activity feed icons, domain display, titles, linkified URLs,
batched avatar fetches (chunks of 25)
- namespace_memory: replace random projection with PCA (numpy SVD) for
2D memory graph layout
- trim logfire extras, add numpy dep

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+227 -76
+2 -2
loq.toml
··· 17 17 18 18 [[rules]] 19 19 path = "src/bot/memory/namespace_memory.py" 20 - max_lines = 836 20 + max_lines = 872 21 21 22 22 [[rules]] 23 23 path = "src/bot/main.py" 24 - max_lines = 726 24 + max_lines = 793
+2 -1
pyproject.toml
··· 10 10 "atproto@git+https://github.com/MarshalX/atproto.git@refs/pull/605/head", 11 11 "fastapi", 12 12 "fastmcp>=0.8.0", 13 - "logfire[anthropic,fastapi,openai,pydantic-ai]", 13 + "logfire[fastapi]", 14 + "numpy>=2.4.4", 14 15 "openai", 15 16 "pydantic-ai", 16 17 "pydantic-settings",
+3 -2
src/bot/agent.py
··· 425 425 async def save_url( 426 426 ctx: RunContext[PhiDeps], 427 427 url: str, 428 - title: str | None = None, 428 + title: str, 429 429 description: str | None = None, 430 430 ) -> str: 431 - """Save a URL as a cosmik card on your PDS. Use when you find something worth bookmarking publicly.""" 431 + """Save a URL as a cosmik card on your PDS. Use when you find something worth bookmarking publicly. 432 + Always provide a concise, descriptive title — this is what appears in the activity feed.""" 432 433 try: 433 434 card = CosmikUrlCard( 434 435 content=UrlContent(url=url, title=title, description=description)
+91 -24
src/bot/main.py
··· 203 203 .card-post {{ border-left-color: #58a6ff; }} 204 204 .card-note {{ border-left-color: #a371f7; }} 205 205 .card-url {{ border-left-color: #2ea043; }} 206 + .card-header {{ 207 + display: flex; align-items: center; gap: 6px; 208 + margin-bottom: 6px; 209 + }} 210 + .card-icon {{ flex-shrink: 0; }} 211 + .card-icon svg {{ display: block; }} 206 212 .card-type {{ 207 213 font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; 208 - margin-bottom: 6px; font-weight: 500; 214 + font-weight: 500; 209 215 }} 210 216 .type-post {{ color: #58a6ff; }} 211 217 .type-note {{ color: #a371f7; }} 212 218 .type-url {{ color: #2ea043; }} 219 + .card-title {{ font-size: 14px; font-weight: 500; color: #c9d1d9; margin-bottom: 4px; }} 213 220 .card-text {{ font-size: 14px; line-height: 1.5; margin-bottom: 8px; word-break: break-word; }} 221 + .card-text a {{ color: #58a6ff; text-decoration: none; }} 222 + .card-text a:hover {{ text-decoration: underline; }} 223 + .card-domain {{ 224 + font-size: 12px; color: #8b949e; margin-bottom: 6px; 225 + display: flex; align-items: center; gap: 4px; 226 + }} 227 + .card-domain a {{ color: #8b949e; }} 228 + .card-domain a:hover {{ color: #c9d1d9; }} 214 229 .card-meta {{ font-size: 12px; color: #484f58; }} 215 230 .card-meta a {{ color: #484f58; }} 216 231 .card-meta a:hover {{ color: #8b949e; }} ··· 251 266 return Math.floor(s / 86400) + 'd ago'; 252 267 }} 253 268 function truncate(s, n) {{ return s.length > n ? s.slice(0, n) + '...' : s; }} 269 + function linkify(text) {{ 270 + return text.replace(/(https?:\/\/[^\s<>"{{}}|\\^`\[\]]+)/g, 271 + '<a href="$1" target="_blank" rel="noopener">$1</a>'); 272 + }} 273 + function getDomain(url) {{ 274 + try {{ return new URL(url).hostname.replace(/^www\./, ''); }} 275 + catch {{ return ''; }} 276 + }} 277 + const icons = {{ 278 + post: `<svg width="14" height="14" viewBox="0 0 600 530" fill="#58a6ff"> 279 + <path d="M135.72 44.03C202.22 93.87 284.5 149.63 300 163.14c15.5-13.51 97.78-69.27 164.28-119.11C528.23-2.96 600-21.03 600 66.94c0 17.58-10.06 147.67-15.96 168.71-20.48 73.22-95.26 91.94-163.03 80.59 118.4 20.18 148.52 86.98 83.52 153.79C395 580.88 300 538.04 300 538.04s-95-42.84-204.53 67.97C30.47 418.22 60.59 351.42 178.99 331.24c-67.77 11.35-142.55-7.37-163.03-80.59C10.06 229.61 0 99.52 0 81.94c0-87.97 71.77-69.9 135.72-37.91z"/> 280 + </svg>`, 281 + note: `<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#a371f7" stroke-width="1.5"> 282 + <path d="M8 1l2.12 4.3 4.74.69-3.43 3.34.81 4.72L8 11.77l-4.24 2.23.81-4.72L1.14 5.94l4.74-.69L8 1z"/> 283 + </svg>`, 284 + url: `<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#2ea043" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> 285 + <path d="M6.75 9.25a3.5 3.5 0 005-.5M9.25 6.75a3.5 3.5 0 00-5 .5M10 3.5l1-1a2.12 2.12 0 013 3l-1 1M6 12.5l-1 1a2.12 2.12 0 01-3-3l1-1"/> 286 + </svg>` 287 + }}; 288 + const labels = {{ 289 + post: 'bluesky', 290 + note: 'semble note', 291 + url: 'semble bookmark' 292 + }}; 293 + function viewUrl(item) {{ 294 + if (item.url) return item.url; 295 + if (item.uri && item.uri.startsWith('at://')) return 'https://pds.ls/' + item.uri; 296 + return ''; 297 + }} 254 298 fetch('/api/activity') 255 299 .then(r => r.json()) 256 300 .then(items => {{ 257 301 const el = document.getElementById('feed'); 258 302 document.getElementById('feed-loading').remove(); 259 303 if (!items.length) {{ el.textContent = 'no recent activity'; return; }} 260 - el.innerHTML = items.map(i => ` 304 + el.innerHTML = items.map(i => {{ 305 + const domain = i.url ? getDomain(i.url) : ''; 306 + const domainHtml = (i.type === 'url' && domain) 307 + ? `<div class="card-domain"><a href="${{i.url}}" target="_blank" rel="noopener">${{domain}}</a></div>` 308 + : ''; 309 + const titleHtml = i.title ? `<div class="card-title">${{i.title}}</div>` : ''; 310 + const link = viewUrl(i); 311 + return ` 261 312 <div class="card card-${{i.type}}"> 262 - <div class="card-type type-${{i.type}}">${{i.type}}</div> 263 - <div class="card-text">${{truncate(i.text || '', 200)}}</div> 313 + <div class="card-header"> 314 + <span class="card-icon">${{icons[i.type] || ''}}</span> 315 + <div class="card-type type-${{i.type}}">${{labels[i.type] || i.type}}</div> 316 + </div> 317 + ${{domainHtml}} 318 + ${{titleHtml}} 319 + <div class="card-text">${{linkify(truncate(i.text || '', 300))}}</div> 264 320 <div class="card-meta"> 265 321 ${{timeAgo(i.time)}} 266 - ${{i.url ? ` &middot; <a href="${{i.url}}" target="_blank" rel="noopener">view</a>` : ''}} 322 + ${{link ? ` &middot; <a href="${{link}}" target="_blank" rel="noopener">view</a>` : ''}} 267 323 </div> 268 - </div> 269 - `).join(''); 324 + </div>`; 325 + }}).join(''); 270 326 }}) 271 327 .catch(() => {{ 272 328 document.getElementById('feed-loading').textContent = 'failed to load activity'; ··· 450 506 card_type = value.get("type", "NOTE") 451 507 item_type = "url" if card_type == "URL" else "note" 452 508 content = value.get("content", {}) 509 + # metadata may be nested under content.metadata (semble lexicon) 510 + meta = content.get("metadata", {}) if isinstance(content, dict) else {} 453 511 if item_type == "url": 454 - text = ( 455 - content.get("title", "") 456 - or content.get("description", "") 457 - or content.get("url", "") 458 - ) 512 + card_title = content.get("title", "") or meta.get("title", "") 513 + desc = content.get("description", "") or meta.get("description", "") 514 + # skip semble tag metadata ("discussed in context of: ...") 515 + if desc and desc.startswith("discussed in context of:"): 516 + desc = "" 517 + # text is the description (or URL fallback), title is separate 518 + text = desc or (content.get("url", "") if not card_title else "") 459 519 else: 520 + card_title = ( 521 + content.get("title", "") if isinstance(content, dict) else "" 522 + ) 460 523 text = ( 461 524 content.get("text", "") 462 525 if isinstance(content, dict) ··· 469 532 { 470 533 "type": item_type, 471 534 "text": text, 535 + "title": card_title or None, 472 536 "time": card_time, 473 537 "uri": rec.get("uri", ""), 474 538 "url": content.get("url") if item_type == "url" else None, ··· 556 620 const radii = {{ phi: 14, user: 9, tag: 5, episodic: 7 }}; 557 621 558 622 async function fetchAvatars(nodes) {{ 559 - // collect identity handles for phi + user nodes 560 623 const identities = nodes 561 624 .filter(d => d.type === 'phi' || d.type === 'user') 562 625 .map(d => {{ ··· 565 628 }}) 566 629 .filter(h => h && !h.includes('example')); 567 630 if (!identities.length) return {{}}; 568 - const params = identities.map(h => 'actors=' + encodeURIComponent(h)).join('&'); 569 - try {{ 570 - const res = await fetch('https://typeahead.waow.tech/xrpc/app.bsky.actor.getProfiles?' + params); 571 - if (!res.ok) return {{}}; 572 - const data = await res.json(); 573 - const map = {{}}; 574 - for (const p of data.profiles || []) {{ 575 - if (p.avatar) map[p.handle] = p.avatar; 576 - }} 577 - return map; 578 - }} catch {{ return {{}}; }} 631 + const map = {{}}; 632 + // batch into chunks of 25 (getProfiles limit) 633 + for (let i = 0; i < identities.length; i += 25) {{ 634 + const chunk = identities.slice(i, i + 25); 635 + const params = chunk.map(h => 'actors=' + encodeURIComponent(h)).join('&'); 636 + try {{ 637 + const res = await fetch('https://typeahead.waow.tech/xrpc/app.bsky.actor.getProfiles?' + params); 638 + if (!res.ok) continue; 639 + const data = await res.json(); 640 + for (const p of data.profiles || []) {{ 641 + if (p.avatar) map[p.handle] = p.avatar; 642 + }} 643 + }} catch {{ /* skip failed batch */ }} 644 + }} 645 + return map; 579 646 }} 580 647 581 648 fetch('/api/memory/graph')
+19 -29
src/bot/memory/namespace_memory.py
··· 3 3 import asyncio 4 4 import hashlib 5 5 import logging 6 - import math 7 - import random 8 6 from datetime import datetime 9 7 from typing import ClassVar 10 8 ··· 615 613 def _project_2d( 616 614 centroids: dict[str, list[float]], 617 615 ) -> dict[str, tuple[float, float]]: 618 - """Project high-dimensional centroids to 2D via fixed random projection.""" 619 - if not centroids: 620 - return {} 621 - dim = len(next(iter(centroids.values()))) 622 - rng = random.Random(42) 623 - axis_a = [rng.gauss(0, 1) for _ in range(dim)] 624 - axis_b = [rng.gauss(0, 1) for _ in range(dim)] 625 - # normalize axes 626 - norm_a = math.sqrt(sum(v * v for v in axis_a)) 627 - norm_b = math.sqrt(sum(v * v for v in axis_b)) 628 - axis_a = [v / norm_a for v in axis_a] 629 - axis_b = [v / norm_b for v in axis_b] 616 + """Project high-dimensional centroids to 2D via PCA (top 2 principal components).""" 617 + import numpy as np 630 618 631 - raw: dict[str, tuple[float, float]] = {} 632 - for nid, vec in centroids.items(): 633 - x = sum(a * b for a, b in zip(axis_a, vec)) 634 - y = sum(a * b for a, b in zip(axis_b, vec)) 635 - raw[nid] = (x, y) 619 + if len(centroids) < 2: 620 + return {nid: (0.0, 0.0) for nid in centroids} 636 621 637 - if not raw: 638 - return {} 639 - xs = [p[0] for p in raw.values()] 640 - ys = [p[1] for p in raw.values()] 641 - x_min, x_max = min(xs), max(xs) 642 - y_min, y_max = min(ys), max(ys) 643 - x_span = x_max - x_min or 1.0 644 - y_span = y_max - y_min or 1.0 622 + ids = list(centroids.keys()) 623 + X = np.array([centroids[nid] for nid in ids]) 624 + X -= X.mean(axis=0) 625 + 626 + # SVD on centered data — U[:, :2] * S[:2] gives the top-2 PC projections 627 + U, S, _ = np.linalg.svd(X, full_matrices=False) 628 + proj = U[:, :2] * S[:2] 629 + 630 + # normalize to [-1, 1] 631 + for col in range(2): 632 + lo, hi = proj[:, col].min(), proj[:, col].max() 633 + span = hi - lo or 1.0 634 + proj[:, col] = 2 * (proj[:, col] - lo) / span - 1 635 + 645 636 return { 646 - nid: (2 * (p[0] - x_min) / x_span - 1, 2 * (p[1] - y_min) / y_span - 1) 647 - for nid, p in raw.items() 637 + nid: (float(proj[i, 0]), float(proj[i, 1])) for i, nid in enumerate(ids) 648 638 } 649 639 650 640 def get_graph_data(self) -> dict:
+46 -17
src/bot/services/notification_poller.py
··· 12 12 logger = logging.getLogger("bot.poller") 13 13 14 14 15 + MAX_CONCURRENT = 3 16 + 17 + 15 18 class NotificationPoller: 16 19 """Polls for and processes Bluesky notifications.""" 17 20 ··· 23 26 self._processed_uris: set[str] = set() 24 27 self._first_poll = True 25 28 self._last_daily_post: datetime | None = None 29 + self._semaphore = asyncio.Semaphore(MAX_CONCURRENT) 30 + self._background_tasks: set[asyncio.Task] = set() 26 31 27 32 async def start(self) -> asyncio.Task: 28 33 """Start polling for notifications.""" ··· 32 37 return self._task 33 38 34 39 async def stop(self): 35 - """Stop polling.""" 40 + """Stop polling and wait for in-flight handlers to finish.""" 36 41 self._running = False 37 42 bot_status.polling_active = False 38 43 if self._task and not self._task.done(): ··· 41 46 await self._task 42 47 except asyncio.CancelledError: 43 48 pass 49 + # wait for any in-flight notification handlers 50 + if self._background_tasks: 51 + await asyncio.gather(*self._background_tasks, return_exceptions=True) 44 52 45 53 async def _poll_loop(self): 46 54 """Main polling loop.""" ··· 55 63 continue 56 64 57 65 try: 58 - await self._maybe_daily_post() 66 + if self._should_do_daily_post(): 67 + task = asyncio.create_task(self._maybe_daily_post()) 68 + self._background_tasks.add(task) 69 + task.add_done_callback(self._background_tasks.discard) 59 70 except Exception as e: 60 71 logger.error(f"daily reflection error: {e}", exc_info=settings.debug) 61 72 ··· 90 101 logger.debug(f"paused, skipping {len(unread)} unread notifications") 91 102 return 92 103 93 - processed_any = False 104 + dispatched = 0 94 105 95 - # Process notifications from oldest to newest 106 + # Dispatch notifications as concurrent background tasks 96 107 for notification in reversed(notifications): 97 108 if notification.is_read or notification.uri in self._processed_uris: 98 109 continue 99 110 100 111 self._processed_uris.add(notification.uri) 101 - await self.handler.handle_notification(notification) 102 - processed_any = True 112 + task = asyncio.create_task(self._handle_with_semaphore(notification)) 113 + self._background_tasks.add(task) 114 + task.add_done_callback(self._background_tasks.discard) 115 + dispatched += 1 103 116 104 - # Mark all notifications as seen 105 - if processed_any: 117 + # Mark as read immediately — don't wait for processing 118 + if dispatched: 106 119 await self.client.mark_notifications_seen(check_time) 107 - logger.info("marked notifications as read") 120 + logger.info(f"dispatched {dispatched} notifications, marked as read") 108 121 109 - # Clean up old processed URIs to prevent memory growth 110 122 if len(self._processed_uris) > 1000: 111 123 self._processed_uris = set(list(self._processed_uris)[-500:]) 112 124 113 - async def _maybe_daily_post(self): 114 - """Post a daily reflection if it's past the target hour and we haven't posted today.""" 125 + async def _handle_with_semaphore(self, notification): 126 + """Handle a single notification with concurrency limiting.""" 127 + async with self._semaphore: 128 + try: 129 + await self.handler.handle_notification(notification) 130 + except Exception as e: 131 + logger.error( 132 + f"notification handler error: {e}", exc_info=settings.debug 133 + ) 134 + bot_status.record_error() 135 + 136 + def _should_do_daily_post(self) -> bool: 137 + """Check if it's time for a daily reflection.""" 115 138 now = datetime.now(UTC) 116 139 if now.hour < settings.daily_reflection_hour: 117 - return 140 + return False 118 141 if bot_status.paused: 119 - return 142 + return False 120 143 if self._last_daily_post and self._last_daily_post.date() == now.date(): 121 - return 144 + return False 145 + return True 122 146 147 + async def _maybe_daily_post(self): 148 + """Post a daily reflection.""" 149 + self._last_daily_post = datetime.now(UTC) 123 150 logger.info("triggering daily reflection") 124 - self._last_daily_post = now 125 - await self.handler.daily_reflection() 151 + try: 152 + await self.handler.daily_reflection() 153 + except Exception as e: 154 + logger.error(f"daily reflection error: {e}", exc_info=settings.debug)
+64 -1
uv.lock
··· 191 191 { name = "fastapi" }, 192 192 { name = "fastmcp" }, 193 193 { name = "logfire", extra = ["fastapi"] }, 194 + { name = "numpy" }, 194 195 { name = "openai" }, 195 196 { name = "pydantic-ai" }, 196 197 { name = "pydantic-settings" }, ··· 213 214 { name = "atproto", git = "https://github.com/MarshalX/atproto.git?rev=refs%2Fpull%2F605%2Fhead" }, 214 215 { name = "fastapi" }, 215 216 { name = "fastmcp", specifier = ">=0.8.0" }, 216 - { name = "logfire", extras = ["anthropic", "fastapi", "openai", "pydantic-ai"] }, 217 + { name = "logfire", extras = ["fastapi"] }, 218 + { name = "numpy", specifier = ">=2.4.4" }, 217 219 { name = "openai" }, 218 220 { name = "pydantic-ai" }, 219 221 { name = "pydantic-settings" }, ··· 1266 1268 { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload-time = "2025-06-30T15:53:11.038Z" }, 1267 1269 { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload-time = "2025-06-30T15:53:12.421Z" }, 1268 1270 { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, 1271 + ] 1272 + 1273 + [[package]] 1274 + name = "numpy" 1275 + version = "2.4.4" 1276 + source = { registry = "https://pypi.org/simple" } 1277 + sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } 1278 + wheels = [ 1279 + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, 1280 + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, 1281 + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, 1282 + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, 1283 + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, 1284 + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, 1285 + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, 1286 + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, 1287 + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, 1288 + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, 1289 + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, 1290 + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, 1291 + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, 1292 + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, 1293 + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, 1294 + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, 1295 + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, 1296 + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, 1297 + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, 1298 + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, 1299 + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, 1300 + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, 1301 + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, 1302 + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, 1303 + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, 1304 + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, 1305 + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, 1306 + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, 1307 + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, 1308 + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, 1309 + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, 1310 + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, 1311 + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, 1312 + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, 1313 + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, 1314 + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, 1315 + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, 1316 + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, 1317 + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, 1318 + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, 1319 + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, 1320 + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, 1321 + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, 1322 + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, 1323 + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, 1324 + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, 1325 + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, 1326 + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, 1327 + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, 1328 + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, 1329 + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, 1330 + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, 1331 + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, 1269 1332 ] 1270 1333 1271 1334 [[package]]