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>
This commit is contained in:
1 parent
f8f70c77e5
commit
c67672777b
18 files changed
+1435
-121
No files matched your search
@@ -0,0 +1,194 @@
|
||||
%% json_bridge.pl — JSON ↔ Prolog term conversion for Talmudic Engine.
|
||||
%%
|
||||
%% Construction §3.3: Bidirectional conversion between the JSON schema
|
||||
%% defined in the spec and internal Prolog terms used by api.pl.
|
||||
%%
|
||||
%% Inbound: JSON dict → Prolog request term
|
||||
%% Outbound: Prolog response term → JSON dict
|
||||
|
||||
:- module(json_bridge, [
|
||||
json_to_request/2,
|
||||
response_to_json/2,
|
||||
parse_root_name/2
|
||||
]).
|
||||
|
||||
:- use_module(library(json)).
|
||||
:- use_module(library(lists)).
|
||||
|
||||
%% ============================================================
|
||||
%% INBOUND: JSON → Prolog request
|
||||
%% ============================================================
|
||||
|
||||
%% Convert a JSON dict (as parsed by SWI-Prolog's json library) to a
|
||||
%% Prolog request term compatible with api:process_request/2.
|
||||
%%
|
||||
%% JSON format (Construction §3.3):
|
||||
%% { "action": "interpret", "case": { "ref": "case-uuid",
|
||||
%% "facts": [...], "question": "...", "context_roots": [...] } }
|
||||
%%
|
||||
%% Prolog format:
|
||||
%% request(interpret, CaseRef, Domain, Roots, Question)
|
||||
|
||||
json_to_request(Dict, request(Action, CaseRef, Domain, Roots, Question)) :-
|
||||
get_dict(action, Dict, ActionStr),
|
||||
atom_string(Action, ActionStr),
|
||||
get_dict(case, Dict, CaseDict),
|
||||
get_dict(ref, CaseDict, RefStr),
|
||||
atom_string(CaseRef, RefStr),
|
||||
%% Extract question
|
||||
( get_dict(question, CaseDict, QuestionStr)
|
||||
-> atom_string(Question, QuestionStr)
|
||||
; Question = none
|
||||
),
|
||||
%% Extract context_roots and derive domain
|
||||
( get_dict(context_roots, CaseDict, RootNames)
|
||||
-> maplist(parse_root_name, RootNames, Roots)
|
||||
; Roots = []
|
||||
),
|
||||
%% Derive domain from roots (first root's known domain, or 'general')
|
||||
( Roots = [FirstRoot|_],
|
||||
root_to_domain(FirstRoot, D)
|
||||
-> Domain = D
|
||||
; Domain = general
|
||||
).
|
||||
|
||||
%% Handle halt_check action (different format — no case, just action)
|
||||
json_to_request(Dict, request(halt_check, Action)) :-
|
||||
get_dict(action, Dict, "halt_check"),
|
||||
get_dict(check, Dict, CheckDict),
|
||||
get_dict(type, CheckDict, TypeStr),
|
||||
atom_string(Type, TypeStr),
|
||||
( get_dict(target, CheckDict, TargetStr)
|
||||
-> atom_string(Target, TargetStr)
|
||||
; Target = unknown
|
||||
),
|
||||
( get_dict(source, CheckDict, SourceStr)
|
||||
-> atom_string(Source, SourceStr)
|
||||
; Source = unknown
|
||||
),
|
||||
Action = action(Type, source(Source), target(Target)).
|
||||
|
||||
|
||||
%% ============================================================
|
||||
%% OUTBOUND: Prolog response → JSON
|
||||
%% ============================================================
|
||||
|
||||
%% Convert a Prolog response from api.pl to a JSON-serializable dict.
|
||||
%%
|
||||
%% Prolog format: response(ok, interpreted, result(...))
|
||||
%% JSON format (Construction §3.3):
|
||||
%% { "status": "resolved", "interpretation": { ... } }
|
||||
|
||||
%% Interpreted response
|
||||
response_to_json(response(ok, interpreted, result(ref(Ref), mitzvot(Mitzvot),
|
||||
verses(Verses), reading(Reading),
|
||||
confidence(Conf))),
|
||||
Dict) :-
|
||||
maplist(term_to_string, Mitzvot, MitzvotStrs),
|
||||
maplist(term_to_string, Verses, VerseStrs),
|
||||
term_to_string(Reading, ReadingStr),
|
||||
atom_string(Conf, ConfStr),
|
||||
atom_string(Ref, RefStr),
|
||||
Dict = json{status: "resolved",
|
||||
interpretation: json{text: ReadingStr,
|
||||
prolog_form: RefStr,
|
||||
grounding: MitzvotStrs,
|
||||
verses: VerseStrs,
|
||||
confidence: ConfStr},
|
||||
challenge: null}.
|
||||
|
||||
%% Challenged response
|
||||
response_to_json(response(ok, resolved_challenged,
|
||||
result(interpretation(Interp), challenge(Kushya))),
|
||||
Dict) :-
|
||||
term_to_string(Interp, InterpStr),
|
||||
term_to_string(Kushya, KushyaStr),
|
||||
Dict = json{status: "challenged",
|
||||
interpretation: json{text: InterpStr,
|
||||
prolog_form: InterpStr,
|
||||
grounding: [],
|
||||
confidence: "medium"},
|
||||
challenge: json{kushya: KushyaStr,
|
||||
contradicting: "",
|
||||
strength: "moderate"}}.
|
||||
|
||||
%% Teiku response
|
||||
response_to_json(response(ok, teiku, result(record(Record))), Dict) :-
|
||||
term_to_string(Record, RecordStr),
|
||||
Dict = json{status: "teiku",
|
||||
interpretation: json{text: RecordStr,
|
||||
prolog_form: RecordStr,
|
||||
grounding: [],
|
||||
confidence: "low"},
|
||||
challenge: null}.
|
||||
|
||||
%% No interpretation
|
||||
response_to_json(response(error, no_interpretation, CaseRef), Dict) :-
|
||||
atom_string(CaseRef, RefStr),
|
||||
Dict = json{status: "teiku",
|
||||
interpretation: json{text: "No interpretation found",
|
||||
prolog_form: RefStr,
|
||||
grounding: [],
|
||||
confidence: "low"},
|
||||
challenge: null}.
|
||||
|
||||
%% No resolution
|
||||
response_to_json(response(error, no_resolution, CaseRef), Dict) :-
|
||||
atom_string(CaseRef, RefStr),
|
||||
Dict = json{status: "teiku",
|
||||
interpretation: json{text: "No resolution found",
|
||||
prolog_form: RefStr,
|
||||
grounding: [],
|
||||
confidence: "low"},
|
||||
challenge: null}.
|
||||
|
||||
%% Halt response
|
||||
response_to_json(response(halt, violations(Halts), action(Action)), Dict) :-
|
||||
maplist(term_to_string, Halts, HaltStrs),
|
||||
term_to_string(Action, ActionStr),
|
||||
Dict = json{status: "halt",
|
||||
violations: HaltStrs,
|
||||
action: ActionStr}.
|
||||
|
||||
%% No halt
|
||||
response_to_json(response(ok, no_halt, action(Action)), Dict) :-
|
||||
term_to_string(Action, ActionStr),
|
||||
Dict = json{status: "ok",
|
||||
violations: [],
|
||||
action: ActionStr}.
|
||||
|
||||
%% Catch-all for unexpected responses
|
||||
response_to_json(Response, Dict) :-
|
||||
term_to_string(Response, Str),
|
||||
Dict = json{status: "error",
|
||||
error: json{code: "UNEXPECTED_RESPONSE",
|
||||
message: Str}}.
|
||||
|
||||
|
||||
%% ============================================================
|
||||
%% HELPERS
|
||||
%% ============================================================
|
||||
|
||||
%% Parse a root name like "bet-resh-aleph" into an atom bet_resh_aleph
|
||||
parse_root_name(Name, Atom) :-
|
||||
( string(Name) -> S = Name
|
||||
; atom_string(Name, S)
|
||||
),
|
||||
split_string(S, "-", "", Parts),
|
||||
atomic_list_concat(Parts, '_', Atom).
|
||||
|
||||
%% Convert any Prolog term to a string
|
||||
term_to_string(Term, Str) :-
|
||||
term_string(Term, Str).
|
||||
|
||||
%% Map known roots to domains (basic mapping; will be extended)
|
||||
root_to_domain(bet_resh_aleph, creation).
|
||||
root_to_domain(yod_tsade_resh, creation).
|
||||
root_to_domain(aleph_lamed_he, holiness).
|
||||
root_to_domain(qof_dalet_shin, holiness).
|
||||
root_to_domain(shin_pe_tet, judgment).
|
||||
root_to_domain(dalet_yod_nun, judgment).
|
||||
root_to_domain(chet_samekh_dalet, mercy).
|
||||
root_to_domain(resh_chet_mem, mercy).
|
||||
root_to_domain(aleph_vav_resh, light).
|
||||
root_to_domain(chet_shin_kaf, darkness).
|
||||
@@ -0,0 +1,145 @@
|
||||
%% socket_server.pl — Unix domain socket listener for Talmudic Engine.
|
||||
%%
|
||||
%% Construction §3.3: "JSON over Unix domain socket"
|
||||
%% Step 14 (Architecture §4): "Connect components"
|
||||
%%
|
||||
%% Listens on a Unix domain socket, accepts JSON requests,
|
||||
%% dispatches to api:process_request/2 via json_bridge,
|
||||
%% returns JSON responses.
|
||||
%%
|
||||
%% Protocol: One JSON object per line (same as Haskell server).
|
||||
%% Connection model: accept, handle one or more requests per
|
||||
%% connection, close when client disconnects.
|
||||
|
||||
:- module(socket_server, [
|
||||
serve/0,
|
||||
serve/1,
|
||||
stop_server/0
|
||||
]).
|
||||
|
||||
:- use_module(library(socket)).
|
||||
:- use_module(library(json)).
|
||||
:- use_module(library(readutil)).
|
||||
|
||||
:- use_module('../interface/api').
|
||||
:- use_module('../interface/json_bridge').
|
||||
|
||||
%% Default socket path — dev path; production uses /var/run/shorashim/talmudic.sock
|
||||
default_socket_path('/tmp/shorashim/talmudic.sock').
|
||||
|
||||
%% Dynamic flag to control server loop
|
||||
:- dynamic server_running/1.
|
||||
|
||||
%% ============================================================
|
||||
%% SERVER ENTRY POINTS
|
||||
%% ============================================================
|
||||
|
||||
%% serve/0 — Start server on default socket path
|
||||
serve :-
|
||||
default_socket_path(Path),
|
||||
serve(Path).
|
||||
|
||||
%% serve/1 — Start server on given socket path
|
||||
serve(Path) :-
|
||||
%% Clean up any stale socket file
|
||||
( exists_file(Path)
|
||||
-> delete_file(Path)
|
||||
; true
|
||||
),
|
||||
%% Ensure parent directory exists
|
||||
file_directory_name(Path, Dir),
|
||||
( exists_directory(Dir)
|
||||
-> true
|
||||
; make_directory_path(Dir)
|
||||
),
|
||||
%% Create Unix domain socket
|
||||
unix_domain_socket(ServerSocket),
|
||||
tcp_bind(ServerSocket, Path),
|
||||
tcp_listen(ServerSocket, 5),
|
||||
format('Talmudic Engine listening on ~w~n', [Path]),
|
||||
flush_output,
|
||||
%% Set running flag
|
||||
assert(server_running(true)),
|
||||
%% Accept loop
|
||||
accept_loop(ServerSocket, Path).
|
||||
|
||||
%% Stop the server gracefully
|
||||
stop_server :-
|
||||
retractall(server_running(_)).
|
||||
|
||||
%% ============================================================
|
||||
%% CONNECTION HANDLING
|
||||
%% ============================================================
|
||||
|
||||
%% Main accept loop — waits for connections
|
||||
accept_loop(ServerSocket, Path) :-
|
||||
( server_running(true)
|
||||
-> catch(
|
||||
( tcp_accept(ServerSocket, ClientSocket, _Peer),
|
||||
tcp_open_socket(ClientSocket, InStream, OutStream),
|
||||
%% Handle this client's requests
|
||||
catch(
|
||||
handle_connection(InStream, OutStream),
|
||||
Error,
|
||||
( format(user_error, 'Connection error: ~w~n', [Error]),
|
||||
flush_output(user_error)
|
||||
)
|
||||
),
|
||||
close(InStream, [force(true)]),
|
||||
close(OutStream, [force(true)]),
|
||||
accept_loop(ServerSocket, Path)
|
||||
),
|
||||
AcceptError,
|
||||
( format(user_error, 'Accept error: ~w~n', [AcceptError]),
|
||||
flush_output(user_error),
|
||||
%% Clean up
|
||||
tcp_close_socket(ServerSocket),
|
||||
( exists_file(Path) -> delete_file(Path) ; true ),
|
||||
fail
|
||||
)
|
||||
)
|
||||
; %% Server stopped — clean up
|
||||
tcp_close_socket(ServerSocket),
|
||||
( exists_file(Path) -> delete_file(Path) ; true ),
|
||||
format('Talmudic Engine stopped.~n'),
|
||||
flush_output
|
||||
).
|
||||
|
||||
%% Handle a single connection — read lines until EOF
|
||||
handle_connection(InStream, OutStream) :-
|
||||
read_line_to_string(InStream, Line),
|
||||
( Line == end_of_file
|
||||
-> true
|
||||
; process_json_line(Line, ResponseJson),
|
||||
%% Write JSON response on a single line
|
||||
json_write_dict(OutStream, ResponseJson),
|
||||
nl(OutStream),
|
||||
flush_output(OutStream),
|
||||
%% Continue reading from this connection
|
||||
handle_connection(InStream, OutStream)
|
||||
).
|
||||
|
||||
|
||||
%% ============================================================
|
||||
%% JSON REQUEST PROCESSING
|
||||
%% ============================================================
|
||||
|
||||
%% Process a JSON line string and produce a JSON response string
|
||||
process_json_line(Line, ResponseJson) :-
|
||||
catch(
|
||||
( %% Parse JSON string to dict
|
||||
atom_json_dict(Line, RequestDict, []),
|
||||
%% Convert JSON to Prolog request
|
||||
json_to_request(RequestDict, Request),
|
||||
%% Dispatch to API
|
||||
process_request(Request, Response),
|
||||
%% Convert response to JSON
|
||||
response_to_json(Response, ResponseJson)
|
||||
),
|
||||
Error,
|
||||
( term_string(Error, ErrorStr),
|
||||
ResponseJson = json{status: "error",
|
||||
error: json{code: "PROCESSING_ERROR",
|
||||
message: ErrorStr}}
|
||||
)
|
||||
).
|
||||
@@ -17,6 +17,8 @@
|
||||
:- use_module(halt/shfichut_damim).
|
||||
:- use_module(halt/gilui_arayot).
|
||||
:- use_module(interface/api).
|
||||
:- use_module(interface/json_bridge).
|
||||
:- use_module(interface/socket_server).
|
||||
|
||||
%% Load test files
|
||||
:- use_module(library(plunit)).
|
||||
|
||||
Reference in new issue
Block a user