langchain-timbr 2.1.2__tar.gz → 2.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/PKG-INFO +1 -1
  2. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/_version.py +2 -2
  3. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langchain/execute_timbr_query_chain.py +3 -1
  4. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langchain/generate_answer_chain.py +4 -0
  5. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langchain/generate_timbr_sql_chain.py +1 -0
  6. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langchain/timbr_sql_agent.py +1 -0
  7. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langchain/validate_timbr_sql_chain.py +1 -0
  8. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/llm_wrapper/llm_wrapper.py +1 -1
  9. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/timbr_llm_connector.py +1 -0
  10. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/utils/general.py +142 -9
  11. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/utils/timbr_llm_utils.py +3 -1
  12. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/utils/timbr_utils.py +15 -2
  13. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/.github/dependabot.yml +0 -0
  14. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/.github/pull_request_template.md +0 -0
  15. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/.github/workflows/_codespell.yml +0 -0
  16. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/.github/workflows/_fossa.yml +0 -0
  17. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/.github/workflows/install-dependencies-and-run-tests.yml +0 -0
  18. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/.github/workflows/publish.yml +0 -0
  19. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/.gitignore +0 -0
  20. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/LICENSE +0 -0
  21. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/README.md +0 -0
  22. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/SECURITY.md +0 -0
  23. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/pyproject.toml +0 -0
  24. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/pytest.ini +0 -0
  25. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/requirements.txt +0 -0
  26. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/requirements310.txt +0 -0
  27. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/requirements311.txt +0 -0
  28. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/__init__.py +0 -0
  29. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/config.py +0 -0
  30. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langchain/__init__.py +0 -0
  31. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langchain/identify_concept_chain.py +0 -0
  32. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langgraph/__init__.py +0 -0
  33. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langgraph/execute_timbr_query_node.py +0 -0
  34. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langgraph/generate_response_node.py +0 -0
  35. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langgraph/generate_timbr_sql_node.py +0 -0
  36. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langgraph/identify_concept_node.py +0 -0
  37. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/langgraph/validate_timbr_query_node.py +0 -0
  38. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/llm_wrapper/timbr_llm_wrapper.py +0 -0
  39. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/utils/prompt_service.py +0 -0
  40. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/src/langchain_timbr/utils/temperature_supported_models.json +0 -0
  41. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/README.md +0 -0
  42. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/conftest.py +0 -0
  43. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/integration/test_agent_integration.py +0 -0
  44. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/integration/test_azure_databricks_provider.py +0 -0
  45. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/integration/test_azure_openai_model.py +0 -0
  46. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/integration/test_chain_pipeline.py +0 -0
  47. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/integration/test_jwt_token.py +0 -0
  48. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/integration/test_langchain_chains.py +0 -0
  49. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/integration/test_langgraph_nodes.py +0 -0
  50. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/integration/test_timeout_functionality.py +0 -0
  51. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/standard/conftest.py +0 -0
  52. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/standard/test_chain_documentation.py +0 -0
  53. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/standard/test_connection_validation.py +0 -0
  54. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/standard/test_llm_wrapper_optional_params.py +0 -0
  55. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/standard/test_optional_llm_integration.py +0 -0
  56. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/standard/test_standard_chain_requirements.py +0 -0
  57. {langchain_timbr-2.1.2 → langchain_timbr-2.1.4}/tests/standard/test_unit_tests.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-timbr
3
- Version: 2.1.2
3
+ Version: 2.1.4
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/
@@ -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.2'
32
- __version_tuple__ = version_tuple = (2, 1, 2)
31
+ __version__ = version = '2.1.4'
32
+ __version_tuple__ = version_tuple = (2, 1, 4)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -169,6 +169,7 @@ class ExecuteTimbrQueryChain(Chain):
169
169
  "verify_ssl": self._verify_ssl,
170
170
  "is_jwt": self._is_jwt,
171
171
  "jwt_tenant_id": self._jwt_tenant_id,
172
+ "additional_headers": {"results-limit": str(self._max_limit)},
172
173
  **self._conn_params,
173
174
  }
174
175
 
@@ -265,7 +266,8 @@ class ExecuteTimbrQueryChain(Chain):
265
266
  rows = run_query(
266
267
  sql,
267
268
  self._get_conn_params(),
268
- llm_prompt=prompt
269
+ llm_prompt=prompt,
270
+ use_query_limit=True,
269
271
  ) if is_sql_valid and is_sql_not_tried else []
270
272
 
271
273
  if iteration < self._no_results_max_retries:
@@ -23,6 +23,7 @@ class GenerateAnswerChain(Chain):
23
23
  is_jwt: Optional[bool] = False,
24
24
  jwt_tenant_id: Optional[str] = None,
25
25
  conn_params: Optional[dict] = None,
26
+ note: Optional[str] = '',
26
27
  debug: Optional[bool] = False,
27
28
  **kwargs,
28
29
  ):
@@ -33,6 +34,7 @@ class GenerateAnswerChain(Chain):
33
34
  :param verify_ssl: Whether to verify SSL certificates (default is True).
34
35
  :param is_jwt: Whether to use JWT authentication (default is False).
35
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
36
38
  :param conn_params: Extra Timbr connection parameters sent with every request (e.g., 'x-api-impersonate-user').
37
39
 
38
40
  ## Example
@@ -77,6 +79,7 @@ class GenerateAnswerChain(Chain):
77
79
  self._jwt_tenant_id = jwt_tenant_id
78
80
  self._debug = to_boolean(debug)
79
81
  self._conn_params = conn_params or {}
82
+ self._note = note
80
83
 
81
84
 
82
85
  @property
@@ -116,6 +119,7 @@ class GenerateAnswerChain(Chain):
116
119
  conn_params=self._get_conn_params(),
117
120
  results=rows,
118
121
  sql=sql,
122
+ note=self._note,
119
123
  debug=self._debug,
120
124
  )
121
125
 
@@ -160,6 +160,7 @@ class GenerateTimbrSqlChain(Chain):
160
160
  "verify_ssl": self._verify_ssl,
161
161
  "is_jwt": self._is_jwt,
162
162
  "jwt_tenant_id": self._jwt_tenant_id,
163
+ "additional_headers": {"results-limit": str(self._max_limit)},
163
164
  **self._conn_params,
164
165
  }
165
166
 
@@ -126,6 +126,7 @@ class TimbrSqlAgent(BaseSingleActionAgent):
126
126
  is_jwt=to_boolean(is_jwt),
127
127
  jwt_tenant_id=jwt_tenant_id,
128
128
  conn_params=conn_params,
129
+ note=note,
129
130
  debug=to_boolean(debug),
130
131
  ) if self._generate_answer else None
131
132
 
@@ -161,6 +161,7 @@ class ValidateTimbrSqlChain(Chain):
161
161
  "verify_ssl": self._verify_ssl,
162
162
  "is_jwt": self._is_jwt,
163
163
  "jwt_tenant_id": self._jwt_tenant_id,
164
+ "additional_headers": {"results-limit": str(self._max_limit)},
164
165
  **self._conn_params,
165
166
  }
166
167
 
@@ -203,7 +203,7 @@ class LlmWrapper(LLM):
203
203
  if azure_tenant_id and azure_client_id:
204
204
  from azure.identity import ClientSecretCredential, get_bearer_token_provider
205
205
  azure_client_secret = pop_param_value(params, ['azure_client_secret', 'llm_client_secret'], default=api_key)
206
- scope = pop_param_value(params, ['azure_scope', 'llm_scope'], default=config.llm_scope)
206
+ scope = pop_param_value(params, ['azure_scope', 'llm_scope', 'scope'], default=config.llm_scope)
207
207
  credential = ClientSecretCredential(
208
208
  tenant_id=azure_tenant_id,
209
209
  client_id=azure_client_id,
@@ -115,6 +115,7 @@ class TimbrLlmConnector:
115
115
  "verify_ssl": self.verify_ssl,
116
116
  "is_jwt": self.is_jwt,
117
117
  "jwt_tenant_id": self.jwt_tenant_id,
118
+ "additional_headers": {"results-limit": str(self.max_limit)},
118
119
  **self.conn_params,
119
120
  }
120
121
 
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from typing import Any, Optional, Union
3
3
  import json
4
+ import re
4
5
 
5
6
  ### A global helper functions to use across the project
6
7
 
@@ -34,6 +35,7 @@ def to_integer(value) -> int:
34
35
  def parse_additional_params(value) -> dict:
35
36
  """
36
37
  Parse additional parameters from string format 'a=1,b=2' or return dict as-is.
38
+ Handles JSON values correctly, including nested structures with commas.
37
39
 
38
40
  Args:
39
41
  value: String in format 'key=value,key2=value2', JSON string, or dict
@@ -49,25 +51,156 @@ def parse_additional_params(value) -> dict:
49
51
  stripped_value = value.strip()
50
52
  if stripped_value.startswith('{') and stripped_value.endswith('}'):
51
53
  try:
52
- return json.loads(stripped_value)
54
+ return _try_parse_json_value(stripped_value)
53
55
  except json.JSONDecodeError:
54
56
  pass
55
57
 
56
- # Fall back to key=value parsing
58
+ # Check if complex parsing is needed (presence of nested structures)
59
+ needs_complex_parsing = any(char in value for char in ['{', '}', '[', ']', '(', ')'])
60
+
61
+ if not needs_complex_parsing:
62
+ # Fast path: simple key=value pairs
63
+ params = {}
64
+ for pair in value.split(','):
65
+ if '=' in pair:
66
+ key, val = pair.split('=', 1)
67
+ params[key.strip().lower()] = _try_parse_json_value(val.strip())
68
+ return params
69
+
70
+ # Complex parsing that handles JSON values with commas
57
71
  params = {}
58
- for pair in (value.split('&') if '&' in value else value.split(',')):
59
- if '=' in pair:
60
- key, val = pair.split('=', 1)
61
- params[key.strip().lower()] = val.strip()
62
- elif ':' in pair:
63
- key, val = pair.split(':', 1)
64
- params[key.strip().lower()] = val.strip()
72
+ i = 0
73
+ while i < len(value):
74
+ # Find the next key=value pair
75
+ equals_pos = value.find('=', i)
76
+ if equals_pos == -1:
77
+ break
78
+
79
+ # Extract the key
80
+ key = value[i:equals_pos].strip()
81
+
82
+ # Find where the value starts
83
+ value_start = equals_pos + 1
84
+
85
+ # Determine where the value ends (considering nested structures)
86
+ value_end = _find_value_end(value, value_start)
87
+
88
+ # Extract and parse the value
89
+ val = value[value_start:value_end].strip()
90
+
91
+ # Try to parse the value as JSON if it looks like JSON
92
+ parsed_val = _try_parse_json_value(val)
93
+
94
+ params[key.lower()] = parsed_val
95
+
96
+ # Move to the next parameter (skip comma if present)
97
+ i = value_end
98
+ if i < len(value) and value[i] == ',':
99
+ i += 1
100
+
65
101
  return params
66
102
  return {}
67
103
  except Exception as e:
68
104
  raise ValueError(f"Failed to parse additional parameters: {e}")
69
105
 
70
106
 
107
+ def _find_value_end(text: str, start: int) -> int:
108
+ """
109
+ Find the end position of a parameter value, considering nested structures.
110
+
111
+ Args:
112
+ text: The full text being parsed
113
+ start: Starting position of the value
114
+
115
+ Returns:
116
+ End position of the value
117
+ """
118
+ depth = {'brace': 0, 'bracket': 0, 'paren': 0}
119
+ in_quotes = False
120
+ quote_char = None
121
+ i = start
122
+
123
+ while i < len(text):
124
+ char = text[i]
125
+
126
+ # Handle quotes
127
+ if char in ('"', "'"):
128
+ if not in_quotes:
129
+ in_quotes = True
130
+ quote_char = char
131
+ elif char == quote_char:
132
+ # Check if it's escaped
133
+ if i > 0 and text[i-1] != '\\':
134
+ in_quotes = False
135
+ quote_char = None
136
+
137
+ # Only process structural characters if not in quotes
138
+ elif not in_quotes:
139
+ if char == '{':
140
+ depth['brace'] += 1
141
+ elif char == '}':
142
+ depth['brace'] -= 1
143
+ elif char == '[':
144
+ depth['bracket'] += 1
145
+ elif char == ']':
146
+ depth['bracket'] -= 1
147
+ elif char == '(':
148
+ depth['paren'] += 1
149
+ elif char == ')':
150
+ depth['paren'] -= 1
151
+ elif char == ',' and all(d == 0 for d in depth.values()):
152
+ # Found a comma at the top level - this is the end of the value
153
+ return i
154
+
155
+ i += 1
156
+
157
+ return i
158
+
159
+
160
+ def _try_parse_json_value(val: str) -> Any:
161
+ """
162
+ Try to parse a string value as JSON. If it fails, return the original string.
163
+ Supports both single and double quotes for JSON-like structures.
164
+
165
+ Args:
166
+ val: String value to parse
167
+
168
+ Returns:
169
+ Parsed JSON value or original string
170
+ """
171
+ val = val.strip()
172
+
173
+ # Check if it looks like JSON
174
+ if (val.startswith('{') and val.endswith('}')) or \
175
+ (val.startswith('[') and val.endswith(']')) or \
176
+ val in ('true', 'false', 'null') or \
177
+ (val.startswith('"') and val.endswith('"')):
178
+ try:
179
+ return json.loads(val)
180
+ except json.JSONDecodeError:
181
+ # Try converting single quotes to double quotes for Python-style dicts
182
+ if val.startswith('{') or val.startswith('['):
183
+ try:
184
+ # Replace single quotes with double quotes, handling escaped quotes
185
+ normalized = val.replace("\\'", "<<<ESCAPED_SINGLE>>>")
186
+ normalized = normalized.replace("'", '"')
187
+ normalized = normalized.replace("<<<ESCAPED_SINGLE>>>", "'")
188
+ return json.loads(normalized)
189
+ except json.JSONDecodeError:
190
+ pass
191
+ # Try to parse as number - validate format first
192
+ if re.match(r'^-?\d+(\.\d+)?$', val):
193
+ try:
194
+ if '.' in val:
195
+ return float(val)
196
+ return int(val)
197
+ except ValueError:
198
+ pass
199
+ pass
200
+
201
+ return val
202
+
203
+
71
204
  def is_llm_type(llm_type, enum_value):
72
205
  """Check if llm_type equals the enum value or its name, case-insensitive."""
73
206
  if llm_type == enum_value:
@@ -545,8 +545,9 @@ def answer_question(
545
545
  conn_params: dict,
546
546
  results: str,
547
547
  sql: Optional[str] = None,
548
- debug: Optional[bool] = False,
549
548
  timeout: Optional[int] = None,
549
+ note: Optional[str] = '',
550
+ debug: Optional[bool] = False,
550
551
  ) -> dict[str, Any]:
551
552
  # Use config default timeout if none provided
552
553
  if timeout is None:
@@ -558,6 +559,7 @@ def answer_question(
558
559
  question=question,
559
560
  formatted_rows=results,
560
561
  additional_context=f"SQL QUERY:\n{sql}\n\n" if sql else "",
562
+ note=note,
561
563
  )
562
564
 
563
565
  apx_token_count = _calculate_token_count(llm, prompt)
@@ -70,7 +70,7 @@ def cache_with_version_check(func):
70
70
  return wrapper
71
71
 
72
72
 
73
- def run_query(sql: str, conn_params: dict, llm_prompt: Optional[str] = None) -> list[list]:
73
+ def run_query(sql: str, conn_params: dict, llm_prompt: Optional[str] = None, use_query_limit = False) -> list[list]:
74
74
  if not conn_params:
75
75
  raise("Please provide connection params.")
76
76
 
@@ -79,9 +79,22 @@ def run_query(sql: str, conn_params: dict, llm_prompt: Optional[str] = None) ->
79
79
  clean_prompt = llm_prompt.replace('\r\n', ' ').replace('\n', ' ').replace('?', '')
80
80
  query = f"-- LLM: {clean_prompt}\n{sql}"
81
81
 
82
+ query_conn_params = conn_params
83
+ if not use_query_limit:
84
+ # Remove results-limit
85
+ if 'additional_headers' in conn_params and 'results-limit' in conn_params['additional_headers']:
86
+ query_upper = query.strip().upper()
87
+ if query_upper.startswith('SHOW') or query_upper.startswith('DESC'):
88
+ query_conn_params = conn_params.copy()
89
+ query_conn_params['additional_headers'] = conn_params['additional_headers'].copy()
90
+ del query_conn_params['additional_headers']['results-limit']
91
+ # If no other additional_headers remain, delete the key entirely
92
+ if not query_conn_params['additional_headers']:
93
+ del query_conn_params['additional_headers']
94
+
82
95
  results = timbr_http_connector.run_query(
83
96
  query=query,
84
- **conn_params,
97
+ **query_conn_params,
85
98
  )
86
99
 
87
100
  return results
File without changes