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 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
+ ]
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,6 @@
1
+ """Core components for extraction operations."""
2
+
3
+ from .llm_core import LLMCore
4
+ from .model_utils import ModelUtils
5
+
6
+ __all__ = ["LLMCore", "ModelUtils"]
@@ -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
+ )