structx 0.4.8__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.
- structx/__init__.py +24 -0
- structx/core/__init__.py +0 -0
- structx/core/config.py +127 -0
- structx/core/exceptions.py +34 -0
- structx/core/models.py +147 -0
- structx/extraction/__init__.py +38 -0
- structx/extraction/core/__init__.py +6 -0
- structx/extraction/core/llm_core.py +206 -0
- structx/extraction/core/model_utils.py +143 -0
- structx/extraction/engines/__init__.py +5 -0
- structx/extraction/engines/extraction_engine.py +221 -0
- structx/extraction/extractor.py +540 -0
- structx/extraction/generator.py +232 -0
- structx/extraction/processors/__init__.py +6 -0
- structx/extraction/processors/data_content.py +367 -0
- structx/extraction/processors/model_operations.py +233 -0
- structx/extraction/result_manager.py +125 -0
- structx/utils/__init__.py +0 -0
- structx/utils/constants.py +36 -0
- structx/utils/file_reader.py +298 -0
- structx/utils/helpers.py +233 -0
- structx/utils/prompts.py +183 -0
- structx/utils/types.py +12 -0
- structx/utils/usage.py +327 -0
- structx-0.4.8.dist-info/METADATA +313 -0
- structx-0.4.8.dist-info/RECORD +29 -0
- structx-0.4.8.dist-info/WHEEL +5 -0
- structx-0.4.8.dist-info/licenses/LICENSE +21 -0
- structx-0.4.8.dist-info/top_level.txt +1 -0
structx/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
structx: Structured data extraction using LLMs
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from structx.core.config import ExtractionConfig, StepConfig
|
|
6
|
+
from structx.core.models import (
|
|
7
|
+
ExtractionGuide,
|
|
8
|
+
ExtractionRequest,
|
|
9
|
+
ModelField,
|
|
10
|
+
QueryRefinement,
|
|
11
|
+
)
|
|
12
|
+
from structx.extraction.extractor import Extractor
|
|
13
|
+
|
|
14
|
+
__version__ = "0.4.8"
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Extractor",
|
|
17
|
+
"ExtractionConfig",
|
|
18
|
+
"StepConfig",
|
|
19
|
+
"ModelField",
|
|
20
|
+
"QueryAnalysis",
|
|
21
|
+
"QueryRefinement",
|
|
22
|
+
"ExtractionGuide",
|
|
23
|
+
"ExtractionRequest",
|
|
24
|
+
]
|
structx/core/__init__.py
ADDED
|
File without changes
|
structx/core/config.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated, Any, Dict, Optional, Union
|
|
3
|
+
|
|
4
|
+
from omegaconf import OmegaConf
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from structx.utils.types import DictStrAny
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StepConfig(BaseModel):
|
|
11
|
+
"""Configuration for an individual extraction step"""
|
|
12
|
+
|
|
13
|
+
temperature: Optional[
|
|
14
|
+
Annotated[
|
|
15
|
+
float, Field(ge=0.0, le=1.0, description="Sampling temperature for LLM")
|
|
16
|
+
]
|
|
17
|
+
] = None
|
|
18
|
+
top_p: Optional[
|
|
19
|
+
Annotated[
|
|
20
|
+
float, Field(ge=0.0, le=1.0, description="Nucleus sampling parameter")
|
|
21
|
+
]
|
|
22
|
+
] = None
|
|
23
|
+
max_tokens: Optional[
|
|
24
|
+
Annotated[int, Field(gt=0, description="Maximum tokens in completion")]
|
|
25
|
+
] = None
|
|
26
|
+
|
|
27
|
+
def model_dump(self, *args, **kwargs) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
Return dictionary of non-None values merged with defaults
|
|
30
|
+
"""
|
|
31
|
+
# Default values for each step
|
|
32
|
+
defaults = {
|
|
33
|
+
"temperature": 0.1,
|
|
34
|
+
"top_p": 0.1,
|
|
35
|
+
"max_tokens": 2000,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Get current values, excluding None
|
|
39
|
+
current = {
|
|
40
|
+
k: v
|
|
41
|
+
for k, v in super().model_dump(*args, **kwargs).items()
|
|
42
|
+
if v is not None
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Merge with defaults, preferring current values
|
|
46
|
+
return {**defaults, **current}
|
|
47
|
+
|
|
48
|
+
class Config:
|
|
49
|
+
validate_assignment = True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ExtractionConfig:
|
|
53
|
+
"""Configuration management for structx using OmegaConf and Pydantic"""
|
|
54
|
+
|
|
55
|
+
DEFAULT_CONFIG = {
|
|
56
|
+
"analysis": {"temperature": 0.2, "top_p": 0.1, "max_tokens": 2000},
|
|
57
|
+
"refinement": {"temperature": 0.1, "top_p": 0.05, "max_tokens": 2000},
|
|
58
|
+
"extraction": {"temperature": 0.0, "top_p": 0.1, "max_tokens": 8192},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
config: Optional[Union[Dict[str, Any], str]] = None,
|
|
64
|
+
config_path: Optional[Union[str, Path]] = None,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Initialize configuration
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config: Optional configuration dictionary or YAML string
|
|
71
|
+
config_path: Optional path to YAML configuration file
|
|
72
|
+
"""
|
|
73
|
+
# Create base config from defaults
|
|
74
|
+
self.conf = OmegaConf.create(self.DEFAULT_CONFIG)
|
|
75
|
+
|
|
76
|
+
# Load from file if provided
|
|
77
|
+
if config_path:
|
|
78
|
+
file_conf = OmegaConf.load(config_path)
|
|
79
|
+
self.conf = OmegaConf.merge(self.conf, file_conf)
|
|
80
|
+
|
|
81
|
+
# Merge with provided config if any
|
|
82
|
+
if config:
|
|
83
|
+
if isinstance(config, str):
|
|
84
|
+
conf_to_merge = OmegaConf.create(config)
|
|
85
|
+
else:
|
|
86
|
+
conf_to_merge = OmegaConf.create(config)
|
|
87
|
+
self.conf = OmegaConf.merge(self.conf, conf_to_merge)
|
|
88
|
+
|
|
89
|
+
# Validate using Pydantic models
|
|
90
|
+
self._validate_config()
|
|
91
|
+
|
|
92
|
+
def _validate_config(self) -> None:
|
|
93
|
+
"""Validate configuration using Pydantic models"""
|
|
94
|
+
for step in ["analysis", "refinement", "extraction"]:
|
|
95
|
+
step_config: DictStrAny = OmegaConf.to_container(self.conf.get(step, {})) # type: ignore
|
|
96
|
+
# Validate using StepConfig model
|
|
97
|
+
StepConfig(**step_config)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def analysis(self) -> DictStrAny:
|
|
101
|
+
"""Get validated analysis step configuration"""
|
|
102
|
+
config: DictStrAny = OmegaConf.to_container(self.conf.analysis) # type: ignore
|
|
103
|
+
return StepConfig(**config).model_dump(exclude_none=True)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def refinement(self) -> DictStrAny:
|
|
107
|
+
"""Get validated refinement step configuration"""
|
|
108
|
+
config: DictStrAny = OmegaConf.to_container(self.conf.refinement) # type: ignore
|
|
109
|
+
return StepConfig(**config).model_dump(exclude_none=True)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def extraction(self) -> DictStrAny:
|
|
113
|
+
"""Get validated extraction step configuration"""
|
|
114
|
+
config: DictStrAny = OmegaConf.to_container(self.conf.extraction) # type: ignore
|
|
115
|
+
return StepConfig(**config).model_dump(exclude_none=True)
|
|
116
|
+
|
|
117
|
+
def save(self, path: str) -> None:
|
|
118
|
+
"""Save configuration to YAML file"""
|
|
119
|
+
OmegaConf.save(self.conf, path)
|
|
120
|
+
|
|
121
|
+
def __str__(self) -> str:
|
|
122
|
+
"""String representation of configuration"""
|
|
123
|
+
return OmegaConf.to_yaml(self.conf)
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
"""Representation of configuration"""
|
|
127
|
+
return self.__str__()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class StructXError(Exception):
|
|
2
|
+
"""Base exception for all structx errors"""
|
|
3
|
+
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConfigurationError(StructXError):
|
|
8
|
+
"""Error in configuration"""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExtractionError(StructXError):
|
|
14
|
+
"""Error during extraction process"""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ValidationError(StructXError):
|
|
20
|
+
"""Error in data validation"""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ModelGenerationError(StructXError):
|
|
26
|
+
"""Error in dynamic model generation"""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FileError(StructXError):
|
|
32
|
+
"""Error in file operations"""
|
|
33
|
+
|
|
34
|
+
pass
|
structx/core/models.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Dict, Generic, List, Optional, Type, Union
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from structx.utils.types import T
|
|
8
|
+
from structx.utils.usage import ExtractorUsage, UsageSummary
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ModelField(BaseModel):
|
|
12
|
+
"""Definition of a field in the extraction model"""
|
|
13
|
+
|
|
14
|
+
name: str = Field(description="Name of the field")
|
|
15
|
+
type: str = Field(description="Type of the field")
|
|
16
|
+
description: str = Field(description="Description of what this field represents")
|
|
17
|
+
validation: Optional[Dict[str, Any]] = Field(
|
|
18
|
+
default_factory=dict, description="Additional validation rules"
|
|
19
|
+
)
|
|
20
|
+
nested_fields: Optional[List["ModelField"]] = Field(
|
|
21
|
+
default=None, description="Fields for nested models"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
class Config:
|
|
25
|
+
validate_assignment = True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class QueryRefinement(BaseModel):
|
|
29
|
+
"""Refined query with structural information"""
|
|
30
|
+
|
|
31
|
+
refined_query: str = Field(description="Expanded query with structure requirements")
|
|
32
|
+
data_characteristics: Optional[List[str]] = Field(
|
|
33
|
+
description="Characteristics of data to extract"
|
|
34
|
+
)
|
|
35
|
+
structural_requirements: Optional[Dict[str, Any]] = Field(
|
|
36
|
+
description="Requirements for data structure"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExtractionGuide(BaseModel):
|
|
41
|
+
"""Guide for structured extraction"""
|
|
42
|
+
|
|
43
|
+
target_columns: List[str] = Field(description="Columns to analyze")
|
|
44
|
+
|
|
45
|
+
structural_patterns: Optional[Dict[str, str]] = Field(
|
|
46
|
+
description="Patterns for structuring data"
|
|
47
|
+
)
|
|
48
|
+
relationship_rules: Optional[List[str]] = Field(
|
|
49
|
+
description="Rules for data relationships"
|
|
50
|
+
)
|
|
51
|
+
organization_principles: Optional[List[str]] = Field(
|
|
52
|
+
description="Principles for data organization"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
class Config:
|
|
56
|
+
extra = "allow" # Allow extra fields to be flexible with LLM responses
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ExtractionRequest(BaseModel):
|
|
60
|
+
"""Request for model generation"""
|
|
61
|
+
|
|
62
|
+
model_name: str = Field(description="Name for generated model")
|
|
63
|
+
model_description: str = Field(description="Description of model purpose")
|
|
64
|
+
fields: List[ModelField] = Field(description="Fields to extract")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class ExtractionResult(Generic[T]):
|
|
69
|
+
"""
|
|
70
|
+
Container for extraction results.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
data: Extracted data (DataFrame or list of model instances)
|
|
74
|
+
failed: DataFrame with failed extractions
|
|
75
|
+
model: Generated or provided model class
|
|
76
|
+
usage: Token usage information across all extraction steps
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
data: Union[pd.DataFrame, List[T]]
|
|
80
|
+
failed: pd.DataFrame
|
|
81
|
+
model: Type[T]
|
|
82
|
+
usage: Optional[ExtractorUsage] = None
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def success_count(self) -> int:
|
|
86
|
+
"""Number of successful extractions"""
|
|
87
|
+
if isinstance(self.data, pd.DataFrame):
|
|
88
|
+
return len(self.data)
|
|
89
|
+
return len(self.data)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def failure_count(self) -> int:
|
|
93
|
+
"""Number of failed extractions"""
|
|
94
|
+
return len(self.failed)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def success_rate(self) -> float:
|
|
98
|
+
"""Success rate as a percentage"""
|
|
99
|
+
total = self.success_count + self.failure_count
|
|
100
|
+
return (self.success_count / total * 100) if total > 0 else 0
|
|
101
|
+
|
|
102
|
+
def get_token_usage(self, detailed: bool = False) -> Optional[UsageSummary]:
|
|
103
|
+
"""
|
|
104
|
+
Get structured token usage information.
|
|
105
|
+
|
|
106
|
+
Provides a detailed breakdown of token usage across all steps of the
|
|
107
|
+
extraction process.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
detailed: If True, includes detailed breakdown of each extraction step
|
|
111
|
+
(useful for multi-document extraction)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
UsageSummary object with token usage information, or None if usage tracking
|
|
115
|
+
isn't available
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
```python
|
|
119
|
+
result = extractor.extract(data, query)
|
|
120
|
+
usage = result.get_token_usage()
|
|
121
|
+
print(f"Total tokens: {usage.total_tokens}")
|
|
122
|
+
|
|
123
|
+
# Access step-specific usage
|
|
124
|
+
for step in usage.steps:
|
|
125
|
+
print(f"{step.name}: {step.tokens} tokens")
|
|
126
|
+
```
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
if not self.usage:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
return self.usage.get_usage_summary(detailed=detailed)
|
|
133
|
+
|
|
134
|
+
def __repr__(self) -> str:
|
|
135
|
+
"""String representation"""
|
|
136
|
+
return (
|
|
137
|
+
f"ExtractionResult(success={self.success_count}, "
|
|
138
|
+
f"failed={self.failure_count}, "
|
|
139
|
+
f"model={self.model.__name__})"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def __str__(self):
|
|
143
|
+
return self.__repr__()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Rebuild the model to ensure nested fields are properly defined
|
|
147
|
+
ModelField.model_rebuild()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extraction module for structured data extraction.
|
|
3
|
+
|
|
4
|
+
This module provides a well-organized architecture for extracting structured data
|
|
5
|
+
from various sources using LLMs with dynamic model generation.
|
|
6
|
+
|
|
7
|
+
Structure:
|
|
8
|
+
- core/: Core LLM operations and utilities
|
|
9
|
+
- processors/: Data processing and model operations
|
|
10
|
+
- engines/: Extraction engines for different strategies
|
|
11
|
+
- extractor.py: Main orchestrator class
|
|
12
|
+
- generator.py: Dynamic model generation
|
|
13
|
+
- result_manager.py: Result management utilities
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Core components
|
|
17
|
+
from .core import LLMCore, ModelUtils
|
|
18
|
+
|
|
19
|
+
# Engines
|
|
20
|
+
from .engines import ExtractionEngine
|
|
21
|
+
from .extractor import Extractor
|
|
22
|
+
from .generator import ModelGenerator
|
|
23
|
+
|
|
24
|
+
# Processors
|
|
25
|
+
from .processors import ContentAnalyzer, DataProcessor, ModelOperations
|
|
26
|
+
from .result_manager import ResultManager
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Extractor",
|
|
30
|
+
"ModelGenerator",
|
|
31
|
+
"ResultManager",
|
|
32
|
+
"LLMCore",
|
|
33
|
+
"ModelUtils",
|
|
34
|
+
"ContentAnalyzer",
|
|
35
|
+
"DataProcessor",
|
|
36
|
+
"ModelOperations",
|
|
37
|
+
"ExtractionEngine",
|
|
38
|
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core LLM operations and query processing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Dict, List, Type
|
|
8
|
+
|
|
9
|
+
from instructor import Instructor
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from tenacity import (
|
|
12
|
+
after_log,
|
|
13
|
+
before_sleep_log,
|
|
14
|
+
retry,
|
|
15
|
+
retry_if_exception_type,
|
|
16
|
+
stop_after_attempt,
|
|
17
|
+
wait_exponential,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from structx.core.config import DictStrAny, ExtractionConfig
|
|
21
|
+
from structx.core.exceptions import ExtractionError
|
|
22
|
+
from structx.core.models import ExtractionGuide, QueryRefinement
|
|
23
|
+
from structx.utils.helpers import handle_errors
|
|
24
|
+
from structx.utils.prompts import (
|
|
25
|
+
guide_system_prompt,
|
|
26
|
+
guide_template,
|
|
27
|
+
query_refinement_system_prompt,
|
|
28
|
+
query_refinement_template,
|
|
29
|
+
)
|
|
30
|
+
from structx.utils.types import ResponseType
|
|
31
|
+
from structx.utils.usage import ExtractionStep, ExtractorUsage, StepUsage
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LLMCore:
|
|
35
|
+
"""
|
|
36
|
+
Core LLM operations with retry logic, usage tracking, and query processing.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
client: Instructor,
|
|
42
|
+
model_name: str,
|
|
43
|
+
config: ExtractionConfig,
|
|
44
|
+
max_retries: int = 3,
|
|
45
|
+
min_wait: int = 1,
|
|
46
|
+
max_wait: int = 10,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Initialize LLM core.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
client: Instructor-patched client
|
|
53
|
+
model_name: Name of the model to use
|
|
54
|
+
config: Extraction configuration
|
|
55
|
+
max_retries: Maximum number of retries for extraction
|
|
56
|
+
min_wait: Minimum seconds to wait between retries
|
|
57
|
+
max_wait: Maximum seconds to wait between retries
|
|
58
|
+
"""
|
|
59
|
+
self.client = client
|
|
60
|
+
self.model_name = model_name
|
|
61
|
+
self.config = config
|
|
62
|
+
self.max_retries = max_retries
|
|
63
|
+
self.min_wait = min_wait
|
|
64
|
+
self.max_wait = max_wait
|
|
65
|
+
self.usage_lock = threading.Lock()
|
|
66
|
+
self.usage = ExtractorUsage()
|
|
67
|
+
|
|
68
|
+
def reset_usage(self) -> None:
|
|
69
|
+
"""Reset usage tracking."""
|
|
70
|
+
with self.usage_lock:
|
|
71
|
+
self.usage = ExtractorUsage()
|
|
72
|
+
|
|
73
|
+
def get_usage(self) -> ExtractorUsage:
|
|
74
|
+
"""Get current usage statistics."""
|
|
75
|
+
with self.usage_lock:
|
|
76
|
+
return self.usage
|
|
77
|
+
|
|
78
|
+
def _create_retry_decorator(self):
|
|
79
|
+
"""Create retry decorator with instance parameters."""
|
|
80
|
+
return retry(
|
|
81
|
+
stop=stop_after_attempt(self.max_retries),
|
|
82
|
+
wait=wait_exponential(
|
|
83
|
+
multiplier=self.min_wait, min=self.min_wait, max=self.max_wait
|
|
84
|
+
),
|
|
85
|
+
retry=retry_if_exception_type(ExtractionError),
|
|
86
|
+
before_sleep=before_sleep_log(logger, logging.DEBUG),
|
|
87
|
+
after=after_log(logger, logging.DEBUG),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@handle_errors(error_message="LLM completion failed", error_type=ExtractionError)
|
|
91
|
+
def complete(
|
|
92
|
+
self,
|
|
93
|
+
messages: List[Dict[str, str]],
|
|
94
|
+
response_model: Type[ResponseType],
|
|
95
|
+
config: DictStrAny,
|
|
96
|
+
step: ExtractionStep,
|
|
97
|
+
) -> ResponseType:
|
|
98
|
+
"""
|
|
99
|
+
Perform LLM completion and track token usage.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
messages: Messages for the completion
|
|
103
|
+
response_model: Pydantic model for response
|
|
104
|
+
config: Configuration for the completion
|
|
105
|
+
step: Step being performed for usage tracking
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Completion result
|
|
109
|
+
"""
|
|
110
|
+
result, completion = self.client.chat.completions.create_with_completion(
|
|
111
|
+
model=self.model_name,
|
|
112
|
+
response_model=response_model,
|
|
113
|
+
messages=messages,
|
|
114
|
+
**config,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
usage = StepUsage.from_completion(completion, step)
|
|
118
|
+
|
|
119
|
+
# Add to usage tracking if available (thread-safe)
|
|
120
|
+
if usage:
|
|
121
|
+
with self.usage_lock:
|
|
122
|
+
self.usage.add_step_usage(usage)
|
|
123
|
+
logger.debug(f"Step {step.value}: {usage.total_tokens} tokens used")
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
def complete_with_retry(
|
|
128
|
+
self,
|
|
129
|
+
messages: List[Dict[str, str]],
|
|
130
|
+
response_model: Type[ResponseType],
|
|
131
|
+
config: DictStrAny,
|
|
132
|
+
step: ExtractionStep,
|
|
133
|
+
) -> ResponseType:
|
|
134
|
+
"""
|
|
135
|
+
Perform LLM completion with retry logic.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
messages: Messages for the completion
|
|
139
|
+
response_model: Pydantic model for response
|
|
140
|
+
config: Configuration for the completion
|
|
141
|
+
step: Step being performed for usage tracking
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Completion result
|
|
145
|
+
"""
|
|
146
|
+
retry_decorator = self._create_retry_decorator()
|
|
147
|
+
|
|
148
|
+
@retry_decorator
|
|
149
|
+
def _complete():
|
|
150
|
+
return self.complete(messages, response_model, config, step)
|
|
151
|
+
|
|
152
|
+
return _complete()
|
|
153
|
+
|
|
154
|
+
@handle_errors(error_message="Query refinement failed", error_type=ExtractionError)
|
|
155
|
+
def refine_query(self, query: str) -> QueryRefinement:
|
|
156
|
+
"""
|
|
157
|
+
Refine and expand query with structural requirements.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
query: Original query string
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Refined query with additional details
|
|
164
|
+
"""
|
|
165
|
+
return self.complete(
|
|
166
|
+
messages=[
|
|
167
|
+
{"role": "system", "content": query_refinement_system_prompt},
|
|
168
|
+
{
|
|
169
|
+
"role": "user",
|
|
170
|
+
"content": query_refinement_template.substitute(query=query),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
response_model=QueryRefinement,
|
|
174
|
+
config=self.config.refinement,
|
|
175
|
+
step=ExtractionStep.REFINEMENT,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@handle_errors(error_message="Guide generation failed", error_type=ExtractionError)
|
|
179
|
+
def generate_extraction_guide(
|
|
180
|
+
self, refined_query: QueryRefinement, data_columns: List[str]
|
|
181
|
+
) -> ExtractionGuide:
|
|
182
|
+
"""
|
|
183
|
+
Generate extraction guide based on refined query.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
refined_query: Refined query with details
|
|
187
|
+
data_columns: Available data columns
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Extraction guide with patterns and rules
|
|
191
|
+
"""
|
|
192
|
+
return self.complete(
|
|
193
|
+
messages=[
|
|
194
|
+
{"role": "system", "content": guide_system_prompt},
|
|
195
|
+
{
|
|
196
|
+
"role": "user",
|
|
197
|
+
"content": guide_template.substitute(
|
|
198
|
+
data_characteristics=refined_query.data_characteristics,
|
|
199
|
+
available_columns=data_columns,
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
response_model=ExtractionGuide,
|
|
204
|
+
config=self.config.refinement,
|
|
205
|
+
step=ExtractionStep.GUIDE,
|
|
206
|
+
)
|