Agent در عمل — ساخت یه دستیار تلگرامی

قسمت ۸ ۲۲ دقیقه

مقدمه: بیا یه چیز واقعی بسازیم

تا الان خیلی تئوری حرف زدیم. حالا وقتشه دست به کد بشیم و یه Agent واقعی بسازیم که آدم‌ها واقعاً ازش استفاده کنن.

تو این اپیزود، یه دستیار هوشمند تلگرامی می‌سازیم. نه یه چت‌بات ساده — بلکه یه Agent واقعی که ابزار داره، حافظه داره و می‌تونه کارای مفید انجام بده. قدم به قدم، از صفر تا دیپلوی.

چی می‌سازیم؟

یه دستیار تلگرامی که:

  • مکالمه طبیعی به فارسی داره
  • می‌تونه آب‌وهوا رو چک کنه
  • می‌تونه محاسبات ریاضی انجام بده
  • حافظه داره و مکالمات قبلی رو یادش می‌مونه
  • تو گروه و چت خصوصی کار می‌کنه

قدم ۱: نصب و راه‌اندازی

# نصب کتابخانه‌ها
# pip install python-telegram-bot openai redis

# ساختار پروژه:
# telegram_agent/
# ├── bot.py          (فایل اصلی)
# ├── agent.py        (منطق Agent)
# ├── tools.py        (ابزارها)
# ├── memory.py       (حافظه)
# ├── config.py       (تنظیمات)
# └── requirements.txt

قدم ۲: تنظیمات

# config.py
import os

TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")  # OpenWeatherMap
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")

# مدل
MODEL_NAME = "gpt-4o-mini"
MAX_TOKENS = 1000

# محدودیت‌ها
MAX_MESSAGES_PER_MINUTE = 10
MAX_HISTORY_LENGTH = 20

قدم ۳: ابزارها

# tools.py
import json
import httpx
from config import WEATHER_API_KEY

# تعریف ابزارها برای OpenAI
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "آب‌وهوای فعلی یه شهر رو می‌ده",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "نام شهر (انگلیسی)"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "عبارت ریاضی رو محاسبه می‌کنه",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "عبارت ریاضی"
                    }
                },
                "required": ["expression"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "تاریخ و ساعت فعلی رو برمی‌گردونه",
            "parameters": {
                "type": "object",
                "properties": {
                    "timezone": {
                        "type": "string",
                        "description": "منطقه زمانی",
                        "default": "Asia/Tehran"
                    }
                }
            }
        }
    },
]

# پیاده‌سازی ابزارها
async def get_weather(city: str) -> str:
    """آب‌وهوای شهر رو از API می‌گیره."""
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {
        "q": city,
        "appid": WEATHER_API_KEY,
        "units": "metric",
        "lang": "fa",
    }
    
    async with httpx.AsyncClient() as client:
        try:
            resp = await client.get(url, params=params)
            data = resp.json()
            
            if resp.status_code != 200:
                return f"خطا: شهر '{city}' پیدا نشد."
            
            temp = data["main"]["temp"]
            feels = data["main"]["feels_like"]
            desc = data["weather"][0]["description"]
            humidity = data["main"]["humidity"]
            
            return (
                f"شهر: {city}\n"
                f"دما: {temp}°C (حس واقعی: {feels}°C)\n"
                f"وضعیت: {desc}\n"
                f"رطوبت: {humidity}%"
            )
        except Exception as e:
            return f"خطا در دریافت آب‌وهوا: {str(e)}"


def calculate(expression: str) -> str:
    """عبارت ریاضی رو محاسبه می‌کنه (امن)."""
    # فقط کاراکترهای ریاضی مجازن
    allowed = set("0123456789+-*/().% ")
    if not all(c in allowed for c in expression):
        return "خطا: فقط عبارات ریاضی ساده مجازه."
    
    try:
        result = eval(expression)
        return f"{expression} = {result}"
    except Exception as e:
        return f"خطا در محاسبه: {str(e)}"


def get_current_time(timezone: str = "Asia/Tehran") -> str:
    """تاریخ و ساعت فعلی."""
    from datetime import datetime
    import pytz
    
    try:
        tz = pytz.timezone(timezone)
        now = datetime.now(tz)
        return now.strftime("%Y-%m-%d %H:%M:%S %Z")
    except:
        return "منطقه زمانی نامعتبره."


# مپ نام ابزار به تابع
TOOL_FUNCTIONS = {
    "get_weather": get_weather,
    "calculate": calculate,
    "get_current_time": get_current_time,
}

async def execute_tool(name: str, args: dict) -> str:
    """ابزار رو اجرا می‌کنه."""
    func = TOOL_FUNCTIONS.get(name)
    if not func:
        return f"ابزار '{name}' وجود نداره."
    
    import asyncio
    if asyncio.iscoroutinefunction(func):
        return await func(**args)
    return func(**args)

قدم ۴: حافظه

# memory.py
import json
import redis.asyncio as redis
from config import REDIS_URL, MAX_HISTORY_LENGTH

class Memory:
    """حافظه مکالمه با Redis."""
    
    def __init__(self):
        self.redis = redis.from_url(REDIS_URL)
    
    def _key(self, chat_id: int) -> str:
        return f"chat:{chat_id}:history"
    
    async def get_history(self, chat_id: int) -> list:
        """تاریخچه مکالمه رو برمی‌گردونه."""
        data = await self.redis.get(self._key(chat_id))
        if data:
            return json.loads(data)
        return []
    
    async def add_message(
        self, chat_id: int, role: str, content: str
    ):
        """یه پیام به تاریخچه اضافه می‌کنه."""
        history = await self.get_history(chat_id)
        history.append({"role": role, "content": content})
        
        # فقط آخرین N پیام رو نگه دار
        if len(history) > MAX_HISTORY_LENGTH:
            history = history[-MAX_HISTORY_LENGTH:]
        
        await self.redis.set(
            self._key(chat_id),
            json.dumps(history, ensure_ascii=False),
            ex=86400 * 7,  # ۷ روز نگه‌داری
        )
    
    async def clear(self, chat_id: int):
        """تاریخچه رو پاک می‌کنه."""
        await self.redis.delete(self._key(chat_id))

قدم ۵: منطق Agent

# agent.py
from openai import AsyncOpenAI
from config import OPENAI_API_KEY, MODEL_NAME, MAX_TOKENS
from tools import TOOLS, execute_tool
from memory import Memory

client = AsyncOpenAI(api_key=OPENAI_API_KEY)
memory = Memory()

SYSTEM_PROMPT = """تو یه دستیار هوشمند فارسی‌زبان هستی.

قوانین:
- همیشه فارسی جواب بده
- مختصر و مفید باش
- اگه نمی‌دونی، بگو نمی‌دونم
- از ابزارها برای کارای واقعی استفاده کن
- هیچ‌وقت اطلاعات جعلی نده

ابزارهای تو:
- get_weather: آب‌وهوای شهر
- calculate: ماشین‌حساب
- get_current_time: تاریخ و ساعت
"""


async def process_message(
    chat_id: int, user_message: str, user_name: str
) -> str:
    """پیام کاربر رو پردازش می‌کنه و جواب می‌ده."""
    
    # تاریخچه مکالمه
    history = await memory.get_history(chat_id)
    
    # ساخت لیست پیام‌ها
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    messages.extend(history)
    messages.append({"role": "user", "content": user_message})
    
    # فراخوانی LLM
    response = await client.chat.completions.create(
        model=MODEL_NAME,
        messages=messages,
        tools=TOOLS,
        max_tokens=MAX_TOKENS,
    )
    
    msg = response.choices[0].message
    
    # اگه ابزار می‌خواد استفاده کنه
    if msg.tool_calls:
        # اجرای همه ابزارها
        tool_results = []
        for tool_call in msg.tool_calls:
            import json
            args = json.loads(tool_call.function.arguments)
            result = await execute_tool(
                tool_call.function.name, args
            )
            tool_results.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })
        
        # فراخوانی دوباره LLM با نتایج ابزار
        messages.append({
            "role": "assistant",
            "content": msg.content,
            "tool_calls": [
                {
                    "id": tc.id,
                    "type": "function",
                    "function": {
                        "name": tc.function.name,
                        "arguments": tc.function.arguments,
                    }
                }
                for tc in msg.tool_calls
            ]
        })
        messages.extend(tool_results)
        
        response = await client.chat.completions.create(
            model=MODEL_NAME,
            messages=messages,
            max_tokens=MAX_TOKENS,
        )
        final_content = response.choices[0].message.content
    else:
        final_content = msg.content
    
    # ذخیره تو حافظه
    await memory.add_message(chat_id, "user", user_message)
    await memory.add_message(chat_id, "assistant", final_content)
    
    return final_content

قدم ۶: بات تلگرام

# bot.py
import logging
from telegram import Update
from telegram.ext import (
    Application,
    CommandHandler,
    MessageHandler,
    filters,
    ContextTypes,
)
from config import TELEGRAM_TOKEN
from agent import process_message
from memory import Memory

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

memory = Memory()


async def start(
    update: Update, context: ContextTypes.DEFAULT_TYPE
):
    """دستور /start"""
    await update.message.reply_text(
        "سلام! من دستیار هوشمند هستم.\n\n"
        "می‌تونم:\n"
        "- سوالاتت رو جواب بدم\n"
        "- آب‌وهوا رو بگم\n"
        "- محاسبات ریاضی انجام بدم\n\n"
        "هر چیزی بپرس!"
    )


async def clear(
    update: Update, context: ContextTypes.DEFAULT_TYPE
):
    """دستور /clear - پاک کردن حافظه"""
    await memory.clear(update.effective_chat.id)
    await update.message.reply_text(
        "حافظه پاک شد! از اول شروع می‌کنیم."
    )


async def handle_message(
    update: Update, context: ContextTypes.DEFAULT_TYPE
):
    """پردازش پیام‌های متنی."""
    if not update.message or not update.message.text:
        return
    
    chat_id = update.effective_chat.id
    user_message = update.message.text
    user_name = update.effective_user.first_name
    
    # تو گروه فقط وقتی mention بشه جواب بده
    if update.effective_chat.type in ["group", "supergroup"]:
        bot_username = context.bot.username
        if f"@{bot_username}" not in user_message:
            return
        # mention رو حذف کن
        user_message = user_message.replace(
            f"@{bot_username}", ""
        ).strip()
    
    # نشون بده داره تایپ می‌کنه
    await context.bot.send_chat_action(
        chat_id=chat_id, action="typing"
    )
    
    try:
        response = await process_message(
            chat_id, user_message, user_name
        )
        await update.message.reply_text(response)
    except Exception as e:
        logger.error(f"Error: {e}")
        await update.message.reply_text(
            "متأسفم، یه مشکلی پیش اومد. دوباره تلاش کن."
        )


def main():
    """راه‌اندازی بات."""
    app = Application.builder().token(TELEGRAM_TOKEN).build()
    
    # دستورات
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("clear", clear))
    
    # پیام‌های متنی
    app.add_handler(
        MessageHandler(
            filters.TEXT & ~filters.COMMAND,
            handle_message,
        )
    )
    
    logger.info("Bot is running...")
    app.run_polling()


if __name__ == "__main__":
    main()

قدم ۷: مدیریت گروه و خصوصی

یه نکته مهم: رفتار بات تو گروه باید فرق داشته باشه با چت خصوصی.

چت خصوصی: به هر پیامی جواب بده. حافظه مختص همون کاربره.

گروه: فقط وقتی mention بشه (@bot_name) جواب بده. حافظه مشترک بین همه اعضای گروهه. این رو تو کد بالا پیاده‌سازی کردیم — توجه کن به بخش handle_message که چک می‌کنه آیا تو گروه هستیم و آیا بات mention شده.

قدم ۸: دیپلوی

# Dockerfile
# FROM python:3.11-slim
# WORKDIR /app
# COPY requirements.txt .
# RUN pip install -r requirements.txt
# COPY . .
# CMD ["python", "bot.py"]

# requirements.txt
# python-telegram-bot==21.0
# openai==1.30.0
# redis==5.0.0
# httpx==0.27.0
# pytz==2024.1

# docker-compose.yml
# version: "3.8"
# services:
#   bot:
#     build: .
#     env_file: .env
#     depends_on:
#       - redis
#     restart: always
#   redis:
#     image: redis:7-alpine
#     volumes:
#       - redis_data:/data
# volumes:
#   redis_data:

# اجرا:
# docker-compose up -d

بهبودهای پیشنهادی

این نسخه پایه‌ست. چند ایده برای بهترش کردن:

  • Rate limiting: جلوی اسپم رو بگیر — حداکثر ۱۰ پیام در دقیقه از هر کاربر
  • ابزارهای بیشتر: جستجوی وب، ترجمه، خلاصه‌سازی لینک
  • پشتیبانی تصویر: با مدل‌های Vision، تصاویر رو هم تحلیل کن
  • دستور /help: راهنمای کامل قابلیت‌ها
  • لاگ و مانیتورینگ: ثبت همه مکالمات برای بهبود
  • Guardrails: از اپیزود قبلی، لایه‌های امنیتی اضافه کن
نکته: این پروژه رو می‌تونی به عنوان نمونه‌کار تو رزومه‌ات بذاری. یه بات تلگرامی که واقعاً کار می‌کنه و ابزار و حافظه داره، نشون می‌ده که مفاهیم Agent رو عملی بلدی.

جمع‌بندی

  • ساخت Agent تلگرامی ترکیب python-telegram-bot + OpenAI API + Redis ه
  • ابزارها (Tools) قدرت واقعی Agent رو نشون می‌دن
  • حافظه (Memory) با Redis پایدار و سریعه
  • مدیریت گروه و خصوصی مهمه
  • دیپلوی با Docker ساده‌ترینه

اپیزود بعدی: تست و دیباگ Agent ها — چطور بفهمی Agent ت درست کار می‌کنه.

نظرات

هنوز نظری ثبت نشده. اولین نفر باشید!

نظر خود را بنویسید