modelfunction-cli 0.1.0 → 0.1.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/README.md +53 -2
- package/fixtures/suite.json +1 -1
- package/fixtures/tools/resume_score.json +37 -0
- package/lib/lint.js +99 -2
- package/lib/mcp-server.js +11 -5
- package/mf.js +5 -3
- package/package.json +3 -2
- package/skills/mf-cases/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx -y modelfunction-cli@0.1.
|
|
8
|
+
npx -y modelfunction-cli@0.1.2 --version
|
|
9
9
|
# 或
|
|
10
10
|
npm i -D modelfunction-cli
|
|
11
11
|
npx mf --version
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
全局安装(可选)后命令为 `mf`,也可以用 `modelfunction`——避开装了 TeX 的机器上 `mf` 与 Metafont 同名:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i -g modelfunction-cli
|
|
18
|
+
modelfunction --version
|
|
19
|
+
```
|
|
20
|
+
|
|
14
21
|
## 五步上手
|
|
15
22
|
|
|
16
23
|
```bash
|
|
@@ -31,6 +38,28 @@ SILICONFLOW_API_KEY=... mf eval --models dsv4-flash
|
|
|
31
38
|
OPENAI_API_KEY=... mf eval --models openai
|
|
32
39
|
```
|
|
33
40
|
|
|
41
|
+
### 自建 / 内网网关
|
|
42
|
+
|
|
43
|
+
企业内网基本都是 OpenAI 兼容协议,写一个 models-file 即可,Key 只走环境变量、不落盘:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"internal-ds": {
|
|
48
|
+
"type": "openai",
|
|
49
|
+
"baseUrl": "https://llm.example.internal/v1",
|
|
50
|
+
"model": "deepseek-v4-flash",
|
|
51
|
+
"apiKeyEnv": "INTERNAL_LLM_KEY"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
INTERNAL_LLM_KEY=... mf eval --suite mf.suite.json \
|
|
58
|
+
--models-file my-models.json --models internal-ds
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`pricing` 可省(省了就不输出成本);要算成本请按自家价目填写,默认 registry 里的价格只是参考值。
|
|
62
|
+
|
|
34
63
|
## 命令
|
|
35
64
|
|
|
36
65
|
| 命令 | 作用 |
|
|
@@ -54,7 +83,7 @@ OPENAI_API_KEY=... mf eval --models openai
|
|
|
54
83
|
"mcpServers": {
|
|
55
84
|
"modelfunction": {
|
|
56
85
|
"command": "npx",
|
|
57
|
-
"args": ["-y", "modelfunction-cli@0.1.
|
|
86
|
+
"args": ["-y", "modelfunction-cli@0.1.2", "mcp"]
|
|
58
87
|
}
|
|
59
88
|
}
|
|
60
89
|
}
|
|
@@ -72,4 +101,26 @@ OPENAI_API_KEY=... mf eval --models openai
|
|
|
72
101
|
|
|
73
102
|
由你或你的编码 Agent 撰写,规则模板见包内 `skills/mf-cases/SKILL.md`。未复核用例(`reviewed:false`)不计入正式分数。
|
|
74
103
|
|
|
104
|
+
工具一多就别手工誊抄,直接让编码 Agent 驱动 `mf`:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# 1. 把 mf 挂成 MCP server
|
|
108
|
+
claude mcp add modelfunction -- npx -y modelfunction-cli@0.1.2 mcp
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
# 2. 让 Agent 扫仓库并写出套件草稿
|
|
113
|
+
用 modelfunction 的 scan_tools 扫 .,probe=true,writeSuite=mf.suite.json
|
|
114
|
+
|
|
115
|
+
# 3. 让 Agent 补用例
|
|
116
|
+
按 skills/mf-cases/SKILL.md 的规则,为每个工具补 3 条正例和 1 条负例,用 add_cases
|
|
117
|
+
写入 mf.suite.json,然后跑 coverage 看还缺哪些参数和负例
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# 4. 人工复核:把认可的用例 reviewed 改为 true,再跑正式评分
|
|
122
|
+
mf coverage --suite mf.suite.json
|
|
123
|
+
mf eval --suite mf.suite.json --models internal-ds
|
|
124
|
+
```
|
|
125
|
+
|
|
75
126
|
License: MIT
|
package/fixtures/suite.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "recruiting-agent",
|
|
3
3
|
"description": "招聘 Agent 的 function-calling 评测集:简历评分、面试题、薪资区间、JD 改写。",
|
|
4
4
|
"tools": [
|
|
5
|
-
{ "ref": "
|
|
5
|
+
{ "ref": "./tools/resume_score.json" },
|
|
6
6
|
{
|
|
7
7
|
"name": "interview_question",
|
|
8
8
|
"description": "根据岗位和候选人背景生成针对性的模拟面试题。",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resume_score",
|
|
3
|
+
"description": "简历与 JD 匹配评分",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"input": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["resume", "jd"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"resume": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"minLength": 20,
|
|
12
|
+
"description": "候选人简历文本"
|
|
13
|
+
},
|
|
14
|
+
"jd": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"minLength": 20,
|
|
17
|
+
"description": "职位描述文本"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"output": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"score": { "type": "number", "minimum": 0, "maximum": 100 },
|
|
25
|
+
"reasons": { "type": "array", "items": { "type": "string" } },
|
|
26
|
+
"matched": { "type": "array", "items": { "type": "string" } },
|
|
27
|
+
"missing": { "type": "array", "items": { "type": "string" } },
|
|
28
|
+
"years": { "type": "number" }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"sideEffects": "none",
|
|
32
|
+
"model": "mock-keyword-v1",
|
|
33
|
+
"price": { "amount": 0.02, "currency": "CNY", "unit": "call" },
|
|
34
|
+
"mcp": true,
|
|
35
|
+
"auth": { "type": "api_key" },
|
|
36
|
+
"rateLimit": { "requests": 60, "window": "1m" }
|
|
37
|
+
}
|
package/lib/lint.js
CHANGED
|
@@ -15,6 +15,42 @@ const GENERIC_NAMES = new Set([
|
|
|
15
15
|
|
|
16
16
|
const WHEN_HINT = /(用于|用来|当|如果|适用|适合|use when|use this|helps? )/i;
|
|
17
17
|
|
|
18
|
+
// Identifier tokens, matched on token boundaries only: `request_text` is text,
|
|
19
|
+
// `request_id` is an identifier. Strong = platform-injected (session/tenant/trace
|
|
20
|
+
// class); weak = business id the user may or may not supply.
|
|
21
|
+
const STRONG_ID_TOKENS = new Set([
|
|
22
|
+
"uuid", "guid", "sid", "nonce", "token", "signature",
|
|
23
|
+
"sessionid", "tenantid", "traceid", "threadid", "conversationid", "correlationid", "requestid",
|
|
24
|
+
"session", "tenant", "trace", "thread", "conversation", "correlation"
|
|
25
|
+
]);
|
|
26
|
+
const WEAK_ID_TOKENS = new Set(["id", "ids"]);
|
|
27
|
+
const OPAQUE_ID_SCHEMA = /uuid|[0-9a-f]{8}-[0-9a-f]{4}/i;
|
|
28
|
+
const DESCRIPTION_ID_HINT = /(会话\s*id|session\s*id|租户|tenant|trace|请求\s*id|request\s*id|签名|signature|\btoken\b|nonce|uuid|标识|编号)/i;
|
|
29
|
+
const ID_SOURCE_HINT = /(来源|取自|来自|传入|注入|平台|系统|上下文|请求头|header|无需|自动|会提供|由调用方|上一步)/i;
|
|
30
|
+
const FIXED_ID_LENGTHS = new Set([8, 16, 24, 32, 36, 40, 64]);
|
|
31
|
+
|
|
32
|
+
function nameTokens(name) {
|
|
33
|
+
return String(name)
|
|
34
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
35
|
+
.split(/[^A-Za-z0-9]+/)
|
|
36
|
+
.map((token) => token.toLowerCase())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function idStrength(name) {
|
|
41
|
+
const tokens = nameTokens(name);
|
|
42
|
+
if (tokens.length === 0) return null;
|
|
43
|
+
const last = tokens[tokens.length - 1];
|
|
44
|
+
const joinedTail = tokens.slice(-2).join("");
|
|
45
|
+
if (STRONG_ID_TOKENS.has(last) || STRONG_ID_TOKENS.has(joinedTail)) return "strong";
|
|
46
|
+
if (WEAK_ID_TOKENS.has(last)) return "weak";
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BOUNDARY_HINT = /(不适用|不要用于|不用于|不能用于|而非|区别于|not for|do not use)/i;
|
|
51
|
+
// Chinese descriptions are compared as bigrams, so mid-0.3s similarity is noise.
|
|
52
|
+
const NO_BOUNDARY_SIMILARITY = 0.5;
|
|
53
|
+
|
|
18
54
|
export function lintSuite(suite) {
|
|
19
55
|
const findings = [];
|
|
20
56
|
const add = (severity, code, tool, message, extra = {}) =>
|
|
@@ -63,6 +99,55 @@ export function lintSuite(suite) {
|
|
|
63
99
|
});
|
|
64
100
|
}
|
|
65
101
|
|
|
102
|
+
for (const name of required) {
|
|
103
|
+
const schema = properties[name];
|
|
104
|
+
if (!schema) continue;
|
|
105
|
+
const description = String(schema.description ?? "");
|
|
106
|
+
const fixedLength =
|
|
107
|
+
typeof schema.minLength === "number" &&
|
|
108
|
+
schema.minLength === schema.maxLength &&
|
|
109
|
+
FIXED_ID_LENGTHS.has(schema.minLength);
|
|
110
|
+
const schemaOpaque =
|
|
111
|
+
(typeof schema.format === "string" && OPAQUE_ID_SCHEMA.test(schema.format)) ||
|
|
112
|
+
(typeof schema.pattern === "string" && OPAQUE_ID_SCHEMA.test(schema.pattern)) ||
|
|
113
|
+
fixedLength;
|
|
114
|
+
const nameStrength = idStrength(name);
|
|
115
|
+
// Name tokens alone are a weak signal: the author picks the names. Schema
|
|
116
|
+
// shape and the description are what the model actually reasons from.
|
|
117
|
+
const strong = schemaOpaque || DESCRIPTION_ID_HINT.test(description) || nameStrength === "strong";
|
|
118
|
+
const weak = nameStrength === "weak";
|
|
119
|
+
const scalar = schema.type === undefined || schema.type === "string";
|
|
120
|
+
if ((strong || weak) && scalar && !ID_SOURCE_HINT.test(description)) {
|
|
121
|
+
add(
|
|
122
|
+
strong ? "warn" : "info",
|
|
123
|
+
"required-unsourced",
|
|
124
|
+
tool.name,
|
|
125
|
+
strong
|
|
126
|
+
? `必填参数 ${name} 像是外部注入的标识,但没说明从哪里获取;模型拿不到它时可能干脆不调用任何工具`
|
|
127
|
+
: `必填参数 ${name} 是业务 ID;如果用户不会在对话里给出,模型可能编造或干脆不调用`,
|
|
128
|
+
{
|
|
129
|
+
param: name,
|
|
130
|
+
suggestion: `改为平台注入(移出 schema),或在 description 写明来源,例如「由平台在会话开始时注入,模型无需生成」`
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const toolCases = (suite.cases ?? []).filter((testCase) => testCase.expectedTool === tool.name);
|
|
137
|
+
if (toolCases.length > 0) {
|
|
138
|
+
for (const name of required) {
|
|
139
|
+
const covered = toolCases.some(
|
|
140
|
+
(testCase) => testCase.expectedArgs && Object.prototype.hasOwnProperty.call(testCase.expectedArgs, name)
|
|
141
|
+
);
|
|
142
|
+
if (!covered) {
|
|
143
|
+
add("info", "required-uncovered", tool.name, `必填参数 ${name} 没有任何用例校验过,模型是否真能填对它还没有证据`, {
|
|
144
|
+
param: name,
|
|
145
|
+
suggestion: `补一条用例在 expectedArgs 里断言 ${name},或把它降为可选 / 改为平台注入`
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
66
151
|
for (const [name, schema] of Object.entries(properties)) {
|
|
67
152
|
if (!schema?.description) {
|
|
68
153
|
add("warn", "param-desc-missing", tool.name, `参数 ${name} 没有说明`, {
|
|
@@ -83,9 +168,21 @@ export function lintSuite(suite) {
|
|
|
83
168
|
}
|
|
84
169
|
|
|
85
170
|
for (const pair of suspectedPairs(suite.tools)) {
|
|
86
|
-
|
|
87
|
-
|
|
171
|
+
const similarity = pair.similarity.toFixed(2);
|
|
172
|
+
const declaresBoundary = [pair.a, pair.b].some((name) => {
|
|
173
|
+
const tool = suite.tools.find((item) => item.name === name);
|
|
174
|
+
return BOUNDARY_HINT.test(tool?.description ?? "");
|
|
88
175
|
});
|
|
176
|
+
if (declaresBoundary) {
|
|
177
|
+
add("info", "pair-overlap", pair.a, `与 ${pair.b} 的描述相似度 ${similarity},已写互斥边界`, {
|
|
178
|
+
pair: [pair.a, pair.b]
|
|
179
|
+
});
|
|
180
|
+
} else if (pair.similarity >= NO_BOUNDARY_SIMILARITY) {
|
|
181
|
+
add("warn", "pair-no-boundary", pair.a, `与 ${pair.b} 的描述相似度 ${similarity},且两边的 description 都没写“不适用于”边界,模型只能靠相似度硬猜`, {
|
|
182
|
+
pair: [pair.a, pair.b],
|
|
183
|
+
suggestion: `在 ${pair.a} 和 ${pair.b} 的 description 里各写一句“不适用于什么”,并补一条歧义用例`
|
|
184
|
+
});
|
|
185
|
+
}
|
|
89
186
|
}
|
|
90
187
|
|
|
91
188
|
const counts = { error: 0, warn: 0, info: 0 };
|
package/lib/mcp-server.js
CHANGED
|
@@ -6,7 +6,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { loadSuite } from "./suite.js";
|
|
9
|
-
import { scanProject, addTools, redactReport } from "./scan.js";
|
|
9
|
+
import { scanProject, addTools, buildSuiteDraft, redactReport } from "./scan.js";
|
|
10
|
+
import { writeFileAtomic } from "./fsutil.js";
|
|
10
11
|
import { probeStdioServer } from "./mcp-probe.js";
|
|
11
12
|
import { addCases, computeCoverage } from "./cases.js";
|
|
12
13
|
|
|
@@ -31,14 +32,15 @@ export async function startMcpServer() {
|
|
|
31
32
|
{
|
|
32
33
|
title: "Scan tools",
|
|
33
34
|
description:
|
|
34
|
-
"扫描仓库里的 function.json / OpenAPI / MCP 配置 / SKILL.md / 代码内定义。probe=true 时连接 stdio MCP server 拉取 tools/list
|
|
35
|
+
"扫描仓库里的 function.json / OpenAPI / MCP 配置 / SKILL.md / 代码内定义。probe=true 时连接 stdio MCP server 拉取 tools/list。不执行工具。默认只读;传 writeSuite 时在该路径写出套件草稿(cases 为空,需补用例并复核)。",
|
|
35
36
|
inputSchema: {
|
|
36
37
|
root: z.string().optional().describe("仓库目录,默认进程当前目录"),
|
|
37
38
|
probe: z.boolean().optional().describe("是否连接 MCP server 拉取工具清单"),
|
|
38
|
-
timeoutMs: z.number().optional().describe("单个 MCP server 超时(毫秒,默认 5000)")
|
|
39
|
+
timeoutMs: z.number().optional().describe("单个 MCP server 超时(毫秒,默认 5000)"),
|
|
40
|
+
writeSuite: z.string().optional().describe("套件草稿输出路径,如 mf.suite.json")
|
|
39
41
|
}
|
|
40
42
|
},
|
|
41
|
-
async ({ root, probe, timeoutMs }) => {
|
|
43
|
+
async ({ root, probe, timeoutMs, writeSuite }) => {
|
|
42
44
|
const report = await scanProject(root ?? process.cwd());
|
|
43
45
|
if (probe) {
|
|
44
46
|
for (const mcpServer of [...report.mcpServers]) {
|
|
@@ -60,7 +62,11 @@ export async function startMcpServer() {
|
|
|
60
62
|
report.probes.push({ server: mcpServer.name, ok: true, toolCount: added.length, tools: added });
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
|
-
|
|
65
|
+
const payload = redactReport(report);
|
|
66
|
+
if (!writeSuite) return json(payload);
|
|
67
|
+
const target = resolve(writeSuite);
|
|
68
|
+
await writeFileAtomic(target, JSON.stringify(buildSuiteDraft(report), null, 2));
|
|
69
|
+
return json({ ...payload, suiteDraftPath: target });
|
|
64
70
|
}
|
|
65
71
|
);
|
|
66
72
|
|
package/mf.js
CHANGED
|
@@ -46,9 +46,9 @@ Usage:
|
|
|
46
46
|
mf mcp # start stdio MCP server for coding agents
|
|
47
47
|
|
|
48
48
|
Eval options:
|
|
49
|
-
--suite <path> eval suite (default:
|
|
49
|
+
--suite <path> eval suite (default: built-in demo suite)
|
|
50
50
|
--models <ids> comma-separated ids from models.json (default: models with API keys set)
|
|
51
|
-
--models-file <path> model registry (default:
|
|
51
|
+
--models-file <path> model registry (default: built-in registry)
|
|
52
52
|
--case <ids> run only these case ids (comma-separated, repeatable)
|
|
53
53
|
--show-cases print per-case results
|
|
54
54
|
--json print JSON instead of the table
|
|
@@ -563,7 +563,9 @@ function splitList(value) {
|
|
|
563
563
|
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
564
564
|
|
|
565
565
|
if (isMain) {
|
|
566
|
-
|
|
566
|
+
let argv = process.argv.slice(2);
|
|
567
|
+
// `npx modelfunction-cli mf scan` passes the bin name through; accept it.
|
|
568
|
+
if (argv[0] === "mf") argv = argv.slice(1);
|
|
567
569
|
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
568
570
|
const pkg = JSON.parse(await readFile(new URL("./package.json", import.meta.url), "utf8"));
|
|
569
571
|
process.stdout.write(`mf ${pkg.version}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelfunction-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "mf — function-calling accuracy scorecard for AI agents",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"agent"
|
|
14
14
|
],
|
|
15
15
|
"bin": {
|
|
16
|
-
"mf": "
|
|
16
|
+
"mf": "mf.js",
|
|
17
|
+
"modelfunction": "mf.js"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"mf.js",
|
package/skills/mf-cases/SKILL.md
CHANGED
|
@@ -9,6 +9,7 @@ description: 为 ModelFunction 评测套件撰写 function-calling 用例。当
|
|
|
9
9
|
|
|
10
10
|
# 输入
|
|
11
11
|
|
|
12
|
+
0. 还没有套件时:先调 `scan_tools`(`probe=true` 可连 MCP server)并传 `writeSuite: "mf.suite.json"`,得到工具清单草稿,再补用例。
|
|
12
13
|
1. 工具定义:`mf.scan.json` 或 `mf.suite.json`(只看名字、description、parameters)
|
|
13
14
|
2. 覆盖率现状:`mf coverage --suite <file> --json`
|
|
14
15
|
3. 可选:用户提供的真实日志、投诉记录、产品文档
|