Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/guides/email.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,16 @@ Each row carries inline action buttons:

If you haven't connected Google yet, the agent surfaces a one-click **Connect Google** button inline in the chat — no need to navigate to Settings → Connections manually.

### In-session preferences (in-memory, wiped on restart)
### Cross-session preferences (persisted via memory store)

Tell the agent how you want classification to behave for this session:
Tell the agent how you want classification to behave:

- *"Treat boss@company.com as urgent"* → calls `set_priority_sender`. That sender bypasses the heuristic and lands in **Urgent** for the rest of the session.
- *"Treat boss@company.com as urgent"* → calls `set_priority_sender`. That sender bypasses the heuristic and lands in **Urgent** across all future sessions.
- *"Treat newsletter@stripe.com as low priority"* → calls `set_low_priority_sender`. That sender lands in **Suggested archives**.
- *"Default informational mail to archive"* → calls `set_category_default("informational", "archive")`. Informational items lift into **Suggested archives** until you reset.
- *"Clear my preferences"* → calls `clear_session_preferences`.

Preferences are stored in process memory only — restarting the agent (or quitting Agent UI) wipes them. This is deliberate: the goal is to prove the value of session-scoped learning before we wire up persistent memory. Once persistent memory ships, the same tools will write through to it without changing this surface.
Preferences persist across agent restarts via the memory store (`email:preferences`). They fall back to session-only (lost on restart) when `GAIA_MEMORY_DISABLED=1` or when the embedding backend is unreachable at startup. Incognito sessions never write preferences to persistent storage.

### Behavioral learning (auto-promotion)

Expand All @@ -148,7 +148,7 @@ Senders you reply to quickly are automatically promoted to priority on the next

`list_inbox`, `get_message`, `get_thread`, `search_messages`, `list_labels`, `triage_inbox`, `pre_scan_inbox`

### Session preferences (in-memory; wiped on agent restart)
### Preferences (persisted across restarts via memory store)

`set_priority_sender`, `set_low_priority_sender`, `set_category_default`, `clear_session_preferences`

Expand Down
28 changes: 24 additions & 4 deletions hub/agents/python/code/gaia_agent_code/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ def _build_index_parser():
parser.add_argument(
"--base-url",
default=None,
help="Lemonade server URL (default: http://localhost:8000/api/v1)",
help="Lemonade server URL (default: http://localhost:13305/api/v1)",
)
parser.add_argument(
"--no-lemonade-check",
Expand All @@ -308,10 +308,30 @@ def _build_index_parser():
default=10,
help="Number of results to return (default: 10)",
)
# FIX 1: use SUPPRESS so the parent's default="." is preserved when --repo is omitted
search_p.add_argument(
"--repo",
default=argparse.SUPPRESS,
help="Path to repository root (overrides parent --repo)",
)

sub.add_parser("status", help="Show index status")
sub.add_parser("clear", help="Clear the index")
sub.add_parser("chat", help="Interactive code Q&A (CodeAgent + code_index tools)")

chat_p = sub.add_parser("chat", help="Interactive code Q&A (CodeAgent + code_index tools)")
# FIX 2: use SUPPRESS so the parent's default="." is preserved when --repo is omitted
chat_p.add_argument(
"--repo",
default=argparse.SUPPRESS,
help="Path to repository root (overrides parent --repo)",
)
# FIX 3: dest="repo" makes --path a true alias for --repo, no changes needed in cmd_index
chat_p.add_argument(
"--path",
dest="repo",
default=argparse.SUPPRESS,
help="Alias for --repo (path to repository root)",
)

return parser

Expand Down Expand Up @@ -509,7 +529,7 @@ def main():
parser.add_argument(
"--path",
"-p",
type=str,
type=str,
default=None,
help="Project directory path. Creates directory if it doesn't exist.",
)
Expand Down Expand Up @@ -606,4 +626,4 @@ def main():


if __name__ == "__main__":
sys.exit(main())
sys.exit(main())
21 changes: 21 additions & 0 deletions hub/agents/python/email/gaia_agent_email/tools/profile_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@

Bounded and idempotent by design — ``_record_interaction`` always does an
upsert into the single per-sender record, so the record count is O(senders)
<<<<<<< HEAD
regardless of how many emails arrive. #1290 can reuse ``_read_interactions``
directly to build richer views.
=======
regardless of how many emails arrive.

Reply behavior is tracked separately in reply records (entity
``email:reply:<sender>``). ``_evaluate_promotions()`` reads these to identify
senders with consistently fast replies and returns them for promotion to
priority senders. Called on-demand at triage time — no background thread.
>>>>>>> upstream/main

Tools registered:

Expand Down Expand Up @@ -52,6 +57,8 @@
_INTERACTION_DOMAIN = "email_agent_interactions"
_INTERACTION_CATEGORY = "interaction"

<<<<<<< HEAD
=======
# Stable entity prefix for per-sender REPLY behavior records.
# Entity = f"{_REPLY_ENTITY_PREFIX}{sender_email}"
_REPLY_ENTITY_PREFIX = "email:reply:"
Expand Down Expand Up @@ -79,6 +86,7 @@
# 50k senders is far beyond any realistic mailbox.
_MAX_REPLY_RECORDS: int = 50000

>>>>>>> upstream/main
# Sanity ceiling on how many distinct-sender interaction records we read in
# one profiling pass. This is NOT a silent truncation cap: if a real inbox
# ever has more distinct senders than this, ``_read_interactions`` logs a
Expand All @@ -101,13 +109,25 @@ def _now_iso() -> str:


def _dominant_category(category_counts: Dict[str, int]) -> str:
<<<<<<< HEAD
"""Return the category with the highest count; ties broken by last alphabetically."""
=======
"""Return the category with the highest count (ties: lexicographic order)."""
>>>>>>> upstream/main
if not category_counts:
return ""
return max(category_counts, key=lambda cat: (category_counts[cat], cat))


class ProfileToolsMixin:
<<<<<<< HEAD
"""Mixin that registers inbox-profiling tools.

State-free at construction time — reads ``self._memory_store`` via
a closure captured when ``_register_profile_tools()`` is called.
"""

=======
"""Mixin that registers inbox-profiling tools and behavioral-learning helpers.

State-free at construction time — reads ``self._memory_store`` via
Expand Down Expand Up @@ -236,6 +256,7 @@ def _evaluate_promotions(self) -> List[str]:
)
return qualified

>>>>>>> upstream/main
def _record_interaction(self, sender: str, category: str) -> None:
"""Update the rolling interaction record for *sender*.

Expand Down
Loading