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.
Files changed (48) hide show
  1. pttai-0.1.0/LICENSE +21 -0
  2. pttai-0.1.0/PKG-INFO +368 -0
  3. pttai-0.1.0/README.md +333 -0
  4. pttai-0.1.0/pttai/__init__.py +37 -0
  5. pttai-0.1.0/pttai/graph.py +917 -0
  6. pttai-0.1.0/pttai/node.py +206 -0
  7. pttai-0.1.0/pttai/nodes/__init__.py +5 -0
  8. pttai-0.1.0/pttai/nodes/_fields.py +71 -0
  9. pttai-0.1.0/pttai/nodes/agent_node.py +173 -0
  10. pttai-0.1.0/pttai/nodes/condition_node.py +53 -0
  11. pttai-0.1.0/pttai/nodes/decision_node.py +217 -0
  12. pttai-0.1.0/pttai/nodes/human_node.py +62 -0
  13. pttai-0.1.0/pttai/nodes/llm_node.py +151 -0
  14. pttai-0.1.0/pttai/state.py +73 -0
  15. pttai-0.1.0/pttai/tools/__init__.py +1 -0
  16. pttai-0.1.0/pttai/tools/rag_tool.py +64 -0
  17. pttai-0.1.0/pttai/validation.py +392 -0
  18. pttai-0.1.0/pttai.egg-info/PKG-INFO +368 -0
  19. pttai-0.1.0/pttai.egg-info/SOURCES.txt +46 -0
  20. pttai-0.1.0/pttai.egg-info/dependency_links.txt +1 -0
  21. pttai-0.1.0/pttai.egg-info/requires.txt +16 -0
  22. pttai-0.1.0/pttai.egg-info/top_level.txt +1 -0
  23. pttai-0.1.0/pyproject.toml +47 -0
  24. pttai-0.1.0/setup.cfg +4 -0
  25. pttai-0.1.0/tests/test_agent_loop.py +57 -0
  26. pttai-0.1.0/tests/test_autoname.py +92 -0
  27. pttai-0.1.0/tests/test_condition_node.py +143 -0
  28. pttai-0.1.0/tests/test_configurable_fields.py +42 -0
  29. pttai-0.1.0/tests/test_graph_build.py +79 -0
  30. pttai-0.1.0/tests/test_human_node.py +67 -0
  31. pttai-0.1.0/tests/test_interrupt_resume.py +30 -0
  32. pttai-0.1.0/tests/test_invoke_e2e.py +38 -0
  33. pttai-0.1.0/tests/test_invoke_shorthand.py +152 -0
  34. pttai-0.1.0/tests/test_multikey_io.py +145 -0
  35. pttai-0.1.0/tests/test_node_runtime_fixes.py +153 -0
  36. pttai-0.1.0/tests/test_parallel.py +318 -0
  37. pttai-0.1.0/tests/test_placeholders.py +71 -0
  38. pttai-0.1.0/tests/test_prompt_cache.py +48 -0
  39. pttai-0.1.0/tests/test_rag.py +40 -0
  40. pttai-0.1.0/tests/test_resilience.py +56 -0
  41. pttai-0.1.0/tests/test_routing.py +72 -0
  42. pttai-0.1.0/tests/test_schema_free.py +139 -0
  43. pttai-0.1.0/tests/test_schema_free_mapreduce.py +118 -0
  44. pttai-0.1.0/tests/test_state.py +34 -0
  45. pttai-0.1.0/tests/test_stream_async.py +40 -0
  46. pttai-0.1.0/tests/test_token.py +102 -0
  47. pttai-0.1.0/tests/test_validation.py +285 -0
  48. 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
+ ![Python](https://img.shields.io/badge/python-%E2%89%A53.10-blue)
53
+ ![LangGraph](https://img.shields.io/badge/LangGraph-1.0-orange)
54
+ ![tests](https://img.shields.io/badge/tests-164%20passing-green)
55
+ ![License](https://img.shields.io/badge/license-MIT-green)
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>&gt;</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).