@ganju/utils 0.0.12 → 0.0.13
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/toolLint.d.ts +1 -1
- package/dist/toolLint.d.ts.map +1 -1
- package/dist/toolLint.js +53 -24
- package/package.json +1 -1
- package/src/toolLint.ts +62 -23
package/dist/toolLint.d.ts
CHANGED
package/dist/toolLint.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolLint.d.ts","sourceRoot":"","sources":["../src/toolLint.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE;QACZ,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,eAAe,CAAC,EAAE,OAAO,CAAC;QAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,aAAa,CAAC,EAAE,OAAO,CAAC;KACzB,CAAC;CACH;AAED,eAAO,MAAM,eAAe,+JAQlB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,YAAY,CAAC;IAGnB,IAAI,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"toolLint.d.ts","sourceRoot":"","sources":["../src/toolLint.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE;QACZ,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,eAAe,CAAC,EAAE,OAAO,CAAC;QAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,aAAa,CAAC,EAAE,OAAO,CAAC;KACzB,CAAC;CACH;AAED,eAAO,MAAM,eAAe,+JAQlB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,YAAY,CAAC;IAGnB,IAAI,CAAC,EAAE,MAAM,CAAC;IAGd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAEjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAEjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAI9B,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IAGxB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,eAAO,MAAM,gCAAgC,KAAK,CAAC;AAKnD,eAAO,MAAM,mBAAmB,KAAK,CAAC;AA2ItC;;;GAGG;AACH,eAAO,MAAM,SAAS,GACpB,OAAO,YAAY,EAAE,EACrB,UAAS,eAAoB,KAC5B,eAAe,EA8FjB,CAAC"}
|
package/dist/toolLint.js
CHANGED
|
@@ -29,13 +29,18 @@ exports.TOOL_LINT_MIN_DESCRIPTION_LENGTH = 40;
|
|
|
29
29
|
// start choosing among them badly. A channel turn hard-caps its list at 40
|
|
30
30
|
// (CHANNEL_MAX_TOOLS); this warns well before that.
|
|
31
31
|
exports.TOOL_LINT_MAX_TOOLS = 25;
|
|
32
|
-
// Two descriptions sharing at least this much of their vocabulary
|
|
33
|
-
//
|
|
32
|
+
// Two descriptions sharing at least this much of their vocabulary read as the
|
|
33
|
+
// same tool to a model. Words are weighted by how rare they are among the
|
|
34
|
+
// server's tools (see descriptionWeights), so tools written from one template
|
|
35
|
+
// — "Use when someone asks about X" — are told apart by their X.
|
|
34
36
|
const DESCRIPTION_OVERLAP = 0.6;
|
|
35
37
|
// Too few content words and two unrelated descriptions can share most of them.
|
|
36
38
|
const MIN_OVERLAP_WORDS = 5;
|
|
37
39
|
// Lowercased, accents off, so "Úsala cuando" and "usala cuando" are one text.
|
|
38
|
-
const fold = (text) => text
|
|
40
|
+
const fold = (text) => text
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.normalize('NFKD')
|
|
43
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
39
44
|
// Wording that tells a model WHEN to call a tool rather than only what it does
|
|
40
45
|
// — English, Spanish and Portuguese, the languages descriptions here are
|
|
41
46
|
// written in. Loose on purpose: a false "says when" costs nothing, a false
|
|
@@ -90,23 +95,32 @@ const nameTokens = (name) => name
|
|
|
90
95
|
.filter(Boolean)
|
|
91
96
|
.sort()
|
|
92
97
|
.join(' ');
|
|
93
|
-
|
|
98
|
+
// How much each word says about which tool a description belongs to: a word
|
|
99
|
+
// in every description (the template, the domain) next to nothing, a word in
|
|
100
|
+
// one of them a lot. Smoothed, so with two tools a shared word still counts.
|
|
101
|
+
const descriptionWeights = (descriptions) => {
|
|
102
|
+
const frequency = new Map();
|
|
103
|
+
for (const words of descriptions) {
|
|
104
|
+
for (const word of words) {
|
|
105
|
+
frequency.set(word, (frequency.get(word) ?? 0) + 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const total = descriptions.length;
|
|
109
|
+
return word => Math.log(1 + total / (frequency.get(word) ?? 1));
|
|
110
|
+
};
|
|
111
|
+
// Jaccard, with each word counted by its weight.
|
|
112
|
+
const weightedJaccard = (a, b, weight) => {
|
|
94
113
|
let shared = 0;
|
|
95
|
-
|
|
114
|
+
let union = 0;
|
|
115
|
+
for (const word of a) {
|
|
116
|
+
union += weight(word);
|
|
96
117
|
if (b.has(word))
|
|
97
|
-
shared
|
|
98
|
-
const union = a.size + b.size - shared;
|
|
99
|
-
return union === 0 ? 0 : shared / union;
|
|
100
|
-
};
|
|
101
|
-
const overlaps = (a, b) => {
|
|
102
|
-
if (nameTokens(a.name) === nameTokens(b.name))
|
|
103
|
-
return true;
|
|
104
|
-
const aWords = contentWords(a.description ?? '');
|
|
105
|
-
const bWords = contentWords(b.description ?? '');
|
|
106
|
-
if (aWords.size < MIN_OVERLAP_WORDS || bWords.size < MIN_OVERLAP_WORDS) {
|
|
107
|
-
return false;
|
|
118
|
+
shared += weight(word);
|
|
108
119
|
}
|
|
109
|
-
|
|
120
|
+
for (const word of b)
|
|
121
|
+
if (!a.has(word))
|
|
122
|
+
union += weight(word);
|
|
123
|
+
return union === 0 ? 0 : shared / union;
|
|
110
124
|
};
|
|
111
125
|
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
112
126
|
// Every property the model may fill in that doesn't say what it is for, down
|
|
@@ -140,6 +154,19 @@ const quoted = (name) => `"${name}"`;
|
|
|
140
154
|
const lintTools = (tools, options = {}) => {
|
|
141
155
|
const others = options.others ?? [];
|
|
142
156
|
const findings = [];
|
|
157
|
+
const everyTool = [...tools, ...others];
|
|
158
|
+
const wordsOf = new Map(everyTool.map(tool => [tool, contentWords(tool.description ?? '')]));
|
|
159
|
+
const weight = descriptionWeights([...wordsOf.values()]);
|
|
160
|
+
const overlaps = (a, b) => {
|
|
161
|
+
if (nameTokens(a.name) === nameTokens(b.name))
|
|
162
|
+
return true;
|
|
163
|
+
const aWords = wordsOf.get(a);
|
|
164
|
+
const bWords = wordsOf.get(b);
|
|
165
|
+
if (aWords.size < MIN_OVERLAP_WORDS || bWords.size < MIN_OVERLAP_WORDS) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return weightedJaccard(aWords, bWords, weight) >= DESCRIPTION_OVERLAP;
|
|
169
|
+
};
|
|
143
170
|
tools.forEach((tool, index) => {
|
|
144
171
|
const description = (tool.description ?? '').trim();
|
|
145
172
|
if (!description) {
|
|
@@ -184,16 +211,18 @@ const lintTools = (tools, options = {}) => {
|
|
|
184
211
|
message: `${quoted(tool.name)} has input properties with no description: ${paths.join(', ')}. A model has to guess what to pass.`
|
|
185
212
|
});
|
|
186
213
|
}
|
|
187
|
-
// Each pair once
|
|
188
|
-
// server
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
214
|
+
// Each pair once — against the tools after this one, then the rest of the
|
|
215
|
+
// server — and one finding per tool, so twenty look-alikes are nineteen
|
|
216
|
+
// lines rather than a hundred and ninety.
|
|
217
|
+
const alike = [...tools.slice(index + 1), ...others]
|
|
218
|
+
.filter(other => other.name !== tool.name && overlaps(tool, other))
|
|
219
|
+
.map(other => other.name);
|
|
220
|
+
if (alike.length > 0) {
|
|
192
221
|
findings.push({
|
|
193
222
|
rule: 'overlapping-tools',
|
|
194
223
|
tool: tool.name,
|
|
195
|
-
|
|
196
|
-
message: `${quoted(tool.name)}
|
|
224
|
+
alike,
|
|
225
|
+
message: `${quoted(tool.name)} looks like ${alike.map(quoted).join(', ')} in name or description, so a model may call one for another. Make each description say what sets it apart.`
|
|
197
226
|
});
|
|
198
227
|
}
|
|
199
228
|
});
|
package/package.json
CHANGED
package/src/toolLint.ts
CHANGED
|
@@ -43,8 +43,9 @@ export interface ToolLintFinding {
|
|
|
43
43
|
// The tool the finding is about. Absent only for `too-many-tools`, which is
|
|
44
44
|
// about the server as a whole.
|
|
45
45
|
tool?: string;
|
|
46
|
-
// `overlapping-tools`: the
|
|
47
|
-
|
|
46
|
+
// `overlapping-tools`: the tools it may be confused with. Each look-alike
|
|
47
|
+
// pair is reported once, on the first of the two.
|
|
48
|
+
alike?: string[];
|
|
48
49
|
// `undescribed-input`: the properties with no description, as dotted paths.
|
|
49
50
|
paths?: string[];
|
|
50
51
|
// `too-many-tools`: how many are enabled, and the number past which it warns.
|
|
@@ -74,15 +75,20 @@ export const TOOL_LINT_MIN_DESCRIPTION_LENGTH = 40;
|
|
|
74
75
|
// (CHANNEL_MAX_TOOLS); this warns well before that.
|
|
75
76
|
export const TOOL_LINT_MAX_TOOLS = 25;
|
|
76
77
|
|
|
77
|
-
// Two descriptions sharing at least this much of their vocabulary
|
|
78
|
-
//
|
|
78
|
+
// Two descriptions sharing at least this much of their vocabulary read as the
|
|
79
|
+
// same tool to a model. Words are weighted by how rare they are among the
|
|
80
|
+
// server's tools (see descriptionWeights), so tools written from one template
|
|
81
|
+
// — "Use when someone asks about X" — are told apart by their X.
|
|
79
82
|
const DESCRIPTION_OVERLAP = 0.6;
|
|
80
83
|
// Too few content words and two unrelated descriptions can share most of them.
|
|
81
84
|
const MIN_OVERLAP_WORDS = 5;
|
|
82
85
|
|
|
83
86
|
// Lowercased, accents off, so "Úsala cuando" and "usala cuando" are one text.
|
|
84
87
|
const fold = (text: string): string =>
|
|
85
|
-
text
|
|
88
|
+
text
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.normalize('NFKD')
|
|
91
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
86
92
|
|
|
87
93
|
// Wording that tells a model WHEN to call a tool rather than only what it does
|
|
88
94
|
// — English, Spanish and Portuguese, the languages descriptions here are
|
|
@@ -147,21 +153,36 @@ const nameTokens = (name: string): string =>
|
|
|
147
153
|
.sort()
|
|
148
154
|
.join(' ');
|
|
149
155
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
// How much each word says about which tool a description belongs to: a word
|
|
157
|
+
// in every description (the template, the domain) next to nothing, a word in
|
|
158
|
+
// one of them a lot. Smoothed, so with two tools a shared word still counts.
|
|
159
|
+
const descriptionWeights = (
|
|
160
|
+
descriptions: Set<string>[]
|
|
161
|
+
): ((word: string) => number) => {
|
|
162
|
+
const frequency = new Map<string, number>();
|
|
163
|
+
for (const words of descriptions) {
|
|
164
|
+
for (const word of words) {
|
|
165
|
+
frequency.set(word, (frequency.get(word) ?? 0) + 1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const total = descriptions.length;
|
|
169
|
+
return word => Math.log(1 + total / (frequency.get(word) ?? 1));
|
|
155
170
|
};
|
|
156
171
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
172
|
+
// Jaccard, with each word counted by its weight.
|
|
173
|
+
const weightedJaccard = (
|
|
174
|
+
a: Set<string>,
|
|
175
|
+
b: Set<string>,
|
|
176
|
+
weight: (word: string) => number
|
|
177
|
+
): number => {
|
|
178
|
+
let shared = 0;
|
|
179
|
+
let union = 0;
|
|
180
|
+
for (const word of a) {
|
|
181
|
+
union += weight(word);
|
|
182
|
+
if (b.has(word)) shared += weight(word);
|
|
163
183
|
}
|
|
164
|
-
|
|
184
|
+
for (const word of b) if (!a.has(word)) union += weight(word);
|
|
185
|
+
return union === 0 ? 0 : shared / union;
|
|
165
186
|
};
|
|
166
187
|
|
|
167
188
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -202,6 +223,21 @@ export const lintTools = (
|
|
|
202
223
|
const others = options.others ?? [];
|
|
203
224
|
const findings: ToolLintFinding[] = [];
|
|
204
225
|
|
|
226
|
+
const everyTool = [...tools, ...others];
|
|
227
|
+
const wordsOf = new Map(
|
|
228
|
+
everyTool.map(tool => [tool, contentWords(tool.description ?? '')])
|
|
229
|
+
);
|
|
230
|
+
const weight = descriptionWeights([...wordsOf.values()]);
|
|
231
|
+
const overlaps = (a: LintableTool, b: LintableTool): boolean => {
|
|
232
|
+
if (nameTokens(a.name) === nameTokens(b.name)) return true;
|
|
233
|
+
const aWords = wordsOf.get(a)!;
|
|
234
|
+
const bWords = wordsOf.get(b)!;
|
|
235
|
+
if (aWords.size < MIN_OVERLAP_WORDS || bWords.size < MIN_OVERLAP_WORDS) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return weightedJaccard(aWords, bWords, weight) >= DESCRIPTION_OVERLAP;
|
|
239
|
+
};
|
|
240
|
+
|
|
205
241
|
tools.forEach((tool, index) => {
|
|
206
242
|
const description = (tool.description ?? '').trim();
|
|
207
243
|
|
|
@@ -250,15 +286,18 @@ export const lintTools = (
|
|
|
250
286
|
});
|
|
251
287
|
}
|
|
252
288
|
|
|
253
|
-
// Each pair once
|
|
254
|
-
// server
|
|
255
|
-
|
|
256
|
-
|
|
289
|
+
// Each pair once — against the tools after this one, then the rest of the
|
|
290
|
+
// server — and one finding per tool, so twenty look-alikes are nineteen
|
|
291
|
+
// lines rather than a hundred and ninety.
|
|
292
|
+
const alike = [...tools.slice(index + 1), ...others]
|
|
293
|
+
.filter(other => other.name !== tool.name && overlaps(tool, other))
|
|
294
|
+
.map(other => other.name);
|
|
295
|
+
if (alike.length > 0) {
|
|
257
296
|
findings.push({
|
|
258
297
|
rule: 'overlapping-tools',
|
|
259
298
|
tool: tool.name,
|
|
260
|
-
|
|
261
|
-
message: `${quoted(tool.name)}
|
|
299
|
+
alike,
|
|
300
|
+
message: `${quoted(tool.name)} looks like ${alike.map(quoted).join(', ')} in name or description, so a model may call one for another. Make each description say what sets it apart.`
|
|
262
301
|
});
|
|
263
302
|
}
|
|
264
303
|
});
|