Files
bikoret/vav_pipeline/lib/vav_pipeline/talmudic_engine_socket.ex
T
Scott KarpandClaude Opus 4.6 c67672777b Step 14: Connect components — chamishim karsay zahav
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>
2026-03-12 17:39:21 -04:00

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