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.
@@ -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,5 @@
1
+ """Extraction engines for different processing strategies."""
2
+
3
+ from .extraction_engine import ExtractionEngine
4
+
5
+ __all__ = ["ExtractionEngine"]
@@ -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
+ )