diff --git a/.blackboxrules b/.blackboxrules
deleted file mode 100644
index 6125caa..0000000
--- a/.blackboxrules
+++ /dev/null
@@ -1,1162 +0,0 @@
-
-# Pipelex Coding Rules
-
-## Guide to write or edit pipelines using the Pipelex language in .plx files
-
-- Always first write your "plan" in natural language, then transcribe it in pipelex.
-- You should ALWAYS RUN validation when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
- - For a specific file: `pipelex validate path_to_file.plx`
- - For all pipelines: `pipelex validate all`
- - **IMPORTANT**: Ensure the Python virtual environment is activated before running `pipelex` commands. For standard installations, the venv is named `.venv` - always check that first. The commands will not work without proper venv activation.
-- Please use POSIX standard for files. (empty lines, no trailing whitespaces, etc.)
-
-### Pipeline File Naming
-- Files must be `.plx` for pipelines (Always add an empty line at the end of the file, and do not add trailing whitespaces to PLX files at all)
-- Files must be `.py` for code defining the data structures
-- Use descriptive names in `snake_case`
-
-### Pipeline File Outline
-A pipeline file has three main sections:
-1. Domain statement
-2. Concept definitions
-3. Pipe definitions
-
-#### Domain Statement
-```plx
-domain = "domain_name"
-description = "Description of the domain" # Optional
-```
-Note: The domain name usually matches the plx filename for single-file domains. For multi-file domains, use the subdirectory name.
-
-#### Concept Definitions
-
-Concepts represent ideas and semantic entities in your pipeline. They define what something *is*, not how it's structured.
-
-```plx
-[concept]
-ConceptName = "Description of the concept"
-```
-
-**Naming Rules:**
-- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story") - lists are handled implicitly by Pipelex
-- Avoid circumstantial adjectives (no "LargeText", use "Text") - focus on the essence of what the concept represents
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number, Page, JSON)
-
-**Native Concepts:**
-Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`, `JSON`. Use these directly or refine them when appropriate.
-
-**Refining Native Concepts:**
-To create a concept that specializes a native concept without adding fields:
-
-```plx
-[concept.Landscape]
-description = "A scenic outdoor photograph"
-refines = "Image"
-```
-
-For details on how to structure concepts with fields, see the "Structuring Models" section below.
-
-#### Pipe Definitions
-
-### Pipe Base Definition
-
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-description = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
-
-The pipes will all have at least this base definition.
-- `inputs`: Dictionary of key being the variable used in the prompts, and the value being the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if PipeSequence) or of the conditional pipes (if PipeCondition).
-So If you have this error:
-`PipeValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['invoice']'``
-That means that the pipe validate_expense is missing the input `invoice` because one of the subpipe is needing it.
-
-NEVER WRITE THE INPUTS BY BREAKING THE LINE LIKE THIS:
-
-```plx
-inputs = {
- input_1 = "ConceptName1",
- input_2 = "ConceptName2"
-}
-```
-
-
-- `output`: The name of the concept to output. The `ConceptName` should have the same name as the python class if you want structured output:
-
-#### Input Multiplicity
-
-By default, inputs expect a single item. Use bracket notation to specify multiple items:
-
-```plx
-## Single item (default)
-inputs = { document = "Text" }
-
-## Variable list - indeterminate number of items
-inputs = { documents = "Text[]" }
-
-## Fixed count - exactly N items
-inputs = { comparison_items = "Image[2]" }
-```
-
-**Key points:**
-- No brackets = single item (default behavior)
-- Use `[]` for lists of unknown length
-- Use `[N]` (where N is an integer) when operation requires exact count (e.g., comparing 2 items)
-
-### Structuring Models
-
-Once you've defined your concepts semantically (see "Concept Definitions" above), you need to specify their structure if they have fields.
-
-#### Three Ways to Structure Concepts
-
-**1. No Structure Needed**
-
-If a concept only refines a native concept without adding fields, use the TOML table syntax shown in "Concept Definitions" above. No structure section is needed.
-
-**2. Inline Structure Definition (RECOMMENDED for most cases)**
-
-For concepts with structured fields, define them inline using TOML syntax:
-
-```plx
-[concept.Invoice]
-description = "A commercial document issued by a seller to a buyer"
-
-[concept.Invoice.structure]
-invoice_number = "The unique invoice identifier" # This will be optional by default
-issue_date = { type = "date", description = "The date the invoice was issued", required = true }
-total_amount = { type = "number", description = "The total invoice amount", required = true }
-vendor_name = "The name of the vendor" # This will be optional by default
-line_items = { type = "list", item_type = "text", description = "List of items" }
-```
-
-**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
-
-**Field properties:** `type`, `description`, `required` (default: false), `default_value`, `choices`, `item_type` (for lists), `key_type` and `value_type` (for dicts)
-
-**Simple syntax** (creates required text field):
-```plx
-field_name = "Field description"
-```
-
-**Detailed syntax** (with explicit properties):
-```plx
-field_name = { type = "text", description = "Field description", default_value = "default" }
-```
-
-**3. Python StructuredContent Class (For Advanced Features)**
-
-Create a Python class when you need:
-- Custom validation logic (@field_validator, @model_validator)
-- Computed properties (@property methods)
-- Custom methods or class methods
-- Complex cross-field validation
-- Reusable structures across multiple domains
-
-```python
-from pipelex.core.stuffs.structured_content import StructuredContent
-from pydantic import Field, field_validator
-
-class Invoice(StructuredContent):
- """A commercial invoice with validation."""
-
- invoice_number: str = Field(description="The unique invoice identifier")
- total_amount: float = Field(ge=0, description="The total invoice amount")
- tax_amount: float = Field(ge=0, description="Tax amount")
-
- @field_validator('tax_amount')
- @classmethod
- def validate_tax(cls, v, info):
- """Ensure tax doesn't exceed total."""
- total = info.data.get('total_amount', 0)
- if v > total:
- raise ValueError('Tax amount cannot exceed total amount')
- return v
-```
-
-**Location:** Create models in `my_project/some_domain/some_domain_struct.py`. Classes inheriting from `StructuredContent` are automatically discovered.
-
-#### Decision Rules for Agents
-
-**If concept already exists:**
-- If it's already inline → KEEP IT INLINE unless user explicitly asks to convert or features require Python class
-- If it's already a Python class → KEEP IT as Python class
-
-**If creating new concept:**
-1. Does it only refine a native concept without adding fields? → Use concept-only declaration
-2. Does it need custom validation, computed properties, or methods? → Use Python class
-3. Otherwise → Use inline structure (fastest and simplest)
-
-**When to suggest conversion to Python class:**
-- User needs validation logic beyond type checking
-- User needs computed properties or custom methods
-- Structure needs to be reused across multiple domains
-- Complex type relationships or inheritance required
-
-#### Inline Structure Limitations
-
-Inline structures:
-- ✅ Support all common field types (text, number, date, list, dict, etc.)
-- ✅ Support required/optional fields, defaults, choices
-- ✅ Generate full Pydantic models with validation
-- ❌ Cannot have custom validators or complex validation logic
-- ❌ Cannot have computed properties or custom methods
-- ❌ Cannot refine custom (non-native) concepts
-- ❌ Limited IDE autocomplete compared to explicit Python classes
-
-
-### Pipe Controllers and Pipe Operators
-
-Look at the Pipes we have in order to adapt it. Pipes are organized in two categories:
-
-1. **Controllers** - For flow control:
- - `PipeSequence` - For creating a sequence of multiple steps
- - `PipeCondition` - If the next pipe depends of the expression of a stuff in the working memory
- - `PipeParallel` - For parallelizing pipes
-
-2. **Operators** - For specific tasks:
- - `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeExtract` - Extract text and images from an image or a PDF
- - `PipeCompose` - For composing text using Jinja2 templates: supports html, markdown, mermaid, etc.
- - `PipeImgGen` - Generate Images
- - `PipeFunc` - For running classic python scripts
-
-### PipeSequence controller
-
-Purpose: PipeSequence executes multiple pipes in a defined order, where each step can use results from original inputs or from previous steps.
-
-#### Basic Definition
-```plx
-[pipe.your_sequence_name]
-type = "PipeSequence"
-description = "Description of what this sequence does"
-inputs = { input_name = "InputType" } # All the inputs of the sub pipes, except the ones generated by intermediate steps
-output = "OutputType"
-steps = [
- { pipe = "first_pipe", result = "first_result" },
- { pipe = "second_pipe", result = "second_result" },
- { pipe = "final_pipe", result = "final_result" }
-]
-```
-
-#### Key Components
-
-1. **Steps Array**: List of pipes to execute in sequence
- - `pipe`: Name of the pipe to execute
- - `result`: Name to assign to the pipe's output that will be in the working memory
-
-#### Using PipeBatch in Steps
-
-You can use PipeBatch functionality within steps using `batch_over` and `batch_as`:
-
-```plx
-steps = [
- { pipe = "process_items", batch_over = "input_list", batch_as = "current_item", result = "processed_items"
- }
-]
-```
-
-1. **batch_over**: Specifies a `ListContent` field to iterate over. Each item in the list will be processed individually and IN PARALLEL by the pipe.
- - Must be a `ListContent` type containing the items to process
- - Can reference inputs or results from previous steps
-
-2. **batch_as**: Defines the name that will be used to reference the current item being processed
- - This name can be used in the pipe's input mappings
- - Makes each item from the batch available as a single element
-
-The result of a batched step will be a `ListContent` containing the outputs from processing each item.
-
-### PipeCondition controller
-
-The PipeCondition controller allows you to implement conditional logic in your pipeline, choosing which pipe to execute based on an evaluated expression. It supports both direct expressions and expression templates.
-
-#### Basic usage
-
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression = "input_data.category"
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-or
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression_template = "{{ input_data.category }}" # Jinja2 code
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-
-#### Key Parameters
-
-- `expression`: Direct boolean or string expression (mutually exclusive with expression_template)
-- `expression_template`: Jinja2 template for more complex conditional logic (mutually exclusive with expression)
-- `outcomes`: Dictionary mapping expression results to pipe codes:
- 1. The key on the left (`small`, `medium`) is the result of `expression` or `expression_template`
- 2. The value on the right (`process_small`, `process_medium`, etc.) is the name of the pipe to trigger
-- `default_outcome`: **Required** - The pipe to execute if the expression doesn't match any key in outcomes. Use `"fail"` if you want the pipeline to fail when no match is found
-
-Example with fail as default:
-```plx
-[pipe.strict_validation]
-type = "PipeCondition"
-description = "Validate with strict matching"
-inputs = { status = "Status" }
-output = "Text"
-expression = "status.value"
-default_outcome = "fail"
-
-[pipe.strict_validation.outcomes]
-approved = "process_approved"
-rejected = "process_rejected"
-```
-
-### PipeLLM operator
-
-PipeLLM is used to:
-1. Generate text or objects with LLMs
-2. Process images with Vision LLMs
-
-#### Basic Usage
-
-Simple Text Generation:
-```plx
-[pipe.write_story]
-type = "PipeLLM"
-description = "Write a short story"
-output = "Text"
-prompt = """
-Write a short story about a programmer.
-"""
-```
-
-Structured Data Extraction:
-```plx
-[pipe.extract_info]
-type = "PipeLLM"
-description = "Extract information"
-inputs = { text = "Text" }
-output = "PersonInfo"
-prompt = """
-Extract person information from this text:
-@text
-"""
-```
-
-Supports system instructions:
-```plx
-[pipe.expert_analysis]
-type = "PipeLLM"
-description = "Expert analysis"
-output = "Analysis"
-system_prompt = "You are a data analysis expert"
-prompt = "Analyze this data"
-```
-
-#### Multiple Outputs
-
-Generate multiple outputs (fixed number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[3]" # Generate exactly 3 ideas
-prompt = "Generate 3 ideas"
-```
-
-Generate multiple outputs (variable number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[]" # Let the LLM decide how many to generate
-prompt = "Generate ideas"
-```
-
-#### Vision
-
-Process images with VLMs (image inputs must be tagged in the prompt):
-```plx
-[pipe.analyze_image]
-type = "PipeLLM"
-description = "Analyze image"
-inputs = { image = "Image" }
-output = "ImageAnalysis"
-prompt = """
-Describe what you see in this image:
-
-$image
-"""
-```
-
-You can also reference images inline in meaningful sentences to guide the Visual LLM:
-```plx
-[pipe.compare_images]
-type = "PipeLLM"
-description = "Compare two images"
-inputs = { photo = "Image", painting = "Image" }
-output = "Analysis"
-prompt = "Analyze the colors in $photo and the shapes in $painting."
-```
-
-#### Writing prompts for PipeLLM
-
-**Insert stuff inside a tagged block**
-
-If the inserted text is supposedly a long text, made of several lines or paragraphs, you want it inserted inside a block, possibly a block tagged and delimlited with proper syntax as one would do in a markdown documentation. To include stuff as a block, use the "@" prefix.
-
-Example template:
-```plx
-prompt = """
-Match the expense with its corresponding invoice:
-
-@expense
-
-@invoices
-"""
-```
-In the example above, the expense data and the invoices data are obviously made of several lines each, that's why it makes sense to use the "@" prefix in order to have them delimited inside a block. Note that our preprocessor will automatically include the block's title, so it doesn't need to be explicitly written in the prompt.
-
-DO NOT write things like "Here is the expense: @expense".
-DO write simply "@expense" alone in an isolated line.
-
-**Insert stuff inline**
-
-If the inserted text is short text and it makes sense to have it inserted directly into a sentence, you want it inserted inline. To insert stuff inline, use the "$" prefix. This will insert the stuff without delimiters and the content will be rendered as plain text.
-
-Example template:
-```plx
-prompt = """
-Your goal is to summarize everything related to $topic in the provided text:
-
-@text
-
-Please provide only the summary, with no additional text or explanations.
-Your summary should not be longer than 2 sentences.
-"""
-```
-
-In the example above, $topic will be inserted inline, whereas @text will be a a delimited block.
-Be sure to make the proper choice of prefix for each insertion.
-
-DO NOT write "$topic" alone in an isolated line.
-DO write things like "Write an essay about $topic" to include text into an actual sentence.
-
-
-### PipeExtract operator
-
-The PipeExtract operator is used to extract text and images from an image or a PDF
-
-#### Simple Text Extraction
-```plx
-[pipe.extract_info]
-type = "PipeExtract"
-description = "extract the information"
-inputs = { document = "PDF" } # or { image = "Image" } if it's an image. This is the only input.
-output = "Page"
-```
-
-Using Extract Model Settings:
-```plx
-[pipe.extract_with_model]
-type = "PipeExtract"
-description = "Extract with specific model"
-inputs = { document = "PDF" }
-output = "Page"
-model = "base_extract_mistral" # Use predefined extract preset or model alias
-```
-
-Only one input is allowed and it must either be an `Image` or a `PDF`. The input can be named anything.
-
-The output concept `Page` is a native concept, with the structure `PageContent`:
-It corresponds to 1 page. Therefore, the PipeExtract is outputing a `ListContent` of `Page`
-
-```python
-class TextAndImagesContent(StuffContent):
- text: TextContent | None
- images: list[ImageContent] | None
-
-class PageContent(StructuredContent): # CONCEPT IS "Page"
- text_and_images: TextAndImagesContent
- page_view: ImageContent | None = None
-```
-- `text_and_images` are the text, and the related images found in the input image or PDF.
-- `page_view` is the screenshot of the whole pdf page/image.
-
-### PipeCompose operator
-
-The PipeCompose operator is used to compose text using Jinja2 templates. It supports various output formats including HTML, Markdown, Mermaid diagrams, and more.
-
-#### Basic Usage
-
-Simple Template Composition:
-```plx
-[pipe.compose_report]
-type = "PipeCompose"
-description = "Compose a report using template"
-inputs = { data = "ReportData" }
-output = "Text"
-template = """
-## Report Summary
-
-Based on the analysis:
-$data
-
-Generated on: {{ current_date }}
-"""
-```
-
-Using Named Templates:
-```plx
-[pipe.use_template]
-type = "PipeCompose"
-description = "Use a predefined template"
-inputs = { content = "Text" }
-output = "Text"
-template_name = "standard_report_template"
-```
-
-Using Nested Template Section (for more control):
-```plx
-[pipe.advanced_template]
-type = "PipeCompose"
-description = "Use advanced template settings"
-inputs = { data = "ReportData" }
-output = "Text"
-
-[pipe.advanced_template.template]
-template = "Report: $data"
-category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-```
-
-CRM Email Template:
-```plx
-[pipe.compose_follow_up_email]
-type = "PipeCompose"
-description = "Compose a personalized follow-up email for CRM"
-inputs = { customer = "Customer", deal = "Deal", sales_rep = "SalesRep" }
-output = "Text"
-template_category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-template = """
-Subject: Following up on our $deal.product_name discussion
-
-Hi $customer.first_name,
-
-I hope this email finds you well! I wanted to follow up on our conversation about $deal.product_name from $deal.last_contact_date.
-
-Based on our discussion, I understand that your key requirements are: $deal.customer_requirements
-
-I'm excited to let you know that we can definitely help you achieve your goals. Here's what I'd like to propose:
-
-**Next Steps:**
-- Schedule a demo tailored to your specific needs
-- Provide you with a customized quote based on your requirements
-- Connect you with our implementation team
-
-Would you be available for a 30-minute call this week? I have openings on:
-{% for slot in available_slots %}
-- {{ slot }}
-{% endfor %}
-
-Looking forward to moving this forward together!
-
-Best regards,
-$sales_rep.name
-$sales_rep.title
-$sales_rep.phone | $sales_rep.email
-"""
-```
-
-#### Key Parameters
-
-- `template`: Inline template string (mutually exclusive with template_name)
-- `template_name`: Name of a predefined template (mutually exclusive with template)
-- `template_category`: Template type ("llm_prompt", "html", "markdown", "mermaid", etc.)
-- `templating_style`: Styling options for template rendering
-- `extra_context`: Additional context variables for template
-
-For more control, you can use a nested `template` section instead of the `template` field:
-- `template.template`: The template string
-- `template.category`: Template type
-- `template.templating_style`: Styling options
-
-#### Template Variables
-
-Use the same variable insertion rules as PipeLLM:
-- `@variable` for block insertion (multi-line content)
-- `$variable` for inline insertion (short text)
-
-### PipeImgGen operator
-
-The PipeImgGen operator is used to generate images using AI image generation models.
-
-#### Basic Usage
-
-Simple Image Generation:
-```plx
-[pipe.generate_image]
-type = "PipeImgGen"
-description = "Generate an image from prompt"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-```
-
-Using Image Generation Settings:
-```plx
-[pipe.generate_photo]
-type = "PipeImgGen"
-description = "Generate a high-quality photo"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Photo"
-model = { model = "fast-img-gen" }
-aspect_ratio = "16:9"
-quality = "hd"
-```
-
-Multiple Image Generation:
-```plx
-[pipe.generate_variations]
-type = "PipeImgGen"
-description = "Generate multiple image variations"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image[3]"
-seed = "auto"
-```
-
-Advanced Configuration:
-```plx
-[pipe.generate_custom]
-type = "PipeImgGen"
-description = "Generate image with custom settings"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-model = "img_gen_preset_name" # Use predefined preset
-aspect_ratio = "1:1"
-quality = "hd"
-background = "transparent"
-output_format = "png"
-is_raw = false
-safety_tolerance = 3
-```
-
-#### Key Parameters
-
-**Image Generation Settings:**
-- `model`: Model choice (preset name or inline settings with model name)
-- `quality`: Image quality ("standard", "hd")
-
-**Output Configuration:**
-- `aspect_ratio`: Image dimensions ("1:1", "16:9", "9:16", etc.)
-- `output_format`: File format ("png", "jpeg", "webp")
-- `background`: Background type ("default", "transparent")
-
-**Generation Control:**
-- `seed`: Random seed (integer or "auto")
-- `is_raw`: Whether to apply post-processing
-- `is_moderated`: Enable content moderation
-- `safety_tolerance`: Content safety level (1-6)
-
-#### Input Requirements
-
-PipeImgGen requires exactly one input that must be either:
-- An `ImgGenPrompt` concept
-- A concept that refines `ImgGenPrompt`
-
-The input can be named anything but must contain the prompt text for image generation.
-
-### PipeFunc operator
-
-The PipeFunc operator is used to run custom Python functions within a pipeline. This allows integration of classic Python scripts and custom logic.
-
-#### Basic Usage
-
-Simple Function Call:
-```plx
-[pipe.process_data]
-type = "PipeFunc"
-description = "Process data using custom function"
-inputs = { input_data = "DataType" }
-output = "ProcessedData"
-function_name = "process_data_function"
-```
-
-File Processing Example:
-```plx
-[pipe.read_file]
-type = "PipeFunc"
-description = "Read file content"
-inputs = { file_path = "FilePath" }
-output = "FileContent"
-function_name = "read_file_content"
-```
-
-#### Key Parameters
-
-- `function_name`: Name of the Python function to call (must be registered in func_registry)
-
-#### Function Requirements
-
-The Python function must:
-
-1. **Be registered** in the `func_registry`
-2. **Accept `working_memory`** as a parameter:
- ```python
- async def my_function(working_memory: WorkingMemory) -> StuffContent | list[StuffContent] | str:
- # Function implementation
- pass
- ```
-
-3. **Return appropriate types**:
- - `StuffContent`: Single content object
- - `list[StuffContent]`: Multiple content objects (becomes ListContent)
- - `str`: Simple string (becomes TextContent)
-
-#### Function Registration
-
-Functions must be registered in the function registry before use:
-
-```python
-from pipelex.system.registries.func_registry import func_registry
-
-@func_registry.register("my_function_name")
-async def my_custom_function(working_memory: WorkingMemory) -> StuffContent:
- # Access inputs from working memory
- input_data = working_memory.get_stuff("input_name")
-
- # Process data
- result = process_logic(input_data.content)
-
- # Return result
- return MyResultContent(data=result)
-```
-
-#### Working Memory Access
-
-Inside the function, access pipeline inputs through working memory:
-
-```python
-async def process_function(working_memory: WorkingMemory) -> TextContent:
- # Get input stuff by name
- input_stuff = working_memory.get_stuff("input_name")
-
- # Access the content
- input_content = input_stuff.content
-
- # Process and return
- processed_text = f"Processed: {input_content.text}"
- return TextContent(text=processed_text)
-```
-
----
-
-### Rules to choose LLM models used in PipeLLMs.
-
-#### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-#### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-#### Using an LLM Handle in a PipeLLM
-
-Here is an example of using a model to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-#### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
----
-
-ALWAYS RUN validation when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-- For a specific bundle/file: `pipelex validate path_to_file.plx`
-- For all pipelines: `pipelex validate all`
-- Remember: Ensure your Python virtual environment is activated (typically `.venv` for standard installations) before running `pipelex` commands.
-
-Then, create an example file to run the pipeline in the `examples` folder.
-But don't write documentation unless asked explicitly to.
-
-## Guide to execute a pipeline and write example code
-
-### Example to execute a pipeline with text output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-
-
-async def hello_world() -> str:
- """
- This function demonstrates the use of a super simple Pipelex pipeline to generate text.
- """
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="hello_world",
- )
-
- return pipe_output.main_stuff_as_str
-
-
-## start Pipelex
-Pipelex.make()
-## run sample using asyncio
-output_text = asyncio.run(hello_world())
-pretty_print(output_text, title="Your first Pipelex output")
-```
-
-### Example to execute a pipeline with structured output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-from pipelex.core.stuffs.image_content import ImageContent
-
-from my_project.gantt.gantt_struct import GanttChart
-
-SAMPLE_NAME = "extract_gantt"
-IMAGE_URL = "assets/gantt/gantt_tree_house.png"
-
-
-async def extract_gantt(image_url: str) -> GanttChart:
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
- # Output the result
- return pipe_output.main_stuff_as(content_type=GanttChart)
-
-
-## start Pipelex
-Pipelex.make()
-
-## run sample using asyncio
-gantt_chart = asyncio.run(extract_gantt(image_url=IMAGE_URL))
-pretty_print(gantt_chart, title="Gantt Chart")
-```
-
-### Setting up the input memory
-
-#### Explanation of input memory
-
-The input memory is a dictionary, where the key is the name of the input variable and the value provides details to make it a stuff object. The relevant definitions are:
-```python
-StuffContentOrData = dict[str, Any] | StuffContent | list[Any] | str
-PipelineInputs = dict[str, StuffContentOrData]
-```
-As you can seen, we made it so different ways can be used to define that stuff using structured content or data.
-
-#### Different ways to set up the input memory
-
-So here are a few concrete examples of calls to execute_pipeline with various ways to set up the input memory:
-
-```python
-## Here we have a single input and it's a Text.
-## If you assign a string, by default it will be considered as a TextContent.
- pipe_output = await execute_pipeline(
- pipe_code="master_advisory_orchestrator",
- inputs={
- "user_input": problem_description,
- },
- )
-
-## Here we have a single input and it's a PDF.
-## Because PDFContent is a native concept, we can use it directly as a value,
-## the system knows what content it corresponds to:
- pipe_output = await execute_pipeline(
- pipe_code="power_extractor_dpe",
- inputs={
- "document": PDFContent(url=pdf_url),
- },
- )
-
-## Here we have a single input and it's an Image.
-## Because ImageContent is a native concept, we can use it directly as a value:
- pipe_output = await execute_pipeline(
- pipe_code="fashion_variation_pipeline",
- inputs={
- "fashion_photo": ImageContent(url=image_url),
- },
- )
-
-## Here we have a single input, it's an image but
-## its actually a more specific concept gantt.GanttImage which refines Image,
-## so we must provide it using a dict with the concept and the content:
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
-
-## Here is a more complex example with multiple inputs assigned using different ways:
- pipe_output = await execute_pipeline(
- pipe_code="retrieve_then_answer",
- dynamic_output_concept_code="contracts.Fees",
- inputs={
- "text": load_text_from_path(path=text_path),
- "question": {
- "concept": "answer.Question",
- "content": question,
- },
- "client_instructions": client_instructions,
- },
- )
-```
-
-### Using the outputs of a pipeline
-
-All pipe executions return a `PipeOutput` object.
-It's a BaseModel which contains the resulting working memory at the end of the execution and the pipeline run id.
-It also provides a bunch of accessor functions and properties to unwrap the main stuff, which is the last stuff added to the working memory:
-
-```python
-
-class PipeOutput(BaseModel):
- working_memory: WorkingMemory = Field(default_factory=WorkingMemory)
- pipeline_run_id: str = Field(default=SpecialPipelineId.UNTITLED)
-
- @property
- def main_stuff(self) -> Stuff:
- ...
-
- def main_stuff_as_list(self, item_type: type[StuffContentType]) -> ListContent[StuffContentType]:
- ...
-
- def main_stuff_as_items(self, item_type: type[StuffContentType]) -> list[StuffContentType]:
- ...
-
- def main_stuff_as(self, content_type: type[StuffContentType]) -> StuffContentType:
- ...
-
- @property
- def main_stuff_as_text(self) -> TextContent:
- ...
-
- @property
- def main_stuff_as_str(self) -> str:
- ...
-
- @property
- def main_stuff_as_image(self) -> ImageContent:
- ...
-
- @property
- def main_stuff_as_text_and_image(self) -> TextAndImagesContent:
- ...
-
- @property
- def main_stuff_as_number(self) -> NumberContent:
- ...
-
- @property
- def main_stuff_as_html(self) -> HtmlContent:
- ...
-
- @property
- def main_stuff_as_mermaid(self) -> MermaidContent:
- ...
-```
-
-As you can see, you can extract any variable from the output working memory.
-
-#### Getting the main stuff as a specific type
-
-Simple text as a string:
-
-```python
-result = pipe_output.main_stuff_as_str
-```
-Structured object (BaseModel):
-
-```python
-result = pipe_output.main_stuff_as(content_type=GanttChart)
-```
-
-If it's a list, you can get a `ListContent` of the specific type.
-
-```python
-result_list_content = pipe_output.main_stuff_as_list(item_type=GanttChart)
-```
-
-or if you want, you can get the actual items as a regular python list:
-
-```python
-result_list = pipe_output.main_stuff_as_items(item_type=GanttChart)
-```
-
----
-
-## Rules to choose LLM models used in PipeLLMs.
-
-### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-### Using an LLM Handle in a PipeLLM
-
-Here is an example of using an llm_handle to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-4.5-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
diff --git a/.cursor/rules/commands.mdc b/.cursor/rules/commands.mdc
deleted file mode 100644
index 6c017d1..0000000
--- a/.cursor/rules/commands.mdc
+++ /dev/null
@@ -1,8 +0,0 @@
----
-alwaysApply: true
-description: Guidelines for running commands
----
-# Commands
-
- - When you want to run commands such as `python`, `pytest` or any of our CLI such as `pipelex` or `cocode`, ALWAYS use the current obvious virtual env. If the installaton is standard, the venv is named `.venv` so always check that first.
-
\ No newline at end of file
diff --git a/.cursor/rules/docs.mdc b/.cursor/rules/docs.mdc
deleted file mode 100644
index 1a16650..0000000
--- a/.cursor/rules/docs.mdc
+++ /dev/null
@@ -1,16 +0,0 @@
----
-alwaysApply: false
-description: Guidelines for writing documentation
-globs:
-- docs/**/*.md
----
-# Writing Docs
-
-Write docs and answer questions about writing docs.
-
-We use Material for MkDocs. All markdown in our docs must be compatible with Material for MkDocs and done using best practices to get the best results with Material for MkDocs.
-
-## MkDocs Markdown Requirements
-
-- Always add a blank line before any bullet lists or numbered lists in MkDocs markdown.
-
diff --git a/.cursor/rules/llms.mdc b/.cursor/rules/llms.mdc
deleted file mode 100644
index 2035388..0000000
--- a/.cursor/rules/llms.mdc
+++ /dev/null
@@ -1,85 +0,0 @@
----
-alwaysApply: false
-description: LLM configuration and usage guidelines
-globs:
-- '*.plx'
-- '*.toml'
----
-# Rules to choose LLM models used in PipeLLMs.
-
-## LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-## LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-## Using an LLM Handle in a PipeLLM
-
-Here is an example of using an llm_handle to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-## LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-4.5-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
diff --git a/.cursor/rules/pytest_standards.mdc b/.cursor/rules/pytest_standards.mdc
deleted file mode 100644
index b1c2f11..0000000
--- a/.cursor/rules/pytest_standards.mdc
+++ /dev/null
@@ -1,107 +0,0 @@
----
-alwaysApply: false
-description: Guidelines for writing unit tests
-globs:
-- tests/**/*.py
----
-# Writing unit tests
-
-## Unit test generalities
-
-NEVER USE unittest.mock or MagicMock. YOU MUST USE pytest-mock instead.
-
-### Test file structure
-
-- Name test files with `test_` prefix
-- Place test files in the appropriate test category directory:
- - `tests/unit/` - for unit tests that test individual functions/classes in isolation
- - `tests/integration/` - for integration tests that test component interactions
- - `tests/e2e/` - for end-to-end tests that test complete workflows
- - `tests/test_pipelines/` - for test pipeline definitions (PLX files and their structuring python files)
-- Fixtures are defined in conftest.py modules at different levels of the hierarchy, their scope is handled by pytest
-- Test data is placed inside test_data.py at different levels of the hierarchy, they must be imported with package paths from the root like `tests.pipelex.test_data`. Their content is all constants, regrouped inside classes to keep things tidy.
-- Always put test inside Test classes.
-- The pipelex pipelines should be stored in `tests/test_pipelines` as well as the related structured Output classes that inherit from `StructuredContent`
-
-### Markers
-
-Apply the appropriate markers:
-- "llm: uses an LLM to generate text or objects"
-- "img_gen: uses an image generation AI"
-- "extract: uses text/image extraction from documents"
-- "inference: uses either an LLM or an image generation AI"
-- "gha_disabled: will not be able to run properly on GitHub Actions"
-
-Several markers may be applied. For instance, if the test uses an LLM, then it uses inference, so you must mark with both `inference`and `llm`.
-
-### Important rules
-
-- Never use the unittest.mock. Use pytest-mock.
-
-### Test Class Structure
-
-- Always group the tests of a module into a test class:
-
-```python
-@pytest.mark.llm
-@pytest.mark.inference
-@pytest.mark.asyncio(loop_scope="class")
-class TestFooBar:
- @pytest.mark.parametrize(
- "topic, test_case_blueprint",
- [
- TestCases.CASE_1,
- TestCases.CASE_2,
- ],
- )
- async def test_pipe_processing(
- self,
- request: FixtureRequest,
- topic: str,
- test_case_blueprint: StuffBlueprint,
- ):
- # Test implementation
-```
-
-- Never more than 1 class per test file.
-- When testing one method, if possible, limit the number of test functions, but with different test cases in parameters
-- Sometimes it can be convenient to access the test's name in its body, for instance to include into a job_id. To achieve that, add the argument `request: FixtureRequest` into the signature and then you can get the test name using `cast(str, request.node.originalname), # type: ignore`.
-
-### Test Data Organization
-
-- If it's not already there, create a `test_data.py` file in the proper test directory
-- Define test cases using `StuffBlueprint`:
-
-```python
-class TestCases:
- CASE_BLUEPRINT_1 = StuffBlueprint(
- name="test_case_1",
- concept_code="domain.ConceptName1",
- value="test_value"
- )
- CASE_BLUEPRINT_2 = StuffBlueprint(
- name="test_case_2",
- concept_code="domain.ConceptName2",
- value="test_value"
- )
-
- CASE_BLUEPRINTS: ClassVar[list[tuple[str, str]]] = [ # topic, blueprint"
- ("topic1", CASE_BLUEPRINT_1),
- ("topic2", CASE_BLUEPRINT_2),
- ]
-```
-
-Note how we avoid initializing a default mutable value within a class instance, instead we use ClassVar.
-Also note that we provide a topic for the test case, which is purely for convenience.
-
-## Best Practices for Testing
-
-- Whenever possible, use strong asserts to test value, not just type and presence.
-- Use parametrize for multiple test cases
-- Test both success and failure cases
-- Verify working memory state
-- Check output structure and content
-- Use meaningful test case names
-- Include docstrings explaining test purpose but not on top of the file
-- Log outputs for debugging
-- Generate reports for cost tracking
diff --git a/.cursor/rules/python_standards.mdc b/.cursor/rules/python_standards.mdc
deleted file mode 100644
index 2e2e0a9..0000000
--- a/.cursor/rules/python_standards.mdc
+++ /dev/null
@@ -1,176 +0,0 @@
----
-alwaysApply: false
-description: Python coding standards and best practices
-globs:
-- '**/*.py'
----
-# Coding Standards & Best Practices for Python Code
-
-This document outlines the core coding standards, best practices, and quality control procedures for the codebase.
-
-## Variables, loops and indexes
-
- - Variable names should have a minimum length of 3 characters. No exceptions: name your `for` loop indexes like `index_foobar`, your exceptions `exc` or more specific like `validation_error` when there are several layers of exceptions, and use `for key, value in ...` for key/value pairs.
- - When looping on the keys of a dict, use `for key in the_dict` rather than `for key in the_dict.keys()` otherwise you won't pass linting.
- - Avoid inline for loops, unless it's ultra-simple and holds on oneline.
- - If you have a variable that will get its value differently through different code paths, declare it first with a type, e.g. `pipe_code: str` but DO NOT give it a default value like `pipe_code: str = ""` unless it's really justified. We want the variable to be unbound until all paths are covered, and the linters will help us avoid bugs this way.
-
-## Enums and tests
-
- - When defining enums related to string values, always inherit from `StrEnum`
- - Never test equality to an enum value: use match/case, even to single out 1 case out of 10 cases. To avoid heavy match/case code in awkward places, add methods to the enum class such as `is_foobar()`. This is to avoid bugs: when new enum values are added we want the linter to complain. Use the `|` operator to group cases
- - As our match/case constructs over enums are always exhaustive, NEVER add a default `case _: ...`. Otherwise, you won't pass linting.
-
-## Imports
-
-### **Imports at the top of the file**
-
- - Import all necessary libraries at the top of the file
- - Do not import libraries in functions or classes unless in very specific cases, to be discussed with the user, as they would required a `# noqa: ...` comment to pass linting
- - Do not bother with ordering the imports, our Ruff linter will handle it for us. Same goes with removing unused imports.
-
-- **Logging and Pretty Printing**:
-
- - Both `log()` and `pretty_print()` can be imported from `pipelex` directly:
- ```python
- from pipelex import log, pretty_print
-
- log.info("Hello, world!")
- ```
- - Both have a title arg which is handy when logging/printing objects:
-
- ```python
- log.verbose("Hello, world!", title="Your first Pipelex log")
- pretty_print(output_object, title="Your first Pipelex output")
- ```
- - Both handle formatting json using Rich, pretty_print makes it prettier.
-
-- **StrEnum and Self type**:
-
- - Both `StrEnum` and `Self` must be imported from `pipelex.types` (handles python retrocompatibility):
- ```python
- from pipelex.types import Self, StrEnum
- ```
-
-## Typing
-
-### **Always Use Type Hints**
-
- - Every function parameter must be typed
- - Every function return must be typed
- - Use type hints for all variables where type is not obvious
- - Use dict, list, tuple types with lowercase first letter: dict[], list[], tuple[]
- - Use type hints for all fields
- - Use Field(default_factory=...) for mutable defaults
- - Use `# pyright: ignore[specificError]` or `# type: ignore` only as a last resort. In particular, if you are sure about the type, you often solve issues by using cast() or creating a new typed variable.
-
-### **BaseModel / Pydantic Standards**
-
- - Use `BaseModel` and respect Pydantic v2 standards
- - Use the modern `ConfigDict` when needed, e.g. `model_config = ConfigDict(extra="forbid", strict=True)`
- - Keep models focused and single-purpose
- - For list fields with non-string items in BaseModels, use `empty_list_factory_of()` to avoid linter complaints:
- ```python
- from pydantic import BaseModel, Field
- from pipelex.tools.typing.pydantic_utils import empty_list_factory_of
-
- class MyModel(BaseModel):
- names: list[str] = Field(default_factory=list) # OK for strings
- numbers: list[int] = Field(default_factory=empty_list_factory_of(int), description="A list of numbers")
- items: list[MyItem] = Field(default_factory=empty_list_factory_of(MyItem), description="A list of items")
- ```
-
-## Factory Pattern
-
- - Use Factory Pattern for object creation when dealing with multiple implementations
- - Our factory methods are named `make_from_...` and such
-
-## Error Handling
-
- - Always catch exceptions at the place where you can add useful context to it.
- - Use try/except blocks with specific exceptions
- - Convert third-party exceptions to our custom ones
- - NEVER catch the generic Exception, only catch specific exceptions, except at the root of CLI commands
- - NEVER raise generic exceptions like ValueError or TypeError, create new error classes and raise them instead
- - Always add `from exc` to the exception
-
- ```python
- try:
- self.models_manager.setup()
- except RoutingProfileLibraryNotFoundError as exc:
- msg = "The routing library could not be found, please call `pipelex init config` to create it"
- raise PipelexSetupError(msg) from exc
- ```
-
- **Note**: Following Ruff rules, we set the error message as a variable before raising it, for cleaner error traces.
-
-## Documentation
-
-1. **Docstring Format**
- ```python
- def process_image(image_path: str, size: tuple[int, int]) -> bytes:
- """Process and resize an image.
-
- Args:
- image_path: Path to the source image
- size: Tuple of (width, height) for resizing
-
- Returns:
- Processed image as bytes
- """
- pass
- ```
-
-2. **Class Documentation**
- ```python
- class ImageProcessor:
- """Handles image processing operations.
-
- Provides methods for resizing, converting, and optimizing images.
- """
- ```
-
-## Code Quality Checks
-
-### Linting and Type Checking
-
-Before finalizing a task, run:
-```bash
-make fix-unused-imports
-make check
-```
-
-This runs multiple code quality tools:
-- Pyright: Static type checking
-- Ruff: Fast Python linter
-- Mypy: Static type checker
-
-Always fix any issues reported by these tools before proceeding.
-
-### Running Tests
-
-1. **Quick Test Run** (no LLM/image generation):
- ```bash
- make tp
- ```
- Runs tests with markers: `(dry_runnable or not (inference or llm or img_gen or extract)) and not (needs_output or pipelex_api)`
-
-2. **Specific Tests**:
- ```bash
- make tp TEST=TestClassName
- # or
- make tp TEST=test_function_name
- ```
- Note: Matches names starting with the provided string.
-
-**Important**: Never run `make ti`, `make test-inference`, `make te`, `make test-extract`, `make tg`, or `make test-img-gen` - these use costly inference.
-
-## Pipelines
-
-- Always validate pipelines after creation/edit with `make validate`.
- Iterate if there are errors.
-
-## Project Structure
-
-- **Tests**: `tests/` directory
-- **Documentation**: `docs/` directory
diff --git a/.cursor/rules/run_pipelex.mdc b/.cursor/rules/run_pipelex.mdc
deleted file mode 100644
index 8387838..0000000
--- a/.cursor/rules/run_pipelex.mdc
+++ /dev/null
@@ -1,231 +0,0 @@
----
-alwaysApply: false
-description: Guidelines for running Pipelex pipelines
-globs:
-- examples/**/*.py
----
-# Guide to execute a pipeline and write example code
-
-## Example to execute a pipeline with text output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-
-
-async def hello_world() -> str:
- """
- This function demonstrates the use of a super simple Pipelex pipeline to generate text.
- """
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="hello_world",
- )
-
- return pipe_output.main_stuff_as_str
-
-
-# start Pipelex
-Pipelex.make()
-# run sample using asyncio
-output_text = asyncio.run(hello_world())
-pretty_print(output_text, title="Your first Pipelex output")
-```
-
-## Example to execute a pipeline with structured output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-from pipelex.core.stuffs.image_content import ImageContent
-
-from my_project.gantt.gantt_struct import GanttChart
-
-SAMPLE_NAME = "extract_gantt"
-IMAGE_URL = "assets/gantt/gantt_tree_house.png"
-
-
-async def extract_gantt(image_url: str) -> GanttChart:
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
- # Output the result
- return pipe_output.main_stuff_as(content_type=GanttChart)
-
-
-# start Pipelex
-Pipelex.make()
-
-# run sample using asyncio
-gantt_chart = asyncio.run(extract_gantt(image_url=IMAGE_URL))
-pretty_print(gantt_chart, title="Gantt Chart")
-```
-
-## Setting up the input memory
-
-### Explanation of input memory
-
-The input memory is a dictionary, where the key is the name of the input variable and the value provides details to make it a stuff object. The relevant definitions are:
-```python
-StuffContentOrData = dict[str, Any] | StuffContent | list[Any] | str
-PipelineInputs = dict[str, StuffContentOrData]
-```
-As you can seen, we made it so different ways can be used to define that stuff using structured content or data.
-
-### Different ways to set up the input memory
-
-So here are a few concrete examples of calls to execute_pipeline with various ways to set up the input memory:
-
-```python
-# Here we have a single input and it's a Text.
-# If you assign a string, by default it will be considered as a TextContent.
- pipe_output = await execute_pipeline(
- pipe_code="master_advisory_orchestrator",
- inputs={
- "user_input": problem_description,
- },
- )
-
-# Here we have a single input and it's a PDF.
-# Because PDFContent is a native concept, we can use it directly as a value,
-# the system knows what content it corresponds to:
- pipe_output = await execute_pipeline(
- pipe_code="power_extractor_dpe",
- inputs={
- "document": PDFContent(url=pdf_url),
- },
- )
-
-# Here we have a single input and it's an Image.
-# Because ImageContent is a native concept, we can use it directly as a value:
- pipe_output = await execute_pipeline(
- pipe_code="fashion_variation_pipeline",
- inputs={
- "fashion_photo": ImageContent(url=image_url),
- },
- )
-
-# Here we have a single input, it's an image but
-# its actually a more specific concept gantt.GanttImage which refines Image,
-# so we must provide it using a dict with the concept and the content:
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
-
-# Here is a more complex example with multiple inputs assigned using different ways:
- pipe_output = await execute_pipeline(
- pipe_code="retrieve_then_answer",
- dynamic_output_concept_code="contracts.Fees",
- inputs={
- "text": load_text_from_path(path=text_path),
- "question": {
- "concept": "answer.Question",
- "content": question,
- },
- "client_instructions": client_instructions,
- },
- )
-```
-
-## Using the outputs of a pipeline
-
-All pipe executions return a `PipeOutput` object.
-It's a BaseModel which contains the resulting working memory at the end of the execution and the pipeline run id.
-It also provides a bunch of accessor functions and properties to unwrap the main stuff, which is the last stuff added to the working memory:
-
-```python
-
-class PipeOutput(BaseModel):
- working_memory: WorkingMemory = Field(default_factory=WorkingMemory)
- pipeline_run_id: str = Field(default=SpecialPipelineId.UNTITLED)
-
- @property
- def main_stuff(self) -> Stuff:
- ...
-
- def main_stuff_as_list(self, item_type: type[StuffContentType]) -> ListContent[StuffContentType]:
- ...
-
- def main_stuff_as_items(self, item_type: type[StuffContentType]) -> list[StuffContentType]:
- ...
-
- def main_stuff_as(self, content_type: type[StuffContentType]) -> StuffContentType:
- ...
-
- @property
- def main_stuff_as_text(self) -> TextContent:
- ...
-
- @property
- def main_stuff_as_str(self) -> str:
- ...
-
- @property
- def main_stuff_as_image(self) -> ImageContent:
- ...
-
- @property
- def main_stuff_as_text_and_image(self) -> TextAndImagesContent:
- ...
-
- @property
- def main_stuff_as_number(self) -> NumberContent:
- ...
-
- @property
- def main_stuff_as_html(self) -> HtmlContent:
- ...
-
- @property
- def main_stuff_as_mermaid(self) -> MermaidContent:
- ...
-```
-
-As you can see, you can extract any variable from the output working memory.
-
-### Getting the main stuff as a specific type
-
-Simple text as a string:
-
-```python
-result = pipe_output.main_stuff_as_str
-```
-Structured object (BaseModel):
-
-```python
-result = pipe_output.main_stuff_as(content_type=GanttChart)
-```
-
-If it's a list, you can get a `ListContent` of the specific type.
-
-```python
-result_list_content = pipe_output.main_stuff_as_list(item_type=GanttChart)
-```
-
-or if you want, you can get the actual items as a regular python list:
-
-```python
-result_list = pipe_output.main_stuff_as_items(item_type=GanttChart)
-```
-
----
-
diff --git a/.cursor/rules/tdd.mdc b/.cursor/rules/tdd.mdc
deleted file mode 100644
index 4b4f058..0000000
--- a/.cursor/rules/tdd.mdc
+++ /dev/null
@@ -1,28 +0,0 @@
----
-alwaysApply: false
-description: Guidelines for writing test-driven development code
----
-# Test-Driven Development Guide
-
-This document outlines our test-driven development (TDD) process and the tools available for testing.
-
-## TDD Cycle
-
-1. **Write a Test First**
-[pytest.mdc](pytest.mdc)
-
-2. **Write the Code**
- - Implement the minimum amount of code needed to pass the test
- - Follow the project's coding standards
- - Keep it simple - don't write more than needed
-
-3. **Run Linting and Type Checking**
-[coding_standards.mdc](coding_standards.mdc)
-
-4. **Refactor if needed**
-If the code needs refactoring, with the best practices [coding_standards.mdc](coding_standards.mdc)
-
-5. **Validate tests**
-
-Remember: The key to TDD is writing the test first and letting it drive your implementation. Always run the full test suite and quality checks before considering a feature complete.
-
diff --git a/.cursor/rules/write_pipelex.mdc b/.cursor/rules/write_pipelex.mdc
deleted file mode 100644
index 9f23faf..0000000
--- a/.cursor/rules/write_pipelex.mdc
+++ /dev/null
@@ -1,862 +0,0 @@
----
-alwaysApply: false
-description: Guidelines for writing Pipelex pipelines
-globs:
-- '**/*.plx'
-- '**/pipelines/**/*.py'
----
-# Guide to write or edit pipelines using the Pipelex language in .plx files
-
-- Always first write your "plan" in natural language, then transcribe it in pipelex.
-- You should ALWAYS RUN validation when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
- - For a specific file: `pipelex validate path_to_file.plx`
- - For all pipelines: `pipelex validate all`
- - **IMPORTANT**: Ensure the Python virtual environment is activated before running `pipelex` commands. For standard installations, the venv is named `.venv` - always check that first. The commands will not work without proper venv activation.
-- Please use POSIX standard for files. (empty lines, no trailing whitespaces, etc.)
-
-## Pipeline File Naming
-- Files must be `.plx` for pipelines (Always add an empty line at the end of the file, and do not add trailing whitespaces to PLX files at all)
-- Files must be `.py` for code defining the data structures
-- Use descriptive names in `snake_case`
-
-## Pipeline File Outline
-A pipeline file has three main sections:
-1. Domain statement
-2. Concept definitions
-3. Pipe definitions
-
-### Domain Statement
-```plx
-domain = "domain_name"
-description = "Description of the domain" # Optional
-```
-Note: The domain name usually matches the plx filename for single-file domains. For multi-file domains, use the subdirectory name.
-
-### Concept Definitions
-
-Concepts represent ideas and semantic entities in your pipeline. They define what something *is*, not how it's structured.
-
-```plx
-[concept]
-ConceptName = "Description of the concept"
-```
-
-**Naming Rules:**
-- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story") - lists are handled implicitly by Pipelex
-- Avoid circumstantial adjectives (no "LargeText", use "Text") - focus on the essence of what the concept represents
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number, Page, JSON)
-
-**Native Concepts:**
-Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`, `JSON`. Use these directly or refine them when appropriate.
-
-**Refining Native Concepts:**
-To create a concept that specializes a native concept without adding fields:
-
-```plx
-[concept.Landscape]
-description = "A scenic outdoor photograph"
-refines = "Image"
-```
-
-For details on how to structure concepts with fields, see the "Structuring Models" section below.
-
-### Pipe Definitions
-
-## Pipe Base Definition
-
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-description = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
-
-The pipes will all have at least this base definition.
-- `inputs`: Dictionary of key being the variable used in the prompts, and the value being the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if PipeSequence) or of the conditional pipes (if PipeCondition).
-So If you have this error:
-`PipeValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['invoice']'``
-That means that the pipe validate_expense is missing the input `invoice` because one of the subpipe is needing it.
-
-NEVER WRITE THE INPUTS BY BREAKING THE LINE LIKE THIS:
-
-```plx
-inputs = {
- input_1 = "ConceptName1",
- input_2 = "ConceptName2"
-}
-```
-
-
-- `output`: The name of the concept to output. The `ConceptName` should have the same name as the python class if you want structured output:
-
-### Input Multiplicity
-
-By default, inputs expect a single item. Use bracket notation to specify multiple items:
-
-```plx
-# Single item (default)
-inputs = { document = "Text" }
-
-# Variable list - indeterminate number of items
-inputs = { documents = "Text[]" }
-
-# Fixed count - exactly N items
-inputs = { comparison_items = "Image[2]" }
-```
-
-**Key points:**
-- No brackets = single item (default behavior)
-- Use `[]` for lists of unknown length
-- Use `[N]` (where N is an integer) when operation requires exact count (e.g., comparing 2 items)
-
-## Structuring Models
-
-Once you've defined your concepts semantically (see "Concept Definitions" above), you need to specify their structure if they have fields.
-
-### Three Ways to Structure Concepts
-
-**1. No Structure Needed**
-
-If a concept only refines a native concept without adding fields, use the TOML table syntax shown in "Concept Definitions" above. No structure section is needed.
-
-**2. Inline Structure Definition (RECOMMENDED for most cases)**
-
-For concepts with structured fields, define them inline using TOML syntax:
-
-```plx
-[concept.Invoice]
-description = "A commercial document issued by a seller to a buyer"
-
-[concept.Invoice.structure]
-invoice_number = "The unique invoice identifier" # This will be optional by default
-issue_date = { type = "date", description = "The date the invoice was issued", required = true }
-total_amount = { type = "number", description = "The total invoice amount", required = true }
-vendor_name = "The name of the vendor" # This will be optional by default
-line_items = { type = "list", item_type = "text", description = "List of items" }
-```
-
-**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
-
-**Field properties:** `type`, `description`, `required` (default: false), `default_value`, `choices`, `item_type` (for lists), `key_type` and `value_type` (for dicts)
-
-**Simple syntax** (creates required text field):
-```plx
-field_name = "Field description"
-```
-
-**Detailed syntax** (with explicit properties):
-```plx
-field_name = { type = "text", description = "Field description", default_value = "default" }
-```
-
-**3. Python StructuredContent Class (For Advanced Features)**
-
-Create a Python class when you need:
-- Custom validation logic (@field_validator, @model_validator)
-- Computed properties (@property methods)
-- Custom methods or class methods
-- Complex cross-field validation
-- Reusable structures across multiple domains
-
-```python
-from pipelex.core.stuffs.structured_content import StructuredContent
-from pydantic import Field, field_validator
-
-class Invoice(StructuredContent):
- """A commercial invoice with validation."""
-
- invoice_number: str = Field(description="The unique invoice identifier")
- total_amount: float = Field(ge=0, description="The total invoice amount")
- tax_amount: float = Field(ge=0, description="Tax amount")
-
- @field_validator('tax_amount')
- @classmethod
- def validate_tax(cls, v, info):
- """Ensure tax doesn't exceed total."""
- total = info.data.get('total_amount', 0)
- if v > total:
- raise ValueError('Tax amount cannot exceed total amount')
- return v
-```
-
-**Location:** Create models in `my_project/some_domain/some_domain_struct.py`. Classes inheriting from `StructuredContent` are automatically discovered.
-
-### Decision Rules for Agents
-
-**If concept already exists:**
-- If it's already inline → KEEP IT INLINE unless user explicitly asks to convert or features require Python class
-- If it's already a Python class → KEEP IT as Python class
-
-**If creating new concept:**
-1. Does it only refine a native concept without adding fields? → Use concept-only declaration
-2. Does it need custom validation, computed properties, or methods? → Use Python class
-3. Otherwise → Use inline structure (fastest and simplest)
-
-**When to suggest conversion to Python class:**
-- User needs validation logic beyond type checking
-- User needs computed properties or custom methods
-- Structure needs to be reused across multiple domains
-- Complex type relationships or inheritance required
-
-### Inline Structure Limitations
-
-Inline structures:
-- ✅ Support all common field types (text, number, date, list, dict, etc.)
-- ✅ Support required/optional fields, defaults, choices
-- ✅ Generate full Pydantic models with validation
-- ❌ Cannot have custom validators or complex validation logic
-- ❌ Cannot have computed properties or custom methods
-- ❌ Cannot refine custom (non-native) concepts
-- ❌ Limited IDE autocomplete compared to explicit Python classes
-
-
-## Pipe Controllers and Pipe Operators
-
-Look at the Pipes we have in order to adapt it. Pipes are organized in two categories:
-
-1. **Controllers** - For flow control:
- - `PipeSequence` - For creating a sequence of multiple steps
- - `PipeCondition` - If the next pipe depends of the expression of a stuff in the working memory
- - `PipeParallel` - For parallelizing pipes
-
-2. **Operators** - For specific tasks:
- - `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeExtract` - Extract text and images from an image or a PDF
- - `PipeCompose` - For composing text using Jinja2 templates: supports html, markdown, mermaid, etc.
- - `PipeImgGen` - Generate Images
- - `PipeFunc` - For running classic python scripts
-
-## PipeSequence controller
-
-Purpose: PipeSequence executes multiple pipes in a defined order, where each step can use results from original inputs or from previous steps.
-
-### Basic Definition
-```plx
-[pipe.your_sequence_name]
-type = "PipeSequence"
-description = "Description of what this sequence does"
-inputs = { input_name = "InputType" } # All the inputs of the sub pipes, except the ones generated by intermediate steps
-output = "OutputType"
-steps = [
- { pipe = "first_pipe", result = "first_result" },
- { pipe = "second_pipe", result = "second_result" },
- { pipe = "final_pipe", result = "final_result" }
-]
-```
-
-### Key Components
-
-1. **Steps Array**: List of pipes to execute in sequence
- - `pipe`: Name of the pipe to execute
- - `result`: Name to assign to the pipe's output that will be in the working memory
-
-### Using PipeBatch in Steps
-
-You can use PipeBatch functionality within steps using `batch_over` and `batch_as`:
-
-```plx
-steps = [
- { pipe = "process_items", batch_over = "input_list", batch_as = "current_item", result = "processed_items"
- }
-]
-```
-
-1. **batch_over**: Specifies a `ListContent` field to iterate over. Each item in the list will be processed individually and IN PARALLEL by the pipe.
- - Must be a `ListContent` type containing the items to process
- - Can reference inputs or results from previous steps
-
-2. **batch_as**: Defines the name that will be used to reference the current item being processed
- - This name can be used in the pipe's input mappings
- - Makes each item from the batch available as a single element
-
-The result of a batched step will be a `ListContent` containing the outputs from processing each item.
-
-## PipeCondition controller
-
-The PipeCondition controller allows you to implement conditional logic in your pipeline, choosing which pipe to execute based on an evaluated expression. It supports both direct expressions and expression templates.
-
-### Basic usage
-
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression = "input_data.category"
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-or
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression_template = "{{ input_data.category }}" # Jinja2 code
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-
-### Key Parameters
-
-- `expression`: Direct boolean or string expression (mutually exclusive with expression_template)
-- `expression_template`: Jinja2 template for more complex conditional logic (mutually exclusive with expression)
-- `outcomes`: Dictionary mapping expression results to pipe codes:
- 1. The key on the left (`small`, `medium`) is the result of `expression` or `expression_template`
- 2. The value on the right (`process_small`, `process_medium`, etc.) is the name of the pipe to trigger
-- `default_outcome`: **Required** - The pipe to execute if the expression doesn't match any key in outcomes. Use `"fail"` if you want the pipeline to fail when no match is found
-
-Example with fail as default:
-```plx
-[pipe.strict_validation]
-type = "PipeCondition"
-description = "Validate with strict matching"
-inputs = { status = "Status" }
-output = "Text"
-expression = "status.value"
-default_outcome = "fail"
-
-[pipe.strict_validation.outcomes]
-approved = "process_approved"
-rejected = "process_rejected"
-```
-
-## PipeLLM operator
-
-PipeLLM is used to:
-1. Generate text or objects with LLMs
-2. Process images with Vision LLMs
-
-### Basic Usage
-
-Simple Text Generation:
-```plx
-[pipe.write_story]
-type = "PipeLLM"
-description = "Write a short story"
-output = "Text"
-prompt = """
-Write a short story about a programmer.
-"""
-```
-
-Structured Data Extraction:
-```plx
-[pipe.extract_info]
-type = "PipeLLM"
-description = "Extract information"
-inputs = { text = "Text" }
-output = "PersonInfo"
-prompt = """
-Extract person information from this text:
-@text
-"""
-```
-
-Supports system instructions:
-```plx
-[pipe.expert_analysis]
-type = "PipeLLM"
-description = "Expert analysis"
-output = "Analysis"
-system_prompt = "You are a data analysis expert"
-prompt = "Analyze this data"
-```
-
-### Multiple Outputs
-
-Generate multiple outputs (fixed number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[3]" # Generate exactly 3 ideas
-prompt = "Generate 3 ideas"
-```
-
-Generate multiple outputs (variable number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[]" # Let the LLM decide how many to generate
-prompt = "Generate ideas"
-```
-
-### Vision
-
-Process images with VLMs (image inputs must be tagged in the prompt):
-```plx
-[pipe.analyze_image]
-type = "PipeLLM"
-description = "Analyze image"
-inputs = { image = "Image" }
-output = "ImageAnalysis"
-prompt = """
-Describe what you see in this image:
-
-$image
-"""
-```
-
-You can also reference images inline in meaningful sentences to guide the Visual LLM:
-```plx
-[pipe.compare_images]
-type = "PipeLLM"
-description = "Compare two images"
-inputs = { photo = "Image", painting = "Image" }
-output = "Analysis"
-prompt = "Analyze the colors in $photo and the shapes in $painting."
-```
-
-### Writing prompts for PipeLLM
-
-**Insert stuff inside a tagged block**
-
-If the inserted text is supposedly a long text, made of several lines or paragraphs, you want it inserted inside a block, possibly a block tagged and delimlited with proper syntax as one would do in a markdown documentation. To include stuff as a block, use the "@" prefix.
-
-Example template:
-```plx
-prompt = """
-Match the expense with its corresponding invoice:
-
-@expense
-
-@invoices
-"""
-```
-In the example above, the expense data and the invoices data are obviously made of several lines each, that's why it makes sense to use the "@" prefix in order to have them delimited inside a block. Note that our preprocessor will automatically include the block's title, so it doesn't need to be explicitly written in the prompt.
-
-DO NOT write things like "Here is the expense: @expense".
-DO write simply "@expense" alone in an isolated line.
-
-**Insert stuff inline**
-
-If the inserted text is short text and it makes sense to have it inserted directly into a sentence, you want it inserted inline. To insert stuff inline, use the "$" prefix. This will insert the stuff without delimiters and the content will be rendered as plain text.
-
-Example template:
-```plx
-prompt = """
-Your goal is to summarize everything related to $topic in the provided text:
-
-@text
-
-Please provide only the summary, with no additional text or explanations.
-Your summary should not be longer than 2 sentences.
-"""
-```
-
-In the example above, $topic will be inserted inline, whereas @text will be a a delimited block.
-Be sure to make the proper choice of prefix for each insertion.
-
-DO NOT write "$topic" alone in an isolated line.
-DO write things like "Write an essay about $topic" to include text into an actual sentence.
-
-
-## PipeExtract operator
-
-The PipeExtract operator is used to extract text and images from an image or a PDF
-
-### Simple Text Extraction
-```plx
-[pipe.extract_info]
-type = "PipeExtract"
-description = "extract the information"
-inputs = { document = "PDF" } # or { image = "Image" } if it's an image. This is the only input.
-output = "Page"
-```
-
-Using Extract Model Settings:
-```plx
-[pipe.extract_with_model]
-type = "PipeExtract"
-description = "Extract with specific model"
-inputs = { document = "PDF" }
-output = "Page"
-model = "base_extract_mistral" # Use predefined extract preset or model alias
-```
-
-Only one input is allowed and it must either be an `Image` or a `PDF`. The input can be named anything.
-
-The output concept `Page` is a native concept, with the structure `PageContent`:
-It corresponds to 1 page. Therefore, the PipeExtract is outputing a `ListContent` of `Page`
-
-```python
-class TextAndImagesContent(StuffContent):
- text: TextContent | None
- images: list[ImageContent] | None
-
-class PageContent(StructuredContent): # CONCEPT IS "Page"
- text_and_images: TextAndImagesContent
- page_view: ImageContent | None = None
-```
-- `text_and_images` are the text, and the related images found in the input image or PDF.
-- `page_view` is the screenshot of the whole pdf page/image.
-
-## PipeCompose operator
-
-The PipeCompose operator is used to compose text using Jinja2 templates. It supports various output formats including HTML, Markdown, Mermaid diagrams, and more.
-
-### Basic Usage
-
-Simple Template Composition:
-```plx
-[pipe.compose_report]
-type = "PipeCompose"
-description = "Compose a report using template"
-inputs = { data = "ReportData" }
-output = "Text"
-template = """
-# Report Summary
-
-Based on the analysis:
-$data
-
-Generated on: {{ current_date }}
-"""
-```
-
-Using Named Templates:
-```plx
-[pipe.use_template]
-type = "PipeCompose"
-description = "Use a predefined template"
-inputs = { content = "Text" }
-output = "Text"
-template_name = "standard_report_template"
-```
-
-Using Nested Template Section (for more control):
-```plx
-[pipe.advanced_template]
-type = "PipeCompose"
-description = "Use advanced template settings"
-inputs = { data = "ReportData" }
-output = "Text"
-
-[pipe.advanced_template.template]
-template = "Report: $data"
-category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-```
-
-CRM Email Template:
-```plx
-[pipe.compose_follow_up_email]
-type = "PipeCompose"
-description = "Compose a personalized follow-up email for CRM"
-inputs = { customer = "Customer", deal = "Deal", sales_rep = "SalesRep" }
-output = "Text"
-template_category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-template = """
-Subject: Following up on our $deal.product_name discussion
-
-Hi $customer.first_name,
-
-I hope this email finds you well! I wanted to follow up on our conversation about $deal.product_name from $deal.last_contact_date.
-
-Based on our discussion, I understand that your key requirements are: $deal.customer_requirements
-
-I'm excited to let you know that we can definitely help you achieve your goals. Here's what I'd like to propose:
-
-**Next Steps:**
-- Schedule a demo tailored to your specific needs
-- Provide you with a customized quote based on your requirements
-- Connect you with our implementation team
-
-Would you be available for a 30-minute call this week? I have openings on:
-{% for slot in available_slots %}
-- {{ slot }}
-{% endfor %}
-
-Looking forward to moving this forward together!
-
-Best regards,
-$sales_rep.name
-$sales_rep.title
-$sales_rep.phone | $sales_rep.email
-"""
-```
-
-### Key Parameters
-
-- `template`: Inline template string (mutually exclusive with template_name)
-- `template_name`: Name of a predefined template (mutually exclusive with template)
-- `template_category`: Template type ("llm_prompt", "html", "markdown", "mermaid", etc.)
-- `templating_style`: Styling options for template rendering
-- `extra_context`: Additional context variables for template
-
-For more control, you can use a nested `template` section instead of the `template` field:
-- `template.template`: The template string
-- `template.category`: Template type
-- `template.templating_style`: Styling options
-
-### Template Variables
-
-Use the same variable insertion rules as PipeLLM:
-- `@variable` for block insertion (multi-line content)
-- `$variable` for inline insertion (short text)
-
-## PipeImgGen operator
-
-The PipeImgGen operator is used to generate images using AI image generation models.
-
-### Basic Usage
-
-Simple Image Generation:
-```plx
-[pipe.generate_image]
-type = "PipeImgGen"
-description = "Generate an image from prompt"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-```
-
-Using Image Generation Settings:
-```plx
-[pipe.generate_photo]
-type = "PipeImgGen"
-description = "Generate a high-quality photo"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Photo"
-model = { model = "fast-img-gen" }
-aspect_ratio = "16:9"
-quality = "hd"
-```
-
-Multiple Image Generation:
-```plx
-[pipe.generate_variations]
-type = "PipeImgGen"
-description = "Generate multiple image variations"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image[3]"
-seed = "auto"
-```
-
-Advanced Configuration:
-```plx
-[pipe.generate_custom]
-type = "PipeImgGen"
-description = "Generate image with custom settings"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-model = "img_gen_preset_name" # Use predefined preset
-aspect_ratio = "1:1"
-quality = "hd"
-background = "transparent"
-output_format = "png"
-is_raw = false
-safety_tolerance = 3
-```
-
-### Key Parameters
-
-**Image Generation Settings:**
-- `model`: Model choice (preset name or inline settings with model name)
-- `quality`: Image quality ("standard", "hd")
-
-**Output Configuration:**
-- `aspect_ratio`: Image dimensions ("1:1", "16:9", "9:16", etc.)
-- `output_format`: File format ("png", "jpeg", "webp")
-- `background`: Background type ("default", "transparent")
-
-**Generation Control:**
-- `seed`: Random seed (integer or "auto")
-- `is_raw`: Whether to apply post-processing
-- `is_moderated`: Enable content moderation
-- `safety_tolerance`: Content safety level (1-6)
-
-### Input Requirements
-
-PipeImgGen requires exactly one input that must be either:
-- An `ImgGenPrompt` concept
-- A concept that refines `ImgGenPrompt`
-
-The input can be named anything but must contain the prompt text for image generation.
-
-## PipeFunc operator
-
-The PipeFunc operator is used to run custom Python functions within a pipeline. This allows integration of classic Python scripts and custom logic.
-
-### Basic Usage
-
-Simple Function Call:
-```plx
-[pipe.process_data]
-type = "PipeFunc"
-description = "Process data using custom function"
-inputs = { input_data = "DataType" }
-output = "ProcessedData"
-function_name = "process_data_function"
-```
-
-File Processing Example:
-```plx
-[pipe.read_file]
-type = "PipeFunc"
-description = "Read file content"
-inputs = { file_path = "FilePath" }
-output = "FileContent"
-function_name = "read_file_content"
-```
-
-### Key Parameters
-
-- `function_name`: Name of the Python function to call (must be registered in func_registry)
-
-### Function Requirements
-
-The Python function must:
-
-1. **Be registered** in the `func_registry`
-2. **Accept `working_memory`** as a parameter:
- ```python
- async def my_function(working_memory: WorkingMemory) -> StuffContent | list[StuffContent] | str:
- # Function implementation
- pass
- ```
-
-3. **Return appropriate types**:
- - `StuffContent`: Single content object
- - `list[StuffContent]`: Multiple content objects (becomes ListContent)
- - `str`: Simple string (becomes TextContent)
-
-### Function Registration
-
-Functions must be registered in the function registry before use:
-
-```python
-from pipelex.system.registries.func_registry import func_registry
-
-@func_registry.register("my_function_name")
-async def my_custom_function(working_memory: WorkingMemory) -> StuffContent:
- # Access inputs from working memory
- input_data = working_memory.get_stuff("input_name")
-
- # Process data
- result = process_logic(input_data.content)
-
- # Return result
- return MyResultContent(data=result)
-```
-
-### Working Memory Access
-
-Inside the function, access pipeline inputs through working memory:
-
-```python
-async def process_function(working_memory: WorkingMemory) -> TextContent:
- # Get input stuff by name
- input_stuff = working_memory.get_stuff("input_name")
-
- # Access the content
- input_content = input_stuff.content
-
- # Process and return
- processed_text = f"Processed: {input_content.text}"
- return TextContent(text=processed_text)
-```
-
----
-
-## Rules to choose LLM models used in PipeLLMs.
-
-### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-### Using an LLM Handle in a PipeLLM
-
-Here is an example of using a model to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
----
-
-ALWAYS RUN validation when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-- For a specific bundle/file: `pipelex validate path_to_file.plx`
-- For all pipelines: `pipelex validate all`
-- Remember: Ensure your Python virtual environment is activated (typically `.venv` for standard installations) before running `pipelex` commands.
-
-Then, create an example file to run the pipeline in the `examples` folder.
-But don't write documentation unless asked explicitly to.
diff --git a/.env.example b/.env.example
index 22b29d7..cbc7ad2 100644
--- a/.env.example
+++ b/.env.example
@@ -1 +1 @@
-PIPELEX_INFERENCE_API_KEY=
\ No newline at end of file
+PIPELEX_GATEWAY_API_KEY=
\ No newline at end of file
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
deleted file mode 100644
index 6125caa..0000000
--- a/.github/copilot-instructions.md
+++ /dev/null
@@ -1,1162 +0,0 @@
-
-# Pipelex Coding Rules
-
-## Guide to write or edit pipelines using the Pipelex language in .plx files
-
-- Always first write your "plan" in natural language, then transcribe it in pipelex.
-- You should ALWAYS RUN validation when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
- - For a specific file: `pipelex validate path_to_file.plx`
- - For all pipelines: `pipelex validate all`
- - **IMPORTANT**: Ensure the Python virtual environment is activated before running `pipelex` commands. For standard installations, the venv is named `.venv` - always check that first. The commands will not work without proper venv activation.
-- Please use POSIX standard for files. (empty lines, no trailing whitespaces, etc.)
-
-### Pipeline File Naming
-- Files must be `.plx` for pipelines (Always add an empty line at the end of the file, and do not add trailing whitespaces to PLX files at all)
-- Files must be `.py` for code defining the data structures
-- Use descriptive names in `snake_case`
-
-### Pipeline File Outline
-A pipeline file has three main sections:
-1. Domain statement
-2. Concept definitions
-3. Pipe definitions
-
-#### Domain Statement
-```plx
-domain = "domain_name"
-description = "Description of the domain" # Optional
-```
-Note: The domain name usually matches the plx filename for single-file domains. For multi-file domains, use the subdirectory name.
-
-#### Concept Definitions
-
-Concepts represent ideas and semantic entities in your pipeline. They define what something *is*, not how it's structured.
-
-```plx
-[concept]
-ConceptName = "Description of the concept"
-```
-
-**Naming Rules:**
-- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story") - lists are handled implicitly by Pipelex
-- Avoid circumstantial adjectives (no "LargeText", use "Text") - focus on the essence of what the concept represents
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number, Page, JSON)
-
-**Native Concepts:**
-Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`, `JSON`. Use these directly or refine them when appropriate.
-
-**Refining Native Concepts:**
-To create a concept that specializes a native concept without adding fields:
-
-```plx
-[concept.Landscape]
-description = "A scenic outdoor photograph"
-refines = "Image"
-```
-
-For details on how to structure concepts with fields, see the "Structuring Models" section below.
-
-#### Pipe Definitions
-
-### Pipe Base Definition
-
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-description = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
-
-The pipes will all have at least this base definition.
-- `inputs`: Dictionary of key being the variable used in the prompts, and the value being the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if PipeSequence) or of the conditional pipes (if PipeCondition).
-So If you have this error:
-`PipeValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['invoice']'``
-That means that the pipe validate_expense is missing the input `invoice` because one of the subpipe is needing it.
-
-NEVER WRITE THE INPUTS BY BREAKING THE LINE LIKE THIS:
-
-```plx
-inputs = {
- input_1 = "ConceptName1",
- input_2 = "ConceptName2"
-}
-```
-
-
-- `output`: The name of the concept to output. The `ConceptName` should have the same name as the python class if you want structured output:
-
-#### Input Multiplicity
-
-By default, inputs expect a single item. Use bracket notation to specify multiple items:
-
-```plx
-## Single item (default)
-inputs = { document = "Text" }
-
-## Variable list - indeterminate number of items
-inputs = { documents = "Text[]" }
-
-## Fixed count - exactly N items
-inputs = { comparison_items = "Image[2]" }
-```
-
-**Key points:**
-- No brackets = single item (default behavior)
-- Use `[]` for lists of unknown length
-- Use `[N]` (where N is an integer) when operation requires exact count (e.g., comparing 2 items)
-
-### Structuring Models
-
-Once you've defined your concepts semantically (see "Concept Definitions" above), you need to specify their structure if they have fields.
-
-#### Three Ways to Structure Concepts
-
-**1. No Structure Needed**
-
-If a concept only refines a native concept without adding fields, use the TOML table syntax shown in "Concept Definitions" above. No structure section is needed.
-
-**2. Inline Structure Definition (RECOMMENDED for most cases)**
-
-For concepts with structured fields, define them inline using TOML syntax:
-
-```plx
-[concept.Invoice]
-description = "A commercial document issued by a seller to a buyer"
-
-[concept.Invoice.structure]
-invoice_number = "The unique invoice identifier" # This will be optional by default
-issue_date = { type = "date", description = "The date the invoice was issued", required = true }
-total_amount = { type = "number", description = "The total invoice amount", required = true }
-vendor_name = "The name of the vendor" # This will be optional by default
-line_items = { type = "list", item_type = "text", description = "List of items" }
-```
-
-**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
-
-**Field properties:** `type`, `description`, `required` (default: false), `default_value`, `choices`, `item_type` (for lists), `key_type` and `value_type` (for dicts)
-
-**Simple syntax** (creates required text field):
-```plx
-field_name = "Field description"
-```
-
-**Detailed syntax** (with explicit properties):
-```plx
-field_name = { type = "text", description = "Field description", default_value = "default" }
-```
-
-**3. Python StructuredContent Class (For Advanced Features)**
-
-Create a Python class when you need:
-- Custom validation logic (@field_validator, @model_validator)
-- Computed properties (@property methods)
-- Custom methods or class methods
-- Complex cross-field validation
-- Reusable structures across multiple domains
-
-```python
-from pipelex.core.stuffs.structured_content import StructuredContent
-from pydantic import Field, field_validator
-
-class Invoice(StructuredContent):
- """A commercial invoice with validation."""
-
- invoice_number: str = Field(description="The unique invoice identifier")
- total_amount: float = Field(ge=0, description="The total invoice amount")
- tax_amount: float = Field(ge=0, description="Tax amount")
-
- @field_validator('tax_amount')
- @classmethod
- def validate_tax(cls, v, info):
- """Ensure tax doesn't exceed total."""
- total = info.data.get('total_amount', 0)
- if v > total:
- raise ValueError('Tax amount cannot exceed total amount')
- return v
-```
-
-**Location:** Create models in `my_project/some_domain/some_domain_struct.py`. Classes inheriting from `StructuredContent` are automatically discovered.
-
-#### Decision Rules for Agents
-
-**If concept already exists:**
-- If it's already inline → KEEP IT INLINE unless user explicitly asks to convert or features require Python class
-- If it's already a Python class → KEEP IT as Python class
-
-**If creating new concept:**
-1. Does it only refine a native concept without adding fields? → Use concept-only declaration
-2. Does it need custom validation, computed properties, or methods? → Use Python class
-3. Otherwise → Use inline structure (fastest and simplest)
-
-**When to suggest conversion to Python class:**
-- User needs validation logic beyond type checking
-- User needs computed properties or custom methods
-- Structure needs to be reused across multiple domains
-- Complex type relationships or inheritance required
-
-#### Inline Structure Limitations
-
-Inline structures:
-- ✅ Support all common field types (text, number, date, list, dict, etc.)
-- ✅ Support required/optional fields, defaults, choices
-- ✅ Generate full Pydantic models with validation
-- ❌ Cannot have custom validators or complex validation logic
-- ❌ Cannot have computed properties or custom methods
-- ❌ Cannot refine custom (non-native) concepts
-- ❌ Limited IDE autocomplete compared to explicit Python classes
-
-
-### Pipe Controllers and Pipe Operators
-
-Look at the Pipes we have in order to adapt it. Pipes are organized in two categories:
-
-1. **Controllers** - For flow control:
- - `PipeSequence` - For creating a sequence of multiple steps
- - `PipeCondition` - If the next pipe depends of the expression of a stuff in the working memory
- - `PipeParallel` - For parallelizing pipes
-
-2. **Operators** - For specific tasks:
- - `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeExtract` - Extract text and images from an image or a PDF
- - `PipeCompose` - For composing text using Jinja2 templates: supports html, markdown, mermaid, etc.
- - `PipeImgGen` - Generate Images
- - `PipeFunc` - For running classic python scripts
-
-### PipeSequence controller
-
-Purpose: PipeSequence executes multiple pipes in a defined order, where each step can use results from original inputs or from previous steps.
-
-#### Basic Definition
-```plx
-[pipe.your_sequence_name]
-type = "PipeSequence"
-description = "Description of what this sequence does"
-inputs = { input_name = "InputType" } # All the inputs of the sub pipes, except the ones generated by intermediate steps
-output = "OutputType"
-steps = [
- { pipe = "first_pipe", result = "first_result" },
- { pipe = "second_pipe", result = "second_result" },
- { pipe = "final_pipe", result = "final_result" }
-]
-```
-
-#### Key Components
-
-1. **Steps Array**: List of pipes to execute in sequence
- - `pipe`: Name of the pipe to execute
- - `result`: Name to assign to the pipe's output that will be in the working memory
-
-#### Using PipeBatch in Steps
-
-You can use PipeBatch functionality within steps using `batch_over` and `batch_as`:
-
-```plx
-steps = [
- { pipe = "process_items", batch_over = "input_list", batch_as = "current_item", result = "processed_items"
- }
-]
-```
-
-1. **batch_over**: Specifies a `ListContent` field to iterate over. Each item in the list will be processed individually and IN PARALLEL by the pipe.
- - Must be a `ListContent` type containing the items to process
- - Can reference inputs or results from previous steps
-
-2. **batch_as**: Defines the name that will be used to reference the current item being processed
- - This name can be used in the pipe's input mappings
- - Makes each item from the batch available as a single element
-
-The result of a batched step will be a `ListContent` containing the outputs from processing each item.
-
-### PipeCondition controller
-
-The PipeCondition controller allows you to implement conditional logic in your pipeline, choosing which pipe to execute based on an evaluated expression. It supports both direct expressions and expression templates.
-
-#### Basic usage
-
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression = "input_data.category"
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-or
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression_template = "{{ input_data.category }}" # Jinja2 code
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-
-#### Key Parameters
-
-- `expression`: Direct boolean or string expression (mutually exclusive with expression_template)
-- `expression_template`: Jinja2 template for more complex conditional logic (mutually exclusive with expression)
-- `outcomes`: Dictionary mapping expression results to pipe codes:
- 1. The key on the left (`small`, `medium`) is the result of `expression` or `expression_template`
- 2. The value on the right (`process_small`, `process_medium`, etc.) is the name of the pipe to trigger
-- `default_outcome`: **Required** - The pipe to execute if the expression doesn't match any key in outcomes. Use `"fail"` if you want the pipeline to fail when no match is found
-
-Example with fail as default:
-```plx
-[pipe.strict_validation]
-type = "PipeCondition"
-description = "Validate with strict matching"
-inputs = { status = "Status" }
-output = "Text"
-expression = "status.value"
-default_outcome = "fail"
-
-[pipe.strict_validation.outcomes]
-approved = "process_approved"
-rejected = "process_rejected"
-```
-
-### PipeLLM operator
-
-PipeLLM is used to:
-1. Generate text or objects with LLMs
-2. Process images with Vision LLMs
-
-#### Basic Usage
-
-Simple Text Generation:
-```plx
-[pipe.write_story]
-type = "PipeLLM"
-description = "Write a short story"
-output = "Text"
-prompt = """
-Write a short story about a programmer.
-"""
-```
-
-Structured Data Extraction:
-```plx
-[pipe.extract_info]
-type = "PipeLLM"
-description = "Extract information"
-inputs = { text = "Text" }
-output = "PersonInfo"
-prompt = """
-Extract person information from this text:
-@text
-"""
-```
-
-Supports system instructions:
-```plx
-[pipe.expert_analysis]
-type = "PipeLLM"
-description = "Expert analysis"
-output = "Analysis"
-system_prompt = "You are a data analysis expert"
-prompt = "Analyze this data"
-```
-
-#### Multiple Outputs
-
-Generate multiple outputs (fixed number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[3]" # Generate exactly 3 ideas
-prompt = "Generate 3 ideas"
-```
-
-Generate multiple outputs (variable number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[]" # Let the LLM decide how many to generate
-prompt = "Generate ideas"
-```
-
-#### Vision
-
-Process images with VLMs (image inputs must be tagged in the prompt):
-```plx
-[pipe.analyze_image]
-type = "PipeLLM"
-description = "Analyze image"
-inputs = { image = "Image" }
-output = "ImageAnalysis"
-prompt = """
-Describe what you see in this image:
-
-$image
-"""
-```
-
-You can also reference images inline in meaningful sentences to guide the Visual LLM:
-```plx
-[pipe.compare_images]
-type = "PipeLLM"
-description = "Compare two images"
-inputs = { photo = "Image", painting = "Image" }
-output = "Analysis"
-prompt = "Analyze the colors in $photo and the shapes in $painting."
-```
-
-#### Writing prompts for PipeLLM
-
-**Insert stuff inside a tagged block**
-
-If the inserted text is supposedly a long text, made of several lines or paragraphs, you want it inserted inside a block, possibly a block tagged and delimlited with proper syntax as one would do in a markdown documentation. To include stuff as a block, use the "@" prefix.
-
-Example template:
-```plx
-prompt = """
-Match the expense with its corresponding invoice:
-
-@expense
-
-@invoices
-"""
-```
-In the example above, the expense data and the invoices data are obviously made of several lines each, that's why it makes sense to use the "@" prefix in order to have them delimited inside a block. Note that our preprocessor will automatically include the block's title, so it doesn't need to be explicitly written in the prompt.
-
-DO NOT write things like "Here is the expense: @expense".
-DO write simply "@expense" alone in an isolated line.
-
-**Insert stuff inline**
-
-If the inserted text is short text and it makes sense to have it inserted directly into a sentence, you want it inserted inline. To insert stuff inline, use the "$" prefix. This will insert the stuff without delimiters and the content will be rendered as plain text.
-
-Example template:
-```plx
-prompt = """
-Your goal is to summarize everything related to $topic in the provided text:
-
-@text
-
-Please provide only the summary, with no additional text or explanations.
-Your summary should not be longer than 2 sentences.
-"""
-```
-
-In the example above, $topic will be inserted inline, whereas @text will be a a delimited block.
-Be sure to make the proper choice of prefix for each insertion.
-
-DO NOT write "$topic" alone in an isolated line.
-DO write things like "Write an essay about $topic" to include text into an actual sentence.
-
-
-### PipeExtract operator
-
-The PipeExtract operator is used to extract text and images from an image or a PDF
-
-#### Simple Text Extraction
-```plx
-[pipe.extract_info]
-type = "PipeExtract"
-description = "extract the information"
-inputs = { document = "PDF" } # or { image = "Image" } if it's an image. This is the only input.
-output = "Page"
-```
-
-Using Extract Model Settings:
-```plx
-[pipe.extract_with_model]
-type = "PipeExtract"
-description = "Extract with specific model"
-inputs = { document = "PDF" }
-output = "Page"
-model = "base_extract_mistral" # Use predefined extract preset or model alias
-```
-
-Only one input is allowed and it must either be an `Image` or a `PDF`. The input can be named anything.
-
-The output concept `Page` is a native concept, with the structure `PageContent`:
-It corresponds to 1 page. Therefore, the PipeExtract is outputing a `ListContent` of `Page`
-
-```python
-class TextAndImagesContent(StuffContent):
- text: TextContent | None
- images: list[ImageContent] | None
-
-class PageContent(StructuredContent): # CONCEPT IS "Page"
- text_and_images: TextAndImagesContent
- page_view: ImageContent | None = None
-```
-- `text_and_images` are the text, and the related images found in the input image or PDF.
-- `page_view` is the screenshot of the whole pdf page/image.
-
-### PipeCompose operator
-
-The PipeCompose operator is used to compose text using Jinja2 templates. It supports various output formats including HTML, Markdown, Mermaid diagrams, and more.
-
-#### Basic Usage
-
-Simple Template Composition:
-```plx
-[pipe.compose_report]
-type = "PipeCompose"
-description = "Compose a report using template"
-inputs = { data = "ReportData" }
-output = "Text"
-template = """
-## Report Summary
-
-Based on the analysis:
-$data
-
-Generated on: {{ current_date }}
-"""
-```
-
-Using Named Templates:
-```plx
-[pipe.use_template]
-type = "PipeCompose"
-description = "Use a predefined template"
-inputs = { content = "Text" }
-output = "Text"
-template_name = "standard_report_template"
-```
-
-Using Nested Template Section (for more control):
-```plx
-[pipe.advanced_template]
-type = "PipeCompose"
-description = "Use advanced template settings"
-inputs = { data = "ReportData" }
-output = "Text"
-
-[pipe.advanced_template.template]
-template = "Report: $data"
-category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-```
-
-CRM Email Template:
-```plx
-[pipe.compose_follow_up_email]
-type = "PipeCompose"
-description = "Compose a personalized follow-up email for CRM"
-inputs = { customer = "Customer", deal = "Deal", sales_rep = "SalesRep" }
-output = "Text"
-template_category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-template = """
-Subject: Following up on our $deal.product_name discussion
-
-Hi $customer.first_name,
-
-I hope this email finds you well! I wanted to follow up on our conversation about $deal.product_name from $deal.last_contact_date.
-
-Based on our discussion, I understand that your key requirements are: $deal.customer_requirements
-
-I'm excited to let you know that we can definitely help you achieve your goals. Here's what I'd like to propose:
-
-**Next Steps:**
-- Schedule a demo tailored to your specific needs
-- Provide you with a customized quote based on your requirements
-- Connect you with our implementation team
-
-Would you be available for a 30-minute call this week? I have openings on:
-{% for slot in available_slots %}
-- {{ slot }}
-{% endfor %}
-
-Looking forward to moving this forward together!
-
-Best regards,
-$sales_rep.name
-$sales_rep.title
-$sales_rep.phone | $sales_rep.email
-"""
-```
-
-#### Key Parameters
-
-- `template`: Inline template string (mutually exclusive with template_name)
-- `template_name`: Name of a predefined template (mutually exclusive with template)
-- `template_category`: Template type ("llm_prompt", "html", "markdown", "mermaid", etc.)
-- `templating_style`: Styling options for template rendering
-- `extra_context`: Additional context variables for template
-
-For more control, you can use a nested `template` section instead of the `template` field:
-- `template.template`: The template string
-- `template.category`: Template type
-- `template.templating_style`: Styling options
-
-#### Template Variables
-
-Use the same variable insertion rules as PipeLLM:
-- `@variable` for block insertion (multi-line content)
-- `$variable` for inline insertion (short text)
-
-### PipeImgGen operator
-
-The PipeImgGen operator is used to generate images using AI image generation models.
-
-#### Basic Usage
-
-Simple Image Generation:
-```plx
-[pipe.generate_image]
-type = "PipeImgGen"
-description = "Generate an image from prompt"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-```
-
-Using Image Generation Settings:
-```plx
-[pipe.generate_photo]
-type = "PipeImgGen"
-description = "Generate a high-quality photo"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Photo"
-model = { model = "fast-img-gen" }
-aspect_ratio = "16:9"
-quality = "hd"
-```
-
-Multiple Image Generation:
-```plx
-[pipe.generate_variations]
-type = "PipeImgGen"
-description = "Generate multiple image variations"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image[3]"
-seed = "auto"
-```
-
-Advanced Configuration:
-```plx
-[pipe.generate_custom]
-type = "PipeImgGen"
-description = "Generate image with custom settings"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-model = "img_gen_preset_name" # Use predefined preset
-aspect_ratio = "1:1"
-quality = "hd"
-background = "transparent"
-output_format = "png"
-is_raw = false
-safety_tolerance = 3
-```
-
-#### Key Parameters
-
-**Image Generation Settings:**
-- `model`: Model choice (preset name or inline settings with model name)
-- `quality`: Image quality ("standard", "hd")
-
-**Output Configuration:**
-- `aspect_ratio`: Image dimensions ("1:1", "16:9", "9:16", etc.)
-- `output_format`: File format ("png", "jpeg", "webp")
-- `background`: Background type ("default", "transparent")
-
-**Generation Control:**
-- `seed`: Random seed (integer or "auto")
-- `is_raw`: Whether to apply post-processing
-- `is_moderated`: Enable content moderation
-- `safety_tolerance`: Content safety level (1-6)
-
-#### Input Requirements
-
-PipeImgGen requires exactly one input that must be either:
-- An `ImgGenPrompt` concept
-- A concept that refines `ImgGenPrompt`
-
-The input can be named anything but must contain the prompt text for image generation.
-
-### PipeFunc operator
-
-The PipeFunc operator is used to run custom Python functions within a pipeline. This allows integration of classic Python scripts and custom logic.
-
-#### Basic Usage
-
-Simple Function Call:
-```plx
-[pipe.process_data]
-type = "PipeFunc"
-description = "Process data using custom function"
-inputs = { input_data = "DataType" }
-output = "ProcessedData"
-function_name = "process_data_function"
-```
-
-File Processing Example:
-```plx
-[pipe.read_file]
-type = "PipeFunc"
-description = "Read file content"
-inputs = { file_path = "FilePath" }
-output = "FileContent"
-function_name = "read_file_content"
-```
-
-#### Key Parameters
-
-- `function_name`: Name of the Python function to call (must be registered in func_registry)
-
-#### Function Requirements
-
-The Python function must:
-
-1. **Be registered** in the `func_registry`
-2. **Accept `working_memory`** as a parameter:
- ```python
- async def my_function(working_memory: WorkingMemory) -> StuffContent | list[StuffContent] | str:
- # Function implementation
- pass
- ```
-
-3. **Return appropriate types**:
- - `StuffContent`: Single content object
- - `list[StuffContent]`: Multiple content objects (becomes ListContent)
- - `str`: Simple string (becomes TextContent)
-
-#### Function Registration
-
-Functions must be registered in the function registry before use:
-
-```python
-from pipelex.system.registries.func_registry import func_registry
-
-@func_registry.register("my_function_name")
-async def my_custom_function(working_memory: WorkingMemory) -> StuffContent:
- # Access inputs from working memory
- input_data = working_memory.get_stuff("input_name")
-
- # Process data
- result = process_logic(input_data.content)
-
- # Return result
- return MyResultContent(data=result)
-```
-
-#### Working Memory Access
-
-Inside the function, access pipeline inputs through working memory:
-
-```python
-async def process_function(working_memory: WorkingMemory) -> TextContent:
- # Get input stuff by name
- input_stuff = working_memory.get_stuff("input_name")
-
- # Access the content
- input_content = input_stuff.content
-
- # Process and return
- processed_text = f"Processed: {input_content.text}"
- return TextContent(text=processed_text)
-```
-
----
-
-### Rules to choose LLM models used in PipeLLMs.
-
-#### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-#### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-#### Using an LLM Handle in a PipeLLM
-
-Here is an example of using a model to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-#### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
----
-
-ALWAYS RUN validation when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-- For a specific bundle/file: `pipelex validate path_to_file.plx`
-- For all pipelines: `pipelex validate all`
-- Remember: Ensure your Python virtual environment is activated (typically `.venv` for standard installations) before running `pipelex` commands.
-
-Then, create an example file to run the pipeline in the `examples` folder.
-But don't write documentation unless asked explicitly to.
-
-## Guide to execute a pipeline and write example code
-
-### Example to execute a pipeline with text output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-
-
-async def hello_world() -> str:
- """
- This function demonstrates the use of a super simple Pipelex pipeline to generate text.
- """
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="hello_world",
- )
-
- return pipe_output.main_stuff_as_str
-
-
-## start Pipelex
-Pipelex.make()
-## run sample using asyncio
-output_text = asyncio.run(hello_world())
-pretty_print(output_text, title="Your first Pipelex output")
-```
-
-### Example to execute a pipeline with structured output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-from pipelex.core.stuffs.image_content import ImageContent
-
-from my_project.gantt.gantt_struct import GanttChart
-
-SAMPLE_NAME = "extract_gantt"
-IMAGE_URL = "assets/gantt/gantt_tree_house.png"
-
-
-async def extract_gantt(image_url: str) -> GanttChart:
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
- # Output the result
- return pipe_output.main_stuff_as(content_type=GanttChart)
-
-
-## start Pipelex
-Pipelex.make()
-
-## run sample using asyncio
-gantt_chart = asyncio.run(extract_gantt(image_url=IMAGE_URL))
-pretty_print(gantt_chart, title="Gantt Chart")
-```
-
-### Setting up the input memory
-
-#### Explanation of input memory
-
-The input memory is a dictionary, where the key is the name of the input variable and the value provides details to make it a stuff object. The relevant definitions are:
-```python
-StuffContentOrData = dict[str, Any] | StuffContent | list[Any] | str
-PipelineInputs = dict[str, StuffContentOrData]
-```
-As you can seen, we made it so different ways can be used to define that stuff using structured content or data.
-
-#### Different ways to set up the input memory
-
-So here are a few concrete examples of calls to execute_pipeline with various ways to set up the input memory:
-
-```python
-## Here we have a single input and it's a Text.
-## If you assign a string, by default it will be considered as a TextContent.
- pipe_output = await execute_pipeline(
- pipe_code="master_advisory_orchestrator",
- inputs={
- "user_input": problem_description,
- },
- )
-
-## Here we have a single input and it's a PDF.
-## Because PDFContent is a native concept, we can use it directly as a value,
-## the system knows what content it corresponds to:
- pipe_output = await execute_pipeline(
- pipe_code="power_extractor_dpe",
- inputs={
- "document": PDFContent(url=pdf_url),
- },
- )
-
-## Here we have a single input and it's an Image.
-## Because ImageContent is a native concept, we can use it directly as a value:
- pipe_output = await execute_pipeline(
- pipe_code="fashion_variation_pipeline",
- inputs={
- "fashion_photo": ImageContent(url=image_url),
- },
- )
-
-## Here we have a single input, it's an image but
-## its actually a more specific concept gantt.GanttImage which refines Image,
-## so we must provide it using a dict with the concept and the content:
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
-
-## Here is a more complex example with multiple inputs assigned using different ways:
- pipe_output = await execute_pipeline(
- pipe_code="retrieve_then_answer",
- dynamic_output_concept_code="contracts.Fees",
- inputs={
- "text": load_text_from_path(path=text_path),
- "question": {
- "concept": "answer.Question",
- "content": question,
- },
- "client_instructions": client_instructions,
- },
- )
-```
-
-### Using the outputs of a pipeline
-
-All pipe executions return a `PipeOutput` object.
-It's a BaseModel which contains the resulting working memory at the end of the execution and the pipeline run id.
-It also provides a bunch of accessor functions and properties to unwrap the main stuff, which is the last stuff added to the working memory:
-
-```python
-
-class PipeOutput(BaseModel):
- working_memory: WorkingMemory = Field(default_factory=WorkingMemory)
- pipeline_run_id: str = Field(default=SpecialPipelineId.UNTITLED)
-
- @property
- def main_stuff(self) -> Stuff:
- ...
-
- def main_stuff_as_list(self, item_type: type[StuffContentType]) -> ListContent[StuffContentType]:
- ...
-
- def main_stuff_as_items(self, item_type: type[StuffContentType]) -> list[StuffContentType]:
- ...
-
- def main_stuff_as(self, content_type: type[StuffContentType]) -> StuffContentType:
- ...
-
- @property
- def main_stuff_as_text(self) -> TextContent:
- ...
-
- @property
- def main_stuff_as_str(self) -> str:
- ...
-
- @property
- def main_stuff_as_image(self) -> ImageContent:
- ...
-
- @property
- def main_stuff_as_text_and_image(self) -> TextAndImagesContent:
- ...
-
- @property
- def main_stuff_as_number(self) -> NumberContent:
- ...
-
- @property
- def main_stuff_as_html(self) -> HtmlContent:
- ...
-
- @property
- def main_stuff_as_mermaid(self) -> MermaidContent:
- ...
-```
-
-As you can see, you can extract any variable from the output working memory.
-
-#### Getting the main stuff as a specific type
-
-Simple text as a string:
-
-```python
-result = pipe_output.main_stuff_as_str
-```
-Structured object (BaseModel):
-
-```python
-result = pipe_output.main_stuff_as(content_type=GanttChart)
-```
-
-If it's a list, you can get a `ListContent` of the specific type.
-
-```python
-result_list_content = pipe_output.main_stuff_as_list(item_type=GanttChart)
-```
-
-or if you want, you can get the actual items as a regular python list:
-
-```python
-result_list = pipe_output.main_stuff_as_items(item_type=GanttChart)
-```
-
----
-
-## Rules to choose LLM models used in PipeLLMs.
-
-### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-### Using an LLM Handle in a PipeLLM
-
-Here is an example of using an llm_handle to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-4.5-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
diff --git a/.github/workflows/tests-check.yml b/.github/workflows/tests-check.yml
index c94ad16..a347c29 100644
--- a/.github/workflows/tests-check.yml
+++ b/.github/workflows/tests-check.yml
@@ -43,8 +43,5 @@ jobs:
source .venv/bin/activate
echo -e "y\nA\n1" |pipelex init
- - name: Boot test
- run: make tp TEST=TestFundamentals
-
- name: Run tests
run: make gha-tests
diff --git a/.gitignore b/.gitignore
index 29670a3..03b3f90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,7 +27,8 @@ dist/
# Results
results/
-# temps
temp/
pipelex_super.toml
-base_llm_deck.toml
+pipelex_override.toml
+telemetry_override.toml
+.pipelex/storage
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e908f73..371dd2e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,7 +7,6 @@
"mypy.runUsingActiveInterpreter": true,
"search.exclude": {
".mypy_cache/*": true,
- "**/dev_context": true
},
"files.exclude": {
"**/__pycache__": true,
@@ -22,6 +21,6 @@
"python.testing.pytestEnabled": true,
"djlint.showInstallError": false,
"files.associations": {
- "*.plx": "plx"
+ "*.mthds": "mthds"
}
}
\ No newline at end of file
diff --git a/.windsurfrules.md b/.windsurfrules.md
deleted file mode 100644
index 6125caa..0000000
--- a/.windsurfrules.md
+++ /dev/null
@@ -1,1162 +0,0 @@
-
-# Pipelex Coding Rules
-
-## Guide to write or edit pipelines using the Pipelex language in .plx files
-
-- Always first write your "plan" in natural language, then transcribe it in pipelex.
-- You should ALWAYS RUN validation when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
- - For a specific file: `pipelex validate path_to_file.plx`
- - For all pipelines: `pipelex validate all`
- - **IMPORTANT**: Ensure the Python virtual environment is activated before running `pipelex` commands. For standard installations, the venv is named `.venv` - always check that first. The commands will not work without proper venv activation.
-- Please use POSIX standard for files. (empty lines, no trailing whitespaces, etc.)
-
-### Pipeline File Naming
-- Files must be `.plx` for pipelines (Always add an empty line at the end of the file, and do not add trailing whitespaces to PLX files at all)
-- Files must be `.py` for code defining the data structures
-- Use descriptive names in `snake_case`
-
-### Pipeline File Outline
-A pipeline file has three main sections:
-1. Domain statement
-2. Concept definitions
-3. Pipe definitions
-
-#### Domain Statement
-```plx
-domain = "domain_name"
-description = "Description of the domain" # Optional
-```
-Note: The domain name usually matches the plx filename for single-file domains. For multi-file domains, use the subdirectory name.
-
-#### Concept Definitions
-
-Concepts represent ideas and semantic entities in your pipeline. They define what something *is*, not how it's structured.
-
-```plx
-[concept]
-ConceptName = "Description of the concept"
-```
-
-**Naming Rules:**
-- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story") - lists are handled implicitly by Pipelex
-- Avoid circumstantial adjectives (no "LargeText", use "Text") - focus on the essence of what the concept represents
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number, Page, JSON)
-
-**Native Concepts:**
-Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`, `JSON`. Use these directly or refine them when appropriate.
-
-**Refining Native Concepts:**
-To create a concept that specializes a native concept without adding fields:
-
-```plx
-[concept.Landscape]
-description = "A scenic outdoor photograph"
-refines = "Image"
-```
-
-For details on how to structure concepts with fields, see the "Structuring Models" section below.
-
-#### Pipe Definitions
-
-### Pipe Base Definition
-
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-description = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
-
-The pipes will all have at least this base definition.
-- `inputs`: Dictionary of key being the variable used in the prompts, and the value being the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if PipeSequence) or of the conditional pipes (if PipeCondition).
-So If you have this error:
-`PipeValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['invoice']'``
-That means that the pipe validate_expense is missing the input `invoice` because one of the subpipe is needing it.
-
-NEVER WRITE THE INPUTS BY BREAKING THE LINE LIKE THIS:
-
-```plx
-inputs = {
- input_1 = "ConceptName1",
- input_2 = "ConceptName2"
-}
-```
-
-
-- `output`: The name of the concept to output. The `ConceptName` should have the same name as the python class if you want structured output:
-
-#### Input Multiplicity
-
-By default, inputs expect a single item. Use bracket notation to specify multiple items:
-
-```plx
-## Single item (default)
-inputs = { document = "Text" }
-
-## Variable list - indeterminate number of items
-inputs = { documents = "Text[]" }
-
-## Fixed count - exactly N items
-inputs = { comparison_items = "Image[2]" }
-```
-
-**Key points:**
-- No brackets = single item (default behavior)
-- Use `[]` for lists of unknown length
-- Use `[N]` (where N is an integer) when operation requires exact count (e.g., comparing 2 items)
-
-### Structuring Models
-
-Once you've defined your concepts semantically (see "Concept Definitions" above), you need to specify their structure if they have fields.
-
-#### Three Ways to Structure Concepts
-
-**1. No Structure Needed**
-
-If a concept only refines a native concept without adding fields, use the TOML table syntax shown in "Concept Definitions" above. No structure section is needed.
-
-**2. Inline Structure Definition (RECOMMENDED for most cases)**
-
-For concepts with structured fields, define them inline using TOML syntax:
-
-```plx
-[concept.Invoice]
-description = "A commercial document issued by a seller to a buyer"
-
-[concept.Invoice.structure]
-invoice_number = "The unique invoice identifier" # This will be optional by default
-issue_date = { type = "date", description = "The date the invoice was issued", required = true }
-total_amount = { type = "number", description = "The total invoice amount", required = true }
-vendor_name = "The name of the vendor" # This will be optional by default
-line_items = { type = "list", item_type = "text", description = "List of items" }
-```
-
-**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
-
-**Field properties:** `type`, `description`, `required` (default: false), `default_value`, `choices`, `item_type` (for lists), `key_type` and `value_type` (for dicts)
-
-**Simple syntax** (creates required text field):
-```plx
-field_name = "Field description"
-```
-
-**Detailed syntax** (with explicit properties):
-```plx
-field_name = { type = "text", description = "Field description", default_value = "default" }
-```
-
-**3. Python StructuredContent Class (For Advanced Features)**
-
-Create a Python class when you need:
-- Custom validation logic (@field_validator, @model_validator)
-- Computed properties (@property methods)
-- Custom methods or class methods
-- Complex cross-field validation
-- Reusable structures across multiple domains
-
-```python
-from pipelex.core.stuffs.structured_content import StructuredContent
-from pydantic import Field, field_validator
-
-class Invoice(StructuredContent):
- """A commercial invoice with validation."""
-
- invoice_number: str = Field(description="The unique invoice identifier")
- total_amount: float = Field(ge=0, description="The total invoice amount")
- tax_amount: float = Field(ge=0, description="Tax amount")
-
- @field_validator('tax_amount')
- @classmethod
- def validate_tax(cls, v, info):
- """Ensure tax doesn't exceed total."""
- total = info.data.get('total_amount', 0)
- if v > total:
- raise ValueError('Tax amount cannot exceed total amount')
- return v
-```
-
-**Location:** Create models in `my_project/some_domain/some_domain_struct.py`. Classes inheriting from `StructuredContent` are automatically discovered.
-
-#### Decision Rules for Agents
-
-**If concept already exists:**
-- If it's already inline → KEEP IT INLINE unless user explicitly asks to convert or features require Python class
-- If it's already a Python class → KEEP IT as Python class
-
-**If creating new concept:**
-1. Does it only refine a native concept without adding fields? → Use concept-only declaration
-2. Does it need custom validation, computed properties, or methods? → Use Python class
-3. Otherwise → Use inline structure (fastest and simplest)
-
-**When to suggest conversion to Python class:**
-- User needs validation logic beyond type checking
-- User needs computed properties or custom methods
-- Structure needs to be reused across multiple domains
-- Complex type relationships or inheritance required
-
-#### Inline Structure Limitations
-
-Inline structures:
-- ✅ Support all common field types (text, number, date, list, dict, etc.)
-- ✅ Support required/optional fields, defaults, choices
-- ✅ Generate full Pydantic models with validation
-- ❌ Cannot have custom validators or complex validation logic
-- ❌ Cannot have computed properties or custom methods
-- ❌ Cannot refine custom (non-native) concepts
-- ❌ Limited IDE autocomplete compared to explicit Python classes
-
-
-### Pipe Controllers and Pipe Operators
-
-Look at the Pipes we have in order to adapt it. Pipes are organized in two categories:
-
-1. **Controllers** - For flow control:
- - `PipeSequence` - For creating a sequence of multiple steps
- - `PipeCondition` - If the next pipe depends of the expression of a stuff in the working memory
- - `PipeParallel` - For parallelizing pipes
-
-2. **Operators** - For specific tasks:
- - `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeExtract` - Extract text and images from an image or a PDF
- - `PipeCompose` - For composing text using Jinja2 templates: supports html, markdown, mermaid, etc.
- - `PipeImgGen` - Generate Images
- - `PipeFunc` - For running classic python scripts
-
-### PipeSequence controller
-
-Purpose: PipeSequence executes multiple pipes in a defined order, where each step can use results from original inputs or from previous steps.
-
-#### Basic Definition
-```plx
-[pipe.your_sequence_name]
-type = "PipeSequence"
-description = "Description of what this sequence does"
-inputs = { input_name = "InputType" } # All the inputs of the sub pipes, except the ones generated by intermediate steps
-output = "OutputType"
-steps = [
- { pipe = "first_pipe", result = "first_result" },
- { pipe = "second_pipe", result = "second_result" },
- { pipe = "final_pipe", result = "final_result" }
-]
-```
-
-#### Key Components
-
-1. **Steps Array**: List of pipes to execute in sequence
- - `pipe`: Name of the pipe to execute
- - `result`: Name to assign to the pipe's output that will be in the working memory
-
-#### Using PipeBatch in Steps
-
-You can use PipeBatch functionality within steps using `batch_over` and `batch_as`:
-
-```plx
-steps = [
- { pipe = "process_items", batch_over = "input_list", batch_as = "current_item", result = "processed_items"
- }
-]
-```
-
-1. **batch_over**: Specifies a `ListContent` field to iterate over. Each item in the list will be processed individually and IN PARALLEL by the pipe.
- - Must be a `ListContent` type containing the items to process
- - Can reference inputs or results from previous steps
-
-2. **batch_as**: Defines the name that will be used to reference the current item being processed
- - This name can be used in the pipe's input mappings
- - Makes each item from the batch available as a single element
-
-The result of a batched step will be a `ListContent` containing the outputs from processing each item.
-
-### PipeCondition controller
-
-The PipeCondition controller allows you to implement conditional logic in your pipeline, choosing which pipe to execute based on an evaluated expression. It supports both direct expressions and expression templates.
-
-#### Basic usage
-
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression = "input_data.category"
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-or
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression_template = "{{ input_data.category }}" # Jinja2 code
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-
-#### Key Parameters
-
-- `expression`: Direct boolean or string expression (mutually exclusive with expression_template)
-- `expression_template`: Jinja2 template for more complex conditional logic (mutually exclusive with expression)
-- `outcomes`: Dictionary mapping expression results to pipe codes:
- 1. The key on the left (`small`, `medium`) is the result of `expression` or `expression_template`
- 2. The value on the right (`process_small`, `process_medium`, etc.) is the name of the pipe to trigger
-- `default_outcome`: **Required** - The pipe to execute if the expression doesn't match any key in outcomes. Use `"fail"` if you want the pipeline to fail when no match is found
-
-Example with fail as default:
-```plx
-[pipe.strict_validation]
-type = "PipeCondition"
-description = "Validate with strict matching"
-inputs = { status = "Status" }
-output = "Text"
-expression = "status.value"
-default_outcome = "fail"
-
-[pipe.strict_validation.outcomes]
-approved = "process_approved"
-rejected = "process_rejected"
-```
-
-### PipeLLM operator
-
-PipeLLM is used to:
-1. Generate text or objects with LLMs
-2. Process images with Vision LLMs
-
-#### Basic Usage
-
-Simple Text Generation:
-```plx
-[pipe.write_story]
-type = "PipeLLM"
-description = "Write a short story"
-output = "Text"
-prompt = """
-Write a short story about a programmer.
-"""
-```
-
-Structured Data Extraction:
-```plx
-[pipe.extract_info]
-type = "PipeLLM"
-description = "Extract information"
-inputs = { text = "Text" }
-output = "PersonInfo"
-prompt = """
-Extract person information from this text:
-@text
-"""
-```
-
-Supports system instructions:
-```plx
-[pipe.expert_analysis]
-type = "PipeLLM"
-description = "Expert analysis"
-output = "Analysis"
-system_prompt = "You are a data analysis expert"
-prompt = "Analyze this data"
-```
-
-#### Multiple Outputs
-
-Generate multiple outputs (fixed number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[3]" # Generate exactly 3 ideas
-prompt = "Generate 3 ideas"
-```
-
-Generate multiple outputs (variable number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[]" # Let the LLM decide how many to generate
-prompt = "Generate ideas"
-```
-
-#### Vision
-
-Process images with VLMs (image inputs must be tagged in the prompt):
-```plx
-[pipe.analyze_image]
-type = "PipeLLM"
-description = "Analyze image"
-inputs = { image = "Image" }
-output = "ImageAnalysis"
-prompt = """
-Describe what you see in this image:
-
-$image
-"""
-```
-
-You can also reference images inline in meaningful sentences to guide the Visual LLM:
-```plx
-[pipe.compare_images]
-type = "PipeLLM"
-description = "Compare two images"
-inputs = { photo = "Image", painting = "Image" }
-output = "Analysis"
-prompt = "Analyze the colors in $photo and the shapes in $painting."
-```
-
-#### Writing prompts for PipeLLM
-
-**Insert stuff inside a tagged block**
-
-If the inserted text is supposedly a long text, made of several lines or paragraphs, you want it inserted inside a block, possibly a block tagged and delimlited with proper syntax as one would do in a markdown documentation. To include stuff as a block, use the "@" prefix.
-
-Example template:
-```plx
-prompt = """
-Match the expense with its corresponding invoice:
-
-@expense
-
-@invoices
-"""
-```
-In the example above, the expense data and the invoices data are obviously made of several lines each, that's why it makes sense to use the "@" prefix in order to have them delimited inside a block. Note that our preprocessor will automatically include the block's title, so it doesn't need to be explicitly written in the prompt.
-
-DO NOT write things like "Here is the expense: @expense".
-DO write simply "@expense" alone in an isolated line.
-
-**Insert stuff inline**
-
-If the inserted text is short text and it makes sense to have it inserted directly into a sentence, you want it inserted inline. To insert stuff inline, use the "$" prefix. This will insert the stuff without delimiters and the content will be rendered as plain text.
-
-Example template:
-```plx
-prompt = """
-Your goal is to summarize everything related to $topic in the provided text:
-
-@text
-
-Please provide only the summary, with no additional text or explanations.
-Your summary should not be longer than 2 sentences.
-"""
-```
-
-In the example above, $topic will be inserted inline, whereas @text will be a a delimited block.
-Be sure to make the proper choice of prefix for each insertion.
-
-DO NOT write "$topic" alone in an isolated line.
-DO write things like "Write an essay about $topic" to include text into an actual sentence.
-
-
-### PipeExtract operator
-
-The PipeExtract operator is used to extract text and images from an image or a PDF
-
-#### Simple Text Extraction
-```plx
-[pipe.extract_info]
-type = "PipeExtract"
-description = "extract the information"
-inputs = { document = "PDF" } # or { image = "Image" } if it's an image. This is the only input.
-output = "Page"
-```
-
-Using Extract Model Settings:
-```plx
-[pipe.extract_with_model]
-type = "PipeExtract"
-description = "Extract with specific model"
-inputs = { document = "PDF" }
-output = "Page"
-model = "base_extract_mistral" # Use predefined extract preset or model alias
-```
-
-Only one input is allowed and it must either be an `Image` or a `PDF`. The input can be named anything.
-
-The output concept `Page` is a native concept, with the structure `PageContent`:
-It corresponds to 1 page. Therefore, the PipeExtract is outputing a `ListContent` of `Page`
-
-```python
-class TextAndImagesContent(StuffContent):
- text: TextContent | None
- images: list[ImageContent] | None
-
-class PageContent(StructuredContent): # CONCEPT IS "Page"
- text_and_images: TextAndImagesContent
- page_view: ImageContent | None = None
-```
-- `text_and_images` are the text, and the related images found in the input image or PDF.
-- `page_view` is the screenshot of the whole pdf page/image.
-
-### PipeCompose operator
-
-The PipeCompose operator is used to compose text using Jinja2 templates. It supports various output formats including HTML, Markdown, Mermaid diagrams, and more.
-
-#### Basic Usage
-
-Simple Template Composition:
-```plx
-[pipe.compose_report]
-type = "PipeCompose"
-description = "Compose a report using template"
-inputs = { data = "ReportData" }
-output = "Text"
-template = """
-## Report Summary
-
-Based on the analysis:
-$data
-
-Generated on: {{ current_date }}
-"""
-```
-
-Using Named Templates:
-```plx
-[pipe.use_template]
-type = "PipeCompose"
-description = "Use a predefined template"
-inputs = { content = "Text" }
-output = "Text"
-template_name = "standard_report_template"
-```
-
-Using Nested Template Section (for more control):
-```plx
-[pipe.advanced_template]
-type = "PipeCompose"
-description = "Use advanced template settings"
-inputs = { data = "ReportData" }
-output = "Text"
-
-[pipe.advanced_template.template]
-template = "Report: $data"
-category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-```
-
-CRM Email Template:
-```plx
-[pipe.compose_follow_up_email]
-type = "PipeCompose"
-description = "Compose a personalized follow-up email for CRM"
-inputs = { customer = "Customer", deal = "Deal", sales_rep = "SalesRep" }
-output = "Text"
-template_category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-template = """
-Subject: Following up on our $deal.product_name discussion
-
-Hi $customer.first_name,
-
-I hope this email finds you well! I wanted to follow up on our conversation about $deal.product_name from $deal.last_contact_date.
-
-Based on our discussion, I understand that your key requirements are: $deal.customer_requirements
-
-I'm excited to let you know that we can definitely help you achieve your goals. Here's what I'd like to propose:
-
-**Next Steps:**
-- Schedule a demo tailored to your specific needs
-- Provide you with a customized quote based on your requirements
-- Connect you with our implementation team
-
-Would you be available for a 30-minute call this week? I have openings on:
-{% for slot in available_slots %}
-- {{ slot }}
-{% endfor %}
-
-Looking forward to moving this forward together!
-
-Best regards,
-$sales_rep.name
-$sales_rep.title
-$sales_rep.phone | $sales_rep.email
-"""
-```
-
-#### Key Parameters
-
-- `template`: Inline template string (mutually exclusive with template_name)
-- `template_name`: Name of a predefined template (mutually exclusive with template)
-- `template_category`: Template type ("llm_prompt", "html", "markdown", "mermaid", etc.)
-- `templating_style`: Styling options for template rendering
-- `extra_context`: Additional context variables for template
-
-For more control, you can use a nested `template` section instead of the `template` field:
-- `template.template`: The template string
-- `template.category`: Template type
-- `template.templating_style`: Styling options
-
-#### Template Variables
-
-Use the same variable insertion rules as PipeLLM:
-- `@variable` for block insertion (multi-line content)
-- `$variable` for inline insertion (short text)
-
-### PipeImgGen operator
-
-The PipeImgGen operator is used to generate images using AI image generation models.
-
-#### Basic Usage
-
-Simple Image Generation:
-```plx
-[pipe.generate_image]
-type = "PipeImgGen"
-description = "Generate an image from prompt"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-```
-
-Using Image Generation Settings:
-```plx
-[pipe.generate_photo]
-type = "PipeImgGen"
-description = "Generate a high-quality photo"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Photo"
-model = { model = "fast-img-gen" }
-aspect_ratio = "16:9"
-quality = "hd"
-```
-
-Multiple Image Generation:
-```plx
-[pipe.generate_variations]
-type = "PipeImgGen"
-description = "Generate multiple image variations"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image[3]"
-seed = "auto"
-```
-
-Advanced Configuration:
-```plx
-[pipe.generate_custom]
-type = "PipeImgGen"
-description = "Generate image with custom settings"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-model = "img_gen_preset_name" # Use predefined preset
-aspect_ratio = "1:1"
-quality = "hd"
-background = "transparent"
-output_format = "png"
-is_raw = false
-safety_tolerance = 3
-```
-
-#### Key Parameters
-
-**Image Generation Settings:**
-- `model`: Model choice (preset name or inline settings with model name)
-- `quality`: Image quality ("standard", "hd")
-
-**Output Configuration:**
-- `aspect_ratio`: Image dimensions ("1:1", "16:9", "9:16", etc.)
-- `output_format`: File format ("png", "jpeg", "webp")
-- `background`: Background type ("default", "transparent")
-
-**Generation Control:**
-- `seed`: Random seed (integer or "auto")
-- `is_raw`: Whether to apply post-processing
-- `is_moderated`: Enable content moderation
-- `safety_tolerance`: Content safety level (1-6)
-
-#### Input Requirements
-
-PipeImgGen requires exactly one input that must be either:
-- An `ImgGenPrompt` concept
-- A concept that refines `ImgGenPrompt`
-
-The input can be named anything but must contain the prompt text for image generation.
-
-### PipeFunc operator
-
-The PipeFunc operator is used to run custom Python functions within a pipeline. This allows integration of classic Python scripts and custom logic.
-
-#### Basic Usage
-
-Simple Function Call:
-```plx
-[pipe.process_data]
-type = "PipeFunc"
-description = "Process data using custom function"
-inputs = { input_data = "DataType" }
-output = "ProcessedData"
-function_name = "process_data_function"
-```
-
-File Processing Example:
-```plx
-[pipe.read_file]
-type = "PipeFunc"
-description = "Read file content"
-inputs = { file_path = "FilePath" }
-output = "FileContent"
-function_name = "read_file_content"
-```
-
-#### Key Parameters
-
-- `function_name`: Name of the Python function to call (must be registered in func_registry)
-
-#### Function Requirements
-
-The Python function must:
-
-1. **Be registered** in the `func_registry`
-2. **Accept `working_memory`** as a parameter:
- ```python
- async def my_function(working_memory: WorkingMemory) -> StuffContent | list[StuffContent] | str:
- # Function implementation
- pass
- ```
-
-3. **Return appropriate types**:
- - `StuffContent`: Single content object
- - `list[StuffContent]`: Multiple content objects (becomes ListContent)
- - `str`: Simple string (becomes TextContent)
-
-#### Function Registration
-
-Functions must be registered in the function registry before use:
-
-```python
-from pipelex.system.registries.func_registry import func_registry
-
-@func_registry.register("my_function_name")
-async def my_custom_function(working_memory: WorkingMemory) -> StuffContent:
- # Access inputs from working memory
- input_data = working_memory.get_stuff("input_name")
-
- # Process data
- result = process_logic(input_data.content)
-
- # Return result
- return MyResultContent(data=result)
-```
-
-#### Working Memory Access
-
-Inside the function, access pipeline inputs through working memory:
-
-```python
-async def process_function(working_memory: WorkingMemory) -> TextContent:
- # Get input stuff by name
- input_stuff = working_memory.get_stuff("input_name")
-
- # Access the content
- input_content = input_stuff.content
-
- # Process and return
- processed_text = f"Processed: {input_content.text}"
- return TextContent(text=processed_text)
-```
-
----
-
-### Rules to choose LLM models used in PipeLLMs.
-
-#### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-#### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-#### Using an LLM Handle in a PipeLLM
-
-Here is an example of using a model to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-#### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
----
-
-ALWAYS RUN validation when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-- For a specific bundle/file: `pipelex validate path_to_file.plx`
-- For all pipelines: `pipelex validate all`
-- Remember: Ensure your Python virtual environment is activated (typically `.venv` for standard installations) before running `pipelex` commands.
-
-Then, create an example file to run the pipeline in the `examples` folder.
-But don't write documentation unless asked explicitly to.
-
-## Guide to execute a pipeline and write example code
-
-### Example to execute a pipeline with text output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-
-
-async def hello_world() -> str:
- """
- This function demonstrates the use of a super simple Pipelex pipeline to generate text.
- """
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="hello_world",
- )
-
- return pipe_output.main_stuff_as_str
-
-
-## start Pipelex
-Pipelex.make()
-## run sample using asyncio
-output_text = asyncio.run(hello_world())
-pretty_print(output_text, title="Your first Pipelex output")
-```
-
-### Example to execute a pipeline with structured output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-from pipelex.core.stuffs.image_content import ImageContent
-
-from my_project.gantt.gantt_struct import GanttChart
-
-SAMPLE_NAME = "extract_gantt"
-IMAGE_URL = "assets/gantt/gantt_tree_house.png"
-
-
-async def extract_gantt(image_url: str) -> GanttChart:
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
- # Output the result
- return pipe_output.main_stuff_as(content_type=GanttChart)
-
-
-## start Pipelex
-Pipelex.make()
-
-## run sample using asyncio
-gantt_chart = asyncio.run(extract_gantt(image_url=IMAGE_URL))
-pretty_print(gantt_chart, title="Gantt Chart")
-```
-
-### Setting up the input memory
-
-#### Explanation of input memory
-
-The input memory is a dictionary, where the key is the name of the input variable and the value provides details to make it a stuff object. The relevant definitions are:
-```python
-StuffContentOrData = dict[str, Any] | StuffContent | list[Any] | str
-PipelineInputs = dict[str, StuffContentOrData]
-```
-As you can seen, we made it so different ways can be used to define that stuff using structured content or data.
-
-#### Different ways to set up the input memory
-
-So here are a few concrete examples of calls to execute_pipeline with various ways to set up the input memory:
-
-```python
-## Here we have a single input and it's a Text.
-## If you assign a string, by default it will be considered as a TextContent.
- pipe_output = await execute_pipeline(
- pipe_code="master_advisory_orchestrator",
- inputs={
- "user_input": problem_description,
- },
- )
-
-## Here we have a single input and it's a PDF.
-## Because PDFContent is a native concept, we can use it directly as a value,
-## the system knows what content it corresponds to:
- pipe_output = await execute_pipeline(
- pipe_code="power_extractor_dpe",
- inputs={
- "document": PDFContent(url=pdf_url),
- },
- )
-
-## Here we have a single input and it's an Image.
-## Because ImageContent is a native concept, we can use it directly as a value:
- pipe_output = await execute_pipeline(
- pipe_code="fashion_variation_pipeline",
- inputs={
- "fashion_photo": ImageContent(url=image_url),
- },
- )
-
-## Here we have a single input, it's an image but
-## its actually a more specific concept gantt.GanttImage which refines Image,
-## so we must provide it using a dict with the concept and the content:
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
-
-## Here is a more complex example with multiple inputs assigned using different ways:
- pipe_output = await execute_pipeline(
- pipe_code="retrieve_then_answer",
- dynamic_output_concept_code="contracts.Fees",
- inputs={
- "text": load_text_from_path(path=text_path),
- "question": {
- "concept": "answer.Question",
- "content": question,
- },
- "client_instructions": client_instructions,
- },
- )
-```
-
-### Using the outputs of a pipeline
-
-All pipe executions return a `PipeOutput` object.
-It's a BaseModel which contains the resulting working memory at the end of the execution and the pipeline run id.
-It also provides a bunch of accessor functions and properties to unwrap the main stuff, which is the last stuff added to the working memory:
-
-```python
-
-class PipeOutput(BaseModel):
- working_memory: WorkingMemory = Field(default_factory=WorkingMemory)
- pipeline_run_id: str = Field(default=SpecialPipelineId.UNTITLED)
-
- @property
- def main_stuff(self) -> Stuff:
- ...
-
- def main_stuff_as_list(self, item_type: type[StuffContentType]) -> ListContent[StuffContentType]:
- ...
-
- def main_stuff_as_items(self, item_type: type[StuffContentType]) -> list[StuffContentType]:
- ...
-
- def main_stuff_as(self, content_type: type[StuffContentType]) -> StuffContentType:
- ...
-
- @property
- def main_stuff_as_text(self) -> TextContent:
- ...
-
- @property
- def main_stuff_as_str(self) -> str:
- ...
-
- @property
- def main_stuff_as_image(self) -> ImageContent:
- ...
-
- @property
- def main_stuff_as_text_and_image(self) -> TextAndImagesContent:
- ...
-
- @property
- def main_stuff_as_number(self) -> NumberContent:
- ...
-
- @property
- def main_stuff_as_html(self) -> HtmlContent:
- ...
-
- @property
- def main_stuff_as_mermaid(self) -> MermaidContent:
- ...
-```
-
-As you can see, you can extract any variable from the output working memory.
-
-#### Getting the main stuff as a specific type
-
-Simple text as a string:
-
-```python
-result = pipe_output.main_stuff_as_str
-```
-Structured object (BaseModel):
-
-```python
-result = pipe_output.main_stuff_as(content_type=GanttChart)
-```
-
-If it's a list, you can get a `ListContent` of the specific type.
-
-```python
-result_list_content = pipe_output.main_stuff_as_list(item_type=GanttChart)
-```
-
-or if you want, you can get the actual items as a regular python list:
-
-```python
-result_list = pipe_output.main_stuff_as_items(item_type=GanttChart)
-```
-
----
-
-## Rules to choose LLM models used in PipeLLMs.
-
-### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-### Using an LLM Handle in a PipeLLM
-
-Here is an example of using an llm_handle to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-4.5-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index 6125caa..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,1162 +0,0 @@
-
-# Pipelex Coding Rules
-
-## Guide to write or edit pipelines using the Pipelex language in .plx files
-
-- Always first write your "plan" in natural language, then transcribe it in pipelex.
-- You should ALWAYS RUN validation when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
- - For a specific file: `pipelex validate path_to_file.plx`
- - For all pipelines: `pipelex validate all`
- - **IMPORTANT**: Ensure the Python virtual environment is activated before running `pipelex` commands. For standard installations, the venv is named `.venv` - always check that first. The commands will not work without proper venv activation.
-- Please use POSIX standard for files. (empty lines, no trailing whitespaces, etc.)
-
-### Pipeline File Naming
-- Files must be `.plx` for pipelines (Always add an empty line at the end of the file, and do not add trailing whitespaces to PLX files at all)
-- Files must be `.py` for code defining the data structures
-- Use descriptive names in `snake_case`
-
-### Pipeline File Outline
-A pipeline file has three main sections:
-1. Domain statement
-2. Concept definitions
-3. Pipe definitions
-
-#### Domain Statement
-```plx
-domain = "domain_name"
-description = "Description of the domain" # Optional
-```
-Note: The domain name usually matches the plx filename for single-file domains. For multi-file domains, use the subdirectory name.
-
-#### Concept Definitions
-
-Concepts represent ideas and semantic entities in your pipeline. They define what something *is*, not how it's structured.
-
-```plx
-[concept]
-ConceptName = "Description of the concept"
-```
-
-**Naming Rules:**
-- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story") - lists are handled implicitly by Pipelex
-- Avoid circumstantial adjectives (no "LargeText", use "Text") - focus on the essence of what the concept represents
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number, Page, JSON)
-
-**Native Concepts:**
-Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`, `JSON`. Use these directly or refine them when appropriate.
-
-**Refining Native Concepts:**
-To create a concept that specializes a native concept without adding fields:
-
-```plx
-[concept.Landscape]
-description = "A scenic outdoor photograph"
-refines = "Image"
-```
-
-For details on how to structure concepts with fields, see the "Structuring Models" section below.
-
-#### Pipe Definitions
-
-### Pipe Base Definition
-
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-description = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
-
-The pipes will all have at least this base definition.
-- `inputs`: Dictionary of key being the variable used in the prompts, and the value being the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if PipeSequence) or of the conditional pipes (if PipeCondition).
-So If you have this error:
-`PipeValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['invoice']'``
-That means that the pipe validate_expense is missing the input `invoice` because one of the subpipe is needing it.
-
-NEVER WRITE THE INPUTS BY BREAKING THE LINE LIKE THIS:
-
-```plx
-inputs = {
- input_1 = "ConceptName1",
- input_2 = "ConceptName2"
-}
-```
-
-
-- `output`: The name of the concept to output. The `ConceptName` should have the same name as the python class if you want structured output:
-
-#### Input Multiplicity
-
-By default, inputs expect a single item. Use bracket notation to specify multiple items:
-
-```plx
-## Single item (default)
-inputs = { document = "Text" }
-
-## Variable list - indeterminate number of items
-inputs = { documents = "Text[]" }
-
-## Fixed count - exactly N items
-inputs = { comparison_items = "Image[2]" }
-```
-
-**Key points:**
-- No brackets = single item (default behavior)
-- Use `[]` for lists of unknown length
-- Use `[N]` (where N is an integer) when operation requires exact count (e.g., comparing 2 items)
-
-### Structuring Models
-
-Once you've defined your concepts semantically (see "Concept Definitions" above), you need to specify their structure if they have fields.
-
-#### Three Ways to Structure Concepts
-
-**1. No Structure Needed**
-
-If a concept only refines a native concept without adding fields, use the TOML table syntax shown in "Concept Definitions" above. No structure section is needed.
-
-**2. Inline Structure Definition (RECOMMENDED for most cases)**
-
-For concepts with structured fields, define them inline using TOML syntax:
-
-```plx
-[concept.Invoice]
-description = "A commercial document issued by a seller to a buyer"
-
-[concept.Invoice.structure]
-invoice_number = "The unique invoice identifier" # This will be optional by default
-issue_date = { type = "date", description = "The date the invoice was issued", required = true }
-total_amount = { type = "number", description = "The total invoice amount", required = true }
-vendor_name = "The name of the vendor" # This will be optional by default
-line_items = { type = "list", item_type = "text", description = "List of items" }
-```
-
-**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
-
-**Field properties:** `type`, `description`, `required` (default: false), `default_value`, `choices`, `item_type` (for lists), `key_type` and `value_type` (for dicts)
-
-**Simple syntax** (creates required text field):
-```plx
-field_name = "Field description"
-```
-
-**Detailed syntax** (with explicit properties):
-```plx
-field_name = { type = "text", description = "Field description", default_value = "default" }
-```
-
-**3. Python StructuredContent Class (For Advanced Features)**
-
-Create a Python class when you need:
-- Custom validation logic (@field_validator, @model_validator)
-- Computed properties (@property methods)
-- Custom methods or class methods
-- Complex cross-field validation
-- Reusable structures across multiple domains
-
-```python
-from pipelex.core.stuffs.structured_content import StructuredContent
-from pydantic import Field, field_validator
-
-class Invoice(StructuredContent):
- """A commercial invoice with validation."""
-
- invoice_number: str = Field(description="The unique invoice identifier")
- total_amount: float = Field(ge=0, description="The total invoice amount")
- tax_amount: float = Field(ge=0, description="Tax amount")
-
- @field_validator('tax_amount')
- @classmethod
- def validate_tax(cls, v, info):
- """Ensure tax doesn't exceed total."""
- total = info.data.get('total_amount', 0)
- if v > total:
- raise ValueError('Tax amount cannot exceed total amount')
- return v
-```
-
-**Location:** Create models in `my_project/some_domain/some_domain_struct.py`. Classes inheriting from `StructuredContent` are automatically discovered.
-
-#### Decision Rules for Agents
-
-**If concept already exists:**
-- If it's already inline → KEEP IT INLINE unless user explicitly asks to convert or features require Python class
-- If it's already a Python class → KEEP IT as Python class
-
-**If creating new concept:**
-1. Does it only refine a native concept without adding fields? → Use concept-only declaration
-2. Does it need custom validation, computed properties, or methods? → Use Python class
-3. Otherwise → Use inline structure (fastest and simplest)
-
-**When to suggest conversion to Python class:**
-- User needs validation logic beyond type checking
-- User needs computed properties or custom methods
-- Structure needs to be reused across multiple domains
-- Complex type relationships or inheritance required
-
-#### Inline Structure Limitations
-
-Inline structures:
-- ✅ Support all common field types (text, number, date, list, dict, etc.)
-- ✅ Support required/optional fields, defaults, choices
-- ✅ Generate full Pydantic models with validation
-- ❌ Cannot have custom validators or complex validation logic
-- ❌ Cannot have computed properties or custom methods
-- ❌ Cannot refine custom (non-native) concepts
-- ❌ Limited IDE autocomplete compared to explicit Python classes
-
-
-### Pipe Controllers and Pipe Operators
-
-Look at the Pipes we have in order to adapt it. Pipes are organized in two categories:
-
-1. **Controllers** - For flow control:
- - `PipeSequence` - For creating a sequence of multiple steps
- - `PipeCondition` - If the next pipe depends of the expression of a stuff in the working memory
- - `PipeParallel` - For parallelizing pipes
-
-2. **Operators** - For specific tasks:
- - `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeExtract` - Extract text and images from an image or a PDF
- - `PipeCompose` - For composing text using Jinja2 templates: supports html, markdown, mermaid, etc.
- - `PipeImgGen` - Generate Images
- - `PipeFunc` - For running classic python scripts
-
-### PipeSequence controller
-
-Purpose: PipeSequence executes multiple pipes in a defined order, where each step can use results from original inputs or from previous steps.
-
-#### Basic Definition
-```plx
-[pipe.your_sequence_name]
-type = "PipeSequence"
-description = "Description of what this sequence does"
-inputs = { input_name = "InputType" } # All the inputs of the sub pipes, except the ones generated by intermediate steps
-output = "OutputType"
-steps = [
- { pipe = "first_pipe", result = "first_result" },
- { pipe = "second_pipe", result = "second_result" },
- { pipe = "final_pipe", result = "final_result" }
-]
-```
-
-#### Key Components
-
-1. **Steps Array**: List of pipes to execute in sequence
- - `pipe`: Name of the pipe to execute
- - `result`: Name to assign to the pipe's output that will be in the working memory
-
-#### Using PipeBatch in Steps
-
-You can use PipeBatch functionality within steps using `batch_over` and `batch_as`:
-
-```plx
-steps = [
- { pipe = "process_items", batch_over = "input_list", batch_as = "current_item", result = "processed_items"
- }
-]
-```
-
-1. **batch_over**: Specifies a `ListContent` field to iterate over. Each item in the list will be processed individually and IN PARALLEL by the pipe.
- - Must be a `ListContent` type containing the items to process
- - Can reference inputs or results from previous steps
-
-2. **batch_as**: Defines the name that will be used to reference the current item being processed
- - This name can be used in the pipe's input mappings
- - Makes each item from the batch available as a single element
-
-The result of a batched step will be a `ListContent` containing the outputs from processing each item.
-
-### PipeCondition controller
-
-The PipeCondition controller allows you to implement conditional logic in your pipeline, choosing which pipe to execute based on an evaluated expression. It supports both direct expressions and expression templates.
-
-#### Basic usage
-
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression = "input_data.category"
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-or
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression_template = "{{ input_data.category }}" # Jinja2 code
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-
-#### Key Parameters
-
-- `expression`: Direct boolean or string expression (mutually exclusive with expression_template)
-- `expression_template`: Jinja2 template for more complex conditional logic (mutually exclusive with expression)
-- `outcomes`: Dictionary mapping expression results to pipe codes:
- 1. The key on the left (`small`, `medium`) is the result of `expression` or `expression_template`
- 2. The value on the right (`process_small`, `process_medium`, etc.) is the name of the pipe to trigger
-- `default_outcome`: **Required** - The pipe to execute if the expression doesn't match any key in outcomes. Use `"fail"` if you want the pipeline to fail when no match is found
-
-Example with fail as default:
-```plx
-[pipe.strict_validation]
-type = "PipeCondition"
-description = "Validate with strict matching"
-inputs = { status = "Status" }
-output = "Text"
-expression = "status.value"
-default_outcome = "fail"
-
-[pipe.strict_validation.outcomes]
-approved = "process_approved"
-rejected = "process_rejected"
-```
-
-### PipeLLM operator
-
-PipeLLM is used to:
-1. Generate text or objects with LLMs
-2. Process images with Vision LLMs
-
-#### Basic Usage
-
-Simple Text Generation:
-```plx
-[pipe.write_story]
-type = "PipeLLM"
-description = "Write a short story"
-output = "Text"
-prompt = """
-Write a short story about a programmer.
-"""
-```
-
-Structured Data Extraction:
-```plx
-[pipe.extract_info]
-type = "PipeLLM"
-description = "Extract information"
-inputs = { text = "Text" }
-output = "PersonInfo"
-prompt = """
-Extract person information from this text:
-@text
-"""
-```
-
-Supports system instructions:
-```plx
-[pipe.expert_analysis]
-type = "PipeLLM"
-description = "Expert analysis"
-output = "Analysis"
-system_prompt = "You are a data analysis expert"
-prompt = "Analyze this data"
-```
-
-#### Multiple Outputs
-
-Generate multiple outputs (fixed number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[3]" # Generate exactly 3 ideas
-prompt = "Generate 3 ideas"
-```
-
-Generate multiple outputs (variable number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[]" # Let the LLM decide how many to generate
-prompt = "Generate ideas"
-```
-
-#### Vision
-
-Process images with VLMs (image inputs must be tagged in the prompt):
-```plx
-[pipe.analyze_image]
-type = "PipeLLM"
-description = "Analyze image"
-inputs = { image = "Image" }
-output = "ImageAnalysis"
-prompt = """
-Describe what you see in this image:
-
-$image
-"""
-```
-
-You can also reference images inline in meaningful sentences to guide the Visual LLM:
-```plx
-[pipe.compare_images]
-type = "PipeLLM"
-description = "Compare two images"
-inputs = { photo = "Image", painting = "Image" }
-output = "Analysis"
-prompt = "Analyze the colors in $photo and the shapes in $painting."
-```
-
-#### Writing prompts for PipeLLM
-
-**Insert stuff inside a tagged block**
-
-If the inserted text is supposedly a long text, made of several lines or paragraphs, you want it inserted inside a block, possibly a block tagged and delimlited with proper syntax as one would do in a markdown documentation. To include stuff as a block, use the "@" prefix.
-
-Example template:
-```plx
-prompt = """
-Match the expense with its corresponding invoice:
-
-@expense
-
-@invoices
-"""
-```
-In the example above, the expense data and the invoices data are obviously made of several lines each, that's why it makes sense to use the "@" prefix in order to have them delimited inside a block. Note that our preprocessor will automatically include the block's title, so it doesn't need to be explicitly written in the prompt.
-
-DO NOT write things like "Here is the expense: @expense".
-DO write simply "@expense" alone in an isolated line.
-
-**Insert stuff inline**
-
-If the inserted text is short text and it makes sense to have it inserted directly into a sentence, you want it inserted inline. To insert stuff inline, use the "$" prefix. This will insert the stuff without delimiters and the content will be rendered as plain text.
-
-Example template:
-```plx
-prompt = """
-Your goal is to summarize everything related to $topic in the provided text:
-
-@text
-
-Please provide only the summary, with no additional text or explanations.
-Your summary should not be longer than 2 sentences.
-"""
-```
-
-In the example above, $topic will be inserted inline, whereas @text will be a a delimited block.
-Be sure to make the proper choice of prefix for each insertion.
-
-DO NOT write "$topic" alone in an isolated line.
-DO write things like "Write an essay about $topic" to include text into an actual sentence.
-
-
-### PipeExtract operator
-
-The PipeExtract operator is used to extract text and images from an image or a PDF
-
-#### Simple Text Extraction
-```plx
-[pipe.extract_info]
-type = "PipeExtract"
-description = "extract the information"
-inputs = { document = "PDF" } # or { image = "Image" } if it's an image. This is the only input.
-output = "Page"
-```
-
-Using Extract Model Settings:
-```plx
-[pipe.extract_with_model]
-type = "PipeExtract"
-description = "Extract with specific model"
-inputs = { document = "PDF" }
-output = "Page"
-model = "base_extract_mistral" # Use predefined extract preset or model alias
-```
-
-Only one input is allowed and it must either be an `Image` or a `PDF`. The input can be named anything.
-
-The output concept `Page` is a native concept, with the structure `PageContent`:
-It corresponds to 1 page. Therefore, the PipeExtract is outputing a `ListContent` of `Page`
-
-```python
-class TextAndImagesContent(StuffContent):
- text: TextContent | None
- images: list[ImageContent] | None
-
-class PageContent(StructuredContent): # CONCEPT IS "Page"
- text_and_images: TextAndImagesContent
- page_view: ImageContent | None = None
-```
-- `text_and_images` are the text, and the related images found in the input image or PDF.
-- `page_view` is the screenshot of the whole pdf page/image.
-
-### PipeCompose operator
-
-The PipeCompose operator is used to compose text using Jinja2 templates. It supports various output formats including HTML, Markdown, Mermaid diagrams, and more.
-
-#### Basic Usage
-
-Simple Template Composition:
-```plx
-[pipe.compose_report]
-type = "PipeCompose"
-description = "Compose a report using template"
-inputs = { data = "ReportData" }
-output = "Text"
-template = """
-## Report Summary
-
-Based on the analysis:
-$data
-
-Generated on: {{ current_date }}
-"""
-```
-
-Using Named Templates:
-```plx
-[pipe.use_template]
-type = "PipeCompose"
-description = "Use a predefined template"
-inputs = { content = "Text" }
-output = "Text"
-template_name = "standard_report_template"
-```
-
-Using Nested Template Section (for more control):
-```plx
-[pipe.advanced_template]
-type = "PipeCompose"
-description = "Use advanced template settings"
-inputs = { data = "ReportData" }
-output = "Text"
-
-[pipe.advanced_template.template]
-template = "Report: $data"
-category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-```
-
-CRM Email Template:
-```plx
-[pipe.compose_follow_up_email]
-type = "PipeCompose"
-description = "Compose a personalized follow-up email for CRM"
-inputs = { customer = "Customer", deal = "Deal", sales_rep = "SalesRep" }
-output = "Text"
-template_category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-template = """
-Subject: Following up on our $deal.product_name discussion
-
-Hi $customer.first_name,
-
-I hope this email finds you well! I wanted to follow up on our conversation about $deal.product_name from $deal.last_contact_date.
-
-Based on our discussion, I understand that your key requirements are: $deal.customer_requirements
-
-I'm excited to let you know that we can definitely help you achieve your goals. Here's what I'd like to propose:
-
-**Next Steps:**
-- Schedule a demo tailored to your specific needs
-- Provide you with a customized quote based on your requirements
-- Connect you with our implementation team
-
-Would you be available for a 30-minute call this week? I have openings on:
-{% for slot in available_slots %}
-- {{ slot }}
-{% endfor %}
-
-Looking forward to moving this forward together!
-
-Best regards,
-$sales_rep.name
-$sales_rep.title
-$sales_rep.phone | $sales_rep.email
-"""
-```
-
-#### Key Parameters
-
-- `template`: Inline template string (mutually exclusive with template_name)
-- `template_name`: Name of a predefined template (mutually exclusive with template)
-- `template_category`: Template type ("llm_prompt", "html", "markdown", "mermaid", etc.)
-- `templating_style`: Styling options for template rendering
-- `extra_context`: Additional context variables for template
-
-For more control, you can use a nested `template` section instead of the `template` field:
-- `template.template`: The template string
-- `template.category`: Template type
-- `template.templating_style`: Styling options
-
-#### Template Variables
-
-Use the same variable insertion rules as PipeLLM:
-- `@variable` for block insertion (multi-line content)
-- `$variable` for inline insertion (short text)
-
-### PipeImgGen operator
-
-The PipeImgGen operator is used to generate images using AI image generation models.
-
-#### Basic Usage
-
-Simple Image Generation:
-```plx
-[pipe.generate_image]
-type = "PipeImgGen"
-description = "Generate an image from prompt"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-```
-
-Using Image Generation Settings:
-```plx
-[pipe.generate_photo]
-type = "PipeImgGen"
-description = "Generate a high-quality photo"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Photo"
-model = { model = "fast-img-gen" }
-aspect_ratio = "16:9"
-quality = "hd"
-```
-
-Multiple Image Generation:
-```plx
-[pipe.generate_variations]
-type = "PipeImgGen"
-description = "Generate multiple image variations"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image[3]"
-seed = "auto"
-```
-
-Advanced Configuration:
-```plx
-[pipe.generate_custom]
-type = "PipeImgGen"
-description = "Generate image with custom settings"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-model = "img_gen_preset_name" # Use predefined preset
-aspect_ratio = "1:1"
-quality = "hd"
-background = "transparent"
-output_format = "png"
-is_raw = false
-safety_tolerance = 3
-```
-
-#### Key Parameters
-
-**Image Generation Settings:**
-- `model`: Model choice (preset name or inline settings with model name)
-- `quality`: Image quality ("standard", "hd")
-
-**Output Configuration:**
-- `aspect_ratio`: Image dimensions ("1:1", "16:9", "9:16", etc.)
-- `output_format`: File format ("png", "jpeg", "webp")
-- `background`: Background type ("default", "transparent")
-
-**Generation Control:**
-- `seed`: Random seed (integer or "auto")
-- `is_raw`: Whether to apply post-processing
-- `is_moderated`: Enable content moderation
-- `safety_tolerance`: Content safety level (1-6)
-
-#### Input Requirements
-
-PipeImgGen requires exactly one input that must be either:
-- An `ImgGenPrompt` concept
-- A concept that refines `ImgGenPrompt`
-
-The input can be named anything but must contain the prompt text for image generation.
-
-### PipeFunc operator
-
-The PipeFunc operator is used to run custom Python functions within a pipeline. This allows integration of classic Python scripts and custom logic.
-
-#### Basic Usage
-
-Simple Function Call:
-```plx
-[pipe.process_data]
-type = "PipeFunc"
-description = "Process data using custom function"
-inputs = { input_data = "DataType" }
-output = "ProcessedData"
-function_name = "process_data_function"
-```
-
-File Processing Example:
-```plx
-[pipe.read_file]
-type = "PipeFunc"
-description = "Read file content"
-inputs = { file_path = "FilePath" }
-output = "FileContent"
-function_name = "read_file_content"
-```
-
-#### Key Parameters
-
-- `function_name`: Name of the Python function to call (must be registered in func_registry)
-
-#### Function Requirements
-
-The Python function must:
-
-1. **Be registered** in the `func_registry`
-2. **Accept `working_memory`** as a parameter:
- ```python
- async def my_function(working_memory: WorkingMemory) -> StuffContent | list[StuffContent] | str:
- # Function implementation
- pass
- ```
-
-3. **Return appropriate types**:
- - `StuffContent`: Single content object
- - `list[StuffContent]`: Multiple content objects (becomes ListContent)
- - `str`: Simple string (becomes TextContent)
-
-#### Function Registration
-
-Functions must be registered in the function registry before use:
-
-```python
-from pipelex.system.registries.func_registry import func_registry
-
-@func_registry.register("my_function_name")
-async def my_custom_function(working_memory: WorkingMemory) -> StuffContent:
- # Access inputs from working memory
- input_data = working_memory.get_stuff("input_name")
-
- # Process data
- result = process_logic(input_data.content)
-
- # Return result
- return MyResultContent(data=result)
-```
-
-#### Working Memory Access
-
-Inside the function, access pipeline inputs through working memory:
-
-```python
-async def process_function(working_memory: WorkingMemory) -> TextContent:
- # Get input stuff by name
- input_stuff = working_memory.get_stuff("input_name")
-
- # Access the content
- input_content = input_stuff.content
-
- # Process and return
- processed_text = f"Processed: {input_content.text}"
- return TextContent(text=processed_text)
-```
-
----
-
-### Rules to choose LLM models used in PipeLLMs.
-
-#### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-#### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-#### Using an LLM Handle in a PipeLLM
-
-Here is an example of using a model to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-#### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
----
-
-ALWAYS RUN validation when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-- For a specific bundle/file: `pipelex validate path_to_file.plx`
-- For all pipelines: `pipelex validate all`
-- Remember: Ensure your Python virtual environment is activated (typically `.venv` for standard installations) before running `pipelex` commands.
-
-Then, create an example file to run the pipeline in the `examples` folder.
-But don't write documentation unless asked explicitly to.
-
-## Guide to execute a pipeline and write example code
-
-### Example to execute a pipeline with text output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-
-
-async def hello_world() -> str:
- """
- This function demonstrates the use of a super simple Pipelex pipeline to generate text.
- """
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="hello_world",
- )
-
- return pipe_output.main_stuff_as_str
-
-
-## start Pipelex
-Pipelex.make()
-## run sample using asyncio
-output_text = asyncio.run(hello_world())
-pretty_print(output_text, title="Your first Pipelex output")
-```
-
-### Example to execute a pipeline with structured output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-from pipelex.core.stuffs.image_content import ImageContent
-
-from my_project.gantt.gantt_struct import GanttChart
-
-SAMPLE_NAME = "extract_gantt"
-IMAGE_URL = "assets/gantt/gantt_tree_house.png"
-
-
-async def extract_gantt(image_url: str) -> GanttChart:
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
- # Output the result
- return pipe_output.main_stuff_as(content_type=GanttChart)
-
-
-## start Pipelex
-Pipelex.make()
-
-## run sample using asyncio
-gantt_chart = asyncio.run(extract_gantt(image_url=IMAGE_URL))
-pretty_print(gantt_chart, title="Gantt Chart")
-```
-
-### Setting up the input memory
-
-#### Explanation of input memory
-
-The input memory is a dictionary, where the key is the name of the input variable and the value provides details to make it a stuff object. The relevant definitions are:
-```python
-StuffContentOrData = dict[str, Any] | StuffContent | list[Any] | str
-PipelineInputs = dict[str, StuffContentOrData]
-```
-As you can seen, we made it so different ways can be used to define that stuff using structured content or data.
-
-#### Different ways to set up the input memory
-
-So here are a few concrete examples of calls to execute_pipeline with various ways to set up the input memory:
-
-```python
-## Here we have a single input and it's a Text.
-## If you assign a string, by default it will be considered as a TextContent.
- pipe_output = await execute_pipeline(
- pipe_code="master_advisory_orchestrator",
- inputs={
- "user_input": problem_description,
- },
- )
-
-## Here we have a single input and it's a PDF.
-## Because PDFContent is a native concept, we can use it directly as a value,
-## the system knows what content it corresponds to:
- pipe_output = await execute_pipeline(
- pipe_code="power_extractor_dpe",
- inputs={
- "document": PDFContent(url=pdf_url),
- },
- )
-
-## Here we have a single input and it's an Image.
-## Because ImageContent is a native concept, we can use it directly as a value:
- pipe_output = await execute_pipeline(
- pipe_code="fashion_variation_pipeline",
- inputs={
- "fashion_photo": ImageContent(url=image_url),
- },
- )
-
-## Here we have a single input, it's an image but
-## its actually a more specific concept gantt.GanttImage which refines Image,
-## so we must provide it using a dict with the concept and the content:
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
-
-## Here is a more complex example with multiple inputs assigned using different ways:
- pipe_output = await execute_pipeline(
- pipe_code="retrieve_then_answer",
- dynamic_output_concept_code="contracts.Fees",
- inputs={
- "text": load_text_from_path(path=text_path),
- "question": {
- "concept": "answer.Question",
- "content": question,
- },
- "client_instructions": client_instructions,
- },
- )
-```
-
-### Using the outputs of a pipeline
-
-All pipe executions return a `PipeOutput` object.
-It's a BaseModel which contains the resulting working memory at the end of the execution and the pipeline run id.
-It also provides a bunch of accessor functions and properties to unwrap the main stuff, which is the last stuff added to the working memory:
-
-```python
-
-class PipeOutput(BaseModel):
- working_memory: WorkingMemory = Field(default_factory=WorkingMemory)
- pipeline_run_id: str = Field(default=SpecialPipelineId.UNTITLED)
-
- @property
- def main_stuff(self) -> Stuff:
- ...
-
- def main_stuff_as_list(self, item_type: type[StuffContentType]) -> ListContent[StuffContentType]:
- ...
-
- def main_stuff_as_items(self, item_type: type[StuffContentType]) -> list[StuffContentType]:
- ...
-
- def main_stuff_as(self, content_type: type[StuffContentType]) -> StuffContentType:
- ...
-
- @property
- def main_stuff_as_text(self) -> TextContent:
- ...
-
- @property
- def main_stuff_as_str(self) -> str:
- ...
-
- @property
- def main_stuff_as_image(self) -> ImageContent:
- ...
-
- @property
- def main_stuff_as_text_and_image(self) -> TextAndImagesContent:
- ...
-
- @property
- def main_stuff_as_number(self) -> NumberContent:
- ...
-
- @property
- def main_stuff_as_html(self) -> HtmlContent:
- ...
-
- @property
- def main_stuff_as_mermaid(self) -> MermaidContent:
- ...
-```
-
-As you can see, you can extract any variable from the output working memory.
-
-#### Getting the main stuff as a specific type
-
-Simple text as a string:
-
-```python
-result = pipe_output.main_stuff_as_str
-```
-Structured object (BaseModel):
-
-```python
-result = pipe_output.main_stuff_as(content_type=GanttChart)
-```
-
-If it's a list, you can get a `ListContent` of the specific type.
-
-```python
-result_list_content = pipe_output.main_stuff_as_list(item_type=GanttChart)
-```
-
-or if you want, you can get the actual items as a regular python list:
-
-```python
-result_list = pipe_output.main_stuff_as_items(item_type=GanttChart)
-```
-
----
-
-## Rules to choose LLM models used in PipeLLMs.
-
-### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-### Using an LLM Handle in a PipeLLM
-
-Here is an example of using an llm_handle to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-4.5-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index ca6563b..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# Changelog
-
-## [v0.6.7] - 2025-12-01
-
-- Bump `pipelex` to `v0.17.3`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-
-## [v0.6.6] - 2025-12-01
-
-- Bump `pipelex` to `v0.17.2`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-
-## [v0.6.5] - 2025-11-26
-
-- Bump `pipelex` to `v0.17.1`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-
-## [v0.6.4] - 2025-11-18
-
-- Bump `pipelex` to `v0.15.7` to fix `pipelex doctor`
-
-## [v0.6.3] - 2025-11-18
-
-- Bump `pipelex` to `v0.15.6`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-- Bump `pytest` to `v9.0.1` (fix typo in `pyproject.toml`)
-
-## [v0.6.2] - 2025-11-13
-
-- Update agent rules
-
-## [v0.6.1] - 2025-11-13
-
-- Bump `pipelex` to `v0.15.4`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-- Update pytest to `v0.9.1`
-
-## [v0.6.0] - 2025-11-07
-
- - Upgraded `pipelex` dependency from `0.14.0` to `0.15.2`
-
-## [v0.5.1] - 2025-10-28
-
-- Updated README.md instructions
-
-## [v0.5.0] - 2025-10-27
-
-- Bump `pipelex` to `v0.14.0`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-
-## [v0.4.0] - 2025-10-21
-
-- Bump `pipelex` to `v0.13.0`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-
-## [v0.3.0] - 2025-10-15
-
-- Bump `pipelex` to `v0.12.0`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-- **Dependency Management**: Added `requirements.txt` and `requirements-dev.txt` with corresponding `Makefile` commands for dependency export
-
-## [v0.2.4] - 2025-09-19
-
-- Stop ignoring backend config
-- Add `gpt-4o` model to `pipelex_inference.toml`
-
-## [v0.2.3] - 2025-09-19
-
-- Fixed Hello World's call to execute_pipeline
-- Added proper e2e test for Hello World
-
-## [v0.2.2] - 2025-09-18
-
-- Bump `pipelex` to `v0.10.2`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-- Remove ugly code from `conftest.py` for unit test env var placeholders, now uses the proper fixture defined in `pipelex.test_extras.shared_pytest_plugins`
-
-## [v0.2.1] - 2025-09-17
-
-- Cleanup env example
-
-## [v0.2.0] - 2025-09-17
-
-- Bump `pipelex` to `v0.10.1`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-- Add `cleanlibraries` and `cleanconfig` make commands
-
-## [v0.1.4] - 2025-09-07
-
-- Make it easier to get running with BlackboxAI LLMs
-
-## [v0.1.3] - 2025-09-06
-
-- Bump `pipelex` to `v0.9.4`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-
-## [v0.1.2] - 2025-09-06
-
-- Better support for Pipelex extension in BlackboxAI IDE
-
-## [v0.1.1] - 2025-09-04
-
-- Updated pipelex Cursor rules
-
-## [v0.1.0] - 2025-09-03
-
-- Bump `pipelex` to `v0.9.0`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/). Renamed `hello_world.toml` to `hello_world.plx` as a consequence.
-
-## [v0.0.2] - 2025-08-27
-
-- Bump `pipelex` to `v0.8.1`: See `Pipelex` changelog [here](https://docs.pipelex.com/changelog/)
-
-## [v0.0.1] - 2025-06-XX
-
-- TBD
diff --git a/CLAUDE.md b/CLAUDE.md
index 6125caa..2f51801 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,1162 +1,39 @@
-
-# Pipelex Coding Rules
+# Pipelex Starter Project
-## Guide to write or edit pipelines using the Pipelex language in .plx files
+## Commands
-- Always first write your "plan" in natural language, then transcribe it in pipelex.
-- You should ALWAYS RUN validation when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
- - For a specific file: `pipelex validate path_to_file.plx`
- - For all pipelines: `pipelex validate all`
- - **IMPORTANT**: Ensure the Python virtual environment is activated before running `pipelex` commands. For standard installations, the venv is named `.venv` - always check that first. The commands will not work without proper venv activation.
-- Please use POSIX standard for files. (empty lines, no trailing whitespaces, etc.)
+### Linting & Type Checking
-### Pipeline File Naming
-- Files must be `.plx` for pipelines (Always add an empty line at the end of the file, and do not add trailing whitespaces to PLX files at all)
-- Files must be `.py` for code defining the data structures
-- Use descriptive names in `snake_case`
-
-### Pipeline File Outline
-A pipeline file has three main sections:
-1. Domain statement
-2. Concept definitions
-3. Pipe definitions
-
-#### Domain Statement
-```plx
-domain = "domain_name"
-description = "Description of the domain" # Optional
-```
-Note: The domain name usually matches the plx filename for single-file domains. For multi-file domains, use the subdirectory name.
-
-#### Concept Definitions
-
-Concepts represent ideas and semantic entities in your pipeline. They define what something *is*, not how it's structured.
-
-```plx
-[concept]
-ConceptName = "Description of the concept"
-```
-
-**Naming Rules:**
-- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story") - lists are handled implicitly by Pipelex
-- Avoid circumstantial adjectives (no "LargeText", use "Text") - focus on the essence of what the concept represents
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number, Page, JSON)
-
-**Native Concepts:**
-Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`, `JSON`. Use these directly or refine them when appropriate.
-
-**Refining Native Concepts:**
-To create a concept that specializes a native concept without adding fields:
-
-```plx
-[concept.Landscape]
-description = "A scenic outdoor photograph"
-refines = "Image"
-```
-
-For details on how to structure concepts with fields, see the "Structuring Models" section below.
-
-#### Pipe Definitions
-
-### Pipe Base Definition
-
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-description = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
-
-The pipes will all have at least this base definition.
-- `inputs`: Dictionary of key being the variable used in the prompts, and the value being the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if PipeSequence) or of the conditional pipes (if PipeCondition).
-So If you have this error:
-`PipeValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['invoice']'``
-That means that the pipe validate_expense is missing the input `invoice` because one of the subpipe is needing it.
-
-NEVER WRITE THE INPUTS BY BREAKING THE LINE LIKE THIS:
-
-```plx
-inputs = {
- input_1 = "ConceptName1",
- input_2 = "ConceptName2"
-}
-```
-
-
-- `output`: The name of the concept to output. The `ConceptName` should have the same name as the python class if you want structured output:
-
-#### Input Multiplicity
-
-By default, inputs expect a single item. Use bracket notation to specify multiple items:
-
-```plx
-## Single item (default)
-inputs = { document = "Text" }
-
-## Variable list - indeterminate number of items
-inputs = { documents = "Text[]" }
-
-## Fixed count - exactly N items
-inputs = { comparison_items = "Image[2]" }
-```
-
-**Key points:**
-- No brackets = single item (default behavior)
-- Use `[]` for lists of unknown length
-- Use `[N]` (where N is an integer) when operation requires exact count (e.g., comparing 2 items)
-
-### Structuring Models
-
-Once you've defined your concepts semantically (see "Concept Definitions" above), you need to specify their structure if they have fields.
-
-#### Three Ways to Structure Concepts
-
-**1. No Structure Needed**
-
-If a concept only refines a native concept without adding fields, use the TOML table syntax shown in "Concept Definitions" above. No structure section is needed.
-
-**2. Inline Structure Definition (RECOMMENDED for most cases)**
-
-For concepts with structured fields, define them inline using TOML syntax:
-
-```plx
-[concept.Invoice]
-description = "A commercial document issued by a seller to a buyer"
-
-[concept.Invoice.structure]
-invoice_number = "The unique invoice identifier" # This will be optional by default
-issue_date = { type = "date", description = "The date the invoice was issued", required = true }
-total_amount = { type = "number", description = "The total invoice amount", required = true }
-vendor_name = "The name of the vendor" # This will be optional by default
-line_items = { type = "list", item_type = "text", description = "List of items" }
-```
-
-**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
-
-**Field properties:** `type`, `description`, `required` (default: false), `default_value`, `choices`, `item_type` (for lists), `key_type` and `value_type` (for dicts)
-
-**Simple syntax** (creates required text field):
-```plx
-field_name = "Field description"
-```
-
-**Detailed syntax** (with explicit properties):
-```plx
-field_name = { type = "text", description = "Field description", default_value = "default" }
-```
-
-**3. Python StructuredContent Class (For Advanced Features)**
-
-Create a Python class when you need:
-- Custom validation logic (@field_validator, @model_validator)
-- Computed properties (@property methods)
-- Custom methods or class methods
-- Complex cross-field validation
-- Reusable structures across multiple domains
-
-```python
-from pipelex.core.stuffs.structured_content import StructuredContent
-from pydantic import Field, field_validator
-
-class Invoice(StructuredContent):
- """A commercial invoice with validation."""
-
- invoice_number: str = Field(description="The unique invoice identifier")
- total_amount: float = Field(ge=0, description="The total invoice amount")
- tax_amount: float = Field(ge=0, description="Tax amount")
-
- @field_validator('tax_amount')
- @classmethod
- def validate_tax(cls, v, info):
- """Ensure tax doesn't exceed total."""
- total = info.data.get('total_amount', 0)
- if v > total:
- raise ValueError('Tax amount cannot exceed total amount')
- return v
-```
-
-**Location:** Create models in `my_project/some_domain/some_domain_struct.py`. Classes inheriting from `StructuredContent` are automatically discovered.
-
-#### Decision Rules for Agents
-
-**If concept already exists:**
-- If it's already inline → KEEP IT INLINE unless user explicitly asks to convert or features require Python class
-- If it's already a Python class → KEEP IT as Python class
-
-**If creating new concept:**
-1. Does it only refine a native concept without adding fields? → Use concept-only declaration
-2. Does it need custom validation, computed properties, or methods? → Use Python class
-3. Otherwise → Use inline structure (fastest and simplest)
-
-**When to suggest conversion to Python class:**
-- User needs validation logic beyond type checking
-- User needs computed properties or custom methods
-- Structure needs to be reused across multiple domains
-- Complex type relationships or inheritance required
-
-#### Inline Structure Limitations
-
-Inline structures:
-- ✅ Support all common field types (text, number, date, list, dict, etc.)
-- ✅ Support required/optional fields, defaults, choices
-- ✅ Generate full Pydantic models with validation
-- ❌ Cannot have custom validators or complex validation logic
-- ❌ Cannot have computed properties or custom methods
-- ❌ Cannot refine custom (non-native) concepts
-- ❌ Limited IDE autocomplete compared to explicit Python classes
-
-
-### Pipe Controllers and Pipe Operators
-
-Look at the Pipes we have in order to adapt it. Pipes are organized in two categories:
-
-1. **Controllers** - For flow control:
- - `PipeSequence` - For creating a sequence of multiple steps
- - `PipeCondition` - If the next pipe depends of the expression of a stuff in the working memory
- - `PipeParallel` - For parallelizing pipes
-
-2. **Operators** - For specific tasks:
- - `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeExtract` - Extract text and images from an image or a PDF
- - `PipeCompose` - For composing text using Jinja2 templates: supports html, markdown, mermaid, etc.
- - `PipeImgGen` - Generate Images
- - `PipeFunc` - For running classic python scripts
-
-### PipeSequence controller
-
-Purpose: PipeSequence executes multiple pipes in a defined order, where each step can use results from original inputs or from previous steps.
-
-#### Basic Definition
-```plx
-[pipe.your_sequence_name]
-type = "PipeSequence"
-description = "Description of what this sequence does"
-inputs = { input_name = "InputType" } # All the inputs of the sub pipes, except the ones generated by intermediate steps
-output = "OutputType"
-steps = [
- { pipe = "first_pipe", result = "first_result" },
- { pipe = "second_pipe", result = "second_result" },
- { pipe = "final_pipe", result = "final_result" }
-]
-```
-
-#### Key Components
-
-1. **Steps Array**: List of pipes to execute in sequence
- - `pipe`: Name of the pipe to execute
- - `result`: Name to assign to the pipe's output that will be in the working memory
-
-#### Using PipeBatch in Steps
-
-You can use PipeBatch functionality within steps using `batch_over` and `batch_as`:
-
-```plx
-steps = [
- { pipe = "process_items", batch_over = "input_list", batch_as = "current_item", result = "processed_items"
- }
-]
-```
-
-1. **batch_over**: Specifies a `ListContent` field to iterate over. Each item in the list will be processed individually and IN PARALLEL by the pipe.
- - Must be a `ListContent` type containing the items to process
- - Can reference inputs or results from previous steps
-
-2. **batch_as**: Defines the name that will be used to reference the current item being processed
- - This name can be used in the pipe's input mappings
- - Makes each item from the batch available as a single element
-
-The result of a batched step will be a `ListContent` containing the outputs from processing each item.
-
-### PipeCondition controller
-
-The PipeCondition controller allows you to implement conditional logic in your pipeline, choosing which pipe to execute based on an evaluated expression. It supports both direct expressions and expression templates.
-
-#### Basic usage
-
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression = "input_data.category"
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-or
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-description = "A conditional pipe to decide whether..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression_template = "{{ input_data.category }}" # Jinja2 code
-default_outcome = "process_medium"
-
-[pipe.conditional_operation.outcomes]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-
-#### Key Parameters
-
-- `expression`: Direct boolean or string expression (mutually exclusive with expression_template)
-- `expression_template`: Jinja2 template for more complex conditional logic (mutually exclusive with expression)
-- `outcomes`: Dictionary mapping expression results to pipe codes:
- 1. The key on the left (`small`, `medium`) is the result of `expression` or `expression_template`
- 2. The value on the right (`process_small`, `process_medium`, etc.) is the name of the pipe to trigger
-- `default_outcome`: **Required** - The pipe to execute if the expression doesn't match any key in outcomes. Use `"fail"` if you want the pipeline to fail when no match is found
-
-Example with fail as default:
-```plx
-[pipe.strict_validation]
-type = "PipeCondition"
-description = "Validate with strict matching"
-inputs = { status = "Status" }
-output = "Text"
-expression = "status.value"
-default_outcome = "fail"
-
-[pipe.strict_validation.outcomes]
-approved = "process_approved"
-rejected = "process_rejected"
-```
-
-### PipeLLM operator
-
-PipeLLM is used to:
-1. Generate text or objects with LLMs
-2. Process images with Vision LLMs
-
-#### Basic Usage
-
-Simple Text Generation:
-```plx
-[pipe.write_story]
-type = "PipeLLM"
-description = "Write a short story"
-output = "Text"
-prompt = """
-Write a short story about a programmer.
-"""
-```
-
-Structured Data Extraction:
-```plx
-[pipe.extract_info]
-type = "PipeLLM"
-description = "Extract information"
-inputs = { text = "Text" }
-output = "PersonInfo"
-prompt = """
-Extract person information from this text:
-@text
-"""
-```
-
-Supports system instructions:
-```plx
-[pipe.expert_analysis]
-type = "PipeLLM"
-description = "Expert analysis"
-output = "Analysis"
-system_prompt = "You are a data analysis expert"
-prompt = "Analyze this data"
-```
-
-#### Multiple Outputs
-
-Generate multiple outputs (fixed number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[3]" # Generate exactly 3 ideas
-prompt = "Generate 3 ideas"
-```
-
-Generate multiple outputs (variable number) - use bracket notation:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-description = "Generate ideas"
-output = "Idea[]" # Let the LLM decide how many to generate
-prompt = "Generate ideas"
-```
-
-#### Vision
-
-Process images with VLMs (image inputs must be tagged in the prompt):
-```plx
-[pipe.analyze_image]
-type = "PipeLLM"
-description = "Analyze image"
-inputs = { image = "Image" }
-output = "ImageAnalysis"
-prompt = """
-Describe what you see in this image:
-
-$image
-"""
-```
-
-You can also reference images inline in meaningful sentences to guide the Visual LLM:
-```plx
-[pipe.compare_images]
-type = "PipeLLM"
-description = "Compare two images"
-inputs = { photo = "Image", painting = "Image" }
-output = "Analysis"
-prompt = "Analyze the colors in $photo and the shapes in $painting."
-```
-
-#### Writing prompts for PipeLLM
-
-**Insert stuff inside a tagged block**
-
-If the inserted text is supposedly a long text, made of several lines or paragraphs, you want it inserted inside a block, possibly a block tagged and delimlited with proper syntax as one would do in a markdown documentation. To include stuff as a block, use the "@" prefix.
-
-Example template:
-```plx
-prompt = """
-Match the expense with its corresponding invoice:
-
-@expense
-
-@invoices
-"""
-```
-In the example above, the expense data and the invoices data are obviously made of several lines each, that's why it makes sense to use the "@" prefix in order to have them delimited inside a block. Note that our preprocessor will automatically include the block's title, so it doesn't need to be explicitly written in the prompt.
-
-DO NOT write things like "Here is the expense: @expense".
-DO write simply "@expense" alone in an isolated line.
-
-**Insert stuff inline**
-
-If the inserted text is short text and it makes sense to have it inserted directly into a sentence, you want it inserted inline. To insert stuff inline, use the "$" prefix. This will insert the stuff without delimiters and the content will be rendered as plain text.
-
-Example template:
-```plx
-prompt = """
-Your goal is to summarize everything related to $topic in the provided text:
-
-@text
-
-Please provide only the summary, with no additional text or explanations.
-Your summary should not be longer than 2 sentences.
-"""
-```
-
-In the example above, $topic will be inserted inline, whereas @text will be a a delimited block.
-Be sure to make the proper choice of prefix for each insertion.
-
-DO NOT write "$topic" alone in an isolated line.
-DO write things like "Write an essay about $topic" to include text into an actual sentence.
-
-
-### PipeExtract operator
-
-The PipeExtract operator is used to extract text and images from an image or a PDF
-
-#### Simple Text Extraction
-```plx
-[pipe.extract_info]
-type = "PipeExtract"
-description = "extract the information"
-inputs = { document = "PDF" } # or { image = "Image" } if it's an image. This is the only input.
-output = "Page"
+After making code changes, always run:
+```bash
+make agent-check
```
+This runs: fix-unused-imports, ruff format, ruff lint, plxt format/lint (`.mthds`/`.toml`), pyright, mypy.
-Using Extract Model Settings:
-```plx
-[pipe.extract_with_model]
-type = "PipeExtract"
-description = "Extract with specific model"
-inputs = { document = "PDF" }
-output = "Page"
-model = "base_extract_mistral" # Use predefined extract preset or model alias
-```
-
-Only one input is allowed and it must either be an `Image` or a `PDF`. The input can be named anything.
-
-The output concept `Page` is a native concept, with the structure `PageContent`:
-It corresponds to 1 page. Therefore, the PipeExtract is outputing a `ListContent` of `Page`
+### Running Tests
-```python
-class TextAndImagesContent(StuffContent):
- text: TextContent | None
- images: list[ImageContent] | None
-
-class PageContent(StructuredContent): # CONCEPT IS "Page"
- text_and_images: TextAndImagesContent
- page_view: ImageContent | None = None
+```bash
+make agent-test
```
-- `text_and_images` are the text, and the related images found in the input image or PDF.
-- `page_view` is the screenshot of the whole pdf page/image.
-
-### PipeCompose operator
-
-The PipeCompose operator is used to compose text using Jinja2 templates. It supports various output formats including HTML, Markdown, Mermaid diagrams, and more.
-
-#### Basic Usage
-
-Simple Template Composition:
-```plx
-[pipe.compose_report]
-type = "PipeCompose"
-description = "Compose a report using template"
-inputs = { data = "ReportData" }
-output = "Text"
-template = """
-## Report Summary
-
-Based on the analysis:
-$data
-
-Generated on: {{ current_date }}
-"""
-```
-
-Using Named Templates:
-```plx
-[pipe.use_template]
-type = "PipeCompose"
-description = "Use a predefined template"
-inputs = { content = "Text" }
-output = "Text"
-template_name = "standard_report_template"
-```
-
-Using Nested Template Section (for more control):
-```plx
-[pipe.advanced_template]
-type = "PipeCompose"
-description = "Use advanced template settings"
-inputs = { data = "ReportData" }
-output = "Text"
-
-[pipe.advanced_template.template]
-template = "Report: $data"
-category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-```
-
-CRM Email Template:
-```plx
-[pipe.compose_follow_up_email]
-type = "PipeCompose"
-description = "Compose a personalized follow-up email for CRM"
-inputs = { customer = "Customer", deal = "Deal", sales_rep = "SalesRep" }
-output = "Text"
-template_category = "html"
-templating_style = { tag_style = "square_brackets", text_format = "html" }
-template = """
-Subject: Following up on our $deal.product_name discussion
-
-Hi $customer.first_name,
-
-I hope this email finds you well! I wanted to follow up on our conversation about $deal.product_name from $deal.last_contact_date.
-
-Based on our discussion, I understand that your key requirements are: $deal.customer_requirements
-
-I'm excited to let you know that we can definitely help you achieve your goals. Here's what I'd like to propose:
-
-**Next Steps:**
-- Schedule a demo tailored to your specific needs
-- Provide you with a customized quote based on your requirements
-- Connect you with our implementation team
-
-Would you be available for a 30-minute call this week? I have openings on:
-{% for slot in available_slots %}
-- {{ slot }}
-{% endfor %}
-
-Looking forward to moving this forward together!
-
-Best regards,
-$sales_rep.name
-$sales_rep.title
-$sales_rep.phone | $sales_rep.email
-"""
-```
-
-#### Key Parameters
-
-- `template`: Inline template string (mutually exclusive with template_name)
-- `template_name`: Name of a predefined template (mutually exclusive with template)
-- `template_category`: Template type ("llm_prompt", "html", "markdown", "mermaid", etc.)
-- `templating_style`: Styling options for template rendering
-- `extra_context`: Additional context variables for template
-
-For more control, you can use a nested `template` section instead of the `template` field:
-- `template.template`: The template string
-- `template.category`: Template type
-- `template.templating_style`: Styling options
+Silent on success, full output on failure. Excludes inference/LLM markers by default.
-#### Template Variables
+Run specific tests (local only): `make tp TEST=test_function_name`
-Use the same variable insertion rules as PipeLLM:
-- `@variable` for block insertion (multi-line content)
-- `$variable` for inline insertion (short text)
-
-### PipeImgGen operator
-
-The PipeImgGen operator is used to generate images using AI image generation models.
-
-#### Basic Usage
-
-Simple Image Generation:
-```plx
-[pipe.generate_image]
-type = "PipeImgGen"
-description = "Generate an image from prompt"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-```
-
-Using Image Generation Settings:
-```plx
-[pipe.generate_photo]
-type = "PipeImgGen"
-description = "Generate a high-quality photo"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Photo"
-model = { model = "fast-img-gen" }
-aspect_ratio = "16:9"
-quality = "hd"
-```
-
-Multiple Image Generation:
-```plx
-[pipe.generate_variations]
-type = "PipeImgGen"
-description = "Generate multiple image variations"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image[3]"
-seed = "auto"
-```
-
-Advanced Configuration:
-```plx
-[pipe.generate_custom]
-type = "PipeImgGen"
-description = "Generate image with custom settings"
-inputs = { prompt = "ImgGenPrompt" }
-output = "Image"
-model = "img_gen_preset_name" # Use predefined preset
-aspect_ratio = "1:1"
-quality = "hd"
-background = "transparent"
-output_format = "png"
-is_raw = false
-safety_tolerance = 3
-```
-
-#### Key Parameters
-
-**Image Generation Settings:**
-- `model`: Model choice (preset name or inline settings with model name)
-- `quality`: Image quality ("standard", "hd")
-
-**Output Configuration:**
-- `aspect_ratio`: Image dimensions ("1:1", "16:9", "9:16", etc.)
-- `output_format`: File format ("png", "jpeg", "webp")
-- `background`: Background type ("default", "transparent")
-
-**Generation Control:**
-- `seed`: Random seed (integer or "auto")
-- `is_raw`: Whether to apply post-processing
-- `is_moderated`: Enable content moderation
-- `safety_tolerance`: Content safety level (1-6)
-
-#### Input Requirements
-
-PipeImgGen requires exactly one input that must be either:
-- An `ImgGenPrompt` concept
-- A concept that refines `ImgGenPrompt`
-
-The input can be named anything but must contain the prompt text for image generation.
-
-### PipeFunc operator
-
-The PipeFunc operator is used to run custom Python functions within a pipeline. This allows integration of classic Python scripts and custom logic.
-
-#### Basic Usage
-
-Simple Function Call:
-```plx
-[pipe.process_data]
-type = "PipeFunc"
-description = "Process data using custom function"
-inputs = { input_data = "DataType" }
-output = "ProcessedData"
-function_name = "process_data_function"
-```
-
-File Processing Example:
-```plx
-[pipe.read_file]
-type = "PipeFunc"
-description = "Read file content"
-inputs = { file_path = "FilePath" }
-output = "FileContent"
-function_name = "read_file_content"
-```
-
-#### Key Parameters
-
-- `function_name`: Name of the Python function to call (must be registered in func_registry)
-
-#### Function Requirements
-
-The Python function must:
-
-1. **Be registered** in the `func_registry`
-2. **Accept `working_memory`** as a parameter:
- ```python
- async def my_function(working_memory: WorkingMemory) -> StuffContent | list[StuffContent] | str:
- # Function implementation
- pass
- ```
-
-3. **Return appropriate types**:
- - `StuffContent`: Single content object
- - `list[StuffContent]`: Multiple content objects (becomes ListContent)
- - `str`: Simple string (becomes TextContent)
-
-#### Function Registration
-
-Functions must be registered in the function registry before use:
-
-```python
-from pipelex.system.registries.func_registry import func_registry
-
-@func_registry.register("my_function_name")
-async def my_custom_function(working_memory: WorkingMemory) -> StuffContent:
- # Access inputs from working memory
- input_data = working_memory.get_stuff("input_name")
-
- # Process data
- result = process_logic(input_data.content)
-
- # Return result
- return MyResultContent(data=result)
-```
-
-#### Working Memory Access
-
-Inside the function, access pipeline inputs through working memory:
-
-```python
-async def process_function(working_memory: WorkingMemory) -> TextContent:
- # Get input stuff by name
- input_stuff = working_memory.get_stuff("input_name")
-
- # Access the content
- input_content = input_stuff.content
-
- # Process and return
- processed_text = f"Processed: {input_content.text}"
- return TextContent(text=processed_text)
-```
-
----
-
-### Rules to choose LLM models used in PipeLLMs.
-
-#### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-#### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-#### Using an LLM Handle in a PipeLLM
-
-Here is an example of using a model to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-#### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
-
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
-
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
----
-
-ALWAYS RUN validation when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-- For a specific bundle/file: `pipelex validate path_to_file.plx`
-- For all pipelines: `pipelex validate all`
-- Remember: Ensure your Python virtual environment is activated (typically `.venv` for standard installations) before running `pipelex` commands.
-
-Then, create an example file to run the pipeline in the `examples` folder.
-But don't write documentation unless asked explicitly to.
-
-## Guide to execute a pipeline and write example code
-
-### Example to execute a pipeline with text output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-
-
-async def hello_world() -> str:
- """
- This function demonstrates the use of a super simple Pipelex pipeline to generate text.
- """
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="hello_world",
- )
-
- return pipe_output.main_stuff_as_str
-
-
-## start Pipelex
-Pipelex.make()
-## run sample using asyncio
-output_text = asyncio.run(hello_world())
-pretty_print(output_text, title="Your first Pipelex output")
-```
-
-### Example to execute a pipeline with structured output
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-from pipelex.core.stuffs.image_content import ImageContent
-
-from my_project.gantt.gantt_struct import GanttChart
-
-SAMPLE_NAME = "extract_gantt"
-IMAGE_URL = "assets/gantt/gantt_tree_house.png"
-
-
-async def extract_gantt(image_url: str) -> GanttChart:
- # Run the pipe
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
- # Output the result
- return pipe_output.main_stuff_as(content_type=GanttChart)
-
-
-## start Pipelex
-Pipelex.make()
-
-## run sample using asyncio
-gantt_chart = asyncio.run(extract_gantt(image_url=IMAGE_URL))
-pretty_print(gantt_chart, title="Gantt Chart")
-```
-
-### Setting up the input memory
-
-#### Explanation of input memory
-
-The input memory is a dictionary, where the key is the name of the input variable and the value provides details to make it a stuff object. The relevant definitions are:
-```python
-StuffContentOrData = dict[str, Any] | StuffContent | list[Any] | str
-PipelineInputs = dict[str, StuffContentOrData]
-```
-As you can seen, we made it so different ways can be used to define that stuff using structured content or data.
-
-#### Different ways to set up the input memory
-
-So here are a few concrete examples of calls to execute_pipeline with various ways to set up the input memory:
-
-```python
-## Here we have a single input and it's a Text.
-## If you assign a string, by default it will be considered as a TextContent.
- pipe_output = await execute_pipeline(
- pipe_code="master_advisory_orchestrator",
- inputs={
- "user_input": problem_description,
- },
- )
-
-## Here we have a single input and it's a PDF.
-## Because PDFContent is a native concept, we can use it directly as a value,
-## the system knows what content it corresponds to:
- pipe_output = await execute_pipeline(
- pipe_code="power_extractor_dpe",
- inputs={
- "document": PDFContent(url=pdf_url),
- },
- )
-
-## Here we have a single input and it's an Image.
-## Because ImageContent is a native concept, we can use it directly as a value:
- pipe_output = await execute_pipeline(
- pipe_code="fashion_variation_pipeline",
- inputs={
- "fashion_photo": ImageContent(url=image_url),
- },
- )
-
-## Here we have a single input, it's an image but
-## its actually a more specific concept gantt.GanttImage which refines Image,
-## so we must provide it using a dict with the concept and the content:
- pipe_output = await execute_pipeline(
- pipe_code="extract_gantt_by_steps",
- inputs={
- "gantt_chart_image": {
- "concept": "gantt.GanttImage",
- "content": ImageContent(url=image_url),
- }
- },
- )
-
-## Here is a more complex example with multiple inputs assigned using different ways:
- pipe_output = await execute_pipeline(
- pipe_code="retrieve_then_answer",
- dynamic_output_concept_code="contracts.Fees",
- inputs={
- "text": load_text_from_path(path=text_path),
- "question": {
- "concept": "answer.Question",
- "content": question,
- },
- "client_instructions": client_instructions,
- },
- )
-```
-
-### Using the outputs of a pipeline
-
-All pipe executions return a `PipeOutput` object.
-It's a BaseModel which contains the resulting working memory at the end of the execution and the pipeline run id.
-It also provides a bunch of accessor functions and properties to unwrap the main stuff, which is the last stuff added to the working memory:
-
-```python
-
-class PipeOutput(BaseModel):
- working_memory: WorkingMemory = Field(default_factory=WorkingMemory)
- pipeline_run_id: str = Field(default=SpecialPipelineId.UNTITLED)
-
- @property
- def main_stuff(self) -> Stuff:
- ...
-
- def main_stuff_as_list(self, item_type: type[StuffContentType]) -> ListContent[StuffContentType]:
- ...
-
- def main_stuff_as_items(self, item_type: type[StuffContentType]) -> list[StuffContentType]:
- ...
-
- def main_stuff_as(self, content_type: type[StuffContentType]) -> StuffContentType:
- ...
-
- @property
- def main_stuff_as_text(self) -> TextContent:
- ...
-
- @property
- def main_stuff_as_str(self) -> str:
- ...
-
- @property
- def main_stuff_as_image(self) -> ImageContent:
- ...
-
- @property
- def main_stuff_as_text_and_image(self) -> TextAndImagesContent:
- ...
-
- @property
- def main_stuff_as_number(self) -> NumberContent:
- ...
-
- @property
- def main_stuff_as_html(self) -> HtmlContent:
- ...
-
- @property
- def main_stuff_as_mermaid(self) -> MermaidContent:
- ...
-```
-
-As you can see, you can extract any variable from the output working memory.
-
-#### Getting the main stuff as a specific type
-
-Simple text as a string:
-
-```python
-result = pipe_output.main_stuff_as_str
-```
-Structured object (BaseModel):
-
-```python
-result = pipe_output.main_stuff_as(content_type=GanttChart)
-```
-
-If it's a list, you can get a `ListContent` of the specific type.
-
-```python
-result_list_content = pipe_output.main_stuff_as_list(item_type=GanttChart)
-```
-
-or if you want, you can get the actual items as a regular python list:
-
-```python
-result_list = pipe_output.main_stuff_as_items(item_type=GanttChart)
-```
-
----
-
-## Rules to choose LLM models used in PipeLLMs.
-
-### LLM Configuration System
-
-In order to use it in a pipe, an LLM is referenced by its llm_handle (alias) and possibly by an llm_preset.
-LLM configurations are managed through the new inference backend system with files located in `.pipelex/inference/`:
-
-- **Model Deck**: `.pipelex/inference/deck/base_deck.toml` and `.pipelex/inference/deck/overrides.toml`
-- **Backends**: `.pipelex/inference/backends.toml` and `.pipelex/inference/backends/*.toml`
-- **Routing**: `.pipelex/inference/routing_profiles.toml`
-
-### LLM Handles
-
-An llm_handle can be either:
-1. **A direct model name** (like "gpt-4o-mini", "claude-3-sonnet") - automatically available for all models loaded by the inference backend system
-2. **An alias** - user-defined shortcuts that map to model names, defined in the `[aliases]` section:
-
-```toml
-[aliases]
-base-claude = "claude-4.5-sonnet"
-base-gpt = "gpt-5"
-base-gemini = "gemini-2.5-flash"
-base-mistral = "mistral-medium"
-```
-
-The system first looks for direct model names, then checks aliases if no direct match is found. The system handles model routing through backends automatically.
-
-### Using an LLM Handle in a PipeLLM
-
-Here is an example of using an llm_handle to specify which LLM to use in a PipeLLM:
-
-```plx
-[pipe.hello_world]
-type = "PipeLLM"
-description = "Write text about Hello World."
-output = "Text"
-model = { model = "gpt-5", temperature = 0.9 }
-prompt = """
-Write a haiku about Hello World.
-"""
-```
-
-As you can see, to use the LLM, you must also indicate the temperature (float between 0 and 1) and max_tokens (either an int or the string "auto").
-
-### LLM Presets
-
-Presets are meant to record the choice of an llm with its hyper parameters (temperature and max_tokens) if it's good for a particular task. LLM Presets are skill-oriented.
-
-Examples:
-```toml
-llm_to_engineer = { model = "base-claude", temperature = 1 }
-llm_to_extract_invoice = { model = "claude-4.5-sonnet", temperature = 0.1, max_tokens = "auto" }
-```
-
-The interest is that these presets can be used to set the LLM choice in a PipeLLM, like this:
-
-```plx
-[pipe.extract_invoice]
-type = "PipeLLM"
-description = "Extract invoice information from an invoice text transcript"
-inputs = { invoice_text = "InvoiceText" }
-output = "Invoice"
-model = "llm_to_extract_invoice"
-prompt = """
-Extract invoice information from this invoice:
-
-The category of this invoice is: $invoice_details.category.
-
-@invoice_text
-"""
-```
+### Other Useful Targets
-The setting here `model = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
-You must not use an LLM preset in a PipeLLM that does not exist in the deck. If needed, you can add llm presets.
+- `make install` - Create venv + install all deps (uses uv)
+- `make li` - Lock + install
+- `make cleanderived` - Remove caches/compiled files (useful when linters get confused)
+- `make validate` / `make v` - Run pipelex validate --all
+- `make tb` - Quick boot test
+- `make fui` - Fix unused imports only
+- `make plxt-format` - Format `.mthds`/`.toml` files with plxt
+- `make plxt-lint` - Lint `.mthds`/`.toml` files with plxt
+## Project Structure
-You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
-
+- Package: `my_project/` (Python 3.10+, target 3.11)
+- Tests: `tests/` (e2e, integration, test_pipelines)
+- Dependency manager: uv (>=0.7.2)
+- Pipelex dependency: `pipelex` package from PyPI (see pyproject.toml)
+- `.mthds` files: Pipelex method definition files in `my_project/`
diff --git a/Makefile b/Makefile
index 803118d..d3e5055 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,6 @@
+SHELL := /bin/bash
+.SHELLFLAGS := -o pipefail -c
+
ifeq ($(wildcard .env),.env)
include .env
export
@@ -6,17 +9,18 @@ VIRTUAL_ENV := $(CURDIR)/.venv
PROJECT_NAME := $(shell grep '^name = ' pyproject.toml | sed -E 's/name = "(.*)"/\1/')
# The "?" is used to make the variable optional, so that it can be overridden by the user.
-PYTHON_VERSION ?= 3.11
+PYTHON_VERSION ?= 3.13
VENV_PYTHON := $(VIRTUAL_ENV)/bin/python
VENV_PYTEST := $(VIRTUAL_ENV)/bin/pytest
VENV_RUFF := $(VIRTUAL_ENV)/bin/ruff
VENV_PYRIGHT := $(VIRTUAL_ENV)/bin/pyright
VENV_MYPY := $(VIRTUAL_ENV)/bin/mypy
VENV_PIPELEX := $(VIRTUAL_ENV)/bin/pipelex
+VENV_PLXT := RUST_LOG=warn "$(VIRTUAL_ENV)/bin/plxt"
UV_MIN_VERSION = $(shell grep -m1 'required-version' pyproject.toml | sed -E 's/.*= *"([^<>=, ]+).*/\1/')
-USUAL_PYTEST_MARKERS := "(dry_runnable or not (inference or llm or imgg or ocr)) and not (needs_output or pipelex_api)"
+USUAL_PYTEST_MARKERS := "(dry_runnable or not inference) and not (needs_output or pipelex_api)"
define PRINT_TITLE
$(eval PROJECT_PART := [$(PROJECT_NAME)])
@@ -48,8 +52,12 @@ make er - Shorthand -> export-requirements
make erd - Shorthand -> export-requirements-dev
make validate - Run the setup sequence to validate the config and libraries
-make format - format with ruff format
-make lint - lint with ruff check
+make format - Format all (ruff-format + plxt-format)
+make lint - Lint all (ruff-lint + plxt-lint)
+make ruff-format - Format Python with ruff
+make ruff-lint - Lint Python with ruff
+make plxt-format - Format .mthds/.toml with plxt
+make plxt-lint - Lint .mthds/.toml with plxt
make pyright - Check types with pyright
make mypy - Check types with mypy
@@ -60,6 +68,8 @@ make reinstall - Reinstall dependencies
make merge-check-ruff-lint - Run ruff merge check without updating files
make merge-check-ruff-format - Run ruff merge check without updating files
+make merge-check-plxt-format - Check .mthds/.toml formatting with plxt
+make merge-check-plxt-lint - Lint .mthds/.toml with plxt
make merge-check-mypy - Run mypy merge check without updating files
make merge-check-pyright - Run pyright merge check without updating files
@@ -77,6 +87,8 @@ make ti - Shorthand -> test-inference
make check - Shorthand -> format lint mypy
make c - Shorthand -> check
make cc - Shorthand -> cleanderived check
+make agent-check - Shorthand -> fix-unused-imports format lint pyright mypy (for AI agents)
+make agent-test - Run unit tests, silent on success, output on failure (for AI agents)
make li - Shorthand -> lock install
make check-unused-imports - Check for unused imports without fixing
make fix-unused-imports - Fix unused imports with ruff
@@ -86,15 +98,15 @@ endef
export HELP
.PHONY: \
- all help env lock install update build \
+ all help env env-verbose check-uv check-uv-verbose lock install update build \
export-requirements export-requirements-dev er erd \
- format lint pyright mypy \
+ format lint ruff-format ruff-lint plxt-format plxt-lint pyright mypy \
cleanderived cleanenv cleanall \
test t test-quiet tq test-with-prints tp test-inference ti \
codex-tests gha-tests \
run-all-tests run-manual-trigger-gha-tests run-gha_disabled-tests \
- validate v check c cc \
- merge-check-ruff-lint merge-check-ruff-format merge-check-mypy merge-check-pyright \
+ validate v check c cc agent-check agent-test \
+ merge-check-ruff-lint merge-check-ruff-format merge-check-plxt-format merge-check-plxt-lint merge-check-mypy merge-check-pyright \
li check-unused-imports fix-unused-imports check-uv check-TODOs
all help:
@@ -105,7 +117,18 @@ all help:
### SETUP
##########################################################################################
+# Quiet check-uv: only shows output if uv is missing (needs install)
check-uv:
+ @command -v uv >/dev/null 2>&1 || { \
+ echo ""; \
+ echo "=== [$(PROJECT_NAME)] ===== (check-uv) ====== Ensuring uv ≥ $(UV_MIN_VERSION) =========="; \
+ echo "uv not found – installing latest …"; \
+ curl -LsSf https://astral.sh/uv/install.sh | sh; \
+ }
+ @uv self update >/dev/null 2>&1 || true
+
+# Verbose check-uv: always shows output (for setup commands)
+check-uv-verbose:
$(call PRINT_TITLE,"Ensuring uv ≥ $(UV_MIN_VERSION)")
@command -v uv >/dev/null 2>&1 || { \
echo "uv not found – installing latest …"; \
@@ -113,27 +136,37 @@ check-uv:
}
@uv self update >/dev/null 2>&1 || true
+# Quiet env: only shows output if venv needs to be created
env: check-uv
+ @if [ ! -d $(VIRTUAL_ENV) ]; then \
+ echo ""; \
+ echo "=== [$(PROJECT_NAME)] ===== (env) ====== Creating virtual environment ================="; \
+ echo "Creating Python virtual env in \`${VIRTUAL_ENV}\`"; \
+ uv venv $(VIRTUAL_ENV) --python $(PYTHON_VERSION); \
+ fi
+
+# Verbose env: always shows output (for setup commands like install, lock, update)
+env-verbose: check-uv-verbose
$(call PRINT_TITLE,"Creating virtual environment")
@if [ ! -d $(VIRTUAL_ENV) ]; then \
echo "Creating Python virtual env in \`${VIRTUAL_ENV}\`"; \
- uv venv $(VIRTUAL_ENV) --python 3.11; \
+ uv venv $(VIRTUAL_ENV) --python $(PYTHON_VERSION); \
else \
echo "Python virtual env already exists in \`${VIRTUAL_ENV}\`"; \
fi
-install: env
+install: env-verbose
$(call PRINT_TITLE,"Installing dependencies")
@. $(VIRTUAL_ENV)/bin/activate && \
uv sync --all-extras && \
echo "Installed dependencies in ${VIRTUAL_ENV}";
-lock: env
+lock: env-verbose
$(call PRINT_TITLE,"Resolving dependencies without update")
@uv lock && \
echo "uv lock without update";
-update: env
+update: env-verbose
$(call PRINT_TITLE,"Updating all dependencies")
@uv pip compile --upgrade pyproject.toml -o requirements.lock && \
uv pip install -e ".[dev]" && \
@@ -157,7 +190,7 @@ erd: export-requirements-dev
validate: env
$(call PRINT_TITLE,"Running setup sequence")
- $(VENV_PIPELEX) validate all
+ $(VENV_PIPELEX) validate --all
##############################################################################################
############################ Cleaning ############################
@@ -209,12 +242,12 @@ cleanall: cleanderived cleanenv cleanconfig
codex-tests: env
$(call PRINT_TITLE,"Unit testing for Codex")
@echo "• Running unit tests for Codex (excluding inference and codex_disabled)"
- $(VENV_PYTEST) --exitfirst --quiet -m "not inference and not codex_disabled" || [ $$? = 5 ]
+ $(VENV_PYTEST) --disable-inference --exitfirst --quiet -m "not inference and not codex_disabled" || [ $$? = 5 ]
gha-tests: env
$(call PRINT_TITLE,"Unit testing for github actions")
@echo "• Running unit tests for github actions (excluding inference and gha_disabled)"
- $(VENV_PYTEST) --exitfirst --quiet -m "not inference and not gha_disabled" || [ $$? = 5 ]
+ $(VENV_PYTEST) --disable-inference --exitfirst --quiet -m "not inference and not gha_disabled" || [ $$? = 5 ]
run-all-tests: env
$(call PRINT_TITLE,"Running all unit tests")
@@ -284,18 +317,42 @@ test-inference: env
ti: test-inference
@echo "> done: ti = test-inference"
+agent-test: env
+ @echo "• Running unit tests..."
+ @tmpfile=$$(mktemp); \
+ $(VENV_PYTEST) -m $(USUAL_PYTEST_MARKERS) -o log_level=WARNING --tb=short -q > "$$tmpfile" 2>&1; \
+ exit_code=$$?; \
+ if [ $$exit_code -ne 0 ]; then grep -vE '\[\s*[0-9]+%\]\s*$$' "$$tmpfile"; fi; \
+ rm -f "$$tmpfile"; \
+ if [ $$exit_code -eq 0 ]; then echo "• All tests passed."; fi; \
+ exit $$exit_code
+
############################################################################################
############################ Linting ############################
############################################################################################
-format: env
+ruff-format: env
$(call PRINT_TITLE,"Formatting with ruff")
@$(VENV_RUFF) format .
-lint: env
+ruff-lint: env
$(call PRINT_TITLE,"Linting with ruff")
@$(VENV_RUFF) check . --fix
+plxt-format: env
+ $(call PRINT_TITLE,"Formatting MTHDS/TOML with plxt")
+ $(VENV_PLXT) fmt
+
+plxt-lint: env
+ $(call PRINT_TITLE,"Linting MTHDS/TOML with plxt")
+ $(VENV_PLXT) lint
+
+format: ruff-format plxt-format
+ @echo "> done: format = ruff-format plxt-format"
+
+lint: ruff-lint plxt-lint
+ @echo "> done: lint = ruff-lint plxt-lint"
+
pyright: env
$(call PRINT_TITLE,"Typechecking with pyright")
$(VENV_PYRIGHT) --pythonpath $(VENV_PYTHON) --project pyproject.toml
@@ -313,6 +370,14 @@ merge-check-ruff-format: env
$(call PRINT_TITLE,"Formatting with ruff")
$(VENV_RUFF) format --check .
+merge-check-plxt-format: env
+ $(call PRINT_TITLE,"Checking MTHDS/TOML formatting with plxt")
+ $(VENV_PLXT) fmt --check
+
+merge-check-plxt-lint: env
+ $(call PRINT_TITLE,"Linting MTHDS/TOML with plxt")
+ $(VENV_PLXT) lint
+
merge-check-ruff-lint: env check-unused-imports
$(call PRINT_TITLE,"Linting with ruff without fixing files")
$(VENV_RUFF) check .
@@ -343,6 +408,9 @@ cc: cleanderived c
check: cleanderived check-unused-imports c
@echo "> done: check"
+agent-check: fix-unused-imports format lint pyright mypy
+ @echo "> done: agent-check"
+
v: validate
@echo "> done: v = validate"
diff --git a/my_project/hello_world.plx b/my_project/hello_world.mthds
similarity index 90%
rename from my_project/hello_world.plx
rename to my_project/hello_world.mthds
index 504b80c..26c1d52 100644
--- a/my_project/hello_world.plx
+++ b/my_project/hello_world.mthds
@@ -1,6 +1,6 @@
-domain = "quick_start"
+domain = "quick_start"
description = "Discovering Pipelex"
[pipe]
@@ -12,4 +12,3 @@ model = { model = "gpt-4o-mini", temperature = 0.9, max_tokens = "auto" }
prompt = """
Write a haiku about Hello World.
"""
-
diff --git a/my_project/hello_world.py b/my_project/hello_world.py
index 5614816..3737deb 100644
--- a/my_project/hello_world.py
+++ b/my_project/hello_world.py
@@ -2,7 +2,7 @@
from pipelex import pretty_print
from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
+from pipelex.pipeline.runner import PipelexRunner
async def hello_world():
@@ -10,15 +10,20 @@ async def hello_world():
This function demonstrates the use of a super simple Pipelex pipeline to generate text.
"""
# Run the pipe
- pipe_output = await execute_pipeline(
+ runner = PipelexRunner()
+ response = await runner.execute_pipeline(
pipe_code="hello_world",
)
+ pipe_output = response.pipe_output
# Print the output
pretty_print(pipe_output, title="Your first Pipelex output")
+ # get the generated text
+ generated_text = pipe_output.main_stuff_as_str
+ pretty_print(generated_text, title="Generated text")
-# start Pipelex
-Pipelex.make()
-# run sample using asyncio
-asyncio.run(hello_world())
+
+if __name__ == "__main__":
+ with Pipelex.make(library_dirs=["my_project"]):
+ asyncio.run(hello_world())
diff --git a/pyproject.toml b/pyproject.toml
index 1cfd5d7..a35b6a6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,22 +1,23 @@
[project]
name = "my-project"
-version = "0.6.7"
+version = "0.7.0"
description = "Replace this with your project description"
# authors = [{ name = "Your Name", email = "your.email@example.com" }]
license = "MIT"
readme = "README.md"
-requires-python = ">=3.10"
+requires-python = ">=3.10,<3.15"
classifiers = [
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
- "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Operating System :: OS Independent",
]
dependencies = [
- "pipelex[mistralai,anthropic,google,google-genai,bedrock,fal]==0.17.3",
+ "pipelex[mistralai,anthropic,google,google-genai,bedrock,fal]>=0.18.1"
]
[tool.setuptools]
@@ -25,19 +26,19 @@ include-package-data = true
[project.optional-dependencies]
dev = [
- "boto3-stubs>=1.35.24",
- "mypy>=1.11.2",
- "pyright>=1.1.405",
- "pytest>=9.0.1",
- "pytest-sugar>=1.0.0",
- "pytest_asyncio>=0.24.0",
- "ruff>=0.6.8",
- "types-aioboto3[bedrock,bedrock-runtime]>=13.4.0",
- "types-aiofiles>=24.1.0.20240626",
- "types-markdown>=3.6.0.20240316",
- "types-networkx>=3.3.0.20241020",
- "types-openpyxl>=3.1.5.20250306",
- "types-PyYAML>=6.0.12.20250326",
+ "boto3-stubs>=1.35.24",
+ "mypy>=1.11.2",
+ "pyright>=1.1.405",
+ "pytest>=9.0.1",
+ "pytest-sugar>=1.0.0",
+ "pytest_asyncio>=0.24.0",
+ "ruff>=0.6.8",
+ "types-aioboto3[bedrock,bedrock-runtime]>=13.4.0",
+ "types-aiofiles>=24.1.0.20240626",
+ "types-markdown>=3.6.0.20240316",
+ "types-networkx>=3.3.0.20241020",
+ "types-openpyxl>=3.1.5.20250306",
+ "types-PyYAML>=6.0.12.20250326",
]
[project.urls]
@@ -160,43 +161,43 @@ typeCheckingMode = "strict"
[tool.pytest]
minversion = "9.0"
addopts = [
- "--import-mode=importlib",
- "-ra",
- "-m",
- "not (inference or llm or img_gen or extract or needs_output or pipelex_api)",
+ "--import-mode=importlib",
+ "-ra",
+ "-m",
+ "not (inference or llm or img_gen or extract or needs_output or pipelex_api)",
]
asyncio_default_fixture_loop_scope = "session"
xfail_strict = true
filterwarnings = [
- "ignore:Support for class-based `config` is deprecated:DeprecationWarning",
- "ignore:websockets.*is deprecated:DeprecationWarning",
- "ignore:typing\\.io is deprecated:DeprecationWarning",
- "ignore:typing\\.re is deprecated:DeprecationWarning",
- "ignore:.*has been moved to cryptography.*",
- "ignore:Use.*Types instead",
+ "ignore:Support for class-based `config` is deprecated:DeprecationWarning",
+ "ignore:websockets.*is deprecated:DeprecationWarning",
+ "ignore:typing\\.io is deprecated:DeprecationWarning",
+ "ignore:typing\\.re is deprecated:DeprecationWarning",
+ "ignore:.*has been moved to cryptography.*",
+ "ignore:Use.*Types instead",
]
markers = [
- "needs_output: tests that need output to be displayed",
- "inference: slow and costly due to inference calls",
- "llm: slow and costly due to llm inference calls",
- "img_gen: slow and costly due to image generation inference calls",
- "extract: slow and costly due to doc extraction inference calls",
- "gha_disabled: tests that should not run in GitHub Actions",
- "codex_disabled: tests that should not run in Codex",
- "dry_runnable: tests that can be run in dry-run mode",
- "pipelex_api: tests that require access to the Pipelex API",
+ "needs_output: tests that need output to be displayed",
+ "inference: slow and costly due to inference calls",
+ "llm: slow and costly due to llm inference calls",
+ "img_gen: slow and costly due to image generation inference calls",
+ "extract: slow and costly due to doc extraction inference calls",
+ "gha_disabled: tests that should not run in GitHub Actions",
+ "codex_disabled: tests that should not run in Codex",
+ "dry_runnable: tests that can be run in dry-run mode",
+ "pipelex_api: tests that require access to the Pipelex API",
]
[tool.ruff]
exclude = [
- ".cursor",
- ".git",
- ".github",
- ".mypy_cache",
- ".ruff_cache",
- ".venv",
- ".vscode",
- "trigger_pipeline",
+ ".cursor",
+ ".git",
+ ".github",
+ ".mypy_cache",
+ ".ruff_cache",
+ ".venv",
+ ".vscode",
+ "trigger_pipeline",
]
line-length = 150
target-version = "py311"
@@ -207,18 +208,18 @@ target-version = "py311"
ignore = ["F401"]
external = ["F401"]
select = [
- "E4",
- "E7",
- "E9",
- "F",
- "A001",
- "A002",
- "A003",
- "RUF008",
- "RUF009",
- "RUF012",
- "RUF013",
- "RUF100",
- "E501",
- "I",
+ "E4",
+ "E7",
+ "E9",
+ "F",
+ "A001",
+ "A002",
+ "A003",
+ "RUF008",
+ "RUF009",
+ "RUF012",
+ "RUF013",
+ "RUF100",
+ "E501",
+ "I",
]
diff --git a/tests/e2e/test_my_project.py b/tests/e2e/test_my_project.py
index 8838acc..d4c69c6 100644
--- a/tests/e2e/test_my_project.py
+++ b/tests/e2e/test_my_project.py
@@ -1,3 +1,5 @@
+import runpy
+
import pytest
@@ -5,4 +7,4 @@
@pytest.mark.inference
class TestMyProject:
def test_hello_world(self):
- import my_project.hello_world # noqa: F401
+ runpy.run_path("my_project/hello_world.py", run_name="__main__")
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 028da5f..ab98a37 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -2,6 +2,8 @@
import pipelex.pipelex
import pytest
from pipelex.system.configuration.config_check import check_is_initialized
+from pipelex.test_extras.shared_pytest_plugins import is_inference_disabled_in_pipelex
+from pytest import FixtureRequest
from rich import print
from rich.console import Console
from rich.traceback import Traceback
@@ -15,11 +17,13 @@ def check_pipelex_initialized():
@pytest.fixture(scope="module", autouse=True)
-def reset_pipelex_config_fixture():
+def reset_pipelex_config_fixture(request: FixtureRequest):
# Code to run before each test
print("\n[magenta]pipelex setup[/magenta]")
try:
- pipelex_instance = pipelex.pipelex.Pipelex.make()
+ pipelex_instance = pipelex.pipelex.Pipelex.make(
+ disable_inference=is_inference_disabled_in_pipelex(request),
+ )
except Exception as exc:
Console().print(Traceback())
pytest.exit(f"Critical Pipelex setup error: {exc}")
diff --git a/uv.lock b/uv.lock
index f5923b6..ac82add 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,6 +1,6 @@
version = 1
revision = 3
-requires-python = ">=3.10"
+requires-python = ">=3.10, <3.15"
resolution-markers = [
"python_full_version >= '3.11'",
"python_full_version < '3.11'",
@@ -213,7 +213,7 @@ wheels = [
[[package]]
name = "anthropic"
-version = "0.75.0"
+version = "0.79.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -225,9 +225,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/b1/91aea3f8fd180d01d133d931a167a78a3737b3fd39ccef2ae8d6619c24fd/anthropic-0.79.0.tar.gz", hash = "sha256:8707aafb3b1176ed6c13e2b1c9fb3efddce90d17aee5d8b83a86c70dcdcca871", size = 509825, upload-time = "2026-02-07T18:06:18.388Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b2/cc0b8e874a18d7da50b0fda8c99e4ac123f23bf47b471827c5f6f3e4a767/anthropic-0.79.0-py3-none-any.whl", hash = "sha256:04cbd473b6bbda4ca2e41dd670fe2f829a911530f01697d0a1e37321eb75f3cf", size = 405918, upload-time = "2026-02-07T18:06:20.246Z" },
]
[[package]]
@@ -343,13 +343,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/b6/f624f5143bc5f7a66b79fc40e67b30b9584471a6143062af420bc83ae887/botocore_stubs-1.41.6-py3-none-any.whl", hash = "sha256:859e4147b5b14dc5eb64fc84fa02424839354368a0fea41da52c7a1d06427e37", size = 66748, upload-time = "2025-12-01T04:14:12.833Z" },
]
+[[package]]
+name = "cached-property"
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/4b/3d870836119dbe9a5e3c9a61af8cc1a8b69d75aea564572e385882d5aefb/cached_property-2.0.1.tar.gz", hash = "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", size = 10574, upload-time = "2024-10-25T15:43:55.667Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/0e/7d8225aab3bc1a0f5811f8e1b557aa034ac04bdf641925b30d3caf586b28/cached_property-2.0.1-py3-none-any.whl", hash = "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb", size = 7428, upload-time = "2024-10-25T15:43:54.711Z" },
+]
+
[[package]]
name = "cachetools"
-version = "6.2.2"
+version = "6.2.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" },
]
[[package]]
@@ -518,11 +527,11 @@ wheels = [
[[package]]
name = "eval-type-backport"
-version = "0.3.0"
+version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/51/23/079e39571d6dd8d90d7a369ecb55ad766efb6bae4e77389629e14458c280/eval_type_backport-0.3.0.tar.gz", hash = "sha256:1638210401e184ff17f877e9a2fa076b60b5838790f4532a21761cc2be67aea1", size = 9272, upload-time = "2025-11-13T20:56:50.845Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/19/d8/2a1c638d9e0aa7e269269a1a1bf423ddd94267f1a01bbe3ad03432b67dd4/eval_type_backport-0.3.0-py3-none-any.whl", hash = "sha256:975a10a0fe333c8b6260d7fdb637698c9a16c3a9e3b6eb943fee6a6f67a37fe8", size = 6061, upload-time = "2025-11-13T20:56:49.499Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" },
]
[[package]]
@@ -551,15 +560,17 @@ wheels = [
[[package]]
name = "fal-client"
-version = "0.9.1"
+version = "0.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "httpx-sse" },
+ { name = "msgpack" },
+ { name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b8/a1/98ab1cea4c2424ee612292bc92b07905e1a15a05584f6c263cde38e6a3a2/fal_client-0.9.1.tar.gz", hash = "sha256:c8f7f88f79c4b4c4f069be9f571be924dc7c4a6bf07c252fe0b75f3c46c8d66d", size = 17085, upload-time = "2025-11-13T18:15:09.911Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/58/223a48a4d0538e73c292086f284480be42ada14223d8067432bf5eeb7aaf/fal_client-0.11.0.tar.gz", hash = "sha256:350f8cd73f5035ae1e2678ce46beb7f9f43d0a96d43586b02cd88fd973e656e1", size = 21823, upload-time = "2026-01-05T15:22:33.606Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/57/775821a71459f2b83bbaa59452a4b1e4772f7c770de88a6f591c9d43c7c8/fal_client-0.9.1-py3-none-any.whl", hash = "sha256:8eba86c947299852c8306f685eee883ce01856543bf4344b87f65abd4b7d7622", size = 11157, upload-time = "2025-11-13T18:15:08.528Z" },
+ { url = "https://files.pythonhosted.org/packages/64/67/7dd4c4b2b375cc3f072ec7bde528d7c8bafb3bcdd7df1e0758d97366a1c8/fal_client-0.11.0-py3-none-any.whl", hash = "sha256:dc4f528299aa9aeefad949e0bed0183fb78c19f0a1b7e7f85d95c859f2f694d7", size = 14771, upload-time = "2026-01-05T15:22:32.116Z" },
]
[[package]]
@@ -715,6 +726,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/a4/7319a2a8add4cc352be9e3efeff5e2aacee917c85ca2fa1647e29089983c/google_auth-2.41.1-py2.py3-none-any.whl", hash = "sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d", size = 221302, upload-time = "2025-09-30T22:51:24.212Z" },
]
+[package.optional-dependencies]
+requests = [
+ { name = "requests" },
+]
+
[[package]]
name = "google-auth-oauthlib"
version = "1.2.3"
@@ -730,21 +746,35 @@ wheels = [
[[package]]
name = "google-genai"
-version = "1.52.0"
+version = "1.55.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
- { name = "google-auth" },
+ { name = "distro" },
+ { name = "google-auth", extra = ["requests"] },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "requests" },
+ { name = "sniffio" },
{ name = "tenacity" },
{ name = "typing-extensions" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/09/4e/0ad8585d05312074bb69711b2d81cfed69ce0ae441913d57bf169bed20a7/google_genai-1.52.0.tar.gz", hash = "sha256:a74e8a4b3025f23aa98d6a0f84783119012ca6c336fd68f73c5d2b11465d7fc5", size = 258743, upload-time = "2025-11-21T02:18:55.742Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/7c/19b59750592702305ae211905985ec8ab56f34270af4a159fba5f0214846/google_genai-1.55.0.tar.gz", hash = "sha256:ae9f1318fedb05c7c1b671a4148724751201e8908a87568364a309804064d986", size = 477615, upload-time = "2025-12-11T02:49:28.624Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/66/03f663e7bca7abe9ccfebe6cb3fe7da9a118fd723a5abb278d6117e7990e/google_genai-1.52.0-py3-none-any.whl", hash = "sha256:c8352b9f065ae14b9322b949c7debab8562982f03bf71d44130cd2b798c20743", size = 261219, upload-time = "2025-11-21T02:18:54.515Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/86/a5a8e32b2d40b30b5fb20e7b8113fafd1e38befa4d1801abd5ce6991065a/google_genai-1.55.0-py3-none-any.whl", hash = "sha256:98c422762b5ff6e16b8d9a1e4938e8e0ad910392a5422e47f5301498d7f373a1", size = 703389, upload-time = "2025-12-11T02:49:27.105Z" },
+]
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.72.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" },
]
[[package]]
@@ -811,6 +841,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
+[[package]]
+name = "importlib-metadata"
+version = "8.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
+]
+
[[package]]
name = "iniconfig"
version = "2.3.0"
@@ -851,6 +893,15 @@ google-genai = [
{ name = "jsonref" },
]
+[[package]]
+name = "invoke"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" },
+]
+
[[package]]
name = "jinja2"
version = "3.1.6"
@@ -975,15 +1026,6 @@ version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/d5/40b617ee19d2d79f606ed37f8a81e51158f126d2af67270c68f2b47ae0d5/json2html-1.3.0.tar.gz", hash = "sha256:8951a53662ae9cfd812685facdba693fc950ffc1c1fd1a8a2d3cf4c34600689c", size = 6977, upload-time = "2019-07-03T20:50:03.023Z" }
-[[package]]
-name = "jsonpath-python"
-version = "1.1.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b8/bf/626a72f2d093c5eb4f4de55b443714afa7231beeae40d4a1c69b5c5aa4d1/jsonpath_python-1.1.4.tar.gz", hash = "sha256:bb3e13854e4807c078a1503ae2d87c211b8bff4d9b40b6455ed583b3b50a7fdd", size = 84766, upload-time = "2025-11-25T12:08:39.521Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ee/bc/52e5bf0d9839e082b976c19afcab7561d0d719c7627483bf5dc251d27eed/jsonpath_python-1.1.4-py3-none-any.whl", hash = "sha256:8700cb8610c44da6e5e9bff50232779c44bf7dc5bc62662d49319ee746898442", size = 12687, upload-time = "2025-11-25T12:08:38.453Z" },
-]
-
[[package]]
name = "jsonref"
version = "1.1.0"
@@ -1195,19 +1237,103 @@ wheels = [
[[package]]
name = "mistralai"
-version = "1.5.2"
+version = "1.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "eval-type-backport" },
{ name = "httpx" },
- { name = "jsonpath-python" },
+ { name = "invoke" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-proto-http" },
+ { name = "opentelemetry-sdk" },
+ { name = "opentelemetry-semantic-conventions" },
{ name = "pydantic" },
{ name = "python-dateutil" },
- { name = "typing-inspect" },
+ { name = "pyyaml" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a5/6f/6e1755c7ce73c5841ee45702770e62164299ef54818db01be1b91ca263fa/mistralai-1.12.1.tar.gz", hash = "sha256:8d8637100f7ae06c31cccb9407b1f0cd7c96005a881e7221077959577c3b4d4d", size = 242499, upload-time = "2026-02-11T09:18:42.734Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/f0/81a2303a8c4cc75003c96dfacc5bd4731b2f8c1490cad6cddec1825f6d57/mistralai-1.12.1-py3-none-any.whl", hash = "sha256:045adccc3526016c951bacf8b1ee73355083b9d6a36370fce0149039ab386d56", size = 500601, upload-time = "2026-02-11T09:18:41.387Z" },
+]
+
+[[package]]
+name = "msgpack"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
+ { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
+ { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
+ { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
+ { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
+ { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
+ { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
+ { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
+ { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
+ { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
+ { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
+ { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
+ { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
+ { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
+ { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
+ { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
+]
+
+[[package]]
+name = "mthds"
+version = "0.0.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-strenum", marker = "python_full_version < '3.11'" },
+ { name = "httpx" },
+ { name = "pydantic" },
+ { name = "semantic-version" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "tomlkit" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/53/8d/88b7c48878864f37c554a131d37352a4ed0ea3918df3e8cb625407ff374a/mistralai-1.5.2.tar.gz", hash = "sha256:f39e6e51e8939aac2602e4badcb18712cbee2df33d86100c559333e609b92d17", size = 133473, upload-time = "2025-03-19T18:40:29.617Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/ad/4effa77d1b8b49a2b8b761f401336d0d145277d7cdc796fab67212993ea6/mthds-0.0.6.tar.gz", hash = "sha256:82e425e3b9dd6c0af1faf016cd95c26e54cf0c01129b48c9fb8da27cccd39043", size = 110295, upload-time = "2026-02-25T18:08:59.83Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/97/5b428225ca4524b9722c8e1b2812c35f958ec5bb6a58c274c6c07a136da8/mistralai-1.5.2-py3-none-any.whl", hash = "sha256:5b1112acebbcad1afd7732ce0bd60614975b64999801c555c54768ac41f506ae", size = 278149, upload-time = "2025-03-19T18:40:28.232Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/c7/3bac46a06a59eb0aba9625ebef1142a1e796265c1fb9deb29e01a2255004/mthds-0.0.6-py3-none-any.whl", hash = "sha256:7083633765269e3ab8f62ae5c489fe70d4d6de526e19e86bc9ebfa8d6d76e22d", size = 48141, upload-time = "2026-02-25T18:08:58.604Z" },
]
[[package]]
@@ -1350,7 +1476,7 @@ wheels = [
[[package]]
name = "my-project"
-version = "0.6.7"
+version = "0.7.0"
source = { virtual = "." }
dependencies = [
{ name = "pipelex", extra = ["anthropic", "bedrock", "fal", "google", "google-genai", "mistralai"] },
@@ -1377,7 +1503,7 @@ dev = [
requires-dist = [
{ name = "boto3-stubs", marker = "extra == 'dev'", specifier = ">=1.35.24" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11.2" },
- { name = "pipelex", extras = ["mistralai", "anthropic", "google", "google-genai", "bedrock", "fal"], specifier = "==0.17.3" },
+ { name = "pipelex", extras = ["mistralai", "anthropic", "google", "google-genai", "bedrock", "fal"], specifier = ">=0.18.1" },
{ name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.405" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.1" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
@@ -1657,6 +1783,88 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" },
]
+[[package]]
+name = "opentelemetry-api"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-common"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-proto" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-http"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "googleapis-common-protos" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-proto-common" },
+ { name = "opentelemetry-proto" },
+ { name = "opentelemetry-sdk" },
+ { name = "requests" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" },
+]
+
+[[package]]
+name = "opentelemetry-proto"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" },
+]
+
+[[package]]
+name = "opentelemetry-sdk"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" },
+]
+
+[[package]]
+name = "opentelemetry-semantic-conventions"
+version = "0.59b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" },
+]
+
[[package]]
name = "packaging"
version = "25.0"
@@ -1775,7 +1983,7 @@ wheels = [
[[package]]
name = "pipelex"
-version = "0.17.3"
+version = "0.18.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1787,27 +1995,35 @@ dependencies = [
{ name = "json2html" },
{ name = "kajson" },
{ name = "markdown" },
+ { name = "mthds" },
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "openai" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-proto-http" },
+ { name = "opentelemetry-sdk" },
+ { name = "opentelemetry-semantic-conventions" },
{ name = "pillow" },
+ { name = "pipelex-tools" },
{ name = "polyfactory" },
+ { name = "portkey-ai" },
{ name = "posthog" },
{ name = "pydantic" },
{ name = "pypdfium2" },
{ name = "python-dotenv" },
{ name = "pyyaml" },
+ { name = "reportlab" },
{ name = "rich" },
+ { name = "semantic-version" },
{ name = "shortuuid" },
{ name = "tomli" },
{ name = "tomlkit" },
{ name = "typer" },
{ name = "typing-extensions" },
- { name = "yattag" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/33/69/98c518fc203b3096bf163124d3eeae8b15f8608c28e4292b977bef99bbc1/pipelex-0.17.3.tar.gz", hash = "sha256:9be425de4faee01d1039f8e97c55a2beb66e91b005be13323709f5843b917d64", size = 368295, upload-time = "2025-12-01T13:45:52.849Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/75/48/e310446b4ab74cf7d96ee3b6bf4ef90345c35ab1bd42c45cf678ff907063/pipelex-0.18.1.tar.gz", hash = "sha256:8dc6e41d94051c027dde1dc0988db1e242e6bfa4a172752b5c31b24f6271caee", size = 691444, upload-time = "2026-02-25T23:39:03.738Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4c/82/1ad4436608bd1c33077d9d0905a0d93640b9c47b310f1c19ae0f96c18224/pipelex-0.17.3-py3-none-any.whl", hash = "sha256:5dda80b9adcfd13433992e2dfd96f484c6339e14511388cb66adf5e22c28cafa", size = 567412, upload-time = "2025-12-01T13:45:51.07Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/66/cbac416125efe082151ff0f6cd8598dfdb489a03b11299553310857341a2/pipelex-0.18.1-py3-none-any.whl", hash = "sha256:c474eb422c3348e21ad618304228d49204da1081bf7b4751073b0be7bd79985f", size = 1015063, upload-time = "2026-02-25T23:39:01.452Z" },
]
[package.optional-dependencies]
@@ -1832,6 +2048,21 @@ mistralai = [
{ name = "mistralai" },
]
+[[package]]
+name = "pipelex-tools"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/06/b0b78881565093fd5c662dbb128b9ed8e090403c1d6110357e1ce8ed9ea2/pipelex_tools-0.2.0.tar.gz", hash = "sha256:8dc6b866aae05dd2d59ac80f5f21ab31c5477b9e09400d86fc2a1b2143868266", size = 143877, upload-time = "2026-02-21T21:04:34.277Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/54/865a714ff7880f148acc762686a8281844e5620af38a307fdf535dc4fb94/pipelex_tools-0.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0d3cac6f46be310a66d242b9ca42a237042661c77ed8de1c4116c2b066f9e977", size = 5077220, upload-time = "2026-02-21T21:04:20.644Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/47/09abdef9d35cadbda5d3909fe45546282f87d48ad0c876f757f4dc5a847a/pipelex_tools-0.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c908648a72b96bef1a68c2ff2ba407e39757e8843f45e1e799d11fab78c405b", size = 4826425, upload-time = "2026-02-21T21:04:22.403Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/35/2dea7a7c615e349807b5aa56604b83e5ed4aa2f7ba6b3249fab7d1d23582/pipelex_tools-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f88bffd036d5813cd2684a6cc6fba46852e25614a3723faa8e5ef16fb0dd28", size = 4965360, upload-time = "2026-02-21T21:04:24.959Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/59/2dacf08122e0302b7bf07ea4617dcbf82946445b8b5fcf137443fde3bd20/pipelex_tools-0.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1cc46c25659c6f42ec088a13fa685f1b49f836b22a7300dc807573f74b3af7f", size = 5216110, upload-time = "2026-02-21T21:04:26.904Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/09/89f87484e575205632d3f97c7f22dd3080473ab34c50801d6f12a031028d/pipelex_tools-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:283210d5f24cbc945a80b3127fc42abc7cf3dbaa18b269d68561bc6a528e1b14", size = 5222398, upload-time = "2026-02-21T21:04:29.134Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/31/6777aa89f9ef096e963b890b748150d4085351edb013edc97b57256488e6/pipelex_tools-0.2.0-py3-none-win32.whl", hash = "sha256:f39eb059f27f2426c3c3dbc990e9d1c13ab66cee3974a1e9d513b4cff24fd7b2", size = 4598419, upload-time = "2026-02-21T21:04:30.819Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/c9/4a3c74f29a5a73f4ca3ea79943212472db5803f13e1de324da5d706f0e01/pipelex_tools-0.2.0-py3-none-win_amd64.whl", hash = "sha256:94488f11c1700bd682cb9da981ee7e7569bc1fa601544ae658254592fc493f56", size = 5393983, upload-time = "2026-02-21T21:04:32.558Z" },
+]
+
[[package]]
name = "platformdirs"
version = "4.5.0"
@@ -1863,6 +2094,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/7c/535646d75a1c510065169ea65693613c7a6bc64491bea13e7dad4f028ff3/polyfactory-3.1.0-py3-none-any.whl", hash = "sha256:78171232342c25906d542513c9f00ebf41eadec2c67b498490a577024dd7e867", size = 61836, upload-time = "2025-11-25T08:10:14.893Z" },
]
+[[package]]
+name = "portkey-ai"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "cached-property" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "tqdm" },
+ { name = "types-requests" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d4/8a/f5bbaab806ad61d9959cb7c88c639200feacac1b2ba7b455b97a2f216e7c/portkey_ai-2.1.0.tar.gz", hash = "sha256:c2558041c568eef8528737978089301cb9be056f166a683251831cbfa6a623cb", size = 567417, upload-time = "2025-11-25T20:32:43.102Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/11/c585b90ac842027e5f4f7f7cee72d3197f58ff24b6d7c5f1243aa8fa96be/portkey_ai-2.1.0-py3-none-any.whl", hash = "sha256:2166033f8e198745947fee5321d0bbcfb005afc35468bd5a948fa83dc16b6767", size = 1181622, upload-time = "2025-11-25T20:32:41.185Z" },
+]
+
[[package]]
name = "posthog"
version = "7.0.1"
@@ -2010,6 +2262,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
]
+[[package]]
+name = "protobuf"
+version = "6.33.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" },
+ { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" },
+ { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" },
+]
+
[[package]]
name = "pyasn1"
version = "0.6.1"
@@ -2175,22 +2442,22 @@ wheels = [
[[package]]
name = "pypdfium2"
-version = "5.1.0"
+version = "4.30.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1c/87/56782107fa242137b77ccddc30519bbb33e7a9eed9da9649d9db45db2c64/pypdfium2-5.1.0.tar.gz", hash = "sha256:46335ca30a1584b804a6824da84d2e846b4b954bdfc342d035b7bf15ed9a14e5", size = 270104, upload-time = "2025-11-23T13:36:52.589Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/14/838b3ba247a0ba92e4df5d23f2bea9478edcfd72b78a39d6ca36ccd84ad2/pypdfium2-4.30.0.tar.gz", hash = "sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16", size = 140239, upload-time = "2024-05-09T18:33:17.552Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1a/d7/46ce255322cd29f0db3772667a0da3db8ed137e1e9b9aa306ac5691765b3/pypdfium2-5.1.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f3dde94d320d582d3c20255b600f1e7e03261bfdea139b7064b54126fc3db4e2", size = 2817789, upload-time = "2025-11-23T13:36:31.423Z" },
- { url = "https://files.pythonhosted.org/packages/19/a5/4ad3c1b336fdc2b7a88d835c56bcd64ce60d4a95d1a9eaafc44f853da582/pypdfium2-5.1.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:dee09b7a3ab1860a17decc97c179a5aaba5a74b2780d53c91daa18d742945892", size = 2940861, upload-time = "2025-11-23T13:36:33.519Z" },
- { url = "https://files.pythonhosted.org/packages/19/93/d13ca66d5e075d7e27736c51c15955cdd3266ac0a8327613c3c520d43693/pypdfium2-5.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1757d6470cbf5b8d1c825350df2ccd79fd0bfcf5753ff566fd02153a486014b1", size = 2980933, upload-time = "2025-11-23T13:36:35.283Z" },
- { url = "https://files.pythonhosted.org/packages/a2/7c/02744ef9e0363af08f9ed47c0e603ef8713e02d4a48492c76d5bf36f65c3/pypdfium2-5.1.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad18e95497423f88b33f2976cb78c27f0bd6ef4b4bf340c901f5f28a234c4f06", size = 2762960, upload-time = "2025-11-23T13:36:37.033Z" },
- { url = "https://files.pythonhosted.org/packages/89/26/f0abcfccb99b0a5c4451b70b0e72ccb7c27387931af01eae982870272202/pypdfium2-5.1.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2faee2f4fbd5bd33dd77c07d15ccaa6687562d883a54c4beb8329ebaee615b7d", size = 3060522, upload-time = "2025-11-23T13:36:38.835Z" },
- { url = "https://files.pythonhosted.org/packages/2f/74/92f508e71178aa85de32454762f84d6f9cef35c468caab3e0f1041dae464/pypdfium2-5.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d688372df169a9dad606c1e5ad34b6e0e6b820f1e0d540b4780711600a7bf8dd", size = 2995178, upload-time = "2025-11-23T13:36:40.319Z" },
- { url = "https://files.pythonhosted.org/packages/94/9f/91ca099ea64b24e19ef05da72e33d0ef0840e104d89cbdcb618da12629b5/pypdfium2-5.1.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cfecd2b20f1c05027aaa2af6bfbcc2835b4c8f6455155b0dc2800ec6a2051965", size = 6321704, upload-time = "2025-11-23T13:36:42.177Z" },
- { url = "https://files.pythonhosted.org/packages/e0/4b/5628cfda9f534b3acc1e2cf50f9e9582cd9cfd86cf2ce718da229de6e709/pypdfium2-5.1.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5698de8e6d662f1b2cdff5cb62e6f0ee79ffaaa13e282251854cbc64cf712449", size = 6329892, upload-time = "2025-11-23T13:36:43.757Z" },
- { url = "https://files.pythonhosted.org/packages/c5/25/5d2db765f8f82129d75ea2883ed26af3d1a64d8daaa20a11005ac681e2c3/pypdfium2-5.1.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd73093fbb1710ea1164cdf27583363e1b663b8cc22d555c84af0ee1af50c7", size = 6409889, upload-time = "2025-11-23T13:36:45.387Z" },
- { url = "https://files.pythonhosted.org/packages/89/d3/135ed8ca46044cd5005cd104ead13bea417777afa65d7af5a710eb68d340/pypdfium2-5.1.0-py3-none-win32.whl", hash = "sha256:11d319cd2e5f71cdc3d68e8a79142b559a0edbcc16fe31d4036fcfc45f0e9ed8", size = 2991546, upload-time = "2025-11-23T13:36:47.373Z" },
- { url = "https://files.pythonhosted.org/packages/52/8f/884a1b2fd7c747a98e9b4c95097c08b39d042a88837ac72f2945a7f6162c/pypdfium2-5.1.0-py3-none-win_amd64.whl", hash = "sha256:4725f347a8c9ff011a7035d8267ee25912ab1b946034ba0b57f3cca89de8847a", size = 3100176, upload-time = "2025-11-23T13:36:49.234Z" },
- { url = "https://files.pythonhosted.org/packages/d7/5c/72448636ea0ccd44878f77bb5d59a2c967a54eec806ee2e0d894ef0d2434/pypdfium2-5.1.0-py3-none-win_arm64.whl", hash = "sha256:47c5593f7eb6ae0f1e5a940d712d733ede580f09ca91de6c3f89611848695c0f", size = 2941500, upload-time = "2025-11-23T13:36:50.69Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/9a/c8ff5cc352c1b60b0b97642ae734f51edbab6e28b45b4fcdfe5306ee3c83/pypdfium2-4.30.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab", size = 2837254, upload-time = "2024-05-09T18:32:48.653Z" },
+ { url = "https://files.pythonhosted.org/packages/21/8b/27d4d5409f3c76b985f4ee4afe147b606594411e15ac4dc1c3363c9a9810/pypdfium2-4.30.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de", size = 2707624, upload-time = "2024-05-09T18:32:51.458Z" },
+ { url = "https://files.pythonhosted.org/packages/11/63/28a73ca17c24b41a205d658e177d68e198d7dde65a8c99c821d231b6ee3d/pypdfium2-4.30.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854", size = 2793126, upload-time = "2024-05-09T18:32:53.581Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/96/53b3ebf0955edbd02ac6da16a818ecc65c939e98fdeb4e0958362bd385c8/pypdfium2-4.30.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2", size = 2591077, upload-time = "2024-05-09T18:32:55.99Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ee/0394e56e7cab8b5b21f744d988400948ef71a9a892cbeb0b200d324ab2c7/pypdfium2-4.30.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad", size = 2864431, upload-time = "2024-05-09T18:32:57.911Z" },
+ { url = "https://files.pythonhosted.org/packages/65/cd/3f1edf20a0ef4a212a5e20a5900e64942c5a374473671ac0780eaa08ea80/pypdfium2-4.30.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f", size = 2812008, upload-time = "2024-05-09T18:32:59.886Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/91/2d517db61845698f41a2a974de90762e50faeb529201c6b3574935969045/pypdfium2-4.30.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163", size = 6181543, upload-time = "2024-05-09T18:33:02.597Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/c4/ed1315143a7a84b2c7616569dfb472473968d628f17c231c39e29ae9d780/pypdfium2-4.30.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e", size = 6175911, upload-time = "2024-05-09T18:33:05.376Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/c4/9e62d03f414e0e3051c56d5943c3bf42aa9608ede4e19dc96438364e9e03/pypdfium2-4.30.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be", size = 6267430, upload-time = "2024-05-09T18:33:08.067Z" },
+ { url = "https://files.pythonhosted.org/packages/90/47/eda4904f715fb98561e34012826e883816945934a851745570521ec89520/pypdfium2-4.30.0-py3-none-win32.whl", hash = "sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e", size = 2775951, upload-time = "2024-05-09T18:33:10.567Z" },
+ { url = "https://files.pythonhosted.org/packages/25/bd/56d9ec6b9f0fc4e0d95288759f3179f0fcd34b1a1526b75673d2f6d5196f/pypdfium2-4.30.0-py3-none-win_amd64.whl", hash = "sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c", size = 2892098, upload-time = "2024-05-09T18:33:13.107Z" },
+ { url = "https://files.pythonhosted.org/packages/be/7a/097801205b991bc3115e8af1edb850d30aeaf0118520b016354cf5ccd3f6/pypdfium2-4.30.0-py3-none-win_arm64.whl", hash = "sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29", size = 2752118, upload-time = "2024-05-09T18:33:15.489Z" },
]
[[package]]
@@ -2336,6 +2603,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
+[[package]]
+name = "reportlab"
+version = "4.4.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "charset-normalizer" },
+ { name = "pillow" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/57/28bfbf0a775b618b6e4d854ef8dd3f5c8988e5d614d8898703502a35f61c/reportlab-4.4.10.tar.gz", hash = "sha256:5cbbb34ac3546039d0086deb2938cdec06b12da3cdb836e813258eb33cd28487", size = 3714962, upload-time = "2026-02-12T10:45:21.325Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/2e/e1798b8b248e1517e74c6cdf10dd6edd485044e7edf46b5f11ffcc5a0add/reportlab-4.4.10-py3-none-any.whl", hash = "sha256:5abc815746ae2bc44e7ff25db96814f921349ca814c992c7eac3c26029bf7c24", size = 1955400, upload-time = "2026-02-12T10:45:18.828Z" },
+]
+
[[package]]
name = "requests"
version = "2.32.5"
@@ -2427,6 +2707,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
]
+[[package]]
+name = "semantic-version"
+version = "2.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" },
+]
+
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -2705,6 +2994,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" },
]
+[[package]]
+name = "types-requests"
+version = "2.32.4.20250913"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" },
+]
+
[[package]]
name = "types-s3transfer"
version = "0.15.0"
@@ -2723,19 +3024,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
-[[package]]
-name = "typing-inspect"
-version = "0.9.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mypy-extensions" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" },
-]
-
[[package]]
name = "typing-inspection"
version = "0.4.2"
@@ -3036,7 +3324,10 @@ wheels = [
]
[[package]]
-name = "yattag"
-version = "1.16.1"
+name = "zipp"
+version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1c/1a/d3b2a2b8f843f5e7138471c4a5c9172ef62bb41239aa4371784b7448110c/yattag-1.16.1.tar.gz", hash = "sha256:baa8f254e7ea5d3e0618281ad2ff5610e0e5360b3608e695c29bfb3b29d051f4", size = 29069, upload-time = "2024-11-02T22:38:30.443Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
+]