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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for model operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Literal, Type
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModelUtils:
|
|
11
|
+
"""
|
|
12
|
+
Utility functions for working with Pydantic models.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def extract_model_schema_info(model: Type[BaseModel]) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Extract model schema information for prompts.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
model: Pydantic model to extract info from
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Formatted string with model field descriptions
|
|
25
|
+
"""
|
|
26
|
+
model_schema = model.model_json_schema()
|
|
27
|
+
model_schema_info = ""
|
|
28
|
+
|
|
29
|
+
# Include field descriptions to help with extraction
|
|
30
|
+
for field, details in model_schema.get("properties", {}).items():
|
|
31
|
+
field_type = details.get("type", "unknown")
|
|
32
|
+
field_desc = details.get("description", "")
|
|
33
|
+
if "enum" in details:
|
|
34
|
+
field_desc += (
|
|
35
|
+
f" Possible values: {', '.join(map(str, details['enum']))}"
|
|
36
|
+
)
|
|
37
|
+
model_schema_info += f"- {field} ({field_type}): {field_desc}\n"
|
|
38
|
+
|
|
39
|
+
return model_schema_info
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def create_model_context(
|
|
43
|
+
model: Type[BaseModel], instruction_type: Literal["text", "document"] = "text"
|
|
44
|
+
) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Create contextual information for custom model extraction.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
model: Pydantic model to create context for
|
|
50
|
+
instruction_type: Type of instruction ("text" or "document")
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Formatted context string for prompts
|
|
54
|
+
"""
|
|
55
|
+
model_schema_info = ModelUtils.extract_model_schema_info(model)
|
|
56
|
+
|
|
57
|
+
if not model_schema_info.strip():
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
if instruction_type == "document":
|
|
61
|
+
return (
|
|
62
|
+
f"\nModel fields and descriptions:\n{model_schema_info}\n"
|
|
63
|
+
f"Ensure all applicable fields are populated with relevant information from the document."
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
return (
|
|
67
|
+
f"\nModel fields and descriptions:\n{model_schema_info}\n"
|
|
68
|
+
f"Ensure all applicable fields are populated with relevant information from the text."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def extract_field_characteristics(model: Type[BaseModel]) -> List[str]:
|
|
73
|
+
"""
|
|
74
|
+
Extract data characteristics from model properties.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
model: Pydantic model to analyze
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of field characteristic descriptions
|
|
81
|
+
"""
|
|
82
|
+
model_schema = model.model_json_schema()
|
|
83
|
+
model_properties = model_schema.get("properties", {})
|
|
84
|
+
data_characteristics = []
|
|
85
|
+
|
|
86
|
+
for prop_name, prop_info in model_properties.items():
|
|
87
|
+
prop_description = prop_info.get("description", "")
|
|
88
|
+
prop_type = prop_info.get("type", "")
|
|
89
|
+
enum_values = prop_info.get("enum", [])
|
|
90
|
+
|
|
91
|
+
if prop_description:
|
|
92
|
+
if enum_values:
|
|
93
|
+
data_characteristics.append(
|
|
94
|
+
f"{prop_name} ({prop_type}): {prop_description}. "
|
|
95
|
+
f"Possible values: {', '.join(map(str, enum_values))}"
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
data_characteristics.append(
|
|
99
|
+
f"{prop_name} ({prop_type}): {prop_description}"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
data_characteristics.append(f"{prop_name} ({prop_type})")
|
|
103
|
+
|
|
104
|
+
return data_characteristics
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def extract_structural_requirements(model: Type[BaseModel]) -> Dict[str, str]:
|
|
108
|
+
"""
|
|
109
|
+
Extract structural requirements from model.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
model: Pydantic model to analyze
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dictionary mapping field names to types
|
|
116
|
+
"""
|
|
117
|
+
model_schema = model.model_json_schema()
|
|
118
|
+
model_properties = model_schema.get("properties", {})
|
|
119
|
+
structural_requirements = {}
|
|
120
|
+
|
|
121
|
+
for prop_name, prop_info in model_properties.items():
|
|
122
|
+
if "type" in prop_info:
|
|
123
|
+
structural_requirements[prop_name] = prop_info["type"]
|
|
124
|
+
|
|
125
|
+
return structural_requirements
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def get_model_description(model: Type[BaseModel]) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Get a descriptive name for the model.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
model: Pydantic model to describe
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Model description string
|
|
137
|
+
"""
|
|
138
|
+
model_schema = model.model_json_schema()
|
|
139
|
+
return (
|
|
140
|
+
model_schema.get("description", "")
|
|
141
|
+
or model_schema.get("title", "")
|
|
142
|
+
or model.__name__
|
|
143
|
+
)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core extraction engine with different processing strategies.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Type, Union
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from pydantic import BaseModel, Field, create_model
|
|
9
|
+
|
|
10
|
+
from structx.core.exceptions import ExtractionError
|
|
11
|
+
from structx.core.models import ExtractionGuide, QueryRefinement
|
|
12
|
+
from structx.extraction.core.llm_core import LLMCore
|
|
13
|
+
from structx.extraction.core.model_utils import ModelUtils
|
|
14
|
+
from structx.utils.helpers import handle_errors
|
|
15
|
+
from structx.utils.prompts import extraction_system_prompt, extraction_template
|
|
16
|
+
from structx.utils.usage import ExtractionStep
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExtractionEngine:
|
|
20
|
+
"""
|
|
21
|
+
Core extraction engine with different processing strategies.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, llm_core: LLMCore):
|
|
25
|
+
"""
|
|
26
|
+
Initialize extraction engine.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
llm_core: LLM core for completions
|
|
30
|
+
"""
|
|
31
|
+
self.llm_core = llm_core
|
|
32
|
+
|
|
33
|
+
@handle_errors(error_message="Text extraction failed", error_type=ExtractionError)
|
|
34
|
+
def extract_with_model(
|
|
35
|
+
self,
|
|
36
|
+
text: str,
|
|
37
|
+
extraction_model: Type[BaseModel],
|
|
38
|
+
refined_query: QueryRefinement,
|
|
39
|
+
guide: ExtractionGuide,
|
|
40
|
+
is_custom_model: bool = False,
|
|
41
|
+
) -> List[BaseModel]:
|
|
42
|
+
"""
|
|
43
|
+
Extract data with enforced structure with retries and usage tracking.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
text: Text to extract from
|
|
47
|
+
extraction_model: Pydantic model for extraction
|
|
48
|
+
refined_query: Refined query with details
|
|
49
|
+
guide: Extraction guide with patterns
|
|
50
|
+
is_custom_model: Whether this is a user-provided model
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of extracted model instances
|
|
54
|
+
"""
|
|
55
|
+
# Create a container model to wrap the list items
|
|
56
|
+
# this is necessary to be able to track token usage, when passing an iterable data model
|
|
57
|
+
# result._raw_response does not exist making usage calculations not possible
|
|
58
|
+
container_name = f"{extraction_model.__name__}Container"
|
|
59
|
+
container_model = create_model(
|
|
60
|
+
container_name,
|
|
61
|
+
__base__=BaseModel,
|
|
62
|
+
items=(
|
|
63
|
+
List[extraction_model],
|
|
64
|
+
Field(description=f"List of {extraction_model.__name__} items"),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Get model context for custom models
|
|
69
|
+
extra_context = ""
|
|
70
|
+
if is_custom_model:
|
|
71
|
+
extra_context = ModelUtils.create_model_context(extraction_model, "text")
|
|
72
|
+
|
|
73
|
+
# Use _perform_llm_completion with the container model
|
|
74
|
+
container = self.llm_core.complete_with_retry(
|
|
75
|
+
messages=[
|
|
76
|
+
{"role": "system", "content": extraction_system_prompt},
|
|
77
|
+
{
|
|
78
|
+
"role": "user",
|
|
79
|
+
"content": extraction_template.substitute(
|
|
80
|
+
query=refined_query.refined_query,
|
|
81
|
+
patterns=guide.structural_patterns,
|
|
82
|
+
rules=(
|
|
83
|
+
guide.relationship_rules + [extra_context]
|
|
84
|
+
if extra_context
|
|
85
|
+
else guide.relationship_rules
|
|
86
|
+
),
|
|
87
|
+
text=text,
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
response_model=container_model,
|
|
92
|
+
config=self.llm_core.config.extraction,
|
|
93
|
+
step=ExtractionStep.EXTRACTION,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Return just the items
|
|
97
|
+
return container.items
|
|
98
|
+
|
|
99
|
+
@handle_errors(error_message="PDF extraction failed", error_type=ExtractionError)
|
|
100
|
+
def extract_with_multimodal_pdf(
|
|
101
|
+
self,
|
|
102
|
+
pdf_path: str,
|
|
103
|
+
extraction_model: Type[BaseModel],
|
|
104
|
+
refined_query: QueryRefinement,
|
|
105
|
+
guide: ExtractionGuide,
|
|
106
|
+
is_custom_model: bool = False,
|
|
107
|
+
) -> List[BaseModel]:
|
|
108
|
+
"""
|
|
109
|
+
Extract data from PDF using instructor's multimodal support.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
pdf_path: Path to PDF file
|
|
113
|
+
extraction_model: Pydantic model for extraction
|
|
114
|
+
refined_query: Refined query with details
|
|
115
|
+
guide: Extraction guide with patterns
|
|
116
|
+
is_custom_model: Whether this is a user-provided model
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of extracted model instances
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
from instructor.multimodal import PDF
|
|
123
|
+
except ImportError:
|
|
124
|
+
raise ImportError(
|
|
125
|
+
"instructor multimodal support is required for PDF processing. "
|
|
126
|
+
"Install it with: pip install instructor[multimodal]"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# For multimodal PDF, we need a single wrapper model, not a container with multiple items
|
|
130
|
+
# This is because instructor's multimodal support expects a single response, not multiple tool calls
|
|
131
|
+
wrapper_name = f"{extraction_model.__name__}List"
|
|
132
|
+
wrapper_model = create_model(
|
|
133
|
+
wrapper_name,
|
|
134
|
+
__base__=BaseModel,
|
|
135
|
+
items=(
|
|
136
|
+
List[extraction_model],
|
|
137
|
+
Field(
|
|
138
|
+
description=f"List of extracted {extraction_model.__name__} items from the document"
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Get model context for custom models
|
|
144
|
+
extra_context = ""
|
|
145
|
+
if is_custom_model:
|
|
146
|
+
extra_context = ModelUtils.create_model_context(
|
|
147
|
+
extraction_model, "document"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
content = [
|
|
151
|
+
f"Extract structured information from this PDF document following these guidelines:\n\n"
|
|
152
|
+
f"Query: {refined_query.refined_query}\n"
|
|
153
|
+
f"Patterns: {guide.structural_patterns}\n"
|
|
154
|
+
f"Rules: {guide.relationship_rules + [extra_context] if extra_context else guide.relationship_rules}\n\n",
|
|
155
|
+
PDF.from_path(pdf_path),
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
logger.info(
|
|
159
|
+
f"Extracting from PDF: {pdf_path} with query: {refined_query.refined_query}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
result = self.llm_core.complete_with_retry(
|
|
163
|
+
response_model=wrapper_model,
|
|
164
|
+
messages=[
|
|
165
|
+
{"role": "system", "content": extraction_system_prompt},
|
|
166
|
+
{
|
|
167
|
+
"role": "user",
|
|
168
|
+
"content": content,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
step=ExtractionStep.EXTRACTION,
|
|
172
|
+
config=self.llm_core.config.extraction,
|
|
173
|
+
)
|
|
174
|
+
logger.info(f"Completed extraction for PDF: {pdf_path}")
|
|
175
|
+
return result.items
|
|
176
|
+
|
|
177
|
+
def extract_from_row_data(
|
|
178
|
+
self,
|
|
179
|
+
row_data: Union[str, Dict[str, Any]],
|
|
180
|
+
extraction_model: Type[BaseModel],
|
|
181
|
+
refined_query: QueryRefinement,
|
|
182
|
+
guide: ExtractionGuide,
|
|
183
|
+
is_custom_model: bool = False,
|
|
184
|
+
) -> List[BaseModel]:
|
|
185
|
+
"""
|
|
186
|
+
Extract data from row data (either text or multimodal).
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
row_data: Row data to extract from
|
|
190
|
+
extraction_model: Pydantic model for extraction
|
|
191
|
+
refined_query: Refined query with details
|
|
192
|
+
guide: Extraction guide with patterns
|
|
193
|
+
is_custom_model: Whether this is a user-provided model
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of extracted model instances
|
|
197
|
+
"""
|
|
198
|
+
# Check if this is a multimodal PDF row
|
|
199
|
+
if (
|
|
200
|
+
isinstance(row_data, dict)
|
|
201
|
+
and row_data.get("multimodal")
|
|
202
|
+
and row_data.get("file_type") == "pdf"
|
|
203
|
+
):
|
|
204
|
+
pdf_path = row_data.get("pdf_path")
|
|
205
|
+
return self.extract_with_multimodal_pdf(
|
|
206
|
+
pdf_path=pdf_path,
|
|
207
|
+
extraction_model=extraction_model,
|
|
208
|
+
refined_query=refined_query,
|
|
209
|
+
guide=guide,
|
|
210
|
+
is_custom_model=is_custom_model,
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
# Handle regular text extraction
|
|
214
|
+
row_text = row_data if isinstance(row_data, str) else str(row_data)
|
|
215
|
+
return self.extract_with_model(
|
|
216
|
+
text=row_text,
|
|
217
|
+
extraction_model=extraction_model,
|
|
218
|
+
refined_query=refined_query,
|
|
219
|
+
guide=guide,
|
|
220
|
+
is_custom_model=is_custom_model,
|
|
221
|
+
)
|