Skip to main content

tketool.pipeline

Typed node and edge orchestration backed by LangGraph. The public API uses Pydantic models and does not expose LangGraph state objects.

pip install tketool.pipeline

Basic graph

from pydantic import BaseModel
from tketool.pipeline import END, START, DirectEdge, Pipeline, node


class Question(BaseModel):
    text: str


class Answer(BaseModel):
    text: str


@node(Question, Answer, node_id="answer")
def answer(value: Question):
    return {"text": value.text.upper()}


graph = Pipeline(Question, Answer).add_node(answer).add_edges(
    DirectEdge(START, "answer"),
    DirectEdge("answer", END),
)

result = graph.invoke({"text": "hello"})

A class node declares the same contract with generics:

from tketool.pipeline import Node


class AnswerNode(Node[Question, Answer]):
    def execute(self, value: Question):
        return {"text": value.text.upper()}

execute() is the only execution method a custom class implements. A graph definition never contains a call field: type selects either a built-in node or an explicitly registered custom node class, and the node owns its behavior.

Per-execution shared context

Use a graph context for resources that change on every invocation, such as the current tenant, request-scoped metadata, permission object, data source, or prompt pool. The caller defines the context class; the pipeline creates one immutable ExecutionContext wrapper for the run and gives that same wrapper to every node, including parallel map workers and retry attempts.

from dataclasses import dataclass
from tketool.pipeline import ExecutionContext, Node, Pipeline


@dataclass(frozen=True)
class RequestContext:
    tenant_id: str
    knowledge_source: object
    prompt_pool: object


class AnswerNode(Node[Question, Answer]):
    def execute(
        self,
        value: Question,
        context: ExecutionContext[RequestContext],
    ):
        session = context.session
        return {"text": f"{session.tenant_id}:{value.text}"}


graph = Pipeline(Question, Answer, context_type=RequestContext)
result = graph.invoke(
    {"text": "hello"},
    context=RequestContext(
        tenant_id="tenant-a",
        knowledge_source=knowledge_source,
        prompt_pool=prompt_pool,
    ),
)

When context_type is declared, invoke, ainvoke, stream, and astream require an instance of that class. Different concurrent invocations receive different wrappers and resources. RunConfig(metadata=...) remains available as ExecutionContext.metadata for run labels and tracing.

The context type can be part of a saved graph definition, but context instances cannot:

types:
  models:
    Question: myapp.models:Question
    Answer: myapp.models:Answer
  contexts:
    RequestContext: myapp.context:RequestContext
  nodes:
    answer: myapp.nodes.answer:AnswerNode

pipeline:
  name: answer_graph
  input: Question
  output: Answer
  context: RequestContext

Session resources travel through LangGraph's run-scoped context, not graph state. They are therefore absent from Send payloads and checkpoints. The caller owns their construction, thread safety, and shutdown. In contrast, dependencies={...} passed to Pipeline.load/save supplies graph-lifetime constructor dependencies such as the built-in PromptNode's fixed pool.

YAML graph definitions

Definitions are versioned, strict, data-only YAML. types gives short names to importable Python symbols; pipeline.nodes creates node instances; flow owns all connections.

version: 1

types:
  models:
    Question: myapp.models:Question
    Answer: myapp.models:Answer
  nodes:
    answer: myapp.nodes.answer:AnswerNode

pipeline:
  name: answer_graph
  input: Question
  output: Answer

  nodes:
    answer:
      type: answer
      params:
        prefix: "result: "
      policy:
        attempts: 3
        retry_on: [ConnectionError, TimeoutError]
        timeout_seconds: 20
        metadata:
          role: llm

  flow:
    start: answer
    steps: []
    end: answer

Load only application modules that the caller explicitly trusts:

from tketool.pipeline import Pipeline

graph = Pipeline.load(
    "answer_graph.yaml",
    allowed_modules=["myapp"],
)
result = graph.invoke({"text": "hello"})

allowed_modules=["myapp"] allows myapp and its submodules. Imports outside that list fail with ComponentResolutionError. YAML uses safe_load; Python object tags and unknown fields are rejected.

Type declarations

Section Value Purpose
types.models package.module:ModelClass Pydantic input/output models used by the graph or built-in nodes
types.contexts package.module:ContextClass optional caller-defined per-execution context type
types.nodes package.module:NodeClass custom Node subclass, decorated node, or module-level function
types.errors package.module:ErrorClass custom exception classes allowed in policy.retry_on

Built-in retry names are Exception, RuntimeError, ValueError, TypeError, ConnectionError, and TimeoutError. Other errors must be declared in types.errors. Paths use module:Symbol; filesystem paths are not accepted.

Built-in node types

type Required definition Behavior
identity input, optional output validates and forwards the input
prompt input, output, params.prompt_key, params.prompt_pool gets an invoker from an injected prompt pool and validates its result
split optional rule/window params splits SplitInput.value into ordered SplitOutput.items
memory params.memory dependency, optional write defaults stores MemoryInput.content and returns MemoryOutput.memory
memory_recall params.memory dependency, optional recall defaults recalls ranked records for MemoryRecallInput.query

Runtime objects are named dependencies, never embedded in YAML:

nodes:
  answer:
    type: prompt
    input: Question
    output: Answer
    params:
      prompt_key: answer_question
      prompt_pool:
        dependency: prompt_pool
      serialize_calls: true
graph = Pipeline.load(
    "graph.yaml",
    allowed_modules=["myapp"],
    dependencies={"prompt_pool": prompt_pool},
)

Split and memory nodes

SplitNode has a fixed value -> items contract so its output can feed a MapEdge. rule: auto selects text or collection behavior from the input; text and collection make a type mismatch fail explicitly. chunk_overlap must be smaller than chunk_size. Text rules use recursive character splitting. The default priority is paragraph (\n\n), line, Chinese/ASCII sentence punctuation, comma/colon, whitespace, and finally character-level fallback, so both paragraph text and long punctuation-delimited strings work without extra configuration. Collection rules use item counts. Boundary whitespace is preserved by default; set strip_whitespace: true only when trimming chunk edges is intentional.

nodes:
  split:
    type: split
    params:
      rule: text
      chunk_size: 800
      chunk_overlap: 80
      separators: ["\n\n", "\n", "。", "!", "?", ";", ".", "!", "?", ";", ",", ",", ":", ":", "、", " ", ""]
      keep_separator: end
      strip_whitespace: true

separators is ordered. To split strictly by paragraphs, configure ["\n\n", ""]; to prioritize punctuation, configure for example ["。", "!", "?", ".", "!", "?", ""]. Keep the final empty string so an individual overlong paragraph or sentence can still be bounded by chunk_size.

Pass a custom object implementing split(value) -> list as a named params.splitter dependency when built-in rules are insufficient.

The two memory nodes use the existing tketool.llm.memory.Memory contract. They do not create a backend, choose a tenant/space, own connection shutdown, or embed a live memory object in YAML. Build the memory once and inject the same instance by name:

from tketool.llm.memory import create_memory
from tketool.pipeline import MemoryNode, MemoryRecallNode
from tketool.storage import MemoryBackend

memory = create_memory(MemoryBackend(), space="user-42")

saved = MemoryNode(
    memory=memory,
    kind="preference",
    tags=["profile"],
).invoke({"content": "用户喜欢乌龙茶", "idempotency_key": "request-1"})

recalled = MemoryRecallNode(
    memory=memory,
    limit=3,
    kinds=["preference"],
    tags=["profile"],
    using=["lexical"],
).invoke({"query": "用户喜欢喝什么?"})

Node-level kind, tags, metadata, idempotency_key, limit, kinds, tags, and using are defaults. Non-null values in each invocation override them; invocation metadata is merged over configured metadata. Semantic or entity recall still requires the corresponding feature to be configured on create_memory.

nodes:
  remember:
    type: memory
    params:
      memory: {dependency: user_memory}
      kind: preference
      tags: [profile]
  recall:
    type: memory_recall
    params:
      memory: {dependency: user_memory}
      limit: 3
      kinds: [preference]
      using: [lexical]
graph = Pipeline.load(
    "graph.yaml",
    allowed_modules=["myapp"],
    dependencies={"user_memory": memory},
)

Flow and edge definitions

The YAML exposes four built-in connection forms. Each steps item must match exactly one form.

Direct connection, including parallel fan-out:

- from: prepare
  to: normalize
- from: prepare
  to: [search, summarize]

A direct edge may adapt the source output with a declared module-level callable. The alias is resolved through the same import allowlist as nodes:

types:
  edges:
    to_memory: myapp.edges:to_memory

pipeline:
  flow:
    steps:
      - from: split
        to: remember
        transform: to_memory

Field-based conditional routing maps a source output field to targets:

- from: router
  switch:
    field: route
    cases:
      fast: quick_answer
      deep: researched_answer

When the route is derived rather than part of the source model, declare a module-level condition instead. A switch must contain exactly one of field or condition:

types:
  edges:
    choose_result: myapp.edges:choose_result

pipeline:
  flow:
    steps:
      - from: answer
        switch:
          condition: choose_result
          cases:
            success: done
            fallback: retry

Wait for all listed branches, then bind each complete node output to a field of the target node input:

- wait_for: [search, summarize]
  then: merge
  inputs:
    search_result: search
    summary_result: summarize

Use into instead of inputs when the target expects an ordered list:

- wait_for: [a, b]
  then: merge
  into: results

Map a list field to parallel worker executions and collect results in original input order:

- foreach: split.items
  run: worker
  as: item
  collect:
    then: merge
    into: results
    preserve_order: true

flow.start is the single graph entry. flow.end is one node id or a list of possible finish nodes. Version 1 requires an acyclic graph, waits for every source in a join, preserves map order, and does not serialize a race/first-result join or a conditional route directly to END.

Custom node rules

A reusable custom node is a module-level class. Declare input/output through the generic base, put behavior in execute(), and accept node_id, config, plus any values listed under YAML params:

from tketool.pipeline import Node


class AnswerNode(Node[Question, Answer]):
    def __init__(self, node_id=None, *, prefix="", config=None):
        self.prefix = prefix
        super().__init__(node_id=node_id, config=config)

    def execute(self, value: Question):
        return Answer(text=f"{self.prefix}{value.text.upper()}")

    def definition_params(self):
        return {"prefix": self.prefix}

definition_params() is needed only when a programmatically created custom node must be saved and reconstructed. Values must be YAML data or objects named in the dependencies mapping passed to save().

The decorator form remains supported:

from tketool.pipeline import node


@node(Question, Answer, node_id="answer")
def answer(value: Question):
    return Answer(text=value.text.upper())

Reference the exported decorated symbol in types.nodes. Decorated nodes and plain functions cannot receive YAML params; use a class when configuration is required. Importable module-level direct-edge transforms and switch conditions are declared through types.edges. Local classes, lambdas, conditional-edge transforms, result adapters, and other undeclared runtime callables remain non-serializable.

Generate, save, and reload

definition = graph.to_definition(dependencies={"prompt_pool": prompt_pool})
yaml_text = graph.to_yaml(dependencies={"prompt_pool": prompt_pool})
graph.save("graph.yaml", dependencies={"prompt_pool": prompt_pool})

same_graph = Pipeline.from_definition(
    definition,
    allowed_modules=["myapp"],
    dependencies={"prompt_pool": prompt_pool},
)
same_graph = Pipeline.from_yaml(
    yaml_text,
    allowed_modules=["myapp"],
    dependencies={"prompt_pool": prompt_pool},
)
same_graph = Pipeline.load(
    "graph.yaml",
    allowed_modules=["myapp"],
    dependencies={"prompt_pool": prompt_pool},
)

These methods save the graph definition, not execution state. LangGraph checkpointers remain the separate mechanism for saving a running thread's state and resuming it.

Prompt node

PromptNode receives the existing prompt pool directly. The pipeline package only relies on the pool's get_invoker(key) structural contract, so it does not own the model, API key, prompt registry, or pool lifecycle.

from tketool.pipeline import PromptNode

prompt_node = PromptNode(
    Question,
    Answer,
    node_id="answer",
    prompt_key="answer_question",
    prompt_pool=prompt_pool,
)

Prompt calls are serialized by default because PromptInvokerPool caches mutable invokers. A caller with a verified thread-safe pool can opt into same-node parallel calls with serialize_calls=False.

Routing and lists

  • ConditionalEdge selects a declared route.
  • MapEdge uses LangGraph Send internally to execute list items concurrently.
  • JoinEdge waits for all branches and restores mapped results to input order.
  • RunConfig(max_concurrency=n) bounds one invocation's concurrency.
  • invoke/ainvoke return the graph output model; stream/astream emit runtime-neutral NodeEvent values.
  • NodeConfig(timeout=...) is enforced by ainvoke/astream and raises the public NodeTimeoutError. Timed synchronous work may continue in its worker thread after the async graph stops waiting. Sync entrypoints do not support node timeouts.

The pre-2.0 stage/decorator API (invoke_all, invoke_one, ExecutionBoard) is intentionally removed rather than emulated with different semantics.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tketool_pipeline-1.4.0.tar.gz (40.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tketool_pipeline-1.4.0-py3-none-any.whl (46.2 kB view details)

Uploaded Python 3

File details

Details for the file tketool_pipeline-1.4.0.tar.gz.

File metadata

  • Download URL: tketool_pipeline-1.4.0.tar.gz
  • Upload date:
  • Size: 40.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.4

File hashes

Hashes for tketool_pipeline-1.4.0.tar.gz
Algorithm Hash digest
SHA256 fc1a8267842447c3e7908df1abb4249d7dd7aa81221cec3c43e01625c22c28e2
MD5 6fe49619eeabba0fda741050273bdebd
BLAKE2b-256 4965ac76d24ece7ca2ee4cc4c9e389fc301c46471d89f7bddaa4ab39561f4378

See more details on using hashes here.

File details

Details for the file tketool_pipeline-1.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for tketool_pipeline-1.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b5a94ca2881a85256114dffb1bb737de518705b0d2f937f53dd790f159103d44
MD5 ab2c21c7e976cecb4d4e27ee2d084a30
BLAKE2b-256 ecbdec1c826465b386ca2e646eb7ce504546606428d5d9d4794d65e0953e99f7

See more details on using hashes here.

Release history Release notifications | RSS feed

2.0.1

2 files

1.7.1

2 files

This release

1.4.0 This release

2 files

1.3.5

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page