langchain-timbr 2.1.14__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langchain_timbr/_version.py +2 -2
- langchain_timbr/langchain/execute_timbr_query_chain.py +62 -19
- langchain_timbr/langchain/generate_answer_chain.py +18 -4
- langchain_timbr/langchain/generate_timbr_sql_chain.py +54 -17
- langchain_timbr/langchain/identify_concept_chain.py +31 -9
- langchain_timbr/langchain/timbr_sql_agent.py +20 -1
- langchain_timbr/langchain/validate_timbr_sql_chain.py +57 -17
- langchain_timbr/langgraph/execute_timbr_query_node.py +3 -0
- langchain_timbr/langgraph/generate_response_node.py +5 -2
- langchain_timbr/langgraph/generate_timbr_sql_node.py +3 -0
- langchain_timbr/langgraph/identify_concept_node.py +3 -0
- langchain_timbr/langgraph/validate_timbr_query_node.py +3 -0
- langchain_timbr/utils/timbr_llm_utils.py +333 -166
- langchain_timbr/utils/timbr_utils.py +27 -0
- {langchain_timbr-2.1.14.dist-info → langchain_timbr-3.0.0.dist-info}/METADATA +1 -1
- langchain_timbr-3.0.0.dist-info/RECORD +28 -0
- langchain_timbr-2.1.14.dist-info/RECORD +0 -28
- {langchain_timbr-2.1.14.dist-info → langchain_timbr-3.0.0.dist-info}/WHEEL +0 -0
- {langchain_timbr-2.1.14.dist-info → langchain_timbr-3.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -268,6 +268,7 @@ def determine_concept(
|
|
|
268
268
|
) -> dict[str, Any]:
|
|
269
269
|
usage_metadata = {}
|
|
270
270
|
determined_concept_name = None
|
|
271
|
+
identify_concept_reason = None
|
|
271
272
|
schema = 'dtimbr'
|
|
272
273
|
|
|
273
274
|
# Use config default timeout if none provided
|
|
@@ -339,10 +340,21 @@ def determine_concept(
|
|
|
339
340
|
if debug:
|
|
340
341
|
usage_metadata['determine_concept']["p_hash"] = encrypt_prompt(prompt)
|
|
341
342
|
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
# Try to parse as JSON first (with 'result' and 'reason' keys)
|
|
344
|
+
try:
|
|
345
|
+
parsed_response = _parse_json_from_llm_response(response)
|
|
346
|
+
if isinstance(parsed_response, dict) and 'result' in parsed_response:
|
|
347
|
+
candidate = parsed_response.get('result', '').strip()
|
|
348
|
+
identify_concept_reason = parsed_response.get('reason', None)
|
|
349
|
+
else:
|
|
350
|
+
# Fallback to plain text if JSON doesn't have expected structure
|
|
351
|
+
candidate = _get_response_text(response).strip()
|
|
352
|
+
except (json.JSONDecodeError, ValueError):
|
|
353
|
+
# If not JSON, treat as plain text (backwards compatibility)
|
|
354
|
+
candidate = _get_response_text(response).strip()
|
|
355
|
+
|
|
344
356
|
if should_validate and candidate not in concepts_and_views.keys():
|
|
345
|
-
error = f"Concept '{
|
|
357
|
+
error = f"Concept '{candidate}' not found in the list of concepts."
|
|
346
358
|
continue
|
|
347
359
|
|
|
348
360
|
determined_concept_name = candidate
|
|
@@ -356,6 +368,7 @@ def determine_concept(
|
|
|
356
368
|
return {
|
|
357
369
|
"concept_metadata": concepts_and_views.get(determined_concept_name) if determined_concept_name else None,
|
|
358
370
|
"concept": determined_concept_name,
|
|
371
|
+
"identify_concept_reason": identify_concept_reason,
|
|
359
372
|
"schema": schema,
|
|
360
373
|
"usage_metadata": usage_metadata,
|
|
361
374
|
}
|
|
@@ -423,14 +436,45 @@ def _build_rel_columns_str(relationships: list[dict], columns_tags: Optional[dic
|
|
|
423
436
|
return '.\n'.join(rel_str_arr) if rel_str_arr else ''
|
|
424
437
|
|
|
425
438
|
|
|
426
|
-
def
|
|
439
|
+
def _parse_sql_and_reason_from_llm_response(response: Any) -> dict:
|
|
440
|
+
"""
|
|
441
|
+
Parse SQL & reason from LLM response. Handles both plain SQL strings and JSON format with 'result' and 'reason' keys.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
dict with 'sql' and 'reason' keys (reason may be None if not provided)
|
|
445
|
+
"""
|
|
446
|
+
# Try to parse as JSON first
|
|
447
|
+
try:
|
|
448
|
+
parsed_json = _parse_json_from_llm_response(response)
|
|
449
|
+
|
|
450
|
+
# Extract SQL from 'result' key and reason from 'reason' key
|
|
451
|
+
if isinstance(parsed_json, dict) and 'result' in parsed_json:
|
|
452
|
+
sql = parsed_json.get('result', '')
|
|
453
|
+
reason = parsed_json.get('reason', None)
|
|
454
|
+
|
|
455
|
+
# Clean the SQL
|
|
456
|
+
sql = (sql
|
|
457
|
+
.replace("```sql", "")
|
|
458
|
+
.replace("```", "")
|
|
459
|
+
.replace('SELECT \n', 'SELECT ')
|
|
460
|
+
.replace(';', '')
|
|
461
|
+
.strip())
|
|
462
|
+
|
|
463
|
+
return {'sql': sql, 'reason': reason}
|
|
464
|
+
except (json.JSONDecodeError, ValueError):
|
|
465
|
+
# If not JSON, treat as plain SQL string (backwards compatibility)
|
|
466
|
+
pass
|
|
467
|
+
|
|
468
|
+
# Fallback to plain text parsing
|
|
427
469
|
response_text = _get_response_text(response)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
470
|
+
sql = (response_text
|
|
471
|
+
.replace("```sql", "")
|
|
472
|
+
.replace("```", "")
|
|
473
|
+
.replace('SELECT \n', 'SELECT ')
|
|
474
|
+
.replace(';', '')
|
|
475
|
+
.strip())
|
|
476
|
+
|
|
477
|
+
return {'sql': sql, 'reason': None}
|
|
434
478
|
|
|
435
479
|
|
|
436
480
|
def _get_active_datasource(conn_params: dict) -> dict:
|
|
@@ -438,6 +482,38 @@ def _get_active_datasource(conn_params: dict) -> dict:
|
|
|
438
482
|
return datasources[0] if datasources else None
|
|
439
483
|
|
|
440
484
|
|
|
485
|
+
def _parse_json_from_llm_response(response: Any) -> dict:
|
|
486
|
+
"""
|
|
487
|
+
Parse JSON from LLM response. Handles markdown code blocks and extracts valid JSON.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
response: LLM response object
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
dict containing parsed JSON
|
|
494
|
+
|
|
495
|
+
Raises:
|
|
496
|
+
json.JSONDecodeError: If response cannot be parsed as JSON
|
|
497
|
+
ValueError: If response format is unexpected
|
|
498
|
+
"""
|
|
499
|
+
response_text = _get_response_text(response)
|
|
500
|
+
|
|
501
|
+
# Remove markdown code block markers if present
|
|
502
|
+
content = response_text.strip()
|
|
503
|
+
if content.startswith("```json"):
|
|
504
|
+
content = content[7:] # Remove ```json
|
|
505
|
+
elif content.startswith("```"):
|
|
506
|
+
content = content[3:] # Remove ```
|
|
507
|
+
|
|
508
|
+
if content.endswith("```"):
|
|
509
|
+
content = content[:-3] # Remove closing ```
|
|
510
|
+
|
|
511
|
+
content = content.strip()
|
|
512
|
+
|
|
513
|
+
# Parse and return JSON
|
|
514
|
+
return json.loads(content)
|
|
515
|
+
|
|
516
|
+
|
|
441
517
|
def _evaluate_sql_enable_reasoning(
|
|
442
518
|
question: str,
|
|
443
519
|
sql_query: str,
|
|
@@ -463,22 +539,8 @@ def _evaluate_sql_enable_reasoning(
|
|
|
463
539
|
|
|
464
540
|
response = _call_llm_with_timeout(llm, prompt, timeout=timeout)
|
|
465
541
|
|
|
466
|
-
# Extract JSON from response content (handle markdown code blocks)
|
|
467
|
-
content = response.content.strip()
|
|
468
|
-
|
|
469
|
-
# Remove markdown code block markers if present
|
|
470
|
-
if content.startswith("```json"):
|
|
471
|
-
content = content[7:] # Remove ```json
|
|
472
|
-
elif content.startswith("```"):
|
|
473
|
-
content = content[3:] # Remove ```
|
|
474
|
-
|
|
475
|
-
if content.endswith("```"):
|
|
476
|
-
content = content[:-3] # Remove closing ```
|
|
477
|
-
|
|
478
|
-
content = content.strip()
|
|
479
|
-
|
|
480
542
|
# Parse JSON response
|
|
481
|
-
evaluation =
|
|
543
|
+
evaluation = _parse_json_from_llm_response(response)
|
|
482
544
|
|
|
483
545
|
return {
|
|
484
546
|
"evaluation": evaluation,
|
|
@@ -571,11 +633,9 @@ def _build_sql_generation_context(
|
|
|
571
633
|
def _generate_sql_with_llm(
|
|
572
634
|
question: str,
|
|
573
635
|
llm: LLM,
|
|
574
|
-
conn_params: dict,
|
|
575
636
|
generate_sql_prompt: Any,
|
|
576
637
|
current_context: dict,
|
|
577
638
|
note: str,
|
|
578
|
-
should_validate_sql: bool,
|
|
579
639
|
timeout: int,
|
|
580
640
|
debug: bool = False,
|
|
581
641
|
) -> dict:
|
|
@@ -614,8 +674,12 @@ def _generate_sql_with_llm(
|
|
|
614
674
|
|
|
615
675
|
response = _call_llm_with_timeout(llm, prompt, timeout=timeout)
|
|
616
676
|
|
|
677
|
+
# Parse response which now includes both SQL and reason
|
|
678
|
+
parsed_response = _parse_sql_and_reason_from_llm_response(response)
|
|
679
|
+
|
|
617
680
|
result = {
|
|
618
|
-
"sql":
|
|
681
|
+
"sql": parsed_response['sql'],
|
|
682
|
+
"generate_sql_reason": parsed_response['reason'],
|
|
619
683
|
"apx_token_count": apx_token_count,
|
|
620
684
|
"usage_metadata": _extract_usage_metadata(response),
|
|
621
685
|
"is_valid": True,
|
|
@@ -625,11 +689,163 @@ def _generate_sql_with_llm(
|
|
|
625
689
|
if debug:
|
|
626
690
|
result["p_hash"] = encrypt_prompt(prompt)
|
|
627
691
|
|
|
628
|
-
if should_validate_sql:
|
|
629
|
-
result["is_valid"], result["error"], result["sql"] = validate_sql(result["sql"], conn_params)
|
|
630
692
|
|
|
631
693
|
return result
|
|
632
694
|
|
|
695
|
+
def handle_generate_sql_reasoning(
|
|
696
|
+
sql_query: str,
|
|
697
|
+
question: str,
|
|
698
|
+
llm: LLM,
|
|
699
|
+
conn_params: dict,
|
|
700
|
+
schema: str,
|
|
701
|
+
concept: str,
|
|
702
|
+
concept_metadata: dict,
|
|
703
|
+
include_tags: bool,
|
|
704
|
+
exclude_properties: list,
|
|
705
|
+
db_is_case_sensitive: bool,
|
|
706
|
+
max_limit: int,
|
|
707
|
+
reasoning_steps: int,
|
|
708
|
+
note: str,
|
|
709
|
+
graph_depth: int,
|
|
710
|
+
usage_metadata: dict,
|
|
711
|
+
timeout: int,
|
|
712
|
+
debug: bool,
|
|
713
|
+
) -> tuple[str, int, str]:
|
|
714
|
+
generate_sql_prompt = get_generate_sql_prompt_template(conn_params)
|
|
715
|
+
context_graph_depth = graph_depth
|
|
716
|
+
reasoned_sql = sql_query
|
|
717
|
+
reasoned_sql_reason = None
|
|
718
|
+
for step in range(reasoning_steps):
|
|
719
|
+
try:
|
|
720
|
+
# Step 1: Evaluate the current SQL
|
|
721
|
+
eval_result = _evaluate_sql_enable_reasoning(
|
|
722
|
+
question=question,
|
|
723
|
+
sql_query=reasoned_sql,
|
|
724
|
+
llm=llm,
|
|
725
|
+
conn_params=conn_params,
|
|
726
|
+
timeout=timeout,
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
usage_metadata[f'sql_reasoning_step_{step + 1}'] = {
|
|
730
|
+
"approximate": eval_result['apx_token_count'],
|
|
731
|
+
**eval_result['usage_metadata'],
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
evaluation = eval_result['evaluation']
|
|
735
|
+
reasoning_status = evaluation.get("assessment", "partial").lower()
|
|
736
|
+
|
|
737
|
+
if reasoning_status == "correct":
|
|
738
|
+
break
|
|
739
|
+
|
|
740
|
+
# Step 2: Regenerate SQL with feedback
|
|
741
|
+
evaluation_note = note + f"\n\nThe previously generated SQL: `{reasoned_sql}` 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}'.\n\nCRITICAL: Return ONLY the SQL query without any explanation or comments."
|
|
742
|
+
|
|
743
|
+
# Increase graph depth for 2nd+ reasoning attempts, up to max of 3
|
|
744
|
+
context_graph_depth = min(3, int(graph_depth) + step) if graph_depth < 3 and step > 0 else graph_depth
|
|
745
|
+
regen_result = _generate_sql_with_llm(
|
|
746
|
+
question=question,
|
|
747
|
+
llm=llm,
|
|
748
|
+
generate_sql_prompt=generate_sql_prompt,
|
|
749
|
+
current_context=_build_sql_generation_context(
|
|
750
|
+
conn_params=conn_params,
|
|
751
|
+
schema=schema,
|
|
752
|
+
concept=concept,
|
|
753
|
+
concept_metadata=concept_metadata,
|
|
754
|
+
graph_depth=context_graph_depth,
|
|
755
|
+
include_tags=include_tags,
|
|
756
|
+
exclude_properties=exclude_properties,
|
|
757
|
+
db_is_case_sensitive=db_is_case_sensitive,
|
|
758
|
+
max_limit=max_limit),
|
|
759
|
+
note=evaluation_note,
|
|
760
|
+
timeout=timeout,
|
|
761
|
+
debug=debug,
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
reasoned_sql = regen_result['sql']
|
|
765
|
+
reasoned_sql_reason = regen_result['generate_sql_reason']
|
|
766
|
+
error = regen_result['error']
|
|
767
|
+
|
|
768
|
+
step_key = f'generate_sql_reasoning_step_{step + 1}'
|
|
769
|
+
usage_metadata[step_key] = {
|
|
770
|
+
"approximate": regen_result['apx_token_count'],
|
|
771
|
+
**regen_result['usage_metadata'],
|
|
772
|
+
}
|
|
773
|
+
if debug and 'p_hash' in regen_result:
|
|
774
|
+
usage_metadata[step_key]['p_hash'] = regen_result['p_hash']
|
|
775
|
+
|
|
776
|
+
if error:
|
|
777
|
+
raise Exception(error)
|
|
778
|
+
|
|
779
|
+
except TimeoutError as e:
|
|
780
|
+
raise Exception(f"LLM call timed out: {str(e)}")
|
|
781
|
+
except Exception as e:
|
|
782
|
+
print(f"Warning: LLM reasoning failed: {e}")
|
|
783
|
+
break
|
|
784
|
+
|
|
785
|
+
return reasoned_sql, context_graph_depth, reasoned_sql_reason
|
|
786
|
+
|
|
787
|
+
def handle_validate_generate_sql(
|
|
788
|
+
sql_query: str,
|
|
789
|
+
question: str,
|
|
790
|
+
llm: LLM,
|
|
791
|
+
conn_params: dict,
|
|
792
|
+
generate_sql_prompt: Any,
|
|
793
|
+
schema: str,
|
|
794
|
+
concept: str,
|
|
795
|
+
concept_metadata: dict,
|
|
796
|
+
include_tags: bool,
|
|
797
|
+
exclude_properties: list,
|
|
798
|
+
db_is_case_sensitive: bool,
|
|
799
|
+
max_limit: int,
|
|
800
|
+
graph_depth: int,
|
|
801
|
+
retries: int,
|
|
802
|
+
timeout: int,
|
|
803
|
+
debug: bool,
|
|
804
|
+
usage_metadata: dict,
|
|
805
|
+
) -> tuple[bool, str, str]:
|
|
806
|
+
is_sql_valid, error, sql_query = validate_sql(sql_query, conn_params)
|
|
807
|
+
validation_attempt = 0
|
|
808
|
+
|
|
809
|
+
while validation_attempt < retries and not is_sql_valid:
|
|
810
|
+
validation_attempt += 1
|
|
811
|
+
validation_err_txt = f"\nThe generated SQL (`{sql_query}`) was invalid with error: {error}. Please generate a corrected query that achieves the intended result." if error and "snowflake" not in llm._llm_type else ""
|
|
812
|
+
|
|
813
|
+
regen_result = _generate_sql_with_llm(
|
|
814
|
+
question=question,
|
|
815
|
+
llm=llm,
|
|
816
|
+
generate_sql_prompt=generate_sql_prompt,
|
|
817
|
+
current_context=_build_sql_generation_context(
|
|
818
|
+
conn_params=conn_params,
|
|
819
|
+
schema=schema,
|
|
820
|
+
concept=concept,
|
|
821
|
+
concept_metadata=concept_metadata,
|
|
822
|
+
graph_depth=graph_depth,
|
|
823
|
+
include_tags=include_tags,
|
|
824
|
+
exclude_properties=exclude_properties,
|
|
825
|
+
db_is_case_sensitive=db_is_case_sensitive,
|
|
826
|
+
max_limit=max_limit),
|
|
827
|
+
note=validation_err_txt,
|
|
828
|
+
timeout=timeout,
|
|
829
|
+
debug=debug,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
regen_error = regen_result['error']
|
|
833
|
+
sql_query = regen_result['sql']
|
|
834
|
+
|
|
835
|
+
validation_key = f'generate_sql_validation_regen_{validation_attempt}'
|
|
836
|
+
usage_metadata[validation_key] = {
|
|
837
|
+
"approximate": regen_result['apx_token_count'],
|
|
838
|
+
**regen_result['usage_metadata'],
|
|
839
|
+
}
|
|
840
|
+
if debug and 'p_hash' in regen_result:
|
|
841
|
+
usage_metadata[validation_key]['p_hash'] = regen_result['p_hash']
|
|
842
|
+
|
|
843
|
+
if regen_error:
|
|
844
|
+
raise Exception(regen_error)
|
|
845
|
+
|
|
846
|
+
is_sql_valid, error, sql_query = validate_sql(sql_query, conn_params)
|
|
847
|
+
|
|
848
|
+
return is_sql_valid, error, sql_query
|
|
633
849
|
|
|
634
850
|
def generate_sql(
|
|
635
851
|
question: str,
|
|
@@ -656,13 +872,11 @@ def generate_sql(
|
|
|
656
872
|
usage_metadata = {}
|
|
657
873
|
concept_metadata = None
|
|
658
874
|
reasoning_status = 'correct'
|
|
659
|
-
|
|
875
|
+
|
|
660
876
|
# Use config default timeout if none provided
|
|
661
877
|
if timeout is None:
|
|
662
878
|
timeout = config.llm_timeout
|
|
663
879
|
|
|
664
|
-
generate_sql_prompt = get_generate_sql_prompt_template(conn_params)
|
|
665
|
-
|
|
666
880
|
if concept and concept != "" and (schema is None or schema != "vtimbr"):
|
|
667
881
|
concepts_list = [concept]
|
|
668
882
|
elif concept and concept != "" and schema == "vtimbr":
|
|
@@ -682,154 +896,105 @@ def generate_sql(
|
|
|
682
896
|
debug=debug,
|
|
683
897
|
timeout=timeout,
|
|
684
898
|
)
|
|
685
|
-
|
|
899
|
+
|
|
900
|
+
concept = determine_concept_res.get('concept')
|
|
901
|
+
identify_concept_reason = determine_concept_res.get('identify_concept_reason', None)
|
|
902
|
+
schema = determine_concept_res.get('schema')
|
|
903
|
+
concept_metadata = determine_concept_res.get('concept_metadata')
|
|
686
904
|
usage_metadata.update(determine_concept_res.get('usage_metadata', {}))
|
|
687
905
|
|
|
688
906
|
if not concept:
|
|
689
907
|
raise Exception("No relevant concept found for the query.")
|
|
690
908
|
|
|
909
|
+
generate_sql_prompt = get_generate_sql_prompt_template(conn_params)
|
|
691
910
|
sql_query = None
|
|
692
|
-
|
|
693
|
-
is_sql_valid = True
|
|
911
|
+
generate_sql_reason = None
|
|
912
|
+
is_sql_valid = True # Assume valid by default; set to False only if validation fails
|
|
694
913
|
error = ''
|
|
695
|
-
while sql_query is None or (should_validate_sql and iteration < retries and not is_sql_valid):
|
|
696
|
-
iteration += 1
|
|
697
|
-
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 ""
|
|
698
914
|
|
|
699
|
-
|
|
700
|
-
|
|
915
|
+
try:
|
|
916
|
+
result = _generate_sql_with_llm(
|
|
917
|
+
question=question,
|
|
918
|
+
llm=llm,
|
|
919
|
+
generate_sql_prompt=generate_sql_prompt,
|
|
920
|
+
current_context=_build_sql_generation_context(
|
|
921
|
+
conn_params=conn_params,
|
|
922
|
+
schema=schema,
|
|
923
|
+
concept=concept,
|
|
924
|
+
concept_metadata=concept_metadata,
|
|
925
|
+
graph_depth=graph_depth,
|
|
926
|
+
include_tags=include_tags,
|
|
927
|
+
exclude_properties=exclude_properties,
|
|
928
|
+
db_is_case_sensitive=db_is_case_sensitive,
|
|
929
|
+
max_limit=max_limit),
|
|
930
|
+
note=note,
|
|
931
|
+
timeout=timeout,
|
|
932
|
+
debug=debug,
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
usage_metadata['generate_sql'] = {
|
|
936
|
+
"approximate": result['apx_token_count'],
|
|
937
|
+
**result['usage_metadata'],
|
|
938
|
+
}
|
|
939
|
+
if debug and 'p_hash' in result:
|
|
940
|
+
usage_metadata['generate_sql']["p_hash"] = result['p_hash']
|
|
941
|
+
|
|
942
|
+
sql_query = result['sql']
|
|
943
|
+
generate_sql_reason = result.get('generate_sql_reason', None)
|
|
944
|
+
error = result['error']
|
|
945
|
+
|
|
946
|
+
if error:
|
|
947
|
+
raise Exception(error)
|
|
948
|
+
|
|
949
|
+
if enable_reasoning and sql_query is not None:
|
|
950
|
+
sql_query, graph_depth, generate_sql_reason = handle_generate_sql_reasoning(
|
|
951
|
+
sql_query=sql_query,
|
|
952
|
+
question=question,
|
|
953
|
+
llm=llm,
|
|
954
|
+
conn_params=conn_params,
|
|
955
|
+
schema=schema,
|
|
956
|
+
concept=concept,
|
|
957
|
+
concept_metadata=concept_metadata,
|
|
958
|
+
include_tags=include_tags,
|
|
959
|
+
exclude_properties=exclude_properties,
|
|
960
|
+
db_is_case_sensitive=db_is_case_sensitive,
|
|
961
|
+
max_limit=max_limit,
|
|
962
|
+
reasoning_steps=reasoning_steps,
|
|
963
|
+
note=note,
|
|
964
|
+
graph_depth=graph_depth,
|
|
965
|
+
usage_metadata=usage_metadata,
|
|
966
|
+
timeout=timeout,
|
|
967
|
+
debug=debug,
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
if should_validate_sql or enable_reasoning:
|
|
971
|
+
# Validate & regenerate only once if reasoning enabled and validation is disabled
|
|
972
|
+
validate_retries = 1 if not should_validate_sql else retries
|
|
973
|
+
is_sql_valid, error, sql_query = handle_validate_generate_sql(
|
|
974
|
+
sql_query=sql_query,
|
|
701
975
|
question=question,
|
|
702
976
|
llm=llm,
|
|
703
977
|
conn_params=conn_params,
|
|
704
978
|
generate_sql_prompt=generate_sql_prompt,
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
max_limit=max_limit),
|
|
715
|
-
note=note + err_txt,
|
|
716
|
-
should_validate_sql=should_validate_sql,
|
|
979
|
+
schema=schema,
|
|
980
|
+
concept=concept,
|
|
981
|
+
concept_metadata=concept_metadata,
|
|
982
|
+
include_tags=include_tags,
|
|
983
|
+
exclude_properties=exclude_properties,
|
|
984
|
+
db_is_case_sensitive=db_is_case_sensitive,
|
|
985
|
+
max_limit=max_limit,
|
|
986
|
+
graph_depth=graph_depth,
|
|
987
|
+
retries=validate_retries,
|
|
717
988
|
timeout=timeout,
|
|
718
989
|
debug=debug,
|
|
990
|
+
usage_metadata=usage_metadata,
|
|
719
991
|
)
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
usage_metadata['generate_sql']["p_hash"] = result['p_hash']
|
|
727
|
-
|
|
728
|
-
sql_query = result['sql']
|
|
729
|
-
is_sql_valid = result['is_valid']
|
|
730
|
-
error = result['error']
|
|
731
|
-
|
|
732
|
-
except TimeoutError as e:
|
|
733
|
-
error = f"LLM call timed out: {str(e)}"
|
|
734
|
-
raise Exception(error)
|
|
735
|
-
except Exception as e:
|
|
736
|
-
error = f"LLM call failed: {str(e)}"
|
|
737
|
-
if should_validate_sql:
|
|
738
|
-
continue
|
|
739
|
-
else:
|
|
740
|
-
raise Exception(error)
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
if enable_reasoning and sql_query is not None:
|
|
744
|
-
for step in range(reasoning_steps):
|
|
745
|
-
try:
|
|
746
|
-
# Step 1: Evaluate the current SQL
|
|
747
|
-
eval_result = _evaluate_sql_enable_reasoning(
|
|
748
|
-
question=question,
|
|
749
|
-
sql_query=sql_query,
|
|
750
|
-
llm=llm,
|
|
751
|
-
conn_params=conn_params,
|
|
752
|
-
timeout=timeout,
|
|
753
|
-
)
|
|
754
|
-
|
|
755
|
-
usage_metadata[f'sql_reasoning_step_{step + 1}'] = {
|
|
756
|
-
"approximate": eval_result['apx_token_count'],
|
|
757
|
-
**eval_result['usage_metadata'],
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
evaluation = eval_result['evaluation']
|
|
761
|
-
reasoning_status = evaluation.get("assessment", "partial").lower()
|
|
762
|
-
|
|
763
|
-
if reasoning_status == "correct":
|
|
764
|
-
break
|
|
765
|
-
|
|
766
|
-
# Step 2: Regenerate SQL with feedback (with validation retries)
|
|
767
|
-
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}'.\n\nCRITICAL: Return ONLY the SQL query without any explanation or comments."
|
|
768
|
-
|
|
769
|
-
# Increase graph depth for 2nd+ reasoning attempts, up to max of 3
|
|
770
|
-
context_graph_depth = min(3, int(graph_depth) + step) if graph_depth < 3 and step > 0 else graph_depth
|
|
771
|
-
|
|
772
|
-
# Regenerate SQL with validation retries
|
|
773
|
-
# Always validate during reasoning to ensure quality, regardless of global should_validate_sql flag
|
|
774
|
-
validation_iteration = 0
|
|
775
|
-
regen_is_valid = False
|
|
776
|
-
regen_error = ''
|
|
777
|
-
regen_sql = None
|
|
778
|
-
|
|
779
|
-
while validation_iteration < retries and (regen_sql is None or not regen_is_valid):
|
|
780
|
-
validation_iteration += 1
|
|
781
|
-
validation_err_txt = f"\nThe regenerated SQL (`{regen_sql}`) was invalid with error: {regen_error}. Please generate a corrected query." if regen_error and "snowflake" not in llm._llm_type else ""
|
|
782
|
-
|
|
783
|
-
regen_result = _generate_sql_with_llm(
|
|
784
|
-
question=question,
|
|
785
|
-
llm=llm,
|
|
786
|
-
conn_params=conn_params,
|
|
787
|
-
generate_sql_prompt=generate_sql_prompt,
|
|
788
|
-
current_context=_build_sql_generation_context(
|
|
789
|
-
conn_params=conn_params,
|
|
790
|
-
schema=schema,
|
|
791
|
-
concept=concept,
|
|
792
|
-
concept_metadata=concept_metadata,
|
|
793
|
-
graph_depth=context_graph_depth,
|
|
794
|
-
include_tags=include_tags,
|
|
795
|
-
exclude_properties=exclude_properties,
|
|
796
|
-
db_is_case_sensitive=db_is_case_sensitive,
|
|
797
|
-
max_limit=max_limit),
|
|
798
|
-
note=evaluation_note + validation_err_txt,
|
|
799
|
-
should_validate_sql=True, # Always validate during reasoning
|
|
800
|
-
timeout=timeout,
|
|
801
|
-
debug=debug,
|
|
802
|
-
)
|
|
803
|
-
|
|
804
|
-
regen_sql = regen_result['sql']
|
|
805
|
-
regen_is_valid = regen_result['is_valid']
|
|
806
|
-
regen_error = regen_result['error']
|
|
807
|
-
|
|
808
|
-
# Track token usage for each validation iteration
|
|
809
|
-
if validation_iteration == 1:
|
|
810
|
-
usage_metadata[f'generate_sql_reasoning_step_{step + 1}'] = {
|
|
811
|
-
"approximate": regen_result['apx_token_count'],
|
|
812
|
-
**regen_result['usage_metadata'],
|
|
813
|
-
}
|
|
814
|
-
if debug and 'p_hash' in regen_result:
|
|
815
|
-
usage_metadata[f'generate_sql_reasoning_step_{step + 1}']['p_hash'] = regen_result['p_hash']
|
|
816
|
-
else:
|
|
817
|
-
usage_metadata[f'generate_sql_reasoning_step_{step + 1}_validation_{validation_iteration}'] = {
|
|
818
|
-
"approximate": regen_result['apx_token_count'],
|
|
819
|
-
**regen_result['usage_metadata'],
|
|
820
|
-
}
|
|
821
|
-
if debug and 'p_hash' in regen_result:
|
|
822
|
-
usage_metadata[f'generate_sql_reasoning_step_{step + 1}_validation_{validation_iteration}']['p_hash'] = regen_result['p_hash']
|
|
823
|
-
|
|
824
|
-
sql_query = regen_sql
|
|
825
|
-
is_sql_valid = regen_is_valid
|
|
826
|
-
error = regen_error
|
|
827
|
-
|
|
828
|
-
except TimeoutError as e:
|
|
829
|
-
raise Exception(f"LLM call timed out: {str(e)}")
|
|
830
|
-
except Exception as e:
|
|
831
|
-
print(f"Warning: LLM reasoning failed: {e}")
|
|
832
|
-
break
|
|
992
|
+
except TimeoutError as e:
|
|
993
|
+
error = f"LLM call timed out: {str(e)}"
|
|
994
|
+
raise Exception(error)
|
|
995
|
+
except Exception as e:
|
|
996
|
+
error = f"LLM call failed: {str(e)}"
|
|
997
|
+
raise Exception(error)
|
|
833
998
|
|
|
834
999
|
return {
|
|
835
1000
|
"sql": sql_query,
|
|
@@ -837,6 +1002,8 @@ def generate_sql(
|
|
|
837
1002
|
"schema": schema,
|
|
838
1003
|
"error": error if not is_sql_valid else None,
|
|
839
1004
|
"is_sql_valid": is_sql_valid if should_validate_sql else None,
|
|
1005
|
+
"identify_concept_reason": identify_concept_reason,
|
|
1006
|
+
"generate_sql_reason": generate_sql_reason,
|
|
840
1007
|
"reasoning_status": reasoning_status,
|
|
841
1008
|
"usage_metadata": usage_metadata,
|
|
842
1009
|
}
|
|
@@ -154,6 +154,33 @@ def get_datasources(conn_params: dict, filter_active: Optional[bool] = False) ->
|
|
|
154
154
|
return res
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
def get_timbr_agent_options(agent_name: str, conn_params: dict) -> dict:
|
|
158
|
+
"""
|
|
159
|
+
Get all options for a specific agent from timbr sys_agents_options table.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
agent_name: Name of the agent to get options for
|
|
163
|
+
conn_params: Connection parameters for Timbr system engine
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Dictionary of option_name -> option_value pairs
|
|
167
|
+
"""
|
|
168
|
+
options = {}
|
|
169
|
+
|
|
170
|
+
# Query agent options (case-insensitive match on agent_name)
|
|
171
|
+
options_query = f"SELECT option_name, option_value FROM timbr.sys_agents_options WHERE LOWER(agent_name) = LOWER('{agent_name}')"
|
|
172
|
+
|
|
173
|
+
results = run_query(options_query, conn_params)
|
|
174
|
+
if len(results) == 0:
|
|
175
|
+
raise Exception(f'Agent "{agent_name}" not found or has no options defined.')
|
|
176
|
+
for row in results:
|
|
177
|
+
option_name = row.get('option_name')
|
|
178
|
+
option_value = row.get('option_value', '')
|
|
179
|
+
options[option_name] = option_value
|
|
180
|
+
|
|
181
|
+
return options
|
|
182
|
+
|
|
183
|
+
|
|
157
184
|
def _validate(sql: str, conn_params: dict) -> bool:
|
|
158
185
|
explain_sql = f"EXPLAIN {sql}"
|
|
159
186
|
explain_res = run_query(explain_sql, conn_params)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-timbr
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
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/
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
langchain_timbr/__init__.py,sha256=qNyk3Rt-8oWr_OGuU_E-6siNZXuCnvVEkj65EIuVbbQ,824
|
|
2
|
+
langchain_timbr/_version.py,sha256=9fL11DDeXfhKmxyz_HrYU3Yy7BjMWrJklCddPlmAj6A,704
|
|
3
|
+
langchain_timbr/config.py,sha256=Y0fTwkhLj8M-vORdVCAFykrSFIifDovV6BETfdIKZ9A,1973
|
|
4
|
+
langchain_timbr/timbr_llm_connector.py,sha256=Y3nzWoocI5txvPGPAxwFsJde9k9l2J9ioB54MYRLrEQ,13288
|
|
5
|
+
langchain_timbr/langchain/__init__.py,sha256=ejcsZKP9PK0j4WrrCCcvBXpDpP-TeRiVb21OIUJqix8,580
|
|
6
|
+
langchain_timbr/langchain/execute_timbr_query_chain.py,sha256=UIUQR8adnWmGCrx6t1nnMrD8yoejgUMr80bf3bl2xUw,20251
|
|
7
|
+
langchain_timbr/langchain/generate_answer_chain.py,sha256=fGq4Ls6cuHBzTCntl1kLHuCp43vEssDkWZA_gPTcm4E,5415
|
|
8
|
+
langchain_timbr/langchain/generate_timbr_sql_chain.py,sha256=mb8VHTpq-Cw2UqDPDE-Z7dmJ7xtInktkN5nr6N9206g,13150
|
|
9
|
+
langchain_timbr/langchain/identify_concept_chain.py,sha256=ga91QnuaXXxVAbx5Cz32jcq-PXby4zZacil2Kr-GTqo,8699
|
|
10
|
+
langchain_timbr/langchain/timbr_sql_agent.py,sha256=Uxvw742o6478LvIIQUVK64tBYzDWObj6DBwWGMi0bi8,21895
|
|
11
|
+
langchain_timbr/langchain/validate_timbr_sql_chain.py,sha256=Z1FfykDDba1lPSTN0J0Y2Fhxe2sOFqV9Y8-rudsTeJQ,13650
|
|
12
|
+
langchain_timbr/langgraph/__init__.py,sha256=mKBFd0x01jWpRujUWe-suX3FFhenPoDxrvzs8I0mum0,457
|
|
13
|
+
langchain_timbr/langgraph/execute_timbr_query_node.py,sha256=1JB8pMkSeBGxDMd64OXjZUUjvizILJN8bkz6ibbpgE8,6229
|
|
14
|
+
langchain_timbr/langgraph/generate_response_node.py,sha256=V1XvbmkRBiJ_cExGpWXsBEUxvlLQA3hsVlsg6NHbD94,2504
|
|
15
|
+
langchain_timbr/langgraph/generate_timbr_sql_node.py,sha256=BKFbvf9_YJmpfmCYPTpDm7z6Lvy0regmYGmNznPMl_0,5563
|
|
16
|
+
langchain_timbr/langgraph/identify_concept_node.py,sha256=APhgyNW9YiG1d7lxiZWyFxABHo6vy8sQaXCDRnSPL8w,3821
|
|
17
|
+
langchain_timbr/langgraph/validate_timbr_query_node.py,sha256=eFAIryr-oVrgbbtVHNQTR6md2XS_3cmC8pLgYQ89BiE,5401
|
|
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=QVmfA9cHO2IPVsKG8V5cuMm2gPfvRq2VzLcx04sqT88,12197
|
|
22
|
+
langchain_timbr/utils/temperature_supported_models.json,sha256=d3UmBUpG38zDjjB42IoGpHTUaf0pHMBRSPY99ao1a3g,1832
|
|
23
|
+
langchain_timbr/utils/timbr_llm_utils.py,sha256=iuCPwEOmd0b5N8LNO3IV7K3qozlalUXvh_0QD8bHH28,41001
|
|
24
|
+
langchain_timbr/utils/timbr_utils.py,sha256=olSwn_BYalkhiHR87zfgjosxxGwtMmsrQTqAtu64kPs,20603
|
|
25
|
+
langchain_timbr-3.0.0.dist-info/METADATA,sha256=vvI-waiUk2ZNRxM791b5zpIIvap8xnCFcwjONW8kZkE,10767
|
|
26
|
+
langchain_timbr-3.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
27
|
+
langchain_timbr-3.0.0.dist-info/licenses/LICENSE,sha256=0ITGFk2alkC7-e--bRGtuzDrv62USIiVyV2Crf3_L_0,1065
|
|
28
|
+
langchain_timbr-3.0.0.dist-info/RECORD,,
|