langchain-timbr 2.1.4__py3-none-any.whl → 2.1.6__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/_version.py +2 -2
- langchain_timbr/config.py +5 -1
- langchain_timbr/langchain/execute_timbr_query_chain.py +11 -0
- langchain_timbr/langchain/generate_answer_chain.py +1 -1
- langchain_timbr/langchain/generate_timbr_sql_chain.py +10 -0
- langchain_timbr/langchain/timbr_sql_agent.py +19 -0
- langchain_timbr/langgraph/execute_timbr_query_node.py +7 -1
- langchain_timbr/langgraph/generate_response_node.py +3 -0
- langchain_timbr/langgraph/generate_timbr_sql_node.py +7 -0
- langchain_timbr/llm_wrapper/llm_wrapper.py +51 -0
- langchain_timbr/utils/timbr_llm_utils.py +353 -61
- {langchain_timbr-2.1.4.dist-info → langchain_timbr-2.1.6.dist-info}/METADATA +8 -32
- langchain_timbr-2.1.6.dist-info/RECORD +28 -0
- {langchain_timbr-2.1.4.dist-info → langchain_timbr-2.1.6.dist-info}/WHEEL +1 -1
- langchain_timbr-2.1.4.dist-info/RECORD +0 -28
- {langchain_timbr-2.1.4.dist-info → langchain_timbr-2.1.6.dist-info}/licenses/LICENSE +0 -0
langchain_timbr/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '2.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (2, 1,
|
|
31
|
+
__version__ = version = '2.1.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 1, 6)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
langchain_timbr/config.py
CHANGED
|
@@ -27,4 +27,8 @@ llm_client_id = os.environ.get('LLM_CLIENT_ID', None)
|
|
|
27
27
|
llm_client_secret = os.environ.get('LLM_CLIENT_SECRET', None)
|
|
28
28
|
llm_endpoint = os.environ.get('LLM_ENDPOINT', None)
|
|
29
29
|
llm_api_version = os.environ.get('LLM_API_VERSION', None)
|
|
30
|
-
llm_scope = os.environ.get('LLM_SCOPE', "https://cognitiveservices.azure.com/.default") # e.g. "api://<your-client-id>/.default"
|
|
30
|
+
llm_scope = os.environ.get('LLM_SCOPE', "https://cognitiveservices.azure.com/.default") # e.g. "api://<your-client-id>/.default"
|
|
31
|
+
|
|
32
|
+
# Whether to enable reasoning during SQL generation
|
|
33
|
+
with_reasoning = to_boolean(os.environ.get('WITH_REASONING', 'false'))
|
|
34
|
+
reasoning_steps = to_integer(os.environ.get('REASONING_STEPS', 2))
|
|
@@ -42,6 +42,8 @@ class ExecuteTimbrQueryChain(Chain):
|
|
|
42
42
|
is_jwt: Optional[bool] = False,
|
|
43
43
|
jwt_tenant_id: Optional[str] = None,
|
|
44
44
|
conn_params: Optional[dict] = None,
|
|
45
|
+
with_reasoning: Optional[bool] = config.with_reasoning,
|
|
46
|
+
reasoning_steps: Optional[int] = config.reasoning_steps,
|
|
45
47
|
debug: Optional[bool] = False,
|
|
46
48
|
**kwargs,
|
|
47
49
|
):
|
|
@@ -69,6 +71,8 @@ class ExecuteTimbrQueryChain(Chain):
|
|
|
69
71
|
:param is_jwt: Whether to use JWT authentication (default is False).
|
|
70
72
|
:param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
|
|
71
73
|
:param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
|
|
74
|
+
:param with_reasoning: Whether to enable reasoning during SQL generation (default is False).
|
|
75
|
+
:param reasoning_steps: Number of reasoning steps to perform if reasoning is enabled (default is 2).
|
|
72
76
|
:param kwargs: Additional arguments to pass to the base
|
|
73
77
|
:return: A list of rows from the Timbr query
|
|
74
78
|
|
|
@@ -137,6 +141,8 @@ class ExecuteTimbrQueryChain(Chain):
|
|
|
137
141
|
self._jwt_tenant_id = jwt_tenant_id
|
|
138
142
|
self._debug = to_boolean(debug)
|
|
139
143
|
self._conn_params = conn_params or {}
|
|
144
|
+
self._with_reasoning = to_boolean(with_reasoning)
|
|
145
|
+
self._reasoning_steps = to_integer(reasoning_steps)
|
|
140
146
|
|
|
141
147
|
|
|
142
148
|
@property
|
|
@@ -209,6 +215,8 @@ class ExecuteTimbrQueryChain(Chain):
|
|
|
209
215
|
note=(self._note or '') + err_txt,
|
|
210
216
|
db_is_case_sensitive=self._db_is_case_sensitive,
|
|
211
217
|
graph_depth=self._graph_depth,
|
|
218
|
+
with_reasoning=self._with_reasoning,
|
|
219
|
+
reasoning_steps=self._reasoning_steps,
|
|
212
220
|
debug=self._debug,
|
|
213
221
|
)
|
|
214
222
|
|
|
@@ -239,6 +247,7 @@ class ExecuteTimbrQueryChain(Chain):
|
|
|
239
247
|
concept_name = inputs.get("concept", self._concept)
|
|
240
248
|
is_sql_valid = True
|
|
241
249
|
error = None
|
|
250
|
+
reasoning_status = None
|
|
242
251
|
usage_metadata = {}
|
|
243
252
|
|
|
244
253
|
if sql and self._should_validate_sql:
|
|
@@ -255,6 +264,7 @@ class ExecuteTimbrQueryChain(Chain):
|
|
|
255
264
|
schema_name = generate_res.get("schema", schema_name)
|
|
256
265
|
concept_name = generate_res.get("concept", concept_name)
|
|
257
266
|
is_sql_valid = generate_res.get("is_sql_valid")
|
|
267
|
+
reasoning_status = generate_res.get("reasoning_status")
|
|
258
268
|
if not is_sql_valid and not self._should_validate_sql:
|
|
259
269
|
is_sql_valid = True
|
|
260
270
|
|
|
@@ -293,6 +303,7 @@ class ExecuteTimbrQueryChain(Chain):
|
|
|
293
303
|
"schema": schema_name,
|
|
294
304
|
"concept": concept_name,
|
|
295
305
|
"error": error if not is_sql_valid else None,
|
|
306
|
+
"reasoning_status": reasoning_status,
|
|
296
307
|
self.usage_metadata_key: usage_metadata,
|
|
297
308
|
}
|
|
298
309
|
|
|
@@ -34,8 +34,8 @@ class GenerateAnswerChain(Chain):
|
|
|
34
34
|
:param verify_ssl: Whether to verify SSL certificates (default is True).
|
|
35
35
|
:param is_jwt: Whether to use JWT authentication (default is False).
|
|
36
36
|
:param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
|
|
37
|
-
:param note: Optional additional note to extend our llm prompt
|
|
38
37
|
:param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
|
|
38
|
+
:param note: Optional additional note to extend our llm prompt
|
|
39
39
|
|
|
40
40
|
## Example
|
|
41
41
|
```
|
|
@@ -39,6 +39,8 @@ class GenerateTimbrSqlChain(Chain):
|
|
|
39
39
|
is_jwt: Optional[bool] = False,
|
|
40
40
|
jwt_tenant_id: Optional[str] = None,
|
|
41
41
|
conn_params: Optional[dict] = None,
|
|
42
|
+
with_reasoning: Optional[bool] = config.with_reasoning,
|
|
43
|
+
reasoning_steps: Optional[int] = config.reasoning_steps,
|
|
42
44
|
debug: Optional[bool] = False,
|
|
43
45
|
**kwargs,
|
|
44
46
|
):
|
|
@@ -64,6 +66,9 @@ class GenerateTimbrSqlChain(Chain):
|
|
|
64
66
|
:param is_jwt: Whether to use JWT authentication (default is False).
|
|
65
67
|
:param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
|
|
66
68
|
:param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
|
|
69
|
+
:param with_reasoning: Whether to enable reasoning during SQL generation (default is False).
|
|
70
|
+
:param reasoning_steps: Number of reasoning steps to perform if reasoning is enabled (default is 2).
|
|
71
|
+
:param debug: Whether to enable debug mode for detailed logging
|
|
67
72
|
:param kwargs: Additional arguments to pass to the base
|
|
68
73
|
|
|
69
74
|
## Example
|
|
@@ -129,6 +134,8 @@ class GenerateTimbrSqlChain(Chain):
|
|
|
129
134
|
self._jwt_tenant_id = jwt_tenant_id
|
|
130
135
|
self._debug = to_boolean(debug)
|
|
131
136
|
self._conn_params = conn_params or {}
|
|
137
|
+
self._with_reasoning = to_boolean(with_reasoning)
|
|
138
|
+
self._reasoning_steps = to_integer(reasoning_steps)
|
|
132
139
|
|
|
133
140
|
|
|
134
141
|
@property
|
|
@@ -184,6 +191,8 @@ class GenerateTimbrSqlChain(Chain):
|
|
|
184
191
|
note=self._note,
|
|
185
192
|
db_is_case_sensitive=self._db_is_case_sensitive,
|
|
186
193
|
graph_depth=self._graph_depth,
|
|
194
|
+
with_reasoning=self._with_reasoning,
|
|
195
|
+
reasoning_steps=self._reasoning_steps,
|
|
187
196
|
debug=self._debug,
|
|
188
197
|
)
|
|
189
198
|
|
|
@@ -197,5 +206,6 @@ class GenerateTimbrSqlChain(Chain):
|
|
|
197
206
|
"concept": concept,
|
|
198
207
|
"is_sql_valid": generate_res.get("is_sql_valid"),
|
|
199
208
|
"error": generate_res.get("error"),
|
|
209
|
+
"reasoning_status": generate_res.get("reasoning_status"),
|
|
200
210
|
self.usage_metadata_key: generate_res.get("usage_metadata"),
|
|
201
211
|
}
|
|
@@ -6,6 +6,7 @@ from langchain.schema import AgentAction, AgentFinish
|
|
|
6
6
|
from ..utils.general import parse_list, to_boolean, to_integer
|
|
7
7
|
from .execute_timbr_query_chain import ExecuteTimbrQueryChain
|
|
8
8
|
from .generate_answer_chain import GenerateAnswerChain
|
|
9
|
+
from .. import config
|
|
9
10
|
|
|
10
11
|
class TimbrSqlAgent(BaseSingleActionAgent):
|
|
11
12
|
def __init__(
|
|
@@ -34,6 +35,8 @@ class TimbrSqlAgent(BaseSingleActionAgent):
|
|
|
34
35
|
is_jwt: Optional[bool] = False,
|
|
35
36
|
jwt_tenant_id: Optional[str] = None,
|
|
36
37
|
conn_params: Optional[dict] = None,
|
|
38
|
+
with_reasoning: Optional[bool] = config.with_reasoning,
|
|
39
|
+
reasoning_steps: Optional[int] = config.reasoning_steps,
|
|
37
40
|
debug: Optional[bool] = False
|
|
38
41
|
):
|
|
39
42
|
"""
|
|
@@ -61,6 +64,8 @@ class TimbrSqlAgent(BaseSingleActionAgent):
|
|
|
61
64
|
:param is_jwt: Whether to use JWT authentication (default is False).
|
|
62
65
|
:param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
|
|
63
66
|
:param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
|
|
67
|
+
:param with_reasoning: Whether to enable reasoning during SQL generation (default is False).
|
|
68
|
+
:param reasoning_steps: Number of reasoning steps to perform if reasoning is enabled (default is 2).
|
|
64
69
|
|
|
65
70
|
## Example
|
|
66
71
|
```
|
|
@@ -113,6 +118,8 @@ class TimbrSqlAgent(BaseSingleActionAgent):
|
|
|
113
118
|
is_jwt=to_boolean(is_jwt),
|
|
114
119
|
jwt_tenant_id=jwt_tenant_id,
|
|
115
120
|
conn_params=conn_params,
|
|
121
|
+
with_reasoning=to_boolean(with_reasoning),
|
|
122
|
+
reasoning_steps=to_integer(reasoning_steps),
|
|
116
123
|
debug=to_boolean(debug),
|
|
117
124
|
)
|
|
118
125
|
self._generate_answer = to_boolean(generate_answer)
|
|
@@ -173,6 +180,7 @@ class TimbrSqlAgent(BaseSingleActionAgent):
|
|
|
173
180
|
"sql": None,
|
|
174
181
|
"schema": None,
|
|
175
182
|
"concept": None,
|
|
183
|
+
"reasoning_status": None,
|
|
176
184
|
"usage_metadata": {},
|
|
177
185
|
},
|
|
178
186
|
log="Empty input received"
|
|
@@ -200,6 +208,7 @@ class TimbrSqlAgent(BaseSingleActionAgent):
|
|
|
200
208
|
"schema": result.get("schema", ""),
|
|
201
209
|
"concept": result.get("concept", ""),
|
|
202
210
|
"error": result.get("error", None),
|
|
211
|
+
"reasoning_status": result.get("reasoning_status", None),
|
|
203
212
|
"usage_metadata": usage_metadata,
|
|
204
213
|
},
|
|
205
214
|
log=f"Successfully executed query on concept: {result.get('concept', '')}"
|
|
@@ -214,6 +223,7 @@ class TimbrSqlAgent(BaseSingleActionAgent):
|
|
|
214
223
|
"sql": None,
|
|
215
224
|
"schema": None,
|
|
216
225
|
"concept": None,
|
|
226
|
+
"reasoning_status": None,
|
|
217
227
|
"usage_metadata": {},
|
|
218
228
|
},
|
|
219
229
|
log=error_context
|
|
@@ -234,6 +244,7 @@ class TimbrSqlAgent(BaseSingleActionAgent):
|
|
|
234
244
|
"sql": None,
|
|
235
245
|
"schema": None,
|
|
236
246
|
"concept": None,
|
|
247
|
+
"reasoning_status": None,
|
|
237
248
|
"usage_metadata": {},
|
|
238
249
|
},
|
|
239
250
|
log="Empty or whitespace-only input received"
|
|
@@ -274,6 +285,7 @@ class TimbrSqlAgent(BaseSingleActionAgent):
|
|
|
274
285
|
"schema": result.get("schema", ""),
|
|
275
286
|
"concept": result.get("concept", ""),
|
|
276
287
|
"error": result.get("error", None),
|
|
288
|
+
"reasoning_status": result.get("reasoning_status", None),
|
|
277
289
|
"usage_metadata": usage_metadata,
|
|
278
290
|
},
|
|
279
291
|
log=f"Successfully executed query on concept: {result.get('concept', '')}"
|
|
@@ -288,6 +300,7 @@ class TimbrSqlAgent(BaseSingleActionAgent):
|
|
|
288
300
|
"sql": None,
|
|
289
301
|
"schema": None,
|
|
290
302
|
"concept": None,
|
|
303
|
+
"reasoning_status": None,
|
|
291
304
|
"usage_metadata": {},
|
|
292
305
|
},
|
|
293
306
|
log=error_context
|
|
@@ -332,6 +345,8 @@ def create_timbr_sql_agent(
|
|
|
332
345
|
is_jwt: Optional[bool] = False,
|
|
333
346
|
jwt_tenant_id: Optional[str] = None,
|
|
334
347
|
conn_params: Optional[dict] = None,
|
|
348
|
+
with_reasoning: Optional[bool] = config.with_reasoning,
|
|
349
|
+
reasoning_steps: Optional[int] = config.reasoning_steps,
|
|
335
350
|
debug: Optional[bool] = False
|
|
336
351
|
) -> AgentExecutor:
|
|
337
352
|
"""
|
|
@@ -361,6 +376,8 @@ def create_timbr_sql_agent(
|
|
|
361
376
|
:param is_jwt: Whether to use JWT authentication (default is False).
|
|
362
377
|
:param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
|
|
363
378
|
:param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
|
|
379
|
+
:param with_reasoning: Whether to enable reasoning during SQL generation (default is False).
|
|
380
|
+
:param reasoning_steps: Number of reasoning steps to perform if reasoning is enabled (default is 2).
|
|
364
381
|
|
|
365
382
|
Returns:
|
|
366
383
|
AgentExecutor: Configured agent executor ready to use
|
|
@@ -427,6 +444,8 @@ def create_timbr_sql_agent(
|
|
|
427
444
|
is_jwt=is_jwt,
|
|
428
445
|
jwt_tenant_id=jwt_tenant_id,
|
|
429
446
|
conn_params=conn_params,
|
|
447
|
+
with_reasoning=with_reasoning,
|
|
448
|
+
reasoning_steps=reasoning_steps,
|
|
430
449
|
debug=debug,
|
|
431
450
|
)
|
|
432
451
|
|
|
@@ -3,7 +3,7 @@ from langchain.llms.base import LLM
|
|
|
3
3
|
from langgraph.graph import StateGraph
|
|
4
4
|
|
|
5
5
|
from ..langchain.execute_timbr_query_chain import ExecuteTimbrQueryChain
|
|
6
|
-
|
|
6
|
+
from .. import config
|
|
7
7
|
|
|
8
8
|
class ExecuteSemanticQueryNode:
|
|
9
9
|
"""
|
|
@@ -36,6 +36,8 @@ class ExecuteSemanticQueryNode:
|
|
|
36
36
|
is_jwt: Optional[bool] = False,
|
|
37
37
|
jwt_tenant_id: Optional[str] = None,
|
|
38
38
|
conn_params: Optional[dict] = None,
|
|
39
|
+
with_reasoning: Optional[bool] = config.with_reasoning,
|
|
40
|
+
reasoning_steps: Optional[int] = config.reasoning_steps,
|
|
39
41
|
debug: Optional[bool] = False,
|
|
40
42
|
**kwargs,
|
|
41
43
|
):
|
|
@@ -63,6 +65,8 @@ class ExecuteSemanticQueryNode:
|
|
|
63
65
|
:param is_jwt: Whether to use JWT authentication (default is False).
|
|
64
66
|
:param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
|
|
65
67
|
:param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
|
|
68
|
+
:param with_reasoning: Whether to enable reasoning during SQL generation (default is False).
|
|
69
|
+
:param reasoning_steps: Number of reasoning steps to perform if reasoning is enabled (default is 2).
|
|
66
70
|
:return: A list of rows from the Timbr query
|
|
67
71
|
"""
|
|
68
72
|
self.chain = ExecuteTimbrQueryChain(
|
|
@@ -89,6 +93,8 @@ class ExecuteSemanticQueryNode:
|
|
|
89
93
|
is_jwt=is_jwt,
|
|
90
94
|
jwt_tenant_id=jwt_tenant_id,
|
|
91
95
|
conn_params=conn_params,
|
|
96
|
+
with_reasoning=with_reasoning,
|
|
97
|
+
reasoning_steps=reasoning_steps,
|
|
92
98
|
debug=debug,
|
|
93
99
|
**kwargs,
|
|
94
100
|
)
|
|
@@ -20,6 +20,7 @@ class GenerateResponseNode:
|
|
|
20
20
|
is_jwt: Optional[bool] = False,
|
|
21
21
|
jwt_tenant_id: Optional[str] = None,
|
|
22
22
|
conn_params: Optional[dict] = None,
|
|
23
|
+
note: Optional[str] = '',
|
|
23
24
|
debug: Optional[bool] = False,
|
|
24
25
|
**kwargs,
|
|
25
26
|
):
|
|
@@ -31,6 +32,7 @@ class GenerateResponseNode:
|
|
|
31
32
|
:param is_jwt: Whether to use JWT authentication (default is False).
|
|
32
33
|
:param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
|
|
33
34
|
:param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
|
|
35
|
+
:param note: Optional additional note to extend our llm prompt
|
|
34
36
|
"""
|
|
35
37
|
self.chain = GenerateAnswerChain(
|
|
36
38
|
llm=llm,
|
|
@@ -40,6 +42,7 @@ class GenerateResponseNode:
|
|
|
40
42
|
is_jwt=is_jwt,
|
|
41
43
|
jwt_tenant_id=jwt_tenant_id,
|
|
42
44
|
conn_params=conn_params,
|
|
45
|
+
note=note,
|
|
43
46
|
debug=debug,
|
|
44
47
|
**kwargs,
|
|
45
48
|
)
|
|
@@ -3,6 +3,7 @@ from langchain.llms.base import LLM
|
|
|
3
3
|
from langgraph.graph import StateGraph
|
|
4
4
|
|
|
5
5
|
from ..langchain.generate_timbr_sql_chain import GenerateTimbrSqlChain
|
|
6
|
+
from .. import config
|
|
6
7
|
|
|
7
8
|
class GenerateTimbrSqlNode:
|
|
8
9
|
"""
|
|
@@ -32,6 +33,8 @@ class GenerateTimbrSqlNode:
|
|
|
32
33
|
is_jwt: Optional[bool] = False,
|
|
33
34
|
jwt_tenant_id: Optional[str] = None,
|
|
34
35
|
conn_params: Optional[dict] = None,
|
|
36
|
+
with_reasoning: Optional[bool] = config.with_reasoning,
|
|
37
|
+
reasoning_steps: Optional[int] = config.reasoning_steps,
|
|
35
38
|
debug: Optional[bool] = False,
|
|
36
39
|
**kwargs,
|
|
37
40
|
):
|
|
@@ -57,6 +60,8 @@ class GenerateTimbrSqlNode:
|
|
|
57
60
|
:param is_jwt: Whether to use JWT authentication (default: False)
|
|
58
61
|
:param jwt_tenant_id: Tenant ID for JWT authentication when using multi-tenant setup
|
|
59
62
|
:param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
|
|
63
|
+
:param with_reasoning: Whether to enable reasoning during SQL generation (default is False).
|
|
64
|
+
:param reasoning_steps: Number of reasoning steps to perform if reasoning is enabled (default is 2).
|
|
60
65
|
"""
|
|
61
66
|
self.chain = GenerateTimbrSqlChain(
|
|
62
67
|
llm=llm,
|
|
@@ -80,6 +85,8 @@ class GenerateTimbrSqlNode:
|
|
|
80
85
|
is_jwt=is_jwt,
|
|
81
86
|
jwt_tenant_id=jwt_tenant_id,
|
|
82
87
|
conn_params=conn_params,
|
|
88
|
+
with_reasoning=with_reasoning,
|
|
89
|
+
reasoning_steps=reasoning_steps,
|
|
83
90
|
debug=debug,
|
|
84
91
|
**kwargs,
|
|
85
92
|
)
|
|
@@ -15,6 +15,7 @@ class LlmTypes(Enum):
|
|
|
15
15
|
Snowflake = 'snowflake-cortex'
|
|
16
16
|
Databricks = 'chat-databricks'
|
|
17
17
|
VertexAI = 'chat-vertexai'
|
|
18
|
+
Bedrock = 'amazon_bedrock_converse_chat'
|
|
18
19
|
Timbr = 'timbr'
|
|
19
20
|
|
|
20
21
|
|
|
@@ -252,6 +253,28 @@ class LlmWrapper(LLM):
|
|
|
252
253
|
credentials=creds,
|
|
253
254
|
**params,
|
|
254
255
|
)
|
|
256
|
+
elif is_llm_type(llm_type, LlmTypes.Bedrock):
|
|
257
|
+
from langchain_aws import ChatBedrockConverse
|
|
258
|
+
llm_model = model or "openai.gpt-oss-20b-1:0"
|
|
259
|
+
params = self._add_temperature(LlmTypes.Bedrock.name, llm_model, **llm_params)
|
|
260
|
+
|
|
261
|
+
aws_region = pop_param_value(params, ['aws_region', 'llm_region', 'region'])
|
|
262
|
+
if aws_region:
|
|
263
|
+
params['region_name'] = aws_region
|
|
264
|
+
aws_access_key_id = pop_param_value(params, ['aws_access_key_id', 'llm_access_key_id', 'access_key_id'])
|
|
265
|
+
if aws_access_key_id:
|
|
266
|
+
params['aws_access_key_id'] = aws_access_key_id
|
|
267
|
+
aws_secret_access_key = pop_param_value(params, ['aws_secret_access_key', 'llm_secret_access_key', 'secret_access_key'], default=api_key)
|
|
268
|
+
if aws_secret_access_key:
|
|
269
|
+
params['aws_secret_access_key'] = aws_secret_access_key
|
|
270
|
+
aws_session_token = pop_param_value(params, ['aws_session_token', 'llm_session_token', 'session_token'])
|
|
271
|
+
if aws_session_token:
|
|
272
|
+
params['aws_session_token'] = aws_session_token
|
|
273
|
+
|
|
274
|
+
return ChatBedrockConverse(
|
|
275
|
+
model=llm_model,
|
|
276
|
+
**params,
|
|
277
|
+
)
|
|
255
278
|
else:
|
|
256
279
|
raise ValueError(f"Unsupported LLM type: {llm_type}")
|
|
257
280
|
|
|
@@ -324,6 +347,31 @@ class LlmWrapper(LLM):
|
|
|
324
347
|
if self.client.credentials:
|
|
325
348
|
client = genai.Client(credentials=self.client.credentials, vertexai=True, project=self.client.project, location=self.client.location)
|
|
326
349
|
models = [m.name.split('/')[-1] for m in client.models.list()]
|
|
350
|
+
elif is_llm_type(self._llm_type, LlmTypes.Bedrock):
|
|
351
|
+
import boto3
|
|
352
|
+
|
|
353
|
+
# Extract SecretStr values properly
|
|
354
|
+
aws_access_key_id = getattr(self.client, 'aws_access_key_id', None)
|
|
355
|
+
if aws_access_key_id and hasattr(aws_access_key_id, '_secret_value'):
|
|
356
|
+
aws_access_key_id = aws_access_key_id._secret_value
|
|
357
|
+
|
|
358
|
+
aws_secret_access_key = getattr(self.client, 'aws_secret_access_key', None)
|
|
359
|
+
if aws_secret_access_key and hasattr(aws_secret_access_key, '_secret_value'):
|
|
360
|
+
aws_secret_access_key = aws_secret_access_key._secret_value
|
|
361
|
+
|
|
362
|
+
aws_session_token = getattr(self.client, 'aws_session_token', None)
|
|
363
|
+
if aws_session_token and hasattr(aws_session_token, '_secret_value'):
|
|
364
|
+
aws_session_token = aws_session_token._secret_value
|
|
365
|
+
|
|
366
|
+
bedrock_client = boto3.client(
|
|
367
|
+
service_name='bedrock',
|
|
368
|
+
region_name=getattr(self.client, 'region_name', None),
|
|
369
|
+
aws_access_key_id=aws_access_key_id,
|
|
370
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
371
|
+
aws_session_token=aws_session_token,
|
|
372
|
+
)
|
|
373
|
+
response = bedrock_client.list_foundation_models()
|
|
374
|
+
models = [model['modelId'] for model in response.get('modelSummaries', [])]
|
|
327
375
|
|
|
328
376
|
except Exception:
|
|
329
377
|
# If model list fetching throws an exception, return default value using get_supported_models
|
|
@@ -341,6 +389,9 @@ class LlmWrapper(LLM):
|
|
|
341
389
|
|
|
342
390
|
|
|
343
391
|
def _call(self, prompt, **kwargs):
|
|
392
|
+
# TODO: Remove this condition on next langchain-timbr major release
|
|
393
|
+
if is_llm_type(self._llm_type, LlmTypes.Bedrock):
|
|
394
|
+
return self.client.invoke(prompt, **kwargs)
|
|
344
395
|
return self.client(prompt, **kwargs)
|
|
345
396
|
|
|
346
397
|
|
|
@@ -4,7 +4,8 @@ import base64, hashlib
|
|
|
4
4
|
from cryptography.fernet import Fernet
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
import concurrent.futures
|
|
7
|
-
import
|
|
7
|
+
import json
|
|
8
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
8
9
|
|
|
9
10
|
from .timbr_utils import get_datasources, get_tags, get_concepts, get_concept_properties, validate_sql, get_properties_description, get_relationships_description
|
|
10
11
|
from .prompt_service import (
|
|
@@ -135,7 +136,7 @@ def _prompt_to_string(prompt: Any) -> str:
|
|
|
135
136
|
return prompt_text.strip()
|
|
136
137
|
|
|
137
138
|
|
|
138
|
-
def _calculate_token_count(llm: LLM, prompt: str) -> int:
|
|
139
|
+
def _calculate_token_count(llm: LLM, prompt: str | list[Any]) -> int:
|
|
139
140
|
"""
|
|
140
141
|
Calculate the token count for a given prompt text using the specified LLM.
|
|
141
142
|
Falls back to basic if the LLM doesn't support token counting.
|
|
@@ -187,21 +188,103 @@ def _get_response_text(response: Any) -> str:
|
|
|
187
188
|
return response_text
|
|
188
189
|
|
|
189
190
|
def _extract_usage_metadata(response: Any) -> dict:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
191
|
+
"""
|
|
192
|
+
Extract usage metadata from LLM response across different providers.
|
|
193
|
+
|
|
194
|
+
Different providers return usage data in different formats:
|
|
195
|
+
- OpenAI/AzureOpenAI: response.response_metadata['token_usage'] or response.usage_metadata
|
|
196
|
+
- Anthropic: response.response_metadata['usage'] or response.usage_metadata
|
|
197
|
+
- Google/VertexAI: response.usage_metadata
|
|
198
|
+
- Bedrock: response.response_metadata['usage'] or response.response_metadata (direct ResponseMetadata)
|
|
199
|
+
- Snowflake: response.response_metadata['usage']
|
|
200
|
+
- Databricks: response.usage_metadata or response.response_metadata
|
|
201
|
+
"""
|
|
202
|
+
usage_metadata = {}
|
|
203
|
+
|
|
204
|
+
# Try to get response_metadata first (most common)
|
|
205
|
+
if hasattr(response, 'response_metadata') and response.response_metadata:
|
|
206
|
+
resp_meta = response.response_metadata
|
|
207
|
+
|
|
208
|
+
# Check for 'usage' key (Anthropic, Bedrock, Snowflake)
|
|
209
|
+
if 'usage' in resp_meta:
|
|
210
|
+
usage_metadata = resp_meta['usage']
|
|
211
|
+
# Check for 'token_usage' key (OpenAI/AzureOpenAI)
|
|
212
|
+
elif 'token_usage' in resp_meta:
|
|
213
|
+
usage_metadata = resp_meta['token_usage']
|
|
214
|
+
# Check for direct token fields in response_metadata (some Bedrock responses)
|
|
215
|
+
elif any(key in resp_meta for key in ['input_tokens', 'output_tokens', 'total_tokens',
|
|
216
|
+
'prompt_tokens', 'completion_tokens']):
|
|
217
|
+
usage_metadata = {
|
|
218
|
+
k: v for k, v in resp_meta.items()
|
|
219
|
+
if k in ['input_tokens', 'output_tokens', 'total_tokens',
|
|
220
|
+
'prompt_tokens', 'completion_tokens']
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Try usage_metadata attribute (Google, VertexAI, some others)
|
|
224
|
+
if not usage_metadata and hasattr(response, 'usage_metadata') and response.usage_metadata:
|
|
225
|
+
usage_meta = response.usage_metadata
|
|
226
|
+
if isinstance(usage_meta, dict):
|
|
227
|
+
# If it has a nested 'usage' key
|
|
228
|
+
if 'usage' in usage_meta:
|
|
229
|
+
usage_metadata = usage_meta['usage']
|
|
230
|
+
else:
|
|
231
|
+
usage_metadata = usage_meta
|
|
232
|
+
else:
|
|
233
|
+
# Handle case where usage_metadata is an object with attributes
|
|
234
|
+
usage_metadata = {
|
|
235
|
+
k: getattr(usage_meta, k)
|
|
236
|
+
for k in dir(usage_meta)
|
|
237
|
+
if not k.startswith('_') and not callable(getattr(usage_meta, k))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Try direct usage attribute (fallback)
|
|
241
|
+
if not usage_metadata and hasattr(response, 'usage') and response.usage:
|
|
242
|
+
usage = response.usage
|
|
243
|
+
if isinstance(usage, dict):
|
|
244
|
+
if 'usage' in usage:
|
|
245
|
+
usage_metadata = usage['usage']
|
|
246
|
+
else:
|
|
247
|
+
usage_metadata = usage
|
|
248
|
+
else:
|
|
249
|
+
# Handle case where usage is an object with attributes
|
|
250
|
+
usage_metadata = {
|
|
251
|
+
k: getattr(usage, k)
|
|
252
|
+
for k in dir(usage)
|
|
253
|
+
if not k.startswith('_') and not callable(getattr(usage, k))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Normalize token field names to standard format
|
|
257
|
+
# Different providers use different names: input_tokens vs prompt_tokens, etc.
|
|
258
|
+
if usage_metadata:
|
|
259
|
+
normalized = {}
|
|
260
|
+
|
|
261
|
+
# Map various input token field names
|
|
262
|
+
if 'input_tokens' in usage_metadata:
|
|
263
|
+
normalized['input_tokens'] = usage_metadata['input_tokens']
|
|
264
|
+
elif 'prompt_tokens' in usage_metadata:
|
|
265
|
+
normalized['input_tokens'] = usage_metadata['prompt_tokens']
|
|
266
|
+
|
|
267
|
+
# Map various output token field names
|
|
268
|
+
if 'output_tokens' in usage_metadata:
|
|
269
|
+
normalized['output_tokens'] = usage_metadata['output_tokens']
|
|
270
|
+
elif 'completion_tokens' in usage_metadata:
|
|
271
|
+
normalized['output_tokens'] = usage_metadata['completion_tokens']
|
|
272
|
+
|
|
273
|
+
# Map total tokens
|
|
274
|
+
if 'total_tokens' in usage_metadata:
|
|
275
|
+
normalized['total_tokens'] = usage_metadata['total_tokens']
|
|
276
|
+
elif 'input_tokens' in normalized and 'output_tokens' in normalized:
|
|
277
|
+
# Calculate total if not provided
|
|
278
|
+
normalized['total_tokens'] = normalized['input_tokens'] + normalized['output_tokens']
|
|
279
|
+
|
|
280
|
+
# Keep any other metadata fields that don't conflict
|
|
281
|
+
for key, value in usage_metadata.items():
|
|
282
|
+
if key not in ['input_tokens', 'prompt_tokens', 'output_tokens',
|
|
283
|
+
'completion_tokens', 'total_tokens']:
|
|
284
|
+
normalized[key] = value
|
|
285
|
+
|
|
286
|
+
return normalized if normalized else usage_metadata
|
|
287
|
+
|
|
205
288
|
return usage_metadata
|
|
206
289
|
|
|
207
290
|
def determine_concept(
|
|
@@ -396,6 +479,149 @@ def _get_active_datasource(conn_params: dict) -> dict:
|
|
|
396
479
|
return datasources[0] if datasources else None
|
|
397
480
|
|
|
398
481
|
|
|
482
|
+
def _evaluate_sql_with_reasoning(
|
|
483
|
+
question: str,
|
|
484
|
+
sql_query: str,
|
|
485
|
+
llm: LLM,
|
|
486
|
+
timeout: int,
|
|
487
|
+
) -> dict:
|
|
488
|
+
"""
|
|
489
|
+
Evaluate if the generated SQL correctly answers the business question.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
dict with 'assessment' ('correct'|'partial'|'incorrect') and 'reasoning'
|
|
493
|
+
"""
|
|
494
|
+
system_prompt = """You are an expert SQL and data analysis evaluator for Timbr.ai knowledge graph queries.
|
|
495
|
+
|
|
496
|
+
**IMPORTANT CONTEXT:**
|
|
497
|
+
- This system uses Timbr.ai, which extends SQL with semantic graph layer, including traversals, measures and more
|
|
498
|
+
- Field names may use special Timbr syntax that is NOT standard SQL but is VALID in this system:
|
|
499
|
+
* `measure.<measure_name>` - References computed measures (e.g., measure.total_balance_amount)
|
|
500
|
+
* `<relationship>[target_table].<property>` - Graph traversal syntax (e.g., has_account[Account].account_name)
|
|
501
|
+
* These are translated by Timbr to standard SQL before execution
|
|
502
|
+
- DO NOT mark queries as incorrect based on field name syntax - Timbr validates this before execution
|
|
503
|
+
|
|
504
|
+
Evaluate whether the generated query correctly addresses the business question:
|
|
505
|
+
- **correct**: The query fully and accurately answers the question
|
|
506
|
+
- **partial**: The query is partially correct or incomplete
|
|
507
|
+
- **incorrect**: The query does not address the question or is wrong
|
|
508
|
+
|
|
509
|
+
Return your evaluation as a JSON object with this exact structure:
|
|
510
|
+
{
|
|
511
|
+
"assessment": "<correct|partial|incorrect>",
|
|
512
|
+
"reasoning": "<short but precise sentence explaining your assessment>"
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
Be concise and objective."""
|
|
516
|
+
|
|
517
|
+
user_prompt = f"""**Business Question:**
|
|
518
|
+
{question}
|
|
519
|
+
|
|
520
|
+
**Generated SQL Query:**
|
|
521
|
+
```sql
|
|
522
|
+
{sql_query}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Please evaluate this result."""
|
|
526
|
+
|
|
527
|
+
messages = [
|
|
528
|
+
SystemMessage(content=system_prompt),
|
|
529
|
+
HumanMessage(content=user_prompt)
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
apx_token_count = _calculate_token_count(llm, messages)
|
|
533
|
+
if hasattr(llm, "_llm_type") and "snowflake" in llm._llm_type:
|
|
534
|
+
_clean_snowflake_prompt(messages)
|
|
535
|
+
|
|
536
|
+
response = _call_llm_with_timeout(llm, messages, timeout=timeout)
|
|
537
|
+
|
|
538
|
+
# Extract JSON from response content (handle markdown code blocks)
|
|
539
|
+
content = response.content.strip()
|
|
540
|
+
|
|
541
|
+
# Remove markdown code block markers if present
|
|
542
|
+
if content.startswith("```json"):
|
|
543
|
+
content = content[7:] # Remove ```json
|
|
544
|
+
elif content.startswith("```"):
|
|
545
|
+
content = content[3:] # Remove ```
|
|
546
|
+
|
|
547
|
+
if content.endswith("```"):
|
|
548
|
+
content = content[:-3] # Remove closing ```
|
|
549
|
+
|
|
550
|
+
content = content.strip()
|
|
551
|
+
|
|
552
|
+
# Parse JSON response
|
|
553
|
+
evaluation = json.loads(content)
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
"evaluation": evaluation,
|
|
557
|
+
"apx_token_count": apx_token_count,
|
|
558
|
+
"usage_metadata": _extract_usage_metadata(response),
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _generate_sql_with_llm(
|
|
563
|
+
question: str,
|
|
564
|
+
llm: LLM,
|
|
565
|
+
conn_params: dict,
|
|
566
|
+
generate_sql_prompt: Any,
|
|
567
|
+
current_context: dict,
|
|
568
|
+
note: str,
|
|
569
|
+
should_validate_sql: bool,
|
|
570
|
+
timeout: int,
|
|
571
|
+
debug: bool = False,
|
|
572
|
+
) -> dict:
|
|
573
|
+
"""
|
|
574
|
+
Generate SQL using LLM based on the provided context and note.
|
|
575
|
+
This function is used for both initial SQL generation and regeneration with feedback.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
current_context: dict containing datasource_type, schema, concept, concept_description,
|
|
579
|
+
concept_tags, columns_str, measures_context, transitive_context,
|
|
580
|
+
sensitivity_txt, max_limit, cur_date
|
|
581
|
+
note: Additional instructions/feedback to include in the prompt
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
dict with 'sql', 'is_valid', 'error', 'apx_token_count', 'usage_metadata', 'p_hash' (if debug)
|
|
585
|
+
"""
|
|
586
|
+
prompt = generate_sql_prompt.format_messages(
|
|
587
|
+
current_date=current_context['cur_date'],
|
|
588
|
+
datasource_type=current_context['datasource_type'],
|
|
589
|
+
schema=current_context['schema'],
|
|
590
|
+
concept=f"`{current_context['concept']}`",
|
|
591
|
+
description=current_context['concept_description'],
|
|
592
|
+
tags=current_context['concept_tags'],
|
|
593
|
+
question=question,
|
|
594
|
+
columns=current_context['columns_str'],
|
|
595
|
+
measures_context=current_context['measures_context'],
|
|
596
|
+
transitive_context=current_context['transitive_context'],
|
|
597
|
+
sensitivity_context=current_context['sensitivity_txt'],
|
|
598
|
+
max_limit=current_context['max_limit'],
|
|
599
|
+
note=note,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
apx_token_count = _calculate_token_count(llm, prompt)
|
|
603
|
+
if hasattr(llm, "_llm_type") and "snowflake" in llm._llm_type:
|
|
604
|
+
_clean_snowflake_prompt(prompt)
|
|
605
|
+
|
|
606
|
+
response = _call_llm_with_timeout(llm, prompt, timeout=timeout)
|
|
607
|
+
|
|
608
|
+
result = {
|
|
609
|
+
"sql": _parse_sql_from_llm_response(response),
|
|
610
|
+
"apx_token_count": apx_token_count,
|
|
611
|
+
"usage_metadata": _extract_usage_metadata(response),
|
|
612
|
+
"is_valid": True,
|
|
613
|
+
"error": None,
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if debug:
|
|
617
|
+
result["p_hash"] = encrypt_prompt(prompt)
|
|
618
|
+
|
|
619
|
+
if should_validate_sql:
|
|
620
|
+
result["is_valid"], result["error"] = validate_sql(result["sql"], conn_params)
|
|
621
|
+
|
|
622
|
+
return result
|
|
623
|
+
|
|
624
|
+
|
|
399
625
|
def generate_sql(
|
|
400
626
|
question: str,
|
|
401
627
|
llm: LLM,
|
|
@@ -413,11 +639,14 @@ def generate_sql(
|
|
|
413
639
|
note: Optional[str] = '',
|
|
414
640
|
db_is_case_sensitive: Optional[bool] = False,
|
|
415
641
|
graph_depth: Optional[int] = 1,
|
|
642
|
+
with_reasoning: Optional[bool] = False,
|
|
643
|
+
reasoning_steps: Optional[int] = 2,
|
|
416
644
|
debug: Optional[bool] = False,
|
|
417
645
|
timeout: Optional[int] = None,
|
|
418
646
|
) -> dict[str, str]:
|
|
419
647
|
usage_metadata = {}
|
|
420
648
|
concept_metadata = None
|
|
649
|
+
reasoning_status = 'correct'
|
|
421
650
|
|
|
422
651
|
# Use config default timeout if none provided
|
|
423
652
|
if timeout is None:
|
|
@@ -466,6 +695,34 @@ def generate_sql(
|
|
|
466
695
|
if rel_prop_str:
|
|
467
696
|
measures_str += f"\n{rel_prop_str}"
|
|
468
697
|
|
|
698
|
+
|
|
699
|
+
# Build context descriptions
|
|
700
|
+
sensitivity_txt = "- Ensure value comparisons are case-insensitive, e.g., use LOWER(column) = 'value'.\n" if db_is_case_sensitive else ""
|
|
701
|
+
measures_context = f"- {MEASURES_DESCRIPTION}: {measures_str}\n" if measures_str else ""
|
|
702
|
+
has_transitive_relationships = any(
|
|
703
|
+
rel.get('is_transitive')
|
|
704
|
+
for rel in relationships.values()
|
|
705
|
+
) if relationships else False
|
|
706
|
+
transitive_context = f"- {TRANSITIVE_RELATIONSHIP_DESCRIPTION}\n" if has_transitive_relationships else ""
|
|
707
|
+
concept_description = f"- Description: {concept_metadata.get('description')}\n" if concept_metadata and concept_metadata.get('description') else ""
|
|
708
|
+
concept_tags = concept_metadata.get('tags') if concept_metadata and concept_metadata.get('tags') else ""
|
|
709
|
+
cur_date = datetime.now().strftime("%Y-%m-%d")
|
|
710
|
+
|
|
711
|
+
# Build context dict for SQL generation
|
|
712
|
+
current_context = {
|
|
713
|
+
'cur_date': cur_date,
|
|
714
|
+
'datasource_type': datasource_type or 'standard sql',
|
|
715
|
+
'schema': schema,
|
|
716
|
+
'concept': concept,
|
|
717
|
+
'concept_description': concept_description or "",
|
|
718
|
+
'concept_tags': concept_tags or "",
|
|
719
|
+
'columns_str': columns_str,
|
|
720
|
+
'measures_context': measures_context,
|
|
721
|
+
'transitive_context': transitive_context,
|
|
722
|
+
'sensitivity_txt': sensitivity_txt,
|
|
723
|
+
'max_limit': max_limit,
|
|
724
|
+
}
|
|
725
|
+
|
|
469
726
|
sql_query = None
|
|
470
727
|
iteration = 0
|
|
471
728
|
is_sql_valid = True
|
|
@@ -474,39 +731,30 @@ def generate_sql(
|
|
|
474
731
|
iteration += 1
|
|
475
732
|
err_txt = f"\nThe original SQL (`{sql_query}`) was invalid with error: {error}. Please generate a corrected query." if error and "snowflake" not in llm._llm_type else ""
|
|
476
733
|
|
|
477
|
-
sensitivity_txt = "- Ensure value comparisons are case-insensitive, e.g., use LOWER(column) = 'value'.\n" if db_is_case_sensitive else ""
|
|
478
|
-
|
|
479
|
-
measures_context = f"- {MEASURES_DESCRIPTION}: {measures_str}\n" if measures_str else ""
|
|
480
|
-
has_transitive_relationships = any(
|
|
481
|
-
rel.get('is_transitive')
|
|
482
|
-
for rel in relationships.values()
|
|
483
|
-
) if relationships else False
|
|
484
|
-
transitive_context = f"- {TRANSITIVE_RELATIONSHIP_DESCRIPTION}\n" if has_transitive_relationships else ""
|
|
485
|
-
concept_description = f"- Description: {concept_metadata.get('description')}\n" if concept_metadata and concept_metadata.get('description') else ""
|
|
486
|
-
concept_tags = concept_metadata.get('tags') if concept_metadata and concept_metadata.get('tags') else ""
|
|
487
|
-
cur_date = datetime.now().strftime("%Y-%m-%d")
|
|
488
|
-
prompt = generate_sql_prompt.format_messages(
|
|
489
|
-
current_date=cur_date,
|
|
490
|
-
datasource_type=datasource_type or 'standard sql',
|
|
491
|
-
schema=schema,
|
|
492
|
-
concept=f"`{concept}`",
|
|
493
|
-
description=concept_description or "",
|
|
494
|
-
tags=concept_tags or "",
|
|
495
|
-
question=question,
|
|
496
|
-
columns=columns_str,
|
|
497
|
-
measures_context=measures_context,
|
|
498
|
-
transitive_context=transitive_context,
|
|
499
|
-
sensitivity_context=sensitivity_txt,
|
|
500
|
-
max_limit=max_limit,
|
|
501
|
-
note=note + err_txt,
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
apx_token_count = _calculate_token_count(llm, prompt)
|
|
505
|
-
if "snowflake" in llm._llm_type:
|
|
506
|
-
_clean_snowflake_prompt(prompt)
|
|
507
|
-
|
|
508
734
|
try:
|
|
509
|
-
|
|
735
|
+
result = _generate_sql_with_llm(
|
|
736
|
+
question=question,
|
|
737
|
+
llm=llm,
|
|
738
|
+
conn_params=conn_params,
|
|
739
|
+
generate_sql_prompt=generate_sql_prompt,
|
|
740
|
+
current_context=current_context,
|
|
741
|
+
note=note + err_txt,
|
|
742
|
+
should_validate_sql=should_validate_sql,
|
|
743
|
+
timeout=timeout,
|
|
744
|
+
debug=debug,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
usage_metadata['generate_sql'] = {
|
|
748
|
+
"approximate": result['apx_token_count'],
|
|
749
|
+
**result['usage_metadata'],
|
|
750
|
+
}
|
|
751
|
+
if debug and 'p_hash' in result:
|
|
752
|
+
usage_metadata['generate_sql']["p_hash"] = result['p_hash']
|
|
753
|
+
|
|
754
|
+
sql_query = result['sql']
|
|
755
|
+
is_sql_valid = result['is_valid']
|
|
756
|
+
error = result['error']
|
|
757
|
+
|
|
510
758
|
except TimeoutError as e:
|
|
511
759
|
error = f"LLM call timed out: {str(e)}"
|
|
512
760
|
raise Exception(error)
|
|
@@ -516,18 +764,61 @@ def generate_sql(
|
|
|
516
764
|
continue
|
|
517
765
|
else:
|
|
518
766
|
raise Exception(error)
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
if with_reasoning and sql_query is not None:
|
|
770
|
+
for step in range(reasoning_steps):
|
|
771
|
+
try:
|
|
772
|
+
# Step 1: Evaluate the current SQL
|
|
773
|
+
eval_result = _evaluate_sql_with_reasoning(
|
|
774
|
+
question=question,
|
|
775
|
+
sql_query=sql_query,
|
|
776
|
+
llm=llm,
|
|
777
|
+
timeout=timeout,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
usage_metadata[f'sql_reasoning_step_{step}'] = {
|
|
781
|
+
"approximate": eval_result['apx_token_count'],
|
|
782
|
+
**eval_result['usage_metadata'],
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
evaluation = eval_result['evaluation']
|
|
786
|
+
reasoning_status = evaluation.get("assessment", "partial").lower()
|
|
787
|
+
|
|
788
|
+
if reasoning_status == "correct":
|
|
789
|
+
break
|
|
790
|
+
|
|
791
|
+
# Step 2: Regenerate SQL with feedback
|
|
792
|
+
evaluation_note = note + f"\n\nThe previously generated SQL: `{sql_query}` was assessed as '{evaluation.get('assessment')}' because: {evaluation.get('reasoning', '*could not determine cause*')}. Please provide a corrected SQL query that better answers the question: '{question}'."
|
|
793
|
+
|
|
794
|
+
regen_result = _generate_sql_with_llm(
|
|
795
|
+
question=question,
|
|
796
|
+
llm=llm,
|
|
797
|
+
conn_params=conn_params,
|
|
798
|
+
generate_sql_prompt=generate_sql_prompt,
|
|
799
|
+
current_context=current_context,
|
|
800
|
+
note=evaluation_note,
|
|
801
|
+
should_validate_sql=should_validate_sql,
|
|
802
|
+
timeout=timeout,
|
|
803
|
+
debug=debug,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
usage_metadata[f'generate_sql_reasoning_step_{step}'] = {
|
|
807
|
+
"approximate": regen_result['apx_token_count'],
|
|
808
|
+
**regen_result['usage_metadata'],
|
|
809
|
+
}
|
|
810
|
+
if debug and 'p_hash' in regen_result:
|
|
811
|
+
usage_metadata[f'generate_sql_reasoning_step_{step}']['p_hash'] = regen_result['p_hash']
|
|
812
|
+
|
|
813
|
+
sql_query = regen_result['sql']
|
|
814
|
+
is_sql_valid = regen_result['is_valid']
|
|
815
|
+
error = regen_result['error']
|
|
816
|
+
|
|
817
|
+
except TimeoutError as e:
|
|
818
|
+
raise Exception(f"LLM call timed out: {str(e)}")
|
|
819
|
+
except Exception as e:
|
|
820
|
+
print(f"Warning: LLM reasoning failed: {e}")
|
|
821
|
+
break
|
|
531
822
|
|
|
532
823
|
return {
|
|
533
824
|
"sql": sql_query,
|
|
@@ -535,6 +826,7 @@ def generate_sql(
|
|
|
535
826
|
"schema": schema,
|
|
536
827
|
"error": error if not is_sql_valid else None,
|
|
537
828
|
"is_sql_valid": is_sql_valid if should_validate_sql else None,
|
|
829
|
+
"reasoning_status": reasoning_status,
|
|
538
830
|
"usage_metadata": usage_metadata,
|
|
539
831
|
}
|
|
540
832
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-timbr
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.6
|
|
4
4
|
Summary: LangChain & LangGraph extensions that parse LLM prompts into Timbr semantic SQL and execute them.
|
|
5
5
|
Project-URL: Homepage, https://github.com/WPSemantix/langchain-timbr
|
|
6
6
|
Project-URL: Documentation, https://docs.timbr.ai/doc/docs/integration/langchain-sdk/
|
|
@@ -19,44 +19,17 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
21
|
Requires-Python: <3.13,>=3.10
|
|
22
|
-
Requires-Dist: anthropic==0.42.0
|
|
23
|
-
Requires-Dist: azure-identity==1.25.0; python_version >= '3.11'
|
|
24
|
-
Requires-Dist: azure-identity>=1.16.1; python_version == '3.10'
|
|
25
22
|
Requires-Dist: cryptography==45.0.7; python_version >= '3.11'
|
|
26
23
|
Requires-Dist: cryptography>=44.0.3; python_version == '3.10'
|
|
27
|
-
Requires-Dist: databricks-langchain==0.7.1
|
|
28
|
-
Requires-Dist: databricks-sdk==0.64.0
|
|
29
|
-
Requires-Dist: google-generativeai==0.8.4
|
|
30
|
-
Requires-Dist: langchain-anthropic==0.3.5; python_version >= '3.11'
|
|
31
|
-
Requires-Dist: langchain-anthropic>=0.3.1; python_version == '3.10'
|
|
32
24
|
Requires-Dist: langchain-community==0.3.30; python_version >= '3.11'
|
|
33
|
-
Requires-Dist: langchain-community>=0.3.
|
|
34
|
-
Requires-Dist: langchain-core
|
|
35
|
-
Requires-Dist: langchain-core>=0.3.58; python_version == '3.10'
|
|
36
|
-
Requires-Dist: langchain-google-genai==2.0.10; python_version >= '3.11'
|
|
37
|
-
Requires-Dist: langchain-google-genai>=2.0.9; python_version == '3.10'
|
|
38
|
-
Requires-Dist: langchain-google-vertexai==2.1.2; python_version >= '3.11'
|
|
39
|
-
Requires-Dist: langchain-google-vertexai>=2.0.28; python_version == '3.10'
|
|
40
|
-
Requires-Dist: langchain-openai==0.3.34; python_version >= '3.11'
|
|
41
|
-
Requires-Dist: langchain-openai>=0.3.16; python_version == '3.10'
|
|
42
|
-
Requires-Dist: langchain-tests==0.3.22; python_version >= '3.11'
|
|
43
|
-
Requires-Dist: langchain-tests>=0.3.20; python_version == '3.10'
|
|
25
|
+
Requires-Dist: langchain-community>=0.3.27; python_version == '3.10'
|
|
26
|
+
Requires-Dist: langchain-core>=0.3.80
|
|
44
27
|
Requires-Dist: langchain==0.3.27; python_version >= '3.11'
|
|
45
28
|
Requires-Dist: langchain>=0.3.25; python_version == '3.10'
|
|
46
29
|
Requires-Dist: langgraph==0.6.8; python_version >= '3.11'
|
|
47
30
|
Requires-Dist: langgraph>=0.3.20; python_version == '3.10'
|
|
48
|
-
Requires-Dist: openai==2.1.0; python_version >= '3.11'
|
|
49
|
-
Requires-Dist: openai>=1.77.0; python_version == '3.10'
|
|
50
|
-
Requires-Dist: opentelemetry-api==1.38.0; python_version == '3.10'
|
|
51
|
-
Requires-Dist: opentelemetry-sdk==1.38.0; python_version == '3.10'
|
|
52
31
|
Requires-Dist: pydantic==2.10.4
|
|
53
|
-
Requires-Dist:
|
|
54
|
-
Requires-Dist: pytimbr-api==2.0.0; python_version >= '3.11'
|
|
55
|
-
Requires-Dist: pytimbr-api>=2.0.0; python_version == '3.10'
|
|
56
|
-
Requires-Dist: snowflake-snowpark-python==1.39.1; python_version >= '3.11'
|
|
57
|
-
Requires-Dist: snowflake-snowpark-python>=1.39.1; python_version == '3.10'
|
|
58
|
-
Requires-Dist: snowflake==1.8.0; python_version >= '3.11'
|
|
59
|
-
Requires-Dist: snowflake>=1.8.0; python_version == '3.10'
|
|
32
|
+
Requires-Dist: pytimbr-api>=2.1.0
|
|
60
33
|
Requires-Dist: tiktoken==0.8.0
|
|
61
34
|
Requires-Dist: transformers==4.57.0; python_version >= '3.11'
|
|
62
35
|
Requires-Dist: transformers>=4.53; python_version == '3.10'
|
|
@@ -70,6 +43,7 @@ Requires-Dist: databricks-sdk==0.64.0; extra == 'all'
|
|
|
70
43
|
Requires-Dist: google-generativeai==0.8.4; extra == 'all'
|
|
71
44
|
Requires-Dist: langchain-anthropic==0.3.5; (python_version >= '3.11') and extra == 'all'
|
|
72
45
|
Requires-Dist: langchain-anthropic>=0.3.1; (python_version == '3.10') and extra == 'all'
|
|
46
|
+
Requires-Dist: langchain-aws<1,>=0.2.35; extra == 'all'
|
|
73
47
|
Requires-Dist: langchain-google-genai==2.0.10; (python_version >= '3.11') and extra == 'all'
|
|
74
48
|
Requires-Dist: langchain-google-genai>=2.0.9; (python_version == '3.10') and extra == 'all'
|
|
75
49
|
Requires-Dist: langchain-google-vertexai==2.1.2; (python_version >= '3.11') and extra == 'all'
|
|
@@ -97,6 +71,8 @@ Requires-Dist: langchain-openai==0.3.34; (python_version >= '3.11') and extra ==
|
|
|
97
71
|
Requires-Dist: langchain-openai>=0.3.16; (python_version == '3.10') and extra == 'azure-openai'
|
|
98
72
|
Requires-Dist: openai==2.1.0; (python_version >= '3.11') and extra == 'azure-openai'
|
|
99
73
|
Requires-Dist: openai>=1.77.0; (python_version == '3.10') and extra == 'azure-openai'
|
|
74
|
+
Provides-Extra: bedrock
|
|
75
|
+
Requires-Dist: langchain-aws==0.2.35; extra == 'bedrock'
|
|
100
76
|
Provides-Extra: databricks
|
|
101
77
|
Requires-Dist: databricks-langchain==0.7.1; extra == 'databricks'
|
|
102
78
|
Requires-Dist: databricks-sdk==0.64.0; extra == 'databricks'
|
|
@@ -158,7 +134,7 @@ python -m pip install langchain-timbr
|
|
|
158
134
|
|
|
159
135
|
### Install with selected LLM providers
|
|
160
136
|
|
|
161
|
-
#### One of: openai, anthropic, google, azure_openai, snowflake, databricks, vertex_ai (or 'all')
|
|
137
|
+
#### One of: openai, anthropic, google, azure_openai, snowflake, databricks, vertex_ai, bedrock (or 'all')
|
|
162
138
|
|
|
163
139
|
```bash
|
|
164
140
|
python -m pip install 'langchain-timbr[<your selected providers, separated by comma w/o space>]'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
langchain_timbr/__init__.py,sha256=gxd6Y6QDmYZtPlYVdXtPIy501hMOZXHjWh2qq4qzt_s,828
|
|
2
|
+
langchain_timbr/_version.py,sha256=EwjLAOHxpsOZpfhlwgV2gSqwFh_1AQNnM-WrOk4-4zg,704
|
|
3
|
+
langchain_timbr/config.py,sha256=CdDuUKbI_sdjNkGVk_QEmeqQHjFlJjBY1VI19JuBG2o,1601
|
|
4
|
+
langchain_timbr/timbr_llm_connector.py,sha256=mdkWskpvmXZre5AzVFn6KfPnVH5YN5MIwfEoXWBLMgY,13170
|
|
5
|
+
langchain_timbr/langchain/__init__.py,sha256=ejcsZKP9PK0j4WrrCCcvBXpDpP-TeRiVb21OIUJqix8,580
|
|
6
|
+
langchain_timbr/langchain/execute_timbr_query_chain.py,sha256=6USOkCJih0yGk7PgYW_zTHJfip4DpFcvkD-VhI2dMp0,16302
|
|
7
|
+
langchain_timbr/langchain/generate_answer_chain.py,sha256=nteA4QZp9CAOskTBl_CokwaMlqnR2g2GvKz2mLs9WVY,4871
|
|
8
|
+
langchain_timbr/langchain/generate_timbr_sql_chain.py,sha256=XmEkEU4q8t66GJZE-pWAAWqYYFFfv9ej6DzjRiTVNRw,9797
|
|
9
|
+
langchain_timbr/langchain/identify_concept_chain.py,sha256=kuzg0jJQpFGIiaxtNhdQ5K4HXveLVwONFNsoipPCteE,7169
|
|
10
|
+
langchain_timbr/langchain/timbr_sql_agent.py,sha256=tIcr2SCSb5LnOA3zreZbzOvVgR8e2NFv4HaLVcLUNCg,20790
|
|
11
|
+
langchain_timbr/langchain/validate_timbr_sql_chain.py,sha256=OcE_7yfb9xpD-I4OS7RG1bY4-yi1UicjvGegOv_vkQU,9567
|
|
12
|
+
langchain_timbr/langgraph/__init__.py,sha256=mKBFd0x01jWpRujUWe-suX3FFhenPoDxrvzs8I0mum0,457
|
|
13
|
+
langchain_timbr/langgraph/execute_timbr_query_node.py,sha256=UddbYiQya_-QlZm-QdImxpvzfiNiRDHyiLAHdpTEzXc,5984
|
|
14
|
+
langchain_timbr/langgraph/generate_response_node.py,sha256=opwscNEXabaSyCFLbzGQFkDFEymJurhNU9aAtm1rnOk,2375
|
|
15
|
+
langchain_timbr/langgraph/generate_timbr_sql_node.py,sha256=TekD9D0rM4aKuzS50Kzwkbshei5NHbTTVTXUC41dnyU,5360
|
|
16
|
+
langchain_timbr/langgraph/identify_concept_node.py,sha256=aiLDFEcz_vM4zZ_ULe1SvJKmI-e4Fb2SibZQaEPz_eY,3649
|
|
17
|
+
langchain_timbr/langgraph/validate_timbr_query_node.py,sha256=-2fuieCz1hv6ua-17zfonme8LQ_OoPnoOBTdGSXkJgs,4793
|
|
18
|
+
langchain_timbr/llm_wrapper/llm_wrapper.py,sha256=j94DqIGECXyfAVayLC7VaNxs_8n1qYFiHY2Qvt2B3Bc,17537
|
|
19
|
+
langchain_timbr/llm_wrapper/timbr_llm_wrapper.py,sha256=sDqDOz0qu8b4WWlagjNceswMVyvEJ8yBWZq2etBh-T0,1362
|
|
20
|
+
langchain_timbr/utils/general.py,sha256=KkehHvIj8GoQ_0KVXLcUVeaYaTtkuzgXmYYx2TXJhI4,10253
|
|
21
|
+
langchain_timbr/utils/prompt_service.py,sha256=QT7kiq72rQno77z1-tvGGD7HlH_wdTQAl_1teSoKEv4,11373
|
|
22
|
+
langchain_timbr/utils/temperature_supported_models.json,sha256=d3UmBUpG38zDjjB42IoGpHTUaf0pHMBRSPY99ao1a3g,1832
|
|
23
|
+
langchain_timbr/utils/timbr_llm_utils.py,sha256=6CHCwMHOPZtLjxh49a4UoxGxgTmuOUO3R8sJMHsGKxI,34970
|
|
24
|
+
langchain_timbr/utils/timbr_utils.py,sha256=SvmQ0wYicODNhmo8c-5_KPDBAfrBVBkUfoO8sPItQhk,17759
|
|
25
|
+
langchain_timbr-2.1.6.dist-info/METADATA,sha256=imT7XLtey6gdk7WJlp6ZcYnndjCE6hAJ6QPzQmgcyJo,10724
|
|
26
|
+
langchain_timbr-2.1.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
27
|
+
langchain_timbr-2.1.6.dist-info/licenses/LICENSE,sha256=0ITGFk2alkC7-e--bRGtuzDrv62USIiVyV2Crf3_L_0,1065
|
|
28
|
+
langchain_timbr-2.1.6.dist-info/RECORD,,
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
langchain_timbr/__init__.py,sha256=gxd6Y6QDmYZtPlYVdXtPIy501hMOZXHjWh2qq4qzt_s,828
|
|
2
|
-
langchain_timbr/_version.py,sha256=TYs-mU9m9aJFweRpjovqttRQsYRnpbbHHMifXX9ryi4,704
|
|
3
|
-
langchain_timbr/config.py,sha256=PEtvNgvnA9UseZJjKgup_O6xdG-VYk3N11nH8p8W1Kg,1410
|
|
4
|
-
langchain_timbr/timbr_llm_connector.py,sha256=mdkWskpvmXZre5AzVFn6KfPnVH5YN5MIwfEoXWBLMgY,13170
|
|
5
|
-
langchain_timbr/langchain/__init__.py,sha256=ejcsZKP9PK0j4WrrCCcvBXpDpP-TeRiVb21OIUJqix8,580
|
|
6
|
-
langchain_timbr/langchain/execute_timbr_query_chain.py,sha256=pedMajyKDI2ZaoyVp1r64nHX015Wy-r96HoJrRlCh48,15579
|
|
7
|
-
langchain_timbr/langchain/generate_answer_chain.py,sha256=XsaQrgBFwoC9ne3jpnuHueUXL1PzNQ75ECC_HVA61Ks,4871
|
|
8
|
-
langchain_timbr/langchain/generate_timbr_sql_chain.py,sha256=3Z0ut78AFCNHKwLwOYH44hzJDIOA-zNF0x8Tjyrvzp4,9098
|
|
9
|
-
langchain_timbr/langchain/identify_concept_chain.py,sha256=kuzg0jJQpFGIiaxtNhdQ5K4HXveLVwONFNsoipPCteE,7169
|
|
10
|
-
langchain_timbr/langchain/timbr_sql_agent.py,sha256=HntpalzCZ-PlHd7na5V0syCMqrREFUpppGM4eHstaZQ,19574
|
|
11
|
-
langchain_timbr/langchain/validate_timbr_sql_chain.py,sha256=OcE_7yfb9xpD-I4OS7RG1bY4-yi1UicjvGegOv_vkQU,9567
|
|
12
|
-
langchain_timbr/langgraph/__init__.py,sha256=mKBFd0x01jWpRujUWe-suX3FFhenPoDxrvzs8I0mum0,457
|
|
13
|
-
langchain_timbr/langgraph/execute_timbr_query_node.py,sha256=rPx_V3OOh-JTGOwrEopHmOmFuM-ngBZdswkW9oZ43hU,5536
|
|
14
|
-
langchain_timbr/langgraph/generate_response_node.py,sha256=BLmsDZfbhncRpO7PEfDpy7CnPE7a55j8QV5jfg30heQ,2247
|
|
15
|
-
langchain_timbr/langgraph/generate_timbr_sql_node.py,sha256=wkau-NajblSVzNIro9IyqawULvz3XaCYSEdYW95vWco,4911
|
|
16
|
-
langchain_timbr/langgraph/identify_concept_node.py,sha256=aiLDFEcz_vM4zZ_ULe1SvJKmI-e4Fb2SibZQaEPz_eY,3649
|
|
17
|
-
langchain_timbr/langgraph/validate_timbr_query_node.py,sha256=-2fuieCz1hv6ua-17zfonme8LQ_OoPnoOBTdGSXkJgs,4793
|
|
18
|
-
langchain_timbr/llm_wrapper/llm_wrapper.py,sha256=_oQZQHJUWskIm2L-86jUGwM5ZhaE34fCsueLHhg4Le0,14944
|
|
19
|
-
langchain_timbr/llm_wrapper/timbr_llm_wrapper.py,sha256=sDqDOz0qu8b4WWlagjNceswMVyvEJ8yBWZq2etBh-T0,1362
|
|
20
|
-
langchain_timbr/utils/general.py,sha256=KkehHvIj8GoQ_0KVXLcUVeaYaTtkuzgXmYYx2TXJhI4,10253
|
|
21
|
-
langchain_timbr/utils/prompt_service.py,sha256=QT7kiq72rQno77z1-tvGGD7HlH_wdTQAl_1teSoKEv4,11373
|
|
22
|
-
langchain_timbr/utils/temperature_supported_models.json,sha256=d3UmBUpG38zDjjB42IoGpHTUaf0pHMBRSPY99ao1a3g,1832
|
|
23
|
-
langchain_timbr/utils/timbr_llm_utils.py,sha256=_4Qz5SX5cXW1Rl_fSBcE9P3uPEaI8DBg3GpXA4uQGoI,23102
|
|
24
|
-
langchain_timbr/utils/timbr_utils.py,sha256=SvmQ0wYicODNhmo8c-5_KPDBAfrBVBkUfoO8sPItQhk,17759
|
|
25
|
-
langchain_timbr-2.1.4.dist-info/METADATA,sha256=Wtzy14BfbEujvuYozb4XcaUz6pp87zxjNNBbt9Z5wSQ,12268
|
|
26
|
-
langchain_timbr-2.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
27
|
-
langchain_timbr-2.1.4.dist-info/licenses/LICENSE,sha256=0ITGFk2alkC7-e--bRGtuzDrv62USIiVyV2Crf3_L_0,1065
|
|
28
|
-
langchain_timbr-2.1.4.dist-info/RECORD,,
|
|
File without changes
|