vulcan-core 1.0.0__py3-none-any.whl → 1.1.1__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.
Potentially problematic release.
This version of vulcan-core might be problematic. Click here for more details.
- vulcan_core/ast_utils.py +25 -6
- vulcan_core/conditions.py +27 -7
- vulcan_core/engine.py +16 -6
- vulcan_core/models.py +12 -1
- vulcan_core/util.py +1 -1
- {vulcan_core-1.0.0.dist-info → vulcan_core-1.1.1.dist-info}/METADATA +7 -7
- vulcan_core-1.1.1.dist-info/RECORD +12 -0
- vulcan_core-1.0.0.dist-info/RECORD +0 -12
- {vulcan_core-1.0.0.dist-info → vulcan_core-1.1.1.dist-info}/LICENSE +0 -0
- {vulcan_core-1.0.0.dist-info → vulcan_core-1.1.1.dist-info}/NOTICE +0 -0
- {vulcan_core-1.0.0.dist-info → vulcan_core-1.1.1.dist-info}/WHEEL +0 -0
vulcan_core/ast_utils.py
CHANGED
|
@@ -24,7 +24,7 @@ class ContractError(Exception):
|
|
|
24
24
|
|
|
25
25
|
class ScopeAccessError(ContractError):
|
|
26
26
|
"""Raised when a callable attempts to access instances not passed as parameters or when decorated functions attempt
|
|
27
|
-
|
|
27
|
+
to access class attributes instead of parameter instance attributes."""
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class NotAFactError(ContractError):
|
|
@@ -152,16 +152,35 @@ class ASTProcessor[T: Callable]:
|
|
|
152
152
|
raise ASTProcessingError(msg)
|
|
153
153
|
|
|
154
154
|
# The source includes the entire line of code (e.g., assignment and condition() call)
|
|
155
|
-
# We need to
|
|
156
|
-
# nested parentheses in the lambda's body correctly
|
|
155
|
+
# We need to extract just the lambda expression, handling nested structures correctly
|
|
157
156
|
source = self.source[lambda_start:]
|
|
157
|
+
|
|
158
|
+
# Track depth of various brackets to ensure we don't split inside valid nested structures apart from trailing
|
|
159
|
+
# arguments within the condition() call
|
|
158
160
|
paren_level = 0
|
|
161
|
+
bracket_level = 0
|
|
162
|
+
brace_level = 0
|
|
163
|
+
|
|
159
164
|
for i, char in enumerate(source):
|
|
160
165
|
if char == "(":
|
|
161
166
|
paren_level += 1
|
|
162
|
-
elif char == ")"
|
|
163
|
-
paren_level
|
|
164
|
-
|
|
167
|
+
elif char == ")":
|
|
168
|
+
if paren_level > 0:
|
|
169
|
+
paren_level -= 1
|
|
170
|
+
elif paren_level == 0: # End of expression in a function call
|
|
171
|
+
return source[:i]
|
|
172
|
+
elif char == "[":
|
|
173
|
+
bracket_level += 1
|
|
174
|
+
elif char == "]":
|
|
175
|
+
if bracket_level > 0:
|
|
176
|
+
bracket_level -= 1
|
|
177
|
+
elif char == "{":
|
|
178
|
+
brace_level += 1
|
|
179
|
+
elif char == "}":
|
|
180
|
+
if brace_level > 0:
|
|
181
|
+
brace_level -= 1
|
|
182
|
+
# Only consider comma as a separator when not inside any brackets
|
|
183
|
+
elif char == "," and paren_level == 0 and bracket_level == 0 and brace_level == 0:
|
|
165
184
|
return source[:i]
|
|
166
185
|
|
|
167
186
|
return source
|
vulcan_core/conditions.py
CHANGED
|
@@ -8,11 +8,11 @@ import re
|
|
|
8
8
|
from abc import abstractmethod
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from enum import Enum, auto
|
|
11
|
+
from functools import lru_cache
|
|
11
12
|
from string import Formatter
|
|
12
13
|
from typing import TYPE_CHECKING
|
|
13
14
|
|
|
14
15
|
from langchain.prompts import ChatPromptTemplate
|
|
15
|
-
from langchain_openai import ChatOpenAI
|
|
16
16
|
from pydantic import BaseModel, Field
|
|
17
17
|
|
|
18
18
|
from vulcan_core.actions import ASTProcessor
|
|
@@ -22,11 +22,16 @@ if TYPE_CHECKING: # pragma: no cover - not used at runtime
|
|
|
22
22
|
from langchain_core.language_models import BaseChatModel
|
|
23
23
|
from langchain_core.runnables import RunnableSerializable
|
|
24
24
|
|
|
25
|
+
import importlib.util
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
25
30
|
|
|
26
31
|
@dataclass(frozen=True, slots=True)
|
|
27
32
|
class Expression(DeclaresFacts):
|
|
28
33
|
"""
|
|
29
|
-
Abstract base class for defining deferred logical expressions. It captures the
|
|
34
|
+
Abstract base class for defining deferred logical expressions. It captures the association of logic with Facts so
|
|
30
35
|
that upon a Fact update, the logical expression can be selectively evaluated. It also provides a set of logical
|
|
31
36
|
operators for combining conditions, resulting in a new CompoundCondition.
|
|
32
37
|
"""
|
|
@@ -58,7 +63,7 @@ class Expression(DeclaresFacts):
|
|
|
58
63
|
@dataclass(frozen=True, slots=True)
|
|
59
64
|
class Condition(FactHandler[ConditionCallable, bool], Expression):
|
|
60
65
|
"""
|
|
61
|
-
A Condition is a container to defer logical
|
|
66
|
+
A Condition is a container to defer logical expressions against a supplied Fact. The expression can be inverted
|
|
62
67
|
using the `~` operator. Conditions also support the '&', '|', and '^' operators for combinatorial logic.
|
|
63
68
|
|
|
64
69
|
Attributes:
|
|
@@ -182,6 +187,7 @@ class LiteralFormatter(Formatter):
|
|
|
182
187
|
@dataclass(frozen=True, slots=True)
|
|
183
188
|
class AICondition(Condition):
|
|
184
189
|
chain: RunnableSerializable
|
|
190
|
+
model: BaseChatModel
|
|
185
191
|
system_template: str
|
|
186
192
|
inquiry_template: str
|
|
187
193
|
func: None = field(init=False, default=None)
|
|
@@ -238,13 +244,23 @@ def ai_condition(model: BaseChatModel, inquiry: str) -> AICondition:
|
|
|
238
244
|
)
|
|
239
245
|
structured_model = model.with_structured_output(BooleanDecision)
|
|
240
246
|
chain = prompt_template | structured_model
|
|
241
|
-
return AICondition(chain=chain, system_template=system, inquiry_template=inquiry, facts=facts)
|
|
247
|
+
return AICondition(chain=chain, model=model, system_template=system, inquiry_template=inquiry, facts=facts)
|
|
242
248
|
|
|
243
249
|
|
|
244
|
-
|
|
250
|
+
@lru_cache(maxsize=1)
|
|
251
|
+
def _detect_default_model() -> BaseChatModel:
|
|
252
|
+
# TODO: Expand this to detect other providers
|
|
253
|
+
if importlib.util.find_spec("langchain_openai"):
|
|
254
|
+
from langchain_openai import ChatOpenAI
|
|
255
|
+
|
|
256
|
+
logger.debug("Using OpenAI as the default LLM model provider.")
|
|
257
|
+
return ChatOpenAI(model="gpt-4o-mini", temperature=0, max_tokens=100) # type: ignore[call-arg] - pyright can't see the args for some reason
|
|
258
|
+
else:
|
|
259
|
+
msg = "Unable to import a default LLM provider. Please install `vulcan_core` with the approriate extras package or specify your custom model explicitly."
|
|
260
|
+
raise ImportError(msg)
|
|
245
261
|
|
|
246
262
|
|
|
247
|
-
def condition(func: ConditionCallable | str) -> Condition:
|
|
263
|
+
def condition(func: ConditionCallable | str, model: BaseChatModel | None = None) -> Condition:
|
|
248
264
|
"""
|
|
249
265
|
Creates a Condition object from a lambda or function. It performs limited static analysis of the code to ensure
|
|
250
266
|
proper usage and discover the facts/attributes accessed by the condition. This allows the rule engine to track
|
|
@@ -284,10 +300,14 @@ def condition(func: ConditionCallable | str) -> Condition:
|
|
|
284
300
|
"""
|
|
285
301
|
|
|
286
302
|
if not isinstance(func, str):
|
|
303
|
+
# Logic condition assumed, ignore kwargs
|
|
287
304
|
processed = ASTProcessor[ConditionCallable](func, condition, bool)
|
|
288
305
|
return Condition(processed.facts, processed.func)
|
|
289
306
|
else:
|
|
290
|
-
|
|
307
|
+
# AI condition assumed
|
|
308
|
+
if not model:
|
|
309
|
+
model = _detect_default_model()
|
|
310
|
+
return ai_condition(model, func)
|
|
291
311
|
|
|
292
312
|
|
|
293
313
|
# TODO: Create a convenience function for creating OnFactChanged conditions
|
vulcan_core/engine.py
CHANGED
|
@@ -9,6 +9,7 @@ from types import MappingProxyType
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
from uuid import UUID, uuid4
|
|
11
11
|
|
|
12
|
+
from vulcan_core.ast_utils import NotAFactError
|
|
12
13
|
from vulcan_core.models import DeclaresFacts, Fact
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING: # pragma: no cover - not used at runtime
|
|
@@ -104,6 +105,10 @@ class RuleEngine:
|
|
|
104
105
|
|
|
105
106
|
if isinstance(fact, partial):
|
|
106
107
|
fact_name = fact.func.__name__
|
|
108
|
+
fact_class = fact.func
|
|
109
|
+
if not issubclass(fact_class, Fact): # type: ignore
|
|
110
|
+
raise NotAFactError(fact_class)
|
|
111
|
+
|
|
107
112
|
if fact_name in self._facts:
|
|
108
113
|
self._facts[fact_name] |= fact
|
|
109
114
|
else:
|
|
@@ -113,19 +118,24 @@ class RuleEngine:
|
|
|
113
118
|
msg = f"Fact '{fact_name}' is missing and lacks sufficient defaults to create from partial: {fact}"
|
|
114
119
|
raise InternalStateError(msg) from err
|
|
115
120
|
else:
|
|
121
|
+
fact_class = type(fact)
|
|
122
|
+
if not issubclass(fact_class, Fact):
|
|
123
|
+
raise NotAFactError(fact_class)
|
|
124
|
+
|
|
116
125
|
self._facts[type(fact).__name__] = fact
|
|
117
126
|
|
|
118
|
-
def rule[T: Fact](self, *, name: str | None = None, when: Expression, then: Action, inverse: Action | None = None):
|
|
127
|
+
def rule[T: Fact](self, *, name: str | None = None, when: Expression, then: Action, inverse: Action | None = None) -> None:
|
|
119
128
|
"""
|
|
120
129
|
Convenience method for adding a rule to the rule engine.
|
|
121
130
|
|
|
122
131
|
Args:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
name (Optional[str]): The name of the rule. Defaults to None.
|
|
133
|
+
when (Expression): The condition that triggers the rule.
|
|
134
|
+
then (Action): The action to be executed when the condition is met.
|
|
135
|
+
inverse (Optional[Action]): The action to be executed when the condition is not met. Defaults to None.
|
|
127
136
|
|
|
128
|
-
Returns:
|
|
137
|
+
Returns:
|
|
138
|
+
None
|
|
129
139
|
"""
|
|
130
140
|
rule = Rule(name, when, then, inverse)
|
|
131
141
|
|
vulcan_core/models.py
CHANGED
|
@@ -120,7 +120,7 @@ class FactMetaclass(type):
|
|
|
120
120
|
|
|
121
121
|
class Fact(ImmutableAttrAsDict, metaclass=FactMetaclass):
|
|
122
122
|
"""
|
|
123
|
-
An abstract class that must be used define rule engine fact schemas and instantiate data into working memory. Facts
|
|
123
|
+
An abstract class that must be used to define rule engine fact schemas and instantiate data into working memory. Facts
|
|
124
124
|
may be combined with partial facts of the same type using the `|` operator. This is useful for Actions that only
|
|
125
125
|
need to update a portion of working memory.
|
|
126
126
|
|
|
@@ -133,8 +133,16 @@ class Fact(ImmutableAttrAsDict, metaclass=FactMetaclass):
|
|
|
133
133
|
lefthand operand is created with the partial Fact's keywords applied.
|
|
134
134
|
"""
|
|
135
135
|
if isinstance(other, Fact):
|
|
136
|
+
if type(self) is not type(other):
|
|
137
|
+
msg = f"Union operator disallowed for types {type(self).__name__} and {type(other).__name__}"
|
|
138
|
+
raise TypeError(msg)
|
|
139
|
+
|
|
136
140
|
return other # type: ignore
|
|
137
141
|
else:
|
|
142
|
+
if type(self) is not other.func:
|
|
143
|
+
msg = f"Union operator disallowed for types {type(self).__name__} and {other.func}"
|
|
144
|
+
raise TypeError(msg)
|
|
145
|
+
|
|
138
146
|
new_fact = copy(self)
|
|
139
147
|
for kw, value in other.keywords.items():
|
|
140
148
|
object.__setattr__(new_fact, kw, value)
|
|
@@ -258,3 +266,6 @@ class RetrieverAdapter(Similarity):
|
|
|
258
266
|
|
|
259
267
|
def __len__(self) -> int:
|
|
260
268
|
raise NotImplementedError
|
|
269
|
+
|
|
270
|
+
def __str__(self) -> str:
|
|
271
|
+
return f"RetrieverAdapter(search_type={self.store.search_type})"
|
vulcan_core/util.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: vulcan-core
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: AI-Hybrid Rules Engine for Logical Reasoning.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: rules,logic,reasoning,ai,artificial intelligence,RAG,LLM
|
|
@@ -15,7 +15,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
15
15
|
Provides-Extra: openai
|
|
16
16
|
Requires-Dist: langchain ; extra == "openai"
|
|
17
17
|
Requires-Dist: langchain-openai ; extra == "openai"
|
|
18
|
-
Requires-Dist: pydantic (>=2.
|
|
18
|
+
Requires-Dist: pydantic (>=2.11.1,<2.12.0)
|
|
19
19
|
Project-URL: Documentation, https://latchfield.com/vulcan/docs
|
|
20
20
|
Project-URL: Homepage, https://latchfield.com/vulcan
|
|
21
21
|
Project-URL: Repository, https://github.com/latchfield/vulcan_core
|
|
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
<img alt="Vulcan Logo" src="https://latchfield.com/vulcan/assets/images/vulcan-logo.svg" height="100px">
|
|
27
27
|
|
|
28
28
|
# AI-Hybrid Rules Engine for Logical Reasoning
|
|
29
|
-

|
|
29
|
+
[](https://pypi.org/project/vulcan-core/)
|
|
30
30
|
|
|
31
31
|
Vulcan is an AI-hybrid rules engine designed for advanced automated reasoning. It combines the power of rule-based decision systems with LLMs (Large Language Models) for improved consistency and explainability in AI-powered systems.
|
|
32
32
|
|
|
@@ -59,14 +59,14 @@ Into repeatable, consistent, and explainable rules:
|
|
|
59
59
|
```python
|
|
60
60
|
# Use natural language for prediction and data retrieval:
|
|
61
61
|
engine.rule(
|
|
62
|
-
when(f"Are {Apple.kind} considered good for baking?"),
|
|
63
|
-
then(Apple(baking=True)),
|
|
62
|
+
when=condition(f"Are {Apple.kind} considered good for baking?"),
|
|
63
|
+
then=action(Apple(baking=True)),
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
# Use computed logic for operations that must be correct:
|
|
67
67
|
engine.rule(
|
|
68
|
-
when(Apple.baking && lambda: Inventory.apples < 10),
|
|
69
|
-
then(Order(apples=10)),
|
|
68
|
+
when=condition(Apple.baking && lambda: Inventory.apples < 10),
|
|
69
|
+
then=action(Order(apples=10)),
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
# Intelligent on-demand rule evaluation:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
vulcan_core/__init__.py,sha256=pjCnmbMjrsp672WXRQeOV0aSKUEoA_mj0o7q_ouMWs8,1057
|
|
2
|
+
vulcan_core/actions.py,sha256=RO5w5X-drxtDY_mVv0xR2njasWkGPt1AZo9RXsBi8X0,917
|
|
3
|
+
vulcan_core/ast_utils.py,sha256=IeAqk3XPg9Ae-N2nkNxORPVs3b4vv_dtHiSEbYkD9N8,12575
|
|
4
|
+
vulcan_core/conditions.py,sha256=cNwp1M9nprda7i76Anhet4lih_6fTKlUyDYRYPmRKmk,13197
|
|
5
|
+
vulcan_core/engine.py,sha256=WjayTDEjKaIEVkkSZyDjdbu1Xy1XIvPewI83l6Sjo9g,9672
|
|
6
|
+
vulcan_core/models.py,sha256=7um3u-rAy9gg2otTnZFGlKfHJKfsvGEosU3mGq_0jyg,8964
|
|
7
|
+
vulcan_core/util.py,sha256=Uq5uWhrfWd8fNv6IeeTFZRGeLBAECPZUx63UjbbSMrA,3420
|
|
8
|
+
vulcan_core-1.1.1.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
|
9
|
+
vulcan_core-1.1.1.dist-info/METADATA,sha256=rDTr3JMTjIGOK1MO9u0oEFyUfsExB4QouvOXwAgd6z4,4424
|
|
10
|
+
vulcan_core-1.1.1.dist-info/NOTICE,sha256=UN1_Gd_Snmu8T62mxExNUdIF2o7DMdGu8bI8rKqhVnc,244
|
|
11
|
+
vulcan_core-1.1.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
12
|
+
vulcan_core-1.1.1.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
vulcan_core/__init__.py,sha256=pjCnmbMjrsp672WXRQeOV0aSKUEoA_mj0o7q_ouMWs8,1057
|
|
2
|
-
vulcan_core/actions.py,sha256=RO5w5X-drxtDY_mVv0xR2njasWkGPt1AZo9RXsBi8X0,917
|
|
3
|
-
vulcan_core/ast_utils.py,sha256=eNFcgmUaAXfkMjNy8rF2LDHslqj-4l2bqiUns_80fXY,11818
|
|
4
|
-
vulcan_core/conditions.py,sha256=3TuXSd7UNlpwQnPy-5YJ-6zNGNZcW7b4iMGzo3fzBQE,12423
|
|
5
|
-
vulcan_core/engine.py,sha256=UtrfNkrokZazW_l3_zmwKyackJLON-mpGvnNmZyjBqM,9313
|
|
6
|
-
vulcan_core/models.py,sha256=j9uA4MjzemN4BCVvifqAdFCanUguMeafhulr6GiI49Q,8480
|
|
7
|
-
vulcan_core/util.py,sha256=THlBzIO9zw7JiKGYB8PIan4oRjsGuNaZpx-lgCmUAVI,3419
|
|
8
|
-
vulcan_core-1.0.0.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
|
9
|
-
vulcan_core-1.0.0.dist-info/METADATA,sha256=q4hqsr_6-G5pCkPFMeZcpgJYn7iq5vAKDYtax9rx4Dk,4349
|
|
10
|
-
vulcan_core-1.0.0.dist-info/NOTICE,sha256=UN1_Gd_Snmu8T62mxExNUdIF2o7DMdGu8bI8rKqhVnc,244
|
|
11
|
-
vulcan_core-1.0.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
12
|
-
vulcan_core-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|