Packages

A comprehensive Elixir object system with AI integration, hierarchical coordination, and meta-schema evolution capabilities

Current section

Files

Jump to
object lib object_dspy_bridge.ex
Raw

lib/object_dspy_bridge.ex

defmodule Object.DSPyBridge do
@moduledoc """
Bridge module integrating DSPy framework with AAOS objects for advanced reasoning capabilities.
Enables objects to use DSPy signatures and LM Studio inference for intelligent behavior.
"""
use GenServer
require Logger
@default_lm_client Object.DSPyBridge.MockLMStudio
# @openai_client Object.OpenAIClient
defstruct [
:object_id,
:lm_client,
:active_signatures,
:reasoning_cache,
:performance_metrics
]
@doc """
Starts a DSPy bridge process for the given object.
## Parameters
- `object_id`: ID of the object to create bridge for
## Returns
`{:ok, pid}` on successful startup
"""
def start_link(object_id) do
GenServer.start_link(__MODULE__, object_id, name: via_registry(object_id))
end
def via_registry(object_id) do
{:via, Registry, {Object.Registry, {:dspy_bridge, object_id}}}
end
def init(object_id) do
state = %__MODULE__{
object_id: object_id,
lm_client: nil,
active_signatures: %{},
reasoning_cache: :ets.new(:reasoning_cache, [:set, :private]),
performance_metrics: %{queries: 0, cache_hits: 0, avg_latency: 0.0}
}
{:ok, state, {:continue, :initialize_lm_client}}
end
def handle_continue(:initialize_lm_client, state) do
# Check configuration for LM provider
config = Application.get_env(:object, :dspy_bridge, [])
provider = config[:provider] || :mock
client = case provider do
:openai ->
# Start OpenAI client if not already running
case GenServer.whereis(Object.OpenAIClient) do
nil ->
{:ok, _pid} = Object.OpenAIClient.start_link(config[:openai] || [])
_pid ->
:ok
end
Object.OpenAIClient
:lmstudio ->
{:ok, client} = @default_lm_client.new(%{
base_url: config[:base_url] || "http://localhost:1234",
timeout: config[:timeout] || 30_000,
streaming: config[:streaming] || true
})
client
_ ->
# Default to mock client
{:ok, client} = @default_lm_client.new(%{})
client
end
Logger.info("DSPy bridge initialized for object #{state.object_id} with provider: #{provider}")
{:noreply, %{state | lm_client: client}}
end
@doc """
Executes reasoning using a registered DSPy signature.
## Parameters
- `object_id`: ID of the object
- `signature_name`: Name of the signature to use
- `inputs`: Input data for reasoning
- `options`: Optional parameters like max_tokens, temperature
## Returns
`{:ok, result}` with structured reasoning output or `{:error, reason}`
## Examples
iex> Object.DSPyBridge.reason_with_signature("obj1", :message_analysis, %{content: "Hello"})
{:ok, %{intent: "greeting", confidence: 0.9}}
"""
def reason_with_signature(object_id, signature_name, inputs, options \\ []) do
try do
GenServer.call(via_registry(object_id), {:reason, signature_name, inputs, options})
rescue
UndefinedFunctionError ->
# Registry not available, return fallback result
{:ok, create_fallback_result(signature_name, inputs)}
catch
:exit, {:noproc, _} ->
# DSPy bridge not started for this object, return a fallback result
{:ok, create_fallback_result(signature_name, inputs)}
:exit, {:shutdown, _} ->
# System shutting down, return fallback result
{:ok, create_fallback_result(signature_name, inputs)}
end
end
@doc """
Executes a DSPy signature directly with the provided specification.
## Parameters
- `object_id`: ID of the object
- `signature`: Signature specification to execute
## Returns
`{:ok, result}` with structured output or `{:error, reason}`
"""
def execute_signature(object_id, signature) do
try do
GenServer.call(via_registry(object_id), {:execute_signature, signature})
rescue
UndefinedFunctionError ->
{:ok, create_fallback_signature_result(signature)}
catch
:exit, {:noproc, _} ->
{:ok, create_fallback_signature_result(signature)}
:exit, {:shutdown, _} ->
{:ok, create_fallback_signature_result(signature)}
end
end
def execute_signature(signature, params) when is_map(signature) do
# Create a mock object ID for direct signature execution
object_id = "signature_exec_#{System.unique_integer([:positive])}"
execute_signature(object_id, Map.merge(signature, params))
end
@doc """
Registers a new DSPy signature for an object.
## Parameters
- `object_id`: ID of the object
- `signature_name`: Name for the signature
- `signature_spec`: Specification with description, inputs, outputs, instructions
## Returns
`:ok` on successful registration
"""
def register_signature(object_id, signature_name, signature_spec) do
GenServer.call(via_registry(object_id), {:register_signature, signature_name, signature_spec})
end
@doc """
Gets performance metrics for the DSPy bridge.
## Parameters
- `object_id`: ID of the object
## Returns
Map with query count, cache hits, and average latency
"""
def get_reasoning_metrics(object_id) do
GenServer.call(via_registry(object_id), :get_metrics)
end
def handle_call({:reason, signature_name, inputs, options}, _from, state) do
start_time = System.monotonic_time(:microsecond)
cache_key = {signature_name, inputs}
result = case :ets.lookup(state.reasoning_cache, cache_key) do
[{^cache_key, cached_result}] ->
new_metrics = update_metrics(state.performance_metrics, :cache_hit, 0)
{:reply, {:ok, cached_result}, %{state | performance_metrics: new_metrics}}
[] ->
case execute_reasoning(state, signature_name, inputs, options) do
{:ok, result} ->
:ets.insert(state.reasoning_cache, {cache_key, result})
latency = System.monotonic_time(:microsecond) - start_time
new_metrics = update_metrics(state.performance_metrics, :query, latency)
{:reply, {:ok, result}, %{state | performance_metrics: new_metrics}}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
result
end
def handle_call({:register_signature, signature_name, signature_spec}, _from, state) do
new_signatures = Map.put(state.active_signatures, signature_name, signature_spec)
{:reply, :ok, %{state | active_signatures: new_signatures}}
end
def handle_call({:execute_signature, _signature}, _from, state) do
# Simple stub implementation that returns a mock result
# In a real implementation, this would use the signature to generate an LLM response
result = %{
response_text: "Generated response based on the signature",
tone: "neutral",
intent: "informational",
confidence: 0.8,
suggests_follow_up: false,
model_info: "mock_model",
reasoning_steps: ["Step 1: Analyzed input", "Step 2: Generated response"]
}
{:reply, {:ok, result}, state}
end
def handle_call(:get_metrics, _from, state) do
{:reply, state.performance_metrics, state}
end
defp create_fallback_signature_result(signature) do
%{
response_text: "Fallback response for signature execution",
reasoning_steps: ["Analyzed signature requirements", "Generated structured response"],
confidence: 0.7,
model_info: "fallback_mock",
execution_time: 50,
signature_name: Map.get(signature, :name, "unknown"),
parameters: signature
}
end
defp create_fallback_result(signature_name, _inputs) do
# Create reasonable fallback results for common signatures used in OORL
case signature_name do
:behavior_adaptation ->
%{
behavior_adjustments: "maintain_current_strategy",
reasoning: "Insufficient context for adaptation",
expected_outcomes: %{stability: 0.8},
risk_assessment: "low"
}
:problem_solving ->
%{
solution_strategy: "gradient_descent_optimization",
confidence: 0.6,
convergence_prediction: 0.7,
resource_requirements: "medium",
implementation_plan: %{
parameter_changes: %{},
learning_rate: 1.0,
exploration_strategy: :unchanged
}
}
_ ->
%{
status: "fallback_executed",
confidence: 0.5,
reasoning: "DSPy bridge not available, using fallback logic"
}
end
end
defp execute_reasoning(state, signature_name, inputs, options) do
case Map.get(state.active_signatures, signature_name) do
nil ->
{:error, "Signature '#{signature_name}' not registered"}
signature_spec ->
case state.lm_client do
nil ->
{:error, "LM client not initialized"}
Object.OpenAIClient ->
# Use OpenAI client with DSPy signature execution
signature = %{
name: to_string(signature_name),
inputs: signature_spec.inputs || [],
outputs: signature_spec.outputs || [],
instructions: signature_spec.instructions || ""
}
case Object.OpenAIClient.execute_dspy_signature(signature, inputs) do
{:ok, result} -> {:ok, result}
error -> error
end
client ->
# Use legacy LM Studio client
prompt = build_dspy_prompt(signature_spec, inputs)
{:ok, response} = @default_lm_client.generate(client, %{
prompt: prompt,
max_tokens: Keyword.get(options, :max_tokens, 1000),
temperature: Keyword.get(options, :temperature, 0.7),
stream: false
})
parse_dspy_response(signature_spec, response)
end
end
end
defp build_dspy_prompt(signature_spec, inputs) do
"""
System: You are an AI assistant following the DSPy framework for structured reasoning.
Signature: #{signature_spec.description}
Input Fields:
#{Enum.map(signature_spec.inputs, fn {field, desc} -> "- #{field}: #{desc}" end) |> Enum.join("\n")}
Output Fields:
#{Enum.map(signature_spec.outputs, fn {field, desc} -> "- #{field}: #{desc}" end) |> Enum.join("\n")}
Instructions: #{signature_spec.instructions}
Input Data:
#{Enum.map(inputs, fn {key, value} -> "#{key}: #{value}" end) |> Enum.join("\n")}
Please provide your response in the following structured format:
#{Enum.map(signature_spec.outputs, fn {field, _} -> "#{field}: [your response for #{field}]" end) |> Enum.join("\n")}
"""
end
defp parse_dspy_response(signature_spec, response) do
try do
output_fields = Enum.map(signature_spec.outputs, fn {field, _} ->
Atom.to_string(field)
end)
parsed = output_fields
|> Enum.reduce(%{}, fn field, acc ->
case Regex.run(~r/#{field}:\s*(.+?)(?=\n\w+:|$)/s, response.content) do
[_, value] -> Map.put(acc, String.to_atom(field), String.trim(value))
nil -> acc
end
end)
{:ok, parsed}
rescue
error ->
{:error, "Failed to parse response: #{inspect(error)}"}
end
end
defp update_metrics(metrics, :cache_hit, _latency) do
%{metrics | cache_hits: metrics.cache_hits + 1}
end
defp update_metrics(metrics, :query, latency) do
new_queries = metrics.queries + 1
new_avg = (metrics.avg_latency * metrics.queries + latency) / new_queries
%{metrics |
queries: new_queries,
avg_latency: new_avg
}
end
defmodule MockLMStudio do
@moduledoc """
Mock LM Studio client for testing and development.
"""
def new(_config) do
{:ok, %{client_id: "mock_client", status: :connected}}
end
def generate(_client, %{prompt: prompt} = _params) do
# Simple mock response
response = %{
content: "Mock response to: #{String.slice(prompt, 0, 50)}...",
tokens_used: 42,
completion_time: 150
}
{:ok, response}
end
def generate(_client, _params) do
response = %{
content: "Mock response generated",
tokens_used: 30,
completion_time: 100
}
{:ok, response}
end
end
end