فهرست مقاله
وقتی پاسخگوی صوتی باید گفتگو را به کارشناس ارجاع دهد، یک جملهٔ «انتقال انجام شد» زمینهٔ کافی نمیسازد. لازم است بدانیم چه درخواستی مطرح بوده، کدام بخش تاریخچه همراه بسته است و دربارهٔ عمل قبلی چه چیزی هنوز نامعلوم مانده است. این درس یک بستهٔ کوچکِ کاملاً ساختگی و ماشین حالت محلی آن را آزمایش میکند؛ هیچ تماس، پیام یا دادهٔ مشتری به سامانهای فرستاده نمیشود.
۱. تصمیم ارجاع با بستهٔ زمینه و رسید تحویل فرق دارد

مثال ما پروندهٔ فرضی Case7 با هدف «بررسی علت خطای ساختگی حسگر» است. علت ارجاع needs_expert انتخاب شده و وضعیت عمل قبلی unknown است. این متن، خلاصهٔ خودکار مدل زبانی یا گزارش تجربهٔ مشتری نیست؛ ورودی آزمایشیِ دستنویس است. حتی اگر مرحلهٔ تحویل محلی بعداً completed شود، وضعیت عمل قبلی بدون شاهد تازه همان unknown میماند.
مستندات ارجاع Microsoft برای SDK v4 میان آغاز ارجاع و رویداد وضعیت تمایز میگذارد و امکان همراهکردن زمینه و رونوشت گفتگو را توضیح میدهد. از این تمایز برای صورتبندی مسئله استفاده میکنیم؛ قالب و ماشین حالتِ این درس، پیادهسازی آن SDK یا پروتکل انتقال تماس نیستند. تمام رویدادهای پذیرفتهشدن و تکمیل را خود آزمون تولید میکند.
آستانهٔ ارجاع تماس به پرسش «چه وقت ارجاع بدهیم» میپردازد. اینجا آن تصمیم از قبل گرفته شده و مسئله «چه چیزی با ارجاع همراه باشد» است. مقصد، ظرفیت کارشناس، اعتبار هویت و دسترسی او در مدل حاضر بررسی نمیشوند؛ درستبودن قالب، مجوز انتقال اطلاعات را اثبات نمیکند.
۲. فیلدهای مجاز و وضعیت عمل را صریح نگه داریم

ورودی دقیقاً پنج فیلد case_id، reason، goal، action_state و history دارد. شناسه حداکثر شانزده نویسهٔ ASCII است و با حرف آغاز میشود. علت فقط یکی از نیاز به کارشناس، کمبود دانش یا درخواست کاربر است. وضعیت عمل یکی از none، in_progress، unknown و completed است. این برچسب، اظهار ورودی است و فایل برای واقعیبودن آن شاهدی جمع نمیکند.
goal رشتهای از یک تا نودوشش نقطهٔ کد است. هر نوبت تاریخچه دقیقاً شمارهٔ seq، نقش role و متن text دارد. نقشهای ورودی از user آغاز میشوند و با assistant تناوب دارند؛ شمارهها صحیح، از صفر تا ۹۹۹۹ و اکیداً صعودیاند، اما لازم نیست پشت سر هم باشند. تاریخچهٔ ورودی حداکثر سیودو نوبت و متن هر نوبت حداکثر صدوبیست نقطهٔ کد دارد.
راهنمای رسمی JSON Schema توضیح میدهد که تعریف properties بهتنهایی حضور فیلدها یا ممنوعیت کلید اضافه را الزام نمیکند. کد ما با مقایسهٔ دقیق کلیدهای دیکشنری، قرارداد بستهٔ خود را اجرا میکند؛ موتور JSON Schema نصب یا اجرا نشده است. کلید ناشناخته، فیلد گمشده و نوع نادرست خطا میدهند؛ تبدیل خودکار یا دورریختن بیاعلان نداریم.
۳. کوتاهسازی تاریخچه باید قابل مشاهده باشد

فقط سه نوبت آخر نگه داشته میشوند. از متن هر نوبت نیز حداکثر چهلوهشت نقطهٔ کد میماند. بسته تعداد نوبتهای منبع، نوبتهای حذفشده و تعداد نقاط کد حذفشدهٔ هر متنِ نگهداشتهشده را ثبت میکند. پرچم shortened وجود هر کدام از این دو نوع کاهش را نشان میدهد؛ این علامت، برابری معنایی نسخهٔ کوتاه و کامل را تضمین نمیکند.
در نمونه شش نوبت با شمارههای صفر تا پنج داریم. سه نوبت اول حذف و شمارههای سه، چهار و پنج نگه داشته میشوند. متن شمارهٔ سه، پنجاهودو حرف A دارد؛ چهلوهشت حرف میماند و چهار نقطهٔ کدِ حذفشده گزارش میشود. نقشهای خروجی assistant، user و assistant باقی میمانند؛ برای اینکه ابتدای قطعه ظاهراً user باشد، نقش یا ترتیب را تغییر نمیدهیم.
مسیر اصلی از deque کراندار پایتون استفاده میکند که هنگام افزودنِ عضو تازه، قدیمیترین عضوِ سوی مقابل را کنار میگذارد. تمام نوبتها در پیمایش بررسی میشوند؛ هر نوبت پیش از کوتاهسازی و افزودن اعتبارسنجی میشود، حتی اگر بعداً از deque خارج شود. بسته فقط پس از پایان موفق کل پیمایش برمیگردد. بنابراین نوبت قدیمیِ نامعتبر با اتکا به حذف آینده، از بررسی قرارداد عبور نمیکند.
۴. کد کامل، مرجع مستقل و حالتهای تحویل محلی

فایل handoff_context_contract.py با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. مرجع مستقل از برش فهرست و پیمایش نویسهها استفاده میکند، نه deque یا تابع آمادهسازی اصلی. سیوشش ترکیبِ تعداد نوبت و طول متن در یک گروه بررسی میشوند. مجموع هجده گروه نامدار است؛ تکرار اجرا یا زیرحالتها، بررسی جدید شمرده نمیشوند.
"""Bounded synthetic handoff envelope and local declared-status transitions.
No call transfer, network delivery, agent acknowledgement, PII detection or action.
"""
from collections import deque
from copy import deepcopy
import json
import platform
import re
REQUEST_KEYS = {'case_id', 'reason', 'goal', 'action_state', 'history'}
TURN_KEYS = {'seq', 'role', 'text'}
REASONS = ('needs_expert', 'missing_knowledge', 'user_request')
ACTION_STATES = ('none', 'in_progress', 'unknown', 'completed')
KEEP_TURNS, KEEP_TEXT = 3, 48
def valid_text(value, maximum):
return (type(value) is str and 1 <= len(value) <= maximum
and all(ord(c) >= 32 and ord(c) != 127
and not 0xD800 <= ord(c) <= 0xDFFF for c in value))
def prepare_context(request):
if type(request) is not dict or set(request) != REQUEST_KEYS:
raise ValueError('request_fields')
case_id = request['case_id']
if type(case_id) is not str or re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]{0,15}', case_id, re.ASCII) is None:
raise ValueError('case_id')
if type(request['reason']) is not str or request['reason'] not in REASONS:
raise ValueError('reason')
if type(request['action_state']) is not str or request['action_state'] not in ACTION_STATES:
raise ValueError('action_state')
if not valid_text(request['goal'], 96):
raise ValueError('goal')
history = request['history']
if type(history) is not list or len(history) > 32:
raise ValueError('history')
tail = deque(maxlen=KEEP_TURNS)
previous = -1
for index, turn in enumerate(history):
if type(turn) is not dict or set(turn) != TURN_KEYS:
raise ValueError('turn_fields')
seq = turn['seq']
if type(seq) is not int or not 0 <= seq <= 9999 or seq <= previous:
raise ValueError('seq')
if type(turn['role']) is not str or turn['role'] != ('user' if index % 2 == 0 else 'assistant'):
raise ValueError('role_order')
text = turn['text']
if not valid_text(text, 120):
raise ValueError('turn_text')
tail.append({'seq': seq, 'role': turn['role'], 'text': text[:KEEP_TEXT],
'omitted_codepoints': max(0, len(text) - KEEP_TEXT)})
previous = seq
kept = list(tail)
omitted_turns = max(0, len(history) - KEEP_TURNS)
return {'schema': 'handoff_context_v1', 'case_id': case_id,
'reason': request['reason'], 'goal': request['goal'],
'action_state': request['action_state'], 'history': kept,
'source_turn_count': len(history), 'omitted_turns': omitted_turns,
'shortened': omitted_turns > 0 or any(t['omitted_codepoints'] > 0 for t in kept)}
def serialize_packet(packet):
# This function encodes prepared packets; it is not a general JSON validator.
raw = json.dumps(packet, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')
if len(raw) > 2048:
raise ValueError('packet_bytes')
return raw
class LocalHandoff:
def __init__(self, request):
self._packet = prepare_context(request)
self._phase = 'prepared'
def snapshot(self):
return {'phase': self._phase, 'packet': deepcopy(self._packet)}
def offer(self):
if self._phase != 'prepared':
raise ValueError('transition')
self._phase = 'offered' # Local intent only, no send or receipt.
def apply_declared_status(self, event):
if type(event) is not dict or set(event) != {'case_id', 'state'}:
raise ValueError('event_fields')
if type(event['case_id']) is not str or event['case_id'] != self._packet['case_id']:
raise ValueError('case_mismatch')
if type(event['state']) is not str or event['state'] not in ('accepted', 'failed', 'completed'):
raise ValueError('event_state')
allowed = {'offered': ('accepted', 'failed'), 'accepted': ('completed', 'failed')}
if event['state'] not in allowed.get(self._phase, ()):
raise ValueError('transition')
self._phase = event['state']
def valid_fixture(count=6, length=12):
return {'case_id': 'Case7', 'reason': 'needs_expert',
'goal': 'بررسی علت خطای ساختگی حسگر', 'action_state': 'unknown',
'history': [{'seq': i, 'role': 'user' if i % 2 == 0 else 'assistant',
'text': 'A' * length} for i in range(count)]}
def slice_oracle(request):
"""Independent valid-domain list slicing and enumeration; no deque/prepare call."""
rows = []
start = max(0, len(request['history']) - 3)
for index, turn in enumerate(request['history']):
if index >= start:
characters = list(turn['text'])
rows.append({'seq': turn['seq'], 'role': turn['role'],
'text': ''.join(characters[i] for i in range(min(48, len(characters)))),
'omitted_codepoints': len(characters) - min(48, len(characters))})
return rows, start
def rejected(call, message):
try:
call()
except ValueError as error:
return str(error) == message
return False
def run_example():
request = valid_fixture()
request['history'][3]['text'] = 'A' * 52
request['history'][4]['text'] = 'ب' * 12
request['history'][5]['text'] = 'کارشناس لازم است'
before = deepcopy(request)
packet = prepare_context(request)
raw = serialize_packet(packet)
flow = LocalHandoff(request)
initial = flow.snapshot()
flow.offer()
offered = flow.snapshot()
wrong_case = rejected(lambda: flow.apply_declared_status({'case_id': 'Other', 'state': 'accepted'}), 'case_mismatch')
wrong_case_unchanged = flow.snapshot() == offered
flow.apply_declared_status({'case_id': 'Case7', 'state': 'accepted'})
accepted = flow.snapshot()
flow.apply_declared_status({'case_id': 'Case7', 'state': 'completed'})
completed = flow.snapshot()
failed = LocalHandoff(request)
failed.offer()
failed.apply_declared_status({'case_id': 'Case7', 'state': 'failed'})
failed_snapshot = failed.snapshot()
premature = LocalHandoff(request)
premature_rejected = rejected(lambda: premature.apply_declared_status({'case_id': 'Case7', 'state': 'accepted'}), 'transition')
premature.offer()
completed_too_soon = rejected(lambda: premature.apply_declared_status({'case_id': 'Case7', 'state': 'completed'}), 'transition')
missing = []
for key in REQUEST_KEYS:
r = valid_fixture()
del r[key]
missing.append(r)
unknown_request = dict(request, phone='synthetic')
unknown_turn = valid_fixture()
unknown_turn['history'][0]['confidence'] = 0.9
no_text = valid_fixture()
del no_text['history'][0]['text']
bad_roles = valid_fixture()
bad_roles['history'][1]['role'] = 'user'
bad_order = valid_fixture()
bad_order['history'][2]['seq'] = 1
bad_seq = valid_fixture()
bad_seq['history'][0]['seq'] = True
invalid_texts = ['', 'a' * 121, 'x\ny', '\ud800', 42]
texts_rejected = []
for text in invalid_texts:
r = valid_fixture()
r['history'][0]['text'] = text
texts_rejected.append(rejected(lambda r=r: prepare_context(r), 'turn_text'))
unicode_request = valid_fixture(1)
unicode_request['history'][0]['text'] = 'x' * 47 + 'e\u0301'
unicode_packet = prepare_context(unicode_request)
explicit_text = valid_fixture(1)
explicit_text['goal'] = 'شمارهٔ ساختگی 555123'
explicit_text['history'][0]['text'] = 'نیم\u200cفاصله'
explicit_packet = prepare_context(explicit_text)
upper = valid_fixture(32, 120)
upper['case_id'] = 'A' * 16
upper['goal'] = chr(0x10000) * 96
for index, turn in enumerate(upper['history']):
turn['seq'] = 9900 + index
turn['text'] = chr(0x10000) * 120
upper_packet = prepare_context(upper)
upper_bytes = len(serialize_packet(upper_packet))
changed_snapshot = flow.snapshot()
changed_snapshot['packet']['history'][0]['text'] = 'changed'
request['history'][3]['text'] = 'changed'
isolated = flow.snapshot() == completed
request = deepcopy(before)
oracle_cases, oracle_ok = 0, True
for count in range(9):
for length in (1, 48, 49, 120):
sample = valid_fixture(count, length)
observed = prepare_context(sample)
expected_rows, expected_omitted = slice_oracle(sample)
oracle_ok &= (observed['history'] == expected_rows and observed['omitted_turns'] == expected_omitted
and observed['source_turn_count'] == count
and observed['shortened'] == (count > 3 or (count > 0 and length > 48)))
oracle_cases += 1
checks = {
'hand_tail_keeps_sequences_three_four_five': [t['seq'] for t in packet['history']] == [3, 4, 5] and packet['omitted_turns'] == 3 and packet['source_turn_count'] == 6,
'hand_text_prefix_and_omitted_codepoints': packet['history'][0]['text'] == 'A' * 48 and packet['history'][0]['omitted_codepoints'] == 4 and packet['history'][1]['omitted_codepoints'] == 0 and packet['shortened'] is True,
'recent_tail_keeps_original_roles_not_relabels': [t['role'] for t in packet['history']] == ['assistant', 'user', 'assistant'],
'action_uncertainty_never_becomes_action_completion': all(s['packet']['action_state'] == 'unknown' for s in [initial, offered, accepted, completed]),
'unknown_request_and_turn_fields_reject': rejected(lambda: prepare_context(unknown_request), 'request_fields') and rejected(lambda: prepare_context(unknown_turn), 'turn_fields'),
'required_top_level_and_turn_fields_reject': all(rejected(lambda r=r: prepare_context(r), 'request_fields') for r in missing) and rejected(lambda: prepare_context(no_text), 'turn_fields'),
'reason_and_action_state_enumerations_reject': rejected(lambda: prepare_context(dict(request, reason='other')), 'reason') and rejected(lambda: prepare_context(dict(request, action_state=True)), 'action_state'),
'case_identifier_and_sequence_type_bounds': all(rejected(lambda value=value: prepare_context(dict(request, case_id=value)), 'case_id') for value in ['A' * 17, 'Case/7', True]) and rejected(lambda: prepare_context(bad_seq), 'seq'),
'source_sequence_and_alternating_roles_enforced': rejected(lambda: prepare_context(bad_roles), 'role_order') and rejected(lambda: prepare_context(bad_order), 'seq'),
'text_bound_control_surrogate_and_type_reject': all(texts_rejected) and rejected(lambda: prepare_context(dict(request, goal='g' * 97)), 'goal'),
'source_history_count_and_list_type_bounds': len(prepare_context(valid_fixture(32))['history']) == 3 and rejected(lambda: prepare_context(valid_fixture(33)), 'history') and rejected(lambda: prepare_context(dict(request, history=())), 'history'),
'codepoint_truncation_is_not_grapheme_or_privacy_filter': unicode_packet['history'][0]['text'] == 'x' * 47 + 'e' and unicode_packet['history'][0]['omitted_codepoints'] == 1 and explicit_packet['goal'] == explicit_text['goal'] and explicit_packet['history'][0]['text'] == 'نیم\u200cفاصله',
'utf8_json_roundtrip_and_adversarial_size_fixture': json.loads(raw) == packet and len(raw) <= 2048 and upper_bytes <= 2048 and upper_packet['omitted_turns'] == 29,
'prepared_packet_and_snapshots_are_isolated': request == before and isolated and prepare_context(request) == packet,
'offered_is_only_local_intent_and_case_correlates': initial['phase'] == 'prepared' and offered['phase'] == 'offered' and wrong_case and wrong_case_unchanged,
'legal_declared_success_and_failure_paths': accepted['phase'] == 'accepted' and completed['phase'] == 'completed' and failed_snapshot['phase'] == 'failed',
'premature_terminal_duplicate_and_unknown_status_reject': premature_rejected and completed_too_soon and rejected(lambda: flow.offer(), 'transition') and rejected(lambda: flow.apply_declared_status({'case_id': 'Case7', 'state': 'accepted'}), 'transition') and rejected(lambda: failed.apply_declared_status({'case_id': 'Case7', 'state': 'accepted'}), 'transition') and rejected(lambda: premature.apply_declared_status({'case_id': 'Case7', 'state': 'sent'}), 'event_state') and rejected(lambda: premature.apply_declared_status({'case_id': 'Case7', 'state': 'accepted', 'extra': True}), 'event_fields') and flow.snapshot() == completed and failed.snapshot() == failed_snapshot,
'independent_list_oracle_for_bounded_history': oracle_cases == 36 and bool(oracle_ok),
}
assert len(checks) == 18 and all(type(value) is bool and value for value in checks.values()), checks
return {'python': platform.python_version(), 'dependencies': 'Python standard library only',
'scope': 'Synthetic bounded context and declared local statuses only; no real identity, privacy guard, action or delivery acknowledgement',
'results': {'prepared_packet': packet, 'serialized_bytes': len(raw),
'upper_bound_fixture_bytes': upper_bytes, 'byte_cap': 2048,
'declared_phase_timeline': [s['phase'] for s in [initial, offered, accepted, completed]],
'preserved_action_state': completed['packet']['action_state'],
'oracle_cases_inside_one_group': oracle_cases}, 'checks': checks}
if __name__ == '__main__':
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
شیء محلی ابتدا prepared است. فراخوانی offer فقط آن را offered میکند؛ هیچ ارسال شبکهای انجام نمیدهد. سپس رویداد ساختگیِ دارای شناسهٔ مطابق میتواند accepted یا failed اعلام کند. پس از accepted، completed یا failed مجاز است. completed زودرس، شناسهٔ دیگر، وضعیت ناشناخته و تکرار پس از حالت پایانی رد میشوند و حالت قبلی باقی میماند. این جدول، سیاست آموزشی ماست.
{
"python": "3.14.5",
"dependencies": "Python standard library only",
"scope": "Synthetic bounded context and declared local statuses only; no real identity, privacy guard, action or delivery acknowledgement",
"results": {
"prepared_packet": {
"schema": "handoff_context_v1",
"case_id": "Case7",
"reason": "needs_expert",
"goal": "بررسی علت خطای ساختگی حسگر",
"action_state": "unknown",
"history": [
{
"seq": 3,
"role": "assistant",
"text": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"omitted_codepoints": 4
},
{
"seq": 4,
"role": "user",
"text": "بببببببببببب",
"omitted_codepoints": 0
},
{
"seq": 5,
"role": "assistant",
"text": "کارشناس لازم است",
"omitted_codepoints": 0
}
],
"source_turn_count": 6,
"omitted_turns": 3,
"shortened": true
},
"serialized_bytes": 508,
"upper_bound_fixture_bytes": 1343,
"byte_cap": 2048,
"declared_phase_timeline": [
"prepared",
"offered",
"accepted",
"completed"
],
"preserved_action_state": "unknown",
"oracle_cases_inside_one_group": 36
},
"checks": {
"hand_tail_keeps_sequences_three_four_five": true,
"hand_text_prefix_and_omitted_codepoints": true,
"recent_tail_keeps_original_roles_not_relabels": true,
"action_uncertainty_never_becomes_action_completion": true,
"unknown_request_and_turn_fields_reject": true,
"required_top_level_and_turn_fields_reject": true,
"reason_and_action_state_enumerations_reject": true,
"case_identifier_and_sequence_type_bounds": true,
"source_sequence_and_alternating_roles_enforced": true,
"text_bound_control_surrogate_and_type_reject": true,
"source_history_count_and_list_type_bounds": true,
"codepoint_truncation_is_not_grapheme_or_privacy_filter": true,
"utf8_json_roundtrip_and_adversarial_size_fixture": true,
"prepared_packet_and_snapshots_are_isolated": true,
"offered_is_only_local_intent_and_case_correlates": true,
"legal_declared_success_and_failure_paths": true,
"premature_terminal_duplicate_and_unknown_status_reject": true,
"independent_list_oracle_for_bounded_history": true
}
}
نمونهٔ آمادهشده در قالب JSON فشردهٔ UTF-8، ۵۰۸ بایت است. سقف قراردادی خروجی ۲۰۴۸ بایت انتخاب شده و یک نمونه با طولهای بیشینه و نقاط کد چهاربایتی، ۱۳۴۳ بایت دارد. اینها اندازهٔ بستههای آزمایشاند؛ سقف سرویس، توکن مدل یا اندازهٔ کل حافظه نیستند. کپی مستقلِ snapshot و مقایسهٔ ورودی نیز تغییر ناخواستهٔ تاریخچه یا بسته را بررسی میکنند.
۵. محدودیت متن و چهار پرسش پاسخدار

رشتهٔ پایتون دنبالهٔ نقاط کد Unicode است. برش در چهلوهشتمین نقطهٔ کد ممکن است نویسهٔ ترکیبی را جدا کند؛ برای مثال e بماند و علامت ترکیبی بعدش حذف شود. حفظ واژه، خوشهٔ نوشتاری یا معنای جمله تضمین نشده است. کنترلهای ASCII و surrogate رد میشوند، اما متن همچنان نیازمند بررسی مناسب کاربرد واقعی است.
پرسش اول: آیا completed در ماشین تحویل یعنی عمل قبلی انجام شده؟ پاسخ: خیر؛ مرحلهٔ تحویل و action_state جدا هستند. در تمام مراحل نمونه، وضعیت عمل unknown میماند. completed نیز فقط رویداد اعلامشدهٔ ساختگی است و تأیید واقعی کارشناس نداریم.
پرسش دوم: چرا قطعهٔ کوتاه میتواند با assistant شروع شود؟ پاسخ: چون سه نوبت آخرِ تاریخچهٔ معتبر را نگه میداریم. نقش و شمارهٔ اصلی حفظ میشوند؛ تبدیل نقش برای زیباترشدن خروجی، شاهد گفتگو را تغییر میدهد.
پرسش سوم: آیا فهرست فیلدهای مجاز، اطلاعات شخصی را حذف میکند؟ پاسخ: خیر؛ متن آزاد میتواند همان اطلاعات را درون خود داشته باشد. آزمون حتی نشان میدهد رشتهٔ عددیِ ساختگی در goal بدون پالایش باقی میماند. این کد ابزار تشخیص یا ناشناسسازی اطلاعات نیست.
پرسش چهارم: آیا offered اجازه میدهد به کاربر بگوییم کارشناس وصل شده؟ پاسخ: خیر؛ فقط قصد محلی ثبت شده است. ماشین حالت ورودی IVR نیز قرارداد جدا دارد. منابع در ۲۴ مهر ۱۴۰۵ خوانده شدند؛ صفحهٔ Microsoft بهروزرسانی ۹ اکتبر ۲۰۲۴ دارد. این تمرین آفلاین، بدون خبر تازه یا ادعای انتقال موفق واقعی است.
منابع و مطالعهٔ بیشتر
- Microsoft Bot FrameworkSDKv4؛ زمینه، رونوشت و رویداد وضعیت ارجاع
- JSON Schema؛ تفاوت properties، required و additionalProperties
- Python؛ تاریخچهٔ کراندار با deque و maxlen
- Python؛ رشته بهعنوان دنبالهٔ نقاط کد Unicode
بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.


