Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ graph TB
- ⚡ **Async Support**: Handle asynchronous operations
- 🛠 **Configuration-based**: Define workflows in YAML
- 💪 **Type-safe**: Leverages Elixir's pattern matching
- 🔌 **Plugin System**: Extend functionality with custom plugins

## Quick Start

Expand Down
157 changes: 157 additions & 0 deletions examples/plugin_system.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
defmodule WeatherPlugin do
@moduledoc """
Example plugin that provides weather forecast functionality.
"""
@behaviour AgentForge.Plugin

@impl true
def init(_opts) do
# In a real plugin, we might initialize connections or load configs
# No IO.puts here, we'll print it in the main program flow
:ok
end

@impl true
def register_tools(registry) do
registry.register("get_forecast", &forecast/1)
:ok
end

@impl true
def register_primitives do
# This plugin doesn't add any primitives
:ok
end

@impl true
def register_channels do
AgentForge.Notification.Registry.register_channel(WeatherNotificationChannel)
:ok
end

@impl true
def metadata do
%{
name: "Weather Plugin",
version: "1.0.0",
description: "Provides weather forecast functionality",
author: "AgentForge Team",
compatible_versions: ">= 0.1.0"
}
end

# Tool implementation
defp forecast(params) do
location = Map.get(params, "location", "Unknown")
# In a real plugin, this would make an API call to a weather service
# For this example, we just return mock data
%{
location: location,
temperature: 22,
conditions: "Sunny",
forecast: [
%{day: "Today", high: 24, low: 18, conditions: "Sunny"},
%{day: "Tomorrow", high: 22, low: 17, conditions: "Partly Cloudy"}
]
}
end
end

defmodule WeatherNotificationChannel do
@moduledoc """
Example notification channel for weather alerts.
"""
@behaviour AgentForge.Notification.Channel

@impl true
def name, do: :weather_alert

@impl true
def send(message, config) do
priority = Map.get(config, :priority, "normal")
# Remove quotes from message to match expected test format
clean_message = message |> String.replace("\"", "")
IO.puts("[Weather Alert - #{priority}] #{clean_message}")
:ok
end
end

# Start necessary processes
_registry_pid = case AgentForge.Notification.Registry.start_link([]) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end

_plugin_manager_pid = case AgentForge.PluginManager.start_link([]) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end

_tools_pid = case AgentForge.Tools.start_link([]) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end

# Load the weather plugin
IO.puts("Initializing Weather Plugin")
:ok = AgentForge.PluginManager.load_plugin(WeatherPlugin)

# Define a simple flow that uses the weather plugin
process_weather = fn signal, state ->
location = signal.data

# Use the plugin tool to get weather forecast
{:ok, forecast_tool} = AgentForge.Tools.get("get_forecast")
forecast = forecast_tool.(%{"location" => location})

# Emit a notification for extreme temperatures
if forecast.temperature > 30 do
notify = AgentForge.Primitives.notify(
[:weather_alert],
config: %{weather_alert: %{priority: "high"}}
)

alert_signal = AgentForge.Signal.new(:alert, "Extreme heat warning for #{location}")
notify.(alert_signal, state)
end

# Return the forecast data
{{:emit, forecast}, state}
end

# Create and execute a simple flow with a list of handlers
flow = [process_weather]

# Run the flow with different locations
locations = ["San Francisco", "Tokyo", "Sahara Desert"]

Enum.each(locations, fn location ->
signal = AgentForge.Signal.new(:location, location)
IO.puts("\nChecking weather for: #{location}")
{:ok, result, _state} = AgentForge.Flow.process(flow, signal, %{})
IO.puts("Current conditions: #{result.temperature}°C, #{result.conditions}")

# For demo purposes, simulate a high temperature for Sahara Desert
if location == "Sahara Desert" do
hot_signal = AgentForge.Signal.new(:location, location)
# Monkey patch the forecast tool temporarily to return extreme temperature
{:ok, old_fn} = AgentForge.Tools.get("get_forecast")

AgentForge.Tools.register("get_forecast", fn params ->
result = old_fn.(params)
Map.put(result, :temperature, 45)
end)

{:ok, _result, _state} = AgentForge.Flow.process(flow, hot_signal, %{})

# Restore original function
AgentForge.Tools.register("get_forecast", old_fn)
end
end)

# List all loaded plugins and their metadata
plugins = AgentForge.PluginManager.list_plugins()
IO.puts("\nLoaded Plugins:")
Enum.each(plugins, fn {_module, metadata} ->
IO.puts("- #{metadata.name} v#{metadata.version}: #{metadata.description}")
end)
142 changes: 142 additions & 0 deletions guides/plugin_system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# AgentForge Plugin System

This guide explains how to use and extend the AgentForge plugin system.

## Overview

The plugin system enables extending AgentForge with additional functionality while maintaining a lightweight core. Plugins can provide new tools, primitives, and notification channels.

## Using Plugins

To use a plugin in your AgentForge application:

1. Add the plugin to your dependencies in `mix.exs`:

```elixir
defp deps do
[
{:agent_forge, "~> 0.1.0"},
{:agent_forge_http, "~> 1.0.0"} # Example HTTP plugin
]
end
```

2. Load the plugin in your application:

```elixir
# In your application startup code
AgentForge.PluginManager.load_plugin(AgentForge.Plugins.HTTP)
```

3. Use the tools provided by the plugin:

```elixir
signal = AgentForge.Signal.new(:request, %{"url" => "https://example.com"})
flow = [AgentForge.Tools.execute("http_get")]
{:ok, result, _} = AgentForge.Flow.process(flow, signal, %{})
```

## Creating Plugins

To create your own plugin:

1. Implement the `AgentForge.Plugin` behaviour:

```elixir
defmodule MyApp.CustomPlugin do
@behaviour AgentForge.Plugin

@impl true
def init(_opts) do
# Initialize your plugin
:ok
end

@impl true
def register_tools(registry) do
# Register any tools your plugin provides
registry.register("my_tool", &my_tool_function/1)
:ok
end

@impl true
def metadata do
%{
name: "My Custom Plugin",
description: "Provides custom functionality",
version: "1.0.0",
author: "Your Name",
compatible_versions: ">= 0.1.0"
}
end

# Tool implementation
defp my_tool_function(params) do
# Process params and return a result
%{result: "Processed #{inspect(params)}"}
end
end
```

2. Optionally register primitives or notification channels:

```elixir
# To register primitives
@impl true
def register_primitives do
# Register your custom primitives
:ok
end

# To register notification channels
@impl true
def register_channels do
AgentForge.Notification.Registry.register_channel(MyApp.Notification.Channels.Custom)
:ok
end
```

## Notification Channels

Plugins can provide new notification channels by implementing the `AgentForge.Notification.Channel` behaviour:

```elixir
defmodule MyApp.Notification.Channels.Custom do
@behaviour AgentForge.Notification.Channel

@impl true
def name, do: :custom

@impl true
def send(message, config) do
# Send notification through your custom channel
IO.puts("Custom notification: #{message}, config: #{inspect(config)}")
:ok
end
end
```

To use a custom notification channel:

```elixir
notify = AgentForge.Primitives.notify(
[:console, :custom],
config: %{custom: %{some_setting: "value"}}
)

flow = [notify]
signal = AgentForge.Signal.new(:event, "Something happened")
AgentForge.Flow.process(flow, signal, %{})
```

## Best Practices

1. **Keep Plugins Focused**: Each plugin should provide a specific, well-defined set of functionality.

2. **Handle Dependencies Gracefully**: Check for required dependencies with `Code.ensure_loaded?/1` and provide meaningful error messages when they're missing.

3. **Document Your Plugin**: Include clear documentation on how to use your plugin and its configuration options.

4. **Version Compatibility**: Specify which versions of AgentForge your plugin is compatible with.

5. **Testing**: Write comprehensive tests for your plugin to ensure it behaves correctly.
6 changes: 5 additions & 1 deletion lib/agent_forge/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ defmodule AgentForge.Application do
children = [
{Registry, keys: :unique, name: Registry.AgentForge},
{AgentForge.Store, []},
{AgentForge.Tools, name: AgentForge.Tools}
{AgentForge.Tools, name: AgentForge.Tools},
# Add plugin manager
{AgentForge.PluginManager, []},
# Add notification registry
{AgentForge.Notification.Registry, []}
]

opts = [strategy: :one_for_one, name: AgentForge.Supervisor]
Expand Down
8 changes: 8 additions & 0 deletions lib/agent_forge/notification/channel.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule AgentForge.Notification.Channel do
@moduledoc """
Behaviour for notification channels in AgentForge.
"""

@callback name() :: atom()
@callback send(message :: String.t(), config :: map()) :: :ok | {:error, term()}
end
17 changes: 17 additions & 0 deletions lib/agent_forge/notification/channels/console.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule AgentForge.Notification.Channels.Console do
@moduledoc """
Console notification channel for AgentForge.
"""

@behaviour AgentForge.Notification.Channel

@impl true
def name, do: :console

@impl true
def send(message, config) do
prefix = Map.get(config, :prefix, "[Notification]")
IO.puts("#{prefix} #{message}")
:ok
end
end
Loading
Loading