From 004f156d8a687404db688f689681b998a530ed5d Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 28 Feb 2026 18:37:57 +0000 Subject: [PATCH 1/3] docs: add shared memory architecture plan Query-scoped cross-peer lookups via Honcho v3 dialectic API. Integration surfaces: follow-ups and email replies. Sensitivity filter with fixed categories. Web-only connection setup, both users must be Pro. Amp-Thread-ID: https://ampcode.com/threads/T-019ca559-525e-753e-adeb-e921592e6b58 Co-authored-by: Amp --- docs/SHARED_MEMORY_PLAN.md | 352 +++++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 docs/SHARED_MEMORY_PLAN.md diff --git a/docs/SHARED_MEMORY_PLAN.md b/docs/SHARED_MEMORY_PLAN.md new file mode 100644 index 0000000..d391f7f --- /dev/null +++ b/docs/SHARED_MEMORY_PLAN.md @@ -0,0 +1,352 @@ +# Shared Memory Between Users — Implementation Plan + +## Status: Architecture Finalized / Ready for Phase 0 + +## Problem + +Memory is currently strictly user-scoped. Each user's Honcho peers (`{userId}-diatribe`, `{userId}-synthesis`) are isolated — no user can access another's facts, context, or session history. This prevents use cases like: + +- **Embodied perspective**: "What would Sarah think about this Kubernetes migration I'm following up on?" +- **Cross-user enrichment**: Follow-up conversations and email note threads that draw on connected users' knowledge and opinions +- **Collaborative recall**: A user's follow-up or email reply is enriched with relevant context from colleagues/family who are also Clairvoyant users + +## Core Insight: Query-Scoped Cross-Peer Lookups + +**This is NOT a data-sharing system.** We do not share raw transcripts, session summaries, or chat messages between users. Instead, at **query time** (during follow-ups and email replies), we make targeted cross-peer queries against connected users' existing Honcho diatribe peers using the v3 dialectic API. + +The key mechanism: `peer.chat(query, { target })` — ask Honcho "What does Sarah's representation know about Kubernetes?" and get back a synthesized, abstracted answer. Honcho returns facts and deductions, not raw speech. + +**Integration surfaces:** +- **Follow-up conversations** (`followupsChat.ts`) — when Ajay continues a follow-up, the system queries connected users' peers for relevant perspective +- **Email note replies** (`emailReply.ts`) — when Ajay replies to an email note, connected users' perspectives enrich the response + +**NOT integration surfaces (unchanged):** +- Real-time glasses responses — no cross-peer queries during live transcription +- `MemoryRecall` handler — stays single-user for now +- `MemoryCapture` handler — no changes, all captures remain private + +## Current Architecture (as-is) + +``` +User A User B + │ │ + ├── Peer: {userA}-diatribe ├── Peer: {userB}-diatribe + ├── Peer: {userA}-synthesis ├── {userB}-synthesis + │ │ + └── Honcho Session (isolated) └── Honcho Session (isolated) +``` + +- **Convex schema**: All tables keyed by single `userId: v.id("users")`. No user-to-user relationships exist. +- **Honcho peer IDs**: Hardcoded to `{userId}-{type}`. Peers are never shared across users. +- **Follow-ups**: `sendFollowupMessage` in `followupsChat.ts` fetches memory from user's own diatribe peer only. +- **Email replies**: `processEmailReply` in `emailReply.ts` fetches peerCard from user's own diatribe peer only. + +## Honcho v3 SDK Capabilities + +Our codebase uses **older SDK patterns**. The v3 SDK provides: + +| Capability | Our current usage | v3 API | +|---|---|---| +| Peer context | `session.getContext({ peerTarget })` | `peer.chat(query)` (dialectic API) | +| Cross-peer queries | Not available | `peer.chat(query, { target })` | +| Messages | `session.addMessages([{ peer_id, content }])` | `peer.message(content)` | +| Observation config | Not used | `SessionPeerConfig` with `observe_me`/`observe_others` | +| Cross-peer representations | Not available | `session.working_rep(peerA, peerB)` | + +**Critical dependency**: `peer.chat(query, { target })` is the core mechanism for cross-peer queries. Phase 0 must confirm this is available. + +## Proposed Architecture (to-be) + +``` +User A (Ajay) User B (Sarah) + │ │ + ├── {ajay}-diatribe (private peer) ├── {sarah}-diatribe (private peer) + ├── {ajay}-synthesis (private peer) ├── {sarah}-synthesis (private peer) + │ │ + └── Connection (bidirectional, consented) ──┘ + │ + ├── status: "active" + ├── sharedMemoryEnabled: true (both opted in) + ├── Labels: Ajay calls it "wife", Sarah calls it "husband" + │ + └── At query time (follow-up / email reply): + 1. Ajay asks about "Kubernetes migration" + 2. System finds active connections with sharedMemoryEnabled + 3. Query: sarah-diatribe.chat( + "What relevant non-sensitive context does this person have about Kubernetes?", + { target: ajay-diatribe } + ) + 4. Sensitivity filter (BAML gate): is this safe to surface? + 5. Inject into follow-up/email response with attribution +``` + +### Layer 1: Convex — User Connections + +Bidirectional, consent-based relationships between users. + +``` +connections table: + - requesterId: v.id("users") // who initiated + - accepterId: v.id("users") // who accepted + - status: "pending" | "active" | "revoked" + - sharedMemoryEnabled: v.boolean() // both must opt in, both must be Pro + + indexes: + by_requester: [requesterId] + by_accepter: [accepterId] + by_pair: [requesterId, accepterId] // uniqueness check +``` + +Per-user labels (since "wife" vs "husband" differs by side): + +``` +connectionLabels table: + - connectionId: v.id("connections") + - userId: v.id("users") // which side's label + - label: v.string() // "wife", "Sarah", "my coworker Alex" + + indexes: + by_connection: [connectionId] + by_user: [userId] +``` + +**Connection setup is web-only.** Both users must already have Clairvoyant accounts. User A enters User B's email on the web dashboard, User B accepts on their dashboard. + +### Layer 2: Query-Time Cross-Peer Lookup + +No shared peer. No shared session. At query time, we query the connected user's existing private diatribe peer via the dialectic API. + +**In `followupsChat.ts` (`sendFollowupMessage`):** + +```typescript +// After fetching user's own memory context (existing code)... + +// Fetch active connections with shared memory enabled +const connections = await ctx.runQuery( + internal.connections.getActiveSharedMemoryConnections, + { userId: user._id } +); + +// For each connection, query the connected user's diatribe peer +const crossPeerContexts = await Promise.all( + connections.map(async (conn) => { + const connectedUserId = conn.requesterId === user._id + ? conn.accepterId + : conn.requesterId; + const label = conn.label; // "wife — Sarah", "coworker — Alex" + + const connectedPeer = await honchoClient.peer(`${connectedUserId}-diatribe`); + const perspective = await connectedPeer.chat( + `What relevant context does this person have about: ${followup.topic}? ` + + `Exclude any health/medical, financial, romantic, legal, or family-conflict information.`, + { target: userPeer } + ); + + return { label, perspective }; + }) +); + +// Pass to BAML for sensitivity filtering + response generation +``` + +**In `emailReply.ts` (`processEmailReply`):** + +Same pattern — query connected users' diatribe peers with the email note's topic/subject as the query. + +### Layer 3: Sensitivity Filter + +Two-layer protection: + +**A) Query-scoped prompt** (in the `peer.chat()` query itself): +> "Exclude any health/medical, financial, romantic/sexual, legal, family-conflict, or credential/account information." + +**B) BAML sensitivity gate** (post-filter, lightweight): + +```baml +enum SensitivityCategory { + SAFE @description("Professional, shared interests, general knowledge, plans") + SENSITIVE @description("Health, finances, legal, romantic, family conflicts, credentials") +} + +function CheckSensitivity(crossPeerContext: string) -> SensitivityCategory { + client "Groq" // Fast, cheap + prompt #" + Classify whether this cross-peer context contains sensitive personal information. + + SENSITIVE categories (return SENSITIVE if ANY are present): + - Health or medical conditions + - Financial details (salary, debt, investments) + - Romantic or sexual content + - Legal matters + - Family conflicts or personal disputes + - Passwords, credentials, or private account details + + Context to classify: + {{ crossPeerContext }} + + {{ ctx.output_format }} + "# +} +``` + +If the gate returns `SENSITIVE`, drop that cross-peer context entirely. No partial filtering — it's all-or-nothing per connection to keep it simple. + +### Layer 4: Attribution in Responses + +Cross-peer context is injected with attribution using the connection label: + +``` +CONNECTED USER PERSPECTIVES: +From "wife — Sarah": +- Sarah has been researching serverless alternatives to Kubernetes +- Sarah expressed concerns about operational complexity of container orchestration +``` + +The BAML response formatter (InterpretFollowupChat / InterpretEmailReply) weaves this naturally: +> "You bookmarked the Kubernetes migration topic. Worth noting — Sarah has been looking into serverless alternatives and has concerns about the operational complexity." + +## Revocation Semantics + +When a connection is revoked (either side): +- **Immediately stop** making cross-peer queries for that connection (`status !== "active"` check) +- **Leave past data** — responses already generated with cross-peer context remain in `followupChatMessages` and `emailThreadMessages`. The consent was valid when the data was surfaced. +- **No Honcho cleanup needed** — we never created shared peers or sessions, so there's nothing to tear down. + +## Pro Gating + +- **Both users must be Pro** to enable `sharedMemoryEnabled` on a connection +- The connection itself (Phase 1) can exist without Pro — it's just a social link +- If either user's Pro lapses, cross-peer queries silently stop (check at query time) +- Cost rationale: each cross-peer query adds 1-2 Honcho API calls + 1 BAML sensitivity gate call per connection + +## Implementation Phases + +### Phase 0: SDK Evaluation (prerequisite) + +**Goal**: Confirm `peer.chat(query, { target })` is available and plan upgrade if needed. + +1. Check current `@honcho-ai/sdk` version in `package.json` against latest v3.x +2. Test if `peer.chat(query, { target })` (dialectic API) works with our version +3. If upgrade needed, plan a separate PR for SDK migration +4. Verify backward compatibility with existing `getContext()` / `addMessages()` patterns +5. **Decision gate**: If dialectic API is unavailable, we need an alternative approach (manual context merging via `getContext()` on both peers) + +### Phase 1: Connections (Convex only, no Honcho changes) + +**Goal**: Let users connect with each other via the web dashboard. + +1. Add `connections` + `connectionLabels` tables to Convex schema +2. Add Convex mutations: `createConnection` (by email lookup), `acceptConnection`, `revokeConnection` +3. Add Convex queries: `getConnectionsForUser`, `getActiveSharedMemoryConnections` +4. Web UI: connections management page + - Search by email → send invite + - Pending invites list → accept/reject + - Active connections → set label, toggle sharedMemoryEnabled, revoke +5. **No memory changes yet** — purely relational infrastructure +6. Both users must be Pro to toggle `sharedMemoryEnabled` + +### Phase 2: Cross-Peer Queries in Follow-ups + +**Goal**: Follow-up conversations draw on connected users' perspectives. + +1. Upgrade SDK if needed (Phase 0 outcome) +2. In `followupsChat.ts` `sendFollowupMessage`: + - After existing `fetchMemoryContext()`, query active connections + - For each connection, call `peer.chat(topic, { target })` against connected user's diatribe peer + - Run BAML `CheckSensitivity` gate on each result + - Pass safe cross-peer contexts to `InterpretFollowupChat` BAML function +3. Extend `FollowupChatContext` BAML class with `crossPeerContext` field +4. Update `InterpretFollowupChat` prompt to handle attributed cross-peer perspectives +5. Add `CheckSensitivity` BAML function in new `baml_src/sensitivity.baml` +6. Test: Ajay follows up on "Kubernetes migration", Sarah's perspective on containers is surfaced with attribution + +### Phase 3: Cross-Peer Queries in Email Replies + +**Goal**: Email note reply threads draw on connected users' perspectives. + +1. In `emailReply.ts` `processEmailReply`: + - After existing peerCard fetch, query active connections + - Same cross-peer query + sensitivity filter pattern as Phase 2 + - Pass to `InterpretEmailReply` BAML function with cross-peer context +2. Extend email reply BAML context class with cross-peer perspectives +3. Update `InterpretEmailReply` prompt for attributed cross-peer weaving +4. Test: Ajay replies to an email note about a topic Sarah has discussed, her perspective enriches the reply + +### Phase 4: Advanced Cross-Peer Features (future) + +**Goal**: Deeper cross-peer reasoning and additional surfaces. + +1. Use `session.working_rep(peerA, peerB)` for pre-computed cross-peer representations (faster than per-query `peer.chat`) +2. Extend to real-time glasses responses (MemoryRecall handler) for "What does Sarah think about X?" queries +3. Add cross-peer context to web chat (`chat.ts` `sendMessage`) for daily recap conversations +4. Group connections (3+ users, e.g., a team sharing context) + +### Phase 5: Passive Sharing & Smart Detection (future) + +**Goal**: Automatically detect when cross-peer context would be relevant. + +1. Topic-overlap detection: when a follow-up topic matches known topics in a connected user's peer representation, proactively surface their perspective +2. "We" detection in queries: "What do we think about X?" triggers cross-peer lookup +3. Connection-label matching: "What did Sarah say about X?" detects "Sarah" as a connection label and routes to cross-peer query + +## Key Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Architecture | **Query-scoped cross-peer lookups** | No shared peers, no shared sessions. Query connected users' existing diatribe peers at query time via dialectic API. Simplest, most privacy-respecting approach. | +| Integration surfaces | **Follow-ups + email replies only** | These are reflective, async surfaces where cross-peer enrichment adds value. Real-time glasses responses stay single-user. | +| Sensitivity filter | **Prompt-scoped + BAML gate** | Two layers: exclude sensitive categories in the dialectic query prompt, then post-filter with a cheap BAML binary classifier. Fixed category list, no user customization. | +| Sensitive categories | **Health, finances, romantic, legal, family conflicts, credentials** | Fixed list, sufficient for Phase 1. No per-user customization needed. | +| Connection setup | **Web dashboard only, existing users only** | Users search by email, send invite, accept on web. No QR codes, no invite-to-join for non-users. Glasses experience is passive. | +| Connection model | **Bidirectional with explicit accept** | Prevents unwanted data exposure. Both sides must consent. | +| Pro gating | **Both users must be Pro** | Shared memory doubles Honcho API calls per query. Connection can exist without Pro, but `sharedMemoryEnabled` requires both. | +| Label storage | **Per-side labels** | "wife" from Ajay's side, "husband" from Sarah's side. Same connection, different labels. | +| Revocation | **Stop future queries, leave past data** | Revocation immediately stops cross-peer queries. Responses already generated remain — consent was valid when surfaced. No Honcho cleanup needed. | +| Data shared | **Honcho abstractions only, never raw data** | Cross-peer queries return Honcho's synthesized facts/deductions, not raw transcripts, session summaries, or chat messages. | + +## Resolved Questions + +1. **Connection discovery**: Web dashboard, email-based lookup. Both users must already have accounts. +2. **Honcho shared peer lifecycle**: Not needed — we use query-time dialectic API against existing private peers. +3. **Revocation semantics**: Stop querying, leave past data. No shared constructs to tear down. +4. **Pro gating**: Both users must be Pro for `sharedMemoryEnabled`. +5. **What data is shared**: Honcho peer abstractions only (facts, deductions, peerCard). Never raw Convex data (summaries, chats, transcripts). +6. **Sensitivity**: Fixed category filter (health, finances, romantic, legal, family conflicts, credentials). Two-layer: prompt + BAML gate. + +## Open Questions + +1. **SDK upgrade scope**: Can we upgrade `@honcho-ai/sdk` without breaking existing `getContext()` / `addMessages()` patterns? Is v3 backward-compatible? +2. **Rate limiting / latency budget**: Each cross-peer query adds ~200-500ms (Honcho) + ~100ms (BAML gate). With N connections, this multiplies. Cap at 3 connections per user? Parallelize queries? +3. **Dialectic API cost**: `peer.chat()` pricing per query. Need to evaluate cost per follow-up/email interaction with cross-peer queries. +4. **Attribution UX**: Should the follow-up/email response explicitly name the connected user ("Sarah thinks...") or be more ambient ("Your team has discussed...")? Current plan: explicit attribution with connection label. + +## Files to Modify (by phase) + +### Phase 0 +- `package.json` / `bun.lock` — evaluate and potentially upgrade `@honcho-ai/sdk` +- Test script to verify `peer.chat(query, { target })` works + +### Phase 1 +- `packages/convex/schema.ts` — add `connections`, `connectionLabels` tables +- `packages/convex/connections.ts` — new file: mutations + queries +- `apps/web/` — connection management UI (search, invite, accept, label, toggle, revoke) + +### Phase 2 +- `packages/convex/followupsChat.ts` — add cross-peer query logic in `sendFollowupMessage` +- `baml_src/followup.baml` — extend `FollowupChatContext` with `crossPeerContext`, update prompt +- `baml_src/sensitivity.baml` — new file: `CheckSensitivity` function +- `packages/convex/connections.ts` — add `getActiveSharedMemoryConnections` query + +### Phase 3 +- `packages/convex/emailReply.ts` — add cross-peer query logic in `processEmailReply` +- `baml_src/emailReply.baml` — extend context class with cross-peer perspectives, update prompt + +### Phase 4 +- `packages/convex/followupsChat.ts` — optimize with `working_rep()` caching +- `apps/application/src/handlers/memory.ts` — extend `MemoryRecall` with cross-peer support +- `packages/convex/chat.ts` — extend `sendMessage` with cross-peer context + +### Phase 5 +- `baml_src/sensitivity.baml` — topic-overlap detection +- `packages/convex/followupsChat.ts` — proactive cross-peer surfacing +- `baml_src/route.baml` — connection-label detection in routing From b41ea0db6551a6284f419ac09f400186f7d189ee Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 28 Feb 2026 20:27:18 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20implement=20Phase=204=20shared=20me?= =?UTF-8?q?mory=20=E2=80=94=20advanced=20cross-peer=20features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Optimize cross-peer queries with peer.representation() instead of peer.chat() - Extend MemoryRecall handler for glasses cross-peer support - Add cross-peer context to web chat (chat.ts, chat.baml, bamlActions.ts) - Add group connections schema and CRUD (connectionGroups, connectionGroupMembers) - Update synthesis.baml with crossPeerPerspectives in MemoryContext - Add public query getActiveSharedMemoryConnectionsByMentraId for app layer - Add getActiveSharedMemoryGroupMembers internal query Amp-Thread-ID: https://ampcode.com/threads/T-019ca5da-0bdc-71c4-b883-2d206a12db38 Co-authored-by: Amp --- .../src/baml_client/inlinedbaml.ts | 2 +- .../src/baml_client/partial_types.ts | 5 + .../src/baml_client/type_builder.ts | 4 +- apps/application/src/baml_client/types.ts | 7 + apps/application/src/handlers/memory.ts | 79 ++ baml_src/chat.baml | 13 + baml_src/synthesis.baml | 14 + docs/SHARED_MEMORY_PLAN.md | 30 +- .../baml_client/baml_client/inlinedbaml.ts | 2 +- .../baml_client/baml_client/partial_types.ts | 5 + .../baml_client/baml_client/type_builder.ts | 4 +- packages/baml_client/baml_client/types.ts | 7 + packages/convex/bamlActions.ts | 41 + packages/convex/chat.ts | 75 ++ packages/convex/connections.ts | 738 ++++++++++++++++++ packages/convex/emailReply.ts | 76 ++ packages/convex/followupsChat.ts | 78 ++ packages/convex/schema.ts | 42 +- 18 files changed, 1203 insertions(+), 19 deletions(-) create mode 100644 packages/convex/connections.ts diff --git a/apps/application/src/baml_client/inlinedbaml.ts b/apps/application/src/baml_client/inlinedbaml.ts index a7cc350..9da67e3 100644 --- a/apps/application/src/baml_client/inlinedbaml.ts +++ b/apps/application/src/baml_client/inlinedbaml.ts @@ -30,7 +30,7 @@ const fileMap = { "route.baml": "enum Router {\n WEATHER @description(\"Current or upcoming weather questions for a specific place.\")\n WEB_SEARCH @description(\"News, current events, facts that change over time such as political events, or topics not obviously location-based.\")\n MAPS @description(\"Finding nearby businesses, restaurants, addresses, or directions.\")\n KNOWLEDGE @description(\"General knowledge that does not fit into other categories.\")\n MEMORY_CAPTURE @description(\"Commands to store new personal facts, preferences, or reminders for future recall.\")\n MEMORY_RECALL @description(\"Questions about the user's memory and personal history, personal preferences, personal opinions, goals, information about the user, or anything that is not a fact.\")\n NOTE_THIS @description(\"User wants to save the current conversation as a note to email. Triggered by phrases like 'add this to a note', 'send this to my email', 'this would be great to remember', 'note this down'.\")\n PASSTHROUGH @description(\"Ambient speech, filler words, incomplete sentences, or unclear utterances that don't require action.\")\n}\n\nclass RoutingBehavior {\n origin string @description(\"Echo of the user's input text verbatim.\")\n routing Router\n}\n\nfunction Route(text: string) -> RoutingBehavior {\n client \"GroqHeavy\"\n prompt #\"\n You are routing a single short utterance to one of several intent categories.\n\n STEP 1 — MENTALLY CLEAN THE UTTERANCE:\n Before routing, internally strip filler and hesitation words like: \"um\", \"uh\", \"hmm\", \"erm\",\n \"like\", \"you know\", \"uh yeah so\", \"okay so\", \"so yeah\", repeated words, false starts,\n and mid-sentence corrections.\n - If, after removing these, there is NO clear question or command left, treat as PASSTHROUGH.\n - If there IS a clear question or command after cleaning, route based on the cleaned meaning.\n - Do NOT remove words that change the core intent (e.g., \"okay\" at the start of a command is fine to keep).\n\n STEP 2 — DETERMINE DIRECTEDNESS:\n Decide if this utterance is DIRECTED AT THE ASSISTANT vs AMBIENT/BACKGROUND speech.\n\n Signs of DIRECTED speech (route to a real category):\n - Question forms: \"what\", \"who\", \"where\", \"when\", \"how\", \"why\", \"can you\", \"could you\", \"is it\"\n - Commands/imperatives: \"remind me\", \"remember that\", \"find me\", \"tell me\", \"look up\", \"show me\"\n - Personal intentions with time references: \"I need to... tomorrow\", \"I should... later\"\n - Assistant wake word or second-person address\n\n Signs of AMBIENT/BACKGROUND speech (PASSTHROUGH):\n - Movie, TV, or audiobook dialogue (dramatic lines, exclamations, third-person narrative)\n e.g., \"I'll never let you go Jack!\", \"You can't handle the truth!\", \"No Luke, I am your father\"\n - Other people's conversations (named characters talking to each other, no request to assistant)\n - Narration or storytelling not involving a request\n - Pure reactions with no follow-up: \"oh wow\", \"no way\", \"that's crazy\", \"ha ha\"\n - Background announcements: \"The next station is Times Square\"\n\n EXCEPTION: If a movie/TV line is quoted AS PART OF a question (e.g., \"What movie is 'I'll never\n let you go Jack' from?\"), do NOT treat as PASSTHROUGH — route to WEB_SEARCH or KNOWLEDGE.\n\n STEP 3 — CHOOSE EXACTLY ONE ROUTE:\n Routes (in priority order when multiple could apply):\n 1. WEATHER → questions about forecast or current weather for a place\n 2. MAPS → requests to find locations, directions, or nearby businesses\n 3. WEB_SEARCH → current events or information likely needing the internet\n 4. KNOWLEDGE → general facts that are stable over time\n 5. MEMORY_CAPTURE → commands or clear intentions to remember/store personal facts, preferences,\n or reminders — including indirect phrasing like \"I need to call the dentist tomorrow\"\n 6. MEMORY_RECALL → questions about the user's past, preferences, or personal information\n 7. NOTE_THIS → requests to save/note/email the current conversation, e.g., \"add this to a note\",\n \"send this to my email\", \"this would be great to remember\", \"note this down\", \"save this\"\n 8. PASSTHROUGH → pure filler, incomplete fragments with no clear request, ambient speech,\n or unclear utterances that don't require action\n\n Additional guidelines:\n - If the utterance mentions weather/forecast, choose WEATHER even if a location is mentioned\n - If the utterance is primarily about finding a place or directions, choose MAPS\n - When in doubt between KNOWLEDGE and WEB_SEARCH, prefer WEB_SEARCH for anything that might change\n - For vague self-talk without clear time/action (\"maybe someday I should...\"), prefer PASSTHROUGH\n - For clear near-term intentions (\"I need to call mom tomorrow\"), prefer MEMORY_CAPTURE\n\n {{ _.role(\"user\") }} {{ text }}\n {{ ctx.output_format }}\n \"#\n}\n\ntest test_weather {\n functions [Route]\n args {\n text \"What is the weather in San Francisco?\"\n }\n @@assert( {{ this.routing == \"WEATHER\"}})\n}\n\ntest test_web_search {\n functions [Route]\n args {\n text \"Who is the current president of the United States?\"\n }\n @@assert( {{ this.routing == \"WEB_SEARCH\"}})\n}\n\ntest test_maps {\n functions [Route]\n args {\n text \"Find me a ramen restaurant near Union Square.\"\n }\n @@assert( {{ this.routing == \"MAPS\"}})\n}\n\ntest test_knowledge {\n functions [Route]\n args {\n text \"What is the capital of France?\"\n }\n @@assert( {{ this.routing == \"KNOWLEDGE\"}})\n}\n\ntest test_knowledge_2 {\n functions [Route]\n args {\n text \"What is the purpose of mitochondria?\"\n }\n @@assert( {{ this.routing == \"KNOWLEDGE\"}})\n}\n\ntest test_memory {\n functions [Route]\n args {\n text \"What is my name?\"\n }\n @@assert( {{ this.routing == \"MEMORY_RECALL\"}})\n}\n\ntest test_memory_2 {\n functions [Route]\n args {\n text \"Koyal, what did I eat yesterday?\"\n }\n @@assert( {{ this.routing == \"MEMORY_RECALL\"}})\n}\n\ntest test_memory_capture {\n functions [Route]\n args {\n text \"Remember that my sister's birthday is May 3rd.\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_memory_capture_task {\n functions [Route]\n args {\n text \"Please remember I need to call the dentist tomorrow.\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_memory_capture_task_2 {\n functions [Route]\n args {\n text \"I need to call the dentist tomorrow.\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_passthrough_filler {\n functions [Route]\n args {\n text \"hmm\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_okay {\n functions [Route]\n args {\n text \"okay\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_fragment {\n functions [Route]\n args {\n text \"uh yeah so\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_weather_with_location {\n functions [Route]\n args {\n text \"What's the weather like for my trip to Paris?\"\n }\n @@assert( {{ this.routing == \"WEATHER\"}})\n}\n\n// ===== FILLER WORD TESTS =====\n// These test that fillers are stripped and the real intent is routed correctly\n\ntest test_weather_with_fillers {\n functions [Route]\n args {\n text \"um what's the weather uh in SF\"\n }\n @@assert( {{ this.routing == \"WEATHER\"}})\n}\n\ntest test_maps_with_fillers {\n functions [Route]\n args {\n text \"uh like find me a coffee shop you know nearby\"\n }\n @@assert( {{ this.routing == \"MAPS\"}})\n}\n\ntest test_knowledge_with_fillers {\n functions [Route]\n args {\n text \"so like um what is the capital of Japan\"\n }\n @@assert( {{ this.routing == \"KNOWLEDGE\"}})\n}\n\ntest test_web_search_with_fillers {\n functions [Route]\n args {\n text \"hmm uh who won the game last night\"\n }\n @@assert( {{ this.routing == \"WEB_SEARCH\"}})\n}\n\ntest test_memory_capture_with_fillers {\n functions [Route]\n args {\n text \"um yeah so remind me to call mom tomorrow\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\n// ===== AMBIENT/MOVIE DIALOGUE TESTS =====\n// These should be PASSTHROUGH as they are background speech, not directed at assistant\n\ntest test_passthrough_movie_titanic {\n functions [Route]\n args {\n text \"I'll never let you go Jack!\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_movie_star_wars {\n functions [Route]\n args {\n text \"No Luke, I am your father\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_movie_few_good_men {\n functions [Route]\n args {\n text \"You can't handle the truth!\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_tv_dialogue {\n functions [Route]\n args {\n text \"Ross, we were on a break!\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_background_announcement {\n functions [Route]\n args {\n text \"The next station is Times Square\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_reaction {\n functions [Route]\n args {\n text \"oh wow that's crazy\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_narration {\n functions [Route]\n args {\n text \"He walked into the room and never looked back\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\n// ===== EDGE CASES =====\n// Movie quote AS PART OF a question should NOT be passthrough\n\ntest test_movie_quote_in_question {\n functions [Route]\n args {\n text \"What movie is 'I'll never let you go Jack' from?\"\n }\n @@assert( {{ this.routing != \"PASSTHROUGH\"}})\n}\n\n// Self-talk with clear near-term intention should be MEMORY_CAPTURE\n\ntest test_self_talk_clear_intent {\n functions [Route]\n args {\n text \"I need to pick up groceries on the way home\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_self_talk_should_call {\n functions [Route]\n args {\n text \"I should call the doctor tomorrow\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\n// Vague self-talk without clear action should be PASSTHROUGH\n\ntest test_self_talk_vague {\n functions [Route]\n args {\n text \"maybe someday I should learn to play guitar\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\n// ===== NOTE_THIS TESTS =====\n// Requests to save/note/email the current conversation\n\ntest test_note_this_add_to_note {\n functions [Route]\n args {\n text \"add this to a note\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\ntest test_note_this_email_me {\n functions [Route]\n args {\n text \"email me this conversation\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\ntest test_note_this_remember {\n functions [Route]\n args {\n text \"this would be great to remember\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\ntest test_note_this_save {\n functions [Route]\n args {\n text \"save this to my notes\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}", "search.baml": "class NewsItem {\n title string\n content string\n}\n\nclass AnswerLines {\n lines string[]\n}\n\nclass QueryResult {\n query string\n results AnswerLines[]\n}\n\nfunction AnswerSearch(query: string, searchResults: NewsItem[], memory: MemoryCore?) -> QueryResult {\n client \"Groq\"\n prompt #\"\n {{ _.role(\"system\")}}\n You are a helpful Web Search Summarizer who summarizes the results of a web search. \n You need give an answer to the following question: {{ query }} given the results of the web search below collected by the user. \n The output should be lines of text that sound like a human would say them to a friend and no more than 10 words per line as per the output format and no more than 3 lines as per the output format: \n \n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (use to personalize response and acknowledge past searches):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n \n If the user has searched for related topics before, naturally acknowledge this (e.g., \"You searched for X yesterday...\" or \"Following up on your interest in Y...\"). Keep it conversational.\n {% endif %}\n {% endif %}\n \n {{ ctx.output_format }}\n {{ _.role(\"user\") }}\n {% for result in searchResults %}\n Title:\n {{ result.title }}\n {{- \"\\n\" -}}\n Content:\n {{ result.content }}\n {{- \"\\n\" -}}\n {% endfor %}\n \"#\n}\n\ntest test_answer_search {\n functions [AnswerSearch]\n args {\n query \"Did Charlie Kirk die?\"\n memory null\n searchResults [\n {\n title \"California state Sen. Scott Wiener labels Charlie Kirk 'a vile bigot who' normalized 'dehumanization' - Fox News\"\n content \"### Recommended Videos Charlie Kirk # California state Sen. Scott Wiener labels Charlie Kirk 'a vile bigot who' normalized 'dehumanization' 2 min \\\"Charlie Kirk did not deserve to die. Also Charlie Kirk was a vile bigot who did immeasurable harm to so many people by normalizing dehumanization. **CHARLIE KIRK VIGILS HELD AT UNIVERSITIES ACROSS AMERICA FOLLOWING ASSASSINATION OF CONSERVATIVE ACTIVIST** \\\"Multiple things can be true: Political violence is toxic & Kirk's assassination must be condemned. Charlie Kirk poses at The Cambridge Union on May 19, 2025 in Cambridge, Cambridgeshire. (Nordin Catic/Getty Images for The Cambridge Union) Video \\\"Charlie Kirk's murder is horrific. 21 mins ago #### California state Sen. Scott Wiener labels Charlie Kirk 'a vile bigot who' normalized 'dehumanization' 1 hour ago 3 hours ago\"\n },\n {\n title \"Erika Kirk delivers moving tribute to husband, Charlie: 'I will never let your legacy die' - Fox News\"\n content \"# Erika Kirk delivers moving tribute to husband, Charlie: 'I will never let your legacy die' #### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' Erika Kirk, the widow of Charlie Kirk, made her first public remarks since her husband's death. Erika Kirk, the widow of the late Charlie Kirk, gave an emotional tribute to her husband and declared that his mission will not end at Turning Point USA's headquarters Friday. Erika Kirk spoke for the first time since the assassination of her husband, Charlie Kirk, at Turning Point USA Sept. #### Erika Kirk delivers moving tribute to husband, Charlie: 'I will never let your legacy die'\"\n },\n {\n title \"Opinion | Charlie Kirk and the Future of Political Violence - The New York Times\"\n content \"Opinion | Charlie Kirk and the Future of Political Violence - The New York Times Opinion|If We Keep This Up, Charlie Kirk Will Not Be the Last to Die https://www.nytimes.com/2025/09/11/opinion/charlie-kirk-assassination-debate.html If We Keep This Up, Charlie Kirk Will Not Be the Last to Die An assassin's bullet cut down Charlie Kirk, one of the nation's most prominent conservative activists and commentators, at a public event on the campus of Utah Valley University. When an assassin shot Kirk, that person killed a man countless students felt like they knew, and the assassin killed him _on a college campus_. Subscribe to The Times to read as many articles as you like. * Your Privacy Choices\"\n },\n {\n title \"Life, Liberty & Levin - Saturday, September 13 - Fox News\"\n content \"Watch the live stream of Fox News and full episodes. Charlie Kirk, Tribute, Legacy Fox News Channel Charlie Kirk: An American Original FOX News Radio Live Channel Coverage Fox News Channel Live ### Bill Maher calls for people to stop comparing Trump to Hitler following Charlie Kirk's assassination ### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' ### Charlie Kirk assassin was 'much closer to mainstream Democrat than any of us wanted to believe': Tim Pool ### Lara Trump says media played role in Charlie Kirk assassination ### Illegal immigrant suspect dead after dragging agent with car, DHS reports ### Friend of Charlie Kirk speaks to his legacy\"\n },\n {\n title \"Kilmeade apologizes for remarks on homelessness, mental illness - Fox News\"\n content \"Watch the live stream of Fox News and full episodes. Fox News Channel FOX News Radio Live Channel Coverage Fox News Channel Live ### Bill Maher calls for people to stop comparing Trump to Hitler following Charlie Kirk's assassination ### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' ### Mark Levin says he knew right away how special Charlie Kirk was ### Lara Trump says media played role in Charlie Kirk assassination ### Questions remain on how suspect in Charlie Kirk murder got access to roof ### Dean Phillips: Part of our core American principles were assassinated with Charlie Kirk ### Alleged assassin of Charlie Kirk held in Utah jail without bail\"\n },\n {\n title \"'His voice will remain,' Charlie Kirk's widow vows after suspect arrested - BBC\"\n content \"* \\\"I will never let his legacy die,\\\" Charlie Kirk's wife says in her first public comments since he was fatally shot at a Utah university campus on Wednesday * Erika vowed to never let Charlie's legacy die, adding that his message will carry on being shared through his campus tour of US universities and his podcast - she did not specify how they would continue 2. ### 'My husband's voice will remain,' Charlie Kirk's widow says as suspected killer in custodypublished at 08:15 BST 08:15 BST published at 03:15 03:15 His widow, Erika Kirk, vows to never let his legacy die, saying in her first public statement on Friday: \\\"The movement my husband built will not die.\\\"\"\n },\n {\n title \"Country singer warns that Charlie Kirk's death has 'awoken' millions - New York Post\"\n content \"For all the people or the hateful people out there, the groups that thought that would quiet Charlie Kirk, you've just awoken millions of other people that are not scared to die, Adcock said Monday on 'The Ingraham Angle.' 'If you live in the life of the Lord and believe in Jesus, you shouldn't be scared to leave this world, and Charlie Kirk was a great example of that.' Adcock had never met Kirk but said he often watched videos of his debates on social media. \\\"For all the people or the hateful people out there, the groups that thought that would quiet Charlie Kirk, you've just awoken millions of other people that are not scared to die,\\\" country artist Gavin Adcock said. Adcock had never met Kirk but said he watched videos of him.\"\n },\n {\n title \"Black Pastor Blasts Efforts To Whitewash Charlie Kirk's Legacy In The Wake Of His Killing - HuffPost\"\n content \"The Rev. Howard-John Wesley on Sunday spoke out against efforts to whitewash the legacy of right-wing political activist Charlie Kirk in the wake of his assassination on a Utah college campus last week, declaring that how you die does not redeem how you live. (Watch video below). The Trump administration has taken steps to honor Kirk following his death. Maggie Haberman Says Trump Is 'Struggling' With Messaging Around Charlie Kirk's Death Charlie Kirk Shooting Suspect Captured, Trump And Utah Authorities Announce GOP Lawmaker Calls Out Trump's 'Over The Top' Rhetoric After Charlie Kirk Killing Wall Street Journal Warns Trump Of 'Dangerous Moment' After Charlie Kirk Assassination\"\n },\n {\n title \"Country singer Gavin Adcock warns those who tried to silence Charlie Kirk have 'awoken' millions more - Fox News\"\n content \"# Country singer Gavin Adcock warns those who tried to silence Charlie Kirk have 'awoken' millions more ## Fans chanted 'Charlie Kirk' as Adcock waved an American flag on stage over the weekend Country singer Gavin Adcock explains why he paid tribute to Charlie Kirk in a series of shows on 'The Ingraham Angle.' Country artist Gavin Adcock warned those who thought they could silence Charlie Kirk that his death has only awakened millions more. \\\"For all the people or the hateful people out there, the groups that thought that would quiet Charlie Kirk, you've just awoken millions of other people that are not scared to die,\\\" Adcock said Monday on \\\"The Ingraham Angle.\\\"\"\n },\n {\n title \"Caroline Sunshine: The solution to America's void isn't banning guns or silencing speech - Fox News\"\n content \"Watch the live stream of Fox News and full episodes. The 'Fox News @ Night' panel discusses reactions to the assassination of Turning Point USA founder Charlie Kirk. Fox News Channel FOX News Radio Live Channel Coverage Fox News Channel Live ### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' ### Charlie Kirk assassin was 'much closer to mainstream Democrat than any of us wanted to believe': Tim Pool ### Donald Trump Jr on Charlie Kirk's assassination: The violence is 'not going both ways' ### 'We have him': Trump says suspected Charlie Kirk assassin is in custody ### Phoenix air traffic control honors Charlie Kirk: 'May God bless your family'\"\n }\n ]\n }\n @@assert({{ this.results[0].lines|length == 3 }})\n}\n", "session_summary.baml": "class SessionSummaryOutput {\n summary string @description(\"1-2 sentence summary of what was discussed\")\n topics string[] @description(\"2-5 key topics or themes from the session\")\n}\n\nfunction SummarizeSession(\n transcripts: string[]\n) -> SessionSummaryOutput {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are summarizing a conversation session from smart glasses.\n\n{{ ctx.output_format }}\n\nCONVERSATION TRANSCRIPTS:\n{% for transcript in transcripts %}\n- {{ transcript }}\n{% endfor %}\n\nInstructions:\n- First, filter out incomplete sentences, single words, filler words (e.g., \"um\", \"uh\", \"okay\"), and obvious transcription errors\n- Write a brief 1-2 sentence summary of what the user discussed or asked about\n- Extract 2-5 key topics or themes (e.g., \"weather\", \"navigation\", \"family\", \"work\")\n- Keep topics as single words or short phrases\n- Focus on what's memorable or might be referenced later\n- If transcripts are empty, trivial, or only contain noise/filler, return summary \"No meaningful activity\" with topics [\"none\"]\n\"#\n}\n\ntest test_summarize_session_weather {\n functions [SummarizeSession]\n args {\n transcripts [\n \"What's the weather like today?\",\n \"Is it going to rain later?\",\n \"Should I bring an umbrella?\"\n ]\n }\n @@assert({{ this.topics|length >= 1 }})\n @@assert({{ this.topics|length <= 5 }})\n}\n\ntest test_summarize_session_mixed {\n functions [SummarizeSession]\n args {\n transcripts [\n \"Navigate to the nearest coffee shop\",\n \"What time does Starbucks close?\",\n \"Remind me to call mom later\",\n \"What's on my calendar today?\"\n ]\n }\n @@assert({{ this.topics|length >= 2 }})\n @@assert({{ this.topics|length <= 5 }})\n}\n\ntest test_summarize_session_empty {\n functions [SummarizeSession]\n args {\n transcripts []\n }\n @@assert({{ this.summary|length > 0 }})\n}\n\ntest test_summarize_session_noise {\n functions [SummarizeSession]\n args {\n transcripts [\n \"um\",\n \"okay\",\n \"uh yeah\",\n \"hmm\"\n ]\n }\n @@assert({{ this.topics|length >= 1 }})\n}\n\ntest test_summarize_session_mixed_quality {\n functions [SummarizeSession]\n args {\n transcripts [\n \"um\",\n \"What's the weather today?\",\n \"okay\",\n \"Find me a coffee shop nearby\"\n ]\n }\n @@assert({{ this.topics|length >= 1 }})\n}\n", - "synthesis.baml": "class MemoryContext {\n explicitFacts string[]\n deductiveFacts string[]\n peerCard string[]\n recentMessages string[]\n sessionSummaries string[] @description(\"Summaries of past sessions, e.g. 'Dec 20: discussed weather and navigation'\")\n}\n\nclass MemorySynthesisLines {\n lines string[] @description(\"1-3 concise lines answering the query\")\n}\n\nfunction SynthesizeMemory(\n query: string,\n context: MemoryContext\n) -> MemorySynthesisLines {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are a memory synthesis agent. Answer the user's query using their stored memories.\n\n{{ ctx.output_format }}\n\nUSER'S BIOGRAPHICAL INFO:\n{% for fact in context.peerCard %}\n{{ fact }}\n{% endfor %}\n\nEXPLICIT FACTS (directly stated by user):\n{% for fact in context.explicitFacts %}\n- {{ fact }}\n{% endfor %}\n\nDEDUCTIVE CONCLUSIONS:\n{% for fact in context.deductiveFacts %}\n- {{ fact }}\n{% endfor %}\n\nRECENT CONVERSATION:\n{% for msg in context.recentMessages %}\n{{ msg }}\n{% endfor %}\n\nPAST SESSION SUMMARIES:\n{% for summary in context.sessionSummaries %}\n- {{ summary }}\n{% endfor %}\n\nQUERY: {{ query }}\n\nInstructions:\n- Provide 1-3 short lines (≤10 words each) that directly answer the query\n- Use a natural, conversational tone like texting a friend\n- If the user's name is known from peerCard, use it naturally\n- You can use contractions and casual language\n- If you don't have enough information to answer, say so briefly\n- Focus on what's most relevant to the query\n- Ignore any alphanumeric IDs or hashes in the facts (e.g., \"j979r44m...\" prefixes) — extract only the meaningful content\n\"#\n}\n\ntest test_memory_synthesis_name {\n functions [SynthesizeMemory]\n args {\n query \"What is my name?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"My name is Ajay Bhargava, that's spelled A-J-A-Y, last name B-H-A-R-G-A-V-A.\",\n \"What is my name?\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_children {\n functions [SynthesizeMemory]\n args {\n query \"Tell me about my daughters\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has two daughters.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters' names are Koyal and Kavya.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters are five and two years old.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has children of different ages.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Children: Two daughters (Koyal and Kavya)\",\n \"Daughters' ages: 5 and 2 years old\"\n ]\n recentMessages [\n \"I have two girls, five and two. Their names are Koyal and Kavya.\",\n \"Tell me about my daughters\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_no_info {\n functions [SynthesizeMemory]\n args {\n query \"What car do I drive?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\"\n ]\n recentMessages [\n \"What car do I drive?\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_age {\n functions [SynthesizeMemory]\n args {\n query \"How old am I?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's date of birth is December 19, 1987.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe is 37 years old.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"How old am I?\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n", + "synthesis.baml": "class MemoryContext {\n explicitFacts string[]\n deductiveFacts string[]\n peerCard string[]\n recentMessages string[]\n sessionSummaries string[] @description(\"Summaries of past sessions, e.g. 'Dec 20: discussed weather and navigation'\")\n crossPeerPerspectives CrossPeerPerspective[] @description(\"Perspectives from connected users with shared memory enabled\")\n}\n\nclass MemorySynthesisLines {\n lines string[] @description(\"1-3 concise lines answering the query\")\n}\n\nfunction SynthesizeMemory(\n query: string,\n context: MemoryContext\n) -> MemorySynthesisLines {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are a memory synthesis agent. Answer the user's query using their stored memories.\n\n{{ ctx.output_format }}\n\nUSER'S BIOGRAPHICAL INFO:\n{% for fact in context.peerCard %}\n{{ fact }}\n{% endfor %}\n\nEXPLICIT FACTS (directly stated by user):\n{% for fact in context.explicitFacts %}\n- {{ fact }}\n{% endfor %}\n\nDEDUCTIVE CONCLUSIONS:\n{% for fact in context.deductiveFacts %}\n- {{ fact }}\n{% endfor %}\n\nRECENT CONVERSATION:\n{% for msg in context.recentMessages %}\n{{ msg }}\n{% endfor %}\n\nPAST SESSION SUMMARIES:\n{% for summary in context.sessionSummaries %}\n- {{ summary }}\n{% endfor %}\n\n{% if context.crossPeerPerspectives | length > 0 %}\nCONNECTED USER PERSPECTIVES:\n{% for p in context.crossPeerPerspectives %}\nFrom \"{{ p.label }}\":\n{{ p.perspective }}\n{% endfor %}\n\n{% endif %}\nQUERY: {{ query }}\n\nInstructions:\n- Provide 1-3 short lines (≤10 words each) that directly answer the query\n- Use a natural, conversational tone like texting a friend\n- If the user's name is known from peerCard, use it naturally\n- You can use contractions and casual language\n- If you don't have enough information to answer, say so briefly\n- Focus on what's most relevant to the query\n- Ignore any alphanumeric IDs or hashes in the facts (e.g., \"j979r44m...\" prefixes) — extract only the meaningful content\n- If connected users have relevant perspectives, naturally mention them (e.g. \"Sarah thinks...\", \"Alex mentioned...\")\n\"#\n}\n\ntest test_memory_synthesis_name {\n functions [SynthesizeMemory]\n args {\n query \"What is my name?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"My name is Ajay Bhargava, that's spelled A-J-A-Y, last name B-H-A-R-G-A-V-A.\",\n \"What is my name?\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_children {\n functions [SynthesizeMemory]\n args {\n query \"Tell me about my daughters\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has two daughters.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters' names are Koyal and Kavya.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters are five and two years old.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has children of different ages.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Children: Two daughters (Koyal and Kavya)\",\n \"Daughters' ages: 5 and 2 years old\"\n ]\n recentMessages [\n \"I have two girls, five and two. Their names are Koyal and Kavya.\",\n \"Tell me about my daughters\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_no_info {\n functions [SynthesizeMemory]\n args {\n query \"What car do I drive?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\"\n ]\n recentMessages [\n \"What car do I drive?\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_age {\n functions [SynthesizeMemory]\n args {\n query \"How old am I?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's date of birth is December 19, 1987.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe is 37 years old.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"How old am I?\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n", "weather.baml": "class WeatherConditionLite {\n id int\n main string\n description string\n icon string\n}\n\nclass CurrentLite {\n temperature float\n feels_like float\n conditions WeatherConditionLite\n humidity int\n pressure int\n wind_speed float\n wind_direction int\n visibility int\n uv_index float\n clouds int\n}\n\nclass TempBlock {\n day float\n min float\n max float\n night float\n}\n\nclass DailyForecastItem {\n date string\n summary string\n temperature TempBlock\n conditions WeatherConditionLite\n precipitation_probability float\n rain float\n}\n\nclass LocationLite {\n lat float\n lon float\n timezone string\n}\n\nclass AlertLite {\n sender_name string\n event string\n start int\n end int\n description string\n tags string[]\n}\n\nclass FormattedWeather {\n location LocationLite\n current CurrentLite\n daily_forecast DailyForecastItem[]\n alerts AlertLite[]\n}\n\nclass WeatherLines {\n lines string[]\n}\n\nfunction SummarizeWeatherFormatted(input: FormattedWeather, unit: string, memory: MemoryCore?) -> WeatherLines {\n client \"openai/gpt-4o-mini\"\n\n prompt #\"\n You are a witty, conversational weather reporter that's super brief even if you have a lot of data to work with.\n Write exactly 10 words that sound like a friendly human texted them to a friend.\n Avoid bullet points, lists, or emoji. \n Use the data to describe the weather on a scale of 1-10 out of 10 and include the \"vibe rating\" of the weather.\n {{ ctx.output_format }}\n\n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (weave naturally if relevant):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n {% endif %}\n {% endif %}\n\n Style rules:\n - You are only allowed to print 3 lines in the list output. \n - Each line can be a continuation of the previous line.\n - The first line should be the scale and the vibe rating.\n - The second line should be the current weather.\n - The third line should be the alert if there is one OR a personalized insight based on user context.\n - Always include the temperature unit symbol (e.g., \"72°F\" or \"22°C\") when mentioning temperatures.\n\n Data (verbatim):\n Location:\n timezone=\"{{ input.location.timezone }}\"\n Now:\n {% if unit == \"C\" %}\n temp_c={{ input.current.temperature }}\n feels_c={{ input.current.feels_like }}\n {% else %}\n temp_f={{ input.current.temperature }}\n feels_f={{ input.current.feels_like }}\n {% endif %}\n desc=\"{{ input.current.conditions.description }}\"\n wind_ms={{ input.current.wind_speed }}\n wind_deg={{ input.current.wind_direction }}\n humidity={{ input.current.humidity }}\n uv_index={{ input.current.uv_index }}\n clouds={{ input.current.clouds }}\n visibility_m={{ input.current.visibility }}\n Today:\n date=\"{{ input.daily_forecast[0].date if input.daily_forecast|length > 0 else \"\" }}\"\n {% if unit == \"C\" %}\n min_c={{ input.daily_forecast[0].temperature.min if input.daily_forecast|length > 0 else 0 }}\n max_c={{ input.daily_forecast[0].temperature.max if input.daily_forecast|length > 0 else 0 }}\n {% else %}\n min_f={{ input.daily_forecast[0].temperature.min if input.daily_forecast|length > 0 else 0 }}\n max_f={{ input.daily_forecast[0].temperature.max if input.daily_forecast|length > 0 else 0 }}\n {% endif %}\n pop={{ input.daily_forecast[0].precipitation_probability if input.daily_forecast|length > 0 else 0.0 }}\n rain_mm={{ input.daily_forecast[0].rain if input.daily_forecast|length > 0 else 0.0 }}\n desc=\"{{ input.daily_forecast[0].summary if input.daily_forecast|length > 0 else \"\" }}\"\n Tomorrow:\n date=\"{{ input.daily_forecast[1].date if input.daily_forecast|length > 1 else \"\" }}\"\n {% if unit == \"C\" %}\n min_c={{ input.daily_forecast[1].temperature.min if input.daily_forecast|length > 1 else 0 }}\n max_c={{ input.daily_forecast[1].temperature.max if input.daily_forecast|length > 1 else 0 }}\n {% else %}\n min_f={{ input.daily_forecast[1].temperature.min if input.daily_forecast|length > 1 else 0 }}\n max_f={{ input.daily_forecast[1].temperature.max if input.daily_forecast|length > 1 else 0 }}\n {% endif %}\n pop={{ input.daily_forecast[1].precipitation_probability if input.daily_forecast|length > 1 else 0.0 }}\n rain_mm={{ input.daily_forecast[1].rain if input.daily_forecast|length > 1 else 0.0 }}\n desc=\"{{ input.daily_forecast[1].summary if input.daily_forecast|length > 1 else \"\" }}\"\n Alert:\n {% if input.alerts|length > 0 %}\n event=\"{{ input.alerts[0].event }}\"\n end_unix={{ input.alerts[0].end }}\n sender=\"{{ input.alerts[0].sender_name }}\"\n tags=\"{{ input.alerts[0].tags | join(\", \") }}\"\n {% endif %}\n \"#\n}\n\ntest test_weather_lines_fahrenheit {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 40.7128\n lon -74.006\n timezone \"America/New_York\"\n }\n current {\n temperature 75.0\n feels_like 75.0\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n humidity 50\n pressure 1013\n wind_speed 5.5\n wind_direction 180\n visibility 10000\n uv_index 6.5\n clouds 10\n }\n daily_forecast [\n {\n date \"2025-08-12T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with clear spells\"\n temperature {\n day 88.46600000000001\n min 65.984\n max 88.46600000000001\n night 76.15400000000005\n }\n conditions {\n id 801\n main \"Clouds\"\n description \"few clouds\"\n icon \"02d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-13T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 87.35\n min 71.00600000000003\n max 87.85400000000006\n night 80.40200000000007\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 4.89\n },\n {\n date \"2025-08-14T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 81.68000000000004\n min 76.424\n max 88.016\n night 80.79800000000003\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 2.85\n },\n {\n date \"2025-08-15T17:00:00.000Z\"\n summary \"You can expect clear sky in the morning, with partly cloudy in the afternoon\"\n temperature {\n day 84.90200000000007\n min 75.72200000000004\n max 85.53200000000001\n night 76.55\n }\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-16T17:00:00.000Z\"\n summary \"There will be partly cloudy today\"\n temperature {\n day 81.57200000000003\n min 74.33600000000007\n max 81.57200000000003\n night 76.55\n }\n conditions {\n id 803\n main \"Clouds\"\n description \"broken clouds\"\n icon \"04d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-17T17:00:00.000Z\"\n summary \"There will be partly cloudy today\"\n temperature {\n day 90.64400000000008\n min 73.90400000000005\n max 90.64400000000008\n night 83.67800000000007\n }\n conditions {\n id 801\n main \"Clouds\"\n description \"few clouds\"\n icon \"02d\"\n }\n precipitation_probability 0.12\n rain 0\n },\n {\n date \"2025-08-18T16:00:00.000Z\"\n summary \"There will be rain until morning, then partly cloudy\"\n temperature {\n day 85.1\n min 75.99199999999999\n max 90.95\n night 80.25800000000001\n }\n conditions {\n id 500\n main \"Rain\"\n description \"light rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 2.4\n },\n {\n date \"2025-08-19T16:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 80.49199999999999\n min 76.13600000000007\n max 84.81200000000005\n night 76.13600000000007\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 11.61\n }\n ]\n alerts [\n {\n sender_name \"NWS Upton NY\"\n event \"Air Quality Alert\"\n start 1755004380\n end 1755054000\n description \"The New York State Department of Environmental Conservation has\\nissued an Air Quality Health Advisory for the following counties:\\n\\nNew York, Bronx, Kings, Queens, Richmond, Nassau, Suffolk,\\nWestchester, Rockland, Orange, Putnam.\\n\\nuntil 11 PM EDT this evening.\\n\\nAir quality levels in outdoor air are predicted to be greater than\\nan Air Quality Index value of 100 for the pollutant of Ground Level\\nOzone. The Air Quality Index, or AQI, was created as an easy way to\\ncorrelate levels of different pollutants to one scale. The higher\\nthe AQI value, the greater the health concern.\\n\\nWhen pollution levels are elevated, the New York State Department of\\nHealth recommends that individuals consider limiting strenuous\\noutdoor physical activity to reduce the risk of adverse health\\neffects. People who may be especially sensitive to the effects of\\nelevated levels of pollutants include the very young, and those with\\npreexisting respiratory problems such as asthma or heart disease.\\nThose with symptoms should consider consulting their personal\\nphysician.\\n\\nFor additional information, please visit the New York State\\nDepartment of Environmental Conservation website at,\\nhttps://on.ny.gov/nyaqi, or call the Air Quality Hotline at\\n1 800 5 3 5, 1 3 4 5.\"\n tags [\n \"Air quality\"\n ]\n }\n ]\n }\n unit \"F\"\n }\n}\n\ntest test_weather_lines_celsius {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 40.7128\n lon -74.006\n timezone \"America/New_York\"\n }\n current {\n temperature 23.9\n feels_like 23.9\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n humidity 50\n pressure 1013\n wind_speed 5.5\n wind_direction 180\n visibility 10000\n uv_index 6.5\n clouds 10\n }\n daily_forecast [\n {\n date \"2025-08-12T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with clear spells\"\n temperature {\n day 31.37\n min 18.88\n max 31.37\n night 24.53\n }\n conditions {\n id 801\n main \"Clouds\"\n description \"few clouds\"\n icon \"02d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-13T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 30.75\n min 21.67\n max 31.03\n night 26.89\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 4.89\n }\n ]\n alerts []\n }\n unit \"C\"\n }\n}\n\ntest test_weather_with_memory_cold_preference {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 43.6532\n lon -79.3832\n timezone \"America/Toronto\"\n }\n current {\n temperature 2.0\n feels_like -3.0\n conditions {\n id 600\n main \"Snow\"\n description \"light snow\"\n icon \"13d\"\n }\n humidity 85\n pressure 1015\n wind_speed 8.5\n wind_direction 320\n visibility 5000\n uv_index 1.0\n clouds 90\n }\n daily_forecast [\n {\n date \"2025-11-12T17:00:00.000Z\"\n summary \"Expect a day of snow with cold temperatures\"\n temperature {\n day 2.0\n min -2.0\n max 3.0\n night -1.0\n }\n conditions {\n id 600\n main \"Snow\"\n description \"light snow\"\n icon \"13d\"\n }\n precipitation_probability 0.8\n rain 0\n },\n {\n date \"2025-11-13T17:00:00.000Z\"\n summary \"Cold and clear skies\"\n temperature {\n day 1.0\n min -4.0\n max 2.0\n night -3.0\n }\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n precipitation_probability 0.1\n rain 0\n }\n ]\n alerts []\n }\n unit \"C\"\n memory {\n userName \"Ajay\"\n userFacts [\n \"Location: Toronto\",\n \"Age: 37\"\n ]\n deductiveFacts [\n \"User likes cold weather because they are from Canada\"\n ]\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_weather_with_memory_no_deductions {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 37.7749\n lon -122.4194\n timezone \"America/Los_Angeles\"\n }\n current {\n temperature 18.0\n feels_like 17.0\n conditions {\n id 500\n main \"Rain\"\n description \"light rain\"\n icon \"10d\"\n }\n humidity 70\n pressure 1012\n wind_speed 4.5\n wind_direction 270\n visibility 8000\n uv_index 2.0\n clouds 75\n }\n daily_forecast [\n {\n date \"2025-11-12T17:00:00.000Z\"\n summary \"Rainy day with mild temperatures\"\n temperature {\n day 18.0\n min 14.0\n max 20.0\n night 15.0\n }\n conditions {\n id 500\n main \"Rain\"\n description \"light rain\"\n icon \"10d\"\n }\n precipitation_probability 0.9\n rain 5.2\n },\n {\n date \"2025-11-13T17:00:00.000Z\"\n summary \"Partly cloudy with chance of rain\"\n temperature {\n day 19.0\n min 15.0\n max 21.0\n night 16.0\n }\n conditions {\n id 802\n main \"Clouds\"\n description \"scattered clouds\"\n icon \"03d\"\n }\n precipitation_probability 0.4\n rain 1.2\n }\n ]\n alerts []\n }\n unit \"C\"\n memory {\n userName \"Sarah\"\n userFacts [\n \"Children: Two daughters (ages 5 and 2)\",\n \"Occupation: Founder of a technology company\"\n ]\n deductiveFacts []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}", } export const getBamlFiles = () => { diff --git a/apps/application/src/baml_client/partial_types.ts b/apps/application/src/baml_client/partial_types.ts index 46e23b9..e552a60 100644 --- a/apps/application/src/baml_client/partial_types.ts +++ b/apps/application/src/baml_client/partial_types.ts @@ -96,6 +96,11 @@ export namespace partial_types { peerCard: string[] recentMessages: string[] sessionSummaries: string[] + crossPeerPerspectives: CrossPeerPerspective[] + } + export interface CrossPeerPerspective { + label?: string | null + perspective?: string | null } export interface MemoryCore { userName?: string | null diff --git a/apps/application/src/baml_client/type_builder.ts b/apps/application/src/baml_client/type_builder.ts index e128fee..76b2ec8 100644 --- a/apps/application/src/baml_client/type_builder.ts +++ b/apps/application/src/baml_client/type_builder.ts @@ -45,7 +45,7 @@ export default class TypeBuilder { LocationLite: ClassViewer<'LocationLite', "lat" | "lon" | "timezone">; - MemoryContext: ClassViewer<'MemoryContext', "explicitFacts" | "deductiveFacts" | "peerCard" | "recentMessages" | "sessionSummaries">; + MemoryContext: ClassViewer<'MemoryContext', "explicitFacts" | "deductiveFacts" | "peerCard" | "recentMessages" | "sessionSummaries" | "crossPeerPerspectives">; MemoryCore: ClassViewer<'MemoryCore', "userName" | "userFacts" | "deductiveFacts">; @@ -127,7 +127,7 @@ export default class TypeBuilder { ]); this.MemoryContext = this.tb.classViewer("MemoryContext", [ - "explicitFacts","deductiveFacts","peerCard","recentMessages","sessionSummaries", + "explicitFacts","deductiveFacts","peerCard","recentMessages","sessionSummaries","crossPeerPerspectives", ]); this.MemoryCore = this.tb.classViewer("MemoryCore", [ diff --git a/apps/application/src/baml_client/types.ts b/apps/application/src/baml_client/types.ts index b8325a7..41f1f7c 100644 --- a/apps/application/src/baml_client/types.ts +++ b/apps/application/src/baml_client/types.ts @@ -141,6 +141,13 @@ export interface MemoryContext { peerCard: string[] recentMessages: string[] sessionSummaries: string[] + crossPeerPerspectives: CrossPeerPerspective[] + +} + +export interface CrossPeerPerspective { + label: string + perspective: string } diff --git a/apps/application/src/handlers/memory.ts b/apps/application/src/handlers/memory.ts index 11ee0a5..104c4f1 100644 --- a/apps/application/src/handlers/memory.ts +++ b/apps/application/src/handlers/memory.ts @@ -1,11 +1,13 @@ import { b } from "@clairvoyant/baml-client"; import { api } from "@convex/_generated/api"; import type { Id } from "@convex/_generated/dataModel"; +import { Honcho } from "@honcho-ai/sdk"; import type { Peer, Session } from "@honcho-ai/sdk"; import type { AppSession } from "@mentra/sdk"; import { updateConversationResponse } from "../core/conversationLogger"; import { checkUserIsPro, convexClient } from "../core/convex"; import type { DisplayQueueManager } from "../core/displayQueue"; +import { env } from "../core/env"; const memoryRunCallIds = new WeakMap(); @@ -198,6 +200,82 @@ export async function MemoryRecall( peerRep = { explicit: [], deductive: [] }; } + // Cross-peer queries: fetch perspectives from connected users + let crossPeerPerspectives: Array<{ label: string; perspective: string }> = []; + try { + const connections = await convexClient.query( + api.connections.getActiveSharedMemoryConnectionsByMentraId, + { mentraUserId }, + ); + + const cappedConnections = connections.slice(0, 3); + if (cappedConnections.length > 0) { + session.logger.info( + `[startMemoryRecallFlow] Querying ${cappedConnections.length} cross-peer connections`, + ); + + const honchoClient = new Honcho({ + apiKey: env.HONCHO_API_KEY, + environment: "production", + workspaceId: "with-context", + }); + + const rawPerspectives = await Promise.all( + cappedConnections.map(async (conn) => { + try { + const connectedPeer = await honchoClient.peer( + `${conn.connectedUserId}-diatribe`, + ); + const rep = await connectedPeer.representation({ + target: `${userId}-diatribe`, + searchQuery: textQuery, + searchTopK: 5, + maxConclusions: 10, + }); + return { + label: conn.label ?? "Connected user", + perspective: typeof rep.representation === "string" ? rep.representation : "", + }; + } catch (error) { + session.logger.warn( + `[startMemoryRecallFlow] Cross-peer query failed for ${conn.connectedUserId}: ${error}`, + ); + return null; + } + }), + ); + + const validPerspectives = rawPerspectives.filter( + (p): p is { label: string; perspective: string } => + p !== null && p.perspective.length > 0, + ); + + // Sensitivity gate + const sensitivityResults = await Promise.all( + validPerspectives.map(async (p) => { + try { + const category = await b.CheckSensitivity(p.perspective); + return { ...p, category }; + } catch { + return { ...p, category: "SENSITIVE" as const }; + } + }), + ); + + crossPeerPerspectives = sensitivityResults + .filter((p) => p.category === "SAFE") + .map(({ label, perspective }) => ({ label, perspective })); + + session.logger.info( + `[startMemoryRecallFlow] Cross-peer: ${validPerspectives.length} valid, ${crossPeerPerspectives.length} safe`, + ); + } + } catch (error) { + session.logger.warn( + `[startMemoryRecallFlow] Cross-peer lookup failed: ${error}`, + ); + } + // Build memory context const memoryContext = { explicitFacts: peerRep.explicit.map((e) => e.content), @@ -207,6 +285,7 @@ export async function MemoryRecall( peerCard: contextData.peerCard, recentMessages: contextData.messages.slice(-5).map((m) => m.content), sessionSummaries, + crossPeerPerspectives, }; // Synthesize response with BAML (replaces .chat() call) diff --git a/baml_src/chat.baml b/baml_src/chat.baml index 363d365..58f2e67 100644 --- a/baml_src/chat.baml +++ b/baml_src/chat.baml @@ -29,6 +29,7 @@ class ChatContext { userFacts string[] @description("Known facts about the user") deductiveFacts string[] @description("Inferred facts about the user") conversationHistory ChatConversationMessage[] @description("Previous messages in this chat") + crossPeerPerspectives CrossPeerPerspective[] @description("Perspectives from connected users with shared memory enabled") } function InterpretChatMessage( @@ -50,6 +51,7 @@ Style: - Reference session context naturally - Keep responses brief - this is chat, not an essay - Don't be overly enthusiastic or use excessive exclamation marks +- If connected user perspectives are available, weave them naturally with attribution (e.g. "Your wife mentioned...", "Alex has been looking into...") DATE: {{ context.date }} @@ -75,6 +77,14 @@ USER PROFILE: - {{ fact }} {% endfor %} +{% endif %} +{% if context.crossPeerPerspectives | length > 0 %} +CONNECTED USER PERSPECTIVES: +{% for p in context.crossPeerPerspectives %} +From "{{ p.label }}": +{{ p.perspective }} +{% endfor %} + {% endif %} {% if context.conversationHistory | length > 0 %} CONVERSATION HISTORY: @@ -114,6 +124,7 @@ test test_chat_simple_greeting { "User is interested in AI technologies" ] conversationHistory [] + crossPeerPerspectives [] } } } @@ -142,6 +153,7 @@ test test_chat_with_history { createdAt "2024-12-24T16:00:00Z" } ] + crossPeerPerspectives [] } } } @@ -157,6 +169,7 @@ test test_chat_no_sessions { userFacts [] deductiveFacts [] conversationHistory [] + crossPeerPerspectives [] } } } diff --git a/baml_src/synthesis.baml b/baml_src/synthesis.baml index f45d166..02c66cf 100644 --- a/baml_src/synthesis.baml +++ b/baml_src/synthesis.baml @@ -4,6 +4,7 @@ class MemoryContext { peerCard string[] recentMessages string[] sessionSummaries string[] @description("Summaries of past sessions, e.g. 'Dec 20: discussed weather and navigation'") + crossPeerPerspectives CrossPeerPerspective[] @description("Perspectives from connected users with shared memory enabled") } class MemorySynthesisLines { @@ -46,6 +47,14 @@ PAST SESSION SUMMARIES: - {{ summary }} {% endfor %} +{% if context.crossPeerPerspectives | length > 0 %} +CONNECTED USER PERSPECTIVES: +{% for p in context.crossPeerPerspectives %} +From "{{ p.label }}": +{{ p.perspective }} +{% endfor %} + +{% endif %} QUERY: {{ query }} Instructions: @@ -56,6 +65,7 @@ Instructions: - If you don't have enough information to answer, say so briefly - Focus on what's most relevant to the query - Ignore any alphanumeric IDs or hashes in the facts (e.g., "j979r44m..." prefixes) — extract only the meaningful content +- If connected users have relevant perspectives, naturally mention them (e.g. "Sarah thinks...", "Alex mentioned...") "# } @@ -78,6 +88,7 @@ test test_memory_synthesis_name { "What is my name?" ] sessionSummaries [] + crossPeerPerspectives [] } } @@assert({{ this.lines|length <= 3 }}) @@ -107,6 +118,7 @@ test test_memory_synthesis_children { "Tell me about my daughters" ] sessionSummaries [] + crossPeerPerspectives [] } } @@assert({{ this.lines|length <= 3 }}) @@ -130,6 +142,7 @@ test test_memory_synthesis_no_info { "What car do I drive?" ] sessionSummaries [] + crossPeerPerspectives [] } } @@assert({{ this.lines|length <= 3 }}) @@ -156,6 +169,7 @@ test test_memory_synthesis_age { "How old am I?" ] sessionSummaries [] + crossPeerPerspectives [] } } @@assert({{ this.lines|length <= 3 }}) diff --git a/docs/SHARED_MEMORY_PLAN.md b/docs/SHARED_MEMORY_PLAN.md index d391f7f..8a0898b 100644 --- a/docs/SHARED_MEMORY_PLAN.md +++ b/docs/SHARED_MEMORY_PLAN.md @@ -1,6 +1,6 @@ # Shared Memory Between Users — Implementation Plan -## Status: Architecture Finalized / Ready for Phase 0 +## Status: Phases 0-4 Complete / Ready for Phase 5 (Passive Sharing & Smart Detection) ## Problem @@ -221,15 +221,16 @@ When a connection is revoked (either side): ## Implementation Phases -### Phase 0: SDK Evaluation (prerequisite) +### Phase 0: SDK Evaluation ✅ COMPLETE **Goal**: Confirm `peer.chat(query, { target })` is available and plan upgrade if needed. -1. Check current `@honcho-ai/sdk` version in `package.json` against latest v3.x -2. Test if `peer.chat(query, { target })` (dialectic API) works with our version -3. If upgrade needed, plan a separate PR for SDK migration -4. Verify backward compatibility with existing `getContext()` / `addMessages()` patterns -5. **Decision gate**: If dialectic API is unavailable, we need an alternative approach (manual context merging via `getContext()` on both peers) +**Result**: Our `@honcho-ai/sdk@1.6.0` already supports the full v3 dialectic API including `peer.chat(query, { target })` for cross-peer queries. No SDK upgrade needed. + +- `peer.chat(query, { target?: string | Peer, session?: string | Session, reasoningLevel?: string })` — confirmed available +- `peer.context({ target })` — cross-peer context retrieval confirmed +- `peer.representation({ target })` — cross-peer representation confirmed +- Existing `session.getContext()` / `session.addMessages()` patterns remain compatible ### Phase 1: Connections (Convex only, no Honcho changes) @@ -272,14 +273,19 @@ When a connection is revoked (either side): 3. Update `InterpretEmailReply` prompt for attributed cross-peer weaving 4. Test: Ajay replies to an email note about a topic Sarah has discussed, her perspective enriches the reply -### Phase 4: Advanced Cross-Peer Features (future) +### Phase 4: Advanced Cross-Peer Features ✅ COMPLETE **Goal**: Deeper cross-peer reasoning and additional surfaces. -1. Use `session.working_rep(peerA, peerB)` for pre-computed cross-peer representations (faster than per-query `peer.chat`) -2. Extend to real-time glasses responses (MemoryRecall handler) for "What does Sarah think about X?" queries -3. Add cross-peer context to web chat (`chat.ts` `sendMessage`) for daily recap conversations -4. Group connections (3+ users, e.g., a team sharing context) +**Result**: All four sub-tasks implemented. + +1. **Optimized with `peer.representation()`**: Replaced `peer.chat()` with `peer.representation({ target, searchQuery, searchTopK: 5, maxConclusions: 10 })` in `followupsChat.ts` and `emailReply.ts`. The `representation()` method returns pre-computed representations without agentic reasoning, which is faster since BAML handles interpretation. Applied across all integration surfaces. + +2. **Extended MemoryRecall for glasses**: Added cross-peer support to `apps/application/src/handlers/memory.ts`. Uses public query `connections.getActiveSharedMemoryConnectionsByMentraId` (since the app layer can't access internal queries), queries up to 3 connected peers via `peer.representation()`, gates through `b.CheckSensitivity()` directly, and passes `crossPeerPerspectives` to `b.SynthesizeMemory()`. Updated `baml_src/synthesis.baml` with cross-peer context in `MemoryContext` class and prompt. + +3. **Added cross-peer to web chat**: Extended `packages/convex/chat.ts` `sendMessage` with the same cross-peer query pattern (connections → representation → sensitivity gate → BAML). Updated `baml_src/chat.baml` `ChatContext` class with `crossPeerPerspectives` field and prompt section. Updated `bamlActions.ts` `interpretChatMessage` args. + +4. **Group connections**: Added `connectionGroups` and `connectionGroupMembers` tables to schema. Added mutations (`createConnectionGroup`, `acceptGroupInvite`, `leaveConnectionGroup`, `toggleGroupSharedMemory`, `updateGroupMemberLabel`), public query (`getGroupsForUser`), and internal query (`getActiveSharedMemoryGroupMembers`) to `connections.ts`. ### Phase 5: Passive Sharing & Smart Detection (future) diff --git a/packages/baml_client/baml_client/inlinedbaml.ts b/packages/baml_client/baml_client/inlinedbaml.ts index 7a44da8..f005229 100644 --- a/packages/baml_client/baml_client/inlinedbaml.ts +++ b/packages/baml_client/baml_client/inlinedbaml.ts @@ -34,7 +34,7 @@ const fileMap = { "route.baml": "enum Router {\n WEATHER @description(\"Current or upcoming weather questions for a specific place.\")\n WEB_SEARCH @description(\"News, current events, facts that change over time such as political events, or topics not obviously location-based.\")\n MAPS @description(\"Finding nearby businesses, restaurants, addresses, or directions.\")\n KNOWLEDGE @description(\"General knowledge that does not fit into other categories.\")\n MEMORY_CAPTURE @description(\"Commands to store new personal facts, preferences, or reminders for future recall.\")\n MEMORY_RECALL @description(\"Questions about the user's memory and personal history, personal preferences, personal opinions, goals, information about the user, or anything that is not a fact.\")\n NOTE_THIS @description(\"User wants to save the current conversation as a note to email. Triggered by phrases like 'add this to a note', 'send this to my email', 'this would be great to remember', 'note this down'.\")\n FOLLOW_UP @description(\"User wants to bookmark or revisit the current topic later. Triggered by: 'follow up on this', 'come back to this', 'revisit this later', 'bookmark this', 'I want to revisit this'.\")\n PASSTHROUGH @description(\"Ambient speech, filler words, incomplete sentences, or unclear utterances that don't require action.\")\n}\n\nclass RoutingBehavior {\n origin string @description(\"Echo of the user's input text verbatim.\")\n routing Router\n}\n\nfunction Route(text: string) -> RoutingBehavior {\n client \"GroqHeavy\"\n prompt #\"\n You are routing a single short utterance to one of several intent categories.\n\n STEP 1 — MENTALLY CLEAN THE UTTERANCE:\n Before routing, internally strip filler and hesitation words like: \"um\", \"uh\", \"hmm\", \"erm\",\n \"like\", \"you know\", \"uh yeah so\", \"okay so\", \"so yeah\", repeated words, false starts,\n and mid-sentence corrections.\n - If, after removing these, there is NO clear question or command left, treat as PASSTHROUGH.\n - If there IS a clear question or command after cleaning, route based on the cleaned meaning.\n - Do NOT remove words that change the core intent (e.g., \"okay\" at the start of a command is fine to keep).\n\n STEP 2 — DETERMINE DIRECTEDNESS:\n Decide if this utterance is DIRECTED AT THE ASSISTANT vs AMBIENT/BACKGROUND speech.\n\n Signs of DIRECTED speech (route to a real category):\n - Question forms: \"what\", \"who\", \"where\", \"when\", \"how\", \"why\", \"can you\", \"could you\", \"is it\"\n - Commands/imperatives: \"remind me\", \"remember that\", \"find me\", \"tell me\", \"look up\", \"show me\"\n - Personal intentions with time references: \"I need to... tomorrow\", \"I should... later\"\n - Assistant wake word or second-person address\n\n Signs of AMBIENT/BACKGROUND speech (PASSTHROUGH):\n - Movie, TV, or audiobook dialogue (dramatic lines, exclamations, third-person narrative)\n e.g., \"I'll never let you go Jack!\", \"You can't handle the truth!\", \"No Luke, I am your father\"\n - Other people's conversations (named characters talking to each other, no request to assistant)\n - Narration or storytelling not involving a request\n - Pure reactions with no follow-up: \"oh wow\", \"no way\", \"that's crazy\", \"ha ha\"\n - Background announcements: \"The next station is Times Square\"\n\n EXCEPTION: If a movie/TV line is quoted AS PART OF a question (e.g., \"What movie is 'I'll never\n let you go Jack' from?\"), do NOT treat as PASSTHROUGH — route to WEB_SEARCH or KNOWLEDGE.\n\n STEP 3 — CHOOSE EXACTLY ONE ROUTE:\n Routes (in priority order when multiple could apply):\n 1. WEATHER → questions about forecast or current weather for a place\n 2. MAPS → requests to find locations, directions, or nearby businesses\n 3. WEB_SEARCH → current events or information likely needing the internet\n 4. KNOWLEDGE → general facts that are stable over time\n 5. MEMORY_CAPTURE → commands or clear intentions to remember/store personal facts, preferences,\n or reminders — including indirect phrasing like \"I need to call the dentist tomorrow\"\n 6. MEMORY_RECALL → questions about the user's past, preferences, or personal information\n 7. NOTE_THIS → requests to save/note/email the current conversation, e.g., \"add this to a note\",\n \"send this to my email\", \"this would be great to remember\", \"note this down\", \"save this\"\n 8. FOLLOW_UP → requests to bookmark or revisit the current topic later, e.g., \"follow up on this\",\n \"come back to this\", \"revisit this later\", \"bookmark this\", \"I want to revisit this\"\n 9. PASSTHROUGH → pure filler, incomplete fragments with no clear request, ambient speech,\n or unclear utterances that don't require action\n\n Additional guidelines:\n - If the utterance mentions weather/forecast, choose WEATHER even if a location is mentioned\n - If the utterance is primarily about finding a place or directions, choose MAPS\n - When in doubt between KNOWLEDGE and WEB_SEARCH, prefer WEB_SEARCH for anything that might change\n - For vague self-talk without clear time/action (\"maybe someday I should...\"), prefer PASSTHROUGH\n - For clear near-term intentions (\"I need to call mom tomorrow\"), prefer MEMORY_CAPTURE\n\n {{ _.role(\"user\") }} {{ text }}\n {{ ctx.output_format }}\n \"#\n}\n\ntest test_weather {\n functions [Route]\n args {\n text \"What is the weather in San Francisco?\"\n }\n @@assert( {{ this.routing == \"WEATHER\"}})\n}\n\ntest test_web_search {\n functions [Route]\n args {\n text \"Who is the current president of the United States?\"\n }\n @@assert( {{ this.routing == \"WEB_SEARCH\"}})\n}\n\ntest test_maps {\n functions [Route]\n args {\n text \"Find me a ramen restaurant near Union Square.\"\n }\n @@assert( {{ this.routing == \"MAPS\"}})\n}\n\ntest test_knowledge {\n functions [Route]\n args {\n text \"What is the capital of France?\"\n }\n @@assert( {{ this.routing == \"KNOWLEDGE\"}})\n}\n\ntest test_knowledge_2 {\n functions [Route]\n args {\n text \"What is the purpose of mitochondria?\"\n }\n @@assert( {{ this.routing == \"KNOWLEDGE\"}})\n}\n\ntest test_memory {\n functions [Route]\n args {\n text \"What is my name?\"\n }\n @@assert( {{ this.routing == \"MEMORY_RECALL\"}})\n}\n\ntest test_memory_2 {\n functions [Route]\n args {\n text \"Koyal, what did I eat yesterday?\"\n }\n @@assert( {{ this.routing == \"MEMORY_RECALL\"}})\n}\n\ntest test_memory_capture {\n functions [Route]\n args {\n text \"Remember that my sister's birthday is May 3rd.\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_memory_capture_task {\n functions [Route]\n args {\n text \"Please remember I need to call the dentist tomorrow.\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_memory_capture_task_2 {\n functions [Route]\n args {\n text \"I need to call the dentist tomorrow.\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_passthrough_filler {\n functions [Route]\n args {\n text \"hmm\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_okay {\n functions [Route]\n args {\n text \"okay\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_fragment {\n functions [Route]\n args {\n text \"uh yeah so\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_weather_with_location {\n functions [Route]\n args {\n text \"What's the weather like for my trip to Paris?\"\n }\n @@assert( {{ this.routing == \"WEATHER\"}})\n}\n\n// ===== FILLER WORD TESTS =====\n// These test that fillers are stripped and the real intent is routed correctly\n\ntest test_weather_with_fillers {\n functions [Route]\n args {\n text \"um what's the weather uh in SF\"\n }\n @@assert( {{ this.routing == \"WEATHER\"}})\n}\n\ntest test_maps_with_fillers {\n functions [Route]\n args {\n text \"uh like find me a coffee shop you know nearby\"\n }\n @@assert( {{ this.routing == \"MAPS\"}})\n}\n\ntest test_knowledge_with_fillers {\n functions [Route]\n args {\n text \"so like um what is the capital of Japan\"\n }\n @@assert( {{ this.routing == \"KNOWLEDGE\"}})\n}\n\ntest test_web_search_with_fillers {\n functions [Route]\n args {\n text \"hmm uh who won the game last night\"\n }\n @@assert( {{ this.routing == \"WEB_SEARCH\"}})\n}\n\ntest test_memory_capture_with_fillers {\n functions [Route]\n args {\n text \"um yeah so remind me to call mom tomorrow\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\n// ===== AMBIENT/MOVIE DIALOGUE TESTS =====\n// These should be PASSTHROUGH as they are background speech, not directed at assistant\n\ntest test_passthrough_movie_titanic {\n functions [Route]\n args {\n text \"I'll never let you go Jack!\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_movie_star_wars {\n functions [Route]\n args {\n text \"No Luke, I am your father\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_movie_few_good_men {\n functions [Route]\n args {\n text \"You can't handle the truth!\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_tv_dialogue {\n functions [Route]\n args {\n text \"Ross, we were on a break!\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_background_announcement {\n functions [Route]\n args {\n text \"The next station is Times Square\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_reaction {\n functions [Route]\n args {\n text \"oh wow that's crazy\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_narration {\n functions [Route]\n args {\n text \"He walked into the room and never looked back\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\n// ===== EDGE CASES =====\n// Movie quote AS PART OF a question should NOT be passthrough\n\ntest test_movie_quote_in_question {\n functions [Route]\n args {\n text \"What movie is 'I'll never let you go Jack' from?\"\n }\n @@assert( {{ this.routing != \"PASSTHROUGH\"}})\n}\n\n// Self-talk with clear near-term intention should be MEMORY_CAPTURE\n\ntest test_self_talk_clear_intent {\n functions [Route]\n args {\n text \"I need to pick up groceries on the way home\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_self_talk_should_call {\n functions [Route]\n args {\n text \"I should call the doctor tomorrow\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\n// Vague self-talk without clear action should be PASSTHROUGH\n\ntest test_self_talk_vague {\n functions [Route]\n args {\n text \"maybe someday I should learn to play guitar\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\n// ===== NOTE_THIS TESTS =====\n// Requests to save/note/email the current conversation\n\ntest test_note_this_add_to_note {\n functions [Route]\n args {\n text \"add this to a note\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\ntest test_note_this_email_me {\n functions [Route]\n args {\n text \"email me this conversation\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\ntest test_note_this_remember {\n functions [Route]\n args {\n text \"this would be great to remember\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\ntest test_note_this_save {\n functions [Route]\n args {\n text \"save this to my notes\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\n// ===== FOLLOW_UP TESTS =====\n// Requests to bookmark or revisit the current topic later\n\ntest test_follow_up_later {\n functions [Route]\n args {\n text \"follow up on this later\"\n }\n @@assert( {{ this.routing == \"FOLLOW_UP\"}})\n}\n\ntest test_follow_up_come_back {\n functions [Route]\n args {\n text \"I want to come back to this\"\n }\n @@assert( {{ this.routing == \"FOLLOW_UP\"}})\n}\n\ntest test_follow_up_bookmark {\n functions [Route]\n args {\n text \"bookmark this\"\n }\n @@assert( {{ this.routing == \"FOLLOW_UP\"}})\n}\n\ntest test_follow_up_revisit {\n functions [Route]\n args {\n text \"revisit this later\"\n }\n @@assert( {{ this.routing == \"FOLLOW_UP\"}})\n}", "search.baml": "class NewsItem {\n title string\n content string\n}\n\nclass AnswerLines {\n lines string[]\n}\n\nclass QueryResult {\n query string\n results AnswerLines[]\n}\n\nfunction AnswerSearch(query: string, searchResults: NewsItem[], memory: MemoryCore?) -> QueryResult {\n client \"Groq\"\n prompt #\"\n {{ _.role(\"system\")}}\n You are a helpful Web Search Summarizer who summarizes the results of a web search. \n You need give an answer to the following question: {{ query }} given the results of the web search below collected by the user. \n The output should be lines of text that sound like a human would say them to a friend and no more than 10 words per line as per the output format and no more than 3 lines as per the output format: \n \n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (use to personalize response and acknowledge past searches):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n \n If the user has searched for related topics before, naturally acknowledge this (e.g., \"You searched for X yesterday...\" or \"Following up on your interest in Y...\"). Keep it conversational.\n {% endif %}\n {% endif %}\n \n {{ ctx.output_format }}\n {{ _.role(\"user\") }}\n {% for result in searchResults %}\n Title:\n {{ result.title }}\n {{- \"\\n\" -}}\n Content:\n {{ result.content }}\n {{- \"\\n\" -}}\n {% endfor %}\n \"#\n}\n\ntest test_answer_search {\n functions [AnswerSearch]\n args {\n query \"Did Charlie Kirk die?\"\n memory null\n searchResults [\n {\n title \"California state Sen. Scott Wiener labels Charlie Kirk 'a vile bigot who' normalized 'dehumanization' - Fox News\"\n content \"### Recommended Videos Charlie Kirk # California state Sen. Scott Wiener labels Charlie Kirk 'a vile bigot who' normalized 'dehumanization' 2 min \\\"Charlie Kirk did not deserve to die. Also Charlie Kirk was a vile bigot who did immeasurable harm to so many people by normalizing dehumanization. **CHARLIE KIRK VIGILS HELD AT UNIVERSITIES ACROSS AMERICA FOLLOWING ASSASSINATION OF CONSERVATIVE ACTIVIST** \\\"Multiple things can be true: Political violence is toxic & Kirk's assassination must be condemned. Charlie Kirk poses at The Cambridge Union on May 19, 2025 in Cambridge, Cambridgeshire. (Nordin Catic/Getty Images for The Cambridge Union) Video \\\"Charlie Kirk's murder is horrific. 21 mins ago #### California state Sen. Scott Wiener labels Charlie Kirk 'a vile bigot who' normalized 'dehumanization' 1 hour ago 3 hours ago\"\n },\n {\n title \"Erika Kirk delivers moving tribute to husband, Charlie: 'I will never let your legacy die' - Fox News\"\n content \"# Erika Kirk delivers moving tribute to husband, Charlie: 'I will never let your legacy die' #### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' Erika Kirk, the widow of Charlie Kirk, made her first public remarks since her husband's death. Erika Kirk, the widow of the late Charlie Kirk, gave an emotional tribute to her husband and declared that his mission will not end at Turning Point USA's headquarters Friday. Erika Kirk spoke for the first time since the assassination of her husband, Charlie Kirk, at Turning Point USA Sept. #### Erika Kirk delivers moving tribute to husband, Charlie: 'I will never let your legacy die'\"\n },\n {\n title \"Opinion | Charlie Kirk and the Future of Political Violence - The New York Times\"\n content \"Opinion | Charlie Kirk and the Future of Political Violence - The New York Times Opinion|If We Keep This Up, Charlie Kirk Will Not Be the Last to Die https://www.nytimes.com/2025/09/11/opinion/charlie-kirk-assassination-debate.html If We Keep This Up, Charlie Kirk Will Not Be the Last to Die An assassin's bullet cut down Charlie Kirk, one of the nation's most prominent conservative activists and commentators, at a public event on the campus of Utah Valley University. When an assassin shot Kirk, that person killed a man countless students felt like they knew, and the assassin killed him _on a college campus_. Subscribe to The Times to read as many articles as you like. * Your Privacy Choices\"\n },\n {\n title \"Life, Liberty & Levin - Saturday, September 13 - Fox News\"\n content \"Watch the live stream of Fox News and full episodes. Charlie Kirk, Tribute, Legacy Fox News Channel Charlie Kirk: An American Original FOX News Radio Live Channel Coverage Fox News Channel Live ### Bill Maher calls for people to stop comparing Trump to Hitler following Charlie Kirk's assassination ### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' ### Charlie Kirk assassin was 'much closer to mainstream Democrat than any of us wanted to believe': Tim Pool ### Lara Trump says media played role in Charlie Kirk assassination ### Illegal immigrant suspect dead after dragging agent with car, DHS reports ### Friend of Charlie Kirk speaks to his legacy\"\n },\n {\n title \"Kilmeade apologizes for remarks on homelessness, mental illness - Fox News\"\n content \"Watch the live stream of Fox News and full episodes. Fox News Channel FOX News Radio Live Channel Coverage Fox News Channel Live ### Bill Maher calls for people to stop comparing Trump to Hitler following Charlie Kirk's assassination ### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' ### Mark Levin says he knew right away how special Charlie Kirk was ### Lara Trump says media played role in Charlie Kirk assassination ### Questions remain on how suspect in Charlie Kirk murder got access to roof ### Dean Phillips: Part of our core American principles were assassinated with Charlie Kirk ### Alleged assassin of Charlie Kirk held in Utah jail without bail\"\n },\n {\n title \"'His voice will remain,' Charlie Kirk's widow vows after suspect arrested - BBC\"\n content \"* \\\"I will never let his legacy die,\\\" Charlie Kirk's wife says in her first public comments since he was fatally shot at a Utah university campus on Wednesday * Erika vowed to never let Charlie's legacy die, adding that his message will carry on being shared through his campus tour of US universities and his podcast - she did not specify how they would continue 2. ### 'My husband's voice will remain,' Charlie Kirk's widow says as suspected killer in custodypublished at 08:15 BST 08:15 BST published at 03:15 03:15 His widow, Erika Kirk, vows to never let his legacy die, saying in her first public statement on Friday: \\\"The movement my husband built will not die.\\\"\"\n },\n {\n title \"Country singer warns that Charlie Kirk's death has 'awoken' millions - New York Post\"\n content \"For all the people or the hateful people out there, the groups that thought that would quiet Charlie Kirk, you've just awoken millions of other people that are not scared to die, Adcock said Monday on 'The Ingraham Angle.' 'If you live in the life of the Lord and believe in Jesus, you shouldn't be scared to leave this world, and Charlie Kirk was a great example of that.' Adcock had never met Kirk but said he often watched videos of his debates on social media. \\\"For all the people or the hateful people out there, the groups that thought that would quiet Charlie Kirk, you've just awoken millions of other people that are not scared to die,\\\" country artist Gavin Adcock said. Adcock had never met Kirk but said he watched videos of him.\"\n },\n {\n title \"Black Pastor Blasts Efforts To Whitewash Charlie Kirk's Legacy In The Wake Of His Killing - HuffPost\"\n content \"The Rev. Howard-John Wesley on Sunday spoke out against efforts to whitewash the legacy of right-wing political activist Charlie Kirk in the wake of his assassination on a Utah college campus last week, declaring that how you die does not redeem how you live. (Watch video below). The Trump administration has taken steps to honor Kirk following his death. Maggie Haberman Says Trump Is 'Struggling' With Messaging Around Charlie Kirk's Death Charlie Kirk Shooting Suspect Captured, Trump And Utah Authorities Announce GOP Lawmaker Calls Out Trump's 'Over The Top' Rhetoric After Charlie Kirk Killing Wall Street Journal Warns Trump Of 'Dangerous Moment' After Charlie Kirk Assassination\"\n },\n {\n title \"Country singer Gavin Adcock warns those who tried to silence Charlie Kirk have 'awoken' millions more - Fox News\"\n content \"# Country singer Gavin Adcock warns those who tried to silence Charlie Kirk have 'awoken' millions more ## Fans chanted 'Charlie Kirk' as Adcock waved an American flag on stage over the weekend Country singer Gavin Adcock explains why he paid tribute to Charlie Kirk in a series of shows on 'The Ingraham Angle.' Country artist Gavin Adcock warned those who thought they could silence Charlie Kirk that his death has only awakened millions more. \\\"For all the people or the hateful people out there, the groups that thought that would quiet Charlie Kirk, you've just awoken millions of other people that are not scared to die,\\\" Adcock said Monday on \\\"The Ingraham Angle.\\\"\"\n },\n {\n title \"Caroline Sunshine: The solution to America's void isn't banning guns or silencing speech - Fox News\"\n content \"Watch the live stream of Fox News and full episodes. The 'Fox News @ Night' panel discusses reactions to the assassination of Turning Point USA founder Charlie Kirk. Fox News Channel FOX News Radio Live Channel Coverage Fox News Channel Live ### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' ### Charlie Kirk assassin was 'much closer to mainstream Democrat than any of us wanted to believe': Tim Pool ### Donald Trump Jr on Charlie Kirk's assassination: The violence is 'not going both ways' ### 'We have him': Trump says suspected Charlie Kirk assassin is in custody ### Phoenix air traffic control honors Charlie Kirk: 'May God bless your family'\"\n }\n ]\n }\n @@assert({{ this.results[0].lines|length == 3 }})\n}\n", "session_summary.baml": "class SessionSummaryOutput {\n summary string @description(\"1-2 sentence summary of what was discussed\")\n topics string[] @description(\"2-5 key topics or themes from the session\")\n}\n\nfunction SummarizeSession(\n transcripts: string[]\n) -> SessionSummaryOutput {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are summarizing a conversation session from smart glasses.\n\n{{ ctx.output_format }}\n\nCONVERSATION TRANSCRIPTS:\n{% for transcript in transcripts %}\n- {{ transcript }}\n{% endfor %}\n\nInstructions:\n- First, filter out incomplete sentences, single words, filler words (e.g., \"um\", \"uh\", \"okay\"), and obvious transcription errors\n- Write a brief 1-2 sentence summary of what the user discussed or asked about\n- Extract 2-5 key topics or themes (e.g., \"weather\", \"navigation\", \"family\", \"work\")\n- Keep topics as single words or short phrases\n- Focus on what's memorable or might be referenced later\n- If transcripts are empty, trivial, or only contain noise/filler, return summary \"No meaningful activity\" with topics [\"none\"]\n\"#\n}\n\ntest test_summarize_session_weather {\n functions [SummarizeSession]\n args {\n transcripts [\n \"What's the weather like today?\",\n \"Is it going to rain later?\",\n \"Should I bring an umbrella?\"\n ]\n }\n @@assert({{ this.topics|length >= 1 }})\n @@assert({{ this.topics|length <= 5 }})\n}\n\ntest test_summarize_session_mixed {\n functions [SummarizeSession]\n args {\n transcripts [\n \"Navigate to the nearest coffee shop\",\n \"What time does Starbucks close?\",\n \"Remind me to call mom later\",\n \"What's on my calendar today?\"\n ]\n }\n @@assert({{ this.topics|length >= 2 }})\n @@assert({{ this.topics|length <= 5 }})\n}\n\ntest test_summarize_session_empty {\n functions [SummarizeSession]\n args {\n transcripts []\n }\n @@assert({{ this.summary|length > 0 }})\n}\n\ntest test_summarize_session_noise {\n functions [SummarizeSession]\n args {\n transcripts [\n \"um\",\n \"okay\",\n \"uh yeah\",\n \"hmm\"\n ]\n }\n @@assert({{ this.topics|length >= 1 }})\n}\n\ntest test_summarize_session_mixed_quality {\n functions [SummarizeSession]\n args {\n transcripts [\n \"um\",\n \"What's the weather today?\",\n \"okay\",\n \"Find me a coffee shop nearby\"\n ]\n }\n @@assert({{ this.topics|length >= 1 }})\n}\n", - "synthesis.baml": "class MemoryContext {\n explicitFacts string[]\n deductiveFacts string[]\n peerCard string[]\n recentMessages string[]\n sessionSummaries string[] @description(\"Summaries of past sessions, e.g. 'Dec 20: discussed weather and navigation'\")\n}\n\nclass MemorySynthesisLines {\n lines string[] @description(\"1-3 concise lines answering the query\")\n}\n\nfunction SynthesizeMemory(\n query: string,\n context: MemoryContext\n) -> MemorySynthesisLines {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are a memory synthesis agent. Answer the user's query using their stored memories.\n\n{{ ctx.output_format }}\n\nUSER'S BIOGRAPHICAL INFO:\n{% for fact in context.peerCard %}\n{{ fact }}\n{% endfor %}\n\nEXPLICIT FACTS (directly stated by user):\n{% for fact in context.explicitFacts %}\n- {{ fact }}\n{% endfor %}\n\nDEDUCTIVE CONCLUSIONS:\n{% for fact in context.deductiveFacts %}\n- {{ fact }}\n{% endfor %}\n\nRECENT CONVERSATION:\n{% for msg in context.recentMessages %}\n{{ msg }}\n{% endfor %}\n\nPAST SESSION SUMMARIES:\n{% for summary in context.sessionSummaries %}\n- {{ summary }}\n{% endfor %}\n\nQUERY: {{ query }}\n\nInstructions:\n- Provide 1-3 short lines (≤10 words each) that directly answer the query\n- Use a natural, conversational tone like texting a friend\n- If the user's name is known from peerCard, use it naturally\n- You can use contractions and casual language\n- If you don't have enough information to answer, say so briefly\n- Focus on what's most relevant to the query\n- Ignore any alphanumeric IDs or hashes in the facts (e.g., \"j979r44m...\" prefixes) — extract only the meaningful content\n\"#\n}\n\ntest test_memory_synthesis_name {\n functions [SynthesizeMemory]\n args {\n query \"What is my name?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"My name is Ajay Bhargava, that's spelled A-J-A-Y, last name B-H-A-R-G-A-V-A.\",\n \"What is my name?\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_children {\n functions [SynthesizeMemory]\n args {\n query \"Tell me about my daughters\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has two daughters.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters' names are Koyal and Kavya.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters are five and two years old.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has children of different ages.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Children: Two daughters (Koyal and Kavya)\",\n \"Daughters' ages: 5 and 2 years old\"\n ]\n recentMessages [\n \"I have two girls, five and two. Their names are Koyal and Kavya.\",\n \"Tell me about my daughters\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_no_info {\n functions [SynthesizeMemory]\n args {\n query \"What car do I drive?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\"\n ]\n recentMessages [\n \"What car do I drive?\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_age {\n functions [SynthesizeMemory]\n args {\n query \"How old am I?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's date of birth is December 19, 1987.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe is 37 years old.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"How old am I?\"\n ]\n sessionSummaries []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n", + "synthesis.baml": "class MemoryContext {\n explicitFacts string[]\n deductiveFacts string[]\n peerCard string[]\n recentMessages string[]\n sessionSummaries string[] @description(\"Summaries of past sessions, e.g. 'Dec 20: discussed weather and navigation'\")\n crossPeerPerspectives CrossPeerPerspective[] @description(\"Perspectives from connected users with shared memory enabled\")\n}\n\nclass MemorySynthesisLines {\n lines string[] @description(\"1-3 concise lines answering the query\")\n}\n\nfunction SynthesizeMemory(\n query: string,\n context: MemoryContext\n) -> MemorySynthesisLines {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are a memory synthesis agent. Answer the user's query using their stored memories.\n\n{{ ctx.output_format }}\n\nUSER'S BIOGRAPHICAL INFO:\n{% for fact in context.peerCard %}\n{{ fact }}\n{% endfor %}\n\nEXPLICIT FACTS (directly stated by user):\n{% for fact in context.explicitFacts %}\n- {{ fact }}\n{% endfor %}\n\nDEDUCTIVE CONCLUSIONS:\n{% for fact in context.deductiveFacts %}\n- {{ fact }}\n{% endfor %}\n\nRECENT CONVERSATION:\n{% for msg in context.recentMessages %}\n{{ msg }}\n{% endfor %}\n\nPAST SESSION SUMMARIES:\n{% for summary in context.sessionSummaries %}\n- {{ summary }}\n{% endfor %}\n\n{% if context.crossPeerPerspectives | length > 0 %}\nCONNECTED USER PERSPECTIVES:\n{% for p in context.crossPeerPerspectives %}\nFrom \"{{ p.label }}\":\n{{ p.perspective }}\n{% endfor %}\n\n{% endif %}\nQUERY: {{ query }}\n\nInstructions:\n- Provide 1-3 short lines (≤10 words each) that directly answer the query\n- Use a natural, conversational tone like texting a friend\n- If the user's name is known from peerCard, use it naturally\n- You can use contractions and casual language\n- If you don't have enough information to answer, say so briefly\n- Focus on what's most relevant to the query\n- Ignore any alphanumeric IDs or hashes in the facts (e.g., \"j979r44m...\" prefixes) — extract only the meaningful content\n- If connected users have relevant perspectives, naturally mention them (e.g. \"Sarah thinks...\", \"Alex mentioned...\")\n\"#\n}\n\ntest test_memory_synthesis_name {\n functions [SynthesizeMemory]\n args {\n query \"What is my name?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"My name is Ajay Bhargava, that's spelled A-J-A-Y, last name B-H-A-R-G-A-V-A.\",\n \"What is my name?\"\n ]\n sessionSummaries []\n crossPeerPerspectives []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_children {\n functions [SynthesizeMemory]\n args {\n query \"Tell me about my daughters\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has two daughters.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters' names are Koyal and Kavya.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters are five and two years old.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has children of different ages.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Children: Two daughters (Koyal and Kavya)\",\n \"Daughters' ages: 5 and 2 years old\"\n ]\n recentMessages [\n \"I have two girls, five and two. Their names are Koyal and Kavya.\",\n \"Tell me about my daughters\"\n ]\n sessionSummaries []\n crossPeerPerspectives []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_no_info {\n functions [SynthesizeMemory]\n args {\n query \"What car do I drive?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\"\n ]\n recentMessages [\n \"What car do I drive?\"\n ]\n sessionSummaries []\n crossPeerPerspectives []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_age {\n functions [SynthesizeMemory]\n args {\n query \"How old am I?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's date of birth is December 19, 1987.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe is 37 years old.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"How old am I?\"\n ]\n sessionSummaries []\n crossPeerPerspectives []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n", "weather.baml": "class WeatherConditionLite {\n id int\n main string\n description string\n icon string\n}\n\nclass CurrentLite {\n temperature float\n feels_like float\n conditions WeatherConditionLite\n humidity int\n pressure int\n wind_speed float\n wind_direction int\n visibility int\n uv_index float\n clouds int\n}\n\nclass TempBlock {\n day float\n min float\n max float\n night float\n}\n\nclass DailyForecastItem {\n date string\n summary string\n temperature TempBlock\n conditions WeatherConditionLite\n precipitation_probability float\n rain float\n}\n\nclass LocationLite {\n lat float\n lon float\n timezone string\n}\n\nclass AlertLite {\n sender_name string\n event string\n start int\n end int\n description string\n tags string[]\n}\n\nclass FormattedWeather {\n location LocationLite\n current CurrentLite\n daily_forecast DailyForecastItem[]\n alerts AlertLite[]\n}\n\nclass WeatherLines {\n lines string[]\n}\n\nfunction SummarizeWeatherFormatted(input: FormattedWeather, unit: string, memory: MemoryCore?) -> WeatherLines {\n client \"openai/gpt-4o-mini\"\n\n prompt #\"\n You are a witty, conversational weather reporter that's super brief even if you have a lot of data to work with.\n Write exactly 10 words that sound like a friendly human texted them to a friend.\n Avoid bullet points, lists, or emoji. \n Use the data to describe the weather on a scale of 1-10 out of 10 and include the \"vibe rating\" of the weather.\n {{ ctx.output_format }}\n\n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (weave naturally if relevant):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n {% endif %}\n {% endif %}\n\n Style rules:\n - You are only allowed to print 3 lines in the list output. \n - Each line can be a continuation of the previous line.\n - The first line should be the scale and the vibe rating.\n - The second line should be the current weather.\n - The third line should be the alert if there is one OR a personalized insight based on user context.\n - Always include the temperature unit symbol (e.g., \"72°F\" or \"22°C\") when mentioning temperatures.\n\n Data (verbatim):\n Location:\n timezone=\"{{ input.location.timezone }}\"\n Now:\n {% if unit == \"C\" %}\n temp_c={{ input.current.temperature }}\n feels_c={{ input.current.feels_like }}\n {% else %}\n temp_f={{ input.current.temperature }}\n feels_f={{ input.current.feels_like }}\n {% endif %}\n desc=\"{{ input.current.conditions.description }}\"\n wind_ms={{ input.current.wind_speed }}\n wind_deg={{ input.current.wind_direction }}\n humidity={{ input.current.humidity }}\n uv_index={{ input.current.uv_index }}\n clouds={{ input.current.clouds }}\n visibility_m={{ input.current.visibility }}\n Today:\n date=\"{{ input.daily_forecast[0].date if input.daily_forecast|length > 0 else \"\" }}\"\n {% if unit == \"C\" %}\n min_c={{ input.daily_forecast[0].temperature.min if input.daily_forecast|length > 0 else 0 }}\n max_c={{ input.daily_forecast[0].temperature.max if input.daily_forecast|length > 0 else 0 }}\n {% else %}\n min_f={{ input.daily_forecast[0].temperature.min if input.daily_forecast|length > 0 else 0 }}\n max_f={{ input.daily_forecast[0].temperature.max if input.daily_forecast|length > 0 else 0 }}\n {% endif %}\n pop={{ input.daily_forecast[0].precipitation_probability if input.daily_forecast|length > 0 else 0.0 }}\n rain_mm={{ input.daily_forecast[0].rain if input.daily_forecast|length > 0 else 0.0 }}\n desc=\"{{ input.daily_forecast[0].summary if input.daily_forecast|length > 0 else \"\" }}\"\n Tomorrow:\n date=\"{{ input.daily_forecast[1].date if input.daily_forecast|length > 1 else \"\" }}\"\n {% if unit == \"C\" %}\n min_c={{ input.daily_forecast[1].temperature.min if input.daily_forecast|length > 1 else 0 }}\n max_c={{ input.daily_forecast[1].temperature.max if input.daily_forecast|length > 1 else 0 }}\n {% else %}\n min_f={{ input.daily_forecast[1].temperature.min if input.daily_forecast|length > 1 else 0 }}\n max_f={{ input.daily_forecast[1].temperature.max if input.daily_forecast|length > 1 else 0 }}\n {% endif %}\n pop={{ input.daily_forecast[1].precipitation_probability if input.daily_forecast|length > 1 else 0.0 }}\n rain_mm={{ input.daily_forecast[1].rain if input.daily_forecast|length > 1 else 0.0 }}\n desc=\"{{ input.daily_forecast[1].summary if input.daily_forecast|length > 1 else \"\" }}\"\n Alert:\n {% if input.alerts|length > 0 %}\n event=\"{{ input.alerts[0].event }}\"\n end_unix={{ input.alerts[0].end }}\n sender=\"{{ input.alerts[0].sender_name }}\"\n tags=\"{{ input.alerts[0].tags | join(\", \") }}\"\n {% endif %}\n \"#\n}\n\ntest test_weather_lines_fahrenheit {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 40.7128\n lon -74.006\n timezone \"America/New_York\"\n }\n current {\n temperature 75.0\n feels_like 75.0\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n humidity 50\n pressure 1013\n wind_speed 5.5\n wind_direction 180\n visibility 10000\n uv_index 6.5\n clouds 10\n }\n daily_forecast [\n {\n date \"2025-08-12T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with clear spells\"\n temperature {\n day 88.46600000000001\n min 65.984\n max 88.46600000000001\n night 76.15400000000005\n }\n conditions {\n id 801\n main \"Clouds\"\n description \"few clouds\"\n icon \"02d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-13T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 87.35\n min 71.00600000000003\n max 87.85400000000006\n night 80.40200000000007\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 4.89\n },\n {\n date \"2025-08-14T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 81.68000000000004\n min 76.424\n max 88.016\n night 80.79800000000003\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 2.85\n },\n {\n date \"2025-08-15T17:00:00.000Z\"\n summary \"You can expect clear sky in the morning, with partly cloudy in the afternoon\"\n temperature {\n day 84.90200000000007\n min 75.72200000000004\n max 85.53200000000001\n night 76.55\n }\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-16T17:00:00.000Z\"\n summary \"There will be partly cloudy today\"\n temperature {\n day 81.57200000000003\n min 74.33600000000007\n max 81.57200000000003\n night 76.55\n }\n conditions {\n id 803\n main \"Clouds\"\n description \"broken clouds\"\n icon \"04d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-17T17:00:00.000Z\"\n summary \"There will be partly cloudy today\"\n temperature {\n day 90.64400000000008\n min 73.90400000000005\n max 90.64400000000008\n night 83.67800000000007\n }\n conditions {\n id 801\n main \"Clouds\"\n description \"few clouds\"\n icon \"02d\"\n }\n precipitation_probability 0.12\n rain 0\n },\n {\n date \"2025-08-18T16:00:00.000Z\"\n summary \"There will be rain until morning, then partly cloudy\"\n temperature {\n day 85.1\n min 75.99199999999999\n max 90.95\n night 80.25800000000001\n }\n conditions {\n id 500\n main \"Rain\"\n description \"light rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 2.4\n },\n {\n date \"2025-08-19T16:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 80.49199999999999\n min 76.13600000000007\n max 84.81200000000005\n night 76.13600000000007\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 11.61\n }\n ]\n alerts [\n {\n sender_name \"NWS Upton NY\"\n event \"Air Quality Alert\"\n start 1755004380\n end 1755054000\n description \"The New York State Department of Environmental Conservation has\\nissued an Air Quality Health Advisory for the following counties:\\n\\nNew York, Bronx, Kings, Queens, Richmond, Nassau, Suffolk,\\nWestchester, Rockland, Orange, Putnam.\\n\\nuntil 11 PM EDT this evening.\\n\\nAir quality levels in outdoor air are predicted to be greater than\\nan Air Quality Index value of 100 for the pollutant of Ground Level\\nOzone. The Air Quality Index, or AQI, was created as an easy way to\\ncorrelate levels of different pollutants to one scale. The higher\\nthe AQI value, the greater the health concern.\\n\\nWhen pollution levels are elevated, the New York State Department of\\nHealth recommends that individuals consider limiting strenuous\\noutdoor physical activity to reduce the risk of adverse health\\neffects. People who may be especially sensitive to the effects of\\nelevated levels of pollutants include the very young, and those with\\npreexisting respiratory problems such as asthma or heart disease.\\nThose with symptoms should consider consulting their personal\\nphysician.\\n\\nFor additional information, please visit the New York State\\nDepartment of Environmental Conservation website at,\\nhttps://on.ny.gov/nyaqi, or call the Air Quality Hotline at\\n1 800 5 3 5, 1 3 4 5.\"\n tags [\n \"Air quality\"\n ]\n }\n ]\n }\n unit \"F\"\n }\n}\n\ntest test_weather_lines_celsius {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 40.7128\n lon -74.006\n timezone \"America/New_York\"\n }\n current {\n temperature 23.9\n feels_like 23.9\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n humidity 50\n pressure 1013\n wind_speed 5.5\n wind_direction 180\n visibility 10000\n uv_index 6.5\n clouds 10\n }\n daily_forecast [\n {\n date \"2025-08-12T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with clear spells\"\n temperature {\n day 31.37\n min 18.88\n max 31.37\n night 24.53\n }\n conditions {\n id 801\n main \"Clouds\"\n description \"few clouds\"\n icon \"02d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-13T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 30.75\n min 21.67\n max 31.03\n night 26.89\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 4.89\n }\n ]\n alerts []\n }\n unit \"C\"\n }\n}\n\ntest test_weather_with_memory_cold_preference {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 43.6532\n lon -79.3832\n timezone \"America/Toronto\"\n }\n current {\n temperature 2.0\n feels_like -3.0\n conditions {\n id 600\n main \"Snow\"\n description \"light snow\"\n icon \"13d\"\n }\n humidity 85\n pressure 1015\n wind_speed 8.5\n wind_direction 320\n visibility 5000\n uv_index 1.0\n clouds 90\n }\n daily_forecast [\n {\n date \"2025-11-12T17:00:00.000Z\"\n summary \"Expect a day of snow with cold temperatures\"\n temperature {\n day 2.0\n min -2.0\n max 3.0\n night -1.0\n }\n conditions {\n id 600\n main \"Snow\"\n description \"light snow\"\n icon \"13d\"\n }\n precipitation_probability 0.8\n rain 0\n },\n {\n date \"2025-11-13T17:00:00.000Z\"\n summary \"Cold and clear skies\"\n temperature {\n day 1.0\n min -4.0\n max 2.0\n night -3.0\n }\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n precipitation_probability 0.1\n rain 0\n }\n ]\n alerts []\n }\n unit \"C\"\n memory {\n userName \"Ajay\"\n userFacts [\n \"Location: Toronto\",\n \"Age: 37\"\n ]\n deductiveFacts [\n \"User likes cold weather because they are from Canada\"\n ]\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_weather_with_memory_no_deductions {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 37.7749\n lon -122.4194\n timezone \"America/Los_Angeles\"\n }\n current {\n temperature 18.0\n feels_like 17.0\n conditions {\n id 500\n main \"Rain\"\n description \"light rain\"\n icon \"10d\"\n }\n humidity 70\n pressure 1012\n wind_speed 4.5\n wind_direction 270\n visibility 8000\n uv_index 2.0\n clouds 75\n }\n daily_forecast [\n {\n date \"2025-11-12T17:00:00.000Z\"\n summary \"Rainy day with mild temperatures\"\n temperature {\n day 18.0\n min 14.0\n max 20.0\n night 15.0\n }\n conditions {\n id 500\n main \"Rain\"\n description \"light rain\"\n icon \"10d\"\n }\n precipitation_probability 0.9\n rain 5.2\n },\n {\n date \"2025-11-13T17:00:00.000Z\"\n summary \"Partly cloudy with chance of rain\"\n temperature {\n day 19.0\n min 15.0\n max 21.0\n night 16.0\n }\n conditions {\n id 802\n main \"Clouds\"\n description \"scattered clouds\"\n icon \"03d\"\n }\n precipitation_probability 0.4\n rain 1.2\n }\n ]\n alerts []\n }\n unit \"C\"\n memory {\n userName \"Sarah\"\n userFacts [\n \"Children: Two daughters (ages 5 and 2)\",\n \"Occupation: Founder of a technology company\"\n ]\n deductiveFacts []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}", } export const getBamlFiles = () => { diff --git a/packages/baml_client/baml_client/partial_types.ts b/packages/baml_client/baml_client/partial_types.ts index 5c0690e..e161698 100644 --- a/packages/baml_client/baml_client/partial_types.ts +++ b/packages/baml_client/baml_client/partial_types.ts @@ -174,6 +174,11 @@ export namespace partial_types { peerCard: string[] recentMessages: string[] sessionSummaries: string[] + crossPeerPerspectives: CrossPeerPerspective[] + } + export interface CrossPeerPerspective { + label?: string | null + perspective?: string | null } export interface MemoryCore { userName?: string | null diff --git a/packages/baml_client/baml_client/type_builder.ts b/packages/baml_client/baml_client/type_builder.ts index 15390f2..bd32a1a 100644 --- a/packages/baml_client/baml_client/type_builder.ts +++ b/packages/baml_client/baml_client/type_builder.ts @@ -73,7 +73,7 @@ export default class TypeBuilder { LocationLite: ClassViewer<'LocationLite', "lat" | "lon" | "timezone">; - MemoryContext: ClassViewer<'MemoryContext', "explicitFacts" | "deductiveFacts" | "peerCard" | "recentMessages" | "sessionSummaries">; + MemoryContext: ClassViewer<'MemoryContext', "explicitFacts" | "deductiveFacts" | "peerCard" | "recentMessages" | "sessionSummaries" | "crossPeerPerspectives">; MemoryCore: ClassViewer<'MemoryCore', "userName" | "userFacts" | "deductiveFacts">; @@ -213,7 +213,7 @@ export default class TypeBuilder { ]); this.MemoryContext = this.tb.classViewer("MemoryContext", [ - "explicitFacts","deductiveFacts","peerCard","recentMessages","sessionSummaries", + "explicitFacts","deductiveFacts","peerCard","recentMessages","sessionSummaries","crossPeerPerspectives", ]); this.MemoryCore = this.tb.classViewer("MemoryCore", [ diff --git a/packages/baml_client/baml_client/types.ts b/packages/baml_client/baml_client/types.ts index 3a69bb9..43b3cd4 100644 --- a/packages/baml_client/baml_client/types.ts +++ b/packages/baml_client/baml_client/types.ts @@ -248,6 +248,13 @@ export interface MemoryContext { peerCard: string[] recentMessages: string[] sessionSummaries: string[] + crossPeerPerspectives: CrossPeerPerspective[] + +} + +export interface CrossPeerPerspective { + label: string + perspective: string } diff --git a/packages/convex/bamlActions.ts b/packages/convex/bamlActions.ts index 1195a35..228e868 100644 --- a/packages/convex/bamlActions.ts +++ b/packages/convex/bamlActions.ts @@ -10,6 +10,7 @@ import type { EmailInterpretation, FollowupChatContext, FollowupChatResponse, + SensitivityCategory, SessionInput, } from "../baml_client/baml_client/types"; import { internalAction } from "./_generated/server"; @@ -65,6 +66,12 @@ export const interpretEmailReply = internalAction({ content: v.string(), }), ), + crossPeerPerspectives: v.array( + v.object({ + label: v.string(), + perspective: v.string(), + }), + ), }), }, handler: async ( @@ -80,6 +87,10 @@ export const interpretEmailReply = internalAction({ direction: m.direction, content: m.content, })), + crossPeerPerspectives: context.crossPeerPerspectives.map((p) => ({ + label: p.label, + perspective: p.perspective, + })), }; const result = await b.InterpretEmailReply(userMessage, emailContext); @@ -114,6 +125,12 @@ export const interpretChatMessage = internalAction({ createdAt: v.string(), }), ), + crossPeerPerspectives: v.array( + v.object({ + label: v.string(), + perspective: v.string(), + }), + ), }), }, handler: async (_, { userMessage, context }): Promise => { @@ -133,6 +150,10 @@ export const interpretChatMessage = internalAction({ content: m.content, createdAt: m.createdAt, })), + crossPeerPerspectives: context.crossPeerPerspectives.map((p) => ({ + label: p.label, + perspective: p.perspective, + })), }; const result = await b.InterpretChatMessage(userMessage, chatContext); @@ -140,6 +161,16 @@ export const interpretChatMessage = internalAction({ }, }); +export const checkSensitivity = internalAction({ + args: { + crossPeerContext: v.string(), + }, + handler: async (_, { crossPeerContext }): Promise => { + const result = await b.CheckSensitivity(crossPeerContext); + return result; + }, +}); + export const interpretFollowupChat = internalAction({ args: { userMessage: v.string(), @@ -169,6 +200,12 @@ export const interpretFollowupChat = internalAction({ url: v.string(), }), ), + crossPeerPerspectives: v.array( + v.object({ + label: v.string(), + perspective: v.string(), + }), + ), }), }, handler: async (_, { userMessage, context }): Promise => { @@ -191,6 +228,10 @@ export const interpretFollowupChat = internalAction({ content: r.content, url: r.url, })), + crossPeerPerspectives: context.crossPeerPerspectives.map((p) => ({ + label: p.label, + perspective: p.perspective, + })), }; const result = await b.InterpretFollowupChat(userMessage, followupContext); diff --git a/packages/convex/chat.ts b/packages/convex/chat.ts index b8278fd..9855ceb 100644 --- a/packages/convex/chat.ts +++ b/packages/convex/chat.ts @@ -170,6 +170,80 @@ export const sendMessage = action({ } } + // Cross-peer queries: fetch perspectives from connected users with shared memory + let crossPeerPerspectives: Array<{ label: string; perspective: string }> = []; + if (honchoKey) { + try { + const connections = (await ctx.runQuery( + internal.connections.getActiveSharedMemoryConnections, + { userId: user._id }, + )) as Array<{ _id: Id<"connections">; connectedUserId: Id<"users">; label: string | null }>; + + const cappedConnections = connections.slice(0, 3); + if (cappedConnections.length > 0) { + console.log(`[Chat] Querying ${cappedConnections.length} cross-peer connections`); + + const crossPeerClient = new Honcho({ + apiKey: honchoKey, + environment: "production", + workspaceId: "with-context", + }); + + const rawPerspectives = await Promise.all( + cappedConnections.map(async (conn) => { + try { + const connectedPeer = await crossPeerClient.peer( + `${conn.connectedUserId}-diatribe`, + ); + const rep = await connectedPeer.representation({ + target: `${user._id}-diatribe`, + searchQuery: `${date} ${content}`, + searchTopK: 5, + maxConclusions: 10, + }); + return { + label: conn.label ?? "Connected user", + perspective: typeof rep.representation === "string" ? rep.representation : "", + }; + } catch (error) { + console.warn( + `[Chat] Cross-peer query failed for ${conn.connectedUserId}: ${error instanceof Error ? error.message : String(error)}`, + ); + return null; + } + }), + ); + + const validPerspectives = rawPerspectives.filter( + (p): p is { label: string; perspective: string } => + p !== null && p.perspective.length > 0, + ); + + const sensitivityResults = await Promise.all( + validPerspectives.map(async (p) => { + const category = (await ctx.runAction( + internal.bamlActions.checkSensitivity, + { crossPeerContext: p.perspective }, + )) as string; + return { ...p, category }; + }), + ); + + crossPeerPerspectives = sensitivityResults + .filter((p) => p.category === "SAFE") + .map(({ label, perspective }) => ({ label, perspective })); + + console.log( + `[Chat] Cross-peer: ${validPerspectives.length} valid, ${crossPeerPerspectives.length} safe`, + ); + } + } catch (error) { + console.warn( + `[Chat] Cross-peer lookup failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + const interpretation = (await ctx.runAction( internal.bamlActions.interpretChatMessage, { @@ -190,6 +264,7 @@ export const sendMessage = action({ content: m.content, createdAt: m.createdAt, })), + crossPeerPerspectives, }, }, )) as ChatInterpretation; diff --git a/packages/convex/connections.ts b/packages/convex/connections.ts new file mode 100644 index 0000000..f47971f --- /dev/null +++ b/packages/convex/connections.ts @@ -0,0 +1,738 @@ +import { v } from "convex/values"; +import type { Id } from "./_generated/dataModel"; +import type { QueryCtx } from "./_generated/server"; +import { internalQuery, mutation, query } from "./_generated/server"; + +// ============================================================================= +// Utilities +// ============================================================================= + +async function getUserByEmail(ctx: QueryCtx, email: string) { + return await ctx.db + .query("users") + .withIndex("by_email", (q) => q.eq("email", email)) + .first(); +} + +async function getConnectionBetween( + ctx: QueryCtx, + userA: Id<"users">, + userB: Id<"users">, +) { + const forward = await ctx.db + .query("connections") + .withIndex("by_pair", (q) => + q.eq("requesterId", userA).eq("accepterId", userB), + ) + .first(); + if (forward) return forward; + + return await ctx.db + .query("connections") + .withIndex("by_pair", (q) => + q.eq("requesterId", userB).eq("accepterId", userA), + ) + .first(); +} + +async function getLabelForUser( + ctx: QueryCtx, + connectionId: Id<"connections">, + userId: Id<"users">, +) { + return await ctx.db + .query("connectionLabels") + .withIndex("by_connection", (q) => q.eq("connectionId", connectionId)) + .filter((q) => q.eq(q.field("userId"), userId)) + .first(); +} + +// ============================================================================= +// Public Mutations +// ============================================================================= + +export const sendConnectionRequest = mutation({ + args: { + mentraUserId: v.string(), + targetEmail: v.string(), + }, + handler: async (ctx, args) => { + const requester = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!requester) throw new Error("User not found"); + + const accepter = await getUserByEmail(ctx, args.targetEmail); + if (!accepter) throw new Error("No user found with that email"); + + if (requester._id === accepter._id) { + throw new Error("Cannot connect with yourself"); + } + + const existing = await getConnectionBetween( + ctx, + requester._id, + accepter._id, + ); + if (existing && existing.status !== "revoked") { + throw new Error( + existing.status === "pending" + ? "Connection request already pending" + : "Connection already exists", + ); + } + + if (existing && existing.status === "revoked") { + await ctx.db.patch(existing._id, { + requesterId: requester._id, + accepterId: accepter._id, + status: "pending", + sharedMemoryEnabled: false, + }); + return existing._id; + } + + return await ctx.db.insert("connections", { + requesterId: requester._id, + accepterId: accepter._id, + status: "pending", + sharedMemoryEnabled: false, + }); + }, +}); + +export const acceptConnection = mutation({ + args: { + mentraUserId: v.string(), + connectionId: v.id("connections"), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) throw new Error("User not found"); + + const connection = await ctx.db.get(args.connectionId); + if (!connection) throw new Error("Connection not found"); + if (connection.accepterId !== user._id) { + throw new Error("Only the invited user can accept"); + } + if (connection.status !== "pending") { + throw new Error("Connection is not pending"); + } + + await ctx.db.patch(args.connectionId, { status: "active" }); + return { success: true }; + }, +}); + +export const rejectConnection = mutation({ + args: { + mentraUserId: v.string(), + connectionId: v.id("connections"), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) throw new Error("User not found"); + + const connection = await ctx.db.get(args.connectionId); + if (!connection) throw new Error("Connection not found"); + if (connection.accepterId !== user._id) { + throw new Error("Only the invited user can reject"); + } + if (connection.status !== "pending") { + throw new Error("Connection is not pending"); + } + + await ctx.db.patch(args.connectionId, { status: "revoked" }); + return { success: true }; + }, +}); + +export const revokeConnection = mutation({ + args: { + mentraUserId: v.string(), + connectionId: v.id("connections"), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) throw new Error("User not found"); + + const connection = await ctx.db.get(args.connectionId); + if (!connection) throw new Error("Connection not found"); + + if ( + connection.requesterId !== user._id && + connection.accepterId !== user._id + ) { + throw new Error("Not a participant in this connection"); + } + if (connection.status !== "active") { + throw new Error("Connection is not active"); + } + + await ctx.db.patch(args.connectionId, { + status: "revoked", + sharedMemoryEnabled: false, + }); + return { success: true }; + }, +}); + +export const toggleSharedMemory = mutation({ + args: { + mentraUserId: v.string(), + connectionId: v.id("connections"), + enabled: v.boolean(), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) throw new Error("User not found"); + + const connection = await ctx.db.get(args.connectionId); + if (!connection) throw new Error("Connection not found"); + + if ( + connection.requesterId !== user._id && + connection.accepterId !== user._id + ) { + throw new Error("Not a participant in this connection"); + } + if (connection.status !== "active") { + throw new Error("Connection must be active to toggle shared memory"); + } + + await ctx.db.patch(args.connectionId, { + sharedMemoryEnabled: args.enabled, + }); + return { success: true }; + }, +}); + +export const updateLabel = mutation({ + args: { + mentraUserId: v.string(), + connectionId: v.id("connections"), + label: v.string(), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) throw new Error("User not found"); + + const connection = await ctx.db.get(args.connectionId); + if (!connection) throw new Error("Connection not found"); + + if ( + connection.requesterId !== user._id && + connection.accepterId !== user._id + ) { + throw new Error("Not a participant in this connection"); + } + + const existing = await getLabelForUser(ctx, args.connectionId, user._id); + if (existing) { + await ctx.db.patch(existing._id, { label: args.label }); + return existing._id; + } + + return await ctx.db.insert("connectionLabels", { + connectionId: args.connectionId, + userId: user._id, + label: args.label, + }); + }, +}); + +// ============================================================================= +// Public Queries +// ============================================================================= + +export const getConnectionsForUser = query({ + args: { mentraUserId: v.string() }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) return []; + + const asRequester = await ctx.db + .query("connections") + .withIndex("by_requester", (q) => q.eq("requesterId", user._id)) + .collect(); + + const asAccepter = await ctx.db + .query("connections") + .withIndex("by_accepter", (q) => q.eq("accepterId", user._id)) + .collect(); + + const all = [...asRequester, ...asAccepter].filter( + (c) => c.status !== "revoked", + ); + + const results = await Promise.all( + all.map(async (conn) => { + const otherUserId = + conn.requesterId === user._id + ? conn.accepterId + : conn.requesterId; + const otherUser = await ctx.db.get(otherUserId); + const label = await getLabelForUser(ctx, conn._id, user._id); + + return { + _id: conn._id, + status: conn.status, + sharedMemoryEnabled: conn.sharedMemoryEnabled, + isRequester: conn.requesterId === user._id, + otherUser: otherUser + ? { _id: otherUser._id, email: otherUser.email ?? null } + : null, + label: label?.label ?? null, + }; + }), + ); + + return results; + }, +}); + +export const getPendingInvitesForUser = query({ + args: { mentraUserId: v.string() }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) return []; + + const pending = await ctx.db + .query("connections") + .withIndex("by_accepter", (q) => q.eq("accepterId", user._id)) + .filter((q) => q.eq(q.field("status"), "pending")) + .collect(); + + const results = await Promise.all( + pending.map(async (conn) => { + const requester = await ctx.db.get(conn.requesterId); + return { + _id: conn._id, + requesterEmail: requester?.email ?? null, + _creationTime: conn._creationTime, + }; + }), + ); + + return results; + }, +}); + +export const getActiveSharedMemoryConnectionsByMentraId = query({ + args: { mentraUserId: v.string() }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) return []; + + const asRequester = await ctx.db + .query("connections") + .withIndex("by_requester", (q) => q.eq("requesterId", user._id)) + .filter((q) => + q.and( + q.eq(q.field("status"), "active"), + q.eq(q.field("sharedMemoryEnabled"), true), + ), + ) + .collect(); + + const asAccepter = await ctx.db + .query("connections") + .withIndex("by_accepter", (q) => q.eq("accepterId", user._id)) + .filter((q) => + q.and( + q.eq(q.field("status"), "active"), + q.eq(q.field("sharedMemoryEnabled"), true), + ), + ) + .collect(); + + const all = [...asRequester, ...asAccepter]; + + const results = await Promise.all( + all.map(async (conn) => { + const connectedUserId = + conn.requesterId === user._id + ? conn.accepterId + : conn.requesterId; + const label = await getLabelForUser(ctx, conn._id, user._id); + + return { + connectedUserId, + label: label?.label ?? null, + }; + }), + ); + + return results; + }, +}); + +// ============================================================================= +// Internal Queries (for Phase 2+ cross-peer lookups) +// ============================================================================= + +export const getActiveSharedMemoryConnections = internalQuery({ + args: { userId: v.id("users") }, + handler: async (ctx, args) => { + const asRequester = await ctx.db + .query("connections") + .withIndex("by_requester", (q) => q.eq("requesterId", args.userId)) + .filter((q) => + q.and( + q.eq(q.field("status"), "active"), + q.eq(q.field("sharedMemoryEnabled"), true), + ), + ) + .collect(); + + const asAccepter = await ctx.db + .query("connections") + .withIndex("by_accepter", (q) => q.eq("accepterId", args.userId)) + .filter((q) => + q.and( + q.eq(q.field("status"), "active"), + q.eq(q.field("sharedMemoryEnabled"), true), + ), + ) + .collect(); + + const all = [...asRequester, ...asAccepter]; + + const results = await Promise.all( + all.map(async (conn) => { + const connectedUserId = + conn.requesterId === args.userId + ? conn.accepterId + : conn.requesterId; + const label = await getLabelForUser(ctx, conn._id, args.userId); + + return { + _id: conn._id, + connectedUserId, + label: label?.label ?? null, + }; + }), + ); + + return results; + }, +}); + +// ============================================================================= +// Group Connections +// ============================================================================= + +export const createConnectionGroup = mutation({ + args: { + mentraUserId: v.string(), + name: v.string(), + memberEmails: v.optional(v.array(v.string())), + }, + handler: async (ctx, args) => { + const creator = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!creator) throw new Error("User not found"); + + const groupId = await ctx.db.insert("connectionGroups", { + name: args.name, + creatorId: creator._id, + sharedMemoryEnabled: false, + }); + + // Add creator as active member + await ctx.db.insert("connectionGroupMembers", { + groupId, + userId: creator._id, + status: "active", + }); + + // Invite members by email + if (args.memberEmails) { + for (const email of args.memberEmails) { + const member = await getUserByEmail(ctx, email); + if (member && member._id !== creator._id) { + const existing = await ctx.db + .query("connectionGroupMembers") + .withIndex("by_group_user", (q) => + q.eq("groupId", groupId).eq("userId", member._id), + ) + .first(); + if (!existing) { + await ctx.db.insert("connectionGroupMembers", { + groupId, + userId: member._id, + status: "pending", + }); + } + } + } + } + + return groupId; + }, +}); + +export const acceptGroupInvite = mutation({ + args: { + mentraUserId: v.string(), + groupId: v.id("connectionGroups"), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) throw new Error("User not found"); + + const membership = await ctx.db + .query("connectionGroupMembers") + .withIndex("by_group_user", (q) => + q.eq("groupId", args.groupId).eq("userId", user._id), + ) + .first(); + if (!membership) throw new Error("No invitation found"); + if (membership.status !== "pending") { + throw new Error("Invitation is not pending"); + } + + await ctx.db.patch(membership._id, { status: "active" }); + return { success: true }; + }, +}); + +export const leaveConnectionGroup = mutation({ + args: { + mentraUserId: v.string(), + groupId: v.id("connectionGroups"), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) throw new Error("User not found"); + + const membership = await ctx.db + .query("connectionGroupMembers") + .withIndex("by_group_user", (q) => + q.eq("groupId", args.groupId).eq("userId", user._id), + ) + .first(); + if (!membership) throw new Error("Not a member of this group"); + + await ctx.db.patch(membership._id, { status: "removed" }); + return { success: true }; + }, +}); + +export const toggleGroupSharedMemory = mutation({ + args: { + mentraUserId: v.string(), + groupId: v.id("connectionGroups"), + enabled: v.boolean(), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) throw new Error("User not found"); + + const group = await ctx.db.get(args.groupId); + if (!group) throw new Error("Group not found"); + if (group.creatorId !== user._id) { + throw new Error("Only the group creator can toggle shared memory"); + } + + await ctx.db.patch(args.groupId, { + sharedMemoryEnabled: args.enabled, + }); + return { success: true }; + }, +}); + +export const updateGroupMemberLabel = mutation({ + args: { + mentraUserId: v.string(), + groupId: v.id("connectionGroups"), + label: v.string(), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) throw new Error("User not found"); + + const membership = await ctx.db + .query("connectionGroupMembers") + .withIndex("by_group_user", (q) => + q.eq("groupId", args.groupId).eq("userId", user._id), + ) + .first(); + if (!membership) throw new Error("Not a member of this group"); + + await ctx.db.patch(membership._id, { label: args.label }); + return { success: true }; + }, +}); + +export const getGroupsForUser = query({ + args: { mentraUserId: v.string() }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_mentra_id", (q) => + q.eq("mentraUserId", args.mentraUserId), + ) + .first(); + if (!user) return []; + + const memberships = await ctx.db + .query("connectionGroupMembers") + .withIndex("by_user", (q) => q.eq("userId", user._id)) + .filter((q) => q.neq(q.field("status"), "removed")) + .collect(); + + const results = await Promise.all( + memberships.map(async (m) => { + const group = await ctx.db.get(m.groupId); + if (!group) return null; + + const allMembers = await ctx.db + .query("connectionGroupMembers") + .withIndex("by_group", (q) => q.eq("groupId", m.groupId)) + .filter((q) => q.neq(q.field("status"), "removed")) + .collect(); + + const memberDetails = await Promise.all( + allMembers.map(async (member) => { + const memberUser = await ctx.db.get(member.userId); + return { + userId: member.userId, + email: memberUser?.email ?? null, + label: member.label ?? null, + status: member.status, + isCreator: member.userId === group.creatorId, + }; + }), + ); + + return { + _id: group._id, + name: group.name, + sharedMemoryEnabled: group.sharedMemoryEnabled, + isCreator: group.creatorId === user._id, + myStatus: m.status, + myLabel: m.label ?? null, + members: memberDetails, + }; + }), + ); + + return results.filter((r) => r !== null); + }, +}); + +export const getActiveSharedMemoryGroupMembers = internalQuery({ + args: { userId: v.id("users") }, + handler: async (ctx, args) => { + // Find groups where this user is an active member and shared memory is enabled + const memberships = await ctx.db + .query("connectionGroupMembers") + .withIndex("by_user", (q) => q.eq("userId", args.userId)) + .filter((q) => q.eq(q.field("status"), "active")) + .collect(); + + const results: Array<{ + groupId: Id<"connectionGroups">; + connectedUserId: Id<"users">; + label: string | null; + }> = []; + + for (const membership of memberships) { + const group = await ctx.db.get(membership.groupId); + if (!group || !group.sharedMemoryEnabled) continue; + + // Get all other active members in this group + const groupMembers = await ctx.db + .query("connectionGroupMembers") + .withIndex("by_group", (q) => q.eq("groupId", membership.groupId)) + .filter((q) => + q.and( + q.eq(q.field("status"), "active"), + q.neq(q.field("userId"), args.userId), + ), + ) + .collect(); + + for (const member of groupMembers) { + // Avoid duplicates if user is already connected via 1:1 + if (!results.some((r) => r.connectedUserId === member.userId)) { + results.push({ + groupId: membership.groupId, + connectedUserId: member.userId, + label: member.label ?? null, + }); + } + } + } + + return results; + }, +}); diff --git a/packages/convex/emailReply.ts b/packages/convex/emailReply.ts index dbd73ef..65bc57f 100644 --- a/packages/convex/emailReply.ts +++ b/packages/convex/emailReply.ts @@ -175,6 +175,80 @@ export const processEmailReply = internalAction({ console.warn("[EmailReply] HONCHO_API_KEY not set, skipping peerCard"); } + // 5b. Cross-peer queries: fetch perspectives from connected users with shared memory + let crossPeerPerspectives: Array<{ label: string; perspective: string }> = []; + if (honchoKey) { + try { + const connections = (await ctx.runQuery( + internal.connections.getActiveSharedMemoryConnections, + { userId: user._id }, + )) as Array<{ _id: Id<"connections">; connectedUserId: Id<"users">; label: string | null }>; + + const cappedConnections = connections.slice(0, 3); + if (cappedConnections.length > 0) { + console.log(`[EmailReply] Querying ${cappedConnections.length} cross-peer connections`); + + const crossPeerClient = new Honcho({ + apiKey: honchoKey, + environment: "production", + workspaceId: "with-context", + }); + + const rawPerspectives = await Promise.all( + cappedConnections.map(async (conn) => { + try { + const connectedPeer = await crossPeerClient.peer( + `${conn.connectedUserId}-diatribe`, + ); + const rep = await connectedPeer.representation({ + target: `${user._id}-diatribe`, + searchQuery: emailNote.subject, + searchTopK: 5, + maxConclusions: 10, + }); + return { + label: conn.label ?? "Connected user", + perspective: typeof rep.representation === "string" ? rep.representation : "", + }; + } catch (error) { + console.warn( + `[EmailReply] Cross-peer query failed for ${conn.connectedUserId}: ${error instanceof Error ? error.message : String(error)}`, + ); + return null; + } + }), + ); + + const validPerspectives = rawPerspectives.filter( + (p): p is { label: string; perspective: string } => + p !== null && p.perspective.length > 0, + ); + + const sensitivityResults = await Promise.all( + validPerspectives.map(async (p) => { + const category = (await ctx.runAction( + internal.bamlActions.checkSensitivity, + { crossPeerContext: p.perspective }, + )) as string; + return { ...p, category }; + }), + ); + + crossPeerPerspectives = sensitivityResults + .filter((p) => p.category === "SAFE") + .map(({ label, perspective }) => ({ label, perspective })); + + console.log( + `[EmailReply] Cross-peer: ${validPerspectives.length} valid, ${crossPeerPerspectives.length} safe`, + ); + } + } catch (error) { + console.warn( + `[EmailReply] Cross-peer lookup failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + // 6. Build context for LLM interpretation console.log("[EmailReply] Step 6: Building LLM context..."); const context = { @@ -188,6 +262,7 @@ export const processEmailReply = internalAction({ content: m.textContent ?? "", createdAt: m.createdAt, })), + crossPeerPerspectives, }; console.log("[EmailReply] ✓ Context built:", { @@ -216,6 +291,7 @@ export const processEmailReply = internalAction({ direction: m.direction, content: m.content, })), + crossPeerPerspectives: context.crossPeerPerspectives, }, }, ); diff --git a/packages/convex/followupsChat.ts b/packages/convex/followupsChat.ts index 626d1ef..3cd6a4b 100644 --- a/packages/convex/followupsChat.ts +++ b/packages/convex/followupsChat.ts @@ -168,6 +168,83 @@ export const sendFollowupMessage = action({ } console.log(`[FollowupChat] Search results: ${searchResults.length} results`); + // Cross-peer queries: fetch perspectives from connected users with shared memory + let crossPeerPerspectives: Array<{ label: string; perspective: string }> = []; + if (honchoKey) { + try { + const connections = (await ctx.runQuery( + internal.connections.getActiveSharedMemoryConnections, + { userId: user._id }, + )) as Array<{ _id: Id<"connections">; connectedUserId: Id<"users">; label: string | null }>; + + const cappedConnections = connections.slice(0, 3); + if (cappedConnections.length > 0) { + console.log(`[FollowupChat] Querying ${cappedConnections.length} cross-peer connections`); + + const honchoClient = new Honcho({ + apiKey: honchoKey, + environment: "production", + workspaceId: "with-context", + }); + + const rawPerspectives = await Promise.all( + cappedConnections.map(async (conn) => { + try { + const connectedPeer = await honchoClient.peer( + `${conn.connectedUserId}-diatribe`, + ); + const rep = await connectedPeer.representation({ + target: `${user._id}-diatribe`, + searchQuery: followup.topic, + searchTopK: 5, + maxConclusions: 10, + }); + const perspective = typeof rep.representation === "string" ? rep.representation : ""; + return { + label: conn.label ?? "Connected user", + perspective, + }; + } catch (error) { + console.warn( + `[FollowupChat] Cross-peer query failed for ${conn.connectedUserId}: ${error instanceof Error ? error.message : String(error)}`, + ); + return null; + } + }), + ); + + // Filter out failed queries and empty perspectives + const validPerspectives = rawPerspectives.filter( + (p): p is { label: string; perspective: string } => + p !== null && p.perspective.length > 0, + ); + + // Run sensitivity gate on each perspective (second protection layer) + const sensitivityResults = await Promise.all( + validPerspectives.map(async (p) => { + const category = (await ctx.runAction( + internal.bamlActions.checkSensitivity, + { crossPeerContext: p.perspective }, + )) as string; + return { ...p, category }; + }), + ); + + crossPeerPerspectives = sensitivityResults + .filter((p) => p.category === "SAFE") + .map(({ label, perspective }) => ({ label, perspective })); + + console.log( + `[FollowupChat] Cross-peer (representation): ${validPerspectives.length} valid, ${crossPeerPerspectives.length} safe`, + ); + } + } catch (error) { + console.warn( + `[FollowupChat] Cross-peer lookup failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + const interpretation = (await ctx.runAction( internal.bamlActions.interpretFollowupChat, { @@ -187,6 +264,7 @@ export const sendFollowupMessage = action({ deductiveFacts: memoryContext.deductiveFacts, } : null, searchResults: searchResults, + crossPeerPerspectives, }, }, )) as FollowupChatResponse; diff --git a/packages/convex/schema.ts b/packages/convex/schema.ts index f92f9db..68c823b 100644 --- a/packages/convex/schema.ts +++ b/packages/convex/schema.ts @@ -19,7 +19,9 @@ export default defineSchema({ state: v.string(), }), ), - }).index("by_mentra_id", ["mentraUserId"]), + }) + .index("by_mentra_id", ["mentraUserId"]) + .index("by_email", ["email"]), preferences: defineTable({ userId: v.id("users"), weatherUnit: v.string(), @@ -160,4 +162,42 @@ export default defineSchema({ .index("by_user", ["userId"]) .index("by_session", ["sessionId"]) .index("by_route", ["route"]), + connections: defineTable({ + requesterId: v.id("users"), + accepterId: v.id("users"), + status: v.union( + v.literal("pending"), + v.literal("active"), + v.literal("revoked"), + ), + sharedMemoryEnabled: v.boolean(), + }) + .index("by_requester", ["requesterId"]) + .index("by_accepter", ["accepterId"]) + .index("by_pair", ["requesterId", "accepterId"]), + connectionLabels: defineTable({ + connectionId: v.id("connections"), + userId: v.id("users"), + label: v.string(), + }) + .index("by_connection", ["connectionId"]) + .index("by_user", ["userId"]), + connectionGroups: defineTable({ + name: v.string(), + creatorId: v.id("users"), + sharedMemoryEnabled: v.boolean(), + }).index("by_creator", ["creatorId"]), + connectionGroupMembers: defineTable({ + groupId: v.id("connectionGroups"), + userId: v.id("users"), + label: v.optional(v.string()), + status: v.union( + v.literal("pending"), + v.literal("active"), + v.literal("removed"), + ), + }) + .index("by_group", ["groupId"]) + .index("by_user", ["userId"]) + .index("by_group_user", ["groupId", "userId"]), }); From 456509b20ac4be97789baeee2831ade72cf2255c Mon Sep 17 00:00:00 2001 From: Ajay Bhargava Date: Sat, 28 Feb 2026 19:53:01 -0500 Subject: [PATCH 3/3] Fixes to the shared memory infrastructure --- .envrc | 1 + baml_src/core.baml | 5 + baml_src/email_reply.baml | 12 + baml_src/followup.baml | 11 + baml_src/sensitivity.baml | 37 + .../baml_client/baml_client/async_client.ts | 116 +- .../baml_client/baml_client/async_request.ts | 52 +- .../baml_client/baml_client/inlinedbaml.ts | 9 +- packages/baml_client/baml_client/parser.ts | 48 +- .../baml_client/baml_client/partial_types.ts | 13 +- .../baml_client/baml_client/sync_client.ts | 44 +- .../baml_client/baml_client/sync_request.ts | 52 +- .../baml_client/baml_client/type_builder.ts | 28 +- packages/baml_client/baml_client/types.ts | 20 +- packages/convex/_generated/api.d.ts | 1568 +++++++++-------- packages/convex/chat.ts | 7 +- packages/convex/emailReply.ts | 7 +- packages/convex/followupsChat.ts | 7 +- 18 files changed, 1214 insertions(+), 823 deletions(-) create mode 100644 .envrc create mode 100644 baml_src/sensitivity.baml diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1abb058 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +source_up diff --git a/baml_src/core.baml b/baml_src/core.baml index 4f8834a..2e42c92 100644 --- a/baml_src/core.baml +++ b/baml_src/core.baml @@ -1,5 +1,10 @@ // Common classes shared across BAML functions +class CrossPeerPerspective { + label string @description("Display name of the connected user") + perspective string @description("Perspective or relevant facts from the connected user's memory") +} + class MemoryCore { userName string? @description("User's name if known") userFacts string[] @description("Relevant biographical facts about the user") diff --git a/baml_src/email_reply.baml b/baml_src/email_reply.baml index 366188e..ff4d4f6 100644 --- a/baml_src/email_reply.baml +++ b/baml_src/email_reply.baml @@ -20,6 +20,7 @@ class EmailContext { sessionTopics string[] @description("Topics from the linked session") peerCard string[] @description("User profile facts from Honcho") conversationHistory ConversationMessage[] @description("Previous messages in this email thread") + crossPeerPerspectives CrossPeerPerspective[] @description("Perspectives from connected users with shared memory enabled") } function InterpretEmailReply( @@ -68,6 +69,14 @@ CONVERSATION HISTORY: --- {% endfor %} +{% endif %} +{% if context.crossPeerPerspectives | length > 0 %} +CONNECTED USER PERSPECTIVES: +{% for p in context.crossPeerPerspectives %} +From "{{ p.label }}": +{{ p.perspective }} +{% endfor %} + {% endif %} USER'S NEW REPLY: {{ userMessage }} @@ -88,6 +97,7 @@ test test_email_reply_simple { conversationHistory [ { direction "outbound", content "Here are your session notes from today..." } ] + crossPeerPerspectives [] } } } @@ -104,6 +114,7 @@ test test_email_reply_with_new_info { conversationHistory [ { direction "outbound", content "Here are your session notes from today..." } ] + crossPeerPerspectives [] } } } @@ -118,6 +129,7 @@ test test_email_reply_no_session { sessionTopics [] peerCard [] conversationHistory [] + crossPeerPerspectives [] } } } diff --git a/baml_src/followup.baml b/baml_src/followup.baml index 596577c..65b507e 100644 --- a/baml_src/followup.baml +++ b/baml_src/followup.baml @@ -16,6 +16,7 @@ class FollowupChatContext { conversationHistory FollowupConversationMessage[] @description("Previous chat messages about this follow-up") memory FollowupMemoryContext? @description("User memory context from Honcho") searchResults FollowupSearchResult[] @description("Web search results relevant to the topic") + crossPeerPerspectives CrossPeerPerspective[] @description("Perspectives from connected users with shared memory enabled") } class FollowupChatResponse { @@ -136,6 +137,14 @@ CONVERSATION HISTORY: --- {% endfor %} +{% endif %} +{% if context.crossPeerPerspectives | length > 0 %} +CONNECTED USER PERSPECTIVES: +{% for p in context.crossPeerPerspectives %} +From "{{ p.label }}": +{{ p.perspective }} +{% endfor %} + {% endif %} USER'S MESSAGE: {{ userMessage }} @@ -158,6 +167,7 @@ test test_followup_chat_simple { ] conversationHistory [] searchResults [] + crossPeerPerspectives [] } } } @@ -187,6 +197,7 @@ test test_followup_chat_with_history { } ] searchResults [] + crossPeerPerspectives [] } } } diff --git a/baml_src/sensitivity.baml b/baml_src/sensitivity.baml new file mode 100644 index 0000000..d46fdc5 --- /dev/null +++ b/baml_src/sensitivity.baml @@ -0,0 +1,37 @@ +// Sensitivity classification for cross-peer shared content + +enum SensitivityCategory { + SAFE @description("Content is safe to share across peers") + SENSITIVE @description("Content contains sensitive personal information that should not be shared") +} + +function CheckSensitivity(crossPeerContext: string) -> SensitivityCategory { + client "Groq" + + prompt #" +Classify whether the following cross-peer context contains sensitive personal information that should NOT be shared between connected users. + +SENSITIVE includes: medical/health details, financial information, passwords/credentials, private relationship issues, legal matters, or anything the user would reasonably expect to remain private. + +SAFE includes: general interests, preferences, professional topics, hobbies, travel plans, food preferences, or other casual information. + +Content to classify: +{{ crossPeerContext }} + +{{ ctx.output_format }} +"# +} + +test test_safe_content { + functions [CheckSensitivity] + args { + crossPeerContext "User enjoys hiking and is interested in AI technology. Works as a software engineer." + } +} + +test test_sensitive_content { + functions [CheckSensitivity] + args { + crossPeerContext "User mentioned they are dealing with a medical diagnosis and has been seeing a therapist." + } +} diff --git a/packages/baml_client/baml_client/async_client.ts b/packages/baml_client/baml_client/async_client.ts index 96b09f2..4cf421e 100644 --- a/packages/baml_client/baml_client/async_client.ts +++ b/packages/baml_client/baml_client/async_client.ts @@ -24,7 +24,7 @@ import { toBamlError, BamlStream, BamlAbortError, Collector } from "@boundaryml/ import type { Checked, Check, RecursivePartialNull as MovedRecursivePartialNull } from "./types" import type { partial_types } from "./partial_types" import type * as types from "./types" -import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" +import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CrossPeerPerspective, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SensitivityCategory, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" import type TypeBuilder from "./type_builder" import { AsyncHttpRequest, AsyncHttpStreamRequest } from "./async_request" import { LlmResponseParser, LlmStreamParser } from "./parser" @@ -240,6 +240,54 @@ export type RecursivePartialNull = MovedRecursivePartialNull } } + async CheckSensitivity( + crossPeerContext: string, + __baml_options__?: BamlCallOptions + ): Promise { + try { + const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) } + const __signal__ = __options__.signal; + + if (__signal__?.aborted) { + throw new BamlAbortError('Operation was aborted', __signal__.reason); + } + + // Check if onTick is provided - route through streaming if so + if (__options__.onTick) { + const __stream__ = this.stream.CheckSensitivity( + crossPeerContext, + __baml_options__ + ); + + return await __stream__.getFinalResponse(); + } + + const __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : + [__options__.collector]) : []; + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + const __raw__ = await this.runtime.callFunction( + "CheckSensitivity", + { + "crossPeerContext": crossPeerContext + }, + this.ctxManager.cloneContext(), + __options__.tb?.__tb(), + __options__.clientRegistry, + __collector__, + __options__.tags || {}, + __env__, + __signal__, + __options__.watchers, + ) + return __raw__.parsed(false) as types.SensitivityCategory + } catch (error) { + throw toBamlError(error); + } + } + async ClassifyForHint( text: string, __baml_options__?: BamlCallOptions @@ -1076,6 +1124,72 @@ export type RecursivePartialNull = MovedRecursivePartialNull } } + CheckSensitivity( + crossPeerContext: string, + __baml_options__?: BamlCallOptions + ): BamlStream + { + try { + const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) } + const __signal__ = __options__.signal; + + if (__signal__?.aborted) { + throw new BamlAbortError('Operation was aborted', __signal__.reason); + } + + let __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : + [__options__.collector]) : []; + + let __onTickWrapper__: (() => void) | undefined; + + // Create collector and wrap onTick if provided + if (__options__.onTick) { + const __tickCollector__ = new Collector("on-tick-collector"); + __collector__ = [...__collector__, __tickCollector__]; + + __onTickWrapper__ = () => { + const __log__ = __tickCollector__.last; + if (__log__) { + try { + __options__.onTick!("Unknown", __log__); + } catch (error) { + console.error("Error in onTick callback for CheckSensitivity", error); + } + } + }; + } + + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + const __raw__ = this.runtime.streamFunction( + "CheckSensitivity", + { + "crossPeerContext": crossPeerContext + }, + undefined, + this.ctxManager.cloneContext(), + __options__.tb?.__tb(), + __options__.clientRegistry, + __collector__, + __options__.tags || {}, + __env__, + __signal__, + __onTickWrapper__, + ) + return new BamlStream( + __raw__, + (a): types.SensitivityCategory => a, + (a): types.SensitivityCategory => a, + this.ctxManager.cloneContext(), + __options__.signal, + ) + } catch (error) { + throw toBamlError(error); + } + } + ClassifyForHint( text: string, __baml_options__?: BamlCallOptions diff --git a/packages/baml_client/baml_client/async_request.ts b/packages/baml_client/baml_client/async_request.ts index a8b9121..a984d51 100644 --- a/packages/baml_client/baml_client/async_request.ts +++ b/packages/baml_client/baml_client/async_request.ts @@ -23,7 +23,7 @@ import type { BamlRuntime, BamlCtxManager, ClientRegistry, Image, Audio, Pdf, Vi import { toBamlError, HTTPRequest } from "@boundaryml/baml" import type { Checked, Check } from "./types" import type * as types from "./types" -import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" +import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CrossPeerPerspective, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SensitivityCategory, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" import type TypeBuilder from "./type_builder" import type * as events from "./events" @@ -116,6 +116,31 @@ env?: Record } } + async CheckSensitivity( + crossPeerContext: string, + __baml_options__?: BamlCallOptions + ): Promise { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return await this.runtime.buildRequest( + "CheckSensitivity", + { + "crossPeerContext": crossPeerContext + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + false, + __env__ + ) + } catch (error) { + throw toBamlError(error); + } + } + async ClassifyForHint( text: string, __baml_options__?: BamlCallOptions @@ -522,6 +547,31 @@ env?: Record } } + async CheckSensitivity( + crossPeerContext: string, + __baml_options__?: BamlCallOptions + ): Promise { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return await this.runtime.buildRequest( + "CheckSensitivity", + { + "crossPeerContext": crossPeerContext + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + true, + __env__ + ) + } catch (error) { + throw toBamlError(error); + } + } + async ClassifyForHint( text: string, __baml_options__?: BamlCallOptions diff --git a/packages/baml_client/baml_client/inlinedbaml.ts b/packages/baml_client/baml_client/inlinedbaml.ts index f005229..11ef417 100644 --- a/packages/baml_client/baml_client/inlinedbaml.ts +++ b/packages/baml_client/baml_client/inlinedbaml.ts @@ -21,18 +21,19 @@ $ pnpm add @boundaryml/baml const fileMap = { "answer.baml": "class QuestionAnalysisResponse {\n original_text string @description(\"Echo of the user's input text verbatim\")\n has_question bool @description(\"Whether the text contains a question\")\n question string? @description(\"Concise summary of the user's question in 10 words or fewer\")\n answer string[] @description(\"Direct answer in 20 words or fewer\")\n}\n\nfunction AnswerQuestion(text: string, memory: MemoryCore?) -> QuestionAnalysisResponse {\n client \"GroqHeavy\"\n prompt #\"\n {{ _.role(\"system\") }}\n You analyze a user's message to detect and answer questions.\n \n Note: The 'text' parameter may be an enhanced version of the original query with additional context woven in.\n \n Instructions:\n - Set original_text to the user's input text verbatim.\n - Set has_question to true only if a question is present; otherwise false.\n - If has_question is true:\n - question: concise (≤10 words) summary without prefixed phrases like 'You asked'.\n - answer: 1-3 lines, each line ≤10 words. Keep it conversational.\n - If no question is present, set question and answer to null.\n - If the user is describing someone else's conversation, set has_question to false.\n\n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (use to personalize answers and acknowledge past questions):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n \n If the user has asked similar questions before or shown interest in related topics, naturally acknowledge this in your answer (e.g., \"You've asked about X before...\" or \"Building on your interest in Y...\"). Keep it conversational and familiar.\n {% endif %}\n {% endif %}\n\n {{ ctx.output_format }}\n\n {{ _.role(\"user\") }} {{ text }}\n \"#\n}\n\ntest test_answer_question {\n functions [AnswerQuestion]\n args {\n text \"What is the capital of France?\"\n memory null\n }\n}\n\ntest test_answer_question_with_memory_repeat_topic {\n functions [AnswerQuestion]\n args {\n text \"Tell me more about quantum computing\"\n memory {\n userName \"Ajay\"\n userFacts [\n \"Occupation: Founder of a technology company\",\n \"Age: 37\"\n ]\n deductiveFacts [\n \"User has asked about quantum computing before\",\n \"User is interested in technology because they work in tech\"\n ]\n }\n }\n}\n\ntest test_answer_question_with_memory_related_interest {\n functions [AnswerQuestion]\n args {\n text \"How does machine learning work?\"\n memory {\n userName \"Sarah\"\n userFacts [\n \"Education: Computer Science degree\",\n \"Occupation: Data scientist\"\n ]\n deductiveFacts [\n \"User has experience with Python programming\",\n \"User has asked about artificial intelligence topics before\"\n ]\n }\n }\n}\n\ntest test_answer_question_with_no_memory_related_interest {\n functions [AnswerQuestion]\n args {\n text \"Tell me more about quantum computing\"\n memory null\n }\n}\n\ntest test_answer_question_with_no_memory_related_interest_2 {\n functions [AnswerQuestion]\n args {\n text \"How does machine learning work?\"\n memory null\n }\n}", - "chat.baml": "// BAML functions for web chat interpretation\n// Used by packages/convex/chat.ts\n\nclass ChatInterpretation {\n response string @description(\"Conversational reply to send back (2-3 sentences max)\")\n extractedFacts string[] @description(\"New facts about the user to store in memory\")\n newTopics string[] @description(\"New topics to add to session (1-2 word tags)\")\n shouldUpdateSummary bool @description(\"Whether the session summary should be enriched\")\n summaryAddition string? @description(\"Text to append to session summary if shouldUpdateSummary is true\")\n}\n\nclass ChatSessionSummary {\n summary string\n topics string[]\n startedAt string\n endedAt string\n}\n\nclass ChatConversationMessage {\n role string @description(\"'user' or 'assistant'\")\n content string\n createdAt string\n}\n\nclass ChatContext {\n date string @description(\"Date of the conversation (YYYY-MM-DD)\")\n sessionSummaries ChatSessionSummary[] @description(\"Session summaries for this date\")\n userName string? @description(\"User's name if known\")\n userFacts string[] @description(\"Known facts about the user\")\n deductiveFacts string[] @description(\"Inferred facts about the user\")\n conversationHistory ChatConversationMessage[] @description(\"Previous messages in this chat\")\n}\n\nfunction InterpretChatMessage(\n userMessage: string,\n context: ChatContext\n) -> ChatInterpretation {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are Clairvoyant, a friendly AI assistant that helps users reflect on their day through chat conversations.\n\nThe user is chatting with you about their day. Analyze their message and provide a structured response.\n\n{{ ctx.output_format }}\n\nStyle:\n- Be warm and casual, like a friend\n- Use their name if known\n- Reference session context naturally\n- Keep responses brief - this is chat, not an essay\n- Don't be overly enthusiastic or use excessive exclamation marks\n\nDATE: {{ context.date }}\n\n{% if context.sessionSummaries | length > 0 %}\nSESSIONS FROM {{ context.date }}:\n{% for session in context.sessionSummaries %}\n- {{ session.summary }}\n Topics: {{ session.topics | join(\", \") }}\n Time: {{ session.startedAt }} to {{ session.endedAt }}\n{% endfor %}\n\n{% else %}\nNo sessions recorded for {{ context.date }} yet.\n\n{% endif %}\n{% if context.userName or context.userFacts | length > 0 or context.deductiveFacts | length > 0 %}\nUSER PROFILE:\n{% if context.userName %}Name: {{ context.userName }}{% endif %}\n{% for fact in context.userFacts %}\n- {{ fact }}\n{% endfor %}\n{% for fact in context.deductiveFacts %}\n- {{ fact }}\n{% endfor %}\n\n{% endif %}\n{% if context.conversationHistory | length > 0 %}\nCONVERSATION HISTORY:\n{% for msg in context.conversationHistory %}\n[{{ msg.role }}] {{ msg.content }}\n---\n{% endfor %}\n\n{% endif %}\nUSER'S NEW MESSAGE:\n{{ userMessage }}\n\nInterpret this message and generate a response. Extract any new facts worth remembering, suggest topic tags for the session, and indicate if the session summary should be updated.\n\"#\n}\n\ntest test_chat_simple_greeting {\n functions [InterpretChatMessage]\n args {\n userMessage \"Hey, how was my day?\"\n context {\n date \"2024-12-24\"\n sessionSummaries [\n {\n summary \"Worked on a presentation for a client meeting about AI integration\"\n topics [\"work\", \"AI\", \"presentation\"]\n startedAt \"2024-12-24T09:00:00Z\"\n endedAt \"2024-12-24T10:30:00Z\"\n }\n ]\n userName \"Ajay\"\n userFacts [\n \"Occupation: Tech founder\",\n \"Location: Toronto\"\n ]\n deductiveFacts [\n \"User is interested in AI technologies\"\n ]\n conversationHistory []\n }\n }\n}\n\ntest test_chat_with_history {\n functions [InterpretChatMessage]\n args {\n userMessage \"Actually, the meeting went really well. We landed the deal!\"\n context {\n date \"2024-12-24\"\n sessionSummaries [\n {\n summary \"Client meeting about AI integration\"\n topics [\"work\", \"AI\", \"client\"]\n startedAt \"2024-12-24T14:00:00Z\"\n endedAt \"2024-12-24T15:00:00Z\"\n }\n ]\n userName \"Sarah\"\n userFacts []\n deductiveFacts []\n conversationHistory [\n {\n role \"assistant\"\n content \"How did your client meeting go today?\"\n createdAt \"2024-12-24T16:00:00Z\"\n }\n ]\n }\n }\n}\n\ntest test_chat_no_sessions {\n functions [InterpretChatMessage]\n args {\n userMessage \"Just checking in, nothing much happened today\"\n context {\n date \"2024-12-24\"\n sessionSummaries []\n userName null\n userFacts []\n deductiveFacts []\n conversationHistory []\n }\n }\n}\n", + "chat.baml": "// BAML functions for web chat interpretation\n// Used by packages/convex/chat.ts\n\nclass ChatInterpretation {\n response string @description(\"Conversational reply to send back (2-3 sentences max)\")\n extractedFacts string[] @description(\"New facts about the user to store in memory\")\n newTopics string[] @description(\"New topics to add to session (1-2 word tags)\")\n shouldUpdateSummary bool @description(\"Whether the session summary should be enriched\")\n summaryAddition string? @description(\"Text to append to session summary if shouldUpdateSummary is true\")\n}\n\nclass ChatSessionSummary {\n summary string\n topics string[]\n startedAt string\n endedAt string\n}\n\nclass ChatConversationMessage {\n role string @description(\"'user' or 'assistant'\")\n content string\n createdAt string\n}\n\nclass ChatContext {\n date string @description(\"Date of the conversation (YYYY-MM-DD)\")\n sessionSummaries ChatSessionSummary[] @description(\"Session summaries for this date\")\n userName string? @description(\"User's name if known\")\n userFacts string[] @description(\"Known facts about the user\")\n deductiveFacts string[] @description(\"Inferred facts about the user\")\n conversationHistory ChatConversationMessage[] @description(\"Previous messages in this chat\")\n crossPeerPerspectives CrossPeerPerspective[] @description(\"Perspectives from connected users with shared memory enabled\")\n}\n\nfunction InterpretChatMessage(\n userMessage: string,\n context: ChatContext\n) -> ChatInterpretation {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are Clairvoyant, a friendly AI assistant that helps users reflect on their day through chat conversations.\n\nThe user is chatting with you about their day. Analyze their message and provide a structured response.\n\n{{ ctx.output_format }}\n\nStyle:\n- Be warm and casual, like a friend\n- Use their name if known\n- Reference session context naturally\n- Keep responses brief - this is chat, not an essay\n- Don't be overly enthusiastic or use excessive exclamation marks\n- If connected user perspectives are available, weave them naturally with attribution (e.g. \"Your wife mentioned...\", \"Alex has been looking into...\")\n\nDATE: {{ context.date }}\n\n{% if context.sessionSummaries | length > 0 %}\nSESSIONS FROM {{ context.date }}:\n{% for session in context.sessionSummaries %}\n- {{ session.summary }}\n Topics: {{ session.topics | join(\", \") }}\n Time: {{ session.startedAt }} to {{ session.endedAt }}\n{% endfor %}\n\n{% else %}\nNo sessions recorded for {{ context.date }} yet.\n\n{% endif %}\n{% if context.userName or context.userFacts | length > 0 or context.deductiveFacts | length > 0 %}\nUSER PROFILE:\n{% if context.userName %}Name: {{ context.userName }}{% endif %}\n{% for fact in context.userFacts %}\n- {{ fact }}\n{% endfor %}\n{% for fact in context.deductiveFacts %}\n- {{ fact }}\n{% endfor %}\n\n{% endif %}\n{% if context.crossPeerPerspectives | length > 0 %}\nCONNECTED USER PERSPECTIVES:\n{% for p in context.crossPeerPerspectives %}\nFrom \"{{ p.label }}\":\n{{ p.perspective }}\n{% endfor %}\n\n{% endif %}\n{% if context.conversationHistory | length > 0 %}\nCONVERSATION HISTORY:\n{% for msg in context.conversationHistory %}\n[{{ msg.role }}] {{ msg.content }}\n---\n{% endfor %}\n\n{% endif %}\nUSER'S NEW MESSAGE:\n{{ userMessage }}\n\nInterpret this message and generate a response. Extract any new facts worth remembering, suggest topic tags for the session, and indicate if the session summary should be updated.\n\"#\n}\n\ntest test_chat_simple_greeting {\n functions [InterpretChatMessage]\n args {\n userMessage \"Hey, how was my day?\"\n context {\n date \"2024-12-24\"\n sessionSummaries [\n {\n summary \"Worked on a presentation for a client meeting about AI integration\"\n topics [\"work\", \"AI\", \"presentation\"]\n startedAt \"2024-12-24T09:00:00Z\"\n endedAt \"2024-12-24T10:30:00Z\"\n }\n ]\n userName \"Ajay\"\n userFacts [\n \"Occupation: Tech founder\",\n \"Location: Toronto\"\n ]\n deductiveFacts [\n \"User is interested in AI technologies\"\n ]\n conversationHistory []\n crossPeerPerspectives []\n }\n }\n}\n\ntest test_chat_with_history {\n functions [InterpretChatMessage]\n args {\n userMessage \"Actually, the meeting went really well. We landed the deal!\"\n context {\n date \"2024-12-24\"\n sessionSummaries [\n {\n summary \"Client meeting about AI integration\"\n topics [\"work\", \"AI\", \"client\"]\n startedAt \"2024-12-24T14:00:00Z\"\n endedAt \"2024-12-24T15:00:00Z\"\n }\n ]\n userName \"Sarah\"\n userFacts []\n deductiveFacts []\n conversationHistory [\n {\n role \"assistant\"\n content \"How did your client meeting go today?\"\n createdAt \"2024-12-24T16:00:00Z\"\n }\n ]\n crossPeerPerspectives []\n }\n }\n}\n\ntest test_chat_no_sessions {\n functions [InterpretChatMessage]\n args {\n userMessage \"Just checking in, nothing much happened today\"\n context {\n date \"2024-12-24\"\n sessionSummaries []\n userName null\n userFacts []\n deductiveFacts []\n conversationHistory []\n crossPeerPerspectives []\n }\n }\n}\n", "clients.baml": "// Learn more about clients at https://docs.boundaryml.com/docs/snippets/clients/overview\n\nclient CustomGPT4o {\n provider openai\n options {\n model \"gpt-4o\"\n api_key env.OPENAI_API_KEY\n }\n}\n\nclient Groq {\n provider openai-generic\n options {\n base_url \"https://api.groq.com/openai/v1\"\n api_key env.GROQ_API_KEY\n model \"openai/gpt-oss-20b\"\n }\n}\n\nclient GroqHeavy {\n provider openai-generic\n options {\n base_url \"https://api.groq.com/openai/v1\"\n api_key env.GROQ_API_KEY\n model \"openai/gpt-oss-120b\"\n }\n}\n\n\nclient CustomGPT4oMini {\n provider openai\n retry_policy Exponential\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n }\n}\n\nclient CustomSonnet {\n provider anthropic\n options {\n model \"claude-3-5-sonnet-20241022\"\n api_key env.ANTHROPIC_API_KEY\n }\n}\n\n\nclient CustomHaiku {\n provider anthropic\n retry_policy Constant\n options {\n model \"claude-3-haiku-20240307\"\n api_key env.ANTHROPIC_API_KEY\n }\n}\n\n// https://docs.boundaryml.com/docs/snippets/clients/round-robin\nclient CustomFast {\n provider round-robin\n options {\n // This will alternate between the two clients\n strategy [CustomGPT4oMini, CustomHaiku]\n }\n}\n\n// https://docs.boundaryml.com/docs/snippets/clients/fallback\nclient OpenaiFallback {\n provider fallback\n options {\n // This will try the clients in order until one succeeds\n strategy [CustomGPT4oMini, CustomGPT4oMini]\n }\n}\n\n// https://docs.boundaryml.com/docs/snippets/clients/retry\nretry_policy Constant {\n max_retries 3\n // Strategy is optional\n strategy {\n type constant_delay\n delay_ms 200\n }\n}\n\nretry_policy Exponential {\n max_retries 2\n // Strategy is optional\n strategy {\n type exponential_backoff\n delay_ms 300\n multiplier 1.5\n max_delay_ms 10000\n }\n}", - "core.baml": "// Common classes shared across BAML functions\n\nclass MemoryCore {\n userName string? @description(\"User's name if known\")\n userFacts string[] @description(\"Relevant biographical facts about the user\")\n deductiveFacts string[] @description(\"Relevant deductive conclusions about the user's preferences and behaviors\")\n}\n\nclass EnhancedQuery {\n original string @description(\"Original user query\")\n enhanced string @description(\"Enhanced query with user context woven in\")\n}\n\nfunction EnhanceQuery(query: string, memory: MemoryCore?) -> EnhancedQuery {\n client \"Groq\"\n \n prompt #\"\n You enhance search and knowledge queries by weaving in relevant user context to get better, more personalized results.\n \n For search queries: Add context to improve external API results (e.g., Tavily, Google).\n For knowledge queries: Clarify ambiguous questions and add context that helps answer more accurately.\n \n Original query: {{ query }}\n \n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (use to enhance query):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n {% endif %}\n {% endif %}\n \n Instructions:\n - Keep the enhanced query concise (≤100 words)\n - Only add context that's RELEVANT to the query topic\n - For questions: Clarify ambiguity using user background (e.g., \"what's the weather\" → \"what's the weather in Toronto where I live\")\n - For searches: Add occupation/expertise to filter results (e.g., \"AI news\" → \"AI news for tech founders\")\n - If past interests/questions are related, reference them\n - If no memory context is relevant, return the original query as enhanced\n - Make it sound natural, not robotic\n \n {{ ctx.output_format }}\n \"#\n}\n\ntest test_enhance_query_with_tech_background {\n functions [EnhanceQuery]\n args {\n query \"What's happening in the world of AI?\"\n memory {\n userName \"Ajay\"\n userFacts [\n \"Occupation: Founder of a technology company\",\n \"Age: 37\"\n ]\n deductiveFacts [\n \"User is interested in AI because they work in tech\",\n \"User has asked about quantum computing before\"\n ]\n }\n }\n}\n\ntest test_enhance_query_no_memory {\n functions [EnhanceQuery]\n args {\n query \"Latest news on climate change\"\n memory null\n }\n}\n\ntest test_enhance_query_knowledge_ambiguous {\n functions [EnhanceQuery]\n args {\n query \"What's the weather like?\"\n memory {\n userName \"Sarah\"\n userFacts [\n \"Location: Toronto, Canada\",\n \"Age: 32\"\n ]\n deductiveFacts [\n \"User checks weather frequently in the morning\"\n ]\n }\n }\n}\n\ntest test_enhance_query_knowledge_context {\n functions [EnhanceQuery]\n args {\n query \"How does that work?\"\n memory {\n userName \"Alex\"\n userFacts [\n \"Occupation: Software engineer\"\n ]\n deductiveFacts [\n \"Asked about quantum computing 1 hour ago\"\n ]\n }\n }\n}\n", + "core.baml": "// Common classes shared across BAML functions\n\nclass CrossPeerPerspective {\n label string @description(\"Display name of the connected user\")\n perspective string @description(\"Perspective or relevant facts from the connected user's memory\")\n}\n\nclass MemoryCore {\n userName string? @description(\"User's name if known\")\n userFacts string[] @description(\"Relevant biographical facts about the user\")\n deductiveFacts string[] @description(\"Relevant deductive conclusions about the user's preferences and behaviors\")\n}\n\nclass EnhancedQuery {\n original string @description(\"Original user query\")\n enhanced string @description(\"Enhanced query with user context woven in\")\n}\n\nfunction EnhanceQuery(query: string, memory: MemoryCore?) -> EnhancedQuery {\n client \"Groq\"\n \n prompt #\"\n You enhance search and knowledge queries by weaving in relevant user context to get better, more personalized results.\n \n For search queries: Add context to improve external API results (e.g., Tavily, Google).\n For knowledge queries: Clarify ambiguous questions and add context that helps answer more accurately.\n \n Original query: {{ query }}\n \n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (use to enhance query):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n {% endif %}\n {% endif %}\n \n Instructions:\n - Keep the enhanced query concise (≤100 words)\n - Only add context that's RELEVANT to the query topic\n - For questions: Clarify ambiguity using user background (e.g., \"what's the weather\" → \"what's the weather in Toronto where I live\")\n - For searches: Add occupation/expertise to filter results (e.g., \"AI news\" → \"AI news for tech founders\")\n - If past interests/questions are related, reference them\n - If no memory context is relevant, return the original query as enhanced\n - Make it sound natural, not robotic\n \n {{ ctx.output_format }}\n \"#\n}\n\ntest test_enhance_query_with_tech_background {\n functions [EnhanceQuery]\n args {\n query \"What's happening in the world of AI?\"\n memory {\n userName \"Ajay\"\n userFacts [\n \"Occupation: Founder of a technology company\",\n \"Age: 37\"\n ]\n deductiveFacts [\n \"User is interested in AI because they work in tech\",\n \"User has asked about quantum computing before\"\n ]\n }\n }\n}\n\ntest test_enhance_query_no_memory {\n functions [EnhanceQuery]\n args {\n query \"Latest news on climate change\"\n memory null\n }\n}\n\ntest test_enhance_query_knowledge_ambiguous {\n functions [EnhanceQuery]\n args {\n query \"What's the weather like?\"\n memory {\n userName \"Sarah\"\n userFacts [\n \"Location: Toronto, Canada\",\n \"Age: 32\"\n ]\n deductiveFacts [\n \"User checks weather frequently in the morning\"\n ]\n }\n }\n}\n\ntest test_enhance_query_knowledge_context {\n functions [EnhanceQuery]\n args {\n query \"How does that work?\"\n memory {\n userName \"Alex\"\n userFacts [\n \"Occupation: Software engineer\"\n ]\n deductiveFacts [\n \"Asked about quantum computing 1 hour ago\"\n ]\n }\n }\n}\n", "daily_summary.baml": "// BAML functions for daily summary synthesis\n// Used by packages/convex/dailySynthesis.ts\n\nclass SessionInput {\n index int @description(\"Session number (1-based)\")\n summary string @description(\"Summary of the session\")\n}\n\nclass DailySummaryResult {\n summary string @description(\"1-2 casual sentences summarizing the user's day\")\n}\n\nfunction SummarizeDailySessions(\n sessions: SessionInput[],\n userProfile: string?\n) -> DailySummaryResult {\n client \"CustomGPT4oMini\"\n \n prompt #\"\nSummarize the user's day based on their glasses session notes. Write 1-2 casual sentences. Be concise and friendly. Focus on what's memorable. If you know the user's name, use it naturally. Personalize based on their profile if available.\n\n{{ ctx.output_format }}\n\n{% if userProfile %}\nUSER PROFILE:\n{{ userProfile }}\n\n{% endif %}\nSESSIONS:\n{% for session in sessions %}\nSession {{ session.index }}: {{ session.summary }}\n{% endfor %}\n\nInstructions:\n- Write 1-2 casual, friendly sentences\n- Focus on what's memorable or interesting about their day\n- Use the user's name naturally if known from the profile\n- Don't start with \"Today\" or \"You\" - vary the opening\n- Keep it conversational, like texting a friend\n\"#\n}\n\ntest test_daily_summary_single_session {\n functions [SummarizeDailySessions]\n args {\n sessions [\n { index 1, summary \"Worked on a presentation for a client meeting. Discussed AI integration strategies.\" }\n ]\n userProfile \"Name: Ajay\\nOccupation: Tech founder\\nInterests: AI, startups\"\n }\n}\n\ntest test_daily_summary_multiple_sessions {\n functions [SummarizeDailySessions]\n args {\n sessions [\n { index 1, summary \"Morning coffee at the local cafe while reviewing emails\" },\n { index 2, summary \"Video call with the team about product roadmap\" },\n { index 3, summary \"Picked up kids from school and helped with homework\" }\n ]\n userProfile \"Name: Sarah\\nLocation: Toronto\\nChildren: Two kids\"\n }\n}\n\ntest test_daily_summary_no_profile {\n functions [SummarizeDailySessions]\n args {\n sessions [\n { index 1, summary \"Went for a run in the park\" },\n { index 2, summary \"Worked from home on a coding project\" }\n ]\n userProfile null\n }\n}\n", - "email_reply.baml": "// BAML functions for email reply interpretation\n// Used by packages/convex/emailReply.ts\n\nclass EmailInterpretation {\n response string @description(\"Conversational reply to send back (2-3 sentences max)\")\n extractedFacts string[] @description(\"New facts about the user to store in memory\")\n newTopics string[] @description(\"New topics to add to session (1-2 word tags)\")\n shouldUpdateSummary bool @description(\"Whether the session summary should be enriched\")\n summaryAddition string? @description(\"Text to append to session summary if shouldUpdateSummary is true\")\n}\n\nclass ConversationMessage {\n direction string @description(\"'outbound' or 'inbound'\")\n content string\n}\n\nclass EmailContext {\n originalSubject string\n sessionSummary string? @description(\"Summary of the linked session, if any\")\n sessionTopics string[] @description(\"Topics from the linked session\")\n peerCard string[] @description(\"User profile facts from Honcho\")\n conversationHistory ConversationMessage[] @description(\"Previous messages in this email thread\")\n}\n\nfunction InterpretEmailReply(\n userMessage: string,\n context: EmailContext\n) -> EmailInterpretation {\n client \"CustomGPT4oMini\"\n \n prompt #\"\nYou are Clairvoyant, a friendly AI assistant that helps users reflect on their day through email conversations.\n\nThe user is replying to a session note email. Analyze their reply and provide:\n1. A warm, conversational response (2-3 sentences max)\n2. Any new facts about the user worth remembering\n3. New topics to tag this session with\n4. Whether to update the session summary, and if so, what to add\n\nStyle:\n- Be warm and casual, like a friend\n- Use their name if known\n- Reference session context naturally\n- Keep responses brief - this is email, not an essay\n- Don't be overly enthusiastic or use excessive exclamation marks\n\n{{ ctx.output_format }}\n\nEMAIL SUBJECT: {{ context.originalSubject }}\n\n{% if context.sessionSummary %}\nSESSION CONTEXT:\n{{ context.sessionSummary }}\nTopics: {{ context.sessionTopics | join(\", \") }}\n\n{% endif %}\n{% if context.peerCard | length > 0 %}\nUSER PROFILE:\n{% for fact in context.peerCard %}\n{{ fact }}\n{% endfor %}\n\n{% endif %}\n{% if context.conversationHistory | length > 1 %}\nCONVERSATION HISTORY:\n{% for msg in context.conversationHistory %}\n[{{ msg.direction }}] {{ msg.content }}\n---\n{% endfor %}\n\n{% endif %}\nUSER'S NEW REPLY:\n{{ userMessage }}\n\nInterpret this reply and generate a response.\n\"#\n}\n\ntest test_email_reply_simple {\n functions [InterpretEmailReply]\n args {\n userMessage \"That was a great session! I really enjoyed the discussion about AI.\"\n context {\n originalSubject \"Your session notes from Dec 21\"\n sessionSummary \"Discussed AI integration strategies for the new product\"\n sessionTopics [\"AI\", \"product\", \"strategy\"]\n peerCard [\"Name: Ajay\", \"Occupation: Tech founder\"]\n conversationHistory [\n { direction \"outbound\", content \"Here are your session notes from today...\" }\n ]\n }\n }\n}\n\ntest test_email_reply_with_new_info {\n functions [InterpretEmailReply]\n args {\n userMessage \"Thanks for the notes! By the way, I forgot to mention that my team is based in Toronto and we're planning to expand to NYC next year.\"\n context {\n originalSubject \"Your session notes from Dec 21\"\n sessionSummary \"Team meeting about expansion plans\"\n sessionTopics [\"team\", \"planning\"]\n peerCard [\"Name: Sarah\"]\n conversationHistory [\n { direction \"outbound\", content \"Here are your session notes from today...\" }\n ]\n }\n }\n}\n\ntest test_email_reply_no_session {\n functions [InterpretEmailReply]\n args {\n userMessage \"Hi! Just wanted to say the glasses are working great.\"\n context {\n originalSubject \"Welcome to Clairvoyant\"\n sessionSummary null\n sessionTopics []\n peerCard []\n conversationHistory []\n }\n }\n}\n", - "followup.baml": "class FollowupTopic {\n topic string @description(\"Short actionable title, 3-6 words max\")\n summary string @description(\"1-2 sentence context about what to follow up on\")\n}\n\nclass FollowupConversationMessage {\n role string @description(\"'user' or 'assistant'\")\n content string\n createdAt string\n}\n\nclass FollowupChatContext {\n topic string @description(\"The follow-up topic title\")\n summary string @description(\"Summary of what this follow-up is about\")\n sourceMessages string[] @description(\"Original messages that led to this follow-up\")\n conversationHistory FollowupConversationMessage[] @description(\"Previous chat messages about this follow-up\")\n memory FollowupMemoryContext? @description(\"User memory context from Honcho\")\n searchResults FollowupSearchResult[] @description(\"Web search results relevant to the topic\")\n}\n\nclass FollowupChatResponse {\n response string @description(\"Conversational reply about the follow-up topic (2-3 sentences max)\")\n extractedFacts string[] @description(\"New facts about the user to store in memory\")\n}\n\nclass FollowupMemoryContext {\n userName string? @description(\"User's name if known\")\n userFacts string[] @description(\"Known facts about the user\")\n deductiveFacts string[] @description(\"Inferred facts about the user\")\n}\n\nclass FollowupSearchResult {\n title string\n content string\n url string\n}\n\nfunction ExtractFollowupTopic(userUtterance: string, recentMessages: string[]) -> FollowupTopic {\n client \"Groq\"\n prompt #\"\n Extract the actionable topic the user wants to follow up on later.\n\n The user just said: {{ userUtterance }}\n\n Recent conversation context (most recent messages shown on display):\n {% for msg in recentMessages %}\n - {{ msg }}\n {% endfor %}\n\n Based on the recent messages, identify:\n 1. The core topic or action item the user wants to revisit\n 2. Enough context so they'll remember what this was about later\n\n Guidelines:\n - Topic should be a short, actionable title (3-6 words)\n - Summary should be 1-2 sentences capturing what to follow up on\n - Focus on what the user might want to act on or revisit\n - If the context is about a place, include the place name\n - If it's about a fact or information, capture the key detail\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest test_extract_followup_restaurant {\n functions [ExtractFollowupTopic]\n args {\n userUtterance \"bookmark this\"\n recentMessages [\"Looking for ramen near you...\", \"Top result: Mensho Tokyo, 4.5 stars, 0.3 miles away\", \"Known for their tori paitan ramen\"]\n }\n}\n\ntest test_extract_followup_weather {\n functions [ExtractFollowupTopic]\n args {\n userUtterance \"I want to come back to this\"\n recentMessages [\"Weather in Paris next week\", \"Expect rain Tuesday through Thursday\", \"Pack an umbrella for your trip\"]\n }\n}\n\nfunction InterpretFollowupChat(\n userMessage: string,\n context: FollowupChatContext\n) -> FollowupChatResponse {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are Clairvoyant, a friendly AI assistant helping the user follow up on a saved topic.\n\nThe user bookmarked this topic to revisit later. Answer their questions using the source context.\n\n{{ ctx.output_format }}\n\nStyle:\n- Be helpful and conversational\n- Reference the original context naturally\n- Keep responses brief - 2-3 sentences max\n- If you don't have enough information, say so honestly\n- Use the user's name if known\n- Reference web search results when relevant for current information\n\nFOLLOW-UP TOPIC: {{ context.topic }}\n\nSUMMARY: {{ context.summary }}\n\n{% if context.memory %}\n{% if context.memory.userName %}\nUSER NAME: {{ context.memory.userName }}\n{% endif %}\n{% if context.memory.userFacts | length > 0 %}\nUSER FACTS:\n{% for fact in context.memory.userFacts %}\n- {{ fact }}\n{% endfor %}\n{% endif %}\n{% endif %}\n\n{% if context.searchResults | length > 0 %}\nRELEVANT WEB SEARCH:\n{% for result in context.searchResults %}\n- {{ result.title }}: {{ result.content }}\n{% endfor %}\n{% endif %}\n\n{% if context.sourceMessages | length > 0 %}\nORIGINAL CONTEXT (messages when this was saved):\n{% for msg in context.sourceMessages %}\n- {{ msg }}\n{% endfor %}\n\n{% endif %}\n{% if context.conversationHistory | length > 0 %}\nCONVERSATION HISTORY:\n{% for msg in context.conversationHistory %}\n[{{ msg.role }}] {{ msg.content }}\n---\n{% endfor %}\n\n{% endif %}\nUSER'S MESSAGE:\n{{ userMessage }}\n\nProvide a helpful response based on the saved context. Also extract any new facts about the user worth remembering (preferences, opinions, decisions, plans, etc.).\n\"#\n}\n\ntest test_followup_chat_simple {\n functions [InterpretFollowupChat]\n args {\n userMessage \"What were the details again?\"\n context {\n topic \"Try Mensho Tokyo ramen\"\n summary \"Found a highly-rated ramen place nearby, known for tori paitan\"\n sourceMessages [\n \"Looking for ramen near you...\",\n \"Top result: Mensho Tokyo, 4.5 stars, 0.3 miles away\",\n \"Known for their tori paitan ramen\"\n ]\n conversationHistory []\n searchResults []\n }\n }\n}\n\ntest test_followup_chat_with_history {\n functions [InterpretFollowupChat]\n args {\n userMessage \"What should I order?\"\n context {\n topic \"Try Mensho Tokyo ramen\"\n summary \"Found a highly-rated ramen place nearby, known for tori paitan\"\n sourceMessages [\n \"Looking for ramen near you...\",\n \"Top result: Mensho Tokyo, 4.5 stars, 0.3 miles away\",\n \"Known for their tori paitan ramen\"\n ]\n conversationHistory [\n {\n role \"user\"\n content \"What were the details again?\"\n createdAt \"2024-12-28T10:00:00Z\"\n }\n {\n role \"assistant\"\n content \"Mensho Tokyo is a ramen spot 0.3 miles away with 4.5 stars. They're known for their tori paitan ramen.\"\n createdAt \"2024-12-28T10:00:01Z\"\n }\n ]\n searchResults []\n }\n }\n}\n", + "email_reply.baml": "// BAML functions for email reply interpretation\n// Used by packages/convex/emailReply.ts\n\nclass EmailInterpretation {\n response string @description(\"Conversational reply to send back (2-3 sentences max)\")\n extractedFacts string[] @description(\"New facts about the user to store in memory\")\n newTopics string[] @description(\"New topics to add to session (1-2 word tags)\")\n shouldUpdateSummary bool @description(\"Whether the session summary should be enriched\")\n summaryAddition string? @description(\"Text to append to session summary if shouldUpdateSummary is true\")\n}\n\nclass ConversationMessage {\n direction string @description(\"'outbound' or 'inbound'\")\n content string\n}\n\nclass EmailContext {\n originalSubject string\n sessionSummary string? @description(\"Summary of the linked session, if any\")\n sessionTopics string[] @description(\"Topics from the linked session\")\n peerCard string[] @description(\"User profile facts from Honcho\")\n conversationHistory ConversationMessage[] @description(\"Previous messages in this email thread\")\n crossPeerPerspectives CrossPeerPerspective[] @description(\"Perspectives from connected users with shared memory enabled\")\n}\n\nfunction InterpretEmailReply(\n userMessage: string,\n context: EmailContext\n) -> EmailInterpretation {\n client \"CustomGPT4oMini\"\n \n prompt #\"\nYou are Clairvoyant, a friendly AI assistant that helps users reflect on their day through email conversations.\n\nThe user is replying to a session note email. Analyze their reply and provide:\n1. A warm, conversational response (2-3 sentences max)\n2. Any new facts about the user worth remembering\n3. New topics to tag this session with\n4. Whether to update the session summary, and if so, what to add\n\nStyle:\n- Be warm and casual, like a friend\n- Use their name if known\n- Reference session context naturally\n- Keep responses brief - this is email, not an essay\n- Don't be overly enthusiastic or use excessive exclamation marks\n\n{{ ctx.output_format }}\n\nEMAIL SUBJECT: {{ context.originalSubject }}\n\n{% if context.sessionSummary %}\nSESSION CONTEXT:\n{{ context.sessionSummary }}\nTopics: {{ context.sessionTopics | join(\", \") }}\n\n{% endif %}\n{% if context.peerCard | length > 0 %}\nUSER PROFILE:\n{% for fact in context.peerCard %}\n{{ fact }}\n{% endfor %}\n\n{% endif %}\n{% if context.conversationHistory | length > 1 %}\nCONVERSATION HISTORY:\n{% for msg in context.conversationHistory %}\n[{{ msg.direction }}] {{ msg.content }}\n---\n{% endfor %}\n\n{% endif %}\n{% if context.crossPeerPerspectives | length > 0 %}\nCONNECTED USER PERSPECTIVES:\n{% for p in context.crossPeerPerspectives %}\nFrom \"{{ p.label }}\":\n{{ p.perspective }}\n{% endfor %}\n\n{% endif %}\nUSER'S NEW REPLY:\n{{ userMessage }}\n\nInterpret this reply and generate a response.\n\"#\n}\n\ntest test_email_reply_simple {\n functions [InterpretEmailReply]\n args {\n userMessage \"That was a great session! I really enjoyed the discussion about AI.\"\n context {\n originalSubject \"Your session notes from Dec 21\"\n sessionSummary \"Discussed AI integration strategies for the new product\"\n sessionTopics [\"AI\", \"product\", \"strategy\"]\n peerCard [\"Name: Ajay\", \"Occupation: Tech founder\"]\n conversationHistory [\n { direction \"outbound\", content \"Here are your session notes from today...\" }\n ]\n crossPeerPerspectives []\n }\n }\n}\n\ntest test_email_reply_with_new_info {\n functions [InterpretEmailReply]\n args {\n userMessage \"Thanks for the notes! By the way, I forgot to mention that my team is based in Toronto and we're planning to expand to NYC next year.\"\n context {\n originalSubject \"Your session notes from Dec 21\"\n sessionSummary \"Team meeting about expansion plans\"\n sessionTopics [\"team\", \"planning\"]\n peerCard [\"Name: Sarah\"]\n conversationHistory [\n { direction \"outbound\", content \"Here are your session notes from today...\" }\n ]\n crossPeerPerspectives []\n }\n }\n}\n\ntest test_email_reply_no_session {\n functions [InterpretEmailReply]\n args {\n userMessage \"Hi! Just wanted to say the glasses are working great.\"\n context {\n originalSubject \"Welcome to Clairvoyant\"\n sessionSummary null\n sessionTopics []\n peerCard []\n conversationHistory []\n crossPeerPerspectives []\n }\n }\n}\n", + "followup.baml": "class FollowupTopic {\n topic string @description(\"Short actionable title, 3-6 words max\")\n summary string @description(\"1-2 sentence context about what to follow up on\")\n}\n\nclass FollowupConversationMessage {\n role string @description(\"'user' or 'assistant'\")\n content string\n createdAt string\n}\n\nclass FollowupChatContext {\n topic string @description(\"The follow-up topic title\")\n summary string @description(\"Summary of what this follow-up is about\")\n sourceMessages string[] @description(\"Original messages that led to this follow-up\")\n conversationHistory FollowupConversationMessage[] @description(\"Previous chat messages about this follow-up\")\n memory FollowupMemoryContext? @description(\"User memory context from Honcho\")\n searchResults FollowupSearchResult[] @description(\"Web search results relevant to the topic\")\n crossPeerPerspectives CrossPeerPerspective[] @description(\"Perspectives from connected users with shared memory enabled\")\n}\n\nclass FollowupChatResponse {\n response string @description(\"Conversational reply about the follow-up topic (2-3 sentences max)\")\n extractedFacts string[] @description(\"New facts about the user to store in memory\")\n}\n\nclass FollowupMemoryContext {\n userName string? @description(\"User's name if known\")\n userFacts string[] @description(\"Known facts about the user\")\n deductiveFacts string[] @description(\"Inferred facts about the user\")\n}\n\nclass FollowupSearchResult {\n title string\n content string\n url string\n}\n\nfunction ExtractFollowupTopic(userUtterance: string, recentMessages: string[]) -> FollowupTopic {\n client \"Groq\"\n prompt #\"\n Extract the actionable topic the user wants to follow up on later.\n\n The user just said: {{ userUtterance }}\n\n Recent conversation context (most recent messages shown on display):\n {% for msg in recentMessages %}\n - {{ msg }}\n {% endfor %}\n\n Based on the recent messages, identify:\n 1. The core topic or action item the user wants to revisit\n 2. Enough context so they'll remember what this was about later\n\n Guidelines:\n - Topic should be a short, actionable title (3-6 words)\n - Summary should be 1-2 sentences capturing what to follow up on\n - Focus on what the user might want to act on or revisit\n - If the context is about a place, include the place name\n - If it's about a fact or information, capture the key detail\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest test_extract_followup_restaurant {\n functions [ExtractFollowupTopic]\n args {\n userUtterance \"bookmark this\"\n recentMessages [\"Looking for ramen near you...\", \"Top result: Mensho Tokyo, 4.5 stars, 0.3 miles away\", \"Known for their tori paitan ramen\"]\n }\n}\n\ntest test_extract_followup_weather {\n functions [ExtractFollowupTopic]\n args {\n userUtterance \"I want to come back to this\"\n recentMessages [\"Weather in Paris next week\", \"Expect rain Tuesday through Thursday\", \"Pack an umbrella for your trip\"]\n }\n}\n\nfunction InterpretFollowupChat(\n userMessage: string,\n context: FollowupChatContext\n) -> FollowupChatResponse {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are Clairvoyant, a friendly AI assistant helping the user follow up on a saved topic.\n\nThe user bookmarked this topic to revisit later. Answer their questions using the source context.\n\n{{ ctx.output_format }}\n\nStyle:\n- Be helpful and conversational\n- Reference the original context naturally\n- Keep responses brief - 2-3 sentences max\n- If you don't have enough information, say so honestly\n- Use the user's name if known\n- Reference web search results when relevant for current information\n\nFOLLOW-UP TOPIC: {{ context.topic }}\n\nSUMMARY: {{ context.summary }}\n\n{% if context.memory %}\n{% if context.memory.userName %}\nUSER NAME: {{ context.memory.userName }}\n{% endif %}\n{% if context.memory.userFacts | length > 0 %}\nUSER FACTS:\n{% for fact in context.memory.userFacts %}\n- {{ fact }}\n{% endfor %}\n{% endif %}\n{% endif %}\n\n{% if context.searchResults | length > 0 %}\nRELEVANT WEB SEARCH:\n{% for result in context.searchResults %}\n- {{ result.title }}: {{ result.content }}\n{% endfor %}\n{% endif %}\n\n{% if context.sourceMessages | length > 0 %}\nORIGINAL CONTEXT (messages when this was saved):\n{% for msg in context.sourceMessages %}\n- {{ msg }}\n{% endfor %}\n\n{% endif %}\n{% if context.conversationHistory | length > 0 %}\nCONVERSATION HISTORY:\n{% for msg in context.conversationHistory %}\n[{{ msg.role }}] {{ msg.content }}\n---\n{% endfor %}\n\n{% endif %}\n{% if context.crossPeerPerspectives | length > 0 %}\nCONNECTED USER PERSPECTIVES:\n{% for p in context.crossPeerPerspectives %}\nFrom \"{{ p.label }}\":\n{{ p.perspective }}\n{% endfor %}\n\n{% endif %}\nUSER'S MESSAGE:\n{{ userMessage }}\n\nProvide a helpful response based on the saved context. Also extract any new facts about the user worth remembering (preferences, opinions, decisions, plans, etc.).\n\"#\n}\n\ntest test_followup_chat_simple {\n functions [InterpretFollowupChat]\n args {\n userMessage \"What were the details again?\"\n context {\n topic \"Try Mensho Tokyo ramen\"\n summary \"Found a highly-rated ramen place nearby, known for tori paitan\"\n sourceMessages [\n \"Looking for ramen near you...\",\n \"Top result: Mensho Tokyo, 4.5 stars, 0.3 miles away\",\n \"Known for their tori paitan ramen\"\n ]\n conversationHistory []\n searchResults []\n crossPeerPerspectives []\n }\n }\n}\n\ntest test_followup_chat_with_history {\n functions [InterpretFollowupChat]\n args {\n userMessage \"What should I order?\"\n context {\n topic \"Try Mensho Tokyo ramen\"\n summary \"Found a highly-rated ramen place nearby, known for tori paitan\"\n sourceMessages [\n \"Looking for ramen near you...\",\n \"Top result: Mensho Tokyo, 4.5 stars, 0.3 miles away\",\n \"Known for their tori paitan ramen\"\n ]\n conversationHistory [\n {\n role \"user\"\n content \"What were the details again?\"\n createdAt \"2024-12-28T10:00:00Z\"\n }\n {\n role \"assistant\"\n content \"Mensho Tokyo is a ramen spot 0.3 miles away with 4.5 stars. They're known for their tori paitan ramen.\"\n createdAt \"2024-12-28T10:00:01Z\"\n }\n ]\n searchResults []\n crossPeerPerspectives []\n }\n }\n}\n", "generators.baml": "// This helps use auto generate libraries you can use in the language of\n// your choice. You can have multiple generators if you use multiple languages.\n// Just ensure that the output_dir is different for each generator.\ngenerator target {\n // Valid values: \"python/pydantic\", \"typescript\", \"ruby/sorbet\", \"rest/openapi\"\n output_type \"typescript\"\n\n // Where the generated code will be saved (relative to baml_src/)\n // Centralized location accessible by both apps/application and packages/convex\n // Outputs to packages/baml_client/ which is the @clairvoyant/baml-client workspace package\n output_dir \"../packages/baml_client/\"\n\n // The version of the BAML package you have installed (e.g. same version as your baml-py or @boundaryml/baml).\n // The BAML VSCode extension version should also match this version.\n version \"0.215.0\"\n\n // Valid values: \"sync\", \"async\"\n // This controls what `b.FunctionName()` will be (sync or async).\n default_client_mode async\n}\n", "hints.baml": "// Proactive hint system for passthrough utterances\n// Surfaces helpful knowledge when the user is talking/thinking aloud about something they have relevant memory for\n\nenum HintCategory {\n HINTABLE @description(\"User self-talk, thinking aloud, discussing a topic where stored knowledge could help.\")\n AMBIENT @description(\"Truly ambient: movie/TV dialogue, announcements, reactions, background speech from others.\")\n}\n\nclass HintEligibility {\n category HintCategory\n topic string? @description(\"If HINTABLE, the core topic or subject the user is discussing (2-5 words). Null if AMBIENT.\")\n}\n\nfunction ClassifyForHint(text: string) -> HintEligibility {\n client \"Groq\"\n prompt #\"\n Classify this utterance for proactive hint eligibility.\n\n HINTABLE (should check memory for helpful hints):\n - User thinking aloud: \"I was trying to remember when that meeting is...\"\n - User self-talk about tasks: \"need to figure out what to get Sarah for her birthday\"\n - User discussing topics: \"that reminds me of the project we were working on\"\n - User musing: \"I wonder if I should take that route today\"\n - Partial planning: \"so if I leave at 3...\"\n\n AMBIENT (no hint needed):\n - Movie/TV quotes: \"I'll never let you go Jack!\"\n - Other people's conversations: \"Ross, we were on a break!\"\n - Background announcements: \"The next station is Times Square\"\n - Pure reactions: \"oh wow\", \"no way\", \"that's crazy\"\n - Filler only: \"um\", \"uh\", \"hmm\"\n - Narration: \"He walked into the room\"\n\n {{ _.role(\"user\") }} {{ text }}\n {{ ctx.output_format }}\n \"#\n}\n\nclass HintResult {\n should_show bool @description(\"True if there's relevant, non-obvious knowledge worth surfacing.\")\n hint string? @description(\"The helpful hint in ≤10 words. Null if should_show is false.\")\n}\n\nfunction GenerateHint(\n topic: string,\n userSpeech: string,\n memory: MemoryCore?\n) -> HintResult {\n client \"Groq\"\n prompt #\"\n You are a proactive assistant. The user said something passively (not asking you directly).\n Decide if you have relevant stored knowledge that would genuinely help them.\n\n USER SAID: \"{{ userSpeech }}\"\n TOPIC: {{ topic }}\n\n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n STORED KNOWLEDGE:\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n {% endif %}\n {% endif %}\n\n RULES:\n - Only show a hint if the stored knowledge is DIRECTLY relevant to what the user just said\n - The hint must add real value — don't just echo what they said\n - If no stored knowledge relates to this topic, set should_show to false\n - Keep hints brief and natural, like a helpful friend chiming in\n - Use conversational phrasing like \"By the way...\" or \"Remember...\" or just state the fact\n - If the user is clearly talking to someone else, set should_show to false\n\n Examples of good hints:\n - User: \"need to figure out what to get Sarah\" → Hint: \"Sarah mentioned she likes pottery\"\n - User: \"I wonder if the dry cleaner is open\" → Hint: \"Last time you went on Tuesday\"\n - User: \"trying to remember that restaurant name\" → Hint: \"You liked Osteria Mozza\"\n\n Examples of when NOT to hint:\n - No relevant stored knowledge\n - User is talking TO someone else (not about something)\n - The hint would just repeat what they said\n - The topic is too vague to be helpful\n\n {{ ctx.output_format }}\n \"#\n}\n\n// Tests for hint eligibility classification\n\ntest test_hintable_self_talk {\n functions [ClassifyForHint]\n args {\n text \"I was trying to remember when that meeting is\"\n }\n @@assert({{ this.category == \"HINTABLE\" }})\n}\n\ntest test_hintable_musing {\n functions [ClassifyForHint]\n args {\n text \"need to figure out what to get Sarah for her birthday\"\n }\n @@assert({{ this.category == \"HINTABLE\" }})\n}\n\ntest test_hintable_thinking {\n functions [ClassifyForHint]\n args {\n text \"I wonder if I should take the highway today\"\n }\n @@assert({{ this.category == \"HINTABLE\" }})\n}\n\ntest test_hintable_planning {\n functions [ClassifyForHint]\n args {\n text \"so if I leave at 3 I could probably make it\"\n }\n @@assert({{ this.category == \"HINTABLE\" }})\n}\n\ntest test_ambient_movie_titanic {\n functions [ClassifyForHint]\n args {\n text \"I'll never let you go Jack!\"\n }\n @@assert({{ this.category == \"AMBIENT\" }})\n}\n\ntest test_ambient_movie_star_wars {\n functions [ClassifyForHint]\n args {\n text \"No Luke, I am your father\"\n }\n @@assert({{ this.category == \"AMBIENT\" }})\n}\n\ntest test_ambient_tv_friends {\n functions [ClassifyForHint]\n args {\n text \"Ross, we were on a break!\"\n }\n @@assert({{ this.category == \"AMBIENT\" }})\n}\n\ntest test_ambient_announcement {\n functions [ClassifyForHint]\n args {\n text \"The next station is Times Square\"\n }\n @@assert({{ this.category == \"AMBIENT\" }})\n}\n\ntest test_ambient_reaction {\n functions [ClassifyForHint]\n args {\n text \"oh wow that's crazy\"\n }\n @@assert({{ this.category == \"AMBIENT\" }})\n}\n\ntest test_ambient_filler {\n functions [ClassifyForHint]\n args {\n text \"hmm\"\n }\n @@assert({{ this.category == \"AMBIENT\" }})\n}\n\ntest test_ambient_narration {\n functions [ClassifyForHint]\n args {\n text \"He walked into the room and never looked back\"\n }\n @@assert({{ this.category == \"AMBIENT\" }})\n}\n\n// Tests for hint generation\n\ntest test_generate_hint_relevant {\n functions [GenerateHint]\n args {\n topic \"Sarah's birthday gift\"\n userSpeech \"need to figure out what to get Sarah for her birthday\"\n memory {\n userName \"Ajay\"\n userFacts [\n \"Sarah is Ajay's sister\",\n \"Sarah mentioned she likes pottery classes\"\n ]\n deductiveFacts []\n }\n }\n @@assert({{ this.should_show == true }})\n}\n\ntest test_generate_hint_no_relevant_memory {\n functions [GenerateHint]\n args {\n topic \"dry cleaner hours\"\n userSpeech \"I wonder if the dry cleaner is open\"\n memory {\n userName \"Ajay\"\n userFacts [\n \"Ajay works at a tech company\"\n ]\n deductiveFacts []\n }\n }\n @@assert({{ this.should_show == false }})\n}\n\ntest test_generate_hint_null_memory {\n functions [GenerateHint]\n args {\n topic \"restaurant name\"\n userSpeech \"trying to remember that restaurant name\"\n memory null\n }\n @@assert({{ this.should_show == false }})\n}\n", "maps.baml": "class PlaceSuggestion {\n id string\n name string\n address string\n snippet string?\n}\n\nclass PlaceLines {\n lines string[]\n}\n\nfunction SummarizePlaces(query: string, places: PlaceSuggestion[], memory: MemoryCore?) -> PlaceLines {\n client \"Groq\"\n prompt #\"\n {{ _.role(\"system\")}}\n You respond like a concise local guide. Use friendly tone and no emojis.\n Always produce at most 3 lines, each 10 words or fewer.\n Prioritize the top places in the input array order.\n Include the place name first, then a short hook (address or snippet).\n If snippet missing, mention cuisine/type inferred from query if possible.\n If there are zero places, output a single line encouraging a retry.\n\n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (weave naturally if relevant):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n {% endif %}\n {% endif %}\n\n {{ ctx.output_format }}\n {{ _.role(\"user\") }}\n {{ query }}\n {% for place in places %}\n - name=\"{{ place.name }}\"\n address=\"{{ place.address }}\"\n snippet=\"{{ place.snippet or \"\" }}\"\n {% endfor %}\n \"#\n}\n\ntest test_summarize_places {\n functions [SummarizePlaces]\n args {\n query \"Find some nice thai restaurants close to me.\"\n places [\n {\n id \"1\"\n name \"Thai Spice\"\n address \"123 Main St\"\n snippet \"Loved for creamy Thai iced tea and curries\"\n }\n {\n id \"2\"\n name \"Bangkok Bites\"\n address \"45 Elm Ave\"\n snippet \"Cozy spot, peanut noodles and tea lattes\"\n }\n {\n id \"3\"\n name \"Siam Express\"\n address \"98 Pine Rd\"\n }\n ]\n }\n @@assert({{ this.lines|length <= 3 }})\n}\n", "note.baml": "class NoteContent {\n title string @description(\"Short, descriptive title for the note (5-10 words max)\")\n summary string @description(\"2-3 sentence overview of the conversation\")\n keyPoints string[] @description(\"3-5 bullet points of important/actionable items\")\n}\n\nfunction AbridgeToNote(transcripts: string[]) -> NoteContent {\n client \"Groq\"\n prompt #\"\n You are a helpful assistant that creates concise, actionable notes from conversation transcripts.\n \n Given the following conversation transcript (array of utterances), extract the most important information\n and create a structured note that would be useful to email to the user later.\n\n Focus on:\n - Key decisions or conclusions reached\n - Action items or tasks mentioned\n - Important facts, dates, or numbers discussed\n - Names and relationships mentioned\n - Any commitments or promises made\n\n Keep the note concise and scannable. The title should capture the main topic.\n The summary should give context. The key points should be actionable or memorable.\n\n {{ ctx.output_format }}\n\n {{ _.role(\"user\") }}\n Transcript:\n {% for utterance in transcripts %}\n - {{ utterance }}\n {% endfor %}\n \"#\n}\n\ntest test_abridge_simple_conversation {\n functions [AbridgeToNote]\n args {\n transcripts [\n \"What's the weather like today?\",\n \"It's sunny and 72 degrees in San Francisco.\",\n \"Great, I should go for a run this afternoon.\",\n \"That sounds like a good idea. The UV index is moderate so bring sunscreen.\",\n \"Oh right, I also need to call mom for her birthday tomorrow.\"\n ]\n }\n @@assert({{ this.title|length > 0 }})\n @@assert({{ this.summary|length > 0 }})\n @@assert({{ this.keyPoints|length >= 1 }})\n @@assert({{ this.keyPoints|length <= 5 }})\n}\n\ntest test_abridge_meeting_notes {\n functions [AbridgeToNote]\n args {\n transcripts [\n \"Let's discuss the project timeline.\",\n \"We need to finish the MVP by March 15th.\",\n \"John will handle the backend, Sarah takes frontend.\",\n \"Budget is $50,000 for the first phase.\",\n \"We should meet again next Tuesday at 2pm.\",\n \"Don't forget to send the specs to the client by Friday.\"\n ]\n }\n @@assert({{ this.title|length > 0 }})\n @@assert({{ this.keyPoints|length >= 3 }})\n}\n", "route.baml": "enum Router {\n WEATHER @description(\"Current or upcoming weather questions for a specific place.\")\n WEB_SEARCH @description(\"News, current events, facts that change over time such as political events, or topics not obviously location-based.\")\n MAPS @description(\"Finding nearby businesses, restaurants, addresses, or directions.\")\n KNOWLEDGE @description(\"General knowledge that does not fit into other categories.\")\n MEMORY_CAPTURE @description(\"Commands to store new personal facts, preferences, or reminders for future recall.\")\n MEMORY_RECALL @description(\"Questions about the user's memory and personal history, personal preferences, personal opinions, goals, information about the user, or anything that is not a fact.\")\n NOTE_THIS @description(\"User wants to save the current conversation as a note to email. Triggered by phrases like 'add this to a note', 'send this to my email', 'this would be great to remember', 'note this down'.\")\n FOLLOW_UP @description(\"User wants to bookmark or revisit the current topic later. Triggered by: 'follow up on this', 'come back to this', 'revisit this later', 'bookmark this', 'I want to revisit this'.\")\n PASSTHROUGH @description(\"Ambient speech, filler words, incomplete sentences, or unclear utterances that don't require action.\")\n}\n\nclass RoutingBehavior {\n origin string @description(\"Echo of the user's input text verbatim.\")\n routing Router\n}\n\nfunction Route(text: string) -> RoutingBehavior {\n client \"GroqHeavy\"\n prompt #\"\n You are routing a single short utterance to one of several intent categories.\n\n STEP 1 — MENTALLY CLEAN THE UTTERANCE:\n Before routing, internally strip filler and hesitation words like: \"um\", \"uh\", \"hmm\", \"erm\",\n \"like\", \"you know\", \"uh yeah so\", \"okay so\", \"so yeah\", repeated words, false starts,\n and mid-sentence corrections.\n - If, after removing these, there is NO clear question or command left, treat as PASSTHROUGH.\n - If there IS a clear question or command after cleaning, route based on the cleaned meaning.\n - Do NOT remove words that change the core intent (e.g., \"okay\" at the start of a command is fine to keep).\n\n STEP 2 — DETERMINE DIRECTEDNESS:\n Decide if this utterance is DIRECTED AT THE ASSISTANT vs AMBIENT/BACKGROUND speech.\n\n Signs of DIRECTED speech (route to a real category):\n - Question forms: \"what\", \"who\", \"where\", \"when\", \"how\", \"why\", \"can you\", \"could you\", \"is it\"\n - Commands/imperatives: \"remind me\", \"remember that\", \"find me\", \"tell me\", \"look up\", \"show me\"\n - Personal intentions with time references: \"I need to... tomorrow\", \"I should... later\"\n - Assistant wake word or second-person address\n\n Signs of AMBIENT/BACKGROUND speech (PASSTHROUGH):\n - Movie, TV, or audiobook dialogue (dramatic lines, exclamations, third-person narrative)\n e.g., \"I'll never let you go Jack!\", \"You can't handle the truth!\", \"No Luke, I am your father\"\n - Other people's conversations (named characters talking to each other, no request to assistant)\n - Narration or storytelling not involving a request\n - Pure reactions with no follow-up: \"oh wow\", \"no way\", \"that's crazy\", \"ha ha\"\n - Background announcements: \"The next station is Times Square\"\n\n EXCEPTION: If a movie/TV line is quoted AS PART OF a question (e.g., \"What movie is 'I'll never\n let you go Jack' from?\"), do NOT treat as PASSTHROUGH — route to WEB_SEARCH or KNOWLEDGE.\n\n STEP 3 — CHOOSE EXACTLY ONE ROUTE:\n Routes (in priority order when multiple could apply):\n 1. WEATHER → questions about forecast or current weather for a place\n 2. MAPS → requests to find locations, directions, or nearby businesses\n 3. WEB_SEARCH → current events or information likely needing the internet\n 4. KNOWLEDGE → general facts that are stable over time\n 5. MEMORY_CAPTURE → commands or clear intentions to remember/store personal facts, preferences,\n or reminders — including indirect phrasing like \"I need to call the dentist tomorrow\"\n 6. MEMORY_RECALL → questions about the user's past, preferences, or personal information\n 7. NOTE_THIS → requests to save/note/email the current conversation, e.g., \"add this to a note\",\n \"send this to my email\", \"this would be great to remember\", \"note this down\", \"save this\"\n 8. FOLLOW_UP → requests to bookmark or revisit the current topic later, e.g., \"follow up on this\",\n \"come back to this\", \"revisit this later\", \"bookmark this\", \"I want to revisit this\"\n 9. PASSTHROUGH → pure filler, incomplete fragments with no clear request, ambient speech,\n or unclear utterances that don't require action\n\n Additional guidelines:\n - If the utterance mentions weather/forecast, choose WEATHER even if a location is mentioned\n - If the utterance is primarily about finding a place or directions, choose MAPS\n - When in doubt between KNOWLEDGE and WEB_SEARCH, prefer WEB_SEARCH for anything that might change\n - For vague self-talk without clear time/action (\"maybe someday I should...\"), prefer PASSTHROUGH\n - For clear near-term intentions (\"I need to call mom tomorrow\"), prefer MEMORY_CAPTURE\n\n {{ _.role(\"user\") }} {{ text }}\n {{ ctx.output_format }}\n \"#\n}\n\ntest test_weather {\n functions [Route]\n args {\n text \"What is the weather in San Francisco?\"\n }\n @@assert( {{ this.routing == \"WEATHER\"}})\n}\n\ntest test_web_search {\n functions [Route]\n args {\n text \"Who is the current president of the United States?\"\n }\n @@assert( {{ this.routing == \"WEB_SEARCH\"}})\n}\n\ntest test_maps {\n functions [Route]\n args {\n text \"Find me a ramen restaurant near Union Square.\"\n }\n @@assert( {{ this.routing == \"MAPS\"}})\n}\n\ntest test_knowledge {\n functions [Route]\n args {\n text \"What is the capital of France?\"\n }\n @@assert( {{ this.routing == \"KNOWLEDGE\"}})\n}\n\ntest test_knowledge_2 {\n functions [Route]\n args {\n text \"What is the purpose of mitochondria?\"\n }\n @@assert( {{ this.routing == \"KNOWLEDGE\"}})\n}\n\ntest test_memory {\n functions [Route]\n args {\n text \"What is my name?\"\n }\n @@assert( {{ this.routing == \"MEMORY_RECALL\"}})\n}\n\ntest test_memory_2 {\n functions [Route]\n args {\n text \"Koyal, what did I eat yesterday?\"\n }\n @@assert( {{ this.routing == \"MEMORY_RECALL\"}})\n}\n\ntest test_memory_capture {\n functions [Route]\n args {\n text \"Remember that my sister's birthday is May 3rd.\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_memory_capture_task {\n functions [Route]\n args {\n text \"Please remember I need to call the dentist tomorrow.\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_memory_capture_task_2 {\n functions [Route]\n args {\n text \"I need to call the dentist tomorrow.\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_passthrough_filler {\n functions [Route]\n args {\n text \"hmm\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_okay {\n functions [Route]\n args {\n text \"okay\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_fragment {\n functions [Route]\n args {\n text \"uh yeah so\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_weather_with_location {\n functions [Route]\n args {\n text \"What's the weather like for my trip to Paris?\"\n }\n @@assert( {{ this.routing == \"WEATHER\"}})\n}\n\n// ===== FILLER WORD TESTS =====\n// These test that fillers are stripped and the real intent is routed correctly\n\ntest test_weather_with_fillers {\n functions [Route]\n args {\n text \"um what's the weather uh in SF\"\n }\n @@assert( {{ this.routing == \"WEATHER\"}})\n}\n\ntest test_maps_with_fillers {\n functions [Route]\n args {\n text \"uh like find me a coffee shop you know nearby\"\n }\n @@assert( {{ this.routing == \"MAPS\"}})\n}\n\ntest test_knowledge_with_fillers {\n functions [Route]\n args {\n text \"so like um what is the capital of Japan\"\n }\n @@assert( {{ this.routing == \"KNOWLEDGE\"}})\n}\n\ntest test_web_search_with_fillers {\n functions [Route]\n args {\n text \"hmm uh who won the game last night\"\n }\n @@assert( {{ this.routing == \"WEB_SEARCH\"}})\n}\n\ntest test_memory_capture_with_fillers {\n functions [Route]\n args {\n text \"um yeah so remind me to call mom tomorrow\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\n// ===== AMBIENT/MOVIE DIALOGUE TESTS =====\n// These should be PASSTHROUGH as they are background speech, not directed at assistant\n\ntest test_passthrough_movie_titanic {\n functions [Route]\n args {\n text \"I'll never let you go Jack!\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_movie_star_wars {\n functions [Route]\n args {\n text \"No Luke, I am your father\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_movie_few_good_men {\n functions [Route]\n args {\n text \"You can't handle the truth!\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_tv_dialogue {\n functions [Route]\n args {\n text \"Ross, we were on a break!\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_background_announcement {\n functions [Route]\n args {\n text \"The next station is Times Square\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_reaction {\n functions [Route]\n args {\n text \"oh wow that's crazy\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\ntest test_passthrough_narration {\n functions [Route]\n args {\n text \"He walked into the room and never looked back\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\n// ===== EDGE CASES =====\n// Movie quote AS PART OF a question should NOT be passthrough\n\ntest test_movie_quote_in_question {\n functions [Route]\n args {\n text \"What movie is 'I'll never let you go Jack' from?\"\n }\n @@assert( {{ this.routing != \"PASSTHROUGH\"}})\n}\n\n// Self-talk with clear near-term intention should be MEMORY_CAPTURE\n\ntest test_self_talk_clear_intent {\n functions [Route]\n args {\n text \"I need to pick up groceries on the way home\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\ntest test_self_talk_should_call {\n functions [Route]\n args {\n text \"I should call the doctor tomorrow\"\n }\n @@assert( {{ this.routing == \"MEMORY_CAPTURE\"}})\n}\n\n// Vague self-talk without clear action should be PASSTHROUGH\n\ntest test_self_talk_vague {\n functions [Route]\n args {\n text \"maybe someday I should learn to play guitar\"\n }\n @@assert( {{ this.routing == \"PASSTHROUGH\"}})\n}\n\n// ===== NOTE_THIS TESTS =====\n// Requests to save/note/email the current conversation\n\ntest test_note_this_add_to_note {\n functions [Route]\n args {\n text \"add this to a note\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\ntest test_note_this_email_me {\n functions [Route]\n args {\n text \"email me this conversation\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\ntest test_note_this_remember {\n functions [Route]\n args {\n text \"this would be great to remember\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\ntest test_note_this_save {\n functions [Route]\n args {\n text \"save this to my notes\"\n }\n @@assert( {{ this.routing == \"NOTE_THIS\"}})\n}\n\n// ===== FOLLOW_UP TESTS =====\n// Requests to bookmark or revisit the current topic later\n\ntest test_follow_up_later {\n functions [Route]\n args {\n text \"follow up on this later\"\n }\n @@assert( {{ this.routing == \"FOLLOW_UP\"}})\n}\n\ntest test_follow_up_come_back {\n functions [Route]\n args {\n text \"I want to come back to this\"\n }\n @@assert( {{ this.routing == \"FOLLOW_UP\"}})\n}\n\ntest test_follow_up_bookmark {\n functions [Route]\n args {\n text \"bookmark this\"\n }\n @@assert( {{ this.routing == \"FOLLOW_UP\"}})\n}\n\ntest test_follow_up_revisit {\n functions [Route]\n args {\n text \"revisit this later\"\n }\n @@assert( {{ this.routing == \"FOLLOW_UP\"}})\n}", "search.baml": "class NewsItem {\n title string\n content string\n}\n\nclass AnswerLines {\n lines string[]\n}\n\nclass QueryResult {\n query string\n results AnswerLines[]\n}\n\nfunction AnswerSearch(query: string, searchResults: NewsItem[], memory: MemoryCore?) -> QueryResult {\n client \"Groq\"\n prompt #\"\n {{ _.role(\"system\")}}\n You are a helpful Web Search Summarizer who summarizes the results of a web search. \n You need give an answer to the following question: {{ query }} given the results of the web search below collected by the user. \n The output should be lines of text that sound like a human would say them to a friend and no more than 10 words per line as per the output format and no more than 3 lines as per the output format: \n \n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (use to personalize response and acknowledge past searches):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n \n If the user has searched for related topics before, naturally acknowledge this (e.g., \"You searched for X yesterday...\" or \"Following up on your interest in Y...\"). Keep it conversational.\n {% endif %}\n {% endif %}\n \n {{ ctx.output_format }}\n {{ _.role(\"user\") }}\n {% for result in searchResults %}\n Title:\n {{ result.title }}\n {{- \"\\n\" -}}\n Content:\n {{ result.content }}\n {{- \"\\n\" -}}\n {% endfor %}\n \"#\n}\n\ntest test_answer_search {\n functions [AnswerSearch]\n args {\n query \"Did Charlie Kirk die?\"\n memory null\n searchResults [\n {\n title \"California state Sen. Scott Wiener labels Charlie Kirk 'a vile bigot who' normalized 'dehumanization' - Fox News\"\n content \"### Recommended Videos Charlie Kirk # California state Sen. Scott Wiener labels Charlie Kirk 'a vile bigot who' normalized 'dehumanization' 2 min \\\"Charlie Kirk did not deserve to die. Also Charlie Kirk was a vile bigot who did immeasurable harm to so many people by normalizing dehumanization. **CHARLIE KIRK VIGILS HELD AT UNIVERSITIES ACROSS AMERICA FOLLOWING ASSASSINATION OF CONSERVATIVE ACTIVIST** \\\"Multiple things can be true: Political violence is toxic & Kirk's assassination must be condemned. Charlie Kirk poses at The Cambridge Union on May 19, 2025 in Cambridge, Cambridgeshire. (Nordin Catic/Getty Images for The Cambridge Union) Video \\\"Charlie Kirk's murder is horrific. 21 mins ago #### California state Sen. Scott Wiener labels Charlie Kirk 'a vile bigot who' normalized 'dehumanization' 1 hour ago 3 hours ago\"\n },\n {\n title \"Erika Kirk delivers moving tribute to husband, Charlie: 'I will never let your legacy die' - Fox News\"\n content \"# Erika Kirk delivers moving tribute to husband, Charlie: 'I will never let your legacy die' #### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' Erika Kirk, the widow of Charlie Kirk, made her first public remarks since her husband's death. Erika Kirk, the widow of the late Charlie Kirk, gave an emotional tribute to her husband and declared that his mission will not end at Turning Point USA's headquarters Friday. Erika Kirk spoke for the first time since the assassination of her husband, Charlie Kirk, at Turning Point USA Sept. #### Erika Kirk delivers moving tribute to husband, Charlie: 'I will never let your legacy die'\"\n },\n {\n title \"Opinion | Charlie Kirk and the Future of Political Violence - The New York Times\"\n content \"Opinion | Charlie Kirk and the Future of Political Violence - The New York Times Opinion|If We Keep This Up, Charlie Kirk Will Not Be the Last to Die https://www.nytimes.com/2025/09/11/opinion/charlie-kirk-assassination-debate.html If We Keep This Up, Charlie Kirk Will Not Be the Last to Die An assassin's bullet cut down Charlie Kirk, one of the nation's most prominent conservative activists and commentators, at a public event on the campus of Utah Valley University. When an assassin shot Kirk, that person killed a man countless students felt like they knew, and the assassin killed him _on a college campus_. Subscribe to The Times to read as many articles as you like. * Your Privacy Choices\"\n },\n {\n title \"Life, Liberty & Levin - Saturday, September 13 - Fox News\"\n content \"Watch the live stream of Fox News and full episodes. Charlie Kirk, Tribute, Legacy Fox News Channel Charlie Kirk: An American Original FOX News Radio Live Channel Coverage Fox News Channel Live ### Bill Maher calls for people to stop comparing Trump to Hitler following Charlie Kirk's assassination ### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' ### Charlie Kirk assassin was 'much closer to mainstream Democrat than any of us wanted to believe': Tim Pool ### Lara Trump says media played role in Charlie Kirk assassination ### Illegal immigrant suspect dead after dragging agent with car, DHS reports ### Friend of Charlie Kirk speaks to his legacy\"\n },\n {\n title \"Kilmeade apologizes for remarks on homelessness, mental illness - Fox News\"\n content \"Watch the live stream of Fox News and full episodes. Fox News Channel FOX News Radio Live Channel Coverage Fox News Channel Live ### Bill Maher calls for people to stop comparing Trump to Hitler following Charlie Kirk's assassination ### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' ### Mark Levin says he knew right away how special Charlie Kirk was ### Lara Trump says media played role in Charlie Kirk assassination ### Questions remain on how suspect in Charlie Kirk murder got access to roof ### Dean Phillips: Part of our core American principles were assassinated with Charlie Kirk ### Alleged assassin of Charlie Kirk held in Utah jail without bail\"\n },\n {\n title \"'His voice will remain,' Charlie Kirk's widow vows after suspect arrested - BBC\"\n content \"* \\\"I will never let his legacy die,\\\" Charlie Kirk's wife says in her first public comments since he was fatally shot at a Utah university campus on Wednesday * Erika vowed to never let Charlie's legacy die, adding that his message will carry on being shared through his campus tour of US universities and his podcast - she did not specify how they would continue 2. ### 'My husband's voice will remain,' Charlie Kirk's widow says as suspected killer in custodypublished at 08:15 BST 08:15 BST published at 03:15 03:15 His widow, Erika Kirk, vows to never let his legacy die, saying in her first public statement on Friday: \\\"The movement my husband built will not die.\\\"\"\n },\n {\n title \"Country singer warns that Charlie Kirk's death has 'awoken' millions - New York Post\"\n content \"For all the people or the hateful people out there, the groups that thought that would quiet Charlie Kirk, you've just awoken millions of other people that are not scared to die, Adcock said Monday on 'The Ingraham Angle.' 'If you live in the life of the Lord and believe in Jesus, you shouldn't be scared to leave this world, and Charlie Kirk was a great example of that.' Adcock had never met Kirk but said he often watched videos of his debates on social media. \\\"For all the people or the hateful people out there, the groups that thought that would quiet Charlie Kirk, you've just awoken millions of other people that are not scared to die,\\\" country artist Gavin Adcock said. Adcock had never met Kirk but said he watched videos of him.\"\n },\n {\n title \"Black Pastor Blasts Efforts To Whitewash Charlie Kirk's Legacy In The Wake Of His Killing - HuffPost\"\n content \"The Rev. Howard-John Wesley on Sunday spoke out against efforts to whitewash the legacy of right-wing political activist Charlie Kirk in the wake of his assassination on a Utah college campus last week, declaring that how you die does not redeem how you live. (Watch video below). The Trump administration has taken steps to honor Kirk following his death. Maggie Haberman Says Trump Is 'Struggling' With Messaging Around Charlie Kirk's Death Charlie Kirk Shooting Suspect Captured, Trump And Utah Authorities Announce GOP Lawmaker Calls Out Trump's 'Over The Top' Rhetoric After Charlie Kirk Killing Wall Street Journal Warns Trump Of 'Dangerous Moment' After Charlie Kirk Assassination\"\n },\n {\n title \"Country singer Gavin Adcock warns those who tried to silence Charlie Kirk have 'awoken' millions more - Fox News\"\n content \"# Country singer Gavin Adcock warns those who tried to silence Charlie Kirk have 'awoken' millions more ## Fans chanted 'Charlie Kirk' as Adcock waved an American flag on stage over the weekend Country singer Gavin Adcock explains why he paid tribute to Charlie Kirk in a series of shows on 'The Ingraham Angle.' Country artist Gavin Adcock warned those who thought they could silence Charlie Kirk that his death has only awakened millions more. \\\"For all the people or the hateful people out there, the groups that thought that would quiet Charlie Kirk, you've just awoken millions of other people that are not scared to die,\\\" Adcock said Monday on \\\"The Ingraham Angle.\\\"\"\n },\n {\n title \"Caroline Sunshine: The solution to America's void isn't banning guns or silencing speech - Fox News\"\n content \"Watch the live stream of Fox News and full episodes. The 'Fox News @ Night' panel discusses reactions to the assassination of Turning Point USA founder Charlie Kirk. Fox News Channel FOX News Radio Live Channel Coverage Fox News Channel Live ### Charlie Kirk's widow makes first public comments since his assassination: 'The movement my husband built will not die' ### Charlie Kirk assassin was 'much closer to mainstream Democrat than any of us wanted to believe': Tim Pool ### Donald Trump Jr on Charlie Kirk's assassination: The violence is 'not going both ways' ### 'We have him': Trump says suspected Charlie Kirk assassin is in custody ### Phoenix air traffic control honors Charlie Kirk: 'May God bless your family'\"\n }\n ]\n }\n @@assert({{ this.results[0].lines|length == 3 }})\n}\n", + "sensitivity.baml": "// Sensitivity classification for cross-peer shared content\n\nenum SensitivityCategory {\n SAFE @description(\"Content is safe to share across peers\")\n SENSITIVE @description(\"Content contains sensitive personal information that should not be shared\")\n}\n\nfunction CheckSensitivity(crossPeerContext: string) -> SensitivityCategory {\n client \"Groq\"\n\n prompt #\"\nClassify whether the following cross-peer context contains sensitive personal information that should NOT be shared between connected users.\n\nSENSITIVE includes: medical/health details, financial information, passwords/credentials, private relationship issues, legal matters, or anything the user would reasonably expect to remain private.\n\nSAFE includes: general interests, preferences, professional topics, hobbies, travel plans, food preferences, or other casual information.\n\nContent to classify:\n{{ crossPeerContext }}\n\n{{ ctx.output_format }}\n\"#\n}\n\ntest test_safe_content {\n functions [CheckSensitivity]\n args {\n crossPeerContext \"User enjoys hiking and is interested in AI technology. Works as a software engineer.\"\n }\n}\n\ntest test_sensitive_content {\n functions [CheckSensitivity]\n args {\n crossPeerContext \"User mentioned they are dealing with a medical diagnosis and has been seeing a therapist.\"\n }\n}\n", "session_summary.baml": "class SessionSummaryOutput {\n summary string @description(\"1-2 sentence summary of what was discussed\")\n topics string[] @description(\"2-5 key topics or themes from the session\")\n}\n\nfunction SummarizeSession(\n transcripts: string[]\n) -> SessionSummaryOutput {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are summarizing a conversation session from smart glasses.\n\n{{ ctx.output_format }}\n\nCONVERSATION TRANSCRIPTS:\n{% for transcript in transcripts %}\n- {{ transcript }}\n{% endfor %}\n\nInstructions:\n- First, filter out incomplete sentences, single words, filler words (e.g., \"um\", \"uh\", \"okay\"), and obvious transcription errors\n- Write a brief 1-2 sentence summary of what the user discussed or asked about\n- Extract 2-5 key topics or themes (e.g., \"weather\", \"navigation\", \"family\", \"work\")\n- Keep topics as single words or short phrases\n- Focus on what's memorable or might be referenced later\n- If transcripts are empty, trivial, or only contain noise/filler, return summary \"No meaningful activity\" with topics [\"none\"]\n\"#\n}\n\ntest test_summarize_session_weather {\n functions [SummarizeSession]\n args {\n transcripts [\n \"What's the weather like today?\",\n \"Is it going to rain later?\",\n \"Should I bring an umbrella?\"\n ]\n }\n @@assert({{ this.topics|length >= 1 }})\n @@assert({{ this.topics|length <= 5 }})\n}\n\ntest test_summarize_session_mixed {\n functions [SummarizeSession]\n args {\n transcripts [\n \"Navigate to the nearest coffee shop\",\n \"What time does Starbucks close?\",\n \"Remind me to call mom later\",\n \"What's on my calendar today?\"\n ]\n }\n @@assert({{ this.topics|length >= 2 }})\n @@assert({{ this.topics|length <= 5 }})\n}\n\ntest test_summarize_session_empty {\n functions [SummarizeSession]\n args {\n transcripts []\n }\n @@assert({{ this.summary|length > 0 }})\n}\n\ntest test_summarize_session_noise {\n functions [SummarizeSession]\n args {\n transcripts [\n \"um\",\n \"okay\",\n \"uh yeah\",\n \"hmm\"\n ]\n }\n @@assert({{ this.topics|length >= 1 }})\n}\n\ntest test_summarize_session_mixed_quality {\n functions [SummarizeSession]\n args {\n transcripts [\n \"um\",\n \"What's the weather today?\",\n \"okay\",\n \"Find me a coffee shop nearby\"\n ]\n }\n @@assert({{ this.topics|length >= 1 }})\n}\n", "synthesis.baml": "class MemoryContext {\n explicitFacts string[]\n deductiveFacts string[]\n peerCard string[]\n recentMessages string[]\n sessionSummaries string[] @description(\"Summaries of past sessions, e.g. 'Dec 20: discussed weather and navigation'\")\n crossPeerPerspectives CrossPeerPerspective[] @description(\"Perspectives from connected users with shared memory enabled\")\n}\n\nclass MemorySynthesisLines {\n lines string[] @description(\"1-3 concise lines answering the query\")\n}\n\nfunction SynthesizeMemory(\n query: string,\n context: MemoryContext\n) -> MemorySynthesisLines {\n client \"GroqHeavy\"\n \n prompt #\"\nYou are a memory synthesis agent. Answer the user's query using their stored memories.\n\n{{ ctx.output_format }}\n\nUSER'S BIOGRAPHICAL INFO:\n{% for fact in context.peerCard %}\n{{ fact }}\n{% endfor %}\n\nEXPLICIT FACTS (directly stated by user):\n{% for fact in context.explicitFacts %}\n- {{ fact }}\n{% endfor %}\n\nDEDUCTIVE CONCLUSIONS:\n{% for fact in context.deductiveFacts %}\n- {{ fact }}\n{% endfor %}\n\nRECENT CONVERSATION:\n{% for msg in context.recentMessages %}\n{{ msg }}\n{% endfor %}\n\nPAST SESSION SUMMARIES:\n{% for summary in context.sessionSummaries %}\n- {{ summary }}\n{% endfor %}\n\n{% if context.crossPeerPerspectives | length > 0 %}\nCONNECTED USER PERSPECTIVES:\n{% for p in context.crossPeerPerspectives %}\nFrom \"{{ p.label }}\":\n{{ p.perspective }}\n{% endfor %}\n\n{% endif %}\nQUERY: {{ query }}\n\nInstructions:\n- Provide 1-3 short lines (≤10 words each) that directly answer the query\n- Use a natural, conversational tone like texting a friend\n- If the user's name is known from peerCard, use it naturally\n- You can use contractions and casual language\n- If you don't have enough information to answer, say so briefly\n- Focus on what's most relevant to the query\n- Ignore any alphanumeric IDs or hashes in the facts (e.g., \"j979r44m...\" prefixes) — extract only the meaningful content\n- If connected users have relevant perspectives, naturally mention them (e.g. \"Sarah thinks...\", \"Alex mentioned...\")\n\"#\n}\n\ntest test_memory_synthesis_name {\n functions [SynthesizeMemory]\n args {\n query \"What is my name?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"My name is Ajay Bhargava, that's spelled A-J-A-Y, last name B-H-A-R-G-A-V-A.\",\n \"What is my name?\"\n ]\n sessionSummaries []\n crossPeerPerspectives []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_children {\n functions [SynthesizeMemory]\n args {\n query \"Tell me about my daughters\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has two daughters.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters' names are Koyal and Kavya.\",\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's daughters are five and two years old.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe has children of different ages.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Children: Two daughters (Koyal and Kavya)\",\n \"Daughters' ages: 5 and 2 years old\"\n ]\n recentMessages [\n \"I have two girls, five and two. Their names are Koyal and Kavya.\",\n \"Tell me about my daughters\"\n ]\n sessionSummaries []\n crossPeerPerspectives []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_no_info {\n functions [SynthesizeMemory]\n args {\n query \"What car do I drive?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's name is Ajay Bhargava.\"\n ]\n deductiveFacts []\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\"\n ]\n recentMessages [\n \"What car do I drive?\"\n ]\n sessionSummaries []\n crossPeerPerspectives []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_memory_synthesis_age {\n functions [SynthesizeMemory]\n args {\n query \"How old am I?\"\n context {\n explicitFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe's date of birth is December 19, 1987.\"\n ]\n deductiveFacts [\n \"j979r44mshwqcstwyjps48mrw57v4vat-diatribe is 37 years old.\"\n ]\n peerCard [\n \"Name: Ajay Bhargava\",\n \"Date of birth: December 19, 1987\",\n \"Age: 37\"\n ]\n recentMessages [\n \"How old am I?\"\n ]\n sessionSummaries []\n crossPeerPerspectives []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n", "weather.baml": "class WeatherConditionLite {\n id int\n main string\n description string\n icon string\n}\n\nclass CurrentLite {\n temperature float\n feels_like float\n conditions WeatherConditionLite\n humidity int\n pressure int\n wind_speed float\n wind_direction int\n visibility int\n uv_index float\n clouds int\n}\n\nclass TempBlock {\n day float\n min float\n max float\n night float\n}\n\nclass DailyForecastItem {\n date string\n summary string\n temperature TempBlock\n conditions WeatherConditionLite\n precipitation_probability float\n rain float\n}\n\nclass LocationLite {\n lat float\n lon float\n timezone string\n}\n\nclass AlertLite {\n sender_name string\n event string\n start int\n end int\n description string\n tags string[]\n}\n\nclass FormattedWeather {\n location LocationLite\n current CurrentLite\n daily_forecast DailyForecastItem[]\n alerts AlertLite[]\n}\n\nclass WeatherLines {\n lines string[]\n}\n\nfunction SummarizeWeatherFormatted(input: FormattedWeather, unit: string, memory: MemoryCore?) -> WeatherLines {\n client \"openai/gpt-4o-mini\"\n\n prompt #\"\n You are a witty, conversational weather reporter that's super brief even if you have a lot of data to work with.\n Write exactly 10 words that sound like a friendly human texted them to a friend.\n Avoid bullet points, lists, or emoji. \n Use the data to describe the weather on a scale of 1-10 out of 10 and include the \"vibe rating\" of the weather.\n {{ ctx.output_format }}\n\n {% if memory %}\n {% set hasUserName = memory.userName is defined and memory.userName %}\n {% set hasUserFacts = memory.userFacts|length > 0 %}\n {% set hasDeductiveFacts = memory.deductiveFacts|length > 0 %}\n {% if hasUserName or hasUserFacts or hasDeductiveFacts %}\n User Context (weave naturally if relevant):\n {% if memory.userName %}Name: {{ memory.userName }}{% endif %}\n {% for fact in memory.userFacts %}\n - {{ fact }}\n {% endfor %}\n {% for fact in memory.deductiveFacts %}\n - {{ fact }}\n {% endfor %}\n {% endif %}\n {% endif %}\n\n Style rules:\n - You are only allowed to print 3 lines in the list output. \n - Each line can be a continuation of the previous line.\n - The first line should be the scale and the vibe rating.\n - The second line should be the current weather.\n - The third line should be the alert if there is one OR a personalized insight based on user context.\n - Always include the temperature unit symbol (e.g., \"72°F\" or \"22°C\") when mentioning temperatures.\n\n Data (verbatim):\n Location:\n timezone=\"{{ input.location.timezone }}\"\n Now:\n {% if unit == \"C\" %}\n temp_c={{ input.current.temperature }}\n feels_c={{ input.current.feels_like }}\n {% else %}\n temp_f={{ input.current.temperature }}\n feels_f={{ input.current.feels_like }}\n {% endif %}\n desc=\"{{ input.current.conditions.description }}\"\n wind_ms={{ input.current.wind_speed }}\n wind_deg={{ input.current.wind_direction }}\n humidity={{ input.current.humidity }}\n uv_index={{ input.current.uv_index }}\n clouds={{ input.current.clouds }}\n visibility_m={{ input.current.visibility }}\n Today:\n date=\"{{ input.daily_forecast[0].date if input.daily_forecast|length > 0 else \"\" }}\"\n {% if unit == \"C\" %}\n min_c={{ input.daily_forecast[0].temperature.min if input.daily_forecast|length > 0 else 0 }}\n max_c={{ input.daily_forecast[0].temperature.max if input.daily_forecast|length > 0 else 0 }}\n {% else %}\n min_f={{ input.daily_forecast[0].temperature.min if input.daily_forecast|length > 0 else 0 }}\n max_f={{ input.daily_forecast[0].temperature.max if input.daily_forecast|length > 0 else 0 }}\n {% endif %}\n pop={{ input.daily_forecast[0].precipitation_probability if input.daily_forecast|length > 0 else 0.0 }}\n rain_mm={{ input.daily_forecast[0].rain if input.daily_forecast|length > 0 else 0.0 }}\n desc=\"{{ input.daily_forecast[0].summary if input.daily_forecast|length > 0 else \"\" }}\"\n Tomorrow:\n date=\"{{ input.daily_forecast[1].date if input.daily_forecast|length > 1 else \"\" }}\"\n {% if unit == \"C\" %}\n min_c={{ input.daily_forecast[1].temperature.min if input.daily_forecast|length > 1 else 0 }}\n max_c={{ input.daily_forecast[1].temperature.max if input.daily_forecast|length > 1 else 0 }}\n {% else %}\n min_f={{ input.daily_forecast[1].temperature.min if input.daily_forecast|length > 1 else 0 }}\n max_f={{ input.daily_forecast[1].temperature.max if input.daily_forecast|length > 1 else 0 }}\n {% endif %}\n pop={{ input.daily_forecast[1].precipitation_probability if input.daily_forecast|length > 1 else 0.0 }}\n rain_mm={{ input.daily_forecast[1].rain if input.daily_forecast|length > 1 else 0.0 }}\n desc=\"{{ input.daily_forecast[1].summary if input.daily_forecast|length > 1 else \"\" }}\"\n Alert:\n {% if input.alerts|length > 0 %}\n event=\"{{ input.alerts[0].event }}\"\n end_unix={{ input.alerts[0].end }}\n sender=\"{{ input.alerts[0].sender_name }}\"\n tags=\"{{ input.alerts[0].tags | join(\", \") }}\"\n {% endif %}\n \"#\n}\n\ntest test_weather_lines_fahrenheit {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 40.7128\n lon -74.006\n timezone \"America/New_York\"\n }\n current {\n temperature 75.0\n feels_like 75.0\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n humidity 50\n pressure 1013\n wind_speed 5.5\n wind_direction 180\n visibility 10000\n uv_index 6.5\n clouds 10\n }\n daily_forecast [\n {\n date \"2025-08-12T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with clear spells\"\n temperature {\n day 88.46600000000001\n min 65.984\n max 88.46600000000001\n night 76.15400000000005\n }\n conditions {\n id 801\n main \"Clouds\"\n description \"few clouds\"\n icon \"02d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-13T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 87.35\n min 71.00600000000003\n max 87.85400000000006\n night 80.40200000000007\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 4.89\n },\n {\n date \"2025-08-14T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 81.68000000000004\n min 76.424\n max 88.016\n night 80.79800000000003\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 2.85\n },\n {\n date \"2025-08-15T17:00:00.000Z\"\n summary \"You can expect clear sky in the morning, with partly cloudy in the afternoon\"\n temperature {\n day 84.90200000000007\n min 75.72200000000004\n max 85.53200000000001\n night 76.55\n }\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-16T17:00:00.000Z\"\n summary \"There will be partly cloudy today\"\n temperature {\n day 81.57200000000003\n min 74.33600000000007\n max 81.57200000000003\n night 76.55\n }\n conditions {\n id 803\n main \"Clouds\"\n description \"broken clouds\"\n icon \"04d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-17T17:00:00.000Z\"\n summary \"There will be partly cloudy today\"\n temperature {\n day 90.64400000000008\n min 73.90400000000005\n max 90.64400000000008\n night 83.67800000000007\n }\n conditions {\n id 801\n main \"Clouds\"\n description \"few clouds\"\n icon \"02d\"\n }\n precipitation_probability 0.12\n rain 0\n },\n {\n date \"2025-08-18T16:00:00.000Z\"\n summary \"There will be rain until morning, then partly cloudy\"\n temperature {\n day 85.1\n min 75.99199999999999\n max 90.95\n night 80.25800000000001\n }\n conditions {\n id 500\n main \"Rain\"\n description \"light rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 2.4\n },\n {\n date \"2025-08-19T16:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 80.49199999999999\n min 76.13600000000007\n max 84.81200000000005\n night 76.13600000000007\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 11.61\n }\n ]\n alerts [\n {\n sender_name \"NWS Upton NY\"\n event \"Air Quality Alert\"\n start 1755004380\n end 1755054000\n description \"The New York State Department of Environmental Conservation has\\nissued an Air Quality Health Advisory for the following counties:\\n\\nNew York, Bronx, Kings, Queens, Richmond, Nassau, Suffolk,\\nWestchester, Rockland, Orange, Putnam.\\n\\nuntil 11 PM EDT this evening.\\n\\nAir quality levels in outdoor air are predicted to be greater than\\nan Air Quality Index value of 100 for the pollutant of Ground Level\\nOzone. The Air Quality Index, or AQI, was created as an easy way to\\ncorrelate levels of different pollutants to one scale. The higher\\nthe AQI value, the greater the health concern.\\n\\nWhen pollution levels are elevated, the New York State Department of\\nHealth recommends that individuals consider limiting strenuous\\noutdoor physical activity to reduce the risk of adverse health\\neffects. People who may be especially sensitive to the effects of\\nelevated levels of pollutants include the very young, and those with\\npreexisting respiratory problems such as asthma or heart disease.\\nThose with symptoms should consider consulting their personal\\nphysician.\\n\\nFor additional information, please visit the New York State\\nDepartment of Environmental Conservation website at,\\nhttps://on.ny.gov/nyaqi, or call the Air Quality Hotline at\\n1 800 5 3 5, 1 3 4 5.\"\n tags [\n \"Air quality\"\n ]\n }\n ]\n }\n unit \"F\"\n }\n}\n\ntest test_weather_lines_celsius {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 40.7128\n lon -74.006\n timezone \"America/New_York\"\n }\n current {\n temperature 23.9\n feels_like 23.9\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n humidity 50\n pressure 1013\n wind_speed 5.5\n wind_direction 180\n visibility 10000\n uv_index 6.5\n clouds 10\n }\n daily_forecast [\n {\n date \"2025-08-12T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with clear spells\"\n temperature {\n day 31.37\n min 18.88\n max 31.37\n night 24.53\n }\n conditions {\n id 801\n main \"Clouds\"\n description \"few clouds\"\n icon \"02d\"\n }\n precipitation_probability 0\n rain 0\n },\n {\n date \"2025-08-13T17:00:00.000Z\"\n summary \"Expect a day of partly cloudy with rain\"\n temperature {\n day 30.75\n min 21.67\n max 31.03\n night 26.89\n }\n conditions {\n id 501\n main \"Rain\"\n description \"moderate rain\"\n icon \"10d\"\n }\n precipitation_probability 1\n rain 4.89\n }\n ]\n alerts []\n }\n unit \"C\"\n }\n}\n\ntest test_weather_with_memory_cold_preference {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 43.6532\n lon -79.3832\n timezone \"America/Toronto\"\n }\n current {\n temperature 2.0\n feels_like -3.0\n conditions {\n id 600\n main \"Snow\"\n description \"light snow\"\n icon \"13d\"\n }\n humidity 85\n pressure 1015\n wind_speed 8.5\n wind_direction 320\n visibility 5000\n uv_index 1.0\n clouds 90\n }\n daily_forecast [\n {\n date \"2025-11-12T17:00:00.000Z\"\n summary \"Expect a day of snow with cold temperatures\"\n temperature {\n day 2.0\n min -2.0\n max 3.0\n night -1.0\n }\n conditions {\n id 600\n main \"Snow\"\n description \"light snow\"\n icon \"13d\"\n }\n precipitation_probability 0.8\n rain 0\n },\n {\n date \"2025-11-13T17:00:00.000Z\"\n summary \"Cold and clear skies\"\n temperature {\n day 1.0\n min -4.0\n max 2.0\n night -3.0\n }\n conditions {\n id 800\n main \"Clear\"\n description \"clear sky\"\n icon \"01d\"\n }\n precipitation_probability 0.1\n rain 0\n }\n ]\n alerts []\n }\n unit \"C\"\n memory {\n userName \"Ajay\"\n userFacts [\n \"Location: Toronto\",\n \"Age: 37\"\n ]\n deductiveFacts [\n \"User likes cold weather because they are from Canada\"\n ]\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}\n\ntest test_weather_with_memory_no_deductions {\n functions [SummarizeWeatherFormatted]\n args {\n input {\n location {\n lat 37.7749\n lon -122.4194\n timezone \"America/Los_Angeles\"\n }\n current {\n temperature 18.0\n feels_like 17.0\n conditions {\n id 500\n main \"Rain\"\n description \"light rain\"\n icon \"10d\"\n }\n humidity 70\n pressure 1012\n wind_speed 4.5\n wind_direction 270\n visibility 8000\n uv_index 2.0\n clouds 75\n }\n daily_forecast [\n {\n date \"2025-11-12T17:00:00.000Z\"\n summary \"Rainy day with mild temperatures\"\n temperature {\n day 18.0\n min 14.0\n max 20.0\n night 15.0\n }\n conditions {\n id 500\n main \"Rain\"\n description \"light rain\"\n icon \"10d\"\n }\n precipitation_probability 0.9\n rain 5.2\n },\n {\n date \"2025-11-13T17:00:00.000Z\"\n summary \"Partly cloudy with chance of rain\"\n temperature {\n day 19.0\n min 15.0\n max 21.0\n night 16.0\n }\n conditions {\n id 802\n main \"Clouds\"\n description \"scattered clouds\"\n icon \"03d\"\n }\n precipitation_probability 0.4\n rain 1.2\n }\n ]\n alerts []\n }\n unit \"C\"\n memory {\n userName \"Sarah\"\n userFacts [\n \"Children: Two daughters (ages 5 and 2)\",\n \"Occupation: Founder of a technology company\"\n ]\n deductiveFacts []\n }\n }\n @@assert({{ this.lines|length <= 3 }})\n @@assert({{ this.lines|length >= 1 }})\n}", diff --git a/packages/baml_client/baml_client/parser.ts b/packages/baml_client/baml_client/parser.ts index ec2e031..49eba16 100644 --- a/packages/baml_client/baml_client/parser.ts +++ b/packages/baml_client/baml_client/parser.ts @@ -23,7 +23,7 @@ import { toBamlError } from "@boundaryml/baml" import type { Checked, Check } from "./types" import type { partial_types } from "./partial_types" import type * as types from "./types" -import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" +import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CrossPeerPerspective, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SensitivityCategory, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" import type TypeBuilder from "./type_builder" export class LlmResponseParser { @@ -99,6 +99,29 @@ export class LlmResponseParser { } } + CheckSensitivity( + llmResponse: string, + __baml_options__?: { tb?: TypeBuilder, clientRegistry?: ClientRegistry, env?: Record } + ): types.SensitivityCategory { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return this.runtime.parseLlmResponse( + "CheckSensitivity", + llmResponse, + false, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + __env__, + ) as types.SensitivityCategory + } catch (error) { + throw toBamlError(error); + } + } + ClassifyForHint( llmResponse: string, __baml_options__?: { tb?: TypeBuilder, clientRegistry?: ClientRegistry, env?: Record } @@ -473,6 +496,29 @@ export class LlmStreamParser { } } + CheckSensitivity( + llmResponse: string, + __baml_options__?: { tb?: TypeBuilder, clientRegistry?: ClientRegistry, env?: Record } + ): types.SensitivityCategory { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return this.runtime.parseLlmResponse( + "CheckSensitivity", + llmResponse, + true, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + __env__, + ) as types.SensitivityCategory + } catch (error) { + throw toBamlError(error); + } + } + ClassifyForHint( llmResponse: string, __baml_options__?: { tb?: TypeBuilder, clientRegistry?: ClientRegistry, env?: Record } diff --git a/packages/baml_client/baml_client/partial_types.ts b/packages/baml_client/baml_client/partial_types.ts index e161698..b6a54ea 100644 --- a/packages/baml_client/baml_client/partial_types.ts +++ b/packages/baml_client/baml_client/partial_types.ts @@ -20,7 +20,7 @@ $ pnpm add @boundaryml/baml import type { Image, Audio, Pdf, Video } from "@boundaryml/baml" import type { Checked, Check } from "./types" -import type { AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines } from "./types" +import type { AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CrossPeerPerspective, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SensitivityCategory, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines } from "./types" import type * as types from "./types" /****************************************************************************** @@ -54,6 +54,7 @@ export namespace partial_types { userFacts: string[] deductiveFacts: string[] conversationHistory: ChatConversationMessage[] + crossPeerPerspectives: CrossPeerPerspective[] } export interface ChatConversationMessage { role?: string | null @@ -77,6 +78,10 @@ export namespace partial_types { direction?: string | null content?: string | null } + export interface CrossPeerPerspective { + label?: string | null + perspective?: string | null + } export interface CurrentLite { temperature?: number | null feels_like?: number | null @@ -106,6 +111,7 @@ export namespace partial_types { sessionTopics: string[] peerCard: string[] conversationHistory: ConversationMessage[] + crossPeerPerspectives: CrossPeerPerspective[] } export interface EmailInterpretation { response?: string | null @@ -125,6 +131,7 @@ export namespace partial_types { conversationHistory: FollowupConversationMessage[] memory?: FollowupMemoryContext | null searchResults: FollowupSearchResult[] + crossPeerPerspectives: CrossPeerPerspective[] } export interface FollowupChatResponse { response?: string | null @@ -176,10 +183,6 @@ export namespace partial_types { sessionSummaries: string[] crossPeerPerspectives: CrossPeerPerspective[] } - export interface CrossPeerPerspective { - label?: string | null - perspective?: string | null - } export interface MemoryCore { userName?: string | null userFacts: string[] diff --git a/packages/baml_client/baml_client/sync_client.ts b/packages/baml_client/baml_client/sync_client.ts index 88767f5..00fcb86 100644 --- a/packages/baml_client/baml_client/sync_client.ts +++ b/packages/baml_client/baml_client/sync_client.ts @@ -22,7 +22,7 @@ import type { BamlRuntime, FunctionResult, BamlCtxManager, Image, Audio, Pdf, Vi import { toBamlError, BamlAbortError, type HTTPRequest } from "@boundaryml/baml" import type { Checked, Check, RecursivePartialNull as MovedRecursivePartialNull } from "./types" import type * as types from "./types" -import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" +import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CrossPeerPerspective, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SensitivityCategory, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" import type TypeBuilder from "./type_builder" import { HttpRequest, HttpStreamRequest } from "./sync_request" import { LlmResponseParser, LlmStreamParser } from "./parser" @@ -222,6 +222,48 @@ export class BamlSyncClient { } } + CheckSensitivity( + crossPeerContext: string, + __baml_options__?: BamlCallOptions + ): types.SensitivityCategory { + try { + const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) } + const __signal__ = __options__.signal; + + if (__signal__?.aborted) { + throw new BamlAbortError('Operation was aborted', __signal__.reason); + } + + // Check if onTick is provided and reject for sync operations + if (__options__.onTick) { + throw new Error("onTick is not supported for synchronous functions. Please use the async client instead."); + } + + const __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : [__options__.collector]) : []; + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + const __raw__ = this.runtime.callFunctionSync( + "CheckSensitivity", + { + "crossPeerContext": crossPeerContext + }, + this.ctxManager.cloneContext(), + __options__.tb?.__tb(), + __options__.clientRegistry, + __collector__, + __options__.tags || {}, + __env__, + __signal__, + __options__.watchers, + ) + return __raw__.parsed(false) as types.SensitivityCategory + } catch (error: any) { + throw toBamlError(error); + } + } + ClassifyForHint( text: string, __baml_options__?: BamlCallOptions diff --git a/packages/baml_client/baml_client/sync_request.ts b/packages/baml_client/baml_client/sync_request.ts index a62508a..7bcaf26 100644 --- a/packages/baml_client/baml_client/sync_request.ts +++ b/packages/baml_client/baml_client/sync_request.ts @@ -22,7 +22,7 @@ import type { BamlRuntime, BamlCtxManager, ClientRegistry, Image, Audio, Pdf, Vi import { toBamlError, HTTPRequest } from "@boundaryml/baml" import type { Checked, Check } from "./types" import type * as types from "./types" -import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" +import type {AlertLite, AnswerLines, ChatContext, ChatConversationMessage, ChatInterpretation, ChatSessionSummary, ConversationMessage, CrossPeerPerspective, CurrentLite, DailyForecastItem, DailySummaryResult, EmailContext, EmailInterpretation, EnhancedQuery, FollowupChatContext, FollowupChatResponse, FollowupConversationMessage, FollowupMemoryContext, FollowupSearchResult, FollowupTopic, FormattedWeather, HintCategory, HintEligibility, HintResult, LocationLite, MemoryContext, MemoryCore, MemorySynthesisLines, NewsItem, NoteContent, PlaceLines, PlaceSuggestion, QueryResult, QuestionAnalysisResponse, Router, RoutingBehavior, SensitivityCategory, SessionInput, SessionSummaryOutput, TempBlock, WeatherConditionLite, WeatherLines} from "./types" import type TypeBuilder from "./type_builder" import type * as events from "./events" @@ -112,6 +112,31 @@ export class HttpRequest { } } + CheckSensitivity( + crossPeerContext: string, + __baml_options__?: BamlCallOptions + ): HTTPRequest { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return this.runtime.buildRequestSync( + "CheckSensitivity", + { + "crossPeerContext": crossPeerContext + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + false, + __env__, + ) + } catch (error) { + throw toBamlError(error); + } + } + ClassifyForHint( text: string, __baml_options__?: BamlCallOptions @@ -518,6 +543,31 @@ export class HttpStreamRequest { } } + CheckSensitivity( + crossPeerContext: string, + __baml_options__?: BamlCallOptions + ): HTTPRequest { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return this.runtime.buildRequestSync( + "CheckSensitivity", + { + "crossPeerContext": crossPeerContext + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + true, + __env__, + ) + } catch (error) { + throw toBamlError(error); + } + } + ClassifyForHint( text: string, __baml_options__?: BamlCallOptions diff --git a/packages/baml_client/baml_client/type_builder.ts b/packages/baml_client/baml_client/type_builder.ts index bd32a1a..faa843d 100644 --- a/packages/baml_client/baml_client/type_builder.ts +++ b/packages/baml_client/baml_client/type_builder.ts @@ -31,7 +31,7 @@ export default class TypeBuilder { AnswerLines: ClassViewer<'AnswerLines', "lines">; - ChatContext: ClassViewer<'ChatContext', "date" | "sessionSummaries" | "userName" | "userFacts" | "deductiveFacts" | "conversationHistory">; + ChatContext: ClassViewer<'ChatContext', "date" | "sessionSummaries" | "userName" | "userFacts" | "deductiveFacts" | "conversationHistory" | "crossPeerPerspectives">; ChatConversationMessage: ClassViewer<'ChatConversationMessage', "role" | "content" | "createdAt">; @@ -41,19 +41,21 @@ export default class TypeBuilder { ConversationMessage: ClassViewer<'ConversationMessage', "direction" | "content">; + CrossPeerPerspective: ClassViewer<'CrossPeerPerspective', "label" | "perspective">; + CurrentLite: ClassViewer<'CurrentLite', "temperature" | "feels_like" | "conditions" | "humidity" | "pressure" | "wind_speed" | "wind_direction" | "visibility" | "uv_index" | "clouds">; DailyForecastItem: ClassViewer<'DailyForecastItem', "date" | "summary" | "temperature" | "conditions" | "precipitation_probability" | "rain">; DailySummaryResult: ClassViewer<'DailySummaryResult', "summary">; - EmailContext: ClassViewer<'EmailContext', "originalSubject" | "sessionSummary" | "sessionTopics" | "peerCard" | "conversationHistory">; + EmailContext: ClassViewer<'EmailContext', "originalSubject" | "sessionSummary" | "sessionTopics" | "peerCard" | "conversationHistory" | "crossPeerPerspectives">; EmailInterpretation: ClassViewer<'EmailInterpretation', "response" | "extractedFacts" | "newTopics" | "shouldUpdateSummary" | "summaryAddition">; EnhancedQuery: ClassViewer<'EnhancedQuery', "original" | "enhanced">; - FollowupChatContext: ClassViewer<'FollowupChatContext', "topic" | "summary" | "sourceMessages" | "conversationHistory" | "memory" | "searchResults">; + FollowupChatContext: ClassViewer<'FollowupChatContext', "topic" | "summary" | "sourceMessages" | "conversationHistory" | "memory" | "searchResults" | "crossPeerPerspectives">; FollowupChatResponse: ClassViewer<'FollowupChatResponse', "response" | "extractedFacts">; @@ -108,14 +110,16 @@ export default class TypeBuilder { Router: EnumViewer<'Router', "WEATHER" | "WEB_SEARCH" | "MAPS" | "KNOWLEDGE" | "MEMORY_CAPTURE" | "MEMORY_RECALL" | "NOTE_THIS" | "FOLLOW_UP" | "PASSTHROUGH">; + SensitivityCategory: EnumViewer<'SensitivityCategory', "SAFE" | "SENSITIVE">; + constructor() { this.tb = new _TypeBuilder({ classes: new Set([ - "AlertLite","AnswerLines","ChatContext","ChatConversationMessage","ChatInterpretation","ChatSessionSummary","ConversationMessage","CurrentLite","DailyForecastItem","DailySummaryResult","EmailContext","EmailInterpretation","EnhancedQuery","FollowupChatContext","FollowupChatResponse","FollowupConversationMessage","FollowupMemoryContext","FollowupSearchResult","FollowupTopic","FormattedWeather","HintEligibility","HintResult","LocationLite","MemoryContext","MemoryCore","MemorySynthesisLines","NewsItem","NoteContent","PlaceLines","PlaceSuggestion","QueryResult","QuestionAnalysisResponse","RoutingBehavior","SessionInput","SessionSummaryOutput","TempBlock","WeatherConditionLite","WeatherLines", + "AlertLite","AnswerLines","ChatContext","ChatConversationMessage","ChatInterpretation","ChatSessionSummary","ConversationMessage","CrossPeerPerspective","CurrentLite","DailyForecastItem","DailySummaryResult","EmailContext","EmailInterpretation","EnhancedQuery","FollowupChatContext","FollowupChatResponse","FollowupConversationMessage","FollowupMemoryContext","FollowupSearchResult","FollowupTopic","FormattedWeather","HintEligibility","HintResult","LocationLite","MemoryContext","MemoryCore","MemorySynthesisLines","NewsItem","NoteContent","PlaceLines","PlaceSuggestion","QueryResult","QuestionAnalysisResponse","RoutingBehavior","SessionInput","SessionSummaryOutput","TempBlock","WeatherConditionLite","WeatherLines", ]), enums: new Set([ - "HintCategory","Router", + "HintCategory","Router","SensitivityCategory", ]), runtime: DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME }); @@ -129,7 +133,7 @@ export default class TypeBuilder { ]); this.ChatContext = this.tb.classViewer("ChatContext", [ - "date","sessionSummaries","userName","userFacts","deductiveFacts","conversationHistory", + "date","sessionSummaries","userName","userFacts","deductiveFacts","conversationHistory","crossPeerPerspectives", ]); this.ChatConversationMessage = this.tb.classViewer("ChatConversationMessage", [ @@ -148,6 +152,10 @@ export default class TypeBuilder { "direction","content", ]); + this.CrossPeerPerspective = this.tb.classViewer("CrossPeerPerspective", [ + "label","perspective", + ]); + this.CurrentLite = this.tb.classViewer("CurrentLite", [ "temperature","feels_like","conditions","humidity","pressure","wind_speed","wind_direction","visibility","uv_index","clouds", ]); @@ -161,7 +169,7 @@ export default class TypeBuilder { ]); this.EmailContext = this.tb.classViewer("EmailContext", [ - "originalSubject","sessionSummary","sessionTopics","peerCard","conversationHistory", + "originalSubject","sessionSummary","sessionTopics","peerCard","conversationHistory","crossPeerPerspectives", ]); this.EmailInterpretation = this.tb.classViewer("EmailInterpretation", [ @@ -173,7 +181,7 @@ export default class TypeBuilder { ]); this.FollowupChatContext = this.tb.classViewer("FollowupChatContext", [ - "topic","summary","sourceMessages","conversationHistory","memory","searchResults", + "topic","summary","sourceMessages","conversationHistory","memory","searchResults","crossPeerPerspectives", ]); this.FollowupChatResponse = this.tb.classViewer("FollowupChatResponse", [ @@ -281,6 +289,10 @@ export default class TypeBuilder { "WEATHER","WEB_SEARCH","MAPS","KNOWLEDGE","MEMORY_CAPTURE","MEMORY_RECALL","NOTE_THIS","FOLLOW_UP","PASSTHROUGH", ]); + this.SensitivityCategory = this.tb.enumViewer("SensitivityCategory", [ + "SAFE","SENSITIVE", + ]); + } reset(): void { diff --git a/packages/baml_client/baml_client/types.ts b/packages/baml_client/baml_client/types.ts index 43b3cd4..c0db585 100644 --- a/packages/baml_client/baml_client/types.ts +++ b/packages/baml_client/baml_client/types.ts @@ -64,6 +64,11 @@ export enum Router { PASSTHROUGH = "PASSTHROUGH", } +export enum SensitivityCategory { + SAFE = "SAFE", + SENSITIVE = "SENSITIVE", +} + export interface AlertLite { sender_name: string event: string @@ -86,6 +91,7 @@ export interface ChatContext { userFacts: string[] deductiveFacts: string[] conversationHistory: ChatConversationMessage[] + crossPeerPerspectives: CrossPeerPerspective[] } @@ -119,6 +125,12 @@ export interface ConversationMessage { } +export interface CrossPeerPerspective { + label: string + perspective: string + +} + export interface CurrentLite { temperature: number feels_like: number @@ -154,6 +166,7 @@ export interface EmailContext { sessionTopics: string[] peerCard: string[] conversationHistory: ConversationMessage[] + crossPeerPerspectives: CrossPeerPerspective[] } @@ -179,6 +192,7 @@ export interface FollowupChatContext { conversationHistory: FollowupConversationMessage[] memory?: FollowupMemoryContext | null searchResults: FollowupSearchResult[] + crossPeerPerspectives: CrossPeerPerspective[] } @@ -252,12 +266,6 @@ export interface MemoryContext { } -export interface CrossPeerPerspective { - label: string - perspective: string - -} - export interface MemoryCore { userName?: string | null userFacts: string[] diff --git a/packages/convex/_generated/api.d.ts b/packages/convex/_generated/api.d.ts index 3c5d346..3d576f3 100644 --- a/packages/convex/_generated/api.d.ts +++ b/packages/convex/_generated/api.d.ts @@ -12,6 +12,7 @@ import type * as analytics from "../analytics.js"; import type * as bamlActions from "../bamlActions.js"; import type * as chat from "../chat.js"; import type * as chatQueries from "../chatQueries.js"; +import type * as connections from "../connections.js"; import type * as conversationLogs from "../conversationLogs.js"; import type * as cronManagement from "../cronManagement.js"; import type * as dailySummaries from "../dailySummaries.js"; @@ -43,45 +44,46 @@ import type * as tavilySearch from "../tavilySearch.js"; import type * as users from "../users.js"; import type { - ApiFromModules, - FilterApi, - FunctionReference, + ApiFromModules, + FilterApi, + FunctionReference, } from "convex/server"; declare const fullApi: ApiFromModules<{ - analytics: typeof analytics; - bamlActions: typeof bamlActions; - chat: typeof chat; - chatQueries: typeof chatQueries; - conversationLogs: typeof conversationLogs; - cronManagement: typeof cronManagement; - dailySummaries: typeof dailySummaries; - dailySynthesis: typeof dailySynthesis; - displayQueue: typeof displayQueue; - emailEntitlements: typeof emailEntitlements; - emailEntitlementsNode: typeof emailEntitlementsNode; - emailEvents: typeof emailEvents; - emailNotes: typeof emailNotes; - emailReply: typeof emailReply; - emailThreadMessages: typeof emailThreadMessages; - "emails/EmailThreadPaywall": typeof emails_EmailThreadPaywall; - "emails/OptOutCheckout": typeof emails_OptOutCheckout; - "emails/SessionNote": typeof emails_SessionNote; - followups: typeof followups; - followupsChat: typeof followupsChat; - followupsChatQueries: typeof followupsChatQueries; - honcho: typeof honcho; - honchoSessions: typeof honchoSessions; - http: typeof http; - inboundEmail: typeof inboundEmail; - init: typeof init; - notes: typeof notes; - optOut: typeof optOut; - payments: typeof payments; - resendClient: typeof resendClient; - sessionSummaries: typeof sessionSummaries; - tavilySearch: typeof tavilySearch; - users: typeof users; + analytics: typeof analytics; + bamlActions: typeof bamlActions; + chat: typeof chat; + chatQueries: typeof chatQueries; + connections: typeof connections; + conversationLogs: typeof conversationLogs; + cronManagement: typeof cronManagement; + dailySummaries: typeof dailySummaries; + dailySynthesis: typeof dailySynthesis; + displayQueue: typeof displayQueue; + emailEntitlements: typeof emailEntitlements; + emailEntitlementsNode: typeof emailEntitlementsNode; + emailEvents: typeof emailEvents; + emailNotes: typeof emailNotes; + emailReply: typeof emailReply; + emailThreadMessages: typeof emailThreadMessages; + "emails/EmailThreadPaywall": typeof emails_EmailThreadPaywall; + "emails/OptOutCheckout": typeof emails_OptOutCheckout; + "emails/SessionNote": typeof emails_SessionNote; + followups: typeof followups; + followupsChat: typeof followupsChat; + followupsChatQueries: typeof followupsChatQueries; + honcho: typeof honcho; + honchoSessions: typeof honchoSessions; + http: typeof http; + inboundEmail: typeof inboundEmail; + init: typeof init; + notes: typeof notes; + optOut: typeof optOut; + payments: typeof payments; + resendClient: typeof resendClient; + sessionSummaries: typeof sessionSummaries; + tavilySearch: typeof tavilySearch; + users: typeof users; }>; /** @@ -93,8 +95,8 @@ declare const fullApi: ApiFromModules<{ * ``` */ export declare const api: FilterApi< - typeof fullApi, - FunctionReference + typeof fullApi, + FunctionReference >; /** @@ -106,752 +108,752 @@ export declare const api: FilterApi< * ``` */ export declare const internal: FilterApi< - typeof fullApi, - FunctionReference + typeof fullApi, + FunctionReference >; export declare const components: { - polar: { - lib: { - createProduct: FunctionReference< - "mutation", - "internal", - { - product: { - createdAt: string; - description: string | null; - id: string; - isArchived: boolean; - isRecurring: boolean; - medias: Array<{ - checksumEtag: string | null; - checksumSha256Base64: string | null; - checksumSha256Hex: string | null; - createdAt: string; - id: string; - isUploaded: boolean; - lastModifiedAt: string | null; - mimeType: string; - name: string; - organizationId: string; - path: string; - publicUrl: string; - service?: string; - size: number; - sizeReadable: string; - storageVersion: string | null; - version: string | null; - }>; - metadata?: Record; - modifiedAt: string | null; - name: string; - organizationId: string; - prices: Array<{ - amountType?: string; - createdAt: string; - id: string; - isArchived: boolean; - maximumAmount?: number | null; - minimumAmount?: number | null; - modifiedAt: string | null; - presetAmount?: number | null; - priceAmount?: number; - priceCurrency?: string; - productId: string; - recurringInterval?: "day" | "week" | "month" | "year" | null; - type?: string; - }>; - recurringInterval?: "day" | "week" | "month" | "year" | null; - }; - }, - any - >; - createSubscription: FunctionReference< - "mutation", - "internal", - { - subscription: { - amount: number | null; - cancelAtPeriodEnd: boolean; - checkoutId: string | null; - createdAt: string; - currency: string | null; - currentPeriodEnd: string | null; - currentPeriodStart: string; - customerCancellationComment?: string | null; - customerCancellationReason?: string | null; - customerId: string; - endedAt: string | null; - id: string; - metadata: Record; - modifiedAt: string | null; - priceId?: string; - productId: string; - recurringInterval: "day" | "week" | "month" | "year" | null; - startedAt: string | null; - status: string; - }; - }, - any - >; - getCurrentSubscription: FunctionReference< - "query", - "internal", - { userId: string }, - { - amount: number | null; - cancelAtPeriodEnd: boolean; - checkoutId: string | null; - createdAt: string; - currency: string | null; - currentPeriodEnd: string | null; - currentPeriodStart: string; - customerCancellationComment?: string | null; - customerCancellationReason?: string | null; - customerId: string; - endedAt: string | null; - id: string; - metadata: Record; - modifiedAt: string | null; - priceId?: string; - product: { - createdAt: string; - description: string | null; - id: string; - isArchived: boolean; - isRecurring: boolean; - medias: Array<{ - checksumEtag: string | null; - checksumSha256Base64: string | null; - checksumSha256Hex: string | null; - createdAt: string; - id: string; - isUploaded: boolean; - lastModifiedAt: string | null; - mimeType: string; - name: string; - organizationId: string; - path: string; - publicUrl: string; - service?: string; - size: number; - sizeReadable: string; - storageVersion: string | null; - version: string | null; - }>; - metadata?: Record; - modifiedAt: string | null; - name: string; - organizationId: string; - prices: Array<{ - amountType?: string; - createdAt: string; - id: string; - isArchived: boolean; - maximumAmount?: number | null; - minimumAmount?: number | null; - modifiedAt: string | null; - presetAmount?: number | null; - priceAmount?: number; - priceCurrency?: string; - productId: string; - recurringInterval?: "day" | "week" | "month" | "year" | null; - type?: string; - }>; - recurringInterval?: "day" | "week" | "month" | "year" | null; - }; - productId: string; - recurringInterval: "day" | "week" | "month" | "year" | null; - startedAt: string | null; - status: string; - } | null - >; - getCustomerByUserId: FunctionReference< - "query", - "internal", - { userId: string }, - { id: string; metadata?: Record; userId: string } | null - >; - getProduct: FunctionReference< - "query", - "internal", - { id: string }, - { - createdAt: string; - description: string | null; - id: string; - isArchived: boolean; - isRecurring: boolean; - medias: Array<{ - checksumEtag: string | null; - checksumSha256Base64: string | null; - checksumSha256Hex: string | null; - createdAt: string; - id: string; - isUploaded: boolean; - lastModifiedAt: string | null; - mimeType: string; - name: string; - organizationId: string; - path: string; - publicUrl: string; - service?: string; - size: number; - sizeReadable: string; - storageVersion: string | null; - version: string | null; - }>; - metadata?: Record; - modifiedAt: string | null; - name: string; - organizationId: string; - prices: Array<{ - amountType?: string; - createdAt: string; - id: string; - isArchived: boolean; - maximumAmount?: number | null; - minimumAmount?: number | null; - modifiedAt: string | null; - presetAmount?: number | null; - priceAmount?: number; - priceCurrency?: string; - productId: string; - recurringInterval?: "day" | "week" | "month" | "year" | null; - type?: string; - }>; - recurringInterval?: "day" | "week" | "month" | "year" | null; - } | null - >; - getSubscription: FunctionReference< - "query", - "internal", - { id: string }, - { - amount: number | null; - cancelAtPeriodEnd: boolean; - checkoutId: string | null; - createdAt: string; - currency: string | null; - currentPeriodEnd: string | null; - currentPeriodStart: string; - customerCancellationComment?: string | null; - customerCancellationReason?: string | null; - customerId: string; - endedAt: string | null; - id: string; - metadata: Record; - modifiedAt: string | null; - priceId?: string; - productId: string; - recurringInterval: "day" | "week" | "month" | "year" | null; - startedAt: string | null; - status: string; - } | null - >; - insertCustomer: FunctionReference< - "mutation", - "internal", - { id: string; metadata?: Record; userId: string }, - string - >; - listCustomerSubscriptions: FunctionReference< - "query", - "internal", - { customerId: string }, - Array<{ - amount: number | null; - cancelAtPeriodEnd: boolean; - checkoutId: string | null; - createdAt: string; - currency: string | null; - currentPeriodEnd: string | null; - currentPeriodStart: string; - customerCancellationComment?: string | null; - customerCancellationReason?: string | null; - customerId: string; - endedAt: string | null; - id: string; - metadata: Record; - modifiedAt: string | null; - priceId?: string; - productId: string; - recurringInterval: "day" | "week" | "month" | "year" | null; - startedAt: string | null; - status: string; - }> - >; - listProducts: FunctionReference< - "query", - "internal", - { includeArchived?: boolean }, - Array<{ - createdAt: string; - description: string | null; - id: string; - isArchived: boolean; - isRecurring: boolean; - medias: Array<{ - checksumEtag: string | null; - checksumSha256Base64: string | null; - checksumSha256Hex: string | null; - createdAt: string; - id: string; - isUploaded: boolean; - lastModifiedAt: string | null; - mimeType: string; - name: string; - organizationId: string; - path: string; - publicUrl: string; - service?: string; - size: number; - sizeReadable: string; - storageVersion: string | null; - version: string | null; - }>; - metadata?: Record; - modifiedAt: string | null; - name: string; - organizationId: string; - priceAmount?: number; - prices: Array<{ - amountType?: string; - createdAt: string; - id: string; - isArchived: boolean; - maximumAmount?: number | null; - minimumAmount?: number | null; - modifiedAt: string | null; - presetAmount?: number | null; - priceAmount?: number; - priceCurrency?: string; - productId: string; - recurringInterval?: "day" | "week" | "month" | "year" | null; - type?: string; - }>; - recurringInterval?: "day" | "week" | "month" | "year" | null; - }> - >; - listUserSubscriptions: FunctionReference< - "query", - "internal", - { userId: string }, - Array<{ - amount: number | null; - cancelAtPeriodEnd: boolean; - checkoutId: string | null; - createdAt: string; - currency: string | null; - currentPeriodEnd: string | null; - currentPeriodStart: string; - customerCancellationComment?: string | null; - customerCancellationReason?: string | null; - customerId: string; - endedAt: string | null; - id: string; - metadata: Record; - modifiedAt: string | null; - priceId?: string; - product: { - createdAt: string; - description: string | null; - id: string; - isArchived: boolean; - isRecurring: boolean; - medias: Array<{ - checksumEtag: string | null; - checksumSha256Base64: string | null; - checksumSha256Hex: string | null; - createdAt: string; - id: string; - isUploaded: boolean; - lastModifiedAt: string | null; - mimeType: string; - name: string; - organizationId: string; - path: string; - publicUrl: string; - service?: string; - size: number; - sizeReadable: string; - storageVersion: string | null; - version: string | null; - }>; - metadata?: Record; - modifiedAt: string | null; - name: string; - organizationId: string; - prices: Array<{ - amountType?: string; - createdAt: string; - id: string; - isArchived: boolean; - maximumAmount?: number | null; - minimumAmount?: number | null; - modifiedAt: string | null; - presetAmount?: number | null; - priceAmount?: number; - priceCurrency?: string; - productId: string; - recurringInterval?: "day" | "week" | "month" | "year" | null; - type?: string; - }>; - recurringInterval?: "day" | "week" | "month" | "year" | null; - } | null; - productId: string; - recurringInterval: "day" | "week" | "month" | "year" | null; - startedAt: string | null; - status: string; - }> - >; - syncProducts: FunctionReference< - "action", - "internal", - { polarAccessToken: string; server: "sandbox" | "production" }, - any - >; - updateProduct: FunctionReference< - "mutation", - "internal", - { - product: { - createdAt: string; - description: string | null; - id: string; - isArchived: boolean; - isRecurring: boolean; - medias: Array<{ - checksumEtag: string | null; - checksumSha256Base64: string | null; - checksumSha256Hex: string | null; - createdAt: string; - id: string; - isUploaded: boolean; - lastModifiedAt: string | null; - mimeType: string; - name: string; - organizationId: string; - path: string; - publicUrl: string; - service?: string; - size: number; - sizeReadable: string; - storageVersion: string | null; - version: string | null; - }>; - metadata?: Record; - modifiedAt: string | null; - name: string; - organizationId: string; - prices: Array<{ - amountType?: string; - createdAt: string; - id: string; - isArchived: boolean; - maximumAmount?: number | null; - minimumAmount?: number | null; - modifiedAt: string | null; - presetAmount?: number | null; - priceAmount?: number; - priceCurrency?: string; - productId: string; - recurringInterval?: "day" | "week" | "month" | "year" | null; - type?: string; - }>; - recurringInterval?: "day" | "week" | "month" | "year" | null; - }; - }, - any - >; - updateProducts: FunctionReference< - "mutation", - "internal", - { - polarAccessToken: string; - products: Array<{ - createdAt: string; - description: string | null; - id: string; - isArchived: boolean; - isRecurring: boolean; - medias: Array<{ - checksumEtag: string | null; - checksumSha256Base64: string | null; - checksumSha256Hex: string | null; - createdAt: string; - id: string; - isUploaded: boolean; - lastModifiedAt: string | null; - mimeType: string; - name: string; - organizationId: string; - path: string; - publicUrl: string; - service?: string; - size: number; - sizeReadable: string; - storageVersion: string | null; - version: string | null; - }>; - metadata?: Record; - modifiedAt: string | null; - name: string; - organizationId: string; - prices: Array<{ - amountType?: string; - createdAt: string; - id: string; - isArchived: boolean; - maximumAmount?: number | null; - minimumAmount?: number | null; - modifiedAt: string | null; - presetAmount?: number | null; - priceAmount?: number; - priceCurrency?: string; - productId: string; - recurringInterval?: "day" | "week" | "month" | "year" | null; - type?: string; - }>; - recurringInterval?: "day" | "week" | "month" | "year" | null; - }>; - }, - any - >; - updateSubscription: FunctionReference< - "mutation", - "internal", - { - subscription: { - amount: number | null; - cancelAtPeriodEnd: boolean; - checkoutId: string | null; - createdAt: string; - currency: string | null; - currentPeriodEnd: string | null; - currentPeriodStart: string; - customerCancellationComment?: string | null; - customerCancellationReason?: string | null; - customerId: string; - endedAt: string | null; - id: string; - metadata: Record; - modifiedAt: string | null; - priceId?: string; - productId: string; - recurringInterval: "day" | "week" | "month" | "year" | null; - startedAt: string | null; - status: string; - }; - }, - any - >; - upsertCustomer: FunctionReference< - "mutation", - "internal", - { id: string; metadata?: Record; userId: string }, - string - >; - }; - }; - crons: { - public: { - del: FunctionReference< - "mutation", - "internal", - { identifier: { id: string } | { name: string } }, - null - >; - get: FunctionReference< - "query", - "internal", - { identifier: { id: string } | { name: string } }, - { - args: Record; - functionHandle: string; - id: string; - name?: string; - schedule: - | { kind: "interval"; ms: number } - | { cronspec: string; kind: "cron"; tz?: string }; - } | null - >; - list: FunctionReference< - "query", - "internal", - {}, - Array<{ - args: Record; - functionHandle: string; - id: string; - name?: string; - schedule: - | { kind: "interval"; ms: number } - | { cronspec: string; kind: "cron"; tz?: string }; - }> - >; - register: FunctionReference< - "mutation", - "internal", - { - args: Record; - functionHandle: string; - name?: string; - schedule: - | { kind: "interval"; ms: number } - | { cronspec: string; kind: "cron"; tz?: string }; - }, - string - >; - }; - }; - resend: { - lib: { - cancelEmail: FunctionReference< - "mutation", - "internal", - { emailId: string }, - null - >; - cleanupAbandonedEmails: FunctionReference< - "mutation", - "internal", - { olderThan?: number }, - null - >; - cleanupOldEmails: FunctionReference< - "mutation", - "internal", - { olderThan?: number }, - null - >; - createManualEmail: FunctionReference< - "mutation", - "internal", - { - from: string; - headers?: Array<{ name: string; value: string }>; - replyTo?: Array; - subject: string; - to: Array | string; - }, - string - >; - get: FunctionReference< - "query", - "internal", - { emailId: string }, - { - bcc?: Array; - bounced?: boolean; - cc?: Array; - clicked?: boolean; - complained: boolean; - createdAt: number; - deliveryDelayed?: boolean; - errorMessage?: string; - failed?: boolean; - finalizedAt: number; - from: string; - headers?: Array<{ name: string; value: string }>; - html?: string; - opened: boolean; - replyTo: Array; - resendId?: string; - segment: number; - status: - | "waiting" - | "queued" - | "cancelled" - | "sent" - | "delivered" - | "delivery_delayed" - | "bounced" - | "failed"; - subject?: string; - template?: { - id: string; - variables?: Record; - }; - text?: string; - to: Array; - } | null - >; - getStatus: FunctionReference< - "query", - "internal", - { emailId: string }, - { - bounced: boolean; - clicked: boolean; - complained: boolean; - deliveryDelayed: boolean; - errorMessage: string | null; - failed: boolean; - opened: boolean; - status: - | "waiting" - | "queued" - | "cancelled" - | "sent" - | "delivered" - | "delivery_delayed" - | "bounced" - | "failed"; - } | null - >; - handleEmailEvent: FunctionReference< - "mutation", - "internal", - { event: any }, - null - >; - sendEmail: FunctionReference< - "mutation", - "internal", - { - bcc?: Array; - cc?: Array; - from: string; - headers?: Array<{ name: string; value: string }>; - html?: string; - options: { - apiKey: string; - initialBackoffMs: number; - onEmailEvent?: { fnHandle: string }; - retryAttempts: number; - testMode: boolean; - }; - replyTo?: Array; - subject?: string; - template?: { - id: string; - variables?: Record; - }; - text?: string; - to: Array; - }, - string - >; - updateManualEmail: FunctionReference< - "mutation", - "internal", - { - emailId: string; - errorMessage?: string; - resendId?: string; - status: - | "waiting" - | "queued" - | "cancelled" - | "sent" - | "delivered" - | "delivery_delayed" - | "bounced" - | "failed"; - }, - null - >; - }; - }; + polar: { + lib: { + createProduct: FunctionReference< + "mutation", + "internal", + { + product: { + createdAt: string; + description: string | null; + id: string; + isArchived: boolean; + isRecurring: boolean; + medias: Array<{ + checksumEtag: string | null; + checksumSha256Base64: string | null; + checksumSha256Hex: string | null; + createdAt: string; + id: string; + isUploaded: boolean; + lastModifiedAt: string | null; + mimeType: string; + name: string; + organizationId: string; + path: string; + publicUrl: string; + service?: string; + size: number; + sizeReadable: string; + storageVersion: string | null; + version: string | null; + }>; + metadata?: Record; + modifiedAt: string | null; + name: string; + organizationId: string; + prices: Array<{ + amountType?: string; + createdAt: string; + id: string; + isArchived: boolean; + maximumAmount?: number | null; + minimumAmount?: number | null; + modifiedAt: string | null; + presetAmount?: number | null; + priceAmount?: number; + priceCurrency?: string; + productId: string; + recurringInterval?: "day" | "week" | "month" | "year" | null; + type?: string; + }>; + recurringInterval?: "day" | "week" | "month" | "year" | null; + }; + }, + any + >; + createSubscription: FunctionReference< + "mutation", + "internal", + { + subscription: { + amount: number | null; + cancelAtPeriodEnd: boolean; + checkoutId: string | null; + createdAt: string; + currency: string | null; + currentPeriodEnd: string | null; + currentPeriodStart: string; + customerCancellationComment?: string | null; + customerCancellationReason?: string | null; + customerId: string; + endedAt: string | null; + id: string; + metadata: Record; + modifiedAt: string | null; + priceId?: string; + productId: string; + recurringInterval: "day" | "week" | "month" | "year" | null; + startedAt: string | null; + status: string; + }; + }, + any + >; + getCurrentSubscription: FunctionReference< + "query", + "internal", + { userId: string }, + { + amount: number | null; + cancelAtPeriodEnd: boolean; + checkoutId: string | null; + createdAt: string; + currency: string | null; + currentPeriodEnd: string | null; + currentPeriodStart: string; + customerCancellationComment?: string | null; + customerCancellationReason?: string | null; + customerId: string; + endedAt: string | null; + id: string; + metadata: Record; + modifiedAt: string | null; + priceId?: string; + product: { + createdAt: string; + description: string | null; + id: string; + isArchived: boolean; + isRecurring: boolean; + medias: Array<{ + checksumEtag: string | null; + checksumSha256Base64: string | null; + checksumSha256Hex: string | null; + createdAt: string; + id: string; + isUploaded: boolean; + lastModifiedAt: string | null; + mimeType: string; + name: string; + organizationId: string; + path: string; + publicUrl: string; + service?: string; + size: number; + sizeReadable: string; + storageVersion: string | null; + version: string | null; + }>; + metadata?: Record; + modifiedAt: string | null; + name: string; + organizationId: string; + prices: Array<{ + amountType?: string; + createdAt: string; + id: string; + isArchived: boolean; + maximumAmount?: number | null; + minimumAmount?: number | null; + modifiedAt: string | null; + presetAmount?: number | null; + priceAmount?: number; + priceCurrency?: string; + productId: string; + recurringInterval?: "day" | "week" | "month" | "year" | null; + type?: string; + }>; + recurringInterval?: "day" | "week" | "month" | "year" | null; + }; + productId: string; + recurringInterval: "day" | "week" | "month" | "year" | null; + startedAt: string | null; + status: string; + } | null + >; + getCustomerByUserId: FunctionReference< + "query", + "internal", + { userId: string }, + { id: string; metadata?: Record; userId: string } | null + >; + getProduct: FunctionReference< + "query", + "internal", + { id: string }, + { + createdAt: string; + description: string | null; + id: string; + isArchived: boolean; + isRecurring: boolean; + medias: Array<{ + checksumEtag: string | null; + checksumSha256Base64: string | null; + checksumSha256Hex: string | null; + createdAt: string; + id: string; + isUploaded: boolean; + lastModifiedAt: string | null; + mimeType: string; + name: string; + organizationId: string; + path: string; + publicUrl: string; + service?: string; + size: number; + sizeReadable: string; + storageVersion: string | null; + version: string | null; + }>; + metadata?: Record; + modifiedAt: string | null; + name: string; + organizationId: string; + prices: Array<{ + amountType?: string; + createdAt: string; + id: string; + isArchived: boolean; + maximumAmount?: number | null; + minimumAmount?: number | null; + modifiedAt: string | null; + presetAmount?: number | null; + priceAmount?: number; + priceCurrency?: string; + productId: string; + recurringInterval?: "day" | "week" | "month" | "year" | null; + type?: string; + }>; + recurringInterval?: "day" | "week" | "month" | "year" | null; + } | null + >; + getSubscription: FunctionReference< + "query", + "internal", + { id: string }, + { + amount: number | null; + cancelAtPeriodEnd: boolean; + checkoutId: string | null; + createdAt: string; + currency: string | null; + currentPeriodEnd: string | null; + currentPeriodStart: string; + customerCancellationComment?: string | null; + customerCancellationReason?: string | null; + customerId: string; + endedAt: string | null; + id: string; + metadata: Record; + modifiedAt: string | null; + priceId?: string; + productId: string; + recurringInterval: "day" | "week" | "month" | "year" | null; + startedAt: string | null; + status: string; + } | null + >; + insertCustomer: FunctionReference< + "mutation", + "internal", + { id: string; metadata?: Record; userId: string }, + string + >; + listCustomerSubscriptions: FunctionReference< + "query", + "internal", + { customerId: string }, + Array<{ + amount: number | null; + cancelAtPeriodEnd: boolean; + checkoutId: string | null; + createdAt: string; + currency: string | null; + currentPeriodEnd: string | null; + currentPeriodStart: string; + customerCancellationComment?: string | null; + customerCancellationReason?: string | null; + customerId: string; + endedAt: string | null; + id: string; + metadata: Record; + modifiedAt: string | null; + priceId?: string; + productId: string; + recurringInterval: "day" | "week" | "month" | "year" | null; + startedAt: string | null; + status: string; + }> + >; + listProducts: FunctionReference< + "query", + "internal", + { includeArchived?: boolean }, + Array<{ + createdAt: string; + description: string | null; + id: string; + isArchived: boolean; + isRecurring: boolean; + medias: Array<{ + checksumEtag: string | null; + checksumSha256Base64: string | null; + checksumSha256Hex: string | null; + createdAt: string; + id: string; + isUploaded: boolean; + lastModifiedAt: string | null; + mimeType: string; + name: string; + organizationId: string; + path: string; + publicUrl: string; + service?: string; + size: number; + sizeReadable: string; + storageVersion: string | null; + version: string | null; + }>; + metadata?: Record; + modifiedAt: string | null; + name: string; + organizationId: string; + priceAmount?: number; + prices: Array<{ + amountType?: string; + createdAt: string; + id: string; + isArchived: boolean; + maximumAmount?: number | null; + minimumAmount?: number | null; + modifiedAt: string | null; + presetAmount?: number | null; + priceAmount?: number; + priceCurrency?: string; + productId: string; + recurringInterval?: "day" | "week" | "month" | "year" | null; + type?: string; + }>; + recurringInterval?: "day" | "week" | "month" | "year" | null; + }> + >; + listUserSubscriptions: FunctionReference< + "query", + "internal", + { userId: string }, + Array<{ + amount: number | null; + cancelAtPeriodEnd: boolean; + checkoutId: string | null; + createdAt: string; + currency: string | null; + currentPeriodEnd: string | null; + currentPeriodStart: string; + customerCancellationComment?: string | null; + customerCancellationReason?: string | null; + customerId: string; + endedAt: string | null; + id: string; + metadata: Record; + modifiedAt: string | null; + priceId?: string; + product: { + createdAt: string; + description: string | null; + id: string; + isArchived: boolean; + isRecurring: boolean; + medias: Array<{ + checksumEtag: string | null; + checksumSha256Base64: string | null; + checksumSha256Hex: string | null; + createdAt: string; + id: string; + isUploaded: boolean; + lastModifiedAt: string | null; + mimeType: string; + name: string; + organizationId: string; + path: string; + publicUrl: string; + service?: string; + size: number; + sizeReadable: string; + storageVersion: string | null; + version: string | null; + }>; + metadata?: Record; + modifiedAt: string | null; + name: string; + organizationId: string; + prices: Array<{ + amountType?: string; + createdAt: string; + id: string; + isArchived: boolean; + maximumAmount?: number | null; + minimumAmount?: number | null; + modifiedAt: string | null; + presetAmount?: number | null; + priceAmount?: number; + priceCurrency?: string; + productId: string; + recurringInterval?: "day" | "week" | "month" | "year" | null; + type?: string; + }>; + recurringInterval?: "day" | "week" | "month" | "year" | null; + } | null; + productId: string; + recurringInterval: "day" | "week" | "month" | "year" | null; + startedAt: string | null; + status: string; + }> + >; + syncProducts: FunctionReference< + "action", + "internal", + { polarAccessToken: string; server: "sandbox" | "production" }, + any + >; + updateProduct: FunctionReference< + "mutation", + "internal", + { + product: { + createdAt: string; + description: string | null; + id: string; + isArchived: boolean; + isRecurring: boolean; + medias: Array<{ + checksumEtag: string | null; + checksumSha256Base64: string | null; + checksumSha256Hex: string | null; + createdAt: string; + id: string; + isUploaded: boolean; + lastModifiedAt: string | null; + mimeType: string; + name: string; + organizationId: string; + path: string; + publicUrl: string; + service?: string; + size: number; + sizeReadable: string; + storageVersion: string | null; + version: string | null; + }>; + metadata?: Record; + modifiedAt: string | null; + name: string; + organizationId: string; + prices: Array<{ + amountType?: string; + createdAt: string; + id: string; + isArchived: boolean; + maximumAmount?: number | null; + minimumAmount?: number | null; + modifiedAt: string | null; + presetAmount?: number | null; + priceAmount?: number; + priceCurrency?: string; + productId: string; + recurringInterval?: "day" | "week" | "month" | "year" | null; + type?: string; + }>; + recurringInterval?: "day" | "week" | "month" | "year" | null; + }; + }, + any + >; + updateProducts: FunctionReference< + "mutation", + "internal", + { + polarAccessToken: string; + products: Array<{ + createdAt: string; + description: string | null; + id: string; + isArchived: boolean; + isRecurring: boolean; + medias: Array<{ + checksumEtag: string | null; + checksumSha256Base64: string | null; + checksumSha256Hex: string | null; + createdAt: string; + id: string; + isUploaded: boolean; + lastModifiedAt: string | null; + mimeType: string; + name: string; + organizationId: string; + path: string; + publicUrl: string; + service?: string; + size: number; + sizeReadable: string; + storageVersion: string | null; + version: string | null; + }>; + metadata?: Record; + modifiedAt: string | null; + name: string; + organizationId: string; + prices: Array<{ + amountType?: string; + createdAt: string; + id: string; + isArchived: boolean; + maximumAmount?: number | null; + minimumAmount?: number | null; + modifiedAt: string | null; + presetAmount?: number | null; + priceAmount?: number; + priceCurrency?: string; + productId: string; + recurringInterval?: "day" | "week" | "month" | "year" | null; + type?: string; + }>; + recurringInterval?: "day" | "week" | "month" | "year" | null; + }>; + }, + any + >; + updateSubscription: FunctionReference< + "mutation", + "internal", + { + subscription: { + amount: number | null; + cancelAtPeriodEnd: boolean; + checkoutId: string | null; + createdAt: string; + currency: string | null; + currentPeriodEnd: string | null; + currentPeriodStart: string; + customerCancellationComment?: string | null; + customerCancellationReason?: string | null; + customerId: string; + endedAt: string | null; + id: string; + metadata: Record; + modifiedAt: string | null; + priceId?: string; + productId: string; + recurringInterval: "day" | "week" | "month" | "year" | null; + startedAt: string | null; + status: string; + }; + }, + any + >; + upsertCustomer: FunctionReference< + "mutation", + "internal", + { id: string; metadata?: Record; userId: string }, + string + >; + }; + }; + crons: { + public: { + del: FunctionReference< + "mutation", + "internal", + { identifier: { id: string } | { name: string } }, + null + >; + get: FunctionReference< + "query", + "internal", + { identifier: { id: string } | { name: string } }, + { + args: Record; + functionHandle: string; + id: string; + name?: string; + schedule: + | { kind: "interval"; ms: number } + | { cronspec: string; kind: "cron"; tz?: string }; + } | null + >; + list: FunctionReference< + "query", + "internal", + {}, + Array<{ + args: Record; + functionHandle: string; + id: string; + name?: string; + schedule: + | { kind: "interval"; ms: number } + | { cronspec: string; kind: "cron"; tz?: string }; + }> + >; + register: FunctionReference< + "mutation", + "internal", + { + args: Record; + functionHandle: string; + name?: string; + schedule: + | { kind: "interval"; ms: number } + | { cronspec: string; kind: "cron"; tz?: string }; + }, + string + >; + }; + }; + resend: { + lib: { + cancelEmail: FunctionReference< + "mutation", + "internal", + { emailId: string }, + null + >; + cleanupAbandonedEmails: FunctionReference< + "mutation", + "internal", + { olderThan?: number }, + null + >; + cleanupOldEmails: FunctionReference< + "mutation", + "internal", + { olderThan?: number }, + null + >; + createManualEmail: FunctionReference< + "mutation", + "internal", + { + from: string; + headers?: Array<{ name: string; value: string }>; + replyTo?: Array; + subject: string; + to: Array | string; + }, + string + >; + get: FunctionReference< + "query", + "internal", + { emailId: string }, + { + bcc?: Array; + bounced?: boolean; + cc?: Array; + clicked?: boolean; + complained: boolean; + createdAt: number; + deliveryDelayed?: boolean; + errorMessage?: string; + failed?: boolean; + finalizedAt: number; + from: string; + headers?: Array<{ name: string; value: string }>; + html?: string; + opened: boolean; + replyTo: Array; + resendId?: string; + segment: number; + status: + | "waiting" + | "queued" + | "cancelled" + | "sent" + | "delivered" + | "delivery_delayed" + | "bounced" + | "failed"; + subject?: string; + template?: { + id: string; + variables?: Record; + }; + text?: string; + to: Array; + } | null + >; + getStatus: FunctionReference< + "query", + "internal", + { emailId: string }, + { + bounced: boolean; + clicked: boolean; + complained: boolean; + deliveryDelayed: boolean; + errorMessage: string | null; + failed: boolean; + opened: boolean; + status: + | "waiting" + | "queued" + | "cancelled" + | "sent" + | "delivered" + | "delivery_delayed" + | "bounced" + | "failed"; + } | null + >; + handleEmailEvent: FunctionReference< + "mutation", + "internal", + { event: any }, + null + >; + sendEmail: FunctionReference< + "mutation", + "internal", + { + bcc?: Array; + cc?: Array; + from: string; + headers?: Array<{ name: string; value: string }>; + html?: string; + options: { + apiKey: string; + initialBackoffMs: number; + onEmailEvent?: { fnHandle: string }; + retryAttempts: number; + testMode: boolean; + }; + replyTo?: Array; + subject?: string; + template?: { + id: string; + variables?: Record; + }; + text?: string; + to: Array; + }, + string + >; + updateManualEmail: FunctionReference< + "mutation", + "internal", + { + emailId: string; + errorMessage?: string; + resendId?: string; + status: + | "waiting" + | "queued" + | "cancelled" + | "sent" + | "delivered" + | "delivery_delayed" + | "bounced" + | "failed"; + }, + null + >; + }; + }; }; diff --git a/packages/convex/chat.ts b/packages/convex/chat.ts index 9855ceb..c8ef0b7 100644 --- a/packages/convex/chat.ts +++ b/packages/convex/chat.ts @@ -195,15 +195,14 @@ export const sendMessage = action({ const connectedPeer = await crossPeerClient.peer( `${conn.connectedUserId}-diatribe`, ); - const rep = await connectedPeer.representation({ - target: `${user._id}-diatribe`, + const rep = await connectedPeer.workingRep(undefined, `${user._id}-diatribe`, { searchQuery: `${date} ${content}`, searchTopK: 5, - maxConclusions: 10, + maxObservations: 10, }); return { label: conn.label ?? "Connected user", - perspective: typeof rep.representation === "string" ? rep.representation : "", + perspective: rep.isEmpty() ? "" : rep.toStringNoTimestamps(), }; } catch (error) { console.warn( diff --git a/packages/convex/emailReply.ts b/packages/convex/emailReply.ts index 65bc57f..eb1566b 100644 --- a/packages/convex/emailReply.ts +++ b/packages/convex/emailReply.ts @@ -200,15 +200,14 @@ export const processEmailReply = internalAction({ const connectedPeer = await crossPeerClient.peer( `${conn.connectedUserId}-diatribe`, ); - const rep = await connectedPeer.representation({ - target: `${user._id}-diatribe`, + const rep = await connectedPeer.workingRep(undefined, `${user._id}-diatribe`, { searchQuery: emailNote.subject, searchTopK: 5, - maxConclusions: 10, + maxObservations: 10, }); return { label: conn.label ?? "Connected user", - perspective: typeof rep.representation === "string" ? rep.representation : "", + perspective: rep.isEmpty() ? "" : rep.toStringNoTimestamps(), }; } catch (error) { console.warn( diff --git a/packages/convex/followupsChat.ts b/packages/convex/followupsChat.ts index 3cd6a4b..52e7b47 100644 --- a/packages/convex/followupsChat.ts +++ b/packages/convex/followupsChat.ts @@ -193,13 +193,12 @@ export const sendFollowupMessage = action({ const connectedPeer = await honchoClient.peer( `${conn.connectedUserId}-diatribe`, ); - const rep = await connectedPeer.representation({ - target: `${user._id}-diatribe`, + const rep = await connectedPeer.workingRep(undefined, `${user._id}-diatribe`, { searchQuery: followup.topic, searchTopK: 5, - maxConclusions: 10, + maxObservations: 10, }); - const perspective = typeof rep.representation === "string" ? rep.representation : ""; + const perspective = rep.isEmpty() ? "" : rep.toStringNoTimestamps(); return { label: conn.label ?? "Connected user", perspective,