# سیستم پرامپت داینامیک: مدیریت رفتار Agent از طریق دیتابیس ## مقدمه یکی از چالش‌های اصلی در توسعه و تنظیم Agent ها، نیاز به تغییر مکرر System Prompt است. در حالت معمول، برای تغییر رفتار Agent باید کد را تغییر داد، ریدپلوی کرد و منتظر ماند. این فرایند برای تست و بهینه‌سازی رفتار Agent بسیار کُند و وقت‌گیر است. ما این مشکل را با یک **سیستم پرامپت داینامیک** حل کرده‌ایم: System Prompt ها در دیتابیس (PostgreSQL) ذخیره می‌شوند و از طریق **پنل ادمین Django** قابل مدیریت هستند. یک **Pre-Hook** در هر درخواست کاربر، آخرین پرامپت‌ها را از دیتابیس می‌خواند و روی Agent اعمال می‌کند. ### مزایای کلیدی - **بدون ریدپلوی**: تغییر رفتار Agent بدون نیاز به تغییر کد یا ریستارت سرویس - **تست آسان**: امکان آزمایش سریع پرامپت‌های مختلف از طریق پنل ادمین - **تاخیر ناچیز**: چون به صورت Pre-Hook اجرا می‌شود، overhead قابل چشم‌پوشی است - **Fallback ایمن**: در صورت عدم دسترسی به دیتابیس، پرامپت پیش‌فرض اعمال می‌شود ## معماری ### نمای کلی ```mermaid graph TD A["پنل ادمین Django"] -->|مدیریت پرامپت‌ها| B["جدول agent_agentprompt"] C["درخواست کاربر"] --> D["Agent pre_hooks"] D --> E["sync_config_hook"] E -->|کوئری| B B -->|پرامپت‌های فعال| E E -->|agent.instructions = new_prompts| F["Agent"] E -->|خطای اتصال| G["retry 3 بار"] G -->|موفق| E G -->|ناموفق| H["default_system_prompt"] H --> F ``` ### جریان داده ``` پنل ادمین Django │ │ CRUD عملیات (ایجاد/ویرایش/حذف/فعال‌سازی) ▼ ┌─────────────────────────────────┐ │ جدول: agent_agentprompt │ │ ────────────────────────────── │ │ id | settings_id | content │ │ | is_active | order │ └───────────────┬─────────────────┘ │ │ SELECT (هر درخواست کاربر) ▼ ┌─────────────────────────────────┐ │ sync_config_hook │ ← src/utils/hooks.py │ ────────────────────────────── │ │ 1. دریافت پرامپت‌های فعال │ │ 2. جایگزینی agent.instructions │ └───────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ Agent با پرامپت‌های جدید │ │ اجرای درخواست کاربر │ └─────────────────────────────────┘ ``` ## پیاده‌سازی ### ساختار فایل‌ها | فایل | مسئولیت | |------|---------| | `src/utils/load_settings.py` | اتصال به دیتابیس، خواندن پرامپت‌ها، مدیریت retry و تعریف پرامپت پیش‌فرض | | `src/utils/hooks.py` | Pre-Hook که پرامپت‌ها را از دیتابیس گرفته و روی Agent اعمال می‌کند | | `src/agents/base_agent.py` | تنظیم Agent با pre_hooks و پرامپت پیش‌فرض اولیه | ### بخش ۱: اتصال به دیتابیس و خواندن پرامپت‌ها فایل: `src/utils/load_settings.py` #### تنظیم Connection Pool ```python from sqlalchemy import create_engine, text from sqlalchemy.exc import OperationalError engine = create_engine( db_url, pool_pre_ping=True, # بررسی زنده بودن اتصال قبل از استفاده pool_recycle=3600, # بستن و بازکردن اتصالات قدیمی‌تر از 1 ساعت pool_size=10, # حداکثر 10 اتصال همزمان max_overflow=20 # اتصالات اضافی در ترافیک بالا ) ``` **چرا `pool_pre_ping=True`؟** بدون این تنظیم، اگر اتصال دیتابیس به دلیل timeout یا مشکلات شبکه قطع شده باشد، اولین درخواست بعد از قطعی با خطا مواجه می‌شود. `pool_pre_ping` قبل از هر استفاده، یک ping ساده به دیتابیس ارسال می‌کند و در صورت قطعی، اتصال جدید می‌سازد. #### خواندن پرامپت‌ها با Retry ```python def get_active_agent_config(retries=3, delay=1): """ Fetches active prompts with automatic retry logic for database "hiccups". """ attempt = 0 while attempt < retries: try: with engine.connect() as conn: query = text(""" SELECT content FROM agent_agentprompt WHERE settings_id = 1 AND is_active = true ORDER BY id ASC """) result = conn.execute(query).fetchall() prompt_list = [row.content for row in result] return {"system_prompts": prompt_list} except OperationalError as e: attempt += 1 print(f"⚠️ DB Connection error (Attempt {attempt}/{retries}): {e}") if attempt < retries: time.sleep(delay) else: print("❌ DB Retry limit reached.") return None except Exception as e: print(f"❌ Unexpected Error: {e}") return None ``` #### ساختار کوئری ```sql SELECT content FROM agent_agentprompt WHERE settings_id = 1 AND is_active = true ORDER BY id ASC ``` | فیلد | توضیح | |------|-------| | `content` | متن پرامپت | | `settings_id = 1` | فقط پرامپت‌های مربوط به تنظیمات فعلی | | `is_active = true` | فقط پرامپت‌های فعال (غیرفعال‌ها نادیده گرفته می‌شوند) | | `ORDER BY id ASC` | حفظ ترتیب تعریف شده پرامپت‌ها | از طریق پنل ادمین می‌توان: - پرامپت جدید **اضافه** کرد - پرامپت موجود را **ویرایش** کرد - یک پرامپت را **غیرفعال** (`is_active = false`) کرد بدون حذف آن - پرامپت را **حذف** کرد - **ترتیب** پرامپت‌ها را تغییر داد #### مکانیزم Retry سیستم retry سه‌مرحله‌ای برای مقابله با مشکلات موقت دیتابیس: ``` تلاش ۱ → خطا → صبر 1 ثانیه تلاش ۲ → خطا → صبر 1 ثانیه تلاش ۳ → خطا → برگشت None (استفاده از پرامپت پیش‌فرض) ``` | پارامتر | مقدار | توضیح | |----------|-------|-------| | `retries` | 3 | حداکثر تعداد تلاش | | `delay` | 1 ثانیه | فاصله بین تلاش‌ها | **نکته**: فقط `OperationalError` (خطاهای اتصال/شبکه) باعث retry می‌شود. خطاهای دیگر (مثل خطای SQL) بلافاصله `None` برمی‌گردانند چون retry کردن آن‌ها فایده‌ای ندارد. #### پرامپت پیش‌فرض (Fallback) ```python def default_system_prompt(): return [ "You are a strict Islamic Knowledge Assistant.", "Your Goal: Answer the user's question using the provided 'Context from the database'.", "STRICT BEHAVIORAL RULE: You must maintain the highest standard of Adab (Etiquette).", "If the user is disrespectful, vulgar, uses profanity, or mocks Islam:", "1. Do NOT engage with the toxicity.", "2. Do NOT lecture them.", "3. Refuse to answer immediately by saying: 'I cannot answer this due to violations of Adab.'", "If the Context is in a different language than the User's Question, you MUST translate ...", "If the answer is explicitly found in the context, answer directly.", "If the answer is NOT found in the context, strictly reply: 'Information not available ...'", "Maintain a respectful, scholarly tone.", "Do not explain your reasoning process in the final output.", ] ``` این پرامپت در دو جا استفاده می‌شود: 1. **هنگام ساخت Agent**: به عنوان `instructions` اولیه 2. **زمان عدم دسترسی به دیتابیس**: وقتی `sync_config_hook` نتواند پرامپت از دیتابیس بخواند، Agent با همین پرامپت پیش‌فرض کار می‌کند ### بخش ۲: Pre-Hook برای اعمال پرامپت فایل: `src/utils/hooks.py` ```python def sync_config_hook(run_input: RunInput, **kwargs): """ Agno Pre-Hook: Fetches the latest Django DB config and injects it into the agent before the run starts. """ # 1. دسترسی به instance ایجنت agent = kwargs.get("agent") if not agent: return # 2. دریافت تنظیمات از دیتابیس config = get_active_agent_config() # 3. اعمال پرامپت‌های جدید if config and config.get("system_prompts"): new_prompts = config["system_prompts"] agent.instructions = new_prompts return run_input ``` #### نحوه کار 1. **دریافت Agent**: فریمورک Agno در `kwargs` شیء agent را پاس می‌دهد 2. **خواندن از دیتابیس**: تابع `get_active_agent_config()` فراخوانی می‌شود (شامل retry) 3. **جایگزینی instructions**: اگر پرامپت‌هایی از دیتابیس برگشت، `agent.instructions` **کاملاً جایگزین** می‌شود 4. **حالت Fallback**: اگر `config` برابر `None` باشد (خطای دیتابیس)، شرط `if` اجرا نمی‌شود و Agent با instructions قبلی (پیش‌فرض) کار می‌کند **نکته مهم**: `agent.instructions` کاملاً **overwrite** می‌شود (نه append). یعنی لیست پرامپت‌های دیتابیس جایگزین کامل لیست قبلی می‌شود. این طراحی عمدی است تا پنل ادمین کنترل کامل روی رفتار Agent داشته باشد. ### بخش ۳: ثبت Hook در Agent فایل: `src/agents/base_agent.py` ```python from src.utils.hooks import sync_config_hook, rag_injection_hook from src.utils.load_settings import default_system_prompt class IslamicScholarAgent: def __init__(self, model, knowledge_base, custom_instructions=None, db_url=None): self.custom_instructions = custom_instructions or default_system_prompt() self.agent = Agent( name="Islamic Scholar Agent", model=model, instructions=self.custom_instructions, # پرامپت اولیه (پیش‌فرض) pre_hooks=[ PromptInjectionGuardrail(), InputLimitGuardrail(), sync_config_hook, # ← اینجا پرامپت از دیتابیس خوانده و اعمال می‌شود rag_injection_hook, ], # ... ) ``` #### ترتیب اجرای Hook ها ``` درخواست کاربر │ ├─ 1. PromptInjectionGuardrail → بررسی امنیتی ورودی ├─ 2. InputLimitGuardrail → بررسی محدودیت طول ورودی ├─ 3. sync_config_hook → خواندن پرامپت از دیتابیس و اعمال ├─ 4. rag_injection_hook → تزریق context از وکتور دیتابیس │ ▼ Agent اجرا می‌شود (با پرامپت‌های تازه از دیتابیس + context از RAG) ``` `sync_config_hook` عمداً **قبل از** `rag_injection_hook` قرار دارد. ابتدا رفتار Agent (instructions) تنظیم می‌شود، سپس داده‌ها (context) به ورودی اضافه می‌شوند. ## زنجیره Fallback کامل ```mermaid graph TD A["sync_config_hook اجرا می‌شود"] --> B["get_active_agent_config فراخوانی"] B --> C{"تلاش ۱: اتصال به DB"} C -->|موفق| D["پرامپت‌های فعال برگشت"] C -->|OperationalError| E{"تلاش ۲"} E -->|موفق| D E -->|OperationalError| F{"تلاش ۳"} F -->|موفق| D F -->|OperationalError| G["return None"] D --> H["agent.instructions = پرامپت‌های دیتابیس"] G --> I["شرط if اجرا نمی‌شود"] I --> J["Agent با instructions فعلی کار می‌کند"] J --> K["default_system_prompt"] H --> L["Agent با پرامپت‌های جدید اجرا می‌شود"] ``` ### سناریوهای مختلف | سناریو | نتیجه | |---------|-------| | دیتابیس در دسترس، پرامپت‌های فعال موجود | پرامپت‌های دیتابیس اعمال می‌شوند | | دیتابیس در دسترس، هیچ پرامپت فعالی نیست | لیست خالی برمی‌گردد، شرط `if` رد می‌شود، پرامپت پیش‌فرض حفظ می‌شود | | دیتابیس موقتاً قطع (تلاش ۱ یا ۲ موفق) | پس از retry موفق، پرامپت‌های دیتابیس اعمال می‌شوند | | دیتابیس کاملاً قطع (هر ۳ تلاش ناموفق) | `None` برمی‌گردد، پرامپت پیش‌فرض حفظ می‌شود | | خطای غیرمنتظره (مثلاً خطای SQL) | بلافاصله `None` برمی‌گردد، پرامپت پیش‌فرض حفظ می‌شود | ## عملکرد و تاخیر ### چرا تاخیر ناچیز است؟ 1. **Connection Pool**: اتصال به دیتابیس از pool گرفته می‌شود (بدون overhead ساخت اتصال جدید) 2. **کوئری ساده**: یک `SELECT` ساده با فیلتر `WHERE` روی فیلدهای ایندکس‌شده 3. **داده کم**: تعداد پرامپت‌ها معمولاً کمتر از ۲۰ رکورد است 4. **pool_pre_ping**: اتصال‌های مُرده سریع شناسایی و جایگزین می‌شوند ### برآورد زمانی | عملیات | زمان تقریبی | |--------|-------------| | گرفتن اتصال از pool | < 1ms | | اجرای کوئری SELECT | 1-5ms | | پردازش نتایج و اعمال | < 1ms | | **مجموع (حالت عادی)** | **2-7ms** | | Retry (در صورت خطا) | +1000ms به ازای هر retry | در مقایسه با زمان پاسخ‌دهی LLM (معمولاً 1-10 ثانیه)، تاخیر 2-7 میلی‌ثانیه‌ای کاملاً قابل چشم‌پوشی است. ## نحوه استفاده از پنل ادمین ### افزودن پرامپت جدید 1. وارد پنل ادمین Django شوید 2. به بخش **Agent Prompts** بروید 3. پرامپت جدید بسازید: - `content`: متن پرامپت - `settings_id`: شماره تنظیمات (معمولاً 1) - `is_active`: تیک بزنید تا فعال باشد 4. ذخیره کنید تغییرات **بلافاصله** در درخواست بعدی کاربر اعمال می‌شوند. ### غیرفعال کردن پرامپت - تیک `is_active` را بردارید - پرامپت از لیست ارسالی به Agent حذف می‌شود اما در دیتابیس باقی می‌ماند - می‌توانید هر زمان دوباره فعالش کنید ### تست رفتار جدید 1. پرامپت‌ها را در پنل ادمین تغییر دهید 2. یک پیام تستی به Agent بفرستید 3. پاسخ را بررسی کنید 4. در صورت نیاز، پرامپت‌ها را اصلاح کنید و دوباره تست کنید این چرخه بدون هیچ ریدپلوی یا ریستارتی انجام می‌شود. ## تنظیمات محیطی | متغیر | توضیح | الزامی | |--------|--------|--------| | `DB_USER` | نام کاربری دیتابیس PostgreSQL | بله | | `DB_PASSWORD` | رمز عبور دیتابیس | بله | | `DB_HOST` | آدرس سرور دیتابیس | بله | | `DB_PORT` | پورت دیتابیس | بله | | `DB_NAME` | نام دیتابیس | بله | ## نتیجه‌گیری سیستم پرامپت داینامیک با ترکیب سه مولفه ساده اما موثر کار می‌کند: 1. **دیتابیس + پنل ادمین**: ذخیره و مدیریت آسان پرامپت‌ها 2. **`get_active_agent_config`**: خواندن امن از دیتابیس با retry و fallback 3. **`sync_config_hook`**: اعمال پرامپت‌ها روی Agent به صورت Pre-Hook این معماری امکان **تغییر لحظه‌ای رفتار Agent** را بدون نیاز به تغییر کد، ریدپلوی یا ریستارت فراهم می‌کند. تاخیر ناشی از خواندن دیتابیس در حد چند میلی‌ثانیه است و در مقایسه با زمان پردازش LLM کاملاً قابل چشم‌پوشی است. همچنین سیستم Fallback تضمین می‌کند که حتی در صورت قطعی دیتابیس، Agent با پرامپت پیش‌فرض به کار خود ادامه می‌دهد.