fix(export,cli): GraphML null-attr coercion + save-result --answer-file (#1502)
Two cross-platform fixes salvaged from #1502: - to_graphml: nx.write_graphml raises ValueError on None attribute values, so a node/edge carrying a null field crashed the export. Coerce None -> "" for node and edge attributes before writing. - save-result: add --answer-file as an alternative to --answer so long or multiline answers can be passed via a file instead of a fragile inline shell arg (notably Windows/PowerShell quoting). Exactly one of --answer / --answer-file is required. The rest of #1502 (a version downgrade and a hand-edited generated skill-windows.md that fails skillgen --check, plus duplicated windows-scripts) is left for rework on the PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
safishamsi
co-authored by
Claude Opus 4.8
parent
4e4935a64a
commit
407a7f142d
@@ -2970,13 +2970,18 @@ def main() -> None:
|
||||
|
||||
p = _ap.ArgumentParser(prog="graphify save-result")
|
||||
p.add_argument("--question", required=True)
|
||||
p.add_argument("--answer", required=True)
|
||||
p.add_argument("--answer", default=None)
|
||||
p.add_argument("--answer-file", dest="answer_file", default=None)
|
||||
p.add_argument("--type", dest="query_type", default="query")
|
||||
p.add_argument("--nodes", nargs="*", default=[])
|
||||
p.add_argument("--outcome", choices=("useful", "dead_end", "corrected"), default=None)
|
||||
p.add_argument("--correction", default=None)
|
||||
p.add_argument("--memory-dir", default=str(Path(_GRAPHIFY_OUT) / "memory"))
|
||||
opts = p.parse_args(sys.argv[2:])
|
||||
if opts.answer_file:
|
||||
opts.answer = Path(opts.answer_file).read_text(encoding="utf-8").strip()
|
||||
elif not opts.answer:
|
||||
p.error("--answer or --answer-file is required")
|
||||
from graphify.ingest import save_query_result as _sqr
|
||||
|
||||
out = _sqr(
|
||||
|
||||
@@ -1535,6 +1535,15 @@ def to_graphml(
|
||||
for _, _, attrs in H.edges(data=True):
|
||||
for k in [k for k in attrs if k.startswith("_")]:
|
||||
del attrs[k]
|
||||
# nx.write_graphml raises ValueError on None attribute values; replace with "".
|
||||
for node_id in H.nodes():
|
||||
for key, val in list(H.nodes[node_id].items()):
|
||||
if val is None:
|
||||
H.nodes[node_id][key] = ""
|
||||
for u, v in H.edges():
|
||||
for key, val in list(H.edges[u, v].items()):
|
||||
if val is None:
|
||||
H.edges[u, v][key] = ""
|
||||
nx.write_graphml(H, output_path)
|
||||
|
||||
|
||||
|
||||
@@ -82,6 +82,23 @@ def test_to_graphml_has_community_attribute():
|
||||
content = out.read_text()
|
||||
assert "community" in content
|
||||
|
||||
def test_to_graphml_tolerates_none_attribute_values():
|
||||
"""nx.write_graphml raises ValueError on a None attribute value; to_graphml
|
||||
must coerce None -> "" so a node/edge with a null field still exports (#1502)."""
|
||||
G = make_graph()
|
||||
communities = cluster(G)
|
||||
# Inject a None-valued attribute on one node and one edge.
|
||||
a_node = next(iter(G.nodes()))
|
||||
G.nodes[a_node]["nullable_field"] = None
|
||||
if G.number_of_edges():
|
||||
u, v = next(iter(G.edges()))
|
||||
G.edges[u, v]["nullable_field"] = None
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = Path(tmp) / "graph.graphml"
|
||||
to_graphml(G, communities, str(out)) # must not raise
|
||||
content = out.read_text()
|
||||
assert "<graphml" in content
|
||||
|
||||
def test_to_html_creates_file():
|
||||
G = make_graph()
|
||||
communities = cluster(G)
|
||||
|
||||
@@ -478,6 +478,27 @@ def test_cli_save_result_rejects_bad_outcome(tmp_path):
|
||||
assert "great" in (r.stderr + r.stdout)
|
||||
|
||||
|
||||
def test_cli_save_result_reads_answer_from_file(tmp_path):
|
||||
"""--answer-file lets callers pass a long/multiline answer via a file instead
|
||||
of a fragile inline arg (Windows/PowerShell quoting), #1502."""
|
||||
ans = tmp_path / "answer.txt"
|
||||
ans.write_text("line one\nline two with a \"quote\"\n", encoding="utf-8")
|
||||
r = _run(["save-result", "--question", "how does auth work?",
|
||||
"--answer-file", str(ans), "--outcome", "useful"], tmp_path)
|
||||
assert r.returncode == 0, r.stderr
|
||||
docs = list((tmp_path / "graphify-out" / "memory").glob("*.md"))
|
||||
assert docs, "save-result wrote no memory doc"
|
||||
body = docs[0].read_text(encoding="utf-8")
|
||||
assert "line one" in body and "line two" in body
|
||||
|
||||
|
||||
def test_cli_save_result_requires_answer_or_answer_file(tmp_path):
|
||||
"""Neither --answer nor --answer-file -> clean argparse error, not a crash."""
|
||||
r = _run(["save-result", "--question", "q", "--outcome", "useful"], tmp_path)
|
||||
assert r.returncode != 0
|
||||
assert "--answer" in (r.stderr + r.stdout)
|
||||
|
||||
|
||||
def test_cli_reflect_cold_start_writes_empty_lessons(tmp_path):
|
||||
"""First run with no graphify-out/memory/ still succeeds and writes a valid doc."""
|
||||
r = _run(["reflect"], tmp_path)
|
||||
|
||||
Reference in New Issue
Block a user