Skip to main content

Config-driven AI pipelines: logic in Python components, wiring in YAML specs.

Project description

karve

From the Old Norse karvi — a small, light, versatile longship built for quick coastal trips rather than grand voyages.

A lightweight, importable Python library for building AI-powered apps where logic lives in Python components and wiring lives in YAML config — swap prompts, models, personalities, or entire pipelines by editing a spec file, never code.

pip install karve            # core: pyyaml + pydantic only
pip install 'karve[all]'     # + httpx, jmespath, fastapi extras

Quickstart

spec.yaml:

pipeline:
  - type: template
    template: "{body}, adventurer"
  - type: shout
    excitement: 3

app.py:

import karve

@karve.step("shout")
class Shout(karve.BaseStep):
    class Config(karve.BaseStep.Config):
        excitement: int = 1

    async def run(self, msg):
        return msg.with_body(str(msg.body).upper() + "!" * self.config.excitement)

pipe = karve.build_pipeline(karve.load_spec("spec.yaml"))
print(pipe.run_sync("hello there").body)
# HELLO THERE, ADVENTURER!!!

That's the whole model: a Message (body + meta) flows through an ordered list of steps. Steps are Python classes registered by name; the YAML spec says which steps run, in what order, with what config. Config typos fail at build time with the step index and field name, not at request time.

Spec format

resources:                      # optional; arbitrary named config data
  personality: !include personalities/grumpy_dwarf.yaml
  prompts: !include prompts/dark_fantasy.yaml

endpoints:                      # optional; conventionally where model endpoints live
  narrator:
    url: https://api.example.com/v1/generate
    headers: { Authorization: "Bearer ${env:NARRATOR_KEY}" }

pipeline:                       # required; ordered list of steps
  - type: template
    template: ${resources.prompts.encounter}
  - type: http-call
    endpoint: ${endpoints.narrator}
    body_map: { prompt: "$.body" }
    response_map: "$.choices[0].text"
  • !include path.yaml splices another file in place (relative to the including file). Include cycles are detected and reported with the chain.
  • ${dotted.path} references any value in the merged spec. A reference that is the whole string splices structured data; embedded references stringify scalars. ${env:NAME} reads environment variables — secrets never live in YAML. References only: no expressions, no conditionals.
  • load_spec(path, overrides={...}) deep-merges overrides over the spec (dicts merge recursively, lists and scalars replace). Overrides always win.
  • Multiple pipelines: use a top-level pipelines: dict of name → step list and select with build_pipeline(spec, name="..."). A singular pipeline: key is the default.

Built-in steps

Exactly six ship with karve. Everything else is userland via @karve.step.

type Purpose Config fields
template Render a prompt with str.format-style vars from the message template (vars from msg.body if dict, plus {body} / {meta[...]})
http-call Generic model/API invocation — LLMs, image gen, plain APIs endpoint: {url, method?, headers?, timeout?}, body_map, response_map, raw
transform Data wrangling between steps expr (JMESPath) or fn (a @transform_fn("name") function) — exactly one
router Choose a sub-pipeline by key key (dotted path, e.g. body.category), routes, default (sub-pipeline | pass | error)
map Fan a sub-pipeline out over each item of a list body steps, concurrency (default 5)
retry Wrap a sub-pipeline with retries steps, attempts (3), backoff (1.0, exponential), retry_on (exception names, ["*"])

Notes:

  • http-call mapping: body_map values and response_map are JMESPath expressions with a $. prefix, evaluated against the message ({"body": ..., "meta": ...}) or response JSON. $.body / $.meta work without jmespath installed; deeper expressions need karve[transform]. Expressions are evaluated recursively inside nested dicts/lists, so chat-style payloads like messages: [{role: user, content: "$.body"}] work directly. Values without the prefix are literals.
  • Sub-pipelines are just nested step lists — the builder recursion applies everywhere, so router routes and map/retry bodies are real pipelines.
  • All step execution is async; pipeline.run_sync(...) wraps asyncio.run for scripts and tests.

FastAPI recipe

from contextlib import asynccontextmanager
from fastapi import FastAPI
from karve import load_spec, build_pipeline
from karve.integrations.fastapi import pipeline_router

@asynccontextmanager
async def lifespan(app):
    spec = load_spec("specs/app.yaml")
    app.state.narrate = build_pipeline(spec, name="narrate")
    app.include_router(pipeline_router(app.state.narrate, "/narrate"))
    yield

app = FastAPI(lifespan=lifespan)

pipeline_router maps request JSON → Message(body=json)pipeline.run{"body": ..., "meta": ...}. Building in lifespan means every spec error surfaces at startup, before a single request is served.

See examples/digest for a complete app: it fetches open GitHub issues, classifies each with a cheap model (map + retry), and has a stronger model write an executive summary — model tiers, prompts, and the data source are all swappable YAML fragments.

Philosophy

  1. Logic in Python, wiring in YAML. No conditionals, loops, or variables invented in YAML syntax. If behavior needs branching, it is a Python component that takes routes/options as data. We are not building a programming language inside YAML.
  2. Provider-agnostic. No Anthropic/OpenAI/Stability-specific code in core. The generic http-call step covers LLMs, image gen, video gen, and self-hosted models identically; provider sugar belongs in userland steps.
  3. Stateless pipelines. The library runs a message through steps and returns. Sessions, history, persistence, resume — all owned by the host app, never the library.

Out of scope, permanently: no YAML logic syntax, no scheduling, no persistence/resume, no agent loops, no UI, no provider SDKs in core.

Security

Specs are code-adjacent: they choose which registered steps run and where HTTP calls go. Only load specs you trust, exactly as you would only import modules you trust. karve parses YAML exclusively through yaml.SafeLoader (never yaml.load), so specs cannot construct arbitrary Python objects — and the test suite greps the codebase to keep it that way. Keep secrets out of specs; use ${env:...}.

Project details


Download files

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

Source Distribution

karve-0.1.0.tar.gz (36.6 kB view details)

Uploaded Source

Built Distribution

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

karve-0.1.0-py3-none-any.whl (22.7 kB view details)

Uploaded Python 3

File details

Details for the file karve-0.1.0.tar.gz.

File metadata

  • Download URL: karve-0.1.0.tar.gz
  • Upload date:
  • Size: 36.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for karve-0.1.0.tar.gz
Algorithm Hash digest
SHA256 82d8f70608790875d9309447269735d52ed9cf5828e1db4a6eb7e9150b71b1fa
MD5 ad598e68828c8f81c82216639a2c42ca
BLAKE2b-256 0ed28e1c974986071c8d395353a88db57c645bd7c95836161b955396a5afba44

See more details on using hashes here.

File details

Details for the file karve-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: karve-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 22.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for karve-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eff02b9e0368eaaf8ad8729d69f117575b31dbb9b5392908f24ac7a2165b09f6
MD5 d55c021f464ee68b8f7f3b7b8252166e
BLAKE2b-256 419a8aa1b4e944e3c117e17be8abbb3082fca18ee990cc703c93f773a4cf00b9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page