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.
@@ -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
+ ![Timbr logo description](https://timbr.ai/wp-content/uploads/2025/01/logotimbrai230125.png)
65
+
66
+ [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FWPSemantix%2Flangchain-timbr.svg?type=shield&issueType=security)](https://app.fossa.com/projects/git%2Bgithub.com%2FWPSemantix%2Flangchain-timbr?ref=badge_shield&issueType=security)
67
+ [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FWPSemantix%2Flangchain-timbr.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FWPSemantix%2Flangchain-timbr?ref=badge_shield&issueType=license)
68
+
69
+ [![Python 3.9](https://img.shields.io/badge/python-3.9-blue)](https://www.python.org/downloads/release/python-3921/)
70
+ [![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-31017/)
71
+ [![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-31112/)
72
+ [![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.