@librechat/agents 4.0.1 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/decisions/deadline.cjs +52 -0
- package/dist/cjs/decisions/deadline.cjs.map +1 -0
- package/dist/cjs/decisions/dialect.cjs +88 -0
- package/dist/cjs/decisions/dialect.cjs.map +1 -0
- package/dist/cjs/decisions/http.cjs +77 -0
- package/dist/cjs/decisions/http.cjs.map +1 -0
- package/dist/cjs/decisions/index.cjs +7 -0
- package/dist/cjs/decisions/presets.cjs +82 -0
- package/dist/cjs/decisions/presets.cjs.map +1 -0
- package/dist/cjs/decisions/questions.cjs +70 -0
- package/dist/cjs/decisions/questions.cjs.map +1 -0
- package/dist/cjs/decisions/structuredChat.cjs +178 -0
- package/dist/cjs/decisions/structuredChat.cjs.map +1 -0
- package/dist/cjs/decisions/traceMarker.cjs +6 -0
- package/dist/cjs/decisions/traceMarker.cjs.map +1 -0
- package/dist/cjs/decisions/transport.cjs +166 -0
- package/dist/cjs/decisions/transport.cjs.map +1 -0
- package/dist/cjs/decisions/types.cjs +44 -0
- package/dist/cjs/decisions/types.cjs.map +1 -0
- package/dist/cjs/graphs/Graph.cjs +4 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +6 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/main.cjs +39 -0
- package/dist/cjs/rerank/index.cjs +2 -0
- package/dist/cjs/rerank/search.cjs +45 -0
- package/dist/cjs/rerank/search.cjs.map +1 -0
- package/dist/cjs/rerank/systemone.cjs +70 -0
- package/dist/cjs/rerank/systemone.cjs.map +1 -0
- package/dist/cjs/run.cjs +4 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +1 -0
- package/dist/cjs/tools/search/search.cjs +3 -3
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +4 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +11 -0
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +87 -14
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/diagnostics.cjs +45 -2
- package/dist/cjs/tools/subagent/diagnostics.cjs.map +1 -1
- package/dist/cjs/tools/subagent/index.cjs +1 -0
- package/dist/esm/decisions/deadline.mjs +52 -0
- package/dist/esm/decisions/deadline.mjs.map +1 -0
- package/dist/esm/decisions/dialect.mjs +87 -0
- package/dist/esm/decisions/dialect.mjs.map +1 -0
- package/dist/esm/decisions/http.mjs +75 -0
- package/dist/esm/decisions/http.mjs.map +1 -0
- package/dist/esm/decisions/index.mjs +8 -0
- package/dist/esm/decisions/presets.mjs +77 -0
- package/dist/esm/decisions/presets.mjs.map +1 -0
- package/dist/esm/decisions/questions.mjs +67 -0
- package/dist/esm/decisions/questions.mjs.map +1 -0
- package/dist/esm/decisions/structuredChat.mjs +178 -0
- package/dist/esm/decisions/structuredChat.mjs.map +1 -0
- package/dist/esm/decisions/traceMarker.mjs +6 -0
- package/dist/esm/decisions/traceMarker.mjs.map +1 -0
- package/dist/esm/decisions/transport.mjs +164 -0
- package/dist/esm/decisions/transport.mjs.map +1 -0
- package/dist/esm/decisions/types.mjs +39 -0
- package/dist/esm/decisions/types.mjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +4 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +6 -1
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/main.mjs +13 -1
- package/dist/esm/rerank/index.mjs +3 -0
- package/dist/esm/rerank/search.mjs +45 -0
- package/dist/esm/rerank/search.mjs.map +1 -0
- package/dist/esm/rerank/systemone.mjs +70 -0
- package/dist/esm/rerank/systemone.mjs.map +1 -0
- package/dist/esm/run.mjs +4 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +1 -1
- package/dist/esm/tools/search/search.mjs +3 -3
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +4 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +11 -0
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +88 -15
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/diagnostics.mjs +44 -3
- package/dist/esm/tools/subagent/diagnostics.mjs.map +1 -1
- package/dist/esm/tools/subagent/index.mjs +1 -0
- package/dist/types/decisions/deadline.d.ts +3 -0
- package/dist/types/decisions/dialect.d.ts +11 -0
- package/dist/types/decisions/http.d.ts +22 -0
- package/dist/types/decisions/index.d.ts +10 -0
- package/dist/types/decisions/presets.d.ts +17 -0
- package/dist/types/decisions/questions.d.ts +8 -0
- package/dist/types/decisions/structuredChat.d.ts +16 -0
- package/dist/types/decisions/traceMarker.d.ts +2 -0
- package/dist/types/decisions/transport.d.ts +29 -0
- package/dist/types/decisions/types.d.ts +119 -0
- package/dist/types/graphs/Graph.d.ts +2 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/rerank/index.d.ts +4 -0
- package/dist/types/rerank/search.d.ts +7 -0
- package/dist/types/rerank/systemone.d.ts +13 -0
- package/dist/types/rerank/types.d.ts +24 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/search/types.d.ts +3 -1
- package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +4 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +13 -0
- package/dist/types/tools/subagent/diagnostics.d.ts +32 -8
- package/dist/types/tools/subagent/index.d.ts +2 -0
- package/dist/types/types/graph.d.ts +3 -0
- package/dist/types/types/run.d.ts +2 -0
- package/package.json +1 -1
- package/src/decisions/deadline.ts +92 -0
- package/src/decisions/dialect.ts +200 -0
- package/src/decisions/http.ts +175 -0
- package/src/decisions/index.ts +14 -0
- package/src/decisions/presets.ts +111 -0
- package/src/decisions/questions.ts +128 -0
- package/src/decisions/structuredChat.ts +364 -0
- package/src/decisions/traceMarker.ts +2 -0
- package/src/decisions/transport.ts +335 -0
- package/src/decisions/types.ts +193 -0
- package/src/graphs/Graph.ts +4 -0
- package/src/index.ts +2 -0
- package/src/langfuseToolOutputTracing.ts +8 -1
- package/src/rerank/index.ts +4 -0
- package/src/rerank/search.ts +85 -0
- package/src/rerank/systemone.ts +140 -0
- package/src/rerank/types.ts +28 -0
- package/src/run.ts +4 -0
- package/src/tools/search/search.ts +4 -4
- package/src/tools/search/tool.ts +27 -25
- package/src/tools/search/types.ts +4 -1
- package/src/tools/subagent/SubagentExecutionRegistry.ts +21 -0
- package/src/tools/subagent/SubagentExecutor.ts +190 -22
- package/src/tools/subagent/diagnostics.ts +88 -9
- package/src/tools/subagent/index.ts +11 -0
- package/src/types/graph.ts +3 -0
- package/src/types/run.ts +2 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DecisionAnswer,
|
|
3
|
+
DecisionDialect,
|
|
4
|
+
DecisionQuestion,
|
|
5
|
+
} from './types';
|
|
6
|
+
import { isDecisionObject } from './types';
|
|
7
|
+
|
|
8
|
+
interface WireQuestion {
|
|
9
|
+
type: string;
|
|
10
|
+
instructions: DecisionQuestion['instructions'];
|
|
11
|
+
criteria?: DecisionQuestion['criteria'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface WireAnswer {
|
|
15
|
+
type?: unknown;
|
|
16
|
+
probability?: unknown;
|
|
17
|
+
noul?: unknown;
|
|
18
|
+
choice?: unknown;
|
|
19
|
+
score?: unknown;
|
|
20
|
+
confidence?: unknown;
|
|
21
|
+
probabilities?: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** System One calls a yes/no question a `noul`; both dialects use a `{true, false}` criterion. */
|
|
25
|
+
export function toWireQuestion(
|
|
26
|
+
question: DecisionQuestion,
|
|
27
|
+
dialect: DecisionDialect
|
|
28
|
+
): WireQuestion {
|
|
29
|
+
const type =
|
|
30
|
+
dialect === 'systemone' && question.type === 'boolean'
|
|
31
|
+
? 'noul'
|
|
32
|
+
: question.type;
|
|
33
|
+
if (question.criteria == null) {
|
|
34
|
+
return { type, instructions: question.instructions };
|
|
35
|
+
}
|
|
36
|
+
const criteria =
|
|
37
|
+
question.type === 'boolean' && typeof question.criteria === 'string'
|
|
38
|
+
? { true: question.criteria }
|
|
39
|
+
: question.criteria;
|
|
40
|
+
return { type, instructions: question.instructions, criteria };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isProbability(value: unknown): value is number {
|
|
44
|
+
return (
|
|
45
|
+
typeof value === 'number' &&
|
|
46
|
+
Number.isFinite(value) &&
|
|
47
|
+
value >= 0 &&
|
|
48
|
+
value <= 1
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `undefined` means malformed; `null` means no distribution was reported. */
|
|
53
|
+
function probabilitiesOf(
|
|
54
|
+
raw: unknown,
|
|
55
|
+
answerType: 'choice' | 'score',
|
|
56
|
+
question?: DecisionQuestion,
|
|
57
|
+
selectedChoice?: string
|
|
58
|
+
): Record<string, number> | null | undefined {
|
|
59
|
+
if (raw == null) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (!isDecisionObject(raw)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const entries = Object.entries(raw);
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (
|
|
70
|
+
(question?.type === 'choice' &&
|
|
71
|
+
entries.length !== Object.keys(question.criteria).length) ||
|
|
72
|
+
(question?.type === 'score' && entries.length !== question.criteria.length)
|
|
73
|
+
) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const probabilities: Record<string, number> = Object.create(null);
|
|
77
|
+
let total = 0;
|
|
78
|
+
let maximum = 0;
|
|
79
|
+
for (const [key, value] of entries) {
|
|
80
|
+
if (!isProbability(value)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
if (question?.type === 'choice' && !Object.hasOwn(question.criteria, key)) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
answerType === 'score' &&
|
|
88
|
+
(!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= entries.length)
|
|
89
|
+
) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
probabilities[key] = value;
|
|
93
|
+
total += value;
|
|
94
|
+
maximum = Math.max(maximum, value);
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
selectedChoice !== undefined &&
|
|
98
|
+
(!Object.hasOwn(probabilities, selectedChoice) ||
|
|
99
|
+
probabilities[selectedChoice] < maximum)
|
|
100
|
+
) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const roundingTolerance = Math.min(
|
|
104
|
+
0.02,
|
|
105
|
+
Math.max(0.01, entries.length * 0.00005 + 0.001)
|
|
106
|
+
);
|
|
107
|
+
return Math.abs(total - 1) <= roundingTolerance ? probabilities : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A validated answer, or `null` for one that cannot safely be read. */
|
|
111
|
+
export function readAnswer(
|
|
112
|
+
answer: unknown,
|
|
113
|
+
dialect: DecisionDialect,
|
|
114
|
+
question?: DecisionQuestion
|
|
115
|
+
): DecisionAnswer | null {
|
|
116
|
+
if (!isDecisionObject(answer)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const record: WireAnswer = answer;
|
|
120
|
+
const booleanType = dialect === 'systemone' ? 'noul' : 'boolean';
|
|
121
|
+
const probability =
|
|
122
|
+
dialect === 'systemone' ? record.noul : record.probability;
|
|
123
|
+
if (record.type === booleanType) {
|
|
124
|
+
if (
|
|
125
|
+
!isProbability(probability) ||
|
|
126
|
+
(question != null && question.type !== 'boolean')
|
|
127
|
+
) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return { type: 'boolean', probability };
|
|
131
|
+
}
|
|
132
|
+
if (record.type !== 'choice' && record.type !== 'score') {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const confidence = record.confidence;
|
|
136
|
+
if (confidence != null && !isProbability(confidence)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const probabilities = probabilitiesOf(
|
|
140
|
+
record.probabilities,
|
|
141
|
+
record.type,
|
|
142
|
+
question,
|
|
143
|
+
record.type === 'choice' && typeof record.choice === 'string'
|
|
144
|
+
? record.choice
|
|
145
|
+
: undefined
|
|
146
|
+
);
|
|
147
|
+
if (probabilities === undefined) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (record.type === 'choice') {
|
|
151
|
+
if (
|
|
152
|
+
typeof record.choice !== 'string' ||
|
|
153
|
+
(question != null &&
|
|
154
|
+
(question.type !== 'choice' ||
|
|
155
|
+
!Object.hasOwn(question.criteria, record.choice)))
|
|
156
|
+
) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
type: 'choice',
|
|
161
|
+
choice: record.choice,
|
|
162
|
+
confidence: confidence ?? null,
|
|
163
|
+
probabilities,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
typeof record.score !== 'number' ||
|
|
168
|
+
!Number.isFinite(record.score) ||
|
|
169
|
+
record.score < 0 ||
|
|
170
|
+
(question != null &&
|
|
171
|
+
(question.type !== 'score' ||
|
|
172
|
+
record.score > question.criteria.length - 1))
|
|
173
|
+
) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (probabilities !== null) {
|
|
177
|
+
let weightedScore = 0;
|
|
178
|
+
let levels = 0;
|
|
179
|
+
for (const [level, probability] of Object.entries(probabilities)) {
|
|
180
|
+
weightedScore += Number(level) * probability;
|
|
181
|
+
levels += 1;
|
|
182
|
+
}
|
|
183
|
+
const roundingTolerance = Math.min(
|
|
184
|
+
0.02,
|
|
185
|
+
0.00005 * (1 + (levels * (levels - 1)) / 2) + 0.001
|
|
186
|
+
);
|
|
187
|
+
if (
|
|
188
|
+
!Number.isFinite(weightedScore) ||
|
|
189
|
+
Math.abs(record.score - weightedScore) > roundingTolerance
|
|
190
|
+
) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
type: 'score',
|
|
196
|
+
score: record.score,
|
|
197
|
+
confidence: confidence ?? null,
|
|
198
|
+
probabilities,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DecisionModel,
|
|
3
|
+
DecisionAnswer,
|
|
4
|
+
DecisionResult,
|
|
5
|
+
DecisionRequest,
|
|
6
|
+
DecisionDialect,
|
|
7
|
+
DecisionCredential,
|
|
8
|
+
DecisionQuestion,
|
|
9
|
+
} from './types';
|
|
10
|
+
import type { DecisionFetch } from './transport';
|
|
11
|
+
import { DecisionError, isDecisionObject, readDecisionUsage } from './types';
|
|
12
|
+
import { validateDecisionQuestions } from './questions';
|
|
13
|
+
import { toWireQuestion, readAnswer } from './dialect';
|
|
14
|
+
import { createTransport } from './transport';
|
|
15
|
+
|
|
16
|
+
export const HTTP_PROVIDER_ID = 'http';
|
|
17
|
+
|
|
18
|
+
export interface HttpDecisionModelOptions {
|
|
19
|
+
providerId?: string;
|
|
20
|
+
apiKey?: DecisionCredential;
|
|
21
|
+
requiresAuth?: boolean;
|
|
22
|
+
/** Full URL, not a base path. */
|
|
23
|
+
endpoint: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
dialect?: DecisionDialect;
|
|
26
|
+
requestKey?: string;
|
|
27
|
+
responseKey?: string;
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
maxRetries?: number;
|
|
30
|
+
fetch?: DecisionFetch;
|
|
31
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
32
|
+
onAnswered?: (label: string, ms: number) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseEnvelope(
|
|
36
|
+
body: string,
|
|
37
|
+
providerId: string,
|
|
38
|
+
readOne: (
|
|
39
|
+
answer: unknown,
|
|
40
|
+
question?: DecisionQuestion
|
|
41
|
+
) => DecisionAnswer | null,
|
|
42
|
+
responseKey?: string,
|
|
43
|
+
expected?: Record<string, DecisionQuestion>
|
|
44
|
+
): DecisionResult {
|
|
45
|
+
let parsed: unknown;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(body);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new DecisionError('malformed_response', 'response was not JSON', {
|
|
50
|
+
provider: providerId,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!isDecisionObject(parsed)) {
|
|
54
|
+
throw new DecisionError(
|
|
55
|
+
'malformed_response',
|
|
56
|
+
'response was not an object',
|
|
57
|
+
{
|
|
58
|
+
provider: providerId,
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const unwrapped =
|
|
63
|
+
responseKey != null && responseKey !== '' ? parsed[responseKey] : parsed;
|
|
64
|
+
if (
|
|
65
|
+
!isDecisionObject(unwrapped) ||
|
|
66
|
+
!isDecisionObject(unwrapped.answers) ||
|
|
67
|
+
Object.keys(unwrapped.answers).length === 0
|
|
68
|
+
) {
|
|
69
|
+
throw new DecisionError(
|
|
70
|
+
'malformed_response',
|
|
71
|
+
'response carried no answers',
|
|
72
|
+
{
|
|
73
|
+
provider: providerId,
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const answers: DecisionResult['answers'] = Object.create(null);
|
|
79
|
+
for (const [id, answer] of Object.entries(unwrapped.answers)) {
|
|
80
|
+
if (expected && !Object.hasOwn(expected, id)) {
|
|
81
|
+
throw new DecisionError(
|
|
82
|
+
'malformed_response',
|
|
83
|
+
'response carried an unknown answer',
|
|
84
|
+
{
|
|
85
|
+
provider: providerId,
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const mapped = readOne(answer, expected?.[id]);
|
|
90
|
+
if (!mapped) {
|
|
91
|
+
throw new DecisionError(
|
|
92
|
+
'malformed_response',
|
|
93
|
+
'response carried an invalid answer',
|
|
94
|
+
{
|
|
95
|
+
provider: providerId,
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
answers[id] = mapped;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
model: typeof unwrapped.model === 'string' ? unwrapped.model : 'unknown',
|
|
104
|
+
answers,
|
|
105
|
+
usage: readDecisionUsage(unwrapped.usage),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Jev and Laya use the same System One HTTP dialect, but may report different confidence metrics. */
|
|
110
|
+
export function createHttpDecisionModel(
|
|
111
|
+
options: HttpDecisionModelOptions
|
|
112
|
+
): DecisionModel {
|
|
113
|
+
const providerId = options.providerId ?? HTTP_PROVIDER_ID;
|
|
114
|
+
const send = createTransport({ ...options, providerId });
|
|
115
|
+
const model = options.model ?? '';
|
|
116
|
+
const dialect: DecisionDialect = options.dialect ?? 'port';
|
|
117
|
+
const { requestKey, responseKey } = options;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: providerId,
|
|
121
|
+
model,
|
|
122
|
+
async decide(request: DecisionRequest): Promise<DecisionResult> {
|
|
123
|
+
return send(
|
|
124
|
+
() => {
|
|
125
|
+
const entries = validateDecisionQuestions(
|
|
126
|
+
request.questions,
|
|
127
|
+
providerId
|
|
128
|
+
);
|
|
129
|
+
const questions: Record<
|
|
130
|
+
string,
|
|
131
|
+
ReturnType<typeof toWireQuestion>
|
|
132
|
+
> = Object.create(null);
|
|
133
|
+
const expected: Record<string, DecisionQuestion> =
|
|
134
|
+
Object.create(null);
|
|
135
|
+
for (const [id, question] of entries) {
|
|
136
|
+
questions[id] = toWireQuestion(question, dialect);
|
|
137
|
+
expected[id] = question;
|
|
138
|
+
}
|
|
139
|
+
const inner = { state: request.state, questions };
|
|
140
|
+
let payload: string;
|
|
141
|
+
try {
|
|
142
|
+
payload = JSON.stringify({
|
|
143
|
+
...(model !== '' ? { model } : {}),
|
|
144
|
+
...(requestKey != null && requestKey !== ''
|
|
145
|
+
? { [requestKey]: inner }
|
|
146
|
+
: inner),
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
throw new DecisionError(
|
|
150
|
+
'bad_request',
|
|
151
|
+
'invalid decision model request',
|
|
152
|
+
{
|
|
153
|
+
provider: providerId,
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
payload,
|
|
159
|
+
parse: (body: string) =>
|
|
160
|
+
parseEnvelope(
|
|
161
|
+
body,
|
|
162
|
+
providerId,
|
|
163
|
+
(answer, question) => readAnswer(answer, dialect, question),
|
|
164
|
+
responseKey,
|
|
165
|
+
expected
|
|
166
|
+
),
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
request.signal,
|
|
170
|
+
request.label ?? 'decide',
|
|
171
|
+
request.timeoutMs
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './questions';
|
|
3
|
+
export * from './presets';
|
|
4
|
+
export {
|
|
5
|
+
createHttpDecisionModel,
|
|
6
|
+
parseEnvelope,
|
|
7
|
+
HTTP_PROVIDER_ID,
|
|
8
|
+
} from './http';
|
|
9
|
+
export { createStructuredChatDecisionModel } from './structuredChat';
|
|
10
|
+
export type { HttpDecisionModelOptions } from './http';
|
|
11
|
+
export type { StructuredChatDecisionModelOptions } from './structuredChat';
|
|
12
|
+
export { toWireQuestion, readAnswer } from './dialect';
|
|
13
|
+
export { createTransport } from './transport';
|
|
14
|
+
export type { DecisionFetch, Transport, TransportOptions } from './transport';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DecisionModel,
|
|
3
|
+
DecisionCredential,
|
|
4
|
+
DecisionProviderSettings,
|
|
5
|
+
} from './types';
|
|
6
|
+
import type { DecisionFetch } from './transport';
|
|
7
|
+
import { createHttpDecisionModel } from './http';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_API_KEY_ENV = 'DECISION_API_KEY';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Known HTTP hosts as settings, not new decision model implementations. Jev and Laya speak the
|
|
13
|
+
* same wire protocol, but their confidence measures and checkpoint calibration differ.
|
|
14
|
+
*/
|
|
15
|
+
export const DECISION_PRESETS: Readonly<
|
|
16
|
+
Record<string, Readonly<DecisionProviderSettings>>
|
|
17
|
+
> = Object.freeze({
|
|
18
|
+
http: Object.freeze({
|
|
19
|
+
dialect: 'port',
|
|
20
|
+
requiresAuth: true,
|
|
21
|
+
apiKeyEnv: DEFAULT_API_KEY_ENV,
|
|
22
|
+
}),
|
|
23
|
+
typesafe: Object.freeze({
|
|
24
|
+
baseURL: 'https://api.typesafe.ai/v1/systemone',
|
|
25
|
+
model: 'jev-latest',
|
|
26
|
+
dialect: 'systemone',
|
|
27
|
+
requiresAuth: true,
|
|
28
|
+
apiKeyEnv: 'TYPESAFE_API_KEY',
|
|
29
|
+
}),
|
|
30
|
+
laya: Object.freeze({
|
|
31
|
+
/** Supply the server's full /v1/systemone URL; no model lets Laya route by language. */
|
|
32
|
+
dialect: 'systemone',
|
|
33
|
+
requiresAuth: false,
|
|
34
|
+
}),
|
|
35
|
+
clickhouse: Object.freeze({
|
|
36
|
+
/** ClickHouse's inference gateway; the token is what `dataplanectl chai auth token` prints, hourly. */
|
|
37
|
+
baseURL: 'https://inference-internal.clickhouse.cloud/v1/systemone',
|
|
38
|
+
model: 'jev-latest',
|
|
39
|
+
dialect: 'systemone',
|
|
40
|
+
requiresAuth: true,
|
|
41
|
+
apiKeyEnv: 'CHAI_AUTH_TOKEN',
|
|
42
|
+
}),
|
|
43
|
+
openrouter: Object.freeze({
|
|
44
|
+
baseURL: 'https://openrouter.ai/api/alpha/decisions',
|
|
45
|
+
model: '~typesafe/jev-latest',
|
|
46
|
+
dialect: 'systemone',
|
|
47
|
+
requiresAuth: true,
|
|
48
|
+
apiKeyEnv: 'OPENROUTER_KEY',
|
|
49
|
+
}),
|
|
50
|
+
cloudflare: Object.freeze({
|
|
51
|
+
/** No default URL: the account id is part of it. */
|
|
52
|
+
model: 'typesafe/jev',
|
|
53
|
+
dialect: 'systemone',
|
|
54
|
+
requestKey: 'input',
|
|
55
|
+
responseKey: 'result',
|
|
56
|
+
requiresAuth: true,
|
|
57
|
+
apiKeyEnv: 'CLOUDFLARE_API_TOKEN',
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export function decisionPreset(
|
|
62
|
+
name: string | undefined
|
|
63
|
+
): DecisionProviderSettings | null {
|
|
64
|
+
if (name == null || name === '') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return Object.hasOwn(DECISION_PRESETS, name)
|
|
68
|
+
? { ...DECISION_PRESETS[name] }
|
|
69
|
+
: null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Caller settings win except that required preset authentication cannot be weakened. */
|
|
73
|
+
export function mergeDecisionSettings(
|
|
74
|
+
preset: DecisionProviderSettings | null,
|
|
75
|
+
configured: DecisionProviderSettings | undefined
|
|
76
|
+
): DecisionProviderSettings {
|
|
77
|
+
return {
|
|
78
|
+
...(preset ?? {}),
|
|
79
|
+
...(configured ?? {}),
|
|
80
|
+
...(preset?.requiresAuth === true ? { requiresAuth: true } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function decisionProviderNames(): string[] {
|
|
85
|
+
return Object.keys(DECISION_PRESETS);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createDecisionModel(
|
|
89
|
+
settings: DecisionProviderSettings,
|
|
90
|
+
apiKey?: DecisionCredential,
|
|
91
|
+
options?: {
|
|
92
|
+
fetch?: DecisionFetch;
|
|
93
|
+
providerId?: string;
|
|
94
|
+
onAnswered?: (label: string, ms: number) => void;
|
|
95
|
+
}
|
|
96
|
+
): DecisionModel {
|
|
97
|
+
return createHttpDecisionModel({
|
|
98
|
+
providerId: options?.providerId,
|
|
99
|
+
apiKey,
|
|
100
|
+
requiresAuth: settings.requiresAuth,
|
|
101
|
+
endpoint: settings.baseURL ?? '',
|
|
102
|
+
model: settings.model,
|
|
103
|
+
dialect: settings.dialect,
|
|
104
|
+
requestKey: settings.requestKey,
|
|
105
|
+
responseKey: settings.responseKey,
|
|
106
|
+
timeoutMs: settings.timeoutMs,
|
|
107
|
+
maxRetries: settings.maxRetries,
|
|
108
|
+
fetch: options?.fetch,
|
|
109
|
+
onAnswered: options?.onAnswered,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BooleanCriteria,
|
|
3
|
+
BooleanQuestion,
|
|
4
|
+
ChoiceQuestion,
|
|
5
|
+
ScoreQuestion,
|
|
6
|
+
DecisionText,
|
|
7
|
+
DecisionQuestion,
|
|
8
|
+
} from './types';
|
|
9
|
+
import { DecisionError, isDecisionObject } from './types';
|
|
10
|
+
|
|
11
|
+
function validQuestionId(id: string): boolean {
|
|
12
|
+
if (id.length === 0 || id.length > 128) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
for (let index = 0; index < id.length; index++) {
|
|
16
|
+
if (id.charCodeAt(index) < 32) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validBooleanCriteria(criteria: BooleanQuestion['criteria']): boolean {
|
|
24
|
+
if (criteria === undefined || typeof criteria === 'string') {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (!isDecisionObject(criteria)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
for (const [side, description] of Object.entries(criteria)) {
|
|
31
|
+
if (
|
|
32
|
+
(side !== 'true' && side !== 'false') ||
|
|
33
|
+
(description !== undefined &&
|
|
34
|
+
typeof description !== 'string' &&
|
|
35
|
+
!Array.isArray(description) &&
|
|
36
|
+
!isDecisionObject(description))
|
|
37
|
+
) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validateDecisionQuestions(
|
|
45
|
+
questions: Record<string, DecisionQuestion>,
|
|
46
|
+
provider: string
|
|
47
|
+
): Array<[string, DecisionQuestion]> {
|
|
48
|
+
if (!isDecisionObject(questions)) {
|
|
49
|
+
throw new DecisionError('bad_request', 'invalid decision model questions', {
|
|
50
|
+
provider,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const supplied = Object.entries(questions);
|
|
54
|
+
if (supplied.length === 0) {
|
|
55
|
+
throw new DecisionError(
|
|
56
|
+
'bad_request',
|
|
57
|
+
'decision model requires questions',
|
|
58
|
+
{
|
|
59
|
+
provider,
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const entries: Array<[string, DecisionQuestion]> = [];
|
|
64
|
+
for (const [id, question] of supplied) {
|
|
65
|
+
if (
|
|
66
|
+
!validQuestionId(id) ||
|
|
67
|
+
!isDecisionObject(question) ||
|
|
68
|
+
!['boolean', 'choice', 'score'].includes(question.type) ||
|
|
69
|
+
(question.type === 'boolean' &&
|
|
70
|
+
!validBooleanCriteria(question.criteria)) ||
|
|
71
|
+
(question.type === 'choice' &&
|
|
72
|
+
(!isDecisionObject(question.criteria) ||
|
|
73
|
+
Object.keys(question.criteria).length === 0)) ||
|
|
74
|
+
(question.type === 'score' &&
|
|
75
|
+
(!Array.isArray(question.criteria) || question.criteria.length < 2))
|
|
76
|
+
) {
|
|
77
|
+
throw new DecisionError(
|
|
78
|
+
'bad_request',
|
|
79
|
+
'invalid decision model question',
|
|
80
|
+
{
|
|
81
|
+
provider,
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (question.type === 'choice') {
|
|
86
|
+
entries.push([id, { ...question, criteria: { ...question.criteria } }]);
|
|
87
|
+
} else if (question.type === 'score') {
|
|
88
|
+
entries.push([id, { ...question, criteria: [...question.criteria] }]);
|
|
89
|
+
} else {
|
|
90
|
+
entries.push([
|
|
91
|
+
id,
|
|
92
|
+
{
|
|
93
|
+
...question,
|
|
94
|
+
...(isDecisionObject(question.criteria)
|
|
95
|
+
? { criteria: { ...question.criteria } }
|
|
96
|
+
: {}),
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return entries;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A yes/no question; `criteria` describes what counts as yes (a string) or both sides. */
|
|
105
|
+
export function booleanQuestion(
|
|
106
|
+
instructions: DecisionText,
|
|
107
|
+
criteria?: BooleanCriteria | string
|
|
108
|
+
): BooleanQuestion {
|
|
109
|
+
return criteria == null
|
|
110
|
+
? { type: 'boolean', instructions }
|
|
111
|
+
: { type: 'boolean', instructions, criteria };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Pick one of the named options; a `null` description means the name speaks for itself. */
|
|
115
|
+
export function choiceQuestion(
|
|
116
|
+
instructions: DecisionText,
|
|
117
|
+
criteria: Record<string, DecisionText | null>
|
|
118
|
+
): ChoiceQuestion {
|
|
119
|
+
return { type: 'choice', instructions, criteria };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Rate against an ordered rubric, level 0 first. */
|
|
123
|
+
export function scoreQuestion(
|
|
124
|
+
instructions: DecisionText,
|
|
125
|
+
levels: DecisionText[]
|
|
126
|
+
): ScoreQuestion {
|
|
127
|
+
return { type: 'score', instructions, criteria: levels };
|
|
128
|
+
}
|