Skip to content

Commit d1471e7

Browse files
authored
Merge pull request #9 from i365dev/feature/plugin-system
Implement Plugin System for AgentForge Framework
2 parents 5e8b12a + 914ba7d commit d1471e7

File tree

15 files changed

+797
-10
lines changed

15 files changed

+797
-10
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ graph TB
2424
-**Async Support**: Handle asynchronous operations
2525
- 🛠 **Configuration-based**: Define workflows in YAML
2626
- 💪 **Type-safe**: Leverages Elixir's pattern matching
27+
- 🔌 **Plugin System**: Extend functionality with custom plugins
2728

2829
## Quick Start
2930

examples/plugin_system.exs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
defmodule WeatherPlugin do
2+
@moduledoc """
3+
Example plugin that provides weather forecast functionality.
4+
"""
5+
@behaviour AgentForge.Plugin
6+
7+
@impl true
8+
def init(_opts) do
9+
# In a real plugin, we might initialize connections or load configs
10+
# No IO.puts here, we'll print it in the main program flow
11+
:ok
12+
end
13+
14+
@impl true
15+
def register_tools(registry) do
16+
registry.register("get_forecast", &forecast/1)
17+
:ok
18+
end
19+
20+
@impl true
21+
def register_primitives do
22+
# This plugin doesn't add any primitives
23+
:ok
24+
end
25+
26+
@impl true
27+
def register_channels do
28+
AgentForge.Notification.Registry.register_channel(WeatherNotificationChannel)
29+
:ok
30+
end
31+
32+
@impl true
33+
def metadata do
34+
%{
35+
name: "Weather Plugin",
36+
version: "1.0.0",
37+
description: "Provides weather forecast functionality",
38+
author: "AgentForge Team",
39+
compatible_versions: ">= 0.1.0"
40+
}
41+
end
42+
43+
# Tool implementation
44+
defp forecast(params) do
45+
location = Map.get(params, "location", "Unknown")
46+
# In a real plugin, this would make an API call to a weather service
47+
# For this example, we just return mock data
48+
%{
49+
location: location,
50+
temperature: 22,
51+
conditions: "Sunny",
52+
forecast: [
53+
%{day: "Today", high: 24, low: 18, conditions: "Sunny"},
54+
%{day: "Tomorrow", high: 22, low: 17, conditions: "Partly Cloudy"}
55+
]
56+
}
57+
end
58+
end
59+
60+
defmodule WeatherNotificationChannel do
61+
@moduledoc """
62+
Example notification channel for weather alerts.
63+
"""
64+
@behaviour AgentForge.Notification.Channel
65+
66+
@impl true
67+
def name, do: :weather_alert
68+
69+
@impl true
70+
def send(message, config) do
71+
priority = Map.get(config, :priority, "normal")
72+
# Remove quotes from message to match expected test format
73+
clean_message = message |> String.replace("\"", "")
74+
IO.puts("[Weather Alert - #{priority}] #{clean_message}")
75+
:ok
76+
end
77+
end
78+
79+
# Start necessary processes
80+
_registry_pid = case AgentForge.Notification.Registry.start_link([]) do
81+
{:ok, pid} -> pid
82+
{:error, {:already_started, pid}} -> pid
83+
end
84+
85+
_plugin_manager_pid = case AgentForge.PluginManager.start_link([]) do
86+
{:ok, pid} -> pid
87+
{:error, {:already_started, pid}} -> pid
88+
end
89+
90+
_tools_pid = case AgentForge.Tools.start_link([]) do
91+
{:ok, pid} -> pid
92+
{:error, {:already_started, pid}} -> pid
93+
end
94+
95+
# Load the weather plugin
96+
IO.puts("Initializing Weather Plugin")
97+
:ok = AgentForge.PluginManager.load_plugin(WeatherPlugin)
98+
99+
# Define a simple flow that uses the weather plugin
100+
process_weather = fn signal, state ->
101+
location = signal.data
102+
103+
# Use the plugin tool to get weather forecast
104+
{:ok, forecast_tool} = AgentForge.Tools.get("get_forecast")
105+
forecast = forecast_tool.(%{"location" => location})
106+
107+
# Emit a notification for extreme temperatures
108+
if forecast.temperature > 30 do
109+
notify = AgentForge.Primitives.notify(
110+
[:weather_alert],
111+
config: %{weather_alert: %{priority: "high"}}
112+
)
113+
114+
alert_signal = AgentForge.Signal.new(:alert, "Extreme heat warning for #{location}")
115+
notify.(alert_signal, state)
116+
end
117+
118+
# Return the forecast data
119+
{{:emit, forecast}, state}
120+
end
121+
122+
# Create and execute a simple flow with a list of handlers
123+
flow = [process_weather]
124+
125+
# Run the flow with different locations
126+
locations = ["San Francisco", "Tokyo", "Sahara Desert"]
127+
128+
Enum.each(locations, fn location ->
129+
signal = AgentForge.Signal.new(:location, location)
130+
IO.puts("\nChecking weather for: #{location}")
131+
{:ok, result, _state} = AgentForge.Flow.process(flow, signal, %{})
132+
IO.puts("Current conditions: #{result.temperature}°C, #{result.conditions}")
133+
134+
# For demo purposes, simulate a high temperature for Sahara Desert
135+
if location == "Sahara Desert" do
136+
hot_signal = AgentForge.Signal.new(:location, location)
137+
# Monkey patch the forecast tool temporarily to return extreme temperature
138+
{:ok, old_fn} = AgentForge.Tools.get("get_forecast")
139+
140+
AgentForge.Tools.register("get_forecast", fn params ->
141+
result = old_fn.(params)
142+
Map.put(result, :temperature, 45)
143+
end)
144+
145+
{:ok, _result, _state} = AgentForge.Flow.process(flow, hot_signal, %{})
146+
147+
# Restore original function
148+
AgentForge.Tools.register("get_forecast", old_fn)
149+
end
150+
end)
151+
152+
# List all loaded plugins and their metadata
153+
plugins = AgentForge.PluginManager.list_plugins()
154+
IO.puts("\nLoaded Plugins:")
155+
Enum.each(plugins, fn {_module, metadata} ->
156+
IO.puts("- #{metadata.name} v#{metadata.version}: #{metadata.description}")
157+
end)

guides/plugin_system.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# AgentForge Plugin System
2+
3+
This guide explains how to use and extend the AgentForge plugin system.
4+
5+
## Overview
6+
7+
The plugin system enables extending AgentForge with additional functionality while maintaining a lightweight core. Plugins can provide new tools, primitives, and notification channels.
8+
9+
## Using Plugins
10+
11+
To use a plugin in your AgentForge application:
12+
13+
1. Add the plugin to your dependencies in `mix.exs`:
14+
15+
```elixir
16+
defp deps do
17+
[
18+
{:agent_forge, "~> 0.1.0"},
19+
{:agent_forge_http, "~> 1.0.0"} # Example HTTP plugin
20+
]
21+
end
22+
```
23+
24+
2. Load the plugin in your application:
25+
26+
```elixir
27+
# In your application startup code
28+
AgentForge.PluginManager.load_plugin(AgentForge.Plugins.HTTP)
29+
```
30+
31+
3. Use the tools provided by the plugin:
32+
33+
```elixir
34+
signal = AgentForge.Signal.new(:request, %{"url" => "https://example.com"})
35+
flow = [AgentForge.Tools.execute("http_get")]
36+
{:ok, result, _} = AgentForge.Flow.process(flow, signal, %{})
37+
```
38+
39+
## Creating Plugins
40+
41+
To create your own plugin:
42+
43+
1. Implement the `AgentForge.Plugin` behaviour:
44+
45+
```elixir
46+
defmodule MyApp.CustomPlugin do
47+
@behaviour AgentForge.Plugin
48+
49+
@impl true
50+
def init(_opts) do
51+
# Initialize your plugin
52+
:ok
53+
end
54+
55+
@impl true
56+
def register_tools(registry) do
57+
# Register any tools your plugin provides
58+
registry.register("my_tool", &my_tool_function/1)
59+
:ok
60+
end
61+
62+
@impl true
63+
def metadata do
64+
%{
65+
name: "My Custom Plugin",
66+
description: "Provides custom functionality",
67+
version: "1.0.0",
68+
author: "Your Name",
69+
compatible_versions: ">= 0.1.0"
70+
}
71+
end
72+
73+
# Tool implementation
74+
defp my_tool_function(params) do
75+
# Process params and return a result
76+
%{result: "Processed #{inspect(params)}"}
77+
end
78+
end
79+
```
80+
81+
2. Optionally register primitives or notification channels:
82+
83+
```elixir
84+
# To register primitives
85+
@impl true
86+
def register_primitives do
87+
# Register your custom primitives
88+
:ok
89+
end
90+
91+
# To register notification channels
92+
@impl true
93+
def register_channels do
94+
AgentForge.Notification.Registry.register_channel(MyApp.Notification.Channels.Custom)
95+
:ok
96+
end
97+
```
98+
99+
## Notification Channels
100+
101+
Plugins can provide new notification channels by implementing the `AgentForge.Notification.Channel` behaviour:
102+
103+
```elixir
104+
defmodule MyApp.Notification.Channels.Custom do
105+
@behaviour AgentForge.Notification.Channel
106+
107+
@impl true
108+
def name, do: :custom
109+
110+
@impl true
111+
def send(message, config) do
112+
# Send notification through your custom channel
113+
IO.puts("Custom notification: #{message}, config: #{inspect(config)}")
114+
:ok
115+
end
116+
end
117+
```
118+
119+
To use a custom notification channel:
120+
121+
```elixir
122+
notify = AgentForge.Primitives.notify(
123+
[:console, :custom],
124+
config: %{custom: %{some_setting: "value"}}
125+
)
126+
127+
flow = [notify]
128+
signal = AgentForge.Signal.new(:event, "Something happened")
129+
AgentForge.Flow.process(flow, signal, %{})
130+
```
131+
132+
## Best Practices
133+
134+
1. **Keep Plugins Focused**: Each plugin should provide a specific, well-defined set of functionality.
135+
136+
2. **Handle Dependencies Gracefully**: Check for required dependencies with `Code.ensure_loaded?/1` and provide meaningful error messages when they're missing.
137+
138+
3. **Document Your Plugin**: Include clear documentation on how to use your plugin and its configuration options.
139+
140+
4. **Version Compatibility**: Specify which versions of AgentForge your plugin is compatible with.
141+
142+
5. **Testing**: Write comprehensive tests for your plugin to ensure it behaves correctly.

lib/agent_forge/application.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ defmodule AgentForge.Application do
77
children = [
88
{Registry, keys: :unique, name: Registry.AgentForge},
99
{AgentForge.Store, []},
10-
{AgentForge.Tools, name: AgentForge.Tools}
10+
{AgentForge.Tools, name: AgentForge.Tools},
11+
# Add plugin manager
12+
{AgentForge.PluginManager, []},
13+
# Add notification registry
14+
{AgentForge.Notification.Registry, []}
1115
]
1216

1317
opts = [strategy: :one_for_one, name: AgentForge.Supervisor]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule AgentForge.Notification.Channel do
2+
@moduledoc """
3+
Behaviour for notification channels in AgentForge.
4+
"""
5+
6+
@callback name() :: atom()
7+
@callback send(message :: String.t(), config :: map()) :: :ok | {:error, term()}
8+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule AgentForge.Notification.Channels.Console do
2+
@moduledoc """
3+
Console notification channel for AgentForge.
4+
"""
5+
6+
@behaviour AgentForge.Notification.Channel
7+
8+
@impl true
9+
def name, do: :console
10+
11+
@impl true
12+
def send(message, config) do
13+
prefix = Map.get(config, :prefix, "[Notification]")
14+
IO.puts("#{prefix} #{message}")
15+
:ok
16+
end
17+
end

0 commit comments

Comments
 (0)