docs: minor updates to message batching
This commit is contained in:
parent
13497ed19f
commit
4080842891
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}'.")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue