satisfactoscript 1.0.0__tar.gz → 1.1.0__tar.gz
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.
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/PKG-INFO +4 -1
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/pyproject.toml +7 -1
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/cli.py +11 -27
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/interpreter.py +2 -1
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/ir.py +23 -1
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/semantic/llm_provider.py +105 -4
- satisfactoscript-1.1.0/src/satisfactoscript/serving/__init__.py +20 -0
- satisfactoscript-1.1.0/src/satisfactoscript/serving/_response_serializer.py +96 -0
- satisfactoscript-1.1.0/src/satisfactoscript/serving/chat_model.py +122 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript.egg-info/PKG-INFO +4 -1
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript.egg-info/SOURCES.txt +5 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript.egg-info/requires.txt +4 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_backend_sql_base.py +8 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_core_join.py +33 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_ir.py +25 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_llm_provider.py +107 -0
- satisfactoscript-1.1.0/tests/test_serving_chat_model.py +147 -0
- satisfactoscript-1.1.0/tests/test_serving_response_serializer.py +183 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/README.md +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/setup.cfg +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/__init__.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/__init__.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/builder_agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/dictionary_agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/exporter.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/history.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/hub.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/lineage_agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/models.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/orchestrator.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/quality_agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/resolver.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/agentic/user_profile.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/backends/__init__.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/backends/bigquery.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/backends/snowpark.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/backends/spark.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/backends/sql_base.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/__init__.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/backend.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/catalog_inspector.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/config.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/context.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/core.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/environment.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/json_schema.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/loaders.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/op_catalog.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/operations.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/patterns.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/registry.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/rule_analyzer.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/rule_executor.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/rule_planner.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/sandbox.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/schema_loader.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/core/writer.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/lineage/__init__.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/lineage/dictionary.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/lineage/renderer.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/lineage/tracker.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/observability/__init__.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/observability/alerts.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/observability/checks.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/observability/contracts.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/observability/history.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/observability/monitor.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/observability/reporter.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/registry.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/semantic/__init__.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/semantic/builder.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/semantic/extractor.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/semantic/glossary.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/semantic/semantic.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/semantic/validator.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/sinks/__init__.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/sinks/jdbc.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/spark_factory.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/utils.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript.egg-info/dependency_links.txt +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript.egg-info/entry_points.txt +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript.egg-info/top_level.txt +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_backend_bigquery.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_backend_protocol.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_backend_snowpark.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_backend_spark.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_builder_agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_catalog_inspector.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_cli.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_config.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_core.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_core_connect_patch.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_core_env_detection.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_core_username.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_dictionary_agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_dummy.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_engine_fake_backend.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_engine_with_backend.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_history.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_hub.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_interpreter.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_json_schema.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_lineage_agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_lineage_dictionary.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_lineage_renderer.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_lineage_tracker.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_loaders.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_observability.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_op_catalog.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_orchestrator.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_patterns.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_quality_agent.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_registry.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_registry_import_paths.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_resolver.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_rule_analyzer.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_rule_executor.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_rule_planner.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_sandbox.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_schema_loader.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_semantic_builder.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_semantic_engine_catalog.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_sink_jdbc.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_user_profile.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_utils_logging.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_utils_safe_columns.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_validator.py +0 -0
- {satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/tests/test_writer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: satisfactoscript
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Declarative data engineering framework — multi-platform (Databricks, Snowflake, BigQuery).
|
|
5
5
|
Author-email: julhouba <houbartjulien80@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -43,6 +43,9 @@ Requires-Dist: google-generativeai>=0.5.0; extra == "llm-google"
|
|
|
43
43
|
Provides-Extra: semantic-pdf
|
|
44
44
|
Requires-Dist: fpdf2>=2.7.0; extra == "semantic-pdf"
|
|
45
45
|
Requires-Dist: matplotlib>=3.7.0; extra == "semantic-pdf"
|
|
46
|
+
Provides-Extra: serving
|
|
47
|
+
Requires-Dist: mlflow<3,>=2.12.0; extra == "serving"
|
|
48
|
+
Requires-Dist: openai>=1.0.0; extra == "serving"
|
|
46
49
|
Provides-Extra: semantic-full
|
|
47
50
|
Requires-Dist: openai>=1.0.0; extra == "semantic-full"
|
|
48
51
|
Requires-Dist: anthropic>=0.30.0; extra == "semantic-full"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "satisfactoscript"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Declarative data engineering framework — multi-platform (Databricks, Snowflake, BigQuery)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -74,6 +74,12 @@ semantic-pdf = [
|
|
|
74
74
|
"matplotlib>=3.7.0"
|
|
75
75
|
]
|
|
76
76
|
|
|
77
|
+
# --- Databricks Model Serving ---
|
|
78
|
+
serving = [
|
|
79
|
+
"mlflow>=2.12.0,<3",
|
|
80
|
+
"openai>=1.0.0",
|
|
81
|
+
]
|
|
82
|
+
|
|
77
83
|
# --- Bundles pratiques ---
|
|
78
84
|
semantic-full = [
|
|
79
85
|
"openai>=1.0.0",
|
|
@@ -117,7 +117,6 @@ def _run_hub(args: argparse.Namespace) -> None:
|
|
|
117
117
|
"""Boucle REPL principale du SatisfactoHub."""
|
|
118
118
|
try:
|
|
119
119
|
from rich.console import Console
|
|
120
|
-
from rich.table import Table as RichTable
|
|
121
120
|
except ImportError:
|
|
122
121
|
print("[ERROR] Le module 'rich' est requis pour le hub. Installez-le avec : pip install rich")
|
|
123
122
|
sys.exit(1)
|
|
@@ -175,7 +174,7 @@ def _run_hub(args: argparse.Namespace) -> None:
|
|
|
175
174
|
|
|
176
175
|
if not args.new_session and len(session) > 0:
|
|
177
176
|
ts = session.created_at.strftime("%Y-%m-%d") if hasattr(session.created_at, "strftime") else str(session.created_at)
|
|
178
|
-
console.print(
|
|
177
|
+
console.print(" [1] Nouvelle session (efface la session précédente)")
|
|
179
178
|
console.print(f" [2] Reprendre la session du {ts} ({len(session)} messages)\n")
|
|
180
179
|
try:
|
|
181
180
|
choice = input("Choix [1/2] : ").strip()
|
|
@@ -255,8 +254,8 @@ def _run_hub(args: argparse.Namespace) -> None:
|
|
|
255
254
|
session.save(scope_id)
|
|
256
255
|
|
|
257
256
|
|
|
258
|
-
def _render_response(console:
|
|
259
|
-
"""
|
|
257
|
+
def _render_response(console: object, response: object) -> None:
|
|
258
|
+
"""Render a HubResponse in the terminal with Rich formatting."""
|
|
260
259
|
from .agentic.models import (
|
|
261
260
|
AgentResponse,
|
|
262
261
|
BuilderResponse,
|
|
@@ -264,6 +263,7 @@ def _render_response(console: "Console", response: object) -> None: # type: ign
|
|
|
264
263
|
LineageResponse,
|
|
265
264
|
QualityResponse,
|
|
266
265
|
)
|
|
266
|
+
from .serving._response_serializer import hub_response_to_text
|
|
267
267
|
|
|
268
268
|
if isinstance(response, AgentResponse):
|
|
269
269
|
if response.mode == "error":
|
|
@@ -271,27 +271,12 @@ def _render_response(console: "Console", response: object) -> None: # type: ign
|
|
|
271
271
|
elif response.mode in ("ambiguous", "needs_clarification"):
|
|
272
272
|
console.print(f"[yellow]{response.clarification_question or response.explanation}[/yellow]")
|
|
273
273
|
else:
|
|
274
|
-
|
|
274
|
+
_render_agent_response_rich(console, response)
|
|
275
275
|
|
|
276
|
-
elif isinstance(response, LineageResponse):
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
console.print(response.diagram)
|
|
281
|
-
else:
|
|
282
|
-
console.print(response.narrative or f"Lignage ({response.mode}) — colonne : {response.column}")
|
|
283
|
-
|
|
284
|
-
elif isinstance(response, QualityResponse):
|
|
285
|
-
if response.error:
|
|
286
|
-
console.print(f"[red]{response.error}[/red]")
|
|
287
|
-
else:
|
|
288
|
-
console.print(response.text_output or response.narrative or "Checks exécutés.")
|
|
289
|
-
|
|
290
|
-
elif isinstance(response, DictionaryResponse):
|
|
291
|
-
if response.error:
|
|
292
|
-
console.print(f"[red]{response.error}[/red]")
|
|
293
|
-
else:
|
|
294
|
-
console.print(response.text_output or response.narrative or f"Champ : {response.column}")
|
|
276
|
+
elif isinstance(response, (LineageResponse, QualityResponse, DictionaryResponse)):
|
|
277
|
+
text = hub_response_to_text(response)
|
|
278
|
+
style = "red" if getattr(response, "error", None) else ""
|
|
279
|
+
console.print(f"[{style}]{text}[/{style}]" if style else text)
|
|
295
280
|
|
|
296
281
|
elif isinstance(response, BuilderResponse):
|
|
297
282
|
if not response.success:
|
|
@@ -305,8 +290,8 @@ def _render_response(console: "Console", response: object) -> None: # type: ign
|
|
|
305
290
|
console.print(str(response))
|
|
306
291
|
|
|
307
292
|
|
|
308
|
-
def
|
|
309
|
-
"""
|
|
293
|
+
def _render_agent_response_rich(console: object, response: object) -> None:
|
|
294
|
+
"""Render an AgentResponse with Rich formatting (tables, colours)."""
|
|
310
295
|
try:
|
|
311
296
|
from rich.table import Table as RichTable
|
|
312
297
|
from .agentic.models import ResponseFormat
|
|
@@ -324,7 +309,6 @@ def _render_agent_response(console: "Console", response: object) -> None: # typ
|
|
|
324
309
|
console.print(f" {result.kpi_label or ''}: [green]{result.kpi_value}[/green]")
|
|
325
310
|
|
|
326
311
|
elif fmt == ResponseFormat.TABLE:
|
|
327
|
-
# Essayer d'afficher via rich.Table si le DataFrame est disponible
|
|
328
312
|
data_df = getattr(result, "data", None)
|
|
329
313
|
if data_df is not None:
|
|
330
314
|
try:
|
|
@@ -15,6 +15,7 @@ import logging
|
|
|
15
15
|
from functools import reduce
|
|
16
16
|
from typing import TYPE_CHECKING, Any
|
|
17
17
|
|
|
18
|
+
from satisfactoscript.core.ir import _normalise_join_type
|
|
18
19
|
from satisfactoscript.core.registry import RuleRegistry
|
|
19
20
|
from satisfactoscript.core.sandbox import SandboxResolver
|
|
20
21
|
from satisfactoscript.core.rule_analyzer import RuleAnalyzer
|
|
@@ -340,7 +341,7 @@ class SchemaInterpreter:
|
|
|
340
341
|
|
|
341
342
|
on_l = key_l if isinstance(key_l, list) else [key_l]
|
|
342
343
|
on_r = key_r if isinstance(key_r, list) else [key_r]
|
|
343
|
-
join_type = j.get("type", "left")
|
|
344
|
+
join_type = _normalise_join_type(j.get("type", "left"))
|
|
344
345
|
|
|
345
346
|
logger.info(" -> [Join] %s JOIN %s(%s) -> %s(%s)", join_type.upper(), alias_l, on_l, alias_r, on_r)
|
|
346
347
|
df_to = dfs[alias_r]
|
|
@@ -11,6 +11,21 @@ from __future__ import annotations
|
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
VALID_JOIN_TYPES: frozenset[str] = frozenset({"left", "right", "inner", "full", "cross"})
|
|
15
|
+
|
|
16
|
+
_JOIN_TYPE_ALIASES: dict[str, str] = {
|
|
17
|
+
"left outer": "left",
|
|
18
|
+
"right outer": "right",
|
|
19
|
+
"outer": "full",
|
|
20
|
+
"full outer": "full",
|
|
21
|
+
"full_outer": "full",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalise_join_type(raw: str) -> str:
|
|
26
|
+
key = raw.strip().lower().replace("-", " ")
|
|
27
|
+
return _JOIN_TYPE_ALIASES.get(key, key)
|
|
28
|
+
|
|
14
29
|
|
|
15
30
|
# ---------------------------------------------------------------------------
|
|
16
31
|
# Leaf types
|
|
@@ -180,12 +195,19 @@ def parse_to_ir(schema_dict: dict) -> ParsedSchema:
|
|
|
180
195
|
else:
|
|
181
196
|
alias_r = table_to
|
|
182
197
|
key_r = j.get("on_to")
|
|
198
|
+
raw_type = j.get("type", "left")
|
|
199
|
+
join_type = _normalise_join_type(raw_type)
|
|
200
|
+
if join_type not in VALID_JOIN_TYPES:
|
|
201
|
+
raise ValueError(
|
|
202
|
+
f"Invalid join type {raw_type!r} for join {alias_l!r} → {alias_r!r}. "
|
|
203
|
+
f"Valid types: {', '.join(sorted(VALID_JOIN_TYPES))}"
|
|
204
|
+
)
|
|
183
205
|
joins.append(ParsedJoin(
|
|
184
206
|
alias_left=alias_l,
|
|
185
207
|
keys_left=key_l if isinstance(key_l, list) else [key_l],
|
|
186
208
|
alias_right=alias_r,
|
|
187
209
|
keys_right=key_r if isinstance(key_r, list) else [key_r],
|
|
188
|
-
join_type=
|
|
210
|
+
join_type=join_type,
|
|
189
211
|
))
|
|
190
212
|
|
|
191
213
|
# --- select_final / add_columns ---
|
{satisfactoscript-1.0.0 → satisfactoscript-1.1.0}/src/satisfactoscript/semantic/llm_provider.py
RENAMED
|
@@ -261,6 +261,103 @@ class GoogleProvider(LLMProvider):
|
|
|
261
261
|
return response.text
|
|
262
262
|
|
|
263
263
|
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# Databricks Foundation Models
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
class DatabricksProvider(LLMProvider):
|
|
269
|
+
"""
|
|
270
|
+
LLM provider for Databricks Foundation Models API (OpenAI-compatible).
|
|
271
|
+
|
|
272
|
+
The Databricks Foundation Models API is OpenAI-compatible and is exposed at
|
|
273
|
+
``{DATABRICKS_HOST}/serving-endpoints``. This provider wraps the OpenAI client
|
|
274
|
+
with the correct base_url and uses a Databricks PAT as the API key.
|
|
275
|
+
|
|
276
|
+
Configuration via environment variables:
|
|
277
|
+
DATABRICKS_HOST — workspace URL (e.g. https://adb-xxx.azuredatabricks.net)
|
|
278
|
+
DATABRICKS_TOKEN — personal access token or service principal secret
|
|
279
|
+
DATABRICKS_LLM_MODEL — serving endpoint name (default: databricks-dbrx-instruct)
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __init__(
|
|
283
|
+
self,
|
|
284
|
+
host: str | None = None,
|
|
285
|
+
token: str | None = None,
|
|
286
|
+
model: str | None = None,
|
|
287
|
+
**kwargs: Any,
|
|
288
|
+
) -> None:
|
|
289
|
+
try:
|
|
290
|
+
import openai as _openai
|
|
291
|
+
except ImportError as exc:
|
|
292
|
+
raise ImportError(
|
|
293
|
+
"[LLMProvider] 'openai' is required for DatabricksProvider. "
|
|
294
|
+
"Install it with: pip install openai"
|
|
295
|
+
) from exc
|
|
296
|
+
|
|
297
|
+
resolved_host = host or os.environ.get("DATABRICKS_HOST", "")
|
|
298
|
+
resolved_token = token or os.environ.get("DATABRICKS_TOKEN", "")
|
|
299
|
+
|
|
300
|
+
if not resolved_host:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
"[DatabricksProvider] DATABRICKS_HOST is not set. "
|
|
303
|
+
"Set it to your workspace URL (e.g. https://adb-xxx.azuredatabricks.net)."
|
|
304
|
+
)
|
|
305
|
+
if not resolved_token:
|
|
306
|
+
raise ValueError(
|
|
307
|
+
"[DatabricksProvider] DATABRICKS_TOKEN is not set. "
|
|
308
|
+
"Set it to a personal access token or service principal secret."
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
base_url = resolved_host.rstrip("/") + "/serving-endpoints"
|
|
312
|
+
self._client = _openai.OpenAI(
|
|
313
|
+
api_key=resolved_token,
|
|
314
|
+
base_url=base_url,
|
|
315
|
+
)
|
|
316
|
+
self._model = model or os.environ.get(
|
|
317
|
+
"DATABRICKS_LLM_MODEL", "databricks-dbrx-instruct"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def provider_name(self) -> str:
|
|
322
|
+
return "databricks"
|
|
323
|
+
|
|
324
|
+
def complete(
|
|
325
|
+
self,
|
|
326
|
+
system_prompt: str,
|
|
327
|
+
user_message: str,
|
|
328
|
+
temperature: float = 0.0,
|
|
329
|
+
response_format: dict | None = None,
|
|
330
|
+
**kwargs: Any,
|
|
331
|
+
) -> str:
|
|
332
|
+
return self.complete_with_history(
|
|
333
|
+
system_prompt=system_prompt,
|
|
334
|
+
history=[{"role": "user", "content": user_message}],
|
|
335
|
+
temperature=temperature,
|
|
336
|
+
response_format=response_format,
|
|
337
|
+
**kwargs,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def complete_with_history(
|
|
341
|
+
self,
|
|
342
|
+
system_prompt: str,
|
|
343
|
+
history: list[dict],
|
|
344
|
+
temperature: float = 0.0,
|
|
345
|
+
response_format: dict | None = None,
|
|
346
|
+
**kwargs: Any,
|
|
347
|
+
) -> str:
|
|
348
|
+
call_kwargs: dict[str, Any] = {
|
|
349
|
+
"model": kwargs.pop("model", self._model),
|
|
350
|
+
"messages": [{"role": "system", "content": system_prompt}] + history,
|
|
351
|
+
"temperature": temperature,
|
|
352
|
+
}
|
|
353
|
+
if response_format:
|
|
354
|
+
call_kwargs["response_format"] = response_format
|
|
355
|
+
call_kwargs.update(kwargs)
|
|
356
|
+
|
|
357
|
+
response = self._client.chat.completions.create(**call_kwargs)
|
|
358
|
+
return response.choices[0].message.content
|
|
359
|
+
|
|
360
|
+
|
|
264
361
|
# ---------------------------------------------------------------------------
|
|
265
362
|
# Factory
|
|
266
363
|
# ---------------------------------------------------------------------------
|
|
@@ -269,6 +366,7 @@ _PROVIDER_MAP = {
|
|
|
269
366
|
"openai": OpenAIProvider,
|
|
270
367
|
"anthropic": AnthropicProvider,
|
|
271
368
|
"google": GoogleProvider,
|
|
369
|
+
"databricks": DatabricksProvider,
|
|
272
370
|
}
|
|
273
371
|
|
|
274
372
|
|
|
@@ -283,8 +381,9 @@ def get_llm_provider(
|
|
|
283
381
|
1. Argument ``provider`` explicite.
|
|
284
382
|
2. Variable d'environnement ``LLM_PROVIDER``.
|
|
285
383
|
3. Auto-détection depuis les clés API présentes dans l'environnement
|
|
286
|
-
(
|
|
287
|
-
|
|
384
|
+
(DATABRICKS_HOST+DATABRICKS_TOKEN → databricks, ANTHROPIC_API_KEY → anthropic,
|
|
385
|
+
OPENAI_API_KEY → openai, GOOGLE_API_KEY → google).
|
|
386
|
+
4. Erreur si aucune clé détectée.
|
|
288
387
|
|
|
289
388
|
Args:
|
|
290
389
|
provider: Nom du provider parmi {'openai', 'anthropic', 'google'}.
|
|
@@ -314,6 +413,8 @@ def get_llm_provider(
|
|
|
314
413
|
|
|
315
414
|
def _auto_detect_provider() -> str:
|
|
316
415
|
"""Détecte le provider depuis les clés API présentes dans l'environnement."""
|
|
416
|
+
if os.environ.get("DATABRICKS_HOST") and os.environ.get("DATABRICKS_TOKEN"):
|
|
417
|
+
return "databricks"
|
|
317
418
|
if os.environ.get("ANTHROPIC_API_KEY"):
|
|
318
419
|
return "anthropic"
|
|
319
420
|
if os.environ.get("OPENAI_API_KEY"):
|
|
@@ -322,6 +423,6 @@ def _auto_detect_provider() -> str:
|
|
|
322
423
|
return "google"
|
|
323
424
|
raise ValueError(
|
|
324
425
|
"[LLMProvider] Aucune clé API détectée. "
|
|
325
|
-
"Définissez ANTHROPIC_API_KEY,
|
|
326
|
-
"dans votre .env ou passez provider= explicitement."
|
|
426
|
+
"Définissez DATABRICKS_HOST+DATABRICKS_TOKEN, ANTHROPIC_API_KEY, "
|
|
427
|
+
"OPENAI_API_KEY ou GOOGLE_API_KEY dans votre .env ou passez provider= explicitement."
|
|
327
428
|
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
satisfactoscript.serving — MLflow ChatModel serving layer.
|
|
3
|
+
|
|
4
|
+
Exposes AgenticHub as an OpenAI-compatible REST endpoint on Databricks Model Serving.
|
|
5
|
+
|
|
6
|
+
Public exports:
|
|
7
|
+
SatisfactoChatModel — mlflow.pyfunc.ChatModel wrapping AgenticHub
|
|
8
|
+
hub_response_to_text — convert any HubResponse to a plain text string
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ._response_serializer import hub_response_to_text
|
|
12
|
+
|
|
13
|
+
__all__ = ["SatisfactoChatModel", "hub_response_to_text"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def __getattr__(name: str): # noqa: N807
|
|
17
|
+
if name == "SatisfactoChatModel":
|
|
18
|
+
from .chat_model import SatisfactoChatModel # noqa: PLC0415
|
|
19
|
+
return SatisfactoChatModel
|
|
20
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hub_response_to_text — converts any HubResponse to a plain text string.
|
|
3
|
+
|
|
4
|
+
Shared by the CLI renderer and the MLflow serving layer so serialization
|
|
5
|
+
logic is not duplicated.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def hub_response_to_text(response: Any) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Convert any HubResponse dataclass to a plain text string.
|
|
16
|
+
|
|
17
|
+
Handles AgentResponse, LineageResponse, QualityResponse,
|
|
18
|
+
DictionaryResponse, and BuilderResponse. Falls back to str() for
|
|
19
|
+
any unrecognised type.
|
|
20
|
+
"""
|
|
21
|
+
from satisfactoscript.agentic.models import (
|
|
22
|
+
AgentResponse,
|
|
23
|
+
BuilderResponse,
|
|
24
|
+
DictionaryResponse,
|
|
25
|
+
LineageResponse,
|
|
26
|
+
QualityResponse,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if isinstance(response, AgentResponse):
|
|
30
|
+
return _serialize_agent_response(response)
|
|
31
|
+
|
|
32
|
+
if isinstance(response, LineageResponse):
|
|
33
|
+
if response.error:
|
|
34
|
+
return f"Error: {response.error}"
|
|
35
|
+
return response.diagram or response.narrative or f"Lineage ({response.mode}) — column: {response.column}"
|
|
36
|
+
|
|
37
|
+
if isinstance(response, QualityResponse):
|
|
38
|
+
if response.error:
|
|
39
|
+
return f"Error: {response.error}"
|
|
40
|
+
return response.text_output or response.narrative or "Quality checks executed."
|
|
41
|
+
|
|
42
|
+
if isinstance(response, DictionaryResponse):
|
|
43
|
+
if response.error:
|
|
44
|
+
return f"Error: {response.error}"
|
|
45
|
+
return response.text_output or response.narrative or f"Field: {response.column}"
|
|
46
|
+
|
|
47
|
+
if isinstance(response, BuilderResponse):
|
|
48
|
+
if not response.success:
|
|
49
|
+
return f"Error: {response.error}"
|
|
50
|
+
parts = [response.yaml_content]
|
|
51
|
+
if response.output_path:
|
|
52
|
+
parts.append(f"Saved to: {response.output_path}")
|
|
53
|
+
return "\n".join(parts)
|
|
54
|
+
|
|
55
|
+
return str(response)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _serialize_agent_response(response: Any) -> str:
|
|
59
|
+
"""Serialize an AgentResponse to plain text."""
|
|
60
|
+
from satisfactoscript.agentic.models import ResponseFormat
|
|
61
|
+
|
|
62
|
+
if response.mode == "error":
|
|
63
|
+
return f"Error: {response.error}"
|
|
64
|
+
|
|
65
|
+
if response.mode in ("ambiguous", "needs_clarification"):
|
|
66
|
+
return response.clarification_question or response.explanation or "Clarification needed."
|
|
67
|
+
|
|
68
|
+
result = getattr(response, "result", None)
|
|
69
|
+
if result is None:
|
|
70
|
+
return response.explanation or "Response received."
|
|
71
|
+
|
|
72
|
+
fmt = getattr(result, "format", None)
|
|
73
|
+
title = getattr(result, "title", "")
|
|
74
|
+
|
|
75
|
+
if fmt == ResponseFormat.KPI:
|
|
76
|
+
label = result.kpi_label or title or response.explanation or "Value"
|
|
77
|
+
return f"{label}: {result.kpi_value}"
|
|
78
|
+
|
|
79
|
+
if fmt == ResponseFormat.TABLE:
|
|
80
|
+
data = getattr(result, "data", None)
|
|
81
|
+
if data is not None:
|
|
82
|
+
try:
|
|
83
|
+
rows = data.limit(20).collect()
|
|
84
|
+
cols = data.columns
|
|
85
|
+
lines = [title] if title else []
|
|
86
|
+
lines.append(" | ".join(cols))
|
|
87
|
+
lines.append("-" * (len(" | ".join(cols))))
|
|
88
|
+
for row in rows:
|
|
89
|
+
lines.append(" | ".join(str(v) for v in row))
|
|
90
|
+
return "\n".join(lines)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
return title or "No data."
|
|
94
|
+
|
|
95
|
+
text = getattr(result, "text_summary", None) or response.explanation
|
|
96
|
+
return text or "Response received."
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SatisfactoChatModel — MLflow ChatModel wrapping AgenticHub.
|
|
3
|
+
|
|
4
|
+
Exposes AgenticHub.ask() as an OpenAI-compatible /chat/completions endpoint
|
|
5
|
+
on Databricks Model Serving. load_context() is called once at pod startup;
|
|
6
|
+
predict() handles each inbound request.
|
|
7
|
+
|
|
8
|
+
Expected artifacts at log time:
|
|
9
|
+
"config" — path to config.yaml
|
|
10
|
+
"models_dir" — path to the semantic models directory
|
|
11
|
+
|
|
12
|
+
Expected environment variables at serving time:
|
|
13
|
+
DATABRICKS_HOST — workspace URL
|
|
14
|
+
DATABRICKS_TOKEN — PAT or service principal secret
|
|
15
|
+
DATABRICKS_LLM_MODEL — Foundation Model endpoint name (optional)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SatisfactoChatModel:
|
|
27
|
+
"""
|
|
28
|
+
MLflow pyfunc ChatModel that delegates to AgenticHub.
|
|
29
|
+
|
|
30
|
+
Lazily imports mlflow so the class can be imported in environments
|
|
31
|
+
where mlflow is not installed (e.g. during unit tests that mock it).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def load_context(self, context: Any) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Called once when the serving endpoint pod starts.
|
|
37
|
+
|
|
38
|
+
Initialises SparkSession, ConfigurationManager, SatisfactoEngine,
|
|
39
|
+
SemanticEngine, DatabricksLLMProvider, and AgenticHub. All heavy
|
|
40
|
+
dependencies are imported here so the module itself stays importable
|
|
41
|
+
without them installed.
|
|
42
|
+
"""
|
|
43
|
+
from pyspark.sql import SparkSession
|
|
44
|
+
|
|
45
|
+
from satisfactoscript.agentic.hub import AgenticHub
|
|
46
|
+
from satisfactoscript.core.config import ConfigurationManager
|
|
47
|
+
from satisfactoscript.core.core import SatisfactoEngine
|
|
48
|
+
from satisfactoscript.semantic.llm_provider import get_llm_provider
|
|
49
|
+
from satisfactoscript.semantic.semantic import SemanticEngine
|
|
50
|
+
|
|
51
|
+
artifacts = getattr(context, "artifacts", {}) or {}
|
|
52
|
+
config_path = artifacts.get("config", "config.yaml")
|
|
53
|
+
models_dir = artifacts.get("models_dir", "semantic")
|
|
54
|
+
|
|
55
|
+
spark = SparkSession.builder.getOrCreate()
|
|
56
|
+
config_mgr = ConfigurationManager(config_path=config_path)
|
|
57
|
+
engine = SatisfactoEngine(spark, config_manager=config_mgr)
|
|
58
|
+
semantic = SemanticEngine(engine, models_dir=models_dir)
|
|
59
|
+
llm = get_llm_provider("databricks")
|
|
60
|
+
|
|
61
|
+
self._hub = AgenticHub(semantic_engine=semantic, llm_provider=llm)
|
|
62
|
+
logger.info("SatisfactoChatModel: AgenticHub initialised.")
|
|
63
|
+
|
|
64
|
+
def predict(
|
|
65
|
+
self,
|
|
66
|
+
context: Any,
|
|
67
|
+
messages: list[dict[str, Any]],
|
|
68
|
+
params: dict[str, Any] | None = None,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Handle a single chat completion request.
|
|
72
|
+
|
|
73
|
+
Extracts the last user message from the OpenAI-format messages list,
|
|
74
|
+
calls hub.ask(), and returns an OpenAI-compatible response dict.
|
|
75
|
+
"""
|
|
76
|
+
from satisfactoscript.serving._response_serializer import hub_response_to_text
|
|
77
|
+
|
|
78
|
+
question = _extract_last_user_message(messages)
|
|
79
|
+
response = self._hub.ask(question)
|
|
80
|
+
text = hub_response_to_text(response)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"choices": [
|
|
84
|
+
{
|
|
85
|
+
"index": 0,
|
|
86
|
+
"message": {"role": "assistant", "content": text},
|
|
87
|
+
"finish_reason": "stop",
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _extract_last_user_message(messages: list[dict[str, Any]]) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Return the content of the last message with role='user'.
|
|
96
|
+
|
|
97
|
+
Raises ValueError if no user message is found.
|
|
98
|
+
"""
|
|
99
|
+
for msg in reversed(messages):
|
|
100
|
+
if isinstance(msg, dict) and msg.get("role") == "user":
|
|
101
|
+
return str(msg.get("content", ""))
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"SatisfactoChatModel.predict(): no message with role='user' found in messages."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _build_mlflow_model() -> "SatisfactoChatModel": # noqa: F821
|
|
108
|
+
"""
|
|
109
|
+
Return a SatisfactoChatModel instance registered as an mlflow.pyfunc.ChatModel.
|
|
110
|
+
|
|
111
|
+
Registers the mlflow.pyfunc flavour so the returned object can be passed
|
|
112
|
+
directly to mlflow.pyfunc.log_model(python_model=...).
|
|
113
|
+
|
|
114
|
+
This is a factory rather than direct inheritance because mlflow is an
|
|
115
|
+
optional dependency — we avoid a top-level mlflow import.
|
|
116
|
+
"""
|
|
117
|
+
import mlflow
|
|
118
|
+
|
|
119
|
+
class _Registered(SatisfactoChatModel, mlflow.pyfunc.ChatModel):
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
return _Registered()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: satisfactoscript
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Declarative data engineering framework — multi-platform (Databricks, Snowflake, BigQuery).
|
|
5
5
|
Author-email: julhouba <houbartjulien80@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -43,6 +43,9 @@ Requires-Dist: google-generativeai>=0.5.0; extra == "llm-google"
|
|
|
43
43
|
Provides-Extra: semantic-pdf
|
|
44
44
|
Requires-Dist: fpdf2>=2.7.0; extra == "semantic-pdf"
|
|
45
45
|
Requires-Dist: matplotlib>=3.7.0; extra == "semantic-pdf"
|
|
46
|
+
Provides-Extra: serving
|
|
47
|
+
Requires-Dist: mlflow<3,>=2.12.0; extra == "serving"
|
|
48
|
+
Requires-Dist: openai>=1.0.0; extra == "serving"
|
|
46
49
|
Provides-Extra: semantic-full
|
|
47
50
|
Requires-Dist: openai>=1.0.0; extra == "semantic-full"
|
|
48
51
|
Requires-Dist: anthropic>=0.30.0; extra == "semantic-full"
|
|
@@ -68,6 +68,9 @@ src/satisfactoscript/semantic/glossary.py
|
|
|
68
68
|
src/satisfactoscript/semantic/llm_provider.py
|
|
69
69
|
src/satisfactoscript/semantic/semantic.py
|
|
70
70
|
src/satisfactoscript/semantic/validator.py
|
|
71
|
+
src/satisfactoscript/serving/__init__.py
|
|
72
|
+
src/satisfactoscript/serving/_response_serializer.py
|
|
73
|
+
src/satisfactoscript/serving/chat_model.py
|
|
71
74
|
src/satisfactoscript/sinks/__init__.py
|
|
72
75
|
src/satisfactoscript/sinks/jdbc.py
|
|
73
76
|
tests/test_agent.py
|
|
@@ -115,6 +118,8 @@ tests/test_sandbox.py
|
|
|
115
118
|
tests/test_schema_loader.py
|
|
116
119
|
tests/test_semantic_builder.py
|
|
117
120
|
tests/test_semantic_engine_catalog.py
|
|
121
|
+
tests/test_serving_chat_model.py
|
|
122
|
+
tests/test_serving_response_serializer.py
|
|
118
123
|
tests/test_sink_jdbc.py
|
|
119
124
|
tests/test_user_profile.py
|
|
120
125
|
tests/test_utils_logging.py
|
|
@@ -226,6 +226,14 @@ class TestSQLQuery:
|
|
|
226
226
|
assert "LEFT JOIN" in sql
|
|
227
227
|
assert "cust_id" in sql
|
|
228
228
|
|
|
229
|
+
def test_join_full_outer(self):
|
|
230
|
+
left = SQLQuery("`orders`")
|
|
231
|
+
right = SQLQuery("`customers`")
|
|
232
|
+
left._joins.append(("full", right, ["customer_id"]))
|
|
233
|
+
sql = left.to_sql()
|
|
234
|
+
assert "FULL OUTER JOIN" in sql
|
|
235
|
+
assert "customer_id" in sql
|
|
236
|
+
|
|
229
237
|
def test_qualify(self):
|
|
230
238
|
q = SQLQuery("`orders`")
|
|
231
239
|
q._qualify = "ROW_NUMBER() OVER (PARTITION BY `id` ORDER BY 1) = 1"
|
|
@@ -154,3 +154,36 @@ class TestProcessSchemaJoin:
|
|
|
154
154
|
# drop must receive both right-side key refs
|
|
155
155
|
joined.drop.assert_called_once_with(df_b["org_r"], df_b["date_r"])
|
|
156
156
|
df_b.columns.assert_not_called()
|
|
157
|
+
|
|
158
|
+
def test_full_outer_join_same_keys(self):
|
|
159
|
+
"""type: full → join(..., how='full') with list-join path."""
|
|
160
|
+
df_a = MagicMock(name="df_a")
|
|
161
|
+
df_b = MagicMock(name="df_b")
|
|
162
|
+
df_a.join.return_value = _joined_df()
|
|
163
|
+
|
|
164
|
+
schema = {
|
|
165
|
+
"join": [{"table_from": "a", "table_to": "b", "on_from": "id", "on_to": "id", "type": "full"}]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
engine = _make_engine()
|
|
169
|
+
engine.process_schema(schema, dataframes_in={"a": df_a, "b": df_b})
|
|
170
|
+
|
|
171
|
+
df_a.join.assert_called_once_with(df_b, on=["id"], how="full")
|
|
172
|
+
|
|
173
|
+
def test_full_outer_join_different_keys(self):
|
|
174
|
+
"""type: full outer (alias) → expression join, right key dropped."""
|
|
175
|
+
df_a = MagicMock(name="df_a")
|
|
176
|
+
df_b = MagicMock(name="df_b")
|
|
177
|
+
joined = _joined_df()
|
|
178
|
+
df_a.join.return_value = joined
|
|
179
|
+
|
|
180
|
+
schema = {
|
|
181
|
+
"join": [{"table_from": "a", "table_to": "b", "on_from": "order_id", "on_to": "id", "type": "full outer"}]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
engine = _make_engine()
|
|
185
|
+
engine.process_schema(schema, dataframes_in={"a": df_a, "b": df_b})
|
|
186
|
+
|
|
187
|
+
_, join_kwargs = df_a.join.call_args
|
|
188
|
+
assert join_kwargs.get("how") == "full"
|
|
189
|
+
joined.drop.assert_called_once_with(df_b["id"])
|