feat: add model override for community labeling
This commit is contained in:
+13
-1
@@ -2141,8 +2141,10 @@ def main() -> None:
|
|||||||
print(" --graph <path> path to graph.json (default <path>/graphify-out/graph.json)")
|
print(" --graph <path> path to graph.json (default <path>/graphify-out/graph.json)")
|
||||||
print(" --no-label keep 'Community N' placeholders (skip LLM community naming)")
|
print(" --no-label keep 'Community N' placeholders (skip LLM community naming)")
|
||||||
print(" --backend=<name> backend to use for community naming (default: auto-detect)")
|
print(" --backend=<name> backend to use for community naming (default: auto-detect)")
|
||||||
|
print(" --model=<name> model to use for community naming")
|
||||||
print(" label <path> (re)name communities with the configured LLM backend, regenerate report")
|
print(" label <path> (re)name communities with the configured LLM backend, regenerate report")
|
||||||
print(" --backend=<name> backend to use (default: auto-detect from API keys)")
|
print(" --backend=<name> backend to use (default: auto-detect from API keys)")
|
||||||
|
print(" --model=<name> model to use for community naming")
|
||||||
print(" query \"<question>\" BFS traversal of graph.json for a question")
|
print(" query \"<question>\" BFS traversal of graph.json for a question")
|
||||||
print(" --dfs use depth-first instead of breadth-first")
|
print(" --dfs use depth-first instead of breadth-first")
|
||||||
print(" --context C explicit edge-context filter (repeatable)")
|
print(" --context C explicit edge-context filter (repeatable)")
|
||||||
@@ -3131,6 +3133,8 @@ def main() -> None:
|
|||||||
no_label = "--no-label" in sys.argv
|
no_label = "--no-label" in sys.argv
|
||||||
_backend_arg = next((a for a in sys.argv if a.startswith("--backend=")), None)
|
_backend_arg = next((a for a in sys.argv if a.startswith("--backend=")), None)
|
||||||
label_backend = _backend_arg.split("=", 1)[1] if _backend_arg else None
|
label_backend = _backend_arg.split("=", 1)[1] if _backend_arg else None
|
||||||
|
_model_arg = next((a for a in sys.argv if a.startswith("--model=")), None)
|
||||||
|
label_model = _model_arg.split("=", 1)[1] if _model_arg else None
|
||||||
_min_cs_arg = next((a for a in sys.argv if a.startswith("--min-community-size=")), None)
|
_min_cs_arg = next((a for a in sys.argv if a.startswith("--min-community-size=")), None)
|
||||||
min_community_size = int(_min_cs_arg.split("=")[1]) if _min_cs_arg else 3
|
min_community_size = int(_min_cs_arg.split("=")[1]) if _min_cs_arg else 3
|
||||||
args = sys.argv[2:]
|
args = sys.argv[2:]
|
||||||
@@ -3143,6 +3147,14 @@ def main() -> None:
|
|||||||
a = args[i_arg]
|
a = args[i_arg]
|
||||||
if a == "--graph" and i_arg + 1 < len(args):
|
if a == "--graph" and i_arg + 1 < len(args):
|
||||||
graph_override = Path(args[i_arg + 1]); i_arg += 2
|
graph_override = Path(args[i_arg + 1]); i_arg += 2
|
||||||
|
elif a == "--backend" and i_arg + 1 < len(args):
|
||||||
|
label_backend = args[i_arg + 1]; i_arg += 2
|
||||||
|
elif a.startswith("--backend="):
|
||||||
|
label_backend = a.split("=", 1)[1]; i_arg += 1
|
||||||
|
elif a == "--model" and i_arg + 1 < len(args):
|
||||||
|
label_model = args[i_arg + 1]; i_arg += 2
|
||||||
|
elif a.startswith("--model="):
|
||||||
|
label_model = a.split("=", 1)[1]; i_arg += 1
|
||||||
elif a == "--resolution" and i_arg + 1 < len(args):
|
elif a == "--resolution" and i_arg + 1 < len(args):
|
||||||
co_resolution = float(args[i_arg + 1]); i_arg += 2
|
co_resolution = float(args[i_arg + 1]); i_arg += 2
|
||||||
elif a.startswith("--resolution="):
|
elif a.startswith("--resolution="):
|
||||||
@@ -3240,7 +3252,7 @@ def main() -> None:
|
|||||||
# The final labels (LLM or placeholder fallback) are persisted to
|
# The final labels (LLM or placeholder fallback) are persisted to
|
||||||
# .graphify_labels.json by the unconditional write below.
|
# .graphify_labels.json by the unconditional write below.
|
||||||
labels, _ = generate_community_labels(
|
labels, _ = generate_community_labels(
|
||||||
G, communities, backend=label_backend, gods=gods
|
G, communities, backend=label_backend, model=label_model, gods=gods
|
||||||
)
|
)
|
||||||
questions = suggest_questions(G, communities, labels)
|
questions = suggest_questions(G, communities, labels)
|
||||||
tokens = {"input": 0, "output": 0}
|
tokens = {"input": 0, "output": 0}
|
||||||
|
|||||||
+19
-5
@@ -1716,7 +1716,13 @@ def _merge_into(merged: dict, result: dict) -> None:
|
|||||||
merged["output_tokens"] += result.get("output_tokens", 0)
|
merged["output_tokens"] += result.get("output_tokens", 0)
|
||||||
|
|
||||||
|
|
||||||
def _call_llm(prompt: str, *, backend: str, max_tokens: int = 200) -> str:
|
def _call_llm(
|
||||||
|
prompt: str,
|
||||||
|
*,
|
||||||
|
backend: str,
|
||||||
|
max_tokens: int = 200,
|
||||||
|
model: str | None = None,
|
||||||
|
) -> str:
|
||||||
"""Send a plain-text prompt to `backend` and return the model's text reply.
|
"""Send a plain-text prompt to `backend` and return the model's text reply.
|
||||||
|
|
||||||
Used by lightweight callers (e.g. `graphify.dedup` LLM tiebreaker) that
|
Used by lightweight callers (e.g. `graphify.dedup` LLM tiebreaker) that
|
||||||
@@ -1740,7 +1746,7 @@ def _call_llm(prompt: str, *, backend: str, max_tokens: int = 200) -> str:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"No API key for backend '{backend}'. Set {_format_backend_env_keys(backend)}."
|
f"No API key for backend '{backend}'. Set {_format_backend_env_keys(backend)}."
|
||||||
)
|
)
|
||||||
mdl = _default_model_for_backend(backend)
|
mdl = model or _default_model_for_backend(backend)
|
||||||
|
|
||||||
if backend == "claude":
|
if backend == "claude":
|
||||||
try:
|
try:
|
||||||
@@ -1769,8 +1775,11 @@ def _call_llm(prompt: str, *, backend: str, max_tokens: int = 200) -> str:
|
|||||||
raise RuntimeError("Claude Code CLI not found on $PATH")
|
raise RuntimeError("Claude Code CLI not found on $PATH")
|
||||||
elif shutil.which("claude") is None:
|
elif shutil.which("claude") is None:
|
||||||
raise RuntimeError("Claude Code CLI not found on $PATH")
|
raise RuntimeError("Claude Code CLI not found on $PATH")
|
||||||
|
cli_args = [claude_cmd, "-p", "--output-format", "json", "--no-session-persistence"]
|
||||||
|
if model is not None:
|
||||||
|
cli_args.extend(["--model", mdl])
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
[claude_cmd, "-p", "--output-format", "json", "--no-session-persistence"],
|
cli_args,
|
||||||
input=prompt,
|
input=prompt,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@@ -2033,6 +2042,7 @@ def label_communities(
|
|||||||
communities,
|
communities,
|
||||||
*,
|
*,
|
||||||
backend: str,
|
backend: str,
|
||||||
|
model: str | None = None,
|
||||||
gods=None,
|
gods=None,
|
||||||
max_communities: int | None = None,
|
max_communities: int | None = None,
|
||||||
top_k: int = _LABEL_TOP_K,
|
top_k: int = _LABEL_TOP_K,
|
||||||
@@ -2084,7 +2094,10 @@ def label_communities(
|
|||||||
# _resolve_max_tokens so GRAPHIFY_MAX_OUTPUT_TOKENS applies here too (#1200).
|
# _resolve_max_tokens so GRAPHIFY_MAX_OUTPUT_TOKENS applies here too (#1200).
|
||||||
max_tokens = _resolve_max_tokens(min(64 + 24 * len(batch_cids), 8192))
|
max_tokens = _resolve_max_tokens(min(64 + 24 * len(batch_cids), 8192))
|
||||||
try:
|
try:
|
||||||
text = _call_llm(prompt, backend=backend, max_tokens=max_tokens)
|
call_kwargs = {"backend": backend, "max_tokens": max_tokens}
|
||||||
|
if model is not None:
|
||||||
|
call_kwargs["model"] = model
|
||||||
|
text = _call_llm(prompt, **call_kwargs)
|
||||||
parsed = _parse_label_response(text, batch_cids)
|
parsed = _parse_label_response(text, batch_cids)
|
||||||
labels.update(parsed)
|
labels.update(parsed)
|
||||||
written += len(parsed)
|
written += len(parsed)
|
||||||
@@ -2109,6 +2122,7 @@ def generate_community_labels(
|
|||||||
communities,
|
communities,
|
||||||
*,
|
*,
|
||||||
backend: str | None = None,
|
backend: str | None = None,
|
||||||
|
model: str | None = None,
|
||||||
gods=None,
|
gods=None,
|
||||||
quiet: bool = False,
|
quiet: bool = False,
|
||||||
) -> tuple[dict[int, str], str]:
|
) -> tuple[dict[int, str], str]:
|
||||||
@@ -2130,7 +2144,7 @@ def generate_community_labels(
|
|||||||
)
|
)
|
||||||
return _placeholder_community_labels(communities), "placeholder"
|
return _placeholder_community_labels(communities), "placeholder"
|
||||||
try:
|
try:
|
||||||
labels = label_communities(G, communities, backend=backend, gods=gods)
|
labels = label_communities(G, communities, backend=backend, model=model, gods=gods)
|
||||||
return labels, "llm"
|
return labels, "llm"
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if not quiet:
|
if not quiet:
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
Backend calls are mocked - no network. Covers the happy path, partial replies,
|
Backend calls are mocked - no network. Covers the happy path, partial replies,
|
||||||
malformed replies, and the no-backend fallback.
|
malformed replies, and the no-backend fallback.
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -40,6 +43,71 @@ def test_label_communities_happy_path(monkeypatch):
|
|||||||
assert captured["backend"] == "gemini"
|
assert captured["backend"] == "gemini"
|
||||||
|
|
||||||
|
|
||||||
|
def test_label_communities_passes_model_override(monkeypatch):
|
||||||
|
G, communities = _graph()
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_call(prompt, *, backend, max_tokens=200, model=None):
|
||||||
|
captured["backend"] = backend
|
||||||
|
captured["model"] = model
|
||||||
|
return '{"0": "Order Management", "1": "Payment Flow"}'
|
||||||
|
|
||||||
|
monkeypatch.setattr("graphify.llm._call_llm", fake_call)
|
||||||
|
labels = label_communities(
|
||||||
|
G,
|
||||||
|
communities,
|
||||||
|
backend="gemini",
|
||||||
|
model="gemini-3.1-flash-lite",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert labels == {0: "Order Management", 1: "Payment Flow"}
|
||||||
|
assert captured == {"backend": "gemini", "model": "gemini-3.1-flash-lite"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_label_cli_passes_model_override(tmp_path, monkeypatch):
|
||||||
|
import graphify.__main__ as cli
|
||||||
|
|
||||||
|
out = tmp_path / "graphify-out"
|
||||||
|
out.mkdir()
|
||||||
|
graph = {
|
||||||
|
"directed": False,
|
||||||
|
"multigraph": False,
|
||||||
|
"nodes": [
|
||||||
|
{"id": "n1", "label": "OrderService", "community": 0},
|
||||||
|
],
|
||||||
|
"links": [],
|
||||||
|
}
|
||||||
|
(out / "graph.json").write_text(json.dumps(graph), encoding="utf-8")
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_generate(G, communities, *, backend=None, model=None, gods=None, quiet=False):
|
||||||
|
captured["backend"] = backend
|
||||||
|
captured["model"] = model
|
||||||
|
return {0: "Orders"}, "llm"
|
||||||
|
|
||||||
|
monkeypatch.setattr("graphify.llm.generate_community_labels", fake_generate)
|
||||||
|
monkeypatch.setattr("graphify.export.to_html", lambda *args, **kwargs: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys,
|
||||||
|
"argv",
|
||||||
|
[
|
||||||
|
"graphify",
|
||||||
|
"label",
|
||||||
|
str(tmp_path),
|
||||||
|
"--backend",
|
||||||
|
"gemini",
|
||||||
|
"--model",
|
||||||
|
"gemini-3.1-flash-lite",
|
||||||
|
"--no-viz",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cli.main()
|
||||||
|
|
||||||
|
assert captured == {"backend": "gemini", "model": "gemini-3.1-flash-lite"}
|
||||||
|
|
||||||
|
|
||||||
def test_label_communities_partial_reply_fills_placeholder(monkeypatch):
|
def test_label_communities_partial_reply_fills_placeholder(monkeypatch):
|
||||||
G, communities = _graph()
|
G, communities = _graph()
|
||||||
monkeypatch.setattr("graphify.llm._call_llm",
|
monkeypatch.setattr("graphify.llm._call_llm",
|
||||||
|
|||||||
Reference in New Issue
Block a user