teren de test TypeSafe: docs offline + script de proba
Separat de produsele ROA. docs/ = documentatia oficiala descarcata ca Markdown (111 pagini), reluabila cu update_docs.sh. typesafe_test.py face un apel cu cate o intrebare din fiecare tip (choice/noul/score) pe o linie de factura de furnizor. Cheia API se ia din TYPESAFE_API_KEY, nu se versioneaza. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KHLUSsKP99G6ebv2fFUKQV
This commit is contained in:
328
docs/patterns/fan-out.md
Normal file
328
docs/patterns/fan-out.md
Normal file
@@ -0,0 +1,328 @@
|
||||
> ## Documentation Index
|
||||
> Fetch the complete documentation index at: https://docs.typesafe.ai/llms.txt
|
||||
> Use this file to discover all available pages before exploring further.
|
||||
|
||||
# Speculative fan-out
|
||||
|
||||
> Send many questions in a single call, including speculative ones, and let your code decide what's relevant.
|
||||
|
||||
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) {
|
||||
if (uncompressed == null) return "";
|
||||
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}
|
||||
</CodeBlock>
|
||||
<div className="pb-8">
|
||||
<a href={href} target="_blank" rel="noreferrer" className="text-primary">
|
||||
Try it in the Playground →
|
||||
</a>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
Because TypeSafe supports sending many questions in a single API call, we recommend putting all of the questions your system needs in a single request, and then using code to decide what is relevant after the fact. All questions are evaluated in parallel, so adding more questions to a call typically doesn't add any latency to the response.
|
||||
|
||||
## Example: support ticket triage
|
||||
|
||||
Let's imagine you are building a support system that needs to triage support tickets. You need to classify the ticket into a category. If it's a bug report, you also need to determine the severity of the bug.
|
||||
|
||||
Instead of asking for the category first and then the severity in a follow-up call, you can ask for both at the same time. If the ticket is not a bug report, you simply ignore the results of the bug severity question.
|
||||
|
||||
### Step 1: speculative fan-out
|
||||
|
||||
<TypesafeExample
|
||||
title="questions"
|
||||
display="questions"
|
||||
example={{
|
||||
state:
|
||||
"Hi, I placed an order (#98423) last Thursday and was charged twice. I also can't log in after the site update, and adding Apple Pay would be really helpful. This is getting frustrating.",
|
||||
questions: {
|
||||
category: {
|
||||
type: 'choice',
|
||||
instructions: 'Determine the broad category of this support ticket',
|
||||
criteria: {
|
||||
bug_report:
|
||||
'The user is reporting something that is broken or producing errors',
|
||||
billing: 'Charges, invoices, refunds, subscriptions',
|
||||
feature_request: 'The user is requesting new functionality',
|
||||
account: 'Login, permissions, profile, security',
|
||||
},
|
||||
},
|
||||
bug_severity: {
|
||||
type: 'score',
|
||||
instructions: 'How severe is the reported issue',
|
||||
criteria: [
|
||||
'Cosmetic; no impact to functionality',
|
||||
'Broken or degraded feature; workaround exists',
|
||||
'Blocking issue; no workaround exists',
|
||||
],
|
||||
},
|
||||
has_reproducible_steps: {
|
||||
type: 'noul',
|
||||
instructions:
|
||||
'The user describes specific steps to reproduce the issue',
|
||||
},
|
||||
refund_requested: {
|
||||
type: 'noul',
|
||||
instructions: 'The user is explicitly asking for a refund or credit',
|
||||
},
|
||||
frustration: {
|
||||
type: 'score',
|
||||
instructions: 'How frustrated the user appears',
|
||||
criteria: ['Calm, matter-of-fact', 'Frustrated but civil', 'Very angry'],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Note>
|
||||
**Speculative questions:** `bug_severity` and `has_reproducible_steps` only matter if the ticket is a bug report. `refund_requested` only matters for billing. We include all upfront because there is no speed cost for additional questions. If the ticket turns out to be a feature request, the bug severity result will be irrelevant, in which case your code path simply ignores it.
|
||||
</Note>
|
||||
|
||||
### Step 2: route with code
|
||||
|
||||
Your code decides what is relevant based on the classification result:
|
||||
|
||||
```python title="triage.py" theme={null}
|
||||
category = response.answers["category"]
|
||||
bug_severity = response.answers["bug_severity"]
|
||||
bug_repro = response.answers["has_reproducible_steps"]
|
||||
refund = response.answers["refund_requested"]
|
||||
frustration = response.answers["frustration"]
|
||||
|
||||
if category.choice == "bug_report":
|
||||
if bug_severity.score > 1.5 and bug_repro.noul > 0.6:
|
||||
escalate_to_engineering(ticket_id, severity="high")
|
||||
else:
|
||||
add_to_bug_backlog(ticket_id)
|
||||
|
||||
elif category.choice == "billing":
|
||||
if refund.noul > 0.7:
|
||||
route_to_billing_with_flag(ticket_id, refund_likely=True)
|
||||
else:
|
||||
route_to_billing(ticket_id)
|
||||
|
||||
elif category.choice == "feature_request":
|
||||
log_feature_request(ticket_id)
|
||||
|
||||
# Frustration is useful regardless of category
|
||||
if frustration.score > 1.5:
|
||||
flag_for_priority_response(ticket_id)
|
||||
```
|
||||
|
||||
Everything needed for the full decision tree comes from one call. Speculative questions are ignored when irrelevant and save a round trip when they are not.
|
||||
Reference in New Issue
Block a user