Three connections built and tested: - Haskell Shoresh Engine via Erlang Port (JSON lines over stdin/stdout) - Prolog Talmudic Engine via Unix domain socket (JSON over /tmp/shorashim/talmudic.sock) - Erlang Sanhedrin via native BEAM calls New: Server.hs (JSON-line server loop), json_bridge.pl + socket_server.pl (Prolog socket interface), ShoreshEnginePort + TalmudicEngineSocket + SanhedrinClient (Elixir GenServer wrappers). BereishitCycle now routes unresolved cases through Talmudic Engine and Sanhedrin. All connections gracefully degrade — pipeline falls back to L1 data when engines unavailable. 46 Elixir tests, 33 Haskell tests, 57 Prolog tests — 0 failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
4.3 KiB
Elixir
142 lines
4.3 KiB
Elixir
defmodule VavPipeline.SanhedrinClient do
|
|
@moduledoc """
|
|
Client interface to the Erlang Sanhedrin deliberation engine.
|
|
|
|
Construction spec §3.2: Native BEAM message passing.
|
|
Protocol: Erlang records via escalation:submit_case/1.
|
|
|
|
The Sanhedrin runs in the same BEAM VM. This module translates
|
|
Elixir maps into Erlang record tuples and calls the Sanhedrin
|
|
API directly.
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Submit a case for deliberation.
|
|
|
|
Construction spec §3.2: submit_case with case_ref, complexity,
|
|
question, context, requested_by.
|
|
|
|
Returns {:ok, decision_map} or {:error, reason}.
|
|
"""
|
|
@spec submit_case(map()) :: {:ok, map()} | {:error, String.t()}
|
|
def submit_case(case_params) do
|
|
case_ref = Map.get(case_params, :case_ref, generate_ref())
|
|
complexity = Map.get(case_params, :complexity, :simple)
|
|
question = Map.get(case_params, :question, "")
|
|
context = Map.get(case_params, :context, %{})
|
|
requested_by = Map.get(case_params, :requested_by, "pipeline")
|
|
|
|
# Build Erlang record tuple matching #sanhedrin_case{}
|
|
# Record: {sanhedrin_case, case_ref, complexity, question, context,
|
|
# requested_by, submitted_at}
|
|
erlang_case =
|
|
{:sanhedrin_case, case_ref, complexity, question, context,
|
|
requested_by, :erlang.system_time(:millisecond)}
|
|
|
|
try do
|
|
case :escalation.submit_case(erlang_case) do
|
|
{:ok, decision_ref} ->
|
|
# Decision is stored asynchronously by the panel.
|
|
# Wait briefly for the result, then fetch from clerk.
|
|
wait_for_decision(decision_ref, case_ref)
|
|
|
|
{:error, {:halted, halt_type, reason}} ->
|
|
{:error, "Halted (#{halt_type}): #{reason}"}
|
|
|
|
{:error, reason} ->
|
|
{:error, "Submission failed: #{inspect(reason)}"}
|
|
end
|
|
catch
|
|
:exit, {:noproc, _} ->
|
|
Logger.warning("SanhedrinClient: Sanhedrin not running, returning teiku")
|
|
{:ok, stub_teiku(case_ref)}
|
|
|
|
kind, reason ->
|
|
Logger.error("SanhedrinClient: #{kind} — #{inspect(reason)}")
|
|
{:error, "Sanhedrin call failed: #{inspect(reason)}"}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Get a previously stored decision from the clerk.
|
|
"""
|
|
@spec get_decision(binary()) :: {:ok, map()} | {:error, :not_found}
|
|
def get_decision(case_ref) do
|
|
try do
|
|
case :clerk.get_decision(case_ref) do
|
|
{:ok, decision} ->
|
|
{:ok, decision_to_map(decision)}
|
|
|
|
:not_found ->
|
|
{:error, :not_found}
|
|
end
|
|
catch
|
|
:exit, {:noproc, _} ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
# --- Private ---
|
|
|
|
defp wait_for_decision(decision_ref, case_ref) do
|
|
# The panel deliberates asynchronously. Poll clerk briefly.
|
|
wait_for_decision(decision_ref, case_ref, 10, 500)
|
|
end
|
|
|
|
defp wait_for_decision(_decision_ref, case_ref, 0, _interval) do
|
|
# Timeout — return teiku (unresolved)
|
|
Logger.warning("SanhedrinClient: deliberation timeout for #{case_ref}")
|
|
{:ok, stub_teiku(case_ref)}
|
|
end
|
|
|
|
defp wait_for_decision(decision_ref, case_ref, retries, interval) do
|
|
Process.sleep(interval)
|
|
|
|
case get_decision(case_ref) do
|
|
{:ok, decision} ->
|
|
{:ok, decision}
|
|
|
|
{:error, :not_found} ->
|
|
wait_for_decision(decision_ref, case_ref, retries - 1, interval)
|
|
end
|
|
end
|
|
|
|
defp decision_to_map({:decision, case_ref, outcome, majority, minority, panel_size,
|
|
delib_time, unanimous_check}) do
|
|
%{
|
|
case_ref: case_ref,
|
|
outcome: outcome,
|
|
majority: majority,
|
|
minority: minority,
|
|
panel_size: panel_size,
|
|
deliberation_time_ms: delib_time,
|
|
unanimous_check: unanimous_check
|
|
}
|
|
end
|
|
|
|
defp decision_to_map(other), do: other
|
|
|
|
defp stub_teiku(case_ref) do
|
|
%{
|
|
case_ref: case_ref,
|
|
outcome: :teiku,
|
|
majority: %{position: "Sanhedrin not available", reasoning: "Engine not running", torah_refs: [], vote_count: 0},
|
|
minority: %{position: "", reasoning: "", torah_refs: [], vote_count: 0},
|
|
panel_size: 0,
|
|
deliberation_time_ms: 0,
|
|
unanimous_check: :passed
|
|
}
|
|
end
|
|
|
|
defp generate_ref do
|
|
:crypto.strong_rand_bytes(16)
|
|
|> Base.encode16(case: :lower)
|
|
|> then(fn hex ->
|
|
<<a::binary-size(8), b::binary-size(4), c::binary-size(4), d::binary-size(4), e::binary-size(12)>> = hex
|
|
"#{a}-#{b}-#{c}-#{d}-#{e}"
|
|
end)
|
|
end
|
|
end
|