langchain-prolog 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ """
2
+ langchain_prolog - A LangChain integration for SWI-Prolog
3
+
4
+ This module provides a bridge between LangChain and SWI-Prolog, allowing seamless
5
+ integration of Prolog's logic programming capabilities into LangChain pipelines.
6
+
7
+ Key Components:
8
+ - PrologConfig: Configuration class for Prolog settings
9
+ - PrologRunnable: Main class for executing Prolog queries
10
+ - PrologTool: Utility class for integrating Prolog queries into LangChain tools
11
+ - PrologResult: Type representing possible Prolog query results
12
+ - PrologInput: Type representing valid input formats
13
+ - PrologRuntimeError: Exception class for Prolog execution errors
14
+
15
+ Requirements:
16
+ - Python 3.9 or higher
17
+ - LangChain 0.3.0 or higher
18
+ - Pydantic 2.0 or higher
19
+ - SWI-Prolog must be installed and accessible in the system path
20
+ - On macOS, requires Homebrew installation of SWI-Prolog
21
+ - The janus_swipl package must be installed
22
+ """
23
+
24
+ from pydantic import ValidationError
25
+
26
+ from ._prolog_init import initialize_prolog
27
+ from .exceptions import (
28
+ PrologFileNotFoundError,
29
+ PrologInitializationError,
30
+ PrologRuntimeError,
31
+ PrologToolException,
32
+ PrologValueError,
33
+ )
34
+ from .runnable import (
35
+ PrologConfig,
36
+ PrologInput,
37
+ PrologResult,
38
+ PrologRunnable,
39
+ )
40
+ from .tool import PrologTool
41
+ from .__version__ import __version__
42
+
43
+
44
+ __all__ = [
45
+ "PrologConfig",
46
+ "PrologInput",
47
+ "PrologRunnable",
48
+ "PrologRuntimeError",
49
+ "PrologFileNotFoundError",
50
+ "PrologInitializationError",
51
+ "PrologToolException",
52
+ "PrologValueError",
53
+ "PrologResult",
54
+ "PrologTool",
55
+ "ValidationError",
56
+ ]
57
+
58
+ # Initialize Prolog immediately when this module is imported
59
+ initialize_prolog()
@@ -0,0 +1,3 @@
1
+ VERSION = (0, 1, 0) # Major, Minor, Patch
2
+
3
+ __version__ = ".".join(map(str, VERSION))
@@ -0,0 +1,205 @@
1
+ """
2
+ SWI-Prolog initialization script for Python.
3
+ Set the following environment variables if using non-standard installation paths:
4
+
5
+ For macOS
6
+ export SWIPL_HOME_DIR=/path/to/swipl/lib/swipl
7
+ export SWIPL_LIB_DIR=/path/to/swipl/lib/arm64-darwin
8
+ export SWIPL_BASE_DIR=/path/to/swipl
9
+
10
+ # For Linux
11
+ export SWIPL_HOME_DIR=/path/to/swi-prolog
12
+ export SWIPL_LIB_DIR=/path/to/swi-prolog/lib/x86_64-linux
13
+
14
+ # For Windows (in PowerShell)
15
+ $env:SWIPL_HOME_DIR="C:\path\to\swipl"
16
+ $env:SWIPL_LIB_DIR="C:\path\to\swipl\bin"
17
+ """
18
+
19
+ import ctypes
20
+ import os
21
+ import platform
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ from .exceptions import PrologInitializationError
26
+ from .logger import logger
27
+
28
+
29
+ def get_env_paths() -> tuple[Optional[Path], Optional[Path], Optional[Path]]:
30
+ """Get paths from environment variables."""
31
+ swipl_home = os.environ.get("SWIPL_HOME_DIR")
32
+ swipl_lib = os.environ.get("SWIPL_LIB_DIR")
33
+ swipl_base = os.environ.get("SWIPL_BASE_DIR")
34
+
35
+ return (
36
+ Path(swipl_home) if swipl_home else None,
37
+ Path(swipl_lib) if swipl_lib else None,
38
+ Path(swipl_base) if swipl_base else None,
39
+ )
40
+
41
+
42
+ def initialize_macos() -> None:
43
+ """Initialize SWI-Prolog environment for macOS."""
44
+ # Try default Homebrew paths first
45
+ homebrew_base = Path("/opt/homebrew/Cellar/swi-prolog")
46
+ versions = [x for x in homebrew_base.iterdir() if x.is_dir()]
47
+ if versions:
48
+ swipl_base = max(versions) # Latest version
49
+ arch = platform.machine()
50
+ swipl_lib = swipl_base / "lib" / "swipl" / "lib" / f"{arch}-darwin"
51
+ swipl_home = swipl_base / "lib" / "swipl"
52
+ else:
53
+ raise PrologInitializationError("No SWI-Prolog versions found in Homebrew")
54
+
55
+ # If default paths don't exist, try environment variables
56
+ if not swipl_lib.exists() or not swipl_home.exists():
57
+ env_home, env_lib, env_base = get_env_paths()
58
+ if env_lib and env_home and env_base:
59
+ swipl_lib = env_lib
60
+ swipl_home = env_home
61
+ swipl_base = env_base
62
+ else:
63
+ raise PrologInitializationError(
64
+ "SWI-Prolog libraries not found. Please set SWIPL_LIB_DIR, SWIPL_HOME_DIR and SWIPL_BASE_DIR"
65
+ )
66
+
67
+ # Create Frameworks directory
68
+ frameworks_dir = swipl_base / "lib" / "Frameworks"
69
+ frameworks_dir.mkdir(parents=True, exist_ok=True)
70
+
71
+ # Create symbolic links
72
+ try:
73
+ for lib in swipl_lib.glob("libswipl*"):
74
+ target = frameworks_dir / lib.name
75
+ if not target.exists():
76
+ target.symlink_to(lib)
77
+ except Exception as e:
78
+ logger.warning(f"Could not create symbolic links: {e}")
79
+
80
+ # Set environment variables
81
+ os.environ["DYLD_LIBRARY_PATH"] = f"{swipl_lib}:{frameworks_dir}"
82
+ os.environ["SWIPL_HOME_DIR"] = str(swipl_home)
83
+
84
+ # Update system path
85
+ paths = [str(swipl_lib), str(frameworks_dir)]
86
+ for path in paths:
87
+ if path not in os.environ["PATH"]:
88
+ logger.info(f"Adding {path} to PATH")
89
+ os.environ["PATH"] = f"{path}:{os.environ['PATH']}"
90
+
91
+ # Load libraries
92
+ try:
93
+ ctypes.CDLL("/opt/homebrew/opt/zlib/lib/libz.1.dylib")
94
+ ctypes.CDLL(str(swipl_lib / "libswipl.dylib"), mode=ctypes.RTLD_GLOBAL)
95
+ except Exception as e:
96
+ logger.warning(f"Could not preload libraries: {e}")
97
+
98
+
99
+ def initialize_linux() -> None:
100
+ """Initialize SWI-Prolog environment for Linux."""
101
+ # Try standard Linux paths
102
+ standard_paths = [
103
+ Path("/usr/lib/swi-prolog"),
104
+ Path("/usr/local/lib/swi-prolog"),
105
+ ]
106
+
107
+ swipl_lib = None
108
+ swipl_home = None
109
+
110
+ # Check standard paths
111
+ for path in standard_paths:
112
+ if path.exists():
113
+ swipl_home = path
114
+ swipl_lib = path / "lib" / (platform.machine() + "-linux")
115
+ if swipl_lib.exists():
116
+ break
117
+
118
+ # If standard paths don't work, try environment variables
119
+ if not swipl_lib or not swipl_home:
120
+ env_home, env_lib, _ = get_env_paths()
121
+ if env_lib and env_home:
122
+ swipl_lib = env_lib
123
+ swipl_home = env_home
124
+ else:
125
+ raise PrologInitializationError(
126
+ "SWI-Prolog libraries not found. Please set SWIPL_LIB_DIR and SWIPL_HOME_DIR"
127
+ )
128
+
129
+ # Set environment variables
130
+ os.environ["LD_LIBRARY_PATH"] = f"{swipl_lib}:{os.environ.get('LD_LIBRARY_PATH', '')}"
131
+ os.environ["SWIPL_HOME_DIR"] = str(swipl_home)
132
+
133
+ # Load library
134
+ try:
135
+ ctypes.CDLL(str(swipl_lib / "libswipl.so"), mode=ctypes.RTLD_GLOBAL)
136
+ except Exception as e:
137
+ logger.warning(f"Could not load SWI-Prolog library: {e}")
138
+
139
+
140
+ def initialize_windows() -> None:
141
+ """Initialize SWI-Prolog environment for Windows."""
142
+ # Try standard Windows paths
143
+ standard_paths = [
144
+ Path("C:/Program Files/swipl"),
145
+ Path("C:/Program Files (x86)/swipl"),
146
+ ]
147
+
148
+ swipl_home = None
149
+ swipl_lib = None
150
+
151
+ # Check standard paths
152
+ for path in standard_paths:
153
+ if path.exists():
154
+ swipl_home = path
155
+ swipl_lib = path / "bin"
156
+ if swipl_lib.exists():
157
+ break
158
+
159
+ # If standard paths don't work, try environment variables
160
+ if not swipl_lib or not swipl_home:
161
+ env_home, env_lib, _ = get_env_paths()
162
+ if env_lib and env_home:
163
+ swipl_lib = env_lib
164
+ swipl_home = env_home
165
+ else:
166
+ raise PrologInitializationError(
167
+ "SWI-Prolog libraries not found. Please set SWIPL_LIB_DIR and SWIPL_HOME_DIR"
168
+ )
169
+
170
+ # Set environment variables
171
+ os.environ["PATH"] = f"{swipl_lib};{os.environ['PATH']}"
172
+ os.environ["SWIPL_HOME_DIR"] = str(swipl_home)
173
+
174
+ # Load library
175
+ try:
176
+ ctypes.CDLL(str(swipl_lib / "libswipl.dll"), mode=ctypes.RTLD_GLOBAL)
177
+ except Exception as e:
178
+ logger.warning(f"Could not load SWI-Prolog library: {e}")
179
+
180
+
181
+ def initialize_prolog() -> None:
182
+ """Initialize SWI-Prolog environment based on operating system."""
183
+ system = platform.system().lower()
184
+
185
+ try:
186
+ if system == "darwin":
187
+ initialize_macos()
188
+ elif system == "linux":
189
+ initialize_linux()
190
+ elif system == "windows":
191
+ initialize_windows()
192
+ else:
193
+ raise PrologInitializationError(f"Unsupported operating system: {system}")
194
+
195
+ logger.info(f"SWI-Prolog initialized for {system}")
196
+ logger.info(f"SWIPL_HOME_DIR: {os.environ.get('SWIPL_HOME_DIR')}")
197
+ if system == "darwin":
198
+ logger.info(f"DYLD_LIBRARY_PATH: {os.environ.get('DYLD_LIBRARY_PATH')}")
199
+ elif system == "linux":
200
+ logger.info(f"LD_LIBRARY_PATH: {os.environ.get('LD_LIBRARY_PATH')}")
201
+ elif system == "windows":
202
+ logger.info(f"PATH: {os.environ.get('PATH')}")
203
+
204
+ except Exception as e:
205
+ raise PrologInitializationError(f"Error initializing SWI-Prolog: {e}")
@@ -0,0 +1,47 @@
1
+ % Consult a file and throw an exception if there are any errors.
2
+ % Thanks to Jan Wielemaker for this code.
3
+ % https://swi-prolog.discourse.group/uploads/short-url/9HaBWEyjPwod9QzimzPZ1OxzHIu.pl
4
+
5
+ :- module(consult_ex,
6
+ [ consult_ex/1
7
+ ]).
8
+
9
+ :- meta_predicate
10
+ consult_ex(:).
11
+
12
+ consult_ex(M:Spec) :-
13
+ nb_setval(consult_errors, 0),
14
+ absolute_file_name(Spec, FullFile,
15
+ [ file_type(prolog),
16
+ access(read)
17
+ ]),
18
+ setup_call_cleanup(
19
+ asserta((user:thread_message_hook(_Term,error,_Lines) :-
20
+ consult_ex:track_messages(FullFile)), Ref),
21
+ consult(M:FullFile),
22
+ erase(Ref)),
23
+ nb_getval(consult_errors, N),
24
+ ( N =:= 0
25
+ -> true
26
+ ; unload_file(FullFile),
27
+ throw(error(consult_error(FullFile),_))
28
+ ).
29
+
30
+ :- public
31
+ track_messages/1.
32
+
33
+ track_messages(FullFile) :-
34
+ source_location(FullFile, _Line),
35
+ nb_getval(consult_errors, N0),
36
+ N is N0+1,
37
+ nb_setval(consult_errors, N),
38
+ fail. % print normally
39
+
40
+ /*******************************
41
+ * MESSAGES *
42
+ *******************************/
43
+
44
+ :- multifile prolog:error_message//1.
45
+
46
+ prolog:error_message(consult_error(File)) -->
47
+ [ 'Could not load file ~p due to errors'-[File] ].
@@ -0,0 +1,61 @@
1
+ from typing import Any
2
+
3
+ from langchain_core.tools import ToolException
4
+
5
+ from .logger import logger
6
+
7
+
8
+ class LangChainPrologException(Exception):
9
+ """Base exception class for langchain-prolog with automatic logging."""
10
+
11
+ def __init__(self, message: str, *args: Any, **kwargs: Any) -> None:
12
+ """Initialize exception and log it.
13
+
14
+ Args:
15
+ message (str): Error message
16
+ *args: Additional positional arguments for Exception
17
+ **kwargs: Additional keyword arguments. Special keys:
18
+ - log_level: Logging level (default: error)
19
+ - exc_info: Exception info to include in log
20
+ """
21
+ super().__init__(message, *args)
22
+
23
+ # Extract logging parameters
24
+ log_level = kwargs.pop("log_level", "error")
25
+ exc_info = kwargs.pop("exc_info", None)
26
+
27
+ # Log the exception
28
+ log_func = getattr(logger, log_level)
29
+ log_func(message, exc_info=exc_info)
30
+
31
+
32
+ class PrologRuntimeError(LangChainPrologException):
33
+ """Raised when a Prolog execution error occurs."""
34
+
35
+ pass
36
+
37
+
38
+ class PrologInitializationError(LangChainPrologException):
39
+ """Raised when Prolog initialization fails."""
40
+
41
+ pass
42
+
43
+
44
+ class PrologValueError(LangChainPrologException):
45
+ """Raised when a value error occurs."""
46
+
47
+ pass
48
+
49
+
50
+ class PrologFileNotFoundError(LangChainPrologException):
51
+ """Raised when a file is not found."""
52
+
53
+ pass
54
+
55
+
56
+ class PrologToolException(LangChainPrologException):
57
+ """Raised when a Prolog tool execution error occurs."""
58
+
59
+ def __init__(self, message: str, *args: Any, **kwargs: Any) -> None:
60
+ super().__init__(message, *args, **kwargs)
61
+ raise ToolException(message, *args)
@@ -0,0 +1,102 @@
1
+ """Logging configuration for the langchain-prolog library."""
2
+
3
+ import logging
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import (
7
+ Any,
8
+ Callable,
9
+ Union,
10
+ )
11
+
12
+
13
+ # Define custom log levels
14
+ TRACE = 5
15
+ logging.addLevelName(TRACE, "TRACE")
16
+
17
+
18
+ class LangChainPrologLogger:
19
+ """Logger for the langchain-prolog package."""
20
+
21
+ def __init__(self, name: str = "langchain_prolog"):
22
+ """Initialize logger with default configuration.
23
+
24
+ Args:
25
+ name (str): Logger name, defaults to 'langchain_prolog'
26
+ """
27
+ self.logger = logging.getLogger(name)
28
+ self._configure_default_logger()
29
+
30
+ def _configure_default_logger(self) -> None:
31
+ """Configure default logging settings."""
32
+ if not self.logger.handlers:
33
+ self.logger.setLevel(logging.WARNING)
34
+
35
+ # Console handler
36
+ console_handler = logging.StreamHandler(sys.stdout)
37
+ console_handler.setLevel(logging.WARNING)
38
+
39
+ # Format
40
+ formatter = logging.Formatter(
41
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
42
+ )
43
+ console_handler.setFormatter(formatter)
44
+
45
+ self.logger.addHandler(console_handler)
46
+
47
+ def setup_file_logging(self, log_file: Union[str, Path] = "langchain_prolog.log") -> None:
48
+ """Setup file logging.
49
+
50
+ Args:
51
+ log_file (str | Path): Path to log file. Defaults to 'langchain_prolog.log'.
52
+ """
53
+
54
+ file_handler = logging.FileHandler(log_file)
55
+ file_handler.setLevel(logging.INFO)
56
+ formatter = logging.Formatter(
57
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
58
+ )
59
+ file_handler.setFormatter(formatter)
60
+ self.logger.addHandler(file_handler)
61
+
62
+ def set_level(self, level: int) -> None:
63
+ """Set logging level.
64
+
65
+ Args:
66
+ level (int): Logging level (e.g., logging.DEBUG, logging.INFO)
67
+ """
68
+ self.logger.setLevel(level)
69
+
70
+ def trace(self, msg: str, *args: Any, **kwargs: Any) -> None:
71
+ """Log at TRACE level."""
72
+ self.logger.log(TRACE, msg, *args, **kwargs)
73
+
74
+ @property
75
+ def debug(self) -> Callable:
76
+ """Debug level logging."""
77
+ return self.logger.debug
78
+
79
+ @property
80
+ def info(self) -> Callable:
81
+ """Info level logging."""
82
+ return self.logger.info
83
+
84
+ @property
85
+ def warning(self) -> Callable:
86
+ """Warning level logging."""
87
+ return self.logger.warning
88
+
89
+ @property
90
+ def error(self) -> Callable:
91
+ """Error level logging."""
92
+ return self.logger.error
93
+
94
+ @property
95
+ def critical(self) -> Callable:
96
+ """Critical level logging."""
97
+ return self.logger.critical
98
+
99
+
100
+ # Create default logger instance
101
+ logger_setup = LangChainPrologLogger()
102
+ logger = logger_setup.logger