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.yamlsplices 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 withbuild_pipeline(spec, name="..."). A singularpipeline: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-callmapping:body_mapvalues andresponse_mapare JMESPath expressions with a$.prefix, evaluated against the message ({"body": ..., "meta": ...}) or response JSON.$.body/$.metawork without jmespath installed; deeper expressions needkarve[transform]. Expressions are evaluated recursively inside nested dicts/lists, so chat-style payloads likemessages: [{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
routerroutes andmap/retrybodies are real pipelines. - All step execution is async;
pipeline.run_sync(...)wrapsasyncio.runfor 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
- 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.
- Provider-agnostic. No Anthropic/OpenAI/Stability-specific code in
core. The generic
http-callstep covers LLMs, image gen, video gen, and self-hosted models identically; provider sugar belongs in userland steps. - 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82d8f70608790875d9309447269735d52ed9cf5828e1db4a6eb7e9150b71b1fa
|
|
| MD5 |
ad598e68828c8f81c82216639a2c42ca
|
|
| BLAKE2b-256 |
0ed28e1c974986071c8d395353a88db57c645bd7c95836161b955396a5afba44
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eff02b9e0368eaaf8ad8729d69f117575b31dbb9b5392908f24ac7a2165b09f6
|
|
| MD5 |
d55c021f464ee68b8f7f3b7b8252166e
|
|
| BLAKE2b-256 |
419a8aa1b4e944e3c117e17be8abbb3082fca18ee990cc703c93f773a4cf00b9
|