Skip to main content

Expose tools as lightweight CLI commands for LLM agents

Project description

โšก agenticli

Expose tools as lightweight CLI commands for LLM agents ๐Ÿ”งโœจ

English | ไธญๆ–‡

Python License PyPI Version GitHub


โœจ What is this?

agenticli converts functions, classes, and schema-based tools into a stable CLI semantic layer. Instead of flooding prompts with large schemas, LLMs just output a single command string.

๐Ÿ’ก Philosophy: bash is everything. The command string is the most stable, restrained, and observable intermediate representation between LLMs and tool systems.

๐ŸŽฏ When to use agenticli?

Scenario agenticli helps?
You have many tools/functions and need a unified interface for LLMs โœ…
You don't want massive schemas injected into prompts โœ…
You want models to see minimal hints, expanding via --help โœ…
You want validation, help, errors, and lifecycle in one place โœ…

๐Ÿš€ Quick Start

pip install agenticli
from typing import Annotated
from agenticli import CommandRegistry, Option, command, command_group

@command_group(name="calc", description="Calculator commands")
class Calc:
    @command(name="add", description="Add numbers")
    def add(self,
        values: Annotated[list[float], Option(positional=True, value_name="n")]
    ) -> dict:
        return {"result": sum(values)}

registry = CommandRegistry()
registry.register(Calc)

# LLM sees this minimal command context:
print(registry.render_llm_context())
# -> You can use the following CLI commands:
#     calc: Calculator commands

result = registry.execute("calc add 10 20 30")
print(result.value if result.ok else result.error.render())
# -> {"result": 60.0}

๐Ÿ—๏ธ Core Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        LLM Output                            โ”‚
โ”‚                    "calc add 10 20 30"                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
                          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      CommandRegistry                          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  Parse   โ”‚โ”€โ–ถโ”‚ Validate โ”‚โ”€โ–ถโ”‚ Execute  โ”‚โ”€โ–ถโ”‚   Result   โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                             โ”‚
โ”‚  โ€ข Command hit/matching      โ€ข Lifecycle callbacks          โ”‚
โ”‚  โ€ข Help generation           โ€ข Error with suggestions        โ”‚
โ”‚  โ€ข Argument injection        โ€ข Chain execution (&&, ||, ;)  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
                          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Your Functions / Tools                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ“‹ Key Features

Feature Description
๐Ÿ”Œ Multiple Registrations Decorators, class inheritance, dataclass, Pydantic, schema wrapping
โšก CLI Parsing Positional args, quoted args, --option value, -o value, --opt=val, flags
๐Ÿ“– Smart Help Auto-generated usage, help text, LLM prompts
โœ… Validation Type coercion, requires/excludes, enums
๐Ÿ’ก Suggestions "Did you mean X?" for unknown commands/options/enums
๐Ÿ”„ Lifecycle Hooks before_execute, after_execute, on_error
๐Ÿƒ Internal Injection Hide callbacks/state from CLI, inject at runtime
๐Ÿ”— Chain Execution Quote-aware `cmd1 && cmd2
โณ Async Execution Native execute_async(..., chain=False)
๐Ÿ”Œ Tool Import Convert LangChain / AutoGen / OpenAI-style tools into CommandSpec

๐Ÿ“ Registration Patterns

1๏ธโƒฃ Decorator (Most Common)

from typing import Annotated
from agenticli import command, Option

@command(name="ls", description="List directory")
def ls(
    path: Annotated[str, Option(short='p', description="Directory path")],
    verbose: Annotated[bool, Option(short='v')] = False,
) -> list[str]:
    import os
    return os.listdir(path)

2๏ธโƒฃ Command Group

from agenticli import command_group, command

@command_group(name="db", description="Database operations")
class Database:
    @command(description="Create database")
    def create(self, name: str) -> None: ...

    @command(description="Drop database")
    def drop(self, name: str) -> None: ...

3๏ธโƒฃ Wrap Existing Tools

from agenticli import wrap_tool

class MyTool:
    name = "my_tool"
    description = "Does something"
    parameters = {"type": "object", "properties": {"x": {"type": "int"}}}
    async def execute(self, **kwargs): return kwargs

registry.register_spec(wrap_tool(MyTool()))

4๏ธโƒฃ Class Inheritance

from agenticli import CliCommand, CommandRegistry

class AddCommand(CliCommand):
    name = "add"
    description = "Add numbers"
    args_model = AddArgs

    async def run(self, **kwargs) -> dict:
        return {"result": sum(kwargs["values"])}

registry.register(AddCommand())

๐Ÿค– LLM Integration

Minimal Tool Schema

Expose only one exec tool to the LLM:

from agenticli import ExecTool

def run_command(command: str, **kwargs):
    result = registry.execute(command)
    return result.value if result.ok else result.error.render()

exec_tool = ExecTool(callback=run_command)
# Tool schema: {name: "exec", params: {command: string, timeout?: int}}

Lifecycle Callbacks

from agenticli import ExecutionCallbacks

def on_error(ctx):
    print(f"Error: {ctx.error.code} - {ctx.error.message}")

registry = CommandRegistry(
    callbacks=ExecutionCallbacks(on_error=on_error)
)

Internal Parameter Injection

from typing import Annotated
from agenticli import Callback, State, command

@command(name="process")
def process(
    data: list[str],
    cache: Annotated[object, State(factory=lambda ctx: load_cache())] = None,
):
    # cache is injected automatically, hidden from CLI
    return cached_transform(data, cache)

๐Ÿ”Œ Import External Tools

You can wrap existing framework tools into agenticli commands through functions in agenticli.adapters:

from agenticli import CommandRegistry
from agenticli.adapters import (
    wrap_autogen_tool,
    wrap_langchain_tool,
    wrap_openai_tool_schema,
)

registry = CommandRegistry()

registry.register_spec(wrap_langchain_tool(my_langchain_tool))
registry.register_spec(wrap_autogen_tool(my_autogen_tool))
registry.register_spec(
    wrap_openai_tool_schema(
        name="search_docs",
        description="Search docs",
        parameters={
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"],
        },
        handler=lambda query: {"query": query},
    )
)

โšก Async API

agenticli now supports native async execution:

result = await registry.execute_async("calc add 1 2 3")
items = await registry.execute_async("cmd1 ; cmd2", chain=True)

๐Ÿงพ Command Syntax

Command parsing uses shell-style quoting for arguments:

weather "New York" --unit fahrenheit
say 'single quoted text'
say "arg with \"nested\" quotes"

Backslash-newline continuations are normalized before parsing:

weather "New York" \
  --unit fahrenheit

Command chains support ;, &&, and ||. Operators inside quotes stay part of the argument instead of splitting the chain:

say "hello ; world" ; say done
say "hello && world" && say ok

Invalid input returns structured errors:

result = registry.execute('weather "Beijing')
assert result.ok is False
assert result.error.code == "parse_error"

registry.execute("missing && weather Beijing", chain=True)
# ["Error: Unknown command"]

๐Ÿ“– Auto-Generated Help

<name> --help and registry.help(name) render help from each command's ValidationAdapter.help_text(). Given:

@command_group(name="files", description="File and text operations")
class Files:
    @command(name="ls", description="List directory contents")
    def ls(
        self,
        path: Annotated[str, Option(short="p", description="Directory path", value_name="PATH", example="/tmp")],
        verbose: Annotated[bool, Option(short="v", description="Also list hidden entries")] = False,
        ext: Annotated[str, Option(short="e", description="Filter by extension", value_name="EXT", example=".log")] = "",
    ) -> list[str]: ...

files ls --help renders:

files ls - List directory contents

Usage:
  ls --path <PATH> [--verbose] [--ext <EXT>]

Args:
  -p,--path PATH required example:/tmp, Directory path
  -v,--verbose flag default:false, Also list hidden entries
  -e,--ext EXT default: example:.log, Filter by extension

Group help lists subcommands instead of arguments. The LLM-facing prompt from registry.render_llm_context() is intentionally minimal and does not embed the full help โ€” it tells the model to use <command> --help when it needs more.

0.2.5: the auto-generated help is now a flat, single-line-per-arg layout โ€” Header / Usage / Args. Modifiers on each arg are space-separated (flag, required, default:X, enum:[โ€ฆ], example:X) with , description appended at the end.

๐Ÿ“ฆ Stable API

# Core
CommandRegistry
CommandRegistry.register(target)
CommandRegistry.register_spec(spec)
CommandRegistry.unregister(name)
CommandRegistry.get(name)
CommandRegistry.has(name)
CommandRegistry.parse(command_str, chain=False)
CommandRegistry.execute(command_str, chain=False)
CommandRegistry.execute_async(command_str, chain=False)
CommandRegistry.discover(directory, *, context_provider=None, recursive=True, package=None, on_error="ignore")
CommandRegistry.match(text, chain=False, mode="command")
CommandRegistry.help(command=None)
CommandRegistry.render_llm_context(detailed=False)
CommandRegistry.commands

# Decorators
command(_func=None, *, name=None, description="", aliases=None, hidden=False, deprecated=None, include_in_prompt=None)
command_group(name="", description="", *, include_in_prompt=True, register_as_command=None)

# Helpers
CliCommand
wrap_tool(tool)
command_from_model(name, model, handler)
command_from_method(name, target, method_name)
wrap_langchain_tool(tool)
wrap_autogen_tool(tool)
wrap_openai_tool_schema(name, parameters, handler, ...)
Option
Injected / Callback / State
ExecutionCallbacks
ExecTool

Upgrading from 0.1.x? See docs/migration_0.2.md.

๐Ÿ’ก Examples

See example/provider_integration.py for a complete calc system with OpenAI/Anthropic integration:

pip install "agenticli[examples]"
python -m example.provider_integration --provider openai
python -m example.provider_integration --provider anthropic

For a local Linux-like command demo (pwd, cd, ls, cat, head, grep, wc):

python -m example.linux_like_shell

Usage pattern demos:

python -m example.decorator
python -m example.command_group
python -m example.cli_command
python -m example.command_from_model
python -m example.command_from_method
python -m example.wrap_tool
python -m example.external_adapters
python -m example.discover
python -m example.context_group_instance
python -m example.context_state_factory
python -m example.context_init

๐Ÿ”Ž Auto-Discovery

CommandRegistry.discover() walks a directory and registers every class that opts in via @command_group or by subclassing CliCommand. An optional context_provider callback binds request-scoped context to each discovered class โ€” useful when the same registry powers many tenants:

from agenticli import CommandRegistry

def provide_context(cls):
    if cls is UserService:
        return cls(user_id="alice", tenant_id="acme")
    return cls()

registry = CommandRegistry()
result = registry.discover(
    "example/discover_cmds",
    context_provider=provide_context,
    package="example.discover_cmds",
)
print(result.registered)   # newly added command names
print(result.errors)       # per-file failures (when on_error="ignore")

See example/discover.py for a runnable demo.

๐Ÿ“š Documentation

Language Link
๐Ÿ‡บ๐Ÿ‡ธ English README README.md
๐Ÿ‡จ๐Ÿ‡ณ ไธญๆ–‡ README README_zh.md
๐Ÿ‡บ๐Ÿ‡ธ English docs/index_en.md
๐Ÿ‡จ๐Ÿ‡ณ ไธญๆ–‡ docs/index_zh.md
Migration docs/migration_0.2.md

๐Ÿ“„ License

MIT

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

agenticli-0.2.5.tar.gz (179.6 kB view details)

Uploaded Source

Built Distribution

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

agenticli-0.2.5-py3-none-any.whl (47.4 kB view details)

Uploaded Python 3

File details

Details for the file agenticli-0.2.5.tar.gz.

File metadata

  • Download URL: agenticli-0.2.5.tar.gz
  • Upload date:
  • Size: 179.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for agenticli-0.2.5.tar.gz
Algorithm Hash digest
SHA256 028e9b711306d42ea8e5920b56c06dea2affdf24efff3c802ba50bea6cde3a32
MD5 f45ae6898d8b7236bb34ac2be146c921
BLAKE2b-256 efdb534ffa3e40df30a8697569071c405ecf27ac952416b5a77532c16d4f9162

See more details on using hashes here.

Provenance

The following attestation bundles were made for agenticli-0.2.5.tar.gz:

Publisher: python-publish.yml on YKONGCO/agenticli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agenticli-0.2.5-py3-none-any.whl.

File metadata

  • Download URL: agenticli-0.2.5-py3-none-any.whl
  • Upload date:
  • Size: 47.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for agenticli-0.2.5-py3-none-any.whl
Algorithm Hash digest
SHA256 c1a432ef814af486b27251e2bd293f655a0f4fd36074773f18fbd8865e940a76
MD5 a5da29458d1f2a4c87fb63d23b47d822
BLAKE2b-256 855ebae813676ef74abbd4b5a0cc25c3dad119b29363c03014746a56f65aaf1c

See more details on using hashes here.

Provenance

The following attestation bundles were made for agenticli-0.2.5-py3-none-any.whl:

Publisher: python-publish.yml on YKONGCO/agenticli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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