=============== LIBRARY RULES =============== From library maintainers: - All extraction functions are async — always use async/await - Use Pydantic v2 models as the schema for extraction - Import from saidex directly — the public API is fully exposed in saidex.__init__ - Use extract_data_from_text for simple text input, extract_data for chat history or multimodal content - Use ExtractionMode.JSON for models that do not support tool calling - To reject hallucinated values, mark fields with Grounded() (Annotated marker) or GroundedField() so the extracted value is verified against the source text ### Install uv and Project Dependencies Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md Follow these steps for first-time setup: install uv, clone the repository, and then sync dependencies to create and populate the virtual environment. ```bash pip install uv # or on macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh # 2. Clone the repository git clone https://github.com/mlauf-labs/saidex cd saidex # 3. Create the virtual environment and install all dependencies uv sync ``` -------------------------------- ### Start vLLM Server Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/images.md Installs vLLM and starts a local vision server using a specified model. Ensure the model is compatible and sufficient VRAM is available. ```bash pip install vllm vllm serve Qwen/Qwen2-VL-7B-Instruct \ --port 8000 \ --trust-remote-code \ --max-model-len 4096 ``` -------------------------------- ### Install Langfuse and Langchain OpenAI Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/langfuse-tracing.md Install Langfuse and a provider package like langchain-openai. Langfuse is required to run the example but not as a SAIDEX runtime dependency. ```bash uv pip install "langfuse>=3" langchain-openai ``` -------------------------------- ### Quick Example: Agent Loop with Tools Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/agent-loop.md Demonstrates how to define a tool with Pydantic arguments and an async handler, then use `extract_data_with_tools` to run an agent loop for ticket resolution. This example shows the complete setup from tool definition to running the agent. ```python import asyncio from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from saidex import Tool, extract_data_with_tools # 1. Describe the tool's arguments with a Pydantic model class OrderStatusArgs(BaseModel): order_id: str = Field(description="Order id, e.g. ORD-1042") # 2. Provide an async handler — its return value is shown to the LLM async def order_status_handler(order_id: str) -> dict: # ... real lookup ... return {"order_id": order_id, "status": "shipped", "eta": "2026-06-15"} order_status = Tool( name="get_order_status", description="Look up the current status and ETA of an order.", parameters=OrderStatusArgs, handler=order_status_handler, ) # 3. Define the final-answer schema and run the loop class TicketResolution(BaseModel): order_id: str current_status: str customer_reply: str = Field(description="Friendly reply for the customer") async def main() -> None: llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) resolution, stats = await extract_data_with_tools( llm, TicketResolution, "Customer asks: where is my order ORD-1042?", tools=[order_status], ) print(resolution) print(f"iterations={stats.iterations} tool_calls={stats.tool_calls}") asyncio.run(main()) ``` -------------------------------- ### Install uv for Development Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/quickstart.md Install the uv package manager and use it to set up the development environment for the Saidex project. ```bash # Install uv (if not already installed) pip install uv git clone https://github.com/mlauf-labs/saidex cd saidex uv sync # creates .venv and installs all dependencies ``` -------------------------------- ### Install SAIDEX Source: https://github.com/mlauf-labs/saidex/blob/develop/README_PYPI.md Install the base SAIDEX package. Use this for general usage. ```bash pip install saidex ``` -------------------------------- ### Start vLLM server for local vision models Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md This bash command installs vLLM and starts a local inference server for a vision model. It requires a compatible vision model to be specified. ```bash pip install vllm vllm serve Qwen/Qwen2-VL-7B-Instruct --port 8000 ``` -------------------------------- ### Install Saidex Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md Install the base Saidex package using pip. For OpenAI models with automatic rate-limit handling, install the optional dependency. ```bash pip install saidex ``` ```bash pip install "saidex[openai]" ``` -------------------------------- ### Install SAIDEX with OpenAI Rate-Limit Handling Source: https://github.com/mlauf-labs/saidex/blob/develop/README_PYPI.md Install SAIDEX with optional dependencies for automatic OpenAI rate-limit handling. This is useful when making frequent calls to OpenAI models. ```bash pip install "saidex[openai]" ``` -------------------------------- ### Install Langfuse Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/observability.md Install the Langfuse library using pip. Ensure you are using a version greater than or equal to 3. ```bash uv pip install "langfuse>=3" ``` -------------------------------- ### Create a Tool Instance Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/agent-loop.md Instantiate the `Tool` dataclass, providing a name, description, Pydantic parameters model, and the async handler function. This example creates a `search_products` tool. ```python search = Tool( name="search_products", description="Search the product catalogue by free-text query.", parameters=SearchArgs, handler=search_handler, ) ``` -------------------------------- ### Complete Pydantic Schema Example Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md A comprehensive example demonstrating best practices for schema design, including enums, nested models, typed lists, and detailed field descriptions with constraints. ```python from enum import Enum from pydantic import BaseModel, Field class Priority(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" class Subtask(BaseModel): title: str = Field(description="Short action item, max one sentence") done: bool = Field(default=False, description="true if already completed") class Task(BaseModel): title: str = Field(description="Short task title, max 80 characters") description: str = Field(description="Full description of what needs to be done") priority: Priority = Field(description="Urgency level: low / medium / high / critical") due_date: str | None = Field( default=None, description="Deadline as YYYY-MM-DD, or null if no deadline is mentioned", ) estimated_hours: float | None = Field( default=None, ge=0, description="Estimated effort in hours, or null if unknown", ) subtasks: list[Subtask] = Field( default_factory=list, description="List of smaller steps; empty list if none mentioned", ) tags: list[str] = Field( default_factory=list, description="Short lowercase labels, e.g. ['backend', 'urgent']", ) ``` -------------------------------- ### Few-Shot Examples Pattern for Schema Extraction Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/extraction.md Provide one or two filled-in examples to significantly improve accuracy for unusual or domain-specific schemas. This pattern includes a system message, an example of invoice text, the ideal output (as a JSON string), and the new invoice text to extract from. ```python import json good_example = MySchema(field_a="value", field_b=42) messages = [ SystemMessage(content="Extract invoice data from the text."), HumanMessage(content="Invoice text: Muster GmbH, 100 EUR, 2026-01-15, paid"), AIMessage(content=good_example.model_dump_json()), # show ideal output HumanMessage(content=f"Now extract from this invoice: {new_invoice_text}"), ] ``` -------------------------------- ### JSON Mode Prompt Example Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/extraction-modes.md Illustrates the system message appended to the prompt in JSON mode, instructing the model to output only a strictly conforming JSON object. ```text Respond with a SINGLE JSON object that strictly conforms to the JSON Schema below. Output ONLY the raw JSON object — no markdown code fences, no comments, and no explanatory text before or after it. JSON Schema for 'MySchema': { ... full JSON Schema ... } ``` -------------------------------- ### Quick Start: Extract Person Information Source: https://github.com/mlauf-labs/saidex/blob/develop/README_PYPI.md Demonstrates basic usage of SAIDEX to extract structured data from text using a Pydantic model and a LangChain LLM. It shows how to define a model, initialize an LLM, and call the extraction function. ```python import asyncio from pydantic import BaseModel, Field from langchain_openai import ChatOpenAI from saidex import extract_data_from_text class PersonInfo(BaseModel): name: str age: int occupation: str async def main(): llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) person, stats = await extract_data_from_text( llm, PersonInfo, "Alice Müller, 34, works as a software engineer in Munich.", ) print(person) # name='Alice Müller' age=34 occupation='software engineer' print(stats.total_retries) # 0 — first attempt was valid asyncio.run(main()) ``` -------------------------------- ### Define Tool Parameters with Pydantic Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/agent-loop.md Define the arguments for a tool using a Pydantic `BaseModel`. This example shows a `SearchArgs` model with a `query` and an optional `limit`. ```python class SearchArgs(BaseModel): query: str = Field(description="Search term") limit: int = Field(default=5, ge=1, le=20, description="Max results") ``` -------------------------------- ### Agent Loop with Tool Extraction and Langfuse Tracing Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/langfuse-tracing.md This example demonstrates how to use `extract_data_with_tools` from Saidex with Langfuse's `CallbackHandler` to trace LLM calls and tool executions within an agent loop. It defines a tool for checking order status and uses it to resolve a customer ticket. ```python import asyncio from langchain_openai import ChatOpenAI from langfuse import get_client from langfuse.langchain import CallbackHandler from pydantic import BaseModel, Field from saidex import Tool, extract_data_with_tools ORDERS = {"ORD-1042": {"status": "shipped", "eta": "2026-06-15", "carrier": "DHL"}} class OrderStatusArgs(BaseModel): order_id: str = Field(description="Order id, e.g. ORD-1042") async def order_status_handler(order_id: str) -> dict: return ORDERS.get(order_id, {"error": f"Order '{order_id}' not found"}) order_status_tool = Tool( name="get_order_status", description="Look up the status, ETA, and carrier of an order by its id.", parameters=OrderStatusArgs, handler=order_status_handler, ) class TicketResolution(BaseModel): order_id: str = Field(description="The order this ticket is about") current_status: str = Field(description="Order status as reported by the tool") customer_reply: str = Field(description="Friendly reply for the customer") async def main() -> None: llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) handler = CallbackHandler() ticket = "Where is my order ORD-1042? It's been two weeks!" resolution, stats = await extract_data_with_tools( llm, TicketResolution, ticket, tools=[order_status_tool], callbacks=[handler], ) print(resolution) print(f"iterations={stats.iterations} tool_calls={stats.tool_calls}") get_client().flush() if __name__ == "__main__": asyncio.run(main()) ``` -------------------------------- ### Extract data from an image URL using GPT-4o Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md Demonstrates extracting structured data (Receipt) from a receipt image URL using OpenAI's GPT-4o model. Ensure the `langchain_openai` and `saidex` libraries are installed. ```python import asyncio from langchain_core.messages import HumanMessage, SystemMessage from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from saidex import extract_data class ReceiptItem(BaseModel): description: str total: float class Receipt(BaseModel): vendor: str date: str | None = None items: list[ReceiptItem] total: float currency: str = "EUR" def image_message_from_url(url: str, prompt: str) -> HumanMessage: return HumanMessage(content=[ {"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": url, "detail": "high"}}, ]) async def main(): llm = ChatOpenAI(model="gpt-4o", temperature=0) messages = [ SystemMessage(content="Extract all data from the receipt image precisely."), image_message_from_url( "https://example.com/receipt.jpg", prompt="Extract the receipt data.", ), ] receipt, stats = await extract_data(llm, Receipt, messages) print(receipt) asyncio.run(main()) ``` -------------------------------- ### Multiple Computed Fields in a Product Model Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md This example demonstrates a Product model with multiple computed fields: price_gross, line_total, and tax_amount. These fields are derived from net price, tax rate, and quantity. ```python from pydantic import BaseModel, Field, computed_field class Product(BaseModel): name: str price_net: float = Field(ge=0, description="Net price in EUR") tax_rate: float = Field(ge=0, le=1, description="Tax rate, e.g. 0.19 for 19 %") quantity: int = Field(ge=1, description="Number of units") @computed_field @property def price_gross(self) -> float: """Net price including tax.""" return round(self.price_net * (1 + self.tax_rate), 2) @computed_field @property def line_total(self) -> float: """Total for all units including tax.""" return round(self.price_gross * self.quantity, 2) @computed_field @property def tax_amount(self) -> float: """Tax portion of line_total.""" return round(self.line_total - self.price_net * self.quantity, 2) ``` -------------------------------- ### Extract Data with Callbacks Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/observability.md Use LangChain's BaseCallbackHandler instances to get automatic tracing of extractions. SAIDEX wraps extractions and LLM calls in spans, with tool executions appearing as their own spans. ```python result, stats = await extract_data( llm, MySchema, messages, callbacks=[my_handler], ) ``` -------------------------------- ### Example Error Message for Validation Failure Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/retry-and-fallback.md This is an example of the structured error message generated when Pydantic validation fails, guiding the LLM on how to correct the output. ```text "The output format is not correct. Please correct it based on the following errors: MISSING REQUIRED FIELDS: - 'revenue_usd_millions': required field is missing TYPE ERRORS: - 'fiscal_year': wrong type. Expected: integer, Received: str = '2025' REQUIRED CHANGES: 1. Add all missing required fields. 2. Correct the data types of the specified fields." ``` -------------------------------- ### Pydantic Model with After Field Validators Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md This example demonstrates a Pydantic model with multiple `@field_validator`s set to `mode='after'`. It includes validators for normalizing email to lowercase, validating and normalizing country codes, cleaning phone numbers, ensuring website URLs start with https, and validating IBAN formats. ```python import re from pydantic import BaseModel, Field, field_validator class ContactInfo(BaseModel): email: str | None = None phone: str | None = None country_code: str = Field(description="ISO 3166-1 alpha-2, e.g. DE") website: str | None = None iban: str | None = Field(default=None, description="IBAN or null") @field_validator("email", mode="after") @classmethod def lowercase_email(cls, v: str | None) -> str | None: return v.lower() if v else v @field_validator("country_code", mode="after") @classmethod def validate_country_code(cls, v: str) -> str: v = v.upper().strip() if not re.match(r"^[A-Z]{2}$", v): raise ValueError( f"'{v}' is not a valid ISO 3166-1 alpha-2 country code. " "Expected exactly 2 uppercase letters, e.g. 'DE', 'US', 'FR'." ) return v @field_validator("phone", mode="after") @classmethod def normalise_phone(cls, v: str | None) -> str | None: """Strip formatting — keep only digits and a single leading '+'.""" if not v: return v digits = re.sub(r"[^\\d+]", "", v) return re.sub(r"\\++", "+", digits) or None @field_validator("website", mode="after") @classmethod def ensure_https(cls, v: str | None) -> str | None: if v and not v.startswith(("http://", "https://")): return f"https://{v}" return v @field_validator("iban", mode="after") @classmethod def validate_iban_format(cls, v: str | None) -> str | None: if not v: return v iban = v.replace(" ", "").upper() if not re.match(r"^[A-Z]{2}\\d{2}[A-Z0-9]{1,30}$", iban): raise ValueError( f"'{v}' does not look like a valid IBAN. " "Expected format: DE89 3704 0044 0532 0130 00" ) return iban ``` -------------------------------- ### Build and Publish Project with uv Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md Build the project's distribution packages or publish them to PyPI using `uv build` and `uv publish`. ```bash # Build source distribution + wheel into dist/ uv build # Publish to PyPI (requires PYPI_TOKEN or interactive login) uv publish ``` -------------------------------- ### Compare Products Using Multiple Images Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/images.md Compares products by passing multiple image paths to extract a combined schema. Uses `ChatOpenAI` with `gpt-4o` and requires `SystemMessage` and `multi_image_message` to be defined. ```python async def compare_products(image_paths: list[str]) -> ComparativeAnalysis | None: llm = ChatOpenAI(model="gpt-4o", temperature=0) messages = [ SystemMessage(content="Analyse all images and provide a comparative analysis."), multi_image_message( image_paths, prompt="Compare these products and fill in the comparison schema.", ), ] result, stats = await extract_data(llm, ComparativeAnalysis, messages) return result asyncio.run(compare_products(["product_a.jpg", "product_b.jpg", "product_c.jpg"])) ``` -------------------------------- ### Validate Date Ordering in a Contract Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Ensures that the end date is after the start date and the signed date is not after the start date. This validator runs after all fields are validated. ```python from pydantic import BaseModel, Field, model_validator class Contract(BaseModel): start_date: str = Field(description="Contract start as YYYY-MM-DD") end_date: str = Field(description="Contract end as YYYY-MM-DD") signed_date: str | None = Field(default=None, description="Signing date or null") @model_validator(mode="after") def check_date_order(self) -> "Contract": if self.end_date <= self.start_date: raise ValueError( f"end_date ({self.end_date}) must be after " f"start_date ({self.start_date})." ) if self.signed_date and self.signed_date > self.start_date: raise ValueError( f"signed_date ({self.signed_date}) must not be after " f"start_date ({self.start_date}). Contracts are signed before they start." ) return self ``` -------------------------------- ### Use String for Dates with Format Hints Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md Illustrates the recommended practice of using strings with descriptive format hints instead of Python's `date`, `datetime`, or `time` objects to avoid serialization issues. ```python from datetime import date from pydantic import Field # BAD due: date # ❌ — serialisation surprises # GOOD due: str | None = Field(default=None, description="Due date as YYYY-MM-DD, or null") # ✅ ``` -------------------------------- ### Example Markdown Field Issue Report Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/quality-analytics.md This is an example of the Markdown output generated by `to_markdown()`. It includes a section for each schema, detailing field issues with success rates, error types, and recovery information. ```markdown # Field issue report ## Invoice — 33% success (2/3 runs failed) | Field | Category | Top error | Hits | In failed runs | Recovery | Samples | | --- | --- | --- | ---: | ---: | ---: | --- | | total | type | float_parsing | 2 | 2 | 0% | `1.299,00`, `forty-two` | | currency | enum | enum | 1 | 0 | 100% | `euros` | ``` -------------------------------- ### Correctly Returning Self from Model Validator Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Highlights the requirement for model validators using `mode='after'` to return `self`. The 'WRONG' example shows a validator returning `None`, while the 'RIGHT' example correctly returns the model instance. ```python # WRONG @model_validator(mode="after") def check_dates(self) -> None: # ❌ must return self if self.end < self.start: raise ValueError("end must be after start") # RIGHT @model_validator(mode="after") def check_dates(self) -> "MyModel": # ✅ if self.end < self.start: raise ValueError("end must be after start") return self ``` -------------------------------- ### Daily Development Commands with uv Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md Execute common development tasks such as running tests, linting, and type checking using `uv run`. ```bash uv run pytest # run the full test suite uv run pytest tests/test_utils.py -v # run a single test file uv run ruff check src/ tests/ # lint uv run ruff check src/ tests/ --fix # lint + auto-fix uv run ruff format src/ tests/ # format code uv run mypy src/ # static type checking ``` -------------------------------- ### Validator Error Handling in Pydantic Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Illustrates how to raise specific ValueErrors within Pydantic validators. The 'BAD' example shows a vague error, while the 'GOOD' example provides actionable feedback to the LLM about the expected date format. ```python # BAD — vague, LLM doesn't know what to do raise ValueError("invalid date") # GOOD — tells the LLM exactly what format is expected raise ValueError( f"date '{v}' is not in the required ISO 8601 format. " "Expected YYYY-MM-DD, e.g. '2026-01-31'." ) ``` -------------------------------- ### Testing with `create_instance_safe` Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/observability.md Demonstrate using `create_instance_safe` in tests to verify model instantiation with valid and invalid data, ensuring clear error messages are generated. ```python from saidex import create_instance_safe def test_clean_money_validator() -> None: instance, err = create_instance_safe(Invoice, net="€ 100,00", gross="119.00") assert instance is not None assert instance.net == 100.0 def test_missing_required_field() -> None: instance, err = create_instance_safe(Invoice, net=100.0) assert instance is None assert "gross" in err ``` -------------------------------- ### Providing Vague vs. Specific Error Messages Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Contrasts vague error messages with specific, actionable ones for LLM retries. The 'WRONG' example shows a generic `ValueError`, while the 'RIGHT' example provides detailed information about the invalid value and the expected format. ```python # WRONG — the LLM doesn't know what to do raise ValueError("invalid value") # ❌ # RIGHT — tell the LLM exactly what is wrong and what is expected raise ValueError( f"'{v}' is not a valid ISO 3166-1 alpha-2 country code. " "Expected exactly 2 uppercase letters, e.g. 'DE', 'US', 'FR'." ) # ✅ ``` -------------------------------- ### Using create_instance_safe as a Public API Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Shows the public API usage of `create_instance_safe` for instantiating schemas with raw data, including how to handle and print structured error messages. ```python from saidex import create_instance_safe instance, error_msg = create_instance_safe(MySchema, **raw_dict) if instance is None: print(error_msg) # structured, field-level error description ``` -------------------------------- ### Using 'before' Mode for Cleaning Symbols in Validators Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Explains the correct mode for cleaning symbols in validators. The 'WRONG' example uses `mode='after'`, which fails because the cleaning happens too late. The 'RIGHT' example uses `mode='before'` to clean the raw string before type coercion. ```python # WRONG — by 'after' time, "€ 42" has already failed float coercion @field_validator("price", mode="after") # ❌ @classmethod def clean_price(cls, v: float) -> float: ... # RIGHT — clean the raw string before Pydantic tries to make it a float @field_validator("price", mode="before") # ✅ @classmethod def clean_price(cls, v: Any) -> Any: ... ``` -------------------------------- ### Correctly Handling Empty Strings in Field Validator Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Illustrates the correct way to implement a field validator for a string field that should not be empty. The 'WRONG' example shows how returning `None` for an empty string fails validation, while the 'RIGHT' example demonstrates raising a `ValueError`. ```python # WRONG — will immediately fail the model if v is any falsy value @field_validator("name", mode="before") @classmethod def clean_name(cls, v: Any) -> Any: if isinstance(v, str): return v.strip() or None # ❌ returns None if strip() gives "" return v # RIGHT — raise ValueError so the retry mechanism kicks in @field_validator("name", mode="before") @classmethod def clean_name(cls, v: Any) -> Any: if isinstance(v, str): cleaned = v.strip() if not cleaned: raise ValueError("name must not be empty") # ✅ return cleaned return v ``` -------------------------------- ### Combining JSON Mode with a Fallback Model Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/extraction-modes.md Demonstrates setting up a local model for JSON mode extraction with a more capable cloud model as a fallback, balancing cost and reliability. ```python local = ChatOpenAI(model="llama3.1", base_url="http://localhost:11434/v1", api_key="x", temperature=0) fallback = ChatOpenAI(model="gpt-4o", temperature=0) result, stats = await extract_data( local, MySchema, messages, mode=ExtractionMode.JSON, fallback_llm_model=fallback, max_primary_retries=2, ) ``` -------------------------------- ### Import Tool Dataclass Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/agent-loop.md Import the `Tool` dataclass from the saidex library. ```python from saidex import Tool ``` -------------------------------- ### Example: Strict Order ID Validation Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/built-in-types.md Creates a strict `OrderId` type that validates against a specific pattern 'ORD-' followed by five digits using `AfterValidator`. ```python import re from typing import Annotated from pydantic import AfterValidator _ORDER_ID_RE = re.compile(r"^ORD-\d{5}$") def validate_order_id(value: str) -> str: if not isinstance(value, str) or not _ORDER_ID_RE.match(value): raise ValueError( f"'{value}' is not a valid order ID. Expected 'ORD-' followed by " f"exactly five digits, e.g. 'ORD-04812'." ) return value OrderId = Annotated[str, AfterValidator(validate_order_id)] ``` -------------------------------- ### Configure Primary Validation Retries Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/retry-and-fallback.md Configure the maximum number of correction attempts for primary validation retries. This example sets it to 5, exceeding the default of 3. ```python result, stats = await extract_data( llm, MySchema, messages, max_primary_retries=5, # allow 5 correction attempts (default: 3) ) ``` -------------------------------- ### Using Extraction Modes Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/extraction-modes.md Demonstrates how to select between the default Tool Calling mode and the Raw JSON mode when extracting data. ```python from saidex import ExtractionMode, extract_data_from_text # Tool calling (default) result, stats = await extract_data_from_text(llm, MySchema, text) # Raw JSON — no tool calling required result, stats = await extract_data_from_text( llm, MySchema, text, mode=ExtractionMode.JSON ) ``` -------------------------------- ### Import Core Saidex Functions Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/quickstart.md Import the primary asynchronous functions for data extraction from the Saidex library. ```python from saidex import extract_data_from_text, extract_data ``` -------------------------------- ### Example: Normalizing Priority Validation Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/built-in-types.md Defines a `Priority` type that validates input against a predefined set of priorities and normalizes the input to lowercase and stripped whitespace using `AfterValidator`. ```python from typing import Annotated from pydantic import AfterValidator _PRIORITIES = {"low", "medium", "high", "critical"} def validate_priority(value: str) -> str: norm = str(value).strip().lower() if norm not in _PRIORITIES: allowed = ", ".join(sorted(_PRIORITIES)) raise ValueError(f"'{value}' is not a valid priority. Use one of: {allowed}.") return norm Priority = Annotated[str, AfterValidator(validate_priority)] ``` -------------------------------- ### Create LangChain Client for vLLM Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/images.md Creates a LangChain ChatOpenAI client configured to connect to a local vLLM server. The API key is not required for vLLM. ```python from langchain_openai import ChatOpenAI def create_vllm_vision_client( model: str = "Qwen/Qwen2-VL-7B-Instruct", base_url: str = "http://localhost:8000/v1", temperature: float = 0.0, ) -> ChatOpenAI: """Create a LangChain client pointing at a local vLLM server.""" return ChatOpenAI( model=model, base_url=base_url, api_key="not-needed", # vLLM does not check the API key temperature=temperature, max_tokens=1024, ) ``` -------------------------------- ### Async Validator for Catalogue Check Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/external-validators.md Use an async validator when the check involves I/O operations like database lookups. This example validates an order against a catalogue before extraction. ```python async def validate_against_catalogue(order: Order) -> None: known = await catalogue.exists(order.sku) if not known: raise ValueError(f"Unknown SKU '{order.sku}'. Use a SKU from the catalogue.") ``` ```python order, stats = await extract_data_from_text(llm, Order, text, validator=validate_against_catalogue) ``` -------------------------------- ### Export Langfuse Credentials as Environment Variables Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/langfuse-tracing.md Set your Langfuse project keys and host using environment variables. The Langfuse client automatically picks these up on startup. ```bash export LANGFUSE_PUBLIC_KEY="pk-lf-..." export LANGFUSE_SECRET_KEY="sk-lf-..." export LANGFUSE_HOST="https://cloud.langfuse.com" # or your self-hosted URL ``` -------------------------------- ### Compare Fields with `model_validator` Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Use `mode="after"` with `@model_validator` to enforce validation rules between multiple fields after they have been parsed, such as ensuring an end date is after a start date. ```python start_date: datetime end_date: datetime @model_validator(mode="after") def check_dates(self): if self.end_date < self.start_date: raise ValueError("end_date must be after start_date") return self ``` -------------------------------- ### Manage Project Dependencies with uv Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md Add, remove, or manage optional dependencies for the project using `uv add`, `uv remove`, and `uv lock`. ```bash # Add a runtime dependency (updates pyproject.toml + uv.lock) uv add langchain-openai # Add a dev-only dependency uv add --group dev ipython # Add an optional extra (e.g. openai rate-limit support) uv add --optional openai openai # Remove a dependency uv remove langchain-openai # Upgrade all dependencies to their latest allowed versions uv lock --upgrade uv sync ``` -------------------------------- ### Model-wide Settings with `model_config` Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/schema-design.md Configure default behaviors for all fields in a model using `ConfigDict`. Useful settings include stripping whitespace and ignoring extra fields. ```python from pydantic import BaseModel, ConfigDict class MySchema(BaseModel): model_config = ConfigDict( str_strip_whitespace=True, # trim accidental leading/trailing spaces extra="ignore", # silently drop extra fields the LLM adds ) name: str value: float ``` -------------------------------- ### Run Extractor Agent with Full Message Control Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/agent-loop.md Use `run_extractor_agent` when precise control over the conversation history is needed. This allows for multi-turn interactions, few-shot examples, and pre-gathered context. ```python from langchain_core.messages import HumanMessage, SystemMessage from saidex import run_extractor_agent messages = [ SystemMessage(content="You are the filing assistant for project X."), HumanMessage(content="Earlier decision: contracts go to folder LEGAL-7."), HumanMessage(content="File this document: 'Service agreement with ACME...'"), ] decision, stats = await run_extractor_agent( llm, FilingDecision, messages, tools=[create_folder, list_folders] ) ``` -------------------------------- ### Validate Address from Flat String or Dict Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Demonstrates creating an Address model instance using both a dictionary and a flat string, showcasing the effectiveness of the 'before' validator. ```python # Both are valid after the validator: Address(street="Hauptstraße 1", city="München", zip_code="80331") Address.model_validate("Hauptstraße 1, 80331 München, DE") ``` -------------------------------- ### Handle Extraction Result with None Check Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/extraction.md Always check if the extraction result is None before proceeding. This example demonstrates error logging and raising an exception when extraction fails after retries. ```python result, stats = await extract_data_from_text(llm, MySchema, text) if result is None: # All retries exhausted — log and handle gracefully logger.error( "Extraction failed after %d retries (fallback used: %s)", stats.total_retries, stats.fallback_used, ) raise RuntimeError("Could not extract structured data") # Safe to use here process(result) ``` -------------------------------- ### Detailed Field Descriptions for Clarity Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md Contrasts Pydantic models with vague field definitions against those with explicit, descriptive `Field` arguments. Emphasizes how descriptions guide the LLM's output. ```python # BAD — the LLM has to guess the meaning, unit, and format class Invoice(BaseModel): amount: float # ❌ — which currency? incl. tax? total or subtotal? date: str # ❌ — which format? invoice date or due date? # GOOD — the LLM knows exactly what is expected class Invoice(BaseModel): amount: float = Field( description="Total invoice amount in EUR including VAT, e.g. 119.00" ) date: str = Field( description="Invoice issue date in ISO 8601 format: YYYY-MM-DD" ) ``` -------------------------------- ### Test Invoice Validator with create_instance_safe Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Demonstrates testing the Invoice validator with `create_instance_safe` for both successful and erroneous inputs. The error message returned is shown to be the exact text sent to the LLM for retries. ```python from saidex import create_instance_safe # Happy path invoice, err = create_instance_safe( Invoice, net="€ 100,00", gross="119.00", date="05.01.2026", paid="ja", ) assert invoice is not None assert invoice.net == 100.0 assert invoice.date == "2026-01-05" assert invoice.paid is True # Error path — check the message the LLM would receive invoice, err = create_instance_safe( Invoice, net=-5.0, # violates ge=0 gross=0.0, date="2026-01-01", paid=True, ) assert invoice is None assert "net" in err # error mentions the field assert "≥ 0" in err or "ge" in err.lower() ``` -------------------------------- ### Structlog Integration for Structured Logging Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/observability.md Integrate structlog with Python's logging module to enable JSON-based log aggregation. This example configures structlog to filter logs at INFO level and above. ```python import logging import structlog structlog.configure( wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), ) log = structlog.get_logger("saidex") ``` -------------------------------- ### Using and Inspecting Computed Fields Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/validators.md Instantiate a Product model and access its computed fields. The model_dump() method includes all computed fields. ```python p = Product(name="Widget", price_net=100.0, tax_rate=0.19, quantity=3) p.price_gross # 119.0 p.line_total # 357.0 p.tax_amount # 57.0 p.model_dump() # includes all computed fields ``` -------------------------------- ### Default RetryConfig Settings Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/retry-and-fallback.md Illustrates the default configuration for network retries, including maximum retries, exponential back-off delays, and common retryable exceptions for httpx and openai. ```python RetryConfig( max_retries=3, retry_delays=[1.0, 2.0, 4.0], # exponential back-off retryable_exceptions=( httpx.ConnectError, httpx.TimeoutException, openai.APIConnectionError, openai.APITimeoutError, ), rate_limit_exceptions=(openai.RateLimitError,), rate_limit_retry_interval=10.0, rate_limit_max_duration_seconds=900.0, ) ``` -------------------------------- ### Message List Immutability Example Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/extraction.md Demonstrates that the `extract_data` function does not mutate the input message list. The original `base_messages` list remains unchanged after multiple calls to `extract_data` with different schemas. ```python base_messages = [SystemMessage(content="Analyse this."), HumanMessage(content=doc)] result_a, _ = await extract_data(llm, SchemaA, base_messages) result_b, _ = await extract_data(llm, SchemaB, base_messages) # base_messages is unchanged after both calls ``` -------------------------------- ### Locale-Aware Grounding Configuration Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/source-grounding.md Shows how to configure locale-aware grounding using dynamic locale fields or static locale hints in Pydantic models. ```python class Invoice(BaseModel): country: str # e.g. "DE" total: float = GroundedField(locale_field="country") # dynamic: from a sibling tax: float = GroundedField(locale="de") # static: fixed hint ``` -------------------------------- ### Invoice Validation Example Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/external-validators.md Demonstrates defining a Pydantic model and an external validator function to check if the sum of line item amounts matches the stated total. This validator is then passed to `extract_data_from_text`. ```python from pydantic import BaseModel from saidex import extract_data_from_text class Invoice(BaseModel): vendor: str lines: list[InvoiceLine] total: float def validate_invoice(inv: Invoice) -> None: line_sum = round(sum(line.amount for line in inv.lines), 2) if line_sum != round(inv.total, 2): raise ValueError( f"Line items sum to {line_sum} but the stated total is {inv.total}. " f"Make the total match the sum of the line items." ) invoice, stats = await extract_data_from_text( llm, Invoice, document, validator=validate_invoice, # sync or async callable ) ``` -------------------------------- ### Standard Async Usage with Saidex Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/observability.md Demonstrates the typical asynchronous usage pattern for Saidex functions like `extract_data_from_text`. Ensure you `await` the coroutine and run it within an asyncio event loop. ```python import asyncio from saidex import extract_data_from_text async def main() -> None: result, stats = await extract_data_from_text(llm, MySchema, text) ... asyncio.run(main()) ``` -------------------------------- ### Extract List of Models from Text Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/batch-extraction.md Define the schema for a single item and use `extract_data_list_from_text` to get a list of models. This function is useful when a document contains multiple instances of the same structured data. ```python from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from saidex import extract_data_list_from_text class InvoiceLine(BaseModel): description: str = Field(description="What was purchased") quantity: int = Field(description="Number of units", ge=1) unit_price: float = Field(description="Price per unit", ge=0) lines, stats = await extract_data_list_from_text(llm, InvoiceLine, document) if lines is not None: print(f"Extracted {stats.item_count} line items") for line in lines: print(line.description, line.quantity, line.unit_price) ``` -------------------------------- ### Synchronous Wrapper for Saidex Functions Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/observability.md Use these synchronous wrappers when operating in a purely synchronous context, such as a CLI script or a Django view. They handle the event loop internally, so no explicit `asyncio` management is needed. ```python from saidex import extract_data_from_text_sync # No async/await, no asyncio.run — just call it. result, stats = extract_data_from_text_sync(llm, MySchema, text) ``` ```python from saidex import extract_data_with_tools_sync # The agent loop has a sync wrapper too: result, stats = extract_data_with_tools_sync(llm, MySchema, text, tools=[my_tool]) ``` -------------------------------- ### Retry vs. Advisory Grounding Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/source-grounding.md Demonstrates the use of default retry behavior and the 'on_mismatch="flag"' option for advisory grounding in Pydantic models. ```python class Invoice(BaseModel): vendor: Annotated[str, Grounded()] # "retry" (default) summary: str = GroundedField(on_mismatch="flag") # advisory only ``` -------------------------------- ### Test Custom Order ID Validator Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/built-in-types.md Test custom validators directly using pytest. This example shows how to assert that a specific validation error message is raised for an invalid order ID. ```python import pytest from saidex import create_instance_safe def test_order_id_rejects_bad_value(): with pytest.raises(ValueError, match="valid order ID"): validate_order_id("12345") def test_schema_reports_field_path(): instance, error = create_instance_safe(MyOrder, order_id="12345") assert instance is None assert "order_id" in error # this text is sent back to the model ``` -------------------------------- ### Example: Lenient Country Code Validation with Reshaping Source: https://github.com/mlauf-labs/saidex/blob/develop/docs/built-in-types.md Creates a `LenientCountryCode` type that first reshapes country names to codes using `BeforeValidator` and then validates the code using the built-in `validate_country_code` with `AfterValidator`. ```python from typing import Annotated, Any from pydantic import BeforeValidator, AfterValidator from saidex import validate_country_code _NAME_TO_CODE = {"germany": "DE", "deutschland": "DE", "united kingdom": "GB"} def name_to_code(value: Any) -> Any: if isinstance(value, str): return _NAME_TO_CODE.get(value.strip().lower(), value) return value # Accepts "Germany", "DE", or "de"; always ends up validated and normalised to "DE". LenientCountryCode = Annotated[str, BeforeValidator(name_to_code), AfterValidator(validate_country_code)] ``` -------------------------------- ### Define and Use a Tool in an Agentic Loop Source: https://github.com/mlauf-labs/saidex/blob/develop/README.md This snippet demonstrates how to define a tool with Pydantic arguments and an async handler, then use it within the `extract_data_with_tools` function to resolve a customer query. The final answer is validated against a Pydantic schema. ```python from pydantic import BaseModel, Field from saidex import Tool, extract_data_with_tools # 1. Describe the tool's arguments with a Pydantic model class OrderStatusArgs(BaseModel): order_id: str = Field(description="Order id, e.g. ORD-1042") # 2. Provide an async handler — its result is shown to the LLM async def order_status_handler(order_id: str) -> dict: return {"order_id": order_id, "status": "shipped", "eta": "2026-06-15"} order_status = Tool( name="get_order_status", description="Look up the current status and ETA of an order.", parameters=OrderStatusArgs, handler=order_status_handler, ) # 3. Define the final-answer schema and run the loop class TicketResolution(BaseModel): order_id: str current_status: str customer_reply: str = Field(description="Friendly reply for the customer") resolution, stats = await extract_data_with_tools( llm, TicketResolution, "Customer asks: where is my order ORD-1042?", tools=[order_status], ) print(resolution.customer_reply) print(stats.iterations, stats.tool_calls) # e.g. 2 1 ```