0
# Plugins
1
2
Extensible plugin architecture with hook specifications for registering models, tools, templates, and commands. This module enables third-party extensions and custom integrations through a comprehensive plugin system built on Pluggy.
3
4
## Capabilities
5
6
### Plugin Discovery and Management
7
8
Functions to discover and work with plugins in the LLM ecosystem.
9
10
```python { .api }
11
def get_plugins(all: bool = False) -> List[dict]:
12
"""
13
Get list of registered plugins with metadata.
14
15
Args:
16
all: If True, include default plugins. If False, only show third-party plugins.
17
18
Returns:
19
List of plugin dictionaries containing:
20
- name: Plugin name
21
- hooks: List of hook names implemented
22
- version: Plugin version (if available)
23
"""
24
25
def load_plugins():
26
"""Load all registered plugins from entry points."""
27
```
28
29
### Plugin Manager
30
31
The global plugin manager instance that coordinates all plugin operations.
32
33
```python { .api }
34
pm: PluginManager
35
"""
36
Global plugin manager instance using Pluggy framework.
37
38
Provides hook calling and plugin management functionality.
39
Available hook callers:
40
- pm.hook.register_models
41
- pm.hook.register_embedding_models
42
- pm.hook.register_tools
43
- pm.hook.register_template_loaders
44
- pm.hook.register_fragment_loaders
45
- pm.hook.register_commands
46
"""
47
```
48
49
### Hook Implementation Decorator
50
51
Decorator for implementing plugin hooks.
52
53
```python { .api }
54
@hookimpl
55
def plugin_function(*args, **kwargs):
56
"""
57
Decorator for implementing plugin hook functions.
58
59
Used to mark functions as implementations of specific hooks.
60
The function name should match the hook specification.
61
"""
62
```
63
64
### Hook Specifications
65
66
The plugin system defines several hook specifications that plugins can implement.
67
68
```python { .api }
69
def register_models(register):
70
"""
71
Hook for registering LLM models.
72
73
Args:
74
register: Function to call with (model, async_model, aliases)
75
76
Example:
77
@hookimpl
78
def register_models(register):
79
register(MyModel("my-model"), aliases=["alias1", "alias2"])
80
"""
81
82
def register_embedding_models(register):
83
"""
84
Hook for registering embedding models.
85
86
Args:
87
register: Function to call with (model, aliases)
88
89
Example:
90
@hookimpl
91
def register_embedding_models(register):
92
register(MyEmbeddingModel("my-embeddings"), aliases=["embed"])
93
"""
94
95
def register_tools(register):
96
"""
97
Hook for registering tools and toolboxes.
98
99
Args:
100
register: Function to call with (tool_or_function, name)
101
102
Example:
103
@hookimpl
104
def register_tools(register):
105
register(my_function, name="my_tool")
106
register(MyToolbox, name="my_toolbox")
107
"""
108
109
def register_template_loaders(register):
110
"""
111
Hook for registering template loaders.
112
113
Args:
114
register: Function to call with (prefix, loader_function)
115
116
Example:
117
@hookimpl
118
def register_template_loaders(register):
119
register("yaml", yaml_template_loader)
120
"""
121
122
def register_fragment_loaders(register):
123
"""
124
Hook for registering fragment loaders.
125
126
Args:
127
register: Function to call with (prefix, loader_function)
128
129
Example:
130
@hookimpl
131
def register_fragment_loaders(register):
132
register("file", file_fragment_loader)
133
"""
134
135
def register_commands(cli):
136
"""
137
Hook for registering CLI commands.
138
139
Args:
140
cli: Click CLI group to add commands to
141
142
Example:
143
@hookimpl
144
def register_commands(cli):
145
@cli.command()
146
def my_command():
147
click.echo("Hello from plugin!")
148
"""
149
```
150
151
## Usage Examples
152
153
### Basic Plugin Implementation
154
155
```python
156
import llm
157
158
# Create a simple plugin file (e.g., my_plugin.py)
159
@llm.hookimpl
160
def register_models(register):
161
"""Register a custom model."""
162
163
class EchoModel(llm.Model):
164
"""A model that echoes back the input."""
165
166
model_id = "echo"
167
168
def prompt(self, prompt, **kwargs):
169
# Simple echo implementation
170
return EchoResponse(f"Echo: {prompt}")
171
172
class EchoResponse(llm.Response):
173
def __init__(self, text):
174
self._text = text
175
176
def text(self):
177
return self._text
178
179
def __iter__(self):
180
yield self._text
181
182
# Register the model with aliases
183
register(EchoModel(), aliases=["echo", "test"])
184
185
# Plugin is automatically discovered and loaded
186
```
187
188
### Tool Plugin Example
189
190
```python
191
import llm
192
import requests
193
194
@llm.hookimpl
195
def register_tools(register):
196
"""Register HTTP tools."""
197
198
def http_get(url: str) -> str:
199
"""Make HTTP GET request and return response text."""
200
try:
201
response = requests.get(url, timeout=10)
202
response.raise_for_status()
203
return response.text[:1000] # Truncate for safety
204
except requests.RequestException as e:
205
raise llm.CancelToolCall(f"HTTP request failed: {e}")
206
207
def http_post(url: str, data: str) -> str:
208
"""Make HTTP POST request with data."""
209
try:
210
response = requests.post(url, data=data, timeout=10)
211
response.raise_for_status()
212
return f"POST successful: {response.status_code}"
213
except requests.RequestException as e:
214
raise llm.CancelToolCall(f"HTTP POST failed: {e}")
215
216
# Register individual tools
217
register(http_get, name="http_get")
218
register(http_post, name="http_post")
219
220
# Register a toolbox
221
class HttpToolbox(llm.Toolbox):
222
"""Collection of HTTP tools."""
223
224
def tools(self):
225
return [
226
llm.Tool.function(http_get),
227
llm.Tool.function(http_post),
228
llm.Tool.function(self.http_head)
229
]
230
231
def http_head(self, url: str) -> dict:
232
"""Make HTTP HEAD request and return headers."""
233
try:
234
response = requests.head(url, timeout=10)
235
response.raise_for_status()
236
return dict(response.headers)
237
except requests.RequestException as e:
238
raise llm.CancelToolCall(f"HTTP HEAD failed: {e}")
239
240
register(HttpToolbox, name="http")
241
242
# Use the registered tools
243
tools = llm.get_tools()
244
http_tools = [t for name, t in tools.items() if name.startswith("http")]
245
```
246
247
### Template Loader Plugin
248
249
```python
250
import llm
251
import yaml
252
import json
253
254
@llm.hookimpl
255
def register_template_loaders(register):
256
"""Register custom template loaders."""
257
258
def yaml_loader(spec: str) -> llm.Template:
259
"""Load template from YAML specification."""
260
try:
261
config = yaml.safe_load(spec)
262
return llm.Template(
263
name=config['name'],
264
prompt=config.get('prompt'),
265
system=config.get('system'),
266
model=config.get('model'),
267
defaults=config.get('defaults', {}),
268
options=config.get('options', {})
269
)
270
except Exception as e:
271
raise ValueError(f"Invalid YAML template: {e}")
272
273
def json_loader(spec: str) -> llm.Template:
274
"""Load template from JSON specification."""
275
try:
276
config = json.loads(spec)
277
return llm.Template(**config)
278
except Exception as e:
279
raise ValueError(f"Invalid JSON template: {e}")
280
281
def file_loader(file_path: str) -> llm.Template:
282
"""Load template from file."""
283
import os
284
if not os.path.exists(file_path):
285
raise FileNotFoundError(f"Template file not found: {file_path}")
286
287
with open(file_path) as f:
288
content = f.read()
289
290
# Simple template format
291
lines = content.strip().split('\n')
292
name = lines[0].replace('# ', '')
293
prompt = '\n'.join(lines[1:])
294
295
return llm.Template(name=name, prompt=prompt)
296
297
register("yaml", yaml_loader)
298
register("json", json_loader)
299
register("file", file_loader)
300
301
# Template loaders are now available
302
loaders = llm.get_template_loaders()
303
print(f"Available loaders: {list(loaders.keys())}")
304
```
305
306
### Fragment Loader Plugin
307
308
```python
309
import llm
310
import os
311
312
@llm.hookimpl
313
def register_fragment_loaders(register):
314
"""Register fragment loaders for modular content."""
315
316
def file_fragment_loader(file_path: str) -> llm.Fragment:
317
"""Load fragment from file."""
318
if not os.path.exists(file_path):
319
raise FileNotFoundError(f"Fragment file not found: {file_path}")
320
321
with open(file_path) as f:
322
content = f.read()
323
324
return llm.Fragment(content, source=f"file:{file_path}")
325
326
def url_fragment_loader(url: str) -> llm.Fragment:
327
"""Load fragment from URL."""
328
import requests
329
try:
330
response = requests.get(url, timeout=10)
331
response.raise_for_status()
332
return llm.Fragment(response.text, source=f"url:{url}")
333
except requests.RequestException as e:
334
raise ValueError(f"Could not load fragment from {url}: {e}")
335
336
def env_fragment_loader(env_var: str) -> llm.Fragment:
337
"""Load fragment from environment variable."""
338
value = os.getenv(env_var)
339
if value is None:
340
raise ValueError(f"Environment variable not set: {env_var}")
341
342
return llm.Fragment(value, source=f"env:{env_var}")
343
344
register("file", file_fragment_loader)
345
register("url", url_fragment_loader)
346
register("env", env_fragment_loader)
347
348
# Fragment loaders enable modular content
349
fragment_loaders = llm.get_fragment_loaders()
350
print(f"Fragment loaders: {list(fragment_loaders.keys())}")
351
```
352
353
### CLI Command Plugin
354
355
```python
356
import llm
357
import click
358
359
@llm.hookimpl
360
def register_commands(cli):
361
"""Register custom CLI commands."""
362
363
@cli.group()
364
def analyze():
365
"""Text analysis commands."""
366
pass
367
368
@analyze.command()
369
@click.argument("text")
370
@click.option("--model", "-m", default="gpt-3.5-turbo")
371
def sentiment(text, model):
372
"""Analyze sentiment of text."""
373
model_obj = llm.get_model(model)
374
response = model_obj.prompt(f"Analyze the sentiment of this text: {text}")
375
click.echo(response.text())
376
377
@analyze.command()
378
@click.argument("text")
379
@click.option("--model", "-m", default="gpt-3.5-turbo")
380
def summarize(text, model):
381
"""Summarize text."""
382
model_obj = llm.get_model(model)
383
response = model_obj.prompt(f"Summarize this text in one sentence: {text}")
384
click.echo(response.text())
385
386
@cli.command()
387
@click.option("--format", type=click.Choice(["json", "yaml", "table"]), default="table")
388
def plugin_info(format):
389
"""Show information about loaded plugins."""
390
plugins = llm.get_plugins(all=True)
391
392
if format == "json":
393
import json
394
click.echo(json.dumps(plugins, indent=2))
395
elif format == "yaml":
396
import yaml
397
click.echo(yaml.dump(plugins))
398
else:
399
# Table format
400
for plugin in plugins:
401
click.echo(f"Plugin: {plugin['name']}")
402
click.echo(f" Hooks: {', '.join(plugin['hooks'])}")
403
if 'version' in plugin:
404
click.echo(f" Version: {plugin['version']}")
405
click.echo()
406
407
# Commands are now available: llm analyze sentiment "I love this!"
408
```
409
410
### Complete Plugin Example
411
412
```python
413
# File: llm_weather_plugin.py
414
import llm
415
import requests
416
import json
417
418
class WeatherModel(llm.KeyModel):
419
"""Model that provides weather information."""
420
421
model_id = "weather"
422
needs_key = "weather"
423
key_env_var = "WEATHER_API_KEY"
424
425
def prompt(self, prompt, **kwargs):
426
# Extract location from prompt (simplified)
427
location = prompt.text() if hasattr(prompt, 'text') else str(prompt)
428
429
# Get weather data
430
api_key = self.get_key()
431
url = f"http://api.openweathermap.org/data/2.5/weather"
432
params = {"q": location, "appid": api_key, "units": "metric"}
433
434
response = requests.get(url, params=params)
435
weather_data = response.json()
436
437
if response.status_code == 200:
438
temp = weather_data["main"]["temp"]
439
desc = weather_data["weather"][0]["description"]
440
result = f"Weather in {location}: {temp}°C, {desc}"
441
else:
442
result = f"Could not get weather for {location}"
443
444
return WeatherResponse(result)
445
446
class WeatherResponse(llm.Response):
447
def __init__(self, text):
448
self._text = text
449
450
def text(self):
451
return self._text
452
453
def __iter__(self):
454
yield self._text
455
456
@llm.hookimpl
457
def register_models(register):
458
"""Register weather model."""
459
register(WeatherModel(), aliases=["weather", "forecast"])
460
461
@llm.hookimpl
462
def register_tools(register):
463
"""Register weather tools."""
464
465
def current_weather(location: str) -> str:
466
"""Get current weather for a location."""
467
# Implementation would use weather API
468
return f"Current weather in {location}: 22°C, sunny"
469
470
def weather_forecast(location: str, days: int = 5) -> str:
471
"""Get weather forecast for a location."""
472
return f"{days}-day forecast for {location}: Mostly sunny"
473
474
register(current_weather, name="weather")
475
register(weather_forecast, name="forecast")
476
477
@llm.hookimpl
478
def register_commands(cli):
479
"""Register weather CLI commands."""
480
481
@cli.command()
482
@click.argument("location")
483
def weather(location):
484
"""Get weather for a location."""
485
model = llm.get_model("weather")
486
response = model.prompt(location)
487
click.echo(response.text())
488
489
# Entry point in setup.py or pyproject.toml:
490
# [project.entry-points."llm"]
491
# weather = "llm_weather_plugin"
492
```
493
494
### Plugin Discovery and Inspection
495
496
```python
497
import llm
498
499
# Load all plugins
500
llm.load_plugins()
501
502
# Get plugin information
503
plugins = llm.get_plugins(all=True)
504
print("All plugins:")
505
for plugin in plugins:
506
print(f"- {plugin['name']}: {plugin['hooks']}")
507
if 'version' in plugin:
508
print(f" Version: {plugin['version']}")
509
510
# Get only third-party plugins
511
third_party = llm.get_plugins(all=False)
512
print(f"\nThird-party plugins: {len(third_party)}")
513
514
# Inspect available extensions
515
tools = llm.get_tools()
516
print(f"Available tools: {len(tools)}")
517
518
template_loaders = llm.get_template_loaders()
519
print(f"Template loaders: {list(template_loaders.keys())}")
520
521
fragment_loaders = llm.get_fragment_loaders()
522
print(f"Fragment loaders: {list(fragment_loaders.keys())}")
523
524
# Direct plugin manager access
525
plugin_names = [llm.pm.get_name(plugin) for plugin in llm.pm.get_plugins()]
526
print(f"Plugin manager has: {plugin_names}")
527
```
528
529
### Plugin Development Best Practices
530
531
```python
532
import llm
533
534
# Example of well-structured plugin
535
class MyPlugin:
536
"""Example plugin demonstrating best practices."""
537
538
def __init__(self):
539
self.initialized = False
540
541
def ensure_initialized(self):
542
if not self.initialized:
543
# Lazy initialization
544
self.setup_resources()
545
self.initialized = True
546
547
def setup_resources(self):
548
# Initialize any required resources
549
pass
550
551
# Global plugin instance
552
plugin_instance = MyPlugin()
553
554
@llm.hookimpl
555
def register_models(register):
556
"""Register models with proper error handling."""
557
try:
558
plugin_instance.ensure_initialized()
559
# Register models
560
pass
561
except Exception as e:
562
# Log error but don't crash plugin loading
563
print(f"Failed to register models: {e}")
564
565
@llm.hookimpl
566
def register_tools(register):
567
"""Register tools with validation."""
568
plugin_instance.ensure_initialized()
569
570
def validated_tool(input_data: str) -> str:
571
"""Tool with input validation."""
572
if not input_data.strip():
573
raise llm.CancelToolCall("Input cannot be empty")
574
575
# Process input
576
return f"Processed: {input_data}"
577
578
register(validated_tool, name="validated_tool")
579
580
# Plugin metadata (for entry points)
581
__version__ = "1.0.0"
582
__author__ = "Plugin Developer"
583
__description__ = "Example plugin for LLM"
584
```
585
586
This comprehensive plugin system enables extensive customization and integration of the LLM package with external services, custom models, specialized tools, and domain-specific functionality while maintaining a clean and consistent API.