pttai 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.
- pttai-0.1.0/LICENSE +21 -0
- pttai-0.1.0/PKG-INFO +368 -0
- pttai-0.1.0/README.md +333 -0
- pttai-0.1.0/pttai/__init__.py +37 -0
- pttai-0.1.0/pttai/graph.py +917 -0
- pttai-0.1.0/pttai/node.py +206 -0
- pttai-0.1.0/pttai/nodes/__init__.py +5 -0
- pttai-0.1.0/pttai/nodes/_fields.py +71 -0
- pttai-0.1.0/pttai/nodes/agent_node.py +173 -0
- pttai-0.1.0/pttai/nodes/condition_node.py +53 -0
- pttai-0.1.0/pttai/nodes/decision_node.py +217 -0
- pttai-0.1.0/pttai/nodes/human_node.py +62 -0
- pttai-0.1.0/pttai/nodes/llm_node.py +151 -0
- pttai-0.1.0/pttai/state.py +73 -0
- pttai-0.1.0/pttai/tools/__init__.py +1 -0
- pttai-0.1.0/pttai/tools/rag_tool.py +64 -0
- pttai-0.1.0/pttai/validation.py +392 -0
- pttai-0.1.0/pttai.egg-info/PKG-INFO +368 -0
- pttai-0.1.0/pttai.egg-info/SOURCES.txt +46 -0
- pttai-0.1.0/pttai.egg-info/dependency_links.txt +1 -0
- pttai-0.1.0/pttai.egg-info/requires.txt +16 -0
- pttai-0.1.0/pttai.egg-info/top_level.txt +1 -0
- pttai-0.1.0/pyproject.toml +47 -0
- pttai-0.1.0/setup.cfg +4 -0
- pttai-0.1.0/tests/test_agent_loop.py +57 -0
- pttai-0.1.0/tests/test_autoname.py +92 -0
- pttai-0.1.0/tests/test_condition_node.py +143 -0
- pttai-0.1.0/tests/test_configurable_fields.py +42 -0
- pttai-0.1.0/tests/test_graph_build.py +79 -0
- pttai-0.1.0/tests/test_human_node.py +67 -0
- pttai-0.1.0/tests/test_interrupt_resume.py +30 -0
- pttai-0.1.0/tests/test_invoke_e2e.py +38 -0
- pttai-0.1.0/tests/test_invoke_shorthand.py +152 -0
- pttai-0.1.0/tests/test_multikey_io.py +145 -0
- pttai-0.1.0/tests/test_node_runtime_fixes.py +153 -0
- pttai-0.1.0/tests/test_parallel.py +318 -0
- pttai-0.1.0/tests/test_placeholders.py +71 -0
- pttai-0.1.0/tests/test_prompt_cache.py +48 -0
- pttai-0.1.0/tests/test_rag.py +40 -0
- pttai-0.1.0/tests/test_resilience.py +56 -0
- pttai-0.1.0/tests/test_routing.py +72 -0
- pttai-0.1.0/tests/test_schema_free.py +139 -0
- pttai-0.1.0/tests/test_schema_free_mapreduce.py +118 -0
- pttai-0.1.0/tests/test_state.py +34 -0
- pttai-0.1.0/tests/test_stream_async.py +40 -0
- pttai-0.1.0/tests/test_token.py +102 -0
- pttai-0.1.0/tests/test_validation.py +285 -0
- pttai-0.1.0/tests/test_validator_fixes.py +130 -0
pttai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Teerat Pipitcharulerd
|
|
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.
|
pttai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pttai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Keras for LangGraph — parallel, typed agent graphs in Python, compiled to LangGraph
|
|
5
|
+
Author-email: Teerat Pipitcharulerd <teerat.pipitcharulerd@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/TeeratP/pttai
|
|
8
|
+
Project-URL: Repository, https://github.com/TeeratP/pttai
|
|
9
|
+
Project-URL: Documentation, https://teeratp.github.io/pttai/
|
|
10
|
+
Keywords: langgraph,agent,llm,ai,workflow,dataflow,agents,langchain,graph
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: langgraph<2,>=1.0
|
|
23
|
+
Requires-Dist: langchain-core<2,>=1.0
|
|
24
|
+
Requires-Dist: pydantic<3,>=2
|
|
25
|
+
Provides-Extra: openai
|
|
26
|
+
Requires-Dist: langchain-openai<2,>=1; extra == "openai"
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0; extra == "openai"
|
|
28
|
+
Provides-Extra: rag
|
|
29
|
+
Requires-Dist: langchain-chroma>=0.2; extra == "rag"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# pttai
|
|
37
|
+
|
|
38
|
+
**A declarative DSL over LangGraph whose typed node-IO enables a build-time
|
|
39
|
+
dataflow lint.** Every node is a self-contained tool-using agent, composed with a
|
|
40
|
+
`>` operator into a *visible* DAG you can fan out, map-reduce, and — because each
|
|
41
|
+
node declares the state keys it reads and writes — statically check for dataflow
|
|
42
|
+
bugs *before* you invoke. It all compiles down to a native LangGraph
|
|
43
|
+
`StateGraph`, so you keep the whole ecosystem (streaming, async, checkpointers,
|
|
44
|
+
LangSmith) with zero lock-in.
|
|
45
|
+
|
|
46
|
+
If you know LangGraph, think of pttai as **Keras for LangGraph**: an ergonomic
|
|
47
|
+
default layer over the same runtime. The value is everything you *don't* write —
|
|
48
|
+
`add_node`/`add_edge`/`add_conditional_edges`, `Send` fan-out plumbing,
|
|
49
|
+
structured-output routing, the tool-call loop — plus a build-time validator that
|
|
50
|
+
catches read-before-written dataflow bugs before you ever invoke.
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+

|
|
54
|
+

|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
<p align="center">
|
|
58
|
+
<img src="figures/architecture.png" width="100%"
|
|
59
|
+
alt="pttai architecture: the > DSL builds a linked node structure in memory; AgenticGraph's _build_graph walks it into add_node/add_edge/Send calls; a build-time dataflow validator gates the build (GraphValidationError on failure, before any model call); on success it compiles to a native LangGraph StateGraph run via invoke/stream/async.">
|
|
60
|
+
</p>
|
|
61
|
+
|
|
62
|
+
<p align="center"><em>The <code>></code> DSL → <code>_build_graph</code> → <strong>build-time validator gate</strong> → native LangGraph <code>StateGraph</code>. (<a href="figures/architecture.svg">SVG</a> · <a href="figures/architecture.mmd">source</a>)</em></p>
|
|
63
|
+
|
|
64
|
+
## pttai vs. raw LangGraph
|
|
65
|
+
|
|
66
|
+
The same tool-using agent — an LLM that calls `add` / `multiply` in a loop until
|
|
67
|
+
it has the answer. Ask it *"What is 21 + 21, then times 3?"* and both print
|
|
68
|
+
**126**. The only thing that differs is how much graph plumbing you write:
|
|
69
|
+
**3 lines vs. 10.**
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# pttai
|
|
73
|
+
from pttai import AgentNode, AgenticGraph
|
|
74
|
+
|
|
75
|
+
agent = AgentNode(name="agent", llm=llm, tools=[add, multiply])
|
|
76
|
+
graph = AgenticGraph(start_node=agent, end_nodes={agent}) # schema-free
|
|
77
|
+
|
|
78
|
+
graph.invoke(message="What is 21 + 21, then times 3?") # -> 126
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# raw LangGraph
|
|
83
|
+
from langgraph.graph import StateGraph, MessagesState, START, END
|
|
84
|
+
from langgraph.prebuilt import ToolNode, tools_condition
|
|
85
|
+
|
|
86
|
+
llm_with_tools = llm.bind_tools([add, multiply])
|
|
87
|
+
|
|
88
|
+
def call_model(state: MessagesState):
|
|
89
|
+
return {"messages": [llm_with_tools.invoke(state["messages"])]}
|
|
90
|
+
|
|
91
|
+
builder = StateGraph(MessagesState)
|
|
92
|
+
builder.add_node("call_model", call_model)
|
|
93
|
+
builder.add_node("tools", ToolNode([add, multiply]))
|
|
94
|
+
builder.add_edge(START, "call_model")
|
|
95
|
+
builder.add_conditional_edges("call_model", tools_condition) # tools? -> "tools" : END
|
|
96
|
+
builder.add_edge("tools", "call_model") # loop back to the model
|
|
97
|
+
graph = builder.compile()
|
|
98
|
+
|
|
99
|
+
graph.invoke({"messages": [{"role": "user", "content": "What is 21 + 21, then times 3?"}]}) # -> 126
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Identical behavior — same tools, same loop, same answer. pttai folds the model
|
|
103
|
+
node, the `ToolNode`, the `tools_condition` edge and the loop-back edge into
|
|
104
|
+
**one `AgentNode`** with a built-in tool-call loop, and infers the state schema
|
|
105
|
+
for you. Both versions run side by side in
|
|
106
|
+
[`examples/vs_langgraph.py`](examples/vs_langgraph.py).
|
|
107
|
+
|
|
108
|
+
## The validator: bugs caught before you ever invoke
|
|
109
|
+
|
|
110
|
+
The line count is the *secondary* win. The one you can't get from raw LangGraph
|
|
111
|
+
or `create_react_agent` is a **build-time dataflow analysis** that *fails the
|
|
112
|
+
build* on read-before-write (including the cyclic, loop-carried case), dangling
|
|
113
|
+
branches, and concurrent unreduced writes — before a single model call. Raw
|
|
114
|
+
LangGraph compiles the same bugs and only trips at runtime, on the first input
|
|
115
|
+
that exercises the broken path.
|
|
116
|
+
|
|
117
|
+
<p align="center">
|
|
118
|
+
<img src="figures/validator_before_after.png" width="100%"
|
|
119
|
+
alt="Two panels on the same buggy RAG pipeline. Left (pttai): the AgenticGraph constructor raises GraphValidationError at build time with 0 model calls. Right (raw LangGraph): compile() succeeds with no dataflow check, then a runtime KeyError on the first invoke; across 12 such bugs all 12 fail at runtime and 8 (simulated) LLM calls are burned.">
|
|
120
|
+
</p>
|
|
121
|
+
|
|
122
|
+
Measured on a 36-pipeline benchmark ([`eval/bugbench/`](eval/bugbench/)): pttai
|
|
123
|
+
catches **12/12** pttai-only dataflow bugs at build with **0 false positives** on
|
|
124
|
+
19 valid pipelines, while raw LangGraph catches **0** of those at build (all 12
|
|
125
|
+
surface at runtime) and burns **8 model calls** — a *simulated, worst-case-ordered*
|
|
126
|
+
figure from an offline fake LLM, not measured real-model cost. And the DSL is
|
|
127
|
+
~**60% less code** — 113 vs 281 LOC across 12 pipelines. (Duplicate node names
|
|
128
|
+
are caught by *both* frameworks at build; the dead-end class is legal LangGraph
|
|
129
|
+
behavior, not a defect — both are excluded above.)
|
|
130
|
+
|
|
131
|
+
<p align="center">
|
|
132
|
+
<img src="figures/bug_catch.png" width="49%"
|
|
133
|
+
alt="Bar chart: pttai catches 12/12 pttai-only dataflow bugs at build; raw LangGraph catches 0 at build (all 12 surface at runtime) and burns 8 simulated LLM calls; pttai has 0 false positives on 19 valid pipelines.">
|
|
134
|
+
<img src="figures/loc_comparison.png" width="49%"
|
|
135
|
+
alt="Grouped bar chart of lines of code per pipeline: pttai 113 LOC vs raw LangGraph 281 LOC across 12 pipelines, about 60% fewer lines.">
|
|
136
|
+
</p>
|
|
137
|
+
|
|
138
|
+
Full methodology and per-class results are in
|
|
139
|
+
[`docs/COMPARISON.md`](docs/COMPARISON.md); regenerate the charts from the
|
|
140
|
+
committed CSV/JSON with `python figures/make_charts.py`.
|
|
141
|
+
|
|
142
|
+
## Examples
|
|
143
|
+
|
|
144
|
+
Two runnable galleries make the "pttai vs. LangGraph" story concrete:
|
|
145
|
+
|
|
146
|
+
- **[`examples/basics/`](examples/basics/)** — one file per feature, each showing
|
|
147
|
+
the pttai version *and* the equivalent raw-LangGraph version side by side. The
|
|
148
|
+
fastest way to see exactly what plumbing pttai folds away — tool loops,
|
|
149
|
+
fan-out/join, map-reduce, structured-output routing, typed state IO,
|
|
150
|
+
human-in-the-loop — one concept at a time.
|
|
151
|
+
- **[`examples/architectures/`](examples/architectures/)** — famous agent
|
|
152
|
+
patterns (router, evaluator-optimizer, orchestrator-workers, reflection, and
|
|
153
|
+
more) built end-to-end in pttai, so you can lift a whole topology instead of a
|
|
154
|
+
single node.
|
|
155
|
+
|
|
156
|
+
Start with `basics/` to learn the primitives, then reach for `architectures/`
|
|
157
|
+
when you're wiring a real system.
|
|
158
|
+
|
|
159
|
+
## Interactive playground
|
|
160
|
+
|
|
161
|
+
`python demo/app.py` launches a local Gradio playground: paste a `>`-DSL snippet,
|
|
162
|
+
click **Build + Validate**, and see the compiled LangGraph diagram and the
|
|
163
|
+
build-time validator output side by side — no API key needed. See [`demo/`](demo/).
|
|
164
|
+
|
|
165
|
+
<p align="center">
|
|
166
|
+
<img src="figures/demo_screenshot.png" width="100%"
|
|
167
|
+
alt="pttai playground: (a) a working RAG QA pipeline (retrieve > rerank > answer) compiles clean with a rendered LangGraph diagram and a green validator; (b) a broken pipeline (rerank wired before retrieve) fails the build — the offending 'rerank' node is painted red and the read-before-write error is shown, before the graph is ever invoked.">
|
|
168
|
+
</p>
|
|
169
|
+
|
|
170
|
+
<p align="center"><em>The playground: (a) a working pipeline compiles clean; (b) a broken one fails the build with the offending node painted <strong>red</strong> and the error attached.</em></p>
|
|
171
|
+
|
|
172
|
+
## Install
|
|
173
|
+
|
|
174
|
+
Not on PyPI yet — install from source:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git clone https://github.com/TeeratP/agentic-framework && cd agentic-framework
|
|
178
|
+
python -m venv .venv && source .venv/bin/activate
|
|
179
|
+
pip install -e ".[openai]" # core + langchain-openai & python-dotenv
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Requires **Python ≥ 3.10** (core deps: LangGraph ≥ 1.0, langchain-core ≥ 1.0,
|
|
183
|
+
Pydantic 2). Other extras: `[rag]` (langchain-chroma for `ChromaRAG`), `[dev]`
|
|
184
|
+
(pytest). For live model calls, set `OPENAI_API_KEY` in your environment or a
|
|
185
|
+
`.env` file.
|
|
186
|
+
|
|
187
|
+
## 30-second example: a multi-agent panel
|
|
188
|
+
|
|
189
|
+
A question goes to `frame` (which sharpens it into one concrete decision), fans
|
|
190
|
+
out to three rival personas — optimist / skeptic / pragmatist — who argue
|
|
191
|
+
**concurrently**, then `verdict` weighs every argument into a one-paragraph
|
|
192
|
+
ruling. The whole thing is the one wiring line at the bottom.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from pttai import AgentNode, AgenticGraph, fanout
|
|
196
|
+
from langchain_openai import ChatOpenAI
|
|
197
|
+
|
|
198
|
+
llm = ChatOpenAI(model="gpt-5.4-nano")
|
|
199
|
+
|
|
200
|
+
frame = AgentNode(name="frame", llm=llm, node_prompt=(
|
|
201
|
+
"Restate the user's question as ONE sharp, concrete decision. One sentence."))
|
|
202
|
+
optimist = AgentNode(name="optimist", llm=llm, node_prompt=(
|
|
203
|
+
"Relentless optimist. Argue FOR the bold move — two strongest upsides."))
|
|
204
|
+
skeptic = AgentNode(name="skeptic", llm=llm, node_prompt=(
|
|
205
|
+
"Hard-nosed skeptic. Argue AGAINST — the two biggest risks."))
|
|
206
|
+
pragmatist = AgentNode(name="pragmatist", llm=llm, node_prompt=(
|
|
207
|
+
"Pragmatist. Propose the smallest concrete next step that de-risks it."))
|
|
208
|
+
verdict = AgentNode(name="verdict", llm=llm, node_prompt=(
|
|
209
|
+
"You are the chair. Weigh all three above into a balanced one-paragraph verdict."))
|
|
210
|
+
|
|
211
|
+
# The line that matters: the three personas run IN PARALLEL, then join at `verdict`.
|
|
212
|
+
frame > fanout(optimist, skeptic, pragmatist) > verdict
|
|
213
|
+
|
|
214
|
+
panel = AgenticGraph(start_node=frame, end_nodes={verdict}) # schema-free
|
|
215
|
+
|
|
216
|
+
out = panel.invoke(message="Should an early-stage SaaS rewrite its monolith into microservices?")
|
|
217
|
+
print(out["messages"][-1].content) # the verdict
|
|
218
|
+
panel.summary() # the topology table (below)
|
|
219
|
+
print(out["token"]) # per-model token totals
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Runs from a single paste with just `OPENAI_API_KEY` set. Full version:
|
|
223
|
+
[`examples/panel.py`](examples/panel.py).
|
|
224
|
+
|
|
225
|
+
## What you get
|
|
226
|
+
|
|
227
|
+
- **`>` wiring** — `a > b > c` builds the graph; branches index by choice. No
|
|
228
|
+
`add_node`/`add_edge` boilerplate.
|
|
229
|
+
- **Parallel `fanout(...)` + deferred join** — `start > fanout(a, b) > combine`
|
|
230
|
+
runs `a` and `b` concurrently and joins **once** after both finish (the
|
|
231
|
+
bracket form `start > [a, b] > combine` wires identically).
|
|
232
|
+
- **`worker.map("field")` map-reduce** — `dispatch > summarize.map("docs") > reduce`
|
|
233
|
+
fans a worker out over a state list via LangGraph `Send`, once per item, in
|
|
234
|
+
parallel, then joins once.
|
|
235
|
+
- **Schema-free typed state** — nodes default to `messages`; `reads=[...]` /
|
|
236
|
+
`writes=[...]` give one node multi-key IO dispatched **by value type** (a
|
|
237
|
+
message-list read is history, a scalar read is interpolated into the prompt).
|
|
238
|
+
`writes={"score": int}` returns native-typed structured output (a real `int`,
|
|
239
|
+
not a string).
|
|
240
|
+
- **`message=` invoke shorthand** — `graph.invoke(message="...")` wraps a string
|
|
241
|
+
(or list of messages) onto `messages`; the full `invoke({...})` state form
|
|
242
|
+
still works.
|
|
243
|
+
- **Per-model token usage** — `out["token"]` is a `{model: {total/input/output_tokens}}`
|
|
244
|
+
breakdown accumulated across every node.
|
|
245
|
+
- **Opt-in OpenAI prompt caching** — `AgenticGraph(..., prompt_cache=True)`
|
|
246
|
+
threads one cache key through every OpenAI `AgentNode` call.
|
|
247
|
+
- **Compile-time validation + `summary()`** — the constructor runs a forward
|
|
248
|
+
dataflow analysis and **fails the build** if a node reads a key nothing
|
|
249
|
+
produces upstream (with the offending key and real writer named), and
|
|
250
|
+
`summary()` prints a Keras-`model.summary()`-style topology table:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
AgenticGraph 'graph' state=AgenticState
|
|
254
|
+
initial: decision, log, messages, token
|
|
255
|
+
--------------------------------------------------------------------------
|
|
256
|
+
node type reads writes available
|
|
257
|
+
frame AgentNode messages log,messages decision,log,messages,token
|
|
258
|
+
optimist AgentNode messages log,messages decision,log,messages,token
|
|
259
|
+
verdict AgentNode messages log,messages decision,log,messages,token
|
|
260
|
+
skeptic AgentNode messages log,messages decision,log,messages,token
|
|
261
|
+
pragmatist AgentNode messages log,messages decision,log,messages,token
|
|
262
|
+
--------------------------------------------------------------------------
|
|
263
|
+
5 nodes · 0 errors · 0 warning(s)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The offline, no-API-key tour of parallelism, map-reduce, typed IO, and
|
|
267
|
+
validation lives in [`examples/parallel_usage.py`](examples/parallel_usage.py).
|
|
268
|
+
|
|
269
|
+
## vs. LangChain's Functional API
|
|
270
|
+
|
|
271
|
+
The closest comparison isn't raw graphs — it's LangChain's own Functional API
|
|
272
|
+
(`@entrypoint` / `@task`), which also lets you skip explicit graph wiring. The
|
|
273
|
+
difference is **visibility of control flow**:
|
|
274
|
+
|
|
275
|
+
| | Functional API (`@entrypoint`/`@task`) | pttai |
|
|
276
|
+
|---|---|---|
|
|
277
|
+
| Control flow | hidden in plain Python (loops, `if`, `await`) | an explicit, declarative DAG |
|
|
278
|
+
| Fan-out / join | you orchestrate futures by hand | `fanout(...)` / `.map("field")`, one line |
|
|
279
|
+
| Inspect the topology | run it and trace | `summary()` prints the static DAG |
|
|
280
|
+
| Catch dataflow bugs | at runtime | at **compile time**, before any invoke |
|
|
281
|
+
|
|
282
|
+
Both are concise. pttai's edge is that the topology is *inspectable and
|
|
283
|
+
validatable* — you can see the fan-out/join/map-reduce structure, render it, and
|
|
284
|
+
have the compiler reject read-before-written bugs — whereas the Functional API
|
|
285
|
+
hides the graph inside ordinary Python, so you lose the auditable DAG.
|
|
286
|
+
|
|
287
|
+
## How it works
|
|
288
|
+
|
|
289
|
+
`a > b` doesn't build an edge — it just sets `a.children = [b]` and returns `b`,
|
|
290
|
+
so `a > b > c` builds a linked structure in memory. `AgenticGraph(...)` walks
|
|
291
|
+
that structure **once** at construction, emits the real LangGraph
|
|
292
|
+
`add_node`/`add_edge`/`Send` calls, runs the dataflow validator, and `compile()`s
|
|
293
|
+
to a native `StateGraph` — which `AgenticGraph` subclasses. So pttai is a
|
|
294
|
+
build-time convenience that disappears at runtime: the execution underneath is
|
|
295
|
+
plain LangGraph (streaming, async, durability, checkpointers, LangSmith all
|
|
296
|
+
included), and you can drop down to it anytime. No lock-in.
|
|
297
|
+
|
|
298
|
+
## Node types
|
|
299
|
+
|
|
300
|
+
All nodes are callables (`__call__(state) -> delta`) invoked by LangGraph with
|
|
301
|
+
the shared state. They return **only the keys they update**; reducers merge them.
|
|
302
|
+
|
|
303
|
+
The two LLM-backed nodes (`AgentNode`, `DecisionNode`) share a common `LLMNode`
|
|
304
|
+
base that owns the model binding and the tool-call loop.
|
|
305
|
+
|
|
306
|
+
| Node | Purpose |
|
|
307
|
+
|------|---------|
|
|
308
|
+
| **`AgentNode`** | Prepends `node_prompt` as a `SystemMessage`, calls the LLM, returns a delta. Pass `tools=[...]` in the constructor to wrap bare callables as `StructuredTool`s and run an internal tool-call loop (capped by `max_tool_iterations`, default 25). `reads`/`writes` give typed multi-key IO. Optional `reasoning_effort` (`"low"`/`"medium"`/`"high"`) for gpt-5.x. |
|
|
309
|
+
| **`DecisionNode`** | LLM branching. Reads from `input_field`, writes its choice to `decision`, routes via conditional edges over a `Literal[*choices]` structured output — the model can only return a valid branch. Also accepts `tools=[...]`: it runs a tool-gathering loop first, *then* routes via structured output (the two never share a call). Wired by indexing a choice (`decision["x"] > handler`); `decision > x` is an error. |
|
|
310
|
+
| **`ConditionNode`** | Deterministic branching with **no LLM**. A Python predicate `condition(state) -> str` returns one of `choices`; routing is free, deterministic, and prompt-less. Wired like `DecisionNode` (`cond["x"] > handler`). |
|
|
311
|
+
| **`HumanNode`** | Resumable human-in-the-loop via LangGraph's `interrupt()`. Surfaces a message (or a custom `show`) for review; the human's reply lands `into` `messages` (or any key a router can gate on). Resumes when the graph is built with a `checkpointer` and invoked with a `thread_id`, via `Command(resume=value)`. |
|
|
312
|
+
|
|
313
|
+
All node types also accept `cache_ttl` (LangGraph `CachePolicy`) and `retry`
|
|
314
|
+
(`RetryPolicy`); `AgenticGraph` auto-provides an `InMemoryCache` when any node
|
|
315
|
+
sets `cache_ttl`. An `AgenticGraph` can itself be embedded as a node in a larger
|
|
316
|
+
graph (`graph_0 > graph_1`), and RAG helpers (`make_retriever_tool`, optional
|
|
317
|
+
`ChromaRAG`) wrap any LangChain retriever as a tool you hand to
|
|
318
|
+
`AgentNode(tools=[...])`.
|
|
319
|
+
|
|
320
|
+
## State
|
|
321
|
+
|
|
322
|
+
`AgenticState` is a `TypedDict` of reduced channels; custom schemas just add more:
|
|
323
|
+
|
|
324
|
+
- **`messages`** — `add_messages` reducer: appends, replaces by matching `id`,
|
|
325
|
+
coerces bare strings to `HumanMessage`, and merges parallel branches.
|
|
326
|
+
- **`log`** — `operator.add`: every node appends a trace line. Seed it with `[]`
|
|
327
|
+
on invoke to capture the trace.
|
|
328
|
+
- **`decision`** — transient routing key written by `DecisionNode`, read by its
|
|
329
|
+
`route()`.
|
|
330
|
+
- **`token`** — per-model usage totals accumulated across nodes.
|
|
331
|
+
|
|
332
|
+
Nodes return deltas and never mutate state in place — that's what keeps
|
|
333
|
+
checkpointing, parallel-branch merges, and subgraph composition correct rather
|
|
334
|
+
than racy.
|
|
335
|
+
|
|
336
|
+
## Limitations
|
|
337
|
+
|
|
338
|
+
Kept honest on purpose:
|
|
339
|
+
|
|
340
|
+
- **Structured multi-write list fields are `str`-only.** `writes=["a", "b"]`
|
|
341
|
+
produces one `str` field per key; use the dict form `writes={"a": int}` for
|
|
342
|
+
native-typed structured output.
|
|
343
|
+
- **Map workers don't echo their source item** and must output `messages` (the
|
|
344
|
+
default) — a non-message write would race N parallel workers on a plain key.
|
|
345
|
+
- **`[b, c] > [d, e]` isn't supported.** Two fan-outs chained directly is
|
|
346
|
+
Python's element-wise list compare, not join wiring — insert a node between.
|
|
347
|
+
- **Async is graph-level only.** `ainvoke`/`astream` run the sync nodes in
|
|
348
|
+
LangGraph's threadpool; true per-node async LLM calls aren't implemented.
|
|
349
|
+
- **`reasoning_effort` is `AgentNode`-only** — it conflicts with `DecisionNode`'s
|
|
350
|
+
structured output on current OpenAI models.
|
|
351
|
+
|
|
352
|
+
## Running it
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
python -m pytest tests/ # full suite, no API calls (a scripted FakeLLM stands in)
|
|
356
|
+
python examples/parallel_usage.py # offline tour: parallel + map-reduce + validation
|
|
357
|
+
python examples/panel.py # live multi-agent panel (needs OPENAI_API_KEY)
|
|
358
|
+
python examples/vs_langgraph.py # the 3-vs-10 comparison, both ways (needs OPENAI_API_KEY)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The **164-test** suite covers state reducers, graph construction, routing, the
|
|
362
|
+
tool-call loop, interrupt/resume, RAG tool wiring, streaming/async, configurable
|
|
363
|
+
fields, parallel fan-out/join, map-reduce, multi-key IO, static validation, and
|
|
364
|
+
node caching/retry/`reasoning_effort`/`durability`.
|
|
365
|
+
|
|
366
|
+
## License
|
|
367
|
+
|
|
368
|
+
MIT — see [LICENSE](LICENSE).
|