修复显式数据源串用旧数据
支持通过目录和文件名加载标准 APS JSON 数据包;表格目录改为先导入独立候选世界,未读取到文件或数据不完整时拒绝排产,避免沿用当前会话数据。补充 JSON 解析、成功排产和空目录防串数据测试。
This commit is contained in:
parent
cd8806f649
commit
b8fb217c09
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
Loading…
Reference in New Issue