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.

strip tag/episodic layer from memory graph, tighten extraction tags

graph now shows only phi + user nodes (clean social graph).
removed dead phi-tag-relationships namespace query.
extraction prompt enforces topic-only tags (no person-*, no meta-categories).
episodic context no longer displays auto-generated tag suffixes.

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

zzstoatzz 263682d1 41b2800a

+20 -118
+9 -24
src/bot/main.py
··· 627 627 <div id="graph"></div> 628 628 <div class="tooltip" id="tooltip"></div> 629 629 <div class="legend"> 630 - <div class="legend-title">nodes positioned by semantic similarity</div> 630 + <div class="legend-title">social graph &middot; positioned by semantic similarity</div> 631 631 <div class="legend-item"><div class="legend-dot" style="background:#58a6ff"></div><span class="legend-label">phi (self)</span></div> 632 632 <div class="legend-item"><div class="legend-dot" style="background:#2ea043"></div><span class="legend-label">identities phi knows</span></div> 633 - <div class="legend-item"><div class="legend-dot" style="background:#8b949e"></div><span class="legend-label">topics from conversations</span></div> 634 - <div class="legend-item"><div class="legend-dot" style="background:#a371f7"></div><span class="legend-label">memories &amp; experiences</span></div> 635 633 </div> 636 634 <script> 637 - const colors = {{ phi: '#58a6ff', user: '#2ea043', tag: '#8b949e', episodic: '#a371f7' }}; 638 - const radii = {{ phi: 14, user: 9, tag: 5, episodic: 7 }}; 635 + const colors = {{ phi: '#58a6ff', user: '#2ea043' }}; 636 + const radii = {{ phi: 14, user: 9 }}; 639 637 640 638 async function fetchAvatars(nodes) {{ 641 639 const identities = nodes ··· 721 719 g.attr('transform', e.transform); 722 720 currentZoom = e.transform; 723 721 label.attr('font-size', d => {{ 724 - const base = d.type === 'phi' ? 13 : d.type === 'user' ? 10 : 9; 722 + const base = d.type === 'phi' ? 13 : 10; 725 723 return base / Math.max(currentZoom.k, 0.5); 726 724 }}); 727 - label.style('display', d => {{ 728 - if (d.type === 'phi' || d.type === 'user') return 'block'; 729 - return currentZoom.k >= 1.2 ? 'block' : 'none'; 730 - }}); 731 725 }})); 732 726 733 - const edgeOpacity = (source, target) => {{ 734 - const s = typeof source === 'object' ? source.type : ''; 735 - const t = typeof target === 'object' ? target.type : ''; 736 - if (s === 'phi' && t === 'user') return 0.7; 737 - if (s === 'user' && t === 'tag') return 0.2; 738 - if (s === 'tag' || t === 'tag') return 0.25; 739 - return 0.4; 740 - }}; 727 + const edgeOpacity = () => 0.7; 741 728 742 729 const simulation = d3.forceSimulation(data.nodes) 743 730 .force('link', d3.forceLink(data.edges).id(d => d.id).distance(40)) ··· 785 772 786 773 const label = g.append('g') 787 774 .selectAll('text') 788 - .data(data.nodes.filter(d => d.type === 'phi' || d.type === 'user' || d.type === 'episodic')) 775 + .data(data.nodes) 789 776 .join('text') 790 777 .text(d => d.label) 791 - .attr('font-size', d => d.type === 'phi' ? 13 : d.type === 'user' ? 10 : 9) 778 + .attr('font-size', d => d.type === 'phi' ? 13 : 10) 792 779 .attr('font-family', "'SF Mono', 'Cascadia Code', 'Fira Code', monospace") 793 - .attr('fill', d => d.type === 'episodic' ? '#a371f7' : '#8b949e') 794 - .attr('fill-opacity', d => d.type === 'episodic' ? 0.6 : 1) 780 + .attr('fill', '#8b949e') 795 781 .attr('text-anchor', 'middle') 796 - .attr('dy', d => radii[d.type] + 14) 797 - .style('display', d => d.type === 'episodic' ? 'none' : 'block'); 782 + .attr('dy', d => radii[d.type] + 14); 798 783 799 784 simulation.on('tick', () => {{ 800 785 link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
+8 -3
src/bot/memory/extraction.py
··· 67 67 <example> 68 68 user: what do you think about the strait of hormuz situation? 69 69 bot: trump considered a blockade, major shipping implications. 70 - observations: [{"content": "interested in geopolitical events around the strait of hormuz", "tags": ["interests", "geopolitics"]}] 70 + observations: [{"content": "interested in geopolitical events around the strait of hormuz", "tags": ["geopolitics"]}] 71 71 reason: the user asked about a specific topic, showing interest. the bot's answer content is not attributed to the user. 72 72 </example> 73 73 <example> 74 74 user: i've been learning rust lately, it's been great for my systems work 75 75 bot: rust is excellent for systems programming. 76 - observations: [{"content": "learning rust for systems programming", "tags": ["interests", "programming"]}] 76 + observations: [{"content": "learning rust for systems programming", "tags": ["rust", "programming"]}] 77 77 reason: the user stated something about themselves directly. 78 78 </example> 79 79 <example> 80 80 user: my name isn't zoë, it's nate. 81 81 bot: sorry about that — you're nate. bad breadcrumb on my end. 82 - observations: [{"content": "name is nate (corrected from previous error)", "tags": ["identity", "correction"]}] 82 + observations: [{"content": "name is nate (corrected from previous error)", "tags": ["correction"]}] 83 83 reason: the user explicitly corrected a factual error. corrections are high-value observations. 84 84 </example> 85 85 <example> ··· 89 89 reason: the user asked a question. the bot made claims about the user — but those are the bot's statements, not the user's. never extract identity from bot output. 90 90 </example> 91 91 </examples> 92 + 93 + tag rules: 94 + - tags categorize the TOPIC, not the person. never use a person's name, handle, or "person-*" as a tag. 95 + - use concrete topics: "atproto", "memory", "music", "infrastructure", "rust" — not meta-categories like "interests" or "identity". 96 + - 1-3 tags per observation. if nothing fits, use an empty list. 92 97 93 98 Deduplicate against existing observations provided in the prompt. Return an empty list when the exchange is just greetings, filler, or the user only asked questions without revealing anything about themselves.""" 94 99
+3 -91
src/bot/memory/namespace_memory.py
··· 537 537 return "" 538 538 lines = ["[PHI'S RELEVANT MEMORIES]"] 539 539 for r in results: 540 - tags = f" [{', '.join(r['tags'])}]" if r.get("tags") else "" 541 - lines.append(f"- {r['content']}{tags}") 540 + lines.append(f"- {r['content']}") 542 541 return "\n".join(lines) 543 542 544 543 async def search_unified( ··· 647 646 """Build graph nodes and edges from memory namespaces with semantic coordinates.""" 648 647 nodes = [{"id": "phi", "label": "phi", "type": "phi"}] 649 648 edges = [] 650 - tag_set: set[str] = set() 651 - user_tags: dict[str, set[str]] = {} # handle -> tags 652 - # vectors for computing semantic positions 653 - tag_vectors: dict[str, list[list[float]]] = {} 654 649 user_vectors: dict[str, list[list[float]]] = {} 655 650 656 651 # discover user namespaces ··· 664 659 ) 665 660 edges.append({"source": "phi", "target": f"user:{handle}"}) 666 661 667 - # get observations for this user to extract tags + vectors 662 + # get observation vectors for semantic positioning 668 663 user_ns = self.client.namespace(ns_summary.id) 669 664 try: 670 665 response = user_ns.query( 671 666 rank_by=("vector", "ANN", [0.5] * 1536), 672 667 top_k=50, 673 668 filters={"kind": ["Eq", "observation"]}, 674 - include_attributes=["tags", "vector"], 669 + include_attributes=["vector"], 675 670 ) 676 671 if response.rows: 677 672 for row in response.rows: 678 673 vec = getattr(row, "vector", None) 679 - for tag in getattr(row, "tags", []) or []: 680 - tag_set.add(tag) 681 - user_tags.setdefault(handle, set()).add(tag) 682 - if vec: 683 - tag_vectors.setdefault(tag, []).append(vec) 684 674 if vec: 685 675 user_vectors.setdefault(handle, []).append(vec) 686 676 except Exception: ··· 688 678 except Exception as e: 689 679 logger.warning(f"failed to list user namespaces: {e}") 690 680 691 - # add tag nodes and user→tag edges 692 - for tag in tag_set: 693 - nodes.append({"id": f"tag:{tag}", "label": tag, "type": "tag"}) 694 - for handle, tags in user_tags.items(): 695 - for tag in tags: 696 - edges.append({"source": f"user:{handle}", "target": f"tag:{tag}"}) 697 - 698 - # episodic memories — group by top tags 699 - episodic_tags: set[str] = set() 700 - episodic_vectors: dict[str, list[list[float]]] = {} 701 - try: 702 - response = self.namespaces["episodic"].query( 703 - rank_by=("vector", "ANN", [0.5] * 1536), 704 - top_k=100, 705 - include_attributes=["tags", "vector"], 706 - ) 707 - if response.rows: 708 - for row in response.rows: 709 - vec = getattr(row, "vector", None) 710 - for tag in getattr(row, "tags", []) or []: 711 - episodic_tags.add(tag) 712 - if vec: 713 - episodic_vectors.setdefault(tag, []).append(vec) 714 - except Exception: 715 - pass 716 - 717 - for tag in episodic_tags: 718 - node_id = f"episodic:{tag}" 719 - nodes.append({"id": node_id, "label": tag, "type": "episodic"}) 720 - edges.append({"source": "phi", "target": node_id}) 721 - # bridge to user tags if shared 722 - if tag in tag_set: 723 - edges.append({"source": f"tag:{tag}", "target": node_id}) 724 - 725 - # read tag-to-tag relationships from phi-tag-relationships 726 - node_ids = {n["id"] for n in nodes} 727 - try: 728 - rel_ns = self.client.namespace("phi-tag-relationships") 729 - rel_response = rel_ns.query( 730 - rank_by=("vector", "ANN", [0.5] * 1536), 731 - top_k=200, 732 - include_attributes=[ 733 - "tag_a", 734 - "tag_b", 735 - "relationship_type", 736 - "confidence", 737 - ], 738 - ) 739 - if rel_response.rows: 740 - for row in rel_response.rows: 741 - tag_a = getattr(row, "tag_a", "") 742 - tag_b = getattr(row, "tag_b", "") 743 - if not tag_a or not tag_b: 744 - continue 745 - # resolve to existing node IDs (prefer tag: over episodic:) 746 - source = ( 747 - f"tag:{tag_a}" 748 - if f"tag:{tag_a}" in node_ids 749 - else f"episodic:{tag_a}" 750 - if f"episodic:{tag_a}" in node_ids 751 - else None 752 - ) 753 - target = ( 754 - f"tag:{tag_b}" 755 - if f"tag:{tag_b}" in node_ids 756 - else f"episodic:{tag_b}" 757 - if f"episodic:{tag_b}" in node_ids 758 - else None 759 - ) 760 - if source and target and source != target: 761 - edges.append({"source": source, "target": target}) 762 - except Exception: 763 - pass # namespace may not exist yet 764 - 765 681 # compute per-node embedding centroids 766 682 def _centroid(vecs: list[list[float]]) -> list[float]: 767 683 n = len(vecs) ··· 769 685 return [sum(v[i] for v in vecs) / n for i in range(dim)] 770 686 771 687 centroids: dict[str, list[float]] = {} 772 - for tag, vecs in tag_vectors.items(): 773 - centroids[f"tag:{tag}"] = _centroid(vecs) 774 688 for handle, vecs in user_vectors.items(): 775 689 centroids[f"user:{handle}"] = _centroid(vecs) 776 - for tag, vecs in episodic_vectors.items(): 777 - centroids[f"episodic:{tag}"] = _centroid(vecs) 778 690 779 691 coords = self._project_2d(centroids) 780 692