MenuPilot 0.1.0__py3-none-any.whl
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.
- menupilot/__init__.py +3 -0
- menupilot/__main__.py +4 -0
- menupilot/agent/__init__.py +0 -0
- menupilot/agent/agent_loop.py +414 -0
- menupilot/agent/matching_engine.py +974 -0
- menupilot/agent/option_expander.py +490 -0
- menupilot/agent/orchestration.py +570 -0
- menupilot/agent/rule_engine.py +509 -0
- menupilot/agent/sandbox.py +216 -0
- menupilot/agent/schema_analyzer.py +1026 -0
- menupilot/agent/template_preprocessor.py +293 -0
- menupilot/agent/token_classifier.py +816 -0
- menupilot/agent/tools.py +365 -0
- menupilot/agent/workflow.py +1072 -0
- menupilot/cli/human_review.py +191 -0
- menupilot/cli/repl.py +821 -0
- menupilot/config.py +113 -0
- menupilot/data/__init__.py +0 -0
- menupilot/data/canonical_schema.py +135 -0
- menupilot/data/mapping_rules.yaml +387 -0
- menupilot/data/memory.py +674 -0
- menupilot/data/token_dict.py +275 -0
- menupilot/excel_io/__init__.py +0 -0
- menupilot/excel_io/excel_reader.py +552 -0
- menupilot/excel_io/excel_writer.py +413 -0
- menupilot/main.py +322 -0
- menupilot/wizard.py +86 -0
- menupilot-0.1.0.dist-info/METADATA +397 -0
- menupilot-0.1.0.dist-info/RECORD +33 -0
- menupilot-0.1.0.dist-info/WHEEL +5 -0
- menupilot-0.1.0.dist-info/entry_points.txt +2 -0
- menupilot-0.1.0.dist-info/licenses/LICENSE +21 -0
- menupilot-0.1.0.dist-info/top_level.txt +1 -0
menupilot/agent/tools.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Tool 注册表 — 将规则函数注册为 LLM 可调用的 Tool。
|
|
3
|
+
|
|
4
|
+
每个 Tool 包含 name / description / parameters(JSON Schema) / handler。
|
|
5
|
+
Handler 函数均为我们的规则管线,不经过 LLM 生成,防止幻觉。
|
|
6
|
+
|
|
7
|
+
未来 LangGraph create_react_agent 可直接消费 TOOLS 列表。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable, Dict, List
|
|
11
|
+
|
|
12
|
+
# ── 类型定义 ──────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
ToolDef = Dict[str, Any] # {name, description, parameters, handler, category}
|
|
15
|
+
|
|
16
|
+
TOOLS: List[ToolDef] = []
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register(
|
|
20
|
+
name: str,
|
|
21
|
+
description: str,
|
|
22
|
+
parameters: Dict[str, Any],
|
|
23
|
+
category: str = "pipeline",
|
|
24
|
+
) -> Callable:
|
|
25
|
+
"""装饰器:将一个函数注册为 LLM-callable Tool。
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name: Tool 名称(LLM 通过此名称调用)。
|
|
29
|
+
description: 自然语言描述(LLM 据此判断何时使用)。
|
|
30
|
+
parameters: JSON Schema 格式的参数定义。
|
|
31
|
+
category: "pipeline"(核心管线,禁止 LLM 自行实现)
|
|
32
|
+
或 "supplementary"(辅助操作,LLM 可生成代码)。
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
装饰器函数。
|
|
36
|
+
"""
|
|
37
|
+
def decorator(handler: Callable) -> Callable:
|
|
38
|
+
TOOLS.append({
|
|
39
|
+
"name": name,
|
|
40
|
+
"description": description,
|
|
41
|
+
"parameters": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": parameters,
|
|
44
|
+
"required": [
|
|
45
|
+
k for k, v in parameters.items()
|
|
46
|
+
if v.get("required", False)
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
"handler": handler,
|
|
50
|
+
"category": category,
|
|
51
|
+
})
|
|
52
|
+
return handler
|
|
53
|
+
return decorator
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_tools_for_langgraph() -> List[Dict[str, Any]]:
|
|
57
|
+
"""返回 LangGraph create_react_agent 兼容的 Tool 列表。
|
|
58
|
+
|
|
59
|
+
LangGraph 期望每个 tool 是一个可调用对象(函数),
|
|
60
|
+
因此返回 handler 函数列表。
|
|
61
|
+
"""
|
|
62
|
+
return [t["handler"] for t in TOOLS]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_tool_by_name(name: str) -> ToolDef:
|
|
66
|
+
"""按名称查找 Tool。"""
|
|
67
|
+
for t in TOOLS:
|
|
68
|
+
if t["name"] == name:
|
|
69
|
+
return t
|
|
70
|
+
raise KeyError(f"Tool 不存在: {name}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
74
|
+
# Tool 注册
|
|
75
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@register(
|
|
79
|
+
name="run_sop_matching",
|
|
80
|
+
description=(
|
|
81
|
+
"执行 SOP 匹配管线——将主数据表的 SOP 代码映射填充到 POS 模板。"
|
|
82
|
+
"调用前通过 ask_user 一次性展示完整列映射方案让用户整体确认,不要逐列询问。"
|
|
83
|
+
"Schema Analyzer 会自动识别列映射,直接展示结果请用户确认即可。"
|
|
84
|
+
"用户说 --sheet N → 传 template_sheet=N"
|
|
85
|
+
),
|
|
86
|
+
parameters={
|
|
87
|
+
"master_path": {"type": "string", "description": "主数据表路径"},
|
|
88
|
+
"template_path": {"type": "string", "description": "POS 模板路径"},
|
|
89
|
+
"output_path": {"type": "string", "description": "输出路径"},
|
|
90
|
+
"target_col": {"type": "string", "description": "目标填充列,默认「配料」"},
|
|
91
|
+
"template_sheet": {"type": "integer", "description": "模板 Sheet 序号,用户说 --sheet N 时传 N"},
|
|
92
|
+
"master_sheet": {"type": "integer", "description": "主数据 Sheet 序号,默认 0"},
|
|
93
|
+
"column_mapping": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"description": "仅当 Schema Analyzer 无法识别列名时才需手动传入",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
category="pipeline",
|
|
99
|
+
)
|
|
100
|
+
def run_sop_matching(
|
|
101
|
+
master_path: str, template_path: str, output_path: str = "",
|
|
102
|
+
target_col: str = "配料", template_sheet: int = 0,
|
|
103
|
+
master_sheet: int = 0, column_mapping: dict = None,
|
|
104
|
+
) -> dict:
|
|
105
|
+
"""执行 SOP 匹配管线(Agent 调用入口)。"""
|
|
106
|
+
if not output_path:
|
|
107
|
+
output_path = template_path.replace(".xlsx", "_output.xlsx")
|
|
108
|
+
from menupilot.agent.orchestration import run_sop_pipeline_kwargs
|
|
109
|
+
return run_sop_pipeline_kwargs(
|
|
110
|
+
master_path=master_path, template_path=template_path,
|
|
111
|
+
output_path=output_path, target_col=target_col,
|
|
112
|
+
template_sheet=template_sheet, master_sheet=master_sheet,
|
|
113
|
+
column_mapping=column_mapping,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@register(
|
|
118
|
+
name="run_option_expansion",
|
|
119
|
+
description=(
|
|
120
|
+
"将产品选项规格(糖度/温度/规格/奶底/茶底)展开为模板明细行。"
|
|
121
|
+
"当用户说「展开选项」「生成规格表」「选项展开」时必须调用此工具。"
|
|
122
|
+
"禁止自行生成展开逻辑。"
|
|
123
|
+
),
|
|
124
|
+
parameters={
|
|
125
|
+
"master_path": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "选项规格主数据表 Excel 路径",
|
|
128
|
+
},
|
|
129
|
+
"template_path": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "空白选项模板 Excel 路径(含表头)",
|
|
132
|
+
},
|
|
133
|
+
"output_path": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"description": "输出 Excel 文件路径",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
category="pipeline",
|
|
139
|
+
)
|
|
140
|
+
def run_option_expansion(
|
|
141
|
+
master_path: str, template_path: str, output_path: str,
|
|
142
|
+
) -> dict:
|
|
143
|
+
"""执行选项规格展开管线。"""
|
|
144
|
+
from menupilot.agent.orchestration import run_expand_pipeline
|
|
145
|
+
exit_code = run_expand_pipeline([
|
|
146
|
+
"--master", master_path, "--template", template_path, "--output", output_path,
|
|
147
|
+
])
|
|
148
|
+
return {"ok": exit_code == 0, "output_path": output_path, "exit_code": exit_code}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
152
|
+
# ask_user — Agent 规划落地锚点
|
|
153
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
154
|
+
|
|
155
|
+
@register(
|
|
156
|
+
name="ask_user",
|
|
157
|
+
description=(
|
|
158
|
+
"当对列语义、用户意图或操作确认不确定时,必须调用此工具。"
|
|
159
|
+
"不要猜测列的含义、不要假设用户的意图——不确定就问。"
|
|
160
|
+
"典型场景:列名含糊(如「自定义字段A」)、用户指令有歧义、写入前确认。"
|
|
161
|
+
),
|
|
162
|
+
parameters={
|
|
163
|
+
"question": {"type": "string", "description": "向用户提出的问题"},
|
|
164
|
+
},
|
|
165
|
+
category="interactive",
|
|
166
|
+
)
|
|
167
|
+
def ask_user(question: str) -> str:
|
|
168
|
+
"""向用户提问并获取回答。"""
|
|
169
|
+
return input(f"\nAgent: {question}\n你: ")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
173
|
+
# read_excel_info — Schema 分析入口
|
|
174
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
175
|
+
|
|
176
|
+
@register(
|
|
177
|
+
name="read_excel_info",
|
|
178
|
+
description=(
|
|
179
|
+
"读取 Excel 文件结构——列名、行数、每列前3行样例值和空值数量。"
|
|
180
|
+
"在操作任何 Excel 文件前必须先调用此工具了解其结构,禁止猜测列名。"
|
|
181
|
+
),
|
|
182
|
+
parameters={
|
|
183
|
+
"filepath": {"type": "string", "description": "Excel 文件路径"},
|
|
184
|
+
"sheet_name": {"type": "integer", "description": "Sheet 序号,默认 0"},
|
|
185
|
+
},
|
|
186
|
+
category="schema",
|
|
187
|
+
)
|
|
188
|
+
def read_excel_info(filepath: str, sheet_name: int = 0) -> dict:
|
|
189
|
+
"""读取 Excel 结构信息。"""
|
|
190
|
+
import pandas as pd
|
|
191
|
+
try:
|
|
192
|
+
df = pd.read_excel(filepath, sheet_name=sheet_name)
|
|
193
|
+
return {
|
|
194
|
+
"filepath": filepath,
|
|
195
|
+
"columns": list(df.columns),
|
|
196
|
+
"row_count": len(df),
|
|
197
|
+
"sample_values": {
|
|
198
|
+
str(c): [str(v) for v in df[c].dropna().head(3).tolist()]
|
|
199
|
+
for c in df.columns
|
|
200
|
+
},
|
|
201
|
+
"null_counts": {str(c): int(df[c].isna().sum()) for c in df.columns},
|
|
202
|
+
"dtypes": {str(c): str(df[c].dtype) for c in df.columns},
|
|
203
|
+
}
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return {"error": str(e)}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@register(
|
|
209
|
+
name="execute_python",
|
|
210
|
+
description=(
|
|
211
|
+
"在安全沙箱中执行 Python 代码以查询或修改 Excel 数据。"
|
|
212
|
+
"仅用于辅助操作(增删改查数据、查看文件结构等)。"
|
|
213
|
+
"严格禁止:在此工具中实现匹配/填充/展开逻辑——"
|
|
214
|
+
"这些必须通过 run_sop_matching 或 run_option_expansion 完成。"
|
|
215
|
+
"可用的 Python 库:pandas, openpyxl, numpy, json, csv, re。"
|
|
216
|
+
),
|
|
217
|
+
parameters={
|
|
218
|
+
"code": {
|
|
219
|
+
"type": "string",
|
|
220
|
+
"description": (
|
|
221
|
+
"要执行的 Python 代码。可使用 pandas 读取 Excel、"
|
|
222
|
+
"openpyxl 操作工作簿。代码中赋值的变量会返回。"
|
|
223
|
+
),
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
category="supplementary",
|
|
227
|
+
)
|
|
228
|
+
def execute_python(code: str) -> dict:
|
|
229
|
+
"""在沙箱中执行 Python 代码。"""
|
|
230
|
+
from menupilot.agent.sandbox import execute as sandbox_execute
|
|
231
|
+
return sandbox_execute(code)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@register(
|
|
235
|
+
name="query_token_dict",
|
|
236
|
+
description=(
|
|
237
|
+
"查询 Token 词典——查看系统中已注册的属性词及其类型。"
|
|
238
|
+
"可用于确认某个值(如「七分糖」)属于哪个维度(糖度/温度/规格等)。"
|
|
239
|
+
),
|
|
240
|
+
parameters={
|
|
241
|
+
"action": {
|
|
242
|
+
"type": "string",
|
|
243
|
+
"enum": ["lookup", "list_types", "list_values"],
|
|
244
|
+
"description": "操作类型:lookup=查单个词, list_types=列出所有类型, list_values=列出某类型下所有词",
|
|
245
|
+
},
|
|
246
|
+
"value": {
|
|
247
|
+
"type": "string",
|
|
248
|
+
"description": "要查询的词(action=lookup 时必填)",
|
|
249
|
+
},
|
|
250
|
+
"token_type": {
|
|
251
|
+
"type": "string",
|
|
252
|
+
"description": "类型名(action=list_values 时必填,如「糖度」「温度」)",
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
category="supplementary",
|
|
256
|
+
)
|
|
257
|
+
def query_token_dict(
|
|
258
|
+
action: str,
|
|
259
|
+
value: str = "",
|
|
260
|
+
token_type: str = "",
|
|
261
|
+
) -> dict:
|
|
262
|
+
"""查询 Token 词典。"""
|
|
263
|
+
from menupilot.data.token_dict import lookup, list_types, get_tokens_by_type
|
|
264
|
+
|
|
265
|
+
if action == "lookup":
|
|
266
|
+
if not value:
|
|
267
|
+
return {"error": "lookup 需要提供 value 参数"}
|
|
268
|
+
result_type = lookup(value)
|
|
269
|
+
return {"value": value, "type": result_type, "is_known": result_type != "UNKNOWN_TOKEN"}
|
|
270
|
+
|
|
271
|
+
if action == "list_types":
|
|
272
|
+
types = list_types()
|
|
273
|
+
return {"types": types}
|
|
274
|
+
|
|
275
|
+
if action == "list_values":
|
|
276
|
+
if not token_type:
|
|
277
|
+
return {"error": "list_values 需要提供 token_type 参数"}
|
|
278
|
+
values = get_tokens_by_type(token_type)
|
|
279
|
+
return {"type": token_type, "values": values}
|
|
280
|
+
|
|
281
|
+
return {"error": f"未知 action: {action}"}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ── 自测 ──────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
passed = 0
|
|
288
|
+
failed = 0
|
|
289
|
+
|
|
290
|
+
def check(condition, msg):
|
|
291
|
+
global passed, failed
|
|
292
|
+
if condition:
|
|
293
|
+
passed += 1
|
|
294
|
+
print(f" PASS {msg}")
|
|
295
|
+
else:
|
|
296
|
+
failed += 1
|
|
297
|
+
print(f" FAIL {msg}")
|
|
298
|
+
|
|
299
|
+
print("=== Tool Registry 自测 ===\n")
|
|
300
|
+
|
|
301
|
+
# ── 1. Tool 注册数量 ──
|
|
302
|
+
print("1. Tool 注册数量")
|
|
303
|
+
check(len(TOOLS) >= 4, f"至少 4 个 Tool 已注册(实际 {len(TOOLS)})")
|
|
304
|
+
print()
|
|
305
|
+
|
|
306
|
+
# ── 2. 每个 Tool 的完整性 ──
|
|
307
|
+
print("2. 每个 Tool 结构完整性")
|
|
308
|
+
required_keys = {"name", "description", "parameters", "handler", "category"}
|
|
309
|
+
for t in TOOLS:
|
|
310
|
+
missing = required_keys - set(t.keys())
|
|
311
|
+
check(not missing, f"{t['name']}: 结构完整")
|
|
312
|
+
check(callable(t["handler"]), f"{t['name']}: handler 可调用")
|
|
313
|
+
check("type" in t["parameters"], f"{t['name']}: parameters 含 type")
|
|
314
|
+
check("properties" in t["parameters"], f"{t['name']}: parameters 含 properties")
|
|
315
|
+
print()
|
|
316
|
+
|
|
317
|
+
# ── 3. 分类正确 ──
|
|
318
|
+
print("3. Tool 分类")
|
|
319
|
+
pipelines = [t for t in TOOLS if t["category"] == "pipeline"]
|
|
320
|
+
supplements = [t for t in TOOLS if t["category"] == "supplementary"]
|
|
321
|
+
check(len(pipelines) >= 2, f"pipeline 类 Tool ≥ 2(实际 {len(pipelines)})")
|
|
322
|
+
check(len(supplements) >= 2, f"supplementary 类 Tool ≥ 2(实际 {len(supplements)})")
|
|
323
|
+
print()
|
|
324
|
+
|
|
325
|
+
# ── 4. get_tool_by_name ──
|
|
326
|
+
print("4. get_tool_by_name 查找")
|
|
327
|
+
t = get_tool_by_name("run_sop_matching")
|
|
328
|
+
check(t["name"] == "run_sop_matching", "找到 run_sop_matching")
|
|
329
|
+
try:
|
|
330
|
+
get_tool_by_name("nonexistent")
|
|
331
|
+
check(False, "不存在的 Tool 应抛 KeyError")
|
|
332
|
+
except KeyError:
|
|
333
|
+
check(True, "不存在的 Tool 正确抛出 KeyError")
|
|
334
|
+
print()
|
|
335
|
+
|
|
336
|
+
# ── 5. query_token_dict ──
|
|
337
|
+
print("5. query_token_dict 功能")
|
|
338
|
+
r = query_token_dict("lookup", value="七分糖")
|
|
339
|
+
check(r["type"] == "糖度", f"七分糖 → 糖度(实际 {r['type']})")
|
|
340
|
+
check(r["is_known"] is True, "is_known=True")
|
|
341
|
+
|
|
342
|
+
r2 = query_token_dict("list_types")
|
|
343
|
+
check("糖度" in r2["types"] and "温度" in r2["types"], "list_types 包含糖度和温度")
|
|
344
|
+
|
|
345
|
+
r3 = query_token_dict("lookup", value="不存在的词xyz")
|
|
346
|
+
check(r3["is_known"] is False, "未知词 is_known=False")
|
|
347
|
+
check(r3["type"] == "UNKNOWN_TOKEN", "未知词 type=UNKNOWN_TOKEN")
|
|
348
|
+
print()
|
|
349
|
+
|
|
350
|
+
# ── 6. get_tools_for_langgraph ──
|
|
351
|
+
print("6. get_tools_for_langgraph")
|
|
352
|
+
lg_tools = get_tools_for_langgraph()
|
|
353
|
+
check(len(lg_tools) == len(TOOLS), f"数量一致({len(lg_tools)})")
|
|
354
|
+
check(all(callable(t) for t in lg_tools), "全部可调用")
|
|
355
|
+
print()
|
|
356
|
+
|
|
357
|
+
# ── 7. execute_python 可以执行 ──
|
|
358
|
+
print("7. execute_python 调用沙箱")
|
|
359
|
+
r = execute_python("x = 1 + 2")
|
|
360
|
+
check(r["result"]["x"] == 3, f"x = 3(实际 {r['result']})")
|
|
361
|
+
check("error" not in r, "无错误")
|
|
362
|
+
print()
|
|
363
|
+
|
|
364
|
+
# ── 汇总 ──
|
|
365
|
+
print(f"=== 结果: {passed} passed, {failed} failed ===")
|