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,138 @@
1
+ from typing import Optional, Union, Dict, Any
2
+ from langchain.chains.base import Chain
3
+ from langchain.llms.base import LLM
4
+
5
+ from ..utils.general import parse_list, to_boolean, to_integer
6
+ from ..utils.timbr_llm_utils import determine_concept
7
+
8
+
9
+ class IdentifyTimbrConceptChain(Chain):
10
+ """
11
+ LangChain chain for identifying relevant concepts from user prompts using Timbr knowledge graphs.
12
+
13
+ This chain analyzes natural language prompts to determine the most appropriate concept(s)
14
+ within a Timbr ontology/knowledge graph that best matches the user's intent. It uses an LLM
15
+ to process prompts and connects to Timbr via URL and token for concept identification.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ llm: LLM,
21
+ url: str,
22
+ token: str,
23
+ ontology: str,
24
+ concepts_list: Optional[Union[list[str], str]] = None,
25
+ views_list: Optional[Union[list[str], str]] = None,
26
+ include_logic_concepts: Optional[bool] = False,
27
+ include_tags: Optional[Union[list[str], str]] = None,
28
+ should_validate: Optional[bool] = False,
29
+ retries: Optional[int] = 3,
30
+ note: Optional[str] = '',
31
+ verify_ssl: Optional[bool] = True,
32
+ is_jwt: Optional[bool] = False,
33
+ jwt_tenant_id: Optional[str] = None,
34
+ conn_params: Optional[dict] = None,
35
+ debug: Optional[bool] = False,
36
+ **kwargs,
37
+ ):
38
+ """
39
+ :param llm: An LLM instance or a function that takes a prompt string and returns the LLM’s response
40
+ :param url: Timbr server url
41
+ :param token: Timbr password or token value
42
+ :param ontology: The name of the ontology/knowledge graph
43
+ :param concepts_list: Optional specific concept options to query
44
+ :param views_list: Optional specific view options to query
45
+ :param include_logic_concepts: Optional boolean to include logic concepts (concepts without unique properties which only inherits from an upper level concept with filter logic) in the query.
46
+ :param include_tags: Optional specific concepts & properties tag options to use in the query (Disabled by default. Use '*' to enable all tags or a string represents a list of tags divided by commas (e.g. 'tag1,tag2')
47
+ :param should_validate: Whether to validate the identified concept before returning it
48
+ :param retries: Number of retry attempts if the identified concept is invalid
49
+ :param note: Optional additional note to extend our llm prompt
50
+ :param verify_ssl: Whether to verify SSL certificates (default is True).
51
+ :param is_jwt: Whether to use JWT authentication (default is False).
52
+ :param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
53
+ :param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
54
+ :param kwargs: Additional arguments to pass to the base
55
+
56
+ ## Example
57
+ ```
58
+ identify_timbr_concept_chain = IdentifyTimbrConceptChain(
59
+ url=<url>,
60
+ token=<token>,
61
+ llm=<llm or timbr_llm_wrapper instance>,
62
+ ontology=<ontology_name>,
63
+ concepts_list=<concepts>,
64
+ views_list=<views>,
65
+ include_tags=<tags>,
66
+ note=<note>,
67
+ )
68
+
69
+ return identify_timbr_concept_chain.invoke({ "prompt": question }).get("concept", None)
70
+ ```
71
+ """
72
+ super().__init__(**kwargs)
73
+ self._llm = llm
74
+ self._url = url
75
+ self._token = token
76
+ self._ontology = ontology
77
+ self._concepts_list = parse_list(concepts_list)
78
+ self._views_list = parse_list(views_list)
79
+ self._include_logic_concepts = to_boolean(include_logic_concepts)
80
+ self._include_tags = parse_list(include_tags)
81
+ self._should_validate = to_boolean(should_validate)
82
+ self._retries = to_integer(retries)
83
+ self._note = note
84
+ self._verify_ssl = to_boolean(verify_ssl)
85
+ self._is_jwt = to_boolean(is_jwt)
86
+ self._jwt_tenant_id = jwt_tenant_id
87
+ self._debug = to_boolean(debug)
88
+ self._conn_params = conn_params or {}
89
+
90
+
91
+ @property
92
+ def usage_metadata_key(self) -> str:
93
+ return "identify_concept_usage_metadata"
94
+
95
+
96
+ @property
97
+ def input_keys(self) -> list:
98
+ return ["prompt"]
99
+
100
+
101
+ @property
102
+ def output_keys(self) -> list:
103
+ return ["schema", "concept", "concept_metadata", self.usage_metadata_key]
104
+
105
+
106
+ def _get_conn_params(self) -> dict:
107
+ return {
108
+ "url": self._url,
109
+ "token": self._token,
110
+ "ontology": self._ontology,
111
+ "verify_ssl": self._verify_ssl,
112
+ "is_jwt": self._is_jwt,
113
+ "jwt_tenant_id": self._jwt_tenant_id,
114
+ **self._conn_params,
115
+ }
116
+
117
+
118
+ def _call(self, inputs: Dict[str, Any], run_manager=None) -> Dict[str, str]:
119
+ prompt = inputs["prompt"]
120
+ res = determine_concept(
121
+ question=prompt,
122
+ llm=self._llm,
123
+ conn_params=self._get_conn_params(),
124
+ concepts_list=self._concepts_list,
125
+ views_list=self._views_list,
126
+ include_logic_concepts=self._include_logic_concepts,
127
+ include_tags=self._include_tags,
128
+ should_validate=self._should_validate,
129
+ retries=self._retries,
130
+ note=self._note,
131
+ debug=self._debug,
132
+ )
133
+
134
+ usage_metadata = res.pop("usage_metadata", {})
135
+ return {
136
+ **res,
137
+ self.usage_metadata_key: usage_metadata,
138
+ }
@@ -0,0 +1,418 @@
1
+ from typing import Optional, Any, Union
2
+ from langchain.agents import AgentExecutor, BaseSingleActionAgent
3
+ from langchain.llms.base import LLM
4
+ from langchain.schema import AgentAction, AgentFinish
5
+
6
+ from ..utils.general import parse_list, to_boolean, to_integer
7
+ from .execute_timbr_query_chain import ExecuteTimbrQueryChain
8
+ from .generate_answer_chain import GenerateAnswerChain
9
+
10
+ class TimbrSqlAgent(BaseSingleActionAgent):
11
+ def __init__(
12
+ self,
13
+ llm: LLM,
14
+ url: str,
15
+ token: str,
16
+ ontology: str,
17
+ schema: Optional[str] = 'dtimbr',
18
+ concept: Optional[str] = None,
19
+ concepts_list: Optional[Union[list[str], str]] = None,
20
+ views_list: Optional[Union[list[str], str]] = None,
21
+ include_logic_concepts: Optional[bool] = False,
22
+ include_tags: Optional[Union[list[str], str]] = None,
23
+ exclude_properties: Optional[Union[list[str], str]] = ['entity_id', 'entity_type', 'entity_label'],
24
+ should_validate_sql: Optional[bool] = True,
25
+ retries: Optional[int] = 3,
26
+ max_limit: Optional[int] = 500,
27
+ retry_if_no_results: Optional[bool] = False,
28
+ no_results_max_retries: Optional[int] = 2,
29
+ generate_answer: Optional[bool] = False,
30
+ note: Optional[str] = '',
31
+ db_is_case_sensitive: Optional[bool] = False,
32
+ graph_depth: Optional[int] = 1,
33
+ verify_ssl: Optional[bool] = True,
34
+ is_jwt: Optional[bool] = False,
35
+ jwt_tenant_id: Optional[str] = None,
36
+ conn_params: Optional[dict] = None,
37
+ debug: Optional[bool] = False
38
+ ):
39
+ """
40
+ :param llm: Language model to use
41
+ :param url: Timbr server URL
42
+ :param token: Timbr authentication token
43
+ :param ontology: Name of the ontology/knowledge graph
44
+ :param schema: Optional specific schema name to query
45
+ :param concept: Optional specific concept name to query
46
+ :param concepts_list: Optional specific concept options to query
47
+ :param views_list: Optional specific view options to query
48
+ :param include_logic_concepts: Optional boolean to include logic concepts (concepts without unique properties which only inherits from an upper level concept with filter logic) in the query.
49
+ :param include_tags: Optional specific concepts & properties tag options to use in the query (Disabled by default). Use '*' to enable all tags or a string represents a list of tags divided by commas (e.g. 'tag1,tag2')
50
+ :param exclude_properties: Optional specific properties to exclude from the query (entity_id, entity_type & entity_label by default).
51
+ :param should_validate_sql: Whether to validate the SQL before executing it
52
+ :param retries: Number of retry attempts if the generated SQL is invalid
53
+ :param max_limit: Maximum number of rows to return
54
+ :retry_if_no_results: Whether to infer the result value from the SQL query. If the query won't return any rows, it will try to re-generate the SQL query then re-run it.
55
+ :param no_results_max_retries: Number of retry attempts to infer the result value from the SQL query
56
+ :param generate_answer: Whether to generate a natural language answer from the query results (default is False, which means the agent will return the SQL and rows only).
57
+ :param note: Optional additional note to extend our llm prompt
58
+ :param db_is_case_sensitive: Whether the database is case sensitive (default is False).
59
+ :param graph_depth: Maximum number of relationship hops to traverse from the source concept during schema exploration (default is 1).
60
+ :param verify_ssl: Whether to verify SSL certificates (default is True).
61
+ :param is_jwt: Whether to use JWT authentication (default is False).
62
+ :param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
63
+ :param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
64
+
65
+ ## Example
66
+ ```
67
+ agent = TimbrSqlAgent(
68
+ llm=<llm>,
69
+ url=<url>,
70
+ token=<token>,
71
+ ontology=<ontology>,
72
+ schema=<schema>,
73
+ concept=<concept>,
74
+ concepts_list=<concepts>,
75
+ views_list=<views>,
76
+ should_validate_sql=<should_validate_sql>,
77
+ retries=<retries>,
78
+ note=<note>,
79
+ )
80
+ ```
81
+ """
82
+ super().__init__()
83
+ self._chain = ExecuteTimbrQueryChain(
84
+ llm=llm,
85
+ url=url,
86
+ token=token,
87
+ ontology=ontology,
88
+ schema=schema,
89
+ concept=concept,
90
+ concepts_list=parse_list(concepts_list),
91
+ views_list=parse_list(views_list),
92
+ include_logic_concepts=to_boolean(include_logic_concepts),
93
+ include_tags=parse_list(include_tags),
94
+ exclude_properties=parse_list(exclude_properties),
95
+ should_validate_sql=to_boolean(should_validate_sql),
96
+ retries=to_integer(retries),
97
+ max_limit=to_integer(max_limit),
98
+ retry_if_no_results=to_boolean(retry_if_no_results),
99
+ no_results_max_retries=to_integer(no_results_max_retries),
100
+ note=note,
101
+ db_is_case_sensitive=to_boolean(db_is_case_sensitive),
102
+ graph_depth=to_integer(graph_depth),
103
+ verify_ssl=to_boolean(verify_ssl),
104
+ is_jwt=to_boolean(is_jwt),
105
+ jwt_tenant_id=jwt_tenant_id,
106
+ conn_params=conn_params,
107
+ debug=to_boolean(debug),
108
+ )
109
+ self._generate_answer = to_boolean(generate_answer)
110
+
111
+ # Pre-initialize the answer chain to avoid creating it on every request
112
+ self._answer_chain = GenerateAnswerChain(
113
+ llm=llm,
114
+ url=url,
115
+ token=token,
116
+ verify_ssl=to_boolean(verify_ssl),
117
+ is_jwt=to_boolean(is_jwt),
118
+ jwt_tenant_id=jwt_tenant_id,
119
+ conn_params=conn_params,
120
+ debug=to_boolean(debug),
121
+ ) if self._generate_answer else None
122
+
123
+
124
+ def _should_skip_answer_generation(self, result: dict) -> bool:
125
+ """
126
+ Determine if answer generation should be skipped based on result content.
127
+ This can save LLM calls when there's an error or no meaningful data.
128
+ """
129
+ if not self._generate_answer:
130
+ return True
131
+
132
+ # Skip if there's an error
133
+ if result.get("error"):
134
+ return True
135
+
136
+ # Skip if no rows returned
137
+ rows = result.get("rows", [])
138
+ if not rows or len(rows) == 0:
139
+ return True
140
+
141
+ return False
142
+
143
+
144
+ @property
145
+ def input_keys(self) -> list[str]:
146
+ """Get the input keys required by the agent."""
147
+ return ["input"]
148
+
149
+
150
+ def plan(
151
+ self, intermediate_steps: list[tuple[AgentAction, str]], **kwargs: Any
152
+ ) -> Union[AgentAction, AgentFinish]:
153
+ """Plan the next action based on the input."""
154
+ user_input = kwargs.get("input", "")
155
+
156
+ # Enhanced input validation
157
+ if not user_input or not user_input.strip():
158
+ return AgentFinish(
159
+ return_values={
160
+ "error": "No input provided or input is empty",
161
+ "answer": None,
162
+ "rows": None,
163
+ "sql": None,
164
+ "schema": None,
165
+ "concept": None,
166
+ "usage_metadata": {},
167
+ },
168
+ log="Empty input received"
169
+ )
170
+
171
+ try:
172
+ result = self._chain.invoke({ "prompt": user_input })
173
+ answer = None
174
+ usage_metadata = result.get(self._chain.usage_metadata_key, {})
175
+
176
+ if self._answer_chain and not self._should_skip_answer_generation(result):
177
+ answer_res = self._answer_chain.invoke({
178
+ "prompt": user_input,
179
+ "rows": result.get("rows"),
180
+ "sql": result.get("sql")
181
+ })
182
+ answer = answer_res.get("answer", "")
183
+ usage_metadata.update(answer_res.get(self._answer_chain.usage_metadata_key, {}))
184
+
185
+ return AgentFinish(
186
+ return_values={
187
+ "answer": answer,
188
+ "rows": result.get("rows", []),
189
+ "sql": result.get("sql", ""),
190
+ "schema": result.get("schema", ""),
191
+ "concept": result.get("concept", ""),
192
+ "error": result.get("error", None),
193
+ "usage_metadata": usage_metadata,
194
+ },
195
+ log=f"Successfully executed query on concept: {result.get('concept', '')}"
196
+ )
197
+ except Exception as e:
198
+ error_context = f"Error in TimbrSqlAgent.plan (sync): {str(e)}"
199
+ return AgentFinish(
200
+ return_values={
201
+ "error": str(e),
202
+ "answer": None,
203
+ "rows": None,
204
+ "sql": None,
205
+ "schema": None,
206
+ "concept": None,
207
+ "usage_metadata": {},
208
+ },
209
+ log=error_context
210
+ )
211
+
212
+ async def aplan(
213
+ self, intermediate_steps: list[tuple[AgentAction, str]], **kwargs: Any
214
+ ) -> Union[AgentAction, AgentFinish]:
215
+ """Async version of the plan method."""
216
+ user_input = kwargs.get("input", "")
217
+
218
+ if not user_input or not user_input.strip():
219
+ return AgentFinish(
220
+ return_values={
221
+ "error": "No input provided or input is empty",
222
+ "answer": None,
223
+ "rows": None,
224
+ "sql": None,
225
+ "schema": None,
226
+ "concept": None,
227
+ "usage_metadata": {},
228
+ },
229
+ log="Empty or whitespace-only input received"
230
+ )
231
+
232
+ try:
233
+ # Use async invoke if available, fallback to sync
234
+ if hasattr(self._chain, 'ainvoke'):
235
+ result = await self._chain.ainvoke({ "prompt": user_input })
236
+ else:
237
+ result = self._chain.invoke({ "prompt": user_input })
238
+
239
+ answer = None
240
+ usage_metadata = result.get("usage_metadata", {})
241
+
242
+ if not self._should_skip_answer_generation(result) and self._answer_chain:
243
+ # Use async invoke if available for answer chain too
244
+ if hasattr(self._answer_chain, 'ainvoke'):
245
+ answer_res = await self._answer_chain.ainvoke({
246
+ "prompt": user_input,
247
+ "rows": result.get("rows"),
248
+ "sql": result.get("sql")
249
+ })
250
+ else:
251
+ answer_res = self._answer_chain.invoke({
252
+ "prompt": user_input,
253
+ "rows": result.get("rows"),
254
+ "sql": result.get("sql")
255
+ })
256
+ answer = answer_res.get("answer", "")
257
+ usage_metadata.update(answer_res.get(self._answer_chain.usage_metadata_key, {}))
258
+
259
+ return AgentFinish(
260
+ return_values={
261
+ "answer": answer,
262
+ "rows": result.get("rows", []),
263
+ "sql": result.get("sql", ""),
264
+ "schema": result.get("schema", ""),
265
+ "concept": result.get("concept", ""),
266
+ "error": result.get("error", None),
267
+ "usage_metadata": usage_metadata,
268
+ },
269
+ log=f"Successfully executed query on concept: {result.get('concept', '')}"
270
+ )
271
+ except Exception as e:
272
+ error_context = f"Error in TimbrSqlAgent.aplan (async): {str(e)}"
273
+ return AgentFinish(
274
+ return_values={
275
+ "error": str(e),
276
+ "answer": None,
277
+ "rows": None,
278
+ "sql": None,
279
+ "schema": None,
280
+ "concept": None,
281
+ "usage_metadata": {},
282
+ },
283
+ log=error_context
284
+ )
285
+
286
+ @property
287
+ def return_values(self) -> list[str]:
288
+ """Get the return values that this agent can produce."""
289
+ return [
290
+ "answer",
291
+ "rows",
292
+ "sql",
293
+ "schema",
294
+ "concept",
295
+ "error",
296
+ "usage_metadata",
297
+ ]
298
+
299
+
300
+ def create_timbr_sql_agent(
301
+ llm: LLM,
302
+ url: str,
303
+ token: str,
304
+ ontology: str,
305
+ schema: Optional[str] = 'dtimbr',
306
+ concept: Optional[str] = None,
307
+ concepts_list: Optional[Union[list[str], str]] = None,
308
+ views_list: Optional[Union[list[str], str]] = None,
309
+ include_logic_concepts: Optional[bool] = False,
310
+ include_tags: Optional[Union[list[str], str]] = None,
311
+ exclude_properties: Optional[Union[list[str], str]] = ['entity_id', 'entity_type', 'entity_label'],
312
+ should_validate_sql: Optional[bool] = True,
313
+ retries: Optional[int] = 3,
314
+ max_limit: Optional[int] = 500,
315
+ retry_if_no_results: Optional[bool] = False,
316
+ no_results_max_retries: Optional[int] = 2,
317
+ generate_answer: Optional[bool] = False,
318
+ note: Optional[str] = '',
319
+ db_is_case_sensitive: Optional[bool] = False,
320
+ graph_depth: Optional[int] = 1,
321
+ verify_ssl: Optional[bool] = True,
322
+ is_jwt: Optional[bool] = False,
323
+ jwt_tenant_id: Optional[str] = None,
324
+ conn_params: Optional[dict] = None,
325
+ debug: Optional[bool] = False
326
+ ) -> AgentExecutor:
327
+ """
328
+ Create and configure a Timbr agent with its executor.
329
+
330
+ :param llm: Language model to use
331
+ :param url: Timbr server URL
332
+ :param token: Timbr authentication token
333
+ :param ontology: Name of the ontology/knowledge graph
334
+ :param schema: Optional specific schema name to query
335
+ :param concept: Optional specific concept name to query
336
+ :param concepts_list: Optional specific concept options to query
337
+ :param views_list: Optional specific view options to query
338
+ :param include_logic_concepts: Optional boolean to include logic concepts (concepts without unique properties which only inherits from an upper level concept with filter logic) in the query.
339
+ :param include_tags: Optional specific concepts & properties tag options to use in the query (Disabled by default. Use '*' to enable all tags or a string represents a list of tags divided by commas (e.g. 'tag1,tag2')
340
+ :param exclude_properties: Optional specific properties to exclude from the query (entity_id, entity_type & entity_label by default).
341
+ :param should_validate_sql: Whether to validate the SQL before executing it
342
+ :param retries: Number of retry attempts if the generated SQL is invalid
343
+ :param max_limit: Maximum number of rows to return
344
+ :retry_if_no_results: Whether to infer the result value from the SQL query. If the query won't return any rows, it will try to re-generate the SQL query then re-run it.
345
+ :param no_results_max_retries: Number of retry attempts to infer the result value from the SQL query
346
+ :param generate_answer: Whether to generate an LLM answer based on the SQL results (default is False, which means the agent will return the SQL and rows only).
347
+ :param note: Optional additional note to extend our llm prompt
348
+ :param db_is_case_sensitive: Whether the database is case sensitive (default is False).
349
+ :param graph_depth: Maximum number of relationship hops to traverse from the source concept during schema exploration (default is 1).
350
+ :param verify_ssl: Whether to verify SSL certificates (default is True).
351
+ :param is_jwt: Whether to use JWT authentication (default is False).
352
+ :param jwt_tenant_id: JWT tenant ID for multi-tenant environments (required when is_jwt=True).
353
+ :param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
354
+
355
+ Returns:
356
+ AgentExecutor: Configured agent executor ready to use
357
+
358
+ ## Example
359
+ ```
360
+ agent = create_timbr_sql_agent(
361
+ llm=<llm>,
362
+ url=<url>,
363
+ token=<token>,
364
+ ontology=<ontology>,
365
+ schema=<schema>,
366
+ concept=<concept>,
367
+ concepts_list=<concepts>,
368
+ views_list=<views>,
369
+ include_tags=<tags>,
370
+ exclude_properties=<properties>,
371
+ should_validate_sql=<should_validate_sql>,
372
+ retries=<retries>,
373
+ note=<note>,
374
+ )
375
+
376
+ result = agent.invoke("What are the total sales for last month?")
377
+
378
+ # Access the components of the result:
379
+ rows = result["rows"]
380
+ sql = result["sql"]
381
+ schema = result["schema"]
382
+ concept = result["concept"]
383
+ error = result["error"]
384
+ ```
385
+ """
386
+ agent = TimbrSqlAgent(
387
+ llm=llm,
388
+ url=url,
389
+ token=token,
390
+ ontology=ontology,
391
+ schema=schema,
392
+ concept=concept,
393
+ concepts_list=concepts_list,
394
+ views_list=views_list,
395
+ include_logic_concepts=include_logic_concepts,
396
+ include_tags=include_tags,
397
+ exclude_properties=exclude_properties,
398
+ should_validate_sql=should_validate_sql,
399
+ retries=retries,
400
+ max_limit=max_limit,
401
+ retry_if_no_results=retry_if_no_results,
402
+ no_results_max_retries=no_results_max_retries,
403
+ generate_answer=generate_answer,
404
+ note=note,
405
+ db_is_case_sensitive=db_is_case_sensitive,
406
+ graph_depth=graph_depth,
407
+ verify_ssl=verify_ssl,
408
+ is_jwt=is_jwt,
409
+ jwt_tenant_id=jwt_tenant_id,
410
+ conn_params=conn_params,
411
+ debug=debug,
412
+ )
413
+
414
+ return AgentExecutor.from_agent_and_tools(
415
+ agent=agent,
416
+ tools=[], # No tools needed as we're directly using the chain
417
+ verbose=True
418
+ )