agenticli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,264 @@
1
+ Metadata-Version: 2.4
2
+ Name: agenticli
3
+ Version: 0.1.0
4
+ Summary: Expose tools as lightweight CLI commands for LLM agents
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
9
+ Requires-Dist: pytest>=8.0; extra == 'dev'
10
+ Provides-Extra: examples
11
+ Requires-Dist: anthropic>=0.34; extra == 'examples'
12
+ Requires-Dist: openai>=2.32.0; extra == 'examples'
13
+ Provides-Extra: pydantic
14
+ Requires-Dist: pydantic<3,>=2; extra == 'pydantic'
15
+ Description-Content-Type: text/markdown
16
+
17
+ <div align="center">
18
+
19
+ # ⚑ agenticli
20
+
21
+ **Expose tools as lightweight CLI commands for LLM agents** πŸ”§βœ¨
22
+
23
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://python.org)
24
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
25
+ [![PyPI](https://img.shields.io/badge/pypi-agenticli-blue.svg)](https://pypi.org/project/agenticli/)
26
+
27
+ </div>
28
+
29
+ ---
30
+
31
+ ## ✨ What is this?
32
+
33
+ **llmcli** 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.
34
+
35
+ > πŸ’‘ **Philosophy: bash is everything.** The command string is the most stable, restrained, and observable intermediate representation between LLMs and tool systems.
36
+
37
+ ## 🎯 When to use llmcli?
38
+
39
+ | Scenario | llmcli helps? |
40
+ |----------|---------------|
41
+ | You have many tools/functions and need a unified interface for LLMs | βœ… |
42
+ | You don't want massive schemas injected into prompts | βœ… |
43
+ | You want models to see minimal hints, expanding via `--help` | βœ… |
44
+ | You want validation, help, errors, and lifecycle in one place | βœ… |
45
+
46
+ ## πŸš€ Quick Start
47
+
48
+ ```bash
49
+ pip install llmcli
50
+ ```
51
+
52
+ ```python
53
+ from typing import Annotated
54
+ from llmcli import CommandRegistry, Option, command, command_group
55
+
56
+ @command_group(name="calc", description="Calculator commands")
57
+ class Calc:
58
+ @command(name="add", description="Add numbers")
59
+ def add(self,
60
+ values: Annotated[list[float], Option(positional=True, value_name="n")]
61
+ ) -> dict:
62
+ return {"result": sum(values)}
63
+
64
+ registry = CommandRegistry()
65
+ registry.register(Calc)
66
+
67
+ # LLM sees this minimal prompt:
68
+ print(registry.get_llm_prompt())
69
+ # -> You can use the following CLI commands:
70
+ # calc: Calculator commands
71
+
72
+ # Execute:
73
+ registry.parse_and_execute("calc add 10 20 30")
74
+ # -> {"result": 60.0}
75
+ ```
76
+
77
+ ## πŸ—οΈ Core Architecture
78
+
79
+ ```
80
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
81
+ β”‚ LLM Output β”‚
82
+ β”‚ "calc add 10 20 30" β”‚
83
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
84
+ β”‚
85
+ β–Ό
86
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
87
+ β”‚ CommandRegistry β”‚
88
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
89
+ β”‚ β”‚ Parse │─▢│ Validate │─▢│ Execute │─▢│ Result β”‚ β”‚
90
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
91
+ β”‚ β”‚
92
+ β”‚ β€’ Command hit/matching β€’ Lifecycle callbacks β”‚
93
+ β”‚ β€’ Help generation β€’ Error with suggestions β”‚
94
+ β”‚ β€’ Argument injection β€’ Chain execution (&&, ||, ;) β”‚
95
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
96
+ β”‚
97
+ β–Ό
98
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
99
+ β”‚ Your Functions / Tools β”‚
100
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
101
+ ```
102
+
103
+ ## πŸ“‹ Key Features
104
+
105
+ | Feature | Description |
106
+ |---------|-------------|
107
+ | πŸ”Œ **Multiple Registrations** | Decorators, class inheritance, dataclass, Pydantic, schema wrapping |
108
+ | ⚑ **CLI Parsing** | Positional args, `--option value`, `-o value`, `--opt=val`, flags |
109
+ | πŸ“– **Smart Help** | Auto-generated usage, help text, LLM prompts |
110
+ | βœ… **Validation** | Type coercion, `requires`/`excludes`, enums |
111
+ | πŸ’‘ **Suggestions** | "Did you mean X?" for unknown commands/options/enums |
112
+ | πŸ”„ **Lifecycle Hooks** | `before_execute`, `after_execute`, `on_error` |
113
+ | πŸƒ **Internal Injection** | Hide callbacks/state from CLI, inject at runtime |
114
+ | πŸ”— **Chain Execution** | `cmd1 && cmd2 || cmd3 ; cmd4` |
115
+
116
+ ## πŸ“ Registration Patterns
117
+
118
+ ### 1️⃣ Decorator (Most Common)
119
+
120
+ ```python
121
+ from typing import Annotated
122
+ from llmcli import command, Option
123
+
124
+ @command(name="ls", description="List directory")
125
+ def ls(
126
+ path: Annotated[str, Option(short='p', description="Directory path")],
127
+ verbose: Annotated[bool, Option(short='v')] = False,
128
+ ) -> list[str]:
129
+ import os
130
+ return os.listdir(path)
131
+ ```
132
+
133
+ ### 2️⃣ Command Group
134
+
135
+ ```python
136
+ from llmcli import command_group, command
137
+
138
+ @command_group(name="db", description="Database operations")
139
+ class Database:
140
+ @command(description="Create database")
141
+ def create(self, name: str) -> None: ...
142
+
143
+ @command(description="Drop database")
144
+ def drop(self, name: str) -> None: ...
145
+ ```
146
+
147
+ ### 3️⃣ Wrap Existing Tools
148
+
149
+ ```python
150
+ from llmcli import wrap_tool
151
+
152
+ class MyTool:
153
+ name = "my_tool"
154
+ description = "Does something"
155
+ parameters = {"type": "object", "properties": {"x": {"type": "int"}}}
156
+ async def execute(self, **kwargs): return kwargs
157
+
158
+ registry.register_spec(wrap_tool(MyTool()))
159
+ ```
160
+
161
+ ### 4️⃣ Class Inheritance
162
+
163
+ ```python
164
+ from llmcli import CliCommand, CommandRegistry
165
+
166
+ class AddCommand(CliCommand):
167
+ name = "add"
168
+ description = "Add numbers"
169
+ args_model = AddArgs
170
+
171
+ async def run(self, **kwargs) -> dict:
172
+ return {"result": sum(kwargs["values"])}
173
+
174
+ registry.register(AddCommand())
175
+ ```
176
+
177
+ ## πŸ€– LLM Integration
178
+
179
+ ### Minimal Tool Schema
180
+
181
+ Expose only one `exec` tool to the LLM:
182
+
183
+ ```python
184
+ from llmcli import ExecTool
185
+
186
+ exec_tool = ExecTool(callback=registry.parse_and_execute)
187
+ # Tool schema: {name: "exec", params: {command: string, timeout?: int}}
188
+ ```
189
+
190
+ ### Lifecycle Callbacks
191
+
192
+ ```python
193
+ from llmcli import ExecutionCallbacks
194
+
195
+ def on_error(ctx):
196
+ print(f"Error: {ctx.error.code} - {ctx.error.message}")
197
+
198
+ registry = CommandRegistry(
199
+ callbacks=ExecutionCallbacks(on_error=on_error)
200
+ )
201
+ ```
202
+
203
+ ### Internal Parameter Injection
204
+
205
+ ```python
206
+ from typing import Annotated
207
+ from llmcli import Callback, State, command
208
+
209
+ @command(name="process")
210
+ def process(
211
+ data: list[str],
212
+ cache: Annotated[object, State(factory=lambda ctx: load_cache())] = None,
213
+ ):
214
+ # cache is injected automatically, hidden from CLI
215
+ return cached_transform(data, cache)
216
+ ```
217
+
218
+ ## πŸ“¦ Stable API
219
+
220
+ ```python
221
+ # Core
222
+ CommandRegistry
223
+ CommandRegistry.register(target)
224
+ CommandRegistry.register_spec(spec)
225
+ CommandRegistry.execute(command_str)
226
+ CommandRegistry.parse_and_execute(command_str)
227
+ CommandRegistry.render_help(command)
228
+ CommandRegistry.get_llm_prompt(detailed=False)
229
+
230
+ # Decorators
231
+ command(name=None, description="", aliases=None, hidden=False, deprecated=None)
232
+ command_group(name, description)
233
+
234
+ # Helpers
235
+ CliCommand
236
+ wrap_tool(tool)
237
+ command_from_model(name, model, handler)
238
+ command_from_method(name, target, method_name)
239
+ Option
240
+ Injected / Callback / State
241
+ ExecutionCallbacks
242
+ ExecTool
243
+ ```
244
+
245
+ ## πŸ’‘ Examples
246
+
247
+ See [example/demo.py](example/demo.py) for a complete calc system with OpenAI/Anthropic integration:
248
+
249
+ ```bash
250
+ pip install "llmcli[examples]"
251
+ python -m example.demo --provider openai
252
+ python -m example.demo --provider anthropic
253
+ ```
254
+
255
+ ## πŸ“š Documentation
256
+
257
+ | Language | Link |
258
+ |----------|------|
259
+ | πŸ‡ΊπŸ‡Έ English | [docs/index_en.md](docs/index_en.md) |
260
+ | πŸ‡¨πŸ‡³ δΈ­ζ–‡ | [docs/index_zh.md](docs/index_zh.md) |
261
+
262
+ ## πŸ“„ License
263
+
264
+ MIT
@@ -0,0 +1,12 @@
1
+ llmcli/__init__.py,sha256=Z3xVTfiY9-Bpx4zXk_Z8GKPOUFfdbY2Av3NBc1y3iFI,2835
2
+ llmcli/builtin.py,sha256=PIkGU6PjrIG3lPrtyGJgx3TYQyUzFZ1L_rl3-l0EfJk,2732
3
+ llmcli/core.py,sha256=W_Nygw6KuqWMYqm4Af3-rTi7mrL6D5Yi84fRllVAJFE,32519
4
+ llmcli/decorators.py,sha256=w5i9KkYd8pxM5Bh46sHtlJq_1KYE_oB41jcqjsJTEEo,4260
5
+ llmcli/parser.py,sha256=_2CEqgQhWjwfd_-m3Jo4FhxAgu0uYR5j9WwFCTdtZ28,10826
6
+ llmcli/tooling.py,sha256=oSxCmNutM9IWhaKbQoI2TXZ9PkMbdQXnGBAvqKANxJQ,9895
7
+ llmcli/types.py,sha256=Dl9FWaS124BU8WTyLuLDG-Nt8H_d9ks8gQvHdx80wp0,9741
8
+ llmcli/validation.py,sha256=ka_vDpcjSGHG_MLhF1LzHQV75eJxQDRnoIb5nRJH8AU,38577
9
+ agenticli-0.1.0.dist-info/METADATA,sha256=TihSvi9jZB-TINJ9TVR0f7LKvYCeZ7SVbxD82fmABaA,8684
10
+ agenticli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ agenticli-0.1.0.dist-info/licenses/LICENSE,sha256=knHFE1wChIbxkrbYlAr02Z7lAxmWeytdNUAqO3nsp9Y,1065
12
+ agenticli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 agenticli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
llmcli/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ """llmcli - expose tools as lightweight CLI commands for LLMs.
2
+
3
+ This package provides a framework for defining CLI commands that can be
4
+ parsed and executed by LLMs. It supports multiple command definition styles
5
+ including decorators, class inheritance, and schema-based wrapping.
6
+
7
+ Typical usage:
8
+ from llmcli import CommandRegistry, command
9
+
10
+ registry = CommandRegistry()
11
+
12
+ @command(description="List directory contents")
13
+ def ls(path: Annotated[str, Option(description="Directory path")]) -> list[str]:
14
+ import os
15
+ return os.listdir(path)
16
+
17
+ registry.register(ls)
18
+ result = registry.execute("ls /tmp")
19
+
20
+ Exports:
21
+ ArgSpec: Argument specification data class.
22
+ CliCommand: Base class for inheritance-based commands.
23
+ CommandError: Structured error with code, message, and hints.
24
+ CommandRegistry: Central registry for command management.
25
+ CommandSpec: Command specification data class.
26
+ ExecutionCallbacks: Lifecycle callbacks for command execution.
27
+ ExecutionContext: Context passed to lifecycle callbacks.
28
+ ExecutionResult: Structured execution result with ok/value/error.
29
+ ExecTool: Built-in command execution tool.
30
+ HitResult: Command matching/detection result.
31
+ Injected: Marker for injected (internal) parameters.
32
+ ParseResult: Parsed command result with arguments.
33
+ Callback: Alias for Injected for callback injection.
34
+ State: Alias for Injected for state injection.
35
+ clear_commands: Clear the global command registry.
36
+ command: Decorator to register a function as a command.
37
+ command_from_method: Create command from a class method.
38
+ command_from_model: Create command from model + handler.
39
+ command_group: Decorator for command groups with subcommands.
40
+ get_registered_commands: Get all registered command specs.
41
+ Option: Per-argument CLI metadata annotation.
42
+ wrap_tool: Wrap a schema-based tool as a command.
43
+ """
44
+
45
+ from llmcli.builtin import ExecTool
46
+ from llmcli.core import CommandRegistry
47
+ from llmcli.decorators import clear_commands, command, command_group, get_registered_commands
48
+ from llmcli.tooling import CliCommand, command_from_method, command_from_model, wrap_tool
49
+ from llmcli.types import ArgSpec, CommandError, CommandSpec, ExecutionCallbacks, ExecutionContext, ExecutionResult, HitResult, ParseResult
50
+ from llmcli.validation import Callback, Injected, Option, State
51
+
52
+ __all__ = [
53
+ "ArgSpec",
54
+ "CliCommand",
55
+ "CommandError",
56
+ "CommandRegistry",
57
+ "CommandSpec",
58
+ "ExecutionCallbacks",
59
+ "ExecutionContext",
60
+ "ExecutionResult",
61
+ "ExecTool",
62
+ "HitResult",
63
+ "Injected",
64
+ "ParseResult",
65
+ "Callback",
66
+ "State",
67
+ "clear_commands",
68
+ "command",
69
+ "command_from_method",
70
+ "command_from_model",
71
+ "command_group",
72
+ "get_registered_commands",
73
+ "Option",
74
+ "wrap_tool",
75
+ ]
llmcli/builtin.py ADDED
@@ -0,0 +1,90 @@
1
+ """Builtin tools for llmcli.
2
+
3
+ This module provides built-in command tools that ship with llmcli,
4
+ including the ExecTool for executing shell commands through a callback.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+ from typing import Any, Awaitable, Callable
11
+
12
+
13
+ class ExecTool:
14
+ """Minimal schema-based exec tool backed by a callback.
15
+
16
+ Provides a command execution tool that delegates to a user-provided
17
+ callback function. The callback receives the command string and any
18
+ additional arguments.
19
+
20
+ Attributes:
21
+ name: Command name, defaults to "exec".
22
+ description: Human-readable description of the command.
23
+ parameters: JSON schema describing the command arguments.
24
+
25
+ Example:
26
+ >>> def execute_cmd(cmd: str, timeout: int = 60):
27
+ ... return subprocess.run(cmd, shell=True, timeout=timeout)
28
+ ...
29
+ >>> tool = ExecTool(callback=execute_cmd)
30
+ >>> spec = wrap_tool(tool)
31
+ """
32
+
33
+ name = "exec"
34
+ description = "Execute a command string through a callback."
35
+ parameters = {
36
+ "type": "object",
37
+ "properties": {
38
+ "command": {
39
+ "type": "string",
40
+ "description": "Command text to execute",
41
+ },
42
+ "timeout": {
43
+ "type": "integer",
44
+ "description": "Execution timeout in seconds",
45
+ "default": 60,
46
+ },
47
+ },
48
+ "required": ["command"],
49
+ }
50
+
51
+ def __init__(self, callback: Callable[..., Any] | Callable[..., Awaitable[Any]]):
52
+ """Initialize ExecTool with execution callback.
53
+
54
+ Args:
55
+ callback: Function to execute with command. Can be sync or async.
56
+ """
57
+ self._callback = callback
58
+
59
+ async def execute(self, **kwargs: Any) -> Any:
60
+ """Execute the command via callback.
61
+
62
+ Args:
63
+ **kwargs: Arguments from parsed command, must include 'command'.
64
+
65
+ Returns:
66
+ Result from the callback function.
67
+ """
68
+ command = kwargs.pop("command", None)
69
+ if command is not None:
70
+ result = self._callback(command, **kwargs)
71
+ else:
72
+ result = self._callback(**kwargs)
73
+ if inspect.isawaitable(result):
74
+ return await result
75
+ return result
76
+
77
+ def to_schema(self) -> dict[str, Any]:
78
+ """Convert tool to OpenAI function schema format.
79
+
80
+ Returns:
81
+ Dictionary with type "function" and function definition.
82
+ """
83
+ return {
84
+ "type": "function",
85
+ "function": {
86
+ "name": self.name,
87
+ "description": self.description,
88
+ "parameters": self.parameters,
89
+ },
90
+ }