penguiflow 2.2.0__tar.gz → 2.2.2__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.
Potentially problematic release.
This version of penguiflow might be problematic. Click here for more details.
- {penguiflow-2.2.0 → penguiflow-2.2.2}/PKG-INFO +42 -68
- {penguiflow-2.2.0 → penguiflow-2.2.2}/README.md +41 -67
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/__init__.py +1 -1
- penguiflow-2.2.2/penguiflow/planner/__init__.py +27 -0
- penguiflow-2.2.2/penguiflow/planner/prompts.py +243 -0
- penguiflow-2.2.2/penguiflow/planner/react.py +1339 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/PKG-INFO +42 -68
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/SOURCES.txt +3 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/pyproject.toml +2 -2
- {penguiflow-2.2.0 → penguiflow-2.2.2}/LICENSE +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/admin.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/bus.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/catalog.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/core.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/debug.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/errors.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/metrics.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/middlewares.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/node.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/patterns.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/policies.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/registry.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/remote.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/state.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/streaming.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/testkit.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/types.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/viz.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/dependency_links.txt +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/entry_points.txt +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/requires.txt +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/top_level.txt +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow_a2a/__init__.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow_a2a/server.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/setup.cfg +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_a2a_server.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_budgets.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_cancel.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_catalog.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_controller.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_core.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_distribution_hooks.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_errors.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_metadata.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_metrics.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_middlewares.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_node.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_patterns.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_planner_prompts.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_property_based.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_react_planner.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_registry.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_remote.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_routing_policy.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_streaming.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_testkit.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_types.py +0 -0
- {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_viz.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: penguiflow
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Async agent orchestration primitives.
|
|
5
5
|
Author: PenguiFlow Team
|
|
6
6
|
License: MIT License
|
|
@@ -53,21 +53,11 @@ Dynamic: license-file
|
|
|
53
53
|
</p>
|
|
54
54
|
|
|
55
55
|
<p align="center">
|
|
56
|
-
<a href="https://github.com/penguiflow/penguiflow/actions/workflows/ci.yml">
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<a href="https://
|
|
60
|
-
|
|
61
|
-
</a>
|
|
62
|
-
<a href="https://nightly.link/penguiflow/penguiflow/workflows/benchmarks/main/benchmarks.json.zip">
|
|
63
|
-
<img src="https://img.shields.io/badge/benchmarks-latest-orange" alt="Benchmarks">
|
|
64
|
-
</a>
|
|
65
|
-
<a href="https://pypi.org/project/penguiflow/">
|
|
66
|
-
<img src="https://img.shields.io/pypi/v/penguiflow.svg" alt="PyPI version">
|
|
67
|
-
</a>
|
|
68
|
-
<a href="https://github.com/penguiflow/penguiflow/blob/main/LICENSE">
|
|
69
|
-
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
|
70
|
-
</a>
|
|
56
|
+
<a href="https://github.com/hurtener/penguiflow/actions/workflows/ci.yml"><img src="https://github.com/hurtener/penguiflow/actions/workflows/ci.yml/badge.svg" alt="CI Status"></a>
|
|
57
|
+
<a href="https://github.com/hurtener/penguiflow"><img src="https://img.shields.io/badge/coverage-85%25-brightgreen" alt="Coverage"></a>
|
|
58
|
+
<a href="https://nightly.link/hurtener/penguiflow/workflows/benchmarks/main/benchmarks.json.zip"><img src="https://img.shields.io/badge/benchmarks-latest-orange" alt="Benchmarks"></a>
|
|
59
|
+
<a href="https://pypi.org/project/penguiflow/"><img src="https://img.shields.io/pypi/v/penguiflow.svg" alt="PyPI version"></a>
|
|
60
|
+
<a href="https://github.com/hurtener/penguiflow/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
71
61
|
</p>
|
|
72
62
|
|
|
73
63
|
**Async-first orchestration library for multi-agent and data pipelines**
|
|
@@ -81,6 +71,7 @@ It provides:
|
|
|
81
71
|
* **Retries, timeouts, backpressure**
|
|
82
72
|
* **Streaming chunks** (LLM-style token emission with `Context.emit_chunk`)
|
|
83
73
|
* **Dynamic loops** (controller nodes)
|
|
74
|
+
* **LLM-driven orchestration** (`ReactPlanner` for autonomous multi-step workflows with tool selection, parallel execution, and pause/resume)
|
|
84
75
|
* **Runtime playbooks** (callable subflows with shared metadata)
|
|
85
76
|
* **Per-trace cancellation** (`PenguiFlow.cancel` with `TraceCancelled` surfacing in nodes)
|
|
86
77
|
* **Deadlines & budgets** (`Message.deadline_s`, `WM.budget_hops`, and `WM.budget_tokens` guardrails that you can leave unset/`None`)
|
|
@@ -369,69 +360,52 @@ The new `penguiflow.testkit` module keeps unit tests tiny:
|
|
|
369
360
|
The harness is covered by `tests/test_testkit.py` and demonstrated in
|
|
370
361
|
`examples/testkit_demo/`.
|
|
371
362
|
|
|
372
|
-
###
|
|
363
|
+
### React Planner - LLM-Driven Orchestration
|
|
373
364
|
|
|
374
|
-
|
|
375
|
-
deterministic:
|
|
365
|
+
Build autonomous agents that select and execute tools dynamically using the ReAct (Reasoning + Acting) pattern:
|
|
376
366
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
* `penguiflow.planner.ReactPlanner` drives a JSON-only ReAct loop over those
|
|
380
|
-
descriptors, validating every LLM action with Pydantic and replaying invalid
|
|
381
|
-
steps to request corrections.
|
|
382
|
-
* LiteLLM stays optional—install `penguiflow[planner]` or inject a custom
|
|
383
|
-
`llm_client` for deterministic/offline runs.
|
|
384
|
-
|
|
385
|
-
See `examples/react_minimal/` for a stubbed end-to-end run.
|
|
386
|
-
|
|
387
|
-
### Trajectory summarisation & pause/resume (Phase B)
|
|
388
|
-
|
|
389
|
-
Phase B adds the tools you need for longer-running, approval-driven flows:
|
|
390
|
-
|
|
391
|
-
* **Token-aware summaries** — `Trajectory.compress()` keeps a compact state and
|
|
392
|
-
the planner can route summaries through a cheaper `summarizer_llm` before
|
|
393
|
-
asking for the next action.
|
|
394
|
-
* **`PlannerPause` contract** — nodes can call `await ctx.pause(...)` to return a
|
|
395
|
-
typed pause payload. Resume the run later with `ReactPlanner.resume(token, user_input=...)`.
|
|
396
|
-
* **Developer hints** — pass `planning_hints={...}` to enforce disallowed tools,
|
|
397
|
-
preferred ordering, or parallelism ceilings.
|
|
367
|
+
```python
|
|
368
|
+
from penguiflow import ReactPlanner, tool, build_catalog
|
|
398
369
|
|
|
399
|
-
|
|
400
|
-
|
|
370
|
+
@tool(desc="Search documentation")
|
|
371
|
+
async def search_docs(args: Query, ctx) -> Documents:
|
|
372
|
+
return Documents(results=await search(args.text))
|
|
401
373
|
|
|
402
|
-
|
|
374
|
+
@tool(desc="Summarize results")
|
|
375
|
+
async def summarize(args: Documents, ctx) -> Summary:
|
|
376
|
+
return Summary(text=await llm_summarize(args.results))
|
|
403
377
|
|
|
404
|
-
|
|
378
|
+
planner = ReactPlanner(
|
|
379
|
+
llm="gpt-4",
|
|
380
|
+
catalog=build_catalog([search_docs, summarize], registry),
|
|
381
|
+
max_iters=10
|
|
382
|
+
)
|
|
405
383
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
* **Hard guardrails** — configure wall-clock deadlines and hop budgets directly
|
|
410
|
-
on `ReactPlanner`; attempts beyond the allotted hops surface deterministic
|
|
411
|
-
violations and ultimately finish with `reason="budget_exhausted"` alongside a
|
|
412
|
-
constraint snapshot.
|
|
413
|
-
* **Typed exit reasons** — runs now finish with one of
|
|
414
|
-
`answer_complete`, `no_path`, or `budget_exhausted`, keeping downstream code
|
|
415
|
-
simple and machine-checkable.
|
|
384
|
+
result = await planner.run("Explain PenguiFlow routing")
|
|
385
|
+
print(result.payload) # LLM orchestrated search → summarize automatically
|
|
386
|
+
```
|
|
416
387
|
|
|
417
|
-
|
|
418
|
-
recover via a cached index without leaving the JSON-only contract.
|
|
388
|
+
**Key capabilities:**
|
|
419
389
|
|
|
420
|
-
|
|
390
|
+
* **Autonomous tool selection** — LLM decides which tools to call and in what order based on your query
|
|
391
|
+
* **Type-safe execution** — All tool inputs/outputs validated with Pydantic, JSON schemas auto-generated from models
|
|
392
|
+
* **Parallel execution** — LLM can fan out to multiple tools concurrently with automatic result joining
|
|
393
|
+
* **Pause/resume workflows** — Add approval gates with `await ctx.pause()`, resume later with user input
|
|
394
|
+
* **Adaptive replanning** — Tool failures feed structured error suggestions back to LLM for recovery
|
|
395
|
+
* **Constraint enforcement** — Set hop budgets, deadlines, and token limits to prevent runaway execution
|
|
396
|
+
* **Planning hints** — Guide LLM behavior with ordering preferences, parallel groups, and tool filters
|
|
421
397
|
|
|
422
|
-
|
|
423
|
-
|
|
398
|
+
**Model support:**
|
|
399
|
+
* Install `penguiflow[planner]` for LiteLLM integration (100+ models: OpenAI, Anthropic, Azure, etc.)
|
|
400
|
+
* Or inject a custom `llm_client` for deterministic/offline testing
|
|
424
401
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
*
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
* **Deterministic telemetry** — branch errors, pauses, and joins are recorded as
|
|
431
|
-
structured observations so follow-up actions can re-plan or finish cleanly.
|
|
402
|
+
**Examples:**
|
|
403
|
+
* `examples/react_minimal/` — Basic sequential flow with stub LLM
|
|
404
|
+
* `examples/react_parallel/` — Parallel shard fan-out with join node
|
|
405
|
+
* `examples/react_pause_resume/` — Approval workflow with planning hints
|
|
406
|
+
* `examples/react_replan/` — Adaptive recovery from tool failures
|
|
432
407
|
|
|
433
|
-
See
|
|
434
|
-
round-trip.
|
|
408
|
+
See **manual.md Section 19** for complete documentation.
|
|
435
409
|
|
|
436
410
|
|
|
437
411
|
## 🧭 Repo Structure
|
|
@@ -5,21 +5,11 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<a href="https://github.com/penguiflow/penguiflow/actions/workflows/ci.yml">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<a href="https://
|
|
12
|
-
|
|
13
|
-
</a>
|
|
14
|
-
<a href="https://nightly.link/penguiflow/penguiflow/workflows/benchmarks/main/benchmarks.json.zip">
|
|
15
|
-
<img src="https://img.shields.io/badge/benchmarks-latest-orange" alt="Benchmarks">
|
|
16
|
-
</a>
|
|
17
|
-
<a href="https://pypi.org/project/penguiflow/">
|
|
18
|
-
<img src="https://img.shields.io/pypi/v/penguiflow.svg" alt="PyPI version">
|
|
19
|
-
</a>
|
|
20
|
-
<a href="https://github.com/penguiflow/penguiflow/blob/main/LICENSE">
|
|
21
|
-
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
|
22
|
-
</a>
|
|
8
|
+
<a href="https://github.com/hurtener/penguiflow/actions/workflows/ci.yml"><img src="https://github.com/hurtener/penguiflow/actions/workflows/ci.yml/badge.svg" alt="CI Status"></a>
|
|
9
|
+
<a href="https://github.com/hurtener/penguiflow"><img src="https://img.shields.io/badge/coverage-85%25-brightgreen" alt="Coverage"></a>
|
|
10
|
+
<a href="https://nightly.link/hurtener/penguiflow/workflows/benchmarks/main/benchmarks.json.zip"><img src="https://img.shields.io/badge/benchmarks-latest-orange" alt="Benchmarks"></a>
|
|
11
|
+
<a href="https://pypi.org/project/penguiflow/"><img src="https://img.shields.io/pypi/v/penguiflow.svg" alt="PyPI version"></a>
|
|
12
|
+
<a href="https://github.com/hurtener/penguiflow/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
23
13
|
</p>
|
|
24
14
|
|
|
25
15
|
**Async-first orchestration library for multi-agent and data pipelines**
|
|
@@ -33,6 +23,7 @@ It provides:
|
|
|
33
23
|
* **Retries, timeouts, backpressure**
|
|
34
24
|
* **Streaming chunks** (LLM-style token emission with `Context.emit_chunk`)
|
|
35
25
|
* **Dynamic loops** (controller nodes)
|
|
26
|
+
* **LLM-driven orchestration** (`ReactPlanner` for autonomous multi-step workflows with tool selection, parallel execution, and pause/resume)
|
|
36
27
|
* **Runtime playbooks** (callable subflows with shared metadata)
|
|
37
28
|
* **Per-trace cancellation** (`PenguiFlow.cancel` with `TraceCancelled` surfacing in nodes)
|
|
38
29
|
* **Deadlines & budgets** (`Message.deadline_s`, `WM.budget_hops`, and `WM.budget_tokens` guardrails that you can leave unset/`None`)
|
|
@@ -321,69 +312,52 @@ The new `penguiflow.testkit` module keeps unit tests tiny:
|
|
|
321
312
|
The harness is covered by `tests/test_testkit.py` and demonstrated in
|
|
322
313
|
`examples/testkit_demo/`.
|
|
323
314
|
|
|
324
|
-
###
|
|
315
|
+
### React Planner - LLM-Driven Orchestration
|
|
325
316
|
|
|
326
|
-
|
|
327
|
-
deterministic:
|
|
317
|
+
Build autonomous agents that select and execute tools dynamically using the ReAct (Reasoning + Acting) pattern:
|
|
328
318
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
* `penguiflow.planner.ReactPlanner` drives a JSON-only ReAct loop over those
|
|
332
|
-
descriptors, validating every LLM action with Pydantic and replaying invalid
|
|
333
|
-
steps to request corrections.
|
|
334
|
-
* LiteLLM stays optional—install `penguiflow[planner]` or inject a custom
|
|
335
|
-
`llm_client` for deterministic/offline runs.
|
|
336
|
-
|
|
337
|
-
See `examples/react_minimal/` for a stubbed end-to-end run.
|
|
338
|
-
|
|
339
|
-
### Trajectory summarisation & pause/resume (Phase B)
|
|
340
|
-
|
|
341
|
-
Phase B adds the tools you need for longer-running, approval-driven flows:
|
|
342
|
-
|
|
343
|
-
* **Token-aware summaries** — `Trajectory.compress()` keeps a compact state and
|
|
344
|
-
the planner can route summaries through a cheaper `summarizer_llm` before
|
|
345
|
-
asking for the next action.
|
|
346
|
-
* **`PlannerPause` contract** — nodes can call `await ctx.pause(...)` to return a
|
|
347
|
-
typed pause payload. Resume the run later with `ReactPlanner.resume(token, user_input=...)`.
|
|
348
|
-
* **Developer hints** — pass `planning_hints={...}` to enforce disallowed tools,
|
|
349
|
-
preferred ordering, or parallelism ceilings.
|
|
319
|
+
```python
|
|
320
|
+
from penguiflow import ReactPlanner, tool, build_catalog
|
|
350
321
|
|
|
351
|
-
|
|
352
|
-
|
|
322
|
+
@tool(desc="Search documentation")
|
|
323
|
+
async def search_docs(args: Query, ctx) -> Documents:
|
|
324
|
+
return Documents(results=await search(args.text))
|
|
353
325
|
|
|
354
|
-
|
|
326
|
+
@tool(desc="Summarize results")
|
|
327
|
+
async def summarize(args: Documents, ctx) -> Summary:
|
|
328
|
+
return Summary(text=await llm_summarize(args.results))
|
|
355
329
|
|
|
356
|
-
|
|
330
|
+
planner = ReactPlanner(
|
|
331
|
+
llm="gpt-4",
|
|
332
|
+
catalog=build_catalog([search_docs, summarize], registry),
|
|
333
|
+
max_iters=10
|
|
334
|
+
)
|
|
357
335
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
* **Hard guardrails** — configure wall-clock deadlines and hop budgets directly
|
|
362
|
-
on `ReactPlanner`; attempts beyond the allotted hops surface deterministic
|
|
363
|
-
violations and ultimately finish with `reason="budget_exhausted"` alongside a
|
|
364
|
-
constraint snapshot.
|
|
365
|
-
* **Typed exit reasons** — runs now finish with one of
|
|
366
|
-
`answer_complete`, `no_path`, or `budget_exhausted`, keeping downstream code
|
|
367
|
-
simple and machine-checkable.
|
|
336
|
+
result = await planner.run("Explain PenguiFlow routing")
|
|
337
|
+
print(result.payload) # LLM orchestrated search → summarize automatically
|
|
338
|
+
```
|
|
368
339
|
|
|
369
|
-
|
|
370
|
-
recover via a cached index without leaving the JSON-only contract.
|
|
340
|
+
**Key capabilities:**
|
|
371
341
|
|
|
372
|
-
|
|
342
|
+
* **Autonomous tool selection** — LLM decides which tools to call and in what order based on your query
|
|
343
|
+
* **Type-safe execution** — All tool inputs/outputs validated with Pydantic, JSON schemas auto-generated from models
|
|
344
|
+
* **Parallel execution** — LLM can fan out to multiple tools concurrently with automatic result joining
|
|
345
|
+
* **Pause/resume workflows** — Add approval gates with `await ctx.pause()`, resume later with user input
|
|
346
|
+
* **Adaptive replanning** — Tool failures feed structured error suggestions back to LLM for recovery
|
|
347
|
+
* **Constraint enforcement** — Set hop budgets, deadlines, and token limits to prevent runaway execution
|
|
348
|
+
* **Planning hints** — Guide LLM behavior with ordering preferences, parallel groups, and tool filters
|
|
373
349
|
|
|
374
|
-
|
|
375
|
-
|
|
350
|
+
**Model support:**
|
|
351
|
+
* Install `penguiflow[planner]` for LiteLLM integration (100+ models: OpenAI, Anthropic, Azure, etc.)
|
|
352
|
+
* Or inject a custom `llm_client` for deterministic/offline testing
|
|
376
353
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
*
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
* **Deterministic telemetry** — branch errors, pauses, and joins are recorded as
|
|
383
|
-
structured observations so follow-up actions can re-plan or finish cleanly.
|
|
354
|
+
**Examples:**
|
|
355
|
+
* `examples/react_minimal/` — Basic sequential flow with stub LLM
|
|
356
|
+
* `examples/react_parallel/` — Parallel shard fan-out with join node
|
|
357
|
+
* `examples/react_pause_resume/` — Approval workflow with planning hints
|
|
358
|
+
* `examples/react_replan/` — Adaptive recovery from tool failures
|
|
384
359
|
|
|
385
|
-
See
|
|
386
|
-
round-trip.
|
|
360
|
+
See **manual.md Section 19** for complete documentation.
|
|
387
361
|
|
|
388
362
|
|
|
389
363
|
## 🧭 Repo Structure
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Planner entry points."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .react import (
|
|
6
|
+
ParallelCall,
|
|
7
|
+
ParallelJoin,
|
|
8
|
+
PlannerAction,
|
|
9
|
+
PlannerFinish,
|
|
10
|
+
PlannerPause,
|
|
11
|
+
ReactPlanner,
|
|
12
|
+
Trajectory,
|
|
13
|
+
TrajectoryStep,
|
|
14
|
+
TrajectorySummary,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ParallelCall",
|
|
19
|
+
"ParallelJoin",
|
|
20
|
+
"PlannerAction",
|
|
21
|
+
"PlannerFinish",
|
|
22
|
+
"PlannerPause",
|
|
23
|
+
"ReactPlanner",
|
|
24
|
+
"Trajectory",
|
|
25
|
+
"TrajectoryStep",
|
|
26
|
+
"TrajectorySummary",
|
|
27
|
+
]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Prompt helpers for the React planner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_summary(summary: Mapping[str, Any]) -> str:
|
|
11
|
+
return "Trajectory summary: " + _compact_json(summary)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def render_resume_user_input(user_input: str) -> str:
|
|
15
|
+
return f"Resume input: {user_input}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def render_planning_hints(hints: Mapping[str, Any]) -> str:
|
|
19
|
+
lines: list[str] = []
|
|
20
|
+
constraints = hints.get("constraints")
|
|
21
|
+
if constraints:
|
|
22
|
+
lines.append(f"Respect the following constraints: {constraints}")
|
|
23
|
+
preferred = hints.get("preferred_order")
|
|
24
|
+
if preferred:
|
|
25
|
+
lines.append(f"Preferred order (if feasible): {preferred}")
|
|
26
|
+
parallels = hints.get("parallel_groups")
|
|
27
|
+
if parallels:
|
|
28
|
+
lines.append(f"Allowed parallel groups: {parallels}")
|
|
29
|
+
disallowed = hints.get("disallow_nodes")
|
|
30
|
+
if disallowed:
|
|
31
|
+
lines.append(f"Disallowed tools: {disallowed}")
|
|
32
|
+
preferred_nodes = hints.get("preferred_nodes")
|
|
33
|
+
if preferred_nodes:
|
|
34
|
+
lines.append(f"Preferred tools: {preferred_nodes}")
|
|
35
|
+
budget = hints.get("budget")
|
|
36
|
+
if budget:
|
|
37
|
+
lines.append(f"Budget hints: {budget}")
|
|
38
|
+
if not lines:
|
|
39
|
+
return ""
|
|
40
|
+
return "\n".join(lines)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def render_disallowed_node(node_name: str) -> str:
|
|
44
|
+
return (
|
|
45
|
+
f"tool '{node_name}' is not permitted by constraints. "
|
|
46
|
+
"Choose an allowed tool or revise the plan."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def render_ordering_hint_violation(expected: Sequence[str], proposed: str) -> str:
|
|
51
|
+
order = ", ".join(expected)
|
|
52
|
+
return (
|
|
53
|
+
"Ordering hint reminder: follow the preferred sequence "
|
|
54
|
+
f"[{order}]. Proposed: {proposed}. Revise the plan."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def render_parallel_limit(max_parallel: int) -> str:
|
|
59
|
+
return (
|
|
60
|
+
f"Parallel action exceeds max_parallel={max_parallel}. Reduce parallel fan-out."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def render_sequential_only(node_name: str) -> str:
|
|
65
|
+
return (
|
|
66
|
+
f"tool '{node_name}' must run sequentially. "
|
|
67
|
+
"Do not include it in a parallel plan."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def render_parallel_setup_error(errors: Sequence[str]) -> str:
|
|
72
|
+
detail = "; ".join(errors)
|
|
73
|
+
return f"Parallel plan invalid: {detail}. Revise the plan and retry."
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def render_empty_parallel_plan() -> str:
|
|
77
|
+
return "Parallel plan must include at least one branch in 'plan'."
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def render_parallel_with_next_node(next_node: str) -> str:
|
|
81
|
+
return (
|
|
82
|
+
f"Parallel plan cannot set next_node='{next_node}'. "
|
|
83
|
+
"Use 'join' to continue or finish the run explicitly."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def render_parallel_unknown_failure(node_name: str) -> str:
|
|
88
|
+
return (
|
|
89
|
+
f"tool '{node_name}' failed during parallel execution. "
|
|
90
|
+
"Investigate the tool and adjust the plan."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_summarizer_messages(
|
|
95
|
+
query: str,
|
|
96
|
+
history: Sequence[Mapping[str, Any]],
|
|
97
|
+
base_summary: Mapping[str, Any],
|
|
98
|
+
) -> list[dict[str, str]]:
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
"role": "system",
|
|
102
|
+
"content": (
|
|
103
|
+
"You are a summariser producing compact JSON state. "
|
|
104
|
+
"Respond with valid JSON matching the TrajectorySummary schema."
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"role": "user",
|
|
109
|
+
"content": _compact_json(
|
|
110
|
+
{
|
|
111
|
+
"query": query,
|
|
112
|
+
"history": list(history),
|
|
113
|
+
"current_summary": dict(base_summary),
|
|
114
|
+
}
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _compact_json(data: Any) -> str:
|
|
121
|
+
return json.dumps(data, ensure_ascii=False, sort_keys=True)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def render_tool(record: Mapping[str, Any]) -> str:
|
|
125
|
+
args_schema = _compact_json(record["args_schema"])
|
|
126
|
+
out_schema = _compact_json(record["out_schema"])
|
|
127
|
+
tags = ", ".join(record.get("tags", ()))
|
|
128
|
+
scopes = ", ".join(record.get("auth_scopes", ()))
|
|
129
|
+
parts = [
|
|
130
|
+
f"- name: {record['name']}",
|
|
131
|
+
f" desc: {record['desc']}",
|
|
132
|
+
f" side_effects: {record['side_effects']}",
|
|
133
|
+
f" args_schema: {args_schema}",
|
|
134
|
+
f" out_schema: {out_schema}",
|
|
135
|
+
]
|
|
136
|
+
if tags:
|
|
137
|
+
parts.append(f" tags: {tags}")
|
|
138
|
+
if scopes:
|
|
139
|
+
parts.append(f" auth_scopes: {scopes}")
|
|
140
|
+
if record.get("cost_hint"):
|
|
141
|
+
parts.append(f" cost_hint: {record['cost_hint']}")
|
|
142
|
+
if record.get("latency_hint_ms") is not None:
|
|
143
|
+
parts.append(f" latency_hint_ms: {record['latency_hint_ms']}")
|
|
144
|
+
if record.get("safety_notes"):
|
|
145
|
+
parts.append(f" safety_notes: {record['safety_notes']}")
|
|
146
|
+
if record.get("extra"):
|
|
147
|
+
parts.append(f" extra: {_compact_json(record['extra'])}")
|
|
148
|
+
return "\n".join(parts)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def build_system_prompt(
|
|
152
|
+
catalog: Sequence[Mapping[str, Any]],
|
|
153
|
+
*,
|
|
154
|
+
extra: str | None = None,
|
|
155
|
+
planning_hints: Mapping[str, Any] | None = None,
|
|
156
|
+
) -> str:
|
|
157
|
+
rendered_tools = "\n".join(render_tool(item) for item in catalog)
|
|
158
|
+
prompt = [
|
|
159
|
+
"You are PenguiFlow ReactPlanner, a JSON-only planner.",
|
|
160
|
+
"Follow these rules strictly:",
|
|
161
|
+
"1. Respond with valid JSON matching the PlannerAction schema.",
|
|
162
|
+
"2. Use the provided tools when necessary; never invent new tool names.",
|
|
163
|
+
"3. Keep 'thought' concise and factual.",
|
|
164
|
+
"4. When the task is complete, set 'next_node' to null "
|
|
165
|
+
"and include the final payload in 'args'.",
|
|
166
|
+
"5. Do not emit plain text outside JSON.",
|
|
167
|
+
"",
|
|
168
|
+
"Available tools:",
|
|
169
|
+
rendered_tools or "(none)",
|
|
170
|
+
]
|
|
171
|
+
if extra:
|
|
172
|
+
prompt.extend(["", "Additional guidance:", extra])
|
|
173
|
+
if planning_hints:
|
|
174
|
+
rendered_hints = render_planning_hints(planning_hints)
|
|
175
|
+
if rendered_hints:
|
|
176
|
+
prompt.extend(["", rendered_hints])
|
|
177
|
+
return "\n".join(prompt)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def build_user_prompt(query: str, context_meta: Mapping[str, Any] | None = None) -> str:
|
|
181
|
+
if context_meta:
|
|
182
|
+
return _compact_json({"query": query, "context": dict(context_meta)})
|
|
183
|
+
return _compact_json({"query": query})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def render_observation(
|
|
187
|
+
*,
|
|
188
|
+
observation: Any | None,
|
|
189
|
+
error: str | None,
|
|
190
|
+
failure: Mapping[str, Any] | None = None,
|
|
191
|
+
) -> str:
|
|
192
|
+
payload: dict[str, Any] = {}
|
|
193
|
+
if observation is not None:
|
|
194
|
+
payload["observation"] = observation
|
|
195
|
+
if error:
|
|
196
|
+
payload["error"] = error
|
|
197
|
+
if failure:
|
|
198
|
+
payload["failure"] = dict(failure)
|
|
199
|
+
if not payload:
|
|
200
|
+
payload["observation"] = None
|
|
201
|
+
return _compact_json(payload)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def render_hop_budget_violation(limit: int) -> str:
|
|
205
|
+
return (
|
|
206
|
+
"Hop budget exhausted; you have used all available tool calls. "
|
|
207
|
+
"Finish with the best answer so far or reply with no_path."
|
|
208
|
+
f" (limit={limit})"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def render_deadline_exhausted() -> str:
|
|
213
|
+
return (
|
|
214
|
+
"Deadline reached. Provide the best available conclusion or return no_path."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def render_validation_error(node_name: str, error: str) -> str:
|
|
219
|
+
return (
|
|
220
|
+
f"args for tool '{node_name}' did not validate: {error}. "
|
|
221
|
+
"Return corrected JSON."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def render_output_validation_error(node_name: str, error: str) -> str:
|
|
226
|
+
return (
|
|
227
|
+
f"tool '{node_name}' returned data that did not validate: {error}. "
|
|
228
|
+
"Ensure the tool output matches the declared schema."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def render_invalid_node(node_name: str, available: Sequence[str]) -> str:
|
|
233
|
+
options = ", ".join(sorted(available))
|
|
234
|
+
return (
|
|
235
|
+
f"tool '{node_name}' is not in the catalog. Choose one of: {options}."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def render_repair_message(error: str) -> str:
|
|
240
|
+
return (
|
|
241
|
+
"Previous response was invalid JSON or schema mismatch: "
|
|
242
|
+
f"{error}. Reply with corrected JSON only."
|
|
243
|
+
)
|