Skip to content

Commit d48e366

Browse files
committed
Give follow-up agent timestamps + silence-aware bump/escalate rule so it stops looping on wait
1 parent 15bca31 commit d48e366

2 files changed

Lines changed: 50 additions & 7 deletions

File tree

linkedin/agents/follow_up.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import logging
10+
from datetime import datetime, timedelta
1011
from typing import Literal
1112

1213
import jinja2
@@ -55,19 +56,50 @@ def _check_required_fields(self):
5556
RECENT_MESSAGES_WINDOW = 6
5657

5758

58-
def _format_recent_messages(messages: list) -> str:
59-
"""Render the last few ChatMessage rows as a `Me:`/`Lead:` transcript."""
59+
def _humanize_age(when: datetime, now: datetime) -> str:
60+
"""Render `when` as a coarse age relative to `now` (e.g. ``3d ago``)."""
61+
delta = now - when
62+
if delta < timedelta(hours=1):
63+
return f"{max(int(delta.total_seconds() // 60), 1)}m ago"
64+
if delta < timedelta(days=1):
65+
return f"{int(delta.total_seconds() // 3600)}h ago"
66+
return f"{delta.days}d ago"
67+
68+
69+
def _format_recent_messages(messages: list, now: datetime) -> str:
70+
"""Render the last few ChatMessage rows as a timestamped transcript."""
6071
if not messages:
6172
return "No recent messages."
6273
lines = []
6374
for m in messages:
64-
speaker = "Me" if m.is_outgoing else "Lead"
6575
content = (m.content or "").strip()
66-
if content:
67-
lines.append(f"{speaker}: {content}")
76+
if not content:
77+
continue
78+
speaker = "Me" if m.is_outgoing else "Lead"
79+
prefix = f"{speaker} ({_humanize_age(m.creation_date, now)})" if m.creation_date else speaker
80+
lines.append(f"{prefix}: {content}")
6881
return "\n".join(lines) or "No recent messages."
6982

7083

84+
def _days_since_last_outgoing(messages: list, now: datetime) -> int | None:
85+
"""Whole days since the most recent outgoing message, or None if there are none."""
86+
timestamps = [m.creation_date for m in messages if m.is_outgoing and m.creation_date]
87+
if not timestamps:
88+
return None
89+
return max((now - max(timestamps)).days, 0)
90+
91+
92+
def _count_unanswered_outgoing(messages: list) -> int:
93+
"""Trailing run of outgoing messages with no lead reply after them."""
94+
count = 0
95+
for m in reversed(messages):
96+
if m.is_outgoing:
97+
count += 1
98+
else:
99+
break
100+
return count
101+
102+
71103
def _format_facts(summary: dict | None) -> str:
72104
"""Render a `{facts: [...]}` summary blob as a bullet list."""
73105
facts = (summary or {}).get("facts") or []
@@ -92,21 +124,27 @@ def _load_recent_messages(deal, limit: int = RECENT_MESSAGES_WINDOW) -> list:
92124

93125
def _render_system_prompt(session, deal, recent_messages: list) -> str:
94126
"""Render the agent system prompt from the Jinja2 template."""
127+
from django.utils import timezone
128+
95129
env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(PROMPTS_DIR)))
96130
template = env.get_template("follow_up_agent.j2")
97131

98132
campaign = deal.campaign
99133
self_prof = session.self_profile
100134
self_name = f"{self_prof.get('first_name', '')} {self_prof.get('last_name', '')}".strip() or session.django_user.username
101135

136+
now = timezone.now()
102137
return template.render(
103138
self_name=self_name,
104139
product_docs=campaign.product_docs or "",
105140
campaign_objective=campaign.campaign_objective or "",
106141
booking_link=campaign.booking_link or "",
107142
profile_summary=_format_facts(deal.profile_summary),
108143
chat_summary=_format_facts(deal.chat_summary),
109-
recent_messages=_format_recent_messages(recent_messages),
144+
recent_messages=_format_recent_messages(recent_messages, now),
145+
today=now.strftime("%Y-%m-%d"),
146+
days_since_last_outgoing=_days_since_last_outgoing(recent_messages, now),
147+
unanswered_outgoing=_count_unanswered_outgoing(recent_messages),
110148
)
111149

112150

linkedin/templates/prompts/follow_up_agent.j2

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ You are: {{ self_name }}
2121
{{ chat_summary }}
2222

2323
## Most Recent Messages (verbatim)
24+
Today is {{ today }}. Each line is tagged with how long ago it was sent.
25+
{% if days_since_last_outgoing is not none -%}
26+
You last messaged this lead {{ days_since_last_outgoing }} day(s) ago.
27+
You have sent {{ unanswered_outgoing }} message(s) in a row without a reply.
28+
{% endif -%}
2429
{{ recent_messages }}
2530

2631
## Instructions
@@ -38,5 +43,5 @@ Decide what to do next for this lead. Choose exactly one action:
3843
- Do NOT sign messages with a name or signature.
3944
- If there are no recent messages, send a first message introducing yourself and the value proposition, grounded in the profile facts.
4045
- If there are recent messages, respond contextually to the literal phrasing of the last message — match its tone and language.
41-
- If they seem uninterested or haven't replied after multiple attempts, mark as completed.
46+
- Use the timestamps to reason about silence. Rough guide: if your last message was < 5 days ago, prefer **wait**; if it's been 5-14 days with no reply, send a short, low-pressure bump that references the prior message; after 3 unanswered outgoing messages in a row, **mark_completed** as cold.
4247
- Include the booking link naturally in the message when suggesting a call or meeting, not as a separate line.

0 commit comments

Comments
 (0)