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.
- semql_prompt-0.3.0/LICENSE +28 -0
- semql_prompt-0.3.0/PKG-INFO +68 -0
- semql_prompt-0.3.0/README.md +44 -0
- semql_prompt-0.3.0/pyproject.toml +37 -0
- semql_prompt-0.3.0/src/semql_prompt/__init__.py +78 -0
- semql_prompt-0.3.0/src/semql_prompt/catalog_tools.py +257 -0
- semql_prompt-0.3.0/src/semql_prompt/prompt.py +1436 -0
- semql_prompt-0.3.0/src/semql_prompt/prompt_budget.py +265 -0
- semql_prompt-0.3.0/src/semql_prompt/py.typed +0 -0
|
@@ -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
|