semql-prompt 0.3.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.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Nikhil Pallamreddy
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: semql-prompt
3
+ Version: 0.3.0
4
+ Summary: LLM-facing prompt rendering for a semql Catalog: the four-role planner/router/presenter/drilldown fragments, cacheable prompt segments, tool-description projection, and prompt-token budgeting.
5
+ Author: Nikhil Pallamreddy
6
+ Author-email: Nikhil Pallamreddy <nikhil.pallamreddy+git@gmail.com>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Database
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: semql>=0.3.0,<0.4
19
+ Requires-Python: >=3.12
20
+ Project-URL: Homepage, https://github.com/npalladium/semql
21
+ Project-URL: Repository, https://github.com/npalladium/semql
22
+ Project-URL: Issues, https://github.com/npalladium/semql/issues
23
+ Description-Content-Type: text/markdown
24
+
25
+ # semql-prompt
26
+
27
+ LLM-facing prompt rendering for a [semql](https://github.com/npalladium/semql)
28
+ `Catalog`.
29
+
30
+ `semql`'s compiler is pure — it turns a `SemanticQuery` into SQL and never
31
+ renders a prompt. This package is the rendering layer on top:
32
+
33
+ - **Four-role prompt fragments** — `build_planner_prompt_fragment`,
34
+ `build_router_prompt_fragment`, `build_presenter_prompt_fragment`,
35
+ `build_drilldown_prompt_fragment`, `build_query_generator_prompt_fragment`.
36
+ - **Cacheable segments** — `CatalogPrompt` (viewer-invariant `static` +
37
+ per-viewer `overlay`) for prompt-cache breakpoints, plus `prompt_hash`.
38
+ - **Tool-description projection** — `to_openai_tools` / `to_langchain_tools`
39
+ / `to_openai_function` for function-calling clients.
40
+ - **Prompt-token budgeting** — `PromptBudget`, `apply_budget`,
41
+ `estimate_tokens`.
42
+
43
+ ## Install
44
+
45
+ ```sh
46
+ pip install semql-prompt
47
+ ```
48
+
49
+ ## Quick start
50
+
51
+ The catalog-level conveniences take the catalog as their first argument:
52
+
53
+ ```python
54
+ from semql import Catalog
55
+ from semql_prompt import planner_prompt, planner_prompt_segments, prompt_hash, to_openai_tools
56
+
57
+ text = planner_prompt(catalog, viewer=viewer)
58
+ segs = planner_prompt_segments(catalog)
59
+ key = prompt_hash(catalog)
60
+ tools = to_openai_tools(catalog, viewer=viewer)
61
+ ```
62
+
63
+ For the lower-level per-role fragment builders, see
64
+ [API reference](../../docs/api/semql_prompt.md).
65
+
66
+ ## License
67
+
68
+ BSD-3-Clause.
@@ -0,0 +1,44 @@
1
+ # semql-prompt
2
+
3
+ LLM-facing prompt rendering for a [semql](https://github.com/npalladium/semql)
4
+ `Catalog`.
5
+
6
+ `semql`'s compiler is pure — it turns a `SemanticQuery` into SQL and never
7
+ renders a prompt. This package is the rendering layer on top:
8
+
9
+ - **Four-role prompt fragments** — `build_planner_prompt_fragment`,
10
+ `build_router_prompt_fragment`, `build_presenter_prompt_fragment`,
11
+ `build_drilldown_prompt_fragment`, `build_query_generator_prompt_fragment`.
12
+ - **Cacheable segments** — `CatalogPrompt` (viewer-invariant `static` +
13
+ per-viewer `overlay`) for prompt-cache breakpoints, plus `prompt_hash`.
14
+ - **Tool-description projection** — `to_openai_tools` / `to_langchain_tools`
15
+ / `to_openai_function` for function-calling clients.
16
+ - **Prompt-token budgeting** — `PromptBudget`, `apply_budget`,
17
+ `estimate_tokens`.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ pip install semql-prompt
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ The catalog-level conveniences take the catalog as their first argument:
28
+
29
+ ```python
30
+ from semql import Catalog
31
+ from semql_prompt import planner_prompt, planner_prompt_segments, prompt_hash, to_openai_tools
32
+
33
+ text = planner_prompt(catalog, viewer=viewer)
34
+ segs = planner_prompt_segments(catalog)
35
+ key = prompt_hash(catalog)
36
+ tools = to_openai_tools(catalog, viewer=viewer)
37
+ ```
38
+
39
+ For the lower-level per-role fragment builders, see
40
+ [API reference](../../docs/api/semql_prompt.md).
41
+
42
+ ## License
43
+
44
+ BSD-3-Clause.
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "semql-prompt"
3
+ version = "0.3.0"
4
+ description = "LLM-facing prompt rendering for a semql Catalog: the four-role planner/router/presenter/drilldown fragments, cacheable prompt segments, tool-description projection, and prompt-token budgeting."
5
+ readme = "README.md"
6
+ license = "BSD-3-Clause"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Nikhil Pallamreddy", email = "nikhil.pallamreddy+git@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.12"
12
+ dependencies = [
13
+ "semql>=0.3.0,<0.4",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Database",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Typing :: Typed",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/npalladium/semql"
29
+ Repository = "https://github.com/npalladium/semql"
30
+ Issues = "https://github.com/npalladium/semql/issues"
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.11.19,<0.12.0"]
34
+ build-backend = "uv_build"
35
+
36
+ [tool.uv.sources]
37
+ semql = { workspace = true, editable = true }
@@ -0,0 +1,78 @@
1
+ """Public surface of the semql-prompt package.
2
+
3
+ The LLM-facing rendering layer for a semql ``Catalog``: the typed
4
+ four-role planner / router / presenter / drilldown prompt fragments, the
5
+ cacheable two-segment ``CatalogPrompt``, tool-description projection
6
+ (OpenAI / LangChain), and prompt-token budgeting.
7
+
8
+ The ``semql`` compiler emits SQL and never renders a prompt; that lives
9
+ here. Catalog-level conveniences (``planner_prompt`` /
10
+ ``planner_prompt_segments`` / ``prompt_hash`` / ``to_openai_tools`` /
11
+ ``to_langchain_tools``) are functions taking the catalog as their first
12
+ argument.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from semql_prompt.catalog_tools import (
18
+ planner_prompt,
19
+ planner_prompt_segments,
20
+ prompt_hash,
21
+ to_langchain_tools,
22
+ to_openai_tools,
23
+ )
24
+ from semql_prompt.prompt import (
25
+ CatalogPrompt,
26
+ ToolDescriptionProjection,
27
+ build_drilldown_prompt_fragment,
28
+ build_planner_prompt_fragment,
29
+ build_planner_prompt_segments,
30
+ build_presenter_prompt_fragment,
31
+ build_query_generator_prompt_fragment,
32
+ build_router_prompt_fragment,
33
+ catalog_prompt_hash,
34
+ filter_tool_descriptions,
35
+ project_tool_descriptions,
36
+ render_catalog_block,
37
+ render_catalog_segments,
38
+ render_saved_query_tool_description,
39
+ render_tool_description,
40
+ to_openai_function,
41
+ )
42
+ from semql_prompt.prompt_budget import (
43
+ BudgetResult,
44
+ PromptBudget,
45
+ apply_budget,
46
+ estimate_tokens,
47
+ )
48
+
49
+ __all__ = [
50
+ # prompt fragments + rendering
51
+ "CatalogPrompt",
52
+ "ToolDescriptionProjection",
53
+ "build_drilldown_prompt_fragment",
54
+ "build_planner_prompt_fragment",
55
+ "build_planner_prompt_segments",
56
+ "build_presenter_prompt_fragment",
57
+ "build_query_generator_prompt_fragment",
58
+ "build_router_prompt_fragment",
59
+ "catalog_prompt_hash",
60
+ "filter_tool_descriptions",
61
+ "project_tool_descriptions",
62
+ "render_catalog_block",
63
+ "render_catalog_segments",
64
+ "render_saved_query_tool_description",
65
+ "render_tool_description",
66
+ "to_openai_function",
67
+ # token budgeting
68
+ "BudgetResult",
69
+ "PromptBudget",
70
+ "apply_budget",
71
+ "estimate_tokens",
72
+ # catalog-level conveniences
73
+ "planner_prompt",
74
+ "planner_prompt_segments",
75
+ "prompt_hash",
76
+ "to_langchain_tools",
77
+ "to_openai_tools",
78
+ ]
@@ -0,0 +1,257 @@
1
+ """Catalog-level prompt conveniences.
2
+
3
+ Each takes the catalog as its first argument and reads it through the
4
+ public surface (``catalog.as_dict()``, ``catalog.policy``,
5
+ ``catalog.views`` / ``.lookups`` / ``.glossary`` / ``.relations`` /
6
+ ``.saved_queries``)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from semql.model import AuthContext, ResolutionContext
13
+
14
+ from semql_prompt.prompt import (
15
+ CatalogPrompt,
16
+ build_planner_prompt_segments,
17
+ catalog_prompt_hash,
18
+ project_tool_descriptions,
19
+ render_saved_query_tool_description,
20
+ render_tool_description,
21
+ to_openai_function,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from semql import Catalog
26
+ from semql.hooks import CubePromptHook
27
+ from semql.model import Cube
28
+ from semql.retrieve import Retriever
29
+ from semql.spec import SavedQuery
30
+
31
+
32
+ def planner_prompt(
33
+ catalog: Catalog,
34
+ *,
35
+ only_exposed: bool = True,
36
+ include_introspection: bool = False,
37
+ viewer: AuthContext | None = None,
38
+ ctx: ResolutionContext | None = None,
39
+ user_query: str | None = None,
40
+ retriever: Retriever | None = None,
41
+ top_k: int = 10,
42
+ retrieval_threshold: int = 50,
43
+ current_date: str | None = None,
44
+ retrieved_snippets: list[str] | None = None,
45
+ extra: str | None = None,
46
+ cube_prompt_hooks: list[CubePromptHook] | None = None,
47
+ ) -> str:
48
+ """Render the planner prompt fragment for ``catalog``.
49
+
50
+ When ``viewer`` is provided, the catalog block shrinks to the cubes
51
+ the viewer is allowed to see. ``ctx`` is the resolution context for
52
+ dimension-value lookups. Retrieval mode narrows the block to the
53
+ top-``top_k`` cubes when ``user_query`` + ``retriever`` are set and the
54
+ catalog exceeds ``retrieval_threshold`` questions."""
55
+ segments = build_planner_prompt_segments(
56
+ catalog.as_dict(),
57
+ only_exposed=only_exposed,
58
+ include_introspection=include_introspection,
59
+ views=catalog.views,
60
+ viewer=viewer,
61
+ policy=catalog.policy,
62
+ lookups=catalog.lookups,
63
+ ctx=ctx,
64
+ glossary=catalog.glossary,
65
+ relations=catalog.relations,
66
+ user_query=user_query,
67
+ retriever=retriever,
68
+ top_k=top_k,
69
+ retrieval_threshold=retrieval_threshold,
70
+ saved_queries=list(catalog.saved_queries.values()),
71
+ cube_prompt_hooks=cube_prompt_hooks,
72
+ )
73
+ return segments.full(
74
+ current_date=current_date,
75
+ retrieved_snippets=retrieved_snippets,
76
+ extra=extra,
77
+ )
78
+
79
+
80
+ def planner_prompt_segments(
81
+ catalog: Catalog,
82
+ *,
83
+ only_exposed: bool = True,
84
+ include_introspection: bool = False,
85
+ viewer: AuthContext | None = None,
86
+ ctx: ResolutionContext | None = None,
87
+ user_query: str | None = None,
88
+ retriever: Retriever | None = None,
89
+ top_k: int = 10,
90
+ retrieval_threshold: int = 50,
91
+ ) -> CatalogPrompt:
92
+ """Render the planner prompt as a cacheable two-segment object — a
93
+ viewer-invariant ``static`` segment plus a per-viewer ``overlay``."""
94
+ return build_planner_prompt_segments(
95
+ catalog.as_dict(),
96
+ only_exposed=only_exposed,
97
+ include_introspection=include_introspection,
98
+ views=catalog.views,
99
+ viewer=viewer,
100
+ policy=catalog.policy,
101
+ lookups=catalog.lookups,
102
+ ctx=ctx,
103
+ glossary=catalog.glossary,
104
+ relations=catalog.relations,
105
+ user_query=user_query,
106
+ retriever=retriever,
107
+ top_k=top_k,
108
+ retrieval_threshold=retrieval_threshold,
109
+ saved_queries=list(catalog.saved_queries.values()),
110
+ )
111
+
112
+
113
+ def prompt_hash(
114
+ catalog: Catalog,
115
+ *,
116
+ only_exposed: bool = True,
117
+ ctx: ResolutionContext | None = None,
118
+ ) -> str:
119
+ """SHA256 hex digest of the static (viewer-invariant) prompt segment —
120
+ stable across viewer changes, so it keys a prompt-fragment cache that a
121
+ catalog mutation invalidates."""
122
+ return catalog_prompt_hash(
123
+ catalog.as_dict(),
124
+ only_exposed=only_exposed,
125
+ lookups=catalog.lookups,
126
+ ctx=ctx,
127
+ glossary=catalog.glossary,
128
+ relations=catalog.relations,
129
+ )
130
+
131
+
132
+ def _visible_tool_targets(
133
+ catalog: Catalog,
134
+ *,
135
+ viewer: AuthContext | None,
136
+ only_exposed: bool,
137
+ ) -> tuple[list[Cube], list[SavedQuery]]:
138
+ """The cubes + saved queries a viewer may expose as tools.
139
+
140
+ The single visibility decision both :func:`to_openai_tools` and
141
+ :func:`to_langchain_tools` consume, so the two exporters can't drift:
142
+ cube gating runs through ``project_tool_descriptions`` (policy- and
143
+ role-aware), saved-query gating through ``required_roles`` (ANY-match,
144
+ the same rule the MCP server applies)."""
145
+ from semql.introspect import iter_cubes
146
+
147
+ by_name = catalog.as_dict()
148
+ proj = project_tool_descriptions(
149
+ by_name,
150
+ only_exposed=only_exposed,
151
+ viewer=viewer,
152
+ policy=catalog.policy,
153
+ )
154
+ visible_cubes = {**proj.invariant, **proj.viewer_gated}
155
+ cubes = [
156
+ c
157
+ for c in iter_cubes(
158
+ by_name,
159
+ include_meta=False,
160
+ only_exposed=only_exposed,
161
+ viewer=None, # proj already determined visibility
162
+ policy=None,
163
+ )
164
+ if c.name in visible_cubes
165
+ ]
166
+ saved = [
167
+ sq
168
+ for sq in catalog.saved_queries.values()
169
+ if not sq.required_roles
170
+ or (viewer is not None and any(r in viewer.roles for r in sq.required_roles))
171
+ ]
172
+ return cubes, saved
173
+
174
+
175
+ def to_openai_tools(
176
+ catalog: Catalog,
177
+ *,
178
+ viewer: AuthContext | None = None,
179
+ only_exposed: bool = True,
180
+ ) -> list[dict[str, Any]]:
181
+ """One OpenAI-format tool dict per visible cube + saved query.
182
+ Role-gated cubes / saved queries are excluded unless ``viewer`` holds a
183
+ matching role."""
184
+ from semql.spec import SemanticQuery
185
+
186
+ cubes, saved = _visible_tool_targets(catalog, viewer=viewer, only_exposed=only_exposed)
187
+ tools: list[dict[str, Any]] = [to_openai_function(c) for c in cubes]
188
+ for sq in saved:
189
+ tools.append(
190
+ {
191
+ "type": "function",
192
+ "function": {
193
+ "name": f"saved_{sq.name}",
194
+ "description": render_saved_query_tool_description(sq),
195
+ "parameters": SemanticQuery.model_json_schema(),
196
+ },
197
+ }
198
+ )
199
+ return tools
200
+
201
+
202
+ def to_langchain_tools(
203
+ catalog: Catalog,
204
+ *,
205
+ viewer: AuthContext | None = None,
206
+ only_exposed: bool = True,
207
+ ) -> list[object]:
208
+ """One LangChain ``StructuredTool`` per visible cube + saved query.
209
+ Requires ``langchain-core``. Cube tools compile a caller-supplied
210
+ ``SemanticQuery``; saved-query tools are zero-arg and run the baked
211
+ query — matching the cube/saved-query split of :func:`to_openai_tools`
212
+ via the shared :func:`_visible_tool_targets`."""
213
+ from typing import cast
214
+
215
+ try:
216
+ from langchain_core.tools import StructuredTool # type: ignore[import-not-found]
217
+ except ImportError:
218
+ raise ImportError(
219
+ "langchain-core is required for to_langchain_tools(). "
220
+ "Install it with: pip install langchain-core"
221
+ ) from None
222
+ structured_tool_cls = cast(Any, StructuredTool)
223
+
224
+ from semql.spec import SemanticQuery
225
+
226
+ cubes, saved = _visible_tool_targets(catalog, viewer=viewer, only_exposed=only_exposed)
227
+ tools: list[object] = []
228
+ for cube in cubes:
229
+ _cube_ref = cube
230
+
231
+ def _run(query: SemanticQuery, *, _c: object = _cube_ref) -> dict[str, object]:
232
+ compiled = catalog.compile(query, viewer=viewer)
233
+ return {"sql": compiled.sql, "params": compiled.params}
234
+
235
+ tools.append(
236
+ structured_tool_cls.from_function(
237
+ func=_run,
238
+ name=f"query_{cube.name}",
239
+ description=render_tool_description(cube),
240
+ args_schema=SemanticQuery,
241
+ )
242
+ )
243
+ for sq in saved:
244
+ _sq_ref = sq
245
+
246
+ def _run_saved(*, _q: SavedQuery = _sq_ref) -> dict[str, object]:
247
+ compiled = catalog.compile(_q.query, viewer=viewer)
248
+ return {"sql": compiled.sql, "params": compiled.params}
249
+
250
+ tools.append(
251
+ structured_tool_cls.from_function(
252
+ func=_run_saved,
253
+ name=f"saved_{sq.name}",
254
+ description=render_saved_query_tool_description(sq),
255
+ )
256
+ )
257
+ return tools