Skip to main content

Local LLM command agent with deterministic execution layer

Project description

knaif

knaif

Turn natural language into validated, executable actions — without letting a language model near your filesystem.

knaif runs a local model. No API key, no token meter, nothing leaves the machine.

import knaif.cli as nk

@nk.command(help="Add a task", keywords=["add", "create"])
def add(title: nk.Arg(help="task title")):
    ...

nk.App([add]).run()      # your CLI now accepts plain English

Why knaif

Most ways of putting an LLM in front of a tool give the model the ability to act — it emits a shell command, or calls tools in a loop, and you hope the guardrails hold.

knaif inverts that. The model is allowed to produce exactly one thing — a plan:

{ "plan": [ { "tool": "convert", "args": { "input": "clip.mp4", "format": "webm" } } ] }

Everything that decides whether that plan runs is ordinary deterministic code: schema validation, arguments checked against a tool registry, sandbox path checks, a safety category per tool, and a confirmation gate. A hallucinated tool name is rejected before execution. An argument your tool doesn't accept is rejected. A path outside the sandbox is rejected — and re-checked after variable substitution, because substitution can introduce a new path.

What that buys you as a developer:

  • Safety is a property of your registry, not the model's mood. You classify a tool destructive once; the gate is code. Swap the model and the guarantee holds.
  • One inference call, not an agent loop. The model proposes the whole plan; everything after is normal Python. No per-step round trip, no token meter, no nondeterminism in the middle of a workflow.
  • It runs on your user's hardware. A small local model plus deterministic code means the marginal cost of the millionth run equals the first — electricity on a laptop.
  • A typed, testable surface. Tools are declared in YAML and implemented as classes, so the whole thing is unit-testable without calling a model at all — mock inference is built in.

Scope is narrow on purpose: knaif is not a general problem solver. It targets tools that are genuinely hard to drive by hand. Free-form shell command generation is explicitly out of scope — knaif will not emit a command its own validator can't account for.

Install

pip install knaif

That's everything you need for mock inference and for Ollama. For in-process llama.cpp there is an extra:

pip install "knaif[llama]"

Heads-up: llama-cpp-python publishes only an sdist to PyPI, so this compiles from source and needs CMake and a C++ toolchain. If you'd rather not build it, use the project's prebuilt wheelspip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu — or just use Ollama, which needs no build step at all.

Try it without writing anything

A complete example CLI ships inside the package, so you can see the whole pipeline work before writing a line:

pip install "knaif[clock]"
python -m knaif.examples.clock "list timezones in europe"
python -m knaif.examples.clock "what time is it in Tokyo"

With no model installed it uses mock inference and still runs end to end — parse, validate, dispatch, result. Add Ollama or a GGUF (below) and the same command starts answering free-form phrasing. The source is ~340 lines across three files (knaif/examples/clock/): pure logic in store.py, then the same CLI built two ways — @nk.command decorators and from_click().

Getting a model

No model is bundled — not in this wheel, not anywhere. knaif is the engine; you choose what runs behind it. Three options, easiest first:

1. Ollama — it manages the download for you, and needs no build step:

ollama pull qwen3:4b
import knaif.cli as nk

app = nk.App([convert], orchestrator=nk.local_ollama("qwen3:4b"))

local_ollama() returns None and warns if Ollama isn't reachable, so your app degrades to mock inference instead of crashing. Its defaults are also tuned for reasoning models like Qwen3 — use the helper rather than building an orchestrator by hand, or you'll hit a JSON-constraint deadlock. Details: Reasoning models on Ollama.

2. A GGUF through llama.cpp — one call, no llama.cpp knowledge required:

app = nk.App([convert], orchestrator=nk.local_llama_cpp("model.gguf"))

local_llama_cpp() picks working defaults (8k context, full GPU offload, JSON handling for thinking-style models) so you don't have to learn llama.cpp's knobs. Override any of them — n_ctx, n_gpu_layers, n_threads — only if you want to. Like local_ollama(), it warns and returns None when the model or llama-cpp-python is missing, so your app falls back to mock rather than crashing.

Any instruct-tuned GGUF that emits clean JSON works. Ready-made ones are at huggingface.co/blackdeep/knaifknaif-qwen3-4b-v1 (~2.5 GB) is the general-purpose choice, knaif-qwen3-1.7b-v1 is smaller and faster:

curl -L -o model.gguf https://huggingface.co/blackdeep/knaif/resolve/main/knaif-qwen3-4b-v1-q4_k_m.gguf

A relative path resolves from your project root (the nearest directory containing .git, else the current directory); an absolute path always works. The warning names the path it actually tried, so a miss is easy to diagnose.

3. Mock inference — the default when no orchestrator is supplied. It plans from registry keywords rather than a model, which makes the whole pipeline testable in CI with no model, no GPU, and no network.

The knaif models pull command and the models.yaml resolver belong to the native CLI and repo checkout, not this package. From a pip install you supply the model explicitly, as above.

Two ways to use it

1. Add a natural-language front end to your own CLI

The self-contained case: everything you need is in this package.

import knaif.cli as nk

@nk.command(help="Convert a file", keywords=["convert", "transcode"])
def convert(src: nk.Arg(help="input file"), fmt: nk.Arg(help="target format")):
    ...

app = nk.App([convert])
app.run()                                          # sys.argv[1] as an utterance
app.invoke("turn report.docx into a pdf", dry_run=True)

Already using click? Wrap the existing group without touching your commands:

from myapp.cli import cli
app = nk.from_click(cli)

You get typed validation, safety categories, dry-run, and variable binding for free — the same pipeline the full engine uses.

2. Build a skill

When your domain outgrows a handful of commands, package it as a skill — a self-contained bundle of a YAML tool registry plus Python handlers. Skills give you intent expansion (one user request becoming a multi-step workflow), per-tool safety categories, and a reusable unit you can ship or hand to someone else.

A minimal skill is a directory with skill.yaml, tools.yaml, and a handlers module:

# my_skill/python/handlers.py
from knaif.handler_api import HandlerContext
from knaif.skill_base import Skill
from knaif.tool import Step

class GreetStep(Step):
    name = "greet"
    def handle(self, args: dict, ctx: HandlerContext) -> dict:
        return {"message": f"hello {args['name']}"}

class MySkill(Skill):
    tools = [GreetStep]

Then load and run it:

from knaif import CommandAgent

agent = CommandAgent.from_skill("my_skill", sandbox="./sandbox")
results = agent.execute_plan(
    {"plan": [{"tool": "greet", "args": {"name": "world"}}]},
    dry_run=True,
)

Every tool is a Step (executes, returns a result) or an Intent (expands into a sub-plan of Steps). HandlerContext carries the sandbox root, dry_run, confirmed, and a confirmation callback. The full contract — registry schema, safety categories, intent expansion, preflight, result formatting — is in TOOL_SCHEMA.md.

Keeping several skills side by side? Point create_agent() at the directory holding them:

from knaif import create_agent, list_skills

print(list_skills(skills_root="./skills"))
agent = create_agent("my_skill", sandbox="./sandbox", skills_root="./skills")

No skills ship in this wheel — the published package is the engine plus the knaif.cli SDK, so nothing pulls in dependencies for domains you don't use. If you want worked examples rather than a blank page, the repository carries two production skills (media processing and document handling) you can read or copy from.

Requirements

  • Python 3.10–3.14
  • A local model for real inference (llama.cpp via the llama extra, or Ollama). Mock inference needs nothing.
  • Skills that shell out to external tools need those tools installed — external binaries are never bundled.

Documentation

Skill authoring registry schema, Step / Intent, safety categories
Developer SDK @nk.command, nk.App, from_click
Architecture the plan pipeline end to end
Inference backends model resolution, llama.cpp and Ollama setup
Released models which models, why those, how they were tuned
Scope and safety what knaif deliberately will not do

Source, issues, and releases: github.com/blackdeep-tech/knaif

License

Apache-2.0 — see LICENSE and NOTICE.

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

knaif-1.0.0.tar.gz (234.1 kB view details)

Uploaded Source

Built Distribution

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

knaif-1.0.0-py3-none-any.whl (146.0 kB view details)

Uploaded Python 3

File details

Details for the file knaif-1.0.0.tar.gz.

File metadata

  • Download URL: knaif-1.0.0.tar.gz
  • Upload date:
  • Size: 234.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for knaif-1.0.0.tar.gz
Algorithm Hash digest
SHA256 cbd029158f4079d2a3f8872eb0ebc2351528a78802ec392b6435f7a0cf4dbc48
MD5 6cc25b120c6d0afb0a2b9737223ea9a2
BLAKE2b-256 56140a9543c0b294055ebbc627a81eb0d98e42990b1e5d730c72aa9ff73584e6

See more details on using hashes here.

File details

Details for the file knaif-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: knaif-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 146.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for knaif-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e25673f022a0f0d639172570aad22cb30c5e8d3fec55ab9d4c70ee4defb59fc1
MD5 c0b8b687f24359fa96f1cc8df743e766
BLAKE2b-256 189f7f52f8e79bb93fcf8b680df2f4d4dce14b7cd7c17ff4f0d626e944028592

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