docs: minor updates to message batching

This commit is contained in:
ajspig 2026-03-18 12:32:23 -04:00
parent 13497ed19f
commit 4080842891
4 changed files with 94 additions and 64 deletions

View File

@ -138,6 +138,27 @@ Sessions define the temporal boundaries of an interaction. How you scope session
- **New session** when the context resets (new conversation, new day, new topic)
- **Reuse session** when context should accumulate (ongoing channel, persistent thread)
**Peer join order matters**
Peers don't have to be added to a session all at once. You can call `session.add_peers()` multiple times as participants appear — and Honcho will take join order into account when reasoning. A peer only has context from the point they joined, not from earlier in the session.
This is particularly useful when importing multi-participant data like email threads or chat logs, where participants join at different points:
```python
session = honcho.session("email-thread-abc")
# First message: just the two original participants
session.add_peers([alice, bob])
session.add_messages([alice.message("Hey, can you review this proposal?")])
# Third message: charlie is CC'd in for the first time
session.add_peers([charlie])
session.add_messages([bob.message("Looping in Charlie who handles approvals.")])
session.add_messages([charlie.message("On it — give me a day to review.")])
```
Honcho knows Charlie joined mid-thread and scopes his context accordingly. `add_peers()` is idempotent — safe to call on every message with all current participants without tracking who's already been added. The exception is if you've set custom peer configuration (e.g. `observe_me`, `observe_others`). Re-calling `add_peers()` will overwrite those settings, so you'll need to track peer configurations.
---
## Application Patterns

View File

@ -150,18 +150,15 @@ honcho_msgs.append(peer.message(
### Multi-Peer Sessions
Each thread's session is linked to all participants using `session.add_peers()`. This means when you query Honcho about a peer, it has context not just from their messages but from the full conversations they participated in.
Each thread's session is linked to participants using `session.add_peers()`. Rather than adding all participants upfront, the script adds peers to the session as they first appear in each message — preserving the order in which people joined the thread.
```python
session = honcho.session(session_id, metadata={
"gmail_thread_id": tid,
"subject": subject,
"source": "gmail",
"message_count": len(msgs),
})
session.add_peers(thread_peers)
new_peers = [p for e in new_emails if (p := ensure_peer(e))]
if new_peers:
session.add_peers(new_peers)
```
### Stripping Quoted Replies
Email threads are full of quoted replies — each message repeats everything above it. The script strips these out so only the new content is stored per message, avoiding duplication in Honcho's memory:
@ -274,6 +271,7 @@ from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
PEER_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
def find_credentials() -> str:
@ -418,7 +416,14 @@ def parse_address_list(header: str) -> list[str]:
def peer_id_from_email(email: str) -> str:
"""Convert email to a valid Honcho peer ID."""
return email.replace("@", "-").replace(".", "-")
peer_id = re.sub(r"[^A-Za-z0-9_-]+", "-", email).strip("-").lower()
peer_id = re.sub(r"-{2,}", "-", peer_id)
if not peer_id:
peer_id = "unknown-peer"
if not PEER_ID_PATTERN.fullmatch(peer_id):
raise ValueError(f"Generated peer ID is invalid: {peer_id!r}")
return peer_id
def fetch_thread_messages(service, thread_id: str) -> list[dict]:
@ -508,7 +513,7 @@ def main():
# Summary
total_msgs = sum(len(v) for v in all_thread_messages.values())
print(f"\nSummary:")
print("\nSummary:")
print(f" Threads: {len(all_thread_messages)}")
print(f" Messages: {total_msgs}")
print(f" Unique participants: {len(seen_peers)}")
@ -531,52 +536,54 @@ def main():
print(f"\nLoading into Honcho workspace '{args.workspace}'...")
honcho = Honcho(workspace_id=args.workspace)
# Create peers
peers = {}
for i, (email, info) in enumerate(seen_peers.items()):
if i > 0 and i % 4 == 0:
time.sleep(1)
# Peers are created lazily and added to sessions as they first appear in each message.
# This preserves the join order so Honcho knows when each participant entered the thread.
peers = {} # email -> Honcho peer object
def ensure_peer(email: str):
if email in peers:
return peers[email]
if email not in seen_peers:
return None
info = seen_peers[email]
peers[email] = honcho.peer(info["peer_id"], metadata={
"email": email,
"name": info["name"],
"source": "gmail",
})
print(f" Peer: {info['peer_id']}")
return peers[email]
# Create sessions and messages per thread
for tid, msgs in all_thread_messages.items():
subject = msgs[0]["subject"] if msgs else "No subject"
session_id = f"gmail-thread-{tid}"
thread_peer_emails = set()
for m in msgs:
thread_peer_emails.add(extract_email(m["from"]))
for addr in parse_address_list(m["to"]):
thread_peer_emails.add(extract_email(addr))
for addr in parse_address_list(m["cc"]):
thread_peer_emails.add(extract_email(addr))
for addr in parse_address_list(m["bcc"]):
thread_peer_emails.add(extract_email(addr))
thread_peers = [peers[e] for e in thread_peer_emails if e in peers]
session = honcho.session(session_id, metadata={
"gmail_thread_id": tid,
"subject": subject,
"source": "gmail",
"message_count": len(msgs),
})
session.add_peers(thread_peers)
honcho_msgs = []
for m in msgs:
email = extract_email(m["from"])
peer = peers.get(email)
msg_count = 0
for i, m in enumerate(msgs):
if i > 0 and i % 4 == 0:
time.sleep(1)
# Add all participants in this message — endpoint is idempotent
msg_addresses = [m["from"]] + parse_address_list(m["to"]) + parse_address_list(m["cc"]) + parse_address_list(m["bcc"])
msg_peers = [p for a in msg_addresses if a and (p := ensure_peer(extract_email(a)))]
if msg_peers:
session.add_peers(msg_peers)
sender_email = extract_email(m["from"])
peer = ensure_peer(sender_email) if sender_email else None
if not peer:
continue
content = m["body"] if m["body"] else m["snippet"]
if not content:
continue
honcho_msgs.append(peer.message(
session.add_messages([peer.message(
content,
metadata={
"gmail_id": m["id"],
@ -586,11 +593,11 @@ def main():
"labels": m["labels"],
},
created_at=m["timestamp"],
))
)])
msg_count += 1
if honcho_msgs:
session.add_messages(honcho_msgs)
print(f" Session {session_id}: {len(honcho_msgs)} messages — {subject[:60]}")
if msg_count:
print(f" Session {session_id}: {msg_count} messages — {subject[:60]}")
print(f"\nDone! Loaded {total_msgs} messages into workspace '{args.workspace}'.")
@ -603,7 +610,7 @@ if __name__ == "__main__":
<CardGroup cols={2}>
<Card title="Design Patterns" icon="cubes" href="/v3/documentation/core-concepts/design-patterns">
See how the Granola integration maps to common Honcho patterns.
See how the Gmail integration maps to common Honcho patterns.
</Card>
<Card title="GitHub Repository" icon="github" href="https://github.com/plastic-labs/honcho/tree/main/examples/gmail">
Source code and example script.

View File

@ -850,7 +850,7 @@ async def main():
from honcho import Honcho
honcho = Honcho(workspace_id="granola_test")
honcho = Honcho(workspace_id="granola")
seen_peers: set[str] = set()
results = {"imported": 0, "skipped": 0, "failed": 0}

View File

@ -295,52 +295,54 @@ def main():
print(f"\nLoading into Honcho workspace '{args.workspace}'...")
honcho = Honcho(workspace_id=args.workspace)
# Create peers
peers = {}
for i, (email, info) in enumerate(seen_peers.items()):
if i > 0 and i % 4 == 0:
time.sleep(1)
# Peers are created lazily and added to sessions as they first appear in each message.
# This preserves the join order so Honcho knows when each participant entered the thread.
peers = {} # email -> Honcho peer object
def ensure_peer(email: str):
if email in peers:
return peers[email]
if email not in seen_peers:
return None
info = seen_peers[email]
peers[email] = honcho.peer(info["peer_id"], metadata={
"email": email,
"name": info["name"],
"source": "gmail",
})
print(f" Peer: {info['peer_id']}")
return peers[email]
# Create sessions and messages per thread
for tid, msgs in all_thread_messages.items():
subject = msgs[0]["subject"] if msgs else "No subject"
session_id = f"gmail-thread-{tid}"
thread_peer_emails = set()
for m in msgs:
thread_peer_emails.add(extract_email(m["from"]))
for addr in parse_address_list(m["to"]):
thread_peer_emails.add(extract_email(addr))
for addr in parse_address_list(m["cc"]):
thread_peer_emails.add(extract_email(addr))
for addr in parse_address_list(m["bcc"]):
thread_peer_emails.add(extract_email(addr))
thread_peers = [peers[e] for e in thread_peer_emails if e in peers]
session = honcho.session(session_id, metadata={
"gmail_thread_id": tid,
"subject": subject,
"source": "gmail",
"message_count": len(msgs),
})
session.add_peers(thread_peers)
honcho_msgs = []
for m in msgs:
email = extract_email(m["from"])
peer = peers.get(email)
msg_count = 0
for i, m in enumerate(msgs):
if i > 0 and i % 4 == 0:
time.sleep(1)
# Add all participants in this message — endpoint is idempotent
msg_addresses = [m["from"]] + parse_address_list(m["to"]) + parse_address_list(m["cc"]) + parse_address_list(m["bcc"])
msg_peers = [p for a in msg_addresses if a and (p := ensure_peer(extract_email(a)))]
if msg_peers:
session.add_peers(msg_peers)
sender_email = extract_email(m["from"])
peer = ensure_peer(sender_email) if sender_email else None
if not peer:
continue
content = m["body"] if m["body"] else m["snippet"]
if not content:
continue
honcho_msgs.append(peer.message(
session.add_messages([peer.message(
content,
metadata={
"gmail_id": m["id"],
@ -350,11 +352,11 @@ def main():
"labels": m["labels"],
},
created_at=m["timestamp"],
))
)])
msg_count += 1
if honcho_msgs:
session.add_messages(honcho_msgs)
print(f" Session {session_id}: {len(honcho_msgs)} messages — {subject[:60]}")
if msg_count:
print(f" Session {session_id}: {msg_count} messages — {subject[:60]}")
print(f"\nDone! Loaded {total_msgs} messages into workspace '{args.workspace}'.")