Source code for langgoap.integrations.subgraph

"""Embed a GOAP planner as a sub-graph inside a larger LangGraph application.

Layer C of the three-layer low-code on-ramp (AD-2).  Offers the most
control: construct a :class:`GoapSubgraph` explicitly, compile it,
and drop it into a parent :class:`~langgraph.graph.StateGraph` via
:func:`add_goap_subgraph`.  All GOAP-internal state (``plan``,
``current_step``, ``execution_history``, ``blacklisted_actions``,
``action_failure_counts``) stays sealed inside the sub-graph; the
parent only sees two keys (default ``"world_state"`` and
``"plan_result"``).

Routing note — **LangGraph ``Send`` is not involved.**  ``Send`` is
the fan-out primitive for a single node; subgraph routing uses the
normal ``add_node`` / ``add_edge`` API.  A GOAP executor may still
use ``Send`` internally to dispatch parallel actions to sibling
worker nodes — see the ``flexible_job_shop`` tutorial for an example.
"""

from __future__ import annotations

from typing import Any, cast

from langchain_core.runnables import Runnable
from langgraph.graph import StateGraph
from langgraph.graph.state import CompiledStateGraph

from langgoap.actions import ActionSpec
from langgoap.goals import GoalSpec
from langgoap.graph.builder import GoapGraph


[docs] class GoapSubgraph: """A GOAP planner packaged as a reusable sub-graph. Args: actions: Actions the planner may use. goal: Goal specification. Pre-resolved at construction time; NL interpretation must happen before this class is built. Example:: sub = GoapSubgraph( actions=[a1, a2, a3], goal=GoalSpec(conditions={"done": True}), ) compiled = sub.compile() result = compiled.invoke({"world_state": {}, "goal": goal}) """
[docs] def __init__( self, actions: list[ActionSpec], goal: GoalSpec, ) -> None: self.actions = actions self.goal = goal
def compile(self, **kwargs: Any) -> CompiledStateGraph: """Compile the sub-graph. Args: **kwargs: Forwarded to :meth:`GoapGraph.compile` (``checkpointer``, ``store``). Returns: A compiled :class:`CompiledStateGraph` using the ``GoapState`` schema internally. """ graph = GoapGraph(actions=self.actions) return graph.compile(**kwargs)
[docs] def add_goap_subgraph( parent_builder: StateGraph, *, name: str, actions: list[ActionSpec], goal: GoalSpec, input_key: str = "world_state", output_key: str = "plan_result", ) -> None: """Attach a GOAP sub-graph as a node inside a parent ``StateGraph``. The sub-graph is compiled and wrapped in a closure that: 1. Reads ``state[input_key]`` from the parent state. 2. Invokes the sub-graph with a fresh ``GoapState`` carrying that world state and the configured ``goal``. 3. Writes the final sub-graph state into ``state[output_key]`` as a plain dict so the parent schema stays decoupled from ``GoapState``. The parent graph must declare ``input_key`` and ``output_key`` in its state schema (both are plain ``dict[str, Any]`` slots). Args: parent_builder: The parent :class:`StateGraph` under construction. name: Node name to register in the parent graph. actions: Actions for the sub-graph's planner. goal: Goal specification for the sub-graph. input_key: Parent-state key providing the initial world state. Defaults to ``"world_state"``. output_key: Parent-state key where the sub-graph's final state will be stored. Defaults to ``"plan_result"``. """ sub = GoapSubgraph(actions=actions, goal=goal) compiled_sub = sub.compile() def _node(parent_state: dict[str, Any]) -> dict[str, Any]: world_state = dict(parent_state.get(input_key) or {}) sub_result = compiled_sub.invoke({"world_state": world_state, "goal": goal}) return {output_key: dict(sub_result)} # StateGraph.add_node's signature is parameterised on the parent # state's TypedDict, which we do not know statically — cast to a # generic Runnable so mypy accepts the insertion. The runtime # contract still holds: the node receives and returns plain dicts. parent_builder.add_node(name, cast(Runnable[Any, Any], _node))
__all__ = ["GoapSubgraph", "add_goap_subgraph"]