mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
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.
221 lines
7.5 KiB
Python
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))
|