langchain-timbr 1.5.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.
- langchain_timbr/__init__.py +17 -0
- langchain_timbr/config.py +21 -0
- langchain_timbr/langchain/__init__.py +16 -0
- langchain_timbr/langchain/execute_timbr_query_chain.py +307 -0
- langchain_timbr/langchain/generate_answer_chain.py +99 -0
- langchain_timbr/langchain/generate_timbr_sql_chain.py +176 -0
- langchain_timbr/langchain/identify_concept_chain.py +138 -0
- langchain_timbr/langchain/timbr_sql_agent.py +418 -0
- langchain_timbr/langchain/validate_timbr_sql_chain.py +187 -0
- langchain_timbr/langgraph/__init__.py +13 -0
- langchain_timbr/langgraph/execute_timbr_query_node.py +108 -0
- langchain_timbr/langgraph/generate_response_node.py +59 -0
- langchain_timbr/langgraph/generate_timbr_sql_node.py +98 -0
- langchain_timbr/langgraph/identify_concept_node.py +78 -0
- langchain_timbr/langgraph/validate_timbr_query_node.py +100 -0
- langchain_timbr/llm_wrapper/llm_wrapper.py +189 -0
- langchain_timbr/llm_wrapper/timbr_llm_wrapper.py +41 -0
- langchain_timbr/timbr_llm_connector.py +398 -0
- langchain_timbr/utils/general.py +70 -0
- langchain_timbr/utils/prompt_service.py +330 -0
- langchain_timbr/utils/temperature_supported_models.json +62 -0
- langchain_timbr/utils/timbr_llm_utils.py +575 -0
- langchain_timbr/utils/timbr_utils.py +475 -0
- langchain_timbr-1.5.0.dist-info/METADATA +103 -0
- langchain_timbr-1.5.0.dist-info/RECORD +27 -0
- langchain_timbr-1.5.0.dist-info/WHEEL +4 -0
- langchain_timbr-1.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
import time
|
|
3
|
+
from pytimbr_api import timbr_http_connector
|
|
4
|
+
from functools import wraps
|
|
5
|
+
|
|
6
|
+
from ..config import cache_timeout, ignore_tags, ignore_tags_prefix
|
|
7
|
+
from .general import to_boolean
|
|
8
|
+
|
|
9
|
+
# Cache dictionary
|
|
10
|
+
_cache = {}
|
|
11
|
+
_ontology_version = None
|
|
12
|
+
_last_version_check = 0
|
|
13
|
+
|
|
14
|
+
def clear_cache():
|
|
15
|
+
"""Clear the cache and reset the ontology version."""
|
|
16
|
+
global _cache, _ontology_version
|
|
17
|
+
# with cache_lock:
|
|
18
|
+
_cache.clear()
|
|
19
|
+
_ontology_version = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_ontology_version(conn_params) -> str:
|
|
23
|
+
"""Fetch the current ontology version."""
|
|
24
|
+
query = "SHOW VERSION"
|
|
25
|
+
res = run_query(query, conn_params)
|
|
26
|
+
return res[0].get("id") if res else "unknown"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _serialize_cache_key(*args, **kwargs):
|
|
30
|
+
"""Serialize arguments into a hashable cache key."""
|
|
31
|
+
def serialize(obj):
|
|
32
|
+
if isinstance(obj, dict):
|
|
33
|
+
return tuple(sorted((k, serialize(v)) for k, v in obj.items()))
|
|
34
|
+
elif isinstance(obj, list):
|
|
35
|
+
return tuple(serialize(x) for x in obj)
|
|
36
|
+
elif isinstance(obj, (str, int, float, bool, type(None))):
|
|
37
|
+
return obj
|
|
38
|
+
raise TypeError(f"Unsupported type for caching: {type(obj)}")
|
|
39
|
+
|
|
40
|
+
return (tuple(serialize(arg) for arg in args), tuple((k, serialize(v)) for k, v in kwargs.items()))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cache_with_version_check(func):
|
|
44
|
+
"""Decorator to cache function results and invalidate if ontology version changes."""
|
|
45
|
+
|
|
46
|
+
@wraps(func)
|
|
47
|
+
def wrapper(*args, **kwargs):
|
|
48
|
+
global _ontology_version, _last_version_check
|
|
49
|
+
|
|
50
|
+
now = time.time()
|
|
51
|
+
if (now - _last_version_check) > cache_timeout:
|
|
52
|
+
conn_params = kwargs.get("conn_params") or args[-1]
|
|
53
|
+
current_version = _get_ontology_version(conn_params)
|
|
54
|
+
|
|
55
|
+
# If version changed, clear cache and set new version
|
|
56
|
+
if _ontology_version != current_version:
|
|
57
|
+
clear_cache()
|
|
58
|
+
_ontology_version = current_version
|
|
59
|
+
|
|
60
|
+
_last_version_check = now
|
|
61
|
+
|
|
62
|
+
# Generate a cache key based on function name and arguments
|
|
63
|
+
cache_key = (func.__name__, _serialize_cache_key(*args, **kwargs))
|
|
64
|
+
if cache_key not in _cache or not _cache[cache_key]:
|
|
65
|
+
# Call the function and store the result in the cache
|
|
66
|
+
_cache[cache_key] = func(*args, **kwargs)
|
|
67
|
+
|
|
68
|
+
return _cache[cache_key]
|
|
69
|
+
|
|
70
|
+
return wrapper
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_query(sql: str, conn_params: dict, llm_prompt: Optional[str] = None) -> list[list]:
|
|
74
|
+
if not conn_params:
|
|
75
|
+
raise("Please provide connection params.")
|
|
76
|
+
|
|
77
|
+
query = sql
|
|
78
|
+
if llm_prompt:
|
|
79
|
+
clean_prompt = llm_prompt.replace('\r\n', ' ').replace('\n', ' ').replace('?', '')
|
|
80
|
+
query = f"-- LLM: {clean_prompt}\n{sql}"
|
|
81
|
+
|
|
82
|
+
results = timbr_http_connector.run_query(
|
|
83
|
+
query=query,
|
|
84
|
+
**conn_params,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return results
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_ontologies(conn_params: dict) -> list[str]:
|
|
91
|
+
query = "SELECT ontology FROM timbr.sys_ontologies"
|
|
92
|
+
res = run_query(query, conn_params)
|
|
93
|
+
return [row.get('ontology') for row in res]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_datasources(conn_params: dict, filter_active: Optional[bool] = False) -> list[dict]:
|
|
97
|
+
query = "SHOW DATASOURCES"
|
|
98
|
+
res = run_query(query, conn_params)
|
|
99
|
+
if filter_active:
|
|
100
|
+
res = [row for row in res if to_boolean(row.get('is_active'))]
|
|
101
|
+
|
|
102
|
+
return res
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def validate_sql(sql: str, conn_params: dict) -> tuple[bool, str]:
|
|
106
|
+
if not sql:
|
|
107
|
+
raise Exception("Please provide SQL to validate.")
|
|
108
|
+
|
|
109
|
+
explain_res = None
|
|
110
|
+
query_res = None
|
|
111
|
+
error = None
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
explain_sql = f"EXPLAIN {sql}"
|
|
115
|
+
explain_res = run_query(explain_sql, conn_params)
|
|
116
|
+
|
|
117
|
+
query_sql = f"SELECT * FROM ({sql.replace(';', '')}) explainable_query WHERE 1=0"
|
|
118
|
+
query_res = run_query(query_sql, conn_params)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
error = str(getattr(e, 'doc', e))
|
|
121
|
+
|
|
122
|
+
return to_boolean(explain_res and explain_res[0].get('PLAN') and query_res is not None), error
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _should_ignore_tag(tag_name: str) -> bool:
|
|
126
|
+
if not tag_name:
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
tag_name_lower = tag_name.lower()
|
|
130
|
+
if tag_name_lower in ignore_tags:
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
for prefix in ignore_tags_prefix:
|
|
134
|
+
if tag_name_lower.startswith(prefix.lower()):
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _prepare_tags_dict(
|
|
141
|
+
type: Optional[str] = 'concept',
|
|
142
|
+
tags_list: Optional[list] = [],
|
|
143
|
+
include_tags: Optional[str] = '',
|
|
144
|
+
) -> dict:
|
|
145
|
+
tags_dict = {}
|
|
146
|
+
if not include_tags:
|
|
147
|
+
return tags_dict # currently empty
|
|
148
|
+
|
|
149
|
+
for tag in tags_list:
|
|
150
|
+
# Make sure that the tag is of the correct type
|
|
151
|
+
if type != tag.get('target_type'):
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
tag_name = tag.get('tag_name')
|
|
155
|
+
|
|
156
|
+
# Check if the tag is included
|
|
157
|
+
if (include_tags != '*' and tag_name not in include_tags) or _should_ignore_tag(tag_name):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
key = tag.get('target_name')
|
|
161
|
+
tag_value = tag.get('tag_value')
|
|
162
|
+
|
|
163
|
+
if key not in tags_dict:
|
|
164
|
+
tags_dict[key] = {}
|
|
165
|
+
|
|
166
|
+
tags_dict[key][tag_name] = tag_value
|
|
167
|
+
|
|
168
|
+
return tags_dict
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@cache_with_version_check
|
|
172
|
+
def get_tags(conn_params: dict, include_tags: Optional[Any] = None) -> dict:
|
|
173
|
+
if not to_boolean(include_tags):
|
|
174
|
+
return {
|
|
175
|
+
"concept_tags": {},
|
|
176
|
+
"view_tags": {},
|
|
177
|
+
"property_tags": {},
|
|
178
|
+
# "relationship_tags": {},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
query = "SHOW TAGS"
|
|
182
|
+
ontology_tags = run_query(query, conn_params)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"concept_tags": _prepare_tags_dict('concept', ontology_tags, include_tags),
|
|
186
|
+
"view_tags": _prepare_tags_dict('ontology view', ontology_tags, include_tags),
|
|
187
|
+
"property_tags": _prepare_tags_dict('property', ontology_tags, include_tags),
|
|
188
|
+
# "relationship_tags": _prepare_tags_dict('relationship', ontology_tags, include_tags),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _should_ignore_list(list: list) -> bool:
|
|
193
|
+
return bool(list and len(list) == 1 and (list[0] == 'none' or list[0] == 'null'))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _should_select_all(list: list) -> bool:
|
|
197
|
+
return list and len(list) == 1 and list[0] == '*'
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@cache_with_version_check
|
|
201
|
+
def get_concepts(
|
|
202
|
+
conn_params,
|
|
203
|
+
concepts_list: Optional[list] = None,
|
|
204
|
+
views_list: Optional[list] = None,
|
|
205
|
+
include_logic_concepts: Optional[bool] = False,
|
|
206
|
+
) -> dict:
|
|
207
|
+
"""Fetch concepts (or views) from timbr.sys_concepts and/or timbr.sys_views."""
|
|
208
|
+
joined_views = ','.join(f"'{v}'" for v in views_list) if views_list else ''
|
|
209
|
+
should_ignore_concepts = _should_ignore_list(concepts_list)
|
|
210
|
+
should_ignore_views = _should_ignore_list(views_list)
|
|
211
|
+
|
|
212
|
+
filter_concepts = " WHERE concept IN (SELECT DISTINCT concept FROM timbr.sys_concept_properties)" if not include_logic_concepts else ""
|
|
213
|
+
if concepts_list:
|
|
214
|
+
if should_ignore_concepts:
|
|
215
|
+
filter_concepts = " WHERE 1 = 0"
|
|
216
|
+
elif _should_select_all(concepts_list):
|
|
217
|
+
filter_concepts = ""
|
|
218
|
+
else:
|
|
219
|
+
joined_concepts = ','.join(f"'{c}'" for c in concepts_list) if concepts_list else ''
|
|
220
|
+
filter_concepts = f" WHERE concept IN ({joined_concepts})" if concepts_list else ""
|
|
221
|
+
|
|
222
|
+
filter_views = f" WHERE view_name IN ({joined_views})" if views_list else ""
|
|
223
|
+
if should_ignore_views:
|
|
224
|
+
filter_views = " WHERE 1 = 0"
|
|
225
|
+
elif _should_select_all(views_list):
|
|
226
|
+
filter_views = ""
|
|
227
|
+
|
|
228
|
+
# if there is concepts_list and not views - filter only concepts
|
|
229
|
+
# if there is views_list and not concepts - filter only views
|
|
230
|
+
# if there is both or none - union the two tables
|
|
231
|
+
if concepts_list and not should_ignore_concepts and not views_list:
|
|
232
|
+
# Only fetch concepts
|
|
233
|
+
query = f"""
|
|
234
|
+
SELECT concept, description, 'false' AS is_view
|
|
235
|
+
FROM timbr.sys_concepts{filter_concepts}
|
|
236
|
+
ORDER BY is_view ASC
|
|
237
|
+
""".strip()
|
|
238
|
+
elif views_list and not should_ignore_views and not concepts_list:
|
|
239
|
+
# Only fetch views
|
|
240
|
+
query = f"""
|
|
241
|
+
SELECT view_name AS concept, description, 'true' AS is_view
|
|
242
|
+
FROM timbr.sys_views{filter_views}
|
|
243
|
+
ORDER BY is_view ASC
|
|
244
|
+
""".strip()
|
|
245
|
+
else:
|
|
246
|
+
# Both or neither => union the two tables (existing logic)
|
|
247
|
+
query = f"""
|
|
248
|
+
SELECT * FROM (
|
|
249
|
+
SELECT concept, description, 'false' AS is_view
|
|
250
|
+
FROM timbr.sys_concepts{filter_concepts}
|
|
251
|
+
UNION ALL
|
|
252
|
+
SELECT view_name AS concept, description, 'true' AS is_view
|
|
253
|
+
FROM timbr.sys_views{filter_views}
|
|
254
|
+
) AS combined
|
|
255
|
+
ORDER BY is_view ASC
|
|
256
|
+
""".strip()
|
|
257
|
+
|
|
258
|
+
res = run_query(query, conn_params)
|
|
259
|
+
uniq_concepts = {}
|
|
260
|
+
for row in res:
|
|
261
|
+
concept = row.get('concept')
|
|
262
|
+
if concept not in uniq_concepts and concept != 'thing':
|
|
263
|
+
uniq_concepts[concept] = row
|
|
264
|
+
|
|
265
|
+
return uniq_concepts
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _generate_column_relationship_description(column_name):
|
|
269
|
+
"""
|
|
270
|
+
Generates a concise description for a column used in text-to-SQL generation.
|
|
271
|
+
|
|
272
|
+
Expected column name formats:
|
|
273
|
+
relationship_name[target_concept].property_name
|
|
274
|
+
or
|
|
275
|
+
relationship_name[target_concept].relationship_name[target_concept].property_name
|
|
276
|
+
(and potentially more nested relationships)
|
|
277
|
+
|
|
278
|
+
For example:
|
|
279
|
+
"includes_product[product].contains[material].material_name"
|
|
280
|
+
|
|
281
|
+
Output example:
|
|
282
|
+
"This column represents the material name from table material using the relationship contains from table product from relationship includes product."
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
# Split the column name into parts using the period as a delimiter.
|
|
287
|
+
parts = column_name.split('.')
|
|
288
|
+
# The final part is the property name; replace underscores with spaces.
|
|
289
|
+
property_name = parts[-1].replace('_', ' ')
|
|
290
|
+
|
|
291
|
+
# Extract relationships (each part before the final property)
|
|
292
|
+
relationships = []
|
|
293
|
+
for part in parts[:-1]:
|
|
294
|
+
if '[' in part and ']' in part:
|
|
295
|
+
relationship, target_concept = part.split('[')
|
|
296
|
+
target_concept = target_concept.rstrip(']')
|
|
297
|
+
# Replace underscores with spaces.
|
|
298
|
+
relationship = relationship.replace('_', ' ')
|
|
299
|
+
target_concept = target_concept.replace('_', ' ')
|
|
300
|
+
relationships.append((relationship, target_concept))
|
|
301
|
+
|
|
302
|
+
col_type = "column"
|
|
303
|
+
if column_name.startswith("measure."):
|
|
304
|
+
col_type = "measure"
|
|
305
|
+
|
|
306
|
+
# Build the description.
|
|
307
|
+
if relationships:
|
|
308
|
+
# The final table is taken from the target of the last relationship.
|
|
309
|
+
final_table = relationships[-1][1]
|
|
310
|
+
description = f"This {col_type} represents the {property_name} from table {final_table}"
|
|
311
|
+
if len(relationships) == 1:
|
|
312
|
+
# Only one relationship in the chain.
|
|
313
|
+
description += f" using the relationship {relationships[0][0]}."
|
|
314
|
+
else:
|
|
315
|
+
# For two or more relationships:
|
|
316
|
+
# The last relationship is applied on the table from the previous relationship.
|
|
317
|
+
# For example, for two relationships:
|
|
318
|
+
# relationships[0] = ("includes product", "product")
|
|
319
|
+
# relationships[1] = ("contains", "material")
|
|
320
|
+
# We want: "using the relationship contains from table product from relationship includes product."
|
|
321
|
+
last_rel, _ = relationships[-1]
|
|
322
|
+
base_table = relationships[-2][1]
|
|
323
|
+
derivation = f" using the relationship {last_rel} from table {base_table}"
|
|
324
|
+
# For any additional relationships (if more than two), append them in order.
|
|
325
|
+
for i in range(len(relationships) - 2, -1, -1):
|
|
326
|
+
derivation += f" from relationship {relationships[i][0]}"
|
|
327
|
+
description += derivation + "."
|
|
328
|
+
else:
|
|
329
|
+
description = f"This {col_type} represents the {property_name}."
|
|
330
|
+
|
|
331
|
+
return description
|
|
332
|
+
except Exception as exp:
|
|
333
|
+
return ""
|
|
334
|
+
|
|
335
|
+
@cache_with_version_check
|
|
336
|
+
def get_relationships_description(conn_params: dict) -> dict:
|
|
337
|
+
"""Fetch relationships data."""
|
|
338
|
+
query = f"""
|
|
339
|
+
SELECT
|
|
340
|
+
relationship_name,
|
|
341
|
+
description
|
|
342
|
+
FROM `timbr`.`SYS_CONCEPT_RELATIONSHIPS`
|
|
343
|
+
WHERE description is not null
|
|
344
|
+
""".strip()
|
|
345
|
+
|
|
346
|
+
res = run_query(query, conn_params)
|
|
347
|
+
relationships_desc = {}
|
|
348
|
+
for row in res:
|
|
349
|
+
relationships_desc[row['relationship_name']] = row['description']
|
|
350
|
+
|
|
351
|
+
return relationships_desc
|
|
352
|
+
|
|
353
|
+
@cache_with_version_check
|
|
354
|
+
def get_properties_description(conn_params: dict) -> dict:
|
|
355
|
+
query = f"""
|
|
356
|
+
SELECT property_name, description
|
|
357
|
+
FROM `timbr`.`SYS_PROPERTIES`
|
|
358
|
+
WHERE description is not null
|
|
359
|
+
""".strip()
|
|
360
|
+
|
|
361
|
+
res = run_query(query, conn_params)
|
|
362
|
+
properties_desc = {}
|
|
363
|
+
for row in res:
|
|
364
|
+
properties_desc[row['property_name']] = row['description']
|
|
365
|
+
|
|
366
|
+
return properties_desc
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _add_relationship_column(
|
|
370
|
+
relationship_name: str,
|
|
371
|
+
relationship_desc: str,
|
|
372
|
+
col_dict: dict,
|
|
373
|
+
relationships: dict,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Add a column to the specified relationship."""
|
|
376
|
+
col_name = col_dict.get('name')
|
|
377
|
+
if col_name:
|
|
378
|
+
if relationship_name not in relationships:
|
|
379
|
+
is_transitive = '*' in col_name
|
|
380
|
+
relationships[relationship_name] = {
|
|
381
|
+
"relationship_name": relationship_name,
|
|
382
|
+
"description": relationship_desc,
|
|
383
|
+
"columns": [],
|
|
384
|
+
"measures": [],
|
|
385
|
+
"is_transitive": is_transitive,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if col_name.startswith('measure.'):
|
|
389
|
+
relationships[relationship_name]['measures'].append(col_dict)
|
|
390
|
+
else:
|
|
391
|
+
relationships[relationship_name]['columns'].append(col_dict)
|
|
392
|
+
|
|
393
|
+
@cache_with_version_check
|
|
394
|
+
def get_concept_properties(
|
|
395
|
+
concept_name: str,
|
|
396
|
+
conn_params: dict,
|
|
397
|
+
properties_desc: dict,
|
|
398
|
+
relationships_desc: dict,
|
|
399
|
+
schema: Optional[str] = 'dtimbr',
|
|
400
|
+
graph_depth: Optional[int] = 1,
|
|
401
|
+
) -> dict:
|
|
402
|
+
rows = []
|
|
403
|
+
desc_query = f"describe concept `{schema}`.`{concept_name}`"
|
|
404
|
+
if schema == 'dtimbr':
|
|
405
|
+
desc_query += f" options (graph_depth='{graph_depth}')"
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
rows = run_query(desc_query, conn_params)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
# skipping new describe concept syntax
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
if not rows:
|
|
414
|
+
legacy_desc_query = f"desc `{schema}`.`{concept_name}`"
|
|
415
|
+
try:
|
|
416
|
+
rows = run_query(legacy_desc_query, conn_params)
|
|
417
|
+
except Exception as e:
|
|
418
|
+
print(f"Error describing concept using legacy desc stmt: {e}")
|
|
419
|
+
|
|
420
|
+
relationships = {}
|
|
421
|
+
columns = []
|
|
422
|
+
measures = []
|
|
423
|
+
|
|
424
|
+
for column in rows:
|
|
425
|
+
col_name = column.get('col_name')
|
|
426
|
+
comment = properties_desc.get(col_name)
|
|
427
|
+
|
|
428
|
+
if col_name:
|
|
429
|
+
if "_type_of_" in col_name:
|
|
430
|
+
comment = f"if this value is 1, the row is of type {col_name.split('_type_of_')[1]}"
|
|
431
|
+
# elif (comment is None or comment == "") and "[" in col_name and "]" in col_name:
|
|
432
|
+
# comment = _generate_column_relationship_description(col_name)
|
|
433
|
+
elif col_name.startswith("~"):
|
|
434
|
+
rel_name = col_name[1:].split('[')[0]
|
|
435
|
+
comment = comment + "; " if comment else ''
|
|
436
|
+
comment = comment + f"This columns means the inverse of `{rel_name}`"
|
|
437
|
+
|
|
438
|
+
if "." in col_name and (comment is None or comment == ""):
|
|
439
|
+
comment = properties_desc.get(col_name.split(".")[-1])
|
|
440
|
+
|
|
441
|
+
if '[' in col_name:
|
|
442
|
+
# This is a relationship column
|
|
443
|
+
rel_path, rel_col_name = col_name.rsplit('.', 1) if '.' in col_name else col_name.rsplit('_', 1) if '_' in col_name else col_name
|
|
444
|
+
rel_name = rel_path.split('[', 1)[0]
|
|
445
|
+
|
|
446
|
+
if rel_name:
|
|
447
|
+
if rel_name.startswith('measure.'):
|
|
448
|
+
rel_name = rel_name.replace('measure.', '')
|
|
449
|
+
|
|
450
|
+
comment = properties_desc.get(rel_col_name, '')
|
|
451
|
+
|
|
452
|
+
rel_col_dict = {
|
|
453
|
+
'name': col_name,
|
|
454
|
+
'col_name': rel_col_name,
|
|
455
|
+
'type': column.get('data_type', 'string').lower(),
|
|
456
|
+
'data_type': column.get('data_type', 'string').lower(),
|
|
457
|
+
'comment': comment,
|
|
458
|
+
}
|
|
459
|
+
_add_relationship_column(
|
|
460
|
+
relationship_name=rel_name,
|
|
461
|
+
relationship_desc=relationships_desc.get(rel_name, ''),
|
|
462
|
+
col_dict=rel_col_dict,
|
|
463
|
+
relationships=relationships
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
elif col_name.startswith("measure."):
|
|
467
|
+
measures.append({ **column, 'comment': comment })
|
|
468
|
+
else:
|
|
469
|
+
columns.append({ **column, 'comment': comment })
|
|
470
|
+
return {
|
|
471
|
+
"columns": columns,
|
|
472
|
+
"measures": measures,
|
|
473
|
+
"relationships": relationships,
|
|
474
|
+
}
|
|
475
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-timbr
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: LangChain & LangGraph extensions that parse LLM prompts into Timbr semantic SQL and execute them.
|
|
5
|
+
Project-URL: Homepage, https://github.com/WPSemantix/langchain-timbr
|
|
6
|
+
Project-URL: Documentation, https://docs.timbr.ai/doc/docs/integration/langchain-sdk/
|
|
7
|
+
Project-URL: Source, https://github.com/WPSemantix/langchain-timbr
|
|
8
|
+
Project-URL: Issues, https://github.com/WPSemantix/langchain-timbr/issues
|
|
9
|
+
Author-email: Bar Cohen <barco@timbr.ai>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: Agents,Knowledge Graph,LLM,LangChain,LangGraph,SQL,Semantic Layer,Timbr
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: <3.13,>=3.9
|
|
23
|
+
Requires-Dist: cryptography>=44.0.3
|
|
24
|
+
Requires-Dist: langchain-community>=0.3.20
|
|
25
|
+
Requires-Dist: langchain-core>=0.3.58
|
|
26
|
+
Requires-Dist: langchain>=0.3.25
|
|
27
|
+
Requires-Dist: langgraph>=0.3.20
|
|
28
|
+
Requires-Dist: pydantic==2.10.4
|
|
29
|
+
Requires-Dist: pytimbr-api>=2.0.0
|
|
30
|
+
Requires-Dist: tiktoken==0.8.0
|
|
31
|
+
Requires-Dist: transformers>=4.51.3
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: anthropic==0.42.0; extra == 'all'
|
|
34
|
+
Requires-Dist: google-generativeai==0.8.4; extra == 'all'
|
|
35
|
+
Requires-Dist: langchain-anthropic>=0.3.1; extra == 'all'
|
|
36
|
+
Requires-Dist: langchain-google-genai>=2.0.9; extra == 'all'
|
|
37
|
+
Requires-Dist: langchain-openai>=0.3.16; extra == 'all'
|
|
38
|
+
Requires-Dist: langchain-tests>=0.3.20; extra == 'all'
|
|
39
|
+
Requires-Dist: openai==1.77.0; extra == 'all'
|
|
40
|
+
Requires-Dist: pyarrow<19.0.0; extra == 'all'
|
|
41
|
+
Requires-Dist: pytest==8.3.4; extra == 'all'
|
|
42
|
+
Requires-Dist: snowflake-snowpark-python>=1.6.0; extra == 'all'
|
|
43
|
+
Requires-Dist: snowflake>=0.8.0; extra == 'all'
|
|
44
|
+
Requires-Dist: uvicorn==0.34.0; extra == 'all'
|
|
45
|
+
Provides-Extra: anthropic
|
|
46
|
+
Requires-Dist: anthropic==0.42.0; extra == 'anthropic'
|
|
47
|
+
Requires-Dist: langchain-anthropic>=0.3.1; extra == 'anthropic'
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: langchain-tests>=0.3.20; extra == 'dev'
|
|
50
|
+
Requires-Dist: pyarrow<19.0.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest==8.3.4; extra == 'dev'
|
|
52
|
+
Requires-Dist: uvicorn==0.34.0; extra == 'dev'
|
|
53
|
+
Provides-Extra: google
|
|
54
|
+
Requires-Dist: google-generativeai==0.8.4; extra == 'google'
|
|
55
|
+
Requires-Dist: langchain-google-genai>=2.0.9; extra == 'google'
|
|
56
|
+
Provides-Extra: openai
|
|
57
|
+
Requires-Dist: langchain-openai>=0.3.16; extra == 'openai'
|
|
58
|
+
Requires-Dist: openai==1.77.0; extra == 'openai'
|
|
59
|
+
Provides-Extra: snowflake
|
|
60
|
+
Requires-Dist: snowflake-snowpark-python>=1.6.0; extra == 'snowflake'
|
|
61
|
+
Requires-Dist: snowflake>=0.8.0; extra == 'snowflake'
|
|
62
|
+
Description-Content-Type: text/markdown
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
[](https://app.fossa.com/projects/git%2Bgithub.com%2FWPSemantix%2Flangchain-timbr?ref=badge_shield&issueType=security)
|
|
67
|
+
[](https://app.fossa.com/projects/git%2Bgithub.com%2FWPSemantix%2Flangchain-timbr?ref=badge_shield&issueType=license)
|
|
68
|
+
|
|
69
|
+
[](https://www.python.org/downloads/release/python-3921/)
|
|
70
|
+
[](https://www.python.org/downloads/release/python-31017/)
|
|
71
|
+
[](https://www.python.org/downloads/release/python-31112/)
|
|
72
|
+
[](https://www.python.org/downloads/release/python-3129/)
|
|
73
|
+
|
|
74
|
+
# Timbr Langchain LLM SDK
|
|
75
|
+
|
|
76
|
+
Timbr langchain LLM SDK is a Python SDK that extends LangChain and LangGraph with custom agents, chains, and nodes for seamless integration with the Timbr semantic layer. It enables converting natural language prompts into optimized semantic-SQL queries and executing them directly against your data.
|
|
77
|
+
|
|
78
|
+
## Dependencies
|
|
79
|
+
- Access to a timbr-server
|
|
80
|
+
- Python from 3.9.13 or newer
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
### Using pip
|
|
85
|
+
```bash
|
|
86
|
+
python -m pip install langchain-timbr
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Using pip from github
|
|
90
|
+
```bash
|
|
91
|
+
pip install git+https://github.com/WPSemantix/langchain-timbr
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Documentation
|
|
95
|
+
|
|
96
|
+
For comprehensive documentation and usage examples, please visit:
|
|
97
|
+
|
|
98
|
+
- [Timbr LangChain Documentation](https://docs.timbr.ai/doc/docs/integration/langchain-sdk)
|
|
99
|
+
- [Timbr LangGraph Documentation](https://docs.timbr.ai/doc/docs/integration/langgraph-sdk)
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
The SDK requires several environment variables to be configured. See [`src/langchain_timbr/config.py`](src/langchain_timbr/config.py) for all available configuration options.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
langchain_timbr/__init__.py,sha256=wPcZEQieE8Bool0TOH4uync5U-jcpEe9g6YjUqwyirg,747
|
|
2
|
+
langchain_timbr/config.py,sha256=NOMjSpo0TVkWT8BdbiGSADU08iknF2bRltFLwQRhpwk,832
|
|
3
|
+
langchain_timbr/timbr_llm_connector.py,sha256=OXRttlEOJf-dTyilnXR6b6Cgl_cWDYrXGXQfmDV6vc8,13206
|
|
4
|
+
langchain_timbr/langchain/__init__.py,sha256=ejcsZKP9PK0j4WrrCCcvBXpDpP-TeRiVb21OIUJqix8,580
|
|
5
|
+
langchain_timbr/langchain/execute_timbr_query_chain.py,sha256=we1qjMS2JdBPRkGMJTzyHkpY3yW62wlh3PF3bTkEe8U,13883
|
|
6
|
+
langchain_timbr/langchain/generate_answer_chain.py,sha256=3q_Fe2tclZsdH0PFkNdZtO4Xe2WQQoxg4sekFic5zt4,3260
|
|
7
|
+
langchain_timbr/langchain/generate_timbr_sql_chain.py,sha256=-RZcdJ1lsivOE6zAm_hyg9txaINMGHWnGmkZepCg2Dk,7458
|
|
8
|
+
langchain_timbr/langchain/identify_concept_chain.py,sha256=m3Lzb0PVeSeE7YpfhjB1OY0x9jcR6a_lTYg5YTTDhIw,5588
|
|
9
|
+
langchain_timbr/langchain/timbr_sql_agent.py,sha256=F-HVqziHS7bVxWTkmvrkseRP5uZTuHQ2JoZNorQR4J8,18025
|
|
10
|
+
langchain_timbr/langchain/validate_timbr_sql_chain.py,sha256=SPW7zimqunZZLRD5d-trglL9RkqrWOy2vjkBc19CWhE,7919
|
|
11
|
+
langchain_timbr/langgraph/__init__.py,sha256=mKBFd0x01jWpRujUWe-suX3FFhenPoDxrvzs8I0mum0,457
|
|
12
|
+
langchain_timbr/langgraph/execute_timbr_query_node.py,sha256=ZL-HsBer073VmkJv59qFCNYJyKOgB8-Ziij4EEBD39c,5263
|
|
13
|
+
langchain_timbr/langgraph/generate_response_node.py,sha256=gChNFSPjK9lKwblgWTia6ETxhY5aIbgGsEykyIlGd90,2065
|
|
14
|
+
langchain_timbr/langgraph/generate_timbr_sql_node.py,sha256=qyL7uqB5k-Bv8rE12f2Ub7wlcAw-pQibEPP1SvFKLu0,4638
|
|
15
|
+
langchain_timbr/langgraph/identify_concept_node.py,sha256=ot9TFdRg8FA9JYVrtHLVi5k0vmUHUfL4ptQDFYYqOoA,3376
|
|
16
|
+
langchain_timbr/langgraph/validate_timbr_query_node.py,sha256=TypUs60OaBhOx9Ceq-15qNVuuAvfrFBjQsPRjWK1StQ,4469
|
|
17
|
+
langchain_timbr/llm_wrapper/llm_wrapper.py,sha256=sNMEqhtZx4S0ZKJCyg8OSE3fAWu1xI6Bp_GoRs7k4dI,6801
|
|
18
|
+
langchain_timbr/llm_wrapper/timbr_llm_wrapper.py,sha256=sDqDOz0qu8b4WWlagjNceswMVyvEJ8yBWZq2etBh-T0,1362
|
|
19
|
+
langchain_timbr/utils/general.py,sha256=753GNpYiyxhfYq59Bi8qvCyuHmTrD1fobcm6U2jZAF4,2394
|
|
20
|
+
langchain_timbr/utils/prompt_service.py,sha256=pJcBz3MKR51ajdU9gkif1r9_K7FxYbpWBiTkKA0A2q0,11144
|
|
21
|
+
langchain_timbr/utils/temperature_supported_models.json,sha256=e8j9O-68eCJhEK_NWowh3C6FE7UXFbR9icjDQfJBkdM,1596
|
|
22
|
+
langchain_timbr/utils/timbr_llm_utils.py,sha256=Gpp3nKG1MiwNBpl2Uua3pmKyxd1OEirRLW0kkxI473E,22462
|
|
23
|
+
langchain_timbr/utils/timbr_utils.py,sha256=p21DwTGhF4iKTLDQBkeBaJDFcXt-Hpu1ij8xzQt00Ng,16958
|
|
24
|
+
langchain_timbr-1.5.0.dist-info/METADATA,sha256=rL_614qtOxRauyvTcl0g6rCQaj80_IZ1lDMkqn167v8,5056
|
|
25
|
+
langchain_timbr-1.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
+
langchain_timbr-1.5.0.dist-info/licenses/LICENSE,sha256=0ITGFk2alkC7-e--bRGtuzDrv62USIiVyV2Crf3_L_0,1065
|
|
27
|
+
langchain_timbr-1.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Timbr.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|