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
@@ -27,6 +27,7 @@ import Shoresh.Teamim
|
||||
import Shoresh.KetivQere
|
||||
import Shoresh.Lexicon
|
||||
import Shoresh.Graph
|
||||
import Shoresh.Server (runServer)
|
||||
import Luchot.Firmware
|
||||
|
||||
main :: IO ()
|
||||
@@ -34,6 +35,7 @@ main = do
|
||||
args <- getArgs
|
||||
case args of
|
||||
["verify"] -> runVerification
|
||||
["server"] -> runServer
|
||||
["verse", book, ch, vs] -> runVerse book ch vs
|
||||
["root", rootText] -> runRoot rootText
|
||||
["gematria", word] -> runGematria word
|
||||
@@ -45,6 +47,7 @@ printUsage = do
|
||||
putStrLn ""
|
||||
putStrLn "Usage:"
|
||||
putStrLn " shoresh-engine verify Run Step 7 verification"
|
||||
putStrLn " shoresh-engine server JSON-line server (Erlang Port mode)"
|
||||
putStrLn " shoresh-engine verse <book> <ch> <vs> Read and parse a verse from L1"
|
||||
putStrLn " shoresh-engine root <root> Activate a root through all 7 binyanim"
|
||||
putStrLn " shoresh-engine gematria <word> Compute gematria of a Hebrew word"
|
||||
|
||||
@@ -79,6 +79,7 @@ data L1Word = L1Word
|
||||
, l1wShoresh :: !(Maybe Text)
|
||||
} deriving (Show, Generic)
|
||||
|
||||
instance ToJSON L1Word
|
||||
instance FromJSON L1Word where
|
||||
parseJSON = withObject "L1Word" $ \v -> L1Word
|
||||
<$> v .: "position"
|
||||
@@ -100,6 +101,7 @@ data L1Verse = L1Verse
|
||||
, l1vWords :: ![L1Word]
|
||||
} deriving (Show, Generic)
|
||||
|
||||
instance ToJSON L1Verse
|
||||
instance FromJSON L1Verse where
|
||||
parseJSON = withObject "L1Verse" $ \v -> L1Verse
|
||||
<$> v .: "ref"
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
-- | Server — JSON-line port interface for pipeline integration.
|
||||
--
|
||||
-- Step 14 (Architecture §4): "Connect components: Elixir pipeline
|
||||
-- calls Haskell engine."
|
||||
--
|
||||
-- Construction spec §3.1: Protocol is Erlang Port (JSON).
|
||||
-- The engine reads one JSON line from stdin, processes it,
|
||||
-- writes one JSON line to stdout, and repeats.
|
||||
--
|
||||
-- Buffering is set to LineBuffering — critical for Erlang Port
|
||||
-- communication. Haskell defaults to block-buffering on non-TTY
|
||||
-- stdout, which would deadlock the port.
|
||||
module Shoresh.Server
|
||||
( runServer
|
||||
, handleRequest
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS8
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.ByteString.Lazy.Char8 as BLC
|
||||
import qualified Data.Map.Strict as Map
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Aeson
|
||||
import Data.Maybe (mapMaybe)
|
||||
import System.IO (hSetBuffering, stdin, stdout, stderr,
|
||||
BufferMode(..), hFlush, hIsEOF)
|
||||
import System.Directory (doesFileExist)
|
||||
import Control.Exception (catch, SomeException)
|
||||
|
||||
import Shoresh.Types
|
||||
import Shoresh.Gematria
|
||||
import Shoresh.Binyanim
|
||||
import Shoresh.Teamim
|
||||
import Shoresh.Lexicon
|
||||
|
||||
|
||||
-- | Run the JSON-line server loop.
|
||||
-- Called by Main.hs when invoked with "server" argument.
|
||||
runServer :: IO ()
|
||||
runServer = do
|
||||
-- Critical: set line buffering on stdin/stdout.
|
||||
-- Haskell defaults to block-buffering on non-TTY, which would
|
||||
-- deadlock the Erlang Port.
|
||||
hSetBuffering stdin LineBuffering
|
||||
hSetBuffering stdout LineBuffering
|
||||
hSetBuffering stderr LineBuffering
|
||||
serverLoop
|
||||
|
||||
|
||||
-- | Main server loop: read line, process, write line, repeat.
|
||||
serverLoop :: IO ()
|
||||
serverLoop = do
|
||||
eof <- hIsEOF stdin
|
||||
if eof
|
||||
then return ()
|
||||
else do
|
||||
line <- BL.fromStrict <$> BS8.getLine
|
||||
response <- processLine line
|
||||
BLC.putStrLn (encode response)
|
||||
hFlush stdout
|
||||
serverLoop
|
||||
|
||||
|
||||
-- | Process a single JSON line and return a response.
|
||||
processLine :: BL.ByteString -> IO Value
|
||||
processLine line = case eitherDecode line of
|
||||
Left err -> return $ errorResponse "PARSE_ERROR" (T.pack err)
|
||||
Right req -> handleRequest req
|
||||
`catch` \e -> return $ errorResponse "INTERNAL_ERROR"
|
||||
(T.pack $ show (e :: SomeException))
|
||||
|
||||
|
||||
-- | Handle a parsed request and produce a response.
|
||||
handleRequest :: Value -> IO Value
|
||||
handleRequest val = case parseOperation val of
|
||||
Nothing -> return $ errorResponse "INVALID_OPERATION" "Missing or invalid 'operation' field"
|
||||
Just "process" -> handleProcess val
|
||||
Just "separate" -> handleSeparate val
|
||||
Just "name" -> handleName val
|
||||
Just "evaluate" -> handleEvaluate val
|
||||
Just op -> return $ errorResponse "UNKNOWN_OPERATION" ("Unknown operation: " <> op)
|
||||
|
||||
|
||||
-- | Extract the operation string from a request.
|
||||
parseOperation :: Value -> Maybe Text
|
||||
parseOperation (Object obj) = case obj .: "operation" of
|
||||
Success t -> Just t
|
||||
_ -> Nothing
|
||||
where
|
||||
(.:) o k = case Map.lookup k (toMap o) of
|
||||
Just (String s) -> Success s
|
||||
_ -> Error "not found"
|
||||
toMap = \o -> case fromJSON (Object o) :: Result (Map.Map Text Value) of
|
||||
Success m -> m
|
||||
Error _ -> Map.empty
|
||||
parseOperation _ = Nothing
|
||||
|
||||
|
||||
-- | Handle "process" operation — root activation, te'amim parse, gematria.
|
||||
-- Construction spec §3.1: Takes text + scope, returns roots_activated,
|
||||
-- parse_tree, dual_readings.
|
||||
handleProcess :: Value -> IO Value
|
||||
handleProcess val = do
|
||||
let reqText = extractText val
|
||||
mscope = extractScope val
|
||||
|
||||
-- Parse roots from input text
|
||||
let roots = extractRoots reqText
|
||||
|
||||
-- Build activated roots with gematria and binyanim
|
||||
let activatedRoots = map activateRoot roots
|
||||
|
||||
-- If scope is provided, load verse from L1 and parse te'amim
|
||||
(parseTree, dualReadings) <- case mscope of
|
||||
Nothing -> return (Null, toJSON ([] :: [Value]))
|
||||
Just (book, ch, vs) -> do
|
||||
let path = "l1/torah/" ++ T.unpack (bookToText book)
|
||||
++ "/" ++ show ch ++ "/" ++ show vs ++ ".json"
|
||||
exists <- doesFileExist path
|
||||
if not exists
|
||||
then return (Null, toJSON ([] :: [Value]))
|
||||
else do
|
||||
mverse <- loadL1Verse path
|
||||
case mverse of
|
||||
Nothing -> return (Null, toJSON ([] :: [Value]))
|
||||
Just verse -> do
|
||||
let annotated = mapMaybe annotateL1Word (l1vWords verse)
|
||||
tree = readTeamim annotated
|
||||
ptJson = treeToJson tree
|
||||
drJson = toJSON ([] :: [Value]) -- Phase 3: extract ketiv/qere
|
||||
return (ptJson, drJson)
|
||||
|
||||
return $ object
|
||||
[ "status" .= ("ok" :: Text)
|
||||
, "result" .= object
|
||||
[ "roots_activated" .= activatedRoots
|
||||
, "parse_tree" .= parseTree
|
||||
, "dual_readings" .= dualReadings
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
-- | Handle "separate" operation — te'amim-based separation.
|
||||
-- Construction spec §3.1 operation "separate".
|
||||
handleSeparate :: Value -> IO Value
|
||||
handleSeparate _val = do
|
||||
-- Phase 2: returns stub separation
|
||||
-- Full implementation requires the annotated words from context
|
||||
return $ object
|
||||
[ "status" .= ("ok" :: Text)
|
||||
, "result" .= object
|
||||
[ "groups" .= ([] :: [Value])
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
-- | Handle "name" operation — assign names to categories.
|
||||
handleName :: Value -> IO Value
|
||||
handleName _val = do
|
||||
return $ object
|
||||
[ "status" .= ("ok" :: Text)
|
||||
, "result" .= object
|
||||
[ "name" .= ("unnamed" :: Text)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
-- | Handle "evaluate" operation — ki tov check.
|
||||
handleEvaluate :: Value -> IO Value
|
||||
handleEvaluate _val = do
|
||||
return $ object
|
||||
[ "status" .= ("ok" :: Text)
|
||||
, "result" .= object
|
||||
[ "evaluation" .= ("teiku" :: Text)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Helpers
|
||||
-- ============================================================
|
||||
|
||||
-- | Extract text from the input field of a request.
|
||||
extractText :: Value -> Text
|
||||
extractText (Object obj) = case Map.lookup "input" (toMap obj) of
|
||||
Just (Object inp) -> case Map.lookup "text" (toMap inp) of
|
||||
Just (String t) -> t
|
||||
_ -> ""
|
||||
_ -> ""
|
||||
where
|
||||
toMap o = case fromJSON (Object o) :: Result (Map.Map Text Value) of
|
||||
Success m -> m
|
||||
Error _ -> Map.empty
|
||||
extractText _ = ""
|
||||
|
||||
|
||||
-- | Extract scope (book, chapter, verse) from the input field.
|
||||
extractScope :: Value -> Maybe (Book, Int, Int)
|
||||
extractScope (Object obj) = do
|
||||
inp <- case Map.lookup "input" (toMap obj) of
|
||||
Just (Object i) -> Just (toMap i)
|
||||
_ -> Nothing
|
||||
scopeObj <- case Map.lookup "scope" inp of
|
||||
Just (Object s) -> Just (toMap s)
|
||||
_ -> Nothing
|
||||
bookVal <- Map.lookup "book" scopeObj
|
||||
bookText <- case bookVal of
|
||||
String s -> Just s
|
||||
_ -> Nothing
|
||||
book <- textToBook bookText
|
||||
ch <- case Map.lookup "chapter" scopeObj of
|
||||
Just (Number n) -> Just (floor n)
|
||||
_ -> Nothing
|
||||
vs <- case Map.lookup "verse" scopeObj of
|
||||
Just (Number n) -> Just (floor n)
|
||||
_ -> Nothing
|
||||
return (book, ch, vs)
|
||||
where
|
||||
toMap o = case fromJSON (Object o) :: Result (Map.Map Text Value) of
|
||||
Success m -> m
|
||||
Error _ -> Map.empty
|
||||
extractScope _ = Nothing
|
||||
|
||||
|
||||
-- | Extract Hebrew roots from input text.
|
||||
extractRoots :: Text -> [Shoresh]
|
||||
extractRoots t =
|
||||
let words' = T.words t
|
||||
-- Try each word as a potential root
|
||||
candidates = mapMaybe textToShoresh words'
|
||||
-- Also try the whole text as a single root
|
||||
wholeRoot = textToShoresh (T.filter (\c -> c >= '\x05D0' && c <= '\x05EA') t)
|
||||
in case candidates of
|
||||
[] -> case wholeRoot of
|
||||
Just r -> [r]
|
||||
Nothing -> []
|
||||
rs -> rs
|
||||
|
||||
|
||||
-- | Activate a root: expand through binyanim, compute gematria, find neighbors.
|
||||
activateRoot :: Shoresh -> Value
|
||||
activateRoot sh =
|
||||
let expanded = expand sh
|
||||
gv = gematriaRoot sh
|
||||
consonantNames = map consonantName [c1 sh, c2 sh, c3 sh]
|
||||
defaultBinyan = case Map.lookup Qal expanded of
|
||||
Just p -> T.pack (show (Shoresh.Types.binyan p))
|
||||
Nothing -> "Qal"
|
||||
in object
|
||||
[ "shoresh" .= consonantNames
|
||||
, "shoresh_hebrew" .= shoreshToText sh
|
||||
, "binyan" .= defaultBinyan
|
||||
, "domain" .= ("unknown" :: Text) -- Phase 3: domain lookup
|
||||
, "torah_refs" .= ([] :: [Value]) -- Phase 3: lexicon lookup
|
||||
, "gematria_value" .= gv
|
||||
, "gematria_neighbors" .= ([] :: [Value]) -- Phase 3: lexicon needed
|
||||
, "neighbors" .= ([] :: [Value]) -- Phase 3: lexicon needed
|
||||
]
|
||||
|
||||
|
||||
-- | Convert an L1 word to an AnnotatedWord for te'amim parsing.
|
||||
annotateL1Word :: L1Word -> Maybe AnnotatedWord
|
||||
annotateL1Word w = case l1wTeamim w of
|
||||
[] -> Nothing
|
||||
(t:_) -> case taamFromName t of
|
||||
Nothing -> Nothing
|
||||
Just taam -> Just AnnotatedWord
|
||||
{ awWord = l1wText w
|
||||
, awTaam = taam
|
||||
, awRole = taamRole taam
|
||||
}
|
||||
|
||||
|
||||
-- | Convert a ParsedVerse tree to JSON.
|
||||
treeToJson :: ParsedVerse -> Value
|
||||
treeToJson (Leaf w) = object
|
||||
[ "type" .= ("leaf" :: Text)
|
||||
, "word" .= awWord w
|
||||
, "taam" .= T.pack (show (awTaam w))
|
||||
]
|
||||
treeToJson (Branch t l r) = object
|
||||
[ "type" .= ("branch" :: Text)
|
||||
, "taam" .= T.pack (show t)
|
||||
, "left" .= treeToJson l
|
||||
, "right" .= treeToJson r
|
||||
]
|
||||
|
||||
|
||||
-- | Build an error response.
|
||||
errorResponse :: Text -> Text -> Value
|
||||
errorResponse code msg = object
|
||||
[ "status" .= ("error" :: Text)
|
||||
, "error" .= object
|
||||
[ "code" .= code
|
||||
, "message" .= msg
|
||||
]
|
||||
]
|
||||
@@ -49,7 +49,7 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Map.Strict (Map)
|
||||
import GHC.Generics (Generic)
|
||||
import Data.Aeson (FromJSON(..), ToJSON(..), withText)
|
||||
import Data.Aeson (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), withText)
|
||||
|
||||
-- | The 22 Hebrew consonants.
|
||||
-- These are the building blocks. No more, no less.
|
||||
@@ -145,6 +145,8 @@ data Binyan
|
||||
|
||||
instance ToJSON Binyan
|
||||
instance FromJSON Binyan
|
||||
instance ToJSONKey Binyan
|
||||
instance FromJSONKey Binyan
|
||||
|
||||
-- | All seven binyanim as a list.
|
||||
allBinyanim :: [Binyan]
|
||||
@@ -298,6 +300,9 @@ data ShoreshEntry = ShoreshEntry
|
||||
, seGlosses :: !(Map Binyan Text)
|
||||
} deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON ShoreshEntry
|
||||
instance FromJSON ShoreshEntry
|
||||
|
||||
|
||||
-- | Raw input to the processing pipeline.
|
||||
data Input = Input
|
||||
@@ -305,18 +310,27 @@ data Input = Input
|
||||
, inputSource :: !Text
|
||||
} deriving (Show, Generic)
|
||||
|
||||
instance ToJSON Input
|
||||
instance FromJSON Input
|
||||
|
||||
-- | A distinguished category after havdalah.
|
||||
data Category = Category
|
||||
{ catName :: !Text
|
||||
, catContent :: !Text
|
||||
} deriving (Show, Generic)
|
||||
|
||||
instance ToJSON Category
|
||||
instance FromJSON Category
|
||||
|
||||
-- | A processed result ready for evaluation.
|
||||
data ProcessedResult = ProcessedResult
|
||||
{ prCategories :: ![Category]
|
||||
, prRoots :: ![ShoreshEntry]
|
||||
} deriving (Show, Generic)
|
||||
|
||||
instance ToJSON ProcessedResult
|
||||
instance FromJSON ProcessedResult
|
||||
|
||||
-- | The evaluation outcome.
|
||||
-- Tov: fits the specification.
|
||||
-- LoTov: does not fit, with reason.
|
||||
@@ -326,3 +340,6 @@ data Evaluation
|
||||
| LoTov !Text
|
||||
| Teiku
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON Evaluation
|
||||
instance FromJSON Evaluation
|
||||
Reference in new issue
Block a user