system-prompts-and-models-o.../personal-brand-engine/agents/whatsapp/responder.py
VoXc2 4bb2442313
Add Personal Brand Engine - 7 AI Agents Automation System
Complete AI-powered personal brand automation for Sami Assiri.\n\n7 agents: LinkedIn, Email, Social Media, WhatsApp, CV Optimizer, Content Strategist, Opportunity Scout.\nInfra: FastAPI + APScheduler + Docker + Ollama/Groq LLM + GitHub Pages landing page.\n83 files, ~10K lines. Cost: $0-5/month.
2026-03-30 11:45:48 +03:00

221 lines
7.5 KiB
Python

"""LLM-powered response generation for WhatsApp conversations."""
from __future__ import annotations
import logging
import re
from pathlib import Path
from typing import Any
import yaml
logger = logging.getLogger(__name__)
_TEMPLATES_PATH = Path(__file__).parent / "prompts" / "whatsapp_templates.yaml"
# Cached templates (loaded once)
_templates: dict | None = None
def _load_templates() -> dict:
"""Load WhatsApp response templates from YAML."""
global _templates
if _templates is None:
if _TEMPLATES_PATH.exists():
with open(_TEMPLATES_PATH, "r", encoding="utf-8") as f:
_templates = yaml.safe_load(f) or {}
else:
_templates = {}
return _templates
def _detect_language(text: str) -> str:
"""Heuristic language detection -- returns ``'ar'`` or ``'en'``.
If the text contains Arabic Unicode characters, assume Arabic.
Otherwise default to English.
"""
arabic_pattern = re.compile(r"[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]+")
arabic_chars = len(arabic_pattern.findall(text))
latin_chars = len(re.findall(r"[a-zA-Z]+", text))
if arabic_chars > 0 and arabic_chars >= latin_chars:
return "ar"
return "en"
def _build_system_prompt(brand_profile: dict, language: str) -> str:
"""Construct the system prompt that tells the LLM how to behave."""
templates = _load_templates()
name = brand_profile.get("name", "Sami Mohammed Assiri")
title = brand_profile.get("title", "Field Services Engineer")
company = brand_profile.get("company", "METCO (Smiths Detection)")
location = brand_profile.get("location", "Riyadh, Saudi Arabia")
calcom_url = brand_profile.get("calcom_url", "https://cal.com/sami-assiri")
cv_url = brand_profile.get("cv_url", "")
linkedin_url = brand_profile.get("linkedin_url", "")
specialties = brand_profile.get("specialties", [
"Airport security systems",
"CT/X-ray screening technology",
"Field service engineering",
"System integration and maintenance",
])
specialties_str = ", ".join(specialties) if isinstance(specialties, list) else str(specialties)
if language == "ar":
return f"""أنت المساعد المهني الذكي لـ {name}.
أنت تتواصل عبر واتساب نيابة عن سامي وتتصرف كمساعده الشخصي.
معلومات عن سامي:
- الاسم: {name}
- المسمى الوظيفي: {title}
- الشركة: {company}
- الموقع: {location}
- التخصصات: {specialties_str}
- رابط الحجز: {calcom_url}
- السيرة الذاتية: {cv_url}
- لينكدإن: {linkedin_url}
التعليمات:
1. رد دائماً بأسلوب مهني ولطيف باللغة العربية.
2. إذا طلب أحد حجز موعد أو اجتماع، وجّهه إلى رابط الحجز: {calcom_url}
3. إذا سأل أحد عن السيرة الذاتية أو الخبرات، شارك المعلومات المتاحة ورابط السيرة الذاتية إن وُجد.
4. إذا كان السؤال خارج نطاق معرفتك، أخبر المرسل أن سامي سيتواصل معه شخصياً.
5. لا تتظاهر بأنك سامي نفسه -- وضّح أنك مساعده الذكي.
6. كن مختصراً ومفيداً -- رسائل واتساب يجب أن تكون قصيرة.
7. إذا أرسل المستخدم رسالة بالإنجليزية، رد بالإنجليزية.
"""
else:
return f"""You are the professional AI assistant for {name}.
You communicate via WhatsApp on behalf of Sami and act as his personal assistant.
About Sami:
- Name: {name}
- Title: {title}
- Company: {company}
- Location: {location}
- Specialties: {specialties_str}
- Booking link: {calcom_url}
- CV: {cv_url}
- LinkedIn: {linkedin_url}
Instructions:
1. Always respond professionally and warmly.
2. If someone requests a meeting or appointment, direct them to the booking link: {calcom_url}
3. If someone asks about Sami's CV or experience, share available information and the CV link if available.
4. If the question is outside your knowledge, let the sender know Sami will follow up personally.
5. Do not pretend to be Sami himself -- clarify you are his AI assistant.
6. Be concise and helpful -- WhatsApp messages should be brief.
7. If the user writes in Arabic, respond in Arabic.
"""
async def generate_response(
llm_client: Any,
message: str,
sender_name: str,
brand_profile: dict,
conversation_history: list[dict[str, str]] | None = None,
) -> str:
"""Generate a context-aware response using the LLM.
Parameters
----------
llm_client:
Any LLM client that supports a ``chat`` or ``generate`` style call.
message:
The incoming message text.
sender_name:
Display name of the sender.
brand_profile:
Parsed brand profile dictionary.
conversation_history:
Optional list of ``{"role": ..., "content": ...}`` dicts.
Returns
-------
str
The generated response text.
"""
language = _detect_language(message)
system_prompt = _build_system_prompt(brand_profile, language)
# Build the messages list for the LLM
messages: list[dict[str, str]] = [{"role": "system", "content": system_prompt}]
# Include recent conversation history (last 10 turns)
if conversation_history:
recent = conversation_history[-10:]
for turn in recent:
if turn.get("role") in ("user", "assistant"):
messages.append(
{"role": turn["role"], "content": turn["content"]}
)
else:
messages.append({"role": "user", "content": message})
# Call the LLM -- support multiple client interfaces
try:
response_text = await _call_llm(llm_client, messages)
except Exception as exc:
logger.error("LLM call failed: %s", exc)
raise
return response_text.strip()
async def _call_llm(
llm_client: Any,
messages: list[dict[str, str]],
) -> str:
"""Invoke the LLM client, handling different API shapes.
Supports:
- OpenAI-compatible (``chat.completions.create``)
- Ollama-style (``chat`` method)
- Groq-style (``chat.completions.create``)
- Generic callable that accepts messages
"""
# OpenAI / Groq compatible interface
if hasattr(llm_client, "chat") and hasattr(llm_client.chat, "completions"):
response = await _async_or_sync(
llm_client.chat.completions.create,
messages=messages,
max_tokens=500,
temperature=0.7,
)
return response.choices[0].message.content
# Ollama-style interface
if hasattr(llm_client, "chat"):
response = await _async_or_sync(
llm_client.chat,
messages=messages,
)
if isinstance(response, dict):
return response.get("message", {}).get("content", "")
return str(response)
# Generic callable
if callable(llm_client):
response = await _async_or_sync(llm_client, messages=messages)
if isinstance(response, str):
return response
return str(response)
raise TypeError(f"Unsupported LLM client type: {type(llm_client)}")
async def _async_or_sync(func: Any, **kwargs: Any) -> Any:
"""Call *func* whether it is sync or async."""
import asyncio
import inspect
if inspect.iscoroutinefunction(func):
return await func(**kwargs)
else:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, lambda: func(**kwargs))