Add optional display name targeting and startup message catch-up logic

This commit is contained in:
2025-10-03 13:23:01 -07:00
parent 485f6ebf47
commit 24f7f3e265

113
main.py
View File

@@ -33,6 +33,10 @@ HISTORY_FILE = Path(os.environ.get("HISTORY_FILE", "chat_history.jsonl"))
MAX_TOKENS_HISTORY = int(os.environ.get("MAX_TOKENS_HISTORY", "2200")) # rough token budget
MAX_MESSAGES_HISTORY = int(os.environ.get("MAX_MESSAGES_HISTORY", "30"))
# Optional targeting helpers
TARGET_DISPLAY_NAME = os.environ.get("TARGET_DISPLAY_NAME", "").strip()
TARGET_CACHE_FILE = Path(os.environ.get("TARGET_CACHE_FILE", "target_id.txt"))
# ---------- Validation ----------
def _require(cond: bool, msg: str):
if not cond:
@@ -109,6 +113,14 @@ def build_chat_messages_for_openai() -> List[Dict[str, str]]:
msgs.append({"role": role, "content": r["content"]})
return msgs
def last_history_record() -> Dict[str, Any] | None:
records = load_history()
return records[-1] if records else None
def last_history_role() -> str | None:
rec = last_history_record()
return rec.get("role") if rec else None
# ---------- OpenAI client ----------
oai = OpenAI(api_key=OPENAI_API_KEY)
@@ -143,15 +155,97 @@ async def resolve_target_entity(client: TelegramClient):
if TARGET_USERNAME:
try:
target = await client.get_entity(TARGET_USERNAME)
if target:
return target
except Exception:
target = None
if not target and TARGET_USER_ID:
try:
target = await client.get_entity(TARGET_USER_ID)
if target:
return target
except Exception:
target = None
# Try cached target ID from a previous run
if not target and TARGET_CACHE_FILE.exists():
try:
cached_id = int(TARGET_CACHE_FILE.read_text().strip())
target = await client.get_entity(cached_id)
if target:
return target
except Exception:
target = None
# Try resolving by display name across dialogs (case-insensitive exact match)
if not target and TARGET_DISPLAY_NAME:
try:
async for d in client.iter_dialogs():
ent = d.entity
name = getattr(d, "name", "") or ""
if name.strip().lower() == TARGET_DISPLAY_NAME.lower():
# Make sure it's a user dialog, not a group
if hasattr(ent, "bot") or getattr(ent, "is_self", False):
pass
return ent
except Exception:
target = None
return target
def cache_target_id(entity) -> None:
try:
TARGET_CACHE_FILE.write_text(str(getattr(entity, "id", "")))
except Exception as e:
print(f"Warning: failed to write target cache file: {e}")
async def startup_catchup_if_needed(client: TelegramClient, target_entity) -> bool:
"""
On startup, if the last message in the target dialog is from them (incoming)
and we haven't replied after that in our local history, generate and send a reply.
Returns True if a catch-up reply was sent.
"""
if not target_entity:
print("startup_catchup_if_needed: no target resolved")
return False
msgs = await client.get_messages(target_entity, limit=1)
if not msgs:
print("startup_catchup_if_needed: target dialog has no messages")
return False
last_msg = msgs[0]
# Only act if the last message is incoming (from them) and it has text
if last_msg.out or not (last_msg.message or "").strip():
return False
# If our last history entry is already an assistant message, we likely replied.
role = last_history_role()
if role == "assistant":
return False
# Ensure that incoming text is reflected in history before generating a response.
last_rec = last_history_record()
if not last_rec or last_rec.get("role") != "user" or last_rec.get("content") != last_msg.message:
append_history("user", last_msg.message or "")
try:
reply = await generate_reply_via_openai()
except Exception as e:
print(f"OpenAI error during startup catch-up: {e}")
reply = random.choice([
"Sorry, just saw this—could you tell me a bit more?",
"I had to step away a minute—where were we?",
"Interesting—what do you like most about that?",
])
append_history("assistant", reply)
await safe_send(client, target_entity, reply)
return True
async def human_delay():
await asyncio.sleep(random.randint(MIN_DELAY_SEC, MAX_DELAY_SEC))
@@ -184,11 +278,23 @@ async def main():
target_entity = await resolve_target_entity(client)
if target_entity:
print(f"Target resolved: id={target_entity.id}, username={getattr(target_entity, 'username', None)}")
cache_target_id(target_entity)
else:
print("Target not resolved yet. Will match dynamically on first incoming message from target.")
if TARGET_USERNAME:
print(f"Hint: couldn't resolve by username '{TARGET_USERNAME}'. Check spelling and privacy.")
if TARGET_USER_ID:
print(f"Hint: couldn't resolve by user id '{TARGET_USER_ID}'.")
if TARGET_DISPLAY_NAME:
print(f"Hint: couldn't resolve by display name '{TARGET_DISPLAY_NAME}'.")
# Optional: send a gentle opener once (only if history is empty)
if not HISTORY_FILE.exists():
# If we already have a target, attempt a startup catch-up reply if their message was last.
catchup_sent = False
if target_entity:
catchup_sent = await startup_catchup_if_needed(client, target_entity)
# Optional: send a gentle opener once (only if history is empty and we didn't just catch up)
if not HISTORY_FILE.exists() and not catchup_sent:
opener = "Oh neat, Houston. I'm up in the Pacific Northwest these days, sort of near the coast. What brought you from the UK to Houston?"
append_history("assistant", opener)
if target_entity:
@@ -204,9 +310,10 @@ async def main():
return
# If target not yet resolved, auto-resolve on first qualifying message
if (not target_entity) and (TARGET_USER_ID == 0 and not TARGET_USERNAME):
if (not target_entity) and (TARGET_USER_ID == 0 and not TARGET_USERNAME and not TARGET_DISPLAY_NAME):
# No explicit target provided; first inbound DM will become the target
target_entity = sender
cache_target_id(target_entity)
print(f"Auto-targeted sender id={sender.id}, username={sender.username}")
if not sender_matches_target(sender, target_entity):