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.
- graphviagent-0.1.0/LICENSE +21 -0
- graphviagent-0.1.0/MANIFEST.in +3 -0
- graphviagent-0.1.0/PKG-INFO +107 -0
- graphviagent-0.1.0/README.md +79 -0
- graphviagent-0.1.0/examples/convert_pipeline.py +87 -0
- graphviagent-0.1.0/examples/dummy_pipeline.py +142 -0
- graphviagent-0.1.0/examples/echo_pipeline.py +36 -0
- graphviagent-0.1.0/examples/grade_pipeline.py +60 -0
- graphviagent-0.1.0/examples/math_pipeline.py +78 -0
- graphviagent-0.1.0/examples/ratio_pipeline.py +52 -0
- graphviagent-0.1.0/examples/stats_pipeline.py +51 -0
- graphviagent-0.1.0/pyproject.toml +45 -0
- graphviagent-0.1.0/setup.cfg +4 -0
- graphviagent-0.1.0/src/graphviagent/__init__.py +5 -0
- graphviagent-0.1.0/src/graphviagent/cli.py +49 -0
- graphviagent-0.1.0/src/graphviagent/discover.py +26 -0
- graphviagent-0.1.0/src/graphviagent/load.py +88 -0
- graphviagent-0.1.0/src/graphviagent/record.py +372 -0
- graphviagent-0.1.0/src/graphviagent/render.py +76 -0
- graphviagent-0.1.0/src/graphviagent/server.py +1378 -0
- graphviagent-0.1.0/src/graphviagent/store.py +91 -0
- graphviagent-0.1.0/src/graphviagent.egg-info/PKG-INFO +107 -0
- graphviagent-0.1.0/src/graphviagent.egg-info/SOURCES.txt +25 -0
- graphviagent-0.1.0/src/graphviagent.egg-info/dependency_links.txt +1 -0
- graphviagent-0.1.0/src/graphviagent.egg-info/entry_points.txt +2 -0
- graphviagent-0.1.0/src/graphviagent.egg-info/requires.txt +5 -0
- graphviagent-0.1.0/src/graphviagent.egg-info/top_level.txt +1 -0
|
@@ -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,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()
|