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>
173 lines
4.8 KiB
Elixir
173 lines
4.8 KiB
Elixir
defmodule VavPipeline.TalmudicEngineSocket do
|
|
@moduledoc """
|
|
Unix domain socket client to the SWI-Prolog Talmudic Engine.
|
|
|
|
Step 14 (Architecture §4): "Connect components: routes to
|
|
Prolog reasoning."
|
|
|
|
Construction spec §3.3: JSON over Unix domain socket.
|
|
Socket: /tmp/shorashim/talmudic.sock (dev)
|
|
/var/run/shorashim/talmudic.sock (production)
|
|
|
|
Connection model: connect, send one JSON line, read one JSON
|
|
line response, keep connection for subsequent requests.
|
|
Reconnects on connection loss.
|
|
"""
|
|
|
|
use GenServer
|
|
|
|
require Logger
|
|
|
|
@default_socket_path "/tmp/shorashim/talmudic.sock"
|
|
@call_timeout 10_000
|
|
@connect_timeout 5_000
|
|
|
|
# --- Client API ---
|
|
|
|
def start_link(opts \\ []) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Send an interpret request to the Talmudic Engine.
|
|
Construction spec §3.3 — action "interpret".
|
|
"""
|
|
@spec interpret(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
|
def interpret(case_ref, context) do
|
|
request = build_request("interpret", case_ref, context)
|
|
call_engine(request)
|
|
end
|
|
|
|
@doc """
|
|
Send a challenge request to the Talmudic Engine.
|
|
Construction spec §3.3 — action "challenge".
|
|
"""
|
|
@spec challenge(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
|
def challenge(case_ref, context) do
|
|
request = build_request("challenge", case_ref, context)
|
|
call_engine(request)
|
|
end
|
|
|
|
@doc """
|
|
Send a resolve request to the Talmudic Engine.
|
|
Construction spec §3.3 — action "resolve".
|
|
"""
|
|
@spec resolve(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
|
def resolve(case_ref, context) do
|
|
request = build_request("resolve", case_ref, context)
|
|
call_engine(request)
|
|
end
|
|
|
|
@doc """
|
|
Send a halt_check request to the Talmudic Engine.
|
|
"""
|
|
@spec halt_check(map()) :: {:ok, map()} | {:error, String.t()}
|
|
def halt_check(action) do
|
|
request = Jason.encode!(%{
|
|
"action" => "halt_check",
|
|
"check" => action
|
|
})
|
|
call_engine(request)
|
|
end
|
|
|
|
# --- GenServer callbacks ---
|
|
|
|
@impl true
|
|
def init(opts) do
|
|
socket_path = Keyword.get(opts, :socket_path, @default_socket_path)
|
|
{:ok, %{socket_path: socket_path, socket: nil}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:request, json_line}, _from, state) do
|
|
case ensure_connected(state) do
|
|
{:ok, state} ->
|
|
case send_and_receive(state.socket, json_line) do
|
|
{:ok, response} ->
|
|
{:reply, {:ok, response}, state}
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("TalmudicEngineSocket: send/receive failed: #{inspect(reason)}")
|
|
# Connection broken — close and retry once
|
|
:gen_tcp.close(state.socket)
|
|
state = %{state | socket: nil}
|
|
|
|
case ensure_connected(state) do
|
|
{:ok, state} ->
|
|
case send_and_receive(state.socket, json_line) do
|
|
{:ok, response} -> {:reply, {:ok, response}, state}
|
|
{:error, reason2} -> {:reply, {:error, "Retry failed: #{inspect(reason2)}"}, %{state | socket: nil}}
|
|
end
|
|
|
|
{:error, reason2} ->
|
|
{:reply, {:error, "Reconnect failed: #{inspect(reason2)}"}, state}
|
|
end
|
|
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:reply, {:error, "Cannot connect: #{inspect(reason)}"}, state}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def terminate(_reason, state) do
|
|
if state.socket do
|
|
:gen_tcp.close(state.socket)
|
|
end
|
|
:ok
|
|
end
|
|
|
|
# --- Private ---
|
|
|
|
defp ensure_connected(%{socket: nil, socket_path: path} = state) do
|
|
case :gen_tcp.connect({:local, path}, 0, [:binary, active: false, packet: :line], @connect_timeout) do
|
|
{:ok, socket} ->
|
|
{:ok, %{state | socket: socket}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp ensure_connected(state), do: {:ok, state}
|
|
|
|
defp send_and_receive(socket, json_line) do
|
|
case :gen_tcp.send(socket, json_line <> "\n") do
|
|
:ok ->
|
|
case :gen_tcp.recv(socket, 0, @call_timeout) do
|
|
{:ok, data} ->
|
|
line = String.trim_trailing(data, "\n")
|
|
case Jason.decode(line) do
|
|
{:ok, parsed} -> {:ok, parsed}
|
|
{:error, err} -> {:error, "JSON decode: #{inspect(err)}"}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp build_request(action, case_ref, context) do
|
|
roots = Map.get(context, :context_roots, [])
|
|
question = Map.get(context, :question, "")
|
|
|
|
Jason.encode!(%{
|
|
"action" => action,
|
|
"case" => %{
|
|
"ref" => case_ref,
|
|
"question" => question,
|
|
"context_roots" => roots
|
|
}
|
|
})
|
|
end
|
|
|
|
defp call_engine(json_line) do
|
|
GenServer.call(__MODULE__, {:request, json_line}, @call_timeout)
|
|
end
|
|
end
|