Skip to content

Commit 32c72e2

Browse files
authored
Simplify plugin manager (#299)
* Simplify plugin manager Removed all the cruft trying to assume and change naming. Removed legacy stuff around list or dict handling Added simple name sense probe on plugin sync / add / remove Signed-off-by: Luke Hinds <[email protected]> * Mark nosec for non concerning pass * Optimise from review --------- Signed-off-by: Luke Hinds <[email protected]>
1 parent 6337734 commit 32c72e2

File tree

11 files changed

+221
-379
lines changed

11 files changed

+221
-379
lines changed

src/agent/cli/commands/plugin_manage.py

Lines changed: 172 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,62 @@
1111
logger = structlog.get_logger(__name__)
1212

1313

14+
def _find_similar_plugin_names(target_name: str, available_names: list[str], max_suggestions: int = 3) -> list[str]:
15+
"""Find similar plugin names using simple string similarity."""
16+
if not available_names:
17+
return []
18+
19+
suggestions = []
20+
21+
# 1. Exact match (case-insensitive)
22+
for name in available_names:
23+
if name.lower() == target_name.lower():
24+
return [name] # Exact match found
25+
26+
# 2. Simple character replacement (- <-> _)
27+
target_normalized = target_name.replace("_", "-")
28+
for name in available_names:
29+
name_normalized = name.replace("_", "-")
30+
if target_normalized.lower() == name_normalized.lower():
31+
suggestions.append(name)
32+
33+
# 3. Basic edit distance for other close matches
34+
if len(suggestions) < max_suggestions:
35+
36+
def simple_edit_distance(s1: str, s2: str) -> int:
37+
"""Simple Levenshtein distance calculation."""
38+
if len(s1) < len(s2):
39+
return simple_edit_distance(s2, s1)
40+
if len(s2) == 0:
41+
return len(s1)
42+
43+
previous_row = list(range(len(s2) + 1))
44+
for i, c1 in enumerate(s1):
45+
current_row = [i + 1]
46+
for j, c2 in enumerate(s2):
47+
insertions = previous_row[j + 1] + 1
48+
deletions = current_row[j] + 1
49+
substitutions = previous_row[j] + (c1 != c2)
50+
current_row.append(min(insertions, deletions, substitutions))
51+
previous_row = current_row
52+
return previous_row[-1]
53+
54+
# Calculate similarity for remaining names
55+
similarities = []
56+
for name in available_names:
57+
if name not in suggestions:
58+
distance = simple_edit_distance(target_name.lower(), name.lower())
59+
# Only suggest if distance is reasonable (less than half the length)
60+
if distance <= min(len(target_name), len(name)) // 2:
61+
similarities.append((name, distance))
62+
63+
# Sort by distance and add to suggestions
64+
similarities.sort(key=lambda x: x[1])
65+
suggestions.extend([name for name, _ in similarities[: max_suggestions - len(suggestions)]])
66+
67+
return suggestions[:max_suggestions]
68+
69+
1470
@click.command()
1571
@click.option("--dry-run", is_flag=True, help="Show what would be changed without making changes")
1672
@click.pass_context
@@ -86,8 +142,8 @@ def sync(ctx, dry_run: bool):
86142
fg="yellow",
87143
)
88144

89-
installed_plugins[plugin_info["package"]] = {
90-
"plugin_name": plugin_name,
145+
installed_plugins[plugin_name] = {
146+
"package_name": plugin_info["package"],
91147
"version": plugin_info["version"],
92148
"capabilities": capabilities,
93149
}
@@ -98,46 +154,41 @@ def sync(ctx, dry_run: bool):
98154
click.secho(f"Failed to discover installed plugins: {e}", fg="red")
99155
ctx.exit(1)
100156

101-
# Calculate changes needed - use package names for consistent comparison
102-
installed_package_names = set(installed_plugins.keys())
157+
# Calculate changes needed - use plugin (entry point) names for comparison
158+
installed_plugin_names = set(installed_plugins.keys())
103159

104160
# Plugins to add (installed but not in config)
105-
packages_to_add = installed_package_names - current_plugins
161+
plugins_to_add = installed_plugin_names - current_plugins
106162

107163
# Plugins to remove (in config but not installed)
108-
packages_to_remove = current_plugins - installed_package_names
164+
plugins_to_remove = current_plugins - installed_plugin_names
109165

110-
# Get packages to add (package names that need to be added)
111-
plugins_to_add = []
112-
for package_name, info in installed_plugins.items():
113-
if package_name in packages_to_add:
114-
plugins_to_add.append((info["plugin_name"], package_name))
115-
116-
# Keep package names for removals since that's how they're stored in config
117-
packages_to_remove_list = list(packages_to_remove)
166+
# Convert to list for processing
167+
plugins_to_add_list = list(plugins_to_add)
168+
plugins_to_remove_list = list(plugins_to_remove)
118169

119170
# Show summary of changes
120-
if plugins_to_add:
121-
click.secho(f"\nPlugins to add ({len(plugins_to_add)}):", fg="green")
122-
for plugin_name, package_name in sorted(plugins_to_add):
123-
package_info = installed_plugins[package_name]
171+
if plugins_to_add_list:
172+
click.secho(f"\nPlugins to add ({len(plugins_to_add_list)}):", fg="green")
173+
for plugin_name in sorted(plugins_to_add_list):
174+
plugin_info = installed_plugins[plugin_name]
124175
click.secho(
125-
f" + {package_name} (plugin: {plugin_name}, v{package_info['version']})",
176+
f" + {plugin_name} (package: {plugin_info['package_name']}, v{plugin_info['version']})",
126177
fg="green",
127178
)
128179

129-
if packages_to_remove_list:
130-
click.secho(f"\nPlugins to remove ({len(packages_to_remove_list)}):", fg="red")
131-
for package_name in sorted(packages_to_remove_list):
132-
click.secho(f" - {package_name} (package no longer installed)", fg="red")
180+
if plugins_to_remove_list:
181+
click.secho(f"\nPlugins to remove ({len(plugins_to_remove_list)}):", fg="red")
182+
for plugin_name in sorted(plugins_to_remove_list):
183+
click.secho(f" - {plugin_name} (plugin no longer installed)", fg="red")
133184

134-
if not plugins_to_add and not packages_to_remove_list:
185+
if not plugins_to_add_list and not plugins_to_remove_list:
135186
click.secho("\n✓ agentup.yml is already in sync with installed plugins", fg="green")
136187
return
137188

138189
if dry_run:
139190
click.secho(
140-
f"\nWould add {len(plugins_to_add)} and remove {len(packages_to_remove_list)} plugins",
191+
f"\nWould add {len(plugins_to_add_list)} and remove {len(plugins_to_remove_list)} plugins",
141192
fg="cyan",
142193
)
143194
click.secho("Run without --dry-run to apply changes", fg="cyan")
@@ -147,11 +198,11 @@ def sync(ctx, dry_run: bool):
147198
changes_made = False
148199

149200
# Add new plugins with discovered capabilities
150-
for _plugin_name, package_name in plugins_to_add:
201+
for plugin_name in plugins_to_add_list:
151202
try:
152-
plugin_info = installed_plugins[package_name]
203+
plugin_info = installed_plugins[plugin_name]
153204

154-
# Create plugin override with package name and capabilities
205+
# Create plugin override with capabilities
155206
from agent.config.intent import CapabilityOverride, PluginOverride
156207

157208
capability_overrides = {}
@@ -161,45 +212,45 @@ def sync(ctx, dry_run: bool):
161212
enabled=cap["enabled"], required_scopes=cap["required_scopes"]
162213
)
163214

164-
# Create plugin override (package name is the key)
215+
# Create plugin override (plugin name is the key)
165216
plugin_override = PluginOverride(
166217
enabled=True,
167218
capabilities=capability_overrides,
168219
)
169-
# Use package name as key for intuitive user experience
170-
intent_config.add_plugin(package_name, plugin_override)
220+
# Use plugin (entry point) name as key to match plugin loading logic
221+
intent_config.add_plugin(plugin_name, plugin_override)
171222

172223
if plugin_info["capabilities"]:
173224
click.secho(
174-
f" ✓ Added {package_name} with {len(plugin_info['capabilities'])} capabilities",
225+
f" ✓ Added {plugin_name} (from {plugin_info['package_name']}) with {len(plugin_info['capabilities'])} capabilities",
175226
fg="green",
176227
)
177228
else:
178229
click.secho(
179-
f" ✓ Added {package_name} with no capabilities",
230+
f" ✓ Added {plugin_name} (from {plugin_info['package_name']}) with no capabilities",
180231
fg="green",
181232
)
182233

183234
changes_made = True
184235
except (KeyError, AttributeError, ValueError, TypeError) as e:
185-
click.secho(f" ✗ Failed to add {package_name}: {e}", fg="red")
236+
click.secho(f" ✗ Failed to add {plugin_name}: {e}", fg="red")
186237

187238
# Remove plugins no longer installed
188-
for package_name in packages_to_remove_list:
239+
for plugin_name in plugins_to_remove_list:
189240
try:
190-
if package_name in intent_config.plugins:
191-
del intent_config.plugins[package_name]
192-
click.secho(f" ✓ Removed {package_name}", fg="green")
241+
if plugin_name in intent_config.plugins:
242+
del intent_config.plugins[plugin_name]
243+
click.secho(f" ✓ Removed {plugin_name}", fg="green")
193244
changes_made = True
194245
except (KeyError, AttributeError) as e:
195-
click.secho(f" ✗ Failed to remove {package_name}: {e}", fg="red")
246+
click.secho(f" ✗ Failed to remove {plugin_name}: {e}", fg="red")
196247

197248
# Save updated configuration
198249
if changes_made:
199250
try:
200251
save_intent_config(intent_config, str(intent_config_path))
201252
click.secho(
202-
f"\n✓ Updated agentup.yml with {len(plugins_to_add)} additions and {len(packages_to_remove_list)} removals",
253+
f"\n✓ Updated agentup.yml with {len(plugins_to_add_list)} additions and {len(plugins_to_remove_list)} removals",
203254
fg="green",
204255
)
205256
except (FileNotFoundError, yaml.YAMLError, PermissionError, OSError) as e:
@@ -274,6 +325,22 @@ def add(ctx, plugin_name: str):
274325
if not plugin_found:
275326
click.secho(f"Plugin '{plugin_name}' is not installed or is not an AgentUp plugin", fg="red")
276327
click.secho(f"Install it first with: uv add {plugin_name}", fg="cyan")
328+
329+
# Suggest similar plugin names from available installed plugins
330+
try:
331+
from agent.plugins.manager import PluginRegistry
332+
333+
registry = PluginRegistry()
334+
available_plugins_info = registry.discover_all_available_plugins()
335+
available_plugins = [p["name"] for p in available_plugins_info]
336+
suggestions = _find_similar_plugin_names(plugin_name, available_plugins)
337+
if suggestions:
338+
if len(suggestions) == 1:
339+
click.secho(f"Did you mean: {suggestions[0]}?", fg="cyan")
340+
else:
341+
click.secho(f"Did you mean one of: {', '.join(suggestions)}?", fg="cyan")
342+
except Exception:
343+
pass # nosec
277344
return
278345

279346
click.secho(
@@ -285,16 +352,62 @@ def add(ctx, plugin_name: str):
285352
click.secho(f"Failed to verify plugin installation: {e}", fg="red")
286353
ctx.exit(1)
287354

288-
# Add plugin to configuration (package name is used as the key)
355+
# Add plugin to configuration with capability discovery
289356
try:
290-
from agent.config.intent import PluginOverride
357+
from agent.config.intent import CapabilityOverride, PluginOverride
358+
359+
# Discover capabilities from the plugin (same logic as sync)
360+
capability_overrides = {}
361+
362+
try:
363+
# Load the plugin and discover capabilities
364+
import importlib.metadata as metadata
365+
366+
entry_points = metadata.entry_points()
367+
if hasattr(entry_points, "select"):
368+
plugin_entries = entry_points.select(group="agentup.plugins")
369+
else:
370+
plugin_entries = entry_points.get("agentup.plugins", [])
371+
372+
for entry_point in plugin_entries:
373+
if entry_point.name == actual_plugin_name:
374+
plugin_class = entry_point.load()
375+
plugin_instance = plugin_class()
376+
377+
# Get capability definitions if available
378+
if hasattr(plugin_instance, "get_capability_definitions"):
379+
cap_definitions = plugin_instance.get_capability_definitions()
380+
381+
for cap_def in cap_definitions:
382+
capability_overrides[cap_def.id] = CapabilityOverride(
383+
enabled=True, required_scopes=cap_def.required_scopes
384+
)
385+
break
386+
except (ImportError, AttributeError, TypeError, ValueError) as e:
387+
click.secho(
388+
f" Warning: Could not discover capabilities for {actual_plugin_name}: {e}",
389+
fg="yellow",
390+
)
291391

292-
plugin_override = PluginOverride(enabled=True)
392+
plugin_override = PluginOverride(
393+
enabled=True,
394+
capabilities=capability_overrides,
395+
)
293396

294-
# Use package name as key for consistent user experience
295-
intent_config.add_plugin(package_name, plugin_override)
397+
# Use plugin (entry point) name as key to match plugin loading logic
398+
intent_config.add_plugin(actual_plugin_name, plugin_override)
296399
save_intent_config(intent_config, str(intent_config_path))
297-
click.secho(f"✓ Added {package_name} to agentup.yml", fg="green")
400+
401+
if capability_overrides:
402+
click.secho(
403+
f"✓ Added {actual_plugin_name} (from package {package_name}) with {len(capability_overrides)} capabilities to agentup.yml",
404+
fg="green",
405+
)
406+
else:
407+
click.secho(
408+
f"✓ Added {actual_plugin_name} (from package {package_name}) with no capabilities to agentup.yml",
409+
fg="green",
410+
)
298411

299412
except (
300413
KeyError,
@@ -330,6 +443,17 @@ def remove(ctx, plugin_name: str):
330443
# Check if plugin is configured
331444
if not intent_config.plugins or plugin_name not in intent_config.plugins:
332445
click.secho(f"Plugin '{plugin_name}' is not configured in agentup.yml", fg="yellow")
446+
447+
# Suggest similar plugin names from configured plugins
448+
if intent_config.plugins:
449+
configured_plugins = list(intent_config.plugins.keys())
450+
suggestions = _find_similar_plugin_names(plugin_name, configured_plugins)
451+
if suggestions:
452+
if len(suggestions) == 1:
453+
click.secho(f"Did you mean: {suggestions[0]}?", fg="cyan")
454+
else:
455+
click.secho(f"Did you mean one of: {', '.join(suggestions)}?", fg="cyan")
456+
333457
return
334458

335459
# Remove the plugin
@@ -365,15 +489,9 @@ def reload(plugin_name: str):
365489
click.secho(f"Plugin '{plugin_name}' not found", fg="yellow")
366490
return
367491

368-
click.secho(f"Reloading plugin '{plugin_name}'...", fg="cyan")
369-
370-
if manager.reload_plugin(plugin_name):
371-
click.secho(f"✓ Successfully reloaded {plugin_name}", fg="green")
372-
else:
373-
click.secho(f"✗ Failed to reload {plugin_name}", fg="red")
374-
click.secho("[dim]Note: Entry point plugins cannot be reloaded[/dim]")
492+
click.secho("Restart the agent to reload plugin changes.", fg="cyan")
375493

376494
except ImportError:
377495
click.secho("Plugin system not available.", fg="red")
378496
except (AttributeError, KeyError, RuntimeError) as e:
379-
click.secho(f"Error reloading plugin: {e}", fg="red")
497+
click.secho(f"Error accessing plugin: {e}", fg="red")

src/agent/config/model.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -495,24 +495,6 @@ def name(self) -> str:
495495
middleware: MiddlewareConfig = Field(default_factory=MiddlewareConfig)
496496
mcp: MCPConfig = Field(default_factory=MCPConfig)
497497

498-
@field_validator("plugins", mode="before")
499-
@classmethod
500-
def validate_plugins_format(cls, v):
501-
"""Convert list format to dict format for backward compatibility."""
502-
if isinstance(v, list):
503-
# Convert list format to dict format using package names as keys
504-
plugin_dict = {}
505-
for plugin_config in v:
506-
if isinstance(plugin_config, dict) and "name" in plugin_config:
507-
plugin_name = plugin_config["name"]
508-
# Use the plugin name as key, store the rest as config
509-
plugin_dict[plugin_name] = plugin_config
510-
else:
511-
# Handle invalid plugin config
512-
continue
513-
return plugin_dict
514-
return v
515-
516498
# AI configuration
517499
ai: dict[str, Any] = Field(default_factory=dict, description="AI settings")
518500
ai_provider: dict[str, Any] = Field(default_factory=dict, description="AI provider configuration")

0 commit comments

Comments
 (0)