graphviagent 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 antsticky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include examples *.py
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: graphviagent
3
+ Version: 0.1.0
4
+ Summary: Local Graph-View-Agent for inspecting and replaying LangGraph pipelines
5
+ Author: antsticky
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/antsticky/graphviagent
8
+ Project-URL: Repository, https://github.com/antsticky/graphviagent
9
+ Project-URL: Issues, https://github.com/antsticky/graphviagent/issues
10
+ Keywords: langgraph,langchain,observability,debugger,tracing
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Debuggers
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: langgraph>=0.2
24
+ Provides-Extra: dev
25
+ Requires-Dist: build; extra == "dev"
26
+ Requires-Dist: twine; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # GraphVIAgent
30
+
31
+ Local Graph-View-Agent for LangGraph. Scan `*_pipeline.py` files, record runs under `.graphviagent/`, and inspect them in a browser UI.
32
+
33
+ Pipeline files do not import GraphVIAgent. Only runs started from the UI or CLI are stored.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install graphviagent
39
+ ```
40
+
41
+ From a clone:
42
+
43
+ ```bash
44
+ python3 -m venv .venv
45
+ source .venv/bin/activate
46
+ pip install -e .
47
+ ```
48
+
49
+ ## List pipelines
50
+
51
+ ```bash
52
+ graphviagent .
53
+ ```
54
+
55
+ ## Open the UI
56
+
57
+ ```bash
58
+ graphviagent serve .
59
+ ```
60
+
61
+ Then open [http://127.0.0.1:8765](http://127.0.0.1:8765).
62
+
63
+ - **Trace** — run a pipeline, inspect the unrolled graph, replay a node
64
+ - **Pipelines** — Airflow-style grid of recent runs
65
+
66
+ Replay and Call node invoke the node again. Side effects will fire.
67
+
68
+ ## Pipeline contract
69
+
70
+ A file is viewable if it is named `*_pipeline.py` and exposes one of:
71
+
72
+ - `build_graph()` / `get_graph()` / `create_graph()`
73
+ - compiled `GRAPH` or `app`
74
+ - `__graph__ = "factory_name"`
75
+
76
+ Optional: `EXAMPLES = [{"some": "input"}]` to prefill the form.
77
+
78
+ If a node update includes `decisions`, `reason`, or `choice`, the UI labels the branch. That is optional.
79
+
80
+ ## Examples
81
+
82
+ This repo includes sample graphs:
83
+
84
+ ```bash
85
+ graphviagent serve examples
86
+ ```
87
+
88
+ - `examples/dummy_pipeline.py` — name-length branch and a polish loop
89
+ - `examples/echo_pipeline.py` — reverse a `text` field
90
+ - `examples/math_pipeline.py` — add or divide; `{a: 12, b: 0, op: "div"}` raises
91
+ - `examples/ratio_pipeline.py` — part / total; `total: 0` raises
92
+ - `examples/stats_pipeline.py` — mean of a list; empty `values` raises
93
+ - `examples/grade_pipeline.py` — score / max; `maximum: 0` raises
94
+ - `examples/convert_pipeline.py` — unit conversion; `per_unit` with `value: 0` or an unknown unit raises
95
+
96
+ Failed runs are saved. The failing node is marked on the graph and shows as a red cell on the Pipelines grid.
97
+
98
+ ## Publish to PyPI
99
+
100
+ ```bash
101
+ pip install -e ".[dev]"
102
+ python -m build
103
+ twine check dist/*
104
+ twine upload dist/*
105
+ ```
106
+
107
+ Create the GitHub repo at [antsticky/graphviagent](https://github.com/antsticky/graphviagent) before the first upload.
@@ -0,0 +1,79 @@
1
+ # GraphVIAgent
2
+
3
+ Local Graph-View-Agent for LangGraph. Scan `*_pipeline.py` files, record runs under `.graphviagent/`, and inspect them in a browser UI.
4
+
5
+ Pipeline files do not import GraphVIAgent. Only runs started from the UI or CLI are stored.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install graphviagent
11
+ ```
12
+
13
+ From a clone:
14
+
15
+ ```bash
16
+ python3 -m venv .venv
17
+ source .venv/bin/activate
18
+ pip install -e .
19
+ ```
20
+
21
+ ## List pipelines
22
+
23
+ ```bash
24
+ graphviagent .
25
+ ```
26
+
27
+ ## Open the UI
28
+
29
+ ```bash
30
+ graphviagent serve .
31
+ ```
32
+
33
+ Then open [http://127.0.0.1:8765](http://127.0.0.1:8765).
34
+
35
+ - **Trace** — run a pipeline, inspect the unrolled graph, replay a node
36
+ - **Pipelines** — Airflow-style grid of recent runs
37
+
38
+ Replay and Call node invoke the node again. Side effects will fire.
39
+
40
+ ## Pipeline contract
41
+
42
+ A file is viewable if it is named `*_pipeline.py` and exposes one of:
43
+
44
+ - `build_graph()` / `get_graph()` / `create_graph()`
45
+ - compiled `GRAPH` or `app`
46
+ - `__graph__ = "factory_name"`
47
+
48
+ Optional: `EXAMPLES = [{"some": "input"}]` to prefill the form.
49
+
50
+ If a node update includes `decisions`, `reason`, or `choice`, the UI labels the branch. That is optional.
51
+
52
+ ## Examples
53
+
54
+ This repo includes sample graphs:
55
+
56
+ ```bash
57
+ graphviagent serve examples
58
+ ```
59
+
60
+ - `examples/dummy_pipeline.py` — name-length branch and a polish loop
61
+ - `examples/echo_pipeline.py` — reverse a `text` field
62
+ - `examples/math_pipeline.py` — add or divide; `{a: 12, b: 0, op: "div"}` raises
63
+ - `examples/ratio_pipeline.py` — part / total; `total: 0` raises
64
+ - `examples/stats_pipeline.py` — mean of a list; empty `values` raises
65
+ - `examples/grade_pipeline.py` — score / max; `maximum: 0` raises
66
+ - `examples/convert_pipeline.py` — unit conversion; `per_unit` with `value: 0` or an unknown unit raises
67
+
68
+ Failed runs are saved. The failing node is marked on the graph and shows as a red cell on the Pipelines grid.
69
+
70
+ ## Publish to PyPI
71
+
72
+ ```bash
73
+ pip install -e ".[dev]"
74
+ python -m build
75
+ twine check dist/*
76
+ twine upload dist/*
77
+ ```
78
+
79
+ Create the GitHub repo at [antsticky/graphviagent](https://github.com/antsticky/graphviagent) before the first upload.
@@ -0,0 +1,87 @@
1
+ from typing import Literal, TypedDict
2
+
3
+ from langgraph.graph import END, START, StateGraph
4
+
5
+ EXAMPLES = [
6
+ {"value": 100, "unit": "c_to_f"},
7
+ {"value": 32, "unit": "f_to_c"},
8
+ {"value": 0, "unit": "per_unit"},
9
+ {"value": 21, "unit": "kelvin"},
10
+ ]
11
+
12
+
13
+ class ConvertState(TypedDict):
14
+ value: float
15
+ unit: str
16
+ path: str
17
+ result: float
18
+ label: str
19
+
20
+
21
+ def parse(state: ConvertState) -> dict:
22
+ return {"value": float(state["value"]), "unit": str(state.get("unit") or "")}
23
+
24
+
25
+ def choose_unit(state: ConvertState) -> dict:
26
+ unit = state["unit"]
27
+ known = {"c_to_f": "c_to_f", "f_to_c": "f_to_c", "per_unit": "per_unit"}
28
+ if unit not in known:
29
+ raise ValueError(f"unknown unit {unit!r}; use c_to_f, f_to_c, or per_unit")
30
+ path = known[unit]
31
+ return {
32
+ "path": path,
33
+ "decisions": [
34
+ {
35
+ "step": "choose_unit",
36
+ "choice": path,
37
+ "reason": f"unit={unit!r}",
38
+ }
39
+ ],
40
+ }
41
+
42
+
43
+ def c_to_f(state: ConvertState) -> dict:
44
+ return {"result": state["value"] * 9 / 5 + 32}
45
+
46
+
47
+ def f_to_c(state: ConvertState) -> dict:
48
+ return {"result": (state["value"] - 32) * 5 / 9}
49
+
50
+
51
+ def per_unit(state: ConvertState) -> dict:
52
+ return {"result": 100 / state["value"]}
53
+
54
+
55
+ def label(state: ConvertState) -> dict:
56
+ names = {
57
+ "c_to_f": f"{state['value']}°C = {state['result']}°F",
58
+ "f_to_c": f"{state['value']}°F = {state['result']}°C",
59
+ "per_unit": f"100 / {state['value']} = {state['result']}",
60
+ }
61
+ return {"label": names[state["path"]]}
62
+
63
+
64
+ def route_unit(state: ConvertState) -> Literal["c_to_f", "f_to_c", "per_unit"]:
65
+ return state["path"] # type: ignore[return-value]
66
+
67
+
68
+ def build_graph():
69
+ graph = StateGraph(ConvertState)
70
+ graph.add_node("parse", parse)
71
+ graph.add_node("choose_unit", choose_unit)
72
+ graph.add_node("c_to_f", c_to_f)
73
+ graph.add_node("f_to_c", f_to_c)
74
+ graph.add_node("per_unit", per_unit)
75
+ graph.add_node("label", label)
76
+ graph.add_edge(START, "parse")
77
+ graph.add_edge("parse", "choose_unit")
78
+ graph.add_conditional_edges(
79
+ "choose_unit",
80
+ route_unit,
81
+ {"c_to_f": "c_to_f", "f_to_c": "f_to_c", "per_unit": "per_unit"},
82
+ )
83
+ graph.add_edge("c_to_f", "label")
84
+ graph.add_edge("f_to_c", "label")
85
+ graph.add_edge("per_unit", "label")
86
+ graph.add_edge("label", END)
87
+ return graph.compile()
@@ -0,0 +1,142 @@
1
+ from operator import add
2
+ from typing import Annotated, Literal, TypedDict
3
+
4
+ from langgraph.graph import END, START, StateGraph
5
+
6
+ EXAMPLES = [
7
+ {"name": "Ada"},
8
+ {"name": "Bo"},
9
+ {"name": "Ada Lovelace"},
10
+ {"name": "Grace"},
11
+ ]
12
+
13
+
14
+ # Shared state. Each node returns only the keys it updates; LangGraph merges them in.
15
+ # `decisions` appends so every branch/loop choice stays visible in the final state.
16
+ class PipelineState(TypedDict):
17
+ name: str
18
+ greeting: str
19
+ shout: str
20
+ polished: str
21
+ passes: int
22
+ path: str
23
+ loop_choice: str
24
+ decisions: Annotated[list[dict], add]
25
+
26
+
27
+ def greet(state: PipelineState) -> dict:
28
+ return {
29
+ "greeting": f"Hello, {state['name']}!",
30
+ "passes": 0,
31
+ "shout": "",
32
+ "polished": "",
33
+ }
34
+
35
+
36
+ def choose_path(state: PipelineState) -> dict:
37
+ """Visible decision node: short names shout, longer names skip."""
38
+ name = state["name"]
39
+ length = len(name)
40
+ threshold = 3
41
+ path = "shout" if length <= threshold else "skip"
42
+ if path == "shout":
43
+ reason = f"len({name!r}) = {length} <= {threshold} → shout"
44
+ else:
45
+ reason = f"len({name!r}) = {length} > {threshold} → skip"
46
+ return {
47
+ "path": path,
48
+ "decisions": [
49
+ {
50
+ "step": "choose_path",
51
+ "choice": path,
52
+ "reason": reason,
53
+ "facts": {"name": name, "name_length": length, "threshold": threshold},
54
+ }
55
+ ],
56
+ }
57
+
58
+
59
+ def shout(state: PipelineState) -> dict:
60
+ return {"shout": state["greeting"].upper()}
61
+
62
+
63
+ def skip_shout(state: PipelineState) -> dict:
64
+ return {"shout": state["greeting"]}
65
+
66
+
67
+ def polish(state: PipelineState) -> dict:
68
+ passes = state["passes"] + 1
69
+ return {
70
+ "passes": passes,
71
+ "polished": f"{state['shout']} (pass {passes})",
72
+ }
73
+
74
+
75
+ def check_loop(state: PipelineState) -> dict:
76
+ """Visible decision node: keep polishing until 2 passes."""
77
+ passes = state["passes"]
78
+ required = 2
79
+ if passes < required:
80
+ choice = "polish"
81
+ reason = f"passes = {passes} < {required} → polish again"
82
+ else:
83
+ choice = "done"
84
+ reason = f"passes = {passes} >= {required} → stop"
85
+ return {
86
+ "loop_choice": choice,
87
+ "decisions": [
88
+ {
89
+ "step": "check_loop",
90
+ "choice": choice,
91
+ "reason": reason,
92
+ "facts": {"passes": passes, "required": required},
93
+ }
94
+ ],
95
+ }
96
+
97
+
98
+ def route_after_choice(state: PipelineState) -> Literal["shout", "skip"]:
99
+ return state["path"] # type: ignore[return-value]
100
+
101
+
102
+ def route_after_loop(state: PipelineState) -> Literal["polish", "done"]:
103
+ return state["loop_choice"] # type: ignore[return-value]
104
+
105
+
106
+ def build_graph():
107
+ graph = StateGraph(PipelineState)
108
+
109
+ graph.add_node("greet", greet)
110
+ graph.add_node("choose_path", choose_path)
111
+ graph.add_node("shout", shout)
112
+ graph.add_node("skip", skip_shout)
113
+ graph.add_node("polish", polish)
114
+ graph.add_node("check_loop", check_loop)
115
+
116
+ graph.add_edge(START, "greet")
117
+ graph.add_edge("greet", "choose_path")
118
+
119
+ graph.add_conditional_edges(
120
+ "choose_path",
121
+ route_after_choice,
122
+ {"shout": "shout", "skip": "skip"},
123
+ )
124
+
125
+ graph.add_edge("shout", "polish")
126
+ graph.add_edge("skip", "polish")
127
+ graph.add_edge("polish", "check_loop")
128
+
129
+ graph.add_conditional_edges(
130
+ "check_loop",
131
+ route_after_loop,
132
+ {"polish": "polish", "done": END},
133
+ )
134
+
135
+ return graph.compile()
136
+
137
+
138
+ if __name__ == "__main__":
139
+ app = build_graph()
140
+ for example in EXAMPLES[:2]:
141
+ print(example, "->", app.invoke(example))
142
+ print("\nInspect with: graphviagent serve examples")
@@ -0,0 +1,36 @@
1
+ """Minimal second pipeline so discovery is not dummy-only."""
2
+
3
+ from typing import TypedDict
4
+
5
+ from langgraph.graph import END, START, StateGraph
6
+
7
+ EXAMPLES = [{"text": "hello"}, {"text": "GraphVIAgent"}]
8
+
9
+
10
+ class EchoState(TypedDict):
11
+ text: str
12
+ echoed: str
13
+ length: int
14
+
15
+
16
+ def echo(state: EchoState) -> dict:
17
+ text = state["text"]
18
+ return {
19
+ "echoed": text[::-1],
20
+ "length": len(text),
21
+ "decisions": [
22
+ {
23
+ "step": "echo",
24
+ "choice": "reverse",
25
+ "reason": f"reversed {len(text)} chars",
26
+ }
27
+ ],
28
+ }
29
+
30
+
31
+ def build_graph():
32
+ graph = StateGraph(EchoState)
33
+ graph.add_node("echo", echo)
34
+ graph.add_edge(START, "echo")
35
+ graph.add_edge("echo", END)
36
+ return graph.compile()
@@ -0,0 +1,60 @@
1
+ from typing import TypedDict
2
+
3
+ from langgraph.graph import END, START, StateGraph
4
+
5
+ EXAMPLES = [
6
+ {"score": 18, "maximum": 20},
7
+ {"score": 18, "maximum": 0},
8
+ {"score": 7, "maximum": 10},
9
+ ]
10
+
11
+
12
+ class GradeState(TypedDict):
13
+ score: float
14
+ maximum: float
15
+ percent: float
16
+ letter: str
17
+
18
+
19
+ def normalize(state: GradeState) -> dict:
20
+ return {"score": float(state["score"]), "maximum": float(state["maximum"])}
21
+
22
+
23
+ def score_percent(state: GradeState) -> dict:
24
+ return {"percent": 100 * state["score"] / state["maximum"]}
25
+
26
+
27
+ def letter(state: GradeState) -> dict:
28
+ percent = state["percent"]
29
+ if percent >= 90:
30
+ grade = "A"
31
+ elif percent >= 80:
32
+ grade = "B"
33
+ elif percent >= 70:
34
+ grade = "C"
35
+ elif percent >= 60:
36
+ grade = "D"
37
+ else:
38
+ grade = "F"
39
+ return {
40
+ "letter": grade,
41
+ "decisions": [
42
+ {
43
+ "step": "letter",
44
+ "choice": grade,
45
+ "reason": f"{percent:.1f}% → {grade}",
46
+ }
47
+ ],
48
+ }
49
+
50
+
51
+ def build_graph():
52
+ graph = StateGraph(GradeState)
53
+ graph.add_node("normalize", normalize)
54
+ graph.add_node("percent", score_percent)
55
+ graph.add_node("letter", letter)
56
+ graph.add_edge(START, "normalize")
57
+ graph.add_edge("normalize", "percent")
58
+ graph.add_edge("percent", "letter")
59
+ graph.add_edge("letter", END)
60
+ return graph.compile()
@@ -0,0 +1,78 @@
1
+ from typing import Literal, TypedDict
2
+
3
+ from langgraph.graph import END, START, StateGraph
4
+
5
+ EXAMPLES = [
6
+ {"a": 12, "b": 3, "op": "div"},
7
+ {"a": 12, "b": 0, "op": "div"},
8
+ {"a": 7, "b": 5, "op": "add"},
9
+ ]
10
+
11
+
12
+ class MathState(TypedDict):
13
+ a: float
14
+ b: float
15
+ op: str
16
+ path: str
17
+ result: float
18
+ summary: str
19
+
20
+
21
+ def parse(state: MathState) -> dict:
22
+ a = float(state["a"])
23
+ b = float(state["b"])
24
+ op = str(state.get("op") or "div").lower()
25
+ return {"a": a, "b": b, "op": op}
26
+
27
+
28
+ def choose_op(state: MathState) -> dict:
29
+ op = state["op"]
30
+ path = "add" if op == "add" else "div"
31
+ if path == "add":
32
+ reason = f"op={op!r} → add {state['a']} + {state['b']}"
33
+ else:
34
+ reason = f"op={op!r} → divide {state['a']} / {state['b']}"
35
+ return {
36
+ "path": path,
37
+ "decisions": [
38
+ {
39
+ "step": "choose_op",
40
+ "choice": path,
41
+ "reason": reason,
42
+ "facts": {"a": state["a"], "b": state["b"], "op": op},
43
+ }
44
+ ],
45
+ }
46
+
47
+
48
+ def add(state: MathState) -> dict:
49
+ return {"result": state["a"] + state["b"]}
50
+
51
+
52
+ def divide(state: MathState) -> dict:
53
+ return {"result": state["a"] / state["b"]}
54
+
55
+
56
+ def format_result(state: MathState) -> dict:
57
+ symbol = "+" if state["path"] == "add" else "/"
58
+ return {"summary": f"{state['a']} {symbol} {state['b']} = {state['result']}"}
59
+
60
+
61
+ def route_op(state: MathState) -> Literal["add", "div"]:
62
+ return state["path"] # type: ignore[return-value]
63
+
64
+
65
+ def build_graph():
66
+ graph = StateGraph(MathState)
67
+ graph.add_node("parse", parse)
68
+ graph.add_node("choose_op", choose_op)
69
+ graph.add_node("add", add)
70
+ graph.add_node("div", divide)
71
+ graph.add_node("format", format_result)
72
+ graph.add_edge(START, "parse")
73
+ graph.add_edge("parse", "choose_op")
74
+ graph.add_conditional_edges("choose_op", route_op, {"add": "add", "div": "div"})
75
+ graph.add_edge("add", "format")
76
+ graph.add_edge("div", "format")
77
+ graph.add_edge("format", END)
78
+ return graph.compile()
@@ -0,0 +1,52 @@
1
+ from typing import TypedDict
2
+
3
+ from langgraph.graph import END, START, StateGraph
4
+
5
+ EXAMPLES = [
6
+ {"part": 3, "total": 10},
7
+ {"part": 3, "total": 0},
8
+ {"part": 25, "total": 40},
9
+ ]
10
+
11
+
12
+ class RatioState(TypedDict):
13
+ part: float
14
+ total: float
15
+ ratio: float
16
+ percent: float
17
+ label: str
18
+
19
+
20
+ def read_values(state: RatioState) -> dict:
21
+ return {"part": float(state["part"]), "total": float(state["total"])}
22
+
23
+
24
+ def compute_ratio(state: RatioState) -> dict:
25
+ return {"ratio": state["part"] / state["total"]}
26
+
27
+
28
+ def to_percent(state: RatioState) -> dict:
29
+ percent = state["ratio"] * 100
30
+ return {
31
+ "percent": percent,
32
+ "label": f"{state['part']} of {state['total']} = {percent:.1f}%",
33
+ "decisions": [
34
+ {
35
+ "step": "to_percent",
36
+ "choice": "scale",
37
+ "reason": f"ratio {state['ratio']} × 100",
38
+ }
39
+ ],
40
+ }
41
+
42
+
43
+ def build_graph():
44
+ graph = StateGraph(RatioState)
45
+ graph.add_node("read", read_values)
46
+ graph.add_node("ratio", compute_ratio)
47
+ graph.add_node("percent", to_percent)
48
+ graph.add_edge(START, "read")
49
+ graph.add_edge("read", "ratio")
50
+ graph.add_edge("ratio", "percent")
51
+ graph.add_edge("percent", END)
52
+ return graph.compile()