boolean-algebra-engine 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.
- api/__init__.py +0 -0
- api/routes.py +386 -0
- boolean_algebra_engine-0.1.0.dist-info/METADATA +213 -0
- boolean_algebra_engine-0.1.0.dist-info/RECORD +19 -0
- boolean_algebra_engine-0.1.0.dist-info/WHEEL +5 -0
- boolean_algebra_engine-0.1.0.dist-info/entry_points.txt +2 -0
- boolean_algebra_engine-0.1.0.dist-info/licenses/LICENSE +674 -0
- boolean_algebra_engine-0.1.0.dist-info/top_level.txt +5 -0
- cli/__init__.py +0 -0
- cli/main.py +411 -0
- core/__init__.py +0 -0
- core/evaluator.py +86 -0
- core/models.py +96 -0
- core/parser.py +73 -0
- core/synthesizer.py +167 -0
- mcp_server/__init__.py +0 -0
- mcp_server/server.py +247 -0
- nl/__init__.py +0 -0
- nl/nl.py +449 -0
api/__init__.py
ADDED
|
File without changes
|
api/routes.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
api/routes.py — REST API wrapping the boolean algebra engine.
|
|
3
|
+
|
|
4
|
+
Endpoints:
|
|
5
|
+
POST /evaluate expression → truth table
|
|
6
|
+
POST /simplify expression → minimal form
|
|
7
|
+
POST /equivalent two expressions → same truth table?
|
|
8
|
+
POST /satisfiable expression → any row outputs 1?
|
|
9
|
+
POST /check-rules list of expressions → contradictions, conflicts
|
|
10
|
+
POST /nl/ask plain English → verified boolean result
|
|
11
|
+
POST /nl/check-rules list of plain English rules → analysis
|
|
12
|
+
GET /health liveness check
|
|
13
|
+
|
|
14
|
+
Run:
|
|
15
|
+
uvicorn api.routes:app --host 0.0.0.0 --port 8080 --reload
|
|
16
|
+
|
|
17
|
+
Environment variables:
|
|
18
|
+
ANTHROPIC_API_KEY — for nl/* endpoints with anthropic provider
|
|
19
|
+
OPENAI_API_KEY — for nl/* endpoints with openai provider
|
|
20
|
+
REDIS_URL — optional, enables caching (default: redis://localhost:6379)
|
|
21
|
+
API_KEY — optional, enables auth (Pro/Team tiers)
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import time
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
from fastapi import FastAPI, HTTPException, Request, Response
|
|
32
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
33
|
+
from pydantic import BaseModel
|
|
34
|
+
|
|
35
|
+
from core.evaluator import evaluate as _evaluate
|
|
36
|
+
from core.synthesizer import synthesize as _synthesize
|
|
37
|
+
from core.parser import validate, infix_to_prefix
|
|
38
|
+
from core.evaluator import _evaluate_prefix
|
|
39
|
+
|
|
40
|
+
app = FastAPI(
|
|
41
|
+
title="Boolean Algebra Engine",
|
|
42
|
+
description="Deterministic boolean logic verification API.",
|
|
43
|
+
version="0.1.0",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
app.add_middleware(
|
|
47
|
+
CORSMiddleware,
|
|
48
|
+
allow_origins=["*"],
|
|
49
|
+
allow_methods=["*"],
|
|
50
|
+
allow_headers=["*"],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Redis cache — optional, degrades gracefully if not available
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
_redis = None
|
|
59
|
+
|
|
60
|
+
def _get_redis():
|
|
61
|
+
global _redis
|
|
62
|
+
if _redis is not None:
|
|
63
|
+
return _redis
|
|
64
|
+
try:
|
|
65
|
+
import redis
|
|
66
|
+
r = redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379"))
|
|
67
|
+
r.ping()
|
|
68
|
+
_redis = r
|
|
69
|
+
except Exception:
|
|
70
|
+
_redis = None
|
|
71
|
+
return _redis
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _cache_get(key: str) -> dict | None:
|
|
75
|
+
r = _get_redis()
|
|
76
|
+
if not r:
|
|
77
|
+
return None
|
|
78
|
+
try:
|
|
79
|
+
val = r.get(key)
|
|
80
|
+
return json.loads(val) if val else None
|
|
81
|
+
except Exception:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _cache_set(key: str, value: dict, ttl: int = 86400):
|
|
86
|
+
r = _get_redis()
|
|
87
|
+
if not r:
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
r.setex(key, ttl, json.dumps(value))
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _cache_key(*parts) -> str:
|
|
96
|
+
raw = ":".join(str(p) for p in parts)
|
|
97
|
+
return "bae:" + hashlib.sha256(raw.encode()).hexdigest()[:16]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Auth — optional, enabled when API_KEY env var is set
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def _check_auth(request: Request):
|
|
105
|
+
required = os.environ.get("API_KEY")
|
|
106
|
+
if not required:
|
|
107
|
+
return
|
|
108
|
+
provided = request.headers.get("X-API-Key")
|
|
109
|
+
if provided != required:
|
|
110
|
+
raise HTTPException(status_code=401, detail="Invalid or missing X-API-Key")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Request / response models
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
class EvaluateRequest(BaseModel):
|
|
118
|
+
expression: str
|
|
119
|
+
|
|
120
|
+
class SimplifyRequest(BaseModel):
|
|
121
|
+
expression: str
|
|
122
|
+
|
|
123
|
+
class EquivalentRequest(BaseModel):
|
|
124
|
+
expression1: str
|
|
125
|
+
expression2: str
|
|
126
|
+
|
|
127
|
+
class SatisfiableRequest(BaseModel):
|
|
128
|
+
expression: str
|
|
129
|
+
|
|
130
|
+
class CheckRulesRequest(BaseModel):
|
|
131
|
+
rules: list[str]
|
|
132
|
+
|
|
133
|
+
class NLAskRequest(BaseModel):
|
|
134
|
+
sentence: str
|
|
135
|
+
provider: str = "anthropic"
|
|
136
|
+
api_key: Optional[str] = None
|
|
137
|
+
model: Optional[str] = None
|
|
138
|
+
base_url: Optional[str] = None
|
|
139
|
+
|
|
140
|
+
class NLCheckRulesRequest(BaseModel):
|
|
141
|
+
rules: list[str]
|
|
142
|
+
provider: str = "anthropic"
|
|
143
|
+
api_key: Optional[str] = None
|
|
144
|
+
model: Optional[str] = None
|
|
145
|
+
base_url: Optional[str] = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Helper — build provider from request fields
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
def _build_provider(provider: str, api_key: Optional[str], model: Optional[str], base_url: Optional[str]):
|
|
153
|
+
from nl.nl import AnthropicProvider, OpenAIProvider, OllamaProvider, OpenAICompatProvider
|
|
154
|
+
if provider == "anthropic":
|
|
155
|
+
return AnthropicProvider(api_key=api_key, model=model or "claude-sonnet-4-6")
|
|
156
|
+
if provider == "openai":
|
|
157
|
+
return OpenAIProvider(api_key=api_key, model=model or "gpt-4o")
|
|
158
|
+
if provider == "ollama":
|
|
159
|
+
return OllamaProvider(model=model or "llama3", base_url=base_url or "http://localhost:11434")
|
|
160
|
+
if provider == "compat":
|
|
161
|
+
if not base_url or not model:
|
|
162
|
+
raise HTTPException(status_code=400, detail="base_url and model required for compat provider")
|
|
163
|
+
return OpenAICompatProvider(api_key=api_key or "", base_url=base_url, model=model)
|
|
164
|
+
raise HTTPException(status_code=400, detail=f"Unknown provider '{provider}'. Choose: anthropic, openai, ollama, compat")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Endpoints
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
@app.get("/health")
|
|
172
|
+
def health():
|
|
173
|
+
redis_ok = _get_redis() is not None
|
|
174
|
+
return {"status": "ok", "version": "0.1.0", "redis": redis_ok}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.post("/evaluate")
|
|
178
|
+
def evaluate(req: EvaluateRequest, request: Request, response: Response):
|
|
179
|
+
_check_auth(request)
|
|
180
|
+
|
|
181
|
+
cache_key = _cache_key("evaluate", req.expression)
|
|
182
|
+
cached = _cache_get(cache_key)
|
|
183
|
+
if cached:
|
|
184
|
+
response.headers["X-Cache"] = "HIT"
|
|
185
|
+
return cached
|
|
186
|
+
|
|
187
|
+
error = validate(req.expression)
|
|
188
|
+
if error:
|
|
189
|
+
raise HTTPException(status_code=422, detail={"error": "invalid_expression", "message": error, "expression": req.expression})
|
|
190
|
+
|
|
191
|
+
t0 = time.perf_counter()
|
|
192
|
+
table, metrics = _evaluate(req.expression)
|
|
193
|
+
elapsed = round((time.perf_counter() - t0) * 1000, 4)
|
|
194
|
+
|
|
195
|
+
result = {
|
|
196
|
+
"expression": table.expression,
|
|
197
|
+
"variables": table.variables,
|
|
198
|
+
"rows": [{**row.inputs, "output": row.output} for row in table.rows],
|
|
199
|
+
"satisfiable": table.satisfiable,
|
|
200
|
+
"tautology": table.tautology,
|
|
201
|
+
"minterms": table.minterms,
|
|
202
|
+
"maxterms": table.maxterms,
|
|
203
|
+
"eval_time_ms": elapsed,
|
|
204
|
+
"rows_evaluated": metrics.rows_evaluated,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_cache_set(cache_key, result, ttl=86400)
|
|
208
|
+
response.headers["X-Cache"] = "MISS"
|
|
209
|
+
response.headers["X-Eval-Time-Ms"] = str(elapsed)
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@app.post("/simplify")
|
|
214
|
+
def simplify(req: SimplifyRequest, request: Request, response: Response):
|
|
215
|
+
_check_auth(request)
|
|
216
|
+
|
|
217
|
+
cache_key = _cache_key("simplify", req.expression)
|
|
218
|
+
cached = _cache_get(cache_key)
|
|
219
|
+
if cached:
|
|
220
|
+
response.headers["X-Cache"] = "HIT"
|
|
221
|
+
return cached
|
|
222
|
+
|
|
223
|
+
error = validate(req.expression)
|
|
224
|
+
if error:
|
|
225
|
+
raise HTTPException(status_code=422, detail={"error": "invalid_expression", "message": error, "expression": req.expression})
|
|
226
|
+
|
|
227
|
+
table, _ = _evaluate(req.expression)
|
|
228
|
+
minimal, metrics = _synthesize(table)
|
|
229
|
+
|
|
230
|
+
result = {
|
|
231
|
+
"original": req.expression,
|
|
232
|
+
"minimal": minimal,
|
|
233
|
+
"changed": minimal != req.expression,
|
|
234
|
+
"prime_implicant_count": metrics.prime_implicant_count,
|
|
235
|
+
"synth_time_ms": metrics.synth_time_ms,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_cache_set(cache_key, result, ttl=86400)
|
|
239
|
+
response.headers["X-Cache"] = "MISS"
|
|
240
|
+
return result
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@app.post("/equivalent")
|
|
244
|
+
def equivalent(req: EquivalentRequest, request: Request, response: Response):
|
|
245
|
+
_check_auth(request)
|
|
246
|
+
|
|
247
|
+
cache_key = _cache_key("equivalent", req.expression1, req.expression2)
|
|
248
|
+
cached = _cache_get(cache_key)
|
|
249
|
+
if cached:
|
|
250
|
+
response.headers["X-Cache"] = "HIT"
|
|
251
|
+
return cached
|
|
252
|
+
|
|
253
|
+
for expr in [req.expression1, req.expression2]:
|
|
254
|
+
error = validate(expr)
|
|
255
|
+
if error:
|
|
256
|
+
raise HTTPException(status_code=422, detail={"error": "invalid_expression", "message": error, "expression": expr})
|
|
257
|
+
|
|
258
|
+
from core.parser import get_variables
|
|
259
|
+
vars1 = set(get_variables(req.expression1))
|
|
260
|
+
vars2 = set(get_variables(req.expression2))
|
|
261
|
+
all_vars = sorted(vars1 | vars2)
|
|
262
|
+
n = len(all_vars)
|
|
263
|
+
|
|
264
|
+
p1 = infix_to_prefix(req.expression1)
|
|
265
|
+
p2 = infix_to_prefix(req.expression2)
|
|
266
|
+
|
|
267
|
+
differing = []
|
|
268
|
+
for i in range(2 ** n):
|
|
269
|
+
values = {var: (i >> (n - 1 - j)) & 1 for j, var in enumerate(all_vars)}
|
|
270
|
+
v1 = {k: v for k, v in values.items() if k in vars1}
|
|
271
|
+
v2 = {k: v for k, v in values.items() if k in vars2}
|
|
272
|
+
out1 = _evaluate_prefix(p1, v1)
|
|
273
|
+
out2 = _evaluate_prefix(p2, v2)
|
|
274
|
+
if out1 != out2:
|
|
275
|
+
differing.append({**values, req.expression1: out1, req.expression2: out2})
|
|
276
|
+
|
|
277
|
+
result: dict = {
|
|
278
|
+
"equivalent": len(differing) == 0,
|
|
279
|
+
"expression1": req.expression1,
|
|
280
|
+
"expression2": req.expression2,
|
|
281
|
+
}
|
|
282
|
+
if differing:
|
|
283
|
+
result["differing_rows"] = differing[:10]
|
|
284
|
+
result["total_differing"] = len(differing)
|
|
285
|
+
|
|
286
|
+
_cache_set(cache_key, result, ttl=86400)
|
|
287
|
+
response.headers["X-Cache"] = "MISS"
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@app.post("/satisfiable")
|
|
292
|
+
def satisfiable(req: SatisfiableRequest, request: Request, response: Response):
|
|
293
|
+
_check_auth(request)
|
|
294
|
+
|
|
295
|
+
cache_key = _cache_key("satisfiable", req.expression)
|
|
296
|
+
cached = _cache_get(cache_key)
|
|
297
|
+
if cached:
|
|
298
|
+
response.headers["X-Cache"] = "HIT"
|
|
299
|
+
return cached
|
|
300
|
+
|
|
301
|
+
error = validate(req.expression)
|
|
302
|
+
if error:
|
|
303
|
+
raise HTTPException(status_code=422, detail={"error": "invalid_expression", "message": error, "expression": req.expression})
|
|
304
|
+
|
|
305
|
+
table, _ = _evaluate(req.expression)
|
|
306
|
+
result: dict = {"satisfiable": table.satisfiable, "expression": req.expression}
|
|
307
|
+
if table.satisfiable:
|
|
308
|
+
first = table.rows[table.minterms[0]]
|
|
309
|
+
result["example"] = {**first.inputs, "output": first.output}
|
|
310
|
+
|
|
311
|
+
_cache_set(cache_key, result, ttl=86400)
|
|
312
|
+
response.headers["X-Cache"] = "MISS"
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@app.post("/check-rules")
|
|
317
|
+
def check_rules(req: CheckRulesRequest, request: Request, response: Response):
|
|
318
|
+
_check_auth(request)
|
|
319
|
+
|
|
320
|
+
cache_key = _cache_key("check-rules", *sorted(req.rules))
|
|
321
|
+
cached = _cache_get(cache_key)
|
|
322
|
+
if cached:
|
|
323
|
+
response.headers["X-Cache"] = "HIT"
|
|
324
|
+
return cached
|
|
325
|
+
|
|
326
|
+
from mcp_server.server import check_prompt_logic
|
|
327
|
+
result = check_prompt_logic(req.rules)
|
|
328
|
+
|
|
329
|
+
_cache_set(cache_key, result, ttl=3600)
|
|
330
|
+
response.headers["X-Cache"] = "MISS"
|
|
331
|
+
return result
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@app.post("/nl/ask")
|
|
335
|
+
def nl_ask(req: NLAskRequest, request: Request, response: Response):
|
|
336
|
+
_check_auth(request)
|
|
337
|
+
|
|
338
|
+
cache_key = _cache_key("nl-ask", req.sentence, req.provider, req.model or "")
|
|
339
|
+
cached = _cache_get(cache_key)
|
|
340
|
+
if cached:
|
|
341
|
+
response.headers["X-Cache"] = "HIT"
|
|
342
|
+
return cached
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
from nl.nl import ask
|
|
346
|
+
prov = _build_provider(req.provider, req.api_key, req.model, req.base_url)
|
|
347
|
+
result = ask(req.sentence, provider=prov)
|
|
348
|
+
except ValueError as e:
|
|
349
|
+
raise HTTPException(status_code=422, detail={"error": "parse_failed", "message": str(e)})
|
|
350
|
+
except Exception as e:
|
|
351
|
+
raise HTTPException(status_code=500, detail={"error": "internal", "message": str(e)})
|
|
352
|
+
|
|
353
|
+
data = {
|
|
354
|
+
"input": result.input_sentence,
|
|
355
|
+
"expression": result.expression,
|
|
356
|
+
"variables": result.variables,
|
|
357
|
+
"minimal": result.minimal,
|
|
358
|
+
"satisfiable": result.satisfiable,
|
|
359
|
+
"tautology": result.tautology,
|
|
360
|
+
"contradiction": result.contradiction,
|
|
361
|
+
"minterms": result.minterms,
|
|
362
|
+
"maxterms": result.maxterms,
|
|
363
|
+
"explanation": result.explanation,
|
|
364
|
+
"rows": result.rows,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_cache_set(cache_key, data, ttl=3600)
|
|
368
|
+
response.headers["X-Cache"] = "MISS"
|
|
369
|
+
return data
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@app.post("/nl/check-rules")
|
|
373
|
+
def nl_check_rules(req: NLCheckRulesRequest, request: Request, response: Response):
|
|
374
|
+
_check_auth(request)
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
from nl.nl import check_rules as _check_rules
|
|
378
|
+
prov = _build_provider(req.provider, req.api_key, req.model, req.base_url)
|
|
379
|
+
result = _check_rules(req.rules, provider=prov)
|
|
380
|
+
except ValueError as e:
|
|
381
|
+
raise HTTPException(status_code=422, detail={"error": "parse_failed", "message": str(e)})
|
|
382
|
+
except Exception as e:
|
|
383
|
+
raise HTTPException(status_code=500, detail={"error": "internal", "message": str(e)})
|
|
384
|
+
|
|
385
|
+
response.headers["X-Cache"] = "MISS"
|
|
386
|
+
return result
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boolean-algebra-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Boolean algebra engine — evaluate expressions, generate truth tables, synthesize minimal forms
|
|
5
|
+
Author-email: Aditya Shrivastava <adityars.work@gmail.com>
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/Shrivastava-Aditya/boolean-algebra-engine-python
|
|
8
|
+
Project-URL: Repository, https://github.com/Shrivastava-Aditya/boolean-algebra-engine-python
|
|
9
|
+
Project-URL: Issues, https://github.com/Shrivastava-Aditya/boolean-algebra-engine-python/issues
|
|
10
|
+
Keywords: boolean,algebra,logic,truth-table,quine-mccluskey,digital-logic
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: cli
|
|
25
|
+
Requires-Dist: typer>=0.12.0; extra == "cli"
|
|
26
|
+
Requires-Dist: rich>=13.0.0; extra == "cli"
|
|
27
|
+
Provides-Extra: mcp
|
|
28
|
+
Requires-Dist: mcp[cli]>=1.0.0; extra == "mcp"
|
|
29
|
+
Provides-Extra: nl-anthropic
|
|
30
|
+
Requires-Dist: anthropic>=0.50.0; extra == "nl-anthropic"
|
|
31
|
+
Provides-Extra: nl-openai
|
|
32
|
+
Requires-Dist: openai>=1.0.0; extra == "nl-openai"
|
|
33
|
+
Provides-Extra: nl
|
|
34
|
+
Requires-Dist: anthropic>=0.50.0; extra == "nl"
|
|
35
|
+
Provides-Extra: api
|
|
36
|
+
Requires-Dist: fastapi>=0.100.0; extra == "api"
|
|
37
|
+
Requires-Dist: uvicorn>=0.20.0; extra == "api"
|
|
38
|
+
Provides-Extra: api-cache
|
|
39
|
+
Requires-Dist: fastapi>=0.100.0; extra == "api-cache"
|
|
40
|
+
Requires-Dist: uvicorn>=0.20.0; extra == "api-cache"
|
|
41
|
+
Requires-Dist: redis>=5.0.0; extra == "api-cache"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
44
|
+
Requires-Dist: httpx>=0.24.0; extra == "dev"
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# boolean-algebra-engine-python
|
|
48
|
+
|
|
49
|
+
A deterministic boolean algebra engine — evaluates expressions, generates truth tables, synthesises minimal forms, and verifies logical consistency.
|
|
50
|
+
|
|
51
|
+
Forked from [boolean-algebra-java](https://github.com/Shrivastava-Aditya/boolean-algebra-java) — original Java implementation written during placement season.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## What it does
|
|
56
|
+
|
|
57
|
+
**Forward:** expression → full truth table, exhaustive 2^n evaluation, exact.
|
|
58
|
+
|
|
59
|
+
**Inverse:** truth table → minimal boolean expression via Quine-McCluskey.
|
|
60
|
+
|
|
61
|
+
**Verification:** satisfiability, contradiction, tautology, equivalence, pairwise conflict detection across rule sets.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Operators
|
|
66
|
+
|
|
67
|
+
| Symbol | Operation | Precedence |
|
|
68
|
+
|--------|-----------|------------|
|
|
69
|
+
| `!` | NOT | 4 (highest) |
|
|
70
|
+
| `.` | AND | 3 |
|
|
71
|
+
| `^` | XOR | 2 |
|
|
72
|
+
| `+` | OR | 1 (lowest) |
|
|
73
|
+
|
|
74
|
+
Variables: uppercase `A`–`Z`. Parentheses override precedence.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Architecture
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
┌──────────────────────────────────────────────────────┐
|
|
82
|
+
│ Interface Layer │
|
|
83
|
+
│ CLI/REPL MCP Server REST API Streamlit UI │
|
|
84
|
+
└───────────────────────┬──────────────────────────────┘
|
|
85
|
+
│
|
|
86
|
+
┌───────────────────────┴──────────────────────────────┐
|
|
87
|
+
│ NL Layer │
|
|
88
|
+
│ plain English → expression → plain English │
|
|
89
|
+
│ Anthropic · OpenAI · Ollama · OpenAI-compat │
|
|
90
|
+
└───────────────────────┬──────────────────────────────┘
|
|
91
|
+
│
|
|
92
|
+
┌───────────────────────┴──────────────────────────────┐
|
|
93
|
+
│ Core Engine │
|
|
94
|
+
│ parser (shunting-yard) → evaluator (prefix stack) │
|
|
95
|
+
│ → synthesizer (Quine-McCluskey)│
|
|
96
|
+
└───────────────────────┬──────────────────────────────┘
|
|
97
|
+
│
|
|
98
|
+
┌───────────────────────┴──────────────────────────────┐
|
|
99
|
+
│ Acceleration Layer (planned) │
|
|
100
|
+
│ numpy · CUDA · Redis │
|
|
101
|
+
└──────────────────────────────────────────────────────┘
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`core/` has zero external dependencies. Every layer above is a thin wrapper. Independently deployable, independently testable.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Project structure
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
core/
|
|
112
|
+
models.py TruthTable, TruthTableRow, PerformanceMetrics
|
|
113
|
+
parser.py Shunting-yard — infix → prefix, validation, variable extraction
|
|
114
|
+
evaluator.py Prefix stack evaluator — exhaustive 2^n row enumeration
|
|
115
|
+
synthesizer.py Quine-McCluskey — truth table → minimal SOP expression
|
|
116
|
+
|
|
117
|
+
mcp_server/
|
|
118
|
+
server.py 5 tools for agent integration (evaluate, simplify,
|
|
119
|
+
equivalent, satisfiable, check_prompt_logic)
|
|
120
|
+
|
|
121
|
+
api/
|
|
122
|
+
routes.py FastAPI — 7 endpoints, Redis cache, optional auth
|
|
123
|
+
|
|
124
|
+
nl/
|
|
125
|
+
nl.py Provider abstraction — Anthropic, OpenAI, Ollama, OpenAI-compat
|
|
126
|
+
|
|
127
|
+
cli/
|
|
128
|
+
main.py typer + rich — REPL and one-shot, all output formats
|
|
129
|
+
|
|
130
|
+
ui/
|
|
131
|
+
app.py Streamlit — Expression, Rule Auditor, Plain English modes
|
|
132
|
+
|
|
133
|
+
tests/ 90 tests — unit, integration, edge cases, round-trips
|
|
134
|
+
|
|
135
|
+
benchmark.py LLM hallucination benchmark — engine as oracle
|
|
136
|
+
visualisations.ipynb Colab notebook — complexity vs variables, conflict graph
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Quickstart
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
git clone https://github.com/Shrivastava-Aditya/boolean-algebra-engine-python
|
|
145
|
+
cd boolean-algebra-engine-python
|
|
146
|
+
pip install -e ".[dev]"
|
|
147
|
+
python3 -m pytest tests/
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Core usage
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from core.evaluator import evaluate
|
|
156
|
+
from core.synthesizer import synthesize
|
|
157
|
+
|
|
158
|
+
table, _ = evaluate("A.(B+C)")
|
|
159
|
+
print(table.variables) # ['A', 'B', 'C']
|
|
160
|
+
print(table.minterms) # [5, 6, 7]
|
|
161
|
+
print(table.satisfiable) # True
|
|
162
|
+
|
|
163
|
+
minimal, _ = synthesize(table)
|
|
164
|
+
print(minimal) # A.C+A.B
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## check_prompt_logic
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from mcp_server.server import check_prompt_logic
|
|
173
|
+
|
|
174
|
+
result = check_prompt_logic([
|
|
175
|
+
"A.B", # approve: good credit AND income verified
|
|
176
|
+
"C", # approve: collateral exists
|
|
177
|
+
"!A", # reject: bad credit
|
|
178
|
+
"!B.!C", # reject: no income AND no collateral
|
|
179
|
+
])
|
|
180
|
+
print(result["summary"])
|
|
181
|
+
# {'total': 4, 'contradictions': 0, 'conflicting_pairs': 3}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Benchmark
|
|
187
|
+
|
|
188
|
+
Measures LLM hallucination rate on boolean logic. Engine is the oracle — ground truth by exhaustive enumeration, no human labelers.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
ollama pull tinyllama
|
|
192
|
+
python3 benchmark.py
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
First result: tinyllama (1B) · 3 variables · 10 cases · **40% hallucination rate**.
|
|
196
|
+
|
|
197
|
+
See `FAILURES.md` for real-world severity analysis of each failure.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Branches
|
|
202
|
+
|
|
203
|
+
| Branch | What |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `master` | Project — engine, interfaces, tests |
|
|
206
|
+
| `product-readme` | Product brief — what it proves, what it's for |
|
|
207
|
+
| `benchmark` | Benchmark methodology, multi-model results (in progress) |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Related
|
|
212
|
+
|
|
213
|
+
- [boolean-algebra-java](https://github.com/Shrivastava-Aditya/boolean-algebra-java) — original Java version
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
api/routes.py,sha256=MUtb2if-jYhXOT2C5GD5mC6EgACawNTANtCPS57UnYc,12646
|
|
3
|
+
boolean_algebra_engine-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
4
|
+
cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
cli/main.py,sha256=yU6Syy61qvtFgVGQADOUhjPFqjRO-D3IRqJE9n4berM,16055
|
|
6
|
+
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
core/evaluator.py,sha256=BqLvyb2JkF7WPmwPZYP1bzqbzLr7G8QJAAxELAypfNU,2655
|
|
8
|
+
core/models.py,sha256=FTepIO8GFxzyRNScprYqUOzuYd10Ta1iTcdHjwMywTo,3255
|
|
9
|
+
core/parser.py,sha256=cDJdlUhTliSZZw6VxlgKe7O_D9YEACsWGXfRviIGp-s,2364
|
|
10
|
+
core/synthesizer.py,sha256=o044C3G9Az1ZCgLFaU-0g68CJvNtZMWW-cBRF-Ye6Ok,5648
|
|
11
|
+
mcp_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
mcp_server/server.py,sha256=-3tyFeAndsCwNY-0pNnhA4xm_DBcAoIQPBBwwx5tNF4,8417
|
|
13
|
+
nl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
nl/nl.py,sha256=YdJo4rMMbTFPFNgyIXDNMn0zLICzt0av1bkx8NiilTU,14742
|
|
15
|
+
boolean_algebra_engine-0.1.0.dist-info/METADATA,sha256=Anwq8pP8wjvFHQ11riofLqaEeSBIyZBuV01VauXsbZU,7890
|
|
16
|
+
boolean_algebra_engine-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
17
|
+
boolean_algebra_engine-0.1.0.dist-info/entry_points.txt,sha256=-iwEp4864Mi7hNnYR1L1p5NxXEhSqJNn3X2mt_92nlk,42
|
|
18
|
+
boolean_algebra_engine-0.1.0.dist-info/top_level.txt,sha256=CDi0ytQMStd1Hy9rROqsiRPzWUS3mN5P8-Jr7ztlVFI,27
|
|
19
|
+
boolean_algebra_engine-0.1.0.dist-info/RECORD,,
|