""" مرحله‌ی generation: پرسش + context بازیابی‌شده → پاسخِ فارسی + چند پیشنهادِ ادامه. اگه context خالی باشه، مدل به‌عنوانِ معلمِ عمومی جواب می‌ده (بدونِ ادعای استناد به کتاب) — این حالت وقتی پیش می‌آد که subject/lessonNumber ارسال نشده یا اون درس هنوز در Qdrant نیست. """ import logging from collections.abc import Iterator from django.conf import settings from . import clients logger = logging.getLogger(__name__) _DEFAULT_FOLLOW_UPS = ['یک مثال دیگه بزن', 'بیشتر توضیح بده'] _SYSTEM = 'تو یک معلمِ فارسی‌زبانِ دبستانی هستی و پاسخ‌ها را روشن، ساده و کوتاه می‌دهی.' def _prompt_with_context(question: str, contexts: list[str]) -> str: joined = '\n\n---\n\n'.join(contexts) return f""" تو معلمِ دبستانی هستی. با تکیه بر «متنِ درس» زیر و با زبانِ خودت، به سوالِ دانش‌آموز پاسخِ روشن و ساده بده. لازم نیست پاسخ عیناً در متن باشد؛ می‌توانی از اطلاعاتِ متن نتیجه‌گیری و توضیح بدهی. فقط اگر متنِ درس آشکارا درباره‌ی موضوعِ دیگری‌ست و هیچ ربطی به سوال ندارد، بنویس: «این موضوع در این درس پوشش داده نشده است.» متنِ درس: {joined} سوالِ دانش‌آموز: {question} """ def _prompt_without_context(question: str) -> str: return f""" به این سوالِ یک دانش‌آموزِ دبستانی به فارسی، ساده و کوتاه پاسخ بده. سوال: {question} """ def generate_answer(question: str, context_chunks: list[dict] | None) -> tuple[str, list[str]]: """ context_chunks: خروجیِ retrieve_context ([{title, summary, score}, ...]) یا None/[]. خروجی: (answer, follow_ups) """ contexts = [c['summary'] for c in (context_chunks or []) if c.get('summary')] prompt = _prompt_with_context(question, contexts) if contexts else _prompt_without_context(question) client = clients.get_openai_client() completion = client.chat.completions.create( model=settings.OPENAI_CHAT_MODEL, messages=[ {'role': 'system', 'content': _SYSTEM}, {'role': 'user', 'content': prompt}, ], temperature=0.3, ) answer = (completion.choices[0].message.content or '').strip() if not answer: answer = 'متأسفم، الان نتونستم پاسخِ مناسبی بسازم. لطفاً دوباره بپرس.' return answer, list(_DEFAULT_FOLLOW_UPS) def stream_answer(question: str, context_chunks: list[dict] | None) -> Iterator[str]: """ همون generate_answer ولی توکن‌به‌توکن (برای SSE). فقط قطعه‌های متنِ پاسخ را yield می‌کند؛ follow_upها ثابت‌اند (_DEFAULT_FOLLOW_UPS) و caller خودش اضافه‌شان می‌کند. """ contexts = [c['summary'] for c in (context_chunks or []) if c.get('summary')] prompt = _prompt_with_context(question, contexts) if contexts else _prompt_without_context(question) client = clients.get_openai_client() stream = client.chat.completions.create( model=settings.OPENAI_CHAT_MODEL, messages=[ {'role': 'system', 'content': _SYSTEM}, {'role': 'user', 'content': prompt}, ], temperature=0.3, stream=True, ) for event in stream: if not event.choices: continue delta = event.choices[0].delta.content or '' if delta: yield delta def default_follow_ups() -> list[str]: return list(_DEFAULT_FOLLOW_UPS)