Packages

Synapse integration for altar_ai - SDK-backed LLM providers for multi-agent workflows. Provides unified adapter layer for Gemini, Claude, and Codex with automatic fallback, workflow actions, signal handlers, and telemetry bridging.

Current section

Files

Jump to
synapse_ai lib synapse ai signal_handlers.ex
Raw

lib/synapse/ai/signal_handlers.ex

defmodule Synapse.AI.SignalHandlers do
@moduledoc """
Pre-built signal handlers for common AI operations.
Register with SignalRouter to enable AI-powered signal processing.
## Example: Classify and Route
Synapse.SignalRouter.register_handler(
:incoming_messages,
&Synapse.AI.SignalHandlers.classify_and_route/2,
labels: ["urgent", "normal", "spam"],
text_path: [:data, :message, :body]
)
## Example: Enrich with Embeddings
Synapse.SignalRouter.register_handler(
:documents,
&Synapse.AI.SignalHandlers.enrich_with_embeddings/2,
text_path: [:data, :content],
adapter: :gemini
)
"""
require Logger
@doc """
Handler that classifies incoming signals and routes based on classification.
## Options
- `:adapter` - The AI adapter to use (default: Composite)
- `:labels` - List of classification labels (required)
- `:text_path` - Path to extract text from signal (default: [:data, :text])
- `:route_prefix` - Prefix for routed topics (default: "classified_")
## Examples
# Routes to :classified_urgent, :classified_normal, or :classified_spam
classify_and_route(signal, labels: ["urgent", "normal", "spam"])
"""
def classify_and_route(signal, opts) do
adapter = Keyword.get(opts, :adapter, Altar.AI.Adapters.Composite.default())
labels = Keyword.fetch!(opts, :labels)
text_path = Keyword.get(opts, :text_path, [:data, :text])
route_prefix = Keyword.get(opts, :route_prefix, "classified_")
text = get_in(signal, text_path)
if is_nil(text) do
Logger.warning("No text found at path #{inspect(text_path)} in signal")
{:route, :classification_failed, signal}
else
case Altar.AI.classify(adapter, text, labels) do
{:ok, classification} ->
route_topic = String.to_atom("#{route_prefix}#{classification.label}")
updated_signal =
signal
|> Map.put(:classification, classification)
|> Map.update(:metadata, %{}, fn meta ->
Map.merge(meta, %{
classified_at: DateTime.utc_now(),
classification_label: classification.label,
classification_confidence: classification.confidence
})
end)
{:route, route_topic, updated_signal}
{:error, error} ->
Logger.error("Classification failed: #{inspect(error)}")
{:route, :classification_failed, Map.put(signal, :error, error)}
end
end
end
@doc """
Handler that enriches signals with vector embeddings.
## Options
- `:adapter` - The AI adapter to use (default: Composite)
- `:text_path` - Path to extract text from signal (default: [:data, :text])
- `:embedding_key` - Key to store embeddings under (default: :embedding)
## Examples
enrich_with_embeddings(signal, text_path: [:data, :content])
"""
def enrich_with_embeddings(signal, opts) do
adapter = Keyword.get(opts, :adapter, Altar.AI.Adapters.Composite.default())
text_path = Keyword.get(opts, :text_path, [:data, :text])
embedding_key = Keyword.get(opts, :embedding_key, :embedding)
text = get_in(signal, text_path)
if is_nil(text) do
Logger.warning("No text found at path #{inspect(text_path)} in signal")
{:continue, signal}
else
case Altar.AI.embed(adapter, text) do
{:ok, vector} ->
updated_signal =
signal
|> put_in([embedding_key], vector)
|> Map.update(:metadata, %{}, fn meta ->
Map.put(meta, :embedded_at, DateTime.utc_now())
end)
{:continue, updated_signal}
{:error, error} ->
Logger.error("Embedding generation failed: #{inspect(error)}")
{:continue, Map.put(signal, :embedding_error, error)}
end
end
end
@doc """
Handler that generates summaries for text content in signals.
## Options
- `:adapter` - The AI adapter to use (default: Composite)
- `:text_path` - Path to extract text from signal (default: [:data, :text])
- `:summary_key` - Key to store summary under (default: :summary)
- `:max_length` - Maximum summary length in words (optional)
## Examples
generate_summary(signal, text_path: [:data, :article], max_length: 100)
"""
def generate_summary(signal, opts) do
adapter = Keyword.get(opts, :adapter, Altar.AI.Adapters.Composite.default())
text_path = Keyword.get(opts, :text_path, [:data, :text])
summary_key = Keyword.get(opts, :summary_key, :summary)
max_length = Keyword.get(opts, :max_length)
text = get_in(signal, text_path)
if is_nil(text) do
Logger.warning("No text found at path #{inspect(text_path)} in signal")
{:continue, signal}
else
prompt = build_summary_prompt(text, max_length)
case Altar.AI.generate(adapter, prompt) do
{:ok, response} ->
updated_signal =
signal
|> put_in([summary_key], response.content)
|> Map.update(:metadata, %{}, fn meta ->
Map.merge(meta, %{
summarized_at: DateTime.utc_now(),
summary_tokens: response.tokens.total
})
end)
{:continue, updated_signal}
{:error, error} ->
Logger.error("Summary generation failed: #{inspect(error)}")
{:continue, Map.put(signal, :summary_error, error)}
end
end
end
defp build_summary_prompt(text, nil) do
"Provide a concise summary of the following text:\n\n#{text}"
end
defp build_summary_prompt(text, max_length) do
"Provide a concise summary (maximum #{max_length} words) of the following text:\n\n#{text}"
end
end