Как строить архитектуру с TypeSafe
Design AI-powered software by keeping code in control and giving System One narrow, structured decisions.
Проектируйте ПО на базе ИИ, сохраняя контроль в коде и поручая System One точечные структурированные решения.
export function TypesafeExample({example, display, title}) {
const keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
function compressToEncodedURIComponent(input) {
if (input == null) return "";
return _compress(input, 6, function (a) {
return keyStrUriSafe.charAt(a);
});
}
function _compress(uncompressed, bitsPerChar, getCharFromInt) {
var i, value, context_dictionary = {}, context_dictionaryToCreate = {}, context_c = "", context_wc = "", context_w = "", context_enlargeIn = 2, context_dictSize = 3, context_numBits = 2, context_data = [], context_data_val = 0, context_data_position = 0, ii;
for (ii = 0; ii < uncompressed.length; ii += 1) {
context_c = uncompressed.charAt(ii);
if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) {
context_dictionary[context_c] = context_dictSize++;
context_dictionaryToCreate[context_c] = true;
}
context_wc = context_w + context_c;
if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) {
context_w = context_wc;
} else {
if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
if (context_w.charCodeAt(0) < 256) {
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
}
value = context_w.charCodeAt(0);
for (i = 0; i < 8; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
} else {
value = 1;
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = 0;
}
value = context_w.charCodeAt(0);
for (i = 0; i < 16; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
delete context_dictionaryToCreate[context_w];
} else {
value = context_dictionary[context_w];
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
context_dictionary[context_wc] = context_dictSize++;
context_w = String(context_c);
}
}
if (context_w !== "") {
if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
if (context_w.charCodeAt(0) < 256) {
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
}
value = context_w.charCodeAt(0);
for (i = 0; i < 8; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
} else {
value = 1;
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = 0;
}
value = context_w.charCodeAt(0);
for (i = 0; i < 16; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
delete context_dictionaryToCreate[context_w];
} else {
value = context_dictionary[context_w];
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
}
value = 2;
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
while (true) {
context_data_val = context_data_val << 1;
if (context_data_position == bitsPerChar - 1) {
context_data.push(getCharFromInt(context_data_val));
break;
} else context_data_position++;
}
return context_data.join("");
}
function buildHref(ex) {
const documentText = ex.state === undefined ? "" : typeof ex.state === "string" ? ex.state : JSON.stringify(ex.state, null, 2);
return "https://console.typesafe.ai/decode#share/" + compressToEncodedURIComponent(JSON.stringify({
apiVersion: "v1",
documentText,
promptsText: JSON.stringify(ex.questions, null, 2),
selectedModels: ex.selectedModels
}));
}
const displayedExample = display === "questions" ? example.questions : example.state === undefined ? {
questions: example.questions
} : {
state: example.state,
questions: example.questions
};
const code = JSON.stringify(displayedExample, null, 2);
const href = buildHref(example);
return <div style={{
margin: "1.25rem 0"
}}>
<CodeBlock language="json" filename={title ?? "request"}>
{code}
}
System One — это модель TypeSafe для создания программного обеспечения на базе ИИ, а не автономных агентов. Она не генерирует код и не выбирает свое следующее действие самостоятельно. Она предоставляет AI-примитивы, которые встраиваются в программный код, поэтому логика остается под полным контролем программы, в то время как модель берет на себя вынесение здравых суждений над неструктурированными данными.
- Сохраняйте управление потоком выполнения (control flow), детерминированные правила и побочные эффекты в коде.
- Разделяйте обобщенные суждения на узкие типизированные вопросы с явными инструкциями и критериями.
- Передавайте в каждый вопрос только тот контекст, который ему необходим.
- Используйте вероятности и уверенность (confidence), чтобы принимать решения в коде, запрашивать ручную проверку или эскалировать кейс.
- Задавайте независимые вопросы одновременно, а затем объединяйте ответы в коде.
Три архитектуры программного обеспечения
TypeSafe спроектирован для создания программного обеспечения на базе ИИ (AI-powered software), в котором код управляет рабочим процессом, а ИИ решает точечные структурированные задачи.
Что делает System One компонуемой
Поскольку каждый ответ строго ограничен предоставленными вариантами, модель возвращает полное распределение вероятностей по этим вариантам вместо генерации значений вне заданной схемы. Цель TypeSafe — обеспечить превосходство в соотношении интеллекта к скорости и стоимости более чем в 100 раз; базовая ставка заключается в том, что более доступный интеллект создаст гораздо больший спрос.
Проектирование рабочего процесса с System One
<Accordion title="Пример: детерминированные правила в коде">
```python theme={null}
days_overdue = (today - invoice.due_date).days
if days_overdue > 30:
route_to_collections(invoice)
```
</Accordion>
Ознакомьтесь с [паттернами System One](/docs/patterns) для изучения надежных способов комбинирования решений модели с кодом.
<Accordion title="Пример: передача только релевантного контекста">
<TypesafeExample
title="request"
display="request"
example={{
state: {
ticket_message: 'My flight was cancelled. Can I get a refund?',
refund_policy: 'Cancelled flights are eligible for a full refund.',
},
selectedModels: ['jev-latest'],
questions: {
policy_supports_refund: {
type: 'noul',
instructions:
'Does the refund policy support the refund requested in the ticket?',
},
},
}}
/>
</Accordion>
<Accordion title="Пример: ссылка на вложенное значение">
Используйте путь через точку и индекс в обратных кавычках, чтобы адресовать вопрос к конкретному вложенному значению, например `support.tickets[0].message`.
<TypesafeExample
title="request"
display="request"
example={{
state: {
support: {
tickets: [
{ message: 'I was charged twice for order A-104.' },
{ message: 'How do I reset my password?' },
],
},
commerce: {
orders: [
{
id: 'A-104',
charges: [
{ amount_usd: 49, status: 'captured' },
{ amount_usd: 49, status: 'captured' },
],
},
],
},
account: {
security: {
password_reset:
'Email a reset link to the address on file.',
},
},
},
selectedModels: ['jev-latest'],
questions: {
duplicate_charge: {
type: 'noul',
instructions:
'Do `support.tickets[0].message` and `commerce.orders[0].charges` indicate a duplicate charge?',
},
password_reset_supported: {
type: 'noul',
instructions:
'Can `account.security.password_reset` resolve the request in `support.tickets[1].message`?',
},
},
}}
/>
</Accordion>
<Info>
Это, пожалуй, важнейшая концепция данного руководства. Широкие вопросы скрывают множество суждений за одним ответом. Атомарные вопросы делают эти суждения прозрачными, позволяя анализировать, настраивать и комбинировать их в коде.
</Info>
<Accordion title="Пример: декомпозиция обнаружения спама">
<TypesafeExample
title="Один широкий вопрос (плохо)"
display="questions"
example={{
state: {
message: {
sender: {
display_name: 'Acme Payroll',
email: 'rewards@claim-bonus.example',
},
subject: 'Urgent: claim your employee bonus',
body:
'You have been selected for a $1,000 bonus. Confirm your payroll password today to receive it.',
links: [
{
text: 'Claim bonus',
url: 'http://claim-bonus.example/acme',
},
],
},
},
selectedModels: ['jev-latest'],
questions: {
is_spam: {
type: 'noul',
instructions: 'Is `message` spam?',
},
},
}}
/>
<TypesafeExample
title="Декомпозированные вопросы (хорошо)"
display="questions"
example={{
state: {
message: {
sender: {
display_name: 'Acme Payroll',
email: 'rewards@claim-bonus.example',
},
subject: 'Urgent: claim your employee bonus',
body:
'You have been selected for a $1,000 bonus. Confirm your payroll password today to receive it.',
links: [
{
text: 'Claim bonus',
url: 'http://claim-bonus.example/acme',
},
],
},
},
selectedModels: ['jev-latest'],
questions: {
requests_credentials: {
type: 'noul',
instructions:
'Does `message.body` ask the recipient to provide a password or other login credential?',
},
offers_unexpected_reward: {
type: 'noul',
instructions:
'Does `message.body` claim the recipient received an unexpected prize, payment, or reward?',
},
creates_time_pressure: {
type: 'noul',
instructions:
'Does `message.subject` or `message.body` pressure the recipient to act quickly?',
},
sender_identity_mismatch: {
type: 'noul',
instructions:
'Does the organization named in `message.sender.display_name` conflict with the domain in `message.sender.email`?',
},
link_domain_mismatch: {
type: 'noul',
instructions:
'Does the domain in `message.links[0].url` conflict with the organization named in `message.sender.display_name`?',
},
disguises_link_destination: {
type: 'noul',
instructions:
'Does `message.links[0].text` conceal or misrepresent the destination in `message.links[0].url`?',
},
},
}}
/>
</Accordion>
<Accordion title="Пример: верификация трассировки вызовов инструментов">
<TypesafeExample
title="Один широкий вопрос (плохо)"
display="questions"
example={{
state: {
request: {
text: "What's the weather in Seattle tomorrow in Fahrenheit?",
location: 'Seattle, WA',
date: '2026-09-03',
unit: 'fahrenheit',
},
available_tools: {
geocode_city: {
description: 'Resolve a city to latitude and longitude.',
parameters: { city: 'string' },
},
get_weather: {
description: 'Get the forecast for coordinates and a date.',
parameters: {
latitude: 'number',
longitude: 'number',
date: 'YYYY-MM-DD',
unit: ['fahrenheit', 'celsius'],
},
},
},
trace: {
tool_calls: [
{
id: 'call_1',
name: 'geocode_city',
arguments: { city: 'Seattle, WA' },
},
{
id: 'call_2',
name: 'get_weather',
arguments: {
latitude: 47.6062,
longitude: -122.3321,
date: '2026-09-03',
unit: 'celsius',
},
},
],
tool_results: [
{
tool_call_id: 'call_1',
output: { latitude: 47.6062, longitude: -122.3321 },
},
],
},
},
selectedModels: ['jev-latest'],
questions: {
tool_calls_are_correct: {
type: 'noul',
instructions:
'Is `trace.tool_calls` correct for `request` and `available_tools`?',
},
},
}}
/>
<TypesafeExample
title="Декомпозированные вопросы (хорошо)"
display="questions"
example={{
state: {
request: {
text: "What's the weather in Seattle tomorrow in Fahrenheit?",
location: 'Seattle, WA',
date: '2026-09-03',
unit: 'fahrenheit',
},
available_tools: {
geocode_city: {
description: 'Resolve a city to latitude and longitude.',
parameters: { city: 'string' },
},
get_weather: {
description: 'Get the forecast for coordinates and a date.',
parameters: {
latitude: 'number',
longitude: 'number',
date: 'YYYY-MM-DD',
unit: ['fahrenheit', 'celsius'],
},
},
},
trace: {
tool_calls: [
{
id: 'call_1',
name: 'geocode_city',
arguments: { city: 'Seattle, WA' },
},
{
id: 'call_2',
name: 'get_weather',
arguments: {
latitude: 47.6062,
longitude: -122.3321,
date: '2026-09-03',
unit: 'celsius',
},
},
],
tool_results: [
{
tool_call_id: 'call_1',
output: { latitude: 47.6062, longitude: -122.3321 },
},
],
},
},
selectedModels: ['jev-latest'],
questions: {
geocode_tool_is_relevant: {
type: 'noul',
instructions:
'Is `trace.tool_calls[0].name` an appropriate tool for resolving `request.location`?',
},
geocode_location_matches: {
type: 'noul',
instructions:
'Does `trace.tool_calls[0].arguments.city` match `request.location`?',
},
geocode_arguments_match_schema: {
type: 'noul',
instructions:
'Does `trace.tool_calls[0].arguments` conform to `available_tools.geocode_city.parameters`?',
},
geocode_result_matches_call: {
type: 'noul',
instructions:
'Does `trace.tool_results[0].tool_call_id` match `trace.tool_calls[0].id`?',
},
weather_tool_is_relevant: {
type: 'noul',
instructions:
'Is `trace.tool_calls[1].name` an appropriate tool for answering `request.text`?',
},
weather_arguments_match_schema: {
type: 'noul',
instructions:
'Does `trace.tool_calls[1].arguments` conform to `available_tools.get_weather.parameters`?',
},
weather_uses_geocoded_coordinates: {
type: 'noul',
instructions:
'Do the coordinates in `trace.tool_calls[1].arguments` match those in `trace.tool_results[0].output`?',
},
weather_date_matches: {
type: 'noul',
instructions:
'Does `trace.tool_calls[1].arguments.date` match `request.date`?',
},
weather_unit_matches: {
type: 'noul',
instructions:
'Does `trace.tool_calls[1].arguments.unit` match `request.unit`?',
},
},
}}
/>
</Accordion>
Структурирование полезно в следующих ситуациях:
* Вопросу требуется контекст или примеры. Длинное описание предыстории или список примеров входных данных лучше вынести в именованные поля рядом с вопросом, где ваш код сможет дополнять или изменять их без переписывания самого вопроса.
* Часть вопроса поступает из вашего кода. Когда значение берется из базы данных, вынесите его в отдельное поле вместо конкатенации в строковый шаблон.
* Несколько вопросов содержат похожие инструкции. Запрос принимает одно состояние и может содержать множество вопросов. Добавление структурированных данных помогает четко разграничить вопросы.
<Accordion title="Пример: ссылка на запись из вашего кода">
Этот вопрос типа Noul сравнивает резюме из состояния с записью из базы данных кандидатов. Запись передается в поле `potential_duplicate` как есть, а вопрос ссылается на нее по имени.
<TypesafeExample
title="questions"
display="questions"
example={{
state: {
resume: {
name: 'John Smith',
location: 'Oakland, CA',
summary: 'Backend engineer with eight years of Python and Go experience.',
experience: [
{ employer: 'Google', title: 'Senior Backend Engineer', years: '2021-2025' },
{ employer: 'Microsoft', title: 'Software Engineer', years: '2017-2021' },
],
},
},
selectedModels: ['jev-latest'],
questions: {
same_as_record_18: {
type: 'noul',
instructions: {
potential_duplicate: { name: 'John Smith', location: 'Oakland, California', last_employer: 'Google' },
question: 'Is the resume for the same person as `potential_duplicate`?',
},
},
},
}}
/>
</Accordion>
Данные `potential_duplicate`, полученные из кода, могут меняться со временем. Вопрос `question` ссылается на них с помощью обратных кавычек.
Описания внутри `criteria` также могут быть объектами. Для Choice описание каждого варианта может быть объектом, в котором указано, что охватывает этот вариант, что относится к другим вариантам, и приведено несколько примеров. Используйте одинаковые имена полей для всех вариантов, чтобы модель могла напрямую сопоставлять их.
<Accordion title="Пример: контрастные критерии для Choice">
<TypesafeExample
title="questions"
display="questions"
example={{
state: 'How many disposable virtual cards can I make per day?',
selectedModels: ['jev-latest'],
questions: {
card_help_topic: {
type: 'choice',
instructions: {
question:
'Which disposable virtual card topic is the user asking about?',
focus: 'Classify the information the user wants.',
},
criteria: {
get_disposable_virtual_card: {
what: 'Purpose, eligibility, or setup',
not_for: 'Quantity, transaction, or merchant restrictions',
examples: [
'How can I get a disposable virtual card?',
'What are disposable cards for?',
],
},
disposable_card_limits: {
what: 'Quantity, transaction, or merchant restrictions',
not_for: 'Purpose, eligibility, or setup',
examples: [
'How many disposable cards can I make per day?',
'Where can I use a disposable card?',
],
},
},
},
},
}}
/>
</Accordion>
На странице каждого типа примитива приведен подробный пример:
* [Noul](/docs/primitives/noul#structured-instructions) сравнивает одно резюме с несколькими записями кандидатов, по одному вопросу на запись, при этом вопросы формируются в коде.
* [Choice](/docs/primitives/choice#structured-instructions-and-criteria) описывает два легко путаемых варианта с указанием того, что охватывает каждый из них, для чего он не предназначен, и примерами.
* [Score](/docs/primitives/score#structured-level-descriptions) задает для каждого уровня описание и примеры ситуаций.
В [кукбуке по каскадному извлечению структурированных данных](/docs/cookbooks/sde_cascade) показан сценарий использования общих формулировок с серией одинаковых вопросов к каждому полю извлеченной записи.
Короткий однозначный вопрос или критерий может оставаться простой строкой. Добавляйте структуру тогда, когда это помогает разделить указания, которые иначе могли бы смешаться. Полный список мест, где допускается использование структуры, см. в разделе [Продвинутый уровень: структура](/docs/primitives/advanced).
См. паттерн [Спекулятивное ветвление (Speculative Fan-Out)](/docs/patterns/fan-out) и [Кукбук по параллельным вопросам](/docs/cookbooks/parallel_questions).
<Accordion title="Пример: объединение сигналов во взвешенную оценку">
```python theme={null}
answers = response.answers
# Объединение независимых сигналов в единую оценку для приложения.
quality = (
0.4 * answers["answers_request"].noul
+ 0.4 * answers["citations_are_supported"].noul
+ 0.2 * (1 - answers["contradicts_context"].noul)
)
```
</Accordion>
В разделе [Композитная оценка (Composite Scoring)](/docs/patterns/composite-scoring) показано, как сохранить детальность отдельных оценок при их объединении. Если у вас нет разметки для обучения downstream-модели, используйте ансамбль более тяжелых reasoning-моделей для ее генерации; в [кукбуке AutoResearch](/docs/cookbooks/autoresearch_feature_discovery) продемонстрировано обучение классической модели на результатах System One.
<Accordion title="Пример: маршрутизация по уверенности">
```python theme={null}
answer = response.answers["card_help_topic"]
if answer.confidence < 0.8:
route_to_human_review(ticket)
else:
route_to_handler(answer.choice, ticket)
```
</Accordion>
О выборе пороговых значений и сопоставлении их с рисками каждого действия см. разделы [Уверенность (Confidence)](/docs/confidence) и [Маршрутизация с контролем уверенности](/docs/patterns/confidence-routing).
Собираем всё воедино
Этот рабочий процесс обработки обращений в службу поддержки сохраняет детерминированную логику в коде, отправляет только релевантный структурированный контекст, оценивает множество атомарных вопросов в одном запросе и объединяет ответы с явными проверками уверенности.
from typesafe_sdk import Choice, Noul, NoulCriteria, Score, TypeSafeClient
def triage_ticket(ticket, customer):
# Обработка детерминированных состояний без вызова модели.
if ticket["status"] == "closed":
return "no_action"
open_orders = [
order for order in customer["orders"] if order["status"] != "delivered"
]
# Включаем только структурированный контекст, необходимый для вопросов ниже.
state = {
"ticket": {
"message": ticket["message"],
"sender": ticket["sender"],
"links": ticket["links"],
},
"customer": {
"plan": customer["plan"],
"open_orders": open_orders,
},
"policy": {
"sensitive_credentials": ["password", "security code", "API key"],
},
}
# Задаем структурированные атомарные вопросы вместе для параллельного выполнения.
questions = {
"topic": Choice(
instructions={
"question": "Which team should handle `ticket.message`?",
"focus": "Classify the customer's primary request.",
},
criteria={
"billing": {
"what": "Charges, invoices, refunds, or subscriptions",
"not_for": "Order tracking or account access",
"examples": ["I was charged twice", "Where is my refund?"],
},
"orders": {
"what": "Order status, delivery, cancellation, or returns",
"not_for": "Charges or account access",
"examples": ["Where is my order?", "Cancel my shipment"],
},
"account": {
"what": "Login, profile, permissions, or security",
"not_for": "Charges or order tracking",
"examples": ["Reset my password", "I cannot sign in"],
},
},
),
"requests_credentials": Noul(
instructions={
"question": "Does the message request a sensitive credential?",
"compare": [
"`ticket.message`",
"`policy.sensitive_credentials`",
],
"focus": "Look for a request to disclose the credential itself.",
},
criteria=NoulCriteria(
true={
"what": "Asks the recipient to disclose a listed credential",
"examples": [
"Reply with your password",
"Send us your API key",
],
},
false={
"what": "Does not ask the recipient to disclose a credential",
"not_for": "A legitimate instruction to reset a credential",
"examples": ["Use this link to reset your password"],
},
),
),
"sender_identity_mismatch": Noul(
instructions={
"question": "Does the claimed sender identity conflict with its domain?",
"compare": [
"`ticket.sender.display_name`",
"`ticket.sender.email`",
],
"focus": "Compare the named organization with the email domain.",
},
criteria=NoulCriteria(
true={
"what": "Claims an organization unrelated to the email domain",
"examples": ["Acme Payroll sent from claim-bonus.example"],
},
false={
"what": "The identity and domain agree or make no conflicting claim",
"examples": ["Acme Payroll sent from acme.example"],
},
),
),
"unexpected_reward": Noul(
instructions={
"question": "Does the message announce an unexpected reward?",
"inspect": "`ticket.message`",
"focus": "Look for an unsolicited prize, payment, or reward claim.",
},
criteria=NoulCriteria(
true={
"what": "Announces an unrequested prize, payment, or reward",
"examples": ["You were selected for a $1,000 bonus"],
},
false={
"what": "Contains no reward claim or discusses an expected payment",
"not_for": "A customer asking about a known refund or payroll deposit",
"examples": ["When will my approved refund arrive?"],
},
),
),
"refund_requested": Noul(
instructions={
"question": "Does the customer explicitly request a refund or credit?",
"inspect": "`ticket.message`",
"focus": "Require a requested remedy, not a billing complaint alone.",
},
criteria=NoulCriteria(
true={
"what": "Directly asks for money back or an account credit",
"examples": ["Please refund the duplicate charge"],
},
false={
"what": "Does not ask for a refund or credit",
"not_for": "A complaint or billing question without a requested remedy",
"examples": ["Why was I charged twice?"],
},
),
),
"mentions_open_order": Noul(
instructions={
"question": "Does the message refer to a supplied open order?",
"compare": [
"`ticket.message`",
"`customer.open_orders`",
],
"focus": "Match an order id or other identifying details.",
},
criteria=NoulCriteria(
true={
"what": "Refers to an open order by id or identifying details",
"examples": ["Where is order A-104?"],
},
false={
"what": "Does not identify any supplied open order",
"not_for": "A generic order question with no matching details",
"examples": ["How long does shipping usually take?"],
},
),
),
"frustration": Score(
instructions={
"question": "How frustrated does the customer appear?",
"inspect": "`ticket.message`",
"focus": "Judge expressed frustration, not issue severity.",
},
criteria=[
{
"what": "Calm and matter-of-fact",
"signals": ["Neutral wording", "No complaint about the experience"],
},
{
"what": "Frustrated but civil",
"signals": ["Expresses annoyance", "Remains constructive"],
},
{
"what": "Very angry or threatening to leave",
"signals": ["Hostile language", "Threatens cancellation or churn"],
},
],
),
}
with TypeSafeClient() as client:
response = client.system_one(
state=state,
questions=questions,
)
# Объединяем независимые сигналы спама с весами, заданными в коде.
answers = response.answers
spam_risk = (
0.45 * answers["requests_credentials"].noul
+ 0.30 * answers["sender_identity_mismatch"].noul
+ 0.25 * answers["unexpected_reward"].noul
)
# Эскалируем неуверенные суждения вместо угадывания.
spam_is_uncertain = 0.4 < spam_risk < 0.6
if spam_is_uncertain or answers["topic"].confidence < 0.75:
return route_to_human_review(ticket)
if spam_risk >= 0.6:
return quarantine_as_spam(ticket)
# Позволяем коду решить, какие спекулятивные ответы важны на этой ветке.
if answers["topic"].choice == "billing":
return route_to_billing(
ticket,
refund_requested=answers["refund_requested"].noul >= 0.7,
)
if answers["topic"].choice == "orders":
return route_to_orders(
ticket,
mentions_open_order=answers["mentions_open_order"].noul >= 0.7,
)
priority = (
"high"
if answers["frustration"].confidence >= 0.7
and answers["frustration"].score >= 1.5
else "normal"
)
return route_to_account_support(ticket, priority=priority)