API output without Persian diacritics or written ezafe ی (company decision); tutor answers too
plain_persian() strips kasra/fatha/damma/shadda/sukun/hamza-above and the written «هی» ezafe, keeping tanwin. Applied at output, not in data: PlainPersianJSONRenderer runs one byte-level pass over every JSON response (~5 ms per MB locally), and to_plain() cleans tutor answers including the stream. Rewriting titles in the DB/seed would have broken question binding by title path and SeedRecord keys. Tests updated to expect the cleaned text; config/tests_plain_persian.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ OFF_TOPIC_MARKER = '[خارج-از-ریاضی]'
|
||||
|
||||
REFUSAL = (
|
||||
'من «پی» هستم و فقط توی ریاضی میتونم کمکت کنم 🙂\n'
|
||||
'یه سوالِ ریاضی بپرس، مثلاً از درسی که الان داری میخونی یا تمرینی که توش گیر کردی.'
|
||||
'یه سوال ریاضی بپرس، مثلاً از درسی که الان داری میخونی یا تمرینی که توش گیر کردی.'
|
||||
)
|
||||
|
||||
# اورژانسِ اجتماعیِ بهزیستی ۱۲۳ و صدای مشاورِ بهزیستی ۱۴۸۰، هر دو سراسری و رایگان.
|
||||
|
||||
@@ -256,8 +256,9 @@ def _to_plain(text: str) -> str:
|
||||
text = re.sub(r'[ \t]+\n', '\n', text)
|
||||
text = re.sub(r'\n{3,}', '\n\n', text)
|
||||
# S_n، a_1، x^2 و توانِ پیچیده که بالا رد شد: همان تبدیلِ seed/دیتابیس (یک رفتار در همهجا).
|
||||
from content.plain_math import convert as _plain
|
||||
return _plain(text).strip()
|
||||
from content.plain_math import convert as _plain, plain_persian
|
||||
# بیاعراب و بی «ی»ِ اضافه، مثلِ همهی پاسخهای API (config/renderers.py)؛ پاسخِ استریمی از آن renderer رد نمیشود.
|
||||
return plain_persian(_plain(text)).strip()
|
||||
|
||||
|
||||
class StreamSanitizer:
|
||||
|
||||
@@ -104,7 +104,7 @@ class AskAIRagPathTests(TestCase):
|
||||
patch('rag.services.clients.get_qdrant_client', return_value=_FakeQdrant()):
|
||||
resp = self._ask({'userQuestion': 'یک سوالِ کلی'})
|
||||
self.assertEqual(resp.status_code, 200, resp.content)
|
||||
self.assertEqual(resp.data['answer'], 'پاسخِ عمومی.')
|
||||
self.assertEqual(resp.data['answer'], 'پاسخ عمومی.')
|
||||
self.assertEqual(resp.data['usedChunks'], [])
|
||||
|
||||
def test_provider_failure_becomes_503_and_releases_slot(self):
|
||||
|
||||
@@ -10,6 +10,8 @@ import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from content.plain_math import plain_persian
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.test import SimpleTestCase, TestCase, override_settings
|
||||
from rest_framework.test import APIClient
|
||||
@@ -109,7 +111,8 @@ class RefusalReplacementTests(SimpleTestCase):
|
||||
result = {}
|
||||
with patch('rag.services.clients.get_openai_client', return_value=_client_answering(text, usage)):
|
||||
out = ''.join(stream_answer('۲+۳؟', None, 'هشتم', result=result))
|
||||
self.assertEqual(out, text)
|
||||
# «بیتغییر» جز پاک شدنِ اعراب/«ی»ِ اضافه (content.plain_math.plain_persian)، که روی هر پاسخ اعمال میشود.
|
||||
self.assertEqual(out, plain_persian(text))
|
||||
self.assertFalse(result['refused'])
|
||||
self.assertEqual(result['usage'], {'promptTokens': 1200, 'completionTokens': 80})
|
||||
|
||||
@@ -141,7 +144,7 @@ class GuardrailThroughTheApiTests(TestCase):
|
||||
done = json.loads(events[-1][1])
|
||||
else:
|
||||
text, done = response.json()['answer'], response.json()
|
||||
self.assertEqual(text, guardrails.REFUSAL)
|
||||
self.assertEqual(text, plain_persian(guardrails.REFUSAL))
|
||||
self.assertEqual(done['usedChunks'], [])
|
||||
self.assertEqual(Message.objects.filter(conversation__user_id=self.user.id, role='user').count(), 2)
|
||||
self.assertEqual(
|
||||
@@ -153,7 +156,7 @@ class GuardrailThroughTheApiTests(TestCase):
|
||||
patch('rag.services.clients.get_openai_client', return_value=_client_answering(MARKER)):
|
||||
response = self.client.post('/api/ai/ask', {'userQuestion': 'حالم خوب نیست، با دوستم دعوام شده'},
|
||||
format='json')
|
||||
self.assertEqual(response.json()['answer'], guardrails.REFUSAL)
|
||||
self.assertEqual(response.json()['answer'], plain_persian(guardrails.REFUSAL))
|
||||
self.assertEqual(response.json()['usedChunks'], [])
|
||||
self.assertEqual(Message.objects.get(role='assistant').text, guardrails.REFUSAL)
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ class ConversationMemoryTests(TestCase):
|
||||
self.assertIn('توان منفی', retrieve_mock.call_args.args[0])
|
||||
|
||||
def test_a_follow_up_is_never_served_from_someone_elses_cache(self):
|
||||
replies = iter(['جوابِ اولِ الف', 'جوابِ «نفهمیدم»ِ الف', 'جوابِ اولِ ب', 'جوابِ «نفهمیدم»ِ ب'])
|
||||
replies = iter(['جوابِ اولِ الف', 'جوابِ «نفهمیدم»ِ الف', 'جوابِ اولِ ب', 'جواب «نفهمیدم» ب'])
|
||||
retrieve = patch('rag.services.retrieval.retrieve_context',
|
||||
return_value=([], {'gradeTitle': 'نهم'}))
|
||||
generate = patch('rag.services.generation.generate_answer',
|
||||
@@ -221,7 +221,7 @@ class ConversationMemoryTests(TestCase):
|
||||
conv_b = self._ask(b, 'تجزیه چیست؟').json()['conversationId']
|
||||
second_b = self._ask(b, 'نفهمیدم', conv_b)
|
||||
|
||||
self.assertEqual(second_b.json()['answer'], 'جوابِ «نفهمیدم»ِ ب')
|
||||
self.assertEqual(second_b.json()['answer'], 'جواب «نفهمیدم» ب')
|
||||
|
||||
|
||||
@override_settings(**RAG_ON)
|
||||
|
||||
Reference in New Issue
Block a user