diff --git a/server/agent_core/intent.py b/server/agent_core/intent.py index cd2e46c..22dc400 100644 --- a/server/agent_core/intent.py +++ b/server/agent_core/intent.py @@ -210,7 +210,7 @@ def _is_imperative_schedule(text: str) -> bool: def _extract_explicit_data_source(text: str) -> dict[str, str]: """Extract an absolute data directory/file pair from a chat command.""" t = str(text or "") - ext = r"(?:xlsx|xlsm|csv|txt|sql)" + ext = r"(?:xlsx|xlsm|csv|txt|sql|json)" file_match = re.search(rf"([A-Za-z]:[\\/][^\r\n\"'<>|,,。;;]*?\.{ext})", t, re.I) dir_match = re.search(r"([A-Za-z]:[\\/][^\r\n\"'<>|,,。;;]+?)\s*目录下", t, re.I) data_dir = "" @@ -261,7 +261,7 @@ def parse_fast(text: str, world: World) -> IntentResult | None: params["drawingAction"] = "analyze" return hit("data.analyze", params) # 带真实表格/SQL 路径的口令优先按数据处理,不能被句尾「知识库」误吞。 - has_data_path = bool(re.search(r"[A-Za-z]:[\\/].*\.(xlsx|xlsm|csv|txt|sql)\b", t, re.I)) + has_data_path = bool(re.search(r"[A-Za-z]:[\\/].*\.(xlsx|xlsm|csv|txt|sql|json)\b", t, re.I)) explicit_data_algorithm_schedule = bool( re.search(r"(?:使用|用).+?数据(?:包)?", t, re.I) and re.search(r"(?:算法|\b(?:EDD|SPT|PRIORITY|FIFO|LPT|CR|ATC)\b|\boptimize\b)", t, re.I) diff --git a/server/aps_domain/workflow.py b/server/aps_domain/workflow.py index 0648f78..67a67d3 100644 --- a/server/aps_domain/workflow.py +++ b/server/aps_domain/workflow.py @@ -1807,7 +1807,6 @@ def _run_flex(store: WorldStore, intent: IntentResult, actor: str, data_dir = str(intent.params.get("dataDir") or "").strip() data_file = str(intent.params.get("dataFile") or "").strip() if data_path or data_dir: - from server.aps_domain.project_analyze import analyze_project_deep source_query = data_path or (os.path.join(data_dir, data_file) if data_file else data_dir) if data_file and not os.path.isfile(source_query): return AgentReply(text=f"数据文件不存在:{source_query},没有执行排产。") @@ -1815,27 +1814,61 @@ def _run_flex(store: WorldStore, intent: IntentResult, actor: str, return AgentReply(text=f"数据目录不存在:{data_dir},没有执行排产。") if data_path and not data_file and not os.path.isfile(data_path) and not os.path.isdir(data_path): return AgentReply(text=f"数据路径不存在:{data_path},没有执行排产。") - try: - source_report = analyze_project_deep( - store.data, session_id, apply_sql=True, - query=source_query, next_id=store.next_id, - ) - except (OSError, PermissionError, ValueError) as exc: - return AgentReply(text=f"数据文件读取失败:{exc}") - if not source_report.get("ok"): - return AgentReply(text=f"数据目录分析失败:{source_report.get('error') or '目录不可用'}") - if not source_report.get("canSchedule"): - missing = (source_report.get("plan") or [])[:3] - detail = ";".join(str(item) for item in missing) - return AgentReply( - text=(f"数据已读取,但暂时不能排产:缺少可排所需的订单、工艺或设备信息。" - + (f"\n{detail}" if detail else "")), - ) - data_source = { - "directory": source_report.get("workDir") or data_dir, - "file": data_file or (os.path.basename(data_path) if data_path else None), - "paths": source_report.get("sourcePaths") or [], - } + if os.path.isfile(source_query) and source_query.lower().endswith(".json"): + from server.state.packs import load_pack + try: + candidate_world = load_pack(source_query) + except (OSError, ValueError, TypeError) as exc: + return AgentReply(text=f"APS 数据包读取失败:{exc},没有执行排产。") + required = ("flexOrders", "flexRoutings", "flexEquipment") + if not isinstance(candidate_world, dict) or not all(candidate_world.get(key) for key in required): + return AgentReply( + text="该 JSON 不是可排产的 APS 数据包:至少需要订单、工艺和设备数据,没有执行排产。", + ) + store.data = candidate_world + store._reset_counters() + data_pack = { + "file": os.path.basename(source_query), + "name": os.path.splitext(os.path.basename(source_query))[0], + "path": source_query, + } + data_source = { + "directory": os.path.dirname(source_query), + "file": os.path.basename(source_query), + "paths": [os.path.abspath(source_query)], + } + else: + from server.aps_domain.project_analyze import analyze_project_deep + from server.state.seed import empty_world + candidate_world = empty_world() + try: + source_report = analyze_project_deep( + candidate_world, session_id, apply_sql=True, + query=source_query, next_id=store.next_id, + ) + except (OSError, PermissionError, ValueError) as exc: + return AgentReply(text=f"数据文件读取失败:{exc}") + if not source_report.get("ok"): + return AgentReply(text=f"数据目录分析失败:{source_report.get('error') or '目录不可用'}") + source_paths = source_report.get("sourcePaths") or [] + if not source_paths: + return AgentReply( + text="指定位置没有找到可读取的排产数据文件,没有执行排产。", + ) + if not source_report.get("canSchedule"): + missing = (source_report.get("plan") or [])[:3] + detail = ";".join(str(item) for item in missing) + return AgentReply( + text=(f"数据已读取,但暂时不能排产:缺少可排所需的订单、工艺或设备信息。" + + (f"\n{detail}" if detail else "")), + ) + store.data = candidate_world + store._reset_counters() + data_source = { + "directory": source_report.get("workDir") or data_dir, + "file": data_file or (os.path.basename(data_path) if data_path else None), + "paths": source_paths, + } data_pack_ref = str(intent.params.get("dataPackRef") or "").strip() if data_pack_ref: from server.state.packs import load_pack, resolve_pack_reference diff --git a/tests/golden/test_optimize_chat_data_selection.py b/tests/golden/test_optimize_chat_data_selection.py index f14b4e5..a522d88 100644 --- a/tests/golden/test_optimize_chat_data_selection.py +++ b/tests/golden/test_optimize_chat_data_selection.py @@ -5,6 +5,7 @@ from pathlib import Path from server.agent_core.intent import parse_fast from server.aps_domain.workflow import handle_intent +from server.contracts import IntentResult from server.state.packs import load_pack, resolve_pack_reference from server.state.store import WorldStore @@ -49,6 +50,21 @@ def test_chat_command_parses_directory_file_and_algorithm_slots(): assert result.params["engine"] == "OPTIMIZE" +def test_chat_command_parses_directory_json_pack_and_algorithm_slots(): + result = parse_fast( + r"使用 D:\aps-data 目录下的 optimize-simulation-v1.json 数据用 EDD 算法生成排产方案", + {}, + ) + + assert result is not None + assert result.intent == "flex.schedule" + assert result.params["dataDir"] == r"D:\aps-data" + assert result.params["dataFile"] == "optimize-simulation-v1.json" + assert result.params["dataPath"].endswith(r"aps-data\optimize-simulation-v1.json") + assert result.params["sortMode"] == "EDD" + assert result.params["engine"] == "OPTIMIZE" + + def test_chat_command_loads_pack_and_materializes_optimize_result(tmp_path, monkeypatch): monkeypatch.setenv("APS_DB_DISABLED", "1") store = WorldStore(path=str(tmp_path / "world.json")) @@ -71,6 +87,54 @@ def test_chat_command_loads_pack_and_materializes_optimize_result(tmp_path, monk assert store.data["flexScheduleVersions"][-1]["algorithmId"] == "optimize.edd" +def test_explicit_json_pack_path_materializes_optimize_result(tmp_path, monkeypatch): + monkeypatch.setenv("APS_DB_DISABLED", "1") + store = WorldStore(path=str(tmp_path / "world.json")) + intent = IntentResult( + intent="flex.schedule", + params={ + "dataDir": str(PACK_PATH.parent), + "dataFile": PACK_PATH.name, + "dataPath": str(PACK_PATH), + "engine": "OPTIMIZE", + "sortMode": "EDD", + }, + confidence=0.95, + source="RULE_FAST", + ) + + reply = asyncio.run(handle_intent(store, "round88-json", intent, actor="test")) + + block = next(block for block in reply.blocks if block.type == "flex-schedule") + assert block.props["dataPack"]["file"] == PACK_PATH.name + assert block.props["dataSource"]["file"] == PACK_PATH.name + assert block.props["dataSource"]["paths"] == [str(PACK_PATH.resolve())] + assert block.props["algorithm"]["rule"] == "EDD" + assert block.props["stats"]["vlCount"] == 10 + assert block.props["stats"]["woCount"] == 72 + + +def test_empty_explicit_directory_does_not_reuse_current_world(tmp_path, monkeypatch): + monkeypatch.setenv("APS_DB_DISABLED", "1") + store = WorldStore(path=str(tmp_path / "world.json")) + store.data = load_pack(str(PACK_PATH)) + store._reset_counters() + before_versions = len(store.data.get("flexScheduleVersions") or []) + empty_dir = tmp_path / "empty-data" + empty_dir.mkdir() + intent = IntentResult( + intent="flex.schedule", + params={"dataDir": str(empty_dir), "engine": "OPTIMIZE", "sortMode": "EDD"}, + confidence=0.95, + source="RULE_FAST", + ) + + reply = asyncio.run(handle_intent(store, "round88-empty", intent, actor="test")) + + assert "没有找到可读取的排产数据文件" in reply.text + assert len(store.data.get("flexScheduleVersions") or []) == before_versions + + def test_unknown_pack_fails_closed_without_creating_schedule(tmp_path, monkeypatch): monkeypatch.setenv("APS_DB_DISABLED", "1") store = WorldStore(path=str(tmp_path / "world.json"))